#!/usr/bin/perl
#
# Support .gitslave files, recursive processing of git commands into slave directories
#
# ++Copyright LIBBK++
#
# Copyright (c) 2003 The Authors. All rights reserved.
#
# This source code is licensed to you under the terms of the file
# LICENSE.TXT in this release for further details.
#
# Mail <projectbaka@baka.org> for further information
#
# --Copyright LIBBK--
#
use strict;
use warnings;
no warnings 'uninitialized';
no warnings 'io';
use Config;
use Cwd;
use File::Spec;
use constant GITSLAVE => '.gitslave';
use Getopt::Long;
use File::Path;
use File::Basename;

my $eval_errs = '';
my ($want_parallel) = (0);
eval 'use Parallel::Iterator;';
if ($@)
{
  undef($want_parallel);
  my $err = $@;
  $err =~ s/\(\@INC [^)]*\)//g;
  $err =~ s/ at .* line \d+\.//g;
  $eval_errs .= $err;
}
my ($want_progress,$progress_count) = (0,0);
eval 'use Term::ProgressBar;';
if ($@)
{
  undef($want_progress);
  my $err = $@;
  $err =~ s/\(\@INC [^)]*\)//g;
  $err =~ s/ at .* line \d+\.//g;
  $eval_errs .= $err;
}
my ($progress,$missingcount,$foundcount);

our($GITSLAVE) = $ENV{'GITSLAVE'}||GITSLAVE;
our($GITS_DIR);
my($TOP);
our(@slaves);
my($USAGE) = "Usage: gits [-p|--parallel COUNT] [-v|--verbose]+ [--quiet] [--help] [--version]\n".
"\t[-n|--no-pager] [--paginate] [--eval-args] [--exclude SLAVE-REGEXP]\n".
"\t[--keep-going] [--no-commit] [--no-hide] [--no-master] [--no-progress]\n".
"\t[--with-ifpresent] [-- GIT-OPTIONS...] SUBCOMMAND [ARGS]...\n";
our(%OPTIONS);
my ($returncode) = 0;
my $pagination;
delete($ENV{'GIT_DIR'});
our($fromcheckout);
our($git) = 'git';

# Some people are stupid enough to set CDPATH and that screws up the formatting
delete($ENV{'CDPATH'});



######################################################################
#
# Translate wait status ($?) into human-readable terms
#
sub exitcode(;$)
{
  my ($ret) = @_;
  $ret = $? if !defined($ret);

  my $sig = $ret & 127;

  # shell exits with 128 + signal for child killed by signal
  $sig = ($ret >> 8) - 128 if (!$sig && ($ret >> 8) > 128);

  if ($sig)
  {
    my $signame = (split(' ', $Config{sig_name}))[$sig];
    return "killed by SIG" . $signame . " (signal " . $sig . ")";
  }

  return "exit " . ($ret >> 8);
}



######################################################################
#
# Perform substitution of path and directory components
#
sub gitsubst($$)
{
  my ($data,$slave) = @_;

  my ($pattern) = quotemeta('%%dir%%');
  if ($data =~ /$pattern/)
  {
    $data =~ s/$pattern/$slave/g;
  }

  $pattern = quotemeta('%%path%%');
  if ($data =~ /$pattern/)
  {
    my $subst = Cwd::realpath($GITS_DIR."/".$slave);
    $data =~ s/$pattern/$subst/g;
  }

  $pattern = quotemeta('%%basename%%');
  if ($data =~ /$pattern/)
  {
    my $subst = basename(Cwd::realpath($GITS_DIR."/".$slave));
    $data =~ s/$pattern/$subst/g;
  }

  $data;
}



######################################################################
#
# Run a command where we don't care about the output
#
sub docmd($)
{
  my ($cmd) = @_;

  if ($OPTIONS{'quiet'})
  {
    $cmd = "{ $cmd ; } 2>/dev/null";
  }
  elsif ($pagination)
  {
    $cmd = "{ $cmd ; } 2>&1";
  }
  print STDERR "Running command: `$cmd`\n" if ($OPTIONS{'verbose'} > 1);
  system($cmd);
  print STDERR "Command " . exitcode() . "\n" if ($OPTIONS{'verbose'} > 2);
  $? == 0;
}



######################################################################
#
# Run a command, retrieving the output
#
sub getcmd($;$)
{
  my ($cmd,$slave) = @_;

  $cmd = gitsubst($cmd,$slave) if ($slave);

  $cmd = "{ $cmd ;} 2>&1";
  print STDERR "Running command: `$cmd`\n" if ($OPTIONS{'verbose'} > 1);
  my $out = `$cmd`;
  # strip out progress messages (e.g. "Writing objects:  94% (47/50)")
  $out =~ s/^.*\d% .*\r(?!\n)//mg;
  if ($OPTIONS{'verbose'} > 2)
  {
    my $tout = $out;
    chomp($tout);
    $tout =~ s/\n/|\n |/g;
    print STDERR "Command " . exitcode() . "\n |$tout|\n";
  }

  wantarray?($?, $out):$out;
}



######################################################################
#
# Find the repository URL for this remote
#
sub find_remote_repo($$)
{
  my ($remote,$base) = @_;

  my ($ret, $master_repo) = getcmd(qq^cd "$base" && $git config "remote.$remote.url"^);
  chomp($master_repo);
  die "Could not find git upstream clone which this is relative to ($base $remote)\n" unless ($ret == 0 && $master_repo);
  $master_repo;
}




######################################################################
#
# Find the absolute path to repository (relatives are relative to upstream repository)
#
# We need to handle the following schemes:
# scheme://domain/~user
# scheme://domain/~user/dir
# scheme://domain/path
# scheme://domain:port/path
# scheme://username:password@domain:port/path
#
# where scheme == http ssh git git+ssh
# where /path == /path/from/root/to/repo
# where /path == /repo
# where /path == /~user
# where /path == /~user/repo
# domain:path
#
# username@domain:path
#
# where path == repo
# where path == /path/to/repo
# where path == path/to/repo
# where path == ~repo
# where path == ~user/repo
#
# Note that git currently cannot clone from the following directory:
# /tmp/foo/bar:bar/baz Any : will cause the system to treat it like a
# hostname or URL scheme as above.
#
# The relative URL schemes we support include:
# ../lib1
# ^/src/repos/lib1
#
sub resolve_repository($$;$$$)
{
  my ($relrepos,$relative,$remote,$absrepos,$localbase) = @_;

  $remote = "origin" unless ($remote);

  $fromcheckout = get_fromcheckout($remote) unless defined($fromcheckout);

  $localbase = "." if (!$localbase || $fromcheckout);

  if ($fromcheckout)
  {
    $relrepos = $relative;
  }

  # Check for already absolute
  return $relrepos if ($relrepos =~ m-^(/|.+?:)-);

  # Find upstream origin URL
  $absrepos = find_remote_repo($remote,$localbase) if (!$absrepos || $localbase ne ".");

  # Nuke trailing .git, if necessary
  $absrepos =~ s:/\.git$::;

  ##################################################
  # Break into method and path sections
  my ($rprefix, $rsuffix) = ("", $absrepos);
  my ($lprefix, $lsuffix) = ("", $relrepos);

  if ($absrepos =~ /(.+?:)(.*)/)
  {
    $rprefix = $1;
    $rsuffix = $2;

    # If this is scheme://hostish/ move /hostish into prefix.
    if ($rsuffix =~ s:^(//[^/]*)/:/:)
    {
      $rprefix .= $1;
    }
  }

  if ($relrepos =~ /(.+?:)(.*)/)
  {
    $lprefix = $1;
    $lsuffix = $2;

    # If this is scheme://hostish/ move /hostish into prefix.
    if ($lsuffix =~ s:^(//[^/]*)/:/:)
    {
      $lprefix .= $1;
    }
  }

  # At this point, prefix has all networkish components, and suffix
  # has all hostish location components.  We don't know if suffix is
  # absolute or "relative" at this point.

  # Handle ^/src/repos/lib1 case
  if ($lsuffix =~ m:^\^(.*):)
  {
    return (($lprefix || $rprefix).$1);
  }

  my $newpath = "$rsuffix/$lsuffix";

  # Handle "xxx//yyy" -> "xxx/yyy"
  while ($newpath =~ s://:/:) {;}

  # Handle "xxx/./yyy" -> "xxx/yyy"
  while ($newpath =~ s:/\./:/:) {;}
  # Handle "./xxx" -> "xxx"
  while ($newpath =~ s:^\./::) {;}
  # Handle "/." -> "/"
  while ($newpath =~ s:^/\.$:/:) {;}
  # Handle "xxx/." -> "xxx"
  while ($newpath =~ s:/\.$::) {;}

  # Handle "/xxx/../yyy" -> "/yyy"
  # Handle "xxx/../yyy" -> "yyy"
  # Handle "/xxx/.." -> "/"
  # Handle "xxx/.." -> ""
  while ($newpath =~ s:(^ | /) [^/]+ / \.\. ($ | /):$1:x) {;}

  $relrepos = ($lprefix || $rprefix).$newpath;
}



######################################################################
#
# check whether command requires slave configuration
#
sub ephemeral_command($;$)
{
  my ($cmd,$depth) = @_;

  if ($cmd eq "prepare" or $cmd eq "resolve" or $cmd eq "clone" or $cmd eq "help" or ($depth && $cmd eq "populate"))
  {
    $missingcount++;
    return 1;
  }
  return 0;
}



######################################################################
#
# Recursive slave list population
#
sub git_slave_list_recursive($$$$$$$);
sub git_slave_list_recursive($$$$$$$)
{
  my ($file,$gitsp,$seenp,$cmd,$base,$vpresentp,$depth) = @_;

  my ($r);

  $foundcount++;

  if (!open($r, "<", $file))
  {
    die "Could not find $file file\n"
      unless (!defined($cmd) or ephemeral_command($cmd,$depth));
    return;
  }
  my ($lineno) = 0;
  while (<$r>)
  {
    $lineno++;
    if (/^\#include\s+\"([^\"]+)\"\s*(\sifpresent)?$/)
    {
      my ($slavefile,$flags) = ($1,$2);
      $slavefile = "$base/$slavefile" unless ($slavefile =~ m:^/:);
      my $newbase = $slavefile;
      $newbase =~ s:^(.*)/.*:$1:;
      if ($flags =~ /ifpresent/ && !$OPTIONS{'with-ifpresent'})
      {
	next unless (-f $slavefile);
      }
      git_slave_list_recursive($slavefile,$gitsp,$seenp,$cmd,$newbase,$vpresentp,$depth+1);
      next;
    }
    next if (/^\s*(?:\#.*)$/);
    next if ($OPTIONS{'exclude'} && /$OPTIONS{'exclude'}/);
    die "Bad line $lineno in $file\n" unless (/^\"(.*)\" \"(.*)\"( ifpresent)?$/);
    my ($baserel,$ckoutrel,$flags) = ($1,$2,$3);

    if ($flags !~ /ifpresent/ || $OPTIONS{'with-ifpresent'} || -d "$base/$ckoutrel/." || $vpresentp->{$ckoutrel})
    {
      my ($relbase) = $base;
      my ($qbase) = quotemeta($GITS_DIR);
      $relbase =~ s:^$qbase/*::;
      $relbase = "$relbase/" if ($relbase);

      $ckoutrel = "$relbase$ckoutrel";

      push(@$gitsp,[$baserel, $ckoutrel, $base]) if (!$seenp->{$ckoutrel});
      $seenp->{$ckoutrel} = 1;
    }
  }
  close($r);
}



######################################################################
#
# Get list of currently configured slaves
#
sub git_slave_list($;@)
{
  my ($cmd,@virtualpresent) = @_;
  my (@gits);
  my (%seen);
  my (%vpresent);
  grep($vpresent{$_}=1,@virtualpresent);

  $missingcount = $foundcount = 0;

  foreach my $slavefile (split(/, /,$GITSLAVE))
  {
    $slavefile = "$GITS_DIR/$slavefile" unless ($slavefile =~ m:^/:);
    git_slave_list_recursive($slavefile,\@gits,\%seen,$cmd,$GITS_DIR,\%vpresent, 0);
  }
  @gits;
}



######################################################################
#
# Configure persistent fromcheckout
#
sub get_fromcheckout($)
{
  `$git config 'gits.$_[0].fromcheckout'`;
}



######################################################################
#
# Configure persistent fromcheckout
#
sub set_fromcheckout($$)
{
  if ($_[0])
  {
    `$git config 'gits.$_[1].fromcheckout' 1`;
    $fromcheckout = 1;
  }
  else
  {
    `$git config --unset 'gits.$_[1].fromcheckout'`;
    $fromcheckout = 0;
  }
  $? == 0;
}



######################################################################
#
# Check for presence of slave
#
my ($warngitslavesync) = 0;

sub missing_slave($)
{
  my ($slave) = @_;
  if (!-d $slave)
  {
    warn "Missing one or more git slaves including $slave, consider 'gits populate'.\n" unless ($warngitslavesync || $OPTIONS{'quiet'});
    $warngitslavesync++;
    return 1;
  }
  return 0;
}



######################################################################
#
# Add shell quoting to avoid problems
#
sub quoteit(@)
{
  my $str;
  foreach my $item (@_)
  {
    my ($i) = $item;
    my $o;
    my $len = length($i);

    foreach(my $x=0;$x<$len;$x++)
    {
      my $c = substr($i,$x,1);
      if ($OPTIONS{'eval-args'})
      {
	$o .= '\\' if ($c =~ /[\\\"]/);
      }
      else
      {
	$o .= '\\' if ($c =~ /[\\\$\`\"]/);
      }
      $o .= $c;
    }
    $str .= '"'.$o.'" ';
  }
  chop($str);
  $str;
}



######################################################################
#
# Populate - clone projects listed in .gitslaves
#
sub do_populate(;$);
sub do_populate(;$)
{
  my ($nocheckout)=@_;
  my $curbranch;
  our %hooked;
  my $wanthooks;

  my ($ret,$out) = getcmd(qq^$git config gits.nohooks^);
  $wanthooks = !$out && -d "git-hooks";

  $nocheckout='-n' if ($nocheckout);

  foreach my $group (@slaves)
  {
    my $slave = $group->[1];

    $curbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd("$git branch --no-color"))))[0] unless ($curbranch);

    if (! -d $slave || (! -f "$slave/config" && ! -d "$slave/.git"))
    {
      print STDERR "gits: Cloning $slave\n" if ($OPTIONS{'verbose'});
      my ($repos) = resolve_repository($group->[0],$group->[1],undef,undef,$group->[2]);
      docmd(qq^$git clone $nocheckout "$repos" "$slave"^) || die "Could not clone $repos onto $slave\n";

      my $subbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd(qq^cd "$slave" && $git branch --no-color^))))[0];
      if ($curbranch ne $subbranch && $curbranch !~ /no branch/)
      {
	print qq^Switching "$slave" to branch "$curbranch"\n^;

	# <TRICKY>Use of -f for git checkout is dangerous, as it throws away
	# local changes; but it is needed here for git 1.6, which treats the
	# empty (but not declared bare!) working tree as an uncommitted change
	# deleting all files that prevents checkout of the new branch</TRICKY>
	my $force = "";
	$force = " -f" if ($nocheckout);
	docmd(qq^cd "$slave" && $git checkout$force -b $curbranch origin/$curbranch^) || die "Branch inconsistency, branch $curbranch does not exist for $slave\n";
      }
    }
    else
    {
      my ($cursubbranch,$subbranch) = grep(s/^\s*\*\s+(.*)|\s+($curbranch)$/$1$2/, split(/\n/,getcmd(qq^cd "$slave" && $git branch --no-color | sort^)));
      if ($curbranch ne $cursubbranch && $curbranch !~ /no branch/ && !$nocheckout)
      {
	print qq^Switching "$slave" to branch "$curbranch"\n^;

	my $gitversion = `$git --version`;
	if ($curbranch ne $subbranch && $gitversion =~ /git version (1\.[12345]\.|1\.6\.[012345])/)
	{
	  docmd(qq^cd "$slave" && $git checkout -b $curbranch origin/$curbranch^) || die "Branch inconsistency, branch $curbranch does not exist for $slave\n";
	}
	else
	{
	  docmd(qq^cd "$slave" && $git checkout $curbranch^) || die "Branch inconsistency, branch $curbranch does not exist for $slave\n";
	}
      }
      else
      {
	my $branchmsg = "";
	$branchmsg = " (branch $curbranch)" if ($curbranch ne "master");
	print STDERR "gits: $slave already exists\n" if ($OPTIONS{'verbose'});
      }
    }

    # Count the number of directory separators for deep slave support
    my @slaves = split(m:/:,$slave);
    my ($updir) = "../..".("/.."x @slaves);

    # Update hooks from master (-regex '.*\\*\$' matches bogus symlink '*')
    docmd(qq^cd "$slave/.git/hooks" && { find . -type l -a '(' -lname '$updir/git-hooks*' -o -regex '.*\\*\$' ')' -exec rm -f '{}' ';'; find $updir/git-hooks/ -type f -exec sh -c 'ln -s \$0 .' '{}' ';'; }^)
      if (!$hooked{"$slave"} && $wanthooks);
    $hooked{"$slave"} = 1;
  }
  # Update master's hooks as well
  docmd(qq^cd .git/hooks && { find . -type l -a '(' -lname '../../git-hooks*' -o -regex '.*\\*\$' ')' -exec rm -f '{}' ';'; find ../../git-hooks/ -type f -exec sh -c 'ln -s \$0 .' '{}' ';'; }^)
    if (!$hooked{"."} && $wanthooks);
  $hooked{"."} = 1;

  if ($missingcount)
  {
    my ($oldfoundcount) = $foundcount;
    @slaves = git_slave_list($ARGV[0]);
    git_slave_list("errorme") if ($missingcount && $foundcount <= $oldfoundcount);
    do_populate($nocheckout);
  }
}



######################################################################
#
# git checkout, gits style
#
# pushprocessing: 0 -- want progress bar
# pushprocessing: 1 -- print informational messages, revert to oldbranch on die
#			(oldbranch is first argument, popped from @args)
# pushprocessing: 2 -- do not abort on errors - return warning
#
sub do_checkout($@);
sub do_checkout($@)
{
  my ($pushprocessing,@args) = @_;
  my ($oldbranch);
  my ($ret, $msg) = (0, undef);
  my ($slavecnt) = -1;
  my ($ok, $okcnt);
  my (%msg);
  my ($group, $slave, $repo) = (undef, ".", $TOP);

  $oldbranch = shift(@args) if ($pushprocessing == 1);

  if (!$OPTIONS{'no-master'})
  {
    my ($cmd) = qq^cd "%%dir%%" && $git checkout ^.quoteit(@args);
    $cmd .= " --" if ($pushprocessing);
    $cmd .= " >/dev/null 2>&1" if ($pushprocessing == 2);
    ($ret, $msg) = getcmd($cmd,$slave);

    if ($ret == 0)
    {
      # Reload list of slaves, which might have changed due to superproject checkout
      @slaves = git_slave_list("checkout");

      # New list of slaves might have stuff we need to check out
      do_populate(1);
    }
  }
  my ($totcnt) = $#slaves + ($OPTIONS{'no-master'} ? 1 : 2);
  my ($progress) = Term::ProgressBar->new({remove=>1, count=>($#slaves+1)}) if ($want_progress && !$pushprocessing);
  $progress->max_update_rate(1) if ($progress);

  # Tricky: This backwards loop is designed so that the return code processing can be shared between superproject and slaves
  my $failed;
  do
  {
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits checkout (@{[join(' ',@args)]}), failed for: '$repo': " . exitcode($ret);
      if ($pushprocessing > 1)
      {
	$failed .= " $repo";
      }
      elsif ($OPTIONS{'keep-going'})
      {
	$warn = "Error in $warn (continuing)\n $msg\n";
	$warn = "\n$warn" if ($progress);
	warn $warn;
      }
      else
      {
	my $diemsg = "Aborting $warn\n ";
	$diemsg = "\n$diemsg" if ($progress);
	if ($ok)
	{
	  $diemsg .= "(ran fine on: $ok, did not run on @{[$totcnt-$okcnt]} other(s); no rollbacks)";
	}
	else
	{
	  $diemsg .= "(first entry, no other successfully executed)";
	}
	# <TODO>try to restore old branch, if desirable/possible</TODO>
	$msg .= do_checkout(2, $oldbranch) if ($pushprocessing == 1);
	die "$diemsg\n  $msg\n";
      }
    }
    else
    {
      $ok .= " $repo";
      $okcnt++;
      if ($pushprocessing)
      {
	$msg =~ s/Switched to branch.*\n//;
	$msg =~ s/Already on \".*\n//;
	push(@{$msg{"$args[0]\@$repo"}}, $repo);
      }
      else
      {
	push(@{$msg{$msg}}, $repo);
      }
    }

    # Forward through the slave list
    if (++$slavecnt <= $#slaves)
    {
      $group = $slaves[$slavecnt];
      $repo = $slave = $group->[1];

      my ($cmd) = qq^cd "%%dir%%" && $git checkout ^.quoteit(@args);
      $cmd .= " --" if ($pushprocessing);
      $cmd .= " >/dev/null 2>&1" if ($pushprocessing == 2);
      ($ret, $msg) = getcmd($cmd,$slave);
    }

    $progress->update($slavecnt) if ($progress);
  } while ($slavecnt <= $#slaves);

  if ($pushprocessing > 1)
  {
    my $msg = "\nUnable to checkout original $args[0] for$failed" if ($failed);
    $msg .= " (successful for $okcnt other(s))" if ($failed);
    return $msg;
  }

  %msg;
}
######################################################################
#
# Find .gitignore for a given path
#
sub findignore($)
{
  my ($mntpt) = @_;

  my $gitignore = $GITS_DIR;
  my $ignorefile = $mntpt;
  if ($mntpt =~ m:.*/.*:)
  {
    my @dirs = split(m:/:,$mntpt);
    # print STDERR "GITIGNORE $gitignore $mntpt $ignorefile $#dirs\n";
    for(my $x=$#dirs-1;$x>=0;$x--)
    {
      my ($tmp) = $GITS_DIR."/".join("/",@dirs[0 .. $x]);
      if (-d "$tmp/.git")
      {
	$gitignore = $tmp;
	$ignorefile = join("/",@dirs[$x+1 .. $#dirs]);
	print STDERR "Found intervening .git, want to use $gitignore dir with content $ignorefile\n" if ($OPTIONS{'verbose'} > 1);
	last;
      }
    }
  }
  ($gitignore,$ignorefile);
}


######################################################################
#
# Standard gits output summmarization routine
#
sub stdout($;$)
{
  my ($msgp,$suppressempty) = @_;
  my $keys = scalar(keys(%{$msgp}));
  my $other = " other";
  my $limit = 7;

  # If all commands return identical strings, generate single header line
  # (unless we are in verbose mode) - suppress all output if all are empty
  if (!$OPTIONS{'verbose'} && $keys == 1)
  {
    return if (defined($msgp->{""}));
    $other = "";
  }

  foreach my $msg (sort { $#{$msgp->{$a}} <=> $#{$msgp->{$b}} } keys %{$msgp})
  {
    $keys--;
    if ($suppressempty && !$OPTIONS{'verbose'} && $msg eq "")
    {
      $limit = 999999;		# "other" not well-defined if some are omitted
      next;
    }
    my $repos = $#{$msgp->{$msg}};
    if ($OPTIONS{'verbose'} == 0 && !$keys && $repos > $limit)
    {
      print "On all$other repositories:\n";
    }
    else
    {
      $limit = $repos + 4 if ($repos + 4 > $limit);
      print "On: ", join(', ',@{$msgp->{$msg}}), ":\n";
    }
    if (length($msg))
    {
      # handle CR, e.g. in git rebase -p output
      $msg =~ s/\r(?!\n)/\n/sg;
      $msg =~ s/^/  /mg;
    }
    print $msg;
  }
}



######################################################################
#
# Main functionality
#
Getopt::Long::Configure("bundling", "no_ignore_case", "no_auto_abbrev", "no_getopt_compat", "require_order");
GetOptions(\%OPTIONS, 'parallel|p=i', 'verbose|v+', 'quiet', 'help', 'version',
	   'pager|paginate!', 'n', 'eval-args', 'exclude=s', 'keep-going',
	   'no-commit', 'no-hide', 'no-master', 'no-progress',
	   'with-ifpresent', 'press-on') || die $USAGE;
# -n option is separate from --pager/--no-pager/--paginate/--no-paginate due
# to limitations of GetOptions API - the last explicit option overrides -n,
# even if -n comes after the last explicit option

while ($ARGV[0] =~ /^--/)
{
  $git .= " " . shift @ARGV;
}
# provide baka terminology compatibility
my $BAKANAMES = { 'inits' => 'prepare', 'clones' => 'attach',
		  'checkouts' => 'populate', 'execs' => 'exec',
		  'resolves' => 'resolve', 'stati' => 'statuses' };
$ARGV[0] = $BAKANAMES->{$ARGV[0]} if $BAKANAMES->{$ARGV[0]};
$OPTIONS{'keep-going'} = 1 if $OPTIONS{'press-on'};

$want_parallel = $OPTIONS{'parallel'} if (defined($want_parallel));
$OPTIONS{'no-progress'} = 1 if ($want_parallel);

# default is pagination; however, if verbose is set, default is no pager
# because verbose generates warnings to STDERR, which is not paginated
$pagination = !$OPTIONS{'verbose'};
# unless gits -n
$pagination = 0 if ($OPTIONS{'n'});
# but explicit --pager/--no-pager/--pagination/--no-pagination overrides that
$pagination = $OPTIONS{'pager'} if (defined $OPTIONS{'pager'});

die "$USAGE" if ($#ARGV < 0 and !($OPTIONS{'version'} or $OPTIONS{'help'}));

print STDERR $eval_errs if ($eval_errs ne '' && $OPTIONS{'verbose'} > 1);

# Find GITS_DIR -- location of git slave file
if ($ENV{'GITS_DIR'} && -f $ENV{'GITS_DIR'}."/".GITSLAVE)
{				# Use gitslave location user told us
  $GITS_DIR = $ENV{'GITS_DIR'};
}
else
{				# Probe for gitslave location
  $GITS_DIR = '.';
  my (@S,$dev,$ino) = stat($GITS_DIR);

  do
  {
    if (!-f $GITS_DIR."/".GITSLAVE)
    {
      $GITS_DIR .= '/..';
      $dev = $S[0];
      $ino = $S[1];
      @S = stat($GITS_DIR);
    }
  } while (!-f $GITS_DIR."/".GITSLAVE && $S[0] == $dev && $S[1] != $ino);
}

# All (non-prepare/clone) operations are relative to gitslave base
if (-f $GITS_DIR."/".GITSLAVE)
{
  $GITS_DIR = Cwd::realpath($GITS_DIR);
  chdir($GITS_DIR) unless ($ARGV[0] eq "prepare" || $ARGV[0] eq "clone");
  print STDERR "gits: Gitslave found in $GITS_DIR\n" if ($OPTIONS{'verbose'} > 1);
}
else
{
  die "Could not find @{[GITSLAVE]}, either you are not inside a meta-module\nor one of its slaves or this is a new meta-module which has not yet\nbeen configured for gits.  Probably it is the first case so you simply\nneed to cd into the correct git checkout with a @{[GITSLAVE]} file, but in\nthe rare event that this is a new meta-module, you need run `gits\nprepare` and then a few `gits attach` commands to set up gits for this\nnew meta-module.\n"
    unless ($#ARGV < 0 or ephemeral_command($ARGV[0]));
}
$TOP = "(" . basename(Cwd::realpath(".")) . ")";

# Get configured pager from environment and/or master (meta-module) settings
# and redirect standard output through pager if it is currently a terminal
# (and a controlling tty is available from which the pager can take commands)
if ($pagination && -t STDOUT && open(TTY, "</dev/tty"))
{
  close(TTY);
  my $PAGER = `$git config core.pager`;
  chomp $PAGER;
  # PAGER environment variable or default only used if core.pager not set
  $PAGER = $ENV{'PAGER'} || "less" if ($?);
  # GIT_PAGER environment variable trumps everything else
  $PAGER = $ENV{'GIT_PAGER'} if (defined $ENV{'GIT_PAGER'});

  # for less, explicitly add -K so that SIGINT will terminate (and cleanup)
  $PAGER .= " -K" if ($PAGER =~ m!^([^\s(`;]*/)?less([-+\$\w ]*)?$!);

  if ($PAGER && $PAGER ne "cat")
  {
    # set LESS environment for pager only (not later git subcommands)
    $PAGER = "LESS=FRSX " . $PAGER if (!$ENV{'LESS'});

    my $pid;
    if ($pid = open(STDOUT, "|$PAGER"))
    {
      # cleanup pager on die
      $SIG{__DIE__} = sub
	{
	  die @_ if $^S; # do nothing if die called within eval
	  kill 'INT', $pid;
	  close STDOUT;
	};
    }
    else
    {
      warn "Unable to run pager '$PAGER': $!\n" unless ($OPTIONS{'quiet'});
    }
  }
}
@slaves = git_slave_list($ARGV[0]);

if (!$OPTIONS{'no-progress'})
{
  if (defined($want_progress) && -t STDERR)
  {
    $want_progress = 1;
  }
  else
  {
    warn "Progress bar unavailable - install Term::ProgressBar\n" .
      " (perl-Term-ProgressBar RPM) (libterm-progressbar-perl dpkg)\n"
      if ($OPTIONS{'verbose'});
  }
}

if ($OPTIONS{'parallel'})
{
  if (!defined($want_parallel))
  {
    warn "Parallelism unavailable - install Parallel::Iterator\n" .
      " (no RPM or dpkg exists, use CPAN)\n";
  }
}


##################################################
if ($ARGV[0] eq 'version' or $OPTIONS{'version'})
{
  my $version = "2.0.1";

  # <TRICKY> If the version string is still UNTAGGED, try to run the local
  # git commands to get the current hash and possibly tag; this is used by
  # Makefile to get the correct string to replace it with.  Otherwise, the
  # installation has replaced it with a version; don't mess with it.  Use
  # string concatenation to keep this check from getting replaced. </TRICKY>

  if ($version eq "{" . "UNTAGGED" . "}")
  {
    my ($ret,$hash) = getcmd("$git log --pretty=format:%H -n 1");
    chomp($hash);

    my ($dret,$desc) = getcmd("$git describe --candidates=1 --tags $hash 2>/dev/null");
    if ($dret)
    {
      $desc = "Untagged ($hash)\n";
    }
    else
    {
      # strip out leading tag alphabetic and space, get just the numbers
      $desc =~ s/^.*?([0-9][0-9._-]*[0-9]).*/$1/;
      $desc =~ y=/_=--=;
    }
    print $desc;
  }
  else
  {
    print "gits version $version\n";
    my ($vret,$vers) = getcmd("$git --version");
    if ($vret || ($vers !~ /version ([2-9]|1\.[6-9])/))
    {
      print STDERR "$git version >= 1.6 required!\n";
      $returncode = 1;
    }
    print $vers;
    ($vret,$vers) = getcmd("perl --version");
    $vers =~ s/^\n//;
    $vers =~ s/This is perl,/Perl/;
    $vers =~ s/\nCopyright.*//s;
    print $vers;
  }
}
##################################################
elsif ($ARGV[0] eq 'help' or $OPTIONS{'help'})
{
  print "$USAGE";
  use FindBin qw($Bin $Script);
  docmd("pod2text < $Bin/$Script |" .
	"sed -n -e '/^DESCRIPTION/,/^BUGS/H'" .
	" -e '/^BUGS/{x;s/\\n[[:upper:]]*/\\n/g;s/\\n\\n/\\n/;p;}'");
  exit;
}
##################################################
elsif ($ARGV[0] eq 'prepare')
{
  die "gits prepare takes no arguments\n" if ($#ARGV != 0);
  my $SUPER_GITS_DIR = $GITS_DIR;
  $GITS_DIR=$ENV{'GITS_DIR'} || ".";
  die "Refusing to prepare, \$GITSLAVE variable is multipath\n" if ($GITSLAVE =~ /, /);
  die "Refusing to prepare, $GITS_DIR/@{[$GITSLAVE]} already exists\n" if (-f $GITS_DIR."/".$GITSLAVE);
  die "Refusing to prepare, $SUPER_GITS_DIR/@{[$GITSLAVE]} already exists\n"
    . " (GITS_DIR=".`pwd`." gits prepare will override)\n" if (-f $SUPER_GITS_DIR."/".$GITSLAVE && !defined($ENV{'GITS_DIR'}));
  open(W, ">", $GITS_DIR."/".$GITSLAVE) || die "Could not create $GITS_DIR/@{[$GITSLAVE]}\n";
  close(W);
  docmd("$git add @{[$GITSLAVE]}");
  docmd(qq^$git commit -m "gits creating @{[$GITSLAVE]}" @{[$GITSLAVE]}^) unless ($OPTIONS{'no-commit'});
}
##################################################
elsif ($ARGV[0] eq 'attach')
{
  my $include;

  if ($include = (grep(/^--recursive=(.+)$/,@ARGV))[0])
  {
    $include =~ s/--recursive=//;
    die "--recursive argument may not have directory components: it must be in base of new slave\n" if ($include =~ m:/:);
    @ARGV = grep(!/^--recursive=.+$/,@ARGV);
  }

  die "gits attach requires at least two arguments\nusage: gits attach [--recursive=<.gitslave>] <repository> <directory> [flags]\n" if ($#ARGV < 2 || $#ARGV > 3 || !$ARGV[1] || !$ARGV[2]);
  my ($repos) = resolve_repository($ARGV[1],$ARGV[2]);
  die "Unknown flag $ARGV[3]\n" if ($ARGV[3] && $ARGV[3] ne "ifpresent");
  die "Refusing to attach, \$GITSLAVE variable is multipath\n" if ($GITSLAVE =~ /, /);
  die "Destination ($ARGV[2]) already exists\n" if (-e $ARGV[2]);
  if ($ARGV[2] =~ m:(.*)/(.*):)
  {
    die "Destination ($ARGV[2]) cannot end in a slash" if (length($2) < 1);
    die "Destination parent ($1) does not exist\n" if (!-d $1);
  }
  my $files = $GITS_DIR."/".$GITSLAVE;
  docmd(qq^$git clone "$repos" "$ARGV[2]"^) || die "Could not clone $repos onto $ARGV[2]\n";

  open(W, ">>", $files);
  print W qq^"$ARGV[1]" "$ARGV[2]"^;
  print W " $ARGV[3]" if ($ARGV[3]);
  print W "\n";
  if ($include)
  {
    print W qq^#include "$ARGV[2]/$include"^;
    print W " $ARGV[3]" if ($ARGV[3]);
    print W "\n";
  }
  close(W);

  if (1)
  {
    my ($gitignore, $ignorefile) = findignore($ARGV[2]);

    my ($needadd) = 0;
    $needadd++ if (! -f "$gitignore/.gitignore");
    open(W, ">>", $gitignore."/.gitignore");
    print W qq^/$ignorefile/\n^;
    close(W);
    docmd(qq^cd "$gitignore" && $git add .gitignore^) if ($needadd);
    if ($gitignore eq $GITS_DIR)
    {
      $files .= " .gitignore";
    }
    else
    {
      my $msg = qq^gits adding "$ARGV[1]" "$ARGV[2]" (.gitignore)^;
      docmd(qq^cd "$gitignore" && $git commit -m '$msg' .gitignore^) unless ($OPTIONS{'no-commit'});
    }
  }
  my $msg = qq^gits adding "$ARGV[1]" "$ARGV[2]"^;
  $msg .= qq^ and recursively $include^ if ($include);
  docmd(qq^$git commit -m '$msg' $files^) unless ($OPTIONS{'no-commit'});

  my $curbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd("$git branch --no-color"))))[0];
  my $subbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd(qq^cd "$ARGV[2]" && $git branch --no-color^))))[0];
  if ($curbranch ne $subbranch)
  {
    print qq^Switching "$ARGV[2]" to branch "$curbranch"\n^;
    docmd(qq^cd "$ARGV[2]" && $git checkout $curbranch --^) || die "Branch inconsistency, branch $curbranch does not exist\n";
  }
}
##################################################
elsif ($ARGV[0] eq 'detach')
{
  die "gits detach requires one directory (sub-module) argument which must exist\nusage: gits detach <directory>\n" if ($#ARGV != 1);

  die "$ARGV[1] is not a git repository or does not exist\n" unless (-d "$ARGV[1]/.git");
  my $files = $GITS_DIR."/".$GITSLAVE;

  die "$files, the gitslave management file, does not exist\n" unless (-f $files);
  open(R,"<",$files) || die "Cannot open $files";
  my $save;
  my ($lineno) = 0;
  my $found = 0;
  while (<R>)
  {
    $lineno++;
    if (/^\s*(?:\#.*)$/)
    {
      $save .= $_;
      next;
    }
    die "Bad line $lineno in $files\n" unless (/^\"(.*)\" \"(.*)\"( ifpresent)?$/);
    if ($2 eq $ARGV[1])
    {
      $found++;
      next;
    }
    $save .= $_;
  }
  close(R);
  die "Could not find requested repository $ARGV[1] in $files for detaching.  Aborting\n" unless ($found);
  open(W,">",$files) || die "Cannot open $files for writing";
  print W $save;
  close(W) || die "Cannot write $files";

  my ($gitignore, $ignorefile) = findignore($ARGV[1]);

  if (open(R,"<","$gitignore/.gitignore"))
  {
    my $save2;
    while (<R>)
    {
      my ($c) = $_;
      chomp($c);
      next if ($c eq "/$ignorefile/");
      $save2 .= $_;
    }
    close(R);
    open(W,">","$gitignore/.gitignore") || die "Cannot open .gitignore for writing";
    print W $save2;
    close(W) || die "Cannot write .gitignore";
    docmd(qq^cd "$gitignore" && $git commit -m 'gits removing ^.quoteit($ARGV[1])."' .gitignore") unless ($gitignore eq $GITS_DIR || $OPTIONS{'no-commit'});
  }

  docmd(qq^rm -rf ^.quoteit($ARGV[1])) unless ($OPTIONS{'no-commit'});
  docmd(qq^$git commit -m 'gits removing ^.quoteit($ARGV[1])."' ".quoteit($GITSLAVE).($gitignore eq $GITS_DIR?" .gitignore":"")) unless ($OPTIONS{'no-commit'});
}
##################################################
elsif ($ARGV[0] eq 'clone')
{
  my $nohooks;
  my $fromcheckout;
  if (grep(/^--nohooks$/,@ARGV))
  {
    $nohooks=1;
    @ARGV = grep(!/^--nohooks$/,@ARGV);
  }

  if (grep(/^--fromcheckout$/,@ARGV))
  {
    $fromcheckout=1;
    @ARGV = grep(!/^--fromcheckout$/,@ARGV);
  }

  if (grep(/^--no-fromcheckout$/,@ARGV))
  {
    $fromcheckout=0;
    @ARGV = grep(!/^--no-fromcheckout$/,@ARGV);
  }

  my ($ret,$out) = getcmd(qq^$git ^.quoteit(@ARGV));
  print STDERR $out;
  if ($ret != 0 ||
      ($out !~ /^Cloning into (.*)\.\.\.\n/
       && $out !~ m^Initialized empty Git repository in (.*)/\.git/\n^))
  {
    die "Could not parse git clone output\n";
  }
  $GITS_DIR = Cwd::realpath($1);
  chdir($1);

  docmd(qq^$git config gits.nohooks 1^) if ($nohooks);
  set_fromcheckout($fromcheckout,"origin") if (defined($fromcheckout));

  my $cmd = "$0 ";
  foreach my $arg (keys %OPTIONS)
  {
    if ($arg eq "exclude" || $arg eq "parallel")
    {
      $cmd .= qq^'--$arg=$OPTIONS{"$arg"}' ^;
    }
    else
    {
      $cmd .= "--$arg ";
    }
  }
  $cmd .= "populate";
  docmd($cmd);
}
##################################################
elsif ($ARGV[0] eq 'populate')
{
  if (grep(/^--nohooks$/,@ARGV))
  {
    docmd(qq^$git config gits.nohooks 1^);
    @ARGV = grep(!/^--nohooks$/,@ARGV);
  }

  if (grep(/^--fromcheckout$/,@ARGV))
  {
    set_fromcheckout(1,"origin");
    @ARGV = grep(!/^--fromcheckout$/,@ARGV);
  }

  if (grep(/^--no-fromcheckout$/,@ARGV))
  {
    set_fromcheckout(0,"origin");
    @ARGV = grep(!/^--no-fromcheckout$/,@ARGV);
  }

  if ($#ARGV > 0)
  {
    my $cmd = shift(@ARGV);
    @slaves = git_slave_list($cmd,@ARGV);
  }

  do_populate();
}
##################################################
elsif ($ARGV[0] eq 'resolve')
{
  if (grep(/^--fromcheckout$/,@ARGV))
  {
    $fromcheckout = 1;
    @ARGV = grep(!/^--fromcheckout$/,@ARGV);
  }

  if (grep(/^--no-fromcheckout$/,@ARGV))
  {
    $fromcheckout = 0;
    @ARGV = grep(!/^--no-fromcheckout$/,@ARGV);
  }

  die "gits resolve requires at least two non-option arguments <possibly-relative url> <location-in-repository> [remote]\n" if ($#ARGV < 2 || $#ARGV > 3 || !$ARGV[1] || !$ARGV[2]);

  print resolve_repository($ARGV[1], $ARGV[2], $ARGV[3])."\n";
}
##################################################
elsif ($ARGV[0] eq 'pulls')
{
  shift(@ARGV);
  my ($ok,$okcnt,%msg);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my $oldbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd("$git branch --no-color"))))[0];
  my @branches = grep(s/branch\.(.*)\.remote=.*/$1/,split(/\n/,getcmd(qq%$git config -l%)));
  my $fetched;
  my $rebase;
  my $force = 0;
  my @fetchargs = grep(/^-[qvaftku]|--(no-)?(quiet|verbose|append|force|tags|keep|update-head-ok|depth=.*|upload-pack=.*|)$/, @ARGV);
  my @mergeargs = grep(/^-[n]|--(no-)?(stat|summary|log|commit|squash|ff|strategy=.*)$/, @ARGV);
  my $rebasearg = " -p";

  # sort branches to list current branch of master repository first
  my $curbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd("$git branch --no-color"))))[0];
  @branches = sort { ($b eq $curbranch) <=> ($a eq $curbranch) } @branches;

  # is this a rebase or merge (default) pull?
  foreach my $arg (@ARGV)
  {
    $rebase = 1 if $arg eq "--rebase";
    $rebase = 0 if $arg eq "--no-rebase";
    # bundled single-character options (e.g. -abc) and non-option arguments
    # (args to options, e.g. "-s ours", or repository/refspec) are too tricky;
    # the special end-of-options flag (--) falls in that category too
    $fetched = -1 if ($arg !~ /^-/ || $arg eq "--" || $arg =~ /^-[^-]{2}/);
  }
  $rebase = -1 if ($rebase && $fetched < 0);
  # only git version 1.6.1 and later support `git rebase -p`
  my $gitversion = `$git --version`;
  if ($rebase < 0 ||
      ($rebase > 0 && $gitversion =~ /git version (1\.[12345]\.|1\.6\.0)/))
  {
    $rebasearg = "";
    warn "Warning: merges may be lost when rebasing!\n" unless ($OPTIONS{'quiet'});
    # <TODO>add some user interaction to confirm possible damaging activity?
    # or determine if there are any merge commits that would be lost?</TODO>
  }

  if ($want_progress)
  {
    # steps are checkouts and fetches; if we pull in every repository for every
    # branch, (1 + branches + (repos * branches)), else (1 + branches + repos)
    my $steps;
    if ($fetched < 0)
    {
      $steps = 1 + ($#branches + 1) * ($#list + 2);
    }
    else
    {
      $steps = 1 + ($#branches + 1) + ($#list + 1);
    }

    $progress = Term::ProgressBar->new({remove=>1, ETA => 'linear', count=>$steps});
  }
  $progress->max_update_rate(1) if ($progress);

  foreach my $branch (@branches)
  {
    my (%newmsg) = do_checkout(1, $oldbranch, $branch);
    my (%results);
    my $pullcmd;
    my $getremote = "REM=`$git config branch.$branch.remote`; " .
      "REF=`$git config branch.$branch.merge | sed 's%.*/%%'`; ";

    # default command on first merge or if there are "tricky" arguments
    if ($fetched < 1 && $rebase < 1)
    {
      $pullcmd = "$git pull ".quoteit(@ARGV);
      # check for branch.<name>.rebase = true only on non-tricky first merge
      # (and when rebase has not been explicitly disabled with --no-rebase)
      $pullcmd = $getremote .
	"if [ \"`$git config --bool branch.$branch.rebase`\" = true ]; then " .
	"$git fetch " . quoteit(@fetchargs) . " && " .
	"$git rebase$rebasearg remotes/\"\$REM/\$REF\"; else " .
	"$pullcmd; fi" if (!$fetched && !defined($rebase));
    }
    # subsequent explicit merge (with --no-rebase)
    elsif (defined($rebase) && !$rebase)
    {
      $pullcmd = $getremote .
	"RURL=`$git config remote.\"\$REM\".url`; " .
	"$git merge -m \"Merge branch '\$REF' of \$RURL into $branch\" " .
	quoteit(@mergeargs) . " remotes/\"\$REM/\$REF\"";
    }
    # subsequent merge (or rebase if branch.<name>.rebase is set)
    elsif (!$rebase)
    {
      $pullcmd = $getremote .
	"if [ \"`$git config --bool branch.$branch.rebase`\" = true ]; then " .
	"$git rebase$rebasearg remotes/\"\$REM/\$REF\"; else " .
	"RURL=`$git config remote.\"\$REM\".url`; " .
	"$git merge -m \"Merge branch '\$REF' of \$RURL into $branch\" " .
	quoteit(@mergeargs) . " remotes/\"\$REM/\$REF\"; fi";
    }
    else
    {
      # rebase (with -p if supported by git version)
      $pullcmd = $getremote . "$git rebase$rebasearg remotes/\"\$REM/\$REF\"";
      # preceded by fetch on first time through
      $pullcmd = "$git fetch ".quoteit(@fetchargs)." && { $pullcmd; }"
	if (!$fetched);
    }
    # for debugging
    # $pullcmd = "set -x; $pullcmd";

    foreach my $group (@list)
    {
      my $slave = $group->[1];
      next if missing_slave($slave);

      $results{$group} = gitsubst(qq^cd "%%dir%%" && $pullcmd^,$slave);
    }
    if ($want_parallel)
    {
      %results = Parallel::Iterator::iterate_as_hash({'workers' => $want_parallel},sub { [getcmd($_[1])] }, \%results);
    }

    foreach my $group (@list)
    {
      my $slave = $group->[1];
      my $repo = $slave;
      $repo = $TOP if ($repo eq ".");

      next if missing_slave($slave);

      my ($ret, $msg);
      if ($want_parallel)
      {
	($ret, $msg) = @{$results{$group}};
      }
      else
      {
	($ret, $msg) = getcmd($results{$group});
      }
      if ($ret != 0)
      {
	chomp($msg);
	my $warn = "gits pulls, failed on branch $branch for: '$repo': " . exitcode($ret);
	if ($OPTIONS{'keep-going'} || $want_parallel)
	{
	  $warn = "Error in $warn (continuing)\n $msg\n";
	  $warn = "\n$warn" if ($progress);
	  warn $warn;
	  $returncode = 2;
	  next;
	}

	my $diemsg = "Aborting $warn\n ";
	if ($ok)
	{
	  $diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
	}
	else
	{
	  $diemsg .= "(first entry, no other successfully executed)";
	}
	# <TODO>some way for do_checkout to only operate on some slaves</TODO>
	$msg .= do_checkout(2, $oldbranch);
	$diemsg = "\n$diemsg" if ($progress);
	die "$diemsg\n  $msg\n";
      }
      $msg = $newmsg{$repo}.$msg;
      $msg =~ s/Switched to branch.*\n//;
      $msg =~ s/Already on \".*\n//;
      $msg =~ s/Current branch $branch is( up to date)/Already$1/;
      $msg =~ s+(Successfully rebased and updated) refs/heads/$branch+$1+;
      $ok .= " $repo";
      $okcnt++;
      # replace module name with %MODULE% to collapse redundant messages
      my $mod = $slave;
      if ($mod eq '.')
      {
	$mod = `$git config remote.origin.url`;
	chomp $mod;
	$mod =~ s=.*/==;
      }
      $msg =~ s/\b$mod\b/%MODULE%/g;
      my $submod = $mod;
      $submod =~ s=.*/==;
      $msg =~ s/\b$submod\b/%MODULE%/g if ($mod ne $submod);
      # remove hash ids in various places to collapse redundant messages
      if (!$OPTIONS{'no-hide'})
      {
	$msg =~ s/\n {3}[0-9a-fA-F]{7}\.\.[0-9a-fA-F]{7} /\n /g;
	$msg =~ s/\nUpdating [0-9a-fA-F]{7}\.\.[0-9a-fA-F]{7}\n/\n/g;
	$msg =~ s/(Fast-forwarded \S+) to [0-9a-fA-F]*\./$1/g;
      }
      push(@{$msg{$msg}}, "$branch\@$repo");

      # fetch / pull is a step
      $progress->update(++$progress_count) if ($progress && $fetched < 1);
    }

    # only fetch for first branch
    $fetched = 1 if (!$fetched);

    # branch checkout is a step
    $progress->update(++$progress_count) if ($progress);
  }

  my $lastmsg = do_checkout(2, $oldbranch);
  $progress->update(++$progress_count) if ($progress);

  stdout(\%msg);

  $lastmsg .= "\n" if ($lastmsg);
  print $lastmsg;
}
##################################################
elsif ($ARGV[0] eq 'pull' || $ARGV[0] eq 'fetch')
{
  my ($ok,$okcnt,%msg);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});

  $progress = Term::ProgressBar->new({remove=>1, ETA => 'linear', count=>($#list+1)}) if ($want_progress);
  $progress->max_update_rate(1) if ($progress);

  my (%results,%worklist);
  foreach my $group (@list)
  {
    my $slave = $group->[1];

    next if missing_slave($slave);

    $worklist{$group} = gitsubst(qq^cd "%%dir%%" && $git ^.quoteit(@ARGV),$slave);
  }

  if ($want_parallel)
  {
    %results = Parallel::Iterator::iterate_as_hash({'workers' => $want_parallel},sub { [getcmd($_[1])] }, \%worklist);
  }

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg);
    if ($want_parallel || ref($results{$group}))
    {
      ($ret, $msg) = @{$results{$group}};
    }
    else
    {
      ($ret, $msg) = getcmd($worklist{$group});
    }
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);
      if ($OPTIONS{'keep-going'} || $want_parallel)
      {
	$warn = "Error in $warn (continuing)\n $msg\n";
	$warn = "\n$warn" if ($progress);
	warn $warn;
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      $diemsg = "\n$diemsg" if ($progress);
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;
    # replace module name with %MODULE% to collapse redundant messages
    my $mod = $slave;
    if ($mod eq '.')
    {
      $mod = `$git config remote.origin.url`;
      chomp $mod;
      $mod =~ s=.*/==;
    }
    $msg =~ s/\b$mod\b/%MODULE%/g;
    my $submod = $mod;
    $submod =~ s=.*/==;
    $msg =~ s/\b$submod\b/%MODULE%/g if ($mod ne $submod);
    # remove hash ids in various places to collapse redundant messages
    if (!$OPTIONS{'no-hide'})
    {
      $msg =~ s/\n {3}[0-9a-fA-F]{7}\.\.[0-9a-fA-F]{7} /\n /g;
      $msg =~ s/\nUpdating [0-9a-fA-F]{7}\.\.[0-9a-fA-F]{7}\n/\n/g;
      $msg =~ s/(Fast-forwarded \S+) to [0-9a-fA-F]*\./$1/g;
    }
    push(@{$msg{$msg}}, $repo);

    $progress->update(++$progress_count) if ($progress);
  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'push')
{
  my ($ok,$okcnt,%msg);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($quick) = 0;

  if (!$>)
  {
    # git uses $HOME only for configuration search, not ~user
    my $dir = $ENV{'HOME'};
    my @statinfo = stat("$dir/.gitconfig") if ($dir);

    # If ~/.gitconfig exists and is owned by root, root push is allowed
    die "push should not be performed as root\n" unless (exists($statinfo[4]) && $statinfo[4] == $>);
  }

  if (grep(/^--quick$/,@ARGV))
  {
    $quick = 1;
    @ARGV = grep(!/^--quick$/,@ARGV);
  }

  $progress = Term::ProgressBar->new({remove=>1, ETA => 'linear', count=>($#list+1)}) if ($want_progress);
  $progress->max_update_rate(1) if ($progress);

  my (%results,%worklist);
  foreach my $group (@list)
  {
    my $slave = $group->[1];

    next if missing_slave($slave);

    if ($quick)
    {
      my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git status^, $slave);
      if (!($msg =~ /Your branch is /))
      {
	$results{$group} = [0, "Skipping push, this branch is up to date.\n"];
	next;
      }
    }

    $worklist{$group} = gitsubst(qq^cd "%%dir%%" && $git ^.quoteit(@ARGV),$slave);
  }

  if ($want_parallel)
  {
    %results = Parallel::Iterator::iterate_as_hash({'workers' => $want_parallel},sub { [getcmd($_[1])] }, \%worklist);
  }

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg);
    if ($want_parallel || ref($results{$group}))
    {
      ($ret, $msg) = @{$results{$group}};
    }
    else
    {
      ($ret, $msg) = getcmd($worklist{$group});
    }
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);
      if ($OPTIONS{'keep-going'} || $want_parallel)
      {
	$warn = "Error in $warn (continuing)\n $msg\n";
	$warn = "\n$warn" if ($progress);
	warn $warn;
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      $diemsg = "\n$diemsg" if ($progress);
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;
    push(@{$msg{$msg}}, $repo);

    $progress->update(++$progress_count) if ($progress);
  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'checkout')
{
  shift(@ARGV);
  my (%msg) = do_checkout(0, @ARGV);

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'archive')
{
  shift(@ARGV);
  my $format;
  my $outfile;
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($ok,$okcnt);
  my (%group,%msg);

  for(my $i=0; $i <= $#ARGV; $i++)
  {
    # OK, not the sanest options parse in the world, but it should be
    # close enough for sane developers to use.
    if ($ARGV[$i] eq "-l" || $ARGV[$i] eq "--list")
    {
      print "gits-tar\n";
      print `$git archive -l`;
      # explicit close will wait in case we have forked a pager
      close(STDOUT);
      exit(0);
    }
    if ($ARGV[$i] eq "--format" && $i < $#ARGV)
    {
      $format = $ARGV[$i+1];
      $ARGV[$i+1] = "tar" if ($format eq "gits-tar");
    }
    if ($ARGV[$i] =~ /^--format=(.*)/)
    {
      $format = $1;
      $ARGV[$i] =~ s/gits-tar/tar/ if ($format eq "gits-tar");
    }
    if (($ARGV[$i] eq "-o" || $ARGV[$i] eq "--output") && $i < $#ARGV)
    {
      $outfile = $ARGV[$i+1];
    }
    if ($ARGV[$i] =~ /^--output=(.*)/)
    {
      $outfile = $1;
    }
  }

  if ($format eq "gits-tar" && !$outfile)
  {
    die "Must provide a --output file with gits-tar archive format\n";
  }
  if ($format eq "gits-tar" && $outfile)
  {
    my $prefix;
    my $cmdargs;

    for(my $i=0; $i <= $#ARGV; $i++)
    {
      if ($ARGV[$i] eq "--prefix" && $i < $#ARGV)
      {
	$prefix=$ARGV[++$i];
	next;
      }
      if ($ARGV[$i] =~ /--prefix=(.*)/)
      {
	$prefix=$1;
	next;
      }
      if (($ARGV[$i] eq "--output" || $ARGV[$i] eq "-o") && $i < $#ARGV)
      {
	$i++;
	next;
      }
      next if ($ARGV[$i] eq "--output=(.*)");
      $cmdargs .= quoteit($ARGV[$i])." ";
    }

    $prefix .= "/" if ($prefix && $prefix !~ m:/$:);
    my $first = 1;
    my $curout = $outfile;
    foreach my $group (@list)
    {
      my $slave = $group->[1];
      my $repo = $slave;
      $repo = $TOP if ($repo eq ".");

      next if missing_slave($slave);

      my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git archive $cmdargs --prefix ${prefix}$slave/ --output $curout^,$slave);
      if ($ret == 0)
      {
	if ($first)
	{
	  undef($first);
	  $curout = ($ENV{'TMPDIR'}||'/tmp')."/gitslave-archive-tmp.$$";
	}
	else
	{
	  my($ret1, $msg1) = getcmd("tar -Af $outfile $curout");
	  unlink($curout) if ($ret1 == 0);
	  $ret = $ret1;
	  $msg .= $msg1;
	}
      }

      if ($ret != 0)
      {
	chomp($msg);
	my $warn = "gits archive failed for: '$repo': " . exitcode($ret);

	if ($OPTIONS{'keep-going'})
	{
	  warn "Error in $warn (continuing)\n $msg\n";
	  $returncode = 2;
	  next;
	}

	my $diemsg = "Aborting $warn\n ";
	if ($ok)
	{
	  $diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
	}
	else
	{
	  $diemsg .= "(first entry, no other successfully executed)";
	}
	die "$diemsg\n  $msg\n";
      }
      $ok .= " $repo";
      $okcnt++;


      push(@{$msg{$msg}}, $repo);
    }
  }
  else
  {
    foreach my $group (@list)
    {
      my $slave = $group->[1];
      my $repo = $slave;
      $repo = $TOP if ($repo eq ".");

      next if missing_slave($slave);

      my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git archive ^.quoteit(@ARGV),$slave);
      if ($ret != 0)
      {
	chomp($msg);
	my $warn = "gits archive failed for: '$repo': " . exitcode($ret);

	if ($OPTIONS{'keep-going'})
	{
	  warn "Error in $warn (continuing)\n $msg\n";
	  $returncode = 2;
	  next;
	}

	my $diemsg = "Aborting $warn\n ";
	if ($ok)
	{
	  $diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
	}
	else
	{
	  $diemsg .= "(first entry, no other successfully executed)";
	}
	die "$diemsg\n  $msg\n";
      }
      $ok .= " $repo";
      $okcnt++;

      push(@{$msg{$msg}}, $repo);
    }

  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'exec')
{
  shift(@ARGV);
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($branch);
  my (%group,%msg);

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && ^.quoteit(@ARGV),$slave);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits exec $ARGV[0], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn (continuing)\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;

    push(@{$msg{$msg}}, $repo);
  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'remote')
{
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($branch);
  my (%group,%msg);
  my ($realurl);
  my ($fromcheckout);

  if ($ARGV[1] eq "add")
  {
    $fromcheckout = 0;
    if (grep(/^--fromcheckout$/,@ARGV))
    {
      $fromcheckout = 1;
      @ARGV = grep(!/^--fromcheckout$/,@ARGV);
    }

    if (grep(/^--no-fromcheckout$/,@ARGV))
    {
      $fromcheckout = 0;
      @ARGV = grep(!/^--no-fromcheckout$/,@ARGV);
    }

    $realurl = $ARGV[$#ARGV];
  }

  # ARGV[2] better be a remote!
  set_fromcheckout($fromcheckout,$ARGV[2]) if (defined($fromcheckout));


  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    if ($ARGV[1] eq "add")
    {
      $ARGV[$#ARGV] = resolve_repository($group->[0],$group->[1],$ARGV[2],$realurl);
    }

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git ^.quoteit(@ARGV),$slave);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits remote $ARGV[1], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn (continuing)\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;

    push(@{$msg{$msg}}, $repo);
  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'update-remote-url')
{
  my ($fromcheckout);

  $fromcheckout = 0;
  if (grep(/^--fromcheckout$/,@ARGV))
  {
    $fromcheckout = 1;
    @ARGV = grep(!/^--fromcheckout$/,@ARGV);
  }

  if (grep(/^--no-fromcheckout$/,@ARGV))
  {
    $fromcheckout = 0;
    @ARGV = grep(!/^--no-fromcheckout$/,@ARGV);
  }

  if ($#ARGV != 2 || !$ARGV[1] || !$ARGV[2])
  {
    my $example = `$git config remote.origin.url`;

    if ($example)
    {
      my ($from) = $fromcheckout?"--fromcheckout":"";
      $example = "Example: gits update-remote-url $from origin $example\n"
    }

    die "gits update-remote-url requires two non-option arguments\nusage: gits update-remote-url [--fromcheckout] [--no-fromcheckout] <remote-name> <new-super-url>\n$example";
  }

  my ($cmd,$name,$url) = @ARGV;
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my (%msg);

  set_fromcheckout($fromcheckout,$name) if (defined($fromcheckout));

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($thisurl) = $url;
    if ($slave ne ".")
    {
      $thisurl = resolve_repository($group->[0],$group->[1],$name,$url,$group->[2]);

      print qq^For slave $slave, "@{[$group->[0]]}" "@{[$group->[1]]}" $name $url  "@{[$group->[2]]}" => $thisurl\n^;
    }

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git config "remote.$name.url" "$thisurl"^,$slave);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits $cmd, failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;

    push(@{$msg{$msg}}, $repo);
  }

  stdout(\%msg);
  set_fromcheckout($fromcheckout, $name);
}
##################################################
elsif ($ARGV[0] eq 'status')
{
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($branch);
  my (%group,%groupc,%msg);

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg) = getcmd(qq^cd "$slave" && $git ^.quoteit(@ARGV),$slave);
    if ($ret != 0 && $ret != 256)
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    elsif ($ret == 256)
    {
      # git status seems to return 1 (nee wait 256) under all circumstances, so do
      # not even propagate this condition.
      #$returncode = 1;
      1;
    }
    $ok .= " $repo";
    $okcnt++;

    my ($premove);
    while ($msg =~ s/(^[^\#].*\n)//)
    {
      $premove .= $1;
    }

    die "gits unexpected status output (missing branch): $msg" unless ($msg =~ s/^\# (?:On branch |Not currently on any branch.)(.+)?\n//);
    my $localbranch = $1 ? $1 : "(no branch)";
    $branch = $localbranch unless ($branch);
    unless ($branch eq $localbranch)
    {
      our %wrongbranches;
      # turn on minimal verbosity to get empty messages in matching repositories
      # (this allows "all other repositories" summarization)
      $OPTIONS{'verbose'} = "0e0" if (!$OPTIONS{'verbose'});
      my $warning = "Top-level $TOP branch '$branch' != slave branch '$localbranch'!\n";
      $msg = $warning . $msg;
      warn $warning if (!$wrongbranches{$localbranch});
      $wrongbranches{$localbranch} = 1;
    }

    if ($msg =~ /^\# Your branch/)
    {
      while ($msg =~ s/^(\# \S.*\n)//)
      {
	$premove .= $1;
      }
      $msg =~ s/^\#\s*\n//;
    }

    $msg =~ s/^no(thing| changes)( added)? to commit .*\n//m;

    while ($msg =~ s/^(\# \S.*\n)//)
    {
      my ($group) = $1;
      while ($msg =~ s/^(\#\s{2,}\S.*\n)//)
      {
	$groupc{$group}->{$1} = 1;
      }
      die "Could not parse git status output for $slave <$group> <$msg>\n" unless ($msg =~ s/^(\#\s*\n)//);

      while ($msg =~ s/^(\#(?:\s*|(\s{2,}|\t)\S.*)\n)//)
      {
	my ($line) = $1;

	if ($line =~ /([^:]+:\s+)(.*)/s)
	{
	  $line = "$1$slave/$2";
	}
	elsif ($line =~ /(\#\s+)(.+)/)
	{
	  $line = "$1$slave/$2 # Do not git(s) add this path\n";
	}
	else
	{
	  next if ($line =~ /\#\s*\n/);
	}

	$group{$group} .= $line;
      }
    }

    push(@{$msg{$premove.$msg}}, $repo);
  }

  print "# On branch $branch\n";
  foreach my $group (sort keys %group)
  {
    print $group;
    foreach my $msg (sort keys %{$groupc{$group}})
    {
      print $msg;
    }
    print "#\n";
    print $group{$group};
    print "#\n";
  }

  stdout(\%msg,1);
}
##################################################
elsif ($ARGV[0] eq 'statuses')
{
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my $oldbranch = (grep(s/\s*\*\s+//, split(/\n/,getcmd("$git branch --no-color"))))[0];
  my @branches = grep(s/branch\.(.*)\.remote=.*/$1/,split(/\n/,getcmd(qq%$git config -l%)));
  my @oldbranch = ($oldbranch);

  shift(@ARGV);
  my $move;
  if ($ARGV[0] eq '-m')
  {
    shift(@ARGV);
    unshift(@oldbranch, '-m');
    $move = '-m';
  }

  my $firstbranch = 1;

  foreach my $branch (@branches)
  {
    my %newmsg = $move
      ? do_checkout(1, $oldbranch, $move, $branch)
	: do_checkout(1, $oldbranch, $branch);

    my (%group,%groupc,%msg);

    foreach my $group (@list)
    {
      my $slave = $group->[1];
      my $repo = $slave;
      $repo = $TOP if ($repo eq ".");

      next if missing_slave($slave);

      my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git status ^.quoteit(@ARGV),$slave);
      if ($ret != 0 && $ret != 256)
      {
	chomp($msg);
	my $warn = "gits statuses, failed on branch $branch for: '$repo': " . exitcode($ret);

	if ($OPTIONS{'keep-going'})
	{
	  warn "Error in $warn\n $msg\n";
	  $returncode = 2;
	  next;
	}

	my $diemsg = "Aborting $warn\n ";
	if ($ok)
	{
	  $diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
	}
	else
	{
	  $diemsg .= "(first entry, no other successfully executed)";
	}
	# <TODO>some way for do_checkout to only operate on some slaves</TODO>
	$msg .= do_checkout(2, @oldbranch);
	die "$diemsg\n  $msg\n";
      }
      elsif ($ret == 256)
      {
	# git status seems to return 1 (nee wait 256) under all circumstances, so do
	# not even propagate this condition.
	#$returncode = 1;
	1;
      }
      $msg = $newmsg{$slave}.$msg;
      $msg =~ s/Switched to branch.*\n//;
      $msg =~ s/Already on \".*\n//;
      $ok .= " $repo";
      $okcnt++;

      my ($premove);
      while ($msg =~ s/(^[^\#].*\n)//)
      {
	$premove .= $1;
      }

      unless ($msg =~ s/^\# (?:On branch |Not currently on any branch.)(.+)?\n//)
      {
	# <TODO>some way for do_checkout to only operate on some slaves</TODO>
	$msg .= do_checkout(2, @oldbranch);
        die "gits unexpected status output (missing branch): $msg";
      }

      if ($msg =~ /^\# Your branch/)
      {
	while ($msg =~ s/^(\# \S.*\n)//)
	{
	  $premove .= $1;
	}
	$msg =~ s/^\#\s*\n//;
      }

      $msg =~ s/^no(thing| changes)( added)? to commit .*\n//m;

      while ($msg =~ s/^(\# \S.*\n)//)
      {
	my ($group) = $1;

	while ($msg =~ s/^(\#\s{2,}\S.*\n)//)
	{
	  $groupc{$group}->{$1} = 1 if ($firstbranch);
	}

	unless ($msg =~ s/^(\#\s*\n)//)
	{
	  # <TODO>some way for do_checkout to only operate on some slaves</TODO>
	  my $c = do_checkout(2, @oldbranch);
	  die "Could not parse git status output for $slave <$group> <$msg>$c\n"
	}
	while ($msg =~ s/^(\#(?:\s*|(\s{2,}|\t)\S.*)\n)//)
	{
	  next unless ($firstbranch);

	  my ($line) = $1;

	  if ($line =~ /([^:]+:\s+)(.*)/s)
	  {
	    $line = "$1$slave/$2";
	  }
	  elsif ($line =~ /(\#\s+)(.+)/)
	  {
	    $line = "$1$slave/$2 # Do not git(s) add this path\n";
	  }
	  else
	  {
	    next if ($line =~ /\#\s*\n/);
	  }

	  $group{$group} .= $line;
	}
      }

      push(@{$msg{$premove.$msg}}, $repo);
    }

    if ($firstbranch)
    {
      $firstbranch = 0;
      foreach my $group (sort keys %group)
      {
	print $group;
	foreach my $msg (sort keys %{$groupc{$group}})
	{
	  print $msg;
	}
	print "#\n";
	print $group{$group};
	print "#\n";
      }
    }

    print "# On branch $branch\n";
    stdout(\%msg,1);
  }
  print do_checkout(2, @oldbranch) . "\n";
}
##################################################
elsif ($ARGV[0] eq 'grep')
{
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my ($out);

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git ^.quoteit(@ARGV),$slave);
    next if ($ret == 256);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }

    $ok .= " $repo";
    $okcnt++;

    $msg =~ s:^(.*\S.*\n):$slave/$1:gm unless (grep(/-h/,@ARGV));

    $out .= $msg;
  }

  print $out;
}
##################################################
elsif ($ARGV[0] eq 'clean')
{
  shift(@ARGV);
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my (@unlinkers);
  my (%msg);
  my ($commonout);

  my $CLEAN_USAGE = "usage: gits clean -f|-n [-dqxX] [--] [path]...\n";
  my %COPTIONS;
  GetOptions(\%COPTIONS, 'd', 'f', 'q', 'n', 'X', 'x') || die $CLEAN_USAGE;

  die "Must set exactly one of -n or -f\n$CLEAN_USAGE" unless (!$COPTIONS{'f'} != !$COPTIONS{'n'});

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($cmd) = qq^cd "%%dir%%" && $git clean -n^;
    $cmd .= " -d" if ($COPTIONS{'d'});
    $cmd .= " -f" if ($COPTIONS{'f'});
    $cmd .= " -X" if ($COPTIONS{'X'});
    $cmd .= " -x" if ($COPTIONS{'x'});
    $cmd .= " -- ".quoteit(@ARGV) if ($#ARGV >= 0);

    my ($ret, $msg) = getcmd($cmd,$slave);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;

    my $newmsg;
    foreach my $line (split(/\n/,$msg))
    {
      if ($line =~ s/(Would( not)? remove )(.*)//)
      {
	my ($pre,$not,$path) = ($1,$2,$3);

	# Canonicalize
	$path = "$slave/$path";
	$path =~ s:^./::;

	# Check directory to see if it matches slavedir
	if ($path =~ s:/$::)
	{
	  my ($qpath) = quotemeta($path);

	  next if grep($_->[1] =~ m:^$qpath$:, @slaves);

	  if (-d "$path/.git")		# Don't remove directories with .git in them.
	  {
	    $commonout .= "Skipping $path (contains .git)\n";
	    next;
	  }

	  # Restore trailing /
	  $path .= "/";
	}
	if ($COPTIONS{'n'})
	{
	  $commonout .= $pre.$path."\n";
	  next;
	}
	next if ($not);
	$commonout .= "Removing $path\n";
	push(@unlinkers,$path);
      }
      else
      {
	$newmsg .= $_;
      }
    }

    push(@{$msg{$newmsg}}, $repo);
  }

  print $commonout unless ($COPTIONS{'q'});

  if (!$COPTIONS{'n'} && $#unlinkers >= 0)
  {
    rmtree(\@unlinkers, $OPTIONS{'verbose'}) || die "Cannot unlink";
  }

  stdout(\%msg);
}
##################################################
elsif ($ARGV[0] eq 'logs')
{
  shift(@ARGV);
  my ($ok,$okcnt);
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});
  my (%group,@msg,$out);
  my ($window) = 28800;				# 8 hours

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git log --date=relative --pretty='tformat: %at %h %ae %ad: %s' ^.quoteit(@ARGV),$slave);
    if ($ret != 0)
    {
      chomp($msg);
      my $warn = "gits exec log, failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	warn "Error in $warn (continuing)\n $msg\n";
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;

    foreach my $line (split(/\n/,$msg))
    {
      chomp($slave);
      chomp($line);
      warn "Bad entry on $slave" unless ($line =~ /^\s*(\d+)\s+([[:xdigit:]]+)\s+(\S+)\s+(.*)/);
      push(@msg, [$1,$2,$repo,$3,$4]);
    }
  }

  @msg = sort { my $x; return($x) if ($x = $b->[0] <=> $a->[0]); $b->[2] cmp $a->[2]; } @msg;

  while (my $line = shift(@msg))
  {
    $out = "$line->[4] <$line->[3]>\n\t$line->[1] $line->[2]\n";
    my $i = 0;
    my $lower = $line->[0] - $window;

    for(;$i <= $#msg && $msg[$i]->[0] > $lower;$i++)
    {
      # Require matching committer
      next if ($line->[4] ne $msg[$i]->[4]);

      # Require matching commit message
      next if ($line->[5] ne $msg[$i]->[5]);

      $out .= "\t$msg[$i]->[1] $msg[$i]->[2]\n";
      $lower = $msg[$i]->[0] - $window;
      splice(@msg,$i--,1);			# Get rid of item we already matched
    }
    print "$out\n";
  }
}
##################################################
else # Generic command
{
  my ($ok,$okcnt);
  my %msg;

  # This is a sign that you are doing something wrong
  if (-f $ARGV[$#ARGV])
  {
    warn "You have a pathname in a generic gits command, this is a sign that the command\nwill not work since it is unlikely to exist in all modules\nProbably you want to cd to the submodule and run a raw git command\n";
  }

  # Run the command for each module
  my (@list) = @slaves;
  unshift(@list,[undef,".","."]) unless ($OPTIONS{'no-master'});

  if ($ARGV[0] eq 'gc' || $ARGV[0] eq 'fsck')
  {
    $progress = Term::ProgressBar->new({remove=>1, ETA => 'linear', count=>(($#list+1))}) if ($want_progress);
    $progress->max_update_rate(1) if ($progress);
  }

  foreach my $group (@list)
  {
    my $slave = $group->[1];
    my $repo = $slave;
    $repo = $TOP if ($repo eq ".");

    next if missing_slave($slave);

    my ($ret, $msg) = getcmd(qq^cd "%%dir%%" && $git ^.quoteit(@ARGV),$slave);
    if ($ret != 0 &&
	!($ARGV[0] eq "commit" && $msg =~ /nothing to commit/) &&
	!($ARGV[0] eq "stash" && $ARGV[1] eq "apply" && $msg =~ /no valid stashed state found/))
    {
      chomp($msg);
      my $warn = "gits $ARGV[0], failed for: '$repo': " . exitcode($ret);

      if ($OPTIONS{'keep-going'})
      {
	$warn = "Error in $warn (continuing)\n $msg\n";
	$warn = "\n$warn" if ($progress);
	warn $warn;
	$returncode = 2;
	next;
      }

      my $diemsg = "Aborting $warn\n ";
      if ($ok)
      {
	$diemsg .= "(ran fine on: $ok, did not run on @{[$#list-$okcnt]} other(s); no rollbacks)";
      }
      else
      {
	$diemsg .= "(first entry, no other successfully executed)";
      }
      $diemsg = "\n$diemsg" if ($progress);
      die "$diemsg\n  $msg\n";
    }
    $ok .= " $repo";
    $okcnt++;
    push(@{$msg{$msg}}, $repo);

    $progress->update(++$progress_count) if ($progress);
  }

  stdout(\%msg);
}
# explicit close will wait in case we have forked a pager
close(STDOUT);
exit($returncode);



=pod

Gitslave Home Page: L<< http://gitslave.sf.net >>

=head1 NAME

gits - The git slave repository tool for super-project multi-repository management

=head1 SYNOPSIS

gits [-p|--parallel COUNT] [-v|--verbose]+ [--quiet] [--help] [--version]
[-n|--no-pager] [--paginate] [--eval-args] [--exclude SLAVE-REGEXP]
[--keep-going] [--no-commit] [--no-hide] [--no-master] [--no-progress]
[--with-ifpresent] [-- GIT-OPTIONS...] SUBCOMMAND [ARGS]...

=head1 OVERVIEW

gits is a program that assists in assembling a meta-project from a
number of individual git repositories which operate (when using gits)
as if they were one git repository instead of many, similar to the way
that CVS works by default and svn (v1.5) can be coerced to work.  Some
of these individual git repositories may be part of other
meta-projects as well.

Unfortunately, the functionality provided by git-submodule is not
sufficient for this mode of operation.  Most git commands, like
checkout or commit, do not recursively descend into the submodules so
you are forced to do execute all git commands N+1 times (leading to
pain and mistakes), also, submodules revisions are tracked in the
supermodule so that change to the slave project made outside the
superproject are not automatically seen.  Since git does not allow
partial checkouts, we are left with little alternative.

Thus, to solve these problems gits was born.  Complexity pain is still
involved, but the hope is that it is minimized compared to all of the
other alternatives.

The basic theory is that there are a few sub-commands (prepare, attach,
populate) which help set up the meta-project for gits operations.
Then, for any git command which is not file specific (git add
<filename>; git reset <filename>; etc), you should use "gits" instead
of "git", and the command will run on all repositories in the project.

=head2 Example Usage

In the following example, we will have the following master
repositories:

  ssh://sourcemaster/src/repos/super.git
  ssh://sourcemaster/src/repos/lib1.git
  ssh://sourcemaster/src/repos/lib2.git


The desired working layout of directories with .git in them on disk
is:

  ..../super
  ..../super/lib1
  ..../super/lib2



=head3 Clone a gits project

gits clone [--[no-]fromcheckout] [--nohooks] <superproject-URL>

This command clones an existing superproject and all associated slave
repositories.  It runs `git clone` on the arguments you provide
(allowing you to change the name of the checked-out repository for
example) and then runs `gits populate` inside that newly checked out
repository.  If you are cloning from an existing gitslave checkout
instead of the master layout (the master layout is specified in the
gits attach commands), you would want to use the --fromcheckout
argument.

By default, if there is a git-hooks directory present in the
superproject, that will manage the .git/hooks directory in all related
repositories.  Adding --nohooks will disable this management.

  --------------------------------------------------
  gits clone ssh://sourcemaster/src/repos/super.git super2
  cd super2
  --------------------------------------------------

The gits flag --with-ifpresent is useful here as one way to populate
conditional repositories.



=head3 Initialize a gits project

gits prepare

You run this command in the git directory which will be your top level
master repository (super here).  Typically you would clone this top
level master from some other location which has all of your git
projects.

  --------------------------------------------------
  git clone ssh://sourcemaster/src/repos/super.git super
  cd super
  gits prepare
  --------------------------------------------------



=head3 Add a slave repository to top level master

  gits attach [--recursive=.gitslave] <repository> <localpath> [flags]

Clone the named git repository into the named local directory, and set
it up for further gits operations.

Typically <localpath> would be a path relative to the top level
working directory, for example, a subdirectory in the top level.

<repository> can also be relative (relative to the URL the top level
master checkout was cloned from).  It may also be an absolute URL but
if so, `gits remote add` is not going to be happy.  If the URL starts
with ^, it will use only the method and hostname from the master's
URL.  Otherwise, it will be relative to the fully qualified path.  We
will show both in operation in the example.

The only [flags] currently supported is "ifpresent" which will be set for
this slave repository.  Other people cloning or populating the
superproject will not check out this subrepository if this flag is
set, unless they add --with-ifpresent or otherwise arrange for the
<localpath> to be created.

If the --recursive=<filename> argument is present (and you must use
the --recursive=<filename> style, not "--recursive filename") the
repository you are attaching will be treated as a recursive gitslave
master underneath the real gitslave master.  The filename will
typically be ".gitslave".  It is relative to the localpath
you checked out and must not have any / in it.

 --------------------------------------------------
 gits attach ../lib1.git lib1
 gits attach ^/src/repos/lib2.git lib2
 gits attach --recursive=.gitslave ../../super2 supersub
 gits push
 --------------------------------------------------

The push in the example is to share the attach with other users.



=head3 Remove a slave repository from top level master

  gits detach <localpath>

Remove the named directory from both the local filesystem, the
.gitslave management file, and the .gitignore file so that it will not
be used in subsequent gits activities.

 --------------------------------------------------
 gits detach lib1
 gits push
 --------------------------------------------------

The push in the example is to share the detach with other users.



=head3 Check out any new slave modules which someone else might have added

  gits populate [--[no-]fromcheckout] [--nohooks] [ifpresent modules to check out]

Go through the list of configured slaves and check out (clone) any which have
not already been retrieved.

With --fromcheckout, assume that the remote repository is a gits checkout
instead of in standard repository layout.  With --no-fromcheckout, assume
that the remote repository has the standard layout.  If either option is
given, sets the default repository layout, which is used when no explicit
option is provided.

The gits flag --with-ifpresent is useful here as one way to populate
conditional repositories, which will check out all conditional
repositories.  You could also specifically ask for certain conditional
modules by listing them on the command line.

By default, if there is a git-hooks directory present in the
superproject, that will manage the .git/hooks directory in all related
repositories.  Adding --nohooks will disable this management.

  --------------------------------------------------
  gits populate
  --------------------------------------------------



=head3 Perform a pull operation for all tracked branches

  gits pulls [pull args]

For each branch being tracked by the superproject, go through the list
of configured slaves, check yourself out to the branch, perform a pull,
then switch back to the branch you were on.

Please see SUBSTITUTION for information on how to replace part of the
command with the slave module name for each executed command.

In the common case where no repository or refspec is provided as an
argument, pulls will perform a fetch once (in the current branch of
each slave) and then rebase (with --preserve-merges) or merge in each
branch.  This reduces redundant network overhead in the common case
where all branches of a repository have the same remote, but if that
is not the case, only the current remote will be fetched.  If you want
to force the slower operation of a pull in each branch, pass the --
argument, e.g. gits pulls --rebase --

  --------------------------------------------------
  gits pulls --rebase
  --------------------------------------------------



=head3 Perform a pull operation for the current branch only

  gits pull [pull args]

Go through the list of configured slaves and perform a pull,  Note that
even though only the current branch HEAD will be advanced, the commits
on other branches will still be fetched.

Please see SUBSTITUTION for information on how to replace part of the
command with the slave module name for each executed command.

  --------------------------------------------------
  gits pull --rebase
  --------------------------------------------------
  gits pull otherhost:/src/work/wb/%%dir%%
  --------------------------------------------------



=head3 Show unified commit logs in a fixed output format

  gits logs [log args]

For each branch being tracked by the superproject, generate a list
of the "log" messages as specified by the log args, and output them in
a fixed format, with related commits grouped together and ordered by
time. Do I<not> provide git log options that modify the output format,
as they will break the ordering and grouping functionality; only
arguments that control the selection of commits should be used.
(Note that using the --date={relative,local,default,iso,rfc,short}
option is okay, and will modify the displayed date format).

The related commit grouping will group together all commits (in any
sub-project) within an 8-hour period that have the same author
e-mail and commit message.

  --------------------------------------------------
  gits logs HEAD...Product-3.1.1
  --------------------------------------------------



=head3 Perform an arbitrary command for all tracked branches

  gits exec <command> [args]

For each slave being tracked by the superproject and the superproject itself,
cd to the root of the slave project directory and execute the listed command.

Please see SUBSTITUTION for information on how to replace part of the
command with the slave module name for each executed command.

  --------------------------------------------------
  gits exec gitk
  --------------------------------------------------
  gits exec git diff
  --------------------------------------------------



=head3 Print out URLs for arbitrary repositories like those used by attach

  gits resolve [--[no-]fromcheckout] <repository> <local_relpath> [remote]

Go through the same process that gits uses for resolving relative
repository URLs into absolute URLs, for debugging and certain porting
efforts.  You may specify the remote of the superproject you wish the
relative repository to be in relation to: by default it uses origin.

With --fromcheckout, resolve URLs as if the repository were a clone of
another gits checkout.  With --no-fromcheckout, resolve URLs as if it were
not a clone of another gits checkout.  By default it uses the saved
repository layout from gits populate or update-remote-url; however gits
resolve does not change the saved default even if --fromcheckout or
--no-fromcheckout is given.

  --------------------------------------------------
  gits resolve ../otherpos otherpos
  gits resolve ../otherpos otherpos otherremote
  --------------------------------------------------



=head3 Update the URL a remote repository points at

  gits update-remote-url [--[no-]fromcheckout] <remote-name> <url>

Update the super-project's remote.name.url using the url and remote
name listed above.  Then go though each slave repository and update
its url using the normal relative url mechanism.

The --fromcheckout option supports using a gits checkout as the remote
repository, adjusting the repository paths from their default.  The
--no-fromcheckout option assumes a normal (as specified in the gits
attach commands) repository layout for the remote. If neither option
is given, the saved repository layout from the most recent gits
populate or update-remote-url command is used; however, this is not
generally correct, as the repository layout of the new remote need not
be the same as the old one.  If either --fromcheckout or
--no-fromcheckout is given, sets the default repository layout
accordingly.

You can use this command after a clone of a local repository (or local gits
checkout, using gits populate --fromcheckout) so that the new repository
uses the same remote origin as the first one.  (The local clone/populate is
much faster than performing a remote clone/populate.)

  --------------------------------------------------
  git clone /home/user/work/wb /home/user/work/newwb
  cd /home/user/work/newwb
  gits populate --fromcheckout
  gits update-remote-url --no-fromcheckout origin ssh://git/src/git/wb
  --------------------------------------------------



=head3 Add a new remote to all repositories

  gits remote add [--[no-]fromcheckout] <git-remote-add options> <repositoryname> <url>

Adds a remote named <name> for the repository at <url> (as modified by
the relative URL rules from `gits attach` and `gits update-remote-url`
and the --fromcheckout argument). The command gits fetch <name> can
then be used to create and update remote-tracking branches
<name>/<branch>.

The <url> would be the absolute url for the superproject and each
slave repository would have a modified version of that path.

Please note that we do not currently support `gits remote set-url` in
a useful way.  See `gits update-remote-url` for an alternate method
which will satisfy most use cases.

 --------------------------------------------------
 gits remote add fred --fromcheckout /home/fred/src/foo
 --------------------------------------------------
 gits remote add backup ssh://backups/src/git/foo
 --------------------------------------------------



=head3 Push a change to a remote repository

  gits push [--quick] [push args]

This is the standard git push command--with the addition of a --quick
option which will only attempt to push for branches which have
outstanding changes.  For slow connections to large number of slave
repositories, the overhead of an empty push can be large.

However, --quick only checks to see if the I<current> branch needs to
push data.  If you have changes on other branches, a slow push is
still required, as it is if you are pushing to a repository other than
the standard origin.

Please see SUBSTITUTION for information on how to replace part of the
command with the slave module name for each executed command.

  --------------------------------------------------
  gits push --quick
  --------------------------------------------------
  gits push otherhost:/src/work/wb/%%dir%%
  --------------------------------------------------



=head3 Get status on all branches

  gits statuses [-m] [status args]

For each branch being tracked by the superproject, go through the list
of configured slaves, check yourself out to the branch, get the git
status, then switch back to the branch you were on.  The output is
summarized for each branch to merge the lists of files in each section.

The -m option will attempt to "move" any uncommitted changes, which
may prevent failures checking out other branches, at the risk of
creating conflicts, which are then moved as well

Other args supported by `git status` are also supported; these are
the same options supported by `git commit`.

  --------------------------------------------------
  gits statuses
  --------------------------------------------------



=head3 Make an archive of the repositories

  gits archive <standard git-archive arguments>

This is the standard git archive command--with the addition of a new
--format option of "gits-tar".  When you select the gits-tar option,
you must supply a on-disk --output file and you cannot use any tar
compression options.  With gits-tar, the output tar archive will be a
unified archive of the entire project, superproject and slaves.  Any
existing prefix will be treated as a directory prefix (e.g. --prefix
foo and --prefix foo/ are the same) and all slave repositories will be
unpacked in their corresponding superproject locations.

Remember the %%type%% substitutions if you choose to not use gits-tar.

  --------------------------------------------------
  gits archive --format gits-tar -o /tmp/foo.tar master
  --------------------------------------------------
  gits archive --format tar -o /tmp/foo-%%basename%%.tar master
  --------------------------------------------------



=head3 Everything else

All other commands are passed directly though to git, with one command
being run per repository.  Output summarizing is performed so that
multiple repositories with the same git output will only have the git
output shown once.  `git status` has a more aggressive summarizing
to merge the lists of files in each section.

  Examples:
  --------------------------------------------------
  gits commit -a -m "This is a change"
  gits push
  gits pull
  gits branch testing
  gits checkout testing
  gits diff master testing
  gits status
  gits ....
  --------------------------------------------------

All normal git commands are supported (plus any potential future
commands) but not all commands make sense to run with gits.  One good
example is git-daemon.

=head1 DESCRIPTION

=head2 --parallel=COUNT -p COUNT

Specify the number of parallel git operations you wish to execute.
Parallelism is only activated for push and pull(s) subcommands.
This can speed up your processing significantly for large
numbers of submodules.

If remote repositories are accessed over ssh, you may also wish to
activate ssh "ControlMaster" multiplexing.

  ssh_config
  --------------------------------------------------
  Host git
     ControlMaster auto
     ControlPath ~/.ssh/master-%r@%h:%p
  --------------------------------------------------

However, there currently is a "auto" race condition so the first batch
of peers do not necessarily take advantage of the multiplexing and
there have occasionally been spurious errors with this enabled.

=head2 --verbose -v

Ask for more information about what is happening.  You may repeat the
flag multiple times to get more information.  One level of verbosity
(-v) will print some minor warnings and will specify every repository
and its output explicitly.  Two levels of verbosity (-vv) will print
the underlying commands being executed and three levels (-vvv) will
print the data being returned from them.

=head2 --quiet

Ask for less information, which currently means discarding the STDERR
of some of the administrative git commands which are executed.

=head2 --help

Print gits usage summary and details of gits options and substitutions
from this documentation, then exit.

=head2 --version

Print version information for gits, git, and Perl.

=head2 --paginate --no-pager -n

Disable or re-enable default pagination of gits output using a pager.
The pager (default is less) and its options are configured just as for
git itself (using core.pager in the master repository or global settings,
or environment variables PAGER or GIT_PAGER, with the same precedence).

Pagination is only enabled if standard output is a tty (and there is a
controlling tty for the pager to take input from).  Pagination is
disabled if the configured pager (from config and environment) is set
to the value "cat" or the empty string.

The -n option will disable pagination, but will be overridden by any
--paginate or --no-pager arguments present on the command line, even if
the -n option is given later.

=head2 --eval-args

When quoting gits arguments, do not quote dollar '$' and backtick '`'
characters, to allow interpolation in the slave environment (but still
quote double-quote and backslash).  This is mostly useful for exec,
where you might want something like the command below to do something
useful.

  gits --eval-args exec echo Directory is '`pwd`'

=head2 --exclude=SLAVE-REGEXP

Provide a regular expression which excludes those slaves from
consideration from gits commands which it matches.

=head2 --keep-going

Do not abort when any subsidiary git command fails - instead print a
warning and continue processing.  Some git command failures will still
be considered fatal and cause gits to abort.

=head2 --no-commit

This flag requests that gits-internal sub-commands, such as prepare or
attach should not commit their changes after they make them.

=head2 --no-hide

This flag requests that gits not hide information when similar (but not
identical) output such as commit hashes is output for slave repositories.

=head2 --no-master

This flag requests that gits only run the listed command on the slave
repositories and NOT the super/master/top repository.  This probably
will not be needed much.

=head2 --no-progress

This flag requests that gits NOT print a progress bar, which it does
by default for slow operations if Term::ProgressBar is loaded.  Slow
operations are the checkout, fetch, pull(s), and push subcommands.
You may use this flag for all operations.

=head2 --with-ifpresent

Operate also on those slave repositories which are marked as
"ifpresent" even if they are not present.  This is mostly useful for
`gits populate` and `gits checkout`.

=head2 SUBCOMMAND ARGS...

Run the specified git command (with associated arguments) on the
repository and all slave repositories.  Typically they are git
commands run over each slave, but there are gits specific commands
such as: pulls, prepare, attach, populate, resolve, exec, logs, and
update-remote-url.  See OVERVIEW for more information on specific
subcommands.



=head1 SUBSTITUTION

Before execution, essentially all commands running over all of the
repositories will go through a substitution phase where certain magic
tokens will be replaced with information about the repository in
question.  These are most often used with `gits exec` and `gits
archive`.

%%dir%% represents the on-disk .gitslave-relative of the repository
(e.g. the second field in .gitslave) with the superproject getting the
value of ".".

%%path%% represents the fully qualified path to the repository in
question.

%%basename%% represents the basename of the %%path%%, which is
typically the last component of %%dir%% except for the superproject
for which it is whatever the last directory name of the path to the
superproject.

Also see --eval-args for an option to support standard shell `cmd`
and $VARIABLE expansion where it might otherwise be quoted.  Run the
following commmands to see the difference:

gits exec echo '`ls -ld $PWD`'

gits --eval-args exec echo '`ls -ld $PWD`'

=head1 BUGS

gits changes directory to the directory where .gitslave exists.
Likewise, when executing most git commands, gits changes directory to
the root of the git slave, so any pathnames passed as arguments to
gits must be absolute, not relative.  Generally this is only a concern
for pre-generated commit messages or things like that, you should NOT
be passing gits the pathnames of files checked into git slaves--you
will likely get the wrong result.

No coding has been performed yet to handle `gits remote set-url` or
`gits branch --set-upstream`.  See `gits update-remote-url` for a
supported method to perform this operation.  Support could be added if
necessary.

You can have partial success, failure, and repositories on which the
operation was never tried and you must recover from such manually.
This is usually not very complicated.  See --keep-going.

Programs like gitk will not show the global system history.

Special care may be needed if one or more of the repositories is a
third party repository and you plan to have a complex branch/tag
management strategy, plan to do (public or private) development on the
third party repository, or might sometimes not want the absolute
latest code on the third party branch.  See the gitslave home page
for more information on workarounds.

The behavior when different branches have different slave modules
associated with them and you checkout back and forth is probably not
ideal (nor are any of the options we have thought of completely
ideal).



=head1 FILES

=head2 .gitslave

The file containing the list of slave repositories (possibly in
relative form) and the directories relative to the master root where
they should be checked out.

The format of this file is:

"possibly-relative-repository-path" "top-level-checkout-relative-path"[ flags]

The flags, which are optional, currently can be the value "ifpresent"
which indicates that gits will only process those modules if the
top-level-checkout-relative-path is already present.

=head1 ENVIRONMENT

=head2 $GITSLAVE

Allow overriding on the name of the .gitslave file (for supplemental
activity, the .gitslave file must still exist even if it is not used
for this particular operation).  Also, allows a comma-space separated
list of files names, the sum of which will be the list of slaves to
use.

An example ".gitslave, .gitslave-extras" would allow an supplemental
list of slaves for unusual activity (e.g. release tagging) to the
normal list.

Alternately, you could play tricks where you only get a subset of the
modules when you don't need to play with everything.

=head1 REQUIREMENTS

perl 5 (probably almost any version of perl 5)

git 1.6 or later (git 1.7 or later preferred)

Optionally uses Parallel::Iterator and Term::ProgressBar if available

=head1 AUTHOR

Seth Robertson

=head1 REPORTING BUGS

Report bugs to L<< http://sourceforge.net/projects/gitslave >>

=head1 COPYRIGHT

Copyright (c) 2008 Seth Robertson.  License is similar to the GNU
Lesser General Public License version 2.1, see LICENSE.TXT for more
details.

=head1 SEE ALSO

git(1), git-submodule(1), git-subtree(google)

Gitslave Home Page: L<< http://gitslave.sf.net >>
