package git import ( "path/filepath" "testing" "time" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) type TagSuite struct { suite.Suite *RepoSuite } func TestTagSuite(t *testing.T) { t.Parallel() suite.Run(t, new(TagSuite)) } func (s *TagSuite) SetupTest() { s.RepoSuite = NewRepoSuite(s.T()) } func (s *TagSuite) TearDownTest() { s.RepoSuite.cleanup() } func (s *TagSuite) setupRepoWithTags() { s.init() // create commits for tagging commit1 := s.commitFile("file1.txt", "content 1", "Add file1") commit2 := s.commitFile("file2.txt", "content 2", "Add file2") commit3 := s.commitFile("file3.txt", "content 3", "Add file3") commit4 := s.commitFile("file4.txt", "content 4", "Add file4") commit5 := s.commitFile("file5.txt", "content 5", "Add file5") // create annotated tags s.createAnnotatedTag( "v1.0.0", commit1, "Tagger One", "tagger1@example.com", "Release version 1.0.0\n\nThis is the first stable release.", s.baseTime.Add(1*time.Hour), ) s.createAnnotatedTag( "v1.1.0", commit2, "Tagger Two", "tagger2@example.com", "Release version 1.1.0", s.baseTime.Add(2*time.Hour), ) // create lightweight tags s.createLightweightTag("v2.0.0", commit3) s.createLightweightTag("v2.1.0", commit4) // create another annotated tag s.createAnnotatedTag( "v3.0.0", commit5, "Tagger Three", "tagger3@example.com", "Major version 3.0.0\n\nBreaking changes included.", s.baseTime.Add(3*time.Hour), ) } func (s *TagSuite) TestTags_All() { s.setupRepoWithTags() tags, err := s.repo.Tags(nil) require.NoError(s.T(), err) // we created 5 tags total (3 annotated, 2 lightweight) assert.Len(s.T(), tags, 5, "expected 5 tags") // verify tags are sorted by creation date (newest first) expectedAnnotated := map[string]bool{ "v1.0.0": true, "v1.1.0": true, "v3.0.0": true, } expectedLightweight := map[string]bool{ "v2.0.0": true, "v2.1.0": true, } for _, tag := range tags { if expectedAnnotated[tag.Name] { // annotated tags should have tagger info assert.NotEmpty(s.T(), tag.Tagger.Name, "annotated tag %s should have tagger name", tag.Name) assert.NotEmpty(s.T(), tag.Message, "annotated tag %s should have message", tag.Name) } else if expectedLightweight[tag.Name] { // lightweight tags won't have tagger info or message (they'll have empty values) } else { s.T().Errorf("unexpected tag name: %s", tag.Name) } } } func (s *TagSuite) TestTags_WithLimit() { s.setupRepoWithTags() tests := []struct { name string limit int expectedCount int }{ { name: "limit 1", limit: 1, expectedCount: 1, }, { name: "limit 2", limit: 2, expectedCount: 2, }, { name: "limit 3", limit: 3, expectedCount: 3, }, { name: "limit 10 (more than available)", limit: 10, expectedCount: 5, }, } for _, tt := range tests { s.Run(tt.name, func() { tags, err := s.repo.Tags(&TagsOptions{ Limit: tt.limit, }) require.NoError(s.T(), err) assert.Len(s.T(), tags, tt.expectedCount, "expected %d tags", tt.expectedCount) }) } } func (s *TagSuite) TestTags_WithOffset() { s.setupRepoWithTags() tests := []struct { name string offset int expectedCount int }{ { name: "offset 0", offset: 0, expectedCount: 5, }, { name: "offset 1", offset: 1, expectedCount: 4, }, { name: "offset 2", offset: 2, expectedCount: 3, }, { name: "offset 4", offset: 4, expectedCount: 1, }, { name: "offset 5 (all skipped)", offset: 5, expectedCount: 0, }, { name: "offset 10 (more than available)", offset: 10, expectedCount: 0, }, } for _, tt := range tests { s.Run(tt.name, func() { tags, err := s.repo.Tags(&TagsOptions{ Offset: tt.offset, }) require.NoError(s.T(), err) assert.Len(s.T(), tags, tt.expectedCount, "expected %d tags", tt.expectedCount) }) } } func (s *TagSuite) TestTags_WithLimitAndOffset() { s.setupRepoWithTags() tests := []struct { name string limit int offset int expectedCount int }{ { name: "limit 2, offset 0", limit: 2, offset: 0, expectedCount: 2, }, { name: "limit 2, offset 1", limit: 2, offset: 1, expectedCount: 2, }, { name: "limit 2, offset 3", limit: 2, offset: 3, expectedCount: 2, }, { name: "limit 2, offset 4", limit: 2, offset: 4, expectedCount: 1, }, { name: "limit 3, offset 2", limit: 3, offset: 2, expectedCount: 3, }, { name: "limit 10, offset 3", limit: 10, offset: 3, expectedCount: 2, }, } for _, tt := range tests { s.Run(tt.name, func() { tags, err := s.repo.Tags(&TagsOptions{ Limit: tt.limit, Offset: tt.offset, }) require.NoError(s.T(), err) assert.Len(s.T(), tags, tt.expectedCount, "expected %d tags", tt.expectedCount) }) } } func (s *TagSuite) TestTags_EmptyRepo() { repoPath := filepath.Join(s.tempDir, "empty-repo") _, err := gogit.PlainInit(repoPath, false) require.NoError(s.T(), err) gitRepo, err := PlainOpen(repoPath) require.NoError(s.T(), err) tags, err := gitRepo.Tags(nil) require.NoError(s.T(), err) if tags != nil { assert.Empty(s.T(), tags, "expected no tags in empty repo") } } func (s *TagSuite) TestTags_Pagination() { s.setupRepoWithTags() allTags, err := s.repo.Tags(nil) require.NoError(s.T(), err) assert.Len(s.T(), allTags, 5, "expected 5 tags") pageSize := 2 var paginatedTags []object.Tag for offset := 0; offset < len(allTags); offset += pageSize { tags, err := s.repo.Tags(&TagsOptions{ Limit: pageSize, Offset: offset, }) require.NoError(s.T(), err) paginatedTags = append(paginatedTags, tags...) } assert.Len(s.T(), paginatedTags, len(allTags), "pagination should return all tags") for i := range allTags { assert.Equal(s.T(), allTags[i].Name, paginatedTags[i].Name, "tag at index %d differs", i) } } func (s *TagSuite) TestTags_VerifyAnnotatedTagFields() { s.setupRepoWithTags() tags, err := s.repo.Tags(nil) require.NoError(s.T(), err) var v1Tag *object.Tag for i := range tags { if tags[i].Name == "v1.0.0" { v1Tag = &tags[i] break } } require.NotNil(s.T(), v1Tag, "v1.0.0 tag not found") assert.Equal(s.T(), "Tagger One", v1Tag.Tagger.Name, "tagger name should match") assert.Equal(s.T(), "tagger1@example.com", v1Tag.Tagger.Email, "tagger email should match") assert.Equal(s.T(), "Release version 1.0.0\n\nThis is the first stable release.\n", v1Tag.Message, "tag message should match") assert.Equal(s.T(), plumbing.TagObject, v1Tag.TargetType, "target type should be CommitObject") assert.False(s.T(), v1Tag.Hash.IsZero(), "tag hash should be set") assert.False(s.T(), v1Tag.Target.IsZero(), "target hash should be set") } func (s *TagSuite) TestTags_NilOptions() { s.setupRepoWithTags() tags, err := s.repo.Tags(nil) require.NoError(s.T(), err) assert.Len(s.T(), tags, 5, "nil options should return all tags") } func (s *TagSuite) TestTags_ZeroLimitAndOffset() { s.setupRepoWithTags() tags, err := s.repo.Tags(&TagsOptions{ Limit: 0, Offset: 0, }) require.NoError(s.T(), err) assert.Len(s.T(), tags, 5, "zero limit should return all tags") } func (s *TagSuite) TestTags_OrderedNewestFirst() { s.setupRepoWithTags() tags, err := s.repo.Tags(nil) require.NoError(s.T(), err) require.Len(s.T(), tags, 5) // v3.0.0 has the latest tagger date (baseTime+3h), should be first assert.Equal(s.T(), "v3.0.0", tags[0].Name, "newest tag should be first") } func (s *TagSuite) TestTags_LatestWithLimit1() { s.setupRepoWithTags() tags, err := s.repo.Tags(&TagsOptions{Limit: 1}) require.NoError(s.T(), err) require.Len(s.T(), tags, 1) assert.Equal(s.T(), "v3.0.0", tags[0].Name, "limit=1 should return the newest tag") } func (s *TagSuite) TestTags_Pattern() { s.setupRepoWithTags() v1tag, err := s.repo.Tags(&TagsOptions{ Pattern: "refs/tags/v1.0.0", }) require.NoError(s.T(), err) assert.Len(s.T(), v1tag, 1, "expected 1 tag") }