blob: 5487799bbee41a216b3e8b6f197a11d5fea63a69 [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
Douglas Anderson833e4192019-09-27 09:23:56 -07007import datetime
Wu, Josh35ce2dc2015-04-03 10:51:17 +08008import math
Simon Glass0d24de92012-01-14 15:12:45 +00009import os
10import re
11import shutil
12import tempfile
13
Simon Glassbf776672020-04-17 18:09:04 -060014from patman import command
15from patman import commit
16from patman import gitutil
17from patman.series import Series
Simon Glass0d24de92012-01-14 15:12:45 +000018
19# Tags that we detect and remove
Simon Glass57699042020-10-29 21:46:18 -060020RE_REMOVE = re.compile(r'^BUG=|^TEST=|^BRANCH=|^Review URL:'
Simon Glassd06e55a2020-10-29 21:46:17 -060021 r'|Reviewed-on:|Commit-\w*:')
Simon Glass0d24de92012-01-14 15:12:45 +000022
23# Lines which are allowed after a TEST= line
Simon Glass57699042020-10-29 21:46:18 -060024RE_ALLOWED_AFTER_TEST = re.compile('^Signed-off-by:')
Simon Glass0d24de92012-01-14 15:12:45 +000025
Ilya Yanok05e5b732012-08-06 23:46:05 +000026# Signoffs
Simon Glass57699042020-10-29 21:46:18 -060027RE_SIGNOFF = re.compile('^Signed-off-by: *(.*)')
Ilya Yanok05e5b732012-08-06 23:46:05 +000028
Sean Anderson6949f702020-05-04 16:28:34 -040029# Cover letter tag
Simon Glass57699042020-10-29 21:46:18 -060030RE_COVER = re.compile('^Cover-([a-z-]*): *(.*)')
Simon Glassfe2f8d92013-03-20 16:43:00 +000031
Simon Glass0d24de92012-01-14 15:12:45 +000032# Patch series tag
Simon Glass57699042020-10-29 21:46:18 -060033RE_SERIES_TAG = re.compile('^Series-([a-z-]*): *(.*)')
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +010034
Douglas Anderson833e4192019-09-27 09:23:56 -070035# Change-Id will be used to generate the Message-Id and then be stripped
Simon Glass57699042020-10-29 21:46:18 -060036RE_CHANGE_ID = re.compile('^Change-Id: *(.*)')
Douglas Anderson833e4192019-09-27 09:23:56 -070037
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +010038# Commit series tag
Simon Glass57699042020-10-29 21:46:18 -060039RE_COMMIT_TAG = re.compile('^Commit-([a-z-]*): *(.*)')
Simon Glass0d24de92012-01-14 15:12:45 +000040
41# Commit tags that we want to collect and keep
Simon Glass57699042020-10-29 21:46:18 -060042RE_TAG = re.compile('^(Tested-by|Acked-by|Reviewed-by|Patch-cc|Fixes): (.*)')
Simon Glass0d24de92012-01-14 15:12:45 +000043
44# The start of a new commit in the git log
Simon Glass57699042020-10-29 21:46:18 -060045RE_COMMIT = re.compile('^commit ([0-9a-f]*)$')
Simon Glass0d24de92012-01-14 15:12:45 +000046
47# We detect these since checkpatch doesn't always do it
Simon Glass57699042020-10-29 21:46:18 -060048RE_SPACE_BEFORE_TAB = re.compile('^[+].* \t')
Simon Glass0d24de92012-01-14 15:12:45 +000049
Sean Anderson0411fff2020-05-04 16:28:35 -040050# Match indented lines for changes
Simon Glass57699042020-10-29 21:46:18 -060051RE_LEADING_WHITESPACE = re.compile(r'^\s')
Sean Anderson0411fff2020-05-04 16:28:35 -040052
Simon Glass0d24de92012-01-14 15:12:45 +000053# States we can be in - can we use range() and still have comments?
54STATE_MSG_HEADER = 0 # Still in the message header
55STATE_PATCH_SUBJECT = 1 # In patch subject (first line of log for a commit)
56STATE_PATCH_HEADER = 2 # In patch header (after the subject)
57STATE_DIFFS = 3 # In the diff part (past --- line)
58
59class PatchStream:
60 """Class for detecting/injecting tags in a patch or series of patches
61
62 We support processing the output of 'git log' to read out the tags we
63 are interested in. We can also process a patch file in order to remove
64 unwanted tags or inject additional ones. These correspond to the two
65 phases of processing.
66 """
67 def __init__(self, series, name=None, is_log=False):
68 self.skip_blank = False # True to skip a single blank line
69 self.found_test = False # Found a TEST= line
Sean Anderson6949f702020-05-04 16:28:34 -040070 self.lines_after_test = 0 # Number of lines found after TEST=
Simon Glass0d24de92012-01-14 15:12:45 +000071 self.warn = [] # List of warnings we have collected
72 self.linenum = 1 # Output line number we are up to
73 self.in_section = None # Name of start...END section we are in
74 self.notes = [] # Series notes
75 self.section = [] # The current section...END section
76 self.series = series # Info about the patch series
77 self.is_log = is_log # True if indent like git log
Sean Anderson6949f702020-05-04 16:28:34 -040078 self.in_change = None # Name of the change list we are in
79 self.change_version = 0 # Non-zero if we are in a change list
Sean Anderson0411fff2020-05-04 16:28:35 -040080 self.change_lines = [] # Lines of the current change
Simon Glass0d24de92012-01-14 15:12:45 +000081 self.blank_count = 0 # Number of blank lines stored up
82 self.state = STATE_MSG_HEADER # What state are we in?
Simon Glass0d24de92012-01-14 15:12:45 +000083 self.signoff = [] # Contents of signoff line
84 self.commit = None # Current commit
85
Simon Glassd93720e2020-10-29 21:46:19 -060086 def _add_to_series(self, line, name, value):
Simon Glass0d24de92012-01-14 15:12:45 +000087 """Add a new Series-xxx tag.
88
89 When a Series-xxx tag is detected, we come here to record it, if we
90 are scanning a 'git log'.
91
92 Args:
93 line: Source line containing tag (useful for debug/error messages)
94 name: Tag name (part after 'Series-')
95 value: Tag value (part after 'Series-xxx: ')
96 """
97 if name == 'notes':
98 self.in_section = name
99 self.skip_blank = False
100 if self.is_log:
101 self.series.AddTag(self.commit, line, name, value)
102
Simon Glassd93720e2020-10-29 21:46:19 -0600103 def _add_to_commit(self, line, name, value):
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100104 """Add a new Commit-xxx tag.
105
106 When a Commit-xxx tag is detected, we come here to record it.
107
108 Args:
109 line: Source line containing tag (useful for debug/error messages)
110 name: Tag name (part after 'Commit-')
111 value: Tag value (part after 'Commit-xxx: ')
112 """
113 if name == 'notes':
114 self.in_section = 'commit-' + name
115 self.skip_blank = False
116
Simon Glassd93720e2020-10-29 21:46:19 -0600117 def _add_commit_rtag(self, rtag_type, who):
Simon Glass7207e2b2020-07-05 21:41:57 -0600118 """Add a response tag to the current commit
119
120 Args:
121 key: rtag type (e.g. 'Reviewed-by')
122 who: Person who gave that rtag, e.g. 'Fred Bloggs <fred@bloggs.org>'
123 """
124 self.commit.AddRtag(rtag_type, who)
125
Simon Glassd93720e2020-10-29 21:46:19 -0600126 def _close_commit(self):
Simon Glass0d24de92012-01-14 15:12:45 +0000127 """Save the current commit into our commit list, and reset our state"""
128 if self.commit and self.is_log:
129 self.series.AddCommit(self.commit)
130 self.commit = None
Bin Meng0d577182016-06-26 23:24:30 -0700131 # If 'END' is missing in a 'Cover-letter' section, and that section
132 # happens to show up at the very end of the commit message, this is
133 # the chance for us to fix it up.
134 if self.in_section == 'cover' and self.is_log:
135 self.series.cover = self.section
136 self.in_section = None
137 self.skip_blank = True
138 self.section = []
Simon Glass0d24de92012-01-14 15:12:45 +0000139
Simon Glassd93720e2020-10-29 21:46:19 -0600140 def _parse_version(self, value, line):
Sean Anderson6949f702020-05-04 16:28:34 -0400141 """Parse a version from a *-changes tag
142
143 Args:
144 value: Tag value (part after 'xxx-changes: '
145 line: Source line containing tag
146
147 Returns:
148 The version as an integer
149 """
150 try:
151 return int(value)
152 except ValueError as str:
153 raise ValueError("%s: Cannot decode version info '%s'" %
Simon Glassd06e55a2020-10-29 21:46:17 -0600154 (self.commit.hash, line))
Sean Anderson6949f702020-05-04 16:28:34 -0400155
Simon Glassd93720e2020-10-29 21:46:19 -0600156 def _finalise_change(self):
157 """_finalise a (multi-line) change and add it to the series or commit"""
Sean Anderson0411fff2020-05-04 16:28:35 -0400158 if not self.change_lines:
159 return
160 change = '\n'.join(self.change_lines)
161
162 if self.in_change == 'Series':
163 self.series.AddChange(self.change_version, self.commit, change)
164 elif self.in_change == 'Cover':
165 self.series.AddChange(self.change_version, None, change)
166 elif self.in_change == 'Commit':
167 self.commit.AddChange(self.change_version, change)
168 self.change_lines = []
169
Simon Glassd93720e2020-10-29 21:46:19 -0600170 def process_line(self, line):
Simon Glass0d24de92012-01-14 15:12:45 +0000171 """Process a single line of a patch file or commit log
172
173 This process a line and returns a list of lines to output. The list
174 may be empty or may contain multiple output lines.
175
176 This is where all the complicated logic is located. The class's
177 state is used to move between different states and detect things
178 properly.
179
180 We can be in one of two modes:
181 self.is_log == True: This is 'git log' mode, where most output is
182 indented by 4 characters and we are scanning for tags
183
184 self.is_log == False: This is 'patch' mode, where we already have
185 all the tags, and are processing patches to remove junk we
186 don't want, and add things we think are required.
187
188 Args:
189 line: text line to process
190
191 Returns:
192 list of output lines, or [] if nothing should be output
193 """
194 # Initially we have no output. Prepare the input line string
195 out = []
196 line = line.rstrip('\n')
Scott Wood4b89b812014-09-25 14:30:46 -0500197
Simon Glass57699042020-10-29 21:46:18 -0600198 commit_match = RE_COMMIT.match(line) if self.is_log else None
Scott Wood4b89b812014-09-25 14:30:46 -0500199
Simon Glass0d24de92012-01-14 15:12:45 +0000200 if self.is_log:
201 if line[:4] == ' ':
202 line = line[4:]
203
204 # Handle state transition and skipping blank lines
Simon Glass57699042020-10-29 21:46:18 -0600205 series_tag_match = RE_SERIES_TAG.match(line)
206 change_id_match = RE_CHANGE_ID.match(line)
207 commit_tag_match = RE_COMMIT_TAG.match(line)
208 cover_match = RE_COVER.match(line)
209 signoff_match = RE_SIGNOFF.match(line)
210 leading_whitespace_match = RE_LEADING_WHITESPACE.match(line)
Simon Glass0d24de92012-01-14 15:12:45 +0000211 tag_match = None
212 if self.state == STATE_PATCH_HEADER:
Simon Glass57699042020-10-29 21:46:18 -0600213 tag_match = RE_TAG.match(line)
Simon Glass0d24de92012-01-14 15:12:45 +0000214 is_blank = not line.strip()
215 if is_blank:
216 if (self.state == STATE_MSG_HEADER
217 or self.state == STATE_PATCH_SUBJECT):
218 self.state += 1
219
220 # We don't have a subject in the text stream of patch files
221 # It has its own line with a Subject: tag
222 if not self.is_log and self.state == STATE_PATCH_SUBJECT:
223 self.state += 1
224 elif commit_match:
225 self.state = STATE_MSG_HEADER
226
Bin Meng94fbd3e2016-06-26 23:24:32 -0700227 # If a tag is detected, or a new commit starts
Douglas Anderson833e4192019-09-27 09:23:56 -0700228 if series_tag_match or commit_tag_match or change_id_match or \
Sean Anderson6949f702020-05-04 16:28:34 -0400229 cover_match or signoff_match or self.state == STATE_MSG_HEADER:
Bin Meng57b6b192016-06-26 23:24:31 -0700230 # but we are already in a section, this means 'END' is missing
231 # for that section, fix it up.
Bin Meng13b98d92016-06-26 23:24:29 -0700232 if self.in_section:
233 self.warn.append("Missing 'END' in section '%s'" % self.in_section)
234 if self.in_section == 'cover':
235 self.series.cover = self.section
236 elif self.in_section == 'notes':
237 if self.is_log:
238 self.series.notes += self.section
239 elif self.in_section == 'commit-notes':
240 if self.is_log:
241 self.commit.notes += self.section
242 else:
243 self.warn.append("Unknown section '%s'" % self.in_section)
244 self.in_section = None
245 self.skip_blank = True
246 self.section = []
Bin Meng57b6b192016-06-26 23:24:31 -0700247 # but we are already in a change list, that means a blank line
248 # is missing, fix it up.
249 if self.in_change:
Sean Anderson6949f702020-05-04 16:28:34 -0400250 self.warn.append("Missing 'blank line' in section '%s-changes'" % self.in_change)
Simon Glassd93720e2020-10-29 21:46:19 -0600251 self._finalise_change()
Sean Anderson6949f702020-05-04 16:28:34 -0400252 self.in_change = None
253 self.change_version = 0
Bin Meng13b98d92016-06-26 23:24:29 -0700254
Simon Glass0d24de92012-01-14 15:12:45 +0000255 # If we are in a section, keep collecting lines until we see END
256 if self.in_section:
257 if line == 'END':
258 if self.in_section == 'cover':
259 self.series.cover = self.section
260 elif self.in_section == 'notes':
261 if self.is_log:
262 self.series.notes += self.section
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100263 elif self.in_section == 'commit-notes':
264 if self.is_log:
265 self.commit.notes += self.section
Simon Glass0d24de92012-01-14 15:12:45 +0000266 else:
267 self.warn.append("Unknown section '%s'" % self.in_section)
268 self.in_section = None
269 self.skip_blank = True
270 self.section = []
271 else:
272 self.section.append(line)
273
Patrick Delaunay7058dd02020-07-02 19:08:24 +0200274 # If we are not in a section, it is an unexpected END
275 elif line == 'END':
Simon Glassd06e55a2020-10-29 21:46:17 -0600276 raise ValueError("'END' wihout section")
Patrick Delaunay7058dd02020-07-02 19:08:24 +0200277
Simon Glass0d24de92012-01-14 15:12:45 +0000278 # Detect the commit subject
279 elif not is_blank and self.state == STATE_PATCH_SUBJECT:
280 self.commit.subject = line
281
282 # Detect the tags we want to remove, and skip blank lines
Simon Glass57699042020-10-29 21:46:18 -0600283 elif RE_REMOVE.match(line) and not commit_tag_match:
Simon Glass0d24de92012-01-14 15:12:45 +0000284 self.skip_blank = True
285
286 # TEST= should be the last thing in the commit, so remove
287 # everything after it
288 if line.startswith('TEST='):
289 self.found_test = True
290 elif self.skip_blank and is_blank:
291 self.skip_blank = False
292
Sean Anderson6949f702020-05-04 16:28:34 -0400293 # Detect Cover-xxx tags
Bin Menge7df2182016-06-26 23:24:28 -0700294 elif cover_match:
Sean Anderson6949f702020-05-04 16:28:34 -0400295 name = cover_match.group(1)
296 value = cover_match.group(2)
297 if name == 'letter':
298 self.in_section = 'cover'
299 self.skip_blank = False
300 elif name == 'letter-cc':
Simon Glassd93720e2020-10-29 21:46:19 -0600301 self._add_to_series(line, 'cover-cc', value)
Sean Anderson6949f702020-05-04 16:28:34 -0400302 elif name == 'changes':
303 self.in_change = 'Cover'
Simon Glassd93720e2020-10-29 21:46:19 -0600304 self.change_version = self._parse_version(value, line)
Simon Glassfe2f8d92013-03-20 16:43:00 +0000305
Simon Glass0d24de92012-01-14 15:12:45 +0000306 # If we are in a change list, key collected lines until a blank one
307 elif self.in_change:
308 if is_blank:
309 # Blank line ends this change list
Simon Glassd93720e2020-10-29 21:46:19 -0600310 self._finalise_change()
Sean Anderson6949f702020-05-04 16:28:34 -0400311 self.in_change = None
312 self.change_version = 0
Simon Glass102061b2014-04-20 10:50:14 -0600313 elif line == '---':
Simon Glassd93720e2020-10-29 21:46:19 -0600314 self._finalise_change()
Sean Anderson6949f702020-05-04 16:28:34 -0400315 self.in_change = None
316 self.change_version = 0
Simon Glassd93720e2020-10-29 21:46:19 -0600317 out = self.process_line(line)
Sean Anderson0411fff2020-05-04 16:28:35 -0400318 elif self.is_log:
319 if not leading_whitespace_match:
Simon Glassd93720e2020-10-29 21:46:19 -0600320 self._finalise_change()
Sean Anderson0411fff2020-05-04 16:28:35 -0400321 self.change_lines.append(line)
Simon Glass0d24de92012-01-14 15:12:45 +0000322 self.skip_blank = False
323
324 # Detect Series-xxx tags
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100325 elif series_tag_match:
326 name = series_tag_match.group(1)
327 value = series_tag_match.group(2)
Simon Glass0d24de92012-01-14 15:12:45 +0000328 if name == 'changes':
329 # value is the version number: e.g. 1, or 2
Sean Anderson6949f702020-05-04 16:28:34 -0400330 self.in_change = 'Series'
Simon Glassd93720e2020-10-29 21:46:19 -0600331 self.change_version = self._parse_version(value, line)
Simon Glass0d24de92012-01-14 15:12:45 +0000332 else:
Simon Glassd93720e2020-10-29 21:46:19 -0600333 self._add_to_series(line, name, value)
Simon Glass0d24de92012-01-14 15:12:45 +0000334 self.skip_blank = True
335
Douglas Anderson833e4192019-09-27 09:23:56 -0700336 # Detect Change-Id tags
337 elif change_id_match:
338 value = change_id_match.group(1)
339 if self.is_log:
340 if self.commit.change_id:
Simon Glassd06e55a2020-10-29 21:46:17 -0600341 raise ValueError(
342 "%s: Two Change-Ids: '%s' vs. '%s'" % self.commit.hash,
343 self.commit.change_id, value)
Douglas Anderson833e4192019-09-27 09:23:56 -0700344 self.commit.change_id = value
345 self.skip_blank = True
346
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100347 # Detect Commit-xxx tags
348 elif commit_tag_match:
349 name = commit_tag_match.group(1)
350 value = commit_tag_match.group(2)
351 if name == 'notes':
Simon Glassd93720e2020-10-29 21:46:19 -0600352 self._add_to_commit(line, name, value)
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100353 self.skip_blank = True
Sean Anderson6949f702020-05-04 16:28:34 -0400354 elif name == 'changes':
355 self.in_change = 'Commit'
Simon Glassd93720e2020-10-29 21:46:19 -0600356 self.change_version = self._parse_version(value, line)
Patrick Delaunaye5ff9ab2020-07-02 19:52:54 +0200357 else:
358 self.warn.append('Line %d: Ignoring Commit-%s' %
Simon Glassd06e55a2020-10-29 21:46:17 -0600359 (self.linenum, name))
Albert ARIBAUD5c8fdd92013-11-12 11:14:41 +0100360
Simon Glass0d24de92012-01-14 15:12:45 +0000361 # Detect the start of a new commit
362 elif commit_match:
Simon Glassd93720e2020-10-29 21:46:19 -0600363 self._close_commit()
Simon Glass0b5b4092014-10-15 02:27:00 -0600364 self.commit = commit.Commit(commit_match.group(1))
Simon Glass0d24de92012-01-14 15:12:45 +0000365
366 # Detect tags in the commit message
367 elif tag_match:
Simon Glass7207e2b2020-07-05 21:41:57 -0600368 rtag_type, who = tag_match.groups()
Simon Glassd93720e2020-10-29 21:46:19 -0600369 self._add_commit_rtag(rtag_type, who)
Simon Glass0d24de92012-01-14 15:12:45 +0000370 # Remove Tested-by self, since few will take much notice
Simon Glass7207e2b2020-07-05 21:41:57 -0600371 if (rtag_type == 'Tested-by' and
372 who.find(os.getenv('USER') + '@') != -1):
Simon Glass0d24de92012-01-14 15:12:45 +0000373 self.warn.append("Ignoring %s" % line)
Simon Glass7207e2b2020-07-05 21:41:57 -0600374 elif rtag_type == 'Patch-cc':
375 self.commit.AddCc(who.split(','))
Simon Glass0d24de92012-01-14 15:12:45 +0000376 else:
Simon Glassd0c57192014-08-28 09:43:38 -0600377 out = [line]
Simon Glass0d24de92012-01-14 15:12:45 +0000378
Simon Glass102061b2014-04-20 10:50:14 -0600379 # Suppress duplicate signoffs
380 elif signoff_match:
Simon Glasse752edc2014-08-28 09:43:35 -0600381 if (self.is_log or not self.commit or
Simon Glassd06e55a2020-10-29 21:46:17 -0600382 self.commit.CheckDuplicateSignoff(signoff_match.group(1))):
Simon Glass102061b2014-04-20 10:50:14 -0600383 out = [line]
384
Simon Glass0d24de92012-01-14 15:12:45 +0000385 # Well that means this is an ordinary line
386 else:
Simon Glass0d24de92012-01-14 15:12:45 +0000387 # Look for space before tab
Simon Glass57699042020-10-29 21:46:18 -0600388 m = RE_SPACE_BEFORE_TAB.match(line)
Simon Glass0d24de92012-01-14 15:12:45 +0000389 if m:
390 self.warn.append('Line %d/%d has space before tab' %
Simon Glassd06e55a2020-10-29 21:46:17 -0600391 (self.linenum, m.start()))
Simon Glass0d24de92012-01-14 15:12:45 +0000392
393 # OK, we have a valid non-blank line
394 out = [line]
395 self.linenum += 1
396 self.skip_blank = False
397 if self.state == STATE_DIFFS:
398 pass
399
400 # If this is the start of the diffs section, emit our tags and
401 # change log
402 elif line == '---':
403 self.state = STATE_DIFFS
404
Sean Anderson6949f702020-05-04 16:28:34 -0400405 # Output the tags (signoff first), then change list
Simon Glass0d24de92012-01-14 15:12:45 +0000406 out = []
Simon Glass0d24de92012-01-14 15:12:45 +0000407 log = self.series.MakeChangeLog(self.commit)
Simon Glasse752edc2014-08-28 09:43:35 -0600408 out += [line]
409 if self.commit:
410 out += self.commit.notes
411 out += [''] + log
Simon Glass0d24de92012-01-14 15:12:45 +0000412 elif self.found_test:
Simon Glass57699042020-10-29 21:46:18 -0600413 if not RE_ALLOWED_AFTER_TEST.match(line):
Simon Glass0d24de92012-01-14 15:12:45 +0000414 self.lines_after_test += 1
415
416 return out
417
Simon Glassd93720e2020-10-29 21:46:19 -0600418 def finalise(self):
Simon Glass0d24de92012-01-14 15:12:45 +0000419 """Close out processing of this patch stream"""
Simon Glassd93720e2020-10-29 21:46:19 -0600420 self._finalise_change()
421 self._close_commit()
Simon Glass0d24de92012-01-14 15:12:45 +0000422 if self.lines_after_test:
423 self.warn.append('Found %d lines after TEST=' %
Simon Glassd06e55a2020-10-29 21:46:17 -0600424 self.lines_after_test)
Simon Glass0d24de92012-01-14 15:12:45 +0000425
Simon Glassd93720e2020-10-29 21:46:19 -0600426 def _write_message_id(self, outfd):
Douglas Anderson833e4192019-09-27 09:23:56 -0700427 """Write the Message-Id into the output.
428
429 This is based on the Change-Id in the original patch, the version,
430 and the prefix.
431
432 Args:
433 outfd: Output stream file object
434 """
435 if not self.commit.change_id:
436 return
437
438 # If the count is -1 we're testing, so use a fixed time
439 if self.commit.count == -1:
440 time_now = datetime.datetime(1999, 12, 31, 23, 59, 59)
441 else:
442 time_now = datetime.datetime.now()
443
444 # In theory there is email.utils.make_msgid() which would be nice
445 # to use, but it already produces something way too long and thus
446 # will produce ugly commit lines if someone throws this into
447 # a "Link:" tag in the final commit. So (sigh) roll our own.
448
449 # Start with the time; presumably we wouldn't send the same series
450 # with the same Change-Id at the exact same second.
451 parts = [time_now.strftime("%Y%m%d%H%M%S")]
452
453 # These seem like they would be nice to include.
454 if 'prefix' in self.series:
455 parts.append(self.series['prefix'])
456 if 'version' in self.series:
457 parts.append("v%s" % self.series['version'])
458
459 parts.append(str(self.commit.count + 1))
460
461 # The Change-Id must be last, right before the @
462 parts.append(self.commit.change_id)
463
464 # Join parts together with "." and write it out.
465 outfd.write('Message-Id: <%s@changeid>\n' % '.'.join(parts))
466
Simon Glassd93720e2020-10-29 21:46:19 -0600467 def process_stream(self, infd, outfd):
Simon Glass0d24de92012-01-14 15:12:45 +0000468 """Copy a stream from infd to outfd, filtering out unwanting things.
469
470 This is used to process patch files one at a time.
471
472 Args:
473 infd: Input stream file object
474 outfd: Output stream file object
475 """
476 # Extract the filename from each diff, for nice warnings
477 fname = None
478 last_fname = None
479 re_fname = re.compile('diff --git a/(.*) b/.*')
Douglas Anderson833e4192019-09-27 09:23:56 -0700480
Simon Glassd93720e2020-10-29 21:46:19 -0600481 self._write_message_id(outfd)
Douglas Anderson833e4192019-09-27 09:23:56 -0700482
Simon Glass0d24de92012-01-14 15:12:45 +0000483 while True:
484 line = infd.readline()
485 if not line:
486 break
Simon Glassd93720e2020-10-29 21:46:19 -0600487 out = self.process_line(line)
Simon Glass0d24de92012-01-14 15:12:45 +0000488
489 # Try to detect blank lines at EOF
490 for line in out:
491 match = re_fname.match(line)
492 if match:
493 last_fname = fname
494 fname = match.group(1)
495 if line == '+':
496 self.blank_count += 1
497 else:
498 if self.blank_count and (line == '-- ' or match):
499 self.warn.append("Found possible blank line(s) at "
Simon Glassd06e55a2020-10-29 21:46:17 -0600500 "end of file '%s'" % last_fname)
Simon Glass0d24de92012-01-14 15:12:45 +0000501 outfd.write('+\n' * self.blank_count)
502 outfd.write(line + '\n')
503 self.blank_count = 0
Simon Glassd93720e2020-10-29 21:46:19 -0600504 self.finalise()
Simon Glass0d24de92012-01-14 15:12:45 +0000505
506
Simon Glassd93720e2020-10-29 21:46:19 -0600507def get_metadata_for_list(commit_range, git_dir=None, count=None,
508 series=None, allow_overwrite=False):
Simon Glasse62f9052012-12-15 10:42:06 +0000509 """Reads out patch series metadata from the commits
510
511 This does a 'git log' on the relevant commits and pulls out the tags we
512 are interested in.
513
514 Args:
515 commit_range: Range of commits to count (e.g. 'HEAD..base')
516 git_dir: Path to git repositiory (None to use default)
517 count: Number of commits to list, or None for no limit
518 series: Series object to add information into. By default a new series
519 is started.
Simon Glass950a2312014-09-05 19:00:23 -0600520 allow_overwrite: Allow tags to overwrite an existing tag
Simon Glasse62f9052012-12-15 10:42:06 +0000521 Returns:
522 A Series object containing information about the commits.
523 """
Simon Glass891b7a02014-09-05 19:00:19 -0600524 if not series:
525 series = Series()
Simon Glass950a2312014-09-05 19:00:23 -0600526 series.allow_overwrite = allow_overwrite
Simon Glass9ad96982016-03-06 19:45:33 -0700527 params = gitutil.LogCmd(commit_range, reverse=True, count=count,
Simon Glasscda2a612014-08-09 15:33:10 -0600528 git_dir=git_dir)
529 stdout = command.RunPipe([params], capture=True).stdout
Simon Glasse62f9052012-12-15 10:42:06 +0000530 ps = PatchStream(series, is_log=True)
531 for line in stdout.splitlines():
Simon Glassd93720e2020-10-29 21:46:19 -0600532 ps.process_line(line)
533 ps.finalise()
Simon Glasse62f9052012-12-15 10:42:06 +0000534 return series
535
Simon Glassd93720e2020-10-29 21:46:19 -0600536def get_metadata(branch, start, count):
Simon Glass0d24de92012-01-14 15:12:45 +0000537 """Reads out patch series metadata from the commits
538
539 This does a 'git log' on the relevant commits and pulls out the tags we
540 are interested in.
541
542 Args:
Simon Glass262130f2020-07-05 21:41:51 -0600543 branch: Branch to use (None for current branch)
544 start: Commit to start from: 0=branch HEAD, 1=next one, etc.
Simon Glass0d24de92012-01-14 15:12:45 +0000545 count: Number of commits to list
546 """
Simon Glassd93720e2020-10-29 21:46:19 -0600547 return get_metadata_for_list(
548 '%s~%d' % (branch if branch else 'HEAD', start), None, count)
Simon Glass0d24de92012-01-14 15:12:45 +0000549
Simon Glassd93720e2020-10-29 21:46:19 -0600550def get_metadata_for_test(text):
Simon Glass6e87ae12017-05-29 15:31:31 -0600551 """Process metadata from a file containing a git log. Used for tests
552
553 Args:
554 text:
555 """
556 series = Series()
557 ps = PatchStream(series, is_log=True)
558 for line in text.splitlines():
Simon Glassd93720e2020-10-29 21:46:19 -0600559 ps.process_line(line)
560 ps.finalise()
Simon Glass6e87ae12017-05-29 15:31:31 -0600561 return series
562
Simon Glassd93720e2020-10-29 21:46:19 -0600563def fix_patch(backup_dir, fname, series, commit):
Simon Glass0d24de92012-01-14 15:12:45 +0000564 """Fix up a patch file, by adding/removing as required.
565
566 We remove our tags from the patch file, insert changes lists, etc.
567 The patch file is processed in place, and overwritten.
568
569 A backup file is put into backup_dir (if not None).
570
571 Args:
572 fname: Filename to patch file to process
573 series: Series information about this patch set
574 commit: Commit object for this patch file
575 Return:
576 A list of errors, or [] if all ok.
577 """
578 handle, tmpname = tempfile.mkstemp()
Simon Glass272cd852019-10-31 07:42:51 -0600579 outfd = os.fdopen(handle, 'w', encoding='utf-8')
580 infd = open(fname, 'r', encoding='utf-8')
Simon Glass0d24de92012-01-14 15:12:45 +0000581 ps = PatchStream(series)
582 ps.commit = commit
Simon Glassd93720e2020-10-29 21:46:19 -0600583 ps.process_stream(infd, outfd)
Simon Glass0d24de92012-01-14 15:12:45 +0000584 infd.close()
585 outfd.close()
586
587 # Create a backup file if required
588 if backup_dir:
589 shutil.copy(fname, os.path.join(backup_dir, os.path.basename(fname)))
590 shutil.move(tmpname, fname)
591 return ps.warn
592
Simon Glassd93720e2020-10-29 21:46:19 -0600593def fix_patches(series, fnames):
Simon Glass0d24de92012-01-14 15:12:45 +0000594 """Fix up a list of patches identified by filenames
595
596 The patch files are processed in place, and overwritten.
597
598 Args:
599 series: The series object
600 fnames: List of patch files to process
601 """
602 # Current workflow creates patches, so we shouldn't need a backup
603 backup_dir = None #tempfile.mkdtemp('clean-patch')
604 count = 0
605 for fname in fnames:
606 commit = series.commits[count]
607 commit.patch = fname
Douglas Anderson833e4192019-09-27 09:23:56 -0700608 commit.count = count
Simon Glassd93720e2020-10-29 21:46:19 -0600609 result = fix_patch(backup_dir, fname, series, commit)
Simon Glass0d24de92012-01-14 15:12:45 +0000610 if result:
Paul Burtona920a172016-09-27 16:03:50 +0100611 print('%d warnings for %s:' % (len(result), fname))
Simon Glass0d24de92012-01-14 15:12:45 +0000612 for warn in result:
Paul Burtona920a172016-09-27 16:03:50 +0100613 print('\t', warn)
Simon Glass0d24de92012-01-14 15:12:45 +0000614 print
615 count += 1
Paul Burtona920a172016-09-27 16:03:50 +0100616 print('Cleaned %d patches' % count)
Simon Glass0d24de92012-01-14 15:12:45 +0000617
Simon Glassd93720e2020-10-29 21:46:19 -0600618def insert_cover_letter(fname, series, count):
Simon Glass0d24de92012-01-14 15:12:45 +0000619 """Inserts a cover letter with the required info into patch 0
620
621 Args:
622 fname: Input / output filename of the cover letter file
623 series: Series object
624 count: Number of patches in the series
625 """
626 fd = open(fname, 'r')
627 lines = fd.readlines()
628 fd.close()
629
630 fd = open(fname, 'w')
631 text = series.cover
632 prefix = series.GetPatchPrefix()
633 for line in lines:
634 if line.startswith('Subject:'):
Wu, Josh35ce2dc2015-04-03 10:51:17 +0800635 # if more than 10 or 100 patches, it should say 00/xx, 000/xxx, etc
636 zero_repeat = int(math.log10(count)) + 1
637 zero = '0' * zero_repeat
638 line = 'Subject: [%s %s/%d] %s\n' % (prefix, zero, count, text[0])
Simon Glass0d24de92012-01-14 15:12:45 +0000639
640 # Insert our cover letter
641 elif line.startswith('*** BLURB HERE ***'):
642 # First the blurb test
643 line = '\n'.join(text[1:]) + '\n'
644 if series.get('notes'):
645 line += '\n'.join(series.notes) + '\n'
646
647 # Now the change list
648 out = series.MakeChangeLog(None)
649 line += '\n' + '\n'.join(out)
650 fd.write(line)
651 fd.close()