使用Git将最近的提交移动到新分支

6465

我如何将在主分支上的最近提交移到一个新的分支,并将主分支重置为这些提交之前的状态?例如,从这个状态:

master A - B - C - D - E

转化为:

newbranch     C - D - E
             /
master A - B 

163
注意:我在这里提出了相反的问题 [https://dev59.com/X3A75IYBdhLWcg3wDUq2]。 - Benjol
7
这个链接是关于如何将最新的git提交移动到一个新的或已经存在的分支中。 - Sagar Naliyapara
23
这里的评论被清理了吗?我这么问是因为每两个月我都会来看这个问题,但我总是在滚动中错过那条评论。 - Tejas Kale
7
旁注:这个问题非常简单。阅读答案时,所有的“不要这样做,因为……”、“更好的解决方案是……”和“警告版本n+……”都出现了(可能是因为太晚了),我觉得即使是非常简单的操作在git中也没有直接的解决方案。一个图形化的版本管理器,在不处理我看起来比较模糊和过时的语法的情况下,只需为新分支添加一个标签,将会如此令人宽慰。我的王国和金徽章将授予第一个“forks” git并开始新方法的人;-)这是紧急的。 - mins
3
请务必阅读前十个答案(左右),因为最好的答案不一定是得票最高的。 - chb
显示剩余3条评论
22个回答

8289

移动到现有分支

如果您想将您的提交移动到一个现有分支,操作如下:

git checkout existingbranch
git merge branchToMoveCommitFrom
git checkout branchToMoveCommitFrom
git reset --hard HEAD~3 # Go back 3 commits. You *will* lose uncommitted work.
git checkout existingbranch

你可以在执行此操作之前,使用git stash将未提交的编辑保存到你的存储区。完成后,你可以使用git stash pop检索已保存的未提交的编辑。

切换到新分支

警告:这种方法有效是因为你使用第一个命令创建了一个新分支:git branch newbranch。如果你想将提交移到一个已存在的分支,你需要在执行git reset --hard HEAD~3之前将你的更改合并到已存在的分支中(参见上面的切换到已存在的分支)。如果你不先合并你的更改,它们将会丢失。

除非涉及其他情况,否则通过创建分支和回滚可以轻松完成此操作。

# Note: Any changes not committed will be lost.
git branch newbranch      # Create a new branch, saving the desired commits
git checkout master       # checkout master, this is the place you want to go back
git reset --hard HEAD~3   # Move master back by 3 commits (Make sure you know how many commits you need to go back)
git checkout newbranch    # Go to the new branch that still has the desired commits

但请确保要回退多少次提交。或者,您可以不使用HEAD~3,而是直接提供要在master(/当前)分支上“还原回去”的提交的哈希值(或引用,如origin/master),例如:
git reset --hard a1b2c3d4

注意:你只会从主分支“丢失”提交,但不用担心,你会在新分支上保留这些提交!一个简单的方法是,在完成上述4个步骤的命令序列后,通过查看`git log -n4`来检查。这将显示新分支的历史记录实际上保留了这3个提交(原因是在创建newbranch时,这些更改已经在master上提交了!)。它们只是从master中删除了,因为`git reset`只影响到执行时被检出的分支,即master(参见git reset描述:重置当前HEAD到指定状态)。然而,`git status`不会显示任何关于新分支的检出,这可能一开始会让人感到惊讶,但这实际上是预期的。
最后,你可能需要强制推送你的最新更改到主仓库:
git push origin master --force

警告:在Git版本2.0及更高版本中,如果您稍后将新分支(git rebase)放在原始(master)分支上,可能需要在重新基于操作期间使用显式的--no-fork-point选项,以避免丢失已传递的提交。设置了branch.autosetuprebase always会增加这种情况发生的可能性。有关详细信息,请参阅John Mellor的答案


287
特别是,不要尝试回到你最后一次向其他人可能已经拉取的另一个存储库推送提交的点之前。 - Greg Hewgill
129
你想知道为什么这样做是有效的。对我来说,你正在创建一个新的分支,从旧分支中移除了3个提交,然后切换到你创建的新分支上。那么,你删除的提交是如何神奇地出现在新分支中的? - Jonathan Dumaine
163
因为我在删除旧分支的提交记录之前创建了新分支,所以这些提交记录仍然存在于新分支中。 - sykora
101
在Git中,分支只是指向历史提交的标记,没有任何内容被复制、创建或删除(除了这些标记)。 - knittl
253
注意:不要在工作副本中有未提交的更改!这让我吃了亏!:( - Adam Tuttle
显示剩余34条评论

1250

对于那些想知道它为什么有效(就像我一开始那样)的人:

你想要回到C,并将D和E移到新分支。起初,它看起来像这样:

A-B-C-D-E (HEAD)
        ↑
      master

在执行git branch newBranch命令后:

    newBranch
        ↓
A-B-C-D-E (HEAD)
        ↑
      master

执行git reset --hard HEAD~2后:

    newBranch
        ↓
A-B-C-D-E (HEAD)
    ↑
  master

因为一个分支只是一个指针,master 指向了最后一次提交。当你创建 newBranch 时,你只是简单地创建了一个指向最后一次提交的新指针。接着使用 git reset 命令将 master 指针向后移动了两个提交。但是由于你没有移动 newBranch,所以它仍然指向最初的提交。


80
为了在主代码库中显示更改,我还需要执行git push origin master --force的操作。 - Dženan
16
这个答案意味着提交记录将会丢失:下一次当你执行 git rebase 时,这3个提交记录将会在 newbranch 中被无声地删除。详情和更安全的替代方法请查看我的答案 - John Mellor
23
@John,那是胡说八道。如果你不知道怎么做就进行变基,会导致提交丢失。如果你的提交丢失了,我为你感到难过,但这个回答并没有丢失你的提交。请注意,上面的图表中没有出现origin/master。如果你推送到origin/master,然后再进行上述更改,当然会出现问题。但这是一种“医生,我这样做时很疼”的问题。而且这已经超出了原始问题所问的范围。我建议你写下你自己的问题来探索你的情况,而不是挟持这个问题。 - Ryan Lundy
1
@John,在你的回答中,你说“不要这样做!git branch -t newbranch”。回去再读一遍答案。没有人建议这样做。 - Ryan Lundy
2
@JohnMellor,你说得对,调用git rebase而没有参数会启用--fork-point选项,这可能会导致提交丢失。在我看来,这是git rebase的一个缺陷。尽管如此,它与我上面的答案无关。许多情况下,使用没有参数的git rebase都会引起麻烦。不幸的是,故事的寓意是即使该分支已经是上游分支,也不要调用git rebase而不传入分支。Git的可用性在过去的八年左右有了很大的改进,但这是一个需要更多工作的领域。 - Ryan Lundy
显示剩余5条评论

593

总的来说...

在这种情况下,sykora公开的方法是最好的选择。但有时它并不是最简单的方法,也不是通用的方法。对于通用方法,请使用git cherry-pick

要实现OP想要的效果,需要进行两个步骤:

第一步 - 记录你想在newbranch上的哪些提交来自master分支

执行以下命令:

git checkout master
git log

注意要在newbranch上放置(比如说3个)指定的提交记录哈希值。在这里我将使用:
C提交记录: 9aa1233
D提交记录: 453ac3d
E提交记录: 612ecb3

注意: 您可以使用前七个字符或整个提交记录哈希值

第2步 - 把它们放在newbranch

git checkout newbranch
git cherry-pick 612ecb3
git cherry-pick 453ac3d
git cherry-pick 9aa1233

OR(在 Git 1.7.2+ 上,使用范围)

git checkout newbranch
git cherry-pick 612ecb3~1..9aa1233

git cherry-pick 将这三个提交应用到 newbranch 分支。


19
如果你不小心提交了错误的非主分支,而应该创建一个新的功能分支,那么这个方法非常有效。 - julianc
9
关于git cherry-pick的信息很好,但是这篇文章中的命令不起作用。1)'git checkout newbranch'应该改为'git checkout -b newbranch',因为newbranch不存在;2)如果你从现有的master分支检出newbranch,则它已经包含了这三个提交,所以没有必要选择它们。最终,为了达到原帖子作者想要的效果,你仍然需要做某种形式的reset --hard HEAD。 - JESii
5
在某些情况下,这是一种有用的方法,值得赞赏。如果你只想把自己的提交(夹杂在别人的提交中)提取到一个新分支中,那么这种方法非常好。 - Tyler V.
11
更好的做法是这样,你可以将提交移到任何分支上。 - skywinder
3
通常可以使用3-5个十六进制数字作为引用,足够让 Git 唯一地识别该提交。还可以使用 "master~3..master" 来表示 "master" 分支上最近的三次提交,而不需要使用哈希值来引用它们。 - Zaz
显示剩余11条评论

448

大多数之前的回答非常危险!

不要这样做:

git branch -t newbranch
git reset --hard HEAD~3
git checkout newbranch

作为下一次运行git rebase(或git pull --rebase)时,这3个提交将被无声地从newbranch中丢弃!(请参见下面的解释)

相反,请执行以下操作:

git reset --keep HEAD~3
git checkout -t -b newbranch
git cherry-pick ..HEAD@{2}
  • 首先,它会丢弃最近的3个提交记录(--keep类似于--hard,但更安全,因为它会失败而不是放弃未提交的更改)。
  • 然后它将分叉newbranch
  • 接下来,它会将那3个提交记录挑选回到newbranch。由于它们不再被引用于一个分支上,所以使用git的reflogHEAD@{2}HEAD在2个操作之前所指向的提交记录,即在我们检出newbranch并使用git reset丢弃这3个提交记录之前。

警告:reflog默认启用,但如果您手动禁用了它(例如使用“裸”的git存储库),则运行git reset --keep HEAD~3后将无法找回这3个提交记录。

另一种不依赖于reflog的替代方法是:

# newbranch will omit the 3 most recent commits.
git checkout -b newbranch HEAD~3
git branch --set-upstream-to=oldbranch
# Cherry-picks the extra commits from oldbranch.
git cherry-pick ..oldbranch
# Discards the 3 most recent commits from oldbranch.
git branch --force oldbranch oldbranch~3

(如果您愿意,可以写@{-1} - 之前检出的分支 - 而不是oldbranch。)

技术解释

为什么第一个示例后面的3个提交会被git rebase丢弃?这是因为git rebase没有参数时默认启用--fork-point选项,它使用本地reflog尝试针对上游分支的强制推送保持稳健。

假设当origin/master包含提交M1、M2、M3时,你从中创建了一个分支,然后自己进行了三次提交:

M1--M2--M3  <-- origin/master
         \
          T1--T2--T3  <-- topic

但是有人通过强制推送 origin/master 来删除 M2,从而改变了历史记录:
M1--M3'  <-- origin/master
 \
  M2--M3--T1--T2--T3  <-- topic

使用本地reflog,git rebase可以看到你从origin/master分支的早期版本进行了分叉,因此M2和M3提交并不真正属于你的主题分支。因此,它合理地假设,由于M2已从上游分支中删除,一旦重新定义主题分支,你也不再想要它在主题分支中:
M1--M3'  <-- origin/master
     \
      T1'--T2'--T3'  <-- topic (rebased)

这种行为很合理,通常在变基时做得很好。因此,以下命令失败的原因是:
git branch -t newbranch
git reset --hard HEAD~3
git checkout newbranch

这是因为他们把reflog保存在了错误的状态。Git将newbranch视为从上游分支的一个修订版本分叉出来,该修订版本包括3个提交,然后执行reset --hard命令重写上游的历史以删除这些提交,所以下一次运行git rebase时会像任何其他已从上游中删除的提交一样将它们丢弃。
但在这种特殊情况下,我们希望这3个提交被视为主题分支的一部分。为了实现这一点,我们需要在不包括这3个提交的早期修订版本处分叉上游。这就是我提出的解决方案所做的,因此它们都保留了正确状态的reflog。
有关更多详细信息,请参见git rebasegit merge-base文档中的--fork-point定义。

27
这个答案说“不要这样做!”,但没有人提出过这样的建议。 - Ryan Lundy
10
大多数人不会修改已发布的历史记录,特别是在“master”分支上。因此,不,他们没有犯严重错误。 - Walf
5
git branch命令中,你所提到的-t选项是隐含的,如果你设置了git config --global branch.autosetuprebase always,即使你没有设置它也是如此。即使你没有设置,我已经向你解释过,在执行这些命令后设置跟踪会导致相同的问题,因为OP可能会按照他们的问题意图进行设置。 - John Mellor
4
@RockLee,是的,解决这种情况的一般方法是从一个安全的起点创建一个新分支(newbranch2),然后将你想要保留的所有提交(commit)都cherry-pick(精选)到新分支(newbranch2)中。Cherry-pick会给提交(commit)分配新的哈希值,所以现在你可以安全地变基(newbranch2),并且可以删除badnewbranch分支了。 - John Mellor
5
很遗憾,这不是被接受的答案。我按照被接受答案中的步骤操作,就像你所描述的那样,丢失了6个提交记录! - Throw Away Account
显示剩余23条评论

404

另一种只使用两个命令的方法,同时保持您当前的工作树不变。

git checkout -b newbranch # switch to a new branch
git branch -f master HEAD~3 # make master point to some older commit

旧版本 - 在我了解git branch -f之前

git checkout -b newbranch # switch to a new branch
git push . +HEAD~3:master # make master point to some older commit 

了解如何push.是一个不错的技巧。


1
当前目录。我猜这只在你在顶层目录时才有效。 - aragaer
3
@GerardSexton 目前是该机构的主管。git 可以推送到远程仓库或 Git URL。本地目录路径也支持 Git URL 语法。请参阅 git help clone 中的 GIT URL 部分。 - weakish
44
我不知道为什么这个评分不高。非常简单,且没有 git reset --hard 的小但潜在的危险。 - Godsmith
7
我猜人们更喜欢三个简单的命令,而不是两个稍微难懂的命令。此外,由于排在前面,获得最高票数的答案自然会得到更多赞成票。 - JS_Riddler
1
我同意 @Godsmith 的观点!为什么这个评分不高呢?强制主分支指向以前的提交是我在这里看到的最好的选择。谢谢 @aragaer - hasse
显示剩余12条评论

271

使用git stash的更简单解决方案

对于提交到错误分支的情况,这里提供了一个更简单的解决方案。假设当前位于分支master,而该分支有三个错误提交:

git reset HEAD~3
git stash
git checkout newbranch
git stash pop

何时使用此方法?

  • 如果您的主要目的是回滚 master
  • 您想保留文件更改
  • 您不关心错误提交上的消息
  • 您尚未推送
  • 您希望这个方法易于记忆
  • 您不希望有像暂存/新分支、查找和复制提交哈希等复杂操作

每行代码的作用

  1. 撤销最近三次提交(包括其消息),但保留所有工作文件不变
  2. 储存所有工作文件的更改,使得 master 的工作树状态恢复到 HEAD~3 时的状态
  3. 切换到现有的分支 newbranch
  4. 将储存的更改应用到您的工作目录并清除暂存

现在您可以像平常一样使用 git addgit commit。所有新的提交都将添加到 newbranch 中。

此方法不能做什么

  • 它不会让随机的临时分支混乱您的树形结构
  • 它不会保留错误提交的消息,因此您需要为这个新的提交添加一个新的提交消息
  • 更新!使用向上箭头键来滚动您的命令缓冲区,以重新应用先前提交的提交和其提交消息(感谢 @ARK)

目标

原帖作者的目标是“将 master 回退到在这些提交之前的状态”,而不会丢失更改,这个方法可以达成此目的。

当我每周一次意外地将新提交提交到 master 而不是 develop 时,我通常会使用此方法。通常情况下,如果只需要回退一个提交,则可以使用第 1 行的 git reset HEAD^ 方法更简单。

如果您已推送了 master 的更改,请不要这样做

其他人可能已经拉取了这些更改。如果您只会重写本地的 master 分支,则将其推送到上游时不影响任何内容,但是将重写的历史记录推送给协作者可能会导致问题。


7
谢谢,我很高兴我读了那么多才到这里,因为这对我来说是一个非常普遍的使用情况。难道我们是不寻常的吗? - Jim Mack
11
我认为我们是非常典型的,"哎呀我不小心提交到主分支了" 是需要撤销少量几个提交的最常见情况。幸运的是,这个解决方案非常简单,现在我已经记住了。 - Slam
14
这应该是被接受的答案。它很简单明了,易于理解和记忆。 - Sina Madani
2
如果您在CLI(命令行)历史记录中有提交消息,那么您也可以轻松地找回它们。我碰巧有我使用的git addgit commit命令,所以我只需要按上箭头并输入几次,就可以恢复所有内容了!现在,一切都回来了,但是在正确的分支上。 - Luke Gedeon
5
如果“newbranch”尚不存在,则可以使用此命令将未暂存的更改移动到新分支,而无需进行隐藏:git checkout -b newbranch - Marianna S.
显示剩余5条评论

40

另一种方法:

1.master 分支重命名为你的 newbranch(假设你当前在 master 分支上):

git branch -m newbranch

2. 从您希望的提交创建一个名为“master”的分支。
git checkout -b master <seven_char_commit_id>

3. 让`master`跟踪`origin/master`:
git branch -u origin/master master

注意

newbranch的上游将是origin/master。在进行任何更改之前,请谨慎行事并确保您完全理解解决方案。


2
喜欢这个解决方案,因为您不必重写git提交标题/描述。 - Vulpo
17
这难道不会弄乱远程上游分支吗?现在newbranch是不是指向origin/master了? - kraxor
3
这个命令是可行的,但它会让我留下一个不再追踪origin/mastermaster 副本。使用以下命令可以恢复正常:git branch -u origin/master master - Wild Pottok
该死的家伙:搞炸了我的代码库,说主分支领先了一些根本不存在的提交,主分支根本没变过。 - undefined

34
这并不是在技术上“移动”它们,但具有相同的效果:
A--B--C  (branch-foo)
 \    ^-- I wanted them here!
  \
   D--E--F--G  (branch-bar)
      ^--^--^-- Opps wrong branch!

While on branch-bar:
$ git reset --hard D # remember the SHAs for E, F, G (or E and G for a range)

A--B--C  (branch-foo)
 \
  \
   D-(E--F--G) detached
   ^-- (branch-bar)

Switch to branch-foo
$ git cherry-pick E..G

A--B--C--E'--F'--G' (branch-foo)
 \   E--F--G detached (This can be ignored)
  \ /
   D--H--I (branch-bar)

Now you won't need to worry about the detached branch because it is basically
like they are in the trash can waiting for the day it gets garbage collected.
Eventually some time in the far future it will look like:

A--B--C--E'--F'--G'--L--M--N--... (branch-foo)
 \
  \
   D--H--I--J--K--.... (branch-bar)

1
是的,在上述情况下,您可以选择在分离的分支上使用“rebase”。 - Sukima

30

如果您已经推送了提交记录,想要做到这一点而不重写历史记录:

git checkout master
git revert <commitID(s)>
git checkout -b new-branch
git cherry-pick <commitID(s)>

现在两个分支都可以被推送而不需要强制操作!


1
但是,接下来你必须处理还原场景,这取决于你的情况,可能会更加棘手。如果你在分支上还原了一个提交,Git仍然会将这些提交视为已经发生,因此为了撤消它,你必须还原还原。这让很多人感到困惑,特别是当他们还原合并并尝试将分支合并回去时,却发现Git认为它已经将该分支合并进去了(这完全是真的)。 - Makoto
1
这就是为什么我会在最后将提交的内容精选,然后放到一个新分支上。这样Git就会将它们视为新的提交,从而解决你的问题。 - teh_senaus
这比看起来更危险,因为你在没有真正理解此状态影响的情况下修改了存储库历史的状态。 - Makoto
7
我不明白你的论点- 这个答案的重点是你并没有改变历史,只是添加了新的提交(这些提交有效地撤销或重做了更改)。这些新的提交可以像平常一样推送和合并。 - teh_senaus

15

我该如何从这里开始

A - B - C - D - E 
                |
                master

转化为这个样子?

A - B - C - D - E 
    |           |
    master      newbranch

通过两个命令

  • git branch -m master newbranch

得到

A - B - C - D - E 
                |
                newbranch

并且

  • git branch master B

得到

A - B - C - D - E
    |           |
    master      newbranch

没错,这很有效且相当容易。Sourcetree GUI 对于在 git shell 中所做的更改有点困惑,但是经过一次 fetch 后,一切都恢复正常了。 - lars k.
是的,它们就像问题中所描述的那样。前几个图表旨在与问题中的图表等效,只是以我想要的方式重新绘制,以便在答案中进行说明。基本上将主分支重命名为newbranch,并在需要的位置创建一个新的主分支。 - Ivan

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