]> git.madduck.net Git - code/myrepos.git/blob - mr

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Add an offline mode
[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] record [-m "message"]
20
21 B<mr> [options] diff
22
23 B<mr> [options] log
24
25 B<mr> [options] register [repository]
26
27 B<mr> [options] config section ["parameter=[value]" ...]
28
29 B<mr> [options] action [params ...]
30
31 B<mr> [options] [online|offline]
32
33 B<mr> [options] remember action [params ...]
34
35 =head1 DESCRIPTION
36
37 B<mr> is a Multiple Repository management tool. It can checkout, update, or
38 perform other actions on a set of repositories as if they were one combined
39 repository. It supports any combination of subversion, git, cvs, mecurial,
40 bzr and darcs repositories, and support for other revision control systems can
41 easily be added.
42
43 B<mr> cds into and operates on all registered repositories at or below your
44 working directory. Or, if you are in a subdirectory of a repository that
45 contains no other registered repositories, it will stay in that directory,
46 and work on only that repository,
47
48 These predefined commands should be fairly familiar to users of any revision
49 control system:
50
51 =over 4
52
53 =item checkout (or co)
54
55 Checks out any repositories that are not already checked out.
56
57 =item update
58
59 Updates each repository from its configured remote repository.
60
61 If a repository isn't checked out yet, it will first check it out.
62
63 =item status
64
65 Displays a status report for each repository, showing what
66 uncommitted changes are present in the repository.
67
68 =item commit (or ci)
69
70 Commits changes to each repository. (By default, changes are pushed to the
71 remote repository too, when using distributed systems like git. If you
72 don't like this default, you can change it in your .mrconfig, or use record
73 instead.)
74
75 The optional -m parameter allows specifying a commit message.
76
77 =item record
78
79 Records changes to the local repository, but does not push them to the
80 remote repository. Only supported for distributed revision control systems.
81
82 The optional -m parameter allows specifying a commit message.
83
84 =item diff
85
86 Show a diff of uncommitted changes.
87
88 =item log
89
90 Show the commit log.
91
92 =back
93
94 These commands are also available:
95
96 =over 4
97
98 =item list (or ls)
99
100 List the repositories that mr will act on.
101
102 =item register
103
104 Register an existing repository in a mrconfig file. By default, the
105 repository in the current directory is registered, or you can specify a
106 directory to register.
107
108 The mrconfig file that is modified is chosen by either the -c option, or by
109 looking for the closest known one at or below the current directory.
110
111 =item config
112
113 Adds, modifies, removes, or prints a value from a mrconfig file. The next
114 parameter is the name of the section the value is in. To add or modify
115 values, use one or more instances of "parameter=value". Use "parameter=" to
116 remove a parameter. Use just "parameter" to get the value of a parameter.
117
118 For example, to add (or edit) a repository in src/foo:
119
120   mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
121
122 To show the command that mr uses to update the repository in src/foo:
123
124   mr config src/foo update
125
126 To see the built-in library of shell functions contained in mr:
127
128   mr config DEFAULT lib
129
130 The ~/.mrconfig file is used by default. To use a different config file,
131 use the -c option.
132
133 =item offline
134
135 Advises mr that it is in offline mode. Any commands that fail in
136 offline mode will be remembered, and retried when mr is told it's online.
137
138 =item online
139
140 Advices mr that it is in online mode again. Commands that failed while in
141 offline mode will be re-run.
142
143 =item remember
144
145 Remember a command, to be run later when mr re-enters online mode. This
146 implicitly puts mr into offline mode. The command can be any regular mr
147 command. This is useful when you know that a command will fail due to being
148 offline, and so don't want to run it right now at all, but just remember
149 to run it when you go back online.
150
151 =item help
152
153 Displays this help.
154
155 =back
156
157 Actions can be abbreviated to any unambiguous substring, so
158 "mr st" is equivalent to "mr status", and "mr up" is equivalent to "mr
159 update"
160
161 Additional parameters can be passed to most commands, and are passed on
162 unchanged to the underlying revision control system. This is mostly useful
163 if the repositories mr will act on all use the same revision control
164 system.
165
166 =head1 OPTIONS
167
168 =over 4
169
170 =item -d directory
171
172 Specifies the topmost directory that B<mr> should work in. The default is
173 the current working directory.
174
175 =item -c mrconfig
176
177 Use the specified mrconfig file. The default is B<~/.mrconfig>
178
179 =item -v
180
181 Be verbose.
182
183 =item -q
184
185 Be quiet.
186
187 =item -s
188
189 Expand the statistics line displayed at the end to include information
190 about exactly which repositories failed and were skipped, if any.
191
192 =item -i
193
194 Interactive mode. If a repository fails to be processed, a subshell will be
195 started which you can use to resolve or investigate the problem. Exit the
196 subshell to continue the mr run.
197
198 =item -n [number]
199
200 If no number if specified, just operate on the repository for the current
201 directory, do not recurse into deeper repositories.
202
203 If a number is specified, will recurse into repositories at most that many
204 subdirectories deep. For example, with -n 2 it would recurse into ./src/foo,
205 but not ./src/packages/bar.
206
207 =item -j [number]
208
209 Run the specified number of jobs in parallel, or an unlimited number of jobs
210 with no number specified. This can greatly speed up operations such as updates.
211 It is not recommended for interactive operations.
212
213 Note that running more than 10 jobs at a time is likely to run afoul of
214 ssh connection limits. Running between 3 and 5 jobs at a time will yeild
215 a good speedup in updates without loading the machine too much.
216
217 =back
218
219 =head1 FILES
220
221 The ~/.mrlog file contains commands that mr has remembered to run later,
222 due to being offline. You can delete or edit this file to remove commands,
223 or even to add other commands for 'mr online' to run. If the file is
224 present, mr assumes it is in offline mode.
225
226 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
227 file in your home directory, and this can in turn chain load .mrconfig files
228 from repositories.
229
230 Here is an example .mrconfig file:
231
232   [src]
233   checkout = svn co svn://svn.example.com/src/trunk src
234   chain = true
235
236   [src/linux-2.6]
237   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
238         cd linux-2.6 &&
239         git checkout -b mybranch origin/master
240
241 The .mrconfig file uses a variant of the INI file format. Lines starting with
242 "#" are comments. Values can be continued to the following line by
243 indenting the line with whitespace.
244
245 The "DEFAULT" section allows setting default values for the sections that
246 come after it.
247
248 The "ALIAS" section allows adding aliases for actions. Each parameter
249 is an alias, and its value is the action to use.
250
251 All other sections add repositories. The section header specifies the
252 directory where the repository is located. This is relative to the directory
253 that contains the mrconfig file, but you can also choose to use absolute
254 paths. (Note that you can use environment variables in section names; they
255 will be passed through the shell for expansion. For example, 
256 "[$HOSTNAME]", or "[${HOSTNAME}foo]")
257
258 Within a section, each parameter defines a shell command to run to handle a
259 given action. mr contains default handlers for "update", "status",
260 "commit", and other standard actions. Normally you only need to specify what
261 to do for "checkout".
262
263 Note that these shell commands are run in a "set -e" shell
264 environment, where any additional parameters you pass are available in
265 "$@". The "checkout" command is run in the parent of the repository
266 directory, since the repository isn't checked out yet. All other commands
267 are run inside the repository, though not necessarily at the top of it.
268
269 The "MR_REPO" environment variable is set to the path to the top of the
270 repository. (For the "register" action, "MR_REPO" is instead set to the 
271 basename of the directory that should be created when checking the
272 repository out.)
273
274 The "MR_CONFIG" environment variable is set to the .mrconfig file
275 that defines the repo being acted on, or, if the repo is not yet in a config
276 file, the .mrconfig file that should be modified to register the repo.
277
278 A few parameters have special meanings:
279
280 =over 4
281
282 =item skip
283
284 If the "skip" parameter is set and its command returns true, then B<mr>
285 will skip acting on that repository. The command is passed the action
286 name in $1.
287
288 Here are two examples. The first skips the repo unless
289 mr is run by joey. The second uses the hours_since function
290 (included in mr's built-in library) to skip updating the repo unless it's
291 been at least 12 hours since the last update.
292
293   skip = test `whoami` != joey
294   skip = [ "$1" = update ] && ! hours_since "$1" 12
295
296 =item order
297
298 The "order" parameter can be used to override the default ordering of
299 repositories. The default order value is 10. Use smaller values to make
300 repositories be processed earlier, and larger values to make repositories
301 be processed later.
302
303 Note that if a repository is located in a subdirectory of another
304 repository, ordering it to be processed earlier is not recommended.
305
306 =item chain
307
308 If the "chain" parameter is set and its command returns true, then B<mr>
309 will try to load a .mrconfig file from the root of the repository. (You
310 should avoid chaining from repositories with untrusted committers.)
311
312 =item include
313
314 If the "include" parameter is set, its command is ran, and should output
315 additional mrconfig file content. The content is included as if it were
316 part of the including file.
317
318 Unlike all other parameters, this parameter does not need to be placed
319 within a section.
320
321 =item lib
322
323 The "lib" parameter can specify some shell code that will be run before each
324 command, this can be a useful way to define shell functions for other commands
325 to use.
326
327 =back
328
329 When looking for a command to run for a given action, mr first looks for
330 a parameter with the same name as the action. If that is not found, it
331 looks for a parameter named "rcs_action" (substituting in the name of the
332 revision control system and the action). The name of the revision control
333 system is itself determined by running each defined "rcs_test" action,
334 until one succeeds.
335
336 Internally, mr has settings for "git_update", "svn_update", etc. To change
337 the action that is performed for a given revision control system, you can
338 override these rcs specific actions. To add a new revision control system,
339 you can just add rcs specific actions for it.
340
341 =head1 AUTHOR
342
343 Copyright 2007 Joey Hess <joey@kitenet.net>
344
345 Licensed under the GNU GPL version 2 or higher.
346
347 http://kitenet.net/~joey/code/mr/
348
349 =cut
350
351 #}}}
352
353 use warnings;
354 use strict;
355 use Getopt::Long;
356 use Cwd qw(getcwd abs_path);
357
358 # things that can happen when mr runs a command
359 use constant {
360         OK => 0,
361         FAILED => 1,
362         SKIPPED => 2,
363         ABORT => 3,
364 };
365
366 # configurables
367 my $config_overridden=0;
368 my $verbose=0;
369 my $quiet=0;
370 my $stats=0;
371 my $interactive=0;
372 my $max_depth;
373 my $no_chdir=0;
374 my $jobs=1;
375 my $directory=getcwd();
376 $ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
377
378 # globals :-(
379 my %config;
380 my %configfiles;
381 my %knownactions;
382 my %alias;
383 my (@ok, @failed, @skipped);
384
385 main();
386
387 my %rcs;
388 sub rcs_test { #{{{
389         my ($action, $dir, $topdir, $subdir) = @_;
390
391         if (exists $rcs{$dir}) {
392                 return $rcs{$dir};
393         }
394
395         my $test="set -e\n";
396         foreach my $rcs_test (
397                         sort {
398                                 length $a <=> length $b 
399                                           ||
400                                        $a cmp $b
401                         } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
402                 my ($rcs)=$rcs_test=~/(.*)_test/;
403                 $test="my_$rcs_test() {\n$config{$topdir}{$subdir}{$rcs_test}\n}\n".$test;
404                 $test.="if my_$rcs_test; then echo $rcs; fi\n";
405         }
406         $test=$config{$topdir}{$subdir}{lib}."\n".$test
407                 if exists $config{$topdir}{$subdir}{lib};
408         
409         print "mr $action: running rcs test >>$test<<\n" if $verbose;
410         my $rcs=`$test`;
411         chomp $rcs;
412         if ($rcs=~/\n/s) {
413                 $rcs=~s/\n/, /g;
414                 print STDERR "mr $action: found multiple possible repository types ($rcs) for $topdir$subdir\n";
415                 return undef;
416         }
417         if (! length $rcs) {
418                 return $rcs{$dir}=undef;
419         }
420         else {
421                 return $rcs{$dir}=$rcs;
422         }
423 } #}}}
424         
425 sub findcommand { #{{{
426         my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
427         
428         if (exists $config{$topdir}{$subdir}{$action}) {
429                 return $config{$topdir}{$subdir}{$action};
430         }
431
432         if ($is_checkout) {
433                 return undef;
434         }
435
436         my $rcs=rcs_test(@_);
437
438         if (defined $rcs && 
439             exists $config{$topdir}{$subdir}{$rcs."_".$action}) {
440                 return $config{$topdir}{$subdir}{$rcs."_".$action};
441         }
442         else {
443                 return undef;
444         }
445 } #}}}
446
447 sub action { #{{{
448         my ($action, $dir, $topdir, $subdir) = @_;
449
450         $ENV{MR_CONFIG}=$configfiles{$topdir};
451         my $lib=exists $config{$topdir}{$subdir}{lib} ?
452                        $config{$topdir}{$subdir}{lib}."\n" : "";
453         my $is_checkout=($action eq 'checkout');
454
455         $ENV{MR_REPO}=$dir;
456
457         if ($is_checkout) {
458                 if (-d $dir) {
459                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
460                         return SKIPPED;
461                 }
462
463                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
464         }
465         elsif ($action =~ /update/) {
466                 if (! -d $dir) {
467                         return action("checkout", $dir, $topdir, $subdir);
468                 }
469         }
470
471         my $skiptest=findcommand("skip", $dir, $topdir, $subdir, $is_checkout);
472         my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
473
474         if (defined $skiptest) {
475                 my $test="set -e;".$lib.
476                         "my_action(){ $skiptest\n }; my_action '$action'";
477                 print "mr $action: running skip test >>$test<<\n" if $verbose;
478                 my $ret=system($test);
479                 if ($ret != 0) {
480                         if (($? & 127) == 2) {
481                                 print STDERR "mr $action: interrupted\n";
482                                 return ABORT;
483                         }
484                         elsif ($? & 127) {
485                                 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
486                                 return ABORT;
487                         }
488                 }
489                 if ($ret >> 8 == 0) {
490                         print "mr $action: $dir skipped per config file\n" if $verbose;
491                         return SKIPPED;
492                 }
493         }
494
495         if ($is_checkout && ! -d $dir) {
496                 print "mr $action: creating parent directory $dir\n" if $verbose;
497                 system("mkdir", "-p", $dir);
498         }
499
500         if (! $no_chdir && ! chdir($dir)) {
501                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
502                 return FAILED;
503         }
504         elsif (! defined $command) {
505                 my $rcs=rcs_test(@_);
506                 if (! defined $rcs) {
507                         print STDERR "mr $action: unknown repository type and no defined $action command for $topdir$subdir\n";
508                         return FAILED;
509                 }
510                 else {
511                         print STDERR "mr $action: no defined action for $rcs repository $topdir$subdir, skipping\n";
512                         return SKIPPED;
513                 }
514         }
515         else {
516                 if (! $no_chdir) {
517                         print "mr $action: $topdir$subdir\n" unless $quiet;
518                 }
519                 else {
520                         my $s=$directory;
521                         $s=~s/^\Q$topdir$subdir\E\/?//;
522                         print "mr $action: $topdir$subdir (in subdir $s)\n" unless $quiet;
523                 }
524                 $command="set -e; ".$lib.
525                         "my_action(){ $command\n }; my_action ".
526                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
527                 print "mr $action: running >>$command<<\n" if $verbose;
528                 my $ret=system($command);
529                 if ($ret != 0) {
530                         if (($? & 127) == 2) {
531                                 print STDERR "mr $action: interrupted\n";
532                                 return ABORT;
533                         }
534                         elsif ($? & 127) {
535                                 print STDERR "mr $action: received signal ".($? & 127)."\n";
536                                 return ABORT;
537                         }
538                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
539                         if ($ret >> 8 != 0) {
540                                 print STDERR "mr $action: command failed\n";
541                                 if (-e "$ENV{HOME}/.mrlog" && $action ne 'remember') {
542                                         @ARGV=($action, @ARGV);
543                                         action("remember", $dir, $topdir, $subdir);
544                                 }
545                         }
546                         elsif ($ret != 0) {
547                                 print STDERR "mr $action: command died ($ret)\n";
548                         }
549                         return FAILED;
550                 }
551                 else {
552                         if ($action eq 'checkout' && ! -d $dir) {
553                                 print STDERR "mr $action: $dir missing after checkout\n";;
554                                 return FAILED;
555                         }
556
557                         return OK;
558                 }
559         }
560 } #}}}
561
562 # run actions on multiple repos, in parallel
563 sub mrs { #{{{
564         my $action=shift;
565         my @repos=@_;
566
567         $| = 1;
568         my @active;
569         my @fhs;
570         my @out;
571         my $running=0;
572         while (@fhs or @repos) {
573                 while ((!$jobs || $running < $jobs) && @repos) {
574                         $running++;
575                         my $repo = shift @repos;
576                         pipe(my $outfh, CHILD_STDOUT);
577                         pipe(my $errfh, CHILD_STDERR);
578                         my $pid;
579                         unless ($pid = fork) {
580                                 die "mr $action: cannot fork: $!" unless defined $pid;
581                                 open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
582                                 open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
583                                 close CHILD_STDOUT;
584                                 close CHILD_STDERR;
585                                 close $outfh;
586                                 close $errfh;
587                                 exit action($action, @$repo);
588                         }
589                         close CHILD_STDOUT;
590                         close CHILD_STDERR;
591                         push @active, [$pid, $repo];
592                         push @fhs, [$outfh, $errfh];
593                         push @out, ['',     ''];
594                 }
595                 my ($rin, $rout) = ('','');
596                 my $nfound;
597                 foreach my $fh (@fhs) {
598                         next unless defined $fh;
599                         vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
600                         vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
601                 }
602                 $nfound = select($rout=$rin, undef, undef, 1);
603                 foreach my $channel (0, 1) {
604                         foreach my $i (0..$#fhs) {
605                                 next unless defined $fhs[$i];
606                                 my $fh = $fhs[$i][$channel];
607                                 next unless defined $fh;
608                                 if (vec($rout, fileno($fh), 1) == 1) {
609                                         my $r = '';
610                                         if (sysread($fh, $r, 1024) == 0) {
611                                                 close($fh);
612                                                 $fhs[$i][$channel] = undef;
613                                                 if (! defined $fhs[$i][0] &&
614                                                     ! defined $fhs[$i][1]) {
615                                                         waitpid($active[$i][0], 0);
616                                                         print STDOUT $out[$i][0];
617                                                         print STDERR $out[$i][1];
618                                                         record($active[$i][1], $? >> 8);
619                                                         splice(@fhs, $i, 1);
620                                                         splice(@active, $i, 1);
621                                                         splice(@out, $i, 1);
622                                                         $running--;
623                                                 }
624                                         }
625                                         $out[$i][$channel] .= $r;
626                                 }
627                         }
628                 }
629         }
630 } #}}}
631
632 sub record { #{{{
633         my $dir=shift()->[0];
634         my $ret=shift;
635
636         if ($ret == OK) {
637                 push @ok, $dir;
638                 print "\n";
639         }
640         elsif ($ret == FAILED) {
641                 if ($interactive) {
642                         chdir($dir) unless $no_chdir;
643                         print STDERR "mr: Starting interactive shell. Exit shell to continue.\n";
644                         system((getpwuid($<))[8]);
645                 }
646                 push @failed, $dir;
647                 print "\n";
648         }
649         elsif ($ret == SKIPPED) {
650                 push @skipped, $dir;
651         }
652         elsif ($ret == ABORT) {
653                 exit 1;
654         }
655         else {
656                 die "unknown exit status $ret";
657         }
658 } #}}}
659
660 sub showstats { #{{{
661         my $action=shift;
662         if (! @ok && ! @failed && ! @skipped) {
663                 die "mr $action: no repositories found to work on\n";
664         }
665         print "mr $action: finished (".join("; ",
666                 showstat($#ok+1, "ok", "ok"),
667                 showstat($#failed+1, "failed", "failed"),
668                 showstat($#skipped+1, "skipped", "skipped"),
669         ).")\n" unless $quiet;
670         if ($stats) {
671                 if (@skipped) {
672                         print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet;
673                 }
674                 if (@failed) {
675                         print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
676                 }
677         }
678 } #}}}
679
680 sub showstat { #{{{
681         my $count=shift;
682         my $singular=shift;
683         my $plural=shift;
684         if ($count) {
685                 return "$count ".($count > 1 ? $plural : $singular);
686         }
687         return;
688 } #}}}
689
690 # an ordered list of repos
691 sub repolist { #{{{
692         my @list;
693         foreach my $topdir (sort keys %config) {
694                 foreach my $subdir (sort keys %{$config{$topdir}}) {
695                         push @list, {
696                                 topdir => $topdir,
697                                 subdir => $subdir,
698                                 order => $config{$topdir}{$subdir}{order},
699                         };
700                 }
701         }
702         return sort {
703                 $a->{order}  <=> $b->{order}
704                              ||
705                 $a->{topdir} cmp $b->{topdir}
706                              ||
707                 $a->{subdir} cmp $b->{subdir}
708         } @list;
709 } #}}}
710
711 # figure out which repos to act on
712 sub selectrepos { #{{{
713         my @repos;
714         foreach my $repo (repolist()) {
715                 my $topdir=$repo->{topdir};
716                 my $subdir=$repo->{subdir};
717
718                 next if $subdir eq 'DEFAULT';
719                 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
720                 my $d=$directory;
721                 $dir.="/" unless $dir=~/\/$/;
722                 $d.="/" unless $d=~/\/$/;
723                 next if $dir ne $d && $dir !~ /^\Q$d\E/;
724                 if (defined $max_depth) {
725                         my @a=split('/', $dir);
726                         my @b=split('/', $d);
727                         do { } while (@a && @b && shift(@a) eq shift(@b));
728                         next if @a > $max_depth || @b > $max_depth;
729                 }
730                 push @repos, [$dir, $topdir, $subdir];
731         }
732         if (! @repos) {
733                 # fallback to find a leaf repo
734                 foreach my $repo (reverse repolist()) {
735                         my $topdir=$repo->{topdir};
736                         my $subdir=$repo->{subdir};
737                         
738                         next if $subdir eq 'DEFAULT';
739                         my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
740                         my $d=$directory;
741                         $dir.="/" unless $dir=~/\/$/;
742                         $d.="/" unless $d=~/\/$/;
743                         if ($d=~/^\Q$dir\E/) {
744                                 push @repos, [$dir, $topdir, $subdir];
745                                 last;
746                         }
747                 }
748                 $no_chdir=1;
749         }
750         return @repos;
751 } #}}}
752
753 sub expandenv { #{{{
754         my $val=shift;
755         
756
757         if ($val=~/\$/) {
758                 $val=`echo "$val"`;
759                 chomp $val;
760         }
761         
762         return $val;
763 } #}}}
764
765 my %loaded;
766 sub loadconfig { #{{{
767         my $f=shift;
768
769         my @toload;
770
771         my $in;
772         my $dir;
773         if (ref $f eq 'GLOB') {
774                 $dir="";
775                 $in=$f; 
776         }
777         else {
778                 if (! -e $f) {
779                         return;
780                 }
781
782                 my $absf=abs_path($f);
783                 if ($loaded{$absf}) {
784                         return;
785                 }
786                 $loaded{$absf}=1;
787
788                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
789                 if (! defined $dir) {
790                         $dir=".";
791                 }
792                 $dir=abs_path($dir)."/";
793                 
794                 if (! exists $configfiles{$dir}) {
795                         $configfiles{$dir}=$f;
796                 }
797
798                 # copy in defaults from first parent
799                 my $parent=$dir;
800                 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
801                         if ($parent eq '/') {
802                                 $parent="";
803                         }
804                         if (exists $config{$parent} &&
805                             exists $config{$parent}{DEFAULT}) {
806                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
807                                 last;
808                         }
809                 }
810                 
811                 print "mr: loading config $f\n" if $verbose;
812                 open($in, "<", $f) || die "mr: open $f: $!\n";
813         }
814         my @lines=<$in>;
815         close $in;
816
817         my $section;
818         my $line=0;
819         while (@lines) {
820                 $_=shift @lines;
821                 $line++;
822                 chomp;
823                 next if /^\s*\#/ || /^\s*$/;
824                 if (/^\[([^\]]*)\]\s*$/) {
825                         $section=expandenv($1);
826                 }
827                 elsif (/^(\w+)\s*=\s*(.*)/) {
828                         my $parameter=$1;
829                         my $value=$2;
830
831                         # continued value
832                         while (@lines && $lines[0]=~/^\s(.+)/) {
833                                 shift(@lines);
834                                 $line++;
835                                 $value.="\n$1";
836                                 chomp $value;
837                         }
838
839                         if ($parameter eq "include") {
840                                 print "mr: including output of \"$value\"\n" if $verbose;
841                                 unshift @lines, `$value`;
842                                 next;
843                         }
844
845                         if (! defined $section) {
846                                 die "$f line $.: parameter ($parameter) not in section\n";
847                         }
848                         if ($section ne 'ALIAS' &&
849                             ! exists $config{$dir}{$section} &&
850                             exists $config{$dir}{DEFAULT}) {
851                                 # copy in defaults
852                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
853                         }
854                         if ($section eq 'ALIAS') {
855                                 $alias{$parameter}=$value;
856                         }
857                         elsif ($parameter eq 'lib') {
858                                 $config{$dir}{$section}{lib}.=$value."\n";
859                         }
860                         else {
861                                 $config{$dir}{$section}{$parameter}=$value;
862                                 if ($parameter =~ /.*_(.*)/) {
863                                         $knownactions{$1}=1;
864                                 }
865                                 else {
866                                         $knownactions{$parameter}=1;
867                                 }
868                                 if ($parameter eq 'chain' &&
869                                     length $dir && $section ne "DEFAULT" &&
870                                     -e $dir.$section."/.mrconfig") {
871                                         my $ret=system($value);
872                                         if ($ret != 0) {
873                                                 if (($? & 127) == 2) {
874                                                         print STDERR "mr: chain test interrupted\n";
875                                                         exit 2;
876                                                 }
877                                                 elsif ($? & 127) {
878                                                         print STDERR "mr: chain test received signal ".($? & 127)."\n";
879                                                 }
880                                         }
881                                         else {
882                                                 push @toload, $dir.$section."/.mrconfig";
883                                         }
884                                 }
885                         }
886                 }
887                 else {
888                         die "$f line $line: parse error\n";
889                 }
890         }
891
892         foreach (@toload) {
893                 loadconfig($_);
894         }
895 } #}}}
896
897 sub modifyconfig { #{{{
898         my $f=shift;
899         # the section to modify or add
900         my $targetsection=shift;
901         # fields to change in the section
902         # To remove a field, set its value to "".
903         my %changefields=@_;
904
905         my @lines;
906         my @out;
907
908         if (-e $f) {
909                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
910                 @lines=<$in>;
911                 close $in;
912         }
913
914         my $formatfield=sub {
915                 my $field=shift;
916                 my @value=split(/\n/, shift);
917
918                 return "$field = ".shift(@value)."\n".
919                         join("", map { "\t$_\n" } @value);
920         };
921         my $addfields=sub {
922                 my @blanks;
923                 while ($out[$#out] =~ /^\s*$/) {
924                         unshift @blanks, pop @out;
925                 }
926                 foreach my $field (sort keys %changefields) {
927                         if (length $changefields{$field}) {
928                                 push @out, "$field = $changefields{$field}\n";
929                                 delete $changefields{$field};
930                         }
931                 }
932                 push @out, @blanks;
933         };
934
935         my $section;
936         while (@lines) {
937                 $_=shift(@lines);
938
939                 if (/^\s*\#/ || /^\s*$/) {
940                         push @out, $_;
941                 }
942                 elsif (/^\[([^\]]*)\]\s*$/) {
943                         if (defined $section && 
944                             $section eq $targetsection) {
945                                 $addfields->();
946                         }
947
948                         $section=expandenv($1);
949
950                         push @out, $_;
951                 }
952                 elsif (/^(\w+)\s*=\s(.*)/) {
953                         my $parameter=$1;
954                         my $value=$2;
955
956                         # continued value
957                         while (@lines && $lines[0]=~/^\s(.+)/) {
958                                 shift(@lines);
959                                 $value.="\n$1";
960                                 chomp $value;
961                         }
962
963                         if ($section eq $targetsection) {
964                                 if (exists $changefields{$parameter}) {
965                                         if (length $changefields{$parameter}) {
966                                                 $value=$changefields{$parameter};
967                                         }
968                                         delete $changefields{$parameter};
969                                 }
970                         }
971
972                         push @out, $formatfield->($parameter, $value);
973                 }
974         }
975
976         if (defined $section && 
977             $section eq $targetsection) {
978                 $addfields->();
979         }
980         elsif (%changefields) {
981                 push @out, "\n[$targetsection]\n";
982                 foreach my $field (sort keys %changefields) {
983                         if (length $changefields{$field}) {
984                                 push @out, $formatfield->($field, $changefields{$field});
985                         }
986                 }
987         }
988
989         open(my $out, ">", $f) || die "mr: write $f: $!\n";
990         print $out @out;
991         close $out;     
992 } #}}}
993
994 sub dispatch { #{{{
995         my $action=shift;
996
997         # actions that do not operate on all repos
998         if ($action eq 'help') {
999                 help(@ARGV);
1000         }
1001         elsif ($action eq 'config') {
1002                 config(@ARGV);
1003         }
1004         elsif ($action eq 'register') {
1005                 register(@ARGV);
1006         }
1007         elsif ($action eq 'remember') {
1008                 my @repos=selectrepos;
1009                 action($action, @{$repos[0]}) if @repos;
1010                 exit 0;
1011         }
1012
1013         if (!$jobs || $jobs > 1) {
1014                 mrs($action, selectrepos());
1015         }
1016         else {
1017                 foreach my $repo (selectrepos()) {
1018                         record($repo, action($action, @$repo));
1019                 }
1020         }
1021 } #}}}
1022
1023 sub help { #{{{
1024         exec($config{''}{DEFAULT}{help}) || die "exec: $!";
1025 } #}}}
1026
1027 sub config { #{{{
1028         if (@_ < 2) {
1029                 die "mr config: not enough parameters\n";
1030         }
1031         my $section=shift;
1032         if ($section=~/^\//) {
1033                 # try to convert to a path relative to the config file
1034                 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
1035                 $dir=abs_path($dir);
1036                 $dir.="/" unless $dir=~/\/$/;
1037                 if ($section=~/^\Q$dir\E(.*)/) {
1038                         $section=$1;
1039                 }
1040         }
1041         my %changefields;
1042         foreach (@_) {
1043                 if (/^([^=]+)=(.*)$/) {
1044                         $changefields{$1}=$2;
1045                 }
1046                 else {
1047                         my $found=0;
1048                         foreach my $topdir (sort keys %config) {
1049                                 if (exists $config{$topdir}{$section} &&
1050                                     exists $config{$topdir}{$section}{$_}) {
1051                                         print $config{$topdir}{$section}{$_}."\n";
1052                                         $found=1;
1053                                         last if $section eq 'DEFAULT';
1054                                 }
1055                         }
1056                         if (! $found) {
1057                                 die "mr config: $section $_ not set\n";
1058                         }
1059                 }
1060         }
1061         modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
1062         exit 0;
1063 } #}}}
1064
1065 sub register { #{{{
1066         if ($config_overridden) {
1067                 # Find the directory that the specified config file is
1068                 # located in.
1069                 ($directory)=abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/;
1070         }
1071         else {
1072                 # Find the closest known mrconfig file to the current
1073                 # directory.
1074                 $directory.="/" unless $directory=~/\/$/;
1075                 my $foundconfig=0;
1076                 foreach my $topdir (reverse sort keys %config) {
1077                         next unless length $topdir;
1078                         if ($directory=~/^\Q$topdir\E/) {
1079                                 $ENV{MR_CONFIG}=$configfiles{$topdir};
1080                                 $directory=$topdir;
1081                                 $foundconfig=1;
1082                                 last;
1083                         }
1084                 }
1085                 if (! $foundconfig) {
1086                         $directory=""; # no config file, use builtin
1087                 }
1088         }
1089         if (@ARGV) {
1090                 my $subdir=shift @ARGV;
1091                 if (! chdir($subdir)) {
1092                         print STDERR "mr register: failed to chdir to $subdir: $!\n";
1093                 }
1094         }
1095
1096         $ENV{MR_REPO}=getcwd();
1097         my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
1098         if (! defined $command) {
1099                 die "mr register: unknown repository type\n";
1100         }
1101
1102         $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
1103         $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n".
1104                 "my_action(){ $command\n }; my_action ".
1105                 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
1106         print "mr register: running >>$command<<\n" if $verbose;
1107         exec($command) || die "exec: $!";
1108 } #}}}
1109
1110 # alias expansion and command stemming
1111 sub expandaction { #{{{
1112         my $action=shift;
1113         if (exists $alias{$action}) {
1114                 $action=$alias{$action};
1115         }
1116         if (! exists $knownactions{$action}) {
1117                 my @matches = grep { /^\Q$action\E/ }
1118                         keys %knownactions, keys %alias;
1119                 if (@matches == 1) {
1120                         $action=$matches[0];
1121                 }
1122                 elsif (@matches == 0) {
1123                         die "mr: unknown action \"$action\" (known actions: ".
1124                                 join(", ", sort keys %knownactions).")\n";
1125                 }
1126                 else {
1127                         die "mr: ambiguous action \"$action\" (matches: ".
1128                                 join(", ", @matches).")\n";
1129                 }
1130         }
1131         return $action;
1132 } #}}}
1133
1134 sub getopts { #{{{
1135         Getopt::Long::Configure("bundling", "no_permute");
1136         my $result=GetOptions(
1137                 "d|directory=s" => sub { $directory=abs_path($_[1]) },
1138                 "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
1139                 "v|verbose" => \$verbose,
1140                 "q|quiet" => \$quiet,
1141                 "s|stats" => \$stats,
1142                 "i|interactive" => \$interactive,
1143                 "n|no-recurse:i" => \$max_depth,
1144                 "j|jobs:i" => \$jobs,
1145         );
1146         if (! $result || @ARGV < 1) {
1147                 die("Usage: mr [-d directory] action [params ...]\n".
1148                     "(Use mr help for man page.)\n");
1149         }
1150 } #}}}
1151
1152 sub init { #{{{
1153         $SIG{INT}=sub {
1154                 print STDERR "mr: interrupted\n";
1155                 exit 2;
1156         };
1157         
1158         # This can happen if it's run in a directory that was removed
1159         # or other strangeness.
1160         if (! defined $directory) {
1161                 die("mr: failed to determine working directory\n");
1162         }
1163         # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
1164         # the config file might be a symlink to elsewhere, and the directory it's
1165         # in is significant.
1166         if ($ENV{MR_CONFIG} !~ /^\//) {
1167                 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
1168         }
1169         # Try to set MR_PATH to the path to the program.
1170         eval {
1171                 use FindBin qw($Bin $Script);
1172                 $ENV{MR_PATH}=$Bin."/".$Script;
1173         };
1174 } #}}}
1175
1176 sub main { #{{{
1177         getopts();
1178         init();
1179         loadconfig(\*DATA);
1180         loadconfig($ENV{MR_CONFIG});
1181         #use Data::Dumper; print Dumper(\%config);
1182
1183         my $action=expandaction(shift @ARGV);
1184         dispatch($action);
1185         showstats($action);
1186
1187         if (@failed) {
1188                 exit 1;
1189         }
1190         elsif (! @ok && @skipped) {
1191                 exit 1;
1192         }
1193         else {
1194                 exit 0;
1195         }
1196 } #}}}
1197
1198 # Finally, some useful actions that mr knows about by default.
1199 # These can be overridden in ~/.mrconfig.
1200 #DATA{{{
1201 __DATA__
1202 [ALIAS]
1203 co = checkout
1204 ci = commit
1205 ls = list
1206
1207 [DEFAULT]
1208 order = 10
1209 lib =
1210         error() {
1211                 echo "mr: $@" >&2
1212                 exit 1
1213         }
1214         warning() {
1215                 echo "mr (warning): $@" >&2
1216         }
1217         info() {
1218                 echo "mr: $@" >&2
1219         }
1220         hours_since() {
1221                 if [ -z "$1" ] || [ -z "$2" ]; then
1222                         error "mr: usage: hours_since action num"
1223                 fi
1224                 for dir in .git .svn .bzr CVS .hg _darcs; do
1225                         if [ -e "$MR_REPO/$dir" ]; then
1226                                 flagfile="$MR_REPO/$dir/.mr_last$1"
1227                                 break
1228                         fi
1229                 done
1230                 if [ -z "$flagfile" ]; then
1231                         error "cannot determine flag filename"
1232                 fi
1233                 delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
1234                 if [ "$delta" -lt "$2" ]; then
1235                         exit 0
1236                 else
1237                         touch "$flagfile"
1238                         exit 1
1239                 fi
1240         }
1241
1242 svn_test = test -d "$MR_REPO"/.svn
1243 git_test = test -d "$MR_REPO"/.git
1244 bzr_test = test -d "$MR_REPO"/.bzr
1245 cvs_test = test -d "$MR_REPO"/CVS
1246 hg_test  = test -d "$MR_REPO"/.hg
1247 darcs_test = test -d "$MR_REPO"/_darcs
1248 git_bare_test =
1249         test -d "$MR_REPO"/refs/heads && test -d "$MR_REPO"/refs/tags &&
1250         test -d "$MR_REPO"/objects && test -f "$MR_REPO"/config &&
1251         test "`GIT_CONFIG="$MR_REPO"/config git config --get core.bare`" = true
1252
1253 svn_update = svn update "$@"
1254 git_update = git pull "$@"
1255 bzr_update = bzr merge "$@"
1256 cvs_update = cvs update "$@"
1257 hg_update  = hg pull "$@" && hg update "$@"
1258 darcs_update = darcs pull -a "$@"
1259
1260 svn_status = svn status "$@"
1261 git_status = git status "$@" || true
1262 bzr_status = bzr status "$@"
1263 cvs_status = cvs status "$@"
1264 hg_status  = hg status "$@"
1265 darcs_status = darcs whatsnew -ls "$@" || true
1266
1267 svn_commit = svn commit "$@"
1268 git_commit = git commit -a "$@" && git push --all
1269 bzr_commit = bzr commit "$@" && bzr push
1270 cvs_commit = cvs commit "$@"
1271 hg_commit  = hg commit -m "$@" && hg push
1272 darcs_commit = darcs record -a -m "$@" && darcs push -a
1273
1274 git_record = git commit -a "$@"
1275 bzr_record = bzr commit "$@"
1276 hg_record  = hg commit -m "$@"
1277 darcs_record = darcs record -a -m "$@"
1278
1279 svn_diff = svn diff "$@"
1280 git_diff = git diff "$@"
1281 bzr_diff = bzr diff "$@"
1282 cvs_diff = cvs diff "$@"
1283 hg_diff  = hg diff "$@"
1284 darcs_diff = darcs diff -u "$@"
1285
1286 svn_log = svn log "$@"
1287 git_log = git log "$@"
1288 bzr_log = bzr log "$@"
1289 cvs_log = cvs log "$@"
1290 hg_log  = hg log "$@"
1291 darcs_log = darcs changes "$@"
1292 git_bare_log = git log "$@"
1293
1294 svn_register =
1295         url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
1296         if [ -z "$url" ]; then
1297                 error "cannot determine svn url"
1298         fi
1299         echo "Registering svn url: $url in $MR_CONFIG"
1300         mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
1301 git_register = 
1302         url="`LC_ALL=C git config --get remote.origin.url`" || true
1303         if [ -z "$url" ]; then
1304                 error "cannot determine git url"
1305         fi
1306         echo "Registering git url: $url in $MR_CONFIG"
1307         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
1308 bzr_register =
1309         url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}'`"
1310         if [ -z "$url" ]; then
1311                 error "cannot determine bzr url"
1312         fi
1313         echo "Registering bzr url: $url in $MR_CONFIG"
1314         mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr clone '$url' '$MR_REPO'"
1315 cvs_register =
1316         repo=`cat CVS/Repository`
1317         root=`cat CVS/Root`
1318         if [ -z "$root" ]; then
1319                 error "cannot determine cvs root"
1320                 fi
1321         echo "Registering cvs repository $repo at root $root"
1322         mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
1323 hg_register = 
1324         url=`hg showconfig paths.default`
1325         echo "Registering mercurial repo url: $url in $MR_CONFIG"
1326         mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
1327 darcs_register = 
1328         url=`cat _darcs/prefs/defaultrepo`
1329         echo "Registering darcs repository $url in $MR_CONFIG"
1330         mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
1331 git_bare_register = 
1332         url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
1333         if [ -z "$url" ]; then
1334                 error "cannot determine git url"
1335         fi
1336         echo "Registering git url: $url in $MR_CONFIG"
1337         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
1338
1339 help =
1340         if [ ! -e "$MR_PATH" ]; then
1341                 error "cannot find program path"
1342         fi
1343         tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
1344         trap "rm -f $tmp" exit
1345         pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
1346         man -l "$tmp" || error "man failed"
1347 list = true
1348 config = 
1349
1350 online =
1351         if [ -e ~/.mrlog ]; then
1352                 info "running offline commands"
1353                 mv -f ~/.mrlog ~/.mrlog.old
1354                 if ! sh ~/.mrlog.old; then
1355                         error "offline commands failed; left in ~/.mrlog.old"
1356                 fi
1357                 rm -f ~/.mrlog.old
1358         else
1359                 info "no offline commands to run"
1360         fi
1361 offline =
1362         touch ~/.mrlog
1363         info "offline mode enabled"
1364 remember =
1365         info "remembering command: 'mr $@'"
1366         command="mr -d '$(pwd)'"
1367         for w in "$@"; do
1368                 command="$command '$w'"
1369         done
1370         if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
1371                 echo "$command" >> ~/.mrlog
1372         fi
1373
1374 ed = echo "A horse is a horse, of course, of course.."
1375 T = echo "I pity the fool."
1376 right = echo "Not found."
1377 #}}}
1378
1379 # vim:sw=8:sts=0:ts=8:noet