167

I am attempting a pretty beefy git merge maneuver right now. One problem that I am coming across is that I made some changes to some code in my branch, but my colleague moved that code to a new file in his branch. So when I did git merge my_branch his_branch, git did not notice that the code in the new file was the same as the old, and so none of my changes are there.

What's the easiest way to go about applying my changes again to the code in the new files. I won't have too many problems finding out which commits need to be reapplied (I can just use git log --stat). But as far as I can tell, there is no way to get git to reapply the changes into the new files. The easiest thing I am seeing right now is to manually reapply the changes, which is not looking like a good idea.

I know that git recognizes blobs, not files, so surely there must be a way to tell it, "apply this exact code change from this commit, except not where it was but where it now is in this new file".

asmeurer
  • 80,291
  • 25
  • 162
  • 229
  • 2
    Not exactly the same, but here is a similar question with good anwsers that might apply: http://stackoverflow.com/questions/2701790/git-merge-with-renamed-files – Mariano Desanze Jan 23 '15 at 16:35
  • 4
    None of the answers describe _why_ git isn't able to perform merges like this automatically. I thought it was supposed to be smart enough to detect renames and perform the appropriate merge automatically? – John Mar 30 '16 at 21:29
  • Perhaps this wasn't true when the question was asked, but modern versions of git (I'm using 1.9.5) can merge changes across renamed and moved files. There is even a `--rename-threshold` option to tweak the amount of similarity that is required. – Todd Owen Feb 27 '17 at 06:26
  • 1
    @ToddOwen that will work if you are doing a vanilla rebase off of an upstream branch, but you'll still run into trouble if you're cherry picking or back-porting a range of changes that may not include the commit that renames the files. – GuyPaddock Apr 19 '17 at 14:33
  • Is/was this a problem at all if you do a rebase first? – hbogert Jul 27 '19 at 13:09

4 Answers4

161

I had a similar issue, and I resolved it by rebasing my work to match the target file organization. This works because git keep tracks of files content, so by rebasing on top of a rename, the changes are applied as necessary.

More precisely, let's say that you modified original.txt on your branch (the local branch), but on the master branch, original.txt has been copied to another one, say copy.txt. This copy has been done in a commit that we name commit CP.

You want to apply all your local changes, commits A and B below, that were made on original.txt, to the new file copy.txt.

 ---- X -----CP------ (master)
       \ 
        `--A---B--- (local)
 

Create a throwaway branch move at the starting point of your changes with git branch move X. That is to say, put the move branch at commit X, the one before the commits you want to merge; most likely, this is the commit from which you branched out to implement your changes. As user @digory doo wrote below, you can do git merge-base master local to find X.

 ---- X (move)-----CP----- (master)
       \ 
        `--A---B--- (local)
 

On this branch, issue the following renaming command:

git mv original.txt copy.txt

This renames the file. Note that copy.txt did not yet exist in your tree at this point.
Commit your change (we name this commit MV).

        ,--MV (move)
       /
 ---- X -----CP----- (master)
       \ 
        `--A---B--- (local)
 

You can now rebase your work on top of move:

git rebase move local

This should work without problem, and your changes are applied to copy.txt in your local branch.

        ,--MV (move)---A'---B'--- (local)
       /
 ---- X -----CP----- (master)
 

Now, you do not necessarily want or need to have commit MV in your main branch's history, because the move operation may lead to a conflict with the copy operation at commit CP in the main branch.

You only have to rebase your work again, discarding the move operation, as follows:

git rebase move local --onto CP

... where CP is the commit where copy.txt was introduced in the other branch. This rebases all the changes on copy.txt on top of the CP commit. Now, your local branch is exactly as if you always modified copy.txt and not original.txt, and you can continue merging with others.

                ,--A''---B''-- (local)
               /
 -----X-------CP----- (master)
 

It is important that the changes are applied on CP or otherwise copy.txt would not exist and the changes would be applied back on original.txt.

Hope this is clear. This answer comes late, but this may be useful to someone else.

coredump
  • 34,744
  • 5
  • 41
  • 70
  • 3
    That's a lot of work, but I think it should work in principle. I think you'll have better luck with merge than rebase, though. – asmeurer Feb 28 '13 at 19:34
  • 10
    This solution only involves basic git commands, unlike editing patches or using `patch` (which involves also potentially a lot of work), and that's why I thought it could be interesting to show it. Also, note that I took more time to write the answer than to actually apply the changes, in my case. At which step would you recommand using a merge? and why? The only difference I see is that by rebasing, I make a temporary commit that is later discarded (commit `MV`), which is not possible with merges only. – coredump Mar 01 '13 at 08:46
  • 2
    With rebase, you have a higher chance of dealing with merge conflicts, because you deal with each commit, whereas with a merge you deal with everything at once, meaning some changes that might have happened with a rebase would be nonexistent with a merge. What I would do is merge, then manually move the merged file. Maybe I misunderstood the idea of your answer, though. – asmeurer Mar 01 '13 at 17:49
  • 5
    I added some ASCII trees to clarify the approach. Regaring merge vs. rebase in that case: what I want to do is take all the changes on 'original.txt' in my branch and apply them to 'copy.txt' in the master branch, because for some reason, 'original.txt' was copied (and not moved) into 'copy.txt' at some point. After that copy, 'original.txt' may also have evolved on the master branch. If I merged directly, my local changes on original.txt would be applied to the modified original.txt in master branch, which would be difficult to merge. Regards. – coredump Mar 04 '13 at 09:32
  • I see. I would recommend just doing `git merge local` from `move`, then manually copying over the file to your original `local` branch. This has git doing less of the work for you (you are manually copying the file), but it will end up being easier because merge conflicts are simpler when merging than when rebasing. – asmeurer Mar 08 '13 at 22:54
  • 1
    Either way, though, I believe this solution will work (though I thankfully don't have a situation to try it in right now), so for now I will mark it as the answer. – asmeurer Mar 08 '13 at 22:55
  • I see how you could merge `local` from `move`, indeed, and then move the file. In practice this depends on context and taste. I would just highlight that those particular rebase operations are unlikely to introduce conflicts by themselves (there can be conflict between `local` and `master` afterwards, but that is another matter). Thanks for accepting this answer :-) – coredump Mar 11 '13 at 14:23
  • The issue is that you can have a branch that merges cleanly, but rebases with conflicts, because later commits "remove" the conflicts. Anyway, if literally moving the file feels too "un-git-ish" to you, you could always use git to do it: `git checkout move -- file`. – asmeurer Mar 11 '13 at 23:18
  • I understand why conflict resolution with merge can be prefered over rebase, for the reason you gave. But there is definitely a case for rebase operations and this one seems appropriate. Conflict may arise when rebasing `local` from `X` to `CP`, and unfortunately, if this is the case, it depends on the actual context; then, you may decide it is more effective to copy the resulting file, or whatever works, than to resolve each conflict in the rebase. I won't argue because this might be the good choice in practice. – coredump Mar 12 '13 at 10:36
  • Will this work if `copy.txt` actually existed beforehand and it was replaced by the `CP` commit? – texasflood Oct 09 '15 at 10:46
  • @texasflood Yes, I don't see a problem with that. – coredump Oct 09 '15 at 20:11
  • @coredump In this scenario I get a conflict when rebasing with `git rebase move local`. Am I doing something wrong? – Tyler Mar 23 '17 at 16:40
  • @coredump I kind of figured it out. If your original.txt is blank, then you get the merge conflict when rebasing, but if your original.txt has content then it works. – Tyler Mar 23 '17 at 16:51
  • Ok, I tried with branches and such that works, I will have to try and see if it can work with multiple local repositories in the future... I used git tag command to create the tags in this tutorial. – oOo Jun 30 '18 at 17:26
  • Still not sure if subsequent changes to original.txt will still end up in copy though... will try so now without completing the entire tutorial... trying this after issueing the rebase command. – oOo Jun 30 '18 at 17:28
  • Though I suddenly realised this would be weird... this would then have to go vice versa... WOW... git is complex and very hard to use ! but might be worth it if it some day works ;) – oOo Jun 30 '18 at 17:29
  • Having this one way street should be usefull though, assuming original.txt to copy.txt trick can be used to also work on "splitted" files... and then hopefully subsequent changes to original.txt can be re-applied to the copies of copy.txt there is another stack overflow subject about this though (mine) and there is a script on github for splitting files... I think it uses a similiar technique wanted to see this technique in action manually... kinda interesting. – oOo Jun 30 '18 at 17:33
  • The script doesn't seem to do the re-basing though... not sure about that. Will ask what script does not do that. – oOo Jun 30 '18 at 17:33
  • Ok interesting so far, instead of re-basing I tried a re-merge so far so good, do get a conflict when trying to merge original with splitted files, but this is probably where I should "fix the history" like in the tutorial will try so now – oOo Jun 30 '18 at 18:24
  • I tried a git rm original.txt that didn't work.. it solved the conflict but merging didn't updated the splitted files... I probably should have done something else... maybe kill it in history or something – oOo Jun 30 '18 at 18:27
  • Gonna give the script a try cause I am getting fed up with this... hopefully that will work better than trying to come up with a manual way based on merging... do want to try and keep history intact. – oOo Jun 30 '18 at 18:28
  • Hmmm the rebasing may be necessary to prevent this merge conflict... will try original tutorial one more time... or I would have to tried a rename of previous commit or something not sure if that is possible maybe not... hmmm – oOo Jun 30 '18 at 18:30
  • Giving up for now on this method... going to try the script method. – oOo Jun 30 '18 at 18:37
  • Tried the script file also not working... errors, split file not part of repository... tomorrow I may try this re-base again to see if I can get it working... and can wrap my head around it... – oOo Jun 30 '18 at 19:05
  • Tried my approach result: fatal: refusing to merge unrelated histories too funny. – oOo Jun 30 '18 at 19:17
  • Gonna try a force see what that does: --allow-unrelated-histories, worked but no real affect so far gonna try some changes to files see what happens – oOo Jun 30 '18 at 19:19
  • Oh wait a file was something duplicated as predicted by me... hmmm how to prevent the duplication... will try the rename trick see if that helps – oOo Jun 30 '18 at 19:19
  • So far git has proven to be a total waste of time... what a total piece of shit. I am starting to understand why open source development goes so slow... it 1. completely requires cooperation otherwise the model breaks down completely and 2. it's very badly suited for restructing code... even with full cooperation everybody would have to wait until restructure is complete and then use it, if they don't things will become insanely difficult apperently. – oOo Jun 30 '18 at 19:23
  • I can only hope it's not a total waste of time, did make a nice document for myself of frequently used commands and tomorrow I can still tried this rebase method see if it does anything at all that is usuable... – oOo Jun 30 '18 at 19:50
  • 1
    @SkybuckFlying Thanks for keeping us updated with the live tweets. – miir Oct 24 '18 at 20:09
34

You can always use git diff (or git format-patch) to generate the patch, then go manually edit the filenames in the patch, and apply it with git apply (or git am).

Short of this, the only way it's going to work automatically is if git's rename detection can figure out that the old and new files are the same thing - which it sounds like they aren't really in your case, just a chunk of them. It's true that git uses blobs, not files, but a blob is just the contents of an entire file, without the filename and metadata attached. So if you have a chunk of code moved between two files, they aren't really the same blob - the rest of the blob's content is different, just the chunk in common.

Cascabel
  • 451,903
  • 67
  • 363
  • 314
  • 1
    Well, it's the best answer so far. I couldn't figure out how to make `git format-patch` work for a commit. If I do `git format-patch SHA1`, it generates a whole bunch of patch files for the whole history. But I guess `git show SHA1 > diff.patch` will work just as well. – asmeurer Aug 16 '10 at 17:29
  • 1
    @asmeurer: use the `-1` option. The normal mode of operation for format-patch is a revision range, like `origin/master..master`, so you can easily prepare a patch series. – Cascabel Aug 16 '10 at 17:31
  • 1
    Actually, another note. `git apply` and `git am` are too picky, because they want the same line numbers. But I am currently being successful with the UNIX `patch` command. – asmeurer Aug 16 '10 at 17:36
  • Thanks for this. It seems that when one's dealing with a large amount of moved (and on their branch modified) files, this is the only reasonable (and quite fast) way. – Honza Kalfus May 27 '21 at 09:25
32

Here is a merge solution of encountering a merge conflict with rename and edit and resolving it with mergetool recognizing the correct 3 merge source files.

  • After a merge fails because of 'deleted file' that you realize was renamed and edited:

    1. You abort the merge.
    2. Commit renamed files on your branch.
    3. And merge again.

Walk-through:

Create a file.txt:

$ git init
Initialized empty Git repository in /tmp/git-rename-and-modify-test/.git/

$ echo "A file." > file.txt
$ git add file.txt
$ git commit -am "file.txt added."
[master (root-commit) 401b10d] file.txt added.
 1 file changed, 1 insertion(+)
 create mode 100644 file.txt

Create a branch where you will edit later:

$ git branch branch-with-edits
Branch branch-with-edits set up to track local branch master.

Create the rename and edit on master:

$ git mv file.txt renamed-and-edited.txt
$ echo "edits on master" >> renamed-and-edited.txt 
$ git commit -am "file.txt + edits -> renamed-and-edited.txt."
[master def790f] file.txt + edits -> renamed-and-edited.txt.
 2 files changed, 2 insertions(+), 1 deletion(-)
 delete mode 100644 file.txt
 create mode 100644 renamed-and-edited.txt

Swap to branch, and edit there too:

$ git checkout branch-with-edits 
Switched to branch 'branch-with-edits'
Your branch is behind 'master' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)
$ 
$ echo "edits on branch" >> file.txt 
$ git commit -am "file.txt edited on branch."
[branch-with-edits 2c4760e] file.txt edited on branch.
 1 file changed, 1 insertion(+)

Attempt to merge master:

$ git merge master
CONFLICT (modify/delete): file.txt deleted in master and modified in HEAD. Version HEAD of file.txt left in tree.
Automatic merge failed; fix conflicts and then commit the result.

Notice the conflict is hard to resolve - and that files were renamed. Abort, mimic the rename:

$ git merge --abort
$ git mv file.txt renamed-and-edited.txt
$ git commit -am "Preparing for merge; Human noticed renames files were edited."
[branch-with-edits ca506da] Preparing for merge; Human noticed renames files were edited.
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename file.txt => renamed-and-edited.txt (100%)

Try merge again:

$ git merge master
Auto-merging renamed-and-edited.txt
CONFLICT (add/add): Merge conflict in renamed-and-edited.txt
Recorded preimage for 'renamed-and-edited.txt'
Automatic merge failed; fix conflicts and then commit the result.

Great! Merge results in a 'normal' conflict that can be resolved with mergetool:

$ git mergetool
Merging:
renamed-and-edited.txt

Normal merge conflict for 'renamed-and-edited.txt':
  {local}: created file
  {remote}: created file
$ git commit 
Recorded resolution for 'renamed-and-edited.txt'.
[branch-with-edits 2264483] Merge branch 'master' into branch-with-edits
Vincent Scheib
  • 15,430
  • 9
  • 58
  • 75
  • Interesting. I'll have to try this the next time I come across this issue. – asmeurer Aug 27 '13 at 01:37
  • 1
    In that solution, it seems to me that the first step, the merge that is aborted, is only useful to find out which files were renamed and edited remotely. If you know them in advance. You could skip that step and, basically, the solution is simply to rename the files manually locally and then merge and resolve the conflicts as usual. –  Mar 31 '16 at 03:07
  • 1
    Thank you. My situation was that I moved `B.txt -> C.txt` and `A.txt -> B.txt` with git mv, and git couldn't automatically match the merge conflicts correctly (was getting merge conflicts between old `B.txt` and new `B.txt`). By using this method, the merge conflicts are now between the correct files. – cib Jan 10 '18 at 13:17
  • This only works when the whole file is moved, but git should generally detect that situation automatically. The tricky situation is when only part of a file is moved. – Robin Green Sep 05 '19 at 10:37
3

My quick solution to this problem (in my case it was not a single file, but a whole directory structure) was:

  • move the file(s) in "my_branch" to the location where they are in "his_branch" (git rm / git add)
  • git commit -m "moved to original location for merging"
  • git merge his_branch (no conflicts this time!)
  • move the file(s) to my desired location (git rm / git add)
  • git commit -m "moved back to final location after merge"

You will have two additional commits in the history.

But since git tracks the moving of the files, git blame, git log etc will still work on these files, since the commits that moved the files did not change them. So I don't see any drawback to this method and it's very straightforward to understand.

Ernesto Baschny
  • 331
  • 2
  • 5