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

handle being in a subdir of a repository
[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 (or co)
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 (or ci)
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 starts by reading the .mrconfig
86 file in your home directory. Each repository specified in a .mrconfig file
87 can also have its own .mrconfig file in its root directory that can
88 optionally be used as well. So you could have a ~/.mrconfig that registers a
89 repository ~/src, that itself contains a ~/src/.mrconfig file, that in turn
90 registers several additional repositories.
91
92 The .mrconfig file uses a variant of the INI file format. Lines starting with
93 "#" are comments. Lines ending with "\" are continued on to the next line.
94 Sections specify where each repository is located, relative to the
95 directory that contains the .mrconfig file.
96
97 Within a section, each parameter defines a shell command to run to handle a
98 given action. Note that these shell commands are run in a "set -e" shell
99 environment, where any additional parameters you pass are available in
100 "$@". B<mr> cds into the repository directory before running
101 a command, except for the "checkout" command, which is run in the parent
102 of the repository directory, since the repository isn't checked out yet.
103
104 There are three special parameters. If the "skip" parameter is set and
105 its command returns nonzero, then B<mr> will skip acting on that repository.
106 If the "chain" parameter is set and its command returns nonzero, then B<mr>
107 will try to load a .mrconfig file from the root of the repository. (You
108 should avoid chaining from repositories with untrusted committers.) The
109 "lib" parameter can specify some shell code that will be run before each
110 command, this can be a useful way to define shell functions other commands
111 can use.
112
113 The "default" section allows setting up default handlers for each action,
114 and is overridden by the contents of other sections. mr contains default
115 handlers for the "update", "status", and "commit" actions, so normally
116 you only need to specify what to do for "checkout".
117
118 The "alias" section allows adding aliases for commands. Each parameter
119 is an alias, and its value is the command to run.
120
121 For example:
122
123   [src]
124   checkout = svn co svn://svn.example.com/src/trunk src
125   chain = true
126
127   [src/linux-2.6]
128   skip = small
129   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
130
131   [default]
132   lib = \
133   small() {
134         case "$(hostname)" in; \
135         slug|snail); \
136                 return 0; ;; ; \
137         esac; \
138         return 1; \
139   }
140
141 =head1 AUTHOR
142
143 Copyright 2007 Joey Hess <joey@kitenet.net>
144
145 Licensed under the GNU GPL version 2 or higher.
146
147 http://kitenet.net/~joey/code/mr/
148
149 =cut
150
151 use warnings;
152 use strict;
153 use Getopt::Long;
154 use Cwd qw(getcwd abs_path);
155
156 my $directory=getcwd();
157 my $config="$ENV{HOME}/.mrconfig";
158 my $verbose=0;
159 my %config;
160 my %knownactions;
161 my %alias;
162
163 Getopt::Long::Configure("no_permute");
164 my $result=GetOptions(
165         "d=s" => sub { $directory=abs_path($_[1]) },
166         "c=s" => \$config,
167         "v" => \$verbose,
168 );
169 if (! $result || @ARGV < 1) {
170         die("Usage: mr [-d directory] action [params ...]\n");
171 }
172
173 loadconfig(\*DATA);
174 loadconfig($config);
175 #use Data::Dumper;
176 #print Dumper(\%config);
177
178 # alias expansion and command stemming
179 my $action=shift @ARGV;
180 if (! exists $knownactions{$action}) {
181         if (exists $alias{$action}) {
182                 $action=$alias{$action};
183         }
184         else {
185                 my @matches = grep { /^\Q$action\E/ }
186                         keys %knownactions, keys %alias;
187                 if (@matches == 1) {
188                         $action=$matches[0];
189                 }
190                 else {
191                         die "mr: ambiguous action \"$action\" (matches @matches)\n";
192                 }
193         }
194 }
195
196 # handle being in a subdir of a repository
197 foreach my $topdir (sort keys %config) {
198         foreach my $subdir (sort keys %{$config{$topdir}}) {
199                 if ($directory =~ /^\Q$topdir$subdir\E\//) {
200                         $directory=$topdir.$subdir;
201                 }
202         }
203 }
204
205 my (@failed, @successful, @skipped);
206 my $first=1;
207 foreach my $topdir (sort keys %config) {
208         foreach my $subdir (sort keys %{$config{$topdir}}) {
209                 next if $subdir eq 'default';
210                 
211                 my $dir=$topdir.$subdir;
212
213                 if (defined $directory &&
214                     $dir ne $directory &&
215                     $dir !~ /^\Q$directory\E\//) {
216                         print "mr $action: $dir skipped per -d parameter ($directory)\n" if $verbose;
217                         push @skipped, $dir;
218                         next;
219                 }
220
221                 print "\n" unless $first;
222                 $first=0;
223
224                 action($action, $dir, $topdir, $subdir);
225
226         }
227 }
228
229 sub action {
230         my ($action, $dir, $topdir, $subdir) = @_;
231         
232         my $lib= exists $config{$topdir}{$subdir}{lib} ?
233                         $config{$topdir}{$subdir}{lib} : "";
234
235         if ($action eq 'checkout') {
236                 if (-d $dir) {
237                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
238                         push @skipped, $dir;
239                         return;
240                 }
241                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
242         }
243         elsif ($action eq 'update') {
244                 if (! -d $dir) {
245                         return action("checkout", $dir, $topdir, $subdir);
246                 }
247         }
248         
249         if (! chdir($dir)) {
250                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
251                 push @skipped, $dir;
252         }
253
254         if (exists $config{$topdir}{$subdir}{skip}) {
255                 my $ret=system($lib.$config{$topdir}{$subdir}{skip});
256                 if ($ret >> 8 == 0) {
257                         print "mr $action: $dir skipped per config file\n" if $verbose;
258                         push @skipped, $dir;
259                         return;
260                 }
261         }
262
263         if (! exists $config{$topdir}{$subdir}{$action}) {
264                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
265                 push @skipped, $dir;
266         }
267         else {
268                 print "mr $action: in $dir\n";
269                 my $command="set -e; ".$lib.
270                         "my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ".
271                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
272                 my $ret=system($command);
273                 if ($ret != 0) {
274                         print STDERR "mr $action: failed to run: $command\n" if $verbose;
275                         push @failed, $topdir.$subdir;
276                         if ($ret >> 8 != 0) {
277                                 print STDERR "mr $action: command failed\n";
278                         }
279                         elsif ($ret != 0) {
280                                 print STDERR "mr $action: command died ($ret)\n";
281                         }
282                 }
283                 else {
284                         push @successful, $dir;
285                 }
286         }
287 }
288
289 sub showstat {
290         my $count=shift;
291         my $singular=shift;
292         my $plural=shift;
293         if ($count) {
294                 return "$count ".($count > 1 ? $plural : $singular);
295         }
296         return;
297 }
298 print "\nmr $action: finished (".join("; ",
299         showstat($#successful+1, "successful", "successful"),
300         showstat($#failed+1, "failed", "failed"),
301         showstat($#skipped+1, "skipped", "skipped"),
302 ).")\n";
303 if (@failed) {
304         exit 1;
305 }
306 elsif (! @successful && @skipped) {
307         exit 1;
308 }
309 exit 0;
310
311 my %loaded;
312 sub loadconfig {
313         my $f=shift;
314
315         my @toload;
316
317         my $in;
318         my $dir;
319         if (ref $f eq 'GLOB') {
320                 $in=$f; 
321                 $dir="";
322         }
323         else {
324                 # $f might be a symlink
325                 my $absf=abs_path($f);
326                 if ($loaded{$absf}) {
327                         return;
328                 }
329                 $loaded{$absf}=1;
330
331                 print "mr: loading config $f\n" if $verbose;
332                 open($in, "<", $f) || die "mr: open $f: $!\n";
333                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
334                 if (! defined $dir) {
335                         $dir=".";
336                 }
337                 $dir=abs_path($dir)."/";
338
339                 # copy in defaults from first parent
340                 my $parent=$dir;
341                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
342                         if (exists $config{$parent} &&
343                             exists $config{$parent}{default}) {
344                                 $config{$dir}{default}={ %{$config{$parent}{default}} };
345                                 last;
346                         }
347                 }
348         }
349
350         my $section;
351         while (<$in>) {
352                 chomp;
353                 next if /^\s*\#/ || /^\s*$/;
354                 if (/^\s*\[([^\]]*)\]\s*$/) {
355                         $section=$1;
356                 }
357                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
358                         my $parameter=$1;
359                         my $value=$2;
360
361                         # continuation line
362                         while ($value=~/(.*)\\$/) {
363                                 $value=$1.<$in>;
364                                 chomp $value;
365                         }
366
367                         if (! defined $section) {
368                                 die "$f line $.: parameter ($parameter) not in section\n";
369                         }
370                         if ($section ne 'alias' &&
371                             ! exists $config{$dir}{$section} &&
372                             exists $config{$dir}{default}) {
373                                 # copy in defaults
374                                 $config{$dir}{$section}={ %{$config{$dir}{default}} };
375                         }
376                         if ($section eq 'alias') {
377                                 $alias{$parameter}=$value;
378                         }
379                         elsif ($parameter eq 'lib') {
380                                 $config{$dir}{$section}{lib}.=$value." ; ";
381                         }
382                         else {
383                                 $config{$dir}{$section}{$parameter}=$value;
384                                 $knownactions{$parameter}=1;
385                                 if ($parameter eq 'chain' &&
386                                     length $dir && $section ne "default" &&
387                                     -e $dir.$section."/.mrconfig" &&
388                                     system($value) >> 8 == 0) {
389                                         push @toload, $dir.$section."/.mrconfig";
390                                 }
391                         }
392                 }
393                 else {
394                         die "$f line $.: parse error\n";
395                 }
396         }
397         close $in;
398
399         foreach (@toload) {
400                 loadconfig($_);
401         }
402 }
403
404 # Finally, some useful actions that mr knows about by default.
405 # These can be overridden in ~/.mrconfig.
406 __DATA__
407 [alias]
408         co = checkout
409         ci = commit
410 [default]
411 lib = \
412         error() { \
413                 echo "mr: $@" >&2; \
414                 exit 1; \
415         }
416 update = \
417         if [ -d .svn ]; then \
418                 svn update "$@"; \
419         elif [ -d .git ]; then \
420                 git pull origin master "$@"; \
421         else \
422                 error "unknown repo type"; \
423         fi
424 status = \
425         if [ -d .svn ]; then \
426                 svn status "$@"; \
427         elif [ -d .git ]; then \
428                 git status "$@" || true; \
429         else \
430                 error "unknown repo type"; \
431         fi
432 commit = \
433         if [ -d .svn ]; then \
434                 svn commit "$@"; \
435         elif [ -d .git ]; then \
436                 git commit -a "$@" && git push --all; \
437         else \
438                 error "unknown repo type"; \
439         fi