use std::collections::{HashMap, HashSet, VecDeque};
use std::process::{Command, Stdio};
use git2::Repository;
use serde_json::Value;
use super::Stack;
use crate::error::StackError;
pub fn detect_gt() -> bool {
Command::new("gt")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub fn is_graphite_repo(repo: &Repository) -> bool {
repo.path().join(".graphite_repo_config").exists()
}
fn read_trunks(repo: &Repository) -> Vec<String> {
let path = repo.path().join(".graphite_repo_config");
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return vec!["main".to_string()],
};
let Ok(json) = serde_json::from_str::<Value>(&content) else {
return vec!["main".to_string()];
};
if let Some(trunks) = json.get("trunks").and_then(|t| t.as_array()) {
let names: Vec<String> = trunks
.iter()
.filter_map(|t| t.get("name").and_then(|n| n.as_str()))
.map(String::from)
.collect();
if !names.is_empty() {
return names;
}
}
if let Some(trunk) = json.get("trunk").and_then(|t| t.as_str()) {
return vec![trunk.to_string()];
}
vec!["main".to_string()]
}
fn build_parent_map(repo: &Repository) -> Result<HashMap<String, String>, StackError> {
let mut map = HashMap::new();
let references = repo
.references_glob("refs/branch-metadata/*")
.map_err(|e| StackError::GtParseFailed {
message: format!("failed to list branch-metadata refs: {e}"),
})?;
for reference in references {
let reference = reference.map_err(|e| StackError::GtParseFailed {
message: format!("failed to read branch-metadata ref: {e}"),
})?;
let Some(refname) = reference.name() else {
continue;
};
let Some(branch) = refname.strip_prefix("refs/branch-metadata/") else {
continue;
};
if branch.is_empty() {
continue;
}
let Ok(object) = reference.peel(git2::ObjectType::Blob) else {
continue;
};
let Ok(blob) = object.into_blob() else {
continue;
};
let Ok(json) = serde_json::from_slice::<Value>(blob.content()) else {
continue;
};
if let Some(parent) = json.get("parentBranchName").and_then(|p| p.as_str()) {
map.insert(branch.to_string(), parent.to_string());
}
}
Ok(map)
}
pub fn current_stack(repo: &Repository, head_branch: &str) -> Result<Option<Stack>, StackError> {
let parent_map = build_parent_map(repo)?;
if !parent_map.contains_key(head_branch) {
return Ok(None);
}
let trunks: HashSet<String> = read_trunks(repo).into_iter().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 parent_map.get(&walk) {
Some(parent) => {
if !upward_seen.insert(parent.clone()) {
break walk.clone();
}
ancestors.push(walk.clone());
walk = parent.clone();
}
None => break walk.clone(),
}
};
ancestors.reverse();
let stack_root = match ancestors.first() {
Some(r) => r.clone(),
None => return Ok(None), };
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 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());
}
}
}
Ok(Some(Stack {
trunk,
branches: stack_branches,
current: head_branch.to_string(),
}))
}