1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5use async_trait::async_trait;
6use changepacks_core::publish::{
7 PublishOutput, resolve_dry_run_publish_command, run_publish_command,
8};
9use changepacks_core::{Config, Language, Package, UpdateType};
10use changepacks_utils::next_version;
11use tokio::fs::{read_to_string, write};
12
13use crate::dry_run::run_managed_dry_run;
14use crate::xml_utils::update_version_in_xml;
15
16#[derive(Debug)]
17pub struct CSharpPackage {
18 name: Option<String>,
19 version: Option<String>,
20 path: PathBuf,
21 relative_path: PathBuf,
22 is_changed: bool,
23 dependencies: HashSet<String>,
24}
25
26impl CSharpPackage {
27 #[must_use]
28 pub fn new(
29 name: Option<String>,
30 version: Option<String>,
31 path: PathBuf,
32 relative_path: PathBuf,
33 ) -> Self {
34 Self {
35 name,
36 version,
37 path,
38 relative_path,
39 is_changed: false,
40 dependencies: HashSet::new(),
41 }
42 }
43}
44
45#[async_trait]
46impl Package for CSharpPackage {
47 fn name(&self) -> Option<&str> {
48 self.name.as_deref()
49 }
50
51 fn version(&self) -> Option<&str> {
52 self.version.as_deref()
53 }
54
55 fn path(&self) -> &Path {
56 &self.path
57 }
58
59 fn relative_path(&self) -> &Path {
60 &self.relative_path
61 }
62
63 async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
64 let current_version = self.version.as_deref().unwrap_or("0.0.0");
65 let new_version = next_version(current_version, update_type)?;
66
67 let csproj_raw = read_to_string(&self.path).await?;
68 let has_version = self.version.is_some();
69
70 let updated_content = update_version_in_xml(&csproj_raw, &new_version, has_version)?;
71
72 write(&self.path, updated_content).await?;
73 self.version = Some(new_version);
74 Ok(())
75 }
76
77 fn language(&self) -> Language {
78 Language::CSharp
79 }
80
81 fn is_changed(&self) -> bool {
82 self.is_changed
83 }
84
85 fn set_changed(&mut self, changed: bool) {
86 self.is_changed = changed;
87 }
88
89 fn set_name(&mut self, name: String) {
90 self.name = Some(name);
91 }
92
93 fn default_publish_command(&self) -> String {
94 "dotnet pack -c Release && dotnet nuget push".to_string()
95 }
96
97 fn default_dry_run_publish_command(&self) -> Option<String> {
98 None
105 }
106
107 #[cfg(not(tarpaulin_include))]
115 async fn dry_run_publish(&self, config: &Config) -> Result<Option<PublishOutput>> {
116 let dir = self
117 .path()
118 .parent()
119 .context("Package directory not found")?;
120
121 if let Some(user_cmd) =
123 resolve_dry_run_publish_command(self.relative_path(), self.language(), None, config)
124 {
125 return Ok(Some(run_publish_command(&user_cmd, dir).await?));
126 }
127
128 Ok(Some(run_managed_dry_run(dir).await?))
130 }
131
132 fn dependencies(&self) -> &HashSet<String> {
133 &self.dependencies
134 }
135
136 fn add_dependency(&mut self, dependency: &str) {
137 self.dependencies.insert(dependency.to_string());
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::fs;
145 use tempfile::TempDir;
146
147 #[tokio::test]
148 async fn test_new() {
149 let temp_dir = TempDir::new().unwrap();
150 let csproj_path = temp_dir.path().join("Test.csproj");
151 fs::write(
152 &csproj_path,
153 r#"<Project Sdk="Microsoft.NET.Sdk">
154 <PropertyGroup>
155 <Version>1.0.0</Version>
156 </PropertyGroup>
157</Project>
158"#,
159 )
160 .unwrap();
161
162 let package = CSharpPackage::new(
163 Some("Test".to_string()),
164 Some("1.0.0".to_string()),
165 csproj_path.clone(),
166 PathBuf::from("Test.csproj"),
167 );
168
169 assert_eq!(package.name(), Some("Test"));
170 assert_eq!(package.version(), Some("1.0.0"));
171 assert_eq!(package.path(), csproj_path);
172 assert_eq!(package.relative_path(), PathBuf::from("Test.csproj"));
173 assert!(!package.is_changed());
174 assert_eq!(package.language(), Language::CSharp);
175 assert_eq!(
176 package.default_publish_command(),
177 "dotnet pack -c Release && dotnet nuget push"
178 );
179 assert!(package.default_dry_run_publish_command().is_none());
182
183 temp_dir.close().unwrap();
184 }
185
186 #[tokio::test]
187 async fn test_set_changed() {
188 let temp_dir = TempDir::new().unwrap();
189 let csproj_path = temp_dir.path().join("Test.csproj");
190 fs::write(
191 &csproj_path,
192 r#"<Project Sdk="Microsoft.NET.Sdk">
193 <PropertyGroup>
194 <Version>1.0.0</Version>
195 </PropertyGroup>
196</Project>
197"#,
198 )
199 .unwrap();
200
201 let mut package = CSharpPackage::new(
202 Some("Test".to_string()),
203 Some("1.0.0".to_string()),
204 csproj_path.clone(),
205 PathBuf::from("Test.csproj"),
206 );
207
208 assert!(!package.is_changed());
209 package.set_changed(true);
210 assert!(package.is_changed());
211 package.set_changed(false);
212 assert!(!package.is_changed());
213
214 temp_dir.close().unwrap();
215 }
216
217 #[tokio::test]
218 async fn test_update_version_patch() {
219 let temp_dir = TempDir::new().unwrap();
220 let csproj_path = temp_dir.path().join("Test.csproj");
221 fs::write(
222 &csproj_path,
223 r#"<Project Sdk="Microsoft.NET.Sdk">
224 <PropertyGroup>
225 <Version>1.0.0</Version>
226 </PropertyGroup>
227</Project>
228"#,
229 )
230 .unwrap();
231
232 let mut package = CSharpPackage::new(
233 Some("Test".to_string()),
234 Some("1.0.0".to_string()),
235 csproj_path.clone(),
236 PathBuf::from("Test.csproj"),
237 );
238
239 package.update_version(UpdateType::Patch).await.unwrap();
240
241 let content = fs::read_to_string(&csproj_path).unwrap();
242 assert!(content.contains("<Version>1.0.1</Version>"));
243
244 temp_dir.close().unwrap();
245 }
246
247 #[tokio::test]
248 async fn test_update_version_minor() {
249 let temp_dir = TempDir::new().unwrap();
250 let csproj_path = temp_dir.path().join("Test.csproj");
251 fs::write(
252 &csproj_path,
253 r#"<Project Sdk="Microsoft.NET.Sdk">
254 <PropertyGroup>
255 <Version>1.0.0</Version>
256 </PropertyGroup>
257</Project>
258"#,
259 )
260 .unwrap();
261
262 let mut package = CSharpPackage::new(
263 Some("Test".to_string()),
264 Some("1.0.0".to_string()),
265 csproj_path.clone(),
266 PathBuf::from("Test.csproj"),
267 );
268
269 package.update_version(UpdateType::Minor).await.unwrap();
270
271 let content = fs::read_to_string(&csproj_path).unwrap();
272 assert!(content.contains("<Version>1.1.0</Version>"));
273
274 temp_dir.close().unwrap();
275 }
276
277 #[tokio::test]
278 async fn test_update_version_major() {
279 let temp_dir = TempDir::new().unwrap();
280 let csproj_path = temp_dir.path().join("Test.csproj");
281 fs::write(
282 &csproj_path,
283 r#"<Project Sdk="Microsoft.NET.Sdk">
284 <PropertyGroup>
285 <Version>1.0.0</Version>
286 </PropertyGroup>
287</Project>
288"#,
289 )
290 .unwrap();
291
292 let mut package = CSharpPackage::new(
293 Some("Test".to_string()),
294 Some("1.0.0".to_string()),
295 csproj_path.clone(),
296 PathBuf::from("Test.csproj"),
297 );
298
299 package.update_version(UpdateType::Major).await.unwrap();
300
301 let content = fs::read_to_string(&csproj_path).unwrap();
302 assert!(content.contains("<Version>2.0.0</Version>"));
303
304 temp_dir.close().unwrap();
305 }
306
307 #[tokio::test]
308 async fn test_update_version_preserves_other_elements() {
309 let temp_dir = TempDir::new().unwrap();
310 let csproj_path = temp_dir.path().join("Test.csproj");
311 let original_content = r#"<Project Sdk="Microsoft.NET.Sdk">
312 <PropertyGroup>
313 <OutputType>Exe</OutputType>
314 <TargetFramework>net8.0</TargetFramework>
315 <Version>1.0.0</Version>
316 <PackageId>MyPackage</PackageId>
317 </PropertyGroup>
318</Project>
319"#;
320 fs::write(&csproj_path, original_content).unwrap();
321
322 let mut package = CSharpPackage::new(
323 Some("Test".to_string()),
324 Some("1.0.0".to_string()),
325 csproj_path.clone(),
326 PathBuf::from("Test.csproj"),
327 );
328
329 package.update_version(UpdateType::Patch).await.unwrap();
330
331 let content = fs::read_to_string(&csproj_path).unwrap();
332 assert!(content.contains("<Version>1.0.1</Version>"));
333 assert!(content.contains("<OutputType>Exe</OutputType>"));
334 assert!(content.contains("<TargetFramework>net8.0</TargetFramework>"));
335 assert!(content.contains("<PackageId>MyPackage</PackageId>"));
336
337 temp_dir.close().unwrap();
338 }
339
340 #[test]
341 fn test_dependencies() {
342 let mut package = CSharpPackage::new(
343 Some("Test".to_string()),
344 Some("1.0.0".to_string()),
345 PathBuf::from("/test/Test.csproj"),
346 PathBuf::from("test/Test.csproj"),
347 );
348
349 assert!(package.dependencies().is_empty());
351
352 package.add_dependency("Newtonsoft.Json");
354 package.add_dependency("CoreLib");
355
356 let deps = package.dependencies();
357 assert_eq!(deps.len(), 2);
358 assert!(deps.contains("Newtonsoft.Json"));
359 assert!(deps.contains("CoreLib"));
360
361 package.add_dependency("Newtonsoft.Json");
363 assert_eq!(package.dependencies().len(), 2);
364 }
365
366 #[test]
367 fn test_set_name() {
368 let mut package = CSharpPackage::new(
369 None,
370 Some("1.0.0".to_string()),
371 PathBuf::from("/test/Test.csproj"),
372 PathBuf::from("Test.csproj"),
373 );
374 assert_eq!(package.name(), None);
375 package.set_name("my-project".to_string());
376 assert_eq!(package.name(), Some("my-project"));
377 }
378}