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.
+respository. 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
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.
been at least 12 hours since the last update.
skip = test $(whoami) != joey
- skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
+ 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>
$ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
my $config_overridden=0;
-my $directory=getcwd();
my $verbose=0;
my $stats=0;
my $no_recurse=0;
my %configfiles;
my %knownactions;
my %alias;
+my $directory=getcwd();
Getopt::Long::Configure("no_permute");
my $result=GetOptions(
"(Use mr help for man page.)\n");
}
+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
next unless length $topdir;
if ($directory=~/^\Q$topdir\E/) {
$ENV{MR_CONFIG}=$configfiles{$topdir};
+ $directory=$topdir;
last;
}
}
}
- my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
- "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
+ if (@ARGV) {
+ my $subdir=shift @ARGV;
+ if (! chdir($subdir)) {
+ print STDERR "mr $action: failed to chdir to $subdir: $!\n";
+ }
+ }
+
+ $ENV{MR_REPO}=getcwd();
+ my $command=findcommand("register", $ENV{MR_CONFIG}, $directory, 'DEFAULT');
+ if (! defined $command) {
+ die "mr $action: unknown repository type\n";
+ }
+
+ $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
+ $command="set -e; ".$config{$ENV{MR_CONFIG}}{$directory}{lib}."\n".
+ "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;
exec($command) || die "exec: $!";
}
+# an ordered list of repos
+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},
+ };
+ }
+}
+@list = sort {
+ $a->{order} <=> $b->{order}
+ ||
+ $a->{topdir} cmp $b->{topdir}
+ ||
+ $a->{subdir} cmp $b->{subdir}
+ } @list;
+
# 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}}) {
+foreach my $repo (@list) {
+ 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 $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
+ foreach my $repo (reverse @list) {
+ 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 $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 ($d=~/^\Q$dir\E/) {
+ push @repos, [$dir, $topdir, $subdir];
+ last;
}
}
$nochdir=1;
else {
foreach my $repo (@repos) {
record($repo, action($action, @$repo));
- print "\n";
}
}
if (! @ok && ! @failed && ! @skipped) {
}
exit 0;
+sub rcs_test { #{{{
+ my ($action, $dir, $topdir, $subdir) = @_;
+
+ 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";
+ }
+ $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 (! length $rcs) {
+ return undef;
+ }
+ else {
+ return $rcs;
+ }
+} #}}}
+
+sub findcommand { #{{{
+ my ($action, $dir, $topdir, $subdir) = @_;
+
+ if (exists $config{$topdir}{$subdir}{$action}) {
+ return $config{$topdir}{$subdir}{$action};
+ }
+
+ my $rcs=rcs_test(@_);
+
+ if (defined $rcs &&
+ exists $config{$topdir}{$subdir}{$rcs."_".$action}) {
+ return $config{$topdir}{$subdir}{$rcs."_".$action};
+ }
+ else {
+ return undef;
+ }
+} #}}}
+
sub action { #{{{
my ($action, $dir, $topdir, $subdir) = @_;
system("mkdir", "-p", $dir);
}
}
- elsif ($action eq 'update') {
+ 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);
+ my $command=findcommand($action, $dir, $topdir, $subdir);
+
+ 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) {
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 command for $rcs repository $topdir$subdir, skipping\n";
+ return SKIPPED;
+ }
}
else {
if (! $nochdir) {
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) {
my $repo = shift @repos;
pipe(my $outfh, CHILD_STDOUT);
pipe(my $errfh, CHILD_STDERR);
- unless (my $pid = fork) {
+ my $pid;
+ unless ($pid = fork) {
die "mr $action: cannot fork: $!" unless defined $pid;
open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
}
close CHILD_STDOUT;
close CHILD_STDERR;
- push @active, $repo;
+ push @active, [$pid, $repo];
push @fhs, [$outfh, $errfh];
push @out, ['', ''];
}
$fhs[$i][$channel] = undef;
if (! defined $fhs[$i][0] &&
! defined $fhs[$i][1]) {
+ waitpid($active[$i][0], 0);
print STDOUT $out[$i][0];
print STDERR $out[$i][1];
- print "\n";
- record($active[$i], $? >> 8);
+ record($active[$i][1], $? >> 8);
splice(@fhs, $i, 1);
splice(@active, $i, 1);
splice(@out, $i, 1);
if ($ret == OK) {
push @ok, $dir;
+ print "\n";
}
elsif ($ret == FAILED) {
push @failed, $dir;
+ print "\n";
}
elsif ($ret == SKIPPED) {
push @skipped, $dir;
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") {
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 commit -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=$(LANG=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="$(LANG=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"
+ 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 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"
- 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'p '$MR_REPO'"
+git_bare_register =
+ url="$(LANG=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