diff --git a/CHANGELOG.md b/CHANGELOG.md index 0884171967..3ca3ac31f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj git fetch` and `jj git push` will now use the single defined remote even if it is not named "origin". +* `jj` with no subcommand now defaults to `jj log` instead of showing help. This + command can be overridden by setting `ui.default-command`. + ### Fixed bugs * Modify/delete conflicts now include context lines diff --git a/docs/config.md b/docs/config.md index afa0a96e9f..278f82aa1b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -124,6 +124,16 @@ Which elements can be colored is not yet documented, but see the [default color configuration](https://github.com/martinvonz/jj/blob/main/src/config/colors.toml) for some examples of what's possible. +### Default command + +When `jj` is run with no explicit subcommand, the value of the +`ui.default-command` setting will used instead. Possible values are any valid +subcommand name, subcommand alias, or user-defined alias (defaults to `"log"`). + +```toml +ui.default-command = "log" +``` + ### Diff format ```toml diff --git a/src/cli_util.rs b/src/cli_util.rs index 396de0767b..f06adb92c6 100644 --- a/src/cli_util.rs +++ b/src/cli_util.rs @@ -2028,6 +2028,49 @@ impl ValueParserFactory for RevisionArg { } } +fn resolve_default_command( + ui: &mut Ui, + config: &config::Config, + app: &Command, + string_args: &mut Vec, +) -> Result<(), CommandError> { + const PRIORITY_FLAGS: &[&str] = &["help", "--help", "-h", "--version", "-V"]; + + let has_priority_flag = string_args + .iter() + .any(|arg| PRIORITY_FLAGS.contains(&arg.as_str())); + if has_priority_flag { + return Ok(()); + } + + let app_clone = app + .clone() + .allow_external_subcommands(true) + .ignore_errors(true); + let matches = app_clone.try_get_matches_from(string_args.clone()).ok(); + + if let Some(matches) = matches { + if matches.subcommand_name().is_none() { + if config.get_string("ui.default-command").is_err() { + writeln!( + ui.hint(), + "Hint: Use `jj -h` for a list of available commands." + )?; + writeln!( + ui.hint(), + "Set the config `ui.default-command = \"log\"` to disable this message." + )?; + } + let default_command = config + .get_string("ui.default-command") + .unwrap_or("log".to_string()); + // Insert the default command directly after the path to the binary. + string_args.insert(1, default_command); + } + } + Ok(()) +} + fn resolve_aliases( config: &config::Config, app: &Command, @@ -2119,6 +2162,7 @@ fn handle_early_args( } pub fn expand_args( + ui: &mut Ui, app: &Command, args_os: ArgsOs, config: &config::Config, @@ -2132,6 +2176,7 @@ pub fn expand_args( } } + resolve_default_command(ui, config, app, &mut string_args)?; resolve_aliases(config, app, &string_args) } @@ -2299,7 +2344,7 @@ impl CliRunner { layered_configs.read_user_config()?; let config = layered_configs.merge(); ui.reset(&config)?; - let string_args = expand_args(&self.app, std::env::args_os(), &config)?; + let string_args = expand_args(ui, &self.app, std::env::args_os(), &config)?; let (matches, args) = parse_args( ui, &self.app, diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e13a540c36..7095a6d93c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3474,9 +3474,7 @@ fn cmd_sparse(ui: &mut Ui, command: &CommandHelper, args: &SparseArgs) -> Result pub fn default_app() -> Command { let app: Command = Commands::augment_subcommands(Args::command()); - app.arg_required_else_help(true) - .subcommand_required(true) - .version(env!("JJ_VERSION")) + app.version(env!("JJ_VERSION")) } pub fn run_command( diff --git a/src/config-schema.json b/src/config-schema.json index 0d7c63a310..6638a972e4 100644 --- a/src/config-schema.json +++ b/src/config-schema.json @@ -51,6 +51,11 @@ "description": "Whether to allow initializing a repo with the native backend", "default": false }, + "default-command": { + "type": "string", + "description": "Default command to run when no explicit command is given", + "default": "log" + }, "default-revset": { "type": "string", "description": "Default set of revisions to show when no explicit revset is given for jj log and similar commands", diff --git a/tests/test_alias.rs b/tests/test_alias.rs index 546dc64c15..86bdac4030 100644 --- a/tests/test_alias.rs +++ b/tests/test_alias.rs @@ -69,7 +69,7 @@ fn test_alias_bad_name() { insta::assert_snapshot!(stderr, @r###" error: unrecognized subcommand 'foo.' - Usage: jj [OPTIONS] + Usage: jj [OPTIONS] [COMMAND] For more information, try '--help'. "###); @@ -86,7 +86,7 @@ fn test_alias_calls_unknown_command() { insta::assert_snapshot!(stderr, @r###" error: unrecognized subcommand 'nonexistent' - Usage: jj [OPTIONS] + Usage: jj [OPTIONS] [COMMAND] For more information, try '--help'. "###); @@ -123,7 +123,7 @@ fn test_alias_calls_help() { To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/docs/tutorial.md. - Usage: jj [OPTIONS] + Usage: jj [OPTIONS] [COMMAND] "###); } diff --git a/tests/test_global_opts.rs b/tests/test_global_opts.rs index fca8012696..035893047d 100644 --- a/tests/test_global_opts.rs +++ b/tests/test_global_opts.rs @@ -47,12 +47,25 @@ fn test_non_utf8_arg() { #[test] fn test_no_subcommand() { let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); - let stderr = test_env.jj_cmd_cli_error(test_env.env_root(), &[]); - insta::assert_snapshot!(stderr.lines().next().unwrap(), @"Jujutsu (An experimental VCS)"); + // Outside of a repo. + let stderr = test_env.jj_cmd_failure(test_env.env_root(), &[]); + insta::assert_snapshot!(stderr, @r###" + Hint: Use `jj -h` for a list of available commands. + Set the config `ui.default-command = "log"` to disable this message. + Error: There is no jj repo in "." + "###); - let stderr = test_env.jj_cmd_cli_error(test_env.env_root(), &["-R."]); - insta::assert_snapshot!(stderr.lines().next().unwrap(), @"error: 'jj' requires a subcommand but one was not provided"); + test_env.add_config(r#"ui.default-command="log""#); + let stderr = test_env.jj_cmd_failure(test_env.env_root(), &[]); + insta::assert_snapshot!(stderr, @r###" + Error: There is no jj repo in "." + "###); + + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["--help"]); + insta::assert_snapshot!(stdout.lines().next().unwrap(), @"Jujutsu (An experimental VCS)"); let stdout = test_env.jj_cmd_success(test_env.env_root(), &["--version"]); let sanitized = stdout.replace(|c: char| c.is_ascii_hexdigit(), "?"); @@ -62,8 +75,28 @@ fn test_no_subcommand() { "{sanitized}" ); - let stdout = test_env.jj_cmd_success(test_env.env_root(), &["--help"]); - insta::assert_snapshot!(stdout.lines().next().unwrap(), @"Jujutsu (An experimental VCS)"); + let stdout = test_env.jj_cmd_success(test_env.env_root(), &["-R", "repo"]); + assert_eq!(stdout, test_env.jj_cmd_success(&repo_path, &["log"])); + + // Inside of a repo. + let stdout = test_env.jj_cmd_success(&repo_path, &[]); + assert_eq!(stdout, test_env.jj_cmd_success(&repo_path, &["log"])); + + let stdout = test_env.jj_cmd_success(&repo_path, &["-T", "show"]); + let stdout = stdout.lines().skip(2).join("\n"); + insta::assert_snapshot!(stdout, @r###" + │ Author: Test User (2001-02-03 04:05:07.000 +07:00) + │ Committer: Test User (2001-02-03 04:05:07.000 +07:00) + │ + │ (no description set) + │ + ◉ Commit ID: 0000000000000000000000000000000000000000 + Change ID: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz + Author: <> (1970-01-01 00:00:00.000 +00:00) + Committer: <> (1970-01-01 00:00:00.000 +00:00) + + (no description set) + "###); } #[test]