]> 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:

fix leaf subdir case
[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 order
235
236 The "order" parameter can be used to override the default ordering of
237 repositories. The default order value is 10. Use smaller values to make
238 repositories be processed earlier, and larger values to make repositories
239 be processed later.
240
241 Note that if a repository is located in a subdirectory of another
242 repository, ordering it to be processed earlier is not recommended.
243
244 =item chain
245
246 If the "chain" parameter is set and its command returns true, then B<mr>
247 will try to load a .mrconfig file from the root of the repository. (You
248 should avoid chaining from repositories with untrusted committers.)
249
250 =item lib
251
252 The "lib" parameter can specify some shell code that will be run before each
253 command, this can be a useful way to define shell functions for other commands
254 to use.
255
256 =back
257
258 =head1 AUTHOR
259
260 Copyright 2007 Joey Hess <joey@kitenet.net>
261
262 Licensed under the GNU GPL version 2 or higher.
263
264 http://kitenet.net/~joey/code/mr/
265
266 =cut
267
268 #}}}
269
270 use warnings;
271 use strict;
272 use Getopt::Long;
273 use Cwd qw(getcwd abs_path);
274 use POSIX "WNOHANG";
275 use constant {
276         OK => 0,
277         FAILED => 1,
278         SKIPPED => 2,
279         ABORT => 3,
280 };
281
282 $SIG{INT}=sub {
283         print STDERR "mr: interrupted\n";
284         exit 2;
285 };
286
287 $ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
288 my $config_overridden=0;
289 my $directory=getcwd();
290 my $verbose=0;
291 my $stats=0;
292 my $no_recurse=0;
293 my $jobs=1;
294 my %config;
295 my %configfiles;
296 my %knownactions;
297 my %alias;
298
299 Getopt::Long::Configure("no_permute");
300 my $result=GetOptions(
301         "d|directory=s" => sub { $directory=abs_path($_[1]) },
302         "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
303         "v|verbose" => \$verbose,
304         "s|stats" => \$stats,
305         "n|no-recurse" => \$no_recurse,
306         "j|jobs=i" => \$jobs,
307 );
308 if (! $result || @ARGV < 1) {
309         die("Usage: mr [-d directory] action [params ...]\n".
310             "(Use mr help for man page.)\n");
311
312 }
313
314 # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
315 # the config file might be a symlink to elsewhere, and the directory it's
316 # in is significant.
317 if ($ENV{MR_CONFIG} !~ /^\//) {
318         $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
319 }
320 # Try to set MR_PATH to the path to the program.
321 eval {
322         use FindBin qw($Bin $Script);
323         $ENV{MR_PATH}=$Bin."/".$Script;
324 };
325
326 loadconfig(\*DATA);
327 loadconfig($ENV{MR_CONFIG});
328 #use Data::Dumper;
329 #print Dumper(\%config);
330
331 # alias expansion and command stemming
332 my $action=shift @ARGV;
333 if (exists $alias{$action}) {
334         $action=$alias{$action};
335 }
336 if (! exists $knownactions{$action}) {
337         my @matches = grep { /^\Q$action\E/ }
338                 keys %knownactions, keys %alias;
339         if (@matches == 1) {
340                 $action=$matches[0];
341         }
342         elsif (@matches == 0) {
343                 die "mr: unknown action \"$action\" (known actions: ".
344                         join(", ", sort keys %knownactions).")\n";
345         }
346         else {
347                 die "mr: ambiguous action \"$action\" (matches: ".
348                         join(", ", @matches).")\n";
349         }
350 }
351
352 # commands that do not operate on all repos
353 if ($action eq 'help') {
354         exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
355 }
356 elsif ($action eq 'config') {
357         if (@ARGV < 2) {
358                 die "mr config: not enough parameters\n";
359         }
360         my $section=shift;
361         if ($section=~/^\//) {
362                 # try to convert to a path relative to the config file
363                 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
364                 $dir=abs_path($dir);
365                 $dir.="/" unless $dir=~/\/$/;
366                 if ($section=~/^\Q$dir\E(.*)/) {
367                         $section=$1;
368                 }
369         }
370         my %changefields;
371         foreach (@ARGV) {
372                 if (/^([^=]+)=(.*)$/) {
373                         $changefields{$1}=$2;
374                 }
375                 else {
376                         my $found=0;
377                         foreach my $topdir (sort keys %config) {
378                                 if (exists $config{$topdir}{$section} &&
379                                     exists $config{$topdir}{$section}{$_}) {
380                                         print $config{$topdir}{$section}{$_}."\n";
381                                         $found=1;
382                                         last if $section eq 'DEFAULT';
383                                 }
384                         }
385                         if (! $found) {
386                                 die "mr $action: $section $_ not set\n";
387                         }
388                 }
389         }
390         modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
391         exit 0;
392 }
393 elsif ($action eq 'register') {
394         if (! $config_overridden) {
395                 # Find the closest known mrconfig file to the current
396                 # directory.
397                 $directory.="/" unless $directory=~/\/$/;
398                 foreach my $topdir (reverse sort keys %config) {
399                         next unless length $topdir;
400                         if ($directory=~/^\Q$topdir\E/) {
401                                 $ENV{MR_CONFIG}=$configfiles{$topdir};
402                                 last;
403                         }
404                 }
405         }
406         my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
407                 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
408                 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
409         print STDERR "mr $action: running >>$command<<\n" if $verbose;
410         exec($command) || die "exec: $!";
411 }
412
413 # an ordered list of repos
414 my @list;
415 foreach my $topdir (sort keys %config) {
416         foreach my $subdir (sort keys %{$config{$topdir}}) {
417                 push @list, {
418                         topdir => $topdir,
419                         subdir => $subdir,
420                         order => $config{$topdir}{$subdir}{order},
421                 };
422         }
423 }
424 @list = sort {
425                 $a->{order}  <=> $b->{order}
426                              ||
427                 $a->{topdir} cmp $b->{topdir}
428                              ||
429                 $a->{subdir} cmp $b->{subdir}
430         } @list;
431
432 # work out what repos to act on
433 my @repos;
434 my $nochdir=0;
435 foreach my $repo (@list) {
436         my $topdir=$repo->{topdir};
437         my $subdir=$repo->{subdir};
438
439         next if $subdir eq 'DEFAULT';
440         my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
441         my $d=$directory;
442         $dir.="/" unless $dir=~/\/$/;
443         $d.="/" unless $d=~/\/$/;
444         next if $no_recurse && $d ne $dir;
445         next if $dir ne $d && $dir !~ /^\Q$d\E/;
446         push @repos, [$dir, $topdir, $subdir];
447 }
448 if (! @repos) {
449         # fallback to find a leaf repo
450         foreach my $repo (reverse @list) {
451                 my $topdir=$repo->{topdir};
452                 my $subdir=$repo->{subdir};
453                 
454                 next if $subdir eq 'DEFAULT';
455                 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
456                 my $d=$directory;
457                 $dir.="/" unless $dir=~/\/$/;
458                 $d.="/" unless $d=~/\/$/;
459                 if ($d=~/^\Q$dir\E/) {
460                         push @repos, [$dir, $topdir, $subdir];
461                         last;
462                 }
463         }
464         $nochdir=1;
465 }
466
467 # run the action on each repository and print stats
468 my (@ok, @failed, @skipped);
469 if ($jobs > 1) {
470         mrs(@repos);
471 }
472 else {
473         foreach my $repo (@repos) {
474                 record($repo, action($action, @$repo));
475                 print "\n";
476         }
477 }
478 if (! @ok && ! @failed && ! @skipped) {
479         die "mr $action: no repositories found to work on\n";
480 }
481 print "mr $action: finished (".join("; ",
482         showstat($#ok+1, "ok", "ok"),
483         showstat($#failed+1, "failed", "failed"),
484         showstat($#skipped+1, "skipped", "skipped"),
485 ).")\n";
486 if ($stats) {
487         if (@skipped) {
488                 print "mr $action: (skipped: ".join(" ", @skipped).")\n";
489         }
490         if (@failed) {
491                 print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
492         }
493 }
494 if (@failed) {
495         exit 1;
496 }
497 elsif (! @ok && @skipped) {
498         exit 1;
499 }
500 exit 0;
501
502 sub action { #{{{
503         my ($action, $dir, $topdir, $subdir) = @_;
504
505         $ENV{MR_CONFIG}=$configfiles{$topdir};
506         my $lib=exists $config{$topdir}{$subdir}{lib} ?
507                        $config{$topdir}{$subdir}{lib}."\n" : "";
508
509         if ($action eq 'checkout') {
510                 if (-d $dir) {
511                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
512                         return SKIPPED;
513                 }
514
515                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
516
517                 if (! -d $dir) {
518                         print "mr $action: creating parent directory $dir\n" if $verbose;
519                         system("mkdir", "-p", $dir);
520                 }
521         }
522         elsif ($action =~ /update/) {
523                 if (! -d $dir) {
524                         return action("checkout", $dir, $topdir, $subdir);
525                 }
526         }
527         
528         $ENV{MR_REPO}=$dir;
529
530         if (exists $config{$topdir}{$subdir}{skip}) {
531                 my $test="set -e;".$lib.
532                         "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
533                 print "mr $action: running skip test >>$test<<\n" if $verbose;
534                 my $ret=system($test);
535                 if ($ret != 0) {
536                         if (($? & 127) == 2) {
537                                 print STDERR "mr $action: interrupted\n";
538                                 return ABORT;
539                         }
540                         elsif ($? & 127) {
541                                 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
542                                 return ABORT;
543                         }
544                 }
545                 if ($ret >> 8 == 0) {
546                         print "mr $action: $dir skipped per config file\n" if $verbose;
547                         return SKIPPED;
548                 }
549         }
550         
551         if (! $nochdir && ! chdir($dir)) {
552                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
553                 return FAILED;
554         }
555         elsif (! exists $config{$topdir}{$subdir}{$action}) {
556                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
557                 return SKIPPED;
558         }
559         else {
560                 if (! $nochdir) {
561                         print "mr $action: $topdir$subdir\n";
562                 }
563                 else {
564                         print "mr $action: $topdir$subdir (in subdir $directory)\n";
565                 }
566                 my $command="set -e; ".$lib.
567                         "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
568                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
569                 print STDERR "mr $action: running >>$command<<\n" if $verbose;
570                 my $ret=system($command);
571                 if ($ret != 0) {
572                         if (($? & 127) == 2) {
573                                 print STDERR "mr $action: interrupted\n";
574                                 return ABORT;
575                         }
576                         elsif ($? & 127) {
577                                 print STDERR "mr $action: received signal ".($? & 127)."\n";
578                                 return ABORT;
579                         }
580                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
581                         if ($ret >> 8 != 0) {
582                                 print STDERR "mr $action: command failed\n";
583                         }
584                         elsif ($ret != 0) {
585                                 print STDERR "mr $action: command died ($ret)\n";
586                         }
587                         return FAILED;
588                 }
589                 else {
590                         if ($action eq 'checkout' && ! -d $dir) {
591                                 print STDERR "mr $action: $dir missing after checkout\n";;
592                                 return FAILED;
593                         }
594
595                         return OK;
596                 }
597         }
598 } #}}}
599
600 # run actions on multiple repos, in parallel
601 sub mrs { #{{{
602         $| = 1;
603         my @active;
604         my @fhs;
605         my @out;
606         my $running=0;
607         while (@fhs or @repos) {
608                 while ($running < $jobs && @repos) {
609                         $running++;
610                         my $repo = shift @repos;
611                         pipe(my $outfh, CHILD_STDOUT);
612                         pipe(my $errfh, CHILD_STDERR);
613                         my $pid;
614                         unless ($pid = fork) {
615                                 die "mr $action: cannot fork: $!" unless defined $pid;
616                                 open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
617                                 open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
618                                 close CHILD_STDOUT;
619                                 close CHILD_STDERR;
620                                 close $outfh;
621                                 close $errfh;
622                                 exit action($action, @$repo);
623                         }
624                         close CHILD_STDOUT;
625                         close CHILD_STDERR;
626                         push @active, [$pid, $repo];
627                         push @fhs, [$outfh, $errfh];
628                         push @out, ['',     ''];
629                 }
630                 my ($rin, $rout) = ('','');
631                 my $nfound;
632                 foreach my $fh (@fhs) {
633                         next unless defined $fh;
634                         vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
635                         vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
636                 }
637                 $nfound = select($rout=$rin, undef, undef, 1);
638                 foreach my $channel (0, 1) {
639                         foreach my $i (0..$#fhs) {
640                                 next unless defined $fhs[$i];
641                                 my $fh = $fhs[$i][$channel];
642                                 next unless defined $fh;
643                                 if (vec($rout, fileno($fh), 1) == 1) {
644                                         my $r = '';
645                                         if (sysread($fh, $r, 1024) == 0) {
646                                                 close($fh);
647                                                 $fhs[$i][$channel] = undef;
648                                                 if (! defined $fhs[$i][0] &&
649                                                     ! defined $fhs[$i][1]) {
650                                                         waitpid($active[$i][0], 0);
651                                                         print STDOUT $out[$i][0];
652                                                         print STDERR $out[$i][1];
653                                                         print "\n";
654                                                         record($active[$i][1], $? >> 8);
655                                                         splice(@fhs, $i, 1);
656                                                         splice(@active, $i, 1);
657                                                         splice(@out, $i, 1);
658                                                         $running--;
659                                                 }
660                                         }
661                                         $out[$i][$channel] .= $r;
662                                 }
663                         }
664                 }
665         }
666 } #}}}
667
668 sub record { #{{{
669         my $dir=shift()->[0];
670         my $ret=shift;
671
672         if ($ret == OK) {
673                 push @ok, $dir;
674         }
675         elsif ($ret == FAILED) {
676                 push @failed, $dir;
677         }
678         elsif ($ret == SKIPPED) {
679                 push @skipped, $dir;
680         }
681         elsif ($ret == ABORT) {
682                 exit 1;
683         }
684         else {
685                 die "unknown exit status $ret";
686         }
687 } #}}}
688
689 sub showstat { #{{{
690         my $count=shift;
691         my $singular=shift;
692         my $plural=shift;
693         if ($count) {
694                 return "$count ".($count > 1 ? $plural : $singular);
695         }
696         return;
697 } #}}}
698
699 my %loaded;
700 sub loadconfig { #{{{
701         my $f=shift;
702
703         my @toload;
704
705         my $in;
706         my $dir;
707         if (ref $f eq 'GLOB') {
708                 $dir="";
709                 $in=$f; 
710         }
711         else {
712                 if (! -e $f) {
713                         return;
714                 }
715
716                 my $absf=abs_path($f);
717                 if ($loaded{$absf}) {
718                         return;
719                 }
720                 $loaded{$absf}=1;
721
722                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
723                 if (! defined $dir) {
724                         $dir=".";
725                 }
726                 $dir=abs_path($dir)."/";
727                 
728                 if (! exists $configfiles{$dir}) {
729                         $configfiles{$dir}=$f;
730                 }
731
732                 # copy in defaults from first parent
733                 my $parent=$dir;
734                 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
735                         if ($parent eq '/') {
736                                 $parent="";
737                         }
738                         if (exists $config{$parent} &&
739                             exists $config{$parent}{DEFAULT}) {
740                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
741                                 last;
742                         }
743                 }
744                 
745                 print "mr: loading config $f\n" if $verbose;
746                 open($in, "<", $f) || die "mr: open $f: $!\n";
747         }
748         my @lines=<$in>;
749         close $in;
750
751         my $section;
752         my $line=0;
753         while (@lines) {
754                 $_=shift @lines;
755                 $line++;
756                 chomp;
757                 next if /^\s*\#/ || /^\s*$/;
758                 if (/^\[([^\]]*)\]\s*$/) {
759                         $section=$1;
760                 }
761                 elsif (/^(\w+)\s*=\s*(.*)/) {
762                         my $parameter=$1;
763                         my $value=$2;
764
765                         # continued value
766                         while (@lines && $lines[0]=~/^\s(.+)/) {
767                                 shift(@lines);
768                                 $line++;
769                                 $value.="\n$1";
770                                 chomp $value;
771                         }
772
773                         if (! defined $section) {
774                                 die "$f line $.: parameter ($parameter) not in section\n";
775                         }
776                         if ($section ne 'ALIAS' &&
777                             ! exists $config{$dir}{$section} &&
778                             exists $config{$dir}{DEFAULT}) {
779                                 # copy in defaults
780                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
781                         }
782                         if ($section eq 'ALIAS') {
783                                 $alias{$parameter}=$value;
784                         }
785                         elsif ($parameter eq 'lib') {
786                                 $config{$dir}{$section}{lib}.=$value."\n";
787                         }
788                         else {
789                                 $config{$dir}{$section}{$parameter}=$value;
790                                 $knownactions{$parameter}=1;
791                                 if ($parameter eq 'chain' &&
792                                     length $dir && $section ne "DEFAULT" &&
793                                     -e $dir.$section."/.mrconfig") {
794                                         my $ret=system($value);
795                                         if ($ret != 0) {
796                                                 if (($? & 127) == 2) {
797                                                         print STDERR "mr $action: chain test interrupted\n";
798                                                         exit 2;
799                                                 }
800                                                 elsif ($? & 127) {
801                                                         print STDERR "mr $action: chain test received signal ".($? & 127)."\n";
802                                                 }
803                                         }
804                                         else {
805                                                 push @toload, $dir.$section."/.mrconfig";
806                                         }
807                                 }
808                         }
809                 }
810                 else {
811                         die "$f line $line: parse error\n";
812                 }
813         }
814
815         foreach (@toload) {
816                 loadconfig($_);
817         }
818 } #}}}
819
820 sub modifyconfig { #{{{
821         my $f=shift;
822         # the section to modify or add
823         my $targetsection=shift;
824         # fields to change in the section
825         # To remove a field, set its value to "".
826         my %changefields=@_;
827
828         my @lines;
829         my @out;
830
831         if (-e $f) {
832                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
833                 @lines=<$in>;
834                 close $in;
835         }
836
837         my $formatfield=sub {
838                 my $field=shift;
839                 my @value=split(/\n/, shift);
840
841                 return "$field = ".shift(@value)."\n".
842                         join("", map { "\t$_\n" } @value);
843         };
844         my $addfields=sub {
845                 my @blanks;
846                 while ($out[$#out] =~ /^\s*$/) {
847                         unshift @blanks, pop @out;
848                 }
849                 foreach my $field (sort keys %changefields) {
850                         if (length $changefields{$field}) {
851                                 push @out, "$field = $changefields{$field}\n";
852                                 delete $changefields{$field};
853                         }
854                 }
855                 push @out, @blanks;
856         };
857
858         my $section;
859         while (@lines) {
860                 $_=shift(@lines);
861
862                 if (/^\s*\#/ || /^\s*$/) {
863                         push @out, $_;
864                 }
865                 elsif (/^\[([^\]]*)\]\s*$/) {
866                         if (defined $section && 
867                             $section eq $targetsection) {
868                                 $addfields->();
869                         }
870
871                         $section=$1;
872
873                         push @out, $_;
874                 }
875                 elsif (/^(\w+)\s*=\s(.*)/) {
876                         my $parameter=$1;
877                         my $value=$2;
878
879                         # continued value
880                         while (@lines && $lines[0]=~/^\s(.+)/) {
881                                 shift(@lines);
882                                 $value.="\n$1";
883                                 chomp $value;
884                         }
885
886                         if ($section eq $targetsection) {
887                                 if (exists $changefields{$parameter}) {
888                                         if (length $changefields{$parameter}) {
889                                                 $value=$changefields{$parameter};
890                                         }
891                                         delete $changefields{$parameter};
892                                 }
893                         }
894
895                         push @out, $formatfield->($parameter, $value);
896                 }
897         }
898
899         if (defined $section && 
900             $section eq $targetsection) {
901                 $addfields->();
902         }
903         elsif (%changefields) {
904                 push @out, "\n[$targetsection]\n";
905                 foreach my $field (sort keys %changefields) {
906                         if (length $changefields{$field}) {
907                                 push @out, $formatfield->($field, $changefields{$field});
908                         }
909                 }
910         }
911
912         open(my $out, ">", $f) || die "mr: write $f: $!\n";
913         print $out @out;
914         close $out;     
915 } #}}}
916
917 # Finally, some useful actions that mr knows about by default.
918 # These can be overridden in ~/.mrconfig.
919 #DATA{{{
920 __DATA__
921 [ALIAS]
922 co = checkout
923 ci = commit
924 ls = list
925
926 [DEFAULT]
927 order = 10
928 lib =
929         error() {
930                 echo "mr: $@" >&2
931                 exit 1
932         }
933         hours_since() {
934                 if [ -z "$1" ] || [ -z "$2" ]; then
935                         error "mr: usage: hours_since action num"
936                 fi
937                 for dir in .git .svn .bzr CVS .hg; do
938                         if [ -e "$MR_REPO/$dir" ]; then
939                                 flagfile="$MR_REPO/$dir/.mr_last$1"
940                                 break
941                         fi
942                 done
943                 if [ -z "$flagfile" ]; then
944                         error "cannot determine flag filename"
945                 fi
946                 delta=$(perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile")
947                 if [ "$delta" -lt "$2" ]; then
948                         exit 0
949                 else
950                         touch "$flagfile"
951                         exit 1
952                 fi
953         }
954
955 update =
956         if [ -d "$MR_REPO"/.svn ]; then
957                 svn update "$@"
958         elif [ -d "$MR_REPO"/.git ]; then
959                 if [ -z "$@" ]; then
960                         git pull -t origin master
961                 else
962                         git pull "$@"
963                 fi
964         elif [ -d "$MR_REPO"/.bzr ]; then
965                 bzr merge "$@"
966         elif [ -d "$MR_REPO"/CVS ]; then
967                 cvs update "$@"
968         elif [ -d "$MR_REPO"/.hg ]; then
969                 hg pull "$@" && hg update "$@"
970         else
971                 error "unknown repo type"
972         fi
973 status =
974         if [ -d "$MR_REPO"/.svn ]; then
975                 svn status "$@"
976         elif [ -d "$MR_REPO"/.git ]; then
977                 git status "$@" || true
978         elif [ -d "$MR_REPO"/.bzr ]; then
979                 bzr status "$@"
980         elif [ -d "$MR_REPO"/CVS ]; then
981                 cvs status "$@"
982         elif [ -d "$MR_REPO"/.hg ]; then
983                 hg status "$@"
984         else
985                 error "unknown repo type"
986         fi
987 commit =
988         if [ -d "$MR_REPO"/.svn ]; then
989                 svn commit "$@"
990         elif [ -d "$MR_REPO"/.git ]; then
991                 git commit -a "$@" && git push --all
992         elif [ -d "$MR_REPO"/.bzr ]; then
993                 bzr commit "$@" && bzr push
994         elif [ -d "$MR_REPO"/CVS ]; then
995                 cvs commit "$@"
996         elif [ -d "$MR_REPO"/.hg ]; then
997                 hg commit -m "$@" && hg push
998         else
999                 error "unknown repo type"
1000         fi
1001 diff =
1002         if [ -d "$MR_REPO"/.svn ]; then
1003                 svn diff "$@"
1004         elif [ -d "$MR_REPO"/.git ]; then
1005                 git diff "$@"
1006         elif [ -d "$MR_REPO"/.bzr ]; then
1007                 bzr diff "$@"
1008         elif [ -d "$MR_REPO"/CVS ]; then
1009                 cvs diff "$@"
1010         elif [ -d "$MR_REPO"/.hg ]; then
1011                 hg diff "$@"
1012         else
1013                 error "unknown repo type"
1014         fi
1015 log =
1016         if [ -d "$MR_REPO"/.svn ]; then
1017                 svn log"$@"
1018         elif [ -d "$MR_REPO"/.git ]; then
1019                 git log "$@"
1020         elif [ -d "$MR_REPO"/.bzr ]; then
1021                 bzr log "$@"
1022         elif [ -d "$MR_REPO"/CVS ]; then
1023                 cvs log "$@"
1024         elif [ -d "$MR_REPO"/.hg ]; then
1025                 hg log "$@"
1026         else
1027                 error "unknown repo type"
1028         fi
1029 register =
1030         if [ -n "$1" ]; then
1031                 cd "$1"
1032         fi
1033         basedir="$(basename $(pwd))"
1034         if [ -d .svn ]; then
1035                 url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
1036                 if [ -z "$url" ]; then
1037                         error "cannot determine svn url"
1038                 fi
1039                 echo "Registering svn url: $url in $MR_CONFIG"
1040                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="svn co $url $basedir"
1041         elif [ -d .git ]; then
1042                 url=$(LANG=C git-config --get remote.origin.url)
1043                 if [ -z "$url" ]; then
1044                         error "cannot determine git url"
1045                 fi
1046                 echo "Registering git url: $url in $MR_CONFIG"
1047                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="git clone $url $basedir"
1048         elif [ -d .bzr ]; then
1049                 url=$(cat .bzr/branch/parent)
1050                 if [ -z "$url" ]; then
1051                         error "cannot determine bzr url"
1052                 fi
1053                 echo "Registering bzr url: $url in $MR_CONFIG"
1054                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
1055         elif [ -d CVS ]; then
1056                 repo=$(cat CVS/Repository)
1057                 root=$(cat CVS/Root)
1058                 if [ -z "$root" ]; then
1059                         error "cannot determine cvs root"
1060                 fi
1061                 echo "Registering cvs repository $repo at root $root"
1062                 mr -c "$MR_CONFIG" config "$(pwd)" \
1063                         checkout="cvs -d '$root' co -d $basedir $repo"
1064         elif [ -d .hg ]; then
1065                 url=$(hg showconfig paths.default)
1066                 echo "Registering mercurial repo url: $url in $MR_CONFIG"
1067                 mr -c "$MR_CONFIG" config "$(pwd)" \
1068                         checkout="hg clone $url $basedir"
1069         else
1070                 error "unable to register this repo type"
1071         fi
1072 help =
1073         if [ ! -e "$MR_PATH" ]; then
1074                 error "cannot find program path"
1075         fi
1076         (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
1077 list = true
1078 config = 
1079
1080 ed = echo "A horse is a horse, of course, of course.."
1081 T = echo "I pity the fool."
1082 right = echo "Not found."
1083 #}}}