diff options
Diffstat (limited to 'b4/mbox.py')
-rw-r--r-- | b4/mbox.py | 375 |
1 files changed, 273 insertions, 102 deletions
@@ -18,6 +18,9 @@ import fnmatch import shutil import pathlib import tempfile +import io +import shlex +import argparse import urllib.parse import xml.etree.ElementTree @@ -25,9 +28,19 @@ import xml.etree.ElementTree import b4 from typing import Optional, Tuple +from string import Template logger = b4.logger +DEFAULT_MERGE_TEMPLATE = """Merge ${patch_or_series} "${seriestitle}" + +${authorname} <${authoremail}> says: + +${covermessage} + +Link: ${midurl} +""" + def make_am(msgs, cmdargs, msgid): config = b4.get_main_config() @@ -35,7 +48,6 @@ def make_am(msgs, cmdargs, msgid): if outdir == '-': cmdargs.nocover = True wantver = cmdargs.wantver - wantname = cmdargs.wantname covertrailers = cmdargs.covertrailers count = len(msgs) logger.info('Analyzing %s messages in the thread', count) @@ -90,48 +102,12 @@ def make_am(msgs, cmdargs, msgid): try: am_msgs = lser.get_am_ready(noaddtrailers=cmdargs.noaddtrailers, - covertrailers=covertrailers, trailer_order=config['trailer-order'], - addmysob=cmdargs.addmysob, addlink=cmdargs.addlink, - linkmask=config['linkmask'], cherrypick=cherrypick, - copyccs=cmdargs.copyccs) + covertrailers=covertrailers, addmysob=cmdargs.addmysob, + addlink=cmdargs.addlink, linkmask=config['linkmask'], cherrypick=cherrypick, + copyccs=cmdargs.copyccs, allowbadchars=cmdargs.allowbadchars) except KeyError: sys.exit(1) - if cmdargs.maildir or config.get('save-maildirs', 'no') == 'yes': - save_maildir = True - dftext = 'maildir' - else: - save_maildir = False - dftext = 'mbx' - - if wantname: - slug = wantname - if wantname.find('.') > -1: - slug = '.'.join(wantname.split('.')[:-1]) - gitbranch = slug - else: - slug = lser.get_slug(extended=True) - gitbranch = lser.get_slug(extended=False) - - if outdir != '-': - am_filename = os.path.join(outdir, f'{slug}.{dftext}') - am_cover = os.path.join(outdir, f'{slug}.cover') - - if os.path.exists(am_filename): - if os.path.isdir(am_filename): - shutil.rmtree(am_filename) - else: - os.unlink(am_filename) - if save_maildir: - b4.save_maildir(am_msgs, am_filename) - else: - with open(am_filename, 'w') as fh: - b4.save_git_am_mbox(am_msgs, fh) - else: - am_filename = None - am_cover = None - b4.save_git_am_mbox(am_msgs, sys.stdout) - logger.info('---') if cherrypick is None: @@ -145,9 +121,9 @@ def make_am(msgs, cmdargs, msgid): # Only check cover letter or first patch if not lmsg or lmsg.counter > 1: continue - for trailer in list(lmsg.followup_trailers): - if trailer[0].lower() == 'obsoleted-by': - lmsg.followup_trailers.remove(trailer) + for ltr in list(lmsg.followup_trailers): + if ltr.lname == 'obsoleted-by': + lmsg.followup_trailers.remove(ltr) if warned: continue logger.critical('---') @@ -160,10 +136,10 @@ def make_am(msgs, cmdargs, msgid): logger.critical('---') logger.critical('NOTE: Some trailers were sent to the cover letter:') tseen = set() - for trailer in lser.patches[0].followup_trailers: - if tuple(trailer[:2]) not in tseen: - logger.critical(' %s: %s', trailer[0], trailer[1]) - tseen.add(tuple(trailer[:2])) + for ltr in lser.patches[0].followup_trailers: + if ltr not in tseen: + logger.critical(' %s', ltr.as_string(omit_extinfo=True)) + tseen.add(ltr) logger.critical('NOTE: Rerun with -t to apply them to all patches') if len(lser.trailer_mismatches): logger.critical('---') @@ -173,6 +149,17 @@ def make_am(msgs, cmdargs, msgid): logger.critical(' Msg From: %s <%s>', fname, femail) logger.critical('NOTE: Rerun with -S to apply them anyway') + top_msgid = None + first_body = None + for lmsg in lser.patches: + if lmsg is not None: + first_body = lmsg.body + top_msgid = lmsg.msgid + break + if top_msgid is None: + logger.critical('Could not find any patches in the series.') + return + topdir = b4.git_get_toplevel() if cmdargs.threeway: @@ -194,62 +181,225 @@ def make_am(msgs, cmdargs, msgid): if not lser.complete and not cmdargs.cherrypick: logger.critical('WARNING: Thread incomplete!') - if lser.has_cover and not cmdargs.nocover: - lser.save_cover(am_cover) + gitbranch = lser.get_slug(extended=False) + am_filename = None - top_msgid = None - first_body = None - for lmsg in lser.patches: - if lmsg is not None: - first_body = lmsg.body - top_msgid = lmsg.msgid - break - if top_msgid is None: - logger.critical('Could not find any patches in the series.') - return + if cmdargs.subcmd == 'am': + wantname = cmdargs.wantname + if cmdargs.maildir or config.get('save-maildirs', 'no') == 'yes': + save_maildir = True + dftext = 'maildir' + else: + save_maildir = False + dftext = 'mbx' + + if wantname: + slug = wantname + if wantname.find('.') > -1: + slug = '.'.join(wantname.split('.')[:-1]) + gitbranch = slug + else: + slug = lser.get_slug(extended=True) + + if outdir != '-': + am_filename = os.path.join(outdir, f'{slug}.{dftext}') + am_cover = os.path.join(outdir, f'{slug}.cover') + + if os.path.exists(am_filename): + if os.path.isdir(am_filename): + shutil.rmtree(am_filename) + else: + os.unlink(am_filename) + if save_maildir: + b4.save_maildir(am_msgs, am_filename) + else: + with open(am_filename, 'w') as fh: + b4.save_git_am_mbox(am_msgs, fh) + else: + am_cover = None + b4.save_git_am_mbox(am_msgs, sys.stdout) - linkurl = config['linkmask'] % top_msgid - if cmdargs.quiltready: - q_dirname = os.path.join(outdir, f'{slug}.patches') - save_as_quilt(am_msgs, q_dirname) - logger.critical('Quilt: %s', q_dirname) + if lser.has_cover and not cmdargs.nocover: + lser.save_cover(am_cover) - logger.critical(' Link: %s', linkurl) + linkurl = config['linkmask'] % top_msgid + if cmdargs.quiltready: + q_dirname = os.path.join(outdir, f'{slug}.patches') + save_as_quilt(am_msgs, q_dirname) + logger.critical('Quilt: %s', q_dirname) + + logger.critical(' Link: %s', linkurl) base_commit = None - matches = re.search(r'base-commit: .*?([0-9a-f]+)', first_body, re.MULTILINE) + matches = re.search(r'base-commit: .*?([\da-f]+)', first_body, re.MULTILINE) if matches: base_commit = matches.groups()[0] else: # Try a more relaxed search - matches = re.search(r'based on .*?([0-9a-f]{40})', first_body, re.MULTILINE) + matches = re.search(r'based on .*?([\da-f]{40})', first_body, re.MULTILINE) if matches: base_commit = matches.groups()[0] - if base_commit: - logger.critical(' Base: %s', base_commit) - else: - if topdir is not None: - if cmdargs.guessbase: - logger.critical(' attempting to guess base-commit...') + if not base_commit and topdir and cmdargs.guessbase: + logger.critical(' Base: attempting to guess base-commit...') + try: + base_commit, nblobs, mismatches = lser.find_base(topdir, branches=cmdargs.guessbranch, + maxdays=cmdargs.guessdays) + if mismatches == 0: + logger.critical(' Base: %s (exact match)', base_commit) + elif nblobs == mismatches: + logger.critical(' Base: failed to guess base') + else: + logger.critical(' Base: %s (best guess, %s/%s blobs matched)', base_commit, + nblobs - mismatches, nblobs) + except IndexError: + logger.critical(' Base: failed to guess base') + + if cmdargs.subcmd == 'shazam': + if not topdir: + logger.critical('Could not figure out where your git dir is, cannot shazam.') + sys.exit(1) + ifh = io.StringIO() + b4.save_git_am_mbox(am_msgs, ifh) + ambytes = ifh.getvalue().encode() + if not cmdargs.makefetchhead: + amflags = config.get('shazam-am-flags', '') + sp = shlex.shlex(amflags, posix=True) + sp.whitespace_split = True + amargs = list(sp) + ecode, out = b4.git_run_command(topdir, ['am'] + amargs, stdin=ambytes, logstderr=True) + logger.info(out.strip()) + if ecode == 0: + thanks_record_am(lser, cherrypick=cherrypick) + sys.exit(ecode) + + if not base_commit: + # Try our best with HEAD, I guess + base_commit = 'HEAD' + + with b4.git_temp_worktree(topdir, base_commit) as gwt: + logger.info('Magic: Preparing a sparse worktree') + ecode, out = b4.git_run_command(gwt, ['sparse-checkout', 'init'], logstderr=True) + if ecode > 0: + logger.critical('Error running sparse-checkout init') + logger.critical(out) + sys.exit(ecode) + ecode, out = b4.git_run_command(gwt, ['checkout'], logstderr=True) + if ecode > 0: + logger.critical('Error running checkout into sparse workdir') + logger.critical(out) + sys.exit(ecode) + ecode, out = b4.git_run_command(gwt, ['am'], stdin=ambytes, logstderr=True) + if ecode > 0: + logger.critical('Unable to cleanly apply series, see failure log below') + logger.critical('---') + logger.critical(out.strip()) + logger.critical('---') + logger.critical('Not fetching into FETCH_HEAD') + sys.exit(ecode) + logger.info('---') + logger.info(out.strip()) + logger.info('---') + logger.info('Fetching into FETCH_HEAD') + gitargs = ['fetch', gwt] + ecode, out = b4.git_run_command(topdir, gitargs, logstderr=True) + if ecode > 0: + logger.critical('Unable to fetch from the worktree') + logger.critical(out.strip()) + sys.exit(ecode) + gitargs = ['rev-parse', '--git-path', 'FETCH_HEAD'] + ecode, fhf = b4.git_run_command(topdir, gitargs, logstderr=True) + if ecode > 0: + logger.critical('Unable to find FETCH_HEAD') + logger.critical(out.strip()) + sys.exit(ecode) + with open(fhf.rstrip(), 'r') as fhh: + contents = fhh.read() + linkurl = config['linkmask'] % top_msgid + if len(am_msgs) > 1: + mmsg = 'patches from %s' % linkurl + else: + mmsg = 'patch from %s' % linkurl + new_contents = contents.replace(gwt, mmsg) + if new_contents != contents: + with open(fhf, 'w') as fhh: + fhh.write(new_contents) + + gitargs = ['rev-parse', '--git-dir'] + ecode, fhf = b4.git_run_command(topdir, gitargs, logstderr=True) + if ecode > 0: + logger.critical('Unable to find git directory') + logger.critical(out.strip()) + sys.exit(ecode) + mmf = os.path.join(fhf.rstrip(), 'b4-cover') + merge_template = DEFAULT_MERGE_TEMPLATE + if config.get('shazam-merge-template'): + # Try to load this template instead try: - base_commit, nblobs, mismatches = lser.find_base(topdir, branches=cmdargs.guessbranch, - maxdays=cmdargs.guessdays) - if mismatches == 0: - logger.critical(' Base: %s (exact match)', base_commit) - elif nblobs == mismatches: - logger.critical(' Base: failed to guess base') - else: - logger.critical(' Base: %s (best guess, %s/%s blobs matched)', base_commit, - nblobs - mismatches, nblobs) - except IndexError: - logger.critical(' Base: failed to guess base') + merge_template = b4.read_template(config['shazam-merge-template']) + except FileNotFoundError: + logger.critical('ERROR: shazam-merge-template says to use %s, but it does not exist', + config['shazam-merge-template']) + sys.exit(2) + + # Write out a sample merge message using the cover letter + if os.path.exists(mmf): + # Make sure any old cover letters don't confuse anyone + os.unlink(mmf) + + if lser.has_cover: + cmsg = lser.patches[0] + parts = b4.LoreMessage.get_body_parts(cmsg.body) + covermessage = parts[1] else: - checked, mismatches = lser.check_applies_clean(topdir, at=cmdargs.guessbranch) - if checked and len(mismatches) == 0 and checked != mismatches: - logger.critical(' Base: applies clean to current tree') - else: - logger.critical(' Base: not specified') + cmsg = lser.patches[1] + covermessage = ('NOTE: No cover letter provided by the author.\n' + ' Add merge commit message here.') + tptvals = { + 'seriestitle': cmsg.subject, + 'authorname': cmsg.fromname, + 'authoremail': cmsg.fromemail, + 'covermessage': covermessage, + 'midurl': linkurl, + } + if len(am_msgs) > 1: + tptvals['patch_or_series'] = 'patch series' + else: + tptvals['patch_or_series'] = 'patch' + + body = Template(merge_template).safe_substitute(tptvals) + with open(mmf, 'w') as mmh: + mmh.write(body) + + mergeflags = config.get('shazam-merge-flags', '--signoff') + sp = shlex.shlex(mergeflags, posix=True) + sp.whitespace_split = True + mergeargs = ['merge', '--no-ff', '-F', mmf, '--edit', 'FETCH_HEAD'] + list(sp) + mergecmd = ['git'] + mergeargs + + thanks_record_am(lser, cherrypick=cherrypick) + if cmdargs.merge: + if not cmdargs.no_interactive: + logger.info('Will exec: %s', ' '.join(mergecmd)) + try: + input('Press Enter to continue or Ctrl-C to abort') + except KeyboardInterrupt: + logger.info('') + sys.exit(130) + else: + logger.info('Invoking: %s', ' '.join(mergecmd)) + # We exec git-merge and let it take over + os.execvp(mergecmd[0], mergecmd) + + logger.info('You can now merge or checkout FETCH_HEAD') + logger.info(' e.g.: %s', ' '.join(mergecmd)) + return + + if not base_commit: + checked, mismatches = lser.check_applies_clean(topdir, at=cmdargs.guessbranch) + if checked and len(mismatches) == 0 and checked != mismatches: + logger.critical(' Base: applies clean to current tree') + base_commit = 'HEAD' else: logger.critical(' Base: not specified') @@ -257,7 +407,6 @@ def make_am(msgs, cmdargs, msgid): logger.critical(' git checkout -b %s %s', gitbranch, base_commit) if cmdargs.outdir != '-': logger.critical(' git am %s', am_filename) - thanks_record_am(lser, cherrypick=cherrypick) @@ -268,6 +417,7 @@ def thanks_record_am(lser, cherrypick=None): filename = '%s.am' % slug patches = list() + msgids = list() at = 0 padlen = len(str(lser.expected)) lmsg = None @@ -276,6 +426,7 @@ def thanks_record_am(lser, cherrypick=None): if pmsg is None: at += 1 continue + msgids.append(pmsg.msgid) if lmsg is None: lmsg = pmsg @@ -305,6 +456,7 @@ def thanks_record_am(lser, cherrypick=None): allto = email.utils.getaddresses([str(x) for x in lmsg.msg.get_all('to', [])]) allcc = email.utils.getaddresses([str(x) for x in lmsg.msg.get_all('cc', [])]) + # TODO: check for reply-to and x-original-from out = { 'msgid': lmsg.msgid, 'subject': lmsg.full_subject, @@ -323,6 +475,11 @@ def thanks_record_am(lser, cherrypick=None): json.dump(out, fh, ensure_ascii=False, indent=4) logger.debug('Wrote %s for thanks tracking', filename) + config = b4.get_main_config() + pwstate = config.get('pw-review-state') + if pwstate: + b4.patchwork_set_state(msgids, pwstate) + def save_as_quilt(am_msgs, q_dirname): if os.path.exists(q_dirname): @@ -366,12 +523,12 @@ def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = N seen_msgids.add(msgid) lsub = b4.LoreSubject(msg['Subject']) if direction > 0 and lsub.reply: - # Does it have an "Obsoleted-by: trailer? + # Does it have an Obsoleted-by: trailer? rmsg = b4.LoreMessage(msg) trailers, mismatches = rmsg.get_trailers() - for tl in trailers: - if tl[0].lower() == 'obsoleted-by': - for chunk in tl[1].split('/'): + for ltr in trailers: + if ltr.lname == 'obsoleted-by': + for chunk in ltr.value.split('/'): if chunk.find('@') > 0 and chunk not in seen_msgids: obsoleted.append(chunk) break @@ -521,7 +678,7 @@ def get_extra_series(msgs: list, direction: int = 1, wantvers: Optional[int] = N return msgs -def get_msgs(cmdargs) -> Tuple[Optional[str], Optional[list]]: +def get_msgs(cmdargs: argparse.Namespace) -> Tuple[Optional[str], Optional[list]]: msgid = None if not cmdargs.localmbox: msgid = b4.get_msgid(cmdargs) @@ -530,12 +687,9 @@ def get_msgs(cmdargs) -> Tuple[Optional[str], Optional[list]]: sys.exit(1) pickings = set() - try: - if cmdargs.cherrypick == '_': - # Just that msgid, please - pickings = {msgid} - except AttributeError: - pass + if 'cherrypick' in cmdargs and cmdargs.cherrypick == '_': + # Just that msgid, please + pickings = {msgid} msgs = b4.get_pi_thread_by_msgid(msgid, useproject=cmdargs.useproject, nocache=cmdargs.nocache, onlymsgids=pickings) if not msgs: @@ -567,6 +721,9 @@ def get_msgs(cmdargs) -> Tuple[Optional[str], Optional[list]]: logger.critical('Mailbox %s does not exist', cmdargs.localmbox) sys.exit(1) + if msgid and 'noparent' in cmdargs and cmdargs.noparent: + msgs = b4.get_strict_thread(msgs, msgid, noparent=True) + if not msgid and msgs: for msg in msgs: msgid = msg.get('Message-ID', None) @@ -578,6 +735,20 @@ def get_msgs(cmdargs) -> Tuple[Optional[str], Optional[list]]: def main(cmdargs): + if cmdargs.subcmd == 'shazam': + # We force some settings + cmdargs.checknewer = True + cmdargs.threeway = False + cmdargs.nopartialreroll = False + cmdargs.outdir = '-' + cmdargs.guessbranch = None + if cmdargs.merge: + cmdargs.makefetchhead = True + if cmdargs.makefetchhead: + cmdargs.guessbase = True + else: + cmdargs.guessbase = False + if cmdargs.checknewer: # Force nocache mode cmdargs.nocache = True @@ -589,7 +760,7 @@ def main(cmdargs): if len(msgs) and cmdargs.checknewer: msgs = get_extra_series(msgs, direction=1, useproject=cmdargs.useproject) - if cmdargs.subcmd == 'am': + if cmdargs.subcmd in ('am', 'shazam'): make_am(msgs, cmdargs, msgid) return |