內容目录

上一个主题

1.1. 版本控制的前世和今生

下一个主题

1.3. 安装Git

本页

1.2. 爱上Git的理由

本章通过一些典型应用展示Git作为版本控制系统的独特用法。对于不熟悉版本控制系统的读者,可以通过这些示例对版本控制拥有感性的认识。如果是有经验的读者,示例中的和SVN的对照可以让您体会到Git的神奇和强大。本章将列举Git的一些闪亮特性,期待能够让您爱上Git。

1.2.1. 每日的工作备份

当我开始撰写本书时才明白写书真的是一个辛苦活。如何让辛苦的工作不会因为笔记本硬盘的意外损坏而丢失?如何防范灾害而不让一个篮子里的鸡蛋都毁于一旦?下面就介绍一下我在写本书时如何使用Git进行文稿备份的,请看图2-1。

../images/work-backup.png

图2-1:利用Git做数据的备份

如图2-1,我的笔记本在公司局域网里的IP地址是192.168.0.100,公司的Git服务器的IP地址是192.168.0.2。公司使用动态IP上网因而没有固定的外网IP,但是公司在数据中心有托管服务器,拥有固定的IP地址,其中一台服务器用作Git服务器镜像。

我的写书习惯大概是这样:一般在写完一个小节,或是画完一张图,我会执行下面的命令提交一次。每一天平均提交3-5次。提交是在笔记本本地完成的,因此在图中没有表示出来。

$ git add -u    # 如果创建了新文件,可以执行 git add -i 命令。
$ git commit

下班后,我会执行一次推送操作,将我在本地Git版本库中的提交同步到公司的Git服务器上。相当于图2-1中的步骤①。

$ git push

因为公司的Git服务器和异地数据中心的Git服务器建立了镜像,所以每当我向公司内网服务器推送的时候,就会自动触发从内网服务器到外网Git服务器的镜像操作。相当于图2-1中的步骤②,步骤②是自动执行的无须人工干预。图2-1中标记为mirror的版本库就是Git镜像版本库,该版本库只向用户提供只读访问服务,而不能对其进行写操作(推送)。

从图2-1中可以看出,我的每日工作保存有三个拷贝,一个在笔记本中,一个在公司内网的服务器上,还有一个在外网的镜像版本库中。鸡蛋分别装在了三个篮子里。

至于如何架设可以实时镜像的Git服务器,会在本书第5篇“第30章 Gitolite服务架设”中予以介绍。

1.2.2. 异地协同工作

为了能够加快写书的进度,熬夜是必须的,这就出现了在公司和在家两地工作同步 的问题。图2-2用于说明我是如何解决两地工作同步的问题的。

../images/workflow.png

图2-2:利用Git实现异地工作协同

我在家里的电脑IP地址是10.0.0.100(家里也有一个小局域网)。如果在家里有时间工作的话,首先要做的就是图2-2中步骤③的操作:从mirror版本库同步数据到本地。只需要一条命令就好了:

$ git pull mirror master

然后在家里的电脑上编辑书稿并提交。当准备完成一天的工作时,就执行下面的命令,相当于图2-2中步骤④的操作:将在家中的提交推送到标记为home的版本库中。

$ git push home

为什么还要再引入另外一个名为home的版本库呢?使用mirror版本库不好么?不要忘了mirror版本库只是一个镜像库,不能提供写操作。

当一早到公司,开始动笔写书之前,先要执行图2-2中步骤⑤的操作,从home版本库将家里做的提交同步到公司的电脑中。

$ git pull home master

公司的小崔是我这本书的忠实读者,我每有新章节出来,他都会执行图2-2中步骤⑥的工作,从公司内网服务器获取我最新的文稿。

$ git pull

一旦发现文字错误,小崔会直接在文稿中修改,然后推送到公司的服务器上(图2-2中步骤⑦)。当然他的这个推送也会自动同步到外网的mirror版本库。

$ git push

而我只要执行git pull操作就可以获得小崔对我文稿的修订(图2-2中的步骤⑧)。采用这种工作方式,文稿竟然分布在5台电脑上拥有6个拷贝,真可谓狡兔三窟。不,比狡兔还要多三窟。

在本节中,出现在Git命令中的mirrorhome是和工作区关联的远程版本库。关于如何注册和使用远程版本库,请参见本书第3篇“第19章 远程版本库”中的内容。

1.2.3. 现场版本控制

所谓现场版本控制,就是在客户现场或在产品部署的现场,进行源代码的修改,并在修改过程中进行版本控制,以便在完成修改后能够将修改结果甚至修改过程一并带走,并能够将修改结果合并至项目对应的代码库中。

SVN的解决方案

如果使用SVN进行版本控制,首先要将服务器上部署的产品代码目录变成SVN工作区,这个过程并不简单而且会显得很繁琐,最后将改动结果导出也非常不方便,具体操作过程如下。

  1. 在其他位置建立一个SVN版本库。

    $ svnadmin create /path/to/repos/project1
    
  2. 在需要版本控制的目录下检出刚刚建立的空版本库。

    $ svn checkout file:///path/to/repos/project1 .
    
  3. 执行文件添加操作,然后执行提交操作。这个提交将是版本库中编号为1的提交。

    $ svn add *
    $ svn ci -m "initialized"
    
  4. 然后开始在工作区中修改文件,提交。

    $ svn ci
    
  5. 如果对修改结果满意,可以通过创建补丁文件的方式将工作成果保存带走。但是SVN很难对每次提交逐一创建补丁,一般用下面的命令与最早的提交进行比较,以创建出一个大补丁文件。

    $ svn diff -r1 > hacks.patch
    

上面用SVN将工作成果导出的过程存在一个致命的缺陷,就是SVN的补丁文件不支持二进制文件,因此采用补丁文件的方式有可能丢失数据,如新增或修改的图形文件会丢失。更为稳妥但也更为复杂的方式可能要用到svnadmin命令将版本库导出。命令如下:

$ svnadmin dump --incremental -r2:HEAD \
  /path/to/repos/project1/ > hacks.dump

svnadmin命令创建的导出文件恢复到版本库中也非常具有挑战性,这里就不再详细说明了。还是来看看Git在这种情况下的表现吧。

Git的解决方案

Git对产品部署目录进行到工作区的转化相比SVN要更为简单,而且使用Git将提交历史导出也更为简练和实用,具体操作过程如下:

  1. 现场版本库创建。直接在需要版本控制的目录下执行Git版本库初始化命令。

    $ git init
    
  2. 添加文件并提交。

    $ git add -A
    $ git commit -m "initialized"
    
  3. 为初始提交建立一个里程碑:“v1”。

    $ git tag v1
    
  4. 然后开始在工作区中工作——修改文件,提交。

    $ git commit -a
    
  5. 当对修改结果满意,想将工作成果保存带走时,可以通过下面的命令,将从v1开始的历次提交逐一导出为补丁文件。转换的补丁文件都包含一个数字前缀,并提取提交日志信息作为文件名,而且补丁文件还提供对二进制文件的支持。下面命令的输出摘自本书第3篇“第20章 补丁文件交互”中的实例。

    $ git format-patch v1..HEAD
    0001-Fix-typo-help-to-help.patch
    0002-Add-I18N-support.patch
    0003-Translate-for-Chinese.patch
    
  6. 通过邮件将补丁文件发出。当然也可以通过其他方式将补丁文件带走。

    $ git send-email *.patch
    

Git创建的补丁文件使用了Git扩展格式,因此在导入时为了避免数据遗漏,要使用Git提供的命令而不能使用GNU patch命令。即使要导入的不是Git版本库,也可以使用Git命令,具体操作请参见本书第7篇“第38章 补丁中的二进制文件”中的相关内容。

1.2.4. 避免引入辅助目录

很多版本控制系统,都要在工作区中引入辅助目录或文件,如SVN要在工作区的每一个子目录下都创建.svn目录,CVS要在工作区的每一个子目录下都创建CVS目录。

这些辅助目录如果出现在服务器上,尤其是Web服务器上是非常危险的,因为这些辅助目录下的Entries文件会暴露出目录下的文件列表,让管理员精心配置的禁止目录浏览的努力全部白费。

还有,SVN的.svn辅助目录下还存在文件的原始拷贝,在文件搜索时结果会加倍。如果您曾经在SVN的工作区用过grep命令进行内容查找,就会明白我指的是什么。

Git没有这个问题,不会在子目录下引入讨厌的辅助目录或文件(.gitignore.gitattributes文件不算)。当然Git还是要在工作区的顶级目录下创建名为.git的目录(版本库目录),不过如果你认为唯一的一个.git目录也过于碍眼,可以将其放到工作区之外的任意目录。一旦这么做了,你在执行Git命令时,要通过命令行(–git-dir)或环境变量GIT_DIR为工作区指定版本库目录,甚至还要指定工作区目录。

Git还专门提供了一个git grep命令,这样在工作区根目录下执行查找时,目录.git也不会对搜索造成影响。

关于辅助目录的详细讨论请参见本书第2篇第4.2节中的内容。

1.2.5. 重写提交说明

很多人可能如我一样,在敲下回车之后,才发现提交说明中出现了错别字,或忘记了写关联的Bug ID。这就需要重写提交说明。

SVN的解决方案

SVN的提交说明默认是禁止更改的,因为SVN的提交说明属于不受版本控制的属性,一旦修改就不可恢复。我建议SVN的管理员只有在配置了版本库更改的外发邮件通知之后,再开放提交说明更改的功能。我发布于SourceForge上的pySvnManager项目,提供了SVN版本库图形化的钩子管理,会简化管理员的配置工作。

即使SVN管理员启用了允许更改提交说明的设置,修改提交说明也还是挺复杂的,看看下面的命令:

$ svn ps --revprop -r <REV> svn:log "new log message..."

Git的解决方案

Git修改提交说明很简单,而且提交说明的修改也是被追踪的。Git修改最新提交的提交说明最为简单,使用一条名为修补提交的命令即可。

$ git commit --amend

这个命令如果不带“-m”参数,会进入提交说明编辑界面,修改原来的提交说明,直到满意为止。

如果要修改某个历史提交的提交说明,Git也可以实现,但要用到另外一个命令:变基命令。例如要修改<commit-id>所标识提交的提交说明,执行下面的命令,并在弹出的变基索引文件中修改相应提交前面的动作的关键字。

$ git rebase -i <commit-id>^

关于如何使用交互式变基操作更改历史提交的提交说明,请参见本书第2篇“第12章 改变历史”中的内容。

1.2.6. 想吃后悔药

假如提交的数据中不小心包含了一个不应该检入的虚拟机文件——大约有1个GB!这时候,您会多么希望这个世界上有后悔药卖啊。

SVN的解决方案

SVN遇到这个问题该怎么办呢?删除错误加入的大文件,再提交,这样的操作是不能解决问题的。虽然表面上去掉了这个文件,但是它依然存在于历史中。

管理员可能是受影响最大的人,因为他要负责管理服务器的磁盘空间占用及版本库的备份。实际上这个问题也只有管理员才能解决,所以你必须向管理员坦白,让他帮你在服务器端彻底删除错误引入的大文件。我要告诉你的是,对于管理员,这并不是一个简单的活。

  1. SVN管理员要是没有历史备份的话,只能从头用svnadmin dump导出整个版本库。
  2. 再用svndumpfilter命令过滤掉不应检入的大文件。
  3. 然后用svnadmin load重建版本库。

上面的操作描述中省略了一些窍门,因为要把窍门说清楚的话,这本书就不是讲Git,而是讲SVN了。

Git的解决方案

如果你用Git,一切就会非常简单,而且你也不必去乞求管理员,因为使用Git,每个人都是管理员。

如果是最新的提交引入了不该提交的大文件:winxp.img,操作起来会非常简单,还是用修补提交命令。

$ git rm --cached winxp.img
$ git commit --amend

如果是历史版本,例如是在<commit-id>所标识的提交中引入的文件,则需要使用变基操作。

$ git rebase -i <commit-id>^

执行交互式变基操作抛弃历史提交,版本库还不能立即瘦身,具体原因和解决方案请参见本书第2篇“第14章 Git库管理”中的内容。除了使用变基操作,Git还有更多的武器可以实现版本库的整理操作,具体请参见本书第6篇第35.4节的内容。

1.2.7. 更好用的提交列表

正确的版本控制系统的使用方法是,一次提交只干一件事:完成一个新功能、修改了一个Bug、或是写完了一节的内容、或是添加了一幅图片,就执行一次提交。而不要在下班时才想起来要提交,那样的话版本控制系统就被降格为文件备份系统了。

但有时在同一个工作区中可能同时在做两件事情,一个是尚未完成的新功能,另外一个是解决刚刚发现的Bug。很多版本控制系统没有提交列表的概念,或者要在命令行指定要提交的文件,或者默认把所有修改内容全部提交,破坏了一个提交干一件事的原则。

SVN的解决方案

SVN 1.5开始提供了变更列表(change list)的功能,通过引入一个新的命令svn changelist来实现。但是我从来就没有用过,因为:

  • 定义一个变更列表太麻烦。例如不支持将当前所有改动的文件加入列表,也不支持将工作区中的新文件全部加入列表。
  • 一个文件不能同时属于两个变更列表。两次变更不许有文件交叉,这样的限制太牵强。
  • 变更列表是一次性的,提交之后自动消失。这样的设计没有问题,但是相比定义列表时的繁琐,以及提交时必须指定列表的繁琐,使用变更列表未免得不偿失。
  • 再有,因为Subversion的提交不能撤销,如果在提交时忘了提供变更列表名称以针对特定的变更列表进行提交,错误的提交内容将无法补救。

总之,SVN的变更列表尚不如鸡肋,食之无味,弃之不可惜。

Git的解决方案

Git通过提交暂存区实现对提交内容的定制,非常完美地实现了对工作区的修改内容进行筛选提交:

  • 执行git add命令将修改内容加入提交暂存区。执行git add -u命令可以将所有修改过的文件加入暂存区,执行git add -A命令可以将本地删除文件和新增文件都登记到提交暂存区,执行git add -p命令甚至可以对一个文件内的修改进行有选择性的添加。
  • 一个修改后的文件被登记到提交暂存区后,可以继续修改,继续修改的内容不会 被提交,除非再对此文件再执行一次git add命令。即一个修改的文件可以拥有两个版本,在提交暂存区中有一个版本,在工作区中有另外一个版本。
  • 执行git commit命令提交,无须设定什么变更列表,直接将登记在暂存区中的内容提交。
  • Git支持对提交的撤消,而且可以撤消任意多次。

只要使用Git,就会时刻在和隐形的提交列表打交道。本书第2篇“第5章 Git暂存区”会详细介绍Git的这一特性,相信你会爱上Git的这个特性。

1.2.8. 更好的差异比较

Git对差异比较进行了扩展,支持对二进制文件的差异比较,这是对GNU的diffpatch命令的重要补充。还有Git的差异比较除了支持基于行的差异比较外,还支持在一行内逐字比较的方式,当向git diff命令传递–word-diff参数时,就会进行逐字比较。

在上面介绍了工作区的文件修改可能会有两个不同的版本,一个是在提交暂存区,一个是在工作区。因此在执行git diff命令时会遇到令Git新手费解的现象。

  • 修改后的文件在执行git diff命令时会看到修改造成的差异。
  • 修改后的文件通过git add命令提交到暂存区后,再执行git diff命令会看不到该文件的差异。
  • 继续对此文件进行修改,再执行git diff命令,会看到新的修改 显示在差异中,而看不到旧的修改。
  • 执行git diff –cached命令才可以看到添加到暂存区中的文件所 做出的修改。

Git差异比较的命令充满了魔法,本书第5章第5.3节会带您破解Git的diff魔法。一旦您习惯了,就会非常喜欢git diff的这个行为。

1.2.9. 工作进度保存

如果工作区的修改尚未完成时,忽然有一个紧急的任务,需要从一个干净的工作区开始新的工作,或要切换到别的分支进行工作,那么如何保存当前尚未完成的工作进度呢?

SVN的解决方案

如果版本库规模不大,最好重新检出一个新的工作区,在新的工作区进行工作。否则,可以执行下面的操作。

$ svn diff > /path/to/saved/patch.file
$ svn revert -R
$ svn switch <new_branch>

在新的分支中工作完毕后,再切换回当前分支,将补丁文件重新应用到工作区。

$ svn switch <original_branch>
$ patch -p1 < /path/to/saved/patch.file

但是切记SVN的补丁文件不支持二进制文件,这种操作方法可能会丢失对二进制文件的更改!

Git 的解决方案

Git提供了一个可以保存和恢复工作进度的命令git stash。这个命令非常方便地解决了这个难题。

在切换到新的工作分支之前,执行git stash保存工作进度,工作区会变得非常干净,然后就可以切换到新的分支中了。

$ git stash
$ git checkout <new_branch>

新的工作分支修改完毕后,再切换回当前分支,调用git stash pop命令则可恢复之前保存的工作进度。

$ git checkout <orignal_branch>
$ git stash pop

本书第2篇“第9章 恢复进度”会为您揭开git stash命令的奥秘。

1.2.10. 代理SVN提交实现移动式办公

使用像SVN一样的集中式版本控制系统,要求使用者和版本控制服务器之间要有网络连接,如果因为出差在外或在家办公访问不到版本控制服务器就无法提交。Git属于分布式版本控制系统,不存在这样的问题。

当版本控制服务器无法实现从SVN到Git的迁移时,仍然可以使用Git进行工作。在这种情况下,Git作为客户端来操作SVN服务器,实现在移动办公状态下的版本提交(当然是在本地Git库中提交)。当能够连通SVN服务器时,一次性将移动办公状态下的本地提交同步给SVN服务器。整个过程对于SVN来说是透明的,没有人知道你是使用Git在进行提交。

使用Git来操作SVN版本控制服务器的一般工作流程为:

  1. 访问SVN服务器,将SVN版本库克隆为一个本地的Git库,一个货真价实的Git库,不过其中包含针对SVN的扩展。

    $ git svn clone <svn_repos_url>
    
  2. 使用Git命令操作本地克隆的版本库,例如提交就使用git commit命令。

  3. 当能够通过网络连接到SVN服务器,并想将本地提交同步给SVN服务器时,先获取SVN服务器上最新的提交,再执行变基操作,最后再将本地提交推送给SVN服务器。

    $ git svn fetch
    $ git svn rebase
    $ git svn dcommit
    

本书第4篇“第26章 Git和SVN协同模型”中会详细介绍这一话题。

1.2.11. 无处不在的分页器

虽然拥有图形化的客户端,但Git更有效率的操作方式还是命令行操作。使用命令行操作的好处一个是快,另外一个就是防止鼠标手的出现。Git的命令行进行了大量的人性化设计,包括命令补全、彩色字符输出等,不过最具特色的还是无处不在的分页器。

在操作其他版本控制系统的命令行时,如果命令的输出超过了一屏,为了能够逐屏显示,需要在命令的后面加上一个管道符号将输出交给一个分页器。例如:

$ svn log | less

而Git则不用如此麻烦,因为常用的Git的命令都带有一个分页器,当一屏显示不下时启动分页器。分页器默认使用less命令(less -FRSX)进行分页。

因为less分页器在翻屏时使用了vi风格的热键,如果您不熟悉vi的话,可能会遇到麻烦。下面是在分页器中常用的热键:

  • 字母q:退出分页器。
  • 字母h:显示分页器帮助。
  • 按空格下翻一页,按字母 b 上翻一页。
  • 字母du:分别代表向下翻动半页和向上翻动半页。
  • 字母jk:分别代表向上翻一行和向下翻一行。
  • 如果行太长被截断,可以用左箭头和右箭头使得窗口内容左右滚动。
  • 输入/pattern:向下寻找和pattern匹配的内容。
  • 输入?pattern:向上寻找和pattern匹配的内容。
  • 字母nN:代表向前或向后继续寻找。
  • 字母g:跳到第一行;字母G:跳到最后一行;输入数字再加字母g:则跳转到对应的行。
  • 输入!<command>:可以执行Shell命令。

对于默认未提供分页器的Git命令,例如git status命令,可以通过下面任一方法启用分页器:

  • git和子命令(如status)之间插入参数-p--paginate,为命令启用内建分页器。如:

    $ git -p status
    
  • 设置Git配置变量,设置完毕后运行相应的命令,将启用内建分页器。

    $ git config --global pager.status true
    

Git命令的分页器支持带颜色的字符输出,对于太长的行则采用截断方式处理(可用左右方向键滚动)。如果不习惯分页器的长行截断模式而希望采用自动折行模式,可以通过下面任一方法进行设置:

  • 通过设置LESS环境变量来实现。

    $ export LESS=FRX
    
  • 或者通过定义Git配置变量来改变分页器的默认行为。

    $ git config --global core.pager 'less -+$LESS -FRX'
    

1.2.12. 快

您有项目托管在sourceforge.net的CVS或SVN服务器上么?或者因为公司的SVN服务器部署在另外一个城市需要经过互联网才能访问?

使用传统的集中式版本控制服务器,如果遇到上面的情况——网络带宽没有保证,那么使用起来一定是慢得让人痛苦不堪。Git作为分布式版本控制系统彻底解决了这个问题,几乎所有的操作都在本地进行,而且还不是一般的快。

还有很多其他的分布式版本控制系统,如Hg、Bazaar等。和这些分布式版本控制系统相比,Git在速度上也有优势,这源自于Git独特的版本库设计。第2篇的相关章节会向您展示Git独特的版本库设计。

其他很多版本控制系统,当输入检出、更新或克隆等命令后,只能双手合十然后望眼欲穿,因为整个操作过程就像是一个黑洞,不知道什么时候才能够完成。而Git在版本库克隆及与版本库同步的时候,能够实时地显示完成的进度,这不但是非常人性化的设计,更体现了Git的智能。Git的智能协议源自于会话过程中在客户端和服务器端各自启用了一个会话的角色,按需传输以及获取进度。