]>
Commit | Line | Data |
---|---|---|
83d290c5 | 1 | # SPDX-License-Identifier: GPL-2.0+ |
0d24de9d SG |
2 | # Copyright (c) 2011 The Chromium OS Authors. |
3 | # | |
0d24de9d | 4 | |
d06e55a7 SG |
5 | """Handles parsing a stream of commits/emails from 'git log' or other source""" |
6 | ||
6b3252e2 | 7 | import collections |
833e4192 | 8 | import datetime |
7457051e | 9 | import io |
35ce2dc4 | 10 | import math |
0d24de9d SG |
11 | import os |
12 | import re | |
6b3252e2 | 13 | import queue |
0d24de9d SG |
14 | import shutil |
15 | import tempfile | |
16 | ||
bf776679 SG |
17 | from patman import commit |
18 | from patman import gitutil | |
19 | from patman.series import Series | |
4583c002 | 20 | from u_boot_pylib import command |
0d24de9d SG |
21 | |
22 | # Tags that we detect and remove | |
57699040 | 23 | RE_REMOVE = re.compile(r'^BUG=|^TEST=|^BRANCH=|^Review URL:' |
d06e55a7 | 24 | r'|Reviewed-on:|Commit-\w*:') |
0d24de9d SG |
25 | |
26 | # Lines which are allowed after a TEST= line | |
57699040 | 27 | RE_ALLOWED_AFTER_TEST = re.compile('^Signed-off-by:') |
0d24de9d | 28 | |
05e5b735 | 29 | # Signoffs |
57699040 | 30 | RE_SIGNOFF = re.compile('^Signed-off-by: *(.*)') |
05e5b735 | 31 | |
6949f70c | 32 | # Cover letter tag |
57699040 | 33 | RE_COVER = re.compile('^Cover-([a-z-]*): *(.*)') |
fe2f8d9e | 34 | |
0d24de9d | 35 | # Patch series tag |
57699040 | 36 | RE_SERIES_TAG = re.compile('^Series-([a-z-]*): *(.*)') |
5c8fdd91 | 37 | |
833e4192 | 38 | # Change-Id will be used to generate the Message-Id and then be stripped |
57699040 | 39 | RE_CHANGE_ID = re.compile('^Change-Id: *(.*)') |
833e4192 | 40 | |
5c8fdd91 | 41 | # Commit series tag |
57699040 | 42 | RE_COMMIT_TAG = re.compile('^Commit-([a-z-]*): *(.*)') |
0d24de9d SG |
43 | |
44 | # Commit tags that we want to collect and keep | |
57699040 | 45 | RE_TAG = re.compile('^(Tested-by|Acked-by|Reviewed-by|Patch-cc|Fixes): (.*)') |
0d24de9d SG |
46 | |
47 | # The start of a new commit in the git log | |
57699040 | 48 | RE_COMMIT = re.compile('^commit ([0-9a-f]*)$') |
0d24de9d SG |
49 | |
50 | # We detect these since checkpatch doesn't always do it | |
57699040 | 51 | RE_SPACE_BEFORE_TAB = re.compile('^[+].* \t') |
0d24de9d | 52 | |
0411fff3 | 53 | # Match indented lines for changes |
57699040 | 54 | RE_LEADING_WHITESPACE = re.compile(r'^\s') |
0411fff3 | 55 | |
6b3252e2 SG |
56 | # Detect a 'diff' line |
57 | RE_DIFF = re.compile(r'^>.*diff --git a/(.*) b/(.*)$') | |
58 | ||
59 | # Detect a context line, like '> @@ -153,8 +153,13 @@ CheckPatch | |
60 | RE_LINE = re.compile(r'>.*@@ \-(\d+),\d+ \+(\d+),\d+ @@ *(.*)') | |
61 | ||
a6123333 PD |
62 | # Detect line with invalid TAG |
63 | RE_INV_TAG = re.compile('^Serie-([a-z-]*): *(.*)') | |
64 | ||
0d24de9d SG |
65 | # States we can be in - can we use range() and still have comments? |
66 | STATE_MSG_HEADER = 0 # Still in the message header | |
67 | STATE_PATCH_SUBJECT = 1 # In patch subject (first line of log for a commit) | |
68 | STATE_PATCH_HEADER = 2 # In patch header (after the subject) | |
69 | STATE_DIFFS = 3 # In the diff part (past --- line) | |
70 | ||
71 | class PatchStream: | |
72 | """Class for detecting/injecting tags in a patch or series of patches | |
73 | ||
74 | We support processing the output of 'git log' to read out the tags we | |
75 | are interested in. We can also process a patch file in order to remove | |
76 | unwanted tags or inject additional ones. These correspond to the two | |
77 | phases of processing. | |
78 | """ | |
e3a816b9 | 79 | def __init__(self, series, is_log=False): |
0d24de9d SG |
80 | self.skip_blank = False # True to skip a single blank line |
81 | self.found_test = False # Found a TEST= line | |
6949f70c | 82 | self.lines_after_test = 0 # Number of lines found after TEST= |
0d24de9d SG |
83 | self.linenum = 1 # Output line number we are up to |
84 | self.in_section = None # Name of start...END section we are in | |
85 | self.notes = [] # Series notes | |
86 | self.section = [] # The current section...END section | |
87 | self.series = series # Info about the patch series | |
88 | self.is_log = is_log # True if indent like git log | |
6949f70c SA |
89 | self.in_change = None # Name of the change list we are in |
90 | self.change_version = 0 # Non-zero if we are in a change list | |
0411fff3 | 91 | self.change_lines = [] # Lines of the current change |
0d24de9d SG |
92 | self.blank_count = 0 # Number of blank lines stored up |
93 | self.state = STATE_MSG_HEADER # What state are we in? | |
0d24de9d | 94 | self.commit = None # Current commit |
dc4b2a97 SG |
95 | # List of unquoted test blocks, each a list of str lines |
96 | self.snippets = [] | |
6b3252e2 SG |
97 | self.cur_diff = None # Last 'diff' line seen (str) |
98 | self.cur_line = None # Last context (@@) line seen (str) | |
dc4b2a97 SG |
99 | self.recent_diff = None # 'diff' line for current snippet (str) |
100 | self.recent_line = None # '@@' line for current snippet (str) | |
6b3252e2 SG |
101 | self.recent_quoted = collections.deque([], 5) |
102 | self.recent_unquoted = queue.Queue() | |
103 | self.was_quoted = None | |
0d24de9d | 104 | |
7457051e SG |
105 | @staticmethod |
106 | def process_text(text, is_comment=False): | |
107 | """Process some text through this class using a default Commit/Series | |
108 | ||
109 | Args: | |
110 | text (str): Text to parse | |
111 | is_comment (bool): True if this is a comment rather than a patch. | |
112 | If True, PatchStream doesn't expect a patch subject at the | |
113 | start, but jumps straight into the body | |
114 | ||
115 | Returns: | |
116 | PatchStream: object with results | |
117 | """ | |
118 | pstrm = PatchStream(Series()) | |
119 | pstrm.commit = commit.Commit(None) | |
120 | infd = io.StringIO(text) | |
121 | outfd = io.StringIO() | |
122 | if is_comment: | |
123 | pstrm.state = STATE_PATCH_HEADER | |
124 | pstrm.process_stream(infd, outfd) | |
125 | return pstrm | |
126 | ||
b5cc3990 | 127 | def _add_warn(self, warn): |
313ef5f8 SG |
128 | """Add a new warning to report to the user about the current commit |
129 | ||
130 | The new warning is added to the current commit if not already present. | |
b5cc3990 SG |
131 | |
132 | Args: | |
133 | warn (str): Warning to report | |
313ef5f8 SG |
134 | |
135 | Raises: | |
136 | ValueError: Warning is generated with no commit associated | |
b5cc3990 | 137 | """ |
313ef5f8 | 138 | if not self.commit: |
42bc156f SG |
139 | print('Warning outside commit: %s' % warn) |
140 | elif warn not in self.commit.warn: | |
313ef5f8 | 141 | self.commit.warn.append(warn) |
b5cc3990 | 142 | |
d93720e1 | 143 | def _add_to_series(self, line, name, value): |
0d24de9d SG |
144 | """Add a new Series-xxx tag. |
145 | ||
146 | When a Series-xxx tag is detected, we come here to record it, if we | |
147 | are scanning a 'git log'. | |
148 | ||
149 | Args: | |
1cb1c0fc SG |
150 | line (str): Source line containing tag (useful for debug/error |
151 | messages) | |
152 | name (str): Tag name (part after 'Series-') | |
153 | value (str): Tag value (part after 'Series-xxx: ') | |
0d24de9d SG |
154 | """ |
155 | if name == 'notes': | |
156 | self.in_section = name | |
157 | self.skip_blank = False | |
158 | if self.is_log: | |
dffa42c3 SG |
159 | warn = self.series.AddTag(self.commit, line, name, value) |
160 | if warn: | |
161 | self.commit.warn.append(warn) | |
0d24de9d | 162 | |
e3a816b9 | 163 | def _add_to_commit(self, name): |
5c8fdd91 AA |
164 | """Add a new Commit-xxx tag. |
165 | ||
166 | When a Commit-xxx tag is detected, we come here to record it. | |
167 | ||
168 | Args: | |
1cb1c0fc | 169 | name (str): Tag name (part after 'Commit-') |
5c8fdd91 AA |
170 | """ |
171 | if name == 'notes': | |
172 | self.in_section = 'commit-' + name | |
173 | self.skip_blank = False | |
174 | ||
d93720e1 | 175 | def _add_commit_rtag(self, rtag_type, who): |
7207e2b9 SG |
176 | """Add a response tag to the current commit |
177 | ||
178 | Args: | |
1cb1c0fc SG |
179 | rtag_type (str): rtag type (e.g. 'Reviewed-by') |
180 | who (str): Person who gave that rtag, e.g. | |
181 | 'Fred Bloggs <fred@bloggs.org>' | |
7207e2b9 | 182 | """ |
a3eeadfe | 183 | self.commit.add_rtag(rtag_type, who) |
7207e2b9 | 184 | |
d93720e1 | 185 | def _close_commit(self): |
0d24de9d SG |
186 | """Save the current commit into our commit list, and reset our state""" |
187 | if self.commit and self.is_log: | |
188 | self.series.AddCommit(self.commit) | |
189 | self.commit = None | |
0d577187 BM |
190 | # If 'END' is missing in a 'Cover-letter' section, and that section |
191 | # happens to show up at the very end of the commit message, this is | |
192 | # the chance for us to fix it up. | |
193 | if self.in_section == 'cover' and self.is_log: | |
194 | self.series.cover = self.section | |
195 | self.in_section = None | |
196 | self.skip_blank = True | |
197 | self.section = [] | |
0d24de9d | 198 | |
6b3252e2 SG |
199 | self.cur_diff = None |
200 | self.recent_diff = None | |
201 | self.recent_line = None | |
202 | ||
d93720e1 | 203 | def _parse_version(self, value, line): |
6949f70c SA |
204 | """Parse a version from a *-changes tag |
205 | ||
206 | Args: | |
1cb1c0fc SG |
207 | value (str): Tag value (part after 'xxx-changes: ' |
208 | line (str): Source line containing tag | |
6949f70c SA |
209 | |
210 | Returns: | |
1cb1c0fc SG |
211 | int: The version as an integer |
212 | ||
213 | Raises: | |
214 | ValueError: the value cannot be converted | |
6949f70c SA |
215 | """ |
216 | try: | |
217 | return int(value) | |
dd147eda | 218 | except ValueError: |
6949f70c | 219 | raise ValueError("%s: Cannot decode version info '%s'" % |
d06e55a7 | 220 | (self.commit.hash, line)) |
6949f70c | 221 | |
d93720e1 SG |
222 | def _finalise_change(self): |
223 | """_finalise a (multi-line) change and add it to the series or commit""" | |
0411fff3 SA |
224 | if not self.change_lines: |
225 | return | |
226 | change = '\n'.join(self.change_lines) | |
227 | ||
228 | if self.in_change == 'Series': | |
229 | self.series.AddChange(self.change_version, self.commit, change) | |
230 | elif self.in_change == 'Cover': | |
231 | self.series.AddChange(self.change_version, None, change) | |
232 | elif self.in_change == 'Commit': | |
a3eeadfe | 233 | self.commit.add_change(self.change_version, change) |
0411fff3 SA |
234 | self.change_lines = [] |
235 | ||
6b3252e2 SG |
236 | def _finalise_snippet(self): |
237 | """Finish off a snippet and add it to the list | |
238 | ||
239 | This is called when we get to the end of a snippet, i.e. the we enter | |
240 | the next block of quoted text: | |
241 | ||
242 | This is a comment from someone. | |
243 | ||
244 | Something else | |
245 | ||
246 | > Now we have some code <----- end of snippet | |
247 | > more code | |
248 | ||
249 | Now a comment about the above code | |
250 | ||
251 | This adds the snippet to our list | |
252 | """ | |
253 | quoted_lines = [] | |
254 | while self.recent_quoted: | |
255 | quoted_lines.append(self.recent_quoted.popleft()) | |
256 | unquoted_lines = [] | |
257 | valid = False | |
258 | while not self.recent_unquoted.empty(): | |
259 | text = self.recent_unquoted.get() | |
260 | if not (text.startswith('On ') and text.endswith('wrote:')): | |
261 | unquoted_lines.append(text) | |
262 | if text: | |
263 | valid = True | |
264 | if valid: | |
265 | lines = [] | |
266 | if self.recent_diff: | |
267 | lines.append('> File: %s' % self.recent_diff) | |
268 | if self.recent_line: | |
269 | out = '> Line: %s / %s' % self.recent_line[:2] | |
270 | if self.recent_line[2]: | |
271 | out += ': %s' % self.recent_line[2] | |
272 | lines.append(out) | |
273 | lines += quoted_lines + unquoted_lines | |
274 | if lines: | |
275 | self.snippets.append(lines) | |
276 | ||
d93720e1 | 277 | def process_line(self, line): |
0d24de9d SG |
278 | """Process a single line of a patch file or commit log |
279 | ||
280 | This process a line and returns a list of lines to output. The list | |
281 | may be empty or may contain multiple output lines. | |
282 | ||
283 | This is where all the complicated logic is located. The class's | |
284 | state is used to move between different states and detect things | |
285 | properly. | |
286 | ||
287 | We can be in one of two modes: | |
288 | self.is_log == True: This is 'git log' mode, where most output is | |
289 | indented by 4 characters and we are scanning for tags | |
290 | ||
291 | self.is_log == False: This is 'patch' mode, where we already have | |
292 | all the tags, and are processing patches to remove junk we | |
293 | don't want, and add things we think are required. | |
294 | ||
295 | Args: | |
1cb1c0fc | 296 | line (str): text line to process |
0d24de9d SG |
297 | |
298 | Returns: | |
1cb1c0fc SG |
299 | list: list of output lines, or [] if nothing should be output |
300 | ||
301 | Raises: | |
302 | ValueError: a fatal error occurred while parsing, e.g. an END | |
303 | without a starting tag, or two commits with two change IDs | |
0d24de9d SG |
304 | """ |
305 | # Initially we have no output. Prepare the input line string | |
306 | out = [] | |
307 | line = line.rstrip('\n') | |
4b89b813 | 308 | |
57699040 | 309 | commit_match = RE_COMMIT.match(line) if self.is_log else None |
4b89b813 | 310 | |
0d24de9d SG |
311 | if self.is_log: |
312 | if line[:4] == ' ': | |
313 | line = line[4:] | |
314 | ||
315 | # Handle state transition and skipping blank lines | |
57699040 SG |
316 | series_tag_match = RE_SERIES_TAG.match(line) |
317 | change_id_match = RE_CHANGE_ID.match(line) | |
318 | commit_tag_match = RE_COMMIT_TAG.match(line) | |
319 | cover_match = RE_COVER.match(line) | |
320 | signoff_match = RE_SIGNOFF.match(line) | |
321 | leading_whitespace_match = RE_LEADING_WHITESPACE.match(line) | |
6b3252e2 SG |
322 | diff_match = RE_DIFF.match(line) |
323 | line_match = RE_LINE.match(line) | |
a6123333 | 324 | invalid_match = RE_INV_TAG.match(line) |
0d24de9d SG |
325 | tag_match = None |
326 | if self.state == STATE_PATCH_HEADER: | |
57699040 | 327 | tag_match = RE_TAG.match(line) |
0d24de9d SG |
328 | is_blank = not line.strip() |
329 | if is_blank: | |
330 | if (self.state == STATE_MSG_HEADER | |
331 | or self.state == STATE_PATCH_SUBJECT): | |
332 | self.state += 1 | |
333 | ||
334 | # We don't have a subject in the text stream of patch files | |
335 | # It has its own line with a Subject: tag | |
336 | if not self.is_log and self.state == STATE_PATCH_SUBJECT: | |
337 | self.state += 1 | |
338 | elif commit_match: | |
339 | self.state = STATE_MSG_HEADER | |
340 | ||
94fbd3e3 | 341 | # If a tag is detected, or a new commit starts |
833e4192 | 342 | if series_tag_match or commit_tag_match or change_id_match or \ |
6949f70c | 343 | cover_match or signoff_match or self.state == STATE_MSG_HEADER: |
57b6b190 BM |
344 | # but we are already in a section, this means 'END' is missing |
345 | # for that section, fix it up. | |
13b98d95 | 346 | if self.in_section: |
b5cc3990 | 347 | self._add_warn("Missing 'END' in section '%s'" % self.in_section) |
13b98d95 BM |
348 | if self.in_section == 'cover': |
349 | self.series.cover = self.section | |
350 | elif self.in_section == 'notes': | |
351 | if self.is_log: | |
352 | self.series.notes += self.section | |
353 | elif self.in_section == 'commit-notes': | |
354 | if self.is_log: | |
355 | self.commit.notes += self.section | |
356 | else: | |
b5cc3990 SG |
357 | # This should not happen |
358 | raise ValueError("Unknown section '%s'" % self.in_section) | |
13b98d95 BM |
359 | self.in_section = None |
360 | self.skip_blank = True | |
361 | self.section = [] | |
57b6b190 BM |
362 | # but we are already in a change list, that means a blank line |
363 | # is missing, fix it up. | |
364 | if self.in_change: | |
b5cc3990 SG |
365 | self._add_warn("Missing 'blank line' in section '%s-changes'" % |
366 | self.in_change) | |
d93720e1 | 367 | self._finalise_change() |
6949f70c SA |
368 | self.in_change = None |
369 | self.change_version = 0 | |
13b98d95 | 370 | |
0d24de9d SG |
371 | # If we are in a section, keep collecting lines until we see END |
372 | if self.in_section: | |
373 | if line == 'END': | |
374 | if self.in_section == 'cover': | |
375 | self.series.cover = self.section | |
376 | elif self.in_section == 'notes': | |
377 | if self.is_log: | |
378 | self.series.notes += self.section | |
5c8fdd91 AA |
379 | elif self.in_section == 'commit-notes': |
380 | if self.is_log: | |
381 | self.commit.notes += self.section | |
0d24de9d | 382 | else: |
b5cc3990 SG |
383 | # This should not happen |
384 | raise ValueError("Unknown section '%s'" % self.in_section) | |
0d24de9d SG |
385 | self.in_section = None |
386 | self.skip_blank = True | |
387 | self.section = [] | |
388 | else: | |
389 | self.section.append(line) | |
390 | ||
7058dd07 PD |
391 | # If we are not in a section, it is an unexpected END |
392 | elif line == 'END': | |
d06e55a7 | 393 | raise ValueError("'END' wihout section") |
7058dd07 | 394 | |
0d24de9d SG |
395 | # Detect the commit subject |
396 | elif not is_blank and self.state == STATE_PATCH_SUBJECT: | |
397 | self.commit.subject = line | |
398 | ||
399 | # Detect the tags we want to remove, and skip blank lines | |
57699040 | 400 | elif RE_REMOVE.match(line) and not commit_tag_match: |
0d24de9d SG |
401 | self.skip_blank = True |
402 | ||
403 | # TEST= should be the last thing in the commit, so remove | |
404 | # everything after it | |
405 | if line.startswith('TEST='): | |
406 | self.found_test = True | |
407 | elif self.skip_blank and is_blank: | |
408 | self.skip_blank = False | |
409 | ||
6949f70c | 410 | # Detect Cover-xxx tags |
e7df218c | 411 | elif cover_match: |
6949f70c SA |
412 | name = cover_match.group(1) |
413 | value = cover_match.group(2) | |
414 | if name == 'letter': | |
415 | self.in_section = 'cover' | |
416 | self.skip_blank = False | |
417 | elif name == 'letter-cc': | |
d93720e1 | 418 | self._add_to_series(line, 'cover-cc', value) |
6949f70c SA |
419 | elif name == 'changes': |
420 | self.in_change = 'Cover' | |
d93720e1 | 421 | self.change_version = self._parse_version(value, line) |
fe2f8d9e | 422 | |
0d24de9d SG |
423 | # If we are in a change list, key collected lines until a blank one |
424 | elif self.in_change: | |
425 | if is_blank: | |
426 | # Blank line ends this change list | |
d93720e1 | 427 | self._finalise_change() |
6949f70c SA |
428 | self.in_change = None |
429 | self.change_version = 0 | |
102061bd | 430 | elif line == '---': |
d93720e1 | 431 | self._finalise_change() |
6949f70c SA |
432 | self.in_change = None |
433 | self.change_version = 0 | |
d93720e1 | 434 | out = self.process_line(line) |
0411fff3 SA |
435 | elif self.is_log: |
436 | if not leading_whitespace_match: | |
d93720e1 | 437 | self._finalise_change() |
0411fff3 | 438 | self.change_lines.append(line) |
0d24de9d SG |
439 | self.skip_blank = False |
440 | ||
441 | # Detect Series-xxx tags | |
5c8fdd91 AA |
442 | elif series_tag_match: |
443 | name = series_tag_match.group(1) | |
444 | value = series_tag_match.group(2) | |
0d24de9d SG |
445 | if name == 'changes': |
446 | # value is the version number: e.g. 1, or 2 | |
6949f70c | 447 | self.in_change = 'Series' |
d93720e1 | 448 | self.change_version = self._parse_version(value, line) |
0d24de9d | 449 | else: |
d93720e1 | 450 | self._add_to_series(line, name, value) |
0d24de9d SG |
451 | self.skip_blank = True |
452 | ||
833e4192 DA |
453 | # Detect Change-Id tags |
454 | elif change_id_match: | |
455 | value = change_id_match.group(1) | |
456 | if self.is_log: | |
457 | if self.commit.change_id: | |
d06e55a7 | 458 | raise ValueError( |
53336e6c SG |
459 | "%s: Two Change-Ids: '%s' vs. '%s'" % |
460 | (self.commit.hash, self.commit.change_id, value)) | |
833e4192 DA |
461 | self.commit.change_id = value |
462 | self.skip_blank = True | |
463 | ||
5c8fdd91 AA |
464 | # Detect Commit-xxx tags |
465 | elif commit_tag_match: | |
466 | name = commit_tag_match.group(1) | |
467 | value = commit_tag_match.group(2) | |
468 | if name == 'notes': | |
e3a816b9 | 469 | self._add_to_commit(name) |
5c8fdd91 | 470 | self.skip_blank = True |
6949f70c SA |
471 | elif name == 'changes': |
472 | self.in_change = 'Commit' | |
d93720e1 | 473 | self.change_version = self._parse_version(value, line) |
e5ff9ab7 | 474 | else: |
b5cc3990 SG |
475 | self._add_warn('Line %d: Ignoring Commit-%s' % |
476 | (self.linenum, name)) | |
5c8fdd91 | 477 | |
a6123333 PD |
478 | # Detect invalid tags |
479 | elif invalid_match: | |
480 | raise ValueError("Line %d: Invalid tag = '%s'" % | |
481 | (self.linenum, line)) | |
482 | ||
0d24de9d SG |
483 | # Detect the start of a new commit |
484 | elif commit_match: | |
d93720e1 | 485 | self._close_commit() |
0b5b409a | 486 | self.commit = commit.Commit(commit_match.group(1)) |
0d24de9d SG |
487 | |
488 | # Detect tags in the commit message | |
489 | elif tag_match: | |
7207e2b9 | 490 | rtag_type, who = tag_match.groups() |
d93720e1 | 491 | self._add_commit_rtag(rtag_type, who) |
0d24de9d | 492 | # Remove Tested-by self, since few will take much notice |
7207e2b9 SG |
493 | if (rtag_type == 'Tested-by' and |
494 | who.find(os.getenv('USER') + '@') != -1): | |
4af99874 | 495 | self._add_warn("Ignoring '%s'" % line) |
7207e2b9 | 496 | elif rtag_type == 'Patch-cc': |
a3eeadfe | 497 | self.commit.add_cc(who.split(',')) |
0d24de9d | 498 | else: |
d0c5719d | 499 | out = [line] |
0d24de9d | 500 | |
102061bd SG |
501 | # Suppress duplicate signoffs |
502 | elif signoff_match: | |
e752edcb | 503 | if (self.is_log or not self.commit or |
a3eeadfe | 504 | self.commit.check_duplicate_signoff(signoff_match.group(1))): |
102061bd SG |
505 | out = [line] |
506 | ||
0d24de9d SG |
507 | # Well that means this is an ordinary line |
508 | else: | |
0d24de9d | 509 | # Look for space before tab |
dd147eda SG |
510 | mat = RE_SPACE_BEFORE_TAB.match(line) |
511 | if mat: | |
b5cc3990 SG |
512 | self._add_warn('Line %d/%d has space before tab' % |
513 | (self.linenum, mat.start())) | |
0d24de9d SG |
514 | |
515 | # OK, we have a valid non-blank line | |
516 | out = [line] | |
517 | self.linenum += 1 | |
518 | self.skip_blank = False | |
6b3252e2 SG |
519 | |
520 | if diff_match: | |
521 | self.cur_diff = diff_match.group(1) | |
522 | ||
523 | # If this is quoted, keep recent lines | |
524 | if not diff_match and self.linenum > 1 and line: | |
525 | if line.startswith('>'): | |
526 | if not self.was_quoted: | |
527 | self._finalise_snippet() | |
528 | self.recent_line = None | |
529 | if not line_match: | |
530 | self.recent_quoted.append(line) | |
531 | self.was_quoted = True | |
532 | self.recent_diff = self.cur_diff | |
533 | else: | |
534 | self.recent_unquoted.put(line) | |
535 | self.was_quoted = False | |
536 | ||
537 | if line_match: | |
538 | self.recent_line = line_match.groups() | |
539 | ||
0d24de9d SG |
540 | if self.state == STATE_DIFFS: |
541 | pass | |
542 | ||
543 | # If this is the start of the diffs section, emit our tags and | |
544 | # change log | |
545 | elif line == '---': | |
546 | self.state = STATE_DIFFS | |
547 | ||
6949f70c | 548 | # Output the tags (signoff first), then change list |
0d24de9d | 549 | out = [] |
0d24de9d | 550 | log = self.series.MakeChangeLog(self.commit) |
e752edcb SG |
551 | out += [line] |
552 | if self.commit: | |
553 | out += self.commit.notes | |
554 | out += [''] + log | |
0d24de9d | 555 | elif self.found_test: |
57699040 | 556 | if not RE_ALLOWED_AFTER_TEST.match(line): |
0d24de9d SG |
557 | self.lines_after_test += 1 |
558 | ||
559 | return out | |
560 | ||
d93720e1 | 561 | def finalise(self): |
0d24de9d | 562 | """Close out processing of this patch stream""" |
6b3252e2 | 563 | self._finalise_snippet() |
d93720e1 SG |
564 | self._finalise_change() |
565 | self._close_commit() | |
0d24de9d | 566 | if self.lines_after_test: |
b5cc3990 | 567 | self._add_warn('Found %d lines after TEST=' % self.lines_after_test) |
0d24de9d | 568 | |
d93720e1 | 569 | def _write_message_id(self, outfd): |
833e4192 DA |
570 | """Write the Message-Id into the output. |
571 | ||
572 | This is based on the Change-Id in the original patch, the version, | |
573 | and the prefix. | |
574 | ||
575 | Args: | |
1cb1c0fc | 576 | outfd (io.IOBase): Output stream file object |
833e4192 DA |
577 | """ |
578 | if not self.commit.change_id: | |
579 | return | |
580 | ||
581 | # If the count is -1 we're testing, so use a fixed time | |
582 | if self.commit.count == -1: | |
583 | time_now = datetime.datetime(1999, 12, 31, 23, 59, 59) | |
584 | else: | |
585 | time_now = datetime.datetime.now() | |
586 | ||
587 | # In theory there is email.utils.make_msgid() which would be nice | |
588 | # to use, but it already produces something way too long and thus | |
589 | # will produce ugly commit lines if someone throws this into | |
590 | # a "Link:" tag in the final commit. So (sigh) roll our own. | |
591 | ||
592 | # Start with the time; presumably we wouldn't send the same series | |
593 | # with the same Change-Id at the exact same second. | |
594 | parts = [time_now.strftime("%Y%m%d%H%M%S")] | |
595 | ||
596 | # These seem like they would be nice to include. | |
597 | if 'prefix' in self.series: | |
598 | parts.append(self.series['prefix']) | |
082c119a | 599 | if 'postfix' in self.series: |
32cc6ae2 | 600 | parts.append(self.series['postfix']) |
833e4192 DA |
601 | if 'version' in self.series: |
602 | parts.append("v%s" % self.series['version']) | |
603 | ||
604 | parts.append(str(self.commit.count + 1)) | |
605 | ||
606 | # The Change-Id must be last, right before the @ | |
607 | parts.append(self.commit.change_id) | |
608 | ||
609 | # Join parts together with "." and write it out. | |
610 | outfd.write('Message-Id: <%s@changeid>\n' % '.'.join(parts)) | |
611 | ||
d93720e1 | 612 | def process_stream(self, infd, outfd): |
0d24de9d SG |
613 | """Copy a stream from infd to outfd, filtering out unwanting things. |
614 | ||
615 | This is used to process patch files one at a time. | |
616 | ||
617 | Args: | |
1cb1c0fc SG |
618 | infd (io.IOBase): Input stream file object |
619 | outfd (io.IOBase): Output stream file object | |
0d24de9d SG |
620 | """ |
621 | # Extract the filename from each diff, for nice warnings | |
622 | fname = None | |
623 | last_fname = None | |
624 | re_fname = re.compile('diff --git a/(.*) b/.*') | |
833e4192 | 625 | |
d93720e1 | 626 | self._write_message_id(outfd) |
833e4192 | 627 | |
0d24de9d SG |
628 | while True: |
629 | line = infd.readline() | |
630 | if not line: | |
631 | break | |
d93720e1 | 632 | out = self.process_line(line) |
0d24de9d SG |
633 | |
634 | # Try to detect blank lines at EOF | |
635 | for line in out: | |
636 | match = re_fname.match(line) | |
637 | if match: | |
638 | last_fname = fname | |
639 | fname = match.group(1) | |
640 | if line == '+': | |
641 | self.blank_count += 1 | |
642 | else: | |
643 | if self.blank_count and (line == '-- ' or match): | |
b5cc3990 SG |
644 | self._add_warn("Found possible blank line(s) at end of file '%s'" % |
645 | last_fname) | |
0d24de9d SG |
646 | outfd.write('+\n' * self.blank_count) |
647 | outfd.write(line + '\n') | |
648 | self.blank_count = 0 | |
d93720e1 | 649 | self.finalise() |
0d24de9d | 650 | |
8f9ba3ab SG |
651 | def insert_tags(msg, tags_to_emit): |
652 | """Add extra tags to a commit message | |
653 | ||
654 | The tags are added after an existing block of tags if found, otherwise at | |
655 | the end. | |
656 | ||
657 | Args: | |
658 | msg (str): Commit message | |
659 | tags_to_emit (list): List of tags to emit, each a str | |
660 | ||
661 | Returns: | |
662 | (str) new message | |
663 | """ | |
664 | out = [] | |
665 | done = False | |
666 | emit_tags = False | |
59747187 | 667 | emit_blank = False |
8f9ba3ab SG |
668 | for line in msg.splitlines(): |
669 | if not done: | |
670 | signoff_match = RE_SIGNOFF.match(line) | |
671 | tag_match = RE_TAG.match(line) | |
672 | if tag_match or signoff_match: | |
673 | emit_tags = True | |
674 | if emit_tags and not tag_match and not signoff_match: | |
675 | out += tags_to_emit | |
676 | emit_tags = False | |
677 | done = True | |
59747187 SG |
678 | emit_blank = not (signoff_match or tag_match) |
679 | else: | |
680 | emit_blank = line | |
8f9ba3ab SG |
681 | out.append(line) |
682 | if not done: | |
59747187 SG |
683 | if emit_blank: |
684 | out.append('') | |
8f9ba3ab SG |
685 | out += tags_to_emit |
686 | return '\n'.join(out) | |
687 | ||
688 | def get_list(commit_range, git_dir=None, count=None): | |
689 | """Get a log of a list of comments | |
690 | ||
691 | This returns the output of 'git log' for the selected commits | |
692 | ||
693 | Args: | |
694 | commit_range (str): Range of commits to count (e.g. 'HEAD..base') | |
695 | git_dir (str): Path to git repositiory (None to use default) | |
696 | count (int): Number of commits to list, or None for no limit | |
697 | ||
698 | Returns | |
699 | str: String containing the contents of the git log | |
700 | """ | |
0157b187 | 701 | params = gitutil.log_cmd(commit_range, reverse=True, count=count, |
8f9ba3ab | 702 | git_dir=git_dir) |
d9800699 | 703 | return command.run_pipe([params], capture=True).stdout |
0d24de9d | 704 | |
d93720e1 SG |
705 | def get_metadata_for_list(commit_range, git_dir=None, count=None, |
706 | series=None, allow_overwrite=False): | |
0d24de9d SG |
707 | """Reads out patch series metadata from the commits |
708 | ||
709 | This does a 'git log' on the relevant commits and pulls out the tags we | |
710 | are interested in. | |
711 | ||
712 | Args: | |
1cb1c0fc SG |
713 | commit_range (str): Range of commits to count (e.g. 'HEAD..base') |
714 | git_dir (str): Path to git repositiory (None to use default) | |
715 | count (int): Number of commits to list, or None for no limit | |
716 | series (Series): Object to add information into. By default a new series | |
e62f905e | 717 | is started. |
1cb1c0fc SG |
718 | allow_overwrite (bool): Allow tags to overwrite an existing tag |
719 | ||
e62f905e | 720 | Returns: |
1cb1c0fc | 721 | Series: Object containing information about the commits. |
0d24de9d | 722 | """ |
891b7a07 SG |
723 | if not series: |
724 | series = Series() | |
950a2313 | 725 | series.allow_overwrite = allow_overwrite |
8f9ba3ab | 726 | stdout = get_list(commit_range, git_dir, count) |
dd147eda | 727 | pst = PatchStream(series, is_log=True) |
0d24de9d | 728 | for line in stdout.splitlines(): |
dd147eda SG |
729 | pst.process_line(line) |
730 | pst.finalise() | |
0d24de9d SG |
731 | return series |
732 | ||
d93720e1 | 733 | def get_metadata(branch, start, count): |
e62f905e SG |
734 | """Reads out patch series metadata from the commits |
735 | ||
736 | This does a 'git log' on the relevant commits and pulls out the tags we | |
737 | are interested in. | |
738 | ||
739 | Args: | |
1cb1c0fc SG |
740 | branch (str): Branch to use (None for current branch) |
741 | start (int): Commit to start from: 0=branch HEAD, 1=next one, etc. | |
742 | count (int): Number of commits to list | |
743 | ||
744 | Returns: | |
745 | Series: Object containing information about the commits. | |
e62f905e | 746 | """ |
d93720e1 SG |
747 | return get_metadata_for_list( |
748 | '%s~%d' % (branch if branch else 'HEAD', start), None, count) | |
e62f905e | 749 | |
d93720e1 | 750 | def get_metadata_for_test(text): |
6e87ae1c SG |
751 | """Process metadata from a file containing a git log. Used for tests |
752 | ||
753 | Args: | |
754 | text: | |
1cb1c0fc SG |
755 | |
756 | Returns: | |
757 | Series: Object containing information about the commits. | |
6e87ae1c SG |
758 | """ |
759 | series = Series() | |
dd147eda | 760 | pst = PatchStream(series, is_log=True) |
6e87ae1c | 761 | for line in text.splitlines(): |
dd147eda SG |
762 | pst.process_line(line) |
763 | pst.finalise() | |
6e87ae1c SG |
764 | return series |
765 | ||
dd147eda | 766 | def fix_patch(backup_dir, fname, series, cmt): |
0d24de9d SG |
767 | """Fix up a patch file, by adding/removing as required. |
768 | ||
769 | We remove our tags from the patch file, insert changes lists, etc. | |
770 | The patch file is processed in place, and overwritten. | |
771 | ||
772 | A backup file is put into backup_dir (if not None). | |
773 | ||
774 | Args: | |
1cb1c0fc SG |
775 | backup_dir (str): Path to directory to use to backup the file |
776 | fname (str): Filename to patch file to process | |
777 | series (Series): Series information about this patch set | |
778 | cmt (Commit): Commit object for this patch file | |
779 | ||
0d24de9d | 780 | Return: |
1cb1c0fc | 781 | list: A list of errors, each str, or [] if all ok. |
0d24de9d SG |
782 | """ |
783 | handle, tmpname = tempfile.mkstemp() | |
272cd85d SG |
784 | outfd = os.fdopen(handle, 'w', encoding='utf-8') |
785 | infd = open(fname, 'r', encoding='utf-8') | |
dd147eda SG |
786 | pst = PatchStream(series) |
787 | pst.commit = cmt | |
788 | pst.process_stream(infd, outfd) | |
0d24de9d SG |
789 | infd.close() |
790 | outfd.close() | |
791 | ||
792 | # Create a backup file if required | |
793 | if backup_dir: | |
794 | shutil.copy(fname, os.path.join(backup_dir, os.path.basename(fname))) | |
795 | shutil.move(tmpname, fname) | |
313ef5f8 | 796 | return cmt.warn |
0d24de9d | 797 | |
d93720e1 | 798 | def fix_patches(series, fnames): |
0d24de9d SG |
799 | """Fix up a list of patches identified by filenames |
800 | ||
801 | The patch files are processed in place, and overwritten. | |
802 | ||
803 | Args: | |
1cb1c0fc SG |
804 | series (Series): The Series object |
805 | fnames (:type: list of str): List of patch files to process | |
0d24de9d SG |
806 | """ |
807 | # Current workflow creates patches, so we shouldn't need a backup | |
808 | backup_dir = None #tempfile.mkdtemp('clean-patch') | |
809 | count = 0 | |
810 | for fname in fnames: | |
dd147eda SG |
811 | cmt = series.commits[count] |
812 | cmt.patch = fname | |
813 | cmt.count = count | |
814 | result = fix_patch(backup_dir, fname, series, cmt) | |
0d24de9d | 815 | if result: |
9994baad SG |
816 | print('%d warning%s for %s:' % |
817 | (len(result), 's' if len(result) > 1 else '', fname)) | |
0d24de9d | 818 | for warn in result: |
9994baad SG |
819 | print('\t%s' % warn) |
820 | print() | |
0d24de9d | 821 | count += 1 |
9994baad | 822 | print('Cleaned %d patch%s' % (count, 'es' if count > 1 else '')) |
0d24de9d | 823 | |
d93720e1 | 824 | def insert_cover_letter(fname, series, count): |
0d24de9d SG |
825 | """Inserts a cover letter with the required info into patch 0 |
826 | ||
827 | Args: | |
1cb1c0fc SG |
828 | fname (str): Input / output filename of the cover letter file |
829 | series (Series): Series object | |
830 | count (int): Number of patches in the series | |
0d24de9d | 831 | """ |
dd147eda SG |
832 | fil = open(fname, 'r') |
833 | lines = fil.readlines() | |
834 | fil.close() | |
0d24de9d | 835 | |
dd147eda | 836 | fil = open(fname, 'w') |
0d24de9d SG |
837 | text = series.cover |
838 | prefix = series.GetPatchPrefix() | |
839 | for line in lines: | |
840 | if line.startswith('Subject:'): | |
35ce2dc4 WJ |
841 | # if more than 10 or 100 patches, it should say 00/xx, 000/xxx, etc |
842 | zero_repeat = int(math.log10(count)) + 1 | |
843 | zero = '0' * zero_repeat | |
844 | line = 'Subject: [%s %s/%d] %s\n' % (prefix, zero, count, text[0]) | |
0d24de9d SG |
845 | |
846 | # Insert our cover letter | |
847 | elif line.startswith('*** BLURB HERE ***'): | |
848 | # First the blurb test | |
849 | line = '\n'.join(text[1:]) + '\n' | |
850 | if series.get('notes'): | |
851 | line += '\n'.join(series.notes) + '\n' | |
852 | ||
853 | # Now the change list | |
854 | out = series.MakeChangeLog(None) | |
855 | line += '\n' + '\n'.join(out) | |
dd147eda SG |
856 | fil.write(line) |
857 | fil.close() |