书名:Git学习指南
ISBN:978-7-115-43676-4
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [德] René Preißel Bjørn Stachmann
译 凌 杰 姜 楠
责任编辑 陈冀康
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Simplified Chinese translation copyright © 2016 by Posts and Telecommunications Press.
ALL RIGHTS RESERVED.
Git: Distributed Version Control Fundamentals and Workflows by Rene PreiBel and Biem Stachmann.
Copyright © 2014 by René Preißel and Bjørn Stachmann.
本书中文简体版由作者René Preißel和Bjørn Stachmann授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
Git是一款免费、开源的分布式版本控制系统,也是当今最为流行的版本控制系统之一,在众多的项目开发中普遍使用,得到程序员和工程师的欢迎和喜爱。
本书是一本面向专业开发者的图书。全书分为26章,从基础概念讲起,依次向读者介绍了有关Git的各种操作和使用技巧,不仅将提交、版本库、分支、合并等命令讲解到位,还介绍了工作流、基于分支的开发、二分法排错、发行版交付、项目的拆分与合并、项目的迁移等内容。
本书适合从事项目开发的专业人士阅读,想要学习Git的读者也可以选用。
欢迎阅读本书。
在前言中,我们将会为你介绍Git究竟能做什么,以及为什么你会需要这本书。
Git的背后有着一个非常精彩的成功故事。2005年4月,Linus Torvalds因不满当时任何一个可用的开源版本控制系统,就亲自着手实现了Git。
时至今日,如果我们在Google中搜索“git version control”这几个关键词,都会看到数以百万计的返回结果。Git已经俨然成为了新型开源项目的一个标准。许多大型的开源项目都已经或正在计划迁移到Git上来。
下面,我们来看一下这么多人之所以会选择Git的原因。
如果你在某一团队中从事开发工作,希望了解如何才能有效地使用Git,那么这本书就是一个正确的选择。本书既不是那种偏重于理论的大部头,也不是一本面面俱到的参考书。我们并不打算解释所有的Git命令(这里可有100多条命令呢)及其全部选项(有些命令甚至有50多个选项)。相反,我们打算在这本书中教你如何在典型的项目环境中使用Git,例如,如何建立起一个Git项目、如何创建一个Git发行版等。
你将在本书中看到以下内容。
Git非常灵活。可为多种不同的角色所用,从偶尔需要版本化少量shell脚本的单一系统管理员,到Linux内核项目中的上百个开发人员,一切皆有可能。当然,这种灵活性不是没有代价的。在开始用Git来开展工作之前,你还必须要做一组决定。例如以下几种。
为了便于入门,下面我们来总结一下工作流及其作用。
我们之所以会重点介绍商业项目中敏捷开发团队的工作,是因为我们相信目前许多专业开发者(包括作者)都处于这样的工作环境中。当然,这里并不包括那些具有特殊要求的大型项目,因为这些项目通常有着很夸张的工作流,而且我们相信这些也不是大多数开发者会感兴趣的项目。另外,这里也不包括那些开源项目的开发,虽然这些项目也可以用Git规划出一个很有意思的工作流。
作为作者,我们显然会希望读者能从头到尾阅读完这本书。但我们还是现实一点吧!你们会有时间阅读它吗?哪怕只阅读几页?恐怕你们的项目正如火如荼地进行着,Git的使用可能仅仅是你此刻要处理的上百个问题中的一个而已。因此,尽管我们在这本书上似乎倾注了一切的努力,但人们还是可能会忽略它的存在,但以下几件事值得你们考虑一下。
是否要阅读一下那些介绍性章节,了解一下工作流?
如果你之前不了解Git的相关知识,那么答案是肯定。你应该必须要掌握那些基本命令及其使用原则,如此才能正确地使用工作流。
已经有Git使用经验的读者,可以跳过哪些章节呢?
我们在每个介绍性章节(第1~11章)的最后一页上都会对该章的内容做一个小结。你可以根据这些小结非常迅速地判断这一章是否有新的内容仍需要了解,或者是否要直接跳过整章内容。另外,对于以下章节,你也可以选择跳过,因为它们只与部分工作流有些关联。
这本书中,我们会在许多例子中使用命令行来进行说明。但这并不意味着Git没有图形界面可用。事实上,我们已经有了两个基于图形界面(Graphical User Interface,GUI)的Git应用程序。并且除此之外,我们也还有很多好用的Git前端程序,下面就是其中的一部分。
但尽管如此,我们还是决定在实例演示中使用命令行,理由如下所示。
在本书的这些例子中,我们主要使用的是Linux和Mac OS系统中标准的bash shell环境。而在Windows中,你也可以选择使用“Git Bash”环境(这其实是msysgit的一个组件,后者是Windows下的一个Git应用程序,你可以去http://msysgit.github.io下载它)或cygwin。
在本书中,我们通常会这样表示一个命令行调用:
> git commit
有趣的是,我们还得表示来自Git的响应信息:
> git --version
git version 1.8.3.4
回顾整个创作过程,我们自己也很惊讶有那么多人以各种方式为这本书的诞生做出了贡献。我们应该要感谢他们所做的一切,因为如果没有他们,这本书不会有目前的样子。
我们首先要感谢Anke、Jan、Elke和Annika,大概现在谁也不记得我们手底下没有笔记本的样子了。
接下来,还要感谢我们在dpunkt出版社的友好团队,是他们出版了这本书的原始版本(即德语版),尤其是Vanessa Wittmer、Nadine Thiele和Ursula Zimpfer这3位编辑。另外,我们还要特别感谢René Schonfeldt、Maurice Kowalski、Jochen Schlosser、Oliver Zeigermann、 Ralf Degner、Michael Schulze-Ruhfus、另外还有六七个匿名审稿人,他们提供了许多有价值的反馈,这帮我们更好的完成了这本书。
当然,我们还要特别感谢Linus Torvalds、Junio C. Hamano以及Git项目的众多提交者,是他们给开发者社区带来了这个奇妙的工具。
在本章中,我们将介绍一个分布式版本控制系统的设计思路,以及它与集中式版本控制系统的不同之处。除此之外,我们还将带你了解分布式版本库的具体工作方式,以及为什么我们会说,在Git中创建分支和合并分支不是个大不了的问题。
在具体探讨分布式版本控制的概念之前,让我们先来快速回顾一下传统的集中式版本控制架构。
图1.1中所显示的就是一个集中式版本控制系统(例如CVS或Subversion)的典型布局。每个开发者都在他或她自己的计算机上有一个包含所有项目文件的工作目录(即工作区)。当该开发者在本地做了修改之后,他或她就会定期将修改提交给某台中央服务器。然后,开发者在执行更新操作的同时也会从该服务器上捡取出其他开发者所做的修改。这台中央服务器上存储着这些文件(即版本库)的当前版本和历史版本。因此,这些被并行开发的分支,以及各种被命名(标记)的版本都将会被集中管理。
图1.1 集中式版本控制
而在分布式版本控制系统(见图1.2)中,开发者环境与服务器环境之间是没有分隔的。每一个开发者都同时拥有一个用于当前文件操作的工作区与一个用于存储该项目所有版本、分支以及标签的本地版本库(我们称其为一份克隆)。每个开发者的修改都会被载入成一次次的新版本提交(commit), 首先提交到其本地版本库中。然后,其他开发者就会立即看到新的版本。通过推送(push)和拉回(pull)命令,我们可以将这些修改从一个版本库传送到另一个版本库中。这样一来,从技术上来看,这里所有的版本库在分布式架构上的地位是同等的。因此从理论上来讲,我们不再需要借助服务器,就可以将某一台开发工作机上所做的所有修改直接传送给另一开发工作机。当然在具体实践中,Git中的服务器版本库也扮演了重要的角色,例如以下这些特型版本库。
图1.2 分布式版本控制
下面,我们再来看看分布式系统相对于集中式的优点有哪些。
其实,版本库本质上就是一个高效的数据存储结构而已,由以下部分组成。
对于所有的数据,它们都会被计算成一个十六进制散列值(例如像1632acb65b01 c6b621d6e1105205773931bb1a41这样的值)。这个散列值将会被用作相关对象的引用,以及日后恢复数据时所需的键值(见图1.3)。
图1.3 版本库中的对象存储
也就是说,一个提交对象的散列值实际上就是它的“版本号”,如果我们持有某一提交的散列值,就可以用它来检查对应版本是否存在于某一版本库中。如果存在,我们就可以将其恢复到当前工作区相应的目录中。如果该版本不存在,我们也可以从其他版本库中单独导入(拉回)该提交所引用的全部对象。
接下来,我们来看看采用这种散列值和这种既定的版本库结构究竟有哪些优势。
对于大多数版本控制系统来说,分支的创建与合并通常会因其特殊性而被认为是高级拓展操作。但由于Git最初就是为了方便那些散落在世界各地的Linux内核开发者而创建的,合并多方努力的结果一直都是其面临的最大挑战之一,所以Git的设计目标之一就是要让分支的创建与合并操作变得尽可能地简单且安全。
在下面的图1.4中,我们向你展示了开发者是如何通过创建分支的方式来进行并行开发的。图中的每一个点都代表了该项目的一个版本(即commit)。而由于在Git中,我们只能对整个项目进行版本化,所以每个点同时也代表了属于同一版本的各个文件。
图1.4 因开发者的并行开发而出现的分支创建操作
如上所示,图中两位开发者的起点是同一个版本。之后两人各自做了修改,并提交了修改。这时候,对于这两位开发者各自的版本库来说,该项目已经有了两个不同的版本。也就是说,他们在这里创建了两个分支。接下来,如果其中一个开发者想要导入另一个人的修改,他/她就可以用Git来进行版本合并。如果合并成功了,Git就会创建一个合并提交,其中会包含两位开发者所做的修改。这时如果另一位开发者也取回了这一提交,两位开发者的项目就又回到了同一个版本。
在上面的例子中,分支的创建是非计划性的,其原因仅仅是两个开发者在并行开发同一个软件罢了。在Git中,我们当然也可以开启有针对性的分支,即显式地创建一个分支(见图1.5)。显式分支通常主要用于协调某一种功能性的并行开发。
图1.5 针对不同任务的显式分支
版本库在执行拉回和推送操作时,可以具体指定其针对的是哪一些分支。当然,除了这些简单的分支创建和合并处理外,我们也可以对分支执行以下动作。
在阅读完本章之后,我们希望你现在基本上熟悉了Git中的这些基本概念。也就是说,即使你现在放下了这本书(当然,希望不会!),你也可以参加与分布式版本控制系统有关的讨论,阐述其中使用散列值的必要性和实用性,介绍Git中的分支创建与合并操作了。
当然,你可能还会有以下疑问。
对于第一个问题,你可以继续阅读下一章内容。在下一章中,你将会看到那些具体用于创建版本库、版本以及版本库之间更替提交的命令。至于其他问题,你也可以参考详细介绍工作流的那些章节。
另外,如果你是一个繁忙的项目管理者,还在犹豫不决是否要采用Git。我们会建议你再看看关于Git的局限性的的讨论,请参见第26章。
第1章 Spring如果你想试着用一下Git的话,那么我们马上就可以开始了。本章将会带领你创建自己的第一个项目。我们会为你演示那些用于提交修改版本、查看历史和与其他开发者交换版本的命令。
首先,我们需要安装好Git。你可以在Git的官网上找到你所需要的一切:
http://git-scm.com/download
Git是一个高可配置软件。首先,我们可以宣布用config命令配置一下用户名和用户邮箱:[1]
> git config --global user.email "hans@mustermann.de"
在这里,我们建议你最好能为接下来的Git测试单独开辟一个项目。总之应先从一个简单的小项目开始。在我们这个小小的示例项目中,first-steps目录下只有两个文本文件,如图2.1所示。
图2.1 我们的示例项目
在开始摆弄这个玩具项目之前,我们建议你最好先做一个备份!尽管在Git中,想要造成永久性的删除或破坏也不是件容易的事情,而且每当你要做某些“危险”动作的时候,Git通常也会发出相应的警告消息。但是,有备无患总是好的。
现在,我们首先需要创建一个版本库,用于存储该项目本身及其历史。为此,我们需要在该项目目录中使用init命令。对于一个带版本库的项目目录,我们通常称之为工作区。
> cd /projects/first-steps
> git init
Initialized empty Git repository in /projects/first-steps/.git/
init命令会在上述目录中创建一个名为.git的隐藏目录,并在其中创建一个版本库。但请注意,该目录在Windows资源管理器或Mac Finder中可能是不可见的。
图2.2 本地版本库所在的目录
接下来,我们需要将foo.txt和bar.txt这两个文件添加到版本库中去。在Git中,我们通常将项目的一个版本称之为一次提交,但这要分两个步骤来实现。第一步,我们要先用add命令来确定哪些文件应被包含在下次提交中。第二步,再用commit命令将修改传送到版本库中,并赋予该提交一个散列值以便标识这次新提交。在这里,我们的散列值为2f43cd0,但可能会有所不同,因为该值取决于文件内容。
> git add foo.txt bar.txt
> git commit --message "Sample project imported."
master (root-commit) 2f43cd0] Sample project imported.
2 files changed, 2 insertions(+), 0 deletions(-)
create mode 100644 bar.txt
create mode 100644 foo.txt
现在,我们来修改一下foo.txt文件的内容,先删除bar.txt文件,再添加一个名为bar.html的新文件。然后,status命令就会显示出该项目自上次提交以来所发生的所有修改。请注意,新文件bar.html在这里被标示成了未跟踪状态,这是因为我们还没有用add命令将其注册到版本库。
> git status
# On branch master
# Changed but not updated:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in
# working directory)
#
# deleted: bar.txt
# modified: foo.txt
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# bar.html
no changes added to commit (use "git add" and/or "git commit -a")
如果我们还想看到更多细节性的内容,也可以通过diff命令来显示其每个被修改的行。当然。有很多人可能会觉得diff的输出是个非常难读的东西。幸运的是,在这一领域,我们有许多工具和开发环境可用,它们可以将这一切显示得更为清晰(见图2.3)。
图2.3 图形工具(kdiff3)中的Diff报告
> git diff foo.txt
diff --git a/foo.txt b/foo.txt
index 1910281..090387f 100644
--- a/foo.txt
+++ b/foo.txt
@@ -1 +1 @@
-foo
\ No newline at end of file
+foo foo
\ No newline at end of file
接下来,所有的修改都必须要先被归档成一次新的提交。我们要对修改过的文件和新文件执行add命令,并对要删除的文件使用rm命令。
> git add foo.txt bar.html
> git rm bar.txt
rm 'bar.txt'
现在再次调用status命令,我们会看到所有的修改已经被纳入了下一次提交中。
> git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: bar.html
# deleted: bar.txt
# modified: foo.txt
#
然后用commit命令提交这些修改。
> git commit --message "Some changes."
[master 7ac0f38] Some changes.
3 files changed, 2 insertions(+), 2 deletions(-)
create mode 100644 bar.html
delete mode 100644 bar.txt
log命令可用来显示项目的历史,所有提交都会按时间顺序被降序排列出来。
> git log
commit 7ac0f38f575a60940ec93c98de11966d784e9e4f
Author: Rene Preissel <rp@eToSquare.de>
Date: Thu Dec 2 09:52:25 2010 +0100
Some changes.
commit 2f43cd047baadc1b52a8367b7cad2cb63bca05b7
Author: Rene Preissel <rp@eToSquare.de>
Date: Thu Dec 2 09:44:24 2010 +0100
Sample project imported.
现在,我们已经有了一个存放项目文件的工作区,以及一个存放项目历史的版本库。在一个像CVS和Subversion这样传统的集中式版本系统中,尽管每个开发者也都有属于他/她自己的工作区,但所有人都共享了一个通用的版本库。而在Git中,每个开发者拥有的是一个属于他/她自己的、自带独立版本库的工作区,因此这已经是一个不依赖于中央服务器的、完整的版本控制系统了。开发者们可以通过交换各自版本库中的提交来实现项目合作。下面我们就来做个试验,先创建一个新的工作区,以便我们模拟第二位开发者的活动。
我们的这位新开发者首先要有一个属于他/她自己的版本库副本(也称为克隆体)。该副本中包含了所有的原始信息与整个项目的历史信息。下面。我们用clone命令来创建一个克隆体。
> git clone /projects/first-steps /projects/first-steps-clone
Cloning into first-steps-clone...
done.
现在,该项目结构如图2.4所示。
图2.4 样例项目与它的克隆体
下面,我们来修改一下first-steps/foo.txt文件,并执行以下操作来创建一次新提交。
> cd /projects/first-steps
> git add foo.txt
> git commit --message "A change in the original."
现在,新的提交已经被存入了我们原来的first-steps版本库中,但其克隆版本库(first-stepsclone)中依然缺失这次提交。为了让你更好地理解这一情况,我们来看一下first-steps的日志。
> git log --oneline
a662055 A change in the original.
7ac0f38 Some changes.
2f43cd0 Sample project imported.
在接下来的步骤中,我们再来修改克隆版本库中的first-steps-clone/bar.html文件,并执行以下操作。
> cd /projects/first-steps-clone
> git add bar.html
> git commit --message "A change in the clone."
> git log --oneline
1fcc06a A change in the clone.
7ac0f38 Some changes.
2f43cd0 Sample project imported.
现在,我们在两个版本库中各做了一次新的提交。接下来,我们要用pull命令将原版本库中的新提交传递给它的克隆体。由于之前我们在创建克隆版本库时,原版本库的路径就已经被存储在了它的克隆体中,因此pull命令知道该从哪里去取回新的提交。
> cd /projects/first-steps-clone
> git pull
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /projects/first-steps
7ac0f38..a662055 master -> origin/master
Merge made by recursive.
foo.txt | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
如上所示,pull命令从原版本库中取回了新的修改,将它们与克隆体中的本地修改进行了对比,并在工作区中合并了两边的修改,创建了一次新的提交。这个过程就是所谓的合并(merge)。
请注意!合并过程在某些情况下可能会带来冲突。一旦遇到了这种情况,Git中就不能进行自动化的版本合并了。在这种情况下,我们就必须要手动清理一些文件,然后再确认要提交哪些修改。
在拉回(pull)、合并(merge)的过程完成之后,我们可以用一个新的log命令来查看结果。这次是日志的图形化版本。
> git log --graph
9e7d7b9 Merge branch ’master’ of /projects/first-steps
*
|\
| * a662055 A change in the original.
* | 1fcc06a A change in the clone.
|/
* 7ac0f38 Some changes.
* 2f43cd0 Sample project imported.
这一次,历史记录不再是一条直线了。在上面的日志中,我们可以很清晰地看到并行开发的过程(即中间的两次提交),以及之后用于合并分支的那次合并提交(即顶部的那次提交)。
在没有参数的情况下,pull命令只在克隆版本库中能发挥作用,因为只有该克隆体中有默认的原版本库的连接。当我们执行pull操作时,也可以用参数来指定任意版本库的路径,以便从某一特定开发分支中提取相关修改。
现在,让我们将克隆体中的修改pull到原版本库中吧。
> cd /projects/first-steps
> git pull /projects/first-steps-clone master
remote: Counting objects: 8, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 5 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (5/5), done.
From /projects/first-steps-clone
* branch master → FETCH_HEAD
Updating a662055..9e7d7b9
Fast-forward
bar.html | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
除了可以用pull命令从其他版本库中取回相关提交外,我们也可以用push命令将提交传送给其他版本库。只不过,push命令只适用于那些没有开发者在上面开展具体工作的版本库。最好的方法就是创建一个不带工作区的版本库,我们称之为裸版本库(bare repository)。你可以使用clone命令的--bare选项来创建一个裸版本库。裸版本库通常可被用来充当开发者们传递提交(使用push命令)的汇聚点,以便其他人可以从中拉回他们所做的修改。下面我们来看一个裸版本库(见图2.5)。
图2.5 裸版本库(一个没有工作区的版本库)
> git clone --bare /projects/first-steps
/projects/first-steps-bare.git
Cloning into bare repository first-steps-bare.git...
done.
为了演示push命令的使用,我们需要再次修改一下firststeps/foo.txt文件,并执行以下操作来创建一次新的提交。
> cd /projects/first-steps
> git add foo.txt
> git commit --message "More changes in the original."
接下来,我们就可以用push命令向共享版本库传送该提交了(见图2.6)。该指令的参数要求与pull命令相同,我们需要指定目标版本库的路径及其分支。
> git push /projects/first-steps-bare.git master
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 293 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To /projects/first-steps-bare.git/
9e7d7b9..7e7e589 master -> master
图2.6 经由共享版本库来进行版本共享
现在,为了让克隆版本库也得到相应的修改,我们需要在执行pull命令时配置参数指向共享版本库的路径参数。
> cd /projects/first-steps-clone
> git pull /projects/first-steps-bare.git master
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ../first-steps-bare
* branch master -> FETCH_HEAD
Updating 9e7d7b9..7e7e589
Fast-forward
foo.txt | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
请注意!如果另一个开发者在我们之前已经做过一次push操作,此次push命令就会被拒绝传送提交。这时候,我们必须要先做一次pull操作,将其他人新上载的更新取回,并在本地合并。
[1] 译者注:示例中似乎少了用户名的部分:git config --global user.name“Hans”