Skip to content

Instantly share code, notes, and snippets.

@drewfish
Created August 2, 2012 23:31
Show Gist options
  • Save drewfish/3241982 to your computer and use it in GitHub Desktop.
Save drewfish/3241982 to your computer and use it in GitHub Desktop.
"git moo" (badly named) shows the relationships between branches that exist in multiple remotes
#!/usr/bin/env perl
# Show a nice table representing the differences between
# the local repo and remotes.
#
# It is assumed that there is a direct relationship between
# the local branch names and remote branch names.
#
# TODO
# * marker for current branch
# * marker if current branch is modified
# * read colors from .gitconfig
# * show remote HEADs?
#
use strict;
use warnings;
use Cwd;
use Getopt::Long;
my %OPTIONS;
# C=color, G=graphic
our $Cgrey = "\e[30;1m";
our $Cred = "\e[31m";
our $Cgreen = "\e[32m";
our $Cyel = "\e[33m";
our $Cblue = "\e[34m";
our $Cbblue = "\e[34;1m";
our $Cbold = "\e[37;1m";
our $Cend = "\e[0m";
our $Gew = "─"; # east-west
our $Gns = "│"; # north-south
our $Gnesw = "┼"; # north-east-south-west
our $Gtrack = "=";
sub git_commits_abbrev {
my $table = shift || die;
my $len = 6;
while ( 1 ) {
my %commits; # dedupe
foreach my $rowname ( keys %$table ) {
foreach my $colname ( keys %{$table->{$rowname}} ) {
my $commit = $table->{$rowname}{$colname}{'commit'};
next unless $commit;
$commits{$commit} = 1;
}
}
my %abbrevs;
foreach my $commit ( keys %commits ) {
my $abbrev = substr($commit, 0, $len);
$abbrevs{$abbrev}++;
}
if ( grep { $_ > 1 } values %abbrevs ) {
$len += 3;
next;
}
last;
}
foreach my $rowname ( keys %$table ) {
foreach my $colname ( keys %{$table->{$rowname}} ) {
my $commit = $table->{$rowname}{$colname}{'commit'};
next unless $commit;
$table->{$rowname}{$colname}{'commit'} = substr($commit, 0, $len);
}
}
}
sub git_distance {
my $refa = shift || die;
my $refb = shift || die;
return undef if $refa eq $refb;
my $log = `git rev-list --left-right --count $refa...$refb`;
return undef unless $log =~ m/^(\d+)\s+(\d+)/;
return {
'pos' => $1,
'neg' => $2,
};
}
sub table_print {
my $table = shift || die; # hashref row (branch) => colum (remote) => hashref details
my %colwidths; # column => max width;
$colwidths{'--ROWNAMES--'} = 0;
foreach my $rowname ( keys %$table ) {
next if $OPTIONS{'local-only'} and not $table->{$rowname}{'local'}{'commit'};
my $text = table_cell_format({ branch => $rowname });
my $len = length $text;
$colwidths{'--ROWNAMES--'} ||= 0;
$colwidths{'--ROWNAMES--'} = $len if $colwidths{'--ROWNAMES--'} < $len;
foreach my $colname ( keys %{$table->{$rowname}} ) {
my $text = table_cell_format($table->{$rowname}{$colname});
my $len = length $text;
$colwidths{$colname} ||= 0;
$colwidths{$colname} = $len if $colwidths{$colname} < $len;
$len = length $colname;
$colwidths{$colname} = $len if $colwidths{$colname} < $len;
}
}
# draw headers
my %row = map { $_ => { remote => $_ } } keys %colwidths;
table_row_print('--ROWNAMES--', \%row, \%colwidths);
# draw horizontal line
print $Cgrey, ($Gew x ($colwidths{'--ROWNAMES--'} + 2));
print $Gnesw, ($Gew x ($colwidths{'local'} + 2));
foreach my $col ( sort keys %colwidths ) {
next if $col eq '--ROWNAMES--';
next if $col eq 'local';
print $Gnesw, ($Gew x ($colwidths{$col} + 2));
}
print $Cend, "\n";
# draw rows
foreach my $rowname ( sort keys %$table ) {
next if $OPTIONS{'local-only'} and not $table->{$rowname}{'local'}{'commit'};
table_row_print($rowname, $table->{$rowname}, \%colwidths);
}
}
sub table_cell_format {
my $cell = shift || {}; # hashref
my $width = shift; # integer, also signifies to use color
my $text = '';
my $len = 0;
if ( $cell->{'remote'} ) {
$text = $cell->{'remote'};
$len = length $text;
if ( $width ) {
$text = "$Cbold$text$Cend";
}
}
if ( $cell->{'branch'} ) {
$text = $cell->{'branch'};
$len = length $text;
if ( $width and $cell->{'local'} ) {
$text = "$Cbblue$text$Cend";
}
}
if ( $cell->{'commit'} ) {
$text = $cell->{'commit'};
$len = length $text;
if ( $width and $cell->{'local'} ) {
$text = "$Cblue$text$Cend";
}
if ( $width and $cell->{'up-to-date'} ) {
$text = "$Cgrey$text$Cend";
}
if ( $cell->{'track'} ) {
my $deco = $Gtrack;
$len += length $deco;
if ( $width ) {
$deco = "$Cblue$Gtrack$Cend";
}
$text .= $deco;
}
if ( $cell->{'distance'} ) {
my $deco = ' ';
$deco .= '+' . $cell->{'distance'}{'pos'} if $cell->{'distance'}{'pos'};
$deco .= '-' . $cell->{'distance'}{'neg'} if $cell->{'distance'}{'neg'};
$len += length $deco;
if ( $width ) {
$deco = ' ';
$deco .= "$Cgreen+" . $cell->{'distance'}{'pos'} . $Cend if $cell->{'distance'}{'pos'};
$deco .= "$Cred-" . $cell->{'distance'}{'neg'} . $Cend if $cell->{'distance'}{'neg'};
}
$text .= $deco;
}
}
my $pad = '';
$pad = ' ' x ($width - $len) if $width;
return $text . $pad;
}
sub table_row_print {
my $rowname = shift || die; # string
my $row = shift || die; # hashref cols => details
my $colwidths = shift || die; # hashref cols => widths
my $cell = {
branch => $rowname,
local => $row->{'local'}{'commit'},
};
$cell = {} if '--ROWNAMES--' eq $rowname;
print ' ', table_cell_format($cell, $colwidths->{'--ROWNAMES--'}), ' ';
print "$Cgrey$Gns$Cend ", table_cell_format($row->{'local'}, $colwidths->{'local'}), ' ';
foreach my $colname ( sort keys %$colwidths ) {
next if $colname eq '--ROWNAMES--';
next if $colname eq 'local';
print "$Cgrey$Gns$Cend ", table_cell_format($row->{$colname}, $colwidths->{$colname}), ' ';
}
print "\n";
}
sub usage {
print "USAGE: git moo {options}\n";
print "Draws a table of local and remote branches and the relationships between them.\n";
print "\n";
print "OPTIONS\n";
print " -h\n";
print " show this help message\n";
print " --local-only or -l\n";
print " only show branches that exist locally\n";
print " --skip-remote {remote} or --sr {remote}\n";
print " don't show the specified remote\n";
print " option can be given multiple times\n";
print "\n";
exit 1;
}
sub main {
GetOptions(\%OPTIONS,
"help|h",
"local-only|l",
"skip-remote|sr=s@",
) or usage();
usage() if $OPTIONS{'help'};
my %table; # row (branch) => column (remote) => hashref details
my $log = `git for-each-ref --format='%(refname) %(objectname) %(upstream)'`;
foreach my $line ( split "\n", $log ) {
my($refname, $commit, $upstream) = split ' ', $line;
if ( $refname =~ m#^refs/heads/([^/]+)$# ) {
my $branch = $1;
$table{$branch}{'local'}{'commit'} = $commit;
$table{$branch}{'local'}{'local'} = 1;
if ( $upstream =~ m#^refs/remotes/([^/]+)/\Q$branch\E$# ) {
my $remote = $1;
$table{$branch}{$remote}{'track'} = 1;
}
next;
}
if ( $refname =~ m#^refs/remotes/([^/]+)/([^/]+)$# ) {
my $remote = $1;
my $branch = $2;
# TODO: show remote HEADs?
next if $branch eq 'HEAD';
$table{$branch}{$remote}{'commit'} = $commit;
if ( $table{$branch}{'local'}{'commit'} and $table{$branch}{'local'}{'commit'} eq $table{$branch}{$remote}{'commit'} ) {
$table{$branch}{$remote}{'up-to-date'} = 1;
}
next;
}
}
if ($OPTIONS{'skip-remote'}) {
foreach my $branch ( keys %table ) {
foreach my $remote ( keys %{$table{$branch}} ) {
if ( grep { $_ eq $remote } @{$OPTIONS{'skip-remote'}} ) {
delete $table{$branch}{$remote};
}
}
}
}
foreach my $branch ( keys %table ) {
next unless $table{$branch}{'local'}{'commit'};
foreach my $remote ( keys %{$table{$branch}} ) {
next if $remote eq 'local';
next unless $table{$branch}{$remote}{'commit'};
my $distance = git_distance($table{$branch}{$remote}{'commit'}, $table{$branch}{'local'}{'commit'});
$table{$branch}{$remote}{'distance'} = $distance if $distance;
}
}
git_commits_abbrev(\%table);
table_print(\%table);
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment