diff --git a/.changeset/changesets/coolly-lusty-moccasin.md b/.changeset/changesets/coolly-lusty-moccasin.md new file mode 100644 index 0000000..f409c97 --- /dev/null +++ b/.changeset/changesets/coolly-lusty-moccasin.md @@ -0,0 +1,4 @@ +--- +cargo-changeset: patch +--- +Improve interactive ignored-files prompt in `init` command diff --git a/.changeset/changesets/fretfully-valid-drum.md b/.changeset/changesets/fretfully-valid-drum.md new file mode 100644 index 0000000..10eff72 --- /dev/null +++ b/.changeset/changesets/fretfully-valid-drum.md @@ -0,0 +1,4 @@ +--- +changeset-test-helpers: minor +--- +Any timeout now prints the screen when tests fail diff --git a/crates/cargo-changeset/src/commands/additional_packages.rs b/crates/cargo-changeset/src/commands/additional_packages.rs index 80b56f2..1c66686 100644 --- a/crates/cargo-changeset/src/commands/additional_packages.rs +++ b/crates/cargo-changeset/src/commands/additional_packages.rs @@ -215,33 +215,15 @@ impl AdditionalPackageInteractionProvider for TerminalAdditionalPackageInteracti &self, package_path: &Path, ) -> changeset_operations::Result> { - let mut patterns = Vec::new(); - println!( - "Enter glob patterns for files that influence this package (one per line, empty line to finish):" - ); - let default = format!("{}/**", package_path.display()); - let first: String = Input::new() - .with_prompt("Glob pattern") - .default(default) - .allow_empty(true) - .interact_text() - .map_err(super::dialoguer_to_operation_error)?; - if first.is_empty() { - return Ok(patterns); - } - patterns.push(first); - loop { - let s: String = Input::new() - .with_prompt("Additional pattern") - .allow_empty(true) - .interact_text() - .map_err(super::dialoguer_to_operation_error)?; - if s.is_empty() { - break; - } - patterns.push(s); - } - Ok(patterns) + Ok(crate::interaction::prompt_multi_value( + &crate::interaction::MultiValuePromptConfig { + intro: "Enter glob patterns for files that influence this package \ + (one per line, empty line to finish):", + first_prompt: "Glob pattern", + additional_prompt: "Additional pattern", + first_default: Some(format!("{}/**", package_path.display())), + }, + )?) } fn prompt_manifest_file_path(&self) -> changeset_operations::Result { diff --git a/crates/cargo-changeset/src/interaction.rs b/crates/cargo-changeset/src/interaction.rs index 47f5e77..9c035d6 100644 --- a/crates/cargo-changeset/src/interaction.rs +++ b/crates/cargo-changeset/src/interaction.rs @@ -154,6 +154,13 @@ impl InteractionProvider for NonInteractiveProvider { } } +pub(crate) struct MultiValuePromptConfig<'a> { + pub(crate) intro: &'a str, + pub(crate) first_prompt: &'a str, + pub(crate) additional_prompt: &'a str, + pub(crate) first_default: Option, +} + #[derive(Default)] pub(crate) struct TerminalInitInteractionProvider; @@ -327,6 +334,40 @@ pub(crate) fn confirm_proceed(prompt: &str) -> crate::error::Result { Ok(confirmed == Some(true)) } +pub(crate) fn prompt_multi_value( + config: &MultiValuePromptConfig<'_>, +) -> std::io::Result> { + let mut values = Vec::new(); + println!("{}", config.intro); + + let mut first_input = Input::::new() + .with_prompt(config.first_prompt) + .allow_empty(true); + if let Some(ref default) = config.first_default { + first_input = first_input.default(default.clone()); + } + let first = first_input.interact_text().map_err(from_dialoguer)?; + let first = first.trim().to_string(); + if first.is_empty() { + return Ok(values); + } + values.push(first); + + loop { + let s: String = Input::new() + .with_prompt(config.additional_prompt) + .allow_empty(true) + .interact_text() + .map_err(from_dialoguer)?; + let s = s.trim().to_string(); + if s.is_empty() { + break; + } + values.push(s); + } + Ok(values) +} + fn from_dialoguer(e: dialoguer::Error) -> std::io::Error { match e { dialoguer::Error::IO(io) => io, @@ -586,22 +627,13 @@ fn prompt_dependency_bump_changelog_template() -> Result { } fn prompt_ignored_files_loop() -> Result> { - let mut patterns = Vec::new(); - loop { - let pattern: String = Input::new() - .with_prompt("Add ignore pattern (empty to finish)") - .default(String::new()) - .allow_empty(true) - .interact_text() - .map_err(from_dialoguer)?; - - let trimmed = pattern.trim().to_string(); - if trimmed.is_empty() { - break; - } - patterns.push(trimmed); - } - Ok(patterns) + Ok(prompt_multi_value(&MultiValuePromptConfig { + intro: "Enter file patterns to exclude from change detection \ + (one per line, empty line to finish):", + first_prompt: "Ignore pattern", + additional_prompt: "Additional pattern", + first_default: None, + })?) } fn prompt_none_bump_promote_message_template() -> Result { diff --git a/crates/cargo-changeset/tests/additional_packages.rs b/crates/cargo-changeset/tests/additional_packages.rs index e33ce2c..8886561 100644 --- a/crates/cargo-changeset/tests/additional_packages.rs +++ b/crates/cargo-changeset/tests/additional_packages.rs @@ -1058,6 +1058,56 @@ mod help_and_ux_tests { } } +#[cfg(not(windows))] +mod interactive_add_tests { + use std::path::PathBuf; + + use changeset_test_helpers::terminal_session::TerminalSession; + + use super::*; + + fn bin_path() -> PathBuf { + assert_cmd::cargo::cargo_bin("cargo-changeset") + } + + #[test] + fn interactive_add_accepts_influence_patterns() { + let workspace = create_workspace_with_helm_chart(); + + let mut session = + TerminalSession::spawn(&bin_path(), &workspace, &["additional-packages", "add"]); + session.wait_for("Package name"); + session.type_line("my-helm-chart"); + session.wait_for("Package directory path"); + session.type_line("charts/my-chart"); + session.wait_for("Glob pattern"); + session.type_line("charts/my-chart/**"); + session.wait_for("Additional pattern"); + session.type_line("charts/shared/**"); + session.wait_for("Additional pattern"); + session.type_line(""); + session.wait_for("version manifest file"); + session.type_line("charts/my-chart/Chart.yaml"); + session.wait_for("Manifest format"); + session.confirm(); + session.wait_for("version field"); + session.type_line("version"); + session.wait_for("Added additional package"); + session.wait_for_exit(); + + let cargo_toml = + fs::read_to_string(workspace.path().join("Cargo.toml")).expect("read Cargo.toml"); + assert!( + cargo_toml.contains("charts/my-chart/**"), + "expected first influence pattern in config, got:\n{cargo_toml}" + ); + assert!( + cargo_toml.contains("charts/shared/**"), + "expected second influence pattern in config, got:\n{cargo_toml}" + ); + } +} + #[cfg(not(windows))] mod interactive_dependencies_tests { use std::path::PathBuf; diff --git a/crates/cargo-changeset/tests/init_command.rs b/crates/cargo-changeset/tests/init_command.rs index fdcca5b..0df2ad7 100644 --- a/crates/cargo-changeset/tests/init_command.rs +++ b/crates/cargo-changeset/tests/init_command.rs @@ -786,6 +786,81 @@ mod workflow_tests { } } +#[cfg(not(windows))] +mod interactive_init_tests { + use std::path::PathBuf; + + use changeset_test_helpers::terminal_session::TerminalSession; + + use super::*; + + fn bin_path() -> PathBuf { + assert_cmd::cargo::cargo_bin("cargo-changeset") + } + + #[test] + fn interactive_init_filtering_adds_patterns() { + let dir = setup_single_package(); + + let mut session = TerminalSession::spawn(&bin_path(), &dir, &["changeset", "init"]); + session.wait_for("Configure git settings?"); + session.send_raw("n"); + session.wait_for("Configure changelog settings?"); + session.send_raw("n"); + session.wait_for("Configure version settings?"); + session.send_raw("n"); + session.wait_for("Configure file filtering?"); + session.send_raw("y"); + session.wait_for("Ignore pattern"); + session.type_line("*.log"); + session.wait_for("Additional pattern"); + session.type_line("tmp/**"); + session.wait_for("Additional pattern"); + session.type_line(""); + session.wait_for("Proceed with initialization?"); + session.send_raw("y"); + session.wait_for_exit(); + + let cargo_toml = + fs::read_to_string(dir.path().join("Cargo.toml")).expect("read Cargo.toml"); + assert!( + cargo_toml.contains("*.log"), + "expected *.log pattern in config, got:\n{cargo_toml}" + ); + assert!( + cargo_toml.contains("tmp/**"), + "expected tmp/** pattern in config, got:\n{cargo_toml}" + ); + } + + #[test] + fn interactive_init_filtering_empty_skips() { + let dir = setup_single_package(); + + let mut session = TerminalSession::spawn(&bin_path(), &dir, &["changeset", "init"]); + session.wait_for("Configure git settings?"); + session.send_raw("n"); + session.wait_for("Configure changelog settings?"); + session.send_raw("n"); + session.wait_for("Configure version settings?"); + session.send_raw("n"); + session.wait_for("Configure file filtering?"); + session.send_raw("y"); + session.wait_for("Ignore pattern"); + session.type_line(""); + session.wait_for("Proceed with initialization?"); + session.send_raw("y"); + session.wait_for_exit(); + + let cargo_toml = + fs::read_to_string(dir.path().join("Cargo.toml")).expect("read Cargo.toml"); + assert!( + !cargo_toml.contains("ignored"), + "expected no ignored_files in config, got:\n{cargo_toml}" + ); + } +} + mod project_type_scenarios { use super::*; diff --git a/crates/changeset-test-helpers/src/terminal_session.rs b/crates/changeset-test-helpers/src/terminal_session.rs index 7c4ee29..ae3452c 100644 --- a/crates/changeset-test-helpers/src/terminal_session.rs +++ b/crates/changeset-test-helpers/src/terminal_session.rs @@ -88,6 +88,10 @@ impl TerminalSession { .to_owned() } + pub fn debug_print_screen(&mut self) { + eprintln!("=== PTY screen ===\n{}\n==================", self.screen()); + } + pub fn wait_for(&mut self, needle: &str) -> &mut Self { let start = Instant::now(); loop { @@ -95,11 +99,10 @@ impl TerminalSession { if self.vt.screen().contents().contains(needle) { return self; } - assert!( - start.elapsed() <= TIMEOUT, - "Timed out waiting for {needle:?}\nScreen:\n{}", - self.screen() - ); + if start.elapsed() > TIMEOUT { + self.debug_print_screen(); + panic!("Timed out waiting for {needle:?}"); + } std::thread::sleep(POLL_INTERVAL); } }