1use std::path::Path;
2
3use semver::Version;
4use toml_edit::{DocumentMut, Item, Table, TableLike, value};
5
6use crate::config::{InitConfig, MetadataSection};
7use crate::error::ManifestError;
8use crate::reader::{read_document, read_version};
9
10const DEPENDENCY_SECTIONS: [&str; 3] = ["dependencies", "dev-dependencies", "build-dependencies"];
11
12pub fn write_version(path: &Path, version: &Version) -> Result<(), ManifestError> {
16 let mut doc = read_document(path)?;
17
18 let package = doc
19 .get_mut("package")
20 .ok_or_else(|| ManifestError::MissingField {
21 path: path.to_path_buf(),
22 field: "package".to_string(),
23 })?;
24
25 let package_table = package
26 .as_table_like_mut()
27 .ok_or_else(|| ManifestError::MissingField {
28 path: path.to_path_buf(),
29 field: "package (as table)".to_string(),
30 })?;
31
32 package_table.insert("version", value(version.to_string()));
33
34 std::fs::write(path, doc.to_string()).map_err(|source| ManifestError::Write {
35 path: path.to_path_buf(),
36 source,
37 })
38}
39
40pub fn remove_workspace_version(path: &Path) -> Result<(), ManifestError> {
44 let mut doc = read_document(path)?;
45
46 let Some(workspace) = doc.get_mut("workspace") else {
47 return Ok(());
48 };
49
50 let Some(workspace_table) = workspace.as_table_like_mut() else {
51 return Ok(());
52 };
53
54 let Some(package) = workspace_table.get_mut("package") else {
55 return Ok(());
56 };
57
58 let Some(package_table) = package.as_table_like_mut() else {
59 return Ok(());
60 };
61
62 package_table.remove("version");
63
64 std::fs::write(path, doc.to_string()).map_err(|source| ManifestError::Write {
65 path: path.to_path_buf(),
66 source,
67 })
68}
69
70pub fn write_workspace_version(path: &Path, version: &Version) -> Result<(), ManifestError> {
76 let mut doc = read_document(path)?;
77
78 let workspace = doc
79 .get_mut("workspace")
80 .ok_or_else(|| ManifestError::MissingField {
81 path: path.to_path_buf(),
82 field: "workspace".to_string(),
83 })?;
84
85 let workspace_table =
86 workspace
87 .as_table_like_mut()
88 .ok_or_else(|| ManifestError::MissingField {
89 path: path.to_path_buf(),
90 field: "workspace (as table)".to_string(),
91 })?;
92
93 let package = workspace_table
94 .entry("package")
95 .or_insert_with(|| Item::Table(Table::new()));
96
97 let package_table = package
98 .as_table_like_mut()
99 .ok_or_else(|| ManifestError::MissingField {
100 path: path.to_path_buf(),
101 field: "workspace.package (as table)".to_string(),
102 })?;
103
104 package_table.insert("version", value(version.to_string()));
105
106 std::fs::write(path, doc.to_string()).map_err(|source| ManifestError::Write {
107 path: path.to_path_buf(),
108 source,
109 })
110}
111
112pub fn verify_version(path: &Path, expected: &Version) -> Result<(), ManifestError> {
117 let actual = read_version(path)?;
118
119 if actual != *expected {
120 return Err(ManifestError::VerificationFailed {
121 path: path.to_path_buf(),
122 expected: expected.to_string(),
123 actual: actual.to_string(),
124 });
125 }
126
127 Ok(())
128}
129
130pub fn write_metadata_section(
136 path: &Path,
137 section: MetadataSection,
138 config: &InitConfig,
139) -> Result<(), ManifestError> {
140 if config.is_empty() {
141 return Ok(());
142 }
143
144 let mut doc = read_document(path)?;
145
146 let changeset_table = navigate_to_changeset_table(&mut doc, section, path)?;
147 populate_changeset_table(changeset_table, config);
148
149 std::fs::write(path, doc.to_string()).map_err(|source| ManifestError::Write {
150 path: path.to_path_buf(),
151 source,
152 })
153}
154
155pub fn update_dependency_version(
167 path: &Path,
168 dependency_name: &str,
169 new_version: &Version,
170) -> Result<bool, ManifestError> {
171 let mut doc = read_document(path)?;
172 let mut changed = false;
173
174 if let Some(workspace) = doc.get_mut("workspace")
175 && let Some(deps) = workspace.get_mut("dependencies")
176 && update_dep_entry(deps, dependency_name, new_version)
177 {
178 changed = true;
179 }
180
181 for section in &DEPENDENCY_SECTIONS {
182 if let Some(deps) = doc.get_mut(section)
183 && update_dep_entry(deps, dependency_name, new_version)
184 {
185 changed = true;
186 }
187 }
188
189 if let Some(target_table) = doc.get_mut("target")
190 && let Some(target_table) = target_table.as_table_like_mut()
191 {
192 for (_, target_value) in target_table.iter_mut() {
193 for section in &DEPENDENCY_SECTIONS {
194 if let Some(deps) = target_value.get_mut(section)
195 && update_dep_entry(deps, dependency_name, new_version)
196 {
197 changed = true;
198 }
199 }
200 }
201 }
202
203 if changed {
204 std::fs::write(path, doc.to_string()).map_err(|source| ManifestError::Write {
205 path: path.to_path_buf(),
206 source,
207 })?;
208 }
209
210 Ok(changed)
211}
212
213fn update_dep_entry(deps: &mut Item, dep_name: &str, new_version: &Version) -> bool {
214 let Some(entry) = deps.get_mut(dep_name) else {
215 return false;
216 };
217
218 if let Some(table) = entry.as_table_like_mut() {
219 return update_versioned_table(table, new_version);
220 }
221
222 if entry.is_str() {
223 *entry = value(new_version.to_string());
224 return true;
225 }
226
227 false
228}
229
230fn update_versioned_table(table: &mut dyn TableLike, new_version: &Version) -> bool {
231 let has_workspace_true = table
232 .get("workspace")
233 .and_then(toml_edit::Item::as_bool)
234 .unwrap_or(false);
235 if has_workspace_true {
236 return false;
237 }
238
239 if table.get("version").is_some() {
240 table.insert("version", value(new_version.to_string()));
241 return true;
242 }
243
244 false
245}
246
247pub(crate) fn navigate_to_changeset_table<'a>(
248 doc: &'a mut DocumentMut,
249 section: MetadataSection,
250 path: &Path,
251) -> Result<&'a mut Table, ManifestError> {
252 let root_key = match section {
253 MetadataSection::Workspace => "workspace",
254 MetadataSection::Package => "package",
255 };
256
257 let root = doc
258 .entry(root_key)
259 .or_insert_with(|| Item::Table(Table::new()));
260
261 let root_table = root
262 .as_table_mut()
263 .ok_or_else(|| ManifestError::InvalidSectionType {
264 path: path.to_path_buf(),
265 section: root_key.to_string(),
266 })?;
267
268 let metadata = root_table
269 .entry("metadata")
270 .or_insert_with(|| Item::Table(Table::new()));
271
272 let metadata_table =
273 metadata
274 .as_table_mut()
275 .ok_or_else(|| ManifestError::InvalidSectionType {
276 path: path.to_path_buf(),
277 section: format!("{root_key}.metadata"),
278 })?;
279
280 let changeset = metadata_table
281 .entry("changeset")
282 .or_insert_with(|| Item::Table(Table::new()));
283
284 let changeset_table =
285 changeset
286 .as_table_mut()
287 .ok_or_else(|| ManifestError::InvalidSectionType {
288 path: path.to_path_buf(),
289 section: format!("{root_key}.metadata.changeset"),
290 })?;
291
292 changeset_table.set_implicit(true);
293
294 Ok(changeset_table)
295}
296
297fn populate_changeset_table(changeset_table: &mut Table, config: &InitConfig) {
298 if let Some(commit) = config.commit {
299 changeset_table.insert("commit", value(commit));
300 }
301
302 if let Some(tags) = config.tags {
303 changeset_table.insert("tags", value(tags));
304 }
305
306 if let Some(keep_changesets) = config.keep_changesets {
307 changeset_table.insert("keep-changesets", value(keep_changesets));
308 }
309
310 if let Some(tag_format) = config.tag_format {
311 changeset_table.insert("tag-format", value(tag_format.as_str()));
312 }
313
314 if let Some(changelog) = config.changelog {
315 changeset_table.insert("changelog", value(changelog.as_str()));
316 }
317
318 if let Some(comparison_links) = config.comparison_links {
319 changeset_table.insert("comparison-links", value(comparison_links.as_str()));
320 }
321
322 if let Some(zero_version_behavior) = config.zero_version_behavior {
323 changeset_table.insert(
324 "zero-version-behavior",
325 value(zero_version_behavior.as_str()),
326 );
327 }
328
329 if let Some(ref dependency_bump_changelog_template) = config.dependency_bump_changelog_template
330 {
331 changeset_table.insert(
332 "dependency-bump-changelog-template",
333 value(dependency_bump_changelog_template.as_str()),
334 );
335 }
336
337 if let Some(ref base_branch) = config.base_branch {
338 changeset_table.insert("base-branch", value(base_branch.as_str()));
339 }
340
341 if let Some(none_bump_behavior) = config.none_bump_behavior {
342 changeset_table.insert("none-bump-behavior", value(none_bump_behavior.as_str()));
343 }
344
345 if let Some(ref none_bump_promote_message_template) = config.none_bump_promote_message_template
346 {
347 changeset_table.insert(
348 "none-bump-promote-message-template",
349 value(none_bump_promote_message_template.as_str()),
350 );
351 }
352
353 if let Some(ref commit_title_template) = config.commit_title_template {
354 changeset_table.insert(
355 "commit-title-template",
356 value(commit_title_template.as_str()),
357 );
358 }
359
360 if let Some(changes_in_body) = config.changes_in_body {
361 changeset_table.insert("changes-in-body", value(changes_in_body));
362 }
363
364 if let Some(ref comparison_links_template) = config.comparison_links_template {
365 changeset_table.insert(
366 "comparison-links-template",
367 value(comparison_links_template.as_str()),
368 );
369 }
370
371 if let Some(ref ignored_files) = config.ignored_files
372 && !ignored_files.is_empty()
373 {
374 let mut arr = toml_edit::Array::new();
375 for pattern in ignored_files {
376 arr.push(pattern.as_str());
377 }
378 changeset_table.insert("ignored-files", Item::Value(toml_edit::Value::Array(arr)));
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn write_version_updates_package_version() {
388 let toml = r#"
389[package]
390name = "test-crate"
391version = "1.0.0"
392"#;
393 let dir = tempfile::tempdir().expect("create temp dir");
394 let path = dir.path().join("Cargo.toml");
395 std::fs::write(&path, toml).expect("write test file");
396
397 write_version(&path, &Version::new(2, 0, 0)).expect("write version");
398
399 let result = read_version(&path).expect("read version");
400 assert_eq!(result, Version::new(2, 0, 0));
401 }
402
403 #[test]
404 fn write_version_converts_inherited_to_literal() {
405 let toml = r#"
406[package]
407name = "test-crate"
408version.workspace = true
409"#;
410 let dir = tempfile::tempdir().expect("create temp dir");
411 let path = dir.path().join("Cargo.toml");
412 std::fs::write(&path, toml).expect("write test file");
413
414 write_version(&path, &Version::new(1, 5, 0)).expect("write version");
415
416 let result = read_version(&path).expect("read version");
417 assert_eq!(result, Version::new(1, 5, 0));
418
419 let content = std::fs::read_to_string(&path).expect("read file");
420 assert!(content.contains(r#"version = "1.5.0""#));
421 assert!(!content.contains("version.workspace"));
422 }
423
424 #[test]
425 fn write_version_preserves_comments() {
426 let toml = r#"# Package configuration
427[package]
428name = "test-crate"
429# Version comment
430version = "1.0.0"
431# After version comment
432edition = "2021"
433"#;
434 let dir = tempfile::tempdir().expect("create temp dir");
435 let path = dir.path().join("Cargo.toml");
436 std::fs::write(&path, toml).expect("write test file");
437
438 write_version(&path, &Version::new(2, 0, 0)).expect("write version");
439
440 let content = std::fs::read_to_string(&path).expect("read file");
441 assert!(content.contains("# Package configuration"));
442 assert!(content.contains("# After version comment"));
443 }
444
445 #[test]
446 fn remove_workspace_version_removes_field() {
447 let toml = r#"
448[workspace]
449members = ["crates/*"]
450
451[workspace.package]
452version = "1.0.0"
453edition = "2021"
454"#;
455 let dir = tempfile::tempdir().expect("create temp dir");
456 let path = dir.path().join("Cargo.toml");
457 std::fs::write(&path, toml).expect("write test file");
458
459 remove_workspace_version(&path).expect("remove workspace version");
460
461 let content = std::fs::read_to_string(&path).expect("read file");
462 assert!(!content.contains(r#"version = "1.0.0""#));
463 assert!(content.contains(r#"edition = "2021""#));
464 }
465
466 #[test]
467 fn remove_workspace_version_preserves_other_fields() {
468 let toml = r#"
469[workspace]
470members = ["crates/*"]
471
472[workspace.package]
473version = "1.0.0"
474edition = "2021"
475license = "MIT"
476"#;
477 let dir = tempfile::tempdir().expect("create temp dir");
478 let path = dir.path().join("Cargo.toml");
479 std::fs::write(&path, toml).expect("write test file");
480
481 remove_workspace_version(&path).expect("remove workspace version");
482
483 let content = std::fs::read_to_string(&path).expect("read file");
484 assert!(content.contains(r#"edition = "2021""#));
485 assert!(content.contains(r#"license = "MIT""#));
486 assert!(content.contains(r#"members = ["crates/*"]"#));
487 }
488
489 #[test]
490 fn verify_version_succeeds_when_matching() {
491 let toml = r#"
492[package]
493name = "test-crate"
494version = "1.2.3"
495"#;
496 let dir = tempfile::tempdir().expect("create temp dir");
497 let path = dir.path().join("Cargo.toml");
498 std::fs::write(&path, toml).expect("write test file");
499
500 verify_version(&path, &Version::new(1, 2, 3)).expect("verify version");
501 }
502
503 #[test]
504 fn verify_version_fails_when_mismatched() {
505 let toml = r#"
506[package]
507name = "test-crate"
508version = "1.0.0"
509"#;
510 let dir = tempfile::tempdir().expect("create temp dir");
511 let path = dir.path().join("Cargo.toml");
512 std::fs::write(&path, toml).expect("write test file");
513
514 let result = verify_version(&path, &Version::new(2, 0, 0));
515 assert!(matches!(
516 result,
517 Err(ManifestError::VerificationFailed { .. })
518 ));
519 }
520
521 #[test]
522 fn write_metadata_creates_workspace_section() {
523 let toml = r#"
524[workspace]
525members = ["crates/*"]
526"#;
527 let dir = tempfile::tempdir().expect("create temp dir");
528 let path = dir.path().join("Cargo.toml");
529 std::fs::write(&path, toml).expect("write test file");
530
531 let config = InitConfig {
532 commit: Some(true),
533 ..Default::default()
534 };
535
536 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
537
538 let content = std::fs::read_to_string(&path).expect("read file");
539 assert!(content.contains("[workspace.metadata.changeset]"));
540 assert!(content.contains("commit = true"));
541 }
542
543 #[test]
544 fn write_metadata_creates_package_section() {
545 let toml = r#"
546[package]
547name = "test-crate"
548version = "1.0.0"
549"#;
550 let dir = tempfile::tempdir().expect("create temp dir");
551 let path = dir.path().join("Cargo.toml");
552 std::fs::write(&path, toml).expect("write test file");
553
554 let config = InitConfig {
555 tags: Some(true),
556 ..Default::default()
557 };
558
559 write_metadata_section(&path, MetadataSection::Package, &config).expect("write metadata");
560
561 let content = std::fs::read_to_string(&path).expect("read file");
562 assert!(content.contains("[package.metadata.changeset]"));
563 assert!(content.contains("tags = true"));
564 }
565
566 #[test]
567 fn write_metadata_preserves_existing_content() {
568 let toml = r#"# Workspace configuration
569[workspace]
570# Members list
571members = ["crates/*"]
572
573[workspace.package]
574edition = "2021"
575"#;
576 let dir = tempfile::tempdir().expect("create temp dir");
577 let path = dir.path().join("Cargo.toml");
578 std::fs::write(&path, toml).expect("write test file");
579
580 let config = InitConfig {
581 commit: Some(true),
582 ..Default::default()
583 };
584
585 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
586
587 let content = std::fs::read_to_string(&path).expect("read file");
588 assert!(content.contains("# Workspace configuration"));
589 assert!(content.contains("# Members list"));
590 assert!(content.contains(r#"members = ["crates/*"]"#));
591 assert!(content.contains(r#"edition = "2021""#));
592 }
593
594 #[test]
595 fn write_metadata_updates_existing_section() {
596 let toml = r#"
597[workspace]
598members = ["crates/*"]
599
600[workspace.metadata.changeset]
601commit = false
602tags = false
603"#;
604 let dir = tempfile::tempdir().expect("create temp dir");
605 let path = dir.path().join("Cargo.toml");
606 std::fs::write(&path, toml).expect("write test file");
607
608 let config = InitConfig {
609 commit: Some(true),
610 tags: Some(true),
611 ..Default::default()
612 };
613
614 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
615
616 let content = std::fs::read_to_string(&path).expect("read file");
617 assert!(content.contains("commit = true"));
618 assert!(content.contains("tags = true"));
619 assert!(!content.contains("commit = false"));
620 assert!(!content.contains("tags = false"));
621 }
622
623 #[test]
624 fn write_metadata_creates_nested_hierarchy() {
625 let toml = r#"
626[workspace]
627members = ["crates/*"]
628"#;
629 let dir = tempfile::tempdir().expect("create temp dir");
630 let path = dir.path().join("Cargo.toml");
631 std::fs::write(&path, toml).expect("write test file");
632
633 let config = InitConfig {
634 commit: Some(true),
635 tags: Some(true),
636 ..Default::default()
637 };
638
639 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
640
641 let content = std::fs::read_to_string(&path).expect("read file");
642 assert!(content.contains("[workspace.metadata.changeset]"));
643 assert!(content.contains("commit = true"));
644 assert!(content.contains("tags = true"));
645 }
646
647 #[test]
648 fn write_metadata_merges_with_existing_metadata() {
649 let toml = r#"
650[workspace]
651members = ["crates/*"]
652
653[workspace.metadata.other]
654key = "value"
655"#;
656 let dir = tempfile::tempdir().expect("create temp dir");
657 let path = dir.path().join("Cargo.toml");
658 std::fs::write(&path, toml).expect("write test file");
659
660 let config = InitConfig {
661 commit: Some(true),
662 ..Default::default()
663 };
664
665 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
666
667 let content = std::fs::read_to_string(&path).expect("read file");
668 assert!(content.contains("[workspace.metadata.other]"));
669 assert!(content.contains(r#"key = "value""#));
670 assert!(content.contains("[workspace.metadata.changeset]"));
671 assert!(content.contains("commit = true"));
672 }
673
674 #[test]
675 fn write_metadata_handles_all_config_options() {
676 use crate::config::{
677 ChangelogLocation, ComparisonLinks, NoneBumpBehavior, TagFormat, ZeroVersionBehavior,
678 };
679
680 let toml = r#"
681[workspace]
682members = ["crates/*"]
683"#;
684 let dir = tempfile::tempdir().expect("create temp dir");
685 let path = dir.path().join("Cargo.toml");
686 std::fs::write(&path, toml).expect("write test file");
687
688 let config = InitConfig {
689 commit: Some(true),
690 tags: Some(true),
691 keep_changesets: Some(false),
692 tag_format: Some(TagFormat::CratePrefixed),
693 changelog: Some(ChangelogLocation::PerPackage),
694 comparison_links: Some(ComparisonLinks::Enabled),
695 zero_version_behavior: Some(ZeroVersionBehavior::AutoPromoteOnMajor),
696 dependency_bump_changelog_template: None,
697 base_branch: None,
698 none_bump_behavior: Some(NoneBumpBehavior::Disallow),
699 none_bump_promote_message_template: Some("My message".to_string()),
700 commit_title_template: Some("Release {new-version}".to_string()),
701 changes_in_body: Some(true),
702 comparison_links_template: Some(
703 "https://github.com/org/repo/compare/{base}...{target}".to_string(),
704 ),
705 ignored_files: Some(vec!["*.lock".to_string(), "docs/**".to_string()]),
706 };
707
708 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
709
710 let content = std::fs::read_to_string(&path).expect("read file");
711 assert!(content.contains("commit = true"));
712 assert!(content.contains("tags = true"));
713 assert!(content.contains("keep-changesets = false"));
714 assert!(content.contains(r#"tag-format = "crate-prefixed""#));
715 assert!(content.contains(r#"changelog = "per-package""#));
716 assert!(content.contains(r#"comparison-links = "enabled""#));
717 assert!(content.contains(r#"zero-version-behavior = "auto-promote-on-major""#));
718 assert!(content.contains(r#"none-bump-behavior = "disallow""#));
719 assert!(content.contains(r#"none-bump-promote-message-template = "My message""#));
720 assert!(content.contains(r#"commit-title-template = "Release {new-version}""#));
721 assert!(content.contains("changes-in-body = true"));
722 assert!(content.contains(
723 r#"comparison-links-template = "https://github.com/org/repo/compare/{base}...{target}""#
724 ));
725 assert!(content.contains(r#"ignored-files = ["*.lock", "docs/**"]"#));
726 }
727
728 #[test]
729 fn write_metadata_skips_none_values() {
730 let toml = r#"
731[workspace]
732members = ["crates/*"]
733"#;
734 let dir = tempfile::tempdir().expect("create temp dir");
735 let path = dir.path().join("Cargo.toml");
736 std::fs::write(&path, toml).expect("write test file");
737
738 let config = InitConfig {
739 commit: Some(true),
740 ..Default::default()
741 };
742
743 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
744
745 let content = std::fs::read_to_string(&path).expect("read file");
746 assert!(content.contains("commit = true"));
747 assert!(!content.contains("tags"));
748 assert!(!content.contains("keep-changesets"));
749 assert!(!content.contains("tag-format"));
750 assert!(!content.contains("changelog"));
751 assert!(!content.contains("comparison-links"));
752 assert!(!content.contains("zero-version-behavior"));
753 }
754
755 #[test]
756 fn write_metadata_writes_correct_enum_values() {
757 use crate::config::{ChangelogLocation, ComparisonLinks, TagFormat, ZeroVersionBehavior};
758
759 let toml = r#"
760[workspace]
761members = ["crates/*"]
762"#;
763 let dir = tempfile::tempdir().expect("create temp dir");
764 let path = dir.path().join("Cargo.toml");
765 std::fs::write(&path, toml).expect("write test file");
766
767 let config = InitConfig {
768 tag_format: Some(TagFormat::VersionOnly),
769 changelog: Some(ChangelogLocation::Root),
770 comparison_links: Some(ComparisonLinks::Auto),
771 zero_version_behavior: Some(ZeroVersionBehavior::EffectiveMinor),
772 ..Default::default()
773 };
774
775 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
776
777 let content = std::fs::read_to_string(&path).expect("read file");
778 assert!(content.contains(r#"tag-format = "version-only""#));
779 assert!(content.contains(r#"changelog = "root""#));
780 assert!(content.contains(r#"comparison-links = "auto""#));
781 assert!(content.contains(r#"zero-version-behavior = "effective-minor""#));
782 }
783
784 #[test]
785 fn write_metadata_empty_config_does_not_modify_file() {
786 let toml = r#"
787[workspace]
788members = ["crates/*"]
789"#;
790 let dir = tempfile::tempdir().expect("create temp dir");
791 let path = dir.path().join("Cargo.toml");
792 std::fs::write(&path, toml).expect("write test file");
793
794 let config = InitConfig::default();
795
796 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
797
798 let content = std::fs::read_to_string(&path).expect("read file");
799 assert!(!content.contains("metadata"));
800 assert!(!content.contains("changeset"));
801 }
802
803 #[test]
804 fn update_dep_version_updates_workspace_deps() {
805 let toml = r#"
806[workspace]
807members = ["crates/*"]
808
809[workspace.dependencies]
810my-crate = { path = "crates/my-crate", version = "1.0.0" }
811"#;
812 let dir = tempfile::tempdir().expect("create temp dir");
813 let path = dir.path().join("Cargo.toml");
814 std::fs::write(&path, toml).expect("write test file");
815
816 let result =
817 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
818 assert!(result);
819
820 let content = std::fs::read_to_string(&path).expect("read file");
821 assert!(content.contains(r#"version = "2.0.0""#));
822 assert!(!content.contains(r#"version = "1.0.0""#));
823 }
824
825 #[test]
826 fn update_dep_version_updates_regular_deps() {
827 let toml = r#"
828[package]
829name = "other-crate"
830version = "0.1.0"
831
832[dependencies]
833my-crate = { path = "../my-crate", version = "1.0.0" }
834"#;
835 let dir = tempfile::tempdir().expect("create temp dir");
836 let path = dir.path().join("Cargo.toml");
837 std::fs::write(&path, toml).expect("write test file");
838
839 let result =
840 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
841 assert!(result);
842
843 let content = std::fs::read_to_string(&path).expect("read file");
844 assert!(content.contains(r#"version = "2.0.0""#));
845 }
846
847 #[test]
848 fn update_dep_version_updates_dev_deps() {
849 let toml = r#"
850[package]
851name = "other-crate"
852version = "0.1.0"
853
854[dev-dependencies]
855my-crate = { path = "../my-crate", version = "1.0.0" }
856"#;
857 let dir = tempfile::tempdir().expect("create temp dir");
858 let path = dir.path().join("Cargo.toml");
859 std::fs::write(&path, toml).expect("write test file");
860
861 let result =
862 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
863 assert!(result);
864
865 let content = std::fs::read_to_string(&path).expect("read file");
866 assert!(content.contains(r#"version = "2.0.0""#));
867 }
868
869 #[test]
870 fn update_dep_version_updates_build_deps() {
871 let toml = r#"
872[package]
873name = "other-crate"
874version = "0.1.0"
875
876[build-dependencies]
877my-crate = { path = "../my-crate", version = "1.0.0" }
878"#;
879 let dir = tempfile::tempdir().expect("create temp dir");
880 let path = dir.path().join("Cargo.toml");
881 std::fs::write(&path, toml).expect("write test file");
882
883 let result =
884 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
885 assert!(result);
886
887 let content = std::fs::read_to_string(&path).expect("read file");
888 assert!(content.contains(r#"version = "2.0.0""#));
889 }
890
891 #[test]
892 fn update_dep_version_skips_workspace_true() {
893 let toml = r#"
894[package]
895name = "other-crate"
896version = "0.1.0"
897
898[dependencies]
899my-crate = { workspace = true }
900"#;
901 let dir = tempfile::tempdir().expect("create temp dir");
902 let path = dir.path().join("Cargo.toml");
903 std::fs::write(&path, toml).expect("write test file");
904
905 let result =
906 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
907 assert!(!result);
908
909 let content = std::fs::read_to_string(&path).expect("read file");
910 assert!(content.contains("workspace = true"));
911 assert!(!content.contains(r#"version = "2.0.0""#));
912 }
913
914 #[test]
915 fn update_dep_version_skips_no_version_key() {
916 let toml = r#"
917[package]
918name = "other-crate"
919version = "0.1.0"
920
921[dependencies]
922my-crate = { path = "../my-crate" }
923"#;
924 let dir = tempfile::tempdir().expect("create temp dir");
925 let path = dir.path().join("Cargo.toml");
926 std::fs::write(&path, toml).expect("write test file");
927
928 let result =
929 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
930 assert!(!result);
931
932 let content = std::fs::read_to_string(&path).expect("read file");
933 assert!(!content.contains(r#"version = "2.0.0""#));
934 }
935
936 #[test]
937 fn update_dep_version_skips_missing_dep() {
938 let toml = r#"
939[package]
940name = "other-crate"
941version = "0.1.0"
942
943[dependencies]
944some-other = "1.0.0"
945"#;
946 let dir = tempfile::tempdir().expect("create temp dir");
947 let path = dir.path().join("Cargo.toml");
948 std::fs::write(&path, toml).expect("write test file");
949
950 let result =
951 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
952 assert!(!result);
953 }
954
955 #[test]
956 fn update_dep_version_preserves_formatting() {
957 let toml = r#"# Root manifest
958[workspace]
959members = ["crates/*"]
960
961# Workspace deps
962[workspace.dependencies]
963my-crate = { path = "crates/my-crate", version = "1.0.0" }
964"#;
965 let dir = tempfile::tempdir().expect("create temp dir");
966 let path = dir.path().join("Cargo.toml");
967 std::fs::write(&path, toml).expect("write test file");
968
969 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
970
971 let content = std::fs::read_to_string(&path).expect("read file");
972 assert!(content.contains("# Root manifest"));
973 assert!(content.contains("# Workspace deps"));
974 }
975
976 #[test]
977 fn update_dep_version_updates_simple_string() {
978 let toml = r#"
979[package]
980name = "other-crate"
981version = "0.1.0"
982
983[dependencies]
984my-crate = "1.0.0"
985"#;
986 let dir = tempfile::tempdir().expect("create temp dir");
987 let path = dir.path().join("Cargo.toml");
988 std::fs::write(&path, toml).expect("write test file");
989
990 let result =
991 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
992 assert!(result);
993
994 let content = std::fs::read_to_string(&path).expect("read file");
995 assert!(content.contains(r#"my-crate = "2.0.0""#));
996 }
997
998 #[test]
999 fn write_metadata_serializes_dependency_bump_changelog_template() {
1000 let toml = r#"
1001[workspace]
1002members = ["crates/*"]
1003"#;
1004 let dir = tempfile::tempdir().expect("create temp dir");
1005 let path = dir.path().join("Cargo.toml");
1006 std::fs::write(&path, toml).expect("write test file");
1007
1008 let config = InitConfig {
1009 dependency_bump_changelog_template: Some(
1010 "Updated dependency `{dependency}` to v{version}".to_string(),
1011 ),
1012 ..Default::default()
1013 };
1014
1015 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
1016
1017 let content = std::fs::read_to_string(&path).expect("read file");
1018 assert!(content.contains("[workspace.metadata.changeset]"));
1019 assert!(content.contains(
1020 r#"dependency-bump-changelog-template = "Updated dependency `{dependency}` to v{version}""#
1021 ));
1022 }
1023
1024 #[test]
1025 fn update_dep_version_updates_multiple_sections() {
1026 let toml = r#"
1027[package]
1028name = "other-crate"
1029version = "0.1.0"
1030
1031[dependencies]
1032my-crate = { path = "../my-crate", version = "1.0.0" }
1033
1034[dev-dependencies]
1035my-crate = { path = "../my-crate", version = "1.0.0" }
1036"#;
1037 let dir = tempfile::tempdir().expect("create temp dir");
1038 let path = dir.path().join("Cargo.toml");
1039 std::fs::write(&path, toml).expect("write test file");
1040
1041 let result =
1042 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1043 assert!(result);
1044
1045 let content = std::fs::read_to_string(&path).expect("read file");
1046 assert!(!content.contains(r#"version = "1.0.0""#));
1047 assert_eq!(content.matches(r#"version = "2.0.0""#).count(), 2);
1048 }
1049
1050 #[test]
1051 fn update_dep_version_returns_true_on_change() {
1052 let toml = r#"
1053[workspace.dependencies]
1054my-crate = { path = "crates/my-crate", version = "1.0.0" }
1055"#;
1056 let dir = tempfile::tempdir().expect("create temp dir");
1057 let path = dir.path().join("Cargo.toml");
1058 std::fs::write(&path, toml).expect("write test file");
1059
1060 let changed =
1061 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1062 assert!(changed);
1063
1064 let not_changed = update_dependency_version(&path, "nonexistent", &Version::new(2, 0, 0))
1065 .expect("update");
1066 assert!(!not_changed);
1067 }
1068
1069 #[test]
1070 fn write_metadata_serializes_base_branch() {
1071 let toml = r#"
1072[workspace]
1073members = ["crates/*"]
1074"#;
1075 let dir = tempfile::tempdir().expect("create temp dir");
1076 let path = dir.path().join("Cargo.toml");
1077 std::fs::write(&path, toml).expect("write test file");
1078
1079 let config = InitConfig {
1080 base_branch: Some("develop".to_string()),
1081 ..Default::default()
1082 };
1083
1084 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
1085
1086 let content = std::fs::read_to_string(&path).expect("read file");
1087 assert!(content.contains("[workspace.metadata.changeset]"));
1088 assert!(content.contains(r#"base-branch = "develop""#));
1089 }
1090
1091 #[test]
1092 fn write_metadata_serializes_none_bump_behavior() {
1093 use crate::config::NoneBumpBehavior;
1094
1095 let toml = r#"
1096[workspace]
1097members = ["crates/*"]
1098"#;
1099 let dir = tempfile::tempdir().expect("create temp dir");
1100 let path = dir.path().join("Cargo.toml");
1101 std::fs::write(&path, toml).expect("write test file");
1102
1103 let config = InitConfig {
1104 none_bump_behavior: Some(NoneBumpBehavior::Disallow),
1105 ..Default::default()
1106 };
1107
1108 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
1109
1110 let content = std::fs::read_to_string(&path).expect("read file");
1111 assert!(content.contains("[workspace.metadata.changeset]"));
1112 assert!(content.contains(r#"none-bump-behavior = "disallow""#));
1113 }
1114
1115 #[test]
1116 fn write_metadata_serializes_none_bump_promote_message_template() {
1117 let toml = r#"
1118[workspace]
1119members = ["crates/*"]
1120"#;
1121 let dir = tempfile::tempdir().expect("create temp dir");
1122 let path = dir.path().join("Cargo.toml");
1123 std::fs::write(&path, toml).expect("write test file");
1124
1125 let config = InitConfig {
1126 none_bump_promote_message_template: Some("chore: internal refactor".to_string()),
1127 ..Default::default()
1128 };
1129
1130 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
1131
1132 let content = std::fs::read_to_string(&path).expect("read file");
1133 assert!(content.contains("[workspace.metadata.changeset]"));
1134 assert!(
1135 content.contains(r#"none-bump-promote-message-template = "chore: internal refactor""#)
1136 );
1137 }
1138
1139 #[test]
1140 fn write_metadata_serializes_commit_title_template() {
1141 let toml = r#"
1142[workspace]
1143members = ["crates/*"]
1144"#;
1145 let dir = tempfile::tempdir().expect("create temp dir");
1146 let path = dir.path().join("Cargo.toml");
1147 std::fs::write(&path, toml).expect("write test file");
1148
1149 let config = InitConfig {
1150 commit_title_template: Some("Release {new-version}".to_string()),
1151 ..Default::default()
1152 };
1153
1154 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
1155
1156 let content = std::fs::read_to_string(&path).expect("read file");
1157 assert!(content.contains(r#"commit-title-template = "Release {new-version}""#));
1158 }
1159
1160 #[test]
1161 fn write_metadata_serializes_changes_in_body() {
1162 let toml = r#"
1163[workspace]
1164members = ["crates/*"]
1165"#;
1166 let dir = tempfile::tempdir().expect("create temp dir");
1167 let path = dir.path().join("Cargo.toml");
1168 std::fs::write(&path, toml).expect("write test file");
1169
1170 let config = InitConfig {
1171 changes_in_body: Some(false),
1172 ..Default::default()
1173 };
1174
1175 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
1176
1177 let content = std::fs::read_to_string(&path).expect("read file");
1178 assert!(content.contains("changes-in-body = false"));
1179 }
1180
1181 #[test]
1182 fn write_metadata_serializes_comparison_links_template() {
1183 let toml = r#"
1184[workspace]
1185members = ["crates/*"]
1186"#;
1187 let dir = tempfile::tempdir().expect("create temp dir");
1188 let path = dir.path().join("Cargo.toml");
1189 std::fs::write(&path, toml).expect("write test file");
1190
1191 let config = InitConfig {
1192 comparison_links_template: Some(
1193 "https://github.com/{repository}/compare/{base}...{target}".to_string(),
1194 ),
1195 ..Default::default()
1196 };
1197
1198 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
1199
1200 let content = std::fs::read_to_string(&path).expect("read file");
1201 assert!(content.contains(
1202 r#"comparison-links-template = "https://github.com/{repository}/compare/{base}...{target}""#
1203 ));
1204 }
1205
1206 #[test]
1207 fn write_metadata_serializes_ignored_files() {
1208 let toml = r#"
1209[workspace]
1210members = ["crates/*"]
1211"#;
1212 let dir = tempfile::tempdir().expect("create temp dir");
1213 let path = dir.path().join("Cargo.toml");
1214 std::fs::write(&path, toml).expect("write test file");
1215
1216 let config = InitConfig {
1217 ignored_files: Some(vec!["*.md".to_string(), "docs/**".to_string()]),
1218 ..Default::default()
1219 };
1220
1221 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
1222
1223 let content = std::fs::read_to_string(&path).expect("read file");
1224 assert!(content.contains(r#"ignored-files = ["*.md", "docs/**"]"#));
1225 }
1226
1227 #[test]
1228 fn write_metadata_skips_empty_ignored_files() {
1229 let toml = r#"
1230[workspace]
1231members = ["crates/*"]
1232"#;
1233 let dir = tempfile::tempdir().expect("create temp dir");
1234 let path = dir.path().join("Cargo.toml");
1235 std::fs::write(&path, toml).expect("write test file");
1236
1237 let config = InitConfig {
1238 ignored_files: Some(vec![]),
1239 commit: Some(true),
1240 ..Default::default()
1241 };
1242
1243 write_metadata_section(&path, MetadataSection::Workspace, &config).expect("write metadata");
1244
1245 let content = std::fs::read_to_string(&path).expect("read file");
1246 assert!(!content.contains("ignored-files"));
1247 assert!(content.contains("commit = true"));
1248 }
1249
1250 #[test]
1251 fn update_dep_version_updates_target_deps() {
1252 let toml = r#"
1253[package]
1254name = "other-crate"
1255version = "0.1.0"
1256
1257[target.'cfg(target_os = "linux")'.dependencies]
1258my-crate = { path = "../my-crate", version = "1.0.0" }
1259"#;
1260 let dir = tempfile::tempdir().expect("create temp dir");
1261 let path = dir.path().join("Cargo.toml");
1262 std::fs::write(&path, toml).expect("write test file");
1263
1264 let result =
1265 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1266 assert!(result);
1267
1268 let content = std::fs::read_to_string(&path).expect("read file");
1269 assert!(content.contains(r#"version = "2.0.0""#));
1270 assert!(!content.contains(r#"version = "1.0.0""#));
1271 }
1272
1273 #[test]
1274 fn update_dep_version_updates_target_dev_deps() {
1275 let toml = r#"
1276[package]
1277name = "other-crate"
1278version = "0.1.0"
1279
1280[target.'cfg(target_os = "linux")'.dev-dependencies]
1281my-crate = { path = "../my-crate", version = "1.0.0" }
1282"#;
1283 let dir = tempfile::tempdir().expect("create temp dir");
1284 let path = dir.path().join("Cargo.toml");
1285 std::fs::write(&path, toml).expect("write test file");
1286
1287 let result =
1288 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1289 assert!(result);
1290
1291 let content = std::fs::read_to_string(&path).expect("read file");
1292 assert!(content.contains(r#"version = "2.0.0""#));
1293 }
1294
1295 #[test]
1296 fn update_dep_version_updates_target_build_deps() {
1297 let toml = r#"
1298[package]
1299name = "other-crate"
1300version = "0.1.0"
1301
1302[target.'cfg(target_os = "linux")'.build-dependencies]
1303my-crate = { path = "../my-crate", version = "1.0.0" }
1304"#;
1305 let dir = tempfile::tempdir().expect("create temp dir");
1306 let path = dir.path().join("Cargo.toml");
1307 std::fs::write(&path, toml).expect("write test file");
1308
1309 let result =
1310 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1311 assert!(result);
1312
1313 let content = std::fs::read_to_string(&path).expect("read file");
1314 assert!(content.contains(r#"version = "2.0.0""#));
1315 }
1316
1317 #[test]
1318 fn update_dep_version_updates_multiple_targets() {
1319 let toml = r#"
1320[package]
1321name = "other-crate"
1322version = "0.1.0"
1323
1324[target.'cfg(target_os = "linux")'.dependencies]
1325my-crate = { path = "../my-crate", version = "1.0.0" }
1326
1327[target.'cfg(target_os = "windows")'.dependencies]
1328my-crate = { path = "../my-crate", version = "1.0.0" }
1329"#;
1330 let dir = tempfile::tempdir().expect("create temp dir");
1331 let path = dir.path().join("Cargo.toml");
1332 std::fs::write(&path, toml).expect("write test file");
1333
1334 let result =
1335 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1336 assert!(result);
1337
1338 let content = std::fs::read_to_string(&path).expect("read file");
1339 assert!(!content.contains(r#"version = "1.0.0""#));
1340 assert_eq!(content.matches(r#"version = "2.0.0""#).count(), 2);
1341 }
1342
1343 #[test]
1344 fn update_dep_version_skips_target_workspace_true() {
1345 let toml = r#"
1346[package]
1347name = "other-crate"
1348version = "0.1.0"
1349
1350[target.'cfg(target_os = "linux")'.dependencies]
1351my-crate = { workspace = true }
1352"#;
1353 let dir = tempfile::tempdir().expect("create temp dir");
1354 let path = dir.path().join("Cargo.toml");
1355 std::fs::write(&path, toml).expect("write test file");
1356
1357 let result =
1358 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1359 assert!(!result);
1360
1361 let content = std::fs::read_to_string(&path).expect("read file");
1362 assert!(content.contains("workspace = true"));
1363 assert!(!content.contains(r#"version = "2.0.0""#));
1364 }
1365
1366 #[test]
1367 fn update_dep_version_skips_target_no_version_key() {
1368 let toml = r#"
1369[package]
1370name = "other-crate"
1371version = "0.1.0"
1372
1373[target.'cfg(target_os = "linux")'.dependencies]
1374my-crate = { path = "../my-crate" }
1375"#;
1376 let dir = tempfile::tempdir().expect("create temp dir");
1377 let path = dir.path().join("Cargo.toml");
1378 std::fs::write(&path, toml).expect("write test file");
1379
1380 let result =
1381 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1382 assert!(!result);
1383
1384 let content = std::fs::read_to_string(&path).expect("read file");
1385 assert!(!content.contains(r#"version = "2.0.0""#));
1386 }
1387
1388 #[test]
1389 fn update_dep_version_updates_target_and_regular() {
1390 let toml = r#"
1391[package]
1392name = "other-crate"
1393version = "0.1.0"
1394
1395[dependencies]
1396my-crate = { path = "../my-crate", version = "1.0.0" }
1397
1398[target.'cfg(target_os = "linux")'.dependencies]
1399my-crate = { path = "../my-crate", version = "1.0.0" }
1400"#;
1401 let dir = tempfile::tempdir().expect("create temp dir");
1402 let path = dir.path().join("Cargo.toml");
1403 std::fs::write(&path, toml).expect("write test file");
1404
1405 let result =
1406 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1407 assert!(result);
1408
1409 let content = std::fs::read_to_string(&path).expect("read file");
1410 assert!(!content.contains(r#"version = "1.0.0""#));
1411 assert_eq!(content.matches(r#"version = "2.0.0""#).count(), 2);
1412 }
1413
1414 #[test]
1415 fn update_dep_version_preserves_target_formatting() {
1416 let toml = r#"# Package manifest
1417[package]
1418name = "other-crate"
1419version = "0.1.0"
1420
1421# Linux-specific deps
1422[target.'cfg(target_os = "linux")'.dependencies]
1423my-crate = { path = "../my-crate", version = "1.0.0" }
1424"#;
1425 let dir = tempfile::tempdir().expect("create temp dir");
1426 let path = dir.path().join("Cargo.toml");
1427 std::fs::write(&path, toml).expect("write test file");
1428
1429 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1430
1431 let content = std::fs::read_to_string(&path).expect("read file");
1432 assert!(content.contains("# Package manifest"));
1433 assert!(content.contains("# Linux-specific deps"));
1434 assert!(content.contains(r#"version = "2.0.0""#));
1435 }
1436
1437 #[test]
1438 fn update_dep_version_updates_target_simple_string() {
1439 let toml = r#"
1440[package]
1441name = "other-crate"
1442version = "0.1.0"
1443
1444[target.'cfg(target_os = "linux")'.dependencies]
1445my-crate = "1.0.0"
1446"#;
1447 let dir = tempfile::tempdir().expect("create temp dir");
1448 let path = dir.path().join("Cargo.toml");
1449 std::fs::write(&path, toml).expect("write test file");
1450
1451 let result =
1452 update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update");
1453 assert!(result);
1454
1455 let content = std::fs::read_to_string(&path).expect("read file");
1456 assert!(content.contains(r#"my-crate = "2.0.0""#));
1457 }
1458}