]> git.madduck.net Git - code/myrepos.git/blob - mr

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

89f3281f1137cda05469f33afd05b7df82008a39
[code/myrepos.git] / mr
1 #!/usr/bin/perl
2
3 #man{{{
4
5 =head1 NAME
6
7 mr - a Multiple Repository management tool
8
9 =head1 SYNOPSIS
10
11 B<mr> [options] checkout
12
13 B<mr> [options] update
14
15 B<mr> [options] status
16
17 B<mr> [options] commit [-m "message"]
18
19 B<mr> [options] diff
20
21 B<mr> [options] log
22
23 B<mr> [options] register [repository]
24
25 B<mr> [options] config section ["parameter=[value]" ...]
26
27 B<mr> [options] action [params ...]
28
29 =head1 DESCRIPTION
30
31 B<mr> is a Multiple Repository management tool. It can checkout, update, or
32 perform other actions on a set of repositories as if they were one combined
33 respository. It supports any combination of subversion, git, cvs, mecurial and
34 bzr repositories, and support for other revision control systems can easily be
35 added.
36
37 B<mr> cds into and operates on all registered repositories at or below your
38 working directory. Or, if you are in a subdirectory of a repository that
39 contains no other registered repositories, it will stay in that directory,
40 and work on only that repository,
41
42 These predefined commands should be fairly familiar to users of any revision
43 control system:
44
45 =over 4
46
47 =item checkout (or co)
48
49 Checks out any repositories that are not already checked out.
50
51 =item update
52
53 Updates each repository from its configured remote repository.
54
55 If a repository isn't checked out yet, it will first check it out.
56
57 =item status
58
59 Displays a status report for each repository, showing what
60 uncommitted changes are present in the repository.
61
62 =item commit (or ci)
63
64 Commits changes to each repository. (By default, changes are pushed to the
65 remote repository too, when using distributed systems like git.)
66
67 The optional -m parameter allows specifying a commit message.
68
69 =item diff
70
71 Show a diff of uncommitted changes.
72
73 =item log
74
75 Show the commit log.
76
77 =back
78
79 These commands are also available:
80
81 =over 4
82
83 =item list (or ls)
84
85 List the repositories that mr will act on.
86
87 =item register
88
89 Register an existing repository in a mrconfig file. By default, the
90 repository in the current directory is registered, or you can specify a
91 directory to register.
92
93 The mrconfig file that is modified is chosen by either the -c option, or by
94 looking for the closest known one at or below the current directory.
95
96 =item config
97
98 Adds, modifies, removes, or prints a value from a mrconfig file. The next
99 parameter is the name of the section the value is in. To add or modify
100 values, use one or more instances of "parameter=value". Use "parameter=" to
101 remove a parameter. Use just "parameter" to get the value of a parameter.
102
103 For example, to add (or edit) a repository in src/foo:
104
105   mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
106
107 To show the command that mr uses to update the repository in src/foo:
108
109   mr config src/foo update
110
111 To see the built-in library of shell functions contained in mr:
112
113   mr config DEFAULT lib
114
115 The ~/.mrconfig file is used by default. To use a different config file,
116 use the -c option.
117
118 =item help
119
120 Displays this help.
121
122 =back
123
124 Actions can be abbreviated to any unambiguous subsctring, so
125 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
126 update"
127
128 Additional parameters can be passed to most commands, and are passed on
129 unchanged to the underlying revision control system. This is mostly useful
130 if the repositories mr will act on all use the same revision control
131 system.
132
133 =head1 OPTIONS
134
135 =over 4
136
137 =item -d directory
138
139 Specifies the topmost directory that B<mr> should work in. The default is
140 the current working directory.
141
142 =item -c mrconfig
143
144 Use the specified mrconfig file. The default is B<~/.mrconfig>
145
146 =item -v
147
148 Be verbose.
149
150 =item -s
151
152 Expand the statistics line displayed at the end to include information
153 about exactly which repositories failed and were skipped, if any.
154
155 =item -n
156
157 Just operate on the repository for the current directory, do not 
158 recurse into deeper repositories.
159
160 =item -j number
161
162 Run the specified number of jobs in parallel. This can greatly speed up
163 operations such as updates. It is not recommended for interactive
164 operations.
165
166 =back
167
168 =head1 FILES
169
170 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
171 file in your home directory, and this can in turn chain load .mrconfig files
172 from repositories.
173
174 Here is an example .mrconfig file:
175
176   [src]
177   checkout = svn co svn://svn.example.com/src/trunk src
178   chain = true
179
180   [src/linux-2.6]
181   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
182         cd linux-2.6 &&
183         git checkout -b mybranch origin/master
184
185 The .mrconfig file uses a variant of the INI file format. Lines starting with
186 "#" are comments. Values can be continued to the following line by
187 indenting the line with whitespace.
188
189 The "DEFAULT" section allows setting default values for the sections that
190 come after it.
191
192 The "ALIAS" section allows adding aliases for actions. Each parameter
193 is an alias, and its value is the action to use.
194
195 All other sections add repositories. The section header specifies the
196 directory where the repository is located. This is relative to the directory
197 that contains the mrconfig file, but you can also choose to use absolute
198 paths.
199
200 Within a section, each parameter defines a shell command to run to handle a
201 given action. mr contains default handlers for "update", "status",
202 "commit", and other standard actions. Normally you only need to specify what
203 to do for "checkout".
204
205 Note that these shell commands are run in a "set -e" shell
206 environment, where any additional parameters you pass are available in
207 "$@". The "checkout" command is run in the parent of the repository
208 directory, since the repository isn't checked out yet. All other commands
209 are run inside the repository, though not necessarily at the top of it.
210
211 The "MR_REPO" environment variable is set to the path to the top of the
212 repository. The "MR_CONFIG" environment variable is set to the .mrconfig file
213 that defines the repo being acted on, or, if the repo is not yet in a config
214 file, the .mrconfig file that should be modified to register the repo.
215
216 A few parameters have special meanings:
217
218 =over 4
219
220 =item skip
221
222 If the "skip" parameter is set and its command returns true, then B<mr>
223 will skip acting on that repository. The command is passed the action
224 name in $1.
225
226 Here are two examples. The first skips the repo unless
227 mr is run by joey. The second uses the hours_since function
228 (included in mr's built-in library) to skip updating the repo unless it's
229 been at least 12 hours since the last update.
230
231   skip = test $(whoami) != joey
232   skip = [ "$1" = update ] && ! hours_since "$1" 12
233
234 =item chain
235
236 If the "chain" parameter is set and its command returns true, then B<mr>
237 will try to load a .mrconfig file from the root of the repository. (You
238 should avoid chaining from repositories with untrusted committers.)
239
240 =item lib
241
242 The "lib" parameter can specify some shell code that will be run before each
243 command, this can be a useful way to define shell functions for other commands
244 to use.
245
246 =back
247
248 =head1 AUTHOR
249
250 Copyright 2007 Joey Hess <joey@kitenet.net>
251
252 Licensed under the GNU GPL version 2 or higher.
253
254 http://kitenet.net/~joey/code/mr/
255
256 =cut
257
258 #}}}
259
260 use warnings;
261 use strict;
262 use Getopt::Long;
263 use Cwd qw(getcwd abs_path);
264 use POSIX "WNOHANG";
265 use constant {
266         OK => 0,
267         FAILED => 1,
268         SKIPPED => 2,
269         ABORT => 3,
270 };
271
272 $SIG{INT}=sub {
273         print STDERR "mr: interrupted\n";
274         exit 2;
275 };
276
277 $ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
278 my $config_overridden=0;
279 my $directory=getcwd();
280 my $verbose=0;
281 my $stats=0;
282 my $no_recurse=0;
283 my $jobs=1;
284 my %config;
285 my %configfiles;
286 my %knownactions;
287 my %alias;
288
289 Getopt::Long::Configure("no_permute");
290 my $result=GetOptions(
291         "d|directory=s" => sub { $directory=abs_path($_[1]) },
292         "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
293         "v|verbose" => \$verbose,
294         "s|stats" => \$stats,
295         "n|no-recurse" => \$no_recurse,
296         "j|jobs=i" => \$jobs,
297 );
298 if (! $result || @ARGV < 1) {
299         die("Usage: mr [-d directory] action [params ...]\n".
300             "(Use mr help for man page.)\n");
301
302 }
303
304 # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
305 # the config file might be a symlink to elsewhere, and the directory it's
306 # in is significant.
307 if ($ENV{MR_CONFIG} !~ /^\//) {
308         $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
309 }
310 # Try to set MR_PATH to the path to the program.
311 eval {
312         use FindBin qw($Bin $Script);
313         $ENV{MR_PATH}=$Bin."/".$Script;
314 };
315
316 loadconfig(\*DATA);
317 loadconfig($ENV{MR_CONFIG});
318 #use Data::Dumper;
319 #print Dumper(\%config);
320
321 # alias expansion and command stemming
322 my $action=shift @ARGV;
323 if (exists $alias{$action}) {
324         $action=$alias{$action};
325 }
326 if (! exists $knownactions{$action}) {
327         my @matches = grep { /^\Q$action\E/ }
328                 keys %knownactions, keys %alias;
329         if (@matches == 1) {
330                 $action=$matches[0];
331         }
332         elsif (@matches == 0) {
333                 die "mr: unknown action \"$action\" (known actions: ".
334                         join(", ", sort keys %knownactions).")\n";
335         }
336         else {
337                 die "mr: ambiguous action \"$action\" (matches: ".
338                         join(", ", @matches).")\n";
339         }
340 }
341
342 # commands that do not operate on all repos
343 if ($action eq 'help') {
344         exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
345 }
346 elsif ($action eq 'config') {
347         if (@ARGV < 2) {
348                 die "mr config: not enough parameters\n";
349         }
350         my $section=shift;
351         if ($section=~/^\//) {
352                 # try to convert to a path relative to the config file
353                 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
354                 $dir=abs_path($dir);
355                 $dir.="/" unless $dir=~/\/$/;
356                 if ($section=~/^\Q$dir\E(.*)/) {
357                         $section=$1;
358                 }
359         }
360         my %changefields;
361         foreach (@ARGV) {
362                 if (/^([^=]+)=(.*)$/) {
363                         $changefields{$1}=$2;
364                 }
365                 else {
366                         my $found=0;
367                         foreach my $topdir (sort keys %config) {
368                                 if (exists $config{$topdir}{$section} &&
369                                     exists $config{$topdir}{$section}{$_}) {
370                                         print $config{$topdir}{$section}{$_}."\n";
371                                         $found=1;
372                                         last if $section eq 'DEFAULT';
373                                 }
374                         }
375                         if (! $found) {
376                                 die "mr $action: $section $_ not set\n";
377                         }
378                 }
379         }
380         modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
381         exit 0;
382 }
383 elsif ($action eq 'register') {
384         if (! $config_overridden) {
385                 # Find the closest known mrconfig file to the current
386                 # directory.
387                 $directory.="/" unless $directory=~/\/$/;
388                 foreach my $topdir (reverse sort keys %config) {
389                         next unless length $topdir;
390                         if ($directory=~/^\Q$topdir\E/) {
391                                 $ENV{MR_CONFIG}=$configfiles{$topdir};
392                                 last;
393                         }
394                 }
395         }
396         my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
397                 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
398                 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
399         print STDERR "mr $action: running >>$command<<\n" if $verbose;
400         exec($command) || die "exec: $!";
401 }
402
403 # work out what repos to act on
404 my @repos;
405 my $nochdir=0;
406 foreach my $topdir (sort keys %config) {
407         foreach my $subdir (sort keys %{$config{$topdir}}) {
408                 next if $subdir eq 'DEFAULT';
409                 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
410                 my $d=$directory;
411                 $dir.="/" unless $dir=~/\/$/;
412                 $d.="/" unless $d=~/\/$/;
413                 next if $no_recurse && $d ne $dir;
414                 next if $dir ne $d && $dir !~ /^\Q$d\E/;
415                 push @repos, [$dir, $topdir, $subdir];
416         }
417 }
418 if (! @repos) {
419         # fallback to find a leaf repo
420         LEAF: foreach my $topdir (reverse sort keys %config) {
421                 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
422                         next if $subdir eq 'DEFAULT';
423                         my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
424                         my $d=$directory;
425                         $dir.="/" unless $dir=~/\/$/;
426                         $d.="/" unless $d=~/\/$/;
427                         if ($d=~/^\Q$dir\E/) {
428                                 push @repos, [$dir, $topdir, $subdir];
429                                 last LEAF;
430                         }
431                 }
432         }
433         $nochdir=1;
434 }
435
436 # run the action on each repository and print stats
437 my (@ok, @failed, @skipped);
438 if ($jobs > 1) {
439         mrs(@repos);
440 }
441 else {
442         foreach my $repo (@repos) {
443                 record($repo, action($action, @$repo));
444                 print "\n";
445         }
446 }
447 if (! @ok && ! @failed && ! @skipped) {
448         die "mr $action: no repositories found to work on\n";
449 }
450 print "mr $action: finished (".join("; ",
451         showstat($#ok+1, "ok", "ok"),
452         showstat($#failed+1, "failed", "failed"),
453         showstat($#skipped+1, "skipped", "skipped"),
454 ).")\n";
455 if ($stats) {
456         if (@skipped) {
457                 print "mr $action: (skipped: ".join(" ", @skipped).")\n";
458         }
459         if (@failed) {
460                 print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
461         }
462 }
463 if (@failed) {
464         exit 1;
465 }
466 elsif (! @ok && @skipped) {
467         exit 1;
468 }
469 exit 0;
470
471 sub action { #{{{
472         my ($action, $dir, $topdir, $subdir) = @_;
473
474         $ENV{MR_CONFIG}=$configfiles{$topdir};
475         my $lib=exists $config{$topdir}{$subdir}{lib} ?
476                        $config{$topdir}{$subdir}{lib}."\n" : "";
477
478         if ($action eq 'checkout') {
479                 if (-d $dir) {
480                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
481                         return SKIPPED;
482                 }
483
484                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
485
486                 if (! -d $dir) {
487                         print "mr $action: creating parent directory $dir\n" if $verbose;
488                         system("mkdir", "-p", $dir);
489                 }
490         }
491         elsif ($action =~ /update/) {
492                 if (! -d $dir) {
493                         return action("checkout", $dir, $topdir, $subdir);
494                 }
495         }
496         
497         $ENV{MR_REPO}=$dir;
498
499         if (exists $config{$topdir}{$subdir}{skip}) {
500                 my $test="set -e;".$lib.
501                         "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
502                 print "mr $action: running skip test >>$test<<\n" if $verbose;
503                 my $ret=system($test);
504                 if ($ret != 0) {
505                         if (($? & 127) == 2) {
506                                 print STDERR "mr $action: interrupted\n";
507                                 return ABORT;
508                         }
509                         elsif ($? & 127) {
510                                 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
511                                 return ABORT;
512                         }
513                 }
514                 if ($ret >> 8 == 0) {
515                         print "mr $action: $dir skipped per config file\n" if $verbose;
516                         return SKIPPED;
517                 }
518         }
519         
520         if (! $nochdir && ! chdir($dir)) {
521                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
522                 return FAILED;
523         }
524         elsif (! exists $config{$topdir}{$subdir}{$action}) {
525                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
526                 return SKIPPED;
527         }
528         else {
529                 if (! $nochdir) {
530                         print "mr $action: $topdir$subdir\n";
531                 }
532                 else {
533                         print "mr $action: $topdir$subdir (in subdir $directory)\n";
534                 }
535                 my $command="set -e; ".$lib.
536                         "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
537                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
538                 print STDERR "mr $action: running >>$command<<\n" if $verbose;
539                 my $ret=system($command);
540                 if ($ret != 0) {
541                         if (($? & 127) == 2) {
542                                 print STDERR "mr $action: interrupted\n";
543                                 return ABORT;
544                         }
545                         elsif ($? & 127) {
546                                 print STDERR "mr $action: received signal ".($? & 127)."\n";
547                                 return ABORT;
548                         }
549                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
550                         if ($ret >> 8 != 0) {
551                                 print STDERR "mr $action: command failed\n";
552                         }
553                         elsif ($ret != 0) {
554                                 print STDERR "mr $action: command died ($ret)\n";
555                         }
556                         return FAILED;
557                 }
558                 else {
559                         if ($action eq 'checkout' && ! -d $dir) {
560                                 print STDERR "mr $action: $dir missing after checkout\n";;
561                                 return FAILED;
562                         }
563
564                         return OK;
565                 }
566         }
567 } #}}}
568
569 # run actions on multiple repos, in parallel
570 sub mrs { #{{{
571         $| = 1;
572         my @active;
573         my @fhs;
574         my @out;
575         my $running=0;
576         while (@fhs or @repos) {
577                 while ($running < $jobs && @repos) {
578                         $running++;
579                         my $repo = shift @repos;
580                         pipe(my $outfh, CHILD_STDOUT);
581                         pipe(my $errfh, CHILD_STDERR);
582                         my $pid;
583                         unless ($pid = fork) {
584                                 die "mr $action: cannot fork: $!" unless defined $pid;
585                                 open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
586                                 open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
587                                 close CHILD_STDOUT;
588                                 close CHILD_STDERR;
589                                 close $outfh;
590                                 close $errfh;
591                                 exit action($action, @$repo);
592                         }
593                         close CHILD_STDOUT;
594                         close CHILD_STDERR;
595                         push @active, [$pid, $repo];
596                         push @fhs, [$outfh, $errfh];
597                         push @out, ['',     ''];
598                 }
599                 my ($rin, $rout) = ('','');
600                 my $nfound;
601                 foreach my $fh (@fhs) {
602                         next unless defined $fh;
603                         vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
604                         vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
605                 }
606                 $nfound = select($rout=$rin, undef, undef, 1);
607                 foreach my $channel (0, 1) {
608                         foreach my $i (0..$#fhs) {
609                                 next unless defined $fhs[$i];
610                                 my $fh = $fhs[$i][$channel];
611                                 next unless defined $fh;
612                                 if (vec($rout, fileno($fh), 1) == 1) {
613                                         my $r = '';
614                                         if (sysread($fh, $r, 1024) == 0) {
615                                                 close($fh);
616                                                 $fhs[$i][$channel] = undef;
617                                                 if (! defined $fhs[$i][0] &&
618                                                     ! defined $fhs[$i][1]) {
619                                                         waitpid($active[$i][0], 0);
620                                                         print STDOUT $out[$i][0];
621                                                         print STDERR $out[$i][1];
622                                                         print "\n";
623                                                         record($active[$i][1], $? >> 8);
624                                                         splice(@fhs, $i, 1);
625                                                         splice(@active, $i, 1);
626                                                         splice(@out, $i, 1);
627                                                         $running--;
628                                                 }
629                                         }
630                                         $out[$i][$channel] .= $r;
631                                 }
632                         }
633                 }
634         }
635 } #}}}
636
637 sub record { #{{{
638         my $dir=shift()->[0];
639         my $ret=shift;
640
641         if ($ret == OK) {
642                 push @ok, $dir;
643         }
644         elsif ($ret == FAILED) {
645                 push @failed, $dir;
646         }
647         elsif ($ret == SKIPPED) {
648                 push @skipped, $dir;
649         }
650         elsif ($ret == ABORT) {
651                 exit 1;
652         }
653         else {
654                 die "unknown exit status $ret";
655         }
656 } #}}}
657
658 sub showstat { #{{{
659         my $count=shift;
660         my $singular=shift;
661         my $plural=shift;
662         if ($count) {
663                 return "$count ".($count > 1 ? $plural : $singular);
664         }
665         return;
666 } #}}}
667
668 my %loaded;
669 sub loadconfig { #{{{
670         my $f=shift;
671
672         my @toload;
673
674         my $in;
675         my $dir;
676         if (ref $f eq 'GLOB') {
677                 $dir="";
678                 $in=$f; 
679         }
680         else {
681                 if (! -e $f) {
682                         return;
683                 }
684
685                 my $absf=abs_path($f);
686                 if ($loaded{$absf}) {
687                         return;
688                 }
689                 $loaded{$absf}=1;
690
691                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
692                 if (! defined $dir) {
693                         $dir=".";
694                 }
695                 $dir=abs_path($dir)."/";
696                 
697                 if (! exists $configfiles{$dir}) {
698                         $configfiles{$dir}=$f;
699                 }
700
701                 # copy in defaults from first parent
702                 my $parent=$dir;
703                 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
704                         if ($parent eq '/') {
705                                 $parent="";
706                         }
707                         if (exists $config{$parent} &&
708                             exists $config{$parent}{DEFAULT}) {
709                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
710                                 last;
711                         }
712                 }
713                 
714                 print "mr: loading config $f\n" if $verbose;
715                 open($in, "<", $f) || die "mr: open $f: $!\n";
716         }
717         my @lines=<$in>;
718         close $in;
719
720         my $section;
721         my $line=0;
722         while (@lines) {
723                 $_=shift @lines;
724                 $line++;
725                 chomp;
726                 next if /^\s*\#/ || /^\s*$/;
727                 if (/^\[([^\]]*)\]\s*$/) {
728                         $section=$1;
729                 }
730                 elsif (/^(\w+)\s*=\s*(.*)/) {
731                         my $parameter=$1;
732                         my $value=$2;
733
734                         # continued value
735                         while (@lines && $lines[0]=~/^\s(.+)/) {
736                                 shift(@lines);
737                                 $line++;
738                                 $value.="\n$1";
739                                 chomp $value;
740                         }
741
742                         if (! defined $section) {
743                                 die "$f line $.: parameter ($parameter) not in section\n";
744                         }
745                         if ($section ne 'ALIAS' &&
746                             ! exists $config{$dir}{$section} &&
747                             exists $config{$dir}{DEFAULT}) {
748                                 # copy in defaults
749                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
750                         }
751                         if ($section eq 'ALIAS') {
752                                 $alias{$parameter}=$value;
753                         }
754                         elsif ($parameter eq 'lib') {
755                                 $config{$dir}{$section}{lib}.=$value."\n";
756                         }
757                         else {
758                                 $config{$dir}{$section}{$parameter}=$value;
759                                 $knownactions{$parameter}=1;
760                                 if ($parameter eq 'chain' &&
761                                     length $dir && $section ne "DEFAULT" &&
762                                     -e $dir.$section."/.mrconfig") {
763                                         my $ret=system($value);
764                                         if ($ret != 0) {
765                                                 if (($? & 127) == 2) {
766                                                         print STDERR "mr $action: chain test interrupted\n";
767                                                         exit 2;
768                                                 }
769                                                 elsif ($? & 127) {
770                                                         print STDERR "mr $action: chain test received signal ".($? & 127)."\n";
771                                                 }
772                                         }
773                                         else {
774                                                 push @toload, $dir.$section."/.mrconfig";
775                                         }
776                                 }
777                         }
778                 }
779                 else {
780                         die "$f line $line: parse error\n";
781                 }
782         }
783
784         foreach (@toload) {
785                 loadconfig($_);
786         }
787 } #}}}
788
789 sub modifyconfig { #{{{
790         my $f=shift;
791         # the section to modify or add
792         my $targetsection=shift;
793         # fields to change in the section
794         # To remove a field, set its value to "".
795         my %changefields=@_;
796
797         my @lines;
798         my @out;
799
800         if (-e $f) {
801                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
802                 @lines=<$in>;
803                 close $in;
804         }
805
806         my $formatfield=sub {
807                 my $field=shift;
808                 my @value=split(/\n/, shift);
809
810                 return "$field = ".shift(@value)."\n".
811                         join("", map { "\t$_\n" } @value);
812         };
813         my $addfields=sub {
814                 my @blanks;
815                 while ($out[$#out] =~ /^\s*$/) {
816                         unshift @blanks, pop @out;
817                 }
818                 foreach my $field (sort keys %changefields) {
819                         if (length $changefields{$field}) {
820                                 push @out, "$field = $changefields{$field}\n";
821                                 delete $changefields{$field};
822                         }
823                 }
824                 push @out, @blanks;
825         };
826
827         my $section;
828         while (@lines) {
829                 $_=shift(@lines);
830
831                 if (/^\s*\#/ || /^\s*$/) {
832                         push @out, $_;
833                 }
834                 elsif (/^\[([^\]]*)\]\s*$/) {
835                         if (defined $section && 
836                             $section eq $targetsection) {
837                                 $addfields->();
838                         }
839
840                         $section=$1;
841
842                         push @out, $_;
843                 }
844                 elsif (/^(\w+)\s*=\s(.*)/) {
845                         my $parameter=$1;
846                         my $value=$2;
847
848                         # continued value
849                         while (@lines && $lines[0]=~/^\s(.+)/) {
850                                 shift(@lines);
851                                 $value.="\n$1";
852                                 chomp $value;
853                         }
854
855                         if ($section eq $targetsection) {
856                                 if (exists $changefields{$parameter}) {
857                                         if (length $changefields{$parameter}) {
858                                                 $value=$changefields{$parameter};
859                                         }
860                                         delete $changefields{$parameter};
861                                 }
862                         }
863
864                         push @out, $formatfield->($parameter, $value);
865                 }
866         }
867
868         if (defined $section && 
869             $section eq $targetsection) {
870                 $addfields->();
871         }
872         elsif (%changefields) {
873                 push @out, "\n[$targetsection]\n";
874                 foreach my $field (sort keys %changefields) {
875                         if (length $changefields{$field}) {
876                                 push @out, $formatfield->($field, $changefields{$field});
877                         }
878                 }
879         }
880
881         open(my $out, ">", $f) || die "mr: write $f: $!\n";
882         print $out @out;
883         close $out;     
884 } #}}}
885
886 # Finally, some useful actions that mr knows about by default.
887 # These can be overridden in ~/.mrconfig.
888 #DATA{{{
889 __DATA__
890 [ALIAS]
891 co = checkout
892 ci = commit
893 ls = list
894
895 [DEFAULT]
896 lib =
897         error() {
898                 echo "mr: $@" >&2
899                 exit 1
900         }
901         hours_since() {
902                 if [ -z "$1" ] || [ -z "$2" ]; then
903                         error "mr: usage: hours_since action num"
904                 fi
905                 for dir in .git .svn .bzr CVS .hg; do
906                         if [ -e "$MR_REPO/$dir" ]; then
907                                 flagfile="$MR_REPO/$dir/.mr_last$1"
908                                 break
909                         fi
910                 done
911                 if [ -z "$flagfile" ]; then
912                         error "cannot determine flag filename"
913                 fi
914                 delta=$(perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile")
915                 if [ "$delta" -lt "$2" ]; then
916                         exit 0
917                 else
918                         touch "$flagfile"
919                         exit 1
920                 fi
921         }
922
923 update =
924         if [ -d "$MR_REPO"/.svn ]; then
925                 svn update "$@"
926         elif [ -d "$MR_REPO"/.git ]; then
927                 if [ -z "$@" ]; then
928                         git pull -t origin master
929                 else
930                         git pull "$@"
931                 fi
932         elif [ -d "$MR_REPO"/.bzr ]; then
933                 bzr merge "$@"
934         elif [ -d "$MR_REPO"/CVS ]; then
935                 cvs update "$@"
936         elif [ -d "$MR_REPO"/.hg ]; then
937                 hg pull "$@" && hg update "$@"
938         else
939                 error "unknown repo type"
940         fi
941 status =
942         if [ -d "$MR_REPO"/.svn ]; then
943                 svn status "$@"
944         elif [ -d "$MR_REPO"/.git ]; then
945                 git status "$@" || true
946         elif [ -d "$MR_REPO"/.bzr ]; then
947                 bzr status "$@"
948         elif [ -d "$MR_REPO"/CVS ]; then
949                 cvs status "$@"
950         elif [ -d "$MR_REPO"/.hg ]; then
951                 hg status "$@"
952         else
953                 error "unknown repo type"
954         fi
955 commit =
956         if [ -d "$MR_REPO"/.svn ]; then
957                 svn commit "$@"
958         elif [ -d "$MR_REPO"/.git ]; then
959                 git commit -a "$@" && git push --all
960         elif [ -d "$MR_REPO"/.bzr ]; then
961                 bzr commit "$@" && bzr push
962         elif [ -d "$MR_REPO"/CVS ]; then
963                 cvs commit "$@"
964         elif [ -d "$MR_REPO"/.hg ]; then
965                 hg commit -m "$@" && hg push
966         else
967                 error "unknown repo type"
968         fi
969 diff =
970         if [ -d "$MR_REPO"/.svn ]; then
971                 svn diff "$@"
972         elif [ -d "$MR_REPO"/.git ]; then
973                 git diff "$@"
974         elif [ -d "$MR_REPO"/.bzr ]; then
975                 bzr diff "$@"
976         elif [ -d "$MR_REPO"/CVS ]; then
977                 cvs diff "$@"
978         elif [ -d "$MR_REPO"/.hg ]; then
979                 hg diff "$@"
980         else
981                 error "unknown repo type"
982         fi
983 log =
984         if [ -d "$MR_REPO"/.svn ]; then
985                 svn log"$@"
986         elif [ -d "$MR_REPO"/.git ]; then
987                 git log "$@"
988         elif [ -d "$MR_REPO"/.bzr ]; then
989                 bzr log "$@"
990         elif [ -d "$MR_REPO"/CVS ]; then
991                 cvs log "$@"
992         elif [ -d "$MR_REPO"/.hg ]; then
993                 hg log "$@"
994         else
995                 error "unknown repo type"
996         fi
997 register =
998         if [ -n "$1" ]; then
999                 cd "$1"
1000         fi
1001         basedir="$(basename $(pwd))"
1002         if [ -d .svn ]; then
1003                 url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
1004                 if [ -z "$url" ]; then
1005                         error "cannot determine svn url"
1006                 fi
1007                 echo "Registering svn url: $url in $MR_CONFIG"
1008                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="svn co $url $basedir"
1009         elif [ -d .git ]; then
1010                 url=$(LANG=C git-config --get remote.origin.url)
1011                 if [ -z "$url" ]; then
1012                         error "cannot determine git url"
1013                 fi
1014                 echo "Registering git url: $url in $MR_CONFIG"
1015                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="git clone $url $basedir"
1016         elif [ -d .bzr ]; then
1017                 url=$(cat .bzr/branch/parent)
1018                 if [ -z "$url" ]; then
1019                         error "cannot determine bzr url"
1020                 fi
1021                 echo "Registering bzr url: $url in $MR_CONFIG"
1022                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
1023         elif [ -d CVS ]; then
1024                 repo=$(cat CVS/Repository)
1025                 root=$(cat CVS/Root)
1026                 if [ -z "$root" ]; then
1027                         error "cannot determine cvs root"
1028                 fi
1029                 echo "Registering cvs repository $repo at root $root"
1030                 mr -c "$MR_CONFIG" config "$(pwd)" \
1031                         checkout="cvs -d '$root' co -d $basedir $repo"
1032         elif [ -d .hg ]; then
1033                 url=$(hg showconfig paths.default)
1034                 echo "Registering mercurial repo url: $url in $MR_CONFIG"
1035                 mr -c "$MR_CONFIG" config "$(pwd)" \
1036                         checkout="hg clone $url $basedir"
1037         else
1038                 error "unable to register this repo type"
1039         fi
1040 help =
1041         if [ ! -e "$MR_PATH" ]; then
1042                 error "cannot find program path"
1043         fi
1044         (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
1045 list = true
1046 config = 
1047
1048 ed = echo "A horse is a horse, of course, of course.."
1049 T = echo "I pity the fool."
1050 right = echo "Not found."
1051 #}}}