带你全面了解 Git 系列 02 - 常用 Git 技巧
上篇文章主要给大家介绍了 Git 的内部原理,整体比较偏理论。那本篇文章则主要会结合大家日常使用 Git 的 10 个高频场景,给大家讲解 Git 的一些实用技巧,那也希望大家在看完本篇文章之后能把一些技巧应用到实际的工作中,帮助大家提高工作效率。
场景 1: 要在不同分支之间进行代码合并,到底是用 git merge 还是 git reabase 或者 git cherry-pick? 不同的方法之间有什么区别呢?
在之前的文章中提到过,便捷的分支操作是 Git 的一大杀手锏,其中不同分支之间的代码合并就有好几种方法 —— git merge
, git rebase
, git cherry-pick
。那这三种方法之间到底有什么区别呢?
我们先来看一下它们都是怎么做分支合并的:
git merge: 基于两个分支的最新提交及其共同的祖先,进行一次三方合并。
git cherry-pick: 获得某个分支上单个提交中引入的变更,然后将其作为一个新的提交应用到当前分支上。
git rebase: 可以理解为一个自动化的 cherry-pick 命令。它把某个分支上的一系列提交,以相同的顺序在当前分支“顺序播放”。
具体都是什么意思呢?来看下面相关的 demo:
先假设进行分支合并前各分支的状态如下图所示
(1)git merge
使用 git merge
进行分支合并的话,它会基于两个分支最新的提交和分支共同的祖先,进行一次三方合并。
就 git merge
来说,它也提供了 3 种不同的合并方式,分别是--ff,--no-ff,--squash
。
--ff
:该参数是默认参数,表示使用 fast-forward 方式合并,即如果待合并的分支在当前分支的下游,也就是说没有分叉时,会发生快速合并,不会生成新的合并提交记录。
合并之后各分支的状态如下:
--no-ff
:表示不使用 fast-forward 方式合并,这种合并方法会在 master 分支上新建一个提交节点。
合并之后各分支的状态如下:
--squash
:把多次分支 commit 历史压缩为一次。然后在 squash 后,还要在当前所在的分支主动做一次 commit,产生一次合并记录。
合并之后各分支的状态如下:
这种方式和 no-ff
很像,区别在于当前分支 master 最新的 commit 没有关联 feature-a 分支。
那以上就是 git merge
在合并不同分支代码的 3 种方式,下面来看看另一种合并方式 git cherry-pick
。
(2)git cherry-pick
git cherry-pick
用来获得在单个提交中引入的变更,然后尝试将其作为一个新的提交引入到你当前分支上。
现在从 feature-a 分支中把 B0 这次提交的代码合并到 master 分支上。
合并之后各分支的状态如下:
(注意,cherry pick 之后会产生一个新的 commit 节点)
以上就是 cherry-pick
的过程,下面来看最后一种方法 git rebase
。
(3)git rebase
git rebase 其实就是一个自动化的 cherry-pick 命令。 它将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。
执行 git rebase
命令, 首先找到当前分支 master 和变基的目标基底分支 feature-a 的最近共同祖先 A3,然后对比当前分支 master 相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后把目标基底分支的变更在当前分支重新应用一遍,最后再将之前另存为临时文件的修改依序应用。(demo 例子中 master 分支没有基于共同祖先 A3 的新的提交,所以没有这里所说的临时文件)
使用 git rebase
合并之后的状态如下:
需要注意的是,使用 rebase 有一个原则 —— 只变基你尚未提交的代码,变基你提交到公共版本库的代码可能会影响其他人。(主要在于会弄乱项目的提交记录)
以上就是合并不同分支代码的几种方式,这几种方法并没有好坏之分,关键看你的场景以及团队喜欢什么样的方式。
场景 2:开发进行到中途时临时加塞了紧急任务怎么处理?
有时,当你在进行某个版本迭代的中途,突然发现一个线上 bug 需要紧急修复。恰好这时候你的工作目录已经处于一个“脏”的状态,你不想把这个“脏”的状态作为一次 commit 进行提交,但是此时又需要切到其他分支去处理线上 bug。
那这种情况怎么进行处理呢?答案就是 git stash
命令。
git stash
命令用来临时地保存一些还没有提交的工作到贮藏区中,并且可以在任何时候从贮藏区中重新应用这些改动(甚至是不同分支)。
(ps: 这里再说明一点,贮藏区和暂存区不一样,暂存区是一个保存了下次将要提交到版本库的文件列表信息的文件,其内容存储在.git/index
目录下,而贮藏区(stash)是保存工作目录“脏”状态的一个栈。其内容存储在 .git/refs/stash
目录下。)
下面来看相关 demo,进入 demo 项目并在 develop 分支的工作区上修改 index.html 文件,这时候我们通过 git status
查看状态:
这时候,我们不想 commit 这个修改,但是又想切换到 master 分支去修改一些问题,我们尝试切换分支:
可以看到,此时 git 中止了你的切换,因为执行这个操作会丢失当前未追踪的文件,我们此时不希望丢失这个文件,(如果你无所谓是否丢失文件,可以是用git checkout -f branch-name
命令来强制切换分支)所以我们需要通过git stash
先将当前未追踪的这些文件贮藏起来。
这时候我们再切换到 master 分支,发现已经成功了。
在 master 分支修改完 bug 并提交之后,我们可以继续回到 develop 分支开发相关功能。此时需要合并 master 的修改(有冲突解决冲突),之后我们可以把之前存储在贮藏区的功能恢复,继续进行开发。
我们使用 git stash list
命令查看贮藏区的内容,如果此时有多条贮藏,都会被列出来。
接着我们使用 git stash pop
命令恢复之前贮藏起来的内容, 并从 stash list 中删除掉这条记录。(如果你不想从贮藏区中删除这条记录,可以使用 git stash apply
来进行恢复,后面想删除时再使用 git stash drop xxx
进行删除)
以上就是 git stash
相关的操作,它完美解决了我们上面提到的这个问题,另外需要注意的是,它和暂存区不同,不要把两者弄混。
场景 3: 不小心引入了一个大文件,如何在 Git 仓库历史中把它删除?
有时,我们在不经意间可能会往代码库中提交一些 size 特别大的文件(比如一些字体文件,大图片等等),这些文件之后并没有真正在项目中被用到,与此同时这些大文件的存在会导致整个 git 仓库的容量暴增,影响每次 clone 和 Fork 代码仓库的时间。
那如何清理 git 仓库历史中存在的大文件呢?
我们来看下相关 demo(为了演示方便,事先往 demo 项目中放了一张 2M 左右的大图片)
先通过 git count-objects
命令查看此时 git 仓库的大小,发现此时仓库的大小超过了 2M。
现在假设我们不知道到底是哪个文件导致了 Git 仓库容量变大,我们需要先从 Git 仓库中找到这个大文件相关的信息。
首先我们通过 git verify-pack
命令查看此时 pack 文件包的相关详细信息,并将返回的信息通过文件大小进行排序,这里只列出文件大小最大的 3 项。
从上述返回结果可以看到,这个占用内存最大的对象是最底部的这个 hash 为 ad1675 的对象,其占用了 2MB 多的内存空间。
现在我们知道了这个大文件的 blob hash,现在我们要通过 blob hash 查找到该文件的相关信息。
你可以使用 git rev-list
列出该对象更详细的信息。
到现在我们已经找到了这个大文件对应的文件名,之后我们可以通过 bfg 工具来进行后续的操作。(MacOS 用户可以通过 homebrew 下载 bfg)
执行完之后,git 历史中关于该文件的内容都不存在了。根据 bfg 的提示,这时候可能还没有完成真正的物理删除,需要执行下列命令:
查看一下此时 git 仓库的大小,发现之前的大文件确实被彻底的删除了。
至此就完成了在 Git 仓库中删除某个大文件这个需求,同样你可以把它用在删除 git 仓库中一些敏感信息文件的场景上。这里再说一下为什么不能用 git rm
进行删除。因为 git rm
只能删除工作区和暂存区的内容,不能删除仓库历史记录中的内容。
场景 4:使用默认的 git log 看信息看的不舒服,有什么其他的个性化选项么?
在提交了某些更新,或者刚克隆了某个项目之后,通常你都会想看看版本库的一些历史提交信息,一般情况下,我们都直接使用 git log
命令进行查看。
如下:
在默认情况下,git log 会按时间的倒序排列所有提交,最近的提交放在最前面。同时会把每个提交对应的 sha-1 值,以及作者信息,还有提交日期及提交说明信息也一并列出来。
有时候我们可能不需要这么多信息,有时候我们又会觉得这些信息还太够,那能不能个性化的展示相关的提交信息呢?
答案当然是肯定的, git log
提供不同的选项参数帮助你个性化的展示想要查看的提交,下面我们介绍一些常见的选项
当你想查看每次提交引入的差异时,可以使用 -p
参数,其效果如下:
可以看到,该选项除了显示默认的一些基本信息之外,还加上了每次提交对应的变化。
如果你觉得不需要查看这么详细的变化信息,只想看看哪些文件改动过了,这时候你可以使用 --stat
参数进行查看,这个选项仅仅只会展示一些概括性的总结信息,这里还使用了 -n
参数,当 n=1 时该参数表示仅展示最近 1 次的提交。(不同参数可以合并使用)
上面介绍了一些查看详细提交信息的方法,那有时候大家又希望仅仅只查看一些简单的提交信息,这时候我们可以使用 --oneline
参数。
可以看到,使用 oneline
参数进行输出的信息就简洁很多,仅仅只有提交相关的 hash 值及提交相关的 message 信息。
还有些小伙伴可能喜欢通过图形化的形式查看 git 的提交记录,当然 git 也能很好的进行支持,当使用 --graph
参数,就可以很形象的展示你的分支、提交及合并的历史(特别是和 oneline 结合时)。
最后介绍一个常用来作为搜索内容的参数 —— -S
。它在 git 中俗称“鹤嘴锄”选项,这个参数很有用,它可以告诉你哪一项内容是什么时候存在或者引入的。
可以看到在 17a0945cb0 这个提交中引入了“这里”这行文本。
除了上面介绍过的之外,git log
还提供了很多其他有用的参数个性化展示你需要的信息。比如 --pretty, --since, --before
等等,有兴趣的同学可以在官网查看更多用法。
场景 5:修改提交完文件后发现有问题,能吃后悔药么?
在日常开发过程中,可能会经常会碰到下列情况:
在工作区修改了某些文件,现在发现有问题了,想撤销这些修改,让工作区回到上一个版本的状态。
修改的内容已经加到暂存区,发现有问题,想把暂存区的内容撤销。
修改的内容已经 commit 了,发现有问题,想把之前的提交撤销掉。
针对以上三种情况,分别可以用 git reset
和 git checkout
来进行实现。
下面以 demo 项目为例来演示相关的场景:
(1)撤销工作区的修改
修改工作区的 index.md 文件,在最上方新增一行字“# 新增内容”。
这个修改没有加到暂存区,现在不想要了,我们可以通过执行git checkout --index.md
命令把 index.md 恢复成和上一次暂存区一样。
如果修改了多个文件想撤销,可以执行 git checkout .
,该指令可以把整个工作区还原成暂存区一样。(当然还有其他方法可以还原,比如使用git reset --hard xxxx
来强制还原整个工作区,但这样做的风险比较高。)
(2) 修改的文件已经添加到暂存区,怎么撤销修改?还是修改工作区的 index.md 文件,在最上方新增一行字 "新增测试文案",这时候我们多执行一步操作,把修改添加到暂存区。
通过 git status
, 和 git diff --cached
我们查看此时暂存区文件的状态:
可以看出,此时暂存区的文件是修改之后的文件。现在我们执行 git reset HEAD
,就能把暂存区的内容还原成上一次提交的状态。
通过执行该命令,确实把暂存区的内容同步成了最近一次的提交相关的内容。
(3)修改的内容已经 commit 了,怎么撤销提交?
最后一种情况,当我们修改了部分文件,commit 之后想要撤销怎么办? 答案还是使用git reset
语法。
现在我们修改了 index.md 文件,并进行了 commit 操作,此时我们查看提交记录
现在我们想撤销最后一次 da5d1b6 的提交,把版本库回退到 66a22b3。此时可以执行git reset 66a22b3
。
再次查看 git 日志, 发现此刻的 HEAD 指针成功回到了 66a22b3 这个提交。
此时,如果发现回退有问题,还想回到 da5d1b6 这个提交,可以使用 git reflog
查看所有提交的历史信息。
这里我们可以找到 da5d1b6 这个提交,使用 git reset da5d1b6
命令,我们又能让版本库回退到 da5d1b6 的状态了。
(⚠️注意:git reset 有三种用法 git reset --soft 仅把版本库中的版本进行回退,暂存区和工作区不动。 git reset 回退版本库和暂存区,工作区不动。 git reset --hard,回退暂存区,版本库和工作区,这个命令比较危险,如果你工作区和暂存区有未提交的文件,可能就丢失了)
想了解更多版本回退相关的信息可点击这里。
场景 6: 如何查看工作区/暂存区的内容变更?
有时候在你想把工作区修改的代码提交到暂存区之前,你可能会想看看此时工作区相对于暂存区做了哪些修改。又或者当你想把暂存区的文件提交到版本库时,你不清楚到底此时暂存区的文件和版本库里又有哪些区别?
在这种情况下,你就可以通过git diff
命令来查看不同区域的差异。
比如查看工作区与暂存区的差异(git diff 默认的做法),暂存区与最后提交之间的差异(git diff --cached),或者比较两个提交记录的差异(git diff master branchB)。
以 demo 项目为例,先修改工作区 index.html 相关的内容,此时想把修改的内容提交到暂存区,在提交之前就可以使用 git diff
来查看暂存区和工作区有差异的地方,以免产生错误提交。
确认无误后把修改的工作区内容提交到暂存区,此时想更近一步把暂存区的内容提交到版本库中,同样的,为了避免错误的提交,现在可以使用 git diff --cached
来查看暂存区的内容和版本库中最新一次提交之间的差异。
以上就是通过 git diff
和git diff --cached
查看工作区和暂存区以及暂存区和版本库之间差异的过程。
最后再介绍一下三点语法,这是一种很便捷的查看差异的语法 —— 通过把 ...
置于另一个分支名后来查看...
后面的分支基于...
前面分支的修改内容。
以上 demo 可以很便捷的查看 feature-a 分支基于 master 分支所有的更改内容。
场景 7: 想在代码提交时,干点不一样的事情,咋办?
通过 git 提供的一些 hooks,能在代码提交的各个不同阶段控制提交的过程。
Git hook 分为客户端 hooks(Client-Side Hooks)和服务端 hooks(Server-Side Hooks),下面列出了所有可以触发 hook 的时机:
Client-Side Hooks
pre-commit: 执行 git commit 命令时触发,常用于检查代码风格
prepare-commit-msg: commit message 编辑器呼起前 default commit message 创建后触发,常用于生成默认的标准化的提交说明
commit-msg: 开发者编写完并确认 commit message 后触发,常用于校验提交说明是否标准
post-commit: 整个 git commit 完成后触发,常用于邮件通知、提醒
applypatch-msg: 执行 git am 命令时触发,常用于检查命令提取出来的提交信息是否符合特定格式
pre-applypatch: git am 提取出补丁并应用于当前分支后,准备提交前触发,常用于执行测试用例或检查缓冲区代码
post-applypatch: git am 提交后触发,常用于通知、或补丁邮件回复(此钩子不能停止 git am 过程)
pre-rebase: 执行 git rebase 命令时触发
post-rewrite: 执行会替换 commit 的命令时触发,比如 git rebase 或 git commit --amend
post-checkout: 执行 git checkout 命令成功后触发,可用于生成特定文档,处理大二进制文件等
post-merge: 成功完成一次 merge 行为后触发
pre-push: 执行 git push 命令时触发,可用于执行测试用例
pre-auto-gc: 执行垃圾回收前触发
Server-Side Hooks
pre-receive: 当服务端收到一个 push 操作请求时触发,可用于检测 push 的内容
update: 与 pre-receive 相似,但当一次 push 想更新多个分支时,pre-receive 只执行一次,而此钩子会为每一分支都执行一次
post-receive: 当整个 push 操作完成时触发,常用于服务侧同步、通知
Git 的钩子都放在 .git/hooks
目录下,在新建一个 git 仓库时,Git 已经在这个文件夹下给我们生成了很多个 .sample 后缀的钩子,当想运行这些钩子时只要把 .sample 后缀去掉就可以了,我们也可以在这些 sample 上面修改逻辑,以此来定义我们自己的钩子。
下面来看个例子,找到我们 demo 项目里的 .git/hooks/commit-msg.sample
目录。这个是 commit msg 信息相关的钩子,在提交 commit 信息时触发。
现在我们简单做点修改,新增一行测试代码,并且把 commit-msg.sample 文件的 .sample 后缀去掉。如果生效的话,在进行 commit 的时候,就会触发这个钩子,在终端输入 call commit msg hook
。
接下来,修改文件,然后输入 commit msg 进行提交,发现终端里确实输出了 call commit msg hook
这行内容。
可以看到,我们自定义的commit msg hook
生效了。
当然,除了这些小功能外,git hooks 能做的事情还有很多。目前应用广泛的 husky 以及很多 ci/cd 的工具都是以 git hooks 为基础进行构建的。
场景 8:在经历好几个迭代周期后,发现某个功能突然失效了,我能怎么样快速定位它是什么时候出问题的呢?
有时,当你刚刚发布一个新版本,你就会接到一些 bug 的反馈,这些 bug 影响的功能你好像很久都没有动过。不知道到底是什么原因导致的这些问题,也不知道是什么时候引入的 bug。
这时候你就可以用 git bisect
来帮你快速有效的定位问题出现的地方。bisect 命令会对你的提交历史进行二分查找来帮助你尽快找到是哪一个提交引入了问题。
下面来看下相关例子,当前 demo 项目中 index.html 的第一行变成了 “He1lo Chris”,但是我们好像记得之前是 “Hello Chris”,我们现在就用 git bisect
来查找一下到底是什么时候改变了这个内容。
首先我们要找到一个确定没有出现 bug 的 commit 点,接着以这个点为参照,开始下列查找的过程。
这时候我们发现,index.html 的内容还是 He1lo,那此时我们作一个 bad 的标记,表示这时问题依然存在。
在标记完之后,git 继续做二分查找,这时我们发现 index.html 的内容变成了 Hello,我们发现它是对的,此时我们对它做一个 good 的标记,表示问题此时没有出现。
这时候还没完,因为 git 要标识出这个问题第一次出现的地方,这时候我们查看 index.html 的内容,发现此时依旧是 Hello,我们继续做标记
至此,git 就帮助我们找到了出现问题的地方——commit hash 为 83209999d3b5efad 的提交引入了这个问题。
这个时候我们来验证一下是不是这样
发现确实是这次提交引入的该问题,到这里我们就使用 git bisect
命令帮助我们快速查找到了引入 bug 的地方。
最后,还要记得使用git bisect reset
命令退出查错过程,回到最近一次的代码提交。
通过git bisect
找到错误引入的地方后,我们现在就可以很轻松的着手去修复相关错误了。
场景 9: 在提交最近一次 commit msg 的时候手快写错了,有什么方式能快速进行修改么?
有时候我们会因为手快写错 commit 信息,那么这个时候 git commit 的 --amend
选项就派上用场了。
来看下相关的例子,我们在 demo 项目最近一次的提交中,把 commit msg 信息写错了。
接下来我们使用下列命令进行修改
修改完之后,再次查看 commit 记录,可以看出之前错误的提交信息已经被新的提交信息替换掉了。
(git commit --amend 不仅可以修改最近一次提交相关的 msg 信息,同样也能修改最近一次提交相关的内容,这里不多赘述,感兴趣的朋友自己去研究)
需要注意的是,这个操作最好是在还没有把提交推到远端的情况下进行,如果已经推到了远端,需使用git push -f origin xxx
强制覆盖远端仓库,但是不建议这样做。
场景 10: 除了上面说的修改最新一次的 commit 记录,如果有其他修改 commit 的需求——比如修改一条旧的 commit 记录,或者修改多条不连续的 commit 记录时应该怎么办呢?
有些开发者对简洁明了的提交记录有追求,往往会在把代码推送到远端之前,美化一下自己的 commit 记录。这时候就需要修改 git 历史的一些 commit 记录。
我们可以通过之前提到的 git rebase
来实现这些需求。
下面以 demo 项目为例分别为大家演示以下几种修改历史 log 记录的场景:
(1)修改历史中的某一条 commit 记录
先查看一下 demo 项目的提交历史
现在想把 eb9834d 这次提交的 commit 内容修改成 "feature-b 的第二次修改 md"。
使用 git rebase -i c9d031b
(这里变基的对象选择为要变内容的父对象)
这时候会弹出如下内容,找到我们想要修改的 eb9834d , 把其前面的 pick 改成 r(表示使用提交,但要修改提交说明),之后 wq 保存退出。
接下来会出现下面的界面
在这个界面把 commit msg 修改一下之后保存
此时操作已经成功,再查看 commit 历史,发现 eb9834d 的内容已经更新了。
(2)把历史中的多个连续 commit 合并成一个
现在来看看如果把历史多个 commit 进行合并有什么办法?假设我们想把 c9d031b 这个提交和 bb7cdf2 这个提交合并成一个提交。
同样的,还是使用 git rebase
命令。
此时,弹出如下界面,把 bb7cdf2 这次提交前面的 pick 修改为 s。即把 bb7cdf2 和 它的上一次提交 c9d031b 进行融合。
完成之后保存退出。弹出下一个界面,让你输入合并之后的信息。
输入完之后保存退出,此时 git 提示变基成功,
再次查看 git 的提交历史
可以看到之前的 c9d031b 和 bb7cdf2 已经融合成一个新的提交记录 34f9c82。
(3)把历史中不联系的多个 commit 合并成一个
现在看看最后一种场景,把 d439ef2 和 7f2fc01 这两个不连续的提交合并成一个提交。
还是使用 git rebase
和之前一样,此时会弹出如下界面:
这个时候,我们需要把 d439ef2 这次提交手动移到 7f2fc01 的寿面,并把 pick 改成 s。
此时保存退出,接下来会出现如下界面,需要你修改合并之后的信息,输入如下内容后保存退出。
此时 git 提示变基成功。
再看看此时的提交记录, 发现 d439ef2 和 7f2fc01 已经合并成一个新的 commit 7ef7ced。
至此,如何修改 git 历史提交记录已经讲完了。那关于这部分内容还是要强调一点,只在你本地的代码进行变基,如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基,不然会对他人产生很大的影响。
总结
本文主要从日常的工作场景出发,首先介绍了不同分支合并代码的三种方法 —— git rebase
, git merge
, git cherry-pick
之间的区别。
之后就是介绍如何使用 git stash
来储藏文件,以及如何使用 bfg 这个工具来删除 git 历史中的无效大文件。
接下来我们又介绍了 git log
的个性化使用方法以及在 git 中使用“后悔药”的几种方式—— git checkout
和 git reset
。
再之后就是如何使用 git diff
更好的查看暂存区/工作区文件的变更信息。以及如何使用 git bisect
辅助查找程序中引入的 bug。
最后还讲到了如何使用 git hooks 来自定义 git 的提交过程以及如何使用 git rebase
和 --amend
命令来修改 git 历史中的提交信息。
以上就是整篇文章的所有内容,如果有任何问题,欢迎大家在评论区进行讨论。
参考文章
版权声明: 本文为 InfoQ 作者【淼💦 淼】的原创文章。
原文链接:【http://xie.infoq.cn/article/954f9b32d7880bcb514184aba】。文章转载请联系作者。
评论