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