X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/a33917423c99bcf6dc02d939e7956a746aa974ca..2ccb790eda1239207af9566efcf0a13d1139b682:/mr diff --git a/mr b/mr index 13807f8..d4622d9 100755 --- a/mr +++ b/mr @@ -16,6 +16,8 @@ B [options] status B [options] commit [-m "message"] +B [options] record [-m "message"] + B [options] diff B [options] log @@ -30,7 +32,7 @@ B [options] action [params ...] 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 -respository. It supports any combination of subversion, git, cvs, mecurial, +repository. It supports any combination of subversion, git, cvs, mecurial, bzr and darcs repositories, and support for other revision control systems can easily be added. @@ -62,7 +64,16 @@ uncommitted changes are present in the repository. =item commit (or ci) Commits changes to each repository. (By default, changes are pushed to the -remote repository too, when using distributed systems like git.) +remote repository too, when using distributed systems like git. If you +don't like this default, you can change it in your .mrconfig, or use record +instead.) + +The optional -m parameter allows specifying a commit message. + +=item record + +Records changes to the local repository, but does not push them to the +remote repository. Only supported for distributed revision control systems. The optional -m parameter allows specifying a commit message. @@ -121,8 +132,8 @@ Displays this help. =back -Actions can be abbreviated to any unambiguous subsctring, so -"mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr +Actions can be abbreviated to any unambiguous substring, so +"mr st" is equivalent to "mr status", and "mr up" is equivalent to "mr update" Additional parameters can be passed to most commands, and are passed on @@ -147,21 +158,39 @@ Use the specified mrconfig file. The default is B<~/.mrconfig> Be verbose. +=item -q + +Be quiet. + =item -s Expand the statistics line displayed at the end to include information about exactly which repositories failed and were skipped, if any. -=item -n +=item -i -Just operate on the repository for the current directory, do not -recurse into deeper repositories. +Interactive mode. If a repository fails to be processed, a subshell will be +started which you can use to resolve or investigate the problem. Exit the +subshell to continue the mr run. -=item -j number +=item -n [number] -Run the specified number of jobs in parallel. This can greatly speed up -operations such as updates. It is not recommended for interactive -operations. +If no number if specified, just operate on the repository for the current +directory, do not recurse into deeper repositories. + +If a number is specified, will recurse into repositories at most that many +subdirectories deep. For example, with -n 2 it would recurse into ./src/foo, +but not ./src/packages/bar. + +=item -j [number] + +Run the specified number of jobs in parallel, or an unlimited number of jobs +with no number specified. This can greatly speed up operations such as updates. +It is not recommended for interactive operations. + +Note that running more than 10 jobs at a time is likely to run afoul of +ssh connection limits. Running between 3 and 5 jobs at a time will yeild +a good speedup in updates without loading the machine too much. =back @@ -232,7 +261,7 @@ mr is run by joey. The second uses the hours_since function (included in mr's built-in library) to skip updating the repo unless it's been at least 12 hours since the last update. - skip = test $(whoami) != joey + skip = test `whoami` != joey skip = [ "$1" = update ] && ! hours_since "$1" 12 =item order @@ -296,7 +325,8 @@ use warnings; use strict; use Getopt::Long; use Cwd qw(getcwd abs_path); -use POSIX "WNOHANG"; + +# things that can happen when mr runs a command use constant { OK => 0, FAILED => 1, @@ -304,90 +334,35 @@ use constant { ABORT => 3, }; -$SIG{INT}=sub { - print STDERR "mr: interrupted\n"; - exit 2; -}; - -$ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig"; +# configurables my $config_overridden=0; my $verbose=0; +my $quiet=0; my $stats=0; -my $no_recurse=0; +my $interactive=0; +my $max_depth; my $no_chdir=0; my $jobs=1; +my $directory=getcwd(); +$ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig"; + +# globals :-( my %config; my %configfiles; my %knownactions; my %alias; -my $directory=getcwd(); my (@ok, @failed, @skipped); -getopts(); - -# This can happen if it's run in a directory that was removed -# or other strangeness. -if (! defined $directory) { - die("mr: failed to determine working directory\n"); -} -# Make sure MR_CONFIG is an absolute path, but don't use abs_path since -# the config file might be a symlink to elsewhere, and the directory it's -# in is significant. -if ($ENV{MR_CONFIG} !~ /^\//) { - $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG}; -} -# Try to set MR_PATH to the path to the program. -eval { - use FindBin qw($Bin $Script); - $ENV{MR_PATH}=$Bin."/".$Script; -}; - -loadconfig(\*DATA); -loadconfig($ENV{MR_CONFIG}); -#use Data::Dumper; -#print Dumper(\%config); - -my $action=expandaction(shift @ARGV); -dispatch($action); -showstats($action); - -if (@failed) { - exit 1; -} -elsif (! @ok && @skipped) { - exit 1; -} -else { - exit 0; -} - -sub dispatch { #{{{ - my $action=shift; - - # actions that do not operate on all repos - if ($action eq 'help') { - help(@ARGV); - } - elsif ($action eq 'config') { - config(@ARGV); - } - elsif ($action eq 'register') { - register(@ARGV); - } - - if ($jobs > 1) { - mrs($action, selectrepos()); - } - else { - foreach my $repo (selectrepos()) { - record($repo, action($action, @$repo)); - } - } -} #}}} +main(); +my %rcs; sub rcs_test { #{{{ my ($action, $dir, $topdir, $subdir) = @_; + if (exists $rcs{$dir}) { + return $rcs{$dir}; + } + my $test="set -e\n"; foreach my $rcs_test ( sort { @@ -405,21 +380,30 @@ sub rcs_test { #{{{ print "mr $action: running rcs test >>$test<<\n" if $verbose; my $rcs=`$test`; chomp $rcs; - if (! length $rcs) { + if ($rcs=~/\n/s) { + $rcs=~s/\n/, /g; + print STDERR "mr $action: found multiple possible repository types ($rcs) for $topdir$subdir\n"; return undef; } + if (! length $rcs) { + return $rcs{$dir}=undef; + } else { - return $rcs; + return $rcs{$dir}=$rcs; } } #}}} sub findcommand { #{{{ - my ($action, $dir, $topdir, $subdir) = @_; + my ($action, $dir, $topdir, $subdir, $is_checkout) = @_; if (exists $config{$topdir}{$subdir}{$action}) { return $config{$topdir}{$subdir}{$action}; } + if ($is_checkout) { + return undef; + } + my $rcs=rcs_test(@_); if (defined $rcs && @@ -437,19 +421,15 @@ sub action { #{{{ $ENV{MR_CONFIG}=$configfiles{$topdir}; my $lib=exists $config{$topdir}{$subdir}{lib} ? $config{$topdir}{$subdir}{lib}."\n" : ""; + my $is_checkout=($action eq 'checkout'); - if ($action eq 'checkout') { + if ($is_checkout) { if (-d $dir) { print "mr $action: $dir already exists, skipping checkout\n" if $verbose; return SKIPPED; } $dir=~s/^(.*)\/[^\/]+\/?$/$1/; - - if (! -d $dir) { - print "mr $action: creating parent directory $dir\n" if $verbose; - system("mkdir", "-p", $dir); - } } elsif ($action =~ /update/) { if (! -d $dir) { @@ -459,8 +439,8 @@ sub action { #{{{ $ENV{MR_REPO}=$dir; - my $skiptest=findcommand("skip", $dir, $topdir, $subdir); - my $command=findcommand($action, $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. @@ -482,7 +462,12 @@ sub action { #{{{ return SKIPPED; } } - + + if ($is_checkout && ! -d $dir) { + print "mr $action: creating parent directory $dir\n" if $verbose; + system("mkdir", "-p", $dir); + } + if (! $no_chdir && ! chdir($dir)) { print STDERR "mr $action: failed to chdir to $dir: $!\n"; return FAILED; @@ -494,18 +479,18 @@ sub action { #{{{ return FAILED; } else { - print STDERR "mr $action: no defined $action command for $rcs repository $topdir$subdir, skipping\n"; + print STDERR "mr $action: no defined action for $rcs repository $topdir$subdir, skipping\n"; return SKIPPED; } } else { if (! $no_chdir) { - print "mr $action: $topdir$subdir\n"; + print "mr $action: $topdir$subdir\n" unless $quiet; } else { my $s=$directory; $s=~s/^\Q$topdir$subdir\E\/?//; - print "mr $action: $topdir$subdir (in subdir $s)\n"; + print "mr $action: $topdir$subdir (in subdir $s)\n" unless $quiet; } $command="set -e; ".$lib. "my_action(){ $command\n }; my_action ". @@ -552,7 +537,7 @@ sub mrs { #{{{ my @out; my $running=0; while (@fhs or @repos) { - while ($running < $jobs && @repos) { + while ((!$jobs || $running < $jobs) && @repos) { $running++; my $repo = shift @repos; pipe(my $outfh, CHILD_STDOUT); @@ -620,6 +605,11 @@ sub record { #{{{ print "\n"; } elsif ($ret == FAILED) { + if ($interactive) { + chdir($dir) unless $no_chdir; + print STDERR "mr: Starting interactive shell. Exit shell to continue.\n"; + system((getpwuid($<))[8]); + } push @failed, $dir; print "\n"; } @@ -635,6 +625,7 @@ sub record { #{{{ } #}}} sub showstats { #{{{ + my $action=shift; if (! @ok && ! @failed && ! @skipped) { die "mr $action: no repositories found to work on\n"; } @@ -642,10 +633,10 @@ sub showstats { #{{{ showstat($#ok+1, "ok", "ok"), showstat($#failed+1, "failed", "failed"), showstat($#skipped+1, "skipped", "skipped"), - ).")\n"; + ).")\n" unless $quiet; if ($stats) { if (@skipped) { - print "mr $action: (skipped: ".join(" ", @skipped).")\n"; + print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet; } if (@failed) { print STDERR "mr $action: (failed: ".join(" ", @failed).")\n"; @@ -696,8 +687,13 @@ sub selectrepos { #{{{ my $d=$directory; $dir.="/" unless $dir=~/\/$/; $d.="/" unless $d=~/\/$/; - next if $no_recurse && $d ne $dir; next if $dir ne $d && $dir !~ /^\Q$d\E/; + if (defined $max_depth) { + my @a=split('/', $dir); + my @b=split('/', $d); + do { } while (@a && @b && shift(@a) eq shift(@b)); + next if @a > $max_depth || @b > $max_depth; + } push @repos, [$dir, $topdir, $subdir]; } if (! @repos) { @@ -830,11 +826,11 @@ sub loadconfig { #{{{ my $ret=system($value); if ($ret != 0) { if (($? & 127) == 2) { - print STDERR "mr $action: chain test interrupted\n"; + print STDERR "mr: chain test interrupted\n"; exit 2; } elsif ($? & 127) { - print STDERR "mr $action: chain test received signal ".($? & 127)."\n"; + print STDERR "mr: chain test received signal ".($? & 127)."\n"; } } else { @@ -949,9 +945,33 @@ sub modifyconfig { #{{{ print $out @out; close $out; } #}}} - + +sub dispatch { #{{{ + my $action=shift; + + # actions that do not operate on all repos + if ($action eq 'help') { + help(@ARGV); + } + elsif ($action eq 'config') { + config(@ARGV); + } + elsif ($action eq 'register') { + register(@ARGV); + } + + if (!$jobs || $jobs > 1) { + mrs($action, selectrepos()); + } + else { + foreach my $repo (selectrepos()) { + record($repo, action($action, @$repo)); + } + } +} #}}} + sub help { #{{{ - exec($config{''}{DEFAULT}{$action}) || die "exec: $!"; + exec($config{''}{DEFAULT}{help}) || die "exec: $!"; } #}}} sub config { #{{{ @@ -984,7 +1004,7 @@ sub config { #{{{ } } if (! $found) { - die "mr $action: $section $_ not set\n"; + die "mr config: $section $_ not set\n"; } } } @@ -997,33 +1017,38 @@ sub register { #{{{ # Find the closest known mrconfig file to the current # directory. $directory.="/" unless $directory=~/\/$/; + my $foundconfig=0; foreach my $topdir (reverse sort keys %config) { next unless length $topdir; if ($directory=~/^\Q$topdir\E/) { $ENV{MR_CONFIG}=$configfiles{$topdir}; $directory=$topdir; + $foundconfig=1; last; } } + if (! $foundconfig) { + $directory=""; # no config file, use builtin + } } if (@ARGV) { my $subdir=shift @ARGV; if (! chdir($subdir)) { - print STDERR "mr $action: failed to chdir to $subdir: $!\n"; + print STDERR "mr register: failed to chdir to $subdir: $!\n"; } } $ENV{MR_REPO}=getcwd(); - my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT'); + my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0); if (! defined $command) { - die "mr $action: unknown repository type\n"; + die "mr register: unknown repository type\n"; } $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); - print "mr $action: running >>$command<<\n" if $verbose; + print "mr register: running >>$command<<\n" if $verbose; exec($command) || die "exec: $!"; } #}}} @@ -1052,14 +1077,16 @@ sub expandaction { #{{{ } #}}} sub getopts { #{{{ - Getopt::Long::Configure("no_permute"); + Getopt::Long::Configure("bundling", "no_permute"); my $result=GetOptions( "d|directory=s" => sub { $directory=abs_path($_[1]) }, "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 }, "v|verbose" => \$verbose, + "q|quiet" => \$quiet, "s|stats" => \$stats, - "n|no-recurse" => \$no_recurse, - "j|jobs=i" => \$jobs, + "i|interactive" => \$interactive, + "n|no-recurse:i" => \$max_depth, + "j|jobs:i" => \$jobs, ); if (! $result || @ARGV < 1) { die("Usage: mr [-d directory] action [params ...]\n". @@ -1067,6 +1094,52 @@ sub getopts { #{{{ } } #}}} +sub init { #{{{ + $SIG{INT}=sub { + print STDERR "mr: interrupted\n"; + exit 2; + }; + + # This can happen if it's run in a directory that was removed + # or other strangeness. + if (! defined $directory) { + die("mr: failed to determine working directory\n"); + } + # Make sure MR_CONFIG is an absolute path, but don't use abs_path since + # the config file might be a symlink to elsewhere, and the directory it's + # in is significant. + if ($ENV{MR_CONFIG} !~ /^\//) { + $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG}; + } + # Try to set MR_PATH to the path to the program. + eval { + use FindBin qw($Bin $Script); + $ENV{MR_PATH}=$Bin."/".$Script; + }; +} #}}} + +sub main { #{{{ + getopts(); + init(); + loadconfig(\*DATA); + loadconfig($ENV{MR_CONFIG}); + #use Data::Dumper; print Dumper(\%config); + + my $action=expandaction(shift @ARGV); + dispatch($action); + showstats($action); + + if (@failed) { + exit 1; + } + elsif (! @ok && @skipped) { + exit 1; + } + else { + exit 0; + } +} #}}} + # Finally, some useful actions that mr knows about by default. # These can be overridden in ~/.mrconfig. #DATA{{{ @@ -1079,7 +1152,6 @@ ls = list [DEFAULT] order = 10 lib = - PWD="$(pwd)" error() { echo "mr: $@" >&2 exit 1 @@ -1103,7 +1175,7 @@ lib = if [ -z "$flagfile" ]; then error "cannot determine flag filename" fi - delta=$(perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile") + delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"` if [ "$delta" -lt "$2" ]; then exit 0 else @@ -1121,10 +1193,10 @@ darcs_test = test -d "$MR_REPO"/_darcs 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 + test "`GIT_CONFIG="$MR_REPO"/config git config --get core.bare`" = true svn_update = svn update "$@" -git_update = if [ "$@" ]; then git pull "$@"; else git pull -t origin master; fi +git_update = git pull "$@" bzr_update = bzr merge "$@" cvs_update = cvs update "$@" hg_update = hg pull "$@" && hg update "$@" @@ -1135,21 +1207,26 @@ git_status = git status "$@" || true bzr_status = bzr status "$@" cvs_status = cvs status "$@" hg_status = hg status "$@" -darcs_status = darcs whatsnew -ls "$@" +darcs_status = darcs whatsnew -ls "$@" || true svn_commit = svn commit "$@" git_commit = git commit -a "$@" && git push --all bzr_commit = bzr commit "$@" && bzr push cvs_commit = cvs commit "$@" hg_commit = hg commit -m "$@" && hg push -darcs_commit = darcs commit -a -m "$@" && darcs push -a +darcs_commit = darcs record -a -m "$@" && darcs push -a + +git_record = git commit -a "$@" +bzr_record = bzr commit "$@" +hg_record = hg commit -m "$@" +darcs_record = darcs record -a -m "$@" svn_diff = svn diff "$@" git_diff = git diff "$@" bzr_diff = bzr diff "$@" cvs_diff = cvs diff "$@" hg_diff = hg diff "$@" -darcs_diff = darcs diff "$@" +darcs_diff = darcs diff -u "$@" svn_log = svn log "$@" git_log = git log "$@" @@ -1160,55 +1237,58 @@ darcs_log = darcs changes "$@" git_bare_log = git log "$@" svn_register = - url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2) + url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2` if [ -z "$url" ]; then error "cannot determine svn url" fi echo "Registering svn url: $url in $MR_CONFIG" - mr -c "$MR_CONFIG" config "$PWD" checkout="svn co '$url' '$MR_REPO'" + mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'" git_register = - url="$(LANG=C git-config --get remote.origin.url)" || true + url="`LC_ALL=C 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="git clone '$url' '$MR_REPO'" + mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'" bzr_register = - url=$(cat .bzr/branch/parent) + url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}'`" if [ -z "$url" ]; then 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 clone '$url' '$MR_REPO'" cvs_register = - repo=$(cat CVS/Repository) - root=$(cat CVS/Root) + repo=`cat CVS/Repository` + root=`cat CVS/Root` if [ -z "$root" ]; then error "cannot determine cvs root" fi echo "Registering cvs repository $repo at root $root" - mr -c "$MR_CONFIG" config "$PWD" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'" + mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'" hg_register = - url=$(hg showconfig paths.default) + url=`hg showconfig paths.default` echo "Registering mercurial repo url: $url in $MR_CONFIG" - mr -c "$MR_CONFIG" config "$PWD" checkout="hg clone '$url' '$MR_REPO'" + mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'" darcs_register = - url=$(cat _darcs/prefs/defaultrepo) + url=`cat _darcs/prefs/defaultrepo` echo "Registering darcs repository $url in $MR_CONFIG" - mr -c "$MR_CONFIG" config "$PWD" checkout="darcs get '$url'p '$MR_REPO'" + mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'" git_bare_register = - url="$(LANG=C GIT_CONFIG=config git-config --get remote.origin.url)" || true + url="`LC_ALL=C GIT_CONFIG=config 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="git clone --bare '$url' '$MR_REPO'" + mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'" help = if [ ! -e "$MR_PATH" ]; then error "cannot find program path" fi - (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed" + tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed" + trap "rm -f $tmp" exit + pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed" + man -l "$tmp" || error "man failed" list = true config =