From 56f8d33503cce1a8ad8b9d50d09686363892953b Mon Sep 17 00:00:00 2001 From: Matías Insaurralde Date: Sat, 28 Mar 2026 01:49:21 +0000 Subject: [PATCH] knotserver/git: add comprehensive tag tests, improve parseTagRecord coverage Signed-off-by: Matías Insaurralde --- knotserver/git/tag_test.go | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file(s) changed, 156 insertion(s)(+), 0 deletion(s)(-) diff --git a/knotserver/git/tag_test.go b/knotserver/git/tag_test.go --- a/knotserver/git/tag_test.go +++ b/knotserver/git/tag_test.go @@ -1,7 +1,9 @@ package git import ( + "os" "path/filepath" + "strings" "testing" "time" @@ -384,3 +386,157 @@ require.NoError(s.T(), err) assert.Len(s.T(), v1tag, 1, "expected 1 tag") } + +func (s *TagSuite) TestTags_PatternGlob() { + s.setupRepoWithTags() + + tags, err := s.repo.Tags(&TagsOptions{ + Pattern: "refs/tags/v1.*", + }) + require.NoError(s.T(), err) + require.Len(s.T(), tags, 2, "glob pattern refs/tags/v1.* should match 2 tags") + + names := map[string]bool{} + for _, t := range tags { + names[t.Name] = true + } + assert.True(s.T(), names["v1.0.0"], "v1.0.0 should match pattern") + assert.True(s.T(), names["v1.1.0"], "v1.1.0 should match pattern") +} + +func (s *TagSuite) TestTags_PatternNoMatch() { + s.setupRepoWithTags() + + tags, err := s.repo.Tags(&TagsOptions{ + Pattern: "refs/tags/v9.*", + }) + require.NoError(s.T(), err) + assert.Empty(s.T(), tags, "non-matching pattern should return no tags") +} + +func (s *TagSuite) TestTags_VerifyLightweightTagFields() { + s.setupRepoWithTags() + + tags, err := s.repo.Tags(nil) + require.NoError(s.T(), err) + + var v2Tag *object.Tag + for i := range tags { + if tags[i].Name == "v2.0.0" { + v2Tag = &tags[i] + break + } + } + require.NotNil(s.T(), v2Tag, "v2.0.0 tag not found") + + assert.Empty(s.T(), v2Tag.Tagger.Name, "lightweight tag should have no tagger name") + assert.Empty(s.T(), v2Tag.Tagger.Email, "lightweight tag should have no tagger email") + assert.True(s.T(), v2Tag.Tagger.When.IsZero(), "lightweight tag should have zero tagger date") + // For a lightweight tag %(contents:subject) returns the commit subject, not a tag annotation + assert.Equal(s.T(), "Add file3", v2Tag.Message, "lightweight tag message should be the commit subject") + assert.False(s.T(), v2Tag.Hash.IsZero(), "lightweight tag hash should be the commit hash") + assert.Equal(s.T(), plumbing.CommitObject, v2Tag.TargetType, "lightweight tag should resolve to a commit") +} + +func (s *TagSuite) TestTags_SubjectOnlyMessage() { + s.setupRepoWithTags() + + tags, err := s.repo.Tags(nil) + require.NoError(s.T(), err) + + var v11Tag *object.Tag + for i := range tags { + if tags[i].Name == "v1.1.0" { + v11Tag = &tags[i] + break + } + } + require.NotNil(s.T(), v11Tag, "v1.1.0 tag not found") + + // v1.1.0 was created with a subject-only message (no body paragraph) + assert.Equal(s.T(), "Release version 1.1.0", v11Tag.Message, + "subject-only tag message should equal the subject line") +} + +func (s *TagSuite) TestTags_FullOrdering() { + s.setupRepoWithTags() + + tags, err := s.repo.Tags(nil) + require.NoError(s.T(), err) + require.Len(s.T(), tags, 5) + + // Annotated tags carry explicit tagger dates (baseTime+3h, +2h, +1h) so they + // sort ahead of lightweight tags whose commits have no explicit author date. + assert.Equal(s.T(), "v3.0.0", tags[0].Name, "v3.0.0 should be newest (baseTime+3h)") + assert.Equal(s.T(), "v1.1.0", tags[1].Name, "v1.1.0 should be second (baseTime+2h)") + assert.Equal(s.T(), "v1.0.0", tags[2].Name, "v1.0.0 should be third (baseTime+1h)") + + // Lightweight tags v2.0.0 and v2.1.0 have zero-time commits and sort to the + // end; their relative order is not guaranteed. + lastName := map[string]bool{tags[3].Name: true, tags[4].Name: true} + assert.True(s.T(), lastName["v2.0.0"], "v2.0.0 should be in the last two positions") + assert.True(s.T(), lastName["v2.1.0"], "v2.1.0 should be in the last two positions") +} + +func (s *TagSuite) TestTags_ForEachRefError() { + s.setupRepoWithTags() + + // Remove .git so the underlying git command fails. + err := os.RemoveAll(filepath.Join(s.repo.path, ".git")) + require.NoError(s.T(), err) + + _, err = s.repo.Tags(nil) + assert.Error(s.T(), err, "Tags should return an error when the git command fails") +} + +func (s *TagSuite) TestParseTagRecord_BodyOnly() { + // When subject is empty and body is non-empty the else branch sets message = body. + fields := []string{ + "v1.0.0", // tagName + "abc123", // objectHash + "tag", // objectType + "def456", // targetHash + "commit", // targetType + "Tagger", // taggerName + "", // taggerEmail + "0", // taggerDate + "", // subject — empty + "body text", // body — non-empty + "", // signature + } + line := strings.Join(fields, fieldSeparator) + tag, ok, err := parseTagRecord(line) + require.NoError(s.T(), err) + require.True(s.T(), ok) + assert.Equal(s.T(), "body text", tag.Message, "body-only message should equal the body field") +} + +func (s *TagSuite) TestParseTagRecord_ShortRecord() { + // A record with fewer than 6 fields must be skipped without error. + short := strings.Join([]string{"v1.0.0", "abc123", "tag", "def456"}, fieldSeparator) + tag, ok, err := parseTagRecord(short) + require.NoError(s.T(), err) + assert.False(s.T(), ok, "short record should be skipped") + assert.Equal(s.T(), object.Tag{}, tag) +} + +func (s *TagSuite) TestParseTagRecord_InvalidObjectType() { + // A record whose objecttype field is unrecognised must surface an error. + fields := []string{ + "v1.0.0", // tagName + "abc123", // objectHash + "invalid_type", // objectType — not a valid git object type + "def456", // targetHash + "commit", // targetType + "Tagger", // taggerName + "", // taggerEmail + "0", // taggerDate + "subject", // subject + "body", // body + "", // signature + } + line := strings.Join(fields, fieldSeparator) + _, ok, err := parseTagRecord(line) + assert.Error(s.T(), err, "invalid object type should return an error") + assert.False(s.T(), ok) +} -- tangled.sh