diff options
author | Konstantin Ryabitsev <konstantin@linuxfoundation.org> | 2022-09-22 13:57:01 -0400 |
---|---|---|
committer | Konstantin Ryabitsev <konstantin@linuxfoundation.org> | 2022-09-22 13:57:01 -0400 |
commit | 6c215d83473d732cf1208c17a00a8ebc7d7526eb (patch) | |
tree | 87b1b8a4d858279a918dc91893ee968e3dc8720d | |
parent | 7eba05fd4531008bf4c453a9f73b31dee213d6ef (diff) | |
download | b4-6c215d83473d732cf1208c17a00a8ebc7d7526eb.tar.gz |
ez: don't send a cover letter for a 1-patch series
Change the behaviour of single-patch series. Instead of insisting on
sending the cover letter, mix it into the patch itself by appending it
to the below-the-cut portion of the patch.
Suggested-by: Amjad Ouled-Ameur <aouledameur@baylibre.com>
Link: https://msgid.link/3b1af982-83f1-d5ed-6df1-c654df481899@baylibre.com
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
-rw-r--r-- | b4/__init__.py | 131 | ||||
-rw-r--r-- | b4/ez.py | 137 | ||||
-rw-r--r-- | docs/contributor/prep.rst | 12 |
3 files changed, 194 insertions, 86 deletions
diff --git a/b4/__init__.py b/b4/__init__.py index 53412ec..3fc9187 100644 --- a/b4/__init__.py +++ b/b4/__init__.py @@ -864,7 +864,7 @@ class LoreTrailer: addr: Optional[Tuple[str, str]] = None lmsg = None # Small list of recognized utility trailers - _utility: Set[str] = {'fixes', 'link', 'buglink', 'obsoleted-by', 'message-id', 'change-id'} + _utility: Set[str] = {'fixes', 'link', 'buglink', 'obsoleted-by', 'message-id', 'change-id', 'base-commit'} def __init__(self, name: Optional[str] = None, value: Optional[str] = None, extinfo: Optional[str] = None, msg: Optional[email.message.Message] = None): @@ -1529,7 +1529,7 @@ class LoreMessage: def find_trailers(body: str, followup: bool = False) -> Tuple[List[LoreTrailer], List[str]]: ignores = {'phone', 'email'} headers = {'subject', 'date', 'from'} - nonperson = {'fixes', 'subject', 'date', 'link', 'buglink', 'obsoleted-by'} + nonperson = {'fixes', 'subject', 'date', 'link', 'buglink', 'obsoleted-by', 'change-id', 'base-commit'} # Ignore everything below standard email signature marker body = body.split('\n-- \n', 1)[0].strip() + '\n' # Fix some more common copypasta trailer wrapping @@ -1598,6 +1598,36 @@ class LoreMessage: return trailers, others @staticmethod + def rebuild_message(headers: List[LoreTrailer], message: str, trailers: List[LoreTrailer], + basement: str, signature: str) -> str: + body = '' + if headers: + for ltr in headers: + # There is no [extdata] in git headers, so we omit it + body += ltr.as_string(omit_extinfo=True) + '\n' + body += '\n' + + if len(message): + body += message.rstrip('\r\n') + '\n' + if len(trailers): + body += '\n' + + for ltr in trailers: + body += ltr.as_string() + '\n' + + if len(basement): + if not len(trailers): + body += '\n' + body += '---\n' + body += basement.rstrip('\r\n') + '\n' + + if len(signature): + body += '-- \n' + body += signature.rstrip('\r\n') + '\n' + + return body + + @staticmethod def get_body_parts(body: str) -> Tuple[List[LoreTrailer], str, List[LoreTrailer], str, str]: # remove any starting/trailing blank lines body = body.replace('\r', '') @@ -1774,36 +1804,23 @@ class LoreMessage: if not hasmysob: logger.info(' + %s', sobtr.as_string(omit_extinfo=True)) - # Reconstitute the message - self.body = '' - if bheaders: - for bltr in bheaders: - # There is no [extdata] in git headers, so we ignore bheader[2] - self.body += bltr.as_string(omit_extinfo=True) + '\n' - self.body += '\n' + if omit_trailers and fixtrailers: + for ltr in fixtrailers: + if ltr.lname in omit_trailers: + fixtrailers.remove(ltr) - newmessage = '' + # Build the new commit message in case we're working directly + # on the tree. + self.message = self.subject + '\n\n' if len(message): - newmessage += message.rstrip('\r\n') + '\n' + self.message = message.rstrip('\r\n') + '\n' if len(fixtrailers): - newmessage += '\n' - + self.message += '\n' if len(fixtrailers): - if omit_trailers is None: - omit_trailers = set() for ltr in fixtrailers: - if ltr.lname not in omit_trailers: - newmessage += ltr.as_string() + '\n' - - self.message = self.subject + '\n\n' + newmessage - self.body += newmessage + self.message += ltr.as_string() + '\n' - if len(basement): - self.body += '---\n' - self.body += basement.rstrip('\r\n') + '\n' - if len(signature): - self.body += '-- \n' - self.body += signature.rstrip('\r\n') + '\n' + self.body = LoreMessage.rebuild_message(bheaders, message, fixtrailers, basement, signature) def get_am_subject(self, indicate_reroll=True): # Return a clean patch subject @@ -2702,9 +2719,60 @@ def git_range_to_patches(gitdir: Optional[str], start: str, end: str, startfrom = 1 fullcount = len(patches) - patches.insert(0, (None, covermsg)) + if fullcount == 0: + raise RuntimeError(f'Could not run rev-list {start}..{end}') + if covermsg: - startfrom = 0 + if fullcount == 1: + # Single-patch series, mix in the cover letter into the patch basement + pmsg = patches[0][1] + pbody = pmsg.get_payload() + pheaders, pmessage, ptrailers, pbasement, psignature = LoreMessage.get_body_parts(pbody) + cbody = covermsg.get_payload() + cheaders, cmessage, ctrailers, cbasement, csignature = LoreMessage.get_body_parts(cbody) + nbparts = list() + nmessage = '' + if len(cmessage.strip()): + cls = LoreSubject(covermsg.get('Subject')) + nmessage += cls.subject + '\n\n' + cmessage.rstrip('\r\n') + '\n' + for ctr in list(ctrailers): + if ctr in ptrailers: + ctrailers.remove(ctr) + if ctrailers: + if nmessage: + nmessage += '\n' + for ctr in ctrailers: + nmessage += ctr.as_string() + '\n' + nbparts.append(nmessage) + + # Find the section with changelogs + utility = None + for section in cbasement.split('---\n'): + if re.search(r'^change-id: ', section, flags=re.I | re.M): + utility = section + continue + if re.search(r'^changes in v\d', section, flags=re.I | re.M): + nbparts.append(section.strip('\r\n') + '\n') + + nbparts.append(pbasement.rstrip('\r\n') + '\n\n') + if utility: + nbparts.append(utility) + + newbasement = '---\n'.join(nbparts) + + pbody = LoreMessage.rebuild_message(pheaders, pmessage, ptrailers, newbasement, csignature) + pmsg.set_payload(pbody, charset='utf-8') + # Add any To: and Cc: headers from the cover_message + toh = covermsg.get('To') + if toh: + pmsg.add_header('To', toh) + cch = covermsg.get('Cc') + if cch: + pmsg.add_header('Cc', cch) + startfrom = 0 + else: + patches.insert(0, (None, covermsg)) + startfrom = 0 # Go through and apply any outstanding fixes if prefixes: @@ -2712,11 +2780,14 @@ def git_range_to_patches(gitdir: Optional[str], start: str, end: str, else: prefixes = '' - for counter in range(startfrom, fullcount+1): + for counter in range(startfrom, len(patches)): msg = patches[counter][1] subject = msg.get('Subject') csubject = re.sub(r'^\[PATCH]\s*', '', subject) - pline = '[PATCH%s %s/%s]' % (prefixes, str(counter).zfill(len(str(fullcount))), fullcount) + if fullcount > 1: + pline = '[PATCH%s %s/%s]' % (prefixes, str(counter).zfill(len(str(fullcount))), fullcount) + else: + pline = '[PATCH%s]' % prefixes msg.replace_header('Subject', f'{pline} {csubject}') inbodyhdrs = list() if mailfrom: @@ -437,7 +437,9 @@ def start_new_series(cmdargs: argparse.Namespace) -> None: 'will be used by the project maintainer to make a decision whether your', 'patches should be reviewed, and in what priority order. Please be very', 'detailed and link to any relevant discussions or sites that the maintainer', - 'can review to better understand your proposed changes.', + 'can review to better understand your proposed changes. If you only have a', + 'single patch in your series, the contents of the cover letter will be', + 'appended to the "under-the-cut" portion of the patch.', '', '# You can add trailers to the cover letter. Any email addresses found in', '# these trailers will be added to the addresses specified/generated during', @@ -906,7 +908,9 @@ def print_pretty_addrs(addrs: list, hdrname: str) -> None: def get_sent_tag_as_patches(tagname: str, revision: Optional[str] = None, - prefixes: Optional[List[str]] = None) -> List[Tuple[str, email.message.Message]]: + prefixes: Optional[List[str]] = None, + hide_cover_to_cc: bool = False + ) -> Tuple[List, List, List[Tuple[str, email.message.Message]]]: gitargs = ['cat-file', '-p', tagname] ecode, tagmsg = b4.git_run_command(None, gitargs) if ecode > 0: @@ -931,13 +935,19 @@ def get_sent_tag_as_patches(tagname: str, revision: Optional[str] = None, # First line is the subject csubject, cbody = cover.split('\n', maxsplit=1) cbody = cbody.strip() + '\n-- \n' + b4.get_email_signature() + if hide_cover_to_cc: + tos, ccs, body = remove_to_cc_trailers(cbody) + else: + tos = list() + ccs = list() cmsg = email.message.EmailMessage() cmsg.add_header('Subject', csubject) cmsg.set_payload(cbody.lstrip(), charset='utf-8') if prefixes is None: prefixes = list() - prefixes.append(f'v{revision}') + if revision != 1: + prefixes.append(f'v{revision}') seriests = int(time.time()) msgid_tpt = make_msgid_tpt(change_id, revision) usercfg = b4.get_user_config() @@ -949,7 +959,7 @@ def get_sent_tag_as_patches(tagname: str, revision: Optional[str] = None, seriests=seriests, thread=True, mailfrom=mailfrom) - return patches + return tos, ccs, patches def make_msgid_tpt(change_id: str, revision: str, domain: Optional[str] = None) -> str: @@ -971,9 +981,25 @@ def make_msgid_tpt(change_id: str, revision: str, domain: Optional[str] = None) return msgid_tpt +def remove_to_cc_trailers(body: str) -> Tuple[List, List, str]: + htrs, cmsg, mtrs, basement, sig = b4.LoreMessage.get_body_parts(body) + tos = list() + ccs = list() + for mtr in list(mtrs): + if mtr.lname == 'to': + tos.append(mtr.addr) + mtrs.remove(mtr) + elif mtr.lname == 'cc': + ccs.append(mtr.addr) + mtrs.remove(mtr) + body = b4.LoreMessage.rebuild_message(htrs, cmsg, mtrs, basement, sig) + return tos, ccs, body + + def get_prep_branch_as_patches(prefixes: Optional[List[str]] = None, - movefrom: bool = True, - thread: bool = True) -> List[Tuple[str, email.message.Message]]: + movefrom: bool = True, thread: bool = True, + hide_cover_to_cc: bool = False + ) -> Tuple[List, List, List[Tuple[str, email.message.Message]]]: cover, tracking = load_cover(strip_comments=True) config = b4.get_main_config() cover_template = DEFAULT_COVER_TEMPLATE @@ -988,6 +1014,7 @@ def get_prep_branch_as_patches(prefixes: Optional[List[str]] = None, # Put together the cover letter csubject, cbody = cover.split('\n', maxsplit=1) + start_commit = get_series_start() base_commit, shortlog, diffstat = get_series_details(start_commit=start_commit) change_id = tracking['series'].get('change-id') @@ -1002,6 +1029,12 @@ def get_prep_branch_as_patches(prefixes: Optional[List[str]] = None, 'signature': b4.get_email_signature(), } body = Template(cover_template.lstrip()).safe_substitute(tptvals) + if hide_cover_to_cc: + tos, ccs, body = remove_to_cc_trailers(body) + else: + tos = list() + ccs = list() + cmsg = email.message.EmailMessage() cmsg.add_header('Subject', csubject) cmsg.set_payload(body, charset='utf-8') @@ -1012,8 +1045,9 @@ def get_prep_branch_as_patches(prefixes: Optional[List[str]] = None, cmsg.add_header('X-b4-tracking', ' '.join(textwrap.wrap(b64tracking.decode(), width=78))) if prefixes is None: prefixes = list() + if revision != 1: + prefixes.append(f'v{revision}') - prefixes.append(f'v{revision}') seriests = int(time.time()) msgid_tpt = make_msgid_tpt(change_id, revision) if movefrom: @@ -1036,12 +1070,12 @@ def get_prep_branch_as_patches(prefixes: Optional[List[str]] = None, thread=thread, mailfrom=mailfrom, ignore_commits=ignore_commits) - return patches + return tos, ccs, patches def format_patch(output_dir: str) -> None: try: - patches = get_prep_branch_as_patches(thread=False, movefrom=False) + tos, ccs, patches = get_prep_branch_as_patches(thread=False, movefrom=False) except RuntimeError as ex: logger.critical('CRITICAL: Failed to convert range to patches: %s', ex) sys.exit(1) @@ -1075,6 +1109,10 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: if cmdargs.prefixes is None: prefixes = list() + config = b4.get_main_config() + if not cmdargs.hide_cover_to_cc and config.get('send-hide-cover-to-cc', '').lower() in {'yes', 'true', '1'}: + cmdargs.hide_cover_to_cc = True + if cmdargs.resend: # We accept both a full tag name and just a vN short form matches = re.search(r'^v(\d+)$', cmdargs.resend) @@ -1090,11 +1128,14 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: prefixes.append('RESEND') try: - patches = get_sent_tag_as_patches(tagname, revision=revision, prefixes=prefixes) + todests, ccdests, patches = get_sent_tag_as_patches(tagname, revision=revision, prefixes=prefixes, + hide_cover_to_cc=cmdargs.hide_cover_to_cc) except RuntimeError as ex: logger.critical('CRITICAL: Failed to convert tag to patches: %s', ex) sys.exit(1) + logger.info('Converted the tag to %s messages', len(patches)) + else: # Check if the cover letter has 'EDITME' in it cover, tracking = load_cover(strip_comments=True) @@ -1106,26 +1147,50 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: sys.exit(1) try: - patches = get_prep_branch_as_patches(prefixes=prefixes) + todests, ccdests, patches = get_prep_branch_as_patches(prefixes=prefixes, + hide_cover_to_cc=cmdargs.hide_cover_to_cc) except RuntimeError as ex: logger.critical('CRITICAL: Failed to convert range to patches: %s', ex) sys.exit(1) - logger.info('Converted the branch to %s patches', len(patches)-1) + logger.info('Converted the branch to %s messages', len(patches)) - config = b4.get_main_config() usercfg = b4.get_user_config() myemail = usercfg.get('email') seen = set() - todests = list() - ccdests = list() - trailers = set() excludes = set() - # These override config values + + if cmdargs.no_trailer_to_cc: + todests = list() + ccdests = list() + else: + seen.update(todests) + seen.update(ccdests) + logger.info('Populating To/Cc addresses') + # Go through the messages to make to/cc headers + for commit, msg in patches: + if not msg: + continue + body = msg.get_payload() + btrs, junk = b4.LoreMessage.find_trailers(body) + for btr in btrs: + if btr.type != 'person': + continue + if btr.addr[1] in seen: + continue + if btr.lname == 'to': + todests.append(btr.addr) + continue + ccdests.append(btr.addr) + + excludes = b4.get_excluded_addrs() + if cmdargs.not_me_too: + excludes.add(myemail) + if cmdargs.to: - todests = [('', x) for x in cmdargs.to] + todests += [('', x) for x in cmdargs.to] seen.update(set(cmdargs.to)) - elif config.get('send-series-to'): + if config.get('send-series-to'): for pair in utils.getaddresses([config.get('send-series-to')]): if pair[1] in seen: continue @@ -1133,9 +1198,9 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: logger.debug('added %s to seen', pair[1]) todests.append(pair) if cmdargs.cc: - ccdests = [('', x) for x in cmdargs.cc] + ccdests += [('', x) for x in cmdargs.cc] seen.update(set(cmdargs.cc)) - elif config.get('send-series-cc'): + if config.get('send-series-cc'): for pair in utils.getaddresses([config.get('send-series-cc')]): if pair[1] in seen: continue @@ -1143,29 +1208,6 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: logger.debug('added %s to seen', pair[1]) ccdests.append(pair) - if not cmdargs.no_trailer_to_cc: - logger.info('Populating To/Cc addresses') - # Go through the messages to make to/cc headers - for commit, msg in patches: - if not msg: - continue - body = msg.get_payload() - parts = b4.LoreMessage.get_body_parts(body) - trailers.update(parts[2]) - - # add addresses seen in trailers - for ltr in trailers: - if ltr.addr and ltr.addr[1] not in seen: - seen.add(ltr.addr[1]) - if ltr.lname == 'to': - todests.append(ltr.addr) - else: - ccdests.append(ltr.addr) - - excludes = b4.get_excluded_addrs() - if cmdargs.not_me_too: - excludes.add(myemail) - allto = list() allcc = list() alldests = set() @@ -1222,11 +1264,6 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: continue if cover_msg is None: cover_msg = copy.deepcopy(msg) - if cmdargs.hide_cover_to_cc or config.get('send-hide-cover-to-cc', '').lower() in {'yes', 'true', '1'}: - logger.debug('Hiding To: and Cc: trailers from the cover letter') - lcm = b4.LoreMessage(msg) - lcm.fix_trailers(omit_trailers=['to', 'cc']) - msg.set_payload(lcm.body, charset='utf-8') msg.add_header('To', b4.format_addrs(allto)) if allcc: @@ -1469,7 +1506,7 @@ def auto_to_cc() -> None: logger.debug('added %s to seen', ltr.addr[1]) extras.append(ltr) - patches = get_prep_branch_as_patches() + tos, ccs, patches = get_prep_branch_as_patches() logger.info('Collecting To/Cc addresses') # Go through the messages to make to/cc headers for commit, msg in patches: diff --git a/docs/contributor/prep.rst b/docs/contributor/prep.rst index b31ed8c..61133d7 100644 --- a/docs/contributor/prep.rst +++ b/docs/contributor/prep.rst @@ -170,12 +170,12 @@ commit should be preserved through all rebase operations. What if I only have a single patch? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Even if you only have a single commit, you should still treat it as a -single-patch series and send it along with a cover letter. It is very -likely that you will be asked to make some changes to your patch or -split it into several patches. Having a cover letter will help retain -the change-id of the series across revisions, and will provide a -convenient place to keep the changelog of revisions. +When you only have a single patch, the contents of the cover letter will +be mixed into the "under-the-cut" portion of the patch. You can just use +the cover letter for extra To/Cc trailers and changelog entries as your +patch goes through revisions. If you add more commits in the future +version, you can fill in the cover letter content with additional +information about the intent of your entire series. .. _prep_recipients: |