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

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