aboutsummaryrefslogtreecommitdiff
path: root/b4/mbox.py
diff options
context:
space:
mode:
Diffstat (limited to 'b4/mbox.py')
-rw-r--r--b4/mbox.py375
1 files changed, 273 insertions, 102 deletions
diff --git a/b4/mbox.py b/b4/mbox.py
index a7e9f78..c2e1555 100644
--- a/b4/mbox.py
+++ b/b4/mbox.py
@@ -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