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
repository. It supports any combination of subversion, git, cvs, mercurial,
-bzr, darcs and fossil repositories, and support for other revision
+bzr, darcs and fossil repositories, and support for other version
control systems can easily be added.
B<mr> cds into and operates on all registered repositories at or below your
looks for a .mrconfig file in the current directory, or in one of its
parent directories.
-These predefined commands should be fairly familiar to users of any revision
+These predefined commands should be fairly familiar to users of any version
control system:
=over 4
=item record
Records changes to the local repository, but does not push them to the
-remote repository. Only supported for distributed revision control systems.
+remote repository. Only supported for distributed version control systems.
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.
+centralized version control systems.
=item diff
update"
Additional parameters can be passed to most commands, and are passed on
-unchanged to the underlying revision control system. This is mostly useful
-if the repositories mr will act on all use the same revision control
+unchanged to the underlying version control system. This is mostly useful
+if the repositories mr will act on all use the same version control
system.
=head1 OPTIONS
Here is an example .mrconfig file:
[src]
- checkout = svn co svn://svn.example.com/src/trunk src
+ checkout = svn checkout svn://svn.example.com/src/trunk src
chain = true
[src/linux-2.6]
Within a section, each parameter defines a shell command to run to handle a
given action. mr contains default handlers for "update", "status",
-"commit", and other standard actions. Normally you only need to specify what
-to do for "checkout".
+"commit", and other standard actions.
+
+Normally you only need to specify what to do for "checkout". Here you
+specify the command to run in order to create a checkout of the repository.
+The command will be run in the parent directory, and must create the
+repository's directory. So use "git clone", "svn checkout", "bzr branch"
+or "bzr checkout" (for a bound branch), etc.
Note that these shell commands are run in a "set -e" shell
environment, where any additional parameters you pass are available in
-"$@". The "checkout" command is run in the parent of the repository
-directory, since the repository isn't checked out yet. All other commands
-are run inside the repository, though not necessarily at the top of it.
+"$@". All commands other than "checkout" 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. (For the "register" action, "MR_REPO" is instead set to the
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.
+The "MR_ACTION" environment variable is set to the command being run
+(update, checkout, etc).
+
A few parameters have special meanings:
=over 4
(included in mr's built-in library) to skip updating the repo unless it's
been at least 12 hours since the last update.
+ [mystuff]
+ checkout = ...
skip = test `whoami` != joey
+
+ [linux]
+ checkout = ...
skip = [ "$1" = update ] && ! hours_since "$1" 12
+
+Another way to use skip is for a lazy checkout. This makes mr skip
+operating on a repo unless it already exists. To enable the
+repo, you have to explicitly check it out (using "mr -d foo checkout").
+
+ [foo]
+ checkout = ...
+ skip = lazy
=item order
Unlike all other parameters, this parameter does not need to be placed
within a section.
+B<mr> ships several libraries that can be included to add support for
+additional version control type things (unison, git-svn, vcsh, git-fake-bare,
+git-subtree). To include them all, you could use:
+
+ include = cat /usr/share/mr/*
+
+See the individual files for details.
+
=item deleted
If the "deleted" parameter is set and its command returns true, then
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,
+looks for a parameter named "VCS_action" (substituting in the name of the
+version control system and the action). The name of the version control
+system is itself determined by running each defined "VCS_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.
+the action that is performed for a given version control system, you can
+override these VCS specific actions. To add a new version control system,
+you can just add VCS specific actions for it.
=head1 UNTRUSTED MRCONFIG FILES
main();
-my %rcs;
-sub rcs_test {
+my %vcs;
+sub vcs_test {
my ($action, $dir, $topdir, $subdir) = @_;
- if (exists $rcs{$dir}) {
- return $rcs{$dir};
+ if (exists $vcs{$dir}) {
+ return $vcs{$dir};
}
my $test="set -e\n";
- foreach my $rcs_test (
+ foreach my $vcs_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";
+ my ($vcs)=$vcs_test=~/(.*)_test/;
+ $test="my_$vcs_test() {\n$config{$topdir}{$subdir}{$vcs_test}\n}\n".$test;
+ $test.="if my_$vcs_test; then echo $vcs; 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 ($rcs=~/\n/s) {
- $rcs=~s/\n/, /g;
- print STDERR "mr $action: found multiple possible repository types ($rcs) for ".fulldir($topdir, $subdir)."\n";
+ print "mr $action: running vcs test >>$test<<\n" if $verbose;
+ my $vcs=`$test`;
+ chomp $vcs;
+ if ($vcs=~/\n/s) {
+ $vcs=~s/\n/, /g;
+ print STDERR "mr $action: found multiple possible repository types ($vcs) for ".fulldir($topdir, $subdir)."\n";
return undef;
}
- if (! length $rcs) {
- return $rcs{$dir}=undef;
+ if (! length $vcs) {
+ return $vcs{$dir}=undef;
}
else {
- return $rcs{$dir}=$rcs;
+ return $vcs{$dir}=$vcs;
}
}
return undef;
}
- my $rcs=rcs_test(@_);
+ my $vcs=vcs_test(@_);
- if (defined $rcs &&
- exists $config{$topdir}{$subdir}{$rcs."_".$action}) {
- return $config{$topdir}{$subdir}{$rcs."_".$action};
+ if (defined $vcs &&
+ exists $config{$topdir}{$subdir}{$vcs."_".$action}) {
+ return $config{$topdir}{$subdir}{$vcs."_".$action};
}
else {
return undef;
my $is_checkout=($action eq 'checkout');
my $is_update=($action =~ /update/);
- $ENV{MR_REPO}=$dir;
+ ($ENV{MR_REPO}=$dir) =~ s!/$!!;
+ $ENV{MR_ACTION}=$action;
foreach my $testname ("skip", "deleted") {
my $testcommand=findcommand($testname, $dir, $topdir, $subdir, $is_checkout);
return FAILED;
}
elsif (! defined $command) {
- my $rcs=rcs_test(@_);
- if (! defined $rcs) {
+ my $vcs=vcs_test(@_);
+ if (! defined $vcs) {
print STDERR "mr $action: unknown repository type and no defined $action command for $fulldir\n";
return FAILED;
}
else {
- print STDERR "mr $action: no defined action for $rcs repository $fulldir, skipping\n";
+ print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n";
return SKIPPED;
}
}
print STDERR "mr $hook: received signal ".($? & 127)."\n";
return ABORT;
}
+ else {
+ return FAILED;
+ }
}
return OK;
return $ret;
}
-# figure out which repos to act on
+# Figure out which repos to act on. Returns a list of array refs
+# in the format:
+#
+# [ "$full_repo_path/", "$mr_config_path/", $section_header ]
sub selectrepos {
my @repos;
foreach my $repo (repolist()) {
close $in unless ref $f eq 'GLOB';
my $section;
+
+ # Keep track of the current line in the config file;
+ # when a file is included track the current line from the include.
my $line=0;
+ my $included=undef;
+ my $includeline=0;
+ my $nextline = sub {
+ if ($included) {
+ $includeline++;
+ $included--;
+ }
+ else {
+ $included=undef;
+ $includeline=0;
+ $line++;
+ }
+ my $l=shift @lines;
+ chomp $l;
+ return $l
+ };
+ my $lineerror = sub {
+ my $msg=shift;
+ if (defined $included) {
+ die "mr: $f line $line include line $includeline: $msg\n";
+ }
+ else {
+ die "mr: $f line $line: $msg\n";
+ }
+ };
+
while (@lines) {
- $_=shift @lines;
- $line++;
- chomp;
+ $_=$nextline->();
+
+ if (! $trusted && /[[:cntrl:]]/) {
+ trusterror("mr: illegal control character", $f, $line, $bootstrap_url);
+ }
+
next if /^\s*\#/ || /^\s*$/;
if (/^\[([^\]]*)\]\s*$/) {
$section=$1;
# continued value
while (@lines && $lines[0]=~/^\s(.+)/) {
- shift(@lines);
- $line++;
$value.="\n$1";
chomp $value;
+ $nextline->();
}
if (! $trusted) {
if ($parameter eq "include") {
print "mr: including output of \"$value\"\n" if $verbose;
- unshift @lines, `$value`;
+ my @inc=`$value`;
if ($?) {
print STDERR "mr: include command exited nonzero ($?)\n";
}
+ $included += @inc;
+ unshift @lines, @inc;
next;
}
if (! defined $section) {
- die "$f line $.: parameter ($parameter) not in section\n";
+ $lineerror->("parameter ($parameter) not in section");
}
if ($section eq 'ALIAS') {
$alias{$parameter}=$value;
$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: chain test interrupted\n";
- exit 2;
+ length $dir && $section ne "DEFAULT") {
+ my $chaindir="$section";
+ if ($chaindir !~ m!^/!) {
+ $chaindir=$dir.$chaindir;
+ }
+ if (-e "$chaindir/.mrconfig") {
+ my $ret=system($value);
+ if ($ret != 0) {
+ if (($? & 127) == 2) {
+ print STDERR "mr: chain test interrupted\n";
+ exit 2;
+ }
+ elsif ($? & 127) {
+ print STDERR "mr: chain test received signal ".($? & 127)."\n";
+ }
}
- elsif ($? & 127) {
- print STDERR "mr: chain test received signal ".($? & 127)."\n";
+ else {
+ push @toload, ["$chaindir/.mrconfig", $chaindir];
}
}
- else {
- push @toload, $dir.$section."/.mrconfig";
- }
}
}
}
else {
- die "$f line $line: parse error\n";
+ $lineerror->("parse error");
}
}
- foreach (@toload) {
- loadconfig($_);
+ foreach my $c (@toload) {
+ loadconfig(@$c);
}
}
is_bzr_checkout() {
LANG=C bzr info | egrep -q '^Checkout'
}
+ lazy() {
+ if [ "$MR_ACTION" = checkout ] || [ -d "$MR_REPO" ]; then
+ return 1
+ else
+ return 0
+ fi
+ }
svn_test = test -d "$MR_REPO"/.svn
git_test = test -d "$MR_REPO"/.git
fossil_commit = fossil commit "$@"
git_record = git commit -a "$@"
-bzr_record = bzr commit "$@"
+bzr_record =
+ if is_bzr_checkout; then
+ bzr commit --local "$@"
+ else
+ bzr commit "$@"
+ fi
hg_record = hg commit -m "$@"
darcs_record = darcs record -a -m "$@"
fossil_record = fossil commit "$@"