use std::fs;
use anyhow::{bail, Context, Result};
use crate::context::CommandContext;
use git_meta_lib::types::{MetaValue, Target, ValueType, GIT_REF_THRESHOLD};
use git_meta_lib::ListEntry;
fn print_result(action: &str, key: &str, target: &Target, json: bool) {
let target_str = match target.value() {
Some(v) => format!("{} {}", target.target_type().as_str(), v),
None => target.target_type().as_str().to_string(),
};
if json {
let json_obj = serde_json::json!({
"action": action,
"key": key,
"target_type": target.target_type().as_str(),
"target_value": target.value().unwrap_or(""),
});
println!("{}", serde_json::to_string(&json_obj).unwrap_or_default());
} else {
println!("{action} key {key} for {target_str}");
}
}
pub fn run(
target_str: &str,
key: &str,
value: Option<&str>,
file: Option<&str>,
value_type_str: &str,
json: bool,
timestamp: Option<i64>,
) -> Result<()> {
let ctx = CommandContext::open(timestamp)?;
let target = ctx.session.resolve_target(&Target::parse(target_str)?)?;
let value_type = value_type_str.parse::<ValueType>()?;
let from_file = file.is_some();
let raw_value = match (value, file) {
(Some(_), Some(_)) => bail!("cannot specify both a value and -F/--file"),
(None, None) => bail!("must specify either a value or -F/--file"),
(Some(v), None) => v.to_string(),
(None, Some(path)) => {
fs::read_to_string(path).with_context(|| format!("failed to read file: {path}"))?
}
};
let use_git_ref =
from_file && matches!(value_type, ValueType::String) && raw_value.len() > GIT_REF_THRESHOLD;
if use_git_ref {
let repo = ctx.session.repo();
let email = ctx.session.email().to_string();
let ts = time::OffsetDateTime::now_utc().unix_timestamp_nanos() as i64 / 1_000_000;
let blob_oid: gix::ObjectId = repo.write_blob(raw_value.as_bytes())?.into();
ctx.session.store().set_with_git_ref(
None,
&target,
key,
&blob_oid.to_string(),
&value_type,
&email,
ts,
true,
)?;
} else {
let handle = ctx.session.target(&target);
let meta_value = match value_type {
ValueType::String => MetaValue::String(raw_value),
ValueType::List => {
let entries = parse_list_input(&raw_value)?;
MetaValue::List(entries)
}
ValueType::Set => {
let values: Vec<String> = serde_json::from_str(&raw_value)?;
MetaValue::Set(values.into_iter().collect())
}
_ => bail!("unsupported value type"),
};
handle.set(key, meta_value)?;
}
print_result("set", key, &target, json);
Ok(())
}
pub fn run_add(
target_str: &str,
key: &str,
value: &str,
json: bool,
timestamp: Option<i64>,
) -> Result<()> {
let ctx = CommandContext::open(timestamp)?;
let target = ctx.session.resolve_target(&Target::parse(target_str)?)?;
ctx.session.target(&target).set_add(key, value)?;
print_result("added", key, &target, json);
Ok(())
}
pub fn run_rm(
target_str: &str,
key: &str,
value: &str,
json: bool,
timestamp: Option<i64>,
) -> Result<()> {
let ctx = CommandContext::open(timestamp)?;
let target = ctx.session.resolve_target(&Target::parse(target_str)?)?;
ctx.session.target(&target).set_remove(key, value)?;
print_result("removed", key, &target, json);
Ok(())
}
fn parse_list_input(raw: &str) -> Result<Vec<ListEntry>> {
let items: Vec<serde_json::Value> = serde_json::from_str(raw)?;
items
.into_iter()
.enumerate()
.map(|(idx, item)| match item {
serde_json::Value::String(s) => Ok(ListEntry {
value: s,
timestamp: idx as i64,
}),
serde_json::Value::Object(ref map) => {
let value = map
.get("value")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("list entry missing 'value' field"))?
.to_string();
let timestamp = match map.get("timestamp") {
Some(serde_json::Value::Number(n)) => n
.as_i64()
.ok_or_else(|| anyhow::anyhow!("list entry 'timestamp' must be integer"))?,
Some(serde_json::Value::String(s)) => s
.parse::<i64>()
.map_err(|_| anyhow::anyhow!("list entry 'timestamp' must be integer"))?,
None => idx as i64,
_ => bail!("list entry 'timestamp' must be integer"),
};
Ok(ListEntry { value, timestamp })
}
other => bail!("invalid list entry: expected string or object, got {other:?}"),
})
.collect()
}