Skip to content

Commit

Permalink
cli: default to log when no subcommand is provided
Browse files Browse the repository at this point in the history
This is a convenience optimization to improve the default user
experience, since `jj log` is a frequently run command. Accessing the
help information explicitly still follows normal CLI conventions, and
instructions are displayed appropriately if the user happens to make a
mistake. Discoverability should not be adversely harmed.

Note that this behavior mirrors what Sapling does [2], where `sl` will
display the smartlog by default.

[1] clap-rs/clap#975
[2] https://sapling-scm.com/docs/overview/smartlog
  • Loading branch information
elasticdog committed Apr 16, 2023
1 parent cf402af commit 4cb52d1
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 12 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions docs/config.md
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions src/cli_util.rs
Expand Up @@ -2028,6 +2028,33 @@ impl ValueParserFactory for RevisionArg {
}
}

fn resolve_default_command(config: &config::Config, app: &Command, string_args: &mut Vec<String>) {
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;
}

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 matches
.as_ref()
.map_or(true, |m| m.subcommand_name().is_none())
{
// Insert the default command directly after the path to the binary.
let default_command = config
.get_string("ui.default-command")
.unwrap_or("log".to_string());
string_args.insert(1, default_command);
}
}

fn resolve_aliases(
config: &config::Config,
app: &Command,
Expand Down Expand Up @@ -2132,6 +2159,7 @@ pub fn expand_args(
}
}

resolve_default_command(config, app, &mut string_args);
resolve_aliases(config, app, &string_args)
}

Expand Down
4 changes: 1 addition & 3 deletions src/commands/mod.rs
Expand Up @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions src/config-schema.json
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions tests/test_alias.rs
Expand Up @@ -69,7 +69,7 @@ fn test_alias_bad_name() {
insta::assert_snapshot!(stderr, @r###"
error: unrecognized subcommand 'foo.'
Usage: jj [OPTIONS] <COMMAND>
Usage: jj [OPTIONS] [COMMAND]
For more information, try '--help'.
"###);
Expand All @@ -86,7 +86,7 @@ fn test_alias_calls_unknown_command() {
insta::assert_snapshot!(stderr, @r###"
error: unrecognized subcommand 'nonexistent'
Usage: jj [OPTIONS] <COMMAND>
Usage: jj [OPTIONS] [COMMAND]
For more information, try '--help'.
"###);
Expand Down Expand Up @@ -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] <COMMAND>
Usage: jj [OPTIONS] [COMMAND]
"###);
}

Expand Down
21 changes: 15 additions & 6 deletions tests/test_global_opts.rs
Expand Up @@ -47,12 +47,17 @@ 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###"
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");
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(), "?");
Expand All @@ -62,8 +67,12 @@ 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"]));
}

#[test]
Expand Down

0 comments on commit 4cb52d1

Please sign in to comment.