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

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