1use std::io::Write;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16use clap::Parser;
17use mkit_core::hash::Hash;
18use mkit_core::object::{Commit, Object};
19use mkit_core::ops::conflict_state::{
20 self, RevertState, in_progress_op_name, is_revert_in_progress,
21};
22use mkit_core::ops::revert::revert as revert_tree;
23use mkit_core::refs::{self, Head};
24use mkit_core::serialize;
25use mkit_core::store::ObjectStore;
26use mkit_core::worktree;
27
28use crate::clap_shim;
29use crate::config;
30use crate::exit;
31use crate::format;
32
33#[derive(Debug, Parser)]
34#[command(
35 name = "mkit revert",
36 about = "Create a new commit that undoes a previous commit."
37)]
38struct RevertOpts {
39 #[arg(long = "continue", conflicts_with_all = ["abort", "commit"])]
41 cont: bool,
42 #[arg(long, conflicts_with_all = ["cont", "commit"])]
44 abort: bool,
45 #[arg(short = 'n', long = "no-commit", conflicts_with_all = ["cont", "abort"])]
49 no_commit: bool,
50 commit: Option<String>,
52}
53
54#[must_use]
55pub fn run(args: &[String]) -> u8 {
56 let opts = match clap_shim::parse::<RevertOpts>("mkit revert", args) {
57 Ok(o) => o,
58 Err(code) => return code,
59 };
60 let cwd = match std::env::current_dir() {
61 Ok(p) => p,
62 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
63 };
64 let store = match ObjectStore::open(&cwd) {
65 Ok(s) => s,
66 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
67 };
68 let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
69 let _lock = match super::acquire_worktree_lock(&cwd) {
70 Ok(l) => l,
71 Err(code) => return code,
72 };
73
74 if opts.abort {
75 abort(&cwd, &mkit_dir, &store)
76 } else if opts.cont {
77 cont(&cwd, &mkit_dir, &store)
78 } else if let Some(hex) = opts.commit.as_deref() {
79 start(&cwd, &mkit_dir, &store, hex, opts.no_commit)
80 } else {
81 super::usage_error("usage: mkit revert <commit> | --continue | --abort")
82 }
83}
84
85fn start(
86 cwd: &std::path::Path,
87 mkit_dir: &std::path::Path,
88 store: &ObjectStore,
89 hex: &str,
90 no_commit: bool,
91) -> u8 {
92 if let Some(op) = in_progress_op_name(mkit_dir) {
93 return emit_err(
94 &format!("a {op} is already in progress (use --continue or --abort)"),
95 exit::GENERAL_ERROR,
96 );
97 }
98 let target: Hash = match super::revspec::resolve_revision(store, mkit_dir, hex) {
99 Ok(h) => h,
100 Err(e) => return emit_err(&format!("bad commit: {e}"), exit::DATAERR),
101 };
102 let ours = match refs::resolve_head(mkit_dir) {
103 Ok(Some(h)) => h,
104 Ok(None) => return emit_err("no commits on current branch", exit::GENERAL_ERROR),
105 Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
106 };
107 let ours_tree = match store.read_object(&ours) {
108 Ok(Object::Commit(c)) => c.tree_hash,
109 Ok(_) => return emit_err("HEAD is not a commit", exit::DATAERR),
110 Err(e) => return emit_err(&format!("read HEAD: {e}"), exit::GENERAL_ERROR),
111 };
112
113 let result = match revert_tree(store, target, ours_tree) {
114 Ok(r) => r,
115 Err(e) => return emit_err(&format!("revert: {e}"), exit::GENERAL_ERROR),
116 };
117
118 if result.has_conflicts() {
119 if let Err(e) = super::ensure_restore_safe(cwd, store, result.tree_hash) {
120 return emit_err(&e, exit::GENERAL_ERROR);
121 }
122 let records = match super::conflict::materialize_conflicts(
123 cwd,
124 store,
125 result.tree_hash,
126 &result.conflicts,
127 ) {
128 Ok(r) => r,
129 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
130 };
131 let state = RevertState {
132 revert_head: target,
133 orig_head: ours,
134 message: result.message.clone(),
135 };
136 if let Err(e) = conflict_state::write_revert_state(mkit_dir, &state, &records) {
137 return emit_err(&format!("write revert state: {e}"), exit::CANTCREAT);
138 }
139 let mut stderr = std::io::stderr().lock();
140 let _ = writeln!(
141 stderr,
142 "revert conflict; resolve the files above, `mkit add` them, then run \
143 `mkit revert --continue` (or `mkit revert --abort`)"
144 );
145 return exit::GENERAL_ERROR;
146 }
147
148 if let Err(e) = super::ensure_restore_safe(cwd, store, result.tree_hash) {
149 return emit_err(&e, exit::GENERAL_ERROR);
150 }
151
152 if no_commit {
155 if let Err(e) = super::restore_worktree_and_index(cwd, store, result.tree_hash) {
156 return emit_err(&e, exit::GENERAL_ERROR);
157 }
158 let mut stderr = std::io::stderr().lock();
159 let _ = writeln!(
160 stderr,
161 "staged revert of {} (no commit; run `mkit commit` when ready)",
162 format::short_hash(&target, 8),
163 );
164 return exit::OK;
165 }
166
167 let commit_hash = match create_commit(cwd, store, result.tree_hash, ours, &result.message) {
168 Ok(h) => h,
169 Err(code) => return code,
170 };
171 if let Err(e) = super::restore_worktree_and_index(cwd, store, result.tree_hash) {
172 return emit_err(&e, exit::GENERAL_ERROR);
173 }
174 if let Err(e) = advance_head(mkit_dir, &commit_hash) {
175 return emit_err(&e, exit::CANTCREAT);
176 }
177 let mut stderr = std::io::stderr().lock();
178 let _ = writeln!(
179 stderr,
180 "reverted {} as {}",
181 format::short_hash(&target, 8),
182 format::short_hash(&commit_hash, 8),
183 );
184 exit::OK
185}
186
187fn cont(cwd: &std::path::Path, mkit_dir: &std::path::Path, store: &ObjectStore) -> u8 {
188 let state = match conflict_state::read_revert_state(mkit_dir) {
189 Ok(Some(s)) => s,
190 Ok(None) => return emit_err("no revert in progress", exit::GENERAL_ERROR),
191 Err(e) => return emit_err(&format!("read revert state: {e}"), exit::GENERAL_ERROR),
192 };
193 let records = match conflict_state::read_conflicts(mkit_dir) {
194 Ok(r) => r,
195 Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
196 };
197 match super::conflict::first_unresolved_marker(cwd, &records) {
198 Ok(Some(path)) => {
199 return emit_err(
200 &format!(
201 "unresolved conflict markers remain in '{path}'; resolve and `mkit add` it"
202 ),
203 exit::GENERAL_ERROR,
204 );
205 }
206 Ok(None) => {}
207 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
208 }
209 if let Err(e) = super::conflict::ensure_conflict_paths_staged(cwd, store, &records) {
210 return emit_err(&e, exit::GENERAL_ERROR);
211 }
212
213 let idx = match super::read_or_seed_index_from_head(cwd, store) {
214 Ok(i) => i,
215 Err(e) => return emit_err(&e, exit::GENERAL_ERROR),
216 };
217 let tree_hash = match worktree::build_tree_from_index(store, &idx) {
218 Ok(t) => t,
219 Err(e) => return emit_err(&format!("build tree from index: {e}"), exit::GENERAL_ERROR),
220 };
221 let parent = match refs::resolve_head(mkit_dir) {
222 Ok(Some(h)) => h,
223 Ok(None) => state.orig_head,
224 Err(e) => return emit_err(&format!("resolve HEAD: {e}"), exit::GENERAL_ERROR),
225 };
226 let commit_hash = match create_commit(cwd, store, tree_hash, parent, &state.message) {
227 Ok(h) => h,
228 Err(code) => return code,
229 };
230 if let Err(e) = super::restore_worktree_and_index(cwd, store, tree_hash) {
231 return emit_err(&e, exit::GENERAL_ERROR);
232 }
233 if let Err(e) = advance_head(mkit_dir, &commit_hash) {
234 return emit_err(&e, exit::CANTCREAT);
235 }
236 if let Err(e) = conflict_state::clear_revert_state(mkit_dir) {
237 return emit_err(&format!("clear revert state: {e}"), exit::GENERAL_ERROR);
238 }
239 let mut stderr = std::io::stderr().lock();
240 let _ = writeln!(
241 stderr,
242 "reverted {} as {}",
243 format::short_hash(&state.revert_head, 8),
244 format::short_hash(&commit_hash, 8),
245 );
246 exit::OK
247}
248
249fn abort(cwd: &std::path::Path, mkit_dir: &std::path::Path, store: &ObjectStore) -> u8 {
250 if !is_revert_in_progress(mkit_dir) {
251 return emit_err("no revert in progress", exit::GENERAL_ERROR);
252 }
253 let state = match conflict_state::read_revert_state(mkit_dir) {
254 Ok(Some(s)) => s,
255 Ok(None) => return emit_err("no revert in progress", exit::GENERAL_ERROR),
256 Err(e) => return emit_err(&format!("read revert state: {e}"), exit::GENERAL_ERROR),
257 };
258 let records = match conflict_state::read_conflicts(mkit_dir) {
259 Ok(r) => r,
260 Err(e) => return emit_err(&format!("read conflicts: {e}"), exit::GENERAL_ERROR),
261 };
262 if let Err(code) = restore_to(cwd, mkit_dir, store, state.orig_head, &records) {
263 return code;
264 }
265 if let Err(e) = conflict_state::clear_revert_state(mkit_dir) {
266 return emit_err(&format!("clear revert state: {e}"), exit::GENERAL_ERROR);
267 }
268 let mut stderr = std::io::stderr().lock();
269 let _ = writeln!(stderr, "revert aborted; HEAD restored");
270 exit::OK
271}
272
273fn restore_to(
274 cwd: &std::path::Path,
275 mkit_dir: &std::path::Path,
276 store: &ObjectStore,
277 target: Hash,
278 records: &[mkit_core::ops::conflict_state::ConflictRecord],
279) -> Result<(), u8> {
280 let target_tree = load_tree_hash(store, target)?;
281 if let Err(e) = super::conflict::ensure_abort_safe(cwd, store, records, target_tree) {
282 return Err(emit_err(&e, exit::GENERAL_ERROR));
283 }
284 if let Err(e) = super::conflict::reset_conflict_paths(cwd, store, records, target_tree) {
285 return Err(emit_err(&e, exit::GENERAL_ERROR));
286 }
287 if let Err(e) = super::ensure_restore_safe(cwd, store, target_tree) {
288 return Err(emit_err(&e, exit::GENERAL_ERROR));
289 }
290 if let Err(e) = super::restore_worktree_and_index(cwd, store, target_tree) {
291 return Err(emit_err(&e, exit::GENERAL_ERROR));
292 }
293 let head = refs::read_head(mkit_dir).unwrap_or(Head::Branch("main".to_string()));
294 match head {
295 Head::Branch(name) => super::write_ref_recording_history(
296 mkit_dir,
297 &name,
298 refs::RefWriteCondition::Any,
299 &target,
300 )
301 .map_err(|e| emit_err(&format!("restore ref: {e}"), exit::CANTCREAT)),
302 Head::Detached(_) => refs::write_head_detached(mkit_dir, &target)
303 .map_err(|e| emit_err(&format!("restore HEAD: {e}"), exit::CANTCREAT)),
304 }
305}
306
307fn create_commit(
308 cwd: &std::path::Path,
309 store: &ObjectStore,
310 tree_hash: Hash,
311 parent: Hash,
312 message: &[u8],
313) -> Result<Hash, u8> {
314 let cfg = config::read_or_default(cwd)
315 .map_err(|e| emit_err(&format!("config: {e}"), exit::CONFIG_ERROR))?;
316 let mut signer =
317 super::commit::load_commit_signer(cwd, &cfg).map_err(|(msg, code)| emit_err(&msg, code))?;
318 let signer_public = signer
319 .public_key()
320 .map_err(|(msg, code)| emit_err(&msg, code))?;
321 let author = super::commit::resolve_author(None, &cfg.user_identity, &signer_public)
322 .map_err(|e| emit_err(&format!("author: {e}"), exit::CONFIG_ERROR))?;
323 let timestamp = SystemTime::now()
324 .duration_since(UNIX_EPOCH)
325 .map_or(0, |d| d.as_secs());
326 let mut unsigned = Commit::new_unannotated(
327 tree_hash,
328 vec![parent],
329 author,
330 signer_public,
331 message.to_vec(),
332 timestamp,
333 [0u8; 64],
334 );
335 let sig = signer
336 .sign_commit(&unsigned)
337 .map_err(|(msg, code)| emit_err(&msg, code))?;
338 unsigned.signature = sig;
339 let bytes = serialize::serialize(&Object::Commit(unsigned))
340 .map_err(|e| emit_err(&format!("serialize: {e}"), exit::DATAERR))?;
341 store
342 .write(&bytes)
343 .map_err(|e| emit_err(&format!("store commit: {e}"), exit::CANTCREAT))
344}
345
346fn load_tree_hash(store: &ObjectStore, commit_hash: Hash) -> Result<Hash, u8> {
347 match store.read_object(&commit_hash) {
348 Ok(Object::Commit(c)) => Ok(c.tree_hash),
349 Ok(_) => Err(emit_err("object is not a commit", exit::DATAERR)),
350 Err(e) => Err(emit_err(&format!("read commit: {e}"), exit::GENERAL_ERROR)),
351 }
352}
353
354fn advance_head(mkit_dir: &std::path::Path, new_head: &Hash) -> Result<(), String> {
355 let head = refs::read_head(mkit_dir).unwrap_or(Head::Branch("main".to_string()));
356 match head {
357 Head::Branch(name) => super::write_ref_recording_history(
358 mkit_dir,
359 &name,
360 refs::RefWriteCondition::Any,
361 new_head,
362 )
363 .map_err(|e| format!("write ref: {e}")),
364 Head::Detached(_) => {
365 refs::write_head_detached(mkit_dir, new_head).map_err(|e| format!("update HEAD: {e}"))
366 }
367 }
368}
369
370fn emit_err(msg: &str, code: u8) -> u8 {
371 let mut stderr = std::io::stderr().lock();
372 let _ = writeln!(stderr, "error: {msg}");
373 code
374}