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

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