持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情
Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.
对任何一个研发来说,Git 几乎都是个必备技能,哪怕不涉及团队协作,我们也需要一个版本管理工具帮助自己记录代码变更。干业务开发这些年,Git 几乎是每天都在用,非常高频,但对于一些高阶用法却非常陌生,每次都是基础的这几个。这篇文章,希望带大家了解一些常见的 Git 高级用法,希望能够帮助大家提高协作效率。同时建议大家利用好 git 官方文档 搜索,这里有许多好东西 :)
基本原理
- 工作区(Workspace): 就是你在电脑里能看到的目录
- 暂存区(Index/Stage):所有变动的文件,Git 都记录在一个区域,叫做"暂存区"
- 本地仓库(Repository):本地的代码仓库,无需网络通信,纯本地管理。
- 远程仓库(Remote):线上仓库,本地仓库某个版本在线上的镜像存在。
撤销更改
用好 git reset ,注意区分不同的选项:
-
--mixed
不删除工作空间改动代码,撤销commit,并且撤销git add .
操作
这个为默认参数,git reset --mixed HEAD^
和git reset HEAD^
效果是一样的。 -
--soft
不删除工作空间改动代码,撤销commit,不撤销git add .
。所以如果我们希望撤销 commit 但保留代码,可以这样操作:git reset --soft HEAD^
-
--hard
删除工作空间改动代码,撤销commit,撤销git add .
注意完成这个操作后,就恢复到了上一次的commit状态。
查看文件变更记录
使用git log -p [文件名]
即可,将会展示这个文件的修改记录,包括 commit message 和详细 diff。
-p
的后面也可以跟一个【文件路径】,【分支名称】等参数:
git log -p origin/master apps/pendah-lite
合并多个 commit
本地开发的时候有的同学可能比较随意,这也说不上错,有时候我们只是希望 git 帮我们管理多次代码变更,至于 commit msg,反正只有自己看,无伤大雅。
造成的结果就是,明明语义上只有一次提交,却发现 master 的 commit 记录里出现了几十次甚至上百次 commit。
这样的 case 在本地是 ok 的,但一旦需要团队合作,请大家务必保证你的 commit 是得体的,干净的。commit 也是 Code Review 的一个很重要的部分。
合并 commit 需要用到 rebase 的能力:
git rebase -i HEAD~{n}
你希望合并过去多少个 commit,n 就填多少。执行之后,git 命令行会让我们选择 squash 哪些 commit,以及调整 commit msg 的内容,调整 commit 顺序。如果仅仅需要保留第一个 commit,那么我们 pick 它,把其他的都 squash 就好:
pickup hash1 msg1
squash hash2 msg2
squash hash3 msg3
修改成以上情况就能把hash2 hash3的commit squash到hash1的commit中。
以行为单位查看 commit
默认的 git log
展示信息比较全,有时候我们希望简短地看一下过去的 N 个 commit,可以直接用 git log --oneline
:
$ git log --oneline
68d777d (HEAD -> master) 我是第3次提交,重新修改的message
8b3a071 第2次提交,新增内容:git commit --amend v2
d3e2d8c 第1次提交,新增readme.txt文件
恢复本地误操作
有时候我们出现误操作,比如不小心reset --hard了,这个时候要用好 git reflog
的能力。
所谓 reflog,语义上代表 Referene logs,我们可以用 git reflog
来显示可引用的历史版本记录。它跟 git log
的区别在于:
使用
git log
命令只可以查看到HEAD指针及其之前的版本信息,如果版本发生过回退操作,则可能会出现,HEAD指针之后仍存在历史提交版本的情况,而这些提交版本信息通过git log
命令是看不到的。即:
git log
命令是显示当前的HEAD
和它的祖先,递归是沿着当前指针的父亲,父亲的父亲,……,这样的原则。
而 reflog 则可以让我们看到所有历史版本信息,可以理解为,当我们需要进行【版本回退】或【恢复操作】时,可以通过 git reflog
找到所需的 commit。
注意:
reflog
并不是Git仓库的一部分,它单独存储,它纯属是本地的。 (git reflog
命令显示的内容,应该是存储在.git/logs/HEAD
文件中,或者是.git/logs/refs
目录中的文件。)也就是说
git reflog
命令中保留了从clone仓库开始,用户所有在本地库中的操作。
可以理解为 git 的 head 是一个指针,reflog 记录了 head 走过的路,可以在 reflog 中找到 reset --hard的那次commit。之后可以使用cherry-pick 来复制那个commit 或者 checkout 来直接把HEAD指针移动到那次提交。
拆分单个文件的修改到多次提交
有时候我们的确在一次文件变更里干了语义独立的两件事,如果作为一个 commit 提交,未免代码和commit 不匹配,但如果硬要拆两次,也有点麻烦。这个时候可以使用 git add -p
,它能帮助在本次提交里面只 add 单个文件的某一部分。
二分法查找有问题的提交
想必大家或多或少都经历过这样的场景:线上出现 bug,一看代码发现有问题,想确定是哪个版本出现的,往前一找,发现上个版本也有问题。就这么一路往前找啊找,很费事费力,有时候大仓库上百个 commit,横不能一个个看过去。其实 git 已经为我们提供了二分查找的能力:git bisect
- git bisect start [终点] [起点] // 标记二分区间 并开始定位问题
- git bisect good / bad // 标记当前节点是否为ok的,标记后git自动切换到下一个中点。循环往复该步骤,直至找到问题commit
- git bisect reset // 退出定位问题状态
修改 commit msg
有的时候我们着急提交,或者暂时没有想到好的 commit msg,就匆匆写了个 "fix", "update" 上去。随后想改的话,问题就来了,如果在这个 commit 前没有别的修改,我们可以直接:
git commit --amend
随后调整 commit msg。或者直接一行搞定:git commit --amend -m "New commit message."
(当然如果你只是需要将当前暂存区的改动,合入上一个commit,连 msg 都不改,可以 git commit --amend --no-edit
就ok)
但很多时候,这个希望改 msg 的 commit 可能已经过了很远,不再是最新的 commit 了。这个时候我们要再次请出来 rebase。
假设我们要改掉倒数第 4,5 两次 commit 的 msg,使用上面提到过的命令:git rebase -i HEAD~5
交互命令行会展示我们最近的 5 次 commit:
pick 43f8707f9 fix: update dependency json5 to ^2.1.1
pick cea1fb88a fix: update dependency verdaccio to ^4.3.3
pick aa540c364 fix: update dependency webpack-dev-server to ^3.8.2
pick c5e078656 chore: update dependency flow-bin to ^0.109.0
pick 11ce0ab34 fix: Fix spelling.
# Rebase 7e59e8ead..11ce0ab34 onto 7e59e8ead (5 commands)
鉴于目前我们只是想改 msg,而不再像上面一样要合 commit,这次我们不用 squash,而是把目标的 commit 前面的 pick 改成 reword:
reword 43f8707f9 fix: update dependency json5 to ^2.1.1
reword cea1fb88a fix: update dependency verdaccio to ^4.3.3
pick aa540c364 fix: update dependency webpack-dev-server to ^3.8.2
pick c5e078656 chore: update dependency flow-bin to ^0.109.0
pick 11ce0ab34 fix: Fix spelling.
# Rebase 7e59e8ead..11ce0ab34 onto 7e59e8ead (5 commands)
保存并退出 vim 后,对于每一个选中的 commit,都会弹出新的编辑器,我们可以在这里调整 commit msg,保存并退出。
fix: update dependency json5 to ^2.1.1
这个时候再看 git log
就会发现 commit msg 已经变了,随后可以 git push -f
推到远程的开发分支(不要在 master 搞事情)
git push --force <remoteName> <branchName>
修改 commit 提交顺序
还是用强大的 rebase,大家前面已经看过,这里不再多说,本质是使用rebase -i来操作。rebase -i 进入的编辑器界面,展示的commit顺序就是从最新到最旧的commit,调整这个文本的顺序就会让rebase命令来调整commit的顺序。
开闭区间
HEAD^
的意思是上一个版本,也可以写成HEAD~1
,如果你进行了2次commit,可以使用HEAD~2
。
假设我们有以下一批 commit:
4b900b2 (HEAD -> feat-ag9920, origin/feat-ag9920) fix: some fix
c6b74a8 fix: aaa
b1b4053 fix: bbb
639999a fix: ccc
9c8b562 fix: ddd
f21760d fix: eee
1052a9c fix: fff
使用 A...B 表示 A - B 的左开右闭区间,A^..B 表示 A - B 的全闭空间。
$ git log --oneline 9c8b562...4b900b2
4b900b2 (HEAD -> feat-ag9920, origin/feat-ag9920) fix: some fix
c6b74a8 fix: aaa
b1b4053 fix: bbb
639999a fix: ccc
$ git log --oneline 9c8b562^...4b900b2
4b900b2 (HEAD -> feat-ag9920, origin/feat-ag9920) fix: some fix
c6b74a8 fix: aaa
b1b4053 fix: bbb
639999a fix: ccc
9c8b562 fix: ddd
基于指定 commit 切出新分支
有时候我们需要对线上进行 hotfix,但 master 可能已经多了很多新提交了,不能贸然基于最新 master 进行修复。这个时候一般需要找到上线的 commit,基于此来切出新的 hotfix 分支,在这里新增 commit 修复问题。
git checkout `{commitID}` -b `{新分支名}`
同步未提交的修改
这一点非常有用,有时候我们的代码改动只是半成品,提 commit 吧不是很得体,毕竟功能没完成,活干了一半,自己看也就罢了,给别人看就会比较惭愧。
但我们如何才能把没提交的改动保存下来呢?
这个时候要用到 git diff 的能力。大家知道,git diff 可以帮助我们看到具体哪个地方做了什么修改。其实还有个更重要的 git apply
的命令,支持我们把 diff 结果应用到代码上。所以,流程如下:
- 将 diff 结果保存下来:
git diff > aaa.diff
这里也可以使用 --output=aaa.diff 来生成 diff 文件。
- 把 aaa.diff 发送给协作者,或者保存着等待以后用
- 协作者在自己的工作区执行 git apply:
git apply aaa.diff
这样就能应用到新的工作区了。注意 diff 可以用来转移/交流没有提交 commit 的修改,patch 则是转移/交流已经 commit 的修改。