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

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