moneta#
A changelog built from git history.
Change entries are Markdown files under .changes. Which release each one
belongs to is the first tag whose tree contains it — so a release is a tag
and nothing else. No commit bumps a version, nothing is moved or deleted at
release time, and the changelog follows from the history.
$ moneta new -k added -m "Support nested .changes directories" -i 42
.changes/20260923T142530Z-added-support-nested-changes-directories.md
$ moneta next
v0.2.0
$ git tag v0.2.0 # that is the entire release ceremony
The rule#
files(T) = the fragment keys under .changes in tag T's tree
baseline(T) = the union of files(A) over every ancestor tag A of T
entries(T) = files(T) minus baseline(T)
Three choices in there are load-bearing, and each rules out a wrong answer:
Tree presence, not “the commit that added the file” : After a rebase, a squash or a cherry-pick, which commit introduced this has several defensible answers. Was it in the release has exactly one.
A union over every ancestor, not a diff against the newest : Picking one predecessor goes wrong the first time a branch is merged. Nothing in attribution depends on semver ordering, so two tags on one commit, a mistyped tag and tags on divergent lineages cannot produce a wrong answer.
Keys, not paths
: A fragment’s key is its filename stem, or an explicit id:. Moving one into
a subdirectory is a delete and an add under path comparison, which would
silently re-credit the entry to a later release.
Editing an entry after it has shipped#
Attribution is fixed by the tags. What an entry says is read from the working
tree, so fixing a typo in an entry from two releases ago reaches the next docs
build without moving it. Deleting the fragment falls back to the text its tag
holds, so tidying .changes never erases history.
The one edit that does move an entry is changing its key after it has shipped:
the old key stays attributed to its release while the new one matches nothing
and lands in Unreleased, so the entry appears twice. moneta check fails on it
and names both keys. Set id: before a release if a rename is coming.
Shallow clones#
moneta refuses to run in a shallow clone, and the refusal is the feature. A grafted history does not fail an ancestry question, it answers it wrongly, so the result would be a changelog quietly missing whole releases — published to a docs site and to a release page with nothing anywhere saying so.
In GitLab CI, set GIT_DEPTH: 0 on any job that runs moneta.
--allow-shallow degrades instead of refusing. It sets "truncated": true in
the JSON and puts a warning at the top of every rendered format, so the result
still says what it is.