How do I find the next commit in Git? (child/children of ref)

Viewed 76041

ref^ refers to the commit before ref. What about getting the commit after ref?

For example, if I git checkout 12345, how do I check out the next commit?

Yes, Git's a DAG node pointer struct tree whatever. How do I find the commit after this one?

17 Answers

To list all the commits, starting from the current one, and then its child, and so on - basically standard git log, but going the other way in time, use something like

git log --reverse --ancestry-path 894e8b4e93d8f3^..master

where 894e8b4e93d8f3 is the first commit you want to show.

The creator of Hudson (now Jenkins), Kohsuke Kawaguchi published (November 2013): kohsuke / git-children-of:

Given a commit, find immediate children of that commit.

#!/bin/bash -e
# given a commit, find immediate children of that commit.
for arg in "$@"; do
  for commit in $(git rev-parse $arg^0); do
    for child in $(git log --format='%H %P' --all | grep -F " $commit" | cut -f1 -d' '); do
      git describe $child
    done
  done
done

As illustrated by this thread, in a VCS based on history represented by a DAG (Directed Acyclic Graph), there is not "one parent" or "one child".

        C1 -> C2 -> C3
      /               \
A -> B                  E -> F
      \               /
        D1 -> D2 ----/

The ordering of commits is done by "topo-order" or "date-order" (see the GitPro book).

But since Git 1.6.0, you can list the children of a commit.

git rev-list --children
git log --children

Note: for parent commits, you have the same issue, with the suffix ^ to a revision parameter meaning the first parent of that commit object. ^<n> means the <n>th parent (i.e. rev^ is equivalent to rev^1).

If you are on branch foo and issue "git merge bar" then foo will be the first parent.
I.e.: The first parent is the branch you were on when you merged, and the second is the commit on the branch that you merged in.

All of Git's internal arrows are one-way, pointing backwards. There is therefore no short convenient syntax for moving forwards: it's just not possible.

It is possible to "move against the arrows", but the way to do it is surprising if you haven't seen it before, and then obvious afterward. Let's say we have:

A <-B <-C <-D <-E   <-- last
        ^
        |
         \--------- middle

Using middle~2 follows the arrows twice from C back to A. So how do we move from C to D? The answer is: we start at E, using the name last, and work backwards until we get to middle, recording the points we visit along the way. Then we just move as far as we want in the direction of last: move one step to D, or two to E.

This is particularly important when we have branches:

          D--E   <-- feature1
         /
...--B--C   <-- master
         \
          F--G   <-- feature2

Which commit is one step after C? There's no correct answer until you add to the question: in the direction of feature___ (fill in the blank).

To enumerate the commits between C (excluding C) itself and, say, G, we use:

git rev-list --topo-order --ancestry-path master..feature2

The --topo-order makes sure that even in the presence of complex branching-and-merging, the commits come out in topologically-sorted order. This is only required if the chain isn't linear. The --ancestry-path constraint means that when we work backwards from feature2, we only list commits that have commit C as one of their own ancestors. That is, if the graph—or the relevant chunk of it anyway—actually looks like this:

A--B--C   <-- master
 \     \
  \     F--G--J   <-- feature2
   \         /
    H-------I   <-- feature3

Then a simple request of the form feature2..master enumerates commits J, G and I, and F and H in some order. With --ancestry-path we knock out H and I: they are not descendants of C, only of A. With --topo-order we make sure that the actual enumeration order is J, then G, then F.

The git rev-list command spills these hash IDs out on its standard output, one per line. To move one step forward in the direction of feature2, then, we just want the last line.

It's possible (and tempting and can be useful) to add --reverse so that git rev-list prints the commits in reversed order after generating them. This does work, but if you use it in a pipeline like this:

git rev-list --topo-order --ancestry-path --reverse <id1>...<id2> | head -1

to just get the "next commit in the direction of id2", and there is a very long list of commits, the git rev-list command can get a broken pipe when it tries to write to head which has stopped reading its input and exited. Since broken-pipe errors are normally ignored by the shell, this mostly works. Just make sure they're ignored in your usage.

It's also tempting to add -n 1 to the git rev-list command, along with --reverse. Don't do it! That makes git rev-list stop after walking one step back, and then reverse the (one-entry) list of commits visited. So this just produces <id2> every time.

Important side note

Note that with "diamond" or "benzene ring" graph fragments:

       I--J
      /    \
...--H      M--...  <-- last
      \    /
       K--L

moving one commit "forward" from H towards last will get you either I or K. There is nothing you can do about that: both commits are one step forward! If you then start from the resulting commit and go another step, you're now committed to whichever path you started on.

The cure for this is to avoid moving one step at a time and getting locked into path-dependent chains. Instead, if you plan to visit an entire ancestry-path chain, before doing anything else, make a complete list of all commits in the chain:

git rev-list --topo-order --reverse --ancestry-path A..B > /tmp/list-of-commits

Then, visit each commit in this list, one at a time, and you'll get the entire chain. The --topo-order will make sure you hit I-and-J in that order, and K-and-L in that order (though there's no easy way to predict whether you'll do the I-J pair before or after the K-L pair).

On the terminal:

$ git log --format='%H %P' --all --reflog | grep -F " [commit-hash]" | cut -f1 -d' '

Or in .gitconfig, section [alias]:

children = "!f() { git log --format='%H %P' --all --reflog | grep -F \" $1\" | cut -f1 -d' '; }; f"

This shows a list of all children of current HEAD in separate lines:

git rev-list --parents --all | awk -v h="$(git rev-parse HEAD)" 'index($0,h)>1{print$1}'

It just prints all commits that have HEAD as their parent.

You can speed it up a little bit, by putting ^HEAD before |, then the ancestors of HEAD will not be searched.

If you want to print children of another commit or branch, just put it in the place of HEAD (in the faster version in both HEAD places).

Each commit stores a pointer to its parent (parents, in case of a merge (standard) commit).

So, there isn't any way to point to a child commit (if there is one) from the parent.

I needed a command to rapidly checkout next commit.
I ended up with this alias:

[alias]
next = "!f() { CMIT=$(git log --ancestry-path --format=%H ${commit}..${1} | tail -1) && git checkout $CMIT; }; f"

You need to specify the branch you want to traverse, eg.:

git next master

You can add it to your .gitconfig file by directly editing it.

An example of its output:

~/home/myRepo | dd9e66ee  git next master
Previous HEAD position was dd9e66e Update README
HEAD is now at d71c74b Rename project; update to JUnit5

By running this command you go in detached head mode, of course :)

Related