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

allow .mrconfig to be a symlink
[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 like "update", "checkout", or "commit".
27
28 =head1 OPTIONS
29
30 =over 4
31
32 =item -d directory
33
34 Specifies the topmost directory that B<mr> should work in. The default is
35 the current working directory. B<mr> will operate on all registered
36 repositories at or under the directory.
37
38 =item -c mrconfig
39
40 Use the specified mrconfig file, instead of looking for on in your home
41 directory.
42
43 =item -v
44
45 Be verbose.
46
47 =back
48
49 =head1 FILES
50
51 B<mr> is configured by .mrconfig files. It searches for .mrconfig files in
52 your home directory, and in the root directory of each repository specified
53 in a .mrconfig file. So you could have a ~/.mrconfig that registers a
54 repository ~/src, that itself contains a ~/src/.mrconfig file, that in turn
55 registers several additional repositories.
56
57 The .mrconfig file uses a variant of the INI file format. Lines starting with
58 "#" are comments. Lines ending with "\" are continued on to the next line.
59 Sections specify where each repository is located, relative to the
60 directory that contains the .mrconfig file.
61
62 Within a section, each parameter defines a shell command to run to handle a
63 given action. Note that these shell commands are run in a "set -e" shell
64 environment, and B<mr> cds into the repository directory before running
65 them, except for the "checkout" command, which is run in the parent of the
66 repository directory, since the repository isn't checked out yet.
67
68 There are two special parameters. If the "skip" parameter is set and
69 its command returns nonzero, then B<mr> will skip acting on that repository.
70
71 The "default" section allows setting up default handlers for each action,
72 and is overridden by the contents of other sections. mr contains default
73 handlers for the "update", "status", and "commit" actions, so normally
74 you only need to specify what to do for "checkout".
75
76 For example:
77
78   [src]
79   checkout = svn co svn://svn.example.com/src/trunk src
80
81   [src/linux-2.6]
82   # only check this out on kodama
83   skip = test $(hostname) != kodama
84   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
85
86 =head1 AUTHOR
87
88 Copyright 2007 Joey Hess <joey@kitenet.net>
89
90 Licensed under the GNU GPL version 2 or higher.
91
92 http://kitenet.net/~joey/code/mr/
93
94 =cut
95
96 use warnings;
97 use strict;
98 use Getopt::Long;
99 use Cwd qw(getcwd abs_path);
100
101 my $directory=getcwd();
102 my $config="$ENV{HOME}/.mrconfig";
103 my $verbose=0;
104 my %config;
105
106 Getopt::Long::Configure("no_permute");
107 my $result=GetOptions(
108         "d=s" => sub { $directory=abs_path($_[1]) },
109         "c=s" => \$config,
110         "v" => \$verbose,
111 );
112 if (! $result || @ARGV < 1) {
113         die("Usage: mr [-d directory] action [params ...]\n");
114 }
115 my $action=shift @ARGV;
116
117 loadconfig(\*DATA);
118 loadconfig($config);
119 #use Data::Dumper;
120 #print Dumper(\%config);
121
122 my (@failures, @successes, @skipped);
123 my $first=1;
124 foreach my $topdir (sort keys %config) {
125         foreach my $subdir (sort keys %{$config{$topdir}}) {
126                 next if $subdir eq 'default';
127                 
128                 my $dir=$topdir.$subdir;
129
130                 if (defined $directory &&
131                     $dir !~ /^\Q$directory\E\//) {
132                         print "mr $action: $dir skipped per -d parameter ($directory)\n" if $verbose;
133                         push @skipped, $dir;
134                         next;
135                 }
136
137                 print "\n" unless $first;
138                 $first=0;
139
140                 if (exists $config{$topdir}{$subdir}{skip}) {
141                         my $ret=system($config{$topdir}{$subdir}{skip});
142                         if ($ret >> 8 == 0) {
143                                 print "mr $action: $dir skipped per config file\n" if $verbose;
144                                 push @skipped, $dir;
145                                 next;
146                         }
147                 }
148
149                 if ($action eq 'checkout') {
150                         if (-e $dir) {
151                                 print "mr $action: $dir already exists, skipping checkout\n";
152                                 push @skipped, $dir;
153                                 next;
154                         }
155                         $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
156                 }
157                 if (! chdir($dir)) {
158                         print STDERR "mr $action: failed to chdir to $dir: $!\n";
159                         push @skipped, $dir;
160                 }
161                 elsif (! exists $config{$topdir}{$subdir}{$action}) {
162                         print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
163                         push @skipped, $dir;
164                 }
165                 else {
166                         print "mr $action: in $dir\n";
167                         my $command="set -e; my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action @ARGV";
168                         my $ret=system($command);
169                         if ($ret != 0) {
170                                 print STDERR "mr $action: failed to run: $command\n" if $verbose;
171                                 push @failures, $topdir.$subdir;
172                                 if ($ret >> 8 != 0) {
173                                         print STDERR "mr $action: command failed\n";
174                                 }
175                                 elsif ($ret != 0) {
176                                         print STDERR "mr $action: command died ($ret)\n";
177                                 }
178                         }
179                         else {
180                                 push @successes, $dir;
181                         }
182                 }
183         }
184 }
185
186 sub showstat {
187         my $count=shift;
188         my $singular=shift;
189         my $plural=shift;
190         if ($count) {
191                 return "$count ".($count > 1 ? $plural : $singular);
192         }
193         return;
194 }
195 print "\nmr $action: finished (".join("; ",
196         showstat($#successes+1, "success", "sucesses"),
197         showstat($#failures+1, "failure", "failures"),
198         showstat($#skipped+1, "skipped", "skipped"),
199 ).")\n";
200 exit @failures ? 1 : 0;
201
202 my %loaded;
203 sub loadconfig {
204         my $f=shift;
205
206         my @toload;
207
208         my $in;
209         my $dir;
210         if (ref $f eq 'GLOB') {
211                 $in=$f; 
212                 $dir="";
213         }
214         else {
215                 # $f might be a symlink
216                 my $absf=abs_path($f);
217                 if ($loaded{$absf}) {
218                         return;
219                 }
220                 $loaded{$absf}=1;
221
222                 print "mr: loading config $f\n" if $verbose;
223                 open($in, "<", $f) || die "mr: open $f: $!\n";
224                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
225                 $dir=abs_path($dir)."/";
226
227                 # copy in defaults from first parent
228                 my $parent=$dir;
229                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
230                         if (exists $config{$parent} &&
231                             exists $config{$parent}{default}) {
232                                 $config{$dir}{default}={ %{$config{$parent}{default}} };
233                                 last;
234                         }
235                 }
236         }
237
238         my $section;
239         while (<$in>) {
240                 chomp;
241                 next if /^\s*\#/ || /^\s*$/;
242                 if (/^\s*\[([^\]]*)\]\s*$/) {
243                         $section=$1;
244                         if (length $dir && $section ne "default" &&
245                             -e $dir.$section."/.mrconfig") {
246                                 push @toload, $dir.$section."/.mrconfig";
247                         }
248                 }
249                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
250                         my $parameter=$1;
251                         my $value=$2;
252
253                         # continuation line
254                         while ($value=~/(.*)\\$/) {
255                                 $value=$1.<$in>;
256                                 chomp $value;
257                         }
258
259                         if (! defined $section) {
260                                 die "$f line $.: parameter ($parameter) not in section\n";
261                         }
262                         if (! exists $config{$dir}{$section} &&
263                               exists $config{$dir}{default}) {
264                                 # copy in defaults
265                                 $config{$dir}{$section}={ %{$config{$dir}{default}} };
266                         }
267                         $config{$dir}{$section}{$parameter}=$value;
268                 }
269                 else {
270                                 die "$f line $.: parse error\n";
271                 }
272         }
273         close $in;
274
275         foreach (@toload) {
276                 loadconfig($_);
277         }
278 }
279
280 __DATA__
281 # Some useful actions that mr knows about by default.
282 # These can be overridden in ~/.mrconfig.
283 [default]
284 update = \
285         if [ -d .svn ]; then \
286                 svn update; \
287         elif [ -d .git ]; then \
288                 git pull origin master; \
289         else \
290                 echo "mr update: unknown RCS"; \
291                 exit 1; \
292         fi
293 status = \
294         if [ -d .svn ]; then \
295                 svn status; \
296         elif [ -d .git ]; then \
297                 git status || true; \
298         else \
299                 echo "mr status: unknown RCS"; \
300                 exit 1; \
301         fi
302 commit = \
303         if [ -d .svn ]; then \
304                 svn commit "$@"; \
305         elif [ -d .git ]; then \
306                 git commit -a "$@"; \
307         else \
308                 echo "mr commit: unknown RCS"; \
309                 exit 1; \
310         fi