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

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