Merge branch 'master' of http://darcs.haskell.org/ghc
[ghc.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 $checked_out_flag = 0;
69 my $get_mode;
70
71 my %tags;
72
73 # Figure out where to get the other repositories from.
74 sub getrepo {
75     my $basedir = ".";
76     my $repo;
77
78     if (defined($defaultrepo)) {
79         $repo = $defaultrepo;
80         chomp $repo;
81     } else {
82         # Figure out where to get the other repositories from,
83         # based on where this GHC repo came from.
84         my $branch = `git branch | grep "\* " | sed "s/^\* //"`; chomp $branch;
85         my $remote = `git config branch.$branch.remote`;         chomp $remote;
86         $repo = `git config remote.$remote.url`;       chomp $repo;
87     }
88
89     my $repo_base;
90     my $checked_out_tree;
91
92     if ($repo =~ /^...*:/) {
93         # HTTP or SSH
94         # Above regex says "at least two chars before the :", to avoid
95         # catching Win32 drives ("C:\").
96         $repo_base = $repo;
97
98         # --checked-out is needed if you want to use a checked-out repo
99         # over SSH or HTTP
100         if ($checked_out_flag) {
101             $checked_out_tree = 1;
102         } else {
103             $checked_out_tree = 0;
104         }
105
106         # Don't drop the last part of the path if specified with -r, as
107         # it expects repos of the form:
108         #
109         #   http://darcs.haskell.org
110         #
111         # rather than
112         #   
113         #   http://darcs.haskell.org/ghc
114         #
115         if (!$defaultrepo) {
116             $repo_base =~ s#/[^/]+/?$##;
117         }
118     }
119     elsif ($repo =~ /^\/|\.\.\/|.:(\/|\\)/) {
120         # Local filesystem, either absolute or relative path
121         # (assumes a checked-out tree):
122         $repo_base = $repo;
123         $checked_out_tree = 1;
124     }
125     else {
126         die "Couldn't work out repo";
127     }
128
129     return $repo_base, $checked_out_tree;
130 }
131
132 sub parsePackages {
133     my @repos;
134     my $lineNum;
135
136     open IN, "< packages" or die "Can't open packages file";
137     @repos = <IN>;
138     close IN;
139
140     @packages = ();
141     $lineNum = 0;
142     foreach (@repos) {
143         chomp;
144         $lineNum++;
145         if (/^([^# ]+) +([^ ]+) +([^ ]+) +([^ ]+)$/) {
146             my %line;
147             $line{"localpath"}  = $1;
148             $line{"tag"}        = $2;
149             $line{"remotepath"} = $3;
150             $line{"vcs"}        = $4;
151             push @packages, \%line;
152         }
153         elsif (! /^(#.*)?$/) {
154             die "Bad content on line $lineNum of packages file: $_";
155         }
156     }
157 }
158
159 sub message {
160     if ($verbose >= 2) {
161         print "@_\n";
162     }
163 }
164
165 sub warning {
166     if ($verbose >= 1) {
167         print "warning: @_\n";
168     }
169 }
170
171 sub scm {
172     my $dir = shift;
173     my $scm = shift;
174     my $pwd;
175
176     if ($dir eq '.') {
177         message "== running $scm @_";
178     } else {
179         message "== $dir: running $scm @_";
180         $pwd = getcwd();
181         chdir($dir);
182     }
183
184     system ($scm, @_) == 0
185         or $ignore_failure
186         or die "$scm failed: $?";
187
188     if ($dir ne '.') {
189         chdir($pwd);
190     }
191 }
192
193 sub scmall {
194     my $command = shift;
195     
196     my $localpath;
197     my $tag;
198     my $remotepath;
199     my $scm;
200     my $line;
201     my $branch_name;
202     my $subcommand;
203
204     my $path;
205     my $wd_before = getcwd;
206
207     my $pwd;
208     my @args;
209
210     my ($repo_base, $checked_out_tree) = getrepo();
211
212     my $is_github_repo = $repo_base =~ m/(git@|git:\/\/|https:\/\/)github.com/;
213
214     parsePackages;
215
216     @args = ();
217
218     if ($command =~ /^remote$/) {
219         while (@_ > 0 && $_[0] =~ /^-/) {
220             push(@args,shift);
221         }
222         if (@_ < 1) { help(); }
223         $subcommand = shift;
224         if ($subcommand ne 'add' && $subcommand ne 'rm' && $subcommand ne 'set-url') {
225             help();
226         }
227         while (@_ > 0 && $_[0] =~ /^-/) {
228             push(@args,shift);
229         }
230         if (($subcommand eq 'add' || $subcommand eq 'rm') && @_ < 1) {
231             help();
232         } elsif (@_ < 1) { # set-url
233             $branch_name = 'origin';
234         } else {
235             $branch_name = shift;
236         }
237     } elsif ($command eq 'new') {
238         if (@_ < 1) {
239             $branch_name = 'origin';
240         } else {
241             $branch_name = shift;
242         }
243     }
244
245     push(@args, @_);
246
247     for $line (@packages) {
248
249         $localpath  = $$line{"localpath"};
250         $tag        = $$line{"tag"};
251         $remotepath = $$line{"remotepath"};
252         $scm        = $$line{"vcs"};
253
254         # Check the SCM is OK as early as possible
255         die "Unknown SCM: $scm" if (($scm ne "darcs") and ($scm ne "git"));
256
257         # We can't create directories on GitHub, so we translate
258         # "package/foo" into "package-foo".
259         if ($is_github_repo) {
260             $remotepath =~ s/\//-/;
261         }
262
263         # Work out the path for this package in the repo we pulled from
264         if ($checked_out_tree) {
265             $path = "$repo_base/$localpath";
266         }
267         else {
268             $path = "$repo_base/$remotepath";
269         }
270
271         if ($command =~ /^(?:g|ge|get)$/) {
272             # Skip any repositories we have not included the tag for
273             if (not defined($tags{$tag})) {
274                 $tags{$tag} = 0;
275             }
276             if ($tags{$tag} == 0) {
277                 next;
278             }
279             
280             if (-d $localpath) {
281                 warning("$localpath already present; omitting")
282                     if $localpath ne ".";
283                 if ($scm eq "git") {
284                     scm ($localpath, $scm, "config", "core.ignorecase", "true");
285                 }
286                 next;
287             }
288
289             # Note that we use "." as the path, as $localpath
290             # doesn't exist yet.
291             if ($scm eq "darcs") {
292                 # The first time round the loop, default the get-mode
293                 if (not defined($get_mode)) {
294                     warning("adding --partial, to override use --complete");
295                     $get_mode = "--partial";
296                 }
297                 scm (".", $scm, "get", $get_mode, $path, $localpath, @args);
298             }
299             else {
300                 scm (".", $scm, "clone", $path, $localpath, @args);
301                 scm ($localpath, $scm, "config", "core.ignorecase", "true");
302             }
303             next;
304         }
305
306         if (-d "$localpath/_darcs") {
307             if (-d "$localpath/.git") {
308                 die "Found both _darcs and .git in $localpath";
309             }
310             $scm = "darcs";
311         } elsif (-d "$localpath/.git") {
312             $scm = "git";
313         } elsif ($tag eq "") {
314             die "Required repo $localpath is missing";
315         } else {
316              message "== $localpath repo not present; skipping";
317              next;
318         }
319
320         # Work out the arguments we should give to the SCM
321         if ($command =~ /^(?:w|wh|wha|what|whats|whatsn|whatsne|whatsnew|status)$/) {
322             if ($scm eq "darcs") {
323                 $command = "whatsnew";
324             }
325             elsif ($scm eq "git") {
326                 $command = "status";
327             }
328             else {
329                 die "Unknown scm";
330             }
331
332             # Hack around 'darcs whatsnew' failing if there are no changes
333             $ignore_failure = 1;
334             scm ($localpath, $scm, $command, @args);
335         }
336         elsif ($command =~ /^commit$/) {
337             # git fails if there is nothing to commit, so ignore failures
338             $ignore_failure = 1;
339             scm ($localpath, $scm, "commit", @args);
340         }
341         elsif ($command =~ /^(?:pus|push)$/) {
342             scm ($localpath, $scm, "push", @args);
343         }
344         elsif ($command =~ /^(?:pul|pull)$/) {
345             scm ($localpath, $scm, "pull", @args);
346         }
347         elsif ($command =~ /^(?:s|se|sen|send)$/) {
348             if ($scm eq "darcs") {
349                 $command = "send";
350             }
351             elsif ($scm eq "git") {
352                 $command = "send-email";
353             }
354             else {
355                 die "Unknown scm";
356             }
357             scm ($localpath, $scm, $command, @args);
358         }
359         elsif ($command =~ /^fetch$/) {
360             scm ($localpath, $scm, "fetch", @args);
361         }
362         elsif ($command =~ /^new$/) {
363             my @scm_args = ("log", "$branch_name..");
364             scm ($localpath, $scm, @scm_args, @args);
365         }
366         elsif ($command =~ /^log$/) {
367             scm ($localpath, $scm, "log", @args);
368         }
369         elsif ($command =~ /^remote$/) {
370             my @scm_args;
371             $ignore_failure = 1;
372             if ($subcommand eq 'add') {
373                 @scm_args = ("remote", "add", $branch_name, $path);
374             } elsif ($subcommand eq 'rm') {
375                 @scm_args = ("remote", "rm", $branch_name);
376             } elsif ($subcommand eq 'set-url') {
377                 @scm_args = ("remote", "set-url", $branch_name, $path);
378             }
379             scm ($localpath, $scm, @scm_args, @args);
380         }
381         elsif ($command =~ /^checkout$/) {
382             # Not all repos are necessarily branched, so ignore failure
383             $ignore_failure = 1;
384             scm ($localpath, $scm, "checkout", @args)
385                 unless $scm eq "darcs";
386         }
387         elsif ($command =~ /^grep$/) {
388             # Hack around 'git grep' failing if there are no matches
389             $ignore_failure = 1;
390             scm ($localpath, $scm, "grep", @args)
391                 unless $scm eq "darcs";
392         }
393         elsif ($command =~ /^clean$/) {
394             scm ($localpath, $scm, "clean", @args)
395                 unless $scm eq "darcs";
396         }
397         elsif ($command =~ /^reset$/) {
398             scm ($localpath, $scm, "reset", @args)
399                 unless $scm eq "darcs";
400         }
401         elsif ($command =~ /^config$/) {
402             scm ($localpath, $scm, "config", @args)
403                 unless $scm eq "darcs";
404         }
405         else {
406             die "Unknown command: $command";
407         }
408     }
409 }
410
411
412 sub help()
413 {
414         # Get the built in help
415         my $help = <<END;
416 What do you want to do?
417 Supported commands:
418
419  * whatsnew
420  * commit
421  * push
422  * pull
423  * get, with options:
424   * --<package-tag>
425   * --complete
426   * --partial
427  * fetch
428  * send
429  * new
430  * remote add <branch-name>
431  * remote rm <branch-name>
432  * remote set-url [--push] <branch-name>
433  * checkout
434  * grep
435  * clean
436  * reset
437  * config
438  * log
439
440 Available package-tags are:
441 END
442
443         # Collect all the tags in the packages file
444         my %available_tags;
445         open IN, "< packages" or die "Can't open packages file";
446         while (<IN>) {
447             chomp;
448             if (/^([^# ]+) +(?:([^ ]+) +)?([^ ]+) +([^ ]+)/) {
449                 if (defined($2) && $2 ne "-") {
450                     $available_tags{$2} = 1;
451                 }
452             }
453             elsif (! /^(#.*)?$/) {
454                 die "Bad line: $_";
455             }
456         }
457         close IN;
458         
459         # Show those tags and the help text
460         my @available_tags = keys %available_tags;
461         print "$help@available_tags\n";
462         exit 1;
463 }
464
465 sub main {
466     if (! -d ".git" || ! -d "compiler") {
467         die "error: sync-all must be run from the top level of the ghc tree."
468     }
469
470     $tags{"-"} = 1;
471     $tags{"dph"} = 1;
472
473     while ($#_ ne -1) {
474         my $arg = shift;
475         # We handle -q here as well as lower down as we need to skip over it
476         # if it comes before the source-control command
477         if ($arg eq "-q") {
478             $verbose = 1;
479         }
480         elsif ($arg eq "-s") {
481             $verbose = 0;
482         }
483         elsif ($arg eq "-r") {
484             $defaultrepo = shift;
485         }
486         elsif ($arg eq "--ignore-failure") {
487             $ignore_failure = 1;
488         }
489         elsif ($arg eq "--complete" || $arg eq "--partial") {
490             $get_mode = $arg;
491         }
492         # Use --checked-out if the remote repos are a checked-out tree,
493         # rather than the master trees.
494         elsif ($arg eq "--checked-out") {
495             $checked_out_flag = 1;
496         }
497         # --<tag> says we grab the libs tagged 'tag' with
498         # 'get'. It has no effect on the other commands.
499         elsif ($arg =~ m/^--no-(.*)$/) {
500             $tags{$1} = 0;
501         }
502         elsif ($arg =~ m/^--(.*)$/) {
503             $tags{$1} = 1;
504         }
505         else {
506             unshift @_, $arg;
507             if (grep /^-q$/, @_) {
508                 $verbose = 1;
509             }
510             last;
511         }
512     }
513
514     if ($#_ eq -1) {
515         help();
516     }
517     else {
518         # Give the command and rest of the arguments to the main loop
519         scmall @_;
520     }
521 }
522
523 END {
524     my $ec = $?;
525     my $pwd = getcwd();
526
527     message "== Checking for old haddock repo";
528     if (-d "utils/haddock/.git") {
529         chdir("utils/haddock");
530         if ((system "git log -1 87e2ca11c3d1b1bc49900fba0b5c5c6f85650718 > /dev/null 2> /dev/null") == 0) {
531             print <<EOF;
532 ============================
533 ATTENTION!
534
535 You have an old haddock repository in your GHC tree!
536
537 Please remove it (e.g. "rm -r utils/haddock"), and then run
538 "./syncs-all get" to get the new repository.
539 ============================
540 EOF
541         }
542         chdir($pwd);
543     }
544
545     message "== Checking for old binary repo";
546     if (-d "libraries/binary/.git") {
547         chdir("libraries/binary");
548         if ((system "git log -1 749ac0efbde3b14901417364a872796598747aaf > /dev/null 2> /dev/null") == 0) {
549             print <<EOF;
550 ============================
551 ATTENTION!
552
553 You have an old binary repository in your GHC tree!
554
555 Please remove it (e.g. "rm -r libraries/binary"), and then run
556 "./syncs-all get" to get the new repository.
557 ============================
558 EOF
559         }
560         chdir($pwd);
561     }
562
563     $? = $ec;
564 }
565
566 main(@ARGV);
567