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 "write_id": outcome.write_id,
100 "warnings": outcome.warnings,
103 }))?;
104 } else {
105 let mut body = format!(
106 "# Renamed\n\n- `{}` → `{}`\n- Path: {} → {}\n- Hash: `{}`",
107 outcome.old_id,
108 outcome.new_id,
109 outcome.old_path,
110 outcome.new_path,
111 outcome.content_hash,
112 );
113 if !outcome.warnings.is_empty() {
114 let parts: Vec<String> =
115 outcome.warnings.iter().map(|w| w.to_string()).collect();
116 body.push_str(&format!("\n- Warnings: {}", parts.join("; ")));
117 }
118 print_markdown(&body);
119 }
120 }
121 }
122 Ok(())
123}
124
125#[cfg(feature = "mem-repo")]
131fn resolve_expected_hash_mem_repo(
132 engine: &memstead_base::Engine,
133 id: &EntityId,
134 args: &Args,
135) -> anyhow::Result<String> {
136 if args.auto_hash || args.force {
137 Ok(engine
138 .get_entity(id)
139 .ok_or_else(|| {
140 CliError::new(
141 ExitKind::NotFound,
142 "ENTITY_NOT_FOUND",
143 format!("entity not found: {id}"),
144 )
145 .with_details(serde_json::json!({ "id": id.to_string() }))
146 })?
147 .content_hash
148 .clone())
149 } else {
150 args.expected_hash
151 .clone()
152 .filter(|h| !h.is_empty())
153 .ok_or_else(|| {
154 CliError::new(
155 ExitKind::Validation,
156 crate::HASH_FLAG_REQUIRED_CODE,
157 "missing --expected-hash. Read the entity first (memstead entity <id>) and pass its `_hash`, \
158 or use --auto-hash / --force.",
159 )
160 .into()
161 })
162 }
163}
164
165fn resolve_expected_hash_filesystem(
166 engine: &memstead_base::Engine,
167 id: &EntityId,
168 args: &Args,
169) -> anyhow::Result<String> {
170 if args.auto_hash || args.force {
171 Ok(engine
172 .get_entity(id)
173 .ok_or_else(|| {
174 CliError::new(
175 ExitKind::NotFound,
176 "ENTITY_NOT_FOUND",
177 format!("entity not found: {id}"),
178 )
179 .with_details(serde_json::json!({ "id": id.to_string() }))
180 })?
181 .content_hash
182 .clone())
183 } else {
184 args.expected_hash
185 .clone()
186 .filter(|h| !h.is_empty())
187 .ok_or_else(|| {
188 CliError::new(
189 ExitKind::Validation,
190 crate::HASH_FLAG_REQUIRED_CODE,
191 "missing --expected-hash. Read the entity first (memstead entity <id>) and pass its `_hash`, \
192 or use --auto-hash / --force.",
193 )
194 .into()
195 })
196 }
197}