#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StashPushPlan {
RefuseNoChanges,
Proceed,
}
pub fn plan_stash_push(worktree_clean: bool) -> StashPushPlan {
if worktree_clean {
StashPushPlan::RefuseNoChanges
} else {
StashPushPlan::Proceed
}
}
pub fn stash_push_should_refuse(worktree_clean: bool) -> bool {
matches!(
plan_stash_push(worktree_clean),
StashPushPlan::RefuseNoChanges
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StashEntryOpPlan {
RefuseEmpty,
Proceed,
}
pub fn plan_stash_entry_op(has_stash: bool) -> StashEntryOpPlan {
if has_stash {
StashEntryOpPlan::Proceed
} else {
StashEntryOpPlan::RefuseEmpty
}
}
pub fn stash_entry_op_should_refuse(has_stash: bool) -> bool {
matches!(
plan_stash_entry_op(has_stash),
StashEntryOpPlan::RefuseEmpty
)
}
pub fn stash_stack_is_empty(count: usize) -> bool {
count == 0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StashOutcomeStatus {
Stashed,
Applied,
AppliedAndDropped,
Dropped,
Cleared,
}
impl StashOutcomeStatus {
pub fn as_str(self) -> &'static str {
match self {
Self::Stashed => "stashed",
Self::Applied => "applied",
Self::AppliedAndDropped => "applied_and_dropped",
Self::Dropped => "dropped",
Self::Cleared => "cleared",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StashMessageMode {
Text,
Json,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StashMutationReport {
pub status: StashOutcomeStatus,
pub stash_index: Option<usize>,
pub cleared_count: Option<usize>,
}
impl StashMutationReport {
pub fn stashed(index: usize) -> Self {
Self {
status: StashOutcomeStatus::Stashed,
stash_index: Some(index),
cleared_count: None,
}
}
pub fn applied(index: usize) -> Self {
Self {
status: StashOutcomeStatus::Applied,
stash_index: Some(index),
cleared_count: None,
}
}
pub fn applied_and_dropped(index: usize) -> Self {
Self {
status: StashOutcomeStatus::AppliedAndDropped,
stash_index: Some(index),
cleared_count: None,
}
}
pub fn dropped(index: usize) -> Self {
Self {
status: StashOutcomeStatus::Dropped,
stash_index: Some(index),
cleared_count: None,
}
}
pub fn cleared(count: usize) -> Self {
Self {
status: StashOutcomeStatus::Cleared,
stash_index: None,
cleared_count: Some(count),
}
}
pub fn json_stash_index(&self) -> Option<usize> {
match self.status {
StashOutcomeStatus::Stashed | StashOutcomeStatus::Applied => self.stash_index,
StashOutcomeStatus::AppliedAndDropped
| StashOutcomeStatus::Dropped
| StashOutcomeStatus::Cleared => None,
}
}
}
pub fn stash_mutation_message(report: &StashMutationReport, mode: StashMessageMode) -> String {
match report.status {
StashOutcomeStatus::Stashed => {
format!("Saved stash@{{{}}}", report.stash_index.unwrap_or(0))
}
StashOutcomeStatus::Applied => {
format!("Applied stash@{{{}}}", report.stash_index.unwrap_or(0))
}
StashOutcomeStatus::AppliedAndDropped => match mode {
StashMessageMode::Json => "Applied and dropped stash".to_string(),
StashMessageMode::Text => format!(
"Applied and dropped stash@{{{}}}",
report.stash_index.unwrap_or(0)
),
},
StashOutcomeStatus::Dropped => {
format!("Dropped stash@{{{}}}", report.stash_index.unwrap_or(0))
}
StashOutcomeStatus::Cleared => {
let count = report.cleared_count.unwrap_or(0);
match mode {
StashMessageMode::Text if stash_stack_is_empty(count) => {
"No stashes to clear".to_string()
}
_ => format!("Cleared {count} stash(es)"),
}
}
}
}
pub const STASH_DEFAULT_LIST_MESSAGE: &str = "WIP on main";
pub fn stash_list_entry_message(message: Option<&str>) -> &str {
message.unwrap_or(STASH_DEFAULT_LIST_MESSAGE)
}
pub fn format_stash_list_line(index: usize, message: Option<&str>) -> String {
format!("stash@{{{index}}}: {}", stash_list_entry_message(message))
}
pub fn stash_list_is_empty(count: usize) -> bool {
stash_stack_is_empty(count)
}
pub fn stash_show_is_empty(change_count: usize) -> bool {
change_count == 0
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StashShowChangeKind {
Modified,
Added,
Deleted,
Unchanged,
}
pub fn stash_show_change_prefix(kind: StashShowChangeKind) -> Option<&'static str> {
match kind {
StashShowChangeKind::Modified => Some("M"),
StashShowChangeKind::Added => Some("A"),
StashShowChangeKind::Deleted => Some("D"),
StashShowChangeKind::Unchanged => None,
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct StashShowBuckets {
pub modified: Vec<String>,
pub added: Vec<String>,
pub deleted: Vec<String>,
}
pub fn bucket_stash_show_changes<'a, I>(changes: I) -> StashShowBuckets
where
I: IntoIterator<Item = (StashShowChangeKind, &'a str)>,
{
let mut buckets = StashShowBuckets::default();
for (kind, path) in changes {
match kind {
StashShowChangeKind::Modified => buckets.modified.push(path.to_string()),
StashShowChangeKind::Added => buckets.added.push(path.to_string()),
StashShowChangeKind::Deleted => buckets.deleted.push(path.to_string()),
StashShowChangeKind::Unchanged => {}
}
}
buckets
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_refuses_only_when_worktree_clean() {
assert_eq!(plan_stash_push(true), StashPushPlan::RefuseNoChanges);
assert_eq!(plan_stash_push(false), StashPushPlan::Proceed);
assert!(stash_push_should_refuse(true));
assert!(!stash_push_should_refuse(false));
}
#[test]
fn entry_ops_refuse_when_stack_empty() {
assert_eq!(plan_stash_entry_op(false), StashEntryOpPlan::RefuseEmpty);
assert_eq!(plan_stash_entry_op(true), StashEntryOpPlan::Proceed);
assert!(stash_entry_op_should_refuse(false));
assert!(!stash_entry_op_should_refuse(true));
assert!(stash_stack_is_empty(0));
assert!(!stash_stack_is_empty(1));
}
#[test]
fn outcome_status_tokens_are_stable() {
assert_eq!(StashOutcomeStatus::Stashed.as_str(), "stashed");
assert_eq!(StashOutcomeStatus::Applied.as_str(), "applied");
assert_eq!(
StashOutcomeStatus::AppliedAndDropped.as_str(),
"applied_and_dropped"
);
assert_eq!(StashOutcomeStatus::Dropped.as_str(), "dropped");
assert_eq!(StashOutcomeStatus::Cleared.as_str(), "cleared");
}
#[test]
fn mutation_messages_match_historical_cli() {
let stashed = StashMutationReport::stashed(0);
assert_eq!(
stash_mutation_message(&stashed, StashMessageMode::Text),
"Saved stash@{0}"
);
assert_eq!(stashed.json_stash_index(), Some(0));
let applied = StashMutationReport::applied(2);
assert_eq!(
stash_mutation_message(&applied, StashMessageMode::Json),
"Applied stash@{2}"
);
assert_eq!(applied.json_stash_index(), Some(2));
let pop = StashMutationReport::applied_and_dropped(1);
assert_eq!(
stash_mutation_message(&pop, StashMessageMode::Text),
"Applied and dropped stash@{1}"
);
assert_eq!(
stash_mutation_message(&pop, StashMessageMode::Json),
"Applied and dropped stash"
);
assert_eq!(pop.json_stash_index(), None);
let dropped = StashMutationReport::dropped(3);
assert_eq!(
stash_mutation_message(&dropped, StashMessageMode::Text),
"Dropped stash@{3}"
);
assert_eq!(dropped.json_stash_index(), None);
let cleared_n = StashMutationReport::cleared(2);
assert_eq!(
stash_mutation_message(&cleared_n, StashMessageMode::Text),
"Cleared 2 stash(es)"
);
let cleared_0 = StashMutationReport::cleared(0);
assert_eq!(
stash_mutation_message(&cleared_0, StashMessageMode::Text),
"No stashes to clear"
);
assert_eq!(
stash_mutation_message(&cleared_0, StashMessageMode::Json),
"Cleared 0 stash(es)"
);
assert_eq!(cleared_0.json_stash_index(), None);
}
#[test]
fn list_and_show_helpers() {
assert_eq!(stash_list_entry_message(None), "WIP on main");
assert_eq!(stash_list_entry_message(Some("wip")), "wip");
assert_eq!(format_stash_list_line(0, None), "stash@{0}: WIP on main");
assert!(stash_list_is_empty(0));
assert!(stash_show_is_empty(0));
assert!(!stash_show_is_empty(2));
assert_eq!(
stash_show_change_prefix(StashShowChangeKind::Modified),
Some("M")
);
assert_eq!(
stash_show_change_prefix(StashShowChangeKind::Unchanged),
None
);
let buckets = bucket_stash_show_changes([
(StashShowChangeKind::Modified, "a.rs"),
(StashShowChangeKind::Added, "b.rs"),
(StashShowChangeKind::Deleted, "c.rs"),
(StashShowChangeKind::Unchanged, "d.rs"),
]);
assert_eq!(buckets.modified, vec!["a.rs".to_string()]);
assert_eq!(buckets.added, vec!["b.rs".to_string()]);
assert_eq!(buckets.deleted, vec!["c.rs".to_string()]);
}
}