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

3c998f3e586ea8bc763319543f048ea1aa8af57d
[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                 next if $subdir eq 'default';
236                 my $dir=$topdir.$subdir;
237                 next if $dir ne $directory && $dir !~ /^\Q$directory\E\//;
238                 push @repos, [$dir, $topdir, $subdir];
239         }
240 }
241 if (! @repos) {
242         # fallback to find a leaf repo
243         LEAF: foreach my $topdir (reverse sort keys %config) {
244                 foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
245                         next if $subdir eq 'default';
246                         my $dir=$topdir.$subdir;
247                         my $d=$directory;
248                         $dir.="/" unless $dir=~/\/$/;
249                         $d.="/" unless $d=~/\/$/;
250                         if ($d=~/^\Q$dir\E/) {
251                                 push @repos, [$dir, $topdir, $subdir];
252                                 last LEAF;
253                         }
254                 }
255         }
256         $nochdir=1;
257 }
258
259 my (@failed, @successful, @skipped);
260 foreach my $repo (@repos) {
261         action($action, @$repo);
262 }
263
264 sub action {
265         my ($action, $dir, $topdir, $subdir) = @_;
266         
267         my $lib= exists $config{$topdir}{$subdir}{lib} ?
268                         $config{$topdir}{$subdir}{lib} : "";
269
270         if ($action eq 'checkout') {
271                 if (-d $dir) {
272                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
273                         push @skipped, $dir;
274                         return;
275                 }
276                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
277         }
278         elsif ($action eq 'update') {
279                 if (! -d $dir) {
280                         return action("checkout", $dir, $topdir, $subdir);
281                 }
282         }
283         
284         $ENV{MR_REPO}=$dir;
285         if (! $nochdir && ! chdir($dir)) {
286                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
287                 push @skipped, $dir;
288         }
289
290         if (exists $config{$topdir}{$subdir}{skip}) {
291                 my $ret=system($lib.$config{$topdir}{$subdir}{skip});
292                 if ($ret >> 8 == 0) {
293                         print "mr $action: $dir skipped per config file\n" if $verbose;
294                         push @skipped, $dir;
295                         return;
296                 }
297         }
298
299         if (! exists $config{$topdir}{$subdir}{$action}) {
300                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
301                 push @skipped, $dir;
302         }
303         else {
304                 print "mr $action: $dir\n";
305                 my $command="set -e; ".$lib.
306                         "my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ".
307                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
308                 my $ret=system($command);
309                 if ($ret != 0) {
310                         print STDERR "mr $action: failed to run: $command\n" if $verbose;
311                         push @failed, $topdir.$subdir;
312                         if ($ret >> 8 != 0) {
313                                 print STDERR "mr $action: command failed\n";
314                         }
315                         elsif ($ret != 0) {
316                                 print STDERR "mr $action: command died ($ret)\n";
317                         }
318                 }
319                 else {
320                         push @successful, $dir;
321                 }
322
323                 print "\n";
324         }
325 }
326
327 sub showstat {
328         my $count=shift;
329         my $singular=shift;
330         my $plural=shift;
331         if ($count) {
332                 return "$count ".($count > 1 ? $plural : $singular);
333         }
334         return;
335 }
336 if (! @successful && ! @failed && ! @skipped) {
337         die "mr $action: no repositories found to work on\n";
338 }
339 print "mr $action: finished (".join("; ",
340         showstat($#successful+1, "successful", "successful"),
341         showstat($#failed+1, "failed", "failed"),
342         showstat($#skipped+1, "skipped", "skipped"),
343 ).")\n";
344 if (@failed) {
345         exit 1;
346 }
347 elsif (! @successful && @skipped) {
348         exit 1;
349 }
350 exit 0;
351
352 my %loaded;
353 sub loadconfig {
354         my $f=shift;
355
356         my @toload;
357
358         my $in;
359         my $dir;
360         if (ref $f eq 'GLOB') {
361                 $in=$f; 
362                 $dir="";
363         }
364         else {
365                 # $f might be a symlink
366                 my $absf=abs_path($f);
367                 if ($loaded{$absf}) {
368                         return;
369                 }
370                 $loaded{$absf}=1;
371
372                 print "mr: loading config $f\n" if $verbose;
373                 open($in, "<", $f) || die "mr: open $f: $!\n";
374                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
375                 if (! defined $dir) {
376                         $dir=".";
377                 }
378                 $dir=abs_path($dir)."/";
379
380                 # copy in defaults from first parent
381                 my $parent=$dir;
382                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
383                         if (exists $config{$parent} &&
384                             exists $config{$parent}{default}) {
385                                 $config{$dir}{default}={ %{$config{$parent}{default}} };
386                                 last;
387                         }
388                 }
389         }
390
391         my $section;
392         while (<$in>) {
393                 chomp;
394                 next if /^\s*\#/ || /^\s*$/;
395                 if (/^\s*\[([^\]]*)\]\s*$/) {
396                         $section=$1;
397                 }
398                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
399                         my $parameter=$1;
400                         my $value=$2;
401
402                         # continuation line
403                         while ($value=~/(.*)\\$/) {
404                                 $value=$1.<$in>;
405                                 chomp $value;
406                         }
407
408                         if (! defined $section) {
409                                 die "$f line $.: parameter ($parameter) not in section\n";
410                         }
411                         if ($section ne 'alias' &&
412                             ! exists $config{$dir}{$section} &&
413                             exists $config{$dir}{default}) {
414                                 # copy in defaults
415                                 $config{$dir}{$section}={ %{$config{$dir}{default}} };
416                         }
417                         if ($section eq 'alias') {
418                                 $alias{$parameter}=$value;
419                         }
420                         elsif ($parameter eq 'lib') {
421                                 $config{$dir}{$section}{lib}.=$value." ; ";
422                         }
423                         else {
424                                 $config{$dir}{$section}{$parameter}=$value;
425                                 $knownactions{$parameter}=1;
426                                 if ($parameter eq 'chain' &&
427                                     length $dir && $section ne "default" &&
428                                     -e $dir.$section."/.mrconfig" &&
429                                     system($value) >> 8 == 0) {
430                                         push @toload, $dir.$section."/.mrconfig";
431                                 }
432                         }
433                 }
434                 else {
435                         die "$f line $.: parse error\n";
436                 }
437         }
438         close $in;
439
440         foreach (@toload) {
441                 loadconfig($_);
442         }
443 }
444
445 # Finally, some useful actions that mr knows about by default.
446 # These can be overridden in ~/.mrconfig.
447 __DATA__
448 [alias]
449         co = checkout
450         ci = commit
451 [default]
452 lib = \
453         error() { \
454                 echo "mr: $@" >&2; \
455                 exit 1; \
456         }
457 update = \
458         if [ -d "$MR_REPO"/.svn ]; then \
459                 svn update "$@"; \
460         elif [ -d "$MR_REPO"/.git ]; then \
461                 git pull origin master "$@"; \
462         else \
463                 error "unknown repo type"; \
464         fi
465 status = \
466         if [ -d "$MR_REPO"/.svn ]; then \
467                 svn status "$@"; \
468         elif [ -d "$MR_REPO"/.git ]; then \
469                 git status "$@" || true; \
470         else \
471                 error "unknown repo type"; \
472         fi
473 commit = \
474         if [ -d "$MR_REPO"/.svn ]; then \
475                 svn commit "$@"; \
476         elif [ -d "$MR_REPO"/.git ]; then \
477                 git commit -a "$@" && git push --all; \
478         else \
479                 error "unknown repo type"; \
480         fi
481 diff = \
482         if [ -d "$MR_REPO"/.svn ]; then \
483                 svn diff "$@"; \
484         elif [ -d "$MR_REPO"/.git ]; then \
485                 git diff "$@"; \
486         else \
487                 error "unknown repo type"; \
488         fi
489 list = true
490 help = \
491         if [ ! -e "$MR_PATH" ]; then \
492                 error "cannot find program path";\
493         fi; \
494         (pod2man -c mr "$MR_PATH" | man -l -) || \
495                 error "pod2man or man failed"