#!/usr/bin/perl
-#man{{{
-
=head1 NAME
mr - a Multiple Repository management tool
B<mr> [options] log
+B<mr> [options] bootstrap url
+
B<mr> [options] register [repository]
B<mr> [options] config section ["parameter=[value]" ...]
B<mr> [options] action [params ...]
+B<mr> [options] [online|offline]
+
+B<mr> [options] remember action [params ...]
+
=head1 DESCRIPTION
B<mr> is a Multiple Repository management tool. It can checkout, update, or
The optional -m parameter allows specifying a commit message.
+=item push
+
+Pushes committed local changes to the remote repository. A no-op for
+centralized revision control systems.
+
=item diff
Show a diff of uncommitted changes.
=over 4
+=item bootstrap url
+
+Causes mr to download the url, save it to a .mrconfig file in the
+current directory, and then check out all repositories listed in it.
+
+(Please only do this if you have reason to trust the url, since
+mrconfig files can contain arbitrary commands!)
+
=item list (or ls)
List the repositories that mr will act on.
The ~/.mrconfig file is used by default. To use a different config file,
use the -c option.
+=item offline
+
+Advises mr that it is in offline mode. Any commands that fail in
+offline mode will be remembered, and retried when mr is told it's online.
+
+=item online
+
+Advices mr that it is in online mode again. Commands that failed while in
+offline mode will be re-run.
+
+=item remember
+
+Remember a command, to be run later when mr re-enters online mode. This
+implicitly puts mr into offline mode. The command can be any regular mr
+command. This is useful when you know that a command will fail due to being
+offline, and so don't want to run it right now at all, but just remember
+to run it when you go back online.
+
=item help
Displays this help.
Use the specified mrconfig file. The default is B<~/.mrconfig>
+=item -p
+
+Search in the current directory, and its parent directories and use
+the first B<.mrconfig> found, instead of the default B<~/.mrconfig>.
+
=item -v
Be verbose.
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
+ssh connection limits. Running between 3 and 5 jobs at a time will yield
a good speedup in updates without loading the machine too much.
=back
override these rcs specific actions. To add a new revision control system,
you can just add rcs specific actions for it.
+The ~/.mrlog file contains commands that mr has remembered to run later,
+due to being offline. You can delete or edit this file to remove commands,
+or even to add other commands for 'mr online' to run. If the file is
+present, mr assumes it is in offline mode.
+
+=head1 EXTENSIONS
+
+mr can be extended to support things such as unison and git-svn. Some
+files providing such extensions are available in /usr/share/mr/. See
+the documentation in the files for details about using them.
+
=head1 AUTHOR
-Copyright 2007 Joey Hess <joey@kitenet.net>
+Copyright 2007-2009 Joey Hess <joey@kitenet.net>
Licensed under the GNU GPL version 2 or higher.
=cut
-#}}}
-
use warnings;
use strict;
use Getopt::Long;
main();
my %rcs;
-sub rcs_test { #{{{
+sub rcs_test {
my ($action, $dir, $topdir, $subdir) = @_;
if (exists $rcs{$dir}) {
else {
return $rcs{$dir}=$rcs;
}
-} #}}}
+}
-sub findcommand { #{{{
+sub findcommand {
my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
if (exists $config{$topdir}{$subdir}{$action}) {
else {
return undef;
}
-} #}}}
+}
-sub action { #{{{
+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');
+ $ENV{MR_REPO}=$dir;
+
if ($is_checkout) {
if (-d $dir) {
print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
}
}
- $ENV{MR_REPO}=$dir;
-
my $skiptest=findcommand("skip", $dir, $topdir, $subdir, $is_checkout);
my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
print STDERR "mr $action: failed ($ret)\n" if $verbose;
if ($ret >> 8 != 0) {
print STDERR "mr $action: command failed\n";
+ if (-e "$ENV{HOME}/.mrlog" && $action ne 'remember') {
+ # recreate original command line to
+ # remember, and avoid recursing
+ my @orig=@ARGV;
+ @ARGV=('-n', $action, @orig);
+ action("remember", $dir, $topdir, $subdir);
+ @ARGV=@orig;
+ }
}
elsif ($ret != 0) {
print STDERR "mr $action: command died ($ret)\n";
return OK;
}
}
-} #}}}
+}
# run actions on multiple repos, in parallel
-sub mrs { #{{{
+sub mrs {
my $action=shift;
my @repos=@_;
}
}
}
-} #}}}
+}
-sub record { #{{{
+sub record {
my $dir=shift()->[0];
my $ret=shift;
else {
die "unknown exit status $ret";
}
-} #}}}
+}
-sub showstats { #{{{
+sub showstats {
my $action=shift;
if (! @ok && ! @failed && ! @skipped) {
die "mr $action: no repositories found to work on\n";
print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
}
}
-} #}}}
+}
-sub showstat { #{{{
+sub showstat {
my $count=shift;
my $singular=shift;
my $plural=shift;
return "$count ".($count > 1 ? $plural : $singular);
}
return;
-} #}}}
+}
# an ordered list of repos
-sub repolist { #{{{
+sub repolist {
my @list;
foreach my $topdir (sort keys %config) {
foreach my $subdir (sort keys %{$config{$topdir}}) {
||
$a->{subdir} cmp $b->{subdir}
} @list;
-} #}}}
+}
# figure out which repos to act on
-sub selectrepos { #{{{
+sub selectrepos {
my @repos;
foreach my $repo (repolist()) {
my $topdir=$repo->{topdir};
$no_chdir=1;
}
return @repos;
-} #}}}
+}
-sub expandenv { #{{{
+sub expandenv {
my $val=shift;
}
return $val;
-} #}}}
+}
my %loaded;
-sub loadconfig { #{{{
+sub loadconfig {
my $f=shift;
my @toload;
if ($parameter eq "include") {
print "mr: including output of \"$value\"\n" if $verbose;
unshift @lines, `$value`;
+ if ($?) {
+ print STDERR "mr: include command exited nonzero ($?)\n";
+ }
next;
}
foreach (@toload) {
loadconfig($_);
}
-} #}}}
+}
-sub modifyconfig { #{{{
+sub modifyconfig {
my $f=shift;
# the section to modify or add
my $targetsection=shift;
open(my $out, ">", $f) || die "mr: write $f: $!\n";
print $out @out;
close $out;
-} #}}}
+}
-sub dispatch { #{{{
+sub dispatch {
my $action=shift;
# actions that do not operate on all repos
elsif ($action eq 'register') {
register(@ARGV);
}
+ elsif ($action eq 'bootstrap') {
+ bootstrap();
+ }
+ elsif ($action eq 'remember' ||
+ $action eq 'offline' ||
+ $action eq 'online') {
+ my @repos=selectrepos;
+ action($action, @{$repos[0]}) if @repos;
+ exit 0;
+ }
if (!$jobs || $jobs > 1) {
mrs($action, selectrepos());
record($repo, action($action, @$repo));
}
}
-} #}}}
+}
-sub help { #{{{
+sub help {
exec($config{''}{DEFAULT}{help}) || die "exec: $!";
-} #}}}
-
-sub config { #{{{
+}
+
+sub config {
if (@_ < 2) {
die "mr config: not enough parameters\n";
}
}
modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
exit 0;
-} #}}}
+}
-sub register { #{{{
- if (! $config_overridden) {
+sub register {
+ if ($config_overridden) {
+ # Find the directory that the specified config file is
+ # located in.
+ ($directory)=abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/;
+ }
+ else {
# Find the closest known mrconfig file to the current
# directory.
$directory.="/" unless $directory=~/\/$/;
join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
print "mr register: running >>$command<<\n" if $verbose;
exec($command) || die "exec: $!";
-} #}}}
+}
+
+sub bootstrap {
+ my $url=shift @ARGV;
+
+ if (! defined $url || ! length $url) {
+ die "mr: bootstrap requires url\n";
+ }
+
+ if (-e ".mrconfig") {
+ die "mr: .mrconfig file already exists, not overwriting with $url\n";
+ }
+
+ if (system("curl", "-s", $url, "-o", ".mrconfig") != 0) {
+ die "mr: download of $url failed\n";
+ }
+
+ exec("mr $ENV{MR_SWITCHES} -c .mrconfig checkout");
+ die "failed to run mr checkout";
+}
# alias expansion and command stemming
-sub expandaction { #{{{
+sub expandaction {
my $action=shift;
if (exists $alias{$action}) {
$action=$alias{$action};
}
}
return $action;
-} #}}}
+}
-sub getopts { #{{{
+sub find_nearest_mrconfig {
+ my $dir=getcwd();
+ while (length $dir) {
+ if (-e "$dir/.mrconfig") {
+ return "$dir/.mrconfig";
+ }
+ $dir=~s/\/[^\/]*$//;
+ }
+ die "no .mrconfig found in path\n";
+}
+
+sub getopts {
+ my @saved=@ARGV;
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 },
+ "p|path" => sub { $ENV{MR_CONFIG}=find_nearest_mrconfig(); $config_overridden=1 },
"v|verbose" => \$verbose,
"q|quiet" => \$quiet,
"s|stats" => \$stats,
die("Usage: mr [-d directory] action [params ...]\n".
"(Use mr help for man page.)\n");
}
-} #}}}
+
+ $ENV{MR_SWITCHES}="";
+ foreach my $option (@saved) {
+ last if $option eq $ARGV[0];
+ $ENV{MR_SWITCHES}.="$option ";
+ }
+}
-sub init { #{{{
+sub init {
$SIG{INT}=sub {
print STDERR "mr: interrupted\n";
exit 2;
use FindBin qw($Bin $Script);
$ENV{MR_PATH}=$Bin."/".$Script;
};
-} #}}}
+}
-sub main { #{{{
+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);
else {
exit 0;
}
-} #}}}
+}
# Finally, some useful actions that mr knows about by default.
# These can be overridden in ~/.mrconfig.
-#DATA{{{
__DATA__
[ALIAS]
co = checkout
svn_update = svn update "$@"
git_update = git pull "$@"
-bzr_update = bzr merge "$@"
+bzr_update = bzr merge --pull "$@"
cvs_update = cvs update "$@"
hg_update = hg pull "$@" && hg update "$@"
darcs_update = darcs pull -a "$@"
hg_record = hg commit -m "$@"
darcs_record = darcs record -a -m "$@"
+svn_push = :
+git_push = git push "$@"
+bzr_push = bzr push "$@"
+cvs_push = :
+hg_push = hg push "$@"
+darcs_push = darcs push -a "$@"
+
svn_diff = svn diff "$@"
git_diff = git diff "$@"
bzr_diff = bzr diff "$@"
man -l "$tmp" || error "man failed"
list = true
config =
+bootstrap =
+
+online =
+ if [ -s ~/.mrlog ]; then
+ info "running offline commands"
+ mv -f ~/.mrlog ~/.mrlog.old
+ if ! sh -e ~/.mrlog.old; then
+ error "offline command failed; left in ~/.mrlog.old"
+ fi
+ rm -f ~/.mrlog.old
+ else
+ info "no offline commands to run"
+ fi
+offline =
+ umask 077
+ touch ~/.mrlog
+ info "offline mode enabled"
+remember =
+ info "remembering command: 'mr $@'"
+ command="mr -d '$(pwd)' $MR_SWITCHES"
+ for w in "$@"; do
+ command="$command '$w'"
+ done
+ if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
+ echo "$command" >> ~/.mrlog
+ fi
ed = echo "A horse is a horse, of course, of course.."
T = echo "I pity the fool."
right = echo "Not found."
-#}}}
# vim:sw=8:sts=0:ts=8:noet