Git使用教程四

本文重点介绍Git分支协作的相关命令,Git一个很重要的特性就是允许在不同的分支上协同操作,然后再进行分支合并。分支合并依赖两个常用命令merge和rebase,在本文主要介绍merge命令的使用,需要知道如何使用merge命令进行分支合并,并且需要掌握merge出现冲突时的解决方式,包括如何回退本次merge到之前的状态。然后介绍了diff命令的使用,需要了解如何查看工作区和暂存区文件的差异,工作区和Git仓库的差异以及暂存区和Git仓库的差异。最后简单介绍了一下HEAD~与HEAD^。

git merge

git merge命令用于将两个或两个以上的开发分支合并一起。

git merge [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]
	[-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]
	[--[no-]allow-unrelated-histories]
	[--[no-]rerere-autoupdate] [-m <msg>] [-F <file>] [<commit>…?]
git merge --abort
git merge --continue

示例一

假设当前master分支有一个文件readme.txt,文件内容如下:

hello java

提交记录如下:

$ git log --graph --pretty=oneline --abbrev-commit
* a2eeb86 (HEAD -> master) hello java commit

从当前分支中新建一个分支dev,并切换至dev分支,然后在dev对文件进行更改提交。

查看dev的commit记录如下:

$ git log --graph --pretty=oneline --abbrev-commit
* 4bfc2a0 (HEAD -> dev) hello c++ commit
* a2eeb86 (master) hello java commit

接下来切换至master主分支,现在想把dev分支的更改合并到master中,可以使用如下命令:

$ git merge dev
Updating a2eeb86..4bfc2a0
Fast-forward
 readme.txt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

注意到上面的Fast-forward信息,表示本次合并采用的是“快进模式”,也就是直接把master指向dev的当前提交,由于当前master分支所指向的提交是dev当前提交的直接上游,所以Git只是简单的将指针向前移动。

虽然主分支没有再次进行commit,只是执行了merge命令,但是仍然会将dev分支的commit记录同步至master分支。使用git log查看如下:

$ git log --graph --pretty=oneline --abbrev-commit
* 4bfc2a0 (HEAD -> master, dev) hello c++ commit
* a2eeb86 hello java commit

可以看出,直接使用git merge命令并没有生成新的提交记录。

示例二

在上述示例中,master分支在执行merge后并没有生成新的提交记录。其实Git也支持在merge的同时生成一条新的记录,使用--no-ff参数可以为merge生成一条新的commit记录。

$ git merge --no-ff dev
Merge made by the 'recursive' strategy.
 readme.txt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
查看一下master的commit记录,内容如下:

$ git log --graph --pretty=oneline --abbrev-commit * a3a56d0 (HEAD -> master) Merge branch 'dev' |\ | * 4bfc2a0 (dev) hello c++ commit |/ * a2eeb86 hello java commit

从显示结果可看出,此次比前面多生成了一条a3a56d0的提交记录,这也是使用--no-ff参数的意义,它可以保留其它分支的历史记录,可以更好地查看merge历史和分支的记录。

示例三

在执行merge时,后面可以跟多个分支名称,这样可以一次合并多个分支到当前分支。

$ git merge --no-ff dev dev02
Merge made by the 'recursive' strategy.
 readme.txt | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)
 
$ git log --graph --pretty=oneline --abbrev-commit
*   d72300d (HEAD -> master) Merge branch 'dev dev02'
|\
| * 576d41c (dev) hello c commit
| * 4bfc2a0 (dev02) hello c++ commit
|/
* a2eeb86 hello java commit

示例四

在执行merge后,如果想撤销本次merge,可以使用如下命令:

$ git reset --merge HEAD
#或者
git reset --merge 4bfc2a0

示例五

在示例一中,如果master分支也对readme.txt文件进行了更改提交,然后才执行merge dev的操作,这时候命令执行结果如下:

$ git merge --no-ff dev
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.

在冲突常见下,如果查看git的commit记录,如果不解决冲突并不会生成新的提交记录。

$ git log --graph --pretty=oneline --abbrev-commit
* 3ca77fe (HEAD -> master) hello c commit
* a2eeb86 hello java commit

打开readme.txt文件,冲突文件内容如下:

hello java
<<<<<<< HEAD
hello c
=======
hello c++
>>>>>>> dev

Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容,<<<<<<<和=======之间的是当前产生冲突的内容,而=======和>>>>>>>是待合并分支产生冲突的内容。

根据具体场景解决冲突,然后再次提交,查看整个提交记录如下:

$ git log --graph --pretty=oneline --abbrev-commit
*   277b300 (HEAD -> master) fix conflict
|\
| * 4844146 (dev) hello c++ commit
* | 3ca77fe hello c commit
|/
* a2eeb86 hello java commit

示例六

在示例五中,产生冲突后手动解决了冲突,可是有时候更改文件太多,可能直接手动解决冲突需要耗费很长的时间,但是这种情况下Git又处于冲突的MERGING状态,还可以使用如下命令废弃当前merge:

$ git merge --abort

git diff

git diff [<options>] [<commit>] [--] [<path>…?]
git diff [<options>] --cached [<commit>] [--] [<path>…?]
git diff [<options>] <commit> <commit> [--] [<path>…?]
git diff [<options>] <blob> <blob>
git diff [<options>] --no-index [--] <path> <path>

查看工作目录和暂存区差异

直接使用git diff是查看工作目录和暂存区文件差异。

$ git diff
diff --git a/readme.txt b/readme.txt
index 24e8850..da661f7 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
 hello common
-hello stage
\ No newline at end of file
+hello worktree
\ No newline at end of file

"---"表示变动前的文件即暂存区文件,"+++"表示变动后的文件即工作目录文件。@@中-1,2表示暂存区的文件从第1行起,总共2行文件,+1,2表示工作目录文件第1行起,总共2行文件。-hello stage表示暂存区即将被更改的内容,+hello worktree表示工作区新增的更改内容,一旦将工作目录readme.txt文件提交后,hello stage将被hello worktree替换。

查看工作目录和Git仓库差异

$ git diff HEAD
diff --git a/readme.txt b/readme.txt
index 35b35f7..da661f7 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
 hello common
-hello repository
\ No newline at end of file
+hello worktree
\ No newline at end of file

查看暂存区和Git仓库差异

$ git diff --cached #或者
$ git diff --staged
diff --git a/readme.txt b/readme.txt
index 35b35f7..24e8850 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
 hello common
-hello repository
\ No newline at end of file
+hello stage
\ No newline at end of file

查看简易的提交差异

$ git diff --cached --stat
 readme.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

查看当前工作目录和dev分支差异

$ git diff dev
diff --git a/readme.txt b/readme.txt
index 5d57af7..5b77d12 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
 hello common
-hello dev
\ No newline at end of file
+hello wordtree
\ No newline at end of file

比较两个历史版本差异

$ git diff HEAD^ HEAD
diff --git a/readme.txt b/readme.txt
index 35b35f7..5d57af7 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
 hello common
-hello repository
\ No newline at end of file
+hello dev
\ No newline at end of file

$ git diff SHA1 SHA2

HEAD~与HEAD^

在Git中,HEAD是一个指向正在工作的本地分支的指针,可以将HEAD理解为当前分支的别名。

HEAD经常与两个操作符连用,这两个操作符分别是~和^,并且有时候两个操作符是可以互换使用的。

一般HEAD与~组合使用表示回退的步数。

$ git log --pretty=oneline
0b23fe59640cc449678f304e49cd37decdb501bd (HEAD -> master) 04 commit
32e055904d42b4cfcae81c51d4b5e9a6c618fb1a 03 commit
047edbce21192b584052c1640bc33087a70711ca 02 commit
b6c96148f624cfd9412652993437be65513f5794 01 commit

假设有上述4次commit,那么:

  • HEAD~或者HEAD~1表示HEAD前一次提交,也即是03 commit;
  • HEAD~~或者HEAD~2表示HEAD的前二次提交,也即是02 commit;
  • HEAD~~~或者HEAD~3表示HEAD的前3次提交,也即是01 commit;

git-rev-parse(1) Manual Page有关于HEAD~与HEAD^的一个很经典的示例。

以下是示例中插图。提交节点B和C都是提交节点A的父节点,父提交按从左到右的顺序排列。

G   H   I   J
 \ /     \ /
  D   E   F
   \  |  / \
    \ | /   |
     \|/    |
      B     C
       \   /
        \ /
         A

A =      = A^0
B = A^   = A^1     = A~1
C = A^2  = A^2
D = A^^  = A^1^1   = A~2
E = B^2  = A^^2
F = B^3  = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2  = B^^2    = A^^^2  = A~2^2
I = F^   = B^3^    = A^^3^
J = F^2  = B^3^2   = A^^3^2

HEAD^主要是控制merge之后回退的方向

HEAD~才是回退的步数

小结

本文主要介绍了两个命令的使用,它们分别是merge和diff。merge命令是必须熟练使用的命令,尤其需要重点掌握如何解决merge冲突,当然了也需要知道如何回退merge到之前的状态。了解diff命令如何使用即可。

有关HEAD~与HEAD^这里仅仅是记录一下,方便后续查阅,目前还没有完全理解这两个命令的差异性。

已经介绍了几个常用的本地操作的Git命令,接下来会结合远程仓库来介绍其它Git操作的命令,如pull、push以及rebase命令。

参考资料

Git教程

Git Community Book 中文版

在Git中,头部^和Head~有什么区别?

评论

您确定要删除吗?删除之后不可恢复