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

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