中文 ▾ 主题 ▾ 最新版本 ▾ 用户手册上次更新于 2.48.0

简介

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

本手册旨在让具有基本 UNIX 命令行技能但没有 Git 先验知识的人阅读。

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

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

后面的章节涵盖了更专业的课题。

通过 man pages 或 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)。 进入此目录后,您将看到它包含项目文件的副本,称为工作树,以及一个名为.git的特殊顶层目录,其中包含有关项目历史的所有信息。

如何检出项目的不同版本

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

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

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

$ git branch
* master

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

大多数项目也使用标签。 标签,像 head 一样,是对项目历史的引用,可以使用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
...

标签应始终指向项目的同一版本,而 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

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

了解历史:提交

项目历史记录中的每个更改都由提交表示。 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”可能会被另一个字母或数字替换。

了解历史:什么是分支?

当我们需要精确时,我们将使用“分支”一词来表示开发线,而“分支头”(或简称“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>

创建一个新的分支 <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 检查引用的完整路径列表,以及当存在具有相同简写名称的多个引用时,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 提供了非常灵活和快速的工具来探索项目的历史记录。

我们从一个专门的工具开始,该工具对于查找将错误引入项目的提交很有用。

如何使用 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 现在与任何分支分离,并直接指向可从 “master” 访问但不能从 v2.6.18 访问的提交(提交 ID 为 65934)。编译并测试它,看看它是否崩溃。假设它确实崩溃了。然后

$ 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] 手册页的 “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 reset 更改当前检出的提交。

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 生成一个 gzipped 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 在一个称为“索引”的特殊暂存区域中维护树内容的快照。

一开始,索引的内容将与 HEAD 的内容相同。 因此,命令 `git diff --cached`(显示 HEAD 和索引之间的差异)此时不应产生任何输出。

修改索引很容易

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

$ 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 hunk 以包含在索引中(通过右键单击 diff hunk 并选择“暂存提交 Hunk”)。

创建良好的提交消息

虽然不是必需的,但最好以一行简短的(不超过 50 个字符)行开始提交消息,总结更改,后跟一个空行,然后是更详尽的描述。 提交消息中直到第一个空行的文本被视为提交标题,并且该标题在整个 Git 中使用。 例如,git-format-patch[1] 将提交转换为电子邮件,并在 Subject 行中使用标题,并在正文中使用提交的其余部分。

忽略文件

一个项目通常会生成你*不想*用 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 .gitignore` 和 `git 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 提交。

如果您使用 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 之间运行三方差异比较,以仅显示内容来自双方的区块(换句话说,当区块的合并结果仅来自阶段 2 时,该部分不冲突且不显示。阶段 3 也是如此)。

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

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

$ 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

但是,最后一条命令在某些情况下可能很危险——如果已经提交的提交可能已经被合并到另一个分支,则永远不要放弃该提交,因为这样做可能会使进一步的合并感到困惑。

快进合并

上面没有提到一个特殊情况,即快进合并,它的处理方式不同。通常,合并会导致合并提交,具有两个父提交,一个指向要合并的两个开发线路中的每一个。

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

修正错误

如果您弄乱了工作目录,但尚未提交您的错误,您可以使用以下命令将整个工作目录返回到上次提交的状态:

$ 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

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

临时搁置正在进行的工作

当您正在处理一些复杂的事情时,您发现了一个不相关但明显且微不足道的错误。您希望在继续之前修复它。您可以使用 git-stash[1] 保存当前工作状态,并在修复错误后(或者,可以选择在不同的分支上执行此操作,然后再返回),取消暂存正在进行中的更改。

$ 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 来抑制这些消息,并且仍然查看真正的错误。

恢复丢失的更改

Reflog

假设您使用 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 一周前指向的内容,而不是当前分支一周前指向的内容。 这允许您查看您签出的内容的历史记录。

默认情况下,reflog 会保留 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 配置为从原始仓库的 HEAD 分支获取更改。 因此,通常您可以通过一个简单的命令来完成上述操作:

$ git pull

此命令会将来自远程分支的更改提取到您的远程跟踪分支 origin/*,并将默认分支合并到当前分支中。

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

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

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

也可以将 git pull 命令的 "remote" 仓库指定为 .,在这种情况下,它只是从当前仓库合并一个分支;因此,以下命令:

$ 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 包含一个 "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

(另请参阅 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 一样,如果这没有导致 快进git push 会报错;有关处理此情况的详细信息,请参阅以下部分。

请注意,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

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

然而,对于需要一种简单的方法来发布正在进行中的补丁系列的人来说,这是一种常见的做法,并且只要您警告其他开发人员这就是您打算管理分支的方式,这是一个可以接受的折衷方案。

当其他人有权推送到同一个仓库时,也可能发生推送以这种方式失败的情况。在这种情况下,正确的解决方案是在首先更新您的工作后重试推送:通过拉取,或者通过获取后变基;请参阅 下一节gitcvs-migration[7] 了解更多信息。

设置共享仓库

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

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

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

  • 由于每个开发人员的仓库都具有项目历史的相同完整副本,因此没有哪个仓库是特殊的,并且另一个开发人员可以轻松地接管项目的维护,无论是通过双方协议,还是因为维护者变得无响应或难以合作。

  • 缺乏一个“提交者”的中心小组意味着减少了对谁“在内”和谁“在外”做出正式决定的需求。

允许 Web 浏览仓库

gitweb cgi 脚本为用户提供了一种简单的方法来浏览项目的修订、文件内容和日志,而无需安装 Git。可以选择启用 RSS/Atom 订阅和 blame/annotation 详细信息等功能。

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

有关使用 CGI 或具有 Perl 功能的服务器设置永久安装的详细信息,请参阅 Git 源代码树中的文件 gitweb/INSTALL 和 gitweb[1]

如何获取具有最小历史的 Git 仓库

浅克隆 及其截断的历史记录,在人们只对项目的最近历史感兴趣,并且从上游获取完整历史记录的成本很高时非常有用。

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

只要合并基础位于最近的历史记录中,在 浅克隆 中合并就可以工作。否则,它将像合并不相关的历史记录一样,并且可能导致巨大的冲突。此限制可能使此类仓库不适合在基于合并的工作流程中使用。

示例

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

这描述了 Tony Luck 如何在他的角色中使用 Git 作为 Linux 内核的 IA64 架构的维护者。

他使用两个公共分支

  • 一个“测试”树,补丁最初放置在该树中,以便在与其他正在进行的开发集成时可以获得一些曝光。只要 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 从发布分支中拉取时,这些有噪音的提交将成为永久历史记录的一部分。

一些配置变量(请参阅 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) 帮助未来的 bug 猎人在使用 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 返回到你开始 rebase 之前的状态

$ git rebase --abort

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

重写单个提交

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

$ git commit --amend

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

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

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

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

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

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

$ git am *.patch

使用交互式 rebase

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

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

$ git rebase -i HEAD~5

这将打开你的编辑器,其中包含执行 rebase 所需的步骤列表。

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

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

rebase 将在 pick 被替换为 edit 时停止,或者当列表中的某个步骤未能机械地解决冲突并需要你的帮助时停止。完成编辑和/或解决冲突后,你可以使用 git rebase --continue 继续。如果你觉得事情变得太棘手,你始终可以使用 git rebase --abort 退出。即使在 rebase 完成后,你仍然可以使用 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 无法知道新的 head 是旧 head 的更新版本;它以与两个开发人员独立并行地在旧 head 和新 head 上完成工作相同的方式处理这种情况。此时,如果有人尝试将新的 head 合并到他们的分支中,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 之间的历史记录 rebase 到 A 之上,你将获得这个线性历史记录

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

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

部分出于这个原因,许多经验丰富的 Git 用户,即使在处理其他方面合并繁重的项目时,也会通过在发布之前针对最新的上游版本进行 rebase 来保持历史记录的线性。

高级分支管理

获取单个分支

除了使用 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

在某些情况下,新的 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 执行非快进更新

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

$ 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”对象 将这些目录层次结构绑定到修订的 有向无环图 中——每个提交都包含一个树的对象名称,该树指定了提交时的目录层次结构。 此外,提交还引用“父”提交对象,这些对象描述了我们如何到达该目录层次结构的历史。

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

对象类型的更多详细信息

提交对象

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

$ 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 名称,这些提交表示项目中历史记录中紧接的前一步。 上面的例子有一个父项; 合并提交可能具有多个。 没有父项的提交称为“根”提交,表示项目的初始修订版。 每个项目必须至少有一个根。 项目也可以有多个根,但这并不常见(或者不一定是好主意)。

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

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

  • 描述此提交的评论。

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

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

树对象

功能通用的 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 对象。 该对象完全独立于其在目录树中的位置,并且重命名文件不会更改与该文件关联的对象。

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

信任

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

同样,你只需要信任顶级树对象的 SHA-1 名称即可信任它引用的整个目录的内容,并且如果你从受信任的来源收到提交的 SHA-1 名称,则可以轻松验证可通过该提交的父项访问的整个提交历史,以及这些提交引用的树的所有内容。

因此,为了在系统中引入一些真正的信任,你需要做的唯一的事情就是对一个特殊说明进行数字签名,其中包含顶级提交的名称。 你的数字签名向其他人表明你信任该提交,并且提交历史的不可变性告诉其他人他们可以信任整个历史。

换句话说,你可以通过仅发送一封电子邮件来轻松验证整个存档,该电子邮件告诉人们顶级提交的名称(SHA-1 哈希值),并使用 GPG/PGP 之类的东西对该电子邮件进行数字签名。

为了协助完成此操作,Git 还提供了 tag 对象……

Tag 对象

tag 对象包含对象,对象类型,tag 名称,创建 tag 的人的姓名(“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] 命令以了解如何创建和验证 tag 对象。 (请注意,git-tag[1] 也可用于创建“轻量级 tag”,它根本不是 tag 对象,而只是名称以 refs/tags/ 开头的简单引用)。

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

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

不幸的是,一旦项目有很多对象,该系统就会变得效率低下。 在一个旧项目上尝试一下

$ git count-objects
6930 objects, 47620 kilobytes

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

你可以通过将这些松散对象移动到一个“打包文件”中来节省空间并加快 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] 命令为你执行打包、修剪等操作,所以通常是你唯一需要的高级命令。

悬挂对象

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

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

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

类似地,当 "ort" 合并策略运行时,发现存在交叉合并,因此存在多个合并基础(这非常罕见,但确实会发生),它将生成一个临时的中间树(或者如果存在大量交叉合并和两个以上的合并基础,则可能更多)作为临时的内部合并基础,同样,这些都是真实的对象,但最终结果不会指向它们,因此它们最终会“悬挂”在你的存储库中。

通常,悬挂对象没什么好担心的。它们甚至可能非常有用:如果你搞砸了什么,悬挂对象可能是你恢复旧树的方式(例如,你进行了变基,并意识到你真的不想这样做——你可以查看你有哪些悬挂对象,并决定将你的头重置到一些旧的悬挂状态)。

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

$ 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 本身没有 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 版本。

索引

索引是一个二进制文件(通常保存在 .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,因此克隆包含项目(“超项目”)的其他开发者可以轻松地克隆所有子模块到相同的修订版本。可以进行超项目的局部检出:您可以告诉 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

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

要告诉 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,它将二进制内容转换为更易于阅读的形式。

查看 "commit" 对象特别有指导意义,因为这些对象往往很小且相当容易理解。特别是,如果你遵循将顶部提交名称放在 .git/HEAD 中的约定,你可以执行

$ git cat-file commit HEAD

来查看顶部提交是什么。

合并多个树

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

要执行三路合并,你首先需要两个要合并的提交,找到它们最近的共同父项(第三个提交),然后比较对应于这三个提交的树。

要获得合并的 "base",请查找两个提交的共同父项

$ git merge-base <commit1> <commit2>

这会打印出它们都基于的提交的名称。你现在应该查找这些提交的树对象,你可以很容易地使用以下命令来完成

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

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

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

要进行合并,请执行

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

这将直接在索引文件中为你执行所有简单的合并操作,你可以直接使用 git write-tree 将结果写出。

合并多个树,续

遗憾的是,许多合并并非易事。如果存在已添加、移动或删除的文件,或者两个分支都修改了同一个文件,你将得到一个包含 "merge entries" 的索引树。这样的索引树不能写出到树对象,你必须使用其他工具解决任何此类合并冲突,然后才能写出结果。

你可以使用 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、stage number 和文件名开头。stage number 是 Git 用于说明它来自哪个树的方式:stage 1 对应于 $orig 树,stage 2 对应于 HEAD 树,stage 3 对应于 $target 树。

之前我们说过简单的合并是在 git read-tree -m 内部完成的。例如,如果文件从 $origHEAD$target 没有更改,或者文件以相同的方式从 $orig 更改为 HEAD 和从 $orig 更改为 $target,显然最终结果是 HEAD 中的内容。上面的例子表明文件 hello.c 以不同的方式从 $orig 更改为 HEAD 和从 $orig 更改为 $target。你可以通过运行你最喜欢的 3 路合并程序来解决这个问题,例如 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

当路径处于 "unmerged" 状态时,为该路径运行 git update-index 会告诉 Git 将该路径标记为已解决。

以上是对 Git 合并在最低级别的描述,旨在帮助你理解在底层发生了什么概念上的事情。在实践中,没有人,甚至 Git 本身,会为此运行三次 git cat-file。有一个 git merge-index 程序会将各个阶段提取到临时文件中,并在其上调用 "merge" 脚本

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

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

破解 Git

本章介绍了 Git 实现的内部细节,可能只有 Git 开发人员需要理解。

对象存储格式

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

无论对象类型如何,所有对象都具有以下特征:它们都使用 zlib 进行解压缩,并且具有一个标头,该标头不仅指定了它们的类型,还提供了有关对象中数据的大小信息。值得注意的是,用于命名对象的 SHA-1 哈希是原始数据加上此标头的哈希,因此 filesha1sum *文件* 与 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" 一词来描述我们现在称为 commit 的内容。

此外,我们不再称其为 "cache",而是称其为 "index";但是,该文件仍称为 read-cache.h

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

在早期,Git(按照 UNIX 的传统)是一堆非常简单的程序,你可以在脚本中使用这些程序,将一个程序的输出通过管道传输到另一个程序。事实证明,这对初始开发有利,因为测试新事物更容易。然而,最近许多这些部分已经变成了内置程序,并且一些核心内容已经被 "libified",即放入 libgit.a 中,以提高性能、可移植性,并避免代码重复。

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

现在是休息一下,让这些信息沉淀下来的好时机。

下一步:熟悉对象命名。阅读 命名提交。有很多方法可以命名对象(不仅仅是修订版!)。所有这些都在 sha1_name.c 中处理。快速浏览一下函数 get_sha1()。许多特殊处理由 get_sha1_basic() 或类似函数完成。

这只是为了让你进入 Git 最 libified 的部分:修订版遍历器。

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

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

这意味着什么?

git rev-list 是修订版遍历器的原始版本,它总是将修订版列表打印到 stdout。它仍然可以正常工作,并且需要这样做,因为大多数新的 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.ccommands[] 数组中的一个条目,以及

  • MakefileBUILTIN_OBJECTS 中的一个条目。

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

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

在这里,再次暂停一下是个好主意。

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

所以,想想您感兴趣的东西,例如,“如何仅通过知道对象名称来访问 blob?” 第一步是找到一个 Git 命令可以做到这一点。 在这个例子中,它是 git showgit cat-file

为了清晰起见,我们坚持使用 git cat-file,因为它

  • 是底层命令,并且

  • 甚至在初始提交中就存在(它作为 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 解释

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

通过 alternates 机制,一个存储库可以从另一个对象数据库(称为 “alternate”)继承其部分对象数据库

裸仓库 (bare repository)

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

blob 对象

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

分支 (branch)

“分支” 是开发的线路。 分支上最新的 提交被称为该分支的顶端 (tip)。 分支的顶端通过分支 head引用,当在该分支上进行更多开发时,分支 head 会向前移动。 单个 Git 存储库可以跟踪任意数量的分支,但您的 工作树仅与其中一个分支(“当前” 或 “已检出” 分支)关联,并且 HEAD 指向该分支。

缓存 (cache)

已过时:请使用 索引

链 (chain)

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

变更集 (changeset)

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

检出 (checkout)

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

挑选 (cherry-picking)

SCM 术语中,“cherry pick” 指的是从一系列更改(通常是提交)中选择更改的子集,并将它们记录为不同代码库之上的一系列新更改。 在 Git 中,这是通过 “git cherry-pick” 命令执行的,以提取现有 提交引入的更改,并根据当前 分支的顶端将其记录为新的提交。

干净的 (clean)

如果工作树与当前 head 引用的 修订版本相对应,则它是干净的。 另请参阅 “脏的”。

提交 (commit)

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

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

提交图概念、表示形式和用法 (commit graph concept, representations and usage)

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

commit-graph 文件

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

提交对象 (commit object)

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

commit-ish(也称为 committish)

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

核心 Git (core Git)

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

DAG

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

悬挂对象 (dangling object)

一个 不可达对象,即使从其他不可达对象也无法 触及;一个悬挂对象在 仓库中的任何引用或对象中都没有对它的引用。

解引用 (dereference)

引用一个符号引用:访问符号引用所指向的引用的动作。递归解引用包括对结果引用重复上述过程,直到找到一个非符号引用。

引用一个标签对象:访问标签所指向的对象的动作。通过对结果对象重复操作来进行递归解引用,直到结果具有指定的对象类型(如果适用)或任何非"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 引用,以找出本地对象数据库中缺少哪些对象,并获取它们。另请参见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

指向分支顶端的提交命名引用。head 存储在 $GIT_DIR/refs/heads/ 目录中的文件中,除非使用 packed refs。(请参见git-pack-refs[1]。)

HEAD

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

head 引用 (head ref)

head的同义词。

钩子 (hook)

在多个 Git 命令的正常执行过程中,会调用可选脚本,使开发人员可以添加功能或检查。通常,这些钩子允许预先验证命令并可能中止,并允许在操作完成后进行后通知。钩子脚本位于 $GIT_DIR/hooks/ 目录中,只需从文件名中删除 .sample 后缀即可启用。在早期版本的 Git 中,你必须使它们可执行。

索引 (index)

一个包含文件和 stat 信息的集合,其内容存储为对象。索引是你的工作区的存储版本。说实话,它还可以包含工作区的第二个,甚至第三个版本,它们在合并时使用。

索引条目 (index entry)

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

主分支 (master)

默认的开发分支。每当你创建一个 Git 仓库时,都会创建一个名为 "master" 的分支,并成为活动分支。在大多数情况下,这包含本地开发,但这纯粹是约定,并非必须。

合并 (merge)

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

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

对象 (object)

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

对象数据库 (object database)

存储一组 "对象",并且单个对象由其对象名称标识。这些对象通常位于 $GIT_DIR/objects/ 中。

对象标识符 (object identifier, oid)

对象名称的同义词。

对象名称 (object name)

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

对象类型 (object type)

标识符 "commit"、"tree"、"tag" 或 "blob" 之一,用于描述对象的类型。

章鱼式合并 (octopus)

将两个以上的分支进行合并

孤立 (orphan)

切换到一个尚不存在的分支(即,一个未出生的分支)。在这样的操作之后,首先创建的提交将成为没有父提交的提交,从而开始新的历史。

origin

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

overlay(覆盖)

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

pack(包)

一组被压缩成一个文件的对象(为了节省空间或有效地传输它们)。

pack index(包索引)

一个 中对象的标识符和其他信息的列表,以帮助有效地访问包的内容。

pathspec(路径规范)

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

Pathspec 用于 "git ls-files", "git ls-tree", "git add", "git grep", "git diff", "git checkout" 和许多其他命令的命令行,以将操作的范围限制为树或工作树的某个子集。 有关路径是相对于当前目录还是顶级目录,请参阅每个命令的文档。 pathspec 语法如下:

  • 任何路径都匹配它本身

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

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

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

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

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

仅包含冒号的 pathspec 表示“没有 pathspec”。 此形式不应与其他 pathspec 组合使用。

top(顶部)

魔法单词 top(魔法签名:/)使模式从工作树的根目录开始匹配,即使您是从子目录中运行命令。

literal(字面量)

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

icase(不区分大小写)

不区分大小写的匹配。

glob(全局匹配)

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/**”匹配 .gitignore 文件位置的目录“abc”内的所有文件,深度无限。

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

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

    Glob 魔法与字面量魔法不兼容。

attr(属性)

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

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

  • ATTR”要求设置属性 ATTR

  • -ATTR”要求取消设置属性 ATTR

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

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

    请注意,当针对树对象进行匹配时,属性仍然从工作树中获取,而不是从给定的树对象中获取。

exclude(排除)

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

parent(父级)

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

peel(剥离)

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

pickaxe(镐)

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

plumbing(管道)

核心 Git 的可爱名称。

porcelain(瓷器)

依赖于 核心 Git 的程序和程序套件的可爱名称,提供了对核心 Git 的高级访问。 Porcelain 比 管道 更多地公开了 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(推送)

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

reachable(可达)

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

reachability bitmaps(可达性位图)

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

rebase(变基)

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

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 (引用日志)

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

refspec (引用规范)

“引用规范”被 fetchpush 用来描述远程引用和本地引用之间的映射。有关详细信息,请参阅git-fetch[1]git-push[1]

remote repository (远程仓库)

用于跟踪同一项目但在其他地方的仓库。要与远程仓库通信,请参阅 fetchpush

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

用于跟踪来自另一个仓库的更改的引用。它通常看起来像refs/remotes/foo/bar (表明它跟踪名为bar的远程仓库foo中的一个分支),并与配置的 fetch 引用规范的右侧匹配。 远程跟踪分支不应包含直接修改或对其进行本地提交。

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 的上下文中,Lisp 标签称为 对象类型)。标签最常用于标记提交祖先中的特定点。

tag object (标签对象)

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

topic branch (主题分支)

由开发人员用来识别概念开发线的常规 Git 分支。 由于分支非常容易且成本低廉,因此通常需要有几个小分支,每个分支都包含定义非常明确的概念或小的增量但相关的更改。

trailer (尾部)

键值元数据。 尾部可以选择性地位于提交消息的末尾。 在其他社区中,可能称为“页脚”或“标签”。 请参阅 git-interpret-trailers[1]

tree (树)

工作树,或 树对象 以及依赖的 blob 和树对象(即工作树的存储表示)。

tree object (树对象)

包含文件名和模式列表以及对关联的 blob 和/或树对象的引用的对象 等价于 目录

tree-ish (也写作 treeish)

一个树对象 或一个可以递归地解除引用为树对象的对象。 解除引用提交对象会产生与修订版本的顶层目录对应的树对象。 以下都是 tree-ish:commit-ish、树对象、指向树对象的标签对象、指向指向树对象的标签对象的标签对象,等等。

unborn (未诞生)

HEAD 可以指向一个尚不存在并且没有任何提交的分支,这样的分支称为未诞生分支。 用户遇到未诞生分支的最典型方式是创建一个新仓库而不从其他地方克隆。 HEAD 将指向尚未诞生的main(或master,具体取决于您的配置)分支。 此外,某些操作可以使用它们的孤立选项让您进入一个未诞生分支。

unmerged index (未合并的索引)

包含未合并的索引条目索引

unreachable object (不可达对象)

分支标签或任何其他引用不可达对象

upstream branch (上游分支)

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

working tree (工作树)

实际检出的文件的树。 工作树通常包含 HEAD 提交的树的内容,以及您已进行但尚未提交的任何本地更改。

worktree (工作区)

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

编写一个关于使用管道和编写脚本的章节。

备用方案、clone -reference 等。

scroll-to-top