One of the cool things about Git is that it has strong cryptographic integrity. If you change any bit in the commit data or any of the files it keeps, all the checksums change, including the commit SHA and every commit SHA since that one. However, that means that in order to amend the commit in any way, for instance to add some comments on something or even sign off on a commit, you have to change the SHA of the commit itself.
Wouldn’t it be nice if you could add data to a commit without changing its SHA? If only there existed an external mechanism to attach data to a commit without modifying the commit message itself. Happy day! It turns out there exists just such a feature in newer versions of Git! As we can see from the Git 1.6.6 release notes where this new functionality was first introduced:
* "git notes" command to annotate existing commits.
Need any more be said? Well, maybe. How do you use it? What does it do? How can it be useful? I’m not sure I can answer all of these questions, but let’s give it a try. First of all, how does one use it?
Well, to add a note to a specific commit, you only need to run
git notes add [commit]
, like this:
$ git notes add HEAD
This will open up your editor to write your commit message. You can also use
the -m
option to provide the note right on the command line:
$ git notes add -m 'I approve - Scott' master~1
That will add a note to the first parent on the last commit on the master
branch. Now, how to view these notes? The easiest way is with the git log
command.
$ git log master
commit 0385bcc3bc66d1b1ec07346c237061574335c3b8
Author: Ryan Tomayko <rtomayko@gmail.com>
Date: Tue Jun 22 20:09:32 2010 -0700
yield to run block right before accepting connections
commit 06ca03a20bb01203e2d6b8996e365f46cb6d59bd
Author: Ryan Tomayko <rtomayko@gmail.com>
Date: Wed May 12 06:47:15 2010 -0700
no need to delete these header names now
Notes:
I approve - Scott
You can see the notes appended automatically in the log output. You can only have one note per commit in a namespace though (I will explain namespaces in the next section), so if you want to add a note to that commit, you have to instead edit the existing one. You can either do this by running:
$ git notes edit master~1
Which will open a text editor with the existing note so you can edit it:
I approve - Scott
#
# Write/edit the notes for the following object:
#
# commit 06ca03a20bb01203e2d6b8996e365f46cb6d59bd
# Author: Ryan Tomayko <rtomayko@gmail.com>
# Date: Wed May 12 06:47:15 2010 -0700
#
# no need to delete these header names now
#
# kidgloves.rb | 2 --
# 1 files changed, 0 insertions(+), 2 deletions(-)
~
~
~
".git/NOTES_EDITMSG" 13L, 338C
Sort of weird, but it works. If you just want to add something to the end of
an existing note, you can run git notes append SHA
, but only in newer
versions of Git (I think 1.7.1 and above).
Since you can only have one note per commit, Git allows you to have multiple namespaces for your notes. The default namespace is called ‘commits’, but you can change that. Let’s say we’re using the ‘commits’ notes namespace to store general comments but we want to also store bugzilla information for our commits. We can also have a ‘bugzilla’ namespace. Here is how we would add a bug number to a commit under the bugzilla namespace:
$ git notes --ref=bugzilla add -m 'bug #15' 0385bcc3
However, now you have to tell Git to specifically look in that namespace:
$ git log --show-notes=bugzilla
commit 0385bcc3bc66d1b1ec07346c237061574335c3b8
Author: Ryan Tomayko <rtomayko@gmail.com>
Date: Tue Jun 22 20:09:32 2010 -0700
yield to run block right before accepting connections
Notes (bugzilla):
bug #15
commit 06ca03a20bb01203e2d6b8996e365f46cb6d59bd
Author: Ryan Tomayko <rtomayko@gmail.com>
Date: Wed May 12 06:47:15 2010 -0700
no need to delete these header names now
Notes:
I approve - Scott
Notice that it also will show your normal notes. You can actually have it show
notes from all your namespaces by running git log --show-notes=*
- if you
have a lot of them, you may want to just alias that. Here is what your log
output might look like if you have a number of notes namespaces:
$ git log -1 --show-notes=*
commit 0385bcc3bc66d1b1ec07346c237061574335c3b8
Author: Ryan Tomayko <rtomayko@gmail.com>
Date: Tue Jun 22 20:09:32 2010 -0700
yield to run block right before accepting connections
Notes:
I approve of this, too - Scott
Notes (bugzilla):
bug #15
Notes (build):
build successful (8/13/10)
You can also switch the current namespace you’re using so that the default for
writing and showing notes is not ‘commits’ but, say, ‘bugzilla’ instead. If you
export the variable GIT_NOTES_REF
to point to something different, then the
--ref
and --show-notes
options are not neccesary. For example:
$ export GIT_NOTES_REF=refs/notes/bugzilla
That will set your default to ‘bugzilla’ instead. It has to start with the ‘refs/notes/’ though.
Now, here is where the general usability of this really breaks down. I am hoping that this will be improved in the future and I put off writing this post because of my concern with this phase of the process, but I figured it has interesting enough functionality as-is that someone might want to play with it.
So, the notes (as you may have noticed in the previous section) are stored as
references, just like branches and tags. This means you can push them to a
server. However, Git has a bit of magic built in to expand a branch name
like ‘master’ to what it really is, which is ‘refs/heads/master’. Unfortunately,
Git has no such magic built in for notes. So, to push your notes to a server,
you cannot simply run something like git push origin bugzilla
. Git will do
this:
$ git push origin bugzilla
error: src refspec bugzilla does not match any.
error: failed to push some refs to 'git@github.com:schacon/kidgloves.git'
However, you can push anything under ‘refs/’ to a server, you just need to be more explicit about it. If you run this it will work fine:
$ git push origin refs/notes/bugzilla
Counting objects: 3, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 263 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@github.com:schacon/kidgloves.git
* [new branch] refs/notes/bugzilla -> refs/notes/bugzilla
In fact, you may want to just make that git push origin refs/notes/*
which
will push all your notes. This is what Git does normally for something like
tags. When you run git push origin --tags
it basically expands to
git push origin refs/tags/*
.
Unfortunately, getting notes is even more difficult. Not only is there no
git fetch --notes
or something, you have to specify both sides of the
refspec (as far as I can tell).
$ git fetch origin refs/notes/*:refs/notes/*
remote: Counting objects: 12, done.
remote: Compressing objects: 100% (8/8), done.
remote: Total 12 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (12/12), done.
From github.com:schacon/kidgloves
* [new branch] refs/notes/bugzilla -> refs/notes/bugzilla
That is basically the only way to get them into your repository from the server.
Yay. If you want to, you can setup your Git config file to automatically pull
them down though. If you look at your .git/config
file you should have a
section that looks like this:
[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*
url = git@github.com:schacon/kidgloves.git
The ‘fetch’ line is the refspec of what Git will try to do if you run just
git fetch origin
. It contains the magic formula of what Git will fetch and
store local references to. For instance, in this case it will take every
branch on the server and give you a local branch under ‘remotes/origin/’ so
you can reference the ‘master’ branch on the server as ‘remotes/origin/master’
or just ‘origin/master’ (it will look under ‘remotes’ when it’s trying to
figure out what you’re doing). If you change that line to
fetch = +refs/heads/*:refs/remotes/manamana/*
then even though your remote
is named ‘origin’, the master branch from your ‘origin’ server will be under
‘manamana/master’.
Anyhow, you can use this to make your notes fetching easier. If you add
multiple fetch
lines, it will do them all. So in addition to the current
fetch
line, you can add a line that looks like this:
fetch = +refs/notes/*:refs/notes/*
Which says also get all the notes references on the server and store them as though they were local notes. Or you can namespace them if you want, but that can cause issues when you try to push them back again.
Now, this is where the main problem is. Merging notes is super difficult.
This means that if you pull down someone’s notes, you edit any note in a
namespace locally and the other developer edits any note in that same namespace,
you’re going to have a hard time getting them back in sync. When the second
person tries to push their notes it will look like a non-fast-forward just
like a normal branch update, but unline a normal branch you can’t just run
git pull
and then try again. You have to check out your notes ref as if it
were a normal branch, which will look ridiculously confusing and then do the
merge and then switch back. It is do-able, but probably not something you
really want to do.
Because of this, it’s probably best to namespace your notes or better just have an automated process create them (like build statuses or bugzilla artifacts). If only one entity is updating your notes, you won’t have merge issues. However, if you want to use them to comment on commits within a team, it is going to be a bit painful.
So far, I’ve heard of people using them to have their ticketing system attach
metadata automatically or have
a system
attach associated mailing list emails
to commits they concern.
Other people just use them entirely locally without pushing them
anywhere to store reminders for themselves and whatnot.
Probably a good start, but the ambitious among you may come up with
something else interesting to do. Let me know!