如何合并主分支和主分支?
How to merge main and master branches?
大约一个月前,我创建了一个 git 存储库,其中的主分支名为 'master'。几天前,当我尝试提交并推送到同一个 repo 时,它将我的更改推送到 'master' 分支,但我收到一条消息,说主分支已更改为 'main' 分支。
我尝试过合并,但我收到一条错误消息,提示无法合并不相关的历史记录(显然他们将拥有不相关的历史记录,因为 'main' 分支刚刚创建)
现在我所有的代码都在 'master' 分支上,它不是主分支,所以我想知道如何将所有代码移动到 'main' 分支?
仅供参考:我做了一些研究,我了解 GitHub 进行此更改的全部原因,我只是想知道如何解决这个问题。
关于 Git 的一点是,只有提交才重要 。提交是 Git 的全部内容。一旦您进入提交,提交本身就会在一个扭曲的小提交球中找到其他提交。那么:are b运行ch 名称有什么用?不是没什么,但有点接近了。
提交的真实名称是其哈希 ID。但是commit hash IDs seem 运行dom,并且没有办法预测某个commit的hash ID是什么。一旦找到 one 提交,就可以使用该提交来查找更多提交。但是你必须首先找到 其中一个,不知何故——这就是 b运行ch 名字的由来。一个名字可以让你 开始 .它让你进入提交的巢穴。从名称中,您现在可以找到某个特定提交的哈希 ID。该提交让您找到另一个提交,这让您找到另一个提交,依此类推。
Now all my code is on the 'master' branch which is not the main branch, so I was wondering how I could move everything to the 'main' branch?
这里的 TL;DR 是你处于一个棘手的情况并且没有唯一的正确答案。你必须决定你想做什么。您可以:
- 将您自己的
master
b运行ch 重命名为 main
并尝试让原始存储库的所有其他克隆用户使用 您的 提交;或
- 弄清楚如何合并 and/or re-do 两个存储库中的部分或全部提交。
换句话说,您可能所要做的就是重命名 b运行ch。但是肯定还是有问题,因为你现在有两个 b运行ch 名字。是时候仔细看看这整个事情了:为什么 重要的是 commits,这些名称是如何工作的?
长
让我们从最简单的相关提交形式开始:一个小的、简单的线性链。假设我们创建一个新的 totally-empty 存储库,其中没有任何提交。关于 Git b运行ch names 有一个规则:a b运行ch name must hold the hash ID of exactly one (1) existing, valid commit.1 因为没有提交,所以不可能有 b运行ch names.
为了解决这个问题,我们进行了第一次提交。如果你使用 GitHub,他们通常会为你 做第一次提交,创建一个只有一个 README and/or LICENSE 类型的文件。有了第一次提交,您就可以创建任意数量的 b运行ch 名称:它们都会存储该一次提交的哈希 ID。
请注意,每个提交都有自己唯一的哈希 ID。这个散列 ID 在 所有 Git 存储库中是通用的。2 这就是为什么 Git 散列 ID 如此大和尽管它们很丑陋。3 它还允许 Git 程序连接到正在使用其他 Git 存储库的其他 Git 程序,并找出哪些提交每个存储库,只需交换哈希 ID。因此哈希 ID 至关重要。但它们对 人类 来说毫无用处,因为他们无法让它们保持正直。所以这就是我们有 b运行ch 名字的原因。
关于这些哈希 ID 和底层 objects(提交,以及 Git 存储的 non-commit objects,还有一件事需要了解,提到在脚注 1 中):哈希 ID 只是存储的 object 的奇特校验和。 Git 使用 哈希 ID 查找 object——提交或其相关数据——,但随后还要确保存储的 object的校验和与它用来查找的内容相匹配。因此,Git 中存储的任何 object 的任何部分都不能更改。如果校验和不匹配,Git 声明存储已损坏,并拒绝继续。
无论如何,假设我们从一个提交开始,一个名为 bra
的 b运行ch,然后又创建了两个提交,这样我们现在就有了一个只有三个提交的小型存储库在里面。这三个提交有三个丑陋的大哈希 ID,是 那些 三个提交所特有的,但我们只称它们为提交 A
、B
和 C
.让我们把它们画成这样。此图中的每个元素都有一个用途:
A <-B <-C <--bra
提交 C
存储两件事:每个文件的快照和一些元数据。快照充当主要提交的数据,并让您取回所有文件,无论它们在您(或任何人)提交时的任何形式 C
。 元数据 包括提交人的姓名、电子邮件地址等;但对于 Git 本身至关重要,提交 C
中的元数据包括早期提交 B
.
的哈希 ID
我们说提交C
指向B
。通过读取commit C
, Git 可以找到更早的commit B
.
的hash ID
提交 B
,当然,还包含数据(每个文件的完整快照)和元数据,包括早期提交的哈希 ID A
。所以从B
,Git可以fid A
.
提交A
有点特殊,因为它是first-ever提交。它没有指向任何较早提交的 backwards-pointing 箭头,因为没有较早提交。 Git 称其为 root commit。它让 Git 停止倒退。
我们需要用来查找此存储库中所有其他提交的提交是提交 C
。为了 find 提交 C
,我们使用 b运行ch 名称 bra
。它包含提交的哈希 ID C
,因此 bra
指向 C
,这就是我们开始的方式。
1不存在现有但无效的提交。说“现有的、有效的提交”的意思是哈希 ID 不仅仅用于 只是 提交,所以你可以有一个有效的哈希 ID,但对于一些不是 提交。但是你还不会处理这些 non-commit 哈希 ID,如果有的话。您确实必须处理提交哈希 ID,因此这些是我们关心的。
2从技术上讲,两个不同的提交 可以 具有相同的哈希 ID,只要这两个 Git 存储库永远不会相遇.满足其 doppelgänger 的提交会导致悲剧和悲伤,所以这很糟糕。 (好吧,从技术上讲,发生的事情是两个 Git s,因为他们有 Git-sex 来交换提交,只是故障。悲伤是那些 Git 的用户s,谁期待什么漂亮的宝宝。)
3到几年前,即使这样也开始变得不够用了。有关详细信息,请参阅 How does the newly found SHA-1 collision affect Git?。
在一个 b运行ch
上添加新提交
鉴于我们有:
A <-B <-C <--bra
我们从 提取 提交 C
到工作区开始。每次提交的内容不能更改,包括存储的文件。4 所以现在我们已经提交 C
“签出”。 Git使用名称bra
来记住C
的哈希ID,并且知道当前提交有这个哈希ID。
我们现在可以进行任何我们喜欢的更改:添加新文件、删除现有文件、更新文件等等。我们用 git add
通知 Git 这些更新。5 然后我们用 git commit
构建一个新的提交。 Git 保存新快照,并添加适当的元数据,包括 当前提交 的哈希 ID,以生成一个新的提交 D
指向现有提交 C
:
A <-B <-C <--bra
\
D
作为git commit
的最后一步,Git 将最新提交的哈希ID存储到b运行ch名称中。由于提交 D
指向现有提交 C
,我们现在要通过名为 bra
的 b运行ch 开始查看存储库,方法是查看提交 D
:
A <-B <-C <-D <--bra
提交现已完成。
4文件的内容在存储库中存储为 blob objects。这会压缩它们和 de-duplicates 它们,因此当两个提交共享相同的文件内容时,它们实际上共享内部 objects。不过,您通常不需要了解或关心这一点。
5git add
步骤操纵Git调用的东西,其index,或暂存区,或(现在很少)缓存。为了在这个答案中保存 space,我省略了所有有用的细节。
多个 b运行ch 名称
要使用多个b运行ch,我们通常添加一个新的b运行ch名称,使用git branch
和git checkout
,或者将两者结合使用git checkout -b
(或在 Git 2.23 或更高版本中,git switch -c
)。这实际工作的方式是它只是创建新的 b运行ch 名称,指向与 current 提交相同的提交 :
A--B--C--D <-- bra, nch
我们现在有两个 b运行ch 名称,但是两个 select 相同的提交 。现在,我们使用哪个 name 并不重要,因为两个名字 select 都提交 D
。但过一会儿,它会变得很重要——Git 总是希望能够告诉我们我们“在”哪个 b运行ch,以便 git status
可以说 on branch bra
或 on branch nch
。为了实现这一点,Git 将特殊名称 HEAD
附加到一个 b运行ch 名称,如下所示:
A--B--C--D <-- bra (HEAD), nch
或者这个:
A--B--C--D <-- bra, nch (HEAD)
无论哪个名称附加了 HEAD
,就是 当前 b运行ch 名称。无论提交这个名称指向,就是当前提交。
现在我们将以通常的方式创建一个新的提交。它获得了一个新的唯一哈希 ID,但我们将其称为提交 E
,以保持理智:只有计算机才能处理真正的哈希 ID。让我们把它画进去:
A--B--C--D <-- bra
\
E <-- nch (HEAD)
更新的 b运行ch 名称是 nch
,因为那是我们的 current b运行ch。 当前提交 现在是提交E
,这就是我们签出的提交
如果我们在Git 2.23或更高版本中git checkout bra
或git switch bra
,我们选择bra
作为我们的当前b运行 ch 并提交 D
作为我们的 当前提交 。所以提交 D
变成签出的:
A--B--C--D <-- bra (HEAD)
\
E <-- nch
现在我们所做的任何新提交都将更新名称 bra
:
F <-- bra (HEAD)
/
A--B--C--D
\
E <-- nch
这是我们通常在 Git 存储库中进行的那种 b运行ching。请注意,提交 A-B-C-D
在 both b运行ches 上,因为无论我们从哪个名称开始,当我们向后工作时,我们都会找到所有这些提交。但是 find commit E
的唯一方法是从 name nch
开始。 find commit F
的唯一方法是从 name bra
.
开始
B运行ch 名称查找提交
这就是 b运行ch 名称的用武之地。他们找到 b运行ch 的开始——好吧,结束?——提交。事实上,这就是 Git 中 b运行ches 的定义方式。 name 保存 b运行ch 上 last 提交的哈希 ID。无论名称中的哈希 ID 是什么,这都是 last 提交,即使有更多提交也是如此。当我们有:
F <-- bra
/
A--B--C--D <-- main
\
E <-- nch
有 三个 最后提交,即使在 D
之后有两个提交。也有三种 方式 来查找提交 A-B-C-D
:我们可以从名称 main
开始并向后工作,或者我们可以从其他两个中的任何一个开始名字和工作倒退。
历史如何关联
假设我们有这个:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
我们可以选择这两个 b运行ch 名称中的任何一个——因此提交 J
或提交 L
——然后请求 Git 到 merge other 最后一次提交。在不讨论任何其他重要细节的情况下,Git 处理此合并请求的方式是向后工作以找到 最佳共享提交 ,在这种情况下,是提交 H
。然后使用提交 H
作为 合并基础 .
进行合并
这一切都有效 因为 两个 b运行ch tip 提交 J
和 L
是相关的:它们有一个共享parent(好吧,在这种情况下,grand-parent)。此共享 parent 是一个共同的起点。因此,它们可以转换为 自共同起点以来的变化 。
更改 b运行ch 名称很简单
每个 Git 存储库都有 自己的私有 b运行ch 名称。当您将两个 Git 存储库相互挂钩时,真正重要的是 提交哈希 ID,因为它们不能更改和唯一标识提交。所以如果我们有:
A--B--C <-- bra (HEAD)
我们可以随意将此名称更改为我们喜欢的任何新名称:
A--B--C <-- xyzzy (HEAD)
没人关心这个名字是 bra
还是 xyzzy
或其他什么——好吧,除了非理性的人,当我们使用令人回味的名字时,他们的脑子里会突然冒出一些想法,比如 plugh
或 colossal-cave-adventure
。而且,当我们使用 Git clones 来分享工作时,我们人类喜欢分享我们的 b运行ch names 也有助于保持我们自己的理智。所以我们通常不会去重命名 b运行ches willy-nilly。但实际名称真的无关紧要,至少 Git 不重要。
如果这是你自己的情况——你有一个 master
,他们将名称更改为 main
——你可以将 master
重命名为 main
你自己,你和他们都会使用相同的名称来查找相同的提交。 这会很简单。不过,这不是你的情况,因为如果这是你的情况,你就不会看到对无关历史的抱怨。
不止一次根提交
上面的所有图表只有一个根提交:在我们的例子中,提交 A
。 (好吧,...--G--H
可能 有一个根提交。)但是在 Git 中有很多不同的方法来创建额外的根提交。一种方法是使用 git checkout --orphan
(或 git switch --orphan
)。假设我们开始于:
A--B--C <-- bra (HEAD)
然后使用此技术创建一个新的 root commit D
,不会 指向 C
,或任何名称为 nch
:
A--B--C <-- bra
D <-- nch (HEAD)
这在 Git 中工作正常,如果我们愿意,我们可以继续创建更多提交:
A--B--C <-- bra
D--E--F <-- nch (HEAD)
我们现在不能做的,只是合并这两个b运行ches,因为git merge
需要找到最佳共同祖先。 Git 通过从每一端开始并向后工作直到历史相遇......在这种情况下,他们永远不会相遇!一个历史记录在 A
结束(开始?),而另一个在 D
结束(开始?),而从未遇到 相同的 提交 [=403] =]两者 b运行ches.
多个存储库
考虑到以上所有内容,让我们将克隆添加到图片中。请记住,每个 Git 代表sitory 本质上是两个数据库:
一个数据库包含提交 objects 和其他内部 Git objects。每个 object 都有一个丑陋的大哈希 ID 作为其键,Git 在一个简单的 key-value datastore.
中查找实际值
另一个数据库有名称——b运行ch 名称、标签名称和其他类似名称——每个名称都存储一个哈希 ID。这些哈希 ID 使您进入提交,以便您可以找到所有提交。
当你运行git克隆<em>url</em>
时,你就有了你的Git 创建一个新的空存储库,其中没有提交,也没有 b运行ches,然后调用一些 other Git 并让 Git 根据您提供的 URL 查看一些 other 存储库。另一个 Git 有两个数据库:提交和其他 objects(由哈希 ID 键控)和 name-to-hash-IDs(由名称键控)。他们向您的 Git、 所有 发送 object,您的 Git 将其放入您自己的数据库中。
您现在有 他们所有的提交,以及 none 他们的 b运行ch 名称。
为了 找到 这些提交,您的 Git 获取了它们的 b运行ch 名称并更改了它们。而不是 master
或 main
,你的 Git 组成了像 origin/master
或 origin/main
这样的名字。这些名字是您的 Git 的 remote-tracking 名字。他们记得 他们的 Git 在 他们的 b运行ch names.
中的哈希 ID
这些 remote-tracking 名称与 find 提交一样有效。实际上,您根本不需要任何 b运行ch 名称。但是 git clone
还没有完全完成:它的最后一步是 运行 git checkout
(或 git switch
),为您选择一些 b运行ch 名称。
当然,你还没有b运行ches,但是git checkout
/ git switch
有一个特殊的功能:如果你要求Git查看一个名字不存在,您的 Git 会扫描您的 remote-tracking 名称 。如果他们有一个 master
,你现在有一个 origin/master
,当你尝试 git checkout master
时,你的 Git 将 创建 你的拥有新的 master
,指向与您的 origin/master
相同的提交。当然,这与 他们的 master
!
相同
这意味着您现在在自己的存储库中拥有:
A--B--C <-- master (HEAD), origin/master
现在,假设 他们 将他们的名字 master
更改为 main
。如果这就是他们所做的一切——如果他们只是重命名他们的 b运行ch——你最终会得到这个,在你 运行 git fetch
从他们那里得到任何新的提交之后(有 none) 并更新您的 remote-tracking 姓名:
A--B--C <-- master (HEAD), origin/master, origin/main
您的 Git 添加 origin/main
到您的存储库,以记住他们的 main
。实际上,他们已经删除了他们的名字 master
,而您的 Git 可能 应该 删除您的 origin/master
以匹配,但 Git 的默认设置=714=] 不会这样做。6 所以你最终得到两个 remote-tracking 名字,其中一个已经过时了。您可以使用以下方法手动清理它:
git branch -d -r origin/master
或:
git fetch --prune origin
(git fetch
的副作用是立即更新所有 remote-tracking 名称,包括从它们那里获取任何新的提交,所以这通常更好。虽然它需要更长的时间,因为它有通过 Internet 或 URL 所在的任何地方调用他们的 Git。)
6要使 Git 以这种方式运行,对于所有存储库,请使用 git config --global fetch.prune true
.
如果他们那样做,事情就会变得合理
假设他们确实这样做了:将他们的master
重命名为main
,而不实际添加或删除任何提交。或者,他们可能会进行重命名,然后添加更多提交。让我们画后者:它有点复杂,但最终结果都是一样的。
他们有:
A--B--C <-- master
你 运行 git clone
得到了:
A--B--C <-- master (HEAD), origin/master
在您自己的存储库中。 (我们可以在 their 存储库中省略 HEAD
因为我们通常不关心哪个 b运行ch they请查看。)然后他们 将 master
重命名 为 main
并添加提交 D-E
。您 运行 git fetch
并得到:
A--B--C <-- master (HEAD), origin/master
\
D--E <-- origin/main
你的Git无法删除origin/master
,即使他们已经没有master
了,所以我们把它留在图中。请注意,它是无害的:它只是标记提交 C
。我们可以删除它——我们可以设置 fetch.prune
或 运行 git fetch --prune
或其他任何东西——或者保留它;这并不重要。 B运行ch 名称无关紧要!只承诺事项。提交 C
仍然存在,无论是否有名称指向它。
无论如何,也许你自己做了新的提交 F
:
F <-- master (HEAD)
/
A--B--C
\
D--E <-- origin/main
如果您要求 Git 合并提交 F
和 E
, 它有效 ,因为它们有一个共同的祖先:F
的parent是C
,E
的parent的parent是C
.
这告诉我们这不是他们所做的。
看起来是什么反而发生了
如果我们假设 你 没有做出一堆不相关的提交,那么在他们的 Git 存储库中一定发生了什么——在 Git Hub——是 他们 做了一个新的 root 提交,并使用名称 main
找到它:
A--B--C <-- master
D <-- main
然后,他们可能删除了他们的名字master
。这让他们在他们的存储库中留下了这个:
A--B--C ???
D <-- main
在这一点上——或者就在这之前——他们可能会也可能不会复制他们的部分或全部A-B-C
提交到[=之后的新提交71=]:
A--B--C ???
D--B'-C' <-- main
在这里,提交 B'
是提交 B
的副本:它对 D
所做的与 B
对 A
所做的一样。同样,C'
是 C
的副本,对 B'
做任何 C
对 B
做的事情。不过,新提交具有 新的和不同的哈希 ID,并向后指向提交 D
作为它们的根。因此,当您 运行 git fetch
将您的 Git 连接到他们的 Git 时,他们的 new 提交就是这些 D-B'-C'
, 这样你在你的存储库中, 结束了:
A--B--C <-- master (HEAD), origin/master
D--B'-C' <-- origin/main
如果你删除你的 origin/master
(因为他们的 master
不见了),什么都没有真正改变:你自己的 Git 仍在寻找提交 C
。他们的 Git 无法 找到提交 C
— 他们现在甚至可能已经把它扔掉了; Gits 最终删除了 un-find-able 提交——但是你的 Git 可以,通过你的 master
。如果您从那时起进行了新的提交,就像我们之前绘制的 F
一样,您甚至会有:
F <-- master (HEAD)
/
A--B--C <-- origin/master
D--B'-C' <-- origin/main
您不能进行合并,因为这些链没有共享历史记录。
那么可以做什么?
你现在面临着一堆选择。使用哪些取决于你想做多少工作,你想让其他人做多少工作,以及你对其他Git 存储库.
您可以:
(仅)继续使用您的提交并强制其他人切换。
没有理由更改提交。原件仍然和以前一样好。有人犯了错误,抄袭了他们。让他们吃掉他们的错误:将您的 master
重命名为 main
,使用 git push --force origin main
,并使 GitHub(或其他中央存储服务器)存储库使用 你的 以 main
的名义提交,每个人都有 agreed-to.
复制您喜欢的提交,将它们添加到 他们 最后一次提交的末尾。
假设他们的提交 C'
与您的(和最初他们的)提交 C
具有相同的 保存的快照 ,或者任何提交都是原件的最后一个副本,您可以在 C'
之后添加您的工作,每次提交使用 git cherry-pick
,或 git rebase --onto
执行多个 cherry-pick 操作。请参阅其他 Whosebug 问题了解如何做到这一点。
与--allow-unrelated-histories
合并。
这种技术可以花费你最少的时间和精力,但它可能是混乱和痛苦的:中间的rebase / cherry-pick选项可能是更快更容易。 --allow-unrelated-histories
所做的只是 假装 在单独的根提交之前,有一个没有文件的提交。在某些情况下,这很容易工作。在大多数情况下,您会遇到一堆需要大量手动工作的“add/add 冲突”。
它还有一个相当丑陋的副作用,即在您的存储库中留下额外的 mostly-useless 提交,然后您将永远随身携带。如果没有人查看这段历史(以及这两个根源),就没有人会 关心 ,但它仍然存在。它是否困扰您(或其他人)完全是另一个问题。
我无法为您选择这些选项中的一个,而且这不一定是所有选项的集合,但此时您至少应该对发生的事情以及为什么会这样有一个很好的理解处理方法。
大约一个月前,我创建了一个 git 存储库,其中的主分支名为 'master'。几天前,当我尝试提交并推送到同一个 repo 时,它将我的更改推送到 'master' 分支,但我收到一条消息,说主分支已更改为 'main' 分支。
我尝试过合并,但我收到一条错误消息,提示无法合并不相关的历史记录(显然他们将拥有不相关的历史记录,因为 'main' 分支刚刚创建)
现在我所有的代码都在 'master' 分支上,它不是主分支,所以我想知道如何将所有代码移动到 'main' 分支?
仅供参考:我做了一些研究,我了解 GitHub 进行此更改的全部原因,我只是想知道如何解决这个问题。
关于 Git 的一点是,只有提交才重要 。提交是 Git 的全部内容。一旦您进入提交,提交本身就会在一个扭曲的小提交球中找到其他提交。那么:are b运行ch 名称有什么用?不是没什么,但有点接近了。
提交的真实名称是其哈希 ID。但是commit hash IDs seem 运行dom,并且没有办法预测某个commit的hash ID是什么。一旦找到 one 提交,就可以使用该提交来查找更多提交。但是你必须首先找到 其中一个,不知何故——这就是 b运行ch 名字的由来。一个名字可以让你 开始 .它让你进入提交的巢穴。从名称中,您现在可以找到某个特定提交的哈希 ID。该提交让您找到另一个提交,这让您找到另一个提交,依此类推。
Now all my code is on the 'master' branch which is not the main branch, so I was wondering how I could move everything to the 'main' branch?
这里的 TL;DR 是你处于一个棘手的情况并且没有唯一的正确答案。你必须决定你想做什么。您可以:
- 将您自己的
master
b运行ch 重命名为main
并尝试让原始存储库的所有其他克隆用户使用 您的 提交;或 - 弄清楚如何合并 and/or re-do 两个存储库中的部分或全部提交。
换句话说,您可能所要做的就是重命名 b运行ch。但是肯定还是有问题,因为你现在有两个 b运行ch 名字。是时候仔细看看这整个事情了:为什么 重要的是 commits,这些名称是如何工作的?
长
让我们从最简单的相关提交形式开始:一个小的、简单的线性链。假设我们创建一个新的 totally-empty 存储库,其中没有任何提交。关于 Git b运行ch names 有一个规则:a b运行ch name must hold the hash ID of exactly one (1) existing, valid commit.1 因为没有提交,所以不可能有 b运行ch names.
为了解决这个问题,我们进行了第一次提交。如果你使用 GitHub,他们通常会为你 做第一次提交,创建一个只有一个 README and/or LICENSE 类型的文件。有了第一次提交,您就可以创建任意数量的 b运行ch 名称:它们都会存储该一次提交的哈希 ID。
请注意,每个提交都有自己唯一的哈希 ID。这个散列 ID 在 所有 Git 存储库中是通用的。2 这就是为什么 Git 散列 ID 如此大和尽管它们很丑陋。3 它还允许 Git 程序连接到正在使用其他 Git 存储库的其他 Git 程序,并找出哪些提交每个存储库,只需交换哈希 ID。因此哈希 ID 至关重要。但它们对 人类 来说毫无用处,因为他们无法让它们保持正直。所以这就是我们有 b运行ch 名字的原因。
关于这些哈希 ID 和底层 objects(提交,以及 Git 存储的 non-commit objects,还有一件事需要了解,提到在脚注 1 中):哈希 ID 只是存储的 object 的奇特校验和。 Git 使用 哈希 ID 查找 object——提交或其相关数据——,但随后还要确保存储的 object的校验和与它用来查找的内容相匹配。因此,Git 中存储的任何 object 的任何部分都不能更改。如果校验和不匹配,Git 声明存储已损坏,并拒绝继续。
无论如何,假设我们从一个提交开始,一个名为 bra
的 b运行ch,然后又创建了两个提交,这样我们现在就有了一个只有三个提交的小型存储库在里面。这三个提交有三个丑陋的大哈希 ID,是 那些 三个提交所特有的,但我们只称它们为提交 A
、B
和 C
.让我们把它们画成这样。此图中的每个元素都有一个用途:
A <-B <-C <--bra
提交 C
存储两件事:每个文件的快照和一些元数据。快照充当主要提交的数据,并让您取回所有文件,无论它们在您(或任何人)提交时的任何形式 C
。 元数据 包括提交人的姓名、电子邮件地址等;但对于 Git 本身至关重要,提交 C
中的元数据包括早期提交 B
.
我们说提交C
指向B
。通过读取commit C
, Git 可以找到更早的commit B
.
提交 B
,当然,还包含数据(每个文件的完整快照)和元数据,包括早期提交的哈希 ID A
。所以从B
,Git可以fid A
.
提交A
有点特殊,因为它是first-ever提交。它没有指向任何较早提交的 backwards-pointing 箭头,因为没有较早提交。 Git 称其为 root commit。它让 Git 停止倒退。
我们需要用来查找此存储库中所有其他提交的提交是提交 C
。为了 find 提交 C
,我们使用 b运行ch 名称 bra
。它包含提交的哈希 ID C
,因此 bra
指向 C
,这就是我们开始的方式。
1不存在现有但无效的提交。说“现有的、有效的提交”的意思是哈希 ID 不仅仅用于 只是 提交,所以你可以有一个有效的哈希 ID,但对于一些不是 提交。但是你还不会处理这些 non-commit 哈希 ID,如果有的话。您确实必须处理提交哈希 ID,因此这些是我们关心的。
2从技术上讲,两个不同的提交 可以 具有相同的哈希 ID,只要这两个 Git 存储库永远不会相遇.满足其 doppelgänger 的提交会导致悲剧和悲伤,所以这很糟糕。 (好吧,从技术上讲,发生的事情是两个 Git s,因为他们有 Git-sex 来交换提交,只是故障。悲伤是那些 Git 的用户s,谁期待什么漂亮的宝宝。)
3到几年前,即使这样也开始变得不够用了。有关详细信息,请参阅 How does the newly found SHA-1 collision affect Git?。
在一个 b运行ch
上添加新提交鉴于我们有:
A <-B <-C <--bra
我们从 提取 提交 C
到工作区开始。每次提交的内容不能更改,包括存储的文件。4 所以现在我们已经提交 C
“签出”。 Git使用名称bra
来记住C
的哈希ID,并且知道当前提交有这个哈希ID。
我们现在可以进行任何我们喜欢的更改:添加新文件、删除现有文件、更新文件等等。我们用 git add
通知 Git 这些更新。5 然后我们用 git commit
构建一个新的提交。 Git 保存新快照,并添加适当的元数据,包括 当前提交 的哈希 ID,以生成一个新的提交 D
指向现有提交 C
:
A <-B <-C <--bra
\
D
作为git commit
的最后一步,Git 将最新提交的哈希ID存储到b运行ch名称中。由于提交 D
指向现有提交 C
,我们现在要通过名为 bra
的 b运行ch 开始查看存储库,方法是查看提交 D
:
A <-B <-C <-D <--bra
提交现已完成。
4文件的内容在存储库中存储为 blob objects。这会压缩它们和 de-duplicates 它们,因此当两个提交共享相同的文件内容时,它们实际上共享内部 objects。不过,您通常不需要了解或关心这一点。
5git add
步骤操纵Git调用的东西,其index,或暂存区,或(现在很少)缓存。为了在这个答案中保存 space,我省略了所有有用的细节。
多个 b运行ch 名称
要使用多个b运行ch,我们通常添加一个新的b运行ch名称,使用git branch
和git checkout
,或者将两者结合使用git checkout -b
(或在 Git 2.23 或更高版本中,git switch -c
)。这实际工作的方式是它只是创建新的 b运行ch 名称,指向与 current 提交相同的提交 :
A--B--C--D <-- bra, nch
我们现在有两个 b运行ch 名称,但是两个 select 相同的提交 。现在,我们使用哪个 name 并不重要,因为两个名字 select 都提交 D
。但过一会儿,它会变得很重要——Git 总是希望能够告诉我们我们“在”哪个 b运行ch,以便 git status
可以说 on branch bra
或 on branch nch
。为了实现这一点,Git 将特殊名称 HEAD
附加到一个 b运行ch 名称,如下所示:
A--B--C--D <-- bra (HEAD), nch
或者这个:
A--B--C--D <-- bra, nch (HEAD)
无论哪个名称附加了 HEAD
,就是 当前 b运行ch 名称。无论提交这个名称指向,就是当前提交。
现在我们将以通常的方式创建一个新的提交。它获得了一个新的唯一哈希 ID,但我们将其称为提交 E
,以保持理智:只有计算机才能处理真正的哈希 ID。让我们把它画进去:
A--B--C--D <-- bra
\
E <-- nch (HEAD)
更新的 b运行ch 名称是 nch
,因为那是我们的 current b运行ch。 当前提交 现在是提交E
,这就是我们签出的提交
如果我们在Git 2.23或更高版本中git checkout bra
或git switch bra
,我们选择bra
作为我们的当前b运行 ch 并提交 D
作为我们的 当前提交 。所以提交 D
变成签出的:
A--B--C--D <-- bra (HEAD)
\
E <-- nch
现在我们所做的任何新提交都将更新名称 bra
:
F <-- bra (HEAD)
/
A--B--C--D
\
E <-- nch
这是我们通常在 Git 存储库中进行的那种 b运行ching。请注意,提交 A-B-C-D
在 both b运行ches 上,因为无论我们从哪个名称开始,当我们向后工作时,我们都会找到所有这些提交。但是 find commit E
的唯一方法是从 name nch
开始。 find commit F
的唯一方法是从 name bra
.
B运行ch 名称查找提交
这就是 b运行ch 名称的用武之地。他们找到 b运行ch 的开始——好吧,结束?——提交。事实上,这就是 Git 中 b运行ches 的定义方式。 name 保存 b运行ch 上 last 提交的哈希 ID。无论名称中的哈希 ID 是什么,这都是 last 提交,即使有更多提交也是如此。当我们有:
F <-- bra
/
A--B--C--D <-- main
\
E <-- nch
有 三个 最后提交,即使在 D
之后有两个提交。也有三种 方式 来查找提交 A-B-C-D
:我们可以从名称 main
开始并向后工作,或者我们可以从其他两个中的任何一个开始名字和工作倒退。
历史如何关联
假设我们有这个:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
我们可以选择这两个 b运行ch 名称中的任何一个——因此提交 J
或提交 L
——然后请求 Git 到 merge other 最后一次提交。在不讨论任何其他重要细节的情况下,Git 处理此合并请求的方式是向后工作以找到 最佳共享提交 ,在这种情况下,是提交 H
。然后使用提交 H
作为 合并基础 .
这一切都有效 因为 两个 b运行ch tip 提交 J
和 L
是相关的:它们有一个共享parent(好吧,在这种情况下,grand-parent)。此共享 parent 是一个共同的起点。因此,它们可以转换为 自共同起点以来的变化 。
更改 b运行ch 名称很简单
每个 Git 存储库都有 自己的私有 b运行ch 名称。当您将两个 Git 存储库相互挂钩时,真正重要的是 提交哈希 ID,因为它们不能更改和唯一标识提交。所以如果我们有:
A--B--C <-- bra (HEAD)
我们可以随意将此名称更改为我们喜欢的任何新名称:
A--B--C <-- xyzzy (HEAD)
没人关心这个名字是 bra
还是 xyzzy
或其他什么——好吧,除了非理性的人,当我们使用令人回味的名字时,他们的脑子里会突然冒出一些想法,比如 plugh
或 colossal-cave-adventure
。而且,当我们使用 Git clones 来分享工作时,我们人类喜欢分享我们的 b运行ch names 也有助于保持我们自己的理智。所以我们通常不会去重命名 b运行ches willy-nilly。但实际名称真的无关紧要,至少 Git 不重要。
如果这是你自己的情况——你有一个 master
,他们将名称更改为 main
——你可以将 master
重命名为 main
你自己,你和他们都会使用相同的名称来查找相同的提交。 这会很简单。不过,这不是你的情况,因为如果这是你的情况,你就不会看到对无关历史的抱怨。
不止一次根提交
上面的所有图表只有一个根提交:在我们的例子中,提交 A
。 (好吧,...--G--H
可能 有一个根提交。)但是在 Git 中有很多不同的方法来创建额外的根提交。一种方法是使用 git checkout --orphan
(或 git switch --orphan
)。假设我们开始于:
A--B--C <-- bra (HEAD)
然后使用此技术创建一个新的 root commit D
,不会 指向 C
,或任何名称为 nch
:
A--B--C <-- bra
D <-- nch (HEAD)
这在 Git 中工作正常,如果我们愿意,我们可以继续创建更多提交:
A--B--C <-- bra
D--E--F <-- nch (HEAD)
我们现在不能做的,只是合并这两个b运行ches,因为git merge
需要找到最佳共同祖先。 Git 通过从每一端开始并向后工作直到历史相遇......在这种情况下,他们永远不会相遇!一个历史记录在 A
结束(开始?),而另一个在 D
结束(开始?),而从未遇到 相同的 提交 [=403] =]两者 b运行ches.
多个存储库
考虑到以上所有内容,让我们将克隆添加到图片中。请记住,每个 Git 代表sitory 本质上是两个数据库:
一个数据库包含提交 objects 和其他内部 Git objects。每个 object 都有一个丑陋的大哈希 ID 作为其键,Git 在一个简单的 key-value datastore.
中查找实际值另一个数据库有名称——b运行ch 名称、标签名称和其他类似名称——每个名称都存储一个哈希 ID。这些哈希 ID 使您进入提交,以便您可以找到所有提交。
当你运行git克隆<em>url</em>
时,你就有了你的Git 创建一个新的空存储库,其中没有提交,也没有 b运行ches,然后调用一些 other Git 并让 Git 根据您提供的 URL 查看一些 other 存储库。另一个 Git 有两个数据库:提交和其他 objects(由哈希 ID 键控)和 name-to-hash-IDs(由名称键控)。他们向您的 Git、 所有 发送 object,您的 Git 将其放入您自己的数据库中。
您现在有 他们所有的提交,以及 none 他们的 b运行ch 名称。
为了 找到 这些提交,您的 Git 获取了它们的 b运行ch 名称并更改了它们。而不是 master
或 main
,你的 Git 组成了像 origin/master
或 origin/main
这样的名字。这些名字是您的 Git 的 remote-tracking 名字。他们记得 他们的 Git 在 他们的 b运行ch names.
这些 remote-tracking 名称与 find 提交一样有效。实际上,您根本不需要任何 b运行ch 名称。但是 git clone
还没有完全完成:它的最后一步是 运行 git checkout
(或 git switch
),为您选择一些 b运行ch 名称。
当然,你还没有b运行ches,但是git checkout
/ git switch
有一个特殊的功能:如果你要求Git查看一个名字不存在,您的 Git 会扫描您的 remote-tracking 名称 。如果他们有一个 master
,你现在有一个 origin/master
,当你尝试 git checkout master
时,你的 Git 将 创建 你的拥有新的 master
,指向与您的 origin/master
相同的提交。当然,这与 他们的 master
!
这意味着您现在在自己的存储库中拥有:
A--B--C <-- master (HEAD), origin/master
现在,假设 他们 将他们的名字 master
更改为 main
。如果这就是他们所做的一切——如果他们只是重命名他们的 b运行ch——你最终会得到这个,在你 运行 git fetch
从他们那里得到任何新的提交之后(有 none) 并更新您的 remote-tracking 姓名:
A--B--C <-- master (HEAD), origin/master, origin/main
您的 Git 添加 origin/main
到您的存储库,以记住他们的 main
。实际上,他们已经删除了他们的名字 master
,而您的 Git 可能 应该 删除您的 origin/master
以匹配,但 Git 的默认设置=714=] 不会这样做。6 所以你最终得到两个 remote-tracking 名字,其中一个已经过时了。您可以使用以下方法手动清理它:
git branch -d -r origin/master
或:
git fetch --prune origin
(git fetch
的副作用是立即更新所有 remote-tracking 名称,包括从它们那里获取任何新的提交,所以这通常更好。虽然它需要更长的时间,因为它有通过 Internet 或 URL 所在的任何地方调用他们的 Git。)
6要使 Git 以这种方式运行,对于所有存储库,请使用 git config --global fetch.prune true
.
如果他们那样做,事情就会变得合理
假设他们确实这样做了:将他们的master
重命名为main
,而不实际添加或删除任何提交。或者,他们可能会进行重命名,然后添加更多提交。让我们画后者:它有点复杂,但最终结果都是一样的。
他们有:
A--B--C <-- master
你 运行 git clone
得到了:
A--B--C <-- master (HEAD), origin/master
在您自己的存储库中。 (我们可以在 their 存储库中省略 HEAD
因为我们通常不关心哪个 b运行ch they请查看。)然后他们 将 master
重命名 为 main
并添加提交 D-E
。您 运行 git fetch
并得到:
A--B--C <-- master (HEAD), origin/master
\
D--E <-- origin/main
你的Git无法删除origin/master
,即使他们已经没有master
了,所以我们把它留在图中。请注意,它是无害的:它只是标记提交 C
。我们可以删除它——我们可以设置 fetch.prune
或 运行 git fetch --prune
或其他任何东西——或者保留它;这并不重要。 B运行ch 名称无关紧要!只承诺事项。提交 C
仍然存在,无论是否有名称指向它。
无论如何,也许你自己做了新的提交 F
:
F <-- master (HEAD)
/
A--B--C
\
D--E <-- origin/main
如果您要求 Git 合并提交 F
和 E
, 它有效 ,因为它们有一个共同的祖先:F
的parent是C
,E
的parent的parent是C
.
这告诉我们这不是他们所做的。
看起来是什么反而发生了
如果我们假设 你 没有做出一堆不相关的提交,那么在他们的 Git 存储库中一定发生了什么——在 Git Hub——是 他们 做了一个新的 root 提交,并使用名称 main
找到它:
A--B--C <-- master
D <-- main
然后,他们可能删除了他们的名字master
。这让他们在他们的存储库中留下了这个:
A--B--C ???
D <-- main
在这一点上——或者就在这之前——他们可能会也可能不会复制他们的部分或全部A-B-C
提交到[=之后的新提交71=]:
A--B--C ???
D--B'-C' <-- main
在这里,提交 B'
是提交 B
的副本:它对 D
所做的与 B
对 A
所做的一样。同样,C'
是 C
的副本,对 B'
做任何 C
对 B
做的事情。不过,新提交具有 新的和不同的哈希 ID,并向后指向提交 D
作为它们的根。因此,当您 运行 git fetch
将您的 Git 连接到他们的 Git 时,他们的 new 提交就是这些 D-B'-C'
, 这样你在你的存储库中, 结束了:
A--B--C <-- master (HEAD), origin/master
D--B'-C' <-- origin/main
如果你删除你的 origin/master
(因为他们的 master
不见了),什么都没有真正改变:你自己的 Git 仍在寻找提交 C
。他们的 Git 无法 找到提交 C
— 他们现在甚至可能已经把它扔掉了; Gits 最终删除了 un-find-able 提交——但是你的 Git 可以,通过你的 master
。如果您从那时起进行了新的提交,就像我们之前绘制的 F
一样,您甚至会有:
F <-- master (HEAD)
/
A--B--C <-- origin/master
D--B'-C' <-- origin/main
您不能进行合并,因为这些链没有共享历史记录。
那么可以做什么?
你现在面临着一堆选择。使用哪些取决于你想做多少工作,你想让其他人做多少工作,以及你对其他Git 存储库.
您可以:
(仅)继续使用您的提交并强制其他人切换。
没有理由更改提交。原件仍然和以前一样好。有人犯了错误,抄袭了他们。让他们吃掉他们的错误:将您的
master
重命名为main
,使用git push --force origin main
,并使 GitHub(或其他中央存储服务器)存储库使用 你的 以main
的名义提交,每个人都有 agreed-to.复制您喜欢的提交,将它们添加到 他们 最后一次提交的末尾。
假设他们的提交
C'
与您的(和最初他们的)提交C
具有相同的 保存的快照 ,或者任何提交都是原件的最后一个副本,您可以在C'
之后添加您的工作,每次提交使用git cherry-pick
,或git rebase --onto
执行多个 cherry-pick 操作。请参阅其他 Whosebug 问题了解如何做到这一点。与
--allow-unrelated-histories
合并。这种技术可以花费你最少的时间和精力,但它可能是混乱和痛苦的:中间的rebase / cherry-pick选项可能是更快更容易。
--allow-unrelated-histories
所做的只是 假装 在单独的根提交之前,有一个没有文件的提交。在某些情况下,这很容易工作。在大多数情况下,您会遇到一堆需要大量手动工作的“add/add 冲突”。它还有一个相当丑陋的副作用,即在您的存储库中留下额外的 mostly-useless 提交,然后您将永远随身携带。如果没有人查看这段历史(以及这两个根源),就没有人会 关心 ,但它仍然存在。它是否困扰您(或其他人)完全是另一个问题。
我无法为您选择这些选项中的一个,而且这不一定是所有选项的集合,但此时您至少应该对发生的事情以及为什么会这样有一个很好的理解处理方法。