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

use newlines to separate shell command lines
[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 lib
170
171 The "lib" parameter can specify some shell code that will be run before each
172 command, this can be a useful way to define shell functions for other commands
173 to use.
174
175 =back
176
177 =head1 AUTHOR
178
179 Copyright 2007 Joey Hess <joey@kitenet.net>
180
181 Licensed under the GNU GPL version 2 or higher.
182
183 http://kitenet.net/~joey/code/mr/
184
185 =cut
186
187 use warnings;
188 use strict;
189 use Getopt::Long;
190 use Cwd qw(getcwd abs_path);
191
192 my $directory=getcwd();
193 my $config="$ENV{HOME}/.mrconfig";
194 my $verbose=0;
195 my %config;
196 my %knownactions;
197 my %alias;
198
199 Getopt::Long::Configure("no_permute");
200 my $result=GetOptions(
201         "d|directory=s" => sub { $directory=abs_path($_[1]) },
202         "c|config=s" => \$config,
203         "verbose" => \$verbose,
204 );
205 if (! $result || @ARGV < 1) {
206         die("Usage: mr [-d directory] action [params ...]\n".
207             "(Use mr help for man page.)\n");
208
209 }
210
211 loadconfig(\*DATA);
212 loadconfig($config);
213 #use Data::Dumper;
214 #print Dumper(\%config);
215
216 eval {
217         use FindBin qw($Bin $Script);
218         $ENV{MR_PATH}=$Bin."/".$Script;
219 };
220
221 # alias expansion and command stemming
222 my $action=shift @ARGV;
223 if (exists $alias{$action}) {
224         $action=$alias{$action};
225 }
226 if (! exists $knownactions{$action}) {
227         my @matches = grep { /^\Q$action\E/ }
228                 keys %knownactions, keys %alias;
229         if (@matches == 1) {
230                 $action=$matches[0];
231         }
232         elsif (@matches == 0) {
233                 die "mr: unknown action \"$action\" (known actions: ".
234                         join(", ", sort keys %knownactions).")\n";
235         }
236         else {
237                 die "mr: ambiguous action \"$action\" (matches: ".
238                         join(", ", @matches).")\n";
239         }
240 }
241
242 if ($action eq 'help') {
243         exec($config{''}{DEFAULT}{help});
244 }
245
246 # work out what repos to act on
247 my @repos;
248 my $nochdir=0;
249 foreach my $topdir (sort keys %config) {
250         foreach my $subdir (sort keys %{$config{$topdir}}) {
251                 next if $subdir eq 'DEFAULT';
252                 my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
253                 my $d=$directory;
254                 $dir.="/" unless $dir=~/\/$/;
255                 $d.="/" unless $d=~/\/$/;
256                 next if $dir ne $directory && $dir !~ /^\Q$directory\E/;
257                 push @repos, [$dir, $topdir, $subdir];
258         }
259 }
260 if (! @repos) {
261         # fallback to find a leaf repo
262         LEAF: foreach my $topdir (reverse sort keys %config) {
263                 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
264                         next if $subdir eq 'DEFAULT';
265                         my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
266                         my $d=$directory;
267                         $dir.="/" unless $dir=~/\/$/;
268                         $d.="/" unless $d=~/\/$/;
269                         if ($d=~/^\Q$dir\E/) {
270                                 push @repos, [$dir, $topdir, $subdir];
271                                 last LEAF;
272                         }
273                 }
274         }
275         $nochdir=1;
276 }
277
278 my (@failed, @successful, @skipped);
279 foreach my $repo (@repos) {
280         action($action, @$repo);
281 }
282
283 sub action {
284         my ($action, $dir, $topdir, $subdir) = @_;
285         
286         my $lib= exists $config{$topdir}{$subdir}{lib} ?
287                         $config{$topdir}{$subdir}{lib}."\n" : "";
288
289         if ($action eq 'checkout') {
290                 if (-d $dir) {
291                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
292                         push @skipped, $dir;
293                         return;
294                 }
295                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
296         }
297         elsif ($action eq 'update') {
298                 if (! -d $dir) {
299                         return action("checkout", $dir, $topdir, $subdir);
300                 }
301         }
302         
303         $ENV{MR_REPO}=$dir;
304
305         if (exists $config{$topdir}{$subdir}{skip}) {
306                 my $test="set -e;".$lib.$config{$topdir}{$subdir}{skip};
307                 print "mr $action: running skip test >>$test<<\n" if $verbose;
308                 my $ret=system($test);
309                 if ($ret >> 8 == 0) {
310                         print "mr $action: $dir skipped per config file\n" if $verbose;
311                         push @skipped, $dir;
312                         return;
313                 }
314         }
315         
316         if (! $nochdir && ! chdir($dir)) {
317                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
318                 push @failed, $dir;
319         }
320         elsif (! exists $config{$topdir}{$subdir}{$action}) {
321                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
322                 push @skipped, $dir;
323         }
324         else {
325                 if (! $nochdir) {
326                         print "mr $action: $topdir$subdir\n";
327                 }
328                 else {
329                         print "mr $action: $topdir$subdir (in subdir $directory)\n";
330                 }
331                 my $command="set -e; ".$lib.
332                         "my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
333                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
334                 print STDERR "mr $action: running >>$command<<\n" if $verbose;
335                 my $ret=system($command);
336                 if ($ret != 0) {
337                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
338                         push @failed, $dir;
339                         if ($ret >> 8 != 0) {
340                                 print STDERR "mr $action: command failed\n";
341                         }
342                         elsif ($ret != 0) {
343                                 print STDERR "mr $action: command died ($ret)\n";
344                         }
345                 }
346                 else {
347                         push @successful, $dir;
348                 }
349
350                 print "\n";
351         }
352 }
353
354 sub showstat {
355         my $count=shift;
356         my $singular=shift;
357         my $plural=shift;
358         if ($count) {
359                 return "$count ".($count > 1 ? $plural : $singular);
360         }
361         return;
362 }
363 if (! @successful && ! @failed && ! @skipped) {
364         die "mr $action: no repositories found to work on\n";
365 }
366 print "mr $action: finished (".join("; ",
367         showstat($#successful+1, "successful", "successful"),
368         showstat($#failed+1, "failed", "failed"),
369         showstat($#skipped+1, "skipped", "skipped"),
370 ).")\n";
371 if (@failed) {
372         exit 1;
373 }
374 elsif (! @successful && @skipped) {
375         exit 1;
376 }
377 exit 0;
378
379 my %loaded;
380 sub loadconfig {
381         my $f=shift;
382
383         my @toload;
384
385         my $in;
386         my $dir;
387         if (ref $f eq 'GLOB') {
388                 $in=$f; 
389                 $dir="";
390         }
391         else {
392                 # $f might be a symlink
393                 my $absf=abs_path($f);
394                 if ($loaded{$absf}) {
395                         return;
396                 }
397                 $loaded{$absf}=1;
398
399                 print "mr: loading config $f\n" if $verbose;
400                 open($in, "<", $f) || die "mr: open $f: $!\n";
401                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
402                 if (! defined $dir) {
403                         $dir=".";
404                 }
405                 $dir=abs_path($dir)."/";
406
407                 # copy in defaults from first parent
408                 my $parent=$dir;
409                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
410                         if (exists $config{$parent} &&
411                             exists $config{$parent}{DEFAULT}) {
412                                 $config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
413                                 last;
414                         }
415                 }
416         }
417
418         my $section;
419         while (<$in>) {
420                 chomp;
421                 next if /^\s*\#/ || /^\s*$/;
422                 if (/^\s*\[([^\]]*)\]\s*$/) {
423                         $section=$1;
424                 }
425                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
426                         my $parameter=$1;
427                         my $value=$2;
428
429                         # continuation line
430                         while ($value=~/(.*)\\$/s) {
431                                 $value=$1."\n".<$in>;
432                                 chomp $value;
433                         }
434
435                         if (! defined $section) {
436                                 die "$f line $.: parameter ($parameter) not in section\n";
437                         }
438                         if ($section ne 'ALIAS' &&
439                             ! exists $config{$dir}{$section} &&
440                             exists $config{$dir}{DEFAULT}) {
441                                 # copy in defaults
442                                 $config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
443                         }
444                         if ($section eq 'ALIAS') {
445                                 $alias{$parameter}=$value;
446                         }
447                         elsif ($parameter eq 'lib') {
448                                 $config{$dir}{$section}{lib}.=$value."\n";
449                         }
450                         else {
451                                 $config{$dir}{$section}{$parameter}=$value;
452                                 $knownactions{$parameter}=1;
453                                 if ($parameter eq 'chain' &&
454                                     length $dir && $section ne "DEFAULT" &&
455                                     -e $dir.$section."/.mrconfig" &&
456                                     system($value) >> 8 == 0) {
457                                         push @toload, $dir.$section."/.mrconfig";
458                                 }
459                         }
460                 }
461                 else {
462                         die "$f line $.: parse error\n";
463                 }
464         }
465         close $in;
466
467         foreach (@toload) {
468                 loadconfig($_);
469         }
470 }
471
472 # Finally, some useful actions that mr knows about by default.
473 # These can be overridden in ~/.mrconfig.
474 __DATA__
475 [ALIAS]
476         co = checkout
477         ci = commit
478         ls = list
479
480 [DEFAULT]
481 lib =                                                   \
482         error() {                                       \
483                 echo "mr: $@" >&2                       \
484                 exit 1                                  \
485         }
486
487 update =                                                \
488         if [ -d "$MR_REPO"/.svn ]; then                 \
489                 svn update "$@"                         \
490         elif [ -d "$MR_REPO"/.git ]; then               \
491                 git pull origin master "$@"             \
492         elif [ -d "$MR_REPO"/.bzr ]; then               \
493                 bzr merge "$@"                          \
494         elif [ -d "$MR_REPO"/CVS ]; then                \
495                 cvs update "$@"                         \
496         else                                            \
497                 error "unknown repo type"               \
498         fi
499 status =                                                \
500         if [ -d "$MR_REPO"/.svn ]; then                 \
501                 svn status "$@"                         \
502         elif [ -d "$MR_REPO"/.git ]; then               \
503                 git status "$@" || true                 \
504         elif [ -d "$MR_REPO"/.bzr ]; then               \
505                 bzr status "$@"                         \
506         elif [ -d "$MR_REPO"/CVS ]; then                \
507                 cvs status "$@"                         \
508         else                                            \
509                 error "unknown repo type"               \
510         fi
511 commit =                                                \
512         if [ -d "$MR_REPO"/.svn ]; then                 \
513                 svn commit "$@"                         \
514         elif [ -d "$MR_REPO"/.git ]; then               \
515                 git commit -a "$@" && git push --all    \
516         elif [ -d "$MR_REPO"/.bzr ]; then               \
517                 bzr commit "$@" && bzr push             \
518         elif [ -d "$MR_REPO"/CVS ]; then                \
519                 cvs commit "$@"                         \
520         else                                            \
521                 error "unknown repo type"               \
522         fi
523 diff =                                                  \
524         if [ -d "$MR_REPO"/.svn ]; then                 \
525                 svn diff "$@"                           \
526         elif [ -d "$MR_REPO"/.git ]; then               \
527                 git diff "$@"                           \
528         elif [ -d "$MR_REPO"/.bzr ]; then               \
529                 bzr diff "$@"                           \
530         elif [ -d "$MR_REPO"/CVS ]; then                \
531                 cvs diff "$@"                           \
532         else                                            \
533                 error "unknown repo type"               \
534         fi
535 log =                                                   \
536         if [ -d "$MR_REPO"/.svn ]; then                 \
537                 svn log"$@"                             \
538         elif [ -d "$MR_REPO"/.git ]; then               \
539                 git log "$@"                            \
540         elif [ -d "$MR_REPO"/.bzr ]; then               \
541                 bzr log "$@"                            \
542         elif [ -d "$MR_REPO"/CVS ]; then                \
543                 cvs log "$@"                            \
544         else                                            \
545                 error "unknown repo type"               \
546         fi
547 list = true
548
549 help =                                                  \
550         if [ ! -e "$MR_PATH" ]; then                    \
551                 error "cannot find program path"        \
552         fi                                              \
553         (pod2man -c mr "$MR_PATH" | man -l -) ||        \
554                 error "pod2man or man failed"
555
556 ed = echo "A horse is a horse, of course, of course.."
557 T = echo "I pity the fool."