X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/72a0284da93c97d390bcc9a0b0b04d0c77f9f281..cec9366aea873ebe2dca710d42f78d080c7d1ff0:/mr?ds=inline diff --git a/mr b/mr index c15b3a3..470927a 100755 --- a/mr +++ b/mr @@ -12,7 +12,11 @@ B [options] update B [options] status -B [options] commit -m "message" +B [options] commit [-m "message"] + +B [options] diff + +B [options] log B [options] action [params ...] @@ -20,10 +24,70 @@ B [options] action [params ...] B is a Multiple Repository management tool. It allows you to register a set of repositories in a .mrconfig file, and then checkout, update, or -perform other actions on all of the repositories at once. +perform other actions on the repositories as if they were one big +respository. Any mix of revision control systems can be used with B, and you can -define arbitrary actions like "update", "checkout", or "commit". +define arbitrary actions for commands like "update", "checkout", or "commit". + +B cds into and operates on all registered repsitories at or below your +working directory. Or, if you are in a subdirectory of a repository that +contains no other registered repositories, it will stay in that directory, +and work on only that repository, + +The predefined commands should be fairly familiar to users of any revision +control system: + +=over 4 + +=item checkout (or co) + +Checks out any repositories that are not already checked out. + +=item update + +Updates each repository from its configured remote repository. + +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. + +=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.) + +The optional -m parameter allows specifying a commit message. + +=item diff + +Show a diff of uncommitted changes. + +=item log + +Show the commit log. + +=item list (or ls) + +List the repositories that mr will act on. + +=item help + +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 +update" + +Additional parameters can be passed to other commands than "commit", they +will be passed on unchanged to the underlying revision control system. +This is mostly useful if the repositories mr will act on all use the same +revision control system. =head1 OPTIONS @@ -32,12 +96,11 @@ define arbitrary actions like "update", "checkout", or "commit". =item -d directory Specifies the topmost directory that B should work in. The default is -the current working directory. B will operate on all registered -repositories at or under the directory. +the current working directory. =item -c mrconfig -Use the specified mrconfig file, instead of looking for on in your home +Use the specified mrconfig file, instead of looking for one in your home directory. =item -v @@ -48,41 +111,67 @@ Be verbose. =head1 FILES -B is configured by .mrconfig files. It searches for .mrconfig files in -your home directory, and in the root directory of each repository specified -in a .mrconfig file. So you could have a ~/.mrconfig that registers a +B is configured by .mrconfig files. It starts by reading the .mrconfig +file in your home directory. Each repository specified in a .mrconfig file +can also have its own .mrconfig file in its root directory that can +optionally be used as well. So you could have a ~/.mrconfig that registers a repository ~/src, that itself contains a ~/src/.mrconfig file, that in turn registers several additional repositories. The .mrconfig file uses a variant of the INI file format. Lines starting with "#" are comments. Lines ending with "\" are continued on to the next line. -Sections specify where each repository is located, relative to the + +The "default" section allows setting default values for the sections that +come after it. + +The "alias" section allows adding aliases for actions. Each parameter +is an alias, and its value is the action to use. + +All other sections specify where each repository is located, relative to the directory that contains the .mrconfig file. Within a section, each parameter defines a shell command to run to handle a -given action. Note that these shell commands are run in a "set -e" shell -environment, and B cds into the repository directory before running -them, except for the "checkout" command, which is run in the parent of the -repository directory, since the repository isn't checked out yet. - -There are two special parameters. If the "skip" parameter is set and +given action. mr contains default handlers for the "update", "status", and +"commit" actions, so normally you only need to specify what to do for +"checkout". + +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. +The "MR_REPO" environment variable is set to the path to the top of the +repository. + +There are three special parameters. If the "skip" parameter is set and its command returns nonzero, then B will skip acting on that repository. - -The "default" section allows setting up default handlers for each action, -and is overridden by the contents of other sections. mr contains default -handlers for the "update", "status", and "commit" actions, so normally -you only need to specify what to do for "checkout". +If the "chain" parameter is set and its command returns nonzero, then B +will try to load a .mrconfig file from the root of the repository. (You +should avoid chaining from repositories with untrusted committers.) 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. For example: [src] checkout = svn co svn://svn.example.com/src/trunk src + chain = true [src/linux-2.6] - # only check this out on kodama - skip = test $(hostname) != kodama + skip = small checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git + [default] + lib = \ + small() { + case "$(hostname)" in; \ + slug|snail); \ + return 0; ;; ; \ + esac; \ + return 1; \ + } + =head1 AUTHOR Copyright 2007 Joey Hess @@ -102,6 +191,8 @@ my $directory=getcwd(); my $config="$ENV{HOME}/.mrconfig"; my $verbose=0; my %config; +my %knownactions; +my %alias; Getopt::Long::Configure("no_permute"); my $result=GetOptions( @@ -110,77 +201,150 @@ my $result=GetOptions( "v" => \$verbose, ); if (! $result || @ARGV < 1) { - die("Usage: mr [-d directory] action [params ...]\n"); + die("Usage: mr [-d directory] action [params ...]\n". + "(Use mr help for man page.)\n"); + } -my $action=shift @ARGV; loadconfig(\*DATA); loadconfig($config); #use Data::Dumper; #print Dumper(\%config); -my (@failures, @successes, @skipped); -my $first=1; +eval { + use FindBin qw($Bin $Script); + $ENV{MR_PATH}=$Bin."/".$Script; +}; + +# alias expansion and command stemming +my $action=shift @ARGV; +if (! exists $knownactions{$action}) { + if (exists $alias{$action}) { + $action=$alias{$action}; + } + else { + my @matches = grep { /^\Q$action\E/ } + keys %knownactions, keys %alias; + if (@matches == 1) { + $action=$matches[0]; + } + elsif (@matches == 0) { + die "mr: unknown action \"$action\" (known actions: ". + join(", ", sort keys %knownactions).")\n"; + } + else { + die "mr: ambiguous action \"$action\" (matches: ". + join(", ", @matches).")\n"; + } + } +} + +if ($action eq 'help') { + exec($config{''}{default}{help}); +} + +# work out what repos to act on +my @repos; +my $nochdir=0; foreach my $topdir (sort keys %config) { foreach my $subdir (sort keys %{$config{$topdir}}) { next if $subdir eq 'default'; - my $dir=$topdir.$subdir; - - if (defined $directory && - $dir ne $directory && - $dir !~ /^\Q$directory\E\//) { - print "mr $action: $dir skipped per -d parameter ($directory)\n" if $verbose; - push @skipped, $dir; - next; + next if $dir ne $directory && $dir !~ /^\Q$directory\E\//; + push @repos, [$dir, $topdir, $subdir]; + } +} +if (! @repos) { + # fallback to find a leaf repo + LEAF: foreach my $topdir (reverse sort keys %config) { + foreach my $subdir (reverse sort keys %{$config{$topdir}}) { + next if $subdir eq 'default'; + my $dir=$topdir.$subdir; + my $d=$directory; + $dir.="/" unless $dir=~/\/$/; + $d.="/" unless $d=~/\/$/; + if ($d=~/^\Q$dir\E/) { + push @repos, [$dir, $topdir, $subdir]; + last LEAF; + } } + } + $nochdir=1; +} - print "\n" unless $first; - $first=0; +my (@failed, @successful, @skipped); +foreach my $repo (@repos) { + action($action, @$repo); +} - if (exists $config{$topdir}{$subdir}{skip}) { - my $ret=system($config{$topdir}{$subdir}{skip}); - if ($ret >> 8 == 0) { - print "mr $action: $dir skipped per config file\n" if $verbose; - push @skipped, $dir; - next; - } - } +sub action { + my ($action, $dir, $topdir, $subdir) = @_; + + my $lib= exists $config{$topdir}{$subdir}{lib} ? + $config{$topdir}{$subdir}{lib} : ""; - if ($action eq 'checkout') { - if (-e $dir) { - print "mr $action: $dir already exists, skipping checkout\n"; - push @skipped, $dir; - next; - } - $dir=~s/^(.*)\/[^\/]+\/?$/$1/; - } - if (! chdir($dir)) { - print STDERR "mr $action: failed to chdir to $dir: $!\n"; + if ($action eq 'checkout') { + if (-d $dir) { + print "mr $action: $dir already exists, skipping checkout\n" if $verbose; push @skipped, $dir; + return; + } + $dir=~s/^(.*)\/[^\/]+\/?$/$1/; + } + elsif ($action eq 'update') { + if (! -d $dir) { + return action("checkout", $dir, $topdir, $subdir); } - elsif (! exists $config{$topdir}{$subdir}{$action}) { - print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n"; + } + + $ENV{MR_REPO}=$dir; + if (! $nochdir && ! chdir($dir)) { + print STDERR "mr $action: failed to chdir to $dir: $!\n"; + push @skipped, $dir; + } + + if (exists $config{$topdir}{$subdir}{skip}) { + my $test="set -e;".$lib.$config{$topdir}{$subdir}{skip}; + print "mr $action: running skip test $test\n" if $verbose; + my $ret=system($test); + if ($ret >> 8 == 0) { + print "mr $action: $dir skipped per config file\n" if $verbose; push @skipped, $dir; + return; + } + } + + if (! exists $config{$topdir}{$subdir}{$action}) { + print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n"; + push @skipped, $dir; + } + else { + if (! $nochdir) { + print "mr $action: $dir\n"; } else { - print "mr $action: in $dir\n"; - my $command="set -e; my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action @ARGV"; - my $ret=system($command); - if ($ret != 0) { - print STDERR "mr $action: failed to run: $command\n" if $verbose; - push @failures, $topdir.$subdir; - if ($ret >> 8 != 0) { - print STDERR "mr $action: command failed\n"; - } - elsif ($ret != 0) { - print STDERR "mr $action: command died ($ret)\n"; - } + print "mr $action: $dir (in subdir $directory)\n"; + } + my $command="set -e; ".$lib. + "my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ". + join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV); + print STDERR "mr $action: running $command\n" if $verbose; + my $ret=system($command); + if ($ret != 0) { + print STDERR "mr $action: failed ($ret)\n" if $verbose; + push @failed, $topdir.$subdir; + if ($ret >> 8 != 0) { + print STDERR "mr $action: command failed\n"; } - else { - push @successes, $dir; + elsif ($ret != 0) { + print STDERR "mr $action: command died ($ret)\n"; } } + else { + push @successful, $dir; + } + + print "\n"; } } @@ -193,12 +357,21 @@ sub showstat { } return; } -print "\nmr $action: finished (".join("; ", - showstat($#successes+1, "success", "successes"), - showstat($#failures+1, "failure", "failures"), +if (! @successful && ! @failed && ! @skipped) { + die "mr $action: no repositories found to work on\n"; +} +print "mr $action: finished (".join("; ", + showstat($#successful+1, "successful", "successful"), + showstat($#failed+1, "failed", "failed"), showstat($#skipped+1, "skipped", "skipped"), ).")\n"; -exit @failures ? 1 : 0; +if (@failed) { + exit 1; +} +elsif (! @successful && @skipped) { + exit 1; +} +exit 0; my %loaded; sub loadconfig { @@ -223,6 +396,9 @@ sub loadconfig { print "mr: loading config $f\n" if $verbose; open($in, "<", $f) || die "mr: open $f: $!\n"; ($dir)=$f=~/^(.*\/)[^\/]+$/; + if (! defined $dir) { + $dir="."; + } $dir=abs_path($dir)."/"; # copy in defaults from first parent @@ -242,10 +418,6 @@ sub loadconfig { next if /^\s*\#/ || /^\s*$/; if (/^\s*\[([^\]]*)\]\s*$/) { $section=$1; - if (length $dir && $section ne "default" && - -e $dir.$section."/.mrconfig") { - push @toload, $dir.$section."/.mrconfig"; - } } elsif (/^\s*(\w+)\s*=\s*(.*)/) { my $parameter=$1; @@ -260,15 +432,31 @@ sub loadconfig { if (! defined $section) { die "$f line $.: parameter ($parameter) not in section\n"; } - if (! exists $config{$dir}{$section} && - exists $config{$dir}{default}) { + if ($section ne 'alias' && + ! exists $config{$dir}{$section} && + exists $config{$dir}{default}) { # copy in defaults $config{$dir}{$section}={ %{$config{$dir}{default}} }; } - $config{$dir}{$section}{$parameter}=$value; + if ($section eq 'alias') { + $alias{$parameter}=$value; + } + elsif ($parameter eq 'lib') { + $config{$dir}{$section}{lib}.=$value." ; "; + } + else { + $config{$dir}{$section}{$parameter}=$value; + $knownactions{$parameter}=1; + if ($parameter eq 'chain' && + length $dir && $section ne "default" && + -e $dir.$section."/.mrconfig" && + system($value) >> 8 == 0) { + push @toload, $dir.$section."/.mrconfig"; + } + } } else { - die "$f line $.: parse error\n"; + die "$f line $.: parse error\n"; } } close $in; @@ -278,34 +466,73 @@ sub loadconfig { } } -__DATA__ -# Some useful actions that mr knows about by default. +# Finally, some useful actions that mr knows about by default. # These can be overridden in ~/.mrconfig. +__DATA__ +[alias] + co = checkout + ci = commit + ls = list [default] +lib = \ + error() { \ + echo "mr: $@" >&2; \ + exit 1; \ + } update = \ - if [ -d .svn ]; then \ - svn update; \ - elif [ -d .git ]; then \ - git pull origin master; \ + if [ -d "$MR_REPO"/.svn ]; then \ + svn update "$@"; \ + elif [ -d "$MR_REPO"/.git ]; then \ + git pull origin master "$@"; \ + elif [ -d "$MR_REPO"/CVS ]; then \ + cvs update "$@"; \ else \ - echo "mr update: unknown RCS"; \ - exit 1; \ + error "unknown repo type"; \ fi status = \ - if [ -d .svn ]; then \ - svn status; \ - elif [ -d .git ]; then \ - git status || true; \ + if [ -d "$MR_REPO"/.svn ]; then \ + svn status "$@"; \ + elif [ -d "$MR_REPO"/.git ]; then \ + git status "$@" || true; \ + elif [ -d "$MR_REPO"/CVS ]; then \ + cvs status "$@"; \ else \ - echo "mr status: unknown RCS"; \ - exit 1; \ + error "unknown repo type"; \ fi commit = \ - if [ -d .svn ]; then \ + if [ -d "$MR_REPO"/.svn ]; then \ svn commit "$@"; \ - elif [ -d .git ]; then \ - git commit -a "$@"; \ + elif [ -d "$MR_REPO"/.git ]; then \ + git commit -a "$@" && git push --all; \ + elif [ -d "$MR_REPO"/CVS ]; then \ + cvs commit "$@"; \ else \ - echo "mr commit: unknown RCS"; \ - exit 1; \ + error "unknown repo type"; \ + fi +diff = \ + if [ -d "$MR_REPO"/.svn ]; then \ + svn diff "$@"; \ + elif [ -d "$MR_REPO"/.git ]; then \ + git diff "$@"; \ + elif [ -d "$MR_REPO"/CVS ]; then \ + cvs diff "$@"; \ + else \ + error "unknown repo type"; \ + fi +log = \ + if [ -d "$MR_REPO"/.svn ]; then \ + svn log"$@"; \ + elif [ -d "$MR_REPO"/.git ]; then \ + git log "$@"; \ + elif [ -d "$MR_REPO"/CVS ]; then \ + cvs log "$@"; \ + else \ + error "unknown repo type"; \ fi +list = true +help = \ + if [ ! -e "$MR_PATH" ]; then \ + error "cannot find program path";\ + fi; \ + (pod2man -c mr "$MR_PATH" | man -l -) || \ + error "pod2man or man failed"