use std::path::Path;
use std::process::Command;
use crate::index::Parser;
use crate::storage::sqlite::TouchedSymbol;
pub(super) fn bead_json(bead_id: &str) -> Option<serde_json::Value> {
let out = Command::new("bd")
.args(["show", bead_id, "--json"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let v: serde_json::Value = serde_json::from_slice(&out.stdout).ok()?;
match v {
serde_json::Value::Array(arr) => arr.into_iter().next(),
other => Some(other),
}
}
pub(super) fn bundle_slugs_from_labels(bead_id: &str) -> Option<String> {
let v = bead_json(bead_id)?;
let labels = v.get("labels")?.as_array()?;
let slugs: Vec<String> = labels
.iter()
.filter_map(|l| l.as_str())
.filter_map(|l| l.strip_prefix("b:"))
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
.collect();
if slugs.is_empty() {
None
} else {
Some(slugs.join(","))
}
}
pub(super) fn resolve_feature_id(bead_id: &str) -> Option<String> {
use std::collections::HashSet;
let mut visited: HashSet<String> = HashSet::new();
let mut frontier: Vec<String> = vec![bead_id.to_string()];
let mut depth = 0;
while !frontier.is_empty() && depth < 10 {
let mut next: Vec<String> = Vec::new();
for id in frontier {
if !visited.insert(id.clone()) {
continue;
}
let v = match bead_json(&id) {
Some(v) => v,
None => continue,
};
if let Some(deps) = v.get("dependencies").and_then(|d| d.as_array()) {
for dep in deps {
let dep_id = match dep.get("id").and_then(|i| i.as_str()) {
Some(i) => i,
None => continue,
};
let dep_type = dep.get("issue_type").and_then(|t| t.as_str()).unwrap_or("");
if dep_type == "feature" {
return Some(dep_id.to_string());
}
next.push(dep_id.to_string());
}
}
}
frontier = next;
depth += 1;
}
None
}
pub(super) fn extract_touched_symbols(
repo_root: &Path,
sha: &str,
files: &[String],
) -> Vec<TouchedSymbol> {
let mut parser = match Parser::new() {
Ok(p) => p,
Err(_) => return Vec::new(),
};
let mut out = Vec::new();
for file in files {
let blob = Command::new("git")
.current_dir(repo_root)
.args(["show", &format!("{}:{}", sha, file)])
.output();
let content = match blob {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).into_owned(),
_ => continue, };
let chunks = match parser.parse_file(Path::new(file), &content) {
Ok(c) => c,
Err(_) => continue,
};
for chunk in chunks {
if let Some(name) = chunk.name {
out.push(TouchedSymbol {
file: file.clone(),
symbol: name,
kind: chunk.chunk_type.to_string(),
});
}
}
}
out
}