aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--b4/__init__.py52
-rw-r--r--b4/command.py2
-rw-r--r--b4/ez.py136
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')
diff --git a/b4/ez.py b/b4/ez.py
index 7187dad..593e9a7 100644
--- a/b4/ez.py
+++ b/b4/ez.py
@@ -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('---')