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