如何修改 Git 提交的历史时间
有时候需要回头修改已有提交的时间:开发机时区配错了,几十个提交全部偏了 8 小时;或者服务器时间没同步,提交时间跑到了未来;又或者一晚上补交的几个 commit 全挤在同一分钟里,想让它们看起来分布得自然一些。
git commit 时可以用 --date 指定时间,但对已经存在的提交,事情要绕一点——因为一个 commit 其实有两个时间。
一、关键前提:两个时间
| 名称 | 含义 | 格式占位符 | 相关环境变量 |
|---|---|---|---|
| Author Date | 作者时间,代码是什么时候写的,git log 默认显示的就是它 |
%ad |
GIT_AUTHOR_DATE |
| Committer Date | 提交时间,这个提交对象是什么时候生成的 | %cd |
GIT_COMMITTER_DATE |
正常提交时两者相同,所以平时感觉不到区别。但只要发生 rebase、cherry-pick、amend,Git 就会保留原来的 author date、刷新 committer date,两者就分开了。
用下面这条命令可以把两个时间一起看到:
1 | |
只改其中一个,会让两者不一致——git log 显示的是你想要的时间,但仓库里实际排序、以及各托管平台的时间线和贡献图可能仍按另一个走。不同平台取哪个时间的实现并不统一,与其纠结,不如两个一起改。下面所有方法都是成对设置的,原因就在这里。
时间格式推荐 ISO 8601 带时区,例如 2026-07-20T09:00:00+08:00。带上时区可以避免 Git 按本机时区去猜,跨机器操作时结果才稳定。
⚠️ 改历史会让 commit hash 全部变化,被改的提交及其后面的所有提交都会得到新的 SHA。已推送到远端的分支需要
git push --force-with-lease;如果有其他人基于该分支工作,改写前务必先沟通。另外,原本有 GPG 签名的提交在改写后签名会失效,需要重新签名(在 amend / rebase 时加-S)。
二、方法一:只改最后一次 commit
最简单的场景,直接 --amend。
PowerShell:
1 | |
Bash:
1 | |
要点:
--date只管 author date,committer date 必须靠环境变量GIT_COMMITTER_DATE,这是最容易踩的坑;--no-edit表示不修改提交信息,不加会弹出编辑器;- PowerShell 里设完环境变量记得
Remove-Item Env:...清掉,否则同一个会话里后续的提交都会被钉在这个时间上。
三、方法二:交互式 rebase(最通用)
要改最近 N 次提交,而且每次的时间都不一样,用交互式 rebase:
1 | |
编辑器里把三行的 pick 改成 edit:
1 | |
保存退出后,rebase 会在每个提交处停下来。在每一站执行一次下面的命令(每次换成对应的时间):
1 | |
重复三次,直到 rebase 结束。中途想放弃就 git rebase --abort,仓库会回到操作前的状态。
要点:
HEAD~3表示「从倒数第 3 个提交的父提交开始」,也就是要改的是最近 3 个;- 如果这三个里包含仓库的第一个 commit,
HEAD~3根本不存在,需要改用git rebase -i --root; - rebase 期间随时可以
git status看当前停在哪一站。
四、方法三:filter-branch 批量改
不想一站一站地手动敲,可以用 filter-branch 一条命令按 SHA 分派不同时间。
先拿到完整 SHA:
1 | |
再执行(在 Git Bash 下运行):
1 | |
要点:
case里必须写完整的 40 位 SHA。$GIT_COMMIT是全长的,短 hash 匹配不上,会静默地什么都不改——这是最常见的失败原因;- 末尾的
HEAD~3..HEAD限定只处理最近三个提交,不加会遍历整个历史,又慢又没必要; --env-filter的内容是一段 sh 脚本,Windows 上请用 Git Bash 执行,PowerShell 的引号规则会和它冲突;FILTER_BRANCH_SQUELCH_WARNING=1用来屏蔽 Git 关于filter-branch已不推荐的告警。
Git 官方推荐用
git-filter-repo替代filter-branch,但它需要额外安装。只改少量提交时,filter-branch已经够用了。
五、方法四:给多个 commit 设置同一个时间
如果这几个提交要用同一个时间,有更短的写法:
1 | |
要点:
--exec会对范围内每个提交执行同一条命令,所以时间必然相同,无法逐个区分;- 它走的是 rebase 的机制,但不会打开编辑器,全程无交互;
- 同样地,它不会自动改 committer date,需要提前
export GIT_COMMITTER_DATE。
六、改坏了怎么回滚
改历史之前最好先记一下当前的 commit hash,或者直接打个临时分支 git branch backup-before-rewrite。万一没记,也还有退路。
filter-branch 之后
filter-branch 会自动留下备份 ref:
1 | |
确认改动无误后再清理备份:
1 | |
注意:备份 ref 还在的时候,再次运行 filter-branch 需要加 -f,否则会报错拒绝执行。
rebase / amend 之后
查 reflog 找到操作前的位置:
1 | |
reflog 默认保留 90 天,所以只要没有手动 git gc --prune,改写前的提交都还能找回来。
七、方法对比
| 方法 | 适用场景 | 是否需要交互 | 能否每个 commit 不同时间 |
|---|---|---|---|
--amend |
只改最后一次 | 否 | — |
rebase -i |
最近 N 次,逐个改 | 是 | 是 |
filter-branch |
批量、可脚本化 | 否 | 是 |
rebase --exec |
批量设成同一时间 | 否 | 否 |
小结
三条经验:
- 两个时间一起改,
--date配GIT_COMMITTER_DATE,只改一个必然留下不一致; - 先想清楚范围,只动最后一个就用
--amend,动 N 个就限定HEAD~N..HEAD,别让工具去遍历整个历史; - 改的是共享分支就先打招呼,force push 之后别人的本地分支会对不上,这个沟通成本远高于改时间本身。