summaryrefslogtreecommitdiff
path: root/bin/mr
diff options
context:
space:
mode:
authorSean Whitton <spwhitton@spwhitton.name>2018-05-11 09:41:23 -0700
committerSean Whitton <spwhitton@spwhitton.name>2018-05-11 09:41:23 -0700
commit2875d62f0de6eedc5db04ca467d2859c8dcd781d (patch)
tree78d712dd378da0552f623a38affa2febf17a4418 /bin/mr
parent9290b12f8d1c6ada15a504ad78cb0b5936bf29ff (diff)
downloaddotfiles-2875d62f0de6eedc5db04ca467d2859c8dcd781d.tar.gz
embed copies of stow and mr in dotfiles repo more sensibly
Diffstat (limited to 'bin/mr')
-rwxr-xr-xbin/mr2333
1 files changed, 0 insertions, 2333 deletions
diff --git a/bin/mr b/bin/mr
deleted file mode 100755
index c3b6fd65..00000000
--- a/bin/mr
+++ /dev/null
@@ -1,2333 +0,0 @@
-#!/usr/bin/env perl
-
-=head1 NAME
-
-mr - a tool to manage all your version control repos
-
-=head1 SYNOPSIS
-
-B<mr> [options] checkout
-
-B<mr> [options] update
-
-B<mr> [options] status
-
-B<mr> [options] clean [-f]
-
-B<mr> [options] commit [-m "message"]
-
-B<mr> [options] record [-m "message"]
-
-B<mr> [options] fetch
-
-B<mr> [options] push
-
-B<mr> [options] diff
-
-B<mr> [options] log
-
-B<mr> [options] grep pattern
-
-B<mr> [options] run command [param ...]
-
-B<mr> [options] bootstrap src [directory]
-
-B<mr> [options] register [repository]
-
-B<mr> [options] config section ["setting=[value]" ...]
-
-B<mr> [options] action [params ...]
-
-B<mr> [options] [online|offline]
-
-B<mr> [options] remember action [params ...]
-
-=head1 DESCRIPTION
-
-B<mr> is a tool to manage all your version control repos. 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, fossil and veracity repositories, and support
-for other version control systems can easily be added.
-
-B<mr> 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
-contains no other registered repositories, it will stay in that directory,
-and work on only that repository,
-
-B<mr> 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. 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 version
-control system:
-
-=over 4
-
-=item checkout (or co)
-
-Checks out any repositories that are not already checked out.
-
-=item update
-
-Updates each repository from its configured remote repository.
-
-If a repository isn't checked out yet, it will first check it out.
-
-=item status
-
-Displays a status report for each repository, showing what
-uncommitted changes are present in the repository. For distributed version
-control systems, also shows unpushed local branches.
-
-=item clean
-
-Print ignored files, untracked files and other cruft in the working directory.
-
-The optional -f parameter allows removing the files as well as printing them.
-
-=item commit (or ci)
-
-Commits changes to each repository. (By default, changes are pushed to the
-remote repository too, when using distributed systems like git. If you
-don't like this default, you can change it in your .mrconfig, or use record
-instead.)
-
-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 version control systems.
-
-The optional -m parameter allows specifying a commit message.
-
-=item fetch
-
-Fetches from each repository's remote repository, but does not
-update the working copy. Only supported for some distributed version
-control systems.
-
-=item push
-
-Pushes committed local changes to the remote repository. A no-op for
-centralized version control systems.
-
-=item diff
-
-Show a diff of uncommitted changes.
-
-=item log
-
-Show the commit log.
-
-=item grep pattern
-
-Searches for a pattern in each repository using the grep subcommand. Uses
-ack-grep on VCS that do not have their own.
-
-=item run command [param ...]
-
-Runs the specified command in each repository.
-
-=back
-
-These commands are also available:
-
-=over 4
-
-=item bootstrap src [directory]
-
-Causes mr to retrieve the source C<src> and use it as a .mrconfig file to
-checkout the repositories listed in it, into the specified directory.
-
-B<mr> understands several types of sources:
-
-=over 4
-
-=item URL for curl
-
-C<src> may be an URL understood by B<curl>.
-
-=item copy via ssh
-
-To use B<scp> to download, the C<src> may have the form
-C<ssh://[user@]host:file>.
-
-=item local file
-
-You can retrieve the config file by other means and pass its B<path> as C<src>.
-
-=item standard input
-
-If source C<src> consists in a single dash C<->, config file is read from
-standard input.
-
-=back
-
-The directory will be created if it does not exist. If no directory is
-specified, the current directory will be used.
-
-As a special case, if source C<src> includes a repository named ".", that
-is checked out into the top of the specified directory.
-
-=item list (or ls)
-
-List the repositories that mr will act on.
-
-=item register
-
-Register an existing repository in a mrconfig file. By default, the
-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 in a parent of the current directory.
-
-=item config
-
-Adds, modifies, removes, or prints a value from a mrconfig file. The next
-parameter is the name of the section the value is in. To add or modify
-values, use one or more instances of "setting=value". Use "setting=" to
-remove a setting. Use just "setting" to get the value of a that setting.
-
-For example, to add (or edit) a repository in src/foo:
-
- mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
-
-To show the command that mr uses to update the repository in src/foo:
-
- mr config src/foo update
-
-To see the built-in library of shell functions contained in mr:
-
- mr config DEFAULT lib
-
-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
-
-Advises mr that it is in offline mode. Any commands that fail in
-offline mode will be remembered, and retried when mr is told it's online.
-
-=item online
-
-Advices mr that it is in online mode again. Commands that failed while in
-offline mode will be re-run.
-
-=item remember
-
-Remember a command, to be run later when mr re-enters online mode. This
-implicitly puts mr into offline mode. The command can be any regular mr
-command. This is useful when you know that a command will fail due to being
-offline, and so don't want to run it right now at all, but just remember
-to run it when you go back online.
-
-=item help
-
-Displays this help.
-
-=back
-
-Actions can be abbreviated to any unambiguous substring, so
-"mr st" is equivalent to "mr status", and "mr up" is equivalent to "mr
-update"
-
-Additional parameters can be passed to most commands, and are passed on
-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
-
-=over 4
-
-=item -d directory
-
-=item --directory directory
-
-Specifies the topmost directory that B<mr> should work in. The default is
-the current working directory.
-
-=item -c mrconfig
-
-=item --config mrconfig
-
-Use the specified mrconfig file. The default is to use both F<~/.mrconfig>
-as well as look for a F<.mrconfig> file in the current directory, or in one
-of its parent directories.
-
-=item -f
-
-=item --force
-
-Force mr to act on repositories that would normally be skipped due to their
-configuration.
-
-=item --force-env
-
-Force mr to execute even though potentially dangerous environment variables
-are set.
-
-=item -v
-
-=item --verbose
-
-Be verbose.
-
-=item -m
-
-=item --minimal
-
-Minimise output. If a command fails or there is any output then the usual
-output will be shown.
-
-=item -q
-
-=item --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
-
-=item --stats
-
-Expand the statistics line displayed at the end to include information
-about exactly which repositories failed and were skipped, if any.
-
-=item -i
-
-=item --interactive
-
-Interactive mode. If a repository fails to be processed, a subshell will be
-started which you can use to resolve or investigate the problem. Exit the
-subshell to continue the mr run.
-
-=item -n [number]
-
-=item --no-recurse [number]
-
-If no number if specified, just operate on the repository for the current
-directory, do not recurse into deeper repositories.
-
-If a number is specified, will recurse into repositories at most that many
-subdirectories deep. For example, with -n 2 it would recurse into ./src/foo,
-but not ./src/packages/bar.
-
-=item -j [number]
-
-=item --jobs [number]
-
-Run the specified number of jobs in parallel, or an unlimited number of jobs
-with no number specified. This can greatly speed up operations such as updates.
-It is not recommended for interactive operations.
-
-Note that running more than 10 jobs at a time is likely to run afoul of
-ssh connection limits. Running between 3 and 5 jobs at a time will yield
-a good speedup in updates without loading the machine too much.
-
-=item -t
-
-=item --trust-all
-
-Trust all mrconfig files even if they are not listed in F<~/.mrtrust>.
-Use with caution.
-
-=item -p
-
-=item --path
-
-This obsolete flag is ignored.
-
-=back
-
-=head1 MRCONFIG FILES
-
-Here is an example F<.mrconfig> file:
-
- [src]
- checkout = svn checkout svn://svn.example.com/src/trunk src
- chain = true
-
- [src/linux-2.6]
- checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
- cd linux-2.6 &&
- git checkout -b mybranch origin/master
-
-The F<.mrconfig> file uses a variant of the INI file format. Lines
-starting with "#" are comments. Values can be continued to the
-following line by indenting the line with whitespace.
-
-The C<DEFAULT> section allows setting default values for the sections that
-come after it.
-
-The C<ALIAS> section allows adding aliases for actions. Each setting
-is an alias, and its value is the action to use.
-
-All other sections add repositories. The section header specifies the
-directory where the repository is located. This is relative to the directory
-that contains the mrconfig file, but you can also choose to use absolute
-paths. (Note that you can use environment variables in section names; they
-will be passed through the shell for expansion. For example,
-C<[$HOSTNAME]>, or C<[${HOSTNAME}foo]>).
-
-Within a section, each setting 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". 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 C<git clone>, C<svn checkout>, C<bzr branch>
-or C<bzr checkout> (for a bound branch), etc.
-
-Note that these shell commands are run in a C<set -e> shell
-environment, where any additional parameters you pass are available in
-C<$@>. All commands other than "checkout" are run inside the repository,
-though not necessarily at the top of it.
-
-The C<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
-basename of the directory that should be created when checking the
-repository out.)
-
-The C<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 F<.mrconfig> file that should be modified to register the repo.
-
-The C<MR_ACTION> environment variable is set to the command being run
-(update, checkout, etc).
-
-A few settings have special meanings:
-
-=over 4
-
-=item skip
-
-If "skip" is set and its command returns true, then B<mr>
-will skip acting on that repository. The command is passed the action
-name in C<$1>.
-
-Here are two examples. The first skips the repo unless
-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 --force -d foo checkout").
-
- [foo]
- checkout = ...
- skip = lazy
-
-=item order
-
-The "order" setting can be used to override the default ordering of
-repositories. The default order value is 10. Use smaller values to make
-repositories be processed earlier, and larger values to make repositories
-be processed later.
-
-Note that if a repository is located in a subdirectory of another
-repository, ordering it to be processed earlier is not recommended.
-
-=item chain
-
-If "chain" is set and its command returns true, then B<mr>
-will try to load a F<.mrconfig> file from the root of the repository.
-
-=item include
-
-If "include" is set, its command is ran, and should output
-additional mrconfig file content. The content is included as if it were
-part of the including file.
-
-Unlike everything else, "include" does not need to be placed within a section.
-
-B<mr> ships several libraries that can be included to add support for
-additional version control type things (unison, git-svn, 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 "deleted" is set and its command returns true, then
-B<mr> 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 among multiple machines,
-to keep track of and remember to delete old repositories.
-
-=item lib
-
-The "lib" setting can contain 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.
-
-Unlike most other settings, this can be specified multiple times, in
-which case the chunks of shell code are accumulatively concatenated
-together.
-
-=item fixups
-
-If "fixups" 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 jobs
-
-If "jobs" is set, run the specified number of jobs in parallel.
-This can greatly speed up operations such as updates.
-
-Note that running more than 10 jobs at a time is likely to run afoul of
-ssh connection limits. Running between 3 and 5 jobs at a time will yield
-a good speedup in updates without loading the machine too much.
-
-=item VCS_action
-
-When looking for a command to run for a given action, mr first looks for
-a setting with the same name as the action. If that is not found, it
-looks for a setting named "VCS_action" (substituting in the name of the
-version control system and the action).
-
-Internally, mr has settings for "git_update", "svn_update", etc. To change
-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.
-
-=item pre_ and post_
-
-If "pre_action" is set, its command is run before mr performs the
-specified action. Similarly, "post_action" commands are run after mr
-successfully performs the specified action. For example, "pre_commit" is
-run before committing; "post_update" is run after updating.
-
-=item _append
-
-Any setting can be suffixed with C<_append>, to add an additional value
-to the existing value of the setting. In this way, actions
-can be constructed accumulatively.
-
-=item VCS_test
-
-The name of the version control system is itself determined by
-running each defined "VCS_test" action, until one succeeds.
-
-=back
-
-=head1 UNTRUSTED MRCONFIG FILES
-
-Since mrconfig files can contain arbitrary shell commands, they can do
-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 F<~/.mrconfig> checks out. To
-avoid worries about evil commands in a mrconfig file, mr defaults to
-reading all mrconfig files other than the main F<~/.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 F<~/.mrtrust>.
-One mrconfig file should be listed per line. Either the full pathname
-should be listed, or the pathname can start with F<~/> to specify a file
-relative to your home directory.
-
-=head1 OFFLINE LOG FILE
-
-The F<~/.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
-
-mr can be extended to support things such as unison and git-svn. Some
-files providing such extensions are available in F</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-2011 Joey Hess <joey@kitenet.net>
-
-Licensed under the GNU GPL version 2 or higher.
-
-http://myrepos.branchable.com/
-
-=cut
-
-use warnings;
-use strict;
-use Getopt::Long;
-use Cwd qw(getcwd abs_path);
-
-# things that can happen when mr runs a command
-use constant {
- OK => 0,
- FAILED => 1,
- SKIPPED => 2,
- ABORT => 3,
-};
-
-# configurables
-my $config_overridden=0;
-my $verbose=0;
-my $minimal=0;
-my $quiet=0;
-my $stats=0;
-my $force=0;
-my $force_env=0;
-my $insecure=0;
-my $interactive=0;
-my $max_depth;
-my $no_chdir=0;
-my $jobs=1;
-my $trust_all=0;
-my $directory=getcwd();
-my $terminal=-t STDOUT && eval{require IO::Pty::Easy;IO::Pty::Easy->import();1;};
-my $erase_line=$terminal && eval{require Term::Cap;my $t=Term::Cap->Tgetent();$t->Tputs('ce');};
-
-my $HOME_MR_CONFIG = "$ENV{HOME}/.mrconfig";
-$ENV{MR_CONFIG}=find_mrconfig();
-
-# globals :-(
-my %config;
-my %configfiles;
-my %knownactions;
-my %alias;
-my (@ok, @failed, @skipped);
-
-main();
-
-sub shellquote {
- my $i=shift;
- $i=~s/'/'"'"'/g;
- return "'$i'";
-}
-
-# Runs a shell command using a supplied function.
-# The lib will be included in the shell command line, and any params
-# will be available in the shell as $1, $2, etc.
-my $lastlib;
-sub runsh {
- my ($action, $topdir, $subdir, $command, $params, $runner) = @_;
-
- # optimisation: avoid running the shell for true and false
- if ($command =~ /^\s*true\s*$/) {
- $?=0;
- return 0;
- }
- elsif ($command =~ /^\s*false\s*$/) {
- $?=0;
- return 1;
- }
-
- my $quotedparams=join(" ", (map { shellquote($_) } @$params));
- my $lib=exists $config{$topdir}{$subdir}{lib} ?
- $config{$topdir}{$subdir}{lib}."\n" : "";
- if ($verbose && (! defined $lastlib || $lastlib ne $lib)) {
- print "mr library now: >>$lib<<\n";
- $lastlib=$lib;
- }
- my $shellcode="set -e;".$lib.
- "my_sh(){ $command\n }; my_sh $quotedparams";
- print "mr $action: running $action >>$command<<\n" if $verbose;
- $runner->($shellcode);
-}
-
-my %perl_cache;
-sub perl {
- my $id=shift;
- my $s=shift;
- if ($s =~ m/^perl:\s+(.*)/s) {
- return $perl_cache{$1} if exists $perl_cache{$1};
- my $sub=eval "sub {$1}";
- if (! defined $sub) {
- print STDERR "mr: bad perl code in $id: $@\n";
- }
- return $perl_cache{$1} = $sub;
- }
- return undef;
-}
-
-my %vcs;
-sub vcs_test {
- my ($action, $dir, $topdir, $subdir) = @_;
-
- if (exists $vcs{$dir}) {
- return $vcs{$dir};
- }
-
- my $test="";
- my %perltest;
- foreach my $vcs_test (
- sort {
- length $a <=> length $b
- ||
- $a cmp $b
- } grep { /_test$/ } keys %{$config{$topdir}{$subdir}}) {
- my ($vcs)=$vcs_test =~ /(.*)_test/;
- my $p=perl($vcs_test, $config{$topdir}{$subdir}{$vcs_test});
- if (defined $p) {
- $perltest{$vcs}=$p;
- }
- else {
- $test="my_$vcs_test() {\n$config{$topdir}{$subdir}{$vcs_test}\n}\n".$test;
- $test.="if my_$vcs_test; then echo $vcs; fi\n";
- }
- }
-
- my @vcs;
- foreach my $vcs (keys %perltest) {
- if ($perltest{$vcs}->()) {
- push @vcs, $vcs;
- }
- }
-
- push @vcs, split(/\n/,
- runsh("vcs test", $topdir, $subdir, $test, [], sub {
- my $sh=shift;
- my $ret=`$sh`;
- return $ret;
- })) if length $test;
- if (@vcs > 1) {
- print STDERR "mr $action: found multiple possible repository types (@vcs) for ".fulldir($topdir, $subdir)."\n";
- return undef;
- }
- if (! @vcs) {
- return $vcs{$dir}=undef;
- }
- else {
- return $vcs{$dir}=$vcs[0];
- }
-}
-
-sub findcommand {
- my ($action, $dir, $topdir, $subdir, $is_checkout) = @_;
-
- if (exists $config{$topdir}{$subdir}{$action}) {
- return $config{$topdir}{$subdir}{$action};
- }
-
- if ($is_checkout) {
- return undef;
- }
-
- my $vcs=vcs_test(@_);
-
- 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 terminal_friendly_spawn {
- my $actionmsg = shift;
- my $sh = shift;
- my $quiet = shift;
- my $minimal = shift;
- my $jobs = shift;
- my $continuous = !$quiet && $minimal && 1 == $jobs;
- my $output = "";
- my $continuous_output = 0;
- if ($terminal) {
- my $pty = IO::Pty::Easy->new;
- $pty->spawn($sh);
- if ($continuous) {
- $| = 1;
- print "$actionmsg$erase_line\r" if $actionmsg;
- while ($pty->is_active) {
- my $data = $pty->getline();
- if (defined $data && $data ne '') {
- print "\n" if ($actionmsg && !$continuous_output);
- $continuous_output = 1;
- print $data;
- }
- }
- }
- else {
- while ($pty->is_active) {
- my $data = $pty->read();
- $output .= $data if defined $data;
- }
- }
- $pty->close;
- }
- else {
- $output = qx/$sh 2>&1/;
- }
- my $ret = $?;
- if ($quiet && $ret != 0) {
- print "$actionmsg\n" if $actionmsg;
- print STDERR $output;
- }
- elsif (!$quiet && (!$minimal || $output)) {
- print "$actionmsg\n" if $actionmsg;
- print $output;
- }
- return ($ret, ($output || $continuous_output) ? 1 : 0);
-}
-
-sub action {
- my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
- my $fulldir=fulldir($topdir, $subdir);
- my $checkout_dir;
-
- $ENV{MR_CONFIG}=$configfiles{$topdir};
- my $is_checkout=($action eq 'checkout');
- my $is_update=($action =~ /update/);
-
- ($ENV{MR_REPO}=$dir) =~ s!/$!!;
- $ENV{MR_ACTION}=$action;
-
- foreach my $testname ("skip", "deleted") {
- next if $force && $testname eq "skip";
-
- my $testcommand=findcommand($testname, $dir, $topdir, $subdir, $is_checkout);
-
- if (defined $testcommand) {
- my $ret=runsh "$testname test", $topdir, $subdir,
- $testcommand, [$action],
- sub { system(shift()) };
- 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) {
- $checkout_dir=$dir;
- if (! $force_checkout) {
- if (-d $dir) {
- print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
- return SKIPPED;
- }
-
- $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
- }
- }
- elsif ($is_update) {
- if (! -d $dir) {
- return action("checkout", $dir, $topdir, $subdir);
- }
- }
-
- my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout);
-
- if ($is_checkout && ! -d $dir) {
- print "mr $action: creating parent directory $dir\n" if $verbose;
- system("mkdir", "-p", $dir);
- }
-
- if (! $no_chdir && ! chdir($dir)) {
- print STDERR "mr $action: failed to chdir to $dir: $!\n";
- return FAILED;
- }
- elsif (! defined $command) {
- 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 $vcs repository $fulldir, skipping\n" unless $minimal;
- return SKIPPED;
- }
- }
- else {
- my $actionmsg;
- if (! $no_chdir) {
- $actionmsg="mr $action: $fulldir";
- }
- else {
- my $s=$directory;
- $s=~s/^\Q$fulldir\E\/?//;
- $actionmsg="mr $action: $fulldir (in subdir $s)";
- }
- print "$actionmsg\n" unless !$jobs || $jobs > 1 || $quiet || $minimal;
-
- my ($hookret, $hook_out)=hook("pre_$action", $topdir, $subdir);
- return $hookret if $hookret != OK;
-
- my ($ret, $out)=runsh $action, $topdir, $subdir,
- $command, \@ARGV, sub {
- my $sh=shift;
- if (!$jobs || $jobs > 1 || $quiet || $minimal) {
- return terminal_friendly_spawn($actionmsg, $sh, $quiet, $minimal, $jobs);
- }
- else {
- system($sh);
- }
- };
- if ($ret != 0) {
- if (($? & 127) == 2) {
- print STDERR "mr $action: interrupted\n";
- return ABORT;
- }
- elsif ($? & 127) {
- print STDERR "mr $action: received signal ".($? & 127)."\n";
- return ABORT;
- }
- print STDERR "mr $action: failed ($ret)\n" if $verbose;
- if ($ret >> 8 != 0) {
- print STDERR "mr $action: command failed\n";
- if (-e "$ENV{HOME}/.mrlog" && $action ne 'remember') {
- # recreate original command line to
- # remember, and avoid recursing
- my @orig=@ARGV;
- @ARGV=('-n', $action, @orig);
- action("remember", $dir, $topdir, $subdir);
- @ARGV=@orig;
- }
- }
- elsif ($ret != 0) {
- print STDERR "mr $action: command died ($ret)\n";
- }
- return FAILED;
- }
- else {
- if ($is_checkout && ! -d $dir) {
- print STDERR "mr $action: $dir missing after checkout\n";;
- return FAILED;
- }
-
- my ($ret, $hook_out)=hook("post_$action", $topdir, $subdir);
- return $ret if $ret != OK;
-
- if ($is_checkout || $is_update) {
- if ($is_checkout && ! $no_chdir) {
- if (! chdir($checkout_dir)) {
- print STDERR "mr $action: failed to chdir to $checkout_dir: $!\n";
- return FAILED;
- }
- }
- my ($ret, $hook_out)=hook("fixups", $topdir, $subdir);
- return $ret if $ret != OK;
- }
-
- return (OK, $out || $hook_out);
- }
- }
-}
-
-sub hook {
- my ($hook, $topdir, $subdir) = @_;
-
- my $command=$config{$topdir}{$subdir}{$hook};
- return OK unless defined $command;
- my ($ret,$out)=runsh $hook, $topdir, $subdir, $command, [], sub {
- my $sh=shift;
- if (!$jobs || $jobs > 1 || $quiet || $minimal) {
- return terminal_friendly_spawn(undef, $sh, $quiet, $minimal, $jobs);
- }
- else {
- system($sh);
- }
- };
- 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, $out);
-}
-
-# run actions on multiple repos, in parallel
-sub mrs {
- my $action=shift;
- my @repos=@_;
-
- $| = 1;
- my @active;
- my @fhs;
- my @out;
- my $running=0;
- while (@fhs or @repos) {
- while ((!$jobs || $running < $jobs) && @repos) {
- $running++;
- my $repo = shift @repos;
- pipe(my $outfh, CHILD_STDOUT);
- pipe(my $errfh, CHILD_STDERR);
- my $pid;
- unless ($pid = fork) {
- die "mr $action: cannot fork: $!" unless defined $pid;
- open(STDOUT, ">&CHILD_STDOUT") || die "mr $action cannot reopen stdout: $!";
- open(STDERR, ">&CHILD_STDERR") || die "mr $action cannot reopen stderr: $!";
- close CHILD_STDOUT;
- close CHILD_STDERR;
- close $outfh;
- close $errfh;
- exit +(action($action, @$repo))[0];
- }
- close CHILD_STDOUT;
- close CHILD_STDERR;
- push @active, [$pid, $repo];
- push @fhs, [$outfh, $errfh];
- push @out, ['', ''];
- }
- my ($rin, $rout) = ('','');
- my $nfound;
- foreach my $fh (@fhs) {
- next unless defined $fh;
- vec($rin, fileno($fh->[0]), 1) = 1 if defined $fh->[0];
- vec($rin, fileno($fh->[1]), 1) = 1 if defined $fh->[1];
- }
- $nfound = select($rout=$rin, undef, undef, 1);
- foreach my $channel (0, 1) {
- foreach my $i (0..$#fhs) {
- next unless defined $fhs[$i];
- my $fh = $fhs[$i][$channel];
- next unless defined $fh;
- if (vec($rout, fileno($fh), 1) == 1) {
- my $r = '';
- if (sysread($fh, $r, 1024) == 0) {
- close($fh);
- $fhs[$i][$channel] = undef;
- if (! defined $fhs[$i][0] &&
- ! defined $fhs[$i][1]) {
- waitpid($active[$i][0], 0);
- print STDOUT $out[$i][0];
- print STDERR $out[$i][1];
- record($active[$i][1], $? >> 8, $out[$i][0] || $out[$i][1]);
- splice(@fhs, $i, 1);
- splice(@active, $i, 1);
- splice(@out, $i, 1);
- $running--;
- }
- }
- $out[$i][$channel] .= $r;
- }
- }
- }
- }
-}
-
-sub record {
- my $dir=shift()->[0];
- my $ret=shift;
- my $out=shift;
-
- if ($ret == OK) {
- push @ok, $dir;
- print "\n" unless $quiet || ($minimal && !$out);
- }
- elsif ($ret == FAILED) {
- if ($interactive) {
- chdir($dir) unless $no_chdir;
- print STDERR "mr: Starting interactive shell. Exit shell to continue.\n";
- system((getpwuid($<))[8], "-i");
- }
- push @failed, $dir;
- print "\n";
- }
- elsif ($ret == SKIPPED) {
- push @skipped, $dir;
- }
- elsif ($ret == ABORT) {
- exit 1;
- }
- else {
- die "unknown exit status $ret";
- }
-}
-
-sub showstats {
- my $action=shift;
- if (! @ok && ! @failed && ! @skipped) {
- die "mr $action: no repositories found to work on\n";
- }
- print "mr $action: finished (".join("; ",
- showstat($#ok+1, "ok", "ok"),
- showstat($#failed+1, "failed", "failed"),
- showstat($#skipped+1, "skipped", "skipped"),
- ).")\n" unless $quiet || $minimal;
- if ($stats) {
- if (@skipped) {
- print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet || $minimal;
- }
- if (@failed) {
- print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
- }
- }
-}
-
-sub showstat {
- my $count=shift;
- my $singular=shift;
- my $plural=shift;
- if ($count) {
- return "$count ".($count > 1 ? $plural : $singular);
- }
- return;
-}
-
-# an ordered list of repos
-sub repolist {
- my @list;
- foreach my $topdir (sort keys %config) {
- foreach my $subdir (sort keys %{$config{$topdir}}) {
- push @list, {
- topdir => $topdir,
- subdir => $subdir,
- order => $config{$topdir}{$subdir}{order},
- };
- }
- }
- return sort {
- $a->{order} <=> $b->{order}
- ||
- $a->{topdir} cmp $b->{topdir}
- ||
- $a->{subdir} cmp $b->{subdir}
- } @list;
-}
-
-sub absrepodir {
- my $repo=shift;
- my $topdir=$repo->{topdir};
- my $subdir=$repo->{subdir};
- my $ret=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
- $ret=~s/\/\.$//;
- my $absret=safe_abs_path($ret);
- return (defined $absret ? $absret : $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()) {
- my $topdir=$repo->{topdir};
- my $subdir=$repo->{subdir};
-
- next if $subdir eq 'DEFAULT';
- my $dir=absrepodir($repo);
- my $d=$directory;
- $dir.="/" unless $dir=~/\/$/;
- $d.="/" unless $d=~/\/$/;
- next if $dir ne $d && $dir !~ /^\Q$d\E/;
- if (defined $max_depth) {
- my @a=split('/', $dir);
- my @b=split('/', $d);
- do { } while (@a && @b && shift(@a) eq shift(@b));
- next if @a > $max_depth || @b > $max_depth;
- }
- push @repos, [$dir, $topdir, $subdir];
- }
- if (! @repos) {
- # fallback to find a leaf repo
- foreach my $repo (reverse repolist()) {
- my $topdir=$repo->{topdir};
- my $subdir=$repo->{subdir};
-
- next if $subdir eq 'DEFAULT';
- my $dir=absrepodir($repo);
- my $d=$directory;
- $dir.="/" unless $dir=~/\/$/;
- $d.="/" unless $d=~/\/$/;
- if ($d=~/^\Q$dir\E/) {
- push @repos, [$dir, $topdir, $subdir];
- last;
- }
- }
- $no_chdir=1;
- }
- return @repos;
-}
-
-sub expandenv {
- my $val=shift;
-
-
- if ($val=~/\$/) {
- $val=`echo "$val"`;
- chomp $val;
- }
-
- return $val;
-}
-
-my %trusted;
-sub is_trusted_config {
- my $config=shift; # must be abs_pathed already
-
- # We always trust ~/.mrconfig.
- return 1 if $config eq safe_abs_path($HOME_MR_CONFIG);
-
- return 1 if $trust_all;
-
- my $trustfile=$ENV{HOME}."/.mrtrust";
-
- if (! %trusted) {
- $trusted{$HOME_MR_CONFIG}=1;
- if (open (TRUST, "<", $trustfile)) {
- while (<TRUST>) {
- chomp;
- s/^~\//$ENV{HOME}\//;
- my $d=safe_abs_path($_);
- $trusted{$d}=1 if defined $d;
- }
- close TRUST;
- }
- }
-
- return $trusted{$config};
-}
-
-
-sub is_trusted_repo {
- my $repo=shift;
-
- # Tightly limit what is allowed in a repo name.
- # No ../, no absolute paths, and no unusual filenames
- # that might try to escape to the shell.
- return $repo =~ /^[-_.+\/A-Za-z0-9]+$/ &&
- $repo !~ /\.\./ && $repo !~ /^\//;
-}
-
-sub is_trusted_checkout {
- my $command=shift;
-
- # To determine if the command is safe, compare it with the
- # *_trusted_checkout config settings. Those settings are
- # templates for allowed commands, so make sure that each word
- # of the command matches the corresponding word of the template.
-
- my @words;
- foreach my $word (split(' ', $command)) {
- # strip quoting
- if ($word=~/^'(.*)'$/) {
- $word=$1;
- }
- elsif ($word=~/^"(.*)"$/) {
- $word=$1;
- }
-
- push @words, $word;
- }
-
- foreach my $key (grep { /_trusted_checkout$/ }
- keys %{$config{''}{DEFAULT}}) {
- my @twords=split(' ', $config{''}{DEFAULT}{$key});
- next if @words > @twords;
-
- my $match=1;
- my $url;
- for (my $c=0; $c < @twords && $match; $c++) {
- if ($twords[$c] eq '$url') {
- # Match all the typical characters found in
- # urls, plus @ which svn can use. Note
- # that the "url" might also be a local
- # directory.
- $match=(
- defined $words[$c] &&
- $words[$c] =~ /^[-_.+:@\/A-Za-z0-9]+$/
- );
- $url=$words[$c];
- }
- elsif ($twords[$c] eq '$repo') {
- # If a repo is not specified, assume it
- # will be the last path component of the
- # url, or something derived from it, and
- # check that.
- if (! defined $words[$c] && defined $url) {
- ($words[$c])=$url=~/\/([^\/]+)\/?$/;
- }
-
- $match=(
- defined $words[$c] &&
- is_trusted_repo($words[$c])
- );
- }
- elsif (defined $words[$c] && $words[$c]=~/^($twords[$c])$/) {
- $match=1;
- }
- else {
- $match=0;
- }
- }
- return 1 if $match;
- }
-
- return 0;
-}
-
-my %loaded;
-sub loadconfig {
- my $f=shift;
- my $dir=shift;
- my $bootstrap_src=shift;
-
- my @toload;
-
- my $in;
- my $trusted;
- if (ref $f eq 'GLOB') {
- $dir="";
- $in=$f;
- $trusted=1;
- }
- else {
- my $absf=safe_abs_path($f);
- if ($loaded{$absf}) {
- return;
- }
- $loaded{$absf}=1;
-
- $trusted=is_trusted_config($absf);
-
- if (! defined $dir) {
- ($dir)=$f=~/^(.*\/)[^\/]+$/;
- if (! defined $dir) {
- $dir=".";
- }
- }
-
- $dir=safe_abs_path($dir)."/";
-
- if (! exists $configfiles{$dir}) {
- $configfiles{$dir}=$f;
- }
-
- # copy in defaults from first parent
- my $parent=$dir;
- while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
- if ($parent eq '/') {
- $parent="";
- }
- if (exists $config{$parent} &&
- exists $config{$parent}{DEFAULT}) {
- $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
- last;
- }
- }
-
- 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 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 $lineno=0;
- my $included=undef;
-
- my $line;
- my $nextline = sub {
- if ($included) {
- $included--;
- }
- else {
- $included=undef;
- $lineno++;
- }
- $line=shift @lines;
- chomp $line;
- return $line;
- };
- my $lineerror = sub {
- my $msg=shift;
- if (defined $included) {
- die "mr: $msg at $f line $lineno, included line: $line\n";
- }
- else {
- die "mr: $msg at $f line $lineno\n";
- }
- };
- my $trusterror = sub {
- my $msg=shift;
-
- if (defined $bootstrap_src) {
- die "mr: $msg in untrusted $bootstrap_src line $lineno\n".
- "(To trust this url, --trust-all can be used; but please use caution;\n".
- "this can allow arbitrary code execution!)\n";
- }
- else {
- die "mr: $msg in untrusted $f line $lineno\n".
- "(To trust this file, list it in ~/.mrtrust.)\n";
- }
- };
-
- while (@lines) {
- $_=$nextline->();
-
- next if /^\s*\#/ || /^\s*$/;
-
- if (! $trusted && /[[:cntrl:]]/) {
- $trusterror->("illegal control character");
- }
-
- if (/^\[([^\]]*)\]\s*$/) {
- $section=$1;
-
- if (! $trusted) {
- if (! is_trusted_repo($section) ||
- $section eq 'ALIAS' ||
- $section eq 'DEFAULT') {
- $trusterror->("illegal section \"[$section]\"");
- }
- }
- $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;
- my $value=$2;
-
- # continued value
- while (@lines && $lines[0]=~/^\s(.+)/) {
- $value.="\n$1";
- chomp $value;
- $nextline->();
- }
-
- if (! $trusted) {
- # Untrusted files can only contain a few
- # settings in specific known-safe formats.
- if ($parameter eq 'checkout') {
- if (! is_trusted_checkout($value)) {
- $trusterror->("illegal checkout command \"$value\"");
- }
- }
- 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.
- }
- else {
- $trusterror->("illegal setting \"$parameter=$value\"");
- }
- }
-
- if ($parameter eq "include") {
- print "mr: including output of \"$value\"\n" if $verbose;
- my @inc=`$value`;
- if ($?) {
- print STDERR "mr: include command exited nonzero ($?)\n";
- }
- $included += @inc;
- unshift @lines, @inc;
- next;
- }
-
- if ($parameter eq 'jobs') {
- print "mr: setting --jobs to \"$value\"\n" if $verbose;
- unless ($value =~ /^\d+$/) {
- print "mr: error: --jobs must be numeric\n";
- exit 2
- }
- $jobs=$value;
- next;
- }
-
- if (! defined $section) {
- $lineerror->("parameter ($parameter) not in section");
- }
- if ($section eq 'ALIAS') {
- $alias{$parameter}=$value;
- }
- elsif ($parameter eq 'lib' or $parameter =~ s/_append$//) {
- $config{$dir}{$section}{$parameter}.="\n".$value."\n";
- }
- else {
- $config{$dir}{$section}{$parameter}=$value;
- if ($parameter =~ /.*_(.*)/) {
- $knownactions{$1}=1;
- }
- else {
- $knownactions{$parameter}=1;
- }
- if ($parameter eq 'chain' &&
- 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";
- }
- }
- else {
- push @toload, ["$chaindir/.mrconfig", $chaindir];
- }
- }
- }
- }
- }
- else {
- $lineerror->("parse error");
- }
- }
-
- 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
- my $targetsection=shift;
- # fields to change in the section
- # To remove a field, set its value to "".
- my %changefields=@_;
-
- my @lines;
- my @out;
-
- if (-e $f) {
- open(my $in, "<", $f) || die "mr: open $f: $!\n";
- @lines=<$in>;
- close $in;
- }
-
- my $formatfield=sub {
- my $field=shift;
- my @value=split(/\n/, shift);
-
- return "$field = ".shift(@value)."\n".
- join("", map { "\t$_\n" } @value);
- };
- my $addfields=sub {
- my @blanks;
- while ($out[$#out] =~ /^\s*$/) {
- unshift @blanks, pop @out;
- }
- foreach my $field (sort keys %changefields) {
- if (length $changefields{$field}) {
- push @out, "$field = $changefields{$field}\n";
- delete $changefields{$field};
- }
- }
- push @out, @blanks;
- };
-
- my $section;
- while (@lines) {
- $_=shift(@lines);
-
- if (/^\s*\#/ || /^\s*$/) {
- push @out, $_;
- }
- elsif (/^\[([^\]]*)\]\s*$/) {
- if (defined $section &&
- $section eq $targetsection) {
- $addfields->();
- }
-
- $section=expandenv($1);
-
- push @out, $_;
- }
- elsif (/^(\w+)\s*=\s(.*)/) {
- my $parameter=$1;
- my $value=$2;
-
- # continued value
- while (@lines && $lines[0]=~/^\s(.+)/) {
- shift(@lines);
- $value.="\n$1";
- chomp $value;
- }
-
- if ($section eq $targetsection) {
- if (exists $changefields{$parameter}) {
- if (length $changefields{$parameter}) {
- $value=$changefields{$parameter};
- }
- delete $changefields{$parameter};
- }
- }
-
- push @out, $formatfield->($parameter, $value);
- }
- }
-
- if (defined $section &&
- $section eq $targetsection) {
- $addfields->();
- }
- elsif (%changefields) {
- push @out, "\n[$targetsection]\n";
- foreach my $field (sort keys %changefields) {
- if (length $changefields{$field}) {
- push @out, $formatfield->($field, $changefields{$field});
- }
- }
- }
-
- open(my $out, ">", $f) || die "mr: write $f: $!\n";
- print $out @out;
- close $out;
-}
-
-sub dispatch {
- my $action=shift;
-
- # actions that do not operate on all repos
- if ($action eq 'config') {
- config(@ARGV);
- }
- elsif ($action eq 'register') {
- register(@ARGV);
- }
- elsif ($action eq 'bootstrap') {
- bootstrap();
- }
- elsif ($action eq 'remember' ||
- $action eq 'offline' ||
- $action eq 'online') {
- my @repos=selectrepos;
- action($action, @{$repos[0]}) if @repos;
- exit 0;
- }
-
- if (!$jobs || $jobs > 1) {
- mrs($action, selectrepos());
- }
- else {
- foreach my $repo (selectrepos()) {
- record($repo, action($action, @$repo));
- }
- }
-}
-
-sub help {
- if (! -e "$ENV{MR_PATH}") {
- die "cannot find the program path";
- }
- exec("perldoc", $ENV{MR_PATH}) || die "exec perldoc: $!";
-}
-
-sub config {
- if (@_ < 2) {
- die "mr config: not enough parameters\n";
- }
- my $section=shift;
- if ($section=~/^\//) {
- # try to convert to a path relative to the config file
- my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
- $dir=safe_abs_path($dir);
- $dir.="/" unless $dir=~/\/$/;
- if ($section=~/^\Q$dir\E(.*)/) {
- $section=$1;
- }
- }
- my %changefields;
- foreach (@_) {
- if (/^([^=]+)=(.*)$/) {
- $changefields{$1}=$2;
- }
- else {
- my $found=0;
- foreach my $topdir (sort keys %config) {
- if (exists $config{$topdir}{$section} &&
- exists $config{$topdir}{$section}{$_}) {
- print $config{$topdir}{$section}{$_}."\n";
- $found=1;
- last if $section eq 'DEFAULT';
- }
- }
- if (! $found) {
- die "mr config: $section $_ not set\n";
- }
- }
- }
- modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
- exit 0;
-}
-
-sub register {
- if ($config_overridden) {
- # Find the directory that the specified config file is
- # located in.
- ($directory)=safe_abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/;
- }
- else {
- # Find the closest known mrconfig file to the current
- # directory.
- $directory.="/" unless $directory=~/\/$/;
- my $foundconfig=0;
- foreach my $topdir (reverse sort keys %config) {
- next unless length $topdir;
- if ($directory=~/^\Q$topdir\E/) {
- $ENV{MR_CONFIG}=$configfiles{$topdir};
- $directory=$topdir;
- $foundconfig=1;
- last;
- }
- }
- if (! $foundconfig) {
- $directory=""; # no config file, use builtin
- }
- }
- if (@ARGV) {
- my $subdir=shift @ARGV;
- if (! chdir($subdir)) {
- print STDERR "mr register: failed to chdir to $subdir: $!\n";
- }
- }
-
- $ENV{MR_REPO}=getcwd();
- my $command=findcommand("register", $ENV{MR_REPO}, $directory, 'DEFAULT', 0);
- if (! defined $command) {
- die "mr register: unknown repository type\n";
- }
-
- $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);
- print "mr register: running >>$command<<\n" if $verbose;
- exec($command) || die "exec: $!";
-}
-
-sub bootstrap {
- eval q{use File::Copy};
- die $@ if $@;
-
- my $src=shift @ARGV;
- my $dir=shift @ARGV || ".";
-
- if (! defined $src || ! length $src) {
- die "mr: bootstrap requires source\n";
- }
-
- # Retrieve config file.
- eval q{use File::Temp};
- die $@ if $@;
- my $tmpconfig=File::Temp->new();
- if ($src =~ m!^[\w\d]+://!) {
- # Download the config file to a temporary location.
- my @downloader;
- if ($src =~ m!^ssh://(.*)!) {
- @downloader = ("scp", $1, $tmpconfig);
- }
- else {
- @downloader = ("curl", "-A", "mr", "-L", "-s", $src, "-o", $tmpconfig);
- push(@downloader, "-k") if $insecure;
- }
- my $status = system(@downloader);
- die "mr bootstrap: invalid SSL certificate for $src (consider -k)\n"
- if $downloader[0] eq 'curl' && $status >> 8 == 60;
- die "mr bootstrap: download of $src failed\n" if $status != 0;
- }
- elsif ($src eq '-') {
- # Config file is read from stdin.
- copy(\*STDIN, $tmpconfig) || die "stdin: $!";
- seek $tmpconfig, 0, 0;
- }
- else {
- # Config file is local.
- die "mr bootstrap: cannot read file '$src'"
- unless -r $src;
- copy($src, $tmpconfig) || die "copy: $!";
- seek $tmpconfig, 0, 0;
- }
-
- # Sanity check on destination directory.
- if (! -e $dir) {
- system("mkdir", "-p", $dir);
- }
- chdir($dir) || die "chdir $dir: $!";
-
- # Special case to handle checkout of the "." repo, which
- # would normally be skipped.
- my $topdir=safe_abs_path(".")."/";
- my @repo=($topdir, $topdir, ".");
- loadconfig($tmpconfig, $topdir, $src);
- record(\@repo, action("checkout", @repo, 1))
- if exists $config{$topdir}{"."}{"checkout"};
-
- if (-e ".mrconfig") {
- print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $src\n";
- }
- else {
- 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 { safe_abs_path($_) ne safe_abs_path($topdir) } @skipped;
- showstats("bootstrap");
- exitstats();
-}
-
-# alias expansion and command stemming
-sub expandaction {
- my $action=shift;
- if (exists $alias{$action}) {
- $action=$alias{$action};
- }
- if (! exists $knownactions{$action}) {
- my @matches = grep { /^\Q$action\E/ }
- keys %knownactions, keys %alias;
- if (@matches == 1) {
- $action=$matches[0];
- }
- elsif (@matches == 0) {
- die "mr: unknown action \"$action\" (known actions: ".
- join(", ", sort keys %knownactions).")\n";
- }
- else {
- die "mr: ambiguous action \"$action\" (matches: ".
- join(", ", @matches).")\n";
- }
- }
- return $action;
-}
-
-sub find_mrconfig {
- my $dir=getcwd();
- while (length $dir) {
- if (-e "$dir/.mrconfig") {
- return "$dir/.mrconfig";
- }
- $dir=~s/\/[^\/]*$//;
- }
- return $HOME_MR_CONFIG;
-}
-
-sub getopts {
- my @saved=@ARGV;
- Getopt::Long::Configure("bundling", "no_permute");
- my $result=GetOptions(
- "d|directory=s" => sub { $directory=safe_abs_path($_[1]) },
- "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
- "p|path" => sub { }, # now default, ignore
- "f|force" => \$force,
- "force-env" => \$force_env,
- "v|verbose" => \$verbose,
- "m|minimal" => \$minimal,
- "q|quiet" => \$quiet,
- "s|stats" => \$stats,
- "k|insecure" => \$insecure,
- "i|interactive" => \$interactive,
- "n|no-recurse:i" => \$max_depth,
- "j|jobs:i" => \$jobs,
- "t|trust-all" => \$trust_all,
- );
- if (! $result || @ARGV < 1) {
- die("Usage: mr [options] action [params ...]\n".
- "(Use mr help for man page.)\n");
- }
-
- $ENV{MR_SWITCHES}="";
- foreach my $option (@saved) {
- last if $option eq $ARGV[0];
- $ENV{MR_SWITCHES}.="$option ";
- }
-}
-
-sub check {
- return if $force_env;
- my @env = qw(GIT_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_WORK_TREE VCSH_COMMAND VCSH_DIRECTORY VCSH_REPO_NAME);
- my $error;
- foreach (@env) {
- if ($ENV{$_}) {
- $error=1;
- print STDERR "mr: environment variable '$_' is set.\n";
- }
- }
- die ("mr: The variables above would lead to very interesting effects.
- Unfortunately, most of those effects result in data loss so we stop here.\n") if $error;
-}
-
-sub init {
- $SIG{INT}=sub {
- print STDERR "$erase_line" if defined $erase_line && $terminal && !$quiet && $minimal && 1 == $jobs;
- print STDERR "mr: interrupted\n";
- exit 2;
- };
-
- # This can happen if it's run in a directory that was removed
- # or other strangeness.
- if (! defined $directory) {
- die("mr: failed to determine working directory\n");
- }
- # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
- # the config file might be a symlink to elsewhere, and the directory it's
- # in is significant.
- if ($ENV{MR_CONFIG} !~ /^\//) {
- $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
- }
- # Try to set MR_PATH to the path to the program.
- eval {
- use FindBin qw($Bin $Script);
- $ENV{MR_PATH}=$Bin."/".$Script;
- };
-}
-
-sub exitstats {
- if (@failed) {
- exit 1;
- }
- else {
- exit 0;
- }
-}
-
-# abs_path crashes on windows and some other platforms when given a file
-# that does not exist.
-sub safe_abs_path {
- my $f=shift;
- my $p=eval { abs_path($f) };
- if ($@) {
- return $f;
- }
- else {
- return $p;
- }
-}
-
-sub main {
- getopts();
- check();
- init();
- help(@ARGV) if $ARGV[0] eq 'help';
-
- startingconfig();
- loadconfig($HOME_MR_CONFIG);
- loadconfig($ENV{MR_CONFIG});
- #use Data::Dumper; print Dumper(\%config);
-
- my $action=expandaction(shift @ARGV);
- dispatch($action);
-
- showstats($action);
- exitstats();
-}
-
-# Finally, some useful actions that mr knows about by default.
-# These can be overridden in ~/.mrconfig.
-__DATA__
-[ALIAS]
-co = checkout
-ci = commit
-ls = list
-
-[DEFAULT]
-order = 10
-lib =
- error() {
- echo "mr: $@" >&2
- exit 1
- }
- warning() {
- echo "mr (warning): $@" >&2
- }
- info() {
- echo "mr: $@" >&2
- }
- hours_since() {
- if [ -z "$1" ] || [ -z "$2" ]; then
- error "mr: usage: hours_since action num"
- fi
- for dir in .git .svn .bzr CVS .hg _darcs _FOSSIL_ .fslckout; do
- if [ -e "$MR_REPO/$dir" ]; then
- flagfile="$MR_REPO/$dir/.mr_last$1"
- break
- fi
- done
- if [ -z "$flagfile" ]; then
- error "cannot determine flag filename"
- fi
- delta=`perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"`
- if [ "$delta" -lt "$2" ]; then
- return 1
- else
- touch "$flagfile"
- return 0
- fi
- }
- is_bzr_checkout() {
- LANG=C bzr info | egrep -q '^Checkout'
- }
- lazy() {
- if [ -d "$MR_REPO" ]; then
- return 1
- else
- return 0
- fi
- }
-
-svn_test = perl: -d "$ENV{MR_REPO}/.svn"
-git_test = perl: -e "$ENV{MR_REPO}/.git"
-bzr_test = perl: -d "$ENV{MR_REPO}/.bzr"
-cvs_test = perl: -d "$ENV{MR_REPO}/CVS"
-hg_test = perl: -d "$ENV{MR_REPO}/.hg"
-darcs_test = perl: -d "$ENV{MR_REPO}/_darcs"
-fossil_test = perl: -f "$ENV{MR_REPO}/_FOSSIL_" || -f "$ENV{MR_REPO}/.fslckout"
-git_bare_test = perl:
- -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
- -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
- `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get core.bare` =~ /true/
-vcsh_test = perl:
- -d "$ENV{MR_REPO}/refs/heads" && -d "$ENV{MR_REPO}/refs/tags" &&
- -d "$ENV{MR_REPO}/objects" && -f "$ENV{MR_REPO}/config" &&
- `GIT_CONFIG="$ENV{MR_REPO}"/config git config --get vcsh.vcsh` =~ /true/
-veracity_test = perl: -d "$ENV{MR_REPO}/.sgdrawer"
-
-svn_update = svn update "$@"
-git_update = git pull "$@"
-bzr_update =
- if is_bzr_checkout; then
- bzr update "$@"
- else
- bzr merge --pull "$@"
- fi
-cvs_update = cvs -q update "$@"
-hg_update = hg pull "$@"; hg update "$@"
-darcs_update = darcs pull -a "$@"
-fossil_update = fossil pull "$@"
-vcsh_update = vcsh run "$MR_REPO" git pull "$@"
-veracity_update = vv pull "$@" && vv update "$@"
-
-git_fetch = git fetch --all --prune --tags "$@"
-git_svn_fetch = git svn fetch "$@"
-darcs_fetch = darcs fetch "$@"
-hg_fetch = hg pull "$@"
-
-svn_clean =
- if [ "x$1" = x-f ] ; then
- shift
- svn-clean "$@"
- else
- svn-clean --print "$@"
- fi
-git_clean =
- if [ "x$1" = x-f ] ; then
- shift
- git clean -dx --force "$@"
- else
- git clean -dx --dry-run "$@"
- fi
-git_svn_clean =
- if [ "x$1" = x-f ] ; then
- shift
- git clean -dx --force "$@"
- else
- git clean -dx --dry-run "$@"
- fi
-bzr_clean =
- if [ "x$1" = x-f ] ; then
- shift
- bzr clean-tree --verbose --force --ignored --unknown --detritus "$@"
- else
- bzr clean-tree --verbose --dry-run --ignored --unknown --detritus "$@"
- fi
-cvs_clean =
- if [ "x$1" = x-f ] ; then
- shift
- cvs-clean "$@"
- else
- cvs-clean --dry-run "$@"
- fi
-hg_clean =
- if [ "x$1" = x-f ] ; then
- shift
- hg purge --print --all "$@"
- hg purge --all "$@"
- else
- hg purge --print --all "$@"
- fi
-fossil_clean =
- if [ "x$1" = x-f ] ; then
- shift
- fossil clean --dry-run --dotfiles --emptydirs "$@"
- else
- fossil clean --force --dotfiles --emptydirs "$@"
- fi
-vcsh_clean =
- if [ "x$1" = x-f ] ; then
- shift
- vcsh run "$MR_REPO" git clean -dx "$@"
- else
- vcsh run "$MR_REPO" git clean -dx --dry-run "$@"
- fi
-
-svn_status = svn status "$@"
-git_status = git status -s "$@" || true; git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline || true; git stash list
-bzr_status = bzr status --short "$@"; bzr missing
-cvs_status = cvs -q status | grep -E '^(File:.*Status:|\?)' | grep -v 'Status: Up-to-date'
-hg_status = hg status "$@"; hg summary --quiet | grep -v 'parent: 0:'
-darcs_status = darcs whatsnew -ls "$@" || true
-fossil_status = fossil changes "$@"
-vcsh_status = vcsh run "$MR_REPO" git -c status.relativePaths=false status -s "$@" || true
-veracity_status = vv status "$@"
-
-svn_commit = svn commit "$@"
-git_commit = git commit -a "$@" && git push --all
-bzr_commit =
- if is_bzr_checkout; then
- bzr commit "$@"
- else
- bzr commit "$@" && bzr push
- fi
-cvs_commit = cvs commit "$@"
-hg_commit = hg commit "$@" && hg push
-darcs_commit = darcs record -a "$@" && darcs push -a
-fossil_commit = fossil commit "$@"
-vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all
-veracity_commit = vv commit "$@" && vv push
-
-git_record = git commit -a "$@"
-bzr_record =
- if is_bzr_checkout; then
- bzr commit --local "$@"
- else
- bzr commit "$@"
- fi
-hg_record = hg commit "$@"
-darcs_record = darcs record -a "$@"
-fossil_record = fossil commit "$@"
-vcsh_record = vcsh run "$MR_REPO" git commit -a "$@"
-veracity_record = vv commit "$@"
-
-svn_push = :
-git_push = git push "$@"
-bzr_push = bzr push "$@"
-cvs_push = :
-hg_push = hg push "$@" || if [ "$?" -eq "255" ] ; then exit 1; else exit 0; fi
-darcs_push = darcs push -a "$@"
-fossil_push = fossil push "$@"
-vcsh_push = vcsh run "$MR_REPO" git push "$@"
-veracity_push = vv push "$@"
-
-svn_diff = svn diff "$@"
-git_diff = git diff "$@"
-bzr_diff = bzr diff "$@"
-cvs_diff = cvs -q diff "$@"
-hg_diff = hg diff "$@"
-darcs_diff = darcs diff -u "$@"
-fossil_diff = fossil diff "$@"
-vcsh_diff = vcsh run "$MR_REPO" git diff "$@"
-veracity_diff = vv diff "$@"
-
-svn_log = svn log "$@"
-git_log = git log "$@"
-bzr_log = bzr log "$@"
-cvs_log = cvs log "$@"
-hg_log = hg log "$@"
-darcs_log = darcs changes "$@"
-git_bare_log = git log "$@"
-fossil_log = fossil timeline "$@"
-vcsh_log = vcsh run "$MR_REPO" git log "$@"
-veracity_log = vv log "$@"
-
-hg_grep = hg grep "$@"
-cvs_grep = ack-grep "$@"
-svn_grep = ack-grep "$@"
-git_svn_grep = git grep "$@"
-git_grep = git grep "$@"
-bzr_grep = ack-grep "$@"
-darcs_grep = ack-grep "$@"
-
-run = "$@"
-
-svn_register =
- url=`LC_ALL=C svn info . | grep -i '^URL:' | cut -d ' ' -f 2`
- if [ -z "$url" ]; then
- error "cannot determine svn url"
- fi
- echo "Registering svn url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "`pwd`" checkout="svn co '$url' '$MR_REPO'"
-git_register =
- url="`LC_ALL=C git config --get remote.origin.url`" || true
- if [ -z "$url" ]; then
- error "cannot determine git url"
- fi
- echo "Registering git url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
-bzr_register =
- url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}' | head -n 1`"
- if [ -z "$url" ]; then
- error "cannot determine bzr url"
- fi
- echo "Registering bzr url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "`pwd`" checkout="bzr branch '$url' '$MR_REPO'"
-cvs_register =
- repo=`cat CVS/Repository`
- root=`cat CVS/Root`
- if [ -z "$root" ]; then
- error "cannot determine cvs root"
- fi
- echo "Registering cvs repository $repo at root $root"
- mr -c "$MR_CONFIG" config "`pwd`" checkout="cvs -d '$root' co -d '$MR_REPO' '$repo'"
-hg_register =
- url=`hg showconfig paths.default`
- echo "Registering mercurial repo url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "`pwd`" checkout="hg clone '$url' '$MR_REPO'"
-darcs_register =
- url=`cat _darcs/prefs/defaultrepo`
- echo "Registering darcs repository $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "`pwd`" checkout="darcs get '$url' '$MR_REPO'"
-git_bare_register =
- url="`LC_ALL=C GIT_CONFIG=config git config --get remote.origin.url`" || true
- if [ -z "$url" ]; then
- error "cannot determine git url"
- fi
- echo "Registering git url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone --bare '$url' '$MR_REPO'"
-vcsh_register =
- url="`LC_ALL=C vcsh run "$MR_REPO" git config --get remote.origin.url`" || true
- if [ -z "$url" ]; then
- error "cannot determine git url"
- fi
- echo "Registering git url: $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "`pwd`" checkout="vcsh clone '$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'"
-veracity_register =
- url=`vv config | grep sync_targets | sed -e 's/sync_targets:*.//g' -e 's/ //g'`
- repo=`vv repo info | grep repository | sed -e 's/Current repository:*.//g' -e 's/ //g'`
- echo "Registering veracity repository $url in $MR_CONFIG"
- mr -c "$MR_CONFIG" config "`pwd`" checkout="mkdir -p '$MR_REPO' && cd '$MR_REPO' && vv checkout '$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 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
-vcsh_old_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
-vcsh_trusted_checkout = vcsh clone $url $repo
-# fossil: messy to do
-veracity_trusted_checkout = vv clone $url $repo
-
-
-list = true
-config =
-bootstrap =
-
-online =
- if [ -s ~/.mrlog ]; then
- info "running offline commands"
- mv -f ~/.mrlog ~/.mrlog.old
- if ! sh -e ~/.mrlog.old; then
- error "offline command failed; left in ~/.mrlog.old"
- fi
- rm -f ~/.mrlog.old
- else
- info "no offline commands to run"
- fi
-offline =
- umask 077
- touch ~/.mrlog
- info "offline mode enabled"
-remember =
- info "remembering command: 'mr $@'"
- command="mr -d '$(pwd)' $MR_SWITCHES"
- for w in "$@"; do
- command="$command '$w'"
- done
- if [ ! -e ~/.mrlog ] || ! grep -q -F "$command" ~/.mrlog; then
- echo "$command" >> ~/.mrlog
- fi
-
-ed = echo "A horse is a horse, of course, of course.."
-T = echo "I pity the fool."
-right = echo "Not found."
-
-# vim:sw=8:sts=0:ts=8:noet
-# Local variables:
-# indent-tabs-mode: t
-# cperl-indent-level: 8
-# End: