Home | History | Annotate | Download | only in checkout
      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