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

* -c was broken, fix. Closes: #449539
[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,
34 bzr and darcs repositories, and support for other revision control systems can
35 easily be 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. (For the "register" action, "MR_REPO" is instead set to the 
213 basename of the directory that should be created when checking the
214 repository out.)
215
216 The "MR_CONFIG" environment variable is set to the .mrconfig file
217 that defines the repo being acted on, or, if the repo is not yet in a config
218 file, the .mrconfig file that should be modified to register the repo.
219
220 A few parameters have special meanings:
221
222 =over 4
223
224 =item skip
225
226 If the "skip" parameter is set and its command returns true, then B<mr>
227 will skip acting on that repository. The command is passed the action
228 name in $1.
229
230 Here are two examples. The first skips the repo unless
231 mr is run by joey. The second uses the hours_since function
232 (included in mr's built-in library) to skip updating the repo unless it's
233 been at least 12 hours since the last update.
234
235   skip = test $(whoami) != joey
236   skip = [ "$1" = update ] && ! hours_since "$1" 12
237
238 =item order
239
240 The "order" parameter can be used to override the default ordering of
241 repositories. The default order value is 10. Use smaller values to make
242 repositories be processed earlier, and larger values to make repositories
243 be processed later.
244
245 Note that if a repository is located in a subdirectory of another
246 repository, ordering it to be processed earlier is not recommended.
247
248 =item chain
249
250 If the "chain" parameter is set and its command returns true, then B<mr>
251 will try to load a .mrconfig file from the root of the repository. (You
252 should avoid chaining from repositories with untrusted committers.)
253
254 =item include
255
256 If the "include" parameter is set, its command is ran, and should output
257 additional mrconfig file content. The content is included as if it were
258 part of the including file.
259
260 Unlike all other parameters, this parameter does not need to be placed
261 within a section.
262
263 =item lib
264
265 The "lib" parameter can specify some shell code that will be run before each
266 command, this can be a useful way to define shell functions for other commands
267 to use.
268
269 =back
270
271 When looking for a command to run for a given action, mr first looks for
272 a parameter with the same name as the action. If that is not found, it
273 looks for a parameter named "rcs_action" (substituting in the name of the
274 revision control system and the action). The name of the revision control
275 system is itself determined by running each defined "rcs_test" action,
276 until one succeeds.
277
278 Internally, mr has settings for "git_update", "svn_update", etc. To change
279 the action that is performed for a given revision control system, you can
280 override these rcs specific actions. To add a new revision control system,
281 you can just add rcs specific actions for it.
282
283 =head1 AUTHOR
284
285 Copyright 2007 Joey Hess <joey@kitenet.net>
286
287 Licensed under the GNU GPL version 2 or higher.
288
289 http://kitenet.net/~joey/code/mr/
290
291 =cut
292
293 #}}}
294
295 use warnings;
296 use strict;
297 use Getopt::Long;
298 use Cwd qw(getcwd abs_path);
299
300 # things that can happen when mr runs a command
301 use constant {
302         OK => 0,
303         FAILED => 1,
304         SKIPPED => 2,
305         ABORT => 3,
306 };
307
308 # configurables
309 my $config_overridden=0;
310 my $verbose=0;
311 my $stats=0;
312 my $no_recurse=0;
313 my $no_chdir=0;
314 my $jobs=1;
315 my $directory=getcwd();
316 $ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
317
318 # globals :-(
319 my %config;
320 my %configfiles;
321 my %knownactions;
322 my %alias;
323 my (@ok, @failed, @skipped);
324
325 main();
326
327 sub rcs_test { #{{{
328         my ($action, $dir, $topdir, $subdir) = @_;
329
330         my $test="set -e\n";
331         foreach my $rcs_test (
332                         sort {
333                                 length $a <=> length $b 
334                                           ||
335                                        $a cmp $b
336                         } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
337                 my ($rcs)=$rcs_test=~/(.*)_test/;
338                 $test="my_$rcs_test() {\n$config{$topdir}{$subdir}{$rcs_test}\n}\n".$test;
339                 $test.="if my_$rcs_test; then echo $rcs; fi\n";
340         }
341         $test=$config{$topdir}{$subdir}{lib}."\n".$test
342                 if exists $config{$topdir}{$subdir}{lib};
343         
344         print "mr $action: running rcs test >>$test<<\n" if $verbose;
345         my $rcs=`$test`;
346         chomp $rcs;
347         if ($rcs=~/\n/s) {
348                 $rcs=~s/\n/, /g;
349                 print STDERR "mr $action: found multiple possible repository types ($rcs) for $topdir$subdir\n";
350                 return undef;
351         }
352         if (! length $rcs) {
353                 return undef;
354         }
355         else {
356                 return $rcs;
357         }
358 } #}}}
359         
360 sub findcommand { #{{{
361         my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
362         
363         if (exists $config{$topdir}{$subdir}{$action}) {
364                 return $config{$topdir}{$subdir}{$action};
365         }
366
367         if ($is_checkout) {
368                 return undef;
369         }
370
371         my $rcs=rcs_test(@_);
372
373         if (defined $rcs && 
374             exists $config{$topdir}{$subdir}{$rcs."_".$action}) {
375                 return $config{$topdir}{$subdir}{$rcs."_".$action};
376         }
377         else {
378                 return undef;
379         }
380 } #}}}
381
382 sub action { #{{{
383         my ($action, $dir, $topdir, $subdir) = @_;
384
385         $ENV{MR_CONFIG}=$configfiles{$topdir};
386         my $lib=exists $config{$topdir}{$subdir}{lib} ?
387                        $config{$topdir}{$subdir}{lib}."\n" : "";
388         my $is_checkout=($action eq 'checkout');
389
390         if ($is_checkout) {
391                 if (-d $dir) {
392                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
393                         return SKIPPED;
394                 }
395
396                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
397         }
398         elsif ($action =~ /update/) {
399                 if (! -d $dir) {
400                         return action("checkout", $dir, $topdir, $subdir);
401                 }
402         }
403
404         $ENV{MR_REPO}=$dir;
405
406         my $skiptest=findcommand("skip", $dir, $topdir, $subdir, $is_checkout);
407         my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
408
409         if (defined $skiptest) {
410                 my $test="set -e;".$lib.
411                         "my_action(){ $skiptest\n }; my_action '$action'";
412                 print "mr $action: running skip test >>$test<<\n" if $verbose;
413                 my $ret=system($test);
414                 if ($ret != 0) {
415                         if (($? & 127) == 2) {
416                                 print STDERR "mr $action: interrupted\n";
417                                 return ABORT;
418                         }
419                         elsif ($? & 127) {
420                                 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
421                                 return ABORT;
422                         }
423                 }
424                 if ($ret >> 8 == 0) {
425                         print "mr $action: $dir skipped per config file\n" if $verbose;
426                         return SKIPPED;
427                 }
428         }
429
430         if ($is_checkout && ! -d $dir) {
431                 print "mr $action: creating parent directory $dir\n" if $verbose;
432                 system("mkdir", "-p", $dir);
433         }
434
435         if (! $no_chdir && ! chdir($dir)) {
436                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
437                 return FAILED;
438         }
439         elsif (! defined $command) {
440                 my $rcs=rcs_test(@_);
441                 if (! defined $rcs) {
442                         print STDERR "mr $action: unknown repository type and no defined $action command for $topdir$subdir\n";
443                         return FAILED;
444                 }
445                 else {
446                         print STDERR "mr $action: no defined $action command for $rcs repository $topdir$subdir, skipping\n";
447                         return SKIPPED;
448                 }
449         }
450         else {
451                 if (! $no_chdir) {
452                         print "mr $action: $topdir$subdir\n";
453                 }
454                 else {
455                         my $s=$directory;
456                         $s=~s/^\Q$topdir$subdir\E\/?//;
457                         print "mr $action: $topdir$subdir (in subdir $s)\n";
458                 }
459                 $command="set -e; ".$lib.
460                         "my_action(){ $command\n }; my_action ".
461                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
462                 print "mr $action: running >>$command<<\n" if $verbose;
463                 my $ret=system($command);
464                 if ($ret != 0) {
465                         if (($? & 127) == 2) {
466                                 print STDERR "mr $action: interrupted\n";
467                                 return ABORT;
468                         }
469                         elsif ($? & 127) {
470                                 print STDERR "mr $action: received signal ".($? & 127)."\n";
471                                 return ABORT;
472                         }
473                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
474                         if ($ret >> 8 != 0) {
475                                 print STDERR "mr $action: command failed\n";
476                         }
477                         elsif ($ret != 0) {
478                                 print STDERR "mr $action: command died ($ret)\n";
479                         }
480                         return FAILED;
481                 }
482                 else {
483                         if ($action eq 'checkout' && ! -d $dir) {
484                                 print STDERR "mr $action: $dir missing after checkout\n";;
485                                 return FAILED;
486                         }
487
488                         return OK;
489                 }
490         }
491 } #}}}
492
493 # run actions on multiple repos, in parallel
494 sub mrs { #{{{
495         my $action=shift;
496         my @repos=@_;
497
498         $| = 1;
499         my @active;
500         my @fhs;
501         my @out;
502         my $running=0;
503         while (@fhs or @repos) {
504                 while ($running < $jobs && @repos) {
505                         $running++;
506                         my $repo = shift @repos;
507                         pipe(my $outfh, CHILD_STDOUT);
508                         pipe(my $errfh, CHILD_STDERR);
509                         my $pid;
510                         unless ($pid = fork) {
511                                 die "mr $action: cannot fork: $!" unless defined $pid;
512                                 open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
513                                 open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
514                                 close CHILD_STDOUT;
515                                 close CHILD_STDERR;
516                                 close $outfh;
517                                 close $errfh;
518                                 exit action($action, @$repo);
519                         }
520                         close CHILD_STDOUT;
521                         close CHILD_STDERR;
522                         push @active, [$pid, $repo];
523                         push @fhs, [$outfh, $errfh];
524                         push @out, ['',     ''];
525                 }
526                 my ($rin, $rout) = ('','');
527                 my $nfound;
528                 foreach my $fh (@fhs) {
529                         next unless defined $fh;
530                         vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
531                         vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
532                 }
533                 $nfound = select($rout=$rin, undef, undef, 1);
534                 foreach my $channel (0, 1) {
535                         foreach my $i (0..$#fhs) {
536                                 next unless defined $fhs[$i];
537                                 my $fh = $fhs[$i][$channel];
538                                 next unless defined $fh;
539                                 if (vec($rout, fileno($fh), 1) == 1) {
540                                         my $r = '';
541                                         if (sysread($fh, $r, 1024) == 0) {
542                                                 close($fh);
543                                                 $fhs[$i][$channel] = undef;
544                                                 if (! defined $fhs[$i][0] &&
545                                                     ! defined $fhs[$i][1]) {
546                                                         waitpid($active[$i][0], 0);
547                                                         print STDOUT $out[$i][0];
548                                                         print STDERR $out[$i][1];
549                                                         record($active[$i][1], $? >> 8);
550                                                         splice(@fhs, $i, 1);
551                                                         splice(@active, $i, 1);
552                                                         splice(@out, $i, 1);
553                                                         $running--;
554                                                 }
555                                         }
556                                         $out[$i][$channel] .= $r;
557                                 }
558                         }
559                 }
560         }
561 } #}}}
562
563 sub record { #{{{
564         my $dir=shift()->[0];
565         my $ret=shift;
566
567         if ($ret == OK) {
568                 push @ok, $dir;
569                 print "\n";
570         }
571         elsif ($ret == FAILED) {
572                 push @failed, $dir;
573                 print "\n";
574         }
575         elsif ($ret == SKIPPED) {
576                 push @skipped, $dir;
577         }
578         elsif ($ret == ABORT) {
579                 exit 1;
580         }
581         else {
582                 die "unknown exit status $ret";
583         }
584 } #}}}
585
586 sub showstats { #{{{
587         my $action=shift;
588         if (! @ok && ! @failed && ! @skipped) {
589                 die "mr $action: no repositories found to work on\n";
590         }
591         print "mr $action: finished (".join("; ",
592                 showstat($#ok+1, "ok", "ok"),
593                 showstat($#failed+1, "failed", "failed"),
594                 showstat($#skipped+1, "skipped", "skipped"),
595         ).")\n";
596         if ($stats) {
597                 if (@skipped) {
598                         print "mr $action: (skipped: ".join(" ", @skipped).")\n";
599                 }
600                 if (@failed) {
601                         print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
602                 }
603         }
604 } #}}}
605
606 sub showstat { #{{{
607         my $count=shift;
608         my $singular=shift;
609         my $plural=shift;
610         if ($count) {
611                 return "$count ".($count > 1 ? $plural : $singular);
612         }
613         return;
614 } #}}}
615
616 # an ordered list of repos
617 sub repolist { #{{{
618         my @list;
619         foreach my $topdir (sort keys %config) {
620                 foreach my $subdir (sort keys %{$config{$topdir}}) {
621                         push @list, {
622                                 topdir => $topdir,
623                                 subdir => $subdir,
624                                 order => $config{$topdir}{$subdir}{order},
625                         };
626                 }
627         }
628         return sort {
629                 $a->{order}  <=> $b->{order}
630                              ||
631                 $a->{topdir} cmp $b->{topdir}
632                              ||
633                 $a->{subdir} cmp $b->{subdir}
634         } @list;
635 } #}}}
636
637 # figure out which repos to act on
638 sub selectrepos { #{{{
639         my @repos;
640         foreach my $repo (repolist()) {
641                 my $topdir=$repo->{topdir};
642                 my $subdir=$repo->{subdir};
643
644                 next if $subdir eq 'DEFAULT';
645                 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
646                 my $d=$directory;
647                 $dir.="/" unless $dir=~/\/$/;
648                 $d.="/" unless $d=~/\/$/;
649                 next if $no_recurse && $d ne $dir;
650                 next if $dir ne $d && $dir !~ /^\Q$d\E/;
651                 push @repos, [$dir, $topdir, $subdir];
652         }
653         if (! @repos) {
654                 # fallback to find a leaf repo
655                 foreach my $repo (reverse repolist()) {
656                         my $topdir=$repo->{topdir};
657                         my $subdir=$repo->{subdir};
658                         
659                         next if $subdir eq 'DEFAULT';
660                         my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
661                         my $d=$directory;
662                         $dir.="/" unless $dir=~/\/$/;
663                         $d.="/" unless $d=~/\/$/;
664                         if ($d=~/^\Q$dir\E/) {
665                                 push @repos, [$dir, $topdir, $subdir];
666                                 last;
667                         }
668                 }
669                 $no_chdir=1;
670         }
671         return @repos;
672 } #}}}
673
674 my %loaded;
675 sub loadconfig { #{{{
676         my $f=shift;
677
678         my @toload;
679
680         my $in;
681         my $dir;
682         if (ref $f eq 'GLOB') {
683                 $dir="";
684                 $in=$f; 
685         }
686         else {
687                 if (! -e $f) {
688                         return;
689                 }
690
691                 my $absf=abs_path($f);
692                 if ($loaded{$absf}) {
693                         return;
694                 }
695                 $loaded{$absf}=1;
696
697                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
698                 if (! defined $dir) {
699                         $dir=".";
700                 }
701                 $dir=abs_path($dir)."/";
702                 
703                 if (! exists $configfiles{$dir}) {
704                         $configfiles{$dir}=$f;
705                 }
706
707                 # copy in defaults from first parent
708                 my $parent=$dir;
709                 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
710                         if ($parent eq '/') {
711                                 $parent="";
712                         }
713                         if (exists $config{$parent} &&
714                             exists $config{$parent}{DEFAULT}) {
715                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
716                                 last;
717                         }
718                 }
719                 
720                 print "mr: loading config $f\n" if $verbose;
721                 open($in, "<", $f) || die "mr: open $f: $!\n";
722         }
723         my @lines=<$in>;
724         close $in;
725
726         my $section;
727         my $line=0;
728         while (@lines) {
729                 $_=shift @lines;
730                 $line++;
731                 chomp;
732                 next if /^\s*\#/ || /^\s*$/;
733                 if (/^\[([^\]]*)\]\s*$/) {
734                         $section=$1;
735                 }
736                 elsif (/^(\w+)\s*=\s*(.*)/) {
737                         my $parameter=$1;
738                         my $value=$2;
739
740                         # continued value
741                         while (@lines && $lines[0]=~/^\s(.+)/) {
742                                 shift(@lines);
743                                 $line++;
744                                 $value.="\n$1";
745                                 chomp $value;
746                         }
747
748                         if ($parameter eq "include") {
749                                 print "mr: including output of \"$value\"\n" if $verbose;
750                                 unshift @lines, `$value`;
751                                 next;
752                         }
753
754                         if (! defined $section) {
755                                 die "$f line $.: parameter ($parameter) not in section\n";
756                         }
757                         if ($section ne 'ALIAS' &&
758                             ! exists $config{$dir}{$section} &&
759                             exists $config{$dir}{DEFAULT}) {
760                                 # copy in defaults
761                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
762                         }
763                         if ($section eq 'ALIAS') {
764                                 $alias{$parameter}=$value;
765                         }
766                         elsif ($parameter eq 'lib') {
767                                 $config{$dir}{$section}{lib}.=$value."\n";
768                         }
769                         else {
770                                 $config{$dir}{$section}{$parameter}=$value;
771                                 if ($parameter =~ /.*_(.*)/) {
772                                         $knownactions{$1}=1;
773                                 }
774                                 else {
775                                         $knownactions{$parameter}=1;
776                                 }
777                                 if ($parameter eq 'chain' &&
778                                     length $dir && $section ne "DEFAULT" &&
779                                     -e $dir.$section."/.mrconfig") {
780                                         my $ret=system($value);
781                                         if ($ret != 0) {
782                                                 if (($? & 127) == 2) {
783                                                         print STDERR "mr: chain test interrupted\n";
784                                                         exit 2;
785                                                 }
786                                                 elsif ($? & 127) {
787                                                         print STDERR "mr: chain test received signal ".($? & 127)."\n";
788                                                 }
789                                         }
790                                         else {
791                                                 push @toload, $dir.$section."/.mrconfig";
792                                         }
793                                 }
794                         }
795                 }
796                 else {
797                         die "$f line $line: parse error\n";
798                 }
799         }
800
801         foreach (@toload) {
802                 loadconfig($_);
803         }
804 } #}}}
805
806 sub modifyconfig { #{{{
807         my $f=shift;
808         # the section to modify or add
809         my $targetsection=shift;
810         # fields to change in the section
811         # To remove a field, set its value to "".
812         my %changefields=@_;
813
814         my @lines;
815         my @out;
816
817         if (-e $f) {
818                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
819                 @lines=<$in>;
820                 close $in;
821         }
822
823         my $formatfield=sub {
824                 my $field=shift;
825                 my @value=split(/\n/, shift);
826
827                 return "$field = ".shift(@value)."\n".
828                         join("", map { "\t$_\n" } @value);
829         };
830         my $addfields=sub {
831                 my @blanks;
832                 while ($out[$#out] =~ /^\s*$/) {
833                         unshift @blanks, pop @out;
834                 }
835                 foreach my $field (sort keys %changefields) {
836                         if (length $changefields{$field}) {
837                                 push @out, "$field = $changefields{$field}\n";
838                                 delete $changefields{$field};
839                         }
840                 }
841                 push @out, @blanks;
842         };
843
844         my $section;
845         while (@lines) {
846                 $_=shift(@lines);
847
848                 if (/^\s*\#/ || /^\s*$/) {
849                         push @out, $_;
850                 }
851                 elsif (/^\[([^\]]*)\]\s*$/) {
852                         if (defined $section && 
853                             $section eq $targetsection) {
854                                 $addfields->();
855                         }
856
857                         $section=$1;
858
859                         push @out, $_;
860                 }
861                 elsif (/^(\w+)\s*=\s(.*)/) {
862                         my $parameter=$1;
863                         my $value=$2;
864
865                         # continued value
866                         while (@lines && $lines[0]=~/^\s(.+)/) {
867                                 shift(@lines);
868                                 $value.="\n$1";
869                                 chomp $value;
870                         }
871
872                         if ($section eq $targetsection) {
873                                 if (exists $changefields{$parameter}) {
874                                         if (length $changefields{$parameter}) {
875                                                 $value=$changefields{$parameter};
876                                         }
877                                         delete $changefields{$parameter};
878                                 }
879                         }
880
881                         push @out, $formatfield->($parameter, $value);
882                 }
883         }
884
885         if (defined $section && 
886             $section eq $targetsection) {
887                 $addfields->();
888         }
889         elsif (%changefields) {
890                 push @out, "\n[$targetsection]\n";
891                 foreach my $field (sort keys %changefields) {
892                         if (length $changefields{$field}) {
893                                 push @out, $formatfield->($field, $changefields{$field});
894                         }
895                 }
896         }
897
898         open(my $out, ">", $f) || die "mr: write $f: $!\n";
899         print $out @out;
900         close $out;     
901 } #}}}
902
903 sub dispatch { #{{{
904         my $action=shift;
905
906         # actions that do not operate on all repos
907         if ($action eq 'help') {
908                 help(@ARGV);
909         }
910         elsif ($action eq 'config') {
911                 config(@ARGV);
912         }
913         elsif ($action eq 'register') {
914                 register(@ARGV);
915         }
916
917         if ($jobs > 1) {
918                 mrs($action, selectrepos());
919         }
920         else {
921                 foreach my $repo (selectrepos()) {
922                         record($repo, action($action, @$repo));
923                 }
924         }
925 } #}}}
926
927 sub help { #{{{
928         exec($config{''}{DEFAULT}{help}) || die "exec: $!";
929 } #}}}
930         
931 sub config { #{{{
932         if (@_ < 2) {
933                 die "mr config: not enough parameters\n";
934         }
935         my $section=shift;
936         if ($section=~/^\//) {
937                 # try to convert to a path relative to the config file
938                 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
939                 $dir=abs_path($dir);
940                 $dir.="/" unless $dir=~/\/$/;
941                 if ($section=~/^\Q$dir\E(.*)/) {
942                         $section=$1;
943                 }
944         }
945         my %changefields;
946         foreach (@_) {
947                 if (/^([^=]+)=(.*)$/) {
948                         $changefields{$1}=$2;
949                 }
950                 else {
951                         my $found=0;
952                         foreach my $topdir (sort keys %config) {
953                                 if (exists $config{$topdir}{$section} &&
954                                     exists $config{$topdir}{$section}{$_}) {
955                                         print $config{$topdir}{$section}{$_}."\n";
956                                         $found=1;
957                                         last if $section eq 'DEFAULT';
958                                 }
959                         }
960                         if (! $found) {
961                                 die "mr config: $section $_ not set\n";
962                         }
963                 }
964         }
965         modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
966         exit 0;
967 } #}}}
968
969 sub register { #{{{
970         if (! $config_overridden) {
971                 # Find the closest known mrconfig file to the current
972                 # directory.
973                 $directory.="/" unless $directory=~/\/$/;
974                 my $foundconfig=0;
975                 foreach my $topdir (reverse sort keys %config) {
976                         next unless length $topdir;
977                         if ($directory=~/^\Q$topdir\E/) {
978                                 $ENV{MR_CONFIG}=$configfiles{$topdir};
979                                 $directory=$topdir;
980                                 $foundconfig=1;
981                                 last;
982                         }
983                 }
984                 if (! $foundconfig) {
985                         $directory=""; # no config file, use builtin
986                 }
987         }
988         if (@ARGV) {
989                 my $subdir=shift @ARGV;
990                 if (! chdir($subdir)) {
991                         print STDERR "mr register: failed to chdir to $subdir: $!\n";
992                 }
993         }
994
995         $ENV{MR_REPO}=getcwd();
996         my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
997         if (! defined $command) {
998                 die "mr register: unknown repository type\n";
999         }
1000
1001         $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
1002         $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n".
1003                 "my_action(){ $command\n }; my_action ".
1004                 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
1005         print "mr register: running >>$command<<\n" if $verbose;
1006         exec($command) || die "exec: $!";
1007 } #}}}
1008
1009 # alias expansion and command stemming
1010 sub expandaction { #{{{
1011         my $action=shift;
1012         if (exists $alias{$action}) {
1013                 $action=$alias{$action};
1014         }
1015         if (! exists $knownactions{$action}) {
1016                 my @matches = grep { /^\Q$action\E/ }
1017                         keys %knownactions, keys %alias;
1018                 if (@matches == 1) {
1019                         $action=$matches[0];
1020                 }
1021                 elsif (@matches == 0) {
1022                         die "mr: unknown action \"$action\" (known actions: ".
1023                                 join(", ", sort keys %knownactions).")\n";
1024                 }
1025                 else {
1026                         die "mr: ambiguous action \"$action\" (matches: ".
1027                                 join(", ", @matches).")\n";
1028                 }
1029         }
1030         return $action;
1031 } #}}}
1032
1033 sub getopts { #{{{
1034         Getopt::Long::Configure("bundling", "no_permute");
1035         my $result=GetOptions(
1036                 "d|directory=s" => sub { $directory=abs_path($_[1]) },
1037                 "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
1038                 "v|verbose" => \$verbose,
1039                 "s|stats" => \$stats,
1040                 "n|no-recurse" => \$no_recurse,
1041                 "j|jobs=i" => \$jobs,
1042         );
1043         if (! $result || @ARGV < 1) {
1044                 die("Usage: mr [-d directory] action [params ...]\n".
1045                     "(Use mr help for man page.)\n");
1046         }
1047 } #}}}
1048
1049 sub init { #{{{
1050         $SIG{INT}=sub {
1051                 print STDERR "mr: interrupted\n";
1052                 exit 2;
1053         };
1054         
1055         # This can happen if it's run in a directory that was removed
1056         # or other strangeness.
1057         if (! defined $directory) {
1058                 die("mr: failed to determine working directory\n");
1059         }
1060         # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
1061         # the config file might be a symlink to elsewhere, and the directory it's
1062         # in is significant.
1063         if ($ENV{MR_CONFIG} !~ /^\//) {
1064                 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
1065         }
1066         # Try to set MR_PATH to the path to the program.
1067         eval {
1068                 use FindBin qw($Bin $Script);
1069                 $ENV{MR_PATH}=$Bin."/".$Script;
1070         };
1071 } #}}}
1072
1073 sub main { #{{{
1074         getopts();
1075         init();
1076         loadconfig(\*DATA);
1077         loadconfig($ENV{MR_CONFIG});
1078         #use Data::Dumper; print Dumper(\%config);
1079
1080         my $action=expandaction(shift @ARGV);
1081         dispatch($action);
1082         showstats($action);
1083
1084         if (@failed) {
1085                 exit 1;
1086         }
1087         elsif (! @ok && @skipped) {
1088                 exit 1;
1089         }
1090         else {
1091                 exit 0;
1092         }
1093 } #}}}
1094
1095 # Finally, some useful actions that mr knows about by default.
1096 # These can be overridden in ~/.mrconfig.
1097 #DATA{{{
1098 __DATA__
1099 [ALIAS]
1100 co = checkout
1101 ci = commit
1102 ls = list
1103
1104 [DEFAULT]
1105 order = 10
1106 lib =
1107         error() {
1108                 echo "mr: $@" >&2
1109                 exit 1
1110         }
1111         warning() {
1112                 echo "mr (warning): $@" >&2
1113         }
1114         info() {
1115                 echo "mr: $@" >&2
1116         }
1117         hours_since() {
1118                 if [ -z "$1" ] || [ -z "$2" ]; then
1119                         error "mr: usage: hours_since action num"
1120                 fi
1121                 for dir in .git .svn .bzr CVS .hg _darcs; do
1122                         if [ -e "$MR_REPO/$dir" ]; then
1123                                 flagfile="$MR_REPO/$dir/.mr_last$1"
1124                                 break
1125                         fi
1126                 done
1127                 if [ -z "$flagfile" ]; then
1128                         error "cannot determine flag filename"
1129                 fi
1130                 delta=$(perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile")
1131                 if [ "$delta" -lt "$2" ]; then
1132                         exit 0
1133                 else
1134                         touch "$flagfile"
1135                         exit 1
1136                 fi
1137         }
1138
1139 svn_test = test -d "$MR_REPO"/.svn
1140 git_test = test -d "$MR_REPO"/.git
1141 bzr_test = test -d "$MR_REPO"/.bzr
1142 cvs_test = test -d "$MR_REPO"/CVS
1143 hg_test  = test -d "$MR_REPO"/.hg
1144 darcs_test = test -d "$MR_REPO"/_darcs
1145 git_bare_test =
1146         test -d "$MR_REPO"/refs/heads && test -d "$MR_REPO"/refs/tags &&
1147         test -d "$MR_REPO"/objects && test -f "$MR_REPO"/config &&
1148         test "$(GIT_CONFIG="$MR_REPO"/config git config --get core.bare)" = true
1149
1150 svn_update = svn update "$@"
1151 git_update = if [ "$@" ]; then git pull "$@"; else git pull -t origin master; fi
1152 bzr_update = bzr merge "$@"
1153 cvs_update = cvs update "$@"
1154 hg_update  = hg pull "$@" && hg update "$@"
1155 darcs_update = darcs pull -a "$@"
1156
1157 svn_status = svn status "$@"
1158 git_status = git status "$@" || true
1159 bzr_status = bzr status "$@"
1160 cvs_status = cvs status "$@"
1161 hg_status  = hg status "$@"
1162 darcs_status = darcs whatsnew -ls "$@"
1163
1164 svn_commit = svn commit "$@"
1165 git_commit = git commit -a "$@" && git push --all
1166 bzr_commit = bzr commit "$@" && bzr push
1167 cvs_commit = cvs commit "$@"
1168 hg_commit  = hg commit -m "$@" && hg push
1169 darcs_commit = darcs commit -a -m "$@" && darcs push -a
1170
1171 svn_diff = svn diff "$@"
1172 git_diff = git diff "$@"
1173 bzr_diff = bzr diff "$@"
1174 cvs_diff = cvs diff "$@"
1175 hg_diff  = hg diff "$@"
1176 darcs_diff = darcs diff "$@"
1177
1178 svn_log = svn log "$@"
1179 git_log = git log "$@"
1180 bzr_log = bzr log "$@"
1181 cvs_log = cvs log "$@"
1182 hg_log  = hg log "$@"
1183 darcs_log = darcs changes "$@"
1184 git_bare_log = git log "$@"
1185
1186 svn_register =
1187         url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
1188         if [ -z "$url" ]; then
1189                 error "cannot determine svn url"
1190         fi
1191         echo "Registering svn url: $url in $MR_CONFIG"
1192         mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
1193 git_register = 
1194         url="$(LANG=C git config --get remote.origin.url)" || true
1195         if [ -z "$url" ]; then
1196                 error "cannot determine git url"
1197         fi
1198         echo "Registering git url: $url in $MR_CONFIG"
1199         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
1200 bzr_register =
1201         url=$(cat .bzr/branch/parent)
1202         if [ -z "$url" ]; then
1203                 error "cannot determine bzr url"
1204         fi
1205         echo "Registering bzr url: $url in $MR_CONFIG"
1206         mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr clone '$url' '$MR_REPO'"
1207 cvs_register =
1208         repo=$(cat CVS/Repository)
1209         root=$(cat CVS/Root)
1210         if [ -z "$root" ]; then
1211                 error "cannot determine cvs root"
1212                 fi
1213         echo "Registering cvs repository $repo at root $root"
1214         mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
1215 hg_register = 
1216         url=$(hg showconfig paths.default)
1217         echo "Registering mercurial repo url: $url in $MR_CONFIG"
1218         mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
1219 darcs_register = 
1220         url=$(cat _darcs/prefs/defaultrepo)
1221         echo "Registering darcs repository $url in $MR_CONFIG"
1222         mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url'p '$MR_REPO'"
1223 git_bare_register = 
1224         url="$(LANG=C GIT_CONFIG=config git config --get remote.origin.url)" || true
1225         if [ -z "$url" ]; then
1226                 error "cannot determine git url"
1227         fi
1228         echo "Registering git url: $url in $MR_CONFIG"
1229         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
1230
1231 help =
1232         if [ ! -e "$MR_PATH" ]; then
1233                 error "cannot find program path"
1234         fi
1235         (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
1236 list = true
1237 config = 
1238
1239 ed = echo "A horse is a horse, of course, of course.."
1240 T = echo "I pity the fool."
1241 right = echo "Not found."
1242 #}}}
1243
1244 # vim:sw=8:sts=0:ts=8:noet