Refactor the code to use more modular and testable approach.

This commit is contained in:
2025-02-25 19:11:19 +00:00
parent 5964da3cef
commit 942e648d56
18 changed files with 1719 additions and 595 deletions
+77 -360
View File
@@ -19,414 +19,131 @@ package cmd
import (
"fmt"
"net/url"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
git "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/transport/http"
"github.com/lithammer/fuzzysearch/fuzzy"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"github.com/lukaszraczylo/pandati"
"github.com/spf13/viper"
"github.com/lukaszraczylo/semver-generator/cmd/utils"
)
var (
err error
repo *Setup
PKG_VERSION string
logger *libpack_logging.Logger
)
type Wording struct {
Patch []string
Minor []string
Major []string
Release []string
}
type Force struct {
Commit string
Patch int
Minor int
Major int
Existing bool
Strict bool
}
type SemVer struct {
Patch int
Minor int
Major int
Release int
EnableReleaseCandidate bool
}
// Setup represents the application setup
type Setup struct {
RepositoryHandler *git.Repository
RepositoryName string
RepositoryBranch string
RepositoryLocalPath string
LocalConfigFile string
Wording Wording
Commits []CommitDetails
Tags []TagDetails
Force Force
Semver SemVer
Generate bool
UseLocal bool
Blacklist []string
GitRepo utils.GitRepository
Config *utils.Config
Semver utils.SemVer
}
type CommitDetails struct {
Timestamp time.Time
Hash string
Author string
Message string
}
type TagDetails struct {
Name string
Hash string
}
func checkMatches(content []string, targets []string) bool {
contentStr := strings.Join(content, " ")
// Initialize the fuzzy search function in the utils package
func init() {
utils.InitLogger(false) // Will be updated in main based on debug flag
// First check if any target matches
hasMatch := false
for _, tgt := range targets {
r := fuzzy.FindNormalizedFold(tgt, content)
if len(r) > 0 {
hasMatch = true
logger.Debug(&libpack_logging.LogMessage{
Message: "Found match",
Pairs: map[string]interface{}{"target": tgt, "match": strings.Join(r, ","), "content": contentStr},
})
break
}
}
// If we have a match, check against blacklist
if hasMatch {
for _, blacklistTerm := range repo.Blacklist {
if strings.Contains(strings.ToLower(contentStr), strings.ToLower(blacklistTerm)) {
logger.Debug(&libpack_logging.LogMessage{
Message: "Blacklisted term detected, ignoring commit",
Pairs: map[string]interface{}{"content": contentStr, "blacklist_term": blacklistTerm},
})
return false
}
}
}
return hasMatch
// Set the fuzzy search function
utils.FuzzyFind = fuzzy.FindNormalizedFold
}
var extractNumber = regexp.MustCompile("[0-9]+")
func parseExistingSemver(tagName string, currentSemver SemVer) (semanticVersion SemVer) {
logger.Debug(&libpack_logging.LogMessage{
Message: "Parsing existing semver",
Pairs: map[string]interface{}{"tag": tagName},
})
tagNameParts := strings.Split(tagName, ".")
if len(tagNameParts) < 3 {
logger.Debug(&libpack_logging.LogMessage{
Message: "Unable to parse incompatible semver ( non x.y.z )",
Pairs: map[string]interface{}{"tag": tagName},
})
return currentSemver
}
semanticVersion.Major, _ = strconv.Atoi(extractNumber.FindAllString(tagNameParts[0], -1)[0])
semanticVersion.Minor, _ = strconv.Atoi(extractNumber.FindAllString(tagNameParts[1], -1)[0])
semanticVersion.Patch, _ = strconv.Atoi(extractNumber.FindAllString(tagNameParts[2], -1)[0])
if len(tagNameParts) > 3 {
semanticVersion.Release, _ = strconv.Atoi(extractNumber.FindAllString(tagNameParts[3], -1)[0])
semanticVersion.EnableReleaseCandidate = true
}
return
}
func (s *Setup) CalculateSemver() SemVer {
for _, commit := range s.Commits {
if params.varExisting || s.Force.Existing {
for _, tagHash := range s.Tags {
if commit.Hash == tagHash.Hash {
logger.Debug(&libpack_logging.LogMessage{
Message: "Found existing tag",
Pairs: map[string]interface{}{"tag": tagHash.Name, "commit": strings.TrimSuffix(commit.Message, "\n")},
})
s.Semver = parseExistingSemver(tagHash.Name, s.Semver)
continue
}
}
}
if !params.varStrict && !s.Force.Strict {
s.Semver.Patch++
logger.Debug(&libpack_logging.LogMessage{
Message: "Incrementing patch (DEFAULT)",
Pairs: map[string]interface{}{"commit": strings.TrimSuffix(commit.Message, "\n"), "semver": s.getSemver()},
})
}
commitSlice := strings.Fields(commit.Message)
matchPatch := checkMatches(commitSlice, s.Wording.Patch)
matchMinor := checkMatches(commitSlice, s.Wording.Minor)
matchMajor := checkMatches(commitSlice, s.Wording.Major)
matchReleaseCandidate := checkMatches(commitSlice, s.Wording.Release)
if matchMajor {
s.Semver.Major++
s.Semver.Minor = 0
s.Semver.Patch = 1
s.Semver.EnableReleaseCandidate = false
s.Semver.Release = 0
logger.Debug(&libpack_logging.LogMessage{
Message: "Incrementing major (WORDING)",
Pairs: map[string]interface{}{"commit": strings.TrimSuffix(commit.Message, "\n"), "semver": s.getSemver()},
})
continue
}
if matchMinor {
s.Semver.Minor++
s.Semver.Patch = 1
s.Semver.EnableReleaseCandidate = false
s.Semver.Release = 0
logger.Debug(&libpack_logging.LogMessage{
Message: "Incrementing minor (WORDING)",
Pairs: map[string]interface{}{"commit": strings.TrimSuffix(commit.Message, "\n"), "semver": s.getSemver()},
})
continue
}
if matchReleaseCandidate {
s.Semver.Release++
s.Semver.Patch = 1
s.Semver.EnableReleaseCandidate = true
logger.Debug(&libpack_logging.LogMessage{
Message: "Incrementing release candidate (WORDING)",
Pairs: map[string]interface{}{"commit": strings.TrimSuffix(commit.Message, "\n"), "semver": s.getSemver()},
})
continue
}
if matchPatch {
s.Semver.Patch++
logger.Debug(&libpack_logging.LogMessage{
Message: "Incrementing patch (WORDING)",
Pairs: map[string]interface{}{"commit": strings.TrimSuffix(commit.Message, "\n"), "semver": s.getSemver()},
})
continue
}
}
return s.Semver
}
func (s *Setup) ListExistingTags() {
logger.Debug(&libpack_logging.LogMessage{
Message: "Listing existing tags",
})
refs, err := s.RepositoryHandler.Tags()
if err != nil {
panic(err)
}
if err := refs.ForEach(func(ref *plumbing.Reference) error {
s.Tags = append(s.Tags, TagDetails{Name: ref.Name().Short(), Hash: ref.Hash().String()})
logger.Debug(&libpack_logging.LogMessage{
Message: "Found tag",
Pairs: map[string]interface{}{"tag": ref.Name().Short(), "hash": ref.Hash().String()},
})
return nil
}); err != nil {
panic(err)
}
}
func (s *Setup) ListCommits() ([]CommitDetails, error) {
var ref *plumbing.Reference
var err error
ref, err = s.RepositoryHandler.Head()
if err != nil {
return []CommitDetails{}, err
}
commitsList, err := s.RepositoryHandler.Log(&git.LogOptions{From: ref.Hash()})
if err != nil {
return []CommitDetails{}, err
}
var tmpResults []CommitDetails
commitsList.ForEach(func(c *object.Commit) error {
tmpResults = append(tmpResults, CommitDetails{Hash: c.Hash.String(), Author: c.Author.String(), Message: c.Message, Timestamp: c.Author.When})
sort.Slice(tmpResults, func(i, j int) bool { return tmpResults[i].Timestamp.Unix() < tmpResults[j].Timestamp.Unix() })
return nil
})
logger.Debug(&libpack_logging.LogMessage{
Message: "Listing commits",
Pairs: map[string]interface{}{"commits": tmpResults},
})
for commitId, cmt := range tmpResults {
if s.Force.Commit != "" && cmt.Hash == s.Force.Commit {
logger.Debug(&libpack_logging.LogMessage{
Message: "Found commit match",
Pairs: map[string]interface{}{"commit": cmt.Hash, "index": commitId},
})
s.Commits = tmpResults[commitId:]
break
} else {
s.Commits = tmpResults
}
}
logger.Debug(&libpack_logging.LogMessage{
Message: "Commits after cut",
Pairs: map[string]interface{}{"commits": s.Commits},
})
return s.Commits, err
}
func (s *Setup) Prepare() error {
if !repo.UseLocal {
u, err := url.Parse(s.RepositoryName)
if err != nil {
logger.Error(&libpack_logging.LogMessage{
Message: "Unable to parse repository URL",
Pairs: map[string]interface{}{"error": err.Error(), "url": s.RepositoryName},
})
return err
}
s.RepositoryLocalPath = fmt.Sprintf("/tmp/semver/%s/%s", u.Path, s.RepositoryBranch)
os.RemoveAll(s.RepositoryLocalPath)
s.RepositoryHandler, err = git.PlainClone(s.RepositoryLocalPath, false, &git.CloneOptions{
URL: s.RepositoryName,
ReferenceName: plumbing.NewBranchReferenceName(s.RepositoryBranch),
SingleBranch: true,
Auth: &http.BasicAuth{
Username: os.Getenv("GITHUB_USERNAME"),
Password: os.Getenv("GITHUB_TOKEN"),
},
Tags: git.AllTags,
})
if err != nil {
logger.Error(&libpack_logging.LogMessage{
Message: "Unable to clone repository",
Pairs: map[string]interface{}{"error": err.Error(), "url": s.RepositoryName},
})
return err
}
} else {
s.RepositoryLocalPath = "./"
s.RepositoryHandler, err = git.PlainOpen(s.RepositoryLocalPath)
if err != nil {
logger.Error(&libpack_logging.LogMessage{
Message: "Unable to open local repository",
Pairs: map[string]interface{}{"error": err.Error(), "path": s.RepositoryLocalPath},
})
return err
}
}
os.Chdir(s.RepositoryLocalPath)
return err
}
func (s *Setup) ForcedVersioning() {
if !pandati.IsZero(s.Force.Major) {
logger.Debug(&libpack_logging.LogMessage{
Message: "Forced versioning (MAJOR)",
Pairs: map[string]interface{}{"major": s.Force.Major},
})
s.Semver.Major = s.Force.Major
}
if !pandati.IsZero(s.Force.Minor) {
logger.Debug(&libpack_logging.LogMessage{
Message: "Forced versioning (MINOR)",
Pairs: map[string]interface{}{"minor": s.Force.Minor},
})
s.Semver.Minor = s.Force.Minor
}
if !pandati.IsZero(s.Force.Patch) {
logger.Debug(&libpack_logging.LogMessage{
Message: "Forced versioning (PATCH)",
Pairs: map[string]interface{}{"patch": s.Force.Minor},
})
s.Semver.Patch = s.Force.Patch
}
}
func (s *Setup) ReadConfig(file string) error {
viper.SetConfigFile(file)
err := viper.ReadInConfig()
if err != nil {
err = fmt.Errorf("fatal error config file: %s", err)
return err
}
viper.UnmarshalKey("wording", &s.Wording)
viper.UnmarshalKey("force", &s.Force)
viper.UnmarshalKey("blacklist", &s.Blacklist)
return err
}
func (s *Setup) getSemver() (semverReturned string) {
semverReturned = fmt.Sprintf("%d.%d.%d", s.Semver.Major, s.Semver.Minor, s.Semver.Patch)
if s.Semver.EnableReleaseCandidate {
semverReturned = fmt.Sprintf("%s-rc.%d", semverReturned, s.Semver.Release)
}
return semverReturned
// getSemver returns the semantic version as a string
func (s *Setup) getSemver() string {
return utils.FormatSemver(s.Semver)
}
// main is the entry point for the application
func main() {
logger = libpack_logging.New()
// Initialize logger
if params.varDebug {
logger.SetOutput(os.Stdout).SetMinLogLevel(libpack_logging.LEVEL_DEBUG)
utils.InitLogger(true)
} else {
utils.InitLogger(false)
}
// Show version if requested
if params.varShowVersion {
var outdatedMsg string
latestRelease, latestRelaseOk := checkLatestRelease()
if PKG_VERSION != latestRelease && latestRelaseOk {
latestRelease, latestReleaseOk := utils.CheckLatestRelease()
if PKG_VERSION != latestRelease && latestReleaseOk {
outdatedMsg = fmt.Sprintf("(Latest available: %s)", latestRelease)
}
logger.Info(&libpack_logging.LogMessage{
Message: "semver-gen",
Pairs: map[string]interface{}{"version": PKG_VERSION, "outdated": outdatedMsg},
utils.Info("semver-gen", map[string]interface{}{
"version": PKG_VERSION,
"outdated": outdatedMsg,
})
if outdatedMsg != "" {
logger.Info(&libpack_logging.LogMessage{
Message: "semver-gen",
Pairs: map[string]interface{}{"message": "You can update automatically with: semver-gen -u"},
utils.Info("semver-gen", map[string]interface{}{
"message": "You can update automatically with: semver-gen -u",
})
}
return
}
// Update package if requested
if params.varUpdate {
updatePackage()
utils.UpdatePackage()
return
}
// Generate semantic version
if repo.Generate || params.varGenerateInTest {
err := repo.ReadConfig(repo.LocalConfigFile)
// Read configuration
config, err := utils.ReadConfig(repo.LocalConfigFile)
if err != nil {
logger.Error(&libpack_logging.LogMessage{
Message: "Unable to find config file semver.yaml. Using defaults and flags.",
Pairs: map[string]interface{}{"file": repo.LocalConfigFile},
utils.Error("Unable to find config file. Using defaults and flags.", map[string]interface{}{
"file": repo.LocalConfigFile,
})
}
err = repo.Prepare()
repo.Config = config
// Setup git repository
gitRepo := utils.GitRepository{
Name: repo.RepositoryName,
Branch: repo.RepositoryBranch,
UseLocal: repo.UseLocal,
StartCommit: repo.Config.Force.Commit,
}
repo.GitRepo = gitRepo
// Prepare repository
err = utils.PrepareRepository(&repo.GitRepo)
if err != nil {
logger.Critical(&libpack_logging.LogMessage{
Message: "Unable to prepare repository",
Pairs: map[string]interface{}{"error": err.Error()},
utils.Critical("Unable to prepare repository", map[string]interface{}{
"error": err.Error(),
})
os.Exit(1)
}
repo.ListCommits()
if params.varExisting || repo.Force.Existing {
repo.ListExistingTags()
// List commits
utils.ListCommits(&repo.GitRepo)
// List existing tags if needed
if params.varExisting || repo.Config.Force.Existing {
utils.ListExistingTags(&repo.GitRepo)
}
repo.ForcedVersioning()
repo.CalculateSemver()
// Apply forced versioning
utils.ApplyForcedVersioning(repo.Config.Force, &repo.Semver)
// Calculate semantic version
repo.Semver = utils.CalculateSemver(
repo.GitRepo.Commits,
repo.GitRepo.Tags,
repo.Config.Wording,
repo.Config.Blacklist,
repo.Semver,
params.varExisting || repo.Config.Force.Existing,
params.varStrict || repo.Config.Force.Strict,
)
// Print semantic version
fmt.Println("SEMVER", repo.getSemver())
}
}