aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKonstantin Ryabitsev <konstantin@linuxfoundation.org>2022-09-22 13:57:01 -0400
committerKonstantin Ryabitsev <konstantin@linuxfoundation.org>2022-09-22 13:57:01 -0400
commit6c215d83473d732cf1208c17a00a8ebc7d7526eb (patch)
tree87b1b8a4d858279a918dc91893ee968e3dc8720d
parent7eba05fd4531008bf4c453a9f73b31dee213d6ef (diff)
downloadb4-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__.py131
-rw-r--r--b4/ez.py137
-rw-r--r--docs/contributor/prep.rst12
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:
diff --git a/b4/ez.py b/b4/ez.py
index dec717e..8880935 100644
--- a/b4/ez.py
+++ b/b4/ez.py
@@ -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: