How can I limit the log to all the descendants of

2019-04-30 07:44发布

问题:

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.

回答1:

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


标签: git git-log