commit-log-editor 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. #!/usr/bin/perl -w
  2. # Copyright (C) 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights reserved.
  3. # Copyright (C) 2009 Torch Mobile 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. # Script to put change log comments in as default check-in comment.
  29. use strict;
  30. use Getopt::Long;
  31. use File::Basename;
  32. use File::Spec;
  33. use FindBin;
  34. use lib $FindBin::Bin;
  35. use VCSUtils;
  36. use webkitdirs;
  37. sub createCommitMessage(@);
  38. sub loadTermReadKey();
  39. sub normalizeLineEndings($$);
  40. sub patchAuthorshipString($$$);
  41. sub removeLongestCommonPrefixEndingInDoubleNewline(\%);
  42. sub isCommitLogEditor($);
  43. my $endl = "\n";
  44. sub printUsageAndExit
  45. {
  46. my $programName = basename($0);
  47. print STDERR <<EOF;
  48. Usage: $programName [--regenerate-log] <log file>
  49. $programName --print-log <ChangeLog file> [<ChangeLog file>...]
  50. $programName --help
  51. EOF
  52. exit 1;
  53. }
  54. my $help = 0;
  55. my $printLog = 0;
  56. my $regenerateLog = 0;
  57. my $getOptionsResult = GetOptions(
  58. 'help' => \$help,
  59. 'print-log' => \$printLog,
  60. 'regenerate-log' => \$regenerateLog,
  61. );
  62. if (!$getOptionsResult || $help) {
  63. printUsageAndExit();
  64. }
  65. die "Can't specify both --print-log and --regenerate-log\n" if $printLog && $regenerateLog;
  66. if ($printLog) {
  67. printUsageAndExit() unless @ARGV;
  68. print createCommitMessage(@ARGV);
  69. exit 0;
  70. }
  71. my $log = $ARGV[0];
  72. if (!$log) {
  73. printUsageAndExit();
  74. }
  75. my $baseDir = baseProductDir();
  76. my $editor = $ENV{SVN_LOG_EDITOR};
  77. $editor = $ENV{CVS_LOG_EDITOR} if !$editor;
  78. $editor = "" if $editor && isCommitLogEditor($editor);
  79. my $splitEditor = 1;
  80. if (!$editor) {
  81. my $builtEditorApplication = "$baseDir/Release/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
  82. if (-x $builtEditorApplication) {
  83. $editor = $builtEditorApplication;
  84. $splitEditor = 0;
  85. }
  86. }
  87. if (!$editor) {
  88. my $builtEditorApplication = "$baseDir/Debug/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
  89. if (-x $builtEditorApplication) {
  90. $editor = $builtEditorApplication;
  91. $splitEditor = 0;
  92. }
  93. }
  94. if (!$editor) {
  95. my $builtEditorApplication = "$ENV{HOME}/Applications/Commit Log Editor.app/Contents/MacOS/Commit Log Editor";
  96. if (-x $builtEditorApplication) {
  97. $editor = $builtEditorApplication;
  98. $splitEditor = 0;
  99. }
  100. }
  101. $editor = $ENV{EDITOR} if !$editor;
  102. $editor = "/usr/bin/vi" if !$editor;
  103. my @editor;
  104. if ($splitEditor) {
  105. @editor = split ' ', $editor;
  106. } else {
  107. @editor = ($editor);
  108. }
  109. my $inChangesToBeCommitted = !isGit();
  110. my @changeLogs = ();
  111. my $logContents = "";
  112. my $existingLog = 0;
  113. open LOG, $log or die "Could not open the log file.";
  114. while (my $curLine = <LOG>) {
  115. if (isGit()) {
  116. if ($curLine =~ /^# Changes to be committed:$/) {
  117. $inChangesToBeCommitted = 1;
  118. } elsif ($inChangesToBeCommitted && $curLine =~ /^# \S/) {
  119. $inChangesToBeCommitted = 0;
  120. }
  121. }
  122. if (!isGit() || $curLine =~ /^#/) {
  123. $logContents .= $curLine;
  124. } else {
  125. # $_ contains the current git log message
  126. # (without the log comment info). We don't need it.
  127. }
  128. $existingLog = isGit() && !($curLine =~ /^#/ || $curLine =~ /^\s*$/) unless $existingLog;
  129. my $changeLogFileName = changeLogFileName();
  130. push @changeLogs, makeFilePathRelative($1) if $inChangesToBeCommitted && ($curLine =~ /^(?:M|A)....(.*$changeLogFileName)\r?\n?$/ || $curLine =~ /^#\t(?:modified|new file): (.*$changeLogFileName)$/) && $curLine !~ /-$changeLogFileName$/;
  131. }
  132. close LOG;
  133. # We want to match the line endings of the existing log file in case they're
  134. # different from perl's line endings.
  135. $endl = $1 if $logContents =~ /(\r?\n)/;
  136. my $keepExistingLog = 1;
  137. if ($regenerateLog && $existingLog && scalar(@changeLogs) > 0 && loadTermReadKey()) {
  138. print "Existing log message detected, Use 'r' to regenerate log message from ChangeLogs, or any other key to keep the existing message.\n";
  139. Term::ReadKey::ReadMode('cbreak');
  140. my $key = Term::ReadKey::ReadKey(0);
  141. Term::ReadKey::ReadMode('normal');
  142. $keepExistingLog = 0 if ($key eq "r");
  143. }
  144. # Don't change anything if there's already a log message (as can happen with git-commit --amend).
  145. exec (@editor, @ARGV) if $existingLog && $keepExistingLog;
  146. my $first = 1;
  147. open NEWLOG, ">$log.edit" or die;
  148. if (isGit() && @changeLogs == 0) {
  149. # populate git commit message with WebKit-format ChangeLog entries unless explicitly disabled
  150. my $branch = gitBranch();
  151. chomp(my $webkitGenerateCommitMessage = `git config --bool branch.$branch.webkitGenerateCommitMessage`);
  152. if ($webkitGenerateCommitMessage eq "") {
  153. chomp($webkitGenerateCommitMessage = `git config --bool core.webkitGenerateCommitMessage`);
  154. }
  155. if ($webkitGenerateCommitMessage ne "false") {
  156. open CHANGELOG_ENTRIES, "-|", "$FindBin::Bin/prepare-ChangeLog --git-index --no-write" or die "prepare-ChangeLog failed: $!.\n";
  157. while (<CHANGELOG_ENTRIES>) {
  158. print NEWLOG normalizeLineEndings($_, $endl);
  159. }
  160. close CHANGELOG_ENTRIES;
  161. }
  162. } else {
  163. print NEWLOG createCommitMessage(@changeLogs);
  164. }
  165. print NEWLOG $logContents;
  166. close NEWLOG;
  167. system (@editor, "$log.edit");
  168. open NEWLOG, "$log.edit" or exit;
  169. my $foundComment = 0;
  170. while (<NEWLOG>) {
  171. $foundComment = 1 if (/\S/ && !/^CVS:/);
  172. }
  173. close NEWLOG;
  174. if ($foundComment) {
  175. open NEWLOG, "$log.edit" or die;
  176. open LOG, ">$log" or die;
  177. while (<NEWLOG>) {
  178. print LOG;
  179. }
  180. close LOG;
  181. close NEWLOG;
  182. }
  183. unlink "$log.edit";
  184. sub createCommitMessage(@)
  185. {
  186. my @changeLogs = @_;
  187. my $topLevel = determineVCSRoot();
  188. my %changeLogSort;
  189. my %changeLogContents;
  190. for my $changeLog (@changeLogs) {
  191. open CHANGELOG, $changeLog or die "Can't open $changeLog";
  192. my $contents = "";
  193. my $blankLines = "";
  194. my $lineCount = 0;
  195. my $date = "";
  196. my $author = "";
  197. my $email = "";
  198. my $hasAuthorInfoToWrite = 0;
  199. while (<CHANGELOG>) {
  200. if (/^\S/) {
  201. last if $contents;
  202. }
  203. if (/\S/) {
  204. $contents .= $blankLines if $contents;
  205. $blankLines = "";
  206. my $line = $_;
  207. # Remove indentation spaces
  208. $line =~ s/^ {8}//;
  209. # Grab the author and the date line
  210. if ($line =~ m/^([0-9]{4}-[0-9]{2}-[0-9]{2})\s+(.*[^\s])\s+<(.*)>/ && $lineCount == 0) {
  211. $date = $1;
  212. $author = $2;
  213. $email = $3;
  214. $hasAuthorInfoToWrite = 1;
  215. next;
  216. }
  217. if ($hasAuthorInfoToWrite) {
  218. my $isReviewedByLine = $line =~ m/^(?:Reviewed|Rubber[ \-]?stamped) by/;
  219. my $isModifiedFileLine = $line =~ m/^\* .*:/;
  220. # Insert the authorship line if needed just above the "Reviewed by" line or the
  221. # first modified file (whichever comes first).
  222. if ($isReviewedByLine || $isModifiedFileLine) {
  223. $hasAuthorInfoToWrite = 0;
  224. my $authorshipString = patchAuthorshipString($author, $email, $date);
  225. if ($authorshipString) {
  226. $contents .= "$authorshipString\n";
  227. $contents .= "\n" if $isModifiedFileLine;
  228. }
  229. }
  230. }
  231. $lineCount++;
  232. $contents .= $line;
  233. } else {
  234. $blankLines .= $_;
  235. }
  236. }
  237. if ($hasAuthorInfoToWrite) {
  238. # We didn't find anywhere to put the authorship info, so just put it at the end.
  239. my $authorshipString = patchAuthorshipString($author, $email, $date);
  240. $contents .= "\n$authorshipString\n" if $authorshipString;
  241. $hasAuthorInfoToWrite = 0;
  242. }
  243. close CHANGELOG;
  244. $changeLog = File::Spec->abs2rel(File::Spec->rel2abs($changeLog), $topLevel);
  245. my $label = dirname($changeLog);
  246. $label = "top level" unless length $label;
  247. my $sortKey = lc $label;
  248. if ($label eq "top level") {
  249. $sortKey = "";
  250. } elsif ($label eq "LayoutTests") {
  251. $sortKey = lc "~, LayoutTests last";
  252. }
  253. $changeLogSort{$sortKey} = $label;
  254. $changeLogContents{$label} = $contents;
  255. }
  256. my $commonPrefix = removeLongestCommonPrefixEndingInDoubleNewline(%changeLogContents);
  257. my $first = 1;
  258. my @result;
  259. push @result, normalizeLineEndings($commonPrefix, $endl);
  260. for my $sortKey (sort keys %changeLogSort) {
  261. my $label = $changeLogSort{$sortKey};
  262. if (keys %changeLogSort > 1) {
  263. push @result, normalizeLineEndings("\n", $endl) if !$first;
  264. $first = 0;
  265. push @result, normalizeLineEndings("$label: ", $endl);
  266. }
  267. push @result, normalizeLineEndings($changeLogContents{$label}, $endl);
  268. }
  269. return join '', @result;
  270. }
  271. sub loadTermReadKey()
  272. {
  273. eval { require Term::ReadKey; };
  274. return !$@;
  275. }
  276. sub normalizeLineEndings($$)
  277. {
  278. my ($string, $endl) = @_;
  279. $string =~ s/\r?\n/$endl/g;
  280. return $string;
  281. }
  282. sub patchAuthorshipString($$$)
  283. {
  284. my ($authorName, $authorEmail, $authorDate) = @_;
  285. return if $authorEmail eq changeLogEmailAddress();
  286. return "Patch by $authorName <$authorEmail> on $authorDate";
  287. }
  288. sub removeLongestCommonPrefixEndingInDoubleNewline(\%)
  289. {
  290. my ($hashOfStrings) = @_;
  291. my @strings = values %{$hashOfStrings};
  292. return "" unless @strings > 1;
  293. my $prefix = shift @strings;
  294. my $prefixLength = length $prefix;
  295. foreach my $string (@strings) {
  296. while ($prefixLength) {
  297. last if substr($string, 0, $prefixLength) eq $prefix;
  298. --$prefixLength;
  299. $prefix = substr($prefix, 0, -1);
  300. }
  301. last unless $prefixLength;
  302. }
  303. return "" unless $prefixLength;
  304. my $lastDoubleNewline = rindex($prefix, "\n\n");
  305. return "" unless $lastDoubleNewline > 0;
  306. foreach my $key (keys %{$hashOfStrings}) {
  307. $hashOfStrings->{$key} = substr($hashOfStrings->{$key}, $lastDoubleNewline);
  308. }
  309. return substr($prefix, 0, $lastDoubleNewline + 2);
  310. }
  311. sub isCommitLogEditor($)
  312. {
  313. my $editor = shift;
  314. return $editor =~ m/commit-log-editor/;
  315. }