oblitum
oblitum

Reputation: 12018

Git log of tags solely, in a given range

I wish to do what git log --no-walk --tags --decorate=short does, but I also want to check for tags in a given range soley (commit range), not just all the tags.

Upvotes: 1

Views: 1706

Answers (1)

torek
torek

Reputation: 488163

Edit: as jthill noted in a comment, git rev-list (and hence git log) has --simplify-by-decoration, which discards commits that do not have a (branch or) tag-name pointing to them:

git log --simplify-by-decoration --decorate=short X..Y

may do just what you want. Unfortunately, testing shows that it also keeps commits that have a branch-name pointing to them. The documentation is not 100% clear on this:

--simplify-by-decoration
Commits that are referred by some branch or tag are selected.

but then later:

The --simplify-by-decoration option allows you to view only the big picture of the topology of the history, by omitting commits that are not referenced by tags. ...

Original answer below line (it should work).


OK, it sounds like you want to combine the effects of something like X..Y with "select only commits that have a tag pointing to them".

There are two main Git commands for doing this kind of operation:

  • git for-each-ref, which lets you work with reference names—any name from anywhere within the refs/ name-space; tags are in refs/tags/.

  • git rev-list, which is actually kind of the same command as git log except that instead of showing commit messages by default, it shows hashes by default. (There are other minor differences, but they are so similar that both git log and git rev-list are built from one source file.)

The main problem you have here is that while git log / git rev-list easily let you restrict which commit(s) are selected, with Y ^X or X..Y for one example, or with --no-walk --tags[=pattern] for another, they're not very good at set intersection operations. They can directly do limited union style operations, e.g., git rev-list --no-walk --tags=foo\* --tags=bar\* --branches=baz\* would print the ID of every commit tagged with a name that matches foo* or bar*, or is pointed-to by a branch name matching baz*.

(The rev-list command can do full, arbitrary set union operations using pipes and --stdin, but not intersections. This is one area where Mercurial's syntax beats Git's. Of course Mercurial has the advantage of having all of Python to fall back onto, internally.)

What you want, though, is the intersection of two sets:

  • set 1: your range selection, probably something like X..Y or --since=date1 --until=date2

  • set 2: every commit pointed-to by (some or all) tags

Computing this set intersection requires some code. On a Unix-like system we have all the tools we need: Git itself, and comm, which can do the set intersection. The comm utility needs its input to be sorted so we'll pipe each set through sort here. So we just need a short (7 line) script.

We start with the usual temp file boilerplate, e.g.:

tf1=$(mktemp) || die "can't make first temp file"
trap "rm -f $tf1" 0 1 2 3 15
tf2=$(mktemp) || die "can't make second temp file"
trap "rm -f $tf1 $tf2" 0 1 2 3 15

Now we get set 1 using git rev-list:

git rev-list <specifiers> | sort > $tf1

Then we get set 2, using git for-each-ref. Assuming we want annotated tags to resolve to their objects (usually a commit, but maybe another annotated tag, but if it's another annotated tag we'll let that tag resolve on its own), we get set 2 with:

git for-each-ref --format='%(object)' refs/tags | sort > $tf2

Note that this is %(object), not %(objectname); the latter would only get us the ID of the annotated tag object.

Finally, we want only commit IDs that are in both temp files, i.e., we want comm -12 which suppresses all but column 3 (lines that are in both files):

comm -12 $tf1 $tf2

The output of this script is the set of commits to show (in an order determined by sorting their commit hashes, so we'll rely on git log to fix the sort order). Now we just use your original git log command, but run it on the selected commits:

git log --no-walk --decorate=short --date-order $(script)

(this is all untested of course).

If git rev-list had a --stdin-intersect option we could do all this within Git and the shell:

git log --no-walk --decorate=short $( \
    git for-each-ref --format='%(object)' refs/tags | \
    git rev-list --stdin-intersect X..Y \
)

but it doesn't, hence the need for the little script.

Upvotes: 2

Related Questions