chronicle/cli/
annotate.rs1use crate::annotate::squash::{
2 collect_source_annotations_v3, collect_source_messages, migrate_amend_annotation,
3 synthesize_squash_annotation_v3, AmendMigrationContext, SquashSynthesisContextV3,
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 wisdom: vec![],
49 staged_notes: staged_notes_text.clone(),
50 };
51 let result = crate::annotate::live::handle_annotate_v3(&git_ops, input)?;
52 let _ = crate::annotate::staging::clear_staged(&git_dir);
53 let json = serde_json::to_string_pretty(&result).context(JsonSnafu)?;
54 println!("{json}");
55 return Ok(());
56 }
57
58 if let Some(json_str) = json_input {
60 let mut input: crate::annotate::live::LiveInput =
61 serde_json::from_str(&json_str).context(JsonSnafu)?;
62 input.staged_notes = staged_notes_text.clone();
63 let result = crate::annotate::live::handle_annotate_v3(&git_ops, input)?;
64 let _ = crate::annotate::staging::clear_staged(&git_dir);
65 let json = serde_json::to_string_pretty(&result).context(JsonSnafu)?;
66 println!("{json}");
67 return Ok(());
68 }
69
70 if auto {
72 let full_sha = git_ops.resolve_ref(&commit).context(GitSnafu)?;
73 let commit_info = git_ops.commit_info(&full_sha).context(GitSnafu)?;
74 let input = crate::annotate::live::LiveInput {
75 commit,
76 summary: commit_info.message,
77 wisdom: vec![],
78 staged_notes: staged_notes_text.clone(),
79 };
80 let result = crate::annotate::live::handle_annotate_v3(&git_ops, input)?;
81 let _ = crate::annotate::staging::clear_staged(&git_dir);
82 let json = serde_json::to_string_pretty(&result).context(JsonSnafu)?;
83 println!("{json}");
84 return Ok(());
85 }
86
87 if live {
88 return run_live(&git_ops);
89 }
90
91 if let Some(sources) = squash_sources {
93 return run_squash_synthesis(&git_ops, &commit, &sources);
94 }
95
96 if let Some(old_sha) = amend_source {
98 return run_amend_migration(&git_ops, &commit, &old_sha);
99 }
100
101 Err(crate::error::ChronicleError::Validation {
102 message: "no annotation mode specified; use --live, --summary, --json, --auto, --squash-sources, or --amend-source".to_string(),
103 location: snafu::Location::default(),
104 })
105}
106
107fn run_squash_synthesis(git_ops: &CliOps, commit: &str, sources: &str) -> Result<()> {
109 let source_shas: Vec<String> = sources
110 .split(',')
111 .map(|s| s.trim().to_string())
112 .filter(|s| !s.is_empty())
113 .collect();
114
115 if source_shas.is_empty() {
116 return Err(crate::error::ChronicleError::Validation {
117 message: "--squash-sources requires at least one source SHA".to_string(),
118 location: snafu::Location::default(),
119 });
120 }
121
122 let resolved_commit = git_ops.resolve_ref(commit).context(GitSnafu)?;
124
125 let source_ann_pairs = collect_source_annotations_v3(git_ops, &source_shas);
127 let source_annotations: Vec<_> = source_ann_pairs
128 .into_iter()
129 .filter_map(|(_, ann)| ann)
130 .collect();
131 let source_messages = collect_source_messages(git_ops, &source_shas);
132
133 let commit_info = git_ops.commit_info(&resolved_commit).context(GitSnafu)?;
135
136 let ctx = SquashSynthesisContextV3 {
137 squash_commit: resolved_commit.clone(),
138 source_annotations,
139 source_messages,
140 squash_message: commit_info.message,
141 };
142
143 let annotation = synthesize_squash_annotation_v3(&ctx);
144
145 let json = serde_json::to_string_pretty(&annotation).context(JsonSnafu)?;
147 git_ops
148 .note_write(&resolved_commit, &json)
149 .context(GitSnafu)?;
150
151 println!("{json}");
152 Ok(())
153}
154
155fn run_amend_migration(git_ops: &CliOps, commit: &str, old_sha: &str) -> Result<()> {
157 let resolved_commit = git_ops.resolve_ref(commit).context(GitSnafu)?;
158
159 let old_note = git_ops.note_read(old_sha).context(GitSnafu)?;
161 let old_json = match old_note {
162 Some(json) => json,
163 None => {
164 return Err(crate::error::ChronicleError::Validation {
165 message: format!("No annotation found for old commit {old_sha}"),
166 location: snafu::Location::default(),
167 });
168 }
169 };
170
171 let old_annotation: crate::schema::v1::Annotation =
172 serde_json::from_str(&old_json).context(JsonSnafu)?;
173
174 let new_info = git_ops.commit_info(&resolved_commit).context(GitSnafu)?;
175
176 let new_diffs = git_ops.diff(&resolved_commit).context(GitSnafu)?;
178 let old_diffs = git_ops.diff(old_sha).context(GitSnafu)?;
179 let new_diff_text = format!("{:?}", new_diffs);
180 let old_diff_text = format!("{:?}", old_diffs);
181 let diff_for_migration = if new_diff_text == old_diff_text {
182 String::new()
183 } else {
184 new_diff_text
185 };
186
187 let ctx = AmendMigrationContext {
188 new_commit: resolved_commit.clone(),
189 new_diff: diff_for_migration,
190 old_annotation,
191 new_message: new_info.message,
192 };
193
194 let annotation = migrate_amend_annotation(&ctx);
195
196 let json = serde_json::to_string_pretty(&annotation).context(JsonSnafu)?;
197 git_ops
198 .note_write(&resolved_commit, &json)
199 .context(GitSnafu)?;
200
201 println!("{json}");
202 Ok(())
203}
204
205fn run_live(git_ops: &CliOps) -> Result<()> {
207 let stdin = std::io::read_to_string(std::io::stdin()).map_err(|e| {
208 crate::error::ChronicleError::Io {
209 source: e,
210 location: snafu::Location::default(),
211 }
212 })?;
213
214 let value: serde_json::Value =
215 serde_json::from_str(&stdin).map_err(|e| crate::error::ChronicleError::Json {
216 source: e,
217 location: snafu::Location::default(),
218 })?;
219
220 if value.get("regions").is_some() {
221 return Err(crate::error::ChronicleError::Validation {
222 message: "v1 annotation format is no longer supported for writing; use v3 format"
223 .to_string(),
224 location: snafu::Location::default(),
225 });
226 }
227
228 let input: crate::annotate::live::LiveInput =
229 serde_json::from_value(value).map_err(|e| crate::error::ChronicleError::Json {
230 source: e,
231 location: snafu::Location::default(),
232 })?;
233
234 let result = crate::annotate::live::handle_annotate_v3(git_ops, input)?;
235 let json =
236 serde_json::to_string_pretty(&result).map_err(|e| crate::error::ChronicleError::Json {
237 source: e,
238 location: snafu::Location::default(),
239 })?;
240 println!("{json}");
241
242 Ok(())
243}