Most Changelog tools are written from the perspective of web service developers. There is only one supported version at a time and the log exists to allow users to see the changes that are happening.
This is an issue as a developer of desktop software, since we would like
to be able to release patches for recent versions without merging in
other new features. This means that the release history will not exactly
match the git history of the develop
branch, since multiple
releases occurred on a specific version branch.
The direct result of this is that the Changelog will be inaccurate for patch releases. Version 1.5.8 will correctly list the changes on version 1.5.0 through 1.5.7, but version 1.6.0 will not list any of the patch releases as existing and take credit for all of the changes in the minor releases.
Below, we examine five solutions to the problem.
Here, we create a single, dedicated release
branch.
Individual releases are denoted by tags on the branch. For patch
releases, individual commits from develop
would be cherry
picked onto the branch and a new tag added. For minor or major releases,
the develop
branch would be merged into the
release
branch. The maintainer might then perform a rebase
to remove the duplicate PRs.
By default, changes from the patch releases will be mentioned again in the minor releases. This can be an issue depending on how declarative versus imperative the notes are. Two different mentions of "Set the font size to 22pt" would cause minimal confusion, but the change log twice mentioning "Double the font size" might cause users to expect a 44pt font.
The unfortunate part is that this can only be solved by rebasing after the merge and deleting the duplicate commits. Since rebasing after falls under the category of worst practices, this is a desided disadvantage
In this format, all releases are based off tags in the develop branch.
With only a single, linear git log, the tools work as expected. This
just leaves the issue of patch releases. To perform a patch release, the
maintainer must rebase the develop
branch to move the
desired commits to just after the previous release. Then a new tag is
created for the new release and the updated develop
would
be force pushed to the server. All developers would then need to rebase
on the latest develop
Assuming that no merges ever occur in the develop
branch
(i.e. every pull request is a squash commit)
Rebasing the entire history for each patch release would be a major time sink for maintainers
All developers would need to rebase all current work in progress every time that there was a patch release, which could result in losing some of that work
Multiple blog posts suggested this as "best practice". Minor releases
still get their own branch which have patch release cherry picked onto
them. However, the next minor release is based off of
develop
and the previous patch releases are ignored. While
the history in the Changelog is slightly inaccurate (since it does not
mention old patch releases), the exact history is still available on
those release branches. A header can be added to the Changelog to inform
the user that the patch information must be pulled from the appropriate
version branch.
Under this approach, git-cliff
and similar tools are
abandoned entirely. Instead, a GitHub action forces every PR to include
an addition to the Changelog. The maintainer then manually edits this
file on each release.
Editing the change log for each release is a bit of a hassle, but less effort than a rebase.
Developers need to perform the extra effort to update the Changelog for each PR. On its own, this should not be significantly more effort than ensuring conventional commits in the titles but
- It is slightly easier for a maintainer to step in and edit a PR title than to edit a Changelog file in a PR
- There will be annoying merge conflicts from every PR editing the Changelog that developers will need to put effort into fixing.
Every major changelog tool seems to use the same algorithm:
- Find the most recent commit (called X) before the current that has a version tag
- Add all commits between the current commit and X to the change set under the current version
- Set the current version the tag for X
- Set the current commit to X
- If at the beginning of the history, quit
- Goto step #1
It is this algorithm that forces everything to be on the same branch. However, an alternate algorithm exists.
- Filter all the tags on the repository to find the list of version tags
- Sort the version tags from old to new
- Pick the oldest tag X and the second old tag Y
- Use git marge-base to find the closest common ancestor to X and Y
- Add all commits between Y and the closest ancestor to the change set
- Remove all commits between X and the closest ancestor from the change set
- Add the Changeset under the tag for Y
- Remove X from the list of tags
- Goto step #3
This procedure allows commits to exist across arbitrary branches while still producing the automated changelog.