use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use crate::cell::{Cell, Generation, Settled, Timestamp, Unknown};
use crate::default_branch;
use crate::git::{InProgressOperation, RecentCommit};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct EntityKey(Arc<Path>);
impl EntityKey {
pub fn new(path: Arc<Path>) -> Self {
EntityKey(path)
}
pub fn path(&self) -> &Path {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Kind {
Repo,
Worktree,
Submodule,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Head {
Branch {
name: Arc<str>,
commit: gix::ObjectId,
},
Detached(gix::ObjectId),
Unborn(Arc<str>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct AheadBehind {
pub ahead: u32,
pub behind: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct DirtyCounts {
pub modified: u32,
pub untracked: u32,
pub deleted: u32,
}
impl DirtyCounts {
pub fn total(&self) -> u32 {
self.modified + self.untracked + self.deleted
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum SyncState {
Tracking(AheadBehind),
NoUpstream,
NoRemote,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum WorktreeState {
Merged,
Gone,
LocalOnly,
Active,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct DefaultBranch(Arc<str>);
impl DefaultBranch {
pub fn new(name: Arc<str>) -> Self {
DefaultBranch(name)
}
pub fn name(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum DefaultBranchStopped {
NoRemote,
AmbiguousRemote,
NameListExhausted,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Diagnostics {
pub default_branch_rung: Option<u8>,
pub default_branch_rung_disagreement: bool,
pub default_branch_rung_two_stale: bool,
pub default_branch_stopped: Option<DefaultBranchStopped>,
pub gitmodules_failed: Option<Arc<str>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum OwnWork {
Did(Arc<str>),
Refused(Arc<str>),
CouldNotAct(Arc<str>),
}
impl OwnWork {
pub fn said(&self) -> &Arc<str> {
match self {
OwnWork::Did(said) | OwnWork::Refused(said) | OwnWork::CouldNotAct(said) => said,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum StepOutcome {
Ok,
Failed(i32),
NotRun,
Cancelled,
OwnWork(OwnWork),
}
impl StepOutcome {
pub fn is_failure(&self) -> bool {
match self {
StepOutcome::Ok => false,
StepOutcome::Failed(_) => true,
StepOutcome::NotRun => false,
StepOutcome::Cancelled => false,
StepOutcome::OwnWork(OwnWork::Did(_)) => false,
StepOutcome::OwnWork(OwnWork::Refused(_)) => false,
StepOutcome::OwnWork(OwnWork::CouldNotAct(_)) => true,
}
}
pub fn is_refusal(&self) -> bool {
match self {
StepOutcome::Ok => false,
StepOutcome::Failed(_) => false,
StepOutcome::NotRun => false,
StepOutcome::Cancelled => false,
StepOutcome::OwnWork(OwnWork::Did(_)) => false,
StepOutcome::OwnWork(OwnWork::Refused(_)) => true,
StepOutcome::OwnWork(OwnWork::CouldNotAct(_)) => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CaptureElision {
pub dropped_lines: usize,
pub kept_head_lines: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct StepResult {
pub label: Arc<str>,
pub outcome: StepOutcome,
pub output: Arc<[u8]>,
pub elapsed: Duration,
pub elision: Option<CaptureElision>,
pub shell: bool,
pub interactive: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct RunningStep {
pub label: Arc<str>,
pub started_at: Timestamp,
pub shell: bool,
pub interactive: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ActionReceipt {
pub label: Arc<str>,
pub steps: Arc<[StepResult]>,
pub skip: Option<Skip>,
pub finished_at: Timestamp,
pub running: Option<RunningStep>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Skip {
Excluded,
Inapplicable,
Unresolved,
}
impl ActionReceipt {
pub fn failed(&self) -> bool {
self.steps.iter().any(|step| step.outcome.is_failure())
}
pub fn not_applicable(&self) -> bool {
self.skip == Some(Skip::Excluded)
}
pub fn inapplicable(&self) -> bool {
self.skip == Some(Skip::Inapplicable)
}
pub fn unresolved(&self) -> bool {
self.skip == Some(Skip::Unresolved)
}
pub fn refused(&self) -> bool {
!self.failed() && self.steps.iter().any(|step| step.outcome.is_refusal())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeleteRisk {
pub uncommitted: bool,
pub unpushed_commits: u32,
pub unpushed_branches: u32,
pub linked_worktrees: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Presence {
#[default]
Present,
Vanished,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct EntityState {
pub key: EntityKey,
pub name: Arc<str>,
pub common_dir: Arc<Path>,
pub kind: Kind,
pub branch: Cell<Head>,
pub sync: Cell<SyncState>,
pub base: Cell<u32>,
pub dirty: Cell<DirtyCounts>,
pub state: Cell<WorktreeState>,
pub default_branch: Cell<DefaultBranch>,
pub diagnostics: Diagnostics,
pub last_action: Option<ActionReceipt>,
pub presence: Presence,
pub excluded: bool,
pub in_progress_operation: Option<InProgressOperation>,
pub recent_commits: Vec<RecentCommit>,
}
impl EntityState {
pub fn new(key: EntityKey, name: Arc<str>, common_dir: Arc<Path>, kind: Kind) -> Self {
let mut entity = EntityState {
key,
name,
common_dir,
kind,
branch: Cell::default(),
sync: Cell::default(),
base: Cell::default(),
dirty: Cell::default(),
state: Cell::default(),
default_branch: Cell::default(),
diagnostics: Diagnostics::default(),
last_action: None,
presence: Presence::default(),
excluded: false,
in_progress_operation: None,
recent_commits: Vec::new(),
};
if matches!(entity.kind, Kind::Repo) {
entity
.state
.settle(Generation::default(), Settled::NotApplicable);
}
if matches!(entity.kind, Kind::Submodule) {
entity.state.settle(
Generation::default(),
Settled::Unknown(Unknown::NoDefaultBranch),
);
entity.base.settle(
Generation::default(),
Settled::Unknown(Unknown::NoDefaultBranch),
);
}
entity
}
pub(crate) fn apply_default_branch_resolution(
&mut self,
generation: Generation,
resolution: default_branch::Resolution,
) {
let rung = resolution.rung;
let disagreement = resolution.disagreement;
let stale_remote_head = resolution.stale_remote_head;
let stopped = resolution.stopped;
let applied = self.default_branch.settle(generation, resolution.settled);
if applied {
self.diagnostics.default_branch_rung = Some(rung);
self.diagnostics.default_branch_rung_disagreement = disagreement;
self.diagnostics.default_branch_rung_two_stale = stale_remote_head;
self.diagnostics.default_branch_stopped = stopped;
}
}
pub(crate) fn apply_branch_probe(
&mut self,
generation: Generation,
branch: Settled<Head>,
in_progress_operation: Option<InProgressOperation>,
recent_commits: Vec<RecentCommit>,
) -> bool {
let applied = self.branch.settle(generation, branch);
if applied {
self.in_progress_operation = in_progress_operation;
self.recent_commits = recent_commits;
}
applied
}
pub(crate) fn probes_state(&self) -> bool {
!matches!(self.kind, Kind::Submodule)
&& !matches!(self.state.settled(), Some(Settled::NotApplicable))
}
pub(crate) fn probes_base(&self) -> bool {
!matches!(self.kind, Kind::Submodule)
&& !matches!(self.base.settled(), Some(Settled::NotApplicable))
}
pub(crate) fn mark_vanished(&mut self) {
self.presence = Presence::Vanished;
let EntityState {
key: _,
name: _,
common_dir: _,
kind: _,
branch,
sync,
base,
dirty,
state,
default_branch,
diagnostics: _,
last_action: _,
presence: _,
excluded: _,
in_progress_operation: _,
recent_commits: _,
} = self;
branch.force_stale();
sync.force_stale();
base.force_stale();
dirty.force_stale();
state.force_stale();
default_branch.force_stale();
}
pub(crate) fn force_stale_status_cells(&mut self) {
let EntityState {
key: _,
name: _,
common_dir: _,
kind: _,
branch: _,
sync: _,
base: _,
dirty,
state,
default_branch: _,
diagnostics: _,
last_action: _,
presence: _,
excluded: _,
in_progress_operation: _,
recent_commits: _,
} = self;
dirty.force_stale();
state.force_stale();
}
pub(crate) fn age_status_cells(&mut self, threshold: Duration) {
let EntityState {
key: _,
name: _,
common_dir: _,
kind: _,
branch: _,
sync: _,
base: _,
dirty,
state,
default_branch: _,
diagnostics: _,
last_action: _,
presence: _,
excluded: _,
in_progress_operation: _,
recent_commits: _,
} = self;
dirty.age_into_stale(threshold);
state.age_into_stale(threshold);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cell::Timestamp;
fn key(path: &str) -> EntityKey {
EntityKey::new(Arc::from(Path::new(path)))
}
#[test]
fn a_submodule_is_constructed_with_state_and_base_unknown() {
let entity = EntityState::new(
key("/repo/vendor/lib"),
Arc::from("lib"),
Arc::from(Path::new("/repo/.git")),
Kind::Submodule,
);
assert!(matches!(
entity.state.settled(),
Some(Settled::Unknown(Unknown::NoDefaultBranch))
));
assert!(matches!(
entity.base.settled(),
Some(Settled::Unknown(Unknown::NoDefaultBranch))
));
}
#[test]
fn a_repo_rows_worktree_state_is_not_applicable_so_no_parent_row_carries_a_question_mark() {
let entity = EntityState::new(
key("/repo"),
Arc::from("repo"),
Arc::from(Path::new("/repo/.git")),
Kind::Repo,
);
assert!(matches!(
entity.state.settled(),
Some(Settled::NotApplicable)
));
assert!(entity.base.settled().is_none());
}
#[test]
fn worktree_state_is_exactly_four_mutually_exclusive_variants() {
fn name(state: WorktreeState) -> &'static str {
match state {
WorktreeState::Merged => "merged",
WorktreeState::Gone => "gone",
WorktreeState::LocalOnly => "local_only",
WorktreeState::Active => "active",
}
}
assert_eq!(name(WorktreeState::Merged), "merged");
assert_eq!(name(WorktreeState::Gone), "gone");
assert_eq!(name(WorktreeState::LocalOnly), "local_only");
assert_eq!(name(WorktreeState::Active), "active");
}
#[test]
fn sync_state_is_exactly_three_mutually_exclusive_variants() {
fn name(value: SyncState) -> &'static str {
match value {
SyncState::Tracking(_) => "tracking",
SyncState::NoUpstream => "no_upstream",
SyncState::NoRemote => "no_remote",
}
}
assert_eq!(
name(SyncState::Tracking(AheadBehind {
ahead: 0,
behind: 0
})),
"tracking"
);
assert_eq!(name(SyncState::NoUpstream), "no_upstream");
assert_eq!(name(SyncState::NoRemote), "no_remote");
}
#[test]
fn step_outcome_is_exactly_five_mutually_exclusive_variants() {
fn name(outcome: &StepOutcome) -> &'static str {
match outcome {
StepOutcome::Ok => "ok",
StepOutcome::Failed(_) => "failed",
StepOutcome::NotRun => "not_run",
StepOutcome::Cancelled => "cancelled",
StepOutcome::OwnWork(work) => match work {
OwnWork::Did(_) => "own_work_did",
OwnWork::Refused(_) => "own_work_refused",
OwnWork::CouldNotAct(_) => "own_work_could_not_act",
},
}
}
assert_eq!(name(&StepOutcome::Ok), "ok");
assert_eq!(name(&StepOutcome::Failed(1)), "failed");
assert_eq!(name(&StepOutcome::NotRun), "not_run");
assert_eq!(name(&StepOutcome::Cancelled), "cancelled");
assert_eq!(name(&did("ignored")), "own_work_did");
assert_eq!(name(&refused("already ignored")), "own_work_refused");
assert_eq!(
name(&could_not_act("no such file")),
"own_work_could_not_act"
);
}
fn did(said: &str) -> StepOutcome {
StepOutcome::OwnWork(OwnWork::Did(Arc::from(said)))
}
fn refused(said: &str) -> StepOutcome {
StepOutcome::OwnWork(OwnWork::Refused(Arc::from(said)))
}
fn could_not_act(said: &str) -> StepOutcome {
StepOutcome::OwnWork(OwnWork::CouldNotAct(Arc::from(said)))
}
#[test]
fn among_the_child_process_outcomes_only_failed_is_a_failure() {
assert!(!StepOutcome::Ok.is_failure());
assert!(StepOutcome::Failed(1).is_failure());
assert!(!StepOutcome::NotRun.is_failure());
assert!(!StepOutcome::Cancelled.is_failure());
}
#[test]
fn only_own_work_repon_could_not_finish_is_a_failure_and_only_a_refusal_is_a_refusal() {
assert!(!did("ignored").is_failure());
assert!(!refused("already ignored").is_failure());
assert!(could_not_act("permission denied").is_failure());
assert!(!did("ignored").is_refusal());
assert!(refused("already ignored").is_refusal());
assert!(!could_not_act("permission denied").is_refusal());
assert!(!StepOutcome::Cancelled.is_refusal());
assert!(!StepOutcome::Failed(1).is_refusal());
}
#[test]
fn own_work_says_its_own_words_whichever_grade_it_is() {
let words = |outcome: StepOutcome| match outcome {
StepOutcome::OwnWork(work) => work.said().to_string(),
StepOutcome::Ok
| StepOutcome::Failed(_)
| StepOutcome::NotRun
| StepOutcome::Cancelled => panic!("built as own work"),
};
assert_eq!(words(did("ignored")), "ignored");
assert_eq!(words(refused("already ignored")), "already ignored");
assert_eq!(words(could_not_act("boom")), "boom");
}
#[test]
fn a_receipt_of_own_work_reads_failed_or_refused_but_never_both() {
let one = |outcome: StepOutcome| {
receipt(
"ignore",
vec![StepResult {
label: Arc::from("ignore"),
outcome,
output: Arc::from(&b""[..]),
elapsed: Duration::from_millis(1),
elision: None,
shell: false,
interactive: false,
}],
)
};
assert!(!one(did("ignored")).failed());
assert!(!one(did("ignored")).refused());
assert!(!one(refused("already ignored")).failed());
assert!(one(refused("already ignored")).refused());
assert!(one(could_not_act("boom")).failed());
assert!(!one(could_not_act("boom")).refused());
}
fn ok_step(label: &str) -> StepResult {
StepResult {
label: Arc::from(label),
outcome: StepOutcome::Ok,
output: Arc::from(&b""[..]),
elapsed: Duration::from_millis(1),
elision: None,
shell: false,
interactive: false,
}
}
fn failed_step(label: &str, code: i32) -> StepResult {
StepResult {
label: Arc::from(label),
outcome: StepOutcome::Failed(code),
output: Arc::from(&b"boom"[..]),
elapsed: Duration::from_millis(2),
elision: None,
shell: false,
interactive: false,
}
}
fn receipt(label: &str, steps: Vec<StepResult>) -> ActionReceipt {
ActionReceipt {
label: Arc::from(label),
steps: Arc::from(steps),
skip: None,
finished_at: Timestamp::now(),
running: None,
}
}
#[test]
fn action_receipt_and_step_result_carry_no_generation_and_no_success_condition_field() {
let original = receipt("reinstall", vec![ok_step("rm -rf node_modules")]);
let ActionReceipt {
label,
steps,
skip,
finished_at: _,
running: _,
} = original;
let StepResult {
label: step_label,
outcome,
shell: _,
interactive: _,
output: _,
elapsed: _,
elision: _,
} = steps[0].clone();
assert_eq!(&*label, "reinstall");
assert_eq!(skip, None);
assert_eq!(&*step_label, "rm -rf node_modules");
assert_eq!(outcome, StepOutcome::Ok);
}
#[test]
fn action_receipt_failed_is_true_only_when_a_step_actually_failed() {
assert!(!receipt("ok", vec![ok_step("a")]).failed());
assert!(receipt("broken", vec![ok_step("a"), failed_step("b", 1)]).failed());
assert!(
!receipt(
"cancelled",
vec![StepResult {
label: Arc::from("a"),
outcome: StepOutcome::Cancelled,
output: Arc::from(&b""[..]),
elapsed: Duration::from_millis(1),
elision: None,
shell: false,
interactive: false,
}]
)
.failed(),
"a cancelled step must never read as a failure"
);
}
#[test]
fn a_worktree_is_constructed_with_state_and_base_unset() {
let entity = EntityState::new(
key("/repo-wt"),
Arc::from("repo-wt"),
Arc::from(Path::new("/repo/.git")),
Kind::Worktree,
);
assert!(entity.state.settled().is_none());
assert!(entity.base.settled().is_none());
}
#[test]
fn marking_an_entity_vanished_keeps_every_cells_value_and_forces_every_one_stale() {
let mut entity = EntityState::new(
key("/repo"),
Arc::from("repo"),
Arc::from(Path::new("/repo/.git")),
Kind::Repo,
);
let generation = Generation::default();
entity.branch.settle(
generation,
Settled::Known {
value: Head::Branch {
name: Arc::from("main"),
commit: gix::hash::Kind::Sha1.null(),
},
at: Timestamp::now(),
stale: false,
},
);
entity.sync.settle(
generation,
Settled::Known {
value: SyncState::Tracking(AheadBehind {
ahead: 1,
behind: 2,
}),
at: Timestamp::now(),
stale: false,
},
);
entity.base.settle(
generation,
Settled::Known {
value: 3,
at: Timestamp::now(),
stale: false,
},
);
entity.dirty.settle(
generation,
Settled::Known {
value: DirtyCounts {
modified: 4,
untracked: 1,
deleted: 2,
},
at: Timestamp::now(),
stale: false,
},
);
entity.state.settle(
generation,
Settled::Known {
value: WorktreeState::Active,
at: Timestamp::now(),
stale: false,
},
);
entity.default_branch.settle(
generation,
Settled::Known {
value: DefaultBranch::new(Arc::from("main")),
at: Timestamp::now(),
stale: false,
},
);
entity.mark_vanished();
assert_eq!(entity.presence, Presence::Vanished);
match entity.branch.settled() {
Some(Settled::Known {
value: Head::Branch { name, .. },
stale: true,
at: _,
}) => assert_eq!(&**name, "main"),
other => panic!("expected branch to keep its value and go stale, got {other:?}"),
}
match entity.sync.settled() {
Some(Settled::Known {
value: SyncState::Tracking(AheadBehind { ahead, behind }),
stale: true,
at: _,
}) => {
assert_eq!(*ahead, 1);
assert_eq!(*behind, 2);
}
other => panic!("expected sync to keep its value and go stale, got {other:?}"),
}
match entity.base.settled() {
Some(Settled::Known {
value: 3,
stale: true,
at: _,
}) => {}
other => panic!("expected base to keep its value and go stale, got {other:?}"),
}
match entity.dirty.settled() {
Some(Settled::Known {
value:
DirtyCounts {
modified: 4,
untracked: 1,
deleted: 2,
},
stale: true,
at: _,
}) => {}
other => panic!("expected dirty to keep its value and go stale, got {other:?}"),
}
match entity.state.settled() {
Some(Settled::Known {
value: WorktreeState::Active,
stale: true,
at: _,
}) => {}
other => panic!("expected state to keep its value and go stale, got {other:?}"),
}
match entity.default_branch.settled() {
Some(Settled::Known {
value,
stale: true,
at: _,
}) => assert_eq!(value.name(), "main"),
other => {
panic!("expected default_branch to keep its value and go stale, got {other:?}")
}
}
}
#[test]
fn marking_a_submodule_vanished_leaves_its_unknown_cells_untouched() {
let mut entity = EntityState::new(
key("/repo/vendor/lib"),
Arc::from("lib"),
Arc::from(Path::new("/repo/.git")),
Kind::Submodule,
);
entity.mark_vanished();
assert_eq!(entity.presence, Presence::Vanished);
assert!(matches!(
entity.state.settled(),
Some(Settled::Unknown(Unknown::NoDefaultBranch))
));
assert!(matches!(
entity.base.settled(),
Some(Settled::Unknown(Unknown::NoDefaultBranch))
));
}
#[test]
fn force_stale_status_cells_stales_only_dirty_and_state() {
let mut entity = EntityState::new(
key("/repo"),
Arc::from("repo"),
Arc::from(Path::new("/repo/.git")),
Kind::Worktree,
);
let generation = Generation::new(1);
entity.branch.settle(
generation,
Settled::Known {
value: Head::Branch {
name: Arc::from("main"),
commit: gix::ObjectId::null(gix::hash::Kind::Sha1),
},
at: Timestamp::now(),
stale: false,
},
);
entity.dirty.settle(
generation,
Settled::Known {
value: DirtyCounts {
modified: 1,
untracked: 0,
deleted: 0,
},
at: Timestamp::now(),
stale: false,
},
);
entity.state.settle(
generation,
Settled::Known {
value: WorktreeState::Active,
at: Timestamp::now(),
stale: false,
},
);
entity.force_stale_status_cells();
match entity.branch.settled() {
Some(Settled::Known {
stale: false,
value: _,
at: _,
}) => {}
other => panic!("expected branch to stay fresh, got {other:?}"),
}
match entity.dirty.settled() {
Some(Settled::Known {
stale: true,
value: _,
at: _,
}) => {}
other => panic!("expected dirty to go stale, got {other:?}"),
}
match entity.state.settled() {
Some(Settled::Known {
stale: true,
value: _,
at: _,
}) => {}
other => panic!("expected state to go stale, got {other:?}"),
}
}
#[test]
fn age_status_cells_stales_dirty_and_state_once_old_enough() {
let mut old_entity = EntityState::new(
key("/repo"),
Arc::from("repo"),
Arc::from(Path::new("/repo/.git")),
Kind::Worktree,
);
let generation = Generation::new(1);
let old_at = Timestamp::at(std::time::SystemTime::now() - Duration::from_secs(600));
old_entity.dirty.settle(
generation,
Settled::Known {
value: DirtyCounts {
modified: 1,
untracked: 0,
deleted: 0,
},
at: old_at,
stale: false,
},
);
old_entity.state.settle(
generation,
Settled::Known {
value: WorktreeState::Active,
at: old_at,
stale: false,
},
);
old_entity.age_status_cells(Duration::from_secs(300));
match old_entity.dirty.settled() {
Some(Settled::Known {
stale: true,
value: _,
at: _,
}) => {}
other => panic!("expected an old dirty value to age into stale, got {other:?}"),
}
match old_entity.state.settled() {
Some(Settled::Known {
stale: true,
value: _,
at: _,
}) => {}
other => panic!("expected an old state value to age into stale, got {other:?}"),
}
let mut fresh_entity = EntityState::new(
key("/repo"),
Arc::from("repo"),
Arc::from(Path::new("/repo/.git")),
Kind::Worktree,
);
fresh_entity.dirty.settle(
generation,
Settled::Known {
value: DirtyCounts {
modified: 1,
untracked: 0,
deleted: 0,
},
at: Timestamp::now(),
stale: false,
},
);
fresh_entity.age_status_cells(Duration::from_secs(300));
match fresh_entity.dirty.settled() {
Some(Settled::Known {
stale: false,
value: _,
at: _,
}) => {}
other => panic!("expected a fresh dirty value to stay fresh, got {other:?}"),
}
}
#[test]
fn marking_an_entity_vanished_leaves_its_action_receipt_untouched() {
let mut entity = EntityState::new(
key("/repo"),
Arc::from("repo"),
Arc::from(Path::new("/repo/.git")),
Kind::Repo,
);
let original = ActionReceipt {
label: Arc::from("reinstall"),
steps: Arc::from(vec![StepResult {
label: Arc::from("pnpm install"),
outcome: StepOutcome::Ok,
output: Arc::from(&b""[..]),
elapsed: Duration::from_millis(1),
elision: None,
shell: false,
interactive: false,
}]),
skip: None,
finished_at: Timestamp::now(),
running: None,
};
entity.last_action = Some(original.clone());
entity.mark_vanished();
assert_eq!(entity.last_action, Some(original));
}
fn known_branch(name: &str) -> Settled<Head> {
Settled::Known {
value: Head::Branch {
name: Arc::from(name),
commit: gix::hash::Kind::Sha1.null(),
},
at: Timestamp::now(),
stale: false,
}
}
fn commit(short_id: &str, summary: &str) -> RecentCommit {
RecentCommit {
short_id: Arc::from(short_id),
summary: Arc::from(summary),
}
}
#[test]
fn a_branch_probe_that_applies_stores_its_in_progress_operation_and_recent_commits() {
let mut entity = EntityState::new(
key("/repo"),
Arc::from("repo"),
Arc::from(Path::new("/repo/.git")),
Kind::Worktree,
);
let commits = vec![commit("abc1234", "a commit")];
let applied = entity.apply_branch_probe(
Generation::default(),
known_branch("main"),
Some(InProgressOperation::Rebase),
commits.clone(),
);
assert!(applied);
assert_eq!(
entity.in_progress_operation,
Some(InProgressOperation::Rebase)
);
assert_eq!(entity.recent_commits, commits);
}
#[test]
fn a_superseded_branch_probe_leaves_the_newer_reads_facts_intact() {
let mut entity = EntityState::new(
key("/repo"),
Arc::from("repo"),
Arc::from(Path::new("/repo/.git")),
Kind::Worktree,
);
let newer_commits = vec![commit("newer12", "the newer read")];
let applied_first = entity.apply_branch_probe(
Generation::new(5),
known_branch("main"),
Some(InProgressOperation::Merge),
newer_commits.clone(),
);
assert!(
applied_first,
"the first write, at Generation 5, must apply"
);
let older_commits = vec![commit("older12", "a stale read")];
let applied_second = entity.apply_branch_probe(
Generation::new(2),
known_branch("main"),
Some(InProgressOperation::Rebase),
older_commits,
);
assert!(
!applied_second,
"a write at an older Generation must not apply"
);
assert_eq!(
entity.in_progress_operation,
Some(InProgressOperation::Merge)
);
assert_eq!(entity.recent_commits, newer_commits);
}
#[test]
fn the_entity_key_is_not_the_common_dir() {
let entity = EntityState::new(
key("/repo-wt"),
Arc::from("repo-wt"),
Arc::from(Path::new("/repo/.git")),
Kind::Worktree,
);
assert_ne!(entity.key.path(), &*entity.common_dir);
}
}