Move cursor to corner and fix safari window size before running benchmark
[WebKit-https.git] / Tools / Scripts / resolve-ChangeLogs
1 #!/usr/bin/perl -w
2
3 # Copyright (C) 2007, 2008, 2009 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 #
9 # 1.  Redistributions of source code must retain the above copyright
10 #     notice, this list of conditions and the following disclaimer. 
11 # 2.  Redistributions in binary form must reproduce the above copyright
12 #     notice, this list of conditions and the following disclaimer in the
13 #     documentation and/or other materials provided with the distribution. 
14 # 3.  Neither the name of Apple Inc. ("Apple") nor the names of
15 #     its contributors may be used to endorse or promote products derived
16 #     from this software without specific prior written permission. 
17 #
18 # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 # Merge and resolve ChangeLog conflicts for svn and git repositories
30
31 use strict;
32
33 use FindBin;
34 use lib $FindBin::Bin;
35
36 use File::Basename;
37 use File::Copy;
38 use File::Path;
39 use File::Spec;
40 use Getopt::Long;
41 use POSIX;
42 use VCSUtils;
43
44 sub canonicalRelativePath($);
45 sub conflictFiles($);
46 sub findChangeLog($);
47 sub findUnmergedChangeLogs();
48 sub fixMergedChangeLogs($;@);
49 sub fixOneMergedChangeLog($);
50 sub hasGitUnmergedFiles();
51 sub isInGitFilterBranch();
52 sub parseFixMerged($$;$);
53 sub removeChangeLogArguments($);
54 sub resolveChangeLog($);
55 sub resolveConflict($);
56 sub showStatus($;$);
57 sub usageAndExit();
58
59 my $isGit = isGit();
60 my $isSVN = isSVN();
61
62 my $SVN = "svn";
63 my $GIT = "git";
64
65 my $fixMerged;
66 my $gitRebaseContinue = 0;
67 my $mergeDriver = 0;
68 my $printWarnings = 1;
69 my $showHelp;
70
71 sub usageAndExit()
72 {
73     print STDERR <<__END__;
74 Usage: @{[ basename($0) ]} [options] [path/to/ChangeLog] [path/to/another/ChangeLog ...]
75   -c|--[no-]continue               run "git rebase --continue" after fixing ChangeLog
76                                    entries (default: --no-continue)
77   -f|--fix-merged [revision-range] fix git-merged ChangeLog entries; if a revision-range
78                                    is specified, run git filter-branch on the range
79   -m|--merge-driver %O %A %B       act as a git merge-driver on files %O %A %B
80   -h|--help                        show this help message
81   -w|--[no-]warnings               show or suppress warnings (default: show warnings)
82 __END__
83     exit 1;
84 }
85
86 my $getOptionsResult = GetOptions(
87     'c|continue!'     => \$gitRebaseContinue,
88     'f|fix-merged:s'  => \&parseFixMerged,
89     'm|merge-driver!' => \$mergeDriver,
90     'h|help'          => \$showHelp,
91     'w|warnings!'     => \$printWarnings,
92 );
93
94 if (!$getOptionsResult || $showHelp) {
95     usageAndExit();
96 }
97
98 my $relativePath = isInGitFilterBranch() ? '.' : chdirReturningRelativePath(determineVCSRoot());
99
100 my @changeLogFiles = removeChangeLogArguments($relativePath);
101
102 if (!defined $fixMerged && !$mergeDriver && scalar(@changeLogFiles) == 0) {
103     @changeLogFiles = findUnmergedChangeLogs();
104 }
105
106 if (!$mergeDriver && scalar(@ARGV) > 0) {
107     print STDERR "ERROR: Files listed on command-line that are not ChangeLogs.\n";
108     undef $getOptionsResult;
109 } elsif (!defined $fixMerged && !$mergeDriver && scalar(@changeLogFiles) == 0) {
110     print STDERR "ERROR: No ChangeLog files listed on command-line or found unmerged.\n";
111     undef $getOptionsResult;
112 } elsif ($gitRebaseContinue && !$isGit) {
113     print STDERR "ERROR: --continue may only be used with a git repository\n";
114     undef $getOptionsResult;
115 } elsif (defined $fixMerged && !$isGit) {
116     print STDERR "ERROR: --fix-merged may only be used with a git repository\n";
117     undef $getOptionsResult;
118 } elsif ($mergeDriver && !$isGit) {
119     print STDERR "ERROR: --merge-driver may only be used with a git repository\n";
120     undef $getOptionsResult;
121 } elsif ($mergeDriver && scalar(@ARGV) < 3) {
122     print STDERR "ERROR: --merge-driver expects %O %A %B as arguments\n";
123     undef $getOptionsResult;
124 }
125
126 if (!$getOptionsResult) {
127     usageAndExit();
128 }
129
130 if (defined $fixMerged && length($fixMerged) > 0) {
131     my $commitRange = $fixMerged;
132     $commitRange = $commitRange . "..HEAD" if index($commitRange, "..") < 0;
133     fixMergedChangeLogs($commitRange, @changeLogFiles);
134 } elsif ($mergeDriver) {
135     my ($base, $theirs, $ours) = @ARGV;
136     if (mergeChangeLogs($ours, $base, $theirs)) {
137         unlink($ours);
138         copy($theirs, $ours) or die $!;
139     } else {
140         exec qw(git merge-file -L THEIRS -L BASE -L OURS), $theirs, $base, $ours;
141     }
142 } elsif (@changeLogFiles) {
143     for my $file (@changeLogFiles) {
144         if (defined $fixMerged) {
145             fixOneMergedChangeLog($file);
146         } else {
147             resolveChangeLog($file);
148         }
149     }
150 } else {
151     print STDERR "ERROR: Unknown combination of switches and arguments.\n";
152     usageAndExit();
153 }
154
155 if ($gitRebaseContinue) {
156     if (hasGitUnmergedFiles()) {
157         print "Unmerged files; skipping '$GIT rebase --continue'.\n";
158     } else {
159         print "Running '$GIT rebase --continue'...\n";
160         print `$GIT rebase --continue`;
161     }
162 }
163
164 exit 0;
165
166 sub canonicalRelativePath($)
167 {
168     my ($originalPath) = @_;
169     my $absolutePath = Cwd::abs_path($originalPath);
170     return File::Spec->abs2rel($absolutePath, Cwd::getcwd());
171 }
172
173 sub conflictFiles($)
174 {
175     my ($file) = @_;
176     my $fileMine;
177     my $fileOlder;
178     my $fileNewer;
179
180     if (-e $file && -e "$file.orig" && -e "$file.rej") {
181         return ("$file.rej", "$file.orig", $file);
182     }
183
184     if ($isSVN) {
185         my $escapedFile = escapeSubversionPath($file);
186         open STAT, "-|", $SVN, "status", $escapedFile or die $!;
187         my $status = <STAT>;
188         close STAT;
189         if (!$status || $status !~ m/^C\s+/) {
190             print STDERR "WARNING: ${file} is not in a conflicted state.\n" if $printWarnings;
191             return ();
192         }
193
194         $fileMine = "${file}.mine" if -e "${file}.mine";
195
196         my $currentRevision;
197         open INFO, "-|", $SVN, "info", $escapedFile or die $!;
198         while (my $line = <INFO>) {
199             if ($line =~ m/^Revision: ([0-9]+)/) {
200                 $currentRevision = $1;
201                 { local $/ = undef; <INFO>; }  # Consume rest of input.
202             }
203         }
204         close INFO;
205         $fileNewer = "${file}.r${currentRevision}" if -e "${file}.r${currentRevision}";
206
207         my @matchingFiles = grep { $_ ne $fileNewer } glob("${file}.r[0-9][0-9]*");
208         if (scalar(@matchingFiles) > 1) {
209             print STDERR "WARNING: Too many conflict files exist for ${file}!\n" if $printWarnings;
210         } else {
211             $fileOlder = shift @matchingFiles;
212         }
213     } elsif ($isGit) {
214         my $gitPrefix = `$GIT rev-parse --show-prefix`;
215         chomp $gitPrefix;
216         open GIT, "-|", $GIT, "ls-files", "--unmerged", $file or die $!;
217         while (my $line = <GIT>) {
218             my ($mode, $hash, $stage, $fileName) = split(' ', $line);
219             my $outputFile;
220             if ($stage == 1) {
221                 $fileOlder = "${file}.BASE.$$";
222                 $outputFile = $fileOlder;
223             } elsif ($stage == 2) {
224                 $fileNewer = "${file}.LOCAL.$$";
225                 $outputFile = $fileNewer;
226             } elsif ($stage == 3) {
227                 $fileMine = "${file}.REMOTE.$$";
228                 $outputFile = $fileMine;
229             } else {
230                 die "Unknown file stage: $stage";
231             }
232             system("$GIT cat-file blob :${stage}:${gitPrefix}${file} > $outputFile");
233             die $! if WEXITSTATUS($?);
234         }
235         close GIT or die $!;
236     } else {
237         die "Unknown version control system";
238     }
239
240     if (!$fileMine && !$fileOlder && !$fileNewer) {
241         print STDERR "WARNING: ${file} does not need merging.\n" if $printWarnings;
242     } elsif (!$fileMine || !$fileOlder || !$fileNewer) {
243         print STDERR "WARNING: ${file} is missing some conflict files.\n" if $printWarnings;
244     }
245
246     return ($fileMine, $fileOlder, $fileNewer);
247 }
248
249 sub findChangeLog($)
250 {
251     return $_[0] if basename($_[0]) eq "ChangeLog";
252
253     my $file = File::Spec->catfile($_[0], "ChangeLog");
254     return $file if -d $_[0] and -e $file;
255
256     return undef;
257 }
258
259 sub findUnmergedChangeLogs()
260 {
261     my $statCommand = "";
262
263     if ($isSVN) {
264         $statCommand = "$SVN stat | grep '^C'";
265     } elsif ($isGit) {
266         $statCommand = "$GIT diff -r --name-status --diff-filter=U -C -C -M";
267     } else {
268         return ();
269     }
270
271     my @results = ();
272     open STAT, "-|", $statCommand or die "The status failed: $!.\n";
273     while (<STAT>) {
274         if ($isSVN) {
275             my $matches;
276             my $file;
277             if (isSVNVersion16OrNewer()) {
278                 $matches = /^([C]).{6} (.+?)[\r\n]*$/;
279                 $file = $2;
280             } else {
281                 $matches = /^([C]).{5} (.+?)[\r\n]*$/;
282                 $file = $2;
283             }
284             if ($matches) {
285                 $file = findChangeLog(normalizePath($file));
286                 push @results, $file if $file;
287             } else {
288                 print;  # error output from svn stat
289             }
290         } elsif ($isGit) {
291             if (/^([U])\t(.+)$/) {
292                 my $file = findChangeLog(normalizePath($2));
293                 push @results, $file if $file;
294             } else {
295                 print;  # error output from git diff
296             }
297         }
298     }
299     close STAT;
300
301     return @results;
302 }
303
304 sub fixMergedChangeLogs($;@)
305 {
306     my $revisionRange = shift;
307     my @changedFiles = @_;
308
309     if (scalar(@changedFiles) < 1) {
310         # Read in list of files changed in $revisionRange
311         open GIT, "-|", $GIT, "diff", "--name-only", $revisionRange or die $!;
312         push @changedFiles, <GIT>;
313         close GIT or die $!;
314         die "No changed files in $revisionRange" if scalar(@changedFiles) < 1;
315         chomp @changedFiles;
316     }
317
318     my @changeLogs = grep { defined $_ } map { findChangeLog($_) } @changedFiles;
319     die "No changed ChangeLog files in $revisionRange" if scalar(@changeLogs) < 1;
320
321     system("$GIT filter-branch --tree-filter 'PREVIOUS_COMMIT=\`$GIT rev-parse \$GIT_COMMIT^\` && MAPPED_PREVIOUS_COMMIT=\`map \$PREVIOUS_COMMIT\` \"$0\" -f \"" . join('" "', @changeLogs) . "\"' $revisionRange");
322
323     # On success, remove the backup refs directory
324     if (WEXITSTATUS($?) == 0) {
325         rmtree(qw(.git/refs/original));
326     }
327 }
328
329 sub fixOneMergedChangeLog($)
330 {
331     my $file = shift;
332     my $patch;
333
334     # Read in patch for incorrectly merged ChangeLog entry
335     {
336         local $/ = undef;
337         open GIT, "-|", $GIT, "diff", ($ENV{GIT_COMMIT} || "HEAD") . "^", $file or die $!;
338         $patch = <GIT>;
339         close GIT or die $!;
340     }
341
342     # Always checkout the previous commit's copy of the ChangeLog
343     system($GIT, "checkout", $ENV{MAPPED_PREVIOUS_COMMIT} || "HEAD^", $file);
344     die $! if WEXITSTATUS($?);
345
346     # The patch must have 0 or more lines of context, then 1 or more lines
347     # of additions, and then 1 or more lines of context.  If not, we skip it.
348     if ($patch =~ /\n@@ -(\d+),(\d+) \+(\d+),(\d+) @@\n( .*\n)*((\+.*\n)+)( .*\n)+$/m) {
349         # Copy the header from the original patch.
350         my $newPatch = substr($patch, 0, index($patch, "@@ -${1},${2} +${3},${4} @@"));
351
352         # Generate a new set of line numbers and patch lengths.  Our new
353         # patch will start with the lines for the fixed ChangeLog entry,
354         # then have 3 lines of context from the top of the current file to
355         # make the patch apply cleanly.
356         $newPatch .= "@@ -1,3 +1," . ($4 - $2 + 3) . " @@\n";
357
358         # We assume that top few lines of the ChangeLog entry are actually
359         # at the bottom of the list of added lines (due to the way the patch
360         # algorithm works), so we simply search through the lines until we
361         # find the date line, then move the rest of the lines to the top.
362         my @patchLines = map { $_ . "\n" } split(/\n/, $6);
363         foreach my $i (0 .. $#patchLines) {
364             if ($patchLines[$i] =~ /^\+\d{4}-\d{2}-\d{2}  /) {
365                 unshift(@patchLines, splice(@patchLines, $i, scalar(@patchLines) - $i));
366                 last;
367             }
368         }
369
370         $newPatch .= join("", @patchLines);
371
372         # Add 3 lines of context to the end
373         open FILE, "<", $file or die $!;
374         for (my $i = 0; $i < 3; $i++) {
375             $newPatch .= " " . <FILE>;
376         }
377         close FILE;
378
379         # Apply the new patch
380         open(PATCH, "| patch -p1 $file > " . File::Spec->devnull()) or die $!;
381         print PATCH $newPatch;
382         close(PATCH) or die $!;
383
384         # Run "git add" on the fixed ChangeLog file
385         system($GIT, "add", $file);
386         die $! if WEXITSTATUS($?);
387
388         showStatus($file, 1);
389     } elsif ($patch) {
390         # Restore the current copy of the ChangeLog file since we can't repatch it
391         system($GIT, "checkout", $ENV{GIT_COMMIT} || "HEAD", $file);
392         die $! if WEXITSTATUS($?);
393         print STDERR "WARNING: Last change to ${file} could not be fixed and re-merged.\n" if $printWarnings;
394     }
395 }
396
397 sub hasGitUnmergedFiles()
398 {
399     my $output = `$GIT ls-files --unmerged`;
400     return $output ne "";
401 }
402
403 sub isInGitFilterBranch()
404 {
405     return exists $ENV{MAPPED_PREVIOUS_COMMIT} && $ENV{MAPPED_PREVIOUS_COMMIT};
406 }
407
408 sub parseFixMerged($$;$)
409 {
410     my ($switchName, $key, $value) = @_;
411     if (defined $key) {
412         if (defined findChangeLog($key)) {
413             unshift(@ARGV, $key);
414             $fixMerged = "";
415         } else {
416             $fixMerged = $key;
417         }
418     } else {
419         $fixMerged = "";
420     }
421 }
422
423 sub removeChangeLogArguments($)
424 {
425     my ($baseDir) = @_;
426     my @results = ();
427
428     for (my $i = 0; $i < scalar(@ARGV); ) {
429         my $file = findChangeLog(canonicalRelativePath(File::Spec->catfile($baseDir, $ARGV[$i])));
430         if (defined $file) {
431             splice(@ARGV, $i, 1);
432             push @results, $file;
433         } else {
434             $i++;
435         }
436     }
437
438     return @results;
439 }
440
441 sub resolveChangeLog($)
442 {
443     my ($file) = @_;
444
445     my ($fileMine, $fileOlder, $fileNewer) = conflictFiles($file);
446
447     return unless $fileMine && $fileOlder && $fileNewer;
448
449     if (mergeChangeLogs($fileMine, $fileOlder, $fileNewer)) {
450         if ($file ne $fileNewer) {
451             unlink($file);
452             rename($fileNewer, $file) or die $!;
453         }
454         unlink($fileMine, $fileOlder);
455         resolveConflict($file);
456         showStatus($file, 1);
457     } else {
458         showStatus($file);
459         print STDERR "WARNING: ${file} could not be merged using fuzz level 3.\n" if $printWarnings;
460         unlink($fileMine, $fileOlder, $fileNewer) if $isGit;
461     }
462 }
463
464 sub resolveConflict($)
465 {
466     my ($file) = @_;
467
468     if ($isSVN) {
469         my $escapedFile = escapeSubversionPath($file);
470         system($SVN, "resolved", $escapedFile);
471         die $! if WEXITSTATUS($?);
472     } elsif ($isGit) {
473         system($GIT, "add", $file);
474         die $! if WEXITSTATUS($?);
475     } else {
476         die "Unknown version control system";
477     }
478 }
479
480 sub showStatus($;$)
481 {
482     my ($file, $isConflictResolved) = @_;
483
484     if ($isSVN) {
485         my $escapedFile = escapeSubversionPath($file);
486         system($SVN, "status", $escapedFile);
487     } elsif ($isGit) {
488         my @args = qw(--name-status);
489         unshift @args, qw(--cached) if $isConflictResolved;
490         system($GIT, "diff", @args, $file);
491     } else {
492         die "Unknown version control system";
493     }
494 }
495