Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/changesets/rancorously-immortal-goosefish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
category: fixed
changeset-operations: patch
---
Enforce `none_bump_behavior: disallow` during changeset creation
5 changes: 5 additions & 0 deletions .changeset/changesets/thriftily-muscular-marmoset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
category: fixed
cargo-changeset: patch
---
Enforce `none_bump_behavior: disallow` during changeset creation, preventing `none` bumps in both interactive and non-interactive modes.
6 changes: 5 additions & 1 deletion crates/cargo-changeset/src/commands/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ pub(super) fn run(args: AddArgs, start_path: &Path, writer: &dyn CliWriter) -> R
);
}

let (root_config, _) = project_provider.load_configs(&project)?;
let none_bump_behavior = root_config.none_bump_behavior();

let changeset_writer = FileSystemChangesetIO::new(project.root());

let input = build_input(&args)?;

let result = if is_interactive() {
let interaction_provider = TerminalInteractionProvider::new(args.editor);
let interaction_provider =
TerminalInteractionProvider::new(args.editor, none_bump_behavior);
let operation = AddOperation::new(project_provider, changeset_writer, interaction_provider);
operation.execute(start_path, &input)?
} else {
Expand Down
38 changes: 32 additions & 6 deletions crates/cargo-changeset/src/interaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ use std::process::Command;
use dialoguer::{Confirm, Input, MultiSelect, Select};
use strum::{EnumMessage, VariantArray};

use changeset_core::{ChangeCategory, PackageInfo};
use changeset_core::{ChangeCategory, NoneBumpBehavior, PackageInfo};
use changeset_git::DEFAULT_BASE_BRANCH;
use changeset_manifest::{
ChangelogLocation, ComparisonLinks, NoneBumpBehavior, ZeroVersionBehavior,
ChangelogLocation, ComparisonLinks, NoneBumpBehavior as ManifestNoneBumpBehavior,
ZeroVersionBehavior,
};
use changeset_operations::traits::{
BumpSelection, CategorySelection, ChangelogSettingsInput, DescriptionInput,
Expand All @@ -31,12 +32,16 @@ pub(crate) use selection_options::{

pub(crate) struct TerminalInteractionProvider {
use_editor: bool,
none_bump_behavior: NoneBumpBehavior,
}

impl TerminalInteractionProvider {
#[must_use]
pub(crate) fn new(use_editor: bool) -> Self {
Self { use_editor }
pub(crate) fn new(use_editor: bool, none_bump_behavior: NoneBumpBehavior) -> Self {
Self {
use_editor,
none_bump_behavior,
}
}
}

Expand Down Expand Up @@ -78,6 +83,27 @@ impl InteractionProvider for TerminalInteractionProvider {
}

fn select_bump_type(&self, package_name: &str) -> Result<BumpSelection> {
if self.none_bump_behavior == NoneBumpBehavior::Disallow {
let options: Vec<(changeset_core::BumpType, &str)> = BumpTypeSelectionOption::VARIANTS
.iter()
.filter(|v| !matches!(v, BumpTypeSelectionOption::None))
.filter_map(|v| {
let label = v.get_message()?;
Some(((*v).into(), label))
})
.collect();
let selection = select_from_options(
&format!("Select bump type for '{package_name}'"),
&options,
0,
)
.map_err(cli_error_to_operation_error)?;
return Ok(match selection {
Some(bump) => BumpSelection::Selected(bump),
None => BumpSelection::Cancelled,
});
}

let selection = select_variant::<BumpTypeSelectionOption>(
&format!("Select bump type for '{package_name}'"),
0,
Expand Down Expand Up @@ -281,7 +307,7 @@ impl InitInteractionProvider for TerminalInitInteractionProvider {
let none_bump_behavior =
select_none_bump_behavior().map_err(cli_error_to_operation_error)?;
let none_bump_promote_message_template =
if none_bump_behavior == NoneBumpBehavior::PromoteToPatch {
if none_bump_behavior == ManifestNoneBumpBehavior::PromoteToPatch {
Some(
prompt_none_bump_promote_message_template()
.map_err(cli_error_to_operation_error)?,
Expand Down Expand Up @@ -519,7 +545,7 @@ fn select_zero_version_behavior() -> CliResult<ZeroVersionBehavior> {
.into())
}

fn select_none_bump_behavior() -> CliResult<NoneBumpBehavior> {
fn select_none_bump_behavior() -> CliResult<ManifestNoneBumpBehavior> {
let selection =
select_variant::<NoneBumpBehaviorSelectionOption>("Select none bump behavior", 0)?;
Ok(selection
Expand Down
187 changes: 187 additions & 0 deletions crates/cargo-changeset/tests/add_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ fn create_workspace_with_underscored_crate() -> TempDir {
}

mod non_interactive {
use indoc::indoc;

use super::*;

#[test]
Expand Down Expand Up @@ -413,6 +415,91 @@ mod non_interactive {
);
}

#[test]
fn add_with_bump_none_rejected_when_disallowed() {
let workspace = WorkspaceBuilder::single_package("test-crate", "1.0.0")
.workspace_toml_extra("[package.metadata.changeset]\nnone-bump-behavior = \"disallow\"")
.build();

assert_cmd::cargo::cargo_bin_cmd!("cargo-changeset")
.arg("add")
.arg("--bump")
.arg("none")
.arg("-m")
.arg("Internal refactoring")
.current_dir(workspace.path())
.assert()
.failure()
.stdout(indoc! {"
Using package: test-crate (1.0.0)
"})
.stderr(indoc! {"
error: changesets with bump type 'none' are disallowed; affected packages: test-crate
"});
}

#[test]
fn add_with_package_bump_none_rejected_when_disallowed() {
let workspace = WorkspaceBuilder::virtual_workspace()
.crate_member("crate-a", "0.1.0")
.crate_member("crate-b", "0.2.0")
.workspace_toml_extra(
"[workspace.metadata.changeset]\nnone-bump-behavior = \"disallow\"",
)
.build();

assert_cmd::cargo::cargo_bin_cmd!("cargo-changeset")
.arg("add")
.arg("--package-bump")
.arg("crate-a:none")
.arg("-m")
.arg("Internal refactoring")
.current_dir(workspace.path())
.assert()
.failure()
.stdout("")
.stderr(indoc! {"
error: changesets with bump type 'none' are disallowed; affected packages: crate-a
"});
}

#[test]
fn add_with_bump_patch_succeeds_when_none_disallowed() {
let workspace = WorkspaceBuilder::single_package("test-crate", "1.0.0")
.workspace_toml_extra("[package.metadata.changeset]\nnone-bump-behavior = \"disallow\"")
.build();

assert_cmd::cargo::cargo_bin_cmd!("cargo-changeset")
.arg("add")
.arg("--bump")
.arg("patch")
.arg("-m")
.arg("Fix a bug")
.current_dir(workspace.path())
.assert()
.success()
.stderr("");

let changeset_dir = workspace.path().join(".changeset/changesets");
let files: Vec<_> = fs::read_dir(&changeset_dir)
.expect("read dir")
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "md"))
.collect();
assert_eq!(files.len(), 1, "should have one changeset file");

let content = fs::read_to_string(files[0].path()).expect("read changeset file");
assert_eq!(
content,
indoc! {"
---
test-crate: patch
---
Fix a bug
"},
);
}

#[test]
fn add_with_stdin_message() {
let workspace = create_single_crate_workspace();
Expand Down Expand Up @@ -1089,6 +1176,106 @@ MOCK_EDITOR_EOF
);
}

#[test]
fn interactive_bump_type_menu_hides_none_when_disallowed() {
let workspace = WorkspaceBuilder::virtual_workspace()
.crate_member("crate-a", "0.1.0")
.crate_member("crate-b", "0.2.0")
.workspace_toml_extra(
"[workspace.metadata.changeset]\nnone-bump-behavior = \"disallow\"",
)
.build();
let mut session = spawn_add_in_workspace(&workspace);

session.wait_for("Select packages to include in changeset");
session.toggle_item(0);
session.confirm();
session.wait_for("Select bump type for 'crate-a'");
session.assert_screen(
"bump type menu without none option",
indoc! {"
Select packages to include in changeset: crate-a (0.1.0)
Select bump type for 'crate-a':
> patch - Bug fixes (backwards compatible)
minor - New features (backwards compatible)
major - Breaking changes"},
);

session.cancel();
session.wait_for_exit();
}

#[test]
fn interactive_full_flow_with_none_disallowed() {
let workspace = WorkspaceBuilder::single_package("test-crate", "1.0.0")
.workspace_toml_extra("[package.metadata.changeset]\nnone-bump-behavior = \"disallow\"")
.build();
let mut session = spawn_add_in_workspace(&workspace);

session.wait_for("Select bump type for 'test-crate'");
session.assert_screen(
"single-crate bump type menu without none",
indoc! {"
Using package: test-crate (1.0.0)
Select bump type for 'test-crate':
> patch - Bug fixes (backwards compatible)
minor - New features (backwards compatible)
major - Breaking changes"},
);
session.confirm();

session.wait_for("Select change category");
session.assert_screen(
"category menu after bump type",
indoc! {"
Using package: test-crate (1.0.0)
Select bump type for 'test-crate': patch - Bug fixes (backwards compatible)
Select change category:
> changed - General changes (default)
added - New features
fixed - Bug fixes
deprecated - Deprecated features
removed - Removed features
security - Security fixes"},
);
session.confirm();

session.wait_for("Enter description (press Enter 3 times to finish):");
session.assert_screen(
"description prompt",
indoc! {"
Using package: test-crate (1.0.0)
Select bump type for 'test-crate': patch - Bug fixes (backwards compatible)
Select change category: changed - General changes (default)

Enter description (press Enter 3 times to finish):"},
);
session.type_line("Test with none disallowed");
session.type_line("");
session.type_line("");

session.wait_for("Created changeset");
session.wait_for_exit();

let changeset_dir = workspace.path().join(".changeset/changesets");
assert!(
changeset_dir.exists(),
".changeset/changesets directory should exist"
);

let files: Vec<_> = fs::read_dir(&changeset_dir)
.expect("read dir")
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "md"))
.collect();
assert_eq!(files.len(), 1, "should have one changeset file");

let content = fs::read_to_string(files[0].path()).expect("read file");
assert!(content.contains("test-crate"));
assert!(content.contains("patch"));
assert!(content.contains("Test with none disallowed"));
}

#[test]
fn interactive_editor_filters_comments() {
let workspace = create_single_crate_workspace();
Expand Down
Loading
Loading