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 struct AnnotateArgs {
11 pub commit: String,
12 pub live: bool,
13 pub squash_sources: Option<String>,
14 pub amend_source: Option<String>,
15 pub summary: Option<String>,
16 pub json_input: Option<String>,
17 pub auto: bool,
18}
19
20pub fn run(args: AnnotateArgs) -> Result<()> {
21 let AnnotateArgs {
22 commit,
23 live,
24 squash_sources,
25 amend_source,
26 summary,
27 json_input,
28 auto,
29 } = args;
30 let repo_dir = std::env::current_dir().map_err(|e| crate::error::ChronicleError::Io {
31 source: e,
32 location: snafu::Location::default(),
33 })?;
34 let git_ops = CliOps::new(repo_dir.clone());
35
36 let git_dir = repo_dir.join(".git");
38 let staged_notes_text = crate::annotate::staging::read_staged(&git_dir)
39 .ok()
40 .filter(|notes| !notes.is_empty())
41 .map(|notes| crate::annotate::staging::format_for_provenance(¬es));
42
43 if let Some(summary_text) = summary {
45 let input = crate::annotate::live::LiveInput {
46 commit,
47 summary: summary_text,
48 motivation: None,
49 rejected_alternatives: vec![],
50 follow_up: None,
51 decisions: vec![],
52 markers: vec![],
53 effort: None,
54 staged_notes: staged_notes_text.clone(),
55 };
56 let result = crate::annotate::live::handle_annotate_v2(&git_ops, input)?;
57 let _ = crate::annotate::staging::clear_staged(&git_dir);
58 let json = serde_json::to_string_pretty(&result).context(JsonSnafu)?;
59 println!("{json}");
60 return Ok(());
61 }
62
63 if let Some(json_str) = json_input {
65 let mut input: crate::annotate::live::LiveInput =
66 serde_json::from_str(&json_str).context(JsonSnafu)?;
67 input.staged_notes = staged_notes_text.clone();
68 let result = crate::annotate::live::handle_annotate_v2(&git_ops, input)?;
69 let _ = crate::annotate::staging::clear_staged(&git_dir);
70 let json = serde_json::to_string_pretty(&result).context(JsonSnafu)?;
71 println!("{json}");
72 return Ok(());
73 }
74
75 if auto {
77 let full_sha = git_ops.resolve_ref(&commit).context(GitSnafu)?;
78 let commit_info = git_ops.commit_info(&full_sha).context(GitSnafu)?;
79 let input = crate::annotate::live::LiveInput {
80 commit,
81 summary: commit_info.message,
82 motivation: None,
83 rejected_alternatives: vec![],
84 follow_up: None,
85 decisions: vec![],
86 markers: vec![],
87 effort: None,
88 staged_notes: staged_notes_text.clone(),
89 };
90 let result = crate::annotate::live::handle_annotate_v2(&git_ops, input)?;
91 let _ = crate::annotate::staging::clear_staged(&git_dir);
92 let json = serde_json::to_string_pretty(&result).context(JsonSnafu)?;
93 println!("{json}");
94 return Ok(());
95 }
96
97 if live {
98 return run_live(&git_ops);
99 }
100
101 if let Some(sources) = squash_sources {
103 return run_squash_synthesis(&git_ops, &commit, &sources);
104 }
105
106 if let Some(old_sha) = amend_source {
108 return run_amend_migration(&git_ops, &commit, &old_sha);
109 }
110
111 let provider = crate::provider::discover_provider().map_err(|e| {
112 crate::error::ChronicleError::Provider {
113 source: e,
114 location: snafu::Location::default(),
115 }
116 })?;
117
118 let annotation = crate::annotate::run(&git_ops, provider.as_ref(), &commit)?;
119
120 let json = serde_json::to_string_pretty(&annotation).map_err(|e| {
121 crate::error::ChronicleError::Json {
122 source: e,
123 location: snafu::Location::default(),
124 }
125 })?;
126 println!("{json}");
127
128 Ok(())
129}
130
131fn run_squash_synthesis(git_ops: &CliOps, commit: &str, sources: &str) -> Result<()> {
133 let source_shas: Vec<String> = sources
134 .split(',')
135 .map(|s| s.trim().to_string())
136 .filter(|s| !s.is_empty())
137 .collect();
138
139 if source_shas.is_empty() {
140 return Err(crate::error::ChronicleError::Validation {
141 message: "--squash-sources requires at least one source SHA".to_string(),
142 location: snafu::Location::default(),
143 });
144 }
145
146 let resolved_commit = git_ops.resolve_ref(commit).context(GitSnafu)?;
148
149 let source_ann_pairs = collect_source_annotations(git_ops, &source_shas);
151 let source_annotations: Vec<_> = source_ann_pairs
152 .into_iter()
153 .filter_map(|(_, ann)| ann)
154 .collect();
155 let source_messages = collect_source_messages(git_ops, &source_shas);
156
157 let commit_info = git_ops.commit_info(&resolved_commit).context(GitSnafu)?;
159
160 let ctx = SquashSynthesisContext {
161 squash_commit: resolved_commit.clone(),
162 diff: String::new(), source_annotations,
164 source_messages,
165 squash_message: commit_info.message,
166 };
167
168 let annotation = synthesize_squash_annotation(&ctx);
169
170 let json = serde_json::to_string_pretty(&annotation).context(JsonSnafu)?;
172 git_ops
173 .note_write(&resolved_commit, &json)
174 .context(GitSnafu)?;
175
176 println!("{json}");
177 Ok(())
178}
179
180fn run_amend_migration(git_ops: &CliOps, commit: &str, old_sha: &str) -> Result<()> {
182 let resolved_commit = git_ops.resolve_ref(commit).context(GitSnafu)?;
183
184 let old_note = git_ops.note_read(old_sha).context(GitSnafu)?;
186 let old_json = match old_note {
187 Some(json) => json,
188 None => {
189 return Err(crate::error::ChronicleError::Validation {
190 message: format!("No annotation found for old commit {old_sha}"),
191 location: snafu::Location::default(),
192 });
193 }
194 };
195
196 let old_annotation: crate::schema::v1::Annotation =
197 serde_json::from_str(&old_json).context(JsonSnafu)?;
198
199 let new_info = git_ops.commit_info(&resolved_commit).context(GitSnafu)?;
200
201 let new_diffs = git_ops.diff(&resolved_commit).context(GitSnafu)?;
203 let old_diffs = git_ops.diff(old_sha).context(GitSnafu)?;
204 let new_diff_text = format!("{:?}", new_diffs);
205 let old_diff_text = format!("{:?}", old_diffs);
206 let diff_for_migration = if new_diff_text == old_diff_text {
207 String::new()
208 } else {
209 new_diff_text
210 };
211
212 let ctx = AmendMigrationContext {
213 new_commit: resolved_commit.clone(),
214 new_diff: diff_for_migration,
215 old_annotation,
216 new_message: new_info.message,
217 };
218
219 let annotation = migrate_amend_annotation(&ctx);
220
221 let json = serde_json::to_string_pretty(&annotation).context(JsonSnafu)?;
222 git_ops
223 .note_write(&resolved_commit, &json)
224 .context(GitSnafu)?;
225
226 println!("{json}");
227 Ok(())
228}
229
230fn run_live(git_ops: &CliOps) -> Result<()> {
232 let stdin = std::io::read_to_string(std::io::stdin()).map_err(|e| {
233 crate::error::ChronicleError::Io {
234 source: e,
235 location: snafu::Location::default(),
236 }
237 })?;
238
239 let value: serde_json::Value =
240 serde_json::from_str(&stdin).map_err(|e| crate::error::ChronicleError::Json {
241 source: e,
242 location: snafu::Location::default(),
243 })?;
244
245 if value.get("regions").is_some() {
246 return Err(crate::error::ChronicleError::Validation {
247 message: "v1 annotation format is no longer supported for writing; use v2 format"
248 .to_string(),
249 location: snafu::Location::default(),
250 });
251 }
252
253 let input: crate::annotate::live::LiveInput =
254 serde_json::from_value(value).map_err(|e| crate::error::ChronicleError::Json {
255 source: e,
256 location: snafu::Location::default(),
257 })?;
258
259 let result = crate::annotate::live::handle_annotate_v2(git_ops, input)?;
260 let json =
261 serde_json::to_string_pretty(&result).map_err(|e| crate::error::ChronicleError::Json {
262 source: e,
263 location: snafu::Location::default(),
264 })?;
265 println!("{json}");
266
267 Ok(())
268}