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

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