diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d93e165f..f8b4e7fa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `--all` is now named `--all-remotes` for `jj branch list` +* There is a new global `--quiet` flag to silence commands' non-primary output. + ### Fixed bugs ## [0.15.1] - 2024-03-06 diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 034bf6673..5723beeb2 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -2137,6 +2137,16 @@ pub struct EarlyArgs { /// When to colorize output (always, never, auto) #[arg(long, value_name = "WHEN", global = true)] pub color: Option, + /// Silence non-primary command output + /// + /// For example, `jj files` will still list files, but it won't tell you if + /// the working copy was snapshotted or if descendants were rebased. + /// + /// Warnings and errors will still be printed. + #[arg(long, global = true, action = ArgAction::SetTrue)] + // Parsing with ignore_errors will crash if this is bool, so use + // Option. + pub quiet: Option, /// Disable the pager #[arg(long, value_name = "WHEN", global = true, action = ArgAction::SetTrue)] // Parsing with ignore_errors will crash if this is bool, so use @@ -2306,6 +2316,9 @@ fn handle_early_args( if let Some(choice) = args.color { args.config_toml.push(format!(r#"ui.color="{choice}""#)); } + if args.quiet.unwrap_or_default() { + args.config_toml.push(r#"ui.quiet=true"#.to_string()); + } if args.no_pager.unwrap_or_default() { args.config_toml.push(r#"ui.paginate="never""#.to_owned()); } diff --git a/cli/src/ui.rs b/cli/src/ui.rs index 51de85c3b..df480849c 100644 --- a/cli/src/ui.rs +++ b/cli/src/ui.rs @@ -163,6 +163,7 @@ impl Write for UiStderr<'_> { pub struct Ui { color: bool, + quiet: bool, pager_cmd: CommandNameAndArgs, paginate: PaginationChoice, progress_indicator: bool, @@ -222,6 +223,10 @@ fn use_color(choice: ColorChoice) -> bool { } } +fn be_quiet(config: &config::Config) -> bool { + config.get_bool("ui.quiet").unwrap_or_default() +} + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize)] #[serde(rename_all(deserialize = "kebab-case"))] pub enum PaginationChoice { @@ -245,6 +250,7 @@ fn pager_setting(config: &config::Config) -> Result Result { let color = use_color(color_setting(config)); + let quiet = be_quiet(config); // Sanitize ANSI escape codes if we're printing to a terminal. Doesn't affect // ANSI escape codes that originate from the formatter itself. let sanitize = io::stdout().is_terminal(); @@ -252,6 +258,7 @@ impl Ui { let progress_indicator = progress_indicator_setting(config); Ok(Ui { color, + quiet, formatter_factory, pager_cmd: pager_setting(config)?, paginate: pagination_setting(config)?, @@ -262,6 +269,7 @@ impl Ui { pub fn reset(&mut self, config: &config::Config) -> Result<(), CommandError> { self.color = use_color(color_setting(config)); + self.quiet = be_quiet(config); self.paginate = pagination_setting(config)?; self.pager_cmd = pager_setting(config)?; self.progress_indicator = progress_indicator_setting(config); @@ -375,14 +383,17 @@ impl Ui { /// Writer to print an update that's not part of the command's main output. pub fn status(&self) -> Box { - Box::new(self.stderr()) + if self.quiet { + Box::new(std::io::sink()) + } else { + Box::new(self.stderr()) + } } /// A formatter to print an update that's not part of the command's main /// output. Returns `None` if `--quiet` was requested. - // TODO: Actually support `--quiet` pub fn status_formatter(&self) -> Option> { - Some(self.stderr_formatter()) + (!self.quiet).then(|| self.stderr_formatter()) } /// Writer to print hint with the default "Hint: " heading. @@ -394,7 +405,7 @@ impl Ui { /// Writer to print hint without the "Hint: " heading. pub fn hint_no_heading(&self) -> Option, &'static str>> { - Some(LabeledWriter::new(self.stderr_formatter(), "hint")) + (!self.quiet).then(|| LabeledWriter::new(self.stderr_formatter(), "hint")) } /// Writer to print hint with the given heading. @@ -402,11 +413,7 @@ impl Ui { &self, heading: H, ) -> Option, &'static str, H>> { - Some(HeadingLabeledWriter::new( - self.stderr_formatter(), - "hint", - heading, - )) + (!self.quiet).then(|| HeadingLabeledWriter::new(self.stderr_formatter(), "hint", heading)) } /// Writer to print warning with the default "Warning: " heading. diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 8ff377a2f..85b4b4ace 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -159,6 +159,10 @@ To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/d Possible values: `true`, `false` * `--color ` — When to colorize output (always, never, auto) +* `--quiet` — Silence non-primary command output + + Possible values: `true`, `false` + * `--no-pager` — Disable the pager Possible values: `true`, `false` diff --git a/cli/tests/test_global_opts.rs b/cli/tests/test_global_opts.rs index 0ad86b770..cf358b9b9 100644 --- a/cli/tests/test_global_opts.rs +++ b/cli/tests/test_global_opts.rs @@ -441,6 +441,19 @@ fn test_color_ui_messages() { "###); } +#[test] +fn test_quiet() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + // Can skip message about new working copy with `--quiet` + std::fs::write(repo_path.join("file1"), "contents").unwrap(); + let (_stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["--quiet", "describe", "-m=new description"]); + insta::assert_snapshot!(stderr, @""); +} + #[test] fn test_early_args() { // Test that help output parses early args @@ -535,6 +548,7 @@ fn test_help() { --at-operation Operation to load the repo at [default: @] [aliases: at-op] --debug Enable debug logging --color When to colorize output (always, never, auto) + --quiet Silence non-primary command output --no-pager Disable the pager --config-toml Additional configuration options (can be repeated) "###); diff --git a/cli/tests/test_resolve_command.rs b/cli/tests/test_resolve_command.rs index 057b76164..133813810 100644 --- a/cli/tests/test_resolve_command.rs +++ b/cli/tests/test_resolve_command.rs @@ -728,20 +728,7 @@ fn test_multiple_conflicts() { std::fs::write(&editor_script, "expect\n\0write\nresolution another_file\n").unwrap(); let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["resolve", "--quiet", "another_file"]); insta::assert_snapshot!(stdout, @""); - insta::assert_snapshot!(stderr, @r###" - Resolving conflicts in: another_file - New conflicts appeared in these commits: - vruxwmqv 3c438f88 conflict | (conflict) conflict - To resolve the conflicts, start by updating to it: - jj new vruxwmqvtpmx - Then use `jj resolve`, or edit the conflict markers in the file directly. - Once the conflicts are resolved, you may want inspect the result with `jj diff`. - Then run `jj squash` to move the resolution into the conflicted commit. - Working copy now at: vruxwmqv 3c438f88 conflict | (conflict) conflict - Parent commit : zsuskuln de7553ef a | a - Parent commit : royxmykx f68bc2f0 b | b - Added 0 files, modified 1 files, removed 0 files - "###); + insta::assert_snapshot!(stderr, @""); // For the rest of the test, we call `jj resolve` several times in a row to // resolve each conflict in the order it chooses.