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

chdir() to config directory before including
[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                         if (exists $config{$parent} &&
1321                             exists $config{$parent}{DEFAULT}) {
1322                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
1323                                 last;
1324                         }
1325                 }
1326                 
1327                 if (! -e $f) {
1328                         return;
1329                 }
1330
1331                 if ($f =~ /\//) {
1332                         print "mr: loading config $f\n" if $verbose;
1333                 } else {
1334                         print "mr: loading config $f (from ".getcwd().")\n" if $verbose;
1335                 }
1336
1337                 open($in, "<", $f) || die "mr: open $f: $!\n";
1338         }
1339         my @lines=<$in>;
1340         close $in unless ref $f eq 'GLOB';
1341
1342         my $section;
1343
1344         # Keep track of the current line in the config file;
1345         # when a file is included track the current line from the include.
1346         my $lineno=0;
1347         my $included=undef;
1348
1349         my $line;
1350         my $nextline = sub {
1351                 if ($included) {
1352                         $included--;
1353                 }
1354                 else {
1355                         $included=undef;
1356                         $lineno++;
1357                 }
1358                 $line=shift @lines;
1359                 chomp $line;
1360                 return $line;
1361         };
1362         my $lineerror = sub {
1363                 my $msg=shift;
1364                 if (defined $included) {
1365                         die "mr: $msg at $f line $lineno, included line: $line\n";
1366                 }
1367                 else {
1368                         die "mr: $msg at $f line $lineno\n";
1369                 }
1370         };
1371         my $trusterror = sub {
1372                 my $msg=shift;
1373         
1374                 if (defined $bootstrap_src) {
1375                         die "mr: $msg in untrusted $bootstrap_src line $lineno\n".
1376                                 "(To trust this url, --trust-all can be used; but please use caution;\n".
1377                                 "this can allow arbitrary code execution!)\n";
1378                 }
1379                 else {
1380                         die "mr: $msg in untrusted $absf line $lineno\n".
1381                                 "(To trust this file, list it in ~/.mrtrust.)\n";
1382                 }
1383         };
1384
1385         while (@lines) {
1386                 $_=$nextline->();
1387
1388                 next if /^\s*\#/ || /^\s*$/;
1389
1390                 if (! $trusted && /[[:cntrl:]]/) {
1391                         $trusterror->("illegal control character");
1392                 }
1393
1394                 if (/^\[([^\]]*)\]\s*$/) {
1395                         $section=$1;
1396
1397                         if (! $trusted) {
1398                                 if (! is_trusted_repo($section) ||
1399                                     $section eq 'ALIAS' ||
1400                                     $section eq 'DEFAULT') {
1401                                         $trusterror->("illegal section \"[$section]\"");
1402                                 }
1403                         }
1404                         $section=expandenv($section) if $trusted;
1405                         if ($section ne 'ALIAS' &&
1406                             ! exists $config{$dir}{$section} &&
1407                             exists $config{$dir}{DEFAULT}) {
1408                                 # copy in defaults
1409                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
1410                         }
1411                 }
1412                 elsif (/^(\w+)\s*=\s*(.*)/) {
1413                         my $parameter=$1;
1414                         my $value=$2;
1415
1416                         # continued value
1417                         while (@lines && $lines[0]=~/^\s(.+)/) {
1418                                 $value.="\n$1";
1419                                 chomp $value;
1420                                 $nextline->();
1421                         }
1422
1423                         if (! $trusted) {
1424                                 # Untrusted files can only contain a few
1425                                 # settings in specific known-safe formats.
1426                                 if ($parameter eq 'checkout') {
1427                                         if (! is_trusted_checkout($value)) {
1428                                                 $trusterror->("illegal checkout command \"$value\"");
1429                                         }
1430                                 }
1431                                 elsif ($parameter eq 'order') {
1432                                         # not interpreted as a command, so
1433                                         # safe.
1434                                 }
1435                                 elsif ($value eq 'true' || $value eq 'false') {
1436                                         # skip=true , deleted=true etc are
1437                                         # safe.
1438                                 }
1439                                 else {
1440                                         $trusterror->("illegal setting \"$parameter=$value\"");
1441                                 }
1442                         }
1443
1444                         if ($parameter eq "include") {
1445                                 print "mr: including output of \"$value\"\n" if $verbose;
1446                                 my @inc=`$value`;
1447                                 if ($?) {
1448                                         print STDERR "mr: include command exited nonzero ($?)\n";
1449                                 }
1450                                 $included += @inc;
1451                                 unshift @lines, @inc;
1452                                 next;
1453                         }
1454
1455                         if (! defined $section) {
1456                                 $lineerror->("parameter ($parameter) not in section");
1457                         }
1458                         if ($section eq 'ALIAS') {
1459                                 $alias{$parameter}=$value;
1460                         }
1461                         elsif ($parameter eq 'lib' or $parameter =~ s/_append$//) {
1462                                 $config{$dir}{$section}{$parameter}.="\n".$value."\n";
1463                         }
1464                         else {
1465                                 $config{$dir}{$section}{$parameter}=$value;
1466                                 if ($parameter =~ /.*_(.*)/) {
1467                                         $knownactions{$1}=1;
1468                                 }
1469                                 else {
1470                                         $knownactions{$parameter}=1;
1471                                 }
1472                                 if ($parameter eq 'chain' &&
1473                                     length $dir && $section ne "DEFAULT") {
1474                                         my $chaindir="$section";
1475                                         if ($chaindir !~ m!^/!) {
1476                                                 $chaindir=$dir.$chaindir;
1477                                         }
1478                                         if (-e "$chaindir/.mrconfig") {
1479                                                 my $ret=system($value);
1480                                                 if ($ret != 0) {
1481                                                         if (($? & 127) == 2) {
1482                                                                 print STDERR "mr: chain test interrupted\n";
1483                                                                 exit 2;
1484                                                         }
1485                                                         elsif ($? & 127) {
1486                                                                 print STDERR "mr: chain test received signal ".($? & 127)."\n";
1487                                                         }
1488                                                 }
1489                                                 else {
1490                                                         push @toload, ["$chaindir/.mrconfig", $chaindir];
1491                                                 }
1492                                         }
1493                                 }
1494                         }
1495                 }
1496                 else {
1497                         $lineerror->("parse error");
1498                 }
1499         }
1500
1501         foreach my $c (@toload) {
1502                 loadconfig(@$c);
1503         }
1504 }
1505
1506 sub startingconfig {
1507         %alias=%config=%configfiles=%knownactions=%loaded=();
1508         my $datapos=tell(DATA);
1509         loadconfig(\*DATA);
1510         seek(DATA,$datapos,0); # rewind
1511 }
1512
1513 sub modifyconfig {
1514         my $f=shift;
1515         # the section to modify or add
1516         my $targetsection=shift;
1517         # fields to change in the section
1518         # To remove a field, set its value to "".
1519         my %changefields=@_;
1520
1521         my @lines;
1522         my @out;
1523
1524         if (-e $f) {
1525                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
1526                 @lines=<$in>;
1527                 close $in;
1528         }
1529
1530         my $formatfield=sub {
1531                 my $field=shift;
1532                 my @value=split(/\n/, shift);
1533
1534                 return "$field = ".shift(@value)."\n".
1535                         join("", map { "\t$_\n" } @value);
1536         };
1537         my $addfields=sub {
1538                 my @blanks;
1539                 while ($out[$#out] =~ /^\s*$/) {
1540                         unshift @blanks, pop @out;
1541                 }
1542                 foreach my $field (sort keys %changefields) {
1543                         if (length $changefields{$field}) {
1544                                 push @out, "$field = $changefields{$field}\n";
1545                                 delete $changefields{$field};
1546                         }
1547                 }
1548                 push @out, @blanks;
1549         };
1550
1551         my $section;
1552         while (@lines) {
1553                 $_=shift(@lines);
1554
1555                 if (/^\s*\#/ || /^\s*$/) {
1556                         push @out, $_;
1557                 }
1558                 elsif (/^\[([^\]]*)\]\s*$/) {
1559                         if (defined $section && 
1560                             $section eq $targetsection) {
1561                                 $addfields->();
1562                         }
1563
1564                         $section=expandenv($1);
1565
1566                         push @out, $_;
1567                 }
1568                 elsif (/^(\w+)\s*=\s(.*)/) {
1569                         my $parameter=$1;
1570                         my $value=$2;
1571
1572                         # continued value
1573                         while (@lines && $lines[0]=~/^\s(.+)/) {
1574                                 shift(@lines);
1575                                 $value.="\n$1";
1576                                 chomp $value;
1577                         }
1578
1579                         if ($section eq $targetsection) {
1580                                 if (exists $changefields{$parameter}) {
1581                                         if (length $changefields{$parameter}) {
1582                                                 $value=$changefields{$parameter};
1583                                         }
1584                                         delete $changefields{$parameter};
1585                                 }
1586                         }
1587
1588                         push @out, $formatfield->($parameter, $value);
1589                 }
1590         }
1591
1592         if (defined $section && 
1593             $section eq $targetsection) {
1594                 $addfields->();
1595         }
1596         elsif (%changefields) {
1597                 push @out, "\n[$targetsection]\n";
1598                 foreach my $field (sort keys %changefields) {
1599                         if (length $changefields{$field}) {
1600                                 push @out, $formatfield->($field, $changefields{$field});
1601                         }
1602                 }
1603         }
1604
1605         open(my $out, ">", $f) || die "mr: write $f: $!\n";
1606         print $out @out;
1607         close $out;     
1608 }
1609
1610 sub dispatch {
1611         my $action=shift;
1612
1613         # actions that do not operate on all repos
1614         if ($action eq 'config') {
1615                 config(@ARGV);
1616         }
1617         elsif ($action eq 'register') {
1618                 register(@ARGV);
1619         }
1620         elsif ($action eq 'bootstrap') {
1621                 bootstrap();
1622         }
1623         elsif ($action eq 'remember' ||
1624                $action eq 'offline' ||
1625                $action eq 'online') {
1626                 my @repos=selectrepos;
1627                 action($action, @{$repos[0]}) if @repos;
1628                 exit 0;
1629         }
1630
1631         if (!$jobs || $jobs > 1) {
1632                 mrs($action, selectrepos());
1633         }
1634         else {
1635                 foreach my $repo (selectrepos()) {
1636                         record($repo, action($action, @$repo));
1637                 }
1638         }
1639 }
1640
1641 sub help {
1642         my $help=q#
1643                 case `uname -s` in
1644                         SunOS)
1645                         SHOWMANFILE="man -f"
1646                         ;;
1647                         Darwin)
1648                         SHOWMANFILE="man"
1649                         ;;
1650                         *)
1651                         SHOWMANFILE="man"
1652                         ;;
1653                 esac
1654                 if [ ! -e "$MR_PATH" ]; then
1655                         error "cannot find program path"
1656                 fi
1657                 tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
1658                 trap "rm -f $tmp" exit
1659                 pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
1660                 $SHOWMANFILE "$tmp" || error "man failed"
1661         #;
1662         exec($help) || die "exec: $!";
1663 }
1664
1665 sub config {
1666         if (@_ < 2) {
1667                 die "mr config: not enough parameters\n";
1668         }
1669         my $section=shift;
1670         if ($section=~/^\//) {
1671                 # try to convert to a path relative to the config file
1672                 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
1673                 $dir=abs_path($dir);
1674                 $dir.="/" unless $dir=~/\/$/;
1675                 if ($section=~/^\Q$dir\E(.*)/) {
1676                         $section=$1;
1677                 }
1678         }
1679         my %changefields;
1680         foreach (@_) {
1681                 if (/^([^=]+)=(.*)$/) {
1682                         $changefields{$1}=$2;
1683                 }
1684                 else {
1685                         my $found=0;
1686                         foreach my $topdir (sort keys %config) {
1687                                 if (exists $config{$topdir}{$section} &&
1688                                     exists $config{$topdir}{$section}{$_}) {
1689                                         print $config{$topdir}{$section}{$_}."\n";
1690                                         $found=1;
1691                                         last if $section eq 'DEFAULT';
1692                                 }
1693                         }
1694                         if (! $found) {
1695                                 die "mr config: $section $_ not set\n";
1696                         }
1697                 }
1698         }
1699         modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
1700         exit 0;
1701 }
1702
1703 sub register {
1704         if ($config_overridden) {
1705                 # Find the directory that the specified config file is
1706                 # located in.
1707                 ($directory)=abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/;
1708         }
1709         else {
1710                 # Find the closest known mrconfig file to the current
1711                 # directory.
1712                 $directory.="/" unless $directory=~/\/$/;
1713                 my $foundconfig=0;
1714                 foreach my $topdir (reverse sort keys %config) {
1715                         next unless length $topdir;
1716                         if ($directory=~/^\Q$topdir\E/) {
1717                                 $ENV{MR_CONFIG}=$configfiles{$topdir};
1718                                 $directory=$topdir;
1719                                 $foundconfig=1;
1720                                 last;
1721                         }
1722                 }
1723                 if (! $foundconfig) {
1724                         $directory=""; # no config file, use builtin
1725                 }
1726         }
1727         if (@ARGV) {
1728                 my $subdir=shift @ARGV;
1729                 if (! chdir($subdir)) {
1730                         print STDERR "mr register: failed to chdir to $subdir: $!\n";
1731                 }
1732         }
1733
1734         $ENV{MR_REPO}=getcwd();
1735         my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
1736         if (! defined $command) {
1737                 die "mr register: unknown repository type\n";
1738         }
1739
1740         $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
1741         $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n".
1742                 "my_action(){ $command\n }; my_action ".
1743                 join(" ", map { s/\\/\\\\/g; s/"/\"/g; '"'.$_.'"' } @ARGV);
1744         print "mr register: running >>$command<<\n" if $verbose;
1745         exec($command) || die "exec: $!";
1746 }
1747
1748 sub bootstrap {
1749         eval q{use File::Copy};
1750         die $@ if $@;
1751
1752         my $src=shift @ARGV;
1753         my $dir=shift @ARGV || ".";
1754         
1755         if (! defined $src || ! length $src) {
1756                 die "mr: bootstrap requires source\n";
1757         }
1758
1759         # Retrieve config file.
1760         eval q{use File::Temp};
1761         die $@ if $@;
1762         my $tmpconfig=File::Temp->new();
1763         if ($src =~ m!^[\w\d]+://!) {
1764                 # Download the config file to a temporary location.
1765                 my @downloader;
1766                 if ($src =~ m!^ssh://(.*)!) {
1767                         @downloader = ("scp", $1, $tmpconfig);
1768                 }
1769                 else {
1770                         @downloader = ("curl", "-A", "mr", "-L", "-s", $src, "-o", $tmpconfig);
1771                         push(@downloader, "-k") if $insecure;
1772                 }
1773                 my $status = system(@downloader);
1774                 die "mr bootstrap: invalid SSL certificate for $src (consider -k)\n"
1775                         if $downloader[0] eq 'curl' && $status >> 8 == 60;
1776                 die "mr bootstrap: download of $src failed\n" if $status != 0;
1777         }
1778         elsif ($src eq '-') {
1779                 # Config file is read from stdin.
1780                 copy(\*STDIN, $tmpconfig) || die "stdin: $!";
1781         }
1782         else {
1783                 # Config file is local.
1784                 die "mr bootstrap: cannot read file '$src'"
1785                         unless -r $src;
1786                 copy($src, $tmpconfig) || die "copy: $!";
1787         }
1788
1789         # Sanity check on destination directory.
1790         if (! -e $dir) {
1791                 system("mkdir", "-p", $dir);
1792         }
1793         chdir($dir) || die "chdir $dir: $!";
1794
1795         # Special case to handle checkout of the "." repo, which 
1796         # would normally be skipped.
1797         my $topdir=abs_path(".")."/";
1798         my @repo=($topdir, $topdir, ".");
1799         loadconfig($tmpconfig, $topdir, $src);
1800         record(\@repo, action("checkout", @repo, 1))
1801                 if exists $config{$topdir}{"."}{"checkout"};
1802
1803         if (-e ".mrconfig") {
1804                 print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $src\n";
1805         }
1806         else {
1807                 move($tmpconfig, ".mrconfig") || die "rename: $!";
1808         }
1809
1810         # Reload the config file (in case we got a different version)
1811         # and checkout everything else.
1812         startingconfig();
1813         loadconfig(".mrconfig");
1814         dispatch("checkout");
1815         @skipped=grep { abs_path($_) ne abs_path($topdir) } @skipped;
1816         showstats("bootstrap");
1817         exitstats();
1818 }
1819
1820 # alias expansion and command stemming
1821 sub expandaction {
1822         my $action=shift;
1823         if (exists $alias{$action}) {
1824                 $action=$alias{$action};
1825         }
1826         if (! exists $knownactions{$action}) {
1827                 my @matches = grep { /^\Q$action\E/ }
1828                         keys %knownactions, keys %alias;
1829                 if (@matches == 1) {
1830                         $action=$matches[0];
1831                 }
1832                 elsif (@matches == 0) {
1833                         die "mr: unknown action \"$action\" (known actions: ".
1834                                 join(", ", sort keys %knownactions).")\n";
1835                 }
1836                 else {
1837                         die "mr: ambiguous action \"$action\" (matches: ".
1838                                 join(", ", @matches).")\n";
1839                 }
1840         }
1841         return $action;
1842 }
1843
1844 sub find_mrconfig {
1845         my $dir=getcwd();
1846         while (length $dir) {
1847                 if (-e "$dir/.mrconfig") {
1848                         return "$dir/.mrconfig";
1849                 }
1850                 $dir=~s/\/[^\/]*$//;
1851         }
1852         return $HOME_MR_CONFIG;
1853 }
1854
1855 sub getopts {
1856         my @saved=@ARGV;
1857         Getopt::Long::Configure("bundling", "no_permute");
1858         my $result=GetOptions(
1859                 "d|directory=s" => sub { $directory=abs_path($_[1]) },
1860                 "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
1861                 "p|path" => sub { }, # now default, ignore
1862                 "f|force" => \$force,
1863                 "v|verbose" => \$verbose,
1864                 "m|minimal" => \$minimal,
1865                 "q|quiet" => \$quiet,
1866                 "s|stats" => \$stats,
1867                 "k|insecure" => \$insecure,
1868                 "i|interactive" => \$interactive,
1869                 "n|no-recurse:i" => \$max_depth,
1870                 "j|jobs:i" => \$jobs,
1871                 "t|trust-all" => \$trust_all,
1872         );
1873         if (! $result || @ARGV < 1) {
1874                 die("Usage: mr [options] action [params ...]\n".
1875                     "(Use mr help for man page.)\n");
1876         }
1877         
1878         $ENV{MR_SWITCHES}="";
1879         foreach my $option (@saved) {
1880                 last if $option eq $ARGV[0];
1881                 $ENV{MR_SWITCHES}.="$option ";
1882         }
1883 }
1884
1885 sub init {
1886         $SIG{INT}=sub {
1887                 print STDERR "mr: interrupted\n";
1888                 exit 2;
1889         };
1890         
1891         # This can happen if it's run in a directory that was removed
1892         # or other strangeness.
1893         if (! defined $directory) {
1894                 die("mr: failed to determine working directory\n");
1895         }
1896         # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
1897         # the config file might be a symlink to elsewhere, and the directory it's
1898         # in is significant.
1899         if ($ENV{MR_CONFIG} !~ /^\//) {
1900                 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
1901         }
1902         # Try to set MR_PATH to the path to the program.
1903         eval {
1904                 use FindBin qw($Bin $Script);
1905                 $ENV{MR_PATH}=$Bin."/".$Script;
1906         };
1907 }
1908         
1909 sub exitstats {
1910         if (@failed) {
1911                 exit 1;
1912         }
1913         else {
1914                 exit 0;
1915         }
1916 }
1917
1918 sub main {
1919         getopts();
1920         init();
1921         help(@ARGV) if $ARGV[0] eq 'help';
1922
1923         startingconfig();
1924         loadconfig($HOME_MR_CONFIG);
1925         loadconfig($ENV{MR_CONFIG});
1926         #use Data::Dumper; print Dumper(\%config);
1927         
1928         my $action=expandaction(shift @ARGV);
1929         dispatch($action);
1930
1931         showstats($action);
1932         exitstats();
1933 }
1934
1935 # Finally, some useful actions that mr knows about by default.
1936 # These can be overridden in ~/.mrconfig.
1937 __DATA__
1938 [ALIAS]
1939 co = checkout
1940 ci = commit
1941 ls = list
1942
1943 [DEFAULT]
1944 order = 10
1945 lib =
1946         error() {
1947                 echo "mr: $@" >&2
1948                 exit 1
1949         }
1950         warning() {
1951                 echo "mr (warning): $@" >&2
1952         }
1953         info() {
1954                 echo "mr: $@" >&2
1955         }
1956         hours_since() {
1957                 if [ -z "$1" ] || [ -z "$2" ]; then
1958                         error "mr: usage: hours_since action num"
1959                 fi
1960                 for dir in .git .svn .bzr CVS .hg _darcs _FOSSIL_; do
1961                         if [ -e "$MR_REPO/$dir" ]; then
1962                                 flagfile="$MR_REPO/$dir/.mr_last$1"
1963                                 break
1964                         fi
1965                 done
1966                 if [ -z "$flagfile" ]; then
1967                         error "cannot determine flag filename"
1968                 fi
1969                 delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
1970                 if [ "$delta" -lt "$2" ]; then
1971                         return 1
1972                 else
1973                         touch "$flagfile"
1974                         return 0
1975                 fi
1976         }
1977         is_bzr_checkout() {
1978                 LANG=C bzr info | egrep -q '^Checkout'
1979         }
1980         lazy() {
1981                 if [ -d "$MR_REPO" ]; then
1982                         return 1
1983                 else
1984                         return 0
1985                 fi
1986         }
1987
1988 svn_test = perl: -d "$ENV{MR_REPO}/.svn"
1989 git_test = perl: -e "$ENV{MR_REPO}/.git"
1990 bzr_test = perl: -d "$ENV{MR_REPO}/.bzr"
1991 cvs_test = perl: -d "$ENV{MR_REPO}/CVS"
1992 hg_test  = perl: -d "$ENV{MR_REPO}/.hg"
1993 darcs_test = perl: -d "$ENV{MR_REPO}/_darcs"
1994 fossil_test = perl: -f "$ENV{MR_REPO}/_FOSSIL_"
1995 git_bare_test = perl: 
1996         -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
1997         -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
1998         `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get core.bare` =~ /true/
1999 vcsh_test = perl:
2000         -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
2001         -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
2002         `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get vcsh.vcsh` =~ /true/
2003 veracity_test  = perl: -d "$ENV{MR_REPO}/.sgdrawer"
2004
2005 svn_update = svn update "$@"
2006 git_update = git pull "$@"
2007 bzr_update = 
2008         if is_bzr_checkout; then
2009                 bzr update "$@"
2010         else
2011                 bzr merge --pull "$@"
2012         fi
2013 cvs_update = cvs -q update "$@"
2014 hg_update  = hg pull "$@"; hg update "$@"
2015 darcs_update = darcs pull -a "$@"
2016 fossil_update = fossil pull "$@"
2017 vcsh_update = vcsh run "$MR_REPO" git pull "$@"
2018 veracity_update = vv pull "$@" && vv update "$@"
2019
2020 git_fetch = git fetch --all --prune --tags
2021 git_svn_fetch = git svn fetch
2022 darcs_fetch = darcs fetch
2023 hg_fetch = hg pull
2024
2025 svn_status = svn status "$@"
2026 git_status = git status -s "$@" || true; git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline || true
2027 bzr_status = bzr status --short "$@"; bzr missing
2028 cvs_status = cvs -q status | grep -E '^(File:.*Status:|\?)' | grep -v 'Status: Up-to-date'
2029 hg_status  = hg status "$@"; hg summary --quiet | grep -v 'parent: 0:'
2030 darcs_status = darcs whatsnew -ls "$@" || true
2031 fossil_status = fossil changes "$@"
2032 vcsh_status = vcsh run "$MR_REPO" git -c status.relativePaths=false status -s "$@" || true
2033 veracity_status = vv status "$@"
2034
2035 svn_commit = svn commit "$@"
2036 git_commit = git commit -a "$@" && git push --all
2037 bzr_commit = 
2038         if is_bzr_checkout; then
2039                 bzr commit "$@"
2040         else
2041                 bzr commit "$@" && bzr push
2042         fi
2043 cvs_commit = cvs commit "$@"
2044 hg_commit  = hg commit "$@" && hg push
2045 darcs_commit = darcs record -a "$@" && darcs push -a
2046 fossil_commit = fossil commit "$@"
2047 vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all
2048 veracity_commit = vv commit "$@" && vv push
2049
2050 git_record = git commit -a "$@"
2051 bzr_record =
2052         if is_bzr_checkout; then
2053                 bzr commit --local "$@"
2054         else
2055                 bzr commit "$@"
2056         fi
2057 hg_record  = hg commit "$@"
2058 darcs_record = darcs record -a "$@"
2059 fossil_record = fossil commit "$@"
2060 vcsh_record = vcsh run "$MR_REPO" git commit -a "$@"
2061 veracity_record = vv commit "$@"
2062
2063 svn_push = :
2064 git_push = git push "$@"
2065 bzr_push = bzr push "$@"
2066 cvs_push = :
2067 hg_push = hg push "$@"
2068 darcs_push = darcs push -a "$@"
2069 fossil_push = fossil push "$@"
2070 vcsh_push = vcsh run "$MR_REPO" git push "$@"
2071 veracity_push = vv push "$@"
2072
2073 svn_diff = svn diff "$@"
2074 git_diff = git diff "$@"
2075 bzr_diff = bzr diff "$@"
2076 cvs_diff = cvs -q diff "$@"
2077 hg_diff  = hg diff "$@"
2078 darcs_diff = darcs diff -u "$@"
2079 fossil_diff = fossil diff "$@"
2080 vcsh_diff = vcsh run "$MR_REPO" git diff "$@"
2081 veracity_diff = vv diff "$@"
2082
2083 svn_log = svn log "$@"
2084 git_log = git log "$@"
2085 bzr_log = bzr log "$@"
2086 cvs_log = cvs log "$@"
2087 hg_log  = hg log "$@"
2088 darcs_log = darcs changes "$@"
2089 git_bare_log = git log "$@"
2090 fossil_log = fossil timeline "$@"
2091 vcsh_log = vcsh run "$MR_REPO" git log "$@"
2092 veracity_log = vv log "$@"
2093
2094 hg_grep = hg grep "$@"
2095 cvs_grep = ack-grep "$@"
2096 svn_grep = ack-grep "$@"
2097 git_svn_grep = git grep "$@"
2098 git_grep = git grep "$@"
2099 bzr_grep = ack-grep "$@"
2100 darcs_grep = ack-grep "$@"
2101
2102 run = "$@"
2103
2104 svn_register =
2105         url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
2106         if [ -z "$url" ]; then
2107                 error "cannot determine svn url"
2108         fi
2109         echo "Registering svn url: $url in $MR_CONFIG"
2110         mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
2111 git_register = 
2112         url="`LC_ALL=C git config --get remote.origin.url`" || true
2113         if [ -z "$url" ]; then
2114                 error "cannot determine git url"
2115         fi
2116         echo "Registering git url: $url in $MR_CONFIG"
2117         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
2118 bzr_register =
2119         url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}' | head -n 1`"
2120         if [ -z "$url" ]; then
2121                 error "cannot determine bzr url"
2122         fi
2123         echo "Registering bzr url: $url in $MR_CONFIG"
2124         mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr branch '$url' '$MR_REPO'"
2125 cvs_register =
2126         repo=`cat CVS/Repository`
2127         root=`cat CVS/Root`
2128         if [ -z "$root" ]; then
2129                 error "cannot determine cvs root"
2130                 fi
2131         echo "Registering cvs repository $repo at root $root"
2132         mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
2133 hg_register = 
2134         url=`hg showconfig paths.default`
2135         echo "Registering mercurial repo url: $url in $MR_CONFIG"
2136         mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
2137 darcs_register = 
2138         url=`cat _darcs/prefs/defaultrepo`
2139         echo "Registering darcs repository $url in $MR_CONFIG"
2140         mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
2141 git_bare_register = 
2142         url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
2143         if [ -z "$url" ]; then
2144                 error "cannot determine git url"
2145         fi
2146         echo "Registering git url: $url in $MR_CONFIG"
2147         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
2148 vcsh_register =
2149         url="`LC_ALL=C vcsh run "$MR_REPO" git config --get remote.origin.url`" || true
2150         if [ -z "$url" ]; then
2151                 error "cannot determine git url"
2152         fi
2153         echo "Registering git url: $url in $MR_CONFIG"
2154         mr -c "$MR_CONFIG" config "`pwd`" checkout="vcsh clone '$url' '$MR_REPO'"
2155 fossil_register =
2156         url=`fossil remote-url`
2157         repo=`fossil info | grep repository | sed -e 's/repository:*.//g' -e 's/ //g'`
2158         echo "Registering fossil repository $url in $MR_CONFIG"
2159         mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && fossil open '$repo'"
2160 veracity_register =
2161         url=`vv config | grep sync_targets | sed -e 's/sync_targets:*.//g' -e 's/ //g'`
2162         repo=`vv repo info | grep repository | sed -e 's/Current repository:*.//g' -e 's/ //g'`
2163         echo "Registering veracity repository $url in $MR_CONFIG"
2164         mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && vv checkout '$repo'"
2165
2166 svn_trusted_checkout = svn co $url $repo
2167 svn_alt_trusted_checkout = svn checkout $url $repo
2168 git_trusted_checkout = git clone $url $repo
2169 bzr_trusted_checkout = bzr checkout|clone|branch|get $url $repo
2170 # cvs: too hard
2171 hg_trusted_checkout = hg clone $url $repo
2172 darcs_trusted_checkout = darcs get $url $repo
2173 git_bare_trusted_checkout = git clone --bare $url $repo
2174 vcsh_old_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
2175 vcsh_trusted_checkout = vcsh clone $url $repo
2176 # fossil: messy to do
2177 veracity_trusted_checkout = vv clone $url $repo
2178
2179
2180 list = true
2181 config = 
2182 bootstrap = 
2183
2184 online =
2185         if [ -s ~/.mrlog ]; then
2186                 info "running offline commands"
2187                 mv -f ~/.mrlog ~/.mrlog.old
2188                 if ! sh -e ~/.mrlog.old; then
2189                         error "offline command failed; left in ~/.mrlog.old"
2190                 fi
2191                 rm -f ~/.mrlog.old
2192         else
2193                 info "no offline commands to run"
2194         fi
2195 offline =
2196         umask 077
2197         touch ~/.mrlog
2198         info "offline mode enabled"
2199 remember =
2200         info "remembering command: 'mr $@'"
2201         command="mr -d '$(pwd)' $MR_SWITCHES"
2202         for w in "$@"; do
2203                 command="$command '$w'"
2204         done
2205         if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
2206                 echo "$command" >> ~/.mrlog
2207         fi
2208
2209 ed = echo "A horse is a horse, of course, of course.."
2210 T = echo "I pity the fool."
2211 right = echo "Not found."
2212
2213 # vim:sw=8:sts=0:ts=8:noet
2214 # Local variables:
2215 # indent-tabs-mode: t
2216 # cperl-indent-level: 8
2217 # End: