1use super::variables::VersionVariables;
6use anyhow::{Context, Result};
7use quick_xml::events::{BytesText, Event};
8use quick_xml::reader::Reader;
9use quick_xml::writer::Writer;
10use regex::Regex;
11use rust_i18n::t;
12use std::io::Cursor;
13use std::path::{Path, PathBuf};
14
15const ASSEMBLY_HEADER: &str = "\
17//------------------------------------------------------------------------------
18// <auto-generated>
19// This code was generated by GitVersion.
20//
21// You can modify this code as we will not overwrite it when re-executing GitVersion
22// </auto-generated>
23//------------------------------------------------------------------------------
24";
25
26fn find_recursive(root: &Path, matches: impl Fn(&Path) -> bool) -> Vec<PathBuf> {
28 let mut out = Vec::new();
29 let mut stack = vec![root.to_path_buf()];
30 while let Some(dir) = stack.pop() {
31 let Ok(entries) = std::fs::read_dir(&dir) else {
32 continue;
33 };
34 for entry in entries.flatten() {
35 let path = entry.path();
36 if path.is_dir() {
37 if path
39 .file_name()
40 .map(|n| n.to_string_lossy().starts_with('.'))
41 .unwrap_or(false)
42 {
43 continue;
44 }
45 stack.push(path);
46 } else if matches(&path) {
47 out.push(path);
48 }
49 }
50 }
51 out.sort();
52 out
53}
54
55pub fn update_assembly_info(
59 vars: &VersionVariables,
60 work_dir: &Path,
61 files: &[String],
62 ensure: bool,
63) -> Result<Vec<PathBuf>> {
64 let targets: Vec<PathBuf> = if files.is_empty() {
65 find_recursive(work_dir, |p| {
66 let name = p
67 .file_name()
68 .map(|n| n.to_string_lossy().to_lowercase())
69 .unwrap_or_default();
70 matches!(
71 name.as_str(),
72 "assemblyinfo.cs" | "assemblyinfo.vb" | "assemblyinfo.fs"
73 )
74 })
75 } else {
76 files.iter().map(|f| work_dir.join(f)).collect()
77 };
78
79 let mut updated = Vec::new();
80 for path in targets {
81 if path.exists() {
82 let content = std::fs::read_to_string(&path)
83 .with_context(|| t!("file.read_failed", path = path.display()).to_string())?;
84 let new = replace_assembly_attributes(&content, vars);
85 std::fs::write(&path, new)?;
86 updated.push(path);
87 } else if ensure {
88 let content = create_assembly_info(&path, vars);
89 if let Some(parent) = path.parent() {
90 std::fs::create_dir_all(parent).ok();
91 }
92 std::fs::write(&path, content)?;
93 updated.push(path);
94 }
95 }
96 Ok(updated)
97}
98
99fn replace_assembly_attributes(content: &str, vars: &VersionVariables) -> String {
101 let replace_attr = |text: &str, attr: &str, value: &str| -> String {
102 let re = Regex::new(&format!(r#"({attr}\s*\(\s*")[^"]*("\s*\))"#)).unwrap();
104 re.replace_all(text, format!("${{1}}{value}${{2}}").as_str())
105 .into_owned()
106 };
107 let mut out = content.to_string();
108 out = replace_attr(&out, "AssemblyFileVersion", &vars.assembly_sem_file_ver);
109 out = replace_attr(
110 &out,
111 "AssemblyInformationalVersion",
112 &vars.informational_version,
113 );
114 out = replace_attr(&out, "AssemblyVersion", &vars.assembly_sem_ver);
115 out
116}
117
118fn create_assembly_info(path: &Path, vars: &VersionVariables) -> String {
120 let ext = path
121 .extension()
122 .map(|e| e.to_string_lossy().to_lowercase())
123 .unwrap_or_default();
124 let (fv, av, iv) = (
125 &vars.assembly_sem_file_ver,
126 &vars.assembly_sem_ver,
127 &vars.informational_version,
128 );
129 match ext.as_str() {
130 "vb" => format!(
131 "{ASSEMBLY_HEADER}\nImports System.Reflection\n\n\
132 <Assembly: AssemblyFileVersion(\"{fv}\")>\n\
133 <Assembly: AssemblyVersion(\"{av}\")>\n\
134 <Assembly: AssemblyInformationalVersion(\"{iv}\")>\n"
135 ),
136 "fs" => format!(
137 "{ASSEMBLY_HEADER}\nnamespace AssemblyInfo\n\nopen System.Reflection\n\n\
138 [<assembly: AssemblyFileVersion(\"{fv}\")>]\n\
139 [<assembly: AssemblyVersion(\"{av}\")>]\n\
140 [<assembly: AssemblyInformationalVersion(\"{iv}\")>]\n\
141 do ()\n"
142 ),
143 _ => format!(
144 "{ASSEMBLY_HEADER}\nusing System.Reflection;\n\n\
145 [assembly: AssemblyFileVersion(\"{fv}\")]\n\
146 [assembly: AssemblyVersion(\"{av}\")]\n\
147 [assembly: AssemblyInformationalVersion(\"{iv}\")]\n"
148 ),
149 }
150}
151
152pub fn update_project_files(
154 vars: &VersionVariables,
155 work_dir: &Path,
156 files: &[String],
157) -> Result<Vec<PathBuf>> {
158 let targets: Vec<PathBuf> = if files.is_empty() {
159 find_recursive(work_dir, |p| {
160 let ext = p
161 .extension()
162 .map(|e| e.to_string_lossy().to_lowercase())
163 .unwrap_or_default();
164 matches!(ext.as_str(), "csproj" | "vbproj" | "fsproj")
165 })
166 } else {
167 files.iter().map(|f| work_dir.join(f)).collect()
168 };
169
170 let mut updated = Vec::new();
171 for path in targets {
172 if !path.exists() {
173 continue;
174 }
175 let content = std::fs::read_to_string(&path)
176 .with_context(|| t!("file.read_failed", path = path.display()).to_string())?;
177 let new = replace_project_elements(&content, vars)
178 .with_context(|| t!("file.xml_update_failed", path = path.display()).to_string())?;
179 std::fs::write(&path, new)?;
180 updated.push(path);
181 }
182 Ok(updated)
183}
184
185const PROJECT_ELEMENTS: [&str; 4] = [
187 "AssemblyVersion",
188 "FileVersion",
189 "InformationalVersion",
190 "Version",
191];
192
193fn replace_project_elements(content: &str, vars: &VersionVariables) -> Result<String> {
199 let value_of = |elem: &str| -> &str {
200 match elem {
201 "AssemblyVersion" => &vars.assembly_sem_ver,
202 "FileVersion" => &vars.assembly_sem_file_ver,
203 "InformationalVersion" => &vars.informational_version,
204 _ => &vars.sem_ver,
205 }
206 };
207
208 let mut reader = Reader::from_str(content);
210 reader.config_mut().trim_text(false);
211 let mut events: Vec<Event<'static>> = Vec::new();
212 loop {
213 match reader.read_event() {
214 Ok(Event::Eof) => break,
215 Ok(ev) => events.push(ev.into_owned()),
216 Err(e) => return Err(anyhow::anyhow!("{}", t!("file.xml_parse_error", error = e))),
217 }
218 }
219
220 let name_of =
221 |e: &quick_xml::events::BytesStart| String::from_utf8_lossy(e.name().as_ref()).into_owned();
222 let end_name_of =
223 |e: &quick_xml::events::BytesEnd| String::from_utf8_lossy(e.name().as_ref()).into_owned();
224
225 let mut existing: std::collections::HashSet<String> = std::collections::HashSet::new();
227 let mut current: Option<String> = None;
228 let mut replaced = false;
229 let mut first_pg_start: Option<usize> = None;
230 let mut first_pg_end: Option<usize> = None;
231 let mut child_indent: Option<String> = None;
232 let mut pg_depth = 0i32;
233
234 for i in 0..events.len() {
235 match &events[i] {
236 Event::Start(e) => {
237 let name = name_of(e);
238 if name == "PropertyGroup" {
239 pg_depth += 1;
240 if first_pg_start.is_none() {
241 first_pg_start = Some(i);
242 }
243 }
244 if PROJECT_ELEMENTS.contains(&name.as_str()) {
245 existing.insert(name.clone());
246 current = Some(name);
247 replaced = false;
248 }
249 }
250 Event::Text(_) => {
251 if first_pg_start.is_some() && first_pg_end.is_none() && child_indent.is_none() {
253 if let (Event::Text(t), Some(Event::Start(_))) = (&events[i], events.get(i + 1))
254 {
255 let s = String::from_utf8_lossy(t.as_ref()).into_owned();
256 if s.contains('\n') {
257 child_indent = Some(s);
258 }
259 }
260 }
261 if let Some(name) = current.clone() {
262 if !replaced {
263 events[i] = Event::Text(BytesText::new(value_of(&name)).into_owned());
264 replaced = true;
265 }
266 }
267 }
268 Event::End(e) => {
269 let name = end_name_of(e);
270 if current.as_deref() == Some(name.as_str()) {
271 current = None;
272 }
273 if name == "PropertyGroup" {
274 pg_depth -= 1;
275 if first_pg_end.is_none() && first_pg_start.is_some() && pg_depth == 0 {
276 first_pg_end = Some(i);
277 }
278 }
279 }
280 _ => {}
281 }
282 }
283
284 let missing: Vec<&str> = PROJECT_ELEMENTS
286 .iter()
287 .filter(|e| !existing.contains(**e))
288 .copied()
289 .collect();
290 if let (Some(end_idx), false) = (first_pg_end, missing.is_empty()) {
291 let indent = child_indent.unwrap_or_else(|| "\n ".into());
292 let insert_at = if end_idx > 0 && matches!(&events[end_idx - 1], Event::Text(_)) {
294 end_idx - 1
295 } else {
296 end_idx
297 };
298 let mut new_events: Vec<Event<'static>> = Vec::new();
299 for elem in &missing {
300 new_events.push(Event::Text(BytesText::new(&indent).into_owned()));
301 new_events.push(Event::Start(
302 quick_xml::events::BytesStart::new(*elem).into_owned(),
303 ));
304 new_events.push(Event::Text(BytesText::new(value_of(elem)).into_owned()));
305 new_events.push(Event::End(
306 quick_xml::events::BytesEnd::new(*elem).into_owned(),
307 ));
308 }
309 events.splice(insert_at..insert_at, new_events);
310 }
311
312 let mut writer = Writer::new(Cursor::new(Vec::new()));
314 for ev in events {
315 writer.write_event(ev)?;
316 }
317 Ok(String::from_utf8(writer.into_inner().into_inner())?)
318}
319
320pub fn update_package_files(
328 vars: &VersionVariables,
329 work_dir: &Path,
330 files: &[String],
331) -> Result<Vec<PathBuf>> {
332 let targets: Vec<PathBuf> = if files.is_empty() {
333 find_recursive(work_dir, |p| {
334 let name = p
335 .file_name()
336 .map(|n| n.to_string_lossy().to_lowercase())
337 .unwrap_or_default();
338 let in_vendor = p.components().any(|c| {
340 let s = c.as_os_str().to_string_lossy();
341 s == "node_modules" || s == "vendor" || s == "target"
342 });
343 !in_vendor
344 && matches!(
345 name.as_str(),
346 "package.json" | "cargo.toml" | "pyproject.toml"
347 )
348 })
349 } else {
350 files.iter().map(|f| work_dir.join(f)).collect()
351 };
352
353 let mut updated = Vec::new();
354 for path in targets {
355 if !path.exists() {
356 continue;
357 }
358 let name = path
359 .file_name()
360 .map(|n| n.to_string_lossy().to_lowercase())
361 .unwrap_or_default();
362 let content = std::fs::read_to_string(&path)
363 .with_context(|| t!("file.read_failed", path = path.display()).to_string())?;
364 let version = &vars.sem_ver;
366 let new = match name.as_str() {
367 "package.json" => update_package_json(&content, version)?,
368 "cargo.toml" => update_cargo_toml(&content, version)?,
369 "pyproject.toml" => update_pyproject_toml(&content, version)?,
370 _ => continue,
371 };
372 if let Some(new) = new {
373 std::fs::write(&path, new)?;
374 updated.push(path);
375 }
376 }
377 Ok(updated)
378}
379
380fn update_package_json(content: &str, version: &str) -> Result<Option<String>> {
382 let mut value: serde_json::Value =
383 serde_json::from_str(content).with_context(|| t!("file.json_parse_failed").to_string())?;
384 let serde_json::Value::Object(map) = &mut value else {
385 return Ok(None);
386 };
387 if !map.contains_key("version") {
388 return Ok(None);
389 }
390 map.insert(
391 "version".into(),
392 serde_json::Value::String(version.to_string()),
393 );
394 let mut out = serde_json::to_string_pretty(&value)?;
395 out.push('\n'); Ok(Some(out))
397}
398
399fn sync_path_dep_versions(deps: &mut dyn toml_edit::TableLike, version: &str) -> bool {
407 let mut changed = false;
408 for (_key, item) in deps.iter_mut() {
409 let Some(dep) = item.as_table_like_mut() else {
410 continue;
411 };
412 if dep.get("path").is_none() {
413 continue;
414 }
415 if let Some(val) = dep.get_mut("version").and_then(|i| i.as_value_mut()) {
416 if val.is_str() {
417 let decor = val.decor().clone();
418 *val = toml_edit::Value::from(version);
419 *val.decor_mut() = decor;
420 changed = true;
421 }
422 }
423 }
424 changed
425}
426
427fn update_cargo_toml(content: &str, version: &str) -> Result<Option<String>> {
440 let mut doc = content
441 .parse::<toml_edit::DocumentMut>()
442 .with_context(|| t!("file.cargo_parse_failed").to_string())?;
443
444 let update_string_version = |table: &mut toml_edit::Table| -> bool {
447 if table.get("version").and_then(|v| v.as_str()).is_some() {
448 table["version"] = toml_edit::value(version);
449 true
450 } else {
451 false
452 }
453 };
454
455 let mut changed = false;
456 if let Some(pkg) = doc.get_mut("package").and_then(|p| p.as_table_mut()) {
458 changed |= update_string_version(pkg);
459 }
460 if let Some(ws_pkg) = doc
462 .get_mut("workspace")
463 .and_then(|w| w.as_table_mut())
464 .and_then(|w| w.get_mut("package"))
465 .and_then(|p| p.as_table_mut())
466 {
467 changed |= update_string_version(ws_pkg);
468 }
469
470 if let Some(ws_deps) = doc
472 .get_mut("workspace")
473 .and_then(|w| w.as_table_mut())
474 .and_then(|w| w.get_mut("dependencies"))
475 .and_then(|d| d.as_table_like_mut())
476 {
477 changed |= sync_path_dep_versions(ws_deps, version);
478 }
479 for table_name in ["dependencies", "dev-dependencies", "build-dependencies"] {
481 if let Some(deps) = doc.get_mut(table_name).and_then(|d| d.as_table_like_mut()) {
482 changed |= sync_path_dep_versions(deps, version);
483 }
484 }
485
486 Ok(if changed { Some(doc.to_string()) } else { None })
487}
488
489fn update_pyproject_toml(content: &str, version: &str) -> Result<Option<String>> {
491 let mut doc = content
492 .parse::<toml_edit::DocumentMut>()
493 .with_context(|| t!("file.pyproject_parse_failed").to_string())?;
494 let mut changed = false;
495 if let Some(project) = doc.get_mut("project").and_then(|p| p.as_table_mut()) {
497 if project.contains_key("version") {
498 project["version"] = toml_edit::value(version);
499 changed = true;
500 }
501 }
502 if let Some(poetry) = doc
504 .get_mut("tool")
505 .and_then(|t| t.as_table_mut())
506 .and_then(|t| t.get_mut("poetry"))
507 .and_then(|p| p.as_table_mut())
508 {
509 if poetry.contains_key("version") {
510 poetry["version"] = toml_edit::value(version);
511 changed = true;
512 }
513 }
514 Ok(if changed { Some(doc.to_string()) } else { None })
515}
516
517pub fn write_wix(vars: &VersionVariables, work_dir: &Path) -> Result<PathBuf> {
519 let mut s = String::new();
520 s.push('\u{feff}'); s.push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
522 s.push_str("<Include xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n");
523 for (key, value) in vars.to_map() {
524 s.push_str(&format!(" <?define {key}=\"{value}\"?>\n"));
525 }
526 s.push_str("</Include>");
527 let path = work_dir.join("GitVersion_WixVersion.wxi");
528 std::fs::write(&path, s)?;
529 Ok(path)
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 fn vars() -> VersionVariables {
537 VersionVariables {
538 assembly_sem_ver: "1.0.1.0".into(),
539 assembly_sem_file_ver: "1.0.1.0".into(),
540 informational_version: "1.0.1-1+Branch.main".into(),
541 sem_ver: "1.0.1-1".into(),
542 ..Default::default()
543 }
544 }
545
546 #[test]
547 fn assembly_attribute_replacement() {
548 let src = "[assembly: AssemblyVersion(\"0.0.0.0\")]\n\
549 [assembly: AssemblyFileVersion(\"0.0.0.0\")]\n\
550 [assembly: AssemblyInformationalVersion(\"0.0.0.0\")]\n";
551 let out = replace_assembly_attributes(src, &vars());
552 assert!(out.contains("AssemblyVersion(\"1.0.1.0\")"));
553 assert!(out.contains("AssemblyFileVersion(\"1.0.1.0\")"));
554 assert!(out.contains("AssemblyInformationalVersion(\"1.0.1-1+Branch.main\")"));
555 }
556
557 #[test]
558 fn project_element_replacement_preserves_structure() {
559 let src = "<Project Sdk=\"Microsoft.NET.Sdk\">\n <!-- 주석 유지 -->\n <PropertyGroup>\n <Version>0.0.0</Version>\n <AssemblyVersion>0.0.0.0</AssemblyVersion>\n </PropertyGroup>\n</Project>";
560 let out = replace_project_elements(src, &vars()).unwrap();
561 assert!(out.contains("<Version>1.0.1-1</Version>"));
562 assert!(out.contains("<AssemblyVersion>1.0.1.0</AssemblyVersion>"));
563 assert!(out.contains("<!-- 주석 유지 -->"));
565 assert!(out.contains("Sdk=\"Microsoft.NET.Sdk\""));
566 }
567
568 #[test]
569 fn project_does_not_touch_unrelated_text() {
570 let src = "<Project><PropertyGroup><Other>0.0.0</Other></PropertyGroup></Project>";
572 let out = replace_project_elements(src, &vars()).unwrap();
573 assert!(out.contains("<Other>0.0.0</Other>"));
574 }
575
576 #[test]
577 fn package_json_version_update() {
578 let src = "{\n \"name\": \"x\",\n \"version\": \"0.0.0\",\n \"private\": true\n}";
579 let out = update_package_json(src, "1.0.1-1").unwrap().unwrap();
580 assert!(out.contains("\"version\": \"1.0.1-1\""));
581 assert!(out.find("\"name\"").unwrap() < out.find("\"version\"").unwrap());
583 assert!(out.contains("\"private\""));
584 }
585
586 #[test]
587 fn cargo_toml_version_update_preserves_comments() {
588 let src = "# comment\n[package]\nname = \"x\" # inline\nversion = \"0.0.0\"\n";
589 let out = update_cargo_toml(src, "1.0.1-1").unwrap().unwrap();
590 assert!(out.contains("version = \"1.0.1-1\""));
591 assert!(out.contains("# comment"));
592 assert!(out.contains("# inline"));
593 }
594
595 #[test]
596 fn package_json_without_version_is_skipped() {
597 let src =
599 "{\n \"name\": \"root\",\n \"private\": true,\n \"workspaces\": [\"packages/*\"]\n}";
600 assert!(update_package_json(src, "1.2.3").unwrap().is_none());
601 }
602
603 #[test]
604 fn package_json_preserves_other_fields_and_format() {
605 let src = "{\n \"name\": \"x\",\n \"version\": \"0.0.0\",\n \"scripts\": {\n \"build\": \"tsc\"\n },\n \"dependencies\": {\n \"left-pad\": \"^1.0.0\"\n }\n}";
606 let out = update_package_json(src, "2.5.0").unwrap().unwrap();
607 assert!(out.contains("\"version\": \"2.5.0\""));
608 assert!(out.contains("\"build\": \"tsc\""));
610 assert!(out.contains("\"left-pad\": \"^1.0.0\""));
611 assert!(out.contains("\n \"name\""));
613 assert!(out.ends_with("}\n"));
614 }
615
616 #[test]
617 fn pyproject_both_sections_updated() {
618 let src =
620 "[project]\nname = \"x\"\nversion = \"0.0.0\"\n\n[tool.poetry]\nversion = \"0.0.0\"\n";
621 let out = update_pyproject_toml(src, "1.2.3").unwrap().unwrap();
622 assert_eq!(out.matches("version = \"1.2.3\"").count(), 2);
623 }
624
625 #[test]
626 fn pyproject_without_version_is_skipped() {
627 let src =
629 "[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n";
630 assert!(update_pyproject_toml(src, "1.2.3").unwrap().is_none());
631 }
632
633 #[test]
634 fn pyproject_dynamic_version_is_skipped() {
635 let src = "[project]\nname = \"x\"\ndynamic = [\"version\"]\n";
638 assert!(update_pyproject_toml(src, "1.2.3").unwrap().is_none());
639 }
640
641 #[test]
642 fn pyproject_preserves_comments() {
643 let src = "# project metadata\n[project]\nname = \"x\" # the name\nversion = \"0.0.0\"\n";
644 let out = update_pyproject_toml(src, "9.0.1").unwrap().unwrap();
645 assert!(out.contains("version = \"9.0.1\""));
646 assert!(out.contains("# project metadata"));
647 assert!(out.contains("# the name"));
648 }
649
650 #[test]
651 fn cargo_toml_workspace_root_updates_workspace_package() {
652 let src = "[workspace]\nmembers = [\"crates/*\"]\n\n[workspace.package]\nversion = \"0.0.1\"\nedition = \"2021\"\n";
654 let out = update_cargo_toml(src, "1.2.3").unwrap().unwrap();
655 assert!(out.contains("version = \"1.2.3\""));
656 assert!(out.contains("edition = \"2021\""));
658 assert!(out.contains("members = [\"crates/*\"]"));
659 }
660
661 #[test]
662 fn cargo_toml_inheriting_member_is_untouched() {
663 let src = "[package]\nname = \"member\"\nversion.workspace = true\n";
665 assert!(update_cargo_toml(src, "1.2.3").unwrap().is_none());
666 }
667
668 #[test]
669 fn cargo_toml_workspace_syncs_internal_path_dep_versions() {
670 let src = "[workspace.package]\nversion = \"0.0.1\"\n\n\
671 [workspace.dependencies]\n\
672 git-warden-core = { path = \"crates/git-warden-core\", version = \"0.0.1\" }\n\
673 serde = \"1\"\n\
674 regex = { version = \"1\" }\n";
675 let out = update_cargo_toml(src, "0.1.0").unwrap().unwrap();
676 assert!(out.contains("[workspace.package]\nversion = \"0.1.0\""));
678 assert!(out.contains(
679 "git-warden-core = { path = \"crates/git-warden-core\", version = \"0.1.0\" }"
680 ));
681 assert!(out.contains("serde = \"1\""));
683 assert!(out.contains("regex = { version = \"1\" }"));
684 }
685
686 #[test]
687 fn cargo_toml_member_syncs_sibling_path_dep() {
688 let src = "[package]\nname = \"app\"\nversion = \"0.0.1\"\n\n\
689 [dependencies]\n\
690 core = { path = \"../core\", version = \"0.0.1\" }\n";
691 let out = update_cargo_toml(src, "0.1.0").unwrap().unwrap();
692 assert!(out.contains("core = { path = \"../core\", version = \"0.1.0\" }"));
693 }
694
695 #[test]
696 fn cargo_toml_path_dep_without_version_is_untouched() {
697 let src = "[dependencies]\nlocal = { path = \"../local\" }\n";
699 assert!(update_cargo_toml(src, "1.0.0").unwrap().is_none());
700 }
701
702 #[test]
703 fn cargo_toml_syncs_path_dep_in_full_table_form() {
704 let src = "[workspace.package]\nversion = \"0.0.1\"\n\n\
705 [workspace.dependencies.core]\n\
706 path = \"crates/core\"\n\
707 version = \"0.0.1\"\n";
708 let out = update_cargo_toml(src, "2.0.0").unwrap().unwrap();
709 assert_eq!(out.matches("\"2.0.0\"").count(), 2);
711 }
712
713 #[test]
714 fn cargo_toml_root_package_and_workspace_both_updated() {
715 let src = "[package]\nname = \"root\"\nversion = \"0.0.1\"\n\n[workspace.package]\nversion = \"0.0.1\"\n";
717 let out = update_cargo_toml(src, "2.0.0").unwrap().unwrap();
718 assert_eq!(out.matches("version = \"2.0.0\"").count(), 2);
719 }
720
721 #[test]
722 fn pyproject_pep621_and_poetry() {
723 let pep621 = "[project]\nname = \"x\"\nversion = \"0.0.0\"\n";
724 let out = update_pyproject_toml(pep621, "1.0.1-1").unwrap().unwrap();
725 assert!(out.contains("version = \"1.0.1-1\""));
726
727 let poetry = "[tool.poetry]\nname = \"x\"\nversion = \"0.0.0\"\n";
728 let out = update_pyproject_toml(poetry, "2.0.0").unwrap().unwrap();
729 assert!(out.contains("version = \"2.0.0\""));
730 }
731
732 #[test]
733 fn create_cs_assembly_info() {
734 let out = create_assembly_info(Path::new("AssemblyInfo.cs"), &vars());
735 assert!(out.contains("using System.Reflection;"));
736 assert!(out.contains("[assembly: AssemblyFileVersion(\"1.0.1.0\")]"));
737 assert!(out.starts_with("//---"));
738 }
739}