Remove macOS Sierra results in LayoutTests/platform
[WebKit-https.git] / Tools / Scripts / git-add-reviewer
1 #!/usr/bin/env perl
2 #
3 # Copyright (C) 2011 Apple Inc. All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
8 # 1.  Redistributions of source code must retain the above copyright
9 #     notice, this list of conditions and the following disclaimer.
10 # 2.  Redistributions in binary form must reproduce the above copyright
11 #     notice, this list of conditions and the following disclaimer in the
12 #     documentation and/or other materials provided with the distribution.
13 #
14 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
15 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
18 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
25 use strict;
26 use warnings;
27
28 use File::Basename;
29 use File::Temp ();
30 use Getopt::Long;
31 use POSIX;
32 use IPC::Open2;
33
34 use FindBin;
35 use lib $FindBin::Bin;
36 use webkitdirs;
37 use VCSUtils;
38
39 my $defaultReviewer = "NOBODY";
40
41 sub addReviewer(\%);
42 sub addReviewerToChangeLog($$$);
43 sub addReviewerToCommitMessage($$$);
44 sub changeLogsForCommit($);
45 sub checkout($);
46 sub cherryPick(\%);
47 sub commit(;$);
48 sub getConfigValue($);
49 sub fail(;$);
50 sub head();
51 sub interactive();
52 sub isAncestor($$);
53 sub nonInteractive();
54 sub rebaseOntoHead($$);
55 sub requireCleanWorkTree();
56 sub resetToCommit($);
57 sub toCommit($);
58 sub usage();
59 sub writeCommitMessageToFile($);
60
61
62 my $interactive = 0;
63 my $showHelp = 0;
64 my $rubberStamp = 0;
65
66 my $programName = basename($0);
67 my $usage = <<EOF;
68 Usage: $programName -i|--interactive upstream
69        $programName commit-ish reviewer
70
71 Adds a reviewer to a git commit in a repository with WebKit-style commit logs
72 and ChangeLogs.
73
74 When run in interactive mode, `upstream` specifies the commit after which
75 reviewers should be added.
76
77 When run in non-interactive mode, `commit-ish` specifies the commit to which
78 the `reviewer` will be added.
79
80 Options:
81   -h|--help          Display this message
82   -i|--interactive   Interactive mode
83   -s|--rubber-stamp  Change `Reviewed by` to `Rubber-stamped by`
84 EOF
85
86 my $getOptionsResult = GetOptions(
87     'h|help' => \$showHelp,
88     'i|interactive' => \$interactive,
89     's|rubber-stamp' => \$rubberStamp,
90 );
91
92 my $gitDirectory = gitDirectory();
93
94 usage() if !$getOptionsResult || $showHelp;
95
96 requireCleanWorkTree();
97 $interactive ? interactive() : nonInteractive();
98 exit;
99
100 sub interactive()
101 {
102     @ARGV == 1 or usage();
103
104     my $upstream = toCommit($ARGV[0]);
105     my $head = head();
106
107     isAncestor($upstream, $head) or die "$ARGV[0] is not an ancestor of HEAD.";
108
109     my @revlist = runCommandWithOutput('git', 'rev-list', '--reverse', '--pretty=oneline', "$upstream..");
110     @revlist or die "Couldn't determine revisions";
111
112     my $tempFile = new File::Temp(UNLINK => 1);
113     foreach my $line (@revlist) {
114         print $tempFile "$defaultReviewer : $line";
115     }
116
117     print $tempFile <<EOF;
118
119 # Change 'NOBODY' to the reviewer for each commit
120 #
121 # If any line starts with "rs" followed by one or more spaces, then the phrase
122 # "Reviewed by" is changed to "Rubber-stamped by" in the ChangeLog(s)/commit
123 # message for that commit.
124 #
125 # Commits may be reordered
126 # Omitted commits will be lost
127 EOF
128
129     close $tempFile;
130
131     my $editor = $ENV{GIT_EDITOR} || getConfigValue("core.editor") || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
132     my $result = system "$editor \"" . $tempFile->filename . "\"";
133     !($result >> 8) or die "Error spawning editor.";
134
135     my @todo = ();
136     open TEMPFILE, '<', $tempFile->filename or die "Error opening temp file.";
137     foreach my $line (<TEMPFILE>) {
138         next if $line =~ /^#/;
139         $line =~ /^(rs\s+)?(.*)\s+:\s+([0-9a-fA-F]+)/ or next;
140         push @todo, {rubberstamp => defined $1 && length $1, reviewer => $2, commit => $3};
141     }
142     close TEMPFILE;
143     @todo or die "No revisions specified.";
144
145     foreach my $item (@todo) {
146         $item->{changeLogs} = changeLogsForCommit($item->{commit});
147     }
148
149     $result = system "git", "checkout", $upstream;
150     !($result >> 8) or die "Error checking out $ARGV[0].";
151
152     my $success = 1;
153     foreach my $item (@todo) {
154         $success = cherryPick(%{$item});
155         $success or last;
156         $success = addReviewer(%{$item});
157         $success or last;
158         $success = commit();
159         $success or last;
160     }
161
162     unless ($success) {
163         resetToCommit($head);
164         exit 1;
165     }
166
167     $result = system "git", "branch", "-f", $head;
168     !($result >> 8) or die "Error updating $head.";
169     $result = system "git", "checkout", $head;
170     exit WEXITSTATUS($result >> 8);
171 }
172
173 sub nonInteractive()
174 {
175     @ARGV == 2 or usage();
176
177     my $commit = toCommit($ARGV[0]);
178     my $reviewer = $ARGV[1];
179     my $head = head();
180     my $headCommit = toCommit($head);
181
182     isAncestor($commit, $head) or die "$ARGV[0] is not an ancestor of HEAD.";
183     chomp($reviewer);
184
185     my %item = (
186         reviewer => $reviewer,
187         rubberstamp => $rubberStamp,
188         commit => $commit,
189     );
190
191     $item{changeLogs} = changeLogsForCommit($commit);
192     $item{changeLogs} or die;
193
194     unless ((($commit eq $headCommit) or checkout($commit))
195             && writeCommitMessageToFile("$gitDirectory/MERGE_MSG")
196             && addReviewer(%item)
197             && commit(1)
198             && (($commit eq $headCommit) or rebaseOntoHead($commit, $head))) {
199         resetToCommit($head);
200         exit 1;
201     }
202 }
203
204 sub usage()
205 {
206     print STDERR $usage;
207     exit 1;
208 }
209
210 sub requireCleanWorkTree()
211 {
212     my $result = system("git rev-parse --verify HEAD > /dev/null") >> 8;
213     $result ||= system(qw(git update-index --refresh)) >> 8;
214     $result ||= system(qw(git diff-files --quiet)) >> 8;
215     $result ||= system(qw(git diff-index --cached --quiet HEAD --)) >> 8;
216     !$result or die "Working tree is dirty"
217 }
218
219 sub fail(;$)
220 {
221     my ($message) = @_;
222     print STDERR $message, "\n" if defined $message;
223     return 0;
224 }
225
226 sub cherryPick(\%)
227 {
228     my ($item) = @_;
229
230     my $result = system "git cherry-pick -n $item->{commit} > /dev/null";
231     !($result >> 8) or return fail("Failed to cherry-pick $item->{commit}");
232
233     return 1;
234 }
235
236 sub addReviewer(\%)
237 {
238     my ($item) = @_;
239
240     return 1 if $item->{reviewer} eq $defaultReviewer;
241
242     foreach my $log (@{$item->{changeLogs}}) {
243         addReviewerToChangeLog($item->{reviewer}, $item->{rubberstamp}, $log) or return fail();
244     }
245
246     addReviewerToCommitMessage($item->{reviewer}, $item->{rubberstamp}, "$gitDirectory/MERGE_MSG") or return fail();
247
248     return 1;
249 }
250
251 sub commit(;$)
252 {
253     my ($amend) = @_;
254
255     my @command = qw(git commit -F);
256     push @command, "$gitDirectory/MERGE_MSG";
257     push @command, "--amend" if $amend;
258     my $result = system @command;
259     !($result >> 8) or return fail("Failed to commit revision");
260
261     return 1;
262 }
263
264 sub addReviewerToChangeLog($$$)
265 {
266     my ($reviewer, $rubberstamp, $log) = @_;
267
268     return addReviewerToFile($reviewer, $rubberstamp, $log, 0);
269 }
270
271 sub addReviewerToCommitMessage($$$)
272 {
273     my ($reviewer, $rubberstamp, $log) = @_;
274
275     return addReviewerToFile($reviewer, $rubberstamp, $log, 1);
276 }
277
278 sub addReviewerToFile
279 {
280     my ($reviewer, $rubberstamp, $log, $isCommitMessage) = @_;
281
282     my $tempFile = new File::Temp(UNLINK => 1);
283
284     open LOG, "<", $log or return fail("Couldn't open $log.");
285
286     my $finished = 0;
287     foreach my $line (<LOG>) {
288         if (!$finished && $line =~ /NOBODY \(OOPS!\)/) {
289             $line =~ s/NOBODY \(OOPS!\)/$reviewer/;
290             $line =~ s/Reviewed/Rubber-stamped/ if $rubberstamp;
291             $finished = 1 unless $isCommitMessage;
292         }
293
294         print $tempFile $line;
295     }
296
297     close $tempFile;
298     close LOG or return fail("Couldn't close $log");
299
300     my $result = system "mv", $tempFile->filename, $log;
301     !($result >> 8) or return fail("Failed to rename $tempFile to $log");
302
303     unless ($isCommitMessage) {
304         my $result = system "git", "add", $log;
305         !($result >> 8) or return fail("Failed to git add");
306     }
307
308     return 1;
309 }
310
311 sub head()
312 {
313     my $head = runCommandWithOutput('git', 'symbolic-ref', 'HEAD');
314     $head =~ /^refs\/heads\/(.*)$/ or die "Couldn't determine current branch.";
315     $head = $1;
316
317     return $head;
318 }
319
320 sub isAncestor($$)
321 {
322     my ($ancestor, $descendant) = @_;
323
324     chomp(my $mergeBase = runCommandWithOutput('git', 'merge-base', $ancestor, $descendant));
325     return $mergeBase eq $ancestor;
326 }
327
328 sub toCommit($)
329 {
330     my ($arg) = @_;
331
332     chomp(my $commit = runCommandWithOutput('git', 'rev-parse', $arg));
333     return $commit;
334 }
335
336 sub changeLogsForCommit($)
337 {
338     my ($commit) = @_;
339
340     my @files = runCommandWithOutput('git', 'diff', '-r', '--name-status', "$commit^", "$commit");
341     @files or return fail("Couldn't determine changed files for $commit.");
342
343     my @changeLogs = map { /^[ACMR]\s*(.*)/; makeFilePathRelative($1) } grep { /^[ACMR].*[^-]ChangeLog/ } @files;
344     return \@changeLogs;
345 }
346
347 sub resetToCommit($)
348 {
349     my ($commit) = @_;
350
351     my $result = system "git", "checkout", "-f", $commit;
352     !($result >> 8) or return fail("Error checking out $commit.");
353
354     return 1;
355 }
356
357 sub writeCommitMessageToFile($)
358 {
359     my ($file) = @_;
360
361     open FILE, ">", $file or return fail("Couldn't open $file.");
362     open MESSAGE, "-|", qw(git rev-list --max-count=1 --pretty=format:%B HEAD) or return fail("Error running git rev-list.");
363     my $commitLine = <MESSAGE>;
364     foreach my $line (<MESSAGE>) {
365         print FILE $line;
366     }
367     close MESSAGE;
368     close FILE or return fail("Couldn't close $file.");
369
370     return 1;
371 }
372
373 sub rebaseOntoHead($$)
374 {
375     my ($upstream, $branch) = @_;
376
377     my $result = system qw(git rebase --onto HEAD), $upstream, $branch;
378     !$result or return fail("Couldn't rebase.");
379
380     return 1;
381 }
382
383 sub checkout($)
384 {
385     my ($commit) = @_;
386
387     my $result = system "git", "checkout", $commit;
388     !$result or return fail("Error checking out $commit.");
389
390     return 1;
391 }
392
393 sub getConfigValue($)
394 {
395     my ($variable) = @_;
396
397     chomp(my $value = runCommandWithOutput('git', 'config', '--get', $variable));
398
399     return $value;
400 }
401
402 sub runCommandWithOutput {
403     my ($output, $input);
404
405     my $pid = open2($output, $input, @_);
406
407     waitpid($pid, 0);
408
409     return <$output>;
410 }