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

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