简体中文 ▾ 主题 ▾ 最新版本 ▾ user-manual 最后更新于 2.48.0

介绍

Git 是一个快速的分布式版本控制系统。

本手册旨在为具备基本 UNIX 命令行技能但没有 Git 经验的用户提供可读性。

仓库和分支 以及 探索 Git 历史 解释了如何使用 Git 获取和研究项目——阅读这些章节可以学习如何构建和测试软件项目的特定版本、搜索回归等。

需要进行实际开发的用户也应该阅读 使用 Git 进行开发与他人共享开发

后续章节涵盖更专业的主题。

完整的参考文档可通过手册页或 git-help[1] 命令获取。例如,对于命令 git clone <repo>,您可以使用

$ man git-clone

$ git help clone

使用后者,您可以使用自己选择的手册查看器;有关更多信息,请参阅 git-help[1]

另请参阅 Git 快速参考,了解 Git 命令的简要概述,不含任何解释。

最后,请参阅 本手册的注意事项和待办事项列表,了解如何帮助使本手册更完整。

仓库和分支

如何获取 Git 仓库

在阅读本手册时,拥有一个 Git 仓库进行实验将非常有用。

获取仓库的最佳方式是使用 git-clone[1] 命令下载现有仓库的副本。如果您还没有想到项目,这里有一些有趣的例子

	# Git itself (approx. 40MB download):
$ git clone git://git.kernel.org/pub/scm/git/git.git
	# the Linux kernel (approx. 640MB download):
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

对于大型项目,初始克隆可能很耗时,但您只需克隆一次。

克隆命令会创建一个以项目命名的目录(在上面的示例中为 gitlinux)。cd 进入此目录后,您会发现它包含项目文件的副本,称为工作树,以及一个名为 .git 的特殊顶级目录,其中包含项目的所有历史信息。

如何检出项目的不同版本

Git 最适合被视为存储文件集合历史记录的工具。它将历史记录存储为项目内容的相互关联的压缩快照集合。在 Git 中,每个这样的版本都称为提交

这些快照不一定都从旧到新排列成一行;相反,工作可能会同时沿着平行的开发线进行,这些开发线称为分支,它们可以合并和分化。

一个 Git 仓库可以跟踪多个分支上的开发。它通过维护一个列表来实现这一点,这些头引用了每个分支上的最新提交;git-branch[1] 命令显示分支头的列表

$ git branch
* master

新克隆的仓库默认包含一个名为“master”的单个分支头,工作目录初始化为该分支头所引用的项目状态。

大多数项目也使用标签。标签和头一样,是对项目历史的引用,可以使用 git-tag[1] 命令列出

$ git tag -l
v2.6.11
v2.6.11-tree
v2.6.12
v2.6.12-rc2
v2.6.12-rc3
v2.6.12-rc4
v2.6.12-rc5
v2.6.12-rc6
v2.6.13
...

标签预期始终指向项目的相同版本,而头则预期随着开发进展而前进。

创建一个指向其中一个版本的新分支头,并使用 git-switch[1] 检出它

$ git switch -c new v2.6.13

然后工作目录会反映项目在被标记为 v2.6.13 时的内容,并且 git-branch[1] 显示两个分支,星号标记当前已检出的分支

$ git branch
  master
* new

如果您决定想查看版本 2.6.17,您可以修改当前分支以指向 v2.6.17,使用

$ git reset --hard v2.6.17

请注意,如果当前分支头是您对历史上特定点的唯一引用,那么重置该分支可能会导致您无法找到它曾经指向的历史记录;因此请谨慎使用此命令。

理解历史:提交

项目历史中的每个更改都由一个提交表示。git-show[1] 命令显示当前分支上的最新提交

$ git show
commit 17cf781661e6d38f737f15f53ab552f1e95960d7
Author: Linus Torvalds <torvalds@ppc970.osdl.org.(none)>
Date:   Tue Apr 19 14:11:06 2005 -0700

    Remove duplicate getenv(DB_ENVIRONMENT) call

    Noted by Tony Luck.

diff --git a/init-db.c b/init-db.c
index 65898fa..b002dc6 100644
--- a/init-db.c
+++ b/init-db.c
@@ -7,7 +7,7 @@

 int main(int argc, char **argv)
 {
-	char *sha1_dir = getenv(DB_ENVIRONMENT), *path;
+	char *sha1_dir, *path;
 	int len, i;

 	if (mkdir(".git", 0755) < 0) {

如您所见,一次提交显示了谁进行了最新更改、他们做了什么以及原因。

每个提交都有一个 40 位的十六进制 ID,有时称为“对象名称”或“SHA-1 ID”,显示在 git show 输出的第一行。您通常可以使用更短的名称(例如标签或分支名称)来引用提交,但这个更长的名称也很有用。最重要的是,它是此提交的全局唯一名称:因此如果您告诉其他人对象名称(例如在电子邮件中),则可以保证该名称将引用其仓库中与您仓库中相同的提交(假设他们的仓库根本有该提交)。由于对象名称是根据提交内容计算的哈希值,因此可以保证提交永远不会在名称不变的情况下发生更改。

事实上,在 Git 概念 中我们将看到,Git 历史中存储的所有内容,包括文件数据和目录内容,都存储在一个对象中,该对象的名称是其内容的哈希值。

理解历史:提交、父提交和可达性

每个提交(项目中的第一个提交除外)也都有一个父提交,它显示此提交之前发生的情况。沿着父提交链最终会将您带回项目的起点。

然而,提交并不形成一个简单的列表;Git 允许开发线分歧然后重新聚合,两条开发线重新聚合的点称为“合并”。因此,表示合并的提交可以有多个父提交,每个父提交代表导致该点的其中一条开发线上最新的提交。

了解其工作原理的最佳方式是使用 gitk[1] 命令;现在在 Git 仓库上运行 gitk 并查找合并提交将有助于理解 Git 如何组织历史。

在下文中,如果提交 X 是提交 Y 的祖先,我们说提交 X 可以从提交 Y “可达”。同样地,您可以说 Y 是 X 的后代,或者存在一条从提交 Y 到提交 X 的父提交链。

理解历史:历史图

我们有时会使用如下所示的图表来表示 Git 历史。提交显示为“o”,它们之间的链接用 - / 和 \ 绘制的线表示。时间从左到右

         o--o--o <-- Branch A
        /
 o--o--o <-- master
        \
         o--o--o <-- Branch B

如果我们需要讨论某个特定提交,字符“o”可以用另一个字母或数字替换。

理解历史:什么是分支?

当我们需要精确时,我们将使用“分支”一词来表示一条开发线,并使用“分支头”(或简称“头”)来表示对分支上最新提交的引用。在上面的例子中,名为“A”的分支头是指向某个特定提交的指针,但我们将导致该点的三条提交线都称为“分支 A”的一部分。

然而,在不会引起混淆的情况下,我们通常将“分支”一词同时用于分支和分支头。

操作分支

创建、删除和修改分支既快速又简单;以下是命令摘要

git branch

列出所有分支。

git branch <branch>

创建一个名为 <branch> 的新分支,引用与当前分支相同的历史点。

git branch <branch> <start-point>

创建一个名为 <branch> 的新分支,引用 <start-point>,该起点可以以您喜欢的任何方式指定,包括使用分支名称或标签名称。

git branch -d <branch>

删除分支 <branch>;如果该分支未完全合并到其上游分支中或未包含在当前分支中,此命令将发出警告并失败。

git branch -D <branch>

无论其合并状态如何,都删除分支 <branch>

git switch <branch>

将当前分支设为 <branch>,更新工作目录以反映 <branch> 所引用的版本。

git switch -c <new> <start-point>

创建名为 <new> 的新分支,引用 <start-point>,并检出它。

特殊符号“HEAD”始终可用于引用当前分支。事实上,Git 在 .git 目录中使用一个名为 HEAD 的文件来记住哪个分支是当前分支

$ cat .git/HEAD
ref: refs/heads/master

不创建新分支即可查看旧版本

git switch 命令通常需要一个分支头,但当使用 --detach 调用时,它也会接受任意提交;例如,您可以检出由标签引用的提交

$ git switch --detach v2.6.17
Note: checking out 'v2.6.17'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another switch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command again. Example:

  git switch -c new_branch_name

HEAD is now at 427abfa Linux v2.6.17

HEAD 随后引用提交的 SHA-1 而不是分支,并且 git branch 显示您不再位于任何分支上

$ cat .git/HEAD
427abfa28afedffadfca9dd8b067eb6d36bac53f
$ git branch
* (detached from v2.6.17)
  master

在这种情况下,我们说 HEAD 是“分离的”。

这是检出特定版本的简单方法,而无需为新分支命名。如果您决定这样做,稍后仍然可以为这个版本创建一个新分支(或标签)。

查看远程仓库中的分支

您克隆时创建的“master”分支是您克隆来源仓库中 HEAD 的副本。不过,该仓库可能还有其他分支,并且您的本地仓库会保留跟踪这些远程分支的本地跟踪分支,您可以使用 git-branch[1]-r 选项查看它们

$ git branch -r
  origin/HEAD
  origin/html
  origin/maint
  origin/man
  origin/master
  origin/next
  origin/seen
  origin/todo

在此示例中,“origin”被称为远程仓库,或简称“remote”。从我们的角度来看,此仓库的分支被称为“远程分支”。上面列出的远程跟踪分支是在克隆时根据远程分支创建的,并将通过 git fetch(因此是 git pull)和 git push 进行更新。有关详细信息,请参阅 使用 git fetch 更新仓库

您可能希望在自己的分支上基于这些远程跟踪分支之一进行构建,就像您对标签所做的那样

$ git switch -c my-todo-copy origin/todo

您也可以直接检出 origin/todo 以查看或编写一次性补丁。请参阅 分离头

请注意,“origin”名称只是 Git 默认用来指代您克隆来源仓库的名称。

命名分支、标签及其他引用

分支、远程跟踪分支和标签都是对提交的引用。所有引用都以 refs 开头的斜杠分隔路径名命名;我们目前使用的名称实际上是简写

  • 分支 testrefs/heads/test 的简写。

  • 标签 v2.6.18refs/tags/v2.6.18 的简写。

  • origin/masterrefs/remotes/origin/master 的简写。

如果例如存在同名的标签和分支,则完整名称偶尔会很有用。

(新创建的引用实际上存储在 .git/refs 目录下,位于其名称给定的路径下。然而,出于效率原因,它们也可以打包在一个文件中;请参阅 git-pack-refs[1])。

另一个有用的快捷方式是,仓库的“HEAD”可以直接使用该仓库的名称来引用。因此,例如,“origin”通常是仓库“origin”中 HEAD 分支的快捷方式。

有关 Git 检查引用的完整路径列表,以及当存在多个具有相同简写名称的引用时它用于决定选择哪个的顺序,请参阅 gitrevisions[7] 手册页的“指定修订”部分。

使用 git fetch 更新仓库

克隆仓库并提交您自己的更改后,您可能希望检查原始仓库是否有更新。

不带任何参数的 git-fetch 命令会将所有远程跟踪分支更新为原始仓库中找到的最新版本。它不会触及您自己的任何分支——甚至包括为您在克隆时创建的“master”分支。

从其他仓库获取分支

您还可以使用 git-remote[1] 跟踪您克隆来源仓库以外的其他仓库中的分支

$ git remote add staging git://git.kernel.org/.../gregkh/staging.git
$ git fetch staging
...
From git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/staging
 * [new branch]      master     -> staging/master
 * [new branch]      staging-linus -> staging/staging-linus
 * [new branch]      staging-next -> staging/staging-next

新的远程跟踪分支将存储在您提供给 git remote add 的简写名称下,在本例中为 staging

$ git branch -r
  origin/HEAD -> origin/master
  origin/master
  staging/master
  staging/staging-linus
  staging/staging-next

如果您稍后运行 git fetch <remote>,则指定 <remote> 的远程跟踪分支将被更新。

如果您查看文件 .git/config,您会发现 Git 添加了一个新的节

$ cat .git/config
...
[remote "staging"]
	url = git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/staging.git
	fetch = +refs/heads/*:refs/remotes/staging/*
...

这就是 Git 跟踪远程分支的原因;您可以通过使用文本编辑器编辑 .git/config 来修改或删除这些配置选项。(有关详细信息,请参阅 git-config[1] 的“配置文件”部分。)

探索 Git 历史

Git 最适合被视为存储文件集合历史记录的工具。它通过存储文件层次结构内容的压缩快照,以及显示这些快照之间关系的“提交”来实现这一点。

Git 提供了极其灵活和快速的工具来探索项目历史。

我们从一个专门的工具开始,它对于查找引入错误到项目中的提交非常有用。

如何使用 bisect 查找回归

假设您的项目版本 2.6.18 正常工作,但“master”上的版本崩溃了。有时,找到这种回归原因的最佳方法是在项目历史中执行暴力搜索,以找到导致问题的特定提交。git-bisect[1] 命令可以帮助您完成此操作

$ git bisect start
$ git bisect good v2.6.18
$ git bisect bad master
Bisecting: 3537 revisions left to test after this
[65934a9a028b88e83e2b0f8b36618fe503349f8e] BLOCK: Make USB storage depend on SCSI rather than selecting it [try #6]

此时如果您运行 git branch,您会看到 Git 暂时将您移动到了“(无分支)”状态。HEAD 现在已从任何分支分离,并直接指向一个提交(提交 ID 为 65934),该提交可从“master”到达但无法从 v2.6.18 到达。编译并测试它,看看是否会崩溃。假设它确实崩溃了。然后

$ git bisect bad
Bisecting: 1769 revisions left to test after this
[7eff82c8b1511017ae605f0c99ac275a7e21b867] i2c-core: Drop useless bitmaskings

检出旧版本。像这样继续,在每个阶段告诉 Git 它给您的版本是好的还是坏的,并注意每次剩余要测试的修订版数量大约减少一半。

大约 13 次测试(在本例中)后,它将输出导致问题的提交的提交 ID。然后您可以使用 git-show[1] 检查该提交,找出是谁编写的,并附上提交 ID 给他们发送错误报告。最后,运行

$ git bisect reset

将您返回到之前的分支。

请注意,git bisect 在每个点为您检出的版本只是一个建议,如果您认为这是一个好主意,您可以自由尝试不同的版本。例如,有时您可能会遇到一个破坏了不相关内容的提交;运行

$ git bisect visualize

它将运行 gitk 并用一个标记“bisect”来标记它选择的提交。选择一个看起来安全的附近提交,记下其提交 ID,然后使用以下命令检出它

$ git reset --hard fb47ddb2db

然后测试,根据需要运行 bisect goodbisect bad,然后继续。

除了 git bisect visualize 然后 git reset --hard fb47ddb2db 之外,您可能只想告诉 Git 您想跳过当前提交

$ git bisect skip

然而,在这种情况下,Git 最终可能无法区分一些最初跳过的提交和后续的坏提交中第一个坏的那个。

如果您有一个可以区分好提交和坏提交的测试脚本,也有方法可以自动化二分查找过程。有关此功能和其他 git bisect 功能的更多信息,请参阅 git-bisect[1]

命名提交

我们已经看到几种命名提交的方式

  • 40 位十六进制对象名称

  • 分支名称:指给定分支头部的提交

  • 标签名称:指给定标签指向的提交(我们已经看到分支和标签是引用的特殊情况)。

  • HEAD:指当前分支的头部

还有更多方式;有关命名修订的完整列表,请参阅 gitrevisions[7] 手册页的“指定修订”部分。一些示例

$ git show fb47ddb2 # the first few characters of the object name
		    # are usually enough to specify it uniquely
$ git show HEAD^    # the parent of the HEAD commit
$ git show HEAD^^   # the grandparent
$ git show HEAD~4   # the great-great-grandparent

回想一下,合并提交可能有一个以上的父提交;默认情况下,^~ 跟随提交中列出的第一个父提交,但您也可以选择

$ git show HEAD^1   # show the first parent of HEAD
$ git show HEAD^2   # show the second parent of HEAD

除了 HEAD 之外,还有其他几个特殊的提交名称

合并(稍后讨论)以及诸如 git reset 之类的操作(它们会更改当前检出的提交),通常会将 ORIG_HEAD 设置为当前操作之前 HEAD 的值。

git fetch 操作始终将最后获取的分支的头存储在 FETCH_HEAD 中。例如,如果您运行 git fetch 而不指定本地分支作为操作目标

$ git fetch git://example.com/proj.git theirbranch

获取的提交仍将可通过 FETCH_HEAD 获取。

当我们讨论合并时,我们还会看到特殊名称 MERGE_HEAD,它指的是我们要合并到当前分支中的另一个分支。

git-rev-parse[1] 命令是一个低级命令,偶尔可用于将提交的某个名称转换为该提交的对象名称

$ git rev-parse origin
e05db0fd4f31dde7005f075a84f96b360d05984b

创建标签

我们还可以创建一个标签来引用某个特定提交;运行以下命令后

$ git tag stable-1 1b2e1d63ff

您可以使用 stable-1 来引用提交 1b2e1d63ff。

这会创建一个“轻量级”标签。如果您还想在标签中包含评论,并可能对其进行加密签名,那么您应该改为创建一个标签对象;有关详细信息,请参阅 git-tag[1] 手册页。

浏览修订版本

git-log[1] 命令可以显示提交列表。它本身显示所有可从父提交到达的提交;但您也可以提出更具体的请求

$ git log v2.5..	# commits since (not reachable from) v2.5
$ git log test..master	# commits reachable from master but not test
$ git log master..test	# ...reachable from test but not master
$ git log master...test	# ...reachable from either test or master,
			#    but not both
$ git log --since="2 weeks ago" # commits from the last 2 weeks
$ git log Makefile      # commits which modify Makefile
$ git log fs/		# ... which modify any file under fs/
$ git log -S'foo()'	# commits which add or remove any file data
			# matching the string 'foo()'

当然,您可以将所有这些结合起来;以下命令查找自 v2.5 以来修改过 Makefilefs 下任何文件的提交

$ git log v2.5.. Makefile fs/

您还可以要求 git log 显示补丁

$ git log -p

有关更多显示选项,请参阅 git-log[1] 手册页中的 --pretty 选项。

请注意,git log 从最新提交开始,然后向后遍历其父提交;然而,由于 Git 历史可以包含多个独立的开发线,因此提交的特定列出顺序可能有些随意。

生成差异

您可以使用 git-diff[1] 在任意两个版本之间生成差异

$ git diff master..test

这会生成两个分支尖端之间的差异。如果您希望找到从它们的共同祖先到测试的差异,您可以使用三个点而不是两个点

$ git diff master...test

有时您需要的是一组补丁;为此您可以使用 git-format-patch[1]

$ git format-patch master..test

将生成一个文件,其中包含可从 test 访问但不能从 master 访问的每个提交的补丁。

查看旧文件版本

您总是可以通过先检出正确的修订版本来查看文件的旧版本。但有时更方便的是无需检出任何内容即可查看单个文件的旧版本;此命令可实现这一点

$ git show v2.5:fs/locks.c

冒号前可以是任何命名提交的内容,冒号后可以是 Git 跟踪的文件的任何路径。

示例

计算分支上的提交数量

假设您想知道 mybranch 自与 origin 分歧以来您已提交了多少次

$ git log --pretty=oneline origin..mybranch | wc -l

或者,您可能会经常看到使用低级命令 git-rev-list[1] 来完成此类事情,它只列出给定所有提交的 SHA-1

$ git rev-list origin..mybranch | wc -l

检查两个分支是否指向相同的历史记录

假设您想检查两个分支是否指向历史中的同一点。

$ git diff origin..master

会告诉您项目内容在两个分支上是否相同;然而,理论上,相同的项目内容可能通过两条不同的历史路径到达。您可以比较对象名称

$ git rev-list origin
e05db0fd4f31dde7005f075a84f96b360d05984b
$ git rev-list master
e05db0fd4f31dde7005f075a84f96b360d05984b

或者您可以回想一下 ... 运算符选择可从一个引用或另一个引用到达但不能同时从两者到达的所有提交;所以

$ git log origin...master

当两个分支相等时,将不返回任何提交。

查找包含给定修复的第一个带标签版本

假设您知道提交 e05db0fd 修复了某个问题。您想找到包含该修复的最早带标签发布版本。

当然,可能有不止一个答案——如果历史在提交 e05db0fd 之后分支了,那么可能存在多个“最早的”带标签发布版本。

您可以直接目视检查自 e05db0fd 后的提交

$ gitk e05db0fd..

或者您可以使用 git-name-rev[1],它将根据指向该提交后代之一的任何标签为该提交命名

$ git name-rev --tags e05db0fd
e05db0fd tags/v1.5.0-rc1^0~23

git-describe[1] 命令执行相反的操作,使用给定提交所基于的标签命名修订版本

$ git describe e05db0fd
v1.5.0-rc0-260-ge05db0f

但这有时可以帮助您猜测在给定提交之后可能会有哪些标签。

如果您只想验证某个带标签版本是否包含给定提交,您可以使用 git-merge-base[1]

$ git merge-base e05db0fd v1.5.0-rc1
e05db0fd4f31dde7005f075a84f96b360d05984b

merge-base 命令会查找给定提交的共同祖先,并且在其中一个提交是另一个提交的后代的情况下,始终返回其中一个;因此,上面的输出显示 e05db0fd 实际上是 v1.5.0-rc1 的祖先。

或者,请注意

$ git log v1.5.0-rc1..e05db0fd

当且仅当 v1.5.0-rc1 包含 e05db0fd 时,才会产生空输出,因为它只输出无法从 v1.5.0-rc1 访问的提交。

作为另一种选择,git-show-branch[1] 命令列出可从其参数到达的提交,并在左侧显示该提交可从哪些参数到达。因此,如果您运行类似以下命令

$ git show-branch e05db0fd v1.5.0-rc0 v1.5.0-rc1 v1.5.0-rc2
! [e05db0fd] Fix warnings in sha1_file.c - use C99 printf format if
available
 ! [v1.5.0-rc0] GIT v1.5.0 preview
  ! [v1.5.0-rc1] GIT v1.5.0-rc1
   ! [v1.5.0-rc2] GIT v1.5.0-rc2
...

那么像这样的行

+ ++ [e05db0fd] Fix warnings in sha1_file.c - use C99 printf format if
available

显示 e05db0fd 可从自身、v1.5.0-rc1 和 v1.5.0-rc2 到达,但不能从 v1.5.0-rc0 到达。

显示特定分支独有的提交

假设您想查看可从名为 master 的分支头到达但不能从仓库中任何其他头到达的所有提交。

我们可以使用 git-show-ref[1] 列出此仓库中的所有头

$ git show-ref --heads
bf62196b5e363d73353a9dcf094c59595f3153b7 refs/heads/core-tutorial
db768d5504c1bb46f63ee9d6e1772bd047e05bf9 refs/heads/maint
a07157ac624b2524a059a3414e99f6f44bebc1e7 refs/heads/master
24dbc180ea14dc1aebe09f14c8ecf32010690627 refs/heads/tutorial-2
1e87486ae06626c2f31eaa63d26fc0fd646c8af2 refs/heads/tutorial-fixes

我们可以借助标准实用程序 cut 和 grep 获取分支头名称并删除 master

$ git show-ref --heads | cut -d' ' -f2 | grep -v '^refs/heads/master'
refs/heads/core-tutorial
refs/heads/maint
refs/heads/tutorial-2
refs/heads/tutorial-fixes

然后我们可以要求查看所有可从 master 访问但不能从这些其他头访问的提交

$ gitk master --not $( git show-ref --heads | cut -d' ' -f2 |
				grep -v '^refs/heads/master' )

显然,有无限多种变体;例如,要查看可从某个头访问但不能从仓库中任何标签访问的所有提交

$ gitk $( git show-ref --heads ) --not  $( git show-ref --tags )

(有关诸如 --not 等提交选择语法的解释,请参阅 gitrevisions[7]。)

为软件发布创建变更日志和 tarball

git-archive[1] 命令可以从项目的任何版本创建 tar 或 zip 归档文件;例如

$ git archive -o latest.tar.gz --prefix=project/ HEAD

将使用 HEAD 生成一个 gzip 压缩的 tar 归档文件,其中每个文件名都以 project/ 为前缀。如果可能,输出文件格式将从输出文件扩展名推断,请参阅 git-archive[1] 获取详细信息。

版本早于 1.7.7 的 Git 不知道 tar.gz 格式,您需要明确使用 gzip

$ git archive --format=tar --prefix=project/ HEAD | gzip >latest.tar.gz

如果您正在发布新版本的软件项目,您可能希望同时制作一个变更日志以包含在发布公告中。

例如,Linus Torvalds 通过给新内核版本打标签,然后运行

$ release-script 2.6.12 2.6.13-rc6 2.6.13-rc7

其中 release-script 是一个看起来像这样的 shell 脚本

#!/bin/sh
stable="$1"
last="$2"
new="$3"
echo "# git tag v$new"
echo "git archive --prefix=linux-$new/ v$new | gzip -9 > ../linux-$new.tar.gz"
echo "git diff v$stable v$new | gzip -9 > ../patch-$new.gz"
echo "git log --no-merges v$new ^v$last > ../ChangeLog-$new"
echo "git shortlog --no-merges v$new ^v$last > ../ShortLog"
echo "git diff --stat --summary -M v$last v$new > ../diffstat-$new"

然后他只需在验证输出命令看起来正常后剪切并粘贴它们。

查找引用了给定文件内容的提交

有人给了你一个文件副本,并询问哪些提交修改了该文件,使得该文件在提交之前或之后包含给定的内容。你可以通过以下方式找到答案

$  git log --raw --abbrev=40 --pretty=oneline |
	grep -B 1 `git hash-object filename`

弄清楚其工作原理留给(高级)学生作为练习。git-log[1]git-diff-tree[1]git-hash-object[1] 手册页可能会有所帮助。

使用 Git 进行开发

告诉 Git 你的名字

在创建任何提交之前,你应该向 Git 介绍你自己。最简单的方法是使用 git-config[1]

$ git config --global user.name 'Your Name Comes Here'
$ git config --global user.email 'you@yourdomain.example.com'

这将在你的主目录中名为 .gitconfig 的文件中添加以下内容

[user]
	name = Your Name Comes Here
	email = you@yourdomain.example.com

有关配置文件的详细信息,请参阅 git-config[1] 的“配置文件”部分。该文件是纯文本文件,因此你也可以使用你喜欢的编辑器对其进行编辑。

创建新仓库

从头开始创建一个新仓库非常简单

$ mkdir project
$ cd project
$ git init

如果你有一些初始内容(例如,一个 tarball)

$ tar xzvf project.tar.gz
$ cd project
$ git init
$ git add . # include everything below ./ in the first commit:
$ git commit

如何进行提交

创建一个新提交需要三个步骤

  1. 使用你喜欢的编辑器对工作目录进行一些更改。

  2. 告知 Git 你的更改。

  3. 使用你在第 2 步中告知 Git 的内容创建提交。

在实践中,你可以根据需要多次交替和重复步骤 1 和 2:为了跟踪你在步骤 3 中想要提交的内容,Git 在一个名为“索引”(index)的特殊暂存区中维护了树内容的快照。

最初,索引的内容将与 HEAD 的内容相同。因此,显示 HEAD 和索引之间差异的命令 git diff --cached 在那时应该不会产生任何输出。

修改索引很简单

要使用新文件或修改后的文件的内容更新索引,请使用

$ git add path/to/file

要从索引和工作树中删除文件,请使用

$ git rm path/to/file

每一步之后,你都可以验证

$ git diff --cached

始终显示 HEAD 和索引文件之间的差异——这是如果你现在创建提交将提交的内容——并且

$ git diff

显示工作树和索引文件之间的差异。

请注意,git add 总是只将文件的当前内容添加到索引中;对同一文件的进一步更改将被忽略,除非你再次对该文件运行 git add

当你准备好时,只需运行

$ git commit

Git 将提示你输入提交信息,然后创建新提交。使用以下命令检查以确保它符合你的预期

$ git show

作为一项特殊快捷方式,

$ git commit -a

将更新索引中所有已修改或已删除的文件,并一步完成提交。

有许多命令对于跟踪你即将提交的内容很有用

$ git diff --cached # difference between HEAD and the index; what
		    # would be committed if you ran "commit" now.
$ git diff	    # difference between the index file and your
		    # working directory; changes that would not
		    # be included if you ran "commit" now.
$ git diff HEAD	    # difference between HEAD and working tree; what
		    # would be committed if you ran "commit -a" now.
$ git status	    # a brief per-file summary of the above.

你还可以使用 git-gui[1] 来创建提交、查看索引和工作树文件中的更改,并单独选择要包含在索引中的差异块(通过右键单击差异块并选择“Stage Hunk For Commit”)。

创建好的提交信息

虽然不是必需的,但最好以一行简短(不超过 50 个字符)的更改摘要作为提交信息的开头,后面跟一个空行,然后是更详细的描述。提交信息中直到第一个空行之前的文本被视为提交标题,该标题在整个 Git 中都会使用。例如,git-format-patch[1] 将提交转换为电子邮件,它将标题用作主题行,其余提交内容用作正文。

忽略文件

项目通常会生成你不想用 Git 跟踪的文件。这通常包括构建过程生成的文件或编辑器创建的临时备份文件。当然,用 Git 跟踪文件只是对其调用 git add 的问题。但这些未跟踪的文件散落在各处很快就会变得令人烦恼;例如,它们使得 git add . 几乎毫无用处,并且它们会不断出现在 git status 的输出中。

你可以通过在工作目录的顶层创建一个名为 .gitignore 的文件来告诉 Git 忽略某些文件,其内容如下

# Lines starting with '#' are considered comments.
# Ignore any file named foo.txt.
foo.txt
# Ignore (generated) html files,
*.html
# except foo.html which is maintained by hand.
!foo.html
# Ignore objects and archives.
*.[oa]

有关语法的详细说明,请参阅 gitignore[5]。你还可以将 .gitignore 文件放置在工作树中的其他目录中,它们将应用于这些目录及其子目录。.gitignore 文件可以像其他任何文件一样添加到你的仓库中(只需像往常一样运行 git add .gitignoregit commit),当排除模式(例如匹配构建输出文件的模式)对克隆你仓库的其他用户也有意义时,这会很方便。

如果你希望排除模式仅影响某些仓库(而不是给定项目的每个仓库),你可以将它们放入你的仓库中名为 .git/info/exclude 的文件,或由 core.excludesFile 配置变量指定的任何文件中。某些 Git 命令也可以直接在命令行上接受排除模式。有关详细信息,请参阅 gitignore[5]

如何合并

你可以使用 git-merge[1] 将两个分叉的开发分支重新合并

$ git merge branchname

将分支 branchname 中的开发内容合并到当前分支中。

合并是通过合并自其历史分叉以来 branchname 中所做的更改以及当前分支中直到最新提交所做的更改来完成的。当合并干净完成时,工作树会被合并结果覆盖;当合并导致冲突时,则会被部分合并的结果覆盖。因此,如果你有未提交的更改涉及到与合并受影响的文件相同的文件,Git 将拒绝继续。大多数情况下,你会希望在合并之前提交你的更改,如果你没有这样做,那么 git-stash[1] 可以在你进行合并时将这些更改暂存起来,并在之后重新应用它们。

如果更改足够独立,Git 将自动完成合并并提交结果(或者在快进的情况下重用现有提交,见下文)。另一方面,如果存在冲突——例如,如果同一个文件在远程分支和本地分支中以两种不同的方式被修改——那么你将收到警告;输出可能看起来像这样

$ git merge next
 100% (4/4) done
Auto-merged file.txt
CONFLICT (content): Merge conflict in file.txt
Automatic merge failed; fix conflicts and then commit the result.

冲突标记会留在有问题的文件中,手动解决冲突后,你可以用新内容更新索引并运行 Git commit,就像你通常创建新文件时那样。

如果你使用 gitk 检查结果提交,你将看到它有两个父提交,一个指向当前分支的顶部,另一个指向另一个分支的顶部。

解决合并

当合并未能自动解决时,Git 会将索引和工作树置于一种特殊状态,为你提供解决合并所需的所有信息。

有冲突的文件在索引中会被特殊标记,因此在你解决问题并更新索引之前,git-commit[1] 将会失败

$ git commit
file.txt: needs merge

此外,git-status[1] 会将这些文件列为“未合并”,并且有冲突的文件会添加冲突标记,如下所示

<<<<<<< HEAD:file.txt
Hello world
=======
Goodbye
>>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt

你只需编辑文件来解决冲突,然后

$ git add file.txt
$ git commit

请注意,提交信息将已为你填写了一些关于合并的信息。通常你可以直接使用此默认信息而无需更改,但如果需要,你可以添加自己的额外注释。

以上就是解决简单合并所需了解的全部内容。但 Git 还提供了更多信息来帮助解决冲突

在合并期间获取冲突解决帮助

所有 Git 能够自动合并的更改都已添加到索引文件中,因此 git-diff[1] 只显示冲突。它使用一种不寻常的语法

$ git diff
diff --cc file.txt
index 802992c,2b60207..0000000
--- a/file.txt
+++ b/file.txt
@@@ -1,1 -1,1 +1,5 @@@
++<<<<<<< HEAD:file.txt
 +Hello world
++=======
+ Goodbye
++>>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt

请记住,我们解决此冲突后将提交的提交将有两个父提交,而不是通常的一个:一个父提交将是 HEAD,即当前分支的尖端;另一个将是另一个分支的尖端,它暂时存储在 MERGE_HEAD 中。

在合并期间,索引会保留每个文件的三个版本。这三个“文件阶段”中的每一个都代表文件的不同版本

$ git show :1:file.txt	# the file in a common ancestor of both branches
$ git show :2:file.txt	# the version from HEAD.
$ git show :3:file.txt	# the version from MERGE_HEAD.

当你要求 git-diff[1] 显示冲突时,它会在工作树中对冲突的合并结果与阶段 2 和 3 进行三方比较,以仅显示内容来自双方(混合)的差异块(换句话说,当一个差异块的合并结果仅来自阶段 2 时,该部分不冲突且不显示。阶段 3 亦然)。

上面的差异显示了 file.txt 的工作树版本与阶段 2 和阶段 3 版本之间的差异。因此,它现在不使用单个 +- 前缀每行,而是使用两列:第一列用于显示第一个父提交与工作目录副本之间的差异,第二列用于显示第二个父提交与工作目录副本之间的差异。(有关格式的详细信息,请参阅 git-diff-files[1] 的“组合差异格式”部分。)

以明显的方式解决冲突后(但在更新索引之前),差异将如下所示

$ git diff
diff --cc file.txt
index 802992c,2b60207..0000000
--- a/file.txt
+++ b/file.txt
@@@ -1,1 -1,1 +1,1 @@@
- Hello world
 -Goodbye
++Goodbye world

这表明我们解决后的版本从第一个父提交中删除了“Hello world”,从第二个父提交中删除了“Goodbye”,并添加了“Goodbye world”,而这在之前两者中都不存在。

一些特殊的差异选项允许将工作目录与这些阶段中的任何一个进行比较

$ git diff -1 file.txt		# diff against stage 1
$ git diff --base file.txt	# same as the above
$ git diff -2 file.txt		# diff against stage 2
$ git diff --ours file.txt	# same as the above
$ git diff -3 file.txt		# diff against stage 3
$ git diff --theirs file.txt	# same as the above.

当使用 ort 合并策略(默认)时,在用合并结果更新工作树之前,Git 会写入一个名为 AUTO_MERGE 的引用,反映它即将写入的树的状态。具有文本冲突且无法自动合并的冲突路径将像在工作树中一样,带有冲突标记写入到此树中。因此,AUTO_MERGE 可以与 git-diff[1] 一起使用,以显示你到目前为止为解决冲突所做的更改。以上述相同示例,解决冲突后我们得到

$ git diff AUTO_MERGE
diff --git a/file.txt b/file.txt
index cd10406..8bf5ae7 100644
--- a/file.txt
+++ b/file.txt
@@ -1,5 +1 @@
-<<<<<<< HEAD:file.txt
-Hello world
-=======
-Goodbye
->>>>>>> 77976da35a11db4580b80ae27e8d65caf5208086:file.txt
+Goodbye world

请注意,差异显示我们删除了冲突标记和内容行的两个版本,并改写为“Goodbye world”。

git-log[1]gitk[1] 命令也为合并提供了特殊帮助

$ git log --merge
$ gitk --merge

这些将显示所有仅存在于 HEAD 或 MERGE_HEAD 上,并涉及到未合并文件的提交。

你还可以使用 git-mergetool[1],它允许你使用 Emacs 或 kdiff3 等外部工具合并未合并的文件。

每次你解决文件中的冲突并更新索引时

$ git add file.txt

该文件的不同阶段将被“折叠”,之后 git diff 将(默认情况下)不再显示该文件的差异。

撤销合并

如果你陷入困境并决定放弃一切,你总是可以通过以下方式返回到合并前的状态

$ git merge --abort

或者,如果你已经提交了你想要放弃的合并,

$ git reset --hard ORIG_HEAD

然而,在某些情况下,最后一个命令可能很危险——如果某个你已经提交的提交本身可能已经合并到另一个分支中,则切勿丢弃它,因为这样做可能会使后续合并混淆。

快进合并

上面没有提到一个特殊情况,它被区别对待。通常,合并会产生一个合并提交,它有两个父提交,分别指向两条被合并的开发线。

然而,如果当前分支是另一个分支的祖先——即当前分支中存在的每个提交都已包含在另一个分支中——那么 Git 只会执行“快进”;当前分支的 HEAD 会向前移动,指向被合并分支的 HEAD,而不会创建任何新提交。

修复错误

如果你搞砸了工作树,但尚未提交你的错误,你可以使用以下命令将整个工作树恢复到上次提交的状态

$ git restore --staged --worktree :/

如果你做了一个后来希望没有做过的提交,有两种根本不同的方法来解决问题

  1. 你可以创建一个新提交来撤销旧提交所做的任何更改。如果你的错误已经公开,这是正确的做法。

  2. 你可以回溯并修改旧提交。如果历史已经公开,你绝不应该这样做;Git 通常不期望项目的“历史”发生变化,并且无法正确执行从历史已更改的分支进行重复合并。

用新提交修复错误

创建一个新提交来恢复之前的更改非常容易;只需将 git-revert[1] 命令传递给错误的提交引用即可;例如,要恢复最近的提交

$ git revert HEAD

这将创建一个新提交,它会撤销 HEAD 中的更改。你将有机会编辑新提交的提交信息。

你也可以恢复更早的更改,例如倒数第二个

$ git revert HEAD^

在这种情况下,Git 会尝试撤销旧更改,同时保留自那时以来的任何更改。如果最近的更改与要恢复的更改重叠,那么你将被要求手动修复冲突,就像解决合并的情况一样。

通过重写历史来修复错误

如果问题提交是最新提交,并且你尚未公开该提交,那么你可以直接使用 git reset 销毁它

或者,你可以编辑工作目录并更新索引来修复你的错误,就像你要创建一个新提交一样,然后运行

$ git commit --amend

这将用一个包含你更改的新提交替换旧提交,同时给你一个机会先编辑旧提交信息。

再次强调,你绝不应该对可能已经合并到另一个分支的提交进行此操作;在这种情况下,请改用 git-revert[1]

也可以替换历史中更早的提交,但这是一个高级主题,将留待另一章讨论。

检出文件的旧版本

在撤销之前不良更改的过程中,你可能会发现使用 git-restore[1] 检出特定文件的旧版本很有用。该命令

$ git restore --source=HEAD^ path/to/file

将 path/to/file 替换为其在提交 HEAD^ 中的内容,并更新索引以匹配。它不改变分支。

如果你只是想查看文件的旧版本,而不修改工作目录,你可以使用 git-show[1] 来实现

$ git show HEAD^:path/to/file

这将显示文件的给定版本。

暂时搁置进行中的工作

当你正在进行复杂工作时,你发现了一个不相关但显而易见的微不足道的 bug。你希望在继续之前修复它。你可以使用 git-stash[1] 来保存你当前的工作状态,并在修复 bug 后(或者,可选地在不同的分支上完成修复后再回来),将进行中的更改从暂存区取出。

$ git stash push -m "work in progress for foo feature"

此命令会将你的更改保存到 stash,并重置你的工作树和索引以匹配当前分支的尖端。然后你就可以像往常一样进行修复了。

... edit and test ...
$ git commit -a -m "blorpl: typofix"

之后,你可以使用 git stash pop 返回到你正在进行的工作

$ git stash pop

确保良好性能

在大型仓库中,Git 依赖压缩来防止历史信息占用过多的磁盘或内存空间。一些 Git 命令可能会自动运行 git-gc[1],因此你无需担心手动运行它。但是,压缩大型仓库可能需要一些时间,因此你可能希望显式调用 gc 以避免在不方便时自动启动压缩。

确保可靠性

检查仓库是否损坏

git-fsck[1] 命令对仓库运行多项自一致性检查,并报告任何问题。这可能需要一些时间。

$ git fsck
dangling commit 7281251ddd2a61e38657c827739c57015671a6b3
dangling commit 2706a059f258c6b245f298dc4ff2ccd30ec21a63
dangling commit 13472b7c4b80851a1bc551779171dcb03655e9b5
dangling blob 218761f9d90712d37a9c5e36f406f92202db07eb
dangling commit bf093535a34a4d35731aa2bd90fe6b176302f14f
dangling commit 8e4bec7f2ddaa268bef999853c25755452100f8e
dangling tree d50bb86186bf27b681d25af89d3b5b68382e4085
dangling tree b24c2473f1fd3d91352a624795be026d64c8841f
...

你将看到关于悬空对象的信息性消息。它们是仍然存在于仓库中但不再被任何分支引用的对象,并且在一段时间后可以通过 gc 删除。你可以运行 git fsck --no-dangling 来抑制这些消息,并仍然查看真正的错误。

恢复丢失的更改

引用日志

假设你使用git reset --hard 修改了一个分支,然后发现该分支是你拥有指向该历史点的唯一引用。

幸运的是,Git 还保留了一个名为“reflog”(引用日志)的日志,记录了每个分支的所有先前值。因此在这种情况下,你仍然可以使用,例如,来找到旧的历史

$ git log master@{1}

这列出了从 master 分支 HEAD 的先前版本可达的提交。这种语法可以与任何接受提交的 Git 命令一起使用,而不仅仅是与 git log。其他一些示例

$ git show master@{2}		# See where the branch pointed 2,
$ git show master@{3}		# 3, ... changes ago.
$ gitk master@{yesterday}	# See where it pointed yesterday,
$ gitk master@{"1 week ago"}	# ... or last week
$ git log --walk-reflogs master	# show reflog entries for master

HEAD 会保留一个单独的引用日志,所以

$ git show HEAD@{"1 week ago"}

将显示 HEAD 一周前指向的内容,而不是当前分支一周前指向的内容。这使你可以查看你已检出内容的修改历史。

引用日志默认保留 30 天,之后可能会被修剪。有关如何控制此修剪的详细信息,请参阅 git-reflog[1]git-gc[1],并参阅 gitrevisions[7] 的“指定修订”部分。

请注意,引用日志历史与普通 Git 历史非常不同。普通历史由处理同一项目的每个仓库共享,而引用日志历史不共享:它只告诉你本地仓库中的分支如何随时间变化。

检查悬空对象

在某些情况下,引用日志可能无法帮助你。例如,假设你删除了一个分支,然后意识到你需要它包含的历史记录。引用日志也会被删除;但是,如果你尚未修剪仓库,那么你可能仍然可以在 git fsck 报告的悬空对象中找到丢失的提交。有关详细信息,请参阅悬空对象

$ git fsck
dangling commit 7281251ddd2a61e38657c827739c57015671a6b3
dangling commit 2706a059f258c6b245f298dc4ff2ccd30ec21a63
dangling commit 13472b7c4b80851a1bc551779171dcb03655e9b5
...

你可以检查其中一个悬空提交,例如,

$ gitk 7281251ddd --not --all

它的作用正如其名:它表示你想要查看由悬空提交所描述的提交历史,而不是由你所有现有分支和标签所描述的历史。因此,你得到的正是从该丢失提交可达的历史记录。(请注意,它可能不仅仅是一个提交:我们只报告“行尖端”为悬空,但可能存在一整个深而复杂的提交历史被丢弃了。)

如果你决定要找回历史,你总是可以创建一个指向它的新引用,例如,一个新的分支

$ git branch recovered-branch 7281251ddd

其他类型的悬空对象(blob 和 tree)也可能存在,并且悬空对象可能在其他情况下出现。

与他人共享开发

使用 git pull 获取更新

在你克隆仓库并提交了一些自己的更改后,你可能希望检查原始仓库的更新并将其合并到你自己的工作中。

我们已经了解了如何使远程跟踪分支保持最新git-fetch[1],以及如何合并两个分支。因此,你可以使用以下命令合并原始仓库 master 分支的更改

$ git fetch
$ git merge origin/master

然而,git-pull[1] 命令提供了一种一步完成此操作的方法

$ git pull origin master

实际上,如果你已经检出了 master,那么该分支已通过 git clone 配置为从 origin 仓库的 HEAD 分支获取更改。因此,通常你只需简单地执行以下操作即可完成上述任务

$ git pull

此命令将从远程分支获取更改到你的远程跟踪分支 origin/*,并将默认分支合并到当前分支。

更一般地说,从远程跟踪分支创建的分支将默认从该分支拉取。有关如何控制这些默认值的详细信息,请参阅 branch.<name>.remotebranch.<name>.merge 选项在 git-config[1] 中的描述,以及 git-checkout[1]--track 选项的讨论。

除了节省你的按键次数外,git pull 还会通过生成一个默认的提交信息来帮助你,其中记录了你从中拉取的分支和仓库。

(但请注意,在快进的情况下不会创建此类提交;相反,你的分支将仅更新以指向来自上游分支的最新提交。)

git pull 命令也可以将 . 作为“远程”仓库,在这种情况下它只会合并当前仓库中的一个分支;所以命令

$ git pull . branch
$ git merge branch

大致等价。

向项目提交补丁

如果你只有少量更改,最简单的提交方式可能就是通过电子邮件发送补丁

首先,使用 git-format-patch[1];例如

$ git format-patch origin

将在当前目录中生成一系列编号文件,当前分支中的每个补丁一个文件,但 origin/HEAD 中的除外。

git format-patch 可以包含一个初始的“封面信”。你可以在 format-patch 在提交信息之后但在补丁本身之前放置的三条虚线之后插入对单个补丁的评论。如果你使用 git notes 来跟踪你的封面信材料,git format-patch --notes 将以类似的方式包含提交的注释。

然后你可以将这些导入到你的邮件客户端并手动发送。但是,如果你有大量要一次性发送,你可能更喜欢使用 git-send-email[1] 脚本来自动化该过程。请先咨询你项目的邮件列表,以确定他们提交补丁的要求。

向项目导入补丁

Git 还提供了一个名为 git-am[1](am 代表“apply mailbox”)的工具,用于导入此类通过电子邮件发送的系列补丁。只需将所有包含补丁的消息按顺序保存到一个邮箱文件,例如 patches.mbox,然后运行

$ git am -3 patches.mbox

Git 将按顺序应用每个补丁;如果发现任何冲突,它将停止,你可以按照“解决合并”中描述的方式修复冲突。(-3 选项告诉 Git 执行合并;如果你希望它只中止并保持你的树和索引不变,你可以省略该选项。)

一旦索引使用冲突解决的结果进行了更新,只需运行,而无需创建新提交

$ git am --continue

Git 将为你创建提交并继续从邮箱中应用剩余的补丁。

最终结果将是一系列提交,原始邮箱中的每个补丁对应一个,其作者和提交日志信息均取自包含相应补丁的消息。

公共 Git 仓库

向项目提交更改的另一种方法是告诉该项目的维护者使用 git-pull[1] 从你的仓库拉取更改。在“使用 git pull 获取更新”一节中,我们将其描述为从“主”仓库获取更新的方法,但它反向也同样适用。

如果你和维护者都在同一台机器上拥有账户,那么你可以直接从彼此的仓库中拉取更改;接受仓库 URL 作为参数的命令也将接受本地目录名

$ git clone /path/to/repository
$ git pull /path/to/other/repository

或 SSH URL

$ git clone ssh://yourhost/~you/repository

对于开发人员较少的项目,或者用于同步少数私有仓库,这可能就是你所需要的全部。

然而,更常见的方法是维护一个单独的公共仓库(通常在不同的主机上),供其他人从中拉取更改。这通常更方便,并且允许你将私有进行中的工作与公开可见的工作清楚地分离。

你将继续在你的个人仓库中进行日常工作,但会定期将更改从你的个人仓库“推”送到你的公共仓库中,允许其他开发人员从该仓库拉取。因此,在有另一个开发人员拥有公共仓库的情况下,更改的流程如下所示

		      you push
your personal repo ------------------> your public repo
      ^                                     |
      |                                     |
      | you pull                            | they pull
      |                                     |
      |                                     |
      |               they push             V
their public repo <------------------- their repo

我们将在以下章节中解释如何进行此操作。

设置公共仓库

假设你的个人仓库在目录 ~/proj 中。我们首先创建仓库的新克隆,并告诉 git daemon 它打算公开

$ git clone --bare ~/proj proj.git
$ touch proj.git/git-daemon-export-ok

生成的目录 proj.git 包含一个“裸”Git 仓库——它只是 .git 目录的内容,没有任何文件在其周围被检出。

接下来,将 proj.git 复制到你计划托管公共仓库的服务器。你可以使用 scp、rsync 或任何最方便的方式。

通过 Git 协议导出 Git 仓库

这是首选方法。

如果服务器由其他人管理,他们应该告诉你将仓库放在哪个目录中,以及它将出现在哪个 git:// URL。然后你可以跳到下面的“推送更改到公共仓库”一节。

否则,你只需启动 git-daemon[1];它将在端口 9418 上监听。默认情况下,它将允许访问任何看起来像 Git 目录并包含魔术文件 git-daemon-export-ok 的目录。将一些目录路径作为 git daemon 参数传递将进一步限制导出到这些路径。

你也可以将 git daemon 作为 inetd 服务运行;有关详细信息,请参阅 git-daemon[1] 手册页。(特别参阅示例部分。)

通过 HTTP 导出 Git 仓库

Git 协议提供了更好的性能和可靠性,但在设置了 Web 服务器的主机上,HTTP 导出可能更容易设置。

你所需要做的就是将新创建的裸 Git 仓库放置在由 Web 服务器导出的目录中,并进行一些调整,以向 Web 客户端提供他们所需的一些额外信息

$ mv proj.git /home/you/public_html/proj.git
$ cd proj.git
$ git --bare update-server-info
$ mv hooks/post-update.sample hooks/post-update

(有关最后两行的解释,请参阅 git-update-server-info[1]githooks[5]。)

宣传 proj.git 的 URL。然后其他人应该能够从该 URL 克隆或拉取,例如使用如下命令行

$ git clone http://yourserver.com/~you/proj.git

(另请参阅 setup-git-server-over-http,了解使用 WebDAV 的更复杂设置,该设置也允许通过 HTTP 推送。)

推送更改到公共仓库

请注意,上面概述的两种技术(通过 httpgit 导出)允许其他维护者获取你的最新更改,但它们不允许写入访问,而你将需要写入访问来用你的私有仓库中创建的最新更改更新公共仓库。

最简单的方法是使用 git-push[1] 和 ssh;要用你的 master 分支的最新状态更新名为 master 的远程分支,运行

$ git push ssh://yourserver.com/~you/proj.git master:master

或者直接

$ git push ssh://yourserver.com/~you/proj.git master

git fetch 类似,如果这没有导致 快进(fast-forward)git push 将会报错;有关处理此情况的详细信息,请参阅以下部分。

请注意,push 的目标通常是一个 裸仓库(bare repository)。你也可以推送到一个包含已检出工作树的仓库,但默认情况下,推送到更新当前已检出分支的操作是被拒绝的,以防止混淆。有关详细信息,请参阅 git-config[1] 中 receive.denyCurrentBranch 选项的描述。

git fetch 类似,你也可以设置配置选项以节省输入;例如

$ git remote add public-repo ssh://yourserver.com/~you/proj.git

将以下内容添加到 .git/config

[remote "public-repo"]
	url = yourserver.com:proj.git
	fetch = +refs/heads/*:refs/remotes/example/*

这让你可以通过简单的以下命令进行相同的推送

$ git push public-repo master

有关 remote.<name>.urlbranch.<name>.remoteremote.<name>.push 选项的解释,请参阅 git-config[1]

当 push 失败时该怎么办

如果 push 不会导致远程分支的 快进,那么它将以类似以下内容的错误失败

 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '...'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

例如,如果你出现以下情况,可能会发生这种情况

你可以通过在分支名称前加上加号来强制 git push 执行更新

$ git push ssh://yourserver.com/~you/proj.git +master

请注意 + 符号的添加。或者,你可以使用 -f 标志来强制远程更新,如下所示

$ git push -f ssh://yourserver.com/~you/proj.git master

通常,当公共仓库中的分支头被修改时,它会修改为指向其之前指向的提交的后代。在这种情况下强制推送,你将打破这个约定。(参见 重写历史的问题。)

尽管如此,对于需要一种简单方式发布正在进行中的补丁系列的人来说,这是一种常见做法,只要你警告其他开发者你打算如何管理该分支,这就是一个可接受的折衷方案。

当其他人有权推送到同一仓库时,推送也可能以这种方式失败。在这种情况下,正确的解决方案是在首先更新你的工作后重试推送:要么通过 pull,要么通过 fetch 后进行 rebase;有关更多信息,请参阅下一节gitcvs-migration[7]

设置共享仓库

另一种协作方式是使用与 CVS 中常用模型类似的方式,即多个具有特殊权限的开发者都推送到并从单个共享仓库拉取。有关如何设置此项的说明,请参阅 gitcvs-migration[7]

然而,尽管 Git 对共享仓库的支持没有任何问题,但通常不推荐这种操作模式,仅仅是因为 Git 支持的协作模式——通过交换补丁和从公共仓库拉取——比中心共享仓库有更多的优势

  • Git 快速导入和合并补丁的能力使单个维护者即使在非常高的速度下也能处理传入的更改。当工作量过大时,git pull 提供了一种简单的方法,让维护者可以将这项工作委托给其他维护者,同时仍然允许对传入更改进行可选审查。

  • 由于每个开发者的仓库都拥有项目历史的完整副本,因此没有哪个仓库是特殊的,其他开发者接管项目维护工作非常简单,无论是通过双方协议,还是因为维护者变得不响应或难以合作。

  • 缺乏一个中心的“提交者”群体意味着对谁“加入”和谁“退出”的正式决策需求减少。

允许通过网页浏览仓库

gitweb cgi 脚本为用户提供了一种无需安装 Git 即可轻松浏览项目修订、文件内容和日志的方式。RSS/Atom feeds 和 blame/annotation 详细信息等功能可以有选择地启用。

git-instaweb[1] 命令提供了一种使用 gitweb 启动浏览仓库的简单方法。使用 instaweb 时的默认服务器是 lighttpd。

有关使用支持 CGI 或 Perl 的服务器进行永久安装的详细说明,请参阅 Git 源码树中的 gitweb/INSTALL 文件和 gitweb[1]

如何获取仅包含最少历史的 Git 仓库

一个具有截断历史的 浅克隆(shallow clone),在只对项目近期历史感兴趣且从上游获取完整历史成本很高时非常有用。

通过指定 git-clone[1]--depth 开关来创建 浅克隆。深度稍后可以使用 git-fetch[1]--depth 开关进行更改,或者使用 --unshallow 恢复完整历史。

浅克隆 中进行合并是可行的,只要合并基础(merge base)在近期历史中。否则,这将类似于合并不相关的历史,并可能导致巨大的冲突。这个限制可能会使这种仓库不适合用于基于合并的工作流。

示例

为 Linux 子系统维护者维护主题分支

这描述了 Tony Luck 如何以 Linux 内核 IA64 架构维护者的身份使用 Git。

他使用两个公共分支

  • 一个“测试”树,补丁最初放置于此,以便在与其他正在进行的开发集成时获得一些曝光。只要 Andrew 愿意,他就可以将此树拉入 -mm。

  • 一个“发布”树,经过测试的补丁会移至此,进行最终的健全性检查,并作为向上游 Linus 发送(通过给他发送“请拉取”请求)的载体。

他还使用了一组临时分支(“主题分支”),每个分支都包含一组逻辑分组的补丁。

要进行此设置,首先通过克隆 Linus 的公共树来创建你的工作树

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git work
$ cd work

Linus 的树将存储在名为 origin/master 的远程跟踪分支中,可以使用 git-fetch[1] 进行更新;你可以使用 git-remote[1] 设置一个“远程”并使用 git-fetch[1] 保持其最新,来跟踪其他公共树;请参阅 仓库和分支

现在创建你将要工作的分支;这些分支从 origin/master 分支的当前尖端开始,并且应该被设置(使用 git-branch[1]--track 选项)以便默认从 Linus 合并更改。

$ git branch --track test origin/master
$ git branch --track release origin/master

这些可以使用 git-pull[1] 轻松保持最新。

$ git switch test && git pull
$ git switch release && git pull

重要提示!如果这些分支中有任何本地更改,则此合并将在历史中创建一个提交对象(如果没有本地更改,Git 将简单地执行“快进”合并)。许多人不喜欢这在 Linux 历史中产生的“噪音”,因此你应该避免在 release 分支中随意执行此操作,因为当你要求 Linus 从 release 分支拉取时,这些嘈杂的提交将成为永久历史的一部分。

一些配置变量(参见 git-config[1])可以使你轻松地将两个分支推送到你的公共树。(参见 设置公共仓库。)

$ cat >> .git/config <<EOF
[remote "mytree"]
	url =  master.kernel.org:/pub/scm/linux/kernel/git/aegl/linux.git
	push = release
	push = test
EOF

然后你可以使用 git-push[1] 推送测试和发布树

$ git push mytree

或者只推送测试和发布分支中的一个

$ git push mytree test

$ git push mytree release

现在应用社区的一些补丁。为保存此补丁(或相关补丁组)的分支想一个简洁有力的名称,并从 Linus 分支的一个最近的稳定标签创建一个新分支。为你的分支选择一个稳定的基础将:1) 帮助你:通过避免包含不相关或可能未经充分测试的更改 2) 帮助未来使用 git bisect 查找问题的错误猎手

$ git switch -c speed-up-spinlocks v2.6.35

现在你应用补丁,运行一些测试,并提交更改。如果补丁是多部分的系列,那么你应该将每个部分作为单独的提交应用到这个分支。

$ ... patch ... test  ... commit [ ... patch ... test ... commit ]*

当你对这项更改的状态满意时,你可以将其合并到“测试”分支中,以便使其公开

$ git switch test && git merge speed-up-spinlocks

你不太可能在这里遇到任何冲突……但如果你在这个步骤上花了一些时间并且也从上游拉取了新版本,那么你可能会遇到。

过了一段时间,当足够的时间过去并且测试完成时,你可以将相同的分支拉入 release 树,准备向上游提交。这就是将每个补丁(或补丁系列)保留在自己的分支中的价值所在。这意味着补丁可以按任何顺序移动到 release 树中。

$ git switch release && git merge speed-up-spinlocks

一段时间后,你会有许多分支,尽管你为它们选择了好听的名字,你可能会忘记它们是做什么的,或者它们处于什么状态。要获取特定分支中包含哪些更改的提醒,请使用

$ git log linux..branchname | git shortlog

要查看它是否已合并到测试或发布分支中,请使用

$ git log test..branchname

$ git log release..branchname

(如果此分支尚未合并,你将看到一些日志条目。如果它已合并,则不会有任何输出。)

一旦补丁完成大循环(从测试移至发布,然后被 Linus 拉取,最后回到你的本地 origin/master 分支),则不再需要此更改的分支。当以下命令的输出为空时,你将检测到此情况

$ git log origin..branchname

为空。此时可以删除分支

$ git branch -d branchname

有些更改非常微不足道,以至于没有必要创建一个单独的分支,然后合并到测试和发布分支中。对于这些更改,只需直接应用到 release 分支,然后将其合并到 test 分支。

将你的工作推送到 mytree 后,你可以使用 git-request-pull[1] 准备一个“请拉取”请求消息发送给 Linus

$ git push mytree
$ git request-pull origin mytree release

以下是一些可以进一步简化所有这些操作的脚本。

==== update script ====
# Update a branch in my Git tree.  If the branch to be updated
# is origin, then pull from kernel.org.  Otherwise merge
# origin/master branch into test|release branch

case "$1" in
test|release)
	git checkout $1 && git pull . origin
	;;
origin)
	before=$(git rev-parse refs/remotes/origin/master)
	git fetch origin
	after=$(git rev-parse refs/remotes/origin/master)
	if [ $before != $after ]
	then
		git log $before..$after | git shortlog
	fi
	;;
*)
	echo "usage: $0 origin|test|release" 1>&2
	exit 1
	;;
esac
==== merge script ====
# Merge a branch into either the test or release branch

pname=$0

usage()
{
	echo "usage: $pname branch test|release" 1>&2
	exit 1
}

git show-ref -q --verify -- refs/heads/"$1" || {
	echo "Can't see branch <$1>" 1>&2
	usage
}

case "$2" in
test|release)
	if [ $(git log $2..$1 | wc -c) -eq 0 ]
	then
		echo $1 already merged into $2 1>&2
		exit 1
	fi
	git checkout $2 && git pull . $1
	;;
*)
	usage
	;;
esac
==== status script ====
# report on status of my ia64 Git tree

gb=$(tput setab 2)
rb=$(tput setab 1)
restore=$(tput setab 9)

if [ `git rev-list test..release | wc -c` -gt 0 ]
then
	echo $rb Warning: commits in release that are not in test $restore
	git log test..release
fi

for branch in `git show-ref --heads | sed 's|^.*/||'`
do
	if [ $branch = test -o $branch = release ]
	then
		continue
	fi

	echo -n $gb ======= $branch ====== $restore " "
	status=
	for ref in test release origin/master
	do
		if [ `git rev-list $ref..$branch | wc -c` -gt 0 ]
		then
			status=$status${ref:0:1}
		fi
	done
	case $status in
	trl)
		echo $rb Need to pull into test $restore
		;;
	rl)
		echo "In test"
		;;
	l)
		echo "Waiting for linus"
		;;
	"")
		echo $rb All done $restore
		;;
	*)
		echo $rb "<$status>" $restore
		;;
	esac
	git log origin/master..$branch | git shortlog
done

重写历史和维护补丁系列

通常,提交只添加到项目中,从不删除或替换。Git 的设计基于此假设,违反此假设将导致 Git 的合并机制(例如)出错。

然而,在某些情况下,违反这个假设可能很有用。

创建完美的补丁系列

假设你是一个大型项目的贡献者,并且你想添加一个复杂的功能,并以一种让其他开发者容易阅读你的更改、验证其正确性并理解你为什么进行每项更改的方式来呈现它。

如果你将所有更改作为单个补丁(或提交)呈现,他们可能会发现一次消化太多。

如果你向他们展示你工作的完整历史,包括错误、更正和死胡同,他们可能会不知所措。

因此,理想情况通常是生成一系列补丁,使得

  1. 每个补丁都可以按顺序应用。

  2. 每个补丁都包含一个逻辑更改,以及解释该更改的消息。

  3. 没有补丁引入回归:在应用系列中的任何初始部分后,生成项目仍然可以编译和工作,并且没有以前没有的错误。

  4. 完整的系列产生的结果与你自己的(可能更混乱!)开发过程相同。

我们将介绍一些可以帮助你完成此操作的工具,解释如何使用它们,然后解释由于你正在重写历史可能出现的一些问题。

使用 git rebase 保持补丁系列最新

假设你在远程跟踪分支 origin 上创建了一个分支 mywork,并在其之上创建了一些提交

$ git switch -c mywork origin
$ vi file.txt
$ git commit
$ vi otherfile.txt
$ git commit
...

你尚未将任何内容合并到 mywork 中,因此它只是在 origin 之上的一系列简单线性补丁

 o--o--O <-- origin
        \
	 a--b--c <-- mywork

上游项目已经完成了一些更有趣的工作,并且 origin 已经向前推进

 o--o--O--o--o--o <-- origin
        \
         a--b--c <-- mywork

此时,你可以使用 pull 将你的更改合并回来;结果将创建一个新的合并提交,像这样

 o--o--O--o--o--o <-- origin
        \        \
         a--b--c--m <-- mywork

然而,如果你更喜欢将 mywork 中的历史保持为简单的提交系列而不包含任何合并,你可以选择使用 git-rebase[1]

$ git switch mywork
$ git rebase origin

这将从 mywork 中移除你的每个提交,暂时将它们保存为补丁(在一个名为 .git/rebase-apply 的目录中),更新 mywork 以指向 origin 的最新版本,然后将每个已保存的补丁应用到新的 mywork。结果将如下所示

 o--o--O--o--o--o <-- origin
		 \
		  a'--b'--c' <-- mywork

在此过程中,它可能会发现冲突。在这种情况下,它会停止并允许你解决冲突;解决冲突后,使用 git add 更新索引内容,然后,不要运行 git commit,只需运行

$ git rebase --continue

Git 将继续应用其余的补丁。

在任何时候,你都可以使用 --abort 选项来中止此过程并将 mywork 恢复到你开始变基之前的状态

$ git rebase --abort

如果你需要在分支中重新排序或编辑多个提交,使用 git rebase -i 可能会更容易,它允许你在变基期间重新排序和压缩提交,以及标记它们进行单独编辑。有关详细信息,请参阅 使用交互式变基,有关替代方案,请参阅 重新排序或从补丁系列中选择

重写单个提交

我们在 通过重写历史来修复错误 中看到,你可以使用以下命令替换最近的提交

$ git commit --amend

这会将旧提交替换为包含你的更改的新提交,让你有机会首先编辑旧的提交消息。这对于修复你上次提交中的拼写错误,或调整暂存不佳的提交的补丁内容非常有用。

如果你需要修改历史中更深层的提交,可以使用 交互式变基的 edit 指令

重新排序或从补丁系列中选择

有时你想编辑历史中更深层的提交。一种方法是使用 git format-patch 创建一系列补丁,然后将状态重置到补丁之前

$ git format-patch origin
$ git reset --hard origin

然后根据需要修改、重新排序或删除补丁,然后再使用 git-am[1] 再次应用它们

$ git am *.patch

使用交互式变基

你也可以使用交互式变基来编辑补丁系列。这与 使用 format-patch 重新排序补丁系列 相同,因此请使用你最喜欢的界面。

将你当前的 HEAD 变基到你想要原样保留的最后一个提交上。例如,如果你想重新排序最后 5 个提交,请使用

$ git rebase -i HEAD~5

这将在你的编辑器中打开一个列表,其中包含执行变基所需的步骤。

pick deadbee The oneline of this commit
pick fa1afe1 The oneline of the next commit
...

# Rebase c0ffeee..deadbee onto c0ffeee
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

如注释中所述,你可以通过编辑列表来重新排序提交、将它们压缩在一起、编辑提交消息等。一旦你满意,保存列表并关闭编辑器,变基将开始。

pick 被替换为 edit,或者列表中的某个步骤无法自动解决冲突并需要你帮助时,变基将停止。当你完成编辑和/或解决冲突后,你可以使用 git rebase --continue 继续。如果你觉得情况变得过于复杂,你始终可以使用 git rebase --abort 退出。即使变基完成后,你仍然可以通过使用 reflog 恢复原始分支。

有关此过程和更多技巧的详细讨论,请参阅 git-rebase[1] 的“交互模式”部分。

其他工具

还有许多其他工具,例如 StGit,它们旨在维护补丁系列。这些超出了本手册的范围。

重写历史的问题

重写分支历史的主要问题与合并有关。假设有人拉取了你的分支并将其合并到他们的分支中,结果大致如下

 o--o--O--o--o--o <-- origin
        \        \
         t--t--t--m <-- their branch:

然后假设你修改了最后三个提交

	 o--o--o <-- new head of origin
	/
 o--o--O--o--o--o <-- old head of origin

如果我们在一个仓库中一起检查所有这些历史,它将看起来像

	 o--o--o <-- new head of origin
	/
 o--o--O--o--o--o <-- old head of origin
        \        \
         t--t--t--m <-- their branch:

Git 无法知道新头是旧头的更新版本;它对待这种情况的方式与两个开发者并行独立地对旧头和新头进行工作时完全相同。此时,如果有人试图将新头合并到他们的分支中,Git 将尝试将两条(旧的和新的)开发线合并在一起,而不是尝试用新的替换旧的。结果很可能出乎意料。

你仍然可以选择发布其历史被重写的分支,并且其他人能够拉取这些分支以检查或测试它们可能很有用,但他们不应尝试将此类分支拉入他们自己的工作中。

对于支持正确合并的真正分布式开发,已发布的分支绝不应该被重写。

为什么二分查找合并提交比二分查找线性历史更难

git-bisect[1] 命令可以正确处理包含合并提交的历史。然而,当它找到的提交是合并提交时,用户可能需要比平时更努力地找出为什么该提交引入了问题。

想象一下这段历史

      ---Z---o---X---...---o---A---C---D
          \                       /
           o---o---Y---...---o---B

假设在上方开发线中,存在于 Z 的某个函数的含义在提交 X 处发生了改变。从 Z 到 A 的提交改变了函数的实现和所有存在于 Z 的调用点,以及它们添加的新调用点,以保持一致。在 A 处没有错误。

同时,假设在下方开发线中,某人在提交 Y 处为该函数添加了一个新的调用点。从 Z 到 B 的提交都假定该函数的旧语义,并且调用者和被调用者彼此一致。在 B 处也没有错误。

进一步假设两条开发线在 C 处干净地合并,因此不需要解决冲突。

然而,C 处的代码是损坏的,因为在下方开发线上添加的调用者尚未转换为上方开发线上引入的新语义。因此,如果你只知道 D 是坏的,Z 是好的,并且 git-bisect[1] 将 C 标识为罪魁祸首,你将如何找出问题是由于语义的这种改变?

git bisect 的结果是一个非合并提交时,你通常应该能够通过只检查该提交来发现问题。开发者可以通过将他们的更改分解为小的独立提交来简化此过程。然而,在上述情况下这无济于事,因为问题从任何单个提交的检查中都不明显;相反,需要对开发有一个全局的视图。更糟糕的是,有问题的函数中的语义更改可能只是上方开发线中更改的一小部分。

另一方面,如果在 C 处不是合并,而是将 Z 到 B 之间的历史变基到 A 的顶部,你将得到这个线性历史

    ---Z---o---X--...---o---A---o---o---Y*--...---o---B*--D*

在 Z 和 D* 之间进行二分查找将命中单个罪魁祸首提交 Y*,并且理解 Y* 为什么损坏可能会更容易。

部分出于这个原因,许多经验丰富的 Git 用户,即使在进行一个大量使用合并的项目时,也会在发布前通过对最新上游版本进行变基来保持历史的线性。

高级分支管理

抓取单个分支

除了使用 git-remote[1],你还可以选择一次只更新一个分支,并将其存储在本地的任意名称下

$ git fetch origin todo:my-todo-work

第一个参数 origin 只是告诉 Git 从你最初克隆的仓库中抓取。第二个参数告诉 Git 从远程仓库抓取名为 todo 的分支,并将其存储在本地名为 refs/heads/my-todo-work 的位置。

你也可以从其他仓库抓取分支;因此

$ git fetch git://example.com/proj.git master:example-master

将创建一个名为 example-master 的新分支,并在其中存储来自给定 URL 仓库中名为 master 的分支。如果你已经有一个名为 example-master 的分支,它将尝试 快进 到 example.com 的 master 分支所指定的提交。更详细地说

git fetch 和快进

在前面的示例中,当更新现有分支时,git fetch 会检查以确保远程分支上的最新提交是你本地分支副本上最新提交的后代,然后才将你的本地分支副本更新为指向新提交。Git 将此过程称为 快进

快进看起来像这样

 o--o--o--o <-- old head of the branch
           \
            o--o--o <-- new head of the branch

在某些情况下,新头可能实际上不是旧头的后代。例如,开发者可能意识到犯了一个严重错误并决定回溯,导致出现以下情况

 o--o--o--o--a--b <-- old head of the branch
           \
            o--o--o <-- new head of the branch

在这种情况下,git fetch 将失败,并打印出警告。

在这种情况下,你仍然可以强制 Git 更新到新头,如下一节所述。然而,请注意,在上述情况下,这可能意味着丢失标记为 ab 的提交,除非你已经创建了指向它们的自己的引用。

强制 git fetch 执行非快进更新

如果 git fetch 失败,因为分支的新头不是旧头的后代,你可以使用以下命令强制更新

$ git fetch git://example.com/proj.git +master:refs/remotes/example/master

请注意 + 符号的添加。或者,你可以使用 -f 标志来强制更新所有抓取的分支,如下所示

$ git fetch -f origin

请注意,旧版本 example/master 指向的提交可能会丢失,正如我们在上一节中看到的。

配置远程跟踪分支

我们上面看到,origin 只是一个快捷方式,用于指代你最初克隆的仓库。此信息存储在 Git 配置变量中,你可以使用 git-config[1] 查看

$ git config -l
core.repositoryformatversion=0
core.filemode=true
core.logallrefupdates=true
remote.origin.url=git://git.kernel.org/pub/scm/git/git.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master

如果你还有其他经常使用的仓库,可以创建类似的配置选项以节省输入;例如,

$ git remote add example git://example.com/proj.git

将以下内容添加到 .git/config

[remote "example"]
	url = git://example.com/proj.git
	fetch = +refs/heads/*:refs/remotes/example/*

另请注意,上述配置可以通过直接编辑文件 .git/config 而不是使用 git-remote[1] 来执行。

配置远程后,以下三个命令将执行相同的操作

$ git fetch git://example.com/proj.git +refs/heads/*:refs/remotes/example/*
$ git fetch example +refs/heads/*:refs/remotes/example/*
$ git fetch example

有关上述配置选项的更多详细信息,请参阅 git-config[1];有关 refspec 语法的更多详细信息,请参阅 git-fetch[1]

Git 概念

Git 建立在少量简单而强大的思想之上。虽然不理解它们也能完成工作,但如果你理解它们,你会发现 Git 更直观。

我们从最重要的 对象数据库(object database)索引(index) 开始。

对象数据库

我们已经在 理解历史:提交 中看到,所有提交都存储在一个 40 位的“对象名称”下。实际上,表示项目历史所需的所有信息都存储在具有此类名称的对象中。在每种情况下,名称都是通过对对象内容进行 SHA-1 哈希计算得出的。SHA-1 哈希是一种加密哈希函数。这对我们来说意味着不可能找到两个具有相同名称的不同对象。这有许多优点,其中包括

  • Git 可以通过比较名称来快速确定两个对象是否相同。

  • 由于对象名称在每个仓库中都以相同的方式计算,因此存储在两个仓库中的相同内容将始终以相同的名称存储。

  • Git 在读取对象时可以检测到错误,通过检查对象的名称是否仍然是其内容的 SHA-1 哈希。

(有关对象格式和 SHA-1 计算的详细信息,请参阅 对象存储格式。)

有四种不同类型的对象:“blob”、“tree”、“commit”和“tag”。

  • “blob”对象 用于存储文件数据。

  • “tree”对象 将一个或多个“blob”对象绑定到目录结构中。此外,tree 对象可以引用其他 tree 对象,从而创建目录层次结构。

  • “commit”对象 将此类目录层次结构绑定到修订的 有向无环图(directed acyclic graph) 中——每个提交都包含一个 tree 对象的名称,该 tree 对象指定了提交时的目录层次结构。此外,一个提交引用“父”提交对象,这些对象描述了我们如何到达该目录层次结构的历史。

  • “tag”对象 象征性地标识并可用于签署其他对象。它包含另一个对象的对象名称和类型、一个符号名称(当然!)以及可选的签名。

对象类型的一些更详细信息

提交对象

“commit”对象将 tree 的物理状态与我们如何到达那里以及为什么到达那里的描述联系起来。使用 git-show[1]git-log[1]--pretty=raw 选项来检查你最喜欢的提交

$ git show -s --pretty=raw 2be7fcb476
commit 2be7fcb4764f2dbcee52635b91fedb1b3dcf7ab4
tree fb3a8bdd0ceddd019615af4d57a53f43d8cee2bf
parent 257a84d9d02e90447b149af58b271c19405edb6a
author Dave Watson <dwatson@mimvista.com> 1187576872 -0400
committer Junio C Hamano <gitster@pobox.com> 1187591163 -0700

    Fix misspelling of 'suppress' in docs

    Signed-off-by: Junio C Hamano <gitster@pobox.com>

如你所见,一个提交由以下内容定义

  • 一个 tree:一个 tree 对象的 SHA-1 名称(如下所述),表示某个时间点目录的内容。

  • 父提交:代表项目历史中紧前步骤的一个或多个提交的 SHA-1 名称。上面的示例有一个父提交;合并提交可能有一个以上。没有父提交的提交称为“根”提交,代表项目的初始修订。每个项目必须至少有一个根。一个项目也可以有多个根,尽管这不常见(或不一定是好主意)。

  • 作者:负责此更改的人的姓名,以及日期。

  • 提交者:实际创建提交的人的姓名,以及完成日期。这可能与作者不同,例如,如果作者是撰写补丁并将其通过电子邮件发送给使用它创建提交的人。

  • 描述此提交的注释。

请注意,提交本身不包含任何有关实际更改的信息;所有更改都是通过比较此提交引用的 tree 内容与其父提交关联的 tree 来计算的。特别是,Git 不会尝试显式记录文件重命名,尽管它可以识别在更改路径上存在相同文件数据时暗示重命名的情况。(例如,请参阅 git-diff[1]-M 选项)。

一个提交通常由 git-commit[1] 创建,该命令创建一个提交,其父级通常是当前的 HEAD,其树则取自当前存储在索引中的内容。

树对象(Tree Object)

多功能的 git-show[1] 命令也可用于检查树对象,但 git-ls-tree[1] 会提供更多细节。

$ git ls-tree fb3a8bdd0ce
100644 blob 63c918c667fa005ff12ad89437f2fdc80926e21c    .gitignore
100644 blob 5529b198e8d14decbe4ad99db3f7fb632de0439d    .mailmap
100644 blob 6ff87c4664981e4397625791c8ea3bbb5f2279a3    COPYING
040000 tree 2fb783e477100ce076f6bf57e4a6f026013dc745    Documentation
100755 blob 3c0032cec592a765692234f1cba47dfdcc3a9200    GIT-VERSION-GEN
100644 blob 289b046a443c0647624607d471289b2c7dcd470b    INSTALL
100644 blob 4eb463797adc693dc168b926b6932ff53f17d0b1    Makefile
100644 blob 548142c327a6790ff8821d67c2ee1eff7a656b52    README
...

如你所见,一个树对象包含一个条目列表,每个条目都有模式、对象类型、SHA-1 名称和名称,并按名称排序。它代表了单个目录树的内容。

对象类型可以是 blob,代表文件的内容,或者另一个树,代表子目录的内容。由于树和 blob,像所有其他对象一样,都通过其内容的 SHA-1 哈希值命名,因此两个树的 SHA-1 名称相同,当且仅当它们的内容(包括递归地,所有子目录的内容)完全相同。这使得 Git 可以快速确定两个相关树对象之间的差异,因为它会忽略任何具有相同对象名称的条目。

(注意:在存在子模块的情况下,树也可以将提交作为条目。有关文档,请参见 子模块。)

请注意,所有文件的模式都是 644 或 755:Git 实际上只关注可执行位。

Blob 对象

你可以使用 git-show[1] 来检查 blob 的内容;例如,从上面的树中,COPYING 条目中的 blob:

$ git show 6ff87c4664

 Note that the only valid version of the GPL as far as this project
 is concerned is _this_ particular version of the license (ie v2, not
 v2.2 or v3.x or whatever), unless explicitly otherwise stated.
...

“blob”对象不过是数据的二进制大块。它不引用任何其他内容,也没有任何属性。

由于 blob 完全由其数据定义,如果目录树中(或存储库的多个不同版本中)的两个文件具有相同的内容,它们将共享同一个 blob 对象。该对象完全独立于其在目录树中的位置,重命名文件不会改变该文件所关联的对象。

请注意,任何树或 blob 对象都可以使用 git-show[1] 配合 <revision>:<path> 语法进行检查。这有时对于浏览当前未检出的树内容很有用。

信任

如果你从一个来源收到一个 blob 的 SHA-1 名称,并从另一个(可能不受信任的)来源收到其内容,只要 SHA-1 名称一致,你仍然可以相信这些内容是正确的。这是因为 SHA-1 的设计使得找到产生相同哈希值的不同内容是不可行的。

类似地,你只需要信任一个顶层树对象的 SHA-1 名称,就可以信任它所指向的整个目录的内容;如果你从一个受信任的来源收到一个提交的 SHA-1 名称,那么你可以轻松验证通过该提交的父级可达的整个提交历史,以及这些提交所引用的所有树的内容。

因此,为了在系统中引入一些真正的信任,你唯一需要做的就是数字签名一份特殊的笔记,其中包含一个顶层提交的名称。你的数字签名向他人表明你信任该提交,而提交历史的不可变性则告诉他人他们可以信任整个历史。

换句话说,你只需发送一封电子邮件,告诉人们顶层提交的名称(SHA-1 哈希),然后使用 GPG/PGP 等工具对该电子邮件进行数字签名,就可以轻松验证整个存档。

为了协助这一点,Git 还提供了标签对象……

标签对象(Tag Object)

一个标签对象包含一个对象、对象类型、标签名称、创建标签的人(“tagger”)的名称以及一条消息,其中可能包含签名,如使用 git-cat-file[1] 所见:

$ git cat-file tag v1.5.0
object 437b1b20df4b356c9342dac8d38849f24ef44f27
type commit
tag v1.5.0
tagger Junio C Hamano <junkio@cox.net> 1171411200 +0000

GIT 1.5.0
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.6 (GNU/Linux)

iD8DBQBF0lGqwMbZpPMRm5oRAuRiAJ9ohBLd7s2kqjkKlq1qqC57SbnmzQCdG4ui
nLE/L9aUXdWeTFPron96DLA=
=2E+0
-----END PGP SIGNATURE-----

请参阅 git-tag[1] 命令,了解如何创建和验证标签对象。(请注意,git-tag[1] 也可以用于创建“轻量级标签”,它们根本不是标签对象,而只是名称以 refs/tags/ 开头的简单引用)。

Git 如何高效存储对象:pack 文件

新创建的对象最初以对象 SHA-1 哈希命名(存储在 .git/objects 中)的文件形式创建。

不幸的是,一旦项目拥有大量对象,这个系统就会变得效率低下。在一个旧项目上试试这个:

$ git count-objects
6930 objects, 47620 kilobytes

第一个数字是保存在单个文件中的对象数量。第二个是这些“松散”对象占用的空间大小。

你可以通过将这些松散对象移动到“pack 文件”中来节省空间并加快 Git 速度,pack 文件以高效压缩格式存储一组对象;pack 文件的格式细节可以在 gitformat-pack[5] 中找到。

要将松散对象放入 pack 文件,只需运行 git repack:

$ git repack
Counting objects: 6020, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6020/6020), done.
Writing objects: 100% (6020/6020), done.
Total 6020 (delta 4070), reused 0 (delta 0)

这会在 .git/objects/pack/ 中创建一个单独的“pack 文件”,其中包含所有当前未打包的对象。然后你可以运行:

$ git prune

来移除所有现在包含在 pack 文件中的“松散”对象。这还会移除任何未引用的对象(例如,当你使用 git reset 移除提交时可能创建的对象)。你可以通过查看 .git/objects 目录或运行以下命令来验证松散对象是否已消失:

$ git count-objects
0 objects, 0 kilobytes

尽管对象文件已经消失,但引用这些对象的任何命令都将像以前一样正常工作。

git-gc[1] 命令为你执行打包、修剪等操作,因此通常是你唯一需要的高级命令。

悬空对象

git-fsck[1] 命令有时会抱怨悬空对象。它们不是问题。

悬空对象最常见的原因是你重新基于了一个分支,或者你从其他重新基于分支的人那里拉取了代码——请参阅 重写历史和维护补丁系列。在这种情况下,原始分支的旧 HEAD 仍然存在,它所指向的一切也仍然存在。只是分支指针本身不再存在了,因为你用另一个替换了它。

还有其他情况会导致悬空对象。例如,“悬空 blob”可能是因为你 git add 了一个文件,但在你实际提交并使其成为大局的一部分之前,你更改了该文件中的其他内容并提交了那个**更新的**内容——你最初添加的旧状态最终没有被任何提交或树指向,所以它现在是一个悬空 blob 对象。

类似地,当“ort”合并策略运行时,如果发现存在交叉合并,从而有多个合并基础(这相当不寻常,但确实会发生),它将生成一个临时的中间树(如果有很多交叉合并并且超过两个合并基础,甚至可能更多)作为临时的内部合并基础,同样,这些都是真实的对象,但最终结果不会指向它们,因此它们最终会“悬空”在你的仓库中。

通常,悬空对象没什么好担心的。它们甚至可能非常有用:如果你搞砸了什么,悬空对象可以是你恢复旧树的方式(比如说,你执行了 rebase,然后意识到你真的不想那样做——你可以查看你有哪些悬空对象,并决定将你的 HEAD 重置到某个旧的悬空状态)。

对于提交,你可以直接使用:

$ gitk <dangling-commit-sha-goes-here> --not --all

这会要求显示所有可从给定提交到达,但不可从任何分支、标签或其他引用到达的历史记录。如果你认为它是你想要的东西,你总是可以为它创建一个新的引用,例如:

$ git branch recovered-branch <dangling-commit-sha-goes-here>

对于 blob 和树,你不能做同样的事情,但你仍然可以检查它们。你可以直接运行:

$ git show <dangling-blob/tree-sha-goes-here>

来显示 blob 的内容(或者,对于树,基本上是该目录的 ls 的内容),这可能会给你一些关于留下该悬空对象的操作的线索。

通常,悬空 blob 和树并不是很有趣。它们几乎总是因为是半途而废的合并基础(如果你的合并冲突是手动解决的,blob 甚至常常包含合并标记),或者仅仅是因为你用 Ctrl+C 之类的操作中断了 git fetch,导致一些新对象留在对象数据库中,但只是悬空且无用。

总之,一旦你确定你对任何悬空状态不感兴趣,你就可以修剪所有不可达的对象:

$ git prune

它们就会消失。(你只应在静止的仓库上运行 git prune——这有点像执行文件系统 fsck 恢复:你不想在文件系统挂载时这样做。git prune 的设计是为了在同时访问仓库的情况下不造成任何伤害,但你可能会收到令人困惑或可怕的消息。)

从仓库损坏中恢复

根据设计,Git 谨慎对待被信任的数据。然而,即使 Git 本身没有 bug,硬件或操作系统错误仍然可能导致数据损坏。

解决此类问题的首要防御是备份。你可以使用克隆(clone),或者仅仅使用 cp、tar 或任何其他备份机制来备份 Git 目录。

作为最后手段,你可以搜索损坏的对象并尝试手动替换它们。在尝试此操作之前,请备份你的仓库,以防在此过程中造成更大的损坏。

我们将假设问题是单个缺失或损坏的 blob,这有时是一个可解决的问题。(恢复缺失的树,尤其是提交,要**困难得多**)。

开始之前,请验证是否存在损坏,并使用 git-fsck[1] 找出损坏的位置;这可能很耗时。

假设输出如下所示:

$ git fsck --full --no-dangling
broken link from    tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
              to    blob 4b9458b3786228369c63936db65827de3cc06200
missing blob 4b9458b3786228369c63936db65827de3cc06200

现在你知道 blob 4b9458b3 缺失,并且树 2d9263c6 指向它。如果你能找到一个该缺失 blob 对象的副本,可能在其他仓库中,你可以将其移动到 .git/objects/4b/9458b3... 中就完成了。假设你找不到。你仍然可以使用 git-ls-tree[1] 检查指向它的树,它可能会输出类似以下内容:

$ git ls-tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
100644 blob 8d14531846b95bfa3564b58ccfb7913a034323b8	.gitignore
100644 blob ebf9bf84da0aab5ed944264a5db2a65fe3a3e883	.mailmap
100644 blob ca442d313d86dc67e0a2e5d584b465bd382cbf5c	COPYING
...
100644 blob 4b9458b3786228369c63936db65827de3cc06200	myfile
...

所以现在你知道缺失的 blob 是名为 myfile 的文件的数据。而且你很有可能也能识别出目录——假设它在 somedirectory 中。如果你幸运的话,缺失的副本可能与你在工作树中检出的 somedirectory/myfile 处的副本相同;你可以使用 git-hash-object[1] 测试是否正确:

$ git hash-object -w somedirectory/myfile

它将创建并存储一个包含 somedirectory/myfile 内容的 blob 对象,并输出该对象的 SHA-1 值。如果你非常幸运,它可能是 4b9458b3786228369c63936db65827de3cc06200,在这种情况下,你猜对了,损坏已修复!

否则,你需要更多信息。你如何判断哪个版本的文件丢失了?

最简单的方法是使用:

$ git log --raw --all --full-history -- somedirectory/myfile

因为你要求原始输出,所以你现在会得到类似以下内容:

commit abc
Author:
Date:
...
:100644 100644 4b9458b newsha M somedirectory/myfile


commit xyz
Author:
Date:

...
:100644 100644 oldsha 4b9458b M somedirectory/myfile

这告诉你文件的紧随其后的版本是“newsha”,而紧随其前的版本是“oldsha”。你还知道从 oldsha 变为 4b9458b 以及从 4b9458b 变为 newsha 的变更所附带的提交消息。

如果你一直提交足够小的更改,你现在很可能能够重建中间状态 4b9458b 的内容。

如果你能做到这一点,你现在可以创建缺失的对象:

$ git hash-object -w <recreated-file>

你的仓库就又恢复正常了!

(顺便说一句,你可以忽略 fsck,直接开始做:

$ git log --raw --all

然后只需在该整个内容中查找缺失对象的 sha (4b9458b)。这取决于你——Git 确实**拥有**大量信息,只是缺少一个特定的 blob 版本。

索引(The Index)

索引是一个二进制文件(通常保存在 .git/index 中),包含一个已排序的路径名列表,每个路径名都带有权限和一个 blob 对象的 SHA-1;git-ls-files[1] 可以显示索引的内容:

$ git ls-files --stage
100644 63c918c667fa005ff12ad89437f2fdc80926e21c 0	.gitignore
100644 5529b198e8d14decbe4ad99db3f7fb632de0439d 0	.mailmap
100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0	COPYING
100644 a37b2152bd26be2c2289e1f57a292534a51a93c7 0	Documentation/.gitignore
100644 fbefe9a45b00a54b58d94d06eca48b03d40a50e0 0	Documentation/Makefile
...
100644 2511aef8d89ab52be5ec6a5e46236b4b6bcd07ea 0	xdiff/xtypes.h
100644 2ade97b2574a9f77e7ae4002a4e07a6a38e46d07 0	xdiff/xutils.c
100644 d5de8292e05e7c36c4b68857c1cf9855e3d2f70a 0	xdiff/xutils.h

请注意,在旧的文档中,你可能会看到索引被称为“当前目录缓存”或简称“缓存”。它有三个重要的特性:

  1. 索引包含生成单个(唯一确定的)树对象所需的所有信息。

    例如,运行 git-commit[1] 会从索引生成这个树对象,将其存储在对象数据库中,并将其用作与新提交关联的树对象。

  2. 索引能够快速比较它定义的树对象和工作树。

    它通过为每个条目存储一些额外数据(例如最后修改时间)来实现这一点。这些数据在上面没有显示,也没有存储在创建的树对象中,但它可以用来快速确定工作目录中的哪些文件与索引中存储的内容不同,从而避免 Git 必须读取这些文件的所有数据来查找更改。

  3. 它能够有效地表示不同树对象之间的合并冲突信息,允许每个路径名与关于所涉树的足够信息关联,以便你可以在它们之间创建三向合并。

    我们在 合并期间获取冲突解决帮助 中看到,在合并期间,索引可以存储单个文件的多个版本(称为“阶段”)。上面 git-ls-files[1] 输出中的第三列是阶段号,对于有合并冲突的文件,其值将非 0。

因此,索引是一种临时暂存区,其中填充了你正在处理的树。

如果你完全清除索引,只要你拥有它所描述的树的名称,你通常就不会丢失任何信息。

子模块

大型项目通常由更小、独立的模块组成。例如,一个嵌入式 Linux 发行版源代码树会包含发行版中的每个软件组件及其一些本地修改;一个电影播放器可能需要针对特定、已知可用的解压缩库版本进行构建;几个独立的程序可能共享相同的构建脚本。

使用集中式版本控制系统,这通常通过将每个模块包含在一个单一仓库中来实现。开发人员可以检出所有模块或只检出他们需要处理的模块。他们甚至可以在一个提交中修改多个模块的文件,同时移动事物或更新 API 和翻译。

Git 不允许部分检出,因此在 Git 中复制这种方法将迫使开发人员保留他们不感兴趣触及的模块的本地副本。在巨大的检出中进行提交会比你预期的慢,因为 Git 必须扫描每个目录以查找更改。如果模块有大量本地历史记录,克隆将耗费很长时间。

从好的方面看,分布式版本控制系统可以更好地与外部源集成。在集中式模型中,外部项目的单一任意快照从其自己的版本控制中导出,然后导入到本地版本控制的供应商分支中。所有历史记录都被隐藏了。通过分布式版本控制,你可以克隆整个外部历史记录,并更容易地跟踪开发和重新合并本地更改。

Git 的子模块支持允许仓库包含作为子目录的外部项目的检出。子模块保持其自身的身份;子模块支持仅存储子模块仓库位置和提交 ID,因此克隆包含项目(“superproject”)的其他开发人员可以轻松地克隆所有子模块的相同版本。可以对超级项目进行部分检出:你可以告诉 Git 不克隆、克隆部分或克隆所有子模块。

从 Git 1.5.3 开始,git-submodule[1] 命令可用。Git 1.5.2 的用户可以在仓库中查找子模块提交并手动检出;更早的版本根本无法识别子模块。

要了解子模块支持如何工作,请创建四个示例仓库,它们稍后可以用作子模块:

$ mkdir ~/git
$ cd ~/git
$ for i in a b c d
do
	mkdir $i
	cd $i
	git init
	echo "module $i" > $i.txt
	git add $i.txt
	git commit -m "Initial commit, submodule $i"
	cd ..
done

现在创建超级项目并添加所有子模块:

$ mkdir super
$ cd super
$ git init
$ for i in a b c d
do
	git submodule add ~/git/$i $i
done
注意
如果你打算发布你的超级项目,请不要在此处使用本地 URL!

查看 git submodule 创建了哪些文件:

$ ls -a
.  ..  .git  .gitmodules  a  b  c  d

命令 git submodule add <repo> <path> 执行以下几项操作:

  • 它将子模块从 <repo> 克隆到当前目录下的指定 <path>,并默认检出 master 分支。

  • 它将子模块的克隆路径添加到 gitmodules[5] 文件中,并将此文件添加到索引中,准备提交。

  • 它将子模块的当前提交 ID 添加到索引中,准备提交。

提交超级项目:

$ git commit -m "Add submodules a, b, c and d."

现在克隆超级项目:

$ cd ..
$ git clone super cloned
$ cd cloned

子模块目录在那里,但它们是空的:

$ ls -a a
.  ..
$ git submodule status
-d266b9873ad50488163457f025db7cdd9683d88b a
-e81d457da15309b4fef4249aba9b50187999670d b
-c1536a972b9affea0f16e0680ba87332dc059146 c
-d96249ff5d57de5de093e6baff9e0aafa5276a74 d
注意
上面显示的提交对象名称对你来说会有所不同,但它们应该与你的仓库的 HEAD 提交对象名称匹配。你可以通过运行 git ls-remote ../a 来检查。

拉取子模块是一个两步过程。首先运行 git submodule init 将子模块仓库 URL 添加到 .git/config

$ git submodule init

现在使用 git submodule update 克隆仓库并检出超级项目中指定的提交:

$ git submodule update
$ cd a
$ ls -a
.  ..  .git  a.txt

git submodule updategit submodule add 之间的一个主要区别是,git submodule update 检出的是一个特定的提交,而不是分支的尖端。它就像检出一个标签:HEAD 是分离的,所以你不在分支上工作。

$ git branch
* (detached from d266b98)
  master

如果你想在子模块中进行更改,并且你的 HEAD 是分离的,那么你应该创建或检出一个分支,进行更改,在子模块内发布更改,然后更新超级项目以引用新的提交:

$ git switch master

$ git switch -c fix-up

然后:

$ echo "adding a line again" >> a.txt
$ git commit -a -m "Updated the submodule from within the superproject."
$ git push
$ cd ..
$ git diff
diff --git a/a b/a
index d266b98..261dfac 160000
--- a/a
+++ b/a
@@ -1 +1 @@
-Subproject commit d266b9873ad50488163457f025db7cdd9683d88b
+Subproject commit 261dfac35cb99d380eb966e102c1197139f7fa24
$ git add a
$ git commit -m "Updated submodule a."
$ git push

如果你也想更新子模块,那么在 git pull 之后你必须运行 git submodule update

子模块的陷阱

在发布引用子模块的超级项目的更改之前,请务必先发布子模块的更改。如果你忘记发布子模块的更改,其他人将无法克隆仓库:

$ cd ~/git/super/a
$ echo i added another line to this file >> a.txt
$ git commit -a -m "doing it wrong this time"
$ cd ..
$ git add a
$ git commit -m "Updated submodule a again."
$ git push
$ cd ~/git/cloned
$ git pull
$ git submodule update
error: pathspec '261dfac35cb99d380eb966e102c1197139f7fa24' did not match any file(s) known to git.
Did you forget to 'git add'?
Unable to checkout '261dfac35cb99d380eb966e102c1197139f7fa24' in submodule path 'a'

在较旧的 Git 版本中,很容易忘记在子模块中提交新增或修改的文件,这会悄无声息地导致与不推送子模块更改类似的问题。从 Git 1.7.0 开始,超级项目中的 git statusgit diff 都会在子模块包含新增或修改文件时将其显示为已修改,以防止意外提交这种状态。git diff 在生成补丁输出或与 --submodule 选项一起使用时,也会在工作树一侧添加 -dirty

$ git diff
diff --git a/sub b/sub
--- a/sub
+++ b/sub
@@ -1 +1 @@
-Subproject commit 3f356705649b5d566d97ff843cf193359229a453
+Subproject commit 3f356705649b5d566d97ff843cf193359229a453-dirty
$ git diff --submodule
Submodule sub 3f35670..3f35670-dirty:

你也不应该将子模块中的分支回溯到超出任何超级项目曾经记录过的提交之外。

如果在未检出分支的情况下在子模块中进行并提交了更改,那么运行 git submodule update 是不安全的。它们将被静默覆盖:

$ cat a.txt
module a
$ echo line added from private2 >> a.txt
$ git commit -a -m "line added inside private2"
$ cd ..
$ git submodule update
Submodule path 'a': checked out 'd266b9873ad50488163457f025db7cdd9683d88b'
$ cd a
$ cat a.txt
module a
注意
这些更改在子模块的 reflog 中仍然可见。

如果你的子模块工作树中有未提交的更改,git submodule update 将不会覆盖它们。相反,你会收到通常的关于无法从脏分支切换的警告。

低层 Git 操作

许多高级命令最初都是作为 shell 脚本实现的,它们使用少量核心的低层 Git 命令。这些在执行不寻常的 Git 操作时仍然很有用,或者只是作为理解其内部工作原理的一种方式。

对象访问和操作

git-cat-file[1] 命令可以显示任何对象的内容,尽管高级的 git-show[1] 通常更有用。

git-commit-tree[1] 命令允许构建具有任意父级和树的提交。

可以使用 git-write-tree[1] 创建树,并可以通过 git-ls-tree[1] 访问其数据。两个树可以使用 git-diff-tree[1] 进行比较。

标签使用 git-mktag[1] 创建,签名可以使用 git-verify-tag[1] 验证,尽管通常使用 git-tag[1] 更简单。

工作流程

高级操作,例如 git-commit[1]git-restore[1],通过在工作树、索引和对象数据库之间移动数据来工作。Git 提供了单独执行这些步骤的低层操作。

通常,所有 Git 操作都在索引文件上进行。有些操作**纯粹**在索引文件上工作(显示索引的当前状态),但大多数操作在索引文件与数据库或工作目录之间移动数据。因此,有四种主要组合:

工作目录 → 索引

git-update-index[1] 命令用工作目录中的信息更新索引。你通常通过指定要更新的文件名来更新索引信息,如下所示:

$ git update-index filename

但是为了避免文件名通配符等常见错误,该命令通常不会添加全新的条目或删除旧的条目,即它通常只会更新现有的缓存条目。

要告诉 Git,你确实意识到某些文件不再存在,或者应该添加新文件,你应该分别使用 --remove--add 标志。

注意!--remove 标志**不**意味着后续文件名必然会被删除:如果文件仍然存在于你的目录结构中,索引将根据它们的新状态进行更新,而不是删除。唯一 --remove 的意思是 update-index 将把已删除的文件视为一个有效的东西,如果文件确实不再存在,它将相应地更新索引。

作为一个特例,你还可以执行 git update-index --refresh,它将刷新每个索引的“stat”信息以匹配当前的 stat 信息。它**不会**更新对象状态本身,它只会更新用于快速测试对象是否仍与其旧的后端存储对象匹配的字段。

前面介绍的 git-add[1] 只是 git-update-index[1] 的一个封装。

索引 → 对象数据库

你使用程序将当前的索引文件写入一个“树”对象:

$ git write-tree

它没有任何选项——它只会将当前索引写入描述该状态的树对象集合中,并返回结果顶层树的名称。你可以随时使用该树通过反向操作重新生成索引:

对象数据库 → 索引

你从对象数据库中读取一个“树”文件,并用它来填充(并覆盖——如果你的索引包含任何你可能想以后恢复的未保存状态,请不要这样做!)你当前的索引。正常操作就是:

$ git read-tree <SHA-1 of tree>

你的索引文件现在将与你之前保存的树等效。然而,那只是你的*索引*文件:你的工作目录内容尚未被修改。

索引 → 工作目录

你通过“检出”文件来从索引更新你的工作目录。这不是一个非常常见的操作,因为通常你会保持文件更新,而不是写入你的工作目录,你会告诉索引文件关于你的工作目录中的更改(即 git update-index)。

然而,如果你决定跳转到一个新版本,或者检出别人的版本,或者只是恢复一个旧的树,你会用 read-tree 填充你的索引文件,然后你需要用以下命令检出结果:

$ git checkout-index filename

或者,如果你想检出所有索引,使用 -a

注意!git checkout-index 通常拒绝覆盖旧文件,所以如果你已经检出了一个旧版本的树,你需要使用 -f 标志(在 -a 标志或文件名**之前**)来**强制**检出。

最后,还有一些零散的东西不纯粹是表示形式之间的转换:

将所有连接起来

要提交一个你用 git write-tree 实例化的树,你需要创建一个引用该树和其背后历史的“提交”对象——最值得注意的是历史中在其之前的“父”提交。

通常,“提交”只有一个父级:特定更改发生之前树的先前状态。然而,有时它可以有两个或更多父提交,在这种情况下我们称之为“合并”,因为这样的提交将两个或更多由其他提交代表的先前状态聚合(“合并”)在一起。

换句话说,虽然“树”代表工作目录的特定目录状态,“提交”则代表该状态在时间上的存在,并解释了我们是如何达到那里的。

你通过给出描述提交时状态的树和父级列表来创建提交对象:

$ git commit-tree <tree> -p <parent> [(-p <parent2>)...]

然后通过 stdin(通过管道或文件重定向,或者直接在 tty 中输入)给出提交的原因。

git commit-tree 将返回代表该提交的对象的名称,你应该将其保存以备后用。通常,你会提交一个新的 HEAD 状态,虽然 Git 不关心你将该状态的记录保存在何处,但在实践中,我们倾向于直接将结果写入 .git/HEAD 指向的文件中,这样我们就可以随时查看上次提交的状态是什么。

这是一张图片,它说明了各个部分是如何协同工作的:

                     commit-tree
                      commit obj
                       +----+
                       |    |
                       |    |
                       V    V
                    +-----------+
                    | Object DB |
                    |  Backing  |
                    |   Store   |
                    +-----------+
                       ^
           write-tree  |     |
             tree obj  |     |
                       |     |  read-tree
                       |     |  tree obj
                             V
                    +-----------+
                    |   Index   |
                    |  "cache"  |
                    +-----------+
         update-index  ^
             blob obj  |     |
                       |     |
    checkout-index -u  |     |  checkout-index
             stat      |     |  blob obj
                             V
                    +-----------+
                    |  Working  |
                    | Directory |
                    +-----------+

检查数据

你可以使用各种辅助工具检查对象数据库和索引中表示的数据。对于每个对象,你可以使用 git-cat-file[1] 来检查对象的详细信息:

$ git cat-file -t <objectname>

显示对象的类型,一旦你知道类型(通常在你找到对象的位置隐含),你可以使用:

$ git cat-file blob|tree|commit|tag <objectname>

来显示其内容。注意!树具有二进制内容,因此有一个特殊的辅助工具用于显示该内容,称为 git ls-tree,它将二进制内容转换为更易于阅读的形式。

查看“提交”对象特别有启发性,因为它们往往很小并且相当自解释。特别是,如果你遵循在 .git/HEAD 中放置顶层提交名称的约定,你可以这样做:

$ git cat-file commit HEAD

来查看顶层提交是什么。

合并多棵树

Git 可以帮助你执行三向合并,通过多次重复合并过程,这又可以用于多向合并。常见的情况是你只进行一次三向合并(协调两条历史线)并提交结果,但如果你愿意,你可以一次性合并多个分支。

要执行三向合并,你需要从你想要合并的两个提交开始,找到它们最近的共同父级(第三个提交),然后比较与这三个提交对应的树。

要获取合并的“基础”,请查找两个提交的共同父级:

$ git merge-base <commit1> <commit2>

这会打印出它们都基于的提交名称。你现在应该查找这些提交的树对象,这可以通过以下方式轻松完成:

$ git cat-file commit <commitname> | head -1

因为树对象信息总是提交对象中的第一行。

一旦你知道要合并的三棵树(一棵“原始”树,也就是共同树,以及两棵“结果”树,也就是你想合并的分支),你就可以将“合并”读取到索引中。如果它必须丢弃你旧的索引内容,它会发出警告,所以你应该确保你已经提交了这些内容——事实上,你通常总是会根据你的最后一次提交进行合并(无论如何,这应该与你当前索引中的内容匹配)。

要执行合并,请执行:

$ git read-tree -m -u <origtree> <yourtree> <targettree>

这将在索引文件中直接为你执行所有琐碎的合并操作,你只需使用 git write-tree 写入结果。

合并多棵树,续

遗憾的是,许多合并并非微不足道。如果有文件被添加、移动或删除,或者如果两个分支都修改了同一个文件,你将得到一个包含“合并条目”的索引树。这样的索引树无法写入树对象,你必须在使用其他工具解决任何此类合并冲突后,才能写入结果。

你可以使用 git ls-files --unmerged 命令检查此类索引状态。示例:

$ git read-tree -m $orig HEAD $target
$ git ls-files --unmerged
100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1	hello.c
100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2	hello.c
100644 cc44c73eb783565da5831b4d820c962954019b69 3	hello.c

git ls-files --unmerged 命令的每行输出都以 blob 模式位、blob SHA-1、阶段号和文件名开头。阶段号是 Git 表示其来源树的方式:阶段 1 对应于 $orig 树,阶段 2 对应于 HEAD 树,阶段 3 对应于 $target 树。

前面我们提到,简单的合并是在 git read-tree -m 内部完成的。例如,如果文件从 $origHEAD$target 没有变化,或者如果文件从 $origHEAD 以及从 $orig$target 的变化方式相同,那么最终结果显然是 HEAD 中的内容。上述示例表明,文件 hello.c$origHEAD 以及从 $orig$target 的变化方式不同。你可以通过在你喜欢的 3-way 合并程序(例如 diff3merge 或 Git 自带的 merge-file)上,自行处理来自这三个阶段的 blob 对象来解决此问题,如下所示:

$ git cat-file blob 263414f >hello.c~1
$ git cat-file blob 06fa6a2 >hello.c~2
$ git cat-file blob cc44c73 >hello.c~3
$ git merge-file hello.c~2 hello.c~1 hello.c~3

这会将合并结果留在 hello.c~2 文件中,如果存在冲突,还会带有冲突标记。在验证合并结果合理后,你可以通过以下方式告诉 Git 此文件的最终合并结果:

$ mv -f hello.c~2 hello.c
$ git update-index hello.c

当一个路径处于“未合并”状态时,对该路径运行 git update-index 会告诉 Git 将该路径标记为已解决。

以上是 Git 最底层合并的描述,旨在帮助你理解其内部概念上发生了什么。实际上,没有人,甚至 Git 本身,会为此运行三次 git cat-file。有一个 git merge-index 程序,它将阶段提取到临时文件,并在此文件上调用一个“合并”脚本:

$ git merge-index git-merge-one-file hello.c

更高级别的 git merge -s resolve 就是用这种方式实现的。

Git 内部探秘

本章涵盖了 Git 实现的内部细节,可能只有 Git 开发者才需要了解。

对象存储格式

所有对象都具有静态确定的“类型”,用于标识对象的格式(即如何使用它,以及它如何引用其他对象)。目前有四种不同的对象类型:“blob”、“tree”、“commit”和“tag”。

无论对象类型如何,所有对象都共享以下特性:它们都使用 zlib 进行压缩,并带有一个头部,该头部不仅指定了它们的类型,还提供了对象中数据的大小信息。值得注意的是,用于命名对象的 SHA-1 散列是原始数据加上此头部的散列,因此 sha1sum file 不会匹配 file 的对象名称(最早的 Git 版本散列方式略有不同,但结论仍然相同)。

以下是一个简短的示例,演示如何手动生成这些散列:

假设有一个包含简单内容的小文本文件:

$ echo "Hello world" >hello.txt

现在我们可以手动生成 Git 会用于此文件的散列:

  • 我们想要为其生成散列的对象类型是“blob”,其大小为 12 字节。

  • 将对象头部前置到文件内容,然后将其提供给 sha1sum

$ { printf "blob 12\0"; cat hello.txt; } | sha1sum
802992c4220de19a90767f3000a79a31b98d0df7  -

可以使用 git hash-object 验证此手动构建的散列,该命令当然会隐藏头部的添加过程:

$ git hash-object hello.txt
802992c4220de19a90767f3000a79a31b98d0df7

因此,对象的通用一致性总是可以独立于对象的内容或类型进行测试:所有对象都可以通过验证 (a) 它们的散列与文件内容匹配,以及 (b) 对象成功解压为字节流,形成 <ascii-type-without-space> + <space> + <ascii-decimal-size> + <byte\0> + <binary-object-data> 序列来验证。

结构化对象可以进一步验证其结构和与其他对象的连接性。这通常通过 git fsck 程序完成,该程序生成所有对象的完整依赖图,并验证它们的内部一致性(除了仅通过散列验证其表面一致性)。

Git 源代码概览

对于新开发者来说,在 Git 源代码中找到方向并非总是容易。本节为你提供一些指导,告诉你从何开始。

一个好的起点是查看初始提交的内容:

$ git switch --detach e83c5163

初始修订奠定了 Git 今天几乎所有内容的基础(尽管细节在某些地方可能有所不同),但它足够小,可以一口气读完。

请注意,自该修订以来,术语已发生变化。例如,该修订中的 README 使用“changeset”一词来描述我们现在所说的提交

此外,我们不再称之为“cache”(缓存),而是“index”(索引);但是,该文件仍然名为 read-cache.h

如果你理解了初始提交中的概念,你应该查看一个较新的版本,并浏览 read-cache-ll.hobject.hcommit.h

早期,Git(遵循 UNIX 传统)是一组极其简单的程序,你在脚本中使用它们,将一个程序的输出通过管道传递给另一个。这对初始开发很有利,因为它更容易测试新事物。然而,最近这些部分中的许多已经变成了内置命令,并且一些核心部分已经“库化”,即为了性能、可移植性以及避免代码重复而被放入 libgit.a。

到目前为止,你已经知道索引是什么(并且在 read-cache-ll.h 中找到相应的数据结构),并且知道只有几种对象类型(blobs、trees、commits 和 tags),它们都从 struct object 继承其通用结构,后者是它们的第一个成员(因此,你可以例如将 (struct object *)commit 强制转换为与 &commit->object 相同的效果,即获取对象名称和标志)。

现在是休息一下,让这些信息消化吸收的好时机。

下一步:熟悉对象命名。阅读命名提交。有很多方式来命名一个对象(不仅仅是修订版!)。所有这些都在 sha1_name.c 中处理。快速查看函数 get_sha1()。很多特殊处理都是由诸如 get_sha1_basic() 之类的函数完成的。

这只是为了让你进入 Git 最库化的部分:修订遍历器(revision walker)。

基本上,git log 的初始版本是一个 shell 脚本:

$ git-rev-list --pretty $(git-rev-parse --default HEAD "$@") | \
	LESS=-S ${PAGER:-less}

这意味着什么?

git rev-list 是修订遍历器的原始版本,它总是将修订列表打印到标准输出。它仍然可用,并且需要如此,因为大多数新的 Git 命令都是从使用 git rev-list 的脚本开始的。

git rev-parse 现在不再那么重要了;它仅用于过滤出与脚本调用的不同底层命令相关的选项。

git rev-list 所做的主要工作包含在 revision.crevision.h 中。它将选项封装在一个名为 rev_info 的结构体中,该结构体控制如何以及遍历哪些修订版等。

git rev-parse 的原始工作现在由函数 setup_revisions() 完成,它解析修订版和修订遍历器的常用命令行选项。这些信息存储在结构体 rev_info 中,供以后使用。调用 setup_revisions() 后,你可以进行自己的命令行选项解析。之后,你必须调用 prepare_revision_walk() 进行初始化,然后可以使用函数 get_revision() 逐个获取提交。

如果你对修订遍历过程的更多细节感兴趣,只需查看 cmd_log() 的首次实现;调用 git show v1.3.0~155^2~4 并向下滚动到该函数(请注意,你不再需要直接调用 setup_pager())。

如今,git log 是一个内置命令,这意味着它包含在命令 git 中。一个内置命令的源代码方面是:

  • 一个名为 cmd_<bla> 的函数,通常在 builtin/<bla.c> 中定义(请注意,旧版 Git 曾将其放在 builtin-<bla>.c 中),并在 builtin.h 中声明。

  • git.c 中的 commands[] 数组中有一个条目,以及

  • Makefile 中的 BUILTIN_OBJECTS 中有一个条目。

有时,一个源文件中包含多个内置命令。例如,cmd_whatchanged()cmd_log() 都位于 builtin/log.c 中,因为它们共享相当多的代码。在这种情况下,那些以其所在 .c 文件名命名的命令必须在 MakefileBUILT_INS 中列出。

git log 在 C 语言中看起来比原始脚本复杂,但这带来了更大的灵活性和性能。

在这里再次暂停一下是个不错的时机。

第三课是:研究代码。真的,这是了解 Git 组织结构的最好方法(在你了解基本概念之后)。

那么,思考一下你感兴趣的事情,比如:“如何仅通过 blob 的对象名称来访问它?”第一步是找到一个可以实现此功能的 Git 命令。在本例中,它要么是 git show,要么是 git cat-file

为了清晰起见,我们继续使用 git cat-file,因为它:

  • 是底层命令(plumbing),并且

  • 甚至在初始提交中就已存在(它字面上只作为 cat-file.c 经历了大约 20 次修订,在成为内置命令时被重命名为 builtin/cat-file.c,然后只经历了不到 10 个版本)。

因此,查看 builtin/cat-file.c,搜索 cmd_cat_file() 并查看它的作用。

        git_config(git_default_config);
        if (argc != 3)
		usage("git cat-file [-t|-s|-e|-p|<type>] <sha1>");
        if (get_sha1(argv[2], sha1))
                die("Not a valid object name %s", argv[2]);

我们跳过显而易见的细节;这里唯一真正有趣的部分是对 get_sha1() 的调用。它尝试将 argv[2] 解释为对象名称,如果它引用了当前仓库中存在的对象,它会将生成的 SHA-1 写入变量 sha1

这里有两点值得注意:

  • get_sha1()成功时返回 0。这可能会让一些新的 Git 开发者感到惊讶,但 UNIX 中有一个悠久的传统:在不同错误情况下返回不同的负数,而在成功时返回 0。

  • get_sha1() 函数签名中的变量 sha1unsigned char * 类型,但实际上期望它是一个指向 unsigned char[20] 的指针。该变量将包含给定提交的 160 位 SHA-1 值。请注意,当 SHA-1 作为 unsigned char * 传递时,它是二进制表示,而十六进制字符的 ASCII 表示则作为 char * 传递。

你会在整个代码中看到这两种情况。

现在,重点来了:

        case 0:
                buf = read_object_with_reference(sha1, argv[1], &size, NULL);

这就是你读取 blob(实际上,不仅是 blob,而是任何类型的对象)的方式。要了解函数 read_object_with_reference() 实际如何工作,请查找其源代码(例如在 Git 仓库中运行 git grep read_object_with | grep ":[a-z]"),然后阅读源代码。

要了解如何使用结果,只需继续阅读 cmd_cat_file()

        write_or_die(1, buf, size);

有时,你不知道在哪里查找某个特性。在许多此类情况下,通过搜索 git log 的输出,然后 git show 相应的提交,会有所帮助。

示例:如果你知道 git bundle 有一些测试用例,但想不起来它在哪里(是的,你可以运行 git grep bundle t/,但这并不能说明重点!):

$ git log --no-merges t/

在分页器(less)中,只需搜索“bundle”,回溯几行,你就会看到它在提交 18449ab0 中。现在只需复制此对象名称,并将其粘贴到命令行中:

$ git show 18449ab0

就是这样。

另一个示例:找出如何将某个脚本变为内置命令:

$ git log --no-merges --diff-filter=A builtin/*.c

你看,Git 实际上是了解 Git 自身源代码的最佳工具!

Git 词汇表

Git 解释

备用对象数据库

通过替补机制,一个仓库可以从另一个对象数据库继承其部分对象数据库,这个被继承的对象数据库被称为“替补”。

裸仓库

裸仓库通常是一个以 .git 后缀命名的目录,它没有本地签出的任何版本控制下的文件副本。也就是说,通常隐藏在 .git 子目录中的所有 Git 管理和控制文件都直接存在于 repository.git 目录中,没有其他文件存在和被签出。公共仓库的发布者通常会提供裸仓库。

blob 对象

无类型对象,例如文件内容。

分支

“分支”是一条开发线。分支上最新的提交被称为该分支的“尖端”(tip)。分支的尖端由一个分支引用,随着分支上进行更多开发,该头会向前移动。单个 Git 仓库可以跟踪任意数量的分支,但你的工作树仅与其中一个(“当前”或“已检出”分支)关联,并且 HEAD 指向该分支。

缓存

已弃用,指:索引

一个对象列表,其中列表中的每个对象都包含对其后继的引用(例如,一个提交的后继可以是其某个父提交)。

变更集

BitKeeper/cvsps 中的“提交”的说法。由于 Git 不存储变更,而是存储状态,因此在 Git 中使用“变更集”一词确实没有意义。

检出

使用对象数据库中的树对象blob更新工作树的全部或部分,如果整个工作树已指向新的分支,则更新索引HEAD的操作。

拣选(cherry-picking)

SCM术语中,“拣选(cherry pick)”意味着从一系列变更(通常是提交)中选择一个子集,并将其作为一组新的变更记录在不同的代码库之上。在 Git 中,这通过“git cherry-pick”命令执行,用于提取现有提交引入的更改,并将其基于当前分支的尖端记录为一个新的提交。

清洁的

如果一个工作树与当前所引用的修订版本相对应,则称其为清洁的。另请参见“脏的”。

提交

作为名词:Git 历史中的一个单独点;一个项目的整个历史表示为一组相互关联的提交。Git 经常在其他版本控制系统使用“修订(revision)”或“版本(version)”的地方使用“提交(commit)”一词。也用作提交对象的简称。

作为动词:通过创建一个新的提交来表示索引的当前状态,并将 HEAD 前进指向新的提交,从而将项目状态的新快照存储到 Git 历史中的行为。

提交图概念、表示和用法

对象数据库中由提交形成的DAG结构的同义词,由分支尖端引用,使用其链接提交的。这种结构是权威的提交图。该图可以通过其他方式表示,例如“commit-graph”文件

commit-graph 文件

“commit-graph”(通常带连字符)文件是提交图的补充表示,它加速了提交图的遍历。“commit-graph”文件存储在 .git/objects/info 目录或备用对象数据库的 info 目录中。

提交对象

一个对象,它包含特定修订版本的信息,例如父提交、提交者、作者、日期以及对应于所存储修订版本顶层目录树对象

commit-ish(或称 committish)

一个提交对象或可以递归解引用为提交对象的对象。以下都是 commit-ish:一个提交对象,一个指向提交对象的标签对象,一个指向标签对象再指向提交对象的标签对象,等等。

Git 核心

Git 的基本数据结构和工具。仅暴露有限的源代码管理工具。

DAG

有向无环图。提交对象形成一个有向无环图,因为它们有父提交(有向),并且提交对象的图是无环的(没有以相同对象开始和结束的)。

悬空对象

一个即使从其他不可达对象也无法到达不可达对象;悬空对象在仓库中没有任何引用或对象指向它。

解引用

指的是符号引用:访问符号引用所指向的引用的行为。递归解引用涉及对结果引用重复上述过程,直到找到一个非符号引用。

指的是标签对象:访问标签所指向的对象的行为。标签通过对结果对象重复操作进行递归解引用,直到结果具有指定的对象类型(如果适用)或任何非“tag”对象类型。在标签上下文中,“递归解引用”的同义词是“剥离(peel)”。

指的是提交对象:访问提交的树对象的行为。提交不能被递归解引用。

除非另有说明,否则在 Git 命令或协议上下文中使用的“解引用”是隐式递归的。

分离 HEAD

通常,HEAD 存储一个分支的名称,对 HEAD 所代表的历史进行操作的命令,都是对 HEAD 指向的分支尖端的历史进行操作。然而,Git 也允许你检出任意一个不一定是任何特定分支尖端的提交。处于这种状态的 HEAD 被称为“分离的”(detached)。

请注意,当 HEAD 分离时,对当前分支历史进行操作的命令(例如使用 git commit 在其之上构建新历史)仍然有效。它们会更新 HEAD 以指向更新后历史的尖端,而不会影响任何分支。而更新或查询当前分支信息的命令(例如设置当前分支与哪个远程跟踪分支集成的 git branch --set-upstream-to)显然不起作用,因为在此状态下没有(真正的)当前分支可供查询。

目录

用“ls”命令获得的列表 :-)

脏的

如果一个工作树包含尚未提交到当前分支的修改,则称其为“脏的”。

邪恶合并

邪恶合并是指一种合并,它引入了在任何父提交中都未出现的更改。

快进

快进是一种特殊类型的合并,当你有一个修订版本,并且你正在“合并”另一个分支的更改,而这些更改恰好是你所拥有版本的后代时,就会发生快进。在这种情况下,你不会创建一个新的合并提交,而是直接更新你的分支以指向与你正在合并的分支相同的修订版本。这在远程仓库远程跟踪分支上会经常发生。

获取

获取一个分支意味着从远程仓库中获取该分支的头引用,找出本地对象数据库中缺少哪些对象,并获取它们。另请参见git-fetch[1]

文件系统

Linus Torvalds 最初将 Git 设计为用户空间文件系统,即用于存储文件和目录的基础设施。这确保了 Git 的效率和速度。

Git 归档

仓库的同义词(对于 arch 人员)。

gitfile

工作树根目录下的一个普通文件 .git,它指向实际仓库所在的目录。有关正确使用方法,请参阅git-worktree[1]git-submodule[1]。有关语法,请参阅gitrepository-layout[5]

嫁接

嫁接(Grafts)通过记录提交的虚假祖先信息,使得两条原本不同的开发线能够连接在一起。这样,你可以让 Git 假装一个提交父提交集合与创建该提交时记录的不同。通过 .git/info/grafts 文件配置。

请注意,嫁接机制已过时,可能导致仓库间对象传输出现问题;请参阅git-replace[1]以获取更灵活和健壮的实现相同功能的系统。

散列

在 Git 的语境中,是对象名称的同义词。

指向分支尖端提交命名引用。头存储在 $GIT_DIR/refs/heads/ 目录下的文件中,打包引用(packed refs)除外。(参阅git-pack-refs[1]。)

HEAD

当前的分支。更详细地说:你的工作树通常派生自HEAD所指向的树的状态。HEAD是你仓库中某个的引用,除非在使用分离头指针,在这种情况下它直接引用一个任意的提交。

头引用

的同义词。

钩子

在多个Git命令的正常执行期间,会调用可选脚本,允许开发者添加功能或进行检查。通常,钩子允许命令在执行前进行预验证并可能被中止,并在操作完成后进行事后通知。钩子脚本位于$GIT_DIR/hooks/目录下,通过简单地删除文件名中的.sample后缀即可启用。在Git的早期版本中,你需要使它们可执行。

索引

一个包含文件统计信息的集合,其内容作为对象存储。索引是你的工作树的一个存储版本。实际上,它还可以包含工作树的第二个甚至第三个版本,这些版本在合并时使用。

索引项

存储在索引中有关特定文件的信息。如果合并已开始但尚未完成(即索引包含该文件的多个版本),则索引项可能未合并。

master

默认的开发分支。无论何时创建Git仓库,都会创建一个名为"master"的分支,并成为活动分支。在大多数情况下,这包含本地开发,尽管这纯粹是约定,并非强制要求。

合并

作为动词:将另一个分支(可能来自外部仓库)的内容带入当前分支。如果被合并的分支来自不同的仓库,则首先抓取远程分支,然后将结果合并到当前分支。这种抓取和合并操作的组合称为拉取。合并由一个自动过程执行,该过程识别自各分支分歧以来所做的更改,然后将所有这些更改一起应用。如果更改发生冲突,可能需要手动干预才能完成合并。

作为名词:除非是快进合并,成功的合并会创建一个新的提交,代表合并结果,并以被合并分支的最新提交作为其父提交。这个提交被称为“合并提交”,有时也简称为“合并”。

对象

Git中的存储单元。它通过其内容的SHA-1唯一标识。因此,对象不能被更改。

对象数据库

存储一组“对象”,单个对象通过其对象名称标识。这些对象通常位于$GIT_DIR/objects/

对象标识符 (oid)

对象名称的同义词。

对象名称

对象的唯一标识符。对象名称通常由一个40个字符的十六进制字符串表示。口语中也称为SHA-1

对象类型

描述对象类型之一的标识符:“提交”、“”、“标签”或“Blob”。

章鱼式合并

合并两个以上分支

孤立分支

指进入一个尚不存在的分支(即未生分支)的行为。在此操作之后,首次创建的提交将成为一个没有父提交的提交,从而开始一个新的历史。

origin

默认的上游仓库。大多数项目至少有一个它们跟踪的上游项目。默认情况下,origin 用于此目的。新的上游更新将被抓取到名为 origin/name-of-upstream-branch 的远程跟踪分支中,你可以使用git branch -r查看。

覆盖

仅更新和添加文件到工作目录,但不删除它们,类似于 cp -R 更新目标目录内容的方式。这是从索引树状对象检出文件时检出的默认模式。相比之下,no-overlay 模式还会删除源中不存在的已跟踪文件,类似于 rsync --delete

打包

一组已压缩到一个文件中的对象(为了节省空间或高效传输)。

包索引

中对象的标识符列表及其他信息,用于协助高效访问包的内容。

路径规范

用于限制Git命令中路径的模式。

路径规范用于“git ls-files”、“git ls-tree”、“git add”、“git grep”、“git diff”、“git checkout”以及许多其他命令的命令行,以将操作范围限制在树或工作树的某个子集。有关路径是相对于当前目录还是顶层目录的,请参阅每个命令的文档。路径规范语法如下:

  • 任何路径都匹配自身

  • 路径规范直到最后一个斜杠表示一个目录前缀。该路径规范的范围仅限于该子树。

  • 路径规范的其余部分是 pathname 剩余部分的模式。相对于目录前缀的路径将使用 fnmatch(3) 与该模式进行匹配;特别是,*? 可以匹配目录分隔符。

例如,Documentation/*.jpg 将匹配 Documentation 子树中的所有 .jpg 文件,包括 Documentation/chapter_1/figure_1.jpg。

以冒号:开头的路径规范具有特殊含义。在短形式中,前导冒号:后跟零个或多个“魔术签名”字母(可选地由另一个冒号:终止),其余部分是要与路径匹配的模式。“魔术签名”由非字母数字、非通配符、非正则表达式特殊字符且非冒号的 ASCII 符号组成。如果模式以不属于“魔术签名”符号集且不是冒号的字符开头,则可以省略终止“魔术签名”的可选冒号。

在长形式中,前导冒号:后跟一个左括号(、一个逗号分隔的零个或多个“魔术词”列表,以及一个右括号),其余部分是要与路径匹配的模式。

仅包含冒号的路径规范表示“没有路径规范”。这种形式不应与其他路径规范结合使用。

top

魔术词top(魔术签名:/)使模式从工作树的根目录开始匹配,即使你在子目录中运行命令也是如此。

字面量

模式中的通配符,例如*?,将被视为字面字符。

icase

不区分大小写的匹配。

通配符模式

Git 将模式视为适用于 fnmatch(3) 并带有 FNM_PATHNAME 标志的 shell 通配符模式:模式中的通配符不会匹配路径名中的 /。例如,“Documentation/*.html”匹配“Documentation/git.html”,但不匹配“Documentation/ppc/ppc.html”或“tools/perf/Documentation/perf.html”。

与完整路径名匹配的模式中,两个连续的星号(**)可能具有特殊含义:

  • 前导的**后跟斜杠表示匹配所有目录。例如,**/foo匹配任意位置的文件或目录foo,与模式foo相同。**/foo/bar匹配直接在目录foo下的任意位置的文件或目录bar

  • 后置的/**匹配其内部的所有内容。例如,abc/**匹配目录“abc”内的所有文件,相对于.gitignore文件的位置,深度无限。

  • 斜杠后跟两个连续的星号再跟一个斜杠,匹配零个或多个目录。例如,a/**/b匹配a/ba/x/ba/x/y/b等。

  • 其他连续的星号被视为无效。

    通配符魔术与字面量魔术不兼容。

attr

attr:之后是空格分隔的“属性要求”列表,所有这些要求都必须满足才能将路径视为匹配;这补充了通常的非魔术路径规范模式匹配。参见gitattributes[5]

路径的每个属性要求采取以下形式之一:

  • "ATTR" 要求属性ATTR被设置。

  • "-ATTR" 要求属性ATTR未设置。

  • "ATTR=VALUE" 要求属性ATTR被设置为字符串VALUE

  • "!ATTR" 要求属性ATTR未指定。

    请注意,在与树对象匹配时,属性仍然从工作树获取,而不是从给定的树对象获取。

排除

路径匹配任何非排除路径规范后,它将通过所有排除路径规范(魔术签名:!或其同义词^)。如果匹配,则该路径将被忽略。如果没有非排除路径规范,则排除将应用于结果集,就像在不带任何路径规范的情况下调用一样。

父提交

提交对象包含一个(可能为空的)开发线中逻辑前身(即其父提交)的列表。

剥离

递归解引用标签对象的行为。

镐式选择

术语镐式选择指的是 diffcore 例程的一个选项,它有助于选择添加或删除给定文本字符串的更改。使用--pickaxe-all选项,它可以用于查看引入或删除(例如)特定文本行的完整变更集。参见git-diff[1]

底层命令

Git核心的可爱名称。

上层命令

依赖于Git核心的程序和程序套件的可爱名称,它们提供对Git核心的高级访问。上层命令比底层命令暴露出更多SCM接口。

每个工作树引用

那些属于每个工作树而非全局的引用。目前仅包括HEAD以及所有以refs/bisect/开头的引用,但将来可能会包含其他不寻常的引用。

伪引用

一种与普通引用具有不同语义的引用。这些引用可以通过普通的Git命令读取,但不能通过诸如git-update-ref[1]之类的命令写入。

Git已知的伪引用如下:

  • FETCH_HEADgit-fetch[1]git-pull[1]写入。它可能引用多个对象ID。每个对象ID都附带元数据,指示其从何处抓取以及抓取状态。

  • MERGE_HEAD在解决合并冲突时由git-merge[1]写入。它包含所有正在合并的提交ID。

拉取

拉取一个分支意味着抓取它并合并它。另请参阅git-pull[1]

推送

推送一个分支意味着从远程仓库获取该分支的头引用,查明它是否是该分支本地头引用的祖先,如果是,则将所有从本地头引用可达且远程仓库中缺失的对象放入远程对象数据库,并更新远程头引用。如果远程不是本地头的祖先,则推送失败。

可达的

给定提交的所有祖先都被认为是可从该提交“可达的”。更一般地,一个对象如果通过一条可以从另一个对象到达,则称其可达:该链遵循标签指向其标记的对象,提交指向其父提交或树,以及指向其包含的树或Blob

可达性位图

可达性位图存储打包文件或多包索引(MIDX)中选定提交集可达性的信息,以加速对象搜索。位图存储在“.bitmap”文件中。一个仓库最多只能使用一个位图文件。该位图文件可以属于一个包,也可以属于仓库的多包索引(如果存在)。

变基

将一个分支上的一系列更改重新应用到不同的基准上,并将该分支的重置为结果。

引用

指向对象名称或另一个引用(后者称为符号引用)的名称。为方便起见,引用在用作Git命令的参数时有时可以缩写;详情请参阅gitrevisions[7]。引用存储在仓库中。

引用命名空间是分层的。引用名称必须以refs/开头或位于层次结构的根目录中。对于后者,它们的名称必须遵循以下规则:

  • 名称仅由大写字符或下划线组成。

  • 名称以“_HEAD”结尾或等于“HEAD”。

    层次结构根目录中存在一些不符合这些规则的非标准引用。以下列表是详尽的,将来不应扩展:

  • AUTO_MERGE

  • BISECT_EXPECTED_REV

  • NOTES_MERGE_PARTIAL

  • NOTES_MERGE_REF

  • MERGE_AUTOSTASH

    不同的子层次结构用于不同的目的。例如,refs/heads/层次结构用于表示本地分支,而refs/tags/层次结构用于表示本地标签。

引用日志

引用日志显示了引用的本地“历史”。换句话说,它可以告诉你这个仓库中倒数第三个修订版本是什么,以及昨天晚上9:14这个仓库的当前状态是什么。详情请参阅git-reflog[1]

引用规范

“引用规范”由抓取推送使用,描述远程引用与本地引用之间的映射。详情请参阅git-fetch[1]git-push[1]

远程仓库

用于跟踪同一项目但位于其他位置的仓库。要与远程仓库通信,请参阅抓取推送

远程跟踪分支

用于跟踪另一个仓库中更改的引用。它通常看起来像refs/remotes/foo/bar(表示它跟踪名为foo的远程仓库中名为bar的分支),并且与配置的抓取引用规范的右侧匹配。远程跟踪分支不应包含直接修改或有本地提交。

仓库

引用的集合,以及包含所有从引用可达的对象的对象数据库,可能还伴随着一个或多个上层命令的元数据。一个仓库可以通过替代机制与其他仓库共享一个对象数据库。

解决

手动修复自动合并失败后遗留的问题的行为。

修订

提交(名词)的同义词。

回退

放弃部分开发,即将指向更早的修订版本

SCM

源代码管理(工具)。

SHA-1

“安全哈希算法1”;一种加密哈希函数。在Git语境中用作对象名称的同义词。

浅克隆

主要是浅仓库的同义词,但这个短语更明确地指出它是通过运行git clone --depth=...命令创建的。

浅仓库

仓库的历史不完整,其中一些提交父提交被“烧毁”(换句话说,Git被告知假装这些提交没有父提交,即使它们记录在提交对象中)。当你只对项目近期历史感兴趣时,这有时很有用,尽管上游记录的实际历史要大得多。浅仓库是通过向git-clone[1]提供--depth选项创建的,其历史随后可以通过git-fetch[1]加深。

暂存项

一个对象,用于临时存储工作目录和索引的内容,以便将来重用。

子模块

一个仓库,它在另一个仓库(后者称为超级项目)内部保存着一个独立项目的历史。

超级项目

一个仓库,在其工作树中将其他项目的仓库作为子模块引用。超级项目知道所包含子模块的提交对象的名称(但不持有其副本)。

符号引用

符号引用:它不包含SHA-1 ID本身,而是以ref: refs/some/thing的格式存在,并在被引用时递归地解引用到这个引用。HEAD是符号引用的一个主要例子。符号引用通过git-symbolic-ref[1]命令操作。

标签

refs/tags/命名空间下的一个引用,指向任意类型的对象(通常标签指向标签提交对象)。与不同,标签不通过commit命令更新。Git标签与Lisp标签(在Git语境中被称为对象类型)无关。标签最常用于标记提交祖先中的特定点。

标签对象

一个对象,包含指向另一个对象的引用,并且可以像提交对象一样包含一条消息。它还可以包含一个(PGP)签名,在这种情况下被称为“签名标签对象”。

主题分支

开发人员用于标识概念性开发线的一个常规Git分支。由于分支非常容易且成本低廉,通常希望有几个小分支,每个分支都包含非常明确的概念或小的增量但相关的更改。

尾注

键值元数据。尾注可选地出现在提交消息的末尾。在其他社区中可能被称为“页脚”或“标签”。参见git-interpret-trailers[1]

要么是一个工作树,要么是一个树对象连同其依赖的Blob和树对象(即工作树的存储表示)。

树对象

一个对象,包含文件名和模式的列表,以及指向相关联的 Blob 和/或树对象的引用。一个等同于一个目录

树状对象 (也作 treeish)

一个树对象或一个可以递归解引用为树对象的对象。解引用提交对象会得到对应修订版本顶层目录的树对象。以下都是树状对象:一个提交状对象、一个树对象、一个指向树对象的标签对象、一个指向指向树对象的标签对象的标签对象等。

未生

HEAD可以指向一个尚不存在且没有任何提交的分支,这样的分支被称为未生分支。用户遇到未生分支最典型的方式是重新创建一个仓库而没有从其他地方克隆。HEAD将指向尚未诞生的main(或master,取决于你的配置)分支。此外,一些操作可以通过它们的孤立分支选项让你进入一个未生分支。

未合并索引

包含未合并索引项索引

不可达对象

一个对象,无法从分支标签或任何其他引用可达

上游分支

默认的分支,它被合并到(或当前分支变基到)所讨论的分支中。它通过 branch.<name>.remote 和 branch.<name>.merge 配置。如果A的上游分支是origin/B,有时我们说“A正在跟踪origin/B”。

工作树

实际检出文件的树。工作树通常包含HEAD提交的树的内容,加上你已做出但尚未提交的任何本地更改。

工作树

一个仓库可以有零个(即裸仓库)或一个或多个工作树附加到它。一个“工作树”由一个“工作树”和仓库元数据组成,其中大部分在同一仓库的其他工作树之间共享,而一些则按每个工作树单独维护(例如索引、HEAD和MERGE_HEAD等伪引用、每个工作树引用和每个工作树配置文件)。

附录 A: Git 快速参考

这是主要命令的快速摘要;前面的章节更详细地解释了它们的工作原理。

创建新仓库

从 tarball 创建

$ tar xzf project.tar.gz
$ cd project
$ git init
Initialized empty Git repository in .git/
$ git add .
$ git commit

从远程仓库创建

$ git clone git://example.com/pub/project.git
$ cd project

管理分支

$ git branch			# list all local branches in this repo
$ git switch test	        # switch working directory to branch "test"
$ git branch new		# create branch "new" starting at current HEAD
$ git branch -d new		# delete branch "new"

除了基于当前 HEAD 创建新分支(默认行为)外,还可以使用

$ git branch new test    # branch named "test"
$ git branch new v2.6.15 # tag named v2.6.15
$ git branch new HEAD^   # commit before the most recent
$ git branch new HEAD^^  # commit before that
$ git branch new test~10 # ten commits before tip of branch "test"

同时创建并切换到新分支

$ git switch -c new v2.6.15

更新并检查你克隆自的仓库中的分支

$ git fetch		# update
$ git branch -r		# list
  origin/master
  origin/next
  ...
$ git switch -c masterwork origin/master

从不同的仓库抓取一个分支,并在你的仓库中为其指定一个新名称

$ git fetch git://example.com/project.git theirbranch:mybranch
$ git fetch git://example.com/project.git v2.6.15:mybranch

保留你经常使用的仓库列表

$ git remote add example git://example.com/project.git
$ git remote			# list remote repositories
example
origin
$ git remote show example	# get details
* remote example
  URL: git://example.com/project.git
  Tracked remote branches
    master
    next
    ...
$ git fetch example		# update branches from example
$ git branch -r			# list all remote branches

探索历史

$ gitk			    # visualize and browse history
$ git log		    # list all commits
$ git log src/		    # ...modifying src/
$ git log v2.6.15..v2.6.16  # ...in v2.6.16, not in v2.6.15
$ git log master..test	    # ...in branch test, not in branch master
$ git log test..master	    # ...in branch master, but not in test
$ git log test...master	    # ...in one branch, not in both
$ git log -S'foo()'	    # ...where difference contain "foo()"
$ git log --since="2 weeks ago"
$ git log -p		    # show patches as well
$ git show		    # most recent commit
$ git diff v2.6.15..v2.6.16 # diff between two tagged versions
$ git diff v2.6.15..HEAD    # diff with current head
$ git grep "foo()"	    # search working directory for "foo()"
$ git grep v2.6.15 "foo()"  # search old tree for "foo()"
$ git show v2.6.15:a.txt    # look at old version of a.txt

搜索回归

$ git bisect start
$ git bisect bad		# current version is bad
$ git bisect good v2.6.13-rc2	# last known good revision
Bisecting: 675 revisions left to test after this
				# test here, then:
$ git bisect good		# if this revision is good, or
$ git bisect bad		# if this revision is bad.
				# repeat until done.

进行更改

确保 Git 知道谁是负责人

$ cat >>~/.gitconfig <<\EOF
[user]
	name = Your Name Comes Here
	email = you@yourdomain.example.com
EOF

选择要包含在下一次提交中的文件内容,然后进行提交

$ git add a.txt    # updated file
$ git add b.txt    # new file
$ git rm c.txt     # old file
$ git commit

或者,一步完成提交的准备和创建

$ git commit d.txt # use latest content only of d.txt
$ git commit -a	   # use latest content of all tracked files

合并

$ git merge test   # merge branch "test" into the current branch
$ git pull git://example.com/project.git master
		   # fetch and merge in remote branch
$ git pull . test  # equivalent to git merge test

分享你的更改

导入或导出补丁

$ git format-patch origin..HEAD # format a patch for each commit
				# in HEAD but not in origin
$ git am mbox # import patches from the mailbox "mbox"

在不同的Git仓库中抓取一个分支,然后合并到当前分支

$ git pull git://example.com/project.git theirbranch

在合并到当前分支之前,将抓取的分支存储到本地分支

$ git pull git://example.com/project.git theirbranch:mybranch

在本地分支上创建提交后,使用你的提交更新远程分支

$ git push ssh://example.com/project.git mybranch:theirbranch

当远程分支和本地分支都命名为“test”时

$ git push ssh://example.com/project.git test

常用远程仓库的快捷方式版本

$ git remote add example ssh://example.com/project.git
$ git push example test

仓库维护

检查损坏

$ git fsck

重新压缩,移除未使用的垃圾

$ git gc

附录 B: 本手册的备注和待办事项列表

待办事项列表

这是一项正在进行的工作。

基本要求

  • 对于一个对UNIX命令行有基本了解但没有Git特殊知识的聪明人来说,它必须能够按顺序从头到尾阅读。如有必要,任何其他先决条件应在出现时具体提及。

  • 尽可能地,章节标题应以无需过多必要知识的语言,清楚地描述它们所解释的任务:例如,“将补丁导入项目”而不是“git am 命令”

考虑如何创建一个清晰的章节依赖图,这将允许人们在不必阅读所有中间内容的情况下,直接跳转到重要主题。

扫描Documentation/以查找遗漏的其他内容;特别是:

  • 操作指南

  • technical/ 中的一些内容?

  • 钩子

  • git[1]中的命令列表

扫描电子邮件存档以查找遗漏的其他内容

扫描手册页,查看是否有任何内容假设了比本手册提供的更多背景知识。

添加更多好的示例。完全由“食谱”式示例组成的章节可能是一个好主意;也许可以把“高级示例”部分作为章节的标准结尾部分?

在适当的地方包含到词汇表的交叉引用。

添加一个关于与其他版本控制系统(包括CVS、Subversion以及单纯的发布tarball系列导入)协作的章节。

编写一个关于使用底层命令和编写脚本的章节。

替代方案、克隆 -reference 等。

scroll-to-top