mod graphite;
use std::collections::{BTreeMap, HashMap};
use git2::Repository;
use crate::error::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StackModel {
None,
Graphite,
}
impl StackModel {
pub fn detect(repo: &Repository) -> Self {
if graphite::detect_gt() && graphite::is_graphite_repo(repo) {
Self::Graphite
} else {
Self::None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Granularity {
Stack,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Stack {
pub trunk: String,
pub diffs: Vec<String>,
pub current: String,
pub parents: HashMap<String, String>,
}
pub fn enumerate_stacks(repo: &Repository, model: StackModel) -> Result<Vec<Stack>> {
match model {
StackModel::None => Ok(vec![]),
StackModel::Graphite => graphite::enumerate_stacks(repo).map_err(Into::into),
}
}
pub fn current_stack(
repo: &Repository,
head_branch: &str,
model: StackModel,
) -> Result<Option<Stack>> {
match model {
StackModel::None => Ok(None),
StackModel::Graphite => graphite::current_stack(repo, head_branch).map_err(Into::into),
}
}
pub fn is_graphite_active(repo: &Repository) -> bool {
graphite::detect_gt() && graphite::is_graphite_repo(repo)
}
pub fn graphite_trunk(repo: &Repository) -> Option<String> {
graphite::graphite_trunk(repo)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StackGroup {
pub stack: Stack,
pub members: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StackGrouping {
pub groups: Vec<StackGroup>,
pub ungrouped: Vec<usize>,
}
pub fn group_by_stack(stacks: &[Option<Stack>]) -> StackGrouping {
let mut map: BTreeMap<(String, Vec<String>), (Stack, Vec<usize>)> = BTreeMap::new();
let mut ungrouped: Vec<usize> = Vec::new();
for (i, opt) in stacks.iter().enumerate() {
match opt {
None => ungrouped.push(i),
Some(stack) => {
let mut key_branches = stack.diffs.clone();
key_branches.sort();
let key = (stack.trunk.clone(), key_branches);
let entry = map
.entry(key)
.or_insert_with(|| (stack.clone(), Vec::new()));
entry.1.push(i);
}
}
}
let groups = map
.into_values()
.map(|(rep_stack, mut members)| {
members.sort_by_key(|&idx| {
let branch = stacks[idx]
.as_ref()
.map(|s| s.current.as_str())
.unwrap_or("");
rep_stack
.diffs
.iter()
.position(|b| b == branch)
.unwrap_or(usize::MAX)
});
StackGroup {
stack: rep_stack,
members,
}
})
.collect();
StackGrouping { groups, ungrouped }
}
#[cfg(test)]
mod tests {
use super::*;
fn stack(trunk: &str, diffs: &[&str], current: &str) -> Stack {
Stack {
trunk: trunk.to_string(),
diffs: diffs.iter().map(|s| s.to_string()).collect(),
current: current.to_string(),
parents: HashMap::new(),
}
}
#[test]
fn empty_input_yields_empty_grouping() {
let result = group_by_stack(&[]);
assert!(result.groups.is_empty());
assert!(result.ungrouped.is_empty());
}
#[test]
fn all_none_yields_all_ungrouped() {
let stacks: Vec<Option<Stack>> = vec![None, None, None];
let result = group_by_stack(&stacks);
assert!(result.groups.is_empty());
assert_eq!(result.ungrouped, vec![0, 1, 2]);
}
#[test]
fn single_stacked_worktree_forms_one_group() {
let stacks = vec![Some(stack("main", &["feat-a"], "feat-a"))];
let result = group_by_stack(&stacks);
assert!(result.ungrouped.is_empty());
assert_eq!(result.groups.len(), 1);
assert_eq!(result.groups[0].members, vec![0]);
assert_eq!(result.groups[0].stack.trunk, "main");
}
#[test]
fn two_worktrees_same_stack_collapse_into_one_group_ordered_bottom_to_top() {
let stacks = vec![
Some(stack("main", &["feat-b", "feat-a"], "feat-a")), Some(stack("main", &["feat-b", "feat-a"], "feat-b")), ];
let result = group_by_stack(&stacks);
assert!(result.ungrouped.is_empty());
assert_eq!(result.groups.len(), 1);
assert_eq!(result.groups[0].members, vec![1, 0]);
}
#[test]
fn same_stack_different_branches_vector_order_still_collapses() {
let stacks = vec![
Some(stack("main", &["feat-a", "feat-b"], "feat-a")),
Some(stack("main", &["feat-b", "feat-a"], "feat-b")),
];
let result = group_by_stack(&stacks);
assert_eq!(
result.groups.len(),
1,
"different branch vector order must still collapse"
);
assert!(result.ungrouped.is_empty());
}
#[test]
fn two_distinct_stacks_on_same_trunk_stay_separate() {
let stacks = vec![
Some(stack("main", &["stack1-a"], "stack1-a")),
Some(stack("main", &["stack2-a"], "stack2-a")),
];
let result = group_by_stack(&stacks);
assert_eq!(result.groups.len(), 2);
assert!(result.ungrouped.is_empty());
}
#[test]
fn mixed_stacked_and_ungrouped() {
let stacks = vec![
None, Some(stack("main", &["feat-a", "feat-b"], "feat-a")),
None, Some(stack("main", &["feat-a", "feat-b"], "feat-b")),
];
let result = group_by_stack(&stacks);
assert_eq!(result.ungrouped, vec![0, 2]);
assert_eq!(result.groups.len(), 1);
assert_eq!(result.groups[0].members, vec![1, 3]);
}
#[test]
fn groups_ordered_deterministically_by_trunk_then_branch_set() {
let stacks = vec![
Some(stack("main", &["z-feat"], "z-feat")),
Some(stack("dev", &["d-feat"], "d-feat")),
Some(stack("main", &["a-feat"], "a-feat")),
];
let result = group_by_stack(&stacks);
assert_eq!(result.groups.len(), 3);
assert_eq!(result.groups[0].stack.trunk, "dev");
assert_eq!(result.groups[1].stack.diffs, vec!["a-feat"]);
assert_eq!(result.groups[2].stack.diffs, vec!["z-feat"]);
}
}