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

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