Git
英语 ▾ 主题 ▾ 最新版本 ▾ 用户手册 最后更新于 2.46.0

简介

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

本手册旨在供具备基本 UNIX 命令行技能但没有 Git 相关知识的人阅读。

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

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

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

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

$ man git-clone

$ git help clone

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

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

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

存储库和分支

如何获取 Git 存储库

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

最佳方法是使用 git-clone[1] 命令下载现有存储库的副本。如果您还没有项目,以下是一些有趣的示例

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

对于大型项目,初始克隆可能需要花费较长时间,但您只需要克隆一次。

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

如何检出项目的不同版本

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

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

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

$ git branch
* master

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

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

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

标签预计始终指向项目的同一版本,而头预计随着开发的进行而前进。

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

$ git switch -c new v2.6.13

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

$ git branch
  master
* new

如果您决定希望查看版本 2.6.17,您可以使用以下命令修改当前分支以指向 v2.6.17:

$ git reset --hard v2.6.17

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

理解历史:提交

git-show[1] 命令显示当前分支上的最新提交

$ git show
commit 17cf781661e6d38f737f15f53ab552f1e95960d7
Author: Linus Torvalds <[email protected].(none)>
Date:   Tue Apr 19 14:11:06 2005 -0700

    Remove duplicate getenv(DB_ENVIRONMENT) call

    Noted by Tony Luck.

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

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

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

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

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

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

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

每个提交(除项目中的第一个提交外)还有一个父提交,它显示了在此提交之前发生了什么。沿着父级的链最终将带您回到项目的开始。

但是,提交并不形成一个简单的列表;Git 允许开发线分叉然后重新合并,开发线重新合并的点称为“合并”。因此,表示合并的提交可以有多个父级,每个父级都表示导致该点的开发线上的最新提交。

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

在下文中,我们说提交 X 从提交 Y “可达”,如果提交 X 是提交 Y 的祖先。等效地,你可以说 Y 是 X 的后代,或者说有一条从提交 Y 到提交 X 的父级链。

理解历史:历史图

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

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

如果我们需要讨论特定的提交,字符“o”可能会被替换为其他字母或数字。

理解历史:什么是分支?

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

但是,当不会产生混淆时,我们通常将“分支”一词用于分支和分支头。

操作分支

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

git branch

列出所有分支。

git branch <branch>

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

git branch <branch> <start-point>

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

git branch -d <branch>

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

git branch -D <branch>

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

git switch <branch>

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

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

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

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

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

在不创建新分支的情况下检查旧版本

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

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

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

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

  git switch -c new_branch_name

HEAD is now at 427abfa Linux v2.6.17

然后 HEAD 指向提交的 SHA-1 而不是分支,并且 git branch 显示你不再处于分支状态。

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

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

这是一种无需为新分支指定名称即可检出特定版本的方法。如果你以后决定这样做,你仍然可以为此版本创建新的分支(或标签)。

检查远程仓库的分支

你在克隆时创建的“master”分支是你在克隆的仓库中的 HEAD 的副本。但是,该仓库可能还包含其他分支,并且你的本地仓库会保留跟踪这些远程分支的每个分支,称为远程跟踪分支,你可以使用 -r 选项查看 git-branch[1]

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

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

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

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

你还可以直接检出 origin/todo 以检查它或编写一次性补丁。请参阅 分离的头

请注意,“origin”这个名称只是 Git 默认用来引用你从中克隆的仓库的名称。

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

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

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

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

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

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

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

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

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

使用 git fetch 更新仓库

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

git-fetch 命令(无参数)将更新所有远程跟踪分支到在原始仓库中找到的最新版本。它不会触及你的任何分支——即使是在克隆时为你创建的“master”分支。

从其他仓库获取分支

你还可以使用 git-remote[1] 跟踪来自你克隆的仓库以外的仓库的分支。

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

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

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

如果你稍后运行 git fetch <remote>,则命名 <remote> 的远程跟踪分支将更新。

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

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

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

探索 Git 历史

Git 最好被认为是存储文件集合历史的工具。它通过存储文件层次结构内容的压缩快照以及显示这些快照之间关系的“提交”来做到这一点。

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

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

如何使用 bisect 查找回归

假设你的项目的 2.6.18 版本有效,但“master”上的版本崩溃了。有时,查找此类回归原因的最佳方法是对项目的历史进行蛮力搜索,以查找导致问题的确切提交。git-bisect[1] 命令可以帮助你做到这一点。

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

如果你在此时运行 git branch,你会看到 Git 已将你临时移至“(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]手册页的“指定修订版本”部分。一些示例

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

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

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

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

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

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

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

获取的提交仍可从FETCH_HEAD访问。

当我们讨论合并时,我们还会看到特殊的名称MERGE_HEAD,它指的是我们正在合并到当前分支中的其他分支。

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

$ git rev-parse origin
e05db0fd4f31dde7005f075a84f96b360d05984b

创建标签

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

$ git tag stable-1 1b2e1d63ff

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

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

浏览修订版本

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

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

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

$ git log v2.5.. Makefile fs/

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

$ git log -p

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

请注意,git log从最新的提交开始,并向后遍历父提交;但是,由于Git历史记录可能包含多条独立的开发线,因此列出提交的特定顺序可能在某种程度上是任意的。

生成差异

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

$ git diff master..test

这将生成两个分支顶端之间的差异。如果您希望查找从它们的共同祖先开始的差异以进行测试,则可以使用三个点而不是两个点

$ git diff master...test

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

$ git format-patch master..test

将为从test开始但不是从master开始可以访问的每个提交生成一个包含补丁的文件。

查看旧文件版本

您始终可以通过首先检出正确的修订版本来查看文件的旧版本。但有时更方便能够查看单个文件的旧版本而不必检出任何内容;此命令可以做到这一点

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

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

示例

计算分支上的提交次数

假设您想知道自mybranchorigin分离以来,您在mybranch上进行了多少次提交

$ 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生成一个压缩的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 '[email protected]'

这会将以下内容添加到您主目录中名为.gitconfig的文件中

[user]
	name = Your Name Comes Here
	email = [email protected]

有关配置文件的详细信息,请参阅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] 创建提交、查看索引和工作树文件的更改,以及单独选择差异块以包含在索引中(通过右键单击差异块并选择“暂存差异块以提交”)。

创建良好的提交信息

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

忽略文件

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

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

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

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

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

如何合并

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

$ git merge branchname

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

合并是通过组合 branchname 中进行的更改以及自它们的历史记录分叉以来在您当前分支中直至最新提交所进行的更改来完成的。当此组合干净地完成时,工作树将被合并结果覆盖,或者当此组合导致冲突时,将被半合并的结果覆盖。因此,如果您有未提交的更改触及与合并影响的文件相同的那些文件,则 Git 将拒绝继续。大多数时候,您需要在合并之前提交更改,如果您没有这样做,则 git-stash[1] 可以将这些更改在您进行合并时移开,并在之后重新应用它们。

如果更改足够独立,Git 将自动完成合并并提交结果(或者在快进的情况下重用现有提交,请参见下文)。另一方面,如果存在冲突——例如,如果在远程分支和本地分支中以两种不同的方式修改了相同的文件——则会发出警告;输出可能如下所示

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

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

如果您使用 gitk 检查生成的提交,您会看到它有两个父级,一个指向当前分支的顶部,另一个指向另一个分支的顶部。

解决合并

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

索引中会对有冲突的文件进行特殊标记,因此,除非您解决问题并更新索引,否则 git-commit[1] 将失败

$ git commit
file.txt: needs merge

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

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

您需要做的就是编辑文件以解决冲突,然后

$ git add file.txt
$ git commit

请注意,提交信息将已为您填写一些有关合并的信息。通常,您可以直接使用此默认消息而不进行更改,但如果需要,您可以添加自己的其他注释。

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

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

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

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

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

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

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

当您要求 git-diff[1] 显示冲突时,它会在工作树中冲突的合并结果与阶段 2 和 3 之间运行三方 diff,以仅显示内容来自双方的混合块(换句话说,当块的合并结果仅来自阶段 2 时,该部分没有冲突,不会显示。阶段 3 也是如此)。

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

在以显而易见的方式解决冲突(但在更新索引之前),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

请注意,diff 显示我们删除了冲突标记和内容行的两个版本,并改写了“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 以避免在不方便时自动压缩。

确保可靠性

检查仓库是否存在损坏

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

恢复丢失的更改

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

默认情况下,reflog 会保留 30 天,之后可能会被修剪。请参阅 git-reflog[1]git-gc[1] 以了解如何控制此修剪,并参阅 gitrevisions[7] 的“指定修订版本”部分以获取详细信息。

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

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

与他人共享开发

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

更一般地,从远程跟踪分支创建的分支将默认从该分支拉取。请参阅 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 可以包含一个初始的“封面信”。您可以在 format-patch 在提交消息之后但在补丁本身之前放置的三条虚线之后插入有关各个补丁的注释。如果您使用 git notes 跟踪您的封面信材料,git format-patch --notes 将以类似的方式包含提交的注释。

然后,您可以将这些导入您的邮件客户端并手动发送。但是,如果您要一次发送很多内容,您可能更愿意使用 git-send-email[1] 脚本来自动化此过程。首先咨询项目的邮件列表以确定他们提交补丁的要求。

导入项目的补丁

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

$ git am -3 patches.mbox

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

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

$ git am --continue

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

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

公共 Git 仓库

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

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

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

或 ssh URL

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

对于开发人员很少的项目,或用于同步一些私有仓库,这可能就是您需要的全部。

但是,更常见的方法是为其他人拉取更改维护一个单独的公共仓库(通常在不同的主机上)。这通常更方便,并允许您将正在进行的私有工作与公开可见的工作清晰地分开。

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

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

我们将在以下部分说明如何执行此操作。

设置公共仓库

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

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

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

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

通过 Git 协议导出 Git 仓库

这是首选方法。

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

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

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

通过 HTTP 导出 git 仓库

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

您需要做的就是在 Web 服务器导出的目录中放置新创建的裸 Git 仓库,并进行一些调整以向 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

(有关使用WebDAV的稍微更复杂的设置,该设置还允许通过HTTP推送,请参阅setup-git-server-over-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为维护者提供了一种简单的方法来将此工作委托给其他维护者,同时仍然允许可选地审查传入的更改。

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

  • "提交者"的中央组的缺乏意味着对谁"在内"和谁"在外"进行正式决策的需求减少了。

允许网页浏览仓库

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

使用instaweb时,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如何在他作为Linux内核IA64架构维护者的角色中使用Git。

他使用两个公共分支

  • 一个“测试”树,最初将补丁放置到其中,以便在与其他正在进行的开发集成时获得一些曝光。Andrew可以随时从该树中拉取到-mm中。

  • 一个“发布”树,经过测试的补丁将被移动到该树中进行最终的健全性检查,并作为将其发送到上游Linus(通过向他发送“请拉取”请求)的工具。

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

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

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

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

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

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

可以使用git-pull[1]轻松地使它们保持最新。

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

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

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

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

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

$ git push mytree

或仅使用以下命令推送测试和发布分支之一

$ git push mytree test

$ git push mytree release

现在要应用社区的一些补丁。为保存此补丁(或相关的补丁组)的分支选择一个简短的名称,并从Linus分支的最新稳定标签创建新分支。为您的分支选择稳定的基础将:1)帮助您:避免包含不相关且可能测试不足的更改 2)帮助将来使用git bisect查找问题的错误跟踪器

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

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

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

当您对更改的状态感到满意时,可以将其合并到“测试”分支中,以准备将其公开

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

您不太可能在此处遇到任何冲突……但是,如果您在此步骤上花费了一些时间并且还从上游拉取了新版本,则可能会遇到冲突。

一段时间后,当有足够的时间并完成测试后,您可以将相同的分支拉取到release树中,准备发送到上游。在这里,您将看到将每个补丁(或补丁系列)保存在其自己的分支中的价值。这意味着可以按任何顺序将补丁移动到release树中。

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

一段时间后,您将拥有多个分支,尽管您为每个分支选择了合适的名称,但您可能会忘记它们的作用或它们的状态。要了解特定分支中有哪些更改,请使用

$ git log linux..branchname | git shortlog

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

$ git log test..branchname

$ git log release..branchname

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

一旦补丁完成整个循环(从测试移动到发布,然后由Linus拉取,最后返回到您的本地origin/master分支),则不再需要此更改的分支。当以下输出为空时,您可以检测到这一点

$ git log origin..branchname

此时可以删除该分支

$ git branch -d branchname

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

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

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

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

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

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

pname=$0

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

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

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

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

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

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

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

重写历史记录和维护补丁系列

通常,提交仅添加到项目中,绝不会被删除或替换。Git的设计基于此假设,违反它会导致Git的合并机制(例如)执行错误的操作。

但是,有一种情况下违反此假设可能很有用。

创建完美的补丁系列

假设您是大型项目的贡献者,并且想要添加一个复杂的功能,并以一种使其他开发人员能够轻松阅读您的更改、验证它们是否正确以及了解您进行每个更改的原因的方式将其呈现给他们。

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

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

所以理想的情况通常是生成一系列补丁,使得

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

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

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

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

我们将介绍一些可以帮助你做到这一点的工具,解释如何使用它们,然后解释由于你正在重写历史而可能出现的一些问题。

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

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

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

你没有执行任何合并到 mywork 中的操作,所以它只是 origin 之上的一系列简单线性补丁

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

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

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

此时,你可以使用 pull 将你的更改合并回来;结果将创建一个新的合并提交,如下所示

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

但是,如果你希望将 mywork 中的历史保持为一系列简单的提交,而没有任何合并,则可以选择使用 git-rebase[1]

$ git switch mywork
$ git rebase origin

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

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

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

$ git rebase --continue

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

在任何时候,你都可以使用 --abort 选项中止此过程并将 mywork 返回到开始重新设置基准之前所处的状态

$ git rebase --abort

如果你需要重新排序或编辑分支中的多个提交,使用 git rebase -i 可能更容易,它允许你重新排序和压缩提交,以及在重新设置基准期间将它们标记为单独编辑。有关详细信息,请参阅 使用交互式 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 重新设置基于你想要保留的最后一个提交。例如,如果你想重新排序最后 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 无法知道新头部是旧头部的更新版本;它将这种情况与两个开发人员并行独立地在旧头部和新头部上完成工作的情况完全相同。此时,如果有人尝试将新头部合并到他们的分支中,Git 将尝试合并两条(旧的和新的)开发路线,而不是尝试用新的替换旧的。结果可能出乎意料。

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

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

为什么对合并提交进行二分查找可能比对线性历史进行二分查找更难

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

想象一下这段历史

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

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

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

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

然而,C 处的代码已损坏,因为在下游开发线上添加的调用者尚未转换为上游开发线上引入的新语义。因此,如果你只知道 D 存在问题,Z 很好,并且 git-bisect[1] 将 C 识别为罪魁祸首,那么你将如何弄清楚问题是由于这种语义更改造成的呢?

git bisect 的结果是非合并提交时,你通常应该能够通过检查该提交来发现问题。开发人员可以通过将他们的更改分解成小的、自包含的提交来简化这一点。然而,在上述情况下,这将无济于事,因为问题从任何单个提交的检查中都不明显;相反,需要全局查看开发情况。更糟糕的是,问题函数中的语义更改可能只是上游开发更改的一小部分。

另一方面,如果你没有在 C 处合并,而是将 Z 到 B 之间的所有历史重新设置基于 A,那么你将得到以下线性历史

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

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

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

高级分支管理

获取单个分支

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

$ git fetch origin todo:my-todo-work

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

你还可以从其他存储库中获取分支;所以

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

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

git fetch 和快进

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

快进看起来像这样

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

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

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

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

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

强制 git fetch 执行非快进更新

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

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

注意添加了+符号。或者,您可以使用-f标志强制更新所有已获取的分支,例如

$ git fetch -f origin

请注意,旧版本 example/master 指向的提交可能会丢失,如上一节所述。

配置远程跟踪分支

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

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

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

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

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

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

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

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

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

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

Git 概念

Git 建立在少量简单但强大的思想之上。虽然在不了解它们的情况下也可以完成工作,但如果您理解了它们,您会发现 Git 更直观。

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

对象数据库

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

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

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

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

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

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

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

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

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

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

更详细地介绍对象类型

提交对象

“commit”对象将 tree 的物理状态与其获取方式和原因相关联。使用git-show[1]git-log[1]--pretty=raw选项来检查您最喜欢的提交

$ git show -s --pretty=raw 2be7fcb476
commit 2be7fcb4764f2dbcee52635b91fedb1b3dcf7ab4
tree fb3a8bdd0ceddd019615af4d57a53f43d8cee2bf
parent 257a84d9d02e90447b149af58b271c19405edb6a
author Dave Watson <[email protected]> 1187576872 -0400
committer Junio C Hamano <[email protected]> 1187591163 -0700

    Fix misspelling of 'suppress' in docs

    Signed-off-by: Junio C Hamano <[email protected]>

如您所见,提交由以下内容定义:

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

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

  • 一个作者:负责此更改的人员的姓名及其日期。

  • 一个提交者:实际创建提交的人员的姓名及其完成日期。这可能与作者不同,例如,如果作者是编写补丁并将其发送给用于创建提交的人员的人。

  • 描述此提交的注释。

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

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

Tree 对象

用途广泛的git-show[1]命令也可用于检查 tree 对象,但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
...

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

对象类型可以是 blob,表示文件的内容,也可以是另一个 tree,表示子目录的内容。由于 tree 和 blob(与所有其他对象一样)都以其内容的 SHA-1 哈希值命名,因此当且仅当两个 tree 的内容(包括递归地包括所有子目录的内容)相同时,它们才具有相同的 SHA-1 名称。这允许 Git 快速确定两个相关 tree 对象之间的差异,因为它可以忽略任何具有相同对象名称的条目。

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

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

Blob 对象

您可以使用git-show[1]检查 blob 的内容;例如,取自上面 tree 中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 对象。对象完全独立于其在目录树中的位置,并且重命名文件不会更改与该文件关联的对象。

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

信任

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

同样,您只需要信任顶级 tree 对象的 SHA-1 名称即可信任它引用的整个目录的内容,如果您从可信源接收提交的 SHA-1 名称,那么您可以轻松验证可通过该提交的父级访问的整个提交历史记录,以及所有这些提交引用的 tree 的所有内容。

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

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

为了协助实现这一点,Git 还提供了 tag 对象……

Tag 对象

tag 对象包含一个对象、对象类型、tag 名称、创建 tag 的人员(“标记者”)的名称以及消息,该消息可能包含签名,如使用git-cat-file[1]所示。

$ git cat-file tag v1.5.0
object 437b1b20df4b356c9342dac8d38849f24ef44f27
type commit
tag v1.5.0
tagger Junio C Hamano <[email protected]> 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 如何有效存储对象:打包文件

新创建的对象最初创建在以对象 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]命令有时会抱怨悬空对象。它们不是问题。

悬空对象的最常见原因是您已重新设置分支的基础,或者您已从重新设置分支基础的其他人员处拉取——请参阅重写历史记录和维护补丁系列。在这种情况下,原始分支的旧头部仍然存在,它指向的所有内容也仍然存在。分支指针本身只是不存在,因为您已将其替换为另一个分支指针。

还有一些其他情况会导致悬空对象。例如,由于你对一个文件执行了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 本身没有 bug,硬件或操作系统错误仍然可能损坏数据。

针对此类问题的首要防御措施是备份。你可以使用克隆或仅使用 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

并在整个内容中查找丢失对象(4b9458b)的 sha。这取决于你,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 1.5.3 版本起,即可使用 git-submodule[1] 命令。使用 Git 1.5.2 的用户可以在仓库中查找子模块提交并手动检出;更早的版本根本无法识别子模块。

要了解子模块支持的工作原理,请创建四个示例仓库,稍后可将其用作子模块。

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

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

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

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

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

git submodule add <repo> <path> 命令执行了以下几件事:

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

  • 它将子模块的克隆路径添加到 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>

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

索引 → 工作目录

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

但是,如果您决定跳转到新版本,或检出其他人的版本,或只是恢复以前的树,则可以使用 read-tree 填充索引文件,然后需要使用以下命令检出结果:

$ git checkout-index filename

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

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

最后,还有一些零碎的东西并非纯粹地从一种表示形式转换为另一种表示形式。

将所有内容联系起来

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

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

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

您可以通过提供描述提交时状态的树和父级列表来创建提交对象:

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

然后在 stdin 上提供提交的原因(通过管道或文件的重定向,或通过在 tty 上键入)。

git commit-tree 将返回表示该提交的对象的名称,您应该将其保存以供以后使用。通常,您会提交新的 HEAD 状态,虽然 Git 不关心您在哪里保存关于该状态的注释,但在实践中,我们倾向于将结果写入 .git/HEAD 指向的文件,以便我们始终可以看到最后一个提交的状态是什么。

这是一张说明各个部分如何组合在一起的图片。

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

检查数据

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

$ git cat-file -t <objectname>

显示对象的类型,一旦您有了类型(通常在您找到对象的位置是隐式的),您可以使用:

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

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

查看“提交”对象特别有启发性,因为这些对象往往很小且相当容易理解。特别是,如果您遵循在 .git/HEAD 中拥有顶级提交名称的约定,则可以执行以下操作:

$ git cat-file commit HEAD

查看顶级提交是什么。

合并多个树

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

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

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

$ git merge-base <commit1> <commit2>

这会打印出两个分支共同基于的提交的名称。现在你应该查找这些提交的树对象,你可以很容易地使用以下命令做到这一点:

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

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

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

要执行合并,请执行以下操作:

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

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

合并多个树,继续

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

你可以使用git ls-files --unmerged命令检查这种索引状态。例如

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

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

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

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

这会将合并结果保存在hello.c~2文件中,如果存在冲突,还会包含冲突标记。在验证合并结果有意义后,你可以告诉Git此文件的最终合并结果是什么:

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

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

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

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

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

修改Git

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

对象存储格式

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

无论对象类型如何,所有对象都具有以下特征:它们都使用zlib进行压缩,并具有一个头部,该头部不仅指定了它们的类型,还提供了有关对象中数据的大小信息。值得注意的是,用于命名对象的SHA-1哈希是原始数据加上此头的哈希,因此sha1sum 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) 对象成功解压为字节流,形成<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的传统)是一堆非常简单的程序,你可以在脚本中使用它们,将一个程序的输出管道到另一个程序。这对于初始开发来说证明是好的,因为测试新事物更容易。但是,最近许多这些部分已成为内置函数,并且一些核心已被“库化”,即出于性能、可移植性原因以及避免代码重复而放入libgit.a中。

现在,你知道索引是什么(并在read-cache-ll.h中找到相应的data structure),并且只有几种对象类型(blob、tree、commit和tag)继承了它们来自struct object的公共结构,这是它们的第一个成员(因此,你可以将例如(struct object *)commit转换为与&commit->object相同的效果,即获取对象名称和标志)。

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

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

这仅仅是为了让你进入Git最“库化”的部分:修订版walker。

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

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

这意味着什么?

git rev-list是修订版walker的原始版本,它始终将修订版列表打印到stdout。它仍然有效,并且需要有效,因为大多数新的Git命令都以使用git rev-list的脚本开始。

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

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

git rev-parse的原始工作现在由setup_revisions()函数完成,该函数解析修订版和修订版walker的常用命令行选项。此信息存储在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文件一样命名的命令必须在Makefile中的BUILT_INS中列出。

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

这里再次是一个暂停的好时机。

第三课是:学习代码。真的,这是了解Git组织结构的最佳方式(在你了解基本概念之后)。

因此,考虑一下你感兴趣的事情,比如“如何仅知道它的对象名称就可以访问一个blob?”。第一步是找到一个你可以用它来做这件事的Git命令。在这个例子中,它是git showgit cat-file

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

  • 是底层命令,并且

  • 即使在最初的提交中也存在(它实际上只经历了大约 20 次修订,作为cat-file.c,在成为内置函数时重命名为builtin/cat-file.c,然后版本少于 10 个)。

所以,查看builtin/cat-file.c,搜索cmd_cat_file()并查看它做了什么。

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

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

这里有两点值得关注

  • get_sha1()成功时返回 0。这可能会让一些新的 Git 黑客感到惊讶,但在 UNIX 中有一个悠久的传统,即在发生不同错误时返回不同的负数——成功时返回 0。

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

您将在整个代码中看到这两件事。

现在,进入重点

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

这就是您读取 blob 的方法(实际上,不仅是 blob,而是任何类型的对象)。要了解函数read_object_with_reference()是如何工作的,请找到它的源代码(在 Git 存储库中类似于git grep read_object_with | grep ":[a-z]"),并阅读源代码。

要了解如何使用结果,只需继续阅读cmd_cat_file()中的内容即可。

        write_or_die(1, buf, size);

有时,您不知道在哪里查找功能。在许多此类情况下,搜索git log的输出,然后git show相应的提交会有所帮助。

示例:如果您知道存在git bundle的一些测试用例,但记不起它在哪里(是的,您可以git grep bundle t/,但这没有说明重点!)

$ git log --no-merges t/

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

$ git show 18449ab0

瞧。

另一个示例:了解如何将某个脚本设为内置函数

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

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

Git 词汇表

Git 解释

备用对象数据库

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

裸存储库

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

Blob 对象

未类型化的对象,例如文件的內容。

分支

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

缓存

已过时,请使用:索引

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

变更集

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

检出

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

樱桃采摘

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

干净

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

提交

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

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

提交图概念、表示和用法

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

提交图文件

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

提交对象

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

提交类(也称为提交式)

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

核心 Git

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

DAG

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

悬空对象

一个不可达对象,即使从其他不可达对象也无法到达;悬空对象在存储库中的任何引用或对象中都没有对其的引用。

解除引用

引用符号引用:访问符号引用指向的引用的操作。递归解除引用涉及对结果引用重复上述过程,直到找到非符号引用。

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

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

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

分离的 HEAD

通常,HEAD存储分支的名称,并且对 HEAD 表示的历史记录进行操作的命令对导致 HEAD 指向的分支顶端的历史记录进行操作。但是,Git 还允许您检出任意提交,该提交不一定属于任何特定分支的顶端。处于这种状态的 HEAD 称为“分离的”。

请注意,在 HEAD 分离状态下,操作当前分支历史记录的命令(例如,git commit 用于在当前分支的基础上构建新的历史记录)仍然有效。这些命令会更新 HEAD 指向更新历史记录的顶端,而不会影响任何分支。显然,更新或查询当前分支信息的命令(例如,git branch --set-upstream-to 用于设置当前分支与哪个远程跟踪分支集成)则无效,因为在这种状态下没有(真正的)当前分支可供查询。

目录

使用 "ls" 命令得到的列表 :-)

脏状态

如果一个工作区包含尚未提交到当前分支的修改,则称该工作区处于“脏状态”。

恶意合并

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

快进合并

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

获取

获取分支是指从远程仓库获取分支的头部引用,找出本地对象数据库中缺少哪些对象,并获取这些对象。另请参阅git-fetch[1]

文件系统

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

Git 归档

仓库的同义词(针对档案人员)。

gitfile

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

嫁接

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

请注意,嫁接机制已经过时,并且可能导致在仓库之间传输对象时出现问题;请参阅git-replace[1],了解更灵活、更强大的系统来实现相同的功能。

哈希值

在 Git 上下文中,是对象名的同义词。

头部

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

HEAD

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

头部引用

头部的同义词。

钩子

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

索引

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

索引条目

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

主分支

默认开发分支。每当你创建一个 Git 仓库时,都会创建一个名为“master”的分支,并成为活动分支。在大多数情况下,它包含本地开发内容,但这仅仅是约定俗成,并非强制要求。

合并

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

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

对象

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

对象数据库

存储一组“对象”,单个对象由其对象名标识。对象通常位于$GIT_DIR/objects/中。

对象标识符 (oid)

对象名的同义词。

对象名

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

对象类型

描述对象类型的标识符之一,“提交”、“”、“标签”或“Blob”。

章鱼合并

合并多个分支

孤立分支

切换到一个尚不存在的分支(即未创建的分支)。在此操作之后,首次创建的提交将成为一个没有父提交的提交,开始新的历史记录。

origin

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

覆盖

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

打包

一组已压缩到一个文件中的对象(以节省空间或有效传输)。

打包索引

打包中对象的标识符列表和其他信息,有助于有效访问打包的内容。

路径规范

用于限制 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(魔术签名:/)使模式从工作区的根目录开始匹配,即使你在子目录中运行命令也是如此。

字面量

模式中的通配符,例如*?,将被视为字面字符。

icase

不区分大小写的匹配。

glob

Git 将模式视为适合由 fnmatch(3) 使用 FNM_PATHNAME 标记进行处理的 shell glob:模式中的通配符将不匹配路径名中的 /。例如,“Documentation/*.html”匹配“Documentation/git.html”,但不匹配“Documentation/ppc/ppc.html”或“tools/perf/Documentation/perf.html”。

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

  • 以“**”开头后跟一个斜杠表示匹配所有目录。例如,“**/foo”匹配任何位置的文件或目录“foo”,与模式“foo”相同。“**/foo/bar”匹配任何位置直接位于目录“foo”下的文件或目录“bar”。

  • 以“/**”结尾表示匹配内部的所有内容。例如,“abc/**”匹配目录“abc”内部的所有文件,相对于.gitignore文件的位置,深度无限。

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

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

    Glob 魔术与字面魔术不兼容。

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

可达性位图存储有关包文件或多包索引 (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

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

refspec

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

remote repository

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

remote-tracking branch

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

repository

一个引用的集合,以及一个对象数据库,其中包含所有可从引用访问的对象,可能还附带一个或多个瓷器的元数据。一个仓库可以通过备用机制与其他仓库共享对象数据库。

resolve

手动修复因自动合并失败而留下的内容。

修订版

提交(名词)的同义词。

回退

丢弃开发过程中的部分内容,即,将HEAD指向更早的修订版

SCM

源代码管理(工具)。

SHA-1

"安全散列算法 1";一种加密散列函数。在 Git 的上下文中,用作对象名称的同义词。

浅克隆

主要等同于浅仓库,但该短语更明确地表明它是通过运行 git clone --depth=... 命令创建的。

浅仓库

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

暂存条目

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

子模块

一个仓库,它在另一个仓库(后者称为主项目)中保存独立项目的历史记录。

主项目

一个仓库,在其工作树中将其他项目的仓库作为子模块引用。主项目知道包含的子模块的提交对象的名称(但不保存它们的副本)。

符号引用

符号引用:它不包含SHA-1 ID 本身,而是采用 ref: refs/some/thing 的格式,并且在引用时,它会递归地解除引用到该引用。HEAD 是符号引用的一个主要示例。符号引用使用git-symbolic-ref[1]命令进行操作。

标签

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

标签对象

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

主题分支

开发人员用来识别概念性开发线的常规 Git 分支。由于分支非常简单且成本低廉,因此通常希望拥有多个小型分支,每个分支包含定义明确的概念或小的增量但相关的更改。

工作树树对象以及相关的blob和树对象(即,工作树的存储表示)。

树对象

一个对象,包含文件名称和模式列表以及对关联的 blob 和/或树对象的引用。等同于目录

树形(也称树形)

一个树对象或一个可以递归解除引用到树对象的对象。解除引用提交对象会生成对应于修订版顶层目录的树对象。以下是所有树形:提交形、树对象、指向树对象的标签对象、指向指向树对象的标签对象的标签对象等。

未诞生

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

未合并索引

包含未合并索引条目索引

不可达对象

分支标签或任何其他引用都无法到达对象

上游分支

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

工作树

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

工作区

一个仓库可以有零个(即裸仓库)或一个或多个工作区附加到它。一个“工作区”由一个“工作树”和仓库元数据组成,其中大部分在单个仓库的其他工作区之间共享,而有些则在每个工作区中单独维护(例如,索引、HEAD 和伪引用,如 MERGE_HEAD,每个工作区的引用和每个工作区的配置文件)。

附录 A:Git 快速参考

这是主要命令的快速总结;前面的章节更详细地解释了它们的工作原理。

创建新的仓库

从 tarball 创建

$ tar xzf project.tar.gz
$ cd project
$ git init
Initialized empty Git repository in .git/
$ git add .
$ git commit

从远程仓库创建

$ git clone git://example.com/pub/project.git
$ cd project

管理分支

$ git branch			# list all local branches in this repo
$ git switch test	        # switch working directory to branch "test"
$ git branch new		# create branch "new" starting at current HEAD
$ git branch -d new		# delete branch "new"

不要将新分支基于当前 HEAD(默认值),请使用

$ git branch new test    # branch named "test"
$ git branch new v2.6.15 # tag named v2.6.15
$ git branch new HEAD^   # commit before the most recent
$ git branch new HEAD^^  # commit before that
$ git branch new test~10 # ten commits before tip of branch "test"

同时创建和切换到新分支

$ git switch -c new v2.6.15

更新和检查您克隆的仓库中的分支

$ git fetch		# update
$ git branch -r		# list
  origin/master
  origin/next
  ...
$ git switch -c masterwork origin/master

从不同的仓库获取分支,并在您的仓库中为其指定一个新名称

$ git fetch git://example.com/project.git theirbranch:mybranch
$ git fetch git://example.com/project.git v2.6.15:mybranch

保留您定期使用的仓库列表

$ git remote add example git://example.com/project.git
$ git remote			# list remote repositories
example
origin
$ git remote show example	# get details
* remote example
  URL: git://example.com/project.git
  Tracked remote branches
    master
    next
    ...
$ git fetch example		# update branches from example
$ git branch -r			# list all remote branches

探索历史

$ gitk			    # visualize and browse history
$ git log		    # list all commits
$ git log src/		    # ...modifying src/
$ git log v2.6.15..v2.6.16  # ...in v2.6.16, not in v2.6.15
$ git log master..test	    # ...in branch test, not in branch master
$ git log test..master	    # ...in branch master, but not in test
$ git log test...master	    # ...in one branch, not in both
$ git log -S'foo()'	    # ...where difference contain "foo()"
$ git log --since="2 weeks ago"
$ git log -p		    # show patches as well
$ git show		    # most recent commit
$ git diff v2.6.15..v2.6.16 # diff between two tagged versions
$ git diff v2.6.15..HEAD    # diff with current head
$ git grep "foo()"	    # search working directory for "foo()"
$ git grep v2.6.15 "foo()"  # search old tree for "foo()"
$ git show v2.6.15:a.txt    # look at old version of a.txt

搜索回归

$ git bisect start
$ git bisect bad		# current version is bad
$ git bisect good v2.6.13-rc2	# last known good revision
Bisecting: 675 revisions left to test after this
				# test here, then:
$ git bisect good		# if this revision is good, or
$ git bisect bad		# if this revision is bad.
				# repeat until done.

进行更改

确保 Git 知道该归咎于谁

$ cat >>~/.gitconfig <<\EOF
[user]
	name = Your Name Comes Here
	email = [email protected]
EOF

选择要包含在下次提交中的文件内容,然后进行提交

$ git add a.txt    # updated file
$ git add b.txt    # new file
$ git rm c.txt     # old file
$ git commit

或者,一步准备并创建提交

$ git commit d.txt # use latest content only of d.txt
$ git commit -a	   # use latest content of all tracked files

合并

$ git merge test   # merge branch "test" into the current branch
$ git pull git://example.com/project.git master
		   # fetch and merge in remote branch
$ git pull . test  # equivalent to git merge test

共享您的更改

导入或导出补丁

$ git format-patch origin..HEAD # format a patch for each commit
				# in HEAD but not in origin
$ git am mbox # import patches from the mailbox "mbox"

在不同的 Git 仓库中获取分支,然后合并到当前分支

$ git pull git://example.com/project.git theirbranch

在合并到当前分支之前,将获取的分支存储到本地分支

$ git pull git://example.com/project.git theirbranch:mybranch

在本地分支上创建提交后,使用您的提交更新远程分支

$ git push ssh://example.com/project.git mybranch:theirbranch

当远程分支和本地分支都名为“test”时

$ git push ssh://example.com/project.git test

常用远程仓库的快捷版本

$ git remote add example ssh://example.com/project.git
$ git push example test

仓库维护

检查损坏

$ git fsck

重新压缩,删除未使用的废弃内容

$ git gc

附录 B:本手册的注释和待办事项列表

待办事项列表

这是一个正在进行的工作。

基本要求

  • 它必须能够按顺序从头到尾阅读,供那些智力正常且具备基本 UNIX 命令行知识,但对 Git 没有特殊了解的人阅读。如有必要,应在出现时专门提及任何其他先决条件。

  • 尽可能地,章节标题应清楚地描述它们解释如何执行的任务,使用不需要太多知识的语言:例如,“将补丁导入项目”,而不是“git am 命令”。

考虑如何创建清晰的章节依赖关系图,以便人们可以访问重要主题,而不必一定阅读中间的所有内容。

扫描 Documentation/ 以查找遗漏的其他内容;特别是

  • 操作方法

  • technical/ 中的一些内容?

  • 钩子

  • git[1] 中的命令列表

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

扫描手册页以查看是否有任何手册页假设的背景知识超过本手册提供的背景知识。

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

在适当的地方,添加指向词汇表的交叉引用。

添加关于与其他版本控制系统(包括 CVS、Subversion 和一系列发行版 tarball 的导入)一起工作的部分。

编写关于使用底层工具和编写脚本的章节。

备用,克隆 -reference 等。

scroll-to-top