如何将现有分支变成 Git 中的孤立分支?

12

有没有办法使现有的分支成为Git中的孤立分支?

git checkout --orphan似乎只能创建一个新的孤立分支?


2
在创建孤立分支后(git 2.9.x/2.10,2016年第三季度),您将能够进行挑选。请参见下面的我的答案 - VonC
这个回答解决了你的问题吗?[我可以孤立一个已存在的分支吗?](https://dev59.com/I3zaa4cB1Zd3GeqPSJ3c)(4.5年后发现可能有重复问题!)。那里的答案非常有效。 - Gabriel Devillers
6个回答

16
你是正确的,git checkout --orphan 只创建新的孤立分支。关键是这个过程不会影响索引。因此,只要你的 Git 版本不太老,Nick Volynkin's answer 就可以起作用。
如果您想保留原始提交消息,可以替换他的:
$ git commit -m'first commit in orphan'

使用:

$ git commit -C master~2

如果您的Git版本较旧,没有git checkout --orphan,那么这样做也可以:
$ commit=<hash>  # or, e.g., commit=$(git rev-parse master~2)
$ git branch newbranch $( \
    git log --no-walk --pretty=format:%B $commit | \
    git commit-tree -F - "${commit}^{tree}" \
)
$ git checkout newbranch
$ git cherry-pick $commit..master # may want -x; see below

您可以从git log中选择起始点,或者使用现有分支名称的~语法(这将继续使用像Nick回答中的master~2)。如果您只想要一个配方,那么这应该可以解决问题,但是如果您想知道发生了什么以及为什么有效(以及何时无效),请继续阅读。 :-)

关于分支需要知道的事情

在我们进一步讨论之前,似乎定义一些项目并描述正在发生的事情是一个好主意。

分支名称与提交图

首先,让我们清楚地区分分支名称,例如masternewbr,以及提交图的各个部分。 分支名称仅指向图表中的一个提交,指定为提示提交分支提示

*--o--o---o--o    <-- master
    \    /
     o--o--o--o   <-- brA
            \
             o    <-- brB

这张图有三个分支顶端,由masterbrAbrB指向。例如,brB的末端祖先沿着一条波浪线向左移动,有时也会向上移动,直到(唯一的)根提交*(与所有其他非根o提交区分开来)。提交*没有向左的提交-没有父提交指向它-这就使它成为根提交。
这个根提交在所有分支上都有。其他提交也在多个分支上。在master上有一个合并提交,它将brA中的提交合并进来,尽管brA有两个提交master没有。要跟随master返回根,必须向左直走,并在合并处向下-向左走,然后向上-向左走,回到brA分叉的地方。
请注意,我们可以有多个分支名称指向单个提交,或者指向嵌入在另一个分支中的“tip”提交的分支名称。
*--o--o---o--o    <-- master
    \    /
     o--o--o      <-- brA
            \
             o    <-- brB, brC

这里我们将分支brA“倒回”了一个提交,所以右侧中间行的提交是brA的最新提交,尽管它比brB的最新提交早一个提交。我们添加了一个新的分支brC,它指向与brB相同的提交(这使得它成为两个最新提交,希望这个提交不是英式英语“rubbish tip”的意思:“呃,这个提交真是个大烂摊!”)。

DAG图

该图具有一系列节点o,每个节点指向通常在其左侧的某些父节点。连接节点的线(或箭头)是有向边:连接图中子节点返回其父节点的单向街道或铁路线。
节点和从子节点到父节点的有向边链接形成了“提交图(commit graph)”。由于这个图是有向的(子节点指向父节点)且无环的(一旦你离开一个节点,你就永远不能回到它),因此被称为有向无环图(Directed Acyclic Graph或DAG)。DAG具有各种漂亮的理论性质,我们可以忽略其中大部分内容。
DAG可能具有“不连通子图(disconnected subgraphs)”的特点。
现在让我们考虑这个备选图:
*--o--o---o--o   <-- master
    \    /
     o--o--o     <-- brA

*--o--o--o       <-- orph

这个新分支,其顶端被命名为orph,有自己的根,并且与其他两个分支完全断开联系。
请注意,多个根是具有(非空)不相交子图的必要前提条件,但是根据您想要查看这些图形的方式,它们可能不足够。如果我们将brA的(最新提交的)尖端合并到orph1,我们将得到以下结果:
*--o--o---o--o   <-- master
    \    /
     o--o--o     <-- brA
            \
*--o--o--o---o   <-- orph

现在这两个“图碎片”已经连接在一起。然而,存在着子图(例如从orph^1brA开始的那些,它们是orph的两个父节点),它们是不相交的。(这与创建孤立分支并没有特别相关,只是你应该了解它们。)


1现代Git拒绝尝试这样的合并,因为这两个分支没有合并基础。旧版本的Git会执行合并,但结果不一定合理。


git checkout --orphan

--orphan分支是git checkout --orphan创建的一种分支:一个将具有新的、断开的根的分支。

它到达那里的方式是创建一个不指向任何提交的分支名称。Git称之为“未出生的分支”,处于这种状态的分支只有一种半存在的状态,因为Git通过泄露实现而暴露了实现细节。

未出生的分支

分支名称定义上总是指向该分支上最新的提交。但这给Git带来了一个问题,特别是在一个完全新的没有任何提交的存储库中:master应该指向哪里呢?

事实上,未出生的分支无法指向任何地方,因为Git通过记录它们作为<名称,提交ID>对来实现分支名称,所以它只能在有提交时记录分支。Git解决这个困境的方法是欺骗:分支名称根本不进入分支记录,而是仅进入HEAD记录。

在Git中,“HEAD”记录了当前分支名称。对于“游离HEAD”模式,“HEAD”记录了实际的提交ID-事实上,这就是Git确定存储库/工作树是否处于“游离HEAD”模式的方式:如果其“HEAD”文件包含一个分支“名称”,则它未分离,如果包含提交ID,则已分离。(不允许任何其他状态。)因此,在创建“孤立分支”时,或者在没有为“主”提交的尴尬时期,Git将名称存储在“HEAD”中,但实际上尚未创建分支名称。(也就是说,没有在“.git / refs / heads /”中输入,没有在“.git / packed-refs”中输入。)
作为一种奇特的副作用,这意味着您只能拥有一个未出生的分支。未出生的分支名称存储在HEAD中。检出另一个分支,带有或不带有--orphan,或者通过ID提交任何提交 - 任何更新HEAD的操作 - 都会清除所有未出生分支的痕迹。(当然,新的git checkout --orphan将其替换为新的未出生分支的痕迹。)
一旦进行第一次提交,新分支就会诞生,因为...

2使用“未打包”引用时,名称只是文件系统中的路径:.git/refs/heads/master。然后,提交ID就是此文件的内容。打包的引用存储方式不同,Git正在发展其他处理名称到ID映射的方法,但这是最基本的,目前仍需要让Git工作。

保留未出现的分支有两种明显的方法,但Git都未使用。(记录一下,它们是:创建一个空文件或使用特殊的“null hash”。空文件技巧有一个明显的缺陷:在命令或计算机崩溃面前非常脆弱,远不如使用null hash。)


提交过程

一般来说,在Git中进行新提交的过程如下:

  1. 更新和/或填充索引,也称为暂存区或缓存:git add各种文件。此步骤创建Git的blob对象,存储实际文件内容。

  2. 将索引写入一个或多个tree对象(git write-tree)。此步骤至少创建一个(顶级)树,对于每个文件和子目录,该树都有条目;对于文件,它列出了blob-ID,对于子目录,它列出了(在创建后)包含子目录文件和树的树。注意,这使索引保持不变,准备好进行下一次提交。

  3. 编写提交对象(git commit-tree)。此步骤需要一堆项目。对于我们的目的来说,最主要的是与此提交相关的(单个)树对象——这是我们刚从第2步得到的——以及父提交ID列表。

  4. 将新提交的ID写入当前分支名称。

第四步是分支名称始终指向尖端提交的“如何和为什么”。`git commit`命令从`HEAD`获取分支名称。在第三步中,它也以同样的方式获取主要(或第一个,通常只有一个)父提交ID:从`HEAD`读取分支名称,然后从分支读取尖端提交ID。(对于合并提交,它从`MERGE_HEAD`读取额外的父ID - 通常只有一个。)
当然,Git的`commit`知道未出生和/或孤立的分支。如果`HEAD`显示`refs/heads/master`,但分支`master`不存在……那么,`master`必须是一个未出生的分支!因此,这个新提交没有父ID。它仍然具有与往常相同的树,但它是一个新的根提交。它仍然将其ID写入分支文件,这会产生创建分支的副作用。
因此,实际上是“在新的孤立分支上进行第一次提交”才创建了该分支。

关于cherry-pick你需要知道的事情

在理论上,Git的cherry-pick命令非常简单(实践有时会变得有些复杂)。让我们回到我们的示例图表,并说明一个典型的cherry-pick操作。这次,为了讨论图表中的一些特定提交,我将给它们单个字母名称:

...--o--o--A--B--C   <-- mong
      \
       o--o          <-- oose

假设我们想要从分支中挑选提交B,并将其合并到分支中。这很简单,只需执行以下操作:
$ git checkout oose; git cherry-pick mong~1

这里的mong~1指代提交B。(这是因为mong指代提交C,而C的父提交是Bmong~1的意思是"沿着第一个父链接的主线向后移动一个父提交。同样,mong~2指代提交Among~3指代在A之前的o等等。只要我们不遍历具有多个父提交的合并提交,一切都非常简单。)

但是git cherry-pick实际上是如何工作的呢?答案是:它首先运行git diff。也就是说,它构建了一个补丁,类似于git log -pgit show所显示的那种。

提交具有完整的树形结构

记住(从我们之前的讨论中)每个提交都有一个附加的树对象。该树保存了该提交时的整个源代码树:当我们进行该提交时,索引/暂存区中的所有内容的快照。
这意味着提交B有一个完整的工作树与之关联。但是我们想要挑选B中所做的更改,而不是B。也就是说,如果我们更改了README.txt,我们想要获取我们所做的更改:不是README.txt的旧版本,也不是新版本,只是更改。
我们找到它的方法是从提交B回到其父提交,即提交A。提交A 也有一个完整的工作树。我们只需在两个提交上运行git diff,它会显示我们在README.txt中所做的更改以及我们所做的任何其他更改。
现在我们有了差异/补丁,回到我们现在所在的分支oose的尖端提交和我们工作树和索引/暂存区中与该提交对应的文件。(默认情况下,如果我们的索引不匹配我们的工作树,git cherry-pick命令将拒绝运行,因此我们知道它们是相同的。)现在Git只需(如同使用git apply一样)应用我们刚刚通过比较提交AB得到的补丁。
因此,无论我们从AB进行了哪些更改,我们现在都要对我们当前的提交/索引/工作树进行这些更改。如果一切顺利,这将给我们修改后的文件,Git会自动将其git add到我们的索引中;然后Git运行git commit以使用提交B的日志消息创建一个新的提交。如果我们运行了git cherry-pick -x,Git会将短语“cherry-picked from ...”添加到我们新提交的日志消息中。

(提示: 通常情况下,您希望使用 -x。它可能应该成为默认设置。主要的例外情况是,当您将刚刚提取的原始提交丢弃时。还可以争论使用 cherry-pick 通常是错误的——这表明您之前做错了什么,现在不得不加以掩盖,而这种掩盖可能在长期内无法持续,但这是另一个[很长]发布的问题。)

孤儿分支中的 Cherry-picking

VonC指出,在 Git 2.9.1 及更高版本中,git cherry-pick 可以在孤儿分支中工作; 在即将发布的版本中,它也适用于序列以及单个提交。但是,这样做不可能有很长时间的原因。

记住,cherry-pick 将一个 转换为一个 补丁,通过将一个提交与其父提交进行差异比较(或在合并提交的情况下,使用 -m 选项选择的父提交)。然后将该补丁应用于当前提交。但是孤立分支——我们尚未创建的分支——没有提交,因此没有当前提交,并且——至少在哲学意义上——没有索引和工作树。简而言之,就是没有可供打补丁的内容
实际上,我们可以(现在 Git 已经这样做了)完全绕过这个问题。如果我们曾经有过一个当前提交——如果我们在某个时刻检出了某些内容——那么我们现在仍然拥有索引和工作树,留存在最近的"当前提交"中。
这是 git checkout --orphan orphanbranch 命令的作用。你可以检出一些现有的提交,从而填充索引和工作树。然后你执行 git checkout --orphan newbranchgit commit 命令,新提交使用当前索引来创建或者实际上是重用一个树。该树与你在执行 git checkout --orphan orphanbranch 命令之前所检出的提交相同。3 这也是我关于非常老的 Git 的主要建议的来源。
$ commit=$(git rev-parse master~2)
$ git branch newbranch $( \
    git log --no-walk --pretty=format:%B $commit | \
    git commit-tree -F - "${commit}^{tree}" \
)
$ git checkout newbranch

首先,我们要找到所需的提交及其树:与master~2相关联的树。 (实际上我们不需要变量commit,但这样写可以让我们从git log输出中复制和粘贴哈希值,而不必计算它距离master或我们要在此使用的任何分支有多远。)

使用${commit}^{tree}告诉Git找到与提交相关联的实际树(这是标准的gitrevisions语法)。git commit-tree 命令将新的提交写入存储库,使用我们刚刚提供的树。新提交的父级来自我们使用-p选项提供的父ID:我们不使用任何选项,因此新提交没有父级,即是根提交。

此新提交的日志消息是我们在标准输入上提供的。为了获取此日志消息,我们使用git log --no-walk --pretty=format:%B,它只是将消息的完整文本打印到标准输出。

< p > git commit-tree 命令的输出结果是新提交的 ID:

$ ... | git commit-tree "master~2^{tree}"
80c40c288811ebc44e0c26a5b305e5b13e8f8985

每次运行都会产生一个不同的ID,除非所有运行在同一秒钟内,因为每个运行都有不同的时间戳集合;实际的ID在这里并不是非常重要。我们将此ID提供给git branch,以创建一个新的分支名称,该分支指向此新根提交作为其尖端提交。
一旦我们在新分支上有了新的根提交,我们就可以git checkout到新分支,并准备好挑选剩余的提交。
事实上,您可以像往常一样将它们组合在一起:

3


git checkout --orphan orphanbranch master~2

首先,它会检出(将其放入索引和工作树中)由master~2所标识的提交的内容,然后设置HEAD,使您处于未命名分支orphanbranch上。


使用 git cherry-pick 到孤立分支并不像我们想象的那么有用

我这里有一个新版本的 Git(它不能通过一些自己的测试——在 t3404-rebase-interactive.sh 中崩溃——但大致上似乎还可以):

$ alias git=$HOME/.../git
$ git --version
git version 2.9.2.370.g27834f4

让我们使用--orphan,将master~2 分支重新命名为 orphanbranch 以进行检查:

$ git checkout --orphan orphanbranch master~2
Switched to a new branch 'orphanbranch'
$ git status
On branch orphanbranch

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

    new file:   .gitignore
    new file:   a.py
    new file:   ast_ex.py
[snip]

因为这是一个新分支,Git 会认为所有东西都是新的。如果我现在尝试使用 git cherry-pick 命令来选择 master~2 或者 master~1:
$ git cherry-pick master~2
error: Your local changes would be overwritten by cherry-pick.
hint: Commit your changes or stash them to proceed.
fatal: cherry-pick failed
$ git cherry-pick master~1
error: Your local changes would be overwritten by cherry-pick.
hint: Commit your changes or stash them to proceed.
fatal: cherry-pick failed

我需要做的是清空所有内容,这种情况下,从master~3应用更改可能不起作用,或者只需进行初始git commit,以基于master~2的树创建新的根提交。

结论

如果你有git checkout --orphan,只需使用它来检出目标提交oldbranch~N(或通过哈希ID,可以从git log输出中复制和粘贴):

$ git checkout --orphan newbranch oldbranch~N

然后立即进行新的提交,就像Nick Volynkin所说的那样(您可以复制它的消息):

$ git commit -C oldbranch~N

为了创建分支,然后使用 git cherry-pickoldbranch~N..oldbranch 来获取剩余的提交记录:
$ git cherry-pick oldbranch~N..oldbranch

(根据您是否计划从oldbranch中删除提交,可以考虑使用-x。)请记住,oldbranch~N..oldbranch不包括提交oldbranch~N本身,但这实际上是有益的,因为它是我们作为新的根提交所做的一个。

2
这是一个有点简短的答案,没有脚注(因为有中间脚注),所以我有点失望。不过还是+1。 - VonC
1
这些“父级”问题在于Git用户会忘记“父提交”而更多地考虑“父分支”的概念(这个概念实际上并不存在)。每当涉及到“父分支”时,我总是想起Git T-Rex的回答:https://dev59.com/LXA75IYBdhLWcg3wrrNS#3162929。 - VonC
1
@VonC:是的,“父分支”在其他版本控制系统(例如Mercurial)中确实存在。Git中没有这个概念的原因是它被证明既是问题也是解决方案。 - torek
1
这表明了Linus对版本控制的愿景有多么不同寻常。他没有被过去的范式所束缚,其中“父分支”是有意义的,而是清晰地将一切都围绕内容(提交)进行定义。分支只成为图形中的一个(短暂的)路径,而不是一个固定的一等对象。 - VonC
1
哇,这是一个非常全面的答案。您介意添加一些 <!-- language: lang-bash --> 吗? - Nick Volynkin
@NickVolynkin:完成了;我还标记了一些(使一些项目正确着色)。它并不完美,因为它会错误地标记 shell 的输出,所以我通常不会费心 :-) - torek

6

我理解你的意思是,你希望孤儿分支已经有了一些提交历史?如果是这样,下面是一个解决方案。

首先你需要选择一个提交记录作为新分支的起点。在我的例子中,这是HEAD~2,也就是sha1=df931da

假设我们有一个简单的仓库,git log --oneline --graph --decorate命令显示如下:

* 4f14671 (HEAD, master) 4
* 1daf6ba 3
* df931da 2
* 410711d 1

现在,行动起来!

# Move to the point where we want new branch to start.
➜  gitorphan git:(master) git checkout HEAD~2

在这里以及之后,➜ gitorphan git:(master) 部分是 zsh 的提示符而不是命令的一部分。

# make an orphan branch
➜  gitorphan git:(df931da) git checkout --orphan orphanbranch
Switched to a new branch 'orphanbranch'

# first commit in it
➜  gitorphan git:(orphanbranch) ✗ git commit -m'first commit in orphan'
[orphanbranch (root-commit) f0d071a] first commit in orphan
 2 files changed, 2 insertions(+)
 create mode 100644 1
 create mode 100644 2

# Check that this is realy an orphan branch
➜  gitorphan git:(orphanbranch) git checkout HEAD^
error: pathspec 'HEAD^' did not match any file(s) known to git.

# Now cherry-pick from previous branch a range of commits
➜  gitorphan git:(orphanbranch) git cherry-pick df931da..master
[orphanbranch 7387de1] 3
 1 file changed, 1 insertion(+)
 create mode 100644 3
[orphanbranch 4d8cc9d] 4
 1 file changed, 1 insertion(+)
 create mode 100644 4

现在分支orphanbranch已经在单个提交中拥有了工作树的快照,此快照为df931da,并且后续提交与主分支中的提交一样。

➜  gitorphan git:(orphanbranch) git log --oneline
4d8cc9d 4
7387de1 3
f0d071a first commit in orphan

1
+1,尽管git 2.9.x/2.10会缩短这个过程。请参见我的答案 - VonC
1
@VonC 一如既往,你总是在 Git 功能的前沿)) - Nick Volynkin
1
它实际上是在10天前与git 2.9.1一起发布的,所以现在已经是旧闻了。 - VonC

4

Nick Volynkin回答涉及在新的孤立分支中至少进行一次提交。
这是因为git cherry-pick df931da..master没有第一次提交将导致“无法将樱桃拣到空头”。

但是,现在使用git 2.9.X/2.10(2016年第三季度)就不再需要了。

请见提交0f974e2(2016年6月6日),作者为Michael J Gruber(mjg
(由Junio C Hamano -- gitster --提交25227f0合并,2016年7月6日)

cherry-pick:允许选择未创建的分支

"git cherry-pick A"可以在未创建的分支上运行,但是"git cherry-pick A..B"则不行。

这意味着解决方案变成:

# make an orphan branch
➜  gitorphan git:(df931da) git checkout --orphan orphanbranch
Switched to a new branch 'orphanbranch'

# Now cherry-pick from previous branch a range of commits
➜  gitorphan git:(orphanbranch) git cherry-pick df931da..master

不需要先进行提交,再进行 cherry-pick。

然而,鉴别性挑选可能无法产生所需的结果,考虑到原始问题的形式... 我应该在这里放一个答案。 - torek
实际上,现在我看了一下,cherry-pick可以做到想要的事情(尽管2.9.1需要第一个单独的cherry-pick步骤,而pre-2.9需要完全单独的提交)。 - torek
或许也不完全是这样。无论如何,继续撰写答案。 - torek

3
这里有一个简单的方法来完成它。
git branch -m master old_master
git checkout --orphan master

-m = 将分支移动到新名称
checkout - 检出新的主干作为孤立分支


3
你如何将它推送到远程仓库?更名后的主分支不会存在于那里,你会收到错误信息... - user2227400

2
假设您已经checkout了一个新的分支,并且做了如下两个提交。13hh93是checkout的校验和,54hdsf是最新提交的校验和:
master => new_branch_1 (13hh93) => new_branch_2 => new_branch_3 (54hdsf)
然后按照以下步骤进行操作。步骤1回到checkout的起点,步骤2从此处创建一个孤立分支。步骤3将剩余的分支应用到您的孤立分支上。
1)git checkout 13hh93 2)git checkout new_orphan_branch --orphan 3)git diff 13hh93 54hdsf | git apply

1
阅读了 torek的答案 后,我发现如果您想要清除一个已经检出的 分支 的历史记录并将其变为孤立状态,而不浪费时间先检出其他分支或分离 HEAD,您可以使用以下命令删除该分支:
git update-ref -d refs/heads/BRANCH

这将使Git进入“孤立分支”模式。现在,您可以像往常一样暂存和提交文件,这将重新创建该分支作为一个没有父级的单个提交。
请注意,您无法以正常的方式删除已签出的分支。尝试运行git branch -d BRANCH(甚至是git branch --delete --force BRANCH)时,会打印错误消息而不是按照您的指示执行:
error: Cannot delete branch 'BRANCH' checked out at '/home/me/folder'

这通常是为了防止删除不想丢失的分支,但在需要一个孤立分支时会阻止其创建。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接