rename .cron to .cron-stage as mcron uses .cron
[james-home.git] / bin / mr.in
blob861c03464a3939eb99e58f5c799bcea7ae7febab
1 #!/usr/bin/env perl
3 =head1 NAME
5 mr - a tool to manage all your version control repos
7 =head1 SYNOPSIS
9 B<mr> [options] checkout
11 B<mr> [options] update
13 B<mr> [options] status
15 B<mr> [options] clean [-f]
17 B<mr> [options] commit [-m "message"]
19 B<mr> [options] record [-m "message"]
21 B<mr> [options] fetch
23 B<mr> [options] push
25 B<mr> [options] diff
27 B<mr> [options] log
29 B<mr> [options] grep pattern
31 B<mr> [options] run command [param ...]
33 B<mr> [options] bootstrap src [directory]
35 B<mr> [options] register [repository]
37 B<mr> [options] config section ["setting=[value]" ...]
39 B<mr> [options] action [params ...]
41 B<mr> [options] [online|offline]
43 B<mr> [options] remember action [params ...]
45 =head1 DESCRIPTION
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.
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,
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.
64 These predefined commands should be fairly familiar to users of any version
65 control system:
67 =over 4
69 =item checkout (or co)
71 Checks out any repositories that are not already checked out.
73 =item update
75 Updates each repository from its configured remote repository.
77 If a repository isn't checked out yet, it will first check it out.
79 =item status
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.
85 =item clean
87 Print ignored files, untracked files and other cruft in the working directory.
89 The optional -f parameter allows removing the files as well as printing them.
91 =item commit (or ci)
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.)
98 The optional -m parameter allows specifying a commit message.
100 =item record
102 Records changes to the local repository, but does not push them to the
103 remote repository. Only supported for distributed version control systems.
105 The optional -m parameter allows specifying a commit message.
107 =item fetch
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.
113 =item push
115 Pushes committed local changes to the remote repository. A no-op for
116 centralized version control systems.
118 =item diff
120 Show a diff of uncommitted changes.
122 =item log
124 Show the commit log.
126 =item grep pattern
128 Searches for a pattern in each repository using the grep subcommand. Uses
129 ack-grep on VCS that do not have their own.
131 =item run command [param ...]
133 Runs the specified command in each repository.
135 =back
137 These commands are also available:
139 =over 4
141 =item bootstrap src [directory]
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.
146 B<mr> understands several types of sources:
148 =over 4
150 =item URL for curl
152 C<src> may be an URL understood by B<curl>.
154 =item copy via ssh
156 To use B<scp> to download, the C<src> may have the form
157 C<ssh://[user@]host:file>.
159 =item local file
161 You can retrieve the config file by other means and pass its B<path> as C<src>.
163 =item standard input
165 If source C<src> consists in a single dash C<->, config file is read from
166 standard input.
168 =back
170 The directory will be created if it does not exist. If no directory is
171 specified, the current directory will be used.
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.
176 =item list (or ls)
178 List the repositories that mr will act on.
180 =item register
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.
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.
189 =item config
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.
196 For example, to add (or edit) a repository in src/foo:
198 mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
200 To show the command that mr uses to update the repository in src/foo:
202 mr config src/foo update
204 To see the built-in library of shell functions contained in mr:
206 mr config DEFAULT lib
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.
211 =item offline
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.
216 =item online
218 Advices mr that it is in online mode again. Commands that failed while in
219 offline mode will be re-run.
221 =item remember
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.
229 =item help
231 Displays this help.
233 =back
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"
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.
244 =head1 OPTIONS
246 =over 4
248 =item -d directory
250 =item --directory directory
252 Specifies the topmost directory that B<mr> should work in. The default is
253 the current working directory.
255 =item -c mrconfig
257 =item --config mrconfig
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.
263 =item -f
265 =item --force
267 Force mr to act on repositories that would normally be skipped due to their
268 configuration.
270 =item --force-env
272 Force mr to execute even though potentially dangerous environment variables
273 are set.
275 =item -v
277 =item --verbose
279 Be verbose.
281 =item -m
283 =item --minimal
285 Minimise output. If a command fails or there is any output then the usual
286 output will be shown.
288 =item -q
290 =item --quiet
292 Be quiet. This suppresses mr's usual output, as well as any output from
293 commands that are run (including stderr output). If a command fails,
294 the output will be shown.
296 =item -k
298 =item --insecure
300 Accept untrusted SSL certificates when bootstrapping.
302 =item -s
304 =item --stats
306 Expand the statistics line displayed at the end to include information
307 about exactly which repositories failed and were skipped, if any.
309 =item -i
311 =item --interactive
313 Interactive mode. If a repository fails to be processed, a subshell will be
314 started which you can use to resolve or investigate the problem. Exit the
315 subshell to continue the mr run.
317 =item -n [number]
319 =item --no-recurse [number]
321 If no number if specified, just operate on the repository for the current
322 directory, do not recurse into deeper repositories.
324 If a number is specified, will recurse into repositories at most that many
325 subdirectories deep. For example, with -n 2 it would recurse into ./src/foo,
326 but not ./src/packages/bar.
328 =item -j [number]
330 =item --jobs [number]
332 Run the specified number of jobs in parallel, or an unlimited number of jobs
333 with no number specified. This can greatly speed up operations such as updates.
334 It is not recommended for interactive operations.
336 Note that running more than 10 jobs at a time is likely to run afoul of
337 ssh connection limits. Running between 3 and 5 jobs at a time will yield
338 a good speedup in updates without loading the machine too much.
340 =item -t
342 =item --trust-all
344 Trust all mrconfig files even if they are not listed in F<~/.mrtrust>.
345 Use with caution.
347 =item -p
349 =item --path
351 This obsolete flag is ignored.
353 =back
355 =head1 MRCONFIG FILES
357 Here is an example F<.mrconfig> file:
359 [src]
360 checkout = svn checkout svn://svn.example.com/src/trunk src
361 chain = true
363 [src/linux-2.6]
364 checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
365 cd linux-2.6 &&
366 git checkout -b mybranch origin/master
368 The F<.mrconfig> file uses a variant of the INI file format. Lines
369 starting with "#" are comments. Values can be continued to the
370 following line by indenting the line with whitespace.
372 The C<DEFAULT> section allows setting default values for the sections that
373 come after it.
375 The C<ALIAS> section allows adding aliases for actions. Each setting
376 is an alias, and its value is the action to use.
378 All other sections add repositories. The section header specifies the
379 directory where the repository is located. This is relative to the directory
380 that contains the mrconfig file, but you can also choose to use absolute
381 paths. (Note that you can use environment variables in section names; they
382 will be passed through the shell for expansion. For example,
383 C<[$HOSTNAME]>, or C<[${HOSTNAME}foo]>).
385 Within a section, each setting defines a shell command to run to handle a
386 given action. mr contains default handlers for "update", "status",
387 "commit", and other standard actions.
389 Normally you only need to specify what to do for "checkout". Here you
390 specify the command to run in order to create a checkout of the repository.
391 The command will be run in the parent directory, and must create the
392 repository's directory. So use C<git clone>, C<svn checkout>, C<bzr branch>
393 or C<bzr checkout> (for a bound branch), etc.
395 Note that these shell commands are run in a C<set -e> shell
396 environment, where any additional parameters you pass are available in
397 C<$@>. All commands other than "checkout" are run inside the repository,
398 though not necessarily at the top of it.
400 The C<MR_REPO> environment variable is set to the path to the top of the
401 repository. (For the "register" action, "MR_REPO" is instead set to the
402 basename of the directory that should be created when checking the
403 repository out.)
405 The C<MR_CONFIG> environment variable is set to the .mrconfig file
406 that defines the repo being acted on, or, if the repo is not yet in a config
407 file, the F<.mrconfig> file that should be modified to register the repo.
409 The C<MR_ACTION> environment variable is set to the command being run
410 (update, checkout, etc).
412 A few settings have special meanings:
414 =over 4
416 =item skip
418 If "skip" is set and its command returns true, then B<mr>
419 will skip acting on that repository. The command is passed the action
420 name in C<$1>.
422 Here are two examples. The first skips the repo unless
423 mr is run by joey. The second uses the hours_since function
424 (included in mr's built-in library) to skip updating the repo unless it's
425 been at least 12 hours since the last update.
427 [mystuff]
428 checkout = ...
429 skip = test `whoami` != joey
431 [linux]
432 checkout = ...
433 skip = [ "$1" = update ] && ! hours_since "$1" 12
435 Another way to use skip is for a lazy checkout. This makes mr skip
436 operating on a repo unless it already exists. To enable the
437 repo, you have to explicitly check it out (using "mr --force -d foo checkout").
439 [foo]
440 checkout = ...
441 skip = lazy
443 =item order
445 The "order" setting can be used to override the default ordering of
446 repositories. The default order value is 10. Use smaller values to make
447 repositories be processed earlier, and larger values to make repositories
448 be processed later.
450 Note that if a repository is located in a subdirectory of another
451 repository, ordering it to be processed earlier is not recommended.
453 =item chain
455 If "chain" is set and its command returns true, then B<mr>
456 will try to load a F<.mrconfig> file from the root of the repository.
458 =item include
460 If "include" is set, its command is ran, and should output
461 additional mrconfig file content. The content is included as if it were
462 part of the including file.
464 Unlike everything else, "include" does not need to be placed within a section.
466 B<mr> ships several libraries that can be included to add support for
467 additional version control type things (unison, git-svn, git-fake-bare,
468 git-subtree). To include them all, you could use:
470 include = cat /usr/share/mr/*
472 See the individual files for details.
474 =item deleted
476 If "deleted" is set and its command returns true, then
477 B<mr> will treat the repository as deleted. It won't ever actually delete
478 the repository, but it will warn if it sees the repository's directory.
479 This is useful when one mrconfig file is shared among multiple machines,
480 to keep track of and remember to delete old repositories.
482 =item lib
484 The "lib" setting can contain some shell code that will be run
485 before each command, this can be a useful way to define shell
486 functions for other commands to use.
488 Unlike most other settings, this can be specified multiple times, in
489 which case the chunks of shell code are accumulatively concatenated
490 together.
492 =item fixups
494 If "fixups" is set, its command is run whenever a repository
495 is checked out, or updated. This provides an easy way to do things
496 like permissions fixups, or other tweaks to the repository content,
497 whenever the repository is changed.
499 =item jobs
501 If "jobs" is set, run the specified number of jobs in parallel.
502 This can greatly speed up operations such as updates.
504 Note that running more than 10 jobs at a time is likely to run afoul of
505 ssh connection limits. Running between 3 and 5 jobs at a time will yield
506 a good speedup in updates without loading the machine too much.
508 =item VCS_action
510 When looking for a command to run for a given action, mr first looks for
511 a setting with the same name as the action. If that is not found, it
512 looks for a setting named "VCS_action" (substituting in the name of the
513 version control system and the action).
515 Internally, mr has settings for "git_update", "svn_update", etc. To change
516 the action that is performed for a given version control system, you can
517 override these VCS specific actions. To add a new version control system,
518 you can just add VCS specific actions for it.
520 =item pre_ and post_
522 If "pre_action" is set, its command is run before mr performs the
523 specified action. Similarly, "post_action" commands are run after mr
524 successfully performs the specified action. For example, "pre_commit" is
525 run before committing; "post_update" is run after updating.
527 =item _append
529 Any setting can be suffixed with C<_append>, to add an additional value
530 to the existing value of the setting. In this way, actions
531 can be constructed accumulatively.
533 =item VCS_test
535 The name of the version control system is itself determined by
536 running each defined "VCS_test" action, until one succeeds.
538 =back
540 =head1 UNTRUSTED MRCONFIG FILES
542 Since mrconfig files can contain arbitrary shell commands, they can do
543 anything. This flexibility is good, but it also allows a malicious mrconfig
544 file to delete your whole home directory. Such a file might be contained
545 inside a repository that your main F<~/.mrconfig> checks out. To
546 avoid worries about evil commands in a mrconfig file, mr defaults to
547 reading all mrconfig files other than the main F<~/.mrconfig> in untrusted
548 mode. In untrusted mode, mrconfig files are limited to running only known
549 safe commands (like "git clone") in a carefully checked manner.
551 To configure mr to trust other mrconfig files, list them in F<~/.mrtrust>.
552 One mrconfig file should be listed per line. Either the full pathname
553 should be listed, or the pathname can start with F<~/> to specify a file
554 relative to your home directory.
556 =head1 OFFLINE LOG FILE
558 The F<~/.mrlog> file contains commands that mr has remembered to run later,
559 due to being offline. You can delete or edit this file to remove commands,
560 or even to add other commands for 'mr online' to run. If the file is
561 present, mr assumes it is in offline mode.
563 =head1 EXTENSIONS
565 mr can be extended to support things such as unison and git-svn. Some
566 files providing such extensions are available in F</usr/share/mr/>. See
567 the documentation in the files for details about using them.
569 =head1 EXIT STATUS
571 mr returns nonzero if a command failed in any of the repositories.
573 =head1 AUTHOR
575 Copyright 2007-2011 Joey Hess <joey@kitenet.net>
577 Licensed under the GNU GPL version 2 or higher.
579 http://myrepos.branchable.com/
581 =cut
583 use warnings;
584 use strict;
585 use Getopt::Long;
586 use Cwd qw(getcwd abs_path);
588 # things that can happen when mr runs a command
589 use constant {
590 OK => 0,
591 FAILED => 1,
592 SKIPPED => 2,
593 ABORT => 3,
596 # configurables
597 my $config_overridden=0;
598 my $verbose=0;
599 my $minimal=0;
600 my $quiet=0;
601 my $stats=0;
602 my $force=0;
603 my $force_env=0;
604 my $insecure=0;
605 my $interactive=0;
606 my $max_depth;
607 my $no_chdir=0;
608 my $jobs=1;
609 my $trust_all=0;
610 my $directory=getcwd();
611 my $terminal=-t STDOUT && eval{require IO::Pty::Easy;IO::Pty::Easy->import();1;};
612 my $erase_line=$terminal && eval{require Term::Cap;my $t=Term::Cap->Tgetent();$t->Tputs('ce');};
614 my $HOME_MR_CONFIG = "$ENV{HOME}/.mrconfig";
615 $ENV{MR_CONFIG}=find_mrconfig();
617 # globals :-(
618 my %config;
619 my %configfiles;
620 my %knownactions;
621 my %alias;
622 my (@ok, @failed, @skipped);
624 main();
626 sub shellquote {
627 my $i=shift;
628 $i=~s/'/'"'"'/g;
629 return "'$i'";
632 # Runs a shell command using a supplied function.
633 # The lib will be included in the shell command line, and any params
634 # will be available in the shell as $1, $2, etc.
635 my $lastlib;
636 sub runsh {
637 my ($action, $topdir, $subdir, $command, $params, $runner) = @_;
639 # optimisation: avoid running the shell for true and false
640 if ($command =~ /^\s*true\s*$/) {
641 $?=0;
642 return 0;
644 elsif ($command =~ /^\s*false\s*$/) {
645 $?=0;
646 return 1;
649 my $quotedparams=join(" ", (map { shellquote($_) } @$params));
650 my $lib=exists $config{$topdir}{$subdir}{lib} ?
651 $config{$topdir}{$subdir}{lib}."\n" : "";
652 if ($verbose && (! defined $lastlib || $lastlib ne $lib)) {
653 print "mr library now: >>$lib<<\n";
654 $lastlib=$lib;
656 my $shellcode="set -e;".$lib.
657 "my_sh(){ $command\n }; my_sh $quotedparams";
658 print "mr $action: running $action >>$command<<\n" if $verbose;
659 $runner->($shellcode);
662 my %perl_cache;
663 sub perl {
664 my $id=shift;
665 my $s=shift;
666 if ($s =~ m/^perl:\s+(.*)/s) {
667 return $perl_cache{$1} if exists $perl_cache{$1};
668 my $sub=eval "sub {$1}";
669 if (! defined $sub) {
670 print STDERR "mr: bad perl code in $id: $@\n";
672 return $perl_cache{$1} = $sub;
674 return undef;
677 my %vcs;
678 sub vcs_test {
679 my ($action, $dir, $topdir, $subdir) = @_;
681 if (exists $vcs{$dir}) {
682 return $vcs{$dir};
685 my $test="";
686 my %perltest;
687 foreach my $vcs_test (
688 sort {
689 length $a <=> length $b
691 $a cmp $b
692 } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
693 my ($vcs)=$vcs_test =~ /(.*)_test/;
694 my $p=perl($vcs_test, $config{$topdir}{$subdir}{$vcs_test});
695 if (defined $p) {
696 $perltest{$vcs}=$p;
698 else {
699 $test="my_$vcs_test() {\n$config{$topdir}{$subdir}{$vcs_test}\n}\n".$test;
700 $test.="if my_$vcs_test; then echo $vcs; fi\n";
704 my @vcs;
705 foreach my $vcs (keys %perltest) {
706 if ($perltest{$vcs}->()) {
707 push @vcs, $vcs;
711 push @vcs, split(/\n/,
712 runsh("vcs test", $topdir, $subdir, $test, [], sub {
713 my $sh=shift;
714 my $ret=`$sh`;
715 return $ret;
716 })) if length $test;
717 if (@vcs > 1) {
718 print STDERR "mr $action: found multiple possible repository types (@vcs) for ".fulldir($topdir, $subdir)."\n";
719 return undef;
721 if (! @vcs) {
722 return $vcs{$dir}=undef;
724 else {
725 return $vcs{$dir}=$vcs[0];
729 sub findcommand {
730 my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
732 if (exists $config{$topdir}{$subdir}{$action}) {
733 return $config{$topdir}{$subdir}{$action};
736 if ($is_checkout) {
737 return undef;
740 my $vcs=vcs_test(@_);
742 if (defined $vcs &&
743 exists $config{$topdir}{$subdir}{$vcs."_".$action}) {
744 return $config{$topdir}{$subdir}{$vcs."_".$action};
746 else {
747 return undef;
751 sub fulldir {
752 my ($topdir, $subdir) = @_;
753 return $subdir =~ /^\// ? $subdir : $topdir.$subdir;
756 sub terminal_friendly_spawn {
757 my $actionmsg = shift;
758 my $sh = shift;
759 my $quiet = shift;
760 my $minimal = shift;
761 my $jobs = shift;
762 my $continuous = !$quiet && $minimal && 1 == $jobs;
763 my $output = "";
764 my $continuous_output = 0;
765 if ($terminal) {
766 my $pty = IO::Pty::Easy->new;
767 $pty->spawn($sh);
768 if ($continuous) {
769 $| = 1;
770 print "$actionmsg$erase_line\r" if $actionmsg;
771 while ($pty->is_active) {
772 my $data = $pty->getline();
773 if (defined $data && $data ne '') {
774 print "\n" if ($actionmsg && !$continuous_output);
775 $continuous_output = 1;
776 print $data;
780 else {
781 while ($pty->is_active) {
782 my $data = $pty->read();
783 $output .= $data if defined $data;
786 $pty->close;
788 else {
789 $output = qx/$sh 2>&1/;
791 my $ret = $?;
792 if ($quiet && $ret != 0) {
793 print "$actionmsg\n" if $actionmsg;
794 print STDERR $output;
796 elsif (!$quiet && (!$minimal || $output)) {
797 print "$actionmsg\n" if $actionmsg;
798 print $output;
800 return ($ret, ($output || $continuous_output) ? 1 : 0);
803 sub action {
804 my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
805 my $fulldir=fulldir($topdir, $subdir);
806 my $checkout_dir;
808 $ENV{MR_CONFIG}=$configfiles{$topdir};
809 my $is_checkout=($action eq 'checkout');
810 my $is_update=($action =~ /update/);
812 ($ENV{MR_REPO}=$dir) =~ s!/$!!;
813 $ENV{MR_ACTION}=$action;
815 foreach my $testname ("skip", "deleted") {
816 next if $force && $testname eq "skip";
818 my $testcommand=findcommand($testname, $dir, $topdir, $subdir, $is_checkout);
820 if (defined $testcommand) {
821 my $ret=runsh "$testname test", $topdir, $subdir,
822 $testcommand, [$action],
823 sub { system(shift()) };
824 if ($ret != 0) {
825 if (($? & 127) == 2) {
826 print STDERR "mr $action: interrupted\n";
827 return ABORT;
829 elsif ($? & 127) {
830 print STDERR "mr $action: $testname test received signal ".($? & 127)."\n";
831 return ABORT;
834 if ($ret >> 8 == 0) {
835 if ($testname eq "deleted") {
836 if (-d $dir) {
837 print STDERR "mr error: $dir should be deleted yet still exists\n";
838 return FAILED;
841 print "mr $action: skip $dir skipped\n" if $verbose;
842 return SKIPPED;
847 if ($is_checkout) {
848 $checkout_dir=$dir;
849 if (! $force_checkout) {
850 if (-d $dir) {
851 print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
852 return SKIPPED;
855 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
858 elsif ($is_update) {
859 if (! -d $dir) {
860 return action("checkout", $dir, $topdir, $subdir);
864 my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
866 if ($is_checkout && ! -d $dir) {
867 print "mr $action: creating parent directory $dir\n" if $verbose;
868 system("mkdir", "-p", $dir);
871 if (! $no_chdir && ! chdir($dir)) {
872 print STDERR "mr $action: failed to chdir to $dir: $!\n";
873 return FAILED;
875 elsif (! defined $command) {
876 my $vcs=vcs_test(@_);
877 if (! defined $vcs) {
878 print STDERR "mr $action: unknown repository type and no defined $action command for $fulldir\n";
879 return FAILED;
881 else {
882 print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n" unless $minimal;
883 return SKIPPED;
886 else {
887 my $actionmsg;
888 if (! $no_chdir) {
889 $actionmsg="mr $action: $fulldir";
891 else {
892 my $s=$directory;
893 $s=~s/^\Q$fulldir\E\/?//;
894 $actionmsg="mr $action: $fulldir (in subdir $s)";
896 print "$actionmsg\n" unless !$jobs || $jobs > 1 || $quiet || $minimal;
898 my ($hookret, $hook_out)=hook("pre_$action", $topdir, $subdir);
899 return $hookret if $hookret != OK;
901 my ($ret, $out)=runsh $action, $topdir, $subdir,
902 $command, \@ARGV, sub {
903 my $sh=shift;
904 if (!$jobs || $jobs > 1 || $quiet || $minimal) {
905 return terminal_friendly_spawn($actionmsg, $sh, $quiet, $minimal, $jobs);
907 else {
908 system($sh);
911 if ($ret != 0) {
912 if (($? & 127) == 2) {
913 print STDERR "mr $action: interrupted\n";
914 return ABORT;
916 elsif ($? & 127) {
917 print STDERR "mr $action: received signal ".($? & 127)."\n";
918 return ABORT;
920 print STDERR "mr $action: failed ($ret)\n" if $verbose;
921 if ($ret >> 8 != 0) {
922 print STDERR "mr $action: command failed\n";
923 if (-e "$ENV{HOME}/.mrlog" && $action ne 'remember') {
924 # recreate original command line to
925 # remember, and avoid recursing
926 my @orig=@ARGV;
927 @ARGV=('-n', $action, @orig);
928 action("remember", $dir, $topdir, $subdir);
929 @ARGV=@orig;
932 elsif ($ret != 0) {
933 print STDERR "mr $action: command died ($ret)\n";
935 return FAILED;
937 else {
938 if ($is_checkout && ! -d $dir) {
939 print STDERR "mr $action: $dir missing after checkout\n";;
940 return FAILED;
943 my ($ret, $hook_out)=hook("post_$action", $topdir, $subdir);
944 return $ret if $ret != OK;
946 if ($is_checkout || $is_update) {
947 if ($is_checkout && ! $no_chdir) {
948 if (! chdir($checkout_dir)) {
949 print STDERR "mr $action: failed to chdir to $checkout_dir: $!\n";
950 return FAILED;
953 my ($ret, $hook_out)=hook("fixups", $topdir, $subdir);
954 return $ret if $ret != OK;
957 return (OK, $out || $hook_out);
962 sub hook {
963 my ($hook, $topdir, $subdir) = @_;
965 my $command=$config{$topdir}{$subdir}{$hook};
966 return OK unless defined $command;
967 my ($ret,$out)=runsh $hook, $topdir, $subdir, $command, [], sub {
968 my $sh=shift;
969 if (!$jobs || $jobs > 1 || $quiet || $minimal) {
970 return terminal_friendly_spawn(undef, $sh, $quiet, $minimal, $jobs);
972 else {
973 system($sh);
976 if ($ret != 0) {
977 if (($? & 127) == 2) {
978 print STDERR "mr $hook: interrupted\n";
979 return ABORT;
981 elsif ($? & 127) {
982 print STDERR "mr $hook: received signal ".($? & 127)."\n";
983 return ABORT;
985 else {
986 return FAILED;
990 return (OK, $out);
993 # run actions on multiple repos, in parallel
994 sub mrs {
995 my $action=shift;
996 my @repos=@_;
998 $| = 1;
999 my @active;
1000 my @fhs;
1001 my @out;
1002 my $running=0;
1003 while (@fhs or @repos) {
1004 while ((!$jobs || $running < $jobs) && @repos) {
1005 $running++;
1006 my $repo = shift @repos;
1007 pipe(my $outfh, CHILD_STDOUT);
1008 pipe(my $errfh, CHILD_STDERR);
1009 my $pid;
1010 unless ($pid = fork) {
1011 die "mr $action: cannot fork: $!" unless defined $pid;
1012 open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
1013 open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
1014 close CHILD_STDOUT;
1015 close CHILD_STDERR;
1016 close $outfh;
1017 close $errfh;
1018 exit +(action($action, @$repo))[0];
1020 close CHILD_STDOUT;
1021 close CHILD_STDERR;
1022 push @active, [$pid, $repo];
1023 push @fhs, [$outfh, $errfh];
1024 push @out, ['', ''];
1026 my ($rin, $rout) = ('','');
1027 my $nfound;
1028 foreach my $fh (@fhs) {
1029 next unless defined $fh;
1030 vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
1031 vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
1033 $nfound = select($rout=$rin, undef, undef, 1);
1034 foreach my $channel (0, 1) {
1035 foreach my $i (0..$#fhs) {
1036 next unless defined $fhs[$i];
1037 my $fh = $fhs[$i][$channel];
1038 next unless defined $fh;
1039 if (vec($rout, fileno($fh), 1) == 1) {
1040 my $r = '';
1041 if (sysread($fh, $r, 1024) == 0) {
1042 close($fh);
1043 $fhs[$i][$channel] = undef;
1044 if (! defined $fhs[$i][0] &&
1045 ! defined $fhs[$i][1]) {
1046 waitpid($active[$i][0], 0);
1047 print STDOUT $out[$i][0];
1048 print STDERR $out[$i][1];
1049 record($active[$i][1], $? >> 8, $out[$i][0] || $out[$i][1]);
1050 splice(@fhs, $i, 1);
1051 splice(@active, $i, 1);
1052 splice(@out, $i, 1);
1053 $running--;
1056 $out[$i][$channel] .= $r;
1063 sub record {
1064 my $dir=shift()->[0];
1065 my $ret=shift;
1066 my $out=shift;
1068 if ($ret == OK) {
1069 push @ok, $dir;
1070 print "\n" unless $quiet || ($minimal && !$out);
1072 elsif ($ret == FAILED) {
1073 if ($interactive) {
1074 chdir($dir) unless $no_chdir;
1075 print STDERR "mr: Starting interactive shell. Exit shell to continue.\n";
1076 system((getpwuid($<))[8], "-i");
1078 push @failed, $dir;
1079 print "\n";
1081 elsif ($ret == SKIPPED) {
1082 push @skipped, $dir;
1084 elsif ($ret == ABORT) {
1085 exit 1;
1087 else {
1088 die "unknown exit status $ret";
1092 sub showstats {
1093 my $action=shift;
1094 if (! @ok && ! @failed && ! @skipped) {
1095 die "mr $action: no repositories found to work on\n";
1097 print "mr $action: finished (".join("; ",
1098 showstat($#ok+1, "ok", "ok"),
1099 showstat($#failed+1, "failed", "failed"),
1100 showstat($#skipped+1, "skipped", "skipped"),
1101 ).")\n" unless $quiet || $minimal;
1102 if ($stats) {
1103 if (@skipped) {
1104 print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet || $minimal;
1106 if (@failed) {
1107 print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
1112 sub showstat {
1113 my $count=shift;
1114 my $singular=shift;
1115 my $plural=shift;
1116 if ($count) {
1117 return "$count ".($count > 1 ? $plural : $singular);
1119 return;
1122 # an ordered list of repos
1123 sub repolist {
1124 my @list;
1125 foreach my $topdir (sort keys %config) {
1126 foreach my $subdir (sort keys %{$config{$topdir}}) {
1127 push @list, {
1128 topdir => $topdir,
1129 subdir => $subdir,
1130 order => $config{$topdir}{$subdir}{order},
1134 return sort {
1135 $a->{order} <=> $b->{order}
1137 $a->{topdir} cmp $b->{topdir}
1139 $a->{subdir} cmp $b->{subdir}
1140 } @list;
1143 sub absrepodir {
1144 my $repo=shift;
1145 my $topdir=$repo->{topdir};
1146 my $subdir=$repo->{subdir};
1147 my $ret=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
1148 $ret=~s/\/\.$//;
1149 my $absret=safe_abs_path($ret);
1150 return (defined $absret ? $absret : $ret);
1153 # Figure out which repos to act on. Returns a list of array refs
1154 # in the format:
1156 # [ "$full_repo_path/", "$mr_config_path/", $section_header ]
1157 sub selectrepos {
1158 my @repos;
1159 foreach my $repo (repolist()) {
1160 my $topdir=$repo->{topdir};
1161 my $subdir=$repo->{subdir};
1163 next if $subdir eq 'DEFAULT';
1164 my $dir=absrepodir($repo);
1165 my $d=$directory;
1166 $dir.="/" unless $dir=~/\/$/;
1167 $d.="/" unless $d=~/\/$/;
1168 next if $dir ne $d && $dir !~ /^\Q$d\E/;
1169 if (defined $max_depth) {
1170 my @a=split('/', $dir);
1171 my @b=split('/', $d);
1172 do { } while (@a && @b && shift(@a) eq shift(@b));
1173 next if @a > $max_depth || @b > $max_depth;
1175 push @repos, [$dir, $topdir, $subdir];
1177 if (! @repos) {
1178 # fallback to find a leaf repo
1179 foreach my $repo (reverse repolist()) {
1180 my $topdir=$repo->{topdir};
1181 my $subdir=$repo->{subdir};
1183 next if $subdir eq 'DEFAULT';
1184 my $dir=absrepodir($repo);
1185 my $d=$directory;
1186 $dir.="/" unless $dir=~/\/$/;
1187 $d.="/" unless $d=~/\/$/;
1188 if ($d=~/^\Q$dir\E/) {
1189 push @repos, [$dir, $topdir, $subdir];
1190 last;
1193 $no_chdir=1;
1195 return @repos;
1198 sub expandenv {
1199 my $val=shift;
1202 if ($val=~/\$/) {
1203 $val=`echo "$val"`;
1204 chomp $val;
1207 return $val;
1210 my %trusted;
1211 sub is_trusted_config {
1212 my $config=shift; # must be abs_pathed already
1214 # We always trust ~/.mrconfig.
1215 return 1 if $config eq safe_abs_path($HOME_MR_CONFIG);
1217 return 1 if $trust_all;
1219 my $trustfile=$ENV{HOME}."/.mrtrust";
1221 if (! %trusted) {
1222 $trusted{$HOME_MR_CONFIG}=1;
1223 if (open (TRUST, "<", $trustfile)) {
1224 while (<TRUST>) {
1225 chomp;
1226 s/^~\//$ENV{HOME}\//;
1227 my $d=safe_abs_path($_);
1228 $trusted{$d}=1 if defined $d;
1230 close TRUST;
1234 return $trusted{$config};
1238 sub is_trusted_repo {
1239 my $repo=shift;
1241 # Tightly limit what is allowed in a repo name.
1242 # No ../, no absolute paths, and no unusual filenames
1243 # that might try to escape to the shell.
1244 return $repo =~ /^[-_.+\/A-Za-z0-9]+$/ &&
1245 $repo !~ /\.\./ && $repo !~ /^\//;
1248 sub is_trusted_checkout {
1249 my $command=shift;
1251 # To determine if the command is safe, compare it with the
1252 # *_trusted_checkout config settings. Those settings are
1253 # templates for allowed commands, so make sure that each word
1254 # of the command matches the corresponding word of the template.
1256 my @words;
1257 foreach my $word (split(' ', $command)) {
1258 # strip quoting
1259 if ($word=~/^'(.*)'$/) {
1260 $word=$1;
1262 elsif ($word=~/^"(.*)"$/) {
1263 $word=$1;
1266 push @words, $word;
1269 foreach my $key (grep { /_trusted_checkout$/ }
1270 keys %{$config{''}{DEFAULT}}) {
1271 my @twords=split(' ', $config{''}{DEFAULT}{$key});
1272 next if @words > @twords;
1274 my $match=1;
1275 my $url;
1276 for (my $c=0; $c < @twords && $match; $c++) {
1277 if ($twords[$c] eq '$url') {
1278 # Match all the typical characters found in
1279 # urls, plus @ which svn can use. Note
1280 # that the "url" might also be a local
1281 # directory.
1282 $match=(
1283 defined $words[$c] &&
1284 $words[$c] =~ /^[-_.+:@\/A-Za-z0-9]+$/
1286 $url=$words[$c];
1288 elsif ($twords[$c] eq '$repo') {
1289 # If a repo is not specified, assume it
1290 # will be the last path component of the
1291 # url, or something derived from it, and
1292 # check that.
1293 if (! defined $words[$c] && defined $url) {
1294 ($words[$c])=$url=~/\/([^\/]+)\/?$/;
1297 $match=(
1298 defined $words[$c] &&
1299 is_trusted_repo($words[$c])
1302 elsif (defined $words[$c] && $words[$c]=~/^($twords[$c])$/) {
1303 $match=1;
1305 else {
1306 $match=0;
1309 return 1 if $match;
1312 return 0;
1315 my %loaded;
1316 sub loadconfig {
1317 my $f=shift;
1318 my $dir=shift;
1319 my $bootstrap_src=shift;
1321 my @toload;
1323 my $in;
1324 my $trusted;
1325 if (ref $f eq 'GLOB') {
1326 $dir="";
1327 $in=$f;
1328 $trusted=1;
1330 else {
1331 my $absf=safe_abs_path($f);
1332 if ($loaded{$absf}) {
1333 return;
1335 $loaded{$absf}=1;
1337 $trusted=is_trusted_config($absf);
1339 if (! defined $dir) {
1340 ($dir)=$f=~/^(.*\/)[^\/]+$/;
1341 if (! defined $dir) {
1342 $dir=".";
1346 $dir=safe_abs_path($dir)."/";
1348 if (! exists $configfiles{$dir}) {
1349 $configfiles{$dir}=$f;
1352 # copy in defaults from first parent
1353 my $parent=$dir;
1354 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
1355 if ($parent eq '/') {
1356 $parent="";
1358 if (exists $config{$parent} &&
1359 exists $config{$parent}{DEFAULT}) {
1360 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
1361 last;
1365 if (! -e $f) {
1366 return;
1369 print "mr: loading config $f\n" if $verbose;
1370 open($in, "<", $f) || die "mr: open $f: $!\n";
1372 my @lines=<$in>;
1373 close $in unless ref $f eq 'GLOB';
1375 my $section;
1377 # Keep track of the current line in the config file;
1378 # when a file is included track the current line from the include.
1379 my $lineno=0;
1380 my $included=undef;
1382 my $line;
1383 my $nextline = sub {
1384 if ($included) {
1385 $included--;
1387 else {
1388 $included=undef;
1389 $lineno++;
1391 $line=shift @lines;
1392 chomp $line;
1393 return $line;
1395 my $lineerror = sub {
1396 my $msg=shift;
1397 if (defined $included) {
1398 die "mr: $msg at $f line $lineno, included line: $line\n";
1400 else {
1401 die "mr: $msg at $f line $lineno\n";
1404 my $trusterror = sub {
1405 my $msg=shift;
1407 if (defined $bootstrap_src) {
1408 die "mr: $msg in untrusted $bootstrap_src line $lineno\n".
1409 "(To trust this url, --trust-all can be used; but please use caution;\n".
1410 "this can allow arbitrary code execution!)\n";
1412 else {
1413 die "mr: $msg in untrusted $f line $lineno\n".
1414 "(To trust this file, list it in ~/.mrtrust.)\n";
1418 while (@lines) {
1419 $_=$nextline->();
1421 next if /^\s*\#/ || /^\s*$/;
1423 if (! $trusted && /[[:cntrl:]]/) {
1424 $trusterror->("illegal control character");
1427 if (/^\[([^\]]*)\]\s*$/) {
1428 $section=$1;
1430 if (! $trusted) {
1431 if (! is_trusted_repo($section) ||
1432 $section eq 'ALIAS' ||
1433 $section eq 'DEFAULT') {
1434 $trusterror->("illegal section \"[$section]\"");
1437 $section=expandenv($section) if $trusted;
1438 if ($section ne 'ALIAS' &&
1439 ! exists $config{$dir}{$section} &&
1440 exists $config{$dir}{DEFAULT}) {
1441 # copy in defaults
1442 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
1445 elsif (/^(\w+)\s*=\s*(.*)/) {
1446 my $parameter=$1;
1447 my $value=$2;
1449 # continued value
1450 while (@lines && $lines[0]=~/^\s(.+)/) {
1451 $value.="\n$1";
1452 chomp $value;
1453 $nextline->();
1456 if (! $trusted) {
1457 # Untrusted files can only contain a few
1458 # settings in specific known-safe formats.
1459 if ($parameter eq 'checkout') {
1460 if (! is_trusted_checkout($value)) {
1461 $trusterror->("illegal checkout command \"$value\"");
1464 elsif ($parameter eq 'order') {
1465 # not interpreted as a command, so
1466 # safe.
1468 elsif ($value eq 'true' || $value eq 'false') {
1469 # skip=true , deleted=true etc are
1470 # safe.
1472 else {
1473 $trusterror->("illegal setting \"$parameter=$value\"");
1477 if ($parameter eq "include") {
1478 print "mr: including output of \"$value\"\n" if $verbose;
1479 my @inc=`$value`;
1480 if ($?) {
1481 print STDERR "mr: include command exited nonzero ($?)\n";
1483 $included += @inc;
1484 unshift @lines, @inc;
1485 next;
1488 if ($parameter eq 'jobs') {
1489 print "mr: setting --jobs to \"$value\"\n" if $verbose;
1490 unless ($value =~ /^\d+$/) {
1491 print "mr: error: --jobs must be numeric\n";
1492 exit 2
1494 $jobs=$value;
1495 next;
1498 if (! defined $section) {
1499 $lineerror->("parameter ($parameter) not in section");
1501 if ($section eq 'ALIAS') {
1502 $alias{$parameter}=$value;
1504 elsif ($parameter eq 'lib' or $parameter =~ s/_append$//) {
1505 $config{$dir}{$section}{$parameter}.="\n".$value."\n";
1507 else {
1508 $config{$dir}{$section}{$parameter}=$value;
1509 if ($parameter =~ /.*_(.*)/) {
1510 $knownactions{$1}=1;
1512 else {
1513 $knownactions{$parameter}=1;
1515 if ($parameter eq 'chain' &&
1516 length $dir && $section ne "DEFAULT") {
1517 my $chaindir="$section";
1518 if ($chaindir !~ m!^/!) {
1519 $chaindir=$dir.$chaindir;
1521 if (-e "$chaindir/.mrconfig") {
1522 my $ret=system($value);
1523 if ($ret != 0) {
1524 if (($? & 127) == 2) {
1525 print STDERR "mr: chain test interrupted\n";
1526 exit 2;
1528 elsif ($? & 127) {
1529 print STDERR "mr: chain test received signal ".($? & 127)."\n";
1532 else {
1533 push @toload, ["$chaindir/.mrconfig", $chaindir];
1539 else {
1540 $lineerror->("parse error");
1544 foreach my $c (@toload) {
1545 loadconfig(@$c);
1549 sub startingconfig {
1550 %alias=%config=%configfiles=%knownactions=%loaded=();
1551 my $datapos=tell(DATA);
1552 loadconfig(\*DATA);
1553 seek(DATA,$datapos,0); # rewind
1556 sub modifyconfig {
1557 my $f=shift;
1558 # the section to modify or add
1559 my $targetsection=shift;
1560 # fields to change in the section
1561 # To remove a field, set its value to "".
1562 my %changefields=@_;
1564 my @lines;
1565 my @out;
1567 if (-e $f) {
1568 open(my $in, "<", $f) || die "mr: open $f: $!\n";
1569 @lines=<$in>;
1570 close $in;
1573 my $formatfield=sub {
1574 my $field=shift;
1575 my @value=split(/\n/, shift);
1577 return "$field = ".shift(@value)."\n".
1578 join("", map { "\t$_\n" } @value);
1580 my $addfields=sub {
1581 my @blanks;
1582 while ($out[$#out] =~ /^\s*$/) {
1583 unshift @blanks, pop @out;
1585 foreach my $field (sort keys %changefields) {
1586 if (length $changefields{$field}) {
1587 push @out, "$field = $changefields{$field}\n";
1588 delete $changefields{$field};
1591 push @out, @blanks;
1594 my $section;
1595 while (@lines) {
1596 $_=shift(@lines);
1598 if (/^\s*\#/ || /^\s*$/) {
1599 push @out, $_;
1601 elsif (/^\[([^\]]*)\]\s*$/) {
1602 if (defined $section &&
1603 $section eq $targetsection) {
1604 $addfields->();
1607 $section=expandenv($1);
1609 push @out, $_;
1611 elsif (/^(\w+)\s*=\s(.*)/) {
1612 my $parameter=$1;
1613 my $value=$2;
1615 # continued value
1616 while (@lines && $lines[0]=~/^\s(.+)/) {
1617 shift(@lines);
1618 $value.="\n$1";
1619 chomp $value;
1622 if ($section eq $targetsection) {
1623 if (exists $changefields{$parameter}) {
1624 if (length $changefields{$parameter}) {
1625 $value=$changefields{$parameter};
1627 delete $changefields{$parameter};
1631 push @out, $formatfield->($parameter, $value);
1635 if (defined $section &&
1636 $section eq $targetsection) {
1637 $addfields->();
1639 elsif (%changefields) {
1640 push @out, "\n[$targetsection]\n";
1641 foreach my $field (sort keys %changefields) {
1642 if (length $changefields{$field}) {
1643 push @out, $formatfield->($field, $changefields{$field});
1648 open(my $out, ">", $f) || die "mr: write $f: $!\n";
1649 print $out @out;
1650 close $out;
1653 sub dispatch {
1654 my $action=shift;
1656 # actions that do not operate on all repos
1657 if ($action eq 'config') {
1658 config(@ARGV);
1660 elsif ($action eq 'register') {
1661 register(@ARGV);
1663 elsif ($action eq 'bootstrap') {
1664 bootstrap();
1666 elsif ($action eq 'remember' ||
1667 $action eq 'offline' ||
1668 $action eq 'online') {
1669 my @repos=selectrepos;
1670 action($action, @{$repos[0]}) if @repos;
1671 exit 0;
1674 if (!$jobs || $jobs > 1) {
1675 mrs($action, selectrepos());
1677 else {
1678 foreach my $repo (selectrepos()) {
1679 record($repo, action($action, @$repo));
1684 sub help {
1685 if (! -e "$ENV{MR_PATH}") {
1686 die "cannot find the program path";
1688 exec("perldoc", $ENV{MR_PATH}) || die "exec perldoc: $!";
1691 sub config {
1692 if (@_ < 2) {
1693 die "mr config: not enough parameters\n";
1695 my $section=shift;
1696 if ($section=~/^\//) {
1697 # try to convert to a path relative to the config file
1698 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
1699 $dir=safe_abs_path($dir);
1700 $dir.="/" unless $dir=~/\/$/;
1701 if ($section=~/^\Q$dir\E(.*)/) {
1702 $section=$1;
1705 my %changefields;
1706 foreach (@_) {
1707 if (/^([^=]+)=(.*)$/) {
1708 $changefields{$1}=$2;
1710 else {
1711 my $found=0;
1712 foreach my $topdir (sort keys %config) {
1713 if (exists $config{$topdir}{$section} &&
1714 exists $config{$topdir}{$section}{$_}) {
1715 print $config{$topdir}{$section}{$_}."\n";
1716 $found=1;
1717 last if $section eq 'DEFAULT';
1720 if (! $found) {
1721 die "mr config: $section $_ not set\n";
1725 modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
1726 exit 0;
1729 sub register {
1730 if ($config_overridden) {
1731 # Find the directory that the specified config file is
1732 # located in.
1733 ($directory)=safe_abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/;
1735 else {
1736 # Find the closest known mrconfig file to the current
1737 # directory.
1738 $directory.="/" unless $directory=~/\/$/;
1739 my $foundconfig=0;
1740 foreach my $topdir (reverse sort keys %config) {
1741 next unless length $topdir;
1742 if ($directory=~/^\Q$topdir\E/) {
1743 $ENV{MR_CONFIG}=$configfiles{$topdir};
1744 $directory=$topdir;
1745 $foundconfig=1;
1746 last;
1749 if (! $foundconfig) {
1750 $directory=""; # no config file, use builtin
1753 if (@ARGV) {
1754 my $subdir=shift @ARGV;
1755 if (! chdir($subdir)) {
1756 print STDERR "mr register: failed to chdir to $subdir: $!\n";
1760 $ENV{MR_REPO}=getcwd();
1761 my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
1762 if (! defined $command) {
1763 die "mr register: unknown repository type\n";
1766 $ENV{MR_REPO}=~s/.*\/(.*)/$1/;
1767 $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n".
1768 "my_action(){ $command\n }; my_action ".
1769 join(" ", map { s/\\/\\\\/g; s/"/\"/g; '"'.$_.'"' } @ARGV);
1770 print "mr register: running >>$command<<\n" if $verbose;
1771 exec($command) || die "exec: $!";
1774 sub bootstrap {
1775 eval q{use File::Copy};
1776 die $@ if $@;
1778 my $src=shift @ARGV;
1779 my $dir=shift @ARGV || ".";
1781 if (! defined $src || ! length $src) {
1782 die "mr: bootstrap requires source\n";
1785 # Retrieve config file.
1786 eval q{use File::Temp};
1787 die $@ if $@;
1788 my $tmpconfig=File::Temp->new();
1789 if ($src =~ m!^[\w\d]+://!) {
1790 # Download the config file to a temporary location.
1791 my @downloader;
1792 if ($src =~ m!^ssh://(.*)!) {
1793 @downloader = ("scp", $1, $tmpconfig);
1795 else {
1796 @downloader = ("curl", "-A", "mr", "-L", "-s", $src, "-o", $tmpconfig);
1797 push(@downloader, "-k") if $insecure;
1799 my $status = system(@downloader);
1800 die "mr bootstrap: invalid SSL certificate for $src (consider -k)\n"
1801 if $downloader[0] eq 'curl' && $status >> 8 == 60;
1802 die "mr bootstrap: download of $src failed\n" if $status != 0;
1804 elsif ($src eq '-') {
1805 # Config file is read from stdin.
1806 copy(\*STDIN, $tmpconfig) || die "stdin: $!";
1807 seek $tmpconfig, 0, 0;
1809 else {
1810 # Config file is local.
1811 die "mr bootstrap: cannot read file '$src'"
1812 unless -r $src;
1813 copy($src, $tmpconfig) || die "copy: $!";
1814 seek $tmpconfig, 0, 0;
1817 # Sanity check on destination directory.
1818 if (! -e $dir) {
1819 system("mkdir", "-p", $dir);
1821 chdir($dir) || die "chdir $dir: $!";
1823 # Special case to handle checkout of the "." repo, which
1824 # would normally be skipped.
1825 my $topdir=safe_abs_path(".")."/";
1826 my @repo=($topdir, $topdir, ".");
1827 loadconfig($tmpconfig, $topdir, $src);
1828 record(\@repo, action("checkout", @repo, 1))
1829 if exists $config{$topdir}{"."}{"checkout"};
1831 if (-e ".mrconfig") {
1832 print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $src\n";
1834 else {
1835 move($tmpconfig, ".mrconfig") || die "rename: $!";
1838 # Reload the config file (in case we got a different version)
1839 # and checkout everything else.
1840 startingconfig();
1841 loadconfig(".mrconfig");
1842 dispatch("checkout");
1843 @skipped=grep { safe_abs_path($_) ne safe_abs_path($topdir) } @skipped;
1844 showstats("bootstrap");
1845 exitstats();
1848 # alias expansion and command stemming
1849 sub expandaction {
1850 my $action=shift;
1851 if (exists $alias{$action}) {
1852 $action=$alias{$action};
1854 if (! exists $knownactions{$action}) {
1855 my @matches = grep { /^\Q$action\E/ }
1856 keys %knownactions, keys %alias;
1857 if (@matches == 1) {
1858 $action=$matches[0];
1860 elsif (@matches == 0) {
1861 die "mr: unknown action \"$action\" (known actions: ".
1862 join(", ", sort keys %knownactions).")\n";
1864 else {
1865 die "mr: ambiguous action \"$action\" (matches: ".
1866 join(", ", @matches).")\n";
1869 return $action;
1872 sub find_mrconfig {
1873 my $dir=getcwd();
1874 while (length $dir) {
1875 if (-e "$dir/.mrconfig") {
1876 return "$dir/.mrconfig";
1878 $dir=~s/\/[^\/]*$//;
1880 return $HOME_MR_CONFIG;
1883 sub getopts {
1884 my @saved=@ARGV;
1885 Getopt::Long::Configure("bundling", "no_permute");
1886 my $result=GetOptions(
1887 "d|directory=s" => sub { $directory=safe_abs_path($_[1]) },
1888 "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
1889 "p|path" => sub { }, # now default, ignore
1890 "f|force" => \$force,
1891 "force-env" => \$force_env,
1892 "v|verbose" => \$verbose,
1893 "m|minimal" => \$minimal,
1894 "q|quiet" => \$quiet,
1895 "s|stats" => \$stats,
1896 "k|insecure" => \$insecure,
1897 "i|interactive" => \$interactive,
1898 "n|no-recurse:i" => \$max_depth,
1899 "j|jobs:i" => \$jobs,
1900 "t|trust-all" => \$trust_all,
1902 if (! $result || @ARGV < 1) {
1903 die("Usage: mr [options] action [params ...]\n".
1904 "(Use mr help for man page.)\n");
1907 $ENV{MR_SWITCHES}="";
1908 foreach my $option (@saved) {
1909 last if $option eq $ARGV[0];
1910 $ENV{MR_SWITCHES}.="$option ";
1914 sub check {
1915 return if $force_env;
1916 my @env = qw(GIT_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_WORK_TREE VCSH_COMMAND VCSH_DIRECTORY VCSH_REPO_NAME);
1917 my $error;
1918 foreach (@env) {
1919 if ($ENV{$_}) {
1920 $error=1;
1921 print STDERR "mr: environment variable '$_' is set.\n";
1924 die ("mr: The variables above would lead to very interesting effects.
1925 Unfortunately, most of those effects result in data loss so we stop here.\n") if $error;
1928 sub init {
1929 $SIG{INT}=sub {
1930 print STDERR "$erase_line" if defined $erase_line && $terminal && !$quiet && $minimal && 1 == $jobs;
1931 print STDERR "mr: interrupted\n";
1932 exit 2;
1935 # This can happen if it's run in a directory that was removed
1936 # or other strangeness.
1937 if (! defined $directory) {
1938 die("mr: failed to determine working directory\n");
1940 # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
1941 # the config file might be a symlink to elsewhere, and the directory it's
1942 # in is significant.
1943 if ($ENV{MR_CONFIG} !~ /^\//) {
1944 $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
1946 # Try to set MR_PATH to the path to the program.
1947 eval {
1948 use FindBin qw($Bin $Script);
1949 $ENV{MR_PATH}=$Bin."/".$Script;
1953 sub exitstats {
1954 if (@failed) {
1955 exit 1;
1957 else {
1958 exit 0;
1962 # abs_path crashes on windows and some other platforms when given a file
1963 # that does not exist.
1964 sub safe_abs_path {
1965 my $f=shift;
1966 my $p=eval { abs_path($f) };
1967 if ($@) {
1968 return $f;
1970 else {
1971 return $p;
1975 sub main {
1976 getopts();
1977 check();
1978 init();
1979 help(@ARGV) if $ARGV[0] eq 'help';
1981 startingconfig();
1982 loadconfig($HOME_MR_CONFIG);
1983 loadconfig($ENV{MR_CONFIG});
1984 #use Data::Dumper; print Dumper(\%config);
1986 my $action=expandaction(shift @ARGV);
1987 dispatch($action);
1989 showstats($action);
1990 exitstats();
1993 # Finally, some useful actions that mr knows about by default.
1994 # These can be overridden in ~/.mrconfig.
1995 __DATA__
1996 [ALIAS]
1997 co = checkout
1998 ci = commit
1999 ls = list
2001 [DEFAULT]
2002 order = 10
2003 lib =
2004 error() {
2005 echo "mr: $@" >&2
2006 exit 1
2008 warning() {
2009 echo "mr (warning): $@" >&2
2011 info() {
2012 echo "mr: $@" >&2
2014 hours_since() {
2015 if [ -z "$1" ] || [ -z "$2" ]; then
2016 error "mr: usage: hours_since action num"
2018 for dir in .git .svn .bzr CVS .hg _darcs _FOSSIL_ .fslckout; do
2019 if [ -e "$MR_REPO/$dir" ]; then
2020 flagfile="$MR_REPO/$dir/.mr_last$1"
2021 break
2023 done
2024 if [ -z "$flagfile" ]; then
2025 error "cannot determine flag filename"
2027 delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
2028 if [ "$delta" -lt "$2" ]; then
2029 return 1
2030 else
2031 touch "$flagfile"
2032 return 0
2035 is_bzr_checkout() {
2036 LANG=C bzr info | egrep -q '^Checkout'
2038 lazy() {
2039 if [ -d "$MR_REPO" ]; then
2040 return 1
2041 else
2042 return 0
2046 svn_test = perl: -d "$ENV{MR_REPO}/.svn"
2047 git_test = perl: -e "$ENV{MR_REPO}/.git"
2048 bzr_test = perl: -d "$ENV{MR_REPO}/.bzr"
2049 cvs_test = perl: -d "$ENV{MR_REPO}/CVS"
2050 hg_test = perl: -d "$ENV{MR_REPO}/.hg"
2051 darcs_test = perl: -d "$ENV{MR_REPO}/_darcs"
2052 fossil_test = perl: -f "$ENV{MR_REPO}/_FOSSIL_" || -f "$ENV{MR_REPO}/.fslckout"
2053 git_bare_test = perl:
2054 -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
2055 -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
2056 `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get core.bare` =~ /true/
2057 vcsh_test = perl:
2058 -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
2059 -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
2060 `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get vcsh.vcsh` =~ /true/
2061 veracity_test = perl: -d "$ENV{MR_REPO}/.sgdrawer"
2063 svn_update = svn update "$@"
2064 git_update = git pull "$@"
2065 bzr_update =
2066 if is_bzr_checkout; then
2067 bzr update "$@"
2068 else
2069 bzr merge --pull "$@"
2071 cvs_update = cvs -q update "$@"
2072 hg_update = hg pull "$@"; hg update "$@"
2073 darcs_update = darcs pull -a "$@"
2074 fossil_update = fossil pull "$@"
2075 vcsh_update = vcsh run "$MR_REPO" git pull "$@"
2076 veracity_update = vv pull "$@" && vv update "$@"
2078 git_fetch = git fetch --all --prune --tags "$@"
2079 git_svn_fetch = git svn fetch "$@"
2080 darcs_fetch = darcs fetch "$@"
2081 hg_fetch = hg pull "$@"
2083 svn_clean =
2084 if [ "x$1" = x-f ] ; then
2085 shift
2086 svn-clean "$@"
2087 else
2088 svn-clean --print "$@"
2090 git_clean =
2091 if [ "x$1" = x-f ] ; then
2092 shift
2093 git clean -dx --force "$@"
2094 else
2095 git clean -dx --dry-run "$@"
2097 git_svn_clean =
2098 if [ "x$1" = x-f ] ; then
2099 shift
2100 git clean -dx --force "$@"
2101 else
2102 git clean -dx --dry-run "$@"
2104 bzr_clean =
2105 if [ "x$1" = x-f ] ; then
2106 shift
2107 bzr clean-tree --verbose --force --ignored --unknown --detritus "$@"
2108 else
2109 bzr clean-tree --verbose --dry-run --ignored --unknown --detritus "$@"
2111 cvs_clean =
2112 if [ "x$1" = x-f ] ; then
2113 shift
2114 cvs-clean "$@"
2115 else
2116 cvs-clean --dry-run "$@"
2118 hg_clean =
2119 if [ "x$1" = x-f ] ; then
2120 shift
2121 hg purge --print --all "$@"
2122 hg purge --all "$@"
2123 else
2124 hg purge --print --all "$@"
2126 fossil_clean =
2127 if [ "x$1" = x-f ] ; then
2128 shift
2129 fossil clean --dry-run --dotfiles --emptydirs "$@"
2130 else
2131 fossil clean --force --dotfiles --emptydirs "$@"
2133 vcsh_clean =
2134 if [ "x$1" = x-f ] ; then
2135 shift
2136 vcsh run "$MR_REPO" git clean -dx "$@"
2137 else
2138 vcsh run "$MR_REPO" git clean -dx --dry-run "$@"
2141 svn_status = svn status "$@"
2142 git_status = git status -s "$@" || true; git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline || true; git --no-pager stash list
2143 bzr_status = bzr status --short "$@"; bzr missing
2144 cvs_status = cvs -q status | grep -E '^(File:.*Status:|\?)' | grep -v 'Status: Up-to-date' || true
2145 hg_status = hg status "$@"; hg summary --quiet | grep -v 'parent: 0:'
2146 darcs_status = darcs whatsnew -ls "$@" || true
2147 fossil_status = fossil changes "$@"
2148 vcsh_status = vcsh status "$MR_REPO" "$@" || true
2149 veracity_status = vv status "$@"
2151 svn_commit = svn commit "$@"
2152 git_commit = git commit -a "$@" && git push --all
2153 bzr_commit =
2154 if is_bzr_checkout; then
2155 bzr commit "$@"
2156 else
2157 bzr commit "$@" && bzr push
2159 cvs_commit = cvs commit "$@"
2160 hg_commit = hg commit "$@" && hg push
2161 darcs_commit = darcs record -a "$@" && darcs push -a
2162 fossil_commit = fossil commit "$@"
2163 vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all
2164 veracity_commit = vv commit "$@" && vv push
2166 git_record = git commit -a "$@"
2167 bzr_record =
2168 if is_bzr_checkout; then
2169 bzr commit --local "$@"
2170 else
2171 bzr commit "$@"
2173 hg_record = hg commit "$@"
2174 darcs_record = darcs record -a "$@"
2175 fossil_record = fossil commit "$@"
2176 vcsh_record = vcsh run "$MR_REPO" git commit -a "$@"
2177 veracity_record = vv commit "$@"
2179 svn_push = :
2180 git_push = git push "$@"
2181 bzr_push = bzr push "$@"
2182 cvs_push = :
2183 hg_push = hg push "$@" || if [ "$?" -eq "255" ] ; then exit 1; else exit 0; fi
2184 darcs_push = darcs push -a "$@"
2185 fossil_push = fossil push "$@"
2186 vcsh_push = vcsh run "$MR_REPO" git push "$@"
2187 veracity_push = vv push "$@"
2189 svn_diff = svn diff "$@"
2190 git_diff = git diff "$@"
2191 bzr_diff = bzr diff "$@"
2192 cvs_diff = cvs -q diff "$@"
2193 hg_diff = hg diff "$@"
2194 darcs_diff = darcs diff -u "$@"
2195 fossil_diff = fossil diff "$@"
2196 vcsh_diff = vcsh run "$MR_REPO" git diff "$@"
2197 veracity_diff = vv diff "$@"
2199 svn_log = svn log "$@"
2200 git_log = git log "$@"
2201 bzr_log = bzr log "$@"
2202 cvs_log = cvs log "$@"
2203 hg_log = hg log "$@"
2204 darcs_log = darcs changes "$@"
2205 git_bare_log = git log "$@"
2206 fossil_log = fossil timeline "$@"
2207 vcsh_log = vcsh run "$MR_REPO" git log "$@"
2208 veracity_log = vv log "$@"
2210 hg_grep = hg grep "$@"
2211 cvs_grep = ack-grep "$@"
2212 svn_grep = ack-grep "$@"
2213 git_svn_grep = git grep "$@"
2214 git_grep = git grep "$@"
2215 bzr_grep = ack-grep "$@"
2216 darcs_grep = ack-grep "$@"
2218 run = "$@"
2220 svn_register =
2221 url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
2222 if [ -z "$url" ]; then
2223 error "cannot determine svn url"
2225 echo "Registering svn url: $url in $MR_CONFIG"
2226 mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
2227 git_register =
2228 url="`LC_ALL=C git config --get remote.origin.url`" || true
2229 if [ -z "$url" ]; then
2230 error "cannot determine git url"
2232 echo "Registering git url: $url in $MR_CONFIG"
2233 mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
2234 bzr_register =
2235 url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}' | head -n 1`"
2236 if [ -z "$url" ]; then
2237 error "cannot determine bzr url"
2239 echo "Registering bzr url: $url in $MR_CONFIG"
2240 mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr branch '$url' '$MR_REPO'"
2241 cvs_register =
2242 repo=`cat CVS/Repository`
2243 root=`cat CVS/Root`
2244 if [ -z "$root" ]; then
2245 error "cannot determine cvs root"
2247 echo "Registering cvs repository $repo at root $root"
2248 mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
2249 hg_register =
2250 url=`hg showconfig paths.default`
2251 echo "Registering mercurial repo url: $url in $MR_CONFIG"
2252 mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
2253 darcs_register =
2254 url=`cat _darcs/prefs/defaultrepo`
2255 echo "Registering darcs repository $url in $MR_CONFIG"
2256 mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
2257 git_bare_register =
2258 url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
2259 if [ -z "$url" ]; then
2260 error "cannot determine git url"
2262 echo "Registering git url: $url in $MR_CONFIG"
2263 mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
2264 vcsh_register =
2265 mr_repo_basename=`basename "$MR_REPO" .git`
2266 url="`LC_ALL=C vcsh run "$mr_repo_basename" git config --get remote.origin.url`" || true
2267 if [ -z "$url" ]; then
2268 error "cannot determine git url"
2270 echo "Registering git url: $url in $MR_CONFIG"
2271 mr -c "$MR_CONFIG" config "`pwd`" checkout="vcsh clone '$url' '$mr_repo_basename'"
2272 fossil_register =
2273 url=`fossil remote-url`
2274 repo=`fossil info | grep repository | sed -e 's/repository:*.//g' -e 's/ //g'`
2275 echo "Registering fossil repository $url in $MR_CONFIG"
2276 mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && fossil open '$repo'"
2277 veracity_register =
2278 url=`vv config | grep sync_targets | sed -e 's/sync_targets:*.//g' -e 's/ //g'`
2279 repo=`vv repo info | grep repository | sed -e 's/Current repository:*.//g' -e 's/ //g'`
2280 echo "Registering veracity repository $url in $MR_CONFIG"
2281 mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && vv checkout '$repo'"
2283 svn_trusted_checkout = svn co $url $repo
2284 svn_alt_trusted_checkout = svn checkout $url $repo
2285 git_trusted_checkout = git clone $url $repo
2286 bzr_trusted_checkout = bzr checkout|clone|branch|get $url $repo
2287 # cvs: too hard
2288 hg_trusted_checkout = hg clone $url $repo
2289 darcs_trusted_checkout = darcs get $url $repo
2290 git_bare_trusted_checkout = git clone --bare $url $repo
2291 vcsh_old_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
2292 vcsh_trusted_checkout = vcsh clone $url $repo
2293 # fossil: messy to do
2294 veracity_trusted_checkout = vv clone $url $repo
2297 list = true
2298 config =
2299 bootstrap =
2301 online =
2302 if [ -s ~/.mrlog ]; then
2303 info "running offline commands"
2304 mv -f ~/.mrlog ~/.mrlog.old
2305 if ! sh -e ~/.mrlog.old; then
2306 error "offline command failed; left in ~/.mrlog.old"
2308 rm -f ~/.mrlog.old
2309 else
2310 info "no offline commands to run"
2312 offline =
2313 umask 077
2314 touch ~/.mrlog
2315 info "offline mode enabled"
2316 remember =
2317 info "remembering command: 'mr $@'"
2318 command="mr -d '$(pwd)' $MR_SWITCHES"
2319 for w in "$@"; do
2320 command="$command '$w'"
2321 done
2322 if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
2323 echo "$command" >> ~/.mrlog
2326 ed = echo "A horse is a horse, of course, of course.."
2327 T = echo "I pity the fool."
2328 right = echo "Not found."
2330 # vim:sw=8:sts=0:ts=8:noet
2331 # Local variables:
2332 # indent-tabs-mode: t
2333 # cperl-indent-level: 8
2334 # End: