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

default section needs to come first
[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. Each repository specified in a .mrconfig file
116 can also have its own .mrconfig file in its root directory that can
117 optionally be used as well. So you could have a ~/.mrconfig that registers a
118 repository ~/src, that itself contains a ~/src/.mrconfig file, that in turn
119 registers several additional repositories.
120
121 The .mrconfig file uses a variant of the INI file format. Lines starting with
122 "#" are comments. Lines ending with "\" are continued on to the next line.
123
124 The "default" section allows setting default values for the sections that
125 come after it.
126
127 The "alias" section allows adding aliases for actions. Each parameter
128 is an alias, and its value is the action to use.
129
130 All other sections specify where each repository is located, relative to the
131 directory that contains the .mrconfig file.
132
133 Within a section, each parameter defines a shell command to run to handle a
134 given action. mr contains default handlers for the "update", "status", and
135 "commit" actions, so normally you only need to specify what to do for
136 "checkout".
137
138 Note that these shell commands are run in a "set -e" shell
139 environment, where any additional parameters you pass are available in
140 "$@". The "checkout" command is run in the parent of the repository
141 directory, since the repository isn't checked out yet. All other commands
142 are run inside the repository, though not necessarily at the top of it.
143 The "MR_REPO" environment variable is set to the path to the top of the
144 repository.
145
146 There are three special parameters. If the "skip" parameter is set and
147 its command returns nonzero, then B<mr> will skip acting on that repository.
148 If the "chain" parameter is set and its command returns nonzero, then B<mr>
149 will try to load a .mrconfig file from the root of the repository. (You
150 should avoid chaining from repositories with untrusted committers.) The
151 "lib" parameter can specify some shell code that will be run before each
152 command, this can be a useful way to define shell functions for other commands
153 to use.
154
155 For example:
156
157   [src]
158   checkout = svn co svn://svn.example.com/src/trunk src
159   chain = true
160
161   [src/linux-2.6]
162   skip = small
163   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
164
165   [default]
166   lib = \
167   small() {
168         case "$(hostname)" in; \
169         slug|snail); \
170                 return 0; ;; ; \
171         esac; \
172         return 1; \
173   }
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=s" => sub { $directory=abs_path($_[1]) },
200         "c=s" => \$config,
201         "v" => \$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 $knownactions{$action}) {
222         if (exists $alias{$action}) {
223                 $action=$alias{$action};
224         }
225         else {
226                 my @matches = grep { /^\Q$action\E/ }
227                         keys %knownactions, keys %alias;
228                 if (@matches == 1) {
229                         $action=$matches[0];
230                 }
231                 elsif (@matches == 0) {
232                         die "mr: unknown action \"$action\" (known actions: ".
233                                 join(", ", sort keys %knownactions).")\n";
234                 }
235                 else {
236                         die "mr: ambiguous action \"$action\" (matches: ".
237                                 join(", ", @matches).")\n";
238                 }
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=$topdir.$subdir;
253                 next if $dir ne $directory && $dir !~ /^\Q$directory\E\//;
254                 push @repos, [$dir, $topdir, $subdir];
255         }
256 }
257 if (! @repos) {
258         # fallback to find a leaf repo
259         LEAF: foreach my $topdir (reverse sort keys %config) {
260                 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
261                         next if $subdir eq 'default';
262                         my $dir=$topdir.$subdir;
263                         my $d=$directory;
264                         $dir.="/" unless $dir=~/\/$/;
265                         $d.="/" unless $d=~/\/$/;
266                         if ($d=~/^\Q$dir\E/) {
267                                 push @repos, [$dir, $topdir, $subdir];
268                                 last LEAF;
269                         }
270                 }
271         }
272         $nochdir=1;
273 }
274
275 my (@failed, @successful, @skipped);
276 foreach my $repo (@repos) {
277         action($action, @$repo);
278 }
279
280 sub action {
281         my ($action, $dir, $topdir, $subdir) = @_;
282         
283         my $lib= exists $config{$topdir}{$subdir}{lib} ?
284                         $config{$topdir}{$subdir}{lib} : "";
285
286         if ($action eq 'checkout') {
287                 if (-d $dir) {
288                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
289                         push @skipped, $dir;
290                         return;
291                 }
292                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
293         }
294         elsif ($action eq 'update') {
295                 if (! -d $dir) {
296                         return action("checkout", $dir, $topdir, $subdir);
297                 }
298         }
299         
300         $ENV{MR_REPO}=$dir;
301         if (! $nochdir && ! chdir($dir)) {
302                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
303                 push @skipped, $dir;
304         }
305
306         if (exists $config{$topdir}{$subdir}{skip}) {
307                 my $test="set -e;".$lib.$config{$topdir}{$subdir}{skip};
308                 print "mr $action: running skip test $test\n" if $verbose;
309                 my $ret=system($test);
310                 if ($ret >> 8 == 0) {
311                         print "mr $action: $dir skipped per config file\n" if $verbose;
312                         push @skipped, $dir;
313                         return;
314                 }
315         }
316
317         if (! exists $config{$topdir}{$subdir}{$action}) {
318                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
319                 push @skipped, $dir;
320         }
321         else {
322                 if (! $nochdir) {
323                         print "mr $action: $dir\n";
324                 }
325                 else {
326                         print "mr $action: $dir (in subdir $directory)\n";
327                 }
328                 my $command="set -e; ".$lib.
329                         "my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ".
330                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
331                 print STDERR "mr $action: running $command\n" if $verbose;
332                 my $ret=system($command);
333                 if ($ret != 0) {
334                         print STDERR "mr $action: failed ($ret)\n" if $verbose;
335                         push @failed, $topdir.$subdir;
336                         if ($ret >> 8 != 0) {
337                                 print STDERR "mr $action: command failed\n";
338                         }
339                         elsif ($ret != 0) {
340                                 print STDERR "mr $action: command died ($ret)\n";
341                         }
342                 }
343                 else {
344                         push @successful, $dir;
345                 }
346
347                 print "\n";
348         }
349 }
350
351 sub showstat {
352         my $count=shift;
353         my $singular=shift;
354         my $plural=shift;
355         if ($count) {
356                 return "$count ".($count > 1 ? $plural : $singular);
357         }
358         return;
359 }
360 if (! @successful && ! @failed && ! @skipped) {
361         die "mr $action: no repositories found to work on\n";
362 }
363 print "mr $action: finished (".join("; ",
364         showstat($#successful+1, "successful", "successful"),
365         showstat($#failed+1, "failed", "failed"),
366         showstat($#skipped+1, "skipped", "skipped"),
367 ).")\n";
368 if (@failed) {
369         exit 1;
370 }
371 elsif (! @successful && @skipped) {
372         exit 1;
373 }
374 exit 0;
375
376 my %loaded;
377 sub loadconfig {
378         my $f=shift;
379
380         my @toload;
381
382         my $in;
383         my $dir;
384         if (ref $f eq 'GLOB') {
385                 $in=$f; 
386                 $dir="";
387         }
388         else {
389                 # $f might be a symlink
390                 my $absf=abs_path($f);
391                 if ($loaded{$absf}) {
392                         return;
393                 }
394                 $loaded{$absf}=1;
395
396                 print "mr: loading config $f\n" if $verbose;
397                 open($in, "<", $f) || die "mr: open $f: $!\n";
398                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
399                 if (! defined $dir) {
400                         $dir=".";
401                 }
402                 $dir=abs_path($dir)."/";
403
404                 # copy in defaults from first parent
405                 my $parent=$dir;
406                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
407                         if (exists $config{$parent} &&
408                             exists $config{$parent}{default}) {
409                                 $config{$dir}{default}={ %{$config{$parent}{default}} };
410                                 last;
411                         }
412                 }
413         }
414
415         my $section;
416         while (<$in>) {
417                 chomp;
418                 next if /^\s*\#/ || /^\s*$/;
419                 if (/^\s*\[([^\]]*)\]\s*$/) {
420                         $section=$1;
421                 }
422                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
423                         my $parameter=$1;
424                         my $value=$2;
425
426                         # continuation line
427                         while ($value=~/(.*)\\$/) {
428                                 $value=$1.<$in>;
429                                 chomp $value;
430                         }
431
432                         if (! defined $section) {
433                                 die "$f line $.: parameter ($parameter) not in section\n";
434                         }
435                         if ($section ne 'alias' &&
436                             ! exists $config{$dir}{$section} &&
437                             exists $config{$dir}{default}) {
438                                 # copy in defaults
439                                 $config{$dir}{$section}={ %{$config{$dir}{default}} };
440                         }
441                         if ($section eq 'alias') {
442                                 $alias{$parameter}=$value;
443                         }
444                         elsif ($parameter eq 'lib') {
445                                 $config{$dir}{$section}{lib}.=$value." ; ";
446                         }
447                         else {
448                                 $config{$dir}{$section}{$parameter}=$value;
449                                 $knownactions{$parameter}=1;
450                                 if ($parameter eq 'chain' &&
451                                     length $dir && $section ne "default" &&
452                                     -e $dir.$section."/.mrconfig" &&
453                                     system($value) >> 8 == 0) {
454                                         push @toload, $dir.$section."/.mrconfig";
455                                 }
456                         }
457                 }
458                 else {
459                         die "$f line $.: parse error\n";
460                 }
461         }
462         close $in;
463
464         foreach (@toload) {
465                 loadconfig($_);
466         }
467 }
468
469 # Finally, some useful actions that mr knows about by default.
470 # These can be overridden in ~/.mrconfig.
471 __DATA__
472 [alias]
473         co = checkout
474         ci = commit
475         ls = list
476 [default]
477 lib = \
478         error() { \
479                 echo "mr: $@" >&2; \
480                 exit 1; \
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"/CVS ]; then \
488                 cvs update "$@"; \
489         else \
490                 error "unknown repo type"; \
491         fi
492 status = \
493         if [ -d "$MR_REPO"/.svn ]; then \
494                 svn status "$@"; \
495         elif [ -d "$MR_REPO"/.git ]; then \
496                 git status "$@" || true; \
497         elif [ -d "$MR_REPO"/CVS ]; then \
498                 cvs status "$@"; \
499         else \
500                 error "unknown repo type"; \
501         fi
502 commit = \
503         if [ -d "$MR_REPO"/.svn ]; then \
504                 svn commit "$@"; \
505         elif [ -d "$MR_REPO"/.git ]; then \
506                 git commit -a "$@" && git push --all; \
507         elif [ -d "$MR_REPO"/CVS ]; then \
508                 cvs commit "$@"; \
509         else \
510                 error "unknown repo type"; \
511         fi
512 diff = \
513         if [ -d "$MR_REPO"/.svn ]; then \
514                 svn diff "$@"; \
515         elif [ -d "$MR_REPO"/.git ]; then \
516                 git diff "$@"; \
517         elif [ -d "$MR_REPO"/CVS ]; then \
518                 cvs diff "$@"; \
519         else \
520                 error "unknown repo type"; \
521         fi
522 log = \
523         if [ -d "$MR_REPO"/.svn ]; then \
524                 svn log"$@"; \
525         elif [ -d "$MR_REPO"/.git ]; then \
526                 git log "$@"; \
527         elif [ -d "$MR_REPO"/CVS ]; then \
528                 cvs log "$@"; \
529         else \
530                 error "unknown repo type"; \
531         fi
532 list = true
533 help = \
534         if [ ! -e "$MR_PATH" ]; then \
535                 error "cannot find program path";\
536         fi; \
537         (pod2man -c mr "$MR_PATH" | man -l -) || \
538                 error "pod2man or man failed"