omni_dev/data/
amendments.rs1use std::fs;
4use std::path::Path;
5
6use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Serialize, Deserialize)]
11pub struct AmendmentFile {
12 pub amendments: Vec<Amendment>,
14}
15
16#[derive(Debug, Serialize, Deserialize)]
18pub struct Amendment {
19 pub commit: String,
21 pub message: String,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub summary: Option<String>,
26}
27
28impl AmendmentFile {
29 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
31 let content = fs::read_to_string(&path).with_context(|| {
32 format!("Failed to read amendment file: {}", path.as_ref().display())
33 })?;
34
35 let amendment_file: AmendmentFile =
36 crate::data::from_yaml(&content).context("Failed to parse YAML amendment file")?;
37
38 amendment_file.validate()?;
39
40 Ok(amendment_file)
41 }
42
43 pub fn validate(&self) -> Result<()> {
45 for (i, amendment) in self.amendments.iter().enumerate() {
47 amendment
48 .validate()
49 .with_context(|| format!("Invalid amendment at index {}", i))?;
50 }
51
52 Ok(())
53 }
54
55 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
57 let yaml_content =
58 serde_yaml::to_string(self).context("Failed to serialize amendments to YAML")?;
59
60 let formatted_yaml = self.format_multiline_yaml(&yaml_content);
62
63 fs::write(&path, formatted_yaml).with_context(|| {
64 format!(
65 "Failed to write amendment file: {}",
66 path.as_ref().display()
67 )
68 })?;
69
70 Ok(())
71 }
72
73 fn format_multiline_yaml(&self, yaml: &str) -> String {
75 let mut result = String::new();
76 let lines: Vec<&str> = yaml.lines().collect();
77 let mut i = 0;
78
79 while i < lines.len() {
80 let line = lines[i];
81
82 if line.trim_start().starts_with("message:") && line.contains('"') {
84 let indent = line.len() - line.trim_start().len();
85 let indent_str = " ".repeat(indent);
86
87 if let Some(start_quote) = line.find('"') {
89 if let Some(end_quote) = line.rfind('"') {
90 if start_quote != end_quote {
91 let quoted_content = &line[start_quote + 1..end_quote];
92
93 if quoted_content.contains("\\n") {
95 result.push_str(&format!("{}message: |\n", indent_str));
97
98 let unescaped = quoted_content.replace("\\n", "\n");
100 for (line_idx, content_line) in unescaped.lines().enumerate() {
101 if line_idx == 0 && content_line.trim().is_empty() {
102 continue;
104 }
105 result.push_str(&format!("{} {}\n", indent_str, content_line));
106 }
107 i += 1;
108 continue;
109 }
110 }
111 }
112 }
113 }
114
115 result.push_str(line);
117 result.push('\n');
118 i += 1;
119 }
120
121 result
122 }
123}
124
125impl Amendment {
126 pub fn new(commit: String, message: String) -> Self {
128 Self {
129 commit,
130 message,
131 summary: None,
132 }
133 }
134
135 pub fn validate(&self) -> Result<()> {
137 if self.commit.len() != crate::git::FULL_HASH_LEN {
139 anyhow::bail!(
140 "Commit hash must be exactly {} characters long, got: {}",
141 crate::git::FULL_HASH_LEN,
142 self.commit.len()
143 );
144 }
145
146 if !self.commit.chars().all(|c| c.is_ascii_hexdigit()) {
147 anyhow::bail!("Commit hash must contain only hexadecimal characters");
148 }
149
150 if !self
151 .commit
152 .chars()
153 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
154 {
155 anyhow::bail!("Commit hash must be lowercase");
156 }
157
158 if self.message.trim().is_empty() {
160 anyhow::bail!("Commit message cannot be empty");
161 }
162
163 Ok(())
164 }
165}