1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5use crate::commit::{CommitType, DEFAULT_COMMIT_PATTERN, default_commit_types};
6use crate::error::ReleaseError;
7use crate::version::BumpLevel;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(default)]
11pub struct ReleaseConfig {
12 pub branches: Vec<String>,
13 pub tag_prefix: String,
14 pub commit_pattern: String,
15 pub breaking_section: String,
16 pub misc_section: String,
17 pub types: Vec<CommitType>,
18 pub changelog: ChangelogConfig,
19 pub version_files: Vec<String>,
20 pub version_files_strict: bool,
21 pub artifacts: Vec<String>,
22 pub floating_tags: bool,
23 pub build_command: Option<String>,
24 pub stage_files: Vec<String>,
26 pub prerelease: Option<String>,
29 pub pre_release_command: Option<String>,
31 pub post_release_command: Option<String>,
33 pub sign_tags: bool,
35 pub draft: bool,
37 pub release_name_template: Option<String>,
41}
42
43impl Default for ReleaseConfig {
44 fn default() -> Self {
45 Self {
46 branches: vec!["main".into(), "master".into()],
47 tag_prefix: "v".into(),
48 commit_pattern: DEFAULT_COMMIT_PATTERN.into(),
49 breaking_section: "Breaking Changes".into(),
50 misc_section: "Miscellaneous".into(),
51 types: default_commit_types(),
52 changelog: ChangelogConfig::default(),
53 version_files: vec![],
54 version_files_strict: false,
55 artifacts: vec![],
56 floating_tags: false,
57 build_command: None,
58 stage_files: vec![],
59 prerelease: None,
60 pre_release_command: None,
61 post_release_command: None,
62 sign_tags: false,
63 draft: false,
64 release_name_template: None,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70#[serde(default)]
71pub struct ChangelogConfig {
72 pub file: Option<String>,
73 pub template: Option<String>,
74}
75
76impl ReleaseConfig {
77 pub fn load(path: &Path) -> Result<Self, ReleaseError> {
79 if !path.exists() {
80 return Ok(Self::default());
81 }
82
83 let contents =
84 std::fs::read_to_string(path).map_err(|e| ReleaseError::Config(e.to_string()))?;
85
86 serde_yaml_ng::from_str(&contents).map_err(|e| ReleaseError::Config(e.to_string()))
87 }
88}
89
90impl<'de> Deserialize<'de> for BumpLevel {
92 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
93 where
94 D: serde::Deserializer<'de>,
95 {
96 let s = String::deserialize(deserializer)?;
97 match s.as_str() {
98 "major" => Ok(BumpLevel::Major),
99 "minor" => Ok(BumpLevel::Minor),
100 "patch" => Ok(BumpLevel::Patch),
101 _ => Err(serde::de::Error::custom(format!("unknown bump level: {s}"))),
102 }
103 }
104}
105
106impl Serialize for BumpLevel {
107 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108 where
109 S: serde::Serializer,
110 {
111 let s = match self {
112 BumpLevel::Major => "major",
113 BumpLevel::Minor => "minor",
114 BumpLevel::Patch => "patch",
115 };
116 serializer.serialize_str(s)
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use std::io::Write;
124
125 #[test]
126 fn default_values() {
127 let config = ReleaseConfig::default();
128 assert_eq!(config.branches, vec!["main", "master"]);
129 assert_eq!(config.tag_prefix, "v");
130 assert_eq!(config.commit_pattern, DEFAULT_COMMIT_PATTERN);
131 assert_eq!(config.breaking_section, "Breaking Changes");
132 assert_eq!(config.misc_section, "Miscellaneous");
133 assert!(!config.types.is_empty());
134 assert!(!config.version_files_strict);
135 assert!(config.artifacts.is_empty());
136 assert!(!config.floating_tags);
137 }
138
139 #[test]
140 fn load_missing_file() {
141 let dir = tempfile::tempdir().unwrap();
142 let path = dir.path().join("nonexistent.yml");
143 let config = ReleaseConfig::load(&path).unwrap();
144 assert_eq!(config.tag_prefix, "v");
145 }
146
147 #[test]
148 fn load_valid_yaml() {
149 let dir = tempfile::tempdir().unwrap();
150 let path = dir.path().join("config.yml");
151 let mut f = std::fs::File::create(&path).unwrap();
152 writeln!(f, "branches:\n - develop\ntag_prefix: release-").unwrap();
153
154 let config = ReleaseConfig::load(&path).unwrap();
155 assert_eq!(config.branches, vec!["develop"]);
156 assert_eq!(config.tag_prefix, "release-");
157 }
158
159 #[test]
160 fn load_partial_yaml() {
161 let dir = tempfile::tempdir().unwrap();
162 let path = dir.path().join("config.yml");
163 std::fs::write(&path, "tag_prefix: rel-\n").unwrap();
164
165 let config = ReleaseConfig::load(&path).unwrap();
166 assert_eq!(config.tag_prefix, "rel-");
167 assert_eq!(config.branches, vec!["main", "master"]);
168 assert_eq!(config.commit_pattern, DEFAULT_COMMIT_PATTERN);
170 assert_eq!(config.breaking_section, "Breaking Changes");
171 assert!(!config.types.is_empty());
172 }
173
174 #[test]
175 fn load_yaml_with_artifacts() {
176 let dir = tempfile::tempdir().unwrap();
177 let path = dir.path().join("config.yml");
178 std::fs::write(
179 &path,
180 "artifacts:\n - \"dist/*.tar.gz\"\n - \"build/output-*\"\n",
181 )
182 .unwrap();
183
184 let config = ReleaseConfig::load(&path).unwrap();
185 assert_eq!(config.artifacts, vec!["dist/*.tar.gz", "build/output-*"]);
186 assert_eq!(config.tag_prefix, "v");
188 }
189
190 #[test]
191 fn load_yaml_with_floating_tags() {
192 let dir = tempfile::tempdir().unwrap();
193 let path = dir.path().join("config.yml");
194 std::fs::write(&path, "floating_tags: true\n").unwrap();
195
196 let config = ReleaseConfig::load(&path).unwrap();
197 assert!(config.floating_tags);
198 assert_eq!(config.tag_prefix, "v");
200 }
201
202 #[test]
203 fn bump_level_roundtrip() {
204 for (level, expected) in [
205 (BumpLevel::Major, "major"),
206 (BumpLevel::Minor, "minor"),
207 (BumpLevel::Patch, "patch"),
208 ] {
209 let yaml = serde_yaml_ng::to_string(&level).unwrap();
210 assert!(yaml.contains(expected));
211 let parsed: BumpLevel = serde_yaml_ng::from_str(&yaml).unwrap();
212 assert_eq!(parsed, level);
213 }
214 }
215
216 #[test]
217 fn types_roundtrip() {
218 let config = ReleaseConfig::default();
219 let yaml = serde_yaml_ng::to_string(&config).unwrap();
220 let parsed: ReleaseConfig = serde_yaml_ng::from_str(&yaml).unwrap();
221 assert_eq!(parsed.types.len(), config.types.len());
222 assert_eq!(parsed.types[0].name, "feat");
223 assert_eq!(parsed.commit_pattern, config.commit_pattern);
224 assert_eq!(parsed.breaking_section, config.breaking_section);
225 }
226}