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^

基本概念图

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来实现的。

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

rebase相关

merge相关

git merge用法

几种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)
  6. 在master上提交c4
  7. 将feature1合并到master,产生提交c5
    此时会导致Criss-Cross现象。我们来分析一下合并之后的情况:
    1. feature1目前指向c1,c1的祖先是c0。
    2. feature2目前指向c3,它的祖先是c2和c1。
    3. master目前指向c5,它的祖先是c4和c1。由于c4的祖先是c2,所以feature2和master有两个共同祖先c1和c2。

下面,我们尝试合并master和feature2。Recursive策略是,首先合并master和feature2的共同祖先,即c1和c2,得到一个虚拟祖先,然后在进行合并。

我们还可以指定不同的diff-algorithm

1
git merge origin/master -s recursive -X diff-algorithm=patience

例如patience策略就能够产生更加优雅的合并结果,例如更好地匹配大括号。

Resolve

Resolve策略是Recursive出现之前旧的合并策略。

Ours

Octopus

这个策略能够同时合并多个分支,但是如果出现需要手工解决的冲突,就会失败。

diff相关

实验:有关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

reset相关

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

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

实验:有关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

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