首页 > 解决方案 > 如何在 git 中创建当前分支的完整孤儿副本?

问题描述

假设我有这样的事情: git log --oneline --graph --decorate

* 286f295 (HEAD -> Super_Branch) Adding super file
* bdeddb8 (origin/empty_base, empty_base) Initial commit

我想让 Super_Branch 成为孤儿并在该分支中创建每个提交的副本。 git subtree split做了类似的事情,但它提取了一些通往孤儿分支的路径的历史;我只想让现有的分支成为孤儿。

理想情况下,我不想使用git checkoutgit checkout --orphan因为我不想更改当前的 HEAD。

标签: git

解决方案


孤儿分支实际上并不存在。真的,说真的,它没有。这就是使它成为孤儿分支的原因。我想也许你想要的是创建一个新的root commit。有几种方法可以做到这一点,但在我们到达那里之前,我需要提供正确的背景。

正如poke 评论的那样,Git 在这里有点奇怪。提交本身就存在。它们是非常真实的实体,有自己独立的存在。每个提交都有自己唯一的名称——它又大又丑的哈希 ID——你(或 Git)可以通过它随时访问那个特定的提交。而且,每次提交都会存储一些数据(所有文件的快照)和一些元数据,例如您的姓名和电子邮件地址以及时间戳(或者实际上是其中的两个)。

每个提交中的元数据项之一是父哈希 ID 列表。这个列表通常只有一个条目。当我们遇到这样一个提交时——一个非常普通的提交——我们喜欢通过将它与它的父级进行比较来查看它。那是什么git log -p或做什么git show:他们首先打印出元数据,并进行适当的格式化,以便我们看到谁提交了以及何时提交,然后运行git diff存储在父级中的快照和这次提交中的快照,看看两者有什么不同。由此,我们推断无论是谁做出了提交,都做出了这些改变。

合并提交只是具有至少两个父级的任何提交。因为它确实有两个父母,我们不能只将它与它的(单)父母进行比较,看看发生了什么变化。所以git log -p只显示元数据,然后继续显示父母双方(虽然一次一个)而不尝试差异。1

因此,这涵盖了一个父级(普通)和两个或多个(合并)的提交。那只剩下一种可能的情况:没有父级的提交。没有父级的提交是根提交。当git log -p显示根提交时,它像往常一样显示元数据,然后对 Git 的特殊空树进行比较,以便看起来所有文件都添加到该提交中。(毕竟,他们是。)


1git show命令确实尝试了差异,并使用组合差异技巧进行。这并没有显示发生了什么变化!它不能,真的。因此,相反,它显示了将每个差异与每个父级组合的结果的一些更改子集。对于某些文件未更改的任何父子比较对,它根本不显示任何内容。对于那些在子文件中与所有父文件不同的文件,它会将每个父与子 diff 混合到组合的 diff 中。一些东西也被扔掉了,而且大多数情况下,你可以看到有人可能不得不解决冲突的领域。

很有用,但它不会告诉您实际发生了什么变化。要找出与其中一位父母相比发生了什么变化,请使用--first-parent-m或明确git diff选择的父母与孩子。


新的空存储库中的第一次提交始终是根提交

当您创建一个新的、完全空的存储库时:

mkdir somedir; cd somedir; git init

你得到一个没有提交的存储库,也没有分支名称。这是因为在 Git 中,像这样的分支名称master 必须包含一些现有提交的原始哈希 ID。没有现有的提交,因此不能有分支名称。

尽管如此,如果你运行git status,Git 会说你是on branch master. 你怎么能在一个不存在的分支上?

答案是:这个分支是一个孤儿分支。它不存在,但你在上面。

如果你使用git checkout -b other从不存在的分支切换master到新的不存在的分支othergit status现在会说on branch other。你继续在一个不存在的分支上——一个孤立的分支——并且继续没有提交。

在您实际提交之前,不会存在任何分支。因此,此时您只能在孤儿分支上。该git checkout -b命令将继续创建孤立分支,并且由于孤立分支实际上并不存在,因此您所在的旧孤立分支继续不存在,并且git branch例如不会显示。2

无论何时你处于这种状态——在一个不存在的分支上,即一个“孤儿”分支——你所做的下一个提交将是一个根提交。也就是说,它将是一个没有父级的提交(并且,像所有新提交一样,一个新的唯一哈希 ID)。现在这个提交已经存在,Git 终于可以创建分支名称了。因此,它采用通过 记住的孤立分支名称HEAD,并创建分支名称,指向新的根提交。现在你有了一个真实的分支名称——一个常规的、普通的分支名称,它确实存在并将显示git branch.


2在这一切之下,这种奇怪的事态其实很简单。.git/HEAD包含当前分支名称的文件会不断更新。没有创建分支!我们只是将一个新名称填入.git/HEAD,而不做任何其他事情。


git checkout --orphan name重新创造这种奇怪的状态

如果你运行:

git checkout -b branch-X

branch-XGit 创建指向当前提交的分支名称。分支现在存在。分支名称包含当前提交的哈希 ID——当然,Git 会附加HEAD到新名称(即,将名称branch-X写入.git/HEAD或者,如果这是添加的工作树,则写入其他更合适的文件)。

但是如果你运行:

git checkout --orphan branch-X

Git不会创建分支名称。相反,它附加HEAD到名称而不创建它。这与我们在完全空的存储库中的状态相同(当然,除了实际上可以创建分支名称:Git 只是选择立即创建它)。

--orphan选项是 Git 1.7.2 中的新选项(在发行说明中已提及)。它只是将 Git 置于一种状态,在这种状态下,您进行的下一次提交将是根提交。创建该根提交将创建分支名称,指向刚刚创建的新提交。

下一次提交的内容是什么?答案和往常一样。当你运行 时git commit,Git 会写出索引中的任何内容作为新提交的快照。向此快照添加来自user.nameuser.email、您的日志消息等的元数据。它还添加了当前提交的父级——或者在这种情况下,没有父级,因为你在一个不存在的分支上。这使得这个新提交成为根提交,内容来自索引,元数据和往常一样。

在您的特定情况下,您希望内容(快照)反映现有提交的内容。所以你可以使用git checkout --orphan,如果你有它——如果你的 Git 至少是 1.7.2——首先检查现有的提交,286f295或者Super_Branch

git checkout 286f295         # detached HEAD, or
git checkout Super_Branch    # HEAD attached to Super_Branch

这会从 commit 填充您的索引和工作树286f295。(确保您没有同时进行未提交的更改:请参阅Checkout another branch when there are uncommitted changes on the current branch。)然后您只需git checkout --orphan一个新的分支名称 - 您不能使用Super_Branch,因为该名称已经存在:

git checkout --orphan tiny_tim

然后git commit提供新的日志消息并使用当前的日期和时间等等。

但是:如果您的 Git 早于 1.7.2,或者您想重用来自 commit 的日志消息286f295怎么办?在这里,你可以作弊:git commit-tree直接使用。该git commit-tree命令在给定一些现有快照的情况下创建一个提交 - 一个树对象,在 Git 内部语言中 - 日志消息来自其标准输入。所以:

git log --no-walk --pretty=format:%B 286f295

将从中提取提交消息286f295。管道git commit-tree将按照您想要的方式设置日志消息。(你仍然是新提交的作者和提交者,“现在”。)因此:

git log --no-walk --pretty=format:%B 286f295 | git commit-tree 286f295^{tree}

将创建所需的根提交,并打印出其哈希 ID。

现在您只需要创建一个分支名称,指向新的提交:

git branch tiny_tim <hash-id>

从哪里来hash_idgit commit-tree命令。


推荐阅读