A small, fast Go CLI that turns git notes into reviewable, line-anchored comments on a commit — then posts them as GitHub PR / GitLab MR review comments.
Annotate code while you read it, browse your notes in a fuzzy-searchable TUI, and push them to a pull/merge request when you're done — without leaving the terminal.
- 📌 Line, block, or general notes — annotate a single line, a range (
file:1-17), a whole file, or the commit itself. - 🧷 Code capture — each note snapshots the source at its location, as of the commit (falling back to the working tree), so the context travels with the note.
- 🔍 Interactive TUI —
listopens a fuzzy-searchable picker (powered by Bubble Tea + Lip Gloss) with a live preview; pick a note to see its full detail.edit,remove, andunsubmitopen the same picker when you omit the index, so you never have to look up a note number. - 🚀 Post to PR/MR —
submitclassifies each note against the PR/MR diff and posts in-diff notes as line comments (true multi-line ranges) and the rest as general comments, via thegh/glabCLIs. - ✅ Submit-once tracking — every posted note is flagged
submitted, so re-runningsubmitskips it and never double-posts.unsubmitclears the flag. - 🗂️ Plain, portable storage — notes are CSV rows in the standard
refs/notes/commitsref; inspectable with plaingit, scriptable from any editor. - 🧭 Any commit — every command works on
HEADby default, or any commit with-c, --commit.
- git — notes are stored in
refs/notes/commits. - Go >= 1.26 — only to build from source (not needed for the Homebrew install).
gh(GitHub) orglab(GitLab) — (optional) only required forsubmit, installed and authenticated.
Install with Homebrew (via the tap):
brew install ideaspaper/tap/gitnotesOr build from source:
git clone https://github.com/ideaspaper/gitnotes.git
cd gitnotes
make install # builds with the version stamped in, installs to $GOPATH/bin
# or just: make build -> ./gitnotesVerify it's on your PATH:
gitnotes version| Command | Description |
|---|---|
gitnotes add -f <loc> -n <note> |
Add a line / block / whole-file note |
gitnotes add -g -n <note> |
Add a general (commit-level) note |
gitnotes list |
Browse notes in a fuzzy-searchable TUI (plain text when piped) |
gitnotes edit [index] [-n <note>] |
Edit a note's text (interactive picker if index omitted) |
gitnotes remove [index] | -a |
Remove one note (or all with -a) |
gitnotes submit <number> [--github|--gitlab] [--dry-run] |
Post notes to PR/MR <number> |
gitnotes unsubmit [index] | -a |
Clear a note's submitted flag so submit posts it again |
gitnotes export [-o <file>] |
Write HEAD's notes as a Markdown review |
gitnotes version |
Print the version |
Commands act on
HEADby default. Pass-c, --commit <commitish>to target another commit (e.g.gitnotes edit -c <sha> 1 -n "…"). This applies toadd,list,edit,remove, andunsubmit;submitandexportalways operate onHEAD.
| Spec | Meaning |
|---|---|
path/to/file.go:14 |
a single line |
path/to/file.go:1-17 |
a block of lines (1 through 17) |
path/to/file.go |
the whole file (no code captured) |
gitnotes add -f internal/cli/commands.go:42 -n "use slog here"
gitnotes add -f internal/note/entry.go:20-34 -n "this block needs a doc comment"
gitnotes add -g -n "overall LGTM, two nits inline"
gitnotes list # fuzzy-search, preview, and inspect notes
gitnotes edit # pick a note interactively, then edit it
gitnotes remove 1
gitnotes submit 42 --dry-run # preview every payload without posting
gitnotes submit 42 # post to PR/MR #42Each commit's note is a CSV document with one row per entry:
file,startLine,endLine,code,note,submitted
pkg/config/config.go:1-17→pkg/config/config.go,1,17,<captured code>,<note>,pkg/config/config.go:14→pkg/config/config.go,14,,<captured code>,<note>,- a general note →
,,,,<note>,
The code column holds the source captured from the file as of the commit; multi-line code is CSV-quoted, so it round-trips losslessly. The submitted column is true once the entry has been posted to a PR/MR. Notes live in the standard refs/notes/commits ref.
submit requires the PR/MR number and derives the diff base from it — GitHub's base branch (e.g. origin/main), GitLab's diff_refs.base_sha — so there's no -b flag. It then classifies each note against that diff:
-
A note whose lines are all inside the diff becomes a line comment. A range (
file:1-17) posts as a true multi-line comment — GitHub viastart_line/line, GitLab via aline_range— anchored across the block. -
Any other note becomes a general PR/issue comment (GitLab: an MR note), rendered as its location, a blank line, then the note:
path/to/file.go:10-14 the note text
Each posted entry is flagged submitted, so re-running submit skips it (• … already submitted, skipping) and only posts new notes — a --dry-run never sets the flag, and unsubmit clears it.
submit auto-detects the platform from the origin remote (override with --github/--gitlab) and shells out to gh / glab. Use --dry-run to print every payload without posting.
gitnotes export [-o <file>] writes HEAD's notes as a readable Markdown review (default git-notes.md) — one section per note with its location, the captured code in a fenced block, and the note text:
# Review notes (a1b2c3d — Fix the parser)
2 note(s).
## #1 — `internal/cli/cli.go:14-20`
```go
const appName = "gitnotes"
```
needs a doc comment here
## #2 — `(general)`
overall LGTMThis repo carries a Claude Code skill definition at .claude/skills/gitnotes/SKILL.md. Install it into your user skills folder so the agent can drive gitnotes while reviewing any codebase — recording findings as line-anchored notes, listing/editing them, and submitting to a PR/MR (invoke with /gitnotes):
mkdir -p ~/.claude/skills
cp -r .claude/skills/gitnotes ~/.claude/skills/It teaches the agent the non-interactive command forms (explicit index, -n, reading notes via list), since an agent has no TTY and the interactive picker needs one.
See LICENSE.