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

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