diff -r 000000000000 -r 4f2f89ce4247 WebKitTools/Scripts/resolve-ChangeLogs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebKitTools/Scripts/resolve-ChangeLogs Fri Sep 17 09:02:29 2010 +0300 @@ -0,0 +1,488 @@ +#!/usr/bin/perl -w + +# Copyright (C) 2007, 2008, 2009 Apple Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of +# its contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Merge and resolve ChangeLog conflicts for svn and git repositories + +use strict; + +use FindBin; +use lib $FindBin::Bin; + +use File::Basename; +use File::Copy; +use File::Path; +use File::Spec; +use Getopt::Long; +use POSIX; +use VCSUtils; + +sub canonicalRelativePath($); +sub conflictFiles($); +sub findChangeLog($); +sub findUnmergedChangeLogs(); +sub fixMergedChangeLogs($;@); +sub fixOneMergedChangeLog($); +sub hasGitUnmergedFiles(); +sub isInGitFilterBranch(); +sub parseFixMerged($$;$); +sub removeChangeLogArguments($); +sub resolveChangeLog($); +sub resolveConflict($); +sub showStatus($;$); +sub usageAndExit(); + +my $isGit = isGit(); +my $isSVN = isSVN(); + +my $SVN = "svn"; +my $GIT = "git"; + +my $fixMerged; +my $gitRebaseContinue = 0; +my $mergeDriver = 0; +my $printWarnings = 1; +my $showHelp; + +my $getOptionsResult = GetOptions( + 'c|continue!' => \$gitRebaseContinue, + 'f|fix-merged:s' => \&parseFixMerged, + 'm|merge-driver!' => \$mergeDriver, + 'h|help' => \$showHelp, + 'w|warnings!' => \$printWarnings, +); + +my $relativePath = isInGitFilterBranch() ? '.' : chdirReturningRelativePath(determineVCSRoot()); + +my @changeLogFiles = removeChangeLogArguments($relativePath); + +if (!defined $fixMerged && !$mergeDriver && scalar(@changeLogFiles) == 0) { + @changeLogFiles = findUnmergedChangeLogs(); +} + +if (!$mergeDriver && scalar(@ARGV) > 0) { + print STDERR "ERROR: Files listed on command-line that are not ChangeLogs.\n"; + undef $getOptionsResult; +} elsif (!defined $fixMerged && !$mergeDriver && scalar(@changeLogFiles) == 0) { + print STDERR "ERROR: No ChangeLog files listed on command-line or found unmerged.\n"; + undef $getOptionsResult; +} elsif ($gitRebaseContinue && !$isGit) { + print STDERR "ERROR: --continue may only be used with a git repository\n"; + undef $getOptionsResult; +} elsif (defined $fixMerged && !$isGit) { + print STDERR "ERROR: --fix-merged may only be used with a git repository\n"; + undef $getOptionsResult; +} elsif ($mergeDriver && !$isGit) { + print STDERR "ERROR: --merge-driver may only be used with a git repository\n"; + undef $getOptionsResult; +} elsif ($mergeDriver && scalar(@ARGV) < 3) { + print STDERR "ERROR: --merge-driver expects %O %A %B as arguments\n"; + undef $getOptionsResult; +} + +sub usageAndExit() +{ + print STDERR <<__END__; +Usage: @{[ basename($0) ]} [options] [path/to/ChangeLog] [path/to/another/ChangeLog ...] + -c|--[no-]continue run "git rebase --continue" after fixing ChangeLog + entries (default: --no-continue) + -f|--fix-merged [revision-range] fix git-merged ChangeLog entries; if a revision-range + is specified, run git filter-branch on the range + -m|--merge-driver %O %A %B act as a git merge-driver on files %O %A %B + -h|--help show this help message + -w|--[no-]warnings show or suppress warnings (default: show warnings) +__END__ + exit 1; +} + +if (!$getOptionsResult || $showHelp) { + usageAndExit(); +} + +if (defined $fixMerged && length($fixMerged) > 0) { + my $commitRange = $fixMerged; + $commitRange = $commitRange . "..HEAD" if index($commitRange, "..") < 0; + fixMergedChangeLogs($commitRange, @changeLogFiles); +} elsif ($mergeDriver) { + my ($base, $theirs, $ours) = @ARGV; + if (mergeChangeLogs($ours, $base, $theirs)) { + unlink($ours); + copy($theirs, $ours) or die $!; + } else { + exec qw(git merge-file -L THEIRS -L BASE -L OURS), $theirs, $base, $ours; + } +} elsif (@changeLogFiles) { + for my $file (@changeLogFiles) { + if (defined $fixMerged) { + fixOneMergedChangeLog($file); + } else { + resolveChangeLog($file); + } + } +} else { + print STDERR "ERROR: Unknown combination of switches and arguments.\n"; + usageAndExit(); +} + +if ($gitRebaseContinue) { + if (hasGitUnmergedFiles()) { + print "Unmerged files; skipping '$GIT rebase --continue'.\n"; + } else { + print "Running '$GIT rebase --continue'...\n"; + print `$GIT rebase --continue`; + } +} + +exit 0; + +sub canonicalRelativePath($) +{ + my ($originalPath) = @_; + my $absolutePath = Cwd::abs_path($originalPath); + return File::Spec->abs2rel($absolutePath, Cwd::getcwd()); +} + +sub conflictFiles($) +{ + my ($file) = @_; + my $fileMine; + my $fileOlder; + my $fileNewer; + + if (-e $file && -e "$file.orig" && -e "$file.rej") { + return ("$file.rej", "$file.orig", $file); + } + + if ($isSVN) { + open STAT, "-|", $SVN, "status", $file or die $!; + my $status = ; + close STAT; + if (!$status || $status !~ m/^C\s+/) { + print STDERR "WARNING: ${file} is not in a conflicted state.\n" if $printWarnings; + return (); + } + + $fileMine = "${file}.mine" if -e "${file}.mine"; + + my $currentRevision; + open INFO, "-|", $SVN, "info", $file or die $!; + while (my $line = ) { + if ($line =~ m/^Revision: ([0-9]+)/) { + $currentRevision = $1; + { local $/ = undef; ; } # Consume rest of input. + } + } + close INFO; + $fileNewer = "${file}.r${currentRevision}" if -e "${file}.r${currentRevision}"; + + my @matchingFiles = grep { $_ ne $fileNewer } glob("${file}.r[0-9][0-9]*"); + if (scalar(@matchingFiles) > 1) { + print STDERR "WARNING: Too many conflict files exist for ${file}!\n" if $printWarnings; + } else { + $fileOlder = shift @matchingFiles; + } + } elsif ($isGit) { + my $gitPrefix = `$GIT rev-parse --show-prefix`; + chomp $gitPrefix; + open GIT, "-|", $GIT, "ls-files", "--unmerged", $file or die $!; + while (my $line = ) { + my ($mode, $hash, $stage, $fileName) = split(' ', $line); + my $outputFile; + if ($stage == 1) { + $fileOlder = "${file}.BASE.$$"; + $outputFile = $fileOlder; + } elsif ($stage == 2) { + $fileNewer = "${file}.LOCAL.$$"; + $outputFile = $fileNewer; + } elsif ($stage == 3) { + $fileMine = "${file}.REMOTE.$$"; + $outputFile = $fileMine; + } else { + die "Unknown file stage: $stage"; + } + system("$GIT cat-file blob :${stage}:${gitPrefix}${file} > $outputFile"); + die $! if WEXITSTATUS($?); + } + close GIT or die $!; + } else { + die "Unknown version control system"; + } + + if (!$fileMine && !$fileOlder && !$fileNewer) { + print STDERR "WARNING: ${file} does not need merging.\n" if $printWarnings; + } elsif (!$fileMine || !$fileOlder || !$fileNewer) { + print STDERR "WARNING: ${file} is missing some conflict files.\n" if $printWarnings; + } + + return ($fileMine, $fileOlder, $fileNewer); +} + +sub findChangeLog($) +{ + return $_[0] if basename($_[0]) eq "ChangeLog"; + + my $file = File::Spec->catfile($_[0], "ChangeLog"); + return $file if -d $_[0] and -e $file; + + return undef; +} + +sub findUnmergedChangeLogs() +{ + my $statCommand = ""; + + if ($isSVN) { + $statCommand = "$SVN stat | grep '^C'"; + } elsif ($isGit) { + $statCommand = "$GIT diff -r --name-status --diff-filter=U -C -C -M"; + } else { + return (); + } + + my @results = (); + open STAT, "-|", $statCommand or die "The status failed: $!.\n"; + while () { + if ($isSVN) { + my $matches; + my $file; + if (isSVNVersion16OrNewer()) { + $matches = /^([C]).{6} (.+?)[\r\n]*$/; + $file = $2; + } else { + $matches = /^([C]).{5} (.+?)[\r\n]*$/; + $file = $2; + } + if ($matches) { + $file = findChangeLog(normalizePath($file)); + push @results, $file if $file; + } else { + print; # error output from svn stat + } + } elsif ($isGit) { + if (/^([U])\t(.+)$/) { + my $file = findChangeLog(normalizePath($2)); + push @results, $file if $file; + } else { + print; # error output from git diff + } + } + } + close STAT; + + return @results; +} + +sub fixMergedChangeLogs($;@) +{ + my $revisionRange = shift; + my @changedFiles = @_; + + if (scalar(@changedFiles) < 1) { + # Read in list of files changed in $revisionRange + open GIT, "-|", $GIT, "diff", "--name-only", $revisionRange or die $!; + push @changedFiles, ; + close GIT or die $!; + die "No changed files in $revisionRange" if scalar(@changedFiles) < 1; + chomp @changedFiles; + } + + my @changeLogs = grep { defined $_ } map { findChangeLog($_) } @changedFiles; + die "No changed ChangeLog files in $revisionRange" if scalar(@changeLogs) < 1; + + system("$GIT filter-branch --tree-filter 'PREVIOUS_COMMIT=\`$GIT rev-parse \$GIT_COMMIT^\` && MAPPED_PREVIOUS_COMMIT=\`map \$PREVIOUS_COMMIT\` \"$0\" -f \"" . join('" "', @changeLogs) . "\"' $revisionRange"); + + # On success, remove the backup refs directory + if (WEXITSTATUS($?) == 0) { + rmtree(qw(.git/refs/original)); + } +} + +sub fixOneMergedChangeLog($) +{ + my $file = shift; + my $patch; + + # Read in patch for incorrectly merged ChangeLog entry + { + local $/ = undef; + open GIT, "-|", $GIT, "diff", ($ENV{GIT_COMMIT} || "HEAD") . "^", $file or die $!; + $patch = ; + close GIT or die $!; + } + + # Always checkout the previous commit's copy of the ChangeLog + system($GIT, "checkout", $ENV{MAPPED_PREVIOUS_COMMIT} || "HEAD^", $file); + die $! if WEXITSTATUS($?); + + # The patch must have 0 or more lines of context, then 1 or more lines + # of additions, and then 1 or more lines of context. If not, we skip it. + if ($patch =~ /\n@@ -(\d+),(\d+) \+(\d+),(\d+) @@\n( .*\n)*((\+.*\n)+)( .*\n)+$/m) { + # Copy the header from the original patch. + my $newPatch = substr($patch, 0, index($patch, "@@ -${1},${2} +${3},${4} @@")); + + # Generate a new set of line numbers and patch lengths. Our new + # patch will start with the lines for the fixed ChangeLog entry, + # then have 3 lines of context from the top of the current file to + # make the patch apply cleanly. + $newPatch .= "@@ -1,3 +1," . ($4 - $2 + 3) . " @@\n"; + + # We assume that top few lines of the ChangeLog entry are actually + # at the bottom of the list of added lines (due to the way the patch + # algorithm works), so we simply search through the lines until we + # find the date line, then move the rest of the lines to the top. + my @patchLines = map { $_ . "\n" } split(/\n/, $6); + foreach my $i (0 .. $#patchLines) { + if ($patchLines[$i] =~ /^\+\d{4}-\d{2}-\d{2} /) { + unshift(@patchLines, splice(@patchLines, $i, scalar(@patchLines) - $i)); + last; + } + } + + $newPatch .= join("", @patchLines); + + # Add 3 lines of context to the end + open FILE, "<", $file or die $!; + for (my $i = 0; $i < 3; $i++) { + $newPatch .= " " . ; + } + close FILE; + + # Apply the new patch + open(PATCH, "| patch -p1 $file > " . File::Spec->devnull()) or die $!; + print PATCH $newPatch; + close(PATCH) or die $!; + + # Run "git add" on the fixed ChangeLog file + system($GIT, "add", $file); + die $! if WEXITSTATUS($?); + + showStatus($file, 1); + } elsif ($patch) { + # Restore the current copy of the ChangeLog file since we can't repatch it + system($GIT, "checkout", $ENV{GIT_COMMIT} || "HEAD", $file); + die $! if WEXITSTATUS($?); + print STDERR "WARNING: Last change to ${file} could not be fixed and re-merged.\n" if $printWarnings; + } +} + +sub hasGitUnmergedFiles() +{ + my $output = `$GIT ls-files --unmerged`; + return $output ne ""; +} + +sub isInGitFilterBranch() +{ + return exists $ENV{MAPPED_PREVIOUS_COMMIT} && $ENV{MAPPED_PREVIOUS_COMMIT}; +} + +sub parseFixMerged($$;$) +{ + my ($switchName, $key, $value) = @_; + if (defined $key) { + if (defined findChangeLog($key)) { + unshift(@ARGV, $key); + $fixMerged = ""; + } else { + $fixMerged = $key; + } + } else { + $fixMerged = ""; + } +} + +sub removeChangeLogArguments($) +{ + my ($baseDir) = @_; + my @results = (); + + for (my $i = 0; $i < scalar(@ARGV); ) { + my $file = findChangeLog(canonicalRelativePath(File::Spec->catfile($baseDir, $ARGV[$i]))); + if (defined $file) { + splice(@ARGV, $i, 1); + push @results, $file; + } else { + $i++; + } + } + + return @results; +} + +sub resolveChangeLog($) +{ + my ($file) = @_; + + my ($fileMine, $fileOlder, $fileNewer) = conflictFiles($file); + + return unless $fileMine && $fileOlder && $fileNewer; + + if (mergeChangeLogs($fileMine, $fileOlder, $fileNewer)) { + if ($file ne $fileNewer) { + unlink($file); + rename($fileNewer, $file) or die $!; + } + unlink($fileMine, $fileOlder); + resolveConflict($file); + showStatus($file, 1); + } else { + showStatus($file); + print STDERR "WARNING: ${file} could not be merged using fuzz level 3.\n" if $printWarnings; + unlink($fileMine, $fileOlder, $fileNewer) if $isGit; + } +} + +sub resolveConflict($) +{ + my ($file) = @_; + + if ($isSVN) { + system($SVN, "resolved", $file); + die $! if WEXITSTATUS($?); + } elsif ($isGit) { + system($GIT, "add", $file); + die $! if WEXITSTATUS($?); + } else { + die "Unknown version control system"; + } +} + +sub showStatus($;$) +{ + my ($file, $isConflictResolved) = @_; + + if ($isSVN) { + system($SVN, "status", $file); + } elsif ($isGit) { + my @args = qw(--name-status); + unshift @args, qw(--cached) if $isConflictResolved; + system($GIT, "diff", @args, $file); + } else { + die "Unknown version control system"; + } +} +