Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(162)

Side by Side Diff: tools/push-to-trunk/push_to_trunk.py

Issue 49653002: Add push-to-trunk python port. (Closed) Base URL: https://v8.googlecode.com/svn/branches/bleeding_edge
Patch Set: Review. Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « tools/push-to-trunk/common_includes.py ('k') | tools/push-to-trunk/test_scripts.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/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
29 import datetime
30 import optparse
31 import sys
32 import tempfile
33
34 from common_includes import *
35
36 TRUNKBRANCH = "TRUNKBRANCH"
37 CHROMIUM = "CHROMIUM"
38 DEPS_FILE = "DEPS_FILE"
39
40 CONFIG = {
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
55 class 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
65 class 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
75 class 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
93 class 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
144 class 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
178 class 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
201 class 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
216 class 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.
224 TextToFile(Command("cat %s | awk --posix '{\
225 if ($0 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}:/) {\
226 if (in_firstblock == 1) {\
227 exit 0;\
228 } else {\
229 in_firstblock = 1;\
230 }\
231 };\
232 print $0;\
233 }'" % self.Config(CHANGELOG_FILE)), self.Config(CHANGELOG_ENTRY_FILE))
234
235 if self.Git("cl dcommit", "PRESUBMIT_TREE_CHECK=\"skip\"") is None:
236 self.Die("'git cl dcommit' failed, please try again.")
237
238
239 class StragglerCommits(Step):
240 def __init__(self):
241 Step.__init__(self, "Fetch straggler commits that sneaked in since this "
242 "script was started.")
243
244 def RunStep(self):
245 if self.Git("svn fetch") is None:
246 self.Die("'git svn fetch' failed.")
247 self.Git("checkout svn/bleeding_edge")
248 self.RestoreIfUnset("prep_commit_msg")
249 args = "log -1 --format=%%H --grep=\"%s\"" % self._state["prep_commit_msg"]
250 prepare_commit_hash = self.Git(args).strip()
251 self.Persist("prepare_commit_hash", prepare_commit_hash)
252
253
254 class SquashCommits(Step):
255 def __init__(self):
256 Step.__init__(self, "Squash commits into one.")
257
258 def RunStep(self):
259 # Instead of relying on "git rebase -i", we'll just create a diff, because
260 # that's easier to automate.
261 self.RestoreIfUnset("prepare_commit_hash")
262 args = "diff svn/trunk %s" % self._state["prepare_commit_hash"]
263 TextToFile(self.Git(args), self.Config(PATCH_FILE))
264
265 # Convert the ChangeLog entry to commit message format:
266 # - remove date
267 # - remove indentation
268 # - merge paragraphs into single long lines, keeping empty lines between
269 # them.
270 self.RestoreIfUnset("date")
271 changelog_entry = FileToText(self.Config(CHANGELOG_ENTRY_FILE))
272
273 # TODO(machenbach): This could create a problem if the changelog contained
274 # any quotation marks.
275 text = Command("echo \"%s\" \
276 | sed -e \"s/^%s: //\" \
277 | sed -e 's/^ *//' \
278 | awk '{ \
279 if (need_space == 1) {\
280 printf(\" \");\
281 };\
282 printf(\"%%s\", $0);\
283 if ($0 ~ /^$/) {\
284 printf(\"\\n\\n\");\
285 need_space = 0;\
286 } else {\
287 need_space = 1;\
288 }\
289 }'" % (changelog_entry, self._state["date"]))
290
291 if not text:
292 self.Die("Commit message editing failed.")
293 TextToFile(text, self.Config(COMMITMSG_FILE))
294 os.remove(self.Config(CHANGELOG_ENTRY_FILE))
295
296
297 class NewBranch(Step):
298 def __init__(self):
299 Step.__init__(self, "Create a new branch from trunk.")
300
301 def RunStep(self):
302 if self.Git("checkout -b %s svn/trunk" % self.Config(TRUNKBRANCH)) is None:
303 self.Die("Checking out a new branch '%s' failed." %
304 self.Config(TRUNKBRANCH))
305
306
307 class ApplyChanges(Step):
308 def __init__(self):
309 Step.__init__(self, "Apply squashed changes.")
310
311 def RunStep(self):
312 self.ApplyPatch(self.Config(PATCH_FILE))
313 Command("rm", "-f %s*" % self.Config(PATCH_FILE))
314
315
316 class SetVersion(Step):
317 def __init__(self):
318 Step.__init__(self, "Set correct version for trunk.")
319
320 def RunStep(self):
321 self.RestoreVersionIfUnset()
322 output = ""
323 for line in FileToText(self.Config(VERSION_FILE)).splitlines():
324 if line.startswith("#define MAJOR_VERSION"):
325 line = re.sub("\d+$", self._state["major"], line)
326 elif line.startswith("#define MINOR_VERSION"):
327 line = re.sub("\d+$", self._state["minor"], line)
328 elif line.startswith("#define BUILD_NUMBER"):
329 line = re.sub("\d+$", self._state["build"], line)
330 elif line.startswith("#define PATCH_LEVEL"):
331 line = re.sub("\d+$", "0", line)
332 elif line.startswith("#define IS_CANDIDATE_VERSION"):
333 line = re.sub("\d+$", "0", line)
334 output += "%s\n" % line
335 TextToFile(output, self.Config(VERSION_FILE))
336
337
338 class CommitTrunk(Step):
339 def __init__(self):
340 Step.__init__(self, "Commit to local trunk branch.")
341
342 def RunStep(self):
343 self.Git("add \"%s\"" % self.Config(VERSION_FILE))
344 if self.Git("commit -F \"%s\"" % self.Config(COMMITMSG_FILE)) is None:
345 self.Die("'git commit' failed.")
346 Command("rm", "-f %s*" % self.Config(COMMITMSG_FILE))
347
348
349 class SanityCheck(Step):
350 def __init__(self):
351 Step.__init__(self, "Sanity check.")
352
353 def RunStep(self):
354 if not self.Confirm("Please check if your local checkout is sane: Inspect "
355 "%s, compile, run tests. Do you want to commit this new trunk "
356 "revision to the repository?" % self.Config(VERSION_FILE)):
357 self.Die("Execution canceled.")
358
359
360 class CommitSVN(Step):
361 def __init__(self):
362 Step.__init__(self, "Commit to SVN.")
363
364 def RunStep(self):
365 result = self.Git("svn dcommit 2>&1")
366 if not result:
367 self.Die("'git svn dcommit' failed.")
368 result = filter(lambda x: re.search(r"^Committed r[0-9]+", x),
369 result.splitlines())
370 if len(result) > 0:
371 trunk_revision = re.sub(r"^Committed r([0-9]+)", r"\1", result[0])
372
373 # Sometimes grepping for the revision fails. No idea why. If you figure
374 # out why it is flaky, please do fix it properly.
375 if not trunk_revision:
376 print("Sorry, grepping for the SVN revision failed. Please look for it "
377 "in the last command's output above and provide it manually (just "
378 "the number, without the leading \"r\").")
379 while not trunk_revision:
380 print "> ",
381 trunk_revision = self.ReadLine()
382 self.Persist("trunk_revision", trunk_revision)
383
384
385 class TagRevision(Step):
386 def __init__(self):
387 Step.__init__(self, "Tag the new revision.")
388
389 def RunStep(self):
390 self.RestoreVersionIfUnset()
391 ver = "%s.%s.%s" % (self._state["major"],
392 self._state["minor"],
393 self._state["build"])
394 if self.Git("svn tag %s -m \"Tagging version %s\"" % (ver, ver)) is None:
395 self.Die("'git svn tag' failed.")
396
397
398 class CheckChromium(Step):
399 def __init__(self):
400 Step.__init__(self, "Ask for chromium checkout.")
401
402 def Run(self):
403 chrome_path = self._options.c
404 if not chrome_path:
405 print ("Do you have a \"NewGit\" Chromium checkout and want "
406 "this script to automate creation of the roll CL? If yes, enter the "
407 "path to (and including) the \"src\" directory here, otherwise just "
408 "press <Return>: "),
409 chrome_path = self.ReadLine()
410 self.Persist("chrome_path", chrome_path)
411
412
413 class SwitchChromium(Step):
414 def __init__(self):
415 Step.__init__(self, "Switch to Chromium checkout.", requires="chrome_path")
416
417 def RunStep(self):
418 v8_path = os.getcwd()
419 self.Persist("v8_path", v8_path)
420 os.chdir(self._state["chrome_path"])
421 self.InitialEnvironmentChecks()
422 # Check for a clean workdir.
423 if self.Git("status -s -uno").strip() != "":
424 self.Die("Workspace is not clean. Please commit or undo your changes.")
425 # Assert that the DEPS file is there.
426 if not os.path.exists(self.Config(DEPS_FILE)):
427 self.Die("DEPS file not present.")
428
429
430 class UpdateChromiumCheckout(Step):
431 def __init__(self):
432 Step.__init__(self, "Update the checkout and create a new branch.",
433 requires="chrome_path")
434
435 def RunStep(self):
436 os.chdir(self._state["chrome_path"])
437 if self.Git("checkout master") is None:
438 self.Die("'git checkout master' failed.")
439 if self.Git("pull") is None:
440 self.Die("'git pull' failed, please try again.")
441
442 self.RestoreIfUnset("trunk_revision")
443 args = "checkout -b v8-roll-%s" % self._state["trunk_revision"]
444 if self.Git(args) is None:
445 self.Die("Failed to checkout a new branch.")
446
447
448 class UploadCL(Step):
449 def __init__(self):
450 Step.__init__(self, "Create and upload CL.", requires="chrome_path")
451
452 def RunStep(self):
453 os.chdir(self._state["chrome_path"])
454
455 # Patch DEPS file.
456 self.RestoreIfUnset("trunk_revision")
457 deps = FileToText(self.Config(DEPS_FILE))
458 deps = re.sub("(?<=\"v8_revision\": \")([0-9]+)(?=\")",
459 self._state["trunk_revision"],
460 deps)
461 TextToFile(deps, self.Config(DEPS_FILE))
462
463 self.RestoreVersionIfUnset()
464 ver = "%s.%s.%s" % (self._state["major"],
465 self._state["minor"],
466 self._state["build"])
467 print "Please enter the email address of a reviewer for the roll CL: ",
468 rev = self.ReadLine()
469 args = "commit -am \"Update V8 to version %s.\n\nTBR=%s\"" % (ver, rev)
470 if self.Git(args) is None:
471 self.Die("'git commit' failed.")
472 if self.Git("cl upload --send-mail", pipe=False) is None:
473 self.Die("'git cl upload' failed, please try again.")
474 print "CL uploaded."
475
476
477 class SwitchV8(Step):
478 def __init__(self):
479 Step.__init__(self, "Returning to V8 checkout.", requires="chrome_path")
480
481 def RunStep(self):
482 self.RestoreIfUnset("v8_path")
483 os.chdir(self._state["v8_path"])
484
485
486 class CleanUp(Step):
487 def __init__(self):
488 Step.__init__(self, "Done!")
489
490 def RunStep(self):
491 self.RestoreVersionIfUnset()
492 ver = "%s.%s.%s" % (self._state["major"],
493 self._state["minor"],
494 self._state["build"])
495 self.RestoreIfUnset("trunk_revision")
496 self.RestoreIfUnset("chrome_path")
497
498 if self._state["chrome_path"]:
499 print("Congratulations, you have successfully created the trunk "
500 "revision %s and rolled it into Chromium. Please don't forget to "
501 "update the v8rel spreadsheet:" % ver)
502 else:
503 print("Congratulations, you have successfully created the trunk "
504 "revision %s. Please don't forget to roll this new version into "
505 "Chromium, and to update the v8rel spreadsheet:" % ver)
506 print "%s\ttrunk\t%s" % (ver, self._state["trunk_revision"])
507
508 self.CommonCleanup()
509 if self.Config(TRUNKBRANCH) != self._state["current_branch"]:
510 self.Git("branch -D %s" % self.Config(TRUNKBRANCH))
511
512
513 def RunScript(config,
514 options,
515 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER):
516 step_classes = [
517 Preparation,
518 FreshBranch,
519 DetectLastPush,
520 PrepareChangeLog,
521 EditChangeLog,
522 IncrementVersion,
523 CommitLocal,
524 UploadStep,
525 CommitRepository,
526 StragglerCommits,
527 SquashCommits,
528 NewBranch,
529 ApplyChanges,
530 SetVersion,
531 CommitTrunk,
532 SanityCheck,
533 CommitSVN,
534 TagRevision,
535 CheckChromium,
536 SwitchChromium,
537 UpdateChromiumCheckout,
538 UploadCL,
539 SwitchV8,
540 CleanUp,
541 ]
542
543 state = {}
544 steps = []
545 number = 0
546
547 for step_class in step_classes:
548 # TODO(machenbach): Factory methods.
549 step = step_class()
550 step.SetNumber(number)
551 step.SetConfig(config)
552 step.SetOptions(options)
553 step.SetState(state)
554 step.SetSideEffectHandler(side_effect_handler)
555 steps.append(step)
556 number += 1
557
558 for step in steps[options.s:]:
559 step.Run()
560
561
562 def BuildOptions():
563 result = optparse.OptionParser()
564 result.add_option("-s", "--step", dest="s",
565 help="Specify the step where to start work. Default: 0.",
566 default=0, type="int")
567 result.add_option("-l", "--last-push", dest="l",
568 help=("Manually specify the git commit ID "
569 "of the last push to trunk."))
570 result.add_option("-c", "--chromium", dest="c",
571 help=("Specify the path to your Chromium src/ "
572 "directory to automate the V8 roll."))
573 return result
574
575
576 def ProcessOptions(options):
577 if options.s < 0:
578 print "Bad step number %d" % options.s
579 return False
580 return True
581
582
583 def Main():
584 parser = BuildOptions()
585 (options, args) = parser.parse_args()
586 if not ProcessOptions(options):
587 parser.print_help()
588 return 1
589 RunScript(CONFIG, options)
590
591 if __name__ == "__main__":
592 sys.exit(Main())
OLDNEW
« no previous file with comments | « tools/push-to-trunk/common_includes.py ('k') | tools/push-to-trunk/test_scripts.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698