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

add help and diff commands
[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] diff
18
19 B<mr> [options] action [params ...]
20
21 =head1 DESCRIPTION
22
23 B<mr> is a Multiple Repository management tool. It allows you to register a
24 set of repositories in a .mrconfig file, and then checkout, update, or
25 perform other actions on all of the repositories at once.
26
27 Any mix of revision control systems can be used with B<mr>, and you can
28 define arbitrary actions for commands like "update", "checkout", or "commit".
29
30 The predefined commands should be fairly familiar to users of any revision
31 control system:
32
33 =over 4
34
35 =item checkout (or co)
36
37 Checks out all the registered repositories that are not already checked
38 out.
39
40 =item update
41
42 Updates each registered repository from its configured remote repository.
43
44 If a repository isn't checked out yet, it will first check it out.
45
46 =item status
47
48 Displays a status report for each registered repository, showing what
49 uncommitted changes are present in the repository.
50
51 =item commit (or ci)
52
53 Commits changes to each registered repository. (By default, changes
54 are pushed to the remote repository too, when using distributed systems
55 like git.)
56
57 The optional -m parameter allows specifying a commit message.
58
59 =item diff
60
61 Show a diff of uncommitted changes.
62
63 =item help
64
65 Displays this help.
66
67 =back
68
69 Actions can be abbreviated to any unambiguous subsctring, so
70 "mr st" is equivilant to "mr status".
71
72 =head1 OPTIONS
73
74 =over 4
75
76 =item -d directory
77
78 Specifies the topmost directory that B<mr> should work in. The default is
79 the current working directory. B<mr> will operate on all registered
80 repositories at or under the directory.
81
82 =item -c mrconfig
83
84 Use the specified mrconfig file, instead of looking for on in your home
85 directory.
86
87 =item -v
88
89 Be verbose.
90
91 =back
92
93 =head1 FILES
94
95 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
96 file in your home directory. Each repository specified in a .mrconfig file
97 can also have its own .mrconfig file in its root directory that can
98 optionally be used as well. So you could have a ~/.mrconfig that registers a
99 repository ~/src, that itself contains a ~/src/.mrconfig file, that in turn
100 registers several additional repositories.
101
102 The .mrconfig file uses a variant of the INI file format. Lines starting with
103 "#" are comments. Lines ending with "\" are continued on to the next line.
104 Sections specify where each repository is located, relative to the
105 directory that contains the .mrconfig file.
106
107 Within a section, each parameter defines a shell command to run to handle a
108 given action. Note that these shell commands are run in a "set -e" shell
109 environment, where any additional parameters you pass are available in
110 "$@". B<mr> cds into the repository directory before running
111 a command, except for the "checkout" command, which is run in the parent
112 of the repository directory, since the repository isn't checked out yet.
113
114 There are three special parameters. If the "skip" parameter is set and
115 its command returns nonzero, then B<mr> will skip acting on that repository.
116 If the "chain" parameter is set and its command returns nonzero, then B<mr>
117 will try to load a .mrconfig file from the root of the repository. (You
118 should avoid chaining from repositories with untrusted committers.) The
119 "lib" parameter can specify some shell code that will be run before each
120 command, this can be a useful way to define shell functions other commands
121 can use.
122
123 The "default" section allows setting up default handlers for each action,
124 and is overridden by the contents of other sections. mr contains default
125 handlers for the "update", "status", and "commit" actions, so normally
126 you only need to specify what to do for "checkout".
127
128 The "alias" section allows adding aliases for commands. Each parameter
129 is an alias, and its value is the command to run.
130
131 For example:
132
133   [src]
134   checkout = svn co svn://svn.example.com/src/trunk src
135   chain = true
136
137   [src/linux-2.6]
138   skip = small
139   checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
140
141   [default]
142   lib = \
143   small() {
144         case "$(hostname)" in; \
145         slug|snail); \
146                 return 0; ;; ; \
147         esac; \
148         return 1; \
149   }
150
151 =head1 AUTHOR
152
153 Copyright 2007 Joey Hess <joey@kitenet.net>
154
155 Licensed under the GNU GPL version 2 or higher.
156
157 http://kitenet.net/~joey/code/mr/
158
159 =cut
160
161 use warnings;
162 use strict;
163 use Getopt::Long;
164 use Cwd qw(getcwd abs_path);
165
166 my $directory=getcwd();
167 my $config="$ENV{HOME}/.mrconfig";
168 my $verbose=0;
169 my %config;
170 my %knownactions;
171 my %alias;
172
173 Getopt::Long::Configure("no_permute");
174 my $result=GetOptions(
175         "d=s" => sub { $directory=abs_path($_[1]) },
176         "c=s" => \$config,
177         "v" => \$verbose,
178 );
179 if (! $result || @ARGV < 1) {
180         die("Usage: mr [-d directory] action [params ...]\n".
181             "(Use mr help for man page.)\n");
182
183 }
184
185 loadconfig(\*DATA);
186 loadconfig($config);
187 #use Data::Dumper;
188 #print Dumper(\%config);
189
190 eval {
191         use FindBin qw($Bin $Script);
192         $ENV{MR_PATH}=$Bin."/".$Script;
193 };
194
195 # alias expansion and command stemming
196 my $action=shift @ARGV;
197 if (! exists $knownactions{$action}) {
198         if (exists $alias{$action}) {
199                 $action=$alias{$action};
200         }
201         else {
202                 my @matches = grep { /^\Q$action\E/ }
203                         keys %knownactions, keys %alias;
204                 if (@matches == 1) {
205                         $action=$matches[0];
206                 }
207                 else {
208                         die "mr: ambiguous action \"$action\" (matches @matches)\n";
209                 }
210         }
211 }
212
213 if ($action eq 'help') {
214         exec($config{''}{default}{help});
215 }
216
217 # handle being in a subdir of a repository
218 foreach my $topdir (sort keys %config) {
219         foreach my $subdir (sort keys %{$config{$topdir}}) {
220                 if ($directory =~ /^\Q$topdir$subdir\E\//) {
221                         $directory=$topdir.$subdir;
222                 }
223         }
224 }
225
226 my (@failed, @successful, @skipped);
227 my $first=1;
228 foreach my $topdir (sort keys %config) {
229         foreach my $subdir (sort keys %{$config{$topdir}}) {
230                 next if $subdir eq 'default';
231                 
232                 my $dir=$topdir.$subdir;
233
234                 if (defined $directory &&
235                     $dir ne $directory &&
236                     $dir !~ /^\Q$directory\E\//) {
237                         next;
238                 }
239
240                 action($action, $dir, $topdir, $subdir);
241         }
242 }
243
244 sub action {
245         my ($action, $dir, $topdir, $subdir) = @_;
246         
247         my $lib= exists $config{$topdir}{$subdir}{lib} ?
248                         $config{$topdir}{$subdir}{lib} : "";
249
250         if ($action eq 'checkout') {
251                 if (-d $dir) {
252                         print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
253                         push @skipped, $dir;
254                         return;
255                 }
256                 $dir=~s/^(.*)\/[^\/]+\/?$/$1/;
257         }
258         elsif ($action eq 'update') {
259                 if (! -d $dir) {
260                         return action("checkout", $dir, $topdir, $subdir);
261                 }
262         }
263         
264         if (! chdir($dir)) {
265                 print STDERR "mr $action: failed to chdir to $dir: $!\n";
266                 push @skipped, $dir;
267         }
268
269         if (exists $config{$topdir}{$subdir}{skip}) {
270                 my $ret=system($lib.$config{$topdir}{$subdir}{skip});
271                 if ($ret >> 8 == 0) {
272                         print "mr $action: $dir skipped per config file\n" if $verbose;
273                         push @skipped, $dir;
274                         return;
275                 }
276         }
277
278         if (! exists $config{$topdir}{$subdir}{$action}) {
279                 print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
280                 push @skipped, $dir;
281         }
282         else {
283                 print "mr $action: in $dir\n";
284                 my $command="set -e; ".$lib.
285                         "my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ".
286                         join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
287                 my $ret=system($command);
288                 if ($ret != 0) {
289                         print STDERR "mr $action: failed to run: $command\n" if $verbose;
290                         push @failed, $topdir.$subdir;
291                         if ($ret >> 8 != 0) {
292                                 print STDERR "mr $action: command failed\n";
293                         }
294                         elsif ($ret != 0) {
295                                 print STDERR "mr $action: command died ($ret)\n";
296                         }
297                 }
298                 else {
299                         push @successful, $dir;
300                 }
301
302                 print "\n" unless $first;
303                 $first=0;
304         }
305 }
306
307 sub showstat {
308         my $count=shift;
309         my $singular=shift;
310         my $plural=shift;
311         if ($count) {
312                 return "$count ".($count > 1 ? $plural : $singular);
313         }
314         return;
315 }
316 print "\n" unless $first;
317 print "mr $action: finished (".join("; ",
318         showstat($#successful+1, "successful", "successful"),
319         showstat($#failed+1, "failed", "failed"),
320         showstat($#skipped+1, "skipped", "skipped"),
321 ).")\n";
322 if (@failed) {
323         exit 1;
324 }
325 elsif (! @successful && @skipped) {
326         exit 1;
327 }
328 exit 0;
329
330 my %loaded;
331 sub loadconfig {
332         my $f=shift;
333
334         my @toload;
335
336         my $in;
337         my $dir;
338         if (ref $f eq 'GLOB') {
339                 $in=$f; 
340                 $dir="";
341         }
342         else {
343                 # $f might be a symlink
344                 my $absf=abs_path($f);
345                 if ($loaded{$absf}) {
346                         return;
347                 }
348                 $loaded{$absf}=1;
349
350                 print "mr: loading config $f\n" if $verbose;
351                 open($in, "<", $f) || die "mr: open $f: $!\n";
352                 ($dir)=$f=~/^(.*\/)[^\/]+$/;
353                 if (! defined $dir) {
354                         $dir=".";
355                 }
356                 $dir=abs_path($dir)."/";
357
358                 # copy in defaults from first parent
359                 my $parent=$dir;
360                 while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
361                         if (exists $config{$parent} &&
362                             exists $config{$parent}{default}) {
363                                 $config{$dir}{default}={ %{$config{$parent}{default}} };
364                                 last;
365                         }
366                 }
367         }
368
369         my $section;
370         while (<$in>) {
371                 chomp;
372                 next if /^\s*\#/ || /^\s*$/;
373                 if (/^\s*\[([^\]]*)\]\s*$/) {
374                         $section=$1;
375                 }
376                 elsif (/^\s*(\w+)\s*=\s*(.*)/) {
377                         my $parameter=$1;
378                         my $value=$2;
379
380                         # continuation line
381                         while ($value=~/(.*)\\$/) {
382                                 $value=$1.<$in>;
383                                 chomp $value;
384                         }
385
386                         if (! defined $section) {
387                                 die "$f line $.: parameter ($parameter) not in section\n";
388                         }
389                         if ($section ne 'alias' &&
390                             ! exists $config{$dir}{$section} &&
391                             exists $config{$dir}{default}) {
392                                 # copy in defaults
393                                 $config{$dir}{$section}={ %{$config{$dir}{default}} };
394                         }
395                         if ($section eq 'alias') {
396                                 $alias{$parameter}=$value;
397                         }
398                         elsif ($parameter eq 'lib') {
399                                 $config{$dir}{$section}{lib}.=$value." ; ";
400                         }
401                         else {
402                                 $config{$dir}{$section}{$parameter}=$value;
403                                 $knownactions{$parameter}=1;
404                                 if ($parameter eq 'chain' &&
405                                     length $dir && $section ne "default" &&
406                                     -e $dir.$section."/.mrconfig" &&
407                                     system($value) >> 8 == 0) {
408                                         push @toload, $dir.$section."/.mrconfig";
409                                 }
410                         }
411                 }
412                 else {
413                         die "$f line $.: parse error\n";
414                 }
415         }
416         close $in;
417
418         foreach (@toload) {
419                 loadconfig($_);
420         }
421 }
422
423 # Finally, some useful actions that mr knows about by default.
424 # These can be overridden in ~/.mrconfig.
425 __DATA__
426 [alias]
427         co = checkout
428         ci = commit
429 [default]
430 lib = \
431         error() { \
432                 echo "mr: $@" >&2; \
433                 exit 1; \
434         }
435 update = \
436         if [ -d .svn ]; then \
437                 svn update "$@"; \
438         elif [ -d .git ]; then \
439                 git pull origin master "$@"; \
440         else \
441                 error "unknown repo type"; \
442         fi
443 status = \
444         if [ -d .svn ]; then \
445                 svn status "$@"; \
446         elif [ -d .git ]; then \
447                 git status "$@" || true; \
448         else \
449                 error "unknown repo type"; \
450         fi
451 commit = \
452         if [ -d .svn ]; then \
453                 svn commit "$@"; \
454         elif [ -d .git ]; then \
455                 git commit -a "$@" && git push --all; \
456         else \
457                 error "unknown repo type"; \
458         fi
459 diff = \
460         if [ -d .svn ]; then \
461                 svn diff "$@"; \
462         elif [ -d .git ]; then \
463                 git diff "$@"; \
464         else \
465                 error "unknown repo type"; \
466         fi
467 help = \
468         if [ ! -e "$MR_PATH" ]; then \
469                 error "cannot find program path";\
470         fi; \
471         (pod2man -c mr "$MR_PATH" | man -l -) || \
472                 error "pod2man or man failed"