Add "./sync-all config" command
[ghc-hetmet.git] / sync-all
1 #!/usr/bin/perl -w
2
3 use strict;
4 use Cwd;
5
6 # Usage:
7 #
8 # ./sync-all [-q] [-s] [--ignore-failure] [-r repo]
9 #            [--nofib] [--testsuite] [--checked-out] cmd [git flags]
10 #
11 # Applies the command "cmd" to each repository in the tree.
12 # sync-all will try to do the right thing for both git and darcs repositories.
13 #
14 # e.g.
15 #      ./sync-all -r http://darcs.haskell.org/ghc get
16 #          To get any repos which do not exist in the local tree
17 #
18 #      ./sync-all pull
19 #          To pull everything from the default repos
20 #
21 # -------------- Flags -------------------
22 #   -q says to be quite, and -s to be silent.
23 #
24 #   --ignore-failure says to ignore errors and move on to the next repository
25 #
26 #   -r repo says to use repo as the location of package repositories
27 #
28 #   --checked-out says that the remote repo is in checked-out layout, as
29 #   opposed to the layout used for the main repo.  By default a repo on
30 #   the local filesystem is assumed to be checked-out, and repos accessed
31 #   via HTTP or SSH are assumed to be in the main repo layout; use
32 #   --checked-out to override the latter.
33 #
34 #   --nofib, --testsuite also get the nofib and testsuite repos respectively
35 #
36 # ------------ Which repos to use -------------
37 # sync-all uses the following algorithm to decide which remote repos to use
38 #
39 #  It always computes the remote repos from a single base, $repo_base
40 #  How is $repo_base set?  
41 #    If you say "-r repo", then that's $repo_base
42 #    otherwise $repo_base is set by asking git where the ghc repo came
43 #    from, and removing the last component (e.g. /ghc.git/ of /ghc/).
44 #
45 #  Then sync-all iterates over the package found in the file
46 #  ./packages; see that file for a description of the contents.
47
48 #    If $repo_base looks like a local filesystem path, or if you give
49 #    the --checked-out flag, sync-all works on repos of form
50 #          $repo_base/<local-path>
51 #    otherwise sync-all works on repos of form
52 #          $repo_base/<remote-path>
53 #    This logic lets you say
54 #      both    sync-all -r http://darcs.haskell.org/ghc-6.12 pull
55 #      and     sync-all -r ../HEAD pull
56 #    The latter is called a "checked-out tree".
57
58 # NB: sync-all *ignores* the defaultrepo of all repos other than the
59 # root one.  So the remote repos must be laid out in one of the two
60 # formats given by <local-path> and <remote-path> in the file 'packages'.
61
62 $| = 1; # autoflush stdout after each print, to avoid output after die
63
64 my $defaultrepo;
65 my @packages;
66 my $verbose = 2;
67 my $ignore_failure = 0;
68 my $want_remote_repo = 0;
69 my $checked_out_flag = 0;
70 my $get_mode;
71
72 # Flags specific to a particular command
73 my $local_repo_unnecessary = 0;
74
75 my %tags;
76
77 # Figure out where to get the other repositories from.
78 sub getrepo {
79     my $basedir = ".";
80     my $repo;
81
82     if (defined($defaultrepo)) {
83         $repo = $defaultrepo;
84         chomp $repo;
85     } else {
86         # Figure out where to get the other repositories from,
87         # based on where this GHC repo came from.
88         my $branch = `git branch | grep "\* " | sed "s/^\* //"`; chomp $branch;
89         my $remote = `git config branch.$branch.remote`;         chomp $remote;
90         $repo = `git config remote.$remote.url`;       chomp $repo;
91     }
92
93     my $repo_base;
94     my $checked_out_tree;
95
96     if ($repo =~ /^...*:/) {
97         # HTTP or SSH
98         # Above regex says "at least two chars before the :", to avoid
99         # catching Win32 drives ("C:\").
100         $repo_base = $repo;
101
102         # --checked-out is needed if you want to use a checked-out repo
103         # over SSH or HTTP
104         if ($checked_out_flag) {
105             $checked_out_tree = 1;
106         } else {
107             $checked_out_tree = 0;
108         }
109
110         # Don't drop the last part of the path if specified with -r, as
111         # it expects repos of the form:
112         #
113         #   http://darcs.haskell.org
114         #
115         # rather than
116         #   
117         #   http://darcs.haskell.org/ghc
118         #
119         if (!$defaultrepo) {
120             $repo_base =~ s#/[^/]+/?$##;
121         }
122     }
123     elsif ($repo =~ /^\/|\.\.\/|.:(\/|\\)/) {
124         # Local filesystem, either absolute or relative path
125         # (assumes a checked-out tree):
126         $repo_base = $repo;
127         $checked_out_tree = 1;
128     }
129     else {
130         die "Couldn't work out repo";
131     }
132
133     return $repo_base, $checked_out_tree;
134 }
135
136 sub parsePackages {
137     my @repos;
138     my $lineNum;
139
140     open IN, "< packages" or die "Can't open packages file";
141     @repos = <IN>;
142     close IN;
143
144     @packages = ();
145     $lineNum = 0;
146     foreach (@repos) {
147         chomp;
148         $lineNum++;
149         if (/^([^# ]+) +([^ ]+) +([^ ]+) +([^ ]+) +([^ ]+)$/) {
150             my %line;
151             $line{"localpath"}  = $1;
152             $line{"tag"}        = $2;
153             $line{"remotepath"} = $3;
154             $line{"vcs"}        = $4;
155             $line{"upstream"}   = $5;
156             push @packages, \%line;
157         }
158         elsif (! /^(#.*)?$/) {
159             die "Bad content on line $lineNum of packages file: $_";
160         }
161     }
162 }
163
164 sub message {
165     if ($verbose >= 2) {
166         print "@_\n";
167     }
168 }
169
170 sub warning {
171     if ($verbose >= 1) {
172         print "warning: @_\n";
173     }
174 }
175
176 sub scm {
177     my $dir = shift;
178     my $scm = shift;
179     my $pwd;
180
181     if ($dir eq '.') {
182         message "== running $scm @_";
183     } else {
184         message "== $dir: running $scm @_";
185         $pwd = getcwd();
186         chdir($dir);
187     }
188
189     system ($scm, @_) == 0
190         or $ignore_failure
191         or die "$scm failed: $?";
192
193     if ($dir ne '.') {
194         chdir($pwd);
195     }
196 }
197
198 sub repoexists {
199     my ($scm, $localpath) = @_;
200     
201     if ($scm eq "darcs") {
202         -d "$localpath/_darcs";
203     }
204     else {
205         -d "$localpath/.git";
206     }
207 }
208
209 sub scmall {
210     my $command = shift;
211     
212     my $localpath;
213     my $tag;
214     my $remotepath;
215     my $scm;
216     my $upstream;
217     my $line;
218     my $branch_name;
219     my $subcommand;
220
221     my $path;
222     my $wd_before = getcwd;
223
224     my @scm_args;
225
226     my $pwd;
227     my @args;
228
229     my ($repo_base, $checked_out_tree) = getrepo();
230
231     my $is_github_repo = $repo_base =~ m/(git@|git:\/\/|https:\/\/)github.com/;
232
233     parsePackages;
234
235     @args = ();
236
237     if ($command =~ /^remote$/) {
238         while (@_ > 0 && $_[0] =~ /^-/) {
239             push(@args,shift);
240         }
241         if (@_ < 1) { help(); }
242         $subcommand = shift;
243         if ($subcommand ne 'add' && $subcommand ne 'rm' && $subcommand ne 'set-url') {
244             help();
245         }
246         while (@_ > 0 && $_[0] =~ /^-/) {
247             push(@args,shift);
248         }
249         if (($subcommand eq 'add' || $subcommand eq 'rm') && @_ < 1) {
250             help();
251         } elsif (@_ < 1) { # set-url
252             $branch_name = 'origin';
253         } else {
254             $branch_name = shift;
255         }
256     } elsif ($command eq 'new' || $command eq 'fetch') {
257         if (@_ < 1) {
258             $branch_name = 'origin';
259         } else {
260             $branch_name = shift;
261         }
262     }
263
264     push(@args, @_);
265
266     for $line (@packages) {
267
268             $localpath  = $$line{"localpath"};
269             $tag        = $$line{"tag"};
270             $remotepath = $$line{"remotepath"};
271             $scm        = $$line{"vcs"};
272             $upstream   = $$line{"upstream"};
273
274             # We can't create directories on GitHub, so we translate
275             # "package/foo" into "package-foo".
276             if ($is_github_repo) {
277                 $remotepath =~ s/\//-/;
278             }
279
280             # Check the SCM is OK as early as possible
281             die "Unknown SCM: $scm" if (($scm ne "darcs") and ($scm ne "git"));
282
283             # Work out the path for this package in the repo we pulled from
284             if ($checked_out_tree) {
285                 $path = "$repo_base/$localpath";
286             }
287             else {
288                 $path = "$repo_base/$remotepath";
289             }
290
291             # Work out the arguments we should give to the SCM
292             if ($command =~ /^(?:w|wh|wha|what|whats|whatsn|whatsne|whatsnew|status)$/) {
293                 @scm_args = (($scm eq "darcs" and "whatsnew")
294                           or ($scm eq "git" and "status"));
295                 
296                 # Hack around 'darcs whatsnew' failing if there are no changes
297                 $ignore_failure = 1;
298             }
299             elsif ($command =~ /^commit$/) {
300                 @scm_args = ("commit");
301                 # git fails if there is nothing to commit, so ignore failures
302                 $ignore_failure = 1;
303             }
304             elsif ($command =~ /^(?:pus|push)$/) {
305                 @scm_args = "push";
306             }
307             elsif ($command =~ /^(?:pul|pull)$/) {
308                 @scm_args = "pull";
309                 # Q: should we append the -a argument for darcs repos?
310             }
311             elsif ($command =~ /^(?:g|ge|get)$/) {
312                 # Skip any repositories we have not included the tag for
313                 if (not defined($tags{$tag})) {
314                     next;
315                 }
316                 
317                 if (-d $localpath) {
318                     warning("$localpath already present; omitting") if $localpath ne ".";
319                     next;
320                 }
321                 
322                 # The first time round the loop, default the get-mode
323                 if ($scm eq "darcs" && not defined($get_mode)) {
324                     warning("adding --partial, to override use --complete");
325                     $get_mode = "--partial";
326                 }
327                 
328                 # The only command that doesn't need a repo
329                 $local_repo_unnecessary = 1;
330                 
331                 if ($scm eq "darcs") {
332                     # Note: we can only use the get-mode with darcs for now
333                     @scm_args = ("get", $get_mode, $path, $localpath);
334                 }
335                 else {
336                     @scm_args = ("clone", $path, $localpath);
337                 }
338             }
339             elsif ($command =~ /^(?:s|se|sen|send)$/) {
340                 @scm_args = (($scm eq "darcs" and "send")
341                           or ($scm eq "git" and "send-email"));
342                 $want_remote_repo = 1;
343             }
344             elsif ($command =~ /^fetch$/) {
345                 @scm_args = ("fetch", "$branch_name");
346             }
347             elsif ($command =~ /^new$/) {
348                 @scm_args = ("log", "$branch_name..");
349             }
350             elsif ($command =~ /^remote$/) {
351                 if ($subcommand eq 'add') {
352                     @scm_args = ("remote", "add", $branch_name, $path);
353                 } elsif ($subcommand eq 'rm') {
354                     @scm_args = ("remote", "rm", $branch_name);
355                 } elsif ($subcommand eq 'set-url') {
356                     @scm_args = ("remote", "set-url", $branch_name, $path);
357                 }
358             }
359             elsif ($command =~ /^grep$/) {
360               @scm_args = ("grep");
361               # Hack around 'git grep' failing if there are no matches
362               $ignore_failure = 1;
363             }
364             elsif ($command =~ /^config$/) {
365                 @scm_args = "config";
366             }
367             else {
368                 die "Unknown command: $command";
369             }
370             
371             # Actually execute the command
372             if (repoexists ($scm, $localpath)) {
373                 if ($want_remote_repo) {
374                     if ($scm eq "darcs") {
375                         scm (".", $scm, @scm_args, @args, "--repodir=$localpath", $path);
376                     } else {
377                         # git pull doesn't like to be used with --work-dir
378                         # I couldn't find an alternative to chdir() here
379                         scm ($localpath, $scm, @scm_args, @args, $path, "master");
380                     }
381                 } else {
382                     # git status *must* be used with --work-dir, if we don't chdir() to the dir
383                     scm ($localpath, $scm, @scm_args, @args);
384                 }
385             }
386             elsif ($local_repo_unnecessary) {
387                 # Don't bother to change directory in this case
388                 scm (".", $scm, @scm_args, @args);
389             }
390             elsif ($tag eq "") {
391                 message "== Required repo $localpath is missing! Skipping";
392             }
393             else {
394                 message "== $localpath repo not present; skipping";
395             }
396     }
397 }
398
399
400 sub help()
401 {
402         # Get the built in help
403         my $help = <<END;
404 What do you want to do?
405 Supported commands:
406
407  * whatsnew
408  * commit
409  * push
410  * pull
411  * get, with options:
412   * --<package-tag>
413   * --complete
414   * --partial
415  * fetch
416  * send
417  * new
418  * remote add <branch-name>
419  * remote rm <branch-name>
420  * remote set-url [--push] <branch-name>
421  * grep
422  * config
423
424 Available package-tags are:
425 END
426
427         # Collect all the tags in the packages file
428         my %available_tags;
429         open IN, "< packages" or die "Can't open packages file";
430         while (<IN>) {
431             chomp;
432             if (/^([^# ]+) +(?:([^ ]+) +)?([^ ]+) +([^ ]+)/) {
433                 if (defined($2) && $2 ne "-") {
434                     $available_tags{$2} = 1;
435                 }
436             }
437             elsif (! /^(#.*)?$/) {
438                 die "Bad line: $_";
439             }
440         }
441         close IN;
442         
443         # Show those tags and the help text
444         my @available_tags = keys %available_tags;
445         print "$help@available_tags\n";
446         exit 1;
447 }
448
449 sub main {
450     if (! -d ".git" || ! -d "compiler") {
451         die "error: sync-all must be run from the top level of the ghc tree."
452     }
453
454     $tags{"-"} = 1;
455     $tags{"dph"} = 1;
456
457     while ($#_ ne -1) {
458         my $arg = shift;
459         # We handle -q here as well as lower down as we need to skip over it
460         # if it comes before the source-control command
461         if ($arg eq "-q") {
462             $verbose = 1;
463         }
464         elsif ($arg eq "-s") {
465             $verbose = 0;
466         }
467         elsif ($arg eq "-r") {
468             $defaultrepo = shift;
469         }
470         elsif ($arg eq "--ignore-failure") {
471             $ignore_failure = 1;
472         }
473         elsif ($arg eq "--complete" || $arg eq "--partial") {
474             $get_mode = $arg;
475         }
476         # Use --checked-out if the remote repos are a checked-out tree,
477         # rather than the master trees.
478         elsif ($arg eq "--checked-out") {
479             $checked_out_flag = 1;
480         }
481         # --<tag> says we grab the libs tagged 'tag' with
482         # 'get'. It has no effect on the other commands.
483         elsif ($arg =~ m/^--/) {
484             $arg =~ s/^--//;
485             $tags{$arg} = 1;
486         }
487         else {
488             unshift @_, $arg;
489             if (grep /^-q$/, @_) {
490                 $verbose = 1;
491             }
492             last;
493         }
494     }
495
496     if ($#_ eq -1) {
497         help();
498     }
499     else {
500         # Give the command and rest of the arguments to the main loop
501         scmall @_;
502     }
503 }
504
505 main(@ARGV);
506