blob: 669ba52f3ecfc95e2348b1346b35966bc24be9f3 [file] [log] [blame]
machenbach@chromium.org935a7792013-11-12 09:05:18 +00001#!/usr/bin/env python
2# Copyright 2013 the V8 project authors. All rights reserved.
3# Redistribution and use in source and binary forms, with or without
4# modification, are permitted provided that the following conditions are
5# met:
6#
7# * Redistributions of source code must retain the above copyright
8# notice, this list of conditions and the following disclaimer.
9# * Redistributions in binary form must reproduce the above
10# copyright notice, this list of conditions and the following
11# disclaimer in the documentation and/or other materials provided
12# with the distribution.
13# * Neither the name of Google Inc. nor the names of its
14# contributors may be used to endorse or promote products derived
15# from this software without specific prior written permission.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29import datetime
30import optparse
31import sys
32import tempfile
33
34from common_includes import *
35
36TRUNKBRANCH = "TRUNKBRANCH"
37CHROMIUM = "CHROMIUM"
38DEPS_FILE = "DEPS_FILE"
39
40CONFIG = {
41 BRANCHNAME: "prepare-push",
42 TRUNKBRANCH: "trunk-push",
43 PERSISTFILE_BASENAME: "/tmp/v8-push-to-trunk-tempfile",
44 TEMP_BRANCH: "prepare-push-temporary-branch-created-by-script",
45 DOT_GIT_LOCATION: ".git",
46 VERSION_FILE: "src/version.cc",
47 CHANGELOG_FILE: "ChangeLog",
48 CHANGELOG_ENTRY_FILE: "/tmp/v8-push-to-trunk-tempfile-changelog-entry",
49 PATCH_FILE: "/tmp/v8-push-to-trunk-tempfile-patch-file",
50 COMMITMSG_FILE: "/tmp/v8-push-to-trunk-tempfile-commitmsg",
51 DEPS_FILE: "DEPS",
52}
53
54
55class Preparation(Step):
56 def __init__(self):
57 Step.__init__(self, "Preparation.")
58
59 def RunStep(self):
60 self.InitialEnvironmentChecks()
61 self.CommonPrepare()
62 self.DeleteBranch(self.Config(TRUNKBRANCH))
63
64
65class FreshBranch(Step):
66 def __init__(self):
67 Step.__init__(self, "Create a fresh branch.")
68
69 def RunStep(self):
70 args = "checkout -b %s svn/bleeding_edge" % self.Config(BRANCHNAME)
71 if self.Git(args) is None:
72 self.Die("Creating branch %s failed." % self.Config(BRANCHNAME))
73
74
75class DetectLastPush(Step):
76 def __init__(self):
77 Step.__init__(self, "Detect commit ID of last push to trunk.")
78
79 def RunStep(self):
80 last_push = (self._options.l or
81 self.Git("log -1 --format=%H ChangeLog").strip())
82 while True:
83 # Print assumed commit, circumventing git's pager.
84 print self.Git("log -1 %s" % last_push)
85 if self.Confirm("Is the commit printed above the last push to trunk?"):
86 break
87 args = "log -1 --format=%H %s^ ChangeLog" % last_push
88 last_push = self.Git(args).strip()
89 self.Persist("last_push", last_push)
90 self._state["last_push"] = last_push
91
92
93class PrepareChangeLog(Step):
94 def __init__(self):
95 Step.__init__(self, "Prepare raw ChangeLog entry.")
96
97 def RunStep(self):
98 self.RestoreIfUnset("last_push")
99
100 # These version numbers are used again later for the trunk commit.
101 self.ReadAndPersistVersion()
102
103 date = datetime.date.today().strftime("%Y-%m-%d")
104 self.Persist("date", date)
105 output = "%s: Version %s.%s.%s\n\n" % (date,
106 self._state["major"],
107 self._state["minor"],
108 self._state["build"])
109 TextToFile(output, self.Config(CHANGELOG_ENTRY_FILE))
110
111 args = "log %s..HEAD --format=%%H" % self._state["last_push"]
112 commits = self.Git(args).strip()
113 for commit in commits.splitlines():
114 # Get the commit's title line.
115 args = "log -1 %s --format=\"%%w(80,8,8)%%s\"" % commit
116 title = "%s\n" % self.Git(args).rstrip()
117 AppendToFile(title, self.Config(CHANGELOG_ENTRY_FILE))
118
119 # Grep for "BUG=xxxx" lines in the commit message and convert them to
120 # "(issue xxxx)".
121 out = self.Git("log -1 %s --format=\"%%B\"" % commit).splitlines()
122 out = filter(lambda x: re.search(r"^BUG=", x), out)
123 out = filter(lambda x: not re.search(r"BUG=$", x), out)
124 out = filter(lambda x: not re.search(r"BUG=none$", x), out)
125
126 # TODO(machenbach): Handle multiple entries (e.g. BUG=123, 234).
127 def FormatIssue(text):
128 text = re.sub(r"BUG=v8:(.*)$", r"(issue \1)", text)
129 text = re.sub(r"BUG=chromium:(.*)$", r"(Chromium issue \1)", text)
130 text = re.sub(r"BUG=(.*)$", r"(Chromium issue \1)", text)
131 return " %s\n" % text
132
133 for line in map(FormatIssue, out):
134 AppendToFile(line, self.Config(CHANGELOG_ENTRY_FILE))
135
136 # Append the commit's author for reference.
137 args = "log -1 %s --format=\"%%w(80,8,8)(%%an)\"" % commit
138 author = self.Git(args).rstrip()
139 AppendToFile("%s\n\n" % author, self.Config(CHANGELOG_ENTRY_FILE))
140
141 msg = " Performance and stability improvements on all platforms.\n"
142 AppendToFile(msg, self.Config(CHANGELOG_ENTRY_FILE))
143
144class EditChangeLog(Step):
145 def __init__(self):
146 Step.__init__(self, "Edit ChangeLog entry.")
147
148 def RunStep(self):
149 print ("Please press <Return> to have your EDITOR open the ChangeLog "
150 "entry, then edit its contents to your liking. When you're done, "
151 "save the file and exit your EDITOR. ")
152 self.ReadLine()
153
154 self.Editor(self.Config(CHANGELOG_ENTRY_FILE))
155 handle, new_changelog = tempfile.mkstemp()
156 os.close(handle)
157
158 # (1) Eliminate tabs, (2) fix too little and (3) too much indentation, and
159 # (4) eliminate trailing whitespace.
160 changelog_entry = FileToText(self.Config(CHANGELOG_ENTRY_FILE)).rstrip()
161 changelog_entry = MSub(r"\t", r" ", changelog_entry)
162 changelog_entry = MSub(r"^ {1,7}([^ ])", r" \1", changelog_entry)
163 changelog_entry = MSub(r"^ {9,80}([^ ])", r" \1", changelog_entry)
164 changelog_entry = MSub(r" +$", r"", changelog_entry)
165
166 if changelog_entry == "":
167 self.Die("Empty ChangeLog entry.")
168
169 with open(new_changelog, "w") as f:
170 f.write(changelog_entry)
171 f.write("\n\n\n") # Explicitly insert two empty lines.
172
173 AppendToFile(FileToText(self.Config(CHANGELOG_FILE)), new_changelog)
174 TextToFile(FileToText(new_changelog), self.Config(CHANGELOG_FILE))
175 os.remove(new_changelog)
176
177
178class IncrementVersion(Step):
179 def __init__(self):
180 Step.__init__(self, "Increment version number.")
181
182 def RunStep(self):
183 self.RestoreIfUnset("build")
184 new_build = str(int(self._state["build"]) + 1)
185
186 if self.Confirm(("Automatically increment BUILD_NUMBER? (Saying 'n' will "
187 "fire up your EDITOR on %s so you can make arbitrary "
188 "changes. When you're done, save the file and exit your "
189 "EDITOR.)" % self.Config(VERSION_FILE))):
190 text = FileToText(self.Config(VERSION_FILE))
191 text = MSub(r"(?<=#define BUILD_NUMBER)(?P<space>\s+)\d*$",
192 r"\g<space>%s" % new_build,
193 text)
194 TextToFile(text, self.Config(VERSION_FILE))
195 else:
196 self.Editor(self.Config(VERSION_FILE))
197
198 self.ReadAndPersistVersion("new_")
199
200
201class CommitLocal(Step):
202 def __init__(self):
203 Step.__init__(self, "Commit to local branch.")
204
205 def RunStep(self):
206 self.RestoreVersionIfUnset("new_")
207 prep_commit_msg = ("Prepare push to trunk. "
208 "Now working on version %s.%s.%s." % (self._state["new_major"],
209 self._state["new_minor"],
210 self._state["new_build"]))
211 self.Persist("prep_commit_msg", prep_commit_msg)
212 if self.Git("commit -a -m \"%s\"" % prep_commit_msg) is None:
213 self.Die("'git commit -a' failed.")
214
215
216class CommitRepository(Step):
217 def __init__(self):
218 Step.__init__(self, "Commit to the repository.")
219
220 def RunStep(self):
221 self.WaitForLGTM()
222 # Re-read the ChangeLog entry (to pick up possible changes).
223 # FIXME(machenbach): This was hanging once with a broken pipe.
machenbach@chromium.org0cc09502013-11-13 12:20:55 +0000224 TextToFile(GetLastChangeLogEntries(self.Config(CHANGELOG_FILE)),
225 self.Config(CHANGELOG_ENTRY_FILE))
machenbach@chromium.org935a7792013-11-12 09:05:18 +0000226
machenbach@chromium.orgb5be0a92013-11-15 10:32:41 +0000227 if self.Git("cl dcommit -f", "PRESUBMIT_TREE_CHECK=\"skip\"") is None:
machenbach@chromium.org935a7792013-11-12 09:05:18 +0000228 self.Die("'git cl dcommit' failed, please try again.")
229
230
231class StragglerCommits(Step):
232 def __init__(self):
233 Step.__init__(self, "Fetch straggler commits that sneaked in since this "
234 "script was started.")
235
236 def RunStep(self):
237 if self.Git("svn fetch") is None:
238 self.Die("'git svn fetch' failed.")
239 self.Git("checkout svn/bleeding_edge")
240 self.RestoreIfUnset("prep_commit_msg")
241 args = "log -1 --format=%%H --grep=\"%s\"" % self._state["prep_commit_msg"]
242 prepare_commit_hash = self.Git(args).strip()
243 self.Persist("prepare_commit_hash", prepare_commit_hash)
244
245
246class SquashCommits(Step):
247 def __init__(self):
248 Step.__init__(self, "Squash commits into one.")
249
250 def RunStep(self):
251 # Instead of relying on "git rebase -i", we'll just create a diff, because
252 # that's easier to automate.
253 self.RestoreIfUnset("prepare_commit_hash")
254 args = "diff svn/trunk %s" % self._state["prepare_commit_hash"]
255 TextToFile(self.Git(args), self.Config(PATCH_FILE))
256
257 # Convert the ChangeLog entry to commit message format:
258 # - remove date
259 # - remove indentation
260 # - merge paragraphs into single long lines, keeping empty lines between
261 # them.
262 self.RestoreIfUnset("date")
263 changelog_entry = FileToText(self.Config(CHANGELOG_ENTRY_FILE))
264
265 # TODO(machenbach): This could create a problem if the changelog contained
266 # any quotation marks.
267 text = Command("echo \"%s\" \
268 | sed -e \"s/^%s: //\" \
269 | sed -e 's/^ *//' \
270 | awk '{ \
271 if (need_space == 1) {\
272 printf(\" \");\
273 };\
274 printf(\"%%s\", $0);\
275 if ($0 ~ /^$/) {\
276 printf(\"\\n\\n\");\
277 need_space = 0;\
278 } else {\
279 need_space = 1;\
280 }\
281 }'" % (changelog_entry, self._state["date"]))
282
283 if not text:
284 self.Die("Commit message editing failed.")
285 TextToFile(text, self.Config(COMMITMSG_FILE))
286 os.remove(self.Config(CHANGELOG_ENTRY_FILE))
287
288
289class NewBranch(Step):
290 def __init__(self):
291 Step.__init__(self, "Create a new branch from trunk.")
292
293 def RunStep(self):
294 if self.Git("checkout -b %s svn/trunk" % self.Config(TRUNKBRANCH)) is None:
295 self.Die("Checking out a new branch '%s' failed." %
296 self.Config(TRUNKBRANCH))
297
298
299class ApplyChanges(Step):
300 def __init__(self):
301 Step.__init__(self, "Apply squashed changes.")
302
303 def RunStep(self):
304 self.ApplyPatch(self.Config(PATCH_FILE))
305 Command("rm", "-f %s*" % self.Config(PATCH_FILE))
306
307
308class SetVersion(Step):
309 def __init__(self):
310 Step.__init__(self, "Set correct version for trunk.")
311
312 def RunStep(self):
313 self.RestoreVersionIfUnset()
314 output = ""
315 for line in FileToText(self.Config(VERSION_FILE)).splitlines():
316 if line.startswith("#define MAJOR_VERSION"):
317 line = re.sub("\d+$", self._state["major"], line)
318 elif line.startswith("#define MINOR_VERSION"):
319 line = re.sub("\d+$", self._state["minor"], line)
320 elif line.startswith("#define BUILD_NUMBER"):
321 line = re.sub("\d+$", self._state["build"], line)
322 elif line.startswith("#define PATCH_LEVEL"):
323 line = re.sub("\d+$", "0", line)
324 elif line.startswith("#define IS_CANDIDATE_VERSION"):
325 line = re.sub("\d+$", "0", line)
326 output += "%s\n" % line
327 TextToFile(output, self.Config(VERSION_FILE))
328
329
330class CommitTrunk(Step):
331 def __init__(self):
332 Step.__init__(self, "Commit to local trunk branch.")
333
334 def RunStep(self):
335 self.Git("add \"%s\"" % self.Config(VERSION_FILE))
336 if self.Git("commit -F \"%s\"" % self.Config(COMMITMSG_FILE)) is None:
337 self.Die("'git commit' failed.")
338 Command("rm", "-f %s*" % self.Config(COMMITMSG_FILE))
339
340
341class SanityCheck(Step):
342 def __init__(self):
343 Step.__init__(self, "Sanity check.")
344
345 def RunStep(self):
346 if not self.Confirm("Please check if your local checkout is sane: Inspect "
347 "%s, compile, run tests. Do you want to commit this new trunk "
348 "revision to the repository?" % self.Config(VERSION_FILE)):
349 self.Die("Execution canceled.")
350
351
352class CommitSVN(Step):
353 def __init__(self):
354 Step.__init__(self, "Commit to SVN.")
355
356 def RunStep(self):
357 result = self.Git("svn dcommit 2>&1")
358 if not result:
359 self.Die("'git svn dcommit' failed.")
360 result = filter(lambda x: re.search(r"^Committed r[0-9]+", x),
361 result.splitlines())
362 if len(result) > 0:
363 trunk_revision = re.sub(r"^Committed r([0-9]+)", r"\1", result[0])
364
365 # Sometimes grepping for the revision fails. No idea why. If you figure
366 # out why it is flaky, please do fix it properly.
367 if not trunk_revision:
368 print("Sorry, grepping for the SVN revision failed. Please look for it "
369 "in the last command's output above and provide it manually (just "
370 "the number, without the leading \"r\").")
371 while not trunk_revision:
372 print "> ",
373 trunk_revision = self.ReadLine()
374 self.Persist("trunk_revision", trunk_revision)
375
376
377class TagRevision(Step):
378 def __init__(self):
379 Step.__init__(self, "Tag the new revision.")
380
381 def RunStep(self):
382 self.RestoreVersionIfUnset()
383 ver = "%s.%s.%s" % (self._state["major"],
384 self._state["minor"],
385 self._state["build"])
386 if self.Git("svn tag %s -m \"Tagging version %s\"" % (ver, ver)) is None:
387 self.Die("'git svn tag' failed.")
388
389
390class CheckChromium(Step):
391 def __init__(self):
392 Step.__init__(self, "Ask for chromium checkout.")
393
394 def Run(self):
395 chrome_path = self._options.c
396 if not chrome_path:
397 print ("Do you have a \"NewGit\" Chromium checkout and want "
398 "this script to automate creation of the roll CL? If yes, enter the "
399 "path to (and including) the \"src\" directory here, otherwise just "
400 "press <Return>: "),
401 chrome_path = self.ReadLine()
402 self.Persist("chrome_path", chrome_path)
403
404
405class SwitchChromium(Step):
406 def __init__(self):
407 Step.__init__(self, "Switch to Chromium checkout.", requires="chrome_path")
408
409 def RunStep(self):
410 v8_path = os.getcwd()
411 self.Persist("v8_path", v8_path)
412 os.chdir(self._state["chrome_path"])
413 self.InitialEnvironmentChecks()
414 # Check for a clean workdir.
415 if self.Git("status -s -uno").strip() != "":
416 self.Die("Workspace is not clean. Please commit or undo your changes.")
417 # Assert that the DEPS file is there.
418 if not os.path.exists(self.Config(DEPS_FILE)):
419 self.Die("DEPS file not present.")
420
421
422class UpdateChromiumCheckout(Step):
423 def __init__(self):
424 Step.__init__(self, "Update the checkout and create a new branch.",
425 requires="chrome_path")
426
427 def RunStep(self):
428 os.chdir(self._state["chrome_path"])
429 if self.Git("checkout master") is None:
430 self.Die("'git checkout master' failed.")
431 if self.Git("pull") is None:
432 self.Die("'git pull' failed, please try again.")
433
434 self.RestoreIfUnset("trunk_revision")
435 args = "checkout -b v8-roll-%s" % self._state["trunk_revision"]
436 if self.Git(args) is None:
437 self.Die("Failed to checkout a new branch.")
438
439
440class UploadCL(Step):
441 def __init__(self):
442 Step.__init__(self, "Create and upload CL.", requires="chrome_path")
443
444 def RunStep(self):
445 os.chdir(self._state["chrome_path"])
446
447 # Patch DEPS file.
448 self.RestoreIfUnset("trunk_revision")
449 deps = FileToText(self.Config(DEPS_FILE))
450 deps = re.sub("(?<=\"v8_revision\": \")([0-9]+)(?=\")",
451 self._state["trunk_revision"],
452 deps)
453 TextToFile(deps, self.Config(DEPS_FILE))
454
455 self.RestoreVersionIfUnset()
456 ver = "%s.%s.%s" % (self._state["major"],
457 self._state["minor"],
458 self._state["build"])
459 print "Please enter the email address of a reviewer for the roll CL: ",
460 rev = self.ReadLine()
461 args = "commit -am \"Update V8 to version %s.\n\nTBR=%s\"" % (ver, rev)
462 if self.Git(args) is None:
463 self.Die("'git commit' failed.")
464 if self.Git("cl upload --send-mail", pipe=False) is None:
465 self.Die("'git cl upload' failed, please try again.")
466 print "CL uploaded."
467
468
469class SwitchV8(Step):
470 def __init__(self):
471 Step.__init__(self, "Returning to V8 checkout.", requires="chrome_path")
472
473 def RunStep(self):
474 self.RestoreIfUnset("v8_path")
475 os.chdir(self._state["v8_path"])
476
477
478class CleanUp(Step):
479 def __init__(self):
480 Step.__init__(self, "Done!")
481
482 def RunStep(self):
483 self.RestoreVersionIfUnset()
484 ver = "%s.%s.%s" % (self._state["major"],
485 self._state["minor"],
486 self._state["build"])
487 self.RestoreIfUnset("trunk_revision")
488 self.RestoreIfUnset("chrome_path")
489
490 if self._state["chrome_path"]:
491 print("Congratulations, you have successfully created the trunk "
492 "revision %s and rolled it into Chromium. Please don't forget to "
493 "update the v8rel spreadsheet:" % ver)
494 else:
495 print("Congratulations, you have successfully created the trunk "
496 "revision %s. Please don't forget to roll this new version into "
497 "Chromium, and to update the v8rel spreadsheet:" % ver)
498 print "%s\ttrunk\t%s" % (ver, self._state["trunk_revision"])
499
500 self.CommonCleanup()
501 if self.Config(TRUNKBRANCH) != self._state["current_branch"]:
502 self.Git("branch -D %s" % self.Config(TRUNKBRANCH))
503
504
505def RunScript(config,
506 options,
507 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER):
508 step_classes = [
509 Preparation,
510 FreshBranch,
511 DetectLastPush,
512 PrepareChangeLog,
513 EditChangeLog,
514 IncrementVersion,
515 CommitLocal,
516 UploadStep,
517 CommitRepository,
518 StragglerCommits,
519 SquashCommits,
520 NewBranch,
521 ApplyChanges,
522 SetVersion,
523 CommitTrunk,
524 SanityCheck,
525 CommitSVN,
526 TagRevision,
527 CheckChromium,
528 SwitchChromium,
529 UpdateChromiumCheckout,
530 UploadCL,
531 SwitchV8,
532 CleanUp,
533 ]
534
535 state = {}
536 steps = []
537 number = 0
538
539 for step_class in step_classes:
540 # TODO(machenbach): Factory methods.
541 step = step_class()
542 step.SetNumber(number)
543 step.SetConfig(config)
544 step.SetOptions(options)
545 step.SetState(state)
546 step.SetSideEffectHandler(side_effect_handler)
547 steps.append(step)
548 number += 1
549
550 for step in steps[options.s:]:
551 step.Run()
552
553
554def BuildOptions():
555 result = optparse.OptionParser()
556 result.add_option("-s", "--step", dest="s",
557 help="Specify the step where to start work. Default: 0.",
558 default=0, type="int")
559 result.add_option("-l", "--last-push", dest="l",
560 help=("Manually specify the git commit ID "
561 "of the last push to trunk."))
562 result.add_option("-c", "--chromium", dest="c",
563 help=("Specify the path to your Chromium src/ "
564 "directory to automate the V8 roll."))
565 return result
566
567
568def ProcessOptions(options):
569 if options.s < 0:
570 print "Bad step number %d" % options.s
571 return False
572 return True
573
574
575def Main():
576 parser = BuildOptions()
577 (options, args) = parser.parse_args()
578 if not ProcessOptions(options):
579 parser.print_help()
580 return 1
581 RunScript(CONFIG, options)
582
583if __name__ == "__main__":
584 sys.exit(Main())