那些年,希望自己早点学会的 Git 用法

275 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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 官方文档 搜索,这里有许多好东西 :)

基本原理

image.png

  • 工作区(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文件

恢复本地误操作

image.png

有时候我们出现误操作,比如不小心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 的修改。