Git进阶
剖析Git Commit
Commit中有什么内容?
使用 git show <commit-id> 可以看到一条commit的详情:
如下所示:
1commit e9aff092d05003238728dcc13f5c4de590b3b2e7 (HEAD -> master) 2Author: Yudu Ban <banyudu@gmail.com> 3Date: Sun Dec 22 19:46:56 2019 +0800 4 5 update readme 6 7diff --git a/README.md b/README.md 8index 84d7acb..e8afba9 100644 9--- a/README.md 10+++ b/README.md 11@@ -1,5 +1,5 @@ 12 This is a demo project. 13 14-Another line. 15+Hello world 16 17 Good bye!
可以看出其包含如下几项内容:
- 作者信息
- 日期信息
- commit message
- 更改详情
git仓库是由一系列的commit组成的,从文件内容上来说,相当于它是由一长串的 diff 组成的。
把commit理解为带有作者信息的diff,就可以较方便地理解很多git操作的原理了。
如何将一个commit应用到别处?
cherry-pick
cherry-pick是一个很方便的功能,它可以单独合并一个commit,而非整个分支。
在合并整个分支和手动再修改一次之间,cheery-pick提供了一个更方便可靠的方式,来实现跨分支的单功能代码合并。
使用方式:
git cherry-pick <commit-id>
按上面的说法,将git commit理解成一个diff,cherry-pick的过程就是找到具体的diff,然后应用到目标分支中,这个过程有可能产生冲突,像正常合并分支一样解决就可以了。
需要注意的是,在git的内部存储中,并不是用diff来存储的,而是用文件快照。如果用diff来存储,每次切换分支或commit时就会面临大量的diff处理,性能会有影响。
patch
patch也是一种保存更改列表的方式,里面保存的信息类似于commit,但略有不同。
patch常见于大型开源项目,尤其是不托管在github、gitlab等平台的,它可以实现不依赖平台的 Merge Request,有些项目(如linux内核)通过email来处理patch。
要生成一个 patch,可以用多种方式:
- git format-patch
- 指定一个commit id,生成其之前的N条commit的patch:
git format-patch -<N> <Commit ID>
- 可指定一个范围,为其生成git patch。
git patch A...B
- 指定一个commit id,生成其之前的N条commit的patch:
- git diff
- 将正常的git diff结果输出至一个文件中,即可作为patch使用。
patch的内容:
1From e9aff092d05003238728dcc13f5c4de590b3b2e7 Mon Sep 17 00:00:00 2001 2From: Yudu Ban <banyudu@gmail.com> 3Date: Sun, 22 Dec 2019 19:46:56 +0800 4Subject: [PATCH] update readme 5 6--- 7 README.md | 2 +- 8 1 file changed, 1 insertion(+), 1 deletion(-) 9 10diff --git a/README.md b/README.md 11index 84d7acb..e8afba9 100644 12--- a/README.md 13+++ b/README.md 14@@ -1,5 +1,5 @@ 15 This is a demo project. 16 17-Another line. 18+Hello world 19 20 Good bye! 21-- 222.21.0 (Apple Git-122.2)
可以看到 patch 和 commit 的内容是大致相同的。
patch的应用方式
git am
可以用来将patch应用到当前项目中,并生成commit。git apply
可以将发动应用到当前仓库,不提交。
我们可以看到 patch 中是有 author 信息的,它是可以被修改的,所以理论上来说,author信息也是能伪造的(最好不要)。
.git 中都藏着什么?
1$ ls .git/ 2config description FETCH_HEAD HEAD hooks index info logs objects ORIG_HEAD packed-refs refs
config
config文件中存储了当前项目中的配置信息
1[core] 2 repositoryformatversion = 0 3 filemode = false 4 bare = false 5 logallrefupdates = true 6 symlinks = false 7 ignorecase = true 8[remote "origin"] 9 url = git@github.com:banyudu/demo.git 10 fetch = +refs/heads/*:refs/remotes/origin/* 11[branch "master"] 12 remote = origin 13 merge = refs/heads/master 14[user] 15 name = banyudu 16 email = banyudu@gmail.com
Git会从多处获取配置文件,并逐层覆盖。顺序是
1graph LR 2global["全局配置 (/etc/gitconfig)"] --> user["用户配置 (~/.gitconfig)"] --> project["项目配置 (.git/config) "]
具体的配置说明,参考 Git Scm
description
GitWeb使用的文件,一般无需关心。
The
description
file is used only by the GitWeb program, so don’t worry about it.
FETCH_HEAD
d3e8a0f81cd445a51b9bc1ed80ab0780e23a038e branch 'master' of github.com:banyudu/demo
HEAD
ref: refs/heads/master
hooks/
hooks中是一些shell脚本,在git执行某阶段操作时,如果有对应的hook脚本,也会被执行。
hooks常被用来做 pre-commit 的校验,commitmsg的校验等。
hooks目录中默认会存有一些 sample 文件。
- applypatch-msg.sample
- commit-msg.sample
- fsmonitor-watchman.sample
- post-update.sample
- pre-applypatch.sample
- pre-commit.sample
- pre-merge-commit.sample
- prepare-commit-msg.sample
- pre-push.sample
- pre-rebase.sample
- pre-receive.sample
- update.sample
在node、前端相关的项目中,可以使用 husky 管理git hooks。
index
简单来说,.git/index中存储的是通过git add暂存到git仓库,但是还尚未提交的更改。
info/
常用的文件是 info/exclude,包含自定义的exclude设置
logs/
本地的commit记录,用于 git reflog命令,可以用于安全恢复之前误删除的commit。
objects/
git内部数据对象存储。
https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
ORIG_HEAD
执行危险操作之前,git会在ORIG_HEAD中记录原来的HEAD。
现在有了 git reflog,这个文件的意义不再那么重要。
packed-refs
对 refs/ 目录的一种优化增强。
所有的分支、tag等都会在refs中有一个对应的文件,但是当tag比较多时,就会出现很多小文件,既浪费空间,又降低性能。
为了解决这个问题,git会将一些不常用的refs放在packed-refs文件中,使用一个文件统一管理。当git在refs/目录中找不到对应的项时,会再从packed-refs文件中查找一次。
refs/
分支、tag会在refs目录中对应存在一个文件,文件内容是commit id。
分支、标签管理
分支、标签的区别及各自的用途
分支和标签都可以用一条Commit及一个名称来表示。
它们的区别是:
- 分支可持续更新,标签不可以
- 分支对应于 .git/refs/heads 中的一个文件,而标签对应于 .git/refs/tags 中的一个文件。(也会共同存在于 .git/packed-refs 中)
远程仓库管理
如何在本地添加多个远程仓库
git remote add
https://help.github.com/cn/github/using-git/adding-a-remote
Fork过的工程如何进行更新?
https://help.github.com/cn/github/collaborating-with-issues-and-pull-requests/syncing-a-fork
git remote add upstream <git url>
git merge upstream/master
列举和查看远程分支、标签
git branch --remote
git ls-remote --tags
好用的功能
git bisect 二分查找
当遇到特别难定位的bug时,可以尝试使用二分查找的功能,定位到开始出问题的commit,辅助解决问题。在对项目不熟悉的时候,这个功能可能会有很大的帮助。
它的原理很简单,先选定一个开始和结束点,其中一个标记为 good(无bug),另一个标记为 bad (有bug),git会自动切换到中间的一条commit。用户再次测试后,标记当前commit是 good、bad或skip(因其它原因无法确定,暂时跳过),git会根据用户的标记再次切换到一个适合的位置。
要测试的次数取决于选择的开始和结束点之间的commit数量,及有无merge等,因为是2的次方,所以一般来说测试6、7次左右就能定位到出问题的commit了。
需要注意的是,在切换代码的同时,还需要考虑依赖版本的问题。首先每次切换后要安装对应的依赖版本(这个过程可能比较耗时),另外就是在排查问题的时候要考虑到依赖版本不一致可能会导致的问题。
git bisect支持自动化测试,需要写一些对应的脚本,但是一般只对有丰富测试用例的工程才有作用。
git blame定位相关人
git blame <filename>
可查看指定文件的每一行对应的最后一次commiter。
上文中也提过,author信息是能伪造的,所以这里的commiter不能作为完全可靠的依据,当然大部分时候不需要考虑这个问题。
最佳实践
代码合并
使用rebase代替merge
在合并两个分支时,如果当前分支的改动很小,则可以使用rebase代替merge,避免生成一条多余的merge commit。
使用 git pull --rebase 代替 git pull
原来和上面一条相同,但是更常见。
在多人协作开发同一分支时,经常会遇到git push失败的问题,原因在于远程的代码比本地的要新,需要先在本地合并远程代码。
这个时候一般来说用 git pull 就可以了,它会自动拉取远程的分支,并在本地执行merge,生成一条Merge Commit。
过程大概是这样的:
- 执行 git fetch,更新本地的remote索引,比如 origin/master
- 执行merge,相当于
git merge origin/master
然后git的HEAD就会变成
Merge branch 'master' of <远程操作地址> into master
也就是产生了一条多余的commit。
很多时候产生一条多余的commit也没什么大不了的,但是它会污染git log,而且在某些场景中(如一些持续构建),会判断最后一条commit的内容,Merge Commit会导致其误判。
而使用 git pull --rebase 的时候,过程会变成:
- 执行 git fetch,更新本地的remote索引,比如 origin/master
- 执行 rebase,相当于 git rebase origin/master
代码回滚
尽量避免使用 git push --force
首先要确定是回滚本地改动,还是回滚已经推送到远程主仓库的代码。
如果是本地改动,使用 git reset 即可,或者直接删除当前分支,再重新checkout。
如果已经推送到远程仓库了,则应尽量使用 git revert,而不是 git push --force,因为后者会导致其他人更新代码报错,需要删除本地的分支才能正常更新。而且CI/CD环境如果配置的是使用 git 动态更新而不是全新clone的话,也会报错。
分支管理模型
Git flow是一种已经被验证多年的模式,大部分项目都可以按照这种模式来开发,或者做一些微调适应具体场景。
https://nvie.com/posts/a-successful-git-branching-model/
版本发布
版本发布时,要对应地生成一个对应版本的tag,方便以后快速定位到相关的代码。
提交信息
一般来说,提交信息应简单扼要地给出改动的描述信息,如性质、模块、基本描述。
业界已有一些最佳实践,其中一种是 conventional commits。一方面它简洁易懂,另一方面也可以根据它自动生成changelog。
也可以考虑在commit信息中加入对应的issue或jira编号,使得commit可以与具体的需求或bug关联起来。
相关链接: