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

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