使用 Git 将最新提交移至新分支

我想将我已承诺掌握的最后几个提交移动到新的分支,并在做出这些提交之前将 master 重新带回去。不幸的是,我的 Git-fu 还不够坚固,有什么帮助吗?

即我该如何去

master A - B - C - D - E

对此吗?

newbranch     C - D - E
             /
master A - B

答案

移至现有分支

如果要将提交移至现有分支 ,则它将如下所示:

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

--keep选项保留您可能在不相关的文件中进行的所有未提交的更改,或者如果必须将这些更改覆盖而中止,则中止 - 与git checkout相似。如果异常终止, git stash您的更改--hard试,或使用--hard来丢失更改(即使来自提交之间未更改的文件!)

移至新分支

此方法通过使用第一个命令( git branch newbranch )创建一个新分支但不切换到该git branch newbranch 。然后,我们回滚当前分支(主节点)并切换到新分支以继续工作。

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.

但是,请确保要返回多少次提交。或者,代替HEAD~3 ,可以简单地提供的提交的哈希(或类似的参考origin/master )要恢复到,例如:

git reset --keep a1b2c3d4

警告:在 Git 2.0 版及更高版本中,如果稍后git rebase将新分支重新设置为原始( master )分支,则在重新设置过程中可能需要显式的--no-fork-point选项,以避免丢失从主分支。 branch.autosetuprebase always设置branch.autosetuprebase always使其更有可能。有关详细信息,请参见John Mellor 的答案

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

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

一般来说...

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

为了实现 OP 想要的功能,它需要两个步骤:

第 1 步 - 记下您希望在新newbranch上提交的主提交

执行

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

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

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

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

另一种方法,仅使用 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 -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 的reflog来做到这一点: HEAD@{2}HEAD以前引用 2 个操作的提交,即在我们 1. 检出newbranch和 2. 使用git reset以丢弃 3 次提交。

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

不依赖引用日志的替代方法是:

# 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 尝试对强行推入的上游分支保持鲁棒性。

假设您在包含提交 M1,M2,M3 的情况下分支了 Origin / master,然后自己进行了三个提交:

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定义。

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

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

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

什么时候使用?

  • 如果您的主要目的是回滚master
  • 您想保留文件更改
  • 您不在乎有关错误提交的消息
  • 你还没推
  • 您希望这容易记住
  • 您不希望出现诸如临时 / 新分支,查找和复制提交散列以及其他麻烦之类的复杂情况

这是做什么的,按行号

  1. 撤消对master的最后三次提交(及其消息),但所有工作文件保持不变
  2. 整理所有工作文件的更改,使master工作树与 HEAD〜3 状态完全相等
  3. 切换到现有分支newbranch
  4. 将隐藏的更改应用于工作目录并清除隐藏

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

这不行

  • 它不会使随机的临时树枝杂乱无章
  • 它不会保留错误的提交消息,因此您需要向此新提交添加新的提交消息。

目标

OP 指出,目标是 “在做出这些提交之前将主服务器重新带回” 而不会丢失更改,而此解决方案可以做到这一点。

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

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

其他人可能已经撤消了这些更改。如果仅重写本地母版,将其推入上游不会有任何影响,但是将重写的历史记录推向协作者可能会引起头痛。

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

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)

为此,无需重写历史记录(例如,如果您已经推送了提交):

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

然后可以不加力地推开两个分支!

只是这种情况:

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

我该如何去

A - B - C - D - E 
                |
                master

对此吗?

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

用两个命令

  • git branch -m 主 newbranch

给予

A - B - C - D - E 
                |
                newbranch

  • git 分支主管 B

给予

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