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

19c29aa68f064595d5e4aa6c421ddbf383d0e4c1
[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" => \$ENV{MR_CONFIG},
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 # Make sure MR_CONFIG is an absolute path, but don't use abs_path since
291 # the config file might be a symlink to elsewhere, and the directory it's
292 # in is significant.
293 if ($ENV{MR_CONFIG} !~ /^\//) {
294         $ENV{MR_CONFIG}=getcwd()."/".$ENV{MR_CONFIG};
295 }
296
297 loadconfig(\*DATA);
298 loadconfig($ENV{MR_CONFIG});
299 #use Data::Dumper;
300 #print Dumper(\%config);
301
302 eval {
303         use FindBin qw($Bin $Script);
304         $ENV{MR_PATH}=$Bin."/".$Script;
305 };
306
307 # alias expansion and command stemming
308 my $action=shift @ARGV;
309 if (exists $alias{$action}) {
310         $action=$alias{$action};
311 }
312 if (! exists $knownactions{$action}) {
313         my @matches = grep { /^\Q$action\E/ }
314                 keys %knownactions, keys %alias;
315         if (@matches == 1) {
316                 $action=$matches[0];
317         }
318         elsif (@matches == 0) {
319                 die "mr: unknown action \"$action\" (known actions: ".
320                         join(", ", sort keys %knownactions).")\n";
321         }
322         else {
323                 die "mr: ambiguous action \"$action\" (matches: ".
324                         join(", ", @matches).")\n";
325         }
326 }
327
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                                 }
358                         }
359                         if (! $found) {
360                                 die "mr $action: $section $_ not set\n";
361                         }
362                 }
363         }
364         modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
365         exit 0;
366 }
367 elsif ($action eq 'register') {
368         my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
369                 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
370                 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
371         print STDERR "mr $action: running >>$command<<\n" if $verbose;
372         exec($command) || die "exec: $!";
373 }
374
375 # work out what repos to act on
376 my @repos;
377 my $nochdir=0;
378 foreach my $topdir (sort keys %config) {
379         foreach my $subdir (sort keys %{$config{$topdir}}) {
380                 next if $subdir eq 'DEFAULT';
381                 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
382                 my $d=$directory;
383                 $dir.="/" unless $dir=~/\/$/;
384                 $d.="/" unless $d=~/\/$/;
385                 next if $no_recurse && $d ne $dir;
386                 next if $dir ne $d && $dir !~ /^\Q$d\E/;
387                 push @repos, [$dir, $topdir, $subdir];
388         }
389 }
390 if (! @repos) {
391         # fallback to find a leaf repo
392         LEAF: foreach my $topdir (reverse sort keys %config) {
393                 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
394                         next if $subdir eq 'DEFAULT';
395                         my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
396                         my $d=$directory;
397                         $dir.="/" unless $dir=~/\/$/;
398                         $d.="/" unless $d=~/\/$/;
399                         if ($d=~/^\Q$dir\E/) {
400                                 push @repos, [$dir, $topdir, $subdir];
401                                 last LEAF;
402                         }
403                 }
404         }
405         $nochdir=1;
406 }
407
408 my (@failed, @ok, @skipped);
409 foreach my $repo (@repos) {
410         action($action, @$repo);
411 }
412
413 sub action { #{{{
414         my ($action, $dir, $topdir, $subdir) = @_;
415
416         $ENV{MR_CONFIG}=$configfiles{$topdir};
417         my $lib=exists $config{$topdir}{$subdir}{lib} ?
418                        $config{$topdir}{$subdir}{lib}."\n" : "";
419
420         if (exists $config{$topdir}{$subdir}{deleted}) {
421                 my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
422                 print "mr $action: running deleted test >>$test<<\n" if $verbose;
423                 my $ret=system($test);
424                 if ($ret != 0) {
425                         if (($? & 127) == 2) {
426                                 print STDERR "mr $action: interrupted\n";
427                                 exit 2;
428                         }
429                         elsif ($? & 127) {
430                                 print STDERR "mr $action: deleted test received signal ".($? & 127)."\n";
431                         }
432                 }
433                 if ($ret >> 8 == 0) {
434                         if (-d $dir) {
435                                 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
436                                 push @failed, $dir;
437                                 return;
438                         }
439                         else {
440                                 print "mr $action: $dir skipped (as deleted) per config file\n" if $verbose;
441                                 push @skipped, $dir;
442                                 return;
443                         }
444                 }
445         }
446
447         if ($action eq 'checkout') {
448                 if (-d $dir) {
449                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
450                         push @skipped, $dir;
451                         return;
452                 }
453
454                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
455
456                 if (! -d $dir) {
457                         print "mr $action: creating parent directory $dir\n" if $verbose;
458                         my $ret=system("mkdir", "-p", $dir);
459                 }
460         }
461         elsif ($action eq 'update') {
462                 if (! -d $dir) {
463                         return action("checkout", $dir, $topdir, $subdir);
464                 }
465         }
466         
467         $ENV{MR_REPO}=$dir;
468
469         if (exists $config{$topdir}{$subdir}{skip}) {
470                 my $test="set -e;".$lib.
471                         "my_action(){ $config{$topdir}{$subdir}{skip}\n }; my_action '$action'";
472                 print "mr $action: running skip test >>$test<<\n" if $verbose;
473                 my $ret=system($test);
474                 if ($ret != 0) {
475                         if (($? & 127) == 2) {
476                                 print STDERR "mr $action: interrupted\n";
477                                 exit 2;
478                         }
479                         elsif ($? & 127) {
480                                 print STDERR "mr $action: skip test received signal ".($? & 127)."\n";
481                                 exit 1;
482                         }
483                 }
484                 if ($ret >> 8 == 0) {
485                         print "mr $action: $dir skipped per config file\n" if $verbose;
486                         push @skipped, $dir;
487                         return;
488                 }
489         }
490         
491         if (! $nochdir && ! chdir($dir)) {
492                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
493                 push @failed, $dir;
494         }
495         elsif (! exists $config{$topdir}{$subdir}{$action}) {
496                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
497                 push @skipped, $dir;
498         }
499         else {
500                 if (! $nochdir) {
501                         print "mr $action: $topdir$subdir\n";
502                 }
503                 else {
504                         print "mr $action: $topdir$subdir (in subdir $directory)\n";
505                 }
506                 my $command="set -e; ".$lib.
507                         "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
508                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
509                 print STDERR "mr $action: running >>$command<<\n" if $verbose;
510                 my $ret=system($command);
511                 if ($ret != 0) {
512                         if (($? & 127) == 2) {
513                                 print STDERR "mr $action: interrupted\n";
514                                 exit 2;
515                         }
516                         elsif ($? & 127) {
517                                 print STDERR "mr $action: received signal ".($? & 127)."\n";
518                         }
519                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
520                         push @failed, $dir;
521                         if ($ret >> 8 != 0) {
522                                 print STDERR "mr $action: command failed\n";
523                         }
524                         elsif ($ret != 0) {
525                                 print STDERR "mr $action: command died ($ret)\n";
526                         }
527                 }
528                 else {
529                         if ($action eq 'checkout' && ! -d $dir) {
530                                 print STDERR "mr $action: $dir missing after checkout\n";;
531                                 push @failed, $dir;
532                                 return;
533                         }
534
535                         push @ok, $dir;
536                 }
537
538                 print "\n";
539         }
540 } #}}}
541
542 sub showstat { #{{{
543         my $count=shift;
544         my $singular=shift;
545         my $plural=shift;
546         if ($count) {
547                 return "$count ".($count > 1 ? $plural : $singular);
548         }
549         return;
550 } #}}}
551 if (! @ok && ! @failed && ! @skipped) {
552         die "mr $action: no repositories found to work on\n";
553 }
554 print "mr $action: finished (".join("; ",
555         showstat($#ok+1, "ok", "ok"),
556         showstat($#failed+1, "failed", "failed"),
557         showstat($#skipped+1, "skipped", "skipped"),
558 ).")\n";
559 if ($stats) {
560         if (@skipped) {
561                 print "mr $action: (skipped: ".join(" ", @skipped).")\n";
562         }
563         if (@failed) {
564                 print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
565         }
566 }
567 if (@failed) {
568         exit 1;
569 }
570 elsif (! @ok && @skipped) {
571         exit 1;
572 }
573 exit 0;
574
575 my %loaded;
576 sub loadconfig { #{{{
577         my $f=shift;
578
579         my @toload;
580
581         my $in;
582         my $dir;
583         if (ref $f eq 'GLOB') {
584                 $dir="";
585                 $in=$f; 
586         }
587         else {
588                 if (! -e $f) {
589                         return;
590                 }
591
592                 my $absf=abs_path($f);
593                 if ($loaded{$absf}) {
594                         return;
595                 }
596                 $loaded{$absf}=1;
597
598                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
599                 if (! defined $dir) {
600                         $dir=".";
601                 }
602                 $dir=abs_path($dir)."/";
603
604                 # copy in defaults from first parent
605                 my $parent=$dir;
606                 while ($parent=~s/^(.*\/)[^\/]+\/?$/$1/) {
607                         if ($parent eq '/') {
608                                 $parent="";
609                         }
610                         if (exists $config{$parent} &&
611                             exists $config{$parent}{DEFAULT}) {
612                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
613                                 last;
614                         }
615                 }
616                 
617                 print "mr: loading config $f\n" if $verbose;
618                 open($in, "<", $f) || die "mr: open $f: $!\n";
619         }
620         my @lines=<$in>;
621         close $in;
622
623         my $section;
624         my $line=0;
625         while (@lines) {
626                 $_=shift @lines;
627                 $line++;
628                 chomp;
629                 next if /^\s*\#/ || /^\s*$/;
630                 if (/^\[([^\]]*)\]\s*$/) {
631                         $section=$1;
632                 }
633                 elsif (/^(\w+)\s*=\s*(.*)/) {
634                         my $parameter=$1;
635                         my $value=$2;
636
637                         # continued value
638                         while (@lines && $lines[0]=~/^\s(.+)/) {
639                                 shift(@lines);
640                                 $line++;
641                                 $value.="\n$1";
642                                 chomp $value;
643                         }
644
645                         if (! defined $section) {
646                                 die "$f line $.: parameter ($parameter) not in section\n";
647                         }
648                         if ($section ne 'ALIAS' &&
649                             ! exists $config{$dir}{$section} &&
650                             exists $config{$dir}{DEFAULT}) {
651                                 # copy in defaults
652                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
653                         }
654                         if ($section eq 'ALIAS') {
655                                 $alias{$parameter}=$value;
656                         }
657                         elsif ($parameter eq 'lib') {
658                                 $config{$dir}{$section}{lib}.=$value."\n";
659                         }
660                         else {
661                                 $config{$dir}{$section}{$parameter}=$value;
662                                 $knownactions{$parameter}=1;
663                                 if (! exists $configfiles{$dir}) {
664                                         $configfiles{$dir}=abs_path($f);
665                                 }
666                                 if ($parameter eq 'chain' &&
667                                     length $dir && $section ne "DEFAULT" &&
668                                     -e $dir.$section."/.mrconfig") {
669                                         my $ret=system($value);
670                                         if ($ret != 0) {
671                                                 if (($? & 127) == 2) {
672                                                         print STDERR "mr $action: chain test interrupted\n";
673                                                         exit 2;
674                                                 }
675                                                 elsif ($? & 127) {
676                                                         print STDERR "mr $action: chain test received signal ".($? & 127)."\n";
677                                                 }
678                                         }
679                                         else {
680                                                 push @toload, $dir.$section."/.mrconfig";
681                                         }
682                                 }
683                         }
684                 }
685                 else {
686                         die "$f line $line: parse error\n";
687                 }
688         }
689
690         foreach (@toload) {
691                 loadconfig($_);
692         }
693 } #}}}
694
695 sub modifyconfig { #{{{
696         my $f=shift;
697         # the section to modify or add
698         my $targetsection=shift;
699         # fields to change in the section
700         # To remove a field, set its value to "".
701         my %changefields=@_;
702
703         my @lines;
704         my @out;
705
706         if (-e $f) {
707                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
708                 @lines=<$in>;
709                 close $in;
710         }
711
712         my $formatfield=sub {
713                 my $field=shift;
714                 my @value=split(/\n/, shift);
715
716                 return "$field = ".shift(@value)."\n".
717                         join("", map { "\t$_\n" } @value);
718         };
719         my $addfields=sub {
720                 my @blanks;
721                 while ($out[$#out] =~ /^\s*$/) {
722                         unshift @blanks, pop @out;
723                 }
724                 foreach my $field (sort keys %changefields) {
725                         if (length $changefields{$field}) {
726                                 push @out, "$field = $changefields{$field}\n";
727                                 delete $changefields{$field};
728                         }
729                 }
730                 push @out, @blanks;
731         };
732
733         my $section;
734         while (@lines) {
735                 $_=shift(@lines);
736
737                 if (/^\s*\#/ || /^\s*$/) {
738                         push @out, $_;
739                 }
740                 elsif (/^\[([^\]]*)\]\s*$/) {
741                         if (defined $section && 
742                             $section eq $targetsection) {
743                                 $addfields->();
744                         }
745
746                         $section=$1;
747
748                         push @out, $_;
749                 }
750                 elsif (/^(\w+)\s*=\s(.*)/) {
751                         my $parameter=$1;
752                         my $value=$2;
753
754                         # continued value
755                         while (@lines && $lines[0]=~/^\s(.+)/) {
756                                 shift(@lines);
757                                 $value.="\n$1";
758                                 chomp $value;
759                         }
760
761                         if ($section eq $targetsection) {
762                                 if (exists $changefields{$parameter}) {
763                                         if (length $changefields{$parameter}) {
764                                                 $value=$changefields{$parameter};
765                                         }
766                                         delete $changefields{$parameter};
767                                 }
768                         }
769
770                         push @out, $formatfield->($parameter, $value);
771                 }
772         }
773
774         if (defined $section && 
775             $section eq $targetsection) {
776                 $addfields->();
777         }
778         elsif (%changefields) {
779                 push @out, "\n[$targetsection]\n";
780                 foreach my $field (sort keys %changefields) {
781                         if (length $changefields{$field}) {
782                                 push @out, $formatfield->($field, $changefields{$field});
783                         }
784                 }
785         }
786
787         open(my $out, ">", $f) || die "mr: write $f: $!\n";
788         print $out @out;
789         close $out;     
790 } #}}}
791
792 # Finally, some useful actions that mr knows about by default.
793 # These can be overridden in ~/.mrconfig.
794 #DATA{{{
795 __DATA__
796 [ALIAS]
797 co = checkout
798 ci = commit
799 ls = list
800
801 [DEFAULT]
802 lib =
803         error() {
804                 echo "mr: $@" >&2
805                 exit 1
806         }
807         hours_since() {
808                 for dir in .git .svn .bzr CVS; do
809                         if [ -e "$MR_REPO/$dir" ]; then
810                                 flagfile="$MR_REPO/$dir/.mr_last$1"
811                                 break
812                         fi
813                 done
814                 if [ -z "$flagfile" ]; then
815                         error "cannot determine flag filename"
816                 fi
817                 perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
818                 touch "$flagfile"
819         }
820
821 update =
822         if [ -d "$MR_REPO"/.svn ]; then
823                 svn update "$@"
824         elif [ -d "$MR_REPO"/.git ]; then
825                 git pull origin master "$@"
826         elif [ -d "$MR_REPO"/.bzr ]; then
827                 bzr merge "$@"
828         elif [ -d "$MR_REPO"/CVS ]; then
829                 cvs update "$@"
830         else
831                 error "unknown repo type"
832         fi
833 status =
834         if [ -d "$MR_REPO"/.svn ]; then
835                 svn status "$@"
836         elif [ -d "$MR_REPO"/.git ]; then
837                 git status "$@" || true
838         elif [ -d "$MR_REPO"/.bzr ]; then
839                 bzr status "$@"
840         elif [ -d "$MR_REPO"/CVS ]; then
841                 cvs status "$@"
842         else
843                 error "unknown repo type"
844         fi
845 commit =
846         if [ -d "$MR_REPO"/.svn ]; then
847                 svn commit "$@"
848         elif [ -d "$MR_REPO"/.git ]; then
849                 git commit -a "$@" && git push --all
850         elif [ -d "$MR_REPO"/.bzr ]; then
851                 bzr commit "$@" && bzr push
852         elif [ -d "$MR_REPO"/CVS ]; then
853                 cvs commit "$@"
854         else
855                 error "unknown repo type"
856         fi
857 diff =
858         if [ -d "$MR_REPO"/.svn ]; then
859                 svn diff "$@"
860         elif [ -d "$MR_REPO"/.git ]; then
861                 git diff "$@"
862         elif [ -d "$MR_REPO"/.bzr ]; then
863                 bzr diff "$@"
864         elif [ -d "$MR_REPO"/CVS ]; then
865                 cvs diff "$@"
866         else
867                 error "unknown repo type"
868         fi
869 log =
870         if [ -d "$MR_REPO"/.svn ]; then
871                 svn log"$@"
872         elif [ -d "$MR_REPO"/.git ]; then
873                 git log "$@"
874         elif [ -d "$MR_REPO"/.bzr ]; then
875                 bzr log "$@"
876         elif [ -d "$MR_REPO"/CVS ]; then
877                 cvs log "$@"
878         else
879                 error "unknown repo type"
880         fi
881 register =
882         if [ -n "$1" ]; then
883                 cd "$1"
884         fi
885         basedir="$(basename $(pwd))"
886         if [ -d .svn ]; then
887                 url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
888                 if [ -z "$url" ]; then
889                         error "cannot determine svn url"
890                 fi
891                 echo "Registering svn url: $url in $MR_CONFIG"
892                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="svn co $url $basedir"
893         elif [ -d .git ]; then
894                 url=$(LANG=C git-config --get remote.origin.url)
895                 if [ -z "$url" ]; then
896                         error "cannot determine git url"
897                 fi
898                 echo "Registering git url: $url in $MR_CONFIG"
899                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="git clone $url $basedir"
900         elif [ -d .bzr ]; then
901                 url=$(cat .bzr/branch/parent)
902                 if [ -z "$url" ]; then
903                         error "cannot determine bzr url"
904                 fi
905                 echo "Registering bzr url: $url in $MR_CONFIG"
906                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
907         elif [ -d CVS ]; then
908                 repo=$(cat CVS/Repository)
909                 root=$(cat CVS/Root)
910                 if [ -z "$root" ]; then
911                         error "cannot determine cvs root"
912                 fi
913                 echo "Registering cvs repository $repo at root $root"
914                 mr -c "$MR_CONFIG" config "$(pwd)" \
915                         checkout="cvs -d '$root' co -d $basedir $repo"
916         else
917                 error "unable to register this repo type"
918         fi
919 help =
920         if [ ! -e "$MR_PATH" ]; then
921                 error "cannot find program path"
922         fi
923         (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
924 list = true
925 config = 
926
927 ed = echo "A horse is a horse, of course, of course.."
928 T = echo "I pity the fool."
929 right = echo "Not found."
930 #}}}