hIpPy
hIpPy

Reputation: 5085

List tags in git repo sorted by how they appear in graph

Given a branch and commit, I want to find the first tag that brought in the given commit.

I want to write a script that would do this.

I do not want to print all tags (which git log --tags --simplify-by-decoration does) but only the tags that appear between the tip of branch and the commit. I can use the --merged and --contains options of git tag command for that but it prints tag sorted by names. I need them sorted in the way they appear in the graph (so I can just do | tail -1) and I cannot do that by sorting them by fieldnames as authordate, committerdate, creatordate, taggerdate.

To give more context, the script is the counterpart to this SO thread of git-find-merge just that using merge commits to denote PRD code is an anti-pattern.

Update:

Below is the script I came up with eventually from torek's answer (github link: git-find-tag script).

~/bin/git-find-tag:

#!/bin/sh

commit=$1
if [ -z $commit ]; then
    echo 1>&2 "fatal: commit is required"
    exit 1
fi
commit=$(git rev-parse $commit)
branch=${2-@}
pattern=${3-.}

# tags between branch and commit sorted as in graph following a pattern
tags=$(
    git log --decorate --simplify-by-decoration --ancestry-path \
        --pretty="format:%D" $commit..$branch \
    | sed 's/, /\n/g' \
    | grep '^tag: ' \
    | sed 's/tag: //' \
    | egrep "$pattern"
)
if [ ! -z "$tags" ]; then
    echo "tags:"
    for tag in $tags; do
        echo "  $tag"
    done
    echo ""
fi

tag=$(echo "$tags" | tail -1)
if [ -z "$tag" ]; then
    # tag not found
    echo 1>&2 "fatal: no tag found"
    exit 1
fi

git show -s "$tag"

Upvotes: 2

Views: 1530

Answers (1)

torek
torek

Reputation: 488213

It seems that git log already does what you want:

git log --oneline --decorate --simplify-by-decoration --ancestry-path start..end

For instance, I ran this on the Git repository for Git:

$ git log --oneline --decorate --simplify-by-decoration --ancestry-path \
    HEAD~20..HEAD
b06d36431 (HEAD -> master, tag: v2.13.0, origin/master, origin/HEAD) Git 2.13
4fa66c85f (tag: v2.13.0-rc2) Git 2.13-rc2
027a3b943 (tag: v2.13.0-rc1) Git 2.13-rc1

(note that there is no --tags here!). See the details below for a caveat.

Details

The problem is not completely solveable in general as there's no strictly defined order in which the graph will be emitted. We know (from the documentation) that --graph turns on --topo-sort; but there are graphs in which two different topological sorts are both valid, such as a classic diamond:

 ...     newer / child commits
  |
  D
 / \
B   C
 \ /
  A      older / parent commits
  |
 ...

The two valid orders here are (D, B, C, A) and (D, C, B, A) and we don't know (unless we cheat1) which order git log will use. If all but A are tagged, the last one will be one of (B, C), but we don't know which one.

If you can be sure there are no ambiguous sorts, VonC's suggestion of --contains, or a more direct git merge-base --is-ancestor test, can determine whether any given tagged commit is an ancestor of any other tagged commit. The definition of "topological order" is "never show children after showing parent", i.e., whatever is "most parent-y" is the one coming out last.

If not, and you want to match what git log will do, you will need to use git log. Since it already does what you want, though, just use it. :-) You would like it to:

  • Log only tagged commits: --simplify-by-decoration does the job, more or less, though it will also include branch names (so beware of just using | tail -1: you need a bit more sophistication so as to take the last tag—but you can just pipe through grep tag: along the way).
  • Show only commits at or before some end point (end) and after some starting point (^start). Note that if you want to include the starting point itself, add the ^@ suffix, which means "all parents of the commit, but not the commit itself".
  • Avoid commits that are not children of the start commit: --ancestry-path does the trick, by excluding commits reachable from end that are not already excluded by ^start.

We spell ^start end as the more familiar start..end here, although either way works.


1The source to git log will (with a lot of difficulty) tell you which sort actually occurs in these cases.

Upvotes: 4

Related Questions