Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit 4cf0d90

Browse files
committed
feat: path-based metadata API following git-notes semantics
Redesign the metadata system to store entries as paths within Git tree objects, rather than mapping target OIDs directly to raw tree OIDs. This provides structured, hierarchical metadata analogous to git-notes but using trees instead of blobs. Library API (MetadataIndex trait): - metadata_show: list all entries in a target's metadata tree - metadata_add: insert a path with optional blob content - metadata_remove_paths: remove entries by glob pattern (with --keep inversion) - metadata_copy: copy metadata from one target to another - metadata_prune: remove entries for non-existent target objects - metadata_get_ref: return the resolved ref name - Retain low-level metadata_get, metadata_set, metadata_remove CLI subcommands (git-notes parity): - list: show all targets with metadata - show: display metadata entries for an object - add: add a path entry (-m, -F, stdin, --force, --allow-empty) - remove: remove by glob patterns (--keep for inversion) - copy: copy metadata between objects (-f for overwrite) - prune: remove stale entries (-n dry run, -v verbose) - get-ref: print the metadata ref name feat: resolve revisions (HEAD, refs, short OIDs) in all object arguments feat: glob matching for remove patterns (*, **, prefix match) Assisted-by: Zed (Claude Opus 4.6)
1 parent 938ec58 commit 4cf0d90

6 files changed

Lines changed: 1262 additions & 147 deletions

File tree

README.md

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,49 @@ Just like notes, metadata added to an object does not alter the object's history
3535

3636
## Usage
3737

38-
Given any blob (file), tree (folder), or commit, add metadata using `metadata add`.
39-
By default, `add` assumes you're adding metadata to `HEAD`.
40-
Alternatively, use the `--oid` option to specify a Git object identifier.
41-
To remove metadata for a particular object, use `metadata remove` and provide glob patterns which represent entries in the metadata tree to be deleted.
42-
Use the `--keep` option to instead specify patterns to keep.
38+
Metadata entries are paths (with optional blob content) stored in a Git tree object, associated with any target object (blob, tree, or commit) via a fanout ref.
39+
The command follows `git notes` semantics: `list`, `show`, `add`, `remove`, `copy`, `prune`, and `get-ref`.
40+
41+
<!-- rumdl-disable MD013 -->
42+
43+
```shell
44+
# Add a path entry to HEAD's metadata tree
45+
git metadata add labels/bug
46+
git metadata add review/status -m approved
47+
48+
# Add metadata to a specific object
49+
git metadata add labels/urgent abc1234
50+
51+
# Show all metadata entries for an object
52+
git metadata show # defaults to HEAD
53+
git metadata show abc1234
54+
55+
# List all targets that have metadata
56+
git metadata list
57+
58+
# Remove entries by glob pattern
59+
git metadata remove 'labels/*'
60+
git metadata remove 'labels/bug' -o abc1234
61+
62+
# Keep only matching entries (remove everything else)
63+
git metadata remove --keep 'review/**'
64+
65+
# Copy metadata from one object to another
66+
git metadata copy abc1234 def5678
67+
68+
# Remove metadata for objects that no longer exist
69+
git metadata prune
70+
git metadata prune -n # dry run
71+
72+
# Print the metadata ref name
73+
git metadata get-ref
74+
75+
# Use a custom ref
76+
git metadata --ref refs/metadata/custom add labels/bug
77+
```
78+
79+
<!-- rumdl-enable MD013 -->
80+
4381
For more information, see `git metadata --help`.
4482

4583
## Installation

src/cli.rs

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,46 +15,98 @@ pub struct Cli {
1515
#[arg(short = 'C', long, global = true)]
1616
pub repo: Option<PathBuf>,
1717

18-
/// The ref under which metadata is stored (e.g. `refs/metadata/commits`).
19-
#[arg(short, long, global = true, default_value = "refs/metadata/commits")]
20-
pub ref_name: String,
18+
/// The ref under which metadata is stored.
19+
#[arg(long, global = true, default_value = "refs/metadata/commits")]
20+
pub r#ref: String,
2121

2222
#[command(subcommand)]
2323
pub command: Command,
2424
}
2525

2626
#[derive(clap::Subcommand)]
2727
pub enum Command {
28-
/// List all entries in the metadata index.
28+
/// List all targets that have metadata.
2929
List,
3030

31-
/// Read the metadata tree OID attached to a target object.
32-
Get {
33-
/// The OID of the target object to look up.
34-
target: String,
31+
/// Show the metadata tree entries for an object.
32+
Show {
33+
/// The target object (OID or revision). Defaults to HEAD.
34+
#[arg(default_value = "HEAD")]
35+
object: String,
3536
},
3637

37-
/// Write or overwrite the metadata tree for a target object.
38-
Set {
39-
/// The OID of the target object.
40-
target: String,
38+
/// Add a path entry to an object's metadata tree.
39+
Add {
40+
/// The path to add (e.g. `labels/bug`, `review/status`).
41+
path: String,
4142

42-
/// The OID of the tree to associate with the target.
43-
tree: String,
43+
/// The target object (OID or revision). Defaults to HEAD.
44+
#[arg(default_value = "HEAD")]
45+
object: String,
4446

45-
/// Overwrite an existing entry without error.
47+
/// Content to store in the blob. Reads from stdin when omitted.
48+
#[arg(short, long)]
49+
message: Option<String>,
50+
51+
/// Read content from a file.
52+
#[arg(short = 'F', long, conflicts_with = "message")]
53+
file: Option<PathBuf>,
54+
55+
/// Overwrite an existing path without error.
4656
#[arg(short, long)]
4757
force: bool,
4858

49-
/// Fanout depth: number of 2-hex-char directory segments.
50-
/// 1 means `ab/cdef01...` (like git-notes). 2 means `ab/cd/ef01...`.
59+
/// Allow adding an entry with empty content.
60+
#[arg(long)]
61+
allow_empty: bool,
62+
63+
/// Fanout depth (number of 2-hex-char directory segments).
5164
#[arg(long, default_value_t = 1)]
5265
shard_level: u8,
5366
},
5467

55-
/// Remove the metadata entry for a target object.
68+
/// Remove path entries from an object's metadata tree.
5669
Remove {
57-
/// The OID of the target object to remove.
58-
target: String,
70+
/// Glob patterns for entries to remove (or keep with `--keep`).
71+
patterns: Vec<String>,
72+
73+
/// The target object (OID or revision). Defaults to HEAD.
74+
#[arg(short, long, default_value = "HEAD")]
75+
object: String,
76+
77+
/// Invert: keep only entries matching the patterns.
78+
#[arg(long)]
79+
keep: bool,
80+
},
81+
82+
/// Copy metadata from one object to another.
83+
Copy {
84+
/// The source object (OID or revision).
85+
from: String,
86+
87+
/// The destination object (OID or revision).
88+
to: String,
89+
90+
/// Overwrite existing metadata on the destination.
91+
#[arg(short, long)]
92+
force: bool,
93+
94+
/// Fanout depth (number of 2-hex-char directory segments).
95+
#[arg(long, default_value_t = 1)]
96+
shard_level: u8,
5997
},
98+
99+
/// Remove metadata for objects that no longer exist.
100+
Prune {
101+
/// Only report what would be pruned; do not actually remove.
102+
#[arg(short = 'n', long)]
103+
dry_run: bool,
104+
105+
/// Print each pruned object.
106+
#[arg(short, long)]
107+
verbose: bool,
108+
},
109+
110+
/// Print the metadata ref name.
111+
GetRef,
60112
}

src/exe.rs

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::path::Path;
22

33
use git2::{Oid, Repository};
44

5-
use crate::{MetadataIndex, MetadataOptions};
5+
use git_metadata::{MetadataEntry, MetadataIndex, MetadataOptions};
66

77
/// Open a repository from the given path, or from the environment / current
88
/// directory when `None` is passed.
@@ -13,37 +13,71 @@ pub fn open_repo(path: Option<&Path>) -> Result<Repository, git2::Error> {
1313
}
1414
}
1515

16-
/// List all entries in the metadata index at `ref_name`.
17-
///
18-
/// Returns `(target_oid, tree_oid)` pairs. An empty `Vec` means no entries
19-
/// are stored under that ref.
16+
/// Resolve a revision string (OID, ref, or `HEAD`) to an [`Oid`].
17+
pub fn resolve_oid(repo: &Repository, rev: &str) -> Result<Oid, git2::Error> {
18+
// Try parsing as a full hex OID first.
19+
if let Ok(oid) = Oid::from_str(rev) {
20+
return Ok(oid);
21+
}
22+
// Fall back to rev-parse.
23+
let obj = repo.revparse_single(rev)?;
24+
Ok(obj.id())
25+
}
26+
27+
/// List all targets that have metadata under `ref_name`.
2028
pub fn list(repo: &Repository, ref_name: &str) -> Result<Vec<(Oid, Oid)>, git2::Error> {
2129
repo.metadata_list(ref_name)
2230
}
2331

24-
/// Read the metadata tree OID attached to `target` under `ref_name`.
25-
///
26-
/// Returns `None` if no entry exists for `target`.
27-
pub fn get(repo: &Repository, ref_name: &str, target: &Oid) -> Result<Option<Oid>, git2::Error> {
28-
repo.metadata_get(ref_name, target)
32+
/// Show the metadata tree entries for `target`.
33+
pub fn show(
34+
repo: &Repository,
35+
ref_name: &str,
36+
target: &Oid,
37+
) -> Result<Vec<MetadataEntry>, git2::Error> {
38+
repo.metadata_show(ref_name, target)
2939
}
3040

31-
/// Write or overwrite the metadata tree for `target` under `ref_name`.
32-
///
33-
/// Returns the new root tree OID committed under `ref_name`.
34-
pub fn set(
41+
/// Add a path entry (with optional content) to a target's metadata tree.
42+
pub fn add(
3543
repo: &Repository,
3644
ref_name: &str,
3745
target: &Oid,
38-
tree: &Oid,
46+
path: &str,
47+
content: Option<&[u8]>,
3948
opts: &MetadataOptions,
4049
) -> Result<Oid, git2::Error> {
41-
repo.metadata_set(ref_name, target, tree, opts)
50+
repo.metadata_add(ref_name, target, path, content, opts)
51+
}
52+
53+
/// Remove path entries matching `patterns` from a target's metadata tree.
54+
pub fn remove_paths(
55+
repo: &Repository,
56+
ref_name: &str,
57+
target: &Oid,
58+
patterns: &[&str],
59+
keep: bool,
60+
) -> Result<bool, git2::Error> {
61+
repo.metadata_remove_paths(ref_name, target, patterns, keep)
62+
}
63+
64+
/// Copy metadata from one target to another.
65+
pub fn copy(
66+
repo: &Repository,
67+
ref_name: &str,
68+
from: &Oid,
69+
to: &Oid,
70+
opts: &MetadataOptions,
71+
) -> Result<Oid, git2::Error> {
72+
repo.metadata_copy(ref_name, from, to, opts)
73+
}
74+
75+
/// Remove metadata for targets whose objects no longer exist.
76+
pub fn prune(repo: &Repository, ref_name: &str, dry_run: bool) -> Result<Vec<Oid>, git2::Error> {
77+
repo.metadata_prune(ref_name, dry_run)
4278
}
4379

44-
/// Remove the metadata entry for `target` under `ref_name`.
45-
///
46-
/// Returns `true` if an entry was removed, `false` if no entry existed.
47-
pub fn remove(repo: &Repository, ref_name: &str, target: &Oid) -> Result<bool, git2::Error> {
48-
repo.metadata_remove(ref_name, target)
80+
/// Return the metadata ref name.
81+
pub fn get_ref(repo: &Repository, ref_name: &str) -> String {
82+
repo.metadata_get_ref(ref_name)
4983
}

0 commit comments

Comments
 (0)