在平时开发中你会出现下面几种情况么?
- 刚提交了一个commit,发现没改对,改完再提交一个相同的commit message
- 不想出现上面提交多个相同commit message的情况,就一直不提交,直到完全开发完毕才整体提交一个commit
前者的问题自然是commit历史很乱,别人看commit message并不知道两次相同commit message的提交有什么区别,必须看diff。 后者的问题是,其他人完全不知道你的开发进度;没有push到远端的代码,就跟没有保存的文档一样,万一硬盘挂了就BBQ了。(我经历过3次硬盘挂掉,包括一个没买多久的SSD...)
本文主要介绍几个概念和一些实践经验,并不会十分详细地介绍各种Git命令和具体操作。详细操作可以在网上自行搜索。
希望通过本文给那些对初学Git、对Git有那么一丝畏惧的同学以自信、工具和方法。希望读完本文之后,你在git commit
的时候更游刃有余。
文章分为两部分
- 通过git log掌控全局情况,通过fsck和reflog知道如何自救
- 通过rebase调整commits,让提交历史更合理、美观、方便后续操作
名词约定
commits
"节点"
commit SHA1
- 远端(remote)
这里是指通过git remote add repo 添加的仓库
commit正交
这是我自己发明的一个词。多个commit正交是指这几个commit的内容互不相干。
全景图:查看提交历史
我习惯开着Fork,随时掌握仓库提交历史的当前状态。有提交会自动刷新。
git log
git log
是最基本的查看提交历史的命令。但其实它有很多可选参数。这里不做介绍。你只需将下面这个git别名加到你的.gitconfig
文件中即可:
[alias]
hist = log --graph --abbrev-commit --decorate --date=relative --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an %cn%C(reset)%C(bold yellow)%d%C(reset)' --all
复制代码
下面是在react
工程根目录下执行git hist
的效果:
tig
另一个工具是tig
(git倒过来)
下面是在react
工程根目录下执行tig --all
的效果:
VSCode
、Fork
或Sourcetree
如果不习惯命令行,可以使用GUI Git工具,如果VSCode的Git Graph
、Fork和Sourcetree。下面是用Fork
查看react
工程的效果:
这里不推崇命令或GUI,哪个方便用哪个,互不排斥。
- 比如在开发机上,命令行显然更方便快捷一些;
- 本地开发GUI更方便一些;
- GUI界面提供大部分Porcelain命令;命令行可以使用很多底层Plumbing命令(参见)
救生圈:先学会自救方法
Fearless的前提是遇到问题知道如何补救。
查找"丢失"
的commit
# 在一个空目录初始化一个仓库
git init
# commit3个空节点
git commit --allow-empty -m 'a'
git commit --allow-empty -m 'b'
git commit --allow-empty -m 'c'
复制代码
记住c
的SHA1位785152a
# reset到b节点
git reset --hard HEAD^1
# 查看历史,c节点”没了“
git hist
复制代码
git show 785152a
复制代码
我们发现c节点仍然存在,只是在git 历史中看不到
如果我没有记住这个SHA1怎么办?
git fsck --lost-found
复制代码
或者
git hist --reflog
复制代码
在Fork
中可以
笨办法:保存commit SHA1
既然只要知道SHA1就可以恢复,那么在做一些“危险”操作时可以截个git hist
的图,以备不时之需。
绝招:git reflog
git记录所有你对仓库的操作。可以通过git reflog命令查看操作历史。并且可以回退到任意时间点。
上面的例子中git reset --hard HEAD^1
之后,可以通过git reflog
查看操作历史:
找到commit: c
,然后git reset --hard HEAD@{1}
就可以恢复到该处:
用Interactive rebase
整理commit
节点
扔掉“过时”的节点
前提:我们在自己的分支独立开发,未合并到其他分支。
我们在开发的过程中经常会出现对某部分代码的反复修改,最后发现,之前提交的commit已经没有存在的意义了。这个时候我们就可以在rebase的时候,将不再需要的节点“扔掉”。
假设我们有三个commit:
提交的内容如上图所示。其中最后一个提交修改了console.log
的内容。
提交console.log(42)
已经没有意义,可以丢掉。后面会讲到如何用Interactive rebase丢掉这种commit节点。
命令行Interactive rebase
在命令行中我们也可以进行Interactive rebase:
git rebase -i HEAD^3
默认情况下git会用vi作为编辑器进行Interactive rebase的编辑:
如果你是vim党,这个操作是非常方便的。
用VS Code插件Gitlens进行Interactive rebase
VS Code中的GitLens也有类似的功能。首先需要设置git的editor为VS Code:
git config --global core.editor "code --wait"
# git config --global core.editor "vim"
复制代码
或编辑.gitconfig文件:
然后运行git rebase -i
后,就会自动打开VS Code进行rebase操作:
具体操作可参考:Interactive rebase editor from the GitLens extension
使用Fork进行Interactive Rebase
在需要整理的所有节点前面的commit上点击鼠标右键,然后选择Interactive Rebase → Rebase Interactively '<branch name>' to Here...
:
点击最下面我们要删掉的commit前的选项,然后选择drop
:
rebase之后的commit,(解决遇到的冲突)
可以看出来,console.log(42)
被删除了:
重新排序
原则:
- 非“正交”节点保持时间上的先后顺序。否则调整顺序很可能会发生冲突
- “正交”节点之间可以调整先后顺序。因为这类节点之间不会产生冲突
下面是对同一个index.js的修改,调整顺序势必产生冲突。但是你仍然可以这么做。只要你知道每次解决冲突时,应该采用哪些代码就行。在实际工作中,多人合作时最好避免这种情况。如果无法避免,最好几个人一起确定如何解决冲突。
下面调整的的是创建package.json文件,这个commit与其他三个commit是“正交”的,所以调整它不会产生冲突。
调整的结果如图:
小结: 使用Fork的Interactive Rebase功能可以方便地调整commit节点。上面只介绍了删除无用commit和调整顺序。 除此之外还可以:
- Edit:重新编辑commit message和文件
- Reword:重新编辑commit message
- Squash:将当前commit与前一个commit合并,并保留commit message
- Fixup:将当前commit与前一个commit合并,并放弃当前commit message
git rebase
注意事项
及时rebase
我们在dev分支开发时master分支也在不断更新。当多人开发,且各自dev分支和master都积累了较多commits,此时提交Merge Request,就会出现很多Merge线。下图是一个真实项目的Merge线:
建议在master有更新时,及时将dev rebase master:
整理dev上的commits:
dev2也做相同的操作,rebase master,注意这里push到origin需要force push
此时再提Merge Request
可以看到,最终的commit历史很简洁。 适时rebase master可以及时发现、解决冲突。将最终Merge出现的大量冲突分散在每次rebase中解决。 降低了最终集中解决冲突可能带来的出错的可能性。
更好的commit message
(2023/01/18 更新)
在commit message最前面添加一个提交类型的emoji,可以一目了然提交类型。并且在提交历史中快速过滤不关心的提交类型:
- init: 🎉 初始化工程
- feat: ✨ 新功能
- WIP: 🚧 Working in progress
- fix: 🐛 修复bug
- refactor:♻️ 重构代码
- ignore: 🙈 修改.gitignore文件
- chore: 🧹 日常(非代码)
- log: 🪵 添加日志
- prune: 🔥 删除代码、文件、目录
- style: 🎨 修改样式
- config: 🛠 修改配置
- doc: 📄 写注释、文档
- tag: 🔖 打tag
在Mac上,可以通过按ctrl+cmd+space
打开emoji输入框。可以通过顶部菜单的Edit→表情与符号打开:
commit message是分为两部分:brief简要信息和detail详细信息, 其中detail信息可以分为多行(一些系统支持markdown格式)。建议在详细信息中提交本次提交中比较关键的修改:
在命令行是通过两个-m
参数实现的:
git commit -m "brief" -m "detail"
复制代码
谨慎git push -f
如果rebase的分支从来没有push到远端。此时你可以随心所欲地调整commit节点。 一旦已经push到远端,那么你push -f时,其他在这个分支的开发者的对应分支,仍然指向就得commit。 这就容易出现各种难以追查的问题。如果你不得不push -f。确保只有你一个人在这个分支上工作。或者,通知所有checkout过这个分支的人,重新checkout。
提交相互正交的commit
上面我们也看到了,正交的commit调整起来不会产生冲突。所以我们在多人协作时,
- 每个人需要修改的文件之间尽可能没有重叠,避免冲突。
- 但总有一些公共文件大家都可能修改。此时可以将文件再细分为多个文件,然后在入口文件统一import。 最后可能发生冲突的文件就只有入口文件。解决冲突更方便些。
- 正交的多个commit单独提交,并保证提交的commit相对独立,以方便cherry-pick后可以正常工作 :比如,当重构代码时,你又不得不维护一个线上版本。正确组织线上版本的commit和文件拆分,可以在需要时,将commit方便地cherry-pick到重构分支。
最后
本文粗浅地介绍了日常开发中commit相关的一些实践心得。希望对大家有帮助。