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