Skip to main content

git_branchless_undo/
lib.rs

1//! Allows undoing to a previous state of the repo.
2//!
3//! This is accomplished by finding the events that have happened since a certain
4//! time and inverting them.
5
6#![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    // FIXME: the information of which branch is checked out is not stored in
72    // the event log. For now, just pretend that `HEAD` was always detached in
73    // the past.
74    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            // Not sure if this can happen. When a repo is created, maybe?
179            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            // For tests: only pump the Cursive event loop if we have no events
486            // of our own to process. Otherwise, the event loop queues up all of
487            // the messages before we can process them, which means that none of
488            // the screenshots are correct.
489            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                // If we haven't received a message yet, defer to `siv.step`
552                // to process the next user input.
553                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        // This isn't really an "invertible" event, in that there's no way to
713        // calculate an inverse event that restores the working copy state to
714        // *before* this snapshot.
715        //
716        // Instead, we're indicating that to undo to the point in the past when
717        // this event occurred, we want to check out the working copy as it
718        // existed at that point in time.
719        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                // Do nothing.
879            }
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                // Create or update the given reference.
916                repo.create_reference(ref_name, *new_oid, true, "branchless undo")?;
917            }
918
919            Event::WorkingCopySnapshot { .. } => {
920                // Should be handled as the checkout target already.
921            }
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/// Restore the repository to a previous state interactively.
952#[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        // Don't let `event_cursor` leak from this scope, since we intend to
966        // determine a new event cursor below.
967        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}