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

03a14b714427111b403f375a61948773f0a15c5c
[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 ".
229                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
230                 my $ret=system($command);
231                 if ($ret != 0) {
232                         print STDERR "mr $action: failed to run: $command\n" if $verbose;
233                         push @failed, $topdir.$subdir;
234                         if ($ret >> 8 != 0) {
235                                 print STDERR "mr $action: command failed\n";
236                         }
237                         elsif ($ret != 0) {
238                                 print STDERR "mr $action: command died ($ret)\n";
239                         }
240                 }
241                 else {
242                         push @successful, $dir;
243                 }
244         }
245 }
246
247 sub showstat {
248         my $count=shift;
249         my $singular=shift;
250         my $plural=shift;
251         if ($count) {
252                 return "$count ".($count > 1 ? $plural : $singular);
253         }
254         return;
255 }
256 print "\nmr $action: finished (".join("; ",
257         showstat($#successful+1, "successful", "successful"),
258         showstat($#failed+1, "failed", "failed"),
259         showstat($#skipped+1, "skipped", "skipped"),
260 ).")\n";
261 exit @failed ? 1 : 0;
262
263 my %loaded;
264 sub loadconfig {
265         my $f=shift;
266
267         my @toload;
268
269         my $in;
270         my $dir;
271         if (ref $f eq 'GLOB') {
272                 $in=$f; 
273                 $dir="";
274         }
275         else {
276                 # $f might be a symlink
277                 my $absf=abs_path($f);
278                 if ($loaded{$absf}) {
279                         return;
280                 }
281                 $loaded{$absf}=1;
282
283                 print "mr: loading config $f\n" if $verbose;
284                 open($in, "<", $f) || die "mr: open $f: $!\n";
285                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
286                 if (! defined $dir) {
287                         $dir=".";
288                 }
289                 $dir=abs_path($dir)."/";
290
291                 # copy in defaults from first parent
292                 my $parent=$dir;
293                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
294                         if (exists $config{$parent} &&
295                             exists $config{$parent}{default}) {
296                                 $config{$dir}{default}={ %{$config{$parent}{default}} };
297                                 last;
298                         }
299                 }
300         }
301
302         my $section;
303         while (<$in>) {
304                 chomp;
305                 next if /^\s*\#/ || /^\s*$/;
306                 if (/^\s*\[([^\]]*)\]\s*$/) {
307                         $section=$1;
308                         if (length $dir && $section ne "default" &&
309                             -e $dir.$section."/.mrconfig") {
310                                 push @toload, $dir.$section."/.mrconfig";
311                         }
312                 }
313                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
314                         my $parameter=$1;
315                         my $value=$2;
316
317                         # continuation line
318                         while ($value=~/(.*)\\$/) {
319                                 $value=$1.<$in>;
320                                 chomp $value;
321                         }
322
323                         if (! defined $section) {
324                                 die "$f line $.: parameter ($parameter) not in section\n";
325                         }
326                         if (! exists $config{$dir}{$section} &&
327                               exists $config{$dir}{default}) {
328                                 # copy in defaults
329                                 $config{$dir}{$section}={ %{$config{$dir}{default}} };
330                         }
331                         $config{$dir}{$section}{$parameter}=$value;
332                         $knownactions{$parameter}=1;
333                 }
334                 else {
335                                 die "$f line $.: parse error\n";
336                 }
337         }
338         close $in;
339
340         foreach (@toload) {
341                 loadconfig($_);
342         }
343 }
344
345 __DATA__
346 # Some useful actions that mr knows about by default.
347 # These can be overridden in ~/.mrconfig.
348 [default]
349 update = \
350         if [ -d .svn ]; then \
351                 svn update; \
352         elif [ -d .git ]; then \
353                 git pull origin master; \
354         else \
355                 echo "mr update: unknown repo type"; \
356                 exit 1; \
357         fi
358 status = \
359         if [ -d .svn ]; then \
360                 svn status; \
361         elif [ -d .git ]; then \
362                 git status || true; \
363         else \
364                 echo "mr status: unknown repo type"; \
365                 exit 1; \
366         fi
367 commit = \
368         if [ -d .svn ]; then \
369                 svn commit "$@"; \
370         elif [ -d .git ]; then \
371                 git commit -a  "$@" \
372         else \
373                 echo "mr commit: unknown repo type"; \
374                 exit 1; \
375         fi