Fearless Git Committing

Fearless Git Committing

在平时开发中你会出现下面几种情况么?

  1. 刚提交了一个commit,发现没改对,改完再提交一个相同的commit message
  2. 不想出现上面提交多个相同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的效果:

VSCodeForkSourcetree

如果不习惯命令行,可以使用GUI Git工具,如果VSCodeGit GraphForkSourcetree。下面是用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
复制代码

image.png

或者

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相关的一些实践心得。希望对大家有帮助。

链接

分类:
开发工具
标签: