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

mr: Make check() even more pedantic
[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 check {
1883         my @env = qw(GIT_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_WORK_TREE VCSH_COMMAND VCSH_DIRECTORY VCSH_REPO_NAME);
1884         foreach (@env) {
1885                 die ("mr: environment variable '$_' is set. You are about to shoot your own foot.\n") if ($ENV{$_});
1886         }
1887 }
1888
1889 sub init {
1890         $SIG{INT}=sub {
1891                 print STDERR "mr: interrupted\n";
1892                 exit 2;
1893         };
1894         
1895         # This can happen if it's run in a directory that was removed
1896         # or other strangeness.
1897         if (! defined $directory) {
1898                 die("mr: failed to determine working directory\n");
1899         }
1900         # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
1901         # the config file might be a symlink to elsewhere, and the directory it's
1902         # in is significant.
1903         if ($ENV{MR_CONFIG} !~ /^\//) {
1904                 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
1905         }
1906         # Try to set MR_PATH to the path to the program.
1907         eval {
1908                 use FindBin qw($Bin $Script);
1909                 $ENV{MR_PATH}=$Bin."/".$Script;
1910         };
1911 }
1912         
1913 sub exitstats {
1914         if (@failed) {
1915                 exit 1;
1916         }
1917         else {
1918                 exit 0;
1919         }
1920 }
1921
1922 sub main {
1923         getopts();
1924         check();
1925         init();
1926         help(@ARGV) if $ARGV[0] eq 'help';
1927
1928         startingconfig();
1929         loadconfig($HOME_MR_CONFIG);
1930         loadconfig($ENV{MR_CONFIG});
1931         #use Data::Dumper; print Dumper(\%config);
1932         
1933         my $action=expandaction(shift @ARGV);
1934         dispatch($action);
1935
1936         showstats($action);
1937         exitstats();
1938 }
1939
1940 # Finally, some useful actions that mr knows about by default.
1941 # These can be overridden in ~/.mrconfig.
1942 __DATA__
1943 [ALIAS]
1944 co = checkout
1945 ci = commit
1946 ls = list
1947
1948 [DEFAULT]
1949 order = 10
1950 lib =
1951         error() {
1952                 echo "mr: $@" >&2
1953                 exit 1
1954         }
1955         warning() {
1956                 echo "mr (warning): $@" >&2
1957         }
1958         info() {
1959                 echo "mr: $@" >&2
1960         }
1961         hours_since() {
1962                 if [ -z "$1" ] || [ -z "$2" ]; then
1963                         error "mr: usage: hours_since action num"
1964                 fi
1965                 for dir in .git .svn .bzr CVS .hg _darcs _FOSSIL_; do
1966                         if [ -e "$MR_REPO/$dir" ]; then
1967                                 flagfile="$MR_REPO/$dir/.mr_last$1"
1968                                 break
1969                         fi
1970                 done
1971                 if [ -z "$flagfile" ]; then
1972                         error "cannot determine flag filename"
1973                 fi
1974                 delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
1975                 if [ "$delta" -lt "$2" ]; then
1976                         return 1
1977                 else
1978                         touch "$flagfile"
1979                         return 0
1980                 fi
1981         }
1982         is_bzr_checkout() {
1983                 LANG=C bzr info | egrep -q '^Checkout'
1984         }
1985         lazy() {
1986                 if [ -d "$MR_REPO" ]; then
1987                         return 1
1988                 else
1989                         return 0
1990                 fi
1991         }
1992
1993 svn_test = perl: -d "$ENV{MR_REPO}/.svn"
1994 git_test = perl: -e "$ENV{MR_REPO}/.git"
1995 bzr_test = perl: -d "$ENV{MR_REPO}/.bzr"
1996 cvs_test = perl: -d "$ENV{MR_REPO}/CVS"
1997 hg_test  = perl: -d "$ENV{MR_REPO}/.hg"
1998 darcs_test = perl: -d "$ENV{MR_REPO}/_darcs"
1999 fossil_test = perl: -f "$ENV{MR_REPO}/_FOSSIL_"
2000 git_bare_test = perl: 
2001         -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
2002         -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
2003         `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get core.bare` =~ /true/
2004 vcsh_test = perl:
2005         -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
2006         -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
2007         `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get vcsh.vcsh` =~ /true/
2008 veracity_test  = perl: -d "$ENV{MR_REPO}/.sgdrawer"
2009
2010 svn_update = svn update "$@"
2011 git_update = git pull "$@"
2012 bzr_update = 
2013         if is_bzr_checkout; then
2014                 bzr update "$@"
2015         else
2016                 bzr merge --pull "$@"
2017         fi
2018 cvs_update = cvs -q update "$@"
2019 hg_update  = hg pull "$@"; hg update "$@"
2020 darcs_update = darcs pull -a "$@"
2021 fossil_update = fossil pull "$@"
2022 vcsh_update = vcsh run "$MR_REPO" git pull "$@"
2023 veracity_update = vv pull "$@" && vv update "$@"
2024
2025 git_fetch = git fetch --all --prune --tags
2026 git_svn_fetch = git svn fetch
2027 darcs_fetch = darcs fetch
2028 hg_fetch = hg pull
2029
2030 svn_clean = 
2031         if [ "x$1" = x-f ] ; then
2032                 shift
2033                 svn-clean "$@"
2034         else
2035                 svn-clean --print "$@"
2036         fi
2037 git_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 git_svn_clean = 
2045         if [ "x$1" = x-f ] ; then
2046                 shift
2047                 git clean -dx --force "$@"
2048         else
2049                 git clean -dx --dry-run "$@"
2050         fi
2051 bzr_clean = 
2052         if [ "x$1" = x-f ] ; then
2053                 shift
2054                 bzr clean-tree --verbose --force --ignored --unknown --detritus "$@"
2055         else
2056                 bzr clean-tree --verbose --dry-run --ignored --unknown --detritus "$@"
2057         fi
2058 cvs_clean = 
2059         if [ "x$1" = x-f ] ; then
2060                 shift
2061                 cvs-clean "$@"
2062         else
2063                 cvs-clean --dry-run "$@"
2064         fi
2065 hg_clean = 
2066         if [ "x$1" = x-f ] ; then
2067                 shift
2068                 hg purge --print --all "$@"
2069                 hg purge --all "$@"
2070         else
2071                 hg purge --print --all "$@"
2072         fi
2073 fossil_clean = 
2074         if [ "x$1" = x-f ] ; then
2075                 shift
2076                 fossil clean --dry-run --dotfiles --emptydirs "$@"
2077         else
2078                 fossil clean --force --dotfiles --emptydirs "$@"
2079         fi
2080 vcsh_clean = 
2081         if [ "x$1" = x-f ] ; then
2082                 shift
2083                 vcsh run "$MR_REPO" git clean -dx "$@"
2084         else
2085                 vcsh run "$MR_REPO" git clean -dx --dry-run "$@"
2086         fi
2087
2088 svn_status = svn status "$@"
2089 git_status = git status -s "$@" || true; git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline || true
2090 bzr_status = bzr status --short "$@"; bzr missing
2091 cvs_status = cvs -q status | grep -E '^(File:.*Status:|\?)' | grep -v 'Status: Up-to-date'
2092 hg_status  = hg status "$@"; hg summary --quiet | grep -v 'parent: 0:'
2093 darcs_status = darcs whatsnew -ls "$@" || true
2094 fossil_status = fossil changes "$@"
2095 vcsh_status = vcsh run "$MR_REPO" git -c status.relativePaths=false status -s "$@" || true
2096 veracity_status = vv status "$@"
2097
2098 svn_commit = svn commit "$@"
2099 git_commit = git commit -a "$@" && git push --all
2100 bzr_commit = 
2101         if is_bzr_checkout; then
2102                 bzr commit "$@"
2103         else
2104                 bzr commit "$@" && bzr push
2105         fi
2106 cvs_commit = cvs commit "$@"
2107 hg_commit  = hg commit "$@" && hg push
2108 darcs_commit = darcs record -a "$@" && darcs push -a
2109 fossil_commit = fossil commit "$@"
2110 vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all
2111 veracity_commit = vv commit "$@" && vv push
2112
2113 git_record = git commit -a "$@"
2114 bzr_record =
2115         if is_bzr_checkout; then
2116                 bzr commit --local "$@"
2117         else
2118                 bzr commit "$@"
2119         fi
2120 hg_record  = hg commit "$@"
2121 darcs_record = darcs record -a "$@"
2122 fossil_record = fossil commit "$@"
2123 vcsh_record = vcsh run "$MR_REPO" git commit -a "$@"
2124 veracity_record = vv commit "$@"
2125
2126 svn_push = :
2127 git_push = git push "$@"
2128 bzr_push = bzr push "$@"
2129 cvs_push = :
2130 hg_push = hg push "$@"
2131 darcs_push = darcs push -a "$@"
2132 fossil_push = fossil push "$@"
2133 vcsh_push = vcsh run "$MR_REPO" git push "$@"
2134 veracity_push = vv push "$@"
2135
2136 svn_diff = svn diff "$@"
2137 git_diff = git diff "$@"
2138 bzr_diff = bzr diff "$@"
2139 cvs_diff = cvs -q diff "$@"
2140 hg_diff  = hg diff "$@"
2141 darcs_diff = darcs diff -u "$@"
2142 fossil_diff = fossil diff "$@"
2143 vcsh_diff = vcsh run "$MR_REPO" git diff "$@"
2144 veracity_diff = vv diff "$@"
2145
2146 svn_log = svn log "$@"
2147 git_log = git log "$@"
2148 bzr_log = bzr log "$@"
2149 cvs_log = cvs log "$@"
2150 hg_log  = hg log "$@"
2151 darcs_log = darcs changes "$@"
2152 git_bare_log = git log "$@"
2153 fossil_log = fossil timeline "$@"
2154 vcsh_log = vcsh run "$MR_REPO" git log "$@"
2155 veracity_log = vv log "$@"
2156
2157 hg_grep = hg grep "$@"
2158 cvs_grep = ack-grep "$@"
2159 svn_grep = ack-grep "$@"
2160 git_svn_grep = git grep "$@"
2161 git_grep = git grep "$@"
2162 bzr_grep = ack-grep "$@"
2163 darcs_grep = ack-grep "$@"
2164
2165 run = "$@"
2166
2167 svn_register =
2168         url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
2169         if [ -z "$url" ]; then
2170                 error "cannot determine svn url"
2171         fi
2172         echo "Registering svn url: $url in $MR_CONFIG"
2173         mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
2174 git_register = 
2175         url="`LC_ALL=C git config --get remote.origin.url`" || true
2176         if [ -z "$url" ]; then
2177                 error "cannot determine git url"
2178         fi
2179         echo "Registering git url: $url in $MR_CONFIG"
2180         mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
2181 bzr_register =
2182         url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}' | head -n 1`"
2183         if [ -z "$url" ]; then
2184                 error "cannot determine bzr url"
2185         fi
2186         echo "Registering bzr url: $url in $MR_CONFIG"
2187         mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr branch '$url' '$MR_REPO'"
2188 cvs_register =
2189         repo=`cat CVS/Repository`
2190         root=`cat CVS/Root`
2191         if [ -z "$root" ]; then
2192                 error "cannot determine cvs root"
2193                 fi
2194         echo "Registering cvs repository $repo at root $root"
2195         mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
2196 hg_register = 
2197         url=`hg showconfig paths.default`
2198         echo "Registering mercurial repo url: $url in $MR_CONFIG"
2199         mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
2200 darcs_register = 
2201         url=`cat _darcs/prefs/defaultrepo`
2202         echo "Registering darcs repository $url in $MR_CONFIG"
2203         mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
2204 git_bare_register = 
2205         url="`LC_ALL=C GIT_CONFIG=config 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="git clone --bare '$url' '$MR_REPO'"
2211 vcsh_register =
2212         url="`LC_ALL=C vcsh run "$MR_REPO" git config --get remote.origin.url`" || true
2213         if [ -z "$url" ]; then
2214                 error "cannot determine git url"
2215         fi
2216         echo "Registering git url: $url in $MR_CONFIG"
2217         mr -c "$MR_CONFIG" config "`pwd`" checkout="vcsh clone '$url' '$MR_REPO'"
2218 fossil_register =
2219         url=`fossil remote-url`
2220         repo=`fossil info | grep repository | sed -e 's/repository:*.//g' -e 's/ //g'`
2221         echo "Registering fossil repository $url in $MR_CONFIG"
2222         mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && fossil open '$repo'"
2223 veracity_register =
2224         url=`vv config | grep sync_targets | sed -e 's/sync_targets:*.//g' -e 's/ //g'`
2225         repo=`vv repo info | grep repository | sed -e 's/Current repository:*.//g' -e 's/ //g'`
2226         echo "Registering veracity repository $url in $MR_CONFIG"
2227         mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && vv checkout '$repo'"
2228
2229 svn_trusted_checkout = svn co $url $repo
2230 svn_alt_trusted_checkout = svn checkout $url $repo
2231 git_trusted_checkout = git clone $url $repo
2232 bzr_trusted_checkout = bzr checkout|clone|branch|get $url $repo
2233 # cvs: too hard
2234 hg_trusted_checkout = hg clone $url $repo
2235 darcs_trusted_checkout = darcs get $url $repo
2236 git_bare_trusted_checkout = git clone --bare $url $repo
2237 vcsh_old_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
2238 vcsh_trusted_checkout = vcsh clone $url $repo
2239 # fossil: messy to do
2240 veracity_trusted_checkout = vv clone $url $repo
2241
2242
2243 list = true
2244 config = 
2245 bootstrap = 
2246
2247 online =
2248         if [ -s ~/.mrlog ]; then
2249                 info "running offline commands"
2250                 mv -f ~/.mrlog ~/.mrlog.old
2251                 if ! sh -e ~/.mrlog.old; then
2252                         error "offline command failed; left in ~/.mrlog.old"
2253                 fi
2254                 rm -f ~/.mrlog.old
2255         else
2256                 info "no offline commands to run"
2257         fi
2258 offline =
2259         umask 077
2260         touch ~/.mrlog
2261         info "offline mode enabled"
2262 remember =
2263         info "remembering command: 'mr $@'"
2264         command="mr -d '$(pwd)' $MR_SWITCHES"
2265         for w in "$@"; do
2266                 command="$command '$w'"
2267         done
2268         if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
2269                 echo "$command" >> ~/.mrlog
2270         fi
2271
2272 ed = echo "A horse is a horse, of course, of course.."
2273 T = echo "I pity the fool."
2274 right = echo "Not found."
2275
2276 # vim:sw=8:sts=0:ts=8:noet
2277 # Local variables:
2278 # indent-tabs-mode: t
2279 # cperl-indent-level: 8
2280 # End: