123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- #!/usr/bin/perl -w
- # Copyright (C) 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights reserved.
- # Copyright (C) 2009 Torch Mobile 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.
- # Script to put change log comments in as default check-in comment.
- use strict;
- use Getopt::Long;
- use File::Basename;
- use File::Spec;
- use FindBin;
- use lib $FindBin::Bin;
- use VCSUtils;
- use webkitdirs;
- sub createCommitMessage(@);
- sub loadTermReadKey();
- sub normalizeLineEndings($$);
- sub patchAuthorshipString($$$);
- sub removeLongestCommonPrefixEndingInDoubleNewline(\%);
- sub isCommitLogEditor($);
- my $endl = "\n";
- sub printUsageAndExit
- {
- my $programName = basename($0);
- print STDERR <<EOF;
- Usage: $programName [--regenerate-log] <log file>
- $programName --print-log <ChangeLog file> [<ChangeLog file>...]
- $programName --help
- EOF
- exit 1;
- }
- my $help = 0;
- my $printLog = 0;
- my $regenerateLog = 0;
- my $getOptionsResult = GetOptions(
- 'help' => \$help,
- 'print-log' => \$printLog,
- 'regenerate-log' => \$regenerateLog,
- );
- if (!$getOptionsResult || $help) {
- printUsageAndExit();
- }
- die "Can't specify both --print-log and --regenerate-log\n" if $printLog && $regenerateLog;
- if ($printLog) {
- printUsageAndExit() unless @ARGV;
- print createCommitMessage(@ARGV);
- exit 0;
- }
- my $log = $ARGV[0];
- if (!$log) {
- printUsageAndExit();
- }
- my $baseDir = baseProductDir();
- my $editor = $ENV{SVN_LOG_EDITOR};
- $editor = $ENV{CVS_LOG_EDITOR} if !$editor;
- $editor = "" if $editor && isCommitLogEditor($editor);
- my $splitEditor = 1;
- if (!$editor) {
- my $builtEditorApplication = "$baseDir/Release/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
- if (-x $builtEditorApplication) {
- $editor = $builtEditorApplication;
- $splitEditor = 0;
- }
- }
- if (!$editor) {
- my $builtEditorApplication = "$baseDir/Debug/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
- if (-x $builtEditorApplication) {
- $editor = $builtEditorApplication;
- $splitEditor = 0;
- }
- }
- if (!$editor) {
- my $builtEditorApplication = "$ENV{HOME}/Applications/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
- if (-x $builtEditorApplication) {
- $editor = $builtEditorApplication;
- $splitEditor = 0;
- }
- }
- $editor = $ENV{EDITOR} if !$editor;
- $editor = "/usr/bin/vi" if !$editor;
- my @editor;
- if ($splitEditor) {
- @editor = split ' ', $editor;
- } else {
- @editor = ($editor);
- }
- my $inChangesToBeCommitted = !isGit();
- my @changeLogs = ();
- my $logContents = "";
- my $existingLog = 0;
- open LOG, $log or die "Could not open the log file.";
- while (my $curLine = <LOG>) {
- if (isGit()) {
- if ($curLine =~ /^# Changes to be committed:$/) {
- $inChangesToBeCommitted = 1;
- } elsif ($inChangesToBeCommitted && $curLine =~ /^# \S/) {
- $inChangesToBeCommitted = 0;
- }
- }
- if (!isGit() || $curLine =~ /^#/) {
- $logContents .= $curLine;
- } else {
- # $_ contains the current git log message
- # (without the log comment info). We don't need it.
- }
- $existingLog = isGit() && !($curLine =~ /^#/ || $curLine =~ /^\s*$/) unless $existingLog;
- my $changeLogFileName = changeLogFileName();
- push @changeLogs, makeFilePathRelative($1) if $inChangesToBeCommitted && ($curLine =~ /^(?:M|A)....(.*$changeLogFileName)\r?\n?$/ || $curLine =~ /^#\t(?:modified|new file): (.*$changeLogFileName)$/) && $curLine !~ /-$changeLogFileName$/;
- }
- close LOG;
- # We want to match the line endings of the existing log file in case they're
- # different from perl's line endings.
- $endl = $1 if $logContents =~ /(\r?\n)/;
- my $keepExistingLog = 1;
- if ($regenerateLog && $existingLog && scalar(@changeLogs) > 0 && loadTermReadKey()) {
- print "Existing log message detected, Use 'r' to regenerate log message from ChangeLogs, or any other key to keep the existing message.\n";
- Term::ReadKey::ReadMode('cbreak');
- my $key = Term::ReadKey::ReadKey(0);
- Term::ReadKey::ReadMode('normal');
- $keepExistingLog = 0 if ($key eq "r");
- }
- # Don't change anything if there's already a log message (as can happen with git-commit --amend).
- exec (@editor, @ARGV) if $existingLog && $keepExistingLog;
- my $first = 1;
- open NEWLOG, ">$log.edit" or die;
- if (isGit() && @changeLogs == 0) {
- # populate git commit message with WebKit-format ChangeLog entries unless explicitly disabled
- my $branch = gitBranch();
- chomp(my $webkitGenerateCommitMessage = `git config --bool branch.$branch.webkitGenerateCommitMessage`);
- if ($webkitGenerateCommitMessage eq "") {
- chomp($webkitGenerateCommitMessage = `git config --bool core.webkitGenerateCommitMessage`);
- }
- if ($webkitGenerateCommitMessage ne "false") {
- open CHANGELOG_ENTRIES, "-|", "$FindBin::Bin/prepare-ChangeLog --git-index --no-write" or die "prepare-ChangeLog failed: $!.\n";
- while (<CHANGELOG_ENTRIES>) {
- print NEWLOG normalizeLineEndings($_, $endl);
- }
- close CHANGELOG_ENTRIES;
- }
- } else {
- print NEWLOG createCommitMessage(@changeLogs);
- }
- print NEWLOG $logContents;
- close NEWLOG;
- system (@editor, "$log.edit");
- open NEWLOG, "$log.edit" or exit;
- my $foundComment = 0;
- while (<NEWLOG>) {
- $foundComment = 1 if (/\S/ && !/^CVS:/);
- }
- close NEWLOG;
- if ($foundComment) {
- open NEWLOG, "$log.edit" or die;
- open LOG, ">$log" or die;
- while (<NEWLOG>) {
- print LOG;
- }
- close LOG;
- close NEWLOG;
- }
- unlink "$log.edit";
- sub createCommitMessage(@)
- {
- my @changeLogs = @_;
- my $topLevel = determineVCSRoot();
- my %changeLogSort;
- my %changeLogContents;
- for my $changeLog (@changeLogs) {
- open CHANGELOG, $changeLog or die "Can't open $changeLog";
- my $contents = "";
- my $blankLines = "";
- my $lineCount = 0;
- my $date = "";
- my $author = "";
- my $email = "";
- my $hasAuthorInfoToWrite = 0;
- while (<CHANGELOG>) {
- if (/^\S/) {
- last if $contents;
- }
- if (/\S/) {
- $contents .= $blankLines if $contents;
- $blankLines = "";
- my $line = $_;
- # Remove indentation spaces
- $line =~ s/^ {8}//;
- # Grab the author and the date line
- if ($line =~ m/^([0-9]{4}-[0-9]{2}-[0-9]{2})\s+(.*[^\s])\s+<(.*)>/ && $lineCount == 0) {
- $date = $1;
- $author = $2;
- $email = $3;
- $hasAuthorInfoToWrite = 1;
- next;
- }
- if ($hasAuthorInfoToWrite) {
- my $isReviewedByLine = $line =~ m/^(?:Reviewed|Rubber[ \-]?stamped) by/;
- my $isModifiedFileLine = $line =~ m/^\* .*:/;
- # Insert the authorship line if needed just above the "Reviewed by" line or the
- # first modified file (whichever comes first).
- if ($isReviewedByLine || $isModifiedFileLine) {
- $hasAuthorInfoToWrite = 0;
- my $authorshipString = patchAuthorshipString($author, $email, $date);
- if ($authorshipString) {
- $contents .= "$authorshipString\n";
- $contents .= "\n" if $isModifiedFileLine;
- }
- }
- }
- $lineCount++;
- $contents .= $line;
- } else {
- $blankLines .= $_;
- }
- }
- if ($hasAuthorInfoToWrite) {
- # We didn't find anywhere to put the authorship info, so just put it at the end.
- my $authorshipString = patchAuthorshipString($author, $email, $date);
- $contents .= "\n$authorshipString\n" if $authorshipString;
- $hasAuthorInfoToWrite = 0;
- }
- close CHANGELOG;
- $changeLog = File::Spec->abs2rel(File::Spec->rel2abs($changeLog), $topLevel);
- my $label = dirname($changeLog);
- $label = "top level" unless length $label;
- my $sortKey = lc $label;
- if ($label eq "top level") {
- $sortKey = "";
- } elsif ($label eq "LayoutTests") {
- $sortKey = lc "~, LayoutTests last";
- }
- $changeLogSort{$sortKey} = $label;
- $changeLogContents{$label} = $contents;
- }
- my $commonPrefix = removeLongestCommonPrefixEndingInDoubleNewline(%changeLogContents);
- my $first = 1;
- my @result;
- push @result, normalizeLineEndings($commonPrefix, $endl);
- for my $sortKey (sort keys %changeLogSort) {
- my $label = $changeLogSort{$sortKey};
- if (keys %changeLogSort > 1) {
- push @result, normalizeLineEndings("\n", $endl) if !$first;
- $first = 0;
- push @result, normalizeLineEndings("$label: ", $endl);
- }
- push @result, normalizeLineEndings($changeLogContents{$label}, $endl);
- }
- return join '', @result;
- }
- sub loadTermReadKey()
- {
- eval { require Term::ReadKey; };
- return !$@;
- }
- sub normalizeLineEndings($$)
- {
- my ($string, $endl) = @_;
- $string =~ s/\r?\n/$endl/g;
- return $string;
- }
- sub patchAuthorshipString($$$)
- {
- my ($authorName, $authorEmail, $authorDate) = @_;
- return if $authorEmail eq changeLogEmailAddress();
- return "Patch by $authorName <$authorEmail> on $authorDate";
- }
- sub removeLongestCommonPrefixEndingInDoubleNewline(\%)
- {
- my ($hashOfStrings) = @_;
- my @strings = values %{$hashOfStrings};
- return "" unless @strings > 1;
- my $prefix = shift @strings;
- my $prefixLength = length $prefix;
- foreach my $string (@strings) {
- while ($prefixLength) {
- last if substr($string, 0, $prefixLength) eq $prefix;
- --$prefixLength;
- $prefix = substr($prefix, 0, -1);
- }
- last unless $prefixLength;
- }
- return "" unless $prefixLength;
- my $lastDoubleNewline = rindex($prefix, "\n\n");
- return "" unless $lastDoubleNewline > 0;
- foreach my $key (keys %{$hashOfStrings}) {
- $hashOfStrings->{$key} = substr($hashOfStrings->{$key}, $lastDoubleNewline);
- }
- return substr($prefix, 0, $lastDoubleNewline + 2);
- }
- sub isCommitLogEditor($)
- {
- my $editor = shift;
- return $editor =~ m/commit-log-editor/;
- }
|