branchless/core/
eventlog.rs

1//! Process our event log.
2//!
3//! We use Git hooks to record the actions that the user takes over time, and put
4//! them in persistent storage. Later, we play back the actions in order to
5//! determine what actions the user took on the repository, and which commits
6//! they're still working on.
7
8use std::cmp::Ordering;
9use std::collections::{HashMap, HashSet};
10
11use std::fmt::Display;
12use std::str::FromStr;
13use std::time::{Duration, SystemTime};
14
15use eyre::Context;
16use tracing::{error, instrument};
17
18use crate::core::effects::{Effects, OperationType};
19use crate::core::repo_ext::RepoExt;
20use crate::git::{CategorizedReferenceName, MaybeZeroOid, NonZeroOid, ReferenceName, Repo};
21
22use super::repo_ext::RepoReferencesSnapshot;
23
24/// When this environment variable is set, we reuse the ID for the transaction
25/// which the caller has already started.
26pub const BRANCHLESS_TRANSACTION_ID_ENV_VAR: &str = "BRANCHLESS_TRANSACTION_ID";
27
28// Wrapper around the row stored directly in the database.
29#[derive(Clone, Debug)]
30struct Row {
31    timestamp: f64,
32    type_: String,
33    event_tx_id: isize,
34    ref1: Option<ReferenceName>,
35    ref2: Option<ReferenceName>,
36    ref_name: Option<ReferenceName>,
37    message: Option<ReferenceName>,
38}
39
40/// The ID associated with the transactions that created an event.
41///
42/// A "event transaction" is a group of logically-related events. For example,
43/// during a rebase operation, all of the rebased commits have different
44/// `CommitEvent`s, but should belong to the same transaction. This improves the
45/// experience for `git undo`, since the user probably wants to see or undo all
46/// of the logically-related events at once, rather than individually.
47///
48/// Note that some logically-related events may not be included together in the
49/// same transaction. For example, if a rebase is interrupted due to a merge
50/// conflict, then the commits applied due to `git rebase` and the commits
51/// applied due to `git rebase --continue` may not share the same transaction.
52/// In this sense, a transaction is "best-effort".
53///
54/// Unlike in a database, there is no specific guarantee that an event
55/// transaction is an atomic unit of work.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum EventTransactionId {
58    /// A normal transaction ID.
59    Id(isize),
60
61    /// A value indicating that the no events should actually be added for this transaction.
62    Suppressed,
63}
64
65impl Display for EventTransactionId {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            EventTransactionId::Id(event_id) => write!(f, "{event_id}"),
69            EventTransactionId::Suppressed => write!(f, "SUPPRESSED"),
70        }
71    }
72}
73
74impl FromStr for EventTransactionId {
75    type Err = <isize as FromStr>::Err;
76
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        if s == "SUPPRESSED" {
79            Ok(EventTransactionId::Suppressed)
80        } else {
81            let event_id = s.parse()?;
82            Ok(EventTransactionId::Id(event_id))
83        }
84    }
85}
86
87/// An event that occurred to one of the commits in the repository.
88#[derive(Clone, Debug, PartialEq)]
89pub enum Event {
90    /// Indicates that the commit was rewritten.
91    ///
92    /// Examples of rewriting include rebases and amended commits.
93    ///
94    /// We typically want to mark the new version of the commit as active and
95    /// the old version of the commit as obsolete.
96    RewriteEvent {
97        /// The timestamp of the event.
98        timestamp: f64,
99
100        /// The transaction ID of the event.
101        event_tx_id: EventTransactionId,
102
103        /// The OID of the commit before the rewrite.
104        old_commit_oid: MaybeZeroOid,
105
106        /// The OID of the commit after the rewrite.
107        new_commit_oid: MaybeZeroOid,
108    },
109
110    /// Indicates that a reference was updated.
111    ///
112    /// The most important reference we track is HEAD. In principle, we can also
113    /// track branch moves in this way, but Git doesn't support the appropriate
114    /// hook until v2.29 (`reference-transaction`).
115    RefUpdateEvent {
116        /// The timestamp of the event.
117        timestamp: f64,
118
119        /// The transaction ID of the event.
120        event_tx_id: EventTransactionId,
121
122        /// The full name of the reference that was updated.
123        ///
124        /// For example, `HEAD` or `refs/heads/master`.
125        ref_name: ReferenceName,
126
127        /// The old referent OID.
128        old_oid: MaybeZeroOid,
129
130        /// The updated referent OID.
131        new_oid: MaybeZeroOid,
132
133        /// A message associated with the rewrite, if any.
134        message: Option<ReferenceName>,
135    },
136
137    /// Indicate that the user made a commit.
138    ///
139    /// User commits should be marked as active.
140    CommitEvent {
141        /// The timestamp of the event.
142        timestamp: f64,
143
144        /// The transaction ID of the event.
145        event_tx_id: EventTransactionId,
146
147        /// The new commit OID.
148        commit_oid: NonZeroOid,
149    },
150
151    /// Indicates that a commit was explicitly obsoleted by the user.
152    ///
153    /// If the commit in question was not already active, then this has no
154    /// practical effect.
155    ObsoleteEvent {
156        /// The timestamp of the event.
157        timestamp: f64,
158
159        /// The transaction ID of the event.
160        event_tx_id: EventTransactionId,
161
162        /// The OID of the commit that was obsoleted.
163        commit_oid: NonZeroOid,
164    },
165
166    /// Indicates that a commit was explicitly un-obsoleted by the user.
167    ///
168    /// If the commit in question was not already obsolete, then this has no
169    /// practical effect.
170    UnobsoleteEvent {
171        /// The timestamp of the event.
172        timestamp: f64,
173
174        /// The transaction ID of the event.
175        event_tx_id: EventTransactionId,
176
177        /// The OID of the commit that was unobsoleted.
178        commit_oid: NonZeroOid,
179    },
180
181    /// Represents a snapshot of the working copy made at a certain time,
182    /// typically before a potentially-destructive operation.
183    WorkingCopySnapshot {
184        /// The timestamp of the event.
185        timestamp: f64,
186
187        /// The transaction ID of the event.
188        event_tx_id: EventTransactionId,
189
190        /// The OID of the current HEAD commit.
191        head_oid: MaybeZeroOid,
192
193        /// The OID of the commit containing metadata about the working copy
194        /// snapshot.
195        commit_oid: NonZeroOid,
196
197        /// The name of the checked-out branch, if any. This should be a full
198        /// reference name like `refs/heads/foo`.
199        ref_name: Option<ReferenceName>,
200    },
201}
202
203impl Event {
204    /// Get the timestamp associated with this event.
205    pub fn get_timestamp(&self) -> SystemTime {
206        let timestamp = match self {
207            Event::RewriteEvent { timestamp, .. } => timestamp,
208            Event::RefUpdateEvent { timestamp, .. } => timestamp,
209            Event::CommitEvent { timestamp, .. } => timestamp,
210            Event::ObsoleteEvent { timestamp, .. } => timestamp,
211            Event::UnobsoleteEvent { timestamp, .. } => timestamp,
212            Event::WorkingCopySnapshot { timestamp, .. } => timestamp,
213        };
214        SystemTime::UNIX_EPOCH + Duration::from_secs_f64(*timestamp)
215    }
216
217    /// Get the event transaction ID associated with this event.
218    pub fn get_event_tx_id(&self) -> EventTransactionId {
219        match self {
220            Event::RewriteEvent { event_tx_id, .. } => *event_tx_id,
221            Event::RefUpdateEvent { event_tx_id, .. } => *event_tx_id,
222            Event::CommitEvent { event_tx_id, .. } => *event_tx_id,
223            Event::ObsoleteEvent { event_tx_id, .. } => *event_tx_id,
224            Event::UnobsoleteEvent { event_tx_id, .. } => *event_tx_id,
225            Event::WorkingCopySnapshot { event_tx_id, .. } => *event_tx_id,
226        }
227    }
228}
229
230impl TryFrom<Event> for Row {
231    type Error = ();
232
233    fn try_from(event: Event) -> Result<Self, Self::Error> {
234        let row = match event {
235            Event::RewriteEvent {
236                event_tx_id: EventTransactionId::Suppressed,
237                ..
238            }
239            | Event::RefUpdateEvent {
240                event_tx_id: EventTransactionId::Suppressed,
241                ..
242            }
243            | Event::CommitEvent {
244                event_tx_id: EventTransactionId::Suppressed,
245                ..
246            }
247            | Event::ObsoleteEvent {
248                event_tx_id: EventTransactionId::Suppressed,
249                ..
250            }
251            | Event::UnobsoleteEvent {
252                event_tx_id: EventTransactionId::Suppressed,
253                ..
254            }
255            | Event::WorkingCopySnapshot {
256                event_tx_id: EventTransactionId::Suppressed,
257                ..
258            } => return Err(()),
259
260            Event::RewriteEvent {
261                timestamp,
262                event_tx_id: EventTransactionId::Id(event_tx_id),
263                old_commit_oid,
264                new_commit_oid,
265            } => Row {
266                timestamp,
267                event_tx_id,
268                type_: String::from("rewrite"),
269                ref1: Some(old_commit_oid.into()),
270                ref2: Some(new_commit_oid.into()),
271                ref_name: None,
272                message: None,
273            },
274
275            Event::RefUpdateEvent {
276                timestamp,
277                event_tx_id: EventTransactionId::Id(event_tx_id),
278                ref_name,
279                old_oid,
280                new_oid,
281                message,
282            } => Row {
283                timestamp,
284                event_tx_id,
285                type_: String::from("ref-move"),
286                ref1: Some(old_oid.into()),
287                ref2: Some(new_oid.into()),
288                ref_name: Some(ref_name),
289                message,
290            },
291
292            Event::CommitEvent {
293                timestamp,
294                event_tx_id: EventTransactionId::Id(event_tx_id),
295                commit_oid,
296            } => Row {
297                timestamp,
298                event_tx_id,
299                type_: String::from("commit"),
300                ref1: Some(commit_oid.into()),
301                ref2: None,
302                ref_name: None,
303                message: None,
304            },
305
306            Event::ObsoleteEvent {
307                timestamp,
308                event_tx_id: EventTransactionId::Id(event_tx_id),
309                commit_oid,
310            } => Row {
311                timestamp,
312                event_tx_id,
313                type_: String::from("hide"), // historical name
314                ref1: Some(commit_oid.into()),
315                ref2: None,
316                ref_name: None,
317                message: None,
318            },
319
320            Event::UnobsoleteEvent {
321                timestamp,
322                event_tx_id: EventTransactionId::Id(event_tx_id),
323                commit_oid,
324            } => Row {
325                timestamp,
326                event_tx_id,
327                type_: String::from("unhide"), // historical name
328                ref1: Some(commit_oid.into()),
329                ref2: None,
330                ref_name: None,
331                message: None,
332            },
333
334            Event::WorkingCopySnapshot {
335                timestamp,
336                event_tx_id: EventTransactionId::Id(event_tx_id),
337                head_oid,
338                commit_oid,
339                ref_name,
340            } => Row {
341                timestamp,
342                event_tx_id,
343                type_: String::from("snapshot"),
344                ref1: Some(head_oid.to_string().into()),
345                ref2: Some(commit_oid.into()),
346                ref_name,
347                message: None,
348            },
349        };
350        Ok(row)
351    }
352}
353
354fn try_from_row_helper(row: &Row) -> Result<Event, eyre::Error> {
355    let row: Row = row.clone();
356    let Row {
357        timestamp,
358        event_tx_id,
359        type_,
360        ref_name,
361        ref1,
362        ref2,
363        message,
364    } = row;
365    let event_tx_id = EventTransactionId::Id(event_tx_id);
366
367    let get_oid =
368        |reference_name: &Option<ReferenceName>, oid_name: &str| -> eyre::Result<MaybeZeroOid> {
369            match reference_name {
370                Some(reference_name) => {
371                    let oid: MaybeZeroOid = reference_name.as_str().parse()?;
372                    Ok(oid)
373                }
374                None => Err(eyre::eyre!(
375                    "OID '{}' was `None` for event type '{}'",
376                    oid_name,
377                    type_
378                )),
379            }
380        };
381
382    let event = match type_.as_str() {
383        "rewrite" => {
384            let old_commit_oid = get_oid(&ref1, "old commit OID")?;
385            let new_commit_oid = get_oid(&ref2, "new commit OID")?;
386            Event::RewriteEvent {
387                timestamp,
388                event_tx_id,
389                old_commit_oid,
390                new_commit_oid,
391            }
392        }
393
394        "ref-move" => {
395            let ref_name =
396                ref_name.ok_or_else(|| eyre::eyre!("ref-move event missing ref name"))?;
397            let ref1 = ref1.ok_or_else(|| eyre::eyre!("ref-move event missing ref1"))?;
398            let ref2 = ref2.ok_or_else(|| eyre::eyre!("ref-move event missing ref2"))?;
399            Event::RefUpdateEvent {
400                timestamp,
401                event_tx_id,
402                ref_name,
403                old_oid: ref1.as_str().parse()?,
404                new_oid: ref2.as_str().parse()?,
405                message,
406            }
407        }
408
409        "commit" => {
410            let commit_oid: NonZeroOid = get_oid(&ref1, "commit OID")?.try_into()?;
411            Event::CommitEvent {
412                timestamp,
413                event_tx_id,
414                commit_oid,
415            }
416        }
417
418        "hide" => {
419            let commit_oid: NonZeroOid = get_oid(&ref1, "commit OID")?.try_into()?;
420            Event::ObsoleteEvent {
421                timestamp,
422                event_tx_id,
423                commit_oid,
424            }
425        }
426
427        "unhide" => {
428            let commit_oid: NonZeroOid = get_oid(&ref1, "commit OID")?.try_into()?;
429            Event::UnobsoleteEvent {
430                timestamp,
431                event_tx_id,
432                commit_oid,
433            }
434        }
435
436        "snapshot" => {
437            let head_oid: MaybeZeroOid = get_oid(&ref1, "head OID")?;
438            let commit_oid: NonZeroOid = get_oid(&ref2, "commit OID")?.try_into()?;
439            Event::WorkingCopySnapshot {
440                timestamp,
441                event_tx_id,
442                head_oid,
443                commit_oid,
444                ref_name,
445            }
446        }
447
448        other => eyre::bail!("Unknown event type {}", other),
449    };
450    Ok(event)
451}
452
453impl TryFrom<Row> for Event {
454    type Error = eyre::Error;
455
456    fn try_from(row: Row) -> Result<Self, Self::Error> {
457        match try_from_row_helper(&row) {
458            Ok(event) => Ok(event),
459            Err(err) => {
460                error!(?row, "Could not convert row into event");
461                Err(err)
462            }
463        }
464    }
465}
466
467/// Stores `Event`s on disk.
468pub struct EventLogDb<'conn> {
469    conn: &'conn rusqlite::Connection,
470}
471
472impl std::fmt::Debug for EventLogDb<'_> {
473    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474        write!(f, "<EventLogDb path={:?}>", self.conn.path())
475    }
476}
477
478#[instrument]
479fn init_tables(conn: &rusqlite::Connection) -> eyre::Result<()> {
480    conn.execute(
481        "
482CREATE TABLE IF NOT EXISTS event_log (
483    timestamp REAL NOT NULL,
484    type TEXT NOT NULL,
485    event_tx_id INTEGER NOT NULL,
486    old_ref TEXT,
487    new_ref TEXT,
488    ref_name TEXT,
489    message TEXT
490)
491",
492        rusqlite::params![],
493    )
494    .wrap_err("Creating `event_log` table")?;
495
496    conn.execute(
497        "
498CREATE TABLE IF NOT EXISTS event_transactions (
499    timestamp REAL NOT NULL,
500
501    -- Set as `PRIMARY KEY` to have SQLite select a value automatically. Set as
502    -- `AUTOINCREMENT` to ensure that SQLite doesn't reuse the value later if a
503    -- row is deleted. (We don't plan to delete rows right now, but maybe
504    -- later?)
505    event_tx_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
506
507    message TEXT
508)
509",
510        rusqlite::params![],
511    )
512    .wrap_err("Creating `event_transactions` table")?;
513
514    Ok(())
515}
516
517impl<'conn> EventLogDb<'conn> {
518    /// Constructor.
519    #[instrument]
520    pub fn new(conn: &'conn rusqlite::Connection) -> eyre::Result<Self> {
521        init_tables(conn)?;
522        Ok(EventLogDb { conn })
523    }
524
525    /// Add events in the given order to the database, in a transaction.
526    ///
527    /// Args:
528    /// * events: The events to add.
529    #[instrument]
530    pub fn add_events(&self, events: Vec<Event>) -> eyre::Result<()> {
531        let tx = self.conn.unchecked_transaction()?;
532        for event in events {
533            let row = match Row::try_from(event) {
534                Ok(row) => row,
535                Err(()) => continue,
536            };
537            let Row {
538                timestamp,
539                type_,
540                event_tx_id,
541                ref1,
542                ref2,
543                ref_name,
544                message,
545            } = row;
546
547            let ref1 = ref1.as_ref().map(|x| x.as_str());
548            let ref2 = ref2.as_ref().map(|x| x.as_str());
549            let ref_name = ref_name.as_ref().map(|x| x.as_str());
550            let message = message.as_ref().map(|x| x.as_str());
551
552            tx.execute(
553                "
554INSERT INTO event_log VALUES (
555    :timestamp,
556    :type,
557    :event_tx_id,
558    :old_ref,
559    :new_ref,
560    :ref_name,
561    :message
562)
563            ",
564                rusqlite::named_params! {
565                    ":timestamp": timestamp,
566                    ":type": &type_,
567                    ":event_tx_id": event_tx_id,
568                    ":old_ref": &ref1,
569                    ":new_ref": &ref2,
570                    ":ref_name": &ref_name,
571                    ":message": &message,
572                },
573            )?;
574        }
575        tx.commit()?;
576        Ok(())
577    }
578
579    /// Get all the events in the database.
580    ///
581    /// Returns: All the events in the database, ordered from oldest to newest.
582    #[instrument]
583
584    pub fn get_events(&self) -> eyre::Result<Vec<Event>> {
585        let mut stmt = self.conn.prepare(
586            "
587SELECT timestamp, type, event_tx_id, old_ref, new_ref, ref_name, message
588FROM event_log
589ORDER BY rowid ASC
590",
591        )?;
592        let rows: rusqlite::Result<Vec<Row>> = stmt
593            .query_map(rusqlite::params![], |row| {
594                let timestamp: f64 = row.get("timestamp")?;
595                let event_tx_id: isize = row.get("event_tx_id")?;
596                let type_: String = row.get("type")?;
597                let ref_name: Option<String> = row.get("ref_name")?;
598                let old_ref: Option<String> = row.get("old_ref")?;
599                let new_ref: Option<String> = row.get("new_ref")?;
600                let message: Option<String> = row.get("message")?;
601
602                Ok(Row {
603                    timestamp,
604                    event_tx_id,
605                    type_,
606                    ref_name: ref_name.map(ReferenceName::from),
607                    ref1: old_ref.map(ReferenceName::from),
608                    ref2: new_ref.map(ReferenceName::from),
609                    message: message.map(ReferenceName::from),
610                })
611            })?
612            .collect();
613        let rows = rows?;
614        rows.into_iter().map(Event::try_from).collect()
615    }
616
617    #[instrument]
618    fn make_transaction_id_inner(
619        &self,
620        now: SystemTime,
621        message: &str,
622    ) -> eyre::Result<EventTransactionId> {
623        if let Ok(transaction_id) = std::env::var(BRANCHLESS_TRANSACTION_ID_ENV_VAR) {
624            if let Ok(transaction_id) = transaction_id.parse::<EventTransactionId>() {
625                return Ok(transaction_id);
626            }
627        }
628
629        let tx = self.conn.unchecked_transaction()?;
630
631        let timestamp = now
632            .duration_since(SystemTime::UNIX_EPOCH)
633            .wrap_err("Calculating event transaction timestamp")?
634            .as_secs_f64();
635        self.conn
636            .execute(
637                "
638            INSERT INTO event_transactions
639            (timestamp, message)
640            VALUES
641            (:timestamp, :message)
642        ",
643                rusqlite::named_params! {
644                    ":timestamp": timestamp,
645                    ":message": message,
646                },
647            )
648            .wrap_err("Creating event transaction")?;
649
650        // Ensure that we query `last_insert_rowid` in a transaction, in case
651        // there's another thread in this process making queries with the same
652        // SQLite connection.
653        let event_tx_id: isize = self.conn.last_insert_rowid().try_into()?;
654        tx.commit()?;
655        Ok(EventTransactionId::Id(event_tx_id))
656    }
657
658    /// Create a new event transaction ID to be used to insert subsequent
659    /// `Event`s into the database.
660    pub fn make_transaction_id(
661        &self,
662        now: SystemTime,
663        message: impl AsRef<str>,
664    ) -> eyre::Result<EventTransactionId> {
665        self.make_transaction_id_inner(now, message.as_ref())
666    }
667
668    /// Get the message associated with the given transaction.
669    pub fn get_transaction_message(&self, event_tx_id: EventTransactionId) -> eyre::Result<String> {
670        let event_tx_id = match event_tx_id {
671            EventTransactionId::Id(event_tx_id) => event_tx_id,
672            EventTransactionId::Suppressed => {
673                eyre::bail!("No message available for suppressed transaction ID")
674            }
675        };
676        let mut stmt = self.conn.prepare(
677            "
678SELECT message
679FROM event_transactions
680WHERE event_tx_id = :event_tx_id
681",
682        )?;
683        let result: String = stmt.query_row(
684            rusqlite::named_params![":event_tx_id": event_tx_id,],
685            |row| {
686                let message: String = row.get("message")?;
687                Ok(message)
688            },
689        )?;
690        Ok(result)
691    }
692}
693
694/// Determine whether a given reference is used to keep a commit alive.
695///
696/// Returns: Whether or not the given reference is used internally to keep the
697/// commit alive, so that it's not collected by Git's garbage collection
698/// mechanism.
699pub fn is_gc_ref(reference_name: &ReferenceName) -> bool {
700    reference_name.as_str().starts_with("refs/branchless/")
701}
702
703/// Determines whether or not updates to the given reference should be ignored.
704///
705/// Returns: Whether or not updates to the given reference should be ignored.
706pub fn should_ignore_ref_updates(reference_name: &ReferenceName) -> bool {
707    if is_gc_ref(reference_name) {
708        return true;
709    }
710
711    matches!(
712        reference_name.as_str(),
713        "ORIG_HEAD"
714            | "CHERRY_PICK"
715            | "REBASE_HEAD"
716            | "CHERRY_PICK_HEAD"
717            // From Git's `is_special_ref` in `refs.c`:
718            | "AUTO_MERGE"
719            | "FETCH_HEAD"
720    )
721}
722
723#[derive(Debug)]
724enum EventClassification {
725    Show,
726    Hide,
727}
728
729/// Whether or not a commit is considered active.
730///
731/// This is determined by the last `Event` that affected the commit. If no
732/// activity has been observed for a commit, it's considered inactive.
733#[derive(Debug)]
734pub enum CommitActivityStatus {
735    /// The commit is active, and should be rendered as part of the commit graph.
736    Active,
737
738    /// No history has been observed for this commit, so it's inactive, and
739    /// should not be rendered as part of the commit graph (unless a descendant
740    /// commit is visible).
741    Inactive,
742
743    /// The commit has been obsoleted by a user event (rewriting or an explicit
744    /// request to obsolete the commit), and should be hidden from the commit
745    /// graph (unless a descendant commit is visible).
746    Obsolete,
747}
748
749#[derive(Debug)]
750struct EventInfo {
751    id: isize,
752    event: Event,
753    event_classification: EventClassification,
754}
755
756/// Events up to this cursor (exclusive) are available to the caller.
757///
758/// The "event cursor" is used to move the event replayer forward or
759/// backward in time, so as to show the state of the repository at that
760/// time.
761///
762/// The cursor is a position in between two events in the event log.
763/// Thus, all events before to the cursor are considered to be in effect,
764/// and all events after the cursor are considered to not have happened
765/// yet.
766#[derive(Clone, Copy, Debug, PartialEq, Eq)]
767pub struct EventCursor {
768    event_id: isize,
769}
770
771/// Processes events in order and determine the repo's visible commits.
772pub struct EventReplayer {
773    /// Events are numbered starting from zero.
774    id_counter: isize,
775
776    /// The list of observed events.
777    events: Vec<Event>,
778
779    /// The name of the reference representing the main branch.
780    main_branch_reference_name: ReferenceName,
781
782    /// The events that have affected each commit.
783    commit_history: HashMap<NonZeroOid, Vec<EventInfo>>,
784
785    /// Map from ref names to ref locations (an OID or another ref name). Works
786    /// around <https://github.com/arxanas/git-branchless/issues/7>.
787    ///
788    /// If an entry is not present, it was either never observed, or it most
789    /// recently changed to point to the zero hash (i.e. it was deleted).
790    ref_locations: HashMap<ReferenceName, NonZeroOid>,
791}
792
793impl std::fmt::Debug for EventReplayer {
794    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
795        write!(
796            f,
797            "<EventReplayer events.len={:?} ref_locations.len={:?}>",
798            self.events.len(),
799            self.ref_locations.len()
800        )
801    }
802}
803
804impl EventReplayer {
805    fn new(main_branch_reference_name: ReferenceName) -> Self {
806        EventReplayer {
807            id_counter: 0,
808            events: vec![],
809            main_branch_reference_name,
810            commit_history: HashMap::new(),
811            ref_locations: HashMap::new(),
812        }
813    }
814
815    /// Construct the replayer from all the events in the database.
816    ///
817    /// Args:
818    /// * `event_log_db`: The database to query events from.
819    ///
820    /// Returns: The constructed replayer.
821    #[instrument]
822    pub fn from_event_log_db(
823        effects: &Effects,
824        repo: &Repo,
825        event_log_db: &EventLogDb,
826    ) -> eyre::Result<Self> {
827        let (_effects, _progress) = effects.start_operation(OperationType::ProcessEvents);
828
829        let main_branch_reference_name = repo.get_main_branch()?.get_reference_name()?;
830        let mut result = EventReplayer::new(main_branch_reference_name);
831        for event in event_log_db.get_events()? {
832            result.process_event(&event);
833        }
834        Ok(result)
835    }
836
837    /// Process the given event.
838    ///
839    /// This also sets the event cursor to point to immediately after the event
840    /// that was just processed.
841    ///
842    /// Args:
843    /// * `event`: The next event to process. Events should be passed to the
844    /// * `replayer` in order from oldest to newest.
845    pub fn process_event(&mut self, event: &Event) {
846        // Drop non-meaningful ref-update events.
847        if let Event::RefUpdateEvent { ref_name, .. } = event {
848            if should_ignore_ref_updates(ref_name) {
849                return;
850            }
851        }
852
853        let event = match self.fix_event_git_v2_31(event.clone()) {
854            None => {
855                return;
856            }
857            Some(event) => {
858                self.events.push(event);
859                self.events.last().unwrap()
860            }
861        };
862        let id = self.id_counter;
863        self.id_counter += 1;
864
865        match &event {
866            Event::RewriteEvent {
867                timestamp: _,
868                event_tx_id: _,
869                old_commit_oid,
870                new_commit_oid,
871            } => {
872                if let MaybeZeroOid::NonZero(old_commit_oid) = old_commit_oid {
873                    self.commit_history
874                        .entry(*old_commit_oid)
875                        .or_default()
876                        .push(EventInfo {
877                            id,
878                            event: event.clone(),
879                            event_classification: EventClassification::Hide,
880                        });
881                }
882                if let MaybeZeroOid::NonZero(new_commit_oid) = new_commit_oid {
883                    self.commit_history
884                        .entry(*new_commit_oid)
885                        .or_default()
886                        .push(EventInfo {
887                            id,
888                            event: event.clone(),
889                            event_classification: EventClassification::Show,
890                        });
891                }
892            }
893
894            // A reference update doesn't indicate a change to a commit, so we
895            // don't include it in the `commit_history`. We'll traverse the
896            // history later to find historical locations of references when
897            // needed.
898            Event::RefUpdateEvent {
899                ref_name, new_oid, ..
900            } => match new_oid {
901                MaybeZeroOid::NonZero(new_oid) => {
902                    self.ref_locations.insert(ref_name.clone(), *new_oid);
903                }
904                MaybeZeroOid::Zero => {
905                    self.ref_locations.remove(ref_name);
906                }
907            },
908
909            Event::CommitEvent {
910                timestamp: _,
911                event_tx_id: _,
912                commit_oid,
913            } => self
914                .commit_history
915                .entry(*commit_oid)
916                .or_default()
917                .push(EventInfo {
918                    id,
919                    event: event.clone(),
920                    event_classification: EventClassification::Show,
921                }),
922
923            Event::ObsoleteEvent {
924                timestamp: _,
925                event_tx_id: _,
926                commit_oid,
927            } => self
928                .commit_history
929                .entry(*commit_oid)
930                .or_default()
931                .push(EventInfo {
932                    id,
933                    event: event.clone(),
934                    event_classification: EventClassification::Hide,
935                }),
936
937            Event::UnobsoleteEvent {
938                timestamp: _,
939                event_tx_id: _,
940                commit_oid,
941            } => self
942                .commit_history
943                .entry(*commit_oid)
944                .or_default()
945                .push(EventInfo {
946                    id,
947                    event: event.clone(),
948                    event_classification: EventClassification::Show,
949                }),
950
951            Event::WorkingCopySnapshot { .. } => {
952                // Do nothing. A working copy snapshot doesn't imply that the
953                // commit has become active or inactive.
954            }
955        };
956    }
957
958    /// See https://github.com/arxanas/git-branchless/issues/7.
959    fn fix_event_git_v2_31(&self, event: Event) -> Option<Event> {
960        let event = match event {
961            // Git v2.31 will sometimes fail to set the `old_ref` field when
962            // deleting refs. This means that undoing the operation later
963            // becomes incorrect, as we just swap the `old_ref` and `new_ref`
964            // values.
965            Event::RefUpdateEvent {
966                timestamp,
967                event_tx_id,
968                ref_name,
969                old_oid: MaybeZeroOid::Zero,
970                new_oid: MaybeZeroOid::Zero,
971                message,
972            } => {
973                let old_oid: MaybeZeroOid = self.ref_locations.get(&ref_name).copied().into();
974                Event::RefUpdateEvent {
975                    timestamp,
976                    event_tx_id,
977                    ref_name,
978                    old_oid,
979                    new_oid: MaybeZeroOid::Zero,
980                    message,
981                }
982            }
983
984            _ => event,
985        };
986
987        match (event, self.events.last()) {
988            // Sometimes, Git v2.31 will issue multiple delete reference
989            // transactions (one for the unpacked refs, and one for the packed
990            // refs). Ignore the duplicate second one, for determinism in
991            // testing. See https://lore.kernel.org/git/YFMCLSdImkW3B1rM@ncase/
992            // for more details.
993            (
994                Event::RefUpdateEvent {
995                    timestamp: _,
996                    event_tx_id: _,
997                    ref ref_name,
998                    old_oid: _,
999                    new_oid: MaybeZeroOid::Zero,
1000                    ref message,
1001                },
1002                Some(Event::RefUpdateEvent {
1003                    timestamp: _,
1004                    event_tx_id: _,
1005                    ref_name: last_ref_name,
1006                    old_oid: _,
1007                    new_oid: MaybeZeroOid::Zero,
1008                    message: last_message,
1009                }),
1010            ) if ref_name == last_ref_name && message == last_message => None,
1011
1012            (event, _) => Some(event),
1013        }
1014    }
1015
1016    fn get_cursor_commit_history(&self, cursor: EventCursor, oid: NonZeroOid) -> Vec<&EventInfo> {
1017        match self.commit_history.get(&oid) {
1018            None => vec![],
1019            Some(history) => history
1020                .iter()
1021                .filter(|event_info| event_info.id < cursor.event_id)
1022                .collect(),
1023        }
1024    }
1025
1026    /// Determines whether a commit is considered "active" at the cursor's point
1027    /// in time.
1028    pub fn get_cursor_commit_activity_status(
1029        &self,
1030        cursor: EventCursor,
1031        oid: NonZeroOid,
1032    ) -> CommitActivityStatus {
1033        let history = self.get_cursor_commit_history(cursor, oid);
1034        match history.last() {
1035            Some(EventInfo {
1036                id: _,
1037                event: _,
1038                event_classification: EventClassification::Show,
1039            }) => CommitActivityStatus::Active,
1040
1041            Some(EventInfo {
1042                id: _,
1043                event: _,
1044                event_classification: EventClassification::Hide,
1045            }) => CommitActivityStatus::Obsolete,
1046
1047            None => CommitActivityStatus::Inactive,
1048        }
1049    }
1050
1051    /// Get the latest event affecting a given commit, as of the cursor's point
1052    /// in time.
1053    ///
1054    /// Args:
1055    /// * `oid`: The OID of the commit to check.
1056    ///
1057    /// Returns: The most recent event that affected that commit. If this commit
1058    /// was not observed by the replayer, returns `None`.
1059    pub fn get_cursor_commit_latest_event(
1060        &self,
1061        cursor: EventCursor,
1062        oid: NonZeroOid,
1063    ) -> Option<&Event> {
1064        let history = self.get_cursor_commit_history(cursor, oid);
1065        let event_info = *history.last()?;
1066        Some(&event_info.event)
1067    }
1068
1069    /// Get all OIDs which have been observed so far. This should be the set of
1070    /// non-inactive commits.
1071    pub fn get_cursor_oids(&self, cursor: EventCursor) -> HashSet<NonZeroOid> {
1072        self.commit_history
1073            .iter()
1074            .filter_map(|(oid, history)| {
1075                if history.iter().any(|event| event.id < cursor.event_id) {
1076                    Some(*oid)
1077                } else {
1078                    None
1079                }
1080            })
1081            .collect()
1082    }
1083
1084    /// Create an event cursor pointing to immediately after the last event.
1085    pub fn make_default_cursor(&self) -> EventCursor {
1086        self.make_cursor(self.events.len().try_into().unwrap())
1087    }
1088
1089    /// Create an event cursor pointing to immediately after the provided event ID.
1090    ///
1091    /// If the event ID is too low or too high, it will be clamped to the valid
1092    /// range for event IDs.
1093    pub fn make_cursor(&self, event_id: isize) -> EventCursor {
1094        let event_id = if event_id < 0 { 0 } else { event_id };
1095        let num_events: isize = self.events.len().try_into().unwrap();
1096        let event_id = if event_id > num_events {
1097            num_events
1098        } else {
1099            event_id
1100        };
1101        EventCursor { event_id }
1102    }
1103
1104    /// Advance the event cursor by the specified number of events.
1105    ///
1106    /// Args:
1107    /// * `num_events`: The number of events to advance by. Can be positive,
1108    ///   zero, or negative. If out of bounds, the cursor is set to the first or
1109    ///   last valid position, as appropriate.
1110    pub fn advance_cursor(&self, cursor: EventCursor, num_events: isize) -> EventCursor {
1111        self.make_cursor(cursor.event_id + num_events)
1112    }
1113
1114    fn get_event_tx_id_before_cursor(&self, cursor: EventCursor) -> Option<EventTransactionId> {
1115        self.get_event_before_cursor(cursor)
1116            .map(|(_event_id, event)| event.get_event_tx_id())
1117    }
1118
1119    /// The event cursor may not be between two events with different transaction
1120    /// IDs (that is, it may not be perfectly in between transactions). Move the
1121    /// cursor forward until it is at the boundary of two transactions
1122    fn snap_to_transaction_boundary(&self, cursor: EventCursor) -> EventCursor {
1123        let next_cursor = self.advance_cursor(cursor, 1);
1124        if cursor == next_cursor {
1125            return cursor;
1126        }
1127        let current_tx_id = self.get_event_tx_id_before_cursor(cursor);
1128        let next_tx_id = self.get_event_tx_id_before_cursor(next_cursor);
1129        if current_tx_id == next_tx_id {
1130            self.snap_to_transaction_boundary(next_cursor)
1131        } else {
1132            cursor
1133        }
1134    }
1135
1136    fn advance_cursor_by_transaction_helper(
1137        &self,
1138        cursor: EventCursor,
1139        num_transactions: isize,
1140    ) -> EventCursor {
1141        match num_transactions.cmp(&0) {
1142            Ordering::Equal => self.snap_to_transaction_boundary(cursor),
1143            Ordering::Greater => {
1144                let next_cursor = self.advance_cursor(cursor, 1);
1145                if cursor == next_cursor {
1146                    return next_cursor;
1147                }
1148                let current_tx_id = self.get_event_tx_id_before_cursor(cursor);
1149                let next_tx_id = self.get_event_tx_id_before_cursor(next_cursor);
1150                let num_transactions = if current_tx_id == next_tx_id {
1151                    num_transactions
1152                } else {
1153                    num_transactions - 1
1154                };
1155                self.advance_cursor_by_transaction_helper(next_cursor, num_transactions)
1156            }
1157            Ordering::Less => {
1158                let prev_cursor = self.advance_cursor(cursor, -1);
1159                if cursor == prev_cursor {
1160                    return prev_cursor;
1161                }
1162                let current_tx_id = self.get_event_tx_id_before_cursor(cursor);
1163                let prev_tx_id = self.get_event_tx_id_before_cursor(prev_cursor);
1164                let num_transactions = if current_tx_id == prev_tx_id {
1165                    num_transactions
1166                } else {
1167                    num_transactions + 1
1168                };
1169                self.advance_cursor_by_transaction_helper(prev_cursor, num_transactions)
1170            }
1171        }
1172    }
1173
1174    /// Advance the cursor to the transaction which is `num_transactions` after
1175    /// the current cursor. `num_transactions` can be negative.
1176    ///
1177    /// The returned cursor will point to the position immediately after the last
1178    /// event in the subsequent transaction.
1179    pub fn advance_cursor_by_transaction(
1180        &self,
1181        cursor: EventCursor,
1182        num_transactions: isize,
1183    ) -> EventCursor {
1184        if self.events.is_empty() {
1185            cursor
1186        } else {
1187            let cursor = self.snap_to_transaction_boundary(cursor);
1188            self.advance_cursor_by_transaction_helper(cursor, num_transactions)
1189        }
1190    }
1191
1192    /// Get the OID of `HEAD` at the cursor's point in time.
1193    ///
1194    /// Returns: The OID pointed to by `HEAD` at that time, or `None` if `HEAD`
1195    /// was never observed.
1196    fn get_cursor_head_oid(&self, cursor: EventCursor) -> Option<NonZeroOid> {
1197        let cursor_event_id: usize = cursor.event_id.try_into().unwrap();
1198        self.events[0..cursor_event_id]
1199            .iter()
1200            .rev()
1201            .find_map(|event| {
1202                match &event {
1203                    Event::RefUpdateEvent {
1204                        ref_name,
1205                        new_oid: MaybeZeroOid::NonZero(new_oid),
1206                        ..
1207                    } if ref_name.as_str() == "HEAD" => Some(*new_oid),
1208                    Event::RefUpdateEvent { .. } => None,
1209
1210                    // Not strictly necessary, but helps to compensate in case
1211                    // the user is not running Git v2.29 or above, and therefore
1212                    // doesn't have the corresponding `RefUpdateEvent`.
1213                    Event::CommitEvent { commit_oid, .. } => Some(*commit_oid),
1214
1215                    Event::WorkingCopySnapshot {
1216                        head_oid: MaybeZeroOid::NonZero(head_oid),
1217                        ..
1218                    } => Some(*head_oid),
1219                    Event::WorkingCopySnapshot {
1220                        head_oid: MaybeZeroOid::Zero,
1221                        ..
1222                    } => None,
1223
1224                    Event::RewriteEvent { .. }
1225                    | Event::ObsoleteEvent { .. }
1226                    | Event::UnobsoleteEvent { .. } => None,
1227                }
1228            })
1229    }
1230
1231    fn get_cursor_branch_oid(
1232        &self,
1233        cursor: EventCursor,
1234        reference_name: &ReferenceName,
1235    ) -> eyre::Result<Option<NonZeroOid>> {
1236        let cursor_event_id: usize = cursor.event_id.try_into().unwrap();
1237        let oid = self.events[0..cursor_event_id]
1238            .iter()
1239            .rev()
1240            .find_map(|event| match &event {
1241                Event::RefUpdateEvent {
1242                    ref_name,
1243                    new_oid: MaybeZeroOid::NonZero(new_oid),
1244                    ..
1245                } if ref_name == reference_name => Some(*new_oid),
1246                _ => None,
1247            });
1248        Ok(oid)
1249    }
1250
1251    /// Get the OID of the main branch at the cursor's point in time.
1252    ///
1253    /// Note that this doesn't handle the case of the user having changed their
1254    /// main branch configuration. That is, if it was previously `master`, and
1255    /// then changed to `main`, we will show only the historical locations of
1256    /// `main`, and never `master`.
1257    ///
1258    /// Args:
1259    /// * `repo`: The Git repository.
1260    ///
1261    /// Returns: A mapping from an OID to the names of branches pointing to that
1262    /// OID.
1263    #[instrument]
1264    fn get_cursor_main_branch_oid(
1265        &self,
1266        cursor: EventCursor,
1267        repo: &Repo,
1268    ) -> eyre::Result<NonZeroOid> {
1269        let main_branch_reference_name = repo.get_main_branch()?.get_reference_name()?;
1270        let main_branch_oid = self.get_cursor_branch_oid(cursor, &main_branch_reference_name)?;
1271        match main_branch_oid {
1272            Some(main_branch_oid) => Ok(main_branch_oid),
1273            None => {
1274                // Assume the main branch just hasn't been observed moving yet,
1275                // so its value at the current time is fine to use.
1276                repo.get_main_branch_oid()
1277            }
1278        }
1279    }
1280
1281    /// Get the mapping of branch OIDs to names at the cursor's point in
1282    /// time.
1283    ///
1284    /// Same as `get_branch_oid_to_names`, but for a previous point in time.
1285    ///
1286    /// Args:
1287    /// * `repo`: The Git repository.
1288    ///
1289    /// Returns: A mapping from an OID to the names of branches pointing to that
1290    /// OID.
1291    fn get_cursor_branch_oid_to_names(
1292        &self,
1293        cursor: EventCursor,
1294        repo: &Repo,
1295    ) -> eyre::Result<HashMap<NonZeroOid, HashSet<ReferenceName>>> {
1296        let mut ref_name_to_oid: HashMap<&ReferenceName, NonZeroOid> = HashMap::new();
1297        let cursor_event_id: usize = cursor.event_id.try_into().unwrap();
1298        for event in self.events[..cursor_event_id].iter() {
1299            match event {
1300                Event::RefUpdateEvent {
1301                    new_oid: MaybeZeroOid::NonZero(new_oid),
1302                    ref_name,
1303                    ..
1304                } => {
1305                    ref_name_to_oid.insert(ref_name, *new_oid);
1306                }
1307                Event::RefUpdateEvent {
1308                    new_oid: MaybeZeroOid::Zero,
1309                    ref_name,
1310                    ..
1311                } => {
1312                    ref_name_to_oid.remove(ref_name);
1313                }
1314                _ => {}
1315            }
1316        }
1317
1318        let mut result: HashMap<NonZeroOid, HashSet<ReferenceName>> = HashMap::new();
1319        for (ref_name, ref_oid) in ref_name_to_oid.iter() {
1320            if let CategorizedReferenceName::LocalBranch { .. } =
1321                CategorizedReferenceName::new(ref_name)
1322            {
1323                result
1324                    .entry(*ref_oid)
1325                    .or_default()
1326                    .insert((*ref_name).clone());
1327            }
1328        }
1329
1330        let main_branch_oid = self.get_cursor_main_branch_oid(cursor, repo)?;
1331        result
1332            .entry(main_branch_oid)
1333            .or_default()
1334            .insert(self.main_branch_reference_name.clone());
1335        Ok(result)
1336    }
1337
1338    /// Get the `RepoReferencesSnapshot` at the cursor's point in time.
1339    pub fn get_references_snapshot(
1340        &self,
1341        repo: &Repo,
1342        cursor: EventCursor,
1343    ) -> eyre::Result<RepoReferencesSnapshot> {
1344        let head_oid = self.get_cursor_head_oid(cursor);
1345        let main_branch_oid = self.get_cursor_main_branch_oid(cursor, repo)?;
1346        let branch_oid_to_names = self.get_cursor_branch_oid_to_names(cursor, repo)?;
1347        Ok(RepoReferencesSnapshot {
1348            head_oid,
1349            main_branch_oid,
1350            branch_oid_to_names,
1351        })
1352    }
1353
1354    /// Get the event immediately before the cursor.
1355    ///
1356    /// Returns: A tuple of event ID and the event that most recently happened.
1357    /// If no event was before the event cursor, returns `None` instead.
1358    pub fn get_event_before_cursor(&self, cursor: EventCursor) -> Option<(isize, &Event)> {
1359        if cursor.event_id == 0 {
1360            None
1361        } else {
1362            let previous_cursor_event_id: usize = (cursor.event_id - 1).try_into().unwrap();
1363            Some((cursor.event_id, &self.events[previous_cursor_event_id]))
1364        }
1365    }
1366
1367    /// Get all the events in the transaction immediately before the cursor.
1368    ///
1369    /// Returns: A tuple of event ID and the events that happened in the most
1370    /// recent transaction. The event ID corresponds to the ID of the first event
1371    /// in the returned list of events. If there were no events before the event
1372    /// cursor (and therefore no transactions), returns `None` instead.
1373    pub fn get_tx_events_before_cursor(&self, cursor: EventCursor) -> Option<(isize, &[Event])> {
1374        let prev_tx_cursor = self.advance_cursor_by_transaction(cursor, -1);
1375        let EventCursor {
1376            event_id: prev_event_id,
1377        } = prev_tx_cursor;
1378        let EventCursor {
1379            event_id: curr_event_id,
1380        } = cursor;
1381        let tx_events =
1382            &self.events[prev_event_id.try_into().unwrap()..curr_event_id.try_into().unwrap()];
1383        match tx_events {
1384            [] => None,
1385            events => Some((prev_event_id + 1, events)),
1386        }
1387    }
1388
1389    /// Get all the events that have happened since the event cursor.
1390    ///
1391    /// Returns: An ordered list of events that have happened since the event
1392    /// cursor, from least recent to most recent.
1393    pub fn get_events_since_cursor(&self, cursor: EventCursor) -> &[Event] {
1394        let cursor_event_id: usize = cursor.event_id.try_into().unwrap();
1395        &self.events[cursor_event_id..]
1396    }
1397}
1398
1399/// Testing helpers.
1400pub mod testing {
1401    use super::*;
1402
1403    /// Create a new `EventReplayer`, for testing.
1404    pub fn new_event_replayer(main_branch_reference_name: ReferenceName) -> EventReplayer {
1405        EventReplayer::new(main_branch_reference_name)
1406    }
1407
1408    /// Create a new transaction ID, for testing.
1409    pub fn new_event_transaction_id(id: isize) -> EventTransactionId {
1410        EventTransactionId::Id(id)
1411    }
1412
1413    /// Create a new event cursor, for testing.
1414    pub fn new_event_cursor(event_id: isize) -> EventCursor {
1415        EventCursor { event_id }
1416    }
1417
1418    /// Remove the timestamp for the event, for determinism in testing.
1419    pub fn redact_event_timestamp(mut event: Event) -> Event {
1420        match event {
1421            Event::RewriteEvent {
1422                ref mut timestamp, ..
1423            }
1424            | Event::RefUpdateEvent {
1425                ref mut timestamp, ..
1426            }
1427            | Event::CommitEvent {
1428                ref mut timestamp, ..
1429            }
1430            | Event::ObsoleteEvent {
1431                ref mut timestamp, ..
1432            }
1433            | Event::UnobsoleteEvent {
1434                ref mut timestamp, ..
1435            }
1436            | Event::WorkingCopySnapshot {
1437                ref mut timestamp, ..
1438            } => *timestamp = 0.0,
1439        }
1440        event
1441    }
1442
1443    /// Get the events stored inside an `EventReplayer`.
1444    pub fn get_event_replayer_events(event_replayer: &EventReplayer) -> &Vec<Event> {
1445        &event_replayer.events
1446    }
1447}