-#!/usr/bin/perl
+#!/usr/bin/env perl
=head1 NAME
-mr - a Multiple Repository management tool
+mr - a tool to manage all your version control repos
=head1 SYNOPSIS
B<mr> [options] status
+B<mr> [options] clean [-f]
+
B<mr> [options] commit [-m "message"]
B<mr> [options] record [-m "message"]
+B<mr> [options] fetch
+
B<mr> [options] push
B<mr> [options] diff
B<mr> [options] log
+B<mr> [options] grep pattern
+
B<mr> [options] run command [param ...]
-B<mr> [options] bootstrap url [directory]
+B<mr> [options] bootstrap src [directory]
B<mr> [options] register [repository]
=head1 DESCRIPTION
-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, fossil and veracity repositories, and support for other version
-control systems can easily be added.
+B<mr> is a tool to manage all your version control repos. 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, 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
working directory. Or, if you are in a subdirectory of a repository that
=item status
Displays a status report for each repository, showing what
-uncommitted changes are present in the repository.
+uncommitted changes are present in the repository. For distributed version
+control systems, also shows unpushed local branches.
+
+=item clean
+
+Print ignored files, untracked files and other cruft in the working directory.
+
+The optional -f parameter allows removing the files as well as printing them.
=item commit (or ci)
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
Show the commit log.
+=item grep pattern
+
+Searches for a pattern in each repository using the grep subcommand. Uses
+ack-grep on VCS that do not have their own.
+
=item run command [param ...]
Runs the specified command in each repository.
=over 4
-=item bootstrap url [directory]
+=item bootstrap src [directory]
+
+Causes mr to retrieve the source C<src> and use it as a .mrconfig file to
+checkout the repositories listed in it, into the specified directory.
+
+B<mr> understands several types of sources:
+
+=over 4
+
+=item URL for curl
+
+C<src> may be an URL understood by B<curl>.
+
+=item copy via ssh
+
+To use B<scp> to download, the C<src> may have the form
+C<ssh://[user@]host:file>.
+
+=item local file
-Causes mr to download the url, and use it as a .mrconfig file
-to checkout the repositories listed in it, into the specified directory.
+You can retrieve the config file by other means and pass its B<path> as C<src>.
+
+=item standard input
+
+If source C<src> consists in a single dash C<->, config file is read from
+standard input.
+
+=back
The directory will be created if it does not exist. If no directory is
specified, the current directory will be used.
-If the .mrconfig file includes a repository named ".", that
+As a special case, if source C<src> includes a repository named ".", that
is checked out into the top of the specified directory.
=item list (or ls)
Be verbose.
+=item -m
+
+=item --minimal
+
+Minimise output. If a command fails or there is any output then the usual
+output will be shown.
+
=item -q
=item --quiet
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/*
Licensed under the GNU GPL version 2 or higher.
-http://kitenet.net/~joey/code/mr/
+http://myrepos.branchable.com/
=cut
# configurables
my $config_overridden=0;
my $verbose=0;
+my $minimal=0;
my $quiet=0;
my $stats=0;
my $force=0;
my $jobs=1;
my $trust_all=0;
my $directory=getcwd();
+my $terminal=-t STDOUT && eval{require IO::Pty::Easy;IO::Pty::Easy->import();1;};
my $HOME_MR_CONFIG = "$ENV{HOME}/.mrconfig";
$ENV{MR_CONFIG}=find_mrconfig();
return $subdir =~ /^\// ? $subdir : $topdir.$subdir;
}
+sub terminal_friendly_spawn {
+ my $actionmsg = shift;
+ my $sh = shift;
+ my $quiet = shift;
+ my $minimal = shift;
+ my $output = "";
+ if ($terminal) {
+ my $pty = IO::Pty::Easy->new;
+ $pty->spawn($sh);
+ while ($pty->is_active) {
+ my $data = $pty->read();
+ $output .= $data if defined $data;
+ }
+ $pty->close;
+ } else {
+ $output = qx/$sh 2>&1/;
+ }
+ my $ret = $?;
+ if ($quiet && $ret != 0) {
+ print "$actionmsg\n" if $actionmsg;
+ print STDERR $output;
+ } elsif (!$quiet && (!$minimal || $output)) {
+ print "$actionmsg\n" if $actionmsg;
+ print $output;
+ }
+ return ($ret, $output ? 1 : 0);
+}
+
sub action {
my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
my $fulldir=fulldir($topdir, $subdir);
+ my $checkout_dir;
$ENV{MR_CONFIG}=$configfiles{$topdir};
my $is_checkout=($action eq 'checkout');
}
if ($is_checkout) {
+ $checkout_dir=$dir;
if (! $force_checkout) {
if (-d $dir) {
print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
return FAILED;
}
else {
- print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n";
+ print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n" unless $minimal;
return SKIPPED;
}
}
$s=~s/^\Q$fulldir\E\/?//;
$actionmsg="mr $action: $fulldir (in subdir $s)";
}
- print "$actionmsg\n" unless $quiet;
+ print "$actionmsg\n" unless $quiet || $minimal;
- my $hookret=hook("pre_$action", $topdir, $subdir);
+ my ($hookret, $hook_out)=hook("pre_$action", $topdir, $subdir);
return $hookret if $hookret != OK;
- my $ret=runsh $action, $topdir, $subdir,
+ my ($ret, $out)=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;
+ if (!$jobs || $jobs > 1 || $quiet || $minimal) {
+ return terminal_friendly_spawn($actionmsg, $sh, $quiet, $minimal);
}
else {
system($sh);
return FAILED;
}
- my $ret=hook("post_$action", $topdir, $subdir);
+ my ($ret, $hook_out)=hook("post_$action", $topdir, $subdir);
return $ret if $ret != OK;
- if (($is_checkout || $is_update)) {
- my $ret=hook("fixups", $topdir, $subdir);
+ 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_out)=hook("fixups", $topdir, $subdir);
return $ret if $ret != OK;
}
- return OK;
+ return (OK, $out || $hook_out);
}
}
}
my $command=$config{$topdir}{$subdir}{$hook};
return OK unless defined $command;
- my $ret=runsh $hook, $topdir, $subdir, $command, [], sub {
+ my ($ret,$out)=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;
+ if (!$jobs || $jobs > 1 || $quiet || $minimal) {
+ return terminal_friendly_spawn(undef, $sh, $quiet, $minimal);
}
else {
system($sh);
}
}
- return OK;
+ return (OK, $out);
}
# run actions on multiple repos, in parallel
close CHILD_STDERR;
close $outfh;
close $errfh;
- exit action($action, @$repo);
+ exit +(action($action, @$repo))[0];
}
close CHILD_STDOUT;
close CHILD_STDERR;
waitpid($active[$i][0], 0);
print STDOUT $out[$i][0];
print STDERR $out[$i][1];
- record($active[$i][1], $? >> 8);
+ record($active[$i][1], $? >> 8, $out[$i][0] || $out[$i][1]);
splice(@fhs, $i, 1);
splice(@active, $i, 1);
splice(@out, $i, 1);
sub record {
my $dir=shift()->[0];
my $ret=shift;
+ my $out=shift;
if ($ret == OK) {
push @ok, $dir;
- print "\n" unless $quiet;
+ print "\n" unless $quiet || ($minimal && !$out);
}
elsif ($ret == FAILED) {
if ($interactive) {
system((getpwuid($<))[8], "-i");
}
push @failed, $dir;
- print "\n" unless $quiet;
+ print "\n";
}
elsif ($ret == SKIPPED) {
push @skipped, $dir;
showstat($#ok+1, "ok", "ok"),
showstat($#failed+1, "failed", "failed"),
showstat($#skipped+1, "skipped", "skipped"),
- ).")\n" unless $quiet;
+ ).")\n" unless $quiet || $minimal;
if ($stats) {
if (@skipped) {
- print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet;
+ print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet || $minimal;
}
if (@failed) {
print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
while (<TRUST>) {
chomp;
s/^~\//$ENV{HOME}\//;
- $trusted{abs_path($_)}=1;
+ my $d=abs_path($_);
+ $trusted{$d}=1 if defined $d;
}
close TRUST;
}
sub loadconfig {
my $f=shift;
my $dir=shift;
- my $bootstrap_url=shift;
+ my $bootstrap_src=shift;
my @toload;
my $trusterror = sub {
my $msg=shift;
- if (defined $bootstrap_url) {
- die "mr: $msg in untrusted $bootstrap_url line $lineno\n".
+ if (defined $bootstrap_src) {
+ die "mr: $msg in untrusted $bootstrap_src line $lineno\n".
"(To trust this url, --trust-all can be used; but please use caution;\n".
"this can allow arbitrary code execution!)\n";
}
while (@lines) {
$_=$nextline->();
+ next if /^\s*\#/ || /^\s*$/;
+
if (! $trusted && /[[:cntrl:]]/) {
$trusterror->("illegal control character");
}
- next if /^\s*\#/ || /^\s*$/;
if (/^\[([^\]]*)\]\s*$/) {
$section=$1;
my $action=shift;
# actions that do not operate on all repos
- if ($action eq 'help') {
- help(@ARGV);
- }
- elsif ($action eq 'config') {
+ if ($action eq 'config') {
config(@ARGV);
}
elsif ($action eq 'register') {
}
sub help {
- exec($config{''}{DEFAULT}{help}) || die "exec: $!";
+ my $help=q#
+ case `uname -s` in
+ SunOS)
+ SHOWMANFILE="man -f"
+ ;;
+ Darwin)
+ SHOWMANFILE="man"
+ ;;
+ *)
+ SHOWMANFILE="man"
+ ;;
+ esac
+ if [ ! -e "$MR_PATH" ]; then
+ error "cannot find program path"
+ fi
+ tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
+ trap "rm -f $tmp" exit
+ pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
+ $SHOWMANFILE "$tmp" || error "man failed"
+ #;
+ exec($help) || die "exec: $!";
}
sub config {
}
sub bootstrap {
- my $url=shift @ARGV;
+ eval q{use File::Copy};
+ die $@ if $@;
+
+ my $src=shift @ARGV;
my $dir=shift @ARGV || ".";
- if (! defined $url || ! length $url) {
- die "mr: bootstrap requires url\n";
+ if (! defined $src || ! length $src) {
+ die "mr: bootstrap requires source\n";
}
-
- # Download the config file to a temporary location.
+
+ # Retrieve config file.
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;
+ if ($src =~ m!^[\w\d]+://!) {
+ # Download the config file to a temporary location.
+ my @downloader;
+ if ($src =~ m!^ssh://(.*)!) {
+ @downloader = ("scp", $1, $tmpconfig);
+ }
+ else {
+ @downloader = ("curl", "-A", "mr", "-L", "-s", $src, "-o", $tmpconfig);
+ push(@downloader, "-k") if $insecure;
+ }
+ my $status = system(@downloader);
+ die "mr bootstrap: invalid SSL certificate for $src (consider -k)\n"
+ if $downloader[0] eq 'curl' && $status >> 8 == 60;
+ die "mr bootstrap: download of $src failed\n" if $status != 0;
+ }
+ elsif ($src eq '-') {
+ # Config file is read from stdin.
+ copy(\*STDIN, $tmpconfig) || die "stdin: $!";
+ }
+ else {
+ # Config file is local.
+ die "mr bootstrap: cannot read file '$src'"
+ unless -r $src;
+ copy($src, $tmpconfig) || die "copy: $!";
+ }
+ # Sanity check on destination directory.
if (! -e $dir) {
system("mkdir", "-p", $dir);
}
# would normally be skipped.
my $topdir=abs_path(".")."/";
my @repo=($topdir, $topdir, ".");
- loadconfig($tmpconfig, $topdir, $url);
+ loadconfig($tmpconfig, $topdir, $src);
record(\@repo, action("checkout", @repo, 1))
if exists $config{$topdir}{"."}{"checkout"};
if (-e ".mrconfig") {
- print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $url\n";
+ print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $src\n";
}
else {
- eval q{use File::Copy};
- die $@ if $@;
move($tmpconfig, ".mrconfig") || die "rename: $!";
}
"p|path" => sub { }, # now default, ignore
"f|force" => \$force,
"v|verbose" => \$verbose,
+ "m|minimal" => \$minimal,
"q|quiet" => \$quiet,
"s|stats" => \$stats,
"k|insecure" => \$insecure,
sub main {
getopts();
init();
+ help(@ARGV) if $ARGV[0] eq 'help';
startingconfig();
loadconfig($HOME_MR_CONFIG);
}
svn_test = perl: -d "$ENV{MR_REPO}/.svn"
-git_test = perl: -d "$ENV{MR_REPO}/.git"
+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"
-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 "$@"
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_clean =
+ if [ "x$1" = x-f ] ; then
+ shift
+ svn-clean "$@"
+ else
+ svn-clean --print "$@"
+ fi
+git_clean =
+ if [ "x$1" = x-f ] ; then
+ shift
+ git clean -dx --force "$@"
+ else
+ git clean -dx --dry-run "$@"
+ fi
+git_svn_clean =
+ if [ "x$1" = x-f ] ; then
+ shift
+ git clean -dx --force "$@"
+ else
+ git clean -dx --dry-run "$@"
+ fi
+bzr_clean =
+ if [ "x$1" = x-f ] ; then
+ shift
+ bzr clean-tree --verbose --force --ignored --unknown --detritus "$@"
+ else
+ bzr clean-tree --verbose --dry-run --ignored --unknown --detritus "$@"
+ fi
+cvs_clean =
+ if [ "x$1" = x-f ] ; then
+ shift
+ cvs-clean "$@"
+ else
+ cvs-clean --dry-run "$@"
+ fi
+hg_clean =
+ if [ "x$1" = x-f ] ; then
+ shift
+ hg purge --print --all "$@"
+ hg purge --all "$@"
+ else
+ hg purge --print --all "$@"
+ fi
+fossil_clean =
+ if [ "x$1" = x-f ] ; then
+ shift
+ fossil clean --dry-run --dotfiles --emptydirs "$@"
+ else
+ fossil clean --force --dotfiles --emptydirs "$@"
+ fi
+vcsh_commit =
+ if [ "x$1" = x-f ] ; then
+ shift
+ vcsh run "$MR_REPO" git clean -dx "$@"
+ else
+ vcsh run "$MR_REPO" git clean -dx --dry-run "$@"
+ fi
+
svn_status = svn status "$@"
-git_status = git status -s "$@" || true
-bzr_status = bzr status --short "$@"
-cvs_status = cvs status "$@"
-hg_status = hg status "$@"
+git_status = git status -s "$@" || true; git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline || true
+bzr_status = bzr status --short "$@"; bzr missing
+cvs_status = cvs -q status | grep -E '^(File:.*Status:|\?)' | grep -v 'Status: Up-to-date'
+hg_status = hg status "$@"; hg summary --quiet | grep -v 'parent: 0:'
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 "$@"
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
+hg_commit = hg commit "$@" && hg push
+darcs_commit = darcs record -a "$@" && darcs push -a
fossil_commit = fossil commit "$@"
-veracity_commit = vv commit -m "@"
+vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all
+veracity_commit = vv commit "$@" && vv push
git_record = git commit -a "$@"
bzr_record =
else
bzr commit "$@"
fi
-hg_record = hg commit -m "$@"
-darcs_record = darcs record -a -m "$@"
+hg_record = hg commit "$@"
+darcs_record = darcs record -a "$@"
fossil_record = fossil commit "$@"
-veracity_record = vv commit -m "@"
+vcsh_record = vcsh run "$MR_REPO" git commit -a "$@"
+veracity_record = vv commit "$@"
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 "$@"
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 "$@"
+darcs_grep = ack-grep "$@"
+
run = "$@"
svn_register =
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'`
hg_trusted_checkout = hg clone $url $repo
darcs_trusted_checkout = darcs get $url $repo
git_bare_trusted_checkout = git clone --bare $url $repo
+vcsh_old_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
+vcsh_trusted_checkout = vcsh clone $url $repo
# fossil: messy to do
veracity_trusted_checkout = vv clone $url $repo
-help =
- case `uname -s` in
- SunOS)
- SHOWMANFILE="man -f"
- ;;
- Darwin)
- SHOWMANFILE="man"
- ;;
- *)
- SHOWMANFILE="man -l"
- ;;
- esac
- if [ ! -e "$MR_PATH" ]; then
- error "cannot find program path"
- fi
- tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
- trap "rm -f $tmp" exit
- pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
- $SHOWMANFILE "$tmp" || error "man failed"
list = true
config =
bootstrap =