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

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