8

How can I get a list of branches that are first-level descendants of the current HEAD?

I can get a list of the whole tree with:

git log --graph --abbrev-commit --pretty=decorate --branches

which gives

* 2eff4a7... (refs/heads/issue-8351) Added a factory factory factory.
* 2e387aa... Refactored all of the factory factories.
| * b3fad52... (refs/heads/issue-8354) Moved the baz out of the constructor.
|/
| * f4cf3fe... (refs/heads/experimental) quuz looking good
| * 1d276b9... Risky fix to replace the quux with the quuz.
| * d6b0222... (refs/heads/issue-7559) Fixed the quux in the foo.
|/
| * 3f4cfff... (refs/heads/dev) wip
|/
* 76f493f... (refs/heads/master) SVN 30192

but I'd like to just get a plain list of the first-level children of the current branch, like this example for master:

dev
issue-7559
issue-8354
issue-8351

and if I was on branch issue-7559 I'd only see:

experimental

How might I go about this?

Greg Bacon
  • 134,834
  • 32
  • 188
  • 245
mskfisher
  • 3,291
  • 4
  • 35
  • 48
  • What exactly do you mean by first-level descendants? What branch and criteria would give the list of 4 branches that you give? – CB Bailey Feb 05 '10 at 17:15
  • Sorry, that example was for `master`. I'm looking to get a list of branches that have no other head between them and the parent. – mskfisher Feb 05 '10 at 17:23
  • Sounds a bit tricky. You could try using nested `git-for-each` loops to test whether the merge base for any other branch and the branch being tested is anything other than the current branch. It sounds quite an arbitrary thing to want though. Is there a bigger problem that you're trying to solve? – CB Bailey Feb 05 '10 at 17:29
  • Yes, it is arbitrary - I was exploring the possibility of organizing branches hierarchically. I'd also like a simplified topological view of the graph with just branch tips, but I couldn't find a good way to get `git log` to squash the commits. – mskfisher Feb 05 '10 at 17:36
  • 1
    @mskfisher: WRT last question, try `--simplify-by-decoration` – Jakub Narębski Feb 05 '10 at 18:51

1 Answers1

3

You could do it as follows.

First the usual preamble:

#! /usr/bin/perl

use warnings;
use strict;

Use git for-each-ref to gather the SHA-1 and name for each ref:

sub refs {
  open my $fh, "-|", "git", "for-each-ref",
                            "--format=%(objectname)\t%(refname:short)"
    or die "$0: failed to run git for-each-ref";

  my %ref2sha;
  while (<$fh>) {
    chomp;
    my($sha,$ref) = split /\t/;
    $ref2sha{$ref} = $sha;
  }

  \%ref2sha;
}

If a commit is a child of HEAD, the set of commits reachable from HEAD excluding everything reachable from the commit in question is the empty set. We can check this relationship with git rev-list.

sub is_child {
  my($ref) = @_;

  # git rev-list ^dev master
  my $refs = `git rev-list ^$ref HEAD -- 2>&1`;
  die "$0: git rev-list-failed.\n$refs" if $?;

  $refs !~ /\S/;
}

For each ref that is a descendant of HEAD but it not equivalent to HEAD, we examine the path from HEAD to that reference using git log. If the path contains the tip of another branch, the ref cannot be a first-level child.

All survivors of this gauntlet are first-level children.

chomp(my $head = `git rev-parse HEAD 2>&1`);
die "$0: git rev-parse failed.\n$head" if $?;

my $ref2sha = refs;
my %headsha = reverse %$ref2sha;

REF:
foreach my $ref (keys %$ref2sha) {
  my $refsha = $ref2sha->{$ref};

  next if $refsha eq $head || !is_child $ref;

  my @log = `git log --pretty=format:%H ..$ref 2>&1`;
  die "$0: git log failed.\n@log" if $?;
  for (@log) {
    chomp;
    next if $_ eq $refsha;
    next REF if exists $headsha{$_};
  }

  print $ref, "\n";
}
Greg Bacon
  • 134,834
  • 32
  • 188
  • 245