ricecoder_github/managers/
release_operations.rs

1//! Release Operations - Handles release publishing and changelog maintenance
2
3use 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/// Release template for customization
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ReleaseTemplate {
13    /// Template name
14    pub name: String,
15    /// Template content with placeholders
16    pub content: String,
17    /// Placeholders in template
18    pub placeholders: Vec<String>,
19}
20
21impl ReleaseTemplate {
22    /// Create a new release template
23    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    /// Apply template with values
35    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
54/// Extract placeholders from template
55fn 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(); // consume second {
62            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(); // consume second }
68                    placeholder.push('}');
69                    placeholders.push(placeholder);
70                    break;
71                }
72            }
73        }
74    }
75
76    placeholders
77}
78
79/// Release publishing result
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ReleasePublishingResult {
82    /// Release ID
83    pub release_id: u64,
84    /// Tag name
85    pub tag_name: String,
86    /// Published URL
87    pub url: String,
88    /// Publish timestamp
89    pub published_at: chrono::DateTime<Utc>,
90    /// Success status
91    pub success: bool,
92}
93
94/// Changelog entry
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct ChangelogEntry {
97    /// Version
98    pub version: String,
99    /// Release date
100    pub date: chrono::DateTime<Utc>,
101    /// Changes in this release
102    pub changes: Vec<String>,
103    /// Contributors
104    pub contributors: Vec<String>,
105}
106
107/// Changelog
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct Changelog {
110    /// Changelog entries
111    pub entries: Vec<ChangelogEntry>,
112    /// Last updated
113    pub last_updated: chrono::DateTime<Utc>,
114}
115
116impl Changelog {
117    /// Create a new changelog
118    pub fn new() -> Self {
119        Self {
120            entries: Vec::new(),
121            last_updated: Utc::now(),
122        }
123    }
124
125    /// Add an entry
126    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    /// Generate markdown
133    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/// Release Operations
172#[derive(Debug, Clone)]
173pub struct ReleaseOperations {
174    /// Release templates
175    templates: HashMap<String, ReleaseTemplate>,
176    /// Changelog
177    changelog: Changelog,
178}
179
180impl ReleaseOperations {
181    /// Create new release operations
182    pub fn new() -> Self {
183        Self {
184            templates: HashMap::new(),
185            changelog: Changelog::new(),
186        }
187    }
188
189    /// Register a release template
190    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    /// Get a template
196    pub fn get_template(&self, name: &str) -> Option<&ReleaseTemplate> {
197        self.templates.get(name)
198    }
199
200    /// Publish a release
201    pub async fn publish_release(&self, release: &Release) -> Result<ReleasePublishingResult> {
202        debug!("Publishing release: {}", release.tag_name);
203
204        // Validate release
205        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        // Create publishing result
214        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    /// Add changelog entry
230    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    /// Get changelog
236    pub fn get_changelog(&self) -> &Changelog {
237        &self.changelog
238    }
239
240    /// Generate changelog markdown
241    pub fn generate_changelog_markdown(&self) -> String {
242        self.changelog.to_markdown()
243    }
244
245    /// Maintain changelog - add new entry
246    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    /// Get release history from changelog
267    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    /// Find latest release in changelog
276    pub fn get_latest_release(&self) -> Option<&ChangelogEntry> {
277        self.changelog.entries.first()
278    }
279
280    /// Get releases between versions
281    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}