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

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