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