Back

Using Git to Generate Changelogs

vendion

It is generally considered best practice for a project to keep a changelog, as this gives people who interact with the project an easy way to see what has changed between releases. The problem is there is no real one-size-fits-all solution to how changelogs should be maintained. Some projects may have a text file that developers manually edit whenever something is changed with the project, while others may parse this out of their version control tool and auto-generate one. In this blog post I will discuss a way to do the latter using git-notes.

Git-notes is a powerful tool that allows for adding additional information to a commit that doesn’t show up in the commit message itself. What is even better is that git-notes doesn’t modify the object it is attached to; instead, they are stored as their own reference in Git. A repository can have as many note references as are needed without causing any conflicts. By default, when git note add <commit hash> is called, it creates a new object under refs/notes/commits that points to the given commit hash, or if no commit hash is given, then it points to the most recent commit. One big downside to how git-notes works is that it can be pretty difficult to tell if a project is making use of them, as by default notes don’t show up in a standard output. When inspecting a commit that has one or more notes attached to it, the only clue is that you will see an unindented line saying followed by the notes. At first I wrote off the idea of using git-notes due to this behavior. But after seeing that Google built a whole code review tool for Git that is backed by git-notes, it started to get me thinking about what else they can be used for. Enter the project changelog.

Setup

Knowing that by default git-notes stores everything in refs/notes/commits, this implies that git-notes could support many references, and in fact it does. Looking at the documentation for git-notes there is a --ref argument that overrides the default note location. This means that changelog entries can be stored in refs/notes/changelog and still be separate from the commit message and leave the default note namespace clear for other things. To record a new changelog entry, the user can use git notes --ref=changelog add <commit hash>. While the syntax is not perfect due to how much typing this requires and leaves space for user error due to typos, this shortcoming can be addressed by creating a new alias in Git for it.

git config --global alias.changelog 'notes --ref=changelog add'

This creates a new changelog alias in Git stored in the global Git configuration file $HOME/.gitconfig which now allows for changelog entries to be recorded with git changelog <commit hash>.

Adding a changelog entry

Thanks to the changelog alias that was created earlier, adding a new changelog entry is as easy as calling git changelog which will then open a text editor with a pretty familiar sight.

## Syncing changelog entries between remotes and systems

Another shortcoming of the way git-notes works is that by default they are not included when a push, pull, or fetch operation is run. This can be addressed by editing the project’s .git/config file and adding the following lines to the remote section(s):

fetch = +refs/notes/changelog:refs/notes/changelog
push = +refs/notes/changelog:refs/notes/changelog

This way, when a push, pull, or fetch operation is done in Git, the changelog notes will also be included.

Writing out the CHANGELOG file

The final thing needed to pull this all together is a way to take this information and write it out in an easy-to-read way. Because the notes themselves are just objects in Git, that means we can easily interact with them like we would anything else. It’s also important that the notes show up with the correct release of the project; project releases, of course, being tracked using git-tag also makes this easy. The way I approached this is with a simple bash script that walks the git commit history, pulling the tags and changelog notes, and writes out a markdown file following the format documented by Keep a Changelog.

#!/usr/bin/env bash

get_tag() {
  local rev="$1"
  local lastTag="$2"

  local tag=''
  IFS='-' read -ra tag <<< "$(git describe --tags "${rev}")"

  if [[ -z "${tag[1]}" ]]; then
    echo -n "${tag[0]}"
  else
    echo -n "${lastTag}"
  fi
}

get_tag_date() {
  local tag="$1"
  local date=''

  date="$(git log -1 --pretty=tformat:%ad --date=short "${tag}")"
  echo -n "${date}"
}

check_git_tag() {
  local rev="$1"

  if git describe --tags "${rev}" &> /dev/null; then
    local tag=''
    tag="$(get_tag "${rev}" "${lastTag}")"

    if [[ "${lastTag}" != "${tag}" ]]; then
      local date=''
      date="$(get_tag_date "${tag}")"
      echo '' # Add a blank line
      echo "## [${tag}] - ${date}"
      lastTag="${tag}"
    fi
  fi
}

get_changelog_notes() {
  local rev="$1"

  if git notes --ref=changelog list "${rev}" &> /dev/null; then
    local line=''
    line="$(git notes --ref=changelog show "${rev}")"
    echo "- ${line}"
  fi
}

get_changelog_notes_by_section() {
  local rev="$1"
  local section="$2"

  if git notes --ref=changelog list "${rev}" &> /dev/null; then
    local line=''
    line="$(git notes --ref=changelog show "${rev}")"
    if [[ "${line}" =~ ^${section} ]]; then
      echo "- ${line}"
    fi
  fi
}

generate_header() {
  echo "# Changelog" echo 'All notable changes to this project will be documented in this file.'
  echo "" # Blank line intentional
  echo 'The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)'
  printf '\\n## %s\\n' "\[Unreleased\]"
}

main() {
  local lastTag='' generate_header

  sections=('\[Added\]' '\[Changed\]' '\[Deprecated\]' '\[Removed\]' '\[Fixed\]' '\[Security\]')
  for rev in $(git rev-list HEAD); do
    for section in "${sections[@]}"; do
      check_git_tag "${rev}"
      get_changelog_notes_by_section "${rev}" "${section}"
    done
  done

  exit 0
}

main "$@"

This will produce output that looks something like the following:

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)

## [Unreleased]

## [v0.3.0] - 2021-08-04

- [Changed] Improve error and logging handling
- [Changed] Clean up the output from the list command
- [Added] Add ability to remove a host entry from the DB
- [Added] Added TUI interface for editing and adding host entries
- [Added] Added a command to write out a SSH config file
- [Added] Add a way to import an existing SSH config file into the DB
- [Changed] Switch from github.com/peterbourgon/diskv to bbolt for managing internal DB store.

## [v0.2.0] - 2015-02-08

...

## [v0.1] - 2014-03-30

There we have it, our changelog stored in a distributed and easy-to-generate way!

About the Author

Adam Jimerson is a freelance software engineer based in Cleveland, Tennessee, with over 17 years of experience specializing in NixOS, Go, PHP/Laravel, and FreeBSD. He maintains several open-source projects and serves the Chattanooga.Digital community.

Comments

No comments yet. Be the first to share your thoughts!

Leave a comment

Name
Email
Website
Comment Markdown supported: bold, italic, links, lists, code