有一次在工作中关于commit 提交的标准引发了这样的讨论:
- 有的喜欢随时 commit 以免更改的内容丢失
- 有的习惯保证每次 commit 都是有实际意义的,功能完整
乍一看,这都有道理呀。那么这两种标准是互斥的,鱼与熊掌不可兼得吗?
Git 针对这种诉求也提供了相关的命令操作。
在 Git doc 中专门有一章来描述 Rewriting History 重写历史,在实际使用 git 的过程中,我们也经常需要修改或完善提交的记录。
比如改变 commit 的描述内容,改变提交的顺序,或者提交组合/拆分等
Note:在本地重写历史记录,也就是在提交的远程 repo 共享前。
修改最后一次提交
这个操作也是比较常见的,我们可能已经 commit 了,但是需要修改下信息;或者想更改提交内容,重新提交。
修改最近提交的信息
$ git commit --amend
执行上面命令之后,将最后一次的提交信息载入到 vim 编辑器中供修改,保存关闭后,编辑器会将更新后的提交信息写入新提交中,成为新的最后一次提交。
修改提交的实际内容
Git doc 中描述的方法不太好理解,我个人也比较习惯于下面的方案
之前我们有介绍过的一个命令,reset
$ git reset --soft HEAD^
就可以将最后一次提交的内容都恢复到暂存区了,可以继续修改再提交。
reset 使用介绍 Git 入门系列(二)- 修改管理 / 撤销操作 / 命令及区间关系
修改多个提交信息
先制造数据测试
- 新建文件 history 1.txt, history 2.txt, history 3.txt, 提交
git commit -m "init txt"
- history 1.txt 添加内容 "commit 1",执行
git commit -m "commit 1"
- history 2.txt 添加内容 "commit 2",执行
git commit -m "commit 2"
- history 3.txt 添加内容 "commit 3",执行
git commit -m "commit 3"
查看日志
$ git log --pretty=format:"%h %s" HEAD~3..HEAD
formate:76f735f commit 3
formate:5284918 commit 2
formate:20a17fa commit 1
formate:8ee6692 init txt
修改之前的提交信息
可以使用交互式变基来处理 (关于变基 rebase 基本用法 Git 进阶 - 衍合 rebase)
如果想修改近两次提交的信息,将想要修改的 最近一次提交的 父提交 作为参数传给 git rebase -i
命令,比如修改最近两次提交,那么就是 HEAD~2^
或者 HEAD~3
(对于这里的表示方式不是很理解,可参考 Git 进阶- 小贴士:祖先引用(~ or ^ ?))当然也可以直接使用 hash 值,这里对应的是 8ee6692
$ git reabse -i HEAD~3
这是一个变基命令,在 HEAD~3.. HEAD
范围内的每一个修改了提交信息的提交及其 所有后裔 都会被重写。(基于父节点)
运行后会在文本编辑器中显示出列表:
pick 20a17fa commit 1
pick 5284918 commit 2
pick 76f735f commit 3
# Rebase 8ee6692..20a17fa onto 8ee6692 (3 commands) # 这里指向了 init txt 的提交
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop = remove commit
# l, label
注意其中的倒序显示。 交互式变基给你一个它将会运行的脚本。 从指定的提交(HEAD~3
)开始,从上到下的依次重演每一个提交引入的修改。 它将最旧的而不是最新的列在上面,因为那会是第一个将要重演的。
如果想修改某一次提交,将前面的 pick 改为 edit (或者简写 s),比如想修改第二次提交
pick 20a17fa commit 1
edit 5284918 commit 2
pick 76f735f commit 3
保存退出编辑时,Git 会回到倒数第二次提交
$ git rebase -i HEAD~3
Stopped at 5284918... commit 2
You can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue
Git 会提示下一步要做什么
输入 git commit --amend
修改提交信息,
然后执行 git rebase --continue
就会自动应用于另外一个提交,如果有多处需要 edit,每一个都要重复这个步骤,直到完成。
上面也可以使用 reword(r) 脚本一气呵成,直接编辑日志,edit 会停留在
amand
处 (但是会更容易理解期实现的过程)
查看日志
$ git log --pretty=format:"%h %s" HEAD~3..HEAD
formate:daf8745 commit 3 # hash 变了
formate:0e38eec commit 2-edit # hash 变了
formate:20a17fa commit 1
formate:8ee6692 init txt
细心的朋友可能已经注意到了,最后两次提交的 hash 是有变化的,正是因为在 "commit 1" 那次提交处做了 rebase
操作,后面会重写,hash 自然就重新生成了。
上面也可以使用 reword(r) 脚本一气呵成,直接编辑日志,edit 会停留在 amand
处 (但是会更容易理解期实现的过程)
pick 20a17fa commit 1
r 0e38eec commit 2-edit
pick daf8745 commit 3
使用了 reword 直接进入了编辑器,修改下日志保存,查看
git log --pretty=formate:"%h %s"
formate:ce9b264 commit 3
formate:3cd2b51 commit 2-edit-2
formate:20a17fa commit 1
formate:8ee6692 init txt
压缩提交
在我们使用 Git 管理过程中,经常使用 commit 来细化本地的改动,但当建立一个 Merge request 的时候,通常需要将这些琐碎的 commit 整合起来,形成一次完整的提交。
通过交互式变基工具,也可以将一连串提交压缩成一个单独的提交。 在变基信息中脚本给出了有用的指令:
如果,指定 “squash(s)” 而不是 “pick” 或 “edit”,Git 将应用两者的修改并合并提交信息在一起。如果想要这两次提交变为一个提交,可以这样修改脚本:
当前日志信息
formate:ce9b264 commit 3
formate:3cd2b51 commit 2-edit-2
formate:20a17fa commit 1
formate:8ee6692 init txt
$ git rebase -i HEAD~3
进入脚本,将这两个提交合并成一个,修改脚本
pick 20a17fa commit 1
squash 3cd2b51 commit 2-edit-2
pick ce9b264 commit 3
对 squash 的描述是 可以融合到前一次提交上,这里要注意必须保留一个pick,如果将所有的 pick 都改为了 squash 那就没有合并的载体了就会报错
保存退出编辑器,可以看到合并后的信息,这个信息可以更改
# This is a combination of 2 commits.
# This is the 1st commit message:
commit 1
# This is the commit message #2:
commit 2-edit-2
保存后,这两次提交就合二为一了
git log --pretty=formate:"%h %s"
formate:e82ca01 commit 3
formate:9410928 commit 1 + 2
formate:8ee6692 init txt
删除/重新排序
交互式变基可以重新排序或者完全移除提交,同上,只要修改运行的脚本就可以了。
如果是删除就直接删除 对应的那一行;(或者将 pick 改成 drop(d) 做 remove 操作)
演示下删除吧, git rebase -i HEAD~2
进入编辑器,删除合并的那条,或者 pick 改为 drop(d)
d 9410928 commit 1 + 2
pick e82ca01 commit 3
保存退出
git log --pretty=formate:"%h %s"
formate:ce3b98b commit 3 # hash 遍历
formate:8ee6692 init txt
重新排序就按照期望更改 commit 的顺序即可,就不做演示了,如果要自己实践的话 还是要追加一个 commit,因为现在第二次commit 是创建文件,如果调换顺序之后,就是在一个不存在的文件上修改,在创建,显然不符合逻辑的。
拆分提交
目前还没有想到使用场景,遇到再继续学习
小结
通过上面的总结,针对篇首提到的问题,我们可以 小步慢跑式提交,随时 commit 保存,在真正 push 前,通过 git rebase -i
使用 squash 脚本将琐碎的提交合并成一个,也达到了commit 功能完整的目的了。
参考
Rewriting History