gitgithub

How to resolve a case issue (upper-/lowercase) in Git branch name? (resulting in multiple branches)


I have a branch in a Github repo with a name like this: feature/X/mybranch. A colleague has made changes to my branch, but committed them to feature/x/mybranch. Github, being seemingly case-sensitive, made a separate branch with his commit.

The issue now is that my Git client only sees one branch (I am on macOS), the original one with upper case. This gives all sorts of issues. Initially, I couldn't even see his commit, and without knowing what the real issue was, I tried different things and I was finally able to get his changes after a git reset --hard. However some side effects are still present. For instance, my client keeps telling me I have commits that haven't been pushed, even though I can see them in Github.

Ideally I just want to merge his branch into my and delete it (or even just delete since I am pretty sure I have all his changes), but I have no idea how to do that. As mentioned, my client (tried both in terminal and Sourcetree) only sees one branch, and I don't really know what would happen if I delete that.


Solution

  • There are several interlocking problems, all of which stem from a single issue: macOS file systems default to case-folding, so that a file named README can be opened using the name readme, and vice versa.

    Git sometimes stores branch name-and-value sets in a single file (.git/packed-refs), and when it does so, the names are case-sensitive, so that feature/X/mybranch and feature/x/mybranch are unrelated branch names. But Git sometimes stores the name as a file name (in/for which the slashes become folder name delimiters), with the hash ID contained in that file, and here, any attempt to use feature/X as if it were separate from feature/x fails, so that you end up with just one file named mybranch in a folder whose name is either x or X.

    The easiest way to fix it, by far, is to work within Git on a case-sensitive file system. For instance, if you have a Linux machine—or virtual machine—you spin up that Linux system, clone the GitHub repository, and now everything Just Works and you have the two different branch names and can fix everything easily, update GitHub, delete the undesired name, and so on. This also covers all the file-name-within-a-commit issues since you can now have two different files whose names are README and readme.

    You don't need to use a separate VM, though, because macOS allows you to create a case-sensitive disk image and mount it. You can then do your work in this mounted volume. This solves most of the problems; the remaining ones are decomposed vs composed Unicode (two different ways to name a file agréable for instance), which Linux allows but macOS still forbids.

    To create a case-sensitive disk image, see my previous answer to a different question. This covers many more situations than your immediate one.


    Given your immediate situation, though, there's a simple and direct way to do this without fussing with a Linux VM or case-sensitive file system, by doing most of the work on GitHub itself. It's a little bit tricky as you need to make sure you have the right commits available locally, but a git fetch origin gets them. You then need to use the FETCH_HEAD file that git fetch writes—in .git/FETCH_HEAD—to extract particular hash IDs.

    The contents of .git/FETCH_HEAD are mostly self-explanatory. It contains the branch name as seen on the other Git, and the hash ID (plus some other stuff): one entry per line, on multiple lines, corresponding to the branch names in that other Git.

    From your own Git, you can then:

    git push origin <hash-id>:<good-name>
    git push --delete origin <bad-name>
    

    to the Git at GitHub. The first command—which may require --force in some cases (if it does, be careful here, and consider using --force-with-lease=refname:hash:)—will create or update the desired branch name with the desired casing, using the fact that GitHub itself has case-sensitivity. So you now have the "right" branch name available there, set to the right hash ID. The second command will delete the name with the undesired casing.

    You may also want to use git branch -r -d to delete the remote-tracking name, if it has the wrong case:

    git branch -r -d <bad-name>
    

    Your own Git can now deal with the Git repository over on GitHub because it has only the good name to deal with, so now:

    git fetch origin
    

    behaves well, and does what you need.

    (None of the above is tested; there could be typos.)