use anyhow::{Context, Result};
use std::collections::HashMap;
use std::io::{self, Write};
use std::process::{Command, Stdio};
#[derive(Debug)]
pub struct CodeReferences {
pub matches: HashMap<String, Vec<(usize, String)>>,
pub total_matches: usize,
}
impl CodeReferences {
fn new() -> Self {
Self {
matches: HashMap::new(),
total_matches: 0,
}
}
}
pub fn is_interactive_tty() -> bool {
use std::io::IsTerminal;
io::stdin().is_terminal() && io::stdout().is_terminal()
}
pub fn find_code_references(issue_id: &str) -> Result<CodeReferences> {
let pattern = format!(r"\<{}\>", regex::escape(issue_id));
let output = Command::new("git")
.args([
"grep",
"-n",
"-I",
"--no-color",
&pattern,
"--",
":(exclude).beads/*",
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.context("Failed to execute git grep")?;
let mut references = CodeReferences::new();
if !output.status.success() {
if output.status.code() == Some(1) {
return Ok(references);
}
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git grep failed: {}", stderr);
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if let Some((file_and_line, content)) = line.split_once(':') {
if let Some((file, line_num_str)) = file_and_line.rsplit_once(':') {
if let Ok(line_num) = line_num_str.parse::<usize>() {
references
.matches
.entry(file.to_string())
.or_default()
.push((line_num, content.to_string()));
references.total_matches += 1;
}
}
}
}
Ok(references)
}
pub fn confirm_code_patch(old_id: &str, new_id: &str, references: &CodeReferences) -> Result<bool> {
println!(
"\nFound {} reference(s) to {} in code:",
references.total_matches, old_id
);
println!();
let mut files: Vec<&String> = references.matches.keys().collect();
files.sort();
for file in files {
let matches = &references.matches[file];
println!(" {}:", file);
for (line_num, content) in matches {
println!(" {}: {}", line_num, content.trim());
}
println!();
}
println!(
"Do you want to replace all occurrences of {} with {} in these files? [Y/n]",
old_id, new_id
);
print!("> ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim().to_lowercase();
Ok(input.is_empty() || input == "y" || input == "yes")
}
pub fn patch_code_files(old_id: &str, new_id: &str, references: &CodeReferences) -> Result<usize> {
let mut files_patched = 0;
let sed_pattern = format!(r"s/\<{}\>/{}/g", regex::escape(old_id), new_id);
for file in references.matches.keys() {
let status = Command::new("sed")
.args(["-i", &sed_pattern, file])
.status()
.context(format!("Failed to patch file: {}", file))?;
if !status.success() {
anyhow::bail!("sed failed to patch file: {}", file);
}
files_patched += 1;
}
Ok(files_patched)
}
pub fn patch_code_for_rename(old_id: &str, new_id: &str) -> Result<Option<usize>> {
if !is_interactive_tty() {
eprintln!("Warning: --mb-patch-code requires an interactive TTY. Skipping code patching.");
return Ok(None);
}
let references = find_code_references(old_id)?;
if references.total_matches == 0 {
return Ok(Some(0));
}
if !confirm_code_patch(old_id, new_id, &references)? {
println!("Skipping code patching.");
return Ok(Some(0));
}
let files_patched = patch_code_files(old_id, new_id, &references)?;
println!("Patched {} file(s) in working copy.", files_patched);
Ok(Some(files_patched))
}
pub fn patch_code_for_migration(id_mapping: &HashMap<String, String>) -> Result<usize> {
if !is_interactive_tty() {
eprintln!("Warning: --mb-patch-code requires an interactive TTY. Skipping code patching.");
return Ok(0);
}
let mut total_files_patched = 0;
for (old_id, new_id) in id_mapping {
let references = find_code_references(old_id)?;
if references.total_matches == 0 {
continue;
}
if !confirm_code_patch(old_id, new_id, &references)? {
println!("Skipping {} -> {}", old_id, new_id);
continue;
}
let files_patched = patch_code_files(old_id, new_id, &references)?;
total_files_patched += files_patched;
println!(
"Patched {} file(s) for {} -> {}",
files_patched, old_id, new_id
);
}
if total_files_patched > 0 {
println!(
"\nTotal: patched {} file(s) in working copy.",
total_files_patched
);
}
Ok(total_files_patched)
}