#!/usr/bin/perl
+#man{{{
+
=head1 NAME
mr - a Multiple Repository management tool
B<mr> [options] status
-B<mr> [options] commit -m "message"
+B<mr> [options] commit [-m "message"]
+
+B<mr> [options] diff
+
+B<mr> [options] log
+
+B<mr> [options] register [repository]
+
+B<mr> [options] config section [parameter=[value] ...]
B<mr> [options] action [params ...]
=head1 DESCRIPTION
-B<mr> is a Multiple Repository management tool. It allows you to register a
-set of repositories in a .mrconfig file, and then checkout, update, or
-perform other actions on all of the repositories at once.
+B<mr> 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 respository. It
+supports any combination of subversion, git, cvs, and bzr repositories,
+and support for other revision 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,
+
+These predefined commands should be fairly familiar to users of any revision
+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.
+
+=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.)
+
+The optional -m parameter allows specifying a commit message.
+
+=item diff
+
+Show a diff of uncommitted changes.
+
+=item log
+
+Show the commit log.
+
+=back
+
+These commands are also available:
+
+=over 4
+
+=item list (or ls)
+
+List the repositories that mr will act on.
+
+=item register
+
+Register an existing repository in the mrconfig file. By default, the
+repository in the current directory is registered, or you can specify a
+directory to register.
+
+By default it registers it to the ~/.mrconfig file. To make it write to a
+different file, use the -c option.
+
+=item config
-Any mix of revision control systems can be used with B<mr>, and you can
-define arbitrary actions like "update", "checkout", or "commit".
+Adds, modifies, removes, or prints a value from the 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 "parameter=value". Use "parameter=" to
+remove a parameter. Use just "parameter" to get the value of a parameter.
+
+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
+
+=item help
+
+Displays this help.
+
+=back
+
+Actions can be abbreviated to any unambiguous subsctring, so
+"mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
+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
+system.
=head1 OPTIONS
=item -d directory
Specifies the topmost directory that B<mr> should work in. The default is
-the current working directory. B<mr> will operate on all registered
-repositories at or under the directory.
+the current working directory.
=item -c mrconfig
-Use the specified mrconfig file, instead of looking for on in your home
+Use the specified mrconfig file, instead of looking for one in your home
directory.
=item -v
Be verbose.
+=item -s
+
+Expand the statistics line displayed at the end to include information
+about exactly which repositories failed and were skipped, if any.
+
=back
=head1 FILES
-B<mr> is configured by .mrconfig files. It searches for .mrconfig files in
-your home directory, and in the root directory of each repository specified
-in a .mrconfig file. So you could have a ~/.mrconfig that registers a
-repository ~/src, that itself contains a ~/src/.mrconfig file, that in turn
-registers several additional repositories.
+B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
+file in your home directory, and this can in turn chain load .mrconfig files
+from repositories.
+
+Here is an example .mrconfig file:
+
+ [src]
+ checkout = svn co 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 .mrconfig file uses a variant of the INI file format. Lines starting with
-"#" are comments. Lines ending with "\" are continued on to the next line.
-Sections specify where each repository is located, relative to the
-directory that contains the .mrconfig file.
+"#" are comments. Values can be continued to the following line by
+indenting the line with whitespace.
+
+The "DEFAULT" section allows setting default values for the sections that
+come after it.
+
+The "ALIAS" section allows adding aliases for actions. Each parameter
+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.
Within a section, each parameter defines a shell command to run to handle a
-given action. Note that these shell commands are run in a "set -e" shell
-environment, and B<mr> cds into the repository directory before running
-them, except for the "checkout" command, which is run in the parent of the
-repository directory, since the repository isn't checked out yet.
+given action. mr contains default handlers for the "update", "status", and
+"commit" actions, so normally you only need to specify what to do for
+"checkout".
-There are two special parameters. If the "skip" parameter is set and
-its command returns nonzero, then B<mr> will skip acting on that repository.
+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.
+The "MR_REPO" environment variable is set to the path to the top of the
+repository, and "MR_CONFIG" is set to the topmost .mrconfig file used.
-The "default" section allows setting up default handlers for each action,
-and is overridden by the contents of other sections. mr contains default
-handlers for the "update", "status", and "commit" actions, so normally
-you only need to specify what to do for "checkout".
+A few parameters have special meanings:
-For example:
+=over 4
- [src]
- checkout = svn co svn://svn.example.com/src/trunk src
+=item skip
- [src/linux-2.6]
- # only check this out on kodama
- skip = test $(hostname) != kodama
- checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
+If the "skip" parameter is set and its command returns nonzero, then B<mr>
+will skip acting on that repository. The command is passed the action
+name in $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.
+
+ skip = test $(whoami) != joey
+ skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
+
+=item chain
+
+If the "chain" parameter is set and its command returns nonzero, then B<mr>
+will try to load a .mrconfig file from the root of the repository. (You
+should avoid chaining from repositories with untrusted committers.)
+
+=item deleted
+
+If the "deleted" parameter is set and its command returns nonzero, 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 amoung multiple machines,
+to keep track of and remember to delete old repositories.
+
+=item lib
+
+The "lib" parameter can specify some shell code that will be run before each
+command, this can be a useful way to define shell functions for other commands
+to use.
+
+=back
=head1 AUTHOR
=cut
+#}}}
+
use warnings;
use strict;
use Getopt::Long;
use Cwd qw(getcwd abs_path);
+$SIG{INT}=sub {
+ print STDERR "mr: interrupted\n";
+ exit 2;
+};
+
+$ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
my $directory=getcwd();
-my $config="$ENV{HOME}/.mrconfig";
my $verbose=0;
+my $stats=0;
my %config;
+my %knownactions;
+my %alias;
Getopt::Long::Configure("no_permute");
my $result=GetOptions(
- "d=s" => sub { $directory=abs_path($_[1]) },
- "c=s" => \$config,
- "v" => \$verbose,
+ "d|directory=s" => sub { $directory=abs_path($_[1]) },
+ "c|config=s" => sub { $ENV{MR_CONFIG}=abs_path($_[1]) },
+ "v|verbose" => \$verbose,
+ "s|stats" => \$stats,
);
if (! $result || @ARGV < 1) {
- die("Usage: mr [-d directory] action [params ...]\n");
+ die("Usage: mr [-d directory] action [params ...]\n".
+ "(Use mr help for man page.)\n");
+
}
-my $action=shift @ARGV;
loadconfig(\*DATA);
-loadconfig($config);
+loadconfig($ENV{MR_CONFIG});
#use Data::Dumper;
#print Dumper(\%config);
-my (@failures, @successes, @skipped);
-my $first=1;
+eval {
+ use FindBin qw($Bin $Script);
+ $ENV{MR_PATH}=$Bin."/".$Script;
+};
+
+# alias expansion and command stemming
+my $action=shift @ARGV;
+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";
+ }
+}
+
+if ($action eq 'help') {
+ exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
+}
+elsif ($action eq 'config') {
+ if (@ARGV < 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}=~/^(.*\/)[^\/]+$/;
+ if ($section=~/^\Q$dir\E(.*)/) {
+ $section=$1;
+ }
+ }
+ my %changefields;
+ foreach (@ARGV) {
+ 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;
+ }
+ }
+ if (! $found) {
+ die "mr $action: $section $_ not set\n";
+ }
+ }
+ }
+ modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
+ exit 0;
+}
+elsif ($action eq 'register') {
+ my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
+ "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
+ join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
+ print STDERR "mr $action: running >>$command<<\n" if $verbose;
+ exec($command) || die "exec: $!";
+}
+
+# work out what repos to act on
+my @repos;
+my $nochdir=0;
foreach my $topdir (sort keys %config) {
foreach my $subdir (sort keys %{$config{$topdir}}) {
- next if $subdir eq 'default';
-
- my $dir=$topdir.$subdir;
+ next if $subdir eq 'DEFAULT';
+ my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
+ my $d=$directory;
+ $dir.="/" unless $dir=~/\/$/;
+ $d.="/" unless $d=~/\/$/;
+ next if $dir ne $directory && $dir !~ /^\Q$directory\E/;
+ push @repos, [$dir, $topdir, $subdir];
+ }
+}
+if (! @repos) {
+ # fallback to find a leaf repo
+ LEAF: foreach my $topdir (reverse sort keys %config) {
+ foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
+ next if $subdir eq 'DEFAULT';
+ my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
+ my $d=$directory;
+ $dir.="/" unless $dir=~/\/$/;
+ $d.="/" unless $d=~/\/$/;
+ if ($d=~/^\Q$dir\E/) {
+ push @repos, [$dir, $topdir, $subdir];
+ last LEAF;
+ }
+ }
+ }
+ $nochdir=1;
+}
+
+my (@failed, @ok, @skipped);
+foreach my $repo (@repos) {
+ action($action, @$repo);
+}
+
+sub action { #{{{
+ my ($action, $dir, $topdir, $subdir) = @_;
+
+ my $lib= exists $config{$topdir}{$subdir}{lib} ?
+ $config{$topdir}{$subdir}{lib}."\n" : "";
- if (defined $directory &&
- $dir !~ /^\Q$directory\E\//) {
- print "mr $action: $dir skipped per -d parameter ($directory)\n" if $verbose;
+ if (exists $config{$topdir}{$subdir}{deleted}) {
+ if (! -d $dir) {
+ return;
+ }
+ else {
+ my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
+ print "mr $action: running deleted test >>$test<<\n" if $verbose;
+ my $ret=system($test);
+ if ($ret != 0) {
+ if (($? & 127) == 2) {
+ print STDERR "mr $action: interrupted\n";
+ exit 2;
+ }
+ elsif ($? & 127) {
+ print STDERR "mr $action: deleted test received signal ".($? & 127)."\n";
+ }
+ if ($ret >> 8 == 0) {
+ print STDERR "mr error: $dir should be deleted yet still exists\n\n";
+ push @failed, $dir;
+ return;
+ }
+ }
+ }
+ }
+
+ if ($action eq 'checkout') {
+ if (-d $dir) {
+ print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
push @skipped, $dir;
- next;
+ return;
}
- print "\n" unless $first;
- $first=0;
+ $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
- if (exists $config{$topdir}{$subdir}{skip}) {
- my $ret=system($config{$topdir}{$subdir}{skip});
+ if (! -d $dir) {
+ print "mr $action: creating parent directory $dir\n" if $verbose;
+ my $ret=system("mkdir", "-p", $dir);
+ }
+ }
+ elsif ($action eq 'update') {
+ if (! -d $dir) {
+ return action("checkout", $dir, $topdir, $subdir);
+ }
+ }
+
+ $ENV{MR_REPO}=$dir;
+
+ if (exists $config{$topdir}{$subdir}{skip}) {
+ my $test="set -e;".$lib.
+ "my_action(){ $config{$topdir}{$subdir}{skip}\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";
+ exit 2;
+ }
+ elsif ($? & 127) {
+ print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
+ exit 1;
+ }
if ($ret >> 8 == 0) {
print "mr $action: $dir skipped per config file\n" if $verbose;
push @skipped, $dir;
- next;
+ return;
}
}
-
- if ($action eq 'checkout') {
- if (-e $dir) {
- print "mr $action: $dir already exists, skipping checkout\n";
- push @skipped, $dir;
- next;
- }
- $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
+ }
+
+ if (! $nochdir && ! chdir($dir)) {
+ print STDERR "mr $action: failed to chdir to $dir: $!\n";
+ push @failed, $dir;
+ }
+ elsif (! exists $config{$topdir}{$subdir}{$action}) {
+ print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
+ push @skipped, $dir;
+ }
+ else {
+ if (! $nochdir) {
+ print "mr $action: $topdir$subdir\n";
}
- if (! chdir($dir)) {
- print STDERR "mr $action: failed to chdir to $dir: $!\n";
- push @skipped, $dir;
+ else {
+ print "mr $action: $topdir$subdir (in subdir $directory)\n";
}
- elsif (! exists $config{$topdir}{$subdir}{$action}) {
- print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
- push @skipped, $dir;
+ my $command="set -e; ".$lib.
+ "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
+ join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
+ print STDERR "mr $action: running >>$command<<\n" if $verbose;
+ my $ret=system($command);
+ if ($ret != 0) {
+ if (($? & 127) == 2) {
+ print STDERR "mr $action: interrupted\n";
+ exit 2;
+ }
+ elsif ($? & 127) {
+ print STDERR "mr $action: received signal ".($? & 127)."\n";
+ }
+ print STDERR "mr $action: failed ($ret)\n" if $verbose;
+ push @failed, $dir;
+ if ($ret >> 8 != 0) {
+ print STDERR "mr $action: command failed\n";
+ }
+ elsif ($ret != 0) {
+ print STDERR "mr $action: command died ($ret)\n";
+ }
}
else {
- print "mr $action: in $dir\n";
- my $command="set -e; my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action @ARGV";
- my $ret=system($command);
- if ($ret != 0) {
- print STDERR "mr $action: failed to run: $command\n" if $verbose;
- push @failures, $topdir.$subdir;
- if ($ret >> 8 != 0) {
- print STDERR "mr $action: command failed\n";
- }
- elsif ($ret != 0) {
- print STDERR "mr $action: command died ($ret)\n";
- }
- }
- else {
- push @successes, $dir;
+ if ($action eq 'checkout' && ! -d $dir) {
+ print STDERR "mr $action: $dir missing after checkout\n";;
+ push @failed, $dir;
+ return;
}
+
+ push @ok, $dir;
}
+
+ print "\n";
}
-}
+} #}}}
-sub showstat {
+sub showstat { #{{{
my $count=shift;
my $singular=shift;
my $plural=shift;
return "$count ".($count > 1 ? $plural : $singular);
}
return;
+} #}}}
+if (! @ok && ! @failed && ! @skipped) {
+ die "mr $action: no repositories found to work on\n";
}
-print "\nmr $action: finished (".join("; ",
- showstat($#successes+1, "success", "sucesses"),
- showstat($#failures+1, "failure", "failures"),
+print "mr $action: finished (".join("; ",
+ showstat($#ok+1, "ok", "ok"),
+ showstat($#failed+1, "failed", "failed"),
showstat($#skipped+1, "skipped", "skipped"),
).")\n";
-exit @failures ? 1 : 0;
+if ($stats) {
+ if (@skipped) {
+ print "mr $action: (skipped: ".join(" ", @skipped).")\n";
+ }
+ if (@failed) {
+ print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
+ }
+}
+if (@failed) {
+ exit 1;
+}
+elsif (! @ok && @skipped) {
+ exit 1;
+}
+exit 0;
my %loaded;
-sub loadconfig {
+sub loadconfig { #{{{
my $f=shift;
my @toload;
my $in;
my $dir;
if (ref $f eq 'GLOB') {
- $in=$f;
$dir="";
+ $in=$f;
}
else {
- # $f might be a symlink
+ if (! -e $f) {
+ return;
+ }
+
my $absf=abs_path($f);
if ($loaded{$absf}) {
return;
}
$loaded{$absf}=1;
- print "mr: loading config $f\n" if $verbose;
- open($in, "<", $f) || die "mr: open $f: $!\n";
($dir)=$f=~/^(.*\/)[^\/]+$/;
+ if (! defined $dir) {
+ $dir=".";
+ }
$dir=abs_path($dir)."/";
# copy in defaults from first parent
my $parent=$dir;
- while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
+ while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
+ if ($parent eq '/') {
+ $parent="";
+ }
if (exists $config{$parent} &&
- exists $config{$parent}{default}) {
- $config{$dir}{default}={ %{$config{$parent}{default}} };
+ exists $config{$parent}{DEFAULT}) {
+ $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
last;
}
}
+
+ print "mr: loading config $f\n" if $verbose;
+ open($in, "<", $f) || die "mr: open $f: $!\n";
}
+ my @lines=<$in>;
+ close $in;
my $section;
- while (<$in>) {
+ my $line=0;
+ while (@lines) {
+ $_=shift @lines;
+ $line++;
chomp;
next if /^\s*\#/ || /^\s*$/;
- if (/^\s*\[([^\]]*)\]\s*$/) {
+ if (/^\[([^\]]*)\]\s*$/) {
$section=$1;
- if (length $dir && $section ne "default" &&
- -e $dir.$section."/.mrconfig") {
- push @toload, $dir.$section."/.mrconfig";
- }
}
- elsif (/^\s*(\w+)\s*=\s*(.*)/) {
+ elsif (/^(\w+)\s*=\s*(.*)/) {
my $parameter=$1;
my $value=$2;
- # continuation line
- while ($value=~/(.*)\\$/) {
- $value=$1.<$in>;
+ # continued value
+ while (@lines && $lines[0]=~/^\s(.+)/) {
+ shift(@lines);
+ $line++;
+ $value.="\n$1";
chomp $value;
}
if (! defined $section) {
die "$f line $.: parameter ($parameter) not in section\n";
}
- if (! exists $config{$dir}{$section} &&
- exists $config{$dir}{default}) {
+ if ($section ne 'ALIAS' &&
+ ! exists $config{$dir}{$section} &&
+ exists $config{$dir}{DEFAULT}) {
# copy in defaults
- $config{$dir}{$section}={ %{$config{$dir}{default}} };
+ $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
+ }
+ if ($section eq 'ALIAS') {
+ $alias{$parameter}=$value;
+ }
+ elsif ($parameter eq 'lib') {
+ $config{$dir}{$section}{lib}.=$value."\n";
+ }
+ else {
+ $config{$dir}{$section}{$parameter}=$value;
+ $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 $action: chain test interrupted\n";
+ exit 2;
+ }
+ elsif ($? & 127) {
+ print STDERR "mr $action: chain test received signal ".($? & 127)."\n";
+ }
+ }
+ else {
+ push @toload, $dir.$section."/.mrconfig";
+ }
+ }
}
- $config{$dir}{$section}{$parameter}=$value;
}
else {
- die "$f line $.: parse error\n";
+ die "$f line $line: parse error\n";
}
}
- close $in;
foreach (@toload) {
loadconfig($_);
}
-}
+} #}}}
-__DATA__
-# Some useful actions that mr knows about by default.
+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=$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;
+} #}}}
+
+# Finally, some useful actions that mr knows about by default.
# These can be overridden in ~/.mrconfig.
-[default]
-update = \
- if [ -d .svn ]; then \
- svn update; \
- elif [ -d .git ]; then \
- git pull origin master; \
- else \
- echo "mr update: unknown RCS"; \
- exit 1; \
+#DATA{{{
+__DATA__
+[ALIAS]
+co = checkout
+ci = commit
+ls = list
+
+[DEFAULT]
+lib =
+ error() {
+ echo "mr: $@" >&2
+ exit 1
+ }
+ hours_since() {
+ for dir in .git .svn .bzr CVS; 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
+ perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
+ touch "$flagfile"
+ }
+
+update =
+ if [ -d "$MR_REPO"/.svn ]; then
+ svn update "$@"
+ elif [ -d "$MR_REPO"/.git ]; then
+ git pull origin master "$@"
+ elif [ -d "$MR_REPO"/.bzr ]; then
+ bzr merge "$@"
+ elif [ -d "$MR_REPO"/CVS ]; then
+ cvs update "$@"
+ else
+ error "unknown repo type"
+ fi
+status =
+ if [ -d "$MR_REPO"/.svn ]; then
+ svn status "$@"
+ elif [ -d "$MR_REPO"/.git ]; then
+ git status "$@" || true
+ elif [ -d "$MR_REPO"/.bzr ]; then
+ bzr status "$@"
+ elif [ -d "$MR_REPO"/CVS ]; then
+ cvs status "$@"
+ else
+ error "unknown repo type"
+ fi
+commit =
+ if [ -d "$MR_REPO"/.svn ]; then
+ svn commit "$@"
+ elif [ -d "$MR_REPO"/.git ]; then
+ git commit -a "$@" && git push --all
+ elif [ -d "$MR_REPO"/.bzr ]; then
+ bzr commit "$@" && bzr push
+ elif [ -d "$MR_REPO"/CVS ]; then
+ cvs commit "$@"
+ else
+ error "unknown repo type"
+ fi
+diff =
+ if [ -d "$MR_REPO"/.svn ]; then
+ svn diff "$@"
+ elif [ -d "$MR_REPO"/.git ]; then
+ git diff "$@"
+ elif [ -d "$MR_REPO"/.bzr ]; then
+ bzr diff "$@"
+ elif [ -d "$MR_REPO"/CVS ]; then
+ cvs diff "$@"
+ else
+ error "unknown repo type"
+ fi
+log =
+ if [ -d "$MR_REPO"/.svn ]; then
+ svn log"$@"
+ elif [ -d "$MR_REPO"/.git ]; then
+ git log "$@"
+ elif [ -d "$MR_REPO"/.bzr ]; then
+ bzr log "$@"
+ elif [ -d "$MR_REPO"/CVS ]; then
+ cvs log "$@"
+ else
+ error "unknown repo type"
+ fi
+register =
+ if [ -n "$1" ]; then
+ cd "$1"
fi
-status = \
- if [ -d .svn ]; then \
- svn status; \
- elif [ -d .git ]; then \
- git status || true; \
- else \
- echo "mr status: unknown RCS"; \
- exit 1; \
+ basedir="$(basename $(pwd))"
+ if [ -d .svn ]; then
+ url=$(LANG=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"
+ mr -c "$MR_CONFIG" config "$(pwd)" checkout="svn co $url $basedir"
+ elif [ -d .git ]; then
+ url=$(LANG=C git-config --get remote.origin.url)
+ if [ -z "$url" ]; then
+ error "cannot determine git url"
+ fi
+ echo "Registering git url: $url"
+ mr -c "$MR_CONFIG" config "$(pwd)" checkout="git clone $url $basedir"
+ elif [ -d .bzr ]; then
+ url=$(cat .bzr/branch/parent)
+ if [ -z "$url" ]; then
+ error "cannot determine bzr url"
+ fi
+ echo "Registering bzr url: $url"
+ mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
+ else
+ error "unable to register this repo type"
fi
-commit = \
- if [ -d .svn ]; then \
- svn commit "$@"; \
- elif [ -d .git ]; then \
- git commit -a "$@"; \
- else \
- echo "mr commit: unknown RCS"; \
- exit 1; \
+help =
+ if [ ! -e "$MR_PATH" ]; then
+ error "cannot find program path"
fi
+ (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
+list = true
+config =
+
+ed = echo "A horse is a horse, of course, of course.."
+T = echo "I pity the fool."
+right = echo "Not found."
+#}}}