设置和配置
获取和创建项目
基本快照
分支与合并
共享和更新项目
检查和比较
打补丁
调试
电子邮件
外部系统
服务器管理
指南
管理
底层命令
- 2.51.1 → 2.52.0 无更改
-
2.51.0
2025-08-18
- 2.48.1 → 2.50.1 无更改
-
2.48.0
2025-01-10
- 2.46.1 → 2.47.3 无更改
-
2.46.0
2024-07-29
- 2.45.1 → 2.45.4 无更改
-
2.45.0
2024-04-29
- 2.44.1 → 2.44.4 无更改
-
2.44.0
2024-02-23
- 2.43.2 → 2.43.7 无变更
-
2.43.1
2024-02-09
-
2.43.0
2023-11-20
- 2.42.1 → 2.42.4 无更改
-
2.42.0
2023-08-21
- 2.39.1 → 2.41.3 无变更
-
2.39.0
2022-12-12
- 2.38.1 → 2.38.5 无更改
-
2.38.0
2022-10-02
- 2.36.1 → 2.37.7 无更改
-
2.36.0
2022-04-18
- 2.34.1 → 2.35.8 无更改
-
2.34.0
2021-11-15
- 2.33.1 → 2.33.8 无更改
-
2.33.0
2021-08-16
- 2.32.1 → 2.32.7 无变更
-
2.32.0
2021-06-06
- 2.30.1 → 2.31.8 无更改
-
2.30.0
2020-12-27
- 2.28.1 → 2.29.3 无更改
-
2.28.0
2020-07-27
- 2.25.1 → 2.27.1 无变化
-
2.25.0
2020-01-13
- 2.24.1 → 2.24.4 无更改
-
2.24.0
2019-11-04
- 2.23.1 → 2.23.4 无更改
-
2.23.0
2019-08-16
- 2.22.1 → 2.22.5 无更改
-
2.22.0
2019-06-07
- 2.21.1 → 2.21.4 无更改
-
2.21.0
2019-02-24
- 2.19.1 → 2.20.5 无更改
-
2.19.0
2018-09-10
- 2.18.1 → 2.18.5 无更改
-
2.18.0
2018-06-21
- 2.17.0 → 2.17.6 无更改
-
2.16.6
2019-12-06
-
2.15.4
2019-12-06
-
2.14.6
2019-12-06
-
2.13.7
2018-05-22
-
2.12.5
2017-09-22
- 2.9.5 → 2.11.4 无更改
-
2.8.6
2017-07-30
-
2.7.6
2017-07-30
-
2.6.7
2017-05-05
- 2.5.6 无更改
-
2.4.12
2017-05-05
- 2.3.10 无更改
-
2.2.3
2015-09-04
-
2.1.4
2014-12-17
-
2.0.5
2014-12-17
简介
Git 是一个快速的分布式版本控制系统。
本手册旨在让具备基本 UNIX 命令行技能但对 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 命令会创建一个以项目命名的目录(上面示例中的 git 或 linux)。当您 cd 进入该目录后,您会发现它包含项目文件的副本,称为 工作目录,以及一个名为 .git 的特殊顶级目录,其中包含有关项目历史的所有信息。
如何检出项目的一个不同版本
Git 最适合被认为是存储文件集合历史的工具。它将历史存储为压缩的、相互关联的项目内容快照的集合。在 Git 中,每个这样的版本称为一个 提交。
这些快照不一定按照从最旧到最新的单一线路排列;相反,工作可以同时沿着平行的开发线进行,称为 分支,这些分支可能会合并和分叉。
一个 Git 仓库可以跟踪多个分支的开发。它通过维护一个 头指针 列表来实现,这些头指针引用每个分支的最新提交;git-branch[1] 命令会显示分支头指针列表。
$ git branch * master
刚克隆的仓库默认包含一个名为“master”的单个分支头指针,工作目录已初始化为该分支头指针所引用的项目状态。
大多数项目也使用 标签。标签与头指针类似,都是对项目历史的引用,可以使用 git-tag[1] 命令列出。
$ git tag -l v2.6.11 v2.6.11-tree v2.6.12 v2.6.12-rc2 v2.6.12-rc3 v2.6.12-rc4 v2.6.12-rc5 v2.6.12-rc6 v2.6.13 ...
标签应始终指向项目的同一版本,而头指针则会随着开发的进行而前进。
创建一个新的分支头指针指向其中一个版本,并使用 git-switch[1] 检出。
$ git switch -c new v2.6.13
工作目录随后会反映项目在标记为 v2.6.13 时的内容,并且 git-branch[1] 会显示两个分支,其中一个星号标记着当前检出的分支。
$ git branch master * new
如果您决定希望查看版本 2.6.17,您可以使用以下命令将当前分支头指针修改为指向 v2.6.17:
$ git reset --hard v2.6.17
请注意,如果当前分支头指针是您对特定历史点的唯一引用,那么重置该分支可能会导致您无法找到它以前指向的历史;因此,请谨慎使用此命令。
理解历史:提交
项目历史中的每个更改都由一个提交表示。git-show[1] 命令会显示当前分支上的最新提交。
$ git show
commit 17cf781661e6d38f737f15f53ab552f1e95960d7
Author: Linus Torvalds <torvalds@ppc970.osdl.org.(none)>
Date: Tue Apr 19 14:11:06 2005 -0700
Remove duplicate getenv(DB_ENVIRONMENT) call
Noted by Tony Luck.
diff --git a/init-db.c b/init-db.c
index 65898fa..b002dc6 100644
--- a/init-db.c
+++ b/init-db.c
@@ -7,7 +7,7 @@
int main(int argc, char **argv)
{
- char *sha1_dir = getenv(DB_ENVIRONMENT), *path;
+ char *sha1_dir, *path;
int len, i;
if (mkdir(".git", 0755) < 0) {
如您所见,提交显示了谁进行了最新的更改,他们做了什么以及为什么。
每个提交都有一个 40 位十六进制的 ID,有时也称为“对象名称”或“SHA-1 ID”,显示在 git show 输出的第一行。通常您可以使用较短的名称(如标签或分支名)来引用提交,但这个更长的名称也可能很有用。最重要的是,它是一个全局唯一的提交名称:因此,如果您(例如在电子邮件中)告诉别人对象名称,那么您可以保证该名称在对方的仓库中会引用与您仓库中相同的提交(前提是对方的仓库中有该提交)。由于对象名称是通过提交内容的哈希计算得出的,因此您可以保证在提交名称改变之前,提交本身永远不会改变。
事实上,在 Git 概念 中,我们将看到 Git 历史中存储的所有内容,包括文件数据和目录内容,都以具有内容哈希名称的对象形式存储。
理解历史:提交、父提交和可达性
每个提交(除了项目中的第一个提交)都有一个父提交,该父提交显示了此提交之前发生的事情。沿着父提交链最终会将您带回项目的起点。
然而,提交并非构成一个简单的列表;Git 允许开发线分叉然后重新汇合,而两条开发线重新汇合的点称为“合并”。因此,代表合并的提交可以有多个父提交,每个父提交代表导致该点的开发线中的最新提交。
了解这一点最佳方式是使用 gitk[1] 命令;现在在 Git 仓库上运行 gitk 并查找合并提交将有助于理解 Git 如何组织历史。
在以下内容中,我们称提交 X 从提交 Y“可达”,如果提交 X 是提交 Y 的祖先。等价地,您可以说 Y 是 X 的后代,或者存在一条从提交 Y 到提交 X 的父提交链。
操作分支
创建、删除和修改分支既快速又简单;以下是命令的摘要。
gitbranch-
列出所有分支。
gitbranch<branch>-
创建一个名为 <branch> 的新分支,指向与当前分支相同的历史点。
gitbranch<branch> <start-point>-
创建一个名为 <branch> 的新分支,指向 <start-point>,它可以以任何您喜欢的方式指定,包括使用分支名或标签名。
gitbranch-d<branch>-
删除分支 <branch>;如果该分支未完全合并到其上游分支或未包含在当前分支中,此命令将以警告失败。
gitbranch-D<branch>-
删除分支 <branch>,无论其合并状态如何。
gitswitch<branch>-
将当前分支设置为 <branch>,并更新工作目录以反映 <branch> 所引用的版本。
gitswitch-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 来查看它或编写一次性补丁。请参阅 分离的 HEAD。
请注意,“origin”这个名字只是 Git 默认用来引用您克隆来源的仓库的名称。
命名分支、标签和其他引用
分支、远程跟踪分支和标签都是对提交的引用。所有引用都使用以 refs 开头的斜杠分隔的路径名;我们到目前为止使用的名称实际上是简写。
-
分支
test是refs/heads/test的简写。 -
标签
v2.6.18是refs/tags/v2.6.18的简写。 -
origin/master是refs/remotes/origin/master的简写。
完整的名称偶尔会有用,例如,如果将来存在同名的标签和分支。
(新创建的引用实际上存储在 .git/refs 目录中,位于其名称指定的路径下。但是,出于效率原因,它们也可能被打包到一个文件中;请参阅 git-pack-refs[1])。
作为另一个有用的快捷方式,“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] 的“配置 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 现在已脱离任何分支,并直接指向一个提交(提交 ID 为 65934),该提交可从“master”到达但不能从 v2.6.18 到达。编译并测试它,看看它是否崩溃。假设它确实崩溃了。然后。
$ git bisect bad Bisecting: 1769 revisions left to test after this [7eff82c8b1511017ae605f0c99ac275a7e21b867] i2c-core: Drop useless bitmaskings
会检出一个旧版本。像这样继续,每次告诉 Git 其提供的版本是好是坏,并注意到需要测试的修订数量每次大约减半。
大约 13 次测试(在此例中)后,它将输出有问题的提交的提交 ID。然后您可以使用 git-show[1] 来检查该提交,找出谁写的,然后用提交 ID 给他们发送您的错误报告。最后,运行。
$ git bisect reset
将您恢复到之前的分支。
请注意,git bisect 在每个阶段为您检出的版本只是一个建议,您可以自由地尝试不同的版本,如果您认为这是个好主意。例如,有时您可能会遇到一个导致其他问题损坏的提交;运行。
$ git bisect visualize
这将运行 gitk 并用“bisect”标记标记它选择的提交。选择一个看起来安全的附近提交,记下其提交 ID,然后使用以下命令检出:
$ git reset --hard fb47ddb2db
然后进行测试,根据需要运行 bisect good 或 bisect bad,然后继续。
而不是 git bisect visualize 然后 git reset --hard fb47ddb2db,您可能只想告诉 Git 您要跳过当前提交。
$ git bisect skip
然而,在这种情况下,Git 可能最终无法区分某些第一个跳过的提交和一个稍后的坏提交之间的第一个坏提交。
如果有一个测试脚本可以区分好提交和坏提交,也有自动执行二分法的方法。有关此及其他 git bisect 功能的更多信息,请参阅 git-bisect[1]。
命名提交
我们已经看到了几种命名提交的方法。
-
40 位十六进制对象名称
-
分支名:引用给定分支头指针的提交。
-
标签名:引用由给定标签指向的提交(我们已经看到分支和标签是引用的特例)。
-
HEAD:引用当前分支的头指针。
还有更多;请参阅 gitrevisions[7] man 页面的“指定修订”部分,以获取命名修订方法 的完整列表。一些示例:
$ 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] man 页面。
浏览修订
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 以来触及 Makefile 或 fs 下任何文件的提交。
$ git log v2.5.. Makefile fs/
您还可以要求 git log 显示补丁。
$ git log -p
有关更多显示选项,请参阅 git-log[1] man 页面的 --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 )
(参见 gitrevisions[7] 关于提交选择语法的说明,例如 --not。)
创建软件发布版的变更日志和 tar 包
git-archive 命令可以从项目的任何版本创建 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] 的 man 页可能会有所帮助。
使用 Git 进行开发
告诉 Git 您的名字
在创建任何提交之前,您应该向 Git 介绍自己。最简单的方法是使用 git-config
$ git config --global user.name 'Your Name Comes Here' $ git config --global user.email 'you@yourdomain.example.com'
这将在您的主目录下的一个名为 .gitconfig 的文件中添加以下内容
[user] name = Your Name Comes Here email = you@yourdomain.example.com
有关配置文件,请参阅 git-config[1] 的“CONFIGURATION FILE”部分。该文件是纯文本,因此您也可以使用您喜欢的编辑器进行编辑。
创建新的仓库
从头开始创建一个新的仓库非常简单
$ mkdir project $ cd project $ git init
如果您有一些初始内容(例如,一个 tar 包)
$ tar xzvf project.tar.gz $ cd project $ git init $ git add . # include everything below ./ in the first commit: $ git commit
如何进行一次提交
创建一个新的提交需要三个步骤
-
使用您喜欢的编辑器对工作目录进行一些更改。
-
将您的更改告知 Git。
-
使用您在步骤 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 块(通过右键单击 diff 块并选择“Stage Hunk For Commit”)。
创建良好的提交消息
虽然不是必需的,但最好在提交消息的开头使用一个简短的(最多 50 个字符)行来总结更改,后面跟着一个空行,然后是一个更详细的描述。提交消息中第一个空行之前的文本被视为提交标题,并且该标题在 Git 中被广泛使用。例如,git-format-patch[1] 将提交转换为电子邮件,它在主题行中使用标题,并在正文中包含提交的其余部分。
忽略文件
项目通常会生成一些您不希望 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 分支的开发合并到当前分支。)
合并是通过合并 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 将会失败
$ 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] 的“COMBINED DIFF FORMAT”部分。)
在以明显的方式解决冲突后(但在更新索引之前),diff 将如下所示
$ git diff diff --cc file.txt index 802992c,2b60207..0000000 --- a/file.txt +++ b/file.txt @@@ -1,1 -1,1 +1,1 @@@ - Hello world -Goodbye ++Goodbye world
这表明我们的已解决版本从第一个父提交中删除了“Hello world”,从第二个父提交中删除了“Goodbye”,并添加了“Goodbye world”,而后者以前在两者中都不存在。
一些特殊的 diff 选项允许 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(默认情况下)将不再显示该文件的 diff。
撤销合并
如果您陷入困境并决定放弃并丢弃所有混乱,您始终可以执行以下操作返回到合并前的状态
$ git merge --abort
或者,如果您已经提交了想要丢弃的合并,
$ git reset --hard ORIG_HEAD
但是,最后一个命令在某些情况下可能很危险——如果您已经将某个提交合并到另一个分支,则永远不要丢弃该提交,因为这样做可能会混淆后续的合并。
快进合并
上面没有提到一个特殊情况,它被区别对待。通常,合并会产生一个合并提交,有两个父提交,一个指向两个合并的开发线中的每一个。
但是,如果当前分支是另一个分支的祖先——那么当前分支中的每个提交都已包含在另一个分支中——那么 Git 只执行“快进”;当前分支的头部会向前移动以指向合并分支的头部,而不会创建任何新的提交。
修复错误
如果您弄乱了工作目录,但尚未提交错误,您可以使用以下命令将整个工作目录恢复到上次提交的状态
$ git restore --staged --worktree :/
如果您进行了后来后悔的提交,有两种根本不同的方法可以解决问题
-
您可以创建一个新的提交来撤销旧提交所做的任何操作。如果您的错误已经公开,这是正确的做法。
-
您可以回过头来修改旧的提交。如果您已经将历史公开,则永远不应该这样做;Git 通常不期望项目的“历史”会改变,并且无法正确执行来自已更改历史的分支的重复合并。
通过新提交修复错误
创建一个新提交来撤销早期更改非常容易;只需将 git-revert[1] 命令指向有问题的提交;例如,要撤销最新的提交
$ git revert HEAD
这将创建一个新的提交,该提交撤销 HEAD 中的更改。您将有机会编辑新提交的提交消息。
您也可以撤销早期更改,例如,倒数第二个
$ git revert HEAD^
在这种情况下,Git 将尝试撤销旧的更改,同时保持自那时以来的所有更改不变。如果最近的更改与要撤销的更改重叠,那么您将被要求手动修复冲突,就像在 解决合并 的情况下一样。
通过重写历史来修复错误
如果出现问题的提交是最近的提交,并且您尚未公开该提交,那么您可以通过 git reset --hard 来销毁它。
或者,您可以像要 创建新提交 一样,编辑工作目录并更新索引来修复您的错误,然后运行
$ 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 来抑制这些消息,并仍然查看真正的错误。
恢复丢失的更改
引用日志
假设您使用 git reset --hard 修改了一个分支,然后意识到该分支是您拥有的唯一指向该历史点的引用。
幸运的是,Git 还保留了一个名为“reflog”(引用日志)的日志,记录每个分支的所有先前值。因此,在这种情况下,您仍然可以使用例如以下命令找到旧的历史记录:
$ git log master@{1}
这列出了可从 master 分支头的先前版本访问的提交。此语法可用于任何接受提交的 Git 命令,而不仅仅是 git log。其他一些示例
$ git show master@{2} # See where the branch pointed 2,
$ git show master@{3} # 3, ... changes ago.
$ gitk master@{yesterday} # See where it pointed yesterday,
$ gitk master@{"1 week ago"} # ... or last week
$ git log --walk-reflogs master # show reflog entries for master
为 HEAD 保留了一个单独的 reflog,所以
$ git show HEAD@{"1 week ago"}
将显示 HEAD 指向一星期前的位置,而不是当前分支一星期前指向的位置。这允许您查看您签出的历史记录。
reflogs 默认保留 30 天,之后可能会被修剪。请参阅 git-reflog[1] 和 git-gc[1] 以了解如何控制此修剪,并参阅 gitrevisions[7] 的“SPECIFYING REVISIONS”部分以了解详细信息。
请注意,reflog 历史与正常的 Git 历史非常不同。虽然正常历史由所有处理相同项目的仓库共享,但 reflog 历史不共享:它只告诉您本地仓库中的分支如何随时间变化。
检查悬空对象
在某些情况下,reflog 可能无法挽救您。例如,假设您删除了一个分支,然后意识到您需要它包含的历史记录。reflog 也被删除了;但是,如果您尚未修剪仓库,那么您仍然可能在 git fsck 报告的悬空对象中找到丢失的提交。有关详细信息,请参阅 悬空对象。
$ git fsck dangling commit 7281251ddd2a61e38657c827739c57015671a6b3 dangling commit 2706a059f258c6b245f298dc4ff2ccd30ec21a63 dangling commit 13472b7c4b80851a1bc551779171dcb03655e9b5 ...
您可以使用例如以下命令检查其中一个悬空提交:
$ gitk 7281251ddd --not --all
这会做它听起来的事:它表示您想查看由悬空提交描述的提交历史,而不是由您所有现有分支和标签描述的历史。因此,您将获得该提交可达的、已丢失的历史记录。(请注意,这可能不止一个提交:我们只报告“线路尖端”为悬空,但可能有一个完整、深入且复杂的提交历史被丢弃。)
如果您决定想要回历史记录,您始终可以创建一个指向它的新引用,例如,一个新的分支
$ git branch recovered-branch 7281251ddd
其他类型的悬空对象(blob 和 tree)也是可能的,并且悬空对象可能在其他情况下出现。
与他人共享开发
使用 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/*,并将默认分支合并到当前分支。
更一般地说,从远程跟踪分支创建的分支将默认从该分支拉取。请参阅 branch. 和 branch. 选项在 git-config[1] 中的描述,以及 git-checkout[1] 中 --track 选项的讨论,以了解如何控制这些默认设置。
除了节省您的键盘输入之外,git pull 还通过生成一个默认的提交消息来记录您拉取的默认分支和仓库,从而为您提供帮助。
(但请注意,在 快进 的情况下不会创建这样的提交;相反,您的分支将只被更新以指向上游分支的最新提交。)
git pull 命令也可以给定 . 作为“远程”仓库,在这种情况下,它只是合并当前仓库中的一个分支;所以命令
$ git pull . branch $ git merge branch
大致等价。
将补丁提交给项目
如果您只有少量更改,提交它们的最简单方法可能是通过电子邮件发送它们作为补丁
首先,使用 git-format-patch[1];例如
$ git format-patch origin
将在当前目录中生成一系列编号的文件,每个文件代表当前分支中但不在 origin/HEAD 中的一个补丁。
git format-patch 可以包含一个初始的“封面信”。您可以在 format-patch 在提交消息之后、补丁本身之前放置的三个破折号行后插入对单个补丁的评论。如果您使用 git notes 来跟踪您的封面信材料,git format-patch --notes 将以类似的方式包含提交的笔记。
然后,您可以将它们导入您的邮件客户端并手动发送。但是,如果您一次要发送很多内容,您可能更喜欢使用 git-send-email[1] 脚本来自动化该过程。首先查阅您项目的邮件列表,以确定他们提交补丁的要求。
将补丁导入项目
Git 还提供了一个名为 git-am[1](am 代表“apply mailbox”,应用邮箱)的工具,用于导入这样一封电子邮件发送的补丁系列。只需将所有包含补丁的消息按顺序保存在一个邮箱文件中,例如 patches.mbox,然后运行
$ git am -3 patches.mbox
Git 将按顺序应用每个补丁;如果发现任何冲突,它将停止,您可以按照“解决合并”中的描述解决冲突。(-3 选项告诉 Git 执行合并;如果您更希望它只中止并保持您的树和索引不变,您可以省略该选项。)
一旦索引使用冲突解决的结果更新,而不是创建新提交,只需运行
$ git am --continue
Git 将为您创建提交并继续应用邮箱中的剩余补丁。
最终结果将是一系列提交,每个提交对应原始邮箱中的一个补丁,作者信息和提交日志消息都取自包含每个补丁的消息。
公共 Git 仓库
提交更改给项目的另一种方法是告诉该项目的维护者使用 git-pull[1] 从您的仓库拉取更改。在“使用 git pull 获取更新”一节中,我们将其描述为获取“主”仓库更新的方式,但它在反方向上同样有效。
如果您和维护者都在同一台机器上有账户,那么您可以直接拉取对方仓库中的更改;接受仓库 URL 作为参数的命令也将接受本地目录名
$ git clone /path/to/repository $ git pull /path/to/other/repository
或 ssh url
$ git clone ssh://yourhost/~you/repository
对于开发者较少或用于同步少量私有仓库的项目,这可能就是您所需要的。
然而,更常见的方法是维护一个单独的公共仓库(通常在不同的主机上)供其他人拉取更改。这通常更方便,并且允许您清晰地分离未完成的私有工作和公开可见的工作。
您将继续在您的个人仓库中进行日常工作,但会定期将更改从您的个人仓库“推”到您的公共仓库,从而允许其他开发人员从该仓库拉取。因此,在一个有另一个开发人员拥有公共仓库的情况下,更改的流程如下所示
you push
your personal repo ------------------> your public repo
^ |
| |
| you pull | they pull
| |
| |
| they push V
their public repo <------------------- their repo
我们在以下各节中对此进行解释。
设置公共仓库
假设您的个人仓库位于目录 ~/proj 中。我们首先创建一个仓库的新克隆,并告知 git daemon 它打算公开
$ git clone --bare ~/proj proj.git $ touch proj.git/git-daemon-export-ok
生成的 proj.git 目录包含一个“裸” Git 仓库——它只是 .git 目录的内容,而没有在其周围检出任何文件。
接下来,将 proj.git 复制到您打算托管公共仓库的服务器上。您可以使用 scp、rsync 或任何最方便的方式。
通过 Git 协议导出 Git 仓库
这是首选方法。
如果有人另外管理服务器,他们应该告诉您应将仓库放在哪个目录中,以及它将出现在哪个 git:// URL。然后您可以跳到下面的“将更改推送到公共仓库”部分。
否则,您所要做的就是启动 git daemon;它将监听端口 9418。默认情况下,它允许访问任何看起来像 Git 目录且包含特殊文件 git-daemon-export-ok 的目录。将一些目录路径作为 git daemon 参数传递将进一步限制导出的目录到那些路径。
您也可以将 git daemon 作为 inetd 服务运行;有关详细信息,请参阅 git-daemon[1] man 页。(尤其请参阅示例部分。)
通过 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 推送。)
将更改推送到公共仓库
最简单的方法是使用 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
有关详细信息,请参阅 remote.、branch. 和 remote. 选项在 git-config[1] 中的说明。
推送失败时该怎么办
如果推送不会导致远程分支 快进,那么它将以类似以下的错误失败
! [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 reset --hard来移除已发布的提交,或者 -
使用
git commit --amend来替换已发布的提交(如 通过重写历史来修复错误),或 -
使用
git rebase来变基任何已发布的提交(如 使用 git rebase 保持补丁系列最新)。
您可以通过在分支名称前加上加号来强制 git push 仍然执行更新。
$ git push ssh://yourserver.com/~you/proj.git +master
注意加号 + 的添加。 或者,您可以使用 -f 标志来强制远程更新,如
$ git push -f ssh://yourserver.com/~you/proj.git master
通常,当公共仓库中的分支头发生修改时,它会修改为指向之前指向的提交的后继者。通过强制推送在这种情况下,您打破了这一惯例。(参见 重写历史的问题。)
尽管如此,这对于那些需要一种简单方式来发布正在进行中的补丁系列的人来说是一种常见的做法,只要您警告其他开发人员您打算如何管理该分支,它就是一种可接受的折衷方案。
当其他人有权推送到同一个仓库时,推送也可能以这种方式失败。在这种情况下,正确的解决方案是在先更新工作后重试推送:可以通过 pull,或通过 fetch 后跟 rebase 来完成;有关更多信息,请参见 下一节 和 gitcvs-migration[7]。
设置共享仓库
另一种协作方式是使用与 CVS 中常用的模型相似的模型,其中具有特殊权限的几位开发人员都推送到并从单个共享仓库中拉取。有关如何设置此项的说明,请参见 gitcvs-migration[7]。
但是,虽然 Git 对共享仓库的支持没有任何问题,但这种操作模式通常不推荐,仅仅因为 Git 支持的协作模式——通过交换补丁和从公共仓库拉取——与中央共享仓库相比有许多优势。
-
Git 快速导入和合并补丁的能力允许单个维护者即使在非常高的速率下也能处理传入的更改。当这变得太多时,
git pull为该维护者提供了一种简单的方法,可以将此任务委派给其他维护者,同时仍然允许可选地审查传入的更改。 -
由于每个开发者的仓库都拥有项目的完整历史记录的相同副本,因此没有特殊的仓库,并且另一位开发人员可以轻松地接管项目的维护,无论是通过双方同意,还是因为维护者变得不响应或难以合作。
-
缺乏一个中央“提交者”小组意味着减少了对谁“在”和谁“不在”的正式决策的需求。
允许通过 Web 浏览仓库
gitweb cgi 脚本为用户提供了一种简单的方式来浏览项目的修订、文件内容和日志,而无需安装 Git。像 RSS/Atom feed 和 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 在他作为 Linux 内核 IA64 架构维护者的角色中如何使用 Git。
他使用两个公共分支
-
一个“test”树,补丁最初放置在此处,以便它们在与其他正在进行的发展集成时获得一些曝光。当 Andrew 想要将其拉入 -mm 时,该树是可用的。
-
一个“release”树,测试过的补丁被移入此处进行最终的健全性检查,并作为发送给 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] 推送 test 和 release 树。
$ git push mytree
或者使用以下命令仅推送 test 和 release 分支中的一个。
$ 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 ]*
当您对更改的状态满意时,您可以将其合并到“test”分支中,以准备公开。
$ git switch test && git merge speed-up-spinlocks
这里不太可能发生冲突……但如果您在此步骤上花费了大量时间并且还从上游拉取了新版本,则可能会发生冲突。
过一段时间后,当有足够的时间并且测试完成时,您可以将同一分支拉到 release 树中,准备好向上游提交。这就是您看到将每个补丁(或补丁系列)保留在其自己的分支中的价值的地方。这意味着补丁可以按任何顺序移动到 release 树中。
$ git switch release && git merge speed-up-spinlocks
一段时间后,您将拥有许多分支,并且尽管您为每个分支选择了精心设计的名称,您可能会忘记它们的作用或它们的状态。要获得关于特定分支中包含哪些更改的提醒,请使用
$ git log linux..branchname | git shortlog
要查看它是否已合并到 test 或 release 分支中,请使用
$ git log test..branchname
或
$ git log release..branchname
(如果此分支尚未合并,您将看到一些日志条目。如果已合并,则不会有输出。)
一旦补丁完成了整个周期(从 test 移动到 release,然后被 Linus 拉取,最后回到您的本地 origin/master 分支),此更改的分支就不再需要了。当以下命令的输出为空时,您会检测到这一点。
$ git log origin..branchname
此时可以删除该分支。
$ git branch -d branchname
有些更改非常微小,以至于不需要创建单独的分支然后合并到 test 和 release 分支中。对于这些更改,只需直接应用于 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 的合并机制(例如)做错事。
然而,在某些情况下,违反此假设可能很有用。
创建完美的补丁系列
假设您是一个大型项目的贡献者,并且您想添加一个复杂的功能,并以一种易于其他开发人员阅读、验证其正确性并理解您为何进行每项更改的方式呈现给他们。
如果您将所有更改都作为单个补丁(或提交)呈现,他们可能会发现一次消化太多。
如果您向他们展示您工作的全部历史记录,包括错误、更正和死胡同,他们可能会不知所措。
因此,理想情况通常是生成一系列补丁,使得
-
每个补丁都可以按顺序应用。
-
每个补丁包含一个逻辑更改,以及解释该更改的消息。
-
没有补丁会引入回归:在应用了系列的任何初始部分之后,结果项目仍然可以编译和运行,并且没有以前不存在的 bug。
-
完整的系列产生了与您自己(可能更混乱!)的开发过程相同的最终结果。
我们将介绍一些可以帮助您做到这一点的工具,解释如何使用它们,然后解释一些可能因您重写历史而出现的问题。
使用 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 重新排序补丁系列相同,因此请选择您喜欢的界面。
在您想保留为不变的最后一个提交上 rebase 您当前的 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] 的“交互模式”部分。
重写历史的问题
重写分支历史的主要问题与合并有关。假设某人 fetch 了您的分支并将其合并到他们的分支中,结果如下:
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 将尝试合并两个(旧的和新的)开发线,而不是尝试用新的替换旧的。结果可能会出乎意料。
您仍然可以选择发布历史被重写的分支,并且其他能够 fetch 这些分支以检查或测试它们可能有用,但他们不应尝试将这些分支 pull 到他们自己的工作中。
为了支持正确合并的真正分布式开发,发布的分支永远不应被重写。
为什么 bisect 合并提交可能比 bisect 线性历史更难
git-bisect[1] 命令正确地处理包含合并提交的历史记录。但是,当找到的提交是合并提交时,用户可能需要比平时付出更多的努力来弄清楚为什么该提交会引入问题。
想象一下这个历史。
---Z---o---X---...---o---A---C---D
\ /
o---o---Y---...---o---B
假设在上层开发线上,Z 处的一个函数含义在提交 X 处发生了变化。从 Z 到 A 的提交同时改变了函数的实现以及 Z 处存在的所有调用点,以及它们添加的新调用点,以保持一致。A 处没有 bug。
假设在此期间,在下层开发线上,有人在提交 Y 处添加了该函数的新调用点。从 Z 到 B 的提交都假定该函数的老语义,调用者和被调用者之间是保持一致的。B 处也没有 bug。
假设进一步,两条开发线在 C 处干净地合并,因此不需要冲突解决。
尽管如此,C 处的代码是损坏的,因为下层开发线上添加的调用者尚未转换为上层开发线上引入的新语义。所以,如果您所知道的只是 D 是坏的,Z 是好的,并且 git-bisect[1] 将 C 识别为罪魁祸首,您将如何弄清楚问题是由于这种语义的变化?
当 git bisect 的结果是非合并提交时,您通常应该能够通过检查该提交来发现问题。开发人员可以通过将他们的更改分解为小的、自包含的提交来使其更容易。然而,在上述情况下,这无济于事,因为问题并非从任何单个提交的检查中就能明显看出;相反,需要对开发进行全局视图。更糟糕的是,有问题的函数中的语义变化可能只是上层开发线上更改的一小部分。
另一方面,如果不是在 C 处合并,而是将 Z 到 B 之间的历史基于 A 进行 rebase,您将得到这个线性历史。
---Z---o---X--...---o---A---o---o---Y*--...---o---B*--D*
在 Z 和 D* 之间进行 bisect 将会命中一个单独的罪魁祸首提交 Y*,并且理解 Y* 为什么是错误的可能更容易。
部分由于这个原因,许多有经验的 Git 用户,即使在处理一个本身就是 merge-heavy 的项目时,也会在发布前通过基于最新的上游版本进行 rebase 来保持历史线性。
高级分支管理
fetch 单个分支
除了使用 git-remote[1] 之外,您还可以选择一次只更新一个分支,并将其本地存储在任意名称下。
$ git fetch origin todo:my-todo-work
第一个参数 origin 只是告诉 Git 从您最初克隆的仓库中 fetch。第二个参数告诉 Git 从远程仓库中 fetch 名为 todo 的分支,并将其本地存储在名为 refs/heads/my-todo-work 的名称下。
您也可以从其他仓库 fetch 分支;所以
$ 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 更新到新的头,如下一节所述。但是,请注意,在上述情况下,这可能意味着丢失标记为 a 和 b 的提交,除非您自己已经创建了指向它们的引用。
强制 git fetch 进行非快进更新
如果 git fetch 因分支的新头不是旧头的后继者而失败,您可以使用以下命令强制更新:
$ git fetch git://example.com/proj.git +master:refs/remotes/example/master
注意加号 + 的添加。或者,您可以使用 -f 标志来强制更新所有 fetched 的分支,如
$ 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 hash 来计算的。SHA-1 hash 是一种加密哈希函数。这对我们来说意味着不可能找到两个具有相同名称的不同的对象。这有许多优点;其中:
-
Git 可以通过比较名称快速确定两个对象是否相同。
-
由于对象名称在每个仓库中的计算方式相同,因此存储在两个仓库中的相同内容将始终存储在相同的名称下。
-
Git 可以在读取对象时检测错误,通过检查对象的名称是否仍然是其内容的 SHA-1 hash。
(有关对象格式和 SHA-1 计算的详细信息,请参见 对象存储格式。)
有四种不同类型的对象:“blob”、“tree”、“commit”和“tag”。
-
一个 “blob”对象 用于存储文件数据。
-
一个 “tree”对象 将一个或多个“blob”对象绑定到目录结构中。此外,一个 tree 对象可以引用其他 tree 对象,从而创建目录层次结构。
-
一个 “commit”对象 将这些目录层次结构绑定到一个修订版的 有向无环图 中——每个提交包含一个 tree 的对象名称,该 tree 指定了提交时的目录层次结构。此外,提交引用“父”提交对象,这些对象描述了到达该目录层次结构的历史。
-
一个 “tag”对象 符号地标识其他对象,并可用于对其进行签名。它包含另一个对象的对象名称和类型、一个符号名称(当然!)以及可选的签名。
对象类型更详细一些:
提交对象
“commit”对象将 tree 的物理状态与如何到达那里以及为什么的描述联系起来。使用 --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:一个 tree 对象(如下定义)的 SHA-1 名称,代表某个时间点的目录内容。
-
parent(s): 项目历史中代表紧随其后的一个或多个提交的 SHA-1 名称。上面的示例有一个 parent;合并提交可能有一个以上。没有 parent 的提交称为“根”提交,代表项目的初始版本。每个项目至少需要有一个根。一个项目也可以有多个根,尽管这并不常见(或者不一定是好事)。
-
an author: 负责此更改的人的姓名,以及更改日期。
-
a committer: 实际创建此提交的人的姓名,以及完成日期。这可能与 author 不同,例如,如果 author 是编写了一个补丁并将其发送给使用该补丁创建提交的人。
-
a comment describing this commit. 描述此提交的注释。
Note that a commit does not itself contain any information about what actually changed; all changes are calculated by comparing the contents of the tree referred to by this commit with the trees associated with its parents. In particular, Git does not attempt to record file renames explicitly, though it can identify cases where the existence of the same file data at changing paths suggests a rename. (See, for example, the -M option to git-diff[1]). 注意,提交本身不包含任何关于实际更改的信息;所有更改都是通过比较此提交引用的树的内容与其 parent 相关的树的内容来计算的。特别是,Git 不会显式记录文件重命名,尽管它可以识别出在不同路径下存在相同文件数据的情况,这表明存在重命名。(例如,请参阅 git-diff[1] 的 -M 选项)。
A commit is usually created by git-commit[1], which creates a commit whose parent is normally the current HEAD, and whose tree is taken from the content currently stored in the index. 提交通常由 git-commit[1] 创建,它创建一个 parent 通常是当前 HEAD 的提交,其 tree 取自当前存储在索引中的内容。
Tree Object
The ever-versatile git-show[1] command can also be used to examine tree objects, but git-ls-tree[1] will give you more details. 万能的 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 ...
As you can see, a tree object contains a list of entries, each with a mode, object type, SHA-1 name, and name, sorted by name. It represents the contents of a single directory tree. 从图中可以看到,一个 tree 对象包含一个条目列表,每个条目都具有模式、对象类型、SHA-1 名称和名称,并按名称排序。它代表单个目录树的内容。
The object type may be a blob, representing the contents of a file, or another tree, representing the contents of a subdirectory. Since trees and blobs, like all other objects, are named by the SHA-1 hash of their contents, two trees have the same SHA-1 name if and only if their contents (including, recursively, the contents of all subdirectories) are identical. This allows Git to quickly determine the differences between two related tree objects, since it can ignore any entries with identical object names. 对象类型可以是 blob,代表文件的内容,也可以是另一个 tree,代表子目录的内容。由于 tree 和 blob,与其他所有对象一样,都以其内容的 SHA-1 哈希命名,因此两个 tree 拥有相同的 SHA-1 名称当且仅当它们的内容(包括递归地,所有子目录的内容)相同。这使得 Git 能够快速确定两个相关 tree 对象之间的差异,因为它会忽略任何具有相同对象名称的条目。
(Note: in the presence of submodules, trees may also have commits as entries. See Submodules for documentation.) (注意:在存在子模块的情况下,tree 也可能包含 commit 作为条目。有关文档,请参阅 Submodules。)
Note that the files all have mode 644 or 755: Git actually only pays attention to the executable bit. 请注意,文件都具有 644 或 755 的权限:Git 实际上只关注可执行位。
Blob Object
You can use git-show[1] to examine the contents of a blob; take, for example, the blob in the entry for COPYING from the tree above. 您可以使用 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. ...
A "blob" object is nothing but a binary blob of data. It doesn’t refer to anything else or have attributes of any kind. 一个“blob”对象只不过是一块二进制数据。它不指向任何其他东西,也没有任何类型的属性。
Since the blob is entirely defined by its data, if two files in a directory tree (or in multiple different versions of the repository) have the same contents, they will share the same blob object. The object is totally independent of its location in the directory tree, and renaming a file does not change the object that file is associated with. 由于 blob 完全由其数据定义,因此如果目录树中的两个文件(或存储库的多个不同版本)具有相同的内容,它们将共享相同的 blob 对象。该对象完全独立于其在目录树中的位置,重命名文件不会改变该文件关联的对象。
Note that any tree or blob object can be examined using git-show[1] with the <revision>:<path> syntax. This can sometimes be useful for browsing the contents of a tree that is not currently checked out. 请注意,任何 tree 或 blob 对象都可以使用 git-show[1] 和 <revision>:<path> 语法进行检查。这有时对于浏览当前未签出的 tree 的内容很有用。
Trust
If you receive the SHA-1 name of a blob from one source, and its contents from another (possibly untrusted) source, you can still trust that those contents are correct as long as the SHA-1 name agrees. This is because the SHA-1 is designed so that it is infeasible to find different contents that produce the same hash. 如果您从一个来源接收到 blob 的 SHA-1 名称,并从另一个(可能不可信)来源接收到其内容,只要 SHA-1 名称匹配,您仍然可以信任这些内容是正确的。这是因为 SHA-1 的设计使得找到生成相同哈希的不同内容是不可能的。
Similarly, you need only trust the SHA-1 name of a top-level tree object to trust the contents of the entire directory that it refers to, and if you receive the SHA-1 name of a commit from a trusted source, then you can easily verify the entire history of commits reachable through parents of that commit, and all of those contents of the trees referred to by those commits. 同样,您只需要信任顶级 tree 对象的文件名即可信任它引用的整个目录的内容,如果您从受信任的来源接收到 commit 的 SHA-1 名称,那么您就可以轻松地验证通过该 commit 的 parent 可达的所有 commit 历史,以及那些 commit 引用的 tree 的所有内容。
So to introduce some real trust in the system, the only thing you need to do is to digitally sign just one special note, which includes the name of a top-level commit. Your digital signature shows others that you trust that commit, and the immutability of the history of commits tells others that they can trust the whole history. 因此,要在系统中引入真正的信任,您只需要做的是对一个特殊的说明进行数字签名,该说明包含顶级 commit 的名称。您的数字签名向他人表明您信任该 commit,而 commit 历史的不可变性则告诉他人他们可以信任整个历史。
In other words, you can easily validate a whole archive by just sending out a single email that tells the people the name (SHA-1 hash) of the top commit, and digitally sign that email using something like GPG/PGP. 换句话说,您可以通过发送一封电子邮件来轻松验证整个存档,该电子邮件告诉人们顶级 commit 的名称(SHA-1 哈希),并使用 GPG/PGP 等工具对该电子邮件进行数字签名。
To assist in this, Git also provides the tag object… 为了协助此目的,Git 还提供了 tag 对象…
Tag Object
A tag object contains an object, object type, tag name, the name of the person ("tagger") who created the tag, and a message, which may contain a signature, as can be seen using git-cat-file[1]. 一个 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-----
See the git-tag[1] command to learn how to create and verify tag objects. (Note that git-tag[1] can also be used to create "lightweight tags", which are not tag objects at all, but just simple references whose names begin with refs/tags/). 请参阅 git-tag[1] 命令了解如何创建和验证 tag 对象。(注意:git-tag[1] 也可用于创建“轻量级 tag”,它们根本不是 tag 对象,而只是名称以 refs/tags/ 开头的简单引用)。
How Git stores objects efficiently: pack files. Git 如何高效地存储对象:pack 文件
Newly created objects are initially created in a file named after the object’s SHA-1 hash (stored in .git/objects). 新创建的对象最初会以对象的 SHA-1 哈希命名(存储在 .git/objects 中)的文件形式创建。
Unfortunately this system becomes inefficient once a project has a lot of objects. Try this on an old project. 不幸的是,一旦项目包含大量对象,这种系统就会变得效率低下。在旧项目上尝试此操作。
$ git count-objects 6930 objects, 47620 kilobytes
The first number is the number of objects which are kept in individual files. The second is the amount of space taken up by those "loose" objects. 第一个数字是保留在单个文件中的对象数量。第二个数字是这些“松散”对象占用的空间量。
You can save space and make Git faster by moving these loose objects in to a "pack file", which stores a group of objects in an efficient compressed format; the details of how pack files are formatted can be found in gitformat-pack[5]. 您可以通过将这些松散对象移至“pack 文件”来节省空间并提高 Git 的速度,pack 文件以高效的压缩格式存储一组对象;pack 文件的格式详情可以在 gitformat-pack[5] 中找到。
To put the loose objects into a pack, just run git repack. 要将松散对象放入 pack,只需运行 git repack。
$ git repack Counting objects: 6020, done. Delta compression using up to 4 threads. Compressing objects: 100% (6020/6020), done. Writing objects: 100% (6020/6020), done. Total 6020 (delta 4070), reused 0 (delta 0)
This creates a single "pack file" in .git/objects/pack/ containing all currently unpacked objects. You can then run. 这会在 .git/objects/pack/ 中创建一个名为“pack 文件”的文件,其中包含所有当前未打包的对象。然后您可以运行。
$ git prune
to remove any of the "loose" objects that are now contained in the pack. This will also remove any unreferenced objects (which may be created when, for example, you use git reset to remove a commit). You can verify that the loose objects are gone by looking at the .git/objects directory or by running. 以删除现在包含在 pack 中的任何“松散”对象。这还将删除任何未引用的对象(例如,当您使用 git reset 删除提交时可能会创建这些对象)。您可以通过查看 .git/objects 目录或运行来验证松散对象是否已消失。
$ git count-objects 0 objects, 0 kilobytes
Although the object files are gone, any commands that refer to those objects will work exactly as they did before. 尽管对象文件已消失,但任何引用这些对象的命令都会像以前一样工作。
Dangling objects. 悬空对象
The git-fsck[1] command will sometimes complain about dangling objects. They are not a problem. git-fsck[1] 命令有时会抱怨悬空对象。它们不是问题。
The most common cause of dangling objects is that you’ve rebased a branch, or you have pulled from somebody else who rebased a branch—see Rewriting history and maintaining patch series. In that case, the old head of the original branch still exists, as does everything it pointed to. The branch pointer itself just doesn’t, since you replaced it with another one. 悬空对象最常见的原因是您 rebase 了某个分支,或者您从 rebase 了某个分支的其他人那里 pull 了——请参阅 Rewriting history and maintaining patch series。在这种情况下,原始分支的旧 head 仍然存在,它指向的所有内容也一样。分支指针本身不存在了,因为您用另一个替换了它。
There are also other situations that cause dangling objects. For example, a "dangling blob" may arise because you did a git add of a file, but then, before you actually committed it and made it part of the bigger picture, you changed something else in that file and committed that updated thing—the old state that you added originally ends up not being pointed to by any commit or tree, so it’s now a dangling blob object. 还有其他导致悬空对象的情况。例如,一个“悬空 blob”可能由于您添加了一个文件,然后在您实际提交它并将其纳入整体之前,您更改了该文件中的其他内容并提交了更新的内容——您最初添加的旧状态最终没有被任何提交或 tree 指向,因此现在它是一个悬空 blob 对象。
Similarly, when the "ort" merge strategy runs, and finds that there are criss-cross merges and thus more than one merge base (which is fairly unusual, but it does happen), it will generate one temporary midway tree (or possibly even more, if you had lots of criss-crossing merges and more than two merge bases) as a temporary internal merge base, and again, those are real objects, but the end result will not end up pointing to them, so they end up "dangling" in your repository. 同样,当“ort”合并策略运行时,发现存在交叉合并,从而有多个合并基(这相当罕见,但确实会发生),它将生成一个临时中间 tree(如果有很多交叉合并和两个以上的合并基,甚至可能更多)作为临时的内部合并基,同样,这些都是真实的对象,但最终结果不会指向它们,因此它们最终会“悬空”在您的存储库中。
Generally, dangling objects aren’t anything to worry about. They can even be very useful: if you screw something up, the dangling objects can be how you recover your old tree (say, you did a rebase, and realized that you really didn’t want to—you can look at what dangling objects you have, and decide to reset your head to some old dangling state). 通常,悬空对象没什么可担心的。它们甚至可能非常有用:如果您搞砸了什么,悬空对象可以帮助您恢复旧的 tree(例如,您 rebase 了,并意识到您真的不想这样做——您可以查看您拥有的悬空对象,并决定将您的 HEAD 重置到某个旧的悬空状态)。
For commits, you can just use. 对于 commit,您只需使用。
$ gitk <dangling-commit-sha-goes-here> --not --all
This asks for all the history reachable from the given commit but not from any branch, tag, or other reference. If you decide it’s something you want, you can always create a new reference to it, e.g. 这会查找从给定 commit 可达但不可从任何分支、tag 或其他引用可达的所有历史。如果您决定想要它,您可以随时创建一个新的引用指向它,例如。
$ git branch recovered-branch <dangling-commit-sha-goes-here>
For blobs and trees, you can’t do the same, but you can still examine them. You can just do. 对于 blob 和 tree,您不能做同样的事情,但仍然可以检查它们。您可以直接执行。
$ git show <dangling-blob/tree-sha-goes-here>
to show what the contents of the blob were (or, for a tree, basically what the ls for that directory was), and that may give you some idea of what the operation was that left that dangling object. 以显示 blob 的内容(或者,对于 tree,基本上是该目录的 ls 输出),这可能会让您对导致该悬空对象的那个操作有所了解。
Usually, dangling blobs and trees aren’t very interesting. They’re almost always the result of either being a half-way mergebase (the blob will often even have the conflict markers from a merge in it, if you have had conflicting merges that you fixed up by hand), or simply because you interrupted a git fetch with ^C or something like that, leaving some of the new objects in the object database, but just dangling and useless. 通常,悬空 blob 和 tree 并不太有趣。它们几乎总是以下情况的结果:要么是半途的合并基(如果发生过冲突合并且您手动修复过,blob 甚至可能包含合并的冲突标记),要么仅仅是因为您用 ^C 或类似的方式中断了 git fetch,导致一些新对象留在对象数据库中,但它们是悬空的且无用的。
Anyway, once you are sure that you’re not interested in any dangling state, you can just prune all unreachable objects. 无论如何,一旦您确定您对任何悬空状态不感兴趣,您就可以修剪所有不可达的对象。
$ git prune
and they’ll be gone. (You should only run git prune on a quiescent repository—it’s kind of like doing a filesystem fsck recovery: you don’t want to do that while the filesystem is mounted. git prune is designed not to cause any harm in such cases of concurrent accesses to a repository but you might receive confusing or scary messages.) 它们就会消失。(您应该只在安静的存储库上运行 git prune ——这有点像进行文件系统 fsck 恢复:您不想在文件系统挂载时进行此操作。git prune 的设计目的是在这种并发访问存储库的情况下不会造成任何损害,但您可能会收到令人困惑或可怕的消息。)
Recovering from repository corruption. 从存储库损坏中恢复
By design, Git treats data trusted to it with caution. However, even in the absence of bugs in Git itself, it is still possible that hardware or operating system errors could corrupt data. Git 的设计宗旨是谨慎对待其信任的数据。然而,即使 Git 本身没有 bug,硬件或操作系统错误仍然可能损坏数据。
The first defense against such problems is backups. You can back up a Git directory using clone, or just using cp, tar, or any other backup mechanism. 防范此类问题的第一道防线是备份。您可以使用 clone 来备份 Git 目录,或者直接使用 cp、tar 或任何其他备份机制。
As a last resort, you can search for the corrupted objects and attempt to replace them by hand. Back up your repository before attempting this in case you corrupt things even more in the process. 作为最后的手段,您可以搜索损坏的对象并尝试手动替换它们。在尝试此操作之前,请备份您的存储库,以防您在此过程中使情况更加恶化。
We’ll assume that the problem is a single missing or corrupted blob, which is sometimes a solvable problem. (Recovering missing trees and especially commits is much harder). 我们假设问题是单个丢失或损坏的 blob,有时这是一个可解决的问题。(恢复丢失的 tree,尤其是 commit,会困难得多)。
Before starting, verify that there is corruption, and figure out where it is with git-fsck[1]; this may be time-consuming. 开始之前,请验证是否存在损坏,并使用 git-fsck[1] 找出其位置;这可能很耗时。
Assume the output looks like this. 假设输出如下所示。
$ git fsck --full --no-dangling
broken link from tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
to blob 4b9458b3786228369c63936db65827de3cc06200
missing blob 4b9458b3786228369c63936db65827de3cc06200
Now you know that blob 4b9458b is missing, and that the tree 2d9263c6 points to it. If you could find just one copy of that missing blob object, possibly in some other repository, you could move it into .git/objects/4b/9458b3... and be done. Suppose you can’t. You can still examine the tree that pointed to it with git-ls-tree[1], which might output something like. 现在您知道 blob 4b9458b 丢失了,并且 tree 2d9263c6 指向它。如果您能找到那个丢失的 blob 对象的一个副本,可能在某个其他存储库中,您可以将其移动到 .git/objects/4b/9458b3... 并完成。假设您不能。您仍然可以使用 git-ls-tree[1] 检查指向它的 tree,这可能会输出类似以下内容:
$ git ls-tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8 100644 blob 8d14531846b95bfa3564b58ccfb7913a034323b8 .gitignore 100644 blob ebf9bf84da0aab5ed944264a5db2a65fe3a3e883 .mailmap 100644 blob ca442d313d86dc67e0a2e5d584b465bd382cbf5c COPYING ... 100644 blob 4b9458b3786228369c63936db65827de3cc06200 myfile ...
So now you know that the missing blob was the data for a file named myfile. And chances are you can also identify the directory—let’s say it’s in somedirectory. If you’re lucky the missing copy might be the same as the copy you have checked out in your working tree at somedirectory/myfile; you can test whether that’s right with git-hash-object[1]. 因此,现在您知道丢失的 blob 是名为 myfile 的文件的内容。并且很有可能您也可以识别出目录——假设它在 somedirectory 中。如果您幸运的话,丢失的副本可能与您在 somedirectory/myfile 的工作目录中签出的副本相同;您可以使用 git-hash-object[1] 来测试是否正确。
$ git hash-object -w somedirectory/myfile
which will create and store a blob object with the contents of somedirectory/myfile, and output the SHA-1 of that object. if you’re extremely lucky it might be 4b9458b3786228369c63936db65827de3cc06200, in which case you’ve guessed right, and the corruption is fixed! 这将创建并存储一个具有 somedirectory/myfile 内容的 blob 对象,并输出该对象的 SHA-1。如果您非常幸运,它可能是 4b9458b3786228369c63936db65827de3cc06200,在这种情况下,您就猜对了,损坏就修复了!
Otherwise, you need more information. How do you tell which version of the file has been lost? 否则,您需要更多信息。您如何确定丢失了哪个文件版本?
The easiest way to do this is with. 最简单的方法是使用。
$ git log --raw --all --full-history -- somedirectory/myfile
Because you’re asking for raw output, you’ll now get something like. 因为您要求原始输出,您现在会得到类似以下的内容。
commit abc Author: Date: ... :100644 100644 4b9458b newsha M somedirectory/myfile commit xyz Author: Date: ... :100644 100644 oldsha 4b9458b M somedirectory/myfile
This tells you that the immediately following version of the file was "newsha", and that the immediately preceding version was "oldsha". You also know the commit messages that went with the change from oldsha to 4b9458b and with the change from 4b9458b to newsha. 这告诉您,紧随其后的文件版本是“newsha”,而紧随其前的版本是“oldsha”。您还知道与从 oldsha 到 4b9458b 的更改以及从 4b9458b 到 newsha 的更改相关的 commit 消息。
If you’ve been committing small enough changes, you may now have a good shot at reconstructing the contents of the in-between state 4b9458b. 如果您提交的更改足够小,您现在可能很有希望重建 4b9458b 之间的状态内容。
If you can do that, you can now recreate the missing object with. 如果您能做到这一点,您现在就可以用…创建丢失的对象。
$ git hash-object -w <recreated-file>
and your repository is good again! 您的存储库又好了!
(Btw, you could have ignored the fsck, and started with doing a. (顺便说一句,您可以忽略 fsck,然后开始执行)。
$ git log --raw --all
and just looked for the sha of the missing object (4b9458b) in that whole thing. It’s up to you—Git does have a lot of information, it is just missing one particular blob version. 并在其中查找丢失对象的 sha(4b9458b)。这取决于您——Git确实有大量信息,只是缺少一个特定的 blob 版本。
The index. 索引
The index is a binary file (generally kept in .git/index) containing a sorted list of path names, each with permissions and the SHA-1 of a blob object; git-ls-files[1] can show you the contents of the index. 索引是一个二进制文件(通常保存在 .git/index 中),包含一个排序的路径名列表,每个路径名都有权限和 blob 对象的 SHA-1;git-ls-files[1] 可以显示索引的内容。
$ git ls-files --stage 100644 63c918c667fa005ff12ad89437f2fdc80926e21c 0 .gitignore 100644 5529b198e8d14decbe4ad99db3f7fb632de0439d 0 .mailmap 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0 COPYING 100644 a37b2152bd26be2c2289e1f57a292534a51a93c7 0 Documentation/.gitignore 100644 fbefe9a45b00a54b58d94d06eca48b03d40a50e0 0 Documentation/Makefile ... 100644 2511aef8d89ab52be5ec6a5e46236b4b6bcd07ea 0 xdiff/xtypes.h 100644 2ade97b2574a9f77e7ae4002a4e07a6a38e46d07 0 xdiff/xutils.c 100644 d5de8292e05e7c36c4b68857c1cf9855e3d2f70a 0 xdiff/xutils.h
Note that in older documentation you may see the index called the "current directory cache" or just the "cache". It has three important properties. 请注意,在旧文档中,您可能会看到索引被称为“当前目录缓存”或仅称为“缓存”。它有三个重要属性。
-
The index contains all the information necessary to generate a single (uniquely determined) tree object. 索引包含生成单个(唯一确定的)tree 对象所需的所有信息。
For example, running git-commit[1] generates this tree object from the index, stores it in the object database, and uses it as the tree object associated with the new commit. 例如,运行 git-commit[1] 会从索引生成此 tree 对象,将其存储在对象数据库中,并将其用作与新提交关联的 tree 对象。
-
The index enables fast comparisons between the tree object it defines and the working tree. 索引能够快速比较它定义的 tree 对象和工作目录。
It does this by storing some additional data for each entry (such as the last modified time). This data is not displayed above, and is not stored in the created tree object, but it can be used to determine quickly which files in the working directory differ from what was stored in the index, and thus save Git from having to read all of the data from such files to look for changes. 它通过为每个条目存储一些额外数据(例如最后修改时间)来实现这一点。此数据未在上面显示,也不会存储在创建的 tree 对象中,但它可以用来快速确定工作目录中的哪些文件与索引中存储的内容不同,从而避免 Git 读取所有这些文件的数据来查找更改。
-
It can efficiently represent information about merge conflicts between different tree objects, allowing each pathname to be associated with sufficient information about the trees involved that you can create a three-way merge between them. 它可以有效地表示不同 tree 对象之间的合并冲突信息,允许将每个路径名与涉及的 tree 的足够信息相关联,以便您可以创建它们之间的三方合并。
We saw in Getting conflict-resolution help during a merge that during a merge the index can store multiple versions of a single file (called "stages"). The third column in the git-ls-files[1] output above is the stage number, and will take on values other than 0 for files with merge conflicts. 在 Getting conflict-resolution help during a merge 中我们看到,在合并期间,索引可以存储单个文件的多个版本(称为“阶段”)。上面 git-ls-files[1] 输出中的第三列是阶段号,对于有合并冲突的文件,它将取非 0 的值。
The index is thus a sort of temporary staging area, which is filled with a tree which you are in the process of working on. 因此,索引是一种临时暂存区,其中填充了您正在处理的 tree。
If you blow the index away entirely, you generally haven’t lost any information as long as you have the name of the tree that it described. 如果您完全清除索引,通常不会丢失任何信息,只要您拥有它所描述的 tree 的名称。
Submodules. 子模块
Large projects are often composed of smaller, self-contained modules. For example, an embedded Linux distribution’s source tree would include every piece of software in the distribution with some local modifications; a movie player might need to build against a specific, known-working version of a decompression library; several independent programs might all share the same build scripts. 大型项目通常由更小的、独立的模块组成。例如,嵌入式 Linux 发行版的源代码树会包含发行版中的所有软件及其本地修改;电影播放器可能需要针对特定、已知工作的解压缩库版本进行构建;多个独立程序可能共享相同的构建脚本。
With centralized revision control systems this is often accomplished by including every module in one single repository. Developers can check out all modules or only the modules they need to work with. They can even modify files across several modules in a single commit while moving things around or updating APIs and translations. 在集中式版本控制系统中,这通常通过将所有模块包含在一个存储库中来实现。开发人员可以签出所有模块或仅签出他们需要处理的模块。他们甚至可以在移动内容或更新 API 和翻译时,在单个提交中修改多个模块的文件。
Git does not allow partial checkouts, so duplicating this approach in Git would force developers to keep a local copy of modules they are not interested in touching. Commits in an enormous checkout would be slower than you’d expect as Git would have to scan every directory for changes. If modules have a lot of local history, clones would take forever. Git 不允许部分签出,因此在 Git 中复制这种方法将迫使开发人员保留他们不感兴趣的模块的本地副本。在一个巨大的签出中进行提交会比您预期的慢,因为 Git 需要扫描每个目录以查找更改。如果模块有大量的本地历史记录,克隆将花费永恒的时间。
On the plus side, distributed revision control systems can much better integrate with external sources. In a centralized model, a single arbitrary snapshot of the external project is exported from its own revision control and then imported into the local revision control on a vendor branch. All the history is hidden. With distributed revision control you can clone the entire external history and much more easily follow development and re-merge local changes. 优点是,分布式版本控制系统可以更好地与外部源集成。在集中式模型中,外部项目的单个任意快照会从其自己的版本控制中导出,然后导入到供应商分支上的本地版本控制中。所有历史记录都隐藏起来。使用分布式版本控制,您可以克隆完整的外部历史记录,并更轻松地跟踪开发和重新合并本地更改。
Git’s submodule support allows a repository to contain, as a subdirectory, a checkout of an external project. Submodules maintain their own identity; the submodule support just stores the submodule repository location and commit ID, so other developers who clone the containing project ("superproject") can easily clone all the submodules at the same revision. Partial checkouts of the superproject are possible: you can tell Git to clone none, some or all of the submodules. Git 的子模块支持允许存储库作为子目录包含外部项目的签出。子模块保持自己的身份;子模块支持只存储子模块存储库的位置和 commit ID,因此克隆包含项目(“superproject”)的其他开发人员可以轻松地在相同的版本上克隆所有子模块。superproject 的部分签出是可能的:您可以告诉 Git 克隆零个、一些或所有子模块。
The git-submodule[1] command is available since Git 1.5.3. Users with Git 1.5.2 can look up the submodule commits in the repository and manually check them out; earlier versions won’t recognize the submodules at all. git-submodule[1] 命令自 Git 1.5.3 起可用。拥有 Git 1.5.2 的用户可以在存储库中查找子模块 commit 并手动签出;早期版本根本不识别子模块。
To see how submodule support works, create four example repositories that can be used later as a submodule. 要查看子模块支持如何工作,请创建四个稍后可用作子模块的示例存储库。
$ 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
Now create the superproject and add all the submodules. 现在创建 superproject 并添加所有子模块。
$ mkdir super $ cd super $ git init $ for i in a b c d do git submodule add ~/git/$i $i done
|
注意
|
Do not use local URLs here if you plan to publish your superproject! 如果您打算发布您的 superproject,请不要在此处使用本地 URL! |
See what files git submodule created. 查看 git submodule 创建了哪些文件。
$ ls -a . .. .git .gitmodules a b c d
The git submodule add <repo> <path> command does a couple of things. git submodule add <repo> <path> 命令执行以下几项操作。
-
It clones the submodule from <repo> to the given <path> under the current directory and by default checks out the master branch. 它将子模块从 <repo> 克隆到当前目录下的指定 <path>,并默认签出 master 分支。
-
It adds the submodule’s clone path to the gitmodules[5] file and adds this file to the index, ready to be committed. 它将子模块的克隆路径添加到 gitmodules[5] 文件,并将此文件添加到索引,准备提交。
-
It adds the submodule’s current commit ID to the index, ready to be committed. 它将子模块的当前 commit ID 添加到索引,准备提交。
Commit the superproject. 提交 superproject。
$ git commit -m "Add submodules a, b, c and d."
Now clone the superproject. 现在克隆 superproject。
$ cd .. $ git clone super cloned $ cd cloned
The submodule directories are there, but they’re empty. 子模块目录存在,但它们是空的。
$ ls -a a . .. $ git submodule status -d266b9873ad50488163457f025db7cdd9683d88b a -e81d457da15309b4fef4249aba9b50187999670d b -c1536a972b9affea0f16e0680ba87332dc059146 c -d96249ff5d57de5de093e6baff9e0aafa5276a74 d
|
注意
|
The commit object names shown above would be different for you, but they should match the HEAD commit object names of your repositories. You can check it by running git ls-remote ../a. 上面显示的 commit 对象名称对您来说会不同,但它们应该与您存储库的 HEAD commit 对象名称匹配。您可以通过运行 git ls-remote ../a 来检查。 |
Pulling down the submodules is a two-step process. First run git submodule init to add the submodule repository URLs to .git/config. 拉取子模块是一个两步过程。首先运行 git submodule init 将子模块存储库 URL 添加到 .git/config。
$ git submodule init
Now use git submodule update to clone the repositories and check out the commits specified in the superproject. 现在使用 git submodule update 来克隆存储库并签出 superproject 中指定的 commit。
$ git submodule update $ cd a $ ls -a . .. .git a.txt
One major difference between git submodule update and git submodule add is that git submodule update checks out a specific commit, rather than the tip of a branch. It’s like checking out a tag: the head is detached, so you’re not working on a branch. git submodule update 和 git submodule add 之间的一个主要区别是 git submodule update 签出的是一个特定的 commit,而不是分支的最新提交。这就像签出一个 tag:HEAD 是分离的,所以您不是在分支上工作。
$ git branch * (detached from d266b98) master
If you want to make a change within a submodule and you have a detached head, then you should create or checkout a branch, make your changes, publish the change within the submodule, and then update the superproject to reference the new commit. 如果您想在子模块中进行更改并且您有一个分离的 HEAD,那么您应该创建一个或签出一个分支,进行更改,在子模块中发布更改,然后更新 superproject 以引用新的 commit。
$ git switch master
或
$ git switch -c fix-up
then. 然后。
$ 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
You have to run git submodule update after git pull if you want to update submodules, too. 如果您也想更新子模块,则必须在 git pull 之后运行 git submodule update。
Pitfalls with submodules. 子模块的陷阱
Always publish the submodule change before publishing the change to the superproject that references it. If you forget to publish the submodule change, others won’t be able to clone the repository. 始终在发布引用它的 superproject 的更改之前发布子模块的更改。如果您忘记发布子模块的更改,其他人将无法克隆存储库。
$ 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'
In older Git versions it could be easily forgotten to commit new or modified files in a submodule, which silently leads to similar problems as not pushing the submodule changes. Starting with Git 1.7.0 both git status and git diff in the superproject show submodules as modified when they contain new or modified files to protect against accidentally committing such a state. git diff will also add a -dirty to the work tree side when generating patch output or used with the --submodule option. 在旧的 Git 版本中,很容易忘记提交子模块中的新文件或修改过的文件,这会静默地导致与不推送子模块更改类似的问题。从 Git 1.7.0 开始,superproject 中的 git status 和 git diff 在子模块包含新文件或修改过的文件时会显示为已修改,以防止意外提交此类状态。git diff 在生成补丁输出或与 --submodule 选项一起使用时,还会向工作树侧添加 -dirty。
$ git diff diff --git a/sub b/sub --- a/sub +++ b/sub @@ -1 +1 @@ -Subproject commit 3f356705649b5d566d97ff843cf193359229a453 +Subproject commit 3f356705649b5d566d97ff843cf193359229a453-dirty $ git diff --submodule Submodule sub 3f35670..3f35670-dirty:
You also should not rewind branches in a submodule beyond commits that were ever recorded in any superproject. 您也不应该将子模块中的分支回溯到任何 superproject 中记录过的 commit 之前。
It’s not safe to run git submodule update if you’ve made and committed changes within a submodule without checking out a branch first. They will be silently overwritten. 如果您在没有先签出分支的情况下在子模块中进行了更改并提交了,那么运行 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
|
注意
|
The changes are still visible in the submodule’s reflog. 更改在子模块的 reflog 中仍然可见。 |
If you have uncommitted changes in your submodule working tree, git submodule update will not overwrite them. Instead, you get the usual warning about not being able switch from a dirty branch. 如果您的子模块工作目录中有未提交的更改,git submodule update 不会覆盖它们。相反,您会收到关于无法从脏分支切换的常规警告。
Low-level Git operations. 低级 Git 操作
Many of the higher-level commands were originally implemented as shell scripts using a smaller core of low-level Git commands. These can still be useful when doing unusual things with Git, or just as a way to understand its inner workings. 许多高级命令最初是作为 shell 脚本实现的,使用了更小的低级 Git 命令核心。当使用 Git 做不寻常的事情时,或者仅仅是为了理解其内部工作原理,它们仍然有用。
Object access and manipulation. 对象访问和操作
The git-cat-file[1] command can show the contents of any object, though the higher-level git-show[1] is usually more useful. git-cat-file[1] 命令可以显示任何对象的内容,尽管更高级的 git-show[1] 通常更有用。
The git-commit-tree[1] command allows constructing commits with arbitrary parents and trees. git-commit-tree[1] 命令允许使用任意 parent 和 tree 来构建提交。
A tree can be created with git-write-tree[1] and its data can be accessed by git-ls-tree[1]. Two trees can be compared with git-diff-tree[1]. 可以使用 git-write-tree[1] 创建 tree,并可以通过 git-ls-tree[1] 访问其数据。可以使用 git-diff-tree[1] 比较两个 tree。
A tag is created with git-mktag[1], and the signature can be verified by git-verify-tag[1], though it is normally simpler to use git-tag[1] for both. tag 是使用 git-mktag[1] 创建的,并且可以使用 git-verify-tag[1] 验证签名,尽管通常使用 git-tag[1] 来同时完成这两项操作更简单。
The Workflow. 工作流程
High-level operations such as git-commit[1] and git-restore[1] work by moving data between the working tree, the index, and the object database. Git provides low-level operations which perform each of these steps individually. git-commit[1] 和 git-restore[1] 等高级操作通过在工作目录、索引和对象数据库之间移动数据来工作。Git 提供低级操作来单独执行这些步骤中的每一步。
Generally, all Git operations work on the index file. Some operations work purely on the index file (showing the current state of the index), but most operations move data between the index file and either the database or the working directory. Thus there are four main combinations. 通常,所有 Git 操作都作用于索引文件。有些操作仅作用于索引文件(显示索引的当前状态),但大多数操作在索引文件与数据库或工作目录之间移动数据。因此有四种主要组合。
working directory → index. 工作目录 → 索引
The git-update-index[1] command updates the index with information from the working directory. You generally update the index information by just specifying the filename you want to update, like so. git-update-index[1] 命令使用工作目录中的信息更新索引。您通常只需指定要更新的文件名即可更新索引信息,例如。
$ git update-index filename
but to avoid common mistakes with filename globbing etc., the command will not normally add totally new entries or remove old entries, i.e. it will normally just update existing cache entries. 但为了避免文件名通配等常见错误,该命令通常不会添加全新的条目或删除旧的条目,也就是说,它通常只会更新现有的缓存条目。
To tell Git that yes, you really do realize that certain files no longer exist, or that new files should be added, you should use the --remove and --add flags respectively. 要告诉 Git 是的,您确实意识到某些文件已不存在,或者应该添加新文件,您应该分别使用 --remove 和 --add 标志。
NOTE! A --remove flag does not mean that subsequent filenames will necessarily be removed: if the files still exist in your directory structure, the index will be updated with their new status, not removed. The only thing --remove means is that update-index will be considering a removed file to be a valid thing, and if the file really does not exist any more, it will update the index accordingly. 注意!--remove 标志不意味着后续文件名一定会被删除:如果文件仍然存在于您的目录结构中,索引将更新为它们的新状态,而不是被删除。 --remove 仅表示 update-index 将考虑删除的文件是有效的内容,如果文件确实不再存在,它将相应地更新索引。
As a special case, you can also do git update-index --refresh, which will refresh the "stat" information of each index to match the current stat information. It will not update the object status itself, and it will only update the fields that are used to quickly test whether an object still matches its old backing store object. 作为一种特殊情况,您还可以执行 git update-index --refresh,它将刷新每个索引的“stat”信息以匹配当前的 stat 信息。它不会更新对象本身的状态,并且它只会更新用于快速测试对象是否仍与其旧的后台存储对象匹配的字段。
The previously introduced git-add[1] is just a wrapper for git-update-index[1]. 之前介绍的 git-add[1] 只是 git-update-index[1] 的一个包装器。
index → object database. 索引 → 对象数据库
You write your current index file to a "tree" object with the program. 您使用程序将当前的索引文件写入一个“tree”对象。
$ git write-tree
that doesn’t come with any options—it will just write out the current index into the set of tree objects that describe that state, and it will return the name of the resulting top-level tree. You can use that tree to re-generate the index at any time by going in the other direction. 它没有任何选项——它只会将当前索引写入描述该状态的 tree 对象集合,并返回结果的顶级 tree 的名称。您可以在任何时候通过反向操作来使用该 tree 来重新生成索引。
object database → index. 对象数据库 → 索引
You read a "tree" file from the object database, and use that to populate (and overwrite—don’t do this if your index contains any unsaved state that you might want to restore later!) your current index. Normal operation is just. 您从对象数据库读取一个“tree”文件,并使用它来填充(并覆盖——如果您的索引包含任何您以后可能想恢复的未保存状态,请不要这样做!)您的当前索引。正常操作只是。
$ git read-tree <SHA-1 of tree>
and your index file will now be equivalent to the tree that you saved earlier. However, that is only your index file: your working directory contents have not been modified. 并且您的索引文件现在将等同于您之前保存的 tree。但是,那只是您的索引文件:您的工作目录内容尚未被修改。
index → working directory. 索引 → 工作目录
You update your working directory from the index by "checking out" files. This is not a very common operation, since normally you’d just keep your files updated, and rather than write to your working directory, you’d tell the index files about the changes in your working directory (i.e. git update-index). 您通过“签出”文件来从索引更新您的工作目录。这不是一个非常常见的操作,因为通常您会保持文件更新,而不是写入您的工作目录,您会告诉索引文件您工作目录中的更改(即 git update-index)。
However, if you decide to jump to a new version, or check out somebody else’s version, or just restore a previous tree, you’d populate your index file with read-tree, and then you need to check out the result with. 但是,如果您决定跳转到新版本,或签出别人的版本,或仅恢复以前的 tree,您将使用 read-tree 填充您的索引文件,然后您需要使用…签出结果。
$ git checkout-index filename
or, if you want to check out all of the index, use -a. 或者,如果您想签出索引中的所有内容,请使用 -a。
NOTE! git checkout-index normally refuses to overwrite old files, so if you have an old version of the tree already checked out, you will need to use the -f flag (before the -a flag or the filename) to force the checkout. 注意!git checkout-index 通常拒绝覆盖旧文件,因此如果您已经签出了旧版本的 tree,您将需要使用 -f 标志(在 -a 标志或文件名之前)来强制签出。
Finally, there are a few odds and ends which are not purely moving from one representation to the other. 最后,还有一些零散的东西,它们不是纯粹地从一种表示形式移动到另一种形式。
Tying it all together. 将它们联系起来
要提交您通过 git write-tree 实例化出来的树,您需要创建一个“提交”对象,该对象引用该树以及其背后的历史记录——最重要的是,它引用了在历史记录中排在其前面的“父”提交。
通常,“提交”只有一个父提交:在某个更改发生之前的树的先前状态。但是,有时它可能有两个或更多父提交,在这种情况下,我们称之为“合并”,因为这种提交会将由其他提交表示的两个或多个先前状态“合并”在一起。
换句话说,虽然“树”表示工作目录的特定目录状态,“提交”表示该状态在时间轴上的位置,并解释了我们是如何到达那里的。
您通过提供描述提交时状态的树以及父提交列表来创建一个提交对象
$ git commit-tree <tree> -p <parent> [(-p <parent2>)...]
然后通过标准输入(通过管道重定向或文件,或者直接在终端输入)提供提交原因。
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,它将二进制内容转换为更易读的形式。
查看“提交”对象尤其有启发性,因为它们通常很小且相当自explanatory。特别是,如果您遵循将顶级提交名称放在 .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 中完成的。例如,如果文件从 $orig 到 HEAD 或 $target 没有改变,或者如果文件从 $orig 到 HEAD 和从 $orig 到 $target 的方式相同,那么最终结果显然是 HEAD 中的内容。上面的例子表明,文件 hello.c 从 $orig 到 HEAD 和从 $orig 到 $target 的方式不同。您可以通过自己运行您喜欢的 3 方合并程序,例如 diff3、merge,或 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 程序,它将阶段提取到临时文件中,并在此之上调用一个“merge”脚本
$ git merge-index git-merge-one-file hello.c
这就是高级别的 git merge -s resolve 的实现方式。
深入 Git
本章涵盖了 Git 实现的内部细节,可能只有 Git 开发者才需要理解。
对象存储格式
所有对象都有一个静态确定的“类型”,该类型标识对象的格式(即它的用途以及它如何引用其他对象)。目前有四种不同的对象类型:“blob”、“tree”、“commit”和“tag”。
无论对象类型如何,所有对象都具有以下共同特征:它们都使用 zlib 进行 deflated,并且有一个头部,该头部不仅指定了它们的类型,还提供了有关对象中数据大小的信息。值得注意的是,用于命名对象的 SHA-1 哈希是原始数据加上此头部的哈希,因此 sha1sum file 与 file 的对象名称不匹配(Git 的早期版本哈希略有不同,但结论仍然相同)。
以下是一个简短的示例,演示了如何手动生成这些哈希
假设有一个包含一些简单内容的小文本文件
$ echo "Hello world" >hello.txt
我们现在可以手动生成 Git 将为此文件使用的哈希
-
我们想要哈希的对象类型是“blob”,大小为 12 字节。
-
在文件内容前加上对象头部,然后将其馈送给
sha1sum
$ { printf "blob 12\0"; cat hello.txt; } | sha1sum
802992c4220de19a90767f3000a79a31b98d0df7 -
可以使用 git hash-object 来验证手动构建的哈希,它当然隐藏了头部的添加
$ git hash-object hello.txt 802992c4220de19a90767f3000a79a31b98d0df7
因此,可以通过验证 (a) 其哈希与文件内容匹配,以及 (b) 对象成功解压为字节流,从而形成一系列 <ascii-type-without-space> + <space> + <ascii-decimal-size> + <byte\0> + <binary-object-data>,来独立于内容或对象类型来测试对象的总体一致性。
结构化对象可以进一步验证其结构和与其他对象的连接性。这通常通过 git fsck 程序来完成,该程序生成所有对象的完整依赖图,并验证其内部一致性(除了通过哈希验证其表面一致性之外)。
Git 源代码鸟瞰图
新开发者要找到 Git 源代码的路径并不总是容易的。本节为您提供一些指导,说明从哪里开始。
一个好的开始点是查看初始提交的内容,使用
$ git switch --detach e83c5163
初始修订奠定了 Git 如今几乎所有内容的基础(尽管在少数地方细节可能有所不同),但它足够小,可以一次性阅读。
请注意,术语自该修订以来已经发生了变化。例如,该修订中的 README 使用“changeset”(变更集)一词来描述我们现在称之为提交的内容。
此外,我们不再称之为“cache”(缓存),而是“index”(索引);但是,该文件仍称为 read-cache.h。
如果您理解了初始提交中的思想,您应该查看一个更近期的版本,并浏览 read-cache-ll.h、object.h 和 commit.h。
在早期,Git(遵循 UNIX 的传统)是一堆极其简单的程序,您可以在脚本中使用它们,将一个程序的输出通过管道传递给另一个程序。这对于初始开发来说是很好的,因为它更容易测试新事物。然而,最近许多这些部分已成为内置命令,并且一些核心已被“lib化”,即为了性能、可移植性以及避免代码重复而被放入 libgit.a 中。
现在,您知道索引是什么了(并在 read-cache-ll.h 中找到相应的数据结构),并且只有几种对象类型(blob、tree、commit 和 tag),它们从 struct object 继承了它们的通用结构,而 struct object 是它们的第一个成员(因此,您可以将例如 (struct object *)commit 强制转换为 &commit->object,即访问对象名称和标志)。
现在是时候休息一下,让这些信息沉淀下来了。
下一步:熟悉对象命名。阅读 命名提交。命名对象(不仅是修订!)有相当多的方法。所有这些都在 sha1_name.c 中处理。只需快速查看 get_sha1() 函数。许多特殊处理是由 get_sha1_basic() 或类似的函数完成的。
这只是为了让您熟悉 Git 中最“lib化”的部分:修订遍历器。
基本上,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.c 和 revision.h 中。它将选项包装在一个名为 rev_info 的结构体中,该结构体控制如何以及遍历哪些修订,等等。
git rev-parse 的原始任务现在由 setup_revisions() 函数承担,该函数解析修订和修订遍历器的通用命令行选项。这些信息存储在 rev_info 结构体中供以后使用。调用 setup_revisions() 后,您可以自行解析命令行选项。之后,您必须调用 prepare_revision_walk() 进行初始化,然后您就可以通过 get_revision() 函数逐个获取提交。
如果您对修订遍历过程的更多细节感兴趣,只需查看 cmd_log() 的第一个实现;调用 git show v1.3.0~155^2~4 并向下滚动到该函数(请注意,您不再需要直接调用 setup_pager())。
如今,git log 是一个内置命令,这意味着它包含在 git 命令中。内置命令的源代码部分是
-
一个名为
cmd_<bla> 的函数,通常定义在 builtin/<bla.c> 中(请注意,Git 的旧版本曾经将其放在builtin-<bla>.c中),并在builtin.h中声明。 -
在
git.c的commands[] 数组中的一个条目,以及 -
在
Makefile的BUILTIN_OBJECTS中的一个条目。
有时,一个源文件包含多个内置命令。例如,cmd_show() 和 cmd_log() 都位于 builtin/log.c 中,因为它们共享相当多的代码。在这种情况下,名称与它们所在 .c 文件名称不同的命令必须在 Makefile 的 BUILT_INS 中列出。
git log 在 C 语言中的实现比原始脚本看起来更复杂,但这提供了更大的灵活性和性能。
在这里,又是一个可以暂停一下的好时机。
第三课是:研究代码。真的,这是了解 Git 组织的最佳方式(在您了解基本概念之后)。
所以,想想您感兴趣的事情,比如,“如何仅凭对象的名称访问一个 blob?”。第一步是找到一个 Git 命令,您可以用它来做到这一点。在此示例中,它是 git show 或 git cat-file。
为了清晰起见,我们仍然以 git cat-file 为例,因为它
-
是底层命令,并且
-
在初始提交时就已经存在了(它在
cat-file.c中经历了大约 20 次修订,在成为内置命令后被重命名为builtin/cat-file.c,然后经历了不到 10 个版本)。
所以,查看 builtin/cat-file.c,搜索 cmd_cat_file() 并查看它的作用。
repo_config(the_repository, git_default_config);
if (argc != 3)
usage("git cat-file [-t|-s|-e|-p|<type>] <sha1>");
if (get_sha1(argv[2], sha1))
die("Not a valid object name %s", argv[2]);
我们跳过显而易见的部分;唯一真正有趣的部分是调用 get_sha1()。它尝试将 argv[2] 解释为对象名称,如果它引用当前存储库中存在的一个对象,它会将生成的 SHA-1 写入变量 sha1。
这里有两件事很有趣
-
get_sha1() 在成功时返回 0。这可能会让一些新的 Git 黑客感到惊讶,但 UNIX 的悠久传统是在发生不同错误时返回不同的负数——成功时返回 0。 -
get_sha1() 函数签名中的变量sha1是unsignedchar*,但实际上期望它是一个指向unsignedchar[20] 的指针。该变量将包含给定提交的 160 位 SHA-1。请注意,每当 SHA-1 作为unsignedchar*传递时,它就是二进制表示,而不是十六进制字符的 ASCII 表示,后者作为char*传递。
您将在代码中看到这两种情况。
现在,进入核心部分
case 0:
buf = odb_read_object_peeled(r->objects, sha1, argv[1], &size, NULL);
这就是读取 blob(实际上,不只是 blob,而是任何类型的对象)的方法。要了解 odb_read_object_peeled() 函数实际上是如何工作的,请找到它的源代码(在 Git 存储库中可能类似于 git grep read_object_with | grep ":[a-z]"),然后阅读源代码。
要了解结果如何使用,只需继续阅读 cmd_cat_file()
write_or_die(1, buf, size);
有时,您不知道要在哪里查找某个功能。在许多此类情况下,通过搜索 git log 的输出,然后 git show 相应的提交,会有所帮助。
示例:如果您知道有一个 git bundle 的测试用例,但不记得它在哪里(是的,您可以 git grep bundle t/,但这并没有说明重点!)
$ git log --no-merges t/
在分页器(less)中,只需搜索“bundle”,然后回溯几行,您就会发现它在提交 18449ab0 中。现在只需复制此对象名称,然后将其粘贴到命令行
$ git show 18449ab0
搞定。
另一个示例:找出如何将某个脚本变成内置命令
$ git log --no-merges --diff-filter=A builtin/*.c
您看,Git 实际上是了解 Git 自身源代码的最佳工具!
Git 词汇表
Git 解释
- 备用对象数据库
- 裸仓库
-
裸仓库通常是一个以适当命名的、带有
.git后缀的目录,但没有本地签出的任何受版本控制的文件副本。也就是说,通常存在于隐藏的.git子目录中的所有 Git 管理和控制文件都直接存在于repository.git目录中,并且不存在其他文件和签出的文件。公共仓库的发布者通常会提供裸仓库。 - blob 对象
-
未类型化的对象,例如文件的内容。
- 分支
-
“分支”是一条开发线。分支上最新的提交被称为该分支的尖端。分支的尖端由一个分支头引用,当在该分支上进行额外开发时,它会向前移动。一个 Git 仓库可以跟踪任意数量的分支,但您的工作树仅与其中一个(“当前”或“已签出的”分支)相关联,并且 HEAD 指向该分支。
- 缓存
-
已废弃,用于:索引。
- 链
- 变更集
-
BitKeeper/cvsps 中“提交”的说法。由于 Git 不存储更改,而是存储状态,因此使用“变更集”一词对 Git 来说确实没有意义。
- 签出
-
通过树对象或blob更新工作树的全部或部分的操作,并将索引和HEAD更新为指向新的分支(如果整个工作树都指向了新分支)。
- 挑选
-
在SCM行话中,“cherry pick”(挑选)意味着从一系列更改(通常是提交)中选择一个子集,并将其作为新一系列更改记录在不同的代码库之上。在 Git 中,这通过“git cherry-pick”命令执行,以提取现有提交引入的更改,并将其记录在当前分支的尖端之上,作为新的提交。
- 干净
- 提交
-
名词:Git 历史中的一个点;项目的整个历史表示为一组相互关联的提交。Git 中“提交”一词的使用场合与其它版本控制系统使用“修订”或“版本”的场合相同。也用作提交对象的简写。
- 提交图概念、表示和用法
-
对象数据库中提交形成的DAG结构的同义词,由分支尖端引用,使用它们链式链接的提交。该结构是最终的提交图。该图可以用其他方式表示,例如“提交图”文件。
- 提交图文件
-
“提交图”(通常是带连字符的)文件是提交图的补充表示,它加快了提交图的遍历。“提交图”文件存储在 .git/objects/info 目录或备用对象数据库的 info 目录中。
- 提交对象
- commit-ish(也称为 committish)
-
一个提交对象或一个可以递归解引用到提交对象的对象。以下都是 commit-ishes:一个提交对象,一个指向提交对象的标签对象,一个指向指向提交对象的标签对象的标签对象,等等。
- 核心 Git
-
Git 的基本数据结构和实用程序。仅公开有限的源代码管理工具。
- DAG
-
有向无环图。由于提交对象具有父提交(有向),并且提交对象图是无环的(没有以相同对象开始和结束的链),因此提交对象构成一个有向无环图。
- 悬空对象
- 解引用
-
指向符号引用:访问由符号引用指向的引用的操作。递归解引用涉及在结果引用上重复上述过程,直到找到非符号引用。
指向提交对象:访问提交的树对象的操作。提交不能递归解引用。
除非另有说明,“解引用”在 Git 命令或协议的上下文中是隐式递归的。
- 分离 HEAD
-
通常 HEAD 存储一个分支的名称,并且对 HEAD 代表的历史进行操作的命令是对到达 HEAD 所指向的分支尖端处的历史进行操作。但是,Git 也允许您签出一个任意的提交,该提交不一定是任何特定分支的尖端。处于这种状态的 HEAD 被称为“分离”。
请注意,在 HEAD 分离状态下,对当前分支历史进行操作的命令(例如,
gitcommit以在其之上构建新历史)仍然有效。它们会更新 HEAD 以指向更新后的历史的尖端,而不会影响任何分支。显然,更新或查询关于当前分支信息的命令(例如,设置当前分支集成的远程跟踪分支的gitbranch--set-upstream-to)将无法正常工作,因为在这种状态下没有(真实的)当前分支可以询问。 - 目录
-
您通过“ls”获得的列表 :-)
- 脏
- 恶性合并
- 快进
-
快进是一种特殊的合并类型,其中您有一个修订,并且您正在“合并”另一个分支的更改,而这些更改恰好是您已有更改的后代。在这种情况下,您不会创建一个新的合并提交,而是将您的分支更新为指向与您正在合并的分支相同的修订。这种情况在远程仓库的远程跟踪分支上会经常发生。
- 获取
-
获取分支意味着从远程仓库获取该分支的头引用,以找出本地对象数据库中缺少哪些对象,并获取它们。另请参阅 git-fetch[1]。
- 文件系统
-
Linus Torvalds 最初设计 Git 是为了作为用户空间文件系统,即用于存储文件和目录的基础设施。这保证了 Git 的效率和速度。
- Git 归档
-
仓库(对 arch 人员而言)的同义词。
- gitfile
-
工作树根目录下的一个纯文件
.git,它指向实际仓库的目录。有关正确用法,请参阅 git-worktree[1] 或 git-submodule[1]。有关语法,请参阅 gitrepository-layout[5]。 - 嫁接
-
嫁接通过记录提交的伪祖先信息,使得两条原本不同的开发线能够连接在一起。这样,您可以让 Git 假装提交所拥有的父提交集合与创建提交时记录的不同。通过
.git/info/grafts文件进行配置。请注意,嫁接机制已过时,可能导致仓库间对象传输出现问题;请参阅git-replace[1]以获取更灵活和健壮的实现相同功能的系统。
- 哈希
-
在 Git 的上下文中,是对象名称的同义词。
- 头
-
对分支末端的提交的命名引用。主干(Heads)存储在
$GIT_DIR/refs/heads/目录下的文件中,除非使用打包引用(packed refs)。(参见 git-pack-refs[1]。) - HEAD
-
当前分支。更详细地说:你的工作目录通常派生自HEAD引用的树的状态。HEAD是对你仓库中的一个主干的引用,除非在使用分离HEAD时,此时它直接引用任意提交。
- 主干引用
-
与主干同义。
- 钩子
-
在一些Git命令的正常执行过程中,会调用可选的脚本,允许开发者添加功能或检查。通常,钩子允许命令在执行前进行验证并可能中止,以及在操作完成后进行后通知。钩子脚本位于
$GIT_DIR/hooks/目录中,只需从文件名中删除.sample后缀即可启用。在早期版本的Git中,你还需要使它们可执行。 - 索引
-
包含文件状态信息,其内容作为对象存储的集合。索引是你工作目录的存储版本。实际上,它还可以包含工作目录的第二版甚至第三版,这些版本在合并时使用。
- 索引条目
-
存储在索引中关于特定文件的信息。如果一个合并已开始但尚未完成(即索引包含该文件的多个版本),则该索引条目可能是未合并的。
- 主分支
-
默认的开发分支。每当你创建一个Git仓库时,都会创建一个名为“master”的分支,并将其设为活动分支。在大多数情况下,这包含本地开发,尽管这纯粹是约定俗成,并非强制要求。
- 合并
-
作为动词:将另一个分支的内容(可能来自外部仓库)引入当前分支。如果合并的分支来自不同的仓库,则首先通过抓取远程分支,然后将结果合并到当前分支。这种抓取和合并操作的组合称为拉取。合并通过一个自动过程执行,该过程识别自分支分叉以来所做的更改,然后将所有这些更改应用在一起。在发生冲突的情况下,可能需要手动干预才能完成合并。
- 对象
-
Git中的存储单位。它通过其内容的SHA-1哈希值进行唯一标识。因此,对象的内容不能被更改。
- 对象数据库
- 对象标识符(oid)
-
与对象名同义。
- 对象名
-
对象的唯一标识符。对象名通常表示为一个40个字符的十六进制字符串。也通俗地称为SHA-1。
- 对象类型
- 章鱼合并
-
合并两个以上的分支。
- 孤儿
-
切换到一个尚不存在(即,一个未出生的分支)的分支。执行此操作后,创建的第一个提交将成为一个没有父提交的提交,从而开始一个新的历史。
- origin
-
默认的上游仓库。大多数项目至少有一个它们跟踪的上游项目。默认情况下,origin用于此目的。新的上游更新将被抓取到名为origin/upstream-branch-name的远程跟踪分支中,您可以使用
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
-
魔术单词
top(魔术签名:/)使模式从工作目录的根目录开始匹配,即使您正在从子目录中运行命令。 - literal
-
模式中的通配符,如
*或?,被视为字面字符。 - icase
-
不区分大小写的匹配。
- glob
-
Git将模式视为shell glob,适用于fnmatch(3)并带有FNM_PATHNAME标志:模式中的通配符不会匹配路径名中的/。例如,“Documentation/*.html”匹配“Documentation/git.html”,但不匹配“Documentation/ppc/ppc.html”或“tools/perf/Documentation/perf.html”。
与完整路径名匹配的模式中,两个连续的星号(
**)可能具有特殊含义:-
前导“
**”后跟斜杠表示在所有目录中匹配。例如,“**/foo”匹配任何地方的“foo”文件或目录。“**/foo/bar”匹配任何地方直接在“foo”目录下的“bar”文件或目录。 -
尾随的“
/**”匹配内部的所有内容。例如,“abc/**”匹配目录“abc”内的所有文件,相对于.gitignore文件所在的位置,深度无限。 -
斜杠后跟两个连续的星号,然后是斜杠,匹配零个或多个目录。例如,“
a/**/b”匹配“a/b”、“a/x/b”、“a/x/y/b”等等。 -
其他连续的星号被视为无效。
Glob魔术与字面魔术不兼容。
-
- attr
-
在
attr:之后是一个空格分隔的“属性要求”列表,所有这些都必须满足,路径才会被视为匹配;这与普通的非魔术路径说明符模式匹配是并行的。参见gitattributes[5]。路径的每个属性要求都采用以下形式之一:
-
"
ATTR"要求设置属性ATTR。 -
"-ATTR"要求未设置属性
ATTR。 -
"
ATTR=VALUE"要求属性ATTR设置为字符串VALUE。 -
"
!ATTR"要求属性ATTR未指定。请注意,在匹配树对象时,属性仍从工作目录获取,而不是从给定的树对象获取。
-
- exclude
-
在路径匹配任何非排除路径说明符后,它将通过所有排除路径说明符(魔术签名:
!或其同义词^)进行处理。如果匹配,则忽略该路径。当没有非排除路径说明符时,排除将应用于结果集,就好像没有指定任何路径说明符一样。
-
- 父提交
-
一个提交对象包含一个(可能为空的)开发线路上游(logical predecessor)列表,即其父提交。
- 剥离
- 锄头
-
术语锄头是指diffcore例程的一个选项,该选项有助于选择添加或删除给定文本字符串的更改。使用
--pickaxe-all选项,可以用来查看引入或删除某特定文本行的完整更改集。参见git-diff[1]。 - 底层命令
-
表示核心Git的昵称。
- 上层命令
-
表示依赖于核心Git的程序和程序套件的昵称,它们提供了对核心Git的高级访问。上层命令比底层命令更多地暴露了SCM的接口。
- 每个工作区引用
-
每个工作区的引用,而不是全局的。目前只有HEAD和所有以
refs/bisect/开头的引用,但将来可能会包含其他不常见的引用。 - 伪引用
-
一个具有与普通引用不同语义的引用。这些引用可以通过常规Git命令读取,但不能通过
git-update-ref[1]等命令写入。Git已知的伪引用如下:
-
FETCH_HEAD由git-fetch[1]或git-pull[1]写入。它可以引用多个对象ID。每个对象ID都附有元数据,指示其从何处抓取以及抓取状态。 -
MERGE_HEAD由git-merge[1]在解决合并冲突时写入。它包含正在合并的所有提交ID。
-
- 拉取
-
拉取一个分支意味着抓取它并合并它。另请参阅git-pull[1]。
- 推送
-
推送一个分支意味着从远程仓库获取该分支的主干引用,找出它是否是该分支本地主干引用的祖先,如果是,则将所有可达于本地主干引用但远程仓库缺失的对象放入远程对象数据库,并更新远程主干引用。如果远程主干不是本地主干的祖先,则推送失败。
- 可达
-
给定提交的所有祖先都被称为从该提交“可达”。更广泛地说,一个对象如果可以通过遵循标签到其标记的对象,提交到其父提交或树,以及树到其包含的树或文件的链从另一个对象到达该对象,则称其为可达。
- 可达性位图
-
可达性位图存储有关打包文件或多打包索引(MIDX)中选定提交集可达性的信息,以加快对象搜索速度。位图存储在“.bitmap”文件中。一个仓库最多只能有一个位图文件在使用中。位图文件可以属于单个打包文件,或仓库的多打包索引(如果存在)。
- 变基
- 引用
-
指向对象名或其他引用(后者称为符号引用)的名称。为了方便起见,在将引用用作Git命令的参数时,有时可以缩写;有关详细信息,请参阅gitrevisions[7]。引用存储在仓库中。
引用命名空间是分层的。引用名称必须以
refs/开头,或者位于层次结构的根目录。对于后者,其名称必须遵循以下规则:-
名称仅由大写字母或下划线组成。
-
名称以“
_HEAD”结尾,或等于“HEAD”。在层次结构的根目录有一些不符合这些规则的不规则引用。以下列表是详尽的,将来不应扩展:
-
AUTO_MERGE -
BISECT_EXPECTED_REV -
NOTES_MERGE_PARTIAL -
NOTES_MERGE_REF -
MERGE_AUTOSTASH不同的子层次结构用于不同的目的。例如,
refs/heads/层次结构用于表示本地分支,而refs/tags/层次结构用于表示本地标签。
-
- 引用日志
-
引用日志显示引用的本地“历史”。换句话说,它可以告诉您此仓库中的倒数第三次修订是什么,以及此仓库昨天下午9:14的当前状态是什么。有关详细信息,请参阅git-reflog[1]。
- 引用规范
-
“引用规范”由抓取和推送用于描述远程引用和本地引用之间的映射。有关详细信息,请参阅git-fetch[1]或git-push[1]。
- 远程仓库
- 远程跟踪分支
-
用于跟踪其他仓库更改的引用。它通常看起来像refs/remotes/foo/bar(表示它跟踪名为foo的远程仓库中的bar分支),并且匹配已配置抓取引用规范的右侧。远程跟踪分支不应包含直接修改或在其上进行本地提交。
- 仓库
-
一组引用以及包含所有可达于引用的对象的对象数据库,可能还附带一个或多个上层命令的元数据。仓库可以通过备用机制与其他仓库共享对象数据库。
- 解决
-
手动修复失败的自动合并所遗留问题的操作。
- 修订
-
与提交(名词)同义。
- 回滚
- SCM
-
源代码管理(工具)。
- SHA-1
-
“安全散列算法1”;一种加密散列函数。在Git的上下文中,用作对象名的同义词。
- 浅层克隆
-
在大多数情况下,与浅层仓库同义,但该短语更明确地表明它是通过运行
git clone --depth=...命令创建的。 - 浅层仓库
-
一个浅层仓库具有不完整的历史,其中一些提交的父提交已被“烧毁”(换句话说,Git被告知假装这些提交没有父提交,即使它们已记录在提交对象中)。当您只对项目的近期历史感兴趣,而上游记录的实际历史非常大时,这有时很有用。通过给git-clone[1]提供
--depth选项来创建浅层仓库,并且其历史可以通过git-fetch[1]稍后加深。 - 暂存条目
- 子模块
- 父项目
- 符号引用
-
符号引用:它不是包含SHA-1 ID本身,而是格式为ref: refs/some/thing,当被引用时,它会递归解引用到该引用。HEAD是符号引用的一个主要例子。符号引用通过git-symbolic-ref[1]命令进行操作。
- 标签
-
refs/tags/命名空间下的引用,指向任意类型的对象(通常标签指向标签或提交对象)。与主干不同,标签不会被commit命令更新。Git标签与Lisp标签(在Git的上下文中称为对象类型)无关。标签最常用于标记提交祖先链中的特定点。 - 标签对象
-
包含指向另一个对象的引用的对象,它可以像提交对象一样包含消息。它还可以包含(PGP)签名,在这种情况下,它被称为“已签名标签对象”。
- 主题分支
-
一个常规的Git分支,由开发者用于标识一个概念性的开发线。由于分支非常容易和廉价,因此通常需要有几个小分支,每个分支都包含非常明确的概念或小的增量但相关的更改。
- 尾部
-
键值元数据。尾部可选地出现在提交消息的末尾。在其他社区中可能被称为“页脚”或“标签”。参见git-interpret-trailers[1]。
- 树
- 树对象
- tree-ish (也可 treeish)
-
一个树对象或一个可以递归解引用到树对象的对象。解引用提交对象会得到与修订的顶层目录对应的树对象。以下都属于tree-ishes:一个commit-ish,一个树对象,一个指向树对象的标签对象,一个指向指向树对象的标签对象的标签对象,等等。
- 未出生
-
HEAD可以指向一个尚不存在且还没有任何提交的分支,这样的分支称为未出生分支。用户最常遇到未出生分支的情况是创建新仓库而不是从别处克隆。HEAD将指向尚未诞生的主(main)(或master,取决于您的配置)分支。此外,一些操作可以通过其孤儿选项让您进入未出生分支。 - 未合并索引
- 不可达对象
- 上游分支
-
要合并到当前分支(或当前分支被变基到其上)的默认分支。它通过branch.<name>.remote和branch.<name>.merge进行配置。如果A的上游分支是origin/B,有时我们说“A正在跟踪origin/B”。
- 工作目录
-
实际检出文件的树。工作目录通常包含HEAD提交树的内容,加上您已做的但尚未提交的任何本地更改。
- 工作区
-
一个仓库可以有零个(即裸仓库)或一个或多个附加到它的工作区。一个“工作区”由一个“工作目录”和仓库元数据组成,其中大部分在单个仓库的其他工作区之间共享,而有些则为每个工作区单独维护(例如,索引、HEAD和伪引用如MERGE_HEAD、每个工作区引用和每个工作区配置文件)。
附录 A:Git快速参考
这是主要命令的快速摘要;前面的章节更详细地解释了它们的工作原理。
创建新仓库
从tarball
$ tar xzf project.tar.gz $ cd project $ git init Initialized empty Git repository in .git/ $ git add . $ git commit
从远程仓库
$ git clone git://example.com/pub/project.git $ cd project
管理分支
$ git branch # list all local branches in this repo $ git switch test # switch working directory to branch "test" $ git branch new # create branch "new" starting at current HEAD $ git branch -d new # delete branch "new"
而不是基于当前HEAD(默认值)创建新分支,请使用
$ git branch new test # branch named "test" $ git branch new v2.6.15 # tag named v2.6.15 $ git branch new HEAD^ # commit before the most recent $ git branch new HEAD^^ # commit before that $ git branch new test~10 # ten commits before tip of branch "test"
同时创建并切换到新分支
$ git switch -c new v2.6.15
更新并检查你克隆的仓库的分支
$ git fetch # update $ git branch -r # list origin/master origin/next ... $ git switch -c masterwork origin/master
从不同仓库抓取一个分支,并给它在你的仓库中一个新名称
$ git fetch git://example.com/project.git theirbranch:mybranch $ git fetch git://example.com/project.git v2.6.15:mybranch
保持一个你经常使用的仓库列表
$ git remote add example git://example.com/project.git
$ git remote # list remote repositories
example
origin
$ git remote show example # get details
* remote example
URL: git://example.com/project.git
Tracked remote branches
master
next
...
$ git fetch example # update branches from example
$ git branch -r # list all remote branches
探索历史
$ gitk # visualize and browse history $ git log # list all commits $ git log src/ # ...modifying src/ $ git log v2.6.15..v2.6.16 # ...in v2.6.16, not in v2.6.15 $ git log master..test # ...in branch test, not in branch master $ git log test..master # ...in branch master, but not in test $ git log test...master # ...in one branch, not in both $ git log -S'foo()' # ...where difference contain "foo()" $ git log --since="2 weeks ago" $ git log -p # show patches as well $ git show # most recent commit $ git diff v2.6.15..v2.6.16 # diff between two tagged versions $ git diff v2.6.15..HEAD # diff with current head $ git grep "foo()" # search working directory for "foo()" $ git grep v2.6.15 "foo()" # search old tree for "foo()" $ git show v2.6.15:a.txt # look at old version of a.txt
搜索回归
$ git bisect start $ git bisect bad # current version is bad $ git bisect good v2.6.13-rc2 # last known good revision Bisecting: 675 revisions left to test after this # test here, then: $ git bisect good # if this revision is good, or $ git bisect bad # if this revision is bad. # repeat until done.
进行更改
确保Git知道谁该负责
$ cat >>~/.gitconfig <<\EOF [user] name = Your Name Comes Here email = you@yourdomain.example.com EOF
选择要在下次提交中包含的文件内容,然后进行提交
$ git add a.txt # updated file $ git add b.txt # new file $ git rm c.txt # old file $ git commit
或者,一次性准备并创建提交
$ git commit d.txt # use latest content only of d.txt $ git commit -a # use latest content of all tracked files
合并
$ git merge test # merge branch "test" into the current branch $ git pull git://example.com/project.git master # fetch and merge in remote branch $ git pull . test # equivalent to git merge test
共享您的更改
导入或导出补丁
$ git format-patch origin..HEAD # format a patch for each commit # in HEAD but not in origin $ git am mbox # import patches from the mailbox "mbox"
在不同的 Git 存储库中获取一个分支,然后合并到当前分支
$ git pull git://example.com/project.git theirbranch
在合并到当前分支之前,将获取的分支存储到本地分支
$ git pull git://example.com/project.git theirbranch:mybranch
在本地分支上创建提交后,用您的提交更新远程分支
$ git push ssh://example.com/project.git mybranch:theirbranch
当远程和本地分支都命名为 "test" 时
$ git push ssh://example.com/project.git test
常用远程存储库的快捷方式版本
$ git remote add example ssh://example.com/project.git $ git push example test
附录 B:本手册的注释和待办事项列表
待办事项列表
这是一项正在进行的工作。
基本要求
-
它必须能够被一个具有类 UNIX 命令行基本知识但对 Git 没有特殊了解的聪明人按顺序、从头到尾地阅读。如果需要,任何其他先决条件都应在出现时专门提及。
-
尽可能使章节标题清楚地描述它们解释如何完成的任务,使用的语言不需要超出必要的知识:例如,“将补丁导入项目”而不是“
gitam命令”
思考如何创建一个清晰的章节依赖图,允许人们在不一定阅读所有中间内容的情况下找到重要主题。
扫描 Documentation/ 目录以查找其他遗漏的内容;特别是
-
操作指南
-
technical/中的一些内容? -
钩子
-
git[1] 中的命令列表
扫描邮件归档以查找其他遗漏的内容
扫描 man pages 以查看是否有任何内容假设的背景知识比本手册提供的更多。
添加更多好的示例。仅包含食谱示例的整个部分可能是一个好主意;也许可以做一个标准的章节结尾部分“高级示例”?
适当时包含到词汇表的交叉引用。
添加一节关于与其他版本控制系统(包括 CVS、Subversion)协同工作的内容,以及仅导入一系列发布 tarball 的内容。
编写一章关于使用底层命令和编写脚本的内容。
备选方案、clone -reference 等。