X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/9a9cbb453427926f2f658734285f6305c7502afb..05c1b453b27f8e2629215d6faba92f12dcf2ab93:/mr diff --git a/mr b/mr index bb98ba6..fd380db 100755 --- a/mr +++ b/mr @@ -1,7 +1,5 @@ #!/usr/bin/perl -#man{{{ - =head1 NAME mr - a Multiple Repository management tool @@ -22,12 +20,18 @@ B [options] diff B [options] log +B [options] bootstrap url + B [options] register [repository] B [options] config section ["parameter=[value]" ...] B [options] action [params ...] +B [options] [online|offline] + +B [options] remember action [params ...] + =head1 DESCRIPTION B is a Multiple Repository management tool. It can checkout, update, or @@ -77,6 +81,11 @@ remote repository. Only supported for distributed revision 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. + =item diff Show a diff of uncommitted changes. @@ -91,6 +100,14 @@ These commands are also available: =over 4 +=item bootstrap url + +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. + +(Please only do this if you have reason to trust the url, since +mrconfig files can contain arbitrary commands!) + =item list (or ls) List the repositories that mr will act on. @@ -126,6 +143,24 @@ To see the built-in library of shell functions contained in mr: The ~/.mrconfig file is used by default. To use a different config file, use the -c option. +=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. @@ -154,6 +189,11 @@ the current working directory. Use the specified mrconfig file. The default is B<~/.mrconfig> +=item -p + +Search in the current directory, and its parent directories and use +the first B<.mrconfig> found, instead of the default B<~/.mrconfig>. + =item -v Be verbose. @@ -189,7 +229,7 @@ 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 yeild +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. =back @@ -311,9 +351,20 @@ 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 ~/.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 /usr/share/mr/. See +the documentation in the files for details about using them. + =head1 AUTHOR -Copyright 2007 Joey Hess +Copyright 2007-2009 Joey Hess Licensed under the GNU GPL version 2 or higher. @@ -321,8 +372,6 @@ http://kitenet.net/~joey/code/mr/ =cut -#}}} - use warnings; use strict; use Getopt::Long; @@ -358,7 +407,7 @@ my (@ok, @failed, @skipped); main(); my %rcs; -sub rcs_test { #{{{ +sub rcs_test { my ($action, $dir, $topdir, $subdir) = @_; if (exists $rcs{$dir}) { @@ -393,9 +442,9 @@ sub rcs_test { #{{{ else { return $rcs{$dir}=$rcs; } -} #}}} +} -sub findcommand { #{{{ +sub findcommand { my ($action, $dir, $topdir, $subdir, $is_checkout) = @_; if (exists $config{$topdir}{$subdir}{$action}) { @@ -415,16 +464,18 @@ sub findcommand { #{{{ else { return undef; } -} #}}} +} -sub action { #{{{ +sub action { my ($action, $dir, $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'); + $ENV{MR_REPO}=$dir; + if ($is_checkout) { if (-d $dir) { print "mr $action: $dir already exists, skipping checkout\n" if $verbose; @@ -439,8 +490,6 @@ sub action { #{{{ } } - $ENV{MR_REPO}=$dir; - my $skiptest=findcommand("skip", $dir, $topdir, $subdir, $is_checkout); my $command=findcommand($action, $dir, $topdir, $subdir, $is_checkout); @@ -511,6 +560,14 @@ sub action { #{{{ 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"; @@ -526,10 +583,10 @@ sub action { #{{{ return OK; } } -} #}}} +} # run actions on multiple repos, in parallel -sub mrs { #{{{ +sub mrs { my $action=shift; my @repos=@_; @@ -596,9 +653,9 @@ sub mrs { #{{{ } } } -} #}}} +} -sub record { #{{{ +sub record { my $dir=shift()->[0]; my $ret=shift; @@ -624,9 +681,9 @@ sub record { #{{{ else { die "unknown exit status $ret"; } -} #}}} +} -sub showstats { #{{{ +sub showstats { my $action=shift; if (! @ok && ! @failed && ! @skipped) { die "mr $action: no repositories found to work on\n"; @@ -644,9 +701,9 @@ sub showstats { #{{{ print STDERR "mr $action: (failed: ".join(" ", @failed).")\n"; } } -} #}}} +} -sub showstat { #{{{ +sub showstat { my $count=shift; my $singular=shift; my $plural=shift; @@ -654,10 +711,10 @@ sub showstat { #{{{ return "$count ".($count > 1 ? $plural : $singular); } return; -} #}}} +} # an ordered list of repos -sub repolist { #{{{ +sub repolist { my @list; foreach my $topdir (sort keys %config) { foreach my $subdir (sort keys %{$config{$topdir}}) { @@ -675,10 +732,10 @@ sub repolist { #{{{ || $a->{subdir} cmp $b->{subdir} } @list; -} #}}} +} # figure out which repos to act on -sub selectrepos { #{{{ +sub selectrepos { my @repos; foreach my $repo (repolist()) { my $topdir=$repo->{topdir}; @@ -717,9 +774,9 @@ sub selectrepos { #{{{ $no_chdir=1; } return @repos; -} #}}} +} -sub expandenv { #{{{ +sub expandenv { my $val=shift; @@ -729,10 +786,10 @@ sub expandenv { #{{{ } return $val; -} #}}} +} my %loaded; -sub loadconfig { #{{{ +sub loadconfig { my $f=shift; my @toload; @@ -808,6 +865,9 @@ sub loadconfig { #{{{ if ($parameter eq "include") { print "mr: including output of \"$value\"\n" if $verbose; unshift @lines, `$value`; + if ($?) { + print STDERR "mr: include command exited nonzero ($?)\n"; + } next; } @@ -861,9 +921,9 @@ sub loadconfig { #{{{ foreach (@toload) { loadconfig($_); } -} #}}} +} -sub modifyconfig { #{{{ +sub modifyconfig { my $f=shift; # the section to modify or add my $targetsection=shift; @@ -958,9 +1018,9 @@ sub modifyconfig { #{{{ open(my $out, ">", $f) || die "mr: write $f: $!\n"; print $out @out; close $out; -} #}}} +} -sub dispatch { #{{{ +sub dispatch { my $action=shift; # actions that do not operate on all repos @@ -973,6 +1033,16 @@ sub dispatch { #{{{ 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()); @@ -982,13 +1052,13 @@ sub dispatch { #{{{ record($repo, action($action, @$repo)); } } -} #}}} +} -sub help { #{{{ +sub help { exec($config{''}{DEFAULT}{help}) || die "exec: $!"; -} #}}} - -sub config { #{{{ +} + +sub config { if (@_ < 2) { die "mr config: not enough parameters\n"; } @@ -1024,10 +1094,15 @@ sub config { #{{{ } modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields; exit 0; -} #}}} +} -sub register { #{{{ - if (! $config_overridden) { +sub register { + if ($config_overridden) { + # Find the directory that the specified config file is + # located in. + ($directory)=abs_path($ENV{MR_CONFIG})=~/^(.*\/)[^\/]+$/; + } + else { # Find the closest known mrconfig file to the current # directory. $directory.="/" unless $directory=~/\/$/; @@ -1064,10 +1139,29 @@ sub register { #{{{ 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; + + 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"; + } + + if (system("curl", "-s", $url, "-o", ".mrconfig") != 0) { + die "mr: download of $url failed\n"; + } + + exec("mr $ENV{MR_SWITCHES} -c .mrconfig checkout"); + die "failed to run mr checkout"; +} # alias expansion and command stemming -sub expandaction { #{{{ +sub expandaction { my $action=shift; if (exists $alias{$action}) { $action=$alias{$action}; @@ -1088,13 +1182,26 @@ sub expandaction { #{{{ } } return $action; -} #}}} +} -sub getopts { #{{{ +sub find_nearest_mrconfig { + my $dir=getcwd(); + while (length $dir) { + if (-e "$dir/.mrconfig") { + return "$dir/.mrconfig"; + } + $dir=~s/\/[^\/]*$//; + } + die "no .mrconfig found in path\n"; +} + +sub getopts { + my @saved=@ARGV; Getopt::Long::Configure("bundling", "no_permute"); 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 }, "v|verbose" => \$verbose, "q|quiet" => \$quiet, "s|stats" => \$stats, @@ -1106,9 +1213,15 @@ sub getopts { #{{{ die("Usage: mr [-d directory] 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 init { #{{{ +sub init { $SIG{INT}=sub { print STDERR "mr: interrupted\n"; exit 2; @@ -1130,15 +1243,16 @@ sub init { #{{{ use FindBin qw($Bin $Script); $ENV{MR_PATH}=$Bin."/".$Script; }; -} #}}} +} -sub main { #{{{ +sub main { getopts(); init(); + loadconfig(\*DATA); loadconfig($ENV{MR_CONFIG}); #use Data::Dumper; print Dumper(\%config); - + my $action=expandaction(shift @ARGV); dispatch($action); showstats($action); @@ -1152,11 +1266,10 @@ sub main { #{{{ else { exit 0; } -} #}}} +} # Finally, some useful actions that mr knows about by default. # These can be overridden in ~/.mrconfig. -#DATA{{{ __DATA__ [ALIAS] co = checkout @@ -1211,7 +1324,7 @@ git_bare_test = svn_update = svn update "$@" git_update = git pull "$@" -bzr_update = bzr merge "$@" +bzr_update = bzr merge --pull "$@" cvs_update = cvs update "$@" hg_update = hg pull "$@" && hg update "$@" darcs_update = darcs pull -a "$@" @@ -1235,6 +1348,13 @@ bzr_record = bzr commit "$@" hg_record = hg commit -m "$@" darcs_record = darcs record -a -m "$@" +svn_push = : +git_push = git push "$@" +bzr_push = bzr push "$@" +cvs_push = : +hg_push = hg push "$@" +darcs_push = darcs push -a "$@" + svn_diff = svn diff "$@" git_diff = git diff "$@" bzr_diff = bzr diff "$@" @@ -1305,10 +1425,35 @@ help = man -l "$tmp" || error "man failed" 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