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

madduck's git repository

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

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

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

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

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

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