aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonstantin Ryabitsev <konstantin@linuxfoundation.org>2022-07-20 10:26:55 -0400
committerKonstantin Ryabitsev <konstantin@linuxfoundation.org>2022-07-20 10:26:55 -0400
commitf0fad6ec193c39626e90d01dcbb6541c66b9fdef (patch)
tree5c256a0b04128d0ddd3d15bf2607ce39b709e5a1
parent45aaaf33f00e3d05456394775802d7551b20757c (diff)
downloadb4-f0fad6ec193c39626e90d01dcbb6541c66b9fdef.tar.gz
ez: refactor based on initial feedback
Significant refactor of (formerly) "b4 submit" based on initial feedback: 1. Split "b4 submit" into three different commands: - ez-series: for managing the series cover letters, tracking info, etc - ez-trailers: for retrieving trailers and updating commits (works on any branch, not just ez-series branches) - ez-send: for sending branches managed by ez-series 2. Refactor to support multiple cover letter strategies: - the default "commit" strategy that keeps the cover letter in an empty commit (should be backwards-compatible with "b4 submit") - the non-invasive "branch-description" strategy that keeps the cover letter in the branch.branchname.description configuration setting and tracking in branch.branchname.b4-tracking - the not-yet-implemented "tag" strategy that mimics the behaviour of git-publish The strategy can be set via b4.ez-cover-strategy variable, e.g. in your .gitconfig: [b4] ez-cover-strategy = branch-description Note, that converting from one strategy to another doesn't work and will probably explode in weird ways right now. Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
-rw-r--r--b4/__init__.py6
-rw-r--r--b4/command.py109
-rw-r--r--b4/ez.py (renamed from b4/submit.py)521
3 files changed, 421 insertions, 215 deletions
diff --git a/b4/__init__.py b/b4/__init__.py
index b3aa84f..6adabbd 100644
--- a/b4/__init__.py
+++ b/b4/__init__.py
@@ -2008,6 +2008,12 @@ def in_directory(dirname):
os.chdir(cdir)
+def git_set_config(fullpath: Optional[str], param: str, value: str, operation: str = '--replace-all'):
+ args = ['config', operation, param, value]
+ ecode, out = git_run_command(fullpath, args)
+ return ecode
+
+
def get_config_from_git(regexp: str, defaults: Optional[dict] = None, multivals: Optional[list] = None) -> dict:
if multivals is None:
multivals = list()
diff --git a/b4/command.py b/b4/command.py
index 49ce767..2db9f7a 100644
--- a/b4/command.py
+++ b/b4/command.py
@@ -71,9 +71,19 @@ def cmd_kr(cmdargs):
b4.kr.main(cmdargs)
-def cmd_submit(cmdargs):
- import b4.submit
- b4.submit.main(cmdargs)
+def cmd_ez_series(cmdargs):
+ import b4.ez
+ b4.ez.cmd_ez_series(cmdargs)
+
+
+def cmd_ez_trailers(cmdargs):
+ import b4.ez
+ b4.ez.cmd_ez_trailers(cmdargs)
+
+
+def cmd_ez_send(cmdargs):
+ import b4.ez
+ b4.ez.cmd_ez_send(cmdargs)
def cmd_am(cmdargs):
@@ -240,52 +250,53 @@ def cmd():
help='Show all developer keys found in a thread')
sp_kr.set_defaults(func=cmd_kr)
- # b4 submit
- sp_submit = subparsers.add_parser('submit', help='Submit patches for review')
- # xg_submit = sp_submit.add_mutually_exclusive_group()
- # xg_submit.add_argument('--web-auth-new', action='store_true', default=False,
- # help='Register a new email and pubkey with a web submission endpoint')
- # xg_submit.add_argument('--web-auth-verify',
- # help='Submit a response to a challenge received from a web submission endpoint')
- sp_submit.add_argument('--edit-cover', action='store_true', default=False,
- help='Edit the cover letter in your defined $EDITOR (or core.editor)')
- sp_submit.add_argument('--reroll', action='store_true', default=False,
- help='Increment revision and add changelog templates to the cover letter')
- nn_submit = sp_submit.add_argument_group('New series', 'Set up a new work branch for a new patch series')
- nn_submit.add_argument('-n', '--new', dest='new_series_name',
- help='Create a new branch and start working on new series')
- nn_submit.add_argument('-f', '--fork-point', dest='fork_point',
- help='Create new branch at this fork point instead of HEAD')
- ag_submit = sp_submit.add_argument_group('Sync trailers', 'Update series with latest received trailers')
- ag_submit.add_argument('-u', '--update-trailers', action='store_true', default=False,
- help='Update commits with latest received trailers')
- ag_submit.add_argument('-s', '--signoff', action='store_true', default=False,
- help='Add my Signed-off-by trailer, if not already present')
- ag_submit.add_argument('-S', '--sloppy-trailers', dest='sloppytrailers', action='store_true', default=False,
- help='Apply trailers without email address match checking')
- ag_submit.add_argument('-F', '--trailers-from', dest='thread_msgid',
- help='Look for new trailers in the thread with this msgid instead of using the change-id')
- se_submit = sp_submit.add_argument_group('Send series', 'Submits your series for review')
- se_submit.add_argument('--send', action='store_true', default=False,
- help='Submit the series for review')
- se_submit.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')
- se_submit.add_argument('-o', '--output-dir',
- help='Do not send, just write patches into this directory (git-format-patch mode)')
- se_submit.add_argument('--prefixes', nargs='+', choices=['RFC', 'WIP', 'RESEND'],
- help='Prefixes to add to PATCH (e.g. RFC, WIP, RESEND)')
- se_submit.add_argument('--no-auto-to-cc', action='store_true', default=False,
- help='Do not automatically collect To: and Cc: addresses')
- se_submit.add_argument('--to', nargs='+',
- help='Addresses to add to the automatically collected To: list')
- se_submit.add_argument('--cc', nargs='+',
- help='Addresses to add to the automatically collected Cc: list')
- se_submit.add_argument('--not-me-too', action='store_true', default=False,
- help='Remove yourself from the To: or Cc: list')
- se_submit.add_argument('--no-sign', action='store_true', default=False,
- help='Do not cryptographically sign your patches with patatt')
-
- sp_submit.set_defaults(func=cmd_submit)
+ # 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)')
+ 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',
+ 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,
+ 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,
+ help='Apply trailers without email address match checking')
+ sp_ezt.set_defaults(func=cmd_ez_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)
cmdargs = parser.parse_args()
diff --git a/b4/submit.py b/b4/ez.py
index 6acbf25..db79fe8 100644
--- a/b4/submit.py
+++ b/b4/ez.py
@@ -143,85 +143,171 @@ Changes in v${newrev}:
# sys.exit(1)
-def start_new_series(cmdargs: argparse.Namespace) -> None:
- status = b4.git_get_repo_status()
- if len(status):
- logger.critical('CRITICAL: Repository contains uncommitted changes.')
- logger.critical(' Stash or commit them first.')
- sys.exit(1)
+def get_base_forkpoint(basebranch: str) -> Tuple[str, int]:
+ # Check that that branch exists
+ gitargs = ['rev-parse', '--verify', '--quiet', basebranch]
+ ecode, out = b4.git_run_command(None, gitargs)
+ if ecode > 0:
+ logger.crtitical('CRITICAL: Could not find branch with this name: %s', basebranch)
+ raise RuntimeError('Branch %s not found', basebranch)
+ # Find merge-base with that branch
+ mybranch = b4.git_get_current_branch()
+ logger.debug('Finding the fork-point with %s', basebranch)
+ gitargs = ['merge-base', '--fork-point', basebranch]
+ lines = b4.git_get_command_lines(None, gitargs)
+ if not lines:
+ logger.crtitical('CRITICAL: Could not find common ancestor with %s', basebranch)
+ raise RuntimeError('Branches %s and %s have no common ancestors', basebranch, mybranch)
+ fp = lines[0]
+ logger.debug('Fork-point between %s and %s is %s', mybranch, basebranch, fp)
+ # Check how many revisions there are between the fork-point and the current HEAD
+ gitargs = ['rev-list', f'{fp}..']
+ lines = b4.git_get_command_lines(None, gitargs)
+ # Arbitrarily, set it to 1000
+ if len(lines) > 1000:
+ logger.critical('CRITICAL: Too many revisions between %s and current branch: %s', basebranch, len(lines))
+ raise RuntimeError('Branches %s and %s are unreasonable as ancestors', basebranch, mybranch)
+ return fp, len(lines)
+
+
+def start_new_series(cmdargs: argparse.Namespace) -> None:
usercfg = b4.get_user_config()
if 'name' not in usercfg or 'email' not in usercfg:
logger.critical('CRITICAL: Unable to add your Signed-off-by: git returned no user.name or user.email')
sys.exit(1)
- if not cmdargs.fork_point:
- cmdargs.fork_point = 'HEAD'
- slug = re.sub(r'\W+', '-', cmdargs.new_series_name).strip('-').lower()
- branchname = 'b4/%s' % slug
- args = ['checkout', '-b', branchname, cmdargs.fork_point]
- ecode, out = b4.git_run_command(None, args, logstderr=True)
- if ecode > 0:
- logger.critical('CRITICAL: Failed to create a new branch %s', branchname)
- logger.critical(out)
- sys.exit(ecode)
- logger.info('Created new branch %s', branchname)
- # create an empty commit containing basic cover letter details
- msgdata = ('EDITME: cover title for %s' % cmdargs.new_series_name,
- '',
- '# Lines starting with # will be removed from the cover letter. You can use',
- '# them to add notes or reminders to yourself.',
- '',
- 'EDITME: describe the purpose of this series. The information you put here',
- 'will be used by the project maintainer to make a decision whether your',
- 'patches should be reviewed, and in what priority order. Please be very',
- 'detailed and link to any relevant discussions or sites that the maintainer',
- 'can review to better understand your proposed changes.',
- '',
- 'Signed-off-by: %s <%s>' % (usercfg.get('name', ''), usercfg.get('email', '')),
- '',
- '# 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 --send stage.',
- '',
- '',
- )
+ cover = None
+ if cmdargs.new_series_name:
+ basebranch = None
+ if not cmdargs.fork_point:
+ cmdargs.fork_point = 'HEAD'
+ else:
+ strategy = get_cover_strategy()
+ # if our strategy is not "commit", then we need to know which branch we're using as base
+ mybranch = b4.git_get_current_branch()
+ if strategy != 'commit':
+ gitargs = ['branch', '-v', '--contains', cmdargs.fork_point]
+ lines = b4.git_get_command_lines(None, gitargs)
+ if not lines:
+ logger.critical('CRITICAL: no branch contains fork-point %s', cmdargs.fork_point)
+ sys.exit(1)
+ for line in lines:
+ chunks = line.split(maxsplit=2)
+ # There's got to be a better way than checking for '*'
+ if chunks[0] != '*':
+ continue
+ if chunks[1] == mybranch:
+ logger.debug('branch %s does contain fork-point %s', mybranch, cmdargs.fork_point)
+ basebranch = mybranch
+ break
+ if basebranch is None:
+ logger.critical('CRITICAL: fork-point %s is not on the current branch.')
+ logger.critical(' Switch to the branch you want to use as base and try again.')
+ sys.exit(1)
+
+ slug = re.sub(r'\W+', '-', cmdargs.new_series_name).strip('-').lower()
+ branchname = 'b4/%s' % slug
+ args = ['checkout', '-b', branchname, cmdargs.fork_point]
+ ecode, out = b4.git_run_command(None, args, logstderr=True)
+ if ecode > 0:
+ logger.critical('CRITICAL: Failed to create a new branch %s', branchname)
+ logger.critical(out)
+ sys.exit(ecode)
+ logger.info('Created new branch %s', branchname)
+ seriesname = cmdargs.new_series_name
+
+ elif cmdargs.base_branch:
+ # Check that strategy isn't "commit" as we don't currently support that
+ if get_cover_strategy() == 'commit':
+ logger.critical('CRITICAL: enrolling branches with "commit" cover strategy is not currently supported')
+ sys.exit(1)
+
+ seriesname = b4.git_get_current_branch()
+ slug = re.sub(r'\W+', '-', seriesname).strip('-').lower()
+ basebranch = cmdargs.base_branch
+ try:
+ forkpoint, commitcount = get_base_forkpoint(basebranch)
+ except RuntimeError:
+ sys.exit(1)
+ # Try loading existing cover info
+ cover, jdata = load_cover()
+
+ logger.info('Will track %s commits for ez-series', commitcount)
+ else:
+ logger.critical('CRITICAL: unknown operation requested')
+ sys.exit(1)
+
+ if not cover:
+ # create a default cover letter and store it where the strategy indicates
+ cover = ('EDITME: cover title for %s' % seriesname,
+ '',
+ '# Lines starting with # will be removed from the cover letter. You can use',
+ '# them to add notes or reminders to yourself.',
+ '',
+ 'EDITME: describe the purpose of this series. The information you put here',
+ 'will be used by the project maintainer to make a decision whether your',
+ 'patches should be reviewed, and in what priority order. Please be very',
+ 'detailed and link to any relevant discussions or sites that the maintainer',
+ 'can review to better understand your proposed changes.',
+ '',
+ 'Signed-off-by: %s <%s>' % (usercfg.get('name', ''), usercfg.get('email', '')),
+ '',
+ '# 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.',
+ '',
+ '',
+ )
+ 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,
},
}
- message = '\n'.join(msgdata) + make_magic_json(tracking)
- args = ['commit', '--allow-empty', '-F', '-']
- ecode, out = b4.git_run_command(None, args, stdin=message.encode(), logstderr=True)
- if ecode > 0:
- logger.critical('CRITICAL: Generating cover letter commit failed:')
- logger.critical(out)
- logger.info('Created empty commit with the cover letter.')
- logger.info('You can prepare your commits now.')
+ store_cover(cover, tracking, new=True)
def make_magic_json(data: dict) -> str:
mj = (f'{MAGIC_MARKER}\n'
- '# This section is used internally by b4 submit for tracking purposes.\n')
+ '# This section is used internally by b4 ez-series for tracking purposes.\n')
return mj + json.dumps(data, indent=2)
-def load_cover(cover_commit: str, strip_comments: bool = False) -> Tuple[str, dict]:
- # Grab the cover contents
- gitargs = ['show', '-s', '--format=%B', cover_commit]
- ecode, out = b4.git_run_command(None, gitargs)
- if ecode > 0:
- logger.critical('CRITICAL: unable to load cover letter')
- sys.exit(1)
- # Split on MAGIC_MARKER
- cover, magic_json = out.split(MAGIC_MARKER)
- # drop everything until the first {
- junk, mdata = magic_json.split('{', maxsplit=1)
- jdata = json.loads('{' + mdata)
+def load_cover(strip_comments: bool = False) -> Tuple[str, dict]:
+ strategy = get_cover_strategy()
+ if strategy == 'commit':
+ cover_commit = find_cover_commit()
+ if not cover_commit:
+ logger.critical('CRITICAL: unable to find cover commit!')
+ sys.exit(1)
+ gitargs = ['show', '-s', '--format=%B', cover_commit]
+ ecode, out = b4.git_run_command(None, gitargs)
+ if ecode > 0:
+ logger.critical('CRITICAL: unable to load cover letter')
+ sys.exit(1)
+ contents = out
+ # Split on MAGIC_MARKER
+ cover, magic_json = contents.split(MAGIC_MARKER)
+ # drop everything until the first {
+ junk, mdata = magic_json.split('{', maxsplit=1)
+ jdata = json.loads('{' + mdata)
+ elif strategy == 'branch-description':
+ mybranch = b4.git_get_current_branch()
+ bcfg = b4.get_config_from_git(rf'branch\.{mybranch}\..*')
+ cover = bcfg.get('description', '')
+ jdata = json.loads(bcfg.get('b4-tracking', '{}'))
+ else:
+ # TODO: implement
+ logger.critical('Not yet supported for %s cover strategy', strategy)
+ sys.exit(0)
+
logger.debug('tracking data: %s', jdata)
if strip_comments:
cover = re.sub(r'^#.*$', '', cover, flags=re.M)
@@ -230,31 +316,78 @@ def load_cover(cover_commit: str, strip_comments: bool = False) -> Tuple[str, di
return cover.strip(), jdata
-def update_cover(commit: str, content: str, tracking: dict) -> None:
- cover_message = content + '\n\n' + make_magic_json(tracking)
- fred = FRCommitMessageEditor()
- fred.add(commit, cover_message)
- args = fr.FilteringOptions.parse_args(['--force', '--quiet', '--refs', f'{commit}~1..HEAD'])
- args.refs = [f'{commit}~1..HEAD']
- frf = fr.RepoFilter(args, commit_callback=fred.callback)
- logger.info('Invoking git-filter-repo to update the cover letter.')
- frf.run()
+def store_cover(content: str, tracking: dict, new: bool = False) -> None:
+ strategy = get_cover_strategy()
+ if strategy == 'commit':
+ cover_message = content + '\n\n' + make_magic_json(tracking)
+ if new:
+ args = ['commit', '--allow-empty', '-F', '-']
+ ecode, out = b4.git_run_command(None, args, stdin=cover_message.encode(), logstderr=True)
+ if ecode > 0:
+ logger.critical('CRITICAL: Generating cover letter commit failed:')
+ logger.critical(out)
+ raise RuntimeError('Error saving cover letter')
+ else:
+ commit = find_cover_commit()
+ if not commit:
+ logger.critical('CRITICAL: Could not find the cover letter commit.')
+ raise RuntimeError('Error saving cover letter (commit not found)')
+ fred = FRCommitMessageEditor()
+ fred.add(commit, cover_message)
+ args = fr.FilteringOptions.parse_args(['--force', '--quiet', '--refs', f'{commit}~1..HEAD'])
+ args.refs = [f'{commit}~1..HEAD']
+ frf = fr.RepoFilter(args, commit_callback=fred.callback)
+ logger.info('Invoking git-filter-repo to update the cover letter.')
+ frf.run()
+
+ if strategy == 'branch-description':
+ mybranch = b4.git_get_current_branch(None)
+ b4.git_set_config(None, f'branch.{mybranch}.description', content)
+ trackstr = json.dumps(tracking)
+ b4.git_set_config(None, f'branch.{mybranch}.b4-tracking', trackstr)
+ logger.info('Updated branch description and tracking info.')
+
+
+def get_cover_strategy(branch: Optional[str] = None) -> str:
+ if branch is None:
+ 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')
+ else:
+ config = b4.get_main_config()
+ strategy = config.get('ez-cover-strategy', 'commit')
+
+ return strategy
-def check_our_branch() -> bool:
+def is_ez_branch() -> bool:
mybranch = b4.git_get_current_branch()
- if mybranch.startswith('b4/'):
+ strategy = get_cover_strategy(mybranch)
+ if strategy == 'commit':
+ if find_cover_commit() is None:
+ return False
return True
- logger.info('CRITICAL: This does not look like a b4-managed branch.')
- logger.info(' "%s" does not start with "b4/"', mybranch)
- return False
+ if strategy == 'branch-description':
+ # See if we have b4-tracking set for this branch
+ bcfg = b4.get_config_from_git(rf'branch\.{mybranch}\..*')
+ if bcfg.get('b4-tracking'):
+ return True
+ return False
+ if strategy == 'tag':
+ logger.critical('CRITICAL: tag strategy not yet supported')
+ sys.exit(1)
+
+ logger.critical('CRITICAL: unknown cover strategy: %s', strategy)
+ sys.exit(1)
def find_cover_commit() -> Optional[str]:
# Walk back commits until we find the cover letter
# Our covers always contain the MAGIC_MARKER line
logger.debug('Looking for the cover letter commit with magic marker "%s"', MAGIC_MARKER)
- gitargs = ['log', '--grep', MAGIC_MARKER, '-F', '--pretty=oneline', '--max-count=1']
+ gitargs = ['log', '--grep', MAGIC_MARKER, '-F', '--pretty=oneline', '--max-count=1', '--since=1.year']
lines = b4.git_get_command_lines(None, gitargs)
if not lines:
return None
@@ -280,8 +413,8 @@ class FRCommitMessageEditor:
commit.message = self.edit_map[commit.original_id]
-def edit_cover(cover_commit: str) -> None:
- cover, tracking = load_cover(cover_commit)
+def edit_cover() -> None:
+ cover, tracking = load_cover()
# What's our editor? And yes, the default is vi, bite me.
corecfg = b4.get_config_from_git(r'core\..*', {'editor': os.environ.get('EDITOR', 'vi')})
editor = corecfg.get('editor')
@@ -305,55 +438,124 @@ def edit_cover(cover_commit: str) -> None:
logger.info('New cover letter blank, leaving current one unchanged.')
return
- update_cover(cover_commit, new_cover, tracking)
+ store_cover(new_cover, tracking)
logger.info('Cover letter updated.')
-def update_trailers(cover_commit: str, cmdargs: argparse.Namespace) -> None:
- if cmdargs.signoff:
- usercfg = b4.get_user_config()
- if 'name' not in usercfg or 'email' not in usercfg:
- logger.critical('CRITICAL: Unable to add your Signed-off-by: git returned no user.name or user.email')
+def get_series_start() -> str:
+ strategy = get_cover_strategy()
+ if strategy == 'commit':
+ # Easy, we start at the cover letter commit
+ return find_cover_commit()
+ if strategy == 'branch-description':
+ mybranch = b4.git_get_current_branch()
+ bcfg = b4.get_config_from_git(rf'branch\.{mybranch}\..*')
+ tracking = bcfg.get('b4-tracking')
+ if not tracking:
+ logger.critical('CRITICAL: Could not find tracking info for %s', mybranch)
+ sys.exit(1)
+ jdata = json.loads(tracking)
+ base_branch = jdata['series']['base-branch']
+ # Find merge-base with the tracking branch
+ logger.debug('Finding the fork-point with %s', base_branch)
+ gitargs = ['merge-base', '--fork-point', base_branch]
+ lines = b4.git_get_command_lines(None, gitargs)
+ if not lines:
+ logger.critical('CRITICAL: Could not find fork-point with base branch %s', base_branch)
sys.exit(1)
+ return lines[0]
+
+ # other strategies not yet implemented
+ logger.critical('CRITICAL: strategy %s not yet implemented', get_cover_strategy())
+ sys.exit(1)
+
+
+def update_trailers(cmdargs: argparse.Namespace) -> None:
+ usercfg = b4.get_user_config()
+ if 'name' not in usercfg or 'email' not in usercfg:
+ logger.critical('CRITICAL: Please set your user.name and user.email')
+ sys.exit(1)
+ if cmdargs.signoff:
signoff = ('Signed-off-by', f"{usercfg['name']} <{usercfg['email']}>", None)
else:
signoff = None
+ # If we are in an ez-series 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:
+ changeid = None
+ myemail = usercfg['email']
+ # There doesn't appear to be a great way to find the first commit
+ # where we're NOT the committer, so we get all commits since "3.months" where
+ # we're the committer and stop at the first non-contiguous parent
+ gitargs = ['log', '-F', f'--committer={myemail}', '--since=3.months', '--format=%H %P']
+ lines = b4.git_get_command_lines(None, gitargs)
+ if not lines:
+ logger.critical('CRITICAL: could not find any commits where committer=%s', myemail)
+ sys.exit(1)
+
+ prevparent = None
+ end = None
+ commit = None
+ for line in lines:
+ commit, parent = line.split()
+ if end is None:
+ end = commit
+ if prevparent is None:
+ prevparent = parent
+ continue
+ if prevparent != commit:
+ 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)
+
try:
- patches = b4.git_range_to_patches(None, cover_commit, 'HEAD')
+ patches = b4.git_range_to_patches(None, start, end)
except RuntimeError as ex:
logger.critical('CRITICAL: Failed to convert range to patches: %s', ex)
sys.exit(1)
logger.info('Calculating patch-ids from %s commits', len(patches)-1)
- msg_map = dict()
commit_map = dict()
+ by_patchid = dict()
+ by_subject = dict()
updates = dict()
# Ignore the cover letter
for commit, msg in patches[1:]:
+ commit_map[commit] = msg
body = msg.get_payload()
patchid = b4.LoreMessage.get_patch_id(body)
- msg_map[patchid] = msg
- commit_map[patchid] = commit
+ subject = msg.get('subject')
+ by_subject[subject] = commit
+ by_patchid[patchid] = commit
parts = b4.LoreMessage.get_body_parts(body)
# Force SOB update
if signoff and (signoff not in parts[2] or (len(signoff) > 1 and parts[2][-1] != signoff)):
- updates[patchid] = list()
+ updates[commit] = list()
if signoff not in parts[2]:
- updates[patchid].append(signoff)
+ updates[commit].append(signoff)
- if cmdargs.thread_msgid:
- cmdargs.msgid = cmdargs.thread_msgid
+ 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)
- else:
- cover, tracking = load_cover(cover_commit, strip_comments=True)
- changeid = tracking['series'].get('change-id')
+ elif 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
if list_msgs:
bbox = b4.LoreMailbox()
@@ -367,17 +569,25 @@ def update_trailers(cover_commit: str, cmdargs: argparse.Namespace) -> None:
if lser.has_cover and len(lser.patches[0].followup_trailers):
addtrailers += list(lser.patches[0].followup_trailers)
if not addtrailers:
- logger.debug('No follow-up trailers received to the %s', lmsg.subject)
+ logger.debug('No follow-up trailers received to: %s', lmsg.subject)
continue
- patchid = b4.LoreMessage.get_patch_id(lmsg.body)
- if patchid not in commit_map:
- logger.debug('No match for patchid %s', patchid)
+ commit = None
+ if lmsg.subject in by_subject:
+ commit = by_subject[lmsg.subject]
+ else:
+ patchid = b4.LoreMessage.get_patch_id(lmsg.body)
+ if patchid in by_patchid:
+ commit = by_patchid[patchid]
+ if not commit:
+ logger.debug('No match for %s', lmsg.full_subject)
continue
+
+ parts = b4.LoreMessage.get_body_parts(lmsg.body)
for ftrailer in addtrailers:
if ftrailer[:3] not in parts[2]:
- if patchid not in updates:
- updates[patchid] = list()
- updates[patchid].append(ftrailer)
+ if commit not in updates:
+ updates[commit] = list()
+ updates[commit].append(ftrailer)
# Check if we've applied mismatched trailers already
if not cmdargs.sloppytrailers and mismatches:
for mtrailer in list(mismatches):
@@ -386,24 +596,24 @@ def update_trailers(cover_commit: str, cmdargs: argparse.Namespace) -> None:
logger.debug('Removing already-applied mismatch %s', check)
mismatches.remove(mtrailer)
+ if len(mismatches):
+ logger.critical('---')
+ logger.critical('NOTE: some trailers ignored due to from/email mismatches:')
+ for tname, tvalue, fname, femail in lser.trailer_mismatches:
+ logger.critical(' ! Trailer: %s: %s', tname, tvalue)
+ logger.critical(' Msg From: %s <%s>', fname, femail)
+ logger.critical('NOTE: Rerun with -S to apply them anyway')
+
if not updates:
logger.info('No trailer updates found.')
return
- if len(mismatches):
- logger.critical('---')
- logger.critical('NOTE: some trailers ignored due to from/email mismatches:')
- for tname, tvalue, fname, femail in lser.trailer_mismatches:
- logger.critical(' ! Trailer: %s: %s', tname, tvalue)
- logger.critical(' Msg From: %s <%s>', fname, femail)
- logger.critical('NOTE: Rerun with -S to apply them anyway')
-
logger.info('---')
# Create the map of new messages
fred = FRCommitMessageEditor()
- for patchid, newtrailers in updates.items():
+ for commit, newtrailers in updates.items():
# Make it a LoreMessage, so we can run attestation on received trailers
- cmsg = b4.LoreMessage(msg_map[patchid])
+ cmsg = b4.LoreMessage(commit_map[commit])
logger.info(' %s', cmsg.subject)
if len(newtrailers):
cmsg.followup_trailers = newtrailers
@@ -412,10 +622,10 @@ def update_trailers(cover_commit: str, cmdargs: argparse.Namespace) -> None:
elif signoff:
logger.info(' > %s: %s', signoff[0], signoff[1])
cmsg.fix_trailers(signoff=signoff)
- fred.add(commit_map[patchid], cmsg.message)
+ fred.add(commit, cmsg.message)
logger.info('---')
- args = fr.FilteringOptions.parse_args(['--force', '--quiet', '--refs', f'{cover_commit}..HEAD'])
- args.refs = [f'{cover_commit}..HEAD']
+ args = fr.FilteringOptions.parse_args(['--force', '--quiet', '--refs', f'{start}..'])
+ args.refs = [f'{start}..']
frf = fr.RepoFilter(args, commit_callback=fred.callback)
logger.info('Invoking git-filter-repo to update trailers.')
frf.run()
@@ -434,22 +644,22 @@ def get_addresses_from_cmd(cmdargs: List[str], msgbytes: bytes) -> List[Tuple[st
return utils.getaddresses(addrs.split('\n'))
-def get_series_details(cover_commit: str) -> Tuple[str, str, str]:
+def get_series_details(start_commit: str) -> Tuple[str, str, str]:
# Not sure if we can reasonably expect all automation to handle this correctly
# gitargs = ['describe', '--long', f'{cover_commit}~1']
- gitargs = ['rev-parse', f'{cover_commit}~1']
+ gitargs = ['rev-parse', f'{start_commit}~1']
lines = b4.git_get_command_lines(None, gitargs)
base_commit = lines[0]
- gitargs = ['shortlog', f'{cover_commit}..']
+ gitargs = ['shortlog', f'{start_commit}..']
ecode, shortlog = b4.git_run_command(None, gitargs)
- gitargs = ['diff', '--stat', f'{cover_commit}..']
+ gitargs = ['diff', '--stat', f'{start_commit}..']
ecode, diffstat = b4.git_run_command(None, gitargs)
return base_commit, shortlog.rstrip(), diffstat.rstrip()
-def send(cover_commit: str, cmdargs: argparse.Namespace) -> None:
+def cmd_ez_send(cmdargs: argparse.Namespace) -> None:
# Check if the cover letter has 'EDITME' in it
- cover, tracking = load_cover(cover_commit, strip_comments=True)
+ 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('---')
@@ -475,7 +685,8 @@ def send(cover_commit: str, cmdargs: argparse.Namespace) -> None:
# Put together the cover letter
csubject, cbody = cover.split('\n', maxsplit=1)
- base_commit, shortlog, diffstat = get_series_details(cover_commit)
+ start_commit = get_series_start()
+ base_commit, shortlog, diffstat = get_series_details(start_commit=start_commit)
change_id = tracking['series'].get('change-id')
revision = tracking['series'].get('revision')
tptvals = {
@@ -519,7 +730,7 @@ def send(cover_commit: str, cmdargs: argparse.Namespace) -> None:
msgid_tpt = f'<{stablepart}-v{revision}-%s-{randompart}@{msgdomain}>'
try:
- patches = b4.git_range_to_patches(None, cover_commit, 'HEAD',
+ patches = b4.git_range_to_patches(None, start_commit, 'HEAD',
covermsg=cmsg, prefixes=prefixes,
msgid_tpt=msgid_tpt,
seriests=seriests,
@@ -679,18 +890,13 @@ def send(cover_commit: str, cmdargs: argparse.Namespace) -> None:
return
logger.info('Recording series message-id in cover letter tracking')
- cover, tracking = load_cover(cover_commit, strip_comments=False)
+ cover, tracking = load_cover(strip_comments=False)
vrev = f'v{revision}'
if 'history' not in tracking['series']:
tracking['series']['history'] = dict()
if vrev not in tracking['series']['history']:
tracking['series']['history'][vrev] = list()
tracking['series']['history'][vrev].append(cover_msgid)
- update_cover(cover_commit, cover, tracking)
-
-
-def reroll(cover_commit: str, cmdargs: argparse.Namespace) -> None:
- cover, tracking = load_cover(cover_commit, strip_comments=False)
oldrev = tracking['series']['revision']
newrev = oldrev + 1
tracking['series']['revision'] = newrev
@@ -721,60 +927,43 @@ def reroll(cover_commit: str, cmdargs: argparse.Namespace) -> None:
new_cover = '---\n'.join(new_sections)
else:
new_cover = cover + '\n\n---\n' + prepend
+
logger.info('Created new revision v%s', newrev)
logger.info('Updating cover letter with templated changelog entries.')
- update_cover(cover_commit, new_cover, tracking)
- logger.info('You may now edit the cover letter using "b4 submit --edit-cover"')
+ store_cover(new_cover, tracking)
-def main(cmdargs: argparse.Namespace) -> None:
+def check_can_gfr():
if not can_gfr:
logger.critical('ERROR: b4 submit requires git-filter-repo. You should be able')
logger.critical(' to install it from your distro packages, or from pip.')
sys.exit(1)
- config = b4.get_main_config()
- if 'submit-endpoint' not in config:
- config['submit-endpoint'] = 'https://lkml.kernel.org/_b4_submit'
-
- if cmdargs.new_series_name:
- start_new_series(cmdargs)
- return
-
- if not check_our_branch():
- return
- cover_commit = find_cover_commit()
- if not cover_commit:
- logger.critical('CRITICAL: Unable to find cover letter commit')
+def cmd_ez_series(cmdargs: argparse.Namespace) -> None:
+ check_can_gfr()
+ status = b4.git_get_repo_status()
+ if len(status):
+ logger.critical('CRITICAL: Repository contains uncommitted changes.')
+ logger.critical(' Stash or commit them first.')
sys.exit(1)
if cmdargs.edit_cover:
- edit_cover(cover_commit)
- return
+ return edit_cover()
- elif cmdargs.update_trailers:
- update_trailers(cover_commit, cmdargs)
- return
-
- elif cmdargs.send:
- send(cover_commit, cmdargs)
- return
+ if is_ez_branch():
+ logger.critical('CRITICAL: This appears to already be an ez-series branch.')
+ sys.exit(1)
- elif cmdargs.reroll:
- reroll(cover_commit, cmdargs)
- return
+ return start_new_series(cmdargs)
- logger.critical('No action requested, please see "b4 submit --help"')
- sys.exit(1)
- # if not can_patatt:
- # logger.critical('ERROR: b4 submit requires patatt library. See:')
- # logger.critical(' https://git.kernel.org/pub/scm/utils/patatt/patatt.git/about/')
- # sys.exit(1)
+def cmd_ez_trailers(cmdargs: argparse.Namespace) -> None:
+ check_can_gfr()
+ status = b4.git_get_repo_status()
+ if len(status):
+ logger.critical('CRITICAL: Repository contains uncommitted changes.')
+ logger.critical(' Stash or commit them first.')
+ sys.exit(1)
- # if cmdargs.web_auth_new:
- # auth_new(cmdargs)
- #
- # if cmdargs.web_auth_verify:
- # auth_verify(cmdargs)
+ update_trailers(cmdargs)