use std::collections::{HashMap, HashSet, VecDeque};
use std::path::Path;
use std::process::{Command, Stdio};
use git2::Repository;
use rusqlite::OpenFlags;
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 {
let git_dir = repo.path();
git_dir.join(".graphite_metadata.db").exists() || git_dir.join(".graphite_repo_config").exists()
}
pub fn graphite_trunk(repo: &Repository) -> Option<String> {
let path = repo.path().join(".graphite_repo_config");
let content = std::fs::read_to_string(&path).ok()?;
let json = serde_json::from_str::<Value>(&content).ok()?;
if let Some(trunks) = json.get("trunks").and_then(|t| t.as_array()) {
let first = trunks
.iter()
.find_map(|t| t.get("name").and_then(|n| n.as_str()))
.map(String::from);
if first.is_some() {
return first;
}
}
json.get("trunk").and_then(|t| t.as_str()).map(String::from)
}
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 db_path = repo.path().join(".graphite_metadata.db");
if db_path.exists() {
if let Ok(map) = build_parent_map_from_sqlite(&db_path) {
return Ok(map);
}
}
build_parent_map_from_refs(repo)
}
fn build_parent_map_from_sqlite(db_path: &Path) -> Result<HashMap<String, String>, StackError> {
let conn = rusqlite::Connection::open_with_flags(
db_path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|e| StackError::GtParseFailed {
message: format!("failed to open .graphite_metadata.db: {e}"),
})?;
let mut stmt = conn
.prepare(
"SELECT branch_name, parent_branch_name \
FROM branch_metadata \
WHERE parent_branch_name IS NOT NULL AND parent_branch_name != ''",
)
.map_err(|e| StackError::GtParseFailed {
message: format!("failed to query .graphite_metadata.db: {e}"),
})?;
let mut map = HashMap::new();
let rows = stmt
.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})
.map_err(|e| StackError::GtParseFailed {
message: format!("failed to read .graphite_metadata.db rows: {e}"),
})?;
for row in rows.flatten() {
map.insert(row.0, row.1);
}
Ok(map)
}
fn build_parent_map_from_refs(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 Ok(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 enumerate_stacks(repo: &Repository) -> Result<Vec<Stack>, StackError> {
let parent_map = build_parent_map(repo)?;
if parent_map.is_empty() {
return Ok(vec![]);
}
let trunks: HashSet<String> = read_trunks(repo).into_iter().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: std::collections::HashMap<String, String> = diffs
.iter()
.filter_map(|b| parent_map.get(b).map(|p| (b.clone(), p.clone())))
.collect();
stacks.push(Stack {
trunk,
diffs,
current,
parents,
});
}
}
Ok(stacks)
}
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());
}
}
}
let parents: std::collections::HashMap<String, String> = stack_branches
.iter()
.filter_map(|b| parent_map.get(b).map(|p| (b.clone(), p.clone())))
.collect();
Ok(Some(Stack {
trunk,
diffs: stack_branches,
current: head_branch.to_string(),
parents,
}))
}