B<mr> [options] record [-m "message"]
+B<mr> [options] fetch
+
B<mr> [options] push
B<mr> [options] diff
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 version
+bzr, darcs, fossil and veracity 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
The optional -m parameter allows specifying a commit message.
+=item fetch
+
+Fetches from each repository's remote repository, but does not
+update the working copy. Only supported for some distributed version
+control systems.
+
=item push
Pushes committed local changes to the remote repository. A no-op for
=item bootstrap url [directory]
-Causes mr to download the url, and use it as a .mrconfig file
-to checkout the repositories listed in it, into the specified directory.
+Causes mr to download the url, and use it as a .mrconfig file to checkout
+the repositories listed in it, into the specified directory.
+
+To use scp to download, the url may have the form ssh://[user@]host:file
The directory will be created if it does not exist. If no directory is
specified, the current directory will be used.
as well as look for a F<.mrconfig> file in the current directory, or in one
of its parent directories.
+=item -f
+
+=item --force
+
+Force mr to act on repositories that would normally be skipped due to their
+configuration.
+
=item -v
=item --verbose
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").
+repo, you have to explicitly check it out (using "mr --force -d foo checkout").
[foo]
checkout = ...
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,
+additional version control type things (unison, git-svn, git-fake-bare,
git-subtree). To include them all, you could use:
include = cat /usr/share/mr/*
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 "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.
+version control system and the action).
Internally, mr has settings for "git_update", "svn_update", etc. To change
the action that is performed for a given version control system, you can
to the existing value of the parameter. In this way, actions
can be constructed accumulatively.
+=item VCS_test
+
+The name of the version control system is itself determined by
+running each defined "VCS_test" action, until one succeeds.
+
=back
=head1 UNTRUSTED MRCONFIG FILES
my $verbose=0;
my $quiet=0;
my $stats=0;
+my $force=0;
my $insecure=0;
my $interactive=0;
my $max_depth;
my $trust_all=0;
my $directory=getcwd();
+my $HOME_MR_CONFIG = "$ENV{HOME}/.mrconfig";
$ENV{MR_CONFIG}=find_mrconfig();
# globals :-(
main();
+sub shellquote {
+ my $i=shift;
+ $i=~s/'/'"'"'/g;
+ return "'$i'";
+}
+
+# Runs a shell command using a supplied function.
+# The lib will be included in the shell command line, and any params
+# will be available in the shell as $1, $2, etc.
+my $lastlib;
+sub runsh {
+ my ($action, $topdir, $subdir, $command, $params, $runner) = @_;
+
+ # optimisation: avoid running the shell for true and false
+ if ($command =~ /^\s*true\s*$/) {
+ $?=0;
+ return 0;
+ }
+ elsif ($command =~ /^\s*false\s*$/) {
+ $?=0;
+ return 1;
+ }
+
+ my $quotedparams=join(" ", (map { shellquote($_) } @$params));
+ my $lib=exists $config{$topdir}{$subdir}{lib} ?
+ $config{$topdir}{$subdir}{lib}."\n" : "";
+ if ($verbose && (! defined $lastlib || $lastlib ne $lib)) {
+ print "mr library now: >>$lib<<\n";
+ $lastlib=$lib;
+ }
+ my $shellcode="set -e;".$lib.
+ "my_sh(){ $command\n }; my_sh $quotedparams";
+ print "mr $action: running $action >>$command<<\n" if $verbose;
+ $runner->($shellcode);
+}
+
+my %perl_cache;
+sub perl {
+ my $id=shift;
+ my $s=shift;
+ if ($s =~ m/^perl:\s+(.*)/s) {
+ return $perl_cache{$1} if exists $perl_cache{$1};
+ my $sub=eval "sub {$1}";
+ if (! defined $sub) {
+ print STDERR "mr: bad perl code in $id: $@\n";
+ }
+ return $perl_cache{$1} = $sub;
+ }
+ return undef;
+}
+
my %vcs;
sub vcs_test {
my ($action, $dir, $topdir, $subdir) = @_;
return $vcs{$dir};
}
- my $test="set -e\n";
+ my $test="";
+ my %perltest;
foreach my $vcs_test (
sort {
length $a <=> length $b
||
$a cmp $b
} grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
- 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";
+ my ($vcs)=$vcs_test =~ /(.*)_test/;
+ my $p=perl($vcs_test, $config{$topdir}{$subdir}{$vcs_test});
+ if (defined $p) {
+ $perltest{$vcs}=$p;
+ }
+ else {
+ $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 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";
+
+ my @vcs;
+ foreach my $vcs (keys %perltest) {
+ if ($perltest{$vcs}->()) {
+ push @vcs, $vcs;
+ }
+ }
+
+ push @vcs, split(/\n/,
+ runsh("vcs test", $topdir, $subdir, $test, [], sub {
+ my $sh=shift;
+ my $ret=`$sh`;
+ return $ret;
+ })) if length $test;
+ if (@vcs > 1) {
+ print STDERR "mr $action: found multiple possible repository types (@vcs) for ".fulldir($topdir, $subdir)."\n";
return undef;
}
- if (! length $vcs) {
+ if (! @vcs) {
return $vcs{$dir}=undef;
}
else {
- return $vcs{$dir}=$vcs;
+ return $vcs{$dir}=$vcs[0];
}
}
sub action {
my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
my $fulldir=fulldir($topdir, $subdir);
+ my $checkout_dir;
$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_ACTION}=$action;
foreach my $testname ("skip", "deleted") {
+ next if $force && $testname eq "skip";
+
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);
+ my $ret=runsh "$testname test", $topdir, $subdir,
+ $testcommand, [$action],
+ sub { system(shift()) };
if ($ret != 0) {
if (($? & 127) == 2) {
print STDERR "mr $action: interrupted\n";
}
if ($is_checkout) {
+ $checkout_dir=$dir;
if (! $force_checkout) {
if (-d $dir) {
print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
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);
- print "mr $action: running >>$command<<\n" if $verbose;
- my $ret;
- if ($quiet) {
- my $output = qx/$command 2>&1/;
- $ret = $?;
- if ($ret != 0) {
- print "$actionmsg\n";
- print STDERR $output;
- }
- }
- else {
- $ret=system($command);
- }
+ my $ret=runsh $action, $topdir, $subdir,
+ $command, \@ARGV, sub {
+ my $sh=shift;
+ if ($quiet) {
+ my $output = qx/$sh 2>&1/;
+ my $ret = $?;
+ if ($ret != 0) {
+ print "$actionmsg\n";
+ print STDERR $output;
+ }
+ return $ret;
+ }
+ else {
+ system($sh);
+ }
+ };
if ($ret != 0) {
if (($? & 127) == 2) {
print STDERR "mr $action: interrupted\n";
my $ret=hook("post_$action", $topdir, $subdir);
return $ret if $ret != OK;
- if (($is_checkout || $is_update)) {
+ if ($is_checkout || $is_update) {
+ if ($is_checkout && ! $no_chdir) {
+ if (! chdir($checkout_dir)) {
+ print STDERR "mr $action: failed to chdir to $checkout_dir: $!\n";
+ return FAILED;
+ }
+ }
my $ret=hook("fixups", $topdir, $subdir);
return $ret if $ret != OK;
}
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);
- }
+ my $ret=runsh $hook, $topdir, $subdir, $command, [], sub {
+ my $sh=shift;
+ if ($quiet) {
+ my $output = qx/$sh 2>&1/;
+ my $ret = $?;
+ if ($ret != 0) {
+ print STDERR $output;
+ }
+ return $ret;
+ }
+ else {
+ system($sh);
+ }
+ };
if ($ret != 0) {
if (($? & 127) == 2) {
print STDERR "mr $hook: interrupted\n";
my $config=shift; # must be abs_pathed already
# We always trust ~/.mrconfig.
- return 1 if $config eq abs_path("$ENV{HOME}/.mrconfig");
+ return 1 if $config eq abs_path($HOME_MR_CONFIG);
return 1 if $trust_all;
my $trustfile=$ENV{HOME}."/.mrtrust";
if (! %trusted) {
- $trusted{"$ENV{HOME}/.mrconfig"}=1;
+ $trusted{$HOME_MR_CONFIG}=1;
if (open (TRUST, "<", $trustfile)) {
while (<TRUST>) {
chomp;
};
my $trusterror = sub {
my $msg=shift;
- my ($err, $file, $lineno, $url)=@_;
if (defined $bootstrap_url) {
- die "mr: $err in untrusted $bootstrap_url line $lineno\n".
+ die "mr: $msg in untrusted $bootstrap_url line $lineno\n".
"(To trust this url, --trust-all can be used; but please use caution;\n".
"this can allow arbitrary code execution!)\n";
}
else {
- die "mr: $err in untrusted $file line $lineno\n".
+ die "mr: $msg in untrusted $f line $lineno\n".
"(To trust this file, list it in ~/.mrtrust.)\n";
}
};
eval q{use File::Temp};
die $@ if $@;
my $tmpconfig=File::Temp->new();
- my @curlargs = ("curl", "-A", "mr", "-L", "-s", $url, "-o", $tmpconfig);
- push(@curlargs, "-k") if $insecure;
- my $curlstatus = system(@curlargs);
- die "mr bootstrap: invalid SSL certificate for $url (consider -k)\n" if $curlstatus >> 8 == 60;
- die "mr bootstrap: download of $url failed\n" if $curlstatus != 0;
+ my @downloader;
+ if ($url =~ m!^ssh://(.*)!) {
+ @downloader = ("scp", $1, $tmpconfig);
+ }
+ else {
+ @downloader = ("curl", "-A", "mr", "-L", "-s", $url, "-o", $tmpconfig);
+ push(@downloader, "-k") if $insecure;
+ }
+ my $status = system(@downloader);
+ die "mr bootstrap: invalid SSL certificate for $url (consider -k)\n"
+ if $downloader[0] eq 'curl' && $status >> 8 == 60;
+ die "mr bootstrap: download of $url failed\n" if $status != 0;
if (! -e $dir) {
system("mkdir", "-p", $dir);
}
$dir=~s/\/[^\/]*$//;
}
- return "$ENV{HOME}/.mrconfig";
+ return $HOME_MR_CONFIG;
}
sub getopts {
"d|directory=s" => sub { $directory=abs_path($_[1]) },
"c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
"p|path" => sub { }, # now default, ignore
+ "f|force" => \$force,
"v|verbose" => \$verbose,
"q|quiet" => \$quiet,
"s|stats" => \$stats,
init();
startingconfig();
- loadconfig("$ENV{HOME}/.mrconfig");
+ loadconfig($HOME_MR_CONFIG);
loadconfig($ENV{MR_CONFIG});
#use Data::Dumper; print Dumper(\%config);
LANG=C bzr info | egrep -q '^Checkout'
}
lazy() {
- if [ "$MR_ACTION" = checkout ] || [ -d "$MR_REPO" ]; then
+ if [ -d "$MR_REPO" ]; then
return 1
else
return 0
fi
}
-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
-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 &&
- test "`GIT_CONFIG="$MR_REPO"/config git config --get core.bare`" = true
+svn_test = perl: -d "$ENV{MR_REPO}/.svn"
+git_test = perl: -e "$ENV{MR_REPO}/.git"
+bzr_test = perl: -d "$ENV{MR_REPO}/.bzr"
+cvs_test = perl: -d "$ENV{MR_REPO}/CVS"
+hg_test = perl: -d "$ENV{MR_REPO}/.hg"
+darcs_test = perl: -d "$ENV{MR_REPO}/_darcs"
+fossil_test = perl: -f "$ENV{MR_REPO}/_FOSSIL_"
+git_bare_test = perl:
+ -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
+ -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
+ `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get core.bare` =~ /true/
+vcsh_test = perl:
+ -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
+ -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
+ `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get vcsh.vcsh` =~ /true/
+veracity_test = perl: -d "$ENV{MR_REPO}/.sgdrawer"
svn_update = svn update "$@"
git_update = git pull "$@"
else
bzr merge --pull "$@"
fi
-cvs_update = cvs update "$@"
-hg_update = hg pull "$@" && hg update "$@"
+cvs_update = cvs -q update "$@"
+hg_update = hg pull "$@"; hg update "$@"
darcs_update = darcs pull -a "$@"
fossil_update = fossil pull "$@"
+vcsh_update = vcsh run "$MR_REPO" git pull "$@"
+veracity_update = vv pull "$@" && vv update "$@"
+
+git_fetch = git fetch --all --prune --tags
+git_svn_fetch = git svn fetch
+darcs_fetch = darcs fetch
+hg_fetch = hg pull
svn_status = svn status "$@"
git_status = git status -s "$@" || true
hg_status = hg status "$@"
darcs_status = darcs whatsnew -ls "$@" || true
fossil_status = fossil changes "$@"
+vcsh_status = vcsh run "$MR_REPO" git -c status.relativePaths=false status -s "$@" || true
+veracity_status = vv status "$@"
svn_commit = svn commit "$@"
git_commit = git commit -a "$@" && git push --all
hg_commit = hg commit -m "$@" && hg push
darcs_commit = darcs record -a -m "$@" && darcs push -a
fossil_commit = fossil commit "$@"
+vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all
+veracity_commit = vv commit -m "@" && vv push
git_record = git commit -a "$@"
bzr_record =
hg_record = hg commit -m "$@"
darcs_record = darcs record -a -m "$@"
fossil_record = fossil commit "$@"
+vcsh_record = vcsh run "$MR_REPO" git commit -a "$@"
+veracity_record = vv commit -m "@"
svn_push = :
git_push = git push "$@"
hg_push = hg push "$@"
darcs_push = darcs push -a "$@"
fossil_push = fossil push "$@"
+vcsh_push = vcsh run "$MR_REPO" git push "$@"
+veracity_push = vv push "$@"
svn_diff = svn diff "$@"
git_diff = git diff "$@"
bzr_diff = bzr diff "$@"
-cvs_diff = cvs diff "$@"
+cvs_diff = cvs -q diff "$@"
hg_diff = hg diff "$@"
darcs_diff = darcs diff -u "$@"
fossil_diff = fossil diff "$@"
+vcsh_diff = vcsh run "$MR_REPO" git diff "$@"
+veracity_diff = vv diff "$@"
svn_log = svn log "$@"
git_log = git log "$@"
darcs_log = darcs changes "$@"
git_bare_log = git log "$@"
fossil_log = fossil timeline "$@"
+vcsh_log = vcsh run "$MR_REPO" git log "$@"
+veracity_log = vv log "$@"
+
+hg_grep = hg grep "$@"
+cvs_grep = ack-grep "$@"
+svn_grep = ack-grep "$@"
+git_svn_grep = git grep "$@"
+git_grep = git grep "$@"
+bzr_grep = ack-grep "$@"
run = "$@"
echo "Registering git url: $url in $MR_CONFIG"
mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
bzr_register =
- url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}'`"
+ url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}' | head -n 1`"
if [ -z "$url" ]; then
error "cannot determine bzr url"
fi
fi
echo "Registering git url: $url in $MR_CONFIG"
mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
+vcsh_register =
+ url="`LC_ALL=C vcsh run "$MR_REPO" 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="vcsh clone '$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'"
+veracity_register =
+ url=`vv config | grep sync_targets | sed -e 's/sync_targets:*.//g' -e 's/ //g'`
+ repo=`vv repo info | grep repository | sed -e 's/Current repository:*.//g' -e 's/ //g'`
+ echo "Registering veracity repository $url in $MR_CONFIG"
+ mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && vv checkout '$repo'"
svn_trusted_checkout = svn co $url $repo
svn_alt_trusted_checkout = svn checkout $url $repo
hg_trusted_checkout = hg clone $url $repo
darcs_trusted_checkout = darcs get $url $repo
git_bare_trusted_checkout = git clone --bare $url $repo
+vcsh_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
# fossil: messy to do
+veracity_trusted_checkout = vv clone $url $repo
help =