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

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