No description
- Rust 99.1%
- Shell 0.8%
- Just 0.1%
|
Some checks failed
CI / check (push) Has been cancelled
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> |
||
|---|---|---|
| .beads | ||
| .forgejo/workflows | ||
| notedb-cli | ||
| notedb-core | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| Justfile | ||
| README.md | ||
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 --
NoteDbUpdateManagerapplies 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, andChangeMessage - 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