diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 1323e56..bf3dac6 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -15,7 +15,7 @@ jobs: - name: Generate Change Log id: generate_log run: | - curl -sf https://gobinaries.com/barelyhuman/commitlog | sh + curl -sSL https://bina.egoist.sh/barelyhuman/commitlog | sh commitlog > CHANGELOG.md - uses: ncipollo/release-action@v1 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..93d4b6c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Test + +on: + - push + - pull_request + +jobs: + build_and_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... diff --git a/cmd/commitlog/commitlog.go b/cmd/commitlog/commitlog.go index b13b8e8..350708f 100644 --- a/cmd/commitlog/commitlog.go +++ b/cmd/commitlog/commitlog.go @@ -34,7 +34,9 @@ func Run(args []string) { log.Fatalln(err) } - changelog, clogErr := clog.CommitLog(*repoPath, *startCommit, *endCommit, *inclusionFlags, *skipClassification) + currentRepository := clog.OpenRepository(*repoPath) + + changelog, clogErr := clog.CommitLog(currentRepository, *startCommit, *endCommit, *inclusionFlags, *skipClassification) if clogErr.Err != nil { log.Fatal(clogErr.Message, clogErr.Err) diff --git a/go.mod b/go.mod index 3e2d61b..e12ded6 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,6 @@ go 1.13 require ( github.com/AlecAivazis/survey/v2 v2.2.8 github.com/fatih/color v1.12.0 + github.com/go-git/go-billy/v5 v5.0.0 // indirect github.com/go-git/go-git/v5 v5.2.0 ) diff --git a/log/gitutils.go b/log/gitutils.go index 037ae88..0981e78 100644 --- a/log/gitutils.go +++ b/log/gitutils.go @@ -11,105 +11,20 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" ) -var ( - latestTag *plumbing.Reference - previousTag *plumbing.Reference -) - -// GetLatestTagFromRepository - Get the latest Tag reference from the repo -func GetLatestTagFromRepository(repository *git.Repository) (*plumbing.Reference, *plumbing.Reference, error) { - tagRefs, err := repository.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()) - - tagCommitHash, err := repository.ResolveRevision(revision) - if err != nil { - return err - } - - commit, err := repository.CommitObject(*tagCommitHash) +func IsHashATag(currentRepository *git.Repository, hash plumbing.Hash) bool { + isTag := false + tags, _ := currentRepository.Tags() + tags.ForEach(func(tagRef *plumbing.Reference) error { + revHash, err := currentRepository.ResolveRevision(plumbing.Revision(tagRef.Name())) 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 + if *revHash == hash { + isTag = true } - - previousTag = tagRef - return nil }) - - if err != nil { - return nil, nil, err - } - - return latestTagName, previousTagReturn, nil -} - -// isCommitToNearestTag - go through git revisions to find the latest tag and the nearest next tag -func isCommitToNearestTag(repo *git.Repository, commit *object.Commit) bool { - if latestTag == nil || previousTag == nil { - var err error - latestTag, previousTag, err = GetLatestTagFromRepository(repo) - if err != nil { - log.Fatal("Error getting latest tags from repository") - } - } - - ref, err := repo.Head() - - if err != nil { - log.Fatal("Unable to get repository HEAD:", err) - } - - tillLatest := latestTag != nil && latestTag.Hash().String() != ref.Hash().String() - - if err != nil { - log.Fatal("Couldn't get latest tag...", err) - } - - if latestTag == nil || previousTag == nil { - return false - } - - // Ignore errors as these are to be optionally checked - followedTagReferenceLatest, err := repo.ResolveRevision(plumbing.Revision(latestTag.Name())) - - if err != nil { - log.Fatal("Failed to get referenced commit hash for latestTag's revision") - } - - followedTagReferencePrev, err := repo.ResolveRevision(plumbing.Revision(previousTag.Name())) - - if err != nil { - log.Fatal("Failed to get referenced commit hash for previous's revision") - } - - if tillLatest { - return *followedTagReferenceLatest == commit.Hash - } - - return *followedTagReferencePrev == commit.Hash - + return isTag } // normalizeCommit - reduces the commit message to the first line and ignore the description text of the commit diff --git a/log/log.go b/log/log.go index dfbf9a2..1ea962d 100644 --- a/log/log.go +++ b/log/log.go @@ -133,29 +133,26 @@ func (container *logContainer) canAddToContainer(skip bool) bool { return true } -/* - TODO: - - [] if the current start is also a tag then get data till prev tag - - [] add in option to include the description, if the commit has a description -*/ - // CommitLog - Generate commit log -func CommitLog(path string, startCommitString string, endCommitString string, inclusionFlags string, skipClassification bool) (string, ErrMessage) { - currentRepository := OpenRepository(path) +func CommitLog(currentRepository *git.Repository, startCommitString string, endCommitString string, inclusionFlags string, skipClassification bool) (string, ErrMessage) { baseCommitReference, err := currentRepository.Head() var startHash, endHash *object.Commit var cIter object.CommitIter if err != nil { - return "", ErrMessage{"Unable to get repository HEAD:", err} + return "", ErrMessage{"Unable to get repository HEAD, are you sure you are in a git repository? Error:", err} } startHash = GetCommitFromString(startCommitString, currentRepository) endHash = GetCommitFromString(endCommitString, currentRepository) + isHeadTag := false if startHash != nil { cIter, err = currentRepository.Log(&git.LogOptions{From: startHash.Hash}) } else { + if IsHashATag(currentRepository, baseCommitReference.Hash()) { + isHeadTag = true + } cIter, err = currentRepository.Log(&git.LogOptions{From: baseCommitReference.Hash()}) } @@ -165,8 +162,26 @@ func CommitLog(path string, startCommitString string, endCommitString string, in var commits []*object.Commit + var latestTag *object.Commit + var previousTag *object.Commit + tagAssignment := 0 + err = cIter.ForEach(func(c *object.Commit) error { commits = append(commits, c) + if isHeadTag && tagAssignment == 0 && latestTag == nil && c.Hash == baseCommitReference.Hash() { + latestTag = c + tagAssignment += 1 + + } else if IsHashATag(currentRepository, c.Hash) { + if latestTag == nil && tagAssignment == 0 { + latestTag = c + tagAssignment += 1 + } + if previousTag == nil && tagAssignment == 1 { + previousTag = c + tagAssignment += 1 + } + } return nil }) @@ -195,15 +210,15 @@ func CommitLog(path string, startCommitString string, endCommitString string, in key = strings.SplitN(strings.TrimSpace(key), ":", 2)[0] normalizedHash := c.Hash.String() + " - " + normalizeCommit(c.Message, scopedKey) - logContainer.AddCommit(key, normalizedHash, skipClassification) - - if endHash == nil && isCommitToNearestTag(currentRepository, c) { + if endHash == nil && previousTag != nil && previousTag.Hash == c.Hash { break } else if endHash != nil && c.Hash == endHash.Hash { break } - } + logContainer.AddCommit(key, normalizedHash, skipClassification) + + } return logContainer.ToMarkdown(skipClassification), ErrMessage{} } diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 0000000..a502cbc --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,176 @@ +package commitlog + +import ( + "log" + "strings" + "testing" + "time" + + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage/memory" +) + +var testCommits []string = []string{ + "fix: fix commit", + "feat: feat commit", + "docs: doc update commit", + "chore: chore commit", + "other commit", +} + +var expectedCommits []string + +func bail(err error) { + if err != nil { + log.Panic(err) + } +} + +func setup() *git.Repository { + var fs = memfs.New() + repo, _ := git.Init(memory.NewStorage(), fs) + wt, err := repo.Worktree() + bail(err) + + for _, testCommitMsg := range testCommits { + commit, err := wt.Commit(testCommitMsg, &git.CommitOptions{ + Author: &object.Signature{ + Name: "Reaper", + Email: "ahoy@barelyhuman.dev", + When: time.Now(), + }, + }) + bail(err) + expectedCommits = append(expectedCommits, commit.String()) + } + + return repo +} + +var repo *git.Repository = setup() + +func TestCommitLogDefault(t *testing.T) { + + log, _ := CommitLog(repo, "", "", SupportedKeys, false) + if log == "" { + t.Fail() + } + + for _, commit := range expectedCommits { + if !strings.Contains(log, commit) { + t.Fail() + } + } + + t.Log(log) + +} + +func TestCommitLogSkipped(t *testing.T) { + + log, _ := CommitLog(repo, "", "", SupportedKeys, true) + if log == "" { + t.Fail() + } + + for _, commit := range expectedCommits { + // Shouldn't contain classification headings + if strings.Contains(log, "##") { + t.Fail() + } + + if !strings.Contains(log, commit) { + t.Fail() + } + } + + t.Log(log) +} + +func TestCommitLogInclusions(t *testing.T) { + + // include only feature commits + log, _ := CommitLog(repo, "", "", "feat", true) + if log == "" { + t.Fail() + } + + ignoredHeadings := []string{ + "## Fixes", + "## Performance", + "## CI", + "## Docs", + "## Chores", + "## Tests", + "## Other Changes", + } + + for _, heading := range ignoredHeadings { + if strings.Contains(log, heading) { + t.Fail() + } + } + + t.Log(log) +} + +func TestCommitLogStartHash(t *testing.T) { + + expectedCommitsLen := len(expectedCommits) + startCommitHash := expectedCommits[expectedCommitsLen-2] + lastCommit := expectedCommits[expectedCommitsLen-1] + acceptedCommitHashes := expectedCommits[0 : expectedCommitsLen-1] + + t.Log("Commits: ", expectedCommits) + t.Log("Start At:", startCommitHash) + + log, _ := CommitLog(repo, startCommitHash, "", SupportedKeys, true) + if log == "" { + t.Fail() + } + + // should have all commits except the last one + if strings.Contains(log, lastCommit) { + t.Fail() + } + + for _, commitHash := range acceptedCommitHashes { + if !strings.Contains(log, commitHash) { + t.Log("Failed at:", commitHash) + t.Fail() + } + } + + t.Log("\n", log) +} + +func TestCommitLogEndHash(t *testing.T) { + + endCommitHash := expectedCommits[1] + firstCommit := expectedCommits[0] + acceptedCommitHashes := expectedCommits[2:] + + t.Log("Commits: ", expectedCommits) + t.Log("End At:", endCommitHash) + + log, _ := CommitLog(repo, "", endCommitHash, SupportedKeys, true) + if log == "" { + t.Fail() + } + + // should have all commits except the first one + if strings.Contains(log, firstCommit) { + t.Fail() + } + + for _, commitHash := range acceptedCommitHashes { + if !strings.Contains(log, commitHash) { + t.Log("Failed at:", commitHash) + t.Fail() + } + } + + t.Log("\n", log) +} diff --git a/log/tag_test.go b/log/tag_test.go new file mode 100644 index 0000000..f8df65f --- /dev/null +++ b/log/tag_test.go @@ -0,0 +1,150 @@ +package commitlog + +import ( + "strings" + "testing" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func getTagOptions(message string) *git.CreateTagOptions { + return &git.CreateTagOptions{ + Message: message, + Tagger: &object.Signature{ + Name: "Test", + Email: "test@reaper.im", + When: time.Now(), + }, + } + +} + +func TestCommitLogSingleTag(t *testing.T) { + secondCommit := expectedCommits[1] + acceptedCommits := expectedCommits[2:] + + t.Log("Commits:", expectedCommits) + t.Log("Tagged:", secondCommit) + + hash, err := repo.ResolveRevision(plumbing.Revision(secondCommit)) + bail(err) + + _, err = repo.CreateTag("0.0.0", *hash, getTagOptions("0.0.0")) + bail(err) + + log, _ := CommitLog(repo, "", "", SupportedKeys, true) + if log == "" { + t.Fail() + } + + if strings.Contains(log, expectedCommits[0]) { + t.Fail() + } + + for _, commit := range acceptedCommits { + if !strings.Contains(log, commit) { + t.Fail() + } + } + + t.Log(log) + + // clean-up + bail(repo.DeleteTag("0.0.0")) + +} + +// Test with 2 tags, one on the second commit and one on the 2nd last commit, +// should only have the last commit in the log +func TestCommitLogDualTag(t *testing.T) { + secondCommit := expectedCommits[1] + secondLastCommit := expectedCommits[len(expectedCommits)-2] + acceptedCommit := expectedCommits[len(expectedCommits)-1] + + t.Log("Commits:", expectedCommits) + t.Log("Tagged:", secondCommit, secondLastCommit) + + secondHash, err := repo.ResolveRevision(plumbing.Revision(secondCommit)) + bail(err) + + secondLastHash, err := repo.ResolveRevision(plumbing.Revision(secondLastCommit)) + bail(err) + + _, err = repo.CreateTag("0.0.0", *secondHash, getTagOptions("0.0.0")) + bail(err) + + _, err = repo.CreateTag("0.0.1", *secondLastHash, getTagOptions("0.0.1")) + bail(err) + + log, _ := CommitLog(repo, "", "", SupportedKeys, true) + if log == "" { + t.Fail() + } + + for _, commit := range expectedCommits { + if commit == acceptedCommit { + if !strings.Contains(log, acceptedCommit) { + t.Fail() + } + } else { + if strings.Contains(log, commit) { + t.Fail() + } + } + + } + + t.Log(log) + + // clean-up + bail(repo.DeleteTag("0.0.0")) + bail(repo.DeleteTag("0.0.1")) +} + +// Test with 2 tags, one on the second commit and one on the last commit, +// should give all commits till the 1st tag +func TestCommitLogHeadTag(t *testing.T) { + secondCommit := expectedCommits[1] + lastCommit := expectedCommits[len(expectedCommits)-1] + + t.Log("Commits:", expectedCommits) + t.Log("Tagged:", secondCommit, lastCommit) + + secondHash, err := repo.ResolveRevision(plumbing.Revision(secondCommit)) + bail(err) + + lastHash, err := repo.ResolveRevision(plumbing.Revision(lastCommit)) + bail(err) + + _, err = repo.CreateTag("0.0.0", *secondHash, getTagOptions("0.0.0")) + bail(err) + + _, err = repo.CreateTag("0.0.1", *lastHash, getTagOptions("0.0.1")) + bail(err) + + log, _ := CommitLog(repo, "", "", SupportedKeys, true) + if log == "" { + t.Fail() + } + + for index, commit := range expectedCommits { + if index <= 1 { + if strings.Contains(log, commit) { + t.Fail() + } + } + if index > 1 && !strings.Contains(log, commit) { + t.Fail() + } + + } + + t.Log(log) + + // clean-up + bail(repo.DeleteTag("0.0.0")) + bail(repo.DeleteTag("0.0.1")) +}