Given the history
X-Y <- feature
/
A-B-C-D-E <- master
I want to get the descendants of a given commit. One solution seems to be:
git log --all --ancestry-path <ref>^!
However, the behaviour is a bit strange:
- when
<rev>
is C
, the result is CDEXY
- when
<rev>
is D
or X
, the result is DEXY
(weird!)
- when
<rev>
is E
, the result is E
My understanding is that the command does not get all children of <rev>
; instead, it gets all children of parent-of(ref). Am I right?
This is unintuitive, error prone, and, quite frankly, irritating. How command should I run instead, in order to limit the log to the descendants of a given commit.
How to limit the log to all the descendants of a given revision
As far as I know, there is no built-in Git command to do that. However, you're almost there. Try
git log --all --ancestry-path ^<rev>
instead. That should limit the log to the descendants of <rev>
; note that, strictly speaking, <rev>
is not a child of itself, so it doesn't get listed.
For instance, in my toy repo (I replicated yours; see the bottom of my answer),
git log --all --ancestry-path ^D
limits the log to commit E
, and
git log --all --ancestry-path ^X
limits the log to commit Y
.
What's wrong with git log --all --ancestry-path D^!
?
TL; DR
My understanding is that the command does not get all children of <rev>
; instead, it gets all children of parent-of(ref). Am I right?
Yes; your bottom commit is off by one.
Details
Because, in your example, commits D
and X
are symmetric, let's just focus on commit D
and deconstruct the command
git log --all --ancestry-path D^!
According to the relevant Git man page,
A suffix ^
followed by an exclamation mark is the same as giving commit <rev>
and then all its parents prefixed with ^
to exclude them (and their ancestors).
Furthermore, according to the git-log
man page,
--all
Pretend as if all the refs in refs/ are listed on the command line as <commit>
.
Therefore, in your case
git log --all --ancestry-path D^!
is equivalent to
git log --ancestry-path D ^C feature master
Moreover, because D
is reachable from master
, the latter command reduces to
git log --ancestry-path ^C feature master
This gives a log of all the commits reachable from either feature
or master, but excluding C
or any of its ancestors, and you get commits D
, E
, X
, and Y
.
As you can see, your bottom commit is off by one. What you really want to run
git log --ancestry-path ^D feature master
which is the same as
git log --all --ancestry-path ^D
Test
The following commands recreate your toy repo:
$ mkdir gittest
$ cd gittest/
$ git init
$ printf "A\n" > README
$ git add README
$ git commit -m "A"
$ printf "B\n" >> README
$ git commit -am "B"
$ printf "C\n" >> README
$ git commit -am "C"
$ git branch feature
$ printf "D\n" >> README
$ git commit -am "D"
$ printf "E\n" >> README
$ git commit -am "E"
$ git checkout feature
$ printf "X\n" >> README
$ git commit -am "X"
$ printf "Y\n" >> README
$ git commit -am "Y"
$ git log --all --oneline --graph --decorate
* e234427 (HEAD -> feature) Y
* cf98c6b X
| * b3d493a (master) E
| * e2bb266 D
|/
* dfe0267 C
* 0be7d42 B
* 674356e A
(Note that commits D
and X
can be referred to by their SHAs, or more simply, by master~
and feature~
, respectively.)
The command you suggested (I've added the --oneline
flag, to reduce the output) indeed does not limit the log to the descendants of the given commit:
# master~ = D
$ git log --all --ancestry-path --oneline master~^!
e234427 Y
cf98c6b X
b3d493a E
e2bb266 D
# feature~ == X
$ git log --all --ancestry-path --oneline feature~^!
e234427 Y
cf98c6b X
b3d493a E
e2bb266 D
but the one I suggest does:
# master~ == D
$ git log --all --ancestry-path --oneline ^master~
b3d493a E
# feature~ == X
$ git log --all --ancestry-path --oneline ^feature~
e234427 Y