mart1n
mart1n

Reputation: 6233

How to apply a Git patch to a file with a different name and path?

I have two repositories. In one, I make changes to file ./hello.test. I commit the changes and create a patch from that commit with git format-patch -1 HEAD. Now, I have a second repository that contains a file that has the same contents as hello.test but is placed in a different directory under a different name: ./blue/red/hi.test. How do I go about applying the aforementioned patch to the hi.test file? I tried git am --directory='blue/red' < patch_file but that of course complains that the files are not named the same (which I thought Git didn't care about?). I know I could probably edit the diff to apply to that specific file but I'm looking for a command solution.

Upvotes: 158

Views: 96423

Answers (6)

LyrePyre
LyrePyre

Reputation: 481

Preconditions

  • We have 2 disjoint Git repositories, ./repo-a and ./repo-b, which we must assume do not share any history.
  • We want to patch a file named hello.test from repo-a onto a file named blue/red/hi.test in repo-b.
  • Our changes to hello.test are recorded as commits in repo-a.
  • Our working trees and indexes in both repos are clean.
repo-a
└── hello.test
repo-b
└── blue
    └── red
        └── hi.test

Postconditions

  • The selected changes to repo-a/hello.test are now reflected in repo-b/blue/red/hi.test.
  • (Optional) The commit information (author, timestamps, etc) from the original changes have been cloned into repo-b minimally modified. See solution (2) below.

(1) If we don't care about preserving commit info:

Solution: git diff + GNU patch

This is essentially @georgebrock's answer as well.

git -C repo-a  diff --patch HEAD~1 -- 'hello.test' > a.patch
patch repo-b/blue/red/hi.test a.patch

Or as a one-liner:

patch repo-b/blue/red/hi.test <(git -C repo-a diff HEAD~1 -- 'hello.test')

This one works so nicely because we explicitly specify our target file on the command line for patch, which lets us ignore any filename discrepancies stemming from the patchfile.

The --patch switch for git diff is optional in most cases, but it costs ya nothing and increases portability in oddball cases.

Finally, you can apply patches with git apply, but then you would need to fixup the patchfile to contain correct filepaths as an intermediate step.

sed -Ei 's|(\S)/hello.test|\1/blue/red/hi.test|g' a.patch
cd repo-b
git apply ../a.patch

(2) If we DO care about preserving commit info:

It seems to me like OP's original approach sought to preserve commit metadata, based on the use of git format-patch and git am.

So let's see if we can fulfill that without running into filename mismatch errors 😄

Aside: The reason this is such a challenge is because fundamentally, git format-patch and git am were designed to be used on repos having shared histories, a.k.a. the same repository (or forks thereof). Using these utilities to apply patches onto unrelated repositories does work, but it's like trying to use a newspaper to make a toothbrush—the results vary extremely widely, and you'll probably experience an icky feeling after all's said and done.

git -C repo-a format-patch -p1 --stdout \
    | sed -E 's|(\S)/hello.test|\1/blue/red/hi.test|g' \
    > b.patch
cd repo-b
git am -3 ../b.patch

The -3 switch to git am enables 3-way merging, which IMO especially with today's mergetools, is the easiest way to fix any broken patches that might remain (aside from trying again with git am -C2 in trivial cases). If you don't anticipate any patch/merge issues, -3 is a precautionary no-op.

Tips for either method (1) or (2)

  • Changes at the end of any file can cause headaches when patching. Usually, you can resolve this by increasing the fuzziness parameter to 3 (e.g. patch -F3) or reducing the -C option to git apply (e.g. git apply -C2), but your mileage may vary.
  • However you generate your patchfiles, you'll definitely want to select diff formats with context. You can't go wrong with the "Unified" diff format, which is context-based (3 lines above and below by default), uses compact line anchors, and is regarded as the most human-readable format.
  • Especially if you're patching changes onto increasingly diverging text files, adding more context lines (e.g. -U5) can save your skin when line numbers seldom match anymore, but it's not a cure-all. In fact, more context can sometimes even worsen the frequency of patch rejections / merge conflicts. If this happens to you, then you probably need to start looking into different methods of splicing your text files...

(3) My wildcard solution ;)

  • Approach = come in from left field and leverage git add --patch instead of any of the previously mentioned tools.
  • Potential benefits:
    • it's literally impossible to end up with merge conflicts or "rejected patches"
    • hunk-by-hunk interactive patching doubles as a code review session
    • no need to hackily repurpose format-patch / am / apply
    • nor perform any intermediate filepath fixups with sed, ...
# (preconditions still hold!)
cp repo-a/hello.test repo-b/blue/red/hi.test
cd repo-b
git add --patch .
# begin interactive patching experience <3
# it's over 😥 no more hunks
git commit -m 'patched in changes from repo-a'

interactive patching experience

  • Hit ? to get a handy legend for the interactive commands 🤑
  • (s for "split" is underrated~)

Personally, this is the solution I would choose almost every time. Especially if it's code you're patching, or any other irregular language (i.e. markup, JSON), interactively patching changes in this way takes you and your collaborators a lot further—specifically in terms of ensuring correctness and generally catching bugs before they're committed.

See also:

  • git checkout --patch = great if the changes already exist within the current repo, in a different file or/and in a different commit-ish (branch, tag, etc).

Upvotes: 1

magiraud
magiraud

Reputation: 1117

There is a simple solution that does not involve manual patch editing nor external script.

In the first repository (this may also export a range of commit, add the -1 flag if you want to select only one commit) :

git format-patch --relative <committish> --stdout > ~/patch

In the second repository :

git am --directory blue/red/ ~/patch

Instead of using --relative in git format-patch, another solution is to use -p<n> option in git am to strip n directories from the path of the patches, as mentioned in a answer to a similar question.

It is also possible to run git format-patch --relative <committish> without the --stdout, and it will generate a set of .patch files. These files can then be fed directly to git am with git am --directory blue/red/ path/to/*.patch.

Upvotes: 100

Mike Robinson
Mike Robinson

Reputation: 8995

FYI: I recently had problems trying to download a patch from Github and applying it to a local file (which was an "override" in a new location).

git am wouldn't apply the patch either because the file was "not in index" or "dirty." But, I found that the simple patch command could apply the patch. It did prompt me for the name of the file to be patched.

Got the job done, anyway ...

Upvotes: 0

oliver
oliver

Reputation: 6791

Building upon the answer by @georgebrock, here's a solution I used:

First, create the patch files as usual (eg. git format-patch commitA..commitB).

Then make sure that your target repository is clean (there should be no changed or untracked files) and apply the patches like this:

cd second-repo
git am ~/00*.patch

For every patch file you will get an error like "error: XYZ does not exist in index". You can now apply this patch file manually:

patch --directory blue/red < ~/0001-*.patch
git add -a
git am --continue

You have to do these three steps for each patch file.

This will preserve the original commit message etc. without requiring any special git format-patch command or editing the patch files.

Upvotes: 9

user746461
user746461

Reputation:

I understand the two files are exactly the same in your situation, thus the patch is likely to succeed.

However, in case you want to apply a patch to a similar, but not exactly the same file, or you want to do an interactive patching, you will use three way merge.

Say you modified File A, let's denote A~1 as the previous version, and you want to apply the diff between A~1 to A to File B.

Open a three way merge tool, for instance Beyond Compare, the path of left panel is A, middle panel is the common ancestor so the path is A~1, the path of right panel is B. Then, the lower panel shows the result of applying the diff between A~1 to A to File B.

The following figure illustrates the idea.

enter image description here

Upvotes: 1

georgebrock
georgebrock

Reputation: 30203

You could create the patch using git diff and then apply it using the patch utility, which allows you to specify the file you want to apply the diff to.

For example:

cd first-repo
git diff HEAD^ -- hello.test > ~/patch_file

cd ../second-repo
patch -p1 blue/red/hi.test ~/patch_file

Upvotes: 155

Related Questions