X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/278c3e527a595dc2bb6c40fdea7cf55c6d5f1207..fb436fec2ca98879769e4ac20d905ed9e717815b:/mr?ds=inline diff --git a/mr b/mr index 9324b4f..b7cd3cb 100755 --- a/mr +++ b/mr @@ -1,8 +1,8 @@ -#!/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 @@ -24,9 +24,11 @@ B [options] diff B [options] log +B [options] grep pattern + B [options] run command [param ...] -B [options] bootstrap url [directory] +B [options] bootstrap src [directory] B [options] register [repository] @@ -40,11 +42,11 @@ B [options] remember action [params ...] =head1 DESCRIPTION -B 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 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 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 @@ -75,7 +77,8 @@ If a repository isn't checked out yet, it will first check it out. =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 commit (or ci) @@ -112,6 +115,11 @@ Show a diff of uncommitted changes. 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. @@ -122,17 +130,39 @@ These commands are also available: =over 4 -=item bootstrap url [directory] +=item bootstrap src [directory] + +Causes mr to retrieve the source C and use it as a .mrconfig file to +checkout the repositories listed in it, into the specified directory. + +B understands several types of sources: + +=over 4 + +=item URL for curl + +C may be an URL understood by B. + +=item copy via ssh + +To use B to download, the C may have the form +C. -Causes mr to download the url, and use it as a .mrconfig file to checkout -the repositories listed in it, into the specified directory. +=item local file -To use scp to download, the url may have the form ssh://[user@]host:file +You can retrieve the config file by other means and pass its B as C. + +=item standard input + +If source C 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 includes a repository named ".", that is checked out into the top of the specified directory. =item list (or ls) @@ -235,6 +265,13 @@ configuration. 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 @@ -518,7 +555,7 @@ Copyright 2007-2011 Joey Hess Licensed under the GNU GPL version 2 or higher. -http://kitenet.net/~joey/code/mr/ +http://myrepos.branchable.com/ =cut @@ -526,6 +563,7 @@ use warnings; use strict; use Getopt::Long; use Cwd qw(getcwd abs_path); +use File::Basename; # things that can happen when mr runs a command use constant { @@ -538,6 +576,7 @@ use constant { # configurables my $config_overridden=0; my $verbose=0; +my $minimal=0; my $quiet=0; my $stats=0; my $force=0; @@ -548,6 +587,7 @@ my $no_chdir=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(); @@ -691,6 +731,34 @@ sub fulldir { 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); @@ -770,7 +838,7 @@ sub action { 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; } } @@ -784,22 +852,16 @@ sub action { $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); @@ -837,7 +899,7 @@ sub action { 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) { @@ -847,11 +909,11 @@ sub action { return FAILED; } } - my $ret=hook("fixups", $topdir, $subdir); + my ($ret, $hook_out)=hook("fixups", $topdir, $subdir); return $ret if $ret != OK; } - return OK; + return (OK, $out || $hook_out); } } } @@ -861,15 +923,10 @@ sub hook { 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); @@ -889,7 +946,7 @@ sub hook { } } - return OK; + return (OK, $out); } # run actions on multiple repos, in parallel @@ -917,7 +974,7 @@ sub mrs { close CHILD_STDERR; close $outfh; close $errfh; - exit action($action, @$repo); + exit +(action($action, @$repo))[0]; } close CHILD_STDOUT; close CHILD_STDERR; @@ -948,7 +1005,7 @@ sub mrs { 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); @@ -965,10 +1022,11 @@ sub mrs { 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) { @@ -977,7 +1035,7 @@ sub record { system((getpwuid($<))[8], "-i"); } push @failed, $dir; - print "\n" unless $quiet; + print "\n"; } elsif ($ret == SKIPPED) { push @skipped, $dir; @@ -999,10 +1057,10 @@ sub showstats { 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"; @@ -1124,7 +1182,8 @@ sub is_trusted_config { while () { chomp; s/^~\//$ENV{HOME}\//; - $trusted{abs_path($_)}=1; + my $d=abs_path($_); + $trusted{$d}=1 if defined $d; } close TRUST; } @@ -1215,11 +1274,12 @@ my %loaded; sub loadconfig { my $f=shift; my $dir=shift; - my $bootstrap_url=shift; + my $bootstrap_src=shift; my @toload; my $in; + my $absf=abs_path($f); my $trusted; if (ref $f eq 'GLOB') { $dir=""; @@ -1227,7 +1287,6 @@ sub loadconfig { $trusted=1; } else { - my $absf=abs_path($f); if ($loaded{$absf}) { return; } @@ -1243,7 +1302,11 @@ sub loadconfig { } $dir=abs_path($dir)."/"; - + + if (chdir($dir)) { + $f=basename($f); + } + if (! exists $configfiles{$dir}) { $configfiles{$dir}=$f; } @@ -1265,7 +1328,12 @@ sub loadconfig { return; } - print "mr: loading config $f\n" if $verbose; + if ($f =~ /\//) { + print "mr: loading config $f\n" if $verbose; + } else { + print "mr: loading config $f (from ".getcwd().")\n" if $verbose; + } + open($in, "<", $f) || die "mr: open $f: $!\n"; } my @lines=<$in>; @@ -1303,13 +1371,13 @@ sub loadconfig { 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"; } else { - die "mr: $msg in untrusted $f line $lineno\n". + die "mr: $msg in untrusted $absf line $lineno\n". "(To trust this file, list it in ~/.mrtrust.)\n"; } }; @@ -1317,11 +1385,12 @@ sub loadconfig { while (@lines) { $_=$nextline->(); + next if /^\s*\#/ || /^\s*$/; + if (! $trusted && /[[:cntrl:]]/) { $trusterror->("illegal control character"); } - next if /^\s*\#/ || /^\s*$/; if (/^\[([^\]]*)\]\s*$/) { $section=$1; @@ -1542,10 +1611,7 @@ sub dispatch { 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') { @@ -1573,7 +1639,27 @@ sub dispatch { } 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 { @@ -1660,30 +1746,47 @@ sub register { } 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 @downloader; - if ($url =~ m!^ssh://(.*)!) { - @downloader = ("scp", $1, $tmpconfig); + 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 { - @downloader = ("curl", "-A", "mr", "-L", "-s", $url, "-o", $tmpconfig); - push(@downloader, "-k") if $insecure; + # Config file is local. + die "mr bootstrap: cannot read file '$src'" + unless -r $src; + copy($src, $tmpconfig) || die "copy: $!"; } - 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; + # Sanity check on destination directory. if (! -e $dir) { system("mkdir", "-p", $dir); } @@ -1693,16 +1796,14 @@ sub bootstrap { # 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: $!"; } @@ -1760,6 +1861,7 @@ sub getopts { "p|path" => sub { }, # now default, ignore "f|force" => \$force, "v|verbose" => \$verbose, + "m|minimal" => \$minimal, "q|quiet" => \$quiet, "s|stats" => \$stats, "k|insecure" => \$insecure, @@ -1816,6 +1918,7 @@ sub exitstats { sub main { getopts(); init(); + help(@ARGV) if $ARGV[0] eq 'help'; startingconfig(); loadconfig($HOME_MR_CONFIG); @@ -1922,7 +2025,7 @@ hg_fetch = hg pull svn_status = svn 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 status "$@" +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 "$@" @@ -1938,11 +2041,11 @@ bzr_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 "$@" vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all -veracity_commit = vv commit -m "@" && vv push +veracity_commit = vv commit "$@" && vv push git_record = git commit -a "$@" bzr_record = @@ -1951,11 +2054,11 @@ 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 "$@" vcsh_record = vcsh run "$MR_REPO" git commit -a "$@" -veracity_record = vv commit -m "@" +veracity_record = vv commit "$@" svn_push = : git_push = git push "$@" @@ -1994,6 +2097,7 @@ svn_grep = ack-grep "$@" git_svn_grep = git grep "$@" git_grep = git grep "$@" bzr_grep = ack-grep "$@" +darcs_grep = ack-grep "$@" run = "$@" @@ -2067,30 +2171,12 @@ bzr_trusted_checkout = bzr checkout|clone|branch|get $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 +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 =