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

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