]> git.madduck.net Git - code/myrepos.git/blob - mr

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

add an example of how to run git w/o getting random crap on stderr
[code/myrepos.git] / mr
1 #!/usr/bin/perl
2
3 #man{{{
4
5 =head1 NAME
6
7 mr - a Multiple Repository management tool
8
9 =head1 SYNOPSIS
10
11 B<mr> [options] checkout
12
13 B<mr> [options] update
14
15 B<mr> [options] status
16
17 B<mr> [options] commit [-m "message"]
18
19 B<mr> [options] diff
20
21 B<mr> [options] log
22
23 B<mr> [options] register [repository]
24
25 B<mr> [options] config section ["parameter=[value]" ...]
26
27 B<mr> [options] action [params ...]
28
29 =head1 DESCRIPTION
30
31 B<mr> is a Multiple Repository management tool. It can checkout, update, or
32 perform other actions on a set of repositories as if they were one combined
33 respository. It supports any combination of subversion, git, cvs, mecurial and
34 bzr repositories, and support for other revision control systems can easily be
35 added.
36
37 B<mr> cds into and operates on all registered repositories at or below your
38 working directory. Or, if you are in a subdirectory of a repository that
39 contains no other registered repositories, it will stay in that directory,
40 and work on only that repository,
41
42 These predefined commands should be fairly familiar to users of any revision
43 control system:
44
45 =over 4
46
47 =item checkout (or co)
48
49 Checks out any repositories that are not already checked out.
50
51 =item update
52
53 Updates each repository from its configured remote repository.
54
55 If a repository isn't checked out yet, it will first check it out.
56
57 =item status
58
59 Displays a status report for each repository, showing what
60 uncommitted changes are present in the repository.
61
62 =item commit (or ci)
63
64 Commits changes to each repository. (By default, changes are pushed to the
65 remote repository too, when using distributed systems like git.)
66
67 The optional -m parameter allows specifying a commit message.
68
69 =item diff
70
71 Show a diff of uncommitted changes.
72
73 =item log
74
75 Show the commit log.
76
77 =back
78
79 These commands are also available:
80
81 =over 4
82
83 =item list (or ls)
84
85 List the repositories that mr will act on.
86
87 =item register
88
89 Register an existing repository in a mrconfig file. By default, the
90 repository in the current directory is registered, or you can specify a
91 directory to register.
92
93 The mrconfig file that is modified is chosen by either the -c option, or by
94 looking for the closest known one at or below the current directory.
95
96 =item config
97
98 Adds, modifies, removes, or prints a value from a mrconfig file. The next
99 parameter is the name of the section the value is in. To add or modify
100 values, use one or more instances of "parameter=value". Use "parameter=" to
101 remove a parameter. Use just "parameter" to get the value of a parameter.
102
103 For example, to add (or edit) a repository in src/foo:
104
105   mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
106
107 To show the command that mr uses to update the repository in src/foo:
108
109   mr config src/foo update
110
111 To see the built-in library of shell functions contained in mr:
112
113   mr config DEFAULT lib
114
115 The ~/.mrconfig file is used by default. To use a different config file,
116 use the -c option.
117
118 =item help
119
120 Displays this help.
121
122 =back
123
124 Actions can be abbreviated to any unambiguous subsctring, so
125 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
126 update"
127
128 Additional parameters can be passed to most commands, and are passed on
129 unchanged to the underlying revision control system. This is mostly useful
130 if the repositories mr will act on all use the same revision control
131 system.
132
133 =head1 OPTIONS
134
135 =over 4
136
137 =item -d directory
138
139 Specifies the topmost directory that B<mr> should work in. The default is
140 the current working directory.
141
142 =item -c mrconfig
143
144 Use the specified mrconfig file. The default is B<~/.mrconfig>
145
146 =item -v
147
148 Be verbose.
149
150 =item -s
151
152 Expand the statistics line displayed at the end to include information
153 about exactly which repositories failed and were skipped, if any.
154
155 =item -n
156
157 Just operate on the repository for the current directory, do not 
158 recurse into deeper repositories.
159
160 =back
161
162 =head1 FILES
163
164 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
165 file in your home directory, and this can in turn chain load .mrconfig files
166 from repositories.
167
168 Here is an example .mrconfig file:
169
170   [src]
171   checkout = svn co svn://svn.example.com/src/trunk src
172   chain = true
173
174   [src/linux-2.6]
175   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
176         cd linux-2.6 &&
177         git checkout -b mybranch origin/master
178
179 The .mrconfig file uses a variant of the INI file format. Lines starting with
180 "#" are comments. Values can be continued to the following line by
181 indenting the line with whitespace.
182
183 The "DEFAULT" section allows setting default values for the sections that
184 come after it.
185
186 The "ALIAS" section allows adding aliases for actions. Each parameter
187 is an alias, and its value is the action to use.
188
189 All other sections add repositories. The section header specifies the
190 directory where the repository is located. This is relative to the directory
191 that contains the mrconfig file, but you can also choose to use absolute
192 paths.
193
194 Within a section, each parameter defines a shell command to run to handle a
195 given action. mr contains default handlers for "update", "status",
196 "commit", and other standard actions. Normally you only need to specify what
197 to do for "checkout".
198
199 Note that these shell commands are run in a "set -e" shell
200 environment, where any additional parameters you pass are available in
201 "$@". The "checkout" command is run in the parent of the repository
202 directory, since the repository isn't checked out yet. All other commands
203 are run inside the repository, though not necessarily at the top of it.
204
205 The "MR_REPO" environment variable is set to the path to the top of the
206 repository. The "MR_CONFIG" environment variable is set to the .mrconfig file
207 that defines the repo being acted on, or, if the repo is not yet in a config
208 file, the .mrconfig file that should be modified to register the repo.
209
210 A few parameters have special meanings:
211
212 =over 4
213
214 =item skip
215
216 If the "skip" parameter is set and its command returns true, then B<mr>
217 will skip acting on that repository. The command is passed the action
218 name in $1.
219
220 Here are two examples. The first skips the repo unless
221 mr is run by joey. The second uses the hours_since function
222 (included in mr's built-in library) to skip updating the repo unless it's
223 been at least 12 hours since the last update.
224
225   skip = test $(whoami) != joey
226   skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
227
228 =item chain
229
230 If the "chain" parameter is set and its command returns true, then B<mr>
231 will try to load a .mrconfig file from the root of the repository. (You
232 should avoid chaining from repositories with untrusted committers.)
233
234 =item lib
235
236 The "lib" parameter can specify some shell code that will be run before each
237 command, this can be a useful way to define shell functions for other commands
238 to use.
239
240 =back
241
242 =head1 AUTHOR
243
244 Copyright 2007 Joey Hess <joey@kitenet.net>
245
246 Licensed under the GNU GPL version 2 or higher.
247
248 http://kitenet.net/~joey/code/mr/
249
250 =cut
251
252 #}}}
253
254 use warnings;
255 use strict;
256 use Getopt::Long;
257 use Cwd qw(getcwd abs_path);
258
259 $SIG{INT}=sub {
260         print STDERR "mr: interrupted\n";
261         exit 2;
262 };
263
264 $ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
265 my $config_overridden=0;
266 my $directory=getcwd();
267 my $verbose=0;
268 my $stats=0;
269 my $no_recurse=0;
270 my %config;
271 my %configfiles;
272 my %knownactions;
273 my %alias;
274
275 Getopt::Long::Configure("no_permute");
276 my $result=GetOptions(
277         "d|directory=s" => sub { $directory=abs_path($_[1]) },
278         "c|config=s" => sub { $ENV{MR_CONFIG}=$_[1]; $config_overridden=1 },
279         "v|verbose" => \$verbose,
280         "s|stats" => \$stats,
281         "n|no-recurse" => \$no_recurse,
282 );
283 if (! $result || @ARGV < 1) {
284         die("Usage: mr [-d directory] action [params ...]\n".
285             "(Use mr help for man page.)\n");
286
287 }
288
289 # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
290 # the config file might be a symlink to elsewhere, and the directory it's
291 # in is significant.
292 if ($ENV{MR_CONFIG} !~ /^\//) {
293         $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
294 }
295
296 loadconfig(\*DATA);
297 loadconfig($ENV{MR_CONFIG});
298 #use Data::Dumper;
299 #print Dumper(\%config);
300
301 eval {
302         use FindBin qw($Bin $Script);
303         $ENV{MR_PATH}=$Bin."/".$Script;
304 };
305
306 # alias expansion and command stemming
307 my $action=shift @ARGV;
308 if (exists $alias{$action}) {
309         $action=$alias{$action};
310 }
311 if (! exists $knownactions{$action}) {
312         my @matches = grep { /^\Q$action\E/ }
313                 keys %knownactions, keys %alias;
314         if (@matches == 1) {
315                 $action=$matches[0];
316         }
317         elsif (@matches == 0) {
318                 die "mr: unknown action \"$action\" (known actions: ".
319                         join(", ", sort keys %knownactions).")\n";
320         }
321         else {
322                 die "mr: ambiguous action \"$action\" (matches: ".
323                         join(", ", @matches).")\n";
324         }
325 }
326
327 # commands that do not operate on all repos
328 if ($action eq 'help') {
329         exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
330 }
331 elsif ($action eq 'config') {
332         if (@ARGV < 2) {
333                 die "mr config: not enough parameters\n";
334         }
335         my $section=shift;
336         if ($section=~/^\//) {
337                 # try to convert to a path relative to the config file
338                 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
339                 $dir=abs_path($dir);
340                 $dir.="/" unless $dir=~/\/$/;
341                 if ($section=~/^\Q$dir\E(.*)/) {
342                         $section=$1;
343                 }
344         }
345         my %changefields;
346         foreach (@ARGV) {
347                 if (/^([^=]+)=(.*)$/) {
348                         $changefields{$1}=$2;
349                 }
350                 else {
351                         my $found=0;
352                         foreach my $topdir (sort keys %config) {
353                                 if (exists $config{$topdir}{$section} &&
354                                     exists $config{$topdir}{$section}{$_}) {
355                                         print $config{$topdir}{$section}{$_}."\n";
356                                         $found=1;
357                                         last if $section eq 'DEFAULT';
358                                 }
359                         }
360                         if (! $found) {
361                                 die "mr $action: $section $_ not set\n";
362                         }
363                 }
364         }
365         modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
366         exit 0;
367 }
368 elsif ($action eq 'register') {
369         if (! $config_overridden) {
370                 # Find the closest known mrconfig file to the current
371                 # directory.
372                 $directory.="/" unless $directory=~/\/$/;
373                 foreach my $topdir (reverse sort keys %config) {
374                         next unless length $topdir;
375                         if ($directory=~/^\Q$topdir\E/) {
376                                 $ENV{MR_CONFIG}=$configfiles{$topdir};
377                                 last;
378                         }
379                 }
380         }
381         my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
382                 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
383                 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
384         print STDERR "mr $action: running >>$command<<\n" if $verbose;
385         exec($command) || die "exec: $!";
386 }
387
388 # work out what repos to act on
389 my @repos;
390 my $nochdir=0;
391 foreach my $topdir (sort keys %config) {
392         foreach my $subdir (sort keys %{$config{$topdir}}) {
393                 next if $subdir eq 'DEFAULT';
394                 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
395                 my $d=$directory;
396                 $dir.="/" unless $dir=~/\/$/;
397                 $d.="/" unless $d=~/\/$/;
398                 next if $no_recurse && $d ne $dir;
399                 next if $dir ne $d && $dir !~ /^\Q$d\E/;
400                 push @repos, [$dir, $topdir, $subdir];
401         }
402 }
403 if (! @repos) {
404         # fallback to find a leaf repo
405         LEAF: foreach my $topdir (reverse sort keys %config) {
406                 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
407                         next if $subdir eq 'DEFAULT';
408                         my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
409                         my $d=$directory;
410                         $dir.="/" unless $dir=~/\/$/;
411                         $d.="/" unless $d=~/\/$/;
412                         if ($d=~/^\Q$dir\E/) {
413                                 push @repos, [$dir, $topdir, $subdir];
414                                 last LEAF;
415                         }
416                 }
417         }
418         $nochdir=1;
419 }
420
421 my (@failed, @ok, @skipped);
422 foreach my $repo (@repos) {
423         action($action, @$repo);
424 }
425
426 sub action { #{{{
427         my ($action, $dir, $topdir, $subdir) = @_;
428
429         $ENV{MR_CONFIG}=$configfiles{$topdir};
430         my $lib=exists $config{$topdir}{$subdir}{lib} ?
431                        $config{$topdir}{$subdir}{lib}."\n" : "";
432
433         if ($action eq 'checkout') {
434                 if (-d $dir) {
435                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
436                         push @skipped, $dir;
437                         return;
438                 }
439
440                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
441
442                 if (! -d $dir) {
443                         print "mr $action: creating parent directory $dir\n" if $verbose;
444                         my $ret=system("mkdir", "-p", $dir);
445                 }
446         }
447         elsif ($action eq 'update') {
448                 if (! -d $dir) {
449                         return action("checkout", $dir, $topdir, $subdir);
450                 }
451         }
452         
453         $ENV{MR_REPO}=$dir;
454
455         if (exists $config{$topdir}{$subdir}{skip}) {
456                 my $test="set -e;".$lib.
457                         "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
458                 print "mr $action: running skip test >>$test<<\n" if $verbose;
459                 my $ret=system($test);
460                 if ($ret != 0) {
461                         if (($? & 127) == 2) {
462                                 print STDERR "mr $action: interrupted\n";
463                                 exit 2;
464                         }
465                         elsif ($? & 127) {
466                                 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
467                                 exit 1;
468                         }
469                 }
470                 if ($ret >> 8 == 0) {
471                         print "mr $action: $dir skipped per config file\n" if $verbose;
472                         push @skipped, $dir;
473                         return;
474                 }
475         }
476         
477         if (! $nochdir && ! chdir($dir)) {
478                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
479                 push @failed, $dir;
480         }
481         elsif (! exists $config{$topdir}{$subdir}{$action}) {
482                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
483                 push @skipped, $dir;
484         }
485         else {
486                 if (! $nochdir) {
487                         print "mr $action: $topdir$subdir\n";
488                 }
489                 else {
490                         print "mr $action: $topdir$subdir (in subdir $directory)\n";
491                 }
492                 my $command="set -e; ".$lib.
493                         "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
494                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
495                 print STDERR "mr $action: running >>$command<<\n" if $verbose;
496                 my $ret=system($command);
497                 if ($ret != 0) {
498                         if (($? & 127) == 2) {
499                                 print STDERR "mr $action: interrupted\n";
500                                 exit 2;
501                         }
502                         elsif ($? & 127) {
503                                 print STDERR "mr $action: received signal ".($? & 127)."\n";
504                         }
505                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
506                         push @failed, $dir;
507                         if ($ret >> 8 != 0) {
508                                 print STDERR "mr $action: command failed\n";
509                         }
510                         elsif ($ret != 0) {
511                                 print STDERR "mr $action: command died ($ret)\n";
512                         }
513                 }
514                 else {
515                         if ($action eq 'checkout' && ! -d $dir) {
516                                 print STDERR "mr $action: $dir missing after checkout\n";;
517                                 push @failed, $dir;
518                                 return;
519                         }
520
521                         push @ok, $dir;
522                 }
523
524                 print "\n";
525         }
526 } #}}}
527
528 sub showstat { #{{{
529         my $count=shift;
530         my $singular=shift;
531         my $plural=shift;
532         if ($count) {
533                 return "$count ".($count > 1 ? $plural : $singular);
534         }
535         return;
536 } #}}}
537 if (! @ok && ! @failed && ! @skipped) {
538         die "mr $action: no repositories found to work on\n";
539 }
540 print "mr $action: finished (".join("; ",
541         showstat($#ok+1, "ok", "ok"),
542         showstat($#failed+1, "failed", "failed"),
543         showstat($#skipped+1, "skipped", "skipped"),
544 ).")\n";
545 if ($stats) {
546         if (@skipped) {
547                 print "mr $action: (skipped: ".join(" ", @skipped).")\n";
548         }
549         if (@failed) {
550                 print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
551         }
552 }
553 if (@failed) {
554         exit 1;
555 }
556 elsif (! @ok && @skipped) {
557         exit 1;
558 }
559 exit 0;
560
561 my %loaded;
562 sub loadconfig { #{{{
563         my $f=shift;
564
565         my @toload;
566
567         my $in;
568         my $dir;
569         if (ref $f eq 'GLOB') {
570                 $dir="";
571                 $in=$f; 
572         }
573         else {
574                 if (! -e $f) {
575                         return;
576                 }
577
578                 my $absf=abs_path($f);
579                 if ($loaded{$absf}) {
580                         return;
581                 }
582                 $loaded{$absf}=1;
583
584                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
585                 if (! defined $dir) {
586                         $dir=".";
587                 }
588                 $dir=abs_path($dir)."/";
589                 
590                 if (! exists $configfiles{$dir}) {
591                         $configfiles{$dir}=$f;
592                 }
593
594                 # copy in defaults from first parent
595                 my $parent=$dir;
596                 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
597                         if ($parent eq '/') {
598                                 $parent="";
599                         }
600                         if (exists $config{$parent} &&
601                             exists $config{$parent}{DEFAULT}) {
602                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
603                                 last;
604                         }
605                 }
606                 
607                 print "mr: loading config $f\n" if $verbose;
608                 open($in, "<", $f) || die "mr: open $f: $!\n";
609         }
610         my @lines=<$in>;
611         close $in;
612
613         my $section;
614         my $line=0;
615         while (@lines) {
616                 $_=shift @lines;
617                 $line++;
618                 chomp;
619                 next if /^\s*\#/ || /^\s*$/;
620                 if (/^\[([^\]]*)\]\s*$/) {
621                         $section=$1;
622                 }
623                 elsif (/^(\w+)\s*=\s*(.*)/) {
624                         my $parameter=$1;
625                         my $value=$2;
626
627                         # continued value
628                         while (@lines && $lines[0]=~/^\s(.+)/) {
629                                 shift(@lines);
630                                 $line++;
631                                 $value.="\n$1";
632                                 chomp $value;
633                         }
634
635                         if (! defined $section) {
636                                 die "$f line $.: parameter ($parameter) not in section\n";
637                         }
638                         if ($section ne 'ALIAS' &&
639                             ! exists $config{$dir}{$section} &&
640                             exists $config{$dir}{DEFAULT}) {
641                                 # copy in defaults
642                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
643                         }
644                         if ($section eq 'ALIAS') {
645                                 $alias{$parameter}=$value;
646                         }
647                         elsif ($parameter eq 'lib') {
648                                 $config{$dir}{$section}{lib}.=$value."\n";
649                         }
650                         else {
651                                 $config{$dir}{$section}{$parameter}=$value;
652                                 $knownactions{$parameter}=1;
653                                 if ($parameter eq 'chain' &&
654                                     length $dir && $section ne "DEFAULT" &&
655                                     -e $dir.$section."/.mrconfig") {
656                                         my $ret=system($value);
657                                         if ($ret != 0) {
658                                                 if (($? & 127) == 2) {
659                                                         print STDERR "mr $action: chain test interrupted\n";
660                                                         exit 2;
661                                                 }
662                                                 elsif ($? & 127) {
663                                                         print STDERR "mr $action: chain test received signal ".($? & 127)."\n";
664                                                 }
665                                         }
666                                         else {
667                                                 push @toload, $dir.$section."/.mrconfig";
668                                         }
669                                 }
670                         }
671                 }
672                 else {
673                         die "$f line $line: parse error\n";
674                 }
675         }
676
677         foreach (@toload) {
678                 loadconfig($_);
679         }
680 } #}}}
681
682 sub modifyconfig { #{{{
683         my $f=shift;
684         # the section to modify or add
685         my $targetsection=shift;
686         # fields to change in the section
687         # To remove a field, set its value to "".
688         my %changefields=@_;
689
690         my @lines;
691         my @out;
692
693         if (-e $f) {
694                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
695                 @lines=<$in>;
696                 close $in;
697         }
698
699         my $formatfield=sub {
700                 my $field=shift;
701                 my @value=split(/\n/, shift);
702
703                 return "$field = ".shift(@value)."\n".
704                         join("", map { "\t$_\n" } @value);
705         };
706         my $addfields=sub {
707                 my @blanks;
708                 while ($out[$#out] =~ /^\s*$/) {
709                         unshift @blanks, pop @out;
710                 }
711                 foreach my $field (sort keys %changefields) {
712                         if (length $changefields{$field}) {
713                                 push @out, "$field = $changefields{$field}\n";
714                                 delete $changefields{$field};
715                         }
716                 }
717                 push @out, @blanks;
718         };
719
720         my $section;
721         while (@lines) {
722                 $_=shift(@lines);
723
724                 if (/^\s*\#/ || /^\s*$/) {
725                         push @out, $_;
726                 }
727                 elsif (/^\[([^\]]*)\]\s*$/) {
728                         if (defined $section && 
729                             $section eq $targetsection) {
730                                 $addfields->();
731                         }
732
733                         $section=$1;
734
735                         push @out, $_;
736                 }
737                 elsif (/^(\w+)\s*=\s(.*)/) {
738                         my $parameter=$1;
739                         my $value=$2;
740
741                         # continued value
742                         while (@lines && $lines[0]=~/^\s(.+)/) {
743                                 shift(@lines);
744                                 $value.="\n$1";
745                                 chomp $value;
746                         }
747
748                         if ($section eq $targetsection) {
749                                 if (exists $changefields{$parameter}) {
750                                         if (length $changefields{$parameter}) {
751                                                 $value=$changefields{$parameter};
752                                         }
753                                         delete $changefields{$parameter};
754                                 }
755                         }
756
757                         push @out, $formatfield->($parameter, $value);
758                 }
759         }
760
761         if (defined $section && 
762             $section eq $targetsection) {
763                 $addfields->();
764         }
765         elsif (%changefields) {
766                 push @out, "\n[$targetsection]\n";
767                 foreach my $field (sort keys %changefields) {
768                         if (length $changefields{$field}) {
769                                 push @out, $formatfield->($field, $changefields{$field});
770                         }
771                 }
772         }
773
774         open(my $out, ">", $f) || die "mr: write $f: $!\n";
775         print $out @out;
776         close $out;     
777 } #}}}
778
779 # Finally, some useful actions that mr knows about by default.
780 # These can be overridden in ~/.mrconfig.
781 #DATA{{{
782 __DATA__
783 [ALIAS]
784 co = checkout
785 ci = commit
786 ls = list
787
788 [DEFAULT]
789 lib =
790         error() {
791                 echo "mr: $@" >&2
792                 exit 1
793         }
794         hours_since() {
795                 for dir in .git .svn .bzr CVS .hg; do
796                         if [ -e "$MR_REPO/$dir" ]; then
797                                 flagfile="$MR_REPO/$dir/.mr_last$1"
798                                 break
799                         fi
800                 done
801                 if [ -z "$flagfile" ]; then
802                         error "cannot determine flag filename"
803                 fi
804                 perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
805                 touch "$flagfile"
806         }
807
808 update =
809         if [ -d "$MR_REPO"/.svn ]; then
810                 svn update "$@"
811         elif [ -d "$MR_REPO"/.git ]; then
812                 if [ -z "$@" ]; then
813                         git pull -t origin master
814                 else
815                         git pull "$@"
816                 fi
817         elif [ -d "$MR_REPO"/.bzr ]; then
818                 bzr merge "$@"
819         elif [ -d "$MR_REPO"/CVS ]; then
820                 cvs update "$@"
821         elif [ -d "$MR_REPO"/.hg ]; then
822                 hg pull "$@" && hg update "$@"
823         else
824                 error "unknown repo type"
825         fi
826 status =
827         if [ -d "$MR_REPO"/.svn ]; then
828                 svn status "$@"
829         elif [ -d "$MR_REPO"/.git ]; then
830                 git status "$@" || true
831         elif [ -d "$MR_REPO"/.bzr ]; then
832                 bzr status "$@"
833         elif [ -d "$MR_REPO"/CVS ]; then
834                 cvs status "$@"
835         elif [ -d "$MR_REPO"/.hg ]; then
836                 hg status "$@"
837         else
838                 error "unknown repo type"
839         fi
840 commit =
841         if [ -d "$MR_REPO"/.svn ]; then
842                 svn commit "$@"
843         elif [ -d "$MR_REPO"/.git ]; then
844                 git commit -a "$@" && git push --all
845         elif [ -d "$MR_REPO"/.bzr ]; then
846                 bzr commit "$@" && bzr push
847         elif [ -d "$MR_REPO"/CVS ]; then
848                 cvs commit "$@"
849         elif [ -d "$MR_REPO"/.hg ]; then
850                 hg commit -m "$@" && hg push
851         else
852                 error "unknown repo type"
853         fi
854 diff =
855         if [ -d "$MR_REPO"/.svn ]; then
856                 svn diff "$@"
857         elif [ -d "$MR_REPO"/.git ]; then
858                 git diff "$@"
859         elif [ -d "$MR_REPO"/.bzr ]; then
860                 bzr diff "$@"
861         elif [ -d "$MR_REPO"/CVS ]; then
862                 cvs diff "$@"
863         elif [ -d "$MR_REPO"/.hg ]; then
864                 hg diff "$@"
865         else
866                 error "unknown repo type"
867         fi
868 log =
869         if [ -d "$MR_REPO"/.svn ]; then
870                 svn log"$@"
871         elif [ -d "$MR_REPO"/.git ]; then
872                 git log "$@"
873         elif [ -d "$MR_REPO"/.bzr ]; then
874                 bzr log "$@"
875         elif [ -d "$MR_REPO"/CVS ]; then
876                 cvs log "$@"
877         elif [ -d "$MR_REPO"/.hg ]; then
878                 hg log "$@"
879         else
880                 error "unknown repo type"
881         fi
882 register =
883         if [ -n "$1" ]; then
884                 cd "$1"
885         fi
886         basedir="$(basename $(pwd))"
887         if [ -d .svn ]; then
888                 url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
889                 if [ -z "$url" ]; then
890                         error "cannot determine svn url"
891                 fi
892                 echo "Registering svn url: $url in $MR_CONFIG"
893                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="svn co $url $basedir"
894         elif [ -d .git ]; then
895                 url=$(LANG=C git-config --get remote.origin.url)
896                 if [ -z "$url" ]; then
897                         error "cannot determine git url"
898                 fi
899                 echo "Registering git url: $url in $MR_CONFIG"
900                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="git clone $url $basedir"
901         elif [ -d .bzr ]; then
902                 url=$(cat .bzr/branch/parent)
903                 if [ -z "$url" ]; then
904                         error "cannot determine bzr url"
905                 fi
906                 echo "Registering bzr url: $url in $MR_CONFIG"
907                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
908         elif [ -d CVS ]; then
909                 repo=$(cat CVS/Repository)
910                 root=$(cat CVS/Root)
911                 if [ -z "$root" ]; then
912                         error "cannot determine cvs root"
913                 fi
914                 echo "Registering cvs repository $repo at root $root"
915                 mr -c "$MR_CONFIG" config "$(pwd)" \
916                         checkout="cvs -d '$root' co -d $basedir $repo"
917         elif [ -d .hg ]; then
918                 url=$(hg showconfig paths.default)
919                 echo "Registering mercurial repo url: $url in $MR_CONFIG"
920                 mr -c "$MR_CONFIG" config "$(pwd)" \
921                         checkout="hg clone $url $basedir"
922         else
923                 error "unable to register this repo type"
924         fi
925 help =
926         if [ ! -e "$MR_PATH" ]; then
927                 error "cannot find program path"
928         fi
929         (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
930 list = true
931 config = 
932
933 ed = echo "A horse is a horse, of course, of course.."
934 T = echo "I pity the fool."
935 right = echo "Not found."
936 #}}}