use worktrunk::git::IntegrationReason;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Divergence {
#[default]
None,
InSync,
Ahead,
Behind,
Diverged,
}
impl Divergence {
pub fn from_counts_with_remote(ahead: usize, behind: usize) -> Self {
match (ahead, behind) {
(0, 0) => Self::InSync,
(_, 0) => Self::Ahead,
(0, _) => Self::Behind,
_ => Self::Diverged,
}
}
pub fn symbol(self) -> &'static str {
match self {
Self::None => "",
Self::InSync => "|",
Self::Ahead => "⇡",
Self::Behind => "⇣",
Self::Diverged => "⇅",
}
}
pub fn styled(self) -> Option<String> {
use color_print::cformat;
if self == Self::None {
None
} else {
Some(cformat!("<dim>{}</>", self.symbol()))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, strum::IntoStaticStr)]
pub enum WorktreeState {
#[strum(serialize = "")]
#[default]
None,
BranchWorktreeMismatch,
Prunable,
Locked,
Branch,
}
impl std::fmt::Display for WorktreeState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::None => Ok(()),
Self::BranchWorktreeMismatch => write!(f, "⚑"),
Self::Prunable => write!(f, "⊟"),
Self::Locked => write!(f, "⊞"),
Self::Branch => write!(f, "/"),
}
}
}
impl serde::Serialize for WorktreeState {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, strum::IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub enum MainState {
#[default]
#[strum(serialize = "")]
None,
IsMain,
WouldConflict,
Empty,
SameCommit,
#[strum(serialize = "integrated")]
Integrated(IntegrationReason),
Orphan,
Diverged,
Ahead,
Behind,
}
impl std::fmt::Display for MainState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::None => Ok(()),
Self::IsMain => write!(f, "^"),
Self::WouldConflict => write!(f, "✗"),
Self::Empty => write!(f, "_"),
Self::SameCommit => write!(f, "–"), Self::Integrated(_) => write!(f, "⊂"),
Self::Orphan => write!(f, "∅"), Self::Diverged => write!(f, "↕"),
Self::Ahead => write!(f, "↑"),
Self::Behind => write!(f, "↓"),
}
}
}
impl MainState {
pub fn styled(&self) -> Option<String> {
use color_print::cformat;
match self {
Self::None => None,
Self::WouldConflict => Some(cformat!("<yellow>{self}</>")),
_ => Some(cformat!("<dim>{self}</>")),
}
}
pub fn integration_reason(&self) -> Option<IntegrationReason> {
match self {
Self::Integrated(reason) => Some(*reason),
_ => None,
}
}
pub fn as_json_str(self) -> Option<&'static str> {
let s: &'static str = self.into();
if s.is_empty() { None } else { Some(s) }
}
pub fn from_integration_and_counts(
is_main: bool,
would_conflict: bool,
integration: Option<MainState>,
is_same_commit_dirty: bool,
is_orphan: bool,
ahead: usize,
behind: usize,
) -> Self {
if is_main {
Self::IsMain
} else if is_orphan {
Self::Orphan
} else if would_conflict {
Self::WouldConflict
} else if let Some(state) = integration {
state
} else if is_same_commit_dirty {
Self::SameCommit
} else {
match (ahead, behind) {
(0, 0) => Self::None,
(_, 0) => Self::Ahead,
(0, _) => Self::Behind,
_ => Self::Diverged,
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tier<T> {
Fired(T),
RuledOut,
Wait,
}
pub fn tier_is_main(is_main: bool) -> Tier<MainState> {
if is_main {
Tier::Fired(MainState::IsMain)
} else {
Tier::RuledOut
}
}
pub fn tier_orphan(is_orphan: Option<bool>) -> Tier<MainState> {
match is_orphan {
Some(true) => Tier::Fired(MainState::Orphan),
Some(false) => Tier::RuledOut,
None => Tier::Wait,
}
}
pub fn tier_would_conflict(
has_merge_tree_conflicts: Option<bool>,
has_working_tree_conflicts: Option<Option<bool>>,
) -> Tier<MainState> {
if let Some(Some(true)) = has_working_tree_conflicts {
return Tier::Fired(MainState::WouldConflict);
}
match has_merge_tree_conflicts {
Some(true) => return Tier::Fired(MainState::WouldConflict),
Some(false) => {}
None => return Tier::Wait,
}
match has_working_tree_conflicts {
Some(_) => Tier::RuledOut,
None => Tier::Wait,
}
}
pub fn tier_integration_or_counts(
counts: Option<super::stats::AheadBehind>,
is_clean: Option<bool>,
integration: Option<MainState>,
) -> Tier<MainState> {
let Some(counts) = counts else {
return Tier::Wait;
};
let Some(is_clean) = is_clean else {
return Tier::Wait;
};
let is_same_commit_dirty = !is_clean && counts.ahead == 0 && counts.behind == 0;
Tier::Fired(MainState::from_integration_and_counts(
false, false, integration,
is_same_commit_dirty,
false, counts.ahead,
counts.behind,
))
}
impl serde::Serialize for MainState {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, strum::IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub enum OperationState {
#[default]
#[strum(serialize = "")]
None,
Conflicts,
Rebase,
Merge,
}
impl std::fmt::Display for OperationState {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::None => Ok(()),
Self::Conflicts => write!(f, "✘"),
Self::Rebase => write!(f, "⤴"),
Self::Merge => write!(f, "⤵"),
}
}
}
impl OperationState {
pub fn styled(&self) -> Option<String> {
use color_print::cformat;
match self {
Self::None => None,
Self::Conflicts => Some(cformat!("<red>{self}</>")),
Self::Rebase | Self::Merge => Some(cformat!("<yellow>{self}</>")),
}
}
pub fn as_json_str(self) -> Option<&'static str> {
let s: &'static str = self.into();
if s.is_empty() { None } else { Some(s) }
}
}
impl serde::Serialize for OperationState {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, strum::IntoStaticStr)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum ActiveGitOperation {
#[strum(serialize = "")]
#[serde(rename = "")]
#[default]
None,
Rebase,
Merge,
}
impl ActiveGitOperation {
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divergence_from_counts_with_remote() {
assert_eq!(
Divergence::from_counts_with_remote(0, 0),
Divergence::InSync
);
assert_eq!(Divergence::from_counts_with_remote(5, 0), Divergence::Ahead);
assert_eq!(
Divergence::from_counts_with_remote(0, 3),
Divergence::Behind
);
assert_eq!(
Divergence::from_counts_with_remote(5, 3),
Divergence::Diverged
);
}
#[test]
fn test_divergence_symbol() {
assert_eq!(Divergence::None.symbol(), "");
assert_eq!(Divergence::InSync.symbol(), "|");
assert_eq!(Divergence::Ahead.symbol(), "⇡");
assert_eq!(Divergence::Behind.symbol(), "⇣");
assert_eq!(Divergence::Diverged.symbol(), "⇅");
}
#[test]
fn test_divergence_styled() {
use insta::assert_snapshot;
assert!(Divergence::None.styled().is_none());
assert_snapshot!(Divergence::InSync.styled().unwrap(), @"[2m|[22m");
assert_snapshot!(Divergence::Ahead.styled().unwrap(), @"[2m⇡[22m");
assert_snapshot!(Divergence::Behind.styled().unwrap(), @"[2m⇣[22m");
assert_snapshot!(Divergence::Diverged.styled().unwrap(), @"[2m⇅[22m");
}
#[test]
fn test_worktree_state_display() {
assert_eq!(format!("{}", WorktreeState::None), "");
assert_eq!(format!("{}", WorktreeState::BranchWorktreeMismatch), "⚑");
assert_eq!(format!("{}", WorktreeState::Prunable), "⊟");
assert_eq!(format!("{}", WorktreeState::Locked), "⊞");
assert_eq!(format!("{}", WorktreeState::Branch), "/");
}
#[test]
fn test_worktree_state_serialize() {
let json = serde_json::to_string(&WorktreeState::None).unwrap();
assert_eq!(json, "\"\"");
let json = serde_json::to_string(&WorktreeState::BranchWorktreeMismatch).unwrap();
assert_eq!(json, "\"⚑\"");
let json = serde_json::to_string(&WorktreeState::Branch).unwrap();
assert_eq!(json, "\"/\"");
}
#[test]
fn test_main_state_display() {
assert_eq!(format!("{}", MainState::None), "");
assert_eq!(format!("{}", MainState::IsMain), "^");
assert_eq!(format!("{}", MainState::WouldConflict), "✗");
assert_eq!(format!("{}", MainState::Empty), "_");
assert_eq!(format!("{}", MainState::SameCommit), "–"); assert_eq!(
format!("{}", MainState::Integrated(IntegrationReason::Ancestor)),
"⊂"
);
assert_eq!(format!("{}", MainState::Orphan), "∅"); assert_eq!(format!("{}", MainState::Diverged), "↕");
assert_eq!(format!("{}", MainState::Ahead), "↑");
assert_eq!(format!("{}", MainState::Behind), "↓");
}
#[test]
fn test_main_state_styled() {
use insta::assert_snapshot;
assert!(MainState::None.styled().is_none());
assert_snapshot!(MainState::WouldConflict.styled().unwrap(), @"[33m✗[39m");
assert_snapshot!(MainState::IsMain.styled().unwrap(), @"[2m^[22m");
assert_snapshot!(MainState::Ahead.styled().unwrap(), @"[2m↑[22m");
assert_snapshot!(MainState::Orphan.styled().unwrap(), @"[2m∅[22m");
}
#[test]
fn test_main_state_serialize() {
let json = serde_json::to_string(&MainState::None).unwrap();
assert_eq!(json, "\"\"");
let json = serde_json::to_string(&MainState::IsMain).unwrap();
assert_eq!(json, "\"^\"");
let json = serde_json::to_string(&MainState::Diverged).unwrap();
assert_eq!(json, "\"↕\"");
let json = serde_json::to_string(&MainState::Orphan).unwrap();
assert_eq!(json, "\"∅\"");
}
#[test]
fn test_main_state_as_json_str() {
assert_eq!(MainState::None.as_json_str(), None);
assert_eq!(MainState::IsMain.as_json_str(), Some("is_main"));
assert_eq!(
MainState::WouldConflict.as_json_str(),
Some("would_conflict")
);
assert_eq!(MainState::Empty.as_json_str(), Some("empty"));
assert_eq!(MainState::SameCommit.as_json_str(), Some("same_commit"));
assert_eq!(
MainState::Integrated(IntegrationReason::TreesMatch).as_json_str(),
Some("integrated")
);
assert_eq!(MainState::Diverged.as_json_str(), Some("diverged"));
assert_eq!(MainState::Ahead.as_json_str(), Some("ahead"));
assert_eq!(MainState::Behind.as_json_str(), Some("behind"));
}
#[test]
fn test_integration_reason_into_static_str() {
let s: &'static str = IntegrationReason::SameCommit.into();
assert_eq!(s, "same-commit");
let s: &'static str = IntegrationReason::Ancestor.into();
assert_eq!(s, "ancestor");
let s: &'static str = IntegrationReason::TreesMatch.into();
assert_eq!(s, "trees-match");
let s: &'static str = IntegrationReason::NoAddedChanges.into();
assert_eq!(s, "no-added-changes");
let s: &'static str = IntegrationReason::MergeAddsNothing.into();
assert_eq!(s, "merge-adds-nothing");
let s: &'static str = IntegrationReason::PatchIdMatch.into();
assert_eq!(s, "patch-id-match");
}
#[test]
fn test_main_state_integration_reason() {
assert_eq!(MainState::None.integration_reason(), None);
assert_eq!(MainState::IsMain.integration_reason(), None);
assert_eq!(MainState::WouldConflict.integration_reason(), None);
assert_eq!(MainState::Empty.integration_reason(), None);
assert_eq!(MainState::SameCommit.integration_reason(), None);
assert_eq!(MainState::Diverged.integration_reason(), None);
assert_eq!(MainState::Ahead.integration_reason(), None);
assert_eq!(MainState::Behind.integration_reason(), None);
assert_eq!(
MainState::Integrated(IntegrationReason::Ancestor).integration_reason(),
Some(IntegrationReason::Ancestor)
);
assert_eq!(
MainState::Integrated(IntegrationReason::TreesMatch).integration_reason(),
Some(IntegrationReason::TreesMatch)
);
assert_eq!(
MainState::Integrated(IntegrationReason::NoAddedChanges).integration_reason(),
Some(IntegrationReason::NoAddedChanges)
);
assert_eq!(
MainState::Integrated(IntegrationReason::MergeAddsNothing).integration_reason(),
Some(IntegrationReason::MergeAddsNothing)
);
assert_eq!(
MainState::Integrated(IntegrationReason::PatchIdMatch).integration_reason(),
Some(IntegrationReason::PatchIdMatch)
);
}
#[test]
fn test_main_state_from_integration_and_counts() {
assert!(matches!(
MainState::from_integration_and_counts(true, false, None, false, false, 5, 3),
MainState::IsMain
));
assert!(matches!(
MainState::from_integration_and_counts(false, true, None, false, true, 0, 0),
MainState::Orphan
));
assert!(matches!(
MainState::from_integration_and_counts(false, true, None, false, false, 5, 3),
MainState::WouldConflict
));
assert!(matches!(
MainState::from_integration_and_counts(
false,
false,
Some(MainState::Empty),
false,
false,
0,
0
),
MainState::Empty
));
assert!(matches!(
MainState::from_integration_and_counts(
false,
false,
Some(MainState::Integrated(IntegrationReason::Ancestor)),
false,
false,
0,
5
),
MainState::Integrated(IntegrationReason::Ancestor)
));
assert!(matches!(
MainState::from_integration_and_counts(false, false, None, true, false, 0, 0),
MainState::SameCommit
));
assert!(matches!(
MainState::from_integration_and_counts(false, false, None, false, true, 0, 0),
MainState::Orphan
));
assert!(matches!(
MainState::from_integration_and_counts(false, false, None, false, false, 3, 2),
MainState::Diverged
));
assert!(matches!(
MainState::from_integration_and_counts(false, false, None, false, false, 3, 0),
MainState::Ahead
));
assert!(matches!(
MainState::from_integration_and_counts(false, false, None, false, false, 0, 2),
MainState::Behind
));
assert!(matches!(
MainState::from_integration_and_counts(false, false, None, false, false, 0, 0),
MainState::None
));
}
#[test]
fn test_tier_is_main() {
assert_eq!(tier_is_main(true), Tier::Fired(MainState::IsMain));
assert_eq!(tier_is_main(false), Tier::RuledOut);
}
#[test]
fn test_tier_orphan() {
assert_eq!(tier_orphan(Some(true)), Tier::Fired(MainState::Orphan));
assert_eq!(tier_orphan(Some(false)), Tier::RuledOut);
assert_eq!(tier_orphan(None), Tier::Wait);
}
#[test]
fn test_tier_would_conflict() {
assert_eq!(
tier_would_conflict(Some(true), None),
Tier::Fired(MainState::WouldConflict)
);
assert_eq!(
tier_would_conflict(None, Some(Some(true))),
Tier::Fired(MainState::WouldConflict)
);
assert_eq!(tier_would_conflict(Some(false), Some(None)), Tier::RuledOut);
assert_eq!(
tier_would_conflict(Some(false), Some(Some(false))),
Tier::RuledOut
);
assert_eq!(tier_would_conflict(None, None), Tier::Wait);
assert_eq!(tier_would_conflict(None, Some(None)), Tier::Wait);
assert_eq!(tier_would_conflict(Some(false), None), Tier::Wait);
}
#[test]
fn test_tier_integration_or_counts() {
use super::super::stats::AheadBehind;
assert_eq!(
tier_integration_or_counts(None, Some(true), None),
Tier::Wait
);
assert_eq!(
tier_integration_or_counts(
Some(AheadBehind {
ahead: 0,
behind: 0
}),
None,
None
),
Tier::Wait
);
assert_eq!(
tier_integration_or_counts(
Some(AheadBehind {
ahead: 0,
behind: 0
}),
Some(true),
None
),
Tier::Fired(MainState::None)
);
assert_eq!(
tier_integration_or_counts(
Some(AheadBehind {
ahead: 0,
behind: 0
}),
Some(false),
None
),
Tier::Fired(MainState::SameCommit)
);
assert_eq!(
tier_integration_or_counts(
Some(AheadBehind {
ahead: 3,
behind: 0
}),
Some(true),
None
),
Tier::Fired(MainState::Ahead)
);
assert_eq!(
tier_integration_or_counts(
Some(AheadBehind {
ahead: 3,
behind: 2
}),
Some(true),
None
),
Tier::Fired(MainState::Diverged)
);
assert_eq!(
tier_integration_or_counts(
Some(AheadBehind {
ahead: 5,
behind: 0
}),
Some(true),
Some(MainState::Integrated(IntegrationReason::Ancestor)),
),
Tier::Fired(MainState::Integrated(IntegrationReason::Ancestor))
);
}
#[test]
fn test_operation_state_display() {
assert_eq!(format!("{}", OperationState::None), "");
assert_eq!(format!("{}", OperationState::Conflicts), "✘");
assert_eq!(format!("{}", OperationState::Rebase), "⤴");
assert_eq!(format!("{}", OperationState::Merge), "⤵");
}
#[test]
fn test_operation_state_styled() {
use insta::assert_snapshot;
assert!(OperationState::None.styled().is_none());
assert_snapshot!(OperationState::Conflicts.styled().unwrap(), @"[31m✘[39m");
assert_snapshot!(OperationState::Rebase.styled().unwrap(), @"[33m⤴[39m");
assert_snapshot!(OperationState::Merge.styled().unwrap(), @"[33m⤵[39m");
}
#[test]
fn test_operation_state_serialize() {
let json = serde_json::to_string(&OperationState::None).unwrap();
assert_eq!(json, "\"\"");
let json = serde_json::to_string(&OperationState::Conflicts).unwrap();
assert_eq!(json, "\"✘\"");
}
#[test]
fn test_operation_state_as_json_str() {
assert_eq!(OperationState::None.as_json_str(), None);
assert_eq!(OperationState::Conflicts.as_json_str(), Some("conflicts"));
assert_eq!(OperationState::Rebase.as_json_str(), Some("rebase"));
assert_eq!(OperationState::Merge.as_json_str(), Some("merge"));
}
#[test]
fn test_git_operation_state_is_none() {
assert!(ActiveGitOperation::None.is_none());
assert!(ActiveGitOperation::default().is_none());
assert!(!ActiveGitOperation::Rebase.is_none());
assert!(!ActiveGitOperation::Merge.is_none());
}
}