设置和配置
获取和创建项目
基本快照
分支与合并
共享和更新项目
检查和比较
打补丁
调试
电子邮件
外部系统
服务器管理
指南
管理
底层命令
- 2.43.2 → 2.50.1 无更改
-
2.43.1
2024-02-09
- 2.25.2 → 2.43.0 无更改
-
2.25.1
2020-02-17
- 2.23.1 → 2.25.0 无更改
-
2.23.0
2019-08-16
- 2.16.6 → 2.22.5 无更改
-
2.15.4
2019-12-06
- 2.14.6 无更改
-
2.13.7
2018-05-22
-
2.12.5
2017-09-22
-
2.11.4
2017-09-22
- 2.10.5 无更改
-
2.9.5
2017-07-30
-
2.8.6
2017-07-30
- 2.5.6 → 2.7.6 无变更
-
2.4.12
2017-05-05
- 2.3.10 无更改
-
2.2.3
2015-09-04
- 2.1.4 无更改
-
2.0.5
2014-12-17
描述
本教程解释了如何使用 Git 的“核心”命令来设置和使用 Git 仓库。
如果您只需要将 Git 用作版本控制系统,您可能更喜欢从“Git 教程简介” (gittutorial[7]) 或 Git 用户手册 开始。
然而,如果您想了解 Git 的内部机制,理解这些底层工具会有所帮助。
Git 的核心通常被称为“管道”(plumbing),而其上更美观的用户界面则被称为“瓷器”(porcelain)。您可能不常直接使用管道,但当瓷器无法正常工作时,了解管道的功能会很有益。
当本文档最初撰写时,许多瓷器命令都是 shell 脚本。为了简化起见,它仍然使用它们作为示例,以说明管道如何组合形成瓷器命令。源代码树在 contrib/examples/ 中包含了一些这些脚本以供参考。尽管这些命令现在不再以 shell 脚本实现,但对管道层命令功能的描述仍然有效。
注意
|
更深入的技术细节通常被标记为“备注”,您可以在首次阅读时跳过。 |
创建 Git 仓库
创建新的 Git 仓库再简单不过了:所有 Git 仓库都始于空,您只需找到一个您想用作工作树的子目录——可以是用于全新项目的空目录,也可以是您想导入到 Git 中的现有工作树。
对于我们的第一个示例,我们将从零开始创建一个全新的仓库,不包含任何预先存在的文件,并将其命名为 git-tutorial。要开始,请为其创建一个子目录,进入该子目录,然后使用 git init 初始化 Git 基础设施。
$ mkdir git-tutorial $ cd git-tutorial $ git init
Git 将回复
Initialized empty Git repository in .git/
这只是 Git 在告诉您,您没有做任何奇怪的事情,并且它已经为您的新项目创建了一个本地 .git
目录设置。您现在将拥有一个 .git
目录,您可以使用 ls 检查它。对于您的新空项目,它应该显示三个条目,其中包括
-
一个名为
HEAD
的文件,其中包含ref:
refs/heads/master
。这类似于一个符号链接,指向相对于HEAD
文件的refs/heads/master
。不用担心
HEAD
链接指向的文件甚至还不存在——您还没有创建将启动您的HEAD
开发分支的提交。 -
一个名为
objects
的子目录,它将包含您项目的所有对象。您通常不需要直接查看这些对象,但您可能需要知道这些对象包含了您仓库中所有真实的 数据。 -
一个名为
refs
的子目录,其中包含对对象的引用。
特别是,refs
子目录将包含另外两个子目录,分别名为 heads
和 tags
。它们的作用正如其名所示:它们包含对任意数量的不同开发 头部(亦称 分支)的引用,以及对您在仓库中创建的用于命名特定版本的任何 标签 的引用。
请注意:特殊的 master
头部是默认分支,这就是为什么即使 .git/HEAD
文件所指向的它尚不存在,它也会被创建的原因。基本上,HEAD
链接应该始终指向您当前正在工作的分支,而您总是默认期望在 master
分支上工作。
然而,这仅仅是一个约定,您可以将分支命名为您想要的任何名称,甚至不必 拥有 一个 master
分支。不过,许多 Git 工具会假定 .git/HEAD
是有效的。
注意
|
一个 对象 由其 160 位 SHA-1 散列(亦称 对象名)标识,而对一个对象的引用始终是该 SHA-1 名称的 40 字节十六进制表示。在 refs 子目录中的文件应包含这些十六进制引用(通常末尾带有最终的 \n),因此当您实际开始填充您的树时,您应该期望在这些 refs 子目录中看到一些包含这些引用的 41 字节文件。 |
注意
|
高级用户在完成本教程后,可能希望查看 gitrepository-layout[5]。 |
您现在已经创建了您的第一个 Git 仓库。当然,由于它是空的,所以用处不大,所以让我们开始用数据填充它。
填充 Git 仓库
我们将保持简单直观,所以我们将从填充几个简单文件开始,以便您有所感受。
首先,只需创建您想要在 Git 仓库中维护的任何随机文件。我们将从几个不太好的示例开始,以便您了解其工作原理。
$ echo "Hello World" >hello $ echo "Silly example" >example
您现在已经在您的工作树(亦称 工作目录)中创建了两个文件,但要实际提交您的辛苦工作,您需要经历两个步骤
-
用关于您工作树状态的信息填充 索引 文件(亦称 缓存)。
-
将该索引文件提交为一个对象。
第一步很简单:当您想告诉 Git 您的工作树有任何更改时,您可以使用 git update-index 程序。该程序通常只接受您想要更新的文件名列表,但为了避免简单的错误,它拒绝将新条目添加到索引(或删除现有条目),除非您明确告诉它您正在使用 --add
标志添加新条目(或使用 --remove
标志删除条目)。
因此,要用您刚刚创建的两个文件填充索引,您可以执行
$ git update-index --add hello example
您现在已经告诉 Git 跟踪这两个文件了。
事实上,当您这样做时,如果您现在查看您的对象目录,您会注意到 Git 已经向对象数据库添加了两个新对象。如果您完全按照上述步骤操作,您现在应该能够执行
$ ls .git/objects/??/*
并看到两个文件
.git/objects/55/7db03de997c86a4a028e1ebd3a1ceb225be238 .git/objects/f2/4c74a2e500f5ee1332c86b94199f52b1d1d962
它们分别对应名为 557db...
和 f24c7...
的对象。
如果您愿意,可以使用 git cat-file 查看这些对象,但您必须使用对象名,而不是对象的文件名
$ git cat-file -t 557db03de997c86a4a028e1ebd3a1ceb225be238
其中 -t
告诉 git cat-file 显示对象的“类型”。Git 会告诉您有一个“blob”对象(即一个普通文件),您可以通过以下方式查看其内容
$ git cat-file blob 557db03
它将打印出“Hello World”。对象 557db03
仅仅是您的文件 hello
的内容。
注意
|
不要将该对象与文件 hello 本身混淆。该对象实际上就是文件中的那些特定 内容,无论您后来如何更改文件 hello 中的内容,我们刚才查看的对象都不会改变。对象是不可变的。 |
注意
|
第二个示例演示了在大多数情况下,您可以将对象名缩写为前几位十六进制数字。 |
总之,正如我们之前提到的,您通常永远不会实际查看对象本身,并且输入长达 40 个字符的十六进制名称也不是您通常想做的事情。上述离题只是为了表明 git update-index 做了些神奇的事情,实际上将您的文件内容保存到了 Git 对象数据库中。
更新索引还做了其他事情:它创建了一个 .git/index
文件。这是描述您当前工作树的索引,也是您应该非常了解的东西。同样,您通常不必担心索引文件本身,但您应该意识到,到目前为止,您实际上并没有真正“检入”您的文件到 Git 中,您只是 告诉 了 Git 它们的存在。
然而,既然 Git 已经知道它们,您现在就可以开始使用一些最基本的 Git 命令来操作文件或查看它们的状态了。
特别是,我们甚至还没有将这两个文件检入 Git,我们将首先向 hello
添加另一行
$ echo "It's a new day for git" >>hello
现在,由于您已经告诉 Git hello
的先前状态,您可以使用 git diff-files 命令询问 Git 与旧索引相比,树中发生了什么变化。
$ git diff-files
哎呀。那不太易读。它只是吐出了它自己的内部版本 diff,但那个内部版本实际上只是告诉您它已注意到“hello”已被修改,并且它拥有的旧对象内容已被其他内容替换。
为了使其可读,我们可以告诉 git diff-files 使用 -p
标志将差异输出为补丁
$ git diff-files -p diff --git a/hello b/hello index 557db03..263414f 100644 --- a/hello +++ b/hello @@ -1 +1,2 @@ Hello World +It's a new day for git
即我们通过在 hello
中添加另一行所引起的更改的差异。
换句话说,git diff-files 总是向我们展示索引中记录的内容与工作树中当前内容之间的差异。这非常有用。
git
diff-files
-p
的常见简写是直接写 git
diff
,它将做同样的事情。
$ git diff diff --git a/hello b/hello index 557db03..263414f 100644 --- a/hello +++ b/hello @@ -1 +1,2 @@ Hello World +It's a new day for git
提交 Git 状态
现在,我们想进入 Git 的下一个阶段,即将 Git 在索引中知道的文件,并将其作为真实的树提交。我们分两个阶段进行:创建 树 对象,并将该 树 对象作为一个 提交 对象进行提交,同时附带对该树的解释以及我们如何达到该状态的信息。
创建树对象很简单,使用 git write-tree 完成。它没有选项或其他输入:git
write-tree
将获取当前索引状态,并写入一个描述整个索引的对象。换句话说,我们现在将所有不同的文件名及其内容(及其权限)绑定在一起,并创建了 Git “目录”对象的等价物
$ git write-tree
这将只输出结果树的名称,在这种情况下(如果您完全按照我描述的做了),它应该是
8988da15d077d4829fc51d8544c097def6644dbb
这是另一个难以理解的对象名。同样,如果您愿意,可以使用 git
cat-file
-t
8988d...
来查看这次对象不是“blob”对象,而是“tree”对象(您也可以使用 git
cat-file
实际输出原始对象内容,但您主要会看到一堆二进制乱码,所以那没那么有趣)。
然而,通常您永远不会单独使用 git write-tree,因为通常您总是使用 git commit-tree 命令将一个树提交到一个提交对象中。事实上,最好根本不单独使用 git write-tree,而是将其结果作为参数传递给 git commit-tree。
git commit-tree 通常接受多个参数——它想知道一个提交的 父 提交是什么,但由于这是这个新仓库中的第一个提交,它没有父提交,所以我们只需要传入树的对象名。然而,git commit-tree 也想在其标准输入上获取一个提交消息,并且它将把提交的结果对象名写入其标准输出。
这就是我们创建 .git/refs/heads/master
文件的地方,该文件由 HEAD
指向。这个文件应该包含指向 master 分支的树顶部的引用,既然这正是 git commit-tree 输出的内容,我们可以通过一系列简单的 shell 命令来完成所有这些操作
$ tree=$(git write-tree) $ commit=$(echo 'Initial commit' | git commit-tree $tree) $ git update-ref HEAD $commit
在这种情况下,这创建了一个与任何其他事物都无关的全新提交。通常,一个项目只需要这样做 一次,之后的所有提交都将基于先前的提交。
同样,通常您永远不会手动执行此操作。有一个名为 git
commit
的有用脚本会为您完成所有这些操作。因此,您本可以只写 git
commit
,它就会为您完成上述神奇的脚本操作。
进行更改
还记得我们是如何对文件 hello
执行 git update-index,然后更改 hello
,并可以将 hello
的新状态与我们在索引文件中保存的状态进行比较的吗?
此外,还记得我曾说过 git write-tree 将 索引 文件的内容写入树中吗?因此,我们刚刚提交的实际上是文件 hello
的 原始 内容,而不是新内容。我们这样做是故意的,旨在展示索引状态与工作树状态之间的差异,以及它们即使在提交时也无需匹配的情况。
和以前一样,如果在我们的 git-tutorial 项目中执行 git
diff-files
-p
,我们仍然会看到上次看到的相同差异:索引文件没有因为提交任何东西而改变。然而,既然我们已经提交了一些东西,我们也可以学习使用一个新命令:git diff-index。
与 git diff-files 不同,后者显示索引文件与工作树之间的差异,而 git diff-index 显示已提交的 树 与索引文件或工作树之间的差异。换句话说,git diff-index 需要一个树来进行比较,在我们进行提交之前,我们无法做到这一点,因为我们没有任何可供比较的对象。
但现在我们可以这样做
$ git diff-index -p HEAD
(其中 -p
的含义与在 git diff-files 中相同),它将向我们展示相同的差异,但原因完全不同。现在我们比较的不是工作树与索引文件,而是工作树与我们刚刚写入的树。碰巧这两者显然是相同的,所以我们得到了相同的结果。
同样,由于这是一个常见操作,您也可以将其简写为
$ git diff HEAD
它最终会为您完成上述操作。
换句话说,git diff-index 通常会将一个树与工作树进行比较,但当给定 --cached
标志时,它会被告知转而仅与索引缓存内容进行比较,并完全忽略当前工作树状态。由于我们刚刚将索引文件写入 HEAD,因此执行 git
diff-index
--cached
-p
HEAD
应该会返回一组空的差异,事实也确实如此。
注意
|
git diff-index 实际上总是使用索引进行比较,因此说它将一个树与工作树进行比较并不完全准确。特别是,要比较的文件列表(“元数据”) 始终 来自索引文件,无论是否使用 这不难理解,只要您意识到 Git 根本不知道(也不关心)那些没有明确告知它的文件。Git 永远不会去 寻找 要比较的文件,它期望您告诉它文件是什么,而这正是索引的作用。 |
然而,我们的下一步是提交我们所做的 更改,并且,为了理解正在发生的事情,请记住“工作树内容”、“索引文件”和“已提交树”之间的区别。我们的工作树中有我们想要提交的更改,并且我们总是必须通过索引文件进行操作,所以我们需要做的第一件事是更新索引缓存
$ git update-index hello
(请注意,这次我们不需要 --add
标志,因为 Git 已经知道该文件了)。
请注意这里不同 git diff-* 版本会发生什么。在我们更新索引中的 hello
之后,git
diff-files
-p
现在没有显示差异,但是 git
diff-index
-p
HEAD
仍然 显示 当前状态与我们提交的状态不同。事实上,现在无论我们是否使用 --cached
标志,git diff-index 都显示相同的差异,因为现在索引与工作树一致。
现在,既然我们已经在索引中更新了 hello
,我们就可以提交新版本了。我们可以通过再次手动写入树并提交树来完成(这次我们必须使用 -p
HEAD
标志来告诉提交,HEAD 是新提交的 父级,并且这不再是初始提交),但您已经做过一次了,所以这次我们直接使用这个有用的脚本
$ git commit
它会启动一个编辑器,供您编写提交消息,并向您介绍您所做的一些事情。
编写您想要的任何消息,所有以 # 开头的行都将被剪除,其余部分将用作更改的提交消息。如果您决定此时不提交任何内容(您可以继续编辑并更新索引),您可以只留下一个空消息。否则 git
commit
将为您提交更改。
您现在已经完成了您的第一个真正的 Git 提交。如果您有兴趣了解 git
commit
实际做了什么,请随意研究:它是一些非常简单的 shell 脚本,用于生成有用的(?)提交消息头,以及一些实际执行提交本身的单行命令(git commit)。
检查更改
虽然创建更改很有用,但如果您稍后能够判断哪些内容发生了变化,那会更有用。对此最有用的命令是 diff 系列中的另一个,即 git diff-tree。
可以向 git diff-tree 提供两个任意的树,它会告诉您它们之间的差异。不过,更常见的是,您可以只给它一个提交对象,它会自行找出该提交的父提交,并直接显示差异。因此,为了获得我们已经见过几次的相同差异,我们现在可以这样做
$ git diff-tree -p HEAD
(同样,-p
表示以人类可读的补丁形式显示差异),它将显示上一次提交(在 HEAD
中)实际更改了什么。
注意
|
这里是 Jon Loeliger 的一个 ASCII 艺术图,它说明了各种 diff-* 命令如何比较事物。 diff-tree +----+ | | | | V V +-----------+ | Object DB | | Backing | | Store | +-----------+ ^ ^ | | | | diff-index --cached | | diff-index | V | +-----------+ | | Index | | | "cache" | | +-----------+ | ^ | | | | diff-files | | V V +-----------+ | Working | | Directory | +-----------+ |
更有趣的是,您还可以给 git diff-tree 加上 --pretty
标志,它会告诉它同时显示提交消息、作者和提交日期,并且您可以告诉它显示一系列完整的差异。或者,您可以告诉它“静默”,完全不显示差异,只显示实际的提交消息。
事实上,结合 git rev-list 程序(它生成修订列表),git diff-tree 最终成为了一个名副其实的更改宝库。您可以通过一个简单的脚本来模拟 git
log
、git
log
-p
等,该脚本将 git
rev-list
的输出通过管道传递给 git
diff-tree
--stdin
,这正是早期版本 git
log
的实现方式。
标记版本
在 Git 中,有两种标签:“轻量标签”和“附注标签”。
“轻量标签”在技术上只不过是一个分支,只不过我们将其放在 .git/refs/tags/
子目录中,而不是将其称为 head
。因此,最简单的标签形式仅涉及
$ git tag my-first-tag
它只是将当前的 HEAD
写入 .git/refs/tags/my-first-tag
文件中,之后您就可以使用此符号名称来指代该特定状态。例如,您可以执行
$ git diff my-first-tag
将您当前的状态与该标签进行比较,此时显然会是一个空差异,但如果您继续开发和提交内容,您可以使用您的标签作为“锚点”来查看自您标记以来发生了什么变化。
“附注标签”实际上是一个真实的 Git 对象,它不仅包含指向您要标记的状态的指针,还包含一个简短的标签名称和消息,并可选择附带一个 PGP 签名,表明您确实创建了该标签。您可以使用 git tag 命令的 -a
或 -s
标志来创建这些附注标签
$ git tag -s <tagname>
它将签署当前的 HEAD
(但您也可以给它另一个参数来指定要标记的对象,例如,您可以使用 git
tag
<tagname> mybranch
来标记当前的 mybranch
点)。
您通常只为主要版本或类似情况创建签名标签,而轻量级标签适用于您想进行的任何标记——任何时候您决定要记住某个特定点,只需为其创建一个私有标签,您就有了该点状态的一个很好的符号名称。
复制仓库
Git 仓库通常是完全自给自足和可重定位的。例如,与 CVS 不同,Git 没有“仓库”和“工作树”的独立概念。一个 Git 仓库通常 就是 工作树,本地的 Git 信息隐藏在 .git
子目录中。没有其他东西。您所看到的即是您所得到的。
注意
|
您可以告诉 Git 将 Git 内部信息从它跟踪的目录中分离出来,但我们暂时忽略这一点:这不是普通项目的工作方式,它实际上只用于特殊用途。因此,“Git 信息总是直接绑定到它所描述的工作树”这种心智模型可能在技术上并非 100% 准确,但对于所有正常使用来说,它是一个很好的模型。 |
这有两层含义
-
如果您对您创建的教程仓库感到厌烦(或者您犯了一个错误并想重新开始),您可以简单地执行
$ rm -rf git-tutorial
它就会消失。没有外部仓库,也没有您创建的项目之外的历史记录。
-
如果您想移动或复制一个 Git 仓库,您可以这样做。有一个 git clone 命令,但如果您只是想创建您仓库的副本(包括所有完整的历史记录),您可以使用常规的
cp
-a
git-tutorial
new-git-tutorial
来完成。请注意,当您移动或复制 Git 仓库时,您的 Git 索引文件(它缓存了各种信息,特别是涉及文件的某些“stat”信息)可能需要刷新。因此,在您执行
cp
-a
创建新副本后,您需要执行$ git update-index --refresh
在新仓库中,以确保索引文件是最新的。
请注意,第二点即使跨机器也成立。您可以使用 任何 常规复制机制复制远程 Git 仓库,无论是 scp、rsync 还是 wget。
复制远程仓库时,您至少需要更新索引缓存,尤其是在处理他人仓库时,您通常希望确保索引缓存处于某种已知状态(您不知道他们做了 什么 以及尚未检入什么),因此通常您会在 git update-index 之前执行一个
$ git read-tree --reset HEAD $ git update-index --refresh
它将强制从 HEAD
指向的树完全重建索引。它将索引内容重置为 HEAD
,然后 git update-index 确保所有索引条目与检出的文件匹配。如果原始仓库在其工作树中有未提交的更改,git
update-index
--refresh
会注意到它们并告诉您需要更新。
以上也可以简单地写成
$ git reset
事实上,许多常见的 Git 命令组合都可以通过 git
xyz
接口进行脚本化。您只需查看各种 git 脚本的作用即可学到东西。例如,git
reset
以前就是 git reset 中实现的上述两行,但像 git status 和 git commit 这样的命令是围绕基本 Git 命令的稍微复杂一些的脚本。
许多(大部分?)公共远程仓库将不包含任何检出的文件,甚至不包含索引文件,而将 只 包含实际的 Git 核心文件。这样的仓库通常甚至没有 .git
子目录,而是直接在仓库中包含所有 Git 文件。
要创建这种“原始”Git 仓库的本地实时副本,您首先需要为项目创建自己的子目录,然后将原始仓库内容复制到 .git
目录中。例如,要创建您自己的 Git 仓库副本,您需要执行以下操作
$ mkdir my-git $ cd my-git $ rsync -rL rsync://rsync.kernel.org/pub/scm/git/git.git/ .git
然后是
$ git read-tree HEAD
来填充索引。然而,现在您已经填充了索引,并且拥有所有 Git 内部文件,但您会注意到您实际上没有任何工作树文件可以操作。要获取这些文件,您需要使用以下命令检出它们
$ git checkout-index -u -a
其中 -u
标志表示您希望检出时保持索引最新(这样您就不必在之后刷新它),而 -a
标志表示“检出所有文件”(如果您有一个过时的副本或较旧版本的已检出树,您可能还需要首先添加 -f
标志,以告诉 git checkout-index 强制 覆盖任何旧文件)。
同样,这一切都可以简化为
$ git clone git://git.kernel.org/pub/scm/git/git.git/ my-git $ cd my-git $ git checkout
它最终会为您完成上述所有操作。
您现在已经成功复制了别人的(我的)远程仓库,并将其检出。
创建新分支
Git 中的分支实际上不过是 .git/refs/
子目录中指向 Git 对象数据库的指针,正如我们已经讨论过的,HEAD
分支只是其中一个对象指针的符号链接。
您随时可以通过选择项目历史中的任意点来创建一个新分支,只需将该对象的 SHA-1 名称写入 .git/refs/heads/
下的文件中即可。您可以使用任何您想要的文件名(实际上,也可以使用子目录),但约定是“普通”分支被称为 master
。但这仅仅是一个约定,并没有任何强制性。
为了举例说明,让我们回到我们之前使用的 git-tutorial 仓库,并在其中创建一个分支。您只需简单地说您想检出一个新分支即可
$ git switch -c mybranch
将基于当前 HEAD
位置创建一个新分支,并切换到该分支。
注意
|
如果您决定在新分支的创建点选择历史中不同于当前 $ git switch -c mybranch earlier-commit 它将在较早的提交处创建新分支 |
您可以通过执行以下操作随时跳回您的原始 master
分支
$ git switch master
(或者任何其他分支名称,就此而言)如果您忘记了您当前所在的分支,一个简单的命令
$ cat .git/HEAD
会告诉您它指向哪里。要获取您拥有的分支列表,您可以说
$ git branch
这曾不过是围绕 ls
.git/refs/heads
的一个简单脚本。您当前所在的分支前面会有一个星号。
有时您可能希望创建新分支,而 不 实际检出并切换到它。如果是这样,只需使用命令
$ git branch <branchname> [startingpoint]
它将只是 创建 该分支,但不会进行任何进一步的操作。然后您可以在稍后——一旦您决定要实际在该分支上开发时——使用常规的 git switch 命令并以分支名作为参数切换到该分支。
合并两个分支
创建分支的其中一个想法是您可以在其中进行一些(可能是实验性的)工作,并最终将其合并回主分支。因此,假设您创建了上述 mybranch
,它最初与原始的 master
分支相同,让我们确保我们在此分支中,并在那里进行一些工作。
$ git switch mybranch $ echo "Work, work, work" >>hello $ git commit -m "Some work." -i hello
在这里,我们只是向 hello
添加了另一行,并且我们通过直接将文件名传递给 git
commit
并带有 -i
标志(它告诉 Git 在进行提交时 包含 该文件以及您迄今为止对索引文件所做的更改),从而实现了 git
update-index
hello
和 git
commit
的简写。 -m
标志用于从命令行提供提交日志消息。
现在,为了让事情更有趣一点,我们假设其他人在原始分支中做了一些工作,并通过回到 master 分支并在那里以不同方式编辑同一个文件来模拟这种情况
$ git switch master
在这里,花点时间看看 hello
的内容,并注意它们是如何不包含我们刚刚在 mybranch
中所做的工作的——因为该工作根本没有发生在 master
分支中。然后执行
$ echo "Play, play, play" >>hello $ echo "Lots of fun" >>example $ git commit -m "Some fun." -i hello example
因为 master 分支显然心情好多了。
现在,您有了两个分支,您决定要合并已完成的工作。在此之前,我们先介绍一个很棒的图形工具,可以帮助您查看正在发生的事情
$ gitk --all
将以图形方式向您显示您的两个分支(这就是 --all
的含义:通常它只显示您当前的 HEAD
)及其历史。您还可以准确地看到它们是如何从共同来源演变而来的。
无论如何,让我们退出 gitk(^Q
或“文件”菜单),并决定将我们在 mybranch
分支上所做的工作合并到 master
分支中(它目前也是我们的 HEAD
)。为此,有一个不错的脚本叫做 git merge,它需要知道您想解决哪些分支以及合并的全部内容是什么
$ git merge -m "Merge work in mybranch" mybranch
其中第一个参数将用作提交消息,如果合并可以自动解决。
现在,在这种情况下,我们故意创建了一个需要手动修复合并的情况,因此 Git 将尽可能自动完成(在本例中,只是合并了 example
文件,该文件在 mybranch
分支中没有差异),然后说
Auto-merging hello CONFLICT (content): Merge conflict in hello Automatic merge failed; fix conflicts and then commit the result.
它告诉您它进行了“自动合并”,但由于 hello
中的冲突而失败。
不用担心。它将 hello
中的(微不足道的)冲突保留了您使用 CVS 时应该已经很熟悉的相同形式,所以我们只需在我们的编辑器中(无论是什么编辑器)打开 hello
,并以某种方式修复它。我建议只让 hello
包含所有四行
Hello World It's a new day for git Play, play, play Work, work, work
一旦您对您的手动合并满意,只需执行一个
$ git commit -i hello
这会非常响亮地警告您,您现在正在提交一个合并(这是正确的,所以不用担心),并且您可以写一个关于您在 git merge 世界中冒险的小合并消息。
完成后,启动 gitk
--all
以图形方式查看历史记录。请注意,mybranch
仍然存在,您可以切换到它,如果愿意,可以继续使用它进行工作。mybranch
分支将不包含合并,但下次您从 master
分支合并它时,Git 将知道您是如何合并的,因此您无需再次执行 那个 合并。
另一个有用的工具,特别是如果您不总是在 X-Window 环境中工作,是 git
show-branch
。
$ git show-branch --topo-order --more=1 master mybranch * [master] Merge work in mybranch ! [mybranch] Some work. -- - [master] Merge work in mybranch *+ [mybranch] Some work. * [master^] Some fun.
前两行表示它正在显示两个分支,标题是它们的树顶提交,您当前在 master
分支上(注意星号 *
字符),后续输出行的第一列用于显示 master
分支中包含的提交,第二列用于 mybranch
分支。显示了三个提交及其标题。所有这些提交在第一列中都有非空白字符(*
显示当前分支上的普通提交,-
是合并提交),这意味着它们现在是 master
分支的一部分。只有“Some work”提交在第二列中有加号 +
字符,因为 mybranch
尚未合并以包含来自 master 分支的这些提交。提交日志消息前面括号中的字符串是您可以用来命名提交的短名称。在上面的示例中,master 和 mybranch 是分支头。master^ 是 master 分支头的第一个父级。如果您想查看更复杂的案例,请参阅 gitrevisions[7]。
注意
|
如果没有 --more=1 选项,git show-branch 将不会输出 [master^] 提交,因为 [mybranch] 提交是 master 和 mybranch 两个分支顶点的共同祖先。详情请参阅 git-show-branch[1]。 |
注意
|
如果在合并之后 master 分支上有更多的提交,默认情况下 git show-branch 将不会显示合并提交本身。在这种情况下,您需要提供 --sparse 选项才能使合并提交可见。 |
现在,让我们假设您是那个在 mybranch
中完成了所有工作的人,您的辛勤工作的成果最终已被合并到 master
分支。让我们回到 mybranch
,然后运行 git merge 以将“上游更改”带回您的分支。
$ git switch mybranch $ git merge -m "Merge upstream changes." master
这将输出类似这样的内容(实际的提交对象名称会有所不同)
Updating from ae3a2da... to a80b4aa.... Fast-forward (no commit created; -m option ignored) example | 1 + hello | 1 + 2 files changed, 2 insertions(+)
由于您的分支没有包含任何超出已合并到 master
分支的内容,所以合并操作实际上并没有执行合并。相反,它只是将您分支的树顶更新为 master
分支的树顶。这通常被称为 快进 合并。
您可以再次运行 gitk
--all
查看提交祖先关系,或者运行 show-branch,它会告诉您这一点。
$ git show-branch master mybranch ! [master] Merge work in mybranch * [mybranch] Merge work in mybranch -- -- [master] Merge work in mybranch
合并外部工作
通常,与他人合并比与自己的分支合并更为常见,因此值得指出的是,Git 也使这变得非常容易,事实上,这与执行 git merge 并无太大区别。实际上,远程合并最终不过是“将远程仓库中的工作获取到临时标签中”,然后执行 git merge。
从远程仓库获取数据是通过,不出所料,git fetch 完成的
$ git fetch <remote-repository>
以下传输方式之一可用于命名要下载的仓库
- SSH
-
remote.machine:/path/to/repo.git/
或ssh://remote.machine/path/to/repo.git/
这种传输方式可用于上传和下载,并且要求您拥有通过
ssh
登录远程机器的权限。它通过交换双方拥有的头部提交来找出对方缺少的对象集合,并传输(接近)最小的对象集合。这是迄今为止在仓库之间交换 Git 对象最有效的方式。 - 本地目录
-
/path/to/repo.git/
这种传输方式与 SSH 传输相同,但使用 sh 在本地机器上运行两端,而不是通过 ssh 在远程机器上运行另一端。
- Git 原生
-
git://remote.machine/path/to/repo.git/
这种传输方式专为匿名下载而设计。与 SSH 传输类似,它会找出下游缺少哪些对象,并传输(接近)最小的对象集。
- HTTP(S)
-
http://remote.machine/path/to/repo.git/
从 http 和 https URL 下载时,首先通过查看
repo.git/refs/
目录中指定的引用名称来获取远程站点的最顶部提交对象名称,然后尝试通过从repo.git/objects/xx/xxx...
下载该提交对象的名称来获取提交对象。然后它读取提交对象以找出其父提交和关联的树对象;它重复此过程直到获得所有必要的对象。由于这种行为,它们有时也被称为 提交遍历器。提交遍历器 有时也被称为 哑传输,因为它们不需要像 Git 原生传输那样任何支持 Git 的智能服务器。任何不支持目录索引的普通 HTTP 服务器就足够了。但您必须使用 git update-server-info 准备您的仓库,以帮助哑传输下载器。
一旦您从远程仓库获取了内容,您就可以将其 merge
到您当前的分支。
然而——fetch
之后立即 merge
是如此常见,以至于它被称为 git
pull
,您可以简单地执行
$ git pull <remote-repository>
并可选择将远程分支名称作为第二个参数。
注意
|
您完全可以不使用任何分支,而是保留尽可能多的本地仓库作为您想要的分支,并使用 git pull 在它们之间进行合并,就像您在分支之间进行合并一样。这种方法的优点是它允许您为每个 branch 检出一组文件,如果您同时处理多条开发线,您可能会发现来回切换更方便。当然,您将付出更多磁盘空间来保存多个工作树的代价,但如今磁盘空间很便宜。 |
您很可能会不时地从同一个远程仓库拉取。作为简写,您可以将远程仓库 URL 存储在本地仓库的配置文件中,如下所示
$ git config remote.linus.url https://git.kernel.org/pub/scm/git/git.git/
并使用“linus”关键字与 git pull,而不是完整的 URL。
示例。
-
git
pull
linus
-
git
pull
linus
tag
v0.99.1
以上等同于
-
git
pull
https://linuxkernel.org.cn/pub/scm/git/git.git/
HEAD
-
git
pull
https://linuxkernel.org.cn/pub/scm/git/git.git/
tag
v0.99.1
合并是如何工作的?
我们说过本教程展示了管道(plumbing)如何帮助您应对无法正常工作的瓷器(porcelain),但到目前为止我们还没有谈到合并的实际工作原理。如果您是第一次阅读本教程,我建议您跳到“发布您的工作”部分,稍后再回来这里。
好的,还在跟着吗?为了给我们提供一个示例,让我们回到之前包含“hello”和“example”文件的仓库,并回到合并前的状态
$ git show-branch --more=2 master mybranch ! [master] Merge work in mybranch * [mybranch] Merge work in mybranch -- -- [master] Merge work in mybranch +* [master^2] Some work. +* [master^] Some fun.
请记住,在运行 git merge 之前,我们的 master
头部在“Some fun.”提交,而我们的 mybranch
头部在“Some work.”提交。
$ git switch -C mybranch master^2 $ git switch master $ git reset --hard master^
回溯后,提交结构应如下所示
$ git show-branch * [master] Some fun. ! [mybranch] Some work. -- * [master] Some fun. + [mybranch] Some work. *+ [master^] Initial commit
现在我们准备手动进行合并实验。
git
merge
命令在合并两个分支时,使用 3 向合并算法。首先,它找到它们之间的共同祖先。它使用的命令是 git merge-base
$ mb=$(git merge-base HEAD mybranch)
该命令将共同祖先的提交对象名称写入标准输出,所以我们将其输出捕获到一个变量中,因为我们将在下一步中使用它。顺便说一句,在这种情况下,共同祖先提交就是“Initial commit”提交。您可以通过以下方式判断它
$ git name-rev --name-only --tags $mb my-first-tag
找出共同祖先提交后,第二步是这样的
$ git read-tree -m -u $mb HEAD mybranch
这与我们之前看到的 git read-tree 命令相同,但它接受三个树,与之前的示例不同。它将每个树的内容读取到索引文件中的不同 暂存区(第一个树进入暂存区 1,第二个进入暂存区 2,依此类推)。将三个树读取到三个暂存区后,在所有三个暂存区中相同的路径会被 折叠 到暂存区 0。同样,在三个暂存区中相同但其中两个相同的路径也会被折叠到暂存区 0,取来自暂存区 2 或暂存区 3 的 SHA-1,以与暂存区 1 不同的那个为准(即只有一方从共同祖先更改)。
在 折叠 操作之后,在三个树中不同的路径会留在非零暂存区。此时,您可以使用此命令检查索引文件
$ git ls-files --stage 100644 7f8b141b65fdcee47321e399a2598a235a032422 0 example 100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1 hello 100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2 hello 100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello
在我们只有两个文件的示例中,我们没有未更改的文件,所以只有 example 导致了折叠。但在现实生活中的大型项目中,当一个提交中只有少量文件更改时,这种 折叠 往往会非常快速地简单合并大部分路径,只在非零暂存区中留下少数实际更改。
要只查看非零暂存区,请使用 --unmerged
标志
$ git ls-files --unmerged 100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1 hello 100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2 hello 100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello
合并的下一步是使用三方合并来合并文件的这三个版本。这通过将 git merge-one-file 命令作为 git merge-index 命令的一个参数来完成
$ git merge-index git-merge-one-file hello Auto-merging hello ERROR: Merge conflict in hello fatal: merge program failed
git merge-one-file 脚本在调用时会带上描述这三个版本的参数,并负责将合并结果留在工作树中。这是一个相当直接的 shell 脚本,并最终调用 RCS 套件中的 merge 程序来执行文件级别的三方合并。在这种情况下,merge 会检测到冲突,并且带有冲突标记的合并结果会留在工作树中。如果您此时再次运行 ls-files
--stage
,就可以看到这一点
$ git ls-files --stage 100644 7f8b141b65fdcee47321e399a2598a235a032422 0 example 100644 557db03de997c86a4a028e1ebd3a1ceb225be238 1 hello 100644 ba42a2a96e3027f3333e13ede4ccf4498c3ae942 2 hello 100644 cc44c73eb783565da5831b4d820c962954019b69 3 hello
这是 git merge 将控制权交还给您后索引文件和工作文件的状态,它将冲突的合并留给您解决。请注意,路径 hello
仍未合并,并且此时您通过 git diff 看到的是自暂存区 2(即您的版本)以来的差异。
发布您的工作
所以,我们可以使用别人远程仓库中的工作,但 您 如何准备一个仓库,让其他人可以从中拉取呢?
您在自己的工作树中进行实际工作,您的主仓库作为其 .git
子目录挂在其下。您 可以 使该仓库可远程访问并让人们从中拉取,但实际上通常不是这样做的。推荐的方法是拥有一个公共仓库,使其可供其他人访问,当您在主工作树中所做的更改状态良好时,从主仓库更新公共仓库。这通常被称为 推送。
注意
|
这个公共仓库可以进一步被镜像,这就是 kernel.org 上的 Git 仓库的管理方式。 |
将更改从您的本地(私有)仓库发布到远程(公共)仓库需要在远程机器上拥有写入权限。您需要有一个 SSH 账户才能在那里运行一个命令,git-receive-pack。
首先,您需要在远程机器上创建一个空仓库,用于存放您的公共仓库。这个空仓库将在以后通过推送到它来填充和保持最新。显然,这个仓库的创建只需要进行一次。
注意
|
git push 使用一对命令:您本地机器上的 git send-pack 和远程机器上的 git-receive-pack。两者之间的网络通信内部使用 SSH 连接。 |
您的私有仓库的 Git 目录通常是 .git
,但您的公共仓库通常以项目名称命名,即 <project>.git
。让我们为项目 my-git
创建这样一个公共仓库。登录远程机器后,创建一个空目录
$ mkdir my-git.git
然后,通过运行 git init 将该目录转换为 Git 仓库,但这次,由于其名称不是通常的 .git
,我们稍微做些不同的事情
$ GIT_DIR=my-git.git git init
确保此目录可供您希望通过所选传输方式拉取更改的其他人访问。此外,您还需要确保您的 $PATH
中有 git-receive-pack 程序。
注意
|
许多 sshd 安装在您直接运行程序时不会将您的 shell 作为登录 shell 调用;这意味着如果您的登录 shell 是 bash,则只读取 .bashrc 而不读取 .bash_profile 。作为一种解决方法,请确保 .bashrc 设置了 $PATH ,以便您可以运行 git-receive-pack 程序。 |
注意
|
如果您计划通过 HTTP 发布此仓库以供访问,此时您应该执行 mv my-git.git/hooks/post-update.sample my-git.git/hooks/post-update 。这确保了每次您推送到此仓库时,都会运行 git update-server-info 。 |
您的“公共仓库”现在已准备好接受您的更改。回到您拥有私有仓库的机器上。在那里,运行此命令
$ git push <public-host>:/path/to/my-git.git master
这将使您的公共仓库与指定的分支头(在本例中为 master
)以及当前仓库中可从它们访问的对象保持同步。
作为一个真实的例子,这是我更新我的公共 Git 仓库的方式。Kernel.org 镜像网络负责传播到其他公开可见的机器。
$ git push master.kernel.org:/pub/scm/git/git.git/
打包您的仓库
早些时候,我们看到在 .git/objects/??/ 目录下,您创建的每个 Git 对象都存储为一个文件。这种表示方式在原子和安全创建方面是高效的,但在网络传输方面却不太方便。由于 Git 对象一旦创建就是不可变的,所以有一种方法可以通过“将它们打包在一起”来优化存储。命令
$ git repack
它将为您完成这项工作。如果您按照教程示例操作,您现在应该已经在 .git/objects/??/ 目录中累积了大约 17 个对象。 git repack 会告诉您它打包了多少个对象,并将打包文件存储在 .git/objects/pack
目录中。
注意
|
您将在 .git/objects/pack 目录中看到两个文件:pack-*.pack 和 pack-*.idx 。它们之间密切相关,如果您因任何原因手动将它们复制到不同的仓库,应确保同时复制它们。前者包含打包中所有对象的数据,后者则包含用于随机访问的索引。 |
如果您很偏执,运行 git verify-pack 命令会检测您是否有损坏的包,但请不要太担心。我们的程序总是完美的 ;-)。
一旦您打包了对象,就不再需要保留打包文件中包含的未打包对象了。
$ git prune-packed
将为您删除它们。
如果您好奇,可以在运行 git
prune-packed
前后尝试运行 find
.git/objects
-type
f
。另外 git
count-objects
会告诉您仓库中有多少未打包对象以及它们占用了多少空间。
注意
|
对于 HTTP 传输,git pull 稍微有些麻烦,因为打包的仓库中可能在相对较大的包中包含相对较少的对象。如果您预期公共仓库会有大量 HTTP 拉取,您可能需要经常重新打包和修剪,或者从不这样做。 |
如果您此时再次运行 git
repack
,它会显示“Nothing new to pack.”(没有新内容可打包)。一旦您继续开发并积累更改,再次运行 git
repack
将创建一个新的包,其中包含自上次打包仓库以来创建的对象。我们建议您在首次导入后(除非您从零开始项目)尽快打包您的项目,然后根据项目的活跃程度,不时运行 git
repack
。
当仓库通过 git
push
和 git
pull
同步时,源仓库中打包的对象通常在目标仓库中以未打包形式存储。虽然这允许您在两端使用不同的打包策略,但这也意味着您可能需要不时地重新打包两个仓库。
与他人协作
尽管 Git 是一个真正的分布式系统,但通常以非正式的开发者层级结构组织项目会很方便。Linux 内核开发就是以这种方式运行的。在 Randy Dunlap 的演示文稿 中有一个很好的插图(第 17 页,“合并到主线”)。
应该强调的是,这种层级结构纯粹是 非正式的。Git 中没有根本性的东西来强制执行这种层级结构所暗示的“补丁流链”。您不必只从一个远程仓库拉取。
“项目负责人”的推荐工作流程如下
-
在您的本地机器上准备您的主仓库。您的工作在那里完成。
-
准备一个可供他人访问的公共仓库。
如果其他人通过哑传输协议(HTTP)从您的仓库拉取,您需要保持此仓库 对哑传输友好。在
git
init
之后,从标准模板复制的$GIT_DIR/hooks/post-update.sample
将包含对 git update-server-info 的调用,但您需要手动使用mv
post-update.sample
post-update
启用钩子。这确保 git update-server-info 保持必要的文件最新。 -
从您的主仓库推送到公共仓库。
-
对公共仓库进行 git repack。这会建立一个大包,其中包含初始对象集作为基线,如果用于从您的仓库拉取的传输支持打包仓库,则可能进行 git prune。
-
继续在您的主仓库中工作。您的更改包括您自己的修改、通过电子邮件收到的补丁,以及从“子系统维护者”的“公共”仓库拉取而产生的合并。
您可以随时重新打包此私有仓库。
-
将您的更改推送到公共仓库,并向公众宣布。
-
每隔一段时间,对公共仓库进行 git repack。回到步骤 5 并继续工作。
一个“子系统维护者”在项目上工作并拥有自己的“公共仓库”的推荐工作周期如下
-
通过对“项目负责人”(如果您在一个子系统上工作,则为“子系统维护者”)的公共仓库运行 git clone 来准备您的工作仓库。用于初始克隆的 URL 存储在 remote.origin.url 配置变量中。
-
准备一个可供他人访问的公共仓库,就像“项目负责人”所做的那样。
-
将“项目负责人”公共仓库中的打包文件复制到您的公共仓库,除非“项目负责人”仓库与您的仓库位于同一台机器上。在后一种情况下,您可以使用
objects/info/alternates
文件指向您借用对象的仓库。 -
从您的主仓库推送到公共仓库。运行 git repack,如果用于从您的仓库拉取的传输支持打包仓库,则可能运行 git prune。
-
继续在您的主仓库中工作。您的更改包括您自己的修改、通过电子邮件收到的补丁,以及从您的“项目负责人”以及可能从您的“子子系统维护者”的“公共”仓库拉取而产生的合并。
您可以随时重新打包此私有仓库。
-
将您的更改推送到您的公共仓库,并要求您的“项目负责人”以及可能您的“子子系统维护者”从那里拉取。
-
每隔一段时间,对公共仓库进行 git repack。回到步骤 5 并继续工作。
对于没有“公共”仓库的“个人开发者”来说,推荐的工作周期有所不同。它如下所示
-
通过 git clone “项目负责人”(如果您在一个子系统上工作,则为“子系统维护者”)的公共仓库来准备您的工作仓库。用于初始克隆的 URL 存储在 remote.origin.url 配置变量中。
-
在您的仓库的 master 分支上进行工作。
-
不时地从上游的公共仓库运行
git
fetch
origin
。这只执行git
pull
的前半部分,但不进行合并。公共仓库的 HEAD 存储在.git/refs/remotes/origin/master
中。 -
使用
git
cherry
origin
查看您的哪些补丁被接受,和/或使用git
rebase
origin
将您未合并的更改向前移植到更新后的上游。 -
使用
git
format-patch
origin
准备补丁以通过电子邮件提交给您的上游,并将其发送出去。回到步骤 2 并继续。
与他人协作,共享仓库风格
如果您来自 CVS 背景,上一节中建议的协作风格对您来说可能比较新。您不必担心。Git 也支持您可能更熟悉的“共享公共仓库”协作风格。
有关详细信息,请参阅 gitcvs-migration[7]。
捆绑您的工作
您很可能同时处理多项任务。使用 Git 的分支可以轻松管理这些或多或少独立的任务。
我们之前已经通过“趣味与工作”的两个分支示例了解了分支的工作方式。如果有两个以上的分支,其理念是相同的。假设您从“master”头部开始,并且在“master”分支中有一些新代码,以及在“commit-fix”和“diff-fix”分支中有两个独立的修复
$ git show-branch ! [commit-fix] Fix commit message normalization. ! [diff-fix] Fix rename detection. * [master] Release candidate #1 --- + [diff-fix] Fix rename detection. + [diff-fix~1] Better common substring algorithm. + [commit-fix] Fix commit message normalization. * [master] Release candidate #1 ++* [diff-fix~2] Pretty-print messages.
这两个修复都已充分测试,此时,您想将它们都合并进来。您可以先合并 diff-fix,然后合并 commit-fix,像这样
$ git merge -m "Merge fix in diff-fix" diff-fix $ git merge -m "Merge fix in commit-fix" commit-fix
这将导致
$ git show-branch ! [commit-fix] Fix commit message normalization. ! [diff-fix] Fix rename detection. * [master] Merge fix in commit-fix --- - [master] Merge fix in commit-fix + * [commit-fix] Fix commit message normalization. - [master~1] Merge fix in diff-fix +* [diff-fix] Fix rename detection. +* [diff-fix~1] Better common substring algorithm. * [master~2] Release candidate #1 ++* [master~3] Pretty-print messages.
然而,当你有一组真正独立的变化时(如果顺序很重要,那么根据定义它们就不是独立的),并没有特别的理由先合并一个分支再合并另一个。你可以一次性将这两个分支合并到当前分支。首先,让我们撤销刚刚的操作并重新开始。我们需要通过将其重置为 master~2 来获取这两个合并之前的 master 分支
$ git reset --hard master~2
你可以确保 git
show-branch
与你刚才执行的两次 git merge 之前的状态匹配。然后,你无需连续运行两次 git merge 命令,而是可以合并这两个分支头(这被称为 创建章鱼式合并)
$ git merge commit-fix diff-fix $ git show-branch ! [commit-fix] Fix commit message normalization. ! [diff-fix] Fix rename detection. * [master] Octopus merge of branches 'diff-fix' and 'commit-fix' --- - [master] Octopus merge of branches 'diff-fix' and 'commit-fix' + * [commit-fix] Fix commit message normalization. +* [diff-fix] Fix rename detection. +* [diff-fix~1] Better common substring algorithm. * [master~1] Release candidate #1 ++* [master~2] Pretty-print messages.
请注意,不要仅仅因为可以就进行章鱼式合并。章鱼式合并是一种有效的做法,如果你同时合并两个以上的独立变更,它通常能让提交历史更容易查看。然而,如果你在合并任何分支时遇到合并冲突,并且需要手动解决,这表明这些分支中的开发毕竟不是独立的,你应该一次合并两个,并记录你如何解决冲突以及为什么你偏好其中一方的更改。否则,这将使项目历史更难跟踪,而不是更容易。