Skip to main content

chronicle/cli/
annotate.rs

1use crate::annotate::squash::{
2    collect_source_annotations, collect_source_messages, migrate_amend_annotation,
3    synthesize_squash_annotation, AmendMigrationContext, SquashSynthesisContext,
4};
5use crate::error::chronicle_error::{GitSnafu, JsonSnafu};
6use crate::error::Result;
7use crate::git::{CliOps, GitOps};
8use snafu::ResultExt;
9
10pub fn run(
11    commit: String,
12    sync: bool,
13    live: bool,
14    squash_sources: Option<String>,
15    amend_source: Option<String>,
16) -> Result<()> {
17    let repo_dir = std::env::current_dir().map_err(|e| crate::error::ChronicleError::Io {
18        source: e,
19        location: snafu::Location::default(),
20    })?;
21    let git_ops = CliOps::new(repo_dir);
22
23    if live {
24        return run_live(&git_ops);
25    }
26
27    // Handle --squash-sources
28    if let Some(sources) = squash_sources {
29        return run_squash_synthesis(&git_ops, &commit, &sources);
30    }
31
32    // Handle --amend-source
33    if let Some(old_sha) = amend_source {
34        return run_amend_migration(&git_ops, &commit, &old_sha);
35    }
36
37    let provider = crate::provider::discover_provider().map_err(|e| {
38        crate::error::ChronicleError::Provider {
39            source: e,
40            location: snafu::Location::default(),
41        }
42    })?;
43
44    let annotation = crate::annotate::run(&git_ops, provider.as_ref(), &commit, sync)?;
45
46    let json = serde_json::to_string_pretty(&annotation).map_err(|e| {
47        crate::error::ChronicleError::Json {
48            source: e,
49            location: snafu::Location::default(),
50        }
51    })?;
52    println!("{json}");
53
54    Ok(())
55}
56
57/// Run squash synthesis from explicit source SHAs (for CI).
58fn run_squash_synthesis(git_ops: &CliOps, commit: &str, sources: &str) -> Result<()> {
59    let source_shas: Vec<String> = sources
60        .split(',')
61        .map(|s| s.trim().to_string())
62        .filter(|s| !s.is_empty())
63        .collect();
64
65    if source_shas.is_empty() {
66        return Err(crate::error::ChronicleError::Validation {
67            message: "--squash-sources requires at least one source SHA".to_string(),
68            location: snafu::Location::default(),
69        });
70    }
71
72    // Resolve the commit SHA
73    let resolved_commit = git_ops.resolve_ref(commit).context(GitSnafu)?;
74
75    // Collect source annotations and messages
76    let source_ann_pairs = collect_source_annotations(git_ops, &source_shas);
77    let source_annotations: Vec<_> = source_ann_pairs
78        .into_iter()
79        .filter_map(|(_, ann)| ann)
80        .collect();
81    let source_messages = collect_source_messages(git_ops, &source_shas);
82
83    // Get squash commit info
84    let commit_info = git_ops.commit_info(&resolved_commit).context(GitSnafu)?;
85
86    let ctx = SquashSynthesisContext {
87        squash_commit: resolved_commit.clone(),
88        diff: String::new(), // MVP: not used for deterministic merge
89        source_annotations,
90        source_messages,
91        squash_message: commit_info.message,
92    };
93
94    let annotation = synthesize_squash_annotation(&ctx);
95
96    // Write as git note
97    let json = serde_json::to_string_pretty(&annotation).context(JsonSnafu)?;
98    git_ops
99        .note_write(&resolved_commit, &json)
100        .context(GitSnafu)?;
101
102    println!("{json}");
103    Ok(())
104}
105
106/// Run amend migration from an explicit old SHA.
107fn run_amend_migration(git_ops: &CliOps, commit: &str, old_sha: &str) -> Result<()> {
108    let resolved_commit = git_ops.resolve_ref(commit).context(GitSnafu)?;
109
110    // Read old annotation
111    let old_note = git_ops.note_read(old_sha).context(GitSnafu)?;
112    let old_json = match old_note {
113        Some(json) => json,
114        None => {
115            return Err(crate::error::ChronicleError::Validation {
116                message: format!("No annotation found for old commit {old_sha}"),
117                location: snafu::Location::default(),
118            });
119        }
120    };
121
122    let old_annotation: crate::schema::Annotation =
123        serde_json::from_str(&old_json).context(JsonSnafu)?;
124
125    let new_info = git_ops.commit_info(&resolved_commit).context(GitSnafu)?;
126
127    // Compute diff comparison to determine if code changed
128    let new_diffs = git_ops.diff(&resolved_commit).context(GitSnafu)?;
129    let old_diffs = git_ops.diff(old_sha).context(GitSnafu)?;
130    let new_diff_text = format!("{:?}", new_diffs);
131    let old_diff_text = format!("{:?}", old_diffs);
132    let diff_for_migration = if new_diff_text == old_diff_text {
133        String::new()
134    } else {
135        new_diff_text
136    };
137
138    let ctx = AmendMigrationContext {
139        new_commit: resolved_commit.clone(),
140        new_diff: diff_for_migration,
141        old_annotation,
142        new_message: new_info.message,
143    };
144
145    let annotation = migrate_amend_annotation(&ctx);
146
147    let json = serde_json::to_string_pretty(&annotation).context(JsonSnafu)?;
148    git_ops
149        .note_write(&resolved_commit, &json)
150        .context(GitSnafu)?;
151
152    println!("{json}");
153    Ok(())
154}
155
156/// Live annotation path: read AnnotateInput JSON from stdin, call handle_annotate,
157/// print AnnotateResult JSON to stdout. Zero LLM cost.
158fn run_live(git_ops: &CliOps) -> Result<()> {
159    let stdin = std::io::read_to_string(std::io::stdin()).map_err(|e| {
160        crate::error::ChronicleError::Io {
161            source: e,
162            location: snafu::Location::default(),
163        }
164    })?;
165
166    let input: crate::mcp::annotate_handler::AnnotateInput =
167        serde_json::from_str(&stdin).map_err(|e| crate::error::ChronicleError::Json {
168            source: e,
169            location: snafu::Location::default(),
170        })?;
171
172    let result = crate::mcp::annotate_handler::handle_annotate(git_ops, input)?;
173
174    let json =
175        serde_json::to_string_pretty(&result).map_err(|e| crate::error::ChronicleError::Json {
176            source: e,
177            location: snafu::Location::default(),
178        })?;
179    println!("{json}");
180
181    Ok(())
182}