3 # Copyright (C) 2011 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
8 # 1. Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # 2. Redistributions in binary form must reproduce the above copyright
11 # notice, this list of conditions and the following disclaimer in the
12 # documentation and/or other materials provided with the distribution.
14 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
15 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
18 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 my $defaultReviewer = "NOBODY";
36 sub addReviewerToChangeLog($$$);
37 sub addReviewerToCommitMessage($$$);
38 sub changeLogsForCommit($);
42 sub getConfigValue($);
48 sub rebaseOntoHead($$);
49 sub requireCleanWorkTree();
53 sub writeCommitMessageToFile($);
59 my $programName = basename($0);
61 Usage: $programName -i|--interactive upstream
62 $programName commit-ish reviewer
64 Adds a reviewer to a git commit in a repository with WebKit-style commit logs
67 When run in interactive mode, `upstream` specifies the commit after which
68 reviewers should be added.
70 When run in non-interactive mode, `commit-ish` specifies the commit to which
71 the `reviewer` will be added.
74 -h|--help Display this message
75 -i|--interactive Interactive mode
78 my $getOptionsResult = GetOptions(
79 'h|help' => \$showHelp,
80 'i|interactive' => \$interactive,
83 usage() if !$getOptionsResult || $showHelp;
85 requireCleanWorkTree();
86 $interactive ? interactive() : nonInteractive();
91 @ARGV == 1 or usage();
93 my $upstream = toCommit($ARGV[0]);
96 isAncestor($upstream, $head) or die "$ARGV[0] is not an ancestor of HEAD.";
98 my @revlist = `git rev-list --reverse --pretty=oneline $upstream..`;
99 @revlist or die "Couldn't determine revisions";
101 my $tempFile = new File::Temp(UNLINK => 1);
102 foreach my $line (@revlist) {
103 print $tempFile "$defaultReviewer : $line";
106 print $tempFile <<EOF;
108 # Change 'NOBODY' to the reviewer for each commit
110 # If any line starts with "rs" followed by one or more spaces, then the phrase
111 # "Reviewed by" is changed to "Rubber-stamped by" in the ChangeLog(s)/commit
112 # message for that commit.
114 # Commits may be reordered
115 # Omitted commits will be lost
120 my $editor = $ENV{GIT_EDITOR} || getConfigValue("core.editor") || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
121 my $result = system "$editor \"" . $tempFile->filename . "\"";
122 !$result or die "Error spawning editor.";
125 open TEMPFILE, '<', $tempFile->filename or die "Error opening temp file.";
126 foreach my $line (<TEMPFILE>) {
127 next if $line =~ /^#/;
128 $line =~ /^(rs\s+)?(.*)\s+:\s+([0-9a-fA-F]+)/ or next;
129 push @todo, {rubberstamp => defined $1 && length $1, reviewer => $2, commit => $3};
132 @todo or die "No revisions specified.";
134 foreach my $item (@todo) {
135 $item->{changeLogs} = changeLogsForCommit($item->{commit});
138 $result = system "git", "checkout", $upstream;
139 !$result or die "Error checking out $ARGV[0].";
142 foreach my $item (@todo) {
143 $success = cherryPick(%{$item});
145 $success = addReviewer(%{$item});
152 resetToCommit($head);
156 $result = system "git", "branch", "-f", $head;
157 !$result or die "Error updating $head.";
158 $result = system "git", "checkout", $head;
159 exit WEXITSTATUS($result);
164 @ARGV == 2 or usage();
166 my $commit = toCommit($ARGV[0]);
167 my $reviewer = $ARGV[1];
169 my $headCommit = toCommit($head);
171 isAncestor($commit, $head) or die "$ARGV[1] is not an ancestor of HEAD.";
174 reviewer => $reviewer,
178 $item{changeLogs} = changeLogsForCommit($commit);
179 $item{changeLogs} or die;
181 unless ((($commit eq $headCommit) or checkout($commit))
182 # FIXME: We need to use $ENV{GIT_DIR}/.git/MERGE_MSG
183 && writeCommitMessageToFile(".git/MERGE_MSG")
184 && addReviewer(%item)
186 && (($commit eq $headCommit) or rebaseOntoHead($commit, $head))) {
187 resetToCommit($head);
198 sub requireCleanWorkTree()
200 my $result = system "git rev-parse --verify HEAD > /dev/null";
201 $result ||= system qw(git update-index --refresh);
202 $result ||= system qw(git diff-files --quiet);
203 $result ||= system qw(git diff-index --cached --quiet HEAD --);
204 !$result or die "Working tree is dirty"
210 print STDERR $message, "\n" if defined $message;
218 my $result = system "git cherry-pick -n $item->{commit} > /dev/null";
219 !$result or return fail("Failed to cherry-pick $item->{commit}");
228 return 1 if $item->{reviewer} eq $defaultReviewer;
230 foreach my $log (@{$item->{changeLogs}}) {
231 addReviewerToChangeLog($item->{reviewer}, $item->{rubberstamp}, $log) or return fail();
234 addReviewerToCommitMessage($item->{reviewer}, $item->{rubberstamp}, ".git/MERGE_MSG") or return fail();
243 my @command = qw(git commit -F .git/MERGE_MSG);
244 push @command, "--amend" if $amend;
245 my $result = system @command;
246 !$result or return fail("Failed to commit revision");
251 sub addReviewerToChangeLog($$$)
253 my ($reviewer, $rubberstamp, $log) = @_;
255 return addReviewerToFile($reviewer, $rubberstamp, $log, 0);
258 sub addReviewerToCommitMessage($$$)
260 my ($reviewer, $rubberstamp, $log) = @_;
262 return addReviewerToFile($reviewer, $rubberstamp, $log, 1);
265 sub addReviewerToFile
267 my ($reviewer, $rubberstamp, $log, $isCommitMessage) = @_;
269 my $tempFile = new File::Temp(UNLINK => 1);
271 open LOG, "<", $log or return fail("Couldn't open $log.");
274 foreach my $line (<LOG>) {
275 if (!$finished && $line =~ /NOBODY \(OOPS!\)/) {
276 $line =~ s/NOBODY \(OOPS!\)/$reviewer/;
277 $line =~ s/Reviewed/Rubber-stamped/ if $rubberstamp;
278 $finished = 1 unless $isCommitMessage;
281 print $tempFile $line;
285 close LOG or return fail("Couldn't close $log");
287 my $result = system "mv", $tempFile->filename, $log;
288 !$result or return fail("Failed to rename $tempFile to $log");
290 unless ($isCommitMessage) {
291 my $result = system "git", "add", $log;
292 !$result or return fail("Failed to git add");
300 my $head = `git symbolic-ref HEAD`;
301 $head =~ /^refs\/heads\/(.*)$/ or die "Couldn't determine current branch.";
309 my ($ancestor, $descendant) = @_;
311 chomp(my $mergeBase = `git merge-base $ancestor $descendant`);
312 return $mergeBase eq $ancestor;
319 chomp(my $commit = `git rev-parse $arg`);
323 sub changeLogsForCommit($)
327 my @files = `git diff -r --name-status $commit^ $commit`;
328 @files or return fail("Couldn't determine changed files for $commit.");
330 my @changeLogs = map { /^[ACMR]\s*(.*)/; $1 } grep { /^[ACMR].*[^-]ChangeLog/ } @files;
338 my $result = system "git", "checkout", "-f", $commit;
339 !$result or return fail("Error checking out $commit.");
344 sub writeCommitMessageToFile($)
348 open FILE, ">", $file or return fail("Couldn't open $file.");
349 open MESSAGE, "-|", qw(git rev-list --max-count=1 --pretty=format:%B HEAD) or return fail("Error running git rev-list.");
350 my $commitLine = <MESSAGE>;
351 foreach my $line (<MESSAGE>) {
355 close FILE or return fail("Couldn't close $file.");
360 sub rebaseOntoHead($$)
362 my ($upstream, $branch) = @_;
364 my $result = system qw(git rebase --onto HEAD), $upstream, $branch;
365 !$result or return fail("Couldn't rebase.");
374 my $result = system "git", "checkout", $commit;
375 !$result or return fail("Error checking out $commit.");
380 sub getConfigValue($)
384 chomp(my $value = `git config --get "$variable"`);