From 3788f0ce3eba865cb9c21481105bdcd026a5d6ef Mon Sep 17 00:00:00 2001
From: percybolmer <percybolmer1@gmail.com>
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
+}
