The letter A styled as Alchemists logo. lchemists
Published January 15, 2024 Updated January 15, 2024
Cover
Git Notes

Git Notes are great for adding supplementary information to commits, tags, and/or trees. They are also mutable which means they can be added, edited, and deleted as often as you like without altering the original commit, tag, and/or tree.

Unfortunately, Git Notes are not automatically enabled by default. They are not even visible via the various hosting services either. For example, GitHub stopped supporting notes in 2014 for no reason. The other hosting services are no better in this regard. Despite the lack of hosting support, Git Notes are fully supported via the Git Command Line Interface (CLI) which is all that matters.

This article will teach you how to leverage notes and make the correct modifications to your Git configuration so you can leverage Git Notes in your own workflows. 🚀

Quick Start

In a nutshell, here’s what you need to add to your global configuration to make Git Notes work (in this case I’m using a test repository for demonstration purposes):

[notes]
  rewriteRef = refs/notes/commits

[remote "origin"]
  url = https://github.com/bkuhlmann/test
  fetch = +refs/heads/*:refs/remotes/origin/*
  fetch = refs/notes/*:refs/notes/*
  fetch = refs/tags/*:refs/tags/*
  push = refs/heads/*:refs/heads/*
  push = refs/notes/*:refs/notes/*
  push = refs/tags/*:refs/tags/*

The rest of this article will explain why the above is necessary and how it all works.

Configuration

You’ll need to expliclity define what you want to fetch and push for your remote origin configuration. By default, when you clone a repository, you’ll see these settings for your remote origin configuration:

[remote "origin"]
  url = https://github.com/bkuhlmann/test
  fetch = +refs/heads/*:refs/remotes/origin/*

These default settings are great for most operations but — to synchronize notes — you’ll need to make additional edits so that your configuration looks like the following:

[remote "origin"]
  url = https://github.com/bkuhlmann/test
  fetch = +refs/heads/*:refs/remotes/origin/*
  fetch = refs/notes/*:refs/notes/*
  fetch = refs/tags/*:refs/tags/*
  push = refs/heads/*:refs/heads/*
  push = refs/notes/*:refs/notes/*
  push = refs/tags/*:refs/tags/*

The above explicitly defines what is to be fetched and pushed (i.e. heads, notes, and tags). By default, the fetching and pushing of heads and tags is handled for you. The automatic pushing of heads is especially important because it’s not a defined configuration and is implicitly configured and resolved automatically for you. Since we need to teach the remote how to handle notes we also need to define (or redefine) head and tag behavior. Otherwise, you’d lose instead of gain functionality.

When talking to Junio C Hamano — a core Git contributor — on Git Lore, I learned the lack of the + prefix is critical when pushing in a manner that will not clobber other’s work. Hamano also recommends that this is a great practice, in general, to prevent downstream history from being disrupted even when you are the only one pushing to your publishing point (i.e. branch) and you rewind your history. This is especially true for me since I heavily rely on a Git Rebase workflow.

Aliases/Functions

When working with Git Notes, you might want to use a few Bash aliases to speed up your workflow:

alias gna="git notes add"
alias gnd="git notes remove"
alias gne="git notes edit"
alias gnl="git notes list"
alias gnp="git notes prune"
alias gns="git notes show"

You can also get fancy and use functions to interactively work with notes. Here’s an example of a Bash function which allows you to select a commit you want to add a note too:

# Label: Git Notes Add (interactive)
# Description: Select which commit note to add for current feature branch.
gnai() {
  local commits=($(_git_branch_shas))

  _git_commit_options "${commits[*]}"

  printf "\n"
  read -p "Enter selection or quit (q): " response
  if [[ "$response" == 'q' ]]; then
    return
  fi

  local selection=${commits[$((response - 1))]}
  printf "%s\n" "Selected: $selection"
  gna "$selection"
}

In a nutshell, the above allows you to acquire the SHAs of all commits on the current branch so you can view and select which commit you want to add a note to. Super handy! Anyway, you can find all of the above and more via my Dotfiles project if you want to pilfer and tweak for your own needs.

Rebase

By default, when using Git Rebase, your notes will be orphaned since the SHA of the original commit will change. To prevent this behavior, you can add the following global configuration:

[notes]
  rewriteRef = refs/notes/commits

The above will ensure your notes are not lost, even when rebasing. For the astute documentation readers, you’ll notice the above is also the default reference for notes but you must set this configuration, though identical to the default, or rebasing with notes won’t work.

Push

When pushing for the first time on a new feature branch, you’ll see the following:

To https://github.com/bkuhlmann/test
   5811bd44c32b..0f2422597c5d  refs/notes/commits -> refs/notes/commits

…​but neither the branch or the notes will be pushed up. GitHub doesn’t report the truth.

When rebasing, you can use git push --force-with-lease as you’ll end up with the following error:

To https://github.com/bkuhlmann/test
 ! [rejected]                  refs/notes/commits -> refs/notes/commits (stale info)
error: failed to push some refs to 'https://github.com/bkuhlmann/test'

You have to drop the --force-with-lease flag.

GitHub

When using GitHub, the following limitations and restrictions are important to be aware of when using Git commit notes via GitHub:

  • Size Limit: Notes are limited to 1MB in size per commit.

  • Visibility: Notes are only visible to users with at least read access to the repo.

  • Format: ASCII Doc or Markdown formatting is not rendered on GitHub.

  • References: You can only push 'refs/notes/*' namespaced note refs.

  • Editing: Notes cannot be edited on GitHub, only through local Git commands.

  • Deleting: To delete notes, you must delete the note ref in Git then force push.

  • Search: Notes are not indexed for search on GitHub, only viewable from commits.

  • Pull Requests: Notes do not show up in pull request diffs or merge commits.

To be clear, this isn’t specific to GitHub but other services like GitLab, Bitbucket, etc. In truth, you shouldn’t rely on these services since Git is more feature rich. Even better, you can use tooling like, Milestoner, as explained below.

Milestoner

Milestoner shines where GitHub fails because Milestoner enhances what Git provides by default by allowing you to view your commits and associated notes in multiple formats, locally, for quick review. The following is an example of using Milestoner to check the status of a project which has not been versioned yet (i.e. milestoner build --format web):

Usage

Whenever you add notes to your commit (i.e. git notes add <sha>) and rebuild your release notes, Milestoner will reflect those changes within the UI. 🎉

Conclusion

I hope you’ve enjoyed this look into using Git Notes so you can get the most out of Git and even enhance your workflow with additional tooling like Bash alliases/functions, Milestoner, and more. Enjoy!