1#![warn(missing_docs)]
7#![warn(
8 clippy::all,
9 clippy::as_conversions,
10 clippy::clone_on_ref_ptr,
11 clippy::dbg_macro
12)]
13#![allow(clippy::too_many_arguments, clippy::blocks_in_if_conditions)]
14
15pub mod tui;
16
17use std::fmt::Write;
18use std::io::{stdin, BufRead, BufReader, Read};
19use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
20use std::time::SystemTime;
21
22use cursive_core::event::Key;
23use cursive_core::traits::Resizable;
24use cursive_core::utils::markup::StyledString;
25use cursive_core::views::{
26 Dialog, EditView, LinearLayout, OnEventView, Panel, ScrollView, TextView,
27};
28use cursive_core::{Cursive, CursiveRunner};
29use eyre::Context;
30use lib::core::check_out::{check_out_commit, CheckOutCommitOptions, CheckoutTarget};
31use lib::core::repo_ext::RepoExt;
32use lib::try_exit_code;
33use lib::util::{ExitCode, EyreExitOr};
34use tracing::instrument;
35
36use crate::tui::{with_siv, SingletonView};
37use git_branchless_revset::resolve_default_smartlog_commits;
38use git_branchless_smartlog::{make_smartlog_graph, render_graph};
39use lib::core::dag::{CommitSet, Dag};
40use lib::core::effects::Effects;
41use lib::core::eventlog::{Event, EventCursor, EventLogDb, EventReplayer, EventTransactionId};
42use lib::core::formatting::{Glyphs, Pluralize, StyledStringBuilder};
43use lib::core::node_descriptors::{
44 BranchesDescriptor, CommitMessageDescriptor, CommitOidDescriptor,
45 DifferentialRevisionDescriptor, ObsolescenceExplanationDescriptor, Redactor,
46 RelativeTimeDescriptor,
47};
48use lib::git::{CategorizedReferenceName, GitRunInfo, MaybeZeroOid, Repo, ResolvedReferenceInfo};
49
50fn render_cursor_smartlog(
51 effects: &Effects,
52 repo: &Repo,
53 dag: &Dag,
54 event_replayer: &EventReplayer,
55 event_cursor: EventCursor,
56) -> eyre::Result<Vec<StyledString>> {
57 let mut dag = dag.set_cursor(effects, repo, event_replayer, event_cursor)?;
58 let references_snapshot = event_replayer.get_references_snapshot(repo, event_cursor)?;
59 dag.sync_from_oids(
60 effects,
61 repo,
62 CommitSet::from(references_snapshot.main_branch_oid),
63 references_snapshot
64 .branch_oid_to_names
65 .keys()
66 .chain(references_snapshot.head_oid.iter())
67 .copied()
68 .collect(),
69 )?;
70
71 let head_info = ResolvedReferenceInfo {
75 oid: references_snapshot.head_oid,
76 reference_name: None,
77 };
78
79 let commits = resolve_default_smartlog_commits(effects, repo, &mut dag)?;
80 let graph = make_smartlog_graph(
81 effects,
82 repo,
83 &dag,
84 event_replayer,
85 event_cursor,
86 &commits,
87 false,
88 )?;
89 let result = render_graph(
90 effects,
91 repo,
92 &dag,
93 &graph,
94 references_snapshot.head_oid,
95 &mut [
96 &mut CommitOidDescriptor::new(true)?,
97 &mut RelativeTimeDescriptor::new(repo, SystemTime::now())?,
98 &mut ObsolescenceExplanationDescriptor::new(event_replayer, event_cursor)?,
99 &mut BranchesDescriptor::new(
100 repo,
101 &head_info,
102 &references_snapshot,
103 &Redactor::Disabled,
104 )?,
105 &mut DifferentialRevisionDescriptor::new(repo, &Redactor::Disabled)?,
106 &mut CommitMessageDescriptor::new(&Redactor::Disabled)?,
107 ],
108 )?;
109 Ok(result)
110}
111
112fn describe_event(glyphs: &Glyphs, repo: &Repo, event: &Event) -> eyre::Result<Vec<StyledString>> {
113 const EMPTY_EVENT_MESSAGE: &str =
114 "This may be an unsupported use-case; see https://github.com/arxanas/git-branchless/issues/57";
115
116 let result = match event {
117 Event::CommitEvent {
118 timestamp: _,
119 event_tx_id: _,
120 commit_oid,
121 } => {
122 vec![
123 StyledStringBuilder::new()
124 .append_plain("Commit ")
125 .append(repo.friendly_describe_commit_from_oid(glyphs, *commit_oid)?)
126 .build(),
127 StyledString::new(),
128 ]
129 }
130
131 Event::ObsoleteEvent {
132 timestamp: _,
133 event_tx_id: _,
134 commit_oid,
135 }
136 | Event::RewriteEvent {
137 timestamp: _,
138 event_tx_id: _,
139 old_commit_oid: MaybeZeroOid::NonZero(commit_oid),
140 new_commit_oid: MaybeZeroOid::Zero,
141 } => {
142 vec![
143 StyledStringBuilder::new()
144 .append_plain("Hide commit ")
145 .append(repo.friendly_describe_commit_from_oid(glyphs, *commit_oid)?)
146 .build(),
147 StyledString::new(),
148 ]
149 }
150
151 Event::UnobsoleteEvent {
152 timestamp: _,
153 event_tx_id: _,
154 commit_oid,
155 }
156 | Event::RewriteEvent {
157 timestamp: _,
158 event_tx_id: _,
159 old_commit_oid: MaybeZeroOid::Zero,
160 new_commit_oid: MaybeZeroOid::NonZero(commit_oid),
161 } => {
162 vec![
163 StyledStringBuilder::new()
164 .append_plain("Unhide commit ")
165 .append(repo.friendly_describe_commit_from_oid(glyphs, *commit_oid)?)
166 .build(),
167 StyledString::new(),
168 ]
169 }
170
171 Event::RefUpdateEvent {
172 timestamp: _,
173 event_tx_id: _,
174 ref_name,
175 old_oid: MaybeZeroOid::Zero,
176 new_oid: MaybeZeroOid::NonZero(new_oid),
177 message: _,
178 } if ref_name.as_str() == "HEAD" => {
179 vec![
181 StyledStringBuilder::new()
182 .append_plain("Check out to ")
183 .append(repo.friendly_describe_commit_from_oid(glyphs, *new_oid)?)
184 .build(),
185 StyledString::new(),
186 ]
187 }
188
189 Event::RefUpdateEvent {
190 timestamp: _,
191 event_tx_id: _,
192 ref_name,
193 old_oid: MaybeZeroOid::NonZero(old_oid),
194 new_oid: MaybeZeroOid::NonZero(new_oid),
195 message: _,
196 } if ref_name.as_str() == "HEAD" => {
197 vec![
198 StyledStringBuilder::new()
199 .append_plain("Check out from ")
200 .append(repo.friendly_describe_commit_from_oid(glyphs, *old_oid)?)
201 .build(),
202 StyledStringBuilder::new()
203 .append_plain(" to ")
204 .append(repo.friendly_describe_commit_from_oid(glyphs, *new_oid)?)
205 .build(),
206 ]
207 }
208
209 Event::RefUpdateEvent {
210 timestamp: _,
211 event_tx_id: _,
212 ref_name,
213 old_oid: MaybeZeroOid::Zero,
214 new_oid: MaybeZeroOid::Zero,
215 message: _,
216 } => {
217 vec![
218 StyledStringBuilder::new()
219 .append_plain("Empty event for ")
220 .append_plain(CategorizedReferenceName::new(ref_name).render_full())
221 .build(),
222 StyledStringBuilder::new()
223 .append_plain(EMPTY_EVENT_MESSAGE)
224 .build(),
225 ]
226 }
227
228 Event::RefUpdateEvent {
229 timestamp: _,
230 event_tx_id: _,
231 ref_name,
232 old_oid: MaybeZeroOid::Zero,
233 new_oid: MaybeZeroOid::NonZero(new_oid),
234 message: _,
235 } => {
236 vec![
237 StyledStringBuilder::new()
238 .append_plain("Create ")
239 .append_plain(CategorizedReferenceName::new(ref_name).friendly_describe())
240 .append_plain(" at ")
241 .append(repo.friendly_describe_commit_from_oid(glyphs, *new_oid)?)
242 .build(),
243 StyledString::new(),
244 ]
245 }
246
247 Event::RefUpdateEvent {
248 timestamp: _,
249 event_tx_id: _,
250 ref_name,
251 old_oid: MaybeZeroOid::NonZero(old_oid),
252 new_oid: MaybeZeroOid::Zero,
253 message: _,
254 } => {
255 vec![
256 StyledStringBuilder::new()
257 .append_plain("Delete ")
258 .append_plain(CategorizedReferenceName::new(ref_name).friendly_describe())
259 .append_plain(" at ")
260 .append(repo.friendly_describe_commit_from_oid(glyphs, *old_oid)?)
261 .build(),
262 StyledString::new(),
263 ]
264 }
265
266 Event::RefUpdateEvent {
267 timestamp: _,
268 event_tx_id: _,
269 ref_name,
270 old_oid: MaybeZeroOid::NonZero(old_oid),
271 new_oid: MaybeZeroOid::NonZero(new_oid),
272 message: _,
273 } => {
274 let ref_name = CategorizedReferenceName::new(ref_name).friendly_describe();
275 vec![
276 StyledStringBuilder::new()
277 .append_plain("Move ")
278 .append_plain(ref_name.clone())
279 .append_plain(" from ")
280 .append(repo.friendly_describe_commit_from_oid(glyphs, *old_oid)?)
281 .build(),
282 StyledStringBuilder::new()
283 .append_plain(" ")
284 .append_plain(" ".repeat(ref_name.len()))
285 .append_plain(" to ")
286 .append(repo.friendly_describe_commit_from_oid(glyphs, *new_oid)?)
287 .build(),
288 ]
289 }
290
291 Event::RewriteEvent {
292 timestamp: _,
293 event_tx_id: _,
294 old_commit_oid: MaybeZeroOid::NonZero(old_commit_oid),
295 new_commit_oid: MaybeZeroOid::NonZero(new_commit_oid),
296 } => {
297 vec![
298 StyledStringBuilder::new()
299 .append_plain("Rewrite commit ")
300 .append(repo.friendly_describe_commit_from_oid(glyphs, *old_commit_oid)?)
301 .build(),
302 StyledStringBuilder::new()
303 .append_plain(" as ")
304 .append(repo.friendly_describe_commit_from_oid(glyphs, *new_commit_oid)?)
305 .build(),
306 ]
307 }
308
309 Event::RewriteEvent {
310 timestamp: _,
311 event_tx_id: _,
312 old_commit_oid: MaybeZeroOid::Zero,
313 new_commit_oid: MaybeZeroOid::Zero,
314 } => {
315 vec![
316 StyledStringBuilder::new()
317 .append_plain("Empty rewrite event.")
318 .build(),
319 StyledStringBuilder::new()
320 .append_plain(EMPTY_EVENT_MESSAGE)
321 .build(),
322 ]
323 }
324
325 Event::WorkingCopySnapshot {
326 timestamp: _,
327 event_tx_id: _,
328 head_oid: MaybeZeroOid::NonZero(head_oid),
329 commit_oid,
330 ref_name: Some(ref_name),
331 } => {
332 let branch_name = CategorizedReferenceName::new(ref_name);
333 vec![
334 StyledStringBuilder::new()
335 .append_plain("Restore snapshot for ")
336 .append_plain(branch_name.friendly_describe())
337 .build(),
338 StyledStringBuilder::new()
339 .append_plain(" pointing to ")
340 .append(repo.friendly_describe_commit_from_oid(glyphs, *head_oid)?)
341 .build(),
342 StyledStringBuilder::new()
343 .append_plain(" backed up using ")
344 .append(repo.friendly_describe_commit_from_oid(glyphs, *commit_oid)?)
345 .build(),
346 ]
347 }
348
349 Event::WorkingCopySnapshot {
350 timestamp: _,
351 event_tx_id: _,
352 head_oid: MaybeZeroOid::NonZero(head_oid),
353 commit_oid,
354 ref_name: None,
355 } => {
356 vec![
357 StyledStringBuilder::new()
358 .append_plain("Restore snapshot for ")
359 .append(repo.friendly_describe_commit_from_oid(glyphs, *head_oid)?)
360 .build(),
361 StyledStringBuilder::new()
362 .append_plain(" backed up using ")
363 .append(repo.friendly_describe_commit_from_oid(glyphs, *commit_oid)?)
364 .build(),
365 ]
366 }
367
368 Event::WorkingCopySnapshot {
369 timestamp: _,
370 event_tx_id: _,
371 head_oid: MaybeZeroOid::Zero,
372 commit_oid,
373 ref_name: Some(ref_name),
374 } => {
375 let branch_name = CategorizedReferenceName::new(ref_name);
376 vec![
377 StyledStringBuilder::new()
378 .append_plain("Restore snapshot for ")
379 .append_plain(branch_name.friendly_describe())
380 .build(),
381 StyledStringBuilder::new()
382 .append_plain(" backed up using ")
383 .append(repo.friendly_describe_commit_from_oid(glyphs, *commit_oid)?)
384 .build(),
385 ]
386 }
387
388 Event::WorkingCopySnapshot {
389 timestamp: _,
390 event_tx_id: _,
391 head_oid: MaybeZeroOid::Zero,
392 commit_oid,
393 ref_name: None,
394 } => {
395 vec![StyledStringBuilder::new()
396 .append_plain("Restore snapshot backed up using ")
397 .append(repo.friendly_describe_commit_from_oid(glyphs, *commit_oid)?)
398 .build()]
399 }
400 };
401 Ok(result)
402}
403
404fn describe_events_numbered(
405 glyphs: &Glyphs,
406 repo: &Repo,
407 events: &[Event],
408) -> Result<Vec<StyledString>, eyre::Error> {
409 let mut lines = Vec::new();
410 for (i, event) in (1..).zip(events) {
411 let num_header = format!("{i}. ");
412 for (j, event_line) in (0..).zip(describe_event(glyphs, repo, event)?) {
413 let prefix = if j == 0 {
414 num_header.clone()
415 } else {
416 " ".repeat(num_header.len())
417 };
418 lines.push(
419 StyledStringBuilder::new()
420 .append_plain(prefix)
421 .append(event_line)
422 .build(),
423 );
424 }
425 }
426 Ok(lines)
427}
428
429#[instrument(skip(siv))]
430fn select_past_event(
431 mut siv: CursiveRunner<Cursive>,
432 effects: &Effects,
433 repo: &Repo,
434 dag: &Dag,
435 event_replayer: &mut EventReplayer,
436) -> eyre::Result<Option<EventCursor>> {
437 #[derive(Clone, Copy, Debug)]
438 enum Message {
439 Init,
440 Next,
441 Previous,
442 GoToEvent,
443 SetEventReplayerCursor { event_id: isize },
444 Help,
445 Quit,
446 SelectEventIdAndQuit,
447 }
448 let (main_tx, main_rx): (Sender<Message>, Receiver<Message>) = channel();
449
450 [
451 ('n'.into(), Message::Next),
452 ('N'.into(), Message::Next),
453 (Key::Right.into(), Message::Next),
454 ('p'.into(), Message::Previous),
455 ('P'.into(), Message::Previous),
456 (Key::Left.into(), Message::Previous),
457 ('h'.into(), Message::Help),
458 ('H'.into(), Message::Help),
459 ('?'.into(), Message::Help),
460 ('g'.into(), Message::GoToEvent),
461 ('G'.into(), Message::GoToEvent),
462 ('q'.into(), Message::Quit),
463 ('Q'.into(), Message::Quit),
464 (
465 cursive_core::event::Key::Enter.into(),
466 Message::SelectEventIdAndQuit,
467 ),
468 ]
469 .iter()
470 .cloned()
471 .for_each(|(event, message): (cursive_core::event::Event, Message)| {
472 siv.add_global_callback(event, {
473 let main_tx = main_tx.clone();
474 move |_siv| main_tx.send(message).unwrap()
475 });
476 });
477
478 let mut cursor = event_replayer.make_default_cursor();
479 let now = SystemTime::now();
480 main_tx.send(Message::Init)?;
481 while siv.is_running() {
482 let message = main_rx.try_recv();
483 if message.is_err() {
484 siv.step();
489 }
490
491 declare_views! {
492 SmartlogView => ScrollView<TextView>,
493 InfoView => TextView,
494 }
495
496 let redraw = |siv: &mut Cursive,
497 event_replayer: &mut EventReplayer,
498 event_cursor: EventCursor|
499 -> eyre::Result<()> {
500 let smartlog =
501 render_cursor_smartlog(effects, repo, dag, event_replayer, event_cursor)?;
502 SmartlogView::find(siv)
503 .get_inner_mut()
504 .set_content(StyledStringBuilder::from_lines(smartlog));
505
506 let event = event_replayer.get_tx_events_before_cursor(event_cursor);
507 let info_view_contents = match event {
508 None => vec![StyledString::plain(
509 "There are no previous available events.",
510 )],
511 Some((event_id, events)) => {
512 let event_description_lines =
513 describe_events_numbered(effects.get_glyphs(), repo, events)?;
514 let relative_time_provider = RelativeTimeDescriptor::new(repo, now)?;
515 let relative_time = if relative_time_provider.is_enabled() {
516 format!(
517 " ({} ago)",
518 RelativeTimeDescriptor::describe_time_delta(
519 now,
520 events[0].get_timestamp()
521 )?
522 )
523 } else {
524 String::new()
525 };
526
527 let mut lines = vec![StyledStringBuilder::new()
528 .append_plain("Repo after transaction ")
529 .append_plain(events[0].get_event_tx_id().to_string())
530 .append_plain(" (event ")
531 .append_plain(event_id.to_string())
532 .append_plain(")")
533 .append_plain(relative_time)
534 .append_plain(". Press 'h' for help, 'q' to quit.")
535 .build()];
536 lines.extend(event_description_lines);
537 lines
538 }
539 };
540 InfoView::find(siv).set_content(StyledStringBuilder::from_lines(info_view_contents));
541 Ok(())
542 };
543
544 match message {
545 Err(TryRecvError::Disconnected) => break,
546
547 Err(TryRecvError::Empty) => {
548 continue;
551 }
552
553 Ok(Message::Init) => {
554 let smartlog_view: SmartlogView = ScrollView::new(TextView::new("")).into();
555 let info_view: InfoView = TextView::new("").into();
556 siv.add_fullscreen_layer(
557 LinearLayout::vertical()
558 .child(
559 Panel::new(smartlog_view)
560 .title("Commit graph")
561 .full_height(),
562 )
563 .child(Panel::new(ScrollView::new(info_view)).title("Events"))
564 .full_width(),
565 );
566 redraw(&mut siv, event_replayer, cursor)?;
567 }
568
569 Ok(Message::Next) => {
570 cursor = event_replayer.advance_cursor_by_transaction(cursor, 1);
571 redraw(&mut siv, event_replayer, cursor)?;
572 }
573
574 Ok(Message::Previous) => {
575 cursor = event_replayer.advance_cursor_by_transaction(cursor, -1);
576 redraw(&mut siv, event_replayer, cursor)?;
577 }
578
579 Ok(Message::SetEventReplayerCursor { event_id }) => {
580 cursor = event_replayer.make_cursor(event_id);
581 redraw(&mut siv, event_replayer, cursor)?;
582 }
583
584 Ok(Message::GoToEvent) => {
585 let main_tx = main_tx.clone();
586 siv.add_layer(
587 OnEventView::new(
588 Dialog::new()
589 .title("Go to event")
590 .content(EditView::new().on_submit(move |siv, text| {
591 match text.parse::<isize>() {
592 Ok(event_id) => {
593 main_tx
594 .send(Message::SetEventReplayerCursor { event_id })
595 .unwrap();
596 siv.pop_layer();
597 }
598 Err(_) => {
599 siv.add_layer(Dialog::info(format!(
600 "Invalid event ID: {text}"
601 )));
602 }
603 }
604 }))
605 .dismiss_button("Cancel"),
606 )
607 .on_event(Key::Esc, |siv| {
608 siv.pop_layer();
609 }),
610 );
611 }
612
613 Ok(Message::Help) => {
614 siv.add_layer(
615 Dialog::new()
616 .title("How to use")
617 .content(TextView::new(
618"Use `git undo` to view and revert to previous states of the repository.
619
620h/?: Show this help.
621q: Quit.
622p/n or <left>/<right>: View next/previous state.
623g: Go to a provided event ID.
624<enter>: Revert the repository to the given state (requires confirmation).
625
626You can also copy a commit hash from the past and manually run `git unhide` or `git rebase` on it.
627",
628 ))
629 .dismiss_button("Close"),
630 );
631 }
632
633 Ok(Message::Quit) => siv.quit(),
634
635 Ok(Message::SelectEventIdAndQuit) => {
636 siv.quit();
637 return Ok(Some(cursor));
638 }
639 };
640
641 if message.is_ok() {
642 siv.refresh();
643 }
644 }
645
646 Ok(None)
647}
648
649fn inverse_event(
650 event: Event,
651 now: SystemTime,
652 event_tx_id: EventTransactionId,
653) -> eyre::Result<Event> {
654 let timestamp = now.duration_since(SystemTime::UNIX_EPOCH)?.as_secs_f64();
655 let inverse_event = match event {
656 Event::CommitEvent {
657 timestamp: _,
658 event_tx_id: _,
659 commit_oid,
660 }
661 | Event::UnobsoleteEvent {
662 timestamp: _,
663 event_tx_id: _,
664 commit_oid,
665 } => Event::ObsoleteEvent {
666 timestamp,
667 event_tx_id,
668 commit_oid,
669 },
670
671 Event::ObsoleteEvent {
672 timestamp: _,
673 event_tx_id: _,
674 commit_oid,
675 } => Event::UnobsoleteEvent {
676 timestamp,
677 event_tx_id,
678 commit_oid,
679 },
680
681 Event::RewriteEvent {
682 timestamp: _,
683 event_tx_id: _,
684 old_commit_oid,
685 new_commit_oid,
686 } => Event::RewriteEvent {
687 timestamp,
688 event_tx_id,
689 old_commit_oid: new_commit_oid,
690 new_commit_oid: old_commit_oid,
691 },
692
693 Event::RefUpdateEvent {
694 timestamp: _,
695 event_tx_id: _,
696 ref_name,
697 old_oid: old_ref,
698 new_oid: new_ref,
699 message: _,
700 } => Event::RefUpdateEvent {
701 timestamp,
702 event_tx_id,
703 ref_name,
704 old_oid: new_ref,
705 new_oid: old_ref,
706 message: None,
707 },
708
709 event @ Event::WorkingCopySnapshot { .. } => event,
717 };
718 Ok(inverse_event)
719}
720
721#[derive(Clone, Debug)]
722struct UndoCheckoutTarget {
723 target: CheckoutTarget,
724 options: CheckOutCommitOptions,
725}
726
727fn extract_checkout_target(
728 events: &[Event],
729) -> eyre::Result<(Option<UndoCheckoutTarget>, Vec<&Event>)> {
730 let mut new_events = Vec::new();
731 let mut checkout_target = None;
732 for event in events.iter() {
733 match event {
734 Event::RefUpdateEvent {
735 timestamp: _,
736 event_tx_id: _,
737 ref_name,
738 old_oid: _,
739 new_oid: MaybeZeroOid::NonZero(new_oid),
740 message: _,
741 } if ref_name.as_str() == "HEAD" => {
742 checkout_target = Some(UndoCheckoutTarget {
743 target: CheckoutTarget::Oid(*new_oid),
744 options: CheckOutCommitOptions {
745 additional_args: vec!["--detach".into()],
746 reset: false,
747 render_smartlog: true,
748 },
749 });
750 }
751
752 Event::WorkingCopySnapshot {
753 timestamp: _,
754 event_tx_id: _,
755 head_oid: _,
756 commit_oid,
757 ref_name,
758 } => {
759 checkout_target = Some(UndoCheckoutTarget {
760 target: CheckoutTarget::Oid(*commit_oid),
761 options: CheckOutCommitOptions {
762 additional_args: match ref_name {
763 Some(ref_name) => {
764 let branch_name = CategorizedReferenceName::new(ref_name);
765 vec!["-B".into(), branch_name.render_suffix().into()]
766 }
767 None => Default::default(),
768 },
769 reset: false,
770 render_smartlog: true,
771 },
772 })
773 }
774
775 event => new_events.push(event),
776 };
777 }
778
779 Ok((checkout_target, new_events))
780}
781
782#[instrument(skip(in_))]
783fn undo_events(
784 in_: &mut impl Read,
785 effects: &Effects,
786 repo: &Repo,
787 git_run_info: &GitRunInfo,
788 event_log_db: &mut EventLogDb,
789 event_replayer: &EventReplayer,
790 event_cursor: EventCursor,
791 skip_confirmation: bool,
792) -> EyreExitOr<()> {
793 let now = SystemTime::now();
794 let event_tx_id = event_log_db.make_transaction_id(now, "undo")?;
795 let head_info = repo.get_head_info()?;
796 let inverse_events: Vec<Event> = event_replayer
797 .get_events_since_cursor(event_cursor)
798 .iter()
799 .rev()
800 .filter(|event| {
801 !matches!(
802 event,
803 Event::RefUpdateEvent {
804 timestamp: _,
805 event_tx_id: _,
806 ref_name,
807 old_oid: MaybeZeroOid::Zero,
808 new_oid: _,
809 message: _,
810 } if ref_name.as_str() == "HEAD"
811 )
812 })
813 .map(|event| inverse_event(event.clone(), now, event_tx_id))
814 .collect::<eyre::Result<Vec<Event>>>()?;
815
816 if inverse_events.is_empty() {
817 writeln!(
818 effects.get_output_stream(),
819 "No undo actions to apply, exiting."
820 )?;
821 return Ok(Ok(()));
822 }
823 writeln!(effects.get_output_stream(), "Will apply these actions:")?;
824 let events = describe_events_numbered(effects.get_glyphs(), repo, &inverse_events)?;
825 for line in events {
826 writeln!(
827 effects.get_output_stream(),
828 "{}",
829 effects.get_glyphs().render(line)?
830 )?;
831 }
832
833 let confirmed = if skip_confirmation {
834 true
835 } else {
836 write!(effects.get_output_stream(), "Confirm? [yN] ")?;
837 let mut user_input = String::new();
838 let mut reader = BufReader::new(in_);
839 match reader.read_line(&mut user_input) {
840 Ok(_size) => {
841 let user_input = user_input.trim();
842 user_input == "y" || user_input == "Y"
843 }
844 Err(_) => false,
845 }
846 };
847 if !confirmed {
848 writeln!(effects.get_output_stream(), "Aborted.")?;
849 return Ok(Err(ExitCode(1)));
850 }
851
852 let num_inverse_events = Pluralize {
853 determiner: None,
854 amount: inverse_events.len(),
855 unit: ("inverse event", "inverse events"),
856 }
857 .to_string();
858
859 let (checkout_target, filtered_events) = extract_checkout_target(&inverse_events)?;
860 if checkout_target.is_some() {
861 repo.detach_head(&head_info)?;
862 }
863 for event in filtered_events.into_iter() {
864 match event {
865 Event::RefUpdateEvent {
866 timestamp: _,
867 event_tx_id: _,
868 ref_name: _,
869 old_oid: MaybeZeroOid::Zero,
870 new_oid: MaybeZeroOid::Zero,
871 message: _,
872 } => {
873 }
875 Event::RefUpdateEvent {
876 timestamp: _,
877 event_tx_id: _,
878 ref_name,
879 old_oid: MaybeZeroOid::NonZero(_),
880 new_oid: MaybeZeroOid::Zero,
881 message: _,
882 } => match repo.find_reference(ref_name)? {
883 Some(mut reference) => {
884 reference.delete().wrap_err("Applying `RefUpdateEvent`")?;
885 }
886 None => {
887 writeln!(
888 effects.get_output_stream(),
889 "Reference {} did not exist, not deleting it.",
890 ref_name.as_str()
891 )?;
892 }
893 },
894 Event::RefUpdateEvent {
895 timestamp: _,
896 event_tx_id: _,
897 ref_name,
898 old_oid: MaybeZeroOid::Zero,
899 new_oid: MaybeZeroOid::NonZero(new_oid),
900 message: _,
901 }
902 | Event::RefUpdateEvent {
903 timestamp: _,
904 event_tx_id: _,
905 ref_name,
906 old_oid: MaybeZeroOid::NonZero(_),
907 new_oid: MaybeZeroOid::NonZero(new_oid),
908 message: _,
909 } => {
910 repo.create_reference(ref_name, *new_oid, true, "branchless undo")?;
912 }
913
914 Event::WorkingCopySnapshot { .. } => {
915 }
917
918 Event::CommitEvent { .. }
919 | Event::ObsoleteEvent { .. }
920 | Event::UnobsoleteEvent { .. }
921 | Event::RewriteEvent { .. } => {
922 event_log_db.add_events(vec![event.clone()])?;
923 }
924 }
925 }
926
927 if let Some(UndoCheckoutTarget { target, options }) = checkout_target {
928 try_exit_code!(check_out_commit(
929 effects,
930 git_run_info,
931 repo,
932 event_log_db,
933 event_tx_id,
934 Some(target),
935 &options,
936 )
937 .wrap_err("Updating to previous HEAD location")?);
938 }
939
940 writeln!(effects.get_output_stream(), "Applied {num_inverse_events}.")?;
941 Ok(Ok(()))
942}
943
944#[instrument]
946pub fn undo(
947 effects: &Effects,
948 git_run_info: &GitRunInfo,
949 interactive: bool,
950 skip_confirmation: bool,
951) -> EyreExitOr<()> {
952 let repo = Repo::from_current_dir()?;
953 let references_snapshot = repo.get_references_snapshot()?;
954 let conn = repo.get_db_conn()?;
955 let mut event_log_db = EventLogDb::new(&conn)?;
956 let mut event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?;
957 let dag = {
958 let event_cursor = event_replayer.make_default_cursor();
961
962 Dag::open_and_sync(
963 effects,
964 &repo,
965 &event_replayer,
966 event_cursor,
967 &references_snapshot,
968 )?
969 };
970
971 let event_cursor = {
972 if interactive {
973 let result = with_siv(effects, |effects, siv| {
974 select_past_event(siv, &effects, &repo, &dag, &mut event_replayer)
975 })?;
976 match result {
977 Some(event_cursor) => event_cursor,
978 None => return Ok(Ok(())),
979 }
980 } else {
981 event_replayer.advance_cursor_by_transaction(event_replayer.make_default_cursor(), -1)
982 }
983 };
984
985 let result = undo_events(
986 &mut stdin(),
987 effects,
988 &repo,
989 git_run_info,
990 &mut event_log_db,
991 &event_replayer,
992 event_cursor,
993 skip_confirmation,
994 )?;
995 Ok(result)
996}
997
998#[allow(missing_docs)]
999pub mod testing {
1000 use std::io::Read;
1001
1002 use cursive_core::{Cursive, CursiveRunner};
1003
1004 use lib::core::dag::Dag;
1005 use lib::core::effects::Effects;
1006 use lib::core::eventlog::{EventCursor, EventLogDb, EventReplayer};
1007 use lib::git::{GitRunInfo, Repo};
1008 use lib::util::EyreExitOr;
1009
1010 pub fn select_past_event(
1011 siv: CursiveRunner<Cursive>,
1012 effects: &Effects,
1013 repo: &Repo,
1014 dag: &Dag,
1015 event_replayer: &mut EventReplayer,
1016 ) -> eyre::Result<Option<EventCursor>> {
1017 super::select_past_event(siv, effects, repo, dag, event_replayer)
1018 }
1019
1020 pub fn undo_events(
1021 in_: &mut impl Read,
1022 effects: &Effects,
1023 repo: &Repo,
1024 git_run_info: &GitRunInfo,
1025 event_log_db: &mut EventLogDb,
1026 event_replayer: &EventReplayer,
1027 event_cursor: EventCursor,
1028 ) -> EyreExitOr<()> {
1029 super::undo_events(
1030 in_,
1031 effects,
1032 repo,
1033 git_run_info,
1034 event_log_db,
1035 event_replayer,
1036 event_cursor,
1037 false,
1038 )
1039 }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045
1046 use lib::core::eventlog::testing::new_event_transaction_id;
1047
1048 #[test]
1049 fn test_optimize_inverse_events() -> eyre::Result<()> {
1050 let event_tx_id = new_event_transaction_id(123);
1051 let input = vec![
1052 Event::RefUpdateEvent {
1053 timestamp: 1.0,
1054 event_tx_id,
1055 ref_name: "HEAD".into(),
1056 old_oid: MaybeZeroOid::NonZero("1".parse()?),
1057 new_oid: MaybeZeroOid::NonZero("2".parse()?),
1058 message: None,
1059 },
1060 Event::RefUpdateEvent {
1061 timestamp: 2.0,
1062 event_tx_id,
1063 ref_name: "HEAD".into(),
1064 old_oid: MaybeZeroOid::NonZero("1".parse()?),
1065 new_oid: MaybeZeroOid::NonZero("3".parse()?),
1066 message: None,
1067 },
1068 ];
1069 insta::assert_debug_snapshot!(extract_checkout_target(&input)?, @r###"
1070 (
1071 Some(
1072 UndoCheckoutTarget {
1073 target: Oid(
1074 NonZeroOid(3000000000000000000000000000000000000000),
1075 ),
1076 options: CheckOutCommitOptions {
1077 additional_args: [
1078 "--detach",
1079 ],
1080 reset: false,
1081 render_smartlog: true,
1082 },
1083 },
1084 ),
1085 [],
1086 )
1087 "###);
1088 Ok(())
1089 }
1090}