# Notes

> Attach annotated notes to commits via RepositoryNotes and NotesRef.

Git notes are auxiliary commits under `refs/notes/*`. The [`notes`](https://docs.rs/grit-lib/latest/grit_lib/notes/index.html) module defines [`NotesRef`](https://docs.rs/grit-lib/latest/grit_lib/notes/struct.NotesRef.html) (for example [`NotesRef::commits`](https://docs.rs/grit-lib/latest/grit_lib/notes/struct.NotesRef.html), or [`NotesRef::parse`](https://docs.rs/grit-lib/latest/grit_lib/notes/struct.NotesRef.html) for a short name like `review`), tree helpers, and [`resolve_configured_notes_ref`](https://docs.rs/grit-lib/latest/grit_lib/notes/fn.resolve_configured_notes_ref.html) for `GIT_NOTES_REF` / `core.notesRef`.

[`Repository::notes`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html) exposes [`RepositoryNotes`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/struct.RepositoryNotes.html):

- `add` attaches a note and returns a [`NoteAddOutcome`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/enum.NoteAddOutcome.html) (`Added` or `Replaced`). Without `force`, an existing note is [`Error::NoteAlreadyExists`](https://docs.rs/grit-lib/latest/grit_lib/error/enum.Error.html). The content bytes are stored as given.
- `read` returns one note's bytes, `list` returns `(object, note blob)` pairs as [`NoteListing`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/struct.NoteListing.html), and `list_with_bodies` returns every [`Note`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/struct.Note.html) with its contents, sorted by object. A missing or non-blob note object is an error, never an empty note.
- [`remove_many`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/struct.RepositoryNotes.html) removes several notes in one notes commit.
- `plan_add` and `plan_remove_many` preview a write without touching objects, refs, or reflogs. [`NoteAddPlan`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/struct.NoteAddPlan.html) and [`NoteRemoveManyPlan`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/struct.NoteRemoveManyPlan.html) report the notes ref tip before the write and the notes commit the write would create. A batch plan applies every removal to one tree, so its predicted tip is the commit `remove_many` writes for the same objects and options.

Writes take an explicit committer and optional author ([`Signature`](https://docs.rs/grit-lib/latest/grit_lib/ident/struct.Signature.html) values, usually from `repo.identity`) and a commit message through [`NotesWriteOptions`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/struct.NotesWriteOptions.html). Each write is one notes commit on top of the tip the existing notes were read from. The ref moves through the repository's ref store as a compare-and-swap update from that same tip, so it works the same on loose/packed refs, reftable, or an injected [`MemoryRefStore`](https://docs.rs/grit-lib/latest/grit_lib/refs/store/struct.MemoryRefStore.html), and an `add` or `remove_many` that races another notes writer fails instead of dropping the other writer's note. The lower-level [`write_notes_commit`](https://docs.rs/grit-lib/latest/grit_lib/notes/fn.write_notes_commit.html) replaces the whole tree with the entries you pass, so callers that read entries themselves own that race. The reflog line (`notes: <message>`) is written in the same transaction whenever the ref already has a reflog or `core.logAllRefUpdates` allows creating one.

A notes ref that points at something other than a notes commit or tree fails with `Error::NotesRefNotCommit`, and a malformed notes tree fails with `Error::NotesObjectKind`.

## Example

```rust
//! Attach a note to a commit and list notes with [`RepositoryNotes`](https://grit-scm.com/docs/library/notes/grit_lib::repo_handles::RepositoryNotes/index.md).
//!
//! Source for the library guide "Notes" page (included in the docs site).

use grit_lib::config::environment::RepositoryOptions;
use grit_lib::notes::NotesRef;
use grit_lib::objects::ObjectId;
use grit_lib::repo::Repository;
use grit_lib::repo_handles::{NoteAddOutcome, NotesWriteOptions};
use grit_lib::state::resolve_head;
use std::path::{Path, PathBuf};

const IDENT: &str = "Library Guide <guide@grit-scm.com> 1735689600 +0000";

fn open_repo(root: &Path) -> Result<Repository, grit_lib::error::Error> {
    let git_dir = if root.join(".git").is_dir() {
        root.join(".git")
    } else {
        root.to_path_buf()
    };
    let work_tree = if root.join(".git").is_dir() {
        Some(root)
    } else {
        None
    };
    let mut opts = RepositoryOptions::from_process();
    if let Some(wt) = work_tree {
        opts = opts.with_work_tree(wt);
    }
    Repository::open(&git_dir, &opts)
}

fn main() -> Result<(), grit_lib::error::Error> {
    let root = std::env::args()
        .nth(1)
        .map(PathBuf::from)
        .or_else(|| std::env::current_dir().ok())
        .ok_or_else(|| grit_lib::error::Error::Message("missing repository path".into()))?;
    let repo = open_repo(&root)?;

    let head = resolve_head(repo.git_dir())?;
    let commit: ObjectId = head
        .oid()
        .cloned()
        .ok_or_else(|| grit_lib::error::Error::Message("HEAD has no commit".into()))?;

    let notes_ref = NotesRef::commits();
    let outcome = repo.notes().add(
        &notes_ref,
        &commit,
        b"library guide note\n",
        &NotesWriteOptions {
            message: "add note".into(),
            committer: IDENT.parse()?,
            author: None,
        },
        false,
    )?;
    println!("added={}", outcome == NoteAddOutcome::Added);

    let body = repo.notes().read(&notes_ref, &commit)?.unwrap_or_default();
    let text = String::from_utf8_lossy(&body);
    println!("note={text}");

    for note in repo.notes().list_with_bodies(&notes_ref, None)?.items {
        println!(
            "{} {}",
            note.object.to_hex(),
            String::from_utf8_lossy(&note.body).trim_end()
        );
    }

    Ok(())
}
```

```bash
cargo run --bin guide_notes /path/to/repo
git notes show HEAD
```

The integration test compares the printed note body to `git notes show`.
