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

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