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), | |
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 SideEffectHandler(object): | |
85 def Command(self, cmd, args="", prefix="", pipe=True): | |
86 return Command(cmd, args, prefix, pipe) | |
87 | |
88 def ReadLine(self): | |
89 return sys.stdin.readline().strip() | |
90 | |
91 DEFAULT_SIDE_EFFECT_HANDLER = SideEffectHandler() | |
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_effect_handler = DEFAULT_SIDE_EFFECT_HANDLER | |
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): | |
114 self._side_effect_handler = 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_effect_handler 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): | |
132 raise NotImplementedError | |
133 | |
134 def ReadLine(self): | |
135 return self._side_effect_handler.ReadLine() | |
136 | |
137 def Command(self, cmd, args="", prefix="", pipe=True): | |
138 return self._side_effect_handler.Command(cmd, args, prefix, pipe) | |
139 | |
140 def Git(self, args="", prefix="", pipe=True): | |
141 return self.Command("git", args, prefix, pipe) | |
142 | |
143 def Editor(self, args): | |
144 return self.Command(os.environ["EDITOR"], args, pipe=False) | |
145 | |
146 def Die(self, msg=""): | |
147 if msg != "": | |
148 print "Error: %s" % msg | |
149 print "Exiting" | |
150 raise Exception(msg) | |
151 | |
152 def Confirm(self, msg): | |
153 print "%s [Y/n] " % msg, | |
154 answer = self.ReadLine() | |
155 return answer == "" or answer == "Y" or answer == "y" | |
156 | |
157 def DeleteBranch(self, name): | |
158 git_result = self.Git("branch").strip() | |
159 for line in git_result.splitlines(): | |
160 if re.match(r".*\s+%s$" % name, line): | |
161 msg = "Branch %s exists, do you want to delete it?" % name | |
162 if self.Confirm(msg): | |
163 if self.Git("branch -D %s" % name) is None: | |
164 self.Die("Deleting branch '%s' failed." % name) | |
165 print "Branch %s deleted." % name | |
166 else: | |
167 msg = "Can't continue. Please delete branch %s and try again." % name | |
168 self.Die(msg) | |
169 | |
170 def Persist(self, var, value): | |
171 value = value or "__EMPTY__" | |
172 TextToFile(value, "%s-%s" % (self._config[PERSISTFILE_BASENAME], var)) | |
173 | |
174 def Restore(self, var): | |
175 value = FileToText("%s-%s" % (self._config[PERSISTFILE_BASENAME], var)) | |
176 value = value or self.Die("Variable '%s' could not be restored." % var) | |
177 return "" if value == "__EMPTY__" else value | |
178 | |
179 def RestoreIfUnset(self, var_name): | |
180 if self._state.get(var_name) is None: | |
181 self._state[var_name] = self.Restore(var_name) | |
182 | |
183 def InitialEnvironmentChecks(self): | |
184 # Cancel if this is not a git checkout. | |
185 if not os.path.exists(self._config[DOT_GIT_LOCATION]): | |
186 self.Die("This is not a git checkout, this script won't work for you.") | |
187 | |
188 # Cancel if EDITOR is unset or not executable. | |
189 if (not os.environ.get("EDITOR") or | |
190 Command("which", os.environ["EDITOR"]) is None): | |
191 self.Die("Please set your EDITOR environment variable, you'll need it.") | |
192 | |
193 def CommonPrepare(self): | |
194 # Check for a clean workdir. | |
195 if self.Git("status -s -uno").strip() != "": | |
196 self.Die("Workspace is not clean. Please commit or undo your changes.") | |
197 | |
198 # Persist current branch. | |
199 current_branch = "" | |
200 git_result = self.Git("status -s -b -uno").strip() | |
201 for line in git_result.splitlines(): | |
202 match = re.match(r"^## (.+)", line) | |
203 if match: | |
204 current_branch = match.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 match = re.match(r"^#define %s\s+(\d*)" % def_name, line) | |
234 if match: | |
235 major = match.group(1) | |
Jakob Kummerow
2013/11/08 10:43:19
nit: "major" is a misleading name. "value" would b
Michael Achenbach
2013/11/08 13:08:52
Done.
| |
236 self.Persist("%s%s" % (prefix, var_name), major) | |
237 self._state["%s%s" % (prefix, var_name)] = major | |
238 for line in LinesInFile(self._config[VERSION_FILE]): | |
239 for (var_name, def_name) in [("major", "MAJOR_VERSION"), | |
240 ("minor", "MINOR_VERSION"), | |
241 ("build", "BUILD_NUMBER"), | |
242 ("patch", "PATCH_LEVEL")]: | |
243 ReadAndPersist(var_name, def_name) | |
244 | |
245 def RestoreVersionIfUnset(self, prefix=""): | |
246 for v in ["major", "minor", "build", "patch"]: | |
247 self.RestoreIfUnset("%s%s" % (prefix, v)) | |
248 | |
249 def WaitForLGTM(self): | |
250 print ("Please wait for an LGTM, then type \"LGTM<Return>\" to commit " | |
251 "your change. (If you need to iterate on the patch or double check " | |
252 "that it's sane, do so in another shell, but remember to not " | |
253 "change the headline of the uploaded CL.") | |
254 answer = "" | |
255 while answer != "LGTM": | |
256 print "> ", | |
257 answer = self.ReadLine() | |
258 if answer != "LGTM": | |
259 print "That was not 'LGTM'." | |
260 | |
261 def WaitForResolvingConflicts(self, patch_file): | |
262 print("Applying the patch \"%s\" failed. Either type \"ABORT<Return>\", " | |
263 "or resolve the conflicts, stage *all* touched files with " | |
264 "'git add', and type \"RESOLVED<Return>\"") | |
265 answer = "" | |
266 while answer != "RESOLVED": | |
267 if answer == "ABORT": | |
268 self.Die("Applying the patch failed.") | |
269 if answer != "": | |
270 print "That was not 'RESOLVED' or 'ABORT'." | |
271 print "> ", | |
272 answer = self.ReadLine() | |
273 | |
274 # Takes a file containing the patch to apply as first argument. | |
275 def ApplyPatch(self, patch_file, reverse_patch=""): | |
276 args = "apply --index --reject %s \"%s\"" % (reverse_patch, patch_file) | |
277 if self.Git(args) is None: | |
278 self.WaitForResolvingConflicts(patch_file) | |
279 | |
280 | |
281 class UploadStep(Step): | |
282 def __init__(self): | |
283 Step.__init__(self, "Upload for code review.") | |
284 | |
285 def RunStep(self): | |
286 print "Please enter the email address of a V8 reviewer for your patch: " | |
287 reviewer = self.ReadLine() | |
288 args = "cl upload -r \"%s\" --send-mail" % reviewer | |
289 if self.Git(args,pipe=False) is None: | |
290 self.Die("'git cl upload' failed, please try again.") | |
OLD | NEW |