diff --git a/integration_tests/src/test/java/de/psanetra/gitsemver/LatestCmdIncludingPreReleasesTests.java b/integration_tests/src/test/java/de/psanetra/gitsemver/LatestCmdIncludingPreReleasesTests.java index 9c67c14..ffbfeec 100644 --- a/integration_tests/src/test/java/de/psanetra/gitsemver/LatestCmdIncludingPreReleasesTests.java +++ b/integration_tests/src/test/java/de/psanetra/gitsemver/LatestCmdIncludingPreReleasesTests.java @@ -2,7 +2,6 @@ import de.psanetra.gitsemver.containers.GitSemverContainer; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Disabled; import static org.assertj.core.api.Assertions.assertThat; @@ -68,7 +67,6 @@ public void shouldReturnVersionsNotReachableFromHEAD() { } @Test - @Disabled("Skipping as it requires a specific version from fork source") public void shouldReturnEmptyVersionOnRepoWithoutTags() { try (var container = new GitSemverContainer()) { container.start(); diff --git a/integration_tests/src/test/java/de/psanetra/gitsemver/LatestCmdTests.java b/integration_tests/src/test/java/de/psanetra/gitsemver/LatestCmdTests.java index 51eec5e..98ac459 100644 --- a/integration_tests/src/test/java/de/psanetra/gitsemver/LatestCmdTests.java +++ b/integration_tests/src/test/java/de/psanetra/gitsemver/LatestCmdTests.java @@ -2,7 +2,6 @@ import de.psanetra.gitsemver.containers.GitSemverContainer; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Disabled; import static org.assertj.core.api.Assertions.assertThat; @@ -47,7 +46,6 @@ public void shouldReturnVersionsNotReachableFromHEAD() { @Test - @Disabled("Skipping as it requires a specific version from fork source") public void shouldReturnEmptyVersionOnRepoWithoutTags() { try (var container = new GitSemverContainer()) { container.start(); diff --git a/integration_tests/src/test/java/de/psanetra/gitsemver/NextCmdTests.java b/integration_tests/src/test/java/de/psanetra/gitsemver/NextCmdTests.java index e531fbb..9aaff78 100644 --- a/integration_tests/src/test/java/de/psanetra/gitsemver/NextCmdTests.java +++ b/integration_tests/src/test/java/de/psanetra/gitsemver/NextCmdTests.java @@ -100,7 +100,7 @@ public void shouldConvertPrereleaseToRelease() { /** * In this case the next command can not calculate the next version based on the commits since the latest release. */ - @Disabled("Skipping as it requires a specific version from fork source") + @Disabled("This fork intentionally does not assert that the latest version tag is reachable from HEAD") @Test public void shouldReturnErrorCodeIfLatestVersionNotReachableFromHEAD() throws IOException, InterruptedException { try (var container = new GitSemverContainer()) { @@ -151,7 +151,6 @@ public void shouldNotReturnErrorCodeIfLatestVersionReachableFromDetachedHEAD() t } - @Disabled("Skipping as it requires a specific version from fork source") @Test public void shouldReturnFirstVersionOnRepoWithoutTags() { try (var container = new GitSemverContainer()) { @@ -252,7 +251,7 @@ public void shouldReturnNewVersionAfterMergeWithFeatureBranchPrallelToRelease() } - @Disabled("Skipping as it requires a specific version from fork source") + @Disabled("This fork intentionally does not assert that the latest version tag is reachable from HEAD") @Test public void shouldPanicIfCommitIsMissingOnShallowClone() { try (var container = new GitSemverContainer()) { diff --git a/latest/latest.go b/latest/latest.go index 023efff..e618c9e 100644 --- a/latest/latest.go +++ b/latest/latest.go @@ -7,7 +7,6 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/pkg/errors" "io" - "os/exec" "strings" ) @@ -52,22 +51,8 @@ func FindLatestVersion(repo *git.Repository, majorVersionFilter int, preRelease } func findLatestVersionTag(repo *git.Repository, majorVersionFilter int, includePreReleases bool) (*plumbing.Reference, error) { - // Use git rev-list to get the latest tag from all branches, not just the current branch - cmd := exec.Command("git", "rev-list", "--tags", "--max-count=1") - worktree, err := repo.Worktree() - if err != nil { - return nil, err - } - - cmd.Dir = worktree.Filesystem.Root() // Use the working directory for git commands - // We no longer need to capture the output of the command - _, err = cmd.Output() - - if err != nil { - return nil, err - } - - // Retrieve the tags from the repository + // Retrieve the tags from the repository. All tags are considered, not just the ones + // reachable from the current branch. tagIter, err := repo.Tags() if err != nil { return nil, err @@ -103,9 +88,13 @@ func findLatestVersionTag(repo *git.Repository, majorVersionFilter int, includeP foundTags = append(foundTags, tag) } - // If no valid tags were found + // If no valid tags were found the repository simply has no matching version yet. + // This is not an error: callers interpret a nil tag as "no previous version" and + // fall back to the empty version (0.0.0). Returning an error here would break + // repositories which have not been released yet, as well as repositories whose + // only tags are pre-releases when pre-releases are excluded. if len(foundTags) == 0 { - return nil, errors.New("no matching semantic version tags found") + return nil, nil } // Find the highest version tag diff --git a/latest/latest_test.go b/latest/latest_test.go index 41568f2..227a06a 100644 --- a/latest/latest_test.go +++ b/latest/latest_test.go @@ -2,8 +2,12 @@ package latest import ( "github.com/PandasWhoCode/git-semver/semver" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" + "time" ) func Test_tagNameToVersion_should_return_version(t *testing.T) { @@ -35,3 +39,91 @@ func Test_tagNameToVersion_should_return_version_if_tag_has_v_prefix(t *testing. version, ) } + +func Test_FindLatestVersion_should_return_no_version_if_repository_has_no_tags(t *testing.T) { + repo, _ := initRepo(t) + + version, tag, err := FindLatestVersion(repo, -1, false) + + require.NoError(t, err) + assert.Nil(t, version) + assert.Nil(t, tag) +} + +func Test_FindLatestVersion_should_return_no_version_if_repository_only_has_pre_release_tags(t *testing.T) { + repo, _ := initRepo(t, "v0.1.0-alpha.1", "v0.1.0-alpha.2") + + version, tag, err := FindLatestVersion(repo, -1, false) + + require.NoError(t, err) + assert.Nil(t, version) + assert.Nil(t, tag) +} + +func Test_FindLatestVersion_should_return_latest_pre_release_if_pre_releases_are_included(t *testing.T) { + repo, _ := initRepo(t, "v0.1.0-alpha.1", "v0.1.0-alpha.2") + + version, tag, err := FindLatestVersion(repo, -1, true) + + require.NoError(t, err) + assert.Equal(t, "0.1.0-alpha.2", version.ToString()) + assert.Equal(t, "v0.1.0-alpha.2", tag.Name().Short()) +} + +func Test_FindLatestVersion_should_ignore_pre_release_tags_if_a_release_tag_exists(t *testing.T) { + repo, _ := initRepo(t, "v0.1.0", "v0.2.0-alpha.1") + + version, tag, err := FindLatestVersion(repo, -1, false) + + require.NoError(t, err) + assert.Equal(t, "0.1.0", version.ToString()) + assert.Equal(t, "v0.1.0", tag.Name().Short()) +} + +func Test_Latest_should_return_empty_version_if_repository_has_no_tags(t *testing.T) { + _, workdir := initRepo(t) + + version, err := Latest(LatestOptions{Workdir: workdir}) + + require.NoError(t, err) + assert.Equal(t, "0.0.0", version.ToString()) +} + +func Test_Latest_should_return_empty_version_if_repository_only_has_pre_release_tags(t *testing.T) { + _, workdir := initRepo(t, "v0.1.0-alpha.1") + + version, err := Latest(LatestOptions{Workdir: workdir}) + + require.NoError(t, err) + assert.Equal(t, "0.0.0", version.ToString()) +} + +// initRepo creates a repository with a single commit which is tagged with all given tag names. +func initRepo(t *testing.T, tagNames ...string) (*git.Repository, string) { + t.Helper() + + workdir := t.TempDir() + + repo, err := git.PlainInit(workdir, false) + assert.NoError(t, err) + + worktree, err := repo.Worktree() + assert.NoError(t, err) + + commit, err := worktree.Commit("feat: initial commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "git-semver", + Email: "git-semver@example.com", + When: time.Now(), + }, + AllowEmptyCommits: true, + }) + assert.NoError(t, err) + + for _, tagName := range tagNames { + _, err = repo.CreateTag(tagName, commit, nil) + assert.NoError(t, err) + } + + return repo, workdir +} diff --git a/next/next_test.go b/next/next_test.go new file mode 100644 index 0000000..9e43d77 --- /dev/null +++ b/next/next_test.go @@ -0,0 +1,103 @@ +package next + +import ( + "github.com/PandasWhoCode/git-semver/semver" + "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" + "testing" + "time" +) + +func Test_Next_should_return_first_version_if_repository_has_no_tags(t *testing.T) { + workdir := initRepo(t, commit{message: "feat: initial commit"}) + + version, err := Next(NextOptions{ + Workdir: workdir, + Stable: true, + MajorVersionFilter: -1, + }) + + require.NoError(t, err) + assert.Equal(t, "1.0.0", version.ToString()) +} + +func Test_Next_should_return_next_version_if_repository_only_has_pre_release_tags(t *testing.T) { + workdir := initRepo( + t, + commit{message: "feat: initial commit", tags: []string{"v0.1.0-alpha.1"}}, + commit{message: "fix: a fix"}, + ) + + version, err := Next(NextOptions{ + Workdir: workdir, + Stable: false, + MajorVersionFilter: -1, + }) + + require.NoError(t, err) + assert.Equal(t, "0.1.0", version.ToString()) +} + +func Test_Next_should_increment_pre_release_counter_if_repository_only_has_pre_release_tags(t *testing.T) { + workdir := initRepo( + t, + commit{message: "feat: initial commit", tags: []string{"v0.1.0-alpha.1"}}, + commit{message: "fix: a fix", tags: []string{"v0.1.0-alpha.2"}}, + commit{message: "fix: another fix"}, + ) + + version, err := Next(NextOptions{ + Workdir: workdir, + Stable: false, + MajorVersionFilter: -1, + PreReleaseOptions: semver.PreReleaseOptions{ + Label: "alpha", + AppendCounter: true, + }, + }) + + require.NoError(t, err) + assert.Equal(t, "0.1.0-alpha.3", version.ToString()) +} + +type commit struct { + message string + tags []string +} + +// initRepo creates a repository containing the given commits and returns its working directory. +func initRepo(t *testing.T, commits ...commit) string { + t.Helper() + + workdir := t.TempDir() + + repo, err := git.PlainInit(workdir, false) + assert.NoError(t, err) + + worktree, err := repo.Worktree() + assert.NoError(t, err) + + for i, c := range commits { + var hash plumbing.Hash + + hash, err = worktree.Commit(c.message, &git.CommitOptions{ + Author: &object.Signature{ + Name: "git-semver", + Email: "git-semver@example.com", + When: time.Now().Add(time.Duration(i) * time.Second), + }, + AllowEmptyCommits: true, + }) + assert.NoError(t, err) + + for _, tagName := range c.tags { + _, err = repo.CreateTag(tagName, hash, nil) + assert.NoError(t, err) + } + } + + return workdir +}