B<mr> is a Multiple Repository management tool. It can checkout, update, or
perform other actions on a set of repositories as if they were one combined
-respository. It supports any combination of subversion, git, cvs, mecurial and
-bzr repositories, and support for other revision control systems can easily be
-added.
+repository. It supports any combination of subversion, git, cvs, mecurial,
+bzr and darcs repositories, and support for other revision control systems can
+easily be added.
B<mr> cds into and operates on all registered repositories at or below your
working directory. Or, if you are in a subdirectory of a repository that
=back
-Actions can be abbreviated to any unambiguous subsctring, so
-"mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
+Actions can be abbreviated to any unambiguous substring, so
+"mr st" is equivalent to "mr status", and "mr up" is equivalent to "mr
update"
Additional parameters can be passed to most commands, and are passed on
Expand the statistics line displayed at the end to include information
about exactly which repositories failed and were skipped, if any.
-=item -n
+=item -n [number]
-Just operate on the repository for the current directory, do not
-recurse into deeper repositories.
+If no number if specified, just operate on the repository for the current
+directory, do not recurse into deeper repositories.
-=item -j number
+If a number is specified, will recurse into repositories at most that many
+subdirectories deep. For example, with -n 2 it would recurse into ./src/foo,
+but not ./src/packages/bar.
-Run the specified number of jobs in parallel. This can greatly speed up
-operations such as updates. It is not recommended for interactive
-operations.
+=item -j [number]
+
+Run the specified number of jobs in parallel, or an unlimited number of jobs
+with no number specified. This can greatly speed up operations such as updates.
+It is not recommended for interactive operations.
+
+Note that running more than 10 jobs at a time is likely to run afoul of
+ssh connection limits. Running between 3 and 5 jobs at a time will yeild
+a good speedup in updates without loading the machine too much.
=back
are run inside the repository, though not necessarily at the top of it.
The "MR_REPO" environment variable is set to the path to the top of the
-repository. The "MR_CONFIG" environment variable is set to the .mrconfig file
+repository. (For the "register" action, "MR_REPO" is instead set to the
+basename of the directory that should be created when checking the
+repository out.)
+
+The "MR_CONFIG" environment variable is set to the .mrconfig file
that defines the repo being acted on, or, if the repo is not yet in a config
file, the .mrconfig file that should be modified to register the repo.
(included in mr's built-in library) to skip updating the repo unless it's
been at least 12 hours since the last update.
- skip = test $(whoami) != joey
- skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
+ skip = test `whoami` != joey
+ skip = [ "$1" = update ] && ! hours_since "$1" 12
+
+=item order
+
+The "order" parameter can be used to override the default ordering of
+repositories. The default order value is 10. Use smaller values to make
+repositories be processed earlier, and larger values to make repositories
+be processed later.
+
+Note that if a repository is located in a subdirectory of another
+repository, ordering it to be processed earlier is not recommended.
=item chain
will try to load a .mrconfig file from the root of the repository. (You
should avoid chaining from repositories with untrusted committers.)
+=item include
+
+If the "include" parameter is set, its command is ran, and should output
+additional mrconfig file content. The content is included as if it were
+part of the including file.
+
+Unlike all other parameters, this parameter does not need to be placed
+within a section.
+
=item lib
The "lib" parameter can specify some shell code that will be run before each
=back
+When looking for a command to run for a given action, mr first looks for
+a parameter with the same name as the action. If that is not found, it
+looks for a parameter named "rcs_action" (substituting in the name of the
+revision control system and the action). The name of the revision control
+system is itself determined by running each defined "rcs_test" action,
+until one succeeds.
+
+Internally, mr has settings for "git_update", "svn_update", etc. To change
+the action that is performed for a given revision control system, you can
+override these rcs specific actions. To add a new revision control system,
+you can just add rcs specific actions for it.
+
=head1 AUTHOR
Copyright 2007 Joey Hess <joey@kitenet.net>
use strict;
use Getopt::Long;
use Cwd qw(getcwd abs_path);
-use POSIX "WNOHANG";
+
+# things that can happen when mr runs a command
use constant {
OK => 0,
FAILED => 1,
ABORT => 3,
};
-$SIG{INT}=sub {
- print STDERR "mr: interrupted\n";
- exit 2;
-};
-
-$ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
+# configurables
my $config_overridden=0;
-my $directory=getcwd();
my $verbose=0;
my $stats=0;
-my $no_recurse=0;
+my $max_depth;
+my $no_chdir=0;
my $jobs=1;
+my $directory=getcwd();
+$ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
+
+# globals :-(
my %config;
my %configfiles;
my %knownactions;
my %alias;
+my (@ok, @failed, @skipped);
-Getopt::Long::Configure("no_permute");
-my $result=GetOptions(
- "d|directory=s" => sub { $directory=abs_path($_[1]) },
- "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
- "v|verbose" => \$verbose,
- "s|stats" => \$stats,
- "n|no-recurse" => \$no_recurse,
- "j|jobs=i" => \$jobs,
-);
-if (! $result || @ARGV < 1) {
- die("Usage: mr [-d directory] action [params ...]\n".
- "(Use mr help for man page.)\n");
-
-}
-
-# Make sure MR_CONFIG is an absolute path, but don't use abs_path since
-# the config file might be a symlink to elsewhere, and the directory it's
-# in is significant.
-if ($ENV{MR_CONFIG} !~ /^\//) {
- $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
-}
-# Try to set MR_PATH to the path to the program.
-eval {
- use FindBin qw($Bin $Script);
- $ENV{MR_PATH}=$Bin."/".$Script;
-};
+main();
-loadconfig(\*DATA);
-loadconfig($ENV{MR_CONFIG});
-#use Data::Dumper;
-#print Dumper(\%config);
+my %rcs;
+sub rcs_test { #{{{
+ my ($action, $dir, $topdir, $subdir) = @_;
-# alias expansion and command stemming
-my $action=shift @ARGV;
-if (exists $alias{$action}) {
- $action=$alias{$action};
-}
-if (! exists $knownactions{$action}) {
- my @matches = grep { /^\Q$action\E/ }
- keys %knownactions, keys %alias;
- if (@matches == 1) {
- $action=$matches[0];
+ if (exists $rcs{$dir}) {
+ return $rcs{$dir};
}
- elsif (@matches == 0) {
- die "mr: unknown action \"$action\" (known actions: ".
- join(", ", sort keys %knownactions).")\n";
+
+ my $test="set -e\n";
+ foreach my $rcs_test (
+ sort {
+ length $a <=> length $b
+ ||
+ $a cmp $b
+ } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
+ my ($rcs)=$rcs_test=~/(.*)_test/;
+ $test="my_$rcs_test() {\n$config{$topdir}{$subdir}{$rcs_test}\n}\n".$test;
+ $test.="if my_$rcs_test; then echo $rcs; fi\n";
}
- else {
- die "mr: ambiguous action \"$action\" (matches: ".
- join(", ", @matches).")\n";
+ $test=$config{$topdir}{$subdir}{lib}."\n".$test
+ if exists $config{$topdir}{$subdir}{lib};
+
+ print "mr $action: running rcs test >>$test<<\n" if $verbose;
+ my $rcs=`$test`;
+ chomp $rcs;
+ if ($rcs=~/\n/s) {
+ $rcs=~s/\n/, /g;
+ print STDERR "mr $action: found multiple possible repository types ($rcs) for $topdir$subdir\n";
+ return undef;
}
-}
-
-# commands that do not operate on all repos
-if ($action eq 'help') {
- exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
-}
-elsif ($action eq 'config') {
- if (@ARGV < 2) {
- die "mr config: not enough parameters\n";
+ if (! length $rcs) {
+ return $rcs{$dir}=undef;
}
- my $section=shift;
- if ($section=~/^\//) {
- # try to convert to a path relative to the config file
- my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
- $dir=abs_path($dir);
- $dir.="/" unless $dir=~/\/$/;
- if ($section=~/^\Q$dir\E(.*)/) {
- $section=$1;
- }
- }
- my %changefields;
- foreach (@ARGV) {
- if (/^([^=]+)=(.*)$/) {
- $changefields{$1}=$2;
- }
- else {
- my $found=0;
- foreach my $topdir (sort keys %config) {
- if (exists $config{$topdir}{$section} &&
- exists $config{$topdir}{$section}{$_}) {
- print $config{$topdir}{$section}{$_}."\n";
- $found=1;
- last if $section eq 'DEFAULT';
- }
- }
- if (! $found) {
- die "mr $action: $section $_ not set\n";
- }
- }
+ else {
+ return $rcs{$dir}=$rcs;
}
- modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
- exit 0;
-}
-elsif ($action eq 'register') {
- if (! $config_overridden) {
- # Find the closest known mrconfig file to the current
- # directory.
- $directory.="/" unless $directory=~/\/$/;
- foreach my $topdir (reverse sort keys %config) {
- next unless length $topdir;
- if ($directory=~/^\Q$topdir\E/) {
- $ENV{MR_CONFIG}=$configfiles{$topdir};
- last;
- }
- }
+} #}}}
+
+sub findcommand { #{{{
+ my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
+
+ if (exists $config{$topdir}{$subdir}{$action}) {
+ return $config{$topdir}{$subdir}{$action};
}
- my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
- "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
- join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
- print STDERR "mr $action: running >>$command<<\n" if $verbose;
- exec($command) || die "exec: $!";
-}
-# work out what repos to act on
-my @repos;
-my $nochdir=0;
-foreach my $topdir (sort keys %config) {
- foreach my $subdir (sort keys %{$config{$topdir}}) {
- next if $subdir eq 'DEFAULT';
- my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
- my $d=$directory;
- $dir.="/" unless $dir=~/\/$/;
- $d.="/" unless $d=~/\/$/;
- next if $no_recurse && $d ne $dir;
- next if $dir ne $d && $dir !~ /^\Q$d\E/;
- push @repos, [$dir, $topdir, $subdir];
- }
-}
-if (! @repos) {
- # fallback to find a leaf repo
- LEAF: foreach my $topdir (reverse sort keys %config) {
- foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
- next if $subdir eq 'DEFAULT';
- my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
- my $d=$directory;
- $dir.="/" unless $dir=~/\/$/;
- $d.="/" unless $d=~/\/$/;
- if ($d=~/^\Q$dir\E/) {
- push @repos, [$dir, $topdir, $subdir];
- last LEAF;
- }
- }
+ if ($is_checkout) {
+ return undef;
}
- $nochdir=1;
-}
-# run the action on each repository and print stats
-my (@ok, @failed, @skipped);
-if ($jobs > 1) {
- mrs(@repos);
-}
-else {
- foreach my $repo (@repos) {
- record($repo, action($action, @$repo));
- print "\n";
- }
-}
-if (! @ok && ! @failed && ! @skipped) {
- die "mr $action: no repositories found to work on\n";
-}
-print "mr $action: finished (".join("; ",
- showstat($#ok+1, "ok", "ok"),
- showstat($#failed+1, "failed", "failed"),
- showstat($#skipped+1, "skipped", "skipped"),
-).")\n";
-if ($stats) {
- if (@skipped) {
- print "mr $action: (skipped: ".join(" ", @skipped).")\n";
+ my $rcs=rcs_test(@_);
+
+ if (defined $rcs &&
+ exists $config{$topdir}{$subdir}{$rcs."_".$action}) {
+ return $config{$topdir}{$subdir}{$rcs."_".$action};
}
- if (@failed) {
- print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
+ else {
+ return undef;
}
-}
-if (@failed) {
- exit 1;
-}
-elsif (! @ok && @skipped) {
- exit 1;
-}
-exit 0;
+} #}}}
sub action { #{{{
my ($action, $dir, $topdir, $subdir) = @_;
$ENV{MR_CONFIG}=$configfiles{$topdir};
my $lib=exists $config{$topdir}{$subdir}{lib} ?
$config{$topdir}{$subdir}{lib}."\n" : "";
+ my $is_checkout=($action eq 'checkout');
- if ($action eq 'checkout') {
+ if ($is_checkout) {
if (-d $dir) {
print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
return SKIPPED;
}
$dir=~s/^(.*)\/[^\/]+\/?$/$1/;
-
- if (! -d $dir) {
- print "mr $action: creating parent directory $dir\n" if $verbose;
- system("mkdir", "-p", $dir);
- }
}
elsif ($action =~ /update/) {
if (! -d $dir) {
return action("checkout", $dir, $topdir, $subdir);
}
}
-
+
$ENV{MR_REPO}=$dir;
- if (exists $config{$topdir}{$subdir}{skip}) {
+ my $skiptest=findcommand("skip", $dir, $topdir, $subdir, $is_checkout);
+ my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
+
+ if (defined $skiptest) {
my $test="set -e;".$lib.
- "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
+ "my_action(){ $skiptest\n }; my_action '$action'";
print "mr $action: running skip test >>$test<<\n" if $verbose;
my $ret=system($test);
if ($ret != 0) {
return SKIPPED;
}
}
-
- if (! $nochdir && ! chdir($dir)) {
+
+ if ($is_checkout && ! -d $dir) {
+ print "mr $action: creating parent directory $dir\n" if $verbose;
+ system("mkdir", "-p", $dir);
+ }
+
+ if (! $no_chdir && ! chdir($dir)) {
print STDERR "mr $action: failed to chdir to $dir: $!\n";
return FAILED;
}
- elsif (! exists $config{$topdir}{$subdir}{$action}) {
- print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
- return SKIPPED;
+ elsif (! defined $command) {
+ my $rcs=rcs_test(@_);
+ if (! defined $rcs) {
+ print STDERR "mr $action: unknown repository type and no defined $action command for $topdir$subdir\n";
+ return FAILED;
+ }
+ else {
+ print STDERR "mr $action: no defined action for $rcs repository $topdir$subdir, skipping\n";
+ return SKIPPED;
+ }
}
else {
- if (! $nochdir) {
+ if (! $no_chdir) {
print "mr $action: $topdir$subdir\n";
}
else {
- print "mr $action: $topdir$subdir (in subdir $directory)\n";
+ my $s=$directory;
+ $s=~s/^\Q$topdir$subdir\E\/?//;
+ print "mr $action: $topdir$subdir (in subdir $s)\n";
}
- my $command="set -e; ".$lib.
- "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
+ $command="set -e; ".$lib.
+ "my_action(){ $command\n }; my_action ".
join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
- print STDERR "mr $action: running >>$command<<\n" if $verbose;
+ print "mr $action: running >>$command<<\n" if $verbose;
my $ret=system($command);
if ($ret != 0) {
if (($? & 127) == 2) {
# run actions on multiple repos, in parallel
sub mrs { #{{{
+ my $action=shift;
+ my @repos=@_;
+
$| = 1;
my @active;
my @fhs;
my @out;
my $running=0;
while (@fhs or @repos) {
- while ($running < $jobs && @repos) {
+ while ((!$jobs || $running < $jobs) && @repos) {
$running++;
my $repo = shift @repos;
pipe(my $outfh, CHILD_STDOUT);
waitpid($active[$i][0], 0);
print STDOUT $out[$i][0];
print STDERR $out[$i][1];
- print "\n";
record($active[$i][1], $? >> 8);
splice(@fhs, $i, 1);
splice(@active, $i, 1);
if ($ret == OK) {
push @ok, $dir;
+ print "\n";
}
elsif ($ret == FAILED) {
push @failed, $dir;
+ print "\n";
}
elsif ($ret == SKIPPED) {
push @skipped, $dir;
}
} #}}}
+sub showstats { #{{{
+ my $action=shift;
+ if (! @ok && ! @failed && ! @skipped) {
+ die "mr $action: no repositories found to work on\n";
+ }
+ print "mr $action: finished (".join("; ",
+ showstat($#ok+1, "ok", "ok"),
+ showstat($#failed+1, "failed", "failed"),
+ showstat($#skipped+1, "skipped", "skipped"),
+ ).")\n";
+ if ($stats) {
+ if (@skipped) {
+ print "mr $action: (skipped: ".join(" ", @skipped).")\n";
+ }
+ if (@failed) {
+ print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
+ }
+ }
+} #}}}
+
sub showstat { #{{{
my $count=shift;
my $singular=shift;
return;
} #}}}
+# an ordered list of repos
+sub repolist { #{{{
+ my @list;
+ foreach my $topdir (sort keys %config) {
+ foreach my $subdir (sort keys %{$config{$topdir}}) {
+ push @list, {
+ topdir => $topdir,
+ subdir => $subdir,
+ order => $config{$topdir}{$subdir}{order},
+ };
+ }
+ }
+ return sort {
+ $a->{order} <=> $b->{order}
+ ||
+ $a->{topdir} cmp $b->{topdir}
+ ||
+ $a->{subdir} cmp $b->{subdir}
+ } @list;
+} #}}}
+
+# figure out which repos to act on
+sub selectrepos { #{{{
+ my @repos;
+ foreach my $repo (repolist()) {
+ my $topdir=$repo->{topdir};
+ my $subdir=$repo->{subdir};
+
+ next if $subdir eq 'DEFAULT';
+ my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
+ my $d=$directory;
+ $dir.="/" unless $dir=~/\/$/;
+ $d.="/" unless $d=~/\/$/;
+ next if $dir ne $d && $dir !~ /^\Q$d\E/;
+ if (defined $max_depth) {
+ my @a=split('/', $dir);
+ my @b=split('/', $d);
+ do { } while (@a && @b && shift(@a) eq shift(@b));
+ next if @a > $max_depth || @b > $max_depth;
+ }
+ push @repos, [$dir, $topdir, $subdir];
+ }
+ if (! @repos) {
+ # fallback to find a leaf repo
+ foreach my $repo (reverse repolist()) {
+ my $topdir=$repo->{topdir};
+ my $subdir=$repo->{subdir};
+
+ next if $subdir eq 'DEFAULT';
+ my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
+ my $d=$directory;
+ $dir.="/" unless $dir=~/\/$/;
+ $d.="/" unless $d=~/\/$/;
+ if ($d=~/^\Q$dir\E/) {
+ push @repos, [$dir, $topdir, $subdir];
+ last;
+ }
+ }
+ $no_chdir=1;
+ }
+ return @repos;
+} #}}}
+
my %loaded;
sub loadconfig { #{{{
my $f=shift;
chomp $value;
}
+ if ($parameter eq "include") {
+ print "mr: including output of \"$value\"\n" if $verbose;
+ unshift @lines, `$value`;
+ next;
+ }
+
if (! defined $section) {
die "$f line $.: parameter ($parameter) not in section\n";
}
}
else {
$config{$dir}{$section}{$parameter}=$value;
- $knownactions{$parameter}=1;
+ if ($parameter =~ /.*_(.*)/) {
+ $knownactions{$1}=1;
+ }
+ else {
+ $knownactions{$parameter}=1;
+ }
if ($parameter eq 'chain' &&
length $dir && $section ne "DEFAULT" &&
-e $dir.$section."/.mrconfig") {
my $ret=system($value);
if ($ret != 0) {
if (($? & 127) == 2) {
- print STDERR "mr $action: chain test interrupted\n";
+ print STDERR "mr: chain test interrupted\n";
exit 2;
}
elsif ($? & 127) {
- print STDERR "mr $action: chain test received signal ".($? & 127)."\n";
+ print STDERR "mr: chain test received signal ".($? & 127)."\n";
}
}
else {
close $out;
} #}}}
+sub dispatch { #{{{
+ my $action=shift;
+
+ # actions that do not operate on all repos
+ if ($action eq 'help') {
+ help(@ARGV);
+ }
+ elsif ($action eq 'config') {
+ config(@ARGV);
+ }
+ elsif ($action eq 'register') {
+ register(@ARGV);
+ }
+
+ if (!$jobs || $jobs > 1) {
+ mrs($action, selectrepos());
+ }
+ else {
+ foreach my $repo (selectrepos()) {
+ record($repo, action($action, @$repo));
+ }
+ }
+} #}}}
+
+sub help { #{{{
+ exec($config{''}{DEFAULT}{help}) || die "exec: $!";
+} #}}}
+
+sub config { #{{{
+ if (@_ < 2) {
+ die "mr config: not enough parameters\n";
+ }
+ my $section=shift;
+ if ($section=~/^\//) {
+ # try to convert to a path relative to the config file
+ my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
+ $dir=abs_path($dir);
+ $dir.="/" unless $dir=~/\/$/;
+ if ($section=~/^\Q$dir\E(.*)/) {
+ $section=$1;
+ }
+ }
+ my %changefields;
+ foreach (@_) {
+ if (/^([^=]+)=(.*)$/) {
+ $changefields{$1}=$2;
+ }
+ else {
+ my $found=0;
+ foreach my $topdir (sort keys %config) {
+ if (exists $config{$topdir}{$section} &&
+ exists $config{$topdir}{$section}{$_}) {
+ print $config{$topdir}{$section}{$_}."\n";
+ $found=1;
+ last if $section eq 'DEFAULT';
+ }
+ }
+ if (! $found) {
+ die "mr config: $section $_ not set\n";
+ }
+ }
+ }
+ modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
+ exit 0;
+} #}}}
+
+sub register { #{{{
+ if (! $config_overridden) {
+ # Find the closest known mrconfig file to the current
+ # directory.
+ $directory.="/" unless $directory=~/\/$/;
+ my $foundconfig=0;
+ foreach my $topdir (reverse sort keys %config) {
+ next unless length $topdir;
+ if ($directory=~/^\Q$topdir\E/) {
+ $ENV{MR_CONFIG}=$configfiles{$topdir};
+ $directory=$topdir;
+ $foundconfig=1;
+ last;
+ }
+ }
+ if (! $foundconfig) {
+ $directory=""; # no config file, use builtin
+ }
+ }
+ if (@ARGV) {
+ my $subdir=shift @ARGV;
+ if (! chdir($subdir)) {
+ print STDERR "mr register: failed to chdir to $subdir: $!\n";
+ }
+ }
+
+ $ENV{MR_REPO}=getcwd();
+ my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
+ if (! defined $command) {
+ die "mr register: unknown repository type\n";
+ }
+
+ $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
+ $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n".
+ "my_action(){ $command\n }; my_action ".
+ join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
+ print "mr register: running >>$command<<\n" if $verbose;
+ exec($command) || die "exec: $!";
+} #}}}
+
+# alias expansion and command stemming
+sub expandaction { #{{{
+ my $action=shift;
+ if (exists $alias{$action}) {
+ $action=$alias{$action};
+ }
+ if (! exists $knownactions{$action}) {
+ my @matches = grep { /^\Q$action\E/ }
+ keys %knownactions, keys %alias;
+ if (@matches == 1) {
+ $action=$matches[0];
+ }
+ elsif (@matches == 0) {
+ die "mr: unknown action \"$action\" (known actions: ".
+ join(", ", sort keys %knownactions).")\n";
+ }
+ else {
+ die "mr: ambiguous action \"$action\" (matches: ".
+ join(", ", @matches).")\n";
+ }
+ }
+ return $action;
+} #}}}
+
+sub getopts { #{{{
+ Getopt::Long::Configure("bundling", "no_permute");
+ my $result=GetOptions(
+ "d|directory=s" => sub { $directory=abs_path($_[1]) },
+ "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
+ "v|verbose" => \$verbose,
+ "s|stats" => \$stats,
+ "n|no-recurse:i" => \$max_depth,
+ "j|jobs:i" => \$jobs,
+ );
+ if (! $result || @ARGV < 1) {
+ die("Usage: mr [-d directory] action [params ...]\n".
+ "(Use mr help for man page.)\n");
+ }
+} #}}}
+
+sub init { #{{{
+ $SIG{INT}=sub {
+ print STDERR "mr: interrupted\n";
+ exit 2;
+ };
+
+ # This can happen if it's run in a directory that was removed
+ # or other strangeness.
+ if (! defined $directory) {
+ die("mr: failed to determine working directory\n");
+ }
+ # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
+ # the config file might be a symlink to elsewhere, and the directory it's
+ # in is significant.
+ if ($ENV{MR_CONFIG} !~ /^\//) {
+ $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
+ }
+ # Try to set MR_PATH to the path to the program.
+ eval {
+ use FindBin qw($Bin $Script);
+ $ENV{MR_PATH}=$Bin."/".$Script;
+ };
+} #}}}
+
+sub main { #{{{
+ getopts();
+ init();
+ loadconfig(\*DATA);
+ loadconfig($ENV{MR_CONFIG});
+ #use Data::Dumper; print Dumper(\%config);
+
+ my $action=expandaction(shift @ARGV);
+ dispatch($action);
+ showstats($action);
+
+ if (@failed) {
+ exit 1;
+ }
+ elsif (! @ok && @skipped) {
+ exit 1;
+ }
+ else {
+ exit 0;
+ }
+} #}}}
+
# Finally, some useful actions that mr knows about by default.
# These can be overridden in ~/.mrconfig.
#DATA{{{
ls = list
[DEFAULT]
+order = 10
lib =
error() {
echo "mr: $@" >&2
exit 1
}
+ warning() {
+ echo "mr (warning): $@" >&2
+ }
+ info() {
+ echo "mr: $@" >&2
+ }
hours_since() {
- for dir in .git .svn .bzr CVS .hg; do
+ if [ -z "$1" ] || [ -z "$2" ]; then
+ error "mr: usage: hours_since action num"
+ fi
+ for dir in .git .svn .bzr CVS .hg _darcs; do
if [ -e "$MR_REPO/$dir" ]; then
flagfile="$MR_REPO/$dir/.mr_last$1"
break
if [ -z "$flagfile" ]; then
error "cannot determine flag filename"
fi
- perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
- touch "$flagfile"
- }
-
-update =
- if [ -d "$MR_REPO"/.svn ]; then
- svn update "$@"
- elif [ -d "$MR_REPO"/.git ]; then
- if [ -z "$@" ]; then
- git pull -t origin master
+ delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
+ if [ "$delta" -lt "$2" ]; then
+ exit 0
else
- git pull "$@"
+ touch "$flagfile"
+ exit 1
fi
- elif [ -d "$MR_REPO"/.bzr ]; then
- bzr merge "$@"
- elif [ -d "$MR_REPO"/CVS ]; then
- cvs update "$@"
- elif [ -d "$MR_REPO"/.hg ]; then
- hg pull "$@" && hg update "$@"
- else
- error "unknown repo type"
- fi
-status =
- if [ -d "$MR_REPO"/.svn ]; then
- svn status "$@"
- elif [ -d "$MR_REPO"/.git ]; then
- git status "$@" || true
- elif [ -d "$MR_REPO"/.bzr ]; then
- bzr status "$@"
- elif [ -d "$MR_REPO"/CVS ]; then
- cvs status "$@"
- elif [ -d "$MR_REPO"/.hg ]; then
- hg status "$@"
- else
- error "unknown repo type"
- fi
-commit =
- if [ -d "$MR_REPO"/.svn ]; then
- svn commit "$@"
- elif [ -d "$MR_REPO"/.git ]; then
- git commit -a "$@" && git push --all
- elif [ -d "$MR_REPO"/.bzr ]; then
- bzr commit "$@" && bzr push
- elif [ -d "$MR_REPO"/CVS ]; then
- cvs commit "$@"
- elif [ -d "$MR_REPO"/.hg ]; then
- hg commit -m "$@" && hg push
- else
- error "unknown repo type"
- fi
-diff =
- if [ -d "$MR_REPO"/.svn ]; then
- svn diff "$@"
- elif [ -d "$MR_REPO"/.git ]; then
- git diff "$@"
- elif [ -d "$MR_REPO"/.bzr ]; then
- bzr diff "$@"
- elif [ -d "$MR_REPO"/CVS ]; then
- cvs diff "$@"
- elif [ -d "$MR_REPO"/.hg ]; then
- hg diff "$@"
- else
- error "unknown repo type"
+ }
+
+svn_test = test -d "$MR_REPO"/.svn
+git_test = test -d "$MR_REPO"/.git
+bzr_test = test -d "$MR_REPO"/.bzr
+cvs_test = test -d "$MR_REPO"/CVS
+hg_test = test -d "$MR_REPO"/.hg
+darcs_test = test -d "$MR_REPO"/_darcs
+git_bare_test =
+ test -d "$MR_REPO"/refs/heads && test -d "$MR_REPO"/refs/tags &&
+ test -d "$MR_REPO"/objects && test -f "$MR_REPO"/config &&
+ test "`GIT_CONFIG="$MR_REPO"/config git config --get core.bare`" = true
+
+svn_update = svn update "$@"
+git_update = if [ "$@" ]; then git pull "$@"; else git pull -t origin master; fi
+bzr_update = bzr merge "$@"
+cvs_update = cvs update "$@"
+hg_update = hg pull "$@" && hg update "$@"
+darcs_update = darcs pull -a "$@"
+
+svn_status = svn status "$@"
+git_status = git status "$@" || true
+bzr_status = bzr status "$@"
+cvs_status = cvs status "$@"
+hg_status = hg status "$@"
+darcs_status = darcs whatsnew -ls "$@"
+
+svn_commit = svn commit "$@"
+git_commit = git commit -a "$@" && git push --all
+bzr_commit = bzr commit "$@" && bzr push
+cvs_commit = cvs commit "$@"
+hg_commit = hg commit -m "$@" && hg push
+darcs_commit = darcs record -a -m "$@" && darcs push -a
+
+svn_diff = svn diff "$@"
+git_diff = git diff "$@"
+bzr_diff = bzr diff "$@"
+cvs_diff = cvs diff "$@"
+hg_diff = hg diff "$@"
+darcs_diff = darcs diff "$@"
+
+svn_log = svn log "$@"
+git_log = git log "$@"
+bzr_log = bzr log "$@"
+cvs_log = cvs log "$@"
+hg_log = hg log "$@"
+darcs_log = darcs changes "$@"
+git_bare_log = git log "$@"
+
+svn_register =
+ url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
+ if [ -z "$url" ]; then
+ error "cannot determine svn url"
fi
-log =
- if [ -d "$MR_REPO"/.svn ]; then
- svn log"$@"
- elif [ -d "$MR_REPO"/.git ]; then
- git log "$@"
- elif [ -d "$MR_REPO"/.bzr ]; then
- bzr log "$@"
- elif [ -d "$MR_REPO"/CVS ]; then
- cvs log "$@"
- elif [ -d "$MR_REPO"/.hg ]; then
- hg log "$@"
- else
- error "unknown repo type"
+ echo "Registering svn url: $url in $MR_CONFIG"
+ mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
+git_register =
+ url="`LC_ALL=C git config --get remote.origin.url`" || true
+ if [ -z "$url" ]; then
+ error "cannot determine git url"
fi
-register =
- if [ -n "$1" ]; then
- cd "$1"
+ echo "Registering git url: $url in $MR_CONFIG"
+ mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
+bzr_register =
+ url=`cat .bzr/branch/parent`
+ if [ -z "$url" ]; then
+ error "cannot determine bzr url"
fi
- basedir="$(basename $(pwd))"
- if [ -d .svn ]; then
- url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
- if [ -z "$url" ]; then
- error "cannot determine svn url"
- fi
- echo "Registering svn url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "$(pwd)" checkout="svn co $url $basedir"
- elif [ -d .git ]; then
- url=$(LANG=C git-config --get remote.origin.url)
- if [ -z "$url" ]; then
- error "cannot determine git url"
+ echo "Registering bzr url: $url in $MR_CONFIG"
+ mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr clone '$url' '$MR_REPO'"
+cvs_register =
+ repo=`cat CVS/Repository`
+ root=`cat CVS/Root`
+ if [ -z "$root" ]; then
+ error "cannot determine cvs root"
fi
- echo "Registering git url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "$(pwd)" checkout="git clone $url $basedir"
- elif [ -d .bzr ]; then
- url=$(cat .bzr/branch/parent)
- if [ -z "$url" ]; then
- error "cannot determine bzr url"
- fi
- echo "Registering bzr url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
- elif [ -d CVS ]; then
- repo=$(cat CVS/Repository)
- root=$(cat CVS/Root)
- if [ -z "$root" ]; then
- error "cannot determine cvs root"
- fi
- echo "Registering cvs repository $repo at root $root"
- mr -c "$MR_CONFIG" config "$(pwd)" \
- checkout="cvs -d '$root' co -d $basedir $repo"
- elif [ -d .hg ]; then
- url=$(hg showconfig paths.default)
- echo "Registering mercurial repo url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "$(pwd)" \
- checkout="hg clone $url $basedir"
- else
- error "unable to register this repo type"
+ echo "Registering cvs repository $repo at root $root"
+ mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
+hg_register =
+ url=`hg showconfig paths.default`
+ echo "Registering mercurial repo url: $url in $MR_CONFIG"
+ mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
+darcs_register =
+ url=`cat _darcs/prefs/defaultrepo`
+ echo "Registering darcs repository $url in $MR_CONFIG"
+ mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
+git_bare_register =
+ url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
+ if [ -z "$url" ]; then
+ error "cannot determine git url"
fi
+ echo "Registering git url: $url in $MR_CONFIG"
+ mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
+
help =
if [ ! -e "$MR_PATH" ]; then
error "cannot find program path"
T = echo "I pity the fool."
right = echo "Not found."
#}}}
+
+# vim:sw=8:sts=0:ts=8:noet