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

* Allow for more complex deleted tests, such as marking a repo deleted on
[code/myrepos.git] / mr
1 #!/usr/bin/perl
2
3 #man{{{
4
5 =head1 NAME
6
7 mr - a Multiple Repository management tool
8
9 =head1 SYNOPSIS
10
11 B<mr> [options] checkout
12
13 B<mr> [options] update
14
15 B<mr> [options] status
16
17 B<mr> [options] commit [-m "message"]
18
19 B<mr> [options] diff
20
21 B<mr> [options] log
22
23 B<mr> [options] register [repository]
24
25 B<mr> [options] config section [parameter=[value] ...]
26
27 B<mr> [options] action [params ...]
28
29 =head1 DESCRIPTION
30
31 B<mr> is a Multiple Repository management tool. It
32 can checkout, update, or perform other actions on
33 a set of repositories as if they were one combined respository. It
34 supports any combination of subversion, git, cvs, and bzr repositories, 
35 and support for other revision control systems can easily be added.
36
37 B<mr> cds into and operates on all registered repositories at or below your
38 working directory. Or, if you are in a subdirectory of a repository that
39 contains no other registered repositories, it will stay in that directory,
40 and work on only that repository,
41
42 These predefined commands should be fairly familiar to users of any revision
43 control system:
44
45 =over 4
46
47 =item checkout (or co)
48
49 Checks out any repositories that are not already checked out.
50
51 =item update
52
53 Updates each repository from its configured remote repository.
54
55 If a repository isn't checked out yet, it will first check it out.
56
57 =item status
58
59 Displays a status report for each repository, showing what
60 uncommitted changes are present in the repository.
61
62 =item commit (or ci)
63
64 Commits changes to each repository. (By default, changes are pushed to the
65 remote repository too, when using distributed systems like git.)
66
67 The optional -m parameter allows specifying a commit message.
68
69 =item diff
70
71 Show a diff of uncommitted changes.
72
73 =item log
74
75 Show the commit log.
76
77 =back
78
79 These commands are also available:
80
81 =over 4
82
83 =item list (or ls)
84
85 List the repositories that mr will act on.
86
87 =item register
88
89 Register an existing repository in the mrconfig file. By default, the
90 repository in the current directory is registered, or you can specify a
91 directory to register.
92
93 By default it registers it to the ~/.mrconfig file. To make it write to a
94 different file, use the -c option.
95
96 =item config
97
98 Adds, modifies, removes, or prints a value from the mrconfig file. The next
99 parameter is the name of the section the value is in. To add or modify
100 values, use one or more instances of "parameter=value". Use "parameter=" to
101 remove a parameter. Use just "parameter" to get the value of a parameter.
102
103 For example, to add (or edit) a repository in src/foo:
104
105   mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
106
107 To show the command that mr uses to update the repository in src/foo:
108
109   mr config src/foo update
110
111 =item help
112
113 Displays this help.
114
115 =back
116
117 Actions can be abbreviated to any unambiguous subsctring, so
118 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
119 update"
120
121 Additional parameters can be passed to most commands, and are passed on
122 unchanged to the underlying revision control system. This is mostly useful
123 if the repositories mr will act on all use the same revision control
124 system.
125
126 =head1 OPTIONS
127
128 =over 4
129
130 =item -d directory
131
132 Specifies the topmost directory that B<mr> should work in. The default is
133 the current working directory.
134
135 =item -c mrconfig
136
137 Use the specified mrconfig file, instead of looking for one in your home
138 directory.
139
140 =item -v
141
142 Be verbose.
143
144 =item -s
145
146 Expand the statistics line displayed at the end to include information
147 about exactly which repositories failed and were skipped, if any.
148
149 =item -n
150
151 Just operate on the repository for the current directory, do not 
152 recurse into deeper repositories.
153
154 =back
155
156 =head1 FILES
157
158 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
159 file in your home directory, and this can in turn chain load .mrconfig files
160 from repositories.
161
162 Here is an example .mrconfig file:
163
164   [src]
165   checkout = svn co svn://svn.example.com/src/trunk src
166   chain = true
167
168   [src/linux-2.6]
169   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git &&
170         cd linux-2.6 &&
171         git checkout -b mybranch origin/master
172
173 The .mrconfig file uses a variant of the INI file format. Lines starting with
174 "#" are comments. Values can be continued to the following line by
175 indenting the line with whitespace.
176
177 The "DEFAULT" section allows setting default values for the sections that
178 come after it.
179
180 The "ALIAS" section allows adding aliases for actions. Each parameter
181 is an alias, and its value is the action to use.
182
183 All other sections add repositories. The section header specifies the
184 directory where the repository is located. This is relative to the directory
185 that contains the mrconfig file, but you can also choose to use absolute
186 paths.
187
188 Within a section, each parameter defines a shell command to run to handle a
189 given action. mr contains default handlers for the "update", "status", and
190 "commit" actions, so normally you only need to specify what to do for
191 "checkout".
192
193 Note that these shell commands are run in a "set -e" shell
194 environment, where any additional parameters you pass are available in
195 "$@". The "checkout" command is run in the parent of the repository
196 directory, since the repository isn't checked out yet. All other commands
197 are run inside the repository, though not necessarily at the top of it.
198 The "MR_REPO" environment variable is set to the path to the top of the
199 repository, and "MR_CONFIG" is set to the topmost .mrconfig file used.
200
201 A few parameters have special meanings:
202
203 =over 4
204
205 =item skip
206
207 If the "skip" parameter is set and its command returns true, then B<mr>
208 will skip acting on that repository. The command is passed the action
209 name in $1.
210
211 Here are two examples. The first skips the repo unless
212 mr is run by joey. The second uses the hours_since function
213 (included in mr's built-in library) to skip updating the repo unless it's
214 been at least 12 hours since the last update.
215
216   skip = test $(whoami) != joey
217   skip = [ "$1" = update ] && [ $(hours_since "$1") -lt 12 ]
218
219 =item chain
220
221 If the "chain" parameter is set and its command returns true, then B<mr>
222 will try to load a .mrconfig file from the root of the repository. (You
223 should avoid chaining from repositories with untrusted committers.)
224
225 =item deleted
226
227 If the "deleted" parameter is set and its command returns true, then
228 B<mr> will treat the repository as deleted. It won't ever actually delete
229 the repository, but it will warn if it sees the repository's directory.
230 This is useful when one mrconfig file is shared amoung multiple machines,
231 to keep track of and remember to delete old repositories.
232
233 =item lib
234
235 The "lib" parameter can specify some shell code that will be run before each
236 command, this can be a useful way to define shell functions for other commands
237 to use.
238
239 =back
240
241 =head1 AUTHOR
242
243 Copyright 2007 Joey Hess <joey@kitenet.net>
244
245 Licensed under the GNU GPL version 2 or higher.
246
247 http://kitenet.net/~joey/code/mr/
248
249 =cut
250
251 #}}}
252
253 use warnings;
254 use strict;
255 use Getopt::Long;
256 use Cwd qw(getcwd abs_path);
257
258 $SIG{INT}=sub {
259         print STDERR "mr: interrupted\n";
260         exit 2;
261 };
262
263 $ENV{MR_CONFIG}="$ENV{HOME}/.mrconfig";
264 my $directory=getcwd();
265 my $verbose=0;
266 my $stats=0;
267 my $no_recurse=0;
268 my %config;
269 my %knownactions;
270 my %alias;
271
272 Getopt::Long::Configure("no_permute");
273 my $result=GetOptions(
274         "d|directory=s" => sub { $directory=abs_path($_[1]) },
275         "c|config=s" => sub { $ENV{MR_CONFIG}=abs_path($_[1]) },
276         "v|verbose" => \$verbose,
277         "s|stats" => \$stats,
278         "n|no-recurse" => \$no_recurse,
279 );
280 if (! $result || @ARGV < 1) {
281         die("Usage: mr [-d directory] action [params ...]\n".
282             "(Use mr help for man page.)\n");
283
284 }
285
286 loadconfig(\*DATA);
287 loadconfig($ENV{MR_CONFIG});
288 #use Data::Dumper;
289 #print Dumper(\%config);
290
291 eval {
292         use FindBin qw($Bin $Script);
293         $ENV{MR_PATH}=$Bin."/".$Script;
294 };
295
296 # alias expansion and command stemming
297 my $action=shift @ARGV;
298 if (exists $alias{$action}) {
299         $action=$alias{$action};
300 }
301 if (! exists $knownactions{$action}) {
302         my @matches = grep { /^\Q$action\E/ }
303                 keys %knownactions, keys %alias;
304         if (@matches == 1) {
305                 $action=$matches[0];
306         }
307         elsif (@matches == 0) {
308                 die "mr: unknown action \"$action\" (known actions: ".
309                         join(", ", sort keys %knownactions).")\n";
310         }
311         else {
312                 die "mr: ambiguous action \"$action\" (matches: ".
313                         join(", ", @matches).")\n";
314         }
315 }
316
317 if ($action eq 'help') {
318         exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
319 }
320 elsif ($action eq 'config') {
321         if (@ARGV < 2) {
322                 die "mr config: not enough parameters\n";
323         }
324         my $section=shift;
325         if ($section=~/^\//) {
326                 # try to convert to a path relative to the config file
327                 my ($dir)=$ENV{MR_CONFIG}=~/^(.*\/)[^\/]+$/;
328                 if ($section=~/^\Q$dir\E(.*)/) {
329                         $section=$1;
330                 }
331         }
332         my %changefields;
333         foreach (@ARGV) {
334                 if (/^([^=]+)=(.*)$/) {
335                         $changefields{$1}=$2;
336                 }
337                 else {
338                         my $found=0;
339                         foreach my $topdir (sort keys %config) {
340                                 if (exists $config{$topdir}{$section} &&
341                                     exists $config{$topdir}{$section}{$_}) {
342                                         print $config{$topdir}{$section}{$_}."\n";
343                                         $found=1;
344                                 }
345                         }
346                         if (! $found) {
347                                 die "mr $action: $section $_ not set\n";
348                         }
349                 }
350         }
351         modifyconfig($ENV{MR_CONFIG}, $section, %changefields) if %changefields;
352         exit 0;
353 }
354 elsif ($action eq 'register') {
355         my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
356                 "my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
357                 join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
358         print STDERR "mr $action: running >>$command<<\n" if $verbose;
359         exec($command) || die "exec: $!";
360 }
361
362 # work out what repos to act on
363 my @repos;
364 my $nochdir=0;
365 foreach my $topdir (sort keys %config) {
366         foreach my $subdir (sort keys %{$config{$topdir}}) {
367                 next if $subdir eq 'DEFAULT';
368                 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
369                 my $d=$directory;
370                 $dir.="/" unless $dir=~/\/$/;
371                 $d.="/" unless $d=~/\/$/;
372                 next if $no_recurse && $d ne $dir;
373                 next if $dir ne $d && $dir !~ /^\Q$d\E/;
374                 push @repos, [$dir, $topdir, $subdir];
375         }
376 }
377 if (! @repos) {
378         # fallback to find a leaf repo
379         LEAF: foreach my $topdir (reverse sort keys %config) {
380                 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
381                         next if $subdir eq 'DEFAULT';
382                         my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
383                         my $d=$directory;
384                         $dir.="/" unless $dir=~/\/$/;
385                         $d.="/" unless $d=~/\/$/;
386                         if ($d=~/^\Q$dir\E/) {
387                                 push @repos, [$dir, $topdir, $subdir];
388                                 last LEAF;
389                         }
390                 }
391         }
392         $nochdir=1;
393 }
394
395 my (@failed, @ok, @skipped);
396 foreach my $repo (@repos) {
397         action($action, @$repo);
398 }
399
400 sub action { #{{{
401         my ($action, $dir, $topdir, $subdir) = @_;
402         
403         my $lib= exists $config{$topdir}{$subdir}{lib} ?
404                         $config{$topdir}{$subdir}{lib}."\n" : "";
405
406         if (exists $config{$topdir}{$subdir}{deleted}) {
407                 my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
408                 print "mr $action: running deleted test >>$test<<\n" if $verbose;
409                 my $ret=system($test);
410                 if ($ret != 0) {
411                         if (($? & 127) == 2) {
412                                 print STDERR "mr $action: interrupted\n";
413                                 exit 2;
414                         }
415                         elsif ($? & 127) {
416                                 print STDERR "mr $action: deleted test received signal ".($? & 127)."\n";
417                         }
418                 }
419                 if ($ret >> 8 == 0) {
420                         if (-d $dir) {
421                                 print STDERR "mr error: $dir should be deleted yet still exists\n\n";
422                                 push @failed, $dir;
423                                 return;
424                         }
425                         else {
426                                 print "mr $action: $dir skipped (as deleted) per config file\n" if $verbose;
427                                 push @skipped, $dir;
428                                 return;
429                         }
430                 }
431         }
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                 # 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"
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"
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"
889                 mr -c "$MR_CONFIG" config "$(pwd)" checkout="bzr clone $url $basedir"
890         else
891                 error "unable to register this repo type"
892         fi
893 help =
894         if [ ! -e "$MR_PATH" ]; then
895                 error "cannot find program path"
896         fi
897         (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
898 list = true
899 config = 
900
901 ed = echo "A horse is a horse, of course, of course.."
902 T = echo "I pity the fool."
903 right = echo "Not found."
904 #}}}