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

initial checkin
[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\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=abs_path($f);
216
217                 if ($loaded{$f}) {
218                         return;
219                 }
220                 $loaded{$f}=1;
221
222                 print "mr: loading config $f\n" if $verbose;
223                 open($in, "<", $f) || die "mr: open $f: $!\n";
224                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
225
226                 # copy in defaults from first parent
227                 my $parent=$dir;
228                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
229                         if (exists $config{$parent} &&
230                             exists $config{$parent}{default}) {
231                                 $config{$dir}{default}={ %{$config{$parent}{default}} };
232                                 last;
233                         }
234                 }
235         }
236
237         my $section;
238         while (<$in>) {
239                 chomp;
240                 next if /^\s*\#/ || /^\s*$/;
241                 if (/^\s*\[([^\]]*)\]\s*$/) {
242                         $section=$1;
243                         if (length $dir && $section ne "default" &&
244                             -e $dir.$section."/.mrconfig") {
245                                 push @toload, $dir.$section."/.mrconfig";
246                         }
247                 }
248                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
249                         my $parameter=$1;
250                         my $value=$2;
251
252                         # continuation line
253                         while ($value=~/(.*)\\$/) {
254                                 $value=$1.<$in>;
255                                 chomp $value;
256                         }
257
258                         if (! defined $section) {
259                                 die "$f line $.: parameter ($parameter) not in section\n";
260                         }
261                         if (! exists $config{$dir}{$section} &&
262                               exists $config{$dir}{default}) {
263                                 # copy in defaults
264                                 $config{$dir}{$section}={ %{$config{$dir}{default}} };
265                         }
266                         $config{$dir}{$section}{$parameter}=$value;
267                 }
268                 else {
269                                 die "$f line $.: parse error\n";
270                 }
271         }
272         close $in;
273
274         foreach (@toload) {
275                 loadconfig($_);
276         }
277 }
278
279 __DATA__
280 # Some useful actions that mr knows about by default.
281 # These can be overridden in ~/.mrconfig.
282 [default]
283 update = \
284         if [ -d .svn ]; then \
285                 svn update; \
286         elif [ -d .git ]; then \
287                 git pull origin master; \
288         else \
289                 echo "mr update: unknown RCS"; \
290                 exit 1; \
291         fi
292 status = \
293         if [ -d .svn ]; then \
294                 svn status; \
295         elif [ -d .git ]; then \
296                 git status || true; \
297         else \
298                 echo "mr status: unknown RCS"; \
299                 exit 1; \
300         fi
301 commit = \
302         if [ -d .svn ]; then \
303                 svn commit "$@"; \
304         elif [ -d .git ]; then \
305                 git commit -a "$@"; \
306         else \
307                 echo "mr commit: unknown RCS"; \
308                 exit 1; \
309         fi