1use std::io::Write;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16use clap::{Parser, ValueEnum};
17use mkit_core::hash::Hash;
18use mkit_core::layout::RepoLayout;
19use mkit_core::object::{Commit, Object};
20use mkit_core::ops::conflict_state::{
21 self, RevertState, in_progress_op_name, is_revert_in_progress,
22};
23use mkit_core::ops::revert::revert as revert_tree;
24use mkit_core::refs;
25use mkit_core::serialize;
26use mkit_core::store::ObjectStore;
27use mkit_core::worktree;
28
29use super::{advance_head, error as emit_err, load_tree_hash};
30use crate::clap_shim;
31use crate::config;
32use crate::exit;
33use crate::format::{self, JsonObject, json_string_array};
34
35#[derive(Debug, Clone, Copy, ValueEnum)]
36enum RevertFormat {
37 Default,
38 Json,
39}
40
41#[derive(Debug, Parser)]
42#[command(
43 name = "mkit revert",
44 about = "Create a new commit that undoes a previous commit."
45)]
46#[allow(clippy::struct_excessive_bools)] struct RevertOpts {
48 #[arg(long = "continue", conflicts_with_all = ["abort", "commit"])]
50 cont: bool,
51 #[arg(long, conflicts_with_all = ["cont", "commit"])]
53 abort: bool,
54 #[arg(short = 'n', long = "no-commit", conflicts_with_all = ["cont", "abort"])]
58 no_commit: bool,
59 #[arg(long = "no-edit")]
62 no_edit: bool,
63 #[arg(long, value_enum, default_value = "default")]
67 format: RevertFormat,
68 commit: Option<String>,
70}
71
72fn emit_err_json(msg: &str, code: u8, json: bool) -> u8 {
75 if json {
76 let mut obj = JsonObject::new();
77 obj.field_bool("ok", false).field_str("error", msg);
78 let mut stdout = std::io::stdout().lock();
79 let _ = writeln!(stdout, "{}", obj.finish());
80 }
81 emit_err(msg, code)
82}
83
84#[must_use]
85pub fn run(args: &[String]) -> u8 {
86 let opts = match clap_shim::parse::<RevertOpts>("mkit revert", args) {
87 Ok(o) => o,
88 Err(code) => return code,
89 };
90 let _ = opts.no_edit; let json = matches!(opts.format, RevertFormat::Json);
92 let cwd = match std::env::current_dir() {
93 Ok(p) => p,
94 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
95 };
96 let layout = match super::resolve_layout(&cwd) {
97 Ok(layout) => layout,
98 Err(code) => return code,
99 };
100 let store = match ObjectStore::open(&layout) {
101 Ok(s) => s,
102 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
103 };
104 let _lock = match super::acquire_worktree_lock(&layout) {
105 Ok(l) => l,
106 Err(code) => return code,
107 };
108
109 if opts.abort {
110 abort(&layout, &store, json)
111 } else if opts.cont {
112 cont(&layout, &store, json)
113 } else if let Some(hex) = opts.commit.as_deref() {
114 start(&layout, &store, hex, opts.no_commit, json)
115 } else {
116 super::usage_error("usage: mkit revert <commit> | --continue | --abort")
117 }
118}
119
120#[allow(clippy::too_many_lines)] fn start(layout: &RepoLayout, store: &ObjectStore, hex: &str, no_commit: bool, json: bool) -> u8 {
122 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
123 if let Some(op) = in_progress_op_name(layout) {
124 return emit_err(
125 &format!("a {op} is already in progress (use --continue or --abort)"),
126 exit::GENERAL_ERROR,
127 );
128 }
129 let target: Hash = match super::revspec::resolve_revision(store, layout, hex) {
130 Ok(h) => super::log::peel_tags(store, h),
135 Err(e) => return emit_err(&format!("bad commit: {e}"), exit::DATAERR),
136 };
137 let ours = match refs::resolve_head(layout) {
138 Ok(Some(h)) => h,
139 Ok(None) => return emit_err("no commits on current branch", exit::GENERAL_ERROR),
140 Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
141 };
142 let ours_tree = match store.read_object(&ours) {
143 Ok(Object::Commit(c)) => c.tree_hash,
144 Ok(_) => return emit_err("HEAD is not a commit", exit::DATAERR),
145 Err(e) => return emit_err(&format!("read HEAD: {e}"), exit::GENERAL_ERROR),
146 };
147
148 let result = match revert_tree(store, target, ours_tree) {
149 Ok(r) => r,
150 Err(e) => return emit_err(&format!("revert: {e}"), exit::GENERAL_ERROR),
151 };
152
153 if result.has_conflicts() {
154 if let Err(e) = super::ensure_restore_safe(layout, store, result.tree_hash) {
155 return emit_err(&e, exit::GENERAL_ERROR);
156 }
157 let records = match super::conflict::materialize_conflicts(
158 layout,
159 store,
160 result.tree_hash,
161 &result.conflicts,
162 ) {
163 Ok(r) => r,
164 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
165 };
166 let state = RevertState {
167 revert_head: target,
168 orig_head: ours,
169 message: result.message.clone(),
170 };
171 if let Err(e) = conflict_state::write_revert_state(layout, &state, &records) {
172 return emit_err(&format!("write revert state: {e}"), exit::CANTCREAT);
173 }
174 if let Err(e) =
177 conflict_state::write_result_tree(layout.worktree_state_dir(), &result.tree_hash)
178 {
179 return emit_err(&format!("write revert state: {e}"), exit::CANTCREAT);
180 }
181 let mut stderr = std::io::stderr().lock();
182 let _ = writeln!(
183 stderr,
184 "revert conflict; resolve the files above, `mkit add` them, then run \
185 `mkit revert --continue` (or `mkit revert --abort`)"
186 );
187 drop(stderr);
188 if json {
189 let paths: Vec<&str> = records.iter().map(|r| r.path.as_str()).collect();
190 let mut obj = JsonObject::new();
191 obj.field_bool("ok", false)
192 .field_str("kind", "conflict")
193 .field_raw("conflicts", &json_string_array(&paths))
194 .field_str("error", "revert conflict; resolve and continue or abort");
195 let mut stdout = std::io::stdout().lock();
196 let _ = writeln!(stdout, "{}", obj.finish());
197 }
198 return exit::GENERAL_ERROR;
199 }
200
201 if let Err(e) = super::ensure_restore_safe(layout, store, result.tree_hash) {
202 return emit_err(&e, exit::GENERAL_ERROR);
203 }
204
205 if no_commit {
208 if let Err(e) = super::restore_worktree_and_index(layout, store, result.tree_hash) {
209 return emit_err(&e, exit::GENERAL_ERROR);
210 }
211 if let Err(e) =
215 super::stage_removed_tombstones(layout, store, Some(ours_tree), result.tree_hash)
216 {
217 return emit_err(&e, exit::GENERAL_ERROR);
218 }
219 let mut stderr = std::io::stderr().lock();
220 let _ = writeln!(
221 stderr,
222 "staged revert of {} (no commit; run `mkit commit` when ready)",
223 format::short_hash(&target, 8),
224 );
225 drop(stderr);
226 if json {
227 let mut obj = JsonObject::new();
228 obj.field_bool("ok", true)
229 .field_str("kind", "no-commit")
230 .field_hash("reverted", &target)
231 .field_hash("tree", &result.tree_hash);
232 let mut stdout = std::io::stdout().lock();
233 let _ = writeln!(stdout, "{}", obj.finish());
234 }
235 return exit::OK;
236 }
237
238 let commit_hash = match create_commit(layout, store, result.tree_hash, ours, &result.message) {
239 Ok(h) => h,
240 Err(code) => return code,
241 };
242 if let Err(e) = super::restore_worktree_and_index(layout, store, result.tree_hash) {
243 return emit_err(&e, exit::GENERAL_ERROR);
244 }
245 if let Err(e) = advance_head(layout, &commit_hash) {
246 return emit_err(&e, exit::CANTCREAT);
247 }
248 let subject = String::from_utf8_lossy(&result.message)
250 .lines()
251 .next()
252 .unwrap_or("")
253 .to_owned();
254 let branch_name = match mkit_core::refs::read_head(layout) {
255 Ok(mkit_core::refs::Head::Branch(b)) => Some(b),
256 _ => None,
257 };
258 let head_ref = match &branch_name {
259 Some(b) => super::summary::HeadRef::Branch(b),
260 None => super::summary::HeadRef::Detached,
261 };
262 let mut stderr = std::io::stderr().lock();
263 super::summary::print_commit_summary(
264 &mut stderr,
265 store,
266 &head_ref,
267 &commit_hash,
268 &subject,
269 false,
270 Some(ours_tree),
271 Some(result.tree_hash),
272 );
273 drop(stderr);
274 if json {
275 let mut obj = JsonObject::new();
276 obj.field_bool("ok", true)
277 .field_str("kind", "commit")
278 .field_hash("hash", &commit_hash)
279 .field_hash("reverted", &target)
280 .field_hash("tree", &result.tree_hash);
281 let mut stdout = std::io::stdout().lock();
282 let _ = writeln!(stdout, "{}", obj.finish());
283 }
284 exit::OK
285}
286
287fn cont(layout: &RepoLayout, store: &ObjectStore, json: bool) -> u8 {
288 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
289 let state = match conflict_state::read_revert_state(layout) {
290 Ok(Some(s)) => s,
291 Ok(None) => return emit_err("no revert in progress", exit::GENERAL_ERROR),
292 Err(e) => return emit_err(&format!("read revert state: {e}"), exit::GENERAL_ERROR),
293 };
294 let records = match conflict_state::read_conflicts(layout.worktree_state_dir()) {
295 Ok(r) => r,
296 Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
297 };
298 match super::conflict::first_unresolved_marker(layout.worktree_root(), &records) {
299 Ok(Some(path)) => {
300 return emit_err(
301 &format!(
302 "unresolved conflict markers remain in '{path}'; resolve and `mkit add` it"
303 ),
304 exit::GENERAL_ERROR,
305 );
306 }
307 Ok(None) => {}
308 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
309 }
310 if let Err(e) = super::conflict::ensure_conflict_paths_staged(layout, store, &records) {
311 return emit_err(&e, exit::GENERAL_ERROR);
312 }
313
314 let idx = match super::read_or_seed_index_from_head(layout, store) {
315 Ok(i) => i,
316 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
317 };
318 let tree_hash = match worktree::build_tree_from_index(store, &idx) {
319 Ok(t) => t,
320 Err(e) => return emit_err(&format!("build tree from index: {e}"), exit::GENERAL_ERROR),
321 };
322 let parent = match refs::resolve_head(layout) {
323 Ok(Some(h)) => h,
324 Ok(None) => state.orig_head,
325 Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
326 };
327 let commit_hash = match create_commit(layout, store, tree_hash, parent, &state.message) {
328 Ok(h) => h,
329 Err(code) => return code,
330 };
331 if let Err(e) = super::sync_index_to_tree(layout, store, tree_hash) {
336 return emit_err(&e, exit::GENERAL_ERROR);
337 }
338 if let Err(e) = advance_head(layout, &commit_hash) {
339 return emit_err(&e, exit::CANTCREAT);
340 }
341 if let Err(e) = conflict_state::clear_revert_state(layout) {
342 return emit_err(&format!("clear revert state: {e}"), exit::GENERAL_ERROR);
343 }
344 let mut stderr = std::io::stderr().lock();
345 let _ = writeln!(
346 stderr,
347 "reverted {} as {}",
348 format::short_hash(&state.revert_head, 8),
349 format::short_hash(&commit_hash, 8),
350 );
351 drop(stderr);
352 if json {
353 let mut obj = JsonObject::new();
354 obj.field_bool("ok", true)
355 .field_str("kind", "commit")
356 .field_hash("hash", &commit_hash)
357 .field_hash("reverted", &state.revert_head)
358 .field_hash("tree", &tree_hash);
359 let mut stdout = std::io::stdout().lock();
360 let _ = writeln!(stdout, "{}", obj.finish());
361 }
362 exit::OK
363}
364
365fn abort(layout: &RepoLayout, store: &ObjectStore, json: bool) -> u8 {
366 let emit_err = |msg: &str, code: u8| emit_err_json(msg, code, json);
367 if !is_revert_in_progress(layout) {
368 return emit_err("no revert in progress", exit::GENERAL_ERROR);
369 }
370 let state = match conflict_state::read_revert_state(layout) {
371 Ok(Some(s)) => s,
372 Ok(None) => return emit_err("no revert in progress", exit::GENERAL_ERROR),
373 Err(e) => return emit_err(&format!("read revert state: {e}"), exit::GENERAL_ERROR),
374 };
375 let records = match conflict_state::read_conflicts(layout.worktree_state_dir()) {
376 Ok(r) => r,
377 Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
378 };
379 if let Err(code) = restore_to(layout, store, state.orig_head, &records) {
380 return code;
381 }
382 if let Err(e) = conflict_state::clear_revert_state(layout) {
383 return emit_err(&format!("clear revert state: {e}"), exit::GENERAL_ERROR);
384 }
385 let mut stderr = std::io::stderr().lock();
386 let _ = writeln!(stderr, "revert aborted; HEAD restored");
387 drop(stderr);
388 if json {
389 let mut obj = JsonObject::new();
390 obj.field_bool("ok", true)
391 .field_str("kind", "aborted")
392 .field_hash("hash", &state.orig_head);
393 let mut stdout = std::io::stdout().lock();
394 let _ = writeln!(stdout, "{}", obj.finish());
395 }
396 exit::OK
397}
398
399fn restore_to(
400 layout: &RepoLayout,
401 store: &ObjectStore,
402 target: Hash,
403 records: &[mkit_core::ops::conflict_state::ConflictRecord],
404) -> Result<(), u8> {
405 let target_tree = load_tree_hash(store, target)?;
406 let op_result = conflict_state::read_result_tree(layout.worktree_state_dir())
409 .ok()
410 .flatten();
411 if let Err(e) =
412 super::conflict::ensure_abort_safe(layout, store, records, target_tree, op_result)
413 {
414 return Err(emit_err(&e, exit::GENERAL_ERROR));
415 }
416 if let Err(e) =
417 super::conflict::reset_conflict_paths(layout, store, records, target_tree, op_result)
418 {
419 return Err(emit_err(&e, exit::GENERAL_ERROR));
420 }
421 if let Err(e) = super::ensure_restore_safe(layout, store, target_tree) {
422 return Err(emit_err(&e, exit::GENERAL_ERROR));
423 }
424 if let Err(e) = super::restore_worktree_and_index(layout, store, target_tree) {
425 return Err(emit_err(&e, exit::GENERAL_ERROR));
426 }
427 super::restore_head_ref(layout, &target)
428}
429
430fn create_commit(
431 layout: &RepoLayout,
432 store: &ObjectStore,
433 tree_hash: Hash,
434 parent: Hash,
435 message: &[u8],
436) -> Result<Hash, u8> {
437 let cfg = config::read_or_default(layout)
438 .map_err(|e| emit_err(&format!("config: {e}"), exit::CONFIG_ERROR))?;
439 let mut signer = super::commit::load_commit_signer(layout, &cfg)
440 .map_err(|(msg, code)| emit_err(&msg, code))?;
441 let signer_public = signer
442 .public_key()
443 .map_err(|(msg, code)| emit_err(&msg, code))?;
444 let author = super::commit::resolve_author(None, &cfg.user_identity, &signer_public)
445 .map_err(|e| emit_err(&format!("author: {e}"), exit::CONFIG_ERROR))?;
446 let timestamp = SystemTime::now()
447 .duration_since(UNIX_EPOCH)
448 .map_or(0, |d| d.as_secs());
449 let mut unsigned = Commit::new_unannotated(
450 tree_hash,
451 vec![parent],
452 author,
453 signer_public,
454 message.to_vec(),
455 timestamp,
456 [0u8; 64],
457 );
458 let sig = signer
459 .sign_commit(&unsigned)
460 .map_err(|(msg, code)| emit_err(&msg, code))?;
461 unsigned.signature = sig;
462 let bytes = serialize::serialize(&Object::Commit(unsigned))
463 .map_err(|e| emit_err(&format!("serialize: {e}"), exit::DATAERR))?;
464 store
465 .write(&bytes)
466 .map_err(|e| emit_err(&format!("store commit: {e}"), exit::CANTCREAT))
467}