爱上Git的理由 **************** 本章通过一些典型应用展示Git作为版本控制系统的独特用法。对于不熟悉版本\ 控制系统的读者,可以通过这些示例对版本控制拥有感性的认识。如果是有经验的\ 读者,示例中的和SVN的对照可以让您体会到Git的神奇和强大。本章将列举Git\ 的一些闪亮特性,期待能够让您爱上Git。 每日的工作备份 =========================== 当我开始撰写本书时才明白写书真的是一个辛苦活。如何让辛苦的工作不会因为\ 笔记本硬盘的意外损坏而丢失?如何防范灾害而不让一个篮子里的鸡蛋都毁于一旦?\ 下面就介绍一下我在写本书时如何使用Git进行文稿备份的,请看图2-1。 .. figure:: /images/meet-git/work-backup.png :scale: 65 图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服务架设”\ 中予以介绍。 异地协同工作 =========================== 为了能够加快写书的进度,熬夜是必须的,这就出现了在公司和在家两地工作同步 的问题。图2-2用于说明我是如何解决两地工作同步的问题的。 .. figure:: /images/meet-git/workflow.png :scale: 65 图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 而我只要执行\ :command:`git pull`\ 操作就可以获得小崔对我文稿的修订\ (图2-2中的步骤⑧)。采用这种工作方式,文稿竟然分布在5台电脑上拥有6个拷贝,\ 真可谓狡兔三窟。不,比狡兔还要多三窟。 在本节中,出现在Git命令中的\ ``mirror``\ 和\ ``home``\ 是和工作区\ 关联的远程版本库。关于如何注册和使用远程版本库,请参见本书第3篇\ “第19章 远程版本库”中的内容。 现场版本控制 ============= 所谓现场版本控制,就是在客户现场或在产品部署的现场,进行源代码的修改,\ 并在修改过程中进行版本控制,以便在完成修改后能够将修改结果甚至修改过程\ 一并带走,并能够将修改结果合并至项目对应的代码库中。 **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的补丁文件\ 不支持二进制文件,因此采用补丁文件的方式有可能丢失数据,如新增或修改\ 的图形文件会丢失。更为稳妥但也更为复杂的方式可能要用到\ :command:`svnadmin`\ 命令将版本库导出。命令如下: :: $ svnadmin dump --incremental -r2:HEAD \ /path/to/repos/project1/ > hacks.dump 将\ :command:`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章 补丁中的二进制文件”\ 中的相关内容。 避免引入辅助目录 ================= 很多版本控制系统,都要在工作区中引入辅助目录或文件,如SVN要在工作区的\ 每一个子目录下都创建\ :file:`.svn`\ 目录,CVS要在工作区的每一个子目录下\ 都创建\ :file:`CVS`\ 目录。 这些辅助目录如果出现在服务器上,尤其是Web服务器上是非常危险的,因为这些\ 辅助目录下的\ :file:`Entries`\ 文件会暴露出目录下的文件列表,让管理员精心\ 配置的禁止目录浏览的努力全部白费。 还有,SVN的\ :file:`.svn`\ 辅助目录下还存在文件的原始拷贝,在文件搜索时\ 结果会加倍。如果您曾经在SVN的工作区用过\ :command:`grep`\ 命令进行内容查找,\ 就会明白我指的是什么。 Git没有这个问题,不会在子目录下引入讨厌的辅助目录或文件(\ :file:`.gitignore`\ 和\ :file:`.gitattributes`\ 文件不算)。当然Git还是要在工作区的顶级目录下\ 创建名为\ :file:`.git`\ 的目录(版本库目录),不过如果你认为唯一的一个\ :file:`.git`\ 目录也过于碍眼,可以将其放到工作区之外的任意目录。一旦这么\ 做了,你在执行Git命令时,要通过命令行(\ :command:`--git-dir`\ )或环境\ 变量\ :command:`GIT_DIR`\ 为工作区指定版本库目录,甚至还要指定工作区目录。 Git还专门提供了一个\ :command:`git grep`\ 命令,这样在工作区根目录下执行\ 查找时,目录\ :file:`.git`\ 也不会对搜索造成影响。 关于辅助目录的详细讨论请参见本书第2篇第4.2节中的内容。 重写提交说明 ============== 很多人可能如我一样,在敲下回车之后,才发现提交说明中出现了错别字,或忘记\ 了写关联的Bug ID。这就需要重写提交说明。 **SVN的解决方案** SVN的提交说明默认是禁止更改的,因为SVN的提交说明属于不受版本控制的属性,\ 一旦修改就不可恢复。我建议SVN的管理员只有在配置了版本库更改的外发邮件\ 通知之后,再开放提交说明更改的功能。我发布于SourceForge上的pySvnManager\ 项目,提供了SVN版本库图形化的钩子管理,会简化管理员的配置工作。 即使SVN管理员启用了允许更改提交说明的设置,修改提交说明也还是挺复杂的,\ 看看下面的命令: :: $ svn ps --revprop -r svn:log "new log message..." **Git的解决方案** Git修改提交说明很简单,而且提交说明的修改也是被追踪的。Git修改最新提交\ 的提交说明最为简单,使用一条名为修补提交的命令即可。 :: $ git commit --amend 这个命令如果不带“-m”参数,会进入提交说明编辑界面,修改原来的提交说明,\ 直到满意为止。 如果要修改某个历史提交的提交说明,Git也可以实现,但要用到另外一个命令:\ 变基命令。例如要修改\ ````\ 所标识提交的提交说明,执行下面的\ 命令,并在弹出的变基索引文件中修改相应提交前面的动作的关键字。 :: $ git rebase -i ^ 关于如何使用交互式变基操作更改历史提交的提交说明,请参见本书第2篇\ “第12章 改变历史”中的内容。 想吃后悔药 ============ 假如提交的数据中不小心包含了一个不应该检入的虚拟机文件——大约有1个GB!\ 这时候,您会多么希望这个世界上有后悔药卖啊。 **SVN的解决方案** SVN遇到这个问题该怎么办呢?删除错误加入的大文件,再提交,这样的操作是\ 不能解决问题的。虽然表面上去掉了这个文件,但是它依然存在于历史中。 管理员可能是受影响最大的人,因为他要负责管理服务器的磁盘空间占用及版本库\ 的备份。实际上这个问题也只有管理员才能解决,所以你必须向管理员坦白,让他\ 帮你在服务器端彻底删除错误引入的大文件。我要告诉你的是,对于管理员,这并\ 不是一个简单的活。 1. SVN管理员要是没有历史备份的话,只能从头用\ :command:`svnadmin dump`\ 导出整个版本库。 2. 再用\ :command:`svndumpfilter`\ 命令过滤掉不应检入的大文件。 3. 然后用\ :command:`svnadmin load`\ 重建版本库。 上面的操作描述中省略了一些窍门,因为要把窍门说清楚的话,这本书就不是讲\ Git,而是讲SVN了。 **Git的解决方案** 如果你用Git,一切就会非常简单,而且你也不必去乞求管理员,因为使用Git,\ 每个人都是管理员。 如果是最新的提交引入了不该提交的大文件:\ :file:`winxp.img`\ ,操作起来会\ 非常简单,还是用修补提交命令。 :: $ git rm --cached winxp.img $ git commit --amend 如果是历史版本,例如是在\ ````\ 所标识的提交中引入的文件,\ 则需要使用变基操作。 :: $ git rebase -i ^ 执行交互式变基操作抛弃历史提交,版本库还不能立即瘦身,具体原因和解决方案\ 请参见本书第2篇“第14章 Git库管理”中的内容。除了使用变基操作,Git还有更多\ 的武器可以实现版本库的整理操作,具体请参见本书第6篇第35.4节的内容。 更好用的提交列表 ====================== 正确的版本控制系统的使用方法是,一次提交只干一件事:完成一个新功能、修改\ 了一个Bug、或是写完了一节的内容、或是添加了一幅图片,就执行一次提交。\ 而不要在下班时才想起来要提交,那样的话版本控制系统就被降格为文件备份系统了。 但有时在同一个工作区中可能同时在做两件事情,一个是尚未完成的新功能,另外\ 一个是解决刚刚发现的Bug。很多版本控制系统没有提交列表的概念,或者要在\ 命令行指定要提交的文件,或者默认把所有修改内容全部提交,破坏了一个提交干\ 一件事的原则。 **SVN的解决方案** SVN 1.5开始提供了变更列表(change list)的功能,通过引入一个新的命令\ :command:`svn changelist`\ 来实现。但是我从来就没有用过,因为: * 定义一个变更列表太麻烦。例如不支持将当前所有改动的文件加入列表,也不支\ 持将工作区中的新文件全部加入列表。 * 一个文件不能同时属于两个变更列表。两次变更不许有文件交叉,这样的限制太\ 牵强。 * 变更列表是一次性的,提交之后自动消失。这样的设计没有问题,但是相比定义\ 列表时的繁琐,以及提交时必须指定列表的繁琐,使用变更列表未免得不偿失。 * 再有,因为Subversion的提交不能撤销,如果在提交时忘了提供变更列表名称\ 以针对特定的变更列表进行提交,错误的提交内容将无法补救。 总之,SVN的变更列表尚不如鸡肋,食之无味,弃之不可惜。 **Git的解决方案** Git通过提交暂存区实现对提交内容的定制,非常完美地实现了对工作区的修改\ 内容进行筛选提交: * 执行\ :command:`git add`\ 命令将修改内容加入提交暂存区。执行\ :command:`git add -u`\ 命令可以将所有修改过的文件加入暂存区,执行\ :command:`git add -A`\ 命令可以将本地删除文件和新增文件都登记到提交\ 暂存区,执行\ :command:`git add -p`\ 命令甚至可以对一个文件内的修改\ 进行有选择性的添加。 * 一个修改后的文件被登记到提交暂存区后,可以继续修改,继续修改的内容不会 被提交,除非再对此文件再执行一次\ :command:`git add`\ 命令。即一个修改\ 的文件可以拥有两个版本,在提交暂存区中有一个版本,在工作区中有另外一个\ 版本。 * 执行\ :command:`git commit`\ 命令提交,无须设定什么变更列表,直接将登记\ 在暂存区中的内容提交。 * Git支持对提交的撤消,而且可以撤消任意多次。 只要使用Git,就会时刻在和隐形的提交列表打交道。本书第2篇“第5章 Git暂存区”\ 会详细介绍Git的这一特性,相信你会爱上Git的这个特性。 更好的差异比较 ================= Git对差异比较进行了扩展,支持对二进制文件的差异比较,这是对GNU的\ :command:`diff`\ 和\ :command:`patch`\ 命令的重要补充。还有Git的差异比较\ 除了支持基于行的差异比较外,还支持在一行内逐字比较的方式,当向\ :command:`git diff`\ 命令传递\ :command:`--word-diff`\ 参数时,就会进行\ 逐字比较。 在上面介绍了工作区的文件修改可能会有两个不同的版本,一个是在提交暂存区,\ 一个是在工作区。因此在执行\ :command:`git diff`\ 命令时会遇到令Git新手\ 费解的现象。 * 修改后的文件在执行\ :command:`git diff`\ 命令时会看到修改造成的差异。 * 修改后的文件通过\ :command:`git add`\ 命令提交到暂存区后,再执行\ :command:`git diff`\ 命令会看不到该文件的差异。 * 继续对此文件进行修改,再执行\ :command:`git diff`\ 命令,会看到新的修改 显示在差异中,而看不到旧的修改。 * 执行\ :command:`git diff --cached`\ 命令才可以看到添加到暂存区中的文件所 做出的修改。 Git差异比较的命令充满了魔法,本书第5章第5.3节会带您破解Git的diff魔法。\ 一旦您习惯了,就会非常喜欢\ :command:`git diff`\ 的这个行为。 工作进度保存 ============== 如果工作区的修改尚未完成时,忽然有一个紧急的任务,需要从一个干净的工作区\ 开始新的工作,或要切换到别的分支进行工作,那么如何保存当前尚未完成的工作\ 进度呢? **SVN的解决方案** 如果版本库规模不大,最好重新检出一个新的工作区,在新的工作区进行工作。\ 否则,可以执行下面的操作。 :: $ svn diff > /path/to/saved/patch.file $ svn revert -R $ svn switch 在新的分支中工作完毕后,再切换回当前分支,将补丁文件重新应用到工作区。 :: $ svn switch $ patch -p1 < /path/to/saved/patch.file 但是切记SVN的补丁文件不支持二进制文件,这种操作方法可能会丢失对二进制\ 文件的更改! **Git 的解决方案** Git提供了一个可以保存和恢复工作进度的命令\ :command:`git stash`\ 。\ 这个命令非常方便地解决了这个难题。 在切换到新的工作分支之前,执行\ :command:`git stash`\ 保存工作进度,\ 工作区会变得非常干净,然后就可以切换到新的分支中了。 :: $ git stash $ git checkout 新的工作分支修改完毕后,再切换回当前分支,调用\ :command:`git stash pop`\ 命令则可恢复之前保存的工作进度。 :: $ git checkout $ git stash pop 本书第2篇“第9章 恢复进度”会为您揭开\ :command:`git stash`\ 命令的奥秘。 代理SVN提交实现移动式办公 ========================== 使用像SVN一样的集中式版本控制系统,要求使用者和版本控制服务器之间要有\ 网络连接,如果因为出差在外或在家办公访问不到版本控制服务器就无法提交。\ Git属于分布式版本控制系统,不存在这样的问题。 当版本控制服务器无法实现从SVN到Git的迁移时,仍然可以使用Git进行工作。\ 在这种情况下,Git作为客户端来操作SVN服务器,实现在移动办公状态下的版本提交\ (当然是在本地Git库中提交)。当能够连通SVN服务器时,一次性将移动办公状态\ 下的本地提交同步给SVN服务器。整个过程对于SVN来说是透明的,没有人知道你\ 是使用Git在进行提交。 使用Git来操作SVN版本控制服务器的一般工作流程为: 1. 访问SVN服务器,将SVN版本库克隆为一个本地的Git库,一个货真价实的Git库,\ 不过其中包含针对SVN的扩展。 :: $ git svn clone 2. 使用Git命令操作本地克隆的版本库,例如提交就使用\ :command:`git commit`\ 命令。 3. 当能够通过网络连接到SVN服务器,并想将本地提交同步给SVN服务器时,先获取\ SVN服务器上最新的提交,再执行变基操作,最后再将本地提交推送给SVN服务器。 :: $ git svn fetch $ git svn rebase $ git svn dcommit 本书第4篇“第26章 Git和SVN协同模型”中会详细介绍这一话题。 无处不在的分页器 ================== 虽然拥有图形化的客户端,但Git更有效率的操作方式还是命令行操作。使用命令行\ 操作的好处一个是快,另外一个就是防止鼠标手的出现。Git的命令行进行了大量\ 的人性化设计,包括命令补全、彩色字符输出等,不过最具特色的还是无处不在的\ 分页器。 在操作其他版本控制系统的命令行时,如果命令的输出超过了一屏,为了能够逐屏\ 显示,需要在命令的后面加上一个管道符号将输出交给一个分页器。例如: :: $ svn log | less 而Git则不用如此麻烦,因为常用的Git的命令都带有一个分页器,当一屏显示\ 不下时启动分页器。分页器默认使用\ :command:`less`\ 命令\ (\ :command:`less -FRSX`\ )进行分页。 因为\ :command:`less`\ 分页器在翻屏时使用了vi风格的热键,如果您\ 不熟悉vi的话,可能会遇到麻烦。下面是在分页器中常用的热键: * 字母\ ``q``\ :退出分页器。 * 字母\ ``h``\ :显示分页器帮助。 * 按空格下翻一页,按字母 b 上翻一页。 * 字母\ ``d``\ 和\ ``u``\ :分别代表向下翻动半页和向上翻动半页。 * 字母\ ``j``\ 和\ ``k``\ :分别代表向上翻一行和向下翻一行。 * 如果行太长被截断,可以用左箭头和右箭头使得窗口内容左右滚动。 * 输入\ ``/pattern``\ :向下寻找和pattern匹配的内容。 * 输入\ ``?pattern``\ :向上寻找和pattern匹配的内容。 * 字母\ ``n``\ 或\ ``N``\ :代表向前或向后继续寻找。 * 字母\ ``g``\ :跳到第一行;字母\ ``G``\ :跳到最后一行;\ 输入数字再加字母\ ``g``\ :则跳转到对应的行。 * 输入\ ``!``\ :可以执行Shell命令。 对于默认未提供分页器的Git命令,例如\ :command:`git status`\ 命令,\ 可以通过下面任一方法启用分页器: * 在\ :command:`git`\ 和子命令(如\ :command:`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' 快 ====== 您有项目托管在sourceforge.net的CVS或SVN服务器上么?或者因为公司的SVN\ 服务器部署在另外一个城市需要经过互联网才能访问? 使用传统的集中式版本控制服务器,如果遇到上面的情况——网络带宽没有保证,\ 那么使用起来一定是慢得让人痛苦不堪。Git作为分布式版本控制系统彻底解决了\ 这个问题,几乎所有的操作都在本地进行,而且还不是一般的快。 还有很多其他的分布式版本控制系统,如Hg、Bazaar等。和这些分布式版本控制\ 系统相比,Git在速度上也有优势,这源自于Git独特的版本库设计。第2篇的相关\ 章节会向您展示Git独特的版本库设计。 其他很多版本控制系统,当输入检出、更新或克隆等命令后,只能双手合十然后\ 望眼欲穿,因为整个操作过程就像是一个黑洞,不知道什么时候才能够完成。而\ Git在版本库克隆及与版本库同步的时候,能够实时地显示完成的进度,这不但是\ 非常人性化的设计,更体现了Git的智能。Git的智能协议源自于会话过程中在\ 客户端和服务器端各自启用了一个会话的角色,按需传输以及获取进度。