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

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