gitgithubmergegit-mergesquash

Git merge error: `fatal: refusing to merge unrelated histories` after merging with `--allow-unrelated-histories` flag, resolving conflicts manually


1. Backstory

I recently switched to a new computer, where I copied all files from my old. Long story short, I did some mistakes setting up git and GitHub remote repositories correctly. I think it started with being unable to reconnect to GitHub, and I ended up deleting my old .git folder (probably not the wisest move).

I have two branches master and apprentice, where I use the apprentice branch for trying out stuff, and then merging into master when I get the code working.

On my new computer, I started working locally and committed to the local apprentice branch. I then tried to push to remote branch, which didn't work. I then force pushed, and ended up overwriting my commit history on apprentice, effectively losing all commits on this branch prior to switching computer. This sucked, but I didn't see a way to undo the damage, neither was it terribly important as I had my most recent code. I, therefore kept working and making commits on apprentice.

2. Problem

My current problem stems from trying to merge the changes on apprentice into master.

Since the entire commit history on apprentice was effectively rewritten, a simple merge didn't work, so I therefore tried merging using the --allow-unrelated-histories flag. This allowed the merge to proceed, after manually resolving all merge conflicts. I was then able to finalise the commit and push to remote, and the remote master branch now had the added changes from apprentice. (Resolving merge conflicts, committing and pushing was done in VS Code.)

Terminal output:
% git checkout master
% git merge apprentice                       
fatal: refusing to merge unrelated histories

% git merge --squash --allow-unrelated-histories apprentice
Auto-merging .Rprofile
CONFLICT (add/add): Merge conflict in <myfile_1>
CONFLICT (add/add): Merge conflict in <myfile_2>
⋮
CONFLICT (add/add): Merge conflict in <myfile_n>
Automatic merge failed; fix conflicts and then commit the result.

Subsequent to merging and pushing, I updated all remote repos

git push --all

However, GitHub tells me apprentice "is [still] 6 commits ahead, 19 commits behind master".*

enter image description here

Looking into what those differences are, it only tells me that there's nothing to compare because apprentice and master are entirely different commit histories.

enter image description here

I therefore tried to merge again (this time no --squash):

% git merge --no-ff -X theirs master
fatal: refusing to merge unrelated histories

% git merge --allow-unrelated-histories apprentice
Auto-merging .Rprofile
CONFLICT (add/add): Merge conflict in <myfile_1>
CONFLICT (add/add): Merge conflict in <myfile_2>
⋮
CONFLICT (add/add): Merge conflict in <myfile_n>
Automatic merge failed; fix conflicts and then commit the result.

Like before, I proceeded to resolve the conflicts manually in VS Code, committed changes and pushed to remote. This resulted in the commits on apprentice being added on top of the previous squashed commit. However, GitHub still tells me that master and apprentice are entirely different commit histories.

3. Questions

This leads me to the following questions:

  1. Why aren't the commit histories resolved when merge conflicts have been resolved and merge completed? Provided that I already completed the merge, resolving any conflicts manually, I would have thought that this error should no longer occur, which leads me to the second question:
  2. How can I re-create the commit histories so that I can proceed to merge "normally" (i.e. without the --allow-unrelated-histories flag) in subsequent merges of the two branches?

Solution

  • Basically, you had this situation, due to restarting the branch on a new computer:

                 master
                   v
    O--O--O--O--O--O
    
    
       O--O--O--O--O
                   ^
               apprentice
    

    If you had done a normal merge, with --allow-unrelated-histories, you would've had this situation:

                    master
                      v
    O--O--O--O--O--O--M
                     /
                    /
       O--O--O--O--O
                   ^
               apprentice
    

    This would've been fine, and any further work on apprentice could be merged on top of master and git would handle this as you expect, only trying to merge the changes introduced by the new commits.

    However, since you did a squash, you have this situation instead:

                    master
                      v
    O--O--O--O--O--O--S
    
    
       O--O--O--O--O
                   ^
               apprentice
    

    S here represents one or more squash commits that were produced by squashing the commits from apprentice. Notice one thing, there's no merge commit involved. As such, git does not store any knowledge of the relationship between apprentice and master, and the next time you try to do another merge between the two, all the commits will again be in play, replaying all those merge conflicts you resolved during the squash+merge.

    If you intend to squash the commits when merging, my advice would be to adopt one of the following two methods:

    The third option I did not present, which you might like to have, keep the original branch unsquashed to preserve the history, but squash+merge on top of master, is not viable, as git will bring up all the history at every merge attempt. Instead you will have to start using cherry-pick and similar to make copies of "new" commits every time, and the hassle is just not worth it.


    For completeness, there is a way to record a merge commit without using the normal merge command with git, in essence recording that relationship. However, this involves telling git explicitly how things are related, and how to disregard double changes, again the hassle is not worth it (in my opinion), so I won't elaborate on this here. Suffice to say it involves git commit-tree and is rather low-level.