OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/python2 | |
2 | |
3 # Copyright 2014 Google Inc. | |
4 # | |
5 # Use of this source code is governed by a BSD-style license that can be | |
6 # found in the LICENSE file. | |
7 | |
8 | |
9 """ | |
10 Skia's Chromium DEPS roll script | |
11 | |
12 This script: | |
13 - searches through the last N Skia git commits to find out the hash that is | |
14 associated with the SVN revision number. | |
15 - creates a new branch in the Chromium tree, modifies the DEPS file to | |
16 point at the given Skia commit, commits, uploads to Rietveld, and | |
17 deletes the local copy of the branch. | |
18 - creates a whitespace-only commit and uploads that to to Rietveld. | |
19 - returns the Chromium tree to its previous state. | |
20 | |
21 Usage: | |
22 %prog -c CHROMIUM_PATH -r REVISION [OPTIONAL_OPTIONS]""" | |
23 | |
24 | |
25 import optparse | |
26 import os | |
27 import re | |
28 import subprocess | |
29 import shutil | |
30 import sys | |
31 import tempfile | |
32 | |
33 | |
34 def test_git(git): | |
35 """Test to see if the git executable can be run. | |
36 | |
37 Args: | |
38 git: git executable. | |
39 Raises: | |
40 OSError on failure | |
41 | |
42 """ | |
43 with open(os.devnull, "w") as devnull: | |
44 subprocess.call([git, '--version'], stdout=devnull) | |
45 | |
46 | |
47 def strip_output(*args, **kwargs): | |
48 """Wrap subprocess.check_output and str.strip() | |
49 | |
50 Pass the given arguments into subprocess.check_output() and return | |
51 the results, after stripping any excess whitespace. | |
52 | |
53 Returns: | |
54 a string without leading or trailing whitespace. | |
borenet
2014/01/06 14:06:46
This may be obvious, but I might include "output o
hal.canary
2014/01/06 18:27:57
Done.
| |
55 """ | |
56 return str(subprocess.check_output(*args, **kwargs)).strip() | |
57 | |
58 | |
59 def find_hash_from_revision(revision, search_depth, git): | |
60 """Finds the hash associated with a revision. | |
61 | |
62 Searches through the last search_depth commits to find out the hash | |
63 that is associated with the SVN revision number. | |
64 | |
65 Args: | |
66 revision: (int) SVN revision number. | |
67 search_depth: (int) Number of revisions to limit the search to. | |
68 git: git executable. | |
69 | |
70 Returns: | |
71 Hash as a string. | |
72 | |
73 Raises an exception on failure | |
74 """ | |
75 skia_url = 'https://skia.googlesource.com/skia.git' | |
borenet
2014/01/06 14:06:46
Would prefer that this be in a global variable.
hal.canary
2014/01/06 18:27:57
Done.
| |
76 temp_dir = tempfile.mkdtemp(prefix='git_skia_tmp_') | |
77 devnull = open(os.devnull, "w") | |
78 revision_format = 'http://skia.googlecode.com/svn/trunk@%d' | |
borenet
2014/01/06 14:06:46
Ditto here.
hal.canary
2014/01/06 18:27:57
Done.
| |
79 revision_regex = re.compile(revision_format % revision) | |
80 try: | |
81 subprocess.check_call( | |
82 [git, 'clone', '--depth=%d' % search_depth, '--single-branch', | |
83 skia_url, temp_dir], stdout=devnull, stderr=devnull) | |
borenet
2014/01/06 14:06:46
This isn't as expensive as it used to be, but mayb
hal.canary
2014/01/06 18:27:57
Done.
| |
84 for i in xrange(search_depth): | |
85 commit = 'origin/master~%d' % i | |
86 output = subprocess.check_output( | |
87 [git, 'log', '-n', '1', '--format=format:%B', commit], | |
88 cwd=temp_dir, stderr=devnull) | |
89 if revision_regex.search(output): | |
90 return strip_output( | |
91 [git, 'log', '-n', '1', '--format=format:%H', commit], | |
92 cwd=temp_dir) | |
93 finally: | |
94 shutil.rmtree(temp_dir) | |
95 devnull.close() | |
96 raise Exception('Failed to find revision.') | |
97 | |
98 | |
99 def fetch_origin(git): | |
100 """Call git fetch | |
101 | |
102 Updates origin/master (via git fetch). Leaves local tree alone. | |
103 Assumes current directory is a git repository. | |
104 | |
105 Args: | |
106 git: git executable. | |
107 Returns: | |
108 the commit hash of origin/master | |
109 """ | |
110 with open(os.devnull, "w") as devnull: | |
111 subprocess.check_call( | |
112 [git, 'fetch', 'origin'], stdout=devnull, stderr=devnull) | |
113 return strip_output([git, 'show-ref', 'origin/master', '--hash']) | |
114 | |
115 | |
116 class GitBranchCLUpload(object): | |
borenet
2014/01/06 14:06:46
This is a really elegant way of handling this. +1
hal.canary
2014/01/06 18:27:57
Thanks!
| |
117 """ | |
118 This class allows one to create a new branch in a repository based | |
119 off of origin/master, make changes to the tree inside the | |
120 with-block, upload that new branch to Rietveld, restore the original | |
121 tree state, and delete the local copy of the new branch. | |
122 | |
123 See roll_deps() for an example of use. | |
124 | |
125 Constructor Args: | |
126 message: the commit message. | |
127 file_list: list of files to pass to `git add`. | |
128 git: git executable. | |
129 set_brach_name: if not None, the name of the branch to use. | |
130 If None, then use a temporary branch that will be deleted. | |
131 """ | |
132 # (Too few public methods) pylint: disable=I0011,R0903 | |
133 def __init__(self, message, file_list, git, set_branch_name): | |
borenet
2014/01/06 14:06:46
I'm not a big fan of having to pass the file_list
| |
134 self._message = message | |
135 self._file_list = file_list | |
136 self._git = git | |
137 self._issue = None | |
138 self._branch_name = set_branch_name | |
139 self._stash = None | |
140 self._original_branch = None | |
141 | |
142 def __enter__(self): | |
143 diff = subprocess.check_output([self._git, 'diff', '--shortstat']) | |
144 self._stash = (0 != len(diff)) | |
145 if self._stash: | |
146 subprocess.check_call([self._git, 'stash', 'save']) | |
147 try: | |
148 self._original_branch = strip_output( | |
149 [self._git, 'symbolic-ref', '--short', 'HEAD']) | |
150 except (subprocess.CalledProcessError,): | |
151 self._original_branch = strip_output( | |
152 [self._git, 'rev-parse', 'HEAD']) | |
153 | |
154 if not self._branch_name: | |
155 self._branch_name = 'autogenerated_deps_roll_branch' | |
borenet
2014/01/06 14:06:46
Please put this in a default_branch_name variable
hal.canary
2014/01/06 18:27:57
Done.
| |
156 | |
157 try: | |
158 subprocess.check_call( | |
159 [self._git, 'checkout', '-b', | |
160 self._branch_name, 'origin/master']) | |
161 except (subprocess.CalledProcessError,): | |
162 # Branch already exists. | |
163 subprocess.check_call([self._git, 'checkout', 'master']) | |
164 subprocess.check_call( | |
165 [self._git, 'branch', '-D', self._branch_name]) | |
166 subprocess.check_call( | |
167 [self._git, 'checkout', '-b', | |
168 self._branch_name, 'origin/master']) | |
169 | |
170 | |
171 def __exit__(self, etype, value, traceback): | |
172 for filename in self._file_list: | |
173 subprocess.check_call([self._git, 'add', filename]) | |
174 subprocess.check_call([self._git, 'commit', '-m', self._message]) | |
borenet
2014/01/06 14:06:46
self._message probably needs to be double quoted a
hal.canary
2014/01/06 18:27:57
Nope. execvp doesn't require that.
| |
175 | |
176 environ = os.environ.copy() | |
177 if sys.platform != 'win32': | |
178 environ['GIT_EDITOR'] = ':' # Bypass the editor | |
179 subprocess.check_call([self._git, 'cl', 'upload'], env=environ) | |
180 | |
181 self._issue = strip_output([self._git, 'cl', 'issue']) | |
182 | |
183 # deal with the aftermath of failed executions of this script. | |
184 if 'autogenerated_deps_roll_branch' == self._original_branch: | |
185 subprocess.check_call([self._git, 'checkout', 'master']) | |
186 else: | |
187 subprocess.check_call( | |
188 [self._git, 'checkout', self._original_branch]) | |
189 | |
190 if 'autogenerated_deps_roll_branch' == self._branch_name: | |
191 subprocess.check_call( | |
192 [self._git, 'branch', '-D', self._branch_name]) | |
193 if self._stash: | |
194 subprocess.check_call([self._git, 'stash', 'pop']) | |
195 | |
196 @property | |
197 def issue(self): | |
198 """ | |
199 Returns: | |
200 a string describing the codereview issue, after __exit__ | |
201 has been called. | |
borenet
2014/01/06 14:06:46
Should this raise an exception if called before __
hal.canary
2014/01/06 18:27:57
I thought about that (which was why I had it in a
| |
202 """ | |
203 return self._issue | |
204 | |
205 | |
206 def change_skia_deps(revision, hashval, depspath): | |
207 """Update the DEPS file. | |
208 | |
209 Modify the skia_revision and skia_hash entries in the given DEPS file. | |
210 | |
211 Args: | |
212 revision: (int) Skia SVN revision. | |
213 hashval: (string) Skia Git hash. | |
214 depspath: (string) path to DEPS file. | |
215 """ | |
216 temp_file = tempfile.NamedTemporaryFile(delete=False, | |
217 prefix='skia_DEPS_ROLL_tmp_') | |
218 try: | |
219 deps_regex_rev = re.compile('"skia_revision": "[0-9]*",') | |
220 deps_regex_hash = re.compile('"skia_hash": "[0-9a-f]*",') | |
221 | |
222 deps_regex_rev_repl = '"skia_revision": "%d",' % revision | |
223 deps_regex_hash_repl = '"skia_hash": "%s",' % hashval | |
224 | |
225 with open(depspath, 'r') as input_stream: | |
226 for line in input_stream: | |
227 line = deps_regex_rev.sub(deps_regex_rev_repl, line) | |
228 line = deps_regex_hash.sub(deps_regex_hash_repl, line) | |
229 temp_file.write(line) | |
230 finally: | |
231 temp_file.close() | |
232 shutil.move(temp_file.name, depspath) | |
233 | |
234 | |
235 def roll_deps(revision, hashval, chromium_dir, save_branches, git): | |
236 """Upload changed DEPS and a whitespace change. | |
237 | |
238 Given the correct hashval, create two Reitveld issues. Returns a | |
239 tuple containing textual description of the two issues. | |
240 | |
241 Args: | |
242 revision: (int) Skia SVN revision. | |
243 hashval: (string) Skia Git hash. | |
244 chromium_dir: (string) path to a local chromium git repository. | |
245 save_branches: (boolean) iff false, delete temprary branches. | |
borenet
2014/01/06 14:06:46
"temporary"
hal.canary
2014/01/06 18:27:57
Done.
| |
246 git: (string) git executable. | |
247 """ | |
248 cwd = os.getcwd() | |
249 os.chdir(chromium_dir) | |
250 try: | |
251 master_hash = fetch_origin(git) | |
252 | |
253 message = 'roll skia DEPS to %d' % revision | |
254 branch = message.replace(' ','_') if save_branches else None | |
255 codereview = GitBranchCLUpload(message, ['DEPS'], git, branch) | |
256 with codereview: | |
borenet
2014/01/06 14:06:46
Why not do this on one line:
with GitBranchCLUploa
hal.canary
2014/01/06 18:27:57
I tried that; that syntax leaves codereview == No
| |
257 change_skia_deps(revision, hashval, 'DEPS') | |
258 if save_branches: | |
259 deps_issue = '%s\n branch: %s' % (codereview.issue, branch) | |
260 else: | |
261 deps_issue = codereview.issue | |
262 | |
263 message = 'whitespace change %s' % master_hash[:8] # Unique name | |
borenet
2014/01/06 14:06:46
Could this point to deps_issue as well? So that w
hal.canary
2014/01/06 18:27:57
That sounds like a pain.
| |
264 branch = message.replace(' ','_') if save_branches else None | |
265 codereview = GitBranchCLUpload(message, ['DEPS'], git, branch) | |
266 with codereview: | |
267 with open('DEPS', 'a') as output_stream: | |
268 output_stream.write('\n') | |
269 if save_branches: | |
270 whitespace_issue = '%s\n branch: %s' % ( | |
271 codereview.issue, branch) | |
272 else: | |
273 whitespace_issue = codereview.issue | |
274 | |
275 return deps_issue, whitespace_issue | |
276 finally: | |
277 os.chdir(cwd) | |
278 | |
279 | |
280 def find_hash_and_roll_deps(revision, chromium_dir, search_depth, | |
281 save_branches, git): | |
282 """Call find_hash_from_revision() and roll_deps(). | |
283 | |
284 Args: | |
285 chromium_dir: (string) path to Chromium Git repository. | |
286 revision: (int) the Skia SVN revision number. | |
287 search_depth: (int) how far back to look for the revision. | |
288 git: (string) Git executable. | |
289 save_branches: (boolean) save the temporary branches. | |
290 """ | |
291 hashval = find_hash_from_revision(revision, search_depth, git) | |
292 if not hashval: | |
293 raise Exception('failed to find revision') | |
294 | |
295 print 'revision = @%d\nhash = %s\n' % (revision, hashval) | |
296 | |
297 deps_issue, whitespace_issue = roll_deps( | |
298 revision, hashval, chromium_dir, save_branches, git) | |
299 print '\nDEPS roll:\n %s\n' % deps_issue | |
300 print 'Whitespace change:\n %s\n' % whitespace_issue | |
301 | |
302 | |
303 def main(args): | |
304 """ | |
305 main function; see module-level docstring and option_parser help. | |
306 """ | |
307 option_parser = optparse.OptionParser(usage=__doc__) | |
308 # Anyone using this script on a regular basis should set the | |
309 # CHROMIUM_REPO_PATH environment variable. | |
310 option_parser.add_option( | |
311 '-c', '--chromium_path', help='Path to Chromium Git repository.', | |
312 default=os.environ.get('CHROMIUM_REPO_PATH')) | |
313 option_parser.add_option( | |
314 '-r', '--revision', help='The Skia SVN revision number', type="int") | |
315 option_parser.add_option( | |
316 '', '--search_depth', help='How far back to look for the revision', | |
317 type="int", default=100) | |
318 option_parser.add_option( | |
319 '', '--git_path', help='Git executable', default='git') | |
320 option_parser.add_option( | |
321 '', '--save_branches', help='Save the temporary branches', | |
322 action="store_true", dest="save_branches", default=False) | |
323 | |
324 options = option_parser.parse_args(args)[0] | |
325 | |
326 if not options.revision and not options.chromium_path: | |
327 option_parser.error('Must specify revision and chromium_path.') | |
328 if not options.revision: | |
329 option_parser.error('Must specify revision.') | |
330 if not options.chromium_path: | |
331 option_parser.error('Must specify chromium_path.') | |
332 test_git(options.git_path) | |
333 | |
334 find_hash_and_roll_deps( | |
335 options.revision, options.chromium_path, options.search_depth, | |
336 options.save_branches, options.git_path) | |
337 | |
338 | |
339 if __name__ == '__main__': | |
340 main(sys.argv[1:]) | |
341 | |
OLD | NEW |