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

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