简体中文 ▾ 主题 ▾ 最新版本 ▾ 用户手册最后更新于 2.53.0

简介

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

本手册旨在让具备基础 UNIX 命令行技能但之前不了解 Git 的读者能够阅读。

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

需要进行实际开发的人员还应阅读 使用 Git 进行开发与他人分享开发成果

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

详尽的参考文档可通过 man 手册或 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

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

clone 命令会创建一个以项目命名的新目录(在上面的示例中为 gitlinux)。在你 cd 进入该目录后,你会发现它包含项目文件的副本,称为 工作区 (working tree),以及一个名为 .git 的特殊顶层目录,其中包含有关项目历史的所有信息。

如何检出项目的不同版本

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

这些快照并不一定都按照从旧到新的单一线条排列;相反,工作可以沿着平行的开发线路同时进行,这些开发线路被称为 分支 (branches),它们可能会合并或分叉。

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

$ git branch
* master

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

大多数项目还会使用 标签 (tags)。标签与 heads 一样,都是指向项目历史的引用,可以使用 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
...

标签通常始终指向项目的同一个版本,而 heads 则随着开发的推进而向前移动。

创建一个指向其中一个版本的新分支 head,并使用 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

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

理解历史:提交

项目历史中的每一次更改都由一个提交表示。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 允许开发线路分叉然后重新汇合,两条开发线路重新汇合的点称为“合并 (merge)”。因此,代表合并的提交可以有多个父节点,每个父节点代表通向该点的其中一条开发线路上的最近提交。

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

在下文中,如果提交 X 是提交 Y 的祖先,我们说提交 X 从提交 Y “可达 (reachable)”。等效地,你可以说 Y 是 X 的后代,或者存在一条父节点链从提交 Y 指向提交 X。

理解历史:历史图解

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

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

如果我们需要讨论某个特定的提交,字符 "o" 可能会被替换为另一个字母或数字。

理解历史:什么是分支?

当我们需要精确表述时,我们会用“分支 (branch)”一词表示一条开发线路,用“分支头 (branch head)”(或简称“head”)表示对分支上最近提交的引用。在上面的示例中,名为 "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>

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

特殊符号 "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 处于“分离 (detached)”状态。

这是一种在无需为新分支命名的情况下检出特定版本的简便方法。如果你后来决定保留,仍然可以为此版本创建一个新分支(或标签)。

查看远程仓库的分支

你在克隆时创建的 "master" 分支是你所克隆的仓库中 HEAD 的副本。不过,那个仓库可能还有其他分支,你的本地仓库会保留跟踪这些远程分支的分支,称为远程跟踪分支 (remote-tracking branches),你可以使用 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 来查看它或编写一个临时补丁。参见 分离 HEAD

请注意,名称 "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] 的 "SPECIFYING REVISIONS" 章节。

使用 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] 的 "CONFIGURATION FILE" 章节。)

探索 Git 历史

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

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

我们从一个专门的工具开始,它对于寻找引入 bug 的提交非常有用。

如何使用 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 暂时将你移到了 "(no branch)"。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] 查看该提交,查明是谁写的,并将 bug 报告连同提交 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] 手册页中的 "SPECIFYING REVISIONS" 部分。一些示例

$ 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 历史可以包含多条独立的开发线,因此列出提交的具体顺序可能带有一定的随机性。

生成差异 (Diffs)

你可以使用 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 跟踪的任何文件路径。

示例

统计分支上的提交数量

假设你想知道自 mybranchorigin 分叉以来,你在该分支上进行了多少次提交

$ 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] 列出此仓库中的所有 heads

$ 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 到达但不能从这些其他 heads 到达的提交

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

显然,可以有无限的变体;例如,要查看所有可从某个 head 到达但不能从仓库中任何标签到达的提交

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

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

为软件发布创建变更日志和压缩包

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] 的 "CONFIGURATION FILE" 部分。该文件是纯文本,因此你也可以使用你喜欢的编辑器进行编辑。

创建新仓库

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

$ mkdir project
$ cd project
$ git init

如果你有一些初始内容(比如一个压缩包)

$ 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] 来创建提交、查看索引和工作树文件中的更改,并单独选择差异块 (diff hunks) 以包含在索引中(通过右键单击差异块并选择 "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 将自动完成合并并提交结果(或者在 快进 (fast-forward) 的情况下复用现有提交,见下文)。另一方面,如果存在冲突——例如,如果同一个文件在远程分支和本地分支中以两种不同的方式被修改——那么你会收到警告;输出可能看起来像这样

$ 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] 会将这些文件列为 "unmerged"(未合并),并且有冲突的文件会添加冲突标记,如下所示

<<<<<<< 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 进行三路 diff,以仅显示内容来自双方混合的区块(换句话说,当一个区块的合并结果仅来自阶段 2 时,该部分不冲突且不显示。阶段 3 同理)。

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

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

$ 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"。

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

$ 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

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

快进合并 (Fast-forward merges)

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

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

纠正错误

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

$ 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
...

你会看到有关悬空对象 (dangling objects) 的提示消息。它们是仍然存在于仓库中但不再被你的任何分支引用的对象,在一段时间后可以(且将会)被 gc 移除。你可以运行 git fsck --no-dangling 来屏蔽这些消息,同时仍然查看真实的错误。

恢复丢失的更改

引用日志 (Reflogs)

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

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

$ git log master@{1}

这将列出从 master 分支头的上一个版本可达的提交。这种语法可以与任何接受提交的 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 也有一个单独的 reflog,所以

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

将显示一周前 HEAD 指向的内容,而不是一周前当前分支指向的内容。这允许你查看你检出内容的记录历史。

默认情况下,reflogs 会保留 30 天,之后可能会被修剪。请参阅 git-reflog[1]git-gc[1] 了解如何控制此修剪,并参阅 gitrevisions[7] 的 "SPECIFYING REVISIONS" 部分了解详情。

请注意,reflog 历史与普通的 Git 历史非常不同。虽然普通历史由每个处理同一项目的仓库共享,但 reflog 历史是不共享的:它只告诉你本地仓库中的分支随时间发生了怎样的变化。

检查悬空对象

在某些情况下,reflog 可能无法挽救你。例如,假设你删除了一个分支,然后意识到你需要它包含的历史记录。reflog 也会被删除;然而,如果你还没有修剪仓库,那么你可能仍然能够在 git fsck 报告的悬空对象中找到丢失的提交。详情请参阅 悬空对象

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

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

$ gitk 7281251ddd --not --all

正如其字面意思:它表示你想查看悬空提交所描述的提交历史,而不是你所有现有分支和标签所描述的历史。因此,你得到了从该提交可达的且已经丢失的准确历史。(注意,它可能不仅仅是一个提交:我们只报告“行末”为悬空,但可能丢失了一整段深层且复杂的提交历史。)

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

$ git branch recovered-branch 7281251ddd

其他类型的悬空对象(blobs 和 trees)也是可能的,并且悬空对象也可能在其他情况下产生。

与他人分享开发成果

使用 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/*,并将默认分支合并到当前分支。

更一般地说,从远程跟踪分支创建的分支默认将从该分支 pull。请参阅 git-config[1]branch.<name>.remotebranch.<name>.merge 选项的描述,以及 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 可以包含一个初始的“封面信 (cover letter)”。你可以在三横线行之后插入关于单个补丁的评论,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 包含一个“裸 (bare)”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

(另请参阅 通过 HTTP 设置 Git 服务器,了解使用 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 一样,如果这不能导致 快进git push 将会报错;有关处理此情况的详细信息,请参阅下一节。

请注意,push 的目标通常是一个 裸 (bare) 仓库。你也可以向具有检出工作树的仓库推送,但默认情况下拒绝通过 push 更新当前检出的分支,以防止混淆。详情请参阅 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

有关详细信息,请参阅 git-config[1]remote.<name>.urlbranch.<name>.remoteremote.<name>.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 为维护者提供了一种简单的方法,将这项工作委托给其他维护者,同时仍然允许对传入的更改进行可选审查。

  • 由于每个开发人员的仓库都有项目历史记录的相同完整副本,因此没有任何仓库是特殊的,另一位开发人员接管项目的维护工作是微不足道的,无论是通过双方协议,还是因为维护者变得不负责或难以合作。

  • 由于缺乏核心的“提交者 (committers)”群体,意味着较少需要就是谁“加入”和谁“离开”做出正式决定。

允许网页浏览仓库

gitweb cgi 脚本为用户提供了一种简便的方法,无需安装 Git 即可浏览你的项目版本、文件内容和日志。RSS/Atom 订阅和 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 恢复完整历史。

只要合并基准在近期历史中,在 浅克隆 内合并就能正常工作。否则,这就像合并无关的历史一样,可能会导致巨大的冲突。这种限制可能使此类仓库不适合用于基于合并的工作流。

示例

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

这描述了 Tony Luck 在担任 Linux 内核 IA64 架构维护者时如何使用 Git。

他使用两个公开分支

  • 一个“测试 (test)”树,补丁最初被放置在这里,以便在与其他正在进行的开发集成时获得一些曝光。Andrew 可以根据需要随时将此树拉取到 -mm 分支中。

  • 一个“发布 (release)”树,经过测试的补丁将被移至此处进行最终的完整性检查,并作为向 Linus 发送上游请求的载体(通过向他发送“请拉取 (please pull)”请求)。

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

要进行此设置,首先通过克隆 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 分支的当前末端开始,并且应该设置为默认从 Linus 那里合并更改(使用 git-branch[1]--track 选项)。

$ 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 将简单地执行“快进 (fast-forward)”合并)。许多人不喜欢这在 Linux 历史记录中产生的“噪音”,因此你应该避免在 release 分支中随意这样做,因为当你请求 Linus 从发布分支拉取时,这些嘈杂的提交将成为永久历史记录的一部分。

一些配置变量(参见 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 ]*

当你对该更改的状态感到满意时,可以将其合并到“测试 (test)”分支中,为公开做准备

$ 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] 准备一条“请拉取 (please pull)”请求消息发给 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] 的“INTERACTIVE MODE”部分。

其他工具

还有许多其他工具(如 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 无法知道新的 head 是旧 head 的更新版本;它对待这种情况的方式与两个开发人员独立地在旧 head 和新 head 上并行工作完全相同。此时,如果有人尝试将新 head 合并到他们的分支中,Git 将尝试将两条(旧的和新的)开发线路合并在一起,而不是尝试用新的替换旧的。结果可能会出乎意料。

你仍然可以选择发布历史记录被重写的分支,其他人能够获取这些分支以便检查或测试可能是有用的,但他们不应尝试将此类分支拉取(pull)到他们自己的工作中。

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

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

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 将此过程称为快进 (fast-forward)

快进看起来像这样

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

在某些情况下,新的 head 可能不是旧 head 的后代。例如,开发人员可能意识到犯了一个严重的错误并决定回溯,导致如下情况

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

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

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

强制 git fetch 进行非快进更新

如果由于分支的新 head 不是旧 head 的后代导致 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 更加直观。

我们从最重要的开始,即对象数据库索引

对象数据库

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

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

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

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

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

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

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

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

  • “commit”对象将此类目录层次结构绑定在一起,形成修订版本的有向无环图——每个提交都包含恰好一个树的对象名称,代表提交时的目录层次结构。此外,提交还引用了描述我们如何到达该目录层次结构历史的“父 (parent)”提交对象。

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

更详细的对象类型说明

提交对象 (Commit Object)

“commit”对象将树的物理状态与关于我们如何到达那里以及原因的描述联系起来。使用 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):一个树对象(定义见下文)的 SHA-1 名称,代表特定时间点目录的内容。

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

  • 作者 (author):负责此更改的人员姓名及其日期。

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

  • 描述此提交的注释。

请注意,提交本身不包含有关实际更改内容的任何信息;所有更改都是通过将此提交引用的树的内容与其父提交关联的树进行比较来计算的。特别地,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(代表文件内容),或者是另一个 tree(代表子目录内容)。由于树和 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 对象。该对象与其在目录树中的位置完全无关,重命名文件不会更改该文件关联的对象。

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

信任

如果你从一个源接收到 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 files)

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

不幸的是,一旦项目拥有大量对象,该系统就会变得效率低下。在旧项目上尝试执行此操作

$ git count-objects
6930 objects, 47620 kilobytes

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

你可以通过将这些松散对象移动到“包文件 (pack file)”中来节省空间并提高 Git 运行速度,包文件以高效的压缩格式存储一组对象;有关包文件格式的详细信息,请参见 gitformat-pack[5]

要将松散对象放入包中,只需运行 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/ 中创建一个包含所有当前未打包对象的单个“包文件”。然后你可以运行

$ git prune

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

$ git count-objects
0 objects, 0 kilobytes

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

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

悬空对象 (Dangling objects)

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

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

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

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

通常,悬空对象没什么好担心的。它们甚至可能非常有用:如果你搞砸了某些事情,悬空对象可能是你恢复旧树的方法(例如,你执行了变基,然后意识到你真的不想这样做——你可以查看你拥有的悬空对象,并决定将你的 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 甚至通常会包含合并冲突标记),或者仅仅是因为你用 ^C 等中断了 git fetch,导致对象数据库中留下了一些新对象,但它们只是悬空且无用。

无论如何,一旦你确定对任何悬空状态都不感兴趣,就可以修剪所有不可达对象

$ git prune

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

从仓库损坏中恢复

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

应对此类问题的首要防线是备份。你可以使用 clone 备份 Git 目录,或者仅使用 cp、tar 或任何其他备份机制。

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

我们假设问题是单个缺失或损坏的 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

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

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

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

  2. 索引能够实现其定义的树对象与工作树之间的快速比较。

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

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

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

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

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

子模块 (Submodules)

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

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

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

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

Git 的子模块支持允许仓库将外部项目的检出作为子目录包含在内。子模块保持自己的身份;子模块支持仅存储子模块仓库位置和提交 ID,因此克隆包含项目(“父项目 (superproject)”)的其他开发人员可以轻松克隆相同修订版本的所有子模块。父项目的部分检出是可能的:你可以告诉 Git 克隆部分或全部子模块,或者一个都不克隆。

git-submodule[1] 命令自 Git 1.5.3 起可用。使用 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 都会在子模块包含新文件或修改过的文件时将其显示为已修改,以防止意外提交此类状态。在生成补丁输出或使用 --submodule 选项时,git diff 也会在工作树端添加 -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 操作

许多高级命令最初是作为使用较小核心的低级 Git 命令的 shell 脚本实现的。这些在执行不寻常的 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>

现在你的索引文件将等同于你之前保存的树。但是,这只是你的索引文件:你的工作目录内容尚未被修改。

索引 → 工作目录

你可以通过“检出 (checking out)”文件从索引更新工作目录。这并不是很常见的操作,因为通常你会保持文件最新,并且你会将工作目录中的更改告知索引文件(即 git update-index),而不是写入工作目录。

但是,如果你决定跳转到新版本,或检出其他人的版本,或者只是恢复之前的树,你会先用 read-tree 填充索引文件,然后你需要检出结果:

$ git checkout-index filename

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

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

最后,还有一些不完全是从一种表示形式移动到另一种表示形式的零碎操作:

整合所有内容

要提交你使用 git write-tree 实例化的树,你需要创建一个引用该树及其背后历史的“提交 (commit)”对象——最显著的是历史记录中在它之前的“父 (parent)”提交。

通常一个“提交”有一个父提交:进行某种更改之前树的前一个状态。但是,有时它可以有两个或更多父提交,在这种情况下,我们称之为“合并 (merge)”,因为此类提交将由其他提交表示的两个或更多前一个状态组合在一起(“合并”)。

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

你通过给提交对象提供描述提交时状态的树以及父提交列表来创建它

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

然后在标准输入 (stdin) 中提供提交的原因(通过管道或文件的重定向,或者直接在终端输入)。

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 可以帮助你执行三方合并,通过多次重复合并过程,三方合并反过来又可以用于多方合并。通常情况是你只进行一次三方合并(协调两条历史线)并提交结果,但如果你愿意,也可以一次性合并多个分支。

要执行三方合并,你从要合并的两个提交开始,找到它们最近的共同父提交(第三个提交),并比较对应于这三个提交的树。

要获得合并的“基准 (base)”,请查找两个提交的共同父提交:

$ 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 以不同方式发生了变化。你可以通过对这三个阶段的 blob 对象运行你最喜欢的三方合并程序(例如 diff3merge 或 Git 自己的 merge-file)来亲自解决此问题,如下所示:

$ 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

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

以上是对最底层 Git 合并过程的描述,旨在帮助您从概念上理解底层发生的机制。在实践中,没有人(甚至 Git 自身)会为此运行三次 git cat-file。存在一个 git merge-index 程序,它会将暂存级(stages)提取到临时文件中,并在其上调用“合并”脚本

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

而更高层级的 git merge -s resolve 正是基于此实现的。

开发 Git (Hacking Git)

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

对象存储格式

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

无论对象类型如何,所有对象都具有以下特征:它们都使用 zlib 进行压缩(deflated),并且都有一个头部,该头部不仅指定了它们的类型,还提供了关于对象中数据大小的信息。值得注意的是,用于命名对象的 SHA-1 哈希值是原始数据加上该头部的哈希,因此 sha1sum filefile 的对象名称并不匹配(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) 对象成功解压(inflate)为一系列字节流,其序列形式为 <ascii类型(不含空格)> + <空格> + <ascii十进制大小> + <byte\0> + <二进制对象数据>

结构化对象可以进一步验证其结构以及与其他对象的连接性。这通常使用 git fsck 程序完成,该程序会生成所有对象的完整依赖图,并验证其内部一致性(除了通过哈希验证其表面一致性之外)。

Git 源代码鸟瞰

新开发者想要在 Git 源代码中找到方向并不总是那么容易。本节提供了一些指导,告诉您从哪里开始。

一个好的起点是查看初始提交的内容,使用

$ git switch --detach e83c5163

初始版本为 Git 今天拥有的一切奠定了基础(尽管某些地方的细节可能有所不同),但它的规模足够小,可以在一次阅读中读完。

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

此外,我们现在不再称其为“cache”,而是称为“index”;然而,头文件仍然叫做 read-cache.h

如果您掌握了初始提交中的思想,您应该查看较新的版本并浏览 read-cache-ll.hobject.hcommit.h

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

到现在,您已经知道索引是什么了(并在 read-cache-ll.h 中找到相应的数据结构),并且知道只有几种对象类型(blob、tree、commit 和 tag),它们从作为其第一个成员的 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 不再那么重要了;它以前仅用于过滤掉与脚本调用的不同底层命令(plumbing)相关的选项。

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.ccommands[] 数组的一个条目,以及

  • MakefileBUILTIN_OBJECTS 的一个条目。

有时,一个源文件中包含多个内置命令。例如,cmd_show()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() 并看看它做了什么。

        repo_config(the_repository, 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() 函数签名中的变量 sha1 类型是 unsigned char *,但实际上预期它是一个指向 unsigned char[20] 的指针。此变量将包含给定提交的 160 位 SHA-1。请注意,每当 SHA-1 以 unsigned char * 传递时,它是二进制表示,与之相对的是以十六进制字符表示的 ASCII 表示,后者以 char * 传递。

在整个代码中,您都会看到这两点。

现在,进入实质部分

        case 0:
                buf = odb_read_object_peeled(r->objects, sha1, argv[1], &size, NULL);

这就是您读取 blob 的方式(实际上,不仅是 blob,还可以是任何类型的对象)。要了解函数 odb_read_object_peeled() 实际是如何工作的,请找到它的源代码(在 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 详解

备用对象数据库 (alternate object database)

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

裸仓库 (bare repository)

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

数据对象 (blob object)

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

分支 (branch)

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

缓存 (cache)

已过时,现指:索引 (index)

链 (chain)

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

变更集 (changeset)

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

检出 (checkout)

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

拣选 (cherry-picking)

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

干净 (clean)

如果工作区与当前头 (head)引用的修订版本一致,则它是干净的。另见“脏 (dirty)”。

提交 (commit)

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

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

提交图概念、表示和用法

由对象数据库中的提交形成的 DAG 结构的同义词,通过分支顶端引用,并使用它们的链接提交。这个结构是决定性的提交图。该图也可以用其他方式表示,例如 “commit-graph”文件

commit-graph 文件

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

提交对象 (commit object)

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

提交类对象 (commit-ish)

一个提交对象或一个可以递归地解引用为提交对象的对象。以下都是提交类对象:提交对象、指向提交对象的标签对象、指向“指向提交对象的标签对象”的标签对象,等等。

核心 Git (core Git)

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

DAG

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

悬空对象 (dangling object)

一个不可达对象,它甚至无法从其他不可达对象到达;悬空对象在仓库中的任何引用或对象中都没有指向它的引用。

解引用 (dereference)

引用符号引用 (symbolic ref):访问符号引用所指向的引用的操作。递归解引用涉及在结果引用上重复上述过程,直到找到一个非符号引用。

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

引用提交对象:访问提交的树对象的操作。提交不能被递归解引用。

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

游离 HEAD (detached HEAD)

通常,HEAD 存储分支的名称,而操作 HEAD 所代表的历史的命令,实际上是在 HEAD 指向的分支顶端所引导的历史上操作。然而,Git 也允许您检出一个不一定是任何特定分支顶端的任意提交。处于这种状态的 HEAD 被称为“游离的”。

请注意,操作当前分支历史的命令(例如 git commit 在其之上构建新历史)在 HEAD 处于游离状态时仍然有效。它们会更新 HEAD 以指向更新历史的顶端,而不会影响任何分支。而更新或查询关于当前分支信息的命令(例如设置当前分支与哪个远程跟踪分支整合的 git branch --set-upstream-to)显然无法工作,因为在这种状态下没有(真正的)当前分支可供查询。

目录 (directory)

您通过“ls”得到的列表 :-)

脏 (dirty)

如果工作区包含尚未提交到当前分支的修改,则称该工作区是“脏”的。

恶意合并 (evil merge)

恶意合并是指引入了未出现在任何父提交中更改的合并

快进 (fast-forward)

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

获取 (fetch)

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

文件系统 (file system)

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

Git 存档 (Git archive)

仓库的同义词(针对 arch 用户)。

gitfile

位于工作区根目录的普通文件 .git,它指向真正的仓库目录。具体用法请参见 git-worktree[1]git-submodule[1]。语法请参见 gitrepository-layout[5]

嫁接 (grafts)

嫁接允许通过为提交记录虚假的祖先信息,将两条原本不同的开发线连接在一起。通过这种方式,您可以让 Git 假装一个提交拥有的父对象集与其创建时记录的不同。通过 .git/info/grafts 文件进行配置。

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

哈希 (hash)

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

头 (head)

指向某个分支顶端提交命名引用。除了使用打包引用(packed refs)的情况外,Heads 存储在 $GIT_DIR/refs/heads/ 目录的文件中。(参见 git-pack-refs[1]。)

HEAD

当前分支。更详细地:您的工作区通常衍生自 HEAD 所引用的树状态。HEAD 是对您仓库中某一个 head 的引用,除非使用了游离 HEAD,在这种情况下,它直接引用一个任意提交。

头引用 (head ref)

head 的同义词。

钩子 (hook)

在几个 Git 命令的正常执行期间,会调用可选的脚本,允许开发者添加功能或检查。通常,钩子允许对命令进行预先验证并可能中止,并允许在操作完成后进行事后通知。钩子脚本位于 $GIT_DIR/hooks/ 目录中,只需删除文件名的 .sample 后缀即可启用。在早期版本的 Git 中,您必须使其可执行。

索引 (index)

一组带有 stat 信息的文件,其内容作为对象存储。索引是您工作区的存储版本。事实上,它还可以包含工作区的第二个甚至第三个版本,这些版本在合并时使用。

索引项 (index entry)

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

master

默认开发分支。每当您创建一个 Git 仓库时,都会创建一个名为“master”的分支,并成为活动分支。在大多数情况下,它包含本地开发内容,尽管这纯粹是惯例,并非强制要求。

合并 (merge)

作为动词:将另一个分支(可能来自外部仓库)的内容带入当前分支。如果合并的分支来自不同的仓库,这是通过先获取 (fetch)远程分支,然后将结果合并到当前分支来完成的。这种获取和合并操作的结合被称为 拉取 (pull)。合并由一个自动过程执行,该过程识别自分支分叉以来所做的更改,然后将所有这些更改一起应用。在更改发生冲突的情况下,可能需要手动干预才能完成合并。

作为名词:除非是快进,否则成功的合并会导致创建一个表示合并结果的新提交,并以被合并分支的顶端作为其父对象。此提交被称为“合并提交”,或者有时简称为“一个合并”。

对象 (object)

Git 中的存储单元。它由其内容的 SHA-1 唯一标识。因此,对象不能被更改。

对象数据库 (object database)

存储一组“对象”,每个单独的对象都通过其对象名称来标识。对象通常位于 $GIT_DIR/objects/ 中。

对象标识符、对象 ID、oid

对象名称的同义词。

对象名称 (object name)

对象的唯一标识符。对象名称通常由 40 个字符的十六进制字符串表示。口语中也称为 SHA-1

对象类型 (object type)

描述对象类型的标识符之一:“commit”、“tree”、“tag”或“blob”。

章鱼合并 (octopus)

合并两个以上的分支

孤立 (orphan)

进入一个尚不存在的分支(即未出生的分支)的行为。在此操作之后,首次创建的提交将成为一个没有父对象的提交,从而开启一段新的历史。

origin

默认的上游仓库。大多数项目至少有一个它们跟踪的上游项目。默认情况下,使用 origin 为此目的。新的上游更新将获取到名为 origin/name-of-upstream-branch 的远程跟踪分支中,您可以使用 git branch -r 查看它们。

覆盖 (overlay)

仅更新和添加文件到工作目录,但不删除它们,类似于 cp -R 更新目标目录内容的方式。这是从索引树类对象检出文件时,检出 (checkout) 的默认模式。相比之下,no-overlay 模式还会删除源中不存在的已跟踪文件,类似于 rsync --delete

包 (pack)

一组已压缩成一个文件的对象(为了节省空间或高效传输)。

包索引 (pack index)

中对象的标识符列表及其他信息,用以辅助高效访问包的内容。

路径规范 (pathspec)

Git 命令中用于限制路径的模式。

路径规范用于 "git ls-files"、"git ls-tree"、"git add"、"git grep"、"git diff"、"git checkout" 以及许多其他命令的命令行中,以将操作范围限制在树或工作区的某个子集。请参阅每个命令的文档,了解路径是相对于当前目录还是顶层目录。路径规范语法如下:

  • 任何路径都匹配其自身

  • 直到最后一个斜杠为止的路径规范代表一个目录前缀。该路径规范的范围仅限于该子树。

  • 路径规范的其余部分是路径名余下部分的模式。相对于目录前缀的路径将使用 fnmatch(3) 与该模式进行匹配;特别是,*? 可以匹配目录分隔符。

例如,Documentation/*.jpg 将匹配 Documentation 子树中的所有 .jpg 文件,包括 Documentation/chapter_1/figure_1.jpg。

以冒号 : 开头的路径规范具有特殊含义。在短格式中,前导冒号 : 后跟零个或多个“魔法签名”字母(可选地以另一个冒号 : 结尾),其余部分是与路径匹配的模式。“魔法签名”由既非字母数字、通配符、正则特殊字符也非冒号的 ASCII 符号组成。如果模式开头的字符不属于“魔法签名”符号集且不是冒号,则可以省略终止“魔法签名”的可选冒号。

在长格式中,前导冒号 : 后跟一个左括号 (、一个由零个或多个“魔法词”组成的逗号分隔列表以及一个右括号 ),其余部分是与路径匹配的模式。

仅包含一个冒号的路径规范意味着“没有路径规范”。此形式不应与其他路径规范结合使用。

top

魔法词 top(魔法签名:/)使模式从工作区的根部开始匹配,即使您是从子目录内部运行命令也是如此。

literal

模式中的通配符(如 *?)被视为普通字符。

icase

不区分大小写的匹配。

glob

Git 将模式视为 shell 通配符,适用于带有 FNM_PATHNAME 标志的 fnmatch(3) 使用:模式中的通配符不会匹配路径名中的 /。例如,“Documentation/*.html”匹配“Documentation/git.html”,但不匹配“Documentation/ppc/ppc.html”或“tools/perf/Documentation/perf.html”。

与完整路径名匹配的模式中,两个连续的星号(**)可能具有特殊含义:

  • 前导“**”后跟一个斜杠表示在所有目录中匹配。例如,“**/foo”匹配任何地方的文件或目录“foo”。“**/foo/bar”匹配直接位于“foo”目录下的任何地方的文件或目录“bar”。

  • 尾随的“/**”匹配其中的所有内容。例如,“abc/**”匹配目录“abc”内的所有文件(相对于 .gitignore 文件的位置),深度无限。

  • 斜杠后跟两个连续的星号,然后是斜杠,匹配零个或多个目录。例如,“a/**/b”匹配“a/b”、“a/x/b”、“a/x/y/b”等等。

  • 其他连续的星号被视为无效。

    Glob 魔法与 literal 魔法不兼容。

attr

attr: 之后是一个以空格分隔的“属性要求”列表,必须满足所有这些要求才能认为路径匹配;这是对通常的非魔法路径规范模式匹配的补充。参见 gitattributes[5]

路径的每个属性要求采用以下形式之一:

  • ATTR”要求设置属性 ATTR

  • -ATTR”要求不设置属性 ATTR

  • ATTR=VALUE”要求将属性 ATTR 设置为字符串 VALUE

  • !ATTR”要求未指定属性 ATTR

    请注意,在与树对象匹配时,属性仍然是从工作区获取的,而不是从给定的树对象获取的。

exclude

在路径匹配任何非排除路径规范后,它将通过所有排除路径规范(魔法签名:! 或其同义词 ^)。如果匹配,则忽略该路径。当没有非排除路径规范时,排除将应用于结果集,就像在没有任何路径规范的情况下调用一样。

父提交 (parent)

一个提交对象包含一个(可能为空的)开发线中逻辑前任的列表,即它的父对象。

剥离 (peel)

递归地解引用一个标签对象的操作。

鹤嘴锄 (pickaxe)

术语 pickaxe 指的是 diffcore 例程的一个选项,旨在帮助选择添加或删除给定文本字符串的更改。使用 --pickaxe-all 选项,它可以用于查看引入或删除某一行特定文本的完整变更集。请参阅 git-diff[1]

底层命令 (plumbing)

核心 Git 的别称。

上层命令 (porcelain)

依赖于核心 Git 的程序和程序集的别称,提供对核心 Git 的高级访问。与底层命令相比,上层命令暴露了更多的 SCM 界面。

单工作区引用 (per-worktree ref)

属于单个工作区而非全局的引用。目前仅包括 HEAD 和任何以 refs/bisect/ 开头的引用,但以后可能包含其他特殊引用。

伪引用 (pseudoref)

语义与普通引用不同的引用。这些引用可以通过正常的 Git 命令读取,但不能由 git-update-ref[1] 等命令写入。

Git 已知的伪引用如下:

  • FETCH_HEADgit-fetch[1]git-pull[1] 写入。它可能引用多个对象 ID。每个对象 ID 都附带有元数据,指明其获取来源和获取状态。

  • MERGE_HEADgit-merge[1] 在解决合并冲突时写入。它包含所有正在被合并的提交 ID。

拉取 (pull)

拉取一个分支意味着获取它并合并它。另请参阅 git-pull[1]

推送 (push)

推送一个分支意味着从远程仓库获取该分支的头引用,找出它是否是该分支本地头引用的祖先,如果是,则将从本地头引用可达且远程仓库中缺少的所有对象放入远程对象数据库中,并更新远程头引用。如果远程不是本地头的祖先,则推送失败。

可达 (reachable)

给定提交的所有祖先都被称为从该提交“可达”。更一般地,如果我们可以通过一条遵循标签到其所标记内容、提交到其父对象或树、以及到其包含的树或blob,从一个对象到达另一个对象,则称该对象是从另一个对象可达的。

可达性位图 (reachability bitmaps)

可达性位图存储有关包文件(packfile)或多包索引(MIDX)中选定提交集的可达性信息,以加速对象搜索。位图存储在“.bitmap”文件中。一个仓库最多只能使用一个位图文件。位图文件可能属于一个包,也可能属于仓库的多包索引(如果存在)。

变基 (rebase)

将一系列更改从一个分支重新应用到不同的基准上,并将该分支的重置为结果。

引用 (ref)

指向对象名称或其他引用的名称(后者称为符号引用)。为方便起见,引用在用作 Git 命令的参数时有时可以缩写;详情请参阅 gitrevisions[7]。引用存储在仓库中。

引用命名空间是层次结构的。引用名称必须以 refs/ 开头,或者位于层次结构的根部。对于后者,它们的名称必须遵循以下规则:

  • 名称仅由大写字母或下划线组成。

  • 名称以“_HEAD”结尾或等于“HEAD”。

    层次结构根部有一些不符合这些规则的不规则引用。以下列表是详尽的,将来不应再扩展:

  • AUTO_MERGE

  • BISECT_EXPECTED_REV

  • NOTES_MERGE_PARTIAL

  • NOTES_MERGE_REF

  • MERGE_AUTOSTASH

    不同的子层次结构用于不同的目的。例如,refs/heads/ 层次结构用于表示本地分支,而 refs/tags/ 层次结构用于表示本地标签。

引用日志 (reflog)

引用日志显示引用的本地“历史”。换句话说,它可以告诉您仓库中的倒数第 3 个修订版本是什么,以及昨天晚上 9:14 仓库的当前状态是什么。详情请参阅 git-reflog[1]

引用规范 (refspec)

“引用规范”由 获取 (fetch)推送 (push) 使用,用于描述远程引用与本地引用之间的映射。详情请参阅 git-fetch[1]git-push[1]

远程仓库 (remote repository)

一个用于跟踪同一个项目但位于其他地方的仓库。要与远程进行通信,请参阅 获取 (fetch)推送 (push)

远程跟踪分支 (remote-tracking branch)

用于关注另一个仓库更改的引用。它通常看起来像 refs/remotes/foo/bar(表示它跟踪名为 foo 的远程仓库中名为 bar 的分支),并且匹配已配置的获取引用规范的右侧。远程跟踪分支不应包含直接修改,也不应在其上进行本地提交。

仓库 (repository)

一组引用连同一个包含从这些引用可达的所有对象的对象数据库,可能还伴随着来自一个或多个上层命令的元数据。仓库可以通过备用机制与其他仓库共享对象数据库。

解决 (resolve)

手动修复失败的自动合并所留下的问题的操作。

修订版本 (revision)

提交 (commit)(名词)的同义词。

回滚 (rewind)

丢弃一部分开发成果,即把头 (head)指向更早的修订版本

SCM

源代码管理(工具)。

SHA-1

“安全哈希算法 1”;一种加密哈希函数。在 Git 上下文中用作对象名称的同义词。

浅克隆 (shallow clone)

通常是浅仓库的同义词,但这个短语更明确地表示它是通过运行 git clone --depth=... 命令创建的。

浅仓库 (shallow repository)

仓库的历史不完整,其中一些提交父对象被切除了(换句话说,Git 被告知假装这些提交没有父对象,即使它们记录在提交对象中)。当您仅对项目的近期历史感兴趣,而上游记录的真实历史要大得多时,这有时很有用。浅仓库通过给 git-clone[1] 提供 --depth 选项来创建,其历史以后可以通过 git-fetch[1] 加深。

储藏条目 (stash entry)

一个用于临时存储工作目录内容和索引以供将来重用的对象

子模块 (submodule)

一个在另一个仓库(后者称为父项目)内部保存独立项目历史的仓库

父项目 (superproject)

一个在其工作区中以子模块形式引用其他项目仓库的仓库。父项目知道所包含子模块的提交对象的名称(但不持有副本)。

符号引用 (symref)

符号引用:它不直接包含 SHA-1 ID,而是格式为 ref: refs/some/thing,当被引用时,它会递归地解引用到这个引用。HEAD 是符号引用的典型例子。符号引用通过 git-symbolic-ref[1] 命令进行操作。

标签 (tag)

位于 refs/tags/ 命名空间下的引用,指向任意类型的对象(通常标签指向标签对象提交对象)。与 head 不同,标签不会被 commit 命令更新。Git 标签与 Lisp 标签(在 Git 上下文中称为对象类型)没有任何关系。标签最常用于标记提交祖先中的特定点。

标签对象 (tag object)

一个包含指向另一个对象的引用对象,它可以像提交对象一样包含一条消息。它还可以包含 (PGP) 签名,在这种情况下,它被称为“签名标签对象”。

主题分支 (topic branch)

由开发者用来标识一个概念性开发线的常规 Git 分支。由于分支非常容易且廉价,因此拥有几个分别包含定义明确的概念或微小增量但相关更改的小分支通常是理想的。

尾注 (trailer)

键值元数据。尾注可选地出现在提交消息的末尾。在其他社区中可能被称为“footers”或“tags”。请参阅 git-interpret-trailers[1]

树 (tree)

工作区,或指树对象连同相关的 blob 和树对象(即工作区的存储表示)。

树对象 (tree object)

一个包含文件名和模式列表以及指向相关 blob 和/或树对象的引用的对象。一棵相当于一个目录

树类对象 (tree-ish)

一个树对象或一个可以递归地解引用为树对象的对象。解引用一个提交对象会得到对应于该修订版本目录的树对象。以下都是树类对象:提交类对象、树对象、指向树对象的标签对象、指向“指向树对象的标签对象”的标签对象,等等。

未出生的 (unborn)

HEAD 可以指向一个尚不存在且其上没有任何提交的分支,这种分支被称为未出生的分支。用户遇到未出生分支的最典型方式是在不从其他地方克隆的情况下重新创建一个仓库。HEAD 将指向尚未诞生的 main(或 master,取决于您的配置)分支。此外,某些操作可以通过其 orphan 选项让您进入一个未出生的分支。

未合并索引 (unmerged index)

一个包含未合并索引项索引

不可达对象 (unreachable object)

一个无法从分支标签或任何其他引用中到达对象

上游分支 (upstream branch)

默认合并到所讨论分支的分支(或者所讨论的分支以此为基准进行变基)。它通过 branch.<name>.remote 和 branch.<name>.merge 进行配置。如果 A 的上游分支是 origin/B,有时我们会说“A 正在跟踪 origin/B”。

工作区 (working tree)

实际检出的文件树。工作区通常包含 HEAD 提交树的内容,加上您已做出但尚未提交的任何本地修改。

工作树 (worktree)

一个仓库可以有零个(即裸仓库)或一个或多个附加到其上的工作树。一个“工作树”由一个“工作区”和仓库元数据组成,其中大部分元数据在单个仓库的其他工作树之间共享,而其中一些元数据按工作树分别维护(例如索引、HEAD 和 pseudorefs 如 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

搜索回归(regressions)

$ 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/ 以查找遗漏的其他内容;特别是:

  • 操作指南 (howto's)

  • technical/ 中的某些内容?

  • 钩子 (hooks)

  • git[1] 中的命令列表

扫描邮件存档以查找遗漏的其他内容

扫描帮助页 (man pages),看看是否有任何页面假设了比本手册提供的更多的背景知识。

添加更多好的示例。包含纯菜谱式示例的整个章节可能是一个好主意;也许将“高级示例”部分作为一个标准的章末部分?

在适当的地方加入对术语表的交叉引用。

添加一个关于与其他版本控制系统(包括 CVS、Subversion)配合使用以及仅导入系列发布压缩包的部分。

编写一章关于使用底层命令和编写脚本的内容。

备用仓库 (Alternates)、clone -reference 等。