From 3788f0ce3eba865cb9c21481105bdcd026a5d6ef Mon Sep 17 00:00:00 2001 From: percybolmer Date: Thu, 31 Dec 2020 04:30:04 +0100 Subject: [PATCH] added my take on the code changes needed (#2) --- logcategories.go | 64 +++++++++++++++++++++++ main.go | 95 ++++++++++++++-------------------- repository.go | 130 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 58 deletions(-) create mode 100644 logcategories.go create mode 100644 repository.go diff --git a/logcategories.go b/logcategories.go new file mode 100644 index 0000000..da75611 --- /dev/null +++ b/logcategories.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "strings" +) + +// LogsByCategory - Type to hold logs by each's category +type LogsByCategory struct { + CI []string + FIX []string + REFACTOR []string + FEATURE []string + DOCS []string + OTHER []string +} + +// printCategory will output all items inside a Log slice and a title +func printCategory(output *strings.Builder, title string, logs []string) { + if len(logs) > 0 { + output.WriteString(fmt.Sprintf("\n\n## %s \n", title)) + for _, item := range logs { + output.WriteString(item + "\n") + } + } +} + +// GenerateMarkdown - Generate markdown output for the collected commits +func (logContainer *LogsByCategory) GenerateMarkdown() string { + var output strings.Builder + output.WriteString("# Changelog \n") + + printCategory(&output, "CI Changes", logContainer.CI) + printCategory(&output, "Fixes", logContainer.FIX) + printCategory(&output, "Performance Fixes", logContainer.REFACTOR) + printCategory(&output, "Feature Fixes", logContainer.FEATURE) + printCategory(&output, "Doc Updates", logContainer.DOCS) + printCategory(&output, "Other Changes", logContainer.OTHER) + + return output.String() +} + +// AddCommitLog will take a commitHash and a commitMessage and append them to their +// apropriate Slice +func (logContainer *LogsByCategory) AddCommitLog(commitHash, commitMessage string) { + message := fmt.Sprintf("%s - %s", commitHash, normalizeCommit(commitMessage)) + + switch { + case strings.Contains(commitMessage, "ci:"): + logContainer.CI = append(logContainer.CI, message) + case strings.Contains(commitMessage, "fix:"): + logContainer.FIX = append(logContainer.FIX, message) + case strings.Contains(commitMessage, "refactor:"): + logContainer.REFACTOR = append(logContainer.REFACTOR, message) + // Golang Switch allows multiple values in cases with , separation + case strings.Contains(commitMessage, "feat:"), strings.Contains(commitMessage, "feature:"): + logContainer.FEATURE = append(logContainer.FEATURE, message) + case strings.Contains(commitMessage, "docs:"): + logContainer.DOCS = append(logContainer.DOCS, message) + + default: + logContainer.OTHER = append(logContainer.OTHER, message) + } +} diff --git a/main.go b/main.go index c028499..41d7e0b 100644 --- a/main.go +++ b/main.go @@ -1,89 +1,68 @@ -// SPDX-License-Identifier: MIT - package main import ( + "flag" "fmt" "log" "os" "strings" - - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/object" ) -// ErrMessage - simple interface around error with a custom message -type ErrMessage struct { - message string - err error -} - func main() { - if len(os.Args) < 2 { - fmt.Println("Usage:\n > commitlog path/to/repo") - os.Exit(0) - } - - path := os.Args[1] - - err := CommitLog(path) + var path string + // Read user input + flag.StringVar(&path, "path", "", "A filepath to a folder containing a github repository") + // Parse Flags + flag.Parse() - if err.err != nil { - log.Fatal(err.message, err.err) + // Make sure user has inserted the needed flags + if path == "" { + flag.Usage() + os.Exit(0) } -} - -// CommitLog - Generate commit log -func CommitLog(path string) ErrMessage { - currentRepository := openRepository(path) - - baseCommitReference, err := currentRepository.Head() + repo, err := Open(path) if err != nil { - return ErrMessage{"Unable to get repository HEAD:", err} + log.Fatal(err) } - cIter, err := currentRepository.Log(&git.LogOptions{From: baseCommitReference.Hash()}) - + commits, err := repo.GetCommits() if err != nil { - return ErrMessage{"Unable to get repository commits:", err} + + log.Fatal(err) } - var commits []*object.Commit + logContainer := new(LogsByCategory) - err = cIter.ForEach(func(c *object.Commit) error { - commits = append(commits, c) - return nil - }) + // we no longer need to fetch latestTag here to compare tillLatest. - if err != nil { - return ErrMessage{"Error getting commits : ", err} - } + // itterate all commits and add them to the log based on hash and Message + for _, c := range commits { - logContainer := logsByCategory{} + logContainer.AddCommitLog(c.Hash.String(), c.Message) - for _, c := range commits { - normalizedHash := c.Hash.String() + " - " + normalizeCommit(c.Message) - switch strings.SplitN(strings.TrimSpace(c.Message), ":", 2)[0] { - case "ci": - logContainer.CI = append(logContainer.CI, normalizedHash) - case "fix": - logContainer.FIX = append(logContainer.FIX, normalizedHash) - case "refactor": - logContainer.REFACTOR = append(logContainer.REFACTOR, normalizedHash) - case "feat", "feature": - logContainer.FEATURE = append(logContainer.FEATURE, normalizedHash) - case "docs": - logContainer.DOCS = append(logContainer.DOCS, normalizedHash) - default: - logContainer.OTHER = append(logContainer.OTHER, normalizedHash) + nearestTag, err := repo.IsCommitNearest(c) + if err != nil { + log.Fatal(err) } - if isCommitToNearestTag(currentRepository, c) { + if nearestTag { break } } fmt.Println(logContainer.ToMarkdown()) - return ErrMessage{} + fmt.Println(logContainer.GenerateMarkdown()) + +} + +func normalizeCommit(commitMessage string) string { + var message string + for i, msg := range strings.Split(commitMessage, "\n") { + if i == 0 { + message = msg + break + } + } + return strings.TrimSuffix(message, "\n") } diff --git a/repository.go b/repository.go new file mode 100644 index 0000000..575538e --- /dev/null +++ b/repository.go @@ -0,0 +1,130 @@ +package main + +import ( + "fmt" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" +) + +// Repository is a struct that holds an Opened github repository and the reference +type Repository struct { + path string + repo *git.Repository + ref *plumbing.Reference +} + +// Open will open up a git repository +// and load the Head reference +func Open(path string) (*Repository, error) { + // Open the github repository + r, err := git.PlainOpen(path) + if err != nil { + return nil, fmt.Errorf("%v:%w", "Error opening Repository: ", err) + } + + // Grab the Git HEAD reference + ref, err := r.Head() + if err != nil { + return nil, fmt.Errorf("%v:%w", "Unable to get repository HEAD:", err) + } + return &Repository{ + path: path, + repo: r, + ref: ref, + }, nil +} + +// GetCommits will extract commit history from the git repository +func (r *Repository) GetCommits() ([]*object.Commit, error) { + // Get the Commit history + cIter, err := r.repo.Log(&git.LogOptions{From: r.ref.Hash()}) + + if err != nil { + return nil, fmt.Errorf("%v:%w", "Unable to get repository commits:", err) + } + + var commits []*object.Commit + + err = cIter.ForEach(func(c *object.Commit) error { + commits = append(commits, c) + return nil + }) + + if err != nil { + return nil, fmt.Errorf("%v:%w", "Error getting commits :", err) + } + return commits, nil +} + +// GetLatestTag is used to check the latestTag +// it will return a reference to the LatestTag and the PreviousTag +func (r *Repository) GetLatestTag() (*plumbing.Reference, *plumbing.Reference, error) { + tagRefs, err := r.repo.Tags() + if err != nil { + return nil, nil, err + } + + var latestTagCommit *object.Commit + var latestTagName *plumbing.Reference + var previousTag *plumbing.Reference + var previousTagReturn *plumbing.Reference + + err = tagRefs.ForEach(func(tagRef *plumbing.Reference) error { + revision := plumbing.Revision(tagRef.Name().String()) + + tagCommitHash, err := r.repo.ResolveRevision(revision) + if err != nil { + return err + } + + commit, err := r.repo.CommitObject(*tagCommitHash) + if err != nil { + return err + } + + if latestTagCommit == nil { + latestTagCommit = commit + latestTagName = tagRef + previousTagReturn = previousTag + } + + if commit.Committer.When.After(latestTagCommit.Committer.When) { + latestTagCommit = commit + latestTagName = tagRef + previousTagReturn = previousTag + } + + previousTag = tagRef + + return nil + }) + if err != nil { + return nil, nil, err + } + + return latestTagName, previousTagReturn, nil +} + +// IsCommitNearest will check if a commit tag Hash is equal to the current repository HEAD tag +// If the Hashes matches, it will return true +func (r *Repository) IsCommitNearest(commit *object.Commit) (bool, error) { + latestTag, previousTag, err := r.GetLatestTag() + + if err != nil { + return false, fmt.Errorf("%v:%w", "Couldn't get latest tag...", err) + } + + if latestTag != nil { + // Compare latest tag Hash with the repository HEAD hash + // Hash() returns a Slice which can be compared without converting to string + // Noticed by /OfficialTomCruise on reddit comments + if latestTag.Hash() == r.ref.Hash() { + return true, nil + } + return previousTag.Hash() == commit.Hash, nil + + } + return false, nil +}