25 | 玩转Git:五种提高代码提交原子性的基本操作
下载APP
关闭
渠道合作
推荐作者
25 | 玩转Git:五种提高代码提交原子性的基本操作
2019-10-21 葛俊 来自北京
《研发效率破局之道》
课程介绍
讲述:葛俊
时长18:20大小16.78M
你好,我是葛俊。今天,我们来聊一聊 Git 吧。
毫无疑问,Git 是当前最流行的代码仓管理系统,可以说是开发者的必备技能。它非常强大,使用得当的话可以大幅助力个人效能的提升。一个最直接的应用就是,可以帮助我们提升代码提交的原子性。如果一个团队的成员都能熟练使用 Git 的话,可以大大提高团队代码的模块化、可读性、可维护性,从而提高团队的研发效能。但可惜的是,现实情况中,由于 Git 比较复杂,用得好的人并不多。
所以接下来,我会通过两篇文章,与你详细讲述如何使用 Git 助力实现代码原子性。今天这篇文章,我先与你分享 Git 支持原子性的 5 种基础操作;下一篇文章,则会给你介绍 Facebook 开发人员是怎样具体应用这些基础操作去实现代码原子性的。
通过这两篇文章,我希望你能够:
了解在分布式代码仓管理系统中,如何通过对代码提交的灵活处理,实现提交的原子性;
帮你学习到 Git 的实用技巧,提高开发效率。
为什么要强调代码提交的原子性呢?这是因为它有以下 3 大好处:
可以让代码结构更清晰、更容易理解;
出了问题之后方便定位,并可以针对性地对问题提交进行“回滚”;
在功能开关的协助下,可以让开发者尽快把代码推送到 origin/master 上进行合并。这正是持续集成的基础。
而 Git 之所以能够方便我们实现原子性提交,主要有两方面的原因:
Git 提供方便、灵活的提交、分支处理功能,使得我们可以灵活地产生提交、修改提交、拆分提交,甚至改变提交的先后顺序。
Git 是一个分布式代码仓管理系统,每个开发人员在本地都有一个代码仓,从而可以放心在本地代码仓中使用上述功能,不用操心会影响到远程共享代码仓。
下面,我就来与你分享 Git 支持原子性的 5 种基础操作,具体包括:
用工作区改动的一部分产生提交;
对当前提交进行拆分;
修改当前提交;
交换多个提交的先后顺序;
修改非当前提交。
基本操作一:把工作区里代码改动的一部分转变为提交
如果是把整个文件添加到提交中,操作很简单:先用 git add < 文件名 > 把需要的文件添加到 Git 暂存区,然后使用 git commit 命令提交即可。这个操作比较常见,我们应该都比较熟悉。
但在工作中,一个文件里的改动常常会包含多个提交的内容。比如,开发一个功能时,我们常常会顺手修复一些格式规范方面的东西;再比如,一个功能比较大的时候,改动常常会涉及几个提交内容。那么,在这些情况下,为了实现代码提交的原子性,我们就需要只把文件里的一部分改动添加到提交中,剩下的部分暂时不产生提交。针对这个需求,Git 提供了 git add -p 命令。
比如,我在 index.js 文件里有两部分改动,一部分是添加一个叫作 timestamp 的 endpoint,另一部分是使用变量来定义一个魔术数字端口:
这时,运行 git add -p index.js 命令,Git 会把文件改动分块儿显示,并提供操作选项,比如我可以通过 y 和 n 指令来选择是否把当前改动添加到 Git 的提交暂存区中,也可以通过 s 指令把改动块儿再进行进一步拆分。通过这些指令,我就可以选择性地只把跟端口更改相关的改动添加到 Git 的暂存区中。
当整个文件的所有改动块儿都处理完成之后,通过 git diff --cached 命令可以看到,我的确只是把需要的那一部分改动,也就是端口相关的改动,添加到了暂存区:
通过 git diff 命令,我们可以看到,endpoint 相关的改动仍留在工作区:
最后,再通过 git commit 命令,我就可以产生一个只包含端口相关改动的提交,实现了将本地代码改动的一部分转变为提交的目的。
通过 git add -p,我们可以把工作区中的代码拆分成多个提交。但是,如果需要拆分的代码已经被放到了一个提交中,怎么办?如果这个提交已经推送到了远程代码仓共享分支,那就没有办法了。但如果这个提交还只是在本地,我们就可以对它进行拆分。
基本操作二:对当前提交进行拆分
所谓当前提交,指的是当前分支的 HEAD 指向的提交。
我继续以上面的代码示例向你解释应该如何操作。假如,我已经把关于 endpoint 的改动和端口的改动产生到了同一个提交里,具体怎么拆分呢?
这时,我可以先“取消”已有的提交,也就是把提交的代码重新放回到工作区中,然后再使用 git add -p 的方法重新产生提交。这里的取消是带引号的,因为在 Git 里所有的提交都是永久存在的,所谓取消,只不过是把当前分支指到了需要取消的提交的前面而已。
首先,我可以用 git log 查看历史,并使用 git show 确认提交包含了 endpoint 改动和端口改动:
然后,用 git branch temp 命令产生一个临时分支 temp,指向当前 HEAD。temp 分支的作用是,预防代码丢失。如果后续工作出现问题的话,我可以使用 git reset --hard temp 把代码仓、暂存区和工作区都恢复到这个位置,从而不会丢失代码。
接下来,运行 git reset HEAD^ 命令,把当前分支指向目标提交 HEAD^,也就是当前提交的父提交。同时,在没有接–hard 或者–soft 参数时,git reset 会把目标提交的内容同时复制到暂存区,但不会复制到工作区。所以,工作区的内容仍然是当前提交的内容,仍然有 endpoint 相关改动和端口相关改动。也就是说,这个命令的效果,就是让我回到了对这两个改动进行提交之前的状态:
最后,我就可以使用上面介绍过的 git add -p 的方法把工作区中的改动拆分成两个提交了。
基本操作三:修改当前提交
如果只需要修改 Commit Message 的话,直接使用 git commit --amend 命令,Git 就会打开你的默认编辑器让你修改,修改完成之后保存退出即可。
如果要修改的是文件内容,可以使用 git add、git rm 等命令把改动添加到暂存区,再运行 git commit --amend,最后输入 Commit Message 保存退出即可。
基本操作四:交换多个提交的先后顺序
有些时候,我们需要把多个提交交换顺序。比如,master 分支上有两个提交 A 和 B,B 在 A 之上,两个提交都还没有推送到 origin/master 上。
这时,我先完成了提交 B,想把它先单独推送到 origin/master 上去,就需要交换 A 和 B 的位置,使得 A 在 B 之上。我可以使用 git rebase --interactive(选项–interactive 可以简写为 -i)来实现这个功能。
首先,还是使用 git branch temp 产生一个临时分支,确保代码不会丢失。然后,使用 git log --oneline --graph 来确认当前提交历史:
接下来,运行
Git 会打开我的默认编辑器,让我选择 rebase 的具体操作:
rebase 命令一般翻译为变基,意思是改变分支的参考基准。具体到 git rebase -i origin/master 命令,就是把从 origin/master 之后到当前 HEAD 的所有提交,也就是 A 和 B,重新有选择地放到 origin/master 上面。你可以选择放或者不放某一个提交,也可以选择放置顺序,还可以选择将多个提交合并成一个,等等。另外,这里说的放一个提交,指的就是在 HEAD 之上应用一个提交的意思。
Git rebase -i 打开编辑器时,里面默认的操作列表是把原有提交全部原封不动地放到新的参考基准上去,具体到这个例子,是用两个 pick 命令把 A 和 B 先后重新放到 origin/master 之上,如果我直接保存退出的话,结果跟 rebase 之前没有任何改变。
这里,因为我需要的操作是交换 A 和 B 的顺序,所以交换两个 pick 指令行,保存退出即可。Git rebase 就会先后把 B 和 A 放到 origin/master 上。
至此,我就完成了交换两个提交的先后顺序。接下来,我可以用 git log 命令,来确认 A 和 B 的确是交换了顺序。
值得注意的是,A 和 B 的 commit SHA1 改变了,因为它们实际上是新产生出来的 A 和 B 的拷贝,原来的两个提交仍然存在(图中的阴影部分),我们还可以用分支 temp 找到它们,但不再需要它们了。如果 temp 分支被删除,A 和 B 也会自动被 Git 的垃圾收集过程 gc 清除掉。
基本操作五:修改非头部提交
在上面的基本操作二、三、四中,我与你介绍的都是对当前分支头部的一个提交或者多个提交进行操作。但在工作中,为了方便实现原子性,我们常常需要修改历史提交,也就是修改非头部提交。对历史提交操作,最方便的方式依然是使用强大的 git rebase -i。
接下来,我继续用上面修改 A 和 B 两个提交的顺序的例子来做说明。在还没有交换提交 A 和 B 的顺序时,也就是 B 在 A 之上的时候,我发现我需要修改提交 A。
首先,我运行 git rebase -i origin/master;然后,在弹出的编辑窗口中把原来的“pick b517154”的一行改为“edit b517154”。其中,b517154 是提交 A 的 SHA1。
而“edit b517154”,是告知 Git rebase 命令,在应用了 b517154 之后,暂停后续的 rebase 操作,直到我手动运行 git rebase --continue 通知它继续运行。这样,当我在编辑器中保存修改并退出之后,git rebase 就会暂停。
这时,我可以运行 git log --oneline --graph --all,确认当前 HEAD 已经指向了我想要修改的提交 A。
接下来,我就可以使用基本操作二中提到的方法对当前提交(也就是 A)进行修改了。具体来说,就是修改文件,之后用 git add < 文件名 >,然后再运行 git commit --amend。
执行完成之后,我就可以运行 git rebase --continue,完成 git rebase -i 的后续操作,也就是在 A 之上再应用提交 B,并把 HEAD 重新指向了 B,从而完成了对历史提交 A 的修改。
经过 rebase 命令,我重新产生了提交 A 和 B。同样的,A 和 B 是新生成的两个提交,原来的 A 和 B 仍然存在。
以上,就是修改历史提交内容的步骤。
如果我们需要对历史提交进行拆分的话,步骤也差不多:首先,使用 git rebase -i,在需要拆分的提交处使用 edit 指令;然后,在 git rebase -i 暂停的时候,使用基本操作 2 的方法对目标提交进行拆分;拆分完成之后,运行 git rebase --continue 即可。
小结
今天,我与你介绍了 Git 支持代码提交原子性的五种基本操作,包括用工作区改动的一部分产生提交、对当前提交进行拆分、修改当前提交、交换多个提交的先后顺序,以及对非头部提交进行修改。
掌握这些基本操作,可以让我们更灵活地对代码提交进行修改、拆分、合并和交换顺序,为使用 Git 实现代码原子性的工作流打好基础。
其实,这些基本操作非常强大和实用,除了可以用来提高提交的原子性外,还可以帮助我们日常开发。比如,我们可以把还未完成的功能尽快产生提交,确保代码不会丢失,等到后面再修改。又比如,可以产生一些用来帮助自己本地开发的提交,始终放在本地,不推送到远程代码仓。
在我看来,Git 学习曲线比较陡而且长,帮助手册也可以说是晦涩难懂,但一旦弄懂,它能让你超级灵活地对本地代码仓进行处理,帮助你发现代码仓管理系统的新天地。git rebase -i 命令,就是一个非常典型的例子。一开始,你会觉得它有些难以理解,但搞懂之后就超级有用,可以帮助你高效地解决非常多的问题。所以,在我看来,在 Git 上投入一些时间绝对值得!
思考题
对于交换多个提交的先后顺序,除了使用 rebase -i 命令外,你还知道什么其他办法吗?
文章中提到,如果一个提交已经推送到了远程代码仓共享分支,那就没有办法对它进行拆分了。这个说法其实有些过于绝对。你知道为什么绝大部分情况下不能拆分,而什么情况下还可以拆分呢?
感谢你的收听,欢迎你在评论区给我留言分享你的观点,也欢迎你把这篇文章分享给更多的朋友一起阅读。我们下期再见!
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 1
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
24 | VIM:如何高性价比地学习VIM的实用技巧?
下一篇
26 | Facebook怎样实现代码提交的原子性?
精选留言(9)
- 我来也2019-10-23git rebase -i 确实很好很强大,哈哈! 课后思考题 1.git cherry-pick 应该可以间接实现 2.git push -f / git rebase 已提交的代码后再git push -f 是危险操作。 人少时,吼一嗓子,再使用。人多时还是放弃吧。哈哈! 可是我经常这么干。🤦♂️ git push -f 后,轻则已拉取代码回本地的人需要手动修复下环境,重则之后推送的分支会被丢失。 (别人以为已经提交了,哪知还有人敢强推代码) 如果时间久了,别人本地分支也没留记录,可能代码就需要手敲了。(概率很小,但也可找回历史提交)展开
作者回复: 看来@我来也 是git的高手呀!分析得非常到位。👍👍👍
共 3 条评论7 - 雷霹雳的爸爸2020-01-16思考题留言区有同学已经答得很帅了,想了下就是做不到答得更好,只对第二个问题大概做一下补充,其实这个本体本质上还是git rebase和远端库共享的潜在冲突的问题,所以从这一点上讲,远端是fork的私库,任何共享都是基于TBD的CI基础上完成就非常重要了,对发布出去的东西,还是得注意品质,后悔药在git世界里不是没有,只是可能会让大家都很痛苦,发出去的东西就当泼出去的水,一路向前,不要回头 其实进来这里就是纯赞的,我本来以为我经常PR之前用git rebase -i仔细梳理就算是会了git了,但是看第一个场景git add -p我就直接跪了,也许以前见过,但是真没印象,一点也没有,我甚至没想过还可以这么干,虽然我不太认同这么部分的管理更新吧,要做这种部分提交,我觉得很大程度上是设计上没太想好,自己本地库这里还在debug的感觉;但是git的强大真的可见一斑展开
作者回复: git add -p 我个人还是常用的。因为在写代码的时候,常常会看到一些小的地方需要修改,就顺手改了。但是在产生提交的时候又不合适放到一个提交里,所以用git add -p 去分开。 另外TUI的git工具tig有一个命令`1`可以非常方便地进行git add -p的操作,可以一行一行的进行挑选。所以拆分也很方便。
共 2 条评论4 - Jxin2019-10-211.很棒,历史提交操作这块以前没概念,一直都是手动来,导致增加很多提交,学习了,练习下应用到工作去。 2.现在工作主要用idea,用git的拉推提交回滚啥的简单操作都是直接在idea上。涉及到拆分提交这类操作就要切命令行敲git 指令。感觉操作不连贯。老师工作中是纯命令的吗?这些操作跟idea是否有对应?如果有应该怎么做? 3.git-history,小工具,但看历史变更更直观,推荐使用。展开
作者回复: 我大部分的Git操作是在命令行中。主要使用原生的Git命令,以及tig,还有gitin。在VSCode中使用Git Graph插件做一些读历史提交的工作。 IDEA我最近没有高频使用。以前使用的时候也没有大量使用它的GUI的Git部分。 git-history,你指的是这个吗? https://github.com/pomber/git-history
共 3 条评论1 - BBQ2021-05-08老师,不知道是不是我的理解有问题 >同时,在没有接–hard 或者–soft 参数时,git reset 会把目标提交的内容同时复制到暂存区,但不会复制到工作区。 我看了您的控制台输出 ,我自己也试了一下,reset 之后,之前Commit 的修改会全部复制到工作区,不是复制到暂存区,reset 之后暂存区并没有内容。 多谢!展开
- 巫山老妖2021-04-16git rebase的操作,我们大多是在合并分支的时候用到,用于原子性提交确实日常没怎么用,因为要让大部人做到原子性提交还是件不容易的事情
- 送普选2021-02-09简单用过git,有个问题,若只提交到本地分支,不推送到远程分支,若本地电脑硬盘故障代码就丢失了,每日多次推送会减少损失,但从远程还是没有最近没推送的代码。请问葛老师我理解对吗?这种情况如何解决?谢谢!共 2 条评论
- scguo2020-12-21给推荐一个git的学习工具,https://github.com/Gazler/githug
- 许童童2019-10-22思考题 可以通过强制push: git push -f 覆盖掉在远程的分支,不过这样做确实很危险。
作者回复: 是的。可以在本地修改git历史,然后git push -f推送到远端共享分支。 再追问一步,这样做的危险性是什么呢?
- 二狗2019-10-21没用过 没看懂(╥╯^╰╥) 看来还得拿栗子实践一下
作者回复: Git很好玩的 :)