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

omit \n between repos in quiet mode
[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 =head1 "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" unless $quiet;
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" unless $quiet;
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 bootstrap: 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 bootstrap: .mrconfig file already exists, not overwriting with $url\n";
1358         }
1359         else {
1360                 eval q{use File::Copy};
1361                 die $@ if $@;
1362                 move($tmpconfig, ".mrconfig") || die "rename: $!";
1363         }
1364
1365         exec("mr $ENV{MR_SWITCHES} -c .mrconfig checkout");
1366         die "failed to run mr checkout";
1367 }
1368
1369 # alias expansion and command stemming
1370 sub expandaction {
1371         my $action=shift;
1372         if (exists $alias{$action}) {
1373                 $action=$alias{$action};
1374         }
1375         if (! exists $knownactions{$action}) {
1376                 my @matches = grep { /^\Q$action\E/ }
1377                         keys %knownactions, keys %alias;
1378                 if (@matches == 1) {
1379                         $action=$matches[0];
1380                 }
1381                 elsif (@matches == 0) {
1382                         die "mr: unknown action \"$action\" (known actions: ".
1383                                 join(", ", sort keys %knownactions).")\n";
1384                 }
1385                 else {
1386                         die "mr: ambiguous action \"$action\" (matches: ".
1387                                 join(", ", @matches).")\n";
1388                 }
1389         }
1390         return $action;
1391 }
1392
1393 sub find_nearest_mrconfig {
1394         my $dir=getcwd();
1395         while (length $dir) {
1396                 if (-e "$dir/.mrconfig") {
1397                         return "$dir/.mrconfig";
1398                 }
1399                 $dir=~s/\/[^\/]*$//;
1400         }
1401         die "no .mrconfig found in path\n";
1402 }
1403
1404 sub getopts {
1405         my @saved=@ARGV;
1406         Getopt::Long::Configure("bundling", "no_permute");
1407         my $result=GetOptions(
1408                 "d|directory=s" => sub { $directory=abs_path($_[1]) },
1409                 "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
1410                 "p|path" => sub { $ENV{MR_CONFIG}=find_nearest_mrconfig(); $config_overridden=1 },
1411                 "v|verbose" => \$verbose,
1412                 "q|quiet" => \$quiet,
1413                 "s|stats" => \$stats,
1414                 "i|interactive" => \$interactive,
1415                 "n|no-recurse:i" => \$max_depth,
1416                 "j|jobs:i" => \$jobs,
1417                 "t|trust-all" => \$trust_all,
1418         );
1419         if (! $result || @ARGV < 1) {
1420                 die("Usage: mr [options] action [params ...]\n".
1421                     "(Use mr help for man page.)\n");
1422         }
1423         
1424         $ENV{MR_SWITCHES}="";
1425         foreach my $option (@saved) {
1426                 last if $option eq $ARGV[0];
1427                 $ENV{MR_SWITCHES}.="$option ";
1428         }
1429 }
1430
1431 sub init {
1432         $SIG{INT}=sub {
1433                 print STDERR "mr: interrupted\n";
1434                 exit 2;
1435         };
1436         
1437         # This can happen if it's run in a directory that was removed
1438         # or other strangeness.
1439         if (! defined $directory) {
1440                 die("mr: failed to determine working directory\n");
1441         }
1442         # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
1443         # the config file might be a symlink to elsewhere, and the directory it's
1444         # in is significant.
1445         if ($ENV{MR_CONFIG} !~ /^\//) {
1446                 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
1447         }
1448         # Try to set MR_PATH to the path to the program.
1449         eval {
1450                 use FindBin qw($Bin $Script);
1451                 $ENV{MR_PATH}=$Bin."/".$Script;
1452         };
1453 }
1454
1455 sub main {
1456         getopts();
1457         init();
1458
1459         loadconfig(\*DATA);
1460         loadconfig($ENV{MR_CONFIG});
1461         #use Data::Dumper; print Dumper(\%config);
1462         
1463         my $action=expandaction(shift @ARGV);
1464         dispatch($action);
1465         showstats($action);
1466
1467         if (@failed) {
1468                 exit 1;
1469         }
1470         elsif (! @ok && @skipped) {
1471                 exit 1;
1472         }
1473         else {
1474                 exit 0;
1475         }
1476 }
1477
1478 # Finally, some useful actions that mr knows about by default.
1479 # These can be overridden in ~/.mrconfig.
1480 __DATA__
1481 [ALIAS]
1482 co = checkout
1483 ci = commit
1484 ls = list
1485
1486 [DEFAULT]
1487 order = 10
1488 lib =
1489         error() {
1490                 echo "mr: $@" >&2
1491                 exit 1
1492         }
1493         warning() {
1494                 echo "mr (warning): $@" >&2
1495         }
1496         info() {
1497                 echo "mr: $@" >&2
1498         }
1499         hours_since() {
1500                 if [ -z "$1" ] || [ -z "$2" ]; then
1501                         error "mr: usage: hours_since action num"
1502                 fi
1503                 for dir in .git .svn .bzr CVS .hg _darcs; do
1504                         if [ -e "$MR_REPO/$dir" ]; then
1505                                 flagfile="$MR_REPO/$dir/.mr_last$1"
1506                                 break
1507                         fi
1508                 done
1509                 if [ -z "$flagfile" ]; then
1510                         error "cannot determine flag filename"
1511                 fi
1512                 delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
1513                 if [ "$delta" -lt "$2" ]; then
1514                         exit 0
1515                 else
1516                         touch "$flagfile"
1517                         exit 1
1518                 fi
1519         }
1520
1521 svn_test = test -d "$MR_REPO"/.svn
1522 git_test = test -d "$MR_REPO"/.git
1523 bzr_test = test -d "$MR_REPO"/.bzr
1524 cvs_test = test -d "$MR_REPO"/CVS
1525 hg_test  = test -d "$MR_REPO"/.hg
1526 darcs_test = test -d "$MR_REPO"/_darcs
1527 git_bare_test =
1528         test -d "$MR_REPO"/refs/heads && test -d "$MR_REPO"/refs/tags &&
1529         test -d "$MR_REPO"/objects && test -f "$MR_REPO"/config &&
1530         test "`GIT_CONFIG="$MR_REPO"/config git config --get core.bare`" = true
1531
1532 svn_update = svn update "$@"
1533 git_update = git pull "$@"
1534 bzr_update = bzr merge --pull "$@"
1535 cvs_update = cvs update "$@"
1536 hg_update  = hg pull "$@" && hg update "$@"
1537 darcs_update = darcs pull -a "$@"
1538
1539 svn_status = svn status "$@"
1540 git_status = git status "$@" || true
1541 bzr_status = bzr status "$@"
1542 cvs_status = cvs status "$@"
1543 hg_status  = hg status "$@"
1544 darcs_status = darcs whatsnew -ls "$@" || true
1545
1546 svn_commit = svn commit "$@"
1547 git_commit = git commit -a "$@" && git push --all
1548 bzr_commit = bzr commit "$@" && bzr push
1549 cvs_commit = cvs commit "$@"
1550 hg_commit  = hg commit -m "$@" && hg push
1551 darcs_commit = darcs record -a -m "$@" && darcs push -a
1552
1553 git_record = git commit -a "$@"
1554 bzr_record = bzr commit "$@"
1555 hg_record  = hg commit -m "$@"
1556 darcs_record = darcs record -a -m "$@"
1557
1558 svn_push = :
1559 git_push = git push "$@"
1560 bzr_push = bzr push "$@"
1561 cvs_push = :
1562 hg_push = hg push "$@"
1563 darcs_push = darcs push -a "$@"
1564
1565 svn_diff = svn diff "$@"
1566 git_diff = git diff "$@"
1567 bzr_diff = bzr diff "$@"
1568 cvs_diff = cvs diff "$@"
1569 hg_diff  = hg diff "$@"
1570 darcs_diff = darcs diff -u "$@"
1571
1572 svn_log = svn log "$@"
1573 git_log = git log "$@"
1574 bzr_log = bzr log "$@"
1575 cvs_log = cvs log "$@"
1576 hg_log  = hg log "$@"
1577 darcs_log = darcs changes "$@"
1578 git_bare_log = git log "$@"
1579
1580 svn_register =
1581         url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
1582         if [ -z "$url" ]; then
1583                 error "cannot determine svn url"
1584         fi
1585         echo "Registering svn url: $url in $MR_CONFIG"
1586         mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
1587 git_register = 
1588         url="`LC_ALL=C git config --get remote.origin.url`" || true
1589         if [ -z "$url" ]; then
1590                 error "cannot determine git url"
1591         fi
1592         echo "Registering git url: $url in $MR_CONFIG"
1593         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
1594 bzr_register =
1595         url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}'`"
1596         if [ -z "$url" ]; then
1597                 error "cannot determine bzr url"
1598         fi
1599         echo "Registering bzr url: $url in $MR_CONFIG"
1600         mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr clone '$url' '$MR_REPO'"
1601 cvs_register =
1602         repo=`cat CVS/Repository`
1603         root=`cat CVS/Root`
1604         if [ -z "$root" ]; then
1605                 error "cannot determine cvs root"
1606                 fi
1607         echo "Registering cvs repository $repo at root $root"
1608         mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
1609 hg_register = 
1610         url=`hg showconfig paths.default`
1611         echo "Registering mercurial repo url: $url in $MR_CONFIG"
1612         mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
1613 darcs_register = 
1614         url=`cat _darcs/prefs/defaultrepo`
1615         echo "Registering darcs repository $url in $MR_CONFIG"
1616         mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
1617 git_bare_register = 
1618         url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
1619         if [ -z "$url" ]; then
1620                 error "cannot determine git url"
1621         fi
1622         echo "Registering git url: $url in $MR_CONFIG"
1623         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
1624
1625 svn_trusted_checkout = svn co $url $repo
1626 svn_alt_trusted_checkout = svn checkout $url $repo
1627 git_trusted_checkout = git clone $url $repo
1628 bzr_trusted_checkout = bzr clone $url $repo
1629 # cvs: too hard
1630 hg_trusted_checkout = hg clone $url $repo
1631 darcs_trusted_checkout = darcs get $url $repo
1632 git_bare_trusted_checkout = git clone --bare $url $repo
1633
1634
1635 help =
1636         case `uname -s` in
1637                 SunOS)
1638                 SHOWMANFILE="man -f"
1639                 ;;
1640                 Darwin)
1641                 SHOWMANFILE="man"
1642                 ;;
1643                 *)
1644                 SHOWMANFILE="man -l"
1645                 ;;
1646         esac
1647         if [ ! -e "$MR_PATH" ]; then
1648                 error "cannot find program path"
1649         fi
1650         tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
1651         trap "rm -f $tmp" exit
1652         pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
1653         $SHOWMANFILE "$tmp" || error "man failed"
1654 list = true
1655 config = 
1656 bootstrap = 
1657
1658 online =
1659         if [ -s ~/.mrlog ]; then
1660                 info "running offline commands"
1661                 mv -f ~/.mrlog ~/.mrlog.old
1662                 if ! sh -e ~/.mrlog.old; then
1663                         error "offline command failed; left in ~/.mrlog.old"
1664                 fi
1665                 rm -f ~/.mrlog.old
1666         else
1667                 info "no offline commands to run"
1668         fi
1669 offline =
1670         umask 077
1671         touch ~/.mrlog
1672         info "offline mode enabled"
1673 remember =
1674         info "remembering command: 'mr $@'"
1675         command="mr -d '$(pwd)' $MR_SWITCHES"
1676         for w in "$@"; do
1677                 command="$command '$w'"
1678         done
1679         if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
1680                 echo "$command" >> ~/.mrlog
1681         fi
1682
1683 ed = echo "A horse is a horse, of course, of course.."
1684 T = echo "I pity the fool."
1685 right = echo "Not found."
1686
1687 # vim:sw=8:sts=0:ts=8:noet