From 08873b13a5a9fa060c22f411d1cf5f4dfbaa8dd9 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Mon, 30 Mar 2026 21:40:55 -0700 Subject: [PATCH] refactor: use typed enums for TargetType and ValueType --- src/commands/bench/db_bench.rs | 12 +- src/commands/bench/serialize_bench.rs | 9 +- src/commands/config.rs | 18 +- src/commands/get.rs | 101 +++-- src/commands/import.rs | 126 ++++-- src/commands/inspect.rs | 43 +- src/commands/list.rs | 8 +- src/commands/log.rs | 6 +- src/commands/materialize.rs | 93 ++-- src/commands/promisor.rs | 7 +- src/commands/prune/auto.rs | 3 +- src/commands/prune/config.rs | 11 +- src/commands/prune/local.rs | 3 +- src/commands/prune/tree.rs | 3 +- src/commands/pull.rs | 21 +- src/commands/rm.rs | 2 +- src/commands/serialize.rs | 36 +- src/commands/set.rs | 12 +- src/commands/show.rs | 22 +- src/commands/stats.rs | 14 +- src/commands/watch.rs | 15 +- src/db.rs | 628 ++++++++++++++++---------- src/main.rs | 5 +- src/types.rs | 33 +- 24 files changed, 770 insertions(+), 461 deletions(-) diff --git a/src/commands/bench/db_bench.rs b/src/commands/bench/db_bench.rs index c193e0d..c9fa19b 100644 --- a/src/commands/bench/db_bench.rs +++ b/src/commands/bench/db_bench.rs @@ -3,6 +3,7 @@ use std::time::{Duration, Instant}; use crate::db::Db; use crate::git_utils; +use crate::types::TargetType; const RESET: &str = "\x1b[0m"; const BOLD: &str = "\x1b[1m"; @@ -33,9 +34,16 @@ pub fn run() -> Result<()> { let mut sizes: Vec = Vec::with_capacity(total); let mut errors = 0u64; - for (target_type, target_value, key) in &keys { + for (target_type_str, target_value, key) in &keys { let t0 = Instant::now(); - match db.get(target_type, target_value, key) { + let target_type = match TargetType::from_str(target_type_str) { + Ok(tt) => tt, + Err(_) => { + errors += 1; + continue; + } + }; + match db.get(&target_type, target_value, key) { Ok(Some((value, _vtype, _is_git_ref))) => { let elapsed = t0.elapsed(); let bytes = value.len(); diff --git a/src/commands/bench/serialize_bench.rs b/src/commands/bench/serialize_bench.rs index 34522f9..d277760 100644 --- a/src/commands/bench/serialize_bench.rs +++ b/src/commands/bench/serialize_bench.rs @@ -12,6 +12,7 @@ use std::io::Write; use std::time::Instant; use crate::db::Db; +use crate::types::{TargetType, ValueType}; const RESET: &str = "\x1b[0m"; const BOLD: &str = "\x1b[1m"; @@ -186,7 +187,7 @@ fn do_serialize(repo: &git2::Repository, db: &Db, ref_name: &str) -> Result<()> /// (which is all we insert in this bench). fn build_bench_tree( repo: &git2::Repository, - metadata_entries: &[(String, String, String, String, String, i64, bool)], + metadata_entries: &[(String, String, String, String, ValueType, i64, bool)], ) -> Result { use crate::types::{build_tree_path, Target}; use std::collections::BTreeMap; @@ -194,7 +195,7 @@ fn build_bench_tree( let mut files: BTreeMap> = BTreeMap::new(); for (target_type, target_value, key, value, value_type, _ts, is_git_ref) in metadata_entries { - if value_type != "string" { + if *value_type != ValueType::String { continue; } let target = if target_type == "project" { @@ -316,11 +317,11 @@ pub fn run(rounds: usize) -> Result<()> { let json_value = serde_json::to_string(&value)?; db.set( - "commit", + &TargetType::Commit, &sha, &key, &json_value, - "string", + &ValueType::String, "bench@bench", timestamp_base + i as i64, )?; diff --git a/src/commands/config.rs b/src/commands/config.rs index a8c97c8..6b62cb1 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Result}; use crate::context::CommandContext; use crate::db::Db; -use crate::types::validate_key; +use crate::types::{validate_key, TargetType, ValueType}; const CONFIG_PREFIX: &str = "meta:"; @@ -44,11 +44,11 @@ fn run_set(ctx: &CommandContext, key: &str, value: &str) -> Result<()> { let stored_value = serde_json::to_string(value)?; ctx.db.set( - "project", + &TargetType::Project, "", key, &stored_value, - "string", + &ValueType::String, &ctx.email, ctx.timestamp, )?; @@ -56,7 +56,7 @@ fn run_set(ctx: &CommandContext, key: &str, value: &str) -> Result<()> { } fn run_get(db: &Db, key: &str) -> Result<()> { - let result = db.get("project", "", key)?; + let result = db.get(&TargetType::Project, "", key)?; if let Some((value, _value_type, _is_git_ref)) = result { let s: String = serde_json::from_str(&value)?; println!("{}", s); @@ -67,10 +67,10 @@ fn run_get(db: &Db, key: &str) -> Result<()> { fn run_list(db: &Db) -> Result<()> { // Use "meta" (without trailing colon) as the prefix, since get_all // appends ":" for LIKE matching: "meta" → matches "meta" OR "meta:%" - let entries = db.get_all("project", "", Some("meta"))?; + let entries = db.get_all(&TargetType::Project, "", Some("meta"))?; for (key, value, value_type, _is_git_ref) in entries { - let display = match value_type.as_str() { - "string" => { + let display = match value_type { + ValueType::String => { let s: String = serde_json::from_str(&value)?; s } @@ -82,7 +82,9 @@ fn run_list(db: &Db) -> Result<()> { } fn run_unset(ctx: &CommandContext, key: &str) -> Result<()> { - let removed = ctx.db.rm("project", "", key, &ctx.email, ctx.timestamp)?; + let removed = ctx + .db + .rm(&TargetType::Project, "", key, &ctx.email, ctx.timestamp)?; if !removed { eprintln!("key '{}' not found", key); } diff --git a/src/commands/get.rs b/src/commands/get.rs index 5b2fc2a..f956ae5 100644 --- a/src/commands/get.rs +++ b/src/commands/get.rs @@ -6,7 +6,7 @@ use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; use crate::list_value::list_values_from_json; -use crate::types::{self, Target, TargetType}; +use crate::types::{self, Target, TargetType, ValueType}; const NODE_VALUE_KEY: &str = "__value"; @@ -24,7 +24,7 @@ pub fn run( let include_target_subtree = target.target_type == TargetType::Path; let mut entries = ctx.db.get_all_with_target_prefix( - target.type_str(), + &target.target_type, target.value_str(), include_target_subtree, key, @@ -35,12 +35,12 @@ pub fn run( if entries.is_empty() && target.target_type != TargetType::Path { let matches = ctx.db - .find_target_values_by_prefix(target.type_str(), target.value_str(), 2)?; + .find_target_values_by_prefix(&target.target_type, target.value_str(), 2)?; if matches.len() == 1 { let expanded = &matches[0]; - entries = ctx - .db - .get_all_with_target_prefix(target.type_str(), expanded, false, key)?; + entries = + ctx.db + .get_all_with_target_prefix(&target.target_type, expanded, false, key)?; if !entries.is_empty() { eprintln!("expanded to {}:{}", target.type_str(), expanded); } @@ -65,11 +65,11 @@ pub fn run( .collect(); if !promised.is_empty() { - let hydrated = hydrate_promised_entries(repo, &ctx.db, target.type_str(), &promised)?; + let hydrated = hydrate_promised_entries(repo, &ctx.db, &target.target_type, &promised)?; if hydrated > 0 { // Re-query to get the now-resolved values entries = ctx.db.get_all_with_target_prefix( - target.type_str(), + &target.target_type, target.value_str(), include_target_subtree, key, @@ -78,7 +78,7 @@ pub fn run( } // Resolve git refs to actual values, skip any remaining promised entries - let resolved: Vec<(String, String, String, String)> = entries + let resolved: Vec<(String, String, String, ValueType)> = entries .into_iter() .filter(|(_, _, _, _, _, is_promised)| !is_promised) .map( @@ -113,7 +113,7 @@ pub fn run( fn hydrate_promised_entries( repo: &Repository, db: &Db, - target_type: &str, + target_type: &TargetType, entries: &[(String, String)], // (target_value, key) ) -> Result { let ns = git_utils::git2_get_namespace(repo)?; @@ -129,17 +129,17 @@ fn hydrate_promised_entries( struct PendingEntry { idx: usize, oids: Vec, - value_type: String, // "string", "list", or "set" + value_type: ValueType, } let mut pending: Vec = Vec::new(); let mut not_found: Vec = Vec::new(); for (idx, (target_value, key)) in entries.iter().enumerate() { - let target_str = if target_type == "project" { + let target_str = if *target_type == TargetType::Project { "project".to_string() } else { - format!("{}:{}", target_type, target_value) + format!("{}:{}", target_type.as_str(), target_value) }; let parsed_target = match Target::parse(&target_str) { Ok(t) => t, @@ -152,7 +152,7 @@ fn hydrate_promised_entries( pending.push(PendingEntry { idx, oids: vec![oid], - value_type: "string".to_string(), + value_type: ValueType::String, }); continue; } @@ -177,7 +177,7 @@ fn hydrate_promised_entries( pending.push(PendingEntry { idx, oids, - value_type: "list".to_string(), + value_type: ValueType::List, }); continue; } @@ -204,7 +204,7 @@ fn hydrate_promised_entries( pending.push(PendingEntry { idx, oids, - value_type: "set".to_string(), + value_type: ValueType::Set, }); continue; } @@ -252,8 +252,8 @@ fn hydrate_promised_entries( for entry in &pending { let (target_value, key) = &entries[entry.idx]; - match entry.value_type.as_str() { - "string" => { + match entry.value_type { + ValueType::String => { let oid = entry.oids[0]; let blob = match repo.find_blob(oid) { Ok(b) => b, @@ -264,10 +264,17 @@ fn hydrate_promised_entries( Err(_) => continue, }; let json_value = serde_json::to_string(content)?; - db.resolve_promised(target_type, target_value, key, &json_value, "string", false)?; + db.resolve_promised( + target_type, + target_value, + key, + &json_value, + &ValueType::String, + false, + )?; hydrated += 1; } - "list" => { + ValueType::List => { // Read all list entry blobs, build JSON array let mut list_entries = Vec::new(); for oid in &entry.oids { @@ -278,10 +285,17 @@ fn hydrate_promised_entries( } } let json_value = serde_json::to_string(&list_entries)?; - db.resolve_promised(target_type, target_value, key, &json_value, "list", false)?; + db.resolve_promised( + target_type, + target_value, + key, + &json_value, + &ValueType::List, + false, + )?; hydrated += 1; } - "set" => { + ValueType::Set => { // Read all set member blobs, build JSON array let mut set_members = Vec::new(); for oid in &entry.oids { @@ -293,10 +307,16 @@ fn hydrate_promised_entries( } set_members.sort(); let json_value = serde_json::to_string(&set_members)?; - db.resolve_promised(target_type, target_value, key, &json_value, "set", false)?; + db.resolve_promised( + target_type, + target_value, + key, + &json_value, + &ValueType::Set, + false, + )?; hydrated += 1; } - _ => {} } } @@ -325,7 +345,7 @@ fn truncate_str(s: &str, max: usize) -> String { fn print_plain( target: &Target, - entries: &[(String, String, String, String)], + entries: &[(String, String, String, ValueType)], value_only: bool, ) -> Result<()> { if value_only { @@ -358,53 +378,51 @@ fn print_plain( } /// Single-key mode: raw string value, or one line per list/set item. -fn print_value_only(value: &str, value_type: &str) -> Result<()> { +fn print_value_only(value: &str, value_type: &ValueType) -> Result<()> { match value_type { - "string" => { + ValueType::String => { let s: String = serde_json::from_str(value)?; println!("{}", s); } - "list" => { + ValueType::List => { for item in list_values_from_json(value)? { println!("{}", item); } } - "set" => { + ValueType::Set => { let mut set: Vec = serde_json::from_str(value)?; set.sort(); for item in set { println!("{}", item); } } - _ => println!("{}", value), } Ok(()) } /// Multi-key mode: compact one-line representation. -fn format_value_compact(value: &str, value_type: &str) -> Result { +fn format_value_compact(value: &str, value_type: &ValueType) -> Result { match value_type { - "string" => { + ValueType::String => { let s: String = serde_json::from_str(value)?; Ok(s) } - "list" => { + ValueType::List => { let list = list_values_from_json(value)?; Ok(list.join(", ")) } - "set" => { + ValueType::Set => { let mut set: Vec = serde_json::from_str(value)?; set.sort(); Ok(set.join(", ")) } - _ => Ok(value.to_string()), } } fn print_json( db: &Db, target: &Target, - entries: &[(String, String, String, String)], + entries: &[(String, String, String, ValueType)], with_authorship: bool, ) -> Result<()> { let mut root = Map::new(); @@ -413,7 +431,7 @@ fn print_json( let parsed_value = parse_stored_value(value, value_type)?; let leaf_value = if with_authorship { - let authorship = db.get_authorship(target.type_str(), entry_target_value, key)?; + let authorship = db.get_authorship(&target.target_type, entry_target_value, key)?; let (author, timestamp) = authorship.unwrap_or_else(|| ("unknown".to_string(), 0)); json!({ "value": parsed_value, @@ -441,22 +459,21 @@ fn print_json( Ok(()) } -fn parse_stored_value(value: &str, value_type: &str) -> Result { +fn parse_stored_value(value: &str, value_type: &ValueType) -> Result { match value_type { - "string" => { + ValueType::String => { let s: String = serde_json::from_str(value)?; Ok(Value::String(s)) } - "list" => { + ValueType::List => { let list = list_values_from_json(value)?; Ok(Value::Array(list.into_iter().map(Value::String).collect())) } - "set" => { + ValueType::Set => { let mut set: Vec = serde_json::from_str(value)?; set.sort(); Ok(Value::Array(set.into_iter().map(Value::String).collect())) } - _ => Ok(serde_json::from_str(value)?), } } diff --git a/src/commands/import.rs b/src/commands/import.rs index 66d7866..f095366 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -6,9 +6,9 @@ use serde_json::Value; use crate::context::CommandContext; use crate::db::Db; -use crate::types::GIT_REF_THRESHOLD; +use crate::types::{ImportFormat, TargetType, ValueType, GIT_REF_THRESHOLD}; -pub fn run(format: &str, dry_run: bool, since: Option<&str>) -> Result<()> { +pub fn run(format: ImportFormat, dry_run: bool, since: Option<&str>) -> Result<()> { let since_epoch = match since { Some(date_str) => { let date = @@ -22,9 +22,8 @@ pub fn run(format: &str, dry_run: bool, since: Option<&str>) -> Result<()> { }; match format { - "entire" => run_entire(dry_run, since_epoch), - "git-ai" => run_git_ai(dry_run, since_epoch), - _ => bail!("unsupported import format: {}", format), + ImportFormat::Entire => run_entire(dry_run, since_epoch), + ImportFormat::GitAi => run_git_ai(dry_run, since_epoch), } } @@ -162,7 +161,8 @@ fn import_checkpoints_from_commits( // Skip if already imported if let Some(db) = db { - if let Ok(Some(_)) = db.get("commit", &commit_sha, "agent:checkpoint-id") { + if let Ok(Some(_)) = db.get(&TargetType::Commit, &commit_sha, "agent:checkpoint-id") + { skipped += 1; continue; } @@ -208,11 +208,11 @@ fn import_checkpoints_from_commits( repo, db, dry_run, - "commit", + &TargetType::Commit, &commit_sha, "agent:checkpoint-id", &json_string(checkpoint_id), - "string", + &ValueType::String, email, ts, )?; @@ -237,11 +237,11 @@ fn import_checkpoints_from_commits( repo, db, dry_run, - "commit", + &TargetType::Commit, &commit_sha, &key, &json_val, - "string", + &ValueType::String, email, ts, )?; @@ -320,7 +320,16 @@ fn import_session( let key = format!("{}:{}", key_prefix, gmeta_key); let json_val = json_encode_value(val)?; count += set_value( - repo, db, dry_run, "commit", commit_sha, &key, &json_val, "string", email, *ts, + repo, + db, + dry_run, + &TargetType::Commit, + commit_sha, + &key, + &json_val, + &ValueType::String, + email, + *ts, )?; *ts += 1; } @@ -337,7 +346,16 @@ fn import_session( let key = format!("{}:{}", key_prefix, gmeta_key); let json_val = json_encode_value(val)?; count += set_value( - repo, db, dry_run, "commit", commit_sha, &key, &json_val, "string", email, *ts, + repo, + db, + dry_run, + &TargetType::Commit, + commit_sha, + &key, + &json_val, + &ValueType::String, + email, + *ts, )?; *ts += 1; } @@ -351,11 +369,11 @@ fn import_session( repo, db, dry_run, - "commit", + &TargetType::Commit, commit_sha, &key, &json_string(&content), - "string", + &ValueType::String, email, *ts, )?; @@ -368,7 +386,16 @@ fn import_session( if !content.trim().is_empty() { let json_val = json_string(&content); count += set_value( - repo, db, dry_run, "commit", commit_sha, &key, &json_val, "string", email, *ts, + repo, + db, + dry_run, + &TargetType::Commit, + commit_sha, + &key, + &json_val, + &ValueType::String, + email, + *ts, )?; *ts += 1; } @@ -381,11 +408,11 @@ fn import_session( repo, db, dry_run, - "commit", + &TargetType::Commit, commit_sha, &key, &json_string(content.trim()), - "string", + &ValueType::String, email, *ts, )?; @@ -415,11 +442,11 @@ fn import_session( repo, db, dry_run, - "commit", + &TargetType::Commit, commit_sha, &key, &json_string(&content), - "string", + &ValueType::String, email, *ts, )?; @@ -455,8 +482,16 @@ fn import_session( } let encoded = crate::list_value::encode_entries(&entries)?; count += set_value( - repo, db, dry_run, "commit", commit_sha, &key, &encoded, "list", - email, *ts, + repo, + db, + dry_run, + &TargetType::Commit, + commit_sha, + &key, + &encoded, + &ValueType::List, + email, + *ts, )?; *ts += lines.len() as i64 + 1; } @@ -590,11 +625,11 @@ fn import_trails( repo, db, dry_run, - "branch", + &TargetType::Branch, &branch_uuid, "review:trail-id", &json_string(&trail_id), - "string", + &ValueType::String, email, ts, )?; @@ -611,11 +646,11 @@ fn import_trails( repo, db, dry_run, - "branch", + &TargetType::Branch, &branch_uuid, &key, &json_val, - "string", + &ValueType::String, email, ts, )?; @@ -632,11 +667,11 @@ fn import_trails( repo, db, dry_run, - "branch", + &TargetType::Branch, &branch_uuid, &key, &json_val, - "string", + &ValueType::String, email, ts, )?; @@ -659,11 +694,11 @@ fn import_trails( repo, db, dry_run, - "branch", + &TargetType::Branch, &branch_uuid, "review:checkpoints", &encoded, - "list", + &ValueType::List, email, ts, )?; @@ -678,11 +713,11 @@ fn import_trails( repo, db, dry_run, - "branch", + &TargetType::Branch, &branch_uuid, "review:discussion", &json_encode_value(&disc)?, - "string", + &ValueType::String, email, ts, )?; @@ -701,20 +736,20 @@ fn set_value( repo: &Repository, db: Option<&Db>, dry_run: bool, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, value: &str, - value_type: &str, + value_type: &ValueType, email: &str, timestamp: i64, ) -> Result { - let use_git_ref = value_type == "string" && value.len() > GIT_REF_THRESHOLD; + let use_git_ref = *value_type == ValueType::String && value.len() > GIT_REF_THRESHOLD; if dry_run { eprintln!( " [dry-run] {}:{} {} = {}{}", - target_type, + target_type.as_str(), &target_value[..7.min(target_value.len())], key, truncate(value, 80), @@ -929,7 +964,10 @@ 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(db) = db { - if db.get("commit", &commit_sha, "agent.blame")?.is_some() { + if db + .get(&TargetType::Commit, &commit_sha, "agent.blame")? + .is_some() + { skipped_exists += 1; continue; } @@ -956,33 +994,33 @@ fn run_git_ai(dry_run: bool, since_epoch: Option) -> Result<()> { }; db.set_with_git_ref( None, - "commit", + &TargetType::Commit, &commit_sha, "agent.blame", &blame_val, - "string", + &ValueType::String, email, commit_ts, is_ref, )?; db.set( - "commit", + &TargetType::Commit, &commit_sha, "agent.git-ai.schema-version", &json_string(&parsed.schema_version), - "string", + &ValueType::String, email, commit_ts, )?; if let Some(ref ver) = parsed.git_ai_version { db.set( - "commit", + &TargetType::Commit, &commit_sha, "agent.git-ai.version", &json_string(ver), - "string", + &ValueType::String, email, commit_ts, )?; @@ -990,11 +1028,11 @@ fn run_git_ai(dry_run: bool, since_epoch: Option) -> Result<()> { if parsed.model != "unknown" { db.set( - "commit", + &TargetType::Commit, &commit_sha, "agent.model", &json_string(&parsed.model), - "string", + &ValueType::String, email, commit_ts, )?; diff --git a/src/commands/inspect.rs b/src/commands/inspect.rs index f1e78a4..2e4e29f 100644 --- a/src/commands/inspect.rs +++ b/src/commands/inspect.rs @@ -167,7 +167,15 @@ fn run_list(db: &Db, target_type: &str, term: Option<&str>) -> Result<()> { let all = db.get_all_metadata()?; // Filter to target type - let mut entries: Vec<&(String, String, String, String, String, i64, bool)> = all + let mut entries: Vec<&( + String, + String, + String, + String, + crate::types::ValueType, + i64, + bool, + )> = all .iter() .filter(|(tt, _, _, _, _, _, _)| tt == target_type) .collect(); @@ -183,7 +191,8 @@ fn run_list(db: &Db, target_type: &str, term: Option<&str>) -> Result<()> { entries.retain(|(_tt, tv, key, value, vtype, _, _)| { fuzzy_matches(&lower_term, tv) || fuzzy_matches(&lower_term, key) - || (vtype == "string" && fuzzy_matches(&lower_term, &decode_string_value(value))) + || (*vtype == crate::types::ValueType::String + && fuzzy_matches(&lower_term, &decode_string_value(value))) }); } @@ -196,8 +205,18 @@ fn run_list(db: &Db, target_type: &str, term: Option<&str>) -> Result<()> { let term_width = terminal_width(); // Group by target_value - let mut by_target: BTreeMap<&str, Vec<&(String, String, String, String, String, i64, bool)>> = - BTreeMap::new(); + let mut by_target: BTreeMap< + &str, + Vec<&( + String, + String, + String, + String, + crate::types::ValueType, + i64, + bool, + )>, + > = BTreeMap::new(); for entry in &entries { by_target.entry(&entry.1).or_default().push(entry); } @@ -302,10 +321,11 @@ fn run_timeline(db: &Db) -> Result<()> { /// Format a value for one-line display, fitting within available width. fn format_value_oneline( value: &str, - value_type: &str, + value_type: &crate::types::ValueType, term_width: usize, key_len: usize, ) -> String { + use crate::types::ValueType; // 2 spaces indent + key + 2 spaces gap = overhead let overhead = 2 + key_len + 2; let available = if term_width > overhead + 5 { @@ -315,7 +335,7 @@ fn format_value_oneline( }; match value_type { - "string" => { + ValueType::String => { let raw = decode_string_value(value); let first_line = raw.lines().next().unwrap_or(""); let has_more = raw.contains('\n') && raw.trim_end_matches('\n') != first_line; @@ -329,7 +349,7 @@ fn format_value_oneline( } s } - "list" => { + ValueType::List => { if let Ok(items) = list_values_from_json(value) { format!("[list: {} items]", items.len()) } else if let Ok(arr) = serde_json::from_str::>(value) { @@ -338,20 +358,13 @@ fn format_value_oneline( "[list]".to_string() } } - "set" => { + ValueType::Set => { if let Ok(members) = serde_json::from_str::>(value) { format!("[set: {} members]", members.len()) } else { "[set]".to_string() } } - _ => { - if value.len() > available { - format!("{}...", &value[..available.saturating_sub(3)]) - } else { - value.to_string() - } - } } } diff --git a/src/commands/list.rs b/src/commands/list.rs index 394bdf2..51d8c45 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -11,7 +11,7 @@ pub fn run_push(target_str: &str, key: &str, value: &str) -> Result<()> { ctx.resolve_target(&mut target)?; ctx.db.list_push( - target.type_str(), + &target.target_type, target.value_str(), key, value, @@ -31,7 +31,7 @@ pub fn run_rm(target_str: &str, key: &str, index: Option) -> Result<()> { let entries = ctx .db - .list_entries(target.type_str(), target.value_str(), key)?; + .list_entries(&target.target_type, target.value_str(), key)?; match index { None => { @@ -51,7 +51,7 @@ pub fn run_rm(target_str: &str, key: &str, index: Option) -> Result<()> { } Some(idx) => { ctx.db.list_rm( - target.type_str(), + &target.target_type, target.value_str(), key, idx, @@ -72,7 +72,7 @@ pub fn run_pop(target_str: &str, key: &str, value: &str) -> Result<()> { ctx.resolve_target(&mut target)?; ctx.db.list_pop( - target.type_str(), + &target.target_type, target.value_str(), key, value, diff --git a/src/commands/log.rs b/src/commands/log.rs index 6d71c05..840baa0 100644 --- a/src/commands/log.rs +++ b/src/commands/log.rs @@ -6,6 +6,7 @@ use git2::{Oid, Repository, Sort}; use crate::context::CommandContext; use crate::git_utils; +use crate::types::TargetType; const RESET: &str = "\x1b[0m"; const BOLD: &str = "\x1b[1m"; @@ -45,7 +46,10 @@ pub fn run( let sha = oid.to_string(); // Fetch metadata before deciding whether to print the commit - let entries = ctx.db.get_all("commit", &sha, None).unwrap_or_default(); + let entries = ctx + .db + .get_all(&TargetType::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 f099d92..cf17ab5 100644 --- a/src/commands/materialize.rs +++ b/src/commands/materialize.rs @@ -8,8 +8,8 @@ use crate::types::{ build_list_entry_tombstone_tree_path, build_list_tree_dir_path, build_set_member_tombstone_tree_path, build_set_tree_dir_path, build_tombstone_tree_path, build_tree_path, decode_key_path_segments, decode_path_target_segments, set_member_id, Target, - LIST_VALUE_DIR, PATH_TARGET_SEPARATOR, SET_VALUE_DIR, STRING_VALUE_BLOB, TOMBSTONE_BLOB, - TOMBSTONE_ROOT, + TargetType, ValueType, LIST_VALUE_DIR, PATH_TARGET_SEPARATOR, SET_VALUE_DIR, STRING_VALUE_BLOB, + TOMBSTONE_BLOB, TOMBSTONE_ROOT, }; use anyhow::Result; @@ -165,7 +165,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { remote_entries.set_tombstones.len() ); for ((tt, tv, k), val) in &remote_entries.values { - let target = format_target_for_display(tt, tv); + let target = format_target_for_display(&TargetType::from_str(tt)?, tv); let val_desc = match val { TreeValue::String(s) => { if s.len() > 50 { @@ -180,7 +180,7 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { eprintln!(" {} {} -> {}", target, k, val_desc); } for ((tt, tv, k), tomb) in &remote_entries.tombstones { - let target = format_target_for_display(tt, tv); + let target = format_target_for_display(&TargetType::from_str(tt)?, tv); eprintln!( " {} {} -> tombstone [ts={}, by={}]", target, k, tomb.timestamp, tomb.email @@ -326,15 +326,16 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { for key in local_entries.values.keys() { if !remote_entries.values.contains_key(key) { let (target_type, target_value, key_name) = key; + let tt = TargetType::from_str(target_type)?; if verbose { eprintln!( "[verbose] applying implicit delete for {} {}", - format_target_for_display(target_type, target_value), + format_target_for_display(&tt, target_value), key_name ); } ctx.db - .apply_tombstone(target_type, target_value, key_name, email, now)?; + .apply_tombstone(&tt, target_value, key_name, email, now)?; } } @@ -611,15 +612,16 @@ pub fn run(remote: Option<&str>, dry_run: bool, verbose: bool) -> Result<()> { for key in base_values.keys() { if !merged_values.contains_key(key) && !merged_tombstones.contains_key(key) { let (target_type, target_value, key_name) = key; + let tt = TargetType::from_str(target_type)?; if verbose { eprintln!( "[verbose] applying legacy delete for {} {}", - format_target_for_display(target_type, target_value), + format_target_for_display(&tt, target_value), key_name ); } ctx.db - .apply_tombstone(target_type, target_value, key_name, email, now)?; + .apply_tombstone(&tt, target_value, key_name, email, now)?; } } } @@ -692,21 +694,22 @@ fn update_db_from_tree( use crate::types::GIT_REF_THRESHOLD; for ((target_type, target_value, key_name), tree_val) in values { + let tt = TargetType::from_str(target_type)?; match tree_val { TreeValue::String(s) => { if s.len() > GIT_REF_THRESHOLD { // Large value: store as git blob reference let blob_oid = repo.blob(s.as_bytes())?; let oid_str = blob_oid.to_string(); - let existing = db.get(target_type, target_value, key_name)?; + let existing = db.get(&tt, target_value, key_name)?; if existing.as_ref().map(|(v, _, _)| v.as_str()) != Some(&oid_str) { db.set_with_git_ref( None, - target_type, + &tt, target_value, key_name, &oid_str, - "string", + &ValueType::String, email, now, true, @@ -714,14 +717,14 @@ fn update_db_from_tree( } } else { let json_val = serde_json::to_string(s)?; - let existing = db.get(target_type, target_value, key_name)?; + let existing = db.get(&tt, target_value, key_name)?; if existing.as_ref().map(|(v, _, _)| v.as_str()) != Some(&json_val) { db.set( - target_type, + &tt, target_value, key_name, &json_val, - "string", + &ValueType::String, email, now, )?; @@ -753,14 +756,14 @@ fn update_db_from_tree( }); } let json_val = encode_entries(&items)?; - let existing = db.get(target_type, target_value, key_name)?; + let existing = db.get(&tt, target_value, key_name)?; if existing.as_ref().map(|(v, _, _)| v.as_str()) != Some(&json_val) { db.set( - target_type, + &tt, target_value, key_name, &json_val, - "list", + &ValueType::List, email, now, )?; @@ -785,14 +788,14 @@ fn update_db_from_tree( .collect(); visible.sort(); let json_val = serde_json::to_string(&visible)?; - let existing = db.get(target_type, target_value, key_name)?; + let existing = db.get(&tt, target_value, key_name)?; if existing.as_ref().map(|(v, _, _)| v.as_str()) != Some(&json_val) { db.set( - target_type, + &tt, target_value, key_name, &json_val, - "set", + &ValueType::Set, email, now, )?; @@ -805,13 +808,8 @@ fn update_db_from_tree( if values.contains_key(key) { continue; } - db.apply_tombstone( - &key.0, - &key.1, - &key.2, - &tombstone.email, - tombstone.timestamp, - )?; + let tt = TargetType::from_str(&key.0)?; + db.apply_tombstone(&tt, &key.1, &key.2, &tombstone.email, tombstone.timestamp)?; } Ok(()) @@ -828,10 +826,11 @@ fn collect_db_changes_from_tree( let mut planned = Vec::new(); for ((target_type, target_value, key_name), tree_val) in values { + let tt = TargetType::from_str(target_type)?; match tree_val { TreeValue::String(s) => { let json_val = serde_json::to_string(s)?; - let existing = db.get(target_type, target_value, key_name)?; + let existing = db.get(&tt, target_value, key_name)?; if existing.as_ref().map(|(v, _, _)| v.as_str()) != Some(&json_val) { planned.push(PlannedDbChange::Set { target_type: target_type.clone(), @@ -867,7 +866,7 @@ fn collect_db_changes_from_tree( }); } let json_val = encode_entries(&items)?; - let existing = db.get(target_type, target_value, key_name)?; + let existing = db.get(&tt, target_value, key_name)?; if existing.as_ref().map(|(v, _, _)| v.as_str()) != Some(&json_val) { planned.push(PlannedDbChange::Set { target_type: target_type.clone(), @@ -902,7 +901,7 @@ fn collect_db_changes_from_tree( .collect(); visible.sort(); let json_val = serde_json::to_string(&visible)?; - let existing = db.get(target_type, target_value, key_name)?; + let existing = db.get(&tt, target_value, key_name)?; if existing.as_ref().map(|(v, _, _)| v.as_str()) != Some(&json_val) { planned.push(PlannedDbChange::Set { target_type: target_type.clone(), @@ -976,12 +975,14 @@ fn print_dry_run_report( value_type, value_preview, } => { + let target_display = if target_type == "project" { + "project".to_string() + } else { + format!("{}:{}", target_type, target_value) + }; println!( " set {} {} ({}) = {}", - format_target_for_display(target_type, target_value), - key, - value_type, - value_preview + target_display, key, value_type, value_preview ); } PlannedDbChange::Remove { @@ -989,27 +990,33 @@ fn print_dry_run_report( target_value, key, } => { - println!( - " rm {} {}", - format_target_for_display(target_type, target_value), - key - ); + let target_display = if target_type == "project" { + "project".to_string() + } else { + format!("{}:{}", target_type, target_value) + }; + println!(" rm {} {}", target_display, key); } } } } } -fn format_target_for_display(target_type: &str, target_value: &str) -> String { - if target_type == "project" { +fn format_target_for_display(target_type: &TargetType, target_value: &str) -> String { + if *target_type == TargetType::Project { "project".to_string() } else { - format!("{}:{}", target_type, target_value) + format!("{}:{}", target_type.as_str(), target_value) } } fn format_key_for_display(key: &Key) -> String { - format!("{} {}", format_target_for_display(&key.0, &key.1), key.2) + let target_display = if key.0 == "project" { + "project".to_string() + } else { + format!("{}:{}", key.0, key.1) + }; + format!("{} {}", target_display, key.2) } /// Three-way merge: base vs local vs remote. diff --git a/src/commands/promisor.rs b/src/commands/promisor.rs index abd6037..b50f41d 100644 --- a/src/commands/promisor.rs +++ b/src/commands/promisor.rs @@ -1,6 +1,7 @@ use anyhow::{bail, Result}; use crate::context::CommandContext; +use crate::types::{TargetType, ValueType}; pub fn run() -> Result<()> { let ctx = CommandContext::open_git2(None)?; @@ -67,9 +68,10 @@ pub fn run() -> Result<()> { skipped_deletes += 1; continue; } + let tt = TargetType::from_str(target_type)?; if ctx .db - .insert_promised(target_type, target_value, key, "string")? + .insert_promised(&tt, target_value, key, &ValueType::String)? { commit_inserted += 1; inserted += 1; @@ -98,9 +100,10 @@ pub fn run() -> Result<()> { let mut commit_skipped = 0; for (target_type, target_value, key) in &keys { + let tt = TargetType::from_str(target_type)?; if ctx .db - .insert_promised(target_type, target_value, key, "string")? + .insert_promised(&tt, target_value, key, &ValueType::String)? { commit_inserted += 1; inserted += 1; diff --git a/src/commands/prune/auto.rs b/src/commands/prune/auto.rs index 77a64bc..11b83e1 100644 --- a/src/commands/prune/auto.rs +++ b/src/commands/prune/auto.rs @@ -2,6 +2,7 @@ use anyhow::{bail, Context, Result}; use chrono::Utc; use crate::db::Db; +use crate::types::TargetType; /// Parsed auto-prune rules from project metadata. pub struct PruneRules { @@ -51,7 +52,7 @@ pub fn read_prune_rules(db: &Db) -> Result> { } fn read_config_string(db: &Db, key: &str) -> Result> { - match db.get("project", "", key)? { + match db.get(&TargetType::Project, "", key)? { Some((value, _, _)) => { let s: String = serde_json::from_str(&value)?; Ok(Some(s)) diff --git a/src/commands/prune/config.rs b/src/commands/prune/config.rs index 5295a99..6f658ef 100644 --- a/src/commands/prune/config.rs +++ b/src/commands/prune/config.rs @@ -3,6 +3,7 @@ use dialoguer::{Confirm, Input, Select}; use super::auto::{parse_size, read_prune_rules}; use crate::context::CommandContext; +use crate::types::{TargetType, ValueType}; pub fn run() -> Result<()> { let ctx = CommandContext::open_gix(None)?; @@ -175,7 +176,7 @@ pub fn run() -> Result<()> { Some(ref v) => set_config(&ctx, "meta:prune:max-keys", v)?, None => { ctx.db.rm( - "project", + &TargetType::Project, "", "meta:prune:max-keys", &ctx.email, @@ -187,7 +188,7 @@ pub fn run() -> Result<()> { Some(ref v) => set_config(&ctx, "meta:prune:max-size", v)?, None => { ctx.db.rm( - "project", + &TargetType::Project, "", "meta:prune:max-size", &ctx.email, @@ -199,7 +200,7 @@ pub fn run() -> Result<()> { Some(ref v) => set_config(&ctx, "meta:prune:min-size", v)?, None => { ctx.db.rm( - "project", + &TargetType::Project, "", "meta:prune:min-size", &ctx.email, @@ -215,11 +216,11 @@ pub fn run() -> Result<()> { fn set_config(ctx: &CommandContext, key: &str, value: &str) -> Result<()> { let stored = serde_json::to_string(value)?; ctx.db.set( - "project", + &TargetType::Project, "", key, &stored, - "string", + &ValueType::String, &ctx.email, ctx.timestamp, )?; diff --git a/src/commands/prune/local.rs b/src/commands/prune/local.rs index 6a56a02..002e2a3 100644 --- a/src/commands/prune/local.rs +++ b/src/commands/prune/local.rs @@ -3,6 +3,7 @@ use rusqlite::params; use super::auto::{parse_since_to_cutoff_ms, read_prune_rules}; use crate::context::CommandContext; +use crate::types::TargetType; pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { let ctx = CommandContext::open_gix(None)?; @@ -19,7 +20,7 @@ pub fn run(dry_run: bool, skip_date: bool) -> Result<()> { Some(ref r) => r.since.clone(), None => { // Check if at least meta:prune:since is set (triggers may be absent) - match ctx.db.get("project", "", "meta:prune:since")? { + match ctx.db.get(&TargetType::Project, "", "meta:prune:since")? { Some((value, _, _)) => { let s: String = serde_json::from_str(&value)?; s diff --git a/src/commands/prune/tree.rs b/src/commands/prune/tree.rs index 3a4670f..758aaf0 100644 --- a/src/commands/prune/tree.rs +++ b/src/commands/prune/tree.rs @@ -13,13 +13,14 @@ use crate::commands::serialize::{ }; use crate::context::CommandContext; use crate::git_utils; +use crate::types::TargetType; pub fn run(dry_run: bool) -> Result<()> { let ctx = CommandContext::open_git2(None)?; let repo = ctx.git2_repo()?; // Read prune rules — need at least meta:prune:since - let since = match ctx.db.get("project", "", "meta:prune:since")? { + let since = match ctx.db.get(&TargetType::Project, "", "meta:prune:since")? { Some((value, _, _)) => { let s: String = serde_json::from_str(&value)?; s diff --git a/src/commands/pull.rs b/src/commands/pull.rs index a5dafd9..3864ac3 100644 --- a/src/commands/pull.rs +++ b/src/commands/pull.rs @@ -4,7 +4,7 @@ use crate::commands::{materialize, serialize}; use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; -use crate::types; +use crate::types::{self, TargetType, ValueType}; pub fn run(remote: Option<&str>, verbose: bool) -> Result<()> { let ctx = CommandContext::open_git2(None)?; @@ -189,16 +189,20 @@ fn insert_promisor_entries( match parse_commit_changes(message) { Some(changes) => { - for (op, target_type, target_value, key) in &changes { + for (op, target_type_str, target_value, key) in &changes { if *op == 'D' { continue; } - if db.insert_promised(target_type, target_value, key, "string")? { + let target_type = TargetType::from_str(target_type_str)?; + if db.insert_promised(&target_type, target_value, key, &ValueType::String)? { count += 1; if verbose { eprintln!( "[verbose] promisor: {} {}:{} {}", - op, target_type, target_value, key + op, + target_type.as_str(), + target_value, + key ); } } @@ -208,13 +212,16 @@ fn insert_promisor_entries( // Root commit without a change list — walk its tree to discover keys let tree = commit.tree()?; let keys = extract_keys_from_tree(repo, &tree)?; - for (target_type, target_value, key) in &keys { - if db.insert_promised(target_type, target_value, key, "string")? { + for (target_type_str, target_value, key) in &keys { + let target_type = TargetType::from_str(target_type_str)?; + if db.insert_promised(&target_type, target_value, key, &ValueType::String)? { count += 1; if verbose { eprintln!( "[verbose] promisor (tree): {}:{} {}", - target_type, target_value, key + target_type.as_str(), + target_value, + key ); } } diff --git a/src/commands/rm.rs b/src/commands/rm.rs index 116fb37..f28fb62 100644 --- a/src/commands/rm.rs +++ b/src/commands/rm.rs @@ -11,7 +11,7 @@ pub fn run(target_str: &str, key: &str) -> Result<()> { ctx.resolve_target(&mut target)?; let removed = ctx.db.rm( - target.type_str(), + &target.target_type, target.value_str(), key, &ctx.email, diff --git a/src/commands/serialize.rs b/src/commands/serialize.rs index 1690a69..12b37d0 100644 --- a/src/commands/serialize.rs +++ b/src/commands/serialize.rs @@ -11,7 +11,7 @@ use crate::list_value::{make_entry_name, parse_entries}; use crate::types::{ build_list_entry_tombstone_tree_path, build_list_tree_dir_path, build_set_member_tombstone_tree_path, build_set_tree_dir_path, build_tombstone_tree_path, - build_tree_path, Target, + build_tree_path, Target, TargetType, ValueType, }; #[derive(serde::Serialize)] @@ -46,8 +46,8 @@ pub fn parse_filter_rules(db: &Db) -> Result> { let mut rules = Vec::new(); // meta:local:filter rules first (higher priority) - if let Some((value, value_type, _)) = db.get("project", "", "meta:local:filter")? { - if value_type == "set" { + if let Some((value, value_type, _)) = db.get(&TargetType::Project, "", "meta:local:filter")? { + if value_type == ValueType::Set { let members: Vec = serde_json::from_str(&value)?; for member in members { rules.push(parse_rule(&member)?); @@ -56,8 +56,8 @@ pub fn parse_filter_rules(db: &Db) -> Result> { } // Then meta:filter rules (shared/corporate) - if let Some((value, value_type, _)) = db.get("project", "", "meta:filter")? { - if value_type == "set" { + if let Some((value, value_type, _)) = db.get(&TargetType::Project, "", "meta:filter")? { + if value_type == ValueType::Set { let members: Vec = serde_json::from_str(&value)?; for member in members { rules.push(parse_rule(&member)?); @@ -372,7 +372,7 @@ pub fn run(verbose: bool) -> Result<()> { // prune it, and keeps the summary counts accurate. let prune_since = ctx .db - .get("project", "", "meta:prune:since")? + .get(&TargetType::Project, "", "meta:prune:since")? .and_then(|(value, _, _)| serde_json::from_str::(&value).ok()); let prune_rules = auto::read_prune_rules(&ctx.db)?; let prune_cutoff_ms = prune_since @@ -404,7 +404,7 @@ pub fn run(verbose: bool) -> Result<()> { } } - type MetaEntry = (String, String, String, String, String, i64, bool); + type MetaEntry = (String, String, String, String, ValueType, i64, bool); type TombEntry = (String, String, String, i64, String); type SetTombEntry = (String, String, String, String, String, i64, String); type ListTombEntry = (String, String, String, String, i64, String); @@ -501,11 +501,10 @@ pub fn run(verbose: bool) -> Result<()> { ) }; targets.insert(label); - match value_type.as_str() { - "string" => string_count += 1, - "list" => list_count += 1, - "set" => set_count += 1, - _ => {} + match value_type { + ValueType::String => string_count += 1, + ValueType::List => list_count += 1, + ValueType::Set => set_count += 1, } } } @@ -722,7 +721,7 @@ pub fn run(verbose: bool) -> Result<()> { /// Used by `gmeta prune` to rebuild a tree from only the surviving entries. pub fn build_filtered_tree( repo: &git2::Repository, - metadata_entries: &[(String, String, String, String, String, i64, bool)], + metadata_entries: &[(String, String, String, String, ValueType, i64, bool)], tombstone_entries: &[(String, String, String, i64, String)], set_tombstone_entries: &[(String, String, String, String, String, i64, String)], list_tombstone_entries: &[(String, String, String, String, i64, String)], @@ -745,7 +744,7 @@ pub fn build_filtered_tree( /// from the existing tree by OID. fn build_tree( repo: &git2::Repository, - metadata_entries: &[(String, String, String, String, String, i64, bool)], + metadata_entries: &[(String, String, String, String, ValueType, i64, bool)], tombstone_entries: &[(String, String, String, i64, String)], set_tombstone_entries: &[(String, String, String, String, String, i64, String)], list_tombstone_entries: &[(String, String, String, String, i64, String)], @@ -774,8 +773,8 @@ fn build_tree( } } - match value_type.as_str() { - "string" => { + match value_type { + ValueType::String => { let full_path = build_tree_path(&target, key)?; if *is_git_ref { let oid = git2::Oid::from_str(value)?; @@ -805,7 +804,7 @@ fn build_tree( files.insert(full_path, raw_value.into_bytes()); } } - "list" => { + ValueType::List => { let list_entries = parse_entries(value).context("failed to decode list value")?; let list_dir_path = build_list_tree_dir_path(&target, key)?; if verbose { @@ -821,7 +820,7 @@ fn build_tree( files.insert(full_path, entry.value.into_bytes()); } } - "set" => { + ValueType::Set => { let members: Vec = serde_json::from_str(value).context("failed to decode set value")?; let set_dir_path = build_set_tree_dir_path(&target, key)?; @@ -838,7 +837,6 @@ fn build_tree( files.insert(full_path, member.into_bytes()); } } - _ => {} } } diff --git a/src/commands/set.rs b/src/commands/set.rs index 09a5195..7393ef9 100644 --- a/src/commands/set.rs +++ b/src/commands/set.rs @@ -60,11 +60,11 @@ pub fn run( let blob_oid = git2_repo.blob(raw_value.as_bytes())?; ctx.db.set_with_git_ref( None, - target.type_str(), + &target.target_type, target.value_str(), key, &blob_oid.to_string(), - value_type.as_str(), + &value_type, &ctx.email, ctx.timestamp, true, @@ -83,11 +83,11 @@ pub fn run( }; ctx.db.set( - target.type_str(), + &target.target_type, target.value_str(), key, &stored_value, - value_type.as_str(), + &value_type, &ctx.email, ctx.timestamp, )?; @@ -111,7 +111,7 @@ pub fn run_add( ctx.resolve_target(&mut target)?; ctx.db.set_add( - target.type_str(), + &target.target_type, target.value_str(), key, value, @@ -136,7 +136,7 @@ pub fn run_rm( ctx.resolve_target(&mut target)?; ctx.db.set_rm( - target.type_str(), + &target.target_type, target.value_str(), key, value, diff --git a/src/commands/show.rs b/src/commands/show.rs index 5013a6a..b1c69c6 100644 --- a/src/commands/show.rs +++ b/src/commands/show.rs @@ -7,6 +7,7 @@ use chrono::{TimeZone, Utc}; use git2::Repository; use crate::context::CommandContext; +use crate::types::{TargetType, ValueType}; const RESET: &str = "\x1b[0m"; const BOLD: &str = "\x1b[1m"; @@ -103,7 +104,10 @@ pub fn run(commit_ref: &str) -> Result<()> { let mut meta_entries: Vec<(String, String, String)> = Vec::new(); // (source, key, display_value) // Metadata on commit: - let commit_entries = ctx.db.get_all("commit", &sha, None).unwrap_or_default(); + let commit_entries = ctx + .db + .get_all(&TargetType::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)); @@ -111,7 +115,10 @@ pub fn run(commit_ref: &str) -> Result<()> { // Metadata on change-id: if let Some(ref cid) = change_id { - let cid_entries = ctx.db.get_all("change-id", cid, None).unwrap_or_default(); + let cid_entries = ctx + .db + .get_all(&TargetType::ChangeId, 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)); @@ -130,24 +137,25 @@ pub fn run(commit_ref: &str) -> Result<()> { } /// Format a stored metadata value for display. -fn format_meta_value(value: &str, value_type: &str) -> String { +fn format_meta_value(value: &str, value_type: &ValueType) -> String { match value_type { - "string" => serde_json::from_str::(value).unwrap_or_else(|_| value.to_string()), - "list" => { + ValueType::String => { + serde_json::from_str::(value).unwrap_or_else(|_| value.to_string()) + } + ValueType::List => { if let Ok(arr) = serde_json::from_str::>(value) { format!("[list: {} items]", arr.len()) } else { value.to_string() } } - "set" => { + ValueType::Set => { if let Ok(arr) = serde_json::from_str::>(value) { format!("[set: {} members]", arr.len()) } else { value.to_string() } } - _ => value.to_string(), } } diff --git a/src/commands/stats.rs b/src/commands/stats.rs index d3adf35..f6603f0 100644 --- a/src/commands/stats.rs +++ b/src/commands/stats.rs @@ -48,7 +48,8 @@ pub fn run() -> Result<()> { let grouped = group_keys_by_integer_pattern(keys); let total: u64 = grouped.values().sum(); - let plural = pluralize_target_type(target_type); + let tt = crate::types::TargetType::from_str(target_type)?; + let plural = tt.pluralize(); println!("{}: {} keys", plural, total); // Sort keys by count descending, then alphabetically @@ -107,17 +108,6 @@ fn group_keys_by_integer_pattern(keys: &BTreeMap) -> BTreeMap &str { - match t { - "commit" => "commits", - "branch" => "branches", - "change-id" => "change-ids", - "path" => "paths", - "project" => "project", - _ => t, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/commands/watch.rs b/src/commands/watch.rs index 082c525..87033e2 100644 --- a/src/commands/watch.rs +++ b/src/commands/watch.rs @@ -12,6 +12,7 @@ use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; use crate::context::CommandContext; use crate::db::Db; use crate::git_utils; +use crate::types::{TargetType, ValueType}; // ANSI colors const BOLD: &str = "\x1b[1m"; @@ -485,7 +486,15 @@ impl WatchState { .or_insert(ts); let branch_id = format!("{}@{}", branch_name, first_seen); let value = serde_json::to_string(&branch_id)?; - db.set("change-id", cid, "branch:id", &value, "string", &email, ts)?; + db.set( + &TargetType::ChangeId, + cid, + "branch:id", + &value, + &ValueType::String, + &email, + ts, + )?; let short_cid = &cid[..16.min(cid.len())]; eprintln!( @@ -500,7 +509,7 @@ impl WatchState { for prompt in prompts { db.list_push_with_repo( Some(&repo), - "change-id", + &TargetType::ChangeId, cid, "agent:prompts", &prompt, @@ -602,7 +611,7 @@ impl WatchState { db.list_push_with_repo( Some(&repo), - "branch", + &TargetType::Branch, &branch_id, "agent:transcripts", &transcript_content, diff --git a/src/db.rs b/src/db.rs index 9980c5d..b588221 100644 --- a/src/db.rs +++ b/src/db.rs @@ -6,7 +6,7 @@ use git2::Repository; use rusqlite::{params, Connection}; use crate::list_value::{encode_entries, ensure_unique_timestamp, parse_entries, ListEntry}; -use crate::types::GIT_REF_THRESHOLD; +use crate::types::{TargetType, ValueType, GIT_REF_THRESHOLD}; /// The time to wait when the database is locked before giving up. const BUSY_TIMEOUT: Duration = Duration::from_secs(5); @@ -168,11 +168,11 @@ impl Db { /// Set a value (upsert). JSON-encodes the value for storage. pub fn set( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, value: &str, - value_type: &str, + value_type: &ValueType, email: &str, timestamp: i64, ) -> Result<()> { @@ -195,18 +195,21 @@ impl Db { pub fn set_with_git_ref( &self, repo: Option<&Repository>, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, value: &str, - value_type: &str, + value_type: &ValueType, email: &str, timestamp: i64, is_git_ref: bool, ) -> Result<()> { + let target_type_str = target_type.as_str(); + let value_type_str = value_type.as_str(); + // Validate that string values are proper JSON strings (not raw objects/arrays) // Skip validation for git refs (they store a SHA, not JSON) - if value_type == "string" && !is_git_ref { + if *value_type == ValueType::String && !is_git_ref { match serde_json::from_str::(value) { Ok(v) if !v.is_string() => { bail!( @@ -233,18 +236,18 @@ impl Db { let git_ref_val: i64 = if is_git_ref { 1 } else { 0 }; let tx = self.conn.unchecked_transaction()?; match value_type { - "string" => { + ValueType::String => { tx.execute( "INSERT INTO metadata (target_type, target_value, key, value, value_type, last_timestamp, is_git_ref) VALUES (?1, ?2, ?3, ?4, 'string', ?5, ?6) ON CONFLICT(target_type, target_value, key) DO UPDATE SET value = excluded.value, value_type = 'string', last_timestamp = excluded.last_timestamp, is_git_ref = excluded.is_git_ref, is_promised = 0", - params![target_type, target_value, key, value, timestamp, git_ref_val], + params![target_type_str, target_value, key, value, timestamp, git_ref_val], )?; let metadata_id: i64 = tx.query_row( "SELECT rowid FROM metadata WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], |row| row.get(0), )?; tx.execute( @@ -257,21 +260,21 @@ impl Db { )?; tx.execute( "DELETE FROM set_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], )?; } - "list" => { + ValueType::List => { tx.execute( "INSERT INTO metadata (target_type, target_value, key, value, value_type, last_timestamp, is_git_ref) VALUES (?1, ?2, ?3, '[]', 'list', ?4, 0) ON CONFLICT(target_type, target_value, key) DO UPDATE SET value = '[]', value_type = 'list', last_timestamp = excluded.last_timestamp, is_git_ref = 0, is_promised = 0", - params![target_type, target_value, key, timestamp], + params![target_type_str, target_value, key, timestamp], )?; let metadata_id: i64 = tx.query_row( "SELECT rowid FROM metadata WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], |row| row.get(0), )?; @@ -285,7 +288,7 @@ impl Db { )?; tx.execute( "DELETE FROM set_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], )?; for entry in parse_entries(value)? { @@ -302,18 +305,18 @@ impl Db { )?; } } - "set" => { + ValueType::Set => { tx.execute( "INSERT INTO metadata (target_type, target_value, key, value, value_type, last_timestamp, is_git_ref) VALUES (?1, ?2, ?3, '[]', 'set', ?4, 0) ON CONFLICT(target_type, target_value, key) DO UPDATE SET value = '[]', value_type = 'set', last_timestamp = excluded.last_timestamp, is_git_ref = 0, is_promised = 0", - params![target_type, target_value, key, timestamp], + params![target_type_str, target_value, key, timestamp], )?; let metadata_id: i64 = tx.query_row( "SELECT rowid FROM metadata WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], |row| row.get(0), )?; @@ -343,7 +346,7 @@ impl Db { )?; tx.execute( "DELETE FROM set_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3 AND member_id = ?4", - params![target_type, target_value, key, crate::types::set_member_id(member)], + params![target_type_str, target_value, key, crate::types::set_member_id(member)], )?; } @@ -362,24 +365,23 @@ impl Db { VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(target_type, target_value, key, member_id) DO UPDATE SET value = excluded.value, timestamp = excluded.timestamp, email = excluded.email", - params![target_type, target_value, key, member_id, member_value, timestamp, email], + params![target_type_str, target_value, key, member_id, member_value, timestamp, email], )?; } } } - _ => bail!("unknown value type: {}", value_type), } tx.execute( "INSERT INTO metadata_log (target_type, target_value, key, value, value_type, operation, email, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, 'set', ?6, ?7)", - params![target_type, target_value, key, value, value_type, email, timestamp], + params![target_type_str, target_value, key, value, value_type_str, email, timestamp], )?; tx.execute( "DELETE FROM metadata_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], )?; tx.commit()?; @@ -391,17 +393,17 @@ impl Db { /// Returns (value, value_type, is_git_ref). pub fn get( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, - ) -> Result> { + ) -> Result> { let mut stmt = self.conn.prepare( "SELECT rowid, value, value_type, is_git_ref FROM metadata WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", )?; let result = stmt - .query_row(params![target_type, target_value, key], |row| { + .query_row(params![target_type.as_str(), target_value, key], |row| { Ok(( row.get::<_, i64>(0)?, row.get::<_, String>(1)?, @@ -412,27 +414,31 @@ impl Db { .optional()?; match result { - Some((metadata_id, _value, value_type, _is_git_ref)) if value_type == "list" => { + Some((metadata_id, _value, ref vt, _is_git_ref)) + if ValueType::from_str(vt)? == ValueType::List => + { Ok(Some(( encode_list_entries_by_metadata_id( &self.conn, self.repo.as_ref(), metadata_id, )?, - value_type, + ValueType::List, false, ))) } - Some((metadata_id, _value, value_type, _is_git_ref)) if value_type == "set" => { + Some((metadata_id, _value, ref vt, _is_git_ref)) + if ValueType::from_str(vt)? == ValueType::Set => + { Ok(Some(( encode_set_values_by_metadata_id(&self.conn, metadata_id)?, - value_type, + ValueType::Set, false, ))) } - Some((_, value, value_type, is_git_ref)) => { + Some((_, value, vt, is_git_ref)) => { let resolved = resolve_blob(self.repo.as_ref(), &value, is_git_ref)?; - Ok(Some((resolved, value_type, is_git_ref))) + Ok(Some((resolved, ValueType::from_str(&vt)?, is_git_ref))) } None => Ok(None), } @@ -444,10 +450,10 @@ impl Db { /// if you need to see them. pub fn get_all( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key_prefix: Option<&str>, - ) -> Result> { + ) -> Result> { Ok(self .get_all_with_target_prefix(target_type, target_value, false, key_prefix)? .into_iter() @@ -460,11 +466,12 @@ impl Db { /// Returns (target_value, key, value, value_type, is_git_ref, is_promised). pub fn get_all_with_target_prefix( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, include_target_subtree: bool, key_prefix: Option<&str>, - ) -> Result> { + ) -> Result> { + let target_type_str = target_type.as_str(); let escaped_target = escape_like_pattern(target_value); let target_like = format!("{}/%", escaped_target); @@ -476,7 +483,7 @@ impl Db { AND (key = ?3 OR key LIKE ?4 ESCAPE '\\') ORDER BY target_value, key", vec![ - Box::new(target_type.to_string()), + Box::new(target_type_str.to_string()), Box::new(target_value.to_string()), Box::new(prefix.to_string()), Box::new(format!("{}:%", escape_like_pattern(prefix))), @@ -487,7 +494,7 @@ impl Db { WHERE target_type = ?1 AND target_value = ?2 ORDER BY target_value, key", vec![ - Box::new(target_type.to_string()), + Box::new(target_type_str.to_string()), Box::new(target_value.to_string()), ], ), @@ -497,7 +504,7 @@ impl Db { AND (key = ?4 OR key LIKE ?5 ESCAPE '\\') ORDER BY target_value, key", vec![ - Box::new(target_type.to_string()), + Box::new(target_type_str.to_string()), Box::new(target_value.to_string()), Box::new(target_like), Box::new(prefix.to_string()), @@ -509,7 +516,7 @@ impl Db { WHERE target_type = ?1 AND (target_value = ?2 OR target_value LIKE ?3 ESCAPE '\\') ORDER BY target_value, key", vec![ - Box::new(target_type.to_string()), + Box::new(target_type_str.to_string()), Box::new(target_value.to_string()), Box::new(target_like), ], @@ -533,22 +540,30 @@ impl Db { let mut results = Vec::new(); for row in rows { - let (metadata_id, target_value, key, value, value_type, is_git_ref, is_promised) = row?; + let (metadata_id, target_value, key, value, value_type_str, is_git_ref, is_promised) = + row?; + let vt = ValueType::from_str(&value_type_str)?; if is_promised { - results.push((target_value, key, value, value_type, false, true)); - } else if value_type == "list" { - let encoded = encode_list_entries_by_metadata_id( - &self.conn, - self.repo.as_ref(), - metadata_id, - )?; - results.push((target_value, key, encoded, value_type, false, false)); - } else if value_type == "set" { - let encoded = encode_set_values_by_metadata_id(&self.conn, metadata_id)?; - results.push((target_value, key, encoded, value_type, false, false)); + results.push((target_value, key, value, vt, false, true)); } else { - let resolved = resolve_blob(self.repo.as_ref(), &value, is_git_ref)?; - results.push((target_value, key, resolved, value_type, is_git_ref, false)); + match vt { + ValueType::List => { + let encoded = encode_list_entries_by_metadata_id( + &self.conn, + self.repo.as_ref(), + metadata_id, + )?; + results.push((target_value, key, encoded, vt, false, false)); + } + ValueType::Set => { + let encoded = encode_set_values_by_metadata_id(&self.conn, metadata_id)?; + results.push((target_value, key, encoded, vt, false, false)); + } + ValueType::String => { + let resolved = resolve_blob(self.repo.as_ref(), &value, is_git_ref)?; + results.push((target_value, key, resolved, vt, is_git_ref, false)); + } + } } } Ok(results) @@ -557,7 +572,7 @@ impl Db { /// Get authorship info for a key from the log (most recent entry). pub fn get_authorship( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, ) -> Result> { @@ -568,7 +583,7 @@ impl Db { )?; let result = stmt - .query_row(params![target_type, target_value, key], |row| { + .query_row(params![target_type.as_str(), target_value, key], |row| { Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)) }) .optional()?; @@ -582,15 +597,15 @@ impl Db { /// Returns Ok(true) if a row was inserted, Ok(false) if it already existed. pub fn insert_promised( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, - value_type: &str, + value_type: &ValueType, ) -> Result { let rows = self.conn.execute( "INSERT OR IGNORE INTO metadata (target_type, target_value, key, value, value_type, last_timestamp, is_git_ref, is_promised) VALUES (?1, ?2, ?3, '', ?4, 0, 0, 1)", - params![target_type, target_value, key, value_type], + params![target_type.as_str(), target_value, key, value_type.as_str()], )?; Ok(rows > 0) } @@ -598,11 +613,11 @@ impl Db { /// Resolve a promised entry by filling in the real value and clearing the flag. pub fn resolve_promised( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, value: &str, - value_type: &str, + value_type: &ValueType, is_git_ref: bool, ) -> Result<()> { let git_ref_val: i64 = if is_git_ref { 1 } else { 0 }; @@ -610,11 +625,11 @@ impl Db { "UPDATE metadata SET value = ?4, value_type = ?5, is_git_ref = ?6, is_promised = 0 WHERE target_type = ?1 AND target_value = ?2 AND key = ?3 AND is_promised = 1", params![ - target_type, + target_type.as_str(), target_value, key, value, - value_type, + value_type.as_str(), git_ref_val ], )?; @@ -622,10 +637,15 @@ impl Db { } /// Delete a promised entry (e.g. if the key no longer exists in the tip tree). - pub fn delete_promised(&self, target_type: &str, target_value: &str, key: &str) -> Result<()> { + pub fn delete_promised( + &self, + target_type: &TargetType, + target_value: &str, + key: &str, + ) -> Result<()> { self.conn.execute( "DELETE FROM metadata WHERE target_type = ?1 AND target_value = ?2 AND key = ?3 AND is_promised = 1", - params![target_type, target_value, key], + params![target_type.as_str(), target_value, key], )?; Ok(()) } @@ -633,19 +653,20 @@ impl Db { /// Remove a key. pub fn rm( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, email: &str, timestamp: i64, ) -> Result { + let target_type_str = target_type.as_str(); let tx = self.conn.unchecked_transaction()?; let metadata_id = tx .query_row( "SELECT rowid FROM metadata WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], |row| row.get::<_, i64>(0), ) .optional()?; @@ -673,20 +694,20 @@ impl Db { VALUES (?1, ?2, ?3, ?4, ?5) ON CONFLICT(target_type, target_value, key) DO UPDATE SET timestamp = excluded.timestamp, email = excluded.email", - params![target_type, target_value, key, timestamp, email], + params![target_type_str, target_value, key, timestamp, email], )?; // Clear per-entry list tombstones — the whole-key tombstone supersedes them tx.execute( "DELETE FROM list_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], )?; tx.execute( "INSERT INTO metadata_log (target_type, target_value, key, value, value_type, operation, email, timestamp) VALUES (?1, ?2, ?3, '', '', 'rm', ?4, ?5)", - params![target_type, target_value, key, email, timestamp], + params![target_type_str, target_value, key, email, timestamp], )?; } @@ -698,7 +719,7 @@ impl Db { /// Push a value onto a list. If the key is currently a string, convert to list first. pub fn list_push( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, value: &str, @@ -720,13 +741,14 @@ impl Db { pub fn list_push_with_repo( &self, repo: Option<&Repository>, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, value: &str, email: &str, timestamp: i64, ) -> Result<()> { + let target_type_str = target_type.as_str(); let tx = self.conn.unchecked_transaction()?; let existing = { let mut stmt = tx.prepare( @@ -734,7 +756,7 @@ impl Db { WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", )?; - stmt.query_row(params![target_type, target_value, key], |row| { + stmt.query_row(params![target_type_str, target_value, key], |row| { Ok(( row.get::<_, i64>(0)?, row.get::<_, String>(1)?, @@ -775,7 +797,7 @@ impl Db { tx.execute( "INSERT INTO metadata (target_type, target_value, key, value, value_type, last_timestamp) VALUES (?1, ?2, ?3, '[]', 'list', ?4)", - params![target_type, target_value, key, timestamp], + params![target_type_str, target_value, key, timestamp], )?; let metadata_id = tx.last_insert_rowid(); (metadata_id, Vec::new()) @@ -812,13 +834,13 @@ impl Db { tx.execute( "INSERT INTO metadata_log (target_type, target_value, key, value, value_type, operation, email, timestamp) VALUES (?1, ?2, ?3, ?4, 'list', 'push', ?5, ?6)", - params![target_type, target_value, key, &new_value, email, timestamp], + params![target_type_str, target_value, key, &new_value, email, timestamp], )?; tx.execute( "DELETE FROM metadata_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], )?; tx.commit()?; @@ -829,13 +851,14 @@ impl Db { /// Pop a value from a list. pub fn list_pop( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, value: &str, email: &str, timestamp: i64, ) -> Result<()> { + let target_type_str = target_type.as_str(); let tx = self.conn.unchecked_transaction()?; let existing = { let mut stmt = tx.prepare( @@ -843,7 +866,7 @@ impl Db { WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", )?; - stmt.query_row(params![target_type, target_value, key], |row| { + stmt.query_row(params![target_type_str, target_value, key], |row| { Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)) }) .optional()? @@ -884,13 +907,13 @@ impl Db { tx.execute( "INSERT INTO metadata_log (target_type, target_value, key, value, value_type, operation, email, timestamp) VALUES (?1, ?2, ?3, ?4, 'list', 'pop', ?5, ?6)", - params![target_type, target_value, key, &new_value, email, timestamp], + params![target_type_str, target_value, key, &new_value, email, timestamp], )?; tx.execute( "DELETE FROM metadata_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], )?; tx.commit()?; @@ -904,7 +927,7 @@ impl Db { /// Get list entries for display (resolved values with timestamps). pub fn list_entries( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, ) -> Result> { @@ -913,7 +936,7 @@ impl Db { .query_row( "SELECT rowid, value_type FROM metadata WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type.as_str(), target_value, key], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)), ) .optional()?; @@ -932,13 +955,14 @@ impl Db { /// Remove a list entry by index, creating a list tombstone for serialization. pub fn list_rm( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, index: usize, email: &str, timestamp: i64, ) -> Result<()> { + let target_type_str = target_type.as_str(); let tx = self.conn.unchecked_transaction()?; let existing = { let mut stmt = tx.prepare( @@ -946,7 +970,7 @@ impl Db { WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", )?; - stmt.query_row(params![target_type, target_value, key], |row| { + stmt.query_row(params![target_type_str, target_value, key], |row| { Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)) }) .optional()? @@ -985,7 +1009,7 @@ impl Db { VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(target_type, target_value, key, entry_name) DO UPDATE SET timestamp = excluded.timestamp, email = excluded.email", - params![target_type, target_value, key, entry_name, timestamp, email], + params![target_type_str, target_value, key, entry_name, timestamp, email], )?; let list_entries: Vec = list_rows @@ -1007,13 +1031,13 @@ impl Db { tx.execute( "INSERT INTO metadata_log (target_type, target_value, key, value, value_type, operation, email, timestamp) VALUES (?1, ?2, ?3, ?4, 'list', 'list:rm', ?5, ?6)", - params![target_type, target_value, key, &new_value, email, timestamp], + params![target_type_str, target_value, key, &new_value, email, timestamp], )?; tx.execute( "DELETE FROM metadata_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], )?; tx.commit()?; @@ -1054,13 +1078,14 @@ impl Db { /// Remove a member from a set. pub fn set_rm( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, value: &str, email: &str, timestamp: i64, ) -> Result<()> { + let target_type_str = target_type.as_str(); let tx = self.conn.unchecked_transaction()?; let existing = { let mut stmt = tx.prepare( @@ -1068,7 +1093,7 @@ impl Db { WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", )?; - stmt.query_row(params![target_type, target_value, key], |row| { + stmt.query_row(params![target_type_str, target_value, key], |row| { Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)) }) .optional()? @@ -1102,7 +1127,7 @@ impl Db { VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(target_type, target_value, key, member_id) DO UPDATE SET value = excluded.value, timestamp = excluded.timestamp, email = excluded.email", - params![target_type, target_value, key, member_id, value, timestamp, email], + params![target_type_str, target_value, key, member_id, value, timestamp, email], )?; let new_value = encode_set_values_by_metadata_id(&tx, metadata_id)?; @@ -1110,13 +1135,13 @@ impl Db { tx.execute( "INSERT INTO metadata_log (target_type, target_value, key, value, value_type, operation, email, timestamp) VALUES (?1, ?2, ?3, ?4, 'set', 'set:rm', ?5, ?6)", - params![target_type, target_value, key, &new_value, email, timestamp], + params![target_type_str, target_value, key, &new_value, email, timestamp], )?; tx.execute( "DELETE FROM metadata_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], )?; tx.commit()?; @@ -1129,13 +1154,14 @@ impl Db { /// Add a member to a set. pub fn set_add( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, value: &str, email: &str, timestamp: i64, ) -> Result<()> { + let target_type_str = target_type.as_str(); let tx = self.conn.unchecked_transaction()?; let existing = { let mut stmt = tx.prepare( @@ -1143,7 +1169,7 @@ impl Db { WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", )?; - stmt.query_row(params![target_type, target_value, key], |row| { + stmt.query_row(params![target_type_str, target_value, key], |row| { Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)) }) .optional()? @@ -1167,7 +1193,7 @@ impl Db { tx.execute( "INSERT INTO metadata (target_type, target_value, key, value, value_type, last_timestamp) VALUES (?1, ?2, ?3, '[]', 'set', ?4)", - params![target_type, target_value, key, timestamp], + params![target_type_str, target_value, key, timestamp], )?; tx.last_insert_rowid() } @@ -1184,7 +1210,7 @@ impl Db { tx.execute( "DELETE FROM set_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3 AND member_id = ?4", - params![target_type, target_value, key, member_id], + params![target_type_str, target_value, key, member_id], )?; let new_value = encode_set_values_by_metadata_id(&tx, metadata_id)?; @@ -1192,13 +1218,13 @@ impl Db { tx.execute( "INSERT INTO metadata_log (target_type, target_value, key, value, value_type, operation, email, timestamp) VALUES (?1, ?2, ?3, ?4, 'set', 'set:add', ?5, ?6)", - params![target_type, target_value, key, &new_value, email, timestamp], + params![target_type_str, target_value, key, &new_value, email, timestamp], )?; tx.execute( "DELETE FROM metadata_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], )?; tx.commit()?; @@ -1209,19 +1235,20 @@ impl Db { /// remove current value (if any), record tombstone, and log the operation. pub fn apply_tombstone( &self, - target_type: &str, + target_type: &TargetType, target_value: &str, key: &str, email: &str, timestamp: i64, ) -> Result<()> { + let target_type_str = target_type.as_str(); let tx = self.conn.unchecked_transaction()?; let metadata_id = tx .query_row( "SELECT rowid FROM metadata WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], |row| row.get::<_, i64>(0), ) .optional()?; @@ -1241,7 +1268,7 @@ impl Db { } tx.execute( "DELETE FROM set_tombstones WHERE target_type = ?1 AND target_value = ?2 AND key = ?3", - params![target_type, target_value, key], + params![target_type_str, target_value, key], )?; tx.execute( @@ -1249,13 +1276,13 @@ impl Db { VALUES (?1, ?2, ?3, ?4, ?5) ON CONFLICT(target_type, target_value, key) DO UPDATE SET timestamp = excluded.timestamp, email = excluded.email", - params![target_type, target_value, key, timestamp, email], + params![target_type_str, target_value, key, timestamp, email], )?; tx.execute( "INSERT INTO metadata_log (target_type, target_value, key, value, value_type, operation, email, timestamp) VALUES (?1, ?2, ?3, '', '', 'rm', ?4, ?5)", - params![target_type, target_value, key, email, timestamp], + params![target_type_str, target_value, key, email, timestamp], )?; tx.commit()?; @@ -1266,7 +1293,7 @@ impl Db { /// Returns (target_type, target_value, key, value, value_type, last_timestamp, is_git_ref). pub fn get_all_metadata( &self, - ) -> Result> { + ) -> Result> { let mut stmt = self.conn.prepare( "SELECT rowid, target_type, target_value, key, value, value_type, last_timestamp, is_git_ref FROM metadata @@ -1295,46 +1322,51 @@ impl Db { target_value, key, value, - value_type, + value_type_str, last_timestamp, is_git_ref, ) = row?; - if value_type == "list" { - let encoded = encode_list_entries_by_metadata_id( - &self.conn, - self.repo.as_ref(), - metadata_id, - )?; - results.push(( - target_type, - target_value, - key, - encoded, - value_type, - last_timestamp, - false, - )); - } else if value_type == "set" { - let encoded = encode_set_values_by_metadata_id(&self.conn, metadata_id)?; - results.push(( - target_type, - target_value, - key, - encoded, - value_type, - last_timestamp, - false, - )); - } else { - results.push(( - target_type, - target_value, - key, - value, - value_type, - last_timestamp, - is_git_ref, - )); + let vt = ValueType::from_str(&value_type_str)?; + match vt { + ValueType::List => { + let encoded = encode_list_entries_by_metadata_id( + &self.conn, + self.repo.as_ref(), + metadata_id, + )?; + results.push(( + target_type, + target_value, + key, + encoded, + vt, + last_timestamp, + false, + )); + } + ValueType::Set => { + let encoded = encode_set_values_by_metadata_id(&self.conn, metadata_id)?; + results.push(( + target_type, + target_value, + key, + encoded, + vt, + last_timestamp, + false, + )); + } + ValueType::String => { + results.push(( + target_type, + target_value, + key, + value, + vt, + last_timestamp, + is_git_ref, + )); + } } } Ok(results) @@ -1598,7 +1630,7 @@ impl Db { /// Returns at most `limit` matches. pub fn find_target_values_by_prefix( &self, - target_type: &str, + target_type: &TargetType, prefix: &str, limit: usize, ) -> Result> { @@ -1610,9 +1642,10 @@ impl Db { ORDER BY target_value LIMIT ?3", )?; - let rows = stmt.query_map(params![target_type, pattern, limit as i64], |row| { - row.get::<_, String>(0) - })?; + let rows = stmt.query_map( + params![target_type.as_str(), pattern, limit as i64], + |row| row.get::<_, String>(0), + )?; let mut results = Vec::new(); for row in rows { results.push(row?); @@ -1813,19 +1846,21 @@ mod tests { fn test_set_and_get() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "agent:model", "\"claude-4.6\"", - "string", + &ValueType::String, "test@test.com", 1000, ) .unwrap(); - let result = db.get("commit", "abc123", "agent:model").unwrap(); + let result = db + .get(&TargetType::Commit, "abc123", "agent:model") + .unwrap(); assert_eq!( result, - Some(("\"claude-4.6\"".to_string(), "string".to_string(), false)) + Some(("\"claude-4.6\"".to_string(), ValueType::String, false)) ); } @@ -1833,17 +1868,29 @@ mod tests { fn test_set_upsert() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", "abc123", "key", "\"v1\"", "string", "a@b.com", 1000, + &TargetType::Commit, + "abc123", + "key", + "\"v1\"", + &ValueType::String, + "a@b.com", + 1000, ) .unwrap(); db.set( - "commit", "abc123", "key", "\"v2\"", "string", "a@b.com", 2000, + &TargetType::Commit, + "abc123", + "key", + "\"v2\"", + &ValueType::String, + "a@b.com", + 2000, ) .unwrap(); - let result = db.get("commit", "abc123", "key").unwrap(); + let result = db.get(&TargetType::Commit, "abc123", "key").unwrap(); assert_eq!( result, - Some(("\"v2\"".to_string(), "string".to_string(), false)) + Some(("\"v2\"".to_string(), ValueType::String, false)) ); } @@ -1851,31 +1898,39 @@ mod tests { fn test_get_all_with_prefix() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "agent:model", "\"claude\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "agent:provider", "\"anthropic\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); db.set( - "commit", "abc123", "other", "\"val\"", "string", "a@b.com", 1000, + &TargetType::Commit, + "abc123", + "other", + "\"val\"", + &ValueType::String, + "a@b.com", + 1000, ) .unwrap(); - let results = db.get_all("commit", "abc123", Some("agent")).unwrap(); + let results = db + .get_all(&TargetType::Commit, "abc123", Some("agent")) + .unwrap(); assert_eq!(results.len(), 2); } @@ -1883,51 +1938,55 @@ mod tests { fn test_get_all_with_prefix_escapes_like_wildcards() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "a%:literal", "\"match\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "abc:anything", "\"should-not-match\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "a_:literal", "\"underscore-match\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "ab:anything", "\"underscore-should-not-match\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); - let percent_results = db.get_all("commit", "abc123", Some("a%")).unwrap(); + let percent_results = db + .get_all(&TargetType::Commit, "abc123", Some("a%")) + .unwrap(); let percent_keys: Vec = percent_results.into_iter().map(|r| r.0).collect(); assert_eq!(percent_keys, vec!["a%:literal".to_string()]); - let underscore_results = db.get_all("commit", "abc123", Some("a_")).unwrap(); + let underscore_results = db + .get_all(&TargetType::Commit, "abc123", Some("a_")) + .unwrap(); let underscore_keys: Vec = underscore_results.into_iter().map(|r| r.0).collect(); assert_eq!(underscore_keys, vec!["a_:literal".to_string()]); } @@ -1936,27 +1995,29 @@ mod tests { fn test_get_all_with_prefix_escapes_backslash() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", r"agent\name:model", "\"match\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "agentxname:model", "\"should-not-match\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); - let results = db.get_all("commit", "abc123", Some(r"agent\name")).unwrap(); + let results = db + .get_all(&TargetType::Commit, "abc123", Some(r"agent\name")) + .unwrap(); let keys: Vec = results.into_iter().map(|r| r.0).collect(); assert_eq!(keys, vec![r"agent\name:model".to_string()]); } @@ -1965,48 +2026,48 @@ mod tests { fn test_get_all_with_target_prefix_for_paths() { let db = Db::open_in_memory().unwrap(); db.set( - "path", + &TargetType::Path, "src/git", "owner", "\"schacon\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); db.set( - "path", + &TargetType::Path, "src/metrics", "owner", "\"kiril\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); db.set( - "path", + &TargetType::Path, "src/observability", "owner", "\"caleb\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); db.set( - "path", + &TargetType::Path, "srcx/metrics", "owner", "\"should-not-match\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); let results = db - .get_all_with_target_prefix("path", "src", true, Some("owner")) + .get_all_with_target_prefix(&TargetType::Path, "src", true, Some("owner")) .unwrap(); let rows: Vec<(String, String)> = results.into_iter().map(|r| (r.0, r.1)).collect(); assert_eq!( @@ -2023,21 +2084,37 @@ mod tests { fn test_rm() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", "abc123", "key", "\"val\"", "string", "a@b.com", 1000, + &TargetType::Commit, + "abc123", + "key", + "\"val\"", + &ValueType::String, + "a@b.com", + 1000, ) .unwrap(); - assert!(db.rm("commit", "abc123", "key", "a@b.com", 2000).unwrap()); - assert_eq!(db.get("commit", "abc123", "key").unwrap(), None); + assert!(db + .rm(&TargetType::Commit, "abc123", "key", "a@b.com", 2000) + .unwrap()); + assert_eq!(db.get(&TargetType::Commit, "abc123", "key").unwrap(), None); } #[test] fn test_rm_creates_tombstone() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", "abc123", "key", "\"val\"", "string", "a@b.com", 1000, + &TargetType::Commit, + "abc123", + "key", + "\"val\"", + &ValueType::String, + "a@b.com", + 1000, ) .unwrap(); - assert!(db.rm("commit", "abc123", "key", "a@b.com", 2000).unwrap()); + assert!(db + .rm(&TargetType::Commit, "abc123", "key", "a@b.com", 2000) + .unwrap()); let tombstones = db.get_all_tombstones().unwrap(); assert_eq!(tombstones.len(), 1); @@ -2057,34 +2134,65 @@ mod tests { fn test_set_clears_tombstone() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", "abc123", "key", "\"v1\"", "string", "a@b.com", 1000, + &TargetType::Commit, + "abc123", + "key", + "\"v1\"", + &ValueType::String, + "a@b.com", + 1000, ) .unwrap(); - assert!(db.rm("commit", "abc123", "key", "a@b.com", 2000).unwrap()); + assert!(db + .rm(&TargetType::Commit, "abc123", "key", "a@b.com", 2000) + .unwrap()); assert_eq!(db.get_all_tombstones().unwrap().len(), 1); db.set( - "commit", "abc123", "key", "\"v2\"", "string", "a@b.com", 3000, + &TargetType::Commit, + "abc123", + "key", + "\"v2\"", + &ValueType::String, + "a@b.com", + 3000, ) .unwrap(); assert_eq!(db.get_all_tombstones().unwrap().len(), 0); - let result = db.get("commit", "abc123", "key").unwrap(); + let result = db.get(&TargetType::Commit, "abc123", "key").unwrap(); assert_eq!( result, - Some(("\"v2\"".to_string(), "string".to_string(), false)) + Some(("\"v2\"".to_string(), ValueType::String, false)) ); } #[test] fn test_list_push() { let db = Db::open_in_memory().unwrap(); - db.list_push("commit", "abc123", "tags", "first", "a@b.com", 1000) - .unwrap(); - db.list_push("commit", "abc123", "tags", "second", "a@b.com", 2000) + db.list_push( + &TargetType::Commit, + "abc123", + "tags", + "first", + "a@b.com", + 1000, + ) + .unwrap(); + db.list_push( + &TargetType::Commit, + "abc123", + "tags", + "second", + "a@b.com", + 2000, + ) + .unwrap(); + let (val, vtype, _) = db + .get(&TargetType::Commit, "abc123", "tags") + .unwrap() .unwrap(); - let (val, vtype, _) = db.get("commit", "abc123", "tags").unwrap().unwrap(); - assert_eq!(vtype, "list"); + assert_eq!(vtype, ValueType::List); let list = crate::list_value::list_values_from_json(&val).unwrap(); assert_eq!(list, vec!["first", "second"]); } @@ -2093,11 +2201,11 @@ mod tests { fn test_set_list_stores_rows_in_list_values_table() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "tags", r#"[{"value":"a","timestamp":1000},{"value":"b","timestamp":1001}]"#, - "list", + &ValueType::List, "a@b.com", 2000, ) @@ -2121,8 +2229,11 @@ mod tests { .unwrap(); assert_eq!(list_rows, 2); - let (val, vtype, _) = db.get("commit", "abc123", "tags").unwrap().unwrap(); - assert_eq!(vtype, "list"); + let (val, vtype, _) = db + .get(&TargetType::Commit, "abc123", "tags") + .unwrap() + .unwrap(); + assert_eq!(vtype, ValueType::List); let list = crate::list_value::list_values_from_json(&val).unwrap(); assert_eq!(list, vec!["a", "b"]); } @@ -2131,21 +2242,21 @@ mod tests { fn test_set_list_replaces_existing_list_rows() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "tags", r#"[{"value":"a","timestamp":1000},{"value":"b","timestamp":1001}]"#, - "list", + &ValueType::List, "a@b.com", 2000, ) .unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "tags", r#"[{"value":"c","timestamp":3000}]"#, - "list", + &ValueType::List, "a@b.com", 4000, ) @@ -2169,7 +2280,10 @@ mod tests { .unwrap(); assert_eq!(list_rows, 1); - let (val, _, _) = db.get("commit", "abc123", "tags").unwrap().unwrap(); + let (val, _, _) = db + .get(&TargetType::Commit, "abc123", "tags") + .unwrap() + .unwrap(); let list = crate::list_value::list_values_from_json(&val).unwrap(); assert_eq!(list, vec!["c"]); } @@ -2178,19 +2292,29 @@ mod tests { fn test_list_push_converts_string() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "key", "\"original\"", - "string", + &ValueType::String, "a@b.com", 1000, ) .unwrap(); - db.list_push("commit", "abc123", "key", "appended", "a@b.com", 2000) + db.list_push( + &TargetType::Commit, + "abc123", + "key", + "appended", + "a@b.com", + 2000, + ) + .unwrap(); + let (val, vtype, _) = db + .get(&TargetType::Commit, "abc123", "key") + .unwrap() .unwrap(); - let (val, vtype, _) = db.get("commit", "abc123", "key").unwrap().unwrap(); - assert_eq!(vtype, "list"); + assert_eq!(vtype, ValueType::List); let list = crate::list_value::list_values_from_json(&val).unwrap(); assert_eq!(list, vec!["original", "appended"]); } @@ -2198,13 +2322,16 @@ mod tests { #[test] fn test_list_pop() { let db = Db::open_in_memory().unwrap(); - db.list_push("commit", "abc123", "tags", "a", "a@b.com", 1000) + db.list_push(&TargetType::Commit, "abc123", "tags", "a", "a@b.com", 1000) + .unwrap(); + db.list_push(&TargetType::Commit, "abc123", "tags", "b", "a@b.com", 2000) .unwrap(); - db.list_push("commit", "abc123", "tags", "b", "a@b.com", 2000) + db.list_pop(&TargetType::Commit, "abc123", "tags", "b", "a@b.com", 3000) .unwrap(); - db.list_pop("commit", "abc123", "tags", "b", "a@b.com", 3000) + let (val, _, _) = db + .get(&TargetType::Commit, "abc123", "tags") + .unwrap() .unwrap(); - let (val, _, _) = db.get("commit", "abc123", "tags").unwrap().unwrap(); let list = crate::list_value::list_values_from_json(&val).unwrap(); assert_eq!(list, vec!["a"]); } @@ -2212,9 +2339,9 @@ mod tests { #[test] fn test_apply_tombstone_removes_list_values_rows() { let db = Db::open_in_memory().unwrap(); - db.list_push("commit", "abc123", "tags", "a", "a@b.com", 1000) + db.list_push(&TargetType::Commit, "abc123", "tags", "a", "a@b.com", 1000) .unwrap(); - db.list_push("commit", "abc123", "tags", "b", "a@b.com", 2000) + db.list_push(&TargetType::Commit, "abc123", "tags", "b", "a@b.com", 2000) .unwrap(); let metadata_id: i64 = db @@ -2235,8 +2362,14 @@ mod tests { .unwrap(); assert_eq!(before_count, 2); - db.apply_tombstone("commit", "abc123", "tags", "user@example.com", 3000) - .unwrap(); + db.apply_tombstone( + &TargetType::Commit, + "abc123", + "tags", + "user@example.com", + 3000, + ) + .unwrap(); let after_count: i64 = db .conn @@ -2247,24 +2380,24 @@ mod tests { ) .unwrap(); assert_eq!(after_count, 0); - assert_eq!(db.get("commit", "abc123", "tags").unwrap(), None); + assert_eq!(db.get(&TargetType::Commit, "abc123", "tags").unwrap(), None); } #[test] fn test_authorship() { let db = Db::open_in_memory().unwrap(); db.set( - "commit", + &TargetType::Commit, "abc123", "key", "\"val\"", - "string", + &ValueType::String, "user@example.com", 42000, ) .unwrap(); let (email, ts) = db - .get_authorship("commit", "abc123", "key") + .get_authorship(&TargetType::Commit, "abc123", "key") .unwrap() .unwrap(); assert_eq!(email, "user@example.com"); @@ -2285,7 +2418,13 @@ mod tests { // set stores the timestamp db.set( - "commit", "abc123", "key", "\"val\"", "string", "a@b.com", 5000, + &TargetType::Commit, + "abc123", + "key", + "\"val\"", + &ValueType::String, + "a@b.com", + 5000, ) .unwrap(); let entries = db.get_all_metadata().unwrap(); @@ -2294,24 +2433,51 @@ mod tests { // upsert updates the timestamp db.set( - "commit", "abc123", "key", "\"val2\"", "string", "a@b.com", 9000, + &TargetType::Commit, + "abc123", + "key", + "\"val2\"", + &ValueType::String, + "a@b.com", + 9000, ) .unwrap(); let entries = db.get_all_metadata().unwrap(); assert_eq!(entries[0].5, 9000); // list_push stores the timestamp - db.list_push("commit", "abc123", "tags", "first", "a@b.com", 11000) - .unwrap(); + db.list_push( + &TargetType::Commit, + "abc123", + "tags", + "first", + "a@b.com", + 11000, + ) + .unwrap(); let entries = db.get_all_metadata().unwrap(); let tags = entries.iter().find(|e| e.2 == "tags").unwrap(); assert_eq!(tags.5, 11000); // list_pop updates the timestamp - db.list_push("commit", "abc123", "tags", "second", "a@b.com", 12000) - .unwrap(); - db.list_pop("commit", "abc123", "tags", "second", "a@b.com", 13000) - .unwrap(); + db.list_push( + &TargetType::Commit, + "abc123", + "tags", + "second", + "a@b.com", + 12000, + ) + .unwrap(); + db.list_pop( + &TargetType::Commit, + "abc123", + "tags", + "second", + "a@b.com", + 13000, + ) + .unwrap(); let entries = db.get_all_metadata().unwrap(); let tags = entries.iter().find(|e| e.2 == "tags").unwrap(); assert_eq!(tags.5, 13000); diff --git a/src/main.rs b/src/main.rs index 9b6a309..9f46b9b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,7 +104,10 @@ fn main() -> Result<()> { format, dry_run, since, - } => commands::import::run(&format, dry_run, since.as_deref()), + } => { + let fmt = types::ImportFormat::from_str(&format)?; + commands::import::run(fmt, dry_run, since.as_deref()) + } Commands::Show { commit } => commands::show::run(&commit), diff --git a/src/types.rs b/src/types.rs index c472213..512e505 100644 --- a/src/types.rs +++ b/src/types.rs @@ -21,7 +21,7 @@ impl TargetType { } } - fn from_str(s: &str) -> Result { + pub fn from_str(s: &str) -> Result { match s { "commit" => Ok(TargetType::Commit), "change-id" => Ok(TargetType::ChangeId), @@ -31,6 +31,17 @@ impl TargetType { _ => bail!("unknown target type: {}", s), } } + + /// Returns the English plural form of this target type for display. + pub fn pluralize(&self) -> &str { + match self { + TargetType::Commit => "commits", + TargetType::ChangeId => "change-ids", + TargetType::Branch => "branches", + TargetType::Path => "paths", + TargetType::Project => "project", + } + } } #[derive(Debug, Clone, PartialEq)] @@ -162,6 +173,26 @@ impl ValueType { } } +/// Supported import source formats. +#[derive(Debug, Clone, PartialEq)] +pub enum ImportFormat { + /// Import the entire git history. + Entire, + /// Import from git-ai format. + GitAi, +} + +impl ImportFormat { + /// Parse an import format string. + pub fn from_str(s: &str) -> Result { + match s { + "entire" => Ok(ImportFormat::Entire), + "git-ai" => Ok(ImportFormat::GitAi), + _ => bail!("unsupported import format: {}", s), + } + } +} + /// Size threshold (in bytes) above which file values are stored as git blob references. pub const GIT_REF_THRESHOLD: usize = 1024;