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