5
\$\begingroup\$

In my company, we use git as VCS, the main server run under linux but all our code is for Windows and we all develop on Windows.

The git tag system is case sensitive on linux but is case insensitive on windows. That is:

  • under linux Test/Tag and TEST/Tag are different tags
  • under windows Test/Tag and TEST/Tag are the same tag

We had a lot of issues from that.

So we decided to add a git hook on server side, that is, a script that will test if a pushed tag already exists on the server case insensitively.

Here is the content of the pre-receive git hook.

#!/bin/sh

process_ref() {
        target="$2"   # target hash. 0s for a delete command
        refname="$3"  # the full ref name

        #detect tags
        case "$refname" in
                refs/tags/*)

                # use grep to count matching refs in .git/refs/tags folder (case unsensitivly)
                count1=`find ./refs/tags -print | grep -ic $refname`

                # use grep to count matching refs in packed-refs if present
                if [ -e "./packed-refs" ]; then
                        count2=`grep -ic "$refname" ./packed-refs`
                else
                        count2="0"
                fi

                # abort if the ref already exist and it's not a delete ref command
                if ([ $count1 -ne 0 ] || [ $count2 -ne 0 ]) && [ $target != "0000000000000000000000000000000000000000" ]; then
                        echo "push failed"
                        echo "tag already exists: $refname"
                        echo "please contact dev team"
                        exit 1
                fi
        esac
}

# iterate thru all given refs.
while read REF; do process_ref $REF; done
\$\endgroup\$
0
2
\$\begingroup\$

Finding existing Git tags

The files in a Git repository's meta storage, such as the content of ./refs/tags, ./packed-refs, are not API. It's best to interact with Git through commands.

You can get the list of tags with the git tag -l command. This will include packed refs as well.

Use early returns

The hook collects counts of matching tags, then matching packed refs, and finally it decides if it should fail based on the collected counts, and if the operation is not a delete. It's usually better to not delay actions when you have enough information to take them.

For example, very early in the process_ref function you can already know if it's a delete operation. It would be better to return 0 right there, to avoid any further unnecessary processing.

The same goes for checking the counts. If the first count in tags is non-zero, you can already fail without further processing.

Use grep -q instead of grep -c and checking for non-zero

When grep -q finds a match, it stops further processing. grep -c continues until the end of the input to collect all counts. That's not necessary in your example.

Use more strict pattern matching

Using grep -ic $refname to find a matching ref in the list of refs may leave room for false positives. For example if there is a tag v1.1, and then you try to push v1, it will fail. This is a bit tortured example, because obviously you won't release v1 after v1.1, but it's good to be aware of this point.

You could make the matching more strict by adding a $ to anchor the end. If both the beginning and the end can be anchored, then you could use the -x flag of grep to match entire lines. Now that would be rock-solid strict.

Always quote parameters

Even though you know that $refname will never contain unsafe characters, it's a good habit to always surround with double-quotes when used as command arguments, for example in grep -ic "$refname".

Use $(...) instead of `...`

`...` is obsolete, use $(...) instead.

Alternative implementation

Putting the above together (and some more), I'd implement the hook like this:

#!/bin/sh

is_delete() {
    [ "$1" = "0000000000000000000000000000000000000000" ]
}

tag_exists() {
    git tag -l | grep -qix "$1"
}

fail() {
    echo "push failed"
    echo "tag already exists: $1"
    echo "please contact dev team"
    exit 1
}

process_ref() {
    target="$2"   # target hash. all 0s for a delete command
    refname="$3"  # the full ref name

    is_delete "$target" && return

    case $refname in
        refs/tags/*)
            tagname=${refname#refs/tags/}
            tag_exists "$tagname" && fail "$refname"
    esac

    return 0
}

while read REF; do process_ref $REF; done
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your answer, the is_delete function bug me. I'm pretty sure you missed a-f (since hash is in hexadecimal). A only 0a-f hash is unlikely for sure but.. \$\endgroup\$
    – Orace
    Oct 23 '18 at 11:12
  • \$\begingroup\$ @Orace well spotted! (And in the end I went for a simpler option.) \$\endgroup\$
    – janos
    Oct 23 '18 at 12:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.