Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I git-apply a patch to a previous revision?

Tags:

git

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 of 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?
like image 285
Chris Harris Avatar asked Nov 07 '10 08:11

Chris Harris


2 Answers

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 <[email protected]>
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 <[email protected]>"
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
like image 53
Cascabel Avatar answered Oct 28 '22 01:10

Cascabel


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)
like image 40
Asherah Avatar answered Oct 28 '22 00:10

Asherah