|
1 # Copyright (C) 2009, 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 # WebKit's Python module for parsing and modifying ChangeLog files |
|
30 |
|
31 import codecs |
|
32 import fileinput # inplace file editing for set_reviewer_in_changelog |
|
33 import os.path |
|
34 import re |
|
35 import textwrap |
|
36 |
|
37 from webkitpy.common.system.deprecated_logging import log |
|
38 from webkitpy.common.config.committers import CommitterList |
|
39 from webkitpy.common.net.bugzilla import parse_bug_id |
|
40 |
|
41 |
|
42 def view_source_url(revision_number): |
|
43 # FIMXE: This doesn't really belong in this file, but we don't have a |
|
44 # better home for it yet. |
|
45 # Maybe eventually a webkit_config.py? |
|
46 return "http://trac.webkit.org/changeset/%s" % revision_number |
|
47 |
|
48 |
|
49 class ChangeLogEntry(object): |
|
50 # e.g. 2009-06-03 Eric Seidel <eric@webkit.org> |
|
51 date_line_regexp = r'^(?P<date>\d{4}-\d{2}-\d{2})\s+(?P<name>.+?)\s+<(?P<email>[^<>]+)>$' |
|
52 |
|
53 def __init__(self, contents, committer_list=CommitterList()): |
|
54 self._contents = contents |
|
55 self._committer_list = committer_list |
|
56 self._parse_entry() |
|
57 |
|
58 def _parse_entry(self): |
|
59 match = re.match(self.date_line_regexp, self._contents, re.MULTILINE) |
|
60 if not match: |
|
61 log("WARNING: Creating invalid ChangeLogEntry:\n%s" % self._contents) |
|
62 |
|
63 # FIXME: group("name") does not seem to be Unicode? Probably due to self._contents not being unicode. |
|
64 self._author_name = match.group("name") if match else None |
|
65 self._author_email = match.group("email") if match else None |
|
66 |
|
67 match = re.search("^\s+Reviewed by (?P<reviewer>.*?)[\.,]?\s*$", self._contents, re.MULTILINE) # Discard everything after the first period |
|
68 self._reviewer_text = match.group("reviewer") if match else None |
|
69 |
|
70 self._reviewer = self._committer_list.committer_by_name(self._reviewer_text) |
|
71 self._author = self._committer_list.committer_by_email(self._author_email) or self._committer_list.committer_by_name(self._author_name) |
|
72 |
|
73 def author_name(self): |
|
74 return self._author_name |
|
75 |
|
76 def author_email(self): |
|
77 return self._author_email |
|
78 |
|
79 def author(self): |
|
80 return self._author # Might be None |
|
81 |
|
82 # FIXME: Eventually we would like to map reviwer names to reviewer objects. |
|
83 # See https://bugs.webkit.org/show_bug.cgi?id=26533 |
|
84 def reviewer_text(self): |
|
85 return self._reviewer_text |
|
86 |
|
87 def reviewer(self): |
|
88 return self._reviewer # Might be None |
|
89 |
|
90 def contents(self): |
|
91 return self._contents |
|
92 |
|
93 def bug_id(self): |
|
94 return parse_bug_id(self._contents) |
|
95 |
|
96 |
|
97 # FIXME: Various methods on ChangeLog should move into ChangeLogEntry instead. |
|
98 class ChangeLog(object): |
|
99 |
|
100 def __init__(self, path): |
|
101 self.path = path |
|
102 |
|
103 _changelog_indent = " " * 8 |
|
104 |
|
105 @staticmethod |
|
106 def parse_latest_entry_from_file(changelog_file): |
|
107 """changelog_file must be a file-like object which returns |
|
108 unicode strings. Use codecs.open or StringIO(unicode()) |
|
109 to pass file objects to this class.""" |
|
110 date_line_regexp = re.compile(ChangeLogEntry.date_line_regexp) |
|
111 entry_lines = [] |
|
112 # The first line should be a date line. |
|
113 first_line = changelog_file.readline() |
|
114 assert(isinstance(first_line, unicode)) |
|
115 if not date_line_regexp.match(first_line): |
|
116 return None |
|
117 entry_lines.append(first_line) |
|
118 |
|
119 for line in changelog_file: |
|
120 # If we've hit the next entry, return. |
|
121 if date_line_regexp.match(line): |
|
122 # Remove the extra newline at the end |
|
123 return ChangeLogEntry(''.join(entry_lines[:-1])) |
|
124 entry_lines.append(line) |
|
125 return None # We never found a date line! |
|
126 |
|
127 def latest_entry(self): |
|
128 # ChangeLog files are always UTF-8, we read them in as such to support Reviewers with unicode in their names. |
|
129 changelog_file = codecs.open(self.path, "r", "utf-8") |
|
130 try: |
|
131 return self.parse_latest_entry_from_file(changelog_file) |
|
132 finally: |
|
133 changelog_file.close() |
|
134 |
|
135 # _wrap_line and _wrap_lines exist to work around |
|
136 # http://bugs.python.org/issue1859 |
|
137 |
|
138 def _wrap_line(self, line): |
|
139 return textwrap.fill(line, |
|
140 width=70, |
|
141 initial_indent=self._changelog_indent, |
|
142 # Don't break urls which may be longer than width. |
|
143 break_long_words=False, |
|
144 subsequent_indent=self._changelog_indent) |
|
145 |
|
146 # Workaround as suggested by guido in |
|
147 # http://bugs.python.org/issue1859#msg60040 |
|
148 |
|
149 def _wrap_lines(self, message): |
|
150 lines = [self._wrap_line(line) for line in message.splitlines()] |
|
151 return "\n".join(lines) |
|
152 |
|
153 # This probably does not belong in changelogs.py |
|
154 def _message_for_revert(self, revision, reason, bug_url): |
|
155 message = "Unreviewed, rolling out r%s.\n" % revision |
|
156 message += "%s\n" % view_source_url(revision) |
|
157 if bug_url: |
|
158 message += "%s\n" % bug_url |
|
159 # Add an extra new line after the rollout links, before any reason. |
|
160 message += "\n" |
|
161 if reason: |
|
162 message += "%s\n\n" % reason |
|
163 return self._wrap_lines(message) |
|
164 |
|
165 def update_for_revert(self, revision, reason, bug_url=None): |
|
166 reviewed_by_regexp = re.compile( |
|
167 "%sReviewed by NOBODY \(OOPS!\)\." % self._changelog_indent) |
|
168 removing_boilerplate = False |
|
169 # inplace=1 creates a backup file and re-directs stdout to the file |
|
170 for line in fileinput.FileInput(self.path, inplace=1): |
|
171 if reviewed_by_regexp.search(line): |
|
172 message_lines = self._message_for_revert(revision, |
|
173 reason, |
|
174 bug_url) |
|
175 print reviewed_by_regexp.sub(message_lines, line), |
|
176 # Remove all the ChangeLog boilerplate between the Reviewed by |
|
177 # line and the first changed file. |
|
178 removing_boilerplate = True |
|
179 elif removing_boilerplate: |
|
180 if line.find('*') >= 0: # each changed file is preceded by a * |
|
181 removing_boilerplate = False |
|
182 |
|
183 if not removing_boilerplate: |
|
184 print line, |
|
185 |
|
186 def set_reviewer(self, reviewer): |
|
187 # inplace=1 creates a backup file and re-directs stdout to the file |
|
188 for line in fileinput.FileInput(self.path, inplace=1): |
|
189 # Trailing comma suppresses printing newline |
|
190 print line.replace("NOBODY (OOPS!)", reviewer.encode("utf-8")), |
|
191 |
|
192 def set_short_description_and_bug_url(self, short_description, bug_url): |
|
193 message = "%s\n %s" % (short_description, bug_url) |
|
194 for line in fileinput.FileInput(self.path, inplace=1): |
|
195 print line.replace("Need a short description and bug URL (OOPS!)", message.encode("utf-8")), |