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

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