Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Verify if a tag was done in the master branch

In this project I am working, we do deployments based on tags. Whilst it's obligatory that the tags are done against the master branch (after you merge the release there), sometimes by mistake someone can tag against a dev or release branch, which is incorrect. That causes several problems.

In our deployment script, there is a step in which we do clone a specific tag from git, using a process like the one described in this question: Download a specific tag with Git

$ git clone
$ git checkout tags/<tag_name>

How can I amend this script to check if this tag was actually done against the master branch? I would like to then stop the deployment and throw an error if the branch is not the master.

Thanks.

like image 384
testTester Avatar asked Apr 05 '18 05:04

testTester


1 Answers

As several people noted, your question cannot really be answered until it is reformulated. This is because a Git tag or branch name simply identifies one specific commit. The desired effect of a branch name is to identify the tip commit of a branch, which changes over time, so that the specific commit it identifies also changes over time. The desired effect of a tag name is to identify one specific commit forever, without changing. So if someone tags master, there will be some moments in time during which parsing the name master produces commit hash H, and parsing the tag name also produces commit hash H:

if test $(git rev-parse master) = $(git rev-parse $tag^{commit}); then
    echo "master and $tag both identify the same commit"
else
    echo "master and $tag identify two different commits"
fi

This particular test is valid until someone makes the branch name master advance, after which it is no longer useful. If we draw this the way I typically like to draw Git commit graphs on StackOverflow, we can see this as:

          tag
           |
           v
...--o--o--H   <-- master
          /
 ...--o--o   <-- develop

Currently the names tag and master both identify commit H, which is a merge commit. As soon as someone creates a new commit on master, though, the graph becomes:

          tag
           |
           v
...--o--o--H--I   <-- master
          /
 ...--o--o   <-- develop

Now master identifies new commit I, so doing the rev-parse tag^{commit} will find H while doing the rev-parse master will find I and they won't be equal and the test will fail.

(I drew commit I as an ordinary commit here, but it could be a merge commit with a second parent. If so, imagine a second backwards-pointing line / arrow emerging from I, pointing to some other earlier commit.)

Philippe's answer comes in two forms and answers a slightly different question. Since branch names do move over time, we can use git branch --contains to find all branch names that make the tagged commit reachable, and see if one of these is master. This will give a true / yes answer for the case above. Unfortunately, it will also tell you that the tag error is contained within master—which is true!—for this graph:

          tag
           |
           v
...--o--o--H   <-- master
          /
 ...--o--G   <-- develop
         ^
         |
       error

This is because the tag error identifies commit G, and commit G is reachable from commit H (by following the second parent of H). In fact any tag along the bottom row, pointing to any commit contained within the develop branch, identifies a commit contained within the master branch, since every commit currently on develop is also on master.

(Incidentally, the difference between using git rev-parse your-tag and using git rev-list -n 1 your-annotated-tag is covered by using git rev-parse $tag^{commit}. The issue here is that an annotated tag has an actual repository object, of type "annotated tag", to which the name points; using git rev-parse your-annotated-tag alone finds the tag object rather than its commit. The ^{commit} suffix syntax is described in the gitrevisions documentation.)

There is a way to tell whether the commit to which any given tag points is in the history of master that occurs only on the first-parent chain. It is not the prettiest: git branch --contains and git merge-base --is-ancestor are the usual building blocks for finding reachability but both follow all parents. To follow only first parents, we need to use git rev-list --first-parent to enumerate all the commits reachable from the name master when following only first parents. Then we simply check whether the tagged revision is in that list. Here's a bad way to do it that makes clear what we are doing:

tagged_commit=$(git rev-parse $tag^{commit}) ||
    fatal "tag $tag does not exist or does not point to a commit object"
found=false
for hash in $(git rev-list --first-parent master); do
    test $hash == $tagged_commit && found=true
done

To make this much faster, it would be better to pipe the git rev-list output through a grep that searches for $tagged_commit (with grep's output discarded since we only care about the status):

if git rev-list --first-parent master | grep $tagged_commit >/dev/null; then
    echo "$tag points to a commit reachable via first-parent from master"
else
    echo "$tag does not point to a commit reachable via first-parent from master"
fi

for instance. (One flaw here is that git rev-list will run all the way through every reachable commit; in a large repository, this can take seconds.)

like image 81
torek Avatar answered Sep 30 '22 00:09

torek