changelog

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: MIT Imports: 19 Imported by: 0

README

changelog

Turn a repository's Conventional-Commits history — or a release-notes archive — into a structured, categorised changelog

Go Reference Pipeline Coverage phpboyscout Go toolkit

Part of the phpboyscout Go toolkit — small, framework-free Go modules extracted from go-tool-base. Docs: changelog.go.phpboyscout.uk


changelog does two things: generate a changelog by walking a repository's git history and parsing Conventional Commits, and parse an existing release-notes document back into a typed model you can query.

// Generate from a repository's tags and commits.
md, err := changelog.GenerateFromRepo(".", changelog.WithSinceTag("v1.2.0"))

// Parse existing notes and inspect them.
cl, err := changelog.Parse(rawNotes) // ErrNoReleases when nothing parseable
if cl.HasBreakingChanges() {
    for _, e := range cl.BreakingChanges() {
        fmt.Println("⚠", e.Scope, e.Description)
    }
}

What it does

  • Conventional-Commits aware. Commits are classified into breaking / feature / fix / performance / other, with scope and description extracted. feat → feature, fix → fix, perf → performance; test/ci are skipped. Breaking changes are detected from both ! and the BREAKING CHANGE: footer.
  • Semver-ordered releases. Tags are validated and ordered with golang.org/x/mod/semver, so releases come out newest-first regardless of tag order in the repo.
  • Reads history directly. Generation opens the repo with go-git (PlainOpenWithOptions, DetectDotGit) — no shelling out to git.
  • Parse and query. Parse / ParseFromArchive turn a markdown changelog (including a gzip/tar release archive) into Changelog → Release → Entry, with helpers like HasBreakingChanges, BreakingChanges, and EntriesByCategory.
  • No framework weight. The graph is go-git, the leodido conventional-commits parser, x/mod, and cockroachdb/errors — enforced by a depfootprint guard.

Install

go get gitlab.com/phpboyscout/go/changelog

Documentation

Full guides: changelog.go.phpboyscout.uk. API reference: pkg.go.dev.

License

See LICENSE.

Documentation

Overview

Package changelog generates and parses changelogs from Conventional Commits, in pure Go.

It serves two directions:

  • Generation. GenerateFromRepo walks a repository's git history (via go-git), parses each Conventional Commit, groups the entries by release and Category, and renders a changelog — replacing an external tool such as git-cliff with an in-process, dependency-free implementation. Options (WithSinceTag, WithMaxReleases, WithIncludeAll) bound and shape the output.
  • Parsing. Parse reads existing changelog notes back into a structured Changelog; ParseFromArchive extracts and parses a changelog bundled in a release archive; FormatSummary renders a concise summary of a parsed changelog. Both this module's own emitted format ("# vX.Y.Z" headings, "*" bullets) and the releaser-pleaser / keep-a-changelog family ("## [vX.Y.Z](url)" link-wrapped headings, "-" bullets, "**scope**:" entries and the inline "**BREAKING**: " marker) are accepted. Non-empty input with no recognisable release returns ErrNoReleases so callers can distinguish "no changelog" from "unparseable format".

Generation buckets commits by ancestry: each commit is attributed to the earliest release whose tag can reach it, so parallel branches and tags placed on merge commits (the normal shape of a merged Release MR) are handled correctly regardless of committer timestamps. Merge commit messages never contribute entries.

Commit classification follows the Conventional Commits specification: `feat`/`fix`/`perf`/etc. map to a Category, and both the `BREAKING CHANGE:` footer and its `BREAKING-CHANGE:` hyphenated synonym mark a breaking change.

The built-in `changelog` command (see pkg/cmd) embeds and displays the generated changelog at runtime.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoReleases = errors.NewSentinel("changelog.no_releases", "no releases found in changelog input")

ErrNoReleases is returned by Parse when the input is non-empty but no release could be recognised, so callers (e.g. a self-update breaking-change check) can distinguish "no changelog" from "unparseable format". Empty input is not an error.

View Source
var ErrSinceTagNotFound = errors.NewSentinel("changelog.since_tag_not_found", "since tag not found in repository")

ErrSinceTagNotFound is returned by the repository generator when the value passed to WithSinceTag does not match any tag in the repository. Callers can distinguish it with errors.Is. Without this a typo'd since tag would silently disable the filter and emit the full history.

Functions

func FormatSummary

func FormatSummary(cl *Changelog) string

FormatSummary produces a human-readable summary of the changelog suitable for terminal output. Breaking changes appear first with a warning prefix.

func GenerateFromRepo

func GenerateFromRepo(repoPath string, opts ...GenerateOption) (string, error)

GenerateFromRepo reads git history from the repository at repoPath and produces a full CHANGELOG.md string. The output is compatible with Parse().

Types

type Category

type Category int

Category classifies a change entry.

const (
	// CategoryBreaking represents a breaking change requiring user action.
	CategoryBreaking Category = iota
	// CategoryFeature represents a new feature.
	CategoryFeature
	// CategoryFix represents a bug fix.
	CategoryFix
	// CategoryPerformance represents a performance improvement.
	CategoryPerformance
	// CategoryOther represents any other change (refactor, docs, chore, etc.).
	CategoryOther
)

type Changelog

type Changelog struct {
	// FromVersion is the starting version (exclusive).
	FromVersion string
	// ToVersion is the ending version (inclusive).
	ToVersion string
	// Releases contains parsed releases ordered from oldest to newest.
	Releases []Release
}

Changelog represents the parsed changelog across multiple releases.

func Parse

func Parse(rawNotes string) (*Changelog, error)

Parse parses raw release notes markdown (as returned by SelfUpdater.GetReleaseNotes) into a structured Changelog. It accepts both this module's own emitted format ("# vX.Y.Z" headings, "*" bullets) and the releaser-pleaser / keep-a-changelog family ("## [vX.Y.Z](url)" headings, "-" bullets, "**scope**:" entries and inline "**BREAKING**: " markers).

Non-empty input in which no release can be recognised returns the (empty) changelog alongside ErrNoReleases; empty input returns an empty changelog with a nil error.

func ParseFromArchive

func ParseFromArchive(r io.Reader) (*Changelog, error)

ParseFromArchive extracts and parses a CHANGELOG.md file from a gzipped tar release archive reader. Returns nil (not an error) if no changelog is found in the archive, allowing callers to fall back to API-based retrieval.

func (*Changelog) BreakingChanges

func (c *Changelog) BreakingChanges() []Entry

BreakingChanges returns all breaking change entries across all releases.

func (*Changelog) EntriesByCategory

func (c *Changelog) EntriesByCategory(cat Category) []Entry

EntriesByCategory returns all entries matching the given category across all releases.

func (*Changelog) HasBreakingChanges

func (c *Changelog) HasBreakingChanges() bool

HasBreakingChanges returns true if any release contains breaking changes.

type Commit added in v0.3.0

type Commit struct {
	// Hash is the full commit SHA.
	Hash string

	// Type is the Conventional Commits type verbatim: "feat", "fix", "perf",
	// "refactor", "chore", "docs", "style", "test", "ci", or whatever else was
	// written. It is empty for a non-conventional commit, which only appears
	// here when WithIncludeAll is set.
	//
	// This is the field Category cannot substitute for: refactor, chore, docs
	// and style all map to CategoryOther, and a consumer deciding a version
	// bump needs to tell them apart.
	Type string

	// Scope is the conventional commit scope. May be empty.
	Scope string

	// Description is the subject line with the type and scope removed.
	Description string

	// Body is everything below the subject, verbatim. It is provided so a
	// consumer can read its own trailers; this module reads it for one thing
	// only, the BREAKING CHANGE footer.
	Body string

	// Breaking reports a "!" marker or a BREAKING CHANGE: / BREAKING-CHANGE:
	// footer.
	Breaking bool

	// Category is the same classification Entry carries, provided so a caller
	// that wants to group commits the way the changelog does need not
	// reimplement the mapping.
	Category Category
}

Commit is one commit as this module read it: the READING model, beside Entry's presentation model.

Why this is not simply a richer Entry

Entry is what the changelog SHOWS, and Parse reads it back out of the rendered text. That round-trip is the module's organising principle — it renders only what it can also read back — and it is why the output carries no hashes, dates or authors.

Neither field below survives that round trip. A hash is not in the rendered output at all, and a type is not recoverable from it, because the renderer collapses refactor, chore, docs and style into a single "Other" heading. Put them on Entry and they would be populated by the generator and silently empty after a Parse, decided by which constructor the caller happened to use — a field that exists gets read, and a doc comment is not present at the call site.

So the two models stay separate. Entry renders and round-trips; Commit describes what a commit was. See spec 0001.

func CommitsFromRepo added in v0.3.0

func CommitsFromRepo(repoPath string, opts ...GenerateOption) ([]Commit, error)

CommitsFromRepo reads git history and returns every conventional commit in range, newest first.

It takes the same options as GenerateFromRepo and walks the same history the same way, so the two never disagree about what is in range. With WithSinceTag, that is the commits after the named tag; with no options, the whole history.

It does not filter the way the generator does

GenerateFromRepo drops test and ci commits, because a changelog reader does not want them. This does not. A consumer reasoning about commits may care, and one that does not should ignore them by its own visible rule rather than because the library quietly removed them first (spec 0001 D3).

Non-conventional commits are dropped, matching the generator, unless WithIncludeAll is set. Merge commits never appear.

type Entry

type Entry struct {
	// Category is the type of change.
	Category Category
	// Scope is the conventional commit scope (e.g., "http", "chat"). May be empty.
	Scope string
	// Description is the change description text.
	Description string
	// Raw is the original unparsed line from the release notes.
	Raw string
}

Entry represents a single change within a release.

type GenerateOption

type GenerateOption func(*generateConfig)

GenerateOption configures changelog generation.

func WithIncludeAll

func WithIncludeAll() GenerateOption

WithIncludeAll includes non-conventional commits under "Other".

func WithMaxReleases

func WithMaxReleases(n int) GenerateOption

WithMaxReleases limits output to the N most recent releases.

func WithPathFilter added in v0.4.0

func WithPathFilter(filter func(path string) bool) GenerateOption

WithPathFilter keeps only the commits that touch a path the filter accepts.

A commit touches a path when its diff against its first parent (against an empty tree for a root commit) changes a file at that path; a rename counts under both its old and its new name. The filter sees repository-relative file paths. A nil filter is the same as no option.

Everything else is unchanged: WithSinceTag still bounds the walk, WithIncludeAll still decides whether non-conventional commits appear, and the order is the order the unfiltered walk gives. A commit that touches nothing the filter accepts is not attributed anywhere; it is dropped.

func WithPaths added in v0.4.0

func WithPaths(paths ...string) GenerateOption

WithPaths keeps only the commits that touch one of the given paths.

A path naming a directory matches every file beneath it (`lint` matches `lint/go.mod` and not `linter/x`); a path naming a file matches that file. A trailing slash is accepted and ignored. No paths is the same as no option.

func WithSinceTag

func WithSinceTag(tag string) GenerateOption

WithSinceTag limits generation to releases after the given tag.

A semver tag (`v1.2.0`, with the `v` optional) bounds the walk by release: every tag at or below it is excluded. Any other existing tag is taken by its exact name (`lint/v0.6.0`, `capabilitygen-v0.6.0`): the commits reachable from it are excluded and nothing else bounds the walk, which is what a component of a repository needs to read its own history. A name that is neither is ErrSinceTagNotFound.

type Release

type Release struct {
	// Version is the release tag (e.g., "v1.5.0").
	Version string
	// Entries contains all parsed change entries for this release.
	Entries []Entry
}

Release represents the parsed changelog for a single release version.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL