aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--b4/__init__.py15
-rw-r--r--b4/command.py102
-rw-r--r--b4/ez.py179
3 files changed, 168 insertions, 128 deletions
diff --git a/b4/__init__.py b/b4/__init__.py
index 798755c..2a6da91 100644
--- a/b4/__init__.py
+++ b/b4/__init__.py
@@ -1326,7 +1326,7 @@ class LoreMessage:
def get_patch_id(diff: str) -> Optional[str]:
gitargs = ['patch-id', '--stable']
ecode, out = git_run_command(None, gitargs, stdin=diff.encode())
- if ecode > 0:
+ if ecode > 0 or not len(out.strip()):
return None
return out.split(maxsplit=1)[0]
@@ -2432,6 +2432,7 @@ def git_range_to_patches(gitdir: Optional[str], start: str, end: str,
seriests: Optional[int] = None,
mailfrom: Optional[Tuple[str, str]] = None,
extrahdrs: Optional[List[Tuple[str, str]]] = None,
+ thread: bool = False,
keepdate: bool = False) -> List[Tuple[str, email.message.Message]]:
patches = list()
commits = git_get_command_lines(gitdir, ['rev-list', '--reverse', f'{start}..{end}'])
@@ -2513,7 +2514,7 @@ def git_range_to_patches(gitdir: Optional[str], start: str, end: str,
if counter > 1 and not covermsg:
# Tread to the first patch
refto = msgid_tpt % str(1)
- if refto:
+ if refto and thread:
msg.add_header('References', refto)
msg.add_header('In-Reply-To', refto)
@@ -2841,7 +2842,11 @@ def patchwork_set_state(msgids: List[str], state: str) -> bool:
def send_smtp(smtp: Union[smtplib.SMTP, smtplib.SMTP_SSL, None], msg: email.message.Message,
fromaddr: str, destaddrs: Optional[Union[Tuple, Set]] = None,
patatt_sign: bool = False, dryrun: bool = False,
- maxheaderlen: Optional[int] = None) -> bool:
+ maxheaderlen: Optional[int] = None,
+ write_to: Optional[str] = None) -> bool:
+
+ if write_to is not None:
+ dryrun = True
if not msg.get('X-Mailer'):
msg.add_header('X-Mailer', f'b4 {__VERSION__}')
msg.set_charset('utf-8')
@@ -2875,6 +2880,10 @@ def send_smtp(smtp: Union[smtplib.SMTP, smtplib.SMTP_SSL, None], msg: email.mess
# patatt.logger = logger
bdata = patatt.rfc2822_sign(bdata)
if dryrun or smtp is None:
+ if write_to:
+ with open(write_to, 'wb') as fh:
+ fh.write(bdata.replace(b'\r\n', b'\n'))
+ return True
logger.info(' --- DRYRUN: message follows ---')
logger.info(' | ' + bdata.decode().rstrip().replace('\n', '\n | '))
logger.info(' --- DRYRUN: message ends ---')
diff --git a/b4/command.py b/b4/command.py
index fb9f6d4..62d9cb1 100644
--- a/b4/command.py
+++ b/b4/command.py
@@ -71,19 +71,19 @@ def cmd_kr(cmdargs):
b4.kr.main(cmdargs)
-def cmd_ez_series(cmdargs):
+def cmd_prep(cmdargs):
import b4.ez
- b4.ez.cmd_ez_series(cmdargs)
+ b4.ez.cmd_prep(cmdargs)
-def cmd_ez_trailers(cmdargs):
+def cmd_trailers(cmdargs):
import b4.ez
- b4.ez.cmd_ez_trailers(cmdargs)
+ b4.ez.cmd_trailers(cmdargs)
-def cmd_ez_send(cmdargs):
+def cmd_send(cmdargs):
import b4.ez
- b4.ez.cmd_ez_send(cmdargs)
+ b4.ez.cmd_send(cmdargs)
def cmd_am(cmdargs):
@@ -250,57 +250,55 @@ def cmd():
help='Show all developer keys found in a thread')
sp_kr.set_defaults(func=cmd_kr)
- # b4 ez commands
- # ez-series
- sp_ezs = subparsers.add_parser('ez-series', help='Simplify work on series submitted for review')
- sp_ezs.add_argument('--edit-cover', action='store_true', default=False,
- help='Edit the cover letter in your defined $EDITOR (or core.editor)')
- sp_ezs.add_argument('--show-revision', action='store_true', default=False,
- help='Show current series revision number')
- sp_ezs.add_argument('--force-revision', default=False, type=int,
- help='Force revision to be this number instead')
- ag_ezn = sp_ezs.add_argument_group('Create new branch', 'Create new branch for ez-series')
- ag_ezn.add_argument('-n', '--new', dest='new_series_name',
- help='Create a new branch and prepare for new series')
- ag_ezn.add_argument('-f', '--fork-point', dest='fork_point',
- help='When creating a new branch, use this fork point instead of HEAD')
- ag_ezn = sp_ezs.add_argument_group('Enroll existing branch', 'Enroll existing branch for ez-series')
- ag_ezn.add_argument('-e', '--enroll-with-base', dest='base_branch',
- help='Enroll current branch, using the branch passed as parameter as base branch')
- sp_ezs.set_defaults(func=cmd_ez_series)
-
- # ez-trailers
- sp_ezt = subparsers.add_parser('ez-trailers', help='Retrieve and apply trailers received for your submission')
- sp_ezt.add_argument('-u', '--update-trailers', action='store_true', default=False,
- help='Update commits with latest received trailers')
- sp_ezt.add_argument('-F', '--trailers-from', dest='msgid',
+ # b4 prep
+ sp_prep = subparsers.add_parser('prep', help='Work on patch series to submit for mailing list review')
+ sp_prep.add_argument('--edit-cover', action='store_true', default=False,
+ help='Edit the cover letter in your defined $EDITOR (or core.editor)')
+ sp_prep.add_argument('--show-revision', action='store_true', default=False,
+ help='Show current series revision number')
+ sp_prep.add_argument('--force-revision', default=False, metavar='N', type=int,
+ help='Force revision to be this number instead')
+ sp_prep.add_argument('--format-patch', metavar='OUTPUT_DIR',
+ help='Output prep-tracked commits as patches')
+ ag_prepn = sp_prep.add_argument_group('Create new branch', 'Create a new branch for working on patch series')
+ ag_prepn.add_argument('-n', '--new', dest='new_series_name',
+ 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_prepe = sp_prep.add_argument_group('Enroll existing branch', 'Enroll existing branch for prep work')
+ ag_prepe.add_argument('-e', '--enroll-with-base', dest='base_branch',
+ help='Enroll current branch, using the branch passed as parameter as base branch')
+ sp_prep.set_defaults(func=cmd_prep)
+
+ # b4 trailers
+ sp_trl = subparsers.add_parser('trailers', help='Operate on trailers received for mailing list reviews')
+ sp_trl.add_argument('-u', '--update', action='store_true', default=False,
+ help='Update branch commits with latest received trailers')
+ sp_trl.add_argument('-F', '--trailers-from', dest='msgid',
help='Look for trailers in the thread with this msgid instead of using the series change-id')
- sp_ezt.add_argument('-s', '--signoff', action='store_true', default=False,
+ sp_trl.add_argument('-s', '--signoff', action='store_true', default=False,
help='Add my Signed-off-by trailer, if not already present')
- sp_ezt.add_argument('-S', '--sloppy-trailers', dest='sloppytrailers', action='store_true', default=False,
+ sp_trl.add_argument('-S', '--sloppy-trailers', dest='sloppytrailers', action='store_true', default=False,
help='Apply trailers without email address match checking')
- sp_ezt.set_defaults(func=cmd_ez_trailers)
+ sp_trl.set_defaults(func=cmd_trailers)
# ez-send
- sp_ezd = subparsers.add_parser('ez-send', help='Submit your series for review on the mailing lists')
- ezd_x = sp_ezd.add_mutually_exclusive_group()
- ezd_x.add_argument('-o', '--output-dir',
- help='Do not send, just write patches into this directory (git-format-patch mode)')
- ezd_x.add_argument('-d', '--dry-run', dest='dryrun', action='store_true', default=False,
- help='Do not actually send, just dump out raw smtp messages to the stdout')
- sp_ezd.add_argument('--prefixes', nargs='+', choices=['RFC', 'WIP', 'RESEND'],
- help='Prefixes to add to PATCH (e.g. RFC, WIP, RESEND)')
- sp_ezd.add_argument('--no-auto-to-cc', action='store_true', default=False,
- help='Do not automatically collect To: and Cc: addresses')
- sp_ezd.add_argument('--to', nargs='+',
- help='Addresses to add to the automatically collected To: list')
- sp_ezd.add_argument('--cc', nargs='+',
- help='Addresses to add to the automatically collected Cc: list')
- sp_ezd.add_argument('--not-me-too', action='store_true', default=False,
- help='Remove yourself from the To: or Cc: list')
- sp_ezd.add_argument('--no-sign', action='store_true', default=False,
- help='Do not cryptographically sign your patches with patatt')
- sp_ezd.set_defaults(func=cmd_ez_send)
+ sp_send = subparsers.add_parser('send', help='Submit your work for review on the mailing lists')
+ sp_send.add_argument('-d', '--dry-run', dest='dryrun', action='store_true', default=False,
+ help='Do not send, just dump out raw smtp messages to the stdout')
+ sp_send.add_argument('-o', '--output-dir',
+ help='Do not send, write raw messages to this directory (forces --dry-run)')
+ sp_send.add_argument('--prefixes', nargs='+', choices=['RFC', 'WIP', 'RESEND'],
+ help='Prefixes to add to PATCH (e.g. RFC, WIP, RESEND)')
+ sp_send.add_argument('--no-auto-to-cc', action='store_true', default=False,
+ help='Do not automatically collect To: and Cc: addresses')
+ sp_send.add_argument('--to', nargs='+', help='Addresses to add to the To: list')
+ 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('--no-sign', action='store_true', default=False,
+ help='Do not cryptographically sign your patches with patatt')
+ sp_send.set_defaults(func=cmd_send)
cmdargs = parser.parse_args()
diff --git a/b4/ez.py b/b4/ez.py
index ec331d0..7851929 100644
--- a/b4/ez.py
+++ b/b4/ez.py
@@ -233,7 +233,7 @@ def start_new_series(cmdargs: argparse.Namespace) -> None:
# Try loading existing cover info
cover, jdata = load_cover()
- logger.info('Will track %s commits for ez-series', commitcount)
+ logger.info('Will track %s commits', commitcount)
else:
logger.critical('CRITICAL: unknown operation requested')
sys.exit(1)
@@ -255,7 +255,7 @@ def start_new_series(cmdargs: argparse.Namespace) -> None:
'',
'# You can add other trailers to the cover letter. Any email addresses found in',
'# these trailers will be added to the addresses specified/generated during',
- '# the ez-send stage.',
+ '# the b4 send stage.',
'',
'',
)
@@ -276,7 +276,7 @@ def start_new_series(cmdargs: argparse.Namespace) -> None:
def make_magic_json(data: dict) -> str:
mj = (f'{MAGIC_MARKER}\n'
- '# This section is used internally by b4 ez-series for tracking purposes.\n')
+ '# This section is used internally by b4 prep for tracking purposes.\n')
return mj + json.dumps(data, indent=2)
@@ -358,20 +358,20 @@ def get_cover_strategy(branch: Optional[str] = None) -> str:
branch = b4.git_get_current_branch()
# Check local branch config for the strategy
bconfig = b4.get_config_from_git(rf'branch\.{branch}\..*')
- if 'b4-ez-cover-strategy' in bconfig:
- strategy = bconfig.get('b4-ez-cover-strategy')
+ if 'b4-prep-cover-strategy' in bconfig:
+ strategy = bconfig.get('b4-prep-cover-strategy')
else:
config = b4.get_main_config()
- strategy = config.get('ez-cover-strategy', 'commit')
+ strategy = config.get('prep-cover-strategy', 'commit')
if strategy in {'commit', 'branch-description'}:
return strategy
- logger.critical('CRITICAL: unknown ez-cover-strategy: %s', strategy)
+ logger.critical('CRITICAL: unknown prep-cover-strategy: %s', strategy)
sys.exit(1)
-def is_ez_branch() -> bool:
+def is_prep_branch() -> bool:
mybranch = b4.git_get_current_branch()
strategy = get_cover_strategy(mybranch)
if strategy == 'commit':
@@ -489,10 +489,15 @@ def update_trailers(cmdargs: argparse.Namespace) -> None:
else:
signoff = None
- # If we are in an ez-series branch, we start from the beginning of the series
+ # If we are in an b4-prep branch, we start from the beginning of the series
# oterwise, we start at the first commit where we're the committer since 3.months
# TODO: consider making that settable?
- if cmdargs.msgid:
+ if is_prep_branch():
+ start = get_series_start()
+ end = 'HEAD'
+ cover, tracking = load_cover(strip_comments=True)
+ changeid = tracking['series'].get('change-id')
+ elif cmdargs.msgid:
changeid = None
myemail = usercfg['email']
# There doesn't appear to be a great way to find the first commit
@@ -518,13 +523,6 @@ def update_trailers(cmdargs: argparse.Namespace) -> None:
break
prevparent = parent
start = f'{commit}~1'
-
- elif is_ez_branch():
- start = get_series_start()
- end = 'HEAD'
- cover, tracking = load_cover(strip_comments=True)
- changeid = tracking['series'].get('change-id')
-
else:
logger.critical('CRITICAL: Please specify -F msgid to look up trailers from remote.')
sys.exit(1)
@@ -675,32 +673,21 @@ def print_pretty_addrs(addrs: list, hdrname: str) -> None:
logger.info(' %s', b4.format_addrs([addr]))
-def cmd_ez_send(cmdargs: argparse.Namespace) -> None:
- # Check if the cover letter has 'EDITME' in it
+def get_prep_branch_as_patches(prefixes: Optional[list] = None,
+ movefrom: bool = True,
+ thread: bool = True) -> List[Tuple[str, email.message.Message]]:
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)
-
config = b4.get_main_config()
cover_template = DEFAULT_COVER_TEMPLATE
- if config.get('submit-cover-template'):
+ if config.get('prep-cover-template'):
# Try to load this template instead
try:
- cover_template = b4.read_template(config['submit-cover-template'])
+ cover_template = b4.read_template(config['prep-cover-template'])
except FileNotFoundError:
- logger.critical('ERROR: submit-cover-template says to use %s, but it does not exist',
- config['submit-cover-template'])
+ logger.critical('ERROR: prep-cover-template says to use %s, but it does not exist',
+ config['prep-cover-template'])
sys.exit(2)
- # Generate the patches and collect all the addresses from trailers
- parts = b4.LoreMessage.get_body_parts(cover)
- trailers = set()
- trailers.update(parts[2])
-
# Put together the cover letter
csubject, cbody = cover.split('\n', maxsplit=1)
start_commit = get_series_start()
@@ -720,9 +707,7 @@ def cmd_ez_send(cmdargs: argparse.Namespace) -> None:
cmsg = email.message.EmailMessage()
cmsg.add_header('Subject', csubject)
cmsg.set_payload(body, charset='utf-8')
- if cmdargs.prefixes:
- prefixes = list(cmdargs.prefixes)
- else:
+ if prefixes is None:
prefixes = list()
prefixes.append(f'v{revision}')
@@ -741,28 +726,76 @@ def cmd_ez_send(cmdargs: argparse.Namespace) -> None:
# Message-IDs must not be predictable to avoid stuffing attacks
randompart = uuid.uuid4().hex[:12]
msgid_tpt = f'<{stablepart}-v{revision}-%s-{randompart}@{msgdomain}>'
+ if movefrom:
+ mailfrom = (myname, myemail)
+ else:
+ mailfrom = None
+ patches = b4.git_range_to_patches(None, start_commit, 'HEAD',
+ covermsg=cmsg, prefixes=prefixes,
+ msgid_tpt=msgid_tpt,
+ seriests=seriests,
+ thread=thread,
+ mailfrom=mailfrom)
+ return patches
+
+
+def format_patch(output_dir: str) -> None:
try:
- patches = b4.git_range_to_patches(None, start_commit, 'HEAD',
- covermsg=cmsg, prefixes=prefixes,
- msgid_tpt=msgid_tpt,
- seriests=seriests,
- mailfrom=(myname, myemail))
+ patches = get_prep_branch_as_patches(thread=False, movefrom=False)
+ except RuntimeError as ex:
+ logger.critical('CRITICAL: Failed to convert range to patches: %s', ex)
+ sys.exit(1)
+
+ logger.info('Writing %s messages into %s', len(patches), output_dir)
+ pathlib.Path(output_dir).mkdir(parents=True, exist_ok=True)
+ for commit, msg in patches:
+ if not msg:
+ continue
+ msg.policy = email.policy.EmailPolicy(utf8=True, cte_type='8bit')
+ subject = msg.get('Subject', '')
+ ls = b4.LoreSubject(subject)
+ filen = '%s.patch' % ls.get_slug(sep='-')
+ with open(os.path.join(output_dir, filen), 'w') as fh:
+ fh.write(msg.as_string(unixfrom=True, maxheaderlen=0))
+ logger.info(' %s', filen)
+
+
+def cmd_send(cmdargs: argparse.Namespace) -> None:
+ # 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])
+
+ try:
+ patches = get_prep_branch_as_patches(prefixes=cmdargs.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)
+ config = b4.get_main_config()
+ usercfg = b4.get_user_config()
+ myemail = usercfg.get('email')
+
seen = set()
todests = list()
- if config.get('submit-to'):
- for pair in utils.getaddresses([config.get('submit-to')]):
+ if config.get('send-series-to'):
+ for pair in utils.getaddresses([config.get('send-series-to')]):
if pair[1] not in seen:
seen.add(pair[1])
todests.append(pair)
ccdests = list()
- if config.get('submit-cc'):
- for pair in utils.getaddresses([config.get('submit-cc')]):
+ if config.get('send-series-cc'):
+ for pair in utils.getaddresses([config.get('send-series-cc')]):
if pair[1] not in seen:
seen.add(pair[1])
ccdests.append(pair)
@@ -846,23 +879,9 @@ def cmd_ez_send(cmdargs: argparse.Namespace) -> None:
allcc = list()
if cmdargs.output_dir:
+ cmdargs.dryrun = True
+ logger.info('Will write out messages into %s', cmdargs.output_dir)
pathlib.Path(cmdargs.output_dir).mkdir(parents=True, exist_ok=True)
- for commit, msg in patches:
- if not msg:
- continue
- msg.add_header('To', b4.format_addrs(allto))
- if allcc:
- msg.add_header('Cc', b4.format_addrs(allcc))
- msg.set_charset('utf-8')
- msg.replace_header('Content-Transfer-Encoding', '8bit')
- msg.policy = email.policy.EmailPolicy(utf8=True, cte_type='8bit')
- subject = msg.get('Subject', '')
- ls = b4.LoreSubject(subject)
- filen = '%s.patch' % ls.get_slug(sep='-')
- with open(os.path.join(cmdargs.output_dir, filen), 'w') as fh:
- fh.write(msg.as_string(unixfrom=True, maxheaderlen=0))
- logger.info(' %s', filen)
- return
# Give the user the last opportunity to bail out
if not cmdargs.dryrun:
@@ -884,7 +903,7 @@ def cmd_ez_send(cmdargs: argparse.Namespace) -> None:
# And now we go through each message to set addressees and send them off
sign = True
- if cmdargs.no_sign or config.get('ez-send-no-sign', '').lower() in {'yes', 'true', 'y'}:
+ if cmdargs.no_sign or config.get('send-no-patatt-sign', '').lower() in {'yes', 'true', 'y'}:
sign = False
identity = config.get('sendemail-identity')
try:
@@ -906,14 +925,23 @@ def cmd_ez_send(cmdargs: argparse.Namespace) -> None:
# 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)))
+ msg.add_header('X-b4-tracking', ' '.join(textwrap.wrap(b64tracking.decode(), width=78)))
msg.add_header('To', b4.format_addrs(allto))
if allcc:
msg.add_header('Cc', b4.format_addrs(allcc))
- logger.info(' %s', msg.get('Subject'))
+ if cmdargs.output_dir:
+ subject = msg.get('Subject', '')
+ ls = b4.LoreSubject(subject)
+ filen = '%s.eml' % ls.get_slug(sep='-')
+ logger.info(' %s', filen)
+ write_to = os.path.join(cmdargs.output_dir, filen)
+ else:
+ write_to = None
+ logger.info(' %s', re.sub(r'\s+', ' ', msg.get('Subject')))
+
if b4.send_smtp(smtp, msg, fromaddr=fromaddr, destaddrs=alldests, patatt_sign=sign,
- dryrun=cmdargs.dryrun):
+ dryrun=cmdargs.dryrun, write_to=write_to):
counter += 1
logger.info('---')
@@ -957,6 +985,7 @@ def cmd_ez_send(cmdargs: argparse.Namespace) -> None:
tagcommit = 'HEAD'
# TODO: make sent/ prefix configurable?
+ revision = tracking['series']['revision']
tagprefix = 'sent/'
if mybranch.startswith('b4/'):
tagname = f'{tagprefix}{mybranch[3:]}-v{revision}'
@@ -982,7 +1011,7 @@ def cmd_ez_send(cmdargs: argparse.Namespace) -> None:
if vrev not in tracking['series']['history']:
tracking['series']['history'][vrev] = list()
tracking['series']['history'][vrev].append(cover_msgid)
- if 'RESEND' in prefixes:
+ if cmdargs.prefixes and 'RESEND' in cmdargs.prefixes:
logger.info('Not incrementing current revision due to RESEND')
store_cover(cover, tracking)
return
@@ -1049,7 +1078,7 @@ def force_revision(forceto: int) -> None:
store_cover(cover, tracking)
-def cmd_ez_series(cmdargs: argparse.Namespace) -> None:
+def cmd_prep(cmdargs: argparse.Namespace) -> None:
check_can_gfr()
status = b4.git_get_repo_status()
if len(status):
@@ -1066,14 +1095,17 @@ def cmd_ez_series(cmdargs: argparse.Namespace) -> None:
if cmdargs.force_revision:
return force_revision(cmdargs.force_revision)
- if is_ez_branch():
- logger.critical('CRITICAL: This appears to already be an ez-series branch.')
+ if cmdargs.format_patch:
+ return format_patch(cmdargs.format_patch)
+
+ if is_prep_branch():
+ logger.critical('CRITICAL: This appears to already be a b4-prep managed branch.')
sys.exit(1)
return start_new_series(cmdargs)
-def cmd_ez_trailers(cmdargs: argparse.Namespace) -> None:
+def cmd_trailers(cmdargs: argparse.Namespace) -> None:
check_can_gfr()
status = b4.git_get_repo_status()
if len(status):
@@ -1081,4 +1113,5 @@ def cmd_ez_trailers(cmdargs: argparse.Namespace) -> None:
logger.critical(' Stash or commit them first.')
sys.exit(1)
- update_trailers(cmdargs)
+ if cmdargs.update:
+ update_trailers(cmdargs)