2008-11-04 Anders Carlsson <andersca@apple.com>
[webkit/qt.git] / WebKitTools / Scripts / resolve-ChangeLogs
blob58471ec379a97065fa1a2e3aac03c57a4579ae0c
1 #!/usr/bin/perl -w
3 # Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
5 # Redistribution and use in source and binary forms, with or without
6 # modification, are permitted provided that the following conditions
7 # are met:
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 Computer, 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.
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.
29 # Merge and resolve ChangeLog conflicts for svn and git repositories
31 use strict;
33 use FindBin;
34 use lib $FindBin::Bin;
36 use File::Basename;
37 use File::Path;
38 use File::Spec;
39 use Getopt::Long;
40 use POSIX;
41 use VCSUtils;
43 sub conflictFiles($);
44 sub findChangeLog($);
45 sub fixChangeLogPatch($);
46 sub fixMergedChangeLogs($;@);
47 sub fixOneMergedChangeLog($);
48 sub mergeChanges($$$);
49 sub parseFixMerged($$;$);
50 sub removeChangeLogArguments();
51 sub resolveChangeLog($);
52 sub resolveConflict($);
53 sub showStatus($;$);
54 sub usageAndExit();
56 my $SVN = "svn";
57 my $GIT = "git";
59 my $fixMerged;
60 my $printWarnings = 1;
61 my $showHelp;
63 my $getOptionsResult = GetOptions(
64 'f|fix-merged:s' => \&parseFixMerged,
65 'h|help' => \$showHelp,
66 'w|warnings!' => \$printWarnings,
69 my @changeLogFiles = removeChangeLogArguments();
71 if (scalar(@ARGV) > 0) {
72 print STDERR "ERROR: Files listed on command-line that are not ChangeLogs.\n";
73 undef $getOptionsResult;
74 } elsif (!defined $fixMerged && scalar(@changeLogFiles) == 0) {
75 print STDERR "ERROR: No ChangeLog files listed on command-line.\n";
76 undef $getOptionsResult;
77 } elsif (defined $fixMerged && !isGit()) {
78 print STDERR "ERROR: --fix-merged may only be used with a git repository\n";
79 undef $getOptionsResult;
82 sub usageAndExit()
84 print STDERR <<__END__;
85 Usage: @{[ basename($0) ]} [options] path/to/ChangeLog [path/to/another/ChangeLog ...]
86 -f|--fix-merged [revision-range] fix git-merged ChangeLog entries; if a revision-range
87 is specified, run git filter-branch on the range
88 -h|--help show this help message
89 -w|--[no-]warnings show or suppress warnings (default: show warnings)
90 __END__
91 exit 1;
94 if (!$getOptionsResult || $showHelp) {
95 usageAndExit();
98 if (defined $fixMerged && length($fixMerged) > 0) {
99 my $commitRange = $fixMerged;
100 $commitRange = $commitRange . "..HEAD" if index($commitRange, "..") < 0;
101 fixMergedChangeLogs($commitRange, @changeLogFiles);
102 } elsif (@changeLogFiles) {
103 for my $file (@changeLogFiles) {
104 if (defined $fixMerged) {
105 fixOneMergedChangeLog($file);
106 } else {
107 resolveChangeLog($file);
110 } else {
111 print STDERR "ERROR: Unknown combination of switches and arguments.\n";
112 usageAndExit();
115 exit 0;
117 sub conflictFiles($)
119 my ($file) = @_;
120 my $fileMine;
121 my $fileOlder;
122 my $fileNewer;
124 if (-e $file && -e "$file.orig" && -e "$file.rej") {
125 return ("$file.rej", "$file.orig", $file);
128 if (isSVN()) {
129 open STAT, "-|", $SVN, "status", $file || die;
130 my $status = <STAT>;
131 close STAT;
132 if (!$status || $status !~ m/^C\s+/) {
133 print STDERR "WARNING: ${file} is not in a conflicted state.\n" if $printWarnings;
134 return ();
137 $fileMine = "${file}.mine" if -e "${file}.mine";
139 my $currentRevision;
140 open INFO, "-|", $SVN, "info", $file || die;
141 while (my $line = <INFO>) {
142 $currentRevision = $1 if $line =~ m/^Revision: ([0-9]+)/;
144 close INFO;
145 $fileNewer = "${file}.r${currentRevision}" if -e "${file}.r${currentRevision}";
147 my @matchingFiles = grep { $_ ne $fileNewer } glob("${file}.r[0-9][0-9]*");
148 if (scalar(@matchingFiles) > 1) {
149 print STDERR "WARNING: Too many conflict files exist for ${file}!\n" if $printWarnings;
150 } else {
151 $fileOlder = shift @matchingFiles;
153 } elsif (isGit()) {
154 my $gitPrefix = `$GIT rev-parse --show-prefix`;
155 chomp $gitPrefix;
156 open GIT, "-|", $GIT, "ls-files", "--unmerged", $file || die;
157 while (my $line = <GIT>) {
158 my ($mode, $hash, $stage, $fileName) = split(' ', $line);
159 my $outputFile;
160 if ($stage == 1) {
161 $fileOlder = "${file}.BASE.$$";
162 $outputFile = $fileOlder;
163 } elsif ($stage == 2) {
164 $fileNewer = "${file}.LOCAL.$$";
165 $outputFile = $fileNewer;
166 } elsif ($stage == 3) {
167 $fileMine = "${file}.REMOTE.$$";
168 $outputFile = $fileMine;
169 } else {
170 die "Unknown file stage: $stage";
172 system("$GIT cat-file blob :${stage}:${gitPrefix}${file} > $outputFile");
174 close GIT;
175 } else {
176 die "Unknown version control system";
179 if (!$fileMine && !$fileOlder && !$fileNewer) {
180 print STDERR "WARNING: ${file} does not need merging.\n" if $printWarnings;
181 } elsif (!$fileMine || !$fileOlder || !$fileNewer) {
182 print STDERR "WARNING: ${file} is missing some conflict files.\n" if $printWarnings;
185 return ($fileMine, $fileOlder, $fileNewer);
188 sub findChangeLog($) {
189 return $_[0] if basename($_[0]) eq "ChangeLog";
191 my $file = File::Spec->catfile($_[0], "ChangeLog");
192 return $file if -d $_[0] and -e $file;
194 return undef;
197 sub fixChangeLogPatch($)
199 my $patch = shift;
200 my $contextLineCount = 3;
202 return $patch if $patch !~ /\n@@ -1,(\d+) \+1,(\d+) @@\n( .*\n)+(\+.*\n)+( .*\n){$contextLineCount}$/m;
203 my ($oldLineCount, $newLineCount) = ($1, $2);
204 return $patch if $oldLineCount <= $contextLineCount;
206 # The diff(1) command is greedy when matching lines, so a new ChangeLog entry will
207 # have lines of context at the top of a patch when the existing entry has the same
208 # date and author as the new entry. This nifty loop alters a ChangeLog patch so
209 # that the added lines ("+") in the patch always start at the beginning of the
210 # patch and there are no initial lines of context.
211 my $newPatch;
212 my $lineCountInState = 0;
213 my $oldContentLineCountReduction = $oldLineCount - $contextLineCount;
214 my $newContentLineCountWithoutContext = $newLineCount - $oldLineCount - $oldContentLineCountReduction;
215 my ($stateHeader, $statePreContext, $stateNewChanges, $statePostContext) = (1..4);
216 my $state = $stateHeader;
217 foreach my $line (split(/\n/, $patch)) {
218 $lineCountInState++;
219 if ($state == $stateHeader && $line =~ /^@@ -1,$oldLineCount \+1,$newLineCount @\@$/) {
220 $line = "@@ -1,$contextLineCount +1," . ($newLineCount - $oldContentLineCountReduction) . " @@";
221 $lineCountInState = 0;
222 $state = $statePreContext;
223 } elsif ($state == $statePreContext && substr($line, 0, 1) eq " ") {
224 $line = "+" . substr($line, 1);
225 if ($lineCountInState == $oldContentLineCountReduction) {
226 $lineCountInState = 0;
227 $state = $stateNewChanges;
229 } elsif ($state == $stateNewChanges && substr($line, 0, 1) eq "+") {
230 # No changes to these lines
231 if ($lineCountInState == $newContentLineCountWithoutContext) {
232 $lineCountInState = 0;
233 $state = $statePostContext;
235 } elsif ($state == $statePostContext) {
236 if (substr($line, 0, 1) eq "+" && $lineCountInState <= $oldContentLineCountReduction) {
237 $line = " " . substr($line, 1);
238 } elsif ($lineCountInState > $contextLineCount && substr($line, 0, 1) eq " ") {
239 next; # Discard
242 $newPatch .= $line . "\n";
245 return $newPatch;
248 sub fixMergedChangeLogs($;@)
250 my $revisionRange = shift;
251 my @changedFiles = @_;
253 if (scalar(@changedFiles) < 1) {
254 # Read in list of files changed in $revisionRange
255 open GIT, "-|", $GIT, "diff", "--name-only", $revisionRange || die;
256 push @changedFiles, <GIT>;
257 close GIT || die;
258 die "No changed files in $revisionRange" if scalar(@changedFiles) < 1;
259 chomp @changedFiles;
262 my @changeLogs = grep { defined $_ } map { findChangeLog($_) } @changedFiles;
263 die "No changed ChangeLog files in $revisionRange" if scalar(@changeLogs) < 1;
265 system("$GIT filter-branch --tree-filter 'PREVIOUS_COMMIT=\`$GIT rev-parse \$GIT_COMMIT^\` && MAPPED_PREVIOUS_COMMIT=\`map \$PREVIOUS_COMMIT\` $0 -f \"" . join('" "', @changeLogs) . "\"' $revisionRange");
267 # On success, remove the backup refs directory
268 if (WEXITSTATUS($?) == 0) {
269 rmtree(qw(.git/refs/original));
273 sub fixOneMergedChangeLog($)
275 my $file = shift;
276 my $patch;
278 # Read in patch for incorrectly merged ChangeLog entry
280 local $/ = undef;
281 open GIT, "-|", $GIT, "diff", ($ENV{GIT_COMMIT} || "HEAD") . "^", $file || die;
282 $patch = <GIT>;
283 close GIT || die;
286 # Always checkout the previous commit's copy of the ChangeLog
287 system($GIT, "checkout", $ENV{MAPPED_PREVIOUS_COMMIT} || "HEAD^", $file);
289 # The patch must have 0 or more lines of context, then 1 or more lines
290 # of additions, and then 1 or more lines of context. If not, we skip it.
291 if ($patch =~ /\n@@ -(\d+),(\d+) \+(\d+),(\d+) @@\n( .*\n)*((\+.*\n)+)( .*\n)+$/m) {
292 # Copy the header from the original patch.
293 my $newPatch = substr($patch, 0, index($patch, "@@ -${1},${2} +${3},${4} @@"));
295 # Generate a new set of line numbers and patch lengths. Our new
296 # patch will start with the lines for the fixed ChangeLog entry,
297 # then have 3 lines of context from the top of the current file to
298 # make the patch apply cleanly.
299 $newPatch .= "@@ -1,3 +1," . ($4 - $2 + 3) . " @@\n";
301 # We assume that top few lines of the ChangeLog entry are actually
302 # at the bottom of the list of added lines (due to the way the patch
303 # algorithm works), so we simply search through the lines until we
304 # find the date line, then move the rest of the lines to the top.
305 my @patchLines = map { $_ . "\n" } split(/\n/, $6);
306 foreach my $i (0 .. $#patchLines) {
307 if ($patchLines[$i] =~ /^\+\d{4}-\d{2}-\d{2} /) {
308 unshift(@patchLines, splice(@patchLines, $i, scalar(@patchLines) - $i));
309 last;
313 $newPatch .= join("", @patchLines);
315 # Add 3 lines of context to the end
316 open FILE, "<", $file || die;
317 for (my $i = 0; $i < 3; $i++) {
318 $newPatch .= " " . <FILE>;
320 close FILE;
322 # Apply the new patch
323 open(PATCH, "| patch -p1 $file > /dev/null") || die;
324 print PATCH $newPatch;
325 close(PATCH) || die;
327 # Run "git add" on the fixed ChangeLog file
328 system($GIT, "add", $file);
330 showStatus($file, 1);
331 } elsif ($patch) {
332 # Restore the current copy of the ChangeLog file since we can't repatch it
333 system($GIT, "checkout", $ENV{GIT_COMMIT} || "HEAD", $file);
334 print STDERR "WARNING: Last change to ${file} could not be fixed and re-merged.\n" if $printWarnings;
338 sub mergeChanges($$$)
340 my ($fileMine, $fileOlder, $fileNewer) = @_;
342 my $traditionalReject = $fileMine =~ /\.rej$/ ? 1 : 0;
344 local $/ = undef;
346 my $patch;
347 if ($traditionalReject) {
348 open(DIFF, "<", $fileMine);
349 $patch = <DIFF>;
350 close(DIFF);
351 rename($fileMine, "$fileMine.save");
352 rename($fileOlder, "$fileOlder.save");
353 } else {
354 open(DIFF, "-|", qw(diff -u), $fileOlder, $fileMine) || die;
355 $patch = <DIFF>;
356 close(DIFF);
359 unlink("${fileNewer}.orig");
360 unlink("${fileNewer}.rej");
362 open(PATCH, "| patch --fuzz=3 $fileNewer > /dev/null") || die;
363 print PATCH fixChangeLogPatch($patch);
364 close(PATCH);
366 my $result;
368 # Refuse to merge the patch if it did not apply cleanly
369 if (-e "${fileNewer}.rej") {
370 unlink("${fileNewer}.rej");
371 unlink($fileNewer);
372 rename("${fileNewer}.orig", $fileNewer);
373 $result = 0;
374 } else {
375 unlink("${fileNewer}.orig");
376 $result = 1;
379 if ($traditionalReject) {
380 rename("$fileMine.save", $fileMine);
381 rename("$fileOlder.save", $fileOlder);
384 return $result;
387 sub parseFixMerged($$;$)
389 my ($switchName, $key, $value) = @_;
390 if (defined $key) {
391 if (defined findChangeLog($key)) {
392 unshift(@ARGV, $key);
393 $fixMerged = "";
394 } else {
395 $fixMerged = $key;
397 } else {
398 $fixMerged = "";
402 sub removeChangeLogArguments()
404 my @results = ();
406 for (my $i = 0; $i < scalar(@ARGV); ) {
407 my $file = findChangeLog($ARGV[$i]);
408 if (defined $file) {
409 splice(@ARGV, $i, 1);
410 push @results, $file;
411 } else {
412 $i++;
416 return @results;
419 sub resolveChangeLog($)
421 my ($file) = @_;
423 my ($fileMine, $fileOlder, $fileNewer) = conflictFiles($file);
425 return unless $fileMine && $fileOlder && $fileNewer;
427 if (mergeChanges($fileMine, $fileOlder, $fileNewer)) {
428 if ($file ne $fileNewer) {
429 unlink($file);
430 rename($fileNewer, $file) || die;
432 unlink($fileMine, $fileOlder);
433 resolveConflict($file);
434 showStatus($file, 1);
435 } else {
436 showStatus($file);
437 print STDERR "WARNING: ${file} could not be merged using fuzz level 3.\n" if $printWarnings;
438 unlink($fileMine, $fileOlder, $fileNewer) if isGit();
442 sub resolveConflict($)
444 my ($file) = @_;
446 if (isSVN()) {
447 system($SVN, "resolved", $file);
448 } elsif (isGit()) {
449 system($GIT, "add", $file);
450 } else {
451 die "Unknown version control system";
455 sub showStatus($;$)
457 my ($file, $isConflictResolved) = @_;
459 if (isSVN()) {
460 system($SVN, "status", $file);
461 } elsif (isGit()) {
462 my @args = qw(--name-status);
463 unshift @args, qw(--cached) if $isConflictResolved;
464 system($GIT, "diff", @args, $file);
465 } else {
466 die "Unknown version control system";