Skip to content
Advertisement

How to prevent git from committing two files with names differing only in case?

We develop in a mixed environment – some people work on Macs and some work on Linux. This has proven to be a bit of a challenge at times, as those people who work on Linux are used to having their filesystems be case sensitive, so there’s no issue committing (accidentally or otherwise) multiple files differing just by case. (e.g. FileName.ext versus filename.ext)

However, when the people on Macs go to check out the repository, having a case-insensitive filesystem means that the two files – differing only in case – overwrite each other and cause general havoc.

I know that there are various git settings to help people on case-insensitive filesystems work better with case changes (e.g. core.ignorecase), but these don’t solve the issue where there’s two different files in the repository, only differing by case.

I realize that the only way to fix it is to make sure the Linux people don’t commit the two files differing only in case in the first place. — Is there some setting in git which will pop up a warning or error if a user on a case-sensitive filesystem attempts to commit file(s) which would be confused with each other on a case-insensitive filesystem?

Advertisement

Answer

There’s nothing built in (although there should be, no doubt). What you can do is provide a pre-commit hook that verifies that all names are OK and prevents the commit if not.

This hook only needs to be run on the Linux box (although making it work on Linux and Mac is easy, it’s just Windows with its default impoverished toolbox that is problematic). You might want to add it to a side branch and give the Linux folks instructions on setting it up.

You may want to check branch names as well, as in git pre-commit or update hook for stopping commit with branch names having Case Insensitive match. (Interesting: the answer on this question is my own; I had forgotten it.)

First, let’s write a “check for case conflict” function. This is just a matter of sorting with case-folding (so that “helloworld” and “helloWorld” are placed adjacent to each other), then using uniq -di to print any duplicate (after case-folding) strings, but no non-duplicates:

sort -f | uniq -di

If this produces any output, these are the “bad names”. Let’s capture the output in a temporary file and check its size, so we can print them to standard output as well:

#! /bin/sh

TF=$(mktemp)
trap "rm -f $TF" 0 1 2 3 15
checkstdin() {
    sort -f | uniq -di > $TF
    test -s $TF || return 0   # if $TF is empty, we are good
    echo "non-unique (after case folding) names found!" 1>&2
    cat $TF 1>&2
    return 1
}

Now we just need to use it on files that will be committed, and perhaps on branch names as well. The former are listed with git ls-files, so:

git ls-files | checkstdin || {
    echo "ERROR - file name collision, stopping commit" 1>&2
    exit 1
}

You can fancy this up to use git diff-index --cached -r --name-only --diff-filter=A HEAD to check only added files, allowing existing case collisions to continue, and/or try to check things across many branches and/or commits, but that gets difficult.

Combine the above two fragments into one script (and test) and then simply copy it to an executable file named .git/hooks/pre-commit.

Checking branch names is a bit trickier. This really should happen when you create the branch name, rather than when you commit to it, and it’s impossible to do a really good job on the client—it has to be done on a centralized server that has a proper global view.

Here is a way to do it on the server in a pre-receive script, in shell script rather than in Python (as in the linked answer). We still need the checkstdin function though, and you might want to do it in an update hook rather than a pre-receive hook, since you don’t need to reject the entire push, just the one branch name.

NULLSHA=0000000000000000000000000000000000000000 # 40 0s

# Verify that the given branch name $1 is unique,
# even IF we fold all existing branch names' cases.
# To be used on any proposed branch creation (we won't
# look at existing branches).
check_new_branch_name() {
    (echo "$1"; git for-each-ref --format='%(refname:short)' refs/heads) |
      checkstdin || {
        echo "ERROR: new branch name $1 is not unique after case-folding" 1>&2
        exit 1  # or set overall failure status
    }
}

while read oldsha newsha refname; do
    ... any other checks ...
    case $oldsha,$refname in
    $NULLSHA,refs/heads/*) check_new_branch_name ${refname#refs/heads/};;
    esac
    ... continue with any other checks ...
done
User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement