How do I git-apply a patch to a previous revision?
Assume I have a branch with the following history:
A - B - C - D
Between B and C I modified a bunch of files including a particular file, call it foo.txt.
Then, on revision D, I modified that same file, foo.txt.
Meanwhile, a friend of mine took a snapshot of my directory at B and decided he would tweak foo.txt in some way. Let's call that revision E. If they were part of the same repository, they would look like this:
A - B - C - D \ E
However, they're not so we just have A - B - C - D in my repository and then he sends me a patch for the differences between B and E.
Since I have messed with foo.txt as well, I can't just apply the patch directly to D, the context for the diff won't match, it's expecting me to be at or around B.
It seems like I would want to kind o开发者_如何学编程f create the hypothetical repository tree I mentioned above, then do a merge between D and E in this repository in order to play back my changes C and D together in the right order.
So my questions:
- Is this the most sensible way to apply changes based on historical 'baselines' ?
- Are there any drawbacks I'm not seeing?
- How does one accomplish this in git cleanly?
You're correct that you do want to take advantage of git's merge faculties. There are still a few possibilities. The most indicative of what actually happened is probably the merge, but a method resulting in a modified version of E
applied on top of D
isn't so bad either. Let's talk about how to do that!
If the patch contains the blobs it applies to (the output from git format-patch
does), then git am
is capable of attempting a three-way merge! The blob SHA1 records in a diff look like this:
diff --git a/foo.txt b/foo.txt
index ca1df77..2c98844 100644
If you've got that, you're in luck. Just use git am --3way <patch>
. Unfortunately, git am
does require the email-style header that format-patch produces, so if the patch came from git diff
instead of git format-patch
, you have to kludge a little. You can add it yourself:
From: Bobby Tables <bobby@drop.org>
Date: 2 Nov 2010
Subject: [PATCH] protect against injection attack
<original diff>
git am
should be able to work with that, and if you don't get it all exactly how you wanted it, you can always git commit --amend
to fix it. (You could also try using git apply --build-fake-ancestor=foo.txt <patch>
, but it really doesn't work in a user-friendly way. I think tricking git am
is easier.)
If the patch doesn't contain any blob SHA1s (i.e. it was created with diff, not a git command), again, tell your friend how to use git, and I'm pretty sure you can still kludge it. You know what version of foo.txt the patch is supposed to apply to! To get its SHA1 from commit B, use:
git ls-tree <SHA1 of B>:<directory containing foo.txt>
It'll be one of the blobs listed. (I know there's a direct way, but I just can't think of it right now.) You can then add a fake git diff header. Suppose its hash is abcdef12:
diff --git a/foo.txt b/foo.txt
index abcdef12..
Git doesn't actually need anything but that first hash; though the git diff output would have the final hash and a mode, am
isn't looking for it, so you can get away with leaving it off. (And yes, I just tested this. I hadn't done this before!)
This will result in a history like A - B - C - D - E'
, where E'
is your friend's patch, but applied to D
; it's a result of a three-way merge between the content in D
, B
, and your friend's patch.
Finally, if you don't want to muck around with any of that, you can do as you said:
git checkout -b bobby <SHA1 of B>
# apply the patch
git commit --author="Bobby Tables <bobby@drop.org>"
git checkout master
git merge bobby
# or `git cherry-pick bobby` to grab the single commit and apply to master
# or `git rebase bobby master` to rebase C and D onto B
I've had a look around—I swear there must be a way to do this automagically in git, but for now your best bet is probably to do what you've said.
Assuming your commit at B
has ID 12345...
:
$ git checkout 12345
Note: checking out '12345....'.
You are in 'detached HEAD' state.
[...]
$ git checkout -b friends_change
$ git apply < patchfile.patch
$ git status
[... something interesting ...]
$ git commit -m "Applying friend's patches to foo"
$ git checkout master
$ git merge friends_change
Of course, you could rebase instead, if you haven't published history to anyone:
$ git rebase friends_change
This'll create a tree like this:
A - B - C - D (old master)
\
E - C' - D' (new master)
I had a very similar problem to this one, and these answers were very helpful in pointing me in the right direction. However what I really wanted to end up with in my case was this:
A - B - E - C - D
First of all, none of these commits had been pushed anywhere yet, so rebasing was safe. If the commits had already been pushed and therefore potentially distributed, I would not have attempted this.
Here's how I did it (substitute as appropriate for the variables in wedges):
git checkout -b temporary <B-sha1>^
git am <E-patchfile>
git log --oneline -1
Now copy-paste the SHA1 and commit message.
git checkout <orig-branch>
git rebase -i <B-sha1>^
Now, in the appropriate place (which should be line #2), add this line:
pick <E-sha1> <E-commit-msg>
Save, let the rebase do its thing, resolving any conflicts as you usually would. Finally:
git branch -d temporary
Since the branch delete wouldn't leave your patch commit dangling, you don't need to use git branch -D
, and using -d
also provides a nice way to verify that you didn't screw up something.
Hopefully this is helpful for other folks in situations similar to mine.
精彩评论