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

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