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