如何只挑选特定文件的更改?

871
如果我想将仅对某个提交中的部分文件进行更改的更改合并到一个Git分支中,该如何实现呢?
假设名为“stuff”的Git提交对文件A、B、C和D进行了更改,但我只想将“stuff”对文件A和B的更改合并。这听起来像是git cherry-pick的工作,但cherry-pick只知道如何合并整个提交,而不是文件的子集。

如果git gui可用,那我会像Cascabel一样操作。如果不行的话,Tyrone Wilson的方法对于精细选择变更是不错的选择。 - undefined
15个回答

985

我会使用cherry-pick -n--no-commit)来进行操作,这样你就能在提交之前检查(和修改)结果:

git cherry-pick -n <commit>

# unstage modifications you don't want to keep, and remove the
# modifications from the work tree as well.
# this does work recursively!
git checkout HEAD <path>

# commit; the message will have been stored for you by cherry-pick
git commit

如果大部分的修改都是你不想要的,那么你可以重置所有内容,然后再添加你需要的内容,而不是检查单个路径(中间步骤):

# unstage everything
git reset HEAD

# stage the modifications you do want
git add <path>

# make the work tree match the index
# (do this from the top level of the repo)
git checkout .

25
除了使用 git checkout . 之外,我建议还要使用 git clean -f 命令,以删除由挑选的提交引入的任何新但不需要的文件。 - rlat
7
对于后一种方法的额外说明:我使用git add -p命令,它可以让你交互式地决定要将哪些更改添加到每个文件的索引中。 - matthaeus
7
如果挑选的提交与当前工作副本差别很大,那么情况就不太妙,但是如果只有一个文件可以干净地应用,那就行了。 - lmat - Reinstate Monica
12
你也可以使用 git reset -p HEAD 来选择性地取消暂存。这相当于 add -p,但很少有人知道它的存在。 - Patrick Schlüter
3
似乎这并没有保留原作者的署名。在某些情况下,这可能无关紧要,但在其他情况下则很重要。 - AlanSE
显示剩余11条评论

269

其他方法对我没用,因为提交有很多变更和与许多其他文件的冲突。我的做法很简单:

git show SHA -- file1.txt file2.txt | git apply -

实际上它不会为你添加文件或提交,因此您可能需要跟进它

git add file1.txt file2.txt
git commit -c SHA

或者,如果您想跳过添加操作,可以使用--cached参数来执行git apply

git show SHA -- file1.txt file2.txt | git apply --cached -

你也可以对整个目录执行相同的操作

git show SHA -- dir1 dir2 | git apply -

3
有趣的方法,谢谢。但是show SHA -- file | apply不就和Mark Longair的回答中的checkout SHA -- file基本上是一样的吗? - Tobias Kienzler
11
不,checkout SHA -- file 将会精确地检出 SHA 版本,而 show SHA -- file | apply 只会应用 SHA 中的更改(就像 cherry-pick 一样)。这很重要,因为(a)源分支中有多个提交更改了给定文件,或者(b)当前目标分支中有一个提交更改了该文件。 - Michael Anderson
19
刚刚发现另一个很好的用途:有选择性地还原,当您只想还原一个文件时(因为git revert会撤销整个提交)。在这种情况下,只需使用命令git show -R SHA -- file1.txt file2.txt | git apply - - Michael Anderson
4
“@RoeiBahumi that has quite a different meaning. git diff SHA -- file1.txt file2.txt | git apply - means apply all the differences between the current version of the file and the version at SHA to the current version. In essence it is the same as git checkout SHA -- file1.txt file2.txt. See my earlier comment for why that is different to what the git show version.” 的意思是:这句话的意思与之前说的不同。“git diff SHA -- file1.txt file2.txt | git apply -”的意思是将文件当前版本和SHA版本间的所有差异应用到当前版本,本质上与“git checkout SHA -- file1.txt file2.txt”相同。详见我之前发表的评论,了解为什么与“git show”版本不同。 - Michael Anderson
12
如果你需要解决冲突,请使用 git apply -3 - 而不是仅使用 git apply -,这样如果发生冲突,你可以使用标准的冲突解决技巧,包括使用 git mergetool - qwertzguy
显示剩余11条评论

177

通常我会使用-p标志与从其他分支进行的git checkout,我发现这比我遇到的大多数其他方法更容易和更精细。

原则上:

git checkout <other_branch_name> <files/to/grab in/list/separated/by/spaces> -p

例子:

git checkout mybranch config/important.yml app/models/important.rb -p

接着会弹出一个对话框,询问您要在哪些“代码块”中进行更改,这基本上是每个连续代码更改的一部分,然后您可以为每个代码块发出信号 y(是)n(否)等。

-ppatch选项适用于git中的各种命令,包括git stash save -p,它允许您选择要从当前工作区中存储的内容。

我有时会在完成大量工作并希望将其分离出来并提交到更具主题性的提交中时使用此技术,使用git add -p并选择每个提交所需的内容 :)


5
我经常使用 git-add -p 命令,但我不知道 git-checkout 命令也有一个 -p 选项。这个选项是否解决了非 -p 答案中提到的合并问题? - Tobias Kienzler
1
至少-p选项可以允许手动编辑这样一个冲突的部分,而cherry-pick也可能会产生。下次需要时我会测试一下,这绝对是一个有趣的方法。 - Tobias Kienzler
4
不破坏分支上并发变更的最佳两个答案之一。 - akostadinov
1
git reset -p HEAD 还允许使用 -p 选项,当您只想从索引中删除一些补丁时,这个选项非常方便。 - Patrick Schlüter
1
有趣的是,您可以使用类似于“foo”的全局名称来获取“文件”,这将在其他分支的上下文中扩展 - 这样您就可以轻松地指向当前分支/工作树中不存在的文件(顺便说一句,如果没有-p,它是无法工作的)。 - Grigory Entin
显示剩余4条评论

62

情况:

您在分支上,假设为master,并且您有一个提交在任何其他分支上。 您必须从该特定提交中选择一个文件。

方法:

步骤1:切换到所需的分支。

git checkout master

步骤2:确保您已复制所需的提交哈希值。

git checkout commit_hash path\to\file

步骤三:现在你已经在你想要的分支上有了所需文件的更改。你只需要将它们添加并提交。

git add path\to\file
git commit -m "Your commit message"

2
太棒了!对于我在\path\to\directory\目录中的所有更改也起作用。 - zaggi
2
非常感谢!这比挑选简单多了。 - menecio
3
覆盖目标分支所做的任何更改。 - Tanveer Badar
2
到目前为止最好的解决方案! - AouledIssa
最适合答案描述中的情况的编程语言。 - Banee Ishaque K
显示剩余2条评论

53

也许这种方法比Jefromi的答案更有优势,因为你不必记住git reset 的哪种行为是正确的 :)

 # Create a branch to throw away, on which we'll do the cherry-pick:
 git checkout -b to-discard

 # Do the cherry-pick:
 git cherry-pick stuff

 # Switch back to the branch you were previously on:
 git checkout -

 # Update the working tree and the index with the versions of A and B
 # from the to-discard branch:
 git checkout to-discard -- A B

 # Commit those changes:
 git commit -m "Cherry-picked changes to A and B from [stuff]"

 # Delete the temporary branch:
 git branch -D to-discard

3
谢谢您的回答。现在这激发了我思考,为什么不跳过“挑选”步骤,直接使用“git checkout stuff -- A B”命令?再用“git commit -C stuff”命令,提交信息也能保持不变。 - Tobias Kienzler
12
只有当在“stuff”上修改的文件未在您当前的分支或者在“HEAD”和“stuff”的共同祖先以及“stuff”的末尾之间的任何地方进行过修改时,该方法才能奏效。如果有更改,则“cherry-pick”会创建正确的结果(基本上是合并的结果),而您的方法会丢弃当前分支中的更改,并保留从共同祖先到“stuff”的所有更改 - 而不仅仅是一个单独提交中的更改。 - Cascabel
2
@Tobias Kienzler:我假设你的起点与“stuff”的父级足够不同,以至于樱桃拣选的结果会使“A”和“B”的内容与提交“stuff”中的内容不同。然而,如果它只是相同的话,你是正确的——你可以按照你所说的去做。 - Mark Longair
@Jeromi,@Mark:感谢你们的反馈。在我的情况下,我正在处理具有完全不相交文件的分支,这导致了我的建议。但是,确实我迟早会遇到麻烦,所以感谢你们提出这个问题。 - Tobias Kienzler
我认为我在另一个帖子中的回答可能是你想要的。 - Ian
git checkout to-discard -- A B 会覆盖当前的 A 和 B,还是与当前的 A 和 B 合并? - Yves

48

挑选特定"提交(commit)"中的更改被称为"cherry-pick"。最简单的解决方案是选择特定文件的所有更改,可以使用

 git checkout source_branch <paths>...

例如:

$ git branch
* master
  twitter_integration
$ git checkout twitter_integration app/models/avatar.rb db/migrate/20090223104419_create_avatars.rb test/unit/models/avatar_test.rb test/functional/models/avatar_test.rb
$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   new file:   app/models/avatar.rb
#   new file:   db/migrate/20090223104419_create_avatars.rb
#   new file:   test/functional/models/avatar_test.rb
#   new file:   test/unit/models/avatar_test.rb
#
$ git commit -m "'Merge' avatar code from 'twitter_integration' branch"
[master]: created 4d3e37b: "'Merge' avatar code from 'twitter_integration' branch"
4 files changed, 72 insertions(+), 0 deletions(-)
create mode 100644 app/models/avatar.rb
create mode 100644 db/migrate/20090223104419_create_avatars.rb
create mode 100644 test/functional/models/avatar_test.rb
create mode 100644 test/unit/models/avatar_test.rb

参考资料及完整解释http://jasonrudolph.com/blog/2009/02/25/git-tip-how-to-merge-specific-files-from-another-branch/

更新:

使用此方法,git不会合并文件,它只会覆盖目标分支上进行的任何其他更改。您需要手动合并更改:

$ git diff HEAD 文件名


8
我也是这么想的,但是如果那些文件在“两个”分支上都发生了变化,这个方法就会完全失败,因为它会丢弃当前分支的更改。 - Tobias Kienzler
你是对的,必须澄清的是,这种方式 git 不会合并(merge),而只是覆盖。然后您可以执行“git diff HEAD 文件名”以查看更改,并手动进行合并。 - cminatti
这对我很有效。一位同事在拉取请求上发表了评论,认为某些更改应该拆分成单独的请求,以便清晰地分离问题。你的方法在这种情况下非常有效。 - 4thex
你也可以使用它从特定的提交中获取特定的文件。git checkout e0032947bd37f44044962ae2f7229d339944f83c example.txt - Rishi Kulshreshtha

33
为了完整起见,对我来说最好的选择是:
git show YOURHASH --no-color -- file1.txt file2.txt dir3 dir4 | git apply -3 --index -

它完全符合OP的要求。在需要时,它会进行冲突解决,就像merge一样。它会执行git add,但不会执行git commit,所以请检查你的git status

6
很美。 - Matthew Poer
谢谢!你能说一下 3 是什么意思吗(... git apply -3 --index ...)? - Dan Nissenbaum
4
-3代表使用“三方合并回退”功能。在实践中,这意味着Git可以插入常见的冲突标记,或者绕过已经被应用的更改。默认的git apply行为对我来说太严格了,它会拒绝应用任何细微差别上的整个补丁。 - kubanczyk
你救了我。谢谢。 - Subhajit Halder

22
有时候,使用checkout从一个提交中获取特定的文件可能会更容易。在我看来,这样做可以给你更多的控制权。
我会这样做:
git checkout <branch|hash> -- path/to/file1 path/to/filen

然后取消暂存所需的更改以适应代码,并在提交之前进行测试。如果一切正常,就提交。

与多个现有答案相同。 - matt

18

我只想挑选所有东西,然后这样做:

git reset --soft HEAD^

然后我会还原我不想要的更改,再进行新的提交。


1
如果挑选的内容会在你不需要的文件中引起许多冲突,那么这种方法并不高效。 - thanos.a

12

使用 git merge --squash branch_name 命令,它将获取另一个分支上的所有更改,并为您准备一个提交。现在删除所有不需要的更改,只保留您想要的更改即可。这样 Git 将不知道有任何合并操作发生过。


谢谢,我不知道有这个合并选项。如果您想挑选一个完整分支的大部分内容,这是一个可行的替代方案(但与 cherry-pick 不同的是,如果没有共同祖先,则无法使用它)。 - Tobias Kienzler
git不会知道有合并的存在吗?我以为git什么都知道的?:\ - jtlz2

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