blob: c165bc18251d04ab18bf352aa851d577384f6078 [file] [log] [blame]
Tom Rini83d290c2018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass0d24de92012-01-14 15:12:45 +00002# Copyright (c) 2011 The Chromium OS Authors.
3#
Simon Glass0d24de92012-01-14 15:12:45 +00004
Simon Glassd06e55a2020-10-29 21:46:17 -06005"""Handles parsing a stream of commits/emails from 'git log' or other source"""
6
Simon Glass6b3252e2020-10-29 21:46:37 -06007import collections
Douglas Anderson833e4192019-09-27 09:23:56 -07008import datetime
Simon Glass74570512020-10-29 21:46:27 -06009import io
Wu, Josh35ce2dc2015-04-03 10:51:17 +080010import math
Simon Glass0d24de92012-01-14 15:12:45 +000011import os
12import re
Simon Glass6b3252e2020-10-29 21:46:37 -060013import queue
Simon Glass0d24de92012-01-14 15:12:45 +000014import shutil
15import tempfile
16
Simon Glassbf776672020-04-17 18:09:04 -060017from patman import command
18from patman import commit
19from patman import gitutil
20from patman.series import Series
Simon Glass0d24de92012-01-14 15:12:45 +000021
22# Tags that we detect and remove
Simon Glass57699042020-10-29 21:46:18 -060023RE_REMOVE = re.compile(r'^BUG=|^TEST=|^BRANCH=|^Review URL:'
Simon Glassd06e55a2020-10-29 21:46:17 -060024 r'|Reviewed-on:|Commit-\w*:')
Simon Glass0d24de92012-01-14 15:12:45 +000025
26# Lines which are allowed after a TEST= line
Simon Glass57699042020-10-29 21:46:18 -060027RE_ALLOWED_AFTER_TEST = re.compile('^Signed-off-by:')
Simon Glass0d24de92012-01-14 15:12:45 +000028
Ilya Yanok05e5b732012-08-06 23:46:05 +000029# Signoffs
Simon Glass57699042020-10-29 21:46:18 -060030RE_SIGNOFF = re.compile('^Signed-off-by: *(.*)')
Ilya Yanok05e5b732012-08-06 23:46:05 +000031
Sean Anderson6949f702020-05-04 16:28:34 -040032# Cover letter tag
Simon Glass57699042020-10-29 21:46:18 -060033RE_COVER = re.compile('^Cover-([a-z-]*): *(.*)')
Simon Glassfe2f8d92013-03-20 16:43:00 +000034
Simon Glass0d24de92012-01-14 15:12:45 +000035# Patch series tag
Simon Glass57699042020-10-29 21:46:18 -060036RE_SERIES_TAG = re.compile('^Series-([a-z-]*): *(.*)')
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +010037
Douglas Anderson833e4192019-09-27 09:23:56 -070038# Change-Id will be used to generate the Message-Id and then be stripped
Simon Glass57699042020-10-29 21:46:18 -060039RE_CHANGE_ID = re.compile('^Change-Id: *(.*)')
Douglas Anderson833e4192019-09-27 09:23:56 -070040
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +010041# Commit series tag
Simon Glass57699042020-10-29 21:46:18 -060042RE_COMMIT_TAG = re.compile('^Commit-([a-z-]*): *(.*)')
Simon Glass0d24de92012-01-14 15:12:45 +000043
44# Commit tags that we want to collect and keep
Simon Glass57699042020-10-29 21:46:18 -060045RE_TAG = re.compile('^(Tested-by|Acked-by|Reviewed-by|Patch-cc|Fixes): (.*)')
Simon Glass0d24de92012-01-14 15:12:45 +000046
47# The start of a new commit in the git log
Simon Glass57699042020-10-29 21:46:18 -060048RE_COMMIT = re.compile('^commit ([0-9a-f]*)$')
Simon Glass0d24de92012-01-14 15:12:45 +000049
50# We detect these since checkpatch doesn't always do it
Simon Glass57699042020-10-29 21:46:18 -060051RE_SPACE_BEFORE_TAB = re.compile('^[+].* \t')
Simon Glass0d24de92012-01-14 15:12:45 +000052
Sean Anderson0411fff2020-05-04 16:28:35 -040053# Match indented lines for changes
Simon Glass57699042020-10-29 21:46:18 -060054RE_LEADING_WHITESPACE = re.compile(r'^\s')
Sean Anderson0411fff2020-05-04 16:28:35 -040055
Simon Glass6b3252e2020-10-29 21:46:37 -060056# Detect a 'diff' line
57RE_DIFF = re.compile(r'^>.*diff --git a/(.*) b/(.*)$')
58
59# Detect a context line, like '> @@ -153,8 +153,13 @@ CheckPatch
60RE_LINE = re.compile(r'>.*@@ \-(\d+),\d+ \+(\d+),\d+ @@ *(.*)')
61
Simon Glass0d24de92012-01-14 15:12:45 +000062# States we can be in - can we use range() and still have comments?
63STATE_MSG_HEADER = 0 # Still in the message header
64STATE_PATCH_SUBJECT = 1 # In patch subject (first line of log for a commit)
65STATE_PATCH_HEADER = 2 # In patch header (after the subject)
66STATE_DIFFS = 3 # In the diff part (past --- line)
67
68class PatchStream:
69 """Class for detecting/injecting tags in a patch or series of patches
70
71 We support processing the output of 'git log' to read out the tags we
72 are interested in. We can also process a patch file in order to remove
73 unwanted tags or inject additional ones. These correspond to the two
74 phases of processing.
75 """
Simon Glasse3a816b2020-10-29 21:46:21 -060076 def __init__(self, series, is_log=False):
Simon Glass0d24de92012-01-14 15:12:45 +000077 self.skip_blank = False # True to skip a single blank line
78 self.found_test = False # Found a TEST= line
Sean Anderson6949f702020-05-04 16:28:34 -040079 self.lines_after_test = 0 # Number of lines found after TEST=
Simon Glass0d24de92012-01-14 15:12:45 +000080 self.linenum = 1 # Output line number we are up to
81 self.in_section = None # Name of start...END section we are in
82 self.notes = [] # Series notes
83 self.section = [] # The current section...END section
84 self.series = series # Info about the patch series
85 self.is_log = is_log # True if indent like git log
Sean Anderson6949f702020-05-04 16:28:34 -040086 self.in_change = None # Name of the change list we are in
87 self.change_version = 0 # Non-zero if we are in a change list
Sean Anderson0411fff2020-05-04 16:28:35 -040088 self.change_lines = [] # Lines of the current change
Simon Glass0d24de92012-01-14 15:12:45 +000089 self.blank_count = 0 # Number of blank lines stored up
90 self.state = STATE_MSG_HEADER # What state are we in?
Simon Glass0d24de92012-01-14 15:12:45 +000091 self.commit = None # Current commit
Simon Glass6b3252e2020-10-29 21:46:37 -060092 self.snippets = [] # List of unquoted test blocks
93 self.cur_diff = None # Last 'diff' line seen (str)
94 self.cur_line = None # Last context (@@) line seen (str)
95 self.recent_diff= None # 'diff' line for current snippet (str)
96 self.recent_line= None # '@@' line for current snippet (str)
97 self.recent_quoted = collections.deque([], 5)
98 self.recent_unquoted = queue.Queue()
99 self.was_quoted = None
Simon Glass0d24de92012-01-14 15:12:45 +0000100
Simon Glass74570512020-10-29 21:46:27 -0600101 @staticmethod
102 def process_text(text, is_comment=False):
103 """Process some text through this class using a default Commit/Series
104
105 Args:
106 text (str): Text to parse
107 is_comment (bool): True if this is a comment rather than a patch.
108 If True, PatchStream doesn't expect a patch subject at the
109 start, but jumps straight into the body
110
111 Returns:
112 PatchStream: object with results
113 """
114 pstrm = PatchStream(Series())
115 pstrm.commit = commit.Commit(None)
116 infd = io.StringIO(text)
117 outfd = io.StringIO()
118 if is_comment:
119 pstrm.state = STATE_PATCH_HEADER
120 pstrm.process_stream(infd, outfd)
121 return pstrm
122
Simon Glassb5cc3992020-10-29 21:46:23 -0600123 def _add_warn(self, warn):
Simon Glass313ef5f2020-10-29 21:46:24 -0600124 """Add a new warning to report to the user about the current commit
125
126 The new warning is added to the current commit if not already present.
Simon Glassb5cc3992020-10-29 21:46:23 -0600127
128 Args:
129 warn (str): Warning to report
Simon Glass313ef5f2020-10-29 21:46:24 -0600130
131 Raises:
132 ValueError: Warning is generated with no commit associated
Simon Glassb5cc3992020-10-29 21:46:23 -0600133 """
Simon Glass313ef5f2020-10-29 21:46:24 -0600134 if not self.commit:
135 raise ValueError('Warning outside commit: %s' % warn)
136 if warn not in self.commit.warn:
137 self.commit.warn.append(warn)
Simon Glassb5cc3992020-10-29 21:46:23 -0600138
Simon Glassd93720e2020-10-29 21:46:19 -0600139 def _add_to_series(self, line, name, value):
Simon Glass0d24de92012-01-14 15:12:45 +0000140 """Add a new Series-xxx tag.
141
142 When a Series-xxx tag is detected, we come here to record it, if we
143 are scanning a 'git log'.
144
145 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600146 line (str): Source line containing tag (useful for debug/error
147 messages)
148 name (str): Tag name (part after 'Series-')
149 value (str): Tag value (part after 'Series-xxx: ')
Simon Glass0d24de92012-01-14 15:12:45 +0000150 """
151 if name == 'notes':
152 self.in_section = name
153 self.skip_blank = False
154 if self.is_log:
Simon Glassdffa42c2020-10-29 21:46:25 -0600155 warn = self.series.AddTag(self.commit, line, name, value)
156 if warn:
157 self.commit.warn.append(warn)
Simon Glass0d24de92012-01-14 15:12:45 +0000158
Simon Glasse3a816b2020-10-29 21:46:21 -0600159 def _add_to_commit(self, name):
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100160 """Add a new Commit-xxx tag.
161
162 When a Commit-xxx tag is detected, we come here to record it.
163
164 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600165 name (str): Tag name (part after 'Commit-')
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100166 """
167 if name == 'notes':
168 self.in_section = 'commit-' + name
169 self.skip_blank = False
170
Simon Glassd93720e2020-10-29 21:46:19 -0600171 def _add_commit_rtag(self, rtag_type, who):
Simon Glass7207e2b2020-07-05 21:41:57 -0600172 """Add a response tag to the current commit
173
174 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600175 rtag_type (str): rtag type (e.g. 'Reviewed-by')
176 who (str): Person who gave that rtag, e.g.
177 'Fred Bloggs <fred@bloggs.org>'
Simon Glass7207e2b2020-07-05 21:41:57 -0600178 """
179 self.commit.AddRtag(rtag_type, who)
180
Simon Glassd93720e2020-10-29 21:46:19 -0600181 def _close_commit(self):
Simon Glass0d24de92012-01-14 15:12:45 +0000182 """Save the current commit into our commit list, and reset our state"""
183 if self.commit and self.is_log:
184 self.series.AddCommit(self.commit)
185 self.commit = None
Bin Meng0d577182016-06-26 23:24:30 -0700186 # If 'END' is missing in a 'Cover-letter' section, and that section
187 # happens to show up at the very end of the commit message, this is
188 # the chance for us to fix it up.
189 if self.in_section == 'cover' and self.is_log:
190 self.series.cover = self.section
191 self.in_section = None
192 self.skip_blank = True
193 self.section = []
Simon Glass0d24de92012-01-14 15:12:45 +0000194
Simon Glass6b3252e2020-10-29 21:46:37 -0600195 self.cur_diff = None
196 self.recent_diff = None
197 self.recent_line = None
198
Simon Glassd93720e2020-10-29 21:46:19 -0600199 def _parse_version(self, value, line):
Sean Anderson6949f702020-05-04 16:28:34 -0400200 """Parse a version from a *-changes tag
201
202 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600203 value (str): Tag value (part after 'xxx-changes: '
204 line (str): Source line containing tag
Sean Anderson6949f702020-05-04 16:28:34 -0400205
206 Returns:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600207 int: The version as an integer
208
209 Raises:
210 ValueError: the value cannot be converted
Sean Anderson6949f702020-05-04 16:28:34 -0400211 """
212 try:
213 return int(value)
Simon Glassdd147ed2020-10-29 21:46:20 -0600214 except ValueError:
Sean Anderson6949f702020-05-04 16:28:34 -0400215 raise ValueError("%s: Cannot decode version info '%s'" %
Simon Glassd06e55a2020-10-29 21:46:17 -0600216 (self.commit.hash, line))
Sean Anderson6949f702020-05-04 16:28:34 -0400217
Simon Glassd93720e2020-10-29 21:46:19 -0600218 def _finalise_change(self):
219 """_finalise a (multi-line) change and add it to the series or commit"""
Sean Anderson0411fff2020-05-04 16:28:35 -0400220 if not self.change_lines:
221 return
222 change = '\n'.join(self.change_lines)
223
224 if self.in_change == 'Series':
225 self.series.AddChange(self.change_version, self.commit, change)
226 elif self.in_change == 'Cover':
227 self.series.AddChange(self.change_version, None, change)
228 elif self.in_change == 'Commit':
229 self.commit.AddChange(self.change_version, change)
230 self.change_lines = []
231
Simon Glass6b3252e2020-10-29 21:46:37 -0600232 def _finalise_snippet(self):
233 """Finish off a snippet and add it to the list
234
235 This is called when we get to the end of a snippet, i.e. the we enter
236 the next block of quoted text:
237
238 This is a comment from someone.
239
240 Something else
241
242 > Now we have some code <----- end of snippet
243 > more code
244
245 Now a comment about the above code
246
247 This adds the snippet to our list
248 """
249 quoted_lines = []
250 while self.recent_quoted:
251 quoted_lines.append(self.recent_quoted.popleft())
252 unquoted_lines = []
253 valid = False
254 while not self.recent_unquoted.empty():
255 text = self.recent_unquoted.get()
256 if not (text.startswith('On ') and text.endswith('wrote:')):
257 unquoted_lines.append(text)
258 if text:
259 valid = True
260 if valid:
261 lines = []
262 if self.recent_diff:
263 lines.append('> File: %s' % self.recent_diff)
264 if self.recent_line:
265 out = '> Line: %s / %s' % self.recent_line[:2]
266 if self.recent_line[2]:
267 out += ': %s' % self.recent_line[2]
268 lines.append(out)
269 lines += quoted_lines + unquoted_lines
270 if lines:
271 self.snippets.append(lines)
272
Simon Glassd93720e2020-10-29 21:46:19 -0600273 def process_line(self, line):
Simon Glass0d24de92012-01-14 15:12:45 +0000274 """Process a single line of a patch file or commit log
275
276 This process a line and returns a list of lines to output. The list
277 may be empty or may contain multiple output lines.
278
279 This is where all the complicated logic is located. The class's
280 state is used to move between different states and detect things
281 properly.
282
283 We can be in one of two modes:
284 self.is_log == True: This is 'git log' mode, where most output is
285 indented by 4 characters and we are scanning for tags
286
287 self.is_log == False: This is 'patch' mode, where we already have
288 all the tags, and are processing patches to remove junk we
289 don't want, and add things we think are required.
290
291 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600292 line (str): text line to process
Simon Glass0d24de92012-01-14 15:12:45 +0000293
294 Returns:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600295 list: list of output lines, or [] if nothing should be output
296
297 Raises:
298 ValueError: a fatal error occurred while parsing, e.g. an END
299 without a starting tag, or two commits with two change IDs
Simon Glass0d24de92012-01-14 15:12:45 +0000300 """
301 # Initially we have no output. Prepare the input line string
302 out = []
303 line = line.rstrip('\n')
Scott Wood4b89b812014-09-25 14:30:46 -0500304
Simon Glass57699042020-10-29 21:46:18 -0600305 commit_match = RE_COMMIT.match(line) if self.is_log else None
Scott Wood4b89b812014-09-25 14:30:46 -0500306
Simon Glass0d24de92012-01-14 15:12:45 +0000307 if self.is_log:
308 if line[:4] == ' ':
309 line = line[4:]
310
311 # Handle state transition and skipping blank lines
Simon Glass57699042020-10-29 21:46:18 -0600312 series_tag_match = RE_SERIES_TAG.match(line)
313 change_id_match = RE_CHANGE_ID.match(line)
314 commit_tag_match = RE_COMMIT_TAG.match(line)
315 cover_match = RE_COVER.match(line)
316 signoff_match = RE_SIGNOFF.match(line)
317 leading_whitespace_match = RE_LEADING_WHITESPACE.match(line)
Simon Glass6b3252e2020-10-29 21:46:37 -0600318 diff_match = RE_DIFF.match(line)
319 line_match = RE_LINE.match(line)
Simon Glass0d24de92012-01-14 15:12:45 +0000320 tag_match = None
321 if self.state == STATE_PATCH_HEADER:
Simon Glass57699042020-10-29 21:46:18 -0600322 tag_match = RE_TAG.match(line)
Simon Glass0d24de92012-01-14 15:12:45 +0000323 is_blank = not line.strip()
324 if is_blank:
325 if (self.state == STATE_MSG_HEADER
326 or self.state == STATE_PATCH_SUBJECT):
327 self.state += 1
328
329 # We don't have a subject in the text stream of patch files
330 # It has its own line with a Subject: tag
331 if not self.is_log and self.state == STATE_PATCH_SUBJECT:
332 self.state += 1
333 elif commit_match:
334 self.state = STATE_MSG_HEADER
335
Bin Meng94fbd3e2016-06-26 23:24:32 -0700336 # If a tag is detected, or a new commit starts
Douglas Anderson833e4192019-09-27 09:23:56 -0700337 if series_tag_match or commit_tag_match or change_id_match or \
Sean Anderson6949f702020-05-04 16:28:34 -0400338 cover_match or signoff_match or self.state == STATE_MSG_HEADER:
Bin Meng57b6b192016-06-26 23:24:31 -0700339 # but we are already in a section, this means 'END' is missing
340 # for that section, fix it up.
Bin Meng13b98d92016-06-26 23:24:29 -0700341 if self.in_section:
Simon Glassb5cc3992020-10-29 21:46:23 -0600342 self._add_warn("Missing 'END' in section '%s'" % self.in_section)
Bin Meng13b98d92016-06-26 23:24:29 -0700343 if self.in_section == 'cover':
344 self.series.cover = self.section
345 elif self.in_section == 'notes':
346 if self.is_log:
347 self.series.notes += self.section
348 elif self.in_section == 'commit-notes':
349 if self.is_log:
350 self.commit.notes += self.section
351 else:
Simon Glassb5cc3992020-10-29 21:46:23 -0600352 # This should not happen
353 raise ValueError("Unknown section '%s'" % self.in_section)
Bin Meng13b98d92016-06-26 23:24:29 -0700354 self.in_section = None
355 self.skip_blank = True
356 self.section = []
Bin Meng57b6b192016-06-26 23:24:31 -0700357 # but we are already in a change list, that means a blank line
358 # is missing, fix it up.
359 if self.in_change:
Simon Glassb5cc3992020-10-29 21:46:23 -0600360 self._add_warn("Missing 'blank line' in section '%s-changes'" %
361 self.in_change)
Simon Glassd93720e2020-10-29 21:46:19 -0600362 self._finalise_change()
Sean Anderson6949f702020-05-04 16:28:34 -0400363 self.in_change = None
364 self.change_version = 0
Bin Meng13b98d92016-06-26 23:24:29 -0700365
Simon Glass0d24de92012-01-14 15:12:45 +0000366 # If we are in a section, keep collecting lines until we see END
367 if self.in_section:
368 if line == 'END':
369 if self.in_section == 'cover':
370 self.series.cover = self.section
371 elif self.in_section == 'notes':
372 if self.is_log:
373 self.series.notes += self.section
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100374 elif self.in_section == 'commit-notes':
375 if self.is_log:
376 self.commit.notes += self.section
Simon Glass0d24de92012-01-14 15:12:45 +0000377 else:
Simon Glassb5cc3992020-10-29 21:46:23 -0600378 # This should not happen
379 raise ValueError("Unknown section '%s'" % self.in_section)
Simon Glass0d24de92012-01-14 15:12:45 +0000380 self.in_section = None
381 self.skip_blank = True
382 self.section = []
383 else:
384 self.section.append(line)
385
Patrick Delaunay7058dd02020-07-02 19:08:24 +0200386 # If we are not in a section, it is an unexpected END
387 elif line == 'END':
Simon Glassd06e55a2020-10-29 21:46:17 -0600388 raise ValueError("'END' wihout section")
Patrick Delaunay7058dd02020-07-02 19:08:24 +0200389
Simon Glass0d24de92012-01-14 15:12:45 +0000390 # Detect the commit subject
391 elif not is_blank and self.state == STATE_PATCH_SUBJECT:
392 self.commit.subject = line
393
394 # Detect the tags we want to remove, and skip blank lines
Simon Glass57699042020-10-29 21:46:18 -0600395 elif RE_REMOVE.match(line) and not commit_tag_match:
Simon Glass0d24de92012-01-14 15:12:45 +0000396 self.skip_blank = True
397
398 # TEST= should be the last thing in the commit, so remove
399 # everything after it
400 if line.startswith('TEST='):
401 self.found_test = True
402 elif self.skip_blank and is_blank:
403 self.skip_blank = False
404
Sean Anderson6949f702020-05-04 16:28:34 -0400405 # Detect Cover-xxx tags
Bin Menge7df2182016-06-26 23:24:28 -0700406 elif cover_match:
Sean Anderson6949f702020-05-04 16:28:34 -0400407 name = cover_match.group(1)
408 value = cover_match.group(2)
409 if name == 'letter':
410 self.in_section = 'cover'
411 self.skip_blank = False
412 elif name == 'letter-cc':
Simon Glassd93720e2020-10-29 21:46:19 -0600413 self._add_to_series(line, 'cover-cc', value)
Sean Anderson6949f702020-05-04 16:28:34 -0400414 elif name == 'changes':
415 self.in_change = 'Cover'
Simon Glassd93720e2020-10-29 21:46:19 -0600416 self.change_version = self._parse_version(value, line)
Simon Glassfe2f8d92013-03-20 16:43:00 +0000417
Simon Glass0d24de92012-01-14 15:12:45 +0000418 # If we are in a change list, key collected lines until a blank one
419 elif self.in_change:
420 if is_blank:
421 # Blank line ends this change list
Simon Glassd93720e2020-10-29 21:46:19 -0600422 self._finalise_change()
Sean Anderson6949f702020-05-04 16:28:34 -0400423 self.in_change = None
424 self.change_version = 0
Simon Glass102061b2014-04-20 10:50:14 -0600425 elif line == '---':
Simon Glassd93720e2020-10-29 21:46:19 -0600426 self._finalise_change()
Sean Anderson6949f702020-05-04 16:28:34 -0400427 self.in_change = None
428 self.change_version = 0
Simon Glassd93720e2020-10-29 21:46:19 -0600429 out = self.process_line(line)
Sean Anderson0411fff2020-05-04 16:28:35 -0400430 elif self.is_log:
431 if not leading_whitespace_match:
Simon Glassd93720e2020-10-29 21:46:19 -0600432 self._finalise_change()
Sean Anderson0411fff2020-05-04 16:28:35 -0400433 self.change_lines.append(line)
Simon Glass0d24de92012-01-14 15:12:45 +0000434 self.skip_blank = False
435
436 # Detect Series-xxx tags
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100437 elif series_tag_match:
438 name = series_tag_match.group(1)
439 value = series_tag_match.group(2)
Simon Glass0d24de92012-01-14 15:12:45 +0000440 if name == 'changes':
441 # value is the version number: e.g. 1, or 2
Sean Anderson6949f702020-05-04 16:28:34 -0400442 self.in_change = 'Series'
Simon Glassd93720e2020-10-29 21:46:19 -0600443 self.change_version = self._parse_version(value, line)
Simon Glass0d24de92012-01-14 15:12:45 +0000444 else:
Simon Glassd93720e2020-10-29 21:46:19 -0600445 self._add_to_series(line, name, value)
Simon Glass0d24de92012-01-14 15:12:45 +0000446 self.skip_blank = True
447
Douglas Anderson833e4192019-09-27 09:23:56 -0700448 # Detect Change-Id tags
449 elif change_id_match:
450 value = change_id_match.group(1)
451 if self.is_log:
452 if self.commit.change_id:
Simon Glassd06e55a2020-10-29 21:46:17 -0600453 raise ValueError(
454 "%s: Two Change-Ids: '%s' vs. '%s'" % self.commit.hash,
455 self.commit.change_id, value)
Douglas Anderson833e4192019-09-27 09:23:56 -0700456 self.commit.change_id = value
457 self.skip_blank = True
458
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100459 # Detect Commit-xxx tags
460 elif commit_tag_match:
461 name = commit_tag_match.group(1)
462 value = commit_tag_match.group(2)
463 if name == 'notes':
Simon Glasse3a816b2020-10-29 21:46:21 -0600464 self._add_to_commit(name)
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100465 self.skip_blank = True
Sean Anderson6949f702020-05-04 16:28:34 -0400466 elif name == 'changes':
467 self.in_change = 'Commit'
Simon Glassd93720e2020-10-29 21:46:19 -0600468 self.change_version = self._parse_version(value, line)
Patrick Delaunaye5ff9ab2020-07-02 19:52:54 +0200469 else:
Simon Glassb5cc3992020-10-29 21:46:23 -0600470 self._add_warn('Line %d: Ignoring Commit-%s' %
471 (self.linenum, name))
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100472
Simon Glass0d24de92012-01-14 15:12:45 +0000473 # Detect the start of a new commit
474 elif commit_match:
Simon Glassd93720e2020-10-29 21:46:19 -0600475 self._close_commit()
Simon Glass0b5b4092014-10-15 02:27:00 -0600476 self.commit = commit.Commit(commit_match.group(1))
Simon Glass0d24de92012-01-14 15:12:45 +0000477
478 # Detect tags in the commit message
479 elif tag_match:
Simon Glass7207e2b2020-07-05 21:41:57 -0600480 rtag_type, who = tag_match.groups()
Simon Glassd93720e2020-10-29 21:46:19 -0600481 self._add_commit_rtag(rtag_type, who)
Simon Glass0d24de92012-01-14 15:12:45 +0000482 # Remove Tested-by self, since few will take much notice
Simon Glass7207e2b2020-07-05 21:41:57 -0600483 if (rtag_type == 'Tested-by' and
484 who.find(os.getenv('USER') + '@') != -1):
Simon Glass4af99872020-10-29 21:46:28 -0600485 self._add_warn("Ignoring '%s'" % line)
Simon Glass7207e2b2020-07-05 21:41:57 -0600486 elif rtag_type == 'Patch-cc':
487 self.commit.AddCc(who.split(','))
Simon Glass0d24de92012-01-14 15:12:45 +0000488 else:
Simon Glassd0c57192014-08-28 09:43:38 -0600489 out = [line]
Simon Glass0d24de92012-01-14 15:12:45 +0000490
Simon Glass102061b2014-04-20 10:50:14 -0600491 # Suppress duplicate signoffs
492 elif signoff_match:
Simon Glasse752edc2014-08-28 09:43:35 -0600493 if (self.is_log or not self.commit or
Simon Glassd06e55a2020-10-29 21:46:17 -0600494 self.commit.CheckDuplicateSignoff(signoff_match.group(1))):
Simon Glass102061b2014-04-20 10:50:14 -0600495 out = [line]
496
Simon Glass0d24de92012-01-14 15:12:45 +0000497 # Well that means this is an ordinary line
498 else:
Simon Glass0d24de92012-01-14 15:12:45 +0000499 # Look for space before tab
Simon Glassdd147ed2020-10-29 21:46:20 -0600500 mat = RE_SPACE_BEFORE_TAB.match(line)
501 if mat:
Simon Glassb5cc3992020-10-29 21:46:23 -0600502 self._add_warn('Line %d/%d has space before tab' %
503 (self.linenum, mat.start()))
Simon Glass0d24de92012-01-14 15:12:45 +0000504
505 # OK, we have a valid non-blank line
506 out = [line]
507 self.linenum += 1
508 self.skip_blank = False
Simon Glass6b3252e2020-10-29 21:46:37 -0600509
510 if diff_match:
511 self.cur_diff = diff_match.group(1)
512
513 # If this is quoted, keep recent lines
514 if not diff_match and self.linenum > 1 and line:
515 if line.startswith('>'):
516 if not self.was_quoted:
517 self._finalise_snippet()
518 self.recent_line = None
519 if not line_match:
520 self.recent_quoted.append(line)
521 self.was_quoted = True
522 self.recent_diff = self.cur_diff
523 else:
524 self.recent_unquoted.put(line)
525 self.was_quoted = False
526
527 if line_match:
528 self.recent_line = line_match.groups()
529
Simon Glass0d24de92012-01-14 15:12:45 +0000530 if self.state == STATE_DIFFS:
531 pass
532
533 # If this is the start of the diffs section, emit our tags and
534 # change log
535 elif line == '---':
536 self.state = STATE_DIFFS
537
Sean Anderson6949f702020-05-04 16:28:34 -0400538 # Output the tags (signoff first), then change list
Simon Glass0d24de92012-01-14 15:12:45 +0000539 out = []
Simon Glass0d24de92012-01-14 15:12:45 +0000540 log = self.series.MakeChangeLog(self.commit)
Simon Glasse752edc2014-08-28 09:43:35 -0600541 out += [line]
542 if self.commit:
543 out += self.commit.notes
544 out += [''] + log
Simon Glass0d24de92012-01-14 15:12:45 +0000545 elif self.found_test:
Simon Glass57699042020-10-29 21:46:18 -0600546 if not RE_ALLOWED_AFTER_TEST.match(line):
Simon Glass0d24de92012-01-14 15:12:45 +0000547 self.lines_after_test += 1
548
549 return out
550
Simon Glassd93720e2020-10-29 21:46:19 -0600551 def finalise(self):
Simon Glass0d24de92012-01-14 15:12:45 +0000552 """Close out processing of this patch stream"""
Simon Glass6b3252e2020-10-29 21:46:37 -0600553 self._finalise_snippet()
Simon Glassd93720e2020-10-29 21:46:19 -0600554 self._finalise_change()
555 self._close_commit()
Simon Glass0d24de92012-01-14 15:12:45 +0000556 if self.lines_after_test:
Simon Glassb5cc3992020-10-29 21:46:23 -0600557 self._add_warn('Found %d lines after TEST=' % self.lines_after_test)
Simon Glass0d24de92012-01-14 15:12:45 +0000558
Simon Glassd93720e2020-10-29 21:46:19 -0600559 def _write_message_id(self, outfd):
Douglas Anderson833e4192019-09-27 09:23:56 -0700560 """Write the Message-Id into the output.
561
562 This is based on the Change-Id in the original patch, the version,
563 and the prefix.
564
565 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600566 outfd (io.IOBase): Output stream file object
Douglas Anderson833e4192019-09-27 09:23:56 -0700567 """
568 if not self.commit.change_id:
569 return
570
571 # If the count is -1 we're testing, so use a fixed time
572 if self.commit.count == -1:
573 time_now = datetime.datetime(1999, 12, 31, 23, 59, 59)
574 else:
575 time_now = datetime.datetime.now()
576
577 # In theory there is email.utils.make_msgid() which would be nice
578 # to use, but it already produces something way too long and thus
579 # will produce ugly commit lines if someone throws this into
580 # a "Link:" tag in the final commit. So (sigh) roll our own.
581
582 # Start with the time; presumably we wouldn't send the same series
583 # with the same Change-Id at the exact same second.
584 parts = [time_now.strftime("%Y%m%d%H%M%S")]
585
586 # These seem like they would be nice to include.
587 if 'prefix' in self.series:
588 parts.append(self.series['prefix'])
589 if 'version' in self.series:
590 parts.append("v%s" % self.series['version'])
591
592 parts.append(str(self.commit.count + 1))
593
594 # The Change-Id must be last, right before the @
595 parts.append(self.commit.change_id)
596
597 # Join parts together with "." and write it out.
598 outfd.write('Message-Id: <%s@changeid>\n' % '.'.join(parts))
599
Simon Glassd93720e2020-10-29 21:46:19 -0600600 def process_stream(self, infd, outfd):
Simon Glass0d24de92012-01-14 15:12:45 +0000601 """Copy a stream from infd to outfd, filtering out unwanting things.
602
603 This is used to process patch files one at a time.
604
605 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600606 infd (io.IOBase): Input stream file object
607 outfd (io.IOBase): Output stream file object
Simon Glass0d24de92012-01-14 15:12:45 +0000608 """
609 # Extract the filename from each diff, for nice warnings
610 fname = None
611 last_fname = None
612 re_fname = re.compile('diff --git a/(.*) b/.*')
Douglas Anderson833e4192019-09-27 09:23:56 -0700613
Simon Glassd93720e2020-10-29 21:46:19 -0600614 self._write_message_id(outfd)
Douglas Anderson833e4192019-09-27 09:23:56 -0700615
Simon Glass0d24de92012-01-14 15:12:45 +0000616 while True:
617 line = infd.readline()
618 if not line:
619 break
Simon Glassd93720e2020-10-29 21:46:19 -0600620 out = self.process_line(line)
Simon Glass0d24de92012-01-14 15:12:45 +0000621
622 # Try to detect blank lines at EOF
623 for line in out:
624 match = re_fname.match(line)
625 if match:
626 last_fname = fname
627 fname = match.group(1)
628 if line == '+':
629 self.blank_count += 1
630 else:
631 if self.blank_count and (line == '-- ' or match):
Simon Glassb5cc3992020-10-29 21:46:23 -0600632 self._add_warn("Found possible blank line(s) at end of file '%s'" %
633 last_fname)
Simon Glass0d24de92012-01-14 15:12:45 +0000634 outfd.write('+\n' * self.blank_count)
635 outfd.write(line + '\n')
636 self.blank_count = 0
Simon Glassd93720e2020-10-29 21:46:19 -0600637 self.finalise()
Simon Glass0d24de92012-01-14 15:12:45 +0000638
Simon Glass8f9ba3a2020-10-29 21:46:36 -0600639def insert_tags(msg, tags_to_emit):
640 """Add extra tags to a commit message
641
642 The tags are added after an existing block of tags if found, otherwise at
643 the end.
644
645 Args:
646 msg (str): Commit message
647 tags_to_emit (list): List of tags to emit, each a str
648
649 Returns:
650 (str) new message
651 """
652 out = []
653 done = False
654 emit_tags = False
655 for line in msg.splitlines():
656 if not done:
657 signoff_match = RE_SIGNOFF.match(line)
658 tag_match = RE_TAG.match(line)
659 if tag_match or signoff_match:
660 emit_tags = True
661 if emit_tags and not tag_match and not signoff_match:
662 out += tags_to_emit
663 emit_tags = False
664 done = True
665 out.append(line)
666 if not done:
667 out.append('')
668 out += tags_to_emit
669 return '\n'.join(out)
670
671def get_list(commit_range, git_dir=None, count=None):
672 """Get a log of a list of comments
673
674 This returns the output of 'git log' for the selected commits
675
676 Args:
677 commit_range (str): Range of commits to count (e.g. 'HEAD..base')
678 git_dir (str): Path to git repositiory (None to use default)
679 count (int): Number of commits to list, or None for no limit
680
681 Returns
682 str: String containing the contents of the git log
683 """
684 params = gitutil.LogCmd(commit_range, reverse=True, count=count,
685 git_dir=git_dir)
686 return command.RunPipe([params], capture=True).stdout
Simon Glass0d24de92012-01-14 15:12:45 +0000687
Simon Glassd93720e2020-10-29 21:46:19 -0600688def get_metadata_for_list(commit_range, git_dir=None, count=None,
689 series=None, allow_overwrite=False):
Simon Glasse62f9052012-12-15 10:42:06 +0000690 """Reads out patch series metadata from the commits
691
692 This does a 'git log' on the relevant commits and pulls out the tags we
693 are interested in.
694
695 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600696 commit_range (str): Range of commits to count (e.g. 'HEAD..base')
697 git_dir (str): Path to git repositiory (None to use default)
698 count (int): Number of commits to list, or None for no limit
699 series (Series): Object to add information into. By default a new series
Simon Glasse62f9052012-12-15 10:42:06 +0000700 is started.
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600701 allow_overwrite (bool): Allow tags to overwrite an existing tag
702
Simon Glasse62f9052012-12-15 10:42:06 +0000703 Returns:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600704 Series: Object containing information about the commits.
Simon Glasse62f9052012-12-15 10:42:06 +0000705 """
Simon Glass891b7a02014-09-05 19:00:19 -0600706 if not series:
707 series = Series()
Simon Glass950a2312014-09-05 19:00:23 -0600708 series.allow_overwrite = allow_overwrite
Simon Glass8f9ba3a2020-10-29 21:46:36 -0600709 stdout = get_list(commit_range, git_dir, count)
Simon Glassdd147ed2020-10-29 21:46:20 -0600710 pst = PatchStream(series, is_log=True)
Simon Glasse62f9052012-12-15 10:42:06 +0000711 for line in stdout.splitlines():
Simon Glassdd147ed2020-10-29 21:46:20 -0600712 pst.process_line(line)
713 pst.finalise()
Simon Glasse62f9052012-12-15 10:42:06 +0000714 return series
715
Simon Glassd93720e2020-10-29 21:46:19 -0600716def get_metadata(branch, start, count):
Simon Glass0d24de92012-01-14 15:12:45 +0000717 """Reads out patch series metadata from the commits
718
719 This does a 'git log' on the relevant commits and pulls out the tags we
720 are interested in.
721
722 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600723 branch (str): Branch to use (None for current branch)
724 start (int): Commit to start from: 0=branch HEAD, 1=next one, etc.
725 count (int): Number of commits to list
726
727 Returns:
728 Series: Object containing information about the commits.
Simon Glass0d24de92012-01-14 15:12:45 +0000729 """
Simon Glassd93720e2020-10-29 21:46:19 -0600730 return get_metadata_for_list(
731 '%s~%d' % (branch if branch else 'HEAD', start), None, count)
Simon Glass0d24de92012-01-14 15:12:45 +0000732
Simon Glassd93720e2020-10-29 21:46:19 -0600733def get_metadata_for_test(text):
Simon Glass6e87ae12017-05-29 15:31:31 -0600734 """Process metadata from a file containing a git log. Used for tests
735
736 Args:
737 text:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600738
739 Returns:
740 Series: Object containing information about the commits.
Simon Glass6e87ae12017-05-29 15:31:31 -0600741 """
742 series = Series()
Simon Glassdd147ed2020-10-29 21:46:20 -0600743 pst = PatchStream(series, is_log=True)
Simon Glass6e87ae12017-05-29 15:31:31 -0600744 for line in text.splitlines():
Simon Glassdd147ed2020-10-29 21:46:20 -0600745 pst.process_line(line)
746 pst.finalise()
Simon Glass6e87ae12017-05-29 15:31:31 -0600747 return series
748
Simon Glassdd147ed2020-10-29 21:46:20 -0600749def fix_patch(backup_dir, fname, series, cmt):
Simon Glass0d24de92012-01-14 15:12:45 +0000750 """Fix up a patch file, by adding/removing as required.
751
752 We remove our tags from the patch file, insert changes lists, etc.
753 The patch file is processed in place, and overwritten.
754
755 A backup file is put into backup_dir (if not None).
756
757 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600758 backup_dir (str): Path to directory to use to backup the file
759 fname (str): Filename to patch file to process
760 series (Series): Series information about this patch set
761 cmt (Commit): Commit object for this patch file
762
Simon Glass0d24de92012-01-14 15:12:45 +0000763 Return:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600764 list: A list of errors, each str, or [] if all ok.
Simon Glass0d24de92012-01-14 15:12:45 +0000765 """
766 handle, tmpname = tempfile.mkstemp()
Simon Glass272cd852019-10-31 07:42:51 -0600767 outfd = os.fdopen(handle, 'w', encoding='utf-8')
768 infd = open(fname, 'r', encoding='utf-8')
Simon Glassdd147ed2020-10-29 21:46:20 -0600769 pst = PatchStream(series)
770 pst.commit = cmt
771 pst.process_stream(infd, outfd)
Simon Glass0d24de92012-01-14 15:12:45 +0000772 infd.close()
773 outfd.close()
774
775 # Create a backup file if required
776 if backup_dir:
777 shutil.copy(fname, os.path.join(backup_dir, os.path.basename(fname)))
778 shutil.move(tmpname, fname)
Simon Glass313ef5f2020-10-29 21:46:24 -0600779 return cmt.warn
Simon Glass0d24de92012-01-14 15:12:45 +0000780
Simon Glassd93720e2020-10-29 21:46:19 -0600781def fix_patches(series, fnames):
Simon Glass0d24de92012-01-14 15:12:45 +0000782 """Fix up a list of patches identified by filenames
783
784 The patch files are processed in place, and overwritten.
785
786 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600787 series (Series): The Series object
788 fnames (:type: list of str): List of patch files to process
Simon Glass0d24de92012-01-14 15:12:45 +0000789 """
790 # Current workflow creates patches, so we shouldn't need a backup
791 backup_dir = None #tempfile.mkdtemp('clean-patch')
792 count = 0
793 for fname in fnames:
Simon Glassdd147ed2020-10-29 21:46:20 -0600794 cmt = series.commits[count]
795 cmt.patch = fname
796 cmt.count = count
797 result = fix_patch(backup_dir, fname, series, cmt)
Simon Glass0d24de92012-01-14 15:12:45 +0000798 if result:
Simon Glass9994baa2020-10-29 21:46:30 -0600799 print('%d warning%s for %s:' %
800 (len(result), 's' if len(result) > 1 else '', fname))
Simon Glass0d24de92012-01-14 15:12:45 +0000801 for warn in result:
Simon Glass9994baa2020-10-29 21:46:30 -0600802 print('\t%s' % warn)
803 print()
Simon Glass0d24de92012-01-14 15:12:45 +0000804 count += 1
Simon Glass9994baa2020-10-29 21:46:30 -0600805 print('Cleaned %d patch%s' % (count, 'es' if count > 1 else ''))
Simon Glass0d24de92012-01-14 15:12:45 +0000806
Simon Glassd93720e2020-10-29 21:46:19 -0600807def insert_cover_letter(fname, series, count):
Simon Glass0d24de92012-01-14 15:12:45 +0000808 """Inserts a cover letter with the required info into patch 0
809
810 Args:
Simon Glass1cb1c0f2020-10-29 21:46:22 -0600811 fname (str): Input / output filename of the cover letter file
812 series (Series): Series object
813 count (int): Number of patches in the series
Simon Glass0d24de92012-01-14 15:12:45 +0000814 """
Simon Glassdd147ed2020-10-29 21:46:20 -0600815 fil = open(fname, 'r')
816 lines = fil.readlines()
817 fil.close()
Simon Glass0d24de92012-01-14 15:12:45 +0000818
Simon Glassdd147ed2020-10-29 21:46:20 -0600819 fil = open(fname, 'w')
Simon Glass0d24de92012-01-14 15:12:45 +0000820 text = series.cover
821 prefix = series.GetPatchPrefix()
822 for line in lines:
823 if line.startswith('Subject:'):
Wu, Josh35ce2dc2015-04-03 10:51:17 +0800824 # if more than 10 or 100 patches, it should say 00/xx, 000/xxx, etc
825 zero_repeat = int(math.log10(count)) + 1
826 zero = '0' * zero_repeat
827 line = 'Subject: [%s %s/%d] %s\n' % (prefix, zero, count, text[0])
Simon Glass0d24de92012-01-14 15:12:45 +0000828
829 # Insert our cover letter
830 elif line.startswith('*** BLURB HERE ***'):
831 # First the blurb test
832 line = '\n'.join(text[1:]) + '\n'
833 if series.get('notes'):
834 line += '\n'.join(series.notes) + '\n'
835
836 # Now the change list
837 out = series.MakeChangeLog(None)
838 line += '\n' + '\n'.join(out)
Simon Glassdd147ed2020-10-29 21:46:20 -0600839 fil.write(line)
840 fil.close()