use anyhow::{Result, anyhow};
use repo::Repository;
use serde::Serialize;
use super::snapshot::ensure_current_state;
use crate::{
cli::{Cli, MarkerCommands, should_output_json},
config::UserConfig,
};
#[derive(Serialize)]
struct MarkerListOutput {
markers: Vec<MarkerEntry>,
}
#[derive(Serialize)]
struct MarkerEntry {
name: String,
change_id: String,
}
#[derive(Serialize)]
struct MarkerOpOutput {
name: String,
change_id: Option<String>,
message: String,
}
#[derive(Serialize)]
struct MarkerBulkDeleteOutput {
deleted: Vec<MarkerEntry>,
count: usize,
message: String,
}
pub fn cmd_marker(cli: &Cli, command: MarkerCommands) -> Result<()> {
let repo = Repository::open(cli.repo.as_ref().unwrap_or(&std::env::current_dir()?))?;
match command {
MarkerCommands::List { filter } => cmd_marker_list(cli, &repo, filter),
MarkerCommands::Create { name, .. } => cmd_marker_create(cli, &repo, name),
MarkerCommands::Delete { name, prefix } => match (name, prefix) {
(Some(name), None) => cmd_marker_delete(cli, &repo, name),
(None, Some(prefix)) => cmd_marker_delete_prefix(cli, &repo, prefix),
(Some(_), Some(_)) => Err(anyhow!(
"marker delete: cannot combine <NAME> with --prefix"
)),
(None, None) => Err(anyhow!(
"marker delete: provide either <NAME> or --prefix <PFX>"
)),
},
MarkerCommands::Show { name } => cmd_marker_show(cli, &repo, name),
}
}
fn cmd_marker_list(cli: &Cli, repo: &Repository, filter: Option<String>) -> Result<()> {
let markers = repo.refs().list_markers()?;
let entries: Vec<MarkerEntry> = markers
.iter()
.filter(|name| match filter.as_deref() {
Some(prefix) if !prefix.is_empty() => name.starts_with(prefix),
_ => true,
})
.filter_map(|name| {
let state = repo.refs().get_marker(name).ok()??;
Some(MarkerEntry {
name: name.clone(),
change_id: state.short(),
})
})
.collect();
let output = MarkerListOutput { markers: entries };
if should_output_json(cli, Some(repo.config())) {
println!("{}", serde_json::to_string(&output)?);
} else {
for entry in &output.markers {
println!("{} -> {}", entry.name, entry.change_id);
}
if output.markers.is_empty() {
println!("No markers");
}
}
Ok(())
}
fn cmd_marker_create(cli: &Cli, repo: &Repository, name: String) -> Result<()> {
let current = ensure_current_state(
repo,
&UserConfig::load_default().unwrap_or_default(),
Some(format!(
"Bootstrap git-overlay before creating marker {}",
name
)),
)?;
repo.refs().create_marker(&name, ¤t)?;
repo.oplog().record_marker_create(&name, ¤t)?;
let output = MarkerOpOutput {
name: name.clone(),
change_id: Some(current.short()),
message: format!("Created marker '{}' at {}", name, current.short()),
};
if should_output_json(cli, Some(repo.config())) {
println!("{}", serde_json::to_string(&output)?);
} else {
println!("{}", output.message);
}
Ok(())
}
fn cmd_marker_delete(cli: &Cli, repo: &Repository, name: String) -> Result<()> {
let state = repo
.refs()
.delete_marker(&name)?
.ok_or_else(|| anyhow!("Marker not found: {}", name))?;
repo.oplog().record_marker_delete(&name, &state)?;
let output = MarkerOpOutput {
name: name.clone(),
change_id: None,
message: format!("Deleted marker '{}'", name),
};
if should_output_json(cli, Some(repo.config())) {
println!("{}", serde_json::to_string(&output)?);
} else {
println!("{}", output.message);
}
Ok(())
}
fn cmd_marker_delete_prefix(cli: &Cli, repo: &Repository, prefix: String) -> Result<()> {
if prefix.is_empty() {
return Err(anyhow!(
"marker delete --prefix: prefix must be non-empty (refusing to delete every marker)"
));
}
let all = repo.refs().list_markers()?;
let matches: Vec<String> = all
.into_iter()
.filter(|name| name.starts_with(&prefix))
.collect();
let mut deleted: Vec<MarkerEntry> = Vec::with_capacity(matches.len());
for name in &matches {
if let Some(state) = repo.refs().delete_marker(name)? {
repo.oplog().record_marker_delete(name, &state)?;
deleted.push(MarkerEntry {
name: name.clone(),
change_id: state.short(),
});
}
}
let count = deleted.len();
let message = match count {
0 => format!("No markers matched prefix '{}'", prefix),
1 => format!("Deleted 1 marker matching prefix '{}'", prefix),
n => format!("Deleted {} markers matching prefix '{}'", n, prefix),
};
let output = MarkerBulkDeleteOutput {
deleted,
count,
message,
};
if should_output_json(cli, Some(repo.config())) {
println!("{}", serde_json::to_string(&output)?);
} else {
println!("{}", output.message);
for entry in &output.deleted {
println!(" {} -> {}", entry.name, entry.change_id);
}
}
Ok(())
}
fn cmd_marker_show(cli: &Cli, repo: &Repository, name: String) -> Result<()> {
let state_id = repo
.refs()
.get_marker(&name)?
.ok_or_else(|| anyhow!("Marker not found: {}", name))?;
let state = repo
.store()
.get_state(&state_id)?
.ok_or_else(|| anyhow!("State not found for marker: {}", name))?;
let output = MarkerOpOutput {
name: name.clone(),
change_id: Some(state_id.short()),
message: format!("Marker '{}' -> {}", name, state_id.short()),
};
if should_output_json(cli, Some(repo.config())) {
println!("{}", serde_json::to_string(&output)?);
} else {
println!("Marker: {}", name);
println!("State: {}", state_id.short());
if let Some(intent) = &state.intent {
println!("Intent: {}", intent);
}
println!("Created: {}", state.created_at.format("%Y-%m-%d %H:%M:%S"));
}
Ok(())
}