blob: 06b7ebe53a3c3655e41048c186bef308b59402c0 [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 os
30import re
31import subprocess
32import sys
33
34PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME"
35TEMP_BRANCH = "TEMP_BRANCH"
36BRANCHNAME = "BRANCHNAME"
37DOT_GIT_LOCATION = "DOT_GIT_LOCATION"
38VERSION_FILE = "VERSION_FILE"
39CHANGELOG_FILE = "CHANGELOG_FILE"
40CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE"
41COMMITMSG_FILE = "COMMITMSG_FILE"
42PATCH_FILE = "PATCH_FILE"
43
44
45def TextToFile(text, file_name):
46 with open(file_name, "w") as f:
47 f.write(text)
48
49
50def AppendToFile(text, file_name):
51 with open(file_name, "a") as f:
52 f.write(text)
53
54
55def LinesInFile(file_name):
56 with open(file_name) as f:
57 for line in f:
58 yield line
59
60
61def FileToText(file_name):
62 with open(file_name) as f:
63 return f.read()
64
65
66def MSub(rexp, replacement, text):
67 return re.sub(rexp, replacement, text, flags=re.MULTILINE)
68
69
machenbach@chromium.org0cc09502013-11-13 12:20:55 +000070def GetLastChangeLogEntries(change_log_file):
71 result = []
72 for line in LinesInFile(change_log_file):
73 if re.search(r"^\d{4}-\d{2}-\d{2}:", line) and result: break
74 result.append(line)
75 return "".join(result)
76
77
machenbach@chromium.orgaf9cfcb2013-11-19 11:05:18 +000078def MakeChangeLogBody(commit_generator):
79 result = ""
80 for (title, body, author) in commit_generator():
81 # Add the commit's title line.
82 result += "%s\n" % title.rstrip()
83
84 # Grep for "BUG=xxxx" lines in the commit message and convert them to
85 # "(issue xxxx)".
86 out = body.splitlines()
87 out = filter(lambda x: re.search(r"^BUG=", x), out)
88 out = filter(lambda x: not re.search(r"BUG=$", x), out)
89 out = filter(lambda x: not re.search(r"BUG=none$", x), out)
90
91 # TODO(machenbach): Handle multiple entries (e.g. BUG=123, 234).
92 def FormatIssue(text):
93 text = re.sub(r"BUG=v8:(.*)$", r"(issue \1)", text)
94 text = re.sub(r"BUG=chromium:(.*)$", r"(Chromium issue \1)", text)
95 text = re.sub(r"BUG=(.*)$", r"(Chromium issue \1)", text)
96 return " %s\n" % text
97
98 for line in map(FormatIssue, out):
99 result += line
100
101 # Append the commit's author for reference.
102 result += "%s\n\n" % author.rstrip()
103 return result
104
105
machenbach@chromium.org935a7792013-11-12 09:05:18 +0000106# Some commands don't like the pipe, e.g. calling vi from within the script or
107# from subscripts like git cl upload.
108def Command(cmd, args="", prefix="", pipe=True):
109 cmd_line = "%s %s %s" % (prefix, cmd, args)
110 print "Command: %s" % cmd_line
111 try:
112 if pipe:
113 return subprocess.check_output(cmd_line, shell=True)
114 else:
115 return subprocess.check_call(cmd_line, shell=True)
116 except subprocess.CalledProcessError:
117 return None
118
119
120# Wrapper for side effects.
121class SideEffectHandler(object):
122 def Command(self, cmd, args="", prefix="", pipe=True):
123 return Command(cmd, args, prefix, pipe)
124
125 def ReadLine(self):
126 return sys.stdin.readline().strip()
127
128DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler()
129
130
131class Step(object):
132 def __init__(self, text="", requires=None):
133 self._text = text
134 self._number = -1
135 self._requires = requires
136 self._side_effect_handler = DEFAULT_SIDE_EFFECT_HANDLER
137
138 def SetNumber(self, number):
139 self._number = number
140
141 def SetConfig(self, config):
142 self._config = config
143
144 def SetState(self, state):
145 self._state = state
146
147 def SetOptions(self, options):
148 self._options = options
149
150 def SetSideEffectHandler(self, handler):
151 self._side_effect_handler = handler
152
153 def Config(self, key):
154 return self._config[key]
155
156 def Run(self):
157 assert self._number >= 0
158 assert self._config is not None
159 assert self._state is not None
160 assert self._side_effect_handler is not None
161 if self._requires:
162 self.RestoreIfUnset(self._requires)
163 if not self._state[self._requires]:
164 return
165 print ">>> Step %d: %s" % (self._number, self._text)
166 self.RunStep()
167
168 def RunStep(self):
169 raise NotImplementedError
170
171 def ReadLine(self):
172 return self._side_effect_handler.ReadLine()
173
174 def Git(self, args="", prefix="", pipe=True):
175 return self._side_effect_handler.Command("git", args, prefix, pipe)
176
177 def Editor(self, args):
178 return self._side_effect_handler.Command(os.environ["EDITOR"], args,
179 pipe=False)
180
181 def Die(self, msg=""):
182 if msg != "":
183 print "Error: %s" % msg
184 print "Exiting"
185 raise Exception(msg)
186
187 def Confirm(self, msg):
188 print "%s [Y/n] " % msg,
189 answer = self.ReadLine()
190 return answer == "" or answer == "Y" or answer == "y"
191
192 def DeleteBranch(self, name):
193 git_result = self.Git("branch").strip()
194 for line in git_result.splitlines():
195 if re.match(r".*\s+%s$" % name, line):
196 msg = "Branch %s exists, do you want to delete it?" % name
197 if self.Confirm(msg):
198 if self.Git("branch -D %s" % name) is None:
199 self.Die("Deleting branch '%s' failed." % name)
200 print "Branch %s deleted." % name
201 else:
202 msg = "Can't continue. Please delete branch %s and try again." % name
203 self.Die(msg)
204
205 def Persist(self, var, value):
206 value = value or "__EMPTY__"
207 TextToFile(value, "%s-%s" % (self._config[PERSISTFILE_BASENAME], var))
208
209 def Restore(self, var):
210 value = FileToText("%s-%s" % (self._config[PERSISTFILE_BASENAME], var))
211 value = value or self.Die("Variable '%s' could not be restored." % var)
212 return "" if value == "__EMPTY__" else value
213
214 def RestoreIfUnset(self, var_name):
215 if self._state.get(var_name) is None:
216 self._state[var_name] = self.Restore(var_name)
217
218 def InitialEnvironmentChecks(self):
219 # Cancel if this is not a git checkout.
220 if not os.path.exists(self._config[DOT_GIT_LOCATION]):
221 self.Die("This is not a git checkout, this script won't work for you.")
222
223 # Cancel if EDITOR is unset or not executable.
224 if (not os.environ.get("EDITOR") or
225 Command("which", os.environ["EDITOR"]) is None):
226 self.Die("Please set your EDITOR environment variable, you'll need it.")
227
228 def CommonPrepare(self):
229 # Check for a clean workdir.
230 if self.Git("status -s -uno").strip() != "":
231 self.Die("Workspace is not clean. Please commit or undo your changes.")
232
233 # Persist current branch.
234 current_branch = ""
235 git_result = self.Git("status -s -b -uno").strip()
236 for line in git_result.splitlines():
237 match = re.match(r"^## (.+)", line)
238 if match:
239 current_branch = match.group(1)
240 break
241 self.Persist("current_branch", current_branch)
242
243 # Fetch unfetched revisions.
244 if self.Git("svn fetch") is None:
245 self.Die("'git svn fetch' failed.")
246
machenbach@chromium.orgaf9cfcb2013-11-19 11:05:18 +0000247 def PrepareBranch(self):
machenbach@chromium.org935a7792013-11-12 09:05:18 +0000248 # Get ahold of a safe temporary branch and check it out.
machenbach@chromium.orgaf9cfcb2013-11-19 11:05:18 +0000249 self.RestoreIfUnset("current_branch")
250 if self._state["current_branch"] != self._config[TEMP_BRANCH]:
machenbach@chromium.org935a7792013-11-12 09:05:18 +0000251 self.DeleteBranch(self._config[TEMP_BRANCH])
252 self.Git("checkout -b %s" % self._config[TEMP_BRANCH])
253
254 # Delete the branch that will be created later if it exists already.
255 self.DeleteBranch(self._config[BRANCHNAME])
256
257 def CommonCleanup(self):
258 self.RestoreIfUnset("current_branch")
259 self.Git("checkout -f %s" % self._state["current_branch"])
260 if self._config[TEMP_BRANCH] != self._state["current_branch"]:
261 self.Git("branch -D %s" % self._config[TEMP_BRANCH])
262 if self._config[BRANCHNAME] != self._state["current_branch"]:
263 self.Git("branch -D %s" % self._config[BRANCHNAME])
264
265 # Clean up all temporary files.
266 Command("rm", "-f %s*" % self._config[PERSISTFILE_BASENAME])
267
268 def ReadAndPersistVersion(self, prefix=""):
269 def ReadAndPersist(var_name, def_name):
270 match = re.match(r"^#define %s\s+(\d*)" % def_name, line)
271 if match:
272 value = match.group(1)
273 self.Persist("%s%s" % (prefix, var_name), value)
274 self._state["%s%s" % (prefix, var_name)] = value
275 for line in LinesInFile(self._config[VERSION_FILE]):
276 for (var_name, def_name) in [("major", "MAJOR_VERSION"),
277 ("minor", "MINOR_VERSION"),
278 ("build", "BUILD_NUMBER"),
279 ("patch", "PATCH_LEVEL")]:
280 ReadAndPersist(var_name, def_name)
281
282 def RestoreVersionIfUnset(self, prefix=""):
283 for v in ["major", "minor", "build", "patch"]:
284 self.RestoreIfUnset("%s%s" % (prefix, v))
285
286 def WaitForLGTM(self):
287 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit "
288 "your change. (If you need to iterate on the patch or double check "
289 "that it's sane, do so in another shell, but remember to not "
290 "change the headline of the uploaded CL.")
291 answer = ""
292 while answer != "LGTM":
293 print "> ",
294 answer = self.ReadLine()
295 if answer != "LGTM":
296 print "That was not 'LGTM'."
297
298 def WaitForResolvingConflicts(self, patch_file):
299 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", "
300 "or resolve the conflicts, stage *all* touched files with "
301 "'git add', and type \"RESOLVED<Return>\"")
302 answer = ""
303 while answer != "RESOLVED":
304 if answer == "ABORT":
305 self.Die("Applying the patch failed.")
306 if answer != "":
307 print "That was not 'RESOLVED' or 'ABORT'."
308 print "> ",
309 answer = self.ReadLine()
310
311 # Takes a file containing the patch to apply as first argument.
312 def ApplyPatch(self, patch_file, reverse_patch=""):
313 args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file)
314 if self.Git(args) is None:
315 self.WaitForResolvingConflicts(patch_file)
316
317
318class UploadStep(Step):
319 def __init__(self):
320 Step.__init__(self, "Upload for code review.")
321
322 def RunStep(self):
323 print "Please enter the email address of a V8 reviewer for your patch: ",
324 reviewer = self.ReadLine()
325 args = "cl upload -r \"%s\" --send-mail" % reviewer
326 if self.Git(args,pipe=False) is None:
327 self.Die("'git cl upload' failed, please try again.")
machenbach@chromium.orgaf9cfcb2013-11-19 11:05:18 +0000328
329
330def RunScript(step_classes,
331 config,
332 options,
333 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER):
334 state = {}
335 steps = []
336 number = 0
337
338 for step_class in step_classes:
339 # TODO(machenbach): Factory methods.
340 step = step_class()
341 step.SetNumber(number)
342 step.SetConfig(config)
343 step.SetOptions(options)
344 step.SetState(state)
345 step.SetSideEffectHandler(side_effect_handler)
346 steps.append(step)
347 number += 1
348
349 for step in steps[options.s:]:
350 step.Run()