Find Git commits that contain multiple specific commits

General problem: Given a set of commits, how do I find the list of commits that have all those commits as ancestors, or relatedly, the first commit(s) that contain all those commits.

I can find branches (similarly tags) that contain the commits by looking for branches that are returned by git branch --contains <commit> for all the commits in the set, but git rev-list doesn’t have a --contains option. Effectively, I’m looking for a way of combining the regular --contains arguments with git rev-list, and limiting the output to commits that contain all the listed commits, not any one of them (which is how --contains works normally).

Specific example: Given commits a, b, c, how can I find the first commit that has all three commits in its ancestry?

For example, given the below tree, how do I find the commit marked X?

* (master)
a *
| |
b c

I assume there’s some magic I can do with git rev-list, and possibly involving the <commit1>...<commit2> notation, but I can’t work out further than that.

  • confusion about git rev-list
  • In my repo, how long must the longest hash prefix be to prevent any overlap?
  • What plumbing command provides the same functionality as git log --follow?
  • In git, list all tags since some tag
  • How to use git blame -S <revs-file>
  • Find all the direct descendants of a given commit
  • Finding large files in the history of a git repository fails
  • What should “git rev-list origin..HEAD” return?
  • 3 Solutions collect form web for “Find Git commits that contain multiple specific commits”

    I guess the answer to that question is that git was not made for this. Git really doesn’t like the idea of “children of a commit”, and there is a very good reason for that: it’s not very well defined. Because a commit doesn’t know a about its children it’s a very vague set. You might not actually have all the branches in your repo and so are missing some children.

    Gits internal storage structure also makes finding the children of a commit a rather expensive operation, as you have to walk the revision graph of all heads to either their corresponding roots or till you saw all the commits whose children you want to know about.

    The only concept of that kind that git supports is the idea of one commit containing another commit. But this feature is only supported by very few git commands (git branch being one of them). And where git supports it, it does not support it for arbitrary commits, but only branch heads.

    This all might seem like a rather harsh limitation of git, but in practice it turns out that you don’t need the “children” of a commit but usually only need to know which branches contain a specific commit.

    That all said: If your really want to get the answer to your question, you will have to write your own script that finds it. The easiest way to go by this is to start with the output of git rev-list --parents --reverse --all. Parsing that line by line, you would build a tree, and for each node mark whether it is a child of the commits you are looking for. You do this by marking the commits themselves once you meet them and then carrying that property on to all their children and so on.

    Once you have a commit that’s marked as containing all the commits, you add it to your “solution list” and mark all its children as dead – they can’t contain any first commits any more. This property will then also be passed on to all its descendants.

    You can save a bit of memory here if you don’t store any parts of the tree that don’t contain any of the commits you asked for.

    edit Hacked some python code

    #!/usr/bin/python -O
    import os
    import sys
    if len(sys.argv) < 2:
        print ("USAGE: {0} <list-of-revs>".format([sys.argv[0]]))
    rev_list = os.popen('git rev-list --parents --reverse --all')
    looking_for = os.popen('git rev-parse {0}'
                           .format(" ".join(sys.argv[1:]))).read().splitlines()
    solutions = set()
    commits = {}
    for line in rev_list:
        line = line.strip().split(" ")
        commit = set()
        sha = line[0]
        for parent in line[1:]:
            if not parent in commits:
            if parent in solutions:
        if sha in looking_for:
        if not "dead" in commit and commit.issuperset(looking_for):
        # only keep commit if it's a child of looking_for
        if len(commit) > 0:
            commits[sha] = commit
    print "\n".join(solutions)

    One possible solution:

    Use ‘git merge-base a b c’ to get the commit to use as the starting point in a call to rev-list; we’ll call it $MERGE_BASE.

    Use ‘git rev-list $MERGE_BASE..HEAD’ call to list all commits from their common ancestor to HEAD. Loop through this output (pseudocode):

    if commit == a || b || c
      $OLDEST_DESCENDANT = commit

    This will work for your example above, but will give a false positive if they have never been merged, were not merged in the commit immediately following the youngest of a,b,c, or if there were multiple merge commits to bring together a,b, and c (if they each resided on their own branch). There’s a bit of work left to find that oldest descendant.

    You then should follow the above with something starts with $OLDEST_DESCENDANT and proceeds backwards in the DAG from it towards HEAD (rev-list –reverse $OLDEST_DESCENDANT~..HEAD), testing to see that the output of ‘rev-list $MERGE_BASE~..$OLDEST contains all desired commits a, b, and c (maybe there’s a better way to test that they are reachable than rev-list, though).

    As twalberg mentions, testing the commits individually like this seems less than optimal and slow, but it’s a start. This approach has the advantage over his merge commit list method in that it will provide a valid response when all the input commits are on the same branch.

    Performance would be affected mostly by the distances between the merge base, head, X, and the youngest of the desired commit set (a, b, and c).

    How about :

    MERGE_BASE=`git merge-base A B C`
    git log $MERGE_BASE...HEAD --merges

    Assuming you have only 1 merge. Even if you have more merges, the oldest one is the one containing changes from all three commits

    Git Baby is a git and github fan, let's start git clone.