blob: a09ae9c73714dbd068e5b6f8f99e93f2c787ae70 [file] [log] [blame]
Tom Rini10e47792018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass26132882012-01-14 15:12:45 +00002# Copyright (c) 2011 The Chromium OS Authors.
3#
Simon Glass26132882012-01-14 15:12:45 +00004
Simon Glassc42b1d02020-10-29 21:46:17 -06005"""Handles parsing a stream of commits/emails from 'git log' or other source"""
6
Simon Glassda8a2922020-10-29 21:46:37 -06007import collections
Douglas Anderson52b5ee82019-09-27 09:23:56 -07008import datetime
Simon Glass06202d62020-10-29 21:46:27 -06009import io
Wu, Josh393aaa22015-04-03 10:51:17 +080010import math
Simon Glass26132882012-01-14 15:12:45 +000011import os
12import re
Simon Glassda8a2922020-10-29 21:46:37 -060013import queue
Simon Glass26132882012-01-14 15:12:45 +000014import shutil
15import tempfile
16
Simon Glassa997ea52020-04-17 18:09:04 -060017from patman import commit
18from patman import gitutil
19from patman.series import Series
Simon Glass131444f2023-02-23 18:18:04 -070020from u_boot_pylib import command
Simon Glass26132882012-01-14 15:12:45 +000021
22# Tags that we detect and remove
Simon Glass06022262020-10-29 21:46:18 -060023RE_REMOVE = re.compile(r'^BUG=|^TEST=|^BRANCH=|^Review URL:'
Simon Glassc42b1d02020-10-29 21:46:17 -060024 r'|Reviewed-on:|Commit-\w*:')
Simon Glass26132882012-01-14 15:12:45 +000025
26# Lines which are allowed after a TEST= line
Simon Glass06022262020-10-29 21:46:18 -060027RE_ALLOWED_AFTER_TEST = re.compile('^Signed-off-by:')
Simon Glass26132882012-01-14 15:12:45 +000028
Ilya Yanok644c1842012-08-06 23:46:05 +000029# Signoffs
Simon Glass06022262020-10-29 21:46:18 -060030RE_SIGNOFF = re.compile('^Signed-off-by: *(.*)')
Ilya Yanok644c1842012-08-06 23:46:05 +000031
Sean Anderson48f46d62020-05-04 16:28:34 -040032# Cover letter tag
Simon Glass06022262020-10-29 21:46:18 -060033RE_COVER = re.compile('^Cover-([a-z-]*): *(.*)')
Simon Glassc72f3da2013-03-20 16:43:00 +000034
Simon Glass26132882012-01-14 15:12:45 +000035# Patch series tag
Simon Glass06022262020-10-29 21:46:18 -060036RE_SERIES_TAG = re.compile('^Series-([a-z-]*): *(.*)')
Albert ARIBAUDd880efd2013-11-12 11:14:41 +010037
Douglas Anderson52b5ee82019-09-27 09:23:56 -070038# Change-Id will be used to generate the Message-Id and then be stripped
Simon Glass06022262020-10-29 21:46:18 -060039RE_CHANGE_ID = re.compile('^Change-Id: *(.*)')
Douglas Anderson52b5ee82019-09-27 09:23:56 -070040
Albert ARIBAUDd880efd2013-11-12 11:14:41 +010041# Commit series tag
Simon Glass06022262020-10-29 21:46:18 -060042RE_COMMIT_TAG = re.compile('^Commit-([a-z-]*): *(.*)')
Simon Glass26132882012-01-14 15:12:45 +000043
44# Commit tags that we want to collect and keep
Simon Glass06022262020-10-29 21:46:18 -060045RE_TAG = re.compile('^(Tested-by|Acked-by|Reviewed-by|Patch-cc|Fixes): (.*)')
Simon Glass26132882012-01-14 15:12:45 +000046
47# The start of a new commit in the git log
Simon Glass06022262020-10-29 21:46:18 -060048RE_COMMIT = re.compile('^commit ([0-9a-f]*)$')
Simon Glass26132882012-01-14 15:12:45 +000049
50# We detect these since checkpatch doesn't always do it
Simon Glass06022262020-10-29 21:46:18 -060051RE_SPACE_BEFORE_TAB = re.compile('^[+].* \t')
Simon Glass26132882012-01-14 15:12:45 +000052
Sean Anderson1a32f922020-05-04 16:28:35 -040053# Match indented lines for changes
Simon Glass06022262020-10-29 21:46:18 -060054RE_LEADING_WHITESPACE = re.compile(r'^\s')
Sean Anderson1a32f922020-05-04 16:28:35 -040055
Simon Glassda8a2922020-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
Patrick Delaunay6bbdd0c2021-07-22 16:51:42 +020062# Detect line with invalid TAG
63RE_INV_TAG = re.compile('^Serie-([a-z-]*): *(.*)')
64
Simon Glass26132882012-01-14 15:12:45 +000065# States we can be in - can we use range() and still have comments?
66STATE_MSG_HEADER = 0 # Still in the message header
67STATE_PATCH_SUBJECT = 1 # In patch subject (first line of log for a commit)
68STATE_PATCH_HEADER = 2 # In patch header (after the subject)
69STATE_DIFFS = 3 # In the diff part (past --- line)
70
Maxim Cournoyer12f99fd2023-10-12 23:06:24 -040071
Simon Glass26132882012-01-14 15:12:45 +000072class PatchStream:
73 """Class for detecting/injecting tags in a patch or series of patches
74
75 We support processing the output of 'git log' to read out the tags we
76 are interested in. We can also process a patch file in order to remove
77 unwanted tags or inject additional ones. These correspond to the two
78 phases of processing.
79 """
Maxim Cournoyer12f99fd2023-10-12 23:06:24 -040080 def __init__(self, series, is_log=False, keep_change_id=False):
Simon Glass26132882012-01-14 15:12:45 +000081 self.skip_blank = False # True to skip a single blank line
82 self.found_test = False # Found a TEST= line
Sean Anderson48f46d62020-05-04 16:28:34 -040083 self.lines_after_test = 0 # Number of lines found after TEST=
Simon Glass26132882012-01-14 15:12:45 +000084 self.linenum = 1 # Output line number we are up to
85 self.in_section = None # Name of start...END section we are in
86 self.notes = [] # Series notes
87 self.section = [] # The current section...END section
88 self.series = series # Info about the patch series
89 self.is_log = is_log # True if indent like git log
Maxim Cournoyer12f99fd2023-10-12 23:06:24 -040090 self.keep_change_id = keep_change_id # True to keep Change-Id tags
Sean Anderson48f46d62020-05-04 16:28:34 -040091 self.in_change = None # Name of the change list we are in
92 self.change_version = 0 # Non-zero if we are in a change list
Sean Anderson1a32f922020-05-04 16:28:35 -040093 self.change_lines = [] # Lines of the current change
Simon Glass26132882012-01-14 15:12:45 +000094 self.blank_count = 0 # Number of blank lines stored up
95 self.state = STATE_MSG_HEADER # What state are we in?
Simon Glass26132882012-01-14 15:12:45 +000096 self.commit = None # Current commit
Simon Glass2112d072020-10-29 21:46:38 -060097 # List of unquoted test blocks, each a list of str lines
98 self.snippets = []
Simon Glassda8a2922020-10-29 21:46:37 -060099 self.cur_diff = None # Last 'diff' line seen (str)
100 self.cur_line = None # Last context (@@) line seen (str)
Simon Glass2112d072020-10-29 21:46:38 -0600101 self.recent_diff = None # 'diff' line for current snippet (str)
102 self.recent_line = None # '@@' line for current snippet (str)
Simon Glassda8a2922020-10-29 21:46:37 -0600103 self.recent_quoted = collections.deque([], 5)
104 self.recent_unquoted = queue.Queue()
105 self.was_quoted = None
Simon Glass26132882012-01-14 15:12:45 +0000106
Simon Glass06202d62020-10-29 21:46:27 -0600107 @staticmethod
108 def process_text(text, is_comment=False):
109 """Process some text through this class using a default Commit/Series
110
111 Args:
112 text (str): Text to parse
113 is_comment (bool): True if this is a comment rather than a patch.
114 If True, PatchStream doesn't expect a patch subject at the
115 start, but jumps straight into the body
116
117 Returns:
118 PatchStream: object with results
119 """
120 pstrm = PatchStream(Series())
121 pstrm.commit = commit.Commit(None)
122 infd = io.StringIO(text)
123 outfd = io.StringIO()
124 if is_comment:
125 pstrm.state = STATE_PATCH_HEADER
126 pstrm.process_stream(infd, outfd)
127 return pstrm
128
Simon Glassb5800392020-10-29 21:46:23 -0600129 def _add_warn(self, warn):
Simon Glass6d00f6c2020-10-29 21:46:24 -0600130 """Add a new warning to report to the user about the current commit
131
132 The new warning is added to the current commit if not already present.
Simon Glassb5800392020-10-29 21:46:23 -0600133
134 Args:
135 warn (str): Warning to report
Simon Glass6d00f6c2020-10-29 21:46:24 -0600136
137 Raises:
138 ValueError: Warning is generated with no commit associated
Simon Glassb5800392020-10-29 21:46:23 -0600139 """
Simon Glass6d00f6c2020-10-29 21:46:24 -0600140 if not self.commit:
Simon Glassb3106562021-03-22 18:01:42 +1300141 print('Warning outside commit: %s' % warn)
142 elif warn not in self.commit.warn:
Simon Glass6d00f6c2020-10-29 21:46:24 -0600143 self.commit.warn.append(warn)
Simon Glassb5800392020-10-29 21:46:23 -0600144
Simon Glass93f61c02020-10-29 21:46:19 -0600145 def _add_to_series(self, line, name, value):
Simon Glass26132882012-01-14 15:12:45 +0000146 """Add a new Series-xxx tag.
147
148 When a Series-xxx tag is detected, we come here to record it, if we
149 are scanning a 'git log'.
150
151 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600152 line (str): Source line containing tag (useful for debug/error
153 messages)
154 name (str): Tag name (part after 'Series-')
155 value (str): Tag value (part after 'Series-xxx: ')
Simon Glass26132882012-01-14 15:12:45 +0000156 """
157 if name == 'notes':
158 self.in_section = name
159 self.skip_blank = False
160 if self.is_log:
Simon Glass8093a8f2020-10-29 21:46:25 -0600161 warn = self.series.AddTag(self.commit, line, name, value)
162 if warn:
163 self.commit.warn.append(warn)
Simon Glass26132882012-01-14 15:12:45 +0000164
Simon Glass5f6caa52020-10-29 21:46:21 -0600165 def _add_to_commit(self, name):
Albert ARIBAUDd880efd2013-11-12 11:14:41 +0100166 """Add a new Commit-xxx tag.
167
168 When a Commit-xxx tag is detected, we come here to record it.
169
170 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600171 name (str): Tag name (part after 'Commit-')
Albert ARIBAUDd880efd2013-11-12 11:14:41 +0100172 """
173 if name == 'notes':
174 self.in_section = 'commit-' + name
175 self.skip_blank = False
176
Simon Glass93f61c02020-10-29 21:46:19 -0600177 def _add_commit_rtag(self, rtag_type, who):
Simon Glass08e91be2020-07-05 21:41:57 -0600178 """Add a response tag to the current commit
179
180 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600181 rtag_type (str): rtag type (e.g. 'Reviewed-by')
182 who (str): Person who gave that rtag, e.g.
183 'Fred Bloggs <fred@bloggs.org>'
Simon Glass08e91be2020-07-05 21:41:57 -0600184 """
Simon Glass530ac272022-01-29 14:14:07 -0700185 self.commit.add_rtag(rtag_type, who)
Simon Glass08e91be2020-07-05 21:41:57 -0600186
Simon Glass93f61c02020-10-29 21:46:19 -0600187 def _close_commit(self):
Simon Glass26132882012-01-14 15:12:45 +0000188 """Save the current commit into our commit list, and reset our state"""
189 if self.commit and self.is_log:
190 self.series.AddCommit(self.commit)
191 self.commit = None
Bin Mengd23121c2016-06-26 23:24:30 -0700192 # If 'END' is missing in a 'Cover-letter' section, and that section
193 # happens to show up at the very end of the commit message, this is
194 # the chance for us to fix it up.
195 if self.in_section == 'cover' and self.is_log:
196 self.series.cover = self.section
197 self.in_section = None
198 self.skip_blank = True
199 self.section = []
Simon Glass26132882012-01-14 15:12:45 +0000200
Simon Glassda8a2922020-10-29 21:46:37 -0600201 self.cur_diff = None
202 self.recent_diff = None
203 self.recent_line = None
204
Simon Glass93f61c02020-10-29 21:46:19 -0600205 def _parse_version(self, value, line):
Sean Anderson48f46d62020-05-04 16:28:34 -0400206 """Parse a version from a *-changes tag
207
208 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600209 value (str): Tag value (part after 'xxx-changes: '
210 line (str): Source line containing tag
Sean Anderson48f46d62020-05-04 16:28:34 -0400211
212 Returns:
Simon Glass06127f92020-10-29 21:46:22 -0600213 int: The version as an integer
214
215 Raises:
216 ValueError: the value cannot be converted
Sean Anderson48f46d62020-05-04 16:28:34 -0400217 """
218 try:
219 return int(value)
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600220 except ValueError:
Sean Anderson48f46d62020-05-04 16:28:34 -0400221 raise ValueError("%s: Cannot decode version info '%s'" %
Simon Glassc42b1d02020-10-29 21:46:17 -0600222 (self.commit.hash, line))
Sean Anderson48f46d62020-05-04 16:28:34 -0400223
Simon Glass93f61c02020-10-29 21:46:19 -0600224 def _finalise_change(self):
225 """_finalise a (multi-line) change and add it to the series or commit"""
Sean Anderson1a32f922020-05-04 16:28:35 -0400226 if not self.change_lines:
227 return
228 change = '\n'.join(self.change_lines)
229
230 if self.in_change == 'Series':
231 self.series.AddChange(self.change_version, self.commit, change)
232 elif self.in_change == 'Cover':
233 self.series.AddChange(self.change_version, None, change)
234 elif self.in_change == 'Commit':
Simon Glass530ac272022-01-29 14:14:07 -0700235 self.commit.add_change(self.change_version, change)
Sean Anderson1a32f922020-05-04 16:28:35 -0400236 self.change_lines = []
237
Simon Glassda8a2922020-10-29 21:46:37 -0600238 def _finalise_snippet(self):
239 """Finish off a snippet and add it to the list
240
241 This is called when we get to the end of a snippet, i.e. the we enter
242 the next block of quoted text:
243
244 This is a comment from someone.
245
246 Something else
247
248 > Now we have some code <----- end of snippet
249 > more code
250
251 Now a comment about the above code
252
253 This adds the snippet to our list
254 """
255 quoted_lines = []
256 while self.recent_quoted:
257 quoted_lines.append(self.recent_quoted.popleft())
258 unquoted_lines = []
259 valid = False
260 while not self.recent_unquoted.empty():
261 text = self.recent_unquoted.get()
262 if not (text.startswith('On ') and text.endswith('wrote:')):
263 unquoted_lines.append(text)
264 if text:
265 valid = True
266 if valid:
267 lines = []
268 if self.recent_diff:
269 lines.append('> File: %s' % self.recent_diff)
270 if self.recent_line:
271 out = '> Line: %s / %s' % self.recent_line[:2]
272 if self.recent_line[2]:
273 out += ': %s' % self.recent_line[2]
274 lines.append(out)
275 lines += quoted_lines + unquoted_lines
276 if lines:
277 self.snippets.append(lines)
278
Simon Glass93f61c02020-10-29 21:46:19 -0600279 def process_line(self, line):
Simon Glass26132882012-01-14 15:12:45 +0000280 """Process a single line of a patch file or commit log
281
282 This process a line and returns a list of lines to output. The list
283 may be empty or may contain multiple output lines.
284
285 This is where all the complicated logic is located. The class's
286 state is used to move between different states and detect things
287 properly.
288
289 We can be in one of two modes:
290 self.is_log == True: This is 'git log' mode, where most output is
291 indented by 4 characters and we are scanning for tags
292
293 self.is_log == False: This is 'patch' mode, where we already have
294 all the tags, and are processing patches to remove junk we
295 don't want, and add things we think are required.
296
297 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600298 line (str): text line to process
Simon Glass26132882012-01-14 15:12:45 +0000299
300 Returns:
Simon Glass06127f92020-10-29 21:46:22 -0600301 list: list of output lines, or [] if nothing should be output
302
303 Raises:
304 ValueError: a fatal error occurred while parsing, e.g. an END
305 without a starting tag, or two commits with two change IDs
Simon Glass26132882012-01-14 15:12:45 +0000306 """
307 # Initially we have no output. Prepare the input line string
308 out = []
309 line = line.rstrip('\n')
Scott Wood616a0ab2014-09-25 14:30:46 -0500310
Simon Glass06022262020-10-29 21:46:18 -0600311 commit_match = RE_COMMIT.match(line) if self.is_log else None
Scott Wood616a0ab2014-09-25 14:30:46 -0500312
Simon Glass26132882012-01-14 15:12:45 +0000313 if self.is_log:
314 if line[:4] == ' ':
315 line = line[4:]
316
317 # Handle state transition and skipping blank lines
Simon Glass06022262020-10-29 21:46:18 -0600318 series_tag_match = RE_SERIES_TAG.match(line)
319 change_id_match = RE_CHANGE_ID.match(line)
320 commit_tag_match = RE_COMMIT_TAG.match(line)
321 cover_match = RE_COVER.match(line)
322 signoff_match = RE_SIGNOFF.match(line)
323 leading_whitespace_match = RE_LEADING_WHITESPACE.match(line)
Simon Glassda8a2922020-10-29 21:46:37 -0600324 diff_match = RE_DIFF.match(line)
325 line_match = RE_LINE.match(line)
Patrick Delaunay6bbdd0c2021-07-22 16:51:42 +0200326 invalid_match = RE_INV_TAG.match(line)
Simon Glass26132882012-01-14 15:12:45 +0000327 tag_match = None
328 if self.state == STATE_PATCH_HEADER:
Simon Glass06022262020-10-29 21:46:18 -0600329 tag_match = RE_TAG.match(line)
Simon Glass26132882012-01-14 15:12:45 +0000330 is_blank = not line.strip()
331 if is_blank:
332 if (self.state == STATE_MSG_HEADER
333 or self.state == STATE_PATCH_SUBJECT):
334 self.state += 1
335
336 # We don't have a subject in the text stream of patch files
337 # It has its own line with a Subject: tag
338 if not self.is_log and self.state == STATE_PATCH_SUBJECT:
339 self.state += 1
340 elif commit_match:
341 self.state = STATE_MSG_HEADER
342
Bin Meng9228cc22016-06-26 23:24:32 -0700343 # If a tag is detected, or a new commit starts
Douglas Anderson52b5ee82019-09-27 09:23:56 -0700344 if series_tag_match or commit_tag_match or change_id_match or \
Sean Anderson48f46d62020-05-04 16:28:34 -0400345 cover_match or signoff_match or self.state == STATE_MSG_HEADER:
Bin Meng50ffc982016-06-26 23:24:31 -0700346 # but we are already in a section, this means 'END' is missing
347 # for that section, fix it up.
Bin Mengf83649f2016-06-26 23:24:29 -0700348 if self.in_section:
Simon Glassb5800392020-10-29 21:46:23 -0600349 self._add_warn("Missing 'END' in section '%s'" % self.in_section)
Bin Mengf83649f2016-06-26 23:24:29 -0700350 if self.in_section == 'cover':
351 self.series.cover = self.section
352 elif self.in_section == 'notes':
353 if self.is_log:
354 self.series.notes += self.section
355 elif self.in_section == 'commit-notes':
356 if self.is_log:
357 self.commit.notes += self.section
358 else:
Simon Glassb5800392020-10-29 21:46:23 -0600359 # This should not happen
360 raise ValueError("Unknown section '%s'" % self.in_section)
Bin Mengf83649f2016-06-26 23:24:29 -0700361 self.in_section = None
362 self.skip_blank = True
363 self.section = []
Bin Meng50ffc982016-06-26 23:24:31 -0700364 # but we are already in a change list, that means a blank line
365 # is missing, fix it up.
366 if self.in_change:
Simon Glassb5800392020-10-29 21:46:23 -0600367 self._add_warn("Missing 'blank line' in section '%s-changes'" %
368 self.in_change)
Simon Glass93f61c02020-10-29 21:46:19 -0600369 self._finalise_change()
Sean Anderson48f46d62020-05-04 16:28:34 -0400370 self.in_change = None
371 self.change_version = 0
Bin Mengf83649f2016-06-26 23:24:29 -0700372
Simon Glass26132882012-01-14 15:12:45 +0000373 # If we are in a section, keep collecting lines until we see END
374 if self.in_section:
375 if line == 'END':
376 if self.in_section == 'cover':
377 self.series.cover = self.section
378 elif self.in_section == 'notes':
379 if self.is_log:
380 self.series.notes += self.section
Albert ARIBAUDd880efd2013-11-12 11:14:41 +0100381 elif self.in_section == 'commit-notes':
382 if self.is_log:
383 self.commit.notes += self.section
Simon Glass26132882012-01-14 15:12:45 +0000384 else:
Simon Glassb5800392020-10-29 21:46:23 -0600385 # This should not happen
386 raise ValueError("Unknown section '%s'" % self.in_section)
Simon Glass26132882012-01-14 15:12:45 +0000387 self.in_section = None
388 self.skip_blank = True
389 self.section = []
390 else:
391 self.section.append(line)
392
Patrick Delaunay2870c1c2020-07-02 19:08:24 +0200393 # If we are not in a section, it is an unexpected END
394 elif line == 'END':
Simon Glassc42b1d02020-10-29 21:46:17 -0600395 raise ValueError("'END' wihout section")
Patrick Delaunay2870c1c2020-07-02 19:08:24 +0200396
Simon Glass26132882012-01-14 15:12:45 +0000397 # Detect the commit subject
398 elif not is_blank and self.state == STATE_PATCH_SUBJECT:
399 self.commit.subject = line
400
401 # Detect the tags we want to remove, and skip blank lines
Simon Glass06022262020-10-29 21:46:18 -0600402 elif RE_REMOVE.match(line) and not commit_tag_match:
Simon Glass26132882012-01-14 15:12:45 +0000403 self.skip_blank = True
404
405 # TEST= should be the last thing in the commit, so remove
406 # everything after it
407 if line.startswith('TEST='):
408 self.found_test = True
409 elif self.skip_blank and is_blank:
410 self.skip_blank = False
411
Sean Anderson48f46d62020-05-04 16:28:34 -0400412 # Detect Cover-xxx tags
Bin Mengd0bc03c2016-06-26 23:24:28 -0700413 elif cover_match:
Sean Anderson48f46d62020-05-04 16:28:34 -0400414 name = cover_match.group(1)
415 value = cover_match.group(2)
416 if name == 'letter':
417 self.in_section = 'cover'
418 self.skip_blank = False
419 elif name == 'letter-cc':
Simon Glass93f61c02020-10-29 21:46:19 -0600420 self._add_to_series(line, 'cover-cc', value)
Sean Anderson48f46d62020-05-04 16:28:34 -0400421 elif name == 'changes':
422 self.in_change = 'Cover'
Simon Glass93f61c02020-10-29 21:46:19 -0600423 self.change_version = self._parse_version(value, line)
Simon Glassc72f3da2013-03-20 16:43:00 +0000424
Simon Glass26132882012-01-14 15:12:45 +0000425 # If we are in a change list, key collected lines until a blank one
426 elif self.in_change:
427 if is_blank:
428 # Blank line ends this change list
Simon Glass93f61c02020-10-29 21:46:19 -0600429 self._finalise_change()
Sean Anderson48f46d62020-05-04 16:28:34 -0400430 self.in_change = None
431 self.change_version = 0
Simon Glass46b34212014-04-20 10:50:14 -0600432 elif line == '---':
Simon Glass93f61c02020-10-29 21:46:19 -0600433 self._finalise_change()
Sean Anderson48f46d62020-05-04 16:28:34 -0400434 self.in_change = None
435 self.change_version = 0
Simon Glass93f61c02020-10-29 21:46:19 -0600436 out = self.process_line(line)
Sean Anderson1a32f922020-05-04 16:28:35 -0400437 elif self.is_log:
438 if not leading_whitespace_match:
Simon Glass93f61c02020-10-29 21:46:19 -0600439 self._finalise_change()
Sean Anderson1a32f922020-05-04 16:28:35 -0400440 self.change_lines.append(line)
Simon Glass26132882012-01-14 15:12:45 +0000441 self.skip_blank = False
442
443 # Detect Series-xxx tags
Albert ARIBAUDd880efd2013-11-12 11:14:41 +0100444 elif series_tag_match:
445 name = series_tag_match.group(1)
446 value = series_tag_match.group(2)
Simon Glass26132882012-01-14 15:12:45 +0000447 if name == 'changes':
448 # value is the version number: e.g. 1, or 2
Sean Anderson48f46d62020-05-04 16:28:34 -0400449 self.in_change = 'Series'
Simon Glass93f61c02020-10-29 21:46:19 -0600450 self.change_version = self._parse_version(value, line)
Simon Glass26132882012-01-14 15:12:45 +0000451 else:
Simon Glass93f61c02020-10-29 21:46:19 -0600452 self._add_to_series(line, name, value)
Simon Glass26132882012-01-14 15:12:45 +0000453 self.skip_blank = True
454
Douglas Anderson52b5ee82019-09-27 09:23:56 -0700455 # Detect Change-Id tags
456 elif change_id_match:
Maxim Cournoyer12f99fd2023-10-12 23:06:24 -0400457 if self.keep_change_id:
458 out = [line]
Douglas Anderson52b5ee82019-09-27 09:23:56 -0700459 value = change_id_match.group(1)
460 if self.is_log:
461 if self.commit.change_id:
Simon Glassc42b1d02020-10-29 21:46:17 -0600462 raise ValueError(
Simon Glass718f1a82020-11-03 13:54:11 -0700463 "%s: Two Change-Ids: '%s' vs. '%s'" %
464 (self.commit.hash, self.commit.change_id, value))
Douglas Anderson52b5ee82019-09-27 09:23:56 -0700465 self.commit.change_id = value
466 self.skip_blank = True
467
Albert ARIBAUDd880efd2013-11-12 11:14:41 +0100468 # Detect Commit-xxx tags
469 elif commit_tag_match:
470 name = commit_tag_match.group(1)
471 value = commit_tag_match.group(2)
472 if name == 'notes':
Simon Glass5f6caa52020-10-29 21:46:21 -0600473 self._add_to_commit(name)
Albert ARIBAUDd880efd2013-11-12 11:14:41 +0100474 self.skip_blank = True
Sean Anderson48f46d62020-05-04 16:28:34 -0400475 elif name == 'changes':
476 self.in_change = 'Commit'
Simon Glass93f61c02020-10-29 21:46:19 -0600477 self.change_version = self._parse_version(value, line)
Sean Anderson25978092024-04-18 22:36:31 -0400478 elif name == 'cc':
479 self.commit.add_cc(value.split(','))
Sean Andersone45678c2024-04-18 22:36:32 -0400480 elif name == 'added-in':
481 version = self._parse_version(value, line)
482 self.commit.add_change(version, '- New')
483 self.series.AddChange(version, None, '- %s' %
484 self.commit.subject)
Patrick Delaunayed73b312020-07-02 19:52:54 +0200485 else:
Simon Glassb5800392020-10-29 21:46:23 -0600486 self._add_warn('Line %d: Ignoring Commit-%s' %
487 (self.linenum, name))
Albert ARIBAUDd880efd2013-11-12 11:14:41 +0100488
Patrick Delaunay6bbdd0c2021-07-22 16:51:42 +0200489 # Detect invalid tags
490 elif invalid_match:
491 raise ValueError("Line %d: Invalid tag = '%s'" %
492 (self.linenum, line))
493
Simon Glass26132882012-01-14 15:12:45 +0000494 # Detect the start of a new commit
495 elif commit_match:
Simon Glass93f61c02020-10-29 21:46:19 -0600496 self._close_commit()
Simon Glassbaec6842014-10-15 02:27:00 -0600497 self.commit = commit.Commit(commit_match.group(1))
Simon Glass26132882012-01-14 15:12:45 +0000498
499 # Detect tags in the commit message
500 elif tag_match:
Simon Glass08e91be2020-07-05 21:41:57 -0600501 rtag_type, who = tag_match.groups()
Simon Glass93f61c02020-10-29 21:46:19 -0600502 self._add_commit_rtag(rtag_type, who)
Simon Glass26132882012-01-14 15:12:45 +0000503 # Remove Tested-by self, since few will take much notice
Simon Glass08e91be2020-07-05 21:41:57 -0600504 if (rtag_type == 'Tested-by' and
505 who.find(os.getenv('USER') + '@') != -1):
Simon Glass3b762cc2020-10-29 21:46:28 -0600506 self._add_warn("Ignoring '%s'" % line)
Simon Glass08e91be2020-07-05 21:41:57 -0600507 elif rtag_type == 'Patch-cc':
Simon Glass530ac272022-01-29 14:14:07 -0700508 self.commit.add_cc(who.split(','))
Simon Glass26132882012-01-14 15:12:45 +0000509 else:
Simon Glass27374d32014-08-28 09:43:38 -0600510 out = [line]
Simon Glass26132882012-01-14 15:12:45 +0000511
Simon Glass46b34212014-04-20 10:50:14 -0600512 # Suppress duplicate signoffs
513 elif signoff_match:
Simon Glassb0cd3412014-08-28 09:43:35 -0600514 if (self.is_log or not self.commit or
Simon Glass530ac272022-01-29 14:14:07 -0700515 self.commit.check_duplicate_signoff(signoff_match.group(1))):
Simon Glass46b34212014-04-20 10:50:14 -0600516 out = [line]
517
Simon Glass26132882012-01-14 15:12:45 +0000518 # Well that means this is an ordinary line
519 else:
Simon Glass26132882012-01-14 15:12:45 +0000520 # Look for space before tab
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600521 mat = RE_SPACE_BEFORE_TAB.match(line)
522 if mat:
Simon Glassb5800392020-10-29 21:46:23 -0600523 self._add_warn('Line %d/%d has space before tab' %
524 (self.linenum, mat.start()))
Simon Glass26132882012-01-14 15:12:45 +0000525
526 # OK, we have a valid non-blank line
527 out = [line]
528 self.linenum += 1
529 self.skip_blank = False
Simon Glassda8a2922020-10-29 21:46:37 -0600530
531 if diff_match:
532 self.cur_diff = diff_match.group(1)
533
534 # If this is quoted, keep recent lines
535 if not diff_match and self.linenum > 1 and line:
536 if line.startswith('>'):
537 if not self.was_quoted:
538 self._finalise_snippet()
539 self.recent_line = None
540 if not line_match:
541 self.recent_quoted.append(line)
542 self.was_quoted = True
543 self.recent_diff = self.cur_diff
544 else:
545 self.recent_unquoted.put(line)
546 self.was_quoted = False
547
548 if line_match:
549 self.recent_line = line_match.groups()
550
Simon Glass26132882012-01-14 15:12:45 +0000551 if self.state == STATE_DIFFS:
552 pass
553
554 # If this is the start of the diffs section, emit our tags and
555 # change log
556 elif line == '---':
557 self.state = STATE_DIFFS
558
Sean Anderson48f46d62020-05-04 16:28:34 -0400559 # Output the tags (signoff first), then change list
Simon Glass26132882012-01-14 15:12:45 +0000560 out = []
Simon Glass26132882012-01-14 15:12:45 +0000561 log = self.series.MakeChangeLog(self.commit)
Simon Glassb0cd3412014-08-28 09:43:35 -0600562 out += [line]
563 if self.commit:
564 out += self.commit.notes
565 out += [''] + log
Simon Glass26132882012-01-14 15:12:45 +0000566 elif self.found_test:
Simon Glass06022262020-10-29 21:46:18 -0600567 if not RE_ALLOWED_AFTER_TEST.match(line):
Simon Glass26132882012-01-14 15:12:45 +0000568 self.lines_after_test += 1
569
570 return out
571
Simon Glass93f61c02020-10-29 21:46:19 -0600572 def finalise(self):
Simon Glass26132882012-01-14 15:12:45 +0000573 """Close out processing of this patch stream"""
Simon Glassda8a2922020-10-29 21:46:37 -0600574 self._finalise_snippet()
Simon Glass93f61c02020-10-29 21:46:19 -0600575 self._finalise_change()
576 self._close_commit()
Simon Glass26132882012-01-14 15:12:45 +0000577 if self.lines_after_test:
Simon Glassb5800392020-10-29 21:46:23 -0600578 self._add_warn('Found %d lines after TEST=' % self.lines_after_test)
Simon Glass26132882012-01-14 15:12:45 +0000579
Simon Glass93f61c02020-10-29 21:46:19 -0600580 def _write_message_id(self, outfd):
Douglas Anderson52b5ee82019-09-27 09:23:56 -0700581 """Write the Message-Id into the output.
582
583 This is based on the Change-Id in the original patch, the version,
584 and the prefix.
585
586 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600587 outfd (io.IOBase): Output stream file object
Douglas Anderson52b5ee82019-09-27 09:23:56 -0700588 """
589 if not self.commit.change_id:
590 return
591
592 # If the count is -1 we're testing, so use a fixed time
593 if self.commit.count == -1:
594 time_now = datetime.datetime(1999, 12, 31, 23, 59, 59)
595 else:
596 time_now = datetime.datetime.now()
597
598 # In theory there is email.utils.make_msgid() which would be nice
599 # to use, but it already produces something way too long and thus
600 # will produce ugly commit lines if someone throws this into
601 # a "Link:" tag in the final commit. So (sigh) roll our own.
602
603 # Start with the time; presumably we wouldn't send the same series
604 # with the same Change-Id at the exact same second.
605 parts = [time_now.strftime("%Y%m%d%H%M%S")]
606
607 # These seem like they would be nice to include.
608 if 'prefix' in self.series:
609 parts.append(self.series['prefix'])
Sean Andersondc1cd132021-10-22 19:07:04 -0400610 if 'postfix' in self.series:
Simon Glass547cba62022-02-11 13:23:18 -0700611 parts.append(self.series['postfix'])
Douglas Anderson52b5ee82019-09-27 09:23:56 -0700612 if 'version' in self.series:
613 parts.append("v%s" % self.series['version'])
614
615 parts.append(str(self.commit.count + 1))
616
617 # The Change-Id must be last, right before the @
618 parts.append(self.commit.change_id)
619
620 # Join parts together with "." and write it out.
621 outfd.write('Message-Id: <%s@changeid>\n' % '.'.join(parts))
622
Simon Glass93f61c02020-10-29 21:46:19 -0600623 def process_stream(self, infd, outfd):
Simon Glass26132882012-01-14 15:12:45 +0000624 """Copy a stream from infd to outfd, filtering out unwanting things.
625
626 This is used to process patch files one at a time.
627
628 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600629 infd (io.IOBase): Input stream file object
630 outfd (io.IOBase): Output stream file object
Simon Glass26132882012-01-14 15:12:45 +0000631 """
632 # Extract the filename from each diff, for nice warnings
633 fname = None
634 last_fname = None
635 re_fname = re.compile('diff --git a/(.*) b/.*')
Douglas Anderson52b5ee82019-09-27 09:23:56 -0700636
Simon Glass93f61c02020-10-29 21:46:19 -0600637 self._write_message_id(outfd)
Douglas Anderson52b5ee82019-09-27 09:23:56 -0700638
Simon Glass26132882012-01-14 15:12:45 +0000639 while True:
640 line = infd.readline()
641 if not line:
642 break
Simon Glass93f61c02020-10-29 21:46:19 -0600643 out = self.process_line(line)
Simon Glass26132882012-01-14 15:12:45 +0000644
645 # Try to detect blank lines at EOF
646 for line in out:
647 match = re_fname.match(line)
648 if match:
649 last_fname = fname
650 fname = match.group(1)
651 if line == '+':
652 self.blank_count += 1
653 else:
654 if self.blank_count and (line == '-- ' or match):
Simon Glassb5800392020-10-29 21:46:23 -0600655 self._add_warn("Found possible blank line(s) at end of file '%s'" %
656 last_fname)
Simon Glass26132882012-01-14 15:12:45 +0000657 outfd.write('+\n' * self.blank_count)
658 outfd.write(line + '\n')
659 self.blank_count = 0
Simon Glass93f61c02020-10-29 21:46:19 -0600660 self.finalise()
Simon Glass26132882012-01-14 15:12:45 +0000661
Simon Glassd0a0a582020-10-29 21:46:36 -0600662def insert_tags(msg, tags_to_emit):
663 """Add extra tags to a commit message
664
665 The tags are added after an existing block of tags if found, otherwise at
666 the end.
667
668 Args:
669 msg (str): Commit message
670 tags_to_emit (list): List of tags to emit, each a str
671
672 Returns:
673 (str) new message
674 """
675 out = []
676 done = False
677 emit_tags = False
Simon Glass6a222e62021-08-01 16:02:39 -0600678 emit_blank = False
Simon Glassd0a0a582020-10-29 21:46:36 -0600679 for line in msg.splitlines():
680 if not done:
681 signoff_match = RE_SIGNOFF.match(line)
682 tag_match = RE_TAG.match(line)
683 if tag_match or signoff_match:
684 emit_tags = True
685 if emit_tags and not tag_match and not signoff_match:
686 out += tags_to_emit
687 emit_tags = False
688 done = True
Simon Glass6a222e62021-08-01 16:02:39 -0600689 emit_blank = not (signoff_match or tag_match)
690 else:
691 emit_blank = line
Simon Glassd0a0a582020-10-29 21:46:36 -0600692 out.append(line)
693 if not done:
Simon Glass6a222e62021-08-01 16:02:39 -0600694 if emit_blank:
695 out.append('')
Simon Glassd0a0a582020-10-29 21:46:36 -0600696 out += tags_to_emit
697 return '\n'.join(out)
698
699def get_list(commit_range, git_dir=None, count=None):
700 """Get a log of a list of comments
701
702 This returns the output of 'git log' for the selected commits
703
704 Args:
705 commit_range (str): Range of commits to count (e.g. 'HEAD..base')
706 git_dir (str): Path to git repositiory (None to use default)
707 count (int): Number of commits to list, or None for no limit
708
709 Returns
710 str: String containing the contents of the git log
711 """
Simon Glass761648b2022-01-29 14:14:11 -0700712 params = gitutil.log_cmd(commit_range, reverse=True, count=count,
Simon Glassd0a0a582020-10-29 21:46:36 -0600713 git_dir=git_dir)
Simon Glass840be732022-01-29 14:14:05 -0700714 return command.run_pipe([params], capture=True).stdout
Simon Glass26132882012-01-14 15:12:45 +0000715
Simon Glass93f61c02020-10-29 21:46:19 -0600716def get_metadata_for_list(commit_range, git_dir=None, count=None,
717 series=None, allow_overwrite=False):
Simon Glass26132882012-01-14 15:12:45 +0000718 """Reads out patch series metadata from the commits
719
720 This does a 'git log' on the relevant commits and pulls out the tags we
721 are interested in.
722
723 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600724 commit_range (str): Range of commits to count (e.g. 'HEAD..base')
725 git_dir (str): Path to git repositiory (None to use default)
726 count (int): Number of commits to list, or None for no limit
727 series (Series): Object to add information into. By default a new series
Simon Glass680da3c2012-12-15 10:42:06 +0000728 is started.
Simon Glass06127f92020-10-29 21:46:22 -0600729 allow_overwrite (bool): Allow tags to overwrite an existing tag
730
Simon Glass680da3c2012-12-15 10:42:06 +0000731 Returns:
Simon Glass06127f92020-10-29 21:46:22 -0600732 Series: Object containing information about the commits.
Simon Glass26132882012-01-14 15:12:45 +0000733 """
Simon Glass4e5d6bc2014-09-05 19:00:19 -0600734 if not series:
735 series = Series()
Simon Glass359b55a62014-09-05 19:00:23 -0600736 series.allow_overwrite = allow_overwrite
Simon Glassd0a0a582020-10-29 21:46:36 -0600737 stdout = get_list(commit_range, git_dir, count)
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600738 pst = PatchStream(series, is_log=True)
Simon Glass26132882012-01-14 15:12:45 +0000739 for line in stdout.splitlines():
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600740 pst.process_line(line)
741 pst.finalise()
Simon Glass26132882012-01-14 15:12:45 +0000742 return series
743
Simon Glass93f61c02020-10-29 21:46:19 -0600744def get_metadata(branch, start, count):
Simon Glass680da3c2012-12-15 10:42:06 +0000745 """Reads out patch series metadata from the commits
746
747 This does a 'git log' on the relevant commits and pulls out the tags we
748 are interested in.
749
750 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600751 branch (str): Branch to use (None for current branch)
752 start (int): Commit to start from: 0=branch HEAD, 1=next one, etc.
753 count (int): Number of commits to list
754
755 Returns:
756 Series: Object containing information about the commits.
Simon Glass680da3c2012-12-15 10:42:06 +0000757 """
Simon Glass93f61c02020-10-29 21:46:19 -0600758 return get_metadata_for_list(
759 '%s~%d' % (branch if branch else 'HEAD', start), None, count)
Simon Glass680da3c2012-12-15 10:42:06 +0000760
Simon Glass93f61c02020-10-29 21:46:19 -0600761def get_metadata_for_test(text):
Simon Glassdf1bc5c2017-05-29 15:31:31 -0600762 """Process metadata from a file containing a git log. Used for tests
763
764 Args:
765 text:
Simon Glass06127f92020-10-29 21:46:22 -0600766
767 Returns:
768 Series: Object containing information about the commits.
Simon Glassdf1bc5c2017-05-29 15:31:31 -0600769 """
770 series = Series()
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600771 pst = PatchStream(series, is_log=True)
Simon Glassdf1bc5c2017-05-29 15:31:31 -0600772 for line in text.splitlines():
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600773 pst.process_line(line)
774 pst.finalise()
Simon Glassdf1bc5c2017-05-29 15:31:31 -0600775 return series
776
Maxim Cournoyer12f99fd2023-10-12 23:06:24 -0400777def fix_patch(backup_dir, fname, series, cmt, keep_change_id=False):
Simon Glass26132882012-01-14 15:12:45 +0000778 """Fix up a patch file, by adding/removing as required.
779
780 We remove our tags from the patch file, insert changes lists, etc.
781 The patch file is processed in place, and overwritten.
782
783 A backup file is put into backup_dir (if not None).
784
785 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600786 backup_dir (str): Path to directory to use to backup the file
787 fname (str): Filename to patch file to process
788 series (Series): Series information about this patch set
789 cmt (Commit): Commit object for this patch file
Maxim Cournoyer12f99fd2023-10-12 23:06:24 -0400790 keep_change_id (bool): Keep the Change-Id tag.
Simon Glass06127f92020-10-29 21:46:22 -0600791
Simon Glass26132882012-01-14 15:12:45 +0000792 Return:
Simon Glass06127f92020-10-29 21:46:22 -0600793 list: A list of errors, each str, or [] if all ok.
Simon Glass26132882012-01-14 15:12:45 +0000794 """
795 handle, tmpname = tempfile.mkstemp()
Simon Glassf544a2d2019-10-31 07:42:51 -0600796 outfd = os.fdopen(handle, 'w', encoding='utf-8')
797 infd = open(fname, 'r', encoding='utf-8')
Maxim Cournoyer12f99fd2023-10-12 23:06:24 -0400798 pst = PatchStream(series, keep_change_id=keep_change_id)
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600799 pst.commit = cmt
800 pst.process_stream(infd, outfd)
Simon Glass26132882012-01-14 15:12:45 +0000801 infd.close()
802 outfd.close()
803
804 # Create a backup file if required
805 if backup_dir:
806 shutil.copy(fname, os.path.join(backup_dir, os.path.basename(fname)))
807 shutil.move(tmpname, fname)
Simon Glass6d00f6c2020-10-29 21:46:24 -0600808 return cmt.warn
Simon Glass26132882012-01-14 15:12:45 +0000809
Maxim Cournoyer12f99fd2023-10-12 23:06:24 -0400810def fix_patches(series, fnames, keep_change_id=False):
Simon Glass26132882012-01-14 15:12:45 +0000811 """Fix up a list of patches identified by filenames
812
813 The patch files are processed in place, and overwritten.
814
815 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600816 series (Series): The Series object
817 fnames (:type: list of str): List of patch files to process
Maxim Cournoyer12f99fd2023-10-12 23:06:24 -0400818 keep_change_id (bool): Keep the Change-Id tag.
Simon Glass26132882012-01-14 15:12:45 +0000819 """
820 # Current workflow creates patches, so we shouldn't need a backup
821 backup_dir = None #tempfile.mkdtemp('clean-patch')
822 count = 0
823 for fname in fnames:
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600824 cmt = series.commits[count]
825 cmt.patch = fname
826 cmt.count = count
Maxim Cournoyer12f99fd2023-10-12 23:06:24 -0400827 result = fix_patch(backup_dir, fname, series, cmt,
828 keep_change_id=keep_change_id)
Simon Glass26132882012-01-14 15:12:45 +0000829 if result:
Simon Glass891316b2020-10-29 21:46:30 -0600830 print('%d warning%s for %s:' %
831 (len(result), 's' if len(result) > 1 else '', fname))
Simon Glass26132882012-01-14 15:12:45 +0000832 for warn in result:
Simon Glass891316b2020-10-29 21:46:30 -0600833 print('\t%s' % warn)
834 print()
Simon Glass26132882012-01-14 15:12:45 +0000835 count += 1
Simon Glass891316b2020-10-29 21:46:30 -0600836 print('Cleaned %d patch%s' % (count, 'es' if count > 1 else ''))
Simon Glass26132882012-01-14 15:12:45 +0000837
Simon Glass93f61c02020-10-29 21:46:19 -0600838def insert_cover_letter(fname, series, count):
Simon Glass26132882012-01-14 15:12:45 +0000839 """Inserts a cover letter with the required info into patch 0
840
841 Args:
Simon Glass06127f92020-10-29 21:46:22 -0600842 fname (str): Input / output filename of the cover letter file
843 series (Series): Series object
844 count (int): Number of patches in the series
Simon Glass26132882012-01-14 15:12:45 +0000845 """
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600846 fil = open(fname, 'r')
847 lines = fil.readlines()
848 fil.close()
Simon Glass26132882012-01-14 15:12:45 +0000849
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600850 fil = open(fname, 'w')
Simon Glass26132882012-01-14 15:12:45 +0000851 text = series.cover
852 prefix = series.GetPatchPrefix()
853 for line in lines:
854 if line.startswith('Subject:'):
Wu, Josh393aaa22015-04-03 10:51:17 +0800855 # if more than 10 or 100 patches, it should say 00/xx, 000/xxx, etc
856 zero_repeat = int(math.log10(count)) + 1
857 zero = '0' * zero_repeat
858 line = 'Subject: [%s %s/%d] %s\n' % (prefix, zero, count, text[0])
Simon Glass26132882012-01-14 15:12:45 +0000859
860 # Insert our cover letter
861 elif line.startswith('*** BLURB HERE ***'):
862 # First the blurb test
863 line = '\n'.join(text[1:]) + '\n'
864 if series.get('notes'):
865 line += '\n'.join(series.notes) + '\n'
866
867 # Now the change list
868 out = series.MakeChangeLog(None)
869 line += '\n' + '\n'.join(out)
Simon Glassf9b6e0f2020-10-29 21:46:20 -0600870 fil.write(line)
871 fil.close()