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

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