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

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