use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::convert::{TryFrom, TryInto};
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use eyre::Context;
use tracing::{error, instrument};
use crate::core::effects::{Effects, OperationType};
use crate::core::repo_ext::RepoExt;
use crate::git::{CategorizedReferenceName, MaybeZeroOid, NonZeroOid, ReferenceName, Repo};
use super::repo_ext::RepoReferencesSnapshot;
pub const BRANCHLESS_TRANSACTION_ID_ENV_VAR: &str = "BRANCHLESS_TRANSACTION_ID";
#[derive(Clone, Debug)]
struct Row {
timestamp: f64,
type_: String,
event_tx_id: isize,
ref1: Option<ReferenceName>,
ref2: Option<ReferenceName>,
ref_name: Option<ReferenceName>,
message: Option<ReferenceName>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EventTransactionId(isize);
impl ToString for EventTransactionId {
fn to_string(&self) -> String {
let EventTransactionId(event_id) = self;
event_id.to_string()
}
}
impl FromStr for EventTransactionId {
type Err = <isize as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let event_id = s.parse()?;
Ok(EventTransactionId(event_id))
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Event {
RewriteEvent {
timestamp: f64,
event_tx_id: EventTransactionId,
old_commit_oid: MaybeZeroOid,
new_commit_oid: MaybeZeroOid,
},
RefUpdateEvent {
timestamp: f64,
event_tx_id: EventTransactionId,
ref_name: ReferenceName,
old_oid: MaybeZeroOid,
new_oid: MaybeZeroOid,
message: Option<ReferenceName>,
},
CommitEvent {
timestamp: f64,
event_tx_id: EventTransactionId,
commit_oid: NonZeroOid,
},
ObsoleteEvent {
timestamp: f64,
event_tx_id: EventTransactionId,
commit_oid: NonZeroOid,
},
UnobsoleteEvent {
timestamp: f64,
event_tx_id: EventTransactionId,
commit_oid: NonZeroOid,
},
WorkingCopySnapshot {
timestamp: f64,
event_tx_id: EventTransactionId,
head_oid: MaybeZeroOid,
commit_oid: NonZeroOid,
ref_name: Option<ReferenceName>,
},
}
impl Event {
pub fn get_timestamp(&self) -> SystemTime {
let timestamp = match self {
Event::RewriteEvent { timestamp, .. } => timestamp,
Event::RefUpdateEvent { timestamp, .. } => timestamp,
Event::CommitEvent { timestamp, .. } => timestamp,
Event::ObsoleteEvent { timestamp, .. } => timestamp,
Event::UnobsoleteEvent { timestamp, .. } => timestamp,
Event::WorkingCopySnapshot { timestamp, .. } => timestamp,
};
SystemTime::UNIX_EPOCH + Duration::from_secs_f64(*timestamp)
}
pub fn get_event_tx_id(&self) -> EventTransactionId {
match self {
Event::RewriteEvent { event_tx_id, .. } => *event_tx_id,
Event::RefUpdateEvent { event_tx_id, .. } => *event_tx_id,
Event::CommitEvent { event_tx_id, .. } => *event_tx_id,
Event::ObsoleteEvent { event_tx_id, .. } => *event_tx_id,
Event::UnobsoleteEvent { event_tx_id, .. } => *event_tx_id,
Event::WorkingCopySnapshot { event_tx_id, .. } => *event_tx_id,
}
}
}
impl From<Event> for Row {
fn from(event: Event) -> Row {
match event {
Event::RewriteEvent {
timestamp,
event_tx_id: EventTransactionId(event_tx_id),
old_commit_oid,
new_commit_oid,
} => Row {
timestamp,
event_tx_id,
type_: String::from("rewrite"),
ref1: Some(old_commit_oid.into()),
ref2: Some(new_commit_oid.into()),
ref_name: None,
message: None,
},
Event::RefUpdateEvent {
timestamp,
event_tx_id: EventTransactionId(event_tx_id),
ref_name,
old_oid,
new_oid,
message,
} => Row {
timestamp,
event_tx_id,
type_: String::from("ref-move"),
ref1: Some(old_oid.into()),
ref2: Some(new_oid.into()),
ref_name: Some(ref_name),
message,
},
Event::CommitEvent {
timestamp,
event_tx_id: EventTransactionId(event_tx_id),
commit_oid,
} => Row {
timestamp,
event_tx_id,
type_: String::from("commit"),
ref1: Some(commit_oid.into()),
ref2: None,
ref_name: None,
message: None,
},
Event::ObsoleteEvent {
timestamp,
event_tx_id: EventTransactionId(event_tx_id),
commit_oid,
} => Row {
timestamp,
event_tx_id,
type_: String::from("hide"), ref1: Some(commit_oid.into()),
ref2: None,
ref_name: None,
message: None,
},
Event::UnobsoleteEvent {
timestamp,
event_tx_id: EventTransactionId(event_tx_id),
commit_oid,
} => Row {
timestamp,
event_tx_id,
type_: String::from("unhide"), ref1: Some(commit_oid.into()),
ref2: None,
ref_name: None,
message: None,
},
Event::WorkingCopySnapshot {
timestamp,
event_tx_id: EventTransactionId(event_tx_id),
head_oid,
commit_oid,
ref_name,
} => Row {
timestamp,
event_tx_id,
type_: String::from("snapshot"),
ref1: Some(head_oid.to_string().into()),
ref2: Some(commit_oid.into()),
ref_name,
message: None,
},
}
}
}
fn try_from_row_helper(row: &Row) -> Result<Event, eyre::Error> {
let row: Row = row.clone();
let Row {
timestamp,
event_tx_id,
type_,
ref_name,
ref1,
ref2,
message,
} = row;
let event_tx_id = EventTransactionId(event_tx_id);
let get_oid =
|reference_name: &Option<ReferenceName>, oid_name: &str| -> eyre::Result<MaybeZeroOid> {
match reference_name {
Some(reference_name) => {
let oid: MaybeZeroOid = reference_name.as_str().parse()?;
Ok(oid)
}
None => Err(eyre::eyre!(
"OID '{}' was `None` for event type '{}'",
oid_name,
type_
)),
}
};
let event = match type_.as_str() {
"rewrite" => {
let old_commit_oid = get_oid(&ref1, "old commit OID")?;
let new_commit_oid = get_oid(&ref2, "new commit OID")?;
Event::RewriteEvent {
timestamp,
event_tx_id,
old_commit_oid,
new_commit_oid,
}
}
"ref-move" => {
let ref_name =
ref_name.ok_or_else(|| eyre::eyre!("ref-move event missing ref name"))?;
let ref1 = ref1.ok_or_else(|| eyre::eyre!("ref-move event missing ref1"))?;
let ref2 = ref2.ok_or_else(|| eyre::eyre!("ref-move event missing ref2"))?;
Event::RefUpdateEvent {
timestamp,
event_tx_id,
ref_name,
old_oid: ref1.as_str().parse()?,
new_oid: ref2.as_str().parse()?,
message,
}
}
"commit" => {
let commit_oid: NonZeroOid = get_oid(&ref1, "commit OID")?.try_into()?;
Event::CommitEvent {
timestamp,
event_tx_id,
commit_oid,
}
}
"hide" => {
let commit_oid: NonZeroOid = get_oid(&ref1, "commit OID")?.try_into()?;
Event::ObsoleteEvent {
timestamp,
event_tx_id,
commit_oid,
}
}
"unhide" => {
let commit_oid: NonZeroOid = get_oid(&ref1, "commit OID")?.try_into()?;
Event::UnobsoleteEvent {
timestamp,
event_tx_id,
commit_oid,
}
}
"snapshot" => {
let head_oid: MaybeZeroOid = get_oid(&ref1, "head OID")?;
let commit_oid: NonZeroOid = get_oid(&ref2, "commit OID")?.try_into()?;
Event::WorkingCopySnapshot {
timestamp,
event_tx_id,
head_oid,
commit_oid,
ref_name,
}
}
other => eyre::bail!("Unknown event type {}", other),
};
Ok(event)
}
impl TryFrom<Row> for Event {
type Error = eyre::Error;
fn try_from(row: Row) -> Result<Self, Self::Error> {
match try_from_row_helper(&row) {
Ok(event) => Ok(event),
Err(err) => {
error!(?row, "Could not convert row into event");
Err(err)
}
}
}
}
pub struct EventLogDb<'conn> {
conn: &'conn rusqlite::Connection,
}
impl std::fmt::Debug for EventLogDb<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<EventLogDb>")
}
}
#[instrument]
fn init_tables(conn: &rusqlite::Connection) -> eyre::Result<()> {
conn.execute(
"
CREATE TABLE IF NOT EXISTS event_log (
timestamp REAL NOT NULL,
type TEXT NOT NULL,
event_tx_id INTEGER NOT NULL,
old_ref TEXT,
new_ref TEXT,
ref_name TEXT,
message TEXT
)
",
rusqlite::params![],
)
.wrap_err("Creating `event_log` table")?;
conn.execute(
"
CREATE TABLE IF NOT EXISTS event_transactions (
timestamp REAL NOT NULL,
-- Set as `PRIMARY KEY` to have SQLite select a value automatically. Set as
-- `AUTOINCREMENT` to ensure that SQLite doesn't reuse the value later if a
-- row is deleted. (We don't plan to delete rows right now, but maybe
-- later?)
event_tx_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
message TEXT
)
",
rusqlite::params![],
)
.wrap_err("Creating `event_transactions` table")?;
Ok(())
}
impl<'conn> EventLogDb<'conn> {
#[instrument]
pub fn new(conn: &'conn rusqlite::Connection) -> eyre::Result<Self> {
init_tables(conn)?;
Ok(EventLogDb { conn })
}
#[instrument]
pub fn add_events(&self, events: Vec<Event>) -> eyre::Result<()> {
let tx = self.conn.unchecked_transaction()?;
for event in events {
let Row {
timestamp,
type_,
event_tx_id,
ref1,
ref2,
ref_name,
message,
} = Row::from(event);
let ref1 = ref1.as_ref().map(|x| x.as_str());
let ref2 = ref2.as_ref().map(|x| x.as_str());
let ref_name = ref_name.as_ref().map(|x| x.as_str());
let message = message.as_ref().map(|x| x.as_str());
tx.execute(
"
INSERT INTO event_log VALUES (
:timestamp,
:type,
:event_tx_id,
:old_ref,
:new_ref,
:ref_name,
:message
)
",
rusqlite::named_params! {
":timestamp": timestamp,
":type": &type_,
":event_tx_id": event_tx_id,
":old_ref": &ref1,
":new_ref": &ref2,
":ref_name": &ref_name,
":message": &message,
},
)?;
}
tx.commit()?;
Ok(())
}
#[instrument]
pub fn get_events(&self) -> eyre::Result<Vec<Event>> {
let mut stmt = self.conn.prepare(
"
SELECT timestamp, type, event_tx_id, old_ref, new_ref, ref_name, message
FROM event_log
ORDER BY rowid ASC
",
)?;
let rows: rusqlite::Result<Vec<Row>> = stmt
.query_map(rusqlite::params![], |row| {
let timestamp: f64 = row.get("timestamp")?;
let event_tx_id: isize = row.get("event_tx_id")?;
let type_: String = row.get("type")?;
let ref_name: Option<String> = row.get("ref_name")?;
let old_ref: Option<String> = row.get("old_ref")?;
let new_ref: Option<String> = row.get("new_ref")?;
let message: Option<String> = row.get("message")?;
Ok(Row {
timestamp,
event_tx_id,
type_,
ref_name: ref_name.map(ReferenceName::from),
ref1: old_ref.map(ReferenceName::from),
ref2: new_ref.map(ReferenceName::from),
message: message.map(ReferenceName::from),
})
})?
.collect();
let rows = rows?;
rows.into_iter().map(Event::try_from).collect()
}
#[instrument]
fn make_transaction_id_inner(
&self,
now: SystemTime,
message: &str,
) -> eyre::Result<EventTransactionId> {
if let Ok(transaction_id) = std::env::var(BRANCHLESS_TRANSACTION_ID_ENV_VAR) {
if let Ok(transaction_id) = transaction_id.parse::<EventTransactionId>() {
return Ok(transaction_id);
}
}
let tx = self.conn.unchecked_transaction()?;
let timestamp = now
.duration_since(SystemTime::UNIX_EPOCH)
.wrap_err("Calculating event transaction timestamp")?
.as_secs_f64();
self.conn
.execute(
"
INSERT INTO event_transactions
(timestamp, message)
VALUES
(:timestamp, :message)
",
rusqlite::named_params! {
":timestamp": timestamp,
":message": message,
},
)
.wrap_err("Creating event transaction")?;
let event_tx_id: isize = self.conn.last_insert_rowid().try_into()?;
tx.commit()?;
Ok(EventTransactionId(event_tx_id))
}
pub fn make_transaction_id(
&self,
now: SystemTime,
message: impl AsRef<str>,
) -> eyre::Result<EventTransactionId> {
self.make_transaction_id_inner(now, message.as_ref())
}
pub fn get_transaction_message(&self, event_tx_id: EventTransactionId) -> eyre::Result<String> {
let EventTransactionId(event_tx_id) = event_tx_id;
let mut stmt = self.conn.prepare(
"
SELECT message
FROM event_transactions
WHERE event_tx_id = :event_tx_id
",
)?;
let result: String = stmt.query_row(
rusqlite::named_params![":event_tx_id": event_tx_id,],
|row| {
let message: String = row.get("message")?;
Ok(message)
},
)?;
Ok(result)
}
}
pub fn is_gc_ref(reference_name: &ReferenceName) -> bool {
reference_name.as_str().starts_with("refs/branchless/")
}
pub fn should_ignore_ref_updates(reference_name: &ReferenceName) -> bool {
if is_gc_ref(reference_name) {
return true;
}
matches!(
reference_name.as_str(),
"ORIG_HEAD" | "CHERRY_PICK" | "REBASE_HEAD" | "CHERRY_PICK_HEAD" | "FETCH_HEAD"
)
}
#[derive(Debug)]
enum EventClassification {
Show,
Hide,
}
#[derive(Debug)]
pub enum CommitActivityStatus {
Active,
Inactive,
Obsolete,
}
#[derive(Debug)]
struct EventInfo {
id: isize,
event: Event,
event_classification: EventClassification,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EventCursor {
event_id: isize,
}
pub struct EventReplayer {
id_counter: isize,
events: Vec<Event>,
main_branch_reference_name: ReferenceName,
commit_history: HashMap<NonZeroOid, Vec<EventInfo>>,
ref_locations: HashMap<ReferenceName, NonZeroOid>,
}
impl std::fmt::Debug for EventReplayer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"<EventReplayer events.len={:?} ref_locations.len={:?}>",
self.events.len(),
self.ref_locations.len()
)
}
}
impl EventReplayer {
fn new(main_branch_reference_name: ReferenceName) -> Self {
EventReplayer {
id_counter: 0,
events: vec![],
main_branch_reference_name,
commit_history: HashMap::new(),
ref_locations: HashMap::new(),
}
}
pub fn from_event_log_db(
effects: &Effects,
repo: &Repo,
event_log_db: &EventLogDb,
) -> eyre::Result<Self> {
let (_effects, _progress) = effects.start_operation(OperationType::ProcessEvents);
let main_branch_reference_name = repo.get_main_branch()?.get_reference_name()?;
let mut result = EventReplayer::new(main_branch_reference_name);
for event in event_log_db.get_events()? {
result.process_event(&event);
}
Ok(result)
}
pub fn process_event(&mut self, event: &Event) {
if let Event::RefUpdateEvent { ref_name, .. } = event {
if should_ignore_ref_updates(ref_name) {
return;
}
}
let event = match self.fix_event_git_v2_31(event.clone()) {
None => {
return;
}
Some(event) => {
self.events.push(event);
self.events.last().unwrap()
}
};
let id = self.id_counter;
self.id_counter += 1;
match &event {
Event::RewriteEvent {
timestamp: _,
event_tx_id: _,
old_commit_oid,
new_commit_oid,
} => {
if let MaybeZeroOid::NonZero(old_commit_oid) = old_commit_oid {
self.commit_history
.entry(*old_commit_oid)
.or_insert_with(Vec::new)
.push(EventInfo {
id,
event: event.clone(),
event_classification: EventClassification::Hide,
});
}
if let MaybeZeroOid::NonZero(new_commit_oid) = new_commit_oid {
self.commit_history
.entry(*new_commit_oid)
.or_insert_with(Vec::new)
.push(EventInfo {
id,
event: event.clone(),
event_classification: EventClassification::Show,
});
}
}
Event::RefUpdateEvent {
ref_name, new_oid, ..
} => match new_oid {
MaybeZeroOid::NonZero(new_oid) => {
self.ref_locations.insert(ref_name.clone(), *new_oid);
}
MaybeZeroOid::Zero => {
self.ref_locations.remove(ref_name);
}
},
Event::CommitEvent {
timestamp: _,
event_tx_id: _,
commit_oid,
} => self
.commit_history
.entry(*commit_oid)
.or_insert_with(Vec::new)
.push(EventInfo {
id,
event: event.clone(),
event_classification: EventClassification::Show,
}),
Event::ObsoleteEvent {
timestamp: _,
event_tx_id: _,
commit_oid,
} => self
.commit_history
.entry(*commit_oid)
.or_insert_with(Vec::new)
.push(EventInfo {
id,
event: event.clone(),
event_classification: EventClassification::Hide,
}),
Event::UnobsoleteEvent {
timestamp: _,
event_tx_id: _,
commit_oid,
} => self
.commit_history
.entry(*commit_oid)
.or_insert_with(Vec::new)
.push(EventInfo {
id,
event: event.clone(),
event_classification: EventClassification::Show,
}),
Event::WorkingCopySnapshot { .. } => {
}
};
}
fn fix_event_git_v2_31(&self, event: Event) -> Option<Event> {
let event = match event {
Event::RefUpdateEvent {
timestamp,
event_tx_id,
ref_name,
old_oid: MaybeZeroOid::Zero,
new_oid: MaybeZeroOid::Zero,
message,
} => {
let old_oid: MaybeZeroOid = self.ref_locations.get(&ref_name).copied().into();
Event::RefUpdateEvent {
timestamp,
event_tx_id,
ref_name,
old_oid,
new_oid: MaybeZeroOid::Zero,
message,
}
}
_ => event,
};
match (event, self.events.last()) {
(
Event::RefUpdateEvent {
timestamp: _,
event_tx_id: _,
ref ref_name,
old_oid: _,
new_oid: MaybeZeroOid::Zero,
ref message,
},
Some(Event::RefUpdateEvent {
timestamp: _,
event_tx_id: _,
ref_name: last_ref_name,
old_oid: _,
new_oid: MaybeZeroOid::Zero,
message: last_message,
}),
) if ref_name == last_ref_name && message == last_message => None,
(event, _) => Some(event),
}
}
fn get_cursor_commit_history(&self, cursor: EventCursor, oid: NonZeroOid) -> Vec<&EventInfo> {
match self.commit_history.get(&oid) {
None => vec![],
Some(history) => history
.iter()
.filter(|event_info| event_info.id < cursor.event_id)
.collect(),
}
}
pub fn get_cursor_commit_activity_status(
&self,
cursor: EventCursor,
oid: NonZeroOid,
) -> CommitActivityStatus {
let history = self.get_cursor_commit_history(cursor, oid);
match history.last() {
Some(EventInfo {
id: _,
event: _,
event_classification: EventClassification::Show,
}) => CommitActivityStatus::Active,
Some(EventInfo {
id: _,
event: _,
event_classification: EventClassification::Hide,
}) => CommitActivityStatus::Obsolete,
None => CommitActivityStatus::Inactive,
}
}
pub fn get_cursor_commit_latest_event(
&self,
cursor: EventCursor,
oid: NonZeroOid,
) -> Option<&Event> {
let history = self.get_cursor_commit_history(cursor, oid);
let event_info = *history.last()?;
Some(&event_info.event)
}
pub fn get_cursor_oids(&self, cursor: EventCursor) -> HashSet<NonZeroOid> {
self.commit_history
.iter()
.filter_map(|(oid, history)| {
if history.iter().any(|event| event.id < cursor.event_id) {
Some(*oid)
} else {
None
}
})
.collect()
}
pub fn make_default_cursor(&self) -> EventCursor {
self.make_cursor(self.events.len().try_into().unwrap())
}
pub fn make_cursor(&self, event_id: isize) -> EventCursor {
let event_id = if event_id < 0 { 0 } else { event_id };
let num_events: isize = self.events.len().try_into().unwrap();
let event_id = if event_id > num_events {
num_events
} else {
event_id
};
EventCursor { event_id }
}
pub fn advance_cursor(&self, cursor: EventCursor, num_events: isize) -> EventCursor {
self.make_cursor(cursor.event_id + num_events)
}
fn get_event_tx_id_before_cursor(&self, cursor: EventCursor) -> Option<EventTransactionId> {
self.get_event_before_cursor(cursor)
.map(|(_event_id, event)| event.get_event_tx_id())
}
fn snap_to_transaction_boundary(&self, cursor: EventCursor) -> EventCursor {
let next_cursor = self.advance_cursor(cursor, 1);
if cursor == next_cursor {
return cursor;
}
let current_tx_id = self.get_event_tx_id_before_cursor(cursor);
let next_tx_id = self.get_event_tx_id_before_cursor(next_cursor);
if current_tx_id == next_tx_id {
self.snap_to_transaction_boundary(next_cursor)
} else {
cursor
}
}
fn advance_cursor_by_transaction_helper(
&self,
cursor: EventCursor,
num_transactions: isize,
) -> EventCursor {
match num_transactions.cmp(&0) {
Ordering::Equal => self.snap_to_transaction_boundary(cursor),
Ordering::Greater => {
let next_cursor = self.advance_cursor(cursor, 1);
if cursor == next_cursor {
return next_cursor;
}
let current_tx_id = self.get_event_tx_id_before_cursor(cursor);
let next_tx_id = self.get_event_tx_id_before_cursor(next_cursor);
let num_transactions = if current_tx_id == next_tx_id {
num_transactions
} else {
num_transactions - 1
};
self.advance_cursor_by_transaction_helper(next_cursor, num_transactions)
}
Ordering::Less => {
let prev_cursor = self.advance_cursor(cursor, -1);
if cursor == prev_cursor {
return prev_cursor;
}
let current_tx_id = self.get_event_tx_id_before_cursor(cursor);
let prev_tx_id = self.get_event_tx_id_before_cursor(prev_cursor);
let num_transactions = if current_tx_id == prev_tx_id {
num_transactions
} else {
num_transactions + 1
};
self.advance_cursor_by_transaction_helper(prev_cursor, num_transactions)
}
}
}
pub fn advance_cursor_by_transaction(
&self,
cursor: EventCursor,
num_transactions: isize,
) -> EventCursor {
if self.events.is_empty() {
cursor
} else {
let cursor = self.snap_to_transaction_boundary(cursor);
self.advance_cursor_by_transaction_helper(cursor, num_transactions)
}
}
fn get_cursor_head_oid(&self, cursor: EventCursor) -> Option<NonZeroOid> {
let cursor_event_id: usize = cursor.event_id.try_into().unwrap();
self.events[0..cursor_event_id]
.iter()
.rev()
.find_map(|event| {
match &event {
Event::RefUpdateEvent {
ref_name,
new_oid: MaybeZeroOid::NonZero(new_oid),
..
} if ref_name.as_str() == "HEAD" => Some(*new_oid),
Event::RefUpdateEvent { .. } => None,
Event::CommitEvent { commit_oid, .. } => Some(*commit_oid),
Event::WorkingCopySnapshot {
head_oid: MaybeZeroOid::NonZero(head_oid),
..
} => Some(*head_oid),
Event::WorkingCopySnapshot {
head_oid: MaybeZeroOid::Zero,
..
} => None,
Event::RewriteEvent { .. }
| Event::ObsoleteEvent { .. }
| Event::UnobsoleteEvent { .. } => None,
}
})
}
fn get_cursor_branch_oid(
&self,
cursor: EventCursor,
reference_name: &ReferenceName,
) -> eyre::Result<Option<NonZeroOid>> {
let cursor_event_id: usize = cursor.event_id.try_into().unwrap();
let oid = self.events[0..cursor_event_id]
.iter()
.rev()
.find_map(|event| match &event {
Event::RefUpdateEvent {
ref_name,
new_oid: MaybeZeroOid::NonZero(new_oid),
..
} if ref_name == reference_name => Some(*new_oid),
_ => None,
});
Ok(oid)
}
fn get_cursor_main_branch_oid(
&self,
cursor: EventCursor,
repo: &Repo,
) -> eyre::Result<NonZeroOid> {
let main_branch_reference_name = repo.get_main_branch()?.get_reference_name()?;
let main_branch_oid = self.get_cursor_branch_oid(cursor, &main_branch_reference_name)?;
match main_branch_oid {
Some(main_branch_oid) => Ok(main_branch_oid),
None => {
repo.get_main_branch_oid()
}
}
}
fn get_cursor_branch_oid_to_names(
&self,
cursor: EventCursor,
repo: &Repo,
) -> eyre::Result<HashMap<NonZeroOid, HashSet<ReferenceName>>> {
let mut ref_name_to_oid: HashMap<&ReferenceName, NonZeroOid> = HashMap::new();
let cursor_event_id: usize = cursor.event_id.try_into().unwrap();
for event in self.events[..cursor_event_id].iter() {
match event {
Event::RefUpdateEvent {
new_oid: MaybeZeroOid::NonZero(new_oid),
ref_name,
..
} => {
ref_name_to_oid.insert(ref_name, *new_oid);
}
Event::RefUpdateEvent {
new_oid: MaybeZeroOid::Zero,
ref_name,
..
} => {
ref_name_to_oid.remove(ref_name);
}
_ => {}
}
}
let mut result: HashMap<NonZeroOid, HashSet<ReferenceName>> = HashMap::new();
for (ref_name, ref_oid) in ref_name_to_oid.iter() {
if let CategorizedReferenceName::LocalBranch { .. } =
CategorizedReferenceName::new(ref_name)
{
result
.entry(*ref_oid)
.or_insert_with(HashSet::new)
.insert((*ref_name).clone());
}
}
let main_branch_oid = self.get_cursor_main_branch_oid(cursor, repo)?;
result
.entry(main_branch_oid)
.or_insert_with(HashSet::new)
.insert(self.main_branch_reference_name.clone());
Ok(result)
}
pub fn get_references_snapshot(
&self,
repo: &Repo,
cursor: EventCursor,
) -> eyre::Result<RepoReferencesSnapshot> {
let head_oid = self.get_cursor_head_oid(cursor);
let main_branch_oid = self.get_cursor_main_branch_oid(cursor, repo)?;
let branch_oid_to_names = self.get_cursor_branch_oid_to_names(cursor, repo)?;
Ok(RepoReferencesSnapshot {
head_oid,
main_branch_oid,
branch_oid_to_names,
})
}
pub fn get_event_before_cursor(&self, cursor: EventCursor) -> Option<(isize, &Event)> {
if cursor.event_id == 0 {
None
} else {
let previous_cursor_event_id: usize = (cursor.event_id - 1).try_into().unwrap();
Some((cursor.event_id, &self.events[previous_cursor_event_id]))
}
}
pub fn get_tx_events_before_cursor(&self, cursor: EventCursor) -> Option<(isize, &[Event])> {
let prev_tx_cursor = self.advance_cursor_by_transaction(cursor, -1);
let EventCursor {
event_id: prev_event_id,
} = prev_tx_cursor;
let EventCursor {
event_id: curr_event_id,
} = cursor;
let tx_events =
&self.events[prev_event_id.try_into().unwrap()..curr_event_id.try_into().unwrap()];
match tx_events {
[] => None,
events => Some((prev_event_id + 1, events)),
}
}
pub fn get_events_since_cursor(&self, cursor: EventCursor) -> &[Event] {
let cursor_event_id: usize = cursor.event_id.try_into().unwrap();
&self.events[cursor_event_id..]
}
}
pub mod testing {
use super::*;
pub fn make_dummy_transaction_id(id: isize) -> EventTransactionId {
EventTransactionId(id)
}
pub fn redact_event_timestamp(mut event: Event) -> Event {
match event {
Event::RewriteEvent {
ref mut timestamp, ..
}
| Event::RefUpdateEvent {
ref mut timestamp, ..
}
| Event::CommitEvent {
ref mut timestamp, ..
}
| Event::ObsoleteEvent {
ref mut timestamp, ..
}
| Event::UnobsoleteEvent {
ref mut timestamp, ..
}
| Event::WorkingCopySnapshot {
ref mut timestamp, ..
} => *timestamp = 0.0,
}
event
}
pub fn get_event_replayer_events(event_replayer: &EventReplayer) -> &Vec<Event> {
&event_replayer.events
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::make_git;
use testing::make_dummy_transaction_id;
#[test]
fn test_drop_non_meaningful_events() -> eyre::Result<()> {
let event_tx_id = make_dummy_transaction_id(123);
let meaningful_event = Event::CommitEvent {
timestamp: 0.0,
event_tx_id,
commit_oid: NonZeroOid::from_str("abc")?,
};
let mut replayer = EventReplayer::new("refs/heads/master".into());
replayer.process_event(&meaningful_event);
replayer.process_event(&Event::RefUpdateEvent {
timestamp: 0.0,
event_tx_id,
ref_name: ReferenceName::from("ORIG_HEAD"),
old_oid: MaybeZeroOid::from_str("abc")?,
new_oid: MaybeZeroOid::from_str("def")?,
message: None,
});
replayer.process_event(&Event::RefUpdateEvent {
timestamp: 0.0,
event_tx_id,
ref_name: ReferenceName::from("CHERRY_PICK_HEAD"),
old_oid: MaybeZeroOid::Zero,
new_oid: MaybeZeroOid::Zero,
message: None,
});
let cursor = replayer.make_default_cursor();
assert_eq!(
replayer.get_event_before_cursor(cursor),
Some((1, &meaningful_event))
);
Ok(())
}
#[test]
fn test_different_event_transaction_ids() -> eyre::Result<()> {
let git = make_git()?;
git.init_repo()?;
git.commit_file("test1", 1)?;
git.run(&["hide", "HEAD"])?;
let repo = git.get_repo()?;
let conn = repo.get_db_conn()?;
let event_log_db = EventLogDb::new(&conn)?;
let events = event_log_db.get_events()?;
let event_tx_ids: Vec<EventTransactionId> =
events.iter().map(|event| event.get_event_tx_id()).collect();
if git.supports_reference_transactions()? {
insta::assert_debug_snapshot!(event_tx_ids, @r###"
[
EventTransactionId(
1,
),
EventTransactionId(
1,
),
EventTransactionId(
2,
),
EventTransactionId(
3,
),
]
"###);
} else {
insta::assert_debug_snapshot!(event_tx_ids, @r###"
[
EventTransactionId(
1,
),
EventTransactionId(
2,
),
]
"###);
}
Ok(())
}
#[test]
fn test_advance_cursor_by_transaction() -> eyre::Result<()> {
let mut event_replayer = EventReplayer::new("refs/heads/master".into());
for (timestamp, event_tx_id) in (0..).zip(&[1, 1, 2, 2, 3, 4]) {
let timestamp: f64 = timestamp.try_into()?;
event_replayer.process_event(&Event::UnobsoleteEvent {
timestamp,
event_tx_id: EventTransactionId(*event_tx_id),
commit_oid: NonZeroOid::from_str("abc")?,
});
}
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 0 }, 1),
EventCursor { event_id: 2 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 1 }, 1),
EventCursor { event_id: 4 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 2 }, 1),
EventCursor { event_id: 4 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 3 }, 1),
EventCursor { event_id: 5 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 4 }, 1),
EventCursor { event_id: 5 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 5 }, 1),
EventCursor { event_id: 6 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 6 }, 1),
EventCursor { event_id: 6 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 6 }, -1),
EventCursor { event_id: 5 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 5 }, -1),
EventCursor { event_id: 4 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 4 }, -1),
EventCursor { event_id: 2 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 3 }, -1),
EventCursor { event_id: 2 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 2 }, -1),
EventCursor { event_id: 0 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 1 }, -1),
EventCursor { event_id: 0 },
);
assert_eq!(
event_replayer.advance_cursor_by_transaction(EventCursor { event_id: 0 }, -1),
EventCursor { event_id: 0 },
);
Ok(())
}
}