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

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