use anyhow::Result;
use colored::Colorize;
use std::path::Path;
use std::process::Command;
use crate::storage::sqlite::{MetadataStore, NewBeadLineage};
use super::causality::commit_numstat;
use super::helpers::*;
use super::LinkOutput;
use crate::cli::OutputConfig;
pub(super) fn run_auto_link(
repo_root: &Path,
store: &MetadataStore,
commit_ref: &str,
output: &OutputConfig,
) -> Result<()> {
let sha = match resolve_commit_sha(repo_root, commit_ref) {
Some(s) => s,
None => return Ok(()),
};
let message = commit_message(repo_root, &sha).unwrap_or_default();
let branch = current_branch(repo_root);
let bead_id = match extract_bead_id(&message, branch.as_deref()) {
Some(b) => b,
None => return Ok(()), };
let existing = store.list_bead_lineage(Some(&bead_id), Some(&sha), 50)?;
if existing
.iter()
.any(|r| r.action_type.as_deref() == Some("commit"))
{
if !output.quiet && !output.json {
println!(
"{} {} already linked to {}",
"•".dimmed(),
bead_id.cyan(),
(&sha[..sha.len().min(8)]).yellow()
);
}
return Ok(());
}
let (touched_files, lines_added, lines_deleted) = match commit_numstat(repo_root, &sha) {
Ok((files, added, deleted)) => (files, Some(added), Some(deleted)),
Err(_) => (Vec::new(), None, None),
};
let bundle_slugs = bundle_slugs_from_labels(&bead_id);
let feature_id = resolve_feature_id(&bead_id);
let touched_symbols = extract_touched_symbols(repo_root, &sha, &touched_files);
let id = store.record_bead_lineage(&NewBeadLineage {
bead_id: bead_id.clone(),
bead_type: None,
commit_sha: Some(sha.clone()),
bundle_slugs,
touched_files: touched_files.clone(),
action_type: Some("commit".to_string()),
feature_id,
lines_added,
lines_deleted,
touched_symbols,
})?;
if output.json {
let out = LinkOutput {
id,
bead_id,
commit_sha: Some(sha),
touched_files,
};
println!("{}", serde_json::to_string_pretty(&out)?);
} else if !output.quiet {
println!(
"{} Auto-linked {} → {} ({} file{})",
"✓".green(),
bead_id.cyan(),
(&sha[..sha.len().min(8)]).yellow(),
touched_files.len(),
if touched_files.len() == 1 { "" } else { "s" },
);
}
Ok(())
}
fn resolve_commit_sha(repo_root: &Path, commit_ref: &str) -> Option<String> {
let out = Command::new("git")
.current_dir(repo_root)
.args(["rev-parse", "--verify", commit_ref])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let sha = String::from_utf8_lossy(&out.stdout).trim().to_string();
if sha.is_empty() {
None
} else {
Some(sha)
}
}
fn commit_message(repo_root: &Path, sha: &str) -> Option<String> {
let out = Command::new("git")
.current_dir(repo_root)
.args(["show", "-s", "--format=%B", sha])
.output()
.ok()?;
if !out.status.success() {
return None;
}
Some(String::from_utf8_lossy(&out.stdout).to_string())
}
fn current_branch(repo_root: &Path) -> Option<String> {
let out = Command::new("git")
.current_dir(repo_root)
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let b = String::from_utf8_lossy(&out.stdout).trim().to_string();
if b.is_empty() || b == "HEAD" {
None
} else {
Some(b)
}
}
pub(super) fn extract_bead_id(message: &str, branch: Option<&str>) -> Option<String> {
let subject = message.lines().next().unwrap_or("");
for line in message.lines() {
let mut parts = line.trim().splitn(2, ':');
if let (Some(key), Some(val)) = (parts.next(), parts.next()) {
if key.trim().eq_ignore_ascii_case("bead") {
if let Some(id) = find_bead_id(val) {
return Some(id);
}
}
}
}
if let Some(id) = parenthesized_bead_id(subject) {
return Some(id);
}
if let Some(id) = find_bead_id(subject) {
return Some(id);
}
branch.and_then(find_bead_id)
}
pub(super) fn parenthesized_bead_id(subject: &str) -> Option<String> {
let mut depth = 0usize;
let mut start = 0usize;
let mut groups: Vec<&str> = Vec::new();
for (i, c) in subject.char_indices() {
if c == '(' {
if depth == 0 {
start = i + 1;
}
depth += 1;
} else if c == ')' && depth > 0 {
depth -= 1;
if depth == 0 {
groups.push(&subject[start..i]);
}
}
}
groups.iter().find_map(|g| find_bead_id(g))
}
pub(super) fn find_bead_id(text: &str) -> Option<String> {
let chars: Vec<char> = text.chars().collect();
let n = chars.len();
let mut i = 0;
while i < n {
let at_boundary = i == 0 || (!chars[i - 1].is_ascii_alphanumeric() && chars[i - 1] != '-');
if at_boundary && chars[i].is_ascii_lowercase() {
let mut j = i;
while j < n && chars[j].is_ascii_lowercase() {
j += 1;
}
if j < n && chars[j] == '-' {
let suffix_start = j + 1;
let mut k = suffix_start;
while k < n && chars[k].is_ascii_alphanumeric() {
k += 1;
}
if k - suffix_start >= 3 {
return Some(chars[i..k].iter().collect());
}
}
i = j.max(i + 1);
continue;
}
i += 1;
}
None
}