下面的一些 git 命令,在使用时常常不知所以,容易混淆,其实原因是对 git 的机制并不了解,因此在本文中介绍相关。
reset hard/soft/mixed
1
2
3$ git reset --hard
$ git reset --mixed
$ git reset --softfetch/pull
1
2$ git fetch
$ git pullreset/revert
diff cached/HEAD
1
2
3
4
5$ git diff --cached
$ git diff --staged
$ git diff HEAD
$ git diff HEAD~1
$ git diff HEAD^rev-list/log
基本概念图
Git的工作区域
注意,图中的 Stage 和 Index 是同一个东西。
下图中,最后一个diff
应该为diff --cached
注意,Git 中的 branch 实际上可以看做是一些列 commit 的集合。commit 是按照 repo 全局的,这可以最大程度实现复用。
HEAD
HEAD实际上是指向当前分支的指针。观察.git/HEAD
,可以发现
1 | ref: refs/heads/master |
接着打开.git/refs/heads/master
,可以发现
1 | fc30ecc0525b71b0f6bf1ce20fe793aa731ae66c |
执行git log
,可以发现存在指向性关系HEAD -> master -> fc30ecc
1 | git log |
容易想到切换分支指令git checkout
就是通过改变HEAD来实现的。
指针算术
HEAD^1
和HEAD~1
的区别
基础配置
使用vim
1 | git config --global core.editor vim |
支持多个SshKey
在一台机器上使用多个SSH Key
1 | ssh-agent bash -c 'ssh-add ~/.ssh/id_rsa_xxx; git clone git@github.com:yyy.git' |
在提交的时候,需要署名为自己,此时需要
1 | git config user.name xxx |
如果之前提交过了,需要
1 | git commit --amend --author "xxx <xxx@yyy.com>" |
状态检查命令
git log
1 | git log remotename/branchname |
介绍几个比较舒服的 git log
1 | git log --pretty=format:"%C(auto)%ad %h %<(10,trunc)%C(dim white)%an %C(auto)%d %s" |
只显示 commit
假设有两类 commits:
- 第一类是普通的 PR,然后 squash merge 产生的
- 第二类是 merge upstream 产生的,同时会保留了 upstream 上的提交记录
在 cherry pick 的时候,会希望能够筛选出第一类的 pr,此时可以使用
1 | git log --no-merges --first-parent |
git status
git rev-list
基础功能是按照 chronological 倒序列出 commit。【Q】这里的 chronological 指的是 commit 时间,还是链接之间的关系呢?
git rev-parse
git show
git diff
介绍
比较两个分支的文件
1 | git diff commit1 commit2 -- path/to/file |
结果如下所示,我们比较的是 a 和 b 两个 commit 中的文件 a。其中左边分支的内容是2,右边分支的内容是1。
可以看到,左边有右边没有的是-
,右边有左边没有的是+
。
站在右边的角度,-
相当于自己删除了什么,+
相当于自己增加了什么。
一般来说,左边设置为“较旧”的代码状态,右边设置为“较新”的代码状态。
1 | $ git diff 3fea159f a02cad -- a |
实验:有关diff
我们进行如下操作:
- init仓库,新建文件1.txt
- 变更1.txt的内容为2,并add+commit
- 变更1.txt的内容为3,并add,不commit
- 变更1.txt的内容为4,不做任何操作
下面进行检查:
git diff --cached
比较Index和HEAD1
2
3
4
5
6
7
8
9
10$ git diff --cached
diff --git a/1.txt b/1.txt
index d8263ee..e440e5c 100644
--- a/1.txt
+++ b/1.txt
@@ -1 +1 @@
-2
\ No newline at end of file
+3
\ No newline at end of filegit diff
比较working directory和Index1
2
3
4
5
6
7
8
9
10$ git diff
diff --git a/1.txt b/1.txt
index e440e5c..bf0d87a 100644
--- a/1.txt
+++ b/1.txt
@@ -1 +1 @@
-3
\ No newline at end of file
+4
\ No newline at end of filegit diff HEAD
比较working directory和HEAD1
2
3
4
5
6
7
8
9
10$ git diff HEAD
diff --git a/1.txt b/1.txt
index d8263ee..bf0d87a 100644
--- a/1.txt
+++ b/1.txt
@@ -1 +1 @@
-2
\ No newline at end of file
+4
\ No newline at end of filegit diff HEAD --cached
比较Index和HEAD1
2
3
4
5
6
7
8
9
10$ git diff HEAD --cached
diff --git a/1.txt b/1.txt
index d8263ee..e440e5c 100644
--- a/1.txt
+++ b/1.txt
@@ -1 +1 @@
-2
\ No newline at end of file
+3
\ No newline at end of filegit diff/apply
将HEAD到working的修改输出到patch文件。然后我们回退到HEAD并apply发现会恢复到working中的结果。当然,reset会导致Index区被清空?1
2
3$ git diff HEAD > patch
$ git reset --hard
$ git apply patch那么我们是否可以用HEAD到working,发现会报错
1
2
3
4
5$ git diff HEAD > patch
$ git reset --mixed
$ git apply patch
error: patch failed: 1.txt:1
error: 1.txt: patch does not apply
checkout相关
切换分支使用git checkout
指令。加上-b
参数可以基于当前分支创建新分支并切换,相当于整合了git branch
命令。
checkout 的指令和 reset 指令有些类似
我们还可以指定某个文件进行checkout,其中-q
表示quiet。
1 | git checkout [-q] [<commit id>] [--] <paths> |
如果我们需要把Index/Stage里面的东西checkout到工作目录中,可以
1 | git checkout-index -a -f |
其中-a
表示全部文件,-f
表示会强制覆盖已存在的文件。可以看出git对所有涉及变动本地目录的操作都很谨慎。
加上-- <path>
就可以checkout单个的文件。需要注意的是,现在checkout就可以直接从Stage/Index检出单个文件了。
1 | git checkout -- 1.txt |
fetch相关
拉取所有分支信息
1 | git fetch |
拉取 upstream/master
1 | git fetch upstream master |
rebase相关
见专门文章。
merge相关
git merge 用法
我们可以指定不同的 merge 策略,如下面使用 recursive 策略合并。
1 | git merge origin/master -s recursive |
我们还可以指定不同的diff-algorithm
,如下面使用patience
策略就能够产生更加优雅的合并结果,例如更好地匹配大括号。
1 | git merge origin/master -X diff-algorithm=patience |
Merge 的策略
Git使用的是三路合并,分别是要合并的两方a和b,和这两方的共同祖先c。通过和共同祖先比较,能够自动解决一些冲突,原因如下:
- 假如a对c中的某个文件1.txt进行了修改,而b没有。如果直接合并a和b,我们并不知道是否该采用这个修改;
- 但如果我们参照c去合并a和b,就可以发现b对c上的1.txt并没有修改,所以应该接受a对1.txt的修改。
Recursive
Recursive是默认策略,这种策略只能同时合并两个分支,如果需要同时合并多个分支,就需要反复进行两两合并。这里反复有点奇怪,难道不是n-1
次么?接着往下看。
显而易见,在合并时,我们序号回溯两个分支A和B的共同祖先,从而确定解决冲突的起点。但在Git中,两个分支可能存在有多个共同祖先,即Criss-Cross现象。我们考虑下面的操作方式:
- 在master上提交c0
- 在master上checkout出分支feature1,并在feature1上提交c1
- 在master上提交c2
- 在master上checkout出分支feature2
- 在feature2上merge分支feature1,产生提交c3
此时feature2(指向c3)的祖先是master(指向c2)和feature1(指向c1)
1 | git init |
git log 查看一下
1 | $ git log |
接着
- 在 master 上提交 c4
- 将 feature1 合并到 master,产生提交 c5
1 | git checkout master |
最后一个动作会导致Criss-Cross现象。我们来分析一下合并之后的情况:
- feature1目前指向c1,c1的祖先是c0。
- feature2目前指向c3,它的祖先是c2和c1。
- master目前指向c5,它的祖先是c4和c1。由于c4的祖先是c2,所以feature2和master有两个共同祖先c1和c2。
我们可以看到git merge-base --all
的输出是两行
1 | git merge-base --all feature2 master |
git log graph 会更清楚
1 | * cb41364 (HEAD -> master) Merge branch 'feature1' |
下面,我们尝试合并master和feature2。Recursive策略是,首先合并master和feature2的共同祖先,即c1和c2,得到一个虚拟祖先,然后在进行合并。
Resolve
Resolve 策略是 Recursive 出现之前旧的合并策略。
Ours 和 Theirs
需要注意,此时的 checkout 只用来解决已有冲突的文件。如果某文件已经被 auto merged 了,那么 checkout theirs 或者 ours 就会失去“作用”。
以我遇到过的这个例子作为说明:components/resolved_ts
这个目录因为冲突太小已经被自动合并了,所以git checkout --theirs
实际就没有一个 theirs。
Octopus
这个策略能够同时合并多个分支,但是如果出现需要手工解决的冲突,就会失败。
Merge 的 diff-algorithm
Merge 策略一般有Squash Merge、Merge Commit 和 Rebase Merge:
- Squash Merge 将所有的 commit 压缩成一个 commit 提交。
- Merge Commit 会创建一个 commit,带上自己分支所有的 commit 进入 master。
- Rebase Merge 相当于自动地帮你把自己分支的 commit 一个一个提交到 master 上。
如下所示,Squash Merge 之后,会丢失自己分支上每一行的 git blame 信息。
1 | mkdir learn_git |
Fast Forward
此外,Merge Commit 还有 ff 和 no ff 的策略。具体来说就是当自己的分支比 Master 要新的时候,是否创建一个 Commit。如下所示,使用 no ff 策略会产生一个空的 Merge commit,你从中diff 不出来任何内容,但事实上 ab0d86ddb766b5e564b0b8eff18a4a81b1334954 相比上次的 HEAD 是有变更的。
1 | calvin@CalvinPC learn_git % git log |
如何判断可以 Fast Forward 合并呢?一个 idea 是
1 | A=$(git rev-parse --verify A) |
git merge-base
在先前我们已经了解了部分 merge-base 命令的使用。
fork-point
根据介绍,假如我们从origin/master
分支位于 b0 时 fork 了一个 topic
分支出来,但随后 b0 又被 rebase 掉了,这个命令能够方便这种情况。
1 | o---B2 |
我们会遇到什么问题呢?因为 b0 被干掉了,所以下面的命令会返回一个更早的 commit
1 | git merge-base origin/master topic |
通过
1 | $ fork_point=$(git merge-base --fork-point origin/master topic) |
可以在 B 上面重新 rebase
1 | o---B2 |
在 merge 的时候是否可以切换分支?
reset相关
git reset命令修改branch,即指向最新commit的指针HEAD,并不修改任何commit。reset可以带有三个参数:
--hard
参数会改变HEAD、index(stage)和working directory--mixed
参数会改变HEAD、index(stage)--soft
参数只会改变HEAD
常见用法
我有一个文件被git add了,现在我想要从staging里面移出这个修改,怎么做?
下面这个操作能够将它移出staging1
git reset --mixed file
注意,我们通常喜欢使用
git rm --cached
,但是他未必是万精油,例如你可能遇到这个错误1
2error: the following file has staged content different from both the
file and the HEAD:现在我想在working dir中也取消这个修改,怎么做?
git reset --hard
似乎不能对单个文件使用。
这里可以1
git checkout file
实验:有关reset
Step 1
本实验说明soft reset不会改变working directory。
首先我们在空目录执行
1
git init
我们创建a.txt,并且填写其内容为
1
我们执行
1
2
3git add .
git commit -m"a=1"
git log得到输出
1
2
3
4
5commit 6bd3de7a2b1637dcb686f72af415c5d48f4d5dc2 (HEAD -> master)
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Fri Oct 30 22:46:37 2020 +0800
a=1修改a.txt的内容为
2
执行
1
git reset --soft
查看a.txt的内容为
2
这说明a.txt没有被reset掉
Step 2
本实验说明soft reset不会改变index
此时,a.txt的内容仍然为2
执行
1
git diff --cached a.txt
可以看到,a.txt已经被提交到了index里面
1
2
3
4
5
6
7
8
9diff --git a/a.txt b/a.txt
index 56a6051..d8263ee 100644
--- a/a.txt
+++ b/a.txt
@@ -1 +1 @@
-1
\ No newline at end of file
+2
\ No newline at end of file执行
1
git reset --soft
检查a.txt的内容仍然为2
Step 3
本实验说明soft reset能改变Repo
此时a.txt的内容仍然为2,执行
1
git commit -m"a=2"
检查git log
可以看到,a=2
已经进入了Repo1
2
3
4
5
6
7
8
9
10
11commit c0a4d93cb05eb39e149ff50d9b7e54257a51234b (HEAD -> master)
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Fri Oct 30 23:03:53 2020 +0800
a=2
commit 6bd3de7a2b1637dcb686f72af415c5d48f4d5dc2
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Fri Oct 30 22:46:37 2020 +0800
a=1执行
HEAD~1
表示HEAD
向前一个版本。1
git reset --soft HEAD~1
检查git log
发现a=2
的提交被回退了1
2
3
4
5commit 6bd3de7a2b1637dcb686f72af415c5d48f4d5dc2 (HEAD -> master)
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Fri Oct 30 22:46:37 2020 +0800
a=1检查a.txt
发现内容还是2,没有变。说明即使回退了Repo,也不会改变工作区。执行
1
git checkout
检查a.txt
发现值内容还是2,这个和图2似乎有矛盾。其实应该要加一个-f
1
git checkout -f
bisect相关
实验:有关bisect
Linux内核易于维护的一个原因就是因为Linus要求每一个commit只做一件事,所以他能够通过git bisect
快速地二分出错误的提交。
执行下面语句,得到10个提交
1 | git init |
我们的目标是找到第一个打印出大于等于5的错误提交。
我们写一个predicate脚本
1 | # test.sh |
执行下面语句,自动查找到第一个故障提交
1 | git bisect start |
下面执行git bisect run
,可以得到以下输出
1 | $ git bisect run ./test.sh |
需要注意,如果某个分支不能被test,应当使用git bisect skip来跳过,或者在bash脚本里面返回125
实验:如果涉及merge呢
其实我们还是在一条线上bisect
1 | echo 1 > a |
Pull Request相关
基础
Fork 项目 A 到自己的 Repo,并 Clone 自己的 Repo 到本地
在本地设置项目 A 为一个叫 upstream 的 remote,并且 fetch
1
2git remote add upstream A
git fetch upstream从项目 A 中创建一个新分支
非常不推荐在本地的 master 上修改,这样的坏处是没有办法及时同步远程分支到本地的 master。
类似从 origin 远程分支的方案1
git checkout -b issueXXXX upstream/master
修改
将某个分支和 upstream 同步
这个通常发生在开发分支 me 在修改过程中,PR 目标 upstream/master 也在修改。在 PR 之前,可能需要解决冲突。
此时,可以先获取 upstream/master 到本地
1 | git fetch upstream |
然后将 me 基于 master 做 rebase
1 | git checkout me |
修改PR
- stash多个commit
- git commit –amend
取消某个历史提交
git revert
将某个分支中的某几个提交应用到另一个分支上
参考 git cherry-pick
git submodule
best practice
从 upstream 拉取 submodule
1 | git submodule update --init --recursive |
更新 submodule 到 upstream
下面的操作表示将 submodule 更新到 upstream/new_commit
版本。
1 | pushd contrib/xxx |
替换 submodule 的 repo
有的时候,我们需要将某个依赖的组件更改为自己的版本。此时可以尝试
- 更新
.gitsubmodule
中对应 submodule
注意,可能还需要修改对应的 branch - 更新
.git/config
中对应 submodule - 执行
git submodule sync
注意,如果需要修改远程分支的,那么下面的这个目录也要提交,虽然看起来是空的,但它涉及.git/modules/contrib/yyy
的一个 commit id。
1 | $ git status |
git am
我们知道,Linux 社区不是用 Github 而是基于邮件来协作的。这是如何实现的呢?就是这命令。
git clean
有的时候我们需要checkout,但是提示有untraced files。可以通过下面的命令删除掉
1 | git clean -xdf |
复杂场景
cherry-pick 多个 commit
- 【1】基于 tikv/master(位于 commitA) checkout 了一个分支 branchA,做了一些修改
- 【2】其他 contributor merge 了一些 PR,tikv/master 更新到了 commitB
- 【3】基于 tikv/master(位于 commitB) checkout 了另一个分支 branchB,然后重复下面的操作:
- 【3a】往 branchB 中添加一些我的代码
- 【3b】branchB merge tikv/master
- 【4】于是现在 branchB 和 tikv/master 的 merge-base 是 commitBN 了
现在希望将 branchB 相对于 commitBN 的 diff(也就是 3a 中添加的所有代码)应用到 branchA 上,然后有几个方案:
- merge/rebase 发现存在一堆和 3a 不相关的冲突,根本无法操作
- git cherry-pick 一系列修改,但因为中间夹杂了很多 3b 操作,导致需要 cp 好多次
- git diff + patch
git cherry-pick 一系列修改
可以用下面的方案,但因为 cherry-pick 容易因为冲突而失败,所以最好在一个脚本里面处理,方便遇到失败就退出。
1 | git rev-list --ancestry-path $from..$to | tac | xargs git cherry-pick |
git diff + patch
需要使用下面的方式,将 a/ 和 b/ 去掉。
1 | git diff $from $to --no-prefix > patch.txt |
常见错误
unexpected disconnect while reading sideband packet
如下的错误基本上是 SSH 的问题,直接 git remote add 一个 SSH 方式而不是 HTTPS 方式的仓库即可。
1 | error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: CANCEL (err 8) |
ERROR: You’re using an RSA key with SHA-1, which is no longer allowed. Please use a newer client or a different key type.
Please see https://github.blog/2021-09-01-improving-git-protocol-security-github/ for more information.
老版本的 git 会有这样的问题,需要用下面的方式生成一个新 key
1 | ssh-keygen -t ecdsa -b 521 -C "your_email@example.com" |
然后借助 ssh-agent
1 | ssh-agent bash -c 'ssh-add ~/.ssh/id_ecdsa; git push' |