X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/7634c277a5d0fedfbeb314d79e6974fb16460472..a7adfa1847accd1a1924f4f069feab9ae6acaaff:/mr diff --git a/mr b/mr index 00d0e1c..3cc603f 100755 --- a/mr +++ b/mr @@ -16,17 +16,26 @@ B [options] commit [-m "message"] B [options] diff +B [options] log + +B [options] register [repository] + +B [options] config section [parameter=[value] ...] + B [options] action [params ...] =head1 DESCRIPTION -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 the repositories as if they were one big -respository. +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, and bzr repositories, +and support for other revision control systems can easily be added. -Any mix of revision control systems can be used with B, and you can -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: @@ -59,10 +68,35 @@ The optional -m parameter allows specifying a commit message. Show a diff of uncommitted changes. -=item list +=item log + +Show the commit log. + +=item list (or ls) List the repositories that mr will act on. +=item register + +Register an existing repository in the mrconfig file. By default, the +epository in the current directory is registered, or you can specify a +directory to register. + +=item config + +Adds, modifies, removes, or prints a value from the mrconfig file. The next +parameter is the name of the section the value is in. To add or modify +values, use one or more instances of "parameter=value". Use "parameter=" to +remove a parameter. Use just "parameter" to get the value of a parameter. + +For example, to add (or edit) a repository in src/foo: + + mr config src/foo checkout="svn co svn://example.com/foo/trunk foo" + +To show the command that mr uses to update the repository in src/foo: + + mr config src/foo update + =item help Displays this help. @@ -70,16 +104,13 @@ Displays this help. =back Actions can be abbreviated to any unambiguous subsctring, so -"mr st" is equivilant to "mr status". +"mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr +update" -B operates on all registered repsitories at or below your working -directory. Or, if you are in a subdirectory of a repository, it will act on -only that repository. - -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. +Additional parameters can be passed to most commands, and are 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 @@ -92,7 +123,7 @@ 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 @@ -104,19 +135,41 @@ Be verbose. =head1 FILES 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. +file in your home directory, and this can in turn chain load .mrconfig files +from repositories. + +Here is an example .mrconfig file: + + [src] + checkout = svn co svn://svn.example.com/src/trunk src + chain = true + + [src/linux-2.6] + checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git && + cd linux-2.6 && + git checkout -b mybranch origin/master 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 -directory that contains the .mrconfig file. +"#" are comments. Values can be continued to the following line by +indenting the line with whitespace. + +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 add repositories. The section header specifies the +directory where the repository is located. This is relative to the directory +that contains the mrconfig file, but you can also choose to use absolute +paths. 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 +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 @@ -124,42 +177,45 @@ 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. +A few parameters have special meanings: + +=over 4 + +=item skip + +If the "skip" parameter is set and its command returns nonzero, then B +will skip acting on that repository. The command is passed the action +name in $1. + +Here are two examples. The first skips the repo unless +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 = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ] + +=item chain + 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 other commands -can use. +should avoid chaining from repositories with untrusted committers.) -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". +=item deleted -The "alias" section allows adding aliases for commands. Each parameter -is an alias, and its value is the command to run. +If the "deleted" parameter is set and its command returns nonzero, then +B will treat the repository as deleted. It won't ever actually delete +the repository, but it will warn if it sees the repsoitory's directory. +This is useful when one mrconfig file is shared amoung multiple machines, +to keep track of and remember to delete old repositories. -For example: +=item lib - [src] - checkout = svn co svn://svn.example.com/src/trunk src - chain = true +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. - [src/linux-2.6] - 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; \ - } +=back =head1 AUTHOR @@ -185,9 +241,9 @@ my %alias; Getopt::Long::Configure("no_permute"); my $result=GetOptions( - "d=s" => sub { $directory=abs_path($_[1]) }, - "c=s" => \$config, - "v" => \$verbose, + "d|directory=s" => sub { $directory=abs_path($_[1]) }, + "c|config=s" => \$config, + "verbose" => \$verbose, ); if (! $result || @ARGV < 1) { die("Usage: mr [-d directory] action [params ...]\n". @@ -207,63 +263,128 @@ eval { # alias expansion and command stemming my $action=shift @ARGV; +if (exists $alias{$action}) { + $action=$alias{$action}; +} if (! exists $knownactions{$action}) { - if (exists $alias{$action}) { - $action=$alias{$action}; + 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 { - my @matches = grep { /^\Q$action\E/ } - keys %knownactions, keys %alias; - if (@matches == 1) { - $action=$matches[0]; + die "mr: ambiguous action \"$action\" (matches: ". + join(", ", @matches).")\n"; + } +} + +if ($action eq 'help') { + exec($config{''}{DEFAULT}{$action}) || die "exec: $!"; +} +elsif ($action eq 'config') { + if (@ARGV < 2) { + die "mr config: not enough parameters\n"; + } + my $section=shift; + if ($section=~/^\//) { + # try to convert to a path relative to $config's dir + my ($dir)=$config=~/^(.*\/)[^\/]+$/; + if ($section=~/^\Q$dir\E(.*)/) { + $section=$1; + } + } + my %changefields; + foreach (@ARGV) { + if (/^([^=]+)=(.*)$/) { + $changefields{$1}=$2; } else { - die "mr: ambiguous action \"$action\" (matches @matches)\n"; + my $found=0; + foreach my $topdir (sort keys %config) { + if (exists $config{$topdir}{$section} && + exists $config{$topdir}{$section}{$_}) { + print $config{$topdir}{$section}{$_}."\n"; + $found=1; + } + } + if (! $found) { + die "mr $action: $section $_ not set\n"; + } } } + modifyconfig($config, $section, %changefields) if %changefields; + exit 0; } - -if ($action eq 'help') { - exec($config{''}{default}{help}); +elsif ($action eq 'register') { + my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n". + "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ". + join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV); + print STDERR "mr $action: running >>$command<<\n" if $verbose; + exec($command) || die "exec: $!"; } -# handle being in a subdir of a repository +# work out what repos to act on +my @repos; my $nochdir=0; foreach my $topdir (sort keys %config) { - foreach my $subdir (reverse sort keys %{$config{$topdir}}) { - next if $subdir eq 'default'; - my $d=$directory."/"; - my $dir=$topdir.$subdir; + foreach my $subdir (sort keys %{$config{$topdir}}) { + next if $subdir eq 'DEFAULT'; + my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir; + my $d=$directory; $dir.="/" unless $dir=~/\/$/; - if ($d =~ /^\Q$dir\E/) { - $directory=$topdir.$subdir; - $nochdir=1; - last; + $d.="/" unless $d=~/\/$/; + 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=($subdir =~/^\//) ? $subdir : $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; } my (@failed, @successful, @skipped); -foreach my $topdir (sort keys %config) { - foreach my $subdir (sort keys %{$config{$topdir}}) { - - my $dir=$topdir.$subdir; - - if (defined $directory && - $dir ne $directory && - $dir !~ /^\Q$directory\E\//) { - next; - } - - action($action, $dir, $topdir, $subdir); - } +foreach my $repo (@repos) { + action($action, @$repo); } sub action { my ($action, $dir, $topdir, $subdir) = @_; my $lib= exists $config{$topdir}{$subdir}{lib} ? - $config{$topdir}{$subdir}{lib} : ""; + $config{$topdir}{$subdir}{lib}."\n" : ""; + + if (exists $config{$topdir}{$subdir}{deleted}) { + if (! -d $dir) { + return; + } + else { + my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted}; + print "mr $action: running deleted test >>$test<<\n" if $verbose; + my $ret=system($test); + if ($ret >> 8 == 0) { + print STDERR "mr error: $dir should be deleted yet still exists\n\n"; + push @failed, $dir; + return; + } + } + } if ($action eq 'checkout') { if (-d $dir) { @@ -280,33 +401,42 @@ sub action { } $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 $ret=system($lib.$config{$topdir}{$subdir}{skip}); + my $test="set -e;".$lib. + "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'"; + 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}) { + + if (! $nochdir && ! chdir($dir)) { + print STDERR "mr $action: failed to chdir to $dir: $!\n"; + push @failed, $dir; + } + elsif (! exists $config{$topdir}{$subdir}{$action}) { print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n"; push @skipped, $dir; } else { - print "mr $action: $dir\n"; + if (! $nochdir) { + print "mr $action: $topdir$subdir\n"; + } + else { + print "mr $action: $topdir$subdir (in subdir $directory)\n"; + } my $command="set -e; ".$lib. - "my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ". + "my_action(){ $config{$topdir}{$subdir}{$action}\n }; 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 to run: $command\n" if $verbose; - push @failed, $topdir.$subdir; + print STDERR "mr $action: failed ($ret)\n" if $verbose; + push @failed, $dir; if ($ret >> 8 != 0) { print STDERR "mr $action: command failed\n"; } @@ -356,19 +486,20 @@ sub loadconfig { my $in; my $dir; if (ref $f eq 'GLOB') { - $in=$f; $dir=""; + $in=$f; } else { - # $f might be a symlink + if (! -e $f) { + return; + } + my $absf=abs_path($f); if ($loaded{$absf}) { return; } $loaded{$absf}=1; - print "mr: loading config $f\n" if $verbose; - open($in, "<", $f) || die "mr: open $f: $!\n"; ($dir)=$f=~/^(.*\/)[^\/]+$/; if (! defined $dir) { $dir="."; @@ -379,50 +510,57 @@ sub loadconfig { my $parent=$dir; while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) { if (exists $config{$parent} && - exists $config{$parent}{default}) { - $config{$dir}{default}={ %{$config{$parent}{default}} }; + exists $config{$parent}{DEFAULT}) { + $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} }; last; } } + + print "mr: loading config $f\n" if $verbose; + open($in, "<", $f) || die "mr: open $f: $!\n"; } + my @lines=<$in>; + close $in; my $section; - while (<$in>) { + while (@lines) { + $_=shift @lines; chomp; next if /^\s*\#/ || /^\s*$/; - if (/^\s*\[([^\]]*)\]\s*$/) { + if (/^\[([^\]]*)\]\s*$/) { $section=$1; } - elsif (/^\s*(\w+)\s*=\s*(.*)/) { + elsif (/^(\w+)\s*=\s*(.*)/) { my $parameter=$1; my $value=$2; - # continuation line - while ($value=~/(.*)\\$/) { - $value=$1.<$in>; + # continued value + while (@lines && $lines[0]=~/^\s(.+)/) { + shift(@lines); + $value.="\n$1"; chomp $value; } if (! defined $section) { die "$f line $.: parameter ($parameter) not in section\n"; } - if ($section ne 'alias' && + if ($section ne 'ALIAS' && ! exists $config{$dir}{$section} && - exists $config{$dir}{default}) { + exists $config{$dir}{DEFAULT}) { # copy in defaults - $config{$dir}{$section}={ %{$config{$dir}{default}} }; + $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} }; } - if ($section eq 'alias') { + if ($section eq 'ALIAS') { $alias{$parameter}=$value; } elsif ($parameter eq 'lib') { - $config{$dir}{$section}{lib}.=$value." ; "; + $config{$dir}{$section}{lib}.=$value."\n"; } else { $config{$dir}{$section}{$parameter}=$value; $knownactions{$parameter}=1; if ($parameter eq 'chain' && - length $dir && $section ne "default" && + length $dir && $section ne "DEFAULT" && -e $dir.$section."/.mrconfig" && system($value) >> 8 == 0) { push @toload, $dir.$section."/.mrconfig"; @@ -433,61 +571,233 @@ sub loadconfig { die "$f line $.: parse error\n"; } } - close $in; foreach (@toload) { loadconfig($_); } } +sub modifyconfig { + my $f=shift; + # the section to modify or add + my $targetsection=shift; + # fields to change in the section + # To remove a field, set its value to "". + my %changefields=@_; + + my @lines; + my @out; + + if (-e $f) { + open(my $in, "<", $f) || die "mr: open $f: $!\n"; + @lines=<$in>; + close $in; + } + + my $formatfield=sub { + my $field=shift; + my @value=split(/\n/, shift); + + return "$field = ".shift(@value)."\n". + join("", map { "\t$_\n" } @value); + }; + my $addfields=sub { + my @blanks; + while ($out[$#out] =~ /^\s*$/) { + unshift @blanks, pop @out; + } + foreach my $field (sort keys %changefields) { + if (length $changefields{$field}) { + push @out, "$field = $changefields{$field}\n"; + delete $changefields{$field}; + } + } + push @out, @blanks; + }; + + my $section; + while (@lines) { + $_=shift(@lines); + + if (/^\s*\#/ || /^\s*$/) { + push @out, $_; + } + elsif (/^\[([^\]]*)\]\s*$/) { + if (defined $section && + $section eq $targetsection) { + $addfields->(); + } + + $section=$1; + + push @out, $_; + } + elsif (/^(\w+)\s*=\s(.*)/) { + my $parameter=$1; + my $value=$2; + + # continued value + while (@lines && $lines[0]=~/^\s(.+)/) { + shift(@lines); + $value.="\n$1"; + chomp $value; + } + + if ($section eq $targetsection) { + if (exists $changefields{$parameter}) { + if (length $changefields{$parameter}) { + $value=$changefields{$parameter}; + } + delete $changefields{$parameter}; + } + } + + push @out, $formatfield->($parameter, $value); + } + } + + if (defined $section && + $section eq $targetsection) { + $addfields->(); + } + elsif (%changefields) { + push @out, "\n[$targetsection]\n"; + foreach my $field (sort keys %changefields) { + if (length $changefields{$field}) { + push @out, $formatfield->($field, $changefields{$field}); + } + } + } + + open(my $out, ">", $f) || die "mr: write $f: $!\n"; + print $out @out; + close $out; +} + # Finally, some useful actions that mr knows about by default. # These can be overridden in ~/.mrconfig. __DATA__ -[alias] - co = checkout - ci = commit -[default] -lib = \ - error() { \ - echo "mr: $@" >&2; \ - exit 1; \ +[ALIAS] +co = checkout +ci = commit +ls = list + +[DEFAULT] +lib = + error() { + echo "mr: $@" >&2 + exit 1 + } + hours_since() { + for dir in .git .svn .bzr CVS; do + if [ -e "$MR_REPO/$dir" ]; then + flagfile="$MR_REPO/$dir/.mr_last$1" + break + fi + done + if [ -z "$flagfile" ]; then + error "cannot determine flag filename" + fi + perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile" + touch "$flagfile" } -update = \ - if [ -d "$MR_REPO"/.svn ]; then \ - svn update "$@"; \ - elif [ -d "$MR_REPO"/.git ]; then \ - git pull origin master "$@"; \ - else \ - error "unknown repo type"; \ + +update = + if [ -d "$MR_REPO"/.svn ]; then + svn update "$@" + elif [ -d "$MR_REPO"/.git ]; then + git pull origin master "$@" + elif [ -d "$MR_REPO"/.bzr ]; then + bzr merge "$@" + elif [ -d "$MR_REPO"/CVS ]; then + cvs update "$@" + else + error "unknown repo type" + fi +status = + if [ -d "$MR_REPO"/.svn ]; then + svn status "$@" + elif [ -d "$MR_REPO"/.git ]; then + git status "$@" || true + elif [ -d "$MR_REPO"/.bzr ]; then + bzr status "$@" + elif [ -d "$MR_REPO"/CVS ]; then + cvs status "$@" + else + error "unknown repo type" fi -status = \ - if [ -d "$MR_REPO"/.svn ]; then \ - svn status "$@"; \ - elif [ -d "$MR_REPO"/.git ]; then \ - git status "$@" || true; \ - else \ - error "unknown repo type"; \ +commit = + if [ -d "$MR_REPO"/.svn ]; then + svn commit "$@" + elif [ -d "$MR_REPO"/.git ]; then + git commit -a "$@" && git push --all + elif [ -d "$MR_REPO"/.bzr ]; then + bzr commit "$@" && bzr push + elif [ -d "$MR_REPO"/CVS ]; then + cvs commit "$@" + else + error "unknown repo type" fi -commit = \ - if [ -d "$MR_REPO"/.svn ]; then \ - svn commit "$@"; \ - elif [ -d "$MR_REPO"/.git ]; then \ - git commit -a "$@" && git push --all; \ - else \ - error "unknown repo type"; \ +diff = + if [ -d "$MR_REPO"/.svn ]; then + svn diff "$@" + elif [ -d "$MR_REPO"/.git ]; then + git diff "$@" + elif [ -d "$MR_REPO"/.bzr ]; then + bzr diff "$@" + elif [ -d "$MR_REPO"/CVS ]; then + cvs diff "$@" + else + error "unknown repo type" fi -diff = \ - if [ -d "$MR_REPO"/.svn ]; then \ - svn diff "$@"; \ - elif [ -d "$MR_REPO"/.git ]; then \ - git diff "$@"; \ - else \ - error "unknown repo type"; \ +log = + if [ -d "$MR_REPO"/.svn ]; then + svn log"$@" + elif [ -d "$MR_REPO"/.git ]; then + git log "$@" + elif [ -d "$MR_REPO"/.bzr ]; then + bzr log "$@" + elif [ -d "$MR_REPO"/CVS ]; then + cvs log "$@" + else + error "unknown repo type" fi +register = + if [ -n "$1" ]; then + cd "$1" + fi + basedir="$(basename $(pwd))" + if [ -d .svn ]; then + url=$(LANG=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" + mr config "$(pwd)" checkout="svn co $url $basedir" + elif [ -d .git ]; then + url=$(LANG=C git-config --get remote.origin.url) + if [ -z "$url" ]; then + error "cannot determine git url" + fi + echo "Registering git url: $url" + mr config "$(pwd)" checkout="git clone $url $basedir" + elif [ -d .bzr ]; then + url=$(cat .bzr/branch/parent) + if [ -z "$url" ]; then + error "cannot determine bzr url" + fi + echo "Registering bzr url: $url" + mr config "$(pwd)" checkout="bzr clone $url $basedir" + else + error "unable to register this repo type" + fi +help = + if [ ! -e "$MR_PATH" ]; then + error "cannot find program path" + fi + (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed" 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" +config = + +ed = echo "A horse is a horse, of course, of course.." +T = echo "I pity the fool."