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

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