chronicle/cli/
backfill.rs1use 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 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 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 let has_note = ops.note_exists(sha).context(GitSnafu)?;
46 if has_note {
47 already_annotated += 1;
48 continue;
49 }
50
51 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}