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

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