No description
  • Rust 99.1%
  • Shell 0.8%
  • Just 0.1%
Find a file
George Shammas 82ad426d0d
Some checks failed
CI / check (push) Has been cancelled
Add account and project CLI subcommands
Expose account-level operations (starred changes, draft comments,
account sequences) and project-level operations (info, change listing,
zombie draft cleanup, project sequences, ref summary) as nested CLI
commands, backed by existing library functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 16:13:51 -05:00
.beads Update beads: close CI and docs issues 2026-02-19 22:01:55 -05:00
.forgejo/workflows Add CI, Justfile, API docs, fix clippy warnings and formatting 2026-02-19 22:01:46 -05:00
notedb-cli Add account and project CLI subcommands 2026-02-20 16:13:51 -05:00
notedb-core Add CI, Justfile, API docs, fix clippy warnings and formatting 2026-02-19 22:01:46 -05:00
.gitignore Initialize Cargo workspace with notedb-core and notedb-cli crates 2026-02-19 19:28:32 -05:00
AGENTS.md init beads 2026-02-19 18:55:32 -05:00
Cargo.lock Update Cargo.lock and beads issue tracker state 2026-02-19 20:09:19 -05:00
Cargo.toml Add core workspace dependencies 2026-02-19 19:30:25 -05:00
CLAUDE.md Add CLAUDE.md and update README.md 2026-02-20 06:02:49 -05:00
Justfile Add CI, Justfile, API docs, fix clippy warnings and formatting 2026-02-19 22:01:46 -05:00
README.md Add CLAUDE.md and update README.md 2026-02-20 06:02:49 -05:00

notedb-rs

A Rust implementation of Gerrit's NoteDB -- a git-backed database for storing code review metadata. This project provides both a library crate (notedb-core) and a CLI tool (notedb-cli) for reading, writing, and inspecting NoteDB data.

Data written by this implementation is wire-compatible with the Java implementation in Gerrit, meaning repositories written by Rust can be read by Java and vice versa.

Features

  • Read & write changes -- load change metadata, patch sets, approvals, reviewers, comments, attention set, and submit requirements from git
  • Wire compatibility -- commit message footers, JSON comment blobs, NoteMap tree layout, and ref naming all match the Java implementation
  • Native Rust git -- uses gitoxide (gix) instead of shelling out to git or binding to libgit2
  • Atomic batch updates -- NoteDbUpdateManager applies multiple change updates atomically via batched ref transactions
  • Draft comments -- read and write per-user draft comments from refs/draft-comments/ refs
  • Starred changes -- query starred change status per account or per change
  • Sequence counters -- atomic auto-incrementing counters (change IDs, account IDs) stored as integer blobs with compare-and-swap
  • History rewriters -- logical deletion of comments and change messages by rewriting commit history
  • Zombie draft cleanup -- detect and remove orphaned draft refs where all comments are already published
  • LRU caching -- thread-safe cache for parsed change state, with automatic invalidation when refs change
  • Builder APIs -- ergonomic builders for HumanComment, PatchSet, PatchSetApproval, and ChangeMessage
  • AllUsers batching -- batch operations on account-level data (drafts, starred changes, external IDs)
  • Full CLI -- inspect and manipulate NoteDB repositories from the command line with text and JSON output
  • CI -- Forgejo CI with formatting, clippy, and test checks

Quick Start

Building

cargo build --workspace

Running Tests

cargo test --workspace

There are 256 tests covering parsing, serialization, wire compatibility, git operations, and end-to-end read/write roundtrips.

Development Commands

A Justfile is provided for common tasks:

just test          # Run all tests
just lint          # Clippy with -D warnings
just fmt           # Auto-format
just fmt-check     # Check formatting
just ci            # All CI checks (fmt + lint + test)
just build         # Release build
just doc           # Generate and open docs

CLI Usage

# List all changes in a repository
notedb-cli --repo /path/to/project.git list

# Show details of a specific change
notedb-cli --repo /path/to/project.git show 12345

# Show change with inline comments
notedb-cli --repo /path/to/project.git show 12345 --comments

# View the commit history of a change's meta ref
notedb-cli --repo /path/to/project.git log 12345

# Create a new change
notedb-cli --repo /path/to/project.git create \
  --subject "Add new feature" \
  --branch refs/heads/main \
  --change-id Iabcdef0123456789 \
  --owner 1000 \
  --commit deadbeef0123456789abcdef0123456789abcdef

# Add a vote
notedb-cli --repo /path/to/project.git vote 12345 \
  --label Code-Review --value 2 --account 1001

# Add an inline comment
notedb-cli --repo /path/to/project.git comment 12345 add \
  --file src/main.rs --line 42 --message "This needs a fix" --account 1001

# List inline comments
notedb-cli --repo /path/to/project.git comment 12345 list

# Manage sequences
notedb-cli --repo /path/to/project.git sequence init changes --seed 1
notedb-cli --repo /path/to/project.git sequence next changes
notedb-cli --repo /path/to/project.git sequence show changes

# JSON output (all commands)
notedb-cli --repo /path/to/project.git --format json list

Global CLI Options

Option Default Description
--repo . Path to the project git repository (bare)
--server-id gerrit Server ID used in account identity formatting
--format text Output format: text or json
-v, --verbose off Enable debug logging

Library Usage

Opening a Repository

use notedb_core::NoteDb;
use notedb_core::entities::{AccountId, ChangeId, ChangeStatus};

let db = NoteDb::open("/path/to/project.git", "my-server")?;

Reading Changes

// Load a single change
let notes = db.load_change(ChangeId(12345))?;
println!("Subject: {}", notes.change().subject);
println!("Status:  {}", notes.change().status);
println!("Owner:   {}", notes.change().owner);
println!("Branch:  {}", notes.change().dest.short_name());

// Access patch sets
for (num, ps) in notes.patch_sets() {
    println!("PS {}: {}", num, ps.commit_id);
}

// Access approvals
for approval in notes.approvals() {
    println!("{} {}{} by {}",
        approval.key.label_id.0,
        if approval.value > 0 { "+" } else { "" },
        approval.value,
        approval.key.account_id);
}

// Access reviewers
use notedb_core::entities::ReviewerState;
for account in notes.reviewers().by_state(ReviewerState::Reviewer) {
    println!("Reviewer: {}", account);
}

// Access comments
for (commit_id, comments) in notes.human_comments() {
    for comment in comments {
        println!("{}:{} - {}", comment.key.filename, comment.line_number, comment.message);
    }
}

// Scan all changes
let ids = db.scan_change_ids()?;
let all_changes = db.scan_changes()?;

Writing Changes

use chrono::Utc;

// Create a new change
let mut update = db.update_change(ChangeId(1), 1, AccountId(100), Utc::now());
update.set_branch("refs/heads/main".into());
update.set_change_key("Iabcdef0123456789".into());
update.set_subject("My new change".into());
update.set_status(ChangeStatus::New);
update.set_commit_id("deadbeef0123456789abcdef0123456789abcdef".into());

update.apply_and_update_ref(db.repo(), None, None)?;

// Update an existing change (add a vote)
let notes = db.load_change(ChangeId(1))?;
let parent_id = db.repo().resolve_ref("refs/changes/01/1/meta")?.unwrap();

let mut update = db.update_change(ChangeId(1), 1, AccountId(200), Utc::now());
update.put_approval("Code-Review", 2, AccountId(200));
update.put_reviewer(AccountId(200), ReviewerState::Reviewer);

update.apply_and_update_ref(db.repo(), Some(parent_id), None)?;

Batch Updates

let mut mgr = db.update_manager();

for i in 1..=5 {
    let mut update = db.update_change(ChangeId(i), 1, AccountId(100), Utc::now());
    update.set_branch("refs/heads/main".into());
    update.set_subject(format!("Change {}", i));
    update.set_status(ChangeStatus::New);
    update.set_commit_id(format!("{:040x}", i));
    mgr.add(update, None);
}

// All ref updates applied atomically
let commits = mgr.execute()?;

Draft Comments

use notedb_core::notes::{load_drafts, scan_drafts_for_change};

// Load drafts for a specific user on a change
let drafts = load_drafts(db.repo(), ChangeId(1), AccountId(100))?;

// Find all users with drafts on a change
let all_drafts = scan_drafts_for_change(db.repo(), ChangeId(1))?;
for (account_id, comments) in &all_drafts {
    println!("User {} has {} draft(s)", account_id, comments.len());
}

Starred Changes

use notedb_core::notes::{is_starred, starred_by_account, starred_by_change};

// Check if a change is starred by an account
let starred = is_starred(db.repo(), ChangeId(1), AccountId(100))?;

// Get all changes starred by an account
let changes = starred_by_account(db.repo(), AccountId(100))?;

// Get all accounts that starred a change
let accounts = starred_by_change(db.repo(), ChangeId(1))?;

Caching

use notedb_core::cache::ChangeNotesCache;

let cache = ChangeNotesCache::new(1000); // capacity of 1000 entries

// Load with automatic caching and invalidation
let state = cache.get_or_load(db.repo(), "my-project", ChangeId(1))?;
println!("Subject: {}", state.change.subject);

// Check cache stats
let stats = cache.stats();
println!("Hits: {}, Misses: {}", stats.hits, stats.misses);

Sequences

// Allocate the next change ID
let next_id = db.next_change_id()?;

Architecture

Crate Structure

notedbrs/
├── notedb-core/          # Library crate
│   └── src/
│       ├── notedb.rs     # NoteDb — main entry point
│       ├── entities/     # Data model (Change, PatchSet, HumanComment, etc.)
│       ├── git/          # Git operations via gitoxide
│       ├── notes/        # High-level read/write APIs
│       │   ├── change_notes.rs    # Read change state
│       │   ├── change_update.rs   # Write change metadata
│       │   ├── update_manager.rs  # Batch operations
│       │   ├── all_users_update.rs # Account-level batching
│       │   ├── starred_changes.rs # Starred change queries
│       │   ├── rewriters.rs       # History rewriting
│       │   ├── zombie_drafts.rs   # Draft cleanup
│       │   └── ...
│       ├── parsing/      # Commit message and footer parsing
│       ├── footers.rs    # Footer key constants
│       ├── builders.rs   # Entity builder patterns
│       ├── cache.rs      # LRU caching layer
│       └── error.rs      # Error types
├── notedb-cli/           # Binary crate
│   └── src/
│       ├── main.rs       # CLI entry point
│       └── commands/     # Subcommand implementations
├── Justfile              # Development commands
└── .forgejo/workflows/   # CI configuration

Module Overview

Module Description
entities All data types mirroring Java NoteDB entities. Serde-compatible with the Java Gson serialization format.
git Low-level git operations: repository access, ref naming, NoteMap (fanout tree), integer blobs, commit building, batch ref updates, atomic sequences.
notes High-level APIs: ChangeNotes (read), ChangeUpdate (write), NoteDbUpdateManager (batch), AllUsersUpdate (account ops), starred changes, history rewriters, zombie draft cleanup, draft comments, change scanning.
parsing Commit message footer parsing/writing, account identity formatting, label footer parsing, attention set JSON parsing.
footers Constants for all 29 NoteDB footer keys (Patch-set, Branch, Label, Attention, etc.).
builders Ergonomic builder patterns for HumanComment, PatchSet, PatchSetApproval, and ChangeMessage.
cache Thread-safe LRU cache for parsed change state with automatic invalidation.

NoteDB Data Model

NoteDB stores code review metadata in git:

  • Meta refs (refs/changes/XX/CCCCCC/meta) -- one commit chain per change, with metadata encoded as commit message footers
  • NoteMap trees -- JSON blobs in the commit tree, keyed by patch set commit SHA, containing inline comments and submit requirements
  • Draft refs (refs/draft-comments/XX/CCCCCC/AAAAAA) -- per-user draft comments stored separately
  • Starred refs (refs/starred-changes/XX/CCCCCC/AAAAAA) -- per-user starred change flags
  • Sequence refs (refs/sequences/changes, etc.) -- atomic counters stored as integer text blobs

Wire Compatibility

This implementation matches the Java NoteDB format:

Format Details
Commit footers JGit trailer convention: Key: value in the last paragraph
Account identity Gerrit User N <N@serverId> pseudonymized format
Label footers LabelName=+/-Value[, uuid][ Gerrit User N <N@server>][ :tag]
Attention footers JSON: {"account":N,"operation":"ADD|REMOVE","reason":"text"}
Comment JSON camelCase field names matching Gson output (patchSetId, lineNbr, etc.)
NoteMap layout 2-level fanout: aa/bbccddee... tree entries containing JSON blobs
Ref naming Sharded by change_id % 100: refs/changes/42/12342/meta

Dependencies

Dependency Purpose
gix Native Rust git implementation (gitoxide)
serde + serde_json JSON serialization for comments and submit requirements
chrono Timestamp handling compatible with Java Instant
thiserror Ergonomic error types
clap CLI argument parsing (CLI crate only)
tracing Structured logging

License

Apache-2.0