理解 Git 的基本概念 (repository branching)

翻译原文链接

1. 介绍

本文介绍版本控制系统 Git

Git 快速成长为当前最为流行的版本控制系统之一。有很多文章教程介绍了Git的使用,那么本文有什么独特之处呢?

插曲

当我第一次使用Git的时候,我阅读了大量的教程,包括Git的用户手册。虽然我学会了基本的使用方式和命令,但我还是觉得没有真正理解Git是如何运行的。因此经常觉得Git的错误提示简直莫名其妙。随着我使用的命令越来越高级,这种情况变得越来越严重了。

使用了几个月之后,我逐渐明白了Git的基本概念。一旦我理解了它们,一切都变得可以理解了。我彻底读懂了用户手册,可以用Git完成各种版本控制的任务。以前那些莫名其妙的晦涩的东西都变得非常清楚了。

理解Git

由此我得到一个结论:只有你理解了Git是如何工作的,你才能真正使用Git。如果你仅仅是临时抱佛脚地记住几个命令应付当前的任务,那么迟早你会遇到大麻烦的。

当前介绍Git的材料中,大概有一半是这么做的:带着你过一遍Git命令,告诉你什么时候该使用社么命令,然后就期望你能灵活运用这些命令。另一边给你讲解所有概念,但是,就我所见,他们的解释只有已经懂了Git的人才能看得懂。

本文会从另一个角度来解释Git--概念性方法。首先,也是最重要的是,我们将解释Git是由哪些要素构成的,以及为什么需要这些要素,然后将说明怎么用Git命令来操作这些要素。

  • 我将从Git的数据模型--repository开始
  • 然后我将由简(向repository添加数据)入繁(分支branch、合并merge等)的讲解各种操作repository的Git命令。
  • 我们将介绍在与他人合作的情况下怎样使用Git。
  • 最后我还要介绍merge的替代品rebase,讨论它的优缺点。

2. Repository 版本库

Repository是什么

Git是用来管理项目的。所谓项目在这里就是指一组不断被改写的文件。Git把这些信息都保存在称为reporsitory的数据结构里。

一个git repository包含:

  • 一组commit objects。
  • 一组指向commit objects的索引,称为heads。(用C语言打个比方,commit objects就是变量,head是指向变量的指针)

Git repository保存在项目所在目录下名为 .git 的子目录里。请注意Git和CVS、SVN等集中式管理系统的区别:

  • 在项目根目录下只有一个 .git 子目录。
  • 保存repository的文件是和项目文件保存在一起的,没有主repository。(在CVS 和 SVN中,主repository保存了历史版本信息,但客户端下载的repository中只有当前版本,看不到历史信息。)

Commit objects 节点

节点包含三个部分:

  • 一组文件,表示了项目在某个给定时刻的状态(注:项目的状态可看作是以项目所有文件当前版本为元素的向量)。
  • 到父节点(可以多个)的索引。
  • 一个SHA1 name,一个长为40的字符串,节点的唯一标识。

一个节点A的父节点是指A的文件是从父节点的文件修改后得到的。通常一个节点有一个父节点,因为我们通常是从项目的一个节点开始,修改一些文件,然后保存文件,项目有了新的状态,即新的节点。merge一节我们将讲述一个节点为什么会有多个父节点。

一个项目总是有一个节点是没有父节点的。这就是项目第一次提交时产生的节点。

综上所述,我们可以把repository看作是一个由节点组成的有向无环图,图的边是由节点指向父节点,沿着边最终会走到项目的第一个节点。从任何一个节点,我们都可以沿着这棵树的边找到它的父节点以及父节点的父节点,了解文件是如何修改成当前的状态的。

Heads

head 就是指向一个节点的索引。每个head都有自己的名字。默认情况下,每个repository都有一个叫做master的head。一个repository可以有很多heads。在任何时刻,只有一个head被当做“当前head”。这个head一般用大写字母 HEAD表示。

请注意这点区别:小写的head用来表示repository中任何一个head,而大写的“HEAD”只表示当前head。Git的文档多次以这种区别表示不同的head。本文中,我们也采用这种表达方式,并且用斜体字符 HEAD 以示强调。

一个简单的Repository

下面我们从零开始为一个项目创建repository。在命令行窗口运行命令 git init。 这个目录不必是空目录。

mkdir [project]
cd [project]
git init

运行这个命令后,[project] 目录下会生成 .git 的子目录。

为了创建一个节点,你需要做下面两件事:

  1. 使用 git add 命令告诉Git哪些文件需要被管理,它们将被提交后生成的节点记录下来。如果一个文件在上次提交之后没有被修改,那么Git将自动把它包含在这次提交生成的节点中。因此,你只需要添加你修改过或新增加的文件。注意,git add 命令对目录的处理是递归的,所以 git add 命令会把目录下的所有修改添加进节点。
  2. 使用 git commit 命令创建一个节点。新节点将以当前 HEAD 为父节点(提交成功后,HEAD 将指向新生成的节点)。

一个常用的方式是:git commit -a 将会自动把所有修改后的文件(不包括新增加的文件)添加到新节点中。

注意,如果你修改了一个文件,但是没有把它添加到新节点,那么 Git 将把前一个版本(修改前的版本)提交到新节点。但对文件的修改还是保留着的。

举个例子,你像这样提交了3次后,repository的结构是这样的:

---->  time  ----->

(A) <-- (B) <-- (C)
                 ^
                 |
               master
                 ^
                 |
                HEAD

其中,(A), (B), 和 (C) 分别是第一次,第二次和第三次提交生成的节点。

介绍了上面的概念后,是时候介绍下面这些命令了:

  • git log 显示从 HEAD 开始回溯到根节点的所有节点的 log 信息。(当然这个命令还能做更多的事)
  • git status 显示相对于 HEAD 哪些文件被修改了。文件有三种类型:不属于节点的新文件(可通过 git add 添加进节点), 属于节点的且修改过的文件, 属于节点未被修改的文件。
  • git diff 显示相对于HEAD,文件有哪些修改。命令选项 --cached 表示比较通过 git add 命令添加到 HEAD 节点的文件;否则比较的是还没有加入节点的文件。(注:一个文件被修改后要经历两个步骤才能进入HEAD,1. 通过 git add,告诉Git这个文件的新版本将要进入节点, 2. 通过 git commit 将上一步add进来的文件版本添加到新创建的节点中,并将 HEAD 指向新节点。使用 --cached 选项,表示比较将要进入节点的新版本和 HEAD 中对应文件版本的差异,不使用则比较还没有使用过 git add 的文件和 HEAD 中对应版本的差异。)
  • git mv 和 git rm 分别表示文件将被移动(改名)和删除,这和 git add 类似。

我个人习惯的工作流程是这样的:

  1. 编辑文件。
  2. git status 查看哪些文件被改变了。
  3. git diff [file] 查看文件具体有哪些修改
  4. git commit -a -m [message] 提交。

节点访问

现在你可以创建节点了。但怎么访问某一指定的节点呢?Git提供了很多种方式供你选用。下面我们列举出一些:

  • 通过 SHA1 名字,这些名字可以通过 git log 得到。
  • 通过 SHA1 名字的前几个字母。
  • 通过head。 例如, 访问 HEAD 节点可以通过 HEAD,你也可以它的名字,比如 master。
  • 使用相对量。符号 (^)表示某个节点的父节点。比如, HEAD^ 表示当前节点的父节点。

3. 分支

分支的意义 The Purpose of Branching

假如你在写一篇论文。已经将初稿提交出去等待审阅。这时你拿到一些新的数据,需要将这些新数据应用到论文中。新数据的整合进行到一半,评审人员在这时候通知你,请你将文章中的某些章节的标题修改后再提交。你会怎么处理呢?

你当然不会在新数据整合到一半的文稿上按评审意见修改。你应该把当前的整合工作保存起来,然后在你的原稿上按评审意见修改。

这就是分支的意义,Git 可以帮助我们轻松完成这些工作。

术语解释:“分支” 和 “head” 几乎是同义词。每一个分支都有一个head,每一个head都代表一个分支。还记得repository是一棵树吧, “分支” 用来表示以 head 为叶子节点的所有祖先节点的序列,而 "head" 仅仅表示 head 所指向的那一个节点,即分支中最近被提交的节点。

创建分支

在创建一个分支之前,假设你的repository是这样的:

(A) -- (B) -- (C)
               |
             master
               |
              HEAD

其中,(B)  是你发给会议评审的版本, (C) 是文稿的当前状态(图上的箭头省略了,所有箭头都指向左边)

为了退回到节点 (B) 开始新的修改,你首先需要知道怎么得到一个节点。你或者通过 git log 得到 (B) 的SHA1 的名字,或使用 HEAD^ (前一节我们介绍过HEAD^ 表示 HEAD 节点的父节点)。

现在你可以使用命令 git branch:

git branch [new-head-name] [reference-to-(B)]

比如:

git branch fix-headers HEAD^

这个命令将以给定的名字创建一个新的head,并将新head指向你要求的节点。如果没有指明节点,新的head将指向 HEAD 所指的节点

现在你的repository结构应该是这样的:

(A) -- (B) ------- (C)
        |           |
   fix-headers    master
                    |
                   HEAD

切换分支

为了开始修改章节标题,我们将fix-headers 分支设置为当前分支。这需要使用命令 get checkout:

git checkout [head-name]

这个命令做了下面的事:This command does the following:

  1. HEAD 指向 [head-name] 所指定的节点
  2. 将所有项目文件恢复到新 HEAD 所指节点的状态。

重要提示:如果在运行 git checkout 之前,还有没提交的修改, Git 的处理方式显得有点奇怪。这种奇怪很多时候是有用的,但是最好你还是不要这样做。请在checkout之前先提交你所有的修改吧。

在 checkout 了 fix-headers head后,你可以修改章节标题了。完成修改后,你做了一次提交。现在repository的结构如下:

         +-------------- (D)
        /                 |
(A) -- (B) -- (C)         |
               |          |
             master  fix-headers
                          |
                         HEAD

(现在你可以看出为什么叫“分支”了:树结构生长出了一个新的分支。请注意(B)和(D)间连线的转角不代表任何含义,它仅仅表示(D)是(B)的孩子节点)

(You can see now why it’s called “branching”: the commit tree has grown a new branch. Note that the angle of the line connecting (B) and (D) is irrelevant; pointers do not store whether they are horizontal or slanted.)

master的祖先节点是 (C), (B), (A)。fix-headers的祖先节点是(D), (B), (A)。你可以用过 git log 来了解这些信息。

相关命令

一些相关命令:

  • git branch 不带参数,列举出现有的head,当前head前会有个星号 *。
  • git diff [head1] .. [head2] 比较head2和head1所指的节点的差异。
  • git diff [head1] ... [head2] (3个点)比较 head2 和 head1、head2共同祖先的差异。比如 diff master ... fix-headers 比较节点(D) 和 (B)。
  • git log [head1]..[head2] 显示head2到head1和head2共同祖先的log信息(即每次commit生成节点时的 -m 参数)。如果用三个点,则显示head1到共同祖先的log。

分支的使用方式

通常我们保留一个“主”分支,或称作“trunk”分支,然后创建一个新分支来实现新的功能。一般我们按照Git的默认方式,将master分支当做主分支。

所以,在上面的例子中,我们最好把节点 (B) 用作主分支(就是提交给评审人的节点)。然后你可以在新分支上做关于新数据的修改。

理想状况下,通过这种使用模式,主分支总是保持一个可发布的状态。其他分支未完成的工作,新功能等等。

这种模式对多个开发人员共同参与的项目尤其重要。这样每个开放人员都可以在自己的分支上开发新功能而不受其他开发人员的影响。

这就是为什么Git用户认为提交很便宜。如果你在你自己的分支上工作,你完全不必担心你的提交会干扰他人的工作。

posted @ 2012-09-16 21:28  不以为然的豆瓣  阅读(1023)  评论(0)    收藏  举报