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_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    // 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 =
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            // Not sure if this can happen. When a repo is created, maybe?
180            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            // For tests: only pump the Cursive event loop if we have no events
485            // of our own to process. Otherwise, the event loop queues up all of
486            // the messages before we can process them, which means that none of
487            // the screenshots are correct.
488            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                // If we haven't received a message yet, defer to `siv.step`
549                // to process the next user input.
550                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        // This isn't really an "invertible" event, in that there's no way to
710        // calculate an inverse event that restores the working copy state to
711        // *before* this snapshot.
712        //
713        // Instead, we're indicating that to undo to the point in the past when
714        // this event occurred, we want to check out the working copy as it
715        // existed at that point in time.
716        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                // Do nothing.
874            }
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                // Create or update the given reference.
911                repo.create_reference(ref_name, *new_oid, true, "branchless undo")?;
912            }
913
914            Event::WorkingCopySnapshot { .. } => {
915                // Should be handled as the checkout target already.
916            }
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/// Restore the repository to a previous state interactively.
945#[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        // Don't let `event_cursor` leak from this scope, since we intend to
959        // determine a new event cursor below.
960        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}