1 # Copyright (c) 2010 Google Inc. All rights reserved. 2 # 3 # Redistribution and use in source and binary forms, with or without 4 # modification, are permitted provided that the following conditions are 5 # met: 6 # 7 # * Redistributions of source code must retain the above copyright 8 # notice, this list of conditions and the following disclaimer. 9 # * Redistributions in binary form must reproduce the above 10 # copyright notice, this list of conditions and the following disclaimer 11 # in the documentation and/or other materials provided with the 12 # distribution. 13 # * Neither the name of Google Inc. nor the names of its 14 # contributors may be used to endorse or promote products derived from 15 # this software without specific prior written permission. 16 # 17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29 import os 30 import StringIO 31 32 from webkitpy.common.config import urls 33 from webkitpy.common.checkout.changelog import ChangeLog 34 from webkitpy.common.checkout.commitinfo import CommitInfo 35 from webkitpy.common.checkout.scm import CommitMessage 36 from webkitpy.common.checkout.deps import DEPS 37 from webkitpy.common.memoized import memoized 38 from webkitpy.common.net.bugzilla import parse_bug_id_from_changelog 39 from webkitpy.common.system.executive import Executive, run_command, ScriptError 40 from webkitpy.common.system.deprecated_logging import log 41 42 43 # This class represents the WebKit-specific parts of the checkout (like ChangeLogs). 44 # FIXME: Move a bunch of ChangeLog-specific processing from SCM to this object. 45 # NOTE: All paths returned from this class should be absolute. 46 class Checkout(object): 47 def __init__(self, scm): 48 self._scm = scm 49 50 def is_path_to_changelog(self, path): 51 return os.path.basename(path) == "ChangeLog" 52 53 def _latest_entry_for_changelog_at_revision(self, changelog_path, revision): 54 changelog_contents = self._scm.contents_at_revision(changelog_path, revision) 55 # contents_at_revision returns a byte array (str()), but we know 56 # that ChangeLog files are utf-8. parse_latest_entry_from_file 57 # expects a file-like object which vends unicode(), so we decode here. 58 # Old revisions of Sources/WebKit/wx/ChangeLog have some invalid utf8 characters. 59 changelog_file = StringIO.StringIO(changelog_contents.decode("utf-8", "ignore")) 60 return ChangeLog.parse_latest_entry_from_file(changelog_file) 61 62 def changelog_entries_for_revision(self, revision): 63 changed_files = self._scm.changed_files_for_revision(revision) 64 # FIXME: This gets confused if ChangeLog files are moved, as 65 # deletes are still "changed files" per changed_files_for_revision. 66 # FIXME: For now we hack around this by caching any exceptions 67 # which result from having deleted files included the changed_files list. 68 changelog_entries = [] 69 for path in changed_files: 70 if not self.is_path_to_changelog(path): 71 continue 72 try: 73 changelog_entries.append(self._latest_entry_for_changelog_at_revision(path, revision)) 74 except ScriptError: 75 pass 76 return changelog_entries 77 78 @memoized 79 def commit_info_for_revision(self, revision): 80 committer_email = self._scm.committer_email_for_revision(revision) 81 changelog_entries = self.changelog_entries_for_revision(revision) 82 # Assume for now that the first entry has everything we need: 83 # FIXME: This will throw an exception if there were no ChangeLogs. 84 if not len(changelog_entries): 85 return None 86 changelog_entry = changelog_entries[0] 87 changelog_data = { 88 "bug_id": parse_bug_id_from_changelog(changelog_entry.contents()), 89 "author_name": changelog_entry.author_name(), 90 "author_email": changelog_entry.author_email(), 91 "author": changelog_entry.author(), 92 "reviewer_text": changelog_entry.reviewer_text(), 93 "reviewer": changelog_entry.reviewer(), 94 } 95 # We could pass the changelog_entry instead of a dictionary here, but that makes 96 # mocking slightly more involved, and would make aggregating data from multiple 97 # entries more difficult to wire in if we need to do that in the future. 98 return CommitInfo(revision, committer_email, changelog_data) 99 100 def bug_id_for_revision(self, revision): 101 return self.commit_info_for_revision(revision).bug_id() 102 103 def _modified_files_matching_predicate(self, git_commit, predicate, changed_files=None): 104 # SCM returns paths relative to scm.checkout_root 105 # Callers (especially those using the ChangeLog class) may 106 # expect absolute paths, so this method returns absolute paths. 107 if not changed_files: 108 changed_files = self._scm.changed_files(git_commit) 109 absolute_paths = [os.path.join(self._scm.checkout_root, path) for path in changed_files] 110 return [path for path in absolute_paths if predicate(path)] 111 112 def modified_changelogs(self, git_commit, changed_files=None): 113 return self._modified_files_matching_predicate(git_commit, self.is_path_to_changelog, changed_files=changed_files) 114 115 def modified_non_changelogs(self, git_commit, changed_files=None): 116 return self._modified_files_matching_predicate(git_commit, lambda path: not self.is_path_to_changelog(path), changed_files=changed_files) 117 118 def commit_message_for_this_commit(self, git_commit, changed_files=None): 119 changelog_paths = self.modified_changelogs(git_commit, changed_files) 120 if not len(changelog_paths): 121 raise ScriptError(message="Found no modified ChangeLogs, cannot create a commit message.\n" 122 "All changes require a ChangeLog. See:\n %s" % urls.contribution_guidelines) 123 124 changelog_messages = [] 125 for changelog_path in changelog_paths: 126 log("Parsing ChangeLog: %s" % changelog_path) 127 changelog_entry = ChangeLog(changelog_path).latest_entry() 128 if not changelog_entry: 129 raise ScriptError(message="Failed to parse ChangeLog: %s" % os.path.abspath(changelog_path)) 130 changelog_messages.append(changelog_entry.contents()) 131 132 # FIXME: We should sort and label the ChangeLog messages like commit-log-editor does. 133 return CommitMessage("".join(changelog_messages).splitlines()) 134 135 def recent_commit_infos_for_files(self, paths): 136 revisions = set(sum(map(self._scm.revisions_changing_file, paths), [])) 137 return set(map(self.commit_info_for_revision, revisions)) 138 139 def suggested_reviewers(self, git_commit, changed_files=None): 140 changed_files = self.modified_non_changelogs(git_commit, changed_files) 141 commit_infos = self.recent_commit_infos_for_files(changed_files) 142 reviewers = [commit_info.reviewer() for commit_info in commit_infos if commit_info.reviewer()] 143 reviewers.extend([commit_info.author() for commit_info in commit_infos if commit_info.author() and commit_info.author().can_review]) 144 return sorted(set(reviewers)) 145 146 def bug_id_for_this_commit(self, git_commit, changed_files=None): 147 try: 148 return parse_bug_id_from_changelog(self.commit_message_for_this_commit(git_commit, changed_files).message()) 149 except ScriptError, e: 150 pass # We might not have ChangeLogs. 151 152 def chromium_deps(self): 153 return DEPS(os.path.join(self._scm.checkout_root, "Source", "WebKit", "chromium", "DEPS")) 154 155 def apply_patch(self, patch, force=False): 156 # It's possible that the patch was not made from the root directory. 157 # We should detect and handle that case. 158 # FIXME: Move _scm.script_path here once we get rid of all the dependencies. 159 args = [self._scm.script_path('svn-apply')] 160 if patch.reviewer(): 161 args += ['--reviewer', patch.reviewer().full_name] 162 if force: 163 args.append('--force') 164 run_command(args, input=patch.contents()) 165 166 def apply_reverse_diff(self, revision): 167 self._scm.apply_reverse_diff(revision) 168 169 # We revert the ChangeLogs because removing lines from a ChangeLog 170 # doesn't make sense. ChangeLogs are append only. 171 changelog_paths = self.modified_changelogs(git_commit=None) 172 if len(changelog_paths): 173 self._scm.revert_files(changelog_paths) 174 175 conflicts = self._scm.conflicted_files() 176 if len(conflicts): 177 raise ScriptError(message="Failed to apply reverse diff for revision %s because of the following conflicts:\n%s" % (revision, "\n".join(conflicts))) 178 179 def apply_reverse_diffs(self, revision_list): 180 for revision in sorted(revision_list, reverse=True): 181 self.apply_reverse_diff(revision) 182