作者从事工作一年半,靠着git
三板斧提交了近百万行代码,常常以十分“粗暴”的方式解决各种git
操作问题。 过年期间,通读了《Pro Git Book》,才了解git
的使用方式竟可以如此“优雅”,总结了一些git
提效技巧,分享给大家。
注意,为保障隐私安全问题,文章所有案例附注的git
相关截图,都来源于github 开源项目tdesign-miniprogram
,作者fork
了该项目进行操作,项目路径点这里。
1. git log
- 查看提交历史
开发中日常使用的git log
命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明,不利于检索。我们可以使用git log --oneline
将 log 记录输出为[简写哈希] [提交说明]
的格式。
1.1 定制历史记录格式
使用--pretty=format:
,可以定制记录的显示格式,如下图所示:
使用的命令如下,format
支持常用格式占位的写法,其中%h
表示提交的简写哈希值,%an
表示作者名字,%ar
表示作者修订日期,%s
表示提交说明。同时可以使用%C(color)
和%Creset
为对应的标签添加颜色。
// 简洁版本,
// 格式: 提交的简写哈希值 - 作者名字(作者修订日期):提交说明
git log --pretty=format:"%h - %an(%ar) : %s"
// 为对应的标签添加颜色
git log --pretty=format:"%C(yellow)%h%Creset - %Cgreen%an%Creset(%ar) : %s"
复制代码
1.2 隐藏合并提交
多人协同开发的代码仓库流程,记录中必定包含许多合并提交,如Merge pull request #1 form XXX
。这类合并提交传递的信息量很少,且十分碍眼,可以为 log
加上 --no-merges
选项来过滤。
// 隐藏合并提交 --no-merges
git log --pretty=format:"%C(yellow)%h%Creset - %Cgreen%an%Creset(%ar) : %s" --no-merges
复制代码
1.3 筛选历史记录
git log
提供了许多非常实用筛选记录的选项,你可以使用类似 -<n>
的选项,来表示仅显示最近的 n
条提交。也可以使用--author="zhenzhencai"
来指定筛选作者zhenzhencai
的提交记录。也可以使用--since
、--until
来过滤指定时间之后(之前)的提交。
例如,要查询用户zhenzhencai
在 2021 年 9 月 1 日-9 月 30 日的最近 10 条提交记录,可以使用如下命令。
// 筛选条件: --author="zhenzhencai" --since="2021-09-01" --until="2021-09-30" -10
git log --pretty=format:"%C(yellow)%h%Creset - %Cgreen%an%Creset(%ar) : %s" --author="zhenzhencai" --since="2021-09-01" --until="2021-09-30" -10
复制代码
筛选记录如下:
1.4 查看提交区间
你想要查看 experiment
分支中还有哪些提交尚未被合并入 master
分支,可以使用 master..experiment
来让 git
显示这些提交。
git log master..experiment
// 显示C、D的提交记录
复制代码
另一个常用场景是,在你提交 mr 到指定分支之前,你想查看会提交哪些记录,那么可以使用:
git log origin/master..HEAD
复制代码
这条记录会显示,当前分支还有哪些提交未被合入 master
分支。如果你留空了其中的一边, Git 会默认为 HEAD
。 例如, git log origin/master..
将会输出与之前例子相同的结果 —— Git 使用 HEAD 来代替留空的一边。
想要了解更多用法,可查看# Git 基础 - 查看提交历史
2. git commit --amend
- 修改最后一次提交
我们知道,git commit
会将暂存区的文件进行提交。在日常开发中,我们常常在提交完了才发现漏掉了几个工作区的文件没有添加,或者提交信息有错别字。
2.1 经典案例
如下图所示,工作区的两个文件并没有提交,同时,提交信息描述不清晰。
解决方法如下:
- 使用
git add .
将工作区的文件存入暂存区。 - 使用
git commit --amend
命令,进入 vim 编辑器。 - 输入按键
i
对内容进行编辑,比如修改提交说明。 - 输入按键
esc
退出编辑,输入按键shift
+:
,然后输入wq
,保存编辑内容。
这样就能精确的修改原来的提交,且不会额外产生提交记录。
2.2 案例 1: 忘记提交未暂存的文件
如果只是忘记提交暂存的文件,那么不必修改提交信息,只需要将工作区的文件添加到暂存区,然后通过--no-edit
命令跳过编辑器环节。
git add .
git commit --amend --no-edit
复制代码
2.3 案例 2: 只想修改提交说明
如果只是想修改提交信息,也不必进入编辑器环节,可以直接使用如下命令修改。
git commit --amend -m "feat: 这是我修改的提交说明"
复制代码
2.4 案例 3: 消除某个额外提交的文件
如果错误提交了某个文件,比如 mock 文件,可以使用该方法消除那个文件。
// 消除指定文件 <file>
git rm —-cached <file>
// 更新提交记录
git commit —-amend
复制代码
注意: 修正会改变提交的 SHA-1 校验和,它类似于一个小的变基,如果已经推送了最后一次提交就不要修正它。
3. git rebase -i
- 修改多个提交
前面提到过,可以使用git commit --amend
修改最近的一次提交,如果想修改在提交历史中较远的提交,可以使用git rebase -i
进行变基。
如上图所示,是前端小菜的最近 6 条 git 记录。临近下班,小菜想将代码推送到远程分支,却发现倒数第 5 条记录有错别字需要修改;倒数第 2-4 条提交说明是相同的(一名合格的工程师应该将其合并成 1 条记录);同时,小菜想删除最新的一条提交记录。
那么可以通过git rebase -i HEAD~6
来变基最近的 6 条 git 记录,其中HEAD~n
表示指定最近的 n 条 git 记录。输入该命令,进入 vim 编辑器。
如上图所示,反序展示了最近 6 条 git 记录(最旧的在最前面),因为变基就是从命令行中指定的提交(HEAD~6
)开始,从上到下的依次重演每一个提交引入的修改。每一条记录的命令都是pick
,表示使用提交。如果想要编辑提交说明,可以改成reword
命令。如果想要合并提交,可以使用squash
命令将提交内容挤压到前一个提交。如果想要删除提交,可以使用drop
命令删除该条 git 记录。
如上图所示,小菜修改了e6753b6
的提交说明,将02e350c
,a7853ac
的提交内容合并到063bfda
上,同时删除了a65477a
分支。保存编辑内容,则会依次变基每条 git 记录。修改结果如下图所示。
注意: 变基会改变提交的 SHA-1 校验和,如果已经推送了提交就不要尝试变基它,不然在多人协同开发中,可能会造成毁灭性的合并问题。
4. git cherry-pick
- 将部分提交合并到其它分支
在多人协同开发的项目中,常常会遇到开发功能互相依赖问题,这时就需要将当前开发分支的部分提交合并到其它开发分支。git cherry-pick
便派上用场了。
git cherry-pick
命令的作用,就是将指定的提交(commit)应用于其他分支,它也支持转移多个提交。
git cherry-pick ([commitId] | [first-commitId]..[last-commitId])
// 其中,[first-commitId]..[last-commitId]是前开后闭区间,即不包括[first-commitId],但包括[last-commitId]
复制代码
例如,代码仓库有master
和feature
两个分支:
a - b - c - d - e master分支
\
f - g - h - i - j feature分支
复制代码
要将feature
分支的部分提交合并到master
分支,则可运行如下命令:
// 切换到master分支
git checkout master
// 假设,将commit g 转移到master分支,则运行
git cherry-pick g
// 假设,将commit g、commit h 转移到master分支,则运行
git cherry-pick g h
// 假设,将commit g 到 commit i 之间的提交 转移到master分支,则运行
git cherry-pick f..i
复制代码
如果操作过程中发生代码冲突,Cherry pick 会停下来,让用户决定如何继续操作。用户解决完代码冲突后,可以使用git cherry-pick --continue
继续操作。
如果发生代码冲突后,用户放弃合并,想回到操作前的样子,则执行git cherry-pick --abort
。
更多配置项,可参考# git cherry-pick 教程
5. git reflog
- 起死回生之术
阳光明媚的某个下午,前端小菜努力打工,开发了 4 个新特性功能,(如下所示 4 个commit
)。
5e5fc6b - zhenzhencai(2 分钟前) : feat: 特性4功能提交
2b389b6 - zhenzhencai(2 分钟前) : feat: 特性3功能提交
6a8af62 - zhenzhencai(2 分钟前) : feat: 特性2功能提交
53f441a - zhenzhencai(3 分钟前) : feat: 特性1功能提交
1967df4 - zhenzhencai(2 天前) : feat: rate components add variant attribute
复制代码
在做特性 5 开发的时候,小菜调试出现问题,一气之下小菜使用git reset --hard 1967df4
回滚到最初的commit
,顺利完成特性 5 的功能,并提交 commit。
0ca6e09 - zhenzhencai(8 秒钟前) : feat: 总算完成了的特性5功能提交
1967df4 - zhenzhencai(2 天前) : feat: rate components add variant attribute
复制代码
打印提交日志,小菜发现出大事了,之前的 4 个特性功能全部丢失了,小菜哭得像个 200 斤的孩子,默默地重新搬砖...
其实,小菜的劳动成果并没有丢失,只是git log
丢失了原来四个特性功能的commitID
,这时,git reflog
派上了用场。
git reflog
显示的是一个 HEAD
指向发生改变的时间列表。比如在你切换分支、用 git commit
进行提交、以及用 git reset
撤销 commit 时,HEAD
指向会改变,reflog
会记录这一切。但是当你进行 git checkout -- <filename>
撤销或者 git stash
存储文件等操作时,HEAD
并不会改变,这些修改从来没有被提交过,因此 reflog
也无法帮助我们恢复它们。
git reflog
记录你的每一步操作日志,我们可以打印出来看下
如上图所示,小菜切换到feat/rate-update
分支,使用git reset --hard origin/feat/rate-update
将本地分支与远程关联分支提交保持一致后,便陆陆续续开发并提交了四个特性功能。然后,小菜强制回滚到1967df4
,提交了特性 5 功能。
我们的需求是找回之前的四个特性功能提交,话不多说,开干。
// 方案1:
// 回滚到 5e5fc6b HEAD@{2}: commit: feat: 特性4功能提交
git reset --hard 5e5fc6b
// 然后cherry-pick 0ca6e09 HEAD@{0}: commit: feat: 总算完成了的特性5功能提交
git cherry-pick 0ca6e09
// 方案2:cherry-pick四个特性分支
// cherry-pick 1967df4..5e5fc6b // ..表示前开后闭区间,即不包括1967df4,但包括5e5fc6b
复制代码
两个方案的区别在于 git 记录的时序问题,当前情景建议使用方案 1。
8f82482 - zhenzhencai(21 分钟前) : feat: 总算完成了的特性5功能提交
5e5fc6b - zhenzhencai(31 分钟前) : feat: 特性4功能提交
2b389b6 - zhenzhencai(31 分钟前) : feat: 特性3功能提交
6a8af62 - zhenzhencai(32 分钟前) : feat: 特性2功能提交
53f441a - zhenzhencai(32 分钟前) : feat: 特性1功能提交
1967df4 - zhenzhencai(2 天前) : feat: rate components add variant attribute
复制代码
操作完后,小菜顺利找回 5 次记录,快乐下班!
6. git revert
- 撤销
上节我们提到,小菜辛辛苦苦开发了五个特性功能,并提交到远程仓库。第二天准备合流时,老板紧急通知因为不可控因素,特性 1、特性 2 功能不上线了!!!( ꒪⌓꒪)
我们知道,如果要取消全部功能,我们可以通过git reset
直接回滚到某一特定提交。
reset
命令格式如下:
git reset [option] [commitId]
复制代码
这里的 option
共有 3 个值,具体含义如下:
--hard
:撤销commit
,撤销add
,删除工作区改动代码。即将[commitId]
到HEAD
的所有提交清空。--mixed
:默认参数。撤销commit
,撤销add
,还原工作区改动代码。即将[commitId]
到HEAD
的所有提交还原到工作区。--soft
:撤销commit
,不撤销add
,还原工作区改动代码。即将[commitId]
到HEAD
的所有提交还原到暂存区。
我们的需求是,上线特性 345,特性 12 暂时不上线。有两种解决方案,一种是新建一个分支,将原开发分支特性 345 的所有提交cherry-pick
出来。另外一种方式是使用git revert
。
revert
与 reset
的作用一样,都是恢复版本,但是它们两的实现方式不同。简单来说,reset
直接会滚到[commitId]
,而 revert
是新增一个提交,但是这个提交是将[commitId]
的内容反向修改回去。
git revert [option] ([commitId] | [first-commitId]..[last-commitId])
// 例如:撤销53f441a 、6a8af62两个提交
git revert 1967df4..6a8af62 // ..表示前开后闭区间,即不包括1967df4,但包括6a8af62
// 或者 使用 --no-edit 跳过编辑器
git revert --no-edit 1967df4..6a8af62
复制代码
撤销后的日志如下,新增了 2 个“特性 1 特性 2 内容反向修改”的提交:
9f498d8 - zhenzhencai(13 秒钟前) : Revert "feat: 特性1功能提交"
8e49cb9 - zhenzhencai(44 秒钟前) : Revert "feat: 特性2功能提交"
8f82482 - zhenzhencai(2 天前) : feat: 总算完成了的特性5功能提交
5e5fc6b - zhenzhencai(2 天前) : feat: 特性4功能提交
2b389b6 - zhenzhencai(2 天前) : feat: 特性3功能提交
6a8af62 - zhenzhencai(2 天前) : feat: 特性2功能提交
53f441a - zhenzhencai(2 天前) : feat: 特性1功能提交
1967df4 - zhenzhencai(4 天前) : feat: rate components add variant attribute
复制代码
Tips:正因为
revert
永远是在新增提交,因此本地仓库版本永远不可能落后于远程仓库,可以直接推送到远程仓库,故而解决了 reset 后推送需要加-f
参数的问题,提高了安全性。
7. git bisec
- 查找哪次提交引入了错误
在多人协同开发中,常常会遇到多个开发特性合流后,导致项目运行出现“缺陷”,有些缺陷是很难直观地排查出来。那么如何定位到究竟是哪位开发者下的“毒”呢?
git bisec
便派上了用场,它用来查找哪一次代码提交引入了错误。它的原理很简单,就是将代码提交的历史,按照两分法不断缩小定位。所谓"两分法",就是将代码历史一分为二,确定问题出在前半部分,还是后半部分,不断执行这个过程,直到范围缩小到某一次代码提交。
git bisect start
命令启动查错,它的格式如下。
git bisect start [end-commit] [start-commit]
// 标识本次提交没有问题,意味着错误是在代码历史的后半段引入的
git bisect good
// 标识本次提交有问题,意味着错误是在代码历史的前半段引入的
git bisect bad
// 退出查错,回到最近一次的代码提交
git bisect reset
复制代码
其中,[end-commit]
是最近的提交,[start-commit]
是更久以前的提交。它们之间的这段历史,就是差错的范围。
举个例子,以下日志展示小菜的最近 7 次特性功能提交,小菜想排查到底是哪一次提交导致运行缺陷。
f0a7500 (HEAD -> feat/rate-update) feat: 特性7功能提交
8b75e0c feat: 特性6功能提交
e83ebe1 feat: 特性5功能提交
5e5fc6b feat: 特性4功能提交
2b389b6 feat: 特性3功能提交
6a8af62 feat: 特性2功能提交
53f441a feat: 特性1功能提交
1967df4 (fix/tabs/controlled, feat/steps/controlled, dependabot/docker/node-17) feat: rate components add variant attribute
复制代码
那么,可以使用git bisect start
命令启动查错:
// 查询区间 53f441a 到 f0a7500 的七个提交
git bisect start f0a7500 53f441a
// 由于 f0a7500 是 HEAD,也可以写成
git bisect start HEAD 53f441a
复制代码
如上图所示,显示了整个查错的过程,二分查找将七条提交记录一分为二,工作区回滚到5e5fc6b feat: 特性4功能提交
,经过测试,代码运行并没有缺陷,证明缺陷是在后面的提交中引入的,输入git bisect good
标记。也就证明缺陷引入在特性 5、特性 6、特性 7 三次提交记录之间。
进行第二次二分查找,工作区回滚到8b75e0c feat: 特性6功能提交
,经过测试,代码运行已经存在缺陷,证明缺陷可能是在本条记录或者前面的提交引入的。输入git bisect bad
标记。
进行第三次二分查找,工作区回滚到e83ebe1 feat: 特性5功能提交
,经过测试,代码运行存在缺陷,证明缺陷可能是在本条记录或者前面的提交引入的。输入git bisect bad
标记。
此时,二分查找结束,e83ebe1 feat: 特性5功能提交
这条记录就是我们要排查的引入错误的“罪魁祸首”。
8. git branch
- 分支管理
在多人协同开发过程中,常常遇到在同一时间段,切换不同的分支修复问题。处理完问题后想回到原来的开发分支,却忘记分支名。我们会使用git branch
查看本地的分支列表,当列表是按照分支命名排序的,当本地分支的数量上百个,根本分不清哪是哪。
8.1 按提交日期排序分支列表
如果按照分支的使用时间排序,那么问题就迎刃而解。使用--sort=-committerdate
可以显示所有本地分支的列表,并根据上次提交的日期对其进行排序。
如果需要查看每一个分支的最后一次提交,可以运行 git branch -vv
命令。
// --sort=-committerdate: 根据上次提交日期对分支进行排序
// -v: 显示每个分支的最后一次提交
git branch --sort=-committerdate -vv
// 显示如下: <分支名> <最后一次提交简写哈希值> <跟踪分支记录> <最后一次提交描述>
feat/rate-update 1967df4 [origin/feat/rate-update: behind 1] feat: rate components add variant attribute
feat/steps/controlled 1967df4 feat: rate components add variant attribute
* bugfix/badge-fix 792cd51 [origin/bugfix/badge-fix: ahead 2] feat: update rate readme.md
develop a170b57 [origin/develop] Merge pull request #1 from zhenzhencai/feat/rate-update
复制代码
8.2 查看分支是否合并
另外,--merged
与 --no-merged
这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支,方便检查是否有遗漏提交未合流。
// 查看哪些分支已经合并到当前分支,不加分支名表示对比当前分支
git branch --merged
// 查看哪些分支还未合并到指定分支(master)
git branch --no-merged master
// 查看哪些分支已经合并到指定分支(develop)
git branch --merged develop
复制代码
8.3 查看分支是否包含某个提交
--contains
与 --no-contains
这两个有用的选项可以过滤分支列表中包含或者未包含某个特定提交的分支,方便检测某次特定提交是否合流。
// 列出包含<commit>这条提交的分支列表
git branch --contains <commit>
// 列出不包含<commit>这条提交的分支列表
git branch --no-contains <commit>
复制代码
8.4 删除已经合并的分支
随着开发者代码贡献量的与日俱增,本地维护的分支会越来越多,需要定期清理。清理的第一目标就是已经合并到主分支的分支。操作步骤如下:
- 使用
git branch --merged <branch>
列出所有已经合并到<branch>
的分支。 - 使用
grep -v "(^*|<branch>)"
过滤掉当前分支(带*)和想过滤的<branch>
。 - 使用
xargs git branch -d
删除所有找到的分支。
git branch --merged <branch> | grep -v "(^*|<branch>)" | xargs git branch -d
// 例如,想要删除所有合并入master的分支,除了develop分支,可以使用:
git branch --merged master | grep -v "(^*|master|develop)" | xargs git branch -d
复制代码
9. git stash
- 保存和恢复工作进度
在日常工作中我们时常遇到这样的问题,当热火朝天地开发新功能时,突然接到了测试分配的紧急 bug 单,需要切换其它分支进行修复。偶尔也会收到产品和后端小哥让你切换特定分支帮他跑个代码的需求。这个情况,就需要将未提交的修改暂存在堆栈上,切换到别的分支“打工”完后,再切换会原来的分支取出原来的修改内容。相信大家git stash
和git stash pop
两件套已经操作得炉火纯青。
但是,git stash
的强大之处不仅于此,你需要掌握更多的技巧。git stash
使用堆栈来保存工作进度记录,如下图所示:
-
git stash list
: 查看工作进度记录堆栈列表,如图所示,当前保存了 6 条工作进度记录。栈顶stash@{0}
表示最新入栈的记录。 -
git stash [save message]
: 将未提交的修改内容保存到堆栈。其中,save message
为可选项,message
为本次保存的注释,方便开发者查阅。如图所示,使用git stash save "feat:在develop分支更新文档"
命令保存最新的工作进度记录到栈顶。 -
git stash pop [stash@{num}]
: 将堆栈中的工作进度记录弹出,应用到当前分支。其中,stash@{num}
是可选项。如果使用git stash pop
那么默认弹出栈顶的内容,即stash@{0}
。如果要弹出对应的工作记录,比如切换到badge-fix
分支,必定是弹出在该分支缓存的stash@{2}
记录,则运行git stash pop stash@{2}
。 -
git stash apply [stash@{num}]
:pop
是出栈操作,如果想多次应用缓存中的工作记录,则无需出栈,使用git stash apply
即可。其中,stash@{num}
是可选项,可通过stash@{num}
指定对应的工作记录。 -
git stash drop [stash@{num}]
: 移除指定的工作记录。 -
git stash clear
: 清空堆栈。
注意:如果想将未跟踪的文件也保存在堆栈中,可以添加
-u
参数,比如使用git stash save "feat:暂存未跟踪的文件" -u
10. git config alais
- 配置别名,提高效率
前几小节提到了许多 git 提效技巧,但是都需要输入很长的 git 命令,git 并不会在你输入部分命令时自动推断出你想要的命令。如果不想每次都输入完整的 Git 命令,可以通过 git config
来轻松地为每一个命令设置一个别名。
git config --global alias.<alias> <command>
// 比如:定制历史记录格式 的命令 取别名为 logs
git config --global alias.logs 'log --pretty=format:"%C(yellow)%h%Creset - %Cgreen%an%Creset(%ar) : %s" --no-merges'
// 后续我们就可以使用如下命令:
git logs
复制代码
以下列出作者常用的别名:
logs = log --pretty=format:"%C(yellow)%h%Creset - %Cgreen%an%Creset(%ar) : %s" --no-merges
logo = log --oneline
amend = commit --amend -m
cam = commit -am
fix = commit --fixup
co = checkout
cob = checkout -b
coo = !git fetch && git checkout
br = branch
brd = branch -d
st = status
unstage = reset --soft HEAD^
undo = reset HEAD~1
rv = revert
cp = cherry-pick
pu = !git push origin `git branch --show-current`
fush = push -f
mg = merge --no-ff
rb = rebase
rbc = rebase --continue
rba = rebase --abort
rbs = rebase --skip
rom = !git fetch && git rebase -i origin/master --autosquash
save = stash push
pop = stash pop
apply = stash apply
rl = reflog
复制代码
如上,20 多条别名,一键配置是十分繁琐的,可以通过编辑配置文件一键添加所有别名。
我们可以使用git config --global -e
在文本编辑器中打开配置文件,按i
进入编辑模式,然后将上述别名配置复制到文件中,按esc
键退出编辑模式,输入按键shift
+:
,然后输入wq
,保存编辑内容。
此时,别名已经生效。
可是,那么多别名,怎么记得住啊?我们可以通过如下命令打印别名列表,多看几次,自然记得住!
git config -l | grep alias | sed 's/^alias\.//g'
复制代码
最后
创作不易,点个赞再走吧೭(˵ᴛ ʏ ᴛ˵)౨