Clean up ChunkedUpdateDrawingAreaProxy
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / common / checkout / api.py
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.memoized import memoized
37 from webkitpy.common.net.bugzilla import parse_bug_id
38 from webkitpy.common.system.executive import Executive, run_command, ScriptError
39 from webkitpy.common.system.deprecated_logging import log
40
41
42 # This class represents the WebKit-specific parts of the checkout (like
43 # ChangeLogs).
44 # FIXME: Move a bunch of ChangeLog-specific processing from SCM to this object.
45 class Checkout(object):
46     def __init__(self, scm):
47         self._scm = scm
48
49     def _is_path_to_changelog(self, path):
50         return os.path.basename(path) == "ChangeLog"
51
52     def _latest_entry_for_changelog_at_revision(self, changelog_path, revision):
53         changelog_contents = self._scm.contents_at_revision(changelog_path, revision)
54         # contents_at_revision returns a byte array (str()), but we know
55         # that ChangeLog files are utf-8.  parse_latest_entry_from_file
56         # expects a file-like object which vends unicode(), so we decode here.
57         changelog_file = StringIO.StringIO(changelog_contents.decode("utf-8"))
58         return ChangeLog.parse_latest_entry_from_file(changelog_file)
59
60     def changelog_entries_for_revision(self, revision):
61         changed_files = self._scm.changed_files_for_revision(revision)
62         return [self._latest_entry_for_changelog_at_revision(path, revision) for path in changed_files if self._is_path_to_changelog(path)]
63
64     @memoized
65     def commit_info_for_revision(self, revision):
66         committer_email = self._scm.committer_email_for_revision(revision)
67         changelog_entries = self.changelog_entries_for_revision(revision)
68         # Assume for now that the first entry has everything we need:
69         # FIXME: This will throw an exception if there were no ChangeLogs.
70         if not len(changelog_entries):
71             return None
72         changelog_entry = changelog_entries[0]
73         changelog_data = {
74             "bug_id": parse_bug_id(changelog_entry.contents()),
75             "author_name": changelog_entry.author_name(),
76             "author_email": changelog_entry.author_email(),
77             "author": changelog_entry.author(),
78             "reviewer_text": changelog_entry.reviewer_text(),
79             "reviewer": changelog_entry.reviewer(),
80         }
81         # We could pass the changelog_entry instead of a dictionary here, but that makes
82         # mocking slightly more involved, and would make aggregating data from multiple
83         # entries more difficult to wire in if we need to do that in the future.
84         return CommitInfo(revision, committer_email, changelog_data)
85
86     def bug_id_for_revision(self, revision):
87         return self.commit_info_for_revision(revision).bug_id()
88
89     def _modified_files_matching_predicate(self, git_commit, predicate, changed_files=None):
90         # SCM returns paths relative to scm.checkout_root
91         # Callers (especially those using the ChangeLog class) may
92         # expect absolute paths, so this method returns absolute paths.
93         if not changed_files:
94             changed_files = self._scm.changed_files(git_commit)
95         absolute_paths = [os.path.join(self._scm.checkout_root, path) for path in changed_files]
96         return [path for path in absolute_paths if predicate(path)]
97
98     def modified_changelogs(self, git_commit, changed_files=None):
99         return self._modified_files_matching_predicate(git_commit, self._is_path_to_changelog, changed_files=changed_files)
100
101     def modified_non_changelogs(self, git_commit, changed_files=None):
102         return self._modified_files_matching_predicate(git_commit, lambda path: not self._is_path_to_changelog(path), changed_files=changed_files)
103
104     def commit_message_for_this_commit(self, git_commit, changed_files=None):
105         changelog_paths = self.modified_changelogs(git_commit, changed_files)
106         if not len(changelog_paths):
107             raise ScriptError(message="Found no modified ChangeLogs, cannot create a commit message.\n"
108                               "All changes require a ChangeLog.  See:\n %s" % urls.contribution_guidelines)
109
110         changelog_messages = []
111         for changelog_path in changelog_paths:
112             log("Parsing ChangeLog: %s" % changelog_path)
113             changelog_entry = ChangeLog(changelog_path).latest_entry()
114             if not changelog_entry:
115                 raise ScriptError(message="Failed to parse ChangeLog: %s" % os.path.abspath(changelog_path))
116             changelog_messages.append(changelog_entry.contents())
117
118         # FIXME: We should sort and label the ChangeLog messages like commit-log-editor does.
119         return CommitMessage("".join(changelog_messages).splitlines())
120
121     def recent_commit_infos_for_files(self, paths):
122         revisions = set(sum(map(self._scm.revisions_changing_file, paths), []))
123         return set(map(self.commit_info_for_revision, revisions))
124
125     def suggested_reviewers(self, git_commit, changed_files=None):
126         changed_files = self.modified_non_changelogs(git_commit, changed_files)
127         commit_infos = self.recent_commit_infos_for_files(changed_files)
128         reviewers = [commit_info.reviewer() for commit_info in commit_infos if commit_info.reviewer()]
129         reviewers.extend([commit_info.author() for commit_info in commit_infos if commit_info.author() and commit_info.author().can_review])
130         return sorted(set(reviewers))
131
132     def bug_id_for_this_commit(self, git_commit, changed_files=None):
133         try:
134             return parse_bug_id(self.commit_message_for_this_commit(git_commit, changed_files).message())
135         except ScriptError, e:
136             pass # We might not have ChangeLogs.
137
138     def apply_patch(self, patch, force=False):
139         # It's possible that the patch was not made from the root directory.
140         # We should detect and handle that case.
141         # FIXME: Move _scm.script_path here once we get rid of all the dependencies.
142         args = [self._scm.script_path('svn-apply')]
143         if patch.reviewer():
144             args += ['--reviewer', patch.reviewer().full_name]
145         if force:
146             args.append('--force')
147         run_command(args, input=patch.contents())
148
149     def apply_reverse_diff(self, revision):
150         self._scm.apply_reverse_diff(revision)
151
152         # We revert the ChangeLogs because removing lines from a ChangeLog
153         # doesn't make sense.  ChangeLogs are append only.
154         changelog_paths = self.modified_changelogs(git_commit=None)
155         if len(changelog_paths):
156             self._scm.revert_files(changelog_paths)
157
158         conflicts = self._scm.conflicted_files()
159         if len(conflicts):
160             raise ScriptError(message="Failed to apply reverse diff for revision %s because of the following conflicts:\n%s" % (revision, "\n".join(conflicts)))
161
162     def apply_reverse_diffs(self, revision_list):
163         for revision in sorted(revision_list, reverse=True):
164             self.apply_reverse_diff(revision)