From 7fc2f4087ddd6648110698d36a335cc767cc8187 Mon Sep 17 00:00:00 2001 From: 0x56696B Date: Tue, 5 May 2026 13:05:12 +0300 Subject: [PATCH 1/5] fix(add): remove checkout_tree call on bare repo during fast-forward --- src/cmds/add/cmd_add.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/cmds/add/cmd_add.py b/src/cmds/add/cmd_add.py index a7e90e2..41122d0 100644 --- a/src/cmds/add/cmd_add.py +++ b/src/cmds/add/cmd_add.py @@ -86,11 +86,7 @@ def add_worktree(add_args: AddArgs) -> Result[None, AddWorktreeError]: ) elif analysis & pg.GIT_MERGE_ANALYSIS_FASTFORWARD: - # NOTE: Untested! - # Move HEAD and working tree forward - _ = bare_repo.checkout_tree(treeish=bare_repo.get(remote_branch.id)) # pyright: ignore[reportUnknownMemberType] bare_repo.lookup_reference(f"refs/heads/{add_args.derive_from_branch}").set_target(remote_branch.id) - bare_repo.head.set_target(remote_branch.id) log.info("Fast-forwarded ref branch; fast_forwarded=%s", remote_branch.id) From 7015cfb7faa3e9448129c64baab2f46a36075766 Mon Sep 17 00:00:00 2001 From: 0x56696B Date: Tue, 5 May 2026 13:21:54 +0300 Subject: [PATCH 2/5] test(add): add tests for fast-forward and bare repo error cases --- tests/test_cmd_add.py | 96 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tests/test_cmd_add.py diff --git a/tests/test_cmd_add.py b/tests/test_cmd_add.py new file mode 100644 index 0000000..50a687f --- /dev/null +++ b/tests/test_cmd_add.py @@ -0,0 +1,96 @@ +from pathlib import Path + +import pygit2 as pg +import pytest +from result import Err, Ok + +from src.cmds.add.args_add import AddArgs +from src.cmds.add.cmd_add import add_worktree +from src.errors.not_bare_repo_err import NotBareRepoErr + + +def _make_bare_repo_with_main_worktree(tmp_path: Path) -> tuple[pg.Repository, Path]: + bare = tmp_path / "bare" + bare.mkdir() + repo = pg.init_repository(str(bare), bare=True) + + sig = pg.Signature("Test", "test@test.com") + tree_id = repo.TreeBuilder().write() + repo.create_commit("refs/heads/main", sig, sig, "Initial commit", tree_id, []) + repo.set_head("refs/heads/main") + + main_path = bare / "main" + branch_ref: pg.Reference = repo.lookup_reference("refs/heads/main") + repo.add_worktree("main", str(main_path), branch_ref) + + return repo, bare + + +class TestAddWorktreeNotBareRepo: + def test_plain_directory_returns_err(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.chdir(str(tmp_path)) + + args = AddArgs( + new_branch_name="feat-x", + derive_from_branch="main", + should_nest_dirs=False, + exclude=[], + _force=False, + ) + result = add_worktree(args) + + assert isinstance(result, Err) + assert isinstance(result.err(), NotBareRepoErr) + + +class TestAddWorktreeUpToDate: + def test_up_to_date_base_creates_worktree(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + repo, bare = _make_bare_repo_with_main_worktree(tmp_path) + monkeypatch.chdir(str(bare)) + + args = AddArgs( + new_branch_name="feat-x", + derive_from_branch="main", + should_nest_dirs=False, + exclude=[], + _force=False, + ) + result = add_worktree(args) + + assert isinstance(result, Ok) + assert (bare / "feat-x").exists() + + +class TestAddWorktreeFastForward: + def test_fast_forward_base_does_not_crash(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + repo, bare = _make_bare_repo_with_main_worktree(tmp_path) + + # Add a second commit to main so it is ahead of HEAD + sig = pg.Signature("Test", "test@test.com") + blob_id = repo.create_blob(b"content") + tb = repo.TreeBuilder() + tb.insert("file.txt", blob_id, pg.GIT_FILEMODE_BLOB) + tree_b = tb.write() + parent: pg.Commit = repo.lookup_reference("refs/heads/main").peel(pg.Commit) + repo.create_commit("refs/heads/main", sig, sig, "second commit", tree_b, [parent.id]) + + # Detach HEAD at the first commit so main is one ahead (FASTFORWARD scenario) + # pygit2 1.19.2 has no set_head_detached; write the HEAD file directly + import os + with open(os.path.join(str(bare), "HEAD"), "w") as f: + f.write(str(parent.id)) + + monkeypatch.chdir(str(bare)) + + args = AddArgs( + new_branch_name="feat-y", + derive_from_branch="main", + should_nest_dirs=False, + exclude=[], + _force=False, + ) + result = add_worktree(args) + + # Must succeed — old code raised GitError from checkout_tree on bare repo + assert isinstance(result, Ok) + assert (bare / "feat-y").exists() From 8de9a4bde87b0f8b3aca68ccfedbc7b24f4c5d58 Mon Sep 17 00:00:00 2001 From: 0x56696B Date: Mon, 11 May 2026 09:41:45 +0300 Subject: [PATCH 3/5] fix(add): comment misleading FF log until fetch is wired in --- src/cmds/add/cmd_add.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cmds/add/cmd_add.py b/src/cmds/add/cmd_add.py index 41122d0..f1841d0 100644 --- a/src/cmds/add/cmd_add.py +++ b/src/cmds/add/cmd_add.py @@ -88,7 +88,10 @@ def add_worktree(add_args: AddArgs) -> Result[None, AddWorktreeError]: elif analysis & pg.GIT_MERGE_ANALYSIS_FASTFORWARD: bare_repo.lookup_reference(f"refs/heads/{add_args.derive_from_branch}").set_target(remote_branch.id) - log.info("Fast-forwarded ref branch; fast_forwarded=%s", remote_branch.id) + # TODO: fetch not yet wired in — remote_branch is peeled from the same ref being updated, + # so set_target() above is a no-op (ref and target are always equal until fetch runs). + # Uncomment and update this log line once fetch is implemented. + # log.info("Fast-forwarded ref branch; fast_forwarded=%s", remote_branch.id) else: # True merge required — pygit2 can do it but you'd need to handle conflicts From 7dffa5f1c3621a47dc01a36c52527627d38817fa Mon Sep 17 00:00:00 2001 From: 0x56696B Date: Mon, 11 May 2026 09:55:08 +0300 Subject: [PATCH 4/5] test(add): fix import order, HEAD newline, assert FF ref advanced --- tests/test_cmd_add.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_cmd_add.py b/tests/test_cmd_add.py index 50a687f..6ccf15e 100644 --- a/tests/test_cmd_add.py +++ b/tests/test_cmd_add.py @@ -1,3 +1,4 @@ +import os from pathlib import Path import pygit2 as pg @@ -76,9 +77,11 @@ def test_fast_forward_base_does_not_crash(self, tmp_path: Path, monkeypatch: pyt # Detach HEAD at the first commit so main is one ahead (FASTFORWARD scenario) # pygit2 1.19.2 has no set_head_detached; write the HEAD file directly - import os with open(os.path.join(str(bare), "HEAD"), "w") as f: - f.write(str(parent.id)) + f.write(str(parent.id) + "\n") + + # Capture target OID (tip of main after second commit) before calling add_worktree + target_oid: pg.Oid = repo.lookup_reference("refs/heads/main").peel(pg.Commit).id monkeypatch.chdir(str(bare)) @@ -94,3 +97,5 @@ def test_fast_forward_base_does_not_crash(self, tmp_path: Path, monkeypatch: pyt # Must succeed — old code raised GitError from checkout_tree on bare repo assert isinstance(result, Ok) assert (bare / "feat-y").exists() + # Verify FASTFORWARD branch was taken: refs/heads/main must point at target_oid + assert repo.lookup_reference("refs/heads/main").peel(pg.Commit).id == target_oid From 7e28ffbdd0d07a9931e3861fb36ce2221f8ac79d Mon Sep 17 00:00:00 2001 From: 0x56696B Date: Mon, 11 May 2026 15:54:34 +0300 Subject: [PATCH 5/5] fix(add): correct misleading TODO and test comment for FF no-op --- src/cmds/add/cmd_add.py | 7 ++++--- tests/test_cmd_add.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cmds/add/cmd_add.py b/src/cmds/add/cmd_add.py index f1841d0..afe354a 100644 --- a/src/cmds/add/cmd_add.py +++ b/src/cmds/add/cmd_add.py @@ -88,9 +88,10 @@ def add_worktree(add_args: AddArgs) -> Result[None, AddWorktreeError]: elif analysis & pg.GIT_MERGE_ANALYSIS_FASTFORWARD: bare_repo.lookup_reference(f"refs/heads/{add_args.derive_from_branch}").set_target(remote_branch.id) - # TODO: fetch not yet wired in — remote_branch is peeled from the same ref being updated, - # so set_target() above is a no-op (ref and target are always equal until fetch runs). - # Uncomment and update this log line once fetch is implemented. + # TODO: this whole block needs rewriting when fetch lands. + # remote_branch must be peeled from refs/remotes/origin/{branch} (remote-tracking ref), + # not refs/heads/{branch} (local ref), which is always already at target — making + # set_target() a structural no-op regardless of whether fetch runs. # log.info("Fast-forwarded ref branch; fast_forwarded=%s", remote_branch.id) else: diff --git a/tests/test_cmd_add.py b/tests/test_cmd_add.py index 6ccf15e..2bc1851 100644 --- a/tests/test_cmd_add.py +++ b/tests/test_cmd_add.py @@ -97,5 +97,5 @@ def test_fast_forward_base_does_not_crash(self, tmp_path: Path, monkeypatch: pyt # Must succeed — old code raised GitError from checkout_tree on bare repo assert isinstance(result, Ok) assert (bare / "feat-y").exists() - # Verify FASTFORWARD branch was taken: refs/heads/main must point at target_oid + # Non-crash check — set_target is a no-op until fetch is wired (see TODO in cmd_add.py FF block) assert repo.lookup_reference("refs/heads/main").peel(pg.Commit).id == target_oid