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