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