Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,16 @@ func GetInitialCommit() (string, error) {
return strings.TrimSpace(string(output)), nil
}

// GetLatestTag returns the most recent tag matching the given prefix, sorted by semver.
// Returns empty string if no matching tags found.
func GetLatestTag(prefix string) (string, string, error) {
// GetLatestTag returns the most recent tag matching the given prefix, sorted by
// semver. Tag lookups run against dir so the caller's repository is read even
// when the process working directory points elsewhere; an empty dir falls back
// to the process working directory. Returns empty string if no matching tags
// found.
func GetLatestTag(dir, prefix string) (string, string, error) {
// Get all tags matching prefix, sorted by version descending
// --sort=-v:refname sorts by version in descending order
cmd := exec.Command("git", "tag", "-l", prefix+"*", "--sort=-v:refname")
cmd.Dir = dir
output, err := cmd.Output()
if err != nil {
return "", "", fmt.Errorf("git tag: %w", err)
Expand All @@ -172,6 +176,7 @@ func GetLatestTag(prefix string) (string, string, error) {

// First valid tag is the latest (git sorted descending by version).
cmd = exec.Command("git", "rev-list", "-n", "1", tag)
cmd.Dir = dir
output, err = cmd.Output()
if err != nil {
return tag, "", fmt.Errorf("git rev-list for tag: %w", err)
Expand Down Expand Up @@ -393,8 +398,12 @@ func remoteRefAlreadyGone(out []byte) bool {

// GetLatestReleaseTag returns the most recent non-prerelease tag (no -rc suffix).
// This is used to find the base version for calculating next release versions.
func GetLatestReleaseTag(prefix string) (string, string, error) {
// Tag lookups run against dir so the caller's repository is read even when the
// process working directory points elsewhere; an empty dir falls back to the
// process working directory.
func GetLatestReleaseTag(dir, prefix string) (string, string, error) {
cmd := exec.Command("git", "tag", "-l", prefix+"*", "--sort=-v:refname")
cmd.Dir = dir
output, err := cmd.Output()
if err != nil {
return "", "", fmt.Errorf("git tag: %w", err)
Expand All @@ -416,6 +425,7 @@ func GetLatestReleaseTag(prefix string) (string, string, error) {
if !strings.Contains(tag, "-rc.") {
// Get the SHA for this tag
cmd = exec.Command("git", "rev-list", "-n", "1", tag)
cmd.Dir = dir
output, err = cmd.Output()
if err != nil {
return tag, "", fmt.Errorf("git rev-list for tag: %w", err)
Expand Down
84 changes: 79 additions & 5 deletions internal/git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ func tagHead(t *testing.T, name string) {
}

func TestGetLatestTag_IgnoresNonVersionTags(t *testing.T) {
newScratchRepo(t)
dir := newScratchRepo(t)
commitFile(t, "a.txt", "one", "first commit")

// Valid version tags plus non-version tags that sort newer by base version.
Expand All @@ -335,7 +335,7 @@ func TestGetLatestTag_IgnoresNonVersionTags(t *testing.T) {
tagHead(t, "v0.6.0-dryrun.1") // higher base version, not a cascade version
tagHead(t, "vnightly") // foreign tag matching the prefix glob

got, sha, err := GetLatestTag("v")
got, sha, err := GetLatestTag(dir, "v")
if err != nil {
t.Fatalf("GetLatestTag() unexpected error: %v", err)
}
Expand All @@ -348,15 +348,15 @@ func TestGetLatestTag_IgnoresNonVersionTags(t *testing.T) {
}

func TestGetLatestReleaseTag_IgnoresNonVersionTags(t *testing.T) {
newScratchRepo(t)
dir := newScratchRepo(t)
commitFile(t, "a.txt", "one", "first commit")

tagHead(t, "v0.5.0")
tagHead(t, "v0.5.1")
tagHead(t, "v0.6.0-dryrun.1") // not an -rc tag, but also not a valid version
tagHead(t, "vnightly")

got, sha, err := GetLatestReleaseTag("v")
got, sha, err := GetLatestReleaseTag(dir, "v")
if err != nil {
t.Fatalf("GetLatestReleaseTag() unexpected error: %v", err)
}
Expand All @@ -375,7 +375,7 @@ func TestGetLatestReleaseTag_SkipsRCButKeepsValidRelease(t *testing.T) {
tagHead(t, "v1.0.0")
tagHead(t, "v1.0.1-rc.0") // valid prerelease, must be skipped for "release"

got, _, err := GetLatestReleaseTag("v")
got, _, err := GetLatestReleaseTag("", "v")
if err != nil {
t.Fatalf("GetLatestReleaseTag() unexpected error: %v", err)
}
Expand All @@ -384,6 +384,80 @@ func TestGetLatestReleaseTag_SkipsRCButKeepsValidRelease(t *testing.T) {
}
}

// initRepoAt initializes a git repository at dir, commits a file, and creates a
// lightweight tag pointing at the resulting commit, all via "git -C" so the
// process working directory is never changed.
func initRepoAt(t *testing.T, dir, tag string) {
t.Helper()
for _, args := range [][]string{
{"-C", dir, "init"},
{"-C", dir, "config", "user.email", "test@example.com"},
{"-C", dir, "config", "user.name", "Test User"},
{"-C", dir, "config", "commit.gpgsign", "false"},
} {
if out, err := exec.Command("git", args...).CombinedOutput(); err != nil {
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
}
}
if err := os.WriteFile(filepath.Join(dir, "f.txt"), []byte("x"), 0o600); err != nil {
t.Fatalf("write file: %v", err)
}
for _, args := range [][]string{
{"-C", dir, "add", "f.txt"},
{"-C", dir, "commit", "-m", "seed"},
{"-C", dir, "tag", tag},
} {
if out, err := exec.Command("git", args...).CombinedOutput(); err != nil {
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
}
}
}

// TestGetLatestTag_ScopesToDir proves the lookup reads the repository at the
// given dir, not the process working directory. The cwd repo carries a decoy
// tag that sorts higher; a cwd-scoped lookup would return it.
func TestGetLatestTag_ScopesToDir(t *testing.T) {
newScratchRepo(t) // cwd is a repo with a higher-sorting decoy tag
commitFile(t, "a.txt", "one", "first commit")
tagHead(t, "v9.9.9")

target := t.TempDir()
initRepoAt(t, target, "v1.2.3")

got, sha, err := GetLatestTag(target, "v")
if err != nil {
t.Fatalf("GetLatestTag() unexpected error: %v", err)
}
if got != "v1.2.3" {
t.Errorf("GetLatestTag() = %q, want %q (must read the dir repo, not cwd)", got, "v1.2.3")
}
if sha == "" {
t.Errorf("GetLatestTag() returned empty SHA for %q", got)
}
}

// TestGetLatestReleaseTag_ScopesToDir mirrors TestGetLatestTag_ScopesToDir for
// the release-tag lookup.
func TestGetLatestReleaseTag_ScopesToDir(t *testing.T) {
newScratchRepo(t)
commitFile(t, "a.txt", "one", "first commit")
tagHead(t, "v9.9.9")

target := t.TempDir()
initRepoAt(t, target, "v1.2.3")

got, sha, err := GetLatestReleaseTag(target, "v")
if err != nil {
t.Fatalf("GetLatestReleaseTag() unexpected error: %v", err)
}
if got != "v1.2.3" {
t.Errorf("GetLatestReleaseTag() = %q, want %q (must read the dir repo, not cwd)", got, "v1.2.3")
}
if sha == "" {
t.Errorf("GetLatestReleaseTag() returned empty SHA for %q", got)
}
}

func TestIsValidVersionTag(t *testing.T) {
tests := []struct {
tag string
Expand Down
4 changes: 2 additions & 2 deletions internal/orchestrate/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ func (o *Orchestrator) calculateVersion() (string, error) {

// If no state, check for latest RC tag
if currentDevVersion == "" {
latestTag, _, err := git.GetLatestTag(tagPrefix)
latestTag, _, err := git.GetLatestTag(o.baseDir, tagPrefix)
if err != nil {
log.Warn("Failed to get latest tag: %v", err)
} else if latestTag != "" {
Expand All @@ -355,7 +355,7 @@ func (o *Orchestrator) calculateVersion() (string, error) {

// Get latest published release (non-RC) as base version for version calculation
// This ensures we continue from v1.0.0 → v1.0.1-rc.0, not restart at v0.1.0-rc.0
latestRelease, releaseSHA, err := git.GetLatestReleaseTag(tagPrefix)
latestRelease, releaseSHA, err := git.GetLatestReleaseTag(o.baseDir, tagPrefix)
if err != nil {
log.Warn("Failed to get latest release tag: %v", err)
} else if latestRelease != "" {
Expand Down
Loading