diff --git a/commandline.go b/commandline.go index 364a74d58e0133391ab57ee1944fd6c4bc76d124..c9baea3b1a0151c7db4a23db5d95923d2d5146d8 100644 --- a/commandline.go +++ b/commandline.go @@ -6,6 +6,8 @@ import ( "time" "github.com/jessevdk/go-flags" + + "github.com/go-git/go-git/v5" ) var ( @@ -65,12 +67,16 @@ func increaseMajor() (string, error) { return next.String(), nil } +var gitRepo *git.Repository + func executeCommand() { arguments = new(commandLineOptions) p := flags.NewParser(arguments, flags.Default) - _, err := p.Parse() + var err error + + _, err = p.Parse() if err != nil { os.Exit(-1) } @@ -83,8 +89,17 @@ func executeCommand() { var newVersion string command := activeCommand.Name + + if arguments.Git || command == "auto" { + gitRepo, err = git.PlainOpen(".") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + } + if command == "auto" { - updateType, err := GetCommitType(".") + updateType, err := GetCommitType(gitRepo) if err != nil { fmt.Println(err) os.Exit(-1) @@ -108,13 +123,13 @@ func executeCommand() { switch command { case "print": if arguments.Git { - version, err := getLatestSemanticTag(".") + version, err := getLatestSemanticTag(gitRepo) if err != nil { fmt.Println(err) os.Exit(-1) } - fmt.Printf("%s", version) + fmt.Printf("%s", version.String()) os.Exit(0) } case "date": diff --git a/git.go b/git.go index 84c29d9c192678e1e652d8e9d437043b3855d2e5..d6a19c313346de76b2bce7073a49100933a9ca48 100644 --- a/git.go +++ b/git.go @@ -2,17 +2,51 @@ package main import ( "fmt" - "strings" - "regexp" "sort" "strconv" + "strings" "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/go-git/go-git/v5/plumbing/storer" ) +var versionRegex = regexp.MustCompile(`^v?(\d+)\.(\d+)\.(\d+).*$`) + +type SemanticVersion struct { + Major int + Minor int + Patch int + Tag string +} + +func (v SemanticVersion) String() string { + return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) +} + +func ParseSemanticVersion(tag string) SemanticVersion { + matches := versionRegex.FindStringSubmatch(tag) + major, _ := strconv.Atoi(matches[1]) + minor, _ := strconv.Atoi(matches[2]) + patch, _ := strconv.Atoi(matches[3]) + return SemanticVersion{major, minor, patch, tag} +} + +func (v SemanticVersion) IsLessThan(w SemanticVersion) bool { + if v.Major != w.Major { + return v.Major < w.Major + } + if v.Minor != w.Minor { + return v.Minor < w.Minor + } + if v.Patch != w.Patch { + return v.Patch < w.Patch + } + return false +} + type CommitType int const ( @@ -22,13 +56,14 @@ const ( FixCommit ) -func GetCommitType(path string) (CommitType, error) { - r, err := git.PlainOpen(path) +func GetCommitType(r *git.Repository) (CommitType, error) { + + latestTag, err := getLatestSemanticTag(r) if err != nil { - return OtherCommit, fmt.Errorf("failed to open repository: %v", err) + return OtherCommit, err } - tagCommit, err := getLatestTagCommit(r) + tagCommit, err := getTagCommit(r, latestTag.Tag) if err != nil { return OtherCommit, err } @@ -41,109 +76,62 @@ func GetCommitType(path string) (CommitType, error) { return commitType, nil } -func getLatestSemanticTag(path string) (string, error) { - r, err := git.PlainOpen(path) - if err != nil { - return "", fmt.Errorf("failed to open repository: %v", err) - } - +func getLatestSemanticTag(r *git.Repository) (SemanticVersion, error) { tagList, err := getSemanticTags(r) if err != nil { - return "", err + return SemanticVersion{}, err } if len(tagList) == 0 { - return "", fmt.Errorf("no semantic tags found") + return SemanticVersion{}, fmt.Errorf("no semantic tags found") } - sort.Slice(tagList, func(i, j int) bool { - return compareSemanticVersions(tagList[i], tagList[j]) - }) - return tagList[len(tagList)-1], nil } -func getSemanticTags(r *git.Repository) ([]string, error) { +func getTagCommit(r *git.Repository, tag string) (*object.Commit, error) { tags, err := r.Tags() if err != nil { return nil, fmt.Errorf("failed to get tags: %v", err) } - var tagList []string - err = tags.ForEach(func(tag *plumbing.Reference) error { - tagName := tag.Name().Short() - if isSemanticVersion(tagName) { - tagList = append(tagList, tagName) + var tagCommit *object.Commit + err = tags.ForEach(func(t *plumbing.Reference) error { + if t.Name().Short() == tag { + tagObj, err := r.TagObject(t.Hash()) + if err != nil { + return err + } + tagCommit, err = tagObj.Commit() + if err != nil { + return err + } + return storer.ErrStop // stop iteration } return nil }) - if err != nil { return nil, fmt.Errorf("failed to iterate over tags: %v", err) } - - return tagList, nil -} - -func isSemanticVersion(tagName string) bool { - versionRegex := regexp.MustCompile(`^v?\d+\.\d+\.\d+.*$`) - return versionRegex.MatchString(tagName) -} - -func compareSemanticVersions(a, b string) bool { - // Remove leading "v" if present - a = strings.TrimPrefix(a, "v") - b = strings.TrimPrefix(b, "v") - - aParts := strings.Split(a, ".") - bParts := strings.Split(b, ".") - - for i := 0; i < len(aParts) && i < len(bParts); i++ { - aNum, _ := strconv.Atoi(aParts[i]) - bNum, _ := strconv.Atoi(bParts[i]) - - if aNum != bNum { - return aNum < bNum - } + if tagCommit == nil { + return nil, fmt.Errorf("tag commit not found in commit log") } - return len(aParts) < len(bParts) + return tagCommit, nil } -func getLatestTagCommit(r *git.Repository) (*object.Commit, error) { +func getSemanticTags(r *git.Repository) ([]SemanticVersion, error) { tags, err := r.Tags() if err != nil { return nil, fmt.Errorf("failed to get tags: %v", err) } - var mostRecentTag *plumbing.Reference - var mostRecentCommit *object.Commit - + var tagList []SemanticVersion err = tags.ForEach(func(tag *plumbing.Reference) error { - obj, err := r.TagObject(tag.Hash()) - if err != nil { - if err == plumbing.ErrObjectNotFound { - // If the tag object is not found, it's likely a lightweight tag pointing directly to a commit - commit, err := r.CommitObject(tag.Hash()) - if err != nil { - return err - } - obj = &object.Tag{Tagger: commit.Author, Message: commit.Message} - } else { - return err - } - } - - commit, err := obj.Commit() - if err != nil { - return err - } - - if mostRecentTag == nil || commit.Committer.When.After(mostRecentCommit.Committer.When) { - mostRecentTag = tag - mostRecentCommit = commit + tagName := tag.Name().Short() + if versionRegex.MatchString(tagName) { + tagList = append(tagList, ParseSemanticVersion(tagName)) } - return nil }) @@ -151,7 +139,11 @@ func getLatestTagCommit(r *git.Repository) (*object.Commit, error) { return nil, fmt.Errorf("failed to iterate over tags: %v", err) } - return mostRecentCommit, nil + sort.Slice(tagList, func(i, j int) bool { + return tagList[i].IsLessThan(tagList[j]) + }) + + return tagList, nil } func getCommitTypeSinceTag(r *git.Repository, tagCommit *object.Commit) (CommitType, error) { @@ -196,19 +188,14 @@ func getCommitTypeSinceTag(r *git.Repository, tagCommit *object.Commit) (CommitT } func containsBreakingChangeFooter(message string) bool { - // Split the commit message by newline to get the footer text lines := strings.Split(message, "\n") - - // Check last lines for the "BREAKING CHANGE" footer for i := len(lines) - 1; i >= 0; i-- { if strings.TrimSpace(lines[i]) == "" { - // Stop checking when an empty line (beginning of the commit body) is reached break } if strings.Contains(strings.ToLower(lines[i]), "breaking change:") { return true } } - return false }