Store Code Discussions in Git using Git Notes

Code discussions contain relevant information. Isn’t it a shame that we keep these in the centralized GitHub/GitLab servers, far away from our decentralized Git code? As soon as we move provider, we’ll lose all old discussions! And how do you ever find the pull requests back from 5 years ago? Symfony has implemented a lightweight solution to this problem years ago using a less-known feature of Git: Git Notes.

Git Notes?

Meet one of Git’s unknown features: Git notes. They are hard to discover… and hard to use. Yet, they provide an interesting feature: Adding extra information to a commit, after you’ve created it.

Git stores all data using immutable objects. This means that you cannot change the objects after creation. For instance, when you change a commit’s message, Git will create a new object and remove the previous one. Git notes are useful when you want to preserve the existing Git object, but want to add extra data. Git notes have a many-to-one relationship with objects: one Git object (commit) can have multiple Git notes references it.

Enough talking, let’s see it in practice!

First, create a commit:

1
2
3
$ git commit --allow-empty -m "Playing with Git notes"
[main a63c1ff] Playing with Git notes
 Date: Mon Jul 8 21:18:00 2024 +0200

Now, we can add a note to this commit using the git notes command:

1
2
3
# 'a63c1ff' is the object (commit) to attach this note, it defaults
# to HEAD (i.e. the latest commit on the current branch)
$ git notes add -m "Extra information of this commit" a63c1ff

Great, we’ve added extra information to that commit! Now, we can view it by using the --notes option:

1
2
3
4
5
6
7
8
9
$ git log --notes
commit a63c1ffcf90a6ee3188a086b14eebf33087aebaa (HEAD -> main)
Author: Wouter de Jong <wouter@example.com>
Date:   Mon Jul 8 21:18:00 2024 +0200

    Playing with Git notes

Notes:
    Extra information of this commit

Cool! As you can see, the commit object didn’t change (the hash is the same as before), but we’ve attached the new note object.

Multiple Git Notes References

By default, notes are stored in the notes/commits ref. But you can categorize Git notes into multiple references, allowing you to add multiple types of notes to a single commit.

You can use the --ref option to specify the note tree:

1
2
3
4
5
6
7
8
9
10
$ git notes --ref=acceptance add -m "Tested-by: Jane doe <jane@example.com>"
$ git log --notes=acceptance
commit a63c1ffcf90a6ee3188a086b14eebf33087aebaa (HEAD -> main)
Author: Wouter de Jong <wouter@example.com>
Date:   Mon Jul 8 21:18:00 2024 +0200

    Playing with Git notes

Notes (acceptance):
    Tested-by: Jane doe <jane@example.com>

The ref/notes/acceptance now holds this new note. Using multiple references allows us to add multiple notes to a single commit (one per reference):

1
2
3
4
5
6
7
8
9
10
11
12
$ git log --notes=commits --notes=acceptance
commit a63c1ffcf90a6ee3188a086b14eebf33087aebaa (HEAD -> main)
Author: Wouter de Jong <wouter@example.com>
Date:   Mon Jul 8 21:18:00 2024 +0200

    Let's use Git notes

Notes:
    Extra information for this commit

Notes (acceptance):
    Tested-by: Jane doe <jane@example.com>

How Symfony uses Git Notes to store GitHub Discussions

Now we know a bit about Git notes, we can look at how we can use them to store code discussions and reviews.

Symfony maintainers use an internal CLI tool to merge contributions. Among other things it checks CI and core team approvals, and it helps us automatically re-target contributions to the correct version.

After merging a branch using Git, the tool also uses the GitHub API to fetch all pull request comments. The comments are stored in a Git Note and attached to the merge commit. This way, whenever you need to find the discussion for a particular feature, you can look up the merge commit and read it. Even if Symfony decides to switch to another platform one day.

Storing GitHub Comments in a Note

First, the tool fetches the pull request comments using the GitHub API via the KnpLabs GitHub API package:

1
2
3
4
5
6
7
8
9
10
use GitHub\Client;

$github = new Client();
$resultPager = new ResultPager($github);

$prComments = $resultPager->fetchAll(
    $github->issues()->comments(),
    'all',
    ['symfony', 'symfony', $prNumber]
);

Then, it loops over all comments and puts it in a temporary file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$notes = '';
foreach ($prComments as $comment) {
    $notes .= <<<EOF
---------------------------------------------------------------------------

by {$comment['user']['login']} at {$comment['created_at']}

{$comment['body']}


    EOF;
}

file_put_contents(getcwd().'/.notes', $notes);

Now, we can create a Git note from the file contents (Symfony puts them in a named “github-comments” reference):

1
2
3
4
5
# first, replace the local github-comments ref with the upstream one
$ git fetch origin refs/notes/github-comments:refs/notes/github-comments

# then, create the new note
$ git notes --ref=github-comments add --file=.notes

Finally, we can push the notes to GitHub. Git doesn’t push notes by default, we have to mention the reference explicitly:

1
$ git push --no-follow-tags origin refs/notes/github-comments

Reading the Notes back

GitHub no longer shows the refs/notes/... objects anywhere. But as it is just another set of objects (just like the commit tree), you can still push and fetch it from GitHub if you reference it directly:

1
2
3
4
$ git fetch origin refs/notes/github-comments:refs/notes/github-comments

# or fetch all note references
$ git fetch origin "refs/notes/*:refs/notes/*"

You can also edit the .git/config file of a project to always fetch notes by default:

1
2
3
  [remote "origin"]
    fetch = +refs/heads/*:refs/remotes/origin/*
+   fetch = +refs/notes/*:refs/notes/*

Now the notes exist locally, you can view them in the Git logs for instance:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$ git log --notes=github-comments
commit 1a16ebc32598faada074e0af12a6a698d2964a5e (HEAD -> 7.2, origin/7.2)
Merge: 09f9eb73b6 1303cd1a56
Author: Nicolas Grekas <nicolas.grekas@gmail.com>
Date:   Wed Jul 10 17:23:17 2024 +0200

    minor #57657 [Messenger] [SQS] Allow to pass `sslmode` as an option (spuf)

    This PR was merged into the 7.2 branch.

    Discussion
    ----------

    [Messenger] [SQS] Allow to pass `sslmode` as an option

    | Q             | A
    | ------------- | ---
    | Branch?       | 7.2
    | Bug fix?      | no
    | New feature?  | no
    | Deprecations? | no
    | Issues        |
    | License       | MIT

    This is a minor. There is an option "sslmode" that is documented as an option but can be passed only in dsn query string.

    Commits
    -------

    1303cd1a56 [Messenger][SQS] Allow to pass sslmode as an option

Notes (github-comments):
    ---------------------------------------------------------------------------

    by carsonbot at 2024-07-04T20:10:34Z

    Hey!

    I see that this is your first PR. That is great! Welcome!

    ...

Summary

  • It’s important to distribute more than just the code
  • Use Git notes to store all sorts of important information to commits
  • You can view old Symfony discussions offline by using the github-comments notes reference