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