]> 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:

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