#![cfg_attr(coverage_nightly, coverage(off))]
use super::display::format_history_output;
use super::TdgCommandConfig;
use crate::cli::TdgOutputFormat;
use crate::tdg::TdgAnalyzer;
use anyhow::{anyhow, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
pub(super) async fn handle_history_command(
analyzer: &TdgAnalyzer,
commit: Option<String>,
since: Option<String>,
range: Option<String>,
path_filter: Option<PathBuf>,
format: TdgOutputFormat,
config: &TdgCommandConfig,
) -> Result<()> {
let storage = analyzer
.storage()
.ok_or_else(|| anyhow!("TDG storage not initialized. Run with --with-git-context flag."))?;
let mut records = query_history_records(storage, commit, since, range, &config.path).await?;
if let Some(target_path) = path_filter {
records.retain(|r| r.identity.path == target_path);
}
if records.is_empty() {
println!("No TDG history found matching criteria.");
return Ok(());
}
let output_str = format_history_output(&records, format)?;
match &config.output {
Some(output_path) => std::fs::write(output_path, output_str)?,
None => println!("{output_str}"),
}
Ok(())
}
async fn query_history_records(
storage: &crate::tdg::TieredStore,
commit: Option<String>,
since: Option<String>,
range: Option<String>,
repo_path: &Path,
) -> Result<Vec<crate::tdg::FullTdgRecord>> {
if let Some(commit_ref) = commit {
let found: Vec<crate::tdg::FullTdgRecord> = storage.get_by_commit(&commit_ref).await?;
if found.is_empty() {
return Err(anyhow!(
"No TDG data found for commit '{}'. Ensure TDG was run with --with-git-context.",
commit_ref
));
}
return Ok(found);
}
let all_records = storage.get_all_with_git_context().await?;
if let Some(since_ref) = since {
return filter_by_git_since(&since_ref, all_records, repo_path);
}
if let Some(range_ref) = range {
return filter_by_git_range(&range_ref, all_records, repo_path);
}
Ok(all_records)
}
fn filter_by_git_since(
since_ref: &str,
mut records: Vec<crate::tdg::storage::FullTdgRecord>,
repo_path: &Path,
) -> Result<Vec<crate::tdg::storage::FullTdgRecord>> {
let output = Command::new("git")
.args(["log", "-1", "--format=%ct", since_ref])
.current_dir(repo_path)
.output()?;
if !output.status.success() {
return Err(anyhow!("Failed to resolve git ref: {since_ref}"));
}
let since_time: i64 = String::from_utf8_lossy(&output.stdout)
.trim()
.parse()
.map_err(|_| anyhow!("Invalid timestamp from git log"))?;
records.retain(|r| {
if let Some(git_ctx) = &r.git_context {
let record_time = git_ctx.commit_timestamp.timestamp();
record_time > since_time
} else {
false
}
});
Ok(records)
}
fn filter_by_git_range(
range_ref: &str,
mut records: Vec<crate::tdg::storage::FullTdgRecord>,
repo_path: &Path,
) -> Result<Vec<crate::tdg::storage::FullTdgRecord>> {
let parts: Vec<&str> = range_ref.split("..").collect();
if parts.len() != 2 {
return Err(anyhow!(
"Invalid range format. Expected 'start..end' (e.g., HEAD~10..HEAD)"
));
}
let get_timestamp = |git_ref: &str| -> Result<i64> {
let output = Command::new("git")
.args(["log", "-1", "--format=%ct", git_ref])
.current_dir(repo_path)
.output()?;
if !output.status.success() {
return Err(anyhow!("Failed to resolve git ref: {git_ref}"));
}
String::from_utf8_lossy(&output.stdout)
.trim()
.parse()
.map_err(|_| anyhow!("Invalid timestamp from git log"))
};
let start_time = get_timestamp(parts[0])?;
let end_time = get_timestamp(parts[1])?;
records.retain(|r| {
if let Some(git_ctx) = &r.git_context {
let record_time = git_ctx.commit_timestamp.timestamp();
record_time >= start_time && record_time <= end_time
} else {
false
}
});
Ok(records)
}