X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/eae9714e0e4c5b5b650dd1ac8b52f7a516cca91a..ae4ab73b5d793b03f91fdcef0fff7405f9f9d75b:/mr?ds=sidebyside diff --git a/mr b/mr index 91eb151..b37d9a5 100755 --- a/mr +++ b/mr @@ -16,11 +16,15 @@ B [options] commit [-m "message"] B [options] record [-m "message"] +B [options] push + B [options] diff B [options] log -B [options] bootstrap url +B [options] run command [param ...] + +B [options] bootstrap url [directory] B [options] register [repository] @@ -37,8 +41,8 @@ B [options] remember action [params ...] B is a Multiple Repository management tool. It can checkout, update, or perform other actions on a set of repositories as if they were one combined repository. It supports any combination of subversion, git, cvs, mercurial, -bzr and darcs repositories, and support for other revision control systems can -easily be added. +bzr, darcs and fossil repositories, and support for other version +control systems can easily be added. B cds into and operates on all registered repositories at or below your working directory. Or, if you are in a subdirectory of a repository that @@ -47,9 +51,11 @@ and work on only that repository, B is configured by .mrconfig files, which list the repositories. It starts by reading the .mrconfig file in your home directory, and this can -in turn chain load .mrconfig files from repositories. +in turn chain load .mrconfig files from repositories. It also automatically +looks for a .mrconfig file in the current directory, or in one of its +parent directories. -These predefined commands should be fairly familiar to users of any revision +These predefined commands should be fairly familiar to users of any version control system: =over 4 @@ -81,14 +87,14 @@ The optional -m parameter allows specifying a commit message. =item record Records changes to the local repository, but does not push them to the -remote repository. Only supported for distributed revision control systems. +remote repository. Only supported for distributed version control systems. The optional -m parameter allows specifying a commit message. =item push Pushes committed local changes to the remote repository. A no-op for -centralized revision control systems. +centralized version control systems. =item diff @@ -98,16 +104,26 @@ Show a diff of uncommitted changes. Show the commit log. +=item run command [param ...] + +Runs the specified command in each repository. + =back These commands are also available: =over 4 -=item bootstrap url +=item bootstrap url [directory] -Causes mr to download the url, save it to a .mrconfig file in the -current directory, and then check out all repositories listed in it. +Causes mr to download the url, and use it as a .mrconfig file +to checkout the repositories listed in it, into the specified directory. + +The directory will be created if it does not exist. If no directory is +specified, the current directory will be used. + +If the .mrconfig file includes a repository named ".", that +is checked out into the top of the specified directory. =item list (or ls) @@ -120,7 +136,7 @@ repository in the current directory is registered, or you can specify a directory to register. The mrconfig file that is modified is chosen by either the -c option, or by -looking for the closest known one at or below the current directory. +looking for the closest known one at or in a parent of the current directory. =item config @@ -141,8 +157,8 @@ To see the built-in library of shell functions contained in mr: mr config DEFAULT lib -The ~/.mrconfig file is used by default. To use a different config file, -use the -c option. +The mrconfig file that is used is chosen by either the -c option, or by +looking for the closest known one at or in a parent of the current directory. =item offline @@ -173,8 +189,8 @@ Actions can be abbreviated to any unambiguous substring, so update" Additional parameters can be passed to most commands, and are passed on -unchanged to the underlying revision control system. This is mostly useful -if the repositories mr will act on all use the same revision control +unchanged to the underlying version control system. This is mostly useful +if the repositories mr will act on all use the same version control system. =head1 OPTIONS @@ -192,14 +208,9 @@ the current working directory. =item --config mrconfig -Use the specified mrconfig file. The default is B<~/.mrconfig> - -=item -p - -=item --path - -Search in the current directory, and its parent directories and use -the first B<.mrconfig> found, instead of the default B<~/.mrconfig>. +Use the specified mrconfig file. The default is to use both B<~/.mrconfig> +as well as look for a .mrconfig file in the current directory, or in one +of its parent directories. =item -v @@ -211,7 +222,15 @@ Be verbose. =item --quiet -Be quiet. +Be quiet. This suppresses mr's usual output, as well as any output from +commands that are run (including stderr output). If a command fails, +the output will be shown. + +=item -k + +=item --insecure + +Accept untrusted SSL certificates when bootstrapping. =item -s @@ -258,14 +277,20 @@ a good speedup in updates without loading the machine too much. Trust all mrconfig files even if they are not listed in ~/.mrtrust. Use with caution. +=item -p + +=item --path + +This obsolete flag is ignored. + =back -=head1 "MRCONFIG FILES" +=head1 MRCONFIG FILES Here is an example .mrconfig file: [src] - checkout = svn co svn://svn.example.com/src/trunk src + checkout = svn checkout svn://svn.example.com/src/trunk src chain = true [src/linux-2.6] @@ -292,14 +317,18 @@ will be passed through the shell for expansion. For example, Within a section, each parameter defines a shell command to run to handle a given action. mr contains default handlers for "update", "status", -"commit", and other standard actions. Normally you only need to specify what -to do for "checkout". +"commit", and other standard actions. + +Normally you only need to specify what to do for "checkout". Here you +specify the command to run in order to create a checkout of the repository. +The command will be run in the parent directory, and must create the +repository's directory. So use "git clone", "svn checkout", "bzr branch" +or "bzr checkout" (for a bound branch), etc. Note that these shell commands are run in a "set -e" shell environment, where any additional parameters you pass are available in -"$@". The "checkout" command is run in the parent of the repository -directory, since the repository isn't checked out yet. All other commands -are run inside the repository, though not necessarily at the top of it. +"$@". All commands other than "checkout" are run inside the repository, +though not necessarily at the top of it. The "MR_REPO" environment variable is set to the path to the top of the repository. (For the "register" action, "MR_REPO" is instead set to the @@ -310,6 +339,9 @@ The "MR_CONFIG" environment variable is set to the .mrconfig file that defines the repo being acted on, or, if the repo is not yet in a config file, the .mrconfig file that should be modified to register the repo. +The "MR_ACTION" environment variable is set to the command being run +(update, checkout, etc). + A few parameters have special meanings: =over 4 @@ -325,8 +357,21 @@ mr is run by joey. The second uses the hours_since function (included in mr's built-in library) to skip updating the repo unless it's been at least 12 hours since the last update. + [mystuff] + checkout = ... skip = test `whoami` != joey + + [linux] + checkout = ... skip = [ "$1" = update ] && ! hours_since "$1" 12 + +Another way to use skip is for a lazy checkout. This makes mr skip +operating on a repo unless it already exists. To enable the +repo, you have to explicitly check it out (using "mr -d foo checkout"). + + [foo] + checkout = ... + skip = lazy =item order @@ -352,46 +397,78 @@ part of the including file. Unlike all other parameters, this parameter does not need to be placed within a section. +B ships several libraries that can be included to add support for +additional version control type things (unison, git-svn, vcsh, git-fake-bare, +git-subtree). To include them all, you could use: + + include = cat /usr/share/mr/* + +See the individual files for details. + +=item deleted + +If the "deleted" parameter is set and its command returns true, then +B will treat the repository as deleted. It won't ever actually delete +the repository, but it will warn if it sees the repository's directory. +This is useful when one mrconfig file is shared amoung multiple machines, +to keep track of and remember to delete old repositories. + =item lib The "lib" parameter can specify some shell code that will be run before each command, this can be a useful way to define shell functions for other commands to use. +=item fixups + +If the "fixups" parameter is set, its command is run whenever a repository +is checked out, or updated. This provides an easy way to do things +like permissions fixups, or other tweaks to the repository content, +whenever the repository is changed. + +=item pre_ and post_ + +If a "pre_action" parameter is set, its command is run before mr performs the +specified action. Similarly, "post_action" parameters are run after mr +successfully performs the specified action. For example, "pre_commit" is +run before committing; "post_update" is run after updating. + =back When looking for a command to run for a given action, mr first looks for a parameter with the same name as the action. If that is not found, it -looks for a parameter named "rcs_action" (substituting in the name of the -revision control system and the action). The name of the revision control -system is itself determined by running each defined "rcs_test" action, +looks for a parameter named "VCS_action" (substituting in the name of the +version control system and the action). The name of the version control +system is itself determined by running each defined "VCS_test" action, until one succeeds. Internally, mr has settings for "git_update", "svn_update", etc. To change -the action that is performed for a given revision control system, you can -override these rcs specific actions. To add a new revision control system, -you can just add rcs specific actions for it. +the action that is performed for a given version control system, you can +override these VCS specific actions. To add a new version control system, +you can just add VCS specific actions for it. -The ~/.mrlog file contains commands that mr has remembered to run later, -due to being offline. You can delete or edit this file to remove commands, -or even to add other commands for 'mr online' to run. If the file is -present, mr assumes it is in offline mode. - -=head "UNTRUSTED MRCONFIG FILES" +=head1 UNTRUSTED MRCONFIG FILES Since mrconfig files can contain arbitrary shell commands, they can do -anything. This flexability is good, but it also allows a malicious mrconfig +anything. This flexibility is good, but it also allows a malicious mrconfig file to delete your whole home directory. Such a file might be contained -inside a repository that your main ~/.mrconfig checks out and chains to. To -avoid worries about evil commands in a mrconfig file, mr -has the ability to read mrconfig files in untrusted mode. Such files are -limited to running only known safe commands (like "git clone") in a -carefully checked manner. +inside a repository that your main ~/.mrconfig checks out. To +avoid worries about evil commands in a mrconfig file, mr defaults to +reading all mrconfig files other than the main ~/.mrconfig in untrusted +mode. In untrusted mode, mrconfig files are limited to running only known +safe commands (like "git clone") in a carefully checked manner. + +To configure mr to trust other mrconfig files, list them in ~/.mrtrust. +One mrconfig file should be listed per line. Either the full pathname +should be listed, or the pathname can start with "~/" to specify a file +relative to your home directory. -By default, mr trusts all mrconfig files. (This default will change in a -future release!) But if you have a ~/.mrtrust file, mr will only trust -mrconfig files that are listed within it. (One file per line.) All other -files will be treated as untrusted. +=head1 OFFLINE LOG FILE + +The ~/.mrlog file contains commands that mr has remembered to run later, +due to being offline. You can delete or edit this file to remove commands, +or even to add other commands for 'mr online' to run. If the file is +present, mr assumes it is in offline mode. =head1 EXTENSIONS @@ -399,9 +476,13 @@ mr can be extended to support things such as unison and git-svn. Some files providing such extensions are available in /usr/share/mr/. See the documentation in the files for details about using them. +=head1 EXIT STATUS + +mr returns nonzero if a command failed in any of the repositories. + =head1 AUTHOR -Copyright 2007-2009 Joey Hess +Copyright 2007-2011 Joey Hess Licensed under the GNU GPL version 2 or higher. @@ -427,13 +508,15 @@ my $config_overridden=0; my $verbose=0; my $quiet=0; my $stats=0; +my $insecure=0; my $interactive=0; my $max_depth; my $no_chdir=0; my $jobs=1; my $trust_all=0; my $directory=getcwd(); -$ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig"; + +$ENV{MR_CONFIG}=find_mrconfig(); # globals :-( my %config; @@ -444,41 +527,41 @@ my (@ok, @failed, @skipped); main(); -my %rcs; -sub rcs_test { +my %vcs; +sub vcs_test { my ($action, $dir, $topdir, $subdir) = @_; - if (exists $rcs{$dir}) { - return $rcs{$dir}; + if (exists $vcs{$dir}) { + return $vcs{$dir}; } my $test="set -e\n"; - foreach my $rcs_test ( + foreach my $vcs_test ( sort { length $a <=> length $b || $a cmp $b } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) { - my ($rcs)=$rcs_test=~/(.*)_test/; - $test="my_$rcs_test() {\n$config{$topdir}{$subdir}{$rcs_test}\n}\n".$test; - $test.="if my_$rcs_test; then echo $rcs; fi\n"; + my ($vcs)=$vcs_test=~/(.*)_test/; + $test="my_$vcs_test() {\n$config{$topdir}{$subdir}{$vcs_test}\n}\n".$test; + $test.="if my_$vcs_test; then echo $vcs; fi\n"; } $test=$config{$topdir}{$subdir}{lib}."\n".$test if exists $config{$topdir}{$subdir}{lib}; - print "mr $action: running rcs test >>$test<<\n" if $verbose; - my $rcs=`$test`; - chomp $rcs; - if ($rcs=~/\n/s) { - $rcs=~s/\n/, /g; - print STDERR "mr $action: found multiple possible repository types ($rcs) for $topdir$subdir\n"; + print "mr $action: running vcs test >>$test<<\n" if $verbose; + my $vcs=`$test`; + chomp $vcs; + if ($vcs=~/\n/s) { + $vcs=~s/\n/, /g; + print STDERR "mr $action: found multiple possible repository types ($vcs) for ".fulldir($topdir, $subdir)."\n"; return undef; } - if (! length $rcs) { - return $rcs{$dir}=undef; + if (! length $vcs) { + return $vcs{$dir}=undef; } else { - return $rcs{$dir}=$rcs; + return $vcs{$dir}=$vcs; } } @@ -493,65 +576,84 @@ sub findcommand { return undef; } - my $rcs=rcs_test(@_); + my $vcs=vcs_test(@_); - if (defined $rcs && - exists $config{$topdir}{$subdir}{$rcs."_".$action}) { - return $config{$topdir}{$subdir}{$rcs."_".$action}; + if (defined $vcs && + exists $config{$topdir}{$subdir}{$vcs."_".$action}) { + return $config{$topdir}{$subdir}{$vcs."_".$action}; } else { return undef; } } +sub fulldir { + my ($topdir, $subdir) = @_; + return $subdir =~ /^\// ? $subdir : $topdir.$subdir; +} + sub action { - my ($action, $dir, $topdir, $subdir) = @_; - + my ($action, $dir, $topdir, $subdir, $force_checkout) = @_; + my $fulldir=fulldir($topdir, $subdir); + $ENV{MR_CONFIG}=$configfiles{$topdir}; my $lib=exists $config{$topdir}{$subdir}{lib} ? $config{$topdir}{$subdir}{lib}."\n" : ""; my $is_checkout=($action eq 'checkout'); + my $is_update=($action =~ /update/); - $ENV{MR_REPO}=$dir; + ($ENV{MR_REPO}=$dir) =~ s!/$!!; + $ENV{MR_ACTION}=$action; + + foreach my $testname ("skip", "deleted") { + my $testcommand=findcommand($testname, $dir, $topdir, $subdir, $is_checkout); + + if (defined $testcommand) { + my $test="set -e;".$lib. + "my_action(){ $testcommand\n }; my_action '$action'"; + print "mr $action: running $testname test >>$test<<\n" if $verbose; + my $ret=system($test); + if ($ret != 0) { + if (($? & 127) == 2) { + print STDERR "mr $action: interrupted\n"; + return ABORT; + } + elsif ($? & 127) { + print STDERR "mr $action: $testname test received signal ".($? & 127)."\n"; + return ABORT; + } + } + if ($ret >> 8 == 0) { + if ($testname eq "deleted") { + if (-d $dir) { + print STDERR "mr error: $dir should be deleted yet still exists\n"; + return FAILED; + } + } + print "mr $action: skip $dir skipped\n" if $verbose; + return SKIPPED; + } + } + } if ($is_checkout) { - if (-d $dir) { - print "mr $action: $dir already exists, skipping checkout\n" if $verbose; - return SKIPPED; + if (! $force_checkout) { + if (-d $dir) { + print "mr $action: $dir already exists, skipping checkout\n" if $verbose; + return SKIPPED; + } + + $dir=~s/^(.*)\/[^\/]+\/?$/$1/; } - - $dir=~s/^(.*)\/[^\/]+\/?$/$1/; } - elsif ($action =~ /update/) { + elsif ($is_update) { if (! -d $dir) { return action("checkout", $dir, $topdir, $subdir); } } - my $skiptest=findcommand("skip", $dir, $topdir, $subdir, $is_checkout); my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout); - if (defined $skiptest) { - my $test="set -e;".$lib. - "my_action(){ $skiptest\n }; my_action '$action'"; - print "mr $action: running skip test >>$test<<\n" if $verbose; - my $ret=system($test); - if ($ret != 0) { - if (($? & 127) == 2) { - print STDERR "mr $action: interrupted\n"; - return ABORT; - } - elsif ($? & 127) { - print STDERR "mr $action: skip test received signal ".($? & 127)."\n"; - return ABORT; - } - } - if ($ret >> 8 == 0) { - print "mr $action: $dir skipped per config file\n" if $verbose; - return SKIPPED; - } - } - if ($is_checkout && ! -d $dir) { print "mr $action: creating parent directory $dir\n" if $verbose; system("mkdir", "-p", $dir); @@ -562,30 +664,47 @@ sub action { return FAILED; } elsif (! defined $command) { - my $rcs=rcs_test(@_); - if (! defined $rcs) { - print STDERR "mr $action: unknown repository type and no defined $action command for $topdir$subdir\n"; + my $vcs=vcs_test(@_); + if (! defined $vcs) { + print STDERR "mr $action: unknown repository type and no defined $action command for $fulldir\n"; return FAILED; } else { - print STDERR "mr $action: no defined action for $rcs repository $topdir$subdir, skipping\n"; + print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n"; return SKIPPED; } } else { + my $actionmsg; if (! $no_chdir) { - print "mr $action: $topdir$subdir\n" unless $quiet; + $actionmsg="mr $action: $fulldir"; } else { my $s=$directory; - $s=~s/^\Q$topdir$subdir\E\/?//; - print "mr $action: $topdir$subdir (in subdir $s)\n" unless $quiet; + $s=~s/^\Q$fulldir\E\/?//; + $actionmsg="mr $action: $fulldir (in subdir $s)"; } + print "$actionmsg\n" unless $quiet; + + my $hookret=hook("pre_$action", $topdir, $subdir); + return $hookret if $hookret != OK; + $command="set -e; ".$lib. "my_action(){ $command\n }; my_action ". - join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV); + join(" ", map { s/\\/\\\\/g; s/"/\"/g; '"'.$_.'"' } @ARGV); print "mr $action: running >>$command<<\n" if $verbose; - my $ret=system($command); + my $ret; + if ($quiet) { + my $output = qx/$command 2>&1/; + $ret = $?; + if ($ret != 0) { + print "$actionmsg\n"; + print STDERR $output; + } + } + else { + $ret=system($command); + } if ($ret != 0) { if (($? & 127) == 2) { print STDERR "mr $action: interrupted\n"; @@ -613,16 +732,62 @@ sub action { return FAILED; } else { - if ($action eq 'checkout' && ! -d $dir) { + if ($is_checkout && ! -d $dir) { print STDERR "mr $action: $dir missing after checkout\n";; return FAILED; } + my $ret=hook("post_$action", $topdir, $subdir); + return $ret if $ret != OK; + + if (($is_checkout || $is_update)) { + my $ret=hook("fixups", $topdir, $subdir); + return $ret if $ret != OK; + } + return OK; } } } +sub hook { + my ($hook, $topdir, $subdir) = @_; + + my $command=$config{$topdir}{$subdir}{$hook}; + return OK unless defined $command; + my $lib=exists $config{$topdir}{$subdir}{lib} ? + $config{$topdir}{$subdir}{lib}."\n" : ""; + my $shell="set -e;".$lib. + "my_hook(){ $command\n }; my_hook"; + print "mr $hook: running >>$shell<<\n" if $verbose; + my $ret; + if ($quiet) { + my $output = qx/$shell 2>&1/; + $ret = $?; + if ($ret != 0) { + print STDERR $output; + } + } + else { + $ret=system($shell); + } + if ($ret != 0) { + if (($? & 127) == 2) { + print STDERR "mr $hook: interrupted\n"; + return ABORT; + } + elsif ($? & 127) { + print STDERR "mr $hook: received signal ".($? & 127)."\n"; + return ABORT; + } + else { + return FAILED; + } + } + + return OK; +} + # run actions on multiple repos, in parallel sub mrs { my $action=shift; @@ -699,16 +864,16 @@ sub record { if ($ret == OK) { push @ok, $dir; - print "\n"; + print "\n" unless $quiet; } elsif ($ret == FAILED) { if ($interactive) { chdir($dir) unless $no_chdir; print STDERR "mr: Starting interactive shell. Exit shell to continue.\n"; - system((getpwuid($<))[8]); + system((getpwuid($<))[8], "-i"); } push @failed, $dir; - print "\n"; + print "\n" unless $quiet; } elsif ($ret == SKIPPED) { push @skipped, $dir; @@ -772,7 +937,19 @@ sub repolist { } @list; } -# figure out which repos to act on +sub repodir { + my $repo=shift; + my $topdir=$repo->{topdir}; + my $subdir=$repo->{subdir}; + my $ret=($subdir =~/^\//) ? $subdir : $topdir.$subdir; + $ret=~s/\/\.$//; + return $ret; +} + +# Figure out which repos to act on. Returns a list of array refs +# in the format: +# +# [ "$full_repo_path/", "$mr_config_path/", $section_header ] sub selectrepos { my @repos; foreach my $repo (repolist()) { @@ -780,7 +957,7 @@ sub selectrepos { my $subdir=$repo->{subdir}; next if $subdir eq 'DEFAULT'; - my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir; + my $dir=repodir($repo); my $d=$directory; $dir.="/" unless $dir=~/\/$/; $d.="/" unless $d=~/\/$/; @@ -800,7 +977,7 @@ sub selectrepos { my $subdir=$repo->{subdir}; next if $subdir eq 'DEFAULT'; - my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir; + my $dir=repodir($repo); my $d=$directory; $dir.="/" unless $dir=~/\/$/; $d.="/" unless $d=~/\/$/; @@ -837,22 +1014,16 @@ sub is_trusted_config { my $trustfile=$ENV{HOME}."/.mrtrust"; - if (! -e $trustfile) { - print "mr: Assuming $config is trusted.\n"; - print "mr: For better security, you are encouraged to create ~/.mrtrust\n"; - print "mr: and list all trusted mrconfig files in it.\n"; - return 1; - } - if (! %trusted) { $trusted{"$ENV{HOME}/.mrconfig"}=1; - open (TRUST, "<", $trustfile) || die "$trustfile: $!"; - while () { - chomp; - s/^~\//$ENV{HOME}\//; - $trusted{abs_path($_)}=1; + if (open (TRUST, "<", $trustfile)) { + while () { + chomp; + s/^~\//$ENV{HOME}\//; + $trusted{abs_path($_)}=1; + } + close TRUST; } - close TRUST; } return $trusted{$config}; @@ -923,7 +1094,7 @@ sub is_trusted_checkout { is_trusted_repo($words[$c]) ); } - elsif (defined $words[$c] && $twords[$c] eq $words[$c]) { + elsif (defined $words[$c] && $words[$c]=~/^($twords[$c])$/) { $match=1; } else { @@ -936,14 +1107,29 @@ sub is_trusted_checkout { return 0; } +sub trusterror { + my ($err, $file, $line, $url)=@_; + + if (defined $url) { + die "$err in untrusted $url line $line\n". + "(To trust this url, --trust-all can be used; but please use caution;\n". + "this can allow arbitrary code execution!)\n"; + } + else { + die "$err in untrusted $file line $line\n". + "(To trust this file, list it in ~/.mrtrust.)\n"; + } +} + my %loaded; sub loadconfig { my $f=shift; + my $dir=shift; + my $bootstrap_url=shift; my @toload; my $in; - my $dir; my $trusted; if (ref $f eq 'GLOB') { $dir=""; @@ -951,10 +1137,6 @@ sub loadconfig { $trusted=1; } else { - if (! -e $f) { - return; - } - my $absf=abs_path($f); if ($loaded{$absf}) { return; @@ -963,10 +1145,13 @@ sub loadconfig { $trusted=is_trusted_config($absf); - ($dir)=$f=~/^(.*\/)[^\/]+$/; if (! defined $dir) { - $dir="."; + ($dir)=$f=~/^(.*\/)[^\/]+$/; + if (! defined $dir) { + $dir="."; + } } + $dir=abs_path($dir)."/"; if (! exists $configfiles{$dir}) { @@ -986,18 +1171,54 @@ sub loadconfig { } } + if (! -e $f) { + return; + } + print "mr: loading config $f\n" if $verbose; open($in, "<", $f) || die "mr: open $f: $!\n"; } my @lines=<$in>; - close $in; + close $in unless ref $f eq 'GLOB'; my $section; + + # Keep track of the current line in the config file; + # when a file is included track the current line from the include. my $line=0; + my $included=undef; + my $includeline=0; + my $nextline = sub { + if ($included) { + $includeline++; + $included--; + } + else { + $included=undef; + $includeline=0; + $line++; + } + my $l=shift @lines; + chomp $l; + return $l + }; + my $lineerror = sub { + my $msg=shift; + if (defined $included) { + die "mr: $f line $line include line $includeline: $msg\n"; + } + else { + die "mr: $f line $line: $msg\n"; + } + }; + while (@lines) { - $_=shift @lines; - $line++; - chomp; + $_=$nextline->(); + + if (! $trusted && /[[:cntrl:]]/) { + trusterror("mr: illegal control character", $f, $line, $bootstrap_url); + } + next if /^\s*\#/ || /^\s*$/; if (/^\[([^\]]*)\]\s*$/) { $section=$1; @@ -1006,10 +1227,16 @@ sub loadconfig { if (! is_trusted_repo($section) || $section eq 'ALIAS' || $section eq 'DEFAULT') { - die "mr: illegal section \"[$section]\" in untrusted $f line $line\n"; + trusterror("mr: illegal section \"[$section]\"", $f, $line, $bootstrap_url) } } $section=expandenv($section) if $trusted; + if ($section ne 'ALIAS' && + ! exists $config{$dir}{$section} && + exists $config{$dir}{DEFAULT}) { + # copy in defaults + $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} }; + } } elsif (/^(\w+)\s*=\s*(.*)/) { my $parameter=$1; @@ -1017,40 +1244,45 @@ sub loadconfig { # continued value while (@lines && $lines[0]=~/^\s(.+)/) { - shift(@lines); - $line++; $value.="\n$1"; chomp $value; + $nextline->(); } if (! $trusted) { - # Untrusted files can only contain checkout - # parameters. - if ($parameter ne 'checkout') { - die "mr: illegal setting \"$parameter=$value\" in untrusted $f line $line\n"; + # Untrusted files can only contain a few + # settings in specific known-safe formats. + if ($parameter eq 'checkout') { + if (! is_trusted_checkout($value)) { + trusterror("mr: illegal checkout command \"$value\"", $f, $line, $bootstrap_url); + } + } + elsif ($parameter eq 'order') { + # not interpreted as a command, so + # safe. + } + elsif ($value eq 'true' || $value eq 'false') { + # skip=true , deleted=true etc are + # safe. } - if (! is_trusted_checkout($value)) { - die "mr: illegal checkout command \"$value\" in untrusted $f line $line\n"; + else { + trusterror("mr: illegal setting \"$parameter=$value\"", $f, $line, $bootstrap_url); } } if ($parameter eq "include") { print "mr: including output of \"$value\"\n" if $verbose; - unshift @lines, `$value`; + my @inc=`$value`; if ($?) { print STDERR "mr: include command exited nonzero ($?)\n"; } + $included += @inc; + unshift @lines, @inc; next; } if (! defined $section) { - die "$f line $.: parameter ($parameter) not in section\n"; - } - if ($section ne 'ALIAS' && - ! exists $config{$dir}{$section} && - exists $config{$dir}{DEFAULT}) { - # copy in defaults - $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} }; + $lineerror->("parameter ($parameter) not in section"); } if ($section eq 'ALIAS') { $alias{$parameter}=$value; @@ -1067,34 +1299,46 @@ sub loadconfig { $knownactions{$parameter}=1; } if ($parameter eq 'chain' && - length $dir && $section ne "DEFAULT" && - -e $dir.$section."/.mrconfig") { - my $ret=system($value); - if ($ret != 0) { - if (($? & 127) == 2) { - print STDERR "mr: chain test interrupted\n"; - exit 2; + length $dir && $section ne "DEFAULT") { + my $chaindir="$section"; + if ($chaindir !~ m!^/!) { + $chaindir=$dir.$chaindir; + } + if (-e "$chaindir/.mrconfig") { + my $ret=system($value); + if ($ret != 0) { + if (($? & 127) == 2) { + print STDERR "mr: chain test interrupted\n"; + exit 2; + } + elsif ($? & 127) { + print STDERR "mr: chain test received signal ".($? & 127)."\n"; + } } - elsif ($? & 127) { - print STDERR "mr: chain test received signal ".($? & 127)."\n"; + else { + push @toload, ["$chaindir/.mrconfig", $chaindir]; } } - else { - push @toload, $dir.$section."/.mrconfig"; - } } } } else { - die "$f line $line: parse error\n"; + $lineerror->("parse error"); } } - foreach (@toload) { - loadconfig($_); + foreach my $c (@toload) { + loadconfig(@$c); } } +sub startingconfig { + %alias=%config=%configfiles=%knownactions=%loaded=(); + my $datapos=tell(DATA); + loadconfig(\*DATA); + seek(DATA,$datapos,0); # rewind +} + sub modifyconfig { my $f=shift; # the section to modify or add @@ -1308,28 +1552,59 @@ sub register { $ENV{MR_REPO}=~s/.*\/(.*)/$1/; $command="set -e; ".$config{$directory}{DEFAULT}{lib}."\n". "my_action(){ $command\n }; my_action ". - join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV); + join(" ", map { s/\\/\\\\/g; s/"/\"/g; '"'.$_.'"' } @ARGV); print "mr register: running >>$command<<\n" if $verbose; exec($command) || die "exec: $!"; } sub bootstrap { my $url=shift @ARGV; - + my $dir=shift @ARGV || "."; + if (! defined $url || ! length $url) { die "mr: bootstrap requires url\n"; } - - if (-e ".mrconfig") { - die "mr: .mrconfig file already exists, not overwriting with $url\n"; + + # Download the config file to a temporary location. + eval q{use File::Temp}; + die $@ if $@; + my $tmpconfig=File::Temp->new(); + my @curlargs = ("curl", "-A", "mr", "-L", "-s", $url, "-o", $tmpconfig); + push(@curlargs, "-k") if $insecure; + my $curlstatus = system(@curlargs); + die "mr bootstrap: invalid SSL certificate for $url (consider -k)\n" if $curlstatus >> 8 == 60; + die "mr bootstrap: download of $url failed\n" if $curlstatus != 0; + + if (! -e $dir) { + system("mkdir", "-p", $dir); } + chdir($dir) || die "chdir $dir: $!"; - if (system("curl", "-A", "mr", "-s", $url, "-o", ".mrconfig") != 0) { - die "mr: download of $url failed\n"; - } + # Special case to handle checkout of the "." repo, which + # would normally be skipped. + my $topdir=abs_path(".")."/"; + my @repo=($topdir, $topdir, "."); + loadconfig($tmpconfig, $topdir, $url); + record(\@repo, action("checkout", @repo, 1)) + if exists $config{$topdir}{"."}{"checkout"}; - exec("mr $ENV{MR_SWITCHES} -c .mrconfig checkout"); - die "failed to run mr checkout"; + if (-e ".mrconfig") { + print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $url\n"; + } + else { + eval q{use File::Copy}; + die $@ if $@; + move($tmpconfig, ".mrconfig") || die "rename: $!"; + } + + # Reload the config file (in case we got a different version) + # and checkout everything else. + startingconfig(); + loadconfig(".mrconfig"); + dispatch("checkout"); + @skipped=grep { abs_path($_) ne abs_path($topdir) } @skipped; + showstats("bootstrap"); + exitstats(); } # alias expansion and command stemming @@ -1356,7 +1631,7 @@ sub expandaction { return $action; } -sub find_nearest_mrconfig { +sub find_mrconfig { my $dir=getcwd(); while (length $dir) { if (-e "$dir/.mrconfig") { @@ -1364,7 +1639,7 @@ sub find_nearest_mrconfig { } $dir=~s/\/[^\/]*$//; } - die "no .mrconfig found in path\n"; + return "$ENV{HOME}/.mrconfig"; } sub getopts { @@ -1373,10 +1648,11 @@ sub getopts { my $result=GetOptions( "d|directory=s" => sub { $directory=abs_path($_[1]) }, "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 }, - "p|path" => sub { $ENV{MR_CONFIG}=find_nearest_mrconfig(); $config_overridden=1 }, + "p|path" => sub { }, # now default, ignore "v|verbose" => \$verbose, "q|quiet" => \$quiet, "s|stats" => \$stats, + "k|insecure" => \$insecure, "i|interactive" => \$interactive, "n|no-recurse:i" => \$max_depth, "j|jobs:i" => \$jobs, @@ -1417,28 +1693,30 @@ sub init { $ENV{MR_PATH}=$Bin."/".$Script; }; } + +sub exitstats { + if (@failed) { + exit 1; + } + else { + exit 0; + } +} sub main { getopts(); init(); - loadconfig(\*DATA); + startingconfig(); + loadconfig("$ENV{HOME}/.mrconfig"); loadconfig($ENV{MR_CONFIG}); #use Data::Dumper; print Dumper(\%config); my $action=expandaction(shift @ARGV); dispatch($action); - showstats($action); - if (@failed) { - exit 1; - } - elsif (! @ok && @skipped) { - exit 1; - } - else { - exit 0; - } + showstats($action); + exitstats(); } # Finally, some useful actions that mr knows about by default. @@ -1466,7 +1744,7 @@ lib = if [ -z "$1" ] || [ -z "$2" ]; then error "mr: usage: hours_since action num" fi - for dir in .git .svn .bzr CVS .hg _darcs; do + for dir in .git .svn .bzr CVS .hg _darcs _FOSSIL_; do if [ -e "$MR_REPO/$dir" ]; then flagfile="$MR_REPO/$dir/.mr_last$1" break @@ -1477,10 +1755,20 @@ lib = fi delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"` if [ "$delta" -lt "$2" ]; then - exit 0 + return 1 else touch "$flagfile" - exit 1 + return 0 + fi + } + is_bzr_checkout() { + LANG=C bzr info | egrep -q '^Checkout' + } + lazy() { + if [ "$MR_ACTION" = checkout ] || [ -d "$MR_REPO" ]; then + return 1 + else + return 0 fi } @@ -1490,6 +1778,7 @@ bzr_test = test -d "$MR_REPO"/.bzr cvs_test = test -d "$MR_REPO"/CVS hg_test = test -d "$MR_REPO"/.hg darcs_test = test -d "$MR_REPO"/_darcs +fossil_test = test -f "$MR_REPO"/_FOSSIL_ git_bare_test = test -d "$MR_REPO"/refs/heads && test -d "$MR_REPO"/refs/tags && test -d "$MR_REPO"/objects && test -f "$MR_REPO"/config && @@ -1497,29 +1786,48 @@ git_bare_test = svn_update = svn update "$@" git_update = git pull "$@" -bzr_update = bzr merge --pull "$@" +bzr_update = + if is_bzr_checkout; then + bzr update "$@" + else + bzr merge --pull "$@" + fi cvs_update = cvs update "$@" hg_update = hg pull "$@" && hg update "$@" darcs_update = darcs pull -a "$@" +fossil_update = fossil pull "$@" svn_status = svn status "$@" -git_status = git status "$@" || true -bzr_status = bzr status "$@" +git_status = git status -s "$@" || true +bzr_status = bzr status --short "$@" cvs_status = cvs status "$@" hg_status = hg status "$@" darcs_status = darcs whatsnew -ls "$@" || true +fossil_status = fossil changes "$@" svn_commit = svn commit "$@" git_commit = git commit -a "$@" && git push --all -bzr_commit = bzr commit "$@" && bzr push +bzr_commit = + if is_bzr_checkout; then + bzr commit "$@" + else + bzr commit "$@" && bzr push + fi cvs_commit = cvs commit "$@" hg_commit = hg commit -m "$@" && hg push darcs_commit = darcs record -a -m "$@" && darcs push -a +fossil_commit = fossil commit "$@" git_record = git commit -a "$@" -bzr_record = bzr commit "$@" +bzr_record = + if is_bzr_checkout; then + bzr commit --local "$@" + else + bzr commit "$@" + fi hg_record = hg commit -m "$@" darcs_record = darcs record -a -m "$@" +fossil_record = fossil commit "$@" svn_push = : git_push = git push "$@" @@ -1527,6 +1835,7 @@ bzr_push = bzr push "$@" cvs_push = : hg_push = hg push "$@" darcs_push = darcs push -a "$@" +fossil_push = fossil push "$@" svn_diff = svn diff "$@" git_diff = git diff "$@" @@ -1534,6 +1843,7 @@ bzr_diff = bzr diff "$@" cvs_diff = cvs diff "$@" hg_diff = hg diff "$@" darcs_diff = darcs diff -u "$@" +fossil_diff = fossil diff "$@" svn_log = svn log "$@" git_log = git log "$@" @@ -1542,6 +1852,9 @@ cvs_log = cvs log "$@" hg_log = hg log "$@" darcs_log = darcs changes "$@" git_bare_log = git log "$@" +fossil_log = fossil timeline "$@" + +run = "$@" svn_register = url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2` @@ -1563,7 +1876,7 @@ bzr_register = error "cannot determine bzr url" fi echo "Registering bzr url: $url in $MR_CONFIG" - mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr clone '$url' '$MR_REPO'" + mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr branch '$url' '$MR_REPO'" cvs_register = repo=`cat CVS/Repository` root=`cat CVS/Root` @@ -1587,24 +1900,42 @@ git_bare_register = fi echo "Registering git url: $url in $MR_CONFIG" mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'" +fossil_register = + url=`fossil remote-url` + repo=`fossil info | grep repository | sed -e 's/repository:*.//g' -e 's/ //g'` + echo "Registering fossil repository $url in $MR_CONFIG" + mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && fossil open '$repo'" svn_trusted_checkout = svn co $url $repo svn_alt_trusted_checkout = svn checkout $url $repo git_trusted_checkout = git clone $url $repo -bzr_trusted_checkout = bzr clone $url $repo +bzr_trusted_checkout = bzr checkout|clone|branch|get $url $repo # cvs: too hard hg_trusted_checkout = hg clone $url $repo darcs_trusted_checkout = darcs get $url $repo git_bare_trusted_checkout = git clone --bare $url $repo +# fossil: messy to do + help = + case `uname -s` in + SunOS) + SHOWMANFILE="man -f" + ;; + Darwin) + SHOWMANFILE="man" + ;; + *) + SHOWMANFILE="man -l" + ;; + esac if [ ! -e "$MR_PATH" ]; then error "cannot find program path" fi tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed" trap "rm -f $tmp" exit pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed" - man -l "$tmp" || error "man failed" + $SHOWMANFILE "$tmp" || error "man failed" list = true config = bootstrap =