这是我喜欢的想法:存储库共享提交,但它们不共享分支名称。
Git 存储库的核心——边缘变得凌乱——实际上是一对数据库。两个数据库都是简单的键值存储:
其中一个——通常是迄今为止最大的——被大而丑陋的哈希 ID 索引/访问。这些是提交哈希 ID,以及其他内部 Git 对象哈希 ID。所以这是对象数据库(尽管 Git 通常不会以这种方式引用它)。
另一个将名称(分支名称、标签名称和所有其他类型的名称)映射到那些丑陋的大哈希 ID。这让我们这些可怜的人使用Git;哈希 ID 不可用。这是名称数据库,每个名称都映射到一个哈希 ID。多个名称可以映射到一个特定的哈希 ID,并且许多哈希 ID 没有映射到它们的名称,所以如果您习惯于从数学集合的角度思考,这是一种非内射、非满射的映射(见满射函数)。
对象数据库中的每个提交对象都有一个唯一的哈希 ID。任何对象的哈希 ID,无论是否提交,都是对构成对象的数据运行加密哈希函数的结果(这就是 Git 进行一致性检查和文件内容去重的方式)。但由于提交的几个属性,每个提交本身都是唯一的,因此每个提交都有一个唯一的哈希 ID。1 使用这些提交哈希 ID,任何两个不同的Git 存储库,使用兼容的 Git 软件——我称之为“两个 Git”——这两个 Git 可以通过比较提交哈希 ID立即判断它们是否具有相同的提交。2
这一切归结为,当我们将两个 Git 相互挂钩时,一个——发送者——说我有 commit之类的a123456...
话,你愿意吗?其他人回答是,请或不,谢谢,我已经有了,这允许发送者将发送者所拥有的接收者缺少的提交发送给接收者。
这个过程——例如,发送我有但你没有的提交——是一个git fetch
或git push
,取决于谁开始对话。如果您开始对话以获取我的提交,那么您正在使用git fetch
. 如果我开始对话以向您发送提交,我正在使用git push
.
1任何熟悉鸽笼原理的人都会立即反对哈希函数,即使是具有 2 160范围 (SHA-1) 或 2 256范围 (SHA-256) 的哈希函数,最终也必须产生冲突。这是真的,当这种情况发生时,Git 停止运行,因此哈希必须足够大,以至于“最终”足够几十年或几个世纪后不用担心。(由于生日问题,这比起初看起来要难。)
2由于提交形成的 DAG,我们可以做得比仅列出所有原始哈希 ID 更好,但如果我们有两个相当小的存储库(例如每个存储数千个提交),那么仅列出所有原始哈希 ID 就很容易了。哈希 ID,查看谁有哪些提交。
这就是使用多个 Git 存储库的大部分内容
说真的,这种传输是我们所需要的,也是原始 Git 中的全部,用于将两个 Git 相互连接。通过比较哈希 ID 并发送我有的提交,而你没有,你最终会分享我的提交。如果我然后转身从你那里得到任何你有我没有的提交,我们现在就我们拥有的提交集而言是平等的。但这不是使用Git 的便捷方式,因此我们现在添加一些附加功能。
分支名称、标签名称、远程跟踪名称等
人类不擅长哈希 ID e9e5ba39a78c8f5057262d49e261b42a8660d5b9
:. 说什么?可能有几个人可以向您重复 Git-repository-for-Git 标签哈希 ID 列表,如果他们已经阅读过它们,但我有时会转置字符,并且不费心尝试记住它们。这就是计算机的用途。我让计算机为我记住哈希 ID:现在master
可能会保留e9e5ba39a78c8f5057262d49e261b42a8660d5b9
。稍后,master
将持有一些其他的哈希 ID。无论它持有什么哈希 ID,也就是说,根据定义,我在这个 Git 存储库中的分支上的最新提交。 master
当我们使用 时git fetch
,Git 会以一种很好的方式为我们维护所有这些东西。我们创建一个远程——一个类似的短名称——origin
我们在它下面存储一个传统的 URL,例如ssh://git@github.com/path/to/repo.git
,或类似的路径名/absolute/path/to/dir/.git
,或任何适当的名称。3
现在我们有了这个短名称,origin
或者fred
我们选择的任何名称,我们的Git 可以调用他们的Git——也就是说,我们的 Git 软件可以使用 ssh 或 https 或其他任何方式来访问他们的 Git 软件;我们的 Git 将在我们的存储库中运行,他们的 Git 将在他们的存储库中运行——一旦我们调用了他们的 Git,我们就可以git fetch
从他们那里获取提交。我们用一个简单的方法来做到这一点:
git fetch origin
例如。
他们的 Git 列出了他们的分支名称和相应的提交哈希 ID。我们的 Git 检查这些哈希 ID 并使用它来确定它们是否有我们没有的提交。如果我们不限制我们的 Git,4我们的 Git 现在会带来我们还没有的所有提交,所以现在我们共享他们的提交。
但是:我们的分支名称是我们用来在存储库中查找特定提交的东西,我们出于某种特定原因喜欢它。我们git fetch
不碰我们的分支名称。相反,我们的 Git 采用它们的每个分支名称并将它们更改为远程跟踪名称。5 Git 通过将远程名称(在本例中为)粘贴在origin
分支名称前面加上一个斜杠来做到这一点。所以这是从哪里来origin/main
的origin/master
:那是我们的他们的分支名称的副本。这是我们的 Git 记住他们的 Git或(无论他们拥有哪个)的方式。main
master
所以,git fetch
让我们获得他们的提交,但对我们自己的分支不做任何事情。由于 Git 使用散列技巧,所有对象(包括所有提交)都是完全只读的,因此此更新仅将新提交添加到我们的存储库。我们分享他们的提交,但不分享他们的分支名称。
3一个非常短的相对路径 like../d/.git
并不比 长得多或难于键入origin
,因此对于这种情况,您可能会想省略远程名称。但不要这样做!Git 需要该远程名称来形成远程跟踪名称。如果您跳过使用远程名称,那么您使用的是 Git 仍然支持的原始模式,这只是 <插入正文部分> 的痛苦。
4您可以故意限制您git fetch
减少网络流量或其他任何事情。大多数时候这几乎是不必要的,因为 Git 很聪明,只带入任何需要的对象。不过,在极少数情况下,它可能很有用。
5 Git 调用这些远程跟踪分支名称。虽然它们不是分支名称,但无论如何不在我们的存储库中。它们是我们复制别人的分支名称。因此,从我们的角度来看,这些是非分支名称,只是碰巧“跟踪”了其他人的(分支)名称。Git 过度使用了动词track和形容词branch,但我们可以在这里减少这种过度使用。
从此时起,您在本地工作,至少直到git push
你现在拥有的是:
- 所有提交;
- 您自己的分支名称;和
- 您的分支名称的副本/记忆。
您可以通过将特定提交 ( ) 提取到工作树和 Git 的索引或暂存区域(同一事物的两个术语)中来使用您的存储库。您最终会做出新的提交,这些提交会添加到现有的提交中。当您进行新提交时,Git 会自动更新您当前的分支名称,以便您的分支名称自动包含您的新提交。git checkout
您已经询问了合并和变基,这些实际上都是相当大的主题,但它们在其他地方已经很好地涵盖了。请记住,这git merge
主要意味着结合工作。尽管git merge
对通常的工作组合方式充满了例外,但这主要是通过进行新的提交来发生的。同时,git rebase
这意味着我对我的存储库中的某些提交感到满意,但是我不喜欢这些提交。我想将它们复制到一组新的和改进的提交中,并可以选择在此过程中使用特殊技巧。
因为你实际上不能更改任何现有的提交,所以复制意味着你得到重复——或者更确切地说,接近重复,“接近”部分取决于你正在改变的内容,假设你正在改变一些东西或者你' d 只使用原件。rebase 的目标是停止使用原始文件并开始使用近似重复文件。由于我们通常使用分支名称来查找我们的提交,Git 通过移动名称来实现这一点:
K--L <-- br2 (originals)
/
...--F--G--H--I--J <-- br1
\
K'-L' <-- proposed new br2
<-- older ... newer -->
我们有 Git 复制K
到K'
,通过使用J
作为基本提交来改进副本,然后我们有 Git 复制L
到,通过使用作为基础来L'
改进副本。这是变基的主要部分。但是现在我们必须停止使用,并开始使用。由于我们通过分支名称找到提交,Git 只需要从提交中“剥离标签”并将其粘贴到提交上。然后 Git 向后工作——这是 Git 中的一个通用主题,它从末尾开始并向后工作——从 开始,所以现在你再也看不到了。K'
L'
K-L
K'-L'
L
L'
L'
K-L
因为提交是共享的,而分支名称不是,如果你从其他 Git 存储库获得提交,你现在有一个问题:他们的分支名称仍然指的是 commit ,而不是你的 new 。K-L
L
L'
git push
不只是一个颠倒的git fetch
当你运行时git push
,你:
- 让你的 Git 调用其他 Git;
- 让你的 Git 给出 Git特定的提交:不仅仅是我拥有的所有你没有的,而是我拥有的某个分支上的每个提交,而你没有(提交我有你没有的,在某个特定的分支上我的选择);
- 最后,一旦这些提交完成,您礼貌地请求(常规
git push
)或命令(git push --force
)他们的Git 应该更新他们的分支名称。
当我们使用 时git fetch
,我们有这些友好的远程跟踪名称,我们的 Git 可以通过这些名称记住它们的分支名称。该push
命令不打扰友好。我们只是建议用一些新的哈希 ID 完全覆盖他们的分支名称。
如果我们发送给他们的提交添加到他们当时看到的分支中,那么这个礼貌的请求(例如,请将您的分支名称develop
设置a123456...
为)可能是可以的。他们有:
...--G--H <-- develop
whereH
代表9876543...
或任何哈希 ID。我们向他们发送了提交I-J
:
...--G--H <-- develop
\
I--J <-- polite request to set "develop" to point to J
如果他们遵守这个礼貌的请求,他们的提交H
仍然可以找到,因为 Git 从分支名称的提交向后工作。 J
导致返回I
导致返回H
,嘿,我们没有丢失任何提交!
但是,在 a 之后git rebase
,我们接受了一些现有的提交并将它们丢弃,以支持新的和改进的替代品。假设他们有:
I--J <-- br1
/
...--F--G--H <-- develop
\
K--L <-- br2
在他们的存储库中,我们现在向他们发送一对K'-L'
添加到 commit 的提交J
:
K'-L' <-- please set br2 here
/
I--J <-- br1
/
...--F--G--H <-- develop
\
K--L <-- br2
这个礼貌的请求,他们将其设置br2
为指向L'
而不是L
,不会很好地接受。他们会说:不!如果我这样做,我会失去提交! 当然,我们希望他们输K-L
,赞成K'-L'
。但除非我们告诉他们,否则他们不知道这一点。
这是git push --force
为了什么。但是,如果他们的 Git 存储库非常活跃,也许我们更早 K-L
,但现在他们有:
K'-L' <-- please set br2 here
/
I--J <-- br1
/
...--F--G--H <-- develop
\
K--L--N <-- br2
我们提议他们放弃整个K-L-N
链条,转而支持我们的K'-L'
.
该--force-with-lease
选项是试图改善这种情况。它在当前的 Git 中运行良好,但可能会出现一些不太正确的更新。(我认为它应该保持“正确”,也有建议这样做,所以我们拭目以待。)
裸仓库
当我们使用 Git 时,我们有一些提交已签出。这个提交来自我们当前的分支,是我们当前的提交。我们可能正在处理一个新的提交。在这种状态下,Git 确实无法接收到我们当前分支的更新:这会搞砸事情。6 因此,Git 将拒绝推送到当前签出的分支。
为了回避这个问题,用于接收请求的 Git 存储库通常设置git push
为裸存储库。裸存储库是没有工作树的存储库。没有工作树,没有人可以在裸存储库中做任何工作。这意味着没有什么可以搞砸的,裸存储库总是可以收到推送。
还有其他方法可以解决这个问题(配置设置receive.denyCurrentBranch
),但另一个回避问题的好方法是避免 git push
:git fetch
在使用两个不同的存储库时使用,您可以同时控制它们。当你在 A 工作时总是从 B 取,而当你在 B 工作时总是从 A 取,你不能踩到自己。
6特别是,Git 的索引包含当前提交的图像,由我们编辑的任何建议的替换文件git add
、我们添加的新文件和/或我们编辑的文件修改git rm
。Git 将从索引中的任何内容构建新提交,新提交的父级将是当前提交,其哈希 ID 存储在分支名称中。因此,更新分支名称也需要更新索引和工作树。如果我们正在做某事,那是个坏主意。即使我们不是,更新分支名称也意味着我们的新提交可能会到达我们没有预料到的地方。整个形势严峻。
底线
在 Git 中,您使用commits在本地工作。这些在您的存储库中:只有一个,您现在在其中,具有一棵工作树和 Git 的索引。7 要从其他 Git 获取提交,请使用git fetch
远程:短名称,如origin
. 添加更多遥控器,使用git remote add
,以添加更多从 获取的位置。用于修改git remote set-url
一个特定远程的 URL,git remote remove
删除一个远程,git remote update
并使 Git 从所有远程获取。8 考虑设置fetch.prune
为true
解决我认为fetch
默认值中的历史错误。9
在 you 之后git fetch
,您拥有所有提交。对他们做任何你喜欢的事。将它们留在您的存储库中,重新设置它们,无论如何。然后,如果另一个 Git 是设置为接收请求的裸Git,请使用发送您的新提交并要求他们设置其分支名称之一以记住您的提交:git push
git push
- 如果这在他们的 Git 中创建了一个新名称,他们可能会允许它。
- 如果这更新了一个名称,使得旧的提交仍然存在,他们可能会允许它。
- 如果这更新了名称以停止在他们身边找到旧的提交,除非您使用其中一个
--force
选项,否则他们将拒绝它;为安全起见--force-with-lease
。
如果另一个 Git不是裸露的,和/或您想直接在其中工作,请更改您正在工作的位置,以便您现在位于另一个存储库中,并用于git fetch
获取您在您所在的存储库中所做的提交工作,刚才。由于fetch
它本身总是安全的,因此10这将是安全的。
永远,永远记住,如果你还没有提交某些东西,那么它不在Git 中(还)。如果你已经提交了它,它就在一个提交中,即使在小灾难之后,你通常也可以取回它。11
7git worktree add
使这张图片复杂化了,所以我们在这里忽略它。
8或者,使用git fetch --all
:这是什么--all
意思,所有遥控器。该git remote update
命令更漂亮,但可以让你在这里做更多的事情。
9也就是说,运行:
git config --global fetch.prune true
一次,在您的全局配置中,在这台特定的机器上设置它。当git fetch
从某个远程 R 获取数据时,它会为它在 R 上看到的每个分支创建或更新。但是如果 R 有一个名为昨天的分支,而今天该分支消失了,那么您的 Git 不会做任何事情:它是昨天在您运行时创建的,并且现在没有创建或更新,所以它不会创建或更新它。这会给你留下一个“陈旧的” 。R/branch
fancy42
fancy42
git fetch
fancy42
fancy42
启用修剪git fetch
后,git remote update
请注意它们现在fancy42
已经消失了,因此您也不应该拥有它们R/fancy42
。你的 Git 会删除它。这不是默认值,因为它最初不是默认值,现在更改它为时已晚。可能有人喜欢甚至依赖它。如果您自己喜欢它,请fetch.prune
设置为false
并且不要运行git remote prune
。
10您可以配置或强制git fetch
设置为“不安全”,但除非您故意这样做或使用git clone --mirror
这样做,否则您不会遇到这种情况。
11这条规则的最大例外是如果 Git 数据库本身被损坏。这往往发生在以下情况:
- 存储库位于共享驱动器(Google Drive、iCloud、OneDrive 等)上;
- 计算机崩溃或未正确关闭,导致操作系统无法将内容写入磁盘;
- 计算机本身着火(这曾经是笔记本电脑的常见问题);或者
- 您使用操作系统强制删除
.git
目录内的 Git 存储库数据库。
否则这些东西真的很可靠。