"Das U-Boot" Source Tree
at master 871 lines 32 kB view raw
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()