How can I tell if one commit is a descendant of another commit?
Asked Answered
N

8

207

With Git, how can I tell if one commit in my branch is a descendant of another commit?

Nolitta answered 9/6, 2010 at 11:35 Comment(2)
Same question asked the opposite: #18345657Nevlin
Could you change your accepted answer? The majority likes the --is-ancestor solution.Heptamerous
I
63

If you want to check this programmatically (e.g. in script), you can check if git merge-base A B is equal to git rev-parse --verify A (then A is reachable from B), or if it is git rev-parse --verify B (then B is reachable from A). git rev-parse is here needed to convert from commit name to commit SHA-1 / commit id.

Using git rev-list like in VonC answer is also possibility.

Edit: in modern Git there is explicit support for this query in the form of git merge-base --is-ancestor.


If one of commits you are asking about is a branch tip, then git branch --contains <commit> or git branch --merged <commit> might be better non-programmatic solution.

Ignacioignacius answered 9/6, 2010 at 13:25 Comment(6)
Possibly the fastest way would be to git checkout -b quickcheck <more-recent-commit-ID> and then git branch --contains <older-commit-ID> (and then git branch -D quickcheck to get rid of the temporary branch).Fabien
Two possible approaches, both of them much worse than the one in @MattR's answer.Bajaj
@jwg: MattR answer is better, but this answer (and probably it being accepted) predates git 1.8.0 and git merge-base --is-ancestor by 2 years.Bilbo
@JakubNarębski Fair enough, sorry.Bajaj
In a large repository (2 million commits), I compared the speed of git branch --contains <commit> and git merge-base --is-ancestor ...: 3m40s vs 0.14sVacua
@JakubNarębski you're entirely right, but these days I think people would want to discover the newer option. Maybe you can edit your answer to incorporate the 1.8.0+ solution?Mycorrhiza
K
348

From Git 1.8.0, this is supported as an option to merge-base:

git merge-base --is-ancestor <maybe-ancestor-commit> <descendant-commit>

From the man page:

--is-ancestor

Check if the first is an ancestor of the second , and exit with status 0 if true, or with status 1 if not. Errors are signaled by a non-zero status that is not 1.

For example:

git merge-base --is-ancestor origin/master master; echo $?
Kalgan answered 23/11, 2012 at 9:54 Comment(6)
Nice! Here's a shell script that wraps this answer up in something with human-reabable output: gist.github.com/simonwhitaker/6354592Purse
In other words: git merge-base THING --is-ancestor OF_THING && echo yes || echo no e.g.: git merge-base my-feature-branch --is-ancestor master && echo yes || echo noLeucas
@smarber git merge-base --is-ancestor -- commit commit works for hashes at my side with git 2.1.4 (Debian/Devuan 7.10 jessie) and 1.9.1 (Ubuntu 14.04 trusty) which are rather ancient now. It works even for Debian wheezy, if you do sudo apt-get install git/wheezy-backports.Faith
Why does git bash just return nothing without the echo yes || echo no?String
@String it is returning something, it's just not printing anything to stdout. If you're familiar with C, it's the same difference as between a return 0 and a printf("All good"). The first is meant for the calling function (so you can do an if with it, for instance), the second is meant for the user. Because git merge-base is foremost meant for use inside a script, it is up to the user to wrap the printing part around it.Rosenquist
Note that --is-ancestor exits with exit code 0 if both commits are identical. For example, git merge-base --is-ancestor HEAD HEAD has exit code 0.Locomotor
I
63

If you want to check this programmatically (e.g. in script), you can check if git merge-base A B is equal to git rev-parse --verify A (then A is reachable from B), or if it is git rev-parse --verify B (then B is reachable from A). git rev-parse is here needed to convert from commit name to commit SHA-1 / commit id.

Using git rev-list like in VonC answer is also possibility.

Edit: in modern Git there is explicit support for this query in the form of git merge-base --is-ancestor.


If one of commits you are asking about is a branch tip, then git branch --contains <commit> or git branch --merged <commit> might be better non-programmatic solution.

Ignacioignacius answered 9/6, 2010 at 13:25 Comment(6)
Possibly the fastest way would be to git checkout -b quickcheck <more-recent-commit-ID> and then git branch --contains <older-commit-ID> (and then git branch -D quickcheck to get rid of the temporary branch).Fabien
Two possible approaches, both of them much worse than the one in @MattR's answer.Bajaj
@jwg: MattR answer is better, but this answer (and probably it being accepted) predates git 1.8.0 and git merge-base --is-ancestor by 2 years.Bilbo
@JakubNarębski Fair enough, sorry.Bajaj
In a large repository (2 million commits), I compared the speed of git branch --contains <commit> and git merge-base --is-ancestor ...: 3m40s vs 0.14sVacua
@JakubNarębski you're entirely right, but these days I think people would want to discover the newer option. Maybe you can edit your answer to incorporate the 1.8.0+ solution?Mycorrhiza
A
17

This kind of operations relies on the notion of range of revisions detailed in the SO question: "Difference in ‘git log origin/master’ vs ‘git log origin/master..’".

git rev-list should be able to walk back from a commit, up until another if reachable.

So I would try:

git rev-list --boundary 85e54e2408..0815fcf18a
0815fcf18a19441c1c26fc3495c4047cf59a06b9
8a1658147a460a0230fb1990f0bc61130ab624b2
-85e54e240836e6efb46978e4a1780f0b45516b20

(Boundary commits are prefixed with -)

If the last commit displayed is the same than the first commit in the git rev-list command, then it is a commit reachable from the second commit.

If the first commit is not reachable from the second, git rev-list should return nothing.

git rev-list --boundary A..B

would finish by A, if A is reachable from B.
It is the same as:

git rev-list --boundary B --not A

,with B a positive reference, and A a negative reference.
It will starts at B and walks back through the graph until it encounters a revision that is reachable from A.
I would argue that if A is directly reachable from B, it will encounter (and display, because of the --boundary option) A itself.

Audi answered 9/6, 2010 at 13:10 Comment(7)
This sounds like a common-enough use-case that I'm surprised that git hasn't yet published a "porcelain" command that does exactly this.Neils
@lsiden: true. Side-note: don't forget that to check something programmatically, you aren't supposed to use porcelain command, (as in #6976973), but plumbing commands (as illustrated in #3879124)Audi
Oh, man, it looks like I have to go back and work on my Shell scripting chops!Neils
question: why does the -85e54e2... in the snippet have a minus? also a possible typo: "... is the same than the first commit ..."Hepsiba
@Hepsiba - means it is a boundary commit. I have edited the answer to make that clearer, as well as to refresh doc links and fix the typo for this 5 years-old answer.Audi
>If the first commit is not reachable from the second, git rev-list should return nothing. then, first commit is ascendent or descendent?Openair
@Openair not descendant. You would need to check if the second commit is reachable from the first to know if the first commit is ascendant. If not, the first and second commit are not related at all.Audi
D
13

Another way would be to use git log and grep.

git log --pretty=format:%H abc123 | grep def456

This will produce one line of output if commit def456 is an ancestor of commit abc123, or no output otherwise.

You can usually get away with omitting the --pretty argument, but it is needed if you want to make sure that you only search through actual commit hashes and not through log comments and so on.

Dashed answered 22/9, 2011 at 3:46 Comment(2)
I expected this solution to be slow, but it is actually quite fast, even for a project with 20k+ commitsGenevagenevan
instead of --pretty I use --oneline: git log --oneline ce2ee3d | grep ec219cc works greatBattleship
A
3

https://mcmap.net/q/22488/-how-can-i-tell-if-one-commit-is-a-descendant-of-another-commit mentions it, now to make it more human friendly:

git-is-ancestor() (
  if git merge-base --is-ancestor "$1" "$2"; then
      echo 'ancestor'
  elif git merge-base --is-ancestor "$2" "$1"; then
      echo 'descendant'
  else
      echo 'unrelated'
  fi
)
alias giia='git-is-ancestor'
Adena answered 29/9, 2016 at 14:57 Comment(1)
this will return 'unrelated' if both parameters point to the same commitWaver
A
2

If you are using git merge-base --is-ancestor, make sure to use Git 2.28 (Q3 2020)

With Git 2.28 (Q3 2020), a few fields in "struct commit" that do not have to always be present have been moved to commit slabs.

See commit c752ad0, commit c49c82a, commit 4844812, commit 6da43d9 (17 Jun 2020) by Abhishek Kumar (abhishekkumar2718).
(Merged by Junio C Hamano -- gitster -- in commit d80bea4, 06 Jul 2020)

commit-graph: introduce commit_graph_data_slab

Signed-off-by: Abhishek Kumar

The struct commit is used in many contexts. However, members generation and graph_pos are only used for commit-graph related operations and otherwise waste memory.

This wastage would have been more pronounced as we transition to generation number v2, which uses 64-bit generation number instead of current 32-bits.

As they are often accessed together, let's introduce struct commit_graph_data and move them to a commit_graph_data slab.

While the overall test suite runs just as fast as master, (series: 26m48s, master: 27m34s, faster by 2.87%), certain commands like git merge-base --is-ancestor were slowed by 40% as discovered by Szeder Gábor.
After minimizing commit-slab access, the slow down persists but is closer to 20%.

Derrick Stolee believes the slow down is attributable to the underlying algorithm rather than the slowness of commit-slab access and we will follow-up in a later series.

Audi answered 19/7, 2020 at 2:3 Comment(1)
By far the fastest way available. Especially if a commit-graph is present!Nadene
A
1

git show-branch branch-sha1 commit-sha1

Where:

  • branch-sha1: the sha1 in your branch you want to check
  • commit-sha1: the sha1 of the commit you want to check against
Ambroid answered 11/11, 2015 at 16:19 Comment(0)
E
-1

Building up on itub's answer, in case you need to do this for all the tags in the repository:

for i in `git tag` ; do echo -ne $i "\t" ; git log --pretty=format:%H $i | (grep <commit to find> || echo ""); done
Exhibitive answered 19/1, 2015 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.