diff options
-rw-r--r-- | b4/__init__.py | 52 | ||||
-rw-r--r-- | b4/command.py | 2 | ||||
-rw-r--r-- | b4/ez.py | 136 |
3 files changed, 153 insertions, 37 deletions
diff --git a/b4/__init__.py b/b4/__init__.py index f54a389..4414b85 100644 --- a/b4/__init__.py +++ b/b4/__init__.py @@ -168,7 +168,7 @@ class LoreMailbox: return '\n'.join(out) - def get_by_msgid(self, msgid): + def get_by_msgid(self, msgid: str) -> Optional['LoreMessage']: if msgid in self.msgid_map: return self.msgid_map[msgid] return None @@ -235,7 +235,7 @@ class LoreMailbox: lser.subject = pser.subject logger.debug('Reconstituted successfully') - def get_series(self, revision=None, sloppytrailers=False, reroll=True): + def get_series(self, revision=None, sloppytrailers=False, reroll=True) -> Optional['LoreSeries']: if revision is None: if not len(self.series): return None @@ -346,7 +346,7 @@ class LoreMailbox: return lser - def add_message(self, msg): + def add_message(self, msg: email.message.Message) -> None: msgid = LoreMessage.get_clean_msgid(msg) if msgid and msgid in self.msgid_map: logger.debug('Already have a message with this msgid, skipping %s', msgid) @@ -411,17 +411,26 @@ class LoreMailbox: class LoreSeries: - def __init__(self, revision, expected): + revision: int + expected: int + patches: List[Optional['LoreMessage']] + followups: List['LoreMessage'] + trailer_mismatches: Set[Tuple[str, str, str, str]] + complete: bool = False + has_cover: bool = False + partial_reroll: bool = False + subject: str + indexes: Optional[List[Tuple[str, str]]] = None + base_commit: Optional[str] = None + change_id: Optional[str] = None + + def __init__(self, revision: int, expected: int) -> None: self.revision = revision self.expected = expected self.patches = [None] * (expected+1) self.followups = list() self.trailer_mismatches = set() - self.complete = False - self.has_cover = False - self.partial_reroll = False self.subject = '(untitled)' - self.indexes = None def __repr__(self): out = list() @@ -430,6 +439,8 @@ class LoreSeries: out.append(' expected: %s' % self.expected) out.append(' complete: %s' % self.complete) out.append(' has_cover: %s' % self.has_cover) + out.append(' base_commit: %s' % self.base_commit) + out.append(' change_id: %s' % self.change_id) out.append(' partial_reroll: %s' % self.partial_reroll) out.append(' patches:') at = 0 @@ -442,7 +453,7 @@ class LoreSeries: return '\n'.join(out) - def add_patch(self, lmsg): + def add_patch(self, lmsg: 'LoreMessage') -> None: while len(self.patches) < lmsg.expected + 1: self.patches.append(None) self.expected = lmsg.expected @@ -456,14 +467,23 @@ class LoreSeries: else: self.patches[lmsg.counter] = lmsg self.complete = not (None in self.patches[1:]) + if lmsg.counter == 0: + # This is a cover letter + if '\nbase-commit:' in lmsg.body: + matches = re.search(r'^base-commit: .*?([\da-f]+)', lmsg.body, flags=re.I | re.M) + if matches: + self.base_commit = matches.groups()[0] + if '\nchange-id:' in lmsg.body: + matches = re.search(r'^change-id:\s+(\S+)', lmsg.body, flags=re.I | re.M) + if matches: + self.change_id = matches.groups()[0] + if self.patches[0] is not None: - # noinspection PyUnresolvedReferences self.subject = self.patches[0].subject elif self.patches[1] is not None: - # noinspection PyUnresolvedReferences self.subject = self.patches[1].subject - def get_slug(self, extended=False): + def get_slug(self, extended: bool = False) -> str: # Find the first non-None entry lmsg = None for lmsg in self.patches: @@ -499,7 +519,7 @@ class LoreSeries: self.add_extra_trailers(self.patches[0].followup_trailers) # noqa def get_am_ready(self, noaddtrailers=False, covertrailers=False, addmysob=False, addlink=False, - linkmask=None, cherrypick=None, copyccs=False, allowbadchars=False) -> list: + linkmask=None, cherrypick=None, copyccs=False, allowbadchars=False) -> List[email.message.Message]: usercfg = get_user_config() config = get_main_config() @@ -1896,8 +1916,10 @@ class LoreSubject: subject = re.sub(r'^\s*\[[^]]*]\s*', '', subject) self.subject = subject - def get_slug(self, sep='_'): - unsafe = '%04d%s%s' % (self.counter, sep, self.subject) + def get_slug(self, sep='_', with_counter: bool = True): + unsafe = self.subject + if with_counter: + unsafe = '%04d%s%s' % (self.counter, sep, unsafe) return re.sub(r'\W+', sep, unsafe).strip(sep).lower() def __repr__(self): diff --git a/b4/command.py b/b4/command.py index 94ba875..5cd4425 100644 --- a/b4/command.py +++ b/b4/command.py @@ -265,6 +265,8 @@ def cmd(): help='Create a new branch for working on a patch series') ag_prepn.add_argument('-f', '--fork-point', dest='fork_point', help='When creating a new branch, use this fork point instead of HEAD') + ag_prepn.add_argument('-F', '--from-thread', metavar='MSGID', dest='msgid', + help='When creating a new branch, use this thread') ag_prepe = sp_prep.add_argument_group('Enroll existing branch', 'Enroll existing branch for prep work') ag_prepe.add_argument('-e', '--enroll', dest='enroll_base', help='Enroll current branch, using the passed tag, branch, or commit as fork base') @@ -23,6 +23,7 @@ import pathlib import base64 import textwrap import gzip +import io from typing import Optional, Tuple, List from email import utils @@ -229,9 +230,76 @@ def start_new_series(cmdargs: argparse.Namespace) -> None: logger.critical('CRITICAL: Unable to add your Signed-off-by: git returned no user.name or user.email') sys.exit(1) + cover = tracking = patches = thread_msgid = revision = None + if cmdargs.msgid: + msgid = b4.get_msgid(cmdargs) + list_msgs = b4.get_pi_thread_by_msgid(msgid) + if not list_msgs: + logger.critical('CRITICAL: no messages in the thread') + sys.exit(1) + lmbx = b4.LoreMailbox() + for msg in list_msgs: + lmbx.add_message(msg) + lser = lmbx.get_series() + if lser.has_cover: + cmsg = lser.patches[0] + b64tracking = cmsg.msg.get('x-b4-tracking') + if b64tracking: + logger.debug('Found x-b4-tracking header, attempting to restore') + try: + ztracking = base64.b64decode(b64tracking) + btracking = gzip.decompress(ztracking) + tracking = json.loads(btracking.decode()) + logger.debug('tracking: %s', tracking) + cover_sections = list() + diffstatre = re.compile(r'^\s*\d+ file.*\d+ (insertion|deletion)', flags=re.M | re.I) + for section in cmsg.body.split('\n---\n'): + # we stop caring once we see a diffstat + if diffstatre.search(section): + break + cover_sections.append(section) + cover = '\n---\n'.join(cover_sections).strip() + except Exception as ex: # noqa + logger.critical('CRITICAL: unable to restore tracking information, ignoring') + logger.critical(' %s', ex) + + else: + thread_msgid = msgid + + if not cover: + logger.debug('Unrecognized cover letter format, will use as-is') + cover = cmsg.body + + cover = (f'{cmsg.subject}\n\n' + f'EDITME: Imported from f{msgid}\n' + f' Please review before sending.\n\n') + cover + + change_id = lser.change_id + if not cmdargs.new_series_name: + if change_id: + cchunks = change_id.split('-') + if len(cchunks) > 2: + cmdargs.new_series_name = '-'.join(cchunks[1:-1]) + else: + slug = cmsg.lsubject.get_slug(with_counter=False) + # If it's longer than 30 chars, use first 3 words + if len(slug) > 30: + slug = '_'.join(slug.split('_')[:3]) + cmdargs.new_series_name = slug + + base_commit = lser.base_commit + if base_commit and not cmdargs.fork_point: + logger.debug('Using %s as fork-point', base_commit) + cmdargs.fork_point = base_commit + + # We start with next revision + revision = lser.revision + 1 + # Do or don't add follow-up trailers? Don't add for now, let them run b4 trailers -u. + patches = lser.get_am_ready(noaddtrailers=True) + logger.info('---') + mybranch = b4.git_get_current_branch() strategy = get_cover_strategy() - cover = None cherry_range = None if cmdargs.new_series_name: basebranch = None @@ -378,15 +446,21 @@ def start_new_series(cmdargs: argparse.Namespace) -> None: cover = '\n'.join(cover) logger.info('Created the default cover letter, you can edit with --edit-cover.') - # We don't need all the entropy of uuid, just some of it - changeid = '%s-%s-%s' % (datetime.date.today().strftime('%Y%m%d'), slug, uuid.uuid4().hex[:12]) - tracking = { - 'series': { - 'revision': 1, - 'change-id': changeid, - 'base-branch': basebranch, - }, - } + if not tracking: + # We don't need all the entropy of uuid, just some of it + changeid = '%s-%s-%s' % (datetime.date.today().strftime('%Y%m%d'), slug, uuid.uuid4().hex[:12]) + if revision is None: + revision = 1 + tracking = { + 'series': { + 'revision': revision, + 'change-id': changeid, + 'base-branch': basebranch, + }, + } + if thread_msgid: + tracking['series']['from-thread'] = thread_msgid + store_cover(cover, tracking, new=True) if cherry_range: gitargs = ['cherry-pick', cherry_range] @@ -396,6 +470,20 @@ def start_new_series(cmdargs: argparse.Namespace) -> None: logger.critical('Could not cherry-pick commits from range %s', cherry_range) sys.exit(1) + if patches: + logger.info('Applying %s patches', len(patches)) + logger.info('---') + ifh = io.StringIO() + b4.save_git_am_mbox(patches, ifh) + ambytes = ifh.getvalue().encode() + ecode, out = b4.git_run_command(None, ['am'], stdin=ambytes, logstderr=True) + logger.info(out.strip()) + if ecode > 0: + logger.critical('Could not apply patches from thread: %s', out) + sys.exit(ecode) + logger.info('---') + logger.info('NOTE: any follow-up trailers were ignored; apply them with b4 trailers -u') + def make_magic_json(data: dict) -> str: mj = (f'{MAGIC_MARKER}\n' @@ -624,6 +712,7 @@ def update_trailers(cmdargs: argparse.Namespace) -> None: end = 'HEAD' cover, tracking = load_cover(strip_comments=True) changeid = tracking['series'].get('change-id') + msgid = tracking['series'].get('from-thread') strategy = get_cover_strategy() if strategy in {'commit', 'tip-commit'}: # We need to me sure we ignore the cover commit @@ -632,6 +721,7 @@ def update_trailers(cmdargs: argparse.Namespace) -> None: ignore_commits = {cover_commit} elif cmdargs.msgid: + msgid = b4.get_msgid(cmdargs) changeid = None myemail = usercfg['email'] # There doesn't appear to be a great way to find the first commit @@ -683,16 +773,18 @@ def update_trailers(cmdargs: argparse.Namespace) -> None: by_subject[subject] = commit by_patchid[patchid] = commit - if cmdargs.msgid: - msgid = b4.get_msgid(cmdargs) - logger.info('Retrieving thread matching %s', msgid) - list_msgs = b4.get_pi_thread_by_msgid(msgid, nocache=True) - elif changeid: + list_msgs = list() + if changeid: logger.info('Checking change-id "%s"', changeid) query = f'"change-id: {changeid}"' - list_msgs = b4.get_pi_search_results(query, nocache=True) - else: - list_msgs = None + smsgs = b4.get_pi_search_results(query, nocache=True) + if smsgs is not None: + list_msgs += smsgs + if msgid: + logger.info('Retrieving thread matching %s', msgid) + tmsgs = b4.get_pi_thread_by_msgid(msgid, nocache=True) + if tmsgs is not None: + list_msgs += tmsgs if list_msgs: bbox = b4.LoreMailbox() @@ -727,10 +819,10 @@ def update_trailers(cmdargs: argparse.Namespace) -> None: updates[commit].append(fltr) # Check if we've applied mismatched trailers already if not cmdargs.sloppytrailers and mismatches: - for mltr in list(mismatches): - if mltr in parts[2]: - logger.debug('Removing already-applied mismatch %s', check) - mismatches.remove(mltr) + for mismatch in list(mismatches): + if b4.LoreTrailer(name=mismatch[0], value=mismatch[1]) in parts[2]: + logger.debug('Removing already-applied mismatch %s', mismatch[0]) + mismatches.remove(mismatch) if len(mismatches): logger.critical('---') |