Changeset 85449 in webkit
- Timestamp:
- May 1, 2011 6:27:17 PM (13 years ago)
- Location:
- trunk/Tools
- Files:
-
- 4 added
- 4 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Tools/ChangeLog
r85447 r85449 10 10 11 11 * Scripts/webkitpy/layout_tests/port/http_server.py: 12 13 2011-05-01 Eric Seidel <eric@webkit.org> 14 15 Reviewed by Adam Barth. 16 17 scm.py should be split into many pieces 18 https://bugs.webkit.org/show_bug.cgi?id=59908 19 20 * Scripts/webkitpy/common/checkout/scm/__init__.py: 21 * Scripts/webkitpy/common/checkout/scm/commitmessage.py: Added. 22 * Scripts/webkitpy/common/checkout/scm/git.py: Added. 23 * Scripts/webkitpy/common/checkout/scm/scm.py: 24 * Scripts/webkitpy/common/checkout/scm/scm_unittest.py: 25 * Scripts/webkitpy/common/checkout/scm/svn.py: Added. 12 26 13 27 2011-05-01 Eric Seidel <eric@webkit.org> -
trunk/Tools/Scripts/webkitpy/common/checkout/scm/__init__.py
r85427 r85449 2 2 3 3 # We only export public API here. 4 from .scm import SCM, SVN, Git, CommitMessage, detect_scm_system, find_checkout_root, default_scm, AuthenticationError, AmbiguousCommitError, CheckoutNeedsUpdate 4 from .commitmessage import CommitMessage 5 from .detection import find_checkout_root, default_scm, detect_scm_system 6 from .git import Git, AmbiguousCommitError 7 from .scm import SCM, AuthenticationError, CheckoutNeedsUpdate 8 from .svn import SVN -
trunk/Tools/Scripts/webkitpy/common/checkout/scm/scm.py
r85427 r85449 36 36 import shutil 37 37 38 from webkitpy.common.memoized import memoized39 38 from webkitpy.common.system.deprecated_logging import error, log 40 39 from webkitpy.common.system.executive import Executive, run_command, ScriptError … … 42 41 43 42 44 def find_checkout_root():45 """Returns the current checkout root (as determined by default_scm().46 47 Returns the absolute path to the top of the WebKit checkout, or None48 if it cannot be determined.49 50 """51 scm_system = default_scm()52 if scm_system:53 return scm_system.checkout_root54 return None55 56 57 def default_scm(patch_directories=None):58 """Return the default SCM object as determined by the CWD and running code.59 60 Returns the default SCM object for the current working directory; if the61 CWD is not in a checkout, then we attempt to figure out if the SCM module62 itself is part of a checkout, and return that one. If neither is part of63 a checkout, None is returned.64 65 """66 cwd = os.getcwd()67 scm_system = detect_scm_system(cwd, patch_directories)68 if not scm_system:69 script_directory = os.path.dirname(os.path.abspath(__file__))70 scm_system = detect_scm_system(script_directory, patch_directories)71 if scm_system:72 log("The current directory (%s) is not a WebKit checkout, using %s" % (cwd, scm_system.checkout_root))73 else:74 error("FATAL: Failed to determine the SCM system for either %s or %s" % (cwd, script_directory))75 return scm_system76 77 78 def detect_scm_system(path, patch_directories=None):79 absolute_path = os.path.abspath(path)80 81 if patch_directories == []:82 patch_directories = None83 84 if SVN.in_working_directory(absolute_path):85 return SVN(cwd=absolute_path, patch_directories=patch_directories)86 87 if Git.in_working_directory(absolute_path):88 return Git(cwd=absolute_path)89 90 return None91 92 93 def first_non_empty_line_after_index(lines, index=0):94 first_non_empty_line = index95 for line in lines[index:]:96 if re.match("^\s*$", line):97 first_non_empty_line += 198 else:99 break100 return first_non_empty_line101 102 103 class CommitMessage:104 def __init__(self, message):105 self.message_lines = message[first_non_empty_line_after_index(message, 0):]106 107 def body(self, lstrip=False):108 lines = self.message_lines[first_non_empty_line_after_index(self.message_lines, 1):]109 if lstrip:110 lines = [line.lstrip() for line in lines]111 return "\n".join(lines) + "\n"112 113 def description(self, lstrip=False, strip_url=False):114 line = self.message_lines[0]115 if lstrip:116 line = line.lstrip()117 if strip_url:118 line = re.sub("^(\s*)<.+> ", "\1", line)119 return line120 121 def message(self):122 return "\n".join(self.message_lines) + "\n"123 124 125 43 class CheckoutNeedsUpdate(ScriptError): 126 44 def __init__(self, script_args, exit_code, output, cwd): … … 128 46 129 47 48 # FIXME: Should be moved onto SCM 130 49 def commit_error_handler(error): 131 50 if re.search("resource out of date", error.output): … … 139 58 self.prompt_for_password = prompt_for_password 140 59 141 142 class AmbiguousCommitError(Exception):143 def __init__(self, num_local_commits, working_directory_is_clean):144 self.num_local_commits = num_local_commits145 self.working_directory_is_clean = working_directory_is_clean146 60 147 61 … … 325 239 def local_commits(self): 326 240 return [] 327 328 329 # A mixin class that represents common functionality for SVN and Git-SVN.330 class SVNRepository:331 def has_authorization_for_realm(self, realm, home_directory=os.getenv("HOME")):332 # Assumes find and grep are installed.333 if not os.path.isdir(os.path.join(home_directory, ".subversion")):334 return False335 find_args = ["find", ".subversion", "-type", "f", "-exec", "grep", "-q", realm, "{}", ";", "-print"]336 find_output = self.run(find_args, cwd=home_directory, error_handler=Executive.ignore_error).rstrip()337 if not find_output or not os.path.isfile(os.path.join(home_directory, find_output)):338 return False339 # Subversion either stores the password in the credential file, indicated by the presence of the key "password",340 # or uses the system password store (e.g. Keychain on Mac OS X) as indicated by the presence of the key "passtype".341 # We assume that these keys will not coincide with the actual credential data (e.g. that a person's username342 # isn't "password") so that we can use grep.343 if self.run(["grep", "password", find_output], cwd=home_directory, return_exit_code=True) == 0:344 return True345 return self.run(["grep", "passtype", find_output], cwd=home_directory, return_exit_code=True) == 0346 347 348 class SVN(SCM, SVNRepository):349 # FIXME: These belong in common.config.urls350 svn_server_host = "svn.webkit.org"351 svn_server_realm = "<http://svn.webkit.org:80> Mac OS Forge"352 353 def __init__(self, cwd, patch_directories, executive=None):354 SCM.__init__(self, cwd, executive)355 self._bogus_dir = None356 if patch_directories == []:357 # FIXME: ScriptError is for Executive, this should probably be a normal Exception.358 raise ScriptError(script_args=svn_info_args, message='Empty list of patch directories passed to SCM.__init__')359 elif patch_directories == None:360 self._patch_directories = [ospath.relpath(cwd, self.checkout_root)]361 else:362 self._patch_directories = patch_directories363 364 @staticmethod365 def in_working_directory(path):366 return os.path.isdir(os.path.join(path, '.svn'))367 368 @classmethod369 def find_uuid(cls, path):370 if not cls.in_working_directory(path):371 return None372 return cls.value_from_svn_info(path, 'Repository UUID')373 374 @classmethod375 def value_from_svn_info(cls, path, field_name):376 svn_info_args = ['svn', 'info', path]377 info_output = run_command(svn_info_args).rstrip()378 match = re.search("^%s: (?P<value>.+)$" % field_name, info_output, re.MULTILINE)379 if not match:380 raise ScriptError(script_args=svn_info_args, message='svn info did not contain a %s.' % field_name)381 return match.group('value')382 383 @staticmethod384 def find_checkout_root(path):385 uuid = SVN.find_uuid(path)386 # If |path| is not in a working directory, we're supposed to return |path|.387 if not uuid:388 return path389 # Search up the directory hierarchy until we find a different UUID.390 last_path = None391 while True:392 if uuid != SVN.find_uuid(path):393 return last_path394 last_path = path395 (path, last_component) = os.path.split(path)396 if last_path == path:397 return None398 399 @staticmethod400 def commit_success_regexp():401 return "^Committed revision (?P<svn_revision>\d+)\.$"402 403 @memoized404 def svn_version(self):405 return self.run(['svn', '--version', '--quiet'])406 407 def working_directory_is_clean(self):408 return self.run(["svn", "diff"], cwd=self.checkout_root, decode_output=False) == ""409 410 def clean_working_directory(self):411 # Make sure there are no locks lying around from a previously aborted svn invocation.412 # This is slightly dangerous, as it's possible the user is running another svn process413 # on this checkout at the same time. However, it's much more likely that we're running414 # under windows and svn just sucks (or the user interrupted svn and it failed to clean up).415 self.run(["svn", "cleanup"], cwd=self.checkout_root)416 417 # svn revert -R is not as awesome as git reset --hard.418 # It will leave added files around, causing later svn update419 # calls to fail on the bots. We make this mirror git reset --hard420 # by deleting any added files as well.421 added_files = reversed(sorted(self.added_files()))422 # added_files() returns directories for SVN, we walk the files in reverse path423 # length order so that we remove files before we try to remove the directories.424 self.run(["svn", "revert", "-R", "."], cwd=self.checkout_root)425 for path in added_files:426 # This is robust against cwd != self.checkout_root427 absolute_path = self.absolute_path(path)428 # Completely lame that there is no easy way to remove both types with one call.429 if os.path.isdir(path):430 os.rmdir(absolute_path)431 else:432 os.remove(absolute_path)433 434 def status_command(self):435 return ['svn', 'status']436 437 def _status_regexp(self, expected_types):438 field_count = 6 if self.svn_version() > "1.6" else 5439 return "^(?P<status>[%s]).{%s} (?P<filename>.+)$" % (expected_types, field_count)440 441 def _add_parent_directories(self, path):442 """Does 'svn add' to the path and its parents."""443 if self.in_working_directory(path):444 return445 dirname = os.path.dirname(path)446 # We have dirname directry - ensure it added.447 if dirname != path:448 self._add_parent_directories(dirname)449 self.add(path)450 451 def add(self, path, return_exit_code=False):452 self._add_parent_directories(os.path.dirname(os.path.abspath(path)))453 return self.run(["svn", "add", path], return_exit_code=return_exit_code)454 455 def delete(self, path):456 parent, base = os.path.split(os.path.abspath(path))457 return self.run(["svn", "delete", "--force", base], cwd=parent)458 459 def exists(self, path):460 return not self.run(["svn", "info", path], return_exit_code=True, decode_output=False)461 462 def changed_files(self, git_commit=None):463 status_command = ["svn", "status"]464 status_command.extend(self._patch_directories)465 # ACDMR: Addded, Conflicted, Deleted, Modified or Replaced466 return self.run_status_and_extract_filenames(status_command, self._status_regexp("ACDMR"))467 468 def changed_files_for_revision(self, revision):469 # As far as I can tell svn diff --summarize output looks just like svn status output.470 # No file contents printed, thus utf-8 auto-decoding in self.run is fine.471 status_command = ["svn", "diff", "--summarize", "-c", revision]472 return self.run_status_and_extract_filenames(status_command, self._status_regexp("ACDMR"))473 474 def revisions_changing_file(self, path, limit=5):475 revisions = []476 # svn log will exit(1) (and thus self.run will raise) if the path does not exist.477 log_command = ['svn', 'log', '--quiet', '--limit=%s' % limit, path]478 for line in self.run(log_command, cwd=self.checkout_root).splitlines():479 match = re.search('^r(?P<revision>\d+) ', line)480 if not match:481 continue482 revisions.append(int(match.group('revision')))483 return revisions484 485 def conflicted_files(self):486 return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("C"))487 488 def added_files(self):489 return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("A"))490 491 def deleted_files(self):492 return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("D"))493 494 @staticmethod495 def supports_local_commits():496 return False497 498 def display_name(self):499 return "svn"500 501 def head_svn_revision(self):502 return self.value_from_svn_info(self.checkout_root, 'Revision')503 504 # FIXME: This method should be on Checkout.505 def create_patch(self, git_commit=None, changed_files=None):506 """Returns a byte array (str()) representing the patch file.507 Patch files are effectively binary since they may contain508 files of multiple different encodings."""509 if changed_files == []:510 return ""511 elif changed_files == None:512 changed_files = []513 return self.run([self.script_path("svn-create-patch")] + changed_files,514 cwd=self.checkout_root, return_stderr=False,515 decode_output=False)516 517 def committer_email_for_revision(self, revision):518 return self.run(["svn", "propget", "svn:author", "--revprop", "-r", revision]).rstrip()519 520 def contents_at_revision(self, path, revision):521 """Returns a byte array (str()) containing the contents522 of path @ revision in the repository."""523 remote_path = "%s/%s" % (self._repository_url(), path)524 return self.run(["svn", "cat", "-r", revision, remote_path], decode_output=False)525 526 def diff_for_revision(self, revision):527 # FIXME: This should probably use cwd=self.checkout_root528 return self.run(['svn', 'diff', '-c', revision])529 530 def _bogus_dir_name(self):531 if sys.platform.startswith("win"):532 parent_dir = tempfile.gettempdir()533 else:534 parent_dir = sys.path[0] # tempdir is not secure.535 return os.path.join(parent_dir, "temp_svn_config")536 537 def _setup_bogus_dir(self, log):538 self._bogus_dir = self._bogus_dir_name()539 if not os.path.exists(self._bogus_dir):540 os.mkdir(self._bogus_dir)541 self._delete_bogus_dir = True542 else:543 self._delete_bogus_dir = False544 if log:545 log.debug(' Html: temp config dir: "%s".', self._bogus_dir)546 547 def _teardown_bogus_dir(self, log):548 if self._delete_bogus_dir:549 shutil.rmtree(self._bogus_dir, True)550 if log:551 log.debug(' Html: removed temp config dir: "%s".', self._bogus_dir)552 self._bogus_dir = None553 554 def diff_for_file(self, path, log=None):555 self._setup_bogus_dir(log)556 try:557 args = ['svn', 'diff']558 if self._bogus_dir:559 args += ['--config-dir', self._bogus_dir]560 args.append(path)561 return self.run(args, cwd=self.checkout_root)562 finally:563 self._teardown_bogus_dir(log)564 565 def show_head(self, path):566 return self.run(['svn', 'cat', '-r', 'BASE', path], decode_output=False)567 568 def _repository_url(self):569 return self.value_from_svn_info(self.checkout_root, 'URL')570 571 def apply_reverse_diff(self, revision):572 # '-c -revision' applies the inverse diff of 'revision'573 svn_merge_args = ['svn', 'merge', '--non-interactive', '-c', '-%s' % revision, self._repository_url()]574 log("WARNING: svn merge has been known to take more than 10 minutes to complete. It is recommended you use git for rollouts.")575 log("Running '%s'" % " ".join(svn_merge_args))576 # FIXME: Should this use cwd=self.checkout_root?577 self.run(svn_merge_args)578 579 def revert_files(self, file_paths):580 # FIXME: This should probably use cwd=self.checkout_root.581 self.run(['svn', 'revert'] + file_paths)582 583 def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None):584 # git-commit and force are not used by SVN.585 svn_commit_args = ["svn", "commit"]586 587 if not username and not self.has_authorization_for_realm(self.svn_server_realm):588 raise AuthenticationError(self.svn_server_host)589 if username:590 svn_commit_args.extend(["--username", username])591 592 svn_commit_args.extend(["-m", message])593 594 if changed_files:595 svn_commit_args.extend(changed_files)596 597 if self.dryrun:598 _log = logging.getLogger("webkitpy.common.system")599 _log.debug('Would run SVN command: "' + " ".join(svn_commit_args) + '"')600 601 # Return a string which looks like a commit so that things which parse this output will succeed.602 return "Dry run, no commit.\nCommitted revision 0."603 604 return self.run(svn_commit_args, cwd=self.checkout_root, error_handler=commit_error_handler)605 606 def svn_commit_log(self, svn_revision):607 svn_revision = self.strip_r_from_svn_revision(svn_revision)608 return self.run(['svn', 'log', '--non-interactive', '--revision', svn_revision])609 610 def last_svn_commit_log(self):611 # BASE is the checkout revision, HEAD is the remote repository revision612 # http://svnbook.red-bean.com/en/1.0/ch03s03.html613 return self.svn_commit_log('BASE')614 615 def propset(self, pname, pvalue, path):616 dir, base = os.path.split(path)617 return self.run(['svn', 'pset', pname, pvalue, base], cwd=dir)618 619 def propget(self, pname, path):620 dir, base = os.path.split(path)621 return self.run(['svn', 'pget', pname, base], cwd=dir).encode('utf-8').rstrip("\n")622 623 624 # All git-specific logic should go here.625 class Git(SCM, SVNRepository):626 627 # Git doesn't appear to document error codes, but seems to return628 # 1 or 128, mostly.629 ERROR_FILE_IS_MISSING = 128630 631 def __init__(self, cwd, executive=None):632 SCM.__init__(self, cwd, executive)633 self._check_git_architecture()634 635 def _machine_is_64bit(self):636 import platform637 # This only is tested on Mac.638 if not platform.mac_ver()[0]:639 return False640 641 # platform.architecture()[0] can be '64bit' even if the machine is 32bit:642 # http://mail.python.org/pipermail/pythonmac-sig/2009-September/021648.html643 # Use the sysctl command to find out what the processor actually supports.644 return self.run(['sysctl', '-n', 'hw.cpu64bit_capable']).rstrip() == '1'645 646 def _executable_is_64bit(self, path):647 # Again, platform.architecture() fails us. On my machine648 # git_bits = platform.architecture(executable=git_path, bits='default')[0]649 # git_bits is just 'default', meaning the call failed.650 file_output = self.run(['file', path])651 return re.search('x86_64', file_output)652 653 def _check_git_architecture(self):654 if not self._machine_is_64bit():655 return656 657 # We could path-search entirely in python or with658 # which.py (http://code.google.com/p/which), but this is easier:659 git_path = self.run(['which', 'git']).rstrip()660 if self._executable_is_64bit(git_path):661 return662 663 webkit_dev_thead_url = "https://lists.webkit.org/pipermail/webkit-dev/2010-December/015249.html"664 log("Warning: This machine is 64-bit, but the git binary (%s) does not support 64-bit.\nInstall a 64-bit git for better performance, see:\n%s\n" % (git_path, webkit_dev_thead_url))665 666 @classmethod667 def in_working_directory(cls, path):668 return run_command(['git', 'rev-parse', '--is-inside-work-tree'], cwd=path, error_handler=Executive.ignore_error).rstrip() == "true"669 670 @classmethod671 def find_checkout_root(cls, path):672 # "git rev-parse --show-cdup" would be another way to get to the root673 (checkout_root, dot_git) = os.path.split(run_command(['git', 'rev-parse', '--git-dir'], cwd=(path or "./")))674 # If we were using 2.6 # checkout_root = os.path.relpath(checkout_root, path)675 if not os.path.isabs(checkout_root): # Sometimes git returns relative paths676 checkout_root = os.path.join(path, checkout_root)677 return checkout_root678 679 @classmethod680 def to_object_name(cls, filepath):681 root_end_with_slash = os.path.join(cls.find_checkout_root(os.path.dirname(filepath)), '')682 return filepath.replace(root_end_with_slash, '')683 684 @classmethod685 def read_git_config(cls, key):686 # FIXME: This should probably use cwd=self.checkout_root.687 # Pass --get-all for cases where the config has multiple values688 return run_command(["git", "config", "--get-all", key],689 error_handler=Executive.ignore_error).rstrip('\n')690 691 @staticmethod692 def commit_success_regexp():693 return "^Committed r(?P<svn_revision>\d+)$"694 695 def discard_local_commits(self):696 # FIXME: This should probably use cwd=self.checkout_root697 self.run(['git', 'reset', '--hard', self.remote_branch_ref()])698 699 def local_commits(self):700 # FIXME: This should probably use cwd=self.checkout_root701 return self.run(['git', 'log', '--pretty=oneline', 'HEAD...' + self.remote_branch_ref()]).splitlines()702 703 def rebase_in_progress(self):704 return os.path.exists(os.path.join(self.checkout_root, '.git/rebase-apply'))705 706 def working_directory_is_clean(self):707 # FIXME: This should probably use cwd=self.checkout_root708 return self.run(['git', 'diff', 'HEAD', '--name-only']) == ""709 710 def clean_working_directory(self):711 # FIXME: These should probably use cwd=self.checkout_root.712 # Could run git clean here too, but that wouldn't match working_directory_is_clean713 self.run(['git', 'reset', '--hard', 'HEAD'])714 # Aborting rebase even though this does not match working_directory_is_clean715 if self.rebase_in_progress():716 self.run(['git', 'rebase', '--abort'])717 718 def status_command(self):719 # git status returns non-zero when there are changes, so we use git diff name --name-status HEAD instead.720 # No file contents printed, thus utf-8 autodecoding in self.run is fine.721 return ["git", "diff", "--name-status", "HEAD"]722 723 def _status_regexp(self, expected_types):724 return '^(?P<status>[%s])\t(?P<filename>.+)$' % expected_types725 726 def add(self, path, return_exit_code=False):727 return self.run(["git", "add", path], return_exit_code=return_exit_code)728 729 def delete(self, path):730 return self.run(["git", "rm", "-f", path])731 732 def exists(self, path):733 return_code = self.run(["git", "show", "HEAD:%s" % path], return_exit_code=True, decode_output=False)734 return return_code != self.ERROR_FILE_IS_MISSING735 736 def merge_base(self, git_commit):737 if git_commit:738 # Special-case HEAD.. to mean working-copy changes only.739 if git_commit.upper() == 'HEAD..':740 return 'HEAD'741 742 if '..' not in git_commit:743 git_commit = git_commit + "^.." + git_commit744 return git_commit745 746 return self.remote_merge_base()747 748 def changed_files(self, git_commit=None):749 # FIXME: --diff-filter could be used to avoid the "extract_filenames" step.750 status_command = ['git', 'diff', '-r', '--name-status', '-C', '-M', "--no-ext-diff", "--full-index", self.merge_base(git_commit)]751 # FIXME: I'm not sure we're returning the same set of files that SVN.changed_files is.752 # Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R)753 return self.run_status_and_extract_filenames(status_command, self._status_regexp("ADM"))754 755 def _changes_files_for_commit(self, git_commit):756 # --pretty="format:" makes git show not print the commit log header,757 changed_files = self.run(["git", "show", "--pretty=format:", "--name-only", git_commit]).splitlines()758 # instead it just prints a blank line at the top, so we skip the blank line:759 return changed_files[1:]760 761 def changed_files_for_revision(self, revision):762 commit_id = self.git_commit_from_svn_revision(revision)763 return self._changes_files_for_commit(commit_id)764 765 def revisions_changing_file(self, path, limit=5):766 # git rev-list head --remove-empty --limit=5 -- path would be equivalent.767 commit_ids = self.run(["git", "log", "--remove-empty", "--pretty=format:%H", "-%s" % limit, "--", path]).splitlines()768 return filter(lambda revision: revision, map(self.svn_revision_from_git_commit, commit_ids))769 770 def conflicted_files(self):771 # We do not need to pass decode_output for this diff command772 # as we're passing --name-status which does not output any data.773 status_command = ['git', 'diff', '--name-status', '-C', '-M', '--diff-filter=U']774 return self.run_status_and_extract_filenames(status_command, self._status_regexp("U"))775 776 def added_files(self):777 return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("A"))778 779 def deleted_files(self):780 return self.run_status_and_extract_filenames(self.status_command(), self._status_regexp("D"))781 782 @staticmethod783 def supports_local_commits():784 return True785 786 def display_name(self):787 return "git"788 789 def head_svn_revision(self):790 git_log = self.run(['git', 'log', '-25'])791 match = re.search("^\s*git-svn-id:.*@(?P<svn_revision>\d+)\ ", git_log, re.MULTILINE)792 if not match:793 return ""794 return str(match.group('svn_revision'))795 796 def prepend_svn_revision(self, diff):797 revision = self.head_svn_revision()798 if not revision:799 return diff800 801 return "Subversion Revision: " + revision + '\n' + diff802 803 def create_patch(self, git_commit=None, changed_files=None):804 """Returns a byte array (str()) representing the patch file.805 Patch files are effectively binary since they may contain806 files of multiple different encodings."""807 command = ['git', 'diff', '--binary', "--no-ext-diff", "--full-index", "-M", self.merge_base(git_commit), "--"]808 if changed_files:809 command += changed_files810 return self.prepend_svn_revision(self.run(command, decode_output=False, cwd=self.checkout_root))811 812 def _run_git_svn_find_rev(self, arg):813 # git svn find-rev always exits 0, even when the revision or commit is not found.814 return self.run(['git', 'svn', 'find-rev', arg], cwd=self.checkout_root).rstrip()815 816 def _string_to_int_or_none(self, string):817 try:818 return int(string)819 except ValueError, e:820 return None821 822 @memoized823 def git_commit_from_svn_revision(self, svn_revision):824 git_commit = self._run_git_svn_find_rev('r%s' % svn_revision)825 if not git_commit:826 # FIXME: Alternatively we could offer to update the checkout? Or return None?827 raise ScriptError(message='Failed to find git commit for revision %s, your checkout likely needs an update.' % svn_revision)828 return git_commit829 830 @memoized831 def svn_revision_from_git_commit(self, git_commit):832 svn_revision = self._run_git_svn_find_rev(git_commit)833 return self._string_to_int_or_none(svn_revision)834 835 def contents_at_revision(self, path, revision):836 """Returns a byte array (str()) containing the contents837 of path @ revision in the repository."""838 return self.run(["git", "show", "%s:%s" % (self.git_commit_from_svn_revision(revision), path)], decode_output=False)839 840 def diff_for_revision(self, revision):841 git_commit = self.git_commit_from_svn_revision(revision)842 return self.create_patch(git_commit)843 844 def diff_for_file(self, path, log=None):845 return self.run(['git', 'diff', 'HEAD', '--', path])846 847 def show_head(self, path):848 return self.run(['git', 'show', 'HEAD:' + self.to_object_name(path)], decode_output=False)849 850 def committer_email_for_revision(self, revision):851 git_commit = self.git_commit_from_svn_revision(revision)852 committer_email = self.run(["git", "log", "-1", "--pretty=format:%ce", git_commit])853 # Git adds an extra @repository_hash to the end of every committer email, remove it:854 return committer_email.rsplit("@", 1)[0]855 856 def apply_reverse_diff(self, revision):857 # Assume the revision is an svn revision.858 git_commit = self.git_commit_from_svn_revision(revision)859 # I think this will always fail due to ChangeLogs.860 self.run(['git', 'revert', '--no-commit', git_commit], error_handler=Executive.ignore_error)861 862 def revert_files(self, file_paths):863 self.run(['git', 'checkout', 'HEAD'] + file_paths)864 865 def _assert_can_squash(self, working_directory_is_clean):866 squash = Git.read_git_config('webkit-patch.commit-should-always-squash')867 should_squash = squash and squash.lower() == "true"868 869 if not should_squash:870 # Only warn if there are actually multiple commits to squash.871 num_local_commits = len(self.local_commits())872 if num_local_commits > 1 or (num_local_commits > 0 and not working_directory_is_clean):873 raise AmbiguousCommitError(num_local_commits, working_directory_is_clean)874 875 def commit_with_message(self, message, username=None, password=None, git_commit=None, force_squash=False, changed_files=None):876 # Username is ignored during Git commits.877 working_directory_is_clean = self.working_directory_is_clean()878 879 if git_commit:880 # Special-case HEAD.. to mean working-copy changes only.881 if git_commit.upper() == 'HEAD..':882 if working_directory_is_clean:883 raise ScriptError(message="The working copy is not modified. --git-commit=HEAD.. only commits working copy changes.")884 self.commit_locally_with_message(message)885 return self._commit_on_branch(message, 'HEAD', username=username, password=password)886 887 # Need working directory changes to be committed so we can checkout the merge branch.888 if not working_directory_is_clean:889 # FIXME: webkit-patch land will modify the ChangeLogs to correct the reviewer.890 # That will modify the working-copy and cause us to hit this error.891 # The ChangeLog modification could be made to modify the existing local commit.892 raise ScriptError(message="Working copy is modified. Cannot commit individual git_commits.")893 return self._commit_on_branch(message, git_commit, username=username, password=password)894 895 if not force_squash:896 self._assert_can_squash(working_directory_is_clean)897 self.run(['git', 'reset', '--soft', self.remote_merge_base()])898 self.commit_locally_with_message(message)899 return self.push_local_commits_to_server(username=username, password=password)900 901 def _commit_on_branch(self, message, git_commit, username=None, password=None):902 branch_ref = self.run(['git', 'symbolic-ref', 'HEAD']).strip()903 branch_name = branch_ref.replace('refs/heads/', '')904 commit_ids = self.commit_ids_from_commitish_arguments([git_commit])905 906 # We want to squash all this branch's commits into one commit with the proper description.907 # We do this by doing a "merge --squash" into a new commit branch, then dcommitting that.908 MERGE_BRANCH_NAME = 'webkit-patch-land'909 self.delete_branch(MERGE_BRANCH_NAME)910 911 # We might be in a directory that's present in this branch but not in the912 # trunk. Move up to the top of the tree so that git commands that expect a913 # valid CWD won't fail after we check out the merge branch.914 os.chdir(self.checkout_root)915 916 # Stuff our change into the merge branch.917 # We wrap in a try...finally block so if anything goes wrong, we clean up the branches.918 commit_succeeded = True919 try:920 self.run(['git', 'checkout', '-q', '-b', MERGE_BRANCH_NAME, self.remote_branch_ref()])921 922 for commit in commit_ids:923 # We're on a different branch now, so convert "head" to the branch name.924 commit = re.sub(r'(?i)head', branch_name, commit)925 # FIXME: Once changed_files and create_patch are modified to separately handle each926 # commit in a commit range, commit each cherry pick so they'll get dcommitted separately.927 self.run(['git', 'cherry-pick', '--no-commit', commit])928 929 self.run(['git', 'commit', '-m', message])930 output = self.push_local_commits_to_server(username=username, password=password)931 except Exception, e:932 log("COMMIT FAILED: " + str(e))933 output = "Commit failed."934 commit_succeeded = False935 finally:936 # And then swap back to the original branch and clean up.937 self.clean_working_directory()938 self.run(['git', 'checkout', '-q', branch_name])939 self.delete_branch(MERGE_BRANCH_NAME)940 941 return output942 943 def svn_commit_log(self, svn_revision):944 svn_revision = self.strip_r_from_svn_revision(svn_revision)945 return self.run(['git', 'svn', 'log', '-r', svn_revision])946 947 def last_svn_commit_log(self):948 return self.run(['git', 'svn', 'log', '--limit=1'])949 950 # Git-specific methods:951 def _branch_ref_exists(self, branch_ref):952 return self.run(['git', 'show-ref', '--quiet', '--verify', branch_ref], return_exit_code=True) == 0953 954 def delete_branch(self, branch_name):955 if self._branch_ref_exists('refs/heads/' + branch_name):956 self.run(['git', 'branch', '-D', branch_name])957 958 def remote_merge_base(self):959 return self.run(['git', 'merge-base', self.remote_branch_ref(), 'HEAD']).strip()960 961 def remote_branch_ref(self):962 # Use references so that we can avoid collisions, e.g. we don't want to operate on refs/heads/trunk if it exists.963 remote_branch_refs = Git.read_git_config('svn-remote.svn.fetch')964 if not remote_branch_refs:965 remote_master_ref = 'refs/remotes/origin/master'966 if not self._branch_ref_exists(remote_master_ref):967 raise ScriptError(message="Can't find a branch to diff against. svn-remote.svn.fetch is not in the git config and %s does not exist" % remote_master_ref)968 return remote_master_ref969 970 # FIXME: What's the right behavior when there are multiple svn-remotes listed?971 # For now, just use the first one.972 first_remote_branch_ref = remote_branch_refs.split('\n')[0]973 return first_remote_branch_ref.split(':')[1]974 975 def commit_locally_with_message(self, message):976 self.run(['git', 'commit', '--all', '-F', '-'], input=message)977 978 def push_local_commits_to_server(self, username=None, password=None):979 dcommit_command = ['git', 'svn', 'dcommit']980 if self.dryrun:981 dcommit_command.append('--dry-run')982 if not self.has_authorization_for_realm(SVN.svn_server_realm):983 raise AuthenticationError(SVN.svn_server_host, prompt_for_password=True)984 if username:985 dcommit_command.extend(["--username", username])986 output = self.run(dcommit_command, error_handler=commit_error_handler, input=password)987 # Return a string which looks like a commit so that things which parse this output will succeed.988 if self.dryrun:989 output += "\nCommitted r0"990 return output991 992 # This function supports the following argument formats:993 # no args : rev-list trunk..HEAD994 # A..B : rev-list A..B995 # A...B : error!996 # A B : [A, B] (different from git diff, which would use "rev-list A..B")997 def commit_ids_from_commitish_arguments(self, args):998 if not len(args):999 args.append('%s..HEAD' % self.remote_branch_ref())1000 1001 commit_ids = []1002 for commitish in args:1003 if '...' in commitish:1004 raise ScriptError(message="'...' is not supported (found in '%s'). Did you mean '..'?" % commitish)1005 elif '..' in commitish:1006 commit_ids += reversed(self.run(['git', 'rev-list', commitish]).splitlines())1007 else:1008 # Turn single commits or branch or tag names into commit ids.1009 commit_ids += self.run(['git', 'rev-parse', '--revs-only', commitish]).splitlines()1010 return commit_ids1011 1012 def commit_message_for_local_commit(self, commit_id):1013 commit_lines = self.run(['git', 'cat-file', 'commit', commit_id]).splitlines()1014 1015 # Skip the git headers.1016 first_line_after_headers = 01017 for line in commit_lines:1018 first_line_after_headers += 11019 if line == "":1020 break1021 return CommitMessage(commit_lines[first_line_after_headers:])1022 1023 def files_changed_summary_for_commit(self, commit_id):1024 return self.run(['git', 'diff-tree', '--shortstat', '--no-commit-id', commit_id]) -
trunk/Tools/Scripts/webkitpy/common/checkout/scm/scm_unittest.py
r85446 r85449 47 47 from datetime import date 48 48 from webkitpy.common.checkout.api import Checkout 49 from .scm import detect_scm_system, SCM, SVN, Git, CheckoutNeedsUpdate, commit_error_handler, AuthenticationError, AmbiguousCommitError, find_checkout_root, default_scm50 49 from webkitpy.common.config.committers import Committer # FIXME: This should not be needed 51 50 from webkitpy.common.net.bugzilla import Attachment # FIXME: This should not be needed … … 53 52 from webkitpy.common.system.outputcapture import OutputCapture 54 53 from webkitpy.tool.mocktool import MockExecutive 54 55 from .detection import find_checkout_root, default_scm, detect_scm_system 56 from .git import Git, AmbiguousCommitError 57 from .scm import SCM, CheckoutNeedsUpdate, commit_error_handler, AuthenticationError 58 from .svn import SVN 59 55 60 56 61 # Eventually we will want to write tests which work for both scms. (like update_webkit, changed_files, etc.)
Note: See TracChangeset
for help on using the changeset viewer.