使用Git部分挑选提交记录

595
我正在处理两个不同的分支:releasedevelopment
我注意到仍然需要将提交到 release 分支的某些更改集成回 development 分支中。问题是我不需要所有提交,只需要特定文件中的某些块,所以简单地使用
git cherry-pick bc66559

这并不能解决问题。

当我执行一个

git show bc66559

我看到了差异,但不知道一个好的方式来部分地应用它到我当前的工作树。

8个回答

965

您需要的核心操作是git add -p-p--patch的同义词)。这提供了一种交互式添加内容的方式,让您决定每个块是否应该添加,甚至可以手动编辑补丁。

要与cherry-pick结合使用:

git cherry-pick -n <commit> # get your patch, but don't commit (-n = --no-commit)
git reset                   # unstage the changes from the cherry-picked commit
git add -p                  # make all your choices (add the changes you do want)
git commit                  # make the commit!

感谢Tim Henigan提醒我git-cherry-pick有一个--no-commit选项,也感谢Felix Rabe指出您需要git reset。如果您只想从提交中留下一些内容,则可以使用git reset <path>...来取消暂存那些文件。

如果需要,您可以为add -p提供特定的路径。如果您从补丁开始,可以用apply替换cherry-pick


如果您真的想要git cherry-pick -p <commit>(该选项不存在),您可以使用

git checkout -p <commit>

这将会把当前提交与您指定的提交进行比较,并允许您逐个应用差异。如果您拉取的提交在您不感兴趣的部分有合并冲突,那么此选项可能更有用。 (但是,请注意,checkoutcherry-pick 不同:checkout 尝试完全应用 <commit> 的内容,而 cherry-pick 则应用其父提交的指定提交的差异。这意味着 checkout 可以应用超过仅该提交的内容,这可能不是您想要的。)


1
实际上,如果我按照git 1.7.5.4的建议操作,“git add -p”会显示“没有更改”,因为所有更改都已经在索引中了。在执行“git add”之前,我需要执行“git reset HEAD”操作 - 是否有任何选项可以避免这种重置? - Felix Rabe
@FelixRabe:我其实很惊讶,在某个时候,cherry-pick -n 显然没有将更改暂存——约定肯定是 --no-commit 选项在提交之前就停止了,也就是说,所有更改都已经暂存。我会在答案中添加重置。 - Cascabel
2
@Blixt 如果另一个分支上的提交不在您当前的分支上,而是A-B-C-D-E-F,则“git checkout -p <F>”不仅会获取来自F的更改,而是将ABCDEF混合在一起,并让您整理出您想要的部分。从F中缩小到只有一些更改是很麻烦的。另一方面,“git cherry-pick -n <F>”仅获取来自F的更改-如果其中一些更改冲突,它会友好地告诉您,以便您可以正确地进行合并。 - Cascabel
2
在2022年,仍然需要像这样做,而且没有“cherry-pick -i”,这太荒谬了。 - Fizz
使用git reset -p来交互式地取消暂存部分补丁,而不是使用git reset && git add -p - undefined
显示剩余2条评论

87

1
是的,我已经使用/发现了这种方法。请注意,它不会创建提交,您必须自己完成,而且没有快速的方法可以使其自动记录在提交消息中哪些内容被挑选(这样)。Git有点笨拙。 - Fizz
2
请注意,这不仅适用于(或提供)从该提交应用更改,而且适用于自分支分叉以来的所有更改(至少在Git 2.25.1中)。 - Paŭlo Ebermann
是的,这绝对不是挑选樱桃。 - haridsv
是的,这绝对不是挑选樱桃。 - undefined

36

假设您想要的更改位于要从中获取更改的分支的头部,请使用git checkout。

对于单个文件:

git checkout branch_that_has_the_changes_you_want path/to/file.rb

对于多个文件,只需串联:

git checkout branch_that_has_the_changes_you_want path/to/file.rb path/to/other_file.rb

5
这将复制整个文件,而不仅仅是更改部分。 - Jeremy List
1
这个答案和其他答案一样,有一个很大的缺点:它不保留更改的原始作者,而是以你的名字提交。如果更改是好的,你就在窃取别人的功劳;如果更改是坏的,你就会自食其果。似乎没有绕过 git cherry-pick -p 的方法,可惜它还没有出现。 - pfalcon

15

Mike Monkiewicz 的回答基础上,您还可以指定一个或多个文件从提供的sha1/branch中检出。

git checkout -p bc66559 -- path/to/file.java 

这将允许您交互式地选择要应用于当前文件版本的更改。


6
如果当前文件的版本与那个版本有显著的不同,这就是一个问题。您将会收到大量的无关变化(这些变化未出现在提交中)。同时,您真正想要的更改可能以“伪装形式”出现,即与当前版本而非原始版本的不同之处。可能会出现您想要的原始不同之处与其他变化冲突,需要进行适当合并;然而在这种情况下您将没有这个机会。 - Kaz

2
如果你想在命令行上指定一个文件列表,并在单个原子命令中完成整个操作,请尝试以下命令: git apply --3way <(git show -- list-of-files) --3way: 如果补丁无法干净地应用,Git 将创建合并冲突,以便您可以运行 git mergetool。省略 --3way 将使 Git 放弃无法干净应用的补丁。
"Original Answer" 翻译成 "最初的回答"。

1
如果你的shell无法处理<(...),请使用等效的git show -- list-of-files | git apply --3way - - raphinesse

1
使用git format-patch将您关心的提交部分切片,使用git am将其应用到另一个分支。
git format-patch <sha> -- path/to/file
git checkout other-branch
git am *.patch

0
实际上,这个问题的最佳解决方案是使用checkout命令。
git checkout <branch> <path1>,<path2> ..

例如,假设您在 master 分支中,您想要获取 dev1 分支上 project1/Controller/WebController1.javaproject1/Service/WebService1.java 的更改,您可以使用以下命令:
git checkout dev1 project1/Controller/WebController1.java project1/Service/WebService1.java

这意味着分支仅从这两个路径的dev1更新。


0
如果“部分挑选”意味着“在文件中选择一些更改但放弃其他更改”,那么可以通过引入git stash来完成:
  1. 进行完整的挑选。
  2. git reset HEAD^将整个挑选的提交转换为未暂存的工作更改。
  3. 现在git stash save --patch:交互式选择要隐藏的不需要的内容。
  4. Git从您的工作副本中回滚了被隐藏的更改。
  5. git commit
  6. 丢弃不需要的更改的隐藏内容:git stash drop
提示:如果您给不需要的更改的隐藏内容命名:git stash save --patch junk,那么如果您现在忘记执行(6),以后您将会认出这个隐藏内容。

如果你只是挑选一些提交,然后重置...你就没有任何东西可以藏起来了。如上所述,你要么使用--no-commit进行挑选,要么使用reset --hard HEAD~ 进行重置。请注意,你正在进行负面操作(选择你不想要的)。上述被接受的解决方案允许积极或消极的方法。 - Pierre-Olivier Vares

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