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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1628563/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-10 07:16:56  来源:igfitidea点击:

Move the most recent commit(s) to a new branch with Git

gitgit-branchbranching-and-merging

提问by Mark A. Nicolosi

I'd like to move the last several commits I've committed to master to a new branch and take master back to before those commits were made. Unfortunately, my Git-fu is not strong enough yet, any help?

我想将我已承诺掌握的最后几次提交移动到一个新分支,并在进行这些提交之前将 master 带回。不幸的是,我的 Git-fu 还不够强大,有什么帮助吗?

I.e. How can I go from this

即我怎么能从这个

master A - B - C - D - E

to this?

到这个?

newbranch     C - D - E
             /
master A - B 

回答by sykora

Moving to an existing branch

转移到现有分支

If you want to move your commits to an existing branch, it will look like this:

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

git checkout existingbranch
git merge master         # Bring the commits here
git checkout master
git reset --keep HEAD~3  # Move master back by 3 commits.
git checkout existingbranch

The --keepoption preserves any uncommitted changes that you might have in unrelated files, or aborts if those changes would have to be overwritten -- similarly to what git checkoutdoes. If it aborts, git stashyour changes and retry, or use --hardto lose the changes (even from files that didn't change between the commits!)

--keep选项保留您在不相关文件中可能具有的任何未提交的更改,或者在必须覆盖这些更改时中止 - 与所做的类似git checkout。如果它中止,git stash您的更改并重试,或用于--hard丢失更改(即使是在提交之间没有更改的文件!)

Moving to a new branch

搬到新的分行

This method works by creating a new branch with the first command (git branch newbranch) but not switching to it. Then we roll back the current branch (master) and switch to the new branch to continue working.

此方法的工作原理是使用第一个命令 ( git branch newbranch)创建一个新分支,但不切换到它。然后我们回滚当前分支(master)并切换到新分支继续工作。

git branch newbranch      # Create a new branch, containing all current commits
git reset --keep 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
# Warning: after this it's not safe to do a rebase in newbranch without extra care.

But do make sure how many commits to go back. Alternatively, instead of HEAD~3, you can simply provide the hash of the commit (or the reference like origin/master) you want to revert back to, e.g:

但是一定要确定要返回多少次提交。或者,HEAD~3您可以简单地提供origin/master要恢复的提交(或类似引用)的哈希值,而不是,例如:

git reset --keep a1b2c3d4

WARNING:With Git version 2.0 and later, if you later git rebasethe new branch upon the original (master) branch, you may need an explicit --no-fork-pointoption during the rebase to avoid losing the commits you moved from the master branch. Having branch.autosetuprebase alwaysset makes this more likely. See John Mellor's answerfor details.

警告:对于 Git 2.0 及更高版本,如果您稍后git rebase在原始 ( master) 分支上创建新分支,您可能需要--no-fork-point在变基期间使用显式选项以避免丢失从主分支移动的提交。拥有branch.autosetuprebase always一套使这个可能性比较大。有关详细信息,请参阅John Mellor 的回答

回答by Ryan Lundy

For those wondering why it works (as I was at first):

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

You want to go back to C, and move D and E to the new branch. Here's what it looks like at first:

您想返回 C,并将 D 和 E 移动到新分支。这是它最初的样子:

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

After git branch newBranch:

之后git branch newBranch

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

After git reset --hard HEAD~2:

之后git reset --hard HEAD~2

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

Since a branch is just a pointer, masterpointed to the last commit. When you made newBranch, you simply made a new pointer to the last commit. Then using git resetyou moved the masterpointer back two commits. But since you didn't move newBranch, it still points to the commit it originally did.

由于分支只是一个指针,因此master指向最后一次提交。当您创建newBranch 时,您只需创建一个指向最后一次提交的新指针。然后使用git reset您将指针移回两次提交。但是由于您没有移动newBranch,它仍然指向它最初所做的提交。

回答by Ivan

In General...

一般来说...

The method exposed by sykora is the best option in this case. But sometimes is not the easiest and it's not a general method. For a general method use git cherry-pick:

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

To achieve what OP wants, its a 2-step process:

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

Step 1 - Note which commits from master you want on a newbranch

第 1 步 - 请注意您希望从 master 提交哪些 newbranch

Execute

执行

git checkout master
git log

Note the hashes of (say 3) commits you want on newbranch. Here I shall use:
C commit: 9aa1233
D commit: 453ac3d
E commit: 612ecb3

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

Note:You can use the first seven characters or the whole commit hash

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

Step 2 - Put them on the newbranch

第 2 步 - 将它们放在 newbranch

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

OR (on Git 1.7.2+, use ranges)

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

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

git cherry-pickapplies those three commits to newbranch.

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

回答by aragaer

Yet another way to do this, using just 2 commands. Also keeps your current working tree intact.

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

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

Old version- before I learned about git branch -f

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

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

Being able to pushto .is a nice trick to know.

能够push.是一个不错的把戏就知道了。

回答by John Mellor

Most previous answers are dangerously wrong!

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

Do NOT do this:

不要这样做:

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

As the next time you run git rebase(or git pull --rebase) those 3 commits would be silently discarded from newbranch! (see explanation below)

下次您运行git rebase(或git pull --rebase)时,这 3 次提交将从newbranch! (见下面的解释)

Instead do this:

而是这样做:

git reset --keep HEAD~3
git checkout -t -b newbranch
git cherry-pick ..HEAD@{2}
  • First it discards the 3 most recent commits (--keepis like --hard, but safer, as fails rather than throw away uncommitted changes).
  • Then it forks off newbranch.
  • Then it cherry-picks those 3 commits back onto newbranch. Since they're no longer referenced by a branch, it does that by using git's reflog: HEAD@{2}is the commit that HEADused to refer to 2 operations ago, i.e. before we 1. checked out newbranchand 2. used git resetto discard the 3 commits.
  • 首先它丢弃 3 个最近的提交(--keep就像--hard,但更安全,因为失败而不是丢弃未提交的更改)。
  • 然后它分叉了newbranch
  • 然后它将这 3 个提交挑选回newbranch. 由于它们不再被分支引用,它通过使用 git 的reflog来做到这一点:HEAD@{2}HEAD用于引用前 2 个操作的提交,即在我们 1. 检出newbranch和 2. 用于git reset丢弃 3 个提交之前。

Warning: the reflog is enabled by default, but if you've manually disabled it (e.g. by using a "bare" git repository), you won't be able to get the 3 commits back after running git reset --keep HEAD~3.

警告:默认情况下会启用 reflog,但如果您手动禁用它(例如,通过使用“裸”git 存储库),您将无法在运行后恢复 3 次提交git reset --keep HEAD~3

An alternative that doesn't rely on the reflog is:

不依赖于 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

(if you prefer you can write @{-1}- the previously checked out branch - instead of oldbranch).

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



Technical explanation

技术说明

Why would git rebasediscard the 3 commits after the first example? It's because git rebasewith no arguments enables the --fork-pointoption by default, which uses the local reflog to try to be robust against the upstream branch being force-pushed.

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

Suppose you branched off origin/master when it contained commits M1, M2, M3, then made three commits yourself:

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

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

but then someone rewrites history by force-pushing origin/master to remove M2:

但是后来有人通过强制推动 origin/master 来删除 M2 来重写历史记录:

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

Using your local reflog, git rebasecan see that you forked from an earlier incarnation of the origin/master branch, and hence that the M2 and M3 commits are not really part of your topic branch. Hence it reasonably assumes that since M2 was removed from the upstream branch, you no longer want it in your topic branch either once the topic branch is rebased:

使用您的本地 reflog,git rebase可以看到您从 origin/master 分支的早期化身分叉,因此 M2 和 M3 提交实际上并不是您主题分支的一部分。因此,它合理地假设,由于 M2 已从上游分支中删除,一旦主题分支重新定位,您就不再需要它在您的主题分支中:

M1--M3'  <-- origin/master
     \
      T1'--T2'--T3'  <-- topic (rebased)

This behavior makes sense, and is generally the right thing to do when rebasing.

这种行为是有道理的,并且通常是在变基时正确的做法。

So the reason that the following commands fail:

所以以下命令失败的原因:

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

is because they leave the reflog in the wrong state. Git sees newbranchas having forked off the upstream branch at a revision that includes the 3 commits, then the reset --hardrewrites the upstream's history to remove the commits, and so next time you run git rebaseit discards them like any other commit that has been removed from the upstream.

是因为他们让 reflog 处于错误的状态。Git 认为newbranch在包含 3 次提交的修订版中分叉了上游分支,然后reset --hard重写上游的历史记录以删除提交,因此下次运行时,git rebase它会像从上游删除的任何其他提交一样丢弃它们。

But in this particular case we want those 3 commits to be considered as part of the topic branch. To achieve that, we need to fork off the upstream at the earlier revision that doesn't include the 3 commits. That's what my suggested solutions do, hence they both leave the reflog in the correct state.

但是在这种特殊情况下,我们希望将这 3 个提交视为主题分支的一部分。为了实现这一点,我们需要在不包括 3 次提交的早期版本中分叉上游。这就是我建议的解决方案所做的,因此它们都使 reflog 处于正确的状态。

For more details, see the definition of --fork-pointin the git rebaseand git merge-basedocs.

有关详细信息,请参阅定义--fork-pointgit的底垫混帐合并基础文档。

回答by Slam

Much simpler solution using git stash

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

Here's a far simpler solution for commits to the wrong branch. Starting on branch masterthat has three mistaken commits:

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

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

When to use this?

什么时候用这个?

  • If your primary purpose is to roll back master
  • You want to keep file changes
  • You don't care about the messages on the mistaken commits
  • You haven't pushed yet
  • You want this to be easy to memorize
  • You don't want complications like temporary/new branches, finding and copying commit hashes, and other headaches
  • 如果您的主要目的是回滚 master
  • 您想保留文件更改
  • 你不关心错误提交的消息
  • 你还没推
  • 你希望这很容易记住
  • 您不希望出现临时/新分支、查找和复制提交哈希以及其他令人头疼的问题

What this does, by line number

这是做什么的,按行号

  1. Undoes the last three commits (and their messages) to master, yet leaves all working files intact
  2. Stashes away all the working file changes, making the masterworking tree exactly equal to the HEAD~3 state
  3. Switches to an existing branch newbranch
  4. Applies the stashed changes to your working directory and clears the stash
  1. 撤消对 的最后三个提交(及其消息)master,但保持所有工作文件完好无损
  2. 隐藏所有工作文件更改,使master工作树完全等于 HEAD~3 状态
  3. 切换到现有分支 newbranch
  4. 将隐藏的更改应用到您的工作目录并清除存储

You can now use git addand git commitas you normally would. All new commits will be added to newbranch.

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

What this doesn't do

这不做什么

  • It doesn't leave random temporary branches cluttering your tree
  • It doesn't preserve the mistaken commit messages,so you'll need to add a new commit message to this new commit
  • Update! Use up-arrow to scroll through your command buffer to reapply the prior commit with its commit message (thanks @ARK)
  • 它不会让随机的临时树枝弄乱你的树
  • 它不会保留错误的提交消息,因此您需要向此新提交添加新的提交消息
  • 更新!使用向上箭头滚动命令缓冲区以重新应用先前的提交及其提交消息(感谢@ARK)

Goals

目标

The OP stated the goal was to "take master back to before those commits were made" without losing changes and this solution does that.

OP 表示目标是“在做出这些提交之前将 master 带回”而不会丢失更改,而这个解决方案就是这样做的。

I do this at least once a week when I accidentally make new commits to masterinstead of develop. Usually I have only one commit to rollback in which case using git reset HEAD^on line 1 is a simpler way to rollback just one commit.

当我不小心对master而不是develop. 通常我只有一次回滚提交,在这种情况下,git reset HEAD^在第 1 行使用是回滚一次提交的一种更简单的方法。

Don't do this if you pushed master's changes upstream

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

Someone else may have pulled those changes. If you are only rewriting your local master there's no impact when it's pushed upstream, but pushing a rewritten history to collaborators can cause headaches.

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

回答by Sukima

This doesn't "move" them in the technical sense but it has the same effect:

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

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)

回答by teh_senaus

To do this without rewriting history (i.e. if you've already pushed the commits):

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

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

Both branches can then be pushed without force!

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

回答by Darkglow

Had just this situation:

刚好遇到这样的情况:

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

I performed:

我表现:

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

I expected that commit I would be the HEAD, but commit L is it now...

我希望提交 I 将成为 HEAD,但现在提交 L 是...

To be sure to land on the right spot in the history its easier to work with the hash of the commit

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

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

回答by Ivan

How can I go from this

我怎样才能离开这里

A - B - C - D - E 
                |
                master

to this?

到这个?

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

With two commands

用两个命令

  • git branch -m master newbranch
  • git branch -m master newbranch

giving

给予

A - B - C - D - E 
                |
                newbranch

and

  • git branch master B
  • git 分支主 B

giving

给予

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