Chromium Code Reviews| 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 |