修改git提交的历史

前言

如果是团队协合中的git仓库,其历史最好不要修改;但如果是没有别人参与的个人项目,那就随便了,自己开心就好。毕竟,历史的条理与历史的真象,在哲学上就是一对矛盾。以下所有操作前,都最好把整目录备份一下,方便出错时从头再来。

修改最近一次提交及指定提交时间

git commit --amend --date "Wed, 01 Jan 2020 15:40:30 +0800"

commit 命令的 --amend 参数这个很简单,应该都用过。主要是 --date 这个参数,用来手工指定本次提交的时间(而不是默认的上次提交时间当前前系统时间),但是,它的日期格式 GIT_COMMITTER_DATE 不是汉语日常习惯的日期格式,很不方便手写,可以通过date命令做转换,如下(注意其中的三层引号是不同的,如果不熟悉bash可以死记,从外到内分别是单、反、双):

git commit --date '`date -R -d "2020/1/1 15:40:30"`'

删除文件

假设要删除passwords.txt的文件,让在整个git历史像不存在过一样

git filter-branch --tree-filter 'rm -f passwords.txt' HEAD

上面命令默认保留空提交,即删除文件后某些提交会成空提交,如清空这些空提交,可加入参数 --prune-empty

单个文件改名/单目录改名

git filter-branch --tree-filter 'if [ -f old-name.txt ]; then mv old-name.txt new-name.txt; fi' HEAD
git filter-branch --tree-filter 'if [ -d old-name ]; then mv old-name new-name; fi' HEAD

如上两行,分别是对单文件改名(old-name.txt  -> new-name.txt), 单目录改名(old-name -> new-name) ,事实上并没区别。

注意 .git/refs/original/refs/heads 目录要为空。否则会报错说"Cannot create a new backup. A previous backup already exists in refs/original/  Force overwriting the backup with -f",然而加-f参数似乎并没有用;可直接删除目录 .git/refs/original。  执行过该命令,就会生成一次.git/refs/original/refs/heads/master,大概是filter-branch的后悔药(备份目录)

如果有多个分支,可以强行将分支们合并,改过名后再滚回到合并前,这样通常更方便些:可一次完成所有分支里的改名,同时避免被一大堆各种分支搞晕。(方法来源于stackoverflow,具体链接忘了)。详细参考:Pro Git-重写历史

子目录变根目录

要把某个子目录foodir/ 下的所有文件(包含其历史),独立出去,成为一个单独的项目。其它文件自然被丢弃了,所以操作前要把整个仓库备份一下,或者在克隆的新仓库上操作。

git filter-branch --subdirectory-filter foodir -- --all

整个项目作为项目的子目录(根目录改子目录)

因为功能扩充、重构等原因,要把项目所有文件移到子目录里,git mv 不能被真正的跟踪。按如下操作,可移到newsubdir子目录中

git filter-branch --index-filter \
'git ls-files -s | sed "s-\t\"*-&newsubdir/-" |
GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
git update-index --index-info &&
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD

参看git联机文档 git filter-branch --help

合并提交(把多次commit合并成一个)

变基(rebase)相关操作,如 git rebase -i HEAD~3 更多参考git-book 重写历史

修改提交历史中的邮箱地址

git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
then
GIT_AUTHOR_NAME="Scott Chacon";
GIT_AUTHOR_EMAIL="schacon@example.com";
git commit-tree "$@";
else
git commit-tree "$@";
fi' HEAD

删除指定姓名的所有提交历史

这样作法不好,跟这人得多大的仇

git filter-branch --commit-filter '
       if [ "$GIT_AUTHOR_NAME" = "Darl McBribe" ];
       then
               skip_commit "$@";
       else
               git commit-tree "$@";
       fi' HEAD

从所有git历史版本中修改某个文件中指定内容(字串替换)

某个文件已经有多个历史版本,但里面有某些宜的内容(比如用户名密码),因此希望保留某完整的历史,而不是简单的将其所有历史中删除,而只把指定字符串替换掉。

git filter-branch --tree-filter '
  if [ -f create_table_sql.txt ];
  then
    sed -i "s/realpasswd/fackstring/g" create_table_sql.txt;
  fi' HEAD

自由的编辑(交互式,而非前面的批处理式)

  1. git rebase -i <commit> 命令打开交互式 rebase 编辑器(若要从第一次提交开始修改,可以使用 git rebase -i --root 命令)。
  2. 在编辑器中,将需要修改的提交行的前缀由 pick 改为 edit,然后保存并关闭编辑器。
  3. Git 将会逐个应用每个提交,并在需要修改的提交上停止。在停止处,可以自由的编辑,然后使用 git commit --amend 命令修改提交,或使用 git reset HEAD~ 命令取消提交并将文件恢复到未提交的状态。
  4. 如果把新近的 commit 拉到早期历史中,其日期将维持,这样插在历史记录显得很突兀;可以使用 --date 参数任意指定。不过日期格式 GIT_COMMITTER_DATE 很不方便手写,可以在shell里通过 date -R -d "2020-05-15 20:30:40" 转换,此二者是一致的,参看前面第一节中所述。
  5. 完成修改后,使用 git rebase --continue 命令继续 rebase 操作,直到所有的提交都被修改或应用。
  6. 如果在 rebase 过程中出现任何问题,可以使用 git rebase --abort 命令取消 rebase 操作并回到修改前的状态。

.EOF.