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

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