[PATCH b4 3/3] review-tui: test shazam-merge-flags and worktree resolution
Christian Brauner <[email protected]> Mon, 22 Jun 2026 14:20:45 +0200
| Newsgroups | org.kernel.linux.tools |
|---|---|
| Message-ID | <[email protected]> |
Cover the take->merge helpers: - _shazam_merge_flags(): the default and an explicit --signoff are stripped (the take dialog's checkbox owns Signed-off-by), while --log/--stat/--gpg-sign pass through. - _worktree_for_branch(): resolves the worktree holding a branch across linked worktrees and returns None when it is not checked out anywhere. Signed-off-by: Christian Brauner (Amutable) <[email protected]> --- src/tests/test_tui_tracking.py | 185 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 184 insertions(+), 1 deletion(-) diff --git a/src/tests/test_tui_tracking.py b/src/tests/test_tui_tracking.py index 022dc0c..7fb52b8 100644 --- a/src/tests/test_tui_tracking.py +++ b/src/tests/test_tui_tracking.py @@ -34,7 +34,12 @@ from b4.review_tui._modals import ( SnoozeScreen, TargetBranchScreen, ) -from b4.review_tui._tracking_app import TrackedSeriesItem, TrackingApp +from b4.review_tui._tracking_app import ( + TrackedSeriesItem, + TrackingApp, + _shazam_merge_flags, + _worktree_for_branch, +) # --------------------------------------------------------------------------- # Compat helper — Textual ≥ 1.0 (pip) uses Static.content, @@ -2298,6 +2303,54 @@ class TestSeriesLifecycle: assert all(p.get('taken') for p in patches), 'all patches should be taken' assert updated['series']['status'] == 'accepted' + def test_record_take_metadata_branch_tip_is_target_not_head( + self, gitdir: str + ) -> None: + """branch-tips records the *target branch* tip, not the current HEAD. + + Regression: the merge-take path advances target_branch in a separate + worktree and never checks out target_branch in the current checkout, so + reading HEAD here recorded the wrong commit (the launch branch) for the + CI-lookup metadata. + """ + change_id = 'branch-tip-1' + branch_name = _create_review_branch( + gitdir, change_id, identifier='test-branch-tip', status='reviewing' + ) + cover_text, trk = b4.review.load_tracking(gitdir, branch_name) + trk['patches'] = [{'subject': 'patch 1', 'message-id': '[email protected]'}] + b4.review.save_tracking_ref(gitdir, branch_name, cover_text, trk) + + # A target branch whose tip is advanced beyond the current HEAD + # (master), so HEAD and the target tip are distinguishable. + b4.git_run_command(gitdir, ['branch', 'take-target']) + ecode, tree = b4.git_run_command(gitdir, ['rev-parse', 'take-target^{tree}']) + assert ecode == 0 + ecode, newsha = b4.git_run_command( + gitdir, + ['commit-tree', tree.strip(), '-p', 'take-target'], + stdin=b'advance target\n', + ) + assert ecode == 0 + b4.git_run_command( + gitdir, ['update-ref', 'refs/heads/take-target', newsha.strip()] + ) + + app = TrackingApp.__new__(TrackingApp) + app._record_take_metadata( + gitdir, branch_name, 'take-target', ['commit-a'], accepted=True + ) + + _, updated = b4.review.load_tracking(gitdir, branch_name) + tips = updated['series'].get('branch-tips', []) + assert tips, 'expected a branch-tips entry' + _, target_tip = b4.git_run_command(gitdir, ['rev-parse', 'take-target']) + _, head = b4.git_run_command(gitdir, ['rev-parse', 'HEAD']) + assert tips[-1]['branch'] == 'take-target' + assert tips[-1]['sha'] == target_tip.strip() + # The bug recorded HEAD (master); guard against a regression. + assert tips[-1]['sha'] != head.strip() + @pytest.mark.asyncio async def test_thank_partial_series_cherrypicks_taken(self, gitdir: str) -> None: """Thanking a 'partial' series proceeds and thanks only taken patches. @@ -2506,6 +2559,136 @@ class TestSeriesLifecycle: ) +class TestShazamMergeFlags: + """The take->merge path passes b4.shazam-merge-flags through verbatim (like + `b4 shazam`) and reconciles Signed-off-by with the take dialog's checkbox as + a single, deduped git-merge flag (config is authoritative). + """ + + def test_unset_config_defaults_to_signoff(self) -> None: + # Default shazam-merge-flags is '--signoff'; with the (config-defaulted) + # checkbox on, exactly one --signoff is passed. + assert _shazam_merge_flags({}, True) == ['--signoff'] + + def test_unset_config_checkbox_off_yields_no_signoff(self) -> None: + assert _shazam_merge_flags({}, False) == ['--no-signoff'] + + def test_config_flags_pass_through_verbatim(self) -> None: + # --log/--stat/--gpg-sign survive unchanged; signoff is appended once. + assert _shazam_merge_flags( + {'shazam-merge-flags': '--gpg-sign --stat --log'}, True + ) == ['--gpg-sign', '--stat', '--log', '--signoff'] + + def test_config_signoff_is_not_duplicated(self) -> None: + # An explicit --signoff in config is deduped against the checkbox flag. + assert _shazam_merge_flags( + {'shazam-merge-flags': '--signoff --log'}, True + ) == ['--log', '--signoff'] + assert _shazam_merge_flags( + {'shazam-merge-flags': '--signoff --log'}, False + ) == ['--log', '--no-signoff'] + + def test_config_no_signoff_overridden_by_checkbox(self) -> None: + assert _shazam_merge_flags( + {'shazam-merge-flags': '--no-signoff --log'}, True + ) == ['--log', '--signoff'] + assert _shazam_merge_flags( + {'shazam-merge-flags': '--no-signoff --log'}, False + ) == ['--log', '--no-signoff'] + + def test_strategy_short_flag_is_passed_through(self) -> None: + # In `git merge`, -s is --strategy (it takes an argument), NOT a short + # form of --signoff. It must survive untouched, else `-s ours` would + # lose the -s and leave `ours` as a bogus merge operand. + assert _shazam_merge_flags( + {'shazam-merge-flags': '-s ours'}, True + ) == ['-s', 'ours', '--signoff'] + + def test_empty_config_still_honors_checkbox(self) -> None: + assert _shazam_merge_flags({'shazam-merge-flags': ''}, True) == ['--signoff'] + assert _shazam_merge_flags( + {'shazam-merge-flags': ''}, False + ) == ['--no-signoff'] + + +class TestWorktreeForBranch: + """Resolving which worktree holds a branch, for cross-worktree takes.""" + + def test_finds_and_ignores_other_worktrees( + self, gitdir: str, tmp_path: pathlib.Path + ) -> None: + # The fixture checks out 'master' in the main worktree. + main = _worktree_for_branch(gitdir, 'master') + assert main is not None and pathlib.Path(main).samefile(gitdir) + + # A branch that is not checked out anywhere resolves to None. + b4.git_run_command(gitdir, ['branch', 'topic']) + assert _worktree_for_branch(gitdir, 'topic') is None + + # Once checked out in a linked worktree, it resolves to that path. + wt = str(tmp_path / 'wt-topic') + ecode, _ = b4.git_run_command(gitdir, ['worktree', 'add', wt, 'topic']) + assert ecode == 0 + found = _worktree_for_branch(gitdir, 'topic') + assert found is not None and pathlib.Path(found).samefile(wt) + + # An unknown branch resolves to None. + assert _worktree_for_branch(gitdir, 'no-such-branch') is None + + +class TestCrossWorktreeFetch: + """git_fetch_am_into_repo must land FETCH_HEAD in the target worktree. + + The merge-take path applies patches via git_fetch_am_into_repo(merge_dir, + ...) and then merges ``git -C merge_dir ... FETCH_HEAD``. FETCH_HEAD is + per-worktree; if the fetch runs against the caller's cwd instead of + merge_dir, the commits land in the wrong worktree and the merge reads a + stale/missing FETCH_HEAD -- silently merging unrelated commits. + """ + + def test_fetch_head_lands_in_target_worktree_not_cwd( + self, gitdir: str, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + # Build an am-able patch on top of master without needing a committer + # identity or disturbing HEAD (mirrors _create_review_branch). + patchfile = pathlib.Path(gitdir) / 'cross_wt_patch.txt' + patchfile.write_text('cross-worktree marker\n') + b4.git_run_command(gitdir, ['add', 'cross_wt_patch.txt']) + ecode, tree = b4.git_run_command(gitdir, ['write-tree']) + assert ecode == 0 + ecode, commit = b4.git_run_command( + gitdir, + ['commit-tree', tree.strip(), '-p', 'master'], + stdin=b'add cross_wt_patch\n', + ) + assert ecode == 0 + ecode, mbox = b4.git_run_command( + gitdir, ['format-patch', '-1', '--stdout', commit.strip()], decode=False + ) + assert ecode == 0 + # Restore a clean master (drop the staged file) so at_base=master and + # its FETCH_HEAD start fresh. + b4.git_run_command(gitdir, ['reset', '--hard', 'master']) + + # Drive the fetch from a *different* worktree than gitdir. gitdir is the + # primary worktree (its .git is a directory) -- the case that used to + # leak the process cwd into the FETCH_HEAD location. + other = str(tmp_path / 'other-wt') + ecode, _ = b4.git_run_command(gitdir, ['worktree', 'add', other, '-b', 'other']) + assert ecode == 0 + monkeypatch.chdir(other) + + b4.git_fetch_am_into_repo(gitdir, mbox, at_base='master', am_flags=['-3']) + + # FETCH_HEAD must be readable from gitdir and carry the patched file -- + # i.e. it landed in gitdir's per-worktree FETCH_HEAD, not `other`'s. + ecode, blob = b4.git_run_command( + gitdir, ['show', 'FETCH_HEAD:cross_wt_patch.txt'] + ) + assert ecode == 0, 'FETCH_HEAD did not land in the target worktree' + assert blob.strip() == 'cross-worktree marker' + + class TestMergeTakeSkipRouting: """Take routing when patches are skipped (bug 6d1d35c). -- 2.53.0