48

If you want to move the HEAD to the parent of the current HEAD, that's easy:

git reset --hard HEAD^

But is there any simple way to do the exact opposite of this operation, that is, set the head to the current head's first child commit?

Right now, I use gitk as a workaround (alt-tab, up-arrow, alt-tab, middle-click), but I would like a more elegant solution, one that can also be used when gitk is not available.

AttishOculus
  • 1,439
  • 1
  • 11
  • 18

11 Answers11

18

Very probably not the fastest possible solution, but it does what I need:

#!/bin/bash

REV=$1

if [[ -z "$REV" ]]; then
    echo "Usage: git-get-child <refspec> [<child-number>]"
    exit
fi

HASH=$(git rev-parse $REV)

NUM=$2

if [[ -z "$NUM" ]]; then
    NUM=1
fi

git rev-list --all --parents | grep " $HASH" | sed -n "${NUM}s/\([^ ]*\) .*$/\\1/p"

The git rev-list --all --parents does exactly what I need: it iterates over all reachable commits, and prints the following line for each:

SHA1_commit SHA1_parent1 SHA1_parent2 etc.

The space in the grep expression ensures that only those lines are found where the SHA1 in question is a parent. Then we get the nth line for the nth child and get the child's SHA1.

Michael
  • 8,362
  • 6
  • 61
  • 88
AttishOculus
  • 1,439
  • 1
  • 11
  • 18
  • I believe it should be `git rev-list --all --parents | grep -m 1 -B $(($NUM-1)) " $HASH" | head -1 | sed 's/ .*//'` else it doesn't quite work when $NUM != 1 – Schwern Feb 27 '10 at 03:08
  • 2
    See below for a potentially significant [performance improvement](http://stackoverflow.com/questions/1761825/referencing-the-child-of-a-commit-in-git/5353204#5353204) – Tyler Apr 07 '11 at 06:01
  • Note that this does *not* find dangling commits, i.e. commits no longer reachable from any branch. `git reset --hard HEAD^` will typically make the HEAD commit unreachable (unless it is also on a branch). So this may not find all commits. To find all commits, `git rev-list --walk-reflog` and/or `git fsck --unreachable` must be used. – sleske Sep 10 '12 at 09:38
  • 1
    should be `git rev-parse` and not `git-rev-parse` -- won't let me edit the post. – ben.snape Feb 13 '13 at 13:29
  • It saved my life. I accidentially merged testing into feature branch and it was a huge mess and not as easy as undoing the latest commit (because it merged tons of them). This command helped me untagle it. – Artem Novikov Oct 03 '22 at 16:34
13

The above method using git rev-list --all considers all available commits, which can be a lot and is often not necessary. If the interesting child commits are reachable from some branch, the number of commits that a script interested in child commits needs to process can be reduced:

branches=$(git branch --contains $commit| grep -v '[*] ('| sed -e 's+^..++')

will determine the set of branches that $commit is an ancestor of. With a modern git, at least version 2.21+, this should do the same without the need for sed (untested):

branches=$(git branch --format='%(refname:short)' --contains $commit| grep -v '[*] (')

Using this set, git rev-list --parents ^$commit $branches should yield exactly the set of all parent-child relationships between $commit and all branch heads that it is an ancestor of.

Rainer Blome
  • 561
  • 5
  • 15
  • 1
    This worked for me, however I did have to filter out the '* (no branch)' row, as I was at that point on a detached branch. For my repository this was about 10 times as fast, 0.13 s vs 1.4 seconds. – Paul Wagland Jan 20 '13 at 15:59
  • 1
    consider simplifying the output of `git branch` with `--format='%(refname:short)` (to remove the leading space and *) – Joshua Goldberg Nov 21 '18 at 19:35
  • 1
    zsh (but not bash) has trouble with $branches on the second commandline, because of the way the newlines are interpreted. Both bash and zsh work fine if branches is stored as an array: `branches=($(git branch --contains ... ))` – Joshua Goldberg Nov 21 '18 at 19:42
  • @Paul Wagland: Good catch. Added `grep` invocation to get rid of such rows, and rows like `* (HEAD detached at $SHA1)`. @Joshua Goldberg: Added `sed` get rid of leading `* `, if any. `--format` is not available on my LTS system, but good to know. Added your suggestion for those fortunate to have it. Regarding zsh vs. bash, I prefer to limit my scripts to what POSIX sh has to offer, thanks for suggesting a solution for zsh users. And thanks for linkifying the reference to the "above" answer. – Rainer Blome Mar 21 '19 at 17:07
6

Based partly on Paul Wagland's answer and partly on his source, I am using the following:

git log --ancestry-path --format=%H ${commit}..master | tail -1

I found that Paul's answer gave me the wrong output for older commits (possibly due to merging?), where the primary difference is the --ancestry-path flag.

Community
  • 1
  • 1
Michael
  • 8,362
  • 6
  • 61
  • 88
4

Based on the answer given in How do I find the next commit in git?, I have another solution that works for me.

Assuming that you want to find the next revision on the "master" branch, then you can do:

git log --reverse ${commit}..master | sed 's/commit //; q'

This also assumes that there is one next revision, but that is kind of assumed by the question anyway.

Community
  • 1
  • 1
Paul Wagland
  • 27,756
  • 10
  • 52
  • 74
4

You can use gitk ... since there can be more than one child there is probably no easy way like HEAD^.

If you want to undo your whole operation you can use the reflog, too. Use git reflog to find your commit’s pointer, which you can use for the reset command. See here.

tanascius
  • 53,078
  • 22
  • 114
  • 136
4

To just move HEAD (as asked - this doesn't update the index or working tree), use:

git reset --soft $(git child)

You'll need to use the configuration listed below.

Explanation

Based on @Michael's answer, I hacked up the child alias in my .gitconfig.

It works as expected in the default case, and is also versatile.

# Get the child commit of the current commit.
# Use $1 instead of 'HEAD' if given. Use $2 instead of curent branch if given.
child = "!bash -c 'git log --format=%H --reverse --ancestry-path ${1:-HEAD}..${2:\"$(git rev-parse --abbrev-ref HEAD)\"} | head -1' -"

It defaults to giving the child of HEAD (unless another commit-ish argument is given) by following the ancestry one step toward the tip of the current branch (unless another commit-ish is given as second argument).

Use %h instead of %H if you want the short hash form.

With a detached head, there is no branch, but getting the first child can still be achieved with this alias:

# For the current (or specified) commit-ish, get the all children, print the first child 
children = "!bash -c 'c=${1:-HEAD}; set -- $(git rev-list --all --not \"$c\"^@ --children | grep $(git rev-parse \"$c\") ); shift; echo $1' -"

Change the $1 to $* to print all the children

Michael
  • 8,362
  • 6
  • 61
  • 88
Tom Hale
  • 40,825
  • 36
  • 187
  • 242
  • this doesn't work if you are on the wrong branch. Any way to generalize this? – TamaMcGlinn Feb 06 '21 at 11:40
  • weird; your `children` alias does work if you are not on the same branch, but fails to print more than one commit for a commit with multiple child commits. – TamaMcGlinn Feb 06 '21 at 11:44
2

It depends on what you're asking. There could be an infinite number of children of the current head in an infinite number of branches, some local, some remote, and many that have been rebased away and are in your repository, but not part of a history you intend to publish.

For a simple case, if you have just done a reset to HEAD^, you can get back the child you just threw away as HEAD@{1}.

Dustin
  • 89,080
  • 21
  • 111
  • 133
2

You can use the gist of the creator for Hudson (now Jenkins) Kohsuke Kawaguchi (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

Put that script in a folder referenced by your $PATH, and simply type:

git children-of <a-commit>
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
1

This answer is not an absolute answer but it is very useful for fast researches.

  • git log from the beginning
  • each line is a abbrev commit with --oneline
  • add optionnaly --graph for multiple ancestors.
  • pipe |
  • step back of n (here: 3) ancestries with --before-context=n (same as -B n) of grep searching an abbrev commit (7 characters)
git log --oneline HEAD | grep -B 3 <an-abbrev-commit>
phili_b
  • 885
  • 9
  • 27
  • 1
    This is what I came up with as well. One nice thing about it: it's super simple and easy to understand. – user98761 Nov 22 '22 at 22:31
0

This post (http://www.jayway.com/2015/03/30/using-git-commits-to-drive-a-live-coding-session/#comment-282667) shows a neat way if doing it if you can create a well defined tag at the end of your commit stack. Essentially git config --global alias.next '!git checkout `git rev-list HEAD..demo-end | tail -1`' where "demo-end" is the last tag.

Alex Dresko
  • 5,179
  • 3
  • 37
  • 57
-2

It is strictly not possible to give a good answer -- since git is distributed, most of the children of the commit you ask about might be in repositories that you don't have on your local machine! That's of course a silly answer, but something to think about. Git rarely implements operations that it can't implement correctly.

u0b34a0f6ae
  • 48,117
  • 14
  • 92
  • 101