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

Pass a fake terminal to subcommands when mr is run in a terminal.
[code/myrepos.git] / mr
1 #!/usr/bin/env perl
2
3 =head1 NAME
4
5 mr - a tool to manage all your version control repos
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] fetch
20
21 B<mr> [options] push
22
23 B<mr> [options] diff
24
25 B<mr> [options] log
26
27 B<mr> [options] grep pattern
28
29 B<mr> [options] run command [param ...]
30
31 B<mr> [options] bootstrap src [directory]
32
33 B<mr> [options] register [repository]
34
35 B<mr> [options] config section ["parameter=[value]" ...]
36
37 B<mr> [options] action [params ...]
38
39 B<mr> [options] [online|offline]
40
41 B<mr> [options] remember action [params ...]
42
43 =head1 DESCRIPTION
44
45 B<mr> is a tool to manage all your version control repos. It can checkout,
46 update, or perform other actions on a set of repositories as if they were
47 one combined repository. It supports any combination of subversion, git,
48 cvs, mercurial, bzr, darcs, fossil and veracity repositories, and support
49 for other version control systems can easily be added.
50
51 B<mr> cds into and operates on all registered repositories at or below your
52 working directory. Or, if you are in a subdirectory of a repository that
53 contains no other registered repositories, it will stay in that directory,
54 and work on only that repository,
55
56 B<mr> is configured by .mrconfig files, which list the repositories. It
57 starts by reading the .mrconfig file in your home directory, and this can
58 in turn chain load .mrconfig files from repositories. It also automatically
59 looks for a .mrconfig file in the current directory, or in one of its
60 parent directories.
61
62 These predefined commands should be fairly familiar to users of any version
63 control system:
64
65 =over 4
66
67 =item checkout (or co)
68
69 Checks out any repositories that are not already checked out.
70
71 =item update
72
73 Updates each repository from its configured remote repository.
74
75 If a repository isn't checked out yet, it will first check it out.
76
77 =item status
78
79 Displays a status report for each repository, showing what
80 uncommitted changes are present in the repository. For distributed version
81 control systems, also shows unpushed local branches.
82
83 =item commit (or ci)
84
85 Commits changes to each repository. (By default, changes are pushed to the
86 remote repository too, when using distributed systems like git. If you
87 don't like this default, you can change it in your .mrconfig, or use record
88 instead.)
89
90 The optional -m parameter allows specifying a commit message.
91
92 =item record
93
94 Records changes to the local repository, but does not push them to the
95 remote repository. Only supported for distributed version control systems.
96
97 The optional -m parameter allows specifying a commit message.
98
99 =item fetch
100
101 Fetches from each repository's remote repository, but does not
102 update the working copy. Only supported for some distributed version
103 control systems.
104
105 =item push
106
107 Pushes committed local changes to the remote repository. A no-op for
108 centralized version control systems.
109
110 =item diff
111
112 Show a diff of uncommitted changes.
113
114 =item log
115
116 Show the commit log.
117
118 =item grep pattern
119
120 Searches for a pattern in each repository using the grep subcommand. Uses
121 ack-grep on VCS that do not have their own.
122
123 =item run command [param ...]
124
125 Runs the specified command in each repository.
126
127 =back
128
129 These commands are also available:
130
131 =over 4
132
133 =item bootstrap src [directory]
134
135 Causes mr to retrieve the source C<src> and use it as a .mrconfig file to
136 checkout the repositories listed in it, into the specified directory.
137
138 B<mr> understands several types of sources:
139
140 =over 4
141
142 =item URL for curl
143
144 C<src> may be an URL understood by B<curl>.
145
146 =item copy via ssh
147
148 To use B<scp> to download, the C<src> may have the form
149 C<ssh://[user@]host:file>.
150
151 =item local file
152
153 You can retrieve the config file by other means and pass its B<path> as C<src>.
154
155 =item standard input
156
157 If source C<src> consists in a single dash C<->, config file is read from
158 standard input.
159
160 =back
161
162 The directory will be created if it does not exist. If no directory is
163 specified, the current directory will be used.
164
165 As a special case, if source C<src> includes a repository named ".", that
166 is checked out into the top of the specified directory.
167
168 =item list (or ls)
169
170 List the repositories that mr will act on.
171
172 =item register
173
174 Register an existing repository in a mrconfig file. By default, the
175 repository in the current directory is registered, or you can specify a
176 directory to register.
177
178 The mrconfig file that is modified is chosen by either the -c option, or by
179 looking for the closest known one at or in a parent of the current directory.
180
181 =item config
182
183 Adds, modifies, removes, or prints a value from a mrconfig file. The next
184 parameter is the name of the section the value is in. To add or modify
185 values, use one or more instances of "parameter=value". Use "parameter=" to
186 remove a parameter. Use just "parameter" to get the value of a parameter.
187
188 For example, to add (or edit) a repository in src/foo:
189
190   mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
191
192 To show the command that mr uses to update the repository in src/foo:
193
194   mr config src/foo update
195
196 To see the built-in library of shell functions contained in mr:
197
198   mr config DEFAULT lib
199
200 The mrconfig file that is used is chosen by either the -c option, or by
201 looking for the closest known one at or in a parent of the current directory.
202
203 =item offline
204
205 Advises mr that it is in offline mode. Any commands that fail in
206 offline mode will be remembered, and retried when mr is told it's online.
207
208 =item online
209
210 Advices mr that it is in online mode again. Commands that failed while in
211 offline mode will be re-run.
212
213 =item remember
214
215 Remember a command, to be run later when mr re-enters online mode. This
216 implicitly puts mr into offline mode. The command can be any regular mr
217 command. This is useful when you know that a command will fail due to being
218 offline, and so don't want to run it right now at all, but just remember
219 to run it when you go back online.
220
221 =item help
222
223 Displays this help.
224
225 =back
226
227 Actions can be abbreviated to any unambiguous substring, so
228 "mr st" is equivalent to "mr status", and "mr up" is equivalent to "mr
229 update"
230
231 Additional parameters can be passed to most commands, and are passed on
232 unchanged to the underlying version control system. This is mostly useful
233 if the repositories mr will act on all use the same version control
234 system.
235
236 =head1 OPTIONS
237
238 =over 4
239
240 =item -d directory
241
242 =item --directory directory
243
244 Specifies the topmost directory that B<mr> should work in. The default is
245 the current working directory.
246
247 =item -c mrconfig
248
249 =item --config mrconfig
250
251 Use the specified mrconfig file. The default is to use both F<~/.mrconfig>
252 as well as look for a F<.mrconfig> file in the current directory, or in one
253 of its parent directories.
254
255 =item -f
256
257 =item --force
258
259 Force mr to act on repositories that would normally be skipped due to their
260 configuration.
261
262 =item -v
263
264 =item --verbose
265
266 Be verbose.
267
268 =item -q
269
270 =item --quiet
271
272 Be quiet. This suppresses mr's usual output, as well as any output from
273 commands that are run (including stderr output). If a command fails,
274 the output will be shown.
275
276 =item -k
277
278 =item --insecure
279
280 Accept untrusted SSL certificates when bootstrapping.
281
282 =item -s
283
284 =item --stats
285
286 Expand the statistics line displayed at the end to include information
287 about exactly which repositories failed and were skipped, if any.
288
289 =item -i
290
291 =item --interactive
292
293 Interactive mode. If a repository fails to be processed, a subshell will be
294 started which you can use to resolve or investigate the problem. Exit the
295 subshell to continue the mr run.
296
297 =item -n [number]
298
299 =item --no-recurse [number]
300
301 If no number if specified, just operate on the repository for the current
302 directory, do not recurse into deeper repositories.
303
304 If a number is specified, will recurse into repositories at most that many
305 subdirectories deep. For example, with -n 2 it would recurse into ./src/foo,
306 but not ./src/packages/bar.
307
308 =item -j [number]
309
310 =item --jobs [number]
311
312 Run the specified number of jobs in parallel, or an unlimited number of jobs
313 with no number specified. This can greatly speed up operations such as updates.
314 It is not recommended for interactive operations.
315
316 Note that running more than 10 jobs at a time is likely to run afoul of
317 ssh connection limits. Running between 3 and 5 jobs at a time will yield
318 a good speedup in updates without loading the machine too much.
319
320 =item -t
321
322 =item --trust-all
323
324 Trust all mrconfig files even if they are not listed in F<~/.mrtrust>.
325 Use with caution.
326
327 =item -p
328
329 =item --path
330
331 This obsolete flag is ignored.
332
333 =back
334
335 =head1 MRCONFIG FILES
336
337 Here is an example F<.mrconfig> file:
338
339   [src]
340   checkout = svn checkout svn://svn.example.com/src/trunk src
341   chain = true
342
343   [src/linux-2.6]
344   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
345         cd linux-2.6 &&
346         git checkout -b mybranch origin/master
347
348 The F<.mrconfig> file uses a variant of the INI file format. Lines
349 starting with "#" are comments. Values can be continued to the
350 following line by indenting the line with whitespace.
351
352 The C<DEFAULT> section allows setting default values for the sections that
353 come after it.
354
355 The C<ALIAS> section allows adding aliases for actions. Each parameter
356 is an alias, and its value is the action to use.
357
358 All other sections add repositories. The section header specifies the
359 directory where the repository is located. This is relative to the directory
360 that contains the mrconfig file, but you can also choose to use absolute
361 paths. (Note that you can use environment variables in section names; they
362 will be passed through the shell for expansion. For example, 
363 C<[$HOSTNAME]>, or C<[${HOSTNAME}foo]>).
364
365 Within a section, each parameter defines a shell command to run to handle a
366 given action. mr contains default handlers for "update", "status",
367 "commit", and other standard actions.
368
369 Normally you only need to specify what to do for "checkout". Here you
370 specify the command to run in order to create a checkout of the repository.
371 The command will be run in the parent directory, and must create the
372 repository's directory. So use C<git clone>, C<svn checkout>, C<bzr branch>
373 or C<bzr checkout> (for a bound branch), etc.
374
375 Note that these shell commands are run in a C<set -e> shell
376 environment, where any additional parameters you pass are available in
377 C<$@>. All commands other than "checkout" are run inside the repository,
378 though not necessarily at the top of it.
379
380 The C<MR_REPO> environment variable is set to the path to the top of the
381 repository. (For the "register" action, "MR_REPO" is instead set to the 
382 basename of the directory that should be created when checking the
383 repository out.)
384
385 The C<MR_CONFIG> environment variable is set to the .mrconfig file
386 that defines the repo being acted on, or, if the repo is not yet in a config
387 file, the F<.mrconfig> file that should be modified to register the repo.
388
389 The C<MR_ACTION> environment variable is set to the command being run
390 (update, checkout, etc).
391
392 A few parameters have special meanings:
393
394 =over 4
395
396 =item skip
397
398 If the "skip" parameter is set and its command returns true, then B<mr>
399 will skip acting on that repository. The command is passed the action
400 name in C<$1>.
401
402 Here are two examples. The first skips the repo unless
403 mr is run by joey. The second uses the hours_since function
404 (included in mr's built-in library) to skip updating the repo unless it's
405 been at least 12 hours since the last update.
406
407   [mystuff]
408   checkout = ...
409   skip = test `whoami` != joey
410
411   [linux]
412   checkout = ...
413   skip = [ "$1" = update ] && ! hours_since "$1" 12
414  
415 Another way to use skip is for a lazy checkout. This makes mr skip
416 operating on a repo unless it already exists. To enable the 
417 repo, you have to explicitly check it out (using "mr --force -d foo checkout").
418
419   [foo]
420   checkout = ...
421   skip = lazy
422
423 =item order
424
425 The "order" parameter can be used to override the default ordering of
426 repositories. The default order value is 10. Use smaller values to make
427 repositories be processed earlier, and larger values to make repositories
428 be processed later.
429
430 Note that if a repository is located in a subdirectory of another
431 repository, ordering it to be processed earlier is not recommended.
432
433 =item chain
434
435 If the "chain" parameter is set and its command returns true, then B<mr>
436 will try to load a F<.mrconfig> file from the root of the repository.
437
438 =item include
439
440 If the "include" parameter is set, its command is ran, and should output
441 additional mrconfig file content. The content is included as if it were
442 part of the including file.
443
444 Unlike all other parameters, this parameter does not need to be placed
445 within a section.
446
447 B<mr> ships several libraries that can be included to add support for
448 additional version control type things (unison, git-svn, git-fake-bare,
449 git-subtree). To include them all, you could use:
450
451   include = cat /usr/share/mr/*
452
453 See the individual files for details.
454
455 =item deleted
456
457 If the "deleted" parameter is set and its command returns true, then
458 B<mr> will treat the repository as deleted. It won't ever actually delete
459 the repository, but it will warn if it sees the repository's directory.
460 This is useful when one mrconfig file is shared among multiple machines,
461 to keep track of and remember to delete old repositories.
462
463 =item lib
464
465 The "lib" parameter can specify some shell code that will be run
466 before each command, this can be a useful way to define shell
467 functions for other commands to use. 
468
469 Unlike most other parameters, this can be specified multiple times, in
470 which case the chunks of shell code are accumulatively concatenated
471 together.
472
473 =item fixups
474
475 If the "fixups" parameter is set, its command is run whenever a repository
476 is checked out, or updated. This provides an easy way to do things
477 like permissions fixups, or other tweaks to the repository content,
478 whenever the repository is changed.
479
480 =item VCS_action
481
482 When looking for a command to run for a given action, mr first looks for
483 a parameter with the same name as the action. If that is not found, it
484 looks for a parameter named "VCS_action" (substituting in the name of the
485 version control system and the action).
486
487 Internally, mr has settings for "git_update", "svn_update", etc. To change
488 the action that is performed for a given version control system, you can
489 override these VCS specific actions. To add a new version control system,
490 you can just add VCS specific actions for it.
491
492 =item pre_ and post_
493
494 If a "pre_action" parameter is set, its command is run before mr performs the
495 specified action. Similarly, "post_action" parameters are run after mr
496 successfully performs the specified action. For example, "pre_commit" is
497 run before committing; "post_update" is run after updating.
498
499 =item _append
500
501 Any parameter can be suffixed with C<_append>, to add an additional value
502 to the existing value of the parameter. In this way, actions 
503 can be constructed accumulatively.
504
505 =item VCS_test
506
507 The name of the version control system is itself determined by
508 running each defined "VCS_test" action, until one succeeds.
509
510 =back
511
512 =head1 UNTRUSTED MRCONFIG FILES
513
514 Since mrconfig files can contain arbitrary shell commands, they can do
515 anything. This flexibility is good, but it also allows a malicious mrconfig
516 file to delete your whole home directory. Such a file might be contained
517 inside a repository that your main F<~/.mrconfig> checks out. To
518 avoid worries about evil commands in a mrconfig file, mr defaults to
519 reading all mrconfig files other than the main F<~/.mrconfig> in untrusted
520 mode. In untrusted mode, mrconfig files are limited to running only known
521 safe commands (like "git clone") in a carefully checked manner.
522
523 To configure mr to trust other mrconfig files, list them in F<~/.mrtrust>.
524 One mrconfig file should be listed per line. Either the full pathname
525 should be listed, or the pathname can start with F<~/> to specify a file
526 relative to your home directory.
527
528 =head1 OFFLINE LOG FILE
529
530 The F<~/.mrlog> file contains commands that mr has remembered to run later,
531 due to being offline. You can delete or edit this file to remove commands,
532 or even to add other commands for 'mr online' to run. If the file is
533 present, mr assumes it is in offline mode.
534
535 =head1 EXTENSIONS
536
537 mr can be extended to support things such as unison and git-svn. Some
538 files providing such extensions are available in F</usr/share/mr/>. See
539 the documentation in the files for details about using them.
540
541 =head1 EXIT STATUS
542
543 mr returns nonzero if a command failed in any of the repositories.
544
545 =head1 AUTHOR
546
547 Copyright 2007-2011 Joey Hess <joey@kitenet.net>
548
549 Licensed under the GNU GPL version 2 or higher.
550
551 http://myrepos.branchable.com/
552
553 =cut
554
555 use warnings;
556 use strict;
557 use Getopt::Long;
558 use Cwd qw(getcwd abs_path);
559
560 # things that can happen when mr runs a command
561 use constant {
562         OK => 0,
563         FAILED => 1,
564         SKIPPED => 2,
565         ABORT => 3,
566 };
567
568 # configurables
569 my $config_overridden=0;
570 my $verbose=0;
571 my $quiet=0;
572 my $stats=0;
573 my $force=0;
574 my $insecure=0;
575 my $interactive=0;
576 my $max_depth;
577 my $no_chdir=0;
578 my $jobs=1;
579 my $trust_all=0;
580 my $directory=getcwd();
581 my $terminal=-t STDOUT && eval{require IO::Pty::Easy;IO::Pty::Easy->import();1;} eq 1;
582
583 my $HOME_MR_CONFIG = "$ENV{HOME}/.mrconfig";
584 $ENV{MR_CONFIG}=find_mrconfig();
585
586 # globals :-(
587 my %config;
588 my %configfiles;
589 my %knownactions;
590 my %alias;
591 my (@ok, @failed, @skipped);
592
593 main();
594
595 sub shellquote {
596         my $i=shift;
597         $i=~s/'/'"'"'/g;
598         return "'$i'";
599 }
600
601 # Runs a shell command using a supplied function.
602 # The lib will be included in the shell command line, and any params
603 # will be available in the shell as $1, $2, etc.
604 my $lastlib;
605 sub runsh {
606         my ($action, $topdir, $subdir, $command, $params, $runner) = @_;
607
608         # optimisation: avoid running the shell for true and false
609         if ($command =~ /^\s*true\s*$/) {
610                 $?=0;
611                 return 0;
612         }
613         elsif ($command =~ /^\s*false\s*$/) {
614                 $?=0;
615                 return 1;
616         }
617         
618         my $quotedparams=join(" ", (map { shellquote($_) } @$params));
619         my $lib=exists $config{$topdir}{$subdir}{lib} ?
620                        $config{$topdir}{$subdir}{lib}."\n" : "";
621         if ($verbose && (! defined $lastlib || $lastlib ne $lib)) {
622                 print "mr library now: >>$lib<<\n";
623                 $lastlib=$lib;
624         }
625         my $shellcode="set -e;".$lib.
626                 "my_sh(){ $command\n }; my_sh $quotedparams";
627         print "mr $action: running $action >>$command<<\n" if $verbose;
628         $runner->($shellcode);
629 }
630
631 my %perl_cache;
632 sub perl {
633         my $id=shift;
634         my $s=shift;
635         if ($s =~ m/^perl:\s+(.*)/s) {
636                 return $perl_cache{$1} if exists $perl_cache{$1};
637                 my $sub=eval "sub {$1}";
638                 if (! defined $sub) {
639                         print STDERR "mr: bad perl code in $id: $@\n";
640                 }
641                 return $perl_cache{$1} = $sub;
642         }
643         return undef;
644 }
645
646 my %vcs;
647 sub vcs_test {
648         my ($action, $dir, $topdir, $subdir) = @_;
649
650         if (exists $vcs{$dir}) {
651                 return $vcs{$dir};
652         }
653
654         my $test="";
655         my %perltest;
656         foreach my $vcs_test (
657                         sort {
658                                 length $a <=> length $b 
659                                           ||
660                                        $a cmp $b
661                         } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
662                 my ($vcs)=$vcs_test =~ /(.*)_test/;
663                 my $p=perl($vcs_test, $config{$topdir}{$subdir}{$vcs_test});
664                 if (defined $p) {
665                         $perltest{$vcs}=$p;
666                 }
667                 else {
668                         $test="my_$vcs_test() {\n$config{$topdir}{$subdir}{$vcs_test}\n}\n".$test;
669                         $test.="if my_$vcs_test; then echo $vcs; fi\n";
670                 }
671         }
672
673         my @vcs;
674         foreach my $vcs (keys %perltest) {
675                 if ($perltest{$vcs}->()) {
676                         push @vcs, $vcs;
677                 }
678         }
679
680         push @vcs, split(/\n/,
681                 runsh("vcs test", $topdir, $subdir, $test, [], sub {
682                         my $sh=shift;
683                         my $ret=`$sh`;
684                         return $ret;
685                 })) if length $test;
686         if (@vcs > 1) {
687                 print STDERR "mr $action: found multiple possible repository types (@vcs) for ".fulldir($topdir, $subdir)."\n";
688                 return undef;
689         }
690         if (! @vcs) {
691                 return $vcs{$dir}=undef;
692         }
693         else {
694                 return $vcs{$dir}=$vcs[0];
695         }
696 }
697         
698 sub findcommand {
699         my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
700         
701         if (exists $config{$topdir}{$subdir}{$action}) {
702                 return $config{$topdir}{$subdir}{$action};
703         }
704
705         if ($is_checkout) {
706                 return undef;
707         }
708
709         my $vcs=vcs_test(@_);
710
711         if (defined $vcs && 
712             exists $config{$topdir}{$subdir}{$vcs."_".$action}) {
713                 return $config{$topdir}{$subdir}{$vcs."_".$action};
714         }
715         else {
716                 return undef;
717         }
718 }
719
720 sub fulldir {
721         my ($topdir, $subdir) = @_;
722         return $subdir =~ /^\// ? $subdir : $topdir.$subdir;
723 }
724
725 sub terminal_friendly_spawn {
726         my $actionmsg = shift;
727         my $sh = shift;
728         my $quiet = shift;
729         my $output = "";
730         if ($terminal) {
731                 my $pty = IO::Pty::Easy->new;
732                 $pty->spawn($sh);
733                 while ($pty->is_active) {
734                         my $data = $pty->read();
735                         $output .= $data if defined $data;
736                 }
737                 $pty->close;
738         } else {
739                 $output = qx/$sh 2>&1/;
740         }
741         my $ret = $?;
742         if ($quiet && $ret != 0) {
743                 print "$actionmsg\n" if $actionmsg;
744                 print STDERR $output;
745         } elsif (!$quiet) {
746                 print "$actionmsg\n" if $actionmsg;
747                 print $output;
748         }
749         return $ret;
750 }
751
752 sub action {
753         my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
754         my $fulldir=fulldir($topdir, $subdir);
755         my $checkout_dir;
756
757         $ENV{MR_CONFIG}=$configfiles{$topdir};
758         my $is_checkout=($action eq 'checkout');
759         my $is_update=($action =~ /update/);
760
761         ($ENV{MR_REPO}=$dir) =~ s!/$!!;
762         $ENV{MR_ACTION}=$action;
763         
764         foreach my $testname ("skip", "deleted") {
765                 next if $force && $testname eq "skip";
766
767                 my $testcommand=findcommand($testname, $dir, $topdir, $subdir, $is_checkout);
768
769                 if (defined $testcommand) {
770                         my $ret=runsh "$testname test", $topdir, $subdir,
771                                 $testcommand, [$action],
772                                 sub { system(shift()) };
773                         if ($ret != 0) {
774                                 if (($? & 127) == 2) {
775                                         print STDERR "mr $action: interrupted\n";
776                                         return ABORT;
777                                 }
778                                 elsif ($? & 127) {
779                                         print STDERR "mr $action: $testname test received signal ".($? & 127)."\n";
780                                         return ABORT;
781                                 }
782                         }
783                         if ($ret >> 8 == 0) {
784                                 if ($testname eq "deleted") {
785                                         if (-d $dir) {
786                                                 print STDERR "mr error: $dir should be deleted yet still exists\n";
787                                                 return FAILED;
788                                         }
789                                 }
790                                 print "mr $action: skip $dir skipped\n" if $verbose;
791                                 return SKIPPED;
792                         }
793                 }
794         }
795
796         if ($is_checkout) {
797                 $checkout_dir=$dir;
798                 if (! $force_checkout) {
799                         if (-d $dir) {
800                                 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
801                                 return SKIPPED;
802                         }
803         
804                         $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
805                 }
806         }
807         elsif ($is_update) {
808                 if (! -d $dir) {
809                         return action("checkout", $dir, $topdir, $subdir);
810                 }
811         }
812
813         my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
814
815         if ($is_checkout && ! -d $dir) {
816                 print "mr $action: creating parent directory $dir\n" if $verbose;
817                 system("mkdir", "-p", $dir);
818         }
819
820         if (! $no_chdir && ! chdir($dir)) {
821                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
822                 return FAILED;
823         }
824         elsif (! defined $command) {
825                 my $vcs=vcs_test(@_);
826                 if (! defined $vcs) {
827                         print STDERR "mr $action: unknown repository type and no defined $action command for $fulldir\n";
828                         return FAILED;
829                 }
830                 else {
831                         print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n";
832                         return SKIPPED;
833                 }
834         }
835         else {
836                 my $actionmsg;
837                 if (! $no_chdir) {
838                         $actionmsg="mr $action: $fulldir";
839                 }
840                 else {
841                         my $s=$directory;
842                         $s=~s/^\Q$fulldir\E\/?//;
843                         $actionmsg="mr $action: $fulldir (in subdir $s)";
844                 }
845                 print "$actionmsg\n" unless $quiet;
846
847                 my $hookret=hook("pre_$action", $topdir, $subdir);
848                 return $hookret if $hookret != OK;
849
850                 my $ret=runsh $action, $topdir, $subdir,
851                         $command, \@ARGV, sub {
852                                 my $sh=shift;
853                                 if (!$jobs || $jobs > 1 || $quiet) {
854                                         return terminal_friendly_spawn($actionmsg, $sh, $quiet);
855                                 }
856                                 else {
857                                         system($sh);
858                                 }
859                         };
860                 if ($ret != 0) {
861                         if (($? & 127) == 2) {
862                                 print STDERR "mr $action: interrupted\n";
863                                 return ABORT;
864                         }
865                         elsif ($? & 127) {
866                                 print STDERR "mr $action: received signal ".($? & 127)."\n";
867                                 return ABORT;
868                         }
869                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
870                         if ($ret >> 8 != 0) {
871                                 print STDERR "mr $action: command failed\n";
872                                 if (-e "$ENV{HOME}/.mrlog" && $action ne 'remember') {
873                                         # recreate original command line to
874                                         # remember, and avoid recursing
875                                         my @orig=@ARGV;
876                                         @ARGV=('-n', $action, @orig);
877                                         action("remember", $dir, $topdir, $subdir);
878                                         @ARGV=@orig;
879                                 }
880                         }
881                         elsif ($ret != 0) {
882                                 print STDERR "mr $action: command died ($ret)\n";
883                         }
884                         return FAILED;
885                 }
886                 else {
887                         if ($is_checkout && ! -d $dir) {
888                                 print STDERR "mr $action: $dir missing after checkout\n";;
889                                 return FAILED;
890                         }
891
892                         my $ret=hook("post_$action", $topdir, $subdir);
893                         return $ret if $ret != OK;
894                         
895                         if ($is_checkout || $is_update) {
896                                 if ($is_checkout && ! $no_chdir) {
897                                         if (! chdir($checkout_dir)) {
898                                                 print STDERR "mr $action: failed to chdir to $checkout_dir: $!\n";
899                                                 return FAILED;
900                                         }
901                                 }
902                                 my $ret=hook("fixups", $topdir, $subdir);
903                                 return $ret if $ret != OK;
904                         }
905                         
906                         return OK;
907                 }
908         }
909 }
910
911 sub hook {
912         my ($hook, $topdir, $subdir) = @_;
913
914         my $command=$config{$topdir}{$subdir}{$hook};
915         return OK unless defined $command;
916         my $ret=runsh $hook, $topdir, $subdir, $command, [], sub {
917                         my $sh=shift;
918                         if (!$jobs || $jobs > 1 || $quiet) {
919                                 return terminal_friendly_spawn(undef, $sh, $quiet);
920                         }
921                         else {
922                                 system($sh);
923                         }
924                 };
925         if ($ret != 0) {
926                 if (($? & 127) == 2) {
927                         print STDERR "mr $hook: interrupted\n";
928                         return ABORT;
929                 }
930                 elsif ($? & 127) {
931                         print STDERR "mr $hook: received signal ".($? & 127)."\n";
932                         return ABORT;
933                 }
934                 else {
935                         return FAILED;
936                 }
937         }
938
939         return OK;
940 }
941
942 # run actions on multiple repos, in parallel
943 sub mrs {
944         my $action=shift;
945         my @repos=@_;
946
947         $| = 1;
948         my @active;
949         my @fhs;
950         my @out;
951         my $running=0;
952         while (@fhs or @repos) {
953                 while ((!$jobs || $running < $jobs) && @repos) {
954                         $running++;
955                         my $repo = shift @repos;
956                         pipe(my $outfh, CHILD_STDOUT);
957                         pipe(my $errfh, CHILD_STDERR);
958                         my $pid;
959                         unless ($pid = fork) {
960                                 die "mr $action: cannot fork: $!" unless defined $pid;
961                                 open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
962                                 open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
963                                 close CHILD_STDOUT;
964                                 close CHILD_STDERR;
965                                 close $outfh;
966                                 close $errfh;
967                                 exit action($action, @$repo);
968                         }
969                         close CHILD_STDOUT;
970                         close CHILD_STDERR;
971                         push @active, [$pid, $repo];
972                         push @fhs, [$outfh, $errfh];
973                         push @out, ['',     ''];
974                 }
975                 my ($rin, $rout) = ('','');
976                 my $nfound;
977                 foreach my $fh (@fhs) {
978                         next unless defined $fh;
979                         vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
980                         vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
981                 }
982                 $nfound = select($rout=$rin, undef, undef, 1);
983                 foreach my $channel (0, 1) {
984                         foreach my $i (0..$#fhs) {
985                                 next unless defined $fhs[$i];
986                                 my $fh = $fhs[$i][$channel];
987                                 next unless defined $fh;
988                                 if (vec($rout, fileno($fh), 1) == 1) {
989                                         my $r = '';
990                                         if (sysread($fh, $r, 1024) == 0) {
991                                                 close($fh);
992                                                 $fhs[$i][$channel] = undef;
993                                                 if (! defined $fhs[$i][0] &&
994                                                     ! defined $fhs[$i][1]) {
995                                                         waitpid($active[$i][0], 0);
996                                                         print STDOUT $out[$i][0];
997                                                         print STDERR $out[$i][1];
998                                                         record($active[$i][1], $? >> 8);
999                                                         splice(@fhs, $i, 1);
1000                                                         splice(@active, $i, 1);
1001                                                         splice(@out, $i, 1);
1002                                                         $running--;
1003                                                 }
1004                                         }
1005                                         $out[$i][$channel] .= $r;
1006                                 }
1007                         }
1008                 }
1009         }
1010 }
1011
1012 sub record {
1013         my $dir=shift()->[0];
1014         my $ret=shift;
1015
1016         if ($ret == OK) {
1017                 push @ok, $dir;
1018                 print "\n" unless $quiet;
1019         }
1020         elsif ($ret == FAILED) {
1021                 if ($interactive) {
1022                         chdir($dir) unless $no_chdir;
1023                         print STDERR "mr: Starting interactive shell. Exit shell to continue.\n";
1024                         system((getpwuid($<))[8], "-i");
1025                 }
1026                 push @failed, $dir;
1027                 print "\n";
1028         }
1029         elsif ($ret == SKIPPED) {
1030                 push @skipped, $dir;
1031         }
1032         elsif ($ret == ABORT) {
1033                 exit 1;
1034         }
1035         else {
1036                 die "unknown exit status $ret";
1037         }
1038 }
1039
1040 sub showstats {
1041         my $action=shift;
1042         if (! @ok && ! @failed && ! @skipped) {
1043                 die "mr $action: no repositories found to work on\n";
1044         }
1045         print "mr $action: finished (".join("; ",
1046                 showstat($#ok+1, "ok", "ok"),
1047                 showstat($#failed+1, "failed", "failed"),
1048                 showstat($#skipped+1, "skipped", "skipped"),
1049         ).")\n" unless $quiet;
1050         if ($stats) {
1051                 if (@skipped) {
1052                         print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet;
1053                 }
1054                 if (@failed) {
1055                         print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
1056                 }
1057         }
1058 }
1059
1060 sub showstat {
1061         my $count=shift;
1062         my $singular=shift;
1063         my $plural=shift;
1064         if ($count) {
1065                 return "$count ".($count > 1 ? $plural : $singular);
1066         }
1067         return;
1068 }
1069
1070 # an ordered list of repos
1071 sub repolist {
1072         my @list;
1073         foreach my $topdir (sort keys %config) {
1074                 foreach my $subdir (sort keys %{$config{$topdir}}) {
1075                         push @list, {
1076                                 topdir => $topdir,
1077                                 subdir => $subdir,
1078                                 order => $config{$topdir}{$subdir}{order},
1079                         };
1080                 }
1081         }
1082         return sort {
1083                 $a->{order}  <=> $b->{order}
1084                              ||
1085                 $a->{topdir} cmp $b->{topdir}
1086                              ||
1087                 $a->{subdir} cmp $b->{subdir}
1088         } @list;
1089 }
1090
1091 sub repodir {
1092         my $repo=shift;
1093         my $topdir=$repo->{topdir};
1094         my $subdir=$repo->{subdir};
1095         my $ret=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
1096         $ret=~s/\/\.$//;
1097         return $ret;
1098 }
1099
1100 # Figure out which repos to act on.  Returns a list of array refs
1101 # in the format:
1102 #
1103 #   [ "$full_repo_path/", "$mr_config_path/", $section_header ]
1104 sub selectrepos {
1105         my @repos;
1106         foreach my $repo (repolist()) {
1107                 my $topdir=$repo->{topdir};
1108                 my $subdir=$repo->{subdir};
1109
1110                 next if $subdir eq 'DEFAULT';
1111                 my $dir=repodir($repo);
1112                 my $d=$directory;
1113                 $dir.="/" unless $dir=~/\/$/;
1114                 $d.="/" unless $d=~/\/$/;
1115                 next if $dir ne $d && $dir !~ /^\Q$d\E/;
1116                 if (defined $max_depth) {
1117                         my @a=split('/', $dir);
1118                         my @b=split('/', $d);
1119                         do { } while (@a && @b && shift(@a) eq shift(@b));
1120                         next if @a > $max_depth || @b > $max_depth;
1121                 }
1122                 push @repos, [$dir, $topdir, $subdir];
1123         }
1124         if (! @repos) {
1125                 # fallback to find a leaf repo
1126                 foreach my $repo (reverse repolist()) {
1127                         my $topdir=$repo->{topdir};
1128                         my $subdir=$repo->{subdir};
1129                         
1130                         next if $subdir eq 'DEFAULT';
1131                         my $dir=repodir($repo);
1132                         my $d=$directory;
1133                         $dir.="/" unless $dir=~/\/$/;
1134                         $d.="/" unless $d=~/\/$/;
1135                         if ($d=~/^\Q$dir\E/) {
1136                                 push @repos, [$dir, $topdir, $subdir];
1137                                 last;
1138                         }
1139                 }
1140                 $no_chdir=1;
1141         }
1142         return @repos;
1143 }
1144
1145 sub expandenv {
1146         my $val=shift;
1147         
1148
1149         if ($val=~/\$/) {
1150                 $val=`echo "$val"`;
1151                 chomp $val;
1152         }
1153         
1154         return $val;
1155 }
1156
1157 my %trusted;
1158 sub is_trusted_config {
1159         my $config=shift; # must be abs_pathed already
1160
1161         # We always trust ~/.mrconfig.
1162         return 1 if $config eq abs_path($HOME_MR_CONFIG);
1163
1164         return 1 if $trust_all;
1165
1166         my $trustfile=$ENV{HOME}."/.mrtrust";
1167
1168         if (! %trusted) {
1169                 $trusted{$HOME_MR_CONFIG}=1;
1170                 if (open (TRUST, "<", $trustfile)) {
1171                         while (<TRUST>) {
1172                                 chomp;
1173                                 s/^~\//$ENV{HOME}\//;
1174                                 my $d=abs_path($_);
1175                                 $trusted{$d}=1 if defined $d;
1176                         }
1177                         close TRUST;
1178                 }
1179         }
1180
1181         return $trusted{$config};
1182 }
1183
1184
1185 sub is_trusted_repo {
1186         my $repo=shift;
1187         
1188         # Tightly limit what is allowed in a repo name.
1189         # No ../, no absolute paths, and no unusual filenames
1190         # that might try to escape to the shell.
1191         return $repo =~ /^[-_.+\/A-Za-z0-9]+$/ &&
1192                $repo !~ /\.\./ && $repo !~ /^\//;
1193 }
1194
1195 sub is_trusted_checkout {
1196         my $command=shift;
1197         
1198         # To determine if the command is safe, compare it with the
1199         # *_trusted_checkout config settings. Those settings are
1200         # templates for allowed commands, so make sure that each word
1201         # of the command matches the corresponding word of the template.
1202         
1203         my @words;
1204         foreach my $word (split(' ', $command)) {
1205                 # strip quoting
1206                 if ($word=~/^'(.*)'$/) {
1207                         $word=$1;
1208                 }
1209                 elsif ($word=~/^"(.*)"$/) {
1210                         $word=$1;
1211                 }
1212
1213                 push @words, $word;
1214         }
1215
1216         foreach my $key (grep { /_trusted_checkout$/ }
1217                          keys %{$config{''}{DEFAULT}}) {
1218                 my @twords=split(' ', $config{''}{DEFAULT}{$key});
1219                 next if @words > @twords;
1220
1221                 my $match=1;
1222                 my $url;
1223                 for (my $c=0; $c < @twords && $match; $c++) {
1224                         if ($twords[$c] eq '$url') {
1225                                 # Match all the typical characters found in
1226                                 # urls, plus @ which svn can use. Note
1227                                 # that the "url" might also be a local
1228                                 # directory.
1229                                 $match=(
1230                                         defined $words[$c] &&
1231                                         $words[$c] =~ /^[-_.+:@\/A-Za-z0-9]+$/
1232                                 );
1233                                 $url=$words[$c];
1234                         }
1235                         elsif ($twords[$c] eq '$repo') {
1236                                 # If a repo is not specified, assume it
1237                                 # will be the last path component of the
1238                                 # url, or something derived from it, and
1239                                 # check that.
1240                                 if (! defined $words[$c] && defined $url) {
1241                                         ($words[$c])=$url=~/\/([^\/]+)\/?$/;
1242                                 }
1243
1244                                 $match=(
1245                                         defined $words[$c] &&
1246                                         is_trusted_repo($words[$c])
1247                                 );
1248                         }
1249                         elsif (defined $words[$c] && $words[$c]=~/^($twords[$c])$/) {
1250                                 $match=1;
1251                         }
1252                         else {
1253                                 $match=0;
1254                         }
1255                 }
1256                 return 1 if $match;
1257         }
1258
1259         return 0;
1260 }
1261
1262 my %loaded;
1263 sub loadconfig {
1264         my $f=shift;
1265         my $dir=shift;
1266         my $bootstrap_src=shift;
1267
1268         my @toload;
1269
1270         my $in;
1271         my $trusted;
1272         if (ref $f eq 'GLOB') {
1273                 $dir="";
1274                 $in=$f;
1275                 $trusted=1;
1276         }
1277         else {
1278                 my $absf=abs_path($f);
1279                 if ($loaded{$absf}) {
1280                         return;
1281                 }
1282                 $loaded{$absf}=1;
1283
1284                 $trusted=is_trusted_config($absf);
1285
1286                 if (! defined $dir) {
1287                         ($dir)=$f=~/^(.*\/)[^\/]+$/;
1288                         if (! defined $dir) {
1289                                 $dir=".";
1290                         }
1291                 }
1292
1293                 $dir=abs_path($dir)."/";
1294                 
1295                 if (! exists $configfiles{$dir}) {
1296                         $configfiles{$dir}=$f;
1297                 }
1298
1299                 # copy in defaults from first parent
1300                 my $parent=$dir;
1301                 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
1302                         if ($parent eq '/') {
1303                                 $parent="";
1304                         }
1305                         if (exists $config{$parent} &&
1306                             exists $config{$parent}{DEFAULT}) {
1307                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
1308                                 last;
1309                         }
1310                 }
1311                 
1312                 if (! -e $f) {
1313                         return;
1314                 }
1315
1316                 print "mr: loading config $f\n" if $verbose;
1317                 open($in, "<", $f) || die "mr: open $f: $!\n";
1318         }
1319         my @lines=<$in>;
1320         close $in unless ref $f eq 'GLOB';
1321
1322         my $section;
1323
1324         # Keep track of the current line in the config file;
1325         # when a file is included track the current line from the include.
1326         my $lineno=0;
1327         my $included=undef;
1328
1329         my $line;
1330         my $nextline = sub {
1331                 if ($included) {
1332                         $included--;
1333                 }
1334                 else {
1335                         $included=undef;
1336                         $lineno++;
1337                 }
1338                 $line=shift @lines;
1339                 chomp $line;
1340                 return $line;
1341         };
1342         my $lineerror = sub {
1343                 my $msg=shift;
1344                 if (defined $included) {
1345                         die "mr: $msg at $f line $lineno, included line: $line\n";
1346                 }
1347                 else {
1348                         die "mr: $msg at $f line $lineno\n";
1349                 }
1350         };
1351         my $trusterror = sub {
1352                 my $msg=shift;
1353         
1354                 if (defined $bootstrap_src) {
1355                         die "mr: $msg in untrusted $bootstrap_src line $lineno\n".
1356                                 "(To trust this url, --trust-all can be used; but please use caution;\n".
1357                                 "this can allow arbitrary code execution!)\n";
1358                 }
1359                 else {
1360                         die "mr: $msg in untrusted $f line $lineno\n".
1361                                 "(To trust this file, list it in ~/.mrtrust.)\n";
1362                 }
1363         };
1364
1365         while (@lines) {
1366                 $_=$nextline->();
1367
1368                 next if /^\s*\#/ || /^\s*$/;
1369
1370                 if (! $trusted && /[[:cntrl:]]/) {
1371                         $trusterror->("illegal control character");
1372                 }
1373
1374                 if (/^\[([^\]]*)\]\s*$/) {
1375                         $section=$1;
1376
1377                         if (! $trusted) {
1378                                 if (! is_trusted_repo($section) ||
1379                                     $section eq 'ALIAS' ||
1380                                     $section eq 'DEFAULT') {
1381                                         $trusterror->("illegal section \"[$section]\"");
1382                                 }
1383                         }
1384                         $section=expandenv($section) if $trusted;
1385                         if ($section ne 'ALIAS' &&
1386                             ! exists $config{$dir}{$section} &&
1387                             exists $config{$dir}{DEFAULT}) {
1388                                 # copy in defaults
1389                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
1390                         }
1391                 }
1392                 elsif (/^(\w+)\s*=\s*(.*)/) {
1393                         my $parameter=$1;
1394                         my $value=$2;
1395
1396                         # continued value
1397                         while (@lines && $lines[0]=~/^\s(.+)/) {
1398                                 $value.="\n$1";
1399                                 chomp $value;
1400                                 $nextline->();
1401                         }
1402
1403                         if (! $trusted) {
1404                                 # Untrusted files can only contain a few
1405                                 # settings in specific known-safe formats.
1406                                 if ($parameter eq 'checkout') {
1407                                         if (! is_trusted_checkout($value)) {
1408                                                 $trusterror->("illegal checkout command \"$value\"");
1409                                         }
1410                                 }
1411                                 elsif ($parameter eq 'order') {
1412                                         # not interpreted as a command, so
1413                                         # safe.
1414                                 }
1415                                 elsif ($value eq 'true' || $value eq 'false') {
1416                                         # skip=true , deleted=true etc are
1417                                         # safe.
1418                                 }
1419                                 else {
1420                                         $trusterror->("illegal setting \"$parameter=$value\"");
1421                                 }
1422                         }
1423
1424                         if ($parameter eq "include") {
1425                                 print "mr: including output of \"$value\"\n" if $verbose;
1426                                 my @inc=`$value`;
1427                                 if ($?) {
1428                                         print STDERR "mr: include command exited nonzero ($?)\n";
1429                                 }
1430                                 $included += @inc;
1431                                 unshift @lines, @inc;
1432                                 next;
1433                         }
1434
1435                         if (! defined $section) {
1436                                 $lineerror->("parameter ($parameter) not in section");
1437                         }
1438                         if ($section eq 'ALIAS') {
1439                                 $alias{$parameter}=$value;
1440                         }
1441                         elsif ($parameter eq 'lib' or $parameter =~ s/_append$//) {
1442                                 $config{$dir}{$section}{$parameter}.="\n".$value."\n";
1443                         }
1444                         else {
1445                                 $config{$dir}{$section}{$parameter}=$value;
1446                                 if ($parameter =~ /.*_(.*)/) {
1447                                         $knownactions{$1}=1;
1448                                 }
1449                                 else {
1450                                         $knownactions{$parameter}=1;
1451                                 }
1452                                 if ($parameter eq 'chain' &&
1453                                     length $dir && $section ne "DEFAULT") {
1454                                         my $chaindir="$section";
1455                                         if ($chaindir !~ m!^/!) {
1456                                                 $chaindir=$dir.$chaindir;
1457                                         }
1458                                         if (-e "$chaindir/.mrconfig") {
1459                                                 my $ret=system($value);
1460                                                 if ($ret != 0) {
1461                                                         if (($? & 127) == 2) {
1462                                                                 print STDERR "mr: chain test interrupted\n";
1463                                                                 exit 2;
1464                                                         }
1465                                                         elsif ($? & 127) {
1466                                                                 print STDERR "mr: chain test received signal ".($? & 127)."\n";
1467                                                         }
1468                                                 }
1469                                                 else {
1470                                                         push @toload, ["$chaindir/.mrconfig", $chaindir];
1471                                                 }
1472                                         }
1473                                 }
1474                         }
1475                 }
1476                 else {
1477                         $lineerror->("parse error");
1478                 }
1479         }
1480
1481         foreach my $c (@toload) {
1482                 loadconfig(@$c);
1483         }
1484 }
1485
1486 sub startingconfig {
1487         %alias=%config=%configfiles=%knownactions=%loaded=();
1488         my $datapos=tell(DATA);
1489         loadconfig(\*DATA);
1490         seek(DATA,$datapos,0); # rewind
1491 }
1492
1493 sub modifyconfig {
1494         my $f=shift;
1495         # the section to modify or add
1496         my $targetsection=shift;
1497         # fields to change in the section
1498         # To remove a field, set its value to "".
1499         my %changefields=@_;
1500
1501         my @lines;
1502         my @out;
1503
1504         if (-e $f) {
1505                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
1506                 @lines=<$in>;
1507                 close $in;
1508         }
1509
1510         my $formatfield=sub {
1511                 my $field=shift;
1512                 my @value=split(/\n/, shift);
1513
1514                 return "$field = ".shift(@value)."\n".
1515                         join("", map { "\t$_\n" } @value);
1516         };
1517         my $addfields=sub {
1518                 my @blanks;
1519                 while ($out[$#out] =~ /^\s*$/) {
1520                         unshift @blanks, pop @out;
1521                 }
1522                 foreach my $field (sort keys %changefields) {
1523                         if (length $changefields{$field}) {
1524                                 push @out, "$field = $changefields{$field}\n";
1525                                 delete $changefields{$field};
1526                         }
1527                 }
1528                 push @out, @blanks;
1529         };
1530
1531         my $section;
1532         while (@lines) {
1533                 $_=shift(@lines);
1534
1535                 if (/^\s*\#/ || /^\s*$/) {
1536                         push @out, $_;
1537                 }
1538                 elsif (/^\[([^\]]*)\]\s*$/) {
1539                         if (defined $section && 
1540                             $section eq $targetsection) {
1541                                 $addfields->();
1542                         }
1543
1544                         $section=expandenv($1);
1545
1546                         push @out, $_;
1547                 }
1548                 elsif (/^(\w+)\s*=\s(.*)/) {
1549                         my $parameter=$1;
1550                         my $value=$2;
1551
1552                         # continued value
1553                         while (@lines && $lines[0]=~/^\s(.+)/) {
1554                                 shift(@lines);
1555                                 $value.="\n$1";
1556                                 chomp $value;
1557                         }
1558
1559                         if ($section eq $targetsection) {
1560                                 if (exists $changefields{$parameter}) {
1561                                         if (length $changefields{$parameter}) {
1562                                                 $value=$changefields{$parameter};
1563                                         }
1564                                         delete $changefields{$parameter};
1565                                 }
1566                         }
1567
1568                         push @out, $formatfield->($parameter, $value);
1569                 }
1570         }
1571
1572         if (defined $section && 
1573             $section eq $targetsection) {
1574                 $addfields->();
1575         }
1576         elsif (%changefields) {
1577                 push @out, "\n[$targetsection]\n";
1578                 foreach my $field (sort keys %changefields) {
1579                         if (length $changefields{$field}) {
1580                                 push @out, $formatfield->($field, $changefields{$field});
1581                         }
1582                 }
1583         }
1584
1585         open(my $out, ">", $f) || die "mr: write $f: $!\n";
1586         print $out @out;
1587         close $out;     
1588 }
1589
1590 sub dispatch {
1591         my $action=shift;
1592
1593         # actions that do not operate on all repos
1594         if ($action eq 'config') {
1595                 config(@ARGV);
1596         }
1597         elsif ($action eq 'register') {
1598                 register(@ARGV);
1599         }
1600         elsif ($action eq 'bootstrap') {
1601                 bootstrap();
1602         }
1603         elsif ($action eq 'remember' ||
1604                $action eq 'offline' ||
1605                $action eq 'online') {
1606                 my @repos=selectrepos;
1607                 action($action, @{$repos[0]}) if @repos;
1608                 exit 0;
1609         }
1610
1611         if (!$jobs || $jobs > 1) {
1612                 mrs($action, selectrepos());
1613         }
1614         else {
1615                 foreach my $repo (selectrepos()) {
1616                         record($repo, action($action, @$repo));
1617                 }
1618         }
1619 }
1620
1621 sub help {
1622         my $help=q#
1623                 case `uname -s` in
1624                         SunOS)
1625                         SHOWMANFILE="man -f"
1626                         ;;
1627                         Darwin)
1628                         SHOWMANFILE="man"
1629                         ;;
1630                         *)
1631                         SHOWMANFILE="man"
1632                         ;;
1633                 esac
1634                 if [ ! -e "$MR_PATH" ]; then
1635                         error "cannot find program path"
1636                 fi
1637                 tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
1638                 trap "rm -f $tmp" exit
1639                 pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
1640                 $SHOWMANFILE "$tmp" || error "man failed"
1641         #;
1642         exec($help) || die "exec: $!";
1643 }
1644
1645 sub config {
1646         if (@_ < 2) {
1647                 die "mr config: not enough parameters\n";
1648         }
1649         my $section=shift;
1650         if ($section=~/^\//) {
1651                 # try to convert to a path relative to the config file
1652                 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
1653                 $dir=abs_path($dir);
1654                 $dir.="/" unless $dir=~/\/$/;
1655                 if ($section=~/^\Q$dir\E(.*)/) {
1656                         $section=$1;
1657                 }
1658         }
1659         my %changefields;
1660         foreach (@_) {
1661                 if (/^([^=]+)=(.*)$/) {
1662                         $changefields{$1}=$2;
1663                 }
1664                 else {
1665                         my $found=0;
1666                         foreach my $topdir (sort keys %config) {
1667                                 if (exists $config{$topdir}{$section} &&
1668                                     exists $config{$topdir}{$section}{$_}) {
1669                                         print $config{$topdir}{$section}{$_}."\n";
1670                                         $found=1;
1671                                         last if $section eq 'DEFAULT';
1672                                 }
1673                         }
1674                         if (! $found) {
1675                                 die "mr config: $section $_ not set\n";
1676                         }
1677                 }
1678         }
1679         modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
1680         exit 0;
1681 }
1682
1683 sub register {
1684         if ($config_overridden) {
1685                 # Find the directory that the specified config file is
1686                 # located in.
1687                 ($directory)=abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/;
1688         }
1689         else {
1690                 # Find the closest known mrconfig file to the current
1691                 # directory.
1692                 $directory.="/" unless $directory=~/\/$/;
1693                 my $foundconfig=0;
1694                 foreach my $topdir (reverse sort keys %config) {
1695                         next unless length $topdir;
1696                         if ($directory=~/^\Q$topdir\E/) {
1697                                 $ENV{MR_CONFIG}=$configfiles{$topdir};
1698                                 $directory=$topdir;
1699                                 $foundconfig=1;
1700                                 last;
1701                         }
1702                 }
1703                 if (! $foundconfig) {
1704                         $directory=""; # no config file, use builtin
1705                 }
1706         }
1707         if (@ARGV) {
1708                 my $subdir=shift @ARGV;
1709                 if (! chdir($subdir)) {
1710                         print STDERR "mr register: failed to chdir to $subdir: $!\n";
1711                 }
1712         }
1713
1714         $ENV{MR_REPO}=getcwd();
1715         my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
1716         if (! defined $command) {
1717                 die "mr register: unknown repository type\n";
1718         }
1719
1720         $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
1721         $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n".
1722                 "my_action(){ $command\n }; my_action ".
1723                 join(" ", map { s/\\/\\\\/g; s/"/\"/g; '"'.$_.'"' } @ARGV);
1724         print "mr register: running >>$command<<\n" if $verbose;
1725         exec($command) || die "exec: $!";
1726 }
1727
1728 sub bootstrap {
1729         eval q{use File::Copy};
1730         die $@ if $@;
1731
1732         my $src=shift @ARGV;
1733         my $dir=shift @ARGV || ".";
1734         
1735         if (! defined $src || ! length $src) {
1736                 die "mr: bootstrap requires source\n";
1737         }
1738
1739         # Retrieve config file.
1740         eval q{use File::Temp};
1741         die $@ if $@;
1742         my $tmpconfig=File::Temp->new();
1743         if ($src =~ m!^[\w\d]+://!) {
1744                 # Download the config file to a temporary location.
1745                 my @downloader;
1746                 if ($src =~ m!^ssh://(.*)!) {
1747                         @downloader = ("scp", $1, $tmpconfig);
1748                 }
1749                 else {
1750                         @downloader = ("curl", "-A", "mr", "-L", "-s", $src, "-o", $tmpconfig);
1751                         push(@downloader, "-k") if $insecure;
1752                 }
1753                 my $status = system(@downloader);
1754                 die "mr bootstrap: invalid SSL certificate for $src (consider -k)\n"
1755                         if $downloader[0] eq 'curl' && $status >> 8 == 60;
1756                 die "mr bootstrap: download of $src failed\n" if $status != 0;
1757         }
1758         elsif ($src eq '-') {
1759                 # Config file is read from stdin.
1760                 copy(\*STDIN, $tmpconfig) || die "stdin: $!";
1761         }
1762         else {
1763                 # Config file is local.
1764                 die "mr bootstrap: cannot read file '$src'"
1765                         unless -r $src;
1766                 copy($src, $tmpconfig) || die "copy: $!";
1767         }
1768
1769         # Sanity check on destination directory.
1770         if (! -e $dir) {
1771                 system("mkdir", "-p", $dir);
1772         }
1773         chdir($dir) || die "chdir $dir: $!";
1774
1775         # Special case to handle checkout of the "." repo, which 
1776         # would normally be skipped.
1777         my $topdir=abs_path(".")."/";
1778         my @repo=($topdir, $topdir, ".");
1779         loadconfig($tmpconfig, $topdir, $src);
1780         record(\@repo, action("checkout", @repo, 1))
1781                 if exists $config{$topdir}{"."}{"checkout"};
1782
1783         if (-e ".mrconfig") {
1784                 print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $src\n";
1785         }
1786         else {
1787                 move($tmpconfig, ".mrconfig") || die "rename: $!";
1788         }
1789
1790         # Reload the config file (in case we got a different version)
1791         # and checkout everything else.
1792         startingconfig();
1793         loadconfig(".mrconfig");
1794         dispatch("checkout");
1795         @skipped=grep { abs_path($_) ne abs_path($topdir) } @skipped;
1796         showstats("bootstrap");
1797         exitstats();
1798 }
1799
1800 # alias expansion and command stemming
1801 sub expandaction {
1802         my $action=shift;
1803         if (exists $alias{$action}) {
1804                 $action=$alias{$action};
1805         }
1806         if (! exists $knownactions{$action}) {
1807                 my @matches = grep { /^\Q$action\E/ }
1808                         keys %knownactions, keys %alias;
1809                 if (@matches == 1) {
1810                         $action=$matches[0];
1811                 }
1812                 elsif (@matches == 0) {
1813                         die "mr: unknown action \"$action\" (known actions: ".
1814                                 join(", ", sort keys %knownactions).")\n";
1815                 }
1816                 else {
1817                         die "mr: ambiguous action \"$action\" (matches: ".
1818                                 join(", ", @matches).")\n";
1819                 }
1820         }
1821         return $action;
1822 }
1823
1824 sub find_mrconfig {
1825         my $dir=getcwd();
1826         while (length $dir) {
1827                 if (-e "$dir/.mrconfig") {
1828                         return "$dir/.mrconfig";
1829                 }
1830                 $dir=~s/\/[^\/]*$//;
1831         }
1832         return $HOME_MR_CONFIG;
1833 }
1834
1835 sub getopts {
1836         my @saved=@ARGV;
1837         Getopt::Long::Configure("bundling", "no_permute");
1838         my $result=GetOptions(
1839                 "d|directory=s" => sub { $directory=abs_path($_[1]) },
1840                 "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
1841                 "p|path" => sub { }, # now default, ignore
1842                 "f|force" => \$force,
1843                 "v|verbose" => \$verbose,
1844                 "q|quiet" => \$quiet,
1845                 "s|stats" => \$stats,
1846                 "k|insecure" => \$insecure,
1847                 "i|interactive" => \$interactive,
1848                 "n|no-recurse:i" => \$max_depth,
1849                 "j|jobs:i" => \$jobs,
1850                 "t|trust-all" => \$trust_all,
1851         );
1852         if (! $result || @ARGV < 1) {
1853                 die("Usage: mr [options] action [params ...]\n".
1854                     "(Use mr help for man page.)\n");
1855         }
1856         
1857         $ENV{MR_SWITCHES}="";
1858         foreach my $option (@saved) {
1859                 last if $option eq $ARGV[0];
1860                 $ENV{MR_SWITCHES}.="$option ";
1861         }
1862 }
1863
1864 sub init {
1865         $SIG{INT}=sub {
1866                 print STDERR "mr: interrupted\n";
1867                 exit 2;
1868         };
1869         
1870         # This can happen if it's run in a directory that was removed
1871         # or other strangeness.
1872         if (! defined $directory) {
1873                 die("mr: failed to determine working directory\n");
1874         }
1875         # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
1876         # the config file might be a symlink to elsewhere, and the directory it's
1877         # in is significant.
1878         if ($ENV{MR_CONFIG} !~ /^\//) {
1879                 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
1880         }
1881         # Try to set MR_PATH to the path to the program.
1882         eval {
1883                 use FindBin qw($Bin $Script);
1884                 $ENV{MR_PATH}=$Bin."/".$Script;
1885         };
1886 }
1887         
1888 sub exitstats {
1889         if (@failed) {
1890                 exit 1;
1891         }
1892         else {
1893                 exit 0;
1894         }
1895 }
1896
1897 sub main {
1898         getopts();
1899         init();
1900         help(@ARGV) if $ARGV[0] eq 'help';
1901
1902         startingconfig();
1903         loadconfig($HOME_MR_CONFIG);
1904         loadconfig($ENV{MR_CONFIG});
1905         #use Data::Dumper; print Dumper(\%config);
1906         
1907         my $action=expandaction(shift @ARGV);
1908         dispatch($action);
1909
1910         showstats($action);
1911         exitstats();
1912 }
1913
1914 # Finally, some useful actions that mr knows about by default.
1915 # These can be overridden in ~/.mrconfig.
1916 __DATA__
1917 [ALIAS]
1918 co = checkout
1919 ci = commit
1920 ls = list
1921
1922 [DEFAULT]
1923 order = 10
1924 lib =
1925         error() {
1926                 echo "mr: $@" >&2
1927                 exit 1
1928         }
1929         warning() {
1930                 echo "mr (warning): $@" >&2
1931         }
1932         info() {
1933                 echo "mr: $@" >&2
1934         }
1935         hours_since() {
1936                 if [ -z "$1" ] || [ -z "$2" ]; then
1937                         error "mr: usage: hours_since action num"
1938                 fi
1939                 for dir in .git .svn .bzr CVS .hg _darcs _FOSSIL_; do
1940                         if [ -e "$MR_REPO/$dir" ]; then
1941                                 flagfile="$MR_REPO/$dir/.mr_last$1"
1942                                 break
1943                         fi
1944                 done
1945                 if [ -z "$flagfile" ]; then
1946                         error "cannot determine flag filename"
1947                 fi
1948                 delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
1949                 if [ "$delta" -lt "$2" ]; then
1950                         return 1
1951                 else
1952                         touch "$flagfile"
1953                         return 0
1954                 fi
1955         }
1956         is_bzr_checkout() {
1957                 LANG=C bzr info | egrep -q '^Checkout'
1958         }
1959         lazy() {
1960                 if [ -d "$MR_REPO" ]; then
1961                         return 1
1962                 else
1963                         return 0
1964                 fi
1965         }
1966
1967 svn_test = perl: -d "$ENV{MR_REPO}/.svn"
1968 git_test = perl: -e "$ENV{MR_REPO}/.git"
1969 bzr_test = perl: -d "$ENV{MR_REPO}/.bzr"
1970 cvs_test = perl: -d "$ENV{MR_REPO}/CVS"
1971 hg_test  = perl: -d "$ENV{MR_REPO}/.hg"
1972 darcs_test = perl: -d "$ENV{MR_REPO}/_darcs"
1973 fossil_test = perl: -f "$ENV{MR_REPO}/_FOSSIL_"
1974 git_bare_test = perl: 
1975         -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
1976         -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
1977         `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get core.bare` =~ /true/
1978 vcsh_test = perl:
1979         -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
1980         -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
1981         `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get vcsh.vcsh` =~ /true/
1982 veracity_test  = perl: -d "$ENV{MR_REPO}/.sgdrawer"
1983
1984 svn_update = svn update "$@"
1985 git_update = git pull "$@"
1986 bzr_update = 
1987         if is_bzr_checkout; then
1988                 bzr update "$@"
1989         else
1990                 bzr merge --pull "$@"
1991         fi
1992 cvs_update = cvs -q update "$@"
1993 hg_update  = hg pull "$@"; hg update "$@"
1994 darcs_update = darcs pull -a "$@"
1995 fossil_update = fossil pull "$@"
1996 vcsh_update = vcsh run "$MR_REPO" git pull "$@"
1997 veracity_update = vv pull "$@" && vv update "$@"
1998
1999 git_fetch = git fetch --all --prune --tags
2000 git_svn_fetch = git svn fetch
2001 darcs_fetch = darcs fetch
2002 hg_fetch = hg pull
2003
2004 svn_status = svn status "$@"
2005 git_status = git status -s "$@" || true; git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline || true
2006 bzr_status = bzr status --short "$@"; bzr missing
2007 cvs_status = cvs -q status | grep -E '^(File:.*Status:|\?)' | grep -v 'Status: Up-to-date'
2008 hg_status  = hg status "$@"; hg summary --quiet | grep -v 'parent: 0:'
2009 darcs_status = darcs whatsnew -ls "$@" || true
2010 fossil_status = fossil changes "$@"
2011 vcsh_status = vcsh run "$MR_REPO" git -c status.relativePaths=false status -s "$@" || true
2012 veracity_status = vv status "$@"
2013
2014 svn_commit = svn commit "$@"
2015 git_commit = git commit -a "$@" && git push --all
2016 bzr_commit = 
2017         if is_bzr_checkout; then
2018                 bzr commit "$@"
2019         else
2020                 bzr commit "$@" && bzr push
2021         fi
2022 cvs_commit = cvs commit "$@"
2023 hg_commit  = hg commit "$@" && hg push
2024 darcs_commit = darcs record -a "$@" && darcs push -a
2025 fossil_commit = fossil commit "$@"
2026 vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all
2027 veracity_commit = vv commit "$@" && vv push
2028
2029 git_record = git commit -a "$@"
2030 bzr_record =
2031         if is_bzr_checkout; then
2032                 bzr commit --local "$@"
2033         else
2034                 bzr commit "$@"
2035         fi
2036 hg_record  = hg commit "$@"
2037 darcs_record = darcs record -a "$@"
2038 fossil_record = fossil commit "$@"
2039 vcsh_record = vcsh run "$MR_REPO" git commit -a "$@"
2040 veracity_record = vv commit "$@"
2041
2042 svn_push = :
2043 git_push = git push "$@"
2044 bzr_push = bzr push "$@"
2045 cvs_push = :
2046 hg_push = hg push "$@"
2047 darcs_push = darcs push -a "$@"
2048 fossil_push = fossil push "$@"
2049 vcsh_push = vcsh run "$MR_REPO" git push "$@"
2050 veracity_push = vv push "$@"
2051
2052 svn_diff = svn diff "$@"
2053 git_diff = git diff "$@"
2054 bzr_diff = bzr diff "$@"
2055 cvs_diff = cvs -q diff "$@"
2056 hg_diff  = hg diff "$@"
2057 darcs_diff = darcs diff -u "$@"
2058 fossil_diff = fossil diff "$@"
2059 vcsh_diff = vcsh run "$MR_REPO" git diff "$@"
2060 veracity_diff = vv diff "$@"
2061
2062 svn_log = svn log "$@"
2063 git_log = git log "$@"
2064 bzr_log = bzr log "$@"
2065 cvs_log = cvs log "$@"
2066 hg_log  = hg log "$@"
2067 darcs_log = darcs changes "$@"
2068 git_bare_log = git log "$@"
2069 fossil_log = fossil timeline "$@"
2070 vcsh_log = vcsh run "$MR_REPO" git log "$@"
2071 veracity_log = vv log "$@"
2072
2073 hg_grep = hg grep "$@"
2074 cvs_grep = ack-grep "$@"
2075 svn_grep = ack-grep "$@"
2076 git_svn_grep = git grep "$@"
2077 git_grep = git grep "$@"
2078 bzr_grep = ack-grep "$@"
2079
2080 run = "$@"
2081
2082 svn_register =
2083         url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
2084         if [ -z "$url" ]; then
2085                 error "cannot determine svn url"
2086         fi
2087         echo "Registering svn url: $url in $MR_CONFIG"
2088         mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
2089 git_register = 
2090         url="`LC_ALL=C git config --get remote.origin.url`" || true
2091         if [ -z "$url" ]; then
2092                 error "cannot determine git url"
2093         fi
2094         echo "Registering git url: $url in $MR_CONFIG"
2095         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
2096 bzr_register =
2097         url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}' | head -n 1`"
2098         if [ -z "$url" ]; then
2099                 error "cannot determine bzr url"
2100         fi
2101         echo "Registering bzr url: $url in $MR_CONFIG"
2102         mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr branch '$url' '$MR_REPO'"
2103 cvs_register =
2104         repo=`cat CVS/Repository`
2105         root=`cat CVS/Root`
2106         if [ -z "$root" ]; then
2107                 error "cannot determine cvs root"
2108                 fi
2109         echo "Registering cvs repository $repo at root $root"
2110         mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
2111 hg_register = 
2112         url=`hg showconfig paths.default`
2113         echo "Registering mercurial repo url: $url in $MR_CONFIG"
2114         mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
2115 darcs_register = 
2116         url=`cat _darcs/prefs/defaultrepo`
2117         echo "Registering darcs repository $url in $MR_CONFIG"
2118         mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
2119 git_bare_register = 
2120         url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
2121         if [ -z "$url" ]; then
2122                 error "cannot determine git url"
2123         fi
2124         echo "Registering git url: $url in $MR_CONFIG"
2125         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
2126 vcsh_register =
2127         url="`LC_ALL=C vcsh run "$MR_REPO" git config --get remote.origin.url`" || true
2128         if [ -z "$url" ]; then
2129                 error "cannot determine git url"
2130         fi
2131         echo "Registering git url: $url in $MR_CONFIG"
2132         mr -c "$MR_CONFIG" config "`pwd`" checkout="vcsh clone '$url' '$MR_REPO'"
2133 fossil_register =
2134         url=`fossil remote-url`
2135         repo=`fossil info | grep repository | sed -e 's/repository:*.//g' -e 's/ //g'`
2136         echo "Registering fossil repository $url in $MR_CONFIG"
2137         mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && fossil open '$repo'"
2138 veracity_register =
2139         url=`vv config | grep sync_targets | sed -e 's/sync_targets:*.//g' -e 's/ //g'`
2140         repo=`vv repo info | grep repository | sed -e 's/Current repository:*.//g' -e 's/ //g'`
2141         echo "Registering veracity repository $url in $MR_CONFIG"
2142         mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && vv checkout '$repo'"
2143
2144 svn_trusted_checkout = svn co $url $repo
2145 svn_alt_trusted_checkout = svn checkout $url $repo
2146 git_trusted_checkout = git clone $url $repo
2147 bzr_trusted_checkout = bzr checkout|clone|branch|get $url $repo
2148 # cvs: too hard
2149 hg_trusted_checkout = hg clone $url $repo
2150 darcs_trusted_checkout = darcs get $url $repo
2151 git_bare_trusted_checkout = git clone --bare $url $repo
2152 vcsh_old_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
2153 vcsh_trusted_checkout = vcsh clone $url $repo
2154 # fossil: messy to do
2155 veracity_trusted_checkout = vv clone $url $repo
2156
2157
2158 list = true
2159 config = 
2160 bootstrap = 
2161
2162 online =
2163         if [ -s ~/.mrlog ]; then
2164                 info "running offline commands"
2165                 mv -f ~/.mrlog ~/.mrlog.old
2166                 if ! sh -e ~/.mrlog.old; then
2167                         error "offline command failed; left in ~/.mrlog.old"
2168                 fi
2169                 rm -f ~/.mrlog.old
2170         else
2171                 info "no offline commands to run"
2172         fi
2173 offline =
2174         umask 077
2175         touch ~/.mrlog
2176         info "offline mode enabled"
2177 remember =
2178         info "remembering command: 'mr $@'"
2179         command="mr -d '$(pwd)' $MR_SWITCHES"
2180         for w in "$@"; do
2181                 command="$command '$w'"
2182         done
2183         if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
2184                 echo "$command" >> ~/.mrlog
2185         fi
2186
2187 ed = echo "A horse is a horse, of course, of course.."
2188 T = echo "I pity the fool."
2189 right = echo "Not found."
2190
2191 # vim:sw=8:sts=0:ts=8:noet
2192 # Local variables:
2193 # indent-tabs-mode: t
2194 # cperl-indent-level: 8
2195 # End: