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

Merge branch 'master' of ssh://kitenet.net/srv/git/kitenet.net/mr
[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 two 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.)
109
110 The "default" section allows setting up default handlers for each action,
111 and is overridden by the contents of other sections. mr contains default
112 handlers for the "update", "status", and "commit" actions, so normally
113 you only need to specify what to do for "checkout".
114
115 For example:
116
117   [src]
118   checkout = svn co svn://svn.example.com/src/trunk src
119   chain = true
120
121   [src/linux-2.6]
122   # only check this out on kodama
123   skip = test $(hostname) != kodama
124   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
125
126 =head1 AUTHOR
127
128 Copyright 2007 Joey Hess <joey@kitenet.net>
129
130 Licensed under the GNU GPL version 2 or higher.
131
132 http://kitenet.net/~joey/code/mr/
133
134 =cut
135
136 use warnings;
137 use strict;
138 use Getopt::Long;
139 use Cwd qw(getcwd abs_path);
140
141 my $directory=getcwd();
142 my $config="$ENV{HOME}/.mrconfig";
143 my $verbose=0;
144 my %config;
145 my %knownactions;
146
147 Getopt::Long::Configure("no_permute");
148 my $result=GetOptions(
149         "d=s" => sub { $directory=abs_path($_[1]) },
150         "c=s" => \$config,
151         "v" => \$verbose,
152 );
153 if (! $result || @ARGV < 1) {
154         die("Usage: mr [-d directory] action [params ...]\n");
155 }
156
157 loadconfig(\*DATA);
158 loadconfig($config);
159 #use Data::Dumper;
160 #print Dumper(\%config);
161
162 my $action=shift @ARGV;
163 if (! $knownactions{$action}) {
164         my @matches = grep { /^\Q$action\E/ } keys %knownactions;
165         if (@matches == 1) {
166                 $action=$matches[0];
167         }
168         else {
169                 die "mr: ambiguous action \"$action\" (matches @matches)\n";
170         }
171 }
172
173 my (@failed, @successful, @skipped);
174 my $first=1;
175 foreach my $topdir (sort keys %config) {
176         foreach my $subdir (sort keys %{$config{$topdir}}) {
177                 next if $subdir eq 'default';
178                 
179                 my $dir=$topdir.$subdir;
180
181                 if (defined $directory &&
182                     $dir ne $directory &&
183                     $dir !~ /^\Q$directory\E\//) {
184                         print "mr $action: $dir skipped per -d parameter ($directory)\n" if $verbose;
185                         push @skipped, $dir;
186                         next;
187                 }
188
189                 print "\n" unless $first;
190                 $first=0;
191
192                 if (exists $config{$topdir}{$subdir}{skip}) {
193                         my $ret=system($config{$topdir}{$subdir}{skip});
194                         if ($ret >> 8 == 0) {
195                                 print "mr $action: $dir skipped per config file\n" if $verbose;
196                                 push @skipped, $dir;
197                                 next;
198                         }
199                 }
200
201                 action($action, $dir, $topdir, $subdir);
202
203         }
204 }
205
206 sub action {
207         my ($action, $dir, $topdir, $subdir) = @_;
208
209         if ($action eq 'checkout') {
210                 if (-d $dir) {
211                         print "mr $action: $dir already exists, skipping checkout\n";
212                         push @skipped, $dir;
213                         return;
214                 }
215                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
216         }
217         if ($action eq 'update') {
218                 if (! -d $dir) {
219                         return action("checkout", $dir, $topdir, $subdir);
220                 }
221         }
222
223         if (! chdir($dir)) {
224                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
225                 push @skipped, $dir;
226         }
227         elsif (! exists $config{$topdir}{$subdir}{$action}) {
228                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
229                 push @skipped, $dir;
230         }
231         else {
232                 print "mr $action: in $dir\n";
233                 my $command="set -e; my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ".
234                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
235                 my $ret=system($command);
236                 if ($ret != 0) {
237                         print STDERR "mr $action: failed to run: $command\n" if $verbose;
238                         push @failed, $topdir.$subdir;
239                         if ($ret >> 8 != 0) {
240                                 print STDERR "mr $action: command failed\n";
241                         }
242                         elsif ($ret != 0) {
243                                 print STDERR "mr $action: command died ($ret)\n";
244                         }
245                 }
246                 else {
247                         push @successful, $dir;
248                 }
249         }
250 }
251
252 sub showstat {
253         my $count=shift;
254         my $singular=shift;
255         my $plural=shift;
256         if ($count) {
257                 return "$count ".($count > 1 ? $plural : $singular);
258         }
259         return;
260 }
261 print "\nmr $action: finished (".join("; ",
262         showstat($#successful+1, "successful", "successful"),
263         showstat($#failed+1, "failed", "failed"),
264         showstat($#skipped+1, "skipped", "skipped"),
265 ).")\n";
266 if (@failed) {
267         exit 1;
268 }
269 elsif (! @successful && @skipped) {
270         exit 1;
271 }
272 exit 0;
273
274 my %loaded;
275 sub loadconfig {
276         my $f=shift;
277
278         my @toload;
279
280         my $in;
281         my $dir;
282         if (ref $f eq 'GLOB') {
283                 $in=$f; 
284                 $dir="";
285         }
286         else {
287                 # $f might be a symlink
288                 my $absf=abs_path($f);
289                 if ($loaded{$absf}) {
290                         return;
291                 }
292                 $loaded{$absf}=1;
293
294                 print "mr: loading config $f\n" if $verbose;
295                 open($in, "<", $f) || die "mr: open $f: $!\n";
296                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
297                 if (! defined $dir) {
298                         $dir=".";
299                 }
300                 $dir=abs_path($dir)."/";
301
302                 # copy in defaults from first parent
303                 my $parent=$dir;
304                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
305                         if (exists $config{$parent} &&
306                             exists $config{$parent}{default}) {
307                                 $config{$dir}{default}={ %{$config{$parent}{default}} };
308                                 last;
309                         }
310                 }
311         }
312
313         my $section;
314         while (<$in>) {
315                 chomp;
316                 next if /^\s*\#/ || /^\s*$/;
317                 if (/^\s*\[([^\]]*)\]\s*$/) {
318                         $section=$1;
319                 }
320                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
321                         my $parameter=$1;
322                         my $value=$2;
323
324                         # continuation line
325                         while ($value=~/(.*)\\$/) {
326                                 $value=$1.<$in>;
327                                 chomp $value;
328                         }
329
330                         if (! defined $section) {
331                                 die "$f line $.: parameter ($parameter) not in section\n";
332                         }
333                         if (! exists $config{$dir}{$section} &&
334                               exists $config{$dir}{default}) {
335                                 # copy in defaults
336                                 $config{$dir}{$section}={ %{$config{$dir}{default}} };
337                         }
338                         $config{$dir}{$section}{$parameter}=$value;
339                         $knownactions{$parameter}=1;
340
341                         if ($parameter eq 'chain' &&
342                             length $dir && $section ne "default" &&
343                             -e $dir.$section."/.mrconfig" &&
344                             system($value) >> 8 == 0) {
345                                 push @toload, $dir.$section."/.mrconfig";
346                         }
347                 }
348                 else {
349                                 die "$f line $.: parse error\n";
350                 }
351         }
352         close $in;
353
354         foreach (@toload) {
355                 loadconfig($_);
356         }
357 }
358
359 # Finally, some useful actions that mr knows about by default.
360 # These can be overridden in ~/.mrconfig.
361 __DATA__
362 [default]
363 update = \
364         if [ -d .svn ]; then \
365                 svn update; \
366         elif [ -d .git ]; then \
367                 git pull origin master; \
368         else \
369                 echo "mr update: unknown repo type"; \
370                 exit 1; \
371         fi
372 status = \
373         if [ -d .svn ]; then \
374                 svn status; \
375         elif [ -d .git ]; then \
376                 git status || true; \
377         else \
378                 echo "mr status: unknown repo type"; \
379                 exit 1; \
380         fi
381 commit = \
382         if [ -d .svn ]; then \
383                 svn commit "$@"; \
384         elif [ -d .git ]; then \
385                 git commit -a "$@" && git push --all; \
386         else \
387                 echo "mr commit: unknown repo type"; \
388                 exit 1; \
389         fi