Skip to main content

chronicle/cli/
backfill.rs

1use crate::annotate::filter::{self, FilterDecision};
2use crate::annotate::gather;
3use crate::error::chronicle_error::{GitSnafu, IoSnafu};
4use crate::error::Result;
5use crate::git::CliOps;
6use crate::git::GitOps;
7use snafu::ResultExt;
8
9pub fn run(limit: usize, dry_run: bool) -> Result<()> {
10    // Find repo dir
11    let output = std::process::Command::new("git")
12        .args(["rev-parse", "--show-toplevel"])
13        .output()
14        .context(IoSnafu)?;
15
16    if !output.status.success() {
17        eprintln!("error: not in a git repository");
18        std::process::exit(1);
19    }
20
21    let repo_dir = String::from_utf8_lossy(&output.stdout).trim().to_string();
22    let ops = CliOps::new(std::path::PathBuf::from(&repo_dir));
23
24    // Get recent commit SHAs
25    let log_output = std::process::Command::new("git")
26        .args(["log", "--format=%H", &format!("-{limit}")])
27        .output()
28        .context(IoSnafu)?;
29
30    let shas: Vec<String> = String::from_utf8_lossy(&log_output.stdout)
31        .lines()
32        .map(|s| s.to_string())
33        .filter(|s| !s.is_empty())
34        .collect();
35
36    eprintln!("Scanning last {} commits on HEAD...", shas.len());
37    eprintln!();
38
39    let mut annotate_count = 0;
40    let mut skip_count = 0;
41    let mut already_annotated = 0;
42
43    for sha in &shas {
44        // Check if already annotated
45        let has_note = ops.note_exists(sha).context(GitSnafu)?;
46        if has_note {
47            already_annotated += 1;
48            continue;
49        }
50
51        // Gather context for filtering
52        let context = match gather::build_context(&ops, sha) {
53            Ok(ctx) => ctx,
54            Err(e) => {
55                eprintln!(
56                    "  SKIP      {}  (error gathering context: {})",
57                    &sha[..7],
58                    e
59                );
60                skip_count += 1;
61                continue;
62            }
63        };
64
65        let short_sha = &sha[..7.min(sha.len())];
66        let short_msg: String = context
67            .commit_message
68            .lines()
69            .next()
70            .unwrap_or("")
71            .chars()
72            .take(60)
73            .collect();
74
75        let decision = filter::pre_llm_filter(&context);
76        match decision {
77            FilterDecision::Annotate => {
78                if dry_run {
79                    let file_count = context.diffs.len();
80                    let line_count: usize =
81                        context.diffs.iter().map(|d| d.changed_line_count()).sum();
82                    eprintln!(
83                        "  ANNOTATE  {}  {} ({} files, {} lines)",
84                        short_sha, short_msg, file_count, line_count
85                    );
86                } else {
87                    eprint!(
88                        "  [{}/{}] {}  {}...",
89                        annotate_count + 1,
90                        shas.len() - already_annotated,
91                        short_sha,
92                        short_msg
93                    );
94                    let provider = match crate::provider::discover_provider() {
95                        Ok(p) => p,
96                        Err(e) => {
97                            eprintln!("\nerror: {e}");
98                            eprintln!("Run `git chronicle setup` or `git chronicle reconfigure` to select a provider.");
99                            std::process::exit(1);
100                        }
101                    };
102                    match crate::annotate::run(&ops, provider.as_ref(), sha) {
103                        Ok(_) => eprintln!(" done"),
104                        Err(e) => eprintln!(" error: {e}"),
105                    }
106                }
107                annotate_count += 1;
108            }
109            FilterDecision::Skip(reason) => {
110                if dry_run {
111                    eprintln!("  SKIP      {}  {} ({})", short_sha, short_msg, reason);
112                }
113                skip_count += 1;
114            }
115            FilterDecision::Trivial(reason) => {
116                if dry_run {
117                    eprintln!("  SKIP      {}  {} ({})", short_sha, short_msg, reason);
118                }
119                skip_count += 1;
120            }
121        }
122    }
123
124    eprintln!();
125    if dry_run {
126        eprintln!(
127            "Would annotate {} of {} commits ({} skipped, {} already annotated).",
128            annotate_count,
129            shas.len(),
130            skip_count,
131            already_annotated
132        );
133    } else if annotate_count > 0 {
134        eprintln!(
135            "Annotated {} commits ({} skipped, {} already annotated).",
136            annotate_count, skip_count, already_annotated
137        );
138    } else {
139        eprintln!("No commits to annotate.");
140    }
141
142    Ok(())
143}