// Copyright 2022 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::ffi::OsString;
use indoc::indoc;
use itertools::Itertools as _;
use regex::Regex;
use testutils::TestResult;
use crate::common::TestEnvironment;
use crate::common::TestWorkDir;
#[test]
fn test_non_utf8_arg() {
let test_env = TestEnvironment::default();
#[cfg(unix)]
let invalid_utf = {
use std::os::unix::ffi::OsStringExt as _;
OsString::from_vec(vec![0x66, 0x6f, 0x80, 0x6f])
};
#[cfg(windows)]
let invalid_utf = {
use std::os::windows::prelude::*;
OsString::from_wide(&[0x0066, 0x006f, 0xD800, 0x006f])
};
let output = test_env.run_jj_in(".", [&invalid_utf]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: Non-UTF-8 argument
[EOF]
[exit status: 2]
");
}
#[test]
fn test_version() {
let test_env = TestEnvironment::default();
let output = test_env.run_jj_in(".", ["--version"]).success();
let stdout = output.stdout.into_raw();
let sanitized = stdout.replace(|c: char| c.is_ascii_hexdigit(), "?");
let expected = [
"jj ?.??.?\n",
"jj ?.??.?-????????????????????????????????????????\n",
// This test could be made to succeed when `jj` is compiled at a merge commit by adding a
// few entries like "jj ?.??.?-????????????????????????????????????????-?????????????????
// ???????????????????????\n" here. However, it might be best to keep it failing, so that
// we avoid releasing `jj` with such `--version` output.
];
assert!(
expected.contains(&sanitized.as_str()),
"`jj version` output: {stdout:?}.\nSanitized: {sanitized:?}\nExpected one of: {expected:?}"
);
}
#[test]
fn test_no_subcommand() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// Outside of a repo.
let output = test_env.run_jj_in(".", [""; 0]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Hint: Use `jj -h` for a list of available commands.
Run `jj config set --user ui.default-command log` to disable this message.
Error: There is no jj repo in "."
[EOF]
[exit status: 1]
"#);
test_env.add_config(r#"ui.default-command="log""#);
let output = test_env.run_jj_in(".", [""; 0]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: There is no jj repo in "."
[EOF]
[exit status: 1]
"#);
let output = test_env.run_jj_in(".", ["--help"]).success();
insta::assert_snapshot!(
output.stdout.normalized().lines().next().unwrap(),
@"Jujutsu (An experimental VCS)");
insta::assert_snapshot!(output.stderr, @"");
let output = test_env.run_jj_in(".", ["-R", "repo"]).success();
assert_eq!(output, work_dir.run_jj(["log"]));
// Inside of a repo.
let output = work_dir.run_jj([""; 0]).success();
assert_eq!(output, work_dir.run_jj(["log"]));
// Command argument that looks like a command name.
work_dir
.run_jj(["bookmark", "create", "-r@", "help"])
.success();
work_dir
.run_jj(["bookmark", "create", "-r@", "log"])
.success();
work_dir
.run_jj(["bookmark", "create", "-r@", "show"])
.success();
// TODO: test_env.run_jj(["-r", "help"]).success()
insta::assert_snapshot!(work_dir.run_jj(["-r", "log"]), @"
@ qpvuntsm test.user@example.com 2001-02-03 08:05:07 help log show e8849ae1
│ (empty) (no description set)
~
[EOF]
");
insta::assert_snapshot!(work_dir.run_jj(["-r", "show"]), @"
@ qpvuntsm test.user@example.com 2001-02-03 08:05:07 help log show e8849ae1
│ (empty) (no description set)
~
[EOF]
");
// Multiple default command strings work.
test_env.add_config(r#"ui.default-command=["commit", "-m", "foo"]"#);
work_dir.run_jj(["new"]).success();
work_dir.write_file("file.txt", "file");
let output = work_dir.run_jj([""; 0]);
insta::assert_snapshot!(output, @"
------- stderr -------
Working copy (@) now at: kxryzmor 8db1ba9a (empty) (no description set)
Parent commit (@-) : lylxulpl 19f3adb2 foo
[EOF]
");
// We get a warning if the default command isn't recognized and looks like
// it should have been specified as an array.
test_env.add_config(r#"ui.default-command="log -n 10""#);
let output = work_dir.run_jj([""; 0]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Warning: To include flags/arguments in `ui.default-command`, use an array instead of a string: `ui.default-command = ["log", "-n", "10"]`
error: unrecognized subcommand 'log -n 10'
tip: a similar subcommand exists: 'log'
Usage: jj [OPTIONS] <COMMAND>
For more information, try '--help'.
[EOF]
[exit status: 2]
"#);
// Hint isn't printed for explicit subcommands.
let output = work_dir.run_jj(["foobar"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
error: unrecognized subcommand 'foobar'
tip: a similar subcommand exists: 'bookmark'
Usage: jj [OPTIONS] <COMMAND>
For more information, try '--help'.
[EOF]
[exit status: 2]
");
// Hint isn't printed for explicit subcommands with spaces.
let output = work_dir.run_jj(["log -n 10"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
error: unrecognized subcommand 'log -n 10'
tip: a similar subcommand exists: 'log'
Usage: jj [OPTIONS] <COMMAND>
For more information, try '--help'.
[EOF]
[exit status: 2]
");
// While the default command is invalid, explicit subcommands still work.
let output = work_dir.run_jj(["status"]);
insta::assert_snapshot!(output, @r"
The working copy has no changes.
Working copy (@) : kxryzmor 8db1ba9a (empty) (no description set)
Parent commit (@-): lylxulpl 19f3adb2 foo
[EOF]
");
}
#[test]
fn test_ignore_working_copy() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.write_file("file", "initial");
let output = work_dir.run_jj(["log", "-T", "commit_id"]);
insta::assert_snapshot!(output, @"
@ 82a10a4d9ef783fd68b661f40ce10dd80d599d9e
◆ 0000000000000000000000000000000000000000
[EOF]
");
// Modify the file. With --ignore-working-copy, we still get the same commit
// ID.
work_dir.write_file("file", "modified");
let output_again = work_dir.run_jj(["log", "-T", "commit_id", "--ignore-working-copy"]);
assert_eq!(output_again, output);
// But without --ignore-working-copy, we get a new commit ID.
let output = work_dir.run_jj(["log", "-T", "commit_id"]);
insta::assert_snapshot!(output, @"
@ 00fc09f48ccf5c8b025a0f93b0ec3b0e4294a598
◆ 0000000000000000000000000000000000000000
[EOF]
");
}
#[test]
fn test_no_integrate_operation() {
let test_env = TestEnvironment::default();
test_env
.run_jj_in(test_env.env_root(), &["git", "init", "repo"])
.success();
let repo_path = test_env.env_root().join("repo");
std::fs::write(repo_path.join("file1"), "initial").unwrap();
test_env
.run_jj_in(&repo_path, &["commit", "-m=initial"])
.success();
let op_log_output = test_env.run_jj_in(&repo_path, &["op", "log"]);
let working_copy_output = test_env.run_jj_in(&repo_path, &["debug", "working-copy"]);
// Modify the working copy and run a mutating operation. With
// --no-integrate-operation, the working copy gets snapshotted and the operation
// gets created, but there's no new operation in the operation log, and the
// working copy state is not updated.
std::fs::write(repo_path.join("file2"), "initial").unwrap();
let output = test_env.run_jj_in(&repo_path, &["squash", "--no-integrate-operation"]);
insta::assert_snapshot!(output.stdout, @"");
insta::assert_snapshot!(output.stderr, @"
Snapshot operation left uncommitted because --no-integrate-operation was requested: 13357990b38a
Operation left uncommitted because --no-integrate-operation was requested: a028de7aa4f4
[EOF]
");
let stderr = output.stderr.into_raw();
let last_line = stderr.lines().last().unwrap();
let op_id_hex = last_line[last_line.len() - 12..].to_string();
let output = test_env.run_jj_in(&repo_path, &["op", "log", "--ignore-working-copy"]);
assert_eq!(output.stdout, op_log_output.stdout);
let output = test_env.run_jj_in(&repo_path, &["debug", "working-copy"]);
assert_eq!(output.stdout, working_copy_output.stdout);
// We can see the resulting log and op log with --at-op
let stdout = test_env.run_jj_in(&repo_path, &["log", "-s", "--at-op", op_id_hex.as_str()]);
insta::assert_snapshot!(stdout, @"
@ mzvwutvl test.user@example.com 2001-02-03 08:05:11 10644da4
│ (empty) (no description set)
○ qpvuntsm test.user@example.com 2001-02-03 08:05:11 599772f7
│ initial
│ A file1
│ A file2
◆ zzzzzzzz root() 00000000
[EOF]
");
let stdout = test_env.run_jj_in(&repo_path, &["op", "log", "--at-op", op_id_hex.as_str()]);
insta::assert_snapshot!(stdout, @"
@ a028de7aa4f4 test-username@host.example.com default@ 2001-02-03 04:05:11.000 +07:00 - 2001-02-03 04:05:11.000 +07:00
│ squash commits into e6fc2362ee5fdd5eb879befc0ae556a2f57b94a0
│ args: jj squash --no-integrate-operation
○ 13357990b38a test-username@host.example.com default@ 2001-02-03 04:05:11.000 +07:00 - 2001-02-03 04:05:11.000 +07:00
│ snapshot working copy
│ args: jj squash --no-integrate-operation
○ e167310e1125 test-username@host.example.com default@ 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
│ commit 289d54a4554ed0d7df9c47d566480a6b773ee431
│ args: jj commit '-m=initial'
○ 55b88fdb1890 test-username@host.example.com default@ 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
│ snapshot working copy
│ args: jj commit '-m=initial'
○ 90267f31f904 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
│ add workspace 'default'
○ 000000000000 root()
[EOF]
");
}
#[test]
fn test_no_integrate_operation_colocated() {
let test_env = TestEnvironment::default();
test_env
.run_jj_in(test_env.env_root(), &["git", "init", "--colocate", "repo"])
.success();
let repo_path = test_env.env_root().join("repo");
std::fs::write(repo_path.join("file1"), "initial").unwrap();
test_env
.run_jj_in(&repo_path, &["commit", "-m=initial"])
.success();
let op_log_output = test_env.run_jj_in(&repo_path, &["op", "log"]);
let working_copy_output = test_env.run_jj_in(&repo_path, &["debug", "working-copy"]);
// Modify the working copy and run a mutating operation. With
// --no-integrate-operation, the working copy gets snapshotted and the operation
// gets created, but there's no new operation in the operation log, and the
// working copy state is not updated.
std::fs::write(repo_path.join("file2"), "initial").unwrap();
let output = test_env.run_jj_in(&repo_path, &["squash", "--no-integrate-operation"]);
insta::assert_snapshot!(output.stdout, @"");
insta::assert_snapshot!(output.stderr, @"
Snapshot operation left uncommitted because --no-integrate-operation was requested: f9d2255ff5a3
Operation left uncommitted because --no-integrate-operation was requested: 0eccbf94f56f
[EOF]
");
let stderr = output.stderr.into_raw();
let last_line = stderr.lines().last().unwrap();
let op_id_hex = last_line[last_line.len() - 12..].to_string();
let output = test_env.run_jj_in(&repo_path, &["op", "log", "--ignore-working-copy"]);
assert_eq!(output.stdout, op_log_output.stdout);
let output = test_env.run_jj_in(&repo_path, &["debug", "working-copy"]);
assert_eq!(output.stdout, working_copy_output.stdout);
// We can see the resulting log and op log with --at-op
let stdout = test_env.run_jj_in(&repo_path, &["log", "-s", "--at-op", op_id_hex.as_str()]);
insta::assert_snapshot!(stdout, @"
@ mzvwutvl test.user@example.com 2001-02-03 08:05:11 10644da4
│ (empty) (no description set)
○ qpvuntsm test.user@example.com 2001-02-03 08:05:11 599772f7
│ initial
│ A file1
│ A file2
◆ zzzzzzzz root() 00000000
[EOF]
");
let stdout = test_env.run_jj_in(&repo_path, &["op", "log", "--at-op", op_id_hex.as_str()]);
insta::assert_snapshot!(stdout, @"
@ 0eccbf94f56f test-username@host.example.com default@ 2001-02-03 04:05:11.000 +07:00 - 2001-02-03 04:05:11.000 +07:00
│ squash commits into e6fc2362ee5fdd5eb879befc0ae556a2f57b94a0
│ args: jj squash --no-integrate-operation
○ f9d2255ff5a3 test-username@host.example.com default@ 2001-02-03 04:05:11.000 +07:00 - 2001-02-03 04:05:11.000 +07:00
│ snapshot working copy
│ args: jj squash --no-integrate-operation
○ 30e6a62058cc test-username@host.example.com default@ 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
│ commit 289d54a4554ed0d7df9c47d566480a6b773ee431
│ args: jj commit '-m=initial'
○ 55b88fdb1890 test-username@host.example.com default@ 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00
│ snapshot working copy
│ args: jj commit '-m=initial'
○ 90267f31f904 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00
│ add workspace 'default'
○ 000000000000 root()
[EOF]
");
}
#[test]
fn test_repo_arg_with_git_init() {
let test_env = TestEnvironment::default();
let output = test_env.run_jj_in(".", ["git", "init", "-R=.", "repo"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: There is no jj repo in "."
[EOF]
[exit status: 1]
"#);
}
#[test]
fn test_repo_arg_with_git_clone() {
let test_env = TestEnvironment::default();
let output = test_env.run_jj_in(".", ["git", "clone", "-R=.", "remote"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: There is no jj repo in "."
[EOF]
[exit status: 1]
"#);
}
#[test]
fn test_resolve_workspace_directory() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let sub_dir = work_dir.create_dir_all("dir/subdir");
// Ancestor of cwd
let output = sub_dir.run_jj(["status"]);
insta::assert_snapshot!(output, @"
The working copy has no changes.
Working copy (@) : qpvuntsm e8849ae1 (empty) (no description set)
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
[EOF]
");
// Explicit subdirectory path
let output = sub_dir.run_jj(["status", "-R", "."]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: There is no jj repo in "."
[EOF]
[exit status: 1]
"#);
// Valid explicit path
let output = sub_dir.run_jj(["status", "-R", "../.."]);
insta::assert_snapshot!(output, @"
The working copy has no changes.
Working copy (@) : qpvuntsm e8849ae1 (empty) (no description set)
Parent commit (@-): zzzzzzzz 00000000 (empty) (no description set)
[EOF]
");
// "../../..".ancestors() contains "../..", but it should never be looked up.
let output = sub_dir.run_jj(["status", "-R", "../../.."]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: There is no jj repo in "../../.."
[EOF]
[exit status: 1]
"#);
}
#[test]
fn test_no_workspace_directory() {
let test_env = TestEnvironment::default();
let work_dir = test_env.work_dir("repo");
work_dir.create_dir_all("");
let output = work_dir.run_jj(["status"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: There is no jj repo in "."
[EOF]
[exit status: 1]
"#);
let output = test_env.run_jj_in(".", ["status", "-R", "repo"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: There is no jj repo in "repo"
[EOF]
[exit status: 1]
"#);
work_dir.create_dir_all(".git");
let output = work_dir.run_jj(["status"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: There is no jj repo in "."
Hint: It looks like this is a git repo. You can create a jj repo backed by it by running this:
jj git init
[EOF]
[exit status: 1]
"#);
}
#[test]
fn test_bad_path() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let sub_dir = work_dir.create_dir_all("dir");
// cwd == workspace_root
let output = work_dir.run_jj(["file", "show", "../out"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Error: Failed to parse fileset: Invalid file pattern
Caused by:
1: --> 1:1
|
1 | ../out
| ^----^
|
= Invalid file pattern
2: Path "../out" is not in the repo "."
3: Invalid component ".." in repo-relative path "../out"
[EOF]
[exit status: 1]
"#);
// cwd != workspace_root, can't be parsed as repo-relative path
let output = sub_dir.run_jj(["file", "show", "../.."]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Error: Failed to parse fileset: Invalid file pattern
Caused by:
1: --> 1:1
|
1 | ../..
| ^---^
|
= Invalid file pattern
2: Path "../.." is not in the repo ".."
3: Invalid component ".." in repo-relative path ".."
[EOF]
[exit status: 1]
"#);
// cwd != workspace_root, can be parsed as repo-relative path
let output = test_env.run_jj_in(".", ["file", "show", "-Rrepo", "out"]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
Error: Failed to parse fileset: Invalid file pattern
Caused by:
1: --> 1:1
|
1 | out
| ^-^
|
= Invalid file pattern
2: Path "out" is not in the repo "repo"
3: Invalid component ".." in repo-relative path "../out"
Hint: Consider using root:"out" to specify repo-relative path
[EOF]
[exit status: 1]
"#);
}
#[test]
fn test_invalid_filesets_looking_like_filepaths() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["file", "show", "abc~"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Error: Failed to parse fileset: Syntax error
Caused by: --> 1:5
|
1 | abc~
| ^---
|
= expected `~` or <primary>
Hint: See https://docs.jj-vcs.dev/latest/filesets/ or use `jj help -k filesets` for filesets syntax and how to match file paths.
[EOF]
[exit status: 1]
");
}
#[test]
fn test_broken_repo_structure() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let store_path = work_dir.root().join(".jj").join("repo").join("store");
let store_type_path = store_path.join("type");
// Test the error message when the git repository can't be located.
work_dir.remove_file(store_path.join("git_target"));
let output = work_dir.run_jj(["log"]);
insta::assert_snapshot!(output.strip_stderr_last_line(), @"
------- stderr -------
Internal error: The repository appears broken or inaccessible
Caused by:
1: Cannot access $TEST_ENV/repo/.jj/repo/store/git_target
[EOF]
[exit status: 255]
");
// Test the error message when the commit backend is of unknown type.
work_dir.write_file(&store_type_path, "unknown");
let output = work_dir.run_jj(["log"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Internal error: This version of the jj binary doesn't support this type of repo
Caused by: Unsupported commit backend type 'unknown'
[EOF]
[exit status: 255]
");
// Test the error message when the file indicating the commit backend type
// cannot be read.
work_dir.remove_file(&store_type_path);
work_dir.create_dir(&store_type_path);
let output = work_dir.run_jj(["log"]);
insta::assert_snapshot!(output.strip_stderr_last_line(), @"
------- stderr -------
Internal error: The repository appears broken or inaccessible
Caused by:
1: Failed to read commit backend type
2: Cannot access $TEST_ENV/repo/.jj/repo/store/type
[EOF]
[exit status: 255]
");
// Test when the .jj directory is empty. The error message is identical to
// the previous one, but writing the default type file would also fail.
work_dir.remove_dir_all(".jj");
work_dir.create_dir(".jj");
let output = work_dir.run_jj(["log"]);
insta::assert_snapshot!(output.strip_stderr_last_line(), @"
------- stderr -------
Internal error: The repository appears broken or inaccessible
Caused by:
1: Failed to read commit backend type
2: Cannot access $TEST_ENV/repo/.jj/repo/store/type
[EOF]
[exit status: 255]
");
}
#[test]
fn test_color_config() {
let mut test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// Test that --color=always is respected.
let output = work_dir.run_jj(["--color=always", "log", "-T", "commit_id"]);
insta::assert_snapshot!(output, @"
[1m[38;5;2m@[0m [38;5;4me8849ae12c709f2321908879bc724fdb2ab8a781[39m
[1m[38;5;14m◆[0m [38;5;4m0000000000000000000000000000000000000000[39m
[EOF]
");
// Test that color is used if it's requested in the config file
test_env.add_config(r#"ui.color="always""#);
let output = work_dir.run_jj(["log", "-T", "commit_id"]);
insta::assert_snapshot!(output, @"
[1m[38;5;2m@[0m [38;5;4me8849ae12c709f2321908879bc724fdb2ab8a781[39m
[1m[38;5;14m◆[0m [38;5;4m0000000000000000000000000000000000000000[39m
[EOF]
");
// Test that --color=never overrides the config.
let output = work_dir.run_jj(["--color=never", "log", "-T", "commit_id"]);
insta::assert_snapshot!(output, @"
@ e8849ae12c709f2321908879bc724fdb2ab8a781
◆ 0000000000000000000000000000000000000000
[EOF]
");
// Test that --color=auto overrides the config.
let output = work_dir.run_jj(["--color=auto", "log", "-T", "commit_id"]);
insta::assert_snapshot!(output, @"
@ e8849ae12c709f2321908879bc724fdb2ab8a781
◆ 0000000000000000000000000000000000000000
[EOF]
");
// Test that --config 'ui.color=never' overrides the config.
let output = work_dir.run_jj(["--config=ui.color=never", "log", "-T", "commit_id"]);
insta::assert_snapshot!(output, @"
@ e8849ae12c709f2321908879bc724fdb2ab8a781
◆ 0000000000000000000000000000000000000000
[EOF]
");
// --color overrides --config 'ui.color=...'.
let output = work_dir.run_jj([
"--color",
"never",
"--config=ui.color=always",
"log",
"-T",
"commit_id",
]);
insta::assert_snapshot!(output, @"
@ e8849ae12c709f2321908879bc724fdb2ab8a781
◆ 0000000000000000000000000000000000000000
[EOF]
");
// Test that NO_COLOR does NOT override the request for color in the config file
test_env.add_env_var("NO_COLOR", "1");
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["log", "-T", "commit_id"]);
insta::assert_snapshot!(output, @"
[1m[38;5;2m@[0m [38;5;4me8849ae12c709f2321908879bc724fdb2ab8a781[39m
[1m[38;5;14m◆[0m [38;5;4m0000000000000000000000000000000000000000[39m
[EOF]
");
// Test that per-repo config overrides the user config.
work_dir
.run_jj(["config", "set", "--repo", "ui.color", "never"])
.success();
let output = work_dir.run_jj(["log", "-T", "commit_id"]);
insta::assert_snapshot!(output, @"
@ e8849ae12c709f2321908879bc724fdb2ab8a781
◆ 0000000000000000000000000000000000000000
[EOF]
");
// Invalid --color
let output = work_dir.run_jj(["log", "--color=foo"]);
insta::assert_snapshot!(output, @"
------- stderr -------
error: invalid value 'foo' for '--color <WHEN>'
[possible values: always, never, debug, auto]
For more information, try '--help'.
[EOF]
[exit status: 2]
");
// Invalid ui.color
let stderr = work_dir.run_jj(["log", "--config=ui.color=true"]);
insta::assert_snapshot!(stderr, @"
------- stderr -------
Config error: Invalid type or value for ui.color
Caused by: wanted string or table
For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`.
[EOF]
[exit status: 1]
");
}
#[test]
fn test_pager_env_config() {
let mut test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
test_env.add_config(r#"ui.pager = "user""#);
test_env.add_env_var("PAGER", "env-base");
test_env.add_env_var("JJ_PAGER", "env-override");
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["config", "list", "ui.pager", "--include-overridden"]);
insta::assert_snapshot!(output, @r#"
# ui.pager = "user"
ui.pager = "env-override"
[EOF]
"#);
let output = work_dir.run_jj([
"config",
"list",
"ui.pager",
"--include-overridden",
"--config=ui.pager=\"command-arg\"",
]);
insta::assert_snapshot!(output, @r#"
# ui.pager = "user"
# ui.pager = "env-override"
ui.pager = "command-arg"
[EOF]
"#);
}
#[test]
fn test_color_ui_messages() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
test_env.add_config("ui.color = 'always'");
// hint and error
let output = test_env.run_jj_in(".", ["-R."]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
[1m[38;5;6mHint: [0m[39mUse `jj -h` for a list of available commands.[39m
[39mRun `jj config set --user ui.default-command log` to disable this message.[39m
[1m[38;5;1mError: [39mThere is no jj repo in "."[0m
[EOF]
[exit status: 1]
"#);
// error source
let output = work_dir.run_jj(["log", ".."]);
insta::assert_snapshot!(output.normalize_backslash(), @r#"
------- stderr -------
[1m[38;5;1mError: [39mFailed to parse fileset: Invalid file pattern[0m
[1m[39mCaused by:[0m
[1m[39m1: [0m[39m --> 1:1[39m
[39m |[39m
[39m1 | ..[39m
[39m | ^^[39m
[39m |[39m
[39m = Invalid file pattern[39m
[1m[39m2: [0m[39mPath ".." is not in the repo "."[39m
[1m[39m3: [0m[39mInvalid component ".." in repo-relative path ".."[39m
[EOF]
[exit status: 1]
"#);
// warning
let output = work_dir.run_jj(["log", "@"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
[1m[38;5;3mWarning: [39mNo matching entries for paths: @[0m
[1m[38;5;3mWarning: [39mThe argument "@" is being interpreted as a fileset expression. To specify a revset, pass -r "@" instead.[0m
[EOF]
"#);
// error inlined in template output
work_dir.run_jj(["new"]).success();
let output = work_dir.run_jj([
"log",
"-r@|@--",
"--config=templates.log_node=commit_id",
"-Tdescription",
]);
insta::assert_snapshot!(output, @"
[38;5;4m8afc18ff677d32e40043e1bc8c1683c2f9c2e916[39m
[1m[39m<[38;5;1mError: [39mNo Commit available>[0m [38;5;8m(elided revisions)[39m
[38;5;4m0000000000000000000000000000000000000000[39m
[EOF]
");
// formatted hint
let output = work_dir.run_jj(["edit", ".."]);
insta::assert_snapshot!(output, @"
------- stderr -------
[1m[38;5;1mError: [39mRevset `..` resolved to more than one revision[0m
[1m[38;5;6mHint: [0m[39mThe revset `..` resolved to these revisions:[39m
[39m [1m[38;5;13mm[38;5;8mzvwutvl[39m [38;5;12m8[38;5;8mafc18ff[39m [38;5;10m(empty)[39m [38;5;10m(no description set)[0m[39m[39m
[39m [1m[38;5;5mq[0m[38;5;8mpvuntsm[39m [1m[38;5;4me[0m[38;5;8m8849ae1[39m [38;5;2m(empty)[39m [38;5;2m(no description set)[39m[39m
[EOF]
[exit status: 1]
");
// commit_summary template with debugging colors
let output = work_dir.run_jj(["st", "--color", "debug"]);
insta::assert_snapshot!(output, @"
The working copy has no changes.
Working copy (@) : [1m[38;5;13m<<commit working_copy change_id shortest prefix::m>>[38;5;8m<<commit working_copy change_id shortest rest::zvwutvl>>[39m<<commit working_copy:: >>[38;5;12m<<commit working_copy commit_id shortest prefix::8>>[38;5;8m<<commit working_copy commit_id shortest rest::afc18ff>>[39m<<commit working_copy:: >>[38;5;10m<<commit working_copy empty::(empty)>>[39m<<commit working_copy:: >>[38;5;10m<<commit working_copy empty description placeholder::(no description set)>>[0m
Parent commit (@-): [1m[38;5;5m<<commit change_id shortest prefix::q>>[0m[38;5;8m<<commit change_id shortest rest::pvuntsm>>[39m<<commit:: >>[1m[38;5;4m<<commit commit_id shortest prefix::e>>[0m[38;5;8m<<commit commit_id shortest rest::8849ae1>>[39m<<commit:: >>[38;5;2m<<commit empty::(empty)>>[39m<<commit:: >>[38;5;2m<<commit empty description placeholder::(no description set)>>[39m
[EOF]
");
// commit_summary template in transaction
let output = work_dir.run_jj(["revert", "--color=debug", "-r@", "-d@"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Reverted 1 commits as follows:
[1m[38;5;5m<<commit change_id shortest prefix::y>>[0m[38;5;8m<<commit change_id shortest rest::ostqsxw>>[39m<<commit:: >>[1m[38;5;4m<<commit commit_id shortest prefix::8b>>[0m[38;5;8m<<commit commit_id shortest rest::f82eec>>[39m<<commit:: >>[38;5;2m<<commit empty::(empty)>>[39m<<commit:: >><<commit description first_line::Revert "">>
[EOF]
"#);
}
#[test]
fn test_quiet() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// Can skip message about new working copy with `--quiet`
work_dir.write_file("file1", "contents");
let output = work_dir.run_jj(["--quiet", "describe", "-m=new description"]);
insta::assert_snapshot!(output, @"");
}
#[test]
fn test_repeated_args() {
// Repeating an argument should be harmless rather than an error, so that an
// argument baked into an alias (or a wrapper command) can also be passed
// explicitly. See https://github.com/jj-vcs/jj/issues/9859 and
// https://github.com/jj-vcs/jj/issues/8101.
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// `--no-pager` is parsed by the early args parser, `--ignore-working-copy`
// is not. The repetition is accepted wherever the occurrences sit relative
// to the subcommand.
for flag in ["--no-pager", "--ignore-working-copy"] {
work_dir.run_jj([flag, flag, "status"]).success();
work_dir.run_jj([flag, "status", flag]).success();
work_dir.run_jj(["status", flag, flag]).success();
}
// Arguments of subcommands are covered as well.
let output = work_dir.run_jj(["log", "--no-graph", "-T=description"]);
let repeated_output = work_dir.run_jj(["log", "--no-graph", "--no-graph", "-T=description"]);
assert_eq!(repeated_output, output);
// Repeating a flag must not disable it: `--quiet` twice still suppresses
// the message about the new working-copy commit.
let output = work_dir.run_jj(["--quiet", "--quiet", "describe", "-m=new description"]);
insta::assert_snapshot!(output, @"");
// For an option that takes a value, the last occurrence wins.
work_dir.run_jj(["new", "-m=child"]).success();
let output = work_dir.run_jj(["log", "--no-graph", "-T=description", "-n=1", "-n=2"]);
insta::assert_snapshot!(output, @r"
child
new description
[EOF]
");
work_dir.run_jj(["status", "-R=.", "-R=."]).success();
// Options that can be specified multiple times keep all of their values.
let output = work_dir.run_jj([
"--config=user.name=Custom User",
"--config=user.email=custom@example.com",
"config",
"list",
"user",
]);
insta::assert_snapshot!(output, @r#"
user.name = "Custom User"
user.email = "custom@example.com"
[EOF]
"#);
let output = work_dir.run_jj(["log", "--no-graph", "-T=description", "-r=@", "-r=@-"]);
insta::assert_snapshot!(output, @r"
child
new description
[EOF]
");
}
#[test]
fn test_early_args() {
// Test that help output parses early args
let test_env = TestEnvironment::default();
// The default is no color.
let output = test_env.run_jj_in(".", ["help"]).success();
insta::assert_snapshot!(
output.stdout.normalized().lines().find(|l| l.contains("Commands:")).unwrap(),
@"Commands:");
// Check that output is colorized.
let output = test_env
.run_jj_in(".", ["--color=always", "help"])
.success();
insta::assert_snapshot!(
output.stdout.normalized().lines().find(|l| l.contains("Commands:")).unwrap(),
@"[1m[33mCommands:[0m");
// Check that early args are accepted after the help command
let output = test_env
.run_jj_in(".", ["help", "--color=always"])
.success();
insta::assert_snapshot!(
output.stdout.normalized().lines().find(|l| l.contains("Commands:")).unwrap(),
@"[1m[33mCommands:[0m");
// Check that early args are accepted after -h/--help
let output = test_env.run_jj_in(".", ["-h", "--color=always"]).success();
insta::assert_snapshot!(
output.stdout.normalized().lines().find(|l| l.contains("Usage:")).unwrap(),
@"[1m[33mUsage:[0m [1m[32mjj[0m [32m[OPTIONS][0m [32m<COMMAND>[0m");
let output = test_env
.run_jj_in(".", ["log", "--help", "--color=always"])
.success();
insta::assert_snapshot!(
output.stdout.normalized().lines().find(|l| l.contains("Usage:")).unwrap(),
@"[1m[33mUsage:[0m [1m[32mjj log[0m [32m[OPTIONS][0m [32m[FILESETS]...[0m");
// Early args are parsed with clap's ignore_errors(), but there is a known
// bug that causes defaults to be unpopulated. Test that the early args are
// tolerant of this bug and don't cause a crash.
test_env.run_jj_in(".", ["--no-pager", "help"]).success();
test_env
.run_jj_in(".", ["--config=ui.color=always", "help"])
.success();
}
#[test]
fn test_config_args() -> TestResult {
let test_env = TestEnvironment::default();
let list_config = |args: &[&str]| {
test_env.run_jj_in(
".",
[&["config", "list", "--include-overridden", "test"], args].concat(),
)
};
std::fs::write(
test_env.env_root().join("file1.toml"),
indoc! {"
test.key1 = 'file1'
test.key2 = 'file1'
"},
)?;
std::fs::write(
test_env.env_root().join("file2.toml"),
indoc! {"
test.key3 = 'file2'
"},
)?;
let output = list_config(&["--config=test.key1=arg1"]);
insta::assert_snapshot!(output, @r#"
test.key1 = "arg1"
[EOF]
"#);
let output = list_config(&["--config-file=file1.toml"]);
insta::assert_snapshot!(output, @"
test.key1 = 'file1'
test.key2 = 'file1'
[EOF]
");
// --config items are inserted to a single layer internally
let output = list_config(&[
"--config=test.key1='arg1'",
"--config=test.key2.sub=true",
"--config=test.key1=arg3",
]);
insta::assert_snapshot!(output, @r#"
test.key1 = "arg3"
test.key2.sub = true
[EOF]
"#);
// --config* arguments are processed in order of appearance
let output = list_config(&[
"--config=test.key1=arg1",
"--config-file=file1.toml",
"--config=test.key2=arg3",
"--config-file=file2.toml",
]);
insta::assert_snapshot!(output, @r#"
# test.key1 = "arg1"
test.key1 = 'file1'
# test.key2 = 'file1'
test.key2 = "arg3"
test.key3 = 'file2'
[EOF]
"#);
let output = test_env.run_jj_in(".", ["config", "list", "--config=foo"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Config error: --config must be specified as NAME=VALUE
For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`.
[EOF]
[exit status: 1]
");
let output = test_env.run_jj_in(".", ["config", "list", "--config-file=unknown.toml"]);
insta::with_settings!({
filters => [("(?m)^([2-9]): .*", "$1: <redacted>")],
}, {
insta::assert_snapshot!(output, @"
------- stderr -------
Config error: Failed to read configuration file
Caused by:
1: Cannot access unknown.toml
2: <redacted>
For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`.
[EOF]
[exit status: 1]
");
});
Ok(())
}
#[test]
fn test_invalid_config() {
// Test that we get a reasonable error if the config is invalid (#55)
let test_env = TestEnvironment::default();
test_env.add_config("[section]key = value-missing-quotes");
let output = test_env.run_jj_in(".", ["git", "init", "repo"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Config error: Configuration cannot be parsed as TOML document
Caused by: TOML parse error at line 1, column 10
|
1 | [section]key = value-missing-quotes
| ^
unexpected key or value, expected newline, `#`
Hint: Check the config file: $TEST_ENV/config/config0002.toml
For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`.
[EOF]
[exit status: 1]
");
}
#[test]
fn test_invalid_config_value() {
// Test that we get a reasonable error if a config value is invalid
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["status", "--config=snapshot.auto-track=[0]"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Config error: Invalid type or value for snapshot.auto-track
Caused by: invalid type: sequence, expected a string
For help, see https://docs.jj-vcs.dev/latest/config/ or use `jj help -k config`.
[EOF]
[exit status: 1]
");
}
#[test]
#[cfg_attr(windows, ignore = "dirs::home_dir() can't be overridden by $HOME")] // TODO
fn test_conditional_config() {
let test_env = TestEnvironment::default();
test_env
.run_jj_in(test_env.home_dir(), ["git", "init", "repo1"])
.success();
test_env
.run_jj_in(test_env.home_dir(), ["git", "init", "repo2"])
.success();
test_env.add_config(indoc! {"
aliases.foo = ['new', 'root()', '-mglobal']
[[--scope]]
--when.repositories = ['~']
aliases.foo = ['new', 'root()', '-mhome']
[[--scope]]
--when.repositories = ['~/repo1']
aliases.foo = ['new', 'root()', '-mrepo1']
"});
// Sanity check
let output = test_env.run_jj_in(".", ["config", "list", "--include-overridden", "aliases"]);
insta::assert_snapshot!(output, @"
aliases.foo = ['new', 'root()', '-mglobal']
[EOF]
");
let output = test_env.run_jj_in(
&test_env.home_dir().join("repo1"),
["config", "list", "--include-overridden", "aliases"],
);
insta::assert_snapshot!(output, @"
# aliases.foo = ['new', 'root()', '-mglobal']
# aliases.foo = ['new', 'root()', '-mhome']
aliases.foo = ['new', 'root()', '-mrepo1']
[EOF]
");
let output = test_env.run_jj_in(
&test_env.home_dir().join("repo2"),
["config", "list", "--include-overridden", "aliases"],
);
insta::assert_snapshot!(output, @"
# aliases.foo = ['new', 'root()', '-mglobal']
aliases.foo = ['new', 'root()', '-mhome']
[EOF]
");
// Aliases can be expanded by using the conditional tables
let output = test_env.run_jj_in(&test_env.home_dir().join("repo1"), ["foo"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Working copy (@) now at: royxmykx 7c486962 (empty) repo1
Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
[EOF]
");
let output = test_env.run_jj_in(&test_env.home_dir().join("repo2"), ["foo"]);
insta::assert_snapshot!(output, @"
------- stderr -------
Working copy (@) now at: yqosqzyt 072741b8 (empty) home
Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
[EOF]
");
}
#[test]
fn test_conditional_config_environments() {
let mut test_env = TestEnvironment::default();
test_env
.run_jj_in(test_env.home_dir(), ["git", "init", "repo"])
.success();
let repo_dir = test_env.home_dir().join("repo");
test_env.add_config(indoc! {"
ui.pager = 'base'
[[--scope]]
--when.environments = ['MY_JJ_ENV=on']
ui.pager = 'kv-match'
[[--scope]]
--when.environments = ['MY_JJ_ENV']
ui.pager = 'key-exists'
[[--scope]]
--when.environments = ['ABSENT_VAR']
ui.pager = 'absent'
"});
test_env.add_env_var("MY_JJ_ENV", "on");
let output = test_env.run_jj_in(
&repo_dir,
["config", "list", "--include-overridden", "ui.pager"],
);
insta::assert_snapshot!(output, @r"
# ui.pager = 'base'
# ui.pager = 'kv-match'
ui.pager = 'key-exists'
[EOF]
");
}
/// Test that `jj` command works with the default configuration.
#[test]
fn test_default_config() -> TestResult {
let mut test_env = TestEnvironment::default();
let config_dir = test_env.env_root().join("empty-config");
std::fs::create_dir(&config_dir)?;
test_env.set_config_path(&config_dir);
let envs_to_drop = test_env
.new_jj_cmd()
.get_envs()
.filter_map(|(name, _)| name.to_str())
.filter(|&name| name.starts_with("JJ_") && name != "JJ_CONFIG")
.map(|name| name.to_owned())
.sorted_unstable()
.collect_vec();
insta::assert_debug_snapshot!(envs_to_drop, @r#"
[
"JJ_EMAIL",
"JJ_OP_HOSTNAME",
"JJ_OP_TIMESTAMP",
"JJ_OP_USERNAME",
"JJ_RANDOMNESS_SEED",
"JJ_TIMESTAMP",
"JJ_TZ_OFFSET_MINS",
"JJ_USER",
]
"#);
let run_jj = |work_dir: &TestWorkDir, args: &[&str]| {
work_dir.run_jj_with(|cmd| {
for name in &envs_to_drop {
cmd.env_remove(name);
}
cmd.args(args)
})
};
let mut insta_settings = insta::Settings::clone_current();
insta_settings.add_filter(r"\b[a-zA-Z0-9\-._]+@[a-zA-Z0-9\-._]+\b", "<user>@<host>");
insta_settings.add_filter(
r"\b[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\b",
"<date-time>",
);
insta_settings.add_filter(r"\b[k-z]{8,12}\b", "<change-id>");
insta_settings.add_filter(r"\b[0-9a-f]{8,12}\b", "<id>");
let _guard = insta_settings.bind_to_scope();
let maskable_op_user = {
let maskable_re = Regex::new(r"^[a-zA-Z0-9\-._]+$")?;
let hostname = whoami::hostname().expect("hostname should be set");
let username = whoami::username().expect("username should be set");
maskable_re.is_match(&hostname) && maskable_re.is_match(&username)
};
let output = run_jj(
&test_env.work_dir(""),
&["config", "list", r#"-Tname ++ "\n""#],
);
insta::assert_snapshot!(output, @"
operation.hostname
operation.username
[EOF]
");
let output = run_jj(&test_env.work_dir(""), &["git", "init", "repo"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Initialized repo in "repo"
[EOF]
"#);
let work_dir = test_env.work_dir("repo");
let output = run_jj(&work_dir, &["new"]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Working copy (@) now at: <change-id> <id> (empty) (no description set)
Parent commit (@-) : <change-id> <id> (empty) (no description set)
Warning: Name and email not configured. Until configured, your commits will be created with the empty identity, and can't be pushed to remotes.
Hint: To configure, run:
jj config set --user user.name "Some One"
jj config set --user user.email "<user>@<host>"
[EOF]
"#);
let output = run_jj(&work_dir, &["log"]);
insta::assert_snapshot!(output, @"
@ <change-id> (no email set) <date-time> <id>
│ (empty) (no description set)
○ <change-id> (no email set) <date-time> <id>
│ (empty) (no description set)
◆ <change-id> root() <id>
[EOF]
");
let time_config =
"--config=template-aliases.'format_time_range(t)'='format_timestamp(t.end())'";
let output = run_jj(&work_dir, &["op", "log", time_config]);
if maskable_op_user {
insta::assert_snapshot!(output, @"
@ <id> <user>@<host> default@ <date-time>
│ new empty commit
│ args: jj new
○ <id> <user>@<host> <date-time>
│ add workspace 'default'
○ <id> root()
[EOF]
");
}
Ok(())
}
#[test]
fn test_no_user_configured() {
// Test that the user is reminded if they haven't configured their name or email
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj_with(|cmd| {
cmd.args(["describe", "-m", "without name"])
.env_remove("JJ_USER")
});
insta::assert_snapshot!(output, @r#"
------- stderr -------
Working copy (@) now at: qpvuntsm 7e7014c2 (empty) without name
Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
Warning: Name not configured. Until configured, your commits will be created with the empty identity, and can't be pushed to remotes.
Hint: To configure, run:
jj config set --user user.name "Some One"
[EOF]
"#);
let output = work_dir.run_jj_with(|cmd| {
cmd.args(["describe", "-m", "without email"])
.env_remove("JJ_EMAIL")
});
insta::assert_snapshot!(output, @r#"
------- stderr -------
Working copy (@) now at: qpvuntsm 876580e3 (empty) without email
Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
Warning: Email not configured. Until configured, your commits will be created with the empty identity, and can't be pushed to remotes.
Hint: To configure, run:
jj config set --user user.email "someone@example.com"
[EOF]
"#);
let output = work_dir.run_jj_with(|cmd| {
cmd.args(["describe", "-m", "without name and email"])
.env_remove("JJ_USER")
.env_remove("JJ_EMAIL")
});
insta::assert_snapshot!(output, @r#"
------- stderr -------
Working copy (@) now at: qpvuntsm f87356a1 (empty) without name and email
Parent commit (@-) : zzzzzzzz 00000000 (empty) (no description set)
Warning: Name and email not configured. Until configured, your commits will be created with the empty identity, and can't be pushed to remotes.
Hint: To configure, run:
jj config set --user user.name "Some One"
jj config set --user user.email "someone@example.com"
[EOF]
"#);
}
#[test]
fn test_help() {
// Test that global options are separated out in the help output
let test_env = TestEnvironment::default();
let output = test_env.run_jj_in(".", ["diffedit", "-h"]);
insta::assert_snapshot!(output, @"
Touch up the content changes in a revision with a diff editor
Usage: jj diffedit [OPTIONS] [FILESETS]...
Arguments:
[FILESETS]... Edit only these paths (unmatched paths will remain unchanged)
Options:
-r, --revision <REVSET> The revision to touch up
-f, --from <REVSET> Show changes from this revision
-t, --to <REVSET> Edit changes in this revision
--tool <NAME> Specify diff editor to be used
--restore-descendants Preserve the content (not the diff) when rebasing descendants
-h, --help Print help (see more with '--help')
Global Options:
-R, --repository <REPOSITORY> Path to repository to operate on
--ignore-working-copy Don't snapshot the working copy, and don't update it
--no-integrate-operation Run the command as usual but don't integrate any operations
--ignore-immutable Allow rewriting immutable commits
--at-operation <AT_OPERATION> Operation to load the repo at [alias: --at-op]
--debug Enable debug logging
--color <WHEN> When to colorize output [possible values: always, never, debug,
auto]
--quiet Silence non-primary command output
--no-pager Disable the pager
--config <NAME=VALUE> Additional configuration options (can be repeated)
--config-file <PATH> Additional configuration files (can be repeated)
[EOF]
");
}
#[test]
fn test_debug_logging_enabled() {
// Test that the debug flag enabled debug logging
let test_env = TestEnvironment::default();
let output = test_env.run_jj_in(".", ["version", "--debug"]).success();
// Split the first log line into a timestamp and the rest.
// The timestamp is constant sized so this is a robust operation.
// Example timestamp: 2022-11-20T06:24:05.477703Z
let (_timestamp, log_line) = output
.stderr
.normalized()
.lines()
.next()
.expect("debug logging on first line")
.split_at(36);
// The log format is currently Pretty so we include the terminal markup.
// Luckily, insta will print this in color when reviewing.
insta::assert_snapshot!(log_line, @"[32m INFO[0m [2mjj_cli::cli_util[0m[2m:[0m debug logging enabled");
}