ricecoder_github/managers/
release_operations.rs1use crate::errors::{GitHubError, Result};
4use crate::models::Release;
5use chrono::Utc;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use tracing::{debug, info};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ReleaseTemplate {
13 pub name: String,
15 pub content: String,
17 pub placeholders: Vec<String>,
19}
20
21impl ReleaseTemplate {
22 pub fn new(name: impl Into<String>, content: impl Into<String>) -> Self {
24 let content_str = content.into();
25 let placeholders = extract_placeholders(&content_str);
26
27 Self {
28 name: name.into(),
29 content: content_str,
30 placeholders,
31 }
32 }
33
34 pub fn apply(&self, values: &HashMap<String, String>) -> Result<String> {
36 let mut result = self.content.clone();
37
38 for placeholder in &self.placeholders {
39 let key = placeholder.trim_start_matches("{{").trim_end_matches("}}");
40 if let Some(value) = values.get(key) {
41 result = result.replace(placeholder, value);
42 } else {
43 return Err(GitHubError::invalid_input(format!(
44 "Missing value for placeholder: {}",
45 placeholder
46 )));
47 }
48 }
49
50 Ok(result)
51 }
52}
53
54fn extract_placeholders(template: &str) -> Vec<String> {
56 let mut placeholders = Vec::new();
57 let mut chars = template.chars().peekable();
58
59 while let Some(ch) = chars.next() {
60 if ch == '{' && chars.peek() == Some(&'{') {
61 chars.next(); let mut placeholder = String::from("{{");
63
64 while let Some(ch) = chars.next() {
65 placeholder.push(ch);
66 if ch == '}' && chars.peek() == Some(&'}') {
67 chars.next(); placeholder.push('}');
69 placeholders.push(placeholder);
70 break;
71 }
72 }
73 }
74 }
75
76 placeholders
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ReleasePublishingResult {
82 pub release_id: u64,
84 pub tag_name: String,
86 pub url: String,
88 pub published_at: chrono::DateTime<Utc>,
90 pub success: bool,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ChangelogEntry {
97 pub version: String,
99 pub date: chrono::DateTime<Utc>,
101 pub changes: Vec<String>,
103 pub contributors: Vec<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct Changelog {
110 pub entries: Vec<ChangelogEntry>,
112 pub last_updated: chrono::DateTime<Utc>,
114}
115
116impl Changelog {
117 pub fn new() -> Self {
119 Self {
120 entries: Vec::new(),
121 last_updated: Utc::now(),
122 }
123 }
124
125 pub fn add_entry(&mut self, entry: ChangelogEntry) {
127 self.entries.push(entry);
128 self.entries.sort_by(|a, b| b.date.cmp(&a.date));
129 self.last_updated = Utc::now();
130 }
131
132 pub fn to_markdown(&self) -> String {
134 let mut markdown = String::from("# Changelog\n\n");
135 markdown.push_str("All notable changes to this project will be documented in this file.\n\n");
136
137 for entry in &self.entries {
138 markdown.push_str(&format!(
139 "## [{}] - {}\n\n",
140 entry.version,
141 entry.date.format("%Y-%m-%d")
142 ));
143
144 if !entry.changes.is_empty() {
145 markdown.push_str("### Changes\n\n");
146 for change in &entry.changes {
147 markdown.push_str(&format!("- {}\n", change));
148 }
149 markdown.push('\n');
150 }
151
152 if !entry.contributors.is_empty() {
153 markdown.push_str("### Contributors\n\n");
154 for contributor in &entry.contributors {
155 markdown.push_str(&format!("- {}\n", contributor));
156 }
157 markdown.push('\n');
158 }
159 }
160
161 markdown
162 }
163}
164
165impl Default for Changelog {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171#[derive(Debug, Clone)]
173pub struct ReleaseOperations {
174 templates: HashMap<String, ReleaseTemplate>,
176 changelog: Changelog,
178}
179
180impl ReleaseOperations {
181 pub fn new() -> Self {
183 Self {
184 templates: HashMap::new(),
185 changelog: Changelog::new(),
186 }
187 }
188
189 pub fn register_template(&mut self, template: ReleaseTemplate) {
191 debug!("Registering template: {}", template.name);
192 self.templates.insert(template.name.clone(), template);
193 }
194
195 pub fn get_template(&self, name: &str) -> Option<&ReleaseTemplate> {
197 self.templates.get(name)
198 }
199
200 pub async fn publish_release(&self, release: &Release) -> Result<ReleasePublishingResult> {
202 debug!("Publishing release: {}", release.tag_name);
203
204 if release.tag_name.is_empty() {
206 return Err(GitHubError::invalid_input("Release tag name cannot be empty"));
207 }
208
209 if release.name.is_empty() {
210 return Err(GitHubError::invalid_input("Release name cannot be empty"));
211 }
212
213 let result = ReleasePublishingResult {
215 release_id: release.id,
216 tag_name: release.tag_name.clone(),
217 url: format!(
218 "https://github.com/releases/tag/{}",
219 release.tag_name
220 ),
221 published_at: Utc::now(),
222 success: true,
223 };
224
225 info!("Release published: {}", release.tag_name);
226 Ok(result)
227 }
228
229 pub fn add_changelog_entry(&mut self, entry: ChangelogEntry) {
231 debug!("Adding changelog entry for version: {}", entry.version);
232 self.changelog.add_entry(entry);
233 }
234
235 pub fn get_changelog(&self) -> &Changelog {
237 &self.changelog
238 }
239
240 pub fn generate_changelog_markdown(&self) -> String {
242 self.changelog.to_markdown()
243 }
244
245 pub fn maintain_changelog(
247 &mut self,
248 version: String,
249 changes: Vec<String>,
250 contributors: Vec<String>,
251 ) -> Result<()> {
252 debug!("Maintaining changelog for version: {}", version);
253
254 let entry = ChangelogEntry {
255 version,
256 date: Utc::now(),
257 changes,
258 contributors,
259 };
260
261 self.add_changelog_entry(entry);
262 info!("Changelog updated");
263 Ok(())
264 }
265
266 pub fn get_release_history(&self) -> Vec<(String, chrono::DateTime<Utc>)> {
268 self.changelog
269 .entries
270 .iter()
271 .map(|e| (e.version.clone(), e.date))
272 .collect()
273 }
274
275 pub fn get_latest_release(&self) -> Option<&ChangelogEntry> {
277 self.changelog.entries.first()
278 }
279
280 pub fn get_releases_between(
282 &self,
283 from_version: &str,
284 to_version: &str,
285 ) -> Vec<&ChangelogEntry> {
286 self.changelog
287 .entries
288 .iter()
289 .filter(|e| e.version.as_str() >= from_version && e.version.as_str() <= to_version)
290 .collect()
291 }
292}
293
294impl Default for ReleaseOperations {
295 fn default() -> Self {
296 Self::new()
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_extract_placeholders() {
306 let template = "Release {{version}} on {{date}}";
307 let placeholders = extract_placeholders(template);
308 assert_eq!(placeholders.len(), 2);
309 assert!(placeholders.contains(&"{{version}}".to_string()));
310 assert!(placeholders.contains(&"{{date}}".to_string()));
311 }
312
313 #[test]
314 fn test_release_template_apply() {
315 let template = ReleaseTemplate::new("test", "Version: {{version}}, Date: {{date}}");
316 let mut values = HashMap::new();
317 values.insert("version".to_string(), "1.0.0".to_string());
318 values.insert("date".to_string(), "2025-01-01".to_string());
319
320 let result = template.apply(&values).unwrap();
321 assert_eq!(result, "Version: 1.0.0, Date: 2025-01-01");
322 }
323
324 #[test]
325 fn test_changelog_to_markdown() {
326 let mut changelog = Changelog::new();
327 let entry = ChangelogEntry {
328 version: "1.0.0".to_string(),
329 date: Utc::now(),
330 changes: vec!["Feature 1".to_string(), "Bug fix".to_string()],
331 contributors: vec!["Alice".to_string()],
332 };
333 changelog.add_entry(entry);
334
335 let markdown = changelog.to_markdown();
336 assert!(markdown.contains("# Changelog"));
337 assert!(markdown.contains("## [1.0.0]"));
338 assert!(markdown.contains("Feature 1"));
339 assert!(markdown.contains("Alice"));
340 }
341}