* [PATCH b4 0/3] shazam: conflict resolution support for b4 shazam -H
@ 2026-03-06 11:52 Christian Brauner
2026-03-06 11:52 ` [PATCH b4 1/3] shazam: refactor git_fetch_am_into_repo for deterministic worktree Christian Brauner
` (2 more replies)
0 siblings, 3 replies; 6+ messages in thread
From: Christian Brauner @ 2026-03-06 11:52 UTC (permalink / raw)
To: Kernel.org Tools
Cc: Christian Brauner, Konstantin Ryabitsev, Christian Brauner,
Claude Opus 4.6
When b4 shazam -H applies patches via git-am in a sparse worktree,
conflicts cause the worktree to be destroyed before the user can act.
Additionally, three-way merge was never enabled, so even
auto-resolvable conflicts would fail.
This series fixes both problems and adds a conflict resolution
workflow:
Patch 1 refactors git_fetch_am_into_repo to use a deterministic
worktree path and a structured AmConflictError exception, laying the
groundwork for the worktree to survive a failed git-am.
Patch 2 passes -3 to git-am so three-way merge is attempted
automatically, and adds a shazam-am-flags config option for further
customization.
Patch 3 adds --resolve, --continue, and --abort flags. With
--resolve, a conflict triggers a workflow where successfully applied
patches are fetched into FETCH_HEAD, a no-ff no-commit merge is
started in the user's working tree, and remaining patches are applied
one at a time with git apply --3way. The user resolves conflicts with
their normal tools in their full working tree rather than in the
sparse worktree.
Sample output:
brauner@so61 ⊼οѕ ~/src/git/linux/vfs/vfs-7.1.casefolding|vfs-7.1.casefolding $%=
> b4 shazam -H --merge-base=HEAD --sloppy-trailers --resolve 20260217214741.1928576-1-cel@kernel.org
Looking up https://lore.kernel.org/20260217214741.1928576-1-cel@kernel.org/
Grabbing thread from lore.kernel.org/all/20260217214741.1928576-1-cel@kernel.org/t.mbox.gz
Checking for newer revisions
Grabbing search results from lore.kernel.org
Analyzing 22 messages in the thread
Analyzing 49 code-review messages
Checking attestation on all messages, may take a moment...
---
✓ [PATCH v8 1/17] fs: Move file_kattr initialization to callers
+ Link: https://patch.msgid.link/20260217214741.1928576-2-cel@kernel.org
+ Reviewed-by: Jan Kara <jack@suse.cz> (✓ DKIM/suse.cz)
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 2/17] fs: Add case sensitivity flags to file_kattr
+ Link: https://patch.msgid.link/20260217214741.1928576-3-cel@kernel.org
+ Reviewed-by: Jan Kara <jack@suse.cz> (✓ DKIM/suse.cz)
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 3/17] fat: Implement fileattr_get for case sensitivity
+ Link: https://patch.msgid.link/20260217214741.1928576-4-cel@kernel.org
+ Reviewed-by: Jan Kara <jack@suse.cz> (✓ DKIM/suse.cz)
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 4/17] exfat: Implement fileattr_get for case sensitivity
+ Link: https://patch.msgid.link/20260217214741.1928576-5-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 5/17] ntfs3: Implement fileattr_get for case sensitivity
+ Link: https://patch.msgid.link/20260217214741.1928576-6-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 6/17] hfs: Implement fileattr_get for case sensitivity
+ Link: https://patch.msgid.link/20260217214741.1928576-7-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 7/17] hfsplus: Report case sensitivity in fileattr_get
+ Link: https://patch.msgid.link/20260217214741.1928576-8-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 8/17] ext4: Report case sensitivity in fileattr_get
+ Link: https://patch.msgid.link/20260217214741.1928576-9-cel@kernel.org
+ Acked-by: Theodore Ts'o <tytso@mit.edu> (✓ DKIM/mit.edu)
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 9/17] xfs: Report case sensitivity in fileattr_get
+ Link: https://patch.msgid.link/20260217214741.1928576-10-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 10/17] cifs: Implement fileattr_get for case sensitivity
+ Link: https://patch.msgid.link/20260217214741.1928576-11-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 11/17] nfs: Implement fileattr_get for case sensitivity
+ Link: https://patch.msgid.link/20260217214741.1928576-12-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 12/17] f2fs: Add case sensitivity reporting to fileattr_get
+ Link: https://patch.msgid.link/20260217214741.1928576-13-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 13/17] vboxsf: Implement fileattr_get for case sensitivity
+ Link: https://patch.msgid.link/20260217214741.1928576-14-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 14/17] isofs: Implement fileattr_get for case sensitivity
+ Link: https://patch.msgid.link/20260217214741.1928576-15-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 15/17] nfsd: Report export case-folding via NFSv3 PATHCONF
+ Link: https://patch.msgid.link/20260217214741.1928576-16-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 16/17] nfsd: Implement NFSv4 FATTR4_CASE_INSENSITIVE and FATTR4_CASE_PRESERVING
+ Link: https://patch.msgid.link/20260217214741.1928576-17-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
✓ [PATCH v8 17/17] ksmbd: Report filesystem case sensitivity via FS_ATTRIBUTE_INFORMATION
+ Link: https://patch.msgid.link/20260217214741.1928576-18-cel@kernel.org
+ Signed-off-by: Christian Brauner <brauner@kernel.org>
---
✓ Signed: DKIM/kernel.org
---
Total patches: 17
---
Base: HEAD
Magic: Preparing a sparse worktree
---
Applying: fs: Move file_kattr initialization to callers
Applying: fs: Add case sensitivity flags to file_kattr
Applying: fat: Implement fileattr_get for case sensitivity
Applying: exfat: Implement fileattr_get for case sensitivity
Applying: ntfs3: Implement fileattr_get for case sensitivity
Applying: hfs: Implement fileattr_get for case sensitivity
Applying: hfsplus: Report case sensitivity in fileattr_get
Applying: ext4: Report case sensitivity in fileattr_get
Applying: xfs: Report case sensitivity in fileattr_get
Applying: cifs: Implement fileattr_get for case sensitivity
Applying: nfs: Implement fileattr_get for case sensitivity
Applying: f2fs: Add case sensitivity reporting to fileattr_get
Applying: vboxsf: Implement fileattr_get for case sensitivity
Applying: isofs: Implement fileattr_get for case sensitivity
Applying: nfsd: Report export case-folding via NFSv3 PATHCONF
Applying: nfsd: Implement NFSv4 FATTR4_CASE_INSENSITIVE and FATTR4_CASE_PRESERVING
Using index info to reconstruct a base tree...
M fs/nfsd/nfs4xdr.c
Falling back to patching base and 3-way merge...
Patch failed at 0016 nfsd: Implement NFSv4 FATTR4_CASE_INSENSITIVE and FATTR4_CASE_PRESERVING
error: Your local changes to the following files would be overwritten by merge:
fs/nfsd/nfs4xdr.c
Please commit your changes or stash them before you merge.
Aborting
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config advice.mergeConflict false"
---
Patch series did not apply cleanly, resolving...
Fetching successfully applied patches into FETCH_HEAD
Merging successfully applied patches into your branch...
Applying remaining patch 1/2...
---
Applied patch to 'fs/nfsd/nfs4xdr.c' with conflicts.
U fs/nfsd/nfs4xdr.c
Resolved 'fs/nfsd/nfs4xdr.c' using previous resolution.
---
Remaining patch 1/2 did not apply cleanly.
Resolve conflicts in your working tree, then run: b4 shazam --continue
To abort: b4 shazam --abort
brauner@so61 ⊼οѕ ~/src/git/linux/vfs/vfs-7.1.casefolding|vfs-7.1.casefolding *+$%=|MERGING
> git add fs/nfsd/nfs4xdr.c
brauner@so61 ⊼οѕ ~/src/git/linux/vfs/vfs-7.1.casefolding|vfs-7.1.casefolding +$%=|MERGING
> b4 shazam --continue
Applying remaining patch 2/2...
Invoking: git commit -F /home/brauner/src/git/linux/vfs/master/.git/worktrees/vfs-7.1.casefolding/b4-cover --signoff --edit
[vfs-7.1.casefolding b125478aef10] Merge patch series "Subject: Exposing case folding behavior"
Signed-off-by: Christian Brauner <brauner@kernel.org>
---
Christian Brauner (3):
shazam: refactor git_fetch_am_into_repo for deterministic worktree
shazam: enable three-way merge for b4 shazam -H
shazam: enable merge conflict resolution for b4 shazam -H --resolve
src/b4/__init__.py | 51 ++++++--
src/b4/command.py | 6 +
src/b4/mbox.py | 361 +++++++++++++++++++++++++++++++++++++++++++++++++----
3 files changed, 385 insertions(+), 33 deletions(-)
---
base-commit: 5b4eaae60fd938a865dda1916885e9a1c7ddce8a
change-id: 20260306-master-89938331cc50
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH b4 1/3] shazam: refactor git_fetch_am_into_repo for deterministic worktree
2026-03-06 11:52 [PATCH b4 0/3] shazam: conflict resolution support for b4 shazam -H Christian Brauner
@ 2026-03-06 11:52 ` Christian Brauner
2026-03-06 16:10 ` Konstantin Ryabitsev
2026-03-06 11:52 ` [PATCH b4 2/3] shazam: enable three-way merge for b4 shazam -H Christian Brauner
2026-03-06 11:52 ` [PATCH b4 3/3] shazam: enable merge conflict resolution for b4 shazam -H --resolve Christian Brauner
2 siblings, 1 reply; 6+ messages in thread
From: Christian Brauner @ 2026-03-06 11:52 UTC (permalink / raw)
To: Kernel.org Tools
Cc: Christian Brauner, Konstantin Ryabitsev, Christian Brauner,
Claude Opus 4.6
Replace the git_temp_worktree context manager with a deterministic
worktree path (<git-common-dir>/b4-shazam-worktree) and manual
lifecycle management. This is preparation for conflict resolution
support where the worktree needs to persist after a failed git-am.
Add AmConflictError(RuntimeError) exception so callers can
distinguish am failures from other errors. Since it inherits from
RuntimeError, existing callers that catch RuntimeError continue to
work unchanged.
Add an am_flags parameter to allow callers to pass additional flags
to git-am (e.g., -3 for three-way merge).
Clean up any stale worktree from a previous run at the start of
each invocation. On am failure, the worktree is intentionally left
behind (for future conflict resolution use); the stale-worktree
cleanup handles this on the next run.
Update the shazam caller in make_am to explicitly catch
AmConflictError, clean up the worktree, and log the failure,
preserving the current user-visible behavior.
Also fix a minor str(gwt) -> gwt in the FETCH_HEAD origin rewrite
since gwt is already a str.
Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Christian Brauner <brauner@kernel.org>
---
src/b4/__init__.py | 51 +++++++++++++++++++++++++++++++++++++++------------
src/b4/mbox.py | 8 ++++++++
2 files changed, 47 insertions(+), 12 deletions(-)
diff --git a/src/b4/__init__.py b/src/b4/__init__.py
index eab290b..0baaec8 100644
--- a/src/b4/__init__.py
+++ b/src/b4/__init__.py
@@ -73,6 +73,13 @@ __VERSION__ = '0.15-dev'
PW_REST_API_VERSION = '1.2'
+class AmConflictError(RuntimeError):
+ def __init__(self, worktree_path: str, output: str):
+ self.worktree_path = worktree_path
+ self.output = output
+ super().__init__(output)
+
+
def _dkim_log_filter(record: logging.LogRecord) -> bool:
# Hide all dkim logging output in normal operation by setting the level to
# DEBUG. If debugging output has been enabled then prefix dkim logging
@@ -4721,31 +4728,48 @@ def git_revparse_obj(gitobj: str, gitdir: Optional[str] = None) -> str:
def git_fetch_am_into_repo(gitdir: Optional[str], ambytes: bytes, at_base: str = 'HEAD',
- origin: Optional[str] = None, check_only: bool = False) -> None:
+ origin: Optional[str] = None, check_only: bool = False,
+ am_flags: Optional[List[str]] = None) -> None:
if gitdir is None:
gitdir = os.getcwd()
topdir = git_get_toplevel(gitdir)
- with git_temp_worktree(topdir, at_base) as gwt:
+ common_dir = git_get_common_dir(topdir)
+ if not common_dir:
+ raise RuntimeError('Unable to determine git common dir')
+ gwt = os.path.join(common_dir, 'b4-shazam-worktree')
+
+ # Clean up any stale worktree from a previous run
+ if os.path.exists(gwt):
+ git_run_command(topdir, ['worktree', 'remove', '--force', gwt])
+
+ gitargs = ['worktree', 'add', '--detach', '--no-checkout', gwt]
+ if at_base:
+ gitargs.append(at_base)
+ ecode, out = git_run_command(topdir, gitargs, logstderr=True)
+ if ecode > 0:
+ raise RuntimeError('Failed to create worktree: %s' % out.strip())
+
+ cleanup = True
+ try:
logger.info('Magic: Preparing a sparse worktree')
- ecode, out = git_run_command(gwt, ['sparse-checkout', 'set'], logstderr=True)
+ ecode, out = git_run_command(gwt, ['sparse-checkout', 'set'], logstderr=True, rundir=gwt)
if ecode > 0:
logger.critical('Error running sparse-checkout set')
logger.critical(out)
raise RuntimeError
- ecode, out = git_run_command(gwt, ['checkout', '-f'], logstderr=True)
+ ecode, out = git_run_command(gwt, ['checkout', '-f'], logstderr=True, rundir=gwt)
if ecode > 0:
logger.critical('Error running checkout into sparse workdir')
logger.critical(out)
raise RuntimeError
- ecode, out = git_run_command(gwt, ['am'], stdin=ambytes, logstderr=True)
+ amargs = ['am']
+ if am_flags:
+ amargs.extend(am_flags)
+ ecode, out = git_run_command(gwt, amargs, stdin=ambytes, logstderr=True, rundir=gwt)
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')
- raise RuntimeError
+ cleanup = False
+ raise AmConflictError(gwt, out.strip())
if check_only:
return
logger.info('---')
@@ -4758,6 +4782,9 @@ def git_fetch_am_into_repo(gitdir: Optional[str], ambytes: bytes, at_base: str =
logger.critical('Unable to fetch from the worktree')
logger.critical(out.strip())
raise RuntimeError
+ finally:
+ if cleanup:
+ git_run_command(topdir, ['worktree', 'remove', '--force', gwt])
if not origin:
return
@@ -4772,7 +4799,7 @@ def git_fetch_am_into_repo(gitdir: Optional[str], ambytes: bytes, at_base: str =
with open(fhf.rstrip(), 'r') as fhh:
contents = fhh.read()
mmsg = 'patches from %s' % origin
- new_contents = contents.replace(str(gwt), mmsg)
+ new_contents = contents.replace(gwt, mmsg)
if new_contents != contents:
with open(fhf.rstrip(), 'w') as fhh:
fhh.write(new_contents)
diff --git a/src/b4/mbox.py b/src/b4/mbox.py
index 3d12f8e..132cbb9 100644
--- a/src/b4/mbox.py
+++ b/src/b4/mbox.py
@@ -389,6 +389,14 @@ def make_am(msgs: List[EmailMessage], cmdargs: argparse.Namespace, msgid: str) -
else:
logger.info(' Base: %s (use --merge-base to override)', base_commit)
b4.git_fetch_am_into_repo(topdir, ambytes=ambytes, at_base=base_commit, origin=linkurl)
+ except b4.AmConflictError as cex:
+ b4.git_run_command(topdir, ['worktree', 'remove', '--force', cex.worktree_path])
+ logger.critical('Unable to cleanly apply series, see failure log below')
+ logger.critical('---')
+ logger.critical(cex.output)
+ logger.critical('---')
+ logger.critical('Not fetching into FETCH_HEAD')
+ sys.exit(1)
except RuntimeError:
sys.exit(1)
--
2.47.3
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH b4 2/3] shazam: enable three-way merge for b4 shazam -H
2026-03-06 11:52 [PATCH b4 0/3] shazam: conflict resolution support for b4 shazam -H Christian Brauner
2026-03-06 11:52 ` [PATCH b4 1/3] shazam: refactor git_fetch_am_into_repo for deterministic worktree Christian Brauner
@ 2026-03-06 11:52 ` Christian Brauner
2026-03-06 11:52 ` [PATCH b4 3/3] shazam: enable merge conflict resolution for b4 shazam -H --resolve Christian Brauner
2 siblings, 0 replies; 6+ messages in thread
From: Christian Brauner @ 2026-03-06 11:52 UTC (permalink / raw)
To: Kernel.org Tools
Cc: Christian Brauner, Konstantin Ryabitsev, Christian Brauner,
Claude Opus 4.6
Pass -3 to git-am so three-way merge is attempted automatically when
applying patches in the sparse worktree. Without this, even
auto-resolvable conflicts cause git-am to fail.
Also support a shazam-am-flags config option to allow users to pass
additional flags to git-am.
Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Christian Brauner <brauner@kernel.org>
---
src/b4/mbox.py | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/b4/mbox.py b/src/b4/mbox.py
index 132cbb9..0a86f6f 100644
--- a/src/b4/mbox.py
+++ b/src/b4/mbox.py
@@ -383,12 +383,21 @@ def make_am(msgs: List[EmailMessage], cmdargs: argparse.Namespace, msgid: str) -
base_commit = get_base_commit(topdir, first_body, lser, cmdargs)
linkurl = linkmask % top_msgid
+
+ am_flags = ['-3']
+ amflags_cfg = str(config.get('shazam-am-flags', ''))
+ if amflags_cfg:
+ sp = shlex.shlex(amflags_cfg, posix=True)
+ sp.whitespace_split = True
+ am_flags.extend(list(sp))
+
try:
if cmdargs.mergebase:
logger.info(' Base: %s', base_commit)
else:
logger.info(' Base: %s (use --merge-base to override)', base_commit)
- b4.git_fetch_am_into_repo(topdir, ambytes=ambytes, at_base=base_commit, origin=linkurl)
+ b4.git_fetch_am_into_repo(topdir, ambytes=ambytes, at_base=base_commit,
+ origin=linkurl, am_flags=am_flags)
except b4.AmConflictError as cex:
b4.git_run_command(topdir, ['worktree', 'remove', '--force', cex.worktree_path])
logger.critical('Unable to cleanly apply series, see failure log below')
--
2.47.3
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH b4 3/3] shazam: enable merge conflict resolution for b4 shazam -H --resolve
2026-03-06 11:52 [PATCH b4 0/3] shazam: conflict resolution support for b4 shazam -H Christian Brauner
2026-03-06 11:52 ` [PATCH b4 1/3] shazam: refactor git_fetch_am_into_repo for deterministic worktree Christian Brauner
2026-03-06 11:52 ` [PATCH b4 2/3] shazam: enable three-way merge for b4 shazam -H Christian Brauner
@ 2026-03-06 11:52 ` Christian Brauner
2 siblings, 0 replies; 6+ messages in thread
From: Christian Brauner @ 2026-03-06 11:52 UTC (permalink / raw)
To: Kernel.org Tools
Cc: Christian Brauner, Konstantin Ryabitsev, Christian Brauner,
Claude Opus 4.6
When b4 shazam -H applies patches via git-am in a temporary sparse
worktree, conflicts previously caused the worktree to be destroyed
before the user could act.
Add a conflict resolution workflow with three new flags:
--resolve When git-am fails, the successfully applied patches are
fetched into FETCH_HEAD, the sparse worktree is removed,
and remaining patches are extracted. A
git merge --no-ff --no-commit FETCH_HEAD is started in
the user's working tree, then remaining patches are
applied one at a time with git apply --3way. On conflict
the user resolves with their normal tools in their full
working tree.
--continue After resolving conflicts, stages the resolution and
applies any further remaining patches. When all patches
are applied, commits the merge with the cover letter
message (opening an editor unless --no-interactive).
--abort Cleans up all state: removes the patches directory,
aborts any in-progress merge, and removes a stale
worktree if one exists.
Without --resolve, a conflict cleans up the worktree and exits with
an error, preserving the original behavior.
Conflict resolution happens in the user's branch rather than the
sparse worktree, which avoids problems with sparse-checkout blocking
git-add on files outside the checkout definition and gives the user
full tree context for resolving conflicts.
Shazam state (origin URL, merge template, flags) is saved to
<git-common-dir>/b4-shazam-state.json so --continue and --abort can
operate across separate invocations.
Patches that applied cleanly are preserved as individual commits
reachable from the merge's second parent via FETCH_HEAD. Patches that
required manual resolution have their changes folded into the merge
commit.
Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Christian Brauner <brauner@kernel.org>
---
src/b4/command.py | 6 +
src/b4/mbox.py | 378 ++++++++++++++++++++++++++++++++++++++++++++++++------
2 files changed, 346 insertions(+), 38 deletions(-)
diff --git a/src/b4/command.py b/src/b4/command.py
index a49a8bc..859dd5f 100644
--- a/src/b4/command.py
+++ b/src/b4/command.py
@@ -231,6 +231,12 @@ def setup_parser() -> argparse.ArgumentParser:
'(default: 3 weeks)'))
sp_sh.add_argument('--merge-base', dest='mergebase', type=str, default=None,
help='(use with -H or -M) Force this base when merging')
+ sp_sh.add_argument('--resolve', dest='shazam_resolve', action='store_true', default=False,
+ help='(use with -H or -M) Enable conflict resolution if patches fail to apply')
+ sp_sh.add_argument('--continue', dest='shazam_continue', action='store_true', default=False,
+ help='Continue after resolving merge conflicts from --resolve')
+ sp_sh.add_argument('--abort', dest='shazam_abort', action='store_true', default=False,
+ help='Abort a conflicted shazam and clean up')
sp_sh.set_defaults(func=cmd_shazam)
# b4 review
diff --git a/src/b4/mbox.py b/src/b4/mbox.py
index 0a86f6f..5cec332 100644
--- a/src/b4/mbox.py
+++ b/src/b4/mbox.py
@@ -384,38 +384,6 @@ def make_am(msgs: List[EmailMessage], cmdargs: argparse.Namespace, msgid: str) -
base_commit = get_base_commit(topdir, first_body, lser, cmdargs)
linkurl = linkmask % top_msgid
- am_flags = ['-3']
- amflags_cfg = str(config.get('shazam-am-flags', ''))
- if amflags_cfg:
- sp = shlex.shlex(amflags_cfg, posix=True)
- sp.whitespace_split = True
- am_flags.extend(list(sp))
-
- try:
- if cmdargs.mergebase:
- logger.info(' Base: %s', base_commit)
- else:
- logger.info(' Base: %s (use --merge-base to override)', base_commit)
- b4.git_fetch_am_into_repo(topdir, ambytes=ambytes, at_base=base_commit,
- origin=linkurl, am_flags=am_flags)
- except b4.AmConflictError as cex:
- b4.git_run_command(topdir, ['worktree', 'remove', '--force', cex.worktree_path])
- logger.critical('Unable to cleanly apply series, see failure log below')
- logger.critical('---')
- logger.critical(cex.output)
- logger.critical('---')
- logger.critical('Not fetching into FETCH_HEAD')
- sys.exit(1)
- except RuntimeError:
- sys.exit(1)
-
- gitargs = ['rev-parse', '--git-dir']
- ecode, out = 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(out.rstrip(), 'b4-cover')
merge_template = DEFAULT_MERGE_TEMPLATE
if config.get('shazam-merge-template'):
# Try to load this template instead
@@ -426,11 +394,6 @@ def make_am(msgs: List[EmailMessage], cmdargs: argparse.Namespace, msgid: str) -
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 and lser.patches[0] is not None:
clmsg: b4.LoreMessage = lser.patches[0]
parts = b4.LoreMessage.get_body_parts(clmsg.body)
@@ -457,11 +420,68 @@ def make_am(msgs: List[EmailMessage], cmdargs: argparse.Namespace, msgid: str) -
else:
tptvals['patch_or_series'] = 'patch'
+ mergeflags = str(config.get('shazam-merge-flags', '--signoff'))
+
+ am_flags = ['-3']
+ amflags_cfg = str(config.get('shazam-am-flags', ''))
+ if amflags_cfg:
+ sp = shlex.shlex(amflags_cfg, posix=True)
+ sp.whitespace_split = True
+ am_flags.extend(list(sp))
+
+ try:
+ if cmdargs.mergebase:
+ logger.info(' Base: %s', base_commit)
+ else:
+ logger.info(' Base: %s (use --merge-base to override)', base_commit)
+ b4.git_fetch_am_into_repo(topdir, ambytes=ambytes, at_base=base_commit,
+ origin=linkurl, am_flags=am_flags)
+ except b4.AmConflictError as cex:
+ gwt = cex.worktree_path
+ if not getattr(cmdargs, 'shazam_resolve', False):
+ b4.git_run_command(topdir, ['worktree', 'remove', '--force', gwt])
+ logger.critical('Unable to cleanly apply series, see failure log below')
+ logger.critical('---')
+ logger.critical(cex.output)
+ logger.critical('---')
+ logger.critical('Not fetching into FETCH_HEAD')
+ logger.critical('Use --resolve to enable conflict resolution')
+ sys.exit(1)
+
+ common_dir = b4.git_get_common_dir(topdir)
+ if not common_dir:
+ logger.critical('Unable to determine git common dir')
+ b4.git_run_command(topdir, ['worktree', 'remove', '--force', gwt])
+ sys.exit(1)
+
+ state = {
+ 'origin': linkurl,
+ 'merge_template_values': tptvals,
+ 'merge_template': merge_template,
+ 'merge_flags': mergeflags,
+ 'no_interactive': cmdargs.no_interactive,
+ }
+ _start_merge_resolve(topdir, cex, common_dir, state)
+ except RuntimeError:
+ sys.exit(1)
+
+ gitargs = ['rev-parse', '--git-dir']
+ ecode, out = 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(out.rstrip(), 'b4-cover')
+
+ # 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)
+
body = Template(merge_template).safe_substitute(tptvals)
with open(mmf, 'w') as mmh:
mmh.write(body)
- mergeflags = str(config.get('shazam-merge-flags', '--signoff'))
sp = shlex.shlex(mergeflags, posix=True)
sp.whitespace_split = True
if cmdargs.no_interactive:
@@ -861,9 +881,291 @@ def minimize_thread(msgs: List[EmailMessage]) -> List[EmailMessage]:
return mmsgs
+def _start_merge_resolve(topdir: str, cex: b4.AmConflictError,
+ common_dir: str, state: Dict) -> None:
+ gwt = cex.worktree_path
+ logger.critical('---')
+ logger.critical(cex.output)
+ logger.critical('---')
+ logger.critical('Patch series did not apply cleanly, resolving...')
+
+ # Find rebase-apply in the worktree
+ ecode, gitdir = b4.git_run_command(gwt, ['rev-parse', '--git-dir'],
+ logstderr=True, rundir=gwt)
+ if ecode > 0:
+ logger.critical('Unable to find git directory in worktree')
+ b4.git_run_command(topdir, ['worktree', 'remove', '--force', gwt])
+ sys.exit(1)
+ rebase_apply = os.path.join(gitdir.strip(), 'rebase-apply')
+ if not os.path.isdir(rebase_apply):
+ logger.critical('No git-am state found in worktree.')
+ b4.git_run_command(topdir, ['worktree', 'remove', '--force', gwt])
+ sys.exit(1)
+
+ # Extract remaining patches
+ with open(os.path.join(rebase_apply, 'next'), 'r') as fh:
+ next_num = int(fh.read().strip())
+ with open(os.path.join(rebase_apply, 'last'), 'r') as fh:
+ last_num = int(fh.read().strip())
+
+ patches_dir = os.path.join(common_dir, 'b4-shazam-patches')
+ if os.path.exists(patches_dir):
+ shutil.rmtree(patches_dir)
+ os.makedirs(patches_dir)
+
+ patch_count = 0
+ for i in range(next_num, last_num + 1):
+ src = os.path.join(rebase_apply, f'{i:04d}')
+ if os.path.exists(src):
+ dst = os.path.join(patches_dir, f'{patch_count:04d}')
+ shutil.copy2(src, dst)
+ patch_count += 1
+
+ with open(os.path.join(patches_dir, 'total'), 'w') as fh:
+ fh.write(str(patch_count))
+ with open(os.path.join(patches_dir, 'current'), 'w') as fh:
+ fh.write('0')
+
+ # Check for uncommitted changes
+ status_lines = b4.git_get_repo_status(topdir)
+ if status_lines:
+ logger.critical('You have uncommitted changes in your working tree.')
+ logger.critical('Please commit or stash them before resolving.')
+ shutil.rmtree(patches_dir)
+ b4.git_run_command(topdir, ['worktree', 'remove', '--force', gwt])
+ sys.exit(1)
+
+ # Fetch successfully applied patches into FETCH_HEAD
+ logger.info('Fetching successfully applied patches into FETCH_HEAD')
+ ecode, out = b4.git_run_command(topdir, ['fetch', gwt], logstderr=True)
+ if ecode > 0:
+ logger.critical('Unable to fetch from the worktree')
+ logger.critical(out.strip())
+ shutil.rmtree(patches_dir)
+ b4.git_run_command(topdir, ['worktree', 'remove', '--force', gwt])
+ sys.exit(1)
+
+ # Rewrite FETCH_HEAD origin
+ origin = state.get('origin')
+ if origin:
+ gitargs = ['rev-parse', '--git-path', 'FETCH_HEAD']
+ ecode, fhf = b4.git_run_command(topdir, gitargs, logstderr=True)
+ if ecode == 0:
+ fhf = fhf.rstrip()
+ with open(fhf, 'r') as fhh:
+ contents = fhh.read()
+ mmsg = 'patches from %s' % origin
+ new_contents = contents.replace(gwt, mmsg)
+ if new_contents != contents:
+ with open(fhf, 'w') as fhh:
+ fhh.write(new_contents)
+
+ # Remove the worktree
+ b4.git_run_command(topdir, ['worktree', 'remove', '--force', gwt])
+
+ # Save state for --continue/--abort
+ state_file = os.path.join(common_dir, 'b4-shazam-state.json')
+ with open(state_file, 'w') as sfh:
+ json.dump(state, sfh, indent=2)
+
+ # Start merge of successfully applied patches
+ logger.info('Merging successfully applied patches into your branch...')
+ ecode, out = b4.git_run_command(topdir, ['merge', '--no-ff', '--no-commit', 'FETCH_HEAD'],
+ logstderr=True, rundir=topdir)
+
+ if ecode > 0:
+ logger.warning('Merge had conflicts:')
+ logger.warning(out.strip())
+ logger.warning('Resolve conflicts, then run: b4 shazam --continue')
+ logger.warning('To abort: b4 shazam --abort')
+ sys.exit(1)
+
+ # Merge was clean, apply remaining patches
+ _apply_remaining_patches(topdir, patches_dir, state, state_file, common_dir)
+ sys.exit(0)
+
+
+def _apply_remaining_patches(topdir: str, patches_dir: str, state: Dict,
+ state_file: str, common_dir: str) -> None:
+ with open(os.path.join(patches_dir, 'total'), 'r') as fh:
+ total = int(fh.read().strip())
+ with open(os.path.join(patches_dir, 'current'), 'r') as fh:
+ current = int(fh.read().strip())
+
+ while current < total:
+ patch_file = os.path.join(patches_dir, f'{current:04d}')
+ if not os.path.exists(patch_file):
+ current += 1
+ continue
+
+ with open(patch_file, 'rb') as fh:
+ patch_data = fh.read()
+
+ logger.info('Applying remaining patch %d/%d...', current + 1, total)
+ ecode, out = b4.git_run_command(topdir, ['apply', '--3way'],
+ stdin=patch_data, logstderr=True, rundir=topdir)
+ if ecode > 0:
+ logger.critical('---')
+ logger.critical(out.strip())
+ logger.critical('---')
+ logger.critical('Remaining patch %d/%d did not apply cleanly.', current + 1, total)
+ logger.critical('Resolve conflicts in your working tree, then run: b4 shazam --continue')
+ logger.critical('To abort: b4 shazam --abort')
+ # Advance past this patch, its changes (with conflict markers) are in the tree
+ with open(os.path.join(patches_dir, 'current'), 'w') as fh:
+ fh.write(str(current + 1))
+ sys.exit(1)
+
+ # Patch applied cleanly, stage it
+ b4.git_run_command(topdir, ['add', '-u'], logstderr=True, rundir=topdir)
+ current += 1
+ with open(os.path.join(patches_dir, 'current'), 'w') as fh:
+ fh.write(str(current))
+
+ # All patches applied, finish the merge
+ _finish_shazam_merge(topdir, state, state_file, common_dir, patches_dir)
+
+
+def _finish_shazam_merge(topdir: str, state: Dict, state_file: str,
+ common_dir: str, patches_dir: str) -> None:
+ b4.git_run_command(topdir, ['add', '-u'], logstderr=True, rundir=topdir)
+
+ gitargs = ['rev-parse', '--git-dir']
+ ecode, out = b4.git_run_command(topdir, gitargs, logstderr=True)
+ if ecode > 0:
+ logger.critical('Unable to find git directory')
+ sys.exit(1)
+ mmf = os.path.join(out.rstrip(), 'b4-cover')
+
+ merge_template = state.get('merge_template', DEFAULT_MERGE_TEMPLATE)
+ tptvals = state.get('merge_template_values', {})
+
+ body = Template(merge_template).safe_substitute(tptvals)
+ with open(mmf, 'w') as mmh:
+ mmh.write(body)
+
+ # Clean up state before committing -- if the commit is interactive
+ # (execvp), we won't get a chance to clean up after.
+ if os.path.exists(patches_dir):
+ shutil.rmtree(patches_dir)
+ if os.path.exists(state_file):
+ os.unlink(state_file)
+
+ no_interactive = state.get('no_interactive', False)
+ mergeflags = str(state.get('merge_flags', ''))
+ commitargs = ['commit', '-F', mmf]
+ if mergeflags:
+ sp = shlex.shlex(mergeflags, posix=True)
+ sp.whitespace_split = True
+ commitargs.extend(list(sp))
+ if no_interactive:
+ commitargs.append('--no-edit')
+ ecode, out = b4.git_run_command(topdir, commitargs, logstderr=True, rundir=topdir)
+ if ecode > 0:
+ logger.critical('Failed to commit merge:')
+ logger.critical(out.strip())
+ sys.exit(1)
+ logger.info(out.strip())
+ else:
+ # Interactive, need the terminal, so exec git directly
+ commitargs.append('--edit')
+ commitcmd = ['git'] + commitargs
+ logger.info('Invoking: %s', ' '.join(commitcmd))
+ if hasattr(sys, '_running_in_pytest'):
+ _out = b4.git_run_command(None, commitargs)
+ sys.exit(_out[0])
+ os.chdir(topdir)
+ os.execvp(commitcmd[0], commitcmd)
+
+ if os.path.exists(mmf):
+ os.unlink(mmf)
+ logger.info('Merge completed successfully.')
+
+
+def _load_shazam_state(require_state: bool = True) -> Tuple[str, str, str, Optional[Dict]]:
+ topdir = b4.git_get_toplevel()
+ if not topdir:
+ logger.critical('Could not figure out where your git dir is.')
+ sys.exit(1)
+ common_dir = b4.git_get_common_dir(topdir)
+ if not common_dir:
+ logger.critical('Unable to determine git common dir.')
+ sys.exit(1)
+
+ state_file = os.path.join(common_dir, 'b4-shazam-state.json')
+ state = None
+ if require_state:
+ if not os.path.exists(state_file):
+ logger.critical('No shazam state found. Nothing to continue.')
+ sys.exit(1)
+ with open(state_file, 'r') as fh:
+ state = json.load(fh)
+ patches_dir = os.path.join(common_dir, 'b4-shazam-patches')
+ if not os.path.isdir(patches_dir):
+ logger.critical('Patches directory not found. State may be corrupted.')
+ logger.critical('Run: b4 shazam --abort')
+ sys.exit(1)
+
+ return topdir, common_dir, state_file, state
+
+
+def shazam_continue(cmdargs: argparse.Namespace) -> None:
+ topdir, common_dir, state_file, state = _load_shazam_state(require_state=True)
+ assert state is not None
+ patches_dir = os.path.join(common_dir, 'b4-shazam-patches')
+
+ # Stage any resolved files
+ b4.git_run_command(topdir, ['add', '-u'], logstderr=True, rundir=topdir)
+
+ # Check for remaining unmerged files
+ ecode, unmerged = b4.git_run_command(topdir, ['diff', '--name-only', '--diff-filter=U'],
+ logstderr=True, rundir=topdir)
+ if unmerged.strip():
+ logger.critical('There are still unresolved conflicts:')
+ logger.critical(unmerged.strip())
+ logger.critical('Resolve them, then run: b4 shazam --continue')
+ sys.exit(1)
+
+ # Apply remaining patches and finish merge
+ _apply_remaining_patches(topdir, patches_dir, state, state_file, common_dir)
+
+
+def shazam_abort(cmdargs: argparse.Namespace) -> None:
+ topdir, common_dir, state_file, _state = _load_shazam_state(require_state=False)
+ found = False
+
+ # Abort in-progress merge if any
+ b4.git_run_command(topdir, ['merge', '--abort'], logstderr=True, rundir=topdir)
+
+ # Clean up patches directory
+ patches_dir = os.path.join(common_dir, 'b4-shazam-patches')
+ if os.path.exists(patches_dir):
+ shutil.rmtree(patches_dir)
+ found = True
+
+ # Clean up worktree if it exists
+ gwt = os.path.join(common_dir, 'b4-shazam-worktree')
+ if os.path.exists(gwt):
+ b4.git_run_command(topdir, ['worktree', 'remove', '--force', gwt])
+ found = True
+
+ if os.path.exists(state_file):
+ os.unlink(state_file)
+ found = True
+
+ if found:
+ logger.info('Shazam aborted and cleaned up.')
+ else:
+ logger.info('No shazam in progress.')
+
+
def main(cmdargs: argparse.Namespace) -> None:
# We force some settings
if cmdargs.subcmd == 'shazam':
+ if getattr(cmdargs, 'shazam_continue', False):
+ return shazam_continue(cmdargs)
+ if getattr(cmdargs, 'shazam_abort', False):
+ return shazam_abort(cmdargs)
cmdargs.checknewer = True
cmdargs.threeway = False
cmdargs.nopartialreroll = False
--
2.47.3
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH b4 1/3] shazam: refactor git_fetch_am_into_repo for deterministic worktree
2026-03-06 11:52 ` [PATCH b4 1/3] shazam: refactor git_fetch_am_into_repo for deterministic worktree Christian Brauner
@ 2026-03-06 16:10 ` Konstantin Ryabitsev
2026-03-24 11:33 ` Christian Brauner
0 siblings, 1 reply; 6+ messages in thread
From: Konstantin Ryabitsev @ 2026-03-06 16:10 UTC (permalink / raw)
To: Kernel.org Tools
Cc: Christian Brauner, Konstantin Ryabitsev, Christian Brauner
On Fri, 06 Mar 2026 12:52:24 +0100, Christian Brauner <brauner@kernel.org> wrote:
> Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
> Signed-off-by: Christian Brauner <brauner@kernel.org>
I don't have general objections to the code, but I'm not fond of using
the Co-developed-by: line as above, because this will *always* add the
agent's fake noreply address to all communication. I recommend you add
the following to your ~/.claude/CLAUDE.md:
| **Agent signoff:**
|
| Do NOT add a Co-developed-by: trailer to commit messages. This may
| interfere with tooling and automation used by many projects. Instead,
| add an Assisted-by: trailer with your exact model identifier. This
| identifier can be found in the system information by checking the
| agent model being used. E.g.:
|
| Assisted-by: claude-sonnet-4-5-20250929
This still accomplishes the same, and avoids the problem of useless
email being generated.
--
KR
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH b4 1/3] shazam: refactor git_fetch_am_into_repo for deterministic worktree
2026-03-06 16:10 ` Konstantin Ryabitsev
@ 2026-03-24 11:33 ` Christian Brauner
0 siblings, 0 replies; 6+ messages in thread
From: Christian Brauner @ 2026-03-24 11:33 UTC (permalink / raw)
To: Konstantin Ryabitsev; +Cc: Kernel.org Tools, Christian Brauner
On Fri, Mar 06, 2026 at 11:10:18AM -0500, Konstantin Ryabitsev wrote:
> On Fri, 06 Mar 2026 12:52:24 +0100, Christian Brauner <brauner@kernel.org> wrote:
> > Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
> > Signed-off-by: Christian Brauner <brauner@kernel.org>
>
> I don't have general objections to the code, but I'm not fond of using
Fwiw, I have not resend because the --resolve mechanism as it stands
silently removes the resolved patches as separate commits which is
obviously undesirable. What I basically need is a workflow where a
failed b4 shazam doesn't just unconditionally fail but let's me resolve
conflicts so that I can continue.
Thanks!
Christian
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-03-24 11:33 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-06 11:52 [PATCH b4 0/3] shazam: conflict resolution support for b4 shazam -H Christian Brauner
2026-03-06 11:52 ` [PATCH b4 1/3] shazam: refactor git_fetch_am_into_repo for deterministic worktree Christian Brauner
2026-03-06 16:10 ` Konstantin Ryabitsev
2026-03-24 11:33 ` Christian Brauner
2026-03-06 11:52 ` [PATCH b4 2/3] shazam: enable three-way merge for b4 shazam -H Christian Brauner
2026-03-06 11:52 ` [PATCH b4 3/3] shazam: enable merge conflict resolution for b4 shazam -H --resolve Christian Brauner
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox