22
33from __future__ import annotations
44
5+ import os
56import pathlib
67import typing as t
78
@@ -1326,6 +1327,49 @@ def test_worktree_prune(git_repo: GitSync, tmp_path: pathlib.Path) -> None:
13261327 assert "error" not in result_dry .lower () or result_dry == ""
13271328
13281329
1330+ def test_worktree_move (git_repo : GitSync , tmp_path : pathlib .Path ) -> None :
1331+ """Test GitWorktreeCmd.move()."""
1332+ # Create a worktree first
1333+ original_path = tmp_path / "move-original-worktree"
1334+ git_repo .cmd .worktrees .add (path = original_path , new_branch = "move-test-branch" )
1335+
1336+ # Get the worktree
1337+ worktree = git_repo .cmd .worktrees .get (worktree_path = str (original_path ))
1338+ assert worktree is not None
1339+
1340+ # Move the worktree to a new location
1341+ new_path = tmp_path / "move-new-worktree"
1342+ result = worktree .move (new_path )
1343+
1344+ # Should succeed (empty string or info message)
1345+ assert result == "" or "error" not in result .lower ()
1346+
1347+ # Verify original path no longer exists in worktree list
1348+ worktrees_after = git_repo .cmd .worktrees .ls ()
1349+ worktree_paths = [wt .worktree_path for wt in worktrees_after ]
1350+ assert str (original_path ) not in worktree_paths
1351+
1352+ # Verify new path exists in worktree list
1353+ assert str (new_path ) in worktree_paths
1354+
1355+
1356+ def test_worktree_repair (git_repo : GitSync , tmp_path : pathlib .Path ) -> None :
1357+ """Test GitWorktreeCmd.repair()."""
1358+ # Create a worktree first
1359+ worktree_path = tmp_path / "repair-test-worktree"
1360+ git_repo .cmd .worktrees .add (path = worktree_path , new_branch = "repair-test-branch" )
1361+
1362+ # Get the worktree
1363+ worktree = git_repo .cmd .worktrees .get (worktree_path = str (worktree_path ))
1364+ assert worktree is not None
1365+
1366+ # Repair should succeed (even if nothing needs repair)
1367+ result = worktree .repair ()
1368+
1369+ # Should succeed (empty string or info message)
1370+ assert result == "" or "error" not in result .lower ()
1371+
1372+
13291373# GitNotes tests
13301374
13311375
@@ -1489,6 +1533,79 @@ def test_notes_get_ref(git_repo: GitSync) -> None:
14891533 assert result == "refs/notes/commits" or result == "" or "notes" in result
14901534
14911535
1536+ def test_notes_edit (git_repo : GitSync , tmp_path : pathlib .Path ) -> None :
1537+ """Test GitNoteCmd.edit() - non-interactive mode via GIT_EDITOR."""
1538+ # Add a note first
1539+ git_repo .cmd .notes .add (message = "Initial note for edit test" , force = True )
1540+
1541+ # Get the note
1542+ head_sha = git_repo .cmd .rev_parse (args = "HEAD" )
1543+ note = git_repo .cmd .notes .get (object_sha = head_sha )
1544+ assert note is not None
1545+
1546+ # Edit with allow_empty (avoid interactive editor by using config)
1547+ # The doctest uses config={'core.editor': 'true'} which sets a no-op editor
1548+ result = note .edit (allow_empty = True , config = {"core.editor" : "true" })
1549+
1550+ # Should succeed (empty string) or show error about editor
1551+ assert result == "" or "error" in result .lower () or isinstance (result , str )
1552+
1553+
1554+ def test_notes_copy (git_repo : GitSync ) -> None :
1555+ """Test GitNoteCmd.copy()."""
1556+ # Create a second commit to copy the note to
1557+ test_file = git_repo .path / "copy_note_test.txt"
1558+ test_file .write_text ("content for copy test" )
1559+ git_repo .cmd .run (["add" , "copy_note_test.txt" ])
1560+ git_repo .cmd .run (["commit" , "-m" , "Commit for copy note test" ])
1561+
1562+ # Get the new commit SHA
1563+ new_commit_sha = git_repo .cmd .rev_parse (args = "HEAD" )
1564+
1565+ # Checkout previous commit to add note there
1566+ git_repo .cmd .run (["checkout" , "HEAD~1" ])
1567+ git_repo .cmd .notes .add (message = "Note to copy" , force = True )
1568+
1569+ # Get the note and copy to new commit
1570+ old_commit_sha = git_repo .cmd .rev_parse (args = "HEAD" )
1571+ note = git_repo .cmd .notes .get (object_sha = old_commit_sha )
1572+ assert note is not None
1573+
1574+ # Copy to new commit (force=True to overwrite if exists)
1575+ result = note .copy (to_object = new_commit_sha , force = True )
1576+
1577+ # Should succeed
1578+ assert result == "" or "error" not in result .lower ()
1579+
1580+ # Go back to main branch
1581+ git_repo .cmd .run (["checkout" , "-" ])
1582+
1583+
1584+ def test_notes_merge (git_repo : GitSync ) -> None :
1585+ """Test GitNotesManager.merge()."""
1586+ # Create a custom notes ref
1587+ custom_ref = "refs/notes/custom"
1588+ custom_notes = git_repo .cmd .notes .__class__ (
1589+ path = git_repo .path ,
1590+ cmd = git_repo .cmd ,
1591+ ref = custom_ref ,
1592+ )
1593+
1594+ # Add a note to the custom ref
1595+ custom_notes .add (message = "Note in custom ref" , force = True )
1596+
1597+ # Merge notes from custom ref to default ref
1598+ result = git_repo .cmd .notes .merge (notes_ref = custom_ref )
1599+
1600+ # Should succeed or show merge info
1601+ assert (
1602+ result == ""
1603+ or "already up to date" in result .lower ()
1604+ or "merged" in result .lower ()
1605+ or "error" not in result .lower ()
1606+ )
1607+
1608+
14921609# GitReflog tests
14931610
14941611
@@ -1590,6 +1707,29 @@ def test_reflog_expire(git_repo: GitSync) -> None:
15901707 assert result == "" or "error" not in result .lower ()
15911708
15921709
1710+ def test_reflog_entry_delete (git_repo : GitSync ) -> None :
1711+ """Test GitReflogEntryCmd.delete()."""
1712+ # Create some commits to have reflog entries
1713+ test_file = git_repo .path / "reflog_delete_test.txt"
1714+ env = os .environ .copy ()
1715+
1716+ for i in range (3 ):
1717+ test_file .write_text (f"content { i } " )
1718+ git_repo .cmd .run (["add" , "reflog_delete_test.txt" ])
1719+ git_repo .cmd .run (["commit" , "-m" , f"Commit { i } " ], env = env )
1720+
1721+ # Get the reflog entries
1722+ entries = git_repo .cmd .reflog .ls ()
1723+ assert len (entries ) >= 3
1724+
1725+ # Get an entry and delete it with dry_run (to avoid actually modifying reflog)
1726+ entry = entries [1 ] # Pick a middle entry
1727+ result = entry .cmd .delete (dry_run = True )
1728+
1729+ # Should succeed or show what would be deleted
1730+ assert result == "" or isinstance (result , str )
1731+
1732+
15931733# GitSubmodule tests
15941734# ==================
15951735
@@ -1601,8 +1741,6 @@ def submodule_repo(
16011741 set_gitconfig : pathlib .Path ,
16021742) -> git .Git :
16031743 """Create a git repository to use as a submodule source."""
1604- import os
1605-
16061744 # Create a repo to serve as submodule source
16071745 source_path = tmp_path / "submodule_source"
16081746 source_path .mkdir ()
@@ -1852,3 +1990,97 @@ def test_submodule_dataclass_properties(
18521990 # Test initialized property
18531991 # After add, submodule should be initialized (prefix not '-')
18541992 assert submodule .initialized is True or submodule .status_prefix == "-"
1993+
1994+
1995+ def test_submodule_entry_deinit (
1996+ git_repo : GitSync ,
1997+ submodule_repo : git .Git ,
1998+ ) -> None :
1999+ """Test GitSubmoduleEntryCmd.deinit()."""
2000+ # Setup
2001+ _setup_submodule_test (git_repo , submodule_repo )
2002+
2003+ # Initialize the submodule first
2004+ git_repo .cmd .submodules .init ()
2005+ git_repo .cmd .submodules .update (init = True )
2006+
2007+ # Get the submodule
2008+ submodule = git_repo .cmd .submodules .get (path = "vendor/lib" )
2009+ assert submodule .cmd is not None
2010+
2011+ # Deinit the submodule (with force to ensure it works even if dirty)
2012+ result = submodule .cmd .deinit (force = True )
2013+
2014+ # Should succeed (empty string or info message)
2015+ assert result == "" or "cleared" in result .lower () or "error" not in result .lower ()
2016+
2017+
2018+ def test_submodule_entry_set_branch (
2019+ git_repo : GitSync ,
2020+ submodule_repo : git .Git ,
2021+ ) -> None :
2022+ """Test GitSubmoduleEntryCmd.set_branch()."""
2023+ # Setup
2024+ _setup_submodule_test (git_repo , submodule_repo )
2025+
2026+ # Get the submodule
2027+ submodule = git_repo .cmd .submodules .get (path = "vendor/lib" )
2028+ assert submodule .cmd is not None
2029+
2030+ # Set branch
2031+ result = submodule .cmd .set_branch (branch = "main" )
2032+
2033+ # Should succeed (empty string) or show error if branch doesn't exist
2034+ assert result == "" or isinstance (result , str )
2035+
2036+ # Verify branch is set in .gitmodules
2037+ gitmodules = (git_repo .path / ".gitmodules" ).read_text ()
2038+ # Branch may or may not be present depending on git version behavior
2039+ assert "vendor/lib" in gitmodules
2040+
2041+
2042+ def test_submodule_entry_set_url (
2043+ git_repo : GitSync ,
2044+ submodule_repo : git .Git ,
2045+ ) -> None :
2046+ """Test GitSubmoduleEntryCmd.set_url()."""
2047+ # Setup
2048+ _setup_submodule_test (git_repo , submodule_repo )
2049+
2050+ # Get the submodule
2051+ submodule = git_repo .cmd .submodules .get (path = "vendor/lib" )
2052+ assert submodule .cmd is not None
2053+
2054+ # Set a new URL
2055+ new_url = "https://example.com/repo.git"
2056+ result = submodule .cmd .set_url (url = new_url )
2057+
2058+ # Should succeed (empty string)
2059+ assert result == "" or isinstance (result , str )
2060+
2061+ # Verify URL is updated in .gitmodules
2062+ gitmodules = (git_repo .path / ".gitmodules" ).read_text ()
2063+ assert new_url in gitmodules
2064+
2065+
2066+ def test_submodule_entry_absorbgitdirs (
2067+ git_repo : GitSync ,
2068+ submodule_repo : git .Git ,
2069+ ) -> None :
2070+ """Test GitSubmoduleEntryCmd.absorbgitdirs()."""
2071+ # Setup
2072+ _setup_submodule_test (git_repo , submodule_repo )
2073+
2074+ # Initialize and update submodule first
2075+ git_repo .cmd .submodules .init ()
2076+ git_repo .cmd .submodules .update (init = True )
2077+
2078+ # Get the submodule
2079+ submodule = git_repo .cmd .submodules .get (path = "vendor/lib" )
2080+ assert submodule .cmd is not None
2081+
2082+ # Absorb git dirs (may already be absorbed, but should succeed)
2083+ result = submodule .cmd .absorbgitdirs ()
2084+
2085+ # Should succeed (empty string or info message)
2086+ assert result == "" or isinstance (result , str )
0 commit comments