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

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