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
29import argparse
30import os
31import sys
32import tempfile
33import urllib2
34
35from common_includes import *
36
37PUSH_MESSAGE_SUFFIX = " (based on bleeding_edge revision r%d)"
38PUSH_MESSAGE_RE = re.compile(r".* \(based on bleeding_edge revision r(\d+)\)$")
39
40class Preparation(Step):
41  MESSAGE = "Preparation."
42
43  def RunStep(self):
44    self.InitialEnvironmentChecks(self.default_cwd)
45    self.CommonPrepare()
46
47    if(self["current_branch"] == self.Config("TRUNKBRANCH")
48       or self["current_branch"] == self.Config("BRANCHNAME")):
49      print "Warning: Script started on branch %s" % self["current_branch"]
50
51    self.PrepareBranch()
52    self.DeleteBranch(self.Config("TRUNKBRANCH"))
53
54
55class FreshBranch(Step):
56  MESSAGE = "Create a fresh branch."
57
58  def RunStep(self):
59    self.GitCreateBranch(self.Config("BRANCHNAME"), "svn/bleeding_edge")
60
61
62class PreparePushRevision(Step):
63  MESSAGE = "Check which revision to push."
64
65  def RunStep(self):
66    if self._options.revision:
67      self["push_hash"] = self.GitSVNFindGitHash(self._options.revision)
68    else:
69      self["push_hash"] = self.GitLog(n=1, format="%H", git_hash="HEAD")
70    if not self["push_hash"]:  # pragma: no cover
71      self.Die("Could not determine the git hash for the push.")
72
73
74class DetectLastPush(Step):
75  MESSAGE = "Detect commit ID of last push to trunk."
76
77  def RunStep(self):
78    last_push = self._options.last_push or self.FindLastTrunkPush()
79    while True:
80      # Print assumed commit, circumventing git's pager.
81      print self.GitLog(n=1, git_hash=last_push)
82      if self.Confirm("Is the commit printed above the last push to trunk?"):
83        break
84      last_push = self.FindLastTrunkPush(parent_hash=last_push)
85
86    if self._options.last_bleeding_edge:
87      # Read the bleeding edge revision of the last push from a command-line
88      # option.
89      last_push_bleeding_edge = self._options.last_bleeding_edge
90    else:
91      # Retrieve the bleeding edge revision of the last push from the text in
92      # the push commit message.
93      last_push_title = self.GitLog(n=1, format="%s", git_hash=last_push)
94      last_push_be_svn = PUSH_MESSAGE_RE.match(last_push_title).group(1)
95      if not last_push_be_svn:  # pragma: no cover
96        self.Die("Could not retrieve bleeding edge revision for trunk push %s"
97                 % last_push)
98      last_push_bleeding_edge = self.GitSVNFindGitHash(last_push_be_svn)
99      if not last_push_bleeding_edge:  # pragma: no cover
100        self.Die("Could not retrieve bleeding edge git hash for trunk push %s"
101                 % last_push)
102
103    # This points to the svn revision of the last push on trunk.
104    self["last_push_trunk"] = last_push
105    # This points to the last bleeding_edge revision that went into the last
106    # push.
107    # TODO(machenbach): Do we need a check to make sure we're not pushing a
108    # revision older than the last push? If we do this, the output of the
109    # current change log preparation won't make much sense.
110    self["last_push_bleeding_edge"] = last_push_bleeding_edge
111
112
113# TODO(machenbach): Code similarities with bump_up_version.py. Merge after
114# turning this script into a pure git script.
115class GetCurrentBleedingEdgeVersion(Step):
116  MESSAGE = "Get latest bleeding edge version."
117
118  def RunStep(self):
119    self.GitCheckoutFile(VERSION_FILE, "svn/bleeding_edge")
120
121    # Store latest version.
122    self.ReadAndPersistVersion("latest_")
123    self["latest_version"] = self.ArrayToVersion("latest_")
124    print "Bleeding edge version: %s" % self["latest_version"]
125
126
127class IncrementVersion(Step):
128  MESSAGE = "Increment version number."
129
130  def RunStep(self):
131    # Retrieve current version from last trunk push.
132    self.GitCheckoutFile(VERSION_FILE, self["last_push_trunk"])
133    self.ReadAndPersistVersion()
134    self["trunk_version"] = self.ArrayToVersion("")
135
136    if self["latest_build"] == "9999":  # pragma: no cover
137      # If version control on bleeding edge was switched off, just use the last
138      # trunk version.
139      self["latest_version"] = self["trunk_version"]
140
141    if SortingKey(self["trunk_version"]) < SortingKey(self["latest_version"]):
142      # If the version on bleeding_edge is newer than on trunk, use it.
143      self.GitCheckoutFile(VERSION_FILE, "svn/bleeding_edge")
144      self.ReadAndPersistVersion()
145
146    if self.Confirm(("Automatically increment BUILD_NUMBER? (Saying 'n' will "
147                     "fire up your EDITOR on %s so you can make arbitrary "
148                     "changes. When you're done, save the file and exit your "
149                     "EDITOR.)" % VERSION_FILE)):
150
151      text = FileToText(os.path.join(self.default_cwd, VERSION_FILE))
152      text = MSub(r"(?<=#define BUILD_NUMBER)(?P<space>\s+)\d*$",
153                  r"\g<space>%s" % str(int(self["build"]) + 1),
154                  text)
155      TextToFile(text, os.path.join(self.default_cwd, VERSION_FILE))
156    else:
157      self.Editor(os.path.join(self.default_cwd, VERSION_FILE))
158
159    # Variables prefixed with 'new_' contain the new version numbers for the
160    # ongoing trunk push.
161    self.ReadAndPersistVersion("new_")
162
163    # Make sure patch level is 0 in a new push.
164    self["new_patch"] = "0"
165
166    self["version"] = "%s.%s.%s" % (self["new_major"],
167                                    self["new_minor"],
168                                    self["new_build"])
169
170
171class PrepareChangeLog(Step):
172  MESSAGE = "Prepare raw ChangeLog entry."
173
174  def Reload(self, body):
175    """Attempts to reload the commit message from rietveld in order to allow
176    late changes to the LOG flag. Note: This is brittle to future changes of
177    the web page name or structure.
178    """
179    match = re.search(r"^Review URL: https://codereview\.chromium\.org/(\d+)$",
180                      body, flags=re.M)
181    if match:
182      cl_url = ("https://codereview.chromium.org/%s/description"
183                % match.group(1))
184      try:
185        # Fetch from Rietveld but only retry once with one second delay since
186        # there might be many revisions.
187        body = self.ReadURL(cl_url, wait_plan=[1])
188      except urllib2.URLError:  # pragma: no cover
189        pass
190    return body
191
192  def RunStep(self):
193    self["date"] = self.GetDate()
194    output = "%s: Version %s\n\n" % (self["date"], self["version"])
195    TextToFile(output, self.Config("CHANGELOG_ENTRY_FILE"))
196    commits = self.GitLog(format="%H",
197        git_hash="%s..%s" % (self["last_push_bleeding_edge"],
198                             self["push_hash"]))
199
200    # Cache raw commit messages.
201    commit_messages = [
202      [
203        self.GitLog(n=1, format="%s", git_hash=commit),
204        self.Reload(self.GitLog(n=1, format="%B", git_hash=commit)),
205        self.GitLog(n=1, format="%an", git_hash=commit),
206      ] for commit in commits.splitlines()
207    ]
208
209    # Auto-format commit messages.
210    body = MakeChangeLogBody(commit_messages, auto_format=True)
211    AppendToFile(body, self.Config("CHANGELOG_ENTRY_FILE"))
212
213    msg = ("        Performance and stability improvements on all platforms."
214           "\n#\n# The change log above is auto-generated. Please review if "
215           "all relevant\n# commit messages from the list below are included."
216           "\n# All lines starting with # will be stripped.\n#\n")
217    AppendToFile(msg, self.Config("CHANGELOG_ENTRY_FILE"))
218
219    # Include unformatted commit messages as a reference in a comment.
220    comment_body = MakeComment(MakeChangeLogBody(commit_messages))
221    AppendToFile(comment_body, self.Config("CHANGELOG_ENTRY_FILE"))
222
223
224class EditChangeLog(Step):
225  MESSAGE = "Edit ChangeLog entry."
226
227  def RunStep(self):
228    print ("Please press <Return> to have your EDITOR open the ChangeLog "
229           "entry, then edit its contents to your liking. When you're done, "
230           "save the file and exit your EDITOR. ")
231    self.ReadLine(default="")
232    self.Editor(self.Config("CHANGELOG_ENTRY_FILE"))
233
234    # Strip comments and reformat with correct indentation.
235    changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE")).rstrip()
236    changelog_entry = StripComments(changelog_entry)
237    changelog_entry = "\n".join(map(Fill80, changelog_entry.splitlines()))
238    changelog_entry = changelog_entry.lstrip()
239
240    if changelog_entry == "":  # pragma: no cover
241      self.Die("Empty ChangeLog entry.")
242
243    # Safe new change log for adding it later to the trunk patch.
244    TextToFile(changelog_entry, self.Config("CHANGELOG_ENTRY_FILE"))
245
246
247class StragglerCommits(Step):
248  MESSAGE = ("Fetch straggler commits that sneaked in since this script was "
249             "started.")
250
251  def RunStep(self):
252    self.GitSVNFetch()
253    self.GitCheckout("svn/bleeding_edge")
254
255
256class SquashCommits(Step):
257  MESSAGE = "Squash commits into one."
258
259  def RunStep(self):
260    # Instead of relying on "git rebase -i", we'll just create a diff, because
261    # that's easier to automate.
262    TextToFile(self.GitDiff("svn/trunk", self["push_hash"]),
263               self.Config("PATCH_FILE"))
264
265    # Convert the ChangeLog entry to commit message format.
266    text = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
267
268    # Remove date and trailing white space.
269    text = re.sub(r"^%s: " % self["date"], "", text.rstrip())
270
271    # Retrieve svn revision for showing the used bleeding edge revision in the
272    # commit message.
273    self["svn_revision"] = self.GitSVNFindSVNRev(self["push_hash"])
274    suffix = PUSH_MESSAGE_SUFFIX % int(self["svn_revision"])
275    text = MSub(r"^(Version \d+\.\d+\.\d+)$", "\\1%s" % suffix, text)
276
277    # Remove indentation and merge paragraphs into single long lines, keeping
278    # empty lines between them.
279    def SplitMapJoin(split_text, fun, join_text):
280      return lambda text: join_text.join(map(fun, text.split(split_text)))
281    strip = lambda line: line.strip()
282    text = SplitMapJoin("\n\n", SplitMapJoin("\n", strip, " "), "\n\n")(text)
283
284    if not text:  # pragma: no cover
285      self.Die("Commit message editing failed.")
286    TextToFile(text, self.Config("COMMITMSG_FILE"))
287
288
289class NewBranch(Step):
290  MESSAGE = "Create a new branch from trunk."
291
292  def RunStep(self):
293    self.GitCreateBranch(self.Config("TRUNKBRANCH"), "svn/trunk")
294
295
296class ApplyChanges(Step):
297  MESSAGE = "Apply squashed changes."
298
299  def RunStep(self):
300    self.ApplyPatch(self.Config("PATCH_FILE"))
301    os.remove(self.Config("PATCH_FILE"))
302
303
304class AddChangeLog(Step):
305  MESSAGE = "Add ChangeLog changes to trunk branch."
306
307  def RunStep(self):
308    # The change log has been modified by the patch. Reset it to the version
309    # on trunk and apply the exact changes determined by this PrepareChangeLog
310    # step above.
311    self.GitCheckoutFile(self.Config("CHANGELOG_FILE"), "svn/trunk")
312    changelog_entry = FileToText(self.Config("CHANGELOG_ENTRY_FILE"))
313    old_change_log = FileToText(self.Config("CHANGELOG_FILE"))
314    new_change_log = "%s\n\n\n%s" % (changelog_entry, old_change_log)
315    TextToFile(new_change_log, self.Config("CHANGELOG_FILE"))
316    os.remove(self.Config("CHANGELOG_ENTRY_FILE"))
317
318
319class SetVersion(Step):
320  MESSAGE = "Set correct version for trunk."
321
322  def RunStep(self):
323    # The version file has been modified by the patch. Reset it to the version
324    # on trunk and apply the correct version.
325    self.GitCheckoutFile(VERSION_FILE, "svn/trunk")
326    self.SetVersion(os.path.join(self.default_cwd, VERSION_FILE), "new_")
327
328
329class CommitTrunk(Step):
330  MESSAGE = "Commit to local trunk branch."
331
332  def RunStep(self):
333    self.GitCommit(file_name = self.Config("COMMITMSG_FILE"))
334    os.remove(self.Config("COMMITMSG_FILE"))
335
336
337class SanityCheck(Step):
338  MESSAGE = "Sanity check."
339
340  def RunStep(self):
341    # TODO(machenbach): Run presubmit script here as it is now missing in the
342    # prepare push process.
343    if not self.Confirm("Please check if your local checkout is sane: Inspect "
344        "%s, compile, run tests. Do you want to commit this new trunk "
345        "revision to the repository?" % VERSION_FILE):
346      self.Die("Execution canceled.")  # pragma: no cover
347
348
349class CommitSVN(Step):
350  MESSAGE = "Commit to SVN."
351
352  def RunStep(self):
353    result = self.GitSVNDCommit()
354    if not result:  # pragma: no cover
355      self.Die("'git svn dcommit' failed.")
356    result = filter(lambda x: re.search(r"^Committed r[0-9]+", x),
357                    result.splitlines())
358    if len(result) > 0:
359      self["trunk_revision"] = re.sub(r"^Committed r([0-9]+)", r"\1",result[0])
360
361    # Sometimes grepping for the revision fails. No idea why. If you figure
362    # out why it is flaky, please do fix it properly.
363    if not self["trunk_revision"]:
364      print("Sorry, grepping for the SVN revision failed. Please look for it "
365            "in the last command's output above and provide it manually (just "
366            "the number, without the leading \"r\").")
367      self.DieNoManualMode("Can't prompt in forced mode.")
368      while not self["trunk_revision"]:
369        print "> ",
370        self["trunk_revision"] = self.ReadLine()
371
372
373class TagRevision(Step):
374  MESSAGE = "Tag the new revision."
375
376  def RunStep(self):
377    self.GitSVNTag(self["version"])
378
379
380class CleanUp(Step):
381  MESSAGE = "Done!"
382
383  def RunStep(self):
384    print("Congratulations, you have successfully created the trunk "
385          "revision %s. Please don't forget to roll this new version into "
386          "Chromium, and to update the v8rel spreadsheet:"
387          % self["version"])
388    print "%s\ttrunk\t%s" % (self["version"], self["trunk_revision"])
389
390    self.CommonCleanup()
391    if self.Config("TRUNKBRANCH") != self["current_branch"]:
392      self.GitDeleteBranch(self.Config("TRUNKBRANCH"))
393
394
395class PushToTrunk(ScriptsBase):
396  def _PrepareOptions(self, parser):
397    group = parser.add_mutually_exclusive_group()
398    group.add_argument("-f", "--force",
399                      help="Don't prompt the user.",
400                      default=False, action="store_true")
401    group.add_argument("-m", "--manual",
402                      help="Prompt the user at every important step.",
403                      default=False, action="store_true")
404    parser.add_argument("-b", "--last-bleeding-edge",
405                        help=("The git commit ID of the last bleeding edge "
406                              "revision that was pushed to trunk. This is "
407                              "used for the auto-generated ChangeLog entry."))
408    parser.add_argument("-l", "--last-push",
409                        help="The git commit ID of the last push to trunk.")
410    parser.add_argument("-R", "--revision",
411                        help="The svn revision to push (defaults to HEAD).")
412
413  def _ProcessOptions(self, options):  # pragma: no cover
414    if not options.manual and not options.reviewer:
415      print "A reviewer (-r) is required in (semi-)automatic mode."
416      return False
417    if not options.manual and not options.author:
418      print "Specify your chromium.org email with -a in (semi-)automatic mode."
419      return False
420    if options.revision and not int(options.revision) > 0:
421      print("The --revision flag must be a positiv integer pointing to a "
422            "valid svn revision.")
423      return False
424
425    options.tbr_commit = not options.manual
426    return True
427
428  def _Config(self):
429    return {
430      "BRANCHNAME": "prepare-push",
431      "TRUNKBRANCH": "trunk-push",
432      "PERSISTFILE_BASENAME": "/tmp/v8-push-to-trunk-tempfile",
433      "CHANGELOG_FILE": "ChangeLog",
434      "CHANGELOG_ENTRY_FILE": "/tmp/v8-push-to-trunk-tempfile-changelog-entry",
435      "PATCH_FILE": "/tmp/v8-push-to-trunk-tempfile-patch-file",
436      "COMMITMSG_FILE": "/tmp/v8-push-to-trunk-tempfile-commitmsg",
437    }
438
439  def _Steps(self):
440    return [
441      Preparation,
442      FreshBranch,
443      PreparePushRevision,
444      DetectLastPush,
445      GetCurrentBleedingEdgeVersion,
446      IncrementVersion,
447      PrepareChangeLog,
448      EditChangeLog,
449      StragglerCommits,
450      SquashCommits,
451      NewBranch,
452      ApplyChanges,
453      AddChangeLog,
454      SetVersion,
455      CommitTrunk,
456      SanityCheck,
457      CommitSVN,
458      TagRevision,
459      CleanUp,
460    ]
461
462
463if __name__ == "__main__":  # pragma: no cover
464  sys.exit(PushToTrunk().Run())
465