diff --git a/.changeset/changesets/constantly-notable-frog.md b/.changeset/changesets/constantly-notable-frog.md new file mode 100644 index 0000000..d5324f9 --- /dev/null +++ b/.changeset/changesets/constantly-notable-frog.md @@ -0,0 +1,5 @@ +--- +category: added +changeset-operations: minor +--- +Add integration tests for target-specific dependency handling during release diff --git a/.changeset/changesets/diffusely-dandy-goldcrest.md b/.changeset/changesets/diffusely-dandy-goldcrest.md new file mode 100644 index 0000000..c6b9698 --- /dev/null +++ b/.changeset/changesets/diffusely-dandy-goldcrest.md @@ -0,0 +1,4 @@ +--- +changeset-manifest: patch +--- +Handle simple string dependency entries during version updates diff --git a/.changeset/changesets/giddily-valid-gaur.md b/.changeset/changesets/giddily-valid-gaur.md new file mode 100644 index 0000000..64c8679 --- /dev/null +++ b/.changeset/changesets/giddily-valid-gaur.md @@ -0,0 +1,5 @@ +--- +category: added +cargo-changeset: minor +--- +Add end-to-end tests for target-specific dependency version updates during release diff --git a/.changeset/changesets/imminently-exuberant-teal.md b/.changeset/changesets/imminently-exuberant-teal.md new file mode 100644 index 0000000..848175f --- /dev/null +++ b/.changeset/changesets/imminently-exuberant-teal.md @@ -0,0 +1,5 @@ +--- +category: added +changeset-manifest: minor +--- +Update dependency versions in `[target.'...'.dependencies]`, `[target.'...'.dev-dependencies]`, and `[target.'...'.build-dependencies]` sections diff --git a/.changeset/changesets/mundanely-spacious-magpie.md b/.changeset/changesets/mundanely-spacious-magpie.md new file mode 100644 index 0000000..5696433 --- /dev/null +++ b/.changeset/changesets/mundanely-spacious-magpie.md @@ -0,0 +1,4 @@ +--- +cargo-changeset: patch +--- +Handle simple string dependency entries during version updates diff --git a/.changeset/changesets/youthfully-exultant-krill.md b/.changeset/changesets/youthfully-exultant-krill.md new file mode 100644 index 0000000..dcd767c --- /dev/null +++ b/.changeset/changesets/youthfully-exultant-krill.md @@ -0,0 +1,5 @@ +--- +category: added +changeset-project: minor +--- +Support `[target.'...'.dependencies]` and `[target.'...'.build-dependencies]` sections in the workspace dependency graph diff --git a/README.md b/README.md index 8d561cf..e3ccf16 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,26 @@ For detailed usage beyond what is covered here, run ### When to add a changeset -Add a changeset whenever your changes affect the behavior, API, or -dependencies of one or more crates in this workspace. Do NOT add a changeset -for changes that are invisible to users of the crate, such as CI -configuration, documentation-only edits, or test-only refactors. +Run `cargo changeset verify` to determine which crates need changeset +coverage. It is the single source of truth — add changesets for every crate +it reports as uncovered. + +### IMPORTANT: Changeset workflow + +`cargo changeset verify` operates in two modes: + +1. **Dirty working directory** — verifies only the uncommitted changes +2. **Clean working directory** — verifies all branch changes against the base branch + +The correct workflow is: + +1. Make code changes (working directory is now dirty) +2. Run `cargo changeset verify` — dirty mode reports which crates need coverage +3. Add changesets for uncovered crates +4. Run `cargo changeset verify` again — should now pass (dirty state includes both code + changesets) +5. Commit everything together + +**NEVER commit without `cargo changeset verify` passing first.** ### How to add a changeset @@ -81,10 +97,14 @@ Run the following command (no interactive prompts): ```bash cargo changeset add \ - --package-bump : \ + --package-bump : \ + --category \ -m "" ``` +The `--category` flag defaults to `changed` if omitted. It determines which +CHANGELOG section the entry appears under. + For changes affecting multiple crates, repeat `--package-bump` for each: ```bash @@ -99,28 +119,20 @@ cargo changeset add \ - `major` — breaking changes to the public API - `minor` — new functionality that is backwards compatible - `patch` — bug fixes and backwards-compatible corrections +- `none` — internal changes invisible to crate consumers ### Writing the description The changeset description appears in the CHANGELOG and is read by users of the crate, not its developers. Write it from the perspective of someone who depends on the crate and wants to know what changed and how it affects them. -Keep it to a single sentence when possible. +Keep it to a single sentence when possible. Use markdown notation where +appropriate (e.g. backticks for code references). Good: "Add `--timeout` flag to control request deadline" Good: "Fix panic when parsing empty configuration files" Bad: "Refactored the timeout module and added a CLI flag" Bad: "Fixed bug in config.rs line 42" - -### Verifying coverage - -After adding a changeset, verify that all affected crates are covered: - -```bash -cargo changeset verify --base main -``` - -Exit code 0 means all changed crates have coverage. ```` --- diff --git a/crates/cargo-changeset/tests/release_command.rs b/crates/cargo-changeset/tests/release_command.rs index f1dde99..724f54e 100644 --- a/crates/cargo-changeset/tests/release_command.rs +++ b/crates/cargo-changeset/tests/release_command.rs @@ -4,7 +4,7 @@ use std::process::Command; use changeset_test_helpers::changesets::{write_changeset, write_multi_changeset}; use changeset_test_helpers::git::{git_add_and_commit, init_git_repo}; use changeset_test_helpers::workspaces::{ - add_helm_chart_config, create_workspace_with_cascade_chain, + WorkspaceBuilder, add_helm_chart_config, create_workspace_with_cascade_chain, create_workspace_with_circular_version_tracking, create_workspace_with_deeply_nested_json_field, create_workspace_with_duplicate_dependency, create_workspace_with_helm_chart, create_workspace_with_invalid_version_field_path, @@ -675,3 +675,165 @@ fn release_version_tracking_deeply_nested_field_paths() { "expected deeply nested upstream_crate version updated to 1.0.1, got:\n{manifest_json}" ); } + +#[test] +fn release_updates_target_specific_dependency_versions() { + let workspace = WorkspaceBuilder::virtual_workspace() + .with_git() + .with_changeset_dir() + .crate_member("crate-a", "1.0.0") + .crate_member("crate-b", "2.0.0") + .crate_toml_extra( + "crate-b", + r#" +[dependencies] +crate-a = { path = "../crate-a", version = "1.0.0" } + +[target.'cfg(target_os = "linux")'.dependencies] +crate-a = { path = "../crate-a", version = "1.0.0" } +"#, + ) + .build(); + + let lockfile_output = Command::new("cargo") + .args(["generate-lockfile"]) + .current_dir(workspace.path()) + .output() + .expect("failed to run cargo generate-lockfile"); + assert!( + lockfile_output.status.success(), + "cargo generate-lockfile failed: {}", + String::from_utf8_lossy(&lockfile_output.stderr) + ); + + git_add_and_commit(&workspace, "Initial commit"); + write_changeset(&workspace, "bump-a.md", "crate-a", "minor", "Add feature"); + git_add_and_commit(&workspace, "Add changeset"); + + assert_cmd::cargo::cargo_bin_cmd!("cargo-changeset") + .arg("release") + .current_dir(workspace.path()) + .assert() + .success(); + + let content = fs::read_to_string(workspace.path().join("crates/crate-b/Cargo.toml")) + .expect("read crate-b Cargo.toml"); + + assert!( + !content.contains(r#"version = "1.0.0""#), + "old version should not remain in crate-b Cargo.toml, got:\n{content}" + ); + assert_eq!( + content.matches(r#"version = "1.1.0""#).count(), + 2, + "both [dependencies] and target-specific section should have 1.1.0, got:\n{content}" + ); +} + +#[test] +fn release_skips_path_only_dependency() { + let workspace = WorkspaceBuilder::virtual_workspace() + .with_git() + .with_changeset_dir() + .crate_member("crate-a", "1.0.0") + .crate_member("crate-b", "2.0.0") + .crate_toml_extra( + "crate-b", + r#" +[dependencies] +crate-a = { path = "../crate-a" } +"#, + ) + .build(); + + let lockfile_output = Command::new("cargo") + .args(["generate-lockfile"]) + .current_dir(workspace.path()) + .output() + .expect("failed to run cargo generate-lockfile"); + assert!( + lockfile_output.status.success(), + "cargo generate-lockfile failed: {}", + String::from_utf8_lossy(&lockfile_output.stderr) + ); + + git_add_and_commit(&workspace, "Initial commit"); + write_changeset(&workspace, "bump-a.md", "crate-a", "minor", "Add feature"); + git_add_and_commit(&workspace, "Add changeset"); + + assert_cmd::cargo::cargo_bin_cmd!("cargo-changeset") + .arg("release") + .current_dir(workspace.path()) + .assert() + .success(); + + let content = fs::read_to_string(workspace.path().join("crates/crate-b/Cargo.toml")) + .expect("read crate-b Cargo.toml"); + + assert!( + content.contains(r#"path = "../crate-a""#), + "path-only dependency should still have path, got:\n{content}" + ); + assert!( + !content.contains("1.1.0"), + "path-only dependency should not get a version added, got:\n{content}" + ); +} + +#[test] +fn release_skips_workspace_true_dependency() { + let workspace = WorkspaceBuilder::virtual_workspace() + .with_git() + .with_changeset_dir() + .crate_member("crate-a", "1.0.0") + .crate_member("crate-b", "2.0.0") + .workspace_toml_extra( + r#" +[workspace.dependencies] +crate-a = { path = "crates/crate-a", version = "1.0.0" } +"#, + ) + .crate_toml_extra( + "crate-b", + r#" +[dependencies] +crate-a = { workspace = true } +"#, + ) + .build(); + + let lockfile_output = Command::new("cargo") + .args(["generate-lockfile"]) + .current_dir(workspace.path()) + .output() + .expect("failed to run cargo generate-lockfile"); + assert!( + lockfile_output.status.success(), + "cargo generate-lockfile failed: {}", + String::from_utf8_lossy(&lockfile_output.stderr) + ); + + git_add_and_commit(&workspace, "Initial commit"); + write_changeset(&workspace, "bump-a.md", "crate-a", "minor", "Add feature"); + git_add_and_commit(&workspace, "Add changeset"); + + assert_cmd::cargo::cargo_bin_cmd!("cargo-changeset") + .arg("release") + .current_dir(workspace.path()) + .assert() + .success(); + + let crate_b_content = fs::read_to_string(workspace.path().join("crates/crate-b/Cargo.toml")) + .expect("read crate-b Cargo.toml"); + assert!( + crate_b_content.contains("workspace = true"), + "workspace = true should remain unchanged in crate-b, got:\n{crate_b_content}" + ); + + let root_content = + fs::read_to_string(workspace.path().join("Cargo.toml")).expect("read root Cargo.toml"); + assert!( + root_content.contains(r#"version = "1.1.0""#), + "workspace.dependencies version should be updated to 1.1.0, got:\n{root_content}" + ); +} diff --git a/crates/changeset-manifest/src/writer.rs b/crates/changeset-manifest/src/writer.rs index 1bd3ae5..d61c757 100644 --- a/crates/changeset-manifest/src/writer.rs +++ b/crates/changeset-manifest/src/writer.rs @@ -1,7 +1,7 @@ use std::path::Path; use semver::Version; -use toml_edit::{DocumentMut, Item, Table, value}; +use toml_edit::{DocumentMut, Item, Table, TableLike, value}; use crate::config::{InitConfig, MetadataSection}; use crate::error::ManifestError; @@ -155,8 +155,10 @@ pub fn write_metadata_section( /// Updates the version of a dependency in all relevant sections of a Cargo.toml. /// /// Checks `[workspace.dependencies]`, `[dependencies]`, `[dev-dependencies]`, -/// and `[build-dependencies]`. Only updates table-form entries that have an -/// explicit `version` key and do NOT have `workspace = true`. +/// `[build-dependencies]`, and all `[target.'...'.dependencies]` (including +/// `dev-dependencies` and `build-dependencies` under each target). Only updates +/// table-form entries that have an explicit `version` key and do NOT have +/// `workspace = true`. /// /// # Errors /// @@ -184,6 +186,20 @@ pub fn update_dependency_version( } } + if let Some(target_table) = doc.get_mut("target") + && let Some(target_table) = target_table.as_table_like_mut() + { + for (_, target_value) in target_table.iter_mut() { + for section in &DEPENDENCY_SECTIONS { + if let Some(deps) = target_value.get_mut(section) + && update_dep_entry(deps, dependency_name, new_version) + { + changed = true; + } + } + } + } + if changed { std::fs::write(path, doc.to_string()).map_err(|source| ManifestError::Write { path: path.to_path_buf(), @@ -200,18 +216,29 @@ fn update_dep_entry(deps: &mut Item, dep_name: &str, new_version: &Version) -> b }; if let Some(table) = entry.as_table_like_mut() { - let has_workspace_true = table - .get("workspace") - .and_then(toml_edit::Item::as_bool) - .unwrap_or(false); - if has_workspace_true { - return false; - } + return update_versioned_table(table, new_version); + } - if table.get("version").is_some() { - table.insert("version", value(new_version.to_string())); - return true; - } + if entry.is_str() { + *entry = value(new_version.to_string()); + return true; + } + + false +} + +fn update_versioned_table(table: &mut dyn TableLike, new_version: &Version) -> bool { + let has_workspace_true = table + .get("workspace") + .and_then(toml_edit::Item::as_bool) + .unwrap_or(false); + if has_workspace_true { + return false; + } + + if table.get("version").is_some() { + table.insert("version", value(new_version.to_string())); + return true; } false @@ -947,7 +974,7 @@ my-crate = { path = "crates/my-crate", version = "1.0.0" } } #[test] - fn update_dep_version_skips_simple_string() { + fn update_dep_version_updates_simple_string() { let toml = r#" [package] name = "other-crate" @@ -962,10 +989,10 @@ my-crate = "1.0.0" let result = update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update"); - assert!(!result); + assert!(result); let content = std::fs::read_to_string(&path).expect("read file"); - assert!(content.contains(r#"my-crate = "1.0.0""#)); + assert!(content.contains(r#"my-crate = "2.0.0""#)); } #[test] @@ -1219,4 +1246,213 @@ members = ["crates/*"] assert!(!content.contains("ignored-files")); assert!(content.contains("commit = true")); } + + #[test] + fn update_dep_version_updates_target_deps() { + let toml = r#" +[package] +name = "other-crate" +version = "0.1.0" + +[target.'cfg(target_os = "linux")'.dependencies] +my-crate = { path = "../my-crate", version = "1.0.0" } +"#; + let dir = tempfile::tempdir().expect("create temp dir"); + let path = dir.path().join("Cargo.toml"); + std::fs::write(&path, toml).expect("write test file"); + + let result = + update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update"); + assert!(result); + + let content = std::fs::read_to_string(&path).expect("read file"); + assert!(content.contains(r#"version = "2.0.0""#)); + assert!(!content.contains(r#"version = "1.0.0""#)); + } + + #[test] + fn update_dep_version_updates_target_dev_deps() { + let toml = r#" +[package] +name = "other-crate" +version = "0.1.0" + +[target.'cfg(target_os = "linux")'.dev-dependencies] +my-crate = { path = "../my-crate", version = "1.0.0" } +"#; + let dir = tempfile::tempdir().expect("create temp dir"); + let path = dir.path().join("Cargo.toml"); + std::fs::write(&path, toml).expect("write test file"); + + let result = + update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update"); + assert!(result); + + let content = std::fs::read_to_string(&path).expect("read file"); + assert!(content.contains(r#"version = "2.0.0""#)); + } + + #[test] + fn update_dep_version_updates_target_build_deps() { + let toml = r#" +[package] +name = "other-crate" +version = "0.1.0" + +[target.'cfg(target_os = "linux")'.build-dependencies] +my-crate = { path = "../my-crate", version = "1.0.0" } +"#; + let dir = tempfile::tempdir().expect("create temp dir"); + let path = dir.path().join("Cargo.toml"); + std::fs::write(&path, toml).expect("write test file"); + + let result = + update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update"); + assert!(result); + + let content = std::fs::read_to_string(&path).expect("read file"); + assert!(content.contains(r#"version = "2.0.0""#)); + } + + #[test] + fn update_dep_version_updates_multiple_targets() { + let toml = r#" +[package] +name = "other-crate" +version = "0.1.0" + +[target.'cfg(target_os = "linux")'.dependencies] +my-crate = { path = "../my-crate", version = "1.0.0" } + +[target.'cfg(target_os = "windows")'.dependencies] +my-crate = { path = "../my-crate", version = "1.0.0" } +"#; + let dir = tempfile::tempdir().expect("create temp dir"); + let path = dir.path().join("Cargo.toml"); + std::fs::write(&path, toml).expect("write test file"); + + let result = + update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update"); + assert!(result); + + let content = std::fs::read_to_string(&path).expect("read file"); + assert!(!content.contains(r#"version = "1.0.0""#)); + assert_eq!(content.matches(r#"version = "2.0.0""#).count(), 2); + } + + #[test] + fn update_dep_version_skips_target_workspace_true() { + let toml = r#" +[package] +name = "other-crate" +version = "0.1.0" + +[target.'cfg(target_os = "linux")'.dependencies] +my-crate = { workspace = true } +"#; + let dir = tempfile::tempdir().expect("create temp dir"); + let path = dir.path().join("Cargo.toml"); + std::fs::write(&path, toml).expect("write test file"); + + let result = + update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update"); + assert!(!result); + + let content = std::fs::read_to_string(&path).expect("read file"); + assert!(content.contains("workspace = true")); + assert!(!content.contains(r#"version = "2.0.0""#)); + } + + #[test] + fn update_dep_version_skips_target_no_version_key() { + let toml = r#" +[package] +name = "other-crate" +version = "0.1.0" + +[target.'cfg(target_os = "linux")'.dependencies] +my-crate = { path = "../my-crate" } +"#; + let dir = tempfile::tempdir().expect("create temp dir"); + let path = dir.path().join("Cargo.toml"); + std::fs::write(&path, toml).expect("write test file"); + + let result = + update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update"); + assert!(!result); + + let content = std::fs::read_to_string(&path).expect("read file"); + assert!(!content.contains(r#"version = "2.0.0""#)); + } + + #[test] + fn update_dep_version_updates_target_and_regular() { + let toml = r#" +[package] +name = "other-crate" +version = "0.1.0" + +[dependencies] +my-crate = { path = "../my-crate", version = "1.0.0" } + +[target.'cfg(target_os = "linux")'.dependencies] +my-crate = { path = "../my-crate", version = "1.0.0" } +"#; + let dir = tempfile::tempdir().expect("create temp dir"); + let path = dir.path().join("Cargo.toml"); + std::fs::write(&path, toml).expect("write test file"); + + let result = + update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update"); + assert!(result); + + let content = std::fs::read_to_string(&path).expect("read file"); + assert!(!content.contains(r#"version = "1.0.0""#)); + assert_eq!(content.matches(r#"version = "2.0.0""#).count(), 2); + } + + #[test] + fn update_dep_version_preserves_target_formatting() { + let toml = r#"# Package manifest +[package] +name = "other-crate" +version = "0.1.0" + +# Linux-specific deps +[target.'cfg(target_os = "linux")'.dependencies] +my-crate = { path = "../my-crate", version = "1.0.0" } +"#; + let dir = tempfile::tempdir().expect("create temp dir"); + let path = dir.path().join("Cargo.toml"); + std::fs::write(&path, toml).expect("write test file"); + + update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update"); + + let content = std::fs::read_to_string(&path).expect("read file"); + assert!(content.contains("# Package manifest")); + assert!(content.contains("# Linux-specific deps")); + assert!(content.contains(r#"version = "2.0.0""#)); + } + + #[test] + fn update_dep_version_updates_target_simple_string() { + let toml = r#" +[package] +name = "other-crate" +version = "0.1.0" + +[target.'cfg(target_os = "linux")'.dependencies] +my-crate = "1.0.0" +"#; + let dir = tempfile::tempdir().expect("create temp dir"); + let path = dir.path().join("Cargo.toml"); + std::fs::write(&path, toml).expect("write test file"); + + let result = + update_dependency_version(&path, "my-crate", &Version::new(2, 0, 0)).expect("update"); + assert!(result); + + let content = std::fs::read_to_string(&path).expect("read file"); + assert!(content.contains(r#"my-crate = "2.0.0""#)); + } } diff --git a/crates/changeset-operations/tests/release_integration.rs b/crates/changeset-operations/tests/release_integration.rs index af470c1..46326ac 100644 --- a/crates/changeset-operations/tests/release_integration.rs +++ b/crates/changeset-operations/tests/release_integration.rs @@ -124,6 +124,99 @@ edition.workspace = true dir } +fn create_workspace_with_target_deps() -> TempDir { + let dir = TempDir::new().expect("create temp dir"); + + fs::write( + dir.path().join("Cargo.toml"), + r#"[workspace] +members = ["crates/*"] +resolver = "2" +"#, + ) + .expect("write workspace Cargo.toml"); + + fs::create_dir_all(dir.path().join("crates/crate-a/src")).expect("create crate-a dir"); + fs::write( + dir.path().join("crates/crate-a/Cargo.toml"), + r#"[package] +name = "crate-a" +version = "1.0.0" +edition = "2021" +"#, + ) + .expect("write crate-a Cargo.toml"); + fs::write(dir.path().join("crates/crate-a/src/lib.rs"), "").expect("write lib.rs"); + + fs::create_dir_all(dir.path().join("crates/crate-b/src")).expect("create crate-b dir"); + fs::write( + dir.path().join("crates/crate-b/Cargo.toml"), + r#"[package] +name = "crate-b" +version = "2.0.0" +edition = "2021" + +[target.'cfg(target_os = "linux")'.dependencies] +crate-a = { path = "../crate-a", version = "1.0.0" } +"#, + ) + .expect("write crate-b Cargo.toml"); + fs::write(dir.path().join("crates/crate-b/src/lib.rs"), "").expect("write lib.rs"); + + fs::create_dir_all(dir.path().join(".changeset/changesets")) + .expect("create .changeset/changesets dir"); + + dir +} + +fn create_workspace_with_standard_and_target_deps() -> TempDir { + let dir = TempDir::new().expect("create temp dir"); + + fs::write( + dir.path().join("Cargo.toml"), + r#"[workspace] +members = ["crates/*"] +resolver = "2" +"#, + ) + .expect("write workspace Cargo.toml"); + + fs::create_dir_all(dir.path().join("crates/crate-a/src")).expect("create crate-a dir"); + fs::write( + dir.path().join("crates/crate-a/Cargo.toml"), + r#"[package] +name = "crate-a" +version = "1.0.0" +edition = "2021" +"#, + ) + .expect("write crate-a Cargo.toml"); + fs::write(dir.path().join("crates/crate-a/src/lib.rs"), "").expect("write lib.rs"); + + fs::create_dir_all(dir.path().join("crates/crate-b/src")).expect("create crate-b dir"); + fs::write( + dir.path().join("crates/crate-b/Cargo.toml"), + r#"[package] +name = "crate-b" +version = "2.0.0" +edition = "2021" + +[dependencies] +crate-a = { path = "../crate-a", version = "1.0.0" } + +[target.'cfg(target_os = "windows")'.dependencies] +crate-a = { path = "../crate-a", version = "1.0.0" } +"#, + ) + .expect("write crate-b Cargo.toml"); + fs::write(dir.path().join("crates/crate-b/src/lib.rs"), "").expect("write lib.rs"); + + fs::create_dir_all(dir.path().join(".changeset/changesets")) + .expect("create .changeset/changesets dir"); + + dir +} + fn write_changeset(dir: &TempDir, filename: &str, package: &str, bump: &str, summary: &str) { let content = format!( r#"--- @@ -2546,3 +2639,42 @@ fn manage_remove_then_release_produces_normal_version() { "crate-a should get normal minor bump" ); } + +#[test] +fn release_updates_target_specific_dep_versions() { + let dir = create_workspace_with_target_deps(); + write_changeset(&dir, "bump-a.md", "crate-a", "minor", "Add feature"); + + let _outcome = run_release(&dir, false, false).expect("release should succeed"); + + let content = + fs::read_to_string(dir.path().join("crates/crate-b/Cargo.toml")).expect("read crate-b"); + assert!( + content.contains(r#"version = "1.1.0""#), + "target-specific dep version should be updated to 1.1.0, got:\n{content}" + ); + assert!( + !content.contains(r#"version = "1.0.0""#), + "old version should no longer appear" + ); +} + +#[test] +fn release_updates_both_standard_and_target_deps() { + let dir = create_workspace_with_standard_and_target_deps(); + write_changeset(&dir, "bump-a.md", "crate-a", "minor", "Add feature"); + + let _outcome = run_release(&dir, false, false).expect("release should succeed"); + + let content = + fs::read_to_string(dir.path().join("crates/crate-b/Cargo.toml")).expect("read crate-b"); + assert!( + !content.contains(r#"version = "1.0.0""#), + "no old version should remain" + ); + assert_eq!( + content.matches(r#"version = "1.1.0""#).count(), + 2, + "both [dependencies] and target section should have updated version, got:\n{content}" + ); +} diff --git a/crates/changeset-project/src/dependency_graph.rs b/crates/changeset-project/src/dependency_graph.rs index 7ee310e..4e84b88 100644 --- a/crates/changeset-project/src/dependency_graph.rs +++ b/crates/changeset-project/src/dependency_graph.rs @@ -12,8 +12,9 @@ pub struct WorkspaceDependencyGraph { } impl WorkspaceDependencyGraph { - /// Builds the dependency graph from the workspace, considering only `[dependencies]` and - /// `[build-dependencies]`. + /// Builds the dependency graph from the workspace, considering `[dependencies]`, + /// `[build-dependencies]`, and their target-specific equivalents under + /// `[target.'...'.dependencies]` and `[target.'...'.build-dependencies]`. /// /// # Errors /// @@ -55,6 +56,28 @@ impl WorkspaceDependencyGraph { } } } + + if let Some(ref target_map) = manifest.target { + for target_deps in target_map.values() { + let target_sections = [ + target_deps.dependencies.as_ref(), + target_deps.build_dependencies.as_ref(), + ]; + for section in target_sections.into_iter().flatten() { + for (key, entry) in section { + let resolved_name = resolve_package_name(key, entry); + if member_names.contains(resolved_name) { + if let Some(set) = depends_on.get_mut(package.name()) { + set.insert(resolved_name.to_string()); + } + if let Some(set) = depended_on_by.get_mut(resolved_name) { + set.insert(package.name().clone()); + } + } + } + } + } + } } Ok(Self { diff --git a/crates/changeset-project/src/manifest.rs b/crates/changeset-project/src/manifest.rs index f531053..6984fa7 100644 --- a/crates/changeset-project/src/manifest.rs +++ b/crates/changeset-project/src/manifest.rs @@ -18,13 +18,15 @@ pub(crate) struct CargoManifest { pub(crate) dependencies: Option>, #[serde(default, rename = "build-dependencies")] pub(crate) build_dependencies: Option>, + #[serde(default)] + pub(crate) target: Option>, } #[derive(Debug, Deserialize)] #[serde(untagged)] pub(crate) enum DependencyEntry { - Simple(IgnoredAny), Table(DependencyTable), + Simple(IgnoredAny), } #[derive(Debug, Deserialize)] @@ -32,6 +34,16 @@ pub(crate) struct DependencyTable { pub(crate) package: Option, } +#[derive(Debug, Deserialize)] +pub(crate) struct TargetDependencies { + #[serde(default)] + pub(crate) dependencies: Option>, + #[serde(default, rename = "dev-dependencies")] + _dev_dependencies: Option>, + #[serde(default, rename = "build-dependencies")] + pub(crate) build_dependencies: Option>, +} + #[derive(Debug, Deserialize)] pub(crate) struct Package { pub(crate) name: String, diff --git a/crates/changeset-project/src/project.rs b/crates/changeset-project/src/project.rs index 39e36c9..4b3c020 100644 --- a/crates/changeset-project/src/project.rs +++ b/crates/changeset-project/src/project.rs @@ -481,6 +481,7 @@ mod tests { }), dependencies: None, build_dependencies: None, + target: None, }; assert_eq!( determine_project_kind(&manifest), @@ -504,6 +505,7 @@ mod tests { }), dependencies: None, build_dependencies: None, + target: None, }; assert_eq!( determine_project_kind(&manifest), @@ -522,6 +524,7 @@ mod tests { workspace: None, dependencies: None, build_dependencies: None, + target: None, }; assert_eq!( determine_project_kind(&manifest), diff --git a/crates/changeset-project/tests/dependency_graph.rs b/crates/changeset-project/tests/dependency_graph.rs new file mode 100644 index 0000000..319bfae --- /dev/null +++ b/crates/changeset-project/tests/dependency_graph.rs @@ -0,0 +1,133 @@ +use changeset_project::{WorkspaceDependencyGraph, discover_project}; + +fn create_workspace_with_target_dep( + target_section: &str, +) -> (tempfile::TempDir, changeset_project::CargoProject) { + let dir = tempfile::tempdir().expect("create temp dir"); + + std::fs::write( + dir.path().join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/*\"]\nresolver = \"2\"\n", + ) + .expect("write root Cargo.toml"); + + let pkg_a = dir.path().join("crates/crate-a"); + std::fs::create_dir_all(pkg_a.join("src")).expect("create crate-a dirs"); + std::fs::write( + pkg_a.join("Cargo.toml"), + "[package]\nname = \"crate-a\"\nversion = \"1.0.0\"\nedition = \"2021\"\n", + ) + .expect("write crate-a Cargo.toml"); + std::fs::write(pkg_a.join("src/lib.rs"), "").expect("write lib.rs"); + + let pkg_b = dir.path().join("crates/crate-b"); + std::fs::create_dir_all(pkg_b.join("src")).expect("create crate-b dirs"); + std::fs::write( + pkg_b.join("Cargo.toml"), + format!( + "[package]\nname = \"crate-b\"\nversion = \"1.0.0\"\nedition = \"2021\"\n\n{target_section}\n" + ), + ) + .expect("write crate-b Cargo.toml"); + std::fs::write(pkg_b.join("src/lib.rs"), "").expect("write lib.rs"); + + let project = discover_project(dir.path()).expect("discover project"); + (dir, project) +} + +#[test] +fn target_specific_dependency_detected() { + let (_dir, project) = create_workspace_with_target_dep( + r#"[target.'cfg(unix)'.dependencies] +crate-a = { path = "../crate-a" }"#, + ); + + let graph = WorkspaceDependencyGraph::build(&project).expect("build graph"); + let deps = graph.direct_dependencies("crate-b"); + assert!(deps.contains("crate-a")); +} + +#[test] +fn target_specific_build_dependency_detected() { + let (_dir, project) = create_workspace_with_target_dep( + r#"[target.'cfg(unix)'.build-dependencies] +crate-a = { path = "../crate-a" }"#, + ); + + let graph = WorkspaceDependencyGraph::build(&project).expect("build graph"); + let deps = graph.direct_dependencies("crate-b"); + assert!(deps.contains("crate-a")); +} + +#[test] +fn target_specific_dependency_with_rename() { + let (_dir, project) = create_workspace_with_target_dep( + r#"[target.'cfg(unix)'.dependencies] +my-alias = { path = "../crate-a", package = "crate-a" }"#, + ); + + let graph = WorkspaceDependencyGraph::build(&project).expect("build graph"); + let deps = graph.direct_dependencies("crate-b"); + assert!(deps.contains("crate-a")); +} + +#[test] +fn target_specific_deps_included_in_transitive_dependents() { + let dir = tempfile::tempdir().expect("create temp dir"); + + std::fs::write( + dir.path().join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/*\"]\nresolver = \"2\"\n", + ) + .expect("write root Cargo.toml"); + + let pkg_a = dir.path().join("crates/crate-a"); + std::fs::create_dir_all(pkg_a.join("src")).expect("create dirs"); + std::fs::write( + pkg_a.join("Cargo.toml"), + "[package]\nname = \"crate-a\"\nversion = \"1.0.0\"\nedition = \"2021\"\n", + ) + .expect("write Cargo.toml"); + std::fs::write(pkg_a.join("src/lib.rs"), "").expect("write lib.rs"); + + let pkg_b = dir.path().join("crates/crate-b"); + std::fs::create_dir_all(pkg_b.join("src")).expect("create dirs"); + std::fs::write( + pkg_b.join("Cargo.toml"), + "[package]\nname = \"crate-b\"\nversion = \"1.0.0\"\nedition = \"2021\"\n\n\ + [target.'cfg(unix)'.dependencies]\n\ + crate-a = { path = \"../crate-a\" }\n", + ) + .expect("write Cargo.toml"); + std::fs::write(pkg_b.join("src/lib.rs"), "").expect("write lib.rs"); + + let pkg_c = dir.path().join("crates/crate-c"); + std::fs::create_dir_all(pkg_c.join("src")).expect("create dirs"); + std::fs::write( + pkg_c.join("Cargo.toml"), + "[package]\nname = \"crate-c\"\nversion = \"1.0.0\"\nedition = \"2021\"\n\n\ + [dependencies]\n\ + crate-b = { path = \"../crate-b\" }\n", + ) + .expect("write Cargo.toml"); + std::fs::write(pkg_c.join("src/lib.rs"), "").expect("write lib.rs"); + + let project = discover_project(dir.path()).expect("discover project"); + let graph = WorkspaceDependencyGraph::build(&project).expect("build graph"); + + let dependents = graph.transitive_dependents("crate-a"); + assert!(dependents.contains("crate-b")); + assert!(dependents.contains("crate-c")); +} + +#[test] +fn target_specific_dev_dependency_not_in_graph() { + let (_dir, project) = create_workspace_with_target_dep( + r#"[target.'cfg(unix)'.dev-dependencies] +crate-a = { path = "../crate-a" }"#, + ); + + let graph = WorkspaceDependencyGraph::build(&project).expect("build graph"); + let deps = graph.direct_dependencies("crate-b"); + assert!(!deps.contains("crate-a")); +}