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

git git-branch branching-and-merging

如何将我最近在 master 上的提交移动到新分支,并将 master 重置为在提交之前?例如从这里:

master A - B - C - D - E

对此:

newbranch     C - D - E
             /
master A - B 

注意:我问了相反的问题 here

eddmann.com/posts/… 这个有效

这里的评论被清除了吗?我问是因为在我每两个月访问这个问题时,我总是滚动浏览该评论。

旁注:这个问题是关于一个非常简单的案例。在答案之后(可能为时已晚)阅读答案和所有“不要这样做,因为......”和“更好的解决方案是......”和“警告版本 n+......”,它在我看来,即使非常简单的操作在 git 中也没有直接的解决方案。一个图形版本管理器,您只需为新分支添加一个标签,而无需处理在我看来晦涩和过时的语法,这将是一种解脱。我的王国和我的金徽章给第一个“分叉” git 并开始新方法的人 ;-) 这很紧急。

请务必通读前十个答案(左右),因为最好的不是最受支持的。

2
25 revs, 23 users 17%

移动到现有分支

如果您想将提交移动到现有分支,它将如下所示:

git checkout existingbranch
git merge master
git checkout master
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 之前将更改合并到现有分支(请参阅移动到现有分支< /em> 上面)。 如果您不先合并更改,它们将会丢失。

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

# 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,而只需在 masterorigin/master 的引用) > (/current) 分支,例如:

git reset --hard a1b2c3d4

*1 你只会从主分支“丢失”提交,但别担心,你会在新分支中拥有这些提交!

最后,you may need 强制将您的最新更改推送到主存储库:

git push origin master --force

警告: 对于 Git 2.0 及更高版本,如果您稍后在原始 (master) 分支上创建新分支 git rebase,则在变基期间可能需要显式 --no-fork-point 选项以避免丢失结转的提交。设置 branch.autosetuprebase always 使这更有可能。有关详细信息,请参阅 John Mellor's answer

尤其是,不要试图返回比您上次将提交推送到其他人可能已从中提取的另一个存储库的点更远的地方。

想知道您是否可以解释为什么会这样。对我来说,您正在创建一个新分支,从您仍在使用的旧分支中删除 3 个提交,然后检查您创建的分支。那么你删除的提交是如何神奇地出现在新分支中的呢?

@Jonathan Dumaine:因为我在从旧分支中删除提交之前创建了新分支。他们还在新的分店里。

git 中的分支只是指向历史提交的标记,没有任何内容被克隆、创建或删除(标记除外)

另请注意:不要对工作副本中未提交的更改执行此操作!这只是咬我! :(

R
Ryan Lundy

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

您想回到 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 resetmaster 指针移回两次提交。但是由于您没有移动 newBranch,它仍然指向它最初所做的提交。

我还需要执行 git push origin master --force 以使更改显示在主存储库中。

这个答案会导致提交丢失:下次您 git rebase 时,这 3 个提交将从 newbranch 中默默地丢弃。有关详细信息和更安全的替代方案,请参阅 my answer

@John,那是胡说八道。在不知道自己在做什么的情况下进行变基会导致提交丢失。如果您丢失了提交,我很抱歉,但这个答案并没有丢失您的提交。请注意,上图中没有出现 origin/master。如果您推送到 origin/master 然后进行上述更改,当然,事情会变得有趣。但这是一个“医生,我这样做的时候很痛”之类的问题。这超出了原始问题的范围。我建议您编写自己的问题来探索您的场景,而不是劫持这个问题。

@John,在您的回答中,您说“不要这样做!git branch -t newbranch”。回去再读一遍答案。 没有人建议这样做。

@Kyralessa,当然,但是如果您查看问题中的图表,很明显他们希望 newbranch 基于他们现有的本地 master 分支。执行接受的答案后,当用户开始在 newbranch 中运行 git rebase 时,git 会提醒他们忘记设置上游分支,因此他们将运行 git branch --set-upstream-to=master 然后 git rebase 并具有相同的问题。他们不妨一开始就使用 git branch -t newbranch

I
Ivan

一般来说...

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

为了实现 OP 想要的,它有两个步骤:

第 1 步 - 记下您希望在新分支上从 master 提交的哪些提交

执行

git checkout master
git log

请注意您想要在 newbranch 上的(比如 3 个)提交的哈希值。在这里我将使用:
C 提交:9aa1233
D 提交:453ac3d
E 提交:612ecb3

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

第 2 步 - 将它们放在新分支上

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

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

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

git cherry-pick 将这三个提交应用于 newbranch。

如果您不小心提交了错误的非 master 分支,这将非常有效,此时您应该创建一个新的功能分支。

git cherry-pick 上的信息很好,但是这篇文章中的命令不起作用。 1) 'git checkout newbranch' 应该是 'git checkout -b newbranch' 因为 newbranch 尚不存在; 2)如果您从现有的 master 分支签出 newbranch ,它已经包含了这三个提交,因此选择它们没有用。归根结底,要获得 OP 想要的东西,您仍然需要进行某种形式的重置 --hard HEAD。

+1 在某些情况下是一种有用的方法。如果您只想将自己的提交(与其他提交)拉入新分支,这很好。

这是更好的答案。这样你就可以将提交移动到任何分支。

我无法使用cherry-pick范围,在其中一个提交上得到“错误:无法应用c682e4b ...”,但是当一次对每个提交使用cherry-pick时,它工作正常(我有在错误发生后执行 git cherry-pick --abort)。评论是因为我认为那些是等价的。这是 git 版本 2.3.8 (Apple Git-58)

J
John Mellor

大多数以前的答案都是危险的错误!

不要这样做:

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,但更安全,因为失败而不是丢弃未提交的更改)。

然后它分叉新分支。

然后它将这 3 个提交挑选回新分支。由于它们不再被分支引用,它通过使用 git 的 reflog 来做到这一点: HEAD@{2} 是 HEAD 之前用于引用 2 个操作的提交,即在我们 1. 签出 newbranch 和 2. 使用 git 之前重置以丢弃 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)。

技术说明

为什么 git rebase 会在第一个示例之后丢弃 3 次提交?这是因为不带参数的 git rebase 默认启用 --fork-point 选项,该选项使用本地 reflog 来尝试对上游分支被强制推送的鲁棒性。

假设您在 origin/master 包含提交 M1、M2、M3 时分支了它,然后您自己进行了三个提交:

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

但随后有人通过强制推送源/主来删除 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 定义。

这个答案说“不要这样做!”上面没有人建议做的事情。

大多数人不会重写已发布的历史记录,尤其是在 master。所以不,他们没有危险的错误。

@Kyralessa,如果您设置了 git config --global branch.autosetuprebase always,您在 git branch 中引用的 -t 会隐式发生。即使您不这样做,我already explained告诉您,如果您在执行这些命令后设置跟踪,也会出现同样的问题,因为 OP 可能打算根据他们的问题这样做。

@RockLee,是的,解决这种情况的一般方法是从安全的起点创建一个新的分支(newbranch2),然后挑选您想要保留的所有提交(从 badnewbranch 到 newbranch2)。 Cherry-picking 将为提交提供新的哈希值,因此您将能够安全地重新设置 newbranch2(并且现在可以删除 badnewbranch)。

太糟糕了,这不是公认的答案。正如您所描述的,我按照接受的答案中的步骤操作并丢失了 6 次提交!

a
aragaer

还有另一种方法,只使用 2 个命令。还可以保持您当前的工作树完好无损。

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. 是一个很好的技巧。

当前目录。我想这只有在您位于顶级目录中时才有效。

本地推送很有趣,但仔细想想,这里的 git branch -f 有什么不同?

@GerardSexton . 是现任董事。 git 可以推送到 REMOTES 或 GIT URL。 path to local directory 是受支持的 Git URL 语法。请参阅 git help clone 中的 GIT URLS 部分。

我不知道为什么这个评分不高。非常简单,并且没有 git reset --hard 的微小但潜在的危险。

@Godsmith 我的猜测是人们更喜欢三个简单的命令而不是两个稍微晦涩的命令。此外,投票最多的答案由于首先显示的性质而获得更多的赞成票。

S
Slam

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

这是提交到错误分支的更简单的解决方案。从具有三个错误提交的分支 master 开始:

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

什么时候用这个?

如果您的主要目的是回滚 master

您想保留文件更改

你不关心错误提交的消息

你还没有推送

你希望这很容易记住

您不希望出现临时/新分支、查找和复制提交哈希以及其他令人头疼的问题

这是做什么的,按行号

撤消对 master 的最后三个提交(及其消息),但保留所有工作文件不变 隐藏所有工作文件更改,使 master 工作树完全等于 HEAD~3 状态 切换到现有分支 newbranch 应用隐藏的更改到您的工作目录并清除存储

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

这不做什么

它不会让随机的临时树枝把你的树弄得乱七八糟

它不会保留错误的提交消息,因此您需要向此新提交添加新的提交消息

更新!使用向上箭头滚动命令缓冲区以重新应用先前的提交及其提交消息(感谢@ARK)

目标

OP 表示目标是在不丢失更改的情况下“让 master 回到提交之前”,而这个解决方案就是这样做的。

当我不小心向 master 而不是 develop 提交新的提交时,我每周至少会这样做一次。通常我只有一个提交回滚,在这种情况下,在第 1 行使用 git reset HEAD^ 是一种更简单的方法来回滚一个提交。

如果您将 master 的更改推送到上游,请不要这样做

其他人可能已经取消了这些更改。如果你只是重写你的本地master,那么当它被推送到上游时没有影响,但是将重写的历史推送给合作者可能会让人头疼。

谢谢,很高兴我读了很多过去/通读了很多东西才能到达这里,因为这对我来说也是一个很常见的用例。我们有那么不典型吗?

我认为我们是完全典型的,“我错误地承诺掌握的oops”是需要恢复少数或更少提交的最常见用例。幸运的是,这个解决方案非常简单,我现在已经记住了。

这应该是公认的答案。它简单明了,易于理解且易于记忆

如果您碰巧在您的 CLI(命令行)历史记录中有提交消息,您也可以轻松地取回提交消息。我碰巧有我使用的 git addgit commit 命令,所以我所要做的就是按向上箭头并输入几次然后砰!一切都回来了,但现在在正确的分支上。

如果“newbranch”还不存在,那么您可以在重置后将未暂存的更改移动到新分支,使用此命令无需存储:git checkout -b newbranch

h
hamdiakoguz

从技术意义上讲,这不会“移动”它们,但具有相同的效果:

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)

是的,您也可以在上述场景中的分离分支上使用 rebase

t
teh_senaus

要在不重写历史记录的情况下执行此操作(即,如果您已经推送了提交):

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

然后可以不用力推动两个分支!

但是随后您必须处理还原情况,根据您的情况,这可能会更加棘手。如果您恢复分支上的提交,Git 仍然会看到这些提交已经发生,因此为了撤消它,您必须恢复恢复。这让很多人感到痛苦,特别是当他们恢复合并并尝试将分支合并回来时,却发现 Git 认为它已经合并了该分支(这是完全正确的)。

这就是为什么我在最后挑选提交,到一个新的分支上。这样 git 将它们视为新提交,从而解决了您的问题。

这比最初看起来更危险,因为您在没有真正了解此状态的含义的情况下更改存储库历史的状态。

我不遵循您的论点-此答案的重点是您没有更改历史记录,而只是添加了新提交(这有效地撤消了重做更改)。这些新提交可以正常推送和合并。

S
Saikat

最简单的方法:

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

git branch -m newbranch

2. 从您希望的提交创建 master 分支:

git checkout -b master <seven_char_commit_id>

例如 git checkout -b master a34bc22

注意: newbranch 的上游是 origin/master

喜欢这个解决方案,因为您不必重写 git commit 标题/描述。

这不会弄乱远程上游分支吗? newbranch 现在不是指向 origin/master 吗?

D
Darkglow

刚遇到这种情况:

Branch one: A B C D E F     J   L M  
                       \ (Merge)
Branch two:             G I   K     N

我表演了:

git branch newbranch 
git reset --hard HEAD~8 
git checkout newbranch

我原以为我会成为 HEAD 的提交,但现在是提交 L ......

为了确保在历史记录中的正确位置,使用提交的哈希更容易

git branch newbranch 
git reset --hard #########
git checkout newbranch
I
Ivan

我怎么能从这个

A - B - C - D - E 
                |
                master

到这个?

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

有两个命令

git 分支 -m 主新分支

给予

A - B - C - D - E 
                |
                newbranch

git 分支主 B

给予

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

是的,这很有效,而且很容易。 Sourcetree GUI 对 git shell 中所做的更改有点困惑,但在 fetch 之后又好了。

是的,他们就像问题一样。前几个图表旨在与问题中的图表等效,只是为了在答案中进行说明而按照我想要的方式重新绘制。基本上将主分支重命名为 newbranch 并在您想要的位置创建一个新的主分支。

S
Shamsul Arefin Sajib

如果您只需要将所有未推送的提交移动到新分支,那么您只需要,

从当前分支创建一个新分支:git branch new-branch-name 推送您的新分支:git push origin new-branch-name 将您的旧(当前)分支恢复到上次推送/稳定状态:git reset --hard origin /旧分行名称

有些人还有其他 upstreams 而不是 origin,他们应该使用适当的 upstream

p
pankaj

你可以做到这只是我使用的 3 个简单步骤。

1)在您要提交最近更新的地方创建新分支。

git branch <branch name>

2) 查找最近提交 ID 以在新分支上提交。

git log

3)复制该提交ID,注意最近提交列表位于顶部。所以你可以找到你的提交。你也可以通过消息找到这个。

git cherry-pick d34bcef232f6c...

您还可以提供一些提交 ID。

git cherry-pick d34bcef...86d2aec

现在你的工作完成了。如果您选择了正确的 id 和正确的分支,那么您将成功。所以在做这件事之前要小心。否则可能会出现另一个问题。

现在你可以推送你的代码了

git push

Q
Quesofat

这里的大多数解决方案都会计算您想要返回的提交数量。我认为这是一种容易出错的方法。计数需要重新计数。

您可以简单地传递您希望在 HEAD 的提交的提交哈希,或者换句话说,您希望通过以下方式成为最后一次提交的提交:

(注意见提交哈希)

为了避免这种情况:

1) git checkout master

2) git branch <feature branch> master

3) git reset --hard <commit hash>

4) git push -f origin master
m
moestly

TLDR

git checkout branch_to_remove_commits
git reset --hard ${hash_of_new_tip}
git checkout -b branch_to_store_commits
# Move commits (single hash, list of hashes or range ffaa..ffoo) 
git cherry-pick ${commit_hash}
git push --set-upstream origin branch_to_store_commits
# Switch back to last branch
git checkout -
git push -f

为了我

git log --pretty=oneline -n ${NUMBER}

最适合识别有问题的提交哈希。

r
rashok

1) 创建一个新分支,将所有更改移动到 new_branch。

git checkout -b new_branch

2)然后回到旧分支。

git checkout master

3)做 git rebase

git rebase -i <short-hash-of-B-commit>

4)然后打开的编辑器包含最后3个提交信息。

...
pick <C's hash> C
pick <D's hash> D
pick <E's hash> E
...

5) 在所有这 3 个提交中将 pick 更改为 drop。然后保存并关闭编辑器。

...
drop <C's hash> C
drop <D's hash> D
drop <E's hash> E
...

6) 现在从当前分支 (master) 中删除最后 3 个提交。现在强制推送分支,在分支名称前加上 + 符号。

git push origin +master
D
David Chelidze

我很惊讶没有人推荐这种方式:

git checkout master
git checkout <commit hash from which you want to split>
git checkout -b new_branch
git rebase master
git checkout master
git reset --hard <commit hash you splitted>

解释:

步骤我们检查我们想要拆分的提交,然后从这个提交创建一个新的分支,做 rebase 将同步 new_branch 和 master。所以现在我们有两个相同的分支,具有相同的提交并在 master 上重置,我们在拆分列表项后清理最后一次提交

这种方式只创建一个分支,其中第一个提交从 Main/Master 更改,但忘记了其余部分。

k
kokoro

使用 Emacs 的 git瓷器 Magit,您只需点击 b s (magit-branch-spinoff) 即可完成此操作。系统会要求您输入新分支的名称,然后按 Enter 键,瞧。

Magit documentation

此命令创建并签出一个从当前分支开始并跟踪的新分支。该分支又被重置为它与上游共享的最后一个提交。如果当前分支没有上游或未推送的提交,则无论如何都会创建新分支,并且不会触及先前的当前分支。这对于在旧分支(可能但不一定是“主”)上已经开始工作后创建功能分支很有用。

R
Raphael Fernandes

我必须将 7 个提交从一个旧分支移动到一个新分支。

git checkout old-branch     # in the example, master
git reset --hard h4sh       # h4sh is the hash for the commit 
git checkout -b new-branch  
git push origin new-branch

之后,这两个分支都与我所做的 7 次提交有关。在 git checkout new-branch 之后,git loggit status 都很好,但是,当访问旧分支 (git checkout old-branch) 时,我收到消息“git 落后 7 次提交并且可以快进”。对我有用的删除此消息的是以下内容:

git checkout old-branch
git status 
> git is behind by 7 commits and can be fast-forwarded
git push origin old-branch -f

在该步骤之后,最后 7 个提交仅被引用为新分支,而之前的提交在 Bitbucket 树中被引用为旧分支和新分支。

b
bunjeeb

如果您是像我这样的 UI 人员并且您正在使用 Visual Studio。然后您可以执行以下操作:就我而言,我想将最新的提交提交到另一个分支。

右键单击之前的一个(提交)。

https://i.stack.imgur.com/2tz8w.png

因此,所有提交更改都将显示在 Git 更改窗格中。现在,隐藏您的更改

https://i.stack.imgur.com/UJxJg.png

转到您的目标分支或从右下角创建一个新分支。

https://i.stack.imgur.com/LnwZO.png

在“Git Changes”中双击你最新的 Stash。 “存储详细信息”窗格将打开。单击“Pop”,然后解决冲突(如果存在)。

https://i.stack.imgur.com/uEGUU.png

最后,提交您的更改。

c
cs94njw

从其他帖子中获取一些想法,避免与重置有关,并且极度偏执,我的解决方案是:

git branch # 更改在新分支中可用 git push # 上传,您可能需要弄乱“--set-upstream”,例如 git push --set-upstream https:/// 通过 a 检查新分支是否在 git GUI 销毁当前目录从 git 存储库重新克隆

我并不骄傲,但我保留了我的数据;)