chronicle/cli/
annotate.rs1use 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 if let Some(sources) = squash_sources {
29 return run_squash_synthesis(&git_ops, &commit, &sources);
30 }
31
32 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
57fn 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 let resolved_commit = git_ops.resolve_ref(commit).context(GitSnafu)?;
74
75 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 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(), source_annotations,
90 source_messages,
91 squash_message: commit_info.message,
92 };
93
94 let annotation = synthesize_squash_annotation(&ctx);
95
96 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
106fn 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 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 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
156fn 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}