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

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

Issue 67763005: Add forced mode to push-to-trunk script. (Closed) Base URL: https://v8.googlecode.com/svn/branches/bleeding_edge
Patch Set: Addressed review comments. 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
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2013 the V8 project authors. All rights reserved. 2 # Copyright 2013 the V8 project authors. All rights reserved.
3 # Redistribution and use in source and binary forms, with or without 3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are 4 # modification, are permitted provided that the following conditions are
5 # met: 5 # met:
6 # 6 #
7 # * Redistributions of source code must retain the above copyright 7 # * Redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer. 8 # notice, this list of conditions and the following disclaimer.
9 # * Redistributions in binary form must reproduce the above 9 # * Redistributions in binary form must reproduce the above
10 # copyright notice, this list of conditions and the following 10 # copyright notice, this list of conditions and the following
(...skipping 12 matching lines...) Expand all
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 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. 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 28
29 import os 29 import os
30 import re 30 import re
31 import subprocess 31 import subprocess
32 import sys 32 import sys
33 import textwrap
33 34
34 PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME" 35 PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME"
35 TEMP_BRANCH = "TEMP_BRANCH" 36 TEMP_BRANCH = "TEMP_BRANCH"
36 BRANCHNAME = "BRANCHNAME" 37 BRANCHNAME = "BRANCHNAME"
37 DOT_GIT_LOCATION = "DOT_GIT_LOCATION" 38 DOT_GIT_LOCATION = "DOT_GIT_LOCATION"
38 VERSION_FILE = "VERSION_FILE" 39 VERSION_FILE = "VERSION_FILE"
39 CHANGELOG_FILE = "CHANGELOG_FILE" 40 CHANGELOG_FILE = "CHANGELOG_FILE"
40 CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE" 41 CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE"
41 COMMITMSG_FILE = "COMMITMSG_FILE" 42 COMMITMSG_FILE = "COMMITMSG_FILE"
42 PATCH_FILE = "PATCH_FILE" 43 PATCH_FILE = "PATCH_FILE"
(...skipping 17 matching lines...) Expand all
60 61
61 def FileToText(file_name): 62 def FileToText(file_name):
62 with open(file_name) as f: 63 with open(file_name) as f:
63 return f.read() 64 return f.read()
64 65
65 66
66 def MSub(rexp, replacement, text): 67 def MSub(rexp, replacement, text):
67 return re.sub(rexp, replacement, text, flags=re.MULTILINE) 68 return re.sub(rexp, replacement, text, flags=re.MULTILINE)
68 69
69 70
71 def Fill80(line):
72 return textwrap.fill(line, width=80, initial_indent=" ",
73 subsequent_indent=" ")
74
75
70 def GetLastChangeLogEntries(change_log_file): 76 def GetLastChangeLogEntries(change_log_file):
71 result = [] 77 result = []
72 for line in LinesInFile(change_log_file): 78 for line in LinesInFile(change_log_file):
73 if re.search(r"^\d{4}-\d{2}-\d{2}:", line) and result: break 79 if re.search(r"^\d{4}-\d{2}-\d{2}:", line) and result: break
74 result.append(line) 80 result.append(line)
75 return "".join(result) 81 return "".join(result)
76 82
77 83
78 def MakeChangeLogBody(commit_generator): 84 def MakeChangeLogBody(commit_generator):
79 result = "" 85 result = ""
80 for (title, body, author) in commit_generator(): 86 for (title, body, author) in commit_generator():
81 # Add the commit's title line. 87 # Add the commit's title line.
82 result += "%s\n" % title.rstrip() 88 result += "%s\n" % title.rstrip()
83 89
84 # Grep for "BUG=xxxx" lines in the commit message and convert them to 90 # Add bug references.
85 # "(issue xxxx)". 91 result += MakeChangeLogBugReference(body)
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 92
101 # Append the commit's author for reference. 93 # Append the commit's author for reference.
102 result += "%s\n\n" % author.rstrip() 94 result += "%s\n\n" % author.rstrip()
103 return result 95 return result
104 96
105 97
98 def MakeChangeLogBugReference(body):
99 # Grep for "BUG=xxxx" lines in the commit message and convert them to
100 # "(issue xxxx)".
101 out = body.splitlines()
102 out = filter(lambda x: re.search(r"^[ \t]*BUG[ \t]*=", x), out)
103 out = filter(lambda x: not re.search(r"BUG[ \t]*=[ \t]*$", x), out)
104 out = filter(lambda x: not re.search(r"BUG[ \t]*=[ \t]*none[ \t]*$", x), out)
105
106 crbugs = []
107 v8bugs = []
108
109 def AddSafe(bugs, bug):
110 try:
111 bugs.append(int(bug))
112 except ValueError:
113 pass
114
115 def AddIssues(text):
116 ref = re.match(r"^[ \t]*BUG[ \t]*=[ \t]*(.*?)[ \t]*$", text)
117 if not ref:
118 return
119 for bug in ref.group(1).split(","):
120 bug = bug.strip()
121 match = re.match(r"^v8[ \t]*:[ \t]*(.*)$", bug)
122 if match: AddSafe(v8bugs, match.group(1))
123 else:
124 match = re.match(r"^(?:chromium[ \t]*:)?[ \t]*(.*)$", bug)
125 if match: AddSafe(crbugs, match.group(1))
126
127 # Add issues to crbugs and v8bugs.
128 map(AddIssues, out)
129
130 # Filter duplicates, sort, stringify.
131 crbugs = map(str, sorted(set(crbugs)))
132 v8bugs = map(str, sorted(set(v8bugs)))
133
134 bug_groups = []
135 def FormatIssues(prefix, bugs):
136 if len(bugs) > 0:
137 plural = "s" if len(bugs) > 1 else ""
138 bug_groups.append("%sissue%s %s" % (prefix, plural, ", ".join(bugs)))
139
140 FormatIssues("Chromium ", crbugs)
141 FormatIssues("", v8bugs)
142
143 if len(bug_groups) > 0:
144 # Format with 8 characters indentation and max 80 character lines.
145 return "%s\n" % Fill80("(%s)" % ", ".join(bug_groups))
146 else:
147 return ""
148
149
106 # Some commands don't like the pipe, e.g. calling vi from within the script or 150 # Some commands don't like the pipe, e.g. calling vi from within the script or
107 # from subscripts like git cl upload. 151 # from subscripts like git cl upload.
108 def Command(cmd, args="", prefix="", pipe=True): 152 def Command(cmd, args="", prefix="", pipe=True):
109 cmd_line = "%s %s %s" % (prefix, cmd, args) 153 cmd_line = "%s %s %s" % (prefix, cmd, args)
110 print "Command: %s" % cmd_line 154 print "Command: %s" % cmd_line
111 try: 155 try:
112 if pipe: 156 if pipe:
113 return subprocess.check_output(cmd_line, shell=True) 157 return subprocess.check_output(cmd_line, shell=True)
114 else: 158 else:
115 return subprocess.check_call(cmd_line, shell=True) 159 return subprocess.check_call(cmd_line, shell=True)
116 except subprocess.CalledProcessError: 160 except subprocess.CalledProcessError:
117 return None 161 return None
118 162
119 163
120 # Wrapper for side effects. 164 # Wrapper for side effects.
121 class SideEffectHandler(object): 165 class SideEffectHandler(object):
122 def Command(self, cmd, args="", prefix="", pipe=True): 166 def Command(self, cmd, args="", prefix="", pipe=True):
123 return Command(cmd, args, prefix, pipe) 167 return Command(cmd, args, prefix, pipe)
124 168
125 def ReadLine(self): 169 def ReadLine(self):
126 return sys.stdin.readline().strip() 170 return sys.stdin.readline().strip()
127 171
128 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() 172 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler()
129 173
130 174
131 class Step(object): 175 class Step(object):
132 def __init__(self, text="", requires=None): 176 def __init__(self, text="", requires=None):
133 self._text = text 177 self._text = text
134 self._number = -1 178 self._number = -1
179 self._options = None
135 self._requires = requires 180 self._requires = requires
136 self._side_effect_handler = DEFAULT_SIDE_EFFECT_HANDLER 181 self._side_effect_handler = DEFAULT_SIDE_EFFECT_HANDLER
137 182
138 def SetNumber(self, number): 183 def SetNumber(self, number):
139 self._number = number 184 self._number = number
140 185
141 def SetConfig(self, config): 186 def SetConfig(self, config):
142 self._config = config 187 self._config = config
143 188
144 def SetState(self, state): 189 def SetState(self, state):
(...skipping 16 matching lines...) Expand all
161 if self._requires: 206 if self._requires:
162 self.RestoreIfUnset(self._requires) 207 self.RestoreIfUnset(self._requires)
163 if not self._state[self._requires]: 208 if not self._state[self._requires]:
164 return 209 return
165 print ">>> Step %d: %s" % (self._number, self._text) 210 print ">>> Step %d: %s" % (self._number, self._text)
166 self.RunStep() 211 self.RunStep()
167 212
168 def RunStep(self): 213 def RunStep(self):
169 raise NotImplementedError 214 raise NotImplementedError
170 215
171 def ReadLine(self): 216 def ReadLine(self, default=None):
172 return self._side_effect_handler.ReadLine() 217 # Don't prompt in forced mode.
218 if self._options and self._options.f and default is not None:
219 print "%s (forced)" % default
220 return default
221 else:
222 return self._side_effect_handler.ReadLine()
173 223
174 def Git(self, args="", prefix="", pipe=True): 224 def Git(self, args="", prefix="", pipe=True):
175 return self._side_effect_handler.Command("git", args, prefix, pipe) 225 return self._side_effect_handler.Command("git", args, prefix, pipe)
176 226
177 def Editor(self, args): 227 def Editor(self, args):
178 return self._side_effect_handler.Command(os.environ["EDITOR"], args, 228 return self._side_effect_handler.Command(os.environ["EDITOR"], args,
179 pipe=False) 229 pipe=False)
180 230
181 def Die(self, msg=""): 231 def Die(self, msg=""):
182 if msg != "": 232 if msg != "":
183 print "Error: %s" % msg 233 print "Error: %s" % msg
184 print "Exiting" 234 print "Exiting"
185 raise Exception(msg) 235 raise Exception(msg)
186 236
237 def DieInForcedMode(self, msg=""):
238 if self._options and self._options.f:
239 msg = msg or "Not implemented in forced mode."
240 self.Die(msg)
241
187 def Confirm(self, msg): 242 def Confirm(self, msg):
188 print "%s [Y/n] " % msg, 243 print "%s [Y/n] " % msg,
189 answer = self.ReadLine() 244 answer = self.ReadLine(default="Y")
190 return answer == "" or answer == "Y" or answer == "y" 245 return answer == "" or answer == "Y" or answer == "y"
191 246
192 def DeleteBranch(self, name): 247 def DeleteBranch(self, name):
193 git_result = self.Git("branch").strip() 248 git_result = self.Git("branch").strip()
194 for line in git_result.splitlines(): 249 for line in git_result.splitlines():
195 if re.match(r".*\s+%s$" % name, line): 250 if re.match(r".*\s+%s$" % name, line):
196 msg = "Branch %s exists, do you want to delete it?" % name 251 msg = "Branch %s exists, do you want to delete it?" % name
197 if self.Confirm(msg): 252 if self.Confirm(msg):
198 if self.Git("branch -D %s" % name) is None: 253 if self.Git("branch -D %s" % name) is None:
199 self.Die("Deleting branch '%s' failed." % name) 254 self.Die("Deleting branch '%s' failed." % name)
(...skipping 13 matching lines...) Expand all
213 268
214 def RestoreIfUnset(self, var_name): 269 def RestoreIfUnset(self, var_name):
215 if self._state.get(var_name) is None: 270 if self._state.get(var_name) is None:
216 self._state[var_name] = self.Restore(var_name) 271 self._state[var_name] = self.Restore(var_name)
217 272
218 def InitialEnvironmentChecks(self): 273 def InitialEnvironmentChecks(self):
219 # Cancel if this is not a git checkout. 274 # Cancel if this is not a git checkout.
220 if not os.path.exists(self._config[DOT_GIT_LOCATION]): 275 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.") 276 self.Die("This is not a git checkout, this script won't work for you.")
222 277
278 # TODO(machenbach): Don't use EDITOR in forced mode as soon as script is
279 # well tested.
223 # Cancel if EDITOR is unset or not executable. 280 # Cancel if EDITOR is unset or not executable.
224 if (not os.environ.get("EDITOR") or 281 if (not os.environ.get("EDITOR") or
225 Command("which", os.environ["EDITOR"]) is None): 282 Command("which", os.environ["EDITOR"]) is None):
226 self.Die("Please set your EDITOR environment variable, you'll need it.") 283 self.Die("Please set your EDITOR environment variable, you'll need it.")
227 284
228 def CommonPrepare(self): 285 def CommonPrepare(self):
229 # Check for a clean workdir. 286 # Check for a clean workdir.
230 if self.Git("status -s -uno").strip() != "": 287 if self.Git("status -s -uno").strip() != "":
231 self.Die("Workspace is not clean. Please commit or undo your changes.") 288 self.Die("Workspace is not clean. Please commit or undo your changes.")
232 289
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after
284 self.RestoreIfUnset("%s%s" % (prefix, v)) 341 self.RestoreIfUnset("%s%s" % (prefix, v))
285 342
286 def WaitForLGTM(self): 343 def WaitForLGTM(self):
287 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit " 344 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 " 345 "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 " 346 "that it's sane, do so in another shell, but remember to not "
290 "change the headline of the uploaded CL.") 347 "change the headline of the uploaded CL.")
291 answer = "" 348 answer = ""
292 while answer != "LGTM": 349 while answer != "LGTM":
293 print "> ", 350 print "> ",
351 # TODO(machenbach): Add default="LGTM" to avoid prompt when script is
352 # well tested and when prepare push cl has TBR flag.
294 answer = self.ReadLine() 353 answer = self.ReadLine()
295 if answer != "LGTM": 354 if answer != "LGTM":
296 print "That was not 'LGTM'." 355 print "That was not 'LGTM'."
297 356
298 def WaitForResolvingConflicts(self, patch_file): 357 def WaitForResolvingConflicts(self, patch_file):
299 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", " 358 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", "
300 "or resolve the conflicts, stage *all* touched files with " 359 "or resolve the conflicts, stage *all* touched files with "
301 "'git add', and type \"RESOLVED<Return>\"") 360 "'git add', and type \"RESOLVED<Return>\"")
361 self.DieInForcedMode()
302 answer = "" 362 answer = ""
303 while answer != "RESOLVED": 363 while answer != "RESOLVED":
304 if answer == "ABORT": 364 if answer == "ABORT":
305 self.Die("Applying the patch failed.") 365 self.Die("Applying the patch failed.")
306 if answer != "": 366 if answer != "":
307 print "That was not 'RESOLVED' or 'ABORT'." 367 print "That was not 'RESOLVED' or 'ABORT'."
308 print "> ", 368 print "> ",
309 answer = self.ReadLine() 369 answer = self.ReadLine()
310 370
311 # Takes a file containing the patch to apply as first argument. 371 # Takes a file containing the patch to apply as first argument.
312 def ApplyPatch(self, patch_file, reverse_patch=""): 372 def ApplyPatch(self, patch_file, reverse_patch=""):
313 args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file) 373 args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file)
314 if self.Git(args) is None: 374 if self.Git(args) is None:
315 self.WaitForResolvingConflicts(patch_file) 375 self.WaitForResolvingConflicts(patch_file)
316 376
317 377
318 class UploadStep(Step): 378 class UploadStep(Step):
319 def __init__(self): 379 def __init__(self):
320 Step.__init__(self, "Upload for code review.") 380 Step.__init__(self, "Upload for code review.")
321 381
322 def RunStep(self): 382 def RunStep(self):
323 print "Please enter the email address of a V8 reviewer for your patch: ", 383 if self._options and self._options.r:
324 reviewer = self.ReadLine() 384 print "Using account %s for review." % self._options.r
385 reviewer = self._options.r
386 else:
387 print "Please enter the email address of a V8 reviewer for your patch: ",
388 self.DieInForcedMode("A reviewer must be specified in forced mode.")
389 reviewer = self.ReadLine()
325 args = "cl upload -r \"%s\" --send-mail" % reviewer 390 args = "cl upload -r \"%s\" --send-mail" % reviewer
326 if self.Git(args,pipe=False) is None: 391 if self.Git(args,pipe=False) is None:
327 self.Die("'git cl upload' failed, please try again.") 392 self.Die("'git cl upload' failed, please try again.")
328 393
329 394
330 def RunScript(step_classes, 395 def RunScript(step_classes,
331 config, 396 config,
332 options, 397 options,
333 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER): 398 side_effect_handler=DEFAULT_SIDE_EFFECT_HANDLER):
334 state = {} 399 state = {}
335 steps = [] 400 steps = []
336 number = 0 401 number = 0
337 402
338 for step_class in step_classes: 403 for step_class in step_classes:
339 # TODO(machenbach): Factory methods. 404 # TODO(machenbach): Factory methods.
340 step = step_class() 405 step = step_class()
341 step.SetNumber(number) 406 step.SetNumber(number)
342 step.SetConfig(config) 407 step.SetConfig(config)
343 step.SetOptions(options) 408 step.SetOptions(options)
344 step.SetState(state) 409 step.SetState(state)
345 step.SetSideEffectHandler(side_effect_handler) 410 step.SetSideEffectHandler(side_effect_handler)
346 steps.append(step) 411 steps.append(step)
347 number += 1 412 number += 1
348 413
349 for step in steps[options.s:]: 414 for step in steps[options.s:]:
350 step.Run() 415 step.Run()
OLDNEW
« no previous file with comments | « no previous file | tools/push-to-trunk/push_to_trunk.py » ('j') | tools/push-to-trunk/push_to_trunk.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698