If a Git alias is not prefixed with an exclamation point !
, Git tries to run the alias within itself, which requires that it not use any special shell features.
The shell features you are using are backquote expansion1 and piping (cmd1 | cmd2
). Thus, your particular alias requires the !
form.
I see the idea behind the alias; you can simplify this alias a bit. But you might also want to make it a bit more robust, which will require a small shell script (it can be embedded into the alias, although that makes it harder to read).
What next
does, and its slight defect (skip to the end if you already know)
The rev-spec HEAD..master
denotes commits reachable from master
that are not reachable from HEAD
. For instance, given a rather complex graph:
o--o--o--o <-- brA
/ / \
...--o--o--o--o---o--o <-- master
\
o--o--o <-- brB
\
o--o--o <-- brC
the sequence brA..master
gets you this subset (taken commits *
-ed, not-taken commits x
-ed, boring unrelated commits left o
):
x--x--x--x <-- brA
/ / \
...--x--x--x--*---*--* <-- master
\
o--o--o <-- brB
\
o--o--o <-- brC
and brB..master
gets you these:
*--*--*--* <-- brA
/ / \
...--x--*--*--*---*--* <-- master
\
x--x--x <-- brB
\
o--o--o <-- brC
Using --ancestry-path
trims the list of selected commits to just those that are descendents of the commit identified by the left-hand name (they must already be ancestors of the right hand name so this constraint has no effect). For the first case, brA..master
, this drops one *
-ed commit, which I will mark with !
here:
x--x--x--x <-- brA
/ / \
...--x--x--x--!---*--* <-- master
\
o--o--o <-- brB
\
o--o--o <-- brC
and that, indeed, takes you to the "next" commit in the direction of master
.
For the brB..master
case, however, --ancestry-path
removes all commits and the set becomes empty, because none of the *
commits is a descendent of the tip of brB
:
!--!--!--! <-- brA
/ / \
...--x--!--!--!---!--! <-- master
\
x--x--x <-- brB
\
o--o--o <-- brC
Of course, rather than a branch name, we just use HEAD
so that all this works with the detached-HEAD cases. But consider this graph as well, a sort of benzene ring of commits. I'll mark the current HEAD
commit with H
:
o--o
/ \
...--H o--o <-- master
\ /
o--o
In this case, HEAD
is the left edge of the "benzene ring". Then HEAD..master
selects all of the ring as well as the rightmost o
:
*--*
/ \
...--H *--* <-- master
\ /
*--*
Meanwhile, --ancestry-path
removes nothing at all: every *
-ed commit is in fact a descendant of H
. The next
alias will choose one of these at semi-random—let's assume it chooses the top one—and move to it:
H--o
/ \
...--o o--o <-- master
\ /
o--o
and at this point next
can never traverse the lower half of the ring, as those two commits are no longer descendants of HEAD
.
In other words, the slight defect is that this assumes there are no internal branch-and-merge sequences between HEAD
and the tip of master
. If there are such sequences, and we wish to visit them, we must2 record the original --ancestry-path
set of commit IDs once, up front, and then walk through them all (in whatever order we like, probably a reverse topological sort that allows us to jump back to the "lower half" of the benzene ring after traversing the "upper half", in our particular example).
Simplifying the alias
What git log ... | head -1 | cut ...
does is produce the list of commit IDs and log messages, extract just the first line (which reads commit <id>
, and extract just the ID from it.
We can do the same thing more simply with git rev-list
. We still need the head -1
(or equivalent) because --no-walk
or -n 1
gives us the commit ID of master
itself, rather than the next commit:
git rev-list --reverse --ancestry-path ..master | head -1
(we get to omit the word HEAD
too since that is the default).
If this produces no output, though, we probably should not run git checkout
. Hence:
id=$(git rev-list --reverse --ancestry-path ..master | head -1)
test -n "$id" && git checkout "$id" || echo 'no more commits'
which is a two-line script. To embed this into an alias is a bit tricky because of all the quoting needed. Instead, I'd just make it a two-line shell script; but if you want it as an alias:
[alias]
next = "!f() { id=$(git rev-list --reverse --ancestry-path ..master | head -1); \
test -n \"$id\" && git checkout $id || echo 'no more commits'; }; f"
Both of these could take the name of the target, rather than hardcoding master
. (But again I'd just dump all the commit IDs into a file and step through the file, with "next" and "prev" aliases, perhaps.)
1This is usually better written as $(command)
instead of `command`
since the parenthesized form nests and is easier to comprehend. Either way, it requires the shell, though.
2It's possible to just record the start and end points, and use a more complex algorithm to pick the next node based on where HEAD
is now and our known traversal order. For instance, suppose we have the start and end points. We can generate the full list into a file, then git rev-parse HEAD
and find where it is in the file, then step to the next one in the file and remove the file, which we won't need until we go to step forward again. But really, it's far easier to just dump the list into the file once, and then visit each entry one at a time without regenerating this file on each step.