diff --git a/init.go b/init.go index 0367ccc..18eb3e3 100644 --- a/init.go +++ b/init.go @@ -1,7 +1,6 @@ package chglog import ( - "errors" "fmt" "os" "sort" @@ -10,55 +9,102 @@ import ( "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/storer" ) -// InitChangelog create a new ChangeLogEntries from a git repo. -func InitChangelog(gitRepo *git.Repository, owner string, notes *ChangeLogNotes, deb *ChangelogDeb, useConventionalCommits bool) (cle ChangeLogEntries, err error) { - var ( - tagRefs storer.ReferenceIter - tags []*semver.Version - start, end plumbing.Hash - ) - - cle = make(ChangeLogEntries, 0) - end = plumbing.ZeroHash - - if tagRefs, err = gitRepo.Tags(); err != nil { - return nil, fmt.Errorf("unable to fetch tags: %w", err) +func versionsInRepo(gitRepo *git.Repository) (map[plumbing.Hash]*semver.Version, error) { + tagRefs, err := gitRepo.Tags() + if err != nil { + return nil, err } + defer tagRefs.Close() - if err = tagRefs.ForEach(func(t *plumbing.Reference) error { - var version *semver.Version + tags := make(map[plumbing.Hash]*semver.Version) + + err = tagRefs.ForEach(func(t *plumbing.Reference) error { + var ( + version *semver.Version + tag *object.Tag + ) tagName := t.Name().Short() + hash := t.Hash() if version, err = semver.NewVersion(tagName); err != nil || version == nil { fmt.Fprintf(os.Stderr, "Warning: unable to parse version from tag: %s : %v\n", tagName, err) return nil } - tags = append(tags, version) + // If this is an annotated tag look up the hash of the commit and use that. + if tag, err = gitRepo.TagObject(t.Hash()); err == nil { + var c *object.Commit + + if c, err = tag.Commit(); err != nil { + return fmt.Errorf("cannot dereference annotated tag: %s : %w", tagName, err) + } + hash = c.Hash + } + + tags[hash] = version + return nil - }); err != nil { + }) + + if err != nil { return nil, err } - sort.Slice(tags, func(i, j int) bool { return tags[i].LessThan(tags[j]) }) + return tags, nil +} - for _, version := range tags { - tagName := version.Original() +func versionsOnBranch(gitRepo *git.Repository) (map[*semver.Version]plumbing.Hash, error) { + repoVersions, err := versionsInRepo(gitRepo) + if err != nil { + return nil, err + } + + refs, err := gitRepo.Log(&git.LogOptions{}) + if err != nil { + return nil, err + } + + defer refs.Close() - t, err := gitRepo.Tag(tagName) - if err != nil { - return nil, err + versions := make(map[*semver.Version]plumbing.Hash) + + err = refs.ForEach(func(c *object.Commit) error { + if v, ok := repoVersions[c.Hash]; ok { + versions[v] = c.Hash } + return nil + }) + + return versions, err +} + +// InitChangelog create a new ChangeLogEntries from a git repo. +func InitChangelog(gitRepo *git.Repository, owner string, notes *ChangeLogNotes, deb *ChangelogDeb, useConventionalCommits bool) (cle ChangeLogEntries, err error) { + var start, end plumbing.Hash + + cle = make(ChangeLogEntries, 0) + end = plumbing.ZeroHash + + versions, err := versionsOnBranch(gitRepo) + if err != nil { + return nil, err + } + + tags := make([]*semver.Version, 0, len(versions)) + for v := range versions { + tags = append(tags, v) + } + sort.Slice(tags, func(i, j int) bool { return tags[i].LessThan(tags[j]) }) + + for _, version := range tags { var ( commits []*object.Commit commitObject *object.Commit - tag *object.Tag ) if version.Prerelease() != "" { @@ -66,27 +112,12 @@ func InitChangelog(gitRepo *git.Repository, owner string, notes *ChangeLogNotes, continue } - if start, err = GitHashFotTag(gitRepo, tagName); err != nil { - return nil, fmt.Errorf("unable to find hash for tag: %s : %w", tagName, err) - } - - // If this is an annotated tag look up the hash of the commit and use that. - if tag, err = gitRepo.TagObject(t.Hash()); err == nil { - var c *object.Commit - - if c, err = tag.Commit(); err != nil { - return nil, fmt.Errorf("cannot dereference annotated tag: %s : %w", tagName, err) - } - start = c.Hash - } + start = versions[version] if commitObject, err = gitRepo.CommitObject(start); err != nil { - // This ignores objects that are off branch which happens when tagging on multiple branches happens. - if errors.Is(err, plumbing.ErrObjectNotFound) { - continue - } - return nil, fmt.Errorf("unable to fetch commit from tag %v: %w", tagName, err) + return nil, fmt.Errorf("unable to fetch commit from tag %v: %w", version.Original(), err) } + if owner == "" { owner = fmt.Sprintf("%s <%s>", commitObject.Committer.Name, commitObject.Committer.Email) } diff --git a/order_test.go b/order_test.go index d527b53..106bcf0 100644 --- a/order_test.go +++ b/order_test.go @@ -122,6 +122,98 @@ func TestOrderChangelog(t *testing.T) { }) } +func TestOffBranchTags(t *testing.T) { + cle, err := Parse("./testdata/gold-order-changelog.yml") + if err != nil { + t.Fatal(err) + } + + // remove odd entries + + goldCLE := make(ChangeLogEntries, len(cle)/2+1) + for i := range cle { + if i%2 == 0 { + goldCLE[i/2] = cle[i] + } + } + + repo := newTestRepo() + tree, err := repo.Git.Worktree() + if err != nil { + t.Fatal(err) + } + + // initial commit on master + + hash := repo.modifyAndCommit("file", defCommitOptions()) + if _, err = repo.Git.CreateTag("v0.0.0", hash, nil); err != nil { + t.Fatal(err) + } + + // second commit on develop + + err = tree.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName("develop"), + Create: true, + }) + if err != nil { + t.Fatal(err) + } + + hash = repo.modifyAndCommit("file", defCommitOptions()) + if _, err = repo.Git.CreateTag("v0.1.0", hash, nil); err != nil { + t.Fatal(err) + } + + // alternate branches for commits + + master := plumbing.NewBranchReferenceName("master") + develop := plumbing.NewBranchReferenceName("develop") + + for i := 2; i <= 10; i++ { + branch := master + if i%2 != 0 { + branch = develop + } + + t.Logf("%v branch=%v\n", i, branch) + + err = tree.Checkout(&git.CheckoutOptions{Branch: branch}) + if err != nil { + t.Fatal(err) + } + + hash := repo.modifyAndCommit("file", defCommitOptions()) + + if _, err = repo.Git.CreateTag(fmt.Sprintf("v0.%d.0", i), hash, nil); err != nil { + t.Fatal(err) + } + } + + testCLE, err := InitChangelog(repo.Git, "", nil, nil, false) + if err != nil { + t.Fatal(err) + } + + // zero all commit hashes (don't care about these) + + for i := range testCLE { + for j := range testCLE[i].Changes { + testCLE[i].Changes[j].Commit = "" + } + } + for i := range goldCLE { + for j := range goldCLE[i].Changes { + goldCLE[i].Changes[j].Commit = "" + } + } + + Convey("Generated entry should be the same as the golden entry", t, func() { + So(testCLE, ShouldResemble, goldCLE) + }) + +} + func TestSemverTag(t *testing.T) { repo := newTestRepo() tag := "1.0.0"