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

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