#!/usr/bin/perl
+#man{{{
+
=head1 NAME
mr - a Multiple Repository management tool
B<mr> [options] log
-B<mr> [options] register repository
+B<mr> [options] register [repository]
B<mr> [options] config section [parameter=[value] ...]
=head1 DESCRIPTION
-B<mr> is a Multiple Repository management tool. It allows you to register a
-set of repositories in a .mrconfig file, and then checkout, update, or
-perform other actions on the repositories as if they were one big
-respository.
-
-Any mix of revision control systems can be used with B<mr>, and you can
-define arbitrary actions for commands like "update", "checkout", or "commit".
+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, and bzr repositories,
+and support for other revision control systems can easily be added.
B<mr> cds into and operates on all registered repsitories at or below your
working directory. Or, if you are in a subdirectory of a repository that
contains no other registered repositories, it will stay in that directory,
and work on only that repository,
-The predefined commands should be fairly familiar to users of any revision
+These predefined commands should be fairly familiar to users of any revision
control system:
=over 4
Show the commit log.
+=back
+
+These commands are also available:
+
+=over 4
+
=item list (or ls)
List the repositories that mr will act on.
=item register
-The next parameter is the directory of an existing repository. The
-repository will be registered in the mrconfig file.
+Register an existing repository in the mrconfig file. By default, the
+epository in the current directory is registered, or you can specify a
+directory to register.
=item config
-Modifies the mrconfig file. The next parameter is the name of the section
-to add or modify, and it is followed by one or more instances of
-"parameter=value". Use "parameter=" to remove a parameter.
+Adds, modifies, removes, or prints a value from the mrconfig file. The next
+parameter is the name of the section the value is in. To add or modify
+values, use one or more instances of "parameter=value". Use "parameter=" to
+remove a parameter. Use just "parameter" to get the value of a parameter.
For example, to add (or edit) a repository in src/foo:
mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
+To show the command that mr uses to update the repository in src/foo:
+
+ mr config src/foo update
+
=item help
Displays this help.
Be verbose.
+=item -s
+
+Expand the statistics line displayed at the end to include information
+about exactly which repositories failed and were skipped, if any.
+
=back
=head1 FILES
chain = true
[src/linux-2.6]
- checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
+ checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
+ cd linux-2.6 &&
+ git checkout -b mybranch origin/master
The .mrconfig file uses a variant of the INI file format. Lines starting with
-"#" are comments. Lines ending with "\" are continued on to the next line.
+"#" are comments. Values can be continued to the following line by
+indenting the line with whitespace.
The "DEFAULT" section allows setting default values for the sections that
come after it.
=item skip
If the "skip" parameter is set and its command returns nonzero, then B<mr>
-will skip acting on that repository.
+will skip acting on that repository. The command is passed the action
+name in $1.
+
+Here are two examples. The first skips the repo unless
+mr is run by joey. The second uses the hours_since function
+(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 ]
=item chain
=cut
+#}}}
+
use warnings;
use strict;
use Getopt::Long;
my $directory=getcwd();
my $config="$ENV{HOME}/.mrconfig";
my $verbose=0;
+my $stats=0;
my %config;
my %knownactions;
my %alias;
my $result=GetOptions(
"d|directory=s" => sub { $directory=abs_path($_[1]) },
"c|config=s" => \$config,
- "verbose" => \$verbose,
+ "v|verbose" => \$verbose,
+ "s|stats" => \$stats,
);
if (! $result || @ARGV < 1) {
die("Usage: mr [-d directory] action [params ...]\n".
$section=$1;
}
}
- my %fields;
+ my %changefields;
foreach (@ARGV) {
if (/^([^=]+)=(.*)$/) {
- $fields{$1}=$2;
+ $changefields{$1}=$2;
}
else {
- die "mr config: expected parameter=value, not \"$_\"\n";
+ 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;
+ }
+ }
+ if (! $found) {
+ die "mr $action: $section $_ not set\n";
+ }
}
}
- modifyconfig($config, $section, %fields);
+ modifyconfig($config, $section, %changefields) if %changefields;
exit 0;
}
elsif ($action eq 'register') {
$nochdir=1;
}
-my (@failed, @successful, @skipped);
+my (@failed, @ok, @skipped);
foreach my $repo (@repos) {
action($action, @$repo);
}
-sub action {
+sub action { #{{{
my ($action, $dir, $topdir, $subdir) = @_;
my $lib= exists $config{$topdir}{$subdir}{lib} ?
$ENV{MR_REPO}=$dir;
if (exists $config{$topdir}{$subdir}{skip}) {
- my $test="set -e;".$lib.$config{$topdir}{$subdir}{skip};
+ my $test="set -e;".$lib.
+ "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
print "mr $action: running skip test >>$test<<\n" if $verbose;
my $ret=system($test);
if ($ret >> 8 == 0) {
}
}
else {
- push @successful, $dir;
+ push @ok, $dir;
}
print "\n";
}
-}
+} #}}}
-sub showstat {
+sub showstat { #{{{
my $count=shift;
my $singular=shift;
my $plural=shift;
return "$count ".($count > 1 ? $plural : $singular);
}
return;
-}
-if (! @successful && ! @failed && ! @skipped) {
+} #}}}
+if (! @ok && ! @failed && ! @skipped) {
die "mr $action: no repositories found to work on\n";
}
print "mr $action: finished (".join("; ",
- showstat($#successful+1, "successful", "successful"),
+ 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 "mr $action: (failed: ".join(" ", @failed).")\n";
+ }
+}
if (@failed) {
exit 1;
}
-elsif (! @successful && @skipped) {
+elsif (! @ok && @skipped) {
exit 1;
}
exit 0;
my %loaded;
-sub loadconfig {
+sub loadconfig { #{{{
my $f=shift;
my @toload;
my $in;
my $dir;
if (ref $f eq 'GLOB') {
- $in=$f;
$dir="";
+ $in=$f;
}
else {
if (! -e $f) {
}
$loaded{$absf}=1;
- print "mr: loading config $f\n" if $verbose;
- open($in, "<", $f) || die "mr: open $f: $!\n";
($dir)=$f=~/^(.*\/)[^\/]+$/;
if (! defined $dir) {
$dir=".";
# copy in defaults from first parent
my $parent=$dir;
- while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
+ while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
+ if ($parent eq '/') {
+ $parent="";
+ }
if (exists $config{$parent} &&
exists $config{$parent}{DEFAULT}) {
$config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
last;
}
}
+
+ print "mr: loading config $f\n" if $verbose;
+ open($in, "<", $f) || die "mr: open $f: $!\n";
}
+ my @lines=<$in>;
+ close $in;
my $section;
- while (<$in>) {
+ my $line=0;
+ while (@lines) {
+ $_=shift @lines;
+ $line++;
chomp;
next if /^\s*\#/ || /^\s*$/;
- if (/^\s*\[([^\]]*)\]\s*$/) {
+ if (/^\[([^\]]*)\]\s*$/) {
$section=$1;
}
- elsif (/^\s*(\w+)\s*=\s*(.*)/) {
+ elsif (/^(\w+)\s*=\s*(.*)/) {
my $parameter=$1;
my $value=$2;
- # continuation line
- while ($value=~/(.*)\\$/s) {
- $value=$1."\n".<$in>;
+ # continued value
+ while (@lines && $lines[0]=~/^\s(.+)/) {
+ shift(@lines);
+ $line++;
+ $value.="\n$1";
chomp $value;
}
}
}
else {
- die "$f line $.: parse error\n";
+ die "$f line $line: parse error\n";
}
}
- close $in;
foreach (@toload) {
loadconfig($_);
}
-}
+} #}}}
-sub modifyconfig {
+sub modifyconfig { #{{{
my $f=shift;
# the section to modify or add
my $targetsection=shift;
close $in;
}
+ my $formatfield=sub {
+ my $field=shift;
+ my @value=split(/\n/, shift);
+
+ return "$field = ".shift(@value)."\n".
+ join("", map { "\t$_\n" } @value);
+ };
my $addfields=sub {
my @blanks;
while ($out[$#out] =~ /^\s*$/) {
foreach my $field (sort keys %changefields) {
if (length $changefields{$field}) {
push @out, "$field = $changefields{$field}\n";
+ delete $changefields{$field};
}
}
push @out, @blanks;
if (/^\s*\#/ || /^\s*$/) {
push @out, $_;
}
- elsif (/^\s*\[([^\]]*)\]\s*$/) {
+ elsif (/^\[([^\]]*)\]\s*$/) {
if (defined $section &&
$section eq $targetsection) {
$addfields->();
push @out, $_;
}
- elsif (/^\s*(\w+)\s*=\s(.*)/) {
+ elsif (/^(\w+)\s*=\s(.*)/) {
my $parameter=$1;
my $value=$2;
- # continuation line
- while ($value=~/(.*\\)$/s) {
- $value=$1."\n".shift(@lines);
+ # continued value
+ while (@lines && $lines[0]=~/^\s(.+)/) {
+ shift(@lines);
+ $value.="\n$1";
chomp $value;
}
}
}
- push @out, "$parameter = $value\n";
+ push @out, $formatfield->($parameter, $value);
}
}
if (defined $section &&
$section eq $targetsection) {
- $addfields->();
+ $addfields->();
}
elsif (%changefields) {
push @out, "\n[$targetsection]\n";
foreach my $field (sort keys %changefields) {
if (length $changefields{$field}) {
- push @out, "$field = $changefields{$field}\n";
+ push @out, $formatfield->($field, $changefields{$field});
}
}
}
open(my $out, ">", $f) || die "mr: write $f: $!\n";
print $out @out;
close $out;
-}
+} #}}}
# Finally, some useful actions that mr knows about by default.
# These can be overridden in ~/.mrconfig.
+#DATA{{{
__DATA__
[ALIAS]
- co = checkout
- ci = commit
- ls = list
+co = checkout
+ci = commit
+ls = list
[DEFAULT]
-lib = \
- error() { \
- echo "mr: $@" >&2 \
- exit 1 \
+lib =
+ error() {
+ echo "mr: $@" >&2
+ exit 1
+ }
+ hours_since() {
+ for dir in .git .svn .bzr CVS; do
+ if [ -e "$MR_REPO/$dir" ]; then
+ flagfile="$MR_REPO/$dir/.mr_last$1"
+ break
+ fi
+ done
+ 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 \
- git pull origin master "$@" \
- elif [ -d "$MR_REPO"/.bzr ]; then \
- bzr merge "$@" \
- elif [ -d "$MR_REPO"/CVS ]; then \
- cvs update "$@" \
- else \
- error "unknown repo type" \
+update =
+ if [ -d "$MR_REPO"/.svn ]; then
+ svn update "$@"
+ elif [ -d "$MR_REPO"/.git ]; then
+ git pull origin master "$@"
+ elif [ -d "$MR_REPO"/.bzr ]; then
+ bzr merge "$@"
+ elif [ -d "$MR_REPO"/CVS ]; then
+ cvs 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 "$@"
+ 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 "$@"
+ 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 "$@" \
- else \
- error "unknown repo type" \
+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 "$@"
+ 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 "$@" \
- else \
- error "unknown repo type" \
+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 "$@"
+ 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 "$@" \
- else \
- error "unknown repo type" \
+register =
+ if [ -n "$1" ]; then
+ cd "$1"
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 "$@" \
- else \
- error "unknown repo type" \
+ 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"
+ mr 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"
+ fi
+ echo "Registering git url: $url"
+ mr 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"
+ mr config "$(pwd)" checkout="bzr clone $url $basedir"
+ else
+ error "unable to register this repo type"
fi
-register = \
- if [ -z "$1" ]; then \
- error "repository directory not specified" \
- fi \
- cd "$1" \
- 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" \
- mr 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" \
- fi \
- echo "Registering git url: $url" \
- mr 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" \
- mr config "$(pwd)" checkout="bzr clone $url $basedir" \
- else \
- error "unable to register this repo type" \
+help =
+ if [ ! -e "$MR_PATH" ]; then
+ error "cannot find program path"
fi
+ (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
list = true
config =
-help = \
- if [ ! -e "$MR_PATH" ]; then \
- error "cannot find program path" \
- fi \
- (pod2man -c mr "$MR_PATH" | man -l -) || \
- error "pod2man or man failed"
ed = echo "A horse is a horse, of course, of course.."
T = echo "I pity the fool."
+right = echo "Not found."
+#}}}