OLD | NEW |
1 # coding=utf8 | 1 # coding=utf8 |
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 """Manages a project checkout. | 5 """Manages a project checkout. |
6 | 6 |
7 Includes support for svn, git-svn and git. | 7 Includes support for svn, git-svn and git. |
8 """ | 8 """ |
9 | 9 |
10 import logging | 10 import logging |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
43 except OSError: | 43 except OSError: |
44 return None | 44 return None |
45 return settings.get(key, None) | 45 return settings.get(key, None) |
46 | 46 |
47 | 47 |
48 class CheckoutBase(object): | 48 class CheckoutBase(object): |
49 def __init__(self, root_dir, project_name): | 49 def __init__(self, root_dir, project_name): |
50 self.root_dir = root_dir | 50 self.root_dir = root_dir |
51 self.project_name = project_name | 51 self.project_name = project_name |
52 self.project_path = os.path.join(self.root_dir, self.project_name) | 52 self.project_path = os.path.join(self.root_dir, self.project_name) |
| 53 self.svn_config_dir = os.path.normpath( |
| 54 os.path.join(self.root_dir, '..', 'subversion_config')) |
| 55 assert self.root_dir |
| 56 assert self.project_name |
| 57 assert self.project_path |
53 | 58 |
54 def get_settings(self, key): | 59 def get_settings(self, key): |
55 return get_code_review_setting(self.project_path, key) | 60 return get_code_review_setting(self.project_path, key) |
56 | 61 |
57 | 62 |
58 class SvnMixIn(object): | 63 class SvnMixIn(object): |
59 """MixIn class to add svn commands common to both svn and git-svn clients.""" | 64 """MixIn class to add svn commands common to both svn and git-svn clients.""" |
60 # These members need to be set by the subclass. | 65 # These members need to be set by the subclass. |
61 commit_user = None | 66 commit_user = None |
62 commit_pwd = None | 67 commit_pwd = None |
63 svn_url = None | 68 svn_url = None |
64 project_path = None | 69 project_path = None |
| 70 svn_config_dir = None |
| 71 |
| 72 def _add_svn_flags(self, args): |
| 73 args = ['svn'] + args + [ |
| 74 '--config-dir', self.svn_config_dir, '--non-interactive'] |
| 75 if self.commit_user: |
| 76 args = args + [ |
| 77 '--username', self.commit_user, '--password', self.commit_pwd] |
| 78 return args |
65 | 79 |
66 def _check_call_svn(self, args, **kwargs): | 80 def _check_call_svn(self, args, **kwargs): |
67 """Runs svn and throws an exception if the command failed.""" | 81 """Runs svn and throws an exception if the command failed.""" |
68 kwargs.setdefault('cwd', self.project_path) | 82 kwargs.setdefault('cwd', self.project_path) |
69 return subprocess2.check_call( | 83 return subprocess2.check_call(self._add_svn_flags(args), **kwargs) |
70 ['svn'] + args + ['--no-auth-cache', '--non-interactive'], **kwargs) | |
71 | 84 |
72 def _capture_svn(self, args, **kwargs): | 85 def _capture_svn(self, args, **kwargs): |
73 """Runs svn and throws an exception if the command failed. | 86 """Runs svn and throws an exception if the command failed. |
74 | 87 |
75 Returns the output. | 88 Returns the output. |
76 """ | 89 """ |
77 kwargs.setdefault('cwd', self.project_path) | 90 kwargs.setdefault('cwd', self.project_path) |
78 if self.commit_user: | 91 return subprocess2.check_capture(self._add_svn_flags(args), **kwargs) |
79 args = args + [ | |
80 '--username', self.commit_user, '--password', self.commit_pwd] | |
81 cmd = ['svn'] + args + ['--no-auth-cache', '--non-interactive'] | |
82 return subprocess2.check_capture(cmd, **kwargs) | |
83 | 92 |
84 @staticmethod | 93 @staticmethod |
85 def _parse_svn_info(output, key): | 94 def _parse_svn_info(output, key): |
86 """Returns value for key from svn info output. | 95 """Returns value for key from svn info output. |
87 | 96 |
88 Case insensitive. | 97 Case insensitive. |
89 """ | 98 """ |
90 values = {} | 99 values = {} |
91 for line in output.splitlines(False): | 100 for line in output.splitlines(False): |
92 if not line: | 101 if not line: |
93 continue | 102 continue |
94 k, v = line.split(':', 1) | 103 k, v = line.split(':', 1) |
95 k = k.strip().lower() | 104 k = k.strip().lower() |
96 v = v.strip() | 105 v = v.strip() |
97 assert not k in values | 106 assert not k in values |
98 values[k] = v | 107 values[k] = v |
99 return values.get(key, None) | 108 return values.get(key, None) |
100 | 109 |
101 def _update_committer(self, revision, new_author): | 110 def _update_committer(self, revision, new_author): |
102 """Changes the author of a commit a posteriori. | 111 """Changes the author of a commit a posteriori. |
103 | 112 |
104 This is necessary since the actual commit is done with a "commit-bot" | 113 This is necessary since the actual commit is done with a "commit-bot" |
105 credential but the original patch author needs to be assigned authorship | 114 credential but the original patch author needs to be assigned authorship |
106 of the revision. | 115 of the revision. |
107 """ | 116 """ |
108 self._check_call_svn( | 117 self._check_call_svn( |
109 ['propset', '--revprop', 'svn:author', | 118 ['propset', '--revprop', 'svn:author', |
110 '-r', revision, | 119 '-r', revision, |
111 new_author, | 120 new_author, |
112 '--username', self.commit_user, | |
113 '--password', self.commit_pwd, | |
114 self.svn_url]) | 121 self.svn_url]) |
115 | 122 |
116 | 123 |
117 class SvnCheckout(CheckoutBase, SvnMixIn): | 124 class SvnCheckout(CheckoutBase, SvnMixIn): |
118 """Manages a subversion checkout. | 125 """Manages a subversion checkout. |
119 | 126 |
120 Commit is not fully implemented yet. Reimplementing all the commands is | 127 Commit is not fully implemented yet. Reimplementing all the commands is |
121 slightly complex, svn add with history needs to be done with svn | 128 slightly complex, svn add with history needs to be done with svn |
122 copy/rename/move and the situation quickly becomes harder when directories are | 129 copy/rename/move and the situation quickly becomes harder when directories are |
123 moved. | 130 moved. |
124 """ | 131 """ |
125 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url): | 132 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url): |
126 super(SvnCheckout, self).__init__(root_dir, project_name) | 133 super(SvnCheckout, self).__init__(root_dir, project_name) |
127 self.commit_user = commit_user | 134 self.commit_user = commit_user |
128 self.commit_pwd = commit_pwd | 135 self.commit_pwd = commit_pwd |
129 self.svn_url = svn_url | 136 self.svn_url = svn_url |
130 assert bool(self.commit_user) == bool(self.commit_pwd) | 137 assert bool(self.commit_user) == bool(self.commit_pwd) |
131 assert bool(self.svn_url) | 138 assert self.svn_url |
132 | 139 |
133 def prepare(self): | 140 def prepare(self): |
134 """Creates the initial checkouts for the repo.""" | 141 """Creates the initial checkouts for the repo.""" |
135 # Will checkout if the directory is not present. | 142 # Will checkout if the directory is not present. |
136 logging.info('Checking out %s in %s' % | 143 logging.info('Checking out %s in %s' % |
137 (self.project_name, self.project_path)) | 144 (self.project_name, self.project_path)) |
138 return self._revert() | 145 return self._revert() |
139 | 146 |
140 def apply_patch(self, patch_data): | 147 def apply_patch(self, patch_data): |
141 """Applies a patch.""" | 148 """Applies a patch.""" |
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
217 | 224 |
218 | 225 |
219 class GitCheckoutBase(CheckoutBase): | 226 class GitCheckoutBase(CheckoutBase): |
220 """Base class for git checkout. Not to be used as-is.""" | 227 """Base class for git checkout. Not to be used as-is.""" |
221 def __init__(self, root_dir, project_name, remote_branch): | 228 def __init__(self, root_dir, project_name, remote_branch): |
222 super(GitCheckoutBase, self).__init__(root_dir, project_name) | 229 super(GitCheckoutBase, self).__init__(root_dir, project_name) |
223 # There is no reason to not hardcode it. | 230 # There is no reason to not hardcode it. |
224 self.remote = 'origin' | 231 self.remote = 'origin' |
225 self.remote_branch = remote_branch | 232 self.remote_branch = remote_branch |
226 self.working_branch = 'working_branch' | 233 self.working_branch = 'working_branch' |
| 234 assert self.remote_branch |
227 | 235 |
228 def prepare(self): | 236 def prepare(self): |
229 """Resets the git repository in a clean state.""" | 237 """Resets the git repository in a clean state.""" |
230 assert os.path.isdir(self.project_path) | 238 assert os.path.isdir(self.project_path) |
231 self._check_call_git(['checkout', 'master', '--force']) | 239 self._check_call_git(['checkout', 'master', '--force']) |
232 self._check_call_git(['pull', self.remote, self.remote_branch]) | 240 self._check_call_git(['pull', self.remote, self.remote_branch]) |
233 self._call_git(['branch', '-D', self.working_branch]) | 241 self._call_git(['branch', '-D', self.working_branch]) |
234 | 242 |
235 def apply_patch(self, patch_data): | 243 def apply_patch(self, patch_data): |
236 """Applies a patch on 'working_branch'.""" | 244 """Applies a patch on 'working_branch'.""" |
237 self._check_call_git( | 245 self._check_call_git( |
238 ['checkout', '-b', self.working_branch, | 246 ['checkout', '-b', self.working_branch, |
239 '%s/%s' % (self.remote, self.remote_branch)]) | 247 '%s/%s' % (self.remote, self.remote_branch)]) |
240 self._check_call_git(['apply', '--index', '-p0'], stdin=patch_data) | 248 self._check_call_git(['apply', '--index', '-p0'], stdin=patch_data) |
241 self._check_call_git(['commit', '-m', 'Committed patch']) | 249 self._check_call_git(['commit', '-m', 'Committed patch']) |
242 | 250 |
243 def commit(self, commit_message, user): | 251 def commit(self, commit_message, user): |
| 252 """Updates the commit message. |
| 253 |
| 254 Subclass needs to dcommit or push.""" |
244 self._check_call_git(['commit', '--amend', '-m', commit_message]) | 255 self._check_call_git(['commit', '--amend', '-m', commit_message]) |
245 | 256 |
246 def _check_call_git(self, args, **kwargs): | 257 def _check_call_git(self, args, **kwargs): |
247 kwargs.setdefault('cwd', self.project_path) | 258 kwargs.setdefault('cwd', self.project_path) |
248 return subprocess2.check_call(['git'] + args, **kwargs) | 259 return subprocess2.check_call(['git'] + args, **kwargs) |
249 | 260 |
250 def _call_git(self, args, **kwargs): | 261 def _call_git(self, args, **kwargs): |
251 """Like check_call but doesn't throw on failure.""" | 262 """Like check_call but doesn't throw on failure.""" |
252 kwargs.setdefault('cwd', self.project_path) | 263 kwargs.setdefault('cwd', self.project_path) |
253 return subprocess2.call(['git'] + args, **kwargs) | 264 return subprocess2.call(['git'] + args, **kwargs) |
(...skipping 10 matching lines...) Expand all Loading... |
264 commit_user, commit_pwd, | 275 commit_user, commit_pwd, |
265 svn_url, trunk): | 276 svn_url, trunk): |
266 """trunk is optional.""" | 277 """trunk is optional.""" |
267 super(GitSvnCheckoutBase, self).__init__( | 278 super(GitSvnCheckoutBase, self).__init__( |
268 root_dir, project_name + '.git', remote_branch) | 279 root_dir, project_name + '.git', remote_branch) |
269 self.commit_user = commit_user | 280 self.commit_user = commit_user |
270 self.commit_pwd = commit_pwd | 281 self.commit_pwd = commit_pwd |
271 # svn_url in this case is the root of the svn repository. | 282 # svn_url in this case is the root of the svn repository. |
272 self.svn_url = svn_url | 283 self.svn_url = svn_url |
273 self.trunk = trunk | 284 self.trunk = trunk |
| 285 assert bool(self.commit_user) == bool(self.commit_pwd) |
| 286 assert self.svn_url |
| 287 assert self.trunk |
274 | 288 |
275 def prepare(self): | 289 def prepare(self): |
276 """Resets the git repository in a clean state.""" | 290 """Resets the git repository in a clean state.""" |
277 self._check_call_git(['checkout', 'master', '--force']) | 291 self._check_call_git(['checkout', 'master', '--force']) |
278 self._check_call_git(['svn', 'rebase']) | 292 self._check_call_git_svn(['rebase']) |
279 self._call_git(['branch', '-D', self.working_branch]) | 293 self._call_git(['branch', '-D', self.working_branch]) |
280 return int(self._git_svn_info('revision')) | 294 return int(self._git_svn_info('revision')) |
281 | 295 |
282 def _git_svn_info(self, key): | 296 def _git_svn_info(self, key): |
| 297 """Calls git svn info. This doesn't support nor need --config-dir.""" |
283 return self._parse_svn_info( | 298 return self._parse_svn_info( |
284 self._check_capture_git(['svn', 'info']), key) | 299 self._check_capture_git(['svn', 'info']), key) |
285 | 300 |
286 def commit(self, commit_message, user): | 301 def commit(self, commit_message, user): |
287 """Commits a patch.""" | 302 """Commits a patch.""" |
288 logging.info('Committing patch for %s' % user) | 303 logging.info('Committing patch for %s' % user) |
289 # Fix the commit message. | 304 # Fix the commit message. |
290 super(GitSvnCheckoutBase, self).commit(commit_message, user) | 305 super(GitSvnCheckoutBase, self).commit(commit_message, user) |
291 # Commit with git svn dcommit, then use svn directly to update the | 306 # Commit with git svn dcommit, then use svn directly to update the |
292 # committer on the revision. | 307 # committer on the revision. |
293 self._check_call_git_svn(['dcommit', '--rmdir', '--find-copies-harder']) | 308 self._check_call_git_svn(['dcommit', '--rmdir', '--find-copies-harder']) |
294 revision = int(self._git_svn_info('revision')) | 309 revision = int(self._git_svn_info('revision')) |
295 # Fix the committer. | 310 # Fix the committer. |
296 self._update_committer(revision, user) | 311 self._update_committer(revision, user) |
297 return revision | 312 return revision |
298 | 313 |
299 def _cache_svn_auth(self): | 314 def _cache_svn_auth(self): |
300 """Caches the svn credentials. It is necessary since git-svn doesn't prompt | 315 """Caches the svn credentials. It is necessary since git-svn doesn't prompt |
301 for it.""" | 316 for it.""" |
302 if not self.commit_user: | 317 if not self.commit_user: |
303 return | 318 return |
304 logging.info('Caching svn credentials for %s' % self.commit_user) | 319 logging.info('Caching svn credentials for %s' % self.commit_user) |
305 subprocess2.check_call( | 320 self._check_call_svn(['ls', self.svn_url], cwd=None) |
306 ['svn', 'ls', self.svn_url, | |
307 '--username', self.commit_user, | |
308 '--password', self.commit_pwd, | |
309 '--non-interactive']) | |
310 | 321 |
311 def _check_call_git_svn(self, args, **kwargs): | 322 def _check_call_git_svn(self, args, **kwargs): |
312 """Handles svn authentication while calling git svn.""" | 323 """Handles svn authentication while calling git svn.""" |
313 args = ['svn'] + args | 324 args = ['svn'] + args + ['--config-dir', self.svn_config_dir] |
314 if self.commit_user: | 325 self._cache_svn_auth() |
315 args = args + ['--username', self.commit_user, '--no-auth-cache'] | 326 return self._check_call_git(args, **kwargs) |
316 return self._check_call_git(args, stdin=self.commit_pwd, **kwargs) | |
317 | 327 |
318 | 328 |
319 class GitSvnPremadeCheckout(GitSvnCheckoutBase): | 329 class GitSvnPremadeCheckout(GitSvnCheckoutBase): |
320 """Manages a git-svn clone made out from an initial git-svn seed. | 330 """Manages a git-svn clone made out from an initial git-svn seed. |
321 | 331 |
322 This class is very similar to GitSvnCheckout but is faster to bootstrap | 332 This class is very similar to GitSvnCheckout but is faster to bootstrap |
323 because it starts right off with an existing git-svn clone. | 333 because it starts right off with an existing git-svn clone. |
324 """ | 334 """ |
325 def __init__(self, | 335 def __init__(self, |
326 root_dir, project_name, remote_branch, | 336 root_dir, project_name, remote_branch, |
327 commit_user, commit_pwd, | 337 commit_user, commit_pwd, |
328 svn_url, trunk, git_url): | 338 svn_url, trunk, git_url): |
329 super(GitSvnPremadeCheckout, self).__init__( | 339 super(GitSvnPremadeCheckout, self).__init__( |
330 root_dir, project_name, remote_branch, | 340 root_dir, project_name, remote_branch, |
331 commit_user, commit_pwd, | 341 commit_user, commit_pwd, |
332 svn_url, trunk) | 342 svn_url, trunk) |
333 self.git_url = git_url | 343 self.git_url = git_url |
| 344 assert self.git_url |
334 | 345 |
335 def prepare(self): | 346 def prepare(self): |
336 """Creates the initial checkout for the repo.""" | 347 """Creates the initial checkout for the repo.""" |
337 if not os.path.isdir(self.project_path): | 348 if not os.path.isdir(self.project_path): |
338 logging.info('Checking out %s in %s' % | 349 logging.info('Checking out %s in %s' % |
339 (self.project_name, self.project_path)) | 350 (self.project_name, self.project_path)) |
340 assert self.remote == 'origin' | 351 assert self.remote == 'origin' |
341 self._check_call_git( | 352 self._check_call_git( |
342 ['clone', self.git_url, self.project_name], | 353 ['clone', self.git_url, self.project_name], |
343 cwd=self.root_dir) | 354 cwd=self.root_dir) |
344 self._check_call_git( | 355 self._check_call_git_svn( |
345 ['svn', 'init', | 356 ['init', |
346 '--prefix', self.remote + '/', | 357 '--prefix', self.remote + '/', |
347 '-T', self.trunk, | 358 '-T', self.trunk, |
348 self.svn_url]) | 359 self.svn_url]) |
349 self._check_call_git_svn(['fetch']) | 360 self._check_call_git_svn(['fetch']) |
350 super(GitSvnPremadeCheckout, self).prepare() | 361 super(GitSvnPremadeCheckout, self).prepare() |
351 return int(self._git_svn_info('revision')) | 362 return int(self._git_svn_info('revision')) |
352 | 363 |
353 | 364 |
354 class GitSvnCheckout(GitSvnCheckoutBase): | 365 class GitSvnCheckout(GitSvnCheckoutBase): |
355 """Manages a git-svn clone. | 366 """Manages a git-svn clone. |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
400 user, message)) | 411 user, message)) |
401 return 'FAKE' | 412 return 'FAKE' |
402 | 413 |
403 @property | 414 @property |
404 def project_name(self): | 415 def project_name(self): |
405 return self.checkout.project_name | 416 return self.checkout.project_name |
406 | 417 |
407 @property | 418 @property |
408 def project_path(self): | 419 def project_path(self): |
409 return self.checkout.project_path | 420 return self.checkout.project_path |
OLD | NEW |