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