如何修复基于提交修正的分支,这导致了"rebase in progress"?



我对 git 比较陌生,所以细节将不胜感激。请指出我是否做错了什么。

所以这就是问题的样子:

  1. 我从远程下载,它只有一个分支,主。
  2. 我承诺给师父
  3. 我将母版重新定位到新获取的源/主站后将更改推送到远程(尽管更改仍未合并到远程)
  4. 我从 master 创建了一个名为 bug1 的新分支并进行了一些更改
  5. 我在获取后将 bug1 重新定位为源/主(此更改尚未合并到远程)
  6. 我从 master 制作了一个分支,叫做 bug2。
  7. 修改了我最近在 master 上的提交(包含对 bug2 有用的所需信息)
  8. 我检查了 bug2,并做了git rebase master,希望在 master 中所做的所有更改现在都会反映在 bug2 中。
  9. 我相信,从观察中,我的目标确实实现了,关于 master 的工作现在在 bug2 中。
  10. 奇怪的是,当我做 git 状态时,我得到这个:
rebase in progress; onto ec578ba
You are currently rebasing branch 'bug2' on 'ec578ba'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git reset HEAD <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified:   c.py
both modified:   f.py
both modified:   s.py
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified:   t.py
modified:   co.txt
modified:   h.py
Untracked files:
(use "git add <file>..." to include in what will be committed)
blablaa.pyc
no changes added to commit (use "git add" and/or "git commit -a")

此外,git 分支 -a 显示:

* (no branch, rebasing bug2)
bug1
bug2
master
remotes/origin/HEAD -> origin/master
remotes/origin/master

我真的不知道我做错了什么...我以为这一切都会从我在网上学到的东西中解决,但显然我需要做一些"合并"来解决这个问题......

有人可以帮忙吗? 非常感谢!这似乎是一个太具体的问题,谷歌,我什至不知道我认为正确的术语。

正如git status所说,你真的处于变基的中间。 (如果你的 Git 版本是 2.0 或更高版本git status非常好且可靠;1.7 和 1.8 版本中有一些重大改进,所以如果你有一个真正古老的git status,你仍然应该使用它,但它不如现代 Git。

您陷入合并的原因是git commit --amend是一种善意的谎言:它实际上并没有改变现有的提交。 相反,它进行了一个新的和改进的提交,然后说:让我们使用这个而不是旧的。不幸的是,任何已经拥有并依赖于旧分支的分支仍然具有旧分支。

让我们详细浏览一下这些:

  1. 我从远程下载,它只有一个分支,主。

我将假设你的意思是你跑了:

git clone <url>
cd <new-clone>

这里。

  1. >我承诺给主人

因此:

<edit some file>
git add <that file>
git commit

现在让我们在这里退后一点,并描述提交是什么和做什么,并快速深入了解它是如何做到的。 (这里会有很多,但你可以记住有很多,稍后在适当的时候再回来。

提交和分支名称

您所做的每次提交都有所有文件的完整快照。 这些文件将永远冻结:它们无法更改。 提交还包含一些元数据,例如您的姓名和电子邮件地址、提交时的日期和时间戳以及提交原因- 您的日志消息。 Git 几乎不使用日志消息,所以你可以输入任何你喜欢的东西,但日志消息的目的是提醒你自己(或其他人)不是你做了什么——Git 可以自己向你展示——而是你为什么这样做,即只是更改本身无法显示的东西。

每个提交都有自己唯一的哈希 ID。 哈希 ID 是一个像83232e38648b51abbcbdb56c94632b6906cc85a6一样丑陋的大字符串;这些出现在很多地方,包括git log输出,有时以缩写形式(83232e3等)。 哈希 ID 是提交的真实"真实名称",也是 Git找到提交的方式:Git 需要在其小爪子中具有哈希 ID,以便提取提交。 因此,如果不是分支名称,你会一直把这些东西记在纸上,或者其他什么——但这很愚蠢,我们有一台电脑,我们可以让它把它们记在一个文件中,或者其他什么。

这就是分支名称的含义:它是一个文件,或文件中的一行,或者记住哈希 ID 的东西。 事实上,它只记住一个哈希 ID:分支中最后一个提交的哈希 ID。 但这没关系,因为每次提交还会记住一个哈希 ID。 具体来说,每个提交都会记住它之前的提交的哈希 ID,Git 称之为级。 所以我们最终得到:

... <-F <-G <-H   <--branch

大写字母代表真正的哈希 ID。名称branch保存上次提交H的哈希 ID。H本身保存早期提交G的哈希ID,该F的哈希ID保存,依此类推。 这允许 Git 向后浏览一系列提交。

要创建一个分支,您只需选择一些现有的提交并为其附加一个名称:

...--F--G   <-- newbranch

H   <-- master

或:

...--F--G--H   <-- newbranch, master

现在你有多个名称,Git 需要一种方法来知道你正在使用哪个名称。 这就是HEAD的用武之地:

...--F--G--H   <-- newbranch (HEAD), master

表示当前名称为newbranch(当前提交为H)。

请记住,我们注意到提交中的文件将始终被冻结。 它们都是只读的。 它们也被压缩,以便占用更少的空间。 我喜欢称这些为冻干。 像这样被冻结,如果你有很多提交不断重用大多数相同的文件,Git 可以而且实际上只是重用已经冻结的文件。 这是 Git 用来防止存储库快速增长的几个技巧之一,即使每次提交都会保存每个文件。 但是,虽然这对存档很有帮助,但对于完成任何新工作显然毫无用处。 您需要可以读写的文件。 因此,git checkout提取一个提交,该提交将成为您当前的提交,并将其所有冻干文件重新冻结到您的工作树中。 在这里,您的文件具有普通的日常格式。 您可以使用它们并使用它们。

Git 可以停在这里——冻结的提交文件,一组特别有趣的冻结文件是当前提交中的文件,以及工作树中的可用集——其他版本控制系统确实停在这里。 但是由于各种原因,Git 隐藏了文件的第三个副本,介于冻结集和可用集之间。 这些副本位于 Git 的索引暂存区域中- 这是同一事物的两个名称。 索引中的内容采用冻干格式。 但与实际提交不同的是,这些文件可以随时替换,或者添加新文件或删除文件。

每当你进行新的提交时,Git 真正做的是将预冻干的索引副本打包到一个提交中。 这运行得非常快,并且根本不需要使用工作树中的内容! 所以你首先必须让 Git 更新它的索引,这就是git add的意义所在。git add命令冻结干燥工作树文件并覆盖索引中的副本,或者如果该文件以前不在索引中,则在那里创建它。

打包文件并添加您的名称和日志消息等后,Git 将提交的父级设置为当前提交。 然后,它将新提交写入所有提交数据库:

...--F--G--H   <-- newbranch (HEAD), master

I

现在提交已经存在,git commit的最后一步是使名称(在本例中为newbranch)指向提交I而不是提交H

...--F--G--H   <-- master

I   <-- newbranch (HEAD)

虽然提交是快照,但 Git 可以显示更改

如果你要求 Git 向你展示提交I,或者为此提交H,Git可以向你显示其中的所有文件。 但这通常不是我们想知道的。 对于提交H,我们想知道:HG有什么不同所以当git log -pgit show显示提交H时,它实际上同时查看GH。 无论文件有何不同,Git 都会比较这两个文件并告诉您更改了哪些内容。 在查看提交I时,Git 会比较HI中的快照,并告诉您更改了哪些内容。

这是一个一般主题:对于任何只有一个父级的提交,Git 可以轻松地提取父级提交并比较两者。 这会将提交转换为一组更改。 请务必记住,这需要选择以前的提交。 然后,Git 将比较两个快照。 只是,对于大多数提交来说,选择是如此明显,以至于我们根本不需要命名它。

返回您的脚步

因此,这让我们回到第 2 步:您向master添加了一个新的提交。 让我们这样画

I   <-- master (HEAD)
/
...--F--G--H   <-- origin/master

(origin/master是你的 Git 记住master在你之前克隆的 Git 存储库中的位置或曾经的位置的方法)。

  1. 我在将母版重新定位到新获取的源/主站后将更改推送(尽管更改仍未合并到远程)

也就是说,您运行了:

git fetch origin              # or just `git fetch`, which does the same thing
git rebase origin/master      # or `git pull --rebase origin master`
git push origin master

假设没有人在origin时向Git仓库添加任何新的提交,变基步骤什么也没做。 不过,最后一步,git push origin master,做了一些重要的事情:

  • 你的 Git 在origin调用 Git
  • 您的 Git 会找出他们拥有哪些提交(通过哈希 ID)。
  • 您的 Git 提供了您没有的任何提交,即提交I,也通过哈希 ID。
  • 您的 Git 以向他们发出请求来结束git push会话:嘿,其他 Git,请将您的master设置为指向提交I

如果他们服从这个请求——这里没有列出他们不应该这样做的理由——他们最终会得到:

...--F--G--H--I   <-- master

他们的存储库中。 您的 Git 将看到他们接受了请求,并将更新您的origin/master以匹配,因此我们可以理顺存储库内容绘制中的扭结并将它们列出为:

...--F--G--H--I   <-- master (HEAD), origin/master

您添加了:

(不过,更改仍未合并到远程中)

但这无关紧要:它既不是真的也不是假的,因为没有什么可以合并的;但他们确实有你的新提交I和他们的master指向I

我从 master 创建了一个名为 bug1 的新分支并进行了一些更改

所以你跑了,例如:

git checkout -b bug1

它添加了一个新的分支标签bug1指向提交I并使该HEAD

...--F--G--H--I   <-- bug1 (HEAD), master, origin/master

然后你修改了一些文件,运行git add,并运行git commit来创建一个新的提交J

...--F--G--H--I   <-- master, origin/master

J   <-- bug1 (HEAD)
  1. 我在获取后将 bug1 重新定位为源/主(此更改尚未合并到远程)

同样,"尚未合并"几乎毫无意义:您的 Git 调用了他们的 Git,向他们提供了您的提交J,并要求他们创建他们的名字bug1指向提交J。 在任何git push期间都没有合并的问题:这只是他们是否接受您更改或创建某些分支名称的请求的问题。 所以他们现在只有两个分支名称,master指向Ibug1指向J。 然后你自己的 Git 对它说:啊,他们接受了创建bug1的请求,所以我现在会记得他们有一个指向提交Jbug1这意味着您自己的存储库现在可以绘制为:

...--F--G--H--I   <-- master, origin/master

J   <-- bug1 (HEAD), origin/bug1
  1. 我从master做了一个分支,叫做bug2。

那是:

git checkout -b bug2 master

或同等学历。 这会选择提交I作为当前提交,并为其创建一个新名称,将I的内容提取到索引和工作树中,并将HEAD附加到新名称:

...--F--G--H--I   <-- bug2 (HEAD), master, origin/master

J   <-- bug1, origin/bug1

(如果你使用git branch,那不会调整HEAD,你可能仍然在提交J,但除此之外,它的工作方式几乎相同。

  1. 修改了我最近在 master 上的提交(包含对 bug2 有用的所需信息)

所以:

git checkout master

此步骤选择提交I作为当前提交,并将HEAD附加到名称master。 如有必要,您的索引和工作树会进行调整以匹配I,但如果您已经在I则这里没有工作;最终结果是:

...--F--G--H--I   <-- bug2, master (HEAD), origin/master

J   <-- bug1, origin/bug1

所以在这里,唯一真正的变化是更改HEAD附加到的名称,除非您在提交J,在这种情况下它也签出了提交I。 然后你做到了:

<make some changes and run git add>
git commit --amend

--amend选项实际上根本不会更改提交I。 相反,它会进行新的、不同的提交。 我们可以称它为I'(以反映它是I的副本),或者只是给它一个全新的字母K。 我会在这里做后者。git commit --amend的棘手之处在于,新的提交不是指向I,而是指向IH

K   <-- master (HEAD)
/
...--F--G--H--I   <-- bug2, origin/master

J   <-- bug1, origin/bug1

请注意,提交I仍然存在,并且仍然通过bug2origin/master被记住。origin的 Git 存储库仍然有提交I;它仍然记得通过其master提交I。 你自己的bug1会记住提交J,它记得提交I,并且 Git 在源上也有J(它必然记住I- 提交在每个拥有它们的 Git 存储库中都是相同的)。

您可以在本地通过查看您自己的所有分支名称(bug1bug2master)并向后移动(一次一个提交)来跟踪谁拥有什么,以查看您可以到达哪些提交。 对远程跟踪名称执行相同的操作,origin/bug1origin/master以查看他们可以访问哪些提交。1

现在我们进入有问题的步骤:

  1. 我检查了 bug2,并做了git rebase master,希望在 master 中所做的所有更改现在都会反映在 bug2 中。

现在我们必须看看 rebase 做了什么——但简而言之,它复制了提交,就像git cherry-pick一样。 它复制的提交基于您为其提供的参数。


1至少,这是他们尽你自己的 Git 所知所拥有的。 它没有在超过三秒钟内调用另一个 Git,谁知道它在那段时间里可能发生了多大的变化! 这是了解谁在更改上游 Git 存储库可能很重要的地方。 它更新的速度有多快?


哪个提交变基副本

让我们回到图表,针对git checkout bug2进行调整:

K   <-- master
/
...--F--G--H--I   <-- bug2 (HEAD), origin/master

J   <-- bug1, origin/bug1

和命令:

git rebase master

在这里,您使用了最简单的git rebase形式。 您可以使用git rebase --onto将变基目标(或多或少地放置副本的位置)与限制器(即复制的内容)分开。 使用最简单的形式,两者是一回事:master既是在哪里放置副本,又是不可以复制什么

Git 现在将从bug2开始,向后浏览图形,列出提交。 这些是I,然后是H,然后是G,等等。 它还将从master开始并向后移动,列出提交:K,然后是H,然后是G,依此类推。第二个列表中的任何内容都将被排除在外阿拉伯数字这样就留下了提交I.

生成的列表是要复制的提交列表(顺序错误,因此 Git 将反转列表,但这里只有一个条目)。 Rebase 现在将尝试复制提交I,以便在提交K之后进行。 复制的确切机制有点复杂——有些git rebase操作实际上git cherry-pick运行,有些则不然——但它意味着无论哪种方式都有相同的结果,并且像git cherry-pick一样工作。 这实质上意味着将某人在H-vs-I路径中所做的更改与某人在H-vs-K路径中所做的更改合并

当然,那个"某人"就是。 这些变化相互冲突。 Git 无法自行解决冲突。 如果 Git 能够自行解决冲突,Git 会尝试进行新的提交——I的副本,除了新提交的父级将被K——然后它会完成并将名称设置为bug指向新提交。 由于它是I的副本(但实际上并没有发生),因此让我们将其称为I',以显示如果它确实发生,事情会是什么样子:

I'  <-- bug2 (HEAD)
/
K   <-- master
/
...--F--G--H--I   <-- origin/master

J   <-- bug1, origin/bug1

您可以通过从提交K中选择所有文件来解决冲突,即删除H-vs-I中尚未在H-vs-K中的所有更改,并仅保留K中的内容。 但是,打扰新提交根本没有真正的意义:只需将名称bug2指向直接提交K,如下所示:

K   <-- bug2 (HEAD), master
/
...--F--G--H--I   <-- origin/master

J   <-- bug1, origin/bug1

你可以git rebase来做到这一点,你只需要更长的形式:

git rebase --onto master <anything that specifies commit I>

--onto master告诉 Git:将副本放在master标识的提交之后,即在K之后,其余的告诉 Git:不要复制提交I,也不要复制任何更早的内容。 所以这将完成这项工作:它不会复制任何提交,然后将名称bug2移动。

在这种情况下,只需将名称移动到自己bug2就更简单了。 这特别容易,因为你从来没有用git push告诉任何其他Git 让这个名字bug2记住任何东西。 所以不需要告诉其他人:哦,嘿,我让你做的bug2? 算了,改用这个。

但是,您确实有问题masterbug1. 你已经告诉其他一些 Git(origin的那个)记住提交I是他们的master,并将提交J作为他们的bug1。 他们大概还记得那些。 你可以说其他 Git:这是提交K。 而且,哦,嘿,你记得的那个承诺I是你的master......记住新的提交K作为您的master这需要力推git push -fgit push --force-with-lease。 你还必须对自己的bug1做一些事情,比如使用git rebase --onto将提交J复制到K之后的新改进提交,然后在origin调用Git,向他们发送新的和改进的提交,并强制他们移动他们的bug1

现在该怎么办

不过,首先,你需要摆脱变基地狱。 最简单的方法是使用:

git rebase --abort

这会让一切恢复到您开始之前的状态。 也就是说,您现在返回到:

K   <-- master
/
...--F--G--H--I   <-- bug2 (HEAD), origin/master

J   <-- bug1, origin/bug1

现在,您可以做许多事情中的一件(或多件)。 以下是两个简单的选项:

  • git checkout -B bug2 master
  • git checkout master; git branch -D bug2; git checkout -b bug2

这些结果是:

K   <-- bug2 (HEAD), master
/
...--F--G--H--I   <-- origin/master

J   <-- bug1, origin/bug1

您现在可以继续处理bug2

在某个时间点(现在或以后的任何时间),您可以运行:

git push --force-with-lease origin master

让你的 Git 调用他们的 Git,向他们提供你的新提交K,并告诉他们:我认为你的master点提交I。 如果是这样,请改为提交K阿拉伯数字这与--force/-f非常相似,只是普通的强制只是向他们发送一个命令:设置你的master如果其他人(可能不是你)在I之后添加了更多的提交,你会导致 Git 在origin丢失这些提交。 在这种情况下,您希望他们的master"丢失"提交I:它已被新的和改进的K所取代。

然后,最终,您可能还需要:

git checkout bug1
git rebase --onto master bug1~1

这个特殊的语法——bug1~1——告诉 Git:bug1开始,通过一次提交向后工作。也就是说,它找到bug1(提交J),然后后退一步提交I。 这些是复制的提交 -I和更早的任何内容 - 所以git rebase现在将通过IJ进行比较来复制J,并在master的尖端提交之后添加这些更改。 如果您已经进行了一些bug2提交,但没有新的master提交,并强制origin/master记住K,那么现在可能如下所示:

L--M   <-- bug2
/
K   <-- master, origin/master
/ 
...--F--G--H   J'   <-- bug1 (HEAD)

I

J   <-- origin/bug1

现在你已经用新的和改进的J'替换了J,你可以在origin告诉 Git,忘记所有关于IJ的事情是可以的:他们应该将bug1设置为记住J'

git push --force-with-lease origin bug1

或者当然是更简单的git push -f origin bug1,它只是假设他们的bug1指向J,让他们忘记I-J部分,只记住J'


2--force-with-lease选项必须告诉另一个 Git:我认为你的名字<名称>指向提交。 如果是这样,请将其设置为它获取此信息的位置与往常相同:您的 Git 有您的origin/master,记住您上次运行 Gitgit fetch或成功git push origin master时您的 Gitmaster在哪里。 所以如果要更新的名称是master,旧的哈希来自你自己的origin/master;如果要更新的名称是bug1,则旧哈希来自您自己的origin/bug1

最新更新