0

If I have a git tree that looks like

A---o---o---o
             \
              C---o---o---o---D
             /
    B---o---o

How do I find C given A, B and (if it helps) D? I guess this is somehow the opposite of git merge-base: that finds (roughly) the "latest common ancestor" of two commits, here I'm looking for the "earliest common descendant".

There could in theory be multiple candidates (e.g. if I added a commit directly merging A and B). But in this case there aren't, so I'm not worried about that situation.

philh
  • 614
  • 5
  • 18
  • 1
    `D` is not just helpful, it's *necessary*. Commits record only their parents, so from `A` it's impossible to "walk forward"; we have to walk backwards from something "ahead of" `A`. In any case finding `C` is a matter of finding the highest common commit, rather than lowest common ancestor, of paths in `git rev-list --ancestry-path A..D` and `git rev-list --ancestry-path B..D`. If you generate the lists in reverse topo order and find an LCA from that, you'll get the commit you want, but Git does not have this built in. – torek Sep 24 '18 at 15:35
  • 1
    Still, if the graph is simple enough, do the two `--ancestry-path --topo-order --reverse`s and the first commit that appears in both outputs is `C`. (You'll want to include revs `A` and `B` in these lists, in general, in case you have a degenerate graph where one of `A` or `B` *is* `C`.) – torek Sep 24 '18 at 15:36
  • "D is not just helpful, it's necessary. Commits record only their parents, so from A it's impossible to "walk forward"; we have to walk backwards from something "ahead of" A." Ah yeah, I feel a bit silly for not realising this. – philh Sep 26 '18 at 12:07

2 Answers2

2

One way would be to look at the list of commits A..D and B..D, and take the earliest line that appears in both lists.

You could use comm to keep the common lines of both histories

git log --ancestry-path --oneline A..D > /tmp/logA
git log --ancestry-path --oneline B..D > /tmp/logB
comm -12 /tmp/logA /tmp/logB | tail -1

One liner version :

comm -12 <(git log --ancestry-path --oneline A..D) <(git log --ancestry-path --oneline B..D) | tail -1
LeGEC
  • 29,595
  • 2
  • 37
  • 78
  • Yeah, I'd been hoping for something pre-existing, but this turned out to be easy enough to do. `diff -u – philh Sep 26 '18 at 11:59
1

Well this is certainly an odd thing to look for, I'd love to know your use case for doing this frequently enough that you're looking for a command for it.

Regardless, the "simplest" way would be to just move backward through the history from D until you find what you want. I'm sure this can be improved, but here's a shell script that does what you want.

#!/bin/sh

startingCommit=$1
target1=$2
target2=$3

#https://stackoverflow.com/a/8574392/3980115
containsElement () {
        local e match="$1"
        shift
        for e; do [[ "$e" == "$match" ]] && return 0; done
        return 1
}

currentCommit=$startingCommit
resultCommit=$startingCommit
result=1

while true ; do
        echo "Trying $currentCommit"
        #https://stackoverflow.com/a/11426834/3980115
        list=( $(git rev-list $currentCommit) )
        if (containsElement $target1 "${list[@]}" && containsElement $target2 "${list[@]}"); then
                resultCommit=$currentCommit
                currentCommit=${list[1]}
                result=0
                continue
        else
                break
        fi
done

if [ "$result" ] ; then
        echo "Earliest descendent: $resultCommit"
else
        echo "No common commit found"
fi

Invoked from the command line with commit SHAs as:

./getDecesdent.sh D A B

Output:

Earliest descendent: C

I have no idea if this would work in a more complicated branching scenario, but I suspect it may will not get the correct result if any of the commits between D and C are merges - I honestly don't know.

However, this will find the correct commit in the situation described in the question - just very slowly. This script works by using rev-list to get all available commits from a given revision, and slowly works its way back through the history until it finds a commit that can't reach both targets. The older the target commits are, the slower this will be.

Vlad274
  • 4,795
  • 1
  • 27
  • 40
  • Thanks for this. I didn't end up using it - I found a way to do it in one line of bash without saving a script - but I appreciate the effort. I'm not actually doing this frequently, it just seemed like something I wanted to be able to do quickly in case I wanted it again in future. It doesn't seem that unusual to me, though since I've not used it before I guess it kind of is. In this case I just had two commits that seemed like the merge between them would be tricky, and I wanted to take a look at the merge. – philh Sep 26 '18 at 12:06