OLD | NEW |
1 # coding=utf8 | 1 # coding=utf8 |
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 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 from __future__ import with_statement | 10 from __future__ import with_statement |
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
73 return get_code_review_setting(self.project_path, key) | 73 return get_code_review_setting(self.project_path, key) |
74 | 74 |
75 def prepare(self): | 75 def prepare(self): |
76 """Checks out a clean copy of the tree and removes any local modification. | 76 """Checks out a clean copy of the tree and removes any local modification. |
77 | 77 |
78 This function shouldn't throw unless the remote repository is inaccessible, | 78 This function shouldn't throw unless the remote repository is inaccessible, |
79 there is no free disk space or hard issues like that. | 79 there is no free disk space or hard issues like that. |
80 """ | 80 """ |
81 raise NotImplementedError() | 81 raise NotImplementedError() |
82 | 82 |
83 def apply_patch(self, patches): | 83 def apply_patch(self, patches, post_processor=None): |
84 """Applies a patch and returns the list of modified files. | 84 """Applies a patch and returns the list of modified files. |
85 | 85 |
86 This function should throw patch.UnsupportedPatchFormat or | 86 This function should throw patch.UnsupportedPatchFormat or |
87 PatchApplicationFailed when relevant. | 87 PatchApplicationFailed when relevant. |
| 88 |
| 89 Args: |
| 90 patches: patch.PatchSet object. |
| 91 post_processor: list of lambda(checkout, patches) to call on each of the |
| 92 modified files. |
88 """ | 93 """ |
89 raise NotImplementedError() | 94 raise NotImplementedError() |
90 | 95 |
91 def commit(self, commit_message, user): | 96 def commit(self, commit_message, user): |
92 """Commits the patch upstream, while impersonating 'user'.""" | 97 """Commits the patch upstream, while impersonating 'user'.""" |
93 raise NotImplementedError() | 98 raise NotImplementedError() |
94 | 99 |
95 | 100 |
96 class RawCheckout(CheckoutBase): | 101 class RawCheckout(CheckoutBase): |
97 """Used to apply a patch locally without any intent to commit it. | 102 """Used to apply a patch locally without any intent to commit it. |
98 | 103 |
99 To be used by the try server. | 104 To be used by the try server. |
100 """ | 105 """ |
101 def prepare(self): | 106 def prepare(self): |
102 """Stubbed out.""" | 107 """Stubbed out.""" |
103 pass | 108 pass |
104 | 109 |
105 def apply_patch(self, patches): | 110 def apply_patch(self, patches, post_processor=None): |
| 111 """Ignores svn properties.""" |
| 112 post_processor = post_processor or [] |
106 for p in patches: | 113 for p in patches: |
107 try: | 114 try: |
108 stdout = '' | 115 stdout = '' |
109 filename = os.path.join(self.project_path, p.filename) | 116 filename = os.path.join(self.project_path, p.filename) |
110 if p.is_delete: | 117 if p.is_delete: |
111 os.remove(filename) | 118 os.remove(filename) |
112 else: | 119 else: |
113 dirname = os.path.dirname(p.filename) | 120 dirname = os.path.dirname(p.filename) |
114 full_dir = os.path.join(self.project_path, dirname) | 121 full_dir = os.path.join(self.project_path, dirname) |
115 if dirname and not os.path.isdir(full_dir): | 122 if dirname and not os.path.isdir(full_dir): |
116 os.makedirs(full_dir) | 123 os.makedirs(full_dir) |
117 if p.is_binary: | 124 if p.is_binary: |
118 with open(os.path.join(filename), 'wb') as f: | 125 with open(os.path.join(filename), 'wb') as f: |
119 f.write(p.get()) | 126 f.write(p.get()) |
120 else: | 127 else: |
121 stdout = subprocess2.check_output( | 128 stdout = subprocess2.check_output( |
122 ['patch', '-p%s' % p.patchlevel], | 129 ['patch', '-p%s' % p.patchlevel], |
123 stdin=p.get(), | 130 stdin=p.get(), |
124 cwd=self.project_path) | 131 cwd=self.project_path) |
125 # Ignore p.svn_properties. | 132 for post in post_processor: |
| 133 post(self, p) |
126 except OSError, e: | 134 except OSError, e: |
127 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e)) | 135 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e)) |
128 except subprocess.CalledProcessError, e: | 136 except subprocess.CalledProcessError, e: |
129 raise PatchApplicationFailed( | 137 raise PatchApplicationFailed( |
130 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None))) | 138 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None))) |
131 | 139 |
132 def commit(self, commit_message, user): | 140 def commit(self, commit_message, user): |
133 """Stubbed out.""" | 141 """Stubbed out.""" |
134 raise NotImplementedError('RawCheckout can\'t commit') | 142 raise NotImplementedError('RawCheckout can\'t commit') |
135 | 143 |
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
216 """Manages a subversion checkout.""" | 224 """Manages a subversion checkout.""" |
217 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url): | 225 def __init__(self, root_dir, project_name, commit_user, commit_pwd, svn_url): |
218 super(SvnCheckout, self).__init__(root_dir, project_name) | 226 super(SvnCheckout, self).__init__(root_dir, project_name) |
219 self.commit_user = commit_user | 227 self.commit_user = commit_user |
220 self.commit_pwd = commit_pwd | 228 self.commit_pwd = commit_pwd |
221 self.svn_url = svn_url | 229 self.svn_url = svn_url |
222 assert bool(self.commit_user) >= bool(self.commit_pwd) | 230 assert bool(self.commit_user) >= bool(self.commit_pwd) |
223 assert self.svn_url | 231 assert self.svn_url |
224 | 232 |
225 def prepare(self): | 233 def prepare(self): |
226 """Creates the initial checkouts for the repo.""" | |
227 # Will checkout if the directory is not present. | 234 # Will checkout if the directory is not present. |
228 if not os.path.isdir(self.project_path): | 235 if not os.path.isdir(self.project_path): |
229 logging.info('Checking out %s in %s' % | 236 logging.info('Checking out %s in %s' % |
230 (self.project_name, self.project_path)) | 237 (self.project_name, self.project_path)) |
231 revision = self._revert() | 238 revision = self._revert() |
232 if revision != self._last_seen_revision: | 239 if revision != self._last_seen_revision: |
233 logging.info('Updated at revision %d' % revision) | 240 logging.info('Updated at revision %d' % revision) |
234 self._last_seen_revision = revision | 241 self._last_seen_revision = revision |
235 return revision | 242 return revision |
236 | 243 |
237 def apply_patch(self, patches): | 244 def apply_patch(self, patches, post_processor=None): |
238 """Applies a patch.""" | 245 post_processor = post_processor or [] |
239 for p in patches: | 246 for p in patches: |
240 try: | 247 try: |
241 stdout = '' | 248 stdout = '' |
242 if p.is_delete: | 249 if p.is_delete: |
243 stdout += self._check_output_svn(['delete', p.filename, '--force']) | 250 stdout += self._check_output_svn(['delete', p.filename, '--force']) |
244 else: | 251 else: |
245 new = not os.path.exists(p.filename) | 252 new = not os.path.exists(p.filename) |
246 | 253 |
247 # svn add while creating directories otherwise svn add on the | 254 # svn add while creating directories otherwise svn add on the |
248 # contained files will silently fail. | 255 # contained files will silently fail. |
(...skipping 18 matching lines...) Expand all Loading... |
267 cmd, stdin=p.get(), cwd=self.project_path) | 274 cmd, stdin=p.get(), cwd=self.project_path) |
268 if new: | 275 if new: |
269 stdout += self._check_output_svn(['add', p.filename, '--force']) | 276 stdout += self._check_output_svn(['add', p.filename, '--force']) |
270 for prop in p.svn_properties: | 277 for prop in p.svn_properties: |
271 stdout += self._check_output_svn( | 278 stdout += self._check_output_svn( |
272 ['propset', prop[0], prop[1], p.filename]) | 279 ['propset', prop[0], prop[1], p.filename]) |
273 for prop, value in self.svn_config.auto_props.iteritems(): | 280 for prop, value in self.svn_config.auto_props.iteritems(): |
274 if fnmatch.fnmatch(p.filename, prop): | 281 if fnmatch.fnmatch(p.filename, prop): |
275 stdout += self._check_output_svn( | 282 stdout += self._check_output_svn( |
276 ['propset'] + value.split('=', 1) + [p.filename]) | 283 ['propset'] + value.split('=', 1) + [p.filename]) |
| 284 for post in post_processor: |
| 285 post(self, p) |
277 except OSError, e: | 286 except OSError, e: |
278 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e)) | 287 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e)) |
279 except subprocess.CalledProcessError, e: | 288 except subprocess.CalledProcessError, e: |
280 raise PatchApplicationFailed( | 289 raise PatchApplicationFailed( |
281 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', ''))) | 290 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', ''))) |
282 | 291 |
283 def commit(self, commit_message, user): | 292 def commit(self, commit_message, user): |
284 logging.info('Committing patch for %s' % user) | 293 logging.info('Committing patch for %s' % user) |
285 assert self.commit_user | 294 assert self.commit_user |
286 handle, commit_filename = tempfile.mkstemp(text=True) | 295 handle, commit_filename = tempfile.mkstemp(text=True) |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
345 """ | 354 """ |
346 assert os.path.isdir(self.project_path) | 355 assert os.path.isdir(self.project_path) |
347 self._check_call_git(['reset', '--hard', '--quiet']) | 356 self._check_call_git(['reset', '--hard', '--quiet']) |
348 branches, active = self._branches() | 357 branches, active = self._branches() |
349 if active != 'master': | 358 if active != 'master': |
350 self._check_call_git(['checkout', 'master', '--force', '--quiet']) | 359 self._check_call_git(['checkout', 'master', '--force', '--quiet']) |
351 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet']) | 360 self._check_call_git(['pull', self.remote, self.remote_branch, '--quiet']) |
352 if self.working_branch in branches: | 361 if self.working_branch in branches: |
353 self._call_git(['branch', '-D', self.working_branch]) | 362 self._call_git(['branch', '-D', self.working_branch]) |
354 | 363 |
355 def apply_patch(self, patches): | 364 def apply_patch(self, patches, post_processor=None): |
356 """Applies a patch on 'working_branch' and switch to it.""" | 365 """Applies a patch on 'working_branch' and switch to it. |
| 366 |
| 367 Also commits the changes on the local branch. |
| 368 |
| 369 Ignores svn properties and raise an exception on unexpected ones. |
| 370 """ |
| 371 post_processor = post_processor or [] |
357 # It this throws, the checkout is corrupted. Maybe worth deleting it and | 372 # It this throws, the checkout is corrupted. Maybe worth deleting it and |
358 # trying again? | 373 # trying again? |
359 self._check_call_git( | 374 self._check_call_git( |
360 ['checkout', '-b', self.working_branch, | 375 ['checkout', '-b', self.working_branch, |
361 '%s/%s' % (self.remote, self.remote_branch)]) | 376 '%s/%s' % (self.remote, self.remote_branch), '--quiet']) |
362 for p in patches: | 377 for p in patches: |
363 try: | 378 try: |
364 stdout = '' | 379 stdout = '' |
365 if p.is_delete: | 380 if p.is_delete: |
366 stdout += self._check_output_git(['rm', p.filename]) | 381 stdout += self._check_output_git(['rm', p.filename]) |
367 else: | 382 else: |
368 dirname = os.path.dirname(p.filename) | 383 dirname = os.path.dirname(p.filename) |
369 full_dir = os.path.join(self.project_path, dirname) | 384 full_dir = os.path.join(self.project_path, dirname) |
370 if dirname and not os.path.isdir(full_dir): | 385 if dirname and not os.path.isdir(full_dir): |
371 os.makedirs(full_dir) | 386 os.makedirs(full_dir) |
372 if p.is_binary: | 387 if p.is_binary: |
373 with open(os.path.join(self.project_path, p.filename), 'wb') as f: | 388 with open(os.path.join(self.project_path, p.filename), 'wb') as f: |
374 f.write(p.get()) | 389 f.write(p.get()) |
375 stdout += self._check_output_git(['add', p.filename]) | 390 stdout += self._check_output_git(['add', p.filename]) |
376 else: | 391 else: |
377 stdout += self._check_output_git( | 392 stdout += self._check_output_git( |
378 ['apply', '--index', '-p%s' % p.patchlevel], stdin=p.get()) | 393 ['apply', '--index', '-p%s' % p.patchlevel], stdin=p.get()) |
379 for prop in p.svn_properties: | 394 for prop in p.svn_properties: |
380 # Ignore some known auto-props flags through .subversion/config, | 395 # Ignore some known auto-props flags through .subversion/config, |
381 # bails out on the other ones. | 396 # bails out on the other ones. |
382 # TODO(maruel): Read ~/.subversion/config and detect the rules that | 397 # TODO(maruel): Read ~/.subversion/config and detect the rules that |
383 # applies here to figure out if the property will be correctly | 398 # applies here to figure out if the property will be correctly |
384 # handled. | 399 # handled. |
385 if not prop[0] in ('svn:eol-style', 'svn:executable'): | 400 if not prop[0] in ('svn:eol-style', 'svn:executable'): |
386 raise patch.UnsupportedPatchFormat( | 401 raise patch.UnsupportedPatchFormat( |
387 p.filename, | 402 p.filename, |
388 'Cannot apply svn property %s to file %s.' % ( | 403 'Cannot apply svn property %s to file %s.' % ( |
389 prop[0], p.filename)) | 404 prop[0], p.filename)) |
| 405 for post in post_processor: |
| 406 post(self, p) |
390 except OSError, e: | 407 except OSError, e: |
391 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e)) | 408 raise PatchApplicationFailed(p.filename, '%s%s' % (stdout, e)) |
392 except subprocess.CalledProcessError, e: | 409 except subprocess.CalledProcessError, e: |
393 raise PatchApplicationFailed( | 410 raise PatchApplicationFailed( |
394 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None))) | 411 p.filename, '%s%s' % (stdout, getattr(e, 'stdout', None))) |
395 # Once all the patches are processed and added to the index, commit the | 412 # Once all the patches are processed and added to the index, commit the |
396 # index. | 413 # index. |
397 self._check_call_git(['commit', '-m', 'Committed patch']) | 414 self._check_call_git(['commit', '-m', 'Committed patch']) |
398 # TODO(maruel): Weirdly enough they don't match, need to investigate. | 415 # TODO(maruel): Weirdly enough they don't match, need to investigate. |
399 #found_files = self._check_output_git( | 416 #found_files = self._check_output_git( |
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
485 # Fix the commit message and author. It returns the git hash, which we | 502 # Fix the commit message and author. It returns the git hash, which we |
486 # ignore unless it's None. | 503 # ignore unless it's None. |
487 if not super(GitSvnCheckoutBase, self).commit(commit_message, user): | 504 if not super(GitSvnCheckoutBase, self).commit(commit_message, user): |
488 return None | 505 return None |
489 # TODO(maruel): git-svn ignores --config-dir as of git-svn version 1.7.4 and | 506 # TODO(maruel): git-svn ignores --config-dir as of git-svn version 1.7.4 and |
490 # doesn't support --with-revprop. | 507 # doesn't support --with-revprop. |
491 # Either learn perl and upstream or suck it. | 508 # Either learn perl and upstream or suck it. |
492 kwargs = {} | 509 kwargs = {} |
493 if self.commit_pwd: | 510 if self.commit_pwd: |
494 kwargs['stdin'] = self.commit_pwd + '\n' | 511 kwargs['stdin'] = self.commit_pwd + '\n' |
| 512 kwargs['stderr'] = subprocess2.STDOUT |
495 self._check_call_git_svn( | 513 self._check_call_git_svn( |
496 ['dcommit', '--rmdir', '--find-copies-harder', | 514 ['dcommit', '--rmdir', '--find-copies-harder', |
497 '--username', self.commit_user], | 515 '--username', self.commit_user], |
498 **kwargs) | 516 **kwargs) |
499 revision = int(self._git_svn_info('revision')) | 517 revision = int(self._git_svn_info('revision')) |
500 return revision | 518 return revision |
501 | 519 |
502 def _cache_svn_auth(self): | 520 def _cache_svn_auth(self): |
503 """Caches the svn credentials. It is necessary since git-svn doesn't prompt | 521 """Caches the svn credentials. It is necessary since git-svn doesn't prompt |
504 for it.""" | 522 for it.""" |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
540 assert self.git_url | 558 assert self.git_url |
541 | 559 |
542 def prepare(self): | 560 def prepare(self): |
543 """Creates the initial checkout for the repo.""" | 561 """Creates the initial checkout for the repo.""" |
544 if not os.path.isdir(self.project_path): | 562 if not os.path.isdir(self.project_path): |
545 logging.info('Checking out %s in %s' % | 563 logging.info('Checking out %s in %s' % |
546 (self.project_name, self.project_path)) | 564 (self.project_name, self.project_path)) |
547 assert self.remote == 'origin' | 565 assert self.remote == 'origin' |
548 # self.project_path doesn't exist yet. | 566 # self.project_path doesn't exist yet. |
549 self._check_call_git( | 567 self._check_call_git( |
550 ['clone', self.git_url, self.project_name], | 568 ['clone', self.git_url, self.project_name, '--quiet'], |
551 cwd=self.root_dir) | 569 cwd=self.root_dir, |
| 570 stderr=subprocess2.STDOUT) |
552 try: | 571 try: |
553 configured_svn_url = self._check_output_git( | 572 configured_svn_url = self._check_output_git( |
554 ['config', 'svn-remote.svn.url']).strip() | 573 ['config', 'svn-remote.svn.url']).strip() |
555 except subprocess.CalledProcessError: | 574 except subprocess.CalledProcessError: |
556 configured_svn_url = '' | 575 configured_svn_url = '' |
557 | 576 |
558 if configured_svn_url.strip() != self.svn_url: | 577 if configured_svn_url.strip() != self.svn_url: |
559 self._check_call_git_svn( | 578 self._check_call_git_svn( |
560 ['init', | 579 ['init', |
561 '--prefix', self.remote + '/', | 580 '--prefix', self.remote + '/', |
(...skipping 22 matching lines...) Expand all Loading... |
584 """Creates the initial checkout for the repo.""" | 603 """Creates the initial checkout for the repo.""" |
585 if not os.path.isdir(self.project_path): | 604 if not os.path.isdir(self.project_path): |
586 logging.info('Checking out %s in %s' % | 605 logging.info('Checking out %s in %s' % |
587 (self.project_name, self.project_path)) | 606 (self.project_name, self.project_path)) |
588 # TODO: Create a shallow clone. | 607 # TODO: Create a shallow clone. |
589 # self.project_path doesn't exist yet. | 608 # self.project_path doesn't exist yet. |
590 self._check_call_git_svn( | 609 self._check_call_git_svn( |
591 ['clone', | 610 ['clone', |
592 '--prefix', self.remote + '/', | 611 '--prefix', self.remote + '/', |
593 '-T', self.trunk, | 612 '-T', self.trunk, |
594 self.svn_url, self.project_path], | 613 self.svn_url, self.project_path, |
595 cwd=self.root_dir) | 614 '--quiet'], |
| 615 cwd=self.root_dir, |
| 616 stderr=subprocess2.STDOUT) |
596 super(GitSvnCheckout, self).prepare() | 617 super(GitSvnCheckout, self).prepare() |
597 return self._get_revision() | 618 return self._get_revision() |
598 | 619 |
599 | 620 |
600 class ReadOnlyCheckout(object): | 621 class ReadOnlyCheckout(object): |
601 """Converts a checkout into a read-only one.""" | 622 """Converts a checkout into a read-only one.""" |
602 def __init__(self, checkout): | 623 def __init__(self, checkout): |
603 self.checkout = checkout | 624 self.checkout = checkout |
604 | 625 |
605 def prepare(self): | 626 def prepare(self): |
606 return self.checkout.prepare() | 627 return self.checkout.prepare() |
607 | 628 |
608 def get_settings(self, key): | 629 def get_settings(self, key): |
609 return self.checkout.get_settings(key) | 630 return self.checkout.get_settings(key) |
610 | 631 |
611 def apply_patch(self, patches): | 632 def apply_patch(self, patches, post_processor=None): |
612 return self.checkout.apply_patch(patches) | 633 return self.checkout.apply_patch(patches, post_processor) |
613 | 634 |
614 def commit(self, message, user): # pylint: disable=R0201 | 635 def commit(self, message, user): # pylint: disable=R0201 |
615 logging.info('Would have committed for %s with message: %s' % ( | 636 logging.info('Would have committed for %s with message: %s' % ( |
616 user, message)) | 637 user, message)) |
617 return 'FAKE' | 638 return 'FAKE' |
618 | 639 |
619 @property | 640 @property |
620 def project_name(self): | 641 def project_name(self): |
621 return self.checkout.project_name | 642 return self.checkout.project_name |
622 | 643 |
623 @property | 644 @property |
624 def project_path(self): | 645 def project_path(self): |
625 return self.checkout.project_path | 646 return self.checkout.project_path |
OLD | NEW |