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
4 changes: 4 additions & 0 deletions .changeset/changesets/coolly-lusty-moccasin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
cargo-changeset: patch
---
Improve interactive ignored-files prompt in `init` command
4 changes: 4 additions & 0 deletions .changeset/changesets/fretfully-valid-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
changeset-test-helpers: minor
---
Any timeout now prints the screen when tests fail
36 changes: 9 additions & 27 deletions crates/cargo-changeset/src/commands/additional_packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,33 +215,15 @@ impl AdditionalPackageInteractionProvider for TerminalAdditionalPackageInteracti
&self,
package_path: &Path,
) -> changeset_operations::Result<Vec<String>> {
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<PathBuf> {
Expand Down
64 changes: 48 additions & 16 deletions crates/cargo-changeset/src/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}

#[derive(Default)]
pub(crate) struct TerminalInitInteractionProvider;

Expand Down Expand Up @@ -327,6 +334,40 @@ pub(crate) fn confirm_proceed(prompt: &str) -> crate::error::Result<bool> {
Ok(confirmed == Some(true))
}

pub(crate) fn prompt_multi_value(
config: &MultiValuePromptConfig<'_>,
) -> std::io::Result<Vec<String>> {
let mut values = Vec::new();
println!("{}", config.intro);

let mut first_input = Input::<String>::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,
Expand Down Expand Up @@ -586,22 +627,13 @@ fn prompt_dependency_bump_changelog_template() -> Result<String> {
}

fn prompt_ignored_files_loop() -> Result<Vec<String>> {
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<String> {
Expand Down
50 changes: 50 additions & 0 deletions crates/cargo-changeset/tests/additional_packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
75 changes: 75 additions & 0 deletions crates/cargo-changeset/tests/init_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down
13 changes: 8 additions & 5 deletions crates/changeset-test-helpers/src/terminal_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,21 @@ 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 {
self.poll();
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);
}
}
Expand Down
Loading