memstead_cli/commands/
rename.rs1use clap::Parser;
7
8use memstead_base::vcs::Actor;
9use memstead_base::{EntityId, RenameEntityArgs};
10
11use crate::CliError;
12use crate::output::{ExitKind, print_json, print_markdown};
13use crate::setup::{CliContext, CliEngine};
14
15#[derive(Parser, Debug)]
16#[command(after_long_help = super::slug_derivation_help())]
17pub struct Args {
18 pub id: String,
20
21 pub new_title: String,
23
24 #[arg(long = "expected-hash", value_name = "HASH")]
26 pub expected_hash: Option<String>,
27
28 #[arg(long, conflicts_with_all = ["expected_hash", "force"])]
30 pub auto_hash: bool,
31
32 #[arg(long, conflicts_with_all = ["expected_hash", "auto_hash"])]
34 pub force: bool,
35
36 #[arg(long)]
40 pub note: Option<String>,
41}
42
43pub fn run(ctx: &CliContext, args: Args) -> anyhow::Result<()> {
44 let id = EntityId::canonical(&args.id);
45 let new_title = args.new_title.clone();
46
47 match ctx.cli_engine()? {
48 #[cfg(feature = "mem-repo")]
49 CliEngine::MemRepo(mut engine) => {
50 let expected_hash = resolve_expected_hash_mem_repo(&engine, &id, &args)?;
51 let result = engine
52 .rename_entity_with_ctx(
53 &id,
54 &new_title,
55 &expected_hash,
56 &crate::setup::cli_ctx_with_note(args.note.clone()),
57 )
58 .map_err(CliError::from_engine_op)?;
59 let mem_changed = engine.take_mem_changed_notices();
60 if ctx.json {
61 let mut body = serde_json::to_value(&result).unwrap_or(serde_json::Value::Null);
62 super::merge_mem_changed_json(&mut body, &mem_changed);
63 print_json(&body)?;
64 } else {
65 print_markdown(&format!(
66 "# Renamed\n\n- `{}` → `{}`\n- Path: {} → {}\n- Hash: `{}`{}",
67 result.old_id,
68 result.new_id,
69 result.old_path,
70 result.new_path,
71 result.content_hash,
72 super::render_mem_changed_block(&mem_changed),
73 ));
74 }
75 }
76 CliEngine::Filesystem(mut engine) => {
77 let expected_hash = resolve_expected_hash_filesystem(&engine, &id, &args)?;
78 let outcome = engine
79 .rename_entity(
80 RenameEntityArgs {
81 id: id.clone(),
82 expected_hash: Some(expected_hash),
83 new_title: new_title.clone(),
84 },
85 Actor::Cli,
86 None,
87 args.note.as_deref(),
88 )
89 .map_err(CliError::from_engine_op)?;
90 if ctx.json {
91 print_json(&serde_json::json!({
92 "old_id": outcome.old_id.as_ref(),
93 "new_id": outcome.new_id.as_ref(),
94 "old_path": outcome.old_path,
95 "new_path": outcome.new_path,
96 "_hash": outcome.content_hash,
97 "warnings": outcome.warnings,
100 }))?;
101 } else {
102 let mut body = format!(
103 "# Renamed\n\n- `{}` → `{}`\n- Path: {} → {}\n- Hash: `{}`",
104 outcome.old_id,
105 outcome.new_id,
106 outcome.old_path,
107 outcome.new_path,
108 outcome.content_hash,
109 );
110 if !outcome.warnings.is_empty() {
111 let parts: Vec<String> =
112 outcome.warnings.iter().map(|w| w.to_string()).collect();
113 body.push_str(&format!("\n- Warnings: {}", parts.join("; ")));
114 }
115 print_markdown(&body);
116 }
117 }
118 }
119 Ok(())
120}
121
122#[cfg(feature = "mem-repo")]
128fn resolve_expected_hash_mem_repo(
129 engine: &memstead_base::Engine,
130 id: &EntityId,
131 args: &Args,
132) -> anyhow::Result<String> {
133 if args.auto_hash || args.force {
134 Ok(engine
135 .get_entity(id)
136 .ok_or_else(|| {
137 CliError::new(
138 ExitKind::NotFound,
139 "ENTITY_NOT_FOUND",
140 format!("entity not found: {id}"),
141 )
142 .with_details(serde_json::json!({ "id": id.to_string() }))
143 })?
144 .content_hash
145 .clone())
146 } else {
147 args.expected_hash
148 .clone()
149 .filter(|h| !h.is_empty())
150 .ok_or_else(|| {
151 CliError::new(
152 ExitKind::Validation,
153 crate::HASH_FLAG_REQUIRED_CODE,
154 "missing --expected-hash. Read the entity first (memstead entity <id>) and pass its `_hash`, \
155 or use --auto-hash / --force.",
156 )
157 .into()
158 })
159 }
160}
161
162fn resolve_expected_hash_filesystem(
163 engine: &memstead_base::Engine,
164 id: &EntityId,
165 args: &Args,
166) -> anyhow::Result<String> {
167 if args.auto_hash || args.force {
168 Ok(engine
169 .get_entity(id)
170 .ok_or_else(|| {
171 CliError::new(
172 ExitKind::NotFound,
173 "ENTITY_NOT_FOUND",
174 format!("entity not found: {id}"),
175 )
176 .with_details(serde_json::json!({ "id": id.to_string() }))
177 })?
178 .content_hash
179 .clone())
180 } else {
181 args.expected_hash
182 .clone()
183 .filter(|h| !h.is_empty())
184 .ok_or_else(|| {
185 CliError::new(
186 ExitKind::Validation,
187 crate::HASH_FLAG_REQUIRED_CODE,
188 "missing --expected-hash. Read the entity first (memstead entity <id>) and pass its `_hash`, \
189 or use --auto-hash / --force.",
190 )
191 .into()
192 })
193 }
194}