diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-28 10:20:02 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-28 10:20:02 +0300 |
| commit | 61ebe59a088af0f758115e63340552f3bbac7c19 (patch) | |
| tree | 1e86f7e8a0ffb487b4831b1cedd414d7e336a9fd /internal | |
| parent | a2242a4f6e65434701e23827e02da71590365a58 (diff) | |
refactor(version): unify version tag detection across packages (gq)
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cli/release.go | 13 | ||||
| -rw-r--r-- | internal/cli/release_test.go | 2 | ||||
| -rw-r--r-- | internal/release/release.go | 13 | ||||
| -rw-r--r-- | internal/release/release_tags_test.go | 54 | ||||
| -rw-r--r-- | internal/showcase/metadata.go | 42 | ||||
| -rw-r--r-- | internal/showcase/metadata_test.go | 5 | ||||
| -rw-r--r-- | internal/version/version_tag.go | 11 | ||||
| -rw-r--r-- | internal/version/version_tag_test.go | 37 |
8 files changed, 114 insertions, 63 deletions
diff --git a/internal/cli/release.go b/internal/cli/release.go index da3f2e9..b5300c6 100644 --- a/internal/cli/release.go +++ b/internal/cli/release.go @@ -5,22 +5,13 @@ import ( "fmt" "os" "path/filepath" - "regexp" "strings" "codeberg.org/snonux/gitsyncer/internal/config" "codeberg.org/snonux/gitsyncer/internal/release" + "codeberg.org/snonux/gitsyncer/internal/version" ) -// isVersionTag checks if a tag name is a version tag -// Supports formats: vX.Y.Z, vX.Y, vX, X.Y.Z, X.Y, X -func isVersionTag(tag string) bool { - // Pattern matches version tags with optional 'v' prefix - pattern := `^v?\d+(\.\d+)?(\.\d+)?$` - matched, _ := regexp.MatchString(pattern, tag) - return matched -} - // HandleCheckReleases checks for version tags without releases and creates them with confirmation func HandleCheckReleases(cfg *config.Config, flags *Flags) int { // Get all repositories from work directory @@ -392,7 +383,7 @@ func processUpdateReleasesForTarget( fmt.Printf("\n Updating existing %s releases...\n", target.name) for _, tag := range existingReleases { - if !isVersionTag(tag) { + if !version.IsVersionTag(tag) { continue } diff --git a/internal/cli/release_test.go b/internal/cli/release_test.go index 5712530..12876ae 100644 --- a/internal/cli/release_test.go +++ b/internal/cli/release_test.go @@ -363,7 +363,7 @@ func TestProcessUpdateReleasesForTarget_UsesCachedAIAndSkipsNonVersionTags(t *te name: "GitHub", owner: "owner", getReleases: func(_ string, _ string) ([]string, error) { - return []string{"latest", "v1.0.0"}, nil + return []string{"latest", "1-beta", "v1.0.0"}, nil }, updateRelease: func(_ string, _ string, tag string, notes string) error { if notes != "cached ai notes" { diff --git a/internal/release/release.go b/internal/release/release.go index 9ddf563..da2c221 100644 --- a/internal/release/release.go +++ b/internal/release/release.go @@ -8,11 +8,11 @@ import ( "net/http" "os" "os/exec" - "regexp" "sort" "strings" "codeberg.org/snonux/gitsyncer/internal/httpclient" + "codeberg.org/snonux/gitsyncer/internal/version" ) // Tag represents a git tag @@ -117,15 +117,6 @@ func (m *Manager) EnsureCodebergReleasesEnabled(owner, repo string) error { return nil } -// isVersionTag checks if a tag name is a version tag -// Supports formats: vX.Y.Z, vX.Y, vX, X.Y.Z, X.Y, X -func isVersionTag(tag string) bool { - // Pattern matches version tags with optional 'v' prefix - pattern := `^v?\d+(\.\d+)?(\.\d+)?$` - matched, _ := regexp.MatchString(pattern, tag) - return matched -} - // GetLocalTags returns all version tags from the local git repository func (m *Manager) GetLocalTags(repoPath string) ([]string, error) { cmd := exec.Command("git", "-C", repoPath, "tag", "--list") @@ -139,7 +130,7 @@ func (m *Manager) GetLocalTags(repoPath string) ([]string, error) { for _, tag := range tags { tag = strings.TrimSpace(tag) - if tag != "" && isVersionTag(tag) { + if tag != "" && version.IsVersionTag(tag) { versionTags = append(versionTags, tag) } } diff --git a/internal/release/release_tags_test.go b/internal/release/release_tags_test.go new file mode 100644 index 0000000..334c165 --- /dev/null +++ b/internal/release/release_tags_test.go @@ -0,0 +1,54 @@ +package release + +import ( + "os" + "os/exec" + "path/filepath" + "reflect" + "testing" +) + +func TestGetLocalTags_FiltersToStrictVersionTags(t *testing.T) { + t.Parallel() + + repoPath := t.TempDir() + runGit(t, repoPath, "init") + runGit(t, repoPath, "config", "user.name", "Test User") + runGit(t, repoPath, "config", "user.email", "test@example.com") + + readmePath := filepath.Join(repoPath, "README.md") + if err := os.WriteFile(readmePath, []byte("test"), 0o644); err != nil { + t.Fatalf("write file: %v", err) + } + runGit(t, repoPath, "add", "README.md") + runGit(t, repoPath, "commit", "-m", "initial commit") + + runGit(t, repoPath, "tag", "v2") + runGit(t, repoPath, "tag", "1.2") + runGit(t, repoPath, "tag", "v1.0.0") + runGit(t, repoPath, "tag", "latest") + runGit(t, repoPath, "tag", "1-beta") + + manager := NewManager("") + tags, err := manager.GetLocalTags(repoPath) + if err != nil { + t.Fatalf("GetLocalTags() error = %v", err) + } + + want := []string{"v1.0.0", "1.2", "v2"} + if !reflect.DeepEqual(tags, want) { + t.Fatalf("GetLocalTags() = %#v, want %#v", tags, want) + } +} + +func runGit(t *testing.T, repoPath string, args ...string) string { + t.Helper() + + cmd := exec.Command("git", append([]string{"-C", repoPath}, args...)...) + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git %v failed: %v\n%s", args, err, string(output)) + } + + return string(output) +} diff --git a/internal/showcase/metadata.go b/internal/showcase/metadata.go index c6c756b..6a8d4d6 100644 --- a/internal/showcase/metadata.go +++ b/internal/showcase/metadata.go @@ -9,6 +9,8 @@ import ( "strconv" "strings" "time" + + "codeberg.org/snonux/gitsyncer/internal/version" ) const unreleasedScorePenalty = 0.75 @@ -29,7 +31,7 @@ type RepoMetadata struct { LinesOfDocs int // Lines of documentation FirstCommitDate string LastCommitDate string - LastActivityDate string // Most recent commit on any local branch (--all); used for activity checks + LastActivityDate string // Most recent commit on any local branch (--all); used for activity checks License string AvgCommitAge float64 // Average age of last 42 commits in days (HEAD only; used for score) TagCount int // Total number of git tags in the repository @@ -374,7 +376,7 @@ func getLatestTag(repoPath string) (string, string, bool, int, error) { // Find the first tag that looks like a version number latestTag := "" for _, tag := range tags { - if isVersionTag(tag) { + if version.IsVersionTag(tag) { latestTag = tag break } @@ -403,39 +405,3 @@ func getLatestTag(repoPath string) (string, string, bool, int, error) { // Return the latest tag and its date return latestTag, tagDate, true, tagCount, nil } - -// isVersionTag checks if a tag looks like a version number -func isVersionTag(tag string) bool { - // Remove 'v' prefix if present - versionStr := strings.TrimPrefix(tag, "v") - - // Check if the remaining string contains at least one digit and one dot - hasDigit := false - hasDot := false - - for _, ch := range versionStr { - if ch >= '0' && ch <= '9' { - hasDigit = true - } else if ch == '.' { - hasDot = true - } else if ch != '-' && ch != '+' && ch != '_' && - (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z') { - // Allow alphanumeric characters and common separators - // but anything else makes it not a version - return false - } - } - - // Must have at least one digit, and either: - // - have a dot (e.g., 1.0, 0.1.2) - // - be just digits (e.g., 2, 2024) - // - start with a digit (e.g., 1-beta) - if hasDigit && len(versionStr) > 0 { - firstChar := versionStr[0] - if firstChar >= '0' && firstChar <= '9' { - return true - } - } - - return hasDigit && hasDot -} diff --git a/internal/showcase/metadata_test.go b/internal/showcase/metadata_test.go index abc4664..e905755 100644 --- a/internal/showcase/metadata_test.go +++ b/internal/showcase/metadata_test.go @@ -59,6 +59,7 @@ func TestGetLatestTag_ReturnsTotalTagCount(t *testing.T) { writeAndCommit("README.md", "first", "first") runGit(t, repoPath, "tag", "notes") + runGit(t, repoPath, "tag", "1-beta") runGit(t, repoPath, "tag", "v1.0.0") writeAndCommit("README.md", "second", "second") @@ -74,8 +75,8 @@ func TestGetLatestTag_ReturnsTotalTagCount(t *testing.T) { if !hasReleases { t.Fatal("expected hasReleases to be true") } - if tagCount != 3 { - t.Fatalf("tagCount = %d, want %d", tagCount, 3) + if tagCount != 4 { + t.Fatalf("tagCount = %d, want %d", tagCount, 4) } } diff --git a/internal/version/version_tag.go b/internal/version/version_tag.go new file mode 100644 index 0000000..0d12ffa --- /dev/null +++ b/internal/version/version_tag.go @@ -0,0 +1,11 @@ +package version + +import "regexp" + +var versionTagPattern = regexp.MustCompile(`^v?\d+(\.\d+)?(\.\d+)?$`) + +// IsVersionTag checks if a tag name is a version tag. +// Supported formats: vX.Y.Z, vX.Y, vX, X.Y.Z, X.Y, X. +func IsVersionTag(tag string) bool { + return versionTagPattern.MatchString(tag) +} diff --git a/internal/version/version_tag_test.go b/internal/version/version_tag_test.go new file mode 100644 index 0000000..93ee3b8 --- /dev/null +++ b/internal/version/version_tag_test.go @@ -0,0 +1,37 @@ +package version + +import "testing" + +func TestIsVersionTag(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + tag string + want bool + }{ + {name: "single digit", tag: "1", want: true}, + {name: "single digit with v prefix", tag: "v1", want: true}, + {name: "major minor", tag: "1.2", want: true}, + {name: "major minor with v prefix", tag: "v1.2", want: true}, + {name: "major minor patch", tag: "1.2.3", want: true}, + {name: "major minor patch with v prefix", tag: "v1.2.3", want: true}, + {name: "empty", tag: "", want: false}, + {name: "letter prefix", tag: "release-1.2.3", want: false}, + {name: "too many components", tag: "1.2.3.4", want: false}, + {name: "trailing dot", tag: "1.", want: false}, + {name: "prerelease style", tag: "1-beta", want: false}, + {name: "prerelease style with v prefix", tag: "v1-beta", want: false}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := IsVersionTag(tc.tag) + if got != tc.want { + t.Fatalf("IsVersionTag(%q) = %v, want %v", tc.tag, got, tc.want) + } + }) + } +} |
