From 425a7171756b44dc44b0eb2b9607c6a4e7a68730 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Mon, 30 Mar 2026 19:42:17 -0700 Subject: [PATCH 1/3] feat: add CommandContext for shared command state --- src/commands/config.rs | 32 ++++----- src/commands/config_prune.rs | 57 ++++++++++----- src/commands/get.rs | 26 ++++--- src/commands/import.rs | 59 +++++++-------- src/commands/inspect.rs | 14 ++-- src/commands/list.rs | 52 +++++--------- src/commands/local_prune.rs | 39 +++++----- src/commands/log.rs | 9 ++- src/commands/materialize.rs | 58 +++++++-------- src/commands/promisor.rs | 21 +++--- src/commands/prune.rs | 31 ++++---- src/commands/pull.rs | 22 +++--- src/commands/push.rs | 27 +++---- src/commands/remote.rs | 26 ++++--- src/commands/rm.rs | 19 ++--- src/commands/serialize.rs | 52 +++++++------- src/commands/set.rs | 81 +++++++++------------ src/commands/show.rs | 15 ++-- src/commands/stats.rs | 13 ++-- src/commands/teardown.rs | 16 +++-- src/commands/watch.rs | 4 +- src/context.rs | 135 +++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 23 files changed, 461 insertions(+), 348 deletions(-) create mode 100644 src/context.rs diff --git a/src/commands/config.rs b/src/commands/config.rs index d56b18d..a8c97c8 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -1,33 +1,30 @@ use anyhow::{bail, Result}; -use chrono::Utc; +use crate::context::CommandContext; use crate::db::Db; -use crate::git_utils; use crate::types::validate_key; const CONFIG_PREFIX: &str = "meta:"; pub fn run(list: bool, unset: bool, key: Option<&str>, value: Option<&str>) -> Result<()> { - let repo = git_utils::discover_repo()?; - let db_path = git_utils::db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_gix(None)?; if list { - return run_list(&db); + return run_list(&ctx.db); } if unset { let key = key.ok_or_else(|| anyhow::anyhow!("--unset requires a key"))?; validate_config_key(key)?; - return run_unset(&repo, &db, key); + return run_unset(&ctx, key); } let key = key.ok_or_else(|| anyhow::anyhow!("key is required"))?; validate_config_key(key)?; match value { - Some(val) => run_set(&repo, &db, key, val), - None => run_get(&db, key), + Some(val) => run_set(&ctx, key, val), + None => run_get(&ctx.db, key), } } @@ -43,19 +40,17 @@ fn validate_config_key(key: &str) -> Result<()> { Ok(()) } -fn run_set(repo: &gix::Repository, db: &Db, key: &str, value: &str) -> Result<()> { - let email = git_utils::get_email(repo)?; - let timestamp = Utc::now().timestamp_millis(); +fn run_set(ctx: &CommandContext, key: &str, value: &str) -> Result<()> { let stored_value = serde_json::to_string(value)?; - db.set( + ctx.db.set( "project", "", key, &stored_value, "string", - &email, - timestamp, + &ctx.email, + ctx.timestamp, )?; Ok(()) } @@ -86,11 +81,8 @@ fn run_list(db: &Db) -> Result<()> { Ok(()) } -fn run_unset(repo: &gix::Repository, db: &Db, key: &str) -> Result<()> { - let email = git_utils::get_email(repo)?; - let timestamp = Utc::now().timestamp_millis(); - - let removed = db.rm("project", "", key, &email, timestamp)?; +fn run_unset(ctx: &CommandContext, key: &str) -> Result<()> { + let removed = ctx.db.rm("project", "", key, &ctx.email, ctx.timestamp)?; if !removed { eprintln!("key '{}' not found", key); } diff --git a/src/commands/config_prune.rs b/src/commands/config_prune.rs index df4a430..2f1ce18 100644 --- a/src/commands/config_prune.rs +++ b/src/commands/config_prune.rs @@ -1,17 +1,13 @@ use anyhow::Result; -use chrono::Utc; use dialoguer::{Confirm, Input, Select}; use crate::commands::auto_prune::{parse_size, read_prune_rules}; -use crate::db::Db; -use crate::git_utils; +use crate::context::CommandContext; pub fn run() -> Result<()> { - let repo = git_utils::discover_repo()?; - let db_path = git_utils::db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_gix(None)?; - let existing = read_prune_rules(&db)?; + let existing = read_prune_rules(&ctx.db)?; if let Some(ref rules) = existing { println!("Current auto-prune configuration:"); @@ -173,27 +169,42 @@ pub fn run() -> Result<()> { } // -- write -- - let email = git_utils::get_email(&repo)?; - let ts = Utc::now().timestamp_millis(); - - set_config(&db, "meta:prune:since", &since, &email, ts)?; + set_config(&ctx, "meta:prune:since", &since)?; match max_keys { - Some(ref v) => set_config(&db, "meta:prune:max-keys", v, &email, ts)?, + Some(ref v) => set_config(&ctx, "meta:prune:max-keys", v)?, None => { - db.rm("project", "", "meta:prune:max-keys", &email, ts)?; + ctx.db.rm( + "project", + "", + "meta:prune:max-keys", + &ctx.email, + ctx.timestamp, + )?; } } match max_size { - Some(ref v) => set_config(&db, "meta:prune:max-size", v, &email, ts)?, + Some(ref v) => set_config(&ctx, "meta:prune:max-size", v)?, None => { - db.rm("project", "", "meta:prune:max-size", &email, ts)?; + ctx.db.rm( + "project", + "", + "meta:prune:max-size", + &ctx.email, + ctx.timestamp, + )?; } } match min_size { - Some(ref v) => set_config(&db, "meta:prune:min-size", v, &email, ts)?, + Some(ref v) => set_config(&ctx, "meta:prune:min-size", v)?, None => { - db.rm("project", "", "meta:prune:min-size", &email, ts)?; + ctx.db.rm( + "project", + "", + "meta:prune:min-size", + &ctx.email, + ctx.timestamp, + )?; } } @@ -201,9 +212,17 @@ pub fn run() -> Result<()> { Ok(()) } -fn set_config(db: &Db, key: &str, value: &str, email: &str, ts: i64) -> Result<()> { +fn set_config(ctx: &CommandContext, key: &str, value: &str) -> Result<()> { let stored = serde_json::to_string(value)?; - db.set("project", "", key, &stored, "string", email, ts)?; + ctx.db.set( + "project", + "", + key, + &stored, + "string", + &ctx.email, + ctx.timestamp, + )?; Ok(()) } diff --git a/src/commands/get.rs b/src/commands/get.rs index d3e5997..5b2fc2a 100644 --- a/src/commands/get.rs +++ b/src/commands/get.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result}; use git2::Repository; use serde_json::{json, Map, Value}; +use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; use crate::list_value::list_values_from_json; @@ -17,13 +18,12 @@ pub fn run( ) -> Result<()> { let mut target = Target::parse(target_str)?; - let repo = git_utils::git2_discover_repo()?; - target.git2_resolve(&repo)?; - let db_path = git_utils::git2_db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_git2(None)?; + ctx.git2_resolve_target(&mut target)?; + let repo = ctx.git2_repo()?; let include_target_subtree = target.target_type == TargetType::Path; - let mut entries = db.get_all_with_target_prefix( + let mut entries = ctx.db.get_all_with_target_prefix( target.type_str(), target.value_str(), include_target_subtree, @@ -33,10 +33,14 @@ pub fn run( // If no exact match, try prefix expansion for non-commit types // (commits are already resolved by git, but change-ids/branches may be partial) if entries.is_empty() && target.target_type != TargetType::Path { - let matches = db.find_target_values_by_prefix(target.type_str(), target.value_str(), 2)?; + let matches = + ctx.db + .find_target_values_by_prefix(target.type_str(), target.value_str(), 2)?; if matches.len() == 1 { let expanded = &matches[0]; - entries = db.get_all_with_target_prefix(target.type_str(), expanded, false, key)?; + entries = ctx + .db + .get_all_with_target_prefix(target.type_str(), expanded, false, key)?; if !entries.is_empty() { eprintln!("expanded to {}:{}", target.type_str(), expanded); } @@ -61,10 +65,10 @@ pub fn run( .collect(); if !promised.is_empty() { - let hydrated = hydrate_promised_entries(&repo, &db, target.type_str(), &promised)?; + let hydrated = hydrate_promised_entries(repo, &ctx.db, target.type_str(), &promised)?; if hydrated > 0 { // Re-query to get the now-resolved values - entries = db.get_all_with_target_prefix( + entries = ctx.db.get_all_with_target_prefix( target.type_str(), target.value_str(), include_target_subtree, @@ -80,7 +84,7 @@ pub fn run( .map( |(entry_target_value, key, value, value_type, is_git_ref, _)| { if is_git_ref { - let resolved_value = resolve_git_ref(&repo, &value)?; + let resolved_value = resolve_git_ref(repo, &value)?; // JSON-encode the resolved content to match normal string format let json_value = serde_json::to_string(&resolved_value)?; Ok((entry_target_value, key, json_value, value_type)) @@ -96,7 +100,7 @@ pub fn run( } if json_output { - print_json(&db, &target, &resolved, with_authorship)?; + print_json(&ctx.db, &target, &resolved, with_authorship)?; } else { print_plain(&target, &resolved, key.is_some() && resolved.len() == 1)?; } diff --git a/src/commands/import.rs b/src/commands/import.rs index 01e70c1..1f2e36d 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -1,12 +1,11 @@ use std::collections::HashSet; use anyhow::{bail, Context, Result}; -use chrono::Utc; use git2::Repository; use serde_json::Value; +use crate::context::CommandContext; use crate::db::Db; -use crate::git_utils; use crate::types::GIT_REF_THRESHOLD; pub fn run(format: &str, dry_run: bool, since: Option<&str>) -> Result<()> { @@ -30,21 +29,17 @@ pub fn run(format: &str, dry_run: bool, since: Option<&str>) -> Result<()> { } fn run_entire(dry_run: bool, since_epoch: Option) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let email = git_utils::git2_get_email(&repo)?; - let fallback_ts = Utc::now().timestamp_millis(); + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; + let email = &ctx.email; + let fallback_ts = ctx.timestamp; - let db_path = git_utils::git2_db_path(&repo)?; - let db = if dry_run { - None - } else { - Some(Db::open(&db_path)?) - }; + let db = if dry_run { None } else { Some(&ctx.db) }; let mut imported_count = 0u64; // Resolve the checkpoints tree (local or remote) - let checkpoints_tree = resolve_entire_ref(&repo, "entire/checkpoints/v1")?; + let checkpoints_tree = resolve_entire_ref(repo, "entire/checkpoints/v1")?; if checkpoints_tree.is_none() { eprintln!("No entire/checkpoints/v1 ref found (local or remote), skipping checkpoints"); } @@ -64,13 +59,13 @@ fn run_entire(dry_run: bool, since_epoch: Option) -> Result<()> { eprintln!("Scanning commits for Entire-Checkpoint trailers..."); } imported_count += - import_checkpoints_from_commits(&repo, cp_tree, &db, &email, dry_run, since_epoch)?; + import_checkpoints_from_commits(repo, cp_tree, db, email, dry_run, since_epoch)?; } // Step 2: Import trails - if let Some(tree) = resolve_entire_ref(&repo, "entire/trails/v1")? { + if let Some(tree) = resolve_entire_ref(repo, "entire/trails/v1")? { eprintln!("Processing entire/trails/v1..."); - imported_count += import_trails(&repo, &tree, &db, &email, fallback_ts, dry_run)?; + imported_count += import_trails(repo, &tree, db, email, fallback_ts, dry_run)?; } else { eprintln!("No entire/trails/v1 ref found, skipping trails"); } @@ -107,7 +102,7 @@ fn resolve_entire_ref<'a>(repo: &'a Repository, refname: &str) -> Result, + db: Option<&Db>, email: &str, dry_run: bool, since_epoch: Option, @@ -298,7 +293,7 @@ fn import_checkpoints_from_commits( fn import_session( repo: &Repository, session_tree: &git2::Tree, - db: &Option, + db: Option<&Db>, commit_sha: &str, key_prefix: &str, email: &str, @@ -507,7 +502,7 @@ fn entry_to_tree<'a>( } /// Load the set of trail IDs that have already been imported. -fn load_imported_trail_ids(db: &Option) -> Result> { +fn load_imported_trail_ids(db: Option<&Db>) -> Result> { let mut ids = HashSet::new(); if let Some(db) = db { let mut stmt = db.conn.prepare( @@ -527,7 +522,7 @@ fn load_imported_trail_ids(db: &Option) -> Result> { fn import_trails( repo: &Repository, root_tree: &git2::Tree, - db: &Option, + db: Option<&Db>, email: &str, base_ts: i64, dry_run: bool, @@ -704,7 +699,7 @@ fn import_trails( /// Large string values (> GIT_REF_THRESHOLD bytes) are stored as git blob refs. fn set_value( repo: &Repository, - db: &Option, + db: Option<&Db>, dry_run: bool, target_type: &str, target_value: &str, @@ -814,15 +809,11 @@ fn truncate(s: &str, max: usize) -> String { const NOTES_REFS: &[&str] = &["refs/remotes/notes/ai", "refs/notes/ai"]; fn run_git_ai(dry_run: bool, since_epoch: Option) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let email = git_utils::git2_get_email(&repo)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; + let email = &ctx.email; - let db_path = git_utils::git2_db_path(&repo)?; - let db = if dry_run { - None - } else { - Some(Db::open(&db_path)?) - }; + let db = if dry_run { None } else { Some(&ctx.db) }; // Locate the notes ref — prefer remote mirror, fall back to local. let notes_ref = NOTES_REFS @@ -938,7 +929,7 @@ fn run_git_ai(dry_run: bool, since_epoch: Option) -> Result<()> { // Check whether we already have data for this commit so we can // report skips without touching the DB on a real run. - if let Some(ref db) = db { + if let Some(db) = db { if db.get("commit", &commit_sha, "agent.blame")?.is_some() { skipped_exists += 1; continue; @@ -956,7 +947,7 @@ fn run_git_ai(dry_run: bool, since_epoch: Option) -> Result<()> { }, ); - if let Some(ref db) = db { + if let Some(db) = db { // agent.blame — store as git blob ref if large let (blame_val, is_ref) = if parsed.blame.len() > GIT_REF_THRESHOLD { let oid = repo.blob(parsed.blame.as_bytes())?; @@ -971,7 +962,7 @@ fn run_git_ai(dry_run: bool, since_epoch: Option) -> Result<()> { "agent.blame", &blame_val, "string", - &email, + email, commit_ts, is_ref, )?; @@ -982,7 +973,7 @@ fn run_git_ai(dry_run: bool, since_epoch: Option) -> Result<()> { "agent.git-ai.schema-version", &json_string(&parsed.schema_version), "string", - &email, + email, commit_ts, )?; @@ -993,7 +984,7 @@ fn run_git_ai(dry_run: bool, since_epoch: Option) -> Result<()> { "agent.git-ai.version", &json_string(ver), "string", - &email, + email, commit_ts, )?; } @@ -1005,7 +996,7 @@ fn run_git_ai(dry_run: bool, since_epoch: Option) -> Result<()> { "agent.model", &json_string(&parsed.model), "string", - &email, + email, commit_ts, )?; } diff --git a/src/commands/inspect.rs b/src/commands/inspect.rs index 3d046ce..0f69733 100644 --- a/src/commands/inspect.rs +++ b/src/commands/inspect.rs @@ -9,8 +9,8 @@ use std::collections::BTreeMap; use anyhow::Result; use chrono::{Duration, Utc}; +use crate::context::CommandContext; use crate::db::Db; -use crate::git_utils; use crate::list_value::list_values_from_json; // ── ANSI colours ────────────────────────────────────────────────────────────── @@ -27,21 +27,19 @@ pub fn run( timeline: bool, promisor: bool, ) -> Result<()> { - let repo = git_utils::discover_repo()?; - let db_path = git_utils::db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_gix(None)?; if promisor { - return run_promisor_list(&db, target_type); + return run_promisor_list(&ctx.db, target_type); } if timeline { - return run_timeline(&db); + return run_timeline(&ctx.db); } match target_type { - None => run_overview(&db), - Some(tt) => run_list(&db, tt, term), + None => run_overview(&ctx.db), + Some(tt) => run_list(&ctx.db, tt, term), } } diff --git a/src/commands/list.rs b/src/commands/list.rs index 06f8210..394bdf2 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,29 +1,22 @@ use anyhow::Result; -use chrono::Utc; -use crate::db::Db; -use crate::git_utils; +use crate::context::CommandContext; use crate::types::{validate_key, Target}; pub fn run_push(target_str: &str, key: &str, value: &str) -> Result<()> { let mut target = Target::parse(target_str)?; validate_key(key)?; - let repo = git_utils::discover_repo()?; - target.resolve(&repo)?; - let db_path = git_utils::db_path(&repo)?; - let email = git_utils::get_email(&repo)?; - let timestamp = Utc::now().timestamp_millis(); + let ctx = CommandContext::open_gix(None)?; + ctx.resolve_target(&mut target)?; - let db = Db::open(&db_path)?; - - db.list_push( + ctx.db.list_push( target.type_str(), target.value_str(), key, value, - &email, - timestamp, + &ctx.email, + ctx.timestamp, )?; Ok(()) @@ -33,12 +26,12 @@ pub fn run_rm(target_str: &str, key: &str, index: Option) -> Result<()> { let mut target = Target::parse(target_str)?; validate_key(key)?; - let repo = git_utils::discover_repo()?; - target.resolve(&repo)?; - let db_path = git_utils::db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_gix(None)?; + ctx.resolve_target(&mut target)?; - let entries = db.list_entries(target.type_str(), target.value_str(), key)?; + let entries = ctx + .db + .list_entries(target.type_str(), target.value_str(), key)?; match index { None => { @@ -57,15 +50,13 @@ pub fn run_rm(target_str: &str, key: &str, index: Option) -> Result<()> { } } Some(idx) => { - let email = git_utils::get_email(&repo)?; - let timestamp = Utc::now().timestamp_millis(); - db.list_rm( + ctx.db.list_rm( target.type_str(), target.value_str(), key, idx, - &email, - timestamp, + &ctx.email, + ctx.timestamp, )?; } } @@ -77,21 +68,16 @@ pub fn run_pop(target_str: &str, key: &str, value: &str) -> Result<()> { let mut target = Target::parse(target_str)?; validate_key(key)?; - let repo = git_utils::discover_repo()?; - target.resolve(&repo)?; - let db_path = git_utils::db_path(&repo)?; - let email = git_utils::get_email(&repo)?; - let timestamp = Utc::now().timestamp_millis(); - - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_gix(None)?; + ctx.resolve_target(&mut target)?; - db.list_pop( + ctx.db.list_pop( target.type_str(), target.value_str(), key, value, - &email, - timestamp, + &ctx.email, + ctx.timestamp, )?; Ok(()) diff --git a/src/commands/local_prune.rs b/src/commands/local_prune.rs index ad0f788..6a3c9b8 100644 --- a/src/commands/local_prune.rs +++ b/src/commands/local_prune.rs @@ -2,13 +2,10 @@ use anyhow::Result; use rusqlite::params; use crate::commands::auto_prune::{parse_since_to_cutoff_ms, read_prune_rules}; -use crate::db::Db; -use crate::git_utils; +use crate::context::CommandContext; pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { - let repo = git_utils::discover_repo()?; - let db_path = git_utils::db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_gix(None)?; let cutoff_ms = if skip_date { // Prune everything (cutoff far in the future so all timestamps qualify) @@ -17,12 +14,12 @@ pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { } else { // Read the since value directly — for manual prune we only need the retention window, // not the triggers (max-keys/max-size). - let rules = read_prune_rules(&db)?; + let rules = read_prune_rules(&ctx.db)?; let since = match rules { Some(ref r) => r.since.clone(), None => { // Check if at least meta:prune:since is set (triggers may be absent) - match db.get("project", "", "meta:prune:since")? { + match ctx.db.get("project", "", "meta:prune:since")? { Some((value, _, _)) => { let s: String = serde_json::from_str(&value)?; s @@ -60,14 +57,14 @@ pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { // Count what will be pruned (never prune project target_type) - let metadata_count: u64 = db.conn.query_row( + let metadata_count: u64 = ctx.db.conn.query_row( "SELECT COUNT(*) FROM metadata WHERE target_type != 'project' AND last_timestamp < ?1", params![cutoff_ms], |row| row.get(0), )?; - let list_values_count: u64 = db.conn.query_row( + let list_values_count: u64 = ctx.db.conn.query_row( "SELECT COUNT(*) FROM list_values WHERE timestamp < ?1 AND metadata_id IN ( @@ -77,21 +74,21 @@ pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { |row| row.get(0), )?; - let tombstone_count: u64 = db.conn.query_row( + let tombstone_count: u64 = ctx.db.conn.query_row( "SELECT COUNT(*) FROM metadata_tombstones WHERE target_type != 'project' AND timestamp < ?1", params![cutoff_ms], |row| row.get(0), )?; - let set_tombstone_count: u64 = db.conn.query_row( + let set_tombstone_count: u64 = ctx.db.conn.query_row( "SELECT COUNT(*) FROM set_tombstones WHERE target_type != 'project' AND timestamp < ?1", params![cutoff_ms], |row| row.get(0), )?; - let log_count: u64 = db.conn.query_row( + let log_count: u64 = ctx.db.conn.query_row( "SELECT COUNT(*) FROM metadata_log WHERE target_type != 'project' AND timestamp < ?1", params![cutoff_ms], @@ -99,14 +96,14 @@ pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { )?; // Count what will survive - let metadata_remaining: u64 = db.conn.query_row( + let metadata_remaining: u64 = ctx.db.conn.query_row( "SELECT COUNT(*) FROM metadata WHERE target_type = 'project' OR last_timestamp >= ?1", params![cutoff_ms], |row| row.get(0), )?; - let list_values_remaining: u64 = db.conn.query_row( + let list_values_remaining: u64 = ctx.db.conn.query_row( "SELECT COUNT(*) FROM list_values WHERE timestamp >= ?1 OR metadata_id IN ( @@ -149,7 +146,7 @@ pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { // Delete in the right order: child rows first, then parent rows // 1. Delete list_values and set_values for metadata rows being pruned - db.conn.execute( + ctx.db.conn.execute( "DELETE FROM list_values WHERE metadata_id IN ( SELECT rowid FROM metadata @@ -158,7 +155,7 @@ pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { params![cutoff_ms], )?; - db.conn.execute( + ctx.db.conn.execute( "DELETE FROM set_values WHERE metadata_id IN ( SELECT rowid FROM metadata @@ -168,7 +165,7 @@ pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { )?; // 2. Delete old list entries from lists that survive (entries older than cutoff) - db.conn.execute( + ctx.db.conn.execute( "DELETE FROM list_values WHERE timestamp < ?1 AND metadata_id IN ( @@ -178,28 +175,28 @@ pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { )?; // 3. Delete the metadata rows themselves - db.conn.execute( + ctx.db.conn.execute( "DELETE FROM metadata WHERE target_type != 'project' AND last_timestamp < ?1", params![cutoff_ms], )?; // 4. Delete old tombstones - db.conn.execute( + ctx.db.conn.execute( "DELETE FROM metadata_tombstones WHERE target_type != 'project' AND timestamp < ?1", params![cutoff_ms], )?; // 5. Delete old set tombstones - db.conn.execute( + ctx.db.conn.execute( "DELETE FROM set_tombstones WHERE target_type != 'project' AND timestamp < ?1", params![cutoff_ms], )?; // 6. Delete old log entries - db.conn.execute( + ctx.db.conn.execute( "DELETE FROM metadata_log WHERE target_type != 'project' AND timestamp < ?1", params![cutoff_ms], diff --git a/src/commands/log.rs b/src/commands/log.rs index 8cbb54a..0df3eff 100644 --- a/src/commands/log.rs +++ b/src/commands/log.rs @@ -4,6 +4,7 @@ use anyhow::{Context, Result}; use git2::{Oid, Repository, Sort}; +use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; @@ -21,12 +22,14 @@ pub fn run( count: usize, // max commits to show metadata_only: bool, // skip commits with no metadata ) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let db_path = git_utils::git2_db_path(&repo)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; + // log needs a separate Db with an embedded repo for blob resolution during reads + let db_path = git_utils::git2_db_path(repo)?; let db = Db::open_with_repo(&db_path, git_utils::git2_discover_repo()?)?; // Resolve start ref → OID - let start_oid = resolve_start(&repo, start_ref)?; + let start_oid = resolve_start(repo, start_ref)?; // Walk commits let mut revwalk = repo.revwalk()?; diff --git a/src/commands/materialize.rs b/src/commands/materialize.rs index 7c1197c..21ab6ef 100644 --- a/src/commands/materialize.rs +++ b/src/commands/materialize.rs @@ -1,8 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; -use anyhow::Result; -use chrono::Utc; - +use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; use crate::list_value::{encode_entries, parse_timestamp_from_entry_name, ListEntry}; @@ -13,6 +11,7 @@ use crate::types::{ LIST_VALUE_DIR, PATH_TARGET_SEPARATOR, SET_VALUE_DIR, STRING_VALUE_BLOB, TOMBSTONE_BLOB, TOMBSTONE_ROOT, }; +use anyhow::Result; type Key = (String, String, String); // (target_type, target_value, key) @@ -113,12 +112,11 @@ enum MergeState { } pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let db_path = git_utils::git2_db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; - let ns = git_utils::git2_get_namespace(&repo)?; - let local_ref_name = git_utils::git2_local_ref(&repo)?; + let ns = git_utils::git2_get_namespace(repo)?; + let local_ref_name = git_utils::git2_local_ref(repo)?; if verbose { eprintln!("[verbose] namespace: {}", ns); @@ -133,7 +131,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { } // Find remote refs to materialize - let remote_refs = find_remote_refs(&repo, &ns, remote)?; + let remote_refs = find_remote_refs(repo, &ns, remote)?; if remote_refs.is_empty() { println!("no remote metadata refs found"); @@ -147,8 +145,8 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { } } - let email = git_utils::git2_get_email(&repo)?; - let now = Utc::now().timestamp_millis(); + let email = &ctx.email; + let now = ctx.timestamp; for (ref_name, remote_oid) in &remote_refs { if verbose { @@ -157,7 +155,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { let remote_commit = repo.find_commit(*remote_oid)?; let remote_tree = remote_commit.tree()?; - let remote_entries = parse_tree(&repo, &remote_tree, "")?; + let remote_entries = parse_tree(repo, &remote_tree, "")?; if verbose { eprintln!( @@ -252,7 +250,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { if can_fast_forward { let local_entries = if let Some(local_c) = &local_commit { - parse_tree(&repo, &local_c.tree()?, "")? + parse_tree(repo, &local_c.tree()?, "")? } else { ParsedTree::default() }; @@ -291,7 +289,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { if dry_run { let mut planned_removals = BTreeSet::new(); let mut planned_changes = collect_db_changes_from_tree( - &db, + &ctx.db, &remote_entries.values, &remote_entries.tombstones, &remote_entries.set_tombstones, @@ -314,13 +312,13 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { // Fast-forward: update SQLite from remote tree first. update_db_from_tree( - &repo, - &db, + repo, + &ctx.db, &remote_entries.values, &remote_entries.tombstones, &remote_entries.set_tombstones, &remote_entries.list_tombstones, - &email, + email, now, )?; @@ -335,7 +333,8 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { key_name ); } - db.apply_tombstone(target_type, target_value, key_name, &email, now)?; + ctx.db + .apply_tombstone(target_type, target_value, key_name, email, now)?; } } @@ -351,7 +350,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { } else { // Need a real merge let local_c = local_commit.as_ref().unwrap(); - let local_entries = parse_tree(&repo, &local_c.tree()?, "")?; + let local_entries = parse_tree(repo, &local_c.tree()?, "")?; if verbose { eprintln!( @@ -385,7 +384,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { merge_strategy, ) = if let Some(base_oid) = merge_base_oid { let base_commit = repo.find_commit(base_oid)?; - let base_entries = parse_tree(&repo, &base_commit.tree()?, "")?; + let base_entries = parse_tree(repo, &base_commit.tree()?, "")?; if verbose { eprintln!( @@ -552,7 +551,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { if dry_run { let mut planned_removals = BTreeSet::new(); let mut planned_changes = collect_db_changes_from_tree( - &db, + &ctx.db, &merged_values, &merged_tombstones, &merged_set_tombstones, @@ -597,13 +596,13 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { eprintln!("[verbose] updating SQLite database..."); } update_db_from_tree( - &repo, - &db, + repo, + &ctx.db, &merged_values, &merged_tombstones, &merged_set_tombstones, &merged_list_tombstones, - &email, + email, now, )?; @@ -619,7 +618,8 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { key_name ); } - db.apply_tombstone(target_type, target_value, key_name, &email, now)?; + ctx.db + .apply_tombstone(target_type, target_value, key_name, email, now)?; } } } @@ -629,7 +629,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { eprintln!("[verbose] building merged tree..."); } let merged_tree_oid = build_merged_tree( - &repo, + repo, &merged_values, &merged_tombstones, &merged_set_tombstones, @@ -646,8 +646,8 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { } let merged_tree = repo.find_tree(merged_tree_oid)?; - let name = git_utils::git2_get_name(&repo)?; - let sig = git2::Signature::now(&name, &email)?; + let name = git_utils::git2_get_name(repo)?; + let sig = git2::Signature::now(&name, email)?; let merge_commit_oid = repo.commit( Some(&local_ref_name), @@ -672,7 +672,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { } if !dry_run { - db.set_last_materialized(now)?; + ctx.db.set_last_materialized(now)?; } Ok(()) diff --git a/src/commands/promisor.rs b/src/commands/promisor.rs index e7c77a9..7ad00c6 100644 --- a/src/commands/promisor.rs +++ b/src/commands/promisor.rs @@ -1,13 +1,12 @@ use anyhow::{bail, Result}; -use crate::db::Db; +use crate::context::CommandContext; use crate::git_utils; pub fn run() -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let ns = git_utils::git2_get_namespace(&repo)?; - let db_path = git_utils::git2_db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; + let ns = git_utils::git2_get_namespace(repo)?; let tracking_ref = format!("refs/{}/remotes/main", ns); let tip_commit = match repo.find_reference(&tracking_ref) { @@ -69,7 +68,10 @@ pub fn run() -> Result<()> { skipped_deletes += 1; continue; } - if db.insert_promised(target_type, target_value, key, "string")? { + if ctx + .db + .insert_promised(target_type, target_value, key, "string")? + { commit_inserted += 1; inserted += 1; } else { @@ -91,13 +93,16 @@ pub fn run() -> Result<()> { None if commit.parent_count() == 0 => { // Root commit without a change list — walk its tree let tree = commit.tree()?; - let keys = super::pull::extract_keys_from_tree_pub(&repo, &tree)?; + let keys = super::pull::extract_keys_from_tree_pub(repo, &tree)?; commits_parsed += 1; let mut commit_inserted = 0; let mut commit_skipped = 0; for (target_type, target_value, key) in &keys { - if db.insert_promised(target_type, target_value, key, "string")? { + if ctx + .db + .insert_promised(target_type, target_value, key, "string")? + { commit_inserted += 1; inserted += 1; } else { diff --git a/src/commands/prune.rs b/src/commands/prune.rs index 0fe2949..d24d121 100644 --- a/src/commands/prune.rs +++ b/src/commands/prune.rs @@ -11,16 +11,15 @@ use crate::commands::auto_prune::parse_since_to_cutoff_ms; use crate::commands::serialize::{ build_filtered_tree, classify_key, count_prune_stats, parse_filter_rules, MAIN_DEST, }; -use crate::db::Db; +use crate::context::CommandContext; use crate::git_utils; pub fn run(dry_run: bool) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let db_path = git_utils::git2_db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; // Read prune rules — need at least meta:prune:since - let since = match db.get("project", "", "meta:prune:since")? { + let since = match ctx.db.get("project", "", "meta:prune:since")? { Some((value, _, _)) => { let s: String = serde_json::from_str(&value)?; s @@ -40,7 +39,7 @@ pub fn run(dry_run: bool) -> Result<()> { .unwrap_or_else(|| "?".to_string()); // Find the current serialized tree - let ref_name = git_utils::git2_local_ref(&repo)?; + let ref_name = git_utils::git2_local_ref(repo)?; let Some(current_commit) = repo .find_reference(&ref_name) .ok() @@ -54,7 +53,7 @@ pub fn run(dry_run: bool) -> Result<()> { }; let tree_oid = current_commit.tree()?.id(); - let (_, current_keys) = count_prune_stats(&repo, tree_oid, tree_oid)?; + let (_, current_keys) = count_prune_stats(repo, tree_oid, tree_oid)?; eprintln!( "Pruning {} (cutoff: {} — entries older than {})", @@ -63,7 +62,7 @@ pub fn run(dry_run: bool) -> Result<()> { eprintln!(" current tree: {} keys", current_keys); // Read filter rules so we produce the same tree as serialize would - let filter_rules = parse_filter_rules(&db)?; + let filter_rules = parse_filter_rules(&ctx.db)?; let is_main_dest = |key: &str| -> bool { match classify_key(key, &filter_rules) { @@ -73,10 +72,10 @@ pub fn run(dry_run: bool) -> Result<()> { }; // Read all metadata and split into kept vs pruned by cutoff + serialize filters - let all_metadata = db.get_all_metadata()?; - let all_tombstones = db.get_all_tombstones()?; - let all_set_tombstones = db.get_all_set_tombstones()?; - let all_list_tombstones = db.get_all_list_tombstones()?; + let all_metadata = ctx.db.get_all_metadata()?; + let all_tombstones = ctx.db.get_all_tombstones()?; + let all_set_tombstones = ctx.db.get_all_set_tombstones()?; + let all_list_tombstones = ctx.db.get_all_list_tombstones()?; // Count entries that would be pruned (old + in main dest) let mut pruned_meta = 0u64; @@ -131,14 +130,14 @@ pub fn run(dry_run: bool) -> Result<()> { // Build a fresh tree from the surviving entries let pruned_tree_oid = build_filtered_tree( - &repo, + repo, &metadata, &tombstones, &set_tombstones, &list_tombstones, )?; - let (keys_dropped, keys_retained) = count_prune_stats(&repo, tree_oid, pruned_tree_oid)?; + let (keys_dropped, keys_retained) = count_prune_stats(repo, tree_oid, pruned_tree_oid)?; eprintln!( " pruned tree: {} keys ({} dropped from tree)", @@ -154,8 +153,8 @@ pub fn run(dry_run: bool) -> Result<()> { } // Commit the pruned tree - let name = git_utils::git2_get_name(&repo)?; - let email = git_utils::git2_get_email(&repo)?; + let name = git_utils::git2_get_name(repo)?; + let email = git_utils::git2_get_email(repo)?; let sig = git2::Signature::now(&name, &email)?; let pruned_tree = repo.find_tree(pruned_tree_oid)?; diff --git a/src/commands/pull.rs b/src/commands/pull.rs index 7679614..d9d47b2 100644 --- a/src/commands/pull.rs +++ b/src/commands/pull.rs @@ -1,15 +1,17 @@ use anyhow::Result; use crate::commands::{materialize, serialize}; +use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; use crate::types; pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let ns = git_utils::git2_get_namespace(&repo)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; + let ns = git_utils::git2_get_namespace(repo)?; - let remote_name = git_utils::resolve_meta_remote(&repo, remote)?; + let remote_name = git_utils::resolve_meta_remote(repo, remote)?; let remote_refspec = format!("refs/{}/main", ns); let tracking_ref = format!("refs/{}/remotes/main", ns); let fetch_refspec = format!("{}:{}", remote_refspec, tracking_ref); @@ -28,7 +30,7 @@ pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { // Fetch latest remote metadata eprintln!("Fetching metadata from {}...", remote_name); - git_utils::git2_run_git(&repo, &["fetch", &remote_name, &fetch_refspec])?; + git_utils::git2_run_git(repo, &["fetch", &remote_name, &fetch_refspec])?; // Get the new tip let new_tip = repo @@ -39,11 +41,9 @@ pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { // Check if we need to materialize even if no new commits were fetched // (e.g. remote add fetched but never materialized) - let db_path = git_utils::git2_db_path(&repo)?; - let db = Db::open(&db_path)?; - let needs_materialize = db.get_last_materialized()?.is_none() + let needs_materialize = ctx.db.get_last_materialized()?.is_none() || repo - .find_reference(&git_utils::git2_local_ref(&repo)?) + .find_reference(&git_utils::git2_local_ref(repo)?) .is_err(); // Count new commits @@ -56,7 +56,7 @@ pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { eprintln!("No new commits, but local state needs materializing."); } (Some(old), Some(new)) => { - let count = count_commits_between(&repo, old, new); + let count = count_commits_between(repo, old, new); eprintln!( "Fetched {} new commit{}.", count, @@ -71,7 +71,7 @@ pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { // Hydrate tip tree blobs so libgit2 can read them let short_ref = format!("{}/remotes/main", ns); - git_utils::hydrate_tip_blobs(&repo, &remote_name, &short_ref)?; + git_utils::hydrate_tip_blobs(repo, &remote_name, &short_ref)?; // Serialize local state so materialize can do a proper 3-way merge eprintln!("Serializing local metadata..."); @@ -86,7 +86,7 @@ pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { // On first materialize, walk the entire history (pass None as old_tip). if let Some(new) = new_tip { let walk_from = if needs_materialize { None } else { old_tip }; - let promisor_count = insert_promisor_entries(&repo, &db, new, walk_from, verbose)?; + let promisor_count = insert_promisor_entries(repo, &ctx.db, new, walk_from, verbose)?; if promisor_count > 0 { eprintln!( "Indexed {} keys from history (available on demand).", diff --git a/src/commands/push.rs b/src/commands/push.rs index 3644edd..52b1ff3 100644 --- a/src/commands/push.rs +++ b/src/commands/push.rs @@ -1,14 +1,16 @@ use anyhow::{bail, Result}; use crate::commands::{materialize, serialize}; +use crate::context::CommandContext; use crate::git_utils; /// Push a README commit to refs/heads/main on the meta remote. /// This only succeeds if the branch doesn't already exist (no force push). pub fn run_readme(remote: Option<&str>, verbose: bool) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; - let remote_name = git_utils::resolve_meta_remote(&repo, remote)?; + let remote_name = git_utils::resolve_meta_remote(repo, remote)?; // Gather project info from .git/config let config = repo.config()?; @@ -18,7 +20,7 @@ pub fn run_readme(remote: Option<&str>, verbose: bool) -> Result<()> { let meta_url = config .get_string(&format!("remote.{}.url", remote_name)) .unwrap_or_else(|_| "unknown".to_string()); - let ns = git_utils::git2_get_namespace(&repo)?; + let ns = git_utils::git2_get_namespace(repo)?; let readme_content = generate_readme(&origin_url, &meta_url, &ns); @@ -61,7 +63,7 @@ pub fn run_readme(remote: Option<&str>, verbose: bool) -> Result<()> { } eprintln!("Pushing README to {}...", remote_name); - let result = git_utils::git2_run_git(&repo, &["push", &remote_name, &push_refspec]); + let result = git_utils::git2_run_git(repo, &["push", &remote_name, &push_refspec]); match result { Ok(_) => { @@ -153,12 +155,13 @@ See `gmeta --help` for the full command reference. const MAX_RETRIES: u32 = 5; pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let ns = git_utils::git2_get_namespace(&repo)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; + let ns = git_utils::git2_get_namespace(repo)?; // Resolve which remote to push to - let remote_name = git_utils::resolve_meta_remote(&repo, remote)?; - let local_ref = git_utils::git2_local_ref(&repo)?; + let remote_name = git_utils::resolve_meta_remote(repo, remote)?; + let local_ref = git_utils::git2_local_ref(repo)?; let remote_refspec = format!("refs/{}/main", ns); if verbose { @@ -205,7 +208,7 @@ pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { } eprintln!("Pushing to {}...", remote_name); - let result = git_utils::git2_run_git(&repo, &["push", &remote_name, &push_refspec]); + let result = git_utils::git2_run_git(repo, &["push", &remote_name, &push_refspec]); match result { Ok(_) => { @@ -229,11 +232,11 @@ pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { // Fetch latest remote data let fetch_refspec = format!("{}:refs/{}/remotes/main", remote_refspec, ns); - git_utils::git2_run_git(&repo, &["fetch", &remote_name, &fetch_refspec])?; + git_utils::git2_run_git(repo, &["fetch", &remote_name, &fetch_refspec])?; // Hydrate tip tree blobs so libgit2 can read them let short_ref = format!("{}/remotes/main", ns); - git_utils::hydrate_tip_blobs(&repo, &remote_name, &short_ref)?; + git_utils::hydrate_tip_blobs(repo, &remote_name, &short_ref)?; // Materialize the remote data (merge into local DB) materialize::run(None, false, verbose)?; @@ -245,7 +248,7 @@ pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { // Rewrite local ref as a single commit on top of the remote tip. // This avoids merge commits in the pushed history — the spec // requires that push always produces a single fast-forward commit. - rebase_local_on_remote(&repo, &local_ref, &remote_tracking_ref, verbose)?; + rebase_local_on_remote(repo, &local_ref, &remote_tracking_ref, verbose)?; } } } diff --git a/src/commands/remote.rs b/src/commands/remote.rs index 25e6b57..3e594ef 100644 --- a/src/commands/remote.rs +++ b/src/commands/remote.rs @@ -1,6 +1,7 @@ use anyhow::{bail, Result}; use crate::commands::{materialize, pull, serialize}; +use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; @@ -56,11 +57,12 @@ fn check_remote_refs(repo: &git2::Repository, url: &str, ns: &str) -> Result<(bo } pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; let ns = namespace_override .map(|s| s.to_string()) .unwrap_or_else(|| { - git_utils::git2_get_namespace(&repo).unwrap_or_else(|_| "meta".to_string()) + git_utils::git2_get_namespace(repo).unwrap_or_else(|_| "meta".to_string()) }); let url = expand_url(url); @@ -71,7 +73,7 @@ pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Resul // Check the remote for meta refs before configuring eprintln!("Checking {}...", url); - match check_remote_refs(&repo, &url, &ns) { + match check_remote_refs(repo, &url, &ns) { Ok((has_match, other_namespaces)) => { if !has_match { if other_namespaces.is_empty() { @@ -135,7 +137,7 @@ pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Resul let fetch_refspec = format!("refs/{ns}/main:refs/{ns}/remotes/main"); eprint!("Fetching metadata (blobless)..."); match git_utils::git2_run_git( - &repo, + repo, &["fetch", "--filter=blob:none", name, &fetch_refspec], ) { Ok(_) => { @@ -165,7 +167,7 @@ pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Resul // Hydrate tip tree blobs so libgit2 can read the metadata eprint!("Hydrating tip blobs..."); - let blob_count = git_utils::hydrate_tip_blobs_counted(&repo, name, &remote_ref)?; + let blob_count = git_utils::hydrate_tip_blobs_counted(repo, name, &remote_ref)?; eprintln!(" {} blobs fetched.", blob_count); // Materialize remote metadata into local SQLite @@ -181,10 +183,10 @@ pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Resul let tracking_ref_name = format!("refs/{}/remotes/main", ns); if let Ok(r) = repo.find_reference(&tracking_ref_name) { if let Ok(tip) = r.peel_to_commit() { - let db_path = git_utils::git2_db_path(&repo)?; + let db_path = git_utils::git2_db_path(repo)?; let db = Db::open(&db_path)?; let count = - pull::insert_promisor_entries_pub(&repo, &db, tip.id(), None, false)?; + pull::insert_promisor_entries_pub(repo, &db, tip.id(), None, false)?; if count > 0 { eprintln!("Indexed {} keys from history (available on demand).", count); } @@ -201,8 +203,9 @@ pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Resul } pub fn run_remove(name: &str) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let ns = git_utils::git2_get_namespace(&repo)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; + let ns = git_utils::git2_get_namespace(repo)?; // Verify this is a meta remote let config = repo.config()?; @@ -253,8 +256,9 @@ pub fn run_remove(name: &str) -> Result<()> { } pub fn run_list() -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let remotes = git_utils::list_meta_remotes(&repo)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; + let remotes = git_utils::list_meta_remotes(repo)?; if remotes.is_empty() { println!("No metadata remotes configured."); diff --git a/src/commands/rm.rs b/src/commands/rm.rs index e54696a..116fb37 100644 --- a/src/commands/rm.rs +++ b/src/commands/rm.rs @@ -1,28 +1,21 @@ use anyhow::Result; -use chrono::Utc; -use crate::db::Db; -use crate::git_utils; +use crate::context::CommandContext; use crate::types::{validate_key, Target}; pub fn run(target_str: &str, key: &str) -> Result<()> { let mut target = Target::parse(target_str)?; validate_key(key)?; - let repo = git_utils::discover_repo()?; - target.resolve(&repo)?; - let db_path = git_utils::db_path(&repo)?; - let email = git_utils::get_email(&repo)?; - let timestamp = Utc::now().timestamp_millis(); + let ctx = CommandContext::open_gix(None)?; + ctx.resolve_target(&mut target)?; - let db = Db::open(&db_path)?; - - let removed = db.rm( + let removed = ctx.db.rm( target.type_str(), target.value_str(), key, - &email, - timestamp, + &ctx.email, + ctx.timestamp, )?; if !removed { diff --git a/src/commands/serialize.rs b/src/commands/serialize.rs index f24fe19..5d69a7d 100644 --- a/src/commands/serialize.rs +++ b/src/commands/serialize.rs @@ -4,6 +4,7 @@ use anyhow::{bail, Context, Result}; use chrono::Utc; use crate::commands::auto_prune::{self, parse_since_to_cutoff_ms}; +use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; use crate::list_value::{make_entry_name, parse_entries}; @@ -199,12 +200,11 @@ fn build_commit_message(changes: &[(char, String, String)]) -> String { } pub fn run(verbose: bool) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let db_path = git_utils::git2_db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; - let local_ref_name = git_utils::git2_local_ref(&repo)?; - let last_materialized = db.get_last_materialized()?; + let local_ref_name = git_utils::git2_local_ref(repo)?; + let last_materialized = ctx.db.get_last_materialized()?; if verbose { eprintln!("[verbose] local ref: {}", local_ref_name); @@ -257,7 +257,7 @@ pub fn run(verbose: bool) -> Result<()> { dirty_target_bases, changes, ) = if let Some(since) = last_materialized { - let modified = db.get_modified_since(since)?; + let modified = ctx.db.get_modified_since(since)?; if verbose { eprintln!( "[verbose] incremental mode: {} entries modified since last materialize", @@ -304,10 +304,10 @@ pub fn run(verbose: bool) -> Result<()> { dirty_bases.insert(target.tree_base_path()); } - let metadata = db.get_all_metadata()?; - let tombstones = db.get_all_tombstones()?; - let set_tombstones = db.get_all_set_tombstones()?; - let list_tombstones = db.get_all_list_tombstones()?; + let metadata = ctx.db.get_all_metadata()?; + let tombstones = ctx.db.get_all_tombstones()?; + let set_tombstones = ctx.db.get_all_set_tombstones()?; + let list_tombstones = ctx.db.get_all_list_tombstones()?; // Note: tombstone/set-tombstone targets don't need to be added to // dirty_bases separately — delete operations are logged in metadata_log, @@ -338,7 +338,7 @@ pub fn run(verbose: bool) -> Result<()> { eprintln!("[verbose] full serialization mode (no previous materialize)"); } - let metadata = db.get_all_metadata()?; + let metadata = ctx.db.get_all_metadata()?; // Full serialize: all entries are adds let changes: Vec<(char, String, String)> = metadata @@ -357,9 +357,9 @@ pub fn run(verbose: bool) -> Result<()> { ( metadata, - db.get_all_tombstones()?, - db.get_all_set_tombstones()?, - db.get_all_list_tombstones()?, + ctx.db.get_all_tombstones()?, + ctx.db.get_all_set_tombstones()?, + ctx.db.get_all_list_tombstones()?, None, changes, ) @@ -374,10 +374,11 @@ pub fn run(verbose: bool) -> Result<()> { // If meta:prune:since is configured, drop entries older than the cutoff // before building the tree. This avoids building a large tree only to // prune it, and keeps the summary counts accurate. - let prune_since = db + let prune_since = ctx + .db .get("project", "", "meta:prune:since")? .and_then(|(value, _, _)| serde_json::from_str::(&value).ok()); - let prune_rules = auto_prune::read_prune_rules(&db)?; + let prune_rules = auto_prune::read_prune_rules(&ctx.db)?; let prune_cutoff_ms = prune_since .as_deref() .map(parse_since_to_cutoff_ms) @@ -400,7 +401,7 @@ pub fn run(verbose: bool) -> Result<()> { }; // ── Read filter rules ─────────────────────────────────────────────────── - let filter_rules = parse_filter_rules(&db)?; + let filter_rules = parse_filter_rules(&ctx.db)?; if verbose && !filter_rules.is_empty() { eprintln!("[verbose] filter rules: {}", filter_rules.len()); for rule in &filter_rules { @@ -552,12 +553,12 @@ pub fn run(verbose: bool) -> Result<()> { } // ── Build and commit trees per destination ────────────────────────────── - let name = git_utils::git2_get_name(&repo)?; - let email = git_utils::git2_get_email(&repo)?; + let name = git_utils::git2_get_name(repo)?; + let email = git_utils::git2_get_email(repo)?; let sig = git2::Signature::now(&name, &email)?; for dest in &all_dests { - let ref_name = git_utils::git2_destination_ref(&repo, dest)?; + let ref_name = git_utils::git2_destination_ref(repo, dest)?; let empty_meta: Vec = Vec::new(); let empty_tomb: Vec = Vec::new(); let empty_set_tomb: Vec = Vec::new(); @@ -581,7 +582,7 @@ pub fn run(verbose: bool) -> Result<()> { eprintln!("Building git tree for {}...", ref_name); let tree_oid = build_tree( - &repo, meta, tombs, set_tombs, list_tombs, existing, dirty, verbose, + repo, meta, tombs, set_tombs, list_tombs, existing, dirty, verbose, )?; if verbose { @@ -642,7 +643,7 @@ pub fn run(verbose: bool) -> Result<()> { .unwrap_or_else(|| "none".to_string()) ); } - if auto_prune::should_prune(&repo, tree_oid, prune_rules)? { + if auto_prune::should_prune(repo, tree_oid, prune_rules)? { eprintln!( "Auto-prune triggered, pruning with --since={}...", prune_rules.since @@ -659,7 +660,8 @@ pub fn run(verbose: bool) -> Result<()> { ); } - let prune_tree_oid = prune_tree(&repo, tree_oid, prune_rules, &db, verbose)?; + let prune_tree_oid = + prune_tree(repo, tree_oid, prune_rules, &ctx.db, verbose)?; if prune_tree_oid != tree_oid { if verbose { @@ -672,7 +674,7 @@ pub fn run(verbose: bool) -> Result<()> { let prune_parent = repo.find_reference(&ref_name)?.peel_to_commit()?; let (keys_dropped, keys_retained) = - count_prune_stats(&repo, tree_oid, prune_tree_oid)?; + count_prune_stats(repo, tree_oid, prune_tree_oid)?; if verbose { eprintln!( @@ -720,7 +722,7 @@ pub fn run(verbose: bool) -> Result<()> { } let now = Utc::now().timestamp_millis(); - db.set_last_materialized(now)?; + ctx.db.set_last_materialized(now)?; Ok(()) } diff --git a/src/commands/set.rs b/src/commands/set.rs index 2a5d2d6..09a5195 100644 --- a/src/commands/set.rs +++ b/src/commands/set.rs @@ -1,43 +1,11 @@ use std::fs; use anyhow::{bail, Context, Result}; -use chrono::Utc; -use crate::db::Db; -use crate::git_utils; +use crate::context::CommandContext; use crate::list_value::{encode_entries, parse_entries}; use crate::types::{validate_key, Target, ValueType, GIT_REF_THRESHOLD}; -struct CommandContext { - target: Target, - db: Db, - email: String, - timestamp: i64, -} - -fn open_context( - target_str: &str, - key: &str, - timestamp_override: Option, -) -> Result { - let mut target = Target::parse(target_str)?; - validate_key(key)?; - - let repo = git_utils::discover_repo()?; - target.resolve(&repo)?; - let db_path = git_utils::db_path(&repo)?; - let email = git_utils::get_email(&repo)?; - let timestamp = timestamp_override.unwrap_or_else(|| Utc::now().timestamp_millis()); - let db = Db::open(&db_path)?; - - Ok(CommandContext { - target, - db, - email, - timestamp, - }) -} - fn print_result(action: &str, key: &str, target: &Target, json: bool) { let target_str = match &target.value { Some(v) => format!("{} {}", target.type_str(), v), @@ -65,7 +33,12 @@ pub fn run( json: bool, timestamp: Option, ) -> Result<()> { - let ctx = open_context(target_str, key, timestamp)?; + let mut target = Target::parse(target_str)?; + validate_key(key)?; + + let ctx = CommandContext::open_gix(timestamp)?; + ctx.resolve_target(&mut target)?; + let value_type = ValueType::from_str(value_type_str)?; let from_file = file.is_some(); @@ -83,12 +56,12 @@ pub fn run( from_file && matches!(value_type, ValueType::String) && raw_value.len() > GIT_REF_THRESHOLD; if use_git_ref { - let repo = git_utils::git2_discover_repo()?; - let blob_oid = repo.blob(raw_value.as_bytes())?; + let git2_repo = ctx.git2_repo()?; + let blob_oid = git2_repo.blob(raw_value.as_bytes())?; ctx.db.set_with_git_ref( None, - ctx.target.type_str(), - ctx.target.value_str(), + target.type_str(), + target.value_str(), key, &blob_oid.to_string(), value_type.as_str(), @@ -110,8 +83,8 @@ pub fn run( }; ctx.db.set( - ctx.target.type_str(), - ctx.target.value_str(), + target.type_str(), + target.value_str(), key, &stored_value, value_type.as_str(), @@ -120,7 +93,7 @@ pub fn run( )?; } - print_result("set", key, &ctx.target, json); + print_result("set", key, &target, json); Ok(()) } @@ -131,16 +104,21 @@ pub fn run_add( json: bool, timestamp: Option, ) -> Result<()> { - let ctx = open_context(target_str, key, timestamp)?; + let mut target = Target::parse(target_str)?; + validate_key(key)?; + + let ctx = CommandContext::open_gix(timestamp)?; + ctx.resolve_target(&mut target)?; + ctx.db.set_add( - ctx.target.type_str(), - ctx.target.value_str(), + target.type_str(), + target.value_str(), key, value, &ctx.email, ctx.timestamp, )?; - print_result("added", key, &ctx.target, json); + print_result("added", key, &target, json); Ok(()) } @@ -151,15 +129,20 @@ pub fn run_rm( json: bool, timestamp: Option, ) -> Result<()> { - let ctx = open_context(target_str, key, timestamp)?; + let mut target = Target::parse(target_str)?; + validate_key(key)?; + + let ctx = CommandContext::open_gix(timestamp)?; + ctx.resolve_target(&mut target)?; + ctx.db.set_rm( - ctx.target.type_str(), - ctx.target.value_str(), + target.type_str(), + target.value_str(), key, value, &ctx.email, ctx.timestamp, )?; - print_result("removed", key, &ctx.target, json); + print_result("removed", key, &target, json); Ok(()) } diff --git a/src/commands/show.rs b/src/commands/show.rs index c6e000a..1fc755f 100644 --- a/src/commands/show.rs +++ b/src/commands/show.rs @@ -6,8 +6,7 @@ use anyhow::{Context, Result}; use chrono::{TimeZone, Utc}; use git2::Repository; -use crate::db::Db; -use crate::git_utils; +use crate::context::CommandContext; // ── ANSI colours ────────────────────────────────────────────────────────────── const RESET: &str = "\x1b[0m"; @@ -19,7 +18,8 @@ const CYAN: &str = "\x1b[36m"; const BLUE: &str = "\x1b[34m"; pub fn run(commit_ref: &str) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; // Resolve the ref to a full commit SHA let obj = repo @@ -34,7 +34,7 @@ pub fn run(commit_ref: &str) -> Result<()> { println!("{YELLOW}Commit:{RESET} {CYAN}{sha}{RESET}"); // Try to get change-id from GitButler - let change_id = get_change_id(&repo, &sha); + let change_id = get_change_id(repo, &sha); if let Some(ref cid) = change_id { println!("{YELLOW}Change-ID:{RESET} {CYAN}{cid}{RESET}"); } @@ -104,14 +104,11 @@ pub fn run(commit_ref: &str) -> Result<()> { } // ── Metadata ──────────────────────────────────────────────────────────── - let db_path = git_utils::git2_db_path(&repo)?; - let db = Db::open(&db_path)?; - // Collect metadata from both commit SHA and change-id let mut meta_entries: Vec<(String, String, String)> = Vec::new(); // (source, key, display_value) // Metadata on commit: - let commit_entries = db.get_all("commit", &sha, None).unwrap_or_default(); + let commit_entries = ctx.db.get_all("commit", &sha, None).unwrap_or_default(); for (key, value, value_type, _is_git_ref) in &commit_entries { let display = format_meta_value(value, value_type); meta_entries.push(("commit".to_string(), key.clone(), display)); @@ -119,7 +116,7 @@ pub fn run(commit_ref: &str) -> Result<()> { // Metadata on change-id: if let Some(ref cid) = change_id { - let cid_entries = db.get_all("change-id", cid, None).unwrap_or_default(); + let cid_entries = ctx.db.get_all("change-id", cid, None).unwrap_or_default(); for (key, value, value_type, _is_git_ref) in &cid_entries { let display = format_meta_value(value, value_type); meta_entries.push(("change-id".to_string(), key.clone(), display)); diff --git a/src/commands/stats.rs b/src/commands/stats.rs index 0698ceb..d3adf35 100644 --- a/src/commands/stats.rs +++ b/src/commands/stats.rs @@ -2,15 +2,12 @@ use std::collections::BTreeMap; use anyhow::Result; -use crate::db::Db; -use crate::git_utils; +use crate::context::CommandContext; pub fn run() -> Result<()> { - let repo = git_utils::discover_repo()?; - let db_path = git_utils::db_path(&repo)?; - let db = Db::open(&db_path)?; + let ctx = CommandContext::open_gix(None)?; - let rows = db.stats_by_target_type_and_key()?; + let rows = ctx.db.stats_by_target_type_and_key()?; if rows.is_empty() { println!("no metadata stored"); @@ -18,14 +15,14 @@ pub fn run() -> Result<()> { } // Show storage counts and size histogram at the top - let (sqlite_count, git_ref_count) = db.stats_storage_counts()?; + let (sqlite_count, git_ref_count) = ctx.db.stats_storage_counts()?; println!( "{} values in sqlite, {} values as git refs", sqlite_count, git_ref_count ); println!(); - let (buckets, _) = db.stats_value_size_histogram()?; + let (buckets, _) = ctx.db.stats_value_size_histogram()?; let max_count = buckets.iter().map(|(_, c)| *c).max().unwrap_or(1).max(1); let bar_width = 30usize; println!("value sizes (inline):"); diff --git a/src/commands/teardown.rs b/src/commands/teardown.rs index 8fa89fe..77d1901 100644 --- a/src/commands/teardown.rs +++ b/src/commands/teardown.rs @@ -1,18 +1,20 @@ use anyhow::Result; +use crate::context::CommandContext; use crate::git_utils; pub fn run() -> Result<()> { - let repo = git_utils::git2_discover_repo()?; - let ns = git_utils::git2_get_namespace(&repo)?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; + let ns = git_utils::git2_get_namespace(repo)?; // Remove the SQLite database - let db = git_utils::git2_db_path(&repo)?; - if db.exists() { - std::fs::remove_file(&db)?; - println!("Removed {}", db.display()); + let db_path = git_utils::git2_db_path(repo)?; + if db_path.exists() { + std::fs::remove_file(&db_path)?; + println!("Removed {}", db_path.display()); } else { - println!("No database found at {}", db.display()); + println!("No database found at {}", db_path.display()); } // Remove all refs under refs/{namespace}/ diff --git a/src/commands/watch.rs b/src/commands/watch.rs index 36e799a..082c525 100644 --- a/src/commands/watch.rs +++ b/src/commands/watch.rs @@ -9,6 +9,7 @@ use anyhow::{bail, Context, Result}; use chrono::Utc; use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; +use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; @@ -23,7 +24,8 @@ const RED: &str = "\x1b[31m"; const RESET: &str = "\x1b[0m"; pub fn run(agent: &str, debounce_secs: u64) -> Result<()> { - let repo = git_utils::git2_discover_repo()?; + let ctx = CommandContext::open_git2(None)?; + let repo = ctx.git2_repo()?; let workdir = repo .workdir() .context("bare repo not supported")? diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..9d7271f --- /dev/null +++ b/src/context.rs @@ -0,0 +1,135 @@ +use std::cell::OnceCell; + +use anyhow::Result; +use chrono::Utc; + +use crate::db::Db; +use crate::git_utils; +use crate::types::Target; + +/// Shared context for all commands. +/// +/// Holds the git repositories (lazily initialized), database, user email, +/// and a timestamp. Commands construct a `CommandContext` at the start and +/// use it throughout their execution, eliminating repeated boilerplate for +/// repo discovery, database opening, and timestamp generation. +/// +/// Inspired by GitButler's `but-ctx`, the context holds both `git2` and +/// `gix` repository handles via `OnceCell`. The primary repo (whichever +/// was used for discovery) is eagerly initialized; the other is lazily +/// discovered on first access if a command needs it. +pub struct CommandContext { + git2_repo: OnceCell, + gix_repo: OnceCell, + /// The gmeta SQLite database. + pub db: Db, + /// The user's email from git config, used for authorship tracking. + pub email: String, + /// Millisecond-precision timestamp for this command invocation. + pub timestamp: i64, +} + +impl CommandContext { + /// Discover the repository via `gix` and build the command context. + /// + /// The `gix` repository is eagerly initialized; `git2` is lazily + /// discovered on first access via [`git2_repo`](Self::git2_repo). + /// + /// # Parameters + /// - `timestamp_override`: If `Some`, uses the given timestamp instead + /// of the current wall-clock time. Useful for deterministic tests and + /// the `set --timestamp` flag. + pub fn open_gix(timestamp_override: Option) -> Result { + let repo = git_utils::discover_repo()?; + let db_path = git_utils::db_path(&repo)?; + let email = git_utils::get_email(&repo)?; + let timestamp = timestamp_override.unwrap_or_else(|| Utc::now().timestamp_millis()); + let db = Db::open(&db_path)?; + + let gix_cell = OnceCell::new(); + // Safe: cell is freshly created, set always succeeds. + let _ = gix_cell.set(repo); + + Ok(Self { + git2_repo: OnceCell::new(), + gix_repo: gix_cell, + db, + email, + timestamp, + }) + } + + /// Discover the repository via `git2` and build the command context. + /// + /// The `git2` repository is eagerly initialized; `gix` is lazily + /// discovered on first access via [`gix_repo`](Self::gix_repo). + /// + /// # Parameters + /// - `timestamp_override`: If `Some`, uses the given timestamp instead + /// of the current wall-clock time. + pub fn open_git2(timestamp_override: Option) -> Result { + let repo = git_utils::git2_discover_repo()?; + let db_path = git_utils::git2_db_path(&repo)?; + let email = git_utils::git2_get_email(&repo)?; + let timestamp = timestamp_override.unwrap_or_else(|| Utc::now().timestamp_millis()); + let db = Db::open(&db_path)?; + + let git2_cell = OnceCell::new(); + let _ = git2_cell.set(repo); + + Ok(Self { + git2_repo: git2_cell, + gix_repo: OnceCell::new(), + db, + email, + timestamp, + }) + } + + /// Access the `git2` repository handle. + /// + /// If the context was opened via `gix`, the `git2` repository is + /// lazily discovered on the first call. + pub fn git2_repo(&self) -> Result<&git2::Repository> { + if let Some(repo) = self.git2_repo.get() { + return Ok(repo); + } + let repo = git_utils::git2_discover_repo()?; + // Cell is single-threaded and was just checked empty; set succeeds. + let _ = self.git2_repo.set(repo); + self.git2_repo + .get() + .ok_or_else(|| anyhow::anyhow!("git2 repository cell unexpectedly empty after set")) + } + + /// Access the `gix` repository handle. + /// + /// If the context was opened via `git2`, the `gix` repository is + /// lazily discovered on the first call. + pub fn gix_repo(&self) -> Result<&gix::Repository> { + if let Some(repo) = self.gix_repo.get() { + return Ok(repo); + } + let repo = git_utils::discover_repo()?; + let _ = self.gix_repo.set(repo); + self.gix_repo + .get() + .ok_or_else(|| anyhow::anyhow!("gix repository cell unexpectedly empty after set")) + } + + /// Resolve a target's partial commit SHA using the `git2` repository. + /// + /// # Parameters + /// - `target`: The target whose commit SHA should be expanded to 40 characters. + pub fn git2_resolve_target(&self, target: &mut Target) -> Result<()> { + target.git2_resolve(self.git2_repo()?) + } + + /// Resolve a target's partial commit SHA using the `gix` repository. + /// + /// # Parameters + /// - `target`: The target whose commit SHA should be expanded to 40 characters. + pub fn resolve_target(&self, target: &mut Target) -> Result<()> { + target.resolve(self.gix_repo()?) + } +} diff --git a/src/main.rs b/src/main.rs index 366a45a..2cd3d4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod cli; mod commands; +mod context; mod db; mod git_utils; mod list_value; From 7b0824a88f77f79dfa165f9abce3624b5467f259 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Mon, 30 Mar 2026 19:52:47 -0700 Subject: [PATCH 2/3] refactor: move namespace and ref helpers to CommandContext --- src/commands/log.rs | 10 ++++------ src/commands/materialize.rs | 6 +++--- src/commands/promisor.rs | 3 +-- src/commands/prune.rs | 2 +- src/commands/pull.rs | 8 +++----- src/commands/push.rs | 8 ++++---- src/commands/remote.rs | 18 ++++-------------- src/commands/serialize.rs | 7 +++---- src/commands/teardown.rs | 2 +- src/context.rs | 17 +++++++++++++++++ src/git_utils.rs | 19 ------------------- 11 files changed, 41 insertions(+), 59 deletions(-) diff --git a/src/commands/log.rs b/src/commands/log.rs index 0df3eff..b7ce698 100644 --- a/src/commands/log.rs +++ b/src/commands/log.rs @@ -5,7 +5,6 @@ use anyhow::{Context, Result}; use git2::{Oid, Repository, Sort}; use crate::context::CommandContext; -use crate::db::Db; use crate::git_utils; // ── ANSI colours ────────────────────────────────────────────────────────────── @@ -22,11 +21,10 @@ pub fn run( count: usize, // max commits to show metadata_only: bool, // skip commits with no metadata ) -> Result<()> { - let ctx = CommandContext::open_git2(None)?; + let mut ctx = CommandContext::open_git2(None)?; + // Attach a repo to the Db so blob-ref values are resolved during reads. + ctx.db.repo = Some(git_utils::git2_discover_repo()?); let repo = ctx.git2_repo()?; - // log needs a separate Db with an embedded repo for blob resolution during reads - let db_path = git_utils::git2_db_path(repo)?; - let db = Db::open_with_repo(&db_path, git_utils::git2_discover_repo()?)?; // Resolve start ref → OID let start_oid = resolve_start(repo, start_ref)?; @@ -48,7 +46,7 @@ pub fn run( let sha = oid.to_string(); // Fetch metadata before deciding whether to print the commit - let entries = db.get_all("commit", &sha, None).unwrap_or_default(); + let entries = ctx.db.get_all("commit", &sha, None).unwrap_or_default(); // get_all returns (key, value, value_type, is_git_ref) // value is a JSON-encoded string for string types let meta: Vec<(String, String)> = entries diff --git a/src/commands/materialize.rs b/src/commands/materialize.rs index 21ab6ef..f099d92 100644 --- a/src/commands/materialize.rs +++ b/src/commands/materialize.rs @@ -115,8 +115,8 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { let ctx = CommandContext::open_git2(None)?; let repo = ctx.git2_repo()?; - let ns = git_utils::git2_get_namespace(repo)?; - let local_ref_name = git_utils::git2_local_ref(repo)?; + let ns = &ctx.namespace; + let local_ref_name = ctx.local_ref(); if verbose { eprintln!("[verbose] namespace: {}", ns); @@ -131,7 +131,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { } // Find remote refs to materialize - let remote_refs = find_remote_refs(repo, &ns, remote)?; + let remote_refs = find_remote_refs(repo, ns, remote)?; if remote_refs.is_empty() { println!("no remote metadata refs found"); diff --git a/src/commands/promisor.rs b/src/commands/promisor.rs index 7ad00c6..abd6037 100644 --- a/src/commands/promisor.rs +++ b/src/commands/promisor.rs @@ -1,12 +1,11 @@ use anyhow::{bail, Result}; use crate::context::CommandContext; -use crate::git_utils; pub fn run() -> Result<()> { let ctx = CommandContext::open_git2(None)?; let repo = ctx.git2_repo()?; - let ns = git_utils::git2_get_namespace(repo)?; + let ns = &ctx.namespace; let tracking_ref = format!("refs/{}/remotes/main", ns); let tip_commit = match repo.find_reference(&tracking_ref) { diff --git a/src/commands/prune.rs b/src/commands/prune.rs index d24d121..c485252 100644 --- a/src/commands/prune.rs +++ b/src/commands/prune.rs @@ -39,7 +39,7 @@ pub fn run(dry_run: bool) -> Result<()> { .unwrap_or_else(|| "?".to_string()); // Find the current serialized tree - let ref_name = git_utils::git2_local_ref(repo)?; + let ref_name = ctx.local_ref(); let Some(current_commit) = repo .find_reference(&ref_name) .ok() diff --git a/src/commands/pull.rs b/src/commands/pull.rs index d9d47b2..a5dafd9 100644 --- a/src/commands/pull.rs +++ b/src/commands/pull.rs @@ -9,7 +9,7 @@ use crate::types; pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { let ctx = CommandContext::open_git2(None)?; let repo = ctx.git2_repo()?; - let ns = git_utils::git2_get_namespace(repo)?; + let ns = &ctx.namespace; let remote_name = git_utils::resolve_meta_remote(repo, remote)?; let remote_refspec = format!("refs/{}/main", ns); @@ -41,10 +41,8 @@ pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { // Check if we need to materialize even if no new commits were fetched // (e.g. remote add fetched but never materialized) - let needs_materialize = ctx.db.get_last_materialized()?.is_none() - || repo - .find_reference(&git_utils::git2_local_ref(repo)?) - .is_err(); + let needs_materialize = + ctx.db.get_last_materialized()?.is_none() || repo.find_reference(&ctx.local_ref()).is_err(); // Count new commits match (old_tip, new_tip) { diff --git a/src/commands/push.rs b/src/commands/push.rs index 52b1ff3..884f6f2 100644 --- a/src/commands/push.rs +++ b/src/commands/push.rs @@ -20,9 +20,9 @@ pub fn run_readme(remote: Option<&str>, verbose: bool) -> Result<()> { let meta_url = config .get_string(&format!("remote.{}.url", remote_name)) .unwrap_or_else(|_| "unknown".to_string()); - let ns = git_utils::git2_get_namespace(repo)?; + let ns = &ctx.namespace; - let readme_content = generate_readme(&origin_url, &meta_url, &ns); + let readme_content = generate_readme(&origin_url, &meta_url, ns); if verbose { eprintln!("[verbose] remote: {}", remote_name); @@ -157,11 +157,11 @@ const MAX_RETRIES: u32 = 5; pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { let ctx = CommandContext::open_git2(None)?; let repo = ctx.git2_repo()?; - let ns = git_utils::git2_get_namespace(repo)?; + let ns = &ctx.namespace; // Resolve which remote to push to let remote_name = git_utils::resolve_meta_remote(repo, remote)?; - let local_ref = git_utils::git2_local_ref(repo)?; + let local_ref = ctx.local_ref(); let remote_refspec = format!("refs/{}/main", ns); if verbose { diff --git a/src/commands/remote.rs b/src/commands/remote.rs index 3e594ef..0535ff0 100644 --- a/src/commands/remote.rs +++ b/src/commands/remote.rs @@ -2,7 +2,6 @@ use anyhow::{bail, Result}; use crate::commands::{materialize, pull, serialize}; use crate::context::CommandContext; -use crate::db::Db; use crate::git_utils; /// Expand shorthand "owner/repo" to a full GitHub SSH URL. @@ -59,11 +58,7 @@ fn check_remote_refs(repo: &git2::Repository, url: &str, ns: &str) -> Result<(bo pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Result<()> { let ctx = CommandContext::open_git2(None)?; let repo = ctx.git2_repo()?; - let ns = namespace_override - .map(|s| s.to_string()) - .unwrap_or_else(|| { - git_utils::git2_get_namespace(repo).unwrap_or_else(|_| "meta".to_string()) - }); + let ns = namespace_override.unwrap_or(&ctx.namespace).to_string(); let url = expand_url(url); // Check if this remote name already exists @@ -136,10 +131,7 @@ pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Resul // Initial blobless fetch let fetch_refspec = format!("refs/{ns}/main:refs/{ns}/remotes/main"); eprint!("Fetching metadata (blobless)..."); - match git_utils::git2_run_git( - repo, - &["fetch", "--filter=blob:none", name, &fetch_refspec], - ) { + match git_utils::git2_run_git(repo, &["fetch", "--filter=blob:none", name, &fetch_refspec]) { Ok(_) => { eprintln!(" done."); @@ -183,10 +175,8 @@ pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Resul let tracking_ref_name = format!("refs/{}/remotes/main", ns); if let Ok(r) = repo.find_reference(&tracking_ref_name) { if let Ok(tip) = r.peel_to_commit() { - let db_path = git_utils::git2_db_path(repo)?; - let db = Db::open(&db_path)?; let count = - pull::insert_promisor_entries_pub(repo, &db, tip.id(), None, false)?; + pull::insert_promisor_entries_pub(repo, &ctx.db, tip.id(), None, false)?; if count > 0 { eprintln!("Indexed {} keys from history (available on demand).", count); } @@ -205,7 +195,7 @@ pub fn run_add(url: &str, name: &str, namespace_override: Option<&str>) -> Resul pub fn run_remove(name: &str) -> Result<()> { let ctx = CommandContext::open_git2(None)?; let repo = ctx.git2_repo()?; - let ns = git_utils::git2_get_namespace(repo)?; + let ns = &ctx.namespace; // Verify this is a meta remote let config = repo.config()?; diff --git a/src/commands/serialize.rs b/src/commands/serialize.rs index 5d69a7d..4fdfed6 100644 --- a/src/commands/serialize.rs +++ b/src/commands/serialize.rs @@ -203,7 +203,7 @@ pub fn run(verbose: bool) -> Result<()> { let ctx = CommandContext::open_git2(None)?; let repo = ctx.git2_repo()?; - let local_ref_name = git_utils::git2_local_ref(repo)?; + let local_ref_name = ctx.local_ref(); let last_materialized = ctx.db.get_last_materialized()?; if verbose { @@ -558,7 +558,7 @@ pub fn run(verbose: bool) -> Result<()> { let sig = git2::Signature::now(&name, &email)?; for dest in &all_dests { - let ref_name = git_utils::git2_destination_ref(repo, dest)?; + let ref_name = ctx.destination_ref(dest); let empty_meta: Vec = Vec::new(); let empty_tomb: Vec = Vec::new(); let empty_set_tomb: Vec = Vec::new(); @@ -660,8 +660,7 @@ pub fn run(verbose: bool) -> Result<()> { ); } - let prune_tree_oid = - prune_tree(repo, tree_oid, prune_rules, &ctx.db, verbose)?; + let prune_tree_oid = prune_tree(repo, tree_oid, prune_rules, &ctx.db, verbose)?; if prune_tree_oid != tree_oid { if verbose { diff --git a/src/commands/teardown.rs b/src/commands/teardown.rs index 77d1901..2b013c3 100644 --- a/src/commands/teardown.rs +++ b/src/commands/teardown.rs @@ -6,7 +6,7 @@ use crate::git_utils; pub fn run() -> Result<()> { let ctx = CommandContext::open_git2(None)?; let repo = ctx.git2_repo()?; - let ns = git_utils::git2_get_namespace(repo)?; + let ns = &ctx.namespace; // Remove the SQLite database let db_path = git_utils::git2_db_path(repo)?; diff --git a/src/context.rs b/src/context.rs index 9d7271f..8b859b6 100644 --- a/src/context.rs +++ b/src/context.rs @@ -27,6 +27,9 @@ pub struct CommandContext { pub email: String, /// Millisecond-precision timestamp for this command invocation. pub timestamp: i64, + /// The metadata namespace from git config (e.g. `"meta"`). + /// Used to construct ref paths like `refs/{ns}/local/main`. + pub namespace: String, } impl CommandContext { @@ -43,6 +46,7 @@ impl CommandContext { let repo = git_utils::discover_repo()?; let db_path = git_utils::db_path(&repo)?; let email = git_utils::get_email(&repo)?; + let namespace = git_utils::get_namespace(&repo)?; let timestamp = timestamp_override.unwrap_or_else(|| Utc::now().timestamp_millis()); let db = Db::open(&db_path)?; @@ -56,6 +60,7 @@ impl CommandContext { db, email, timestamp, + namespace, }) } @@ -71,6 +76,7 @@ impl CommandContext { let repo = git_utils::git2_discover_repo()?; let db_path = git_utils::git2_db_path(&repo)?; let email = git_utils::git2_get_email(&repo)?; + let namespace = git_utils::git2_get_namespace(&repo)?; let timestamp = timestamp_override.unwrap_or_else(|| Utc::now().timestamp_millis()); let db = Db::open(&db_path)?; @@ -83,6 +89,7 @@ impl CommandContext { db, email, timestamp, + namespace, }) } @@ -117,6 +124,16 @@ impl CommandContext { .ok_or_else(|| anyhow::anyhow!("gix repository cell unexpectedly empty after set")) } + /// The local serialization ref, e.g. `refs/meta/local/main`. + pub fn local_ref(&self) -> String { + format!("refs/{}/local/main", self.namespace) + } + + /// A ref for a named destination, e.g. `refs/meta/local/{destination}`. + pub fn destination_ref(&self, destination: &str) -> String { + format!("refs/{}/local/{}", self.namespace, destination) + } + /// Resolve a target's partial commit SHA using the `git2` repository. /// /// # Parameters diff --git a/src/git_utils.rs b/src/git_utils.rs index 894e810..6ff8eae 100644 --- a/src/git_utils.rs +++ b/src/git_utils.rs @@ -44,25 +44,6 @@ pub fn git2_get_namespace(repo: &Repository) -> Result { Ok(ns) } -/// Get the local ref name for serialization (git2). -pub fn git2_local_ref(repo: &Repository) -> Result { - let ns = git2_get_namespace(repo)?; - Ok(format!("refs/{}/local/main", ns)) -} - -/// Get the ref name for a named destination (git2). -pub fn git2_destination_ref(repo: &Repository, destination: &str) -> Result { - let ns = git2_get_namespace(repo)?; - Ok(format!("refs/{}/local/{}", ns, destination)) -} - -/// Get the ref pattern for remote metadata (git2). -#[allow(dead_code)] -pub fn git2_remote_ref(repo: &Repository, remote: &str) -> Result { - let ns = git2_get_namespace(repo)?; - Ok(format!("refs/{}/{}", ns, remote)) -} - /// Expand a partial commit SHA to the full 40-char hex string (git2). pub fn git2_resolve_commit_sha(repo: &Repository, partial: &str) -> Result { let obj = repo From 7425686d00e66430ba393097f62705c34b856555 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Mon, 30 Mar 2026 20:04:34 -0700 Subject: [PATCH 3/3] refactor: reorganize prune commands into submodule --- src/commands/mod.rs | 3 - src/commands/{auto_prune.rs => prune/auto.rs} | 0 .../{config_prune.rs => prune/config.rs} | 2 +- .../{local_prune.rs => prune/local.rs} | 2 +- src/commands/prune/mod.rs | 4 ++ src/commands/{prune.rs => prune/tree.rs} | 2 +- src/commands/serialize.rs | 10 ++-- src/db.rs | 31 ---------- src/git_utils.rs | 56 +------------------ src/main.rs | 6 +- src/types.rs | 4 -- 11 files changed, 16 insertions(+), 104 deletions(-) rename src/commands/{auto_prune.rs => prune/auto.rs} (100%) rename src/commands/{config_prune.rs => prune/config.rs} (99%) rename src/commands/{local_prune.rs => prune/local.rs} (98%) create mode 100644 src/commands/prune/mod.rs rename src/commands/{prune.rs => prune/tree.rs} (98%) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b18f583..37f87b9 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,12 +1,9 @@ -pub mod auto_prune; pub mod bench; pub mod config; -pub mod config_prune; pub mod get; pub mod import; pub mod inspect; pub mod list; -pub mod local_prune; pub mod log; pub mod materialize; pub mod promisor; diff --git a/src/commands/auto_prune.rs b/src/commands/prune/auto.rs similarity index 100% rename from src/commands/auto_prune.rs rename to src/commands/prune/auto.rs diff --git a/src/commands/config_prune.rs b/src/commands/prune/config.rs similarity index 99% rename from src/commands/config_prune.rs rename to src/commands/prune/config.rs index 2f1ce18..5295a99 100644 --- a/src/commands/config_prune.rs +++ b/src/commands/prune/config.rs @@ -1,7 +1,7 @@ use anyhow::Result; use dialoguer::{Confirm, Input, Select}; -use crate::commands::auto_prune::{parse_size, read_prune_rules}; +use super::auto::{parse_size, read_prune_rules}; use crate::context::CommandContext; pub fn run() -> Result<()> { diff --git a/src/commands/local_prune.rs b/src/commands/prune/local.rs similarity index 98% rename from src/commands/local_prune.rs rename to src/commands/prune/local.rs index 6a3c9b8..6a56a02 100644 --- a/src/commands/local_prune.rs +++ b/src/commands/prune/local.rs @@ -1,7 +1,7 @@ use anyhow::Result; use rusqlite::params; -use crate::commands::auto_prune::{parse_since_to_cutoff_ms, read_prune_rules}; +use super::auto::{parse_since_to_cutoff_ms, read_prune_rules}; use crate::context::CommandContext; pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { diff --git a/src/commands/prune/mod.rs b/src/commands/prune/mod.rs new file mode 100644 index 0000000..2d33015 --- /dev/null +++ b/src/commands/prune/mod.rs @@ -0,0 +1,4 @@ +pub mod auto; +pub mod config; +pub mod local; +pub mod tree; diff --git a/src/commands/prune.rs b/src/commands/prune/tree.rs similarity index 98% rename from src/commands/prune.rs rename to src/commands/prune/tree.rs index c485252..3a4670f 100644 --- a/src/commands/prune.rs +++ b/src/commands/prune/tree.rs @@ -7,7 +7,7 @@ use anyhow::Result; -use crate::commands::auto_prune::parse_since_to_cutoff_ms; +use super::auto::parse_since_to_cutoff_ms; use crate::commands::serialize::{ build_filtered_tree, classify_key, count_prune_stats, parse_filter_rules, MAIN_DEST, }; diff --git a/src/commands/serialize.rs b/src/commands/serialize.rs index 4fdfed6..cce8b63 100644 --- a/src/commands/serialize.rs +++ b/src/commands/serialize.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeMap, BTreeSet}; use anyhow::{bail, Context, Result}; use chrono::Utc; -use crate::commands::auto_prune::{self, parse_since_to_cutoff_ms}; +use crate::commands::prune::auto::{self, parse_since_to_cutoff_ms}; use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; @@ -378,7 +378,7 @@ pub fn run(verbose: bool) -> Result<()> { .db .get("project", "", "meta:prune:since")? .and_then(|(value, _, _)| serde_json::from_str::(&value).ok()); - let prune_rules = auto_prune::read_prune_rules(&ctx.db)?; + let prune_rules = auto::read_prune_rules(&ctx.db)?; let prune_cutoff_ms = prune_since .as_deref() .map(parse_since_to_cutoff_ms) @@ -643,7 +643,7 @@ pub fn run(verbose: bool) -> Result<()> { .unwrap_or_else(|| "none".to_string()) ); } - if auto_prune::should_prune(repo, tree_oid, prune_rules)? { + if auto::should_prune(repo, tree_oid, prune_rules)? { eprintln!( "Auto-prune triggered, pruning with --since={}...", prune_rules.since @@ -1100,7 +1100,7 @@ fn merge_dir_into_tree( pub fn prune_tree( repo: &git2::Repository, tree_oid: git2::Oid, - rules: &auto_prune::PruneRules, + rules: &auto::PruneRules, db: &Db, verbose: bool, ) -> Result { @@ -1130,7 +1130,7 @@ pub fn prune_tree( // Check min-size: if the subtree is smaller than the threshold, keep it if min_size > 0 { - let size = auto_prune::compute_tree_size_for(repo, &subtree)?; + let size = auto::compute_tree_size_for(repo, &subtree)?; if size < min_size { if verbose { eprintln!( diff --git a/src/db.rs b/src/db.rs index 8560c0d..9980c5d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1429,37 +1429,6 @@ impl Db { Ok(results) } - /// Get the set of (target_type, target_value, key) that have been locally - /// modified since a given timestamp. - #[allow(dead_code)] - pub fn get_locally_modified_keys( - &self, - since: Option, - ) -> Result> { - use std::collections::HashSet; - - let since_ts = since.unwrap_or(0); - let mut stmt = self.conn.prepare( - "SELECT DISTINCT target_type, target_value, key - FROM metadata_log - WHERE timestamp > ?1", - )?; - - let rows = stmt.query_map(params![since_ts], |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - )) - })?; - - let mut result = HashSet::new(); - for row in rows { - result.insert(row?); - } - Ok(result) - } - /// Get the last materialized timestamp. pub fn get_last_materialized(&self) -> Result> { let mut stmt = self diff --git a/src/git_utils.rs b/src/git_utils.rs index 6ff8eae..90af9cb 100644 --- a/src/git_utils.rs +++ b/src/git_utils.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process::Command; use anyhow::{bail, Context, Result}; @@ -312,65 +312,11 @@ pub fn get_email(repo: &gix::Repository) -> Result { Ok(gix_config_string(repo, "user.email", "unknown")) } -/// Get the user's name from Git config. -#[allow(dead_code)] -pub fn get_name(repo: &gix::Repository) -> Result { - Ok(gix_config_string(repo, "user.name", "unknown")) -} - /// Get the meta namespace from Git config (defaults to "meta"). pub fn get_namespace(repo: &gix::Repository) -> Result { Ok(gix_config_string(repo, "meta.namespace", "meta")) } -/// Get the local ref name for serialization. -#[allow(dead_code)] -pub fn local_ref(repo: &gix::Repository) -> Result { - let ns = get_namespace(repo)?; - Ok(format!("refs/{}/local/main", ns)) -} - -/// Get the ref name for a named destination (e.g. "private" -> "refs/meta/local/private"). -#[allow(dead_code)] -pub fn destination_ref(repo: &gix::Repository, destination: &str) -> Result { - let ns = get_namespace(repo)?; - Ok(format!("refs/{}/local/{}", ns, destination)) -} - -/// Get the ref pattern for remote metadata. -#[allow(dead_code)] -pub fn remote_ref(repo: &gix::Repository, remote: &str) -> Result { - let ns = get_namespace(repo)?; - Ok(format!("refs/{}/{}", ns, remote)) -} - -/// Run a git CLI command in the repository's working directory. -#[allow(dead_code)] -pub fn run_git(repo: &gix::Repository, args: &[&str]) -> Result { - let workdir = repo.workdir().unwrap_or_else(|| repo.git_dir()); - - run_git_in(workdir, args) -} - -fn run_git_in(workdir: &Path, args: &[&str]) -> Result { - let output = Command::new("git") - .args(args) - .current_dir(workdir) - .output() - .context("failed to run git command")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - bail!( - "git {} failed: {}", - args.first().unwrap_or(&""), - stderr.trim() - ); - } - - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} - /// Expand a partial commit SHA to the full 40-char hex string. pub fn resolve_commit_sha(repo: &gix::Repository, partial: &str) -> Result { let obj = repo diff --git a/src/main.rs b/src/main.rs index 2cd3d4a..9b6a309 100644 --- a/src/main.rs +++ b/src/main.rs @@ -138,12 +138,12 @@ fn main() -> Result<()> { value, } => commands::config::run(list, unset, key.as_deref(), value.as_deref()), - Commands::ConfigPrune => commands::config_prune::run(), + Commands::ConfigPrune => commands::prune::config::run(), - Commands::Prune { dry_run } => commands::prune::run(dry_run), + Commands::Prune { dry_run } => commands::prune::tree::run(dry_run), Commands::LocalPrune { dry_run, skip_date } => { - commands::local_prune::run(dry_run, skip_date) + commands::prune::local::run(dry_run, skip_date) } Commands::Teardown => commands::teardown::run(), diff --git a/src/types.rs b/src/types.rs index 561f095..c472213 100644 --- a/src/types.rs +++ b/src/types.rs @@ -271,7 +271,6 @@ pub fn validate_key(key: &str) -> Result<()> { /// Build the full tree path segments for a key under a target. /// Key is split by ':' into subtree segments. -#[allow(dead_code)] pub fn key_to_path_segments(key: &str) -> Vec { key.split(':').map(|s| s.to_string()).collect() } @@ -298,21 +297,18 @@ pub fn build_key_tree_path(target: &Target, key: &str) -> Result { } /// Build the full tree path for a string value. -#[allow(dead_code)] pub fn build_tree_path(target: &Target, key: &str) -> Result { let key_path = build_key_tree_path(target, key)?; Ok(format!("{}/{}", key_path, STRING_VALUE_BLOB)) } /// Build the list directory path for a key. -#[allow(dead_code)] pub fn build_list_tree_dir_path(target: &Target, key: &str) -> Result { let key_path = build_key_tree_path(target, key)?; Ok(format!("{}/{}", key_path, LIST_VALUE_DIR)) } /// Build the tombstone blob path for a key. -#[allow(dead_code)] pub fn build_tombstone_tree_path(target: &Target, key: &str) -> Result { validate_key(key)?; let base = target.tree_base_path();