如何在git中使现有分支成为孤儿

Laz*_*rks 9 git branch git-branch

有没有办法让现有的分支成为git中的孤儿?

git checkout --orphan似乎只创造了一个新的孤儿?

Nic*_*kin 6

我是否理解您,您希望孤立分支已经拥有提交历史记录?如果是这样,这是一个解决方案.

首先,你需要选择一个提交来启动新的分支.在我的例子中,这将是HEAD~2,sha1=df931da.

说,我们有一个简单的回购.git log --oneline --graph --decorate显示以下内容:

* 4f14671 (HEAD, master) 4
* 1daf6ba 3
* df931da 2
* 410711d 1
Run Code Online (Sandbox Code Playgroud)

行动吧!

# Move to the point where we want new branch to start.
?  gitorphan git:(master) git checkout HEAD~2
Run Code Online (Sandbox Code Playgroud)

这里和更进一步的? 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
Run Code Online (Sandbox Code Playgroud)

现在,分支orphanbranch在单个提交中具有df931da处的工作树的快照,并且进一步提交就像它们在主服务器中一样.

?  gitorphan git:(orphanbranch) git log --oneline
4d8cc9d 4
7387de1 3
f0d071a first commit in orphan
Run Code Online (Sandbox Code Playgroud)


tor*_*rek 5

git checkout --orphan只创建新的孤立分支是正确的。诀窍在于,此过程使索引不受干扰。因此,只要您的Git不太古老,Nick Volynkin的答案就会起作用。

如果要保留原始提交消息,则可以替换他的:

$ git commit -m'first commit in orphan'
Run Code Online (Sandbox Code Playgroud)

与:

$ git commit -C master~2
Run Code Online (Sandbox Code Playgroud)

如果您的Git已经足够老了,还git checkout --orphan应该这样做:

$ git commit -C master~2
Run Code Online (Sandbox Code Playgroud)

您可以在其中选择起点git log或使用~具有现有分支名称的语法(这将继续master~2在Nick的答案中使用)。

如果您想要的只是一个食谱,那应该可以解决问题,但是,如果您想知道发生了什么,以及为什么它起作用(何时不起作用),请继续阅读。:-)

您需要了解的有关分支的知识

在进行下一步之前,最好先定义一些项目并描述正在发生的事情。

分支名称与提交图

首先,让我们做一个明确的区分分支之间的名称,如masternewbr和的各个部分提交图形。分支名称仅指向图中的一个提交,称为“ 提交提交”或“ 分支提示 ”:

*--o--o---o--o    <-- master
    \    /
     o--o--o--o   <-- brA
            \
             o    <-- brB
Run Code Online (Sandbox Code Playgroud)

该图有三个分支提示,指向的通过masterbrAbrBbrB例如,尖端的祖先以一条摇摆的线向后移动,总是向左移动,有时也向上移动到(单个)根提交*(与所有其他非根o提交区分开)。提交*没有左边提交的事实(没有父提交指向)是使它成为根提交的原因。

此根落实在所有分支上。其他提交也在多个分支上。在上有一个合并提交,即使例如有两个未提交的提交,它也master从中引入提交。若要返回到根目录,您必须在合并中向左直走,并且在合并时也要向下和向左,然后再在拆分处向上和向左返回。brAbrAmastermasterbrA

请注意,我们可以有多个分支名称指向单个提交,或者分支名称指向嵌入在另一个分支中的“提示”提交:

*--o--o---o--o    <-- master
    \    /
     o--o--o      <-- brA
            \
             o    <-- brB, brC
Run Code Online (Sandbox Code Playgroud)

在这里,我们brA通过一次提交“重绕”分支,因此右侧的中间行提交是的尖端brA,即使它是从尖端返回的一次提交brB。我们添加了一个新分支,brC该分支指向与(使其两次成为小费的提交相同的提交brB;我们希望此提交不是英式“垃圾小费”一词的小费:嗯,这个提交是绝对的秘诀! ”)。

DAG

该图具有一系列节点o,每个节点都指向一些通常位于其左侧的父级。连接节点的线(实际上是箭头)是有向边:单向街道或铁路线(如果需要的话),将图中的子节点连接回其父节点。

节点以及从子级到父级的有向边链接形成提交图。由于该图是针对(孩子父母)和非周期(一旦你离开一个节点,你永远不能回来的话),这就是所谓的d irected 一个循环拉斐或DAG。DAG具有各种良好的理论属性,对于该SO答案,我们可以忽略其中的大多数。

DAG可能断开了子图的连接

现在让我们考虑这个替代图形:

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

*--o--o--o       <-- orph
Run Code Online (Sandbox Code Playgroud)

这个新分支的尖端名为orph,具有自己的根,并且与其他两个分支完全断开连接。

请注意,多个根是具有(非空)不相交子图的必要先决条件,但是取决于您如何查看这些图,它们可能还不够。如果我们要合并(尖端提交)brAorph1,我们将得到:

*--o--o---o--o   <-- master
    \    /
     o--o--o     <-- brA
            \
*--o--o--o---o   <-- orph
Run Code Online (Sandbox Code Playgroud)

现在将两个“图形片段”合并在一起。然而,存在的子图(例如那些由起始orph^1brA,的两个亲本orph),其是不相交的。(这与创建孤立分支没有特别关系,只是您应该了解它们。)


1 Modern Git拒绝随意进行这种合并的尝试,因为这两个分支没有合并基础。较旧版本的Git会合并,不一定会产生明智的结果。


git checkout --orphan

orph分支是那种分支git checkout --orphan使得:一个分支,将有一个新的,断开的根源。

到达那里的方式是创建一个分支名称,该分支名称指向完全不提交。Git将此称为“未完成的分支”,处于这种状态的分支只有一种半存在,因为Git泄漏了实现。

未来的分支

根据定义,分支名称始终指向该分支上最尖端的提交。但这给Git带来了一个问题,尤其是在完全没有提交的全新仓库中:哪里可以master指向?

事实是,未出生的分支无法指向任何地方,并且由于Git通过将分支名称记录为<name,commit-ID>对2来实现分支名称,因此它只有在提交后才能记录分支。Git解决这个难题的方法是作弊:分支名称根本不进入分支记录,而仅进入HEAD记录。

HEAD,在Git中,记录了当前分支的名字。对于“分离式HEAD”模式,HEAD记录实际的提交ID,实际上,这就是Git确定存储库/工作树是否处于“分离式HEAD”模式的方式:如果其HEAD文件包含分支名称,则不分离,如果包含提交ID,则将其分离。(不允许其他状态。)

因此,要创建一个“孤立的分支”,或者在那个尴尬的时期(尚未提交)master,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都不使用。(作为记录,这些操作是:创建一个空文件,或使用特殊的“空哈希”。空文件技巧有一个明显的缺陷:面对命令或计算机崩溃,它将非常脆弱,远比使用空哈希值。)


提交过程

通常,在Git 中进行提交的过程如下:

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

  2. 将索引写入一个或多个对象(git write-tree)。此步骤会创建(或在极少数情况下会重用)至少一棵(顶层)树。该树具有每个文件和子目录的条目;对于文件,它列出了blob-ID,对于子目录,列出了(创建后)包含子目录的文件和树的树。注意,顺便说一句,这使索引不受干扰,为下一次提交做好了准备。

  3. 编写一个提交对象(git commit-tree)。这一步需要一堆物品。出于我们的目的,主要有趣的是与此提交一起使用的(单个)树对象(这是我们从第2步中刚刚获得的树对象)以及父提交ID的列表。

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

步骤4是分支名称始终指向提示提交的方式和原因。该git commit命令从中获取分支名称HEAD。它还在步骤3中以相同的方式获取主要(或first,通常仅)父提交ID:它读取HEAD以获取分支名称,然后从分支中读取提示提交ID。(对于合并提交,它从中读取其他父ID(通常只有一个MERGE_HEAD)。)

Git commit当然知道有关未出生和/或孤儿的分支。如果HEADrefs/heads/master,但是分支master不存在……那么,那master一定是一个未出生的分支!因此,此新提交没有父ID。它仍然具有与以往相同的树,但这是一个新的根落实。仍然会将其ID写入分支文件,这具有创建branch的副作用。

因此,它将实际创建分支的新的孤立分支上进行首次提交

您需要了解的关于樱桃采摘的事情

Git的cherry-pick命令在理论上非常简单(有时操作会有些复杂)。让我们回到示例图并说明典型的“樱桃拾取”操作。这次,为了讨论图表中的某些特定提交,我将给它们提供单名字母:

...--o--o--A--B--C   <-- mong
      \
       o--o          <-- oose
Run Code Online (Sandbox Code Playgroud)

假设我们想B从一个分支mong到另一个分支挑选承诺oose。这很容易,我们只需要:

$ git checkout oose; git cherry-pick mong~1
Run Code Online (Sandbox Code Playgroud)

在那里mong~1候任提交B。(这之所以有效,是因为mong指定commit C,并且Cparent的父项是B,并且mong~1意思是“沿第一个父级链接的主线移回一个父commit。同样,mong~2指定commit A,并mong~3指定o前一个A,依此类推。不要遍历具有多个父级的合并提交,此处的操作非常简单。)

但是git cherry-pick实际上如何工作?答案是:它首先运行git diff。也就是说,它构造了一个补丁,其类型为git log -pgit show

提交有完整的树

记住(根据我们先前的讨论),每个提交都有一个附加的树对象。该树保留了该提交时的整个源树:进行该提交时索引/临时区域中所有内容的快照。

这意味着提交B具有与其关联的整个完整工作树。但是,我们要摘樱桃的变化,我们在做B,而不是 B。也就是说,如果我们进行了更改README.txt,我们希望得到所做的更改:不是旧版本README.txt,也不是新版本,而仅仅是更改。

我们发现这一点的方式是,我们从提交B返回到其父级,即commit A。Commit A 具有一个完整的工作树。我们只运行git diff了两次提交,向我们展示了我们在中所做的更改README.txt以及所做的任何其他更改。

现在我们有了diff / patch,我们回到现在的位置-branch的最尖端提交oose,以及我们在工作树和索引/临时区域中与该提交相对应的文件。(git cherry-pick默认情况下,如果索引与我们的工作树不匹配,该命令将完全拒绝运行,因此我们知道它们是相同的。)现在,Git只需应用(与一样git apply)我们刚刚通过比较提交A和获取而获得的补丁。B

因此,无论我们从A到进行了什么更改B,我们现在都进行了更改,到当前的提交/索引/工作树。如果一切顺利,这将为我们提供修改后的文件,Git会自动git add将其添加到索引中;然后Git运行git commit使用commit的日志消息进行新的提交B。如果我们运行了git cherry-pick -x,Git将短语“从...中摘樱桃”添加到新提交的日志消息中。

提示:您通常想使用-x 它可能应该是默认值。主要的例外是当您不保留刚刚挑选的原始提交时。有人还可以说使用cherry-pick通常是错误的-这是一种表示您确实在做错某事,确实是现在,现在必须将其纸上记录,并且从长远来看,纸上记录可能不会成立,但这完全是另一个[长期]发布。)

在一个孤儿分支采摘樱桃

VonC指出,在Git 2.9.1和更高版本中,它git cherry-pick在孤立分支中工作;在即将发布的版本中,它既可以用于序列,也可以用于单个提交。但是有一个原因,使这种情况不可能长期存在。

请记住,通过将提交相对于其父项(或者,如果是合并提交,则是使用该选项选择的父项)来比较,从而cherry-pick将一棵树变成一个补丁-m。然后,它将补丁应用到当前提交。但是,孤立分支(我们尚未建立的分支)没有提交,因此也没有当前提交,并且至少从哲学的角度来看,没有索引,也没有工作树。根本没有补丁

但是,实际上,我们可以(并且现在Git可以)完全绕开它。如果我们曾经进行过一次当前提交(如果我们在某个时候已经签出了某些内容),那么现在,只要有最新提交的“当前提交”,我们现在仍然有一个索引和一个工作树。

这是做什么的git checkout --orphan orphanbranch。您签出一些现有的提交,并因此填充索引和工作树。然后,您git checkout --orphan newbranchgit commit新提交将使用当前索引来创建(或实际上重用)树。该树您在执行之前签出的提交是同一棵树git checkout --orphan orphanbranch3

这也是我的旧版Git食谱的主要部分:

$ 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
Run Code Online (Sandbox Code Playgroud)

首先,我们找到所需的提交及其树:与关联的树master~2。(我们实际上并不需要该变量commit,但是这样写就可以使我们从git log输出中剪切并粘贴一个哈希,而不必计算它离master我们将在此处使用的分支或尖端的距离有多远)

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

这个新提交的日志消息就是我们在标准输入中提供的内容。要获取此日志消息,我们使用git log --no-walk --pretty=format:%B,它只是将消息的全文打印到标准输出中。

git commit-tree命令将新提交的ID作为其输出生成:

*--o--o---o--o    <-- master
    \    /
     o--o--o--o   <-- brA
            \
             o    <-- brB
Run Code Online (Sandbox Code Playgroud)

(每次运行都会产生一个不同的ID,除非所有运行都在相同的一秒钟内运行,因为每个运行都有不同的时间戳集;此处的实际ID并不十分重要)。我们使用该ID来git branch创建一个新的分支名称,该分支名称将指向此新的根提交,作为其尖端提交。

一旦在新分支上有了新的root提交,就可以git checkout建立新分支,并且我们准备好挑选其余的提交。


3实际上,您可以像往常一样将它们组合在一起:

git checkout --orphan orphanbranch master~2
Run Code Online (Sandbox Code Playgroud)

它首先检出(放入索引和工作树)由标识的提交的内容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
Run Code Online (Sandbox Code Playgroud)

让我们看看,用--orphanmaster~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]
Run Code Online (Sandbox Code Playgroud)

因为这一个新分支,所以Git看起来好像一切都是新的。如果我现在尝试git cherry-pick要么master~2master~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
Run Code Online (Sandbox Code Playgroud)

我要做的就是清除所有内容,在这种情况下,应用从master~3到的更改master~2不太可能起作用,或者git commit无论如何只是进行初始操作,以基于from的树进行新的根提交master~2

结论

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

$ git checkout --orphan newbranch oldbranch~N
Run Code Online (Sandbox Code Playgroud)

然后立即执行新提交,如Nick Volynkin所说(您可以复制其消息):

$ git commit -C oldbranch~N
Run Code Online (Sandbox Code Playgroud)

从而创建分支;然后使用git cherry-pickwith oldbranch~N..oldbranch来获取剩余的提交:

$ git cherry-pick oldbranch~N..oldbranch
Run Code Online (Sandbox Code Playgroud)

(也许使用-x,取决于您是否打算从中剥离提交oldbranch。)请记住,oldbranch~N..oldbranch排除提交oldbranch~N本身,但这实际上很好,因为这是我们作为新的根提交所做的。

  • 这是一个简短的答案,没有脚注(最后,因为有中间脚注),所以我有些失望。虽然还是+1。 (2认同)

Mat*_*ias 2

假设您已签出一个新分支并进行了两次提交,如下所示。13hh93 是签出的校验和,54hdsf 是最新提交的校验和:

主=> new_branch_1 (13hh93) => new_branch_2 => new_branch_3 (54hdsf)

然后进行如下操作。步骤 1 开始结账。第 2 步从中创建一个孤立分支。步骤 3 将分支的其余部分应用到您的孤立分支。

1) git checkout 13hh93 2) git checkout new_orphan_branch --orphan 3) git diff 13hh93 54hdsf | git 应用