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:
- The other system obviously is treating the two names as different.
- 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.