]> git.madduck.net Git - code/myrepos.git/blob - mr

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

* Fix line number display for config file parse errors.
[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         my $line=0;
531         while (@lines) {
532                 $_=shift @lines;
533                 $line++;
534                 chomp;
535                 next if /^\s*\#/ || /^\s*$/;
536                 if (/^\[([^\]]*)\]\s*$/) {
537                         $section=$1;
538                 }
539                 elsif (/^(\w+)\s*=\s*(.*)/) {
540                         my $parameter=$1;
541                         my $value=$2;
542
543                         # continued value
544                         while (@lines && $lines[0]=~/^\s(.+)/) {
545                                 shift(@lines);
546                                 $line++;
547                                 $value.="\n$1";
548                                 chomp $value;
549                         }
550
551                         if (! defined $section) {
552                                 die "$f line $.: parameter ($parameter) not in section\n";
553                         }
554                         if ($section ne 'ALIAS' &&
555                             ! exists $config{$dir}{$section} &&
556                             exists $config{$dir}{DEFAULT}) {
557                                 # copy in defaults
558                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
559                         }
560                         if ($section eq 'ALIAS') {
561                                 $alias{$parameter}=$value;
562                         }
563                         elsif ($parameter eq 'lib') {
564                                 $config{$dir}{$section}{lib}.=$value."\n";
565                         }
566                         else {
567                                 $config{$dir}{$section}{$parameter}=$value;
568                                 $knownactions{$parameter}=1;
569                                 if ($parameter eq 'chain' &&
570                                     length $dir && $section ne "DEFAULT" &&
571                                     -e $dir.$section."/.mrconfig" &&
572                                     system($value) >> 8 == 0) {
573                                         push @toload, $dir.$section."/.mrconfig";
574                                 }
575                         }
576                 }
577                 else {
578                         die "$f line $line: parse error\n";
579                 }
580         }
581
582         foreach (@toload) {
583                 loadconfig($_);
584         }
585 } #}}}
586
587 sub modifyconfig { #{{{
588         my $f=shift;
589         # the section to modify or add
590         my $targetsection=shift;
591         # fields to change in the section
592         # To remove a field, set its value to "".
593         my %changefields=@_;
594
595         my @lines;
596         my @out;
597
598         if (-e $f) {
599                 open(my $in, "<", $f) || die "mr: open $f: $!\n";
600                 @lines=<$in>;
601                 close $in;
602         }
603
604         my $formatfield=sub {
605                 my $field=shift;
606                 my @value=split(/\n/, shift);
607
608                 return "$field = ".shift(@value)."\n".
609                         join("", map { "\t$_\n" } @value);
610         };
611         my $addfields=sub {
612                 my @blanks;
613                 while ($out[$#out] =~ /^\s*$/) {
614                         unshift @blanks, pop @out;
615                 }
616                 foreach my $field (sort keys %changefields) {
617                         if (length $changefields{$field}) {
618                                 push @out, "$field = $changefields{$field}\n";
619                                 delete $changefields{$field};
620                         }
621                 }
622                 push @out, @blanks;
623         };
624
625         my $section;
626         while (@lines) {
627                 $_=shift(@lines);
628
629                 if (/^\s*\#/ || /^\s*$/) {
630                         push @out, $_;
631                 }
632                 elsif (/^\[([^\]]*)\]\s*$/) {
633                         if (defined $section && 
634                             $section eq $targetsection) {
635                                 $addfields->();
636                         }
637
638                         $section=$1;
639
640                         push @out, $_;
641                 }
642                 elsif (/^(\w+)\s*=\s(.*)/) {
643                         my $parameter=$1;
644                         my $value=$2;
645
646                         # continued value
647                         while (@lines && $lines[0]=~/^\s(.+)/) {
648                                 shift(@lines);
649                                 $value.="\n$1";
650                                 chomp $value;
651                         }
652
653                         if ($section eq $targetsection) {
654                                 if (exists $changefields{$parameter}) {
655                                         if (length $changefields{$parameter}) {
656                                                 $value=$changefields{$parameter};
657                                         }
658                                         delete $changefields{$parameter};
659                                 }
660                         }
661
662                         push @out, $formatfield->($parameter, $value);
663                 }
664         }
665
666         if (defined $section && 
667             $section eq $targetsection) {
668                 $addfields->();
669         }
670         elsif (%changefields) {
671                 push @out, "\n[$targetsection]\n";
672                 foreach my $field (sort keys %changefields) {
673                         if (length $changefields{$field}) {
674                                 push @out, $formatfield->($field, $changefields{$field});
675                         }
676                 }
677         }
678
679         open(my $out, ">", $f) || die "mr: write $f: $!\n";
680         print $out @out;
681         close $out;     
682 } #}}}
683
684 # Finally, some useful actions that mr knows about by default.
685 # These can be overridden in ~/.mrconfig.
686 #DATA{{{
687 __DATA__
688 [ALIAS]
689 co = checkout
690 ci = commit
691 ls = list
692
693 [DEFAULT]
694 lib =
695         error() {
696                 echo "mr: $@" >&2
697                 exit 1
698         }
699         hours_since() {
700                 for dir in .git .svn .bzr CVS; do
701                         if [ -e "$MR_REPO/$dir" ]; then
702                                 flagfile="$MR_REPO/$dir/.mr_last$1"
703                                 break
704                         fi
705                 done
706                 if [ -z "$flagfile" ]; then
707                         error "cannot determine flag filename"
708                 fi
709                 perl -wle 'print -f shift() ? int((-M _) * 24) : 9999' "$flagfile"
710                 touch "$flagfile"
711         }
712
713 update =
714         if [ -d "$MR_REPO"/.svn ]; then
715                 svn update "$@"
716         elif [ -d "$MR_REPO"/.git ]; then
717                 git pull origin master "$@"
718         elif [ -d "$MR_REPO"/.bzr ]; then
719                 bzr merge "$@"
720         elif [ -d "$MR_REPO"/CVS ]; then
721                 cvs update "$@"
722         else
723                 error "unknown repo type"
724         fi
725 status =
726         if [ -d "$MR_REPO"/.svn ]; then
727                 svn status "$@"
728         elif [ -d "$MR_REPO"/.git ]; then
729                 git status "$@" || true
730         elif [ -d "$MR_REPO"/.bzr ]; then
731                 bzr status "$@"
732         elif [ -d "$MR_REPO"/CVS ]; then
733                 cvs status "$@"
734         else
735                 error "unknown repo type"
736         fi
737 commit =
738         if [ -d "$MR_REPO"/.svn ]; then
739                 svn commit "$@"
740         elif [ -d "$MR_REPO"/.git ]; then
741                 git commit -a "$@" && git push --all
742         elif [ -d "$MR_REPO"/.bzr ]; then
743                 bzr commit "$@" && bzr push
744         elif [ -d "$MR_REPO"/CVS ]; then
745                 cvs commit "$@"
746         else
747                 error "unknown repo type"
748         fi
749 diff =
750         if [ -d "$MR_REPO"/.svn ]; then
751                 svn diff "$@"
752         elif [ -d "$MR_REPO"/.git ]; then
753                 git diff "$@"
754         elif [ -d "$MR_REPO"/.bzr ]; then
755                 bzr diff "$@"
756         elif [ -d "$MR_REPO"/CVS ]; then
757                 cvs diff "$@"
758         else
759                 error "unknown repo type"
760         fi
761 log =
762         if [ -d "$MR_REPO"/.svn ]; then
763                 svn log"$@"
764         elif [ -d "$MR_REPO"/.git ]; then
765                 git log "$@"
766         elif [ -d "$MR_REPO"/.bzr ]; then
767                 bzr log "$@"
768         elif [ -d "$MR_REPO"/CVS ]; then
769                 cvs log "$@"
770         else
771                 error "unknown repo type"
772         fi
773 register =
774         if [ -n "$1" ]; then
775                 cd "$1"
776         fi
777         basedir="$(basename $(pwd))"
778         if [ -d .svn ]; then
779                 url=$(LANG=C svn info . | grep -i ^URL: | cut -d ' ' -f 2)
780                 if [ -z "$url" ]; then
781                         error "cannot determine svn url"
782                 fi
783                 echo "Registering svn url: $url"
784                 mr config "$(pwd)" checkout="svn co $url $basedir"
785         elif [ -d .git ]; then
786                 url=$(LANG=C git-config --get remote.origin.url)
787                 if [ -z "$url" ]; then
788                         error "cannot determine git url"
789                 fi
790                 echo "Registering git url: $url"
791                 mr config "$(pwd)" checkout="git clone $url $basedir"
792         elif [ -d .bzr ]; then
793                 url=$(cat .bzr/branch/parent)
794                 if [ -z "$url" ]; then
795                         error "cannot determine bzr url"
796                 fi
797                 echo "Registering bzr url: $url"
798                 mr config "$(pwd)" checkout="bzr clone $url $basedir"
799         else
800                 error "unable to register this repo type"
801         fi
802 help =
803         if [ ! -e "$MR_PATH" ]; then
804                 error "cannot find program path"
805         fi
806         (pod2man -c mr "$MR_PATH" | man -l -) || error "pod2man or man failed"
807 list = true
808 config = 
809
810 ed = echo "A horse is a horse, of course, of course.."
811 T = echo "I pity the fool."
812 #}}}