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

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个回答

9

如果没有交互式变基,最简单的方法可能是从要拆分的提交之前的提交开始创建一个新分支,然后使用cherry-pick -n命令来挑选该提交,重置、存储、提交文件移动,重新应用存储并提交更改,然后与原先的分支合并或挑选后续提交。 (然后将原先的分支名称切换到当前头部。)(最好遵循MBO的建议进行交互式变基。)


根据SO标准,这应该被归类为“不是答案”;但对于其他人来说仍然可能有帮助,所以如果您不介意,请将其移动到原始帖子的评论中。 - YakovL
@YakovL 看起来很合理。根据最小作用量原则,我不会删除答案,但如果其他人这样做,我也不会反对。 - William Pursell
2
这将比所有的rebase -i建议都容易得多。我认为这没有得到很多关注是因为缺乏任何格式,不过既然您已经有了126k分,并且可能知道如何使用SO,也许您可以重新审查一下它。;) - erikbstack
git rebase -i 在功能上等同于一系列的 git cherry-pick 操作。 - undefined
从技术上讲,这可能不是最准确的答案,但在实践中感觉更安全。我觉得用这种方法很少会出错,尤其是在历史记录较深时拆分提交。 - undefined
@arcanemachine 我同意。多年来,我发现我很少使用高级工具,而是更倾向于直接操作对象。除了rebase。我经常使用它,但通常只用于简单的情况。当我在进行交互式rebase失败或在进行带有合并冲突的cherry-pick时,我总是无法理解关于当前状态的消息,最后不得不使用reset --hard并手动修复问题。 - undefined

9

与最新提交一起工作

如果您只想从现有提交中提取某些内容并保留原始提交,则可以使用

git reset --patch HEAD^

这条命令可以让你只重置你需要的代码块,用法是git restore --staged <file>,代替git reset HEAD^

当你选择好要重置的代码块后,就会有一些修改被暂存,这些修改将会在下一次提交时被撤销。现在你可以更改最近的提交信息,将这些修改从其中删除。

git commit --amend --no-edit

如果您有未提交的代码块,可以通过以下方式将其添加到单独的提交中:

git add .
git commit -m "new commit"

使用不是最新提交的版本

当然可以像上面建议的那样使用git rebase --interactive命令来回到之前的某些提交。

离题的事实:

在Mercurial中,他们有hg split - 这是继hg absorb之后我希望在Git中看到的第二个功能。


6

以下是在IntelliJ IDEA, PyCharm, PhpStorm等工具中如何拆分提交的方法:

  1. 在版本控制日志窗口中,选择您想要拆分的提交,右键单击并选择从这里交互式重写提交历史

  2. 将您想要拆分的提交标记为edit,然后单击开始重写提交历史

  3. 您应该会看到一个黄色标签,表示 HEAD 设置为该提交。右键单击该提交,然后选择撤销提交

  4. 现在这些提交已经回到了暂存区,您可以将它们分别提交。在所有更改已提交后,旧提交变为无效。


4

虽然已经过去8年了,但也许有人仍会发现这很有帮助。 我可以在不使用rebase -i的情况下完成这个技巧。 关键是要使git回到在执行git commit之前的状态:

# first rewind back (mind the dot,
# though it can be any valid path,
# for instance if you want to apply only a subset of the commit)
git reset --hard <previous-commit> .

# apply the changes
git checkout <commit-you-want-to-split>

# we're almost there, but the changes are in the index at the moment,
# hence one more step (exactly as git gently suggests):
# (use "git reset HEAD <file>..." to unstage)
git reset

操作完成后,您将看到类似于 Unstaged changes after reset: 这样的提示文字,此时您的仓库状态就像您要提交所有这些文件一样。从现在开始,您可以像往常一样轻松地再次提交它。希望这有所帮助。


使用reset --hard命令时要非常非常小心。它会毫不留情地摧毁未被追踪的更改,而不会给出任何警告或确认提示。 - undefined

3
大多数现有的答案都建议使用交互式变基——git rebase -i或类似的方法。对于像我这样害怕“交互式”方法并且在下楼时喜欢抓手栏的人,这里提供一种替代方法。
假设你的历史记录看起来像…—>P–>Q–>R–>…–>Z=mybranch,你想将P–>Q拆分为两个提交,以得到P–>Q1–>Q'–>R'–>…Z'=mybranch,其中Q'R'等处的代码状态与QR等处相同。
在开始之前,如果你很谨慎,可以备份mybranch,以免丢失历史记录。
git checkout mybranch
git checkout -b mybranch-backup

首先,查看P(你想要拆分的提交之前的提交),并创建一个新分支进行工作

git checkout P
git checkout -b mybranch-splitting

现在,从 Q 中检出您想要的任何文件,并进行编辑以创建新的中间提交:
git checkout Q file1.txt file2.txt
[…edit, stage commit with “git add”, etc…]
git commit -m "Refactored the widgets"

注意此提交的哈希值,记为Q1。现在检出Q的完整状态,在分离的HEAD上检出Q1,提交此更改(创建Q'),并将工作分支拉取到它:
git checkout Q
git reset --soft Q1
git commit -m "Added unit tests for widgets"
git branch -f mybranch-splitting

你现在处于mybranch-splitting分支的Q'状态,它应该与Q完全相同。现在将原始分支(从QZ)变基到此分支上:
git rebase --onto HEAD Q mybranch

现在,mybranch 应该看起来像 ... P -> Q1 –> Q' –> R' –> ... Z',就像你想要的那样。因此,在检查一切是否正常工作之后,您可以删除您的工作和备份分支,并(如果适用)将重写的 mybranch 推送到上游。如果它已经被推送,您需要强制推送,并且所有关于强制推送的常规警告都适用。
git push --force mybranch
git branch -d mybranch-splitting mybranch-backup

4
备份分支在变基后非常有用。由于您只是拆分提交,因此要确保树保持不变。因此,您执行 git diff mybranch-backup 来确保没有意外遗漏。如果它显示差异-您可以使用 git reset --hard mybranch-backup 重新开始。另外,我认为 git checkout Q file1.txt file2.txtreset HEAD^commit -p 更脆弱。 - nponeccop
这看起来很像 git cherry-pick --no-commit 的功能。 - undefined

2

2
我编写了一个脚本来使这个过程更容易。将内容复制到一个名为git-split的文件中,放入您的$PATH目录下并使其可执行。然后您就可以使用git split来运行此脚本。请参阅脚本中的注释以了解如何使用:
#!/usr/bin/env zsh

# Use `git split` to split a commit into two commits.
# 
# First, use `git checkout --patch HEAD~` to stage changes that you
# want to split from the HEAD commit into a separate commit. Then run
# `git split` (this script) and enter a commit message for the new commit.

set -e

git commit --fixup=HEAD --quiet
git revert --no-commit HEAD
git revert --quit
if ! git commit $@; then
    git cherry-pick --no-commit HEAD
    git reset --soft HEAD~
    exit 0
fi
git -c sequence.editor=: rebase --autosquash --autostash --interactive HEAD~3

2

我使用了rebase。编辑提交并不适用于我,因为它已经选择了提交文件并允许您对其进行修改,但我想将所有文件作为未跟踪的文件添加,这样我就可以只选择其中一些文件。步骤如下:

  1. git rebase -i HEAD~5(我想要拆分历史记录中的倒数第5个提交)
  2. 复制目标提交的ID(稍后需要用到它)
  3. 使用d标记要删除的提交;在提交后添加b行以停止重定位进程并稍后继续。即使这是最后一个提交,这也给了您一些余地,以防万一出现问题,只需git rebase --abort并重置所有内容。
  4. 当重定位到达断点时,请使用git cherry-pick -n <COMMIT ID>。这将挑选提交更改而不选择提交本身,将其保留为未跟踪的更改。
  5. 添加您想要包含在第一个提交中的文件(或使用git add -i和补丁,以便您可以添加特定的块)
  6. 提交您的更改。
  7. 决定如何处理剩余的更改。在我的情况下,我希望它们出现在历史记录的末尾,并且没有冲突,因此我使用了git stash,但您也可以直接提交它们。
  8. git rebase --continue以选择其他更改
作为交互式变基的忠实粉丝,这是我能想到的最简单和最直接的步骤。希望这对于任何遇到此问题的人都有所帮助!

2

如果你有这个:

A - B <- mybranch

当您在提交B中提交了一些内容:

/modules/a/file1
/modules/a/file2
/modules/b/file3
/modules/b/file4

但是你想把B拆分成C和D,得到这个结果:
A - C - D <-mybranch

你可以按照以下方式划分内容(来自不同目录的内容在不同的提交中)...
将分割点之前的提交重置为当前分支:
```html

Reset the branch back to the commit before the one to split:

```
git checkout mybranch
git reset --hard A

创建第一个提交(C):
git checkout B /modules/a
git add -u
git commit -m "content of /modules/a"

创建第二次提交(D):
git checkout B /modules/b
git add -u
git commit -m "content of /modules/b"

2
如果B以上还有提交记录,该怎么办? - CoolMind

0

如果你的更改主要是添加新内容,那么这种方法最有用。

有时候你不想失去与正在分割的提交相关联的提交信息。如果您已经提交了一些要拆分的更改,可以执行以下操作:

  1. 从文件中编辑您想要删除的更改(即删除行或更改文件以适应第一个提交)。您可以使用您选择的编辑器和 git checkout -p HEAD^ -- path/to/file 的组合将一些更改还原到当前树中。
  2. 将此编辑作为新提交进行提交,例如 git add .; git commit -m 'removal of things that should be changed later' ,因此您将在历史记录中保留原始提交,并且您还将拥有另一个提交,其中包含您所做的更改,因此在拆分后第一个提交中,当前HEAD上的文件看起来像您想要的样子。
000aaa Original commit
000bbb removal of things that should be changed later
  1. 使用 git revert HEAD 撤销编辑,这将创建一个撤销提交。文件将看起来像原始提交,并且您的历史记录现在将如下所示:
000aaa Original commit
000bbb removal of things that should be changed later
000ccc Revert "removal of things that should be changed later" (assuming you didn't edit commit message immediately)

现在,您可以使用git rebase -i将前两个提交压缩/合并为一个,如果之前没有为还原提交提供有意义的提交消息,则可以选择修改还原提交。最终您应该只剩下一个提交。
000ddd Original commit, but without some content that is changed later
000eee Things that should be changed later

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