Rules of thumb
<rev>~<n>
goes backward n parents from rev, selecting the first parent each time.
- Use
<rev>^<n>
to select the n-th immediate parent of merge commit rev.
- Use
~
most of the time — to go back a number of generations and always choosing the first parent of merge commits, commonly what you want.
- Use
^
on merge commits — because they have two or more immediate parents.
- In my experience, selecting a particular immediate parent of a merge commit by its index order, e.g.,
B^3
, is rare. It’s also error-prone. Just use a hash when you can.
Mnemonics:
- Tilde
~
is almost linear in appearance and wants to go backward in a straight line.
- Caret
^
suggests a merge commit: an interesting segment of a tree or a fork in the road.
Tilde
The “Specifying Revisions” section of the git rev-parse
documentation defines ~
as
<rev>~<n>
, e.g. HEAD~
, main~3
A suffix ~<n>
to a revision parameter means the commit object that is the nth generation ancestor of the named commit object, following only the first parents. For example, <rev>~3
is equivalent to <rev>^^^
which is equivalent to <rev>^1^1^1
…
You can get to parents of any commit anywhere in history. You can also move back through generations: for example, main~2
means the grandparent of the tip of the main branch, favoring the first parent on merge commits.
Caret
Git history is nonlinear: a directed acyclic graph (DAG) or tree. For a commit with only one parent, rev~
and rev^
mean the same thing. The caret selector becomes useful with merge commits because each one is the child of two or more parents — and strains language borrowed from biology.
HEAD^
means the first immediate parent of the tip of the current branch (main
in this example repository). HEAD^
is short for HEAD^1
, and you can also address HEAD^2
and so on as appropriate. The same section of the git rev-parse
documentation defines it as
<rev>^
, e.g. HEAD^
, v1.5.1^0
A suffix ^
to a revision parameter means the first parent of that commit object. ^<n>
means the nth parent ([e.g.] <rev>^
is equivalent to <rev>^1
). As a special rule, <rev>^0
means the commit itself and is used when <rev>
is the object name of a tag object that refers to a commit object.
Example from Git’s Documentation
Chain these specifiers or selectors arbitrarily, e.g., rev~3^2
in English is the second parent of the merge commit that is the great-grandparent (three generations back) of rev.
The aforementioned section of the git rev-parse
documentation traces many paths through a notional git history in which time flows generally downward. Commits D
, F
, B
, and A
are merge commits.
Here is an illustration, by Jon Loeliger. Both commit nodes B
and C
are parents of commit node A
. Parent commits are ordered left-to-right.
G H I J
\ / \ /
D E F
\ | / \
\ | / |
\|/ |
B C
\ /
\ /
A
A = = A^0
B = A^ = A^1 = A~1
C = A^2
D = A^^ = A^1^1 = A~2
E = B^2 = A^^2
F = B^3 = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2 = B^^2 = A^^^2 = A~2^2
I = F^ = B^3^ = A^^3^
J = F^2 = B^3^2 = A^^3^2
Run the code below to create a git repository whose history matches the illustration’s structure and makes A
the HEAD
commit. The resulting SHA-1 hashes on your machine will match the ones in this answer.
#! /usr/bin/env perl
use strict;
use warnings;
use subs qw/ postorder /;
use File::Temp qw/ mkdtemp /;
my %sha1;
my %parents = (
A => [ qw/ B C / ],
B => [ qw/ D E F / ],
C => [ qw/ F / ],
D => [ qw/ G H / ],
F => [ qw/ I J / ],
);
sub postorder {
my($root,$hash) = @_;
my @parents = @{ $parents{$root} || [] };
postorder($_, $hash) for @parents;
return if $sha1{$root};
@parents = map "-p $sha1{$_}", @parents;
chomp($sha1{$root} = `git commit-tree @parents -m "$root" $hash`);
die "$0: git commit-tree failed" if $?;
system("git tag '$root' '$sha1{$root}'") == 0 or die "$0: git tag failed";
}
$0 =~ s!^.*/!!; # / fix Stack Overflow highlighting
my $repo = mkdtemp "repoXXXXXXXX";
chdir $repo or die "$0: chdir: $!";
system("git init") == 0 or die "$0: git init failed";
system("git config user.name 'Git User'") == 0 or die "$0: user.name failed";
system("git config user.email 'git.user\@example.com'") == 0 or die "$0: user.email failed";
system("git config init.defaultBranch main") == 0 or die "$0: default branch failed";
$ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = 'Mon Oct 29 10:15:31 2018 +0900';
chomp(my $tree = `git write-tree`); die "$0: git write-tree failed" if $?;
postorder 'A', $tree;
system "git update-ref HEAD $sha1{A}"; die "$0: git update-ref failed" if $?;
system "git update-ref main $sha1{A}"; die "$0: git update-ref failed" if $?;
# for browsing history - http://blog.kfish.org/2010/04/git-lola.html
system "git config alias.lol 'log --graph --decorate --pretty=oneline --abbrev-commit'";
system "git config alias.lola 'log --graph --decorate --pretty=oneline --abbrev-commit --all'";
The aliases in the new throwaway repo for git lol
and git lola
allow you to view history as below. git lol
gives an ASCII-artish view of the current branch, and git lola
does the same for all branches. In action:
$ git lol
* 0a92ac1 (HEAD -> main, tag: A) A
|\
| * 00d2fb2 (tag: C) C
| |
| \
*-. | 75e7e8a (tag: B) B
|\ \|
| | * f3fe76f (tag: F) F
| | |\
| | | * c193a93 (tag: J) J
| | * 27c9a4e (tag: I) I
| * 63a46d4 (tag: E) E
* d209203 (tag: D) D
|\
| * ff7985a (tag: H) H
* 365f7d0 (tag: G) G
The output of git log
shows child commits before their parents — or with time generally flowing upward, opposite to how it’s presented in the illustration. To make the output match the illustration, we’d like to toss in --reverse
, but it isn’t compatible with --graph
. In this case, a quick hack to reverse the lines of output with tac
and flip the connectors in the graph makes time flow downward.
$ git lol | tr '.\\/' "'/\\\\" | tac
* 365f7d0 (tag: G) G
| * ff7985a (tag: H) H
|/
* d209203 (tag: D) D
| * 63a46d4 (tag: E) E
| | * 27c9a4e (tag: I) I
| | | * c193a93 (tag: J) J
| | |/
| | * f3fe76f (tag: F) F
|/ /|
*-' | 75e7e8a (tag: B) B
| /
| |
| * 00d2fb2 (tag: C) C
|/
* 0a92ac1 (HEAD -> main, tag: A) A
The tags allow you to address commits by the same names from the illustration and check your understanding with git describe
.
$ git describe A^
B
$ git describe A~^3~
I
$ git describe A~^3^2
J
$ git describe A^2~
F
We can even validate the illustration.
#! /usr/bin/env perl
use strict;
use warnings;
my $verbose = 0;
my $pass = 1;
while (<DATA>) {
chomp;
my($commit,@revs) = split /\s+=(?:\s+=)*\s+/;
my $result = "$commit:\n";
foreach my $rev (@revs) {
my $status = "OK";
chomp(my $describe = `git describe '$rev' 2>&1`);
$status = "FAIL - [$describe]" unless $describe eq $commit;
$result .= sprintf(" - %-7s (%s)\n", $rev, $status);
$pass = 0 unless $status eq "OK";
}
print $result if $verbose or $result =~ /FAIL/;
}
print $pass ? "PASS\n" : "FAIL\n";
__DATA__
A = = A^0
B = A^ = A^1 = A~1
C = A^2
D = A^^ = A^1^1 = A~2
E = B^2 = A^^2
F = B^3 = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2 = B^^2 = A^^^2 = A~2^2
I = F^ = B^3^ = A^^3^
J = F^2 = B^3^2 = A^^3^2
The “Specifying Revisions” in the git rev-parse
documentation is full of great information and is worth an in-depth read. See also Git Tools - Revision Selection from the book Pro Git.
Order of Parent Commits
In practice, treat parents’ order as arbitrary and query a merge commit’s metadata as below.
In our example repository, commit A
is a merge commit. We know this because it has multiple immediate parents. The Merge header in the output of git show A
displays its parents’ object names or hashes.
commit 0a92ac13d0abe16fc74d0b4c6fa67326fdbc112c
Merge: 75e7e8a 00d2fb2
Author: Git User <[email protected]>
Date: Mon Oct 29 10:15:31 2018 +0900
A
We can confirm the ordering by asking git rev-parse
to show the immediate parents of A
in sequence.
$ git rev-parse A^1 A^2
75e7e8acabbf65d63c0525d95fe529730771e7a2
00d2fb266838e6111e68ab3bbce5c0b9f1035ec9
Querying the non-existent third parent of A
results in an error.
$ git rev-parse A^3
A^3
fatal: ambiguous argument 'A^3': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
If you want to extract the parents only, use pretty format %P
for the full hashes
$ git log -1 --pretty=%P A
75e7e8acabbf65d63c0525d95fe529730771e7a2 00d2fb266838e6111e68ab3bbce5c0b9f1035ec9
or %p
for abbreviated hashes
$ git log -1 --pretty=%p A
75e7e8a 00d2fb2
or the ^@
suffix with git rev-parse
as in
$ git rev-parse A^@
75e7e8acabbf65d63c0525d95fe529730771e7a2
00d2fb266838e6111e68ab3bbce5c0b9f1035ec9