how do you move duplicate branches with different capitalizations?

If you’re on a Linux system, this is easy, because Git there is always case-sensitive: readme and README are two different files and master and Master are two different branches.

If you’re on a typical Windows or macOS file system, with case-insensitive native file names,1 it gets messy: Git thinks that master and Master are two different names but sometimes uses those names as ordinary files in the file system. It then expects those two different names to remain different, but the OS insists on behaving otherwise. Since Git only does this sometimes, though, sometimes it works the way Git thinks it should.

What you can do is make use of two facts:

  1. The other system obviously is treating the two names as different.
  2. Git doesn’t actually care what a branch name is—how it’s spelled, etc—it just cares about the stored hash ID in each name.

So, step 1 of fixing the problem is to run:

git ls-remote origin

This will spit out a few, or many, lines of output, e..g:

e2850a27a95c6f5b141dd88398b1702d2e524a81    HEAD
898f80736c75878acc02dc55672317fcc0e0a5a6    refs/heads/maint
e2850a27a95c6f5b141dd88398b1702d2e524a81    refs/heads/master
5d2a92d10f831493d4b0b258ee3cd18d4a7ae995    refs/heads/next
4141ae219919d2e29f3939983be8501770f247a3    refs/heads/seen
1637c5d31a0b0df8e3dcef7072720fe0381520ac    refs/heads/todo
d5aef6e4d58cfe1549adef5b436f3ace984e8c86    refs/tags/gitgui-0.10.0
3d654be48f65545c4d3e35f5d3bbed5489820930    refs/tags/gitgui-0.10.0^{}
[... tons of tags snipped here]

The thing on the left, in each column, is the raw hash ID. This raw hash ID is what Git cares about. The thing on the right is the name under which the other Git repository is storing the raw hash ID. The refs/heads/* names are their branch names.

You may wish to save all of this output in a file. Note, too, that we’re counting on the other Git repository not to have any changes made to it while we’re working. If it is a highly active repository, you’re better off bringing up a Linux system (or Linux VM) and just doing ordinary Git stuff in it.

(Side note: git ls-remote -h origin trims the output to just the branch names, which can be pretty handy if there are many tags.)


1Technically, case-sensitivity is per file system. On macOS you can easily set up a case-sensitive disk image, mount it, clone there, and fix everything there, without even having to bother with a VM. There may be case-sensitive file systems available for Windows systems as well, but I don’t know of any, nor whether there’s anything as relatively easy as the macOS “.dmg” trick.


What to do after step 1

If their master and Master are both storing the same raw hash ID, you can now just ask them to delete one of the two names:

git push --delete origin Master

(assuming the one you want to get rid of is the one with the one uppercase M). You are now done.

If their master and Master store different raw hash IDs, you must now figure out what to do about this. You are going to delete one of these two names. If their master is ahead of their Master, you can do the same thing you would do if they were equal: just delete their Master. But if their master is behind their Master, or both are ahead of and behind each other, you can’t quite do that, at least not safely.

Understanding this requires understanding what “ahead of” and “behind” actually mean. Commit hash IDs, in Git, are part of a directed acyclic graph or DAG. We need a brief aside about graph theory and partially ordered sets.

Partially ordered sets

Most people are familiar with total order, which we get with, e.g., integers: 1 comes before 2, 2 comes before 3. Any number greater than 3 comes after all of these.

In a graph, however, we can have a simple line:

A--B--C--D--...

or a fork:

     C   <-- last
    /
A--B
    \
     D   <-- also-last

Here, A comes before B, and B comes before both C and D, but we can’t say that C comes before or after D. In a sense, they’re both in the same spot, right after B.

This is also the case for Git commits. If each uppercase letter stands in for some raw commit hash, and last and also-last are actually master and Master, then we have a case where deleting one of the two names will make it impossible for Git to find the two “last” commits. Git actually works by having names that identify each “last commit”, and working backwards from there, so Git needs both names to find both commits C and D here.

Applying that partial order thing to Git

In Git-practical terms, what this means is that if you can determine that master really “comes after” Master—if they have:

...--G--H   <-- Master
         \
          I   <-- master

then you can just delete Master safely. They will now have:

...--G--H--I   <-- master

and they will still be able to find all their commits.

But if that’s not safe, you should have them create a new name for the later commit. That is, if they have this:

...--G--H   <-- master
         \
          I   <-- Master

or this:

       H   <-- master
      /
...--G
      \
       I   <-- Master

then what you should do is create, in their Git repository, a new name that identifies the same commit as their Master. To do this, you can run:

git push origin <hash>:<new-name>

For instance, if the hash ID for their master is deadbeef, and you choose as the new branch name, the name save-for-now, you would run:

git push origin deadbeef:save-for-now

This will create, in their Git repository, this kind of situation:

       H   <-- master
      /
...--G
      \
       I   <-- Master, save-for-now

It’s now safe to run:

git push origin --delete Master

to ask them to delete their name Master:

       H   <-- master
      /
...--G
      \
       I   <-- save-for-now

Note that if you’re in a “safe” situation, such as when both names identify the same last commit, you can still do this safe stuff:

git push origin <hash>:save-for-now

creates:

...--G--H   <-- Master, master, save-for-now

and then deleting Master gives you:

...--G--H   <-- master, save-for-now

(all in their Git repository of course).

Now that you’ve fixed things, just run git fetch --prune origin

You can now have your Git re-synchronize your repository with their Git repository. You may have to run this git fetch --prune origin twice, because your local Git could conceivably delete the name Master after updating master, and with your Git storing that name in a file, your Git might delete both names, but this is safe because the second git fetch will restore it. (In general Git seems to do these in ASCII order so that a single fetch will work right, but I don’t know if this is guaranteed for all Git versions past and future.)

If all the commits were safe before, so that you could just safely delete their Master, you now have a normal setup in which to solve everything normally. If they weren’t safe before, you now have an origin/safe-for-now remote-tracking name in your own Git repository. This name doesn’t clash with your origin/master name, so there is no problem solving everything normally.

Either way, in other words, it’s now safe to go back to work.

CLICK HERE to find out more related problems solutions.

Leave a Comment

Your email address will not be published.

Scroll to Top