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

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