1use crate::git::{CommitInfo, CommitInfoForAI, RemoteInfo};
4use serde::{Deserialize, Serialize};
5
6pub mod amendments;
7pub mod context;
8pub mod yaml;
9
10pub use amendments::*;
11pub use context::*;
12pub use yaml::*;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct RepositoryView {
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub versions: Option<VersionInfo>,
20 pub explanation: FieldExplanation,
22 pub working_directory: WorkingDirectoryInfo,
24 pub remotes: Vec<RemoteInfo>,
26 pub ai: AiInfo,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub branch_info: Option<BranchInfo>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub pr_template: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub branch_prs: Option<Vec<PullRequest>>,
37 pub commits: Vec<CommitInfo>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct RepositoryViewForAI {
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub versions: Option<VersionInfo>,
47 pub explanation: FieldExplanation,
49 pub working_directory: WorkingDirectoryInfo,
51 pub remotes: Vec<RemoteInfo>,
53 pub ai: AiInfo,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub branch_info: Option<BranchInfo>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub pr_template: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub branch_prs: Option<Vec<PullRequest>>,
64 pub commits: Vec<CommitInfoForAI>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct FieldExplanation {
71 pub text: String,
73 pub fields: Vec<FieldDocumentation>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct FieldDocumentation {
80 pub name: String,
82 pub text: String,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub command: Option<String>,
87 pub present: bool,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct WorkingDirectoryInfo {
94 pub clean: bool,
96 pub untracked_changes: Vec<FileStatusInfo>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct FileStatusInfo {
103 pub status: String,
105 pub file: String,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct VersionInfo {
112 pub omni_dev: String,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct AiInfo {
119 pub scratch: String,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct BranchInfo {
126 pub branch: String,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct PullRequest {
133 pub number: u64,
135 pub title: String,
137 pub state: String,
139 pub url: String,
141 pub body: String,
143}
144
145impl RepositoryView {
146 pub fn update_field_presence(&mut self) {
148 for field in &mut self.explanation.fields {
149 field.present = match field.name.as_str() {
150 "working_directory.clean" => true, "working_directory.untracked_changes" => true, "remotes" => true, "commits[].hash" => !self.commits.is_empty(),
154 "commits[].author" => !self.commits.is_empty(),
155 "commits[].date" => !self.commits.is_empty(),
156 "commits[].original_message" => !self.commits.is_empty(),
157 "commits[].in_main_branches" => !self.commits.is_empty(),
158 "commits[].analysis.detected_type" => !self.commits.is_empty(),
159 "commits[].analysis.detected_scope" => !self.commits.is_empty(),
160 "commits[].analysis.proposed_message" => !self.commits.is_empty(),
161 "commits[].analysis.file_changes.total_files" => !self.commits.is_empty(),
162 "commits[].analysis.file_changes.files_added" => !self.commits.is_empty(),
163 "commits[].analysis.file_changes.files_deleted" => !self.commits.is_empty(),
164 "commits[].analysis.file_changes.file_list" => !self.commits.is_empty(),
165 "commits[].analysis.diff_summary" => !self.commits.is_empty(),
166 "commits[].analysis.diff_file" => !self.commits.is_empty(),
167 "versions.omni_dev" => self.versions.is_some(),
168 "ai.scratch" => true,
169 "branch_info.branch" => self.branch_info.is_some(),
170 "pr_template" => self.pr_template.is_some(),
171 "branch_prs" => self.branch_prs.is_some(),
172 "branch_prs[].number" => {
173 self.branch_prs.as_ref().is_some_and(|prs| !prs.is_empty())
174 }
175 "branch_prs[].title" => self.branch_prs.as_ref().is_some_and(|prs| !prs.is_empty()),
176 "branch_prs[].state" => self.branch_prs.as_ref().is_some_and(|prs| !prs.is_empty()),
177 "branch_prs[].url" => self.branch_prs.as_ref().is_some_and(|prs| !prs.is_empty()),
178 "branch_prs[].body" => self.branch_prs.as_ref().is_some_and(|prs| !prs.is_empty()),
179 _ => false, }
181 }
182 }
183}
184
185impl Default for FieldExplanation {
186 fn default() -> Self {
188 Self {
189 text: [
190 "Field documentation for the YAML output format. Each entry describes the purpose and content of fields returned by the view command.",
191 "",
192 "Field structure:",
193 "- name: Specifies the YAML field path",
194 "- text: Provides a description of what the field contains",
195 "- command: Shows the corresponding command used to obtain that data (if applicable)",
196 "- present: Indicates whether this field is present in the current output",
197 "",
198 "IMPORTANT FOR AI ASSISTANTS: If a field shows present=true, it is guaranteed to be somewhere in this document. AI assistants should search the entire document thoroughly for any field marked as present=true, as it is definitely included in the output."
199 ].join("\n"),
200 fields: vec![
201 FieldDocumentation {
202 name: "working_directory.clean".to_string(),
203 text: "Boolean indicating if the working directory has no uncommitted changes".to_string(),
204 command: Some("git status".to_string()),
205 present: false, },
207 FieldDocumentation {
208 name: "working_directory.untracked_changes".to_string(),
209 text: "Array of files with uncommitted changes, showing git status and file path".to_string(),
210 command: Some("git status --porcelain".to_string()),
211 present: false,
212 },
213 FieldDocumentation {
214 name: "remotes".to_string(),
215 text: "Array of git remotes with their URLs and detected main branch names".to_string(),
216 command: Some("git remote -v".to_string()),
217 present: false,
218 },
219 FieldDocumentation {
220 name: "commits[].hash".to_string(),
221 text: "Full SHA-1 hash of the commit".to_string(),
222 command: Some("git log --format=%H".to_string()),
223 present: false,
224 },
225 FieldDocumentation {
226 name: "commits[].author".to_string(),
227 text: "Commit author name and email address".to_string(),
228 command: Some("git log --format=%an <%ae>".to_string()),
229 present: false,
230 },
231 FieldDocumentation {
232 name: "commits[].date".to_string(),
233 text: "Commit date in ISO format with timezone".to_string(),
234 command: Some("git log --format=%aI".to_string()),
235 present: false,
236 },
237 FieldDocumentation {
238 name: "commits[].original_message".to_string(),
239 text: "The original commit message as written by the author".to_string(),
240 command: Some("git log --format=%B".to_string()),
241 present: false,
242 },
243 FieldDocumentation {
244 name: "commits[].in_main_branches".to_string(),
245 text: "Array of remote main branches that contain this commit (empty if not pushed)".to_string(),
246 command: Some("git branch -r --contains <commit>".to_string()),
247 present: false,
248 },
249 FieldDocumentation {
250 name: "commits[].analysis.detected_type".to_string(),
251 text: "Automatically detected conventional commit type (feat, fix, docs, test, chore, etc.)".to_string(),
252 command: None,
253 present: false,
254 },
255 FieldDocumentation {
256 name: "commits[].analysis.detected_scope".to_string(),
257 text: "Automatically detected scope based on file paths (commands, config, tests, etc.)".to_string(),
258 command: None,
259 present: false,
260 },
261 FieldDocumentation {
262 name: "commits[].analysis.proposed_message".to_string(),
263 text: "AI-generated conventional commit message based on file changes".to_string(),
264 command: None,
265 present: false,
266 },
267 FieldDocumentation {
268 name: "commits[].analysis.file_changes.total_files".to_string(),
269 text: "Total number of files modified in this commit".to_string(),
270 command: Some("git show --name-only <commit>".to_string()),
271 present: false,
272 },
273 FieldDocumentation {
274 name: "commits[].analysis.file_changes.files_added".to_string(),
275 text: "Number of new files added in this commit".to_string(),
276 command: Some("git show --name-status <commit> | grep '^A'".to_string()),
277 present: false,
278 },
279 FieldDocumentation {
280 name: "commits[].analysis.file_changes.files_deleted".to_string(),
281 text: "Number of files deleted in this commit".to_string(),
282 command: Some("git show --name-status <commit> | grep '^D'".to_string()),
283 present: false,
284 },
285 FieldDocumentation {
286 name: "commits[].analysis.file_changes.file_list".to_string(),
287 text: "Array of files changed with their git status (M=modified, A=added, D=deleted)".to_string(),
288 command: Some("git show --name-status <commit>".to_string()),
289 present: false,
290 },
291 FieldDocumentation {
292 name: "commits[].analysis.diff_summary".to_string(),
293 text: "Git diff --stat output showing lines changed per file".to_string(),
294 command: Some("git show --stat <commit>".to_string()),
295 present: false,
296 },
297 FieldDocumentation {
298 name: "commits[].analysis.diff_file".to_string(),
299 text: "Path to file containing full diff content showing line-by-line changes with added, removed, and context lines.\n\
300 AI assistants should read this file to understand the specific changes made in the commit.".to_string(),
301 command: Some("git show <commit>".to_string()),
302 present: false,
303 },
304 FieldDocumentation {
305 name: "versions.omni_dev".to_string(),
306 text: "Version of the omni-dev tool".to_string(),
307 command: Some("omni-dev --version".to_string()),
308 present: false,
309 },
310 FieldDocumentation {
311 name: "ai.scratch".to_string(),
312 text: "Path to AI scratch directory (controlled by AI_SCRATCH environment variable)".to_string(),
313 command: Some("echo $AI_SCRATCH".to_string()),
314 present: false,
315 },
316 FieldDocumentation {
317 name: "branch_info.branch".to_string(),
318 text: "Current branch name (only present in branch commands)".to_string(),
319 command: Some("git branch --show-current".to_string()),
320 present: false,
321 },
322 FieldDocumentation {
323 name: "pr_template".to_string(),
324 text: "Pull request template content from .github/pull_request_template.md (only present in branch commands when file exists)".to_string(),
325 command: None,
326 present: false,
327 },
328 FieldDocumentation {
329 name: "branch_prs".to_string(),
330 text: "Pull requests created from the current branch (only present in branch commands)".to_string(),
331 command: None,
332 present: false,
333 },
334 FieldDocumentation {
335 name: "branch_prs[].number".to_string(),
336 text: "Pull request number".to_string(),
337 command: None,
338 present: false,
339 },
340 FieldDocumentation {
341 name: "branch_prs[].title".to_string(),
342 text: "Pull request title".to_string(),
343 command: None,
344 present: false,
345 },
346 FieldDocumentation {
347 name: "branch_prs[].state".to_string(),
348 text: "Pull request state (open, closed, merged)".to_string(),
349 command: None,
350 present: false,
351 },
352 FieldDocumentation {
353 name: "branch_prs[].url".to_string(),
354 text: "Pull request URL".to_string(),
355 command: None,
356 present: false,
357 },
358 FieldDocumentation {
359 name: "branch_prs[].body".to_string(),
360 text: "Pull request description/body content".to_string(),
361 command: None,
362 present: false,
363 },
364 ],
365 }
366 }
367}
368
369impl RepositoryViewForAI {
370 pub fn from_repository_view(repo_view: RepositoryView) -> anyhow::Result<Self> {
372 let commits: Result<Vec<_>, _> = repo_view
374 .commits
375 .into_iter()
376 .map(CommitInfoForAI::from_commit_info)
377 .collect();
378
379 Ok(Self {
380 versions: repo_view.versions,
381 explanation: repo_view.explanation,
382 working_directory: repo_view.working_directory,
383 remotes: repo_view.remotes,
384 ai: repo_view.ai,
385 branch_info: repo_view.branch_info,
386 pr_template: repo_view.pr_template,
387 branch_prs: repo_view.branch_prs,
388 commits: commits?,
389 })
390 }
391}