use std::collections::{HashMap, HashSet, VecDeque};
use git2::Repository;
use super::Stack;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct BranchMetadata {
pub parent: String,
pub parent_revision: Option<String>,
}
pub(crate) struct StackMetadata {
pub trunks: Vec<String>,
pub parents: HashMap<String, BranchMetadata>,
pub pr_titles: HashMap<String, String>,
pub stack_numbers: HashMap<String, u64>,
}
pub(crate) fn enumerate(repo: &Repository, meta: &StackMetadata) -> Vec<Stack> {
let mut parent_map: HashMap<String, String> = meta
.parents
.iter()
.map(|(branch, m)| (branch.clone(), m.parent.clone()))
.collect();
if parent_map.is_empty() {
return vec![];
}
parent_map.retain(|branch, _| crate::resolve::branch_exists(repo, branch));
if parent_map.is_empty() {
return vec![];
}
let trunks: HashSet<String> = meta.trunks.iter().cloned().collect();
let mut reverse_map: HashMap<String, Vec<String>> = HashMap::new();
for (branch, parent) in &parent_map {
reverse_map
.entry(parent.clone())
.or_default()
.push(branch.clone());
}
let mut root_branches: Vec<String> = parent_map
.iter()
.filter(|(_, p)| trunks.contains(*p))
.map(|(b, _)| b.clone())
.collect();
root_branches.sort();
let mut stacks: Vec<Stack> = Vec::new();
let mut visited: HashSet<String> = HashSet::new();
for root in root_branches {
if visited.contains(&root) {
continue;
}
let trunk = parent_map.get(&root).cloned().unwrap_or_default();
let mut diffs: Vec<String> = Vec::new();
let mut queue: VecDeque<String> = VecDeque::new();
queue.push_back(root);
while let Some(branch) = queue.pop_front() {
if !visited.insert(branch.clone()) {
continue;
}
if trunks.contains(&branch) {
continue;
}
diffs.push(branch.clone());
if let Some(children) = reverse_map.get(&branch) {
let mut sorted = children.clone();
sorted.sort();
for child in sorted {
queue.push_back(child);
}
}
}
if !diffs.is_empty() {
let current = diffs[0].clone();
let parents: HashMap<String, String> = diffs
.iter()
.filter_map(|b| parent_map.get(b).map(|p| (b.clone(), p.clone())))
.collect();
let number = diffs
.iter()
.find_map(|b| meta.stack_numbers.get(b).copied());
stacks.push(Stack {
trunk,
diffs,
current,
parents,
number,
});
}
}
stacks
}
pub(crate) fn current(meta: &StackMetadata, head_branch: &str) -> Option<Stack> {
if !meta.parents.contains_key(head_branch) {
return None;
}
let trunks: HashSet<String> = meta.trunks.iter().cloned().collect();
let mut walk = head_branch.to_string();
let mut ancestors: Vec<String> = Vec::new();
let mut upward_seen: HashSet<String> = HashSet::new();
upward_seen.insert(walk.clone());
let trunk = loop {
if trunks.contains(&walk) {
break walk.clone();
}
match meta.parents.get(&walk) {
Some(entry) => {
if !upward_seen.insert(entry.parent.clone()) {
break walk.clone();
}
ancestors.push(walk.clone());
walk = entry.parent.clone();
}
None => break walk.clone(),
}
};
ancestors.reverse();
let stack_root = ancestors.first()?.clone();
let mut reverse_map: HashMap<String, Vec<String>> = HashMap::new();
for (branch, entry) in &meta.parents {
reverse_map
.entry(entry.parent.clone())
.or_default()
.push(branch.clone());
}
let mut stack_branches: Vec<String> = Vec::new();
let mut queue: VecDeque<String> = VecDeque::new();
let mut visited: HashSet<String> = HashSet::new();
queue.push_back(stack_root);
while let Some(branch) = queue.pop_front() {
if !visited.insert(branch.clone()) {
continue;
}
if trunks.contains(&branch) {
continue;
}
stack_branches.push(branch.clone());
if let Some(children) = reverse_map.get(&branch) {
for child in children {
queue.push_back(child.clone());
}
}
}
let parents: HashMap<String, String> = stack_branches
.iter()
.filter_map(|b| {
meta.parents
.get(b)
.map(|entry| (b.clone(), entry.parent.clone()))
})
.collect();
let number = stack_branches
.iter()
.find_map(|b| meta.stack_numbers.get(b).copied());
Some(Stack {
trunk,
diffs: stack_branches,
current: head_branch.to_string(),
parents,
number,
})
}
pub(crate) fn changeset_walk(meta: &StackMetadata, head_branch: &str) -> Vec<String> {
let trunks: HashSet<String> = meta.trunks.iter().cloned().collect();
let mut ancestors_desc: Vec<String> = Vec::new();
{
let mut walk = head_branch.to_string();
let mut seen: HashSet<String> = HashSet::new();
seen.insert(walk.clone());
loop {
if trunks.contains(&walk) {
break;
}
let Some(entry) = meta.parents.get(&walk) else {
break;
};
let parent = entry.parent.clone();
if trunks.contains(&parent) {
break;
}
if !meta.parents.contains_key(&parent) {
break;
}
if !seen.insert(parent.clone()) {
break; }
ancestors_desc.push(parent.clone());
walk = parent;
}
}
ancestors_desc.reverse();
let mut reverse_map: HashMap<String, Vec<String>> = HashMap::new();
for (branch, entry) in &meta.parents {
reverse_map
.entry(entry.parent.clone())
.or_default()
.push(branch.clone());
}
for children in reverse_map.values_mut() {
children.sort();
}
fn visit_descendants(
branch: &str,
reverse_map: &HashMap<String, Vec<String>>,
visited: &mut HashSet<String>,
out: &mut Vec<String>,
) {
if let Some(children) = reverse_map.get(branch) {
for child in children {
if visited.insert(child.clone()) {
out.push(child.clone());
visit_descendants(child, reverse_map, visited, out);
}
}
}
}
let mut descendants: Vec<String> = Vec::new();
let mut visited: HashSet<String> = HashSet::new();
visited.insert(head_branch.to_string());
visit_descendants(head_branch, &reverse_map, &mut visited, &mut descendants);
ancestors_desc
.into_iter()
.chain(std::iter::once(head_branch.to_string()))
.chain(descendants)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use git_workon_fixture::prelude::*;
fn meta(trunk: &str, parents: &[(&str, &str)], numbers: &[(&str, u64)]) -> StackMetadata {
StackMetadata {
trunks: vec![trunk.to_string()],
parents: parents
.iter()
.map(|(branch, parent)| {
(
branch.to_string(),
BranchMetadata {
parent: parent.to_string(),
parent_revision: None,
},
)
})
.collect(),
pr_titles: HashMap::new(),
stack_numbers: numbers
.iter()
.map(|(branch, n)| (branch.to_string(), *n))
.collect(),
}
}
#[test]
fn enumerate_sets_stack_number_from_any_member_branch() {
let fixture = FixtureBuilder::new()
.branch("feat-a")
.branch("feat-b")
.build()
.unwrap();
let repo = fixture.repo().unwrap();
let meta = meta(
"main",
&[("feat-a", "main"), ("feat-b", "feat-a")],
&[("feat-b", 12)],
);
let stacks = enumerate(repo, &meta);
assert_eq!(stacks.len(), 1);
assert_eq!(stacks[0].number, Some(12));
}
#[test]
fn enumerate_leaves_number_none_when_unnumbered() {
let fixture = FixtureBuilder::new().branch("feat-a").build().unwrap();
let repo = fixture.repo().unwrap();
let meta = meta("main", &[("feat-a", "main")], &[]);
let stacks = enumerate(repo, &meta);
assert_eq!(stacks.len(), 1);
assert_eq!(stacks[0].number, None);
}
#[test]
fn current_sets_stack_number_from_any_member_branch() {
let meta = meta(
"main",
&[("feat-a", "main"), ("feat-b", "feat-a")],
&[("feat-a", 7)],
);
let stack = current(&meta, "feat-b").expect("feat-b is tracked");
assert_eq!(stack.number, Some(7));
}
}