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

c15b3a393725f28ec55a84613a87daefcea2f668
[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 ne $directory &&
132                     $dir !~ /^\Q$directory\E\//) {
133                         print "mr $action: $dir skipped per -d parameter ($directory)\n" if $verbose;
134                         push @skipped, $dir;
135                         next;
136                 }
137
138                 print "\n" unless $first;
139                 $first=0;
140
141                 if (exists $config{$topdir}{$subdir}{skip}) {
142                         my $ret=system($config{$topdir}{$subdir}{skip});
143                         if ($ret >> 8 == 0) {
144                                 print "mr $action: $dir skipped per config file\n" if $verbose;
145                                 push @skipped, $dir;
146                                 next;
147                         }
148                 }
149
150                 if ($action eq 'checkout') {
151                         if (-e $dir) {
152                                 print "mr $action: $dir already exists, skipping checkout\n";
153                                 push @skipped, $dir;
154                                 next;
155                         }
156                         $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
157                 }
158                 if (! chdir($dir)) {
159                         print STDERR "mr $action: failed to chdir to $dir: $!\n";
160                         push @skipped, $dir;
161                 }
162                 elsif (! exists $config{$topdir}{$subdir}{$action}) {
163                         print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
164                         push @skipped, $dir;
165                 }
166                 else {
167                         print "mr $action: in $dir\n";
168                         my $command="set -e; my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action @ARGV";
169                         my $ret=system($command);
170                         if ($ret != 0) {
171                                 print STDERR "mr $action: failed to run: $command\n" if $verbose;
172                                 push @failures, $topdir.$subdir;
173                                 if ($ret >> 8 != 0) {
174                                         print STDERR "mr $action: command failed\n";
175                                 }
176                                 elsif ($ret != 0) {
177                                         print STDERR "mr $action: command died ($ret)\n";
178                                 }
179                         }
180                         else {
181                                 push @successes, $dir;
182                         }
183                 }
184         }
185 }
186
187 sub showstat {
188         my $count=shift;
189         my $singular=shift;
190         my $plural=shift;
191         if ($count) {
192                 return "$count ".($count > 1 ? $plural : $singular);
193         }
194         return;
195 }
196 print "\nmr $action: finished (".join("; ",
197         showstat($#successes+1, "success", "successes"),
198         showstat($#failures+1, "failure", "failures"),
199         showstat($#skipped+1, "skipped", "skipped"),
200 ).")\n";
201 exit @failures ? 1 : 0;
202
203 my %loaded;
204 sub loadconfig {
205         my $f=shift;
206
207         my @toload;
208
209         my $in;
210         my $dir;
211         if (ref $f eq 'GLOB') {
212                 $in=$f; 
213                 $dir="";
214         }
215         else {
216                 # $f might be a symlink
217                 my $absf=abs_path($f);
218                 if ($loaded{$absf}) {
219                         return;
220                 }
221                 $loaded{$absf}=1;
222
223                 print "mr: loading config $f\n" if $verbose;
224                 open($in, "<", $f) || die "mr: open $f: $!\n";
225                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
226                 $dir=abs_path($dir)."/";
227
228                 # copy in defaults from first parent
229                 my $parent=$dir;
230                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
231                         if (exists $config{$parent} &&
232                             exists $config{$parent}{default}) {
233                                 $config{$dir}{default}={ %{$config{$parent}{default}} };
234                                 last;
235                         }
236                 }
237         }
238
239         my $section;
240         while (<$in>) {
241                 chomp;
242                 next if /^\s*\#/ || /^\s*$/;
243                 if (/^\s*\[([^\]]*)\]\s*$/) {
244                         $section=$1;
245                         if (length $dir && $section ne "default" &&
246                             -e $dir.$section."/.mrconfig") {
247                                 push @toload, $dir.$section."/.mrconfig";
248                         }
249                 }
250                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
251                         my $parameter=$1;
252                         my $value=$2;
253
254                         # continuation line
255                         while ($value=~/(.*)\\$/) {
256                                 $value=$1.<$in>;
257                                 chomp $value;
258                         }
259
260                         if (! defined $section) {
261                                 die "$f line $.: parameter ($parameter) not in section\n";
262                         }
263                         if (! exists $config{$dir}{$section} &&
264                               exists $config{$dir}{default}) {
265                                 # copy in defaults
266                                 $config{$dir}{$section}={ %{$config{$dir}{default}} };
267                         }
268                         $config{$dir}{$section}{$parameter}=$value;
269                 }
270                 else {
271                                 die "$f line $.: parse error\n";
272                 }
273         }
274         close $in;
275
276         foreach (@toload) {
277                 loadconfig($_);
278         }
279 }
280
281 __DATA__
282 # Some useful actions that mr knows about by default.
283 # These can be overridden in ~/.mrconfig.
284 [default]
285 update = \
286         if [ -d .svn ]; then \
287                 svn update; \
288         elif [ -d .git ]; then \
289                 git pull origin master; \
290         else \
291                 echo "mr update: unknown RCS"; \
292                 exit 1; \
293         fi
294 status = \
295         if [ -d .svn ]; then \
296                 svn status; \
297         elif [ -d .git ]; then \
298                 git status || true; \
299         else \
300                 echo "mr status: unknown RCS"; \
301                 exit 1; \
302         fi
303 commit = \
304         if [ -d .svn ]; then \
305                 svn commit "$@"; \
306         elif [ -d .git ]; then \
307                 git commit -a "$@"; \
308         else \
309                 echo "mr commit: unknown RCS"; \
310                 exit 1; \
311         fi