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

2b1a36c22b21df656ca592cca16439fd029ad00d
[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 my %jobs;
573 sub mrs { #{{{
574         $| = 1;
575         my @fhs;
576         my @out;
577         my $running=0;
578         while (@fhs or @repos) {
579                 while ($running < $jobs && @repos) {
580                         $SIG{CHLD}='DEFAULT';
581                         $running++;
582                         my $repo = shift @repos;
583                         my $pid = open(my $fh, "-|");
584                         if (! $pid) {
585                                 open(STDERR, ">&STDOUT");
586                                 exit action($action, @$repo);
587                         }
588                         $jobs{$pid}=$repo;
589                         push @fhs, $fh;
590                         push @out, "";
591                         reaper();
592                         $SIG{CHLD}=\&reaper;
593                 }
594                 my ($rin, $rout) = ('','', '');
595                 my $nfound;
596                 foreach my $x (@fhs) {
597                         next unless defined $x;
598                         vec($rin, fileno($x), 1) = 1;
599                 }
600                 $nfound = select($rout=$rin, undef, undef, 1);
601                 foreach my $i (0..$#fhs) {
602                         my $fh = $fhs[$i];
603                         next unless defined $fh;
604                         if (vec($rout, fileno($fh), 1) == 1) {
605                                 my $r = '';
606                                 if (sysread($fh, $r, 1024) == 0) {
607                                         close($fh);
608                                         $fhs[$i] = undef;
609                                         $running--;
610                                         print $out[$i];
611                                         $out[$i]='';
612                                 }
613                                 $out[$i] .= $r;
614                         }
615                 }
616                 while (@fhs and !defined $fhs[0]) {
617                         shift @fhs;
618                         shift @out;
619                 }
620         }
621 } #}}}
622
623 sub reaper { #{{{
624         while ((my $pid = waitpid(-1, &WNOHANG)) > 0) {
625                 record($jobs{$pid}, $? >> 8) if exists $jobs{$pid};
626         }
627 } #}}}
628                 
629 sub record { #{{{
630         my $dir=shift()->[0];
631         my $ret=shift;
632
633         if ($ret == OK) {
634                 push @ok, $dir;
635         }
636         elsif ($ret == FAILED) {
637                 push @failed, $dir;
638         }
639         elsif ($ret == SKIPPED) {
640                 push @skipped, $dir;
641         }
642         elsif ($ret == ABORT) {
643                 exit 1;
644         }
645         else {
646                 die "unknown exit status $ret";
647         }
648 } #}}}
649
650 sub showstat { #{{{
651         my $count=shift;
652         my $singular=shift;
653         my $plural=shift;
654         if ($count) {
655                 return "$count ".($count > 1 ? $plural : $singular);
656         }
657         return;
658 } #}}}
659
660 my %loaded;
661 sub loadconfig { #{{{
662         my $f=shift;
663
664         my @toload;
665
666         my $in;
667         my $dir;
668         if (ref $f eq 'GLOB') {
669                 $dir="";
670                 $in=$f; 
671         }
672         else {
673                 if (! -e $f) {
674                         return;
675                 }
676
677                 my $absf=abs_path($f);
678                 if ($loaded{$absf}) {
679                         return;
680                 }
681                 $loaded{$absf}=1;
682
683                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
684                 if (! defined $dir) {
685                         $dir=".";
686                 }
687                 $dir=abs_path($dir)."/";
688                 
689                 if (! exists $configfiles{$dir}) {
690                         $configfiles{$dir}=$f;
691                 }
692
693                 # copy in defaults from first parent
694                 my $parent=$dir;
695                 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
696                         if ($parent eq '/') {
697                                 $parent="";
698                         }
699                         if (exists $config{$parent} &&
700                             exists $config{$parent}{DEFAULT}) {
701                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
702                                 last;
703                         }
704                 }
705                 
706                 print "mr: loading config $f\n" if $verbose;
707                 open($in, "<", $f) || die "mr: open $f: $!\n";
708         }
709         my @lines=<$in>;
710         close $in;
711
712         my $section;
713         my $line=0;
714         while (@lines) {
715                 $_=shift @lines;
716                 $line++;
717                 chomp;
718                 next if /^\s*\#/ || /^\s*$/;
719                 if (/^\[([^\]]*)\]\s*$/) {
720                         $section=$1;
721                 }
722                 elsif (/^(\w+)\s*=\s*(.*)/) {
723                         my $parameter=$1;
724                         my $value=$2;
725
726                         # continued value
727                         while (@lines && $lines[0]=~/^\s(.+)/) {
728                                 shift(@lines);
729                                 $line++;
730                                 $value.="\n$1";
731                                 chomp $value;
732                         }
733
734                         if (! defined $section) {
735                                 die "$f line $.: parameter ($parameter) not in section\n";
736                         }
737                         if ($section ne 'ALIAS' &&
738                             ! exists $config{$dir}{$section} &&
739                             exists $config{$dir}{DEFAULT}) {
740                                 # copy in defaults
741                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
742                         }
743                         if ($section eq 'ALIAS') {
744                                 $alias{$parameter}=$value;
745                         }
746                         elsif ($parameter eq 'lib') {
747                                 $config{$dir}{$section}{lib}.=$value."\n";
748                         }
749                         else {
750                                 $config{$dir}{$section}{$parameter}=$value;
751                                 $knownactions{$parameter}=1;
752                                 if ($parameter eq 'chain' &&
753                                     length $dir && $section ne "DEFAULT" &&
754                                     -e $dir.$section."/.mrconfig") {
755                                         my $ret=system($value);
756                                         if ($ret != 0) {
757                                                 if (($? & 127) == 2) {
758                                                         print STDERR "mr $action: chain test interrupted\n";
759                                                         exit 2;
760                                                 }
761                                                 elsif ($? & 127) {
762                                                         print STDERR "mr $action: chain test received signal ".($? & 127)."\n";
763                                                 }
764                                         }
765                                         else {
766                                                 push @toload, $dir.$section."/.mrconfig";
767                                         }
768                                 }
769                         }
770                 }
771                 else {
772                         die "$f line $line: parse error\n";
773                 }
774         }
775
776         foreach (@toload) {
777                 loadconfig($_);
778         }
779 } #}}}
780
781 sub modifyconfig { #{{{
782         my $f=shift;
783         # the section to modify or add
784         my $targetsection=shift;
785         # fields to change in the section
786         # To remove a field, set its value to "".
787         my %changefields=@_;
788
789         my @lines;
790         my @out;
791
792         if (-e $f) {
793                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
794                 @lines=<$in>;
795                 close $in;
796         }
797
798         my $formatfield=sub {
799                 my $field=shift;
800                 my @value=split(/\n/, shift);
801
802                 return "$field = ".shift(@value)."\n".
803                         join("", map { "\t$_\n" } @value);
804         };
805         my $addfields=sub {
806                 my @blanks;
807                 while ($out[$#out] =~ /^\s*$/) {
808                         unshift @blanks, pop @out;
809                 }
810                 foreach my $field (sort keys %changefields) {
811                         if (length $changefields{$field}) {
812                                 push @out, "$field = $changefields{$field}\n";
813                                 delete $changefields{$field};
814                         }
815                 }
816                 push @out, @blanks;
817         };
818
819         my $section;
820         while (@lines) {
821                 $_=shift(@lines);
822
823                 if (/^\s*\#/ || /^\s*$/) {
824                         push @out, $_;
825                 }
826                 elsif (/^\[([^\]]*)\]\s*$/) {
827                         if (defined $section && 
828                             $section eq $targetsection) {
829                                 $addfields->();
830                         }
831
832                         $section=$1;
833
834                         push @out, $_;
835                 }
836                 elsif (/^(\w+)\s*=\s(.*)/) {
837                         my $parameter=$1;
838                         my $value=$2;
839
840                         # continued value
841                         while (@lines && $lines[0]=~/^\s(.+)/) {
842                                 shift(@lines);
843                                 $value.="\n$1";
844                                 chomp $value;
845                         }
846
847                         if ($section eq $targetsection) {
848                                 if (exists $changefields{$parameter}) {
849                                         if (length $changefields{$parameter}) {
850                                                 $value=$changefields{$parameter};
851                                         }
852                                         delete $changefields{$parameter};
853                                 }
854                         }
855
856                         push @out, $formatfield->($parameter, $value);
857                 }
858         }
859
860         if (defined $section && 
861             $section eq $targetsection) {
862                 $addfields->();
863         }
864         elsif (%changefields) {
865                 push @out, "\n[$targetsection]\n";
866                 foreach my $field (sort keys %changefields) {
867                         if (length $changefields{$field}) {
868                                 push @out, $formatfield->($field, $changefields{$field});
869                         }
870                 }
871         }
872
873         open(my $out, ">", $f) || die "mr: write $f: $!\n";
874         print $out @out;
875         close $out;     
876 } #}}}
877
878 # Finally, some useful actions that mr knows about by default.
879 # These can be overridden in ~/.mrconfig.
880 #DATA{{{
881 __DATA__
882 [ALIAS]
883 co = checkout
884 ci = commit
885 ls = list
886
887 [DEFAULT]
888 lib =
889         error() {
890                 echo "mr: $@" >&2
891                 exit 1
892         }
893         hours_since() {
894                 for dir in .git .svn .bzr CVS .hg; do
895                         if [ -e "$MR_REPO/$dir" ]; then
896                                 flagfile="$MR_REPO/$dir/.mr_last$1"
897                                 break
898                         fi
899                 done
900                 if [ -z "$flagfile" ]; then
901                         error "cannot determine flag filename"
902                 fi
903                 perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
904                 touch "$flagfile"
905         }
906
907 update =
908         if [ -d "$MR_REPO"/.svn ]; then
909                 svn update "$@"
910         elif [ -d "$MR_REPO"/.git ]; then
911                 if [ -z "$@" ]; then
912                         git pull -t origin master
913                 else
914                         git pull "$@"
915                 fi
916         elif [ -d "$MR_REPO"/.bzr ]; then
917                 bzr merge "$@"
918         elif [ -d "$MR_REPO"/CVS ]; then
919                 cvs update "$@"
920         elif [ -d "$MR_REPO"/.hg ]; then
921                 hg pull "$@" && hg update "$@"
922         else
923                 error "unknown repo type"
924         fi
925 status =
926         if [ -d "$MR_REPO"/.svn ]; then
927                 svn status "$@"
928         elif [ -d "$MR_REPO"/.git ]; then
929                 git status "$@" || true
930         elif [ -d "$MR_REPO"/.bzr ]; then
931                 bzr status "$@"
932         elif [ -d "$MR_REPO"/CVS ]; then
933                 cvs status "$@"
934         elif [ -d "$MR_REPO"/.hg ]; then
935                 hg status "$@"
936         else
937                 error "unknown repo type"
938         fi
939 commit =
940         if [ -d "$MR_REPO"/.svn ]; then
941                 svn commit "$@"
942         elif [ -d "$MR_REPO"/.git ]; then
943                 git commit -a "$@" && git push --all
944         elif [ -d "$MR_REPO"/.bzr ]; then
945                 bzr commit "$@" && bzr push
946         elif [ -d "$MR_REPO"/CVS ]; then
947                 cvs commit "$@"
948         elif [ -d "$MR_REPO"/.hg ]; then
949                 hg commit -m "$@" && hg push
950         else
951                 error "unknown repo type"
952         fi
953 diff =
954         if [ -d "$MR_REPO"/.svn ]; then
955                 svn diff "$@"
956         elif [ -d "$MR_REPO"/.git ]; then
957                 git diff "$@"
958         elif [ -d "$MR_REPO"/.bzr ]; then
959                 bzr diff "$@"
960         elif [ -d "$MR_REPO"/CVS ]; then
961                 cvs diff "$@"
962         elif [ -d "$MR_REPO"/.hg ]; then
963                 hg diff "$@"
964         else
965                 error "unknown repo type"
966         fi
967 log =
968         if [ -d "$MR_REPO"/.svn ]; then
969                 svn log"$@"
970         elif [ -d "$MR_REPO"/.git ]; then
971                 git log "$@"
972         elif [ -d "$MR_REPO"/.bzr ]; then
973                 bzr log "$@"
974         elif [ -d "$MR_REPO"/CVS ]; then
975                 cvs log "$@"
976         elif [ -d "$MR_REPO"/.hg ]; then
977                 hg log "$@"
978         else
979                 error "unknown repo type"
980         fi
981 register =
982         if [ -n "$1" ]; then
983                 cd "$1"
984         fi
985         basedir="$(basename $(pwd))"
986         if [ -d .svn ]; then
987                 url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
988                 if [ -z "$url" ]; then
989                         error "cannot determine svn url"
990                 fi
991                 echo "Registering svn url: $url in $MR_CONFIG"
992                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="svn co $url $basedir"
993         elif [ -d .git ]; then
994                 url=$(LANG=C git-config --get remote.origin.url)
995                 if [ -z "$url" ]; then
996                         error "cannot determine git url"
997                 fi
998                 echo "Registering git url: $url in $MR_CONFIG"
999                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="git clone $url $basedir"
1000         elif [ -d .bzr ]; then
1001                 url=$(cat .bzr/branch/parent)
1002                 if [ -z "$url" ]; then
1003                         error "cannot determine bzr url"
1004                 fi
1005                 echo "Registering bzr url: $url in $MR_CONFIG"
1006                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
1007         elif [ -d CVS ]; then
1008                 repo=$(cat CVS/Repository)
1009                 root=$(cat CVS/Root)
1010                 if [ -z "$root" ]; then
1011                         error "cannot determine cvs root"
1012                 fi
1013                 echo "Registering cvs repository $repo at root $root"
1014                 mr -c "$MR_CONFIG" config "$(pwd)" \
1015                         checkout="cvs -d '$root' co -d $basedir $repo"
1016         elif [ -d .hg ]; then
1017                 url=$(hg showconfig paths.default)
1018                 echo "Registering mercurial repo url: $url in $MR_CONFIG"
1019                 mr -c "$MR_CONFIG" config "$(pwd)" \
1020                         checkout="hg clone $url $basedir"
1021         else
1022                 error "unable to register this repo type"
1023         fi
1024 help =
1025         if [ ! -e "$MR_PATH" ]; then
1026                 error "cannot find program path"
1027         fi
1028         (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
1029 list = true
1030 config = 
1031
1032 ed = echo "A horse is a horse, of course, of course.."
1033 T = echo "I pity the fool."
1034 right = echo "Not found."
1035 #}}}