Chromium Code Reviews| OLD | NEW |
|---|---|
| (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 os | |
| 30 import re | |
| 31 import subprocess | |
| 32 import sys | |
| 33 | |
| 34 PERSISTFILE_BASENAME = "PERSISTFILE_BASENAME" | |
| 35 TEMP_BRANCH = "TEMP_BRANCH" | |
| 36 BRANCHNAME = "BRANCHNAME" | |
| 37 DOT_GIT_LOCATION = "DOT_GIT_LOCATION" | |
| 38 VERSION_FILE = "VERSION_FILE" | |
| 39 CHANGELOG_FILE = "CHANGELOG_FILE" | |
| 40 CHANGELOG_ENTRY_FILE = "CHANGELOG_ENTRY_FILE" | |
| 41 COMMITMSG_FILE = "COMMITMSG_FILE" | |
| 42 PATCH_FILE = "PATCH_FILE" | |
| 43 | |
| 44 | |
| 45 def TextToFile(text, file_name): | |
| 46 with open(file_name, "w") as f: | |
| 47 f.write(text) | |
| 48 | |
| 49 | |
| 50 def AppendToFile(text, file_name): | |
| 51 with open(file_name, "a") as f: | |
| 52 f.write(text) | |
| 53 | |
| 54 | |
| 55 def LinesInFile(file_name): | |
| 56 with open(file_name) as f: | |
| 57 for line in f: | |
| 58 yield line | |
| 59 | |
| 60 | |
| 61 def FileToText(file_name): | |
| 62 with open(file_name) as f: | |
| 63 return f.read() | |
| 64 | |
| 65 | |
| 66 def MSub(rexp, replacement, text): | |
| 67 return re.sub(rexp, replacement, text, flags=re.MULTILINE) | |
| 68 | |
| 69 | |
| 70 # Some commands don't like the pipe, e.g. calling vi from within the script or | |
| 71 # from subscripts like git cl upload. | |
| 72 def Command(cmd, args="", prefix="", pipe=True): | |
| 73 stdout = subprocess.PIPE if pipe else None | |
| 74 p = subprocess.Popen("%s %s %s" % (prefix, cmd, args), | |
|
Jakob Kummerow
2013/11/06 16:37:23
Have you tried using subprocess.check_output()? Th
Michael Achenbach
2013/11/07 15:56:46
Same result. Try:
python -c "import subprocess; su
| |
| 75 shell=True, | |
| 76 stdout=stdout) | |
| 77 p.wait() | |
| 78 if p.returncode != 0: | |
| 79 return None | |
| 80 return p.stdout.read() if pipe else 0 | |
| 81 | |
| 82 | |
| 83 # Wrapper for side effects. | |
| 84 class SideEffectsHandler(object): | |
| 85 def Command(self, cmd, args="", prefix="", pipe=True): | |
| 86 return Command(cmd, args, prefix, pipe) | |
| 87 | |
| 88 def ReadLine(): | |
| 89 return sys.stdin.readline().strip() | |
| 90 | |
| 91 DEFAULT_SIDE_EFFECTS_HANDLER = SideEffectsHandler() | |
| 92 | |
| 93 | |
| 94 class Step(object): | |
| 95 def __init__(self, text="", requires=None): | |
| 96 self._text = text | |
| 97 self._number = -1 | |
| 98 self._requires = requires | |
| 99 self._side_effects_hanlder = DEFAULT_SIDE_EFFECTS_HANDLER | |
|
Jakob Kummerow
2013/11/06 16:37:23
nit: typo
Michael Achenbach
2013/11/07 15:56:46
Done.
| |
| 100 | |
| 101 def SetNumber(self, number): | |
| 102 self._number = number | |
| 103 | |
| 104 def SetConfig(self, config): | |
| 105 self._config = config | |
| 106 | |
| 107 def SetState(self, state): | |
| 108 self._state = state | |
| 109 | |
| 110 def SetOptions(self, options): | |
| 111 self._options = options | |
| 112 | |
| 113 def SetSideEffectHandler(self, handler): | |
|
Jakob Kummerow
2013/11/06 16:37:23
nit: please use the same spelling: if the field is
Michael Achenbach
2013/11/07 15:56:46
Done.
| |
| 114 self._side_effects_hanlder = handler | |
| 115 | |
| 116 def Config(self, key): | |
| 117 return self._config[key] | |
| 118 | |
| 119 def Run(self): | |
| 120 assert self._number >= 0 | |
| 121 assert self._config is not None | |
| 122 assert self._state is not None | |
| 123 assert self._side_effects_hanlder is not None | |
| 124 if self._requires: | |
| 125 self.RestoreIfUnset(self._requires) | |
| 126 if not self._state[self._requires]: | |
| 127 return | |
| 128 print ">>> Step %d: %s" % (self._number, self._text) | |
| 129 self.RunStep() | |
| 130 | |
| 131 def RunStep(self): | |
|
Jakob Kummerow
2013/11/06 16:37:23
maybe "raise NotImplementedError" here to clarify
Michael Achenbach
2013/11/07 15:56:46
Done.
| |
| 132 pass | |
| 133 | |
| 134 def ReadLine(self): | |
| 135 return self._side_effects_hanlder.ReadLine() | |
| 136 | |
| 137 def CommandSE(self, cmd, args="", prefix="", pipe=True): | |
|
Jakob Kummerow
2013/11/06 16:37:23
Hm... I'm not too happy with the -SE postfix. How
Michael Achenbach
2013/11/07 15:56:46
Done.
Jakob Kummerow
2013/11/08 10:43:19
Sorry if I didn't express that clearly enough; my
Michael Achenbach
2013/11/08 13:08:51
Done.
| |
| 138 return self._side_effects_hanlder.Command(cmd, args, prefix, pipe) | |
| 139 | |
| 140 def Git(self, args="", prefix="", pipe=True): | |
| 141 return self.CommandSE("git", args, prefix, pipe) | |
| 142 | |
| 143 def Editor(self, args): | |
| 144 return self.CommandSE(os.environ["EDITOR"], | |
| 145 args, pipe=False) | |
|
Jakob Kummerow
2013/11/06 16:37:23
nit: fits on last line
Michael Achenbach
2013/11/07 15:56:46
Done.
| |
| 146 | |
| 147 def Die(self, msg=""): | |
| 148 if msg != "": | |
| 149 print "Error: %s" % msg | |
| 150 print "Exiting" | |
| 151 raise Exception(msg) | |
| 152 | |
| 153 def Confirm(self, msg): | |
| 154 print "%s [Y/n] " % msg | |
|
Jakob Kummerow
2013/11/06 16:37:23
pro tip: if you add a comma:
print "%s [Y/n] " %
Michael Achenbach
2013/11/07 15:56:46
Good to know. I'll also replace all the sys.stdout
| |
| 155 answer = self.ReadLine() | |
| 156 return answer == "" or answer == "Y" or answer == "y" | |
| 157 | |
| 158 def DeleteBranch(self, name): | |
| 159 git_result = self.Git("branch").strip() | |
| 160 for line in git_result.splitlines(): | |
| 161 if re.match(r".*\s+%s$" % name, line): | |
| 162 msg = "Branch %s exists, do you want to delete it?" % name | |
| 163 if self.Confirm(msg): | |
| 164 if self.Git("branch -D %s" % name) is None: | |
| 165 self.Die("Deleting branch '%s' failed." % name) | |
| 166 print "Branch %s deleted." % name | |
| 167 else: | |
| 168 msg = "Can't continue. Please delete branch %s and try again." % name | |
| 169 self.Die(msg) | |
| 170 | |
| 171 def Persist(self, var_name, value): | |
|
Jakob Kummerow
2013/11/06 16:37:23
Per-variable persist/restore is fine for now if yo
Michael Achenbach
2013/11/07 15:56:46
Separate CL.
| |
| 172 value = value or "__EMPTY__" | |
|
Jakob Kummerow
2013/11/06 16:37:23
IIRC __EMPTY__ was a hack around the fact that Bas
Michael Achenbach
2013/11/07 15:56:46
Separate CL.
| |
| 173 TextToFile(value, "%s-%s" % (self._config[PERSISTFILE_BASENAME], var_name)) | |
| 174 | |
| 175 def Restore(self, var_name): | |
| 176 value = FileToText("%s-%s" % (self._config[PERSISTFILE_BASENAME], var_name)) | |
| 177 value = value or self.Die( "Variable '%s' could not be restored." % var_name ) | |
|
Jakob Kummerow
2013/11/06 16:37:23
nit: 80col. It's probably enough to delete the sup
Michael Achenbach
2013/11/07 15:56:46
Done.
| |
| 178 return "" if value == "__EMPTY__" else value | |
| 179 | |
| 180 def RestoreIfUnset(self, var_name): | |
| 181 if self._state.get(var_name) is None: | |
| 182 self._state[var_name] = self.Restore(var_name) | |
| 183 | |
| 184 def InitialEnvironmentChecks(self): | |
| 185 # Cancel if this is not a git checkout. | |
| 186 if not os.path.exists(self._config[DOT_GIT_LOCATION]): | |
| 187 self.Die("This is not a git checkout, this script won't work for you.") | |
| 188 | |
| 189 # Cancel if EDITOR is unset or not executable. | |
| 190 if (not os.environ.get("EDITOR") or | |
| 191 Command("which", os.environ["EDITOR"]) is None): | |
| 192 self.Die("Please set your EDITOR environment variable, you'll need it.") | |
| 193 | |
| 194 def CommonPrepare(self): | |
| 195 # Check for a clean workdir. | |
| 196 if self.Git("status -s -uno").strip() != "": | |
| 197 self.Die("Workspace is not clean. Please commit or undo your changes.") | |
| 198 | |
| 199 # Persist current branch. | |
| 200 current_branch = "" | |
| 201 git_result = self.Git("status -s -b -uno").strip() | |
| 202 for line in git_result.splitlines(): | |
| 203 if re.match(r"^##.*", line): | |
|
Jakob Kummerow
2013/11/06 16:37:23
Let's match only once here.
match = re.match(r"^#
Michael Achenbach
2013/11/07 15:56:46
Done.
| |
| 204 current_branch = re.match(r"^## (.+)", line).group(1) | |
| 205 break | |
| 206 self.Persist("current_branch", current_branch) | |
| 207 | |
| 208 # Fetch unfetched revisions. | |
| 209 if self.Git("svn fetch") is None: | |
| 210 self.Die("'git svn fetch' failed.") | |
| 211 | |
| 212 # Get ahold of a safe temporary branch and check it out. | |
| 213 if current_branch != self._config[TEMP_BRANCH]: | |
| 214 self.DeleteBranch(self._config[TEMP_BRANCH]) | |
| 215 self.Git("checkout -b %s" % self._config[TEMP_BRANCH]) | |
| 216 | |
| 217 # Delete the branch that will be created later if it exists already. | |
| 218 self.DeleteBranch(self._config[BRANCHNAME]) | |
| 219 | |
| 220 def CommonCleanup(self): | |
| 221 self.RestoreIfUnset("current_branch") | |
| 222 self.Git("checkout -f %s" % self._state["current_branch"]) | |
| 223 if self._config[TEMP_BRANCH] != self._state["current_branch"]: | |
| 224 self.Git("branch -D %s" % self._config[TEMP_BRANCH]) | |
| 225 if self._config[BRANCHNAME] != self._state["current_branch"]: | |
| 226 self.Git("branch -D %s" % self._config[BRANCHNAME]) | |
| 227 | |
| 228 # Clean up all temporary files. | |
| 229 Command("rm", "-f %s*" % self._config[PERSISTFILE_BASENAME]) | |
| 230 | |
| 231 def ReadAndPersistVersion(self, prefix=""): | |
| 232 def ReadAndPersist(var_name, def_name): | |
| 233 if re.match(r"^#define %s\s+\d*" % def_name, line): | |
|
Jakob Kummerow
2013/11/06 16:37:23
same "match only once" comment applies here.
Michael Achenbach
2013/11/07 15:56:46
Done.
| |
| 234 major = re.match(r"^#define %s\s+(\d*)" % def_name, line).group(1) | |
| 235 self.Persist("%s%s" % (prefix, var_name), major) | |
| 236 self._state["%s%s" % (prefix, var_name)] = major | |
| 237 for line in LinesInFile(self._config[VERSION_FILE]): | |
| 238 for (var_name, def_name) in [("major", "MAJOR_VERSION"), | |
| 239 ("minor", "MINOR_VERSION"), | |
| 240 ("build", "BUILD_NUMBER"), | |
| 241 ("patch", "PATCH_LEVEL")]: | |
| 242 ReadAndPersist(var_name, def_name) | |
| 243 | |
| 244 def RestoreVersionIfUnset(self, prefix=""): | |
| 245 for v in ["major", "minor", "build", "patch"]: | |
| 246 self.RestoreIfUnset("%s%s" % (prefix, v)) | |
| 247 | |
| 248 def WaitForLGTM(self): | |
| 249 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit " | |
| 250 "your change. (If you need to iterate on the patch or double check " | |
| 251 "that it's sane, do so in another shell, but remember to not " | |
| 252 "change the headline of the uploaded CL.") | |
| 253 answer = "" | |
| 254 while answer != "LGTM": | |
| 255 sys.stdout.write("> ") | |
| 256 answer = self.ReadLine() | |
| 257 if answer != "LGTM": | |
| 258 print "That was not 'LGTM'." | |
| 259 | |
| 260 def WaitForResolvingConflicts(self, patch_file): | |
| 261 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", " | |
| 262 "or resolve the conflicts, stage *all* touched files with " | |
| 263 "'git add', and type \"RESOLVED<Return>\"") | |
| 264 answer = "" | |
| 265 while answer != "RESOLVED": | |
| 266 if answer == "ABORT": | |
| 267 self.Die("Applying the patch failed.") | |
| 268 if answer != "": | |
| 269 print "That was not 'RESOLVED' or 'ABORT'." | |
| 270 sys.stdout.write("> ") | |
| 271 answer = self.ReadLine() | |
| 272 | |
| 273 # Takes a file containing the patch to apply as first argument. | |
| 274 def ApplyPatch(self, patch_file, reverse_patch=""): | |
| 275 args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file) | |
| 276 if self.Git(args) is None: | |
| 277 self.WaitForResolvingConflicts(patch_file) | |
| 278 | |
| 279 | |
| 280 class UploadStep(Step): | |
| 281 def __init__(self): | |
| 282 Step.__init__(self, "Upload for code review.") | |
| 283 | |
| 284 def RunStep(self): | |
| 285 print "Please enter the email address of a V8 reviewer for your patch: " | |
| 286 reviewer = self.ReadLine() | |
| 287 args = "cl upload -r \"%s\" --send-mail" % reviewer | |
| 288 if self.Git(args,pipe=False) is None: | |
| 289 self.Die("'git cl upload' failed, please try again.") | |
| OLD | NEW |