将之前的一次提交拆分为多个提交

1758

在不创建分支以及在新分支上进行大量工作的情况下,是否有可能在将提交保存到本地仓库后将单个提交分成几个不同的提交?


68
学习如何进行此操作的好资料是 Pro Git §6.4 Git 工具 - 重写历史,其中包括“拆分提交”一节。 - user456814
7
上面评论中链接的文档非常好,比下面的回答更好地解释了问题。 - Blaisorblade
3
我建议使用这个别名 https://dev59.com/Smct5IYBdhLWcg3wCZMT#19267103。它允许使用 git autorebase split COMMIT_ID 命令来拆分提交。 - Jérôme Pouiller
2
没有交互式变基最简单的方法可能是从要拆分的提交之前的提交开始创建一个新分支,cherry-pick -n提交,重置,stash,提交文件移动,重新应用stash并提交更改,然后将其与以前的分支合并或cherry-pick后续提交。 (然后将以前的分支名称切换为当前头。)(最好遵循MBO的建议进行交互式变基。)(摘自下面的2010年答案) - William Pursell
3
我在之前的提交中,在rebase期间意外压缩了两个提交,导致出现了问题。我的解决方法是检出被压缩的提交,然后执行git reset HEAD~git stash,接着使用git cherry-pick来挑选压缩中的第一个提交,最后再执行git stash pop。虽然我的cherry-pick情况比较特殊,但是git stashgit stash pop对于其他情况也非常方便。 - SOFe
21个回答

2440

git rebase -i可以实现这一功能。

首先,从一个干净的工作目录开始:执行git status应该不会显示待处理的修改、删除或添加。

现在,您需要决定要拆分哪个提交。

A)拆分最近的提交

要拆分最近的提交,请执行以下操作:

$ git reset HEAD~

现在按照通常的方式逐个提交碎片,根据需要进行多次提交。

B) 进一步分割较早的提交

这需要进行 变基(rebase),也就是重写历史。为了指定正确的提交,你有几个选择:

  • 如果它是三个提交之前,那么

  $ git rebase -i HEAD~3

这里的3是指回退了多少个提交。

  • 如果它在你要计算的范围之外,那么

      $ git rebase -i 123abcd~
    

    其中123abcd是你想要拆分的提交的SHA1值。

  • 如果你在一个不同的分支(比如特性分支),想要合并到 master 分支:

  •   $ git rebase -i master
    

    当你进入rebase编辑界面时,找到你想要拆分的提交。在那一行的开头,用edit (e简写)替换pick。保存缓冲区并退出。现在,rebase就会停在你想要编辑的提交之后。然后进行以下操作:

    $ git reset HEAD~
    

    按照通常的方式逐个提交代码片段,根据需求生成尽可能多的提交记录。

    最后。

    $ git rebase --continue
    

    28
    manojlds的答案中实际上有这个链接到git-scm文档的内容,它也清楚地解释了拆分提交的过程。 - user456814
    89
    你还应该尝试使用 git add -p 命令来部分添加文件,如果需要只提交部分内容的差异,则可以在命令中加入 e 选项进行编辑。如果你想要保留一些工作但又不想包含在当前的提交中,那么 git stash 命令也非常有用。 - Craig Ringer
    4
    如果你想拆分并重新排序提交,我喜欢先进行拆分,然后再使用另一个 git rebase -i HEAD^3 命令单独重新排序。这样,如果拆分出现问题,你就不必撤销太多的工作。 - David M. Lloyd
    3
    @CraigRinger,比 git add -p 更好的是 git add -i,但似乎不太为人所知。 - Phillipp
    7
    在执行 git reset HEAD~ 后,最新提交到 HEAD 中的文件将会保留在磁盘上,它们并不会丢失。我的翻译是否符合您的要求呢? - Wayne Conrad
    显示剩余30条评论

    407

    来自 git-rebase 手册(SPLITTING COMMITS 章节)

    在交互模式下,您可以使用“edit”操作标记提交。然而,这并不一定意味着 git rebase 期望此编辑的结果恰好是一个提交。实际上,您可以撤销该提交,也可以添加其他提交。这可用于将提交拆分为两个:

    • 使用命令 git rebase -i <commit>^ 开始一个交互式 rebase,其中 <commit> 是您要拆分的提交。实际上,只要包含该提交的任何提交范围都可以。

    • 使用“edit”操作标记要拆分的提交。

    • 当编辑该提交时,执行 git reset HEAD^ 命令。其效果是将 HEAD 向后移动一个提交,索引跟随移动,但工作树保持不变。

    • 现在将您想要包含在第一个提交中的更改添加到索引中。您可以使用 git add(可能是交互式的)或 git gui(或两者都使用)来完成此操作。

    • 以适当的提交消息提交当前索引。

    • 重复最后两个步骤,直到您的工作树保持干净。

    • 使用 git rebase --continue 继续 rebase 过程。


    20
    在Windows中,你需要使用~代替^ - Kevin Kuszyk
    26
    警告:采用这种方法会丢失提交信息。 - user420667
    20
    是的,当然。毕竟我们正在“重置”提交,消息也包括在内。如果你知道你将要分割一个提交但想要保留部分或全部的消息,明智的做法是复制一份该消息。因此,在进行“变基(rebase)”之前使用git show查看提交,或者如果你忘记了或更喜欢这样做:稍后通过“引用日志(reflog)”返回到它。直到两周后或其他时间垃圾回收将其清除之前,其中任何内容都不会真正被“丢失”。 - underscore_d
    13
    在Windows上,~^是不同的东西。你仍然想要使用插入符号^,因此你需要根据你的Shell适当地对其进行转义。在PowerShell中,它是HEAD`^。在cmd.exe中,你可以将其加倍来进行转义,像这样:HEAD^^。在大多数(全部?)的Shell中,你可以用引号括起来,如:"HEAD^"。 - AndrewF
    27
    您还可以使用 git commit --reuse-message=abcd123 命令。它的短选项是 -C - j0057
    显示剩余3条评论

    80

    之前的回答已经讲解了如何使用git rebase -i来编辑要拆分的提交,并将其分成不同的部分提交。

    当你想要将更改拆分为单独的文件时,这种方法很有效,但你需要知道更多细节。

    在到达要拆分的提交后,使用rebase -i并标记其为edit后,你有两个选择:

    1. 使用git reset HEAD~,逐个检查补丁,并使用git add -p选择每个提交中所需的补丁。

    2. 编辑工作副本以删除不想要的更改;提交中间状态;然后为下一轮拉取完整提交。

    如果你正在拆分一个大型提交,则选项2很有用,因为它可以让你检查中间版本是否能够顺利构建和运行。具体步骤如下:

    在使用rebase -i并进行edit操作后,执行以下操作:

    git reset --soft HEAD~
    

    要撤销提交但保留索引中的已提交文件,您可以进行软重置(使用--soft选项)。如果您的初始提交离最终结果还很远,则可以省略--soft进行混合重置。唯一的区别是您是从所有更改都已暂存开始还是从它们全部未暂存开始。

    现在进入编辑代码阶段。您可以删除更改,删除添加的文件,并做任何您想要构建所寻找的系列的第一个提交的操作。您还可以构建并运行它,并确认您拥有一组一致的源代码。

    一旦您满意了,根据需要对文件进行暂存/取消暂存(我喜欢使用git gui),然后通过UI或命令行提交更改。

    git commit
    

    第一个提交已完成。现在您想将工作副本恢复到分割后的提交之后的状态,以便可以将更多的更改用于下一次提交。要找到您正在编辑的提交的sha1,请使用git status。在状态的前几行中,您将看到当前正在执行的变基命令,在其中可以找到原始提交的sha1:

    $ git status
    interactive rebase in progress; onto be83b41
    Last commands done (3 commands done):
       pick 4847406 US135756: add debugging to the file download code
       e 65dfb6a US135756: write data and download from remote
      (see more in file .git/rebase-merge/done)
    ...
    
    在这种情况下,我正在编辑的提交具有SHA1 65dfb6a。知道了这一点,我可以使用git checkout命令的形式,在我的工作目录中检出该提交的内容,该命令需要同时指定提交和文件位置。在这里,我使用.作为文件位置来替换整个工作副本:
    git checkout 65dfb6a .
    

    别忘了句号!

    这将检出和暂存文件,它们与你正在编辑的提交之后的文件相同,但相对于你之前所做的提交,因此你已经提交的任何更改都不会成为该提交的一部分。

    现在,您可以直接提交它以完成分割,或者再次循环,删除某些部分的提交,然后再进行临时提交。

    如果您想要为一个或多个提交重用原始提交消息,则可以直接从rebase的工作文件中使用它:

    git commit --file .git/rebase-merge/message
    

    最后,一旦您提交了所有更改,

    git rebase --continue
    

    会继续进行并完成变基操作。


    4
    谢谢!这应该是被接受的答案。如果早些看到它,就可以节省我今天很多时间和痛苦了。这是唯一一个在最终提交的结果将您带回编辑提交相同状态的答案。 - Doug Coburn
    3
    我喜欢你使用原始提交信息的方式。 - Salamandar
    使用选项2时,当我执行git checkout *正在编辑的Sha* .时,它总是显示从*不在Git日志中的某个Sha*更新了0个路径,并且没有任何更改。 - Noumenon

    50

    使用 git rebase --interactive 来编辑先前的提交,运行 git reset HEAD~,然后运行git add -p添加一些内容,之后进行一次提交,再添加更多内容并进行另一次提交,这样可以任意多次。完成后,运行 git rebase --continue,你会发现所有拆分的提交都在你的堆栈中。

    重要提示:请注意,您可以随意更改并不必担心丢失旧更改,因为您始终可以运行 git reflog 找到包含所需更改(假设我们将其称为 a8c4ab)的项目点,然后运行 git reset a8c4ab

    以下是一系列命令以展示它的用法:

    mkdir git-test; cd git-test; git init

    现在添加一个文件 A

    vi A

    添加这一行:

    one

    git commit -am one

    接着在A中添加下面这一行:

    two

    git commit -am two

    然后在A中添加下面这一行:

    three

    git commit -am three

    现在,文件A看起来像这样:

    one
    two
    three
    

    我们的 git log 看起来像下面这样(好吧,我使用 git log --pretty=oneline --pretty="%h %cn %cr ---- %s"

    bfb8e46 Rose Perrone 4 seconds ago ---- three
    2b613bc Rose Perrone 14 seconds ago ---- two
    9aac58f Rose Perrone 24 seconds ago ---- one
    

    假设我们想要拆分第二次提交two

    git rebase --interactive HEAD~2

    这会出现一个类似于下面的消息:

    pick 2b613bc two
    pick bfb8e46 three
    

    将第一个 pick 改为 e 来编辑该提交。

    git reset HEAD~

    git diff 显示我们刚才取消了我们为第二个提交所做的提交:

    diff --git a/A b/A
    index 5626abf..814f4a4 100644
    --- a/A
    +++ b/A
    @@ -1 +1,2 @@
     one
    +two
    

    让我们执行这个变更,将“第三个”添加到文件 A 中的那行。

    git add .

    通常,在交互式 rebase 过程中,我们会运行 git rebase --continue,因为我们通常只是想回到我们提交记录的堆栈中以编辑早期的提交。但是这次,我们想创建一个新的提交。所以我们将运行 git commit -am 'two and a third'。现在我们编辑文件 A 并添加一行 two and two thirds

    git add . git commit -am 'two and two thirds' git rebase --continue

    我们的提交 three 与之发生了冲突,所以让我们解决它:

    我们将更改

    one
    <<<<<<< HEAD
    two and a third
    two and two thirds
    =======
    two
    three
    >>>>>>> bfb8e46... three
    
    one
    two and a third
    two and two thirds
    three
    

    git add .; git rebase --continue

    现在我们的git log -p看起来像这样:

    commit e59ca35bae8360439823d66d459238779e5b4892
    Author: Rose Perrone <roseperrone@fake.com>
    Date:   Sun Jul 7 13:57:00 2013 -0700
    
        three
    
    diff --git a/A b/A
    index 5aef867..dd8fb63 100644
    --- a/A
    +++ b/A
    @@ -1,3 +1,4 @@
     one
     two and a third
     two and two thirds
    +three
    
    commit 4a283ba9bf83ef664541b467acdd0bb4d770ab8e
    Author: Rose Perrone <roseperrone@fake.com>
    Date:   Sun Jul 7 14:07:07 2013 -0700
    
        two and two thirds
    
    diff --git a/A b/A
    index 575010a..5aef867 100644
    --- a/A
    +++ b/A
    @@ -1,2 +1,3 @@
     one
     two and a third
    +two and two thirds
    
    commit 704d323ca1bc7c45ed8b1714d924adcdc83dfa44
    Author: Rose Perrone <roseperrone@fake.com>
    Date:   Sun Jul 7 14:06:40 2013 -0700
    
        two and a third
    
    diff --git a/A b/A
    index 5626abf..575010a 100644
    --- a/A
    +++ b/A
    @@ -1 +1,2 @@
     one
    +two and a third
    
    commit 9aac58f3893488ec643fecab3c85f5a2f481586f
    Author: Rose Perrone <roseperrone@fake.com>
    Date:   Sun Jul 7 13:56:40 2013 -0700
    
        one
    
    diff --git a/A b/A
    new file mode 100644
    index 0000000..5626abf
    --- /dev/null
    +++ b/A
    @@ -0,0 +1 @@
    +one
    

    关于玩弄:如果您处于重新定义的状态,即使没有reflog,也不需要将所有内容都回滚。 git rebase --abort 就可以完成。 - Victor Sergienko
    通常来说,相比于依赖于 reflog 或 rebase --abort,在一个单独的分支上进行变基更容易。这样,如果你需要切换任务,你可以轻松地切换回非变基版本。 - undefined

    20

    git rebase --interactive可以用来将一个提交拆分成多个小提交。 在 Git 文档中关于rebase的简明介绍中有这个过程的详细步骤 - 分割提交

    在交互模式下,您可以使用“edit”操作标记提交。然而,这并不意味着git rebase期望此编辑的结果恰好是一个提交。实际上,您可以撤消提交或添加其他提交。这可用于将提交拆分为两个:
    1. 使用git rebase -i ^开始交互式rebase,其中是要拆分的提交。实际上,任何提交范围都可以,只要包含该提交即可。 2. 使用“edit”操作标记要拆分的提交。 3. 编辑提交时,执行git reset HEAD^。其效果是将HEAD倒退了一步,并且索引也随之变化。但是,工作树保持不变。 4. 现在将要包含在第一个提交中的更改添加到索引中。您可以使用git add(可能是交互性的)或git gui(或两者都有)来完成此操作。 5. 使用适当的提交消息提交当前索引。 6. 重复最后两个步骤,直到您的工作树变干净。 7. 使用git stash在每次提交、测试和修复必要的提交之后储存未提交的更改,以确保中间版本一致(编译、通过测试套件等)。

    在Windows系统下,记住^是命令行中的转义字符:它应该重复一次。例如,使用git reset HEAD^^而不是git reset HEAD^ - Frédéric
    @Frédéric :s 我从未遇到过这种情况。至少在PowerShell中不是这样的。然后使用 ^ 两次会将当前HEAD上面的两个提交重置。 - Farway
    @Farway,请在经典命令行中尝试。PowerShell则完全不同,它的转义字符是反引号。 - Frédéric
    2
    总结一下:在 cmd.exe 或 PowerShell 中使用 "HEAD^",在 cmd.exe 中使用 HEAD^^,在 PowerShell 中使用 HEAD`^。了解 shell(以及您特定的 shell)如何工作(即命令如何变成传递给程序的单个部分),这对于将在线命令转换为特定 shell 的正确字符非常有用。(不特定于 Windows。) - AndrewF
    你需要执行 git push --force-with-lease 命令才能重写远程(GitHub)上的历史记录。 - PhantomS

    15

    现在在Windows上最新的TortoiseGit中你可以非常容易地做到这一点。

    打开rebase对话框,配置它,然后按照以下步骤进行操作。

    • 右键单击要拆分的提交并选择“编辑”(在pick、squash、delete之间)。
    • 点击“开始”开始rebasing。
    • 一旦到达要拆分的提交,请勾选“编辑/拆分”按钮并直接点击“Amend”。提交对话框将打开。
      Edit/Split commit
    • 取消选择要放在另一个提交中的文件。
    • 编辑提交消息,然后点击“commit”。
    • 直到有要提交的文件,提交对话框会一次又一次地打开。当没有更多要提交的文件时,它还会问你是否要再添加一个提交。

    非常有帮助,谢谢TortoiseGit!


    14

    一个必要命令的快速参考,因为我基本知道该做什么但总是会忘记正确的语法:

    git rebase -i <sha1_before_split>
    # mark the targeted commit with 'edit'
    git reset HEAD^
    git add ...
    git commit -m "First part"
    git add ...
    git commit -m "Second part"
    git rebase --continue
    

    感谢Emmanuel Bernard的博客文章 《如何使用Git将提交分成两个》


    13
    另一种分割提交的方法是调用:

    git rebase -i <branch or commit>
    

    将您想要拆分的提交标记为edit,然后复制该行。这将在rebase期间呈现出两次提交。在编辑第一个提交时,删除您希望在第二个提交中拥有的所有内容。完成后(如果适用)运行测试,git add -Agit commit --amend(在此处调整提交消息)和git rebase --continue。现在您已经处于第二个提交上,它刚刚引入了您刚刚从第一个提交中删除的更改。

    我认为这比仔细使用git add -p要容易得多。


    真的是更简单的方法!谢谢,因为我没有想到! - Cyrille Pontvieux

    10
    请注意还有git reset --soft HEAD^命令。它类似于git reset(默认为--mixed),但保留索引内容。因此,如果您已添加/删除文件,则已经在索引中了。

    事实证明,在处理大型提交时非常有用。


    9

    19
    提供一些解决问题的方法,而不只是给出“阅读手册”(RTFM)的建议,会更有帮助。 - Jordan Dea-Mattson

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