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¶
- The boundary is exclusive.
v1.2.0itself 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
vis optional.WithSinceTag("1.2.0")andWithSinceTag("v1.2.0")resolve to the same boundary, whichever form the tag itself uses. - Comparison is semver, not string:
v1.9.0is belowv1.10.0. - Pre-release tags sort below their release, so
WithSinceTag("v1.0.0-rc.1")keepsv1.0.0. - If the since tag is the newest tag and nothing is untagged, the output is the empty
string with a
nilerror.
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.
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¶
- The cap is applied after the sections are assembled, keeping the newest
n. n <= 0means no cap.WithMaxReleases(0)andWithMaxReleases(-1)both emit everything; neither is an error.Unreleasedcounts as one of then. On a repository with untagged commits,WithMaxReleases(2)yieldsUnreleasedplus 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¶
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.
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
Unreleasedsection. - 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.