diff --git a/commandline.go b/commandline.go index 10e17d95be9ce9e37f51e870e92744fac16ec0e5..f9041efa10cc97c4775016dcaeea167f8b8f956b 100644 --- a/commandline.go +++ b/commandline.go @@ -28,6 +28,8 @@ type commandLineOptions struct { } `command:"init" description:"init new version"` Date struct { } `command:"date" description:"print the current date and time in the format YYYYMMDDHHMMSS"` + Auto struct { + } `command:"auto" description:"check the git repository and increase the version if necessary. Implies --git"` } func increasePatch() (string, error) { @@ -78,8 +80,29 @@ func executeCommand() { } var newVersion string + command := activeCommand.Name + if command == "auto" { + updateType, err := GetCommitType(".") + if err != nil { + fmt.Println(err) + os.Exit(-1) + } + + switch updateType { + case BreakingCommit: + command = "major" + case FeatCommit: + command = "minor" + case FixCommit: + command = "patch" + case OtherCommit: + fmt.Println("No changes found") + os.Exit(0) + } - switch activeCommand.Name { + } + + switch command { case "date": currentTime := time.Now() build = currentTime.Format("20060102150405") @@ -98,6 +121,7 @@ func executeCommand() { newVersion, err = increaseMinor() case "patch": newVersion, err = increasePatch() + } if err != nil { diff --git a/git.go b/git.go new file mode 100644 index 0000000000000000000000000000000000000000..7cc3dfe2952101717611a513996bd317ebccc18f --- /dev/null +++ b/git.go @@ -0,0 +1,127 @@ +package main + +import ( + "fmt" + "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" +) + +type CommitType int + +const ( + FixCommit CommitType = iota + FeatCommit + BreakingCommit + OtherCommit +) + +func GetCommitType(path string) (CommitType, error) { + r, err := git.PlainOpen(path) + if err != nil { + return OtherCommit, fmt.Errorf("failed to open repository: %v", err) + } + + tagCommit, err := getLatestTagCommit(r) + if err != nil { + return OtherCommit, err + } + + commitType, err := getCommitTypeSinceTag(r, tagCommit) + if err != nil { + return OtherCommit, err + } + + return commitType, nil +} + +func getLatestTagCommit(r *git.Repository) (*object.Commit, error) { + tags, err := r.Tags() + if err != nil { + return nil, fmt.Errorf("failed to get tags: %v", err) + } + + var ( + mostRecentTag *plumbing.Reference + mostRecentCommit *object.Commit + ) + + 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 + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to iterate over tags: %v", err) + } + + return mostRecentCommit, nil +} + +func getCommitTypeSinceTag(r *git.Repository, tagCommit *object.Commit) (CommitType, error) { + commitLog, err := r.Log(&git.LogOptions{From: tagCommit.Hash}) + if err != nil { + return OtherCommit, fmt.Errorf("failed to get commit log: %v", err) + } + + var commitType CommitType + err = commitLog.ForEach(func(commit *object.Commit) error { + if strings.HasPrefix(commit.Message, "feat:") { + commitType = FeatCommit + } else if strings.HasPrefix(commit.Message, "fix:") { + commitType = FixCommit + } else if containsBreakingChangeFooter(commit.Message) { + commitType = BreakingCommit + } else { + commitType = OtherCommit + } + return nil + }) + if err != nil { + return OtherCommit, fmt.Errorf("failed to iterate over commit log: %v", err) + } + + return commitType, nil +} + +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 +}