Generate from git history¶
GenerateFromRepo reads a repository's tags and commits and returns a changelog as
markdown. It opens the repo with go-git (PlainOpenWithOptions with DetectDotGit), so
the path can be the repo root or any subdirectory of it — no git binary is invoked.
Releases come out newest-first, entries grouped by category within each release. Nothing is written to disk; the string is yours to do something with.
Bound how much history you get¶
With no options, the entire history is emitted — every release back to the first commit. There is no default window, so on a repository with two hundred tags you get two hundred sections. Bound it explicitly:
// Only releases after a given tag (exclusive).
md, err := changelog.GenerateFromRepo(".", changelog.WithSinceTag("v1.2.0"))
// Keep only the newest N sections.
md, err = changelog.GenerateFromRepo(".", changelog.WithMaxReleases(10))
The two combine: WithSinceTag sets the lower bound and WithMaxReleases caps the count
within it. Watch one detail — Unreleased counts as one of the N, so on a repository
with untagged commits WithMaxReleases(2) gives you Unreleased and one tagged release.
WithSinceTag is validated against the repository's tags. A value matching no semver tag
and naming no existing tag is rejected with ErrSinceTagNotFound rather than silently
disabling the filter and emitting everything, so a typo surfaces immediately:
md, err := changelog.GenerateFromRepo(".", changelog.WithSinceTag("v.1.2.0"))
if errors.Is(err, changelog.ErrSinceTagNotFound) {
// the tag is wrong, not the repository
}
Full behaviour of all three options, including what n <= 0 does, is in generation
options.
Include commits that predate the convention¶
WithIncludeAll does not widen the range — it widens what counts as a commit worth
listing. Without it, a subject that is not a valid Conventional Commit is dropped
silently; with it, that subject appears verbatim under Other:
Reach for it on a repository that adopted the convention partway through. It does not
bring back test and ci commits, which are dropped by type rather than by parse
failure.
Generate for a repository you are not standing in¶
The path is a repository path, not a working directory, and DetectDotGit means any
subdirectory works too:
If no .git exists at the path or above it, you get opening git repository: repository
does not exist.
Write it to CHANGELOG.md¶
There is no option for this — the module returns a string and stops:
md, err := changelog.GenerateFromRepo(".", changelog.WithMaxReleases(20))
if err != nil {
return err
}
if err := os.WriteFile("CHANGELOG.md", []byte(md), 0o644); err != nil {
return err
}
Regeneration rewrites the whole document from history. There is no incremental mode and nothing merges with an existing file, so writing the string is a full overwrite by design.
Generate inside CI¶
Two things catch people out in a pipeline:
- Fetch the full history. A shallow clone fails with
iterating commits: object not found, because the walk needs commits the clone never fetched. In GitLab CI setGIT_DEPTH: "0". - Fetch tags. Without tags there are no releases to attribute commits to, and the
entire history comes back as one
Unreleasedsection.
What each commit becomes¶
Every commit subject is parsed as a Conventional Commit. The type selects a category
(feat → feature, fix → fix, perf → performance, others → other), test and ci are
dropped, and a breaking marker or footer overrides the type. Scope and description are
carried onto the entry.
Only eleven commit types are recognised at all — security: and deps: are not among
them. The full list, and what happens to everything else, is in commit
classification.
When the output is not what you expected¶
| Symptom | Cause |
|---|---|
| Empty string, no error | No commits, or every commit was dropped by classification |
Everything under one Unreleased heading |
No semver tags — non-semver tags such as release-4 are invisible |
| A commit is missing | Its type is test or ci, it is a merge commit, or its subject did not parse |
| A commit is in the wrong release | Attribution is by ancestry, not date — see how commits are attributed to releases |
| A version heading with nothing under it | Two tags on the same commit; the lower one claimed the entries |
| A version missing entirely | Every commit in it was dropped, so the section was omitted |
iterating commits: object not found |
Shallow clone |
Limitations¶
GenerateFromRepo cannot filter by path, scope or author, cannot emit commit hashes,
dates, authors or links, cannot change the output format, and reads nothing from the
commit body except the breaking-change footer. The reasoning is in what changelog does
not do.