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