Skip to content

Generation options

GenerateFromRepo(repoPath string, opts ...GenerateOption) (string, error) takes zero or more options. There are five. They are independent, they combine, and passing the same one twice keeps the last value.

Option Type Default when omitted Effect
WithSinceTag(tag) string "" — no lower bound, the whole history is emitted Emits only releases strictly after tag; a non-semver tag name bounds the walk by exact name
WithMaxReleases(n) int 0 — no cap Keeps at most the n newest sections
WithIncludeAll() (none) off — non-conforming commits are dropped Keeps commits whose message is not a valid Conventional Commit, under Other
WithPathFilter(f) func(path string) bool nil — every commit counts Keeps only commits whose diff against their first parent changes a file f accepts
WithPaths(paths...) ...string none — every commit counts WithPathFilter for "under one of these directories, or exactly this file"

What the defaults actually are

With no options at all, GenerateFromRepo emits every release in the repository, from the newest tag down to the first commit, plus an Unreleased section if anything is untagged. There is no recency window, no default release count, and no default since boundary.

Repositories with long histories therefore produce long output. Bound it explicitly with WithSinceTag or WithMaxReleases; nothing bounds it for you.

WithSinceTag: emit only releases after a tag

md, err := changelog.GenerateFromRepo(".", changelog.WithSinceTag("v1.2.0"))
  • The boundary is exclusive. v1.2.0 itself does not appear; the releases above it do.
  • Commits reachable from the since tag are excluded from every later release, so a commit cannot be re-attributed forward by the filter.
  • A leading v is optional. WithSinceTag("1.2.0") and WithSinceTag("v1.2.0") resolve to the same boundary, whichever form the tag itself uses.
  • Comparison is semver, not string: v1.9.0 is below v1.10.0.
  • Pre-release tags sort below their release, so WithSinceTag("v1.0.0-rc.1") keeps v1.0.0.
  • If the since tag is the newest tag and nothing is untagged, the output is the empty string with a nil error.

When the tag does not exist

The value is validated against the repository's tags before it is applied. An unmatched value returns ErrSinceTagNotFound wrapped with the offending value, and no output:

A tag that is not a version

A tag whose name is not semver is taken by its exact name: WithSinceTag("lint/v0.6.0") excludes the commits reachable from that tag and nothing else, so the repository's own release tags do not bound the walk. This is the shape a component of a repository needs, a directory with tags of its own, together with WithPaths:

commits, err := changelog.CommitsFromRepo(".",
    changelog.WithSinceTag("lint/v0.6.0"),
    changelog.WithPaths("lint"),
)

For GenerateFromRepo the same exclusion applies and the remaining commits are bucketed by the semver tags as usual; the exact tag is a boundary, never a release of its own.

"v9.9.9": since tag not found in repository

This includes a tag that exists in the repository but is not valid semver. WithSinceTag("release-4") fails with ErrSinceTagNotFound even though release-4 is a real tag, because non-semver tags are discarded before the lookup.

WithMaxReleases: cap the number of sections

md, err := changelog.GenerateFromRepo(".", changelog.WithMaxReleases(10))
  • The cap is applied after the sections are assembled, keeping the newest n.
  • n <= 0 means no cap. WithMaxReleases(0) and WithMaxReleases(-1) both emit everything; neither is an error.
  • Unreleased counts as one of the n. On a repository with untagged commits, WithMaxReleases(2) yields Unreleased plus one tagged release. There is no option to exclude the unreleased section from the count or from the output.
  • The cap counts sections, not entries. A section kept by the cap keeps all of its entries.

WithIncludeAll: keep non-conforming commits

md, err := changelog.GenerateFromRepo(".", changelog.WithIncludeAll())

WithIncludeAll has nothing to do with how much history is included — it controls commit messages, not range. Without it, a commit whose subject is not a valid Conventional Commit is dropped silently. With it, that commit becomes an Other entry whose description is the full subject line, with no scope.

### Other

* just a plain message

It does not resurrect test and ci commits: those are dropped by type after a successful parse, not by parse failure. See commit classification.

Combining options

Options are applied in the order given to a single configuration struct, then acted on independently:

md, err := changelog.GenerateFromRepo(".",
    changelog.WithSinceTag("v1.2.0"),  // lower bound
    changelog.WithMaxReleases(5),      // cap within that bound
    changelog.WithIncludeAll(),        // and keep unparseable subjects
)

WithSinceTag is applied first and sets what exists; WithMaxReleases then trims the newest n of what survived. Ordering the option arguments differently does not change the result.

What there is no option for

  • Choosing which commit types are dropped, or renaming a category heading.
  • Excluding the Unreleased section.
  • Filtering by scope, by path, or by author.
  • Including commit hashes, dates, authors or links in the output.
  • Changing the heading levels or bullet characters that are emitted.
  • Writing to a file, or merging into an existing CHANGELOG.md.

The reasoning for these is in What changelog does not do.

WithPathFilter and WithPaths: only the commits that touch something

commits, err := changelog.CommitsFromRepo(".", changelog.WithSinceTag("lint/v0.6.0"),
    changelog.WithPaths("lint"))

A commit touches a path when its diff against its first parent (against an empty tree for a root commit) changes a file there; a rename counts under both names. The filter sees repository-relative file paths, so WithPaths("lint") matches lint/go.mod and not linter/x, and WithPaths("go.mod") matches that file alone. A trailing slash is ignored. The ordering and the WithSinceTag boundary are unchanged; a commit that touches nothing you asked for is simply absent.

WithPathFilter takes the predicate directly, for a rule WithPaths cannot say: "everything outside these directories" is func(p string) bool { return !under(p) }. A nil filter, like no paths, is the same as no option.

Both apply to GenerateFromRepo too, so a generated changelog and a read of the same commits agree about what is in range.