git基础概念和基础命令

下面的一些 git 命令,在使用时常常不知所以,容易混淆,其实原因是对 git 的机制并不了解,因此在本文中介绍相关。

  1. reset hard/soft/mixed

    1
    2
    3
    $ git reset --hard
    $ git reset --mixed
    $ git reset --soft
  2. fetch/pull

    1
    2
    $ git fetch
    $ git pull
  3. reset/revert

  4. diff cached/HEAD

    1
    2
    3
    4
    5
    $ git diff --cached
    $ git diff --staged
    $ git diff HEAD
    $ git diff HEAD~1
    $ git diff HEAD^
  5. rev-list/log

基本概念图

Git的工作区域

注意,图中的 Stage 和 Index 是同一个东西。

下图中,最后一个diff应该为diff --cached

注意,Git 中的 branch 实际上可以看做是一些列 commit 的集合。commit 是按照 repo 全局的,这可以最大程度实现复用。

HEAD实际上是指向当前分支的指针。观察.git/HEAD,可以发现

1
ref: refs/heads/master

接着打开.git/refs/heads/master,可以发现

1
fc30ecc0525b71b0f6bf1ce20fe793aa731ae66c

执行git log,可以发现存在指向性关系HEAD -> master -> fc30ecc

1
2
git log
commit fc30ecc0525b71b0f6bf1ce20fe793aa731ae66c (HEAD -> master)

容易想到切换分支指令git checkout就是通过改变HEAD来实现的。

指针算术

HEAD^1HEAD~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
2
git config user.name xxx
git config user.email xxx@yyy.com

如果之前提交过了,需要

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:

  1. 第一类是普通的 PR,然后 squash merge 产生的
  2. 第二类是 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
2
3
4
5
6
7
8
$ git diff  3fea159f  a02cad -- a
diff --git a/a b/a
index 0cfbf08..d00491f 100644
--- a/a
+++ b/a
@@ -1 +1 @@
-2
+1

实验:有关diff

我们进行如下操作:

  1. init仓库,新建文件1.txt
  2. 变更1.txt的内容为2,并add+commit
  3. 变更1.txt的内容为3,并add,不commit
  4. 变更1.txt的内容为4,不做任何操作

下面进行检查:

  1. git diff --cached
    比较Index和HEAD

    1
    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 file
  2. git diff
    比较working directory和Index

    1
    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 file
  3. git diff HEAD
    比较working directory和HEAD

    1
    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 file
  4. git diff HEAD --cached
    比较Index和HEAD

    1
    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 file
  5. git 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。通过和共同祖先比较,能够自动解决一些冲突,原因如下:

  1. 假如a对c中的某个文件1.txt进行了修改,而b没有。如果直接合并a和b,我们并不知道是否该采用这个修改;
  2. 但如果我们参照c去合并a和b,就可以发现b对c上的1.txt并没有修改,所以应该接受a对1.txt的修改。

Recursive

Recursive是默认策略,这种策略只能同时合并两个分支,如果需要同时合并多个分支,就需要反复进行两两合并。这里反复有点奇怪,难道不是n-1次么?接着往下看。
显而易见,在合并时,我们序号回溯两个分支A和B的共同祖先,从而确定解决冲突的起点。但在Git中,两个分支可能存在有多个共同祖先,即Criss-Cross现象。我们考虑下面的操作方式:

  1. 在master上提交c0
  2. 在master上checkout出分支feature1,并在feature1上提交c1
  3. 在master上提交c2
  4. 在master上checkout出分支feature2
  5. 在feature2上merge分支feature1,产生提交c3
    此时feature2(指向c3)的祖先是master(指向c2)和feature1(指向c1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
git init

echo c0 > a
git add a
git commit -m"c0"

git checkout -b feature1 master
echo c1 > a
git add a
git commit -m"c1"

git checkout master
echo c2 > a
git add a
git commit -m"c2"

git checkout -b feature2 master
git merge feature1
git add a # resolve conflict
git commit # c3

git log 查看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ git log
commit 26efcbcb040f5ccf1e72f61e37183cdc94274faa (HEAD -> feature2)
Merge: 697ff75(c2 aka master) 55641ac(c1 aka feature1)
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Thu Oct 20 22:56:35 2022 +0800

c3

commit 697ff7593dbdd08d275a2235279008c4c985ba70 (master)
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Thu Oct 20 22:55:36 2022 +0800

c2

commit 55641ac983def5857b1838912c772c3c68bf56f7 (feature1)
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Thu Oct 20 22:55:36 2022 +0800

c1

commit ce3e66776e8f58cb9b76a2073eed2ab1a4416ad9
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Thu Oct 20 22:55:36 2022 +0800

c0

接着

  1. 在 master 上提交 c4
  2. 将 feature1 合并到 master,产生提交 c5
1
2
3
4
5
6
7
8
9
git checkout master
echo c4 > a
git add a
git commit -m"c4"

git merge feature1
echo c5 > a
git add a
git commit # c5

最后一个动作会导致Criss-Cross现象。我们来分析一下合并之后的情况:

  1. feature1目前指向c1,c1的祖先是c0。
  2. feature2目前指向c3,它的祖先是c2和c1。
  3. master目前指向c5,它的祖先是c4和c1。由于c4的祖先是c2,所以feature2和master有两个共同祖先c1和c2。

我们可以看到git merge-base --all的输出是两行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
git merge-base --all feature2 master
55641ac983def5857b1838912c772c3c68bf56f7
697ff7593dbdd08d275a2235279008c4c985ba70

$ git show 55641ac983def5857b1838912c772c3c68bf56f7
commit 55641ac983def5857b1838912c772c3c68bf56f7 (feature1)
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Thu Oct 20 22:55:36 2022 +0800

c1

diff --git a/a b/a
index 3df4201..3c13c8f 100644
Binary files a/a and b/a differ

$ git show 697ff7593dbdd08d275a2235279008c4c985ba70
commit 697ff7593dbdd08d275a2235279008c4c985ba70
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Thu Oct 20 22:55:36 2022 +0800

c2

diff --git a/a b/a
index 3df4201..2d67d4f 100644
Binary files a/a and b/a differ

git log graph 会更清楚

1
2
3
4
5
6
7
8
9
10
*   cb41364 (HEAD -> master) Merge branch 'feature1'
|\
* | 12f6057 c4
| | * 26efcbc (feature2) c3
| |/|
|/|/
| * 55641ac (feature1) c1
* | 697ff75 c2
|/
* ce3e667 c0

下面,我们尝试合并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:

  1. Squash Merge 将所有的 commit 压缩成一个 commit 提交。
  2. Merge Commit 会创建一个 commit,带上自己分支所有的 commit 进入 master。
  3. Rebase Merge 相当于自动地帮你把自己分支的 commit 一个一个提交到 master 上。

如下所示,Squash Merge 之后,会丢失自己分支上每一行的 git blame 信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
mkdir learn_git
pushd learn_git

git init
echo 1 > a
git add a
git commit -m"1"

git checkout -b derived
git checkout master

echo 2 > a
git add a
GIT_COMMITTER_NAME="u2" GIT_COMMITTER_EMAIL="u2@a.com" git commit -m"2" --author="u2 <u2@a.com>"

echo "2\n3" > a
git add a
GIT_COMMITTER_NAME="u3" GIT_COMMITTER_EMAIL="u3@a.com" git commit -m"3" --author="u3 <u3@a.com>"

git checkout derived

git merge --squash master

git commit

git checkout derived
git blame a
git checkout master
git blame a

Fast Forward

此外,Merge Commit 还有 ff 和 no ff 的策略。具体来说就是当自己的分支比 Master 要新的时候,是否创建一个 Commit。如下所示,使用 no ff 策略会产生一个空的 Merge commit,你从中diff 不出来任何内容,但事实上 ab0d86ddb766b5e564b0b8eff18a4a81b1334954 相比上次的 HEAD 是有变更的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
calvin@CalvinPC learn_git % git log
commit ab0d86ddb766b5e564b0b8eff18a4a81b1334954 (HEAD -> derived)
Merge: 2a5c78c 90b3d50
Author: CalvinNeo <calvinneo1995@gmail.com>
Date: Fri Sep 30 11:18:09 2022 +0800

Merge branch 'master' into derived

commit 2a5c78cbd5151c3d9d87643a5633cb9cfac4f29f
Author: CalvinNeo <calvinneo1995@gmail.com>
Date: Fri Sep 30 11:11:35 2022 +0800

1

commit 90b3d50440900a18845d239d53b6247d5e02082d (master)
Author: u3 <u3@a.com>
Date: Fri Sep 30 11:11:35 2022 +0800

3

commit a10189c06c285784fdafee1d0acfcfaf393e2fbe
Author: u2 <u2@a.com>
Date: Fri Sep 30 11:11:35 2022 +0800

2

如何判断可以 Fast Forward 合并呢?一个 idea

1
2
3
4
5
A=$(git rev-parse --verify A)
if test "$A" = "$(git merge-base A B)"
then
... A is an ancestor of B ...
fi

git merge-base

在先前我们已经了解了部分 merge-base 命令的使用。

fork-point

根据介绍,假如我们从origin/master分支位于 b0 时 fork 了一个 topic 分支出来,但随后 b0 又被 rebase 掉了,这个命令能够方便这种情况。

1
2
3
4
5
6
7
         o---B2
/
---o---o---B1--o---o---o---B (origin/master)
\
B0
\
D0---D1---D (topic)

我们会遇到什么问题呢?因为 b0 被干掉了,所以下面的命令会返回一个更早的 commit

1
git merge-base origin/master topic

通过

1
2
$ fork_point=$(git merge-base --fork-point origin/master topic)
$ git rebase --onto origin/master $fork_point topic

可以在 B 上面重新 rebase

1
2
3
4
5
6
7
         o---B2
/
---o---o---B1--o---o---o---B (origin/master)
\ \
B0 D0'--D1'--D' (topic - updated)
\
D0---D1---D (topic - old)

在 merge 的时候是否可以切换分支?

reset相关

git reset命令修改branch,即指向最新commit的指针HEAD,并不修改任何commit。reset可以带有三个参数:

  1. --hard参数会改变HEAD、index(stage)和working directory
  2. --mixed参数会改变HEAD、index(stage)
  3. --soft参数只会改变HEAD

常见用法

  1. 我有一个文件被git add了,现在我想要从staging里面移出这个修改,怎么做?
    下面这个操作能够将它移出staging

    1
    git reset --mixed file

    注意,我们通常喜欢使用git rm --cached,但是他未必是万精油,例如你可能遇到这个错误

    1
    2
    error: the following file has staged content different from both the
    file and the HEAD:
  2. 现在我想在working dir中也取消这个修改,怎么做?
    git reset --hard似乎不能对单个文件使用。
    这里可以

    1
    git checkout file

实验:有关reset

Step 1

本实验说明soft reset不会改变working directory。

  1. 首先我们在空目录执行

    1
    git init
  2. 我们创建a.txt,并且填写其内容为1

  3. 我们执行

    1
    2
    3
    git add .
    git commit -m"a=1"
    git log

    得到输出

    1
    2
    3
    4
    5
    commit 6bd3de7a2b1637dcb686f72af415c5d48f4d5dc2 (HEAD -> master)
    Author: Calvin Neo <calvinneo1995@gmail.com>
    Date: Fri Oct 30 22:46:37 2020 +0800

    a=1
  4. 修改a.txt的内容为2

  5. 执行

    1
    git reset --soft
  6. 查看a.txt的内容为2
    这说明a.txt没有被reset掉

Step 2

本实验说明soft reset不会改变index

  1. 此时,a.txt的内容仍然为2

  2. 执行

    1
    git diff --cached a.txt

    可以看到,a.txt已经被提交到了index里面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    diff --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
  3. 执行

    1
    git reset --soft
  4. 检查a.txt的内容仍然为2

Step 3

本实验说明soft reset能改变Repo

  1. 此时a.txt的内容仍然为2,执行

    1
    git commit -m"a=2"
  2. 检查git log
    可以看到,a=2已经进入了Repo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    commit 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
  3. 执行
    HEAD~1表示HEAD向前一个版本。

    1
    git reset --soft HEAD~1
  4. 检查git log
    发现a=2的提交被回退了

    1
    2
    3
    4
    5
    commit 6bd3de7a2b1637dcb686f72af415c5d48f4d5dc2 (HEAD -> master)
    Author: Calvin Neo <calvinneo1995@gmail.com>
    Date: Fri Oct 30 22:46:37 2020 +0800

    a=1
  5. 检查a.txt
    发现内容还是2,没有变。说明即使回退了Repo,也不会改变工作区。

  6. 执行

    1
    git checkout
  7. 检查a.txt
    发现值内容还是2,这个和图2似乎有矛盾。其实应该要加一个-f

    1
    git checkout -f

bisect相关

实验:有关bisect

Linux内核易于维护的一个原因就是因为Linus要求每一个commit只做一件事,所以他能够通过git bisect快速地二分出错误的提交。

执行下面语句,得到10个提交

1
2
3
4
5
git init
for i in {1..10}
do
echo "print '$i'" > p.py && git add . && git commit -m"p $i"
done

我们的目标是找到第一个打印出大于等于5的错误提交。
我们写一个predicate脚本

1
2
3
4
5
6
7
# test.sh
printed=$(python p.py)
if [ $printed -lt 5 ]; then
exit 0; # good
else
exit 1; # bad
fi

执行下面语句,自动查找到第一个故障提交

1
2
3
4
git bisect start
git status
git bisect bad HEAD
git bisect good HEAD~9

下面执行git bisect run,可以得到以下输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git bisect run ./test.sh
running ./test.sh
Bisecting: 1 revision left to test after this (roughly 1 step)
[552b511a6d14f48a258338527fb6532a6852d1c2] p 3
running ./test.sh
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[53c92c7bd287ae1cc06a0ff1eeb57f6bb9525424] p 4
running ./test.sh
6254d17800fc63757fd6b478e80db23480aec6f7 is the first bad commit
commit 6254d17800fc63757fd6b478e80db23480aec6f7
Author: Calvin Neo <calvinneo1995@gmail.com>
Date: Mon Nov 9 22:56:36 2020 +0800

p 5

:100644 100644 06cfe93d366cdbd541402affbcbf2305c1d7409b a6b723841de0d34ba0a7d30e7ba3b16ff48ad8e4 M p.py
bisect run success

需要注意,如果某个分支不能被test,应当使用git bisect skip来跳过,或者在bash脚本里面返回125

实验:如果涉及merge呢

其实我们还是在一条线上bisect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
echo 1 > a
echo 2 > b
git init
git add .
git commit -m"lca"

git checkout -b changea
echo 2 > a
git add .
git commit -m"a"

echo 3 > a
git add .
git commit -m"b"

git checkout master

git checkout -b changeb

echo 2 > b
git add .
git commit -m"c"

echo 3 > b
git add .
git commit -m"d"

git merge changea

Pull Request相关

基础

  1. Fork 项目 A 到自己的 Repo,并 Clone 自己的 Repo 到本地

  2. 在本地设置项目 A 为一个叫 upstream 的 remote,并且 fetch

    1
    2
    git remote add upstream A
    git fetch upstream
  3. 从项目 A 中创建一个新分支
    非常不推荐在本地的 master 上修改,这样的坏处是没有办法及时同步远程分支到本地的 master。
    类似从 origin 远程分支的方案

    1
    git checkout -b issueXXXX upstream/master
  4. 修改

将某个分支和 upstream 同步

这个通常发生在开发分支 me 在修改过程中,PR 目标 upstream/master 也在修改。在 PR 之前,可能需要解决冲突。
此时,可以先获取 upstream/master 到本地

1
2
3
git fetch upstream
git checkout master
git merge upstream/master

然后将 me 基于 master 做 rebase

1
2
git checkout me
git rebase master

修改PR

  1. stash多个commit
  2. 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
2
3
4
5
6
pushd contrib/xxx
git fetch upstream new_commit
git checkout upstream/new_commit
popd
git add contrib/xxx
git commit

替换 submodule 的 repo

有的时候,我们需要将某个依赖的组件更改为自己的版本。此时可以尝试

  1. 更新.gitsubmodule中对应 submodule
    注意,可能还需要修改对应的 branch
  2. 更新.git/config中对应 submodule
  3. 执行git submodule sync

注意,如果需要修改远程分支的,那么下面的这个目录也要提交,虽然看起来是空的,但它涉及.git/modules/contrib/yyy的一个 commit id。

1
2
3
4
5
6
7
8
$ git status
On branch xxx
Your branch is up-to-date with 'origin/xxx'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: contrib/yyy (new commits)

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 上,然后有几个方案:

  1. merge/rebase 发现存在一堆和 3a 不相关的冲突,根本无法操作
  2. git cherry-pick 一系列修改,但因为中间夹杂了很多 3b 操作,导致需要 cp 好多次
  3. 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
2
git diff $from $to  --no-prefix > patch.txt
patch -p0 < patch.txt

常见错误

unexpected disconnect while reading sideband packet
如下的错误基本上是 SSH 的问题,直接 git remote add 一个 SSH 方式而不是 HTTPS 方式的仓库即可。

1
2
3
4
error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: CANCEL (err 8)
send-pack: unexpected disconnect while reading sideband packet
fatal: the remote end hung up unexpectedly
Everything up-to-date

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'

Reference

  1. https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%AB%98%E7%BA%A7%E5%90%88%E5%B9%B6
  2. https://morningspace.github.io/tech/git-merge-stories-2/
  3. https://blog.walterlv.com/post/git-merge-strategy.html#resolve
  4. https://aaronflower.github.io/essays/github-fork-pull-workflow.html
    简介如何创建PR