@@ -623,3 +623,259 @@ def test_remote_update(
623623
624624 # update typically returns "Fetching <remote>" message
625625 assert "fetching" in result .lower () or result == ""
626+
627+
628+ # =============================================================================
629+ # GitTagCmd / GitTagManager Tests
630+ # =============================================================================
631+
632+
633+ class TagCreateFixture (t .NamedTuple ):
634+ """Test fixture for GitTagManager.create() operations."""
635+
636+ test_id : str
637+ tag_name : str
638+ message : str | None
639+ annotate : bool | None
640+ ref : str | None
641+ force : bool | None
642+
643+
644+ TAG_CREATE_FIXTURES : list [TagCreateFixture ] = [
645+ TagCreateFixture (
646+ test_id = "create-lightweight-tag" ,
647+ tag_name = "lightweight-v1.0" ,
648+ message = None ,
649+ annotate = None ,
650+ ref = None ,
651+ force = None ,
652+ ),
653+ TagCreateFixture (
654+ test_id = "create-annotated-tag" ,
655+ tag_name = "annotated-v1.0" ,
656+ message = "Release version 1.0" ,
657+ annotate = None , # message implies annotated
658+ ref = None ,
659+ force = None ,
660+ ),
661+ TagCreateFixture (
662+ test_id = "create-tag-explicit-annotate" ,
663+ tag_name = "explicit-annotated-v1.0" ,
664+ message = "Explicit annotated tag" ,
665+ annotate = True ,
666+ ref = None ,
667+ force = None ,
668+ ),
669+ TagCreateFixture (
670+ test_id = "create-tag-at-ref" ,
671+ tag_name = "ref-v1.0" ,
672+ message = "Tag at HEAD" ,
673+ annotate = None ,
674+ ref = "HEAD" ,
675+ force = None ,
676+ ),
677+ ]
678+
679+
680+ @pytest .mark .parametrize (
681+ list (TagCreateFixture ._fields ),
682+ TAG_CREATE_FIXTURES ,
683+ ids = [test .test_id for test in TAG_CREATE_FIXTURES ],
684+ )
685+ def test_tag_create (
686+ git_repo : GitSync ,
687+ test_id : str ,
688+ tag_name : str ,
689+ message : str | None ,
690+ annotate : bool | None ,
691+ ref : str | None ,
692+ force : bool | None ,
693+ ) -> None :
694+ """Test GitTagManager.create() with various scenarios."""
695+ result = git_repo .cmd .tags .create (
696+ name = tag_name ,
697+ message = message ,
698+ annotate = annotate ,
699+ ref = ref ,
700+ force = force ,
701+ )
702+
703+ # create returns empty string on success
704+ assert result == ""
705+
706+ # Verify tag exists
707+ tag = git_repo .cmd .tags .get (tag_name = tag_name )
708+ assert tag is not None
709+ assert tag .tag_name == tag_name
710+
711+
712+ def test_tag_create_force (git_repo : GitSync ) -> None :
713+ """Test GitTagManager.create() with force flag to replace existing tag."""
714+ tag_name = "force-replace-tag"
715+
716+ # Create initial tag
717+ git_repo .cmd .tags .create (name = tag_name , message = "Initial tag" )
718+
719+ # Creating same tag without force returns error message
720+ result_without_force = git_repo .cmd .tags .create (
721+ name = tag_name , message = "Replacement tag"
722+ )
723+ assert "already exists" in result_without_force .lower ()
724+
725+ # Creating same tag with force should succeed
726+ result = git_repo .cmd .tags .create (
727+ name = tag_name , message = "Replacement tag" , force = True
728+ )
729+ # Force update returns "Updated tag" message
730+ assert result == "" or "updated tag" in result .lower ()
731+
732+
733+ class TagDeleteFixture (t .NamedTuple ):
734+ """Test fixture for GitTagCmd.delete() operations."""
735+
736+ test_id : str
737+ tag_name : str
738+ message : str
739+
740+
741+ TAG_DELETE_FIXTURES : list [TagDeleteFixture ] = [
742+ TagDeleteFixture (
743+ test_id = "delete-lightweight-tag" ,
744+ tag_name = "delete-lightweight" ,
745+ message = "" , # empty message = lightweight tag
746+ ),
747+ TagDeleteFixture (
748+ test_id = "delete-annotated-tag" ,
749+ tag_name = "delete-annotated" ,
750+ message = "Annotated tag to delete" ,
751+ ),
752+ ]
753+
754+
755+ @pytest .mark .parametrize (
756+ list (TagDeleteFixture ._fields ),
757+ TAG_DELETE_FIXTURES ,
758+ ids = [test .test_id for test in TAG_DELETE_FIXTURES ],
759+ )
760+ def test_tag_delete (
761+ git_repo : GitSync ,
762+ test_id : str ,
763+ tag_name : str ,
764+ message : str ,
765+ ) -> None :
766+ """Test GitTagCmd.delete() with various scenarios."""
767+ # Create tag first
768+ if message :
769+ git_repo .cmd .tags .create (name = tag_name , message = message )
770+ else :
771+ git_repo .cmd .tags .create (name = tag_name )
772+
773+ # Get and delete the tag
774+ tag = git_repo .cmd .tags .get (tag_name = tag_name )
775+ assert tag is not None
776+
777+ result = tag .delete ()
778+ assert "deleted tag" in result .lower ()
779+
780+ # Verify tag is gone
781+ with pytest .raises (ObjectDoesNotExist ):
782+ git_repo .cmd .tags .get (tag_name = tag_name )
783+
784+
785+ class TagListFixture (t .NamedTuple ):
786+ """Test fixture for GitTagManager.ls() operations."""
787+
788+ test_id : str
789+ setup_tags : list [str ]
790+ pattern : str | None
791+ expected_min_count : int
792+
793+
794+ TAG_LIST_FIXTURES : list [TagListFixture ] = [
795+ TagListFixture (
796+ test_id = "list-all-tags" ,
797+ setup_tags = ["list-v1.0" , "list-v2.0" , "list-v3.0" ],
798+ pattern = None ,
799+ expected_min_count = 3 ,
800+ ),
801+ TagListFixture (
802+ test_id = "list-tags-with-pattern" ,
803+ setup_tags = ["pattern-alpha" , "pattern-beta" , "other-tag" ],
804+ pattern = "pattern-*" ,
805+ expected_min_count = 2 ,
806+ ),
807+ ]
808+
809+
810+ @pytest .mark .parametrize (
811+ list (TagListFixture ._fields ),
812+ TAG_LIST_FIXTURES ,
813+ ids = [test .test_id for test in TAG_LIST_FIXTURES ],
814+ )
815+ def test_tag_list (
816+ git_repo : GitSync ,
817+ test_id : str ,
818+ setup_tags : list [str ],
819+ pattern : str | None ,
820+ expected_min_count : int ,
821+ ) -> None :
822+ """Test GitTagManager.ls() with various scenarios."""
823+ # Create setup tags
824+ for tag_name in setup_tags :
825+ git_repo .cmd .tags .create (name = tag_name , message = f"Tag { tag_name } " )
826+
827+ # List tags
828+ tags = git_repo .cmd .tags .ls (pattern = pattern )
829+
830+ # Verify minimum count
831+ assert len (tags ) >= expected_min_count
832+
833+
834+ def test_tag_filter (git_repo : GitSync ) -> None :
835+ """Test GitTagManager.filter() with QueryList filtering."""
836+ # Create tags with different prefixes
837+ git_repo .cmd .tags .create (name = "release-1.0" , message = "Release 1.0" )
838+ git_repo .cmd .tags .create (name = "release-2.0" , message = "Release 2.0" )
839+ git_repo .cmd .tags .create (name = "beta-1.0" , message = "Beta 1.0" )
840+
841+ # Filter by prefix
842+ release_tags = git_repo .cmd .tags .filter (tag_name__startswith = "release-" )
843+ assert len (release_tags ) >= 2
844+
845+ beta_tags = git_repo .cmd .tags .filter (tag_name__contains = "beta" )
846+ assert len (beta_tags ) >= 1
847+
848+
849+ def test_tag_show (git_repo : GitSync ) -> None :
850+ """Test GitTagCmd.show() for annotated tags."""
851+ tag_name = "show-test-tag"
852+ message = "This is a test tag for show"
853+
854+ # Create annotated tag
855+ git_repo .cmd .tags .create (name = tag_name , message = message )
856+
857+ # Get tag and show
858+ tag = git_repo .cmd .tags .get (tag_name = tag_name )
859+ assert tag is not None
860+
861+ result = tag .show ()
862+
863+ # show output should contain tag name and message
864+ assert tag_name in result
865+ assert message in result
866+
867+
868+ def test_tag_verify_unsigned (git_repo : GitSync ) -> None :
869+ """Test GitTagCmd.verify() for unsigned/lightweight tags."""
870+ tag_name = "verify-unsigned-tag"
871+
872+ # Create lightweight tag (can't be verified)
873+ git_repo .cmd .tags .create (name = tag_name )
874+
875+ tag = git_repo .cmd .tags .get (tag_name = tag_name )
876+ assert tag is not None
877+
878+ result = tag .verify ()
879+
880+ # verify on unsigned tag should return error message
881+ assert "error" in result .lower () or "cannot verify" in result .lower ()
0 commit comments