33from __future__ import annotations
44
55import asyncio
6- import typing as t
76from pathlib import Path
87
98import pytest
109
10+ from libvcs .pytest_plugin import CreateRepoPytestFixtureFn
1111from libvcs .sync ._async .git import AsyncGitSync
1212from libvcs .sync .git import GitRemote
1313
@@ -64,7 +64,7 @@ class TestAsyncGitSyncObtain:
6464 async def test_obtain_basic (
6565 self ,
6666 tmp_path : Path ,
67- create_git_remote_repo : t . Any ,
67+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
6868 ) -> None :
6969 """Test basic obtain operation."""
7070 remote_repo = create_git_remote_repo ()
@@ -83,7 +83,7 @@ async def test_obtain_basic(
8383 async def test_obtain_shallow (
8484 self ,
8585 tmp_path : Path ,
86- create_git_remote_repo : t . Any ,
86+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
8787 ) -> None :
8888 """Test shallow clone via obtain."""
8989 remote_repo = create_git_remote_repo ()
@@ -106,7 +106,7 @@ class TestAsyncGitSyncUpdateRepo:
106106 async def test_update_repo_basic (
107107 self ,
108108 tmp_path : Path ,
109- create_git_remote_repo : t . Any ,
109+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
110110 ) -> None :
111111 """Test basic update_repo operation."""
112112 remote_repo = create_git_remote_repo ()
@@ -128,7 +128,7 @@ async def test_update_repo_basic(
128128 async def test_update_repo_obtains_if_missing (
129129 self ,
130130 tmp_path : Path ,
131- create_git_remote_repo : t . Any ,
131+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
132132 ) -> None :
133133 """Test update_repo clones if repo doesn't exist."""
134134 remote_repo = create_git_remote_repo ()
@@ -153,7 +153,7 @@ class TestAsyncGitSyncGetRevision:
153153 async def test_get_revision (
154154 self ,
155155 tmp_path : Path ,
156- create_git_remote_repo : t . Any ,
156+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
157157 ) -> None :
158158 """Test get_revision returns current HEAD or 'initial'."""
159159 remote_repo = create_git_remote_repo ()
@@ -179,7 +179,7 @@ class TestAsyncGitSyncRemotes:
179179 async def test_remotes_get (
180180 self ,
181181 tmp_path : Path ,
182- create_git_remote_repo : t . Any ,
182+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
183183 ) -> None :
184184 """Test remotes_get returns remotes."""
185185 remote_repo = create_git_remote_repo ()
@@ -199,7 +199,7 @@ async def test_remotes_get(
199199 async def test_remote_get_single (
200200 self ,
201201 tmp_path : Path ,
202- create_git_remote_repo : t . Any ,
202+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
203203 ) -> None :
204204 """Test remote() returns single remote."""
205205 remote_repo = create_git_remote_repo ()
@@ -219,7 +219,7 @@ async def test_remote_get_single(
219219 async def test_set_remote (
220220 self ,
221221 tmp_path : Path ,
222- create_git_remote_repo : t . Any ,
222+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
223223 ) -> None :
224224 """Test set_remote adds a new remote."""
225225 remote_repo = create_git_remote_repo ()
@@ -249,7 +249,7 @@ class TestAsyncGitSyncStatus:
249249 async def test_status (
250250 self ,
251251 tmp_path : Path ,
252- create_git_remote_repo : t . Any ,
252+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
253253 ) -> None :
254254 """Test status() returns GitStatus without error."""
255255 remote_repo = create_git_remote_repo ()
@@ -269,14 +269,269 @@ async def test_status(
269269 assert isinstance (status , GitStatus )
270270
271271
272+ class TestAsyncGitRepoFixture :
273+ """Tests for the async_git_repo pytest fixture."""
274+
275+ @pytest .mark .asyncio
276+ async def test_async_git_repo_fixture (
277+ self ,
278+ async_git_repo : AsyncGitSync ,
279+ ) -> None :
280+ """Test that async_git_repo fixture provides a working repository."""
281+ # Verify the repo exists and is initialized
282+ assert async_git_repo .path .exists ()
283+ assert (async_git_repo .path / ".git" ).exists ()
284+
285+ # Verify we can perform async operations
286+ revision = await async_git_repo .get_revision ()
287+ assert revision
288+ assert len (revision .strip ()) == 40 # Full SHA
289+
290+ @pytest .mark .asyncio
291+ async def test_async_git_repo_status (
292+ self ,
293+ async_git_repo : AsyncGitSync ,
294+ ) -> None :
295+ """Test that status() works on fixture-provided repo."""
296+ from libvcs .sync .git import GitStatus
297+
298+ status = await async_git_repo .status ()
299+ assert isinstance (status , GitStatus )
300+
301+ @pytest .mark .asyncio
302+ async def test_async_git_repo_remotes (
303+ self ,
304+ async_git_repo : AsyncGitSync ,
305+ ) -> None :
306+ """Test that remotes are properly configured on fixture-provided repo."""
307+ remotes = await async_git_repo .remotes_get ()
308+ assert "origin" in remotes
309+
310+
311+ class TestAsyncGitSyncFromPipUrl :
312+ """Tests for AsyncGitSync.from_pip_url()."""
313+
314+ def test_from_pip_url_https (self , tmp_path : Path ) -> None :
315+ """Test from_pip_url with git+https URL."""
316+ repo = AsyncGitSync .from_pip_url (
317+ pip_url = "git+https://github.com/test/repo.git" ,
318+ path = tmp_path / "pip_repo" ,
319+ )
320+ assert repo .url == "https://github.com/test/repo.git"
321+ assert repo .path == tmp_path / "pip_repo"
322+
323+ def test_from_pip_url_with_revision (self , tmp_path : Path ) -> None :
324+ """Test from_pip_url with revision specifier."""
325+ repo = AsyncGitSync .from_pip_url (
326+ pip_url = "git+https://github.com/test/repo.git@v1.0.0" ,
327+ path = tmp_path / "pip_repo" ,
328+ )
329+ assert repo .url == "https://github.com/test/repo.git"
330+ assert repo .rev == "v1.0.0"
331+
332+ def test_from_pip_url_ssh (self , tmp_path : Path ) -> None :
333+ """Test from_pip_url with git+ssh URL."""
334+ repo = AsyncGitSync .from_pip_url (
335+ pip_url = "git+ssh://git@github.com/test/repo.git" ,
336+ path = tmp_path / "pip_repo" ,
337+ )
338+ assert repo .url == "ssh://git@github.com/test/repo.git"
339+
340+
341+ class TestAsyncGitSyncGetGitVersion :
342+ """Tests for AsyncGitSync.get_git_version()."""
343+
344+ @pytest .mark .asyncio
345+ async def test_get_git_version (
346+ self ,
347+ async_git_repo : AsyncGitSync ,
348+ ) -> None :
349+ """Test get_git_version returns version string."""
350+ version = await async_git_repo .get_git_version ()
351+ assert "git version" in version
352+
353+ @pytest .mark .asyncio
354+ async def test_get_git_version_format (
355+ self ,
356+ async_git_repo : AsyncGitSync ,
357+ ) -> None :
358+ """Test get_git_version returns expected format."""
359+ version = await async_git_repo .get_git_version ()
360+ # Version string should contain numbers
361+ import re
362+
363+ assert re .search (r"\d+\.\d+" , version )
364+
365+
366+ class TestAsyncGitSyncUpdateRepoStash :
367+ """Tests for AsyncGitSync.update_repo() stash handling."""
368+
369+ @pytest .mark .asyncio
370+ async def test_update_repo_with_uncommitted_changes (
371+ self ,
372+ tmp_path : Path ,
373+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
374+ ) -> None :
375+ """Test update_repo with uncommitted changes triggers stash."""
376+ from libvcs .pytest_plugin import git_remote_repo_single_commit_post_init
377+
378+ # Create remote with a commit
379+ remote_repo = create_git_remote_repo (
380+ remote_repo_post_init = git_remote_repo_single_commit_post_init
381+ )
382+ repo_path = tmp_path / "stash_test_repo"
383+
384+ repo = AsyncGitSync (
385+ url = f"file://{ remote_repo } " ,
386+ path = repo_path ,
387+ )
388+ await repo .obtain ()
389+
390+ # Create uncommitted changes
391+ test_file = repo_path / "local_change.txt"
392+ test_file .write_text ("local uncommitted content" )
393+ await repo .cmd .run (["add" , "local_change.txt" ])
394+
395+ # Update should handle the uncommitted changes (may stash if needed)
396+ # This tests the stash logic path
397+ await repo .update_repo ()
398+
399+ # Repo should still exist and be valid
400+ assert repo_path .exists ()
401+ revision = await repo .get_revision ()
402+ assert revision
403+
404+ @pytest .mark .asyncio
405+ async def test_update_repo_clean_working_tree (
406+ self ,
407+ tmp_path : Path ,
408+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
409+ ) -> None :
410+ """Test update_repo with clean working tree skips stash."""
411+ from libvcs .pytest_plugin import git_remote_repo_single_commit_post_init
412+
413+ remote_repo = create_git_remote_repo (
414+ remote_repo_post_init = git_remote_repo_single_commit_post_init
415+ )
416+ repo_path = tmp_path / "clean_repo"
417+
418+ repo = AsyncGitSync (
419+ url = f"file://{ remote_repo } " ,
420+ path = repo_path ,
421+ )
422+ await repo .obtain ()
423+
424+ # No changes - update should succeed without stash
425+ await repo .update_repo ()
426+
427+ assert repo_path .exists ()
428+
429+
430+ class TestAsyncGitSyncSetRemotes :
431+ """Tests for AsyncGitSync.set_remotes()."""
432+
433+ @pytest .mark .asyncio
434+ async def test_set_remotes_overwrite_false (
435+ self ,
436+ tmp_path : Path ,
437+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
438+ ) -> None :
439+ """Test set_remotes with overwrite=False preserves existing remotes."""
440+ remote_repo = create_git_remote_repo ()
441+ repo_path = tmp_path / "set_remotes_repo"
442+
443+ repo = AsyncGitSync (
444+ url = f"file://{ remote_repo } " ,
445+ path = repo_path ,
446+ )
447+ await repo .obtain ()
448+
449+ # Get original origin URL
450+ original_remotes = await repo .remotes_get ()
451+ original_origin_url = original_remotes ["origin" ].fetch_url
452+
453+ # Try to set remotes with overwrite=False
454+ await repo .set_remotes (overwrite = False )
455+
456+ # Origin should still have same URL
457+ updated_remotes = await repo .remotes_get ()
458+ assert updated_remotes ["origin" ].fetch_url == original_origin_url
459+
460+ @pytest .mark .asyncio
461+ async def test_set_remotes_adds_new_remote (
462+ self ,
463+ tmp_path : Path ,
464+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
465+ ) -> None :
466+ """Test set_remotes adds new remotes from configuration."""
467+ remote_repo = create_git_remote_repo ()
468+ upstream_repo = create_git_remote_repo ()
469+ repo_path = tmp_path / "add_remote_repo"
470+
471+ repo = AsyncGitSync (
472+ url = f"file://{ remote_repo } " ,
473+ path = repo_path ,
474+ remotes = {"upstream" : f"file://{ upstream_repo } " },
475+ )
476+ await repo .obtain ()
477+
478+ # Set remotes should add upstream
479+ await repo .set_remotes (overwrite = False )
480+
481+ remotes = await repo .remotes_get ()
482+ assert "origin" in remotes
483+ assert "upstream" in remotes
484+
485+
486+ class TestAsyncGitSyncGetCurrentRemoteName :
487+ """Tests for AsyncGitSync.get_current_remote_name()."""
488+
489+ @pytest .mark .asyncio
490+ async def test_get_current_remote_name_default (
491+ self ,
492+ async_git_repo : AsyncGitSync ,
493+ ) -> None :
494+ """Test get_current_remote_name returns origin by default."""
495+ remote_name = await async_git_repo .get_current_remote_name ()
496+ assert remote_name == "origin"
497+
498+ @pytest .mark .asyncio
499+ async def test_get_current_remote_name_on_detached_head (
500+ self ,
501+ tmp_path : Path ,
502+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
503+ ) -> None :
504+ """Test get_current_remote_name on detached HEAD falls back to origin."""
505+ from libvcs .pytest_plugin import git_remote_repo_single_commit_post_init
506+
507+ remote_repo = create_git_remote_repo (
508+ remote_repo_post_init = git_remote_repo_single_commit_post_init
509+ )
510+ repo_path = tmp_path / "detached_head_repo"
511+
512+ repo = AsyncGitSync (
513+ url = f"file://{ remote_repo } " ,
514+ path = repo_path ,
515+ )
516+ await repo .obtain ()
517+
518+ # Detach HEAD
519+ head = await repo .cmd .rev_parse (args = "HEAD" , verify = True )
520+ await repo .cmd .checkout (branch = head .strip (), detach = True )
521+
522+ # Should fall back to 'origin'
523+ remote_name = await repo .get_current_remote_name ()
524+ assert remote_name == "origin"
525+
526+
272527class TestAsyncGitSyncConcurrency :
273528 """Tests for concurrent AsyncGitSync operations."""
274529
275530 @pytest .mark .asyncio
276531 async def test_concurrent_obtain (
277532 self ,
278533 tmp_path : Path ,
279- create_git_remote_repo : t . Any ,
534+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
280535 ) -> None :
281536 """Test multiple concurrent obtain operations."""
282537 remote_repo = create_git_remote_repo ()
@@ -301,7 +556,7 @@ async def clone_repo(i: int) -> AsyncGitSync:
301556 async def test_concurrent_status_calls (
302557 self ,
303558 tmp_path : Path ,
304- create_git_remote_repo : t . Any ,
559+ create_git_remote_repo : CreateRepoPytestFixtureFn ,
305560 ) -> None :
306561 """Test multiple concurrent status calls on same repo."""
307562 remote_repo = create_git_remote_repo ()
0 commit comments