Linux 和 UNIX 系统使用了许多 VCS,从已经绝迹的 Revision Control System (RCS) 和 Concurrent Versions System (CVS) 到更现代的系统,如 Subversion、Mercurial、Bazaar、Arch 和 Darcs。具有讽刺意味的是(尤其在 Linux 领域中),Git 作为一个商业 VCS(BitKeeper)的替代产品开发,BitKeeper 具有独特、印象深刻的功能,并且是一个免费版本。BitKeeper 依然让人印象深刻,但是许可更改最后导致 Torvalds 开始寻找替代产品;在免费软件潮流下,他最后决定编写自己的软件。像 Linux 内核一样,Git 现在由许多开源开发人员贡献产品增强、bug 修复和其他内容。Git 的吸引力在于它强大的功能,也在于它的成本低廉 —— 免费软件,它很快被许多开源软件项目、研究所和组织所采纳。一旦 “进入” 公司或研究所之后,Git 既可以作为 VCS 又可以作为协作平台,这促使许多传统 “源代码” 之外的项目采用它。正如本文将介绍的,Git 在复杂、分布式 Web 开发项目中非常有用,这些项目对内容和开发都有很多要求,需要不同人员之间一直保持交互。
2005年4月3日,Linus发布了Linux内核的一个候选版本2.6.12-rc2,他表示这个发布候选版本并不是太有趣,“diffstat(用于显示文件差异统计信息的工具)输出说明了一切:这是许多非常小的变更,融合了大量小的改进和错误修复”,但这个版本成为了一个重要的标志,因为这是最后一个Linux内核的非Git版本。
BitKeeper
在使用BitKeeper(分布式版本控制系统)之前的十年里,Linux内核的版本控制一直是Linus自己。具体流程如下:开发人员将tarballs和补丁提交给几个Linus信任的助手。助手们通过审核后,将补丁发送给Linus。最后,Linus亲手将它们合并到自己的源代码树中,然后发布。当然,Linus本人并不是一个“完美的版本控制服务”。1998年,知名程序员Larry McVoy通过Linux内核邮件列表首次提出了BitKeeper的想法,他写道:“很明显,我们的领袖[Linus]目前有点超负荷,补丁可能会丢失。”
尽管如今看来,这种手动的工作流程也未免太原始了,但在当时,Linus认为这种工作流程比其他选择(主要是CVS)更好。后来,2007年Linus在Google发表了关于Git的演讲,他提到了他的一个核心设计原则:“有什么是CVS不会做的?”当然,这种厌恶自然也延伸到了SVN,同样是在那次演讲中,他笑着说:“观众席中有Subversion用户吗?你们可以离席了。我非常讨厌CVS,所以我认为Subversion是有史以来最没有意义的项目。有一段时间里Subversion的整体口号是正确实现CVS。以这个口号为出发点,你将一事无成。因为CVS根本无法正确实现。”
Linus认为CVS的核心问题在于其集中化的性质。由于Linux开发人员有数百名之多,所以Linus认为每个人都拥有自己独立的代码库副本至关重要,因为只有这样他们才能在自己的分支上开发。这不仅对线下的工作有帮助,而且对内部管理也很有帮助。每位开发人员都可以自由地向自己的代码库提交任何代码,而且他们有机会说服社区他们的变更是有价值的。这样可以防止拥有提交权限的贡献者成为唯一的代码库的守门人。
BitKeeper与CVS截然不同。在前面提到的1998年关于BitKeeper的介绍中,Larry McVoy勾勒了一个系统,这个系统与如今我们所了解的源代码控制非常相似,但在当时却完全不同。Larry McVoy写道:支持所有这些操作的机制是一个分布式源代码管理系统。该系统的主要特点是:
每个人都有一个代码库(相比之下,CVS只有一个代码库)。代码变更可以作为“超级补丁”(又称为“变更集”)发送。变更集只是一个补丁文件,包含以下内容:
所有变更,按照修订版本组织。
一个标识符,表示补丁应该应用在树的哪个位置(如果你的更新落后于补丁发送者,则补丁将失败)。
所有变更的修订历史。
元数据,如路径名变更,符号标签(如 alpha2 或 linux-2.1.133)等。
一个名为“开发线”(LOD)的新概念。从逻辑上讲,它是一个分支,但不需要在分支上。补丁可以(也将会)成为自己的 LOD。你可以在LOD上执行“将此应用于主分支”之类的操作。
后来,Linus对BitKeeper表示了极大的赞赏,认为BitKeeper改变了他的看法,而Git的灵感也来源于此:“BitKeeper不仅是第一个我觉得值得尝试的版本控制系统,也让我明白了使用这类系统的意义以及如何正确使用这类系统。所以,从技术的角度来看,Git的许多方面虽然与BitKeeper有很大的区别(这是另一个设计目标,因为我不希望Git成为BitKeeper的克隆),但Git的许多工作流程都借鉴了BitKeeper。”
许可
虽然Linus对BitKeeper的评价很高,但2002年在Linux内部使用该工具的问题上,他的决定引发了Linux内核邮件列表上大规模的争论。争论源自何处?Larry McVoy在构建BitKeeper时,将其作为了一个商业的闭源项目(BitMover)。尽管人们能够使用BitKeeper的免费社区版本,但有一个限制性许可。
根据维基百科记载:“社区版BitKeeper的许可允许开发人员在开源或自由软件项目中免费使用该工具,前提是这些开发人员在使用BitKeeper期间以及之后的一年内,不得参与竞争工具(如Concurrent Versions System、GNU arch、Subversion以及ClearCase)的开发。不论竞争工具是免费还是专有的,这条限制都适用。”
自由软件的宣传者Richard Stallman也发表了意见:“BitKeeper许可掌握了控制权。实际上是在说,‘你没有权利使用BitKeeper,只有临时的使用许可,而我们可以随时撤销这个许可。我们允许你使用BitKeeper,你应该对此心存感激。不要做任何我们不喜欢的事情,否则我们可能会撤销这些特权。自由软件运动的产生正是源自大家对这种许可的愤怒。’”
但Linus采取了更务实的观点,从他的角度来看,他不过是需要最适合的工具,而不会在意这些工具源自何方。2007年,他曾表示:“尽管BitKeeper的许可有问题,但我仍然很高兴,因为坦白说,对于我而言,我做开源是因为我认为开源才是构建软件的唯一正确方式。但同时,我会使用最适合的工具,坦白说,BitKeeper就是我想要的工具。”
然而,这段不安的联姻无法持久。2005年,Linux内核开发人员Andrew Tridgell违反了许可,并实施了逆向工程。他能够提取BitKeeper代码而不需要遵循BitKeeper许可。在Andrew Tridgell看来,这完全符合道德准则:“我在编写这个工具时根本没有使用BitKeeper,因此从未受过BitKeeper许可的约束。”
然而Larry McVoy对此有不同的看法。最初,Linus是站在他这边的:“如果有人编写一个免费的替代品,Larry完全能接收……Larry不能接受的是,有人通过逆向工程他的代码编写了一个免费的替代品。Larry的道德立场非常明确:‘你可以与我竞争,但你不能搭我的便车。自己解决问题,公平竞争。不要通过我的解决方案来竞争。’BitKeeper许可也正是为了表达这一点:‘少来搭我的便车,你这个爱占便宜的家伙。’而我[Linus]无法反驳这一点。”
Linus充当了三个月的调停,但最终未能达成和解。
2005年4月6日,Linus通过Linux内核邮件列表发了一封主题为“内核SCM长篇故事……”的邮件,开启了一系列改变整个行业的事件:“可能部分人已知情,在过去的一两个月里,我们一直在努力解决BitKeeper的使用冲突。但并未取得成功,因此内核团队正在寻找替代方案。”他开玩笑地谈到了历史:“并不是说我选择BitKeeper完全没有冲突”,并强调了他对BitKeeper团队的感激之情。尽管如此,Linus回顾这段时光时仍然充满了喜悦:
事实上,BitKeeper从根本上改变了我们的做事方式。从细粒度的变更集跟踪,到我最终信任次级维护者承担更加重要的工作,我们无需再逐个出补丁。因此,与BitKeeper的三年合作绝对没有浪费,我相信我们改进了工作方式,而我正在考虑的事情之一就是确保这些方式持续有效。我想说,我个人对BitKeeper和Larry非常满意。虽然我们的合作未能成功,但对Linux内核的开发产生了重大影响。我们必须找到一套工具来代替BitKeeper的功能。
Linus去度假
虽然4月6日Linus通过邮件列表公布了双方合作失败的消息,但实际上他已经展开了紧张的工作。就在2.6.12-rc2发布的三天前,他停止了Linux内核的工作,并全力寻找BitKeeper的替代方案。
Linus的目标是“在两周内拿出可以投入使用的工具。”他在4月6日的邮件中宣布:“我将离线一周(你可以认为‘Linus去度假了’),我请求继续维护BitKeeper树的人可以把结果作为(单独的)补丁发送给我,因为我也需要合并一部分代码。”
很明显,Linus的邮件透漏出了紧迫感,Linux内核的下一个版本发布受阻,他必须尽快解决这个问题。他甚至在4月7日的一封邮件中提到了最坏打算,Linux内核可能会转而使用集中式的版本控制系统:“请注意,我厌恶集中式的SCM模型,但如果事态严重,导致我们短时间内(一个月或两个月)无法并行合并代码,我会通过一个信赖的网站搭建SVN之类的系统,允许少数几个人提交代码,并将合并的工作分给几个人,避免我自己成为瓶颈。”
虽然事后来看,结果很明显,但在当时,根据这些邮件,编写一个自定义的版本控制系统尚遥不可及。从4月6日的第一封邮件到4月12日的最后一封邮件,期间来往的邮件多达205封,他们讨论了许多其他开源替代方案,比如Monotone、GNU arch、Bazaar-ng、Darcs,其中一些工具的创建者也参与进来宣传自己的项目。主要考虑因素,尤其是从Linus的角度来看,是每个工具的总体性能。总的来说,205封邮件涉及很多关于各种工具的性能和效率的讨论。
每个人心中最大的忧虑在于:现有工具中是否有一款适用于Linux内核这种规模的项目?
4月8日,距离最初的邮件发出过去了两天,也就是Linus全身心投入工作5天后,他分享了一个最新消息:“由于monotone太慢,请各位在手头的工作之余,试试看这个快速的挑战:kernel.org:/pub/linux/kernel/people/torvalds/”
Git诞生了。
Git的第一行代码
有关Linus编写初版Git仅用了两周时间的传闻,我们需要加上一则补充说明:如今我们所说的git主要是面向用户的命令和整体工作流程,但当时的目标和任务有很大的不同,而且范围非常有限。人们在邮件列表上争论各种工具和方法的优缺点时,有人粗略描述了他们的需求:“慢一点没关系,只要不是慢得离谱。临时解决方案的目的是建立线上的补丁工作流程。”
初版的Git更像是一个内容可寻址的文件系统,算不上是一个完整的源代码管理系统。Linus在另一封邮件中解释说:我称之为“提交”,但实际上更简单。实际上表明“这是我的 <目录的当前状态> ,我从 <之前的目录状态集合> 到达了这个状态,其原因是<**>”。
顺便说一下,我的设计意图就在于此。“git”并不在意合并之类的操作。你可以使用任何SCM合并代码。“git”所做的只是跟踪目录状态(以及发展到该状态的经过),仅此而已。git并不会执行合并操作,实际功能并不多。也就是说,你在Git存档上“拉取”或“推送”时,就会得到所有目录状态的“联合”。其中的HEAD就是指向“目录状态之海”的一个指针,但要想合并两个目录状态,就必须使用其他工具。
虽然来往的邮件讨论了各种工作流程和移动提交,但Linus构建的客户端并没有考虑这些操作。Linus表示:“实际上‘Git’非常简单,仅用了4天就编写完成了。大部分时间实际上并不是在编写代码,而是思考数据结构。”
这是他后来反复提及的感受,有时会被断章取义,但确实Linus所编写代码的新颖之处就在于数据结构的选择。Linus分享了前几次提交,但没有关于工作流程的讨论。少数人主张使用SQL来存储变更。Linus在后来的对话(很符合他个人的发言风格)中表示:
> 为什么后端使用了目录树,而没有使用sql?
因为sql很糟糕?
我可以想出几百万种方法来降低速度。请你提出提高速度的方法。
—— Linus
但也有一些人看好Git,并渴望使用Git。就在Linus请求大家查看他的代码的同一天,有几个人发回了一些脚本,其中包含了在他的基础之上构建的其他功能。在Linus投入真正的工作两周后,即2005年4月17日,他发了一封邮件:“第一次真正的内核Git合并!”。
Git:不仅仅是 VCS
Git 可以促进不同位置(不同 Internet 连接级别)成千上万开发人员之间的分布式开发,没有明显的性能或访问瓶颈。Git 中支持这些基本要求的最重要的方面包括:
* 使用中央存储库,但为项目中的每一名开发人员提供完整的项目源代码副本。这可以保证所有开发人员都能完成他们的工作,无论他们当前的连接情况如何。
* 支持快速、可靠地在软件项目中创建不同的工作集(称为分支),可以跨分支共享更改(称为合并)。分支可以轻松支持不同版本的软件包,无论这些版本是永久的还是实验性的。合并一般是源代码控制系统中的关键方面,在面向分支的 VCS 尤其常见。
* 可以轻松在开发人员子集之间共享分支和代码更改,这些更改无需先签入中央存储库。
这些设计决策及其实现是 Git 成功和流行的关键所在。当然,Git 也满足标准的 VCS 要求,比如不变性和问责制。不变性 表示将更改提交到存储库之后,它们就成为项目历史记录不变的一部分。即使之后可以取消(称为还原),但这些更改和取消这些更改的替换代码也都成为项目历史不变的一部分。问责制 意味着可以轻松确定谁做了什么更改以及何时做了该更改。为什么做出更改仍然无法确定(尽管在提交更改时需要一些注释),但至少我们知道该去问谁。Git 使用内部索引跟踪文件的状态和存储库中的目录。它还存储能够反映这些文件和目录更改的对象,以简化后续更新。Git 索引和这些对象与本地存储库中的实际文件和目录不同 — 这个模型可以轻松确定本地更改,但尚未提交给本地存储库或远程中央存储库(如果有)的文件和目录。有些 Git 命令可以操作索引,另一些可以操作实际文件和目录内容,如果您用错了命令,可能会发现文件没有被更新。
获取 Git
大部分 Linux 发行版在其包存储库中提供 Git 包。在 Ubuntu、Debian 和使用 .deb 包格式的类似系统中,您需要安装 git-core 包。在基于 RPM 的系统上,比如 Fedora、Red Hat、Centos、SUSE 和 Mandriva,主 Git 包称为 git。基本的 Git 包需要使用 Perl、Perl 库进行加密和错误处理,系统上还将安装补丁实用工具。
如果您的 Linux 系统需要最新最棒的 Git 版本,Git 网站还提供了预打包 .deb 和 rpm 包,以及最新 Git 源代码(如果需要构建自己的版本)的下载链接。Git 站点还提供了预编译版的 Git for Mac® OS X、本机 Windows®、Windows 系统上的 Cygwin、Sun/Oracle Solaris 系统的下载链接。目前,IBM® AIX 和 Hewlett-Packard HP-UX 系统管理员必须通过它们平台的源代码构建 Git。有关获取和构建平台 Git 的信息,请参见参考资料。
主 Git 包包含 git 可执行文件和一些辅助 Git 应用程序。可以猜想,还有大量其他与 Git 相关的包可供使用,一些常用的 Git 相关包包括:
* git-cola:用于处理 Git 存储库中文件和目录的 GUI
* git-doc:在本地安装 Git 用户手册、教程和文档
* git-gui:浏览和处理 Git 存储库的 GUI;使用 gitk 包
* git-web:基于 Web 的图形化 Git 存储库接口
* gitk:一个浏览和处理 Git 存储库的简单 GUI
* qgit:基于 Qt 的图形化应用程序,用户查看和浏览 Git 存储库
git-gui、git-web、gitk 和 qgit 包提供类似的功能,git-web 是基于 Web 的,所有其他包都是在本地运行的。刚开始接触 Git 时,这些包都非常有用,而 git-web 包可能是分布式环境中的最佳选择。
如果您对 Git 感兴趣,但是又已经使用了其他 VCS,那么以下包非常有用:
* git-cvs:该包提供 Git 与 CVS 存储库之间的交互操作,使您能将 CVS 存储库和更改历史导入 Git,在 Git 中进行处理,将更改重新合并到 CVS 存储库,并导入 CVS 存储库的更新。
* git-svn:该包提供 Git 和 Subversion 存储库之间的交互操作,使您能将 Subversion 存储库和更改历史导入 Git,在 Git 中进行处理,将更改重新合并到 Subversion 存储库,并导入 Subversion 存储库的更新。
通用 Git 命令
Git 就是我们常说的命令套件 平台,这意味着您使用的主命令是 git,命令行的第一个参数表示您想运行的特定 Git 命令。任何时候,只要运行不带任何参数的 git 就可以看到通用 Git 命令清单。以下列出部分该清单:
* add:向 Git 索引添加一个新文件。您必须提交该文件,以向 Git 存储库添加其内容。
* branch:使您能列出签出(check out)的分支,确定当前处理的分支,创建新分支,销毁已经创建或签出的本地分支副本。该命令不能切换到其他分支:使用 Git 的 checkout 命令实现。
* checkout:签出指定的分支或文件/目录。如果签出一个分支,则该分支将成为工作分支。如果指定一个特定文件或目录,则该文件或目录将更新以匹配当前签入工作分支目录的版本。您还可以使用该命令基于指定现有分支的可选跟踪更改新建一个分支。
* commit:记录 Git 索引中的文件和目录更改。指定想提交更改的文件和目录时,您可以使用 -a 选项将所有挂起更改添加到 Git 跟踪的文件,也可以使用 --interactive 选项选择希望一起提交的文件和目录更改。(如果处理的是几个涉及大量文件的不同任务,但又想一起提交某些更改,那么后者将非常有用。提交是对本地存储库执行的操作;如果使用远程中央存储库,那么必须使用 Git 的 push 命令将本地更改传输到远程存储库。)
* diff:显示本地文件与其他提交之间,或者两次不同提交的文件之间的不同之处。使用该命令时常常指定文件名,显示指定文件与提交到当前分支存储库的文件版本之间的不同之处。
* fetch:从另一个存储检索索引更新,指出已经创建的新标记,并提供有关已经提交到该存储库但尚未在本地显示的文件或目录更改的信息。然后,可以使用 git log 命令检测可用更改。要在 fetch 之后实际检索相关文件更改,您可以使用 git pull 或 git rebase 命令。
* grep:查询或显示当前分支中文件模式的匹配。该命令支持大部分您所喜爱的 GNU grep 选项。
* log:展示当前分支或当前分支中指定文件的提交日志信息。
* merge:将一个分支的更改合并到另一个。该命令提供一些选项,用于确定是否自动提交合并的更改,这使您能在实际接受这些更改之前了解合并的影响。
* mv:重命名 Git 当前跟踪的文件、目录或符号链接。
* pull:从另一个存储库检索索引更新,并将其合并到当前分支的文件或目录。
* push:使用本地索引和对象更改信息更新远程存储库。
* rebase:更新当前分支以匹配远程分支,修改尚未传输到远程分支的本地提交,使其适用于远程分支的当前状态。这是一个强大而危险的命令,因为它会根据需要重写命令以便合并。如果对远程存储库的更改频率和范围不大,可以使用 git pull 命令替代。
* rm:移除 Git 当前跟踪的文件、目录或符号链接。
* stash:临时将当前更改推入堆栈,并将当前签出返回到原始状态。git stash save 在本地堆栈中保存当前更改,git stash apply 检索并重新应用它们。如果您想 fetch 远程更改或者要在不永久提交进行中的更改的情况下执行 rebase,那么这将非常有用。
* status:展示当前分支的状态,指出尚未提交的更改、不会跟踪的文件等等。
所有 Git 命令都接受 --help 选项,这是一个常用的选项,可以获取任何命令的详细信息,查看每个命令接受的命令行选项清单等等。您还可以执行命令 git help command 获取有关任何 Git 命令的帮助。有关 Git 命令的完整清单,请执行 man git 命令查看 Git 的在线参考信息。
设置一个新的 Git 项目
要对不受任何修订控件控制的现有项目使用 Git,请执行以下步骤:
1. 更改为包含项目源代码的目录:
$ cd www.move2linux.com
$ ls
greenbar_wide.gif images index.html legacy.html services.html
2. 使用 git init 命令在当前目录中创建一个空的存储库:
$ git init
Initialized empty Git repository in .git/
3. 还可以选择使用 git status 命令查看新项目的状态。
该命令列出当前目录中未跟踪的所有文件和目录,表示 Git 了解它们的存在但是没有指示跟踪该文件:
$ git status
# On branch master
#
# Initial commit
#
# Untracked files:
# (use "git add
#
# greenbar_wide.gif
# images/
# index.html
# legacy.html
# services.html
nothing added to commit but untracked files present...
4. 将项目中的文件和目录添加到新的 Git 存储库。
您可以明确列出它们或者使用句点(.)作为 “当前目录内容” 的传统快捷方式:
$ git add .
5. 再次执行 git status 命令验证当前目录及其子目录中所有文件都已经添加到新项目中:
$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
# (use "git rm --cached
#
# new file: greenbar_wide.gif
# new file: images/digits/b/0.gif
# new file: images/digits/b/1.gif
# new file: images/digits/b/4.gif
# new file: images/digits/b/5.gif
# new file: images/digits/b/6.gif
# new file: images/digits/b/7.gif
# new file: images/digits/b/8.gif
# new file: images/digits/b/9.gif
# new file: index.html
# new file: legacy.html
# new file: services.html
#
6. 执行 git commit 命令签入初始文件。
除非使用 -m "commit message" 选项在命令行上指定提交消息,否则该命令将启动默认编辑器,您必须在其中输入一个与提交关联的注释。保存命令和现有编辑器之后,Git 将签入与更改关联的文件,并显示有关提交和关联文件的信息。
$ git commit
Created initial commit dfbd6cc: Initial checkin
12 files changed, 285 insertions(+), 0 deletions(-)
...
现在,您可以使用上述命令在 Git 中处理项目文件了。
更改现有 Git 项目
如果可以更改其他人已经创建的 Git 项目,事情会变得更加简单。您可以使用 git clone 命令创建自己的 Git 项目工作副本:
$ git clone /home/jake/src/maps
该示例在当前工作目录中创建一个 Git 地图项目副本。您可以将目录更改为地图项目副本,并使用之前讨论的命令在 Git 中开始处理该项目中的文件。
复制 Git 项目
复制位于另一个机器上的 Git 项目非常简单。Git 默认支持 Secure shell (SSH) 和 HTTP 协议,如果远程系统上运行着 Git daemon,还可以使用特别有效的 git 协议导出感兴趣的项目。Git 默认使用 SSH,因此用于通过 SSH 复制目录的语法与您的预期完全一致:
$ git clone remote-machine:/home/jake/src/maps
$ git clone ssh://remote-machine/home/jake/src/maps
注意:通过 SSH 复制 Git 项目要求您授权对远程系统的访问,这是最好使用 git 协议的又一原因,尽管它要求您运行 Git daemon。
使用 Git 进行分布式 Web 开发
存储库(如上文创建的存储库)包含 Git 项目中所有文件的工作副本,以及 Git 跟踪更改、分支、标记等内容时需要的所有文件。默认情况下,推入包含项目文件特定工作副本的 Git 存储库仅更新该项目的索引,而不是项目中的实际文件。这是因为如果您同时还在更新的文件上工作,更新文件本身将造成合并冲突。
要创建可以推入的 Git 项目,您需要创建一个所谓的裸 存储库 — 即不包含文件工作副本,仅包含 Git 索引、反映该索引更新的对象,以及 Git 要求的其他文件。因为裸存储库不包含文件的工作副本,因此没人能真正在其中工作,它仅用作在其所包含项目上工作的所有开发人员的收集点。
执行以下步骤可在Web 服务器上创建 一个包含网站内容且您和其他开发人员可以执行推入的 Git 存储库。该过程还将使用包含新 Git 存储库签出版本的目录以及文件推入共享 Git 存储库时更新的目录替换现有的 Web 内容目录。有许多方法可以做到这一点:为了该示例的简单易懂,您的网站上仅涉及 HTML 内容。处理网站的其他部分时可以使用相同的原则。
1. 在 Web 服务器上使用 SSH,并更改为包含 Web 内容的目录。
如果 Git 没有跟踪您的 Web 内容,则使用 上一节 中描述的过程在此设置 Git 存储库。例如:
$ ssh somehost
$ cd /var/www/html
$ git init
$ git add .
$ git commit -m "Initial commit"
2. 更改到上一级目录,通过复制刚刚为 Web 内容创建的项目创建一个裸 Git 存储库:
$ cd ..
$ git clone --bare html html.git
使用 .git 扩展名创建裸存储库是一个好习惯,这样您就可以使用 gitweb 等工具查看,这些工具要求该扩展名。
3. 重新命名现有的 Web 目录,并通过复制裸存储库使用相同的名称新建一个 Git 项目:
$ mv html html.OLD && git clone html.git html
新项目目录包含对应于 Web 服务器内容的 Git 项目中所有文件的签出版本。
4. 在裸存储库的 hooks 子目录中编辑(或创建)一个更新后处理(post-update)脚本,将更改推入包含 Web 内容的签出文件的新项目目录。
该脚本将在裸存储库跟踪的任何文件更新后执行一次。确保该脚本可以执行:
$ pushd html.git/hooks
$ emacs post-update
$ chmod 755 post-update
更新后处理脚本应该类似 清单 1。
清单 1. 更新后处理脚本
#!/bin/bash
WEB_DIR="/var/www/html"
export GIT_DIR="$WEB_DIR/.git"
pushd $WEB_DIR > /dev/null
git pull
popd > /dev/null
注意,该脚本必须使用 /bin/bash 作为解释器才能使用该 shell 的 pushd 和 popd 内置命令。如果已经存在更新后处理文件,您可以在验证解释器之后将脚本的其余部分添加到该文件。您必须确保现有更新后处理脚本中的现有命令前面没有 exec 命令,它会阻止执行文件中的后续行。现在,您或任何开发人员可以在 Web 服务器上复制裸存储库并开始处理您的网站。您和任何其他开发人员都可以一起处理网站的文件,可以执行任何以下任务:
* 在网站签出中处理文件并直接推入共享的中央存储库。
* 在网站签出中处理文件并将您签出中的提交更改推入合作方的签出。在将文件推入共享中央存储库(及网站)之前您可以协作处理这些文件。
尽管 Git 比大部分 VCS 要快很多,但如果您要处理的是一个大型的复杂网站且用户很多,在网站目录中直接工作是不现实的。访客可能会接受到某些文件的更新,而其他文件却尚未更新。 如果尚未收到 CSS 更新,而使用它们的更新页面已经被加载,那么很可能出现问题。通常可以使用两种方法避免这个问题:一是在 Web 服务器中使用符号链接之类的解决方案以实际指向您的内容目录,并在希望激活更新内容时切换它们;二是修改 Web 服务器配置文件以指向其他目录,并在希望激活新内容时重启 Web 服务器。
迁移到 Git 的八个理由
1. 快速
如果你每移动一下鼠标都要等待五秒,是不是很受不了?版本控制也是一样的,每一个命令多那么几秒钟,一天下来也会浪费你不少时间。Git的操作非常快速,你可以把时间用在别的更有意义的地方。
2. 离线工作
在没有网络的情况下如何工作?如果你用SVN或者CVS的话就很麻烦。而Git可以让你在本地做所有操作,提交代码,查看历史,合并,创建分支等等。
3. 回退
人难免犯错。我很喜欢Git的一点就是你可以“undo”几乎所有的命令。你可以用这个功能来修正你刚刚提交的代码中的一个问题或者回滚整个代码提交操作。你甚至可以恢复一个被删除的提交,因为在后端,Git几乎不做任何删除操作。
4. 省心
你有没有丢失过版本库?我有,而那种头疼的感觉现在还记忆犹新。而用Git的话,我就不必担心这个问题,因为任何一个人机器上的版本都是一个完整的备份。
5. 选择有用的代码提交
当你把纽带,冰块还有西红柿一起扔进搅拌机的时候至少有两个问题。第一,搅拌过后,没有人知道之前扔进去了些什么东西。第二,你不能回退,重新把西红柿拿出来。同样的,当你提交了一堆无关的更改,例如功能A加强,新增功能B,功能C修复,想要理清这一堆代码到底干了什么是很困难的。当然,当发现功能A出问题的时候,你无法单独回滚功能A。Git可以通过创建“颗粒提交”,帮你解决这个问题。“staging area”的概念可以让你决定到底那些东西需要提交,或者更新,精确到行。
6. 自由选择工作方式
使用Git,你可以同时和多个远程代码库连接,“rebase”而不是"merge"甚至只连接某个模块。但是你也可以选择一个中央版本库,就像SVN那样。你依然可以利用Git的其他优点。
7. 保持工作独立
把不同的问题分开处理将有助于跟踪问题的进度。当你在为功能A工作的时候,其他人不应该被你还没有完成的代码所影响。分支是解决这个问题的办法。虽然其他的版本控制软件业有分支系统,但是Git是第一个把这个系统变得简单而快速的系统。
8. 随大流
虽然只有死于才随着波浪前进,但是很多时候聪明的程序员也是随大流的。越来越多的公司,开源项目使用Git,包括Ruby On Rails,jQuery,Perl,Debian,Linux Kernel等等。拥有一个强大的社区是很大的优势,有很多教程、工具。
不迁移到 Git 的十个理由
1. 复杂的信息模型
Git 的信息模型是很复杂的,而且你必须对他们都很了解。在这个方面上你看看 Subversion:有文件、工作目录、资源库、版本、分支和标签。你需要了解的就是这些东西,实际上,分支、标签和文件你已经了解,但如果使用 Git ,你拥有更多的概念需要了解:文件、工作树、索引、本地资源库、远程资源库、远程、提交、treeishes、分支和 stash。你需要了解比 Subversion 更多得多的知识点。
2. 让人抓狂的命令行语法
Git 的命令行语法完全是随意的而且不一致,例如 git pull 基本上跟 git merge 和 git fetch 一样,git branch 和 git checkout 合并就变成 git checkout -b,git reset 命令的不同参数做的事情完全不一样,指定文件名后命令的语义完全不同等等。
而最为壮观的就是 git am 命令了,据我所知,这是因为 Linus 在当年某个晚上为了解决通过电子邮件阅读补丁而使用的不同的补丁语法,特别是在邮件的标题上。
3. 蹩脚、让人费解的文档
说起 Git 的这个文档,我唯一想说的就是“操”。他们是为计算机科学家在写文档,而不是用户。在这里举个例子:
git-push – Update remote refs along with associated objects
如果是针对用户而言,应该描述为:
git-push – Upload changes from your local repository into a remote repository
另外一个例子:
git-rebase – Forward-port local commits to the updated upstream head
翻译: git-rebase – Sequentially regenerate a series of commits so they can be applied directly to the head node
4. 信息模型的扩散
刚才我在第一点提到的 Git 的信息模型是非常复杂的,而且还想癌细胞一样一直在扩散,当然一直在使用 Git ,就会不断的冒出各种新的概念,例如 refs, tags, the reflog, fast-forward commits, detached head state (!), remote branches, tracking, namespaces 之类的。
5. 漏洞百出的抽象
Git 包含太多不是抽象的抽象,在定义用户接口和实现上经常没有任何区别,这是可以理解的,对一个高级用户来说他需要了解一些功能的具体实现,以掌握各个命令的微妙之处。但大量的内部细节对初学者来说简直是噩梦。有这么一个说法,关于水暖器材和瓷器,但你必须成为一个水暖工才能知道器材如何安装在瓷器上。
很多人对我的抱怨予以回应说:你无需使用所有的命令,你可以向 Subversion 一样来使用 Git。这是狡辩,就好比是告诉一个老奶奶说高速公路并不可怕,她可以在高速路上靠左边的快车道上以时速 20 公里爬行,一样的道理。Git 并没有提供任何有用的子集,每个命令都会连带着对其他命令的要求,很简单的动作经常需要很复杂的动作来撤销或者改进。
下面是一个 Github 项目维护者的一些善意的建议:
在分支和 master 上寻找合并的基准: ‘git merge-base master yourbranch’
假设你已经提交了更改记录,从对你的提交重新基准化到合并准,然后创建一个新分支
git rebase –onto <basecommit> HEAD~1 HEAD
git checkout -b my-new-branch
检出你的 ruggedisation 分支,然后移除提交: ‘git reset –hard HEAD~1′
合并新的分支到 ruggedisation: ‘git merge my-new-branch’
检出 master (‘git checkout master’), 合并新分支 (‘git merge my-new-branch’), 然后检查合并后的情况,接着移除合并 (‘git reset –hard HEAD~1′).
提交新的分支 (‘git push origin my-new-branch’) 并记录 pull 请求
翻译:“奶奶,在高速公路上开车很容易的。松开离合器,让转速超过 6000 转使车轮打滑,然后进入第一个弯道并上高速公路,看路牌到出口前,使用手刹漂移转向出口。
6. 维护简单,但是提交麻烦
Git 很强大的一点就是代码基准库的维护,你必须合并来自大量不同源的提交,非常适合大规模并行开发。但是这些都不是为大多数 Git 的用户设计的,他们只是需要编写代码,可能好几个月都在同一个分支上,对他们来说 Git 是带有 4 个手柄的双锅的咖啡机,但用户只想立即喝到咖啡。
有趣的是,我并不认为这是 Git 在设计中做的权衡。它完全是忽视了真正的用户需求、混淆架构和接口。如果你是一个架构师,那么 Git 是很棒的。但对用户来说它很糟糕,已经有不少人在为 Git 编写一些简化的接口,例如 easygit。
7. 不安全的版本控制
作为一个版本控制系统而言,它必须承诺的就是:一旦代码提交到系统,那么我将保证代码的安全,你做的任何改动你都可以找回。而 Git 食言了,有很多方法可以让整个资料库完全崩溃而且不可恢复:
git add . / … / git push -f origin master
git push origin +master
git rebase -i <some commit that has already been pushed and worked from> / git push
8. 将版本控制库维护者的责任移给贡献者
在传统的开源项目中,只需要一个人负责处理分支和合并这样复杂的操作,那就是维护者。而其他人只需要简单的更新提交、更新提交、不断的更新提交。而现在 Git 让每个用户都需要了解作为维护者才需要知道的各种操作,烦不胜烦。而维护者呢,无所事事,翘起二郎腿喝咖啡。
9. Git 的历史是一堆谎言
开发工作主要的产出就是源代码,一个维护良好的代码历史就对一个产品来说非常的重要,关于重新基准化有很多的争论,多数是依赖于对凌乱合并和不可读的日子的审美判断。而重新基准化为开发者提供一个“干净整洁”的却毫无用途历史记录,而实际上正确的解决方法是更好的日志输出以及对不想要的合并进行过滤。
10. 简单任务也要诸多命令
如果你在开发一个开源项目,你做了一些改变,然后想与其他人分享,你只需要:
修改代码
执行 svn commit
如果你增加了一些新文件:
添加文件
svn add
svn commit
如果你的项目托管在 Github 类的网站中,那么你需要:
Make some changes
git add [not to be confused with svn add]
git commit
git push
到此为止,你的更改只完成了一半,接下来你需要登录到 Github,查找你的提交,然后发布一个 “pull request” ,这样其他人才可以获取你的改动
在现实中,Github 的维护者希望你的改动是功能方面的分支,他们会要求你这样操作:
git checkout master [to make sure each new feature starts from the baseline]
git checkout -b newfeature
Make some changes
git add [not to be confused with svn add]
git commit
git push
然后登录到 Github,切换到你的新特性分支,发布 “pull request”
为了将你的更改从你的本地目录中移到实际的项目资源库,你需要:add, commit, push, “click pull request”, pull, merge, push.
下面是一个流程图向你展示一个典型的开发者在 Subversion 上要做的工作:
"Bread and butter"是与远程 SVN 资料库操作的命令和概念。
然后我们再来看看如果你的项目托管在 Github 上会是怎样的:
如果 Git 的强大之处是分支和合并,那么它的弱点就是让简单的任务变得非常复杂。
Git 是一个强大、灵活的 VCS,提供了许多协作功能,因为它的目标用户就是分布式开发人员。Git 以全新而独特的方式支持协作开发,包括网站的共享开发,基于 Web 的应用程序等等,完全值得花些时间理解它的内部机制,并学习常用的 Git 命令子集。
Git 助力,两个月新增 80 万行代码,Linux 内核也没有崩溃
2020年8月,当 Linux 5.8 RC 版本开放测试时,大多数的新闻都聚焦于它的大小,称其为“史上最大的内核版本”。正如 Linus Torvalds 本人指出的那样,“尽管没有任何一件事情能脱颖而出……但 5.8 似乎是我们有史以来最大的发行版之一。”
确实,刚刚发布的 Linux 内核 5.8 RC 具有超过 14,000 个 commit,约 80 万行新代码以及大约 100 名新贡献者。要知道,距离 5.7 正式版发布才仅仅过去了约 2 个月的时间。Linux 内核维护者 Steven Rostedt 认为,5.8 之所以变得如此之大,很有可能是因为 COVID-19 疫情让很多人难以出门旅行,所有人都因此能够在这期间完成比平时更多的工作。
Rostedt 表示,从一个经验丰富的 Linux 内核贡献者和维护者的角度来看,5.8 RC 发行版特别令人震惊的并不是它的大小,而是它的空前规模对于那些正在维护它的人来说却没有造成困扰,“我认为这是因为 Linux 具有比世界上任何软件项目都好的工作流程。”
拥有最佳的工作流程意味着什么?对 Rostedt 而言,这归结为 Linux 内核开发人员随着时间的推移建立的一系列基本规则,以使他们能够持续不断地大规模、可靠地发展 Linux 内核项目。Rostedt 站在一个 Linux 内核资深维护者的角度,为我们分享了庞大的 Linux 内核项目 30 年来是如何有条不紊地运转的。
第一个关键因素是 Git
首先让我们从 Linux 项目的历史来看。在该项目的早期(1991-2002年),人们只能直接将补丁发送给 Linus Torvalds。准确地说,Linus 从项目的子维护者那里获取补丁,而这些子维护者从其他代码贡献者那里获取补丁。随着 Linux 内核变得越来越大,代码越来越复杂,很快他们就发现,一切都变得很难扩展和跟踪,并且项目将始终面临合并不兼容代码的风险。
这导致 Linus 开始探索包括 BitKeeper 在内的各种版本管理工具。BitKeeper 是一种最早的分布式版本管理的方法,其他的版本管理系统通常使用签出/修改/签入协议,而 BitKeeper 则向所有人提供整个仓库的副本,并允许开发人员将其变更发送出去以进行合并。Linux 在 2002 年开始短暂地采用了 BitKeeper,但是由于其本身是一个专有软件,被认为不符合社区对开源工作的信念,于是该工具在 2005 年停止使用。为了寻找替代品,Linus 消失了一段时间,并带着 git 回来了,后者成为了更强大的分布式版本管理系统,并且是管理流程的第一个重要实例化。Git 的出现使 Linux 开发在今天依然运转良好。
Rostedt 为我们列出了 Linux 内核工作流程中,围绕 Git 展开的七个重要基本原则。
七大基本原则
每次commit只能做一件事
Linux 的中心原则是,所有更改都必须分解为小步骤进行 —— 您的每个commit都只能做一件事。这并不意味着每个 commit 都必须很小,比如对在数千个文件中使用的函数的API进行简单更改,可以使更改量很大,但仍然可以接受,因为它是针对某一项单一任务的更改。通过始终遵循此原则,项目维护者可以更轻松地识别和隔离任何有问题的更改,而不影响其他的功能。
commit 不能破坏构建
不仅应该将所有更改分解为尽可能小的变量,而且还不能破坏内核。即每个步骤都必须完全起作用,并且不引起退化。这就是为什么对函数原型的更改还必须更新调用它的每个文件,以防止构建中断的原因。因此,每个步骤都必须作为一个独立的更改来工作,这将我们带到了下一点:
所有代码都是二等分的
如果在某个时候发现了错误,则需要知道是哪个更改导致了问题。从本质上讲,二等分是一种操作,它使开发者可以找到所有发生错误的确切时间点。
为此,请转到最后一个已知的工作 commit 所在的节点,并且已知第一个 commit 已损坏,然后在该点测试代码。如果可行,则前进到下一个节点;如果不是,则返回更上层的节点。这样一来,开发者就可以在十几次编译/测试中,从成千上万的可能 commit 中分离出导致问题出现的 commit 。Git 甚至可以通过 git bisect 功能帮助自动化该过程。
重要的是,这只有在开发者遵守以前的规则的情况下才能很好地起作用:每个 commit 仅做一件事。否则,您将不知道是 commit 的许多更改中的哪一个导致了问题;如果 commit 破坏了构建让整个项目无法正常启动,同时等分线又恰好落在了该 commit 上,则您将不知道接下来是该往上一个节点测试还是往下一个节点测试,因为它们都有问题。这意味着您永远都不应编写依赖于将来 commit 的 commit ,例如:调用尚不存在的函数,或更改全局函数的参数而不更改同一 commit 中的所有调用者。
永远不要 rebase 公共分支
Linux 项目工作流程不允许 rebase 他人使用的任何公共分支。因为 rebase 这些公共分支后,已重新基准化的 commit 将不再与基于原存储库中的相同 commit 匹配。在树的层次结构中,不是叶子的公共主干部分不能重新设置基准,否则将会破坏层次结构中的下游分支。
Git 正确合并
其他的版本管理系统是合并来自不同分支代码的噩梦,它们通常难以弄清代码冲突,并且需要大量的手动工作来解决。而 Git 的结构可以轻松完成这项工作,因此 Linux 项目也从中直接受益。这就是为什么 5.8 版本的大小并不重要的重要原因。在 5.8-RC1 发布周期中,平均每天有 200个 commit ,并从 5.7 版本中继承了 880 个合并。一些维护者注意到了其中增加的工作量,但是对此仍然没有感到什么太大的压力或者导致倦怠。
保留定义明确的 commit 日志
不幸的是,这可能是许多其他项目忽略的最重要的原则之一。每个 commit 都必须是独立的,这也应该包括与该 commit 相应的日志。内核贡献者必须在更改的 commit 日志中做出说明,让所有人了解与正在进行的更改相关的所有内容。Rostedt 提到,他自己的一些最冗长和最具描述性的变更日志,往往是针对一些单行代码提交的,因为这些单行代码更改是非常细微的错误修复,且代码本身包含的信息极少。因此更改的代码越少,日志反而应该说明得更详细。
在一个 commit 过了几年之后,几乎没有人会记得当初为什么进行更改。Git 的 blame 功能就可以显示这些代码的修改记录。比如一些 commit 可能非常古老,也许您需要去除一个锁定,或者对某些代码进行更改,而又不确切知道它为什么存在,就可以使用 git blame 来查看。编写良好的代码更改日志可以帮助确定是否可以删除该代码或如何对其进行修改。Rostedt 说:“有好几次我很高兴能在代码上看到详细的变更日志,因为我不得不删除这些代码,而变更日志的描述让我知道我这么做是可以的。”
持续测试和集成
最后一项基本原则是开发过程中进行持续测试和持续集成。在向上游发送 commit 请求之前,开发者会测试每个 commit 。Linux 社区还有一个名为 Linux-next 的镜像 ,它提取维护人员在其存储库的特定分支上进行的所有更改,并对其进行测试以确保它们能正确集成。Linux-next 非常有效地运行着整个内核的可测试分支,该分支将用于下一个发行版。Linux-next 是一个公共仓库,任何人都可以测试它,这种情况经常发生 —— 人们现在甚至发布有关 Linux-next 中代码的错误报告。事实上,已经进入 Linux-next 几周的代码基本上可以确定会最终进入主线发行版中。
软件开发行业的黄金标准
所有的这些原则制度使 Linux 社区能够以如此庞大的规模(常规 9 周为一个版本迭代周期)发布令人难以置信的可靠代码(每个版本平均 10,000 次 commit ,最后一个版本超过 14,000 次 commit )。
Rostedt 指出,Linux 项目取得空前成功的另一个因素是他们社区的文化。Linux 内核社区内部存在一种持续改进的文化,这使他们能够首先采用这些实践。同时他们还有一种信任的文化,“我们有一条清晰的途径,人们可以通过该途径做出贡献,并随着时间的推移证明他们愿意且有能力推进该项目的发展。这将建立一个相互信任的关系网,这些关系对于项目的长期成功至关重要。”
Rostedt 认为,内核开发者的肩上承担着比其他任何项目都要重的责任。“在内核层,我们别无选择,只能遵循这些做法。因为所有其他应用程序都在内核之上运行,内核中的任何性能问题或错误都将导致上层的应用程序出现性能问题或错误。我们必须完美处理内核中的错误,否则整个计算机系统都将受到损害。我们非常关心每个错误,因为内核中的错误带来的风险很高,这种思维方式也能让我们很好地服务于任何软件项目。”
上层的应用程序会因为错误而崩溃,造成的后果可能是惹恼用户,但风险不高。而内核的错误可能导致的后果是让计算机上的一切都出现问题,承担着巨大的风险。
这就是 Linux 内核开发工作流程被视为软件开发行业黄金标准的原因。