From e7564108c7e68094f1a6320f4166185a3e29cac1 Mon Sep 17 00:00:00 2001 From: Konstantin Ryabitsev Date: Tue, 30 Aug 2022 11:19:07 -0400 Subject: ez: reimplement --resend Instead of --resend just being a flag to add a RESEND prefix, allow us to actually resend a previously sent series using the tag we have applied and stored in sent/. E.g. if we have previously sent a v2, thus automatically rerolling a v3, we can resend a v2 again without needing to do anything else by using: b4 send --resend v2 alternatively, we can use a full tag name: b4 send --resend sent/some-series-topic-v2 Signed-off-by: Konstantin Ryabitsev --- b4/command.py | 4 +- b4/ez.py | 256 ++++++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 170 insertions(+), 90 deletions(-) diff --git a/b4/command.py b/b4/command.py index 1678d45..7eb7700 100644 --- a/b4/command.py +++ b/b4/command.py @@ -298,8 +298,8 @@ def cmd(): sp_send.add_argument('--cc', nargs='+', help='Addresses to add to the Cc: list') sp_send.add_argument('--not-me-too', action='store_true', default=False, help='Remove yourself from the To: or Cc: list') - sp_send.add_argument('--resend', action='store_true', default=False, - help='This is a resend of a previously sent series') + sp_send.add_argument('--resend', default=None, + help='Resend a previously sent version of the series') sp_send.add_argument('--no-sign', action='store_true', default=False, help='Do not cryptographically sign your patches with patatt') ag_sendh = sp_send.add_argument_group('Web submission', 'Authenticate with the web submission endpoint') diff --git a/b4/ez.py b/b4/ez.py index 2752385..8a1b8ed 100644 --- a/b4/ez.py +++ b/b4/ez.py @@ -821,6 +821,72 @@ def print_pretty_addrs(addrs: list, hdrname: str) -> None: logger.info(' %s', b4.format_addrs([addr])) +def get_sent_tag_as_patches(tagname: str, revision: Optional[str] = None, + prefixes: Optional[List[str]] = None) -> List[Tuple[str, email.message.Message]]: + gitargs = ['cat-file', '-p', tagname] + ecode, tagmsg = b4.git_run_command(None, gitargs) + if ecode > 0: + raise RuntimeError('No such tag: %s' % tagname) + # junk the headers + junk, cover = tagmsg.split('\n\n', maxsplit=1) + # Check that we have base-commit: in the body + matches = re.search(r'^base-commit:\s*(.*)$', cover, flags=re.I | re.M) + if not matches: + raise RuntimeError('Tag %s does not contain base-commit info' % tagname) + base_commit = matches.groups()[0] + matches = re.search(r'^change-id:\s*(.*)$', cover, flags=re.I | re.M) + if not matches: + raise RuntimeError('Tag %s does not contain change-id info' % tagname) + change_id = matches.groups()[0] + if revision is None: + matches = re.search(r'.*-v(\d+)$', tagname, flags=re.I | re.M) + if not matches: + raise RuntimeError('Could not grok revision number from %s' % tagname) + revision = matches.groups()[0] + + # First line is the subject + csubject, cbody = cover.split('\n', maxsplit=1) + cbody = cbody.strip() + '\n-- \n' + b4.get_email_signature() + + 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}') + seriests = int(time.time()) + msgid_tpt = make_msgid_tpt(change_id, revision) + usercfg = b4.get_user_config() + mailfrom = (usercfg.get('name'), usercfg.get('email')) + + patches = b4.git_range_to_patches(None, base_commit, tagname, + covermsg=cmsg, prefixes=prefixes, + msgid_tpt=msgid_tpt, + seriests=seriests, + thread=True, + mailfrom=mailfrom) + return patches + + +def make_msgid_tpt(change_id: str, revision: str, domain: Optional[str] = None) -> str: + if not domain: + usercfg = b4.get_user_config() + myemail = usercfg.get('email') + if myemail: + domain = re.sub(r'^[^@]*@', '', myemail) + else: + # Use the hostname of the system + import platform + domain = platform.node() + + chunks = change_id.rsplit('-', maxsplit=1) + stablepart = chunks[0] + # Message-IDs must not be predictable to avoid stuffing attacks + randompart = uuid.uuid4().hex[:12] + msgid_tpt = f'<{stablepart}-v{revision}-%s-{randompart}@{domain}>' + return msgid_tpt + + def get_prep_branch_as_patches(prefixes: Optional[List[str]] = None, movefrom: bool = True, thread: bool = True) -> List[Tuple[str, email.message.Message]]: @@ -855,27 +921,20 @@ def get_prep_branch_as_patches(prefixes: Optional[List[str]] = None, cmsg = email.message.EmailMessage() cmsg.add_header('Subject', csubject) cmsg.set_payload(body, charset='utf-8') + # Store tracking info in the header in a safe format, which should allow us to + # fully restore our work from the already sent series. + ztracking = gzip.compress(bytes(json.dumps(tracking), 'utf-8')) + b64tracking = base64.b64encode(ztracking) + cmsg.add_header('X-b4-tracking', ' '.join(textwrap.wrap(b64tracking.decode(), width=78))) if prefixes is None: prefixes = list() prefixes.append(f'v{revision}') seriests = int(time.time()) - usercfg = b4.get_user_config() - myemail = usercfg.get('email') - myname = usercfg.get('name') - if myemail: - msgdomain = re.sub(r'^[^@]*@', '', myemail) - else: - # Use the hostname of the system - import platform - msgdomain = platform.node() - chunks = change_id.rsplit('-', maxsplit=1) - stablepart = chunks[0] - # Message-IDs must not be predictable to avoid stuffing attacks - randompart = uuid.uuid4().hex[:12] - msgid_tpt = f'<{stablepart}-v{revision}-%s-{randompart}@{msgdomain}>' + msgid_tpt = make_msgid_tpt(change_id, revision) if movefrom: - mailfrom = (myname, myemail) + usercfg = b4.get_user_config() + mailfrom = (usercfg.get('name'), usercfg.get('email')) else: mailfrom = None @@ -924,38 +983,58 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: if cmdargs.auth_verify: auth_verify(cmdargs) return - # Check if the cover letter has 'EDITME' in it - cover, tracking = load_cover(strip_comments=True) - if 'EDITME' in cover: - logger.critical('CRITICAL: Looks like the cover letter needs to be edited first.') - logger.info('---') - logger.info(cover) - logger.info('---') - sys.exit(1) - - trailers = set() - parts = b4.LoreMessage.get_body_parts(cover) - trailers.update(parts[2]) + # Should we make the sent/ prefix configurable? + tagprefix = 'sent/' + mybranch = b4.git_get_current_branch() prefixes = cmdargs.prefixes if cmdargs.prefixes is None: prefixes = list() + if cmdargs.resend: + # We accept both a full tag name and just a vN short form + matches = re.search(r'^v(\d+)$', cmdargs.resend) + if matches: + revision = matches.groups()[0] + if mybranch.startswith('b4/'): + tagname = f'{tagprefix}{mybranch[3:]}-v{revision}' + else: + tagname = f'{tagprefix}{mybranch}-v{revision}' + else: + revision = None + tagname = cmdargs.resend + prefixes.append('RESEND') + try: + patches = get_sent_tag_as_patches(tagname, revision=revision, prefixes=prefixes) + except RuntimeError as ex: + logger.critical('CRITICAL: Failed to convert tag to patches: %s', ex) + sys.exit(1) - try: - patches = get_prep_branch_as_patches(prefixes=prefixes) - except RuntimeError as ex: - logger.critical('CRITICAL: Failed to convert range to patches: %s', ex) - sys.exit(1) + else: + # Check if the cover letter has 'EDITME' in it + cover, tracking = load_cover(strip_comments=True) + if 'EDITME' in cover: + logger.critical('CRITICAL: Looks like the cover letter needs to be edited first.') + logger.info('---') + logger.info(cover) + logger.info('---') + sys.exit(1) + + try: + patches = get_prep_branch_as_patches(prefixes=prefixes) + 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 patches', len(patches)-1) config = b4.get_main_config() usercfg = b4.get_user_config() myemail = usercfg.get('email') seen = set() todests = list() + trailers = set() if config.get('send-series-to'): for pair in utils.getaddresses([config.get('send-series-to')]): if pair[1] not in seen: @@ -1013,12 +1092,12 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: parts = b4.LoreMessage.get_body_parts(body) trailers.update(parts[2]) msgbytes = msg.as_bytes() - if tocmd: + if commit and tocmd: for pair in get_addresses_from_cmd(tocmd, msgbytes): if pair[1] not in seen: seen.add(pair[1]) todests.append(pair) - if cccmd: + if commit and cccmd: for pair in get_addresses_from_cmd(cccmd, msgbytes): if pair[1] not in seen: seen.add(pair[1]) @@ -1080,7 +1159,7 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: if cmdargs.no_sign or config.get('send-no-patatt-sign', '').lower() in {'yes', 'true', 'y'}: sign = False - cover_msgid = None + cover_msgid = cover_body = None # TODO: Need to send obsoleted-by follow-ups, just need to figure out where. send_msgs = list() for commit, msg in patches: @@ -1088,11 +1167,13 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: continue if cover_msgid is None: cover_msgid = b4.LoreMessage.get_clean_msgid(msg) - # Store tracking info in the header in a safe format, which should allow us to - # fully restore our work from the already sent series. - ztracking = gzip.compress(bytes(json.dumps(tracking), 'utf-8')) - b64tracking = base64.b64encode(ztracking) - msg.add_header('X-b4-tracking', ' '.join(textwrap.wrap(b64tracking.decode(), width=78))) + lsubject = b4.LoreSubject(msg.get('subject')) + cbody = msg.get_payload() + # Remove signature + chunks = cbody.rsplit('\n-- \n') + if len(chunks) > 1: + cbody = chunks[0] + '\n' + cover_body = lsubject.subject + '\n\n' + cbody msg.add_header('To', b4.format_addrs(allto)) if allcc: @@ -1135,63 +1216,62 @@ def cmd_send(cmdargs: argparse.Namespace) -> None: logger.debug('Not updating cover/tracking on resend') return - mybranch = b4.git_get_current_branch() + cover, tracking = load_cover(strip_comments=True) revision = tracking['series']['revision'] - try: - strategy = get_cover_strategy() - if strategy == 'commit': - # Detach the head at our parent commit and apply the cover-less series - cover_commit = find_cover_commit() - gitargs = ['checkout', f'{cover_commit}~1'] - ecode, out = b4.git_run_command(None, gitargs) - if ecode > 0: - raise RuntimeError('Could not switch to a detached head') - # cherry-pick from cover letter to the last commit - last_commit = patches[-1][0] - gitargs = ['cherry-pick', f'{cover_commit}..{last_commit}'] - ecode, out = b4.git_run_command(None, gitargs) - if ecode > 0: - raise RuntimeError('Could not cherry-pick the cover-less range') - # Find out the head commit - gitargs = ['rev-parse', 'HEAD'] - ecode, out = b4.git_run_command(None, gitargs) - if ecode > 0: - raise RuntimeError('Could not find the HEAD commit of the detached head') - tagcommit = out.strip() - # Switch back to our branch - gitargs = ['checkout', mybranch] - ecode, out = b4.git_run_command(None, gitargs) - if ecode > 0: - raise RuntimeError('Could not switch back to %s' % mybranch) - elif strategy == 'tip-commit': - cover_commit = find_cover_commit() - tagcommit = f'{cover_commit}~1' - else: - tagcommit = 'HEAD' + if mybranch.startswith('b4/'): + tagname = f'{tagprefix}{mybranch[3:]}-v{revision}' + else: + tagname = f'{tagprefix}{mybranch}-v{revision}' - # TODO: make sent/ prefix configurable? - tagprefix = 'sent/' - if mybranch.startswith('b4/'): - tagname = f'{tagprefix}{mybranch[3:]}-v{revision}' - else: - tagname = f'{tagprefix}{mybranch}-v{revision}' + logger.debug('checking if we already have %s', tagname) + gitargs = ['rev-parse', f'refs/tags/{tagname}'] + ecode, out = b4.git_run_command(None, gitargs) + if ecode > 0: + try: + strategy = get_cover_strategy() + if strategy == 'commit': + # Detach the head at our parent commit and apply the cover-less series + cover_commit = find_cover_commit() + gitargs = ['checkout', f'{cover_commit}~1'] + ecode, out = b4.git_run_command(None, gitargs) + if ecode > 0: + raise RuntimeError('Could not switch to a detached head') + # cherry-pick from cover letter to the last commit + last_commit = patches[-1][0] + gitargs = ['cherry-pick', f'{cover_commit}..{last_commit}'] + ecode, out = b4.git_run_command(None, gitargs) + if ecode > 0: + raise RuntimeError('Could not cherry-pick the cover-less range') + # Find out the head commit + gitargs = ['rev-parse', 'HEAD'] + ecode, out = b4.git_run_command(None, gitargs) + if ecode > 0: + raise RuntimeError('Could not find the HEAD commit of the detached head') + tagcommit = out.strip() + # Switch back to our branch + gitargs = ['checkout', mybranch] + ecode, out = b4.git_run_command(None, gitargs) + if ecode > 0: + raise RuntimeError('Could not switch back to %s' % mybranch) + elif strategy == 'tip-commit': + cover_commit = find_cover_commit() + tagcommit = f'{cover_commit}~1' + else: + tagcommit = 'HEAD' - logger.debug('checking if we already have %s', tagname) - gitargs = ['rev-parse', f'refs/tags/{tagname}'] - ecode, out = b4.git_run_command(None, gitargs) - if ecode > 0: logger.info('Tagging %s', tagname) gitargs = ['tag', '-a', '-F', '-', tagname, tagcommit] - ecode, out = b4.git_run_command(None, gitargs, stdin=cover.encode()) + ecode, out = b4.git_run_command(None, gitargs, stdin=cover_body.encode()) if ecode > 0: # Not a fatal error, just complain about it logger.info('Could not tag %s as %s:', tagcommit, tagname) logger.info(out) - else: - logger.info('NOTE: Tagname %s already exists', tagname) - except RuntimeError as ex: - logger.critical('Error tagging the revision: %s', ex) + except RuntimeError as ex: + logger.critical('Error tagging the revision: %s', ex) + + else: + logger.info('NOTE: Tagname %s already exists', tagname) if not cover_msgid: return -- cgit v1.2.3