Git历险记(五)

转载自:http://www.infoq.com/cn/news/2011/03/git-adventures-branch-merge

分支与合并

在Git里面我们可以创建不同的分支,来进行调试、发布、维护等不同工作,而互不干扰。下面我们还是来创建一个试验仓库,看一下Git分支运作的台前幕后:

$rm -rf test_branch_proj
$mkdir test_branch_proj
$cd test_branch_proj
$git init
Initialized empty Git repository in /home/test/test_branch_proj/.git/

我们如以往一样,创建一个“readme.txt”文件并把它提交到仓库中:

$echo "hello, world" > readme.txt
$git add readme.txt
$git commit -m "project init"
[master (root-commit) 0797f4f] project init  1 files changed, 1 insertions(+), 0 deletions(-)  create mode 100644 readme.txt

我们来看一下工作目录(working tree)的当前状态:

$git status
# On branch master
nothing to commit (working directory clean)

大家如果注意的话,可以看到“# On branch master”这么一行,这表示我们现在正在主分支(master)上工作。当我们新建了一个本地仓库,一般就是默认处在主分支(master)上。下面我们一起看一下Git是如何存储一个分支的:

$cd .git
$cat HEAD
ref: refs/heads/master

“.git/HEAD”这个文件里保存的是我们当前在哪个分支上工作的信息。

在Git中,分支的命名信息保存在“.git/refs/heads”目录下:

$ls refs/heads
master

我们可以看到目录里面有一个名叫“master”文件,我们来看一下里面的内容:

$cat refs/heads/master
12c875f17c2ed8c37d31b40fb328138a9027f337

大家可以看到这是一个“SHA1哈希串值”,也就是一个对象名,我们再看看这是一个什么类型的对象:

$cat refs/heads/master | xargs git cat-file -t
commit

是的,这是一个提交(commit),“master”文件里面存有主分支(master)最新提交的“对象名”;我们根据这个“对象名”就可以可找到对应的树对象(tree)和二进制对象(blob),简而言之就是我能够按“名”索引找到这个分支里所有的对象。

读者朋友把我们文章里的示例在自己的机器上执行时会发现,“cat refs/heads/master”命令的执行结果和和文章中的不同。在本文里这个提交(commit)的名字是: “12c875f17c2ed8c37d31b40fb328138a9027f337”,前面我讲Git是根据对象的内容生成“SHA1哈希串值”作为 名字,只要内容一样,那么的对应的名字肯定是一样的,为什么这里面会不一样呢? Git确实根据内容来生成名字的,而且同名(SHA1哈希串值)肯定会有 相同内容,但是提交对象(commit)和其它对象有点不一样,它里面会多一个时间戳(timestamp),所以在不同的时间生成的提交对象,即使内容 完全一样其名字也不会相同。

下面命令主是查看主分支最新提交的内容:

$cat refs/heads/master | xargs git cat-file -p
tree 0bd1dc15d804534cf25c5cb53260fd03c84fd4b9
author liuhui998 <liuhui998@nospam.com> 1300697913 +0800
committer liuhui998 <liuhui998@nospam.com> 1300697913 +0800
project init

“1300697913 +0800”这就是时间戳(timestamp)。

现在查看此分支里面所包含的数据(blob)

$cat refs/heads/master | xargs git cat-file -p | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob 4b5fa63702dd96796042e92787f464e28f09f17d  readme.txt

查看当前的readme.txt

$git cat-file -p 4b5fa63
hello, world
$cd ..

好的,前面是在主分支(master)里面玩,下面我们想要创建一个自己的测试分支来玩一下。git branch命令可以创建一个新的分支,也可以查看当前仓库里有的分支。下面先创建一个叫“test”的分支: 

$git branch test

再来看一下当前项目仓库中有几个分支:

$git branch
* master   test

我们现在签出“test”分支到工作目录里:

$git checkout test

现在再来看一下我们处在哪个分支上:

$git branch   master
* test

好的,我们现在在“test”分支里面了,那么我们就修改一下“readme.txt”这个文件,再把它提交到本地的仓库里面支:

$echo "In test branch" >> readme.txt
$git add readme.txt
$git commit -m "test branch modified"
[test 7f3c997] test branch modified  1 files changed, 1 insertions(+), 0 deletions(-)

当看当前版本所包含的blob:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p

我们现在再像前面一样的看看Git如何存储“test”这个分支的,先来看看“.git/HEAD”这个文件是否指向了新的分支:

$cd .git
$cat HEAD
ref: refs/heads/test

没错,“.git/HEAD”确实指向的“test”分支。再来看看“.git/refs/heads”目录里的内容:

$ls refs/heads
master
test

我们可以看到目录里面多了一个名叫“test”文件,我们来看一下里面的内容:

$cat refs/heads/test
7f3c9972577a221b0a30b58981a554aafe10a104

查看测试分支(test)最新提交的内容:

$cat refs/heads/test | xargs git cat-file -p
tree 7fa3bfbeae072063c32621ff08d51f512a3bac53
parent b765df9edd4db791530f14c2e107aa40907fed1b
author liuhui998 <liuhui998@nospam.com> 1300698655 +0800
committer liuhui998 <liuhui998@nospam.com> 1300698655 +0800      test branch modified

再来查看此分支里面所包含的数据(blob):

$cat refs/heads/test | xargs git cat-file -p | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob ebe01d6c3c2bbb74e043715310098d8da2baa4bf  readme.txt

查看当前”readme.txt”文件里的内容:

$git cat-file -p ebe01d6
hello, world
In test branch
cd ..

我们再回到主分支里面:

$git checkout master
Switched to branch 'master'
$git checkout master
$cat readme.txt
hello, world

如我们想看看主分支(master)和测试分支(test)之间的差异,可以使用git diff命令来查看它们之间的diff

$git diff test
diff --git a/readme.txt b/readme.txt
index ebe01d6..4b5fa63 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1 @@  hello, world
-In test branch

大家可以以到当前分支与测试分支(test)相比,少了一行内容:“-In test branch”。

如果执行完git diff命令后认为测试分支(test)的修改无误,能合并时,可以用git merge命令把它合并到主分支(master)中:

$git merge test
Updating b765df9..7f3c997
Fast-forward  readme.txt |  1 +  1 files changed, 1 insertions(+), 0 deletions(-)

“Updating b765df9..7f3c997”表示现在正在更新合并“b765df9”和“7f3c997”两个提交(commit)之间的内容;“b765df9”代表着主分支(master),“7f3c997”代表测试分支(test)。

“Fast-forward”在这里可以理解为顺利合并,没有冲突。“readme.txt | 1 +”表示这个文件有一行被修改,“1 files changed, 1 insertions(+), 0 deletions(-)”,表示这一次合并只有一个文件被修改,一行新数据插入,0 行被删除。

我们现在看一下合并后的“readme.txt”的内容:

$cat readme.txt
hello, world
In test branch

内容没有错,是“master”分支和“test”分支合并后的结果,再用“git status”看一下,当前工作目录的状态也是干净的(clean)。

$git status
# On branch master
nothing to commit (working directory clean)

好的,现在测试分支(test)结束了它的使命,没有存在的价值的,可以用“git branch -d”命令把这个分支删掉:

$git branch -d test
Deleted branch test (was 61ce004).

如果你想要删除的分支还没有被合并到其它分支中去,那么就不能用“git branch -d”来删除它,需要改用“git branch -D”来强制删除。

如何处理冲突(conflict)

前面说了分支的一些事情,还简单地合并了一个分支。但是平时多人协作的工作过程中,几乎没有不碰到冲突(conflict)的情况,下面的示例就是剖析一下冲突成因及背后的故事:

还是老规矩,新建一个空的Git仓库作试验:

$rm -rf test_merge_proj
$mkdir test_merge_proj
$cd test_merge_proj
$git init
Initialized empty Git repository in /home/test/test_merge_proj/.git/

在主分支里建一个“readme.txt”的文件,并且提交本地仓库的主分支里(master):

$echo "hello, world" > readme.txt
$git add readme.txt
$git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
# (use "git rm --cached <file>..." to unstage)
#
# new file: readme.txt
#
git commit -m "project init"
[master (root-commit) d58353e] project init  1 files changed, 1 insertions(+), 0 deletions(-)  create mode 100644 readme.txt

当看当前版本所包含的blob:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob 4b5fa63702dd96796042e92787f464e28f09f17d  readme.txt

虽然前面把“readme.txt”这个文件提交了,但是暂存区里还是会暂存一下,直到下次“git add”时把它冲掉:

$git ls-files --stage
100644 4b5fa63702dd96796042e92787f464e28f09f17d 0 readme.txt

然后再创建测试分支(test branch),并且切换到测试分支下工作:

$git branch test
$git checkout test
Switched to branch 'test'

再在测试分支里改写“readme.txt”的内容,并且提交到本地仓库中:

$echo "hello, mundo" > readme.txt
$git add readme.txt
$git commit -m "test branch modified"
[test 7459649] test branch modified  1 files changed, 1 insertions(+), 1 deletions(-)

现在看一下当前分支里的“readme.txt”的“SHA1哈希串值”确实不同了:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob 034a81de5dfb592a22039db1a9f3f50f66f474dd  readme.txt

暂存区里的东东也不一样了:

$git ls-files --stage
100644 034a81de5dfb592a22039db1a9f3f50f66f474dd 0 readme.txt 

现在我们切换到主分支(master)下工作,再在“readme.txt”上作一些修改,并把它提交到本地的仓库里面:

$git checkout master
Switched to branch 'master'
$git add readme.txt
echo "hola,world" > readme.txt
$git add readme.txt
$git commit -m "master branch modified"
[master 269ef45] master branch modified  1 files changed, 1 insertions(+), 1 deletions(-)

现在再来看一下当前分支里的“readme.txt”的“SHA1哈希串值”:

$git cat-file -p HEAD | head -n 1 | cut -b6-15 | xargs git cat-file -p
100644 blob aac629fb789684a5d9c662e6548fdc595608c002  readme.txt

暂存区里的内容也改变了:

$git ls-files --stage
100644 aac629fb789684a5d9c662e6548fdc595608c002 0 readme.txt

主分支(master) 和测试分支(test)里的内容已经各自改变了(diverged),我们现在用“git merge”命令来把两个分支合一下看看:

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

合并命令的执行结果不是“Fast-foward”,而是“CONFLICT”。是的,两个分支的内容有差异,致使它们不能自动合并(Auto-merging)。

还是先看一下工作目录的状态:

$git status
# On branch master
# Unmerged paths:
# (use "git add/rm <file>..." as appropriate to mark resolution)
#
# both modified:    readme.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

现在Git提示当前有一个文件“readme.txt”没有被合并,原因是“both modified”。

再看一下暂存区里的内容:

$git ls-files --stage
100644 4b5fa63702dd96796042e92787f464e28f09f17d 1 readme.txt
100644 aac629fb789684a5d9c662e6548fdc595608c002 2 readme.txt
100644 034a81de5dfb592a22039db1a9f3f50f66f474dd 3 readme.txt

看一下里面的每个blob对象的内容:

$git cat-file -p 4b5fa6
hello, world
$git cat-file -p aac629
hola,world
$git cat-file -p 034a81
hello, mundo

我们不难发现,“aac629”是当前主分支的内容,“034a81”是测试分支里的内容,而“4b5fa6”是它们共同父对象(Parent)里的内容。因为在合并过程中出现了错误,所以Git把它们三个放到了暂存区了。

现在我们再来看一下工作目录里的“readme.txt”文件的内容:

$cat readme.txt
<<<<<<< HEAD
hola,world
=======
hello, mundo
>>>>>>> test

“<<<<<<< HEAD“下面就是当前版本里的内容;而“=======”之下,“>>>>>>> test”之上则表示测试分支里与之对应的有冲突的容。修复冲突时我们要做的,一般就是把“ <<<<<<< HEAD”,“=======”和“ >>>>>>> test”这些东东先去掉,然后把代码改成我们想要的内容。

假设我们用编辑器把“readme.txt“改成了下面的内容:

$cat readme.txt
hola, mundo

然再把改好的“readme.txt”用“git add”添加到暂存区中,最后再用“git commit”提交到本地仓库中,这个冲突(conflict)就算解决了:


$git add readme.txt
$git commit -m "fix conflict"
[master ebe2f18] fix conflict

这里看起来比较怪异的地方是Git解决了冲突的办法:怎么用“git add”添加到暂存区去,“git add”不是用来未暂存文件的吧,怎么又来解决冲突了。不过我想如果你仔细读过上一篇文章的话就不难理解,因为Git是一个“snapshot”存储系统,所有新增加的内容都是直接存储的,而不是和老版本作一个比较后存储新旧版本间的差异。

Git里面合并两个版本之间的同一文件,如果两者间内容相同则不作处理,两者间内容不同但是可以合并则产生一个新的blob对象,两者间内容不同但是合并时产生了冲突,那么我们解决了冲突后要把文件“git add”到暂存区中再“git commit”提交到本地仓库即可,这就和前面一样产生一个新的blob对象。

假设我们对合并的结果不满意,可以用下面的命令来撤消前面的合并:

$git reset --hard HEAD^
HEAD is now at 050d890 master branch modified

git reset(2)命令的输出结果可以看到,主分支已经回到了合并前的状态了。

我们再用下面的命令看一下“readme.txt”文件,确认一下文件改回来没有:

$cat readme.txt
hola,world

小结

由于Git采用了“SHA1哈希串值内容寻值”、“快照存储(snapshot)”等方法, Git中创建分支代价是很小的速度很快;也这是因为如此,它处理合并冲突的方法与众不同。

在这里我想起了“C语言就是汇编(计算机硬件)的一个马甲”这句话,其实Git也就是底层文件系统的一个马甲,只不过它带了版本控制功能,而且更加高效。Git里有些命令可能不是很好理解(如解决合并冲突用git add),但是对于系统层而言,它是最高效的,就像是C语言的数组下标从0开始一样。

 

时间: 2024-09-20 12:20:57

Git历险记(五)的相关文章

Git历险记(二)

转载自:http://www.infoq.com/cn/news/2011/01/git-adventures-install-config 各位同学,上回Git历险记(一)讲了一个 "hello Git" 的小故事.有的同学可能是玩过了其它分布式版本控制系统(DVCS),看完之后就触类旁通对Git就了然于胸了:也有的同学可能还如我当初入手Git一样,对它还是摸不着头脑. 从这一篇开始,我就将比较"啰嗦"的和大家一起从零开始经历Git使用的每一步,当然对我而言这也是

Git历险记(四)

转载自:http://www.infoq.com/cn/news/2011/03/git-adventures-index-commit 我想如果看过<Git历险记>的前面三篇文章的朋友可能已经知道怎么用git add,git commit这两个命令了:知道它们一个是把文件暂存到索引中为下一次提交做准备,一个创建新的提交(commit).但是它们台前幕后的一些有趣的细节大家不一定知晓,请允许我一一道来. 1. Git 索引是一个在你的工作目录(working tree)和项目仓库间的暂存区域(

Git历险记(三)

转载自:http://www.infoq.com/cn/news/2011/02/git-adventures-local-repository 如果我们要把一个项目加入到Git的版本管理中,可以在项目所在的目录用git init命令建立一个空的本地仓库,然后再用git add命令把它们都加入到Git本地仓库的暂存区(stage or index)中,最后再用git commit命令提交到本地仓库里. 创建一个新的项目目录,并生成一些简单的文件内容: $ mkdir test_proj $ cd

【git学习五】git基础之git分支

1.背景                最早用github的时候,我傻傻的问舍友大神,git里面的branch是干什么的,他用了很直白的解释,我至今还记得."branch就是你可以自己建立一个分支,随便乱搞而不影响整个项目".git分支应该是git最nb的特色吧,分支的建立和合并都十分方便.               大体的原理是这样的(图借用progit),比如说我们有一个master,还有一个分支是testing,git用head指针标记哪个分支正在被执行操作. 2.分支管理  

Linux下Git和GitHub使用方法详解

一.linux上安装git软件 可以直接从发行版本的源里进行安装 # sudo apt-get install git   //ubuntu发行版下 # yum -y install git     //redhat.centos发行版下 二.使用https用户名密码认证连接github 1.在github上创建项目 首先需要从github上申请一个帐号,申请完成后在点击右上角的"+" 号创建一个新的repository项目,如下: 2.主机上初始化项目并同步到github服务器上 在

Git~GitLab当它是一个源代码管理工具时

最近开始接触和使用GitLab,用它来做源代码的版本控制,CI.CD持续集成和持续交付,感觉功能确实很强大,今天也只能先说一下它的源代码管理功能,核心就是GIT,对GIT进行了封装,提供了一些扩展功能,事实上GitLab类似于GitHub,都是以Git以基础的! 下面我们来看一个场景,首先你在GitLab上建立了一个Project,然后本地有对应的项目,希望把本地现有的项目迁入到GitHub上,主要分为以下几个步骤: 一 在远程建立一个仓库,它有https和ssh的地址 二 本地建立仓库文件夹

Git系列(五):三个Git图形化工具

在本文里,我们来了解几个能帮你在日常工作中舒服地用上 Git 的工具. 我是在这许多漂亮界面出来之前学习的 Git,而且我的日常工作经常是基于字符界面的,所以 Git 本身自带的大部分功能已经足够我用了.在我看来,最好能理解 Git 的工作原理.不过,能有的选也不错,下面这些就是能让你不用终端就可以开始使用 Git 的一些方式. KDE Dolphin 里的 Git 我是一个 KDE 用户,如果不在 Plasma 桌面环境下,就是在 Fluxbox 的应用层.Dolphin 是一个非常优秀的文件

Git与GitHub学习笔记(五)一次提交失败的记录

代码已经跟踪了,添加注释说明,但是总是添加不了 error: pathspec 'live-page'' did not match any file(s) known to git. 重复了好多遍,最后发现代码还是没有提交 D:\wamp64\www\study-line>git commit -m 'add live-page' error: pathspec 'live-page'' did not match any file(s) known to git. D:\wamp64\www

Git使用基础篇

 Git是一个分布式的版本控制工具,本篇文章从介绍Git开始,重点在于介绍Git的基本命令和使用技巧,让你尝试使用Git的同时,体验到原来一个版 本控制工具可以对开发产生如此之多的影响,文章分为两部分,第一部分介绍Git的一些常用命令,其中穿插介绍Git的基本概念和原理,第二篇重点介绍 Git的使用技巧,最后会在Git Hub上创建一个开源项目开启你的Git实战之旅 1.Git是什么         Git在Wikipedia上的定义:它是一个免费的.分布式的版本控制工具,或是一个强调了速度快的