"Das U-Boot" Source Tree
1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2011 The Chromium OS Authors.
3#
4
5"""Handles parsing a stream of commits/emails from 'git log' or other source"""
6
7import collections
8import datetime
9import io
10import math
11import os
12import re
13import queue
14import shutil
15import tempfile
16
17from patman import commit
18from patman import gitutil
19from patman.series import Series
20from u_boot_pylib import command
21
22# Tags that we detect and remove
23RE_REMOVE = re.compile(r'^BUG=|^TEST=|^BRANCH=|^Review URL:'
24 r'|Reviewed-on:|Commit-\w*:')
25
26# Lines which are allowed after a TEST= line
27RE_ALLOWED_AFTER_TEST = re.compile('^Signed-off-by:')
28
29# Signoffs
30RE_SIGNOFF = re.compile('^Signed-off-by: *(.*)')
31
32# Cover letter tag
33RE_COVER = re.compile('^Cover-([a-z-]*): *(.*)')
34
35# Patch series tag
36RE_SERIES_TAG = re.compile('^Series-([a-z-]*): *(.*)')
37
38# Change-Id will be used to generate the Message-Id and then be stripped
39RE_CHANGE_ID = re.compile('^Change-Id: *(.*)')
40
41# Commit series tag
42RE_COMMIT_TAG = re.compile('^Commit-([a-z-]*): *(.*)')
43
44# Commit tags that we want to collect and keep
45RE_TAG = re.compile('^(Tested-by|Acked-by|Reviewed-by|Patch-cc|Fixes): (.*)')
46
47# The start of a new commit in the git log
48RE_COMMIT = re.compile('^commit ([0-9a-f]*)$')
49
50# We detect these since checkpatch doesn't always do it
51RE_SPACE_BEFORE_TAB = re.compile(r'^[+].* \t')
52
53# Match indented lines for changes
54RE_LEADING_WHITESPACE = re.compile(r'^\s')
55
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
62# Detect line with invalid TAG
63RE_INV_TAG = re.compile('^Serie-([a-z-]*): *(.*)')
64
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
71
72class 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 """
80 def __init__(self, series, is_log=False, keep_change_id=False):
81 self.skip_blank = False # True to skip a single blank line
82 self.found_test = False # Found a TEST= line
83 self.lines_after_test = 0 # Number of lines found after TEST=
84 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
90 self.keep_change_id = keep_change_id # True to keep Change-Id tags
91 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
93 self.change_lines = [] # Lines of the current change
94 self.blank_count = 0 # Number of blank lines stored up
95 self.state = STATE_MSG_HEADER # What state are we in?
96 self.commit = None # Current commit
97 # List of unquoted test blocks, each a list of str lines
98 self.snippets = []
99 self.cur_diff = None # Last 'diff' line seen (str)
100 self.cur_line = None # Last context (@@) line seen (str)
101 self.recent_diff = None # 'diff' line for current snippet (str)
102 self.recent_line = None # '@@' line for current snippet (str)
103 self.recent_quoted = collections.deque([], 5)
104 self.recent_unquoted = queue.Queue()
105 self.was_quoted = None
106
107 @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
129 def _add_warn(self, warn):
130 """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.
133
134 Args:
135 warn (str): Warning to report
136
137 Raises:
138 ValueError: Warning is generated with no commit associated
139 """
140 if not self.commit:
141 print('Warning outside commit: %s' % warn)
142 elif warn not in self.commit.warn:
143 self.commit.warn.append(warn)
144
145 def _add_to_series(self, line, name, value):
146 """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:
152 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: ')
156 """
157 if name == 'notes':
158 self.in_section = name
159 self.skip_blank = False
160 if self.is_log:
161 warn = self.series.AddTag(self.commit, line, name, value)
162 if warn:
163 self.commit.warn.append(warn)
164
165 def _add_to_commit(self, name):
166 """Add a new Commit-xxx tag.
167
168 When a Commit-xxx tag is detected, we come here to record it.
169
170 Args:
171 name (str): Tag name (part after 'Commit-')
172 """
173 if name == 'notes':
174 self.in_section = 'commit-' + name
175 self.skip_blank = False
176
177 def _add_commit_rtag(self, rtag_type, who):
178 """Add a response tag to the current commit
179
180 Args:
181 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>'
184 """
185 self.commit.add_rtag(rtag_type, who)
186
187 def _close_commit(self):
188 """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
192 # 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 = []
200
201 self.cur_diff = None
202 self.recent_diff = None
203 self.recent_line = None
204
205 def _parse_version(self, value, line):
206 """Parse a version from a *-changes tag
207
208 Args:
209 value (str): Tag value (part after 'xxx-changes: '
210 line (str): Source line containing tag
211
212 Returns:
213 int: The version as an integer
214
215 Raises:
216 ValueError: the value cannot be converted
217 """
218 try:
219 return int(value)
220 except ValueError:
221 raise ValueError("%s: Cannot decode version info '%s'" %
222 (self.commit.hash, line))
223
224 def _finalise_change(self):
225 """_finalise a (multi-line) change and add it to the series or commit"""
226 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':
235 self.commit.add_change(self.change_version, change)
236 self.change_lines = []
237
238 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
279 def process_line(self, line):
280 """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:
298 line (str): text line to process
299
300 Returns:
301 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
306 """
307 # Initially we have no output. Prepare the input line string
308 out = []
309 line = line.rstrip('\n')
310
311 commit_match = RE_COMMIT.match(line) if self.is_log else None
312
313 if self.is_log:
314 if line[:4] == ' ':
315 line = line[4:]
316
317 # Handle state transition and skipping blank lines
318 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)
324 diff_match = RE_DIFF.match(line)
325 line_match = RE_LINE.match(line)
326 invalid_match = RE_INV_TAG.match(line)
327 tag_match = None
328 if self.state == STATE_PATCH_HEADER:
329 tag_match = RE_TAG.match(line)
330 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
343 # If a tag is detected, or a new commit starts
344 if series_tag_match or commit_tag_match or change_id_match or \
345 cover_match or signoff_match or self.state == STATE_MSG_HEADER:
346 # but we are already in a section, this means 'END' is missing
347 # for that section, fix it up.
348 if self.in_section:
349 self._add_warn("Missing 'END' in section '%s'" % self.in_section)
350 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:
359 # This should not happen
360 raise ValueError("Unknown section '%s'" % self.in_section)
361 self.in_section = None
362 self.skip_blank = True
363 self.section = []
364 # but we are already in a change list, that means a blank line
365 # is missing, fix it up.
366 if self.in_change:
367 self._add_warn("Missing 'blank line' in section '%s-changes'" %
368 self.in_change)
369 self._finalise_change()
370 self.in_change = None
371 self.change_version = 0
372
373 # 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
381 elif self.in_section == 'commit-notes':
382 if self.is_log:
383 self.commit.notes += self.section
384 else:
385 # This should not happen
386 raise ValueError("Unknown section '%s'" % self.in_section)
387 self.in_section = None
388 self.skip_blank = True
389 self.section = []
390 else:
391 self.section.append(line)
392
393 # If we are not in a section, it is an unexpected END
394 elif line == 'END':
395 raise ValueError("'END' wihout section")
396
397 # 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
402 elif RE_REMOVE.match(line) and not commit_tag_match:
403 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
412 # Detect Cover-xxx tags
413 elif cover_match:
414 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':
420 self._add_to_series(line, 'cover-cc', value)
421 elif name == 'changes':
422 self.in_change = 'Cover'
423 self.change_version = self._parse_version(value, line)
424
425 # 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
429 self._finalise_change()
430 self.in_change = None
431 self.change_version = 0
432 elif line == '---':
433 self._finalise_change()
434 self.in_change = None
435 self.change_version = 0
436 out = self.process_line(line)
437 elif self.is_log:
438 if not leading_whitespace_match:
439 self._finalise_change()
440 self.change_lines.append(line)
441 self.skip_blank = False
442
443 # Detect Series-xxx tags
444 elif series_tag_match:
445 name = series_tag_match.group(1)
446 value = series_tag_match.group(2)
447 if name == 'changes':
448 # value is the version number: e.g. 1, or 2
449 self.in_change = 'Series'
450 self.change_version = self._parse_version(value, line)
451 else:
452 self._add_to_series(line, name, value)
453 self.skip_blank = True
454
455 # Detect Change-Id tags
456 elif change_id_match:
457 if self.keep_change_id:
458 out = [line]
459 value = change_id_match.group(1)
460 if self.is_log:
461 if self.commit.change_id:
462 raise ValueError(
463 "%s: Two Change-Ids: '%s' vs. '%s'" %
464 (self.commit.hash, self.commit.change_id, value))
465 self.commit.change_id = value
466 self.skip_blank = True
467
468 # 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':
473 self._add_to_commit(name)
474 self.skip_blank = True
475 elif name == 'changes':
476 self.in_change = 'Commit'
477 self.change_version = self._parse_version(value, line)
478 elif name == 'cc':
479 self.commit.add_cc(value.split(','))
480 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)
485 else:
486 self._add_warn('Line %d: Ignoring Commit-%s' %
487 (self.linenum, name))
488
489 # Detect invalid tags
490 elif invalid_match:
491 raise ValueError("Line %d: Invalid tag = '%s'" %
492 (self.linenum, line))
493
494 # Detect the start of a new commit
495 elif commit_match:
496 self._close_commit()
497 self.commit = commit.Commit(commit_match.group(1))
498
499 # Detect tags in the commit message
500 elif tag_match:
501 rtag_type, who = tag_match.groups()
502 self._add_commit_rtag(rtag_type, who)
503 # Remove Tested-by self, since few will take much notice
504 if (rtag_type == 'Tested-by' and
505 who.find(os.getenv('USER') + '@') != -1):
506 self._add_warn("Ignoring '%s'" % line)
507 elif rtag_type == 'Patch-cc':
508 self.commit.add_cc(who.split(','))
509 else:
510 out = [line]
511
512 # Suppress duplicate signoffs
513 elif signoff_match:
514 if (self.is_log or not self.commit or
515 self.commit.check_duplicate_signoff(signoff_match.group(1))):
516 out = [line]
517
518 # Well that means this is an ordinary line
519 else:
520 # Look for space before tab
521 mat = RE_SPACE_BEFORE_TAB.match(line)
522 if mat:
523 self._add_warn('Line %d/%d has space before tab' %
524 (self.linenum, mat.start()))
525
526 # OK, we have a valid non-blank line
527 out = [line]
528 self.linenum += 1
529 self.skip_blank = False
530
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
551 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
559 # Output the tags (signoff first), then change list
560 out = []
561 log = self.series.MakeChangeLog(self.commit)
562 out += [line]
563 if self.commit:
564 out += self.commit.notes
565 out += [''] + log
566 elif self.found_test:
567 if not RE_ALLOWED_AFTER_TEST.match(line):
568 self.lines_after_test += 1
569
570 return out
571
572 def finalise(self):
573 """Close out processing of this patch stream"""
574 self._finalise_snippet()
575 self._finalise_change()
576 self._close_commit()
577 if self.lines_after_test:
578 self._add_warn('Found %d lines after TEST=' % self.lines_after_test)
579
580 def _write_message_id(self, outfd):
581 """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:
587 outfd (io.IOBase): Output stream file object
588 """
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'])
610 if 'postfix' in self.series:
611 parts.append(self.series['postfix'])
612 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
623 def process_stream(self, infd, outfd):
624 """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:
629 infd (io.IOBase): Input stream file object
630 outfd (io.IOBase): Output stream file object
631 """
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/.*')
636
637 self._write_message_id(outfd)
638
639 while True:
640 line = infd.readline()
641 if not line:
642 break
643 out = self.process_line(line)
644
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):
655 self._add_warn("Found possible blank line(s) at end of file '%s'" %
656 last_fname)
657 outfd.write('+\n' * self.blank_count)
658 outfd.write(line + '\n')
659 self.blank_count = 0
660 self.finalise()
661
662def 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
678 emit_blank = False
679 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
689 emit_blank = not (signoff_match or tag_match)
690 else:
691 emit_blank = line
692 out.append(line)
693 if not done:
694 if emit_blank:
695 out.append('')
696 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 """
712 params = gitutil.log_cmd(commit_range, reverse=True, count=count,
713 git_dir=git_dir)
714 return command.run_pipe([params], capture=True).stdout
715
716def get_metadata_for_list(commit_range, git_dir=None, count=None,
717 series=None, allow_overwrite=False):
718 """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:
724 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
728 is started.
729 allow_overwrite (bool): Allow tags to overwrite an existing tag
730
731 Returns:
732 Series: Object containing information about the commits.
733 """
734 if not series:
735 series = Series()
736 series.allow_overwrite = allow_overwrite
737 stdout = get_list(commit_range, git_dir, count)
738 pst = PatchStream(series, is_log=True)
739 for line in stdout.splitlines():
740 pst.process_line(line)
741 pst.finalise()
742 return series
743
744def get_metadata(branch, start, count):
745 """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:
751 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.
757 """
758 return get_metadata_for_list(
759 '%s~%d' % (branch if branch else 'HEAD', start), None, count)
760
761def get_metadata_for_test(text):
762 """Process metadata from a file containing a git log. Used for tests
763
764 Args:
765 text:
766
767 Returns:
768 Series: Object containing information about the commits.
769 """
770 series = Series()
771 pst = PatchStream(series, is_log=True)
772 for line in text.splitlines():
773 pst.process_line(line)
774 pst.finalise()
775 return series
776
777def fix_patch(backup_dir, fname, series, cmt, keep_change_id=False):
778 """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:
786 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
790 keep_change_id (bool): Keep the Change-Id tag.
791
792 Return:
793 list: A list of errors, each str, or [] if all ok.
794 """
795 handle, tmpname = tempfile.mkstemp()
796 outfd = os.fdopen(handle, 'w', encoding='utf-8')
797 infd = open(fname, 'r', encoding='utf-8')
798 pst = PatchStream(series, keep_change_id=keep_change_id)
799 pst.commit = cmt
800 pst.process_stream(infd, outfd)
801 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)
808 return cmt.warn
809
810def fix_patches(series, fnames, keep_change_id=False):
811 """Fix up a list of patches identified by filenames
812
813 The patch files are processed in place, and overwritten.
814
815 Args:
816 series (Series): The Series object
817 fnames (:type: list of str): List of patch files to process
818 keep_change_id (bool): Keep the Change-Id tag.
819 """
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:
824 cmt = series.commits[count]
825 cmt.patch = fname
826 cmt.count = count
827 result = fix_patch(backup_dir, fname, series, cmt,
828 keep_change_id=keep_change_id)
829 if result:
830 print('%d warning%s for %s:' %
831 (len(result), 's' if len(result) > 1 else '', fname))
832 for warn in result:
833 print('\t%s' % warn)
834 print()
835 count += 1
836 print('Cleaned %d patch%s' % (count, 'es' if count > 1 else ''))
837
838def insert_cover_letter(fname, series, count):
839 """Inserts a cover letter with the required info into patch 0
840
841 Args:
842 fname (str): Input / output filename of the cover letter file
843 series (Series): Series object
844 count (int): Number of patches in the series
845 """
846 fil = open(fname, 'r')
847 lines = fil.readlines()
848 fil.close()
849
850 fil = open(fname, 'w')
851 text = series.cover
852 prefix = series.GetPatchPrefix()
853 for line in lines:
854 if line.startswith('Subject:'):
855 # 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])
859
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)
870 fil.write(line)
871 fil.close()