X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/69a60fb0a34b214c9ff6d55350b4663b4452c397..ae4ab73b5d793b03f91fdcef0fff7405f9f9d75b:/mr diff --git a/mr b/mr index 3f04b5b..b37d9a5 100755 --- a/mr +++ b/mr @@ -22,6 +22,8 @@ B [options] diff B [options] log +B [options] run command [param ...] + B [options] bootstrap url [directory] B [options] register [repository] @@ -39,7 +41,7 @@ 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, darcs and fossil repositories, and support for other revision +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 @@ -53,7 +55,7 @@ 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 @@ -85,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 @@ -102,6 +104,10 @@ 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: @@ -183,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 @@ -216,7 +222,9 @@ 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 @@ -282,7 +290,7 @@ This obsolete flag is ignored. 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] @@ -309,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 @@ -327,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 @@ -342,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 @@ -369,6 +397,22 @@ 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 @@ -393,15 +437,15 @@ run before committing; "post_update" is run after updating. 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. =head1 UNTRUSTED MRCONFIG FILES @@ -483,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 ".fulldir($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; } } @@ -532,11 +576,11 @@ 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; @@ -558,7 +602,39 @@ sub action { 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 (! $force_checkout) { @@ -576,30 +652,8 @@ sub action { } } - 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); @@ -610,34 +664,47 @@ sub action { return FAILED; } elsif (! defined $command) { - my $rcs=rcs_test(@_); - if (! defined $rcs) { + 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 $fulldir, 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: $fulldir\n" unless $quiet; + $actionmsg="mr $action: $fulldir"; } else { my $s=$directory; $s=~s/^\Q$fulldir\E\/?//; - print "mr $action: $fulldir (in subdir $s)\n" unless $quiet; + $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"; @@ -693,7 +760,17 @@ sub hook { my $shell="set -e;".$lib. "my_hook(){ $command\n }; my_hook"; print "mr $hook: running >>$shell<<\n" if $verbose; - my $ret=system($shell); + 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"; @@ -703,6 +780,9 @@ sub hook { print STDERR "mr $hook: received signal ".($? & 127)."\n"; return ABORT; } + else { + return FAILED; + } } return OK; @@ -866,7 +946,10 @@ sub repodir { return $ret; } -# figure out which repos to act on +# 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()) { @@ -1011,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 { @@ -1025,14 +1108,24 @@ sub is_trusted_checkout { } sub trusterror { - die shift()."\n". - "(To trust this file, list it in ~/.mrtrust.)\n"; + 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; @@ -1044,10 +1137,6 @@ sub loadconfig { $trusted=1; } else { - if (! -e $f) { - return; - } - my $absf=abs_path($f); if ($loaded{$absf}) { return; @@ -1082,6 +1171,10 @@ sub loadconfig { } } + if (! -e $f) { + return; + } + print "mr: loading config $f\n" if $verbose; open($in, "<", $f) || die "mr: open $f: $!\n"; } @@ -1089,11 +1182,43 @@ sub loadconfig { 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; @@ -1102,7 +1227,7 @@ sub loadconfig { if (! is_trusted_repo($section) || $section eq 'ALIAS' || $section eq 'DEFAULT') { - trusterror "mr: illegal section \"[$section]\" in untrusted $f line $line"; + trusterror("mr: illegal section \"[$section]\"", $f, $line, $bootstrap_url) } } $section=expandenv($section) if $trusted; @@ -1119,34 +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') { - trusterror "mr: illegal setting \"$parameter=$value\" in untrusted $f line $line"; + # 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. } - if (! is_trusted_checkout($value)) { - trusterror "mr: illegal checkout command \"$value\" in untrusted $f line $line"; + elsif ($value eq 'true' || $value eq 'false') { + # skip=true , deleted=true etc are + # safe. + } + 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"; + $lineerror->("parameter ($parameter) not in section"); } if ($section eq 'ALIAS') { $alias{$parameter}=$value; @@ -1163,31 +1299,36 @@ 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); } } @@ -1411,7 +1552,7 @@ 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: $!"; } @@ -1443,7 +1584,7 @@ sub bootstrap { # would normally be skipped. my $topdir=abs_path(".")."/"; my @repo=($topdir, $topdir, "."); - loadconfig($tmpconfig, $topdir); + loadconfig($tmpconfig, $topdir, $url); record(\@repo, action("checkout", @repo, 1)) if exists $config{$topdir}{"."}{"checkout"}; @@ -1620,6 +1761,16 @@ lib = 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 + } svn_test = test -d "$MR_REPO"/.svn git_test = test -d "$MR_REPO"/.git @@ -1635,7 +1786,12 @@ 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 "$@" @@ -1651,14 +1807,24 @@ 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 "$@" @@ -1688,6 +1854,8 @@ 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` if [ -z "$url" ]; then @@ -1708,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` @@ -1741,7 +1909,7 @@ fossil_register = 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