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

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