B<mr> [options] record [-m "message"]
+B<mr> [options] push
+
B<mr> [options] diff
B<mr> [options] log
+B<mr> [options] run command [param ...]
+
B<mr> [options] bootstrap url [directory]
B<mr> [options] register [repository]
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 and darcs repositories, and support for other revision control systems can
-easily be added.
+bzr, darcs and fossil 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
B<mr> is configured by .mrconfig files, which list the repositories. It
starts by reading the .mrconfig file in your home directory, and this can
-in turn chain load .mrconfig files from repositories.
+in turn chain load .mrconfig files from repositories. It also automatically
+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
control system:
Show the commit log.
+=item run command [param ...]
+
+Runs the specified command in each repository.
+
=back
These commands are also available:
directory to register.
The mrconfig file that is modified is chosen by either the -c option, or by
-looking for the closest known one at or below the current directory.
+looking for the closest known one at or in a parent of the current directory.
=item config
mr config DEFAULT lib
-The ~/.mrconfig file is used by default. To use a different config file,
-use the -c option.
+The mrconfig file that is used is chosen by either the -c option, or by
+looking for the closest known one at or in a parent of the current directory.
=item offline
=item --config mrconfig
-Use the specified mrconfig file. The default is B<~/.mrconfig>
-
-=item -p
-
-=item --path
-
-Search in the current directory, and its parent directories and use
-the first B<.mrconfig> found, instead of the default B<~/.mrconfig>.
+Use the specified mrconfig file. The default is to use both B<~/.mrconfig>
+as well as look for a .mrconfig file in the current directory, or in one
+of its parent directories.
=item -v
=item --quiet
-Be quiet.
+Be quiet. This supresses mr's usual output, as well as any output from
+commands that are run (including stderr output). If a command fails,
+the output will be shown.
=item -k
Trust all mrconfig files even if they are not listed in ~/.mrtrust.
Use with caution.
+=item -p
+
+=item --path
+
+This obsolete flag is ignored.
+
=back
-=head1 "MRCONFIG FILES"
+=head1 MRCONFIG FILES
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
Unlike all other parameters, this parameter does not need to be placed
within a section.
+=item deleted
+
+If the "deleted" parameter is set and its command returns true, then
+B<mr> will treat the repository as deleted. It won't ever actually delete
+the repository, but it will warn if it sees the repository's directory.
+This is useful when one mrconfig file is shared amoung multiple machines,
+to keep track of and remember to delete old repositories.
+
=item lib
The "lib" parameter can specify some shell code that will be run before each
command, this can be a useful way to define shell functions for other commands
to use.
+=item fixups
+
+If the "fixups" parameter is set, its command is run whenever a repository
+is checked out, or updated. This provides an easy way to do things
+like permissions fixups, or other tweaks to the repository content,
+whenever the repository is changed.
+
+=item pre_ and post_
+
+If a "pre_action" parameter is set, its command is run before mr performs the
+specified action. Similarly, "post_action" parameters are run after mr
+successfully performs the specified action. For example, "pre_commit" is
+run before committing; "post_update" is run after updating.
+
=back
When looking for a command to run for a given action, mr first looks for
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 "UNTRUSTED MRCONFIG FILES"
+=head1 UNTRUSTED MRCONFIG FILES
Since mrconfig files can contain arbitrary shell commands, they can do
-anything. This flexability is good, but it also allows a malicious mrconfig
+anything. This flexibility is good, but it also allows a malicious mrconfig
file to delete your whole home directory. Such a file might be contained
-inside a repository that your main ~/.mrconfig checks out and chains to. To
-avoid worries about evil commands in a mrconfig file, mr
-has the ability to read mrconfig files in untrusted mode. Such files are
-limited to running only known safe commands (like "git clone") in a
-carefully checked manner.
+inside a repository that your main ~/.mrconfig checks out. To
+avoid worries about evil commands in a mrconfig file, mr defaults to
+reading all mrconfig files other than the main ~/.mrconfig in untrusted
+mode. In untrusted mode, mrconfig files are limited to running only known
+safe commands (like "git clone") in a carefully checked manner.
+
+To configure mr to trust other mrconfig files, list them in ~/.mrtrust.
+One mrconfig file should be listed per line. Either the full pathname
+should be listed, or the pathname can start with "~/" to specify a file
+relative to your home directory.
-By default, mr trusts all mrconfig files. (This default will change in a
-future release!) But if you have a ~/.mrtrust file, mr will only trust
-mrconfig files that are listed within it. (One file per line.) All other
-files will be treated as untrusted.
+=head1 OFFLINE LOG FILE
+
+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
files providing such extensions are available in /usr/share/mr/. See
the documentation in the files for details about using them.
+=head1 EXIT STATUS
+
+mr returns nonzero if a command failed in any of the repositories.
+
=head1 AUTHOR
-Copyright 2007-2009 Joey Hess <joey@kitenet.net>
+Copyright 2007-2011 Joey Hess <joey@kitenet.net>
Licensed under the GNU GPL version 2 or higher.
my $jobs=1;
my $trust_all=0;
my $directory=getcwd();
-$ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
+
+$ENV{MR_CONFIG}=find_mrconfig();
# globals :-(
my %config;
chomp $rcs;
if ($rcs=~/\n/s) {
$rcs=~s/\n/, /g;
- print STDERR "mr $action: found multiple possible repository types ($rcs) for $topdir$subdir\n";
+ print STDERR "mr $action: found multiple possible repository types ($rcs) for ".fulldir($topdir, $subdir)."\n";
return undef;
}
if (! length $rcs) {
}
}
+sub fulldir {
+ my ($topdir, $subdir) = @_;
+ return $subdir =~ /^\// ? $subdir : $topdir.$subdir;
+}
+
sub action {
my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
-
+ my $fulldir=fulldir($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');
+ my $is_update=($action =~ /update/);
$ENV{MR_REPO}=$dir;
+
+ foreach my $testname ("skip", "deleted") {
+ my $testcommand=findcommand($testname, $dir, $topdir, $subdir, $is_checkout);
+
+ if (defined $testcommand) {
+ my $test="set -e;".$lib.
+ "my_action(){ $testcommand\n }; my_action '$action'";
+ print "mr $action: running $testname test >>$test<<\n" if $verbose;
+ my $ret=system($test);
+ if ($ret != 0) {
+ if (($? & 127) == 2) {
+ print STDERR "mr $action: interrupted\n";
+ return ABORT;
+ }
+ elsif ($? & 127) {
+ print STDERR "mr $action: $testname test received signal ".($? & 127)."\n";
+ return ABORT;
+ }
+ }
+ if ($ret >> 8 == 0) {
+ if ($testname eq "deleted") {
+ if (-d $dir) {
+ print STDERR "mr error: $dir should be deleted yet still exists\n";
+ return FAILED;
+ }
+ }
+ print "mr $action: skip $dir skipped\n" if $verbose;
+ return SKIPPED;
+ }
+ }
+ }
if ($is_checkout) {
if (! $force_checkout) {
$dir=~s/^(.*)\/[^\/]+\/?$/$1/;
}
}
- elsif ($action =~ /update/) {
+ elsif ($is_update) {
if (! -d $dir) {
return action("checkout", $dir, $topdir, $subdir);
}
}
- 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(){ $skiptest\n }; my_action '$action'";
- print "mr $action: running skip test >>$test<<\n" if $verbose;
- my $ret=system($test);
- if ($ret != 0) {
- if (($? & 127) == 2) {
- print STDERR "mr $action: interrupted\n";
- return ABORT;
- }
- elsif ($? & 127) {
- print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
- return ABORT;
- }
- }
- if ($ret >> 8 == 0) {
- print "mr $action: $dir skipped per config file\n" if $verbose;
- return SKIPPED;
- }
- }
-
if ($is_checkout && ! -d $dir) {
print "mr $action: creating parent directory $dir\n" if $verbose;
system("mkdir", "-p", $dir);
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";
+ 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 $topdir$subdir, skipping\n";
+ print STDERR "mr $action: no defined action for $rcs repository $fulldir, skipping\n";
return SKIPPED;
}
}
else {
+ my $actionmsg;
if (! $no_chdir) {
- print "mr $action: $topdir$subdir\n" unless $quiet;
+ $actionmsg="mr $action: $fulldir";
}
else {
my $s=$directory;
- $s=~s/^\Q$topdir$subdir\E\/?//;
- print "mr $action: $topdir$subdir (in subdir $s)\n" unless $quiet;
+ $s=~s/^\Q$fulldir\E\/?//;
+ $actionmsg="mr $action: $fulldir (in subdir $s)";
}
+ print "$actionmsg\n" unless $quiet;
+
+ my $hookret=hook("pre_$action", $topdir, $subdir);
+ return $hookret if $hookret != OK;
+
$command="set -e; ".$lib.
"my_action(){ $command\n }; my_action ".
- join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
+ join(" ", map { s/\\/\\\\/g; s/"/\"/g; '"'.$_.'"' } @ARGV);
print "mr $action: running >>$command<<\n" if $verbose;
- my $ret=system($command);
+ my $ret;
+ if ($quiet) {
+ my $output = qx/$command 2>&1/;
+ $ret = $?;
+ if ($ret != 0) {
+ print "$actionmsg\n";
+ print STDERR $output;
+ }
+ }
+ else {
+ $ret=system($command);
+ }
if ($ret != 0) {
if (($? & 127) == 2) {
print STDERR "mr $action: interrupted\n";
return FAILED;
}
else {
- if ($action eq 'checkout' && ! -d $dir) {
+ if ($is_checkout && ! -d $dir) {
print STDERR "mr $action: $dir missing after checkout\n";;
return FAILED;
}
+ my $ret=hook("post_$action", $topdir, $subdir);
+ return $ret if $ret != OK;
+
+ if (($is_checkout || $is_update)) {
+ my $ret=hook("fixups", $topdir, $subdir);
+ return $ret if $ret != OK;
+ }
+
return OK;
}
}
}
+sub hook {
+ my ($hook, $topdir, $subdir) = @_;
+
+ my $command=$config{$topdir}{$subdir}{$hook};
+ return OK unless defined $command;
+ my $lib=exists $config{$topdir}{$subdir}{lib} ?
+ $config{$topdir}{$subdir}{lib}."\n" : "";
+ my $shell="set -e;".$lib.
+ "my_hook(){ $command\n }; my_hook";
+ print "mr $hook: running >>$shell<<\n" if $verbose;
+ my $ret;
+ if ($quiet) {
+ my $output = qx/$shell 2>&1/;
+ $ret = $?;
+ if ($ret != 0) {
+ print STDERR $output;
+ }
+ }
+ else {
+ $ret=system($shell);
+ }
+ if ($ret != 0) {
+ if (($? & 127) == 2) {
+ print STDERR "mr $hook: interrupted\n";
+ return ABORT;
+ }
+ elsif ($? & 127) {
+ print STDERR "mr $hook: received signal ".($? & 127)."\n";
+ return ABORT;
+ }
+ else {
+ return FAILED;
+ }
+ }
+
+ return OK;
+}
+
# run actions on multiple repos, in parallel
sub mrs {
my $action=shift;
my $trustfile=$ENV{HOME}."/.mrtrust";
- if (! -e $trustfile) {
- print "mr: Assuming $config is trusted.\n";
- print "mr: For better security, you are encouraged to create ~/.mrtrust\n";
- print "mr: and list all trusted mrconfig files in it.\n";
- return 1;
- }
-
if (! %trusted) {
$trusted{"$ENV{HOME}/.mrconfig"}=1;
- open (TRUST, "<", $trustfile) || die "$trustfile: $!";
- while (<TRUST>) {
- chomp;
- s/^~\//$ENV{HOME}\//;
- $trusted{abs_path($_)}=1;
+ if (open (TRUST, "<", $trustfile)) {
+ while (<TRUST>) {
+ chomp;
+ s/^~\//$ENV{HOME}\//;
+ $trusted{abs_path($_)}=1;
+ }
+ close TRUST;
}
- close TRUST;
}
return $trusted{$config};
is_trusted_repo($words[$c])
);
}
- elsif (defined $words[$c] && $twords[$c] eq $words[$c]) {
+ elsif (defined $words[$c] && $words[$c]=~/^($twords[$c])$/) {
$match=1;
}
else {
return 0;
}
+sub trusterror {
+ my ($err, $file, $line, $url)=@_;
+
+ if (defined $url) {
+ die "$err in untrusted $url line $line\n".
+ "(To trust this url, --trust-all can be used; but please use caution;\n".
+ "this can allow arbitrary code execution!)\n";
+ }
+ else {
+ die "$err in untrusted $file line $line\n".
+ "(To trust this file, list it in ~/.mrtrust.)\n";
+ }
+}
+
my %loaded;
sub loadconfig {
my $f=shift;
my $dir=shift;
+ my $bootstrap_url=shift;
my @toload;
$trusted=1;
}
else {
- if (! -e $f) {
- return;
- }
-
my $absf=abs_path($f);
if ($loaded{$absf}) {
return;
}
}
+ if (! -e $f) {
+ return;
+ }
+
print "mr: loading config $f\n" if $verbose;
open($in, "<", $f) || die "mr: open $f: $!\n";
}
if (! is_trusted_repo($section) ||
$section eq 'ALIAS' ||
$section eq 'DEFAULT') {
- die "mr: illegal section \"[$section]\" in untrusted $f line $line\n";
+ trusterror("mr: illegal section \"[$section]\"", $f, $line, $bootstrap_url)
}
}
$section=expandenv($section) if $trusted;
}
if (! $trusted) {
- # Untrusted files can only contain checkout
- # parameters.
- if ($parameter ne 'checkout') {
- die "mr: illegal setting \"$parameter=$value\" in untrusted $f line $line\n";
+ # Untrusted files can only contain a few
+ # settings in specific known-safe formats.
+ if ($parameter eq 'checkout') {
+ if (! is_trusted_checkout($value)) {
+ trusterror("mr: illegal checkout command \"$value\"", $f, $line, $bootstrap_url);
+ }
+ }
+ elsif ($parameter eq 'order') {
+ # not interpreted as a command, so
+ # safe.
}
- if (! is_trusted_checkout($value)) {
- die "mr: illegal checkout command \"$value\" in untrusted $f line $line\n";
+ elsif ($value eq 'true' || $value eq 'false') {
+ # skip=true , deleted=true etc are
+ # safe.
+ }
+ else {
+ trusterror("mr: illegal setting \"$parameter=$value\"", $f, $line, $bootstrap_url);
}
}
$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);
+ join(" ", map { s/\\/\\\\/g; s/"/\"/g; '"'.$_.'"' } @ARGV);
print "mr register: running >>$command<<\n" if $verbose;
exec($command) || die "exec: $!";
}
# would normally be skipped.
my $topdir=abs_path(".")."/";
my @repo=($topdir, $topdir, ".");
- loadconfig($tmpconfig, $topdir);
+ loadconfig($tmpconfig, $topdir, $url);
record(\@repo, action("checkout", @repo, 1))
if exists $config{$topdir}{"."}{"checkout"};
return $action;
}
-sub find_nearest_mrconfig {
+sub find_mrconfig {
my $dir=getcwd();
while (length $dir) {
if (-e "$dir/.mrconfig") {
}
$dir=~s/\/[^\/]*$//;
}
- die "no .mrconfig found in path\n";
+ return "$ENV{HOME}/.mrconfig";
}
sub getopts {
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 },
+ "p|path" => sub { }, # now default, ignore
"v|verbose" => \$verbose,
"q|quiet" => \$quiet,
"s|stats" => \$stats,
if (@failed) {
exit 1;
}
- elsif (! @ok && @skipped) {
- exit 1;
- }
else {
exit 0;
}
init();
startingconfig();
+ loadconfig("$ENV{HOME}/.mrconfig");
loadconfig($ENV{MR_CONFIG});
#use Data::Dumper; print Dumper(\%config);
if [ -z "$1" ] || [ -z "$2" ]; then
error "mr: usage: hours_since action num"
fi
- for dir in .git .svn .bzr CVS .hg _darcs; do
+ for dir in .git .svn .bzr CVS .hg _darcs _FOSSIL_; do
if [ -e "$MR_REPO/$dir" ]; then
flagfile="$MR_REPO/$dir/.mr_last$1"
break
fi
delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
if [ "$delta" -lt "$2" ]; then
- exit 0
+ return 1
else
touch "$flagfile"
- exit 1
+ return 0
fi
}
+ is_bzr_checkout() {
+ LANG=C bzr info | egrep -q '^Checkout'
+ }
svn_test = test -d "$MR_REPO"/.svn
git_test = test -d "$MR_REPO"/.git
cvs_test = test -d "$MR_REPO"/CVS
hg_test = test -d "$MR_REPO"/.hg
darcs_test = test -d "$MR_REPO"/_darcs
+fossil_test = test -f "$MR_REPO"/_FOSSIL_
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 &&
svn_update = svn update "$@"
git_update = git pull "$@"
-bzr_update = bzr merge --pull "$@"
+bzr_update =
+ if is_bzr_checkout; then
+ bzr update "$@"
+ else
+ bzr merge --pull "$@"
+ fi
cvs_update = cvs update "$@"
hg_update = hg pull "$@" && hg update "$@"
darcs_update = darcs pull -a "$@"
+fossil_update = fossil pull "$@"
svn_status = svn status "$@"
-git_status = git status "$@" || true
-bzr_status = bzr status "$@"
+git_status = git status -s "$@" || true
+bzr_status = bzr status --short "$@"
cvs_status = cvs status "$@"
hg_status = hg status "$@"
darcs_status = darcs whatsnew -ls "$@" || true
+fossil_status = fossil changes "$@"
svn_commit = svn commit "$@"
git_commit = git commit -a "$@" && git push --all
-bzr_commit = bzr commit "$@" && bzr push
+bzr_commit =
+ if is_bzr_checkout; then
+ bzr commit "$@"
+ else
+ bzr commit "$@" && bzr push
+ fi
cvs_commit = cvs commit "$@"
hg_commit = hg commit -m "$@" && hg push
darcs_commit = darcs record -a -m "$@" && darcs push -a
+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 "$@"
svn_push = :
git_push = git push "$@"
cvs_push = :
hg_push = hg push "$@"
darcs_push = darcs push -a "$@"
+fossil_push = fossil push "$@"
svn_diff = svn diff "$@"
git_diff = git diff "$@"
cvs_diff = cvs diff "$@"
hg_diff = hg diff "$@"
darcs_diff = darcs diff -u "$@"
+fossil_diff = fossil diff "$@"
svn_log = svn log "$@"
git_log = git log "$@"
hg_log = hg log "$@"
darcs_log = darcs changes "$@"
git_bare_log = git log "$@"
+fossil_log = fossil timeline "$@"
+
+run = "$@"
svn_register =
url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
error "cannot determine bzr url"
fi
echo "Registering bzr url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr clone '$url' '$MR_REPO'"
+ mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr branch '$url' '$MR_REPO'"
cvs_register =
repo=`cat CVS/Repository`
root=`cat CVS/Root`
fi
echo "Registering git url: $url in $MR_CONFIG"
mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
+fossil_register =
+ url=`fossil remote-url`
+ repo=`fossil info | grep repository | sed -e 's/repository:*.//g' -e 's/ //g'`
+ echo "Registering fossil repository $url in $MR_CONFIG"
+ mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && fossil open '$repo'"
svn_trusted_checkout = svn co $url $repo
svn_alt_trusted_checkout = svn checkout $url $repo
git_trusted_checkout = git clone $url $repo
-bzr_trusted_checkout = bzr clone $url $repo
+bzr_trusted_checkout = bzr checkout|clone|branch|get $url $repo
# cvs: too hard
hg_trusted_checkout = hg clone $url $repo
darcs_trusted_checkout = darcs get $url $repo
git_bare_trusted_checkout = git clone --bare $url $repo
+# fossil: messy to do
help =