jj/cli/tests/test_log_command.rs
2024-04-15 22:21:31 +02:00

1536 lines
46 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 crate::common::{get_stdout_string, TestEnvironment};
#[test]
fn test_log_with_empty_revision() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
let stderr = test_env.jj_cmd_cli_error(&repo_path, &["log", "-r="]);
insta::assert_snapshot!(stderr, @r###"
error: a value is required for '--revisions <REVISIONS>' but none was supplied
For more information, try '--help'.
"###);
}
#[test]
fn test_log_with_no_template() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
let stderr = test_env.jj_cmd_cli_error(&repo_path, &["log", "-T"]);
insta::assert_snapshot!(stderr, @r###"
error: a value is required for '--template <TEMPLATE>' but none was supplied
For more information, try '--help'.
Hint: The following template aliases are defined:
- builtin_change_id_with_hidden_and_divergent_info
- builtin_log_comfortable
- builtin_log_compact
- builtin_log_detailed
- builtin_log_node
- builtin_log_node_ascii
- builtin_log_oneline
- builtin_op_log_comfortable
- builtin_op_log_compact
- builtin_op_log_node
- builtin_op_log_node_ascii
- commit_summary_separator
- description_placeholder
- email_placeholder
- name_placeholder
"###);
}
#[test]
fn test_log_with_or_without_diff() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "add a file"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "a new commit"]);
std::fs::write(repo_path.join("file1"), "foo\nbar\n").unwrap();
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description"]);
insta::assert_snapshot!(stdout, @r###"
@ a new commit
◉ add a file
"###);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "-p"]);
insta::assert_snapshot!(stdout, @r###"
@ a new commit
│ Modified regular file file1:
│ 1 1: foo
│ 2: bar
◉ add a file
│ Added regular file file1:
│ 1: foo
"###);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "--no-graph"]);
insta::assert_snapshot!(stdout, @r###"
a new commit
add a file
"###);
// `-p` for default diff output, `-s` for summary
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "-p", "-s"]);
insta::assert_snapshot!(stdout, @r###"
@ a new commit
│ M file1
│ Modified regular file file1:
│ 1 1: foo
│ 2: bar
◉ add a file
│ A file1
│ Added regular file file1:
│ 1: foo
"###);
// `-s` for summary, `--git` for git diff (which implies `-p`)
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "-s", "--git"]);
insta::assert_snapshot!(stdout, @r###"
@ a new commit
│ M file1
│ diff --git a/file1 b/file1
│ index 257cc5642c...3bd1f0e297 100644
│ --- a/file1
│ +++ b/file1
│ @@ -1,1 +1,2 @@
│ foo
│ +bar
◉ add a file
│ A file1
│ diff --git a/file1 b/file1
│ new file mode 100644
│ index 0000000000..257cc5642c
│ --- /dev/null
│ +++ b/file1
│ @@ -1,0 +1,1 @@
│ +foo
"###);
// `-p` enables default "summary" output, so `-s` is noop
let stdout = test_env.jj_cmd_success(
&repo_path,
&[
"log",
"-T",
"description",
"-p",
"-s",
"--config-toml=ui.diff.format='summary'",
],
);
insta::assert_snapshot!(stdout, @r###"
@ a new commit
│ M file1
◉ add a file
│ A file1
"###);
// `-p` enables default "color-words" diff output, so `--color-words` is noop
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "-p", "--color-words"],
);
insta::assert_snapshot!(stdout, @r###"
@ a new commit
│ Modified regular file file1:
│ 1 1: foo
│ 2: bar
◉ add a file
│ Added regular file file1:
│ 1: foo
"###);
// `--git` enables git diff, so `-p` is noop
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "--no-graph", "-p", "--git"],
);
insta::assert_snapshot!(stdout, @r###"
a new commit
diff --git a/file1 b/file1
index 257cc5642c...3bd1f0e297 100644
--- a/file1
+++ b/file1
@@ -1,1 +1,2 @@
foo
+bar
add a file
diff --git a/file1 b/file1
new file mode 100644
index 0000000000..257cc5642c
--- /dev/null
+++ b/file1
@@ -1,0 +1,1 @@
+foo
"###);
// Cannot use both `--git` and `--color-words`
let stderr = test_env.jj_cmd_cli_error(
&repo_path,
&[
"log",
"-T",
"description",
"--no-graph",
"-p",
"--git",
"--color-words",
],
);
insta::assert_snapshot!(stderr, @r###"
error: the argument '--git' cannot be used with '--color-words'
Usage: jj log --template <TEMPLATE> --no-graph --patch --git [PATHS]...
For more information, try '--help'.
"###);
// `-s` with or without graph
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "-s"]);
insta::assert_snapshot!(stdout, @r###"
@ a new commit
│ M file1
◉ add a file
│ A file1
"###);
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "--no-graph", "-s"],
);
insta::assert_snapshot!(stdout, @r###"
a new commit
M file1
add a file
A file1
"###);
// `--git` implies `-p`, with or without graph
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "-r", "@", "--git"],
);
insta::assert_snapshot!(stdout, @r###"
@ a new commit
│ diff --git a/file1 b/file1
~ index 257cc5642c...3bd1f0e297 100644
--- a/file1
+++ b/file1
@@ -1,1 +1,2 @@
foo
+bar
"###);
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "-r", "@", "--no-graph", "--git"],
);
insta::assert_snapshot!(stdout, @r###"
a new commit
diff --git a/file1 b/file1
index 257cc5642c...3bd1f0e297 100644
--- a/file1
+++ b/file1
@@ -1,1 +1,2 @@
foo
+bar
"###);
// `--color-words` implies `-p`, with or without graph
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "-r", "@", "--color-words"],
);
insta::assert_snapshot!(stdout, @r###"
@ a new commit
│ Modified regular file file1:
~ 1 1: foo
2: bar
"###);
let stdout = test_env.jj_cmd_success(
&repo_path,
&[
"log",
"-T",
"description",
"-r",
"@",
"--no-graph",
"--color-words",
],
);
insta::assert_snapshot!(stdout, @r###"
a new commit
Modified regular file file1:
1 1: foo
2: bar
"###);
}
#[test]
fn test_log_null_terminate_multiline_descriptions() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
test_env.jj_cmd_ok(
&repo_path,
&["commit", "-m", "commit 1 line 1", "-m", "commit 1 line 2"],
);
test_env.jj_cmd_ok(
&repo_path,
&["commit", "-m", "commit 2 line 1", "-m", "commit 2 line 2"],
);
test_env.jj_cmd_ok(
&repo_path,
&["describe", "-m", "commit 3 line 1", "-m", "commit 3 line 2"],
);
let stdout = test_env.jj_cmd_success(
&repo_path,
&[
"log",
"-r",
"~root()",
"-T",
r#"description ++ "\0""#,
"--no-graph",
],
);
insta::assert_debug_snapshot!(
stdout,
@r###""commit 3 line 1\n\ncommit 3 line 2\n\0commit 2 line 1\n\ncommit 2 line 2\n\0commit 1 line 1\n\ncommit 1 line 2\n\0""###
)
}
#[test]
fn test_log_shortest_accessors() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
let render = |rev, template| {
test_env.jj_cmd_success(
&repo_path,
&["log", "--no-graph", "-r", rev, "-T", template],
)
};
test_env.add_config(
r#"
[template-aliases]
'format_id(id)' = 'id.shortest(12).prefix() ++ "[" ++ id.shortest(12).rest() ++ "]"'
"#,
);
std::fs::write(repo_path.join("file"), "original file\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "initial"]);
test_env.jj_cmd_ok(&repo_path, &["branch", "c", "original"]);
insta::assert_snapshot!(
render("original", r#"format_id(change_id) ++ " " ++ format_id(commit_id)"#),
@"q[pvuntsmwlqt] b[a1a30916d29]");
// Create a chain of 10 commits
for i in 1..10 {
test_env.jj_cmd_ok(&repo_path, &["new", "-m", &format!("commit{i}")]);
std::fs::write(repo_path.join("file"), format!("file {i}\n")).unwrap();
}
// Create 2^3 duplicates of the chain
for _ in 0..3 {
test_env.jj_cmd_ok(&repo_path, &["duplicate", "description(commit)"]);
}
insta::assert_snapshot!(
render("original", r#"format_id(change_id) ++ " " ++ format_id(commit_id)"#),
@"qpv[untsmwlqt] ba1[a30916d29]");
insta::assert_snapshot!(
render("::@", r#"change_id.shortest() ++ " " ++ commit_id.shortest() ++ "\n""#),
@r###"
wq 03
km f7
kp e7
zn 38
yo 0cf
vr 9e
yq 06
ro 1f
mz 7b
qpv ba1
zzz 00
"###);
insta::assert_snapshot!(
render("::@", r#"format_id(change_id) ++ " " ++ format_id(commit_id) ++ "\n""#),
@r###"
wq[nwkozpkust] 03[f51310b83e]
km[kuslswpqwq] f7[7fb1909080]
kp[qxywonksrl] e7[15ad5db646]
zn[kkpsqqskkl] 38[622e54e2e5]
yo[stqsxwqrlt] 0cf[42f60199c]
vr[uxwmqvtpmx] 9e[6015e4e622]
yq[osqzytrlsw] 06[f34d9b1475]
ro[yxmykxtrkr] 1f[99a5e19891]
mz[vwutvlkqwt] 7b[1f7dee65b4]
qpv[untsmwlqt] ba1[a30916d29]
zzz[zzzzzzzzz] 00[0000000000]
"###);
// Can get shorter prefixes in configured revset
test_env.add_config(r#"revsets.short-prefixes = "(@----)::""#);
insta::assert_snapshot!(
render("::@", r#"format_id(change_id) ++ " " ++ format_id(commit_id) ++ "\n""#),
@r###"
w[qnwkozpkust] 03[f51310b83e]
km[kuslswpqwq] f[77fb1909080]
kp[qxywonksrl] e[715ad5db646]
z[nkkpsqqskkl] 3[8622e54e2e5]
y[ostqsxwqrlt] 0c[f42f60199c]
vr[uxwmqvtpmx] 9e[6015e4e622]
yq[osqzytrlsw] 06f[34d9b1475]
ro[yxmykxtrkr] 1f[99a5e19891]
mz[vwutvlkqwt] 7b[1f7dee65b4]
qpv[untsmwlqt] ba1[a30916d29]
zzz[zzzzzzzzz] 00[0000000000]
"###);
// Can disable short prefixes by setting to empty string
test_env.add_config(r#"revsets.short-prefixes = """#);
insta::assert_snapshot!(
render("::@", r#"format_id(change_id) ++ " " ++ format_id(commit_id) ++ "\n""#),
@r###"
wq[nwkozpkust] 03[f51310b83e]
km[kuslswpqwq] f7[7fb1909080]
kp[qxywonksrl] e7[15ad5db646]
zn[kkpsqqskkl] 38[622e54e2e5]
yo[stqsxwqrlt] 0cf[42f60199c]
vr[uxwmqvtpmx] 9e[6015e4e622]
yq[osqzytrlsw] 06f[34d9b1475]
ro[yxmykxtrkr] 1f[99a5e19891]
mz[vwutvlkqwt] 7b[1f7dee65b4]
qpv[untsmwlqt] ba1[a30916d29]
zzz[zzzzzzzzz] 00[0000000000]
"###);
}
#[test]
fn test_log_bad_short_prefixes() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
// Error on bad config of short prefixes
test_env.add_config(r#"revsets.short-prefixes = "!nval!d""#);
let stderr = test_env.jj_cmd_failure(&repo_path, &["status"]);
insta::assert_snapshot!(stderr,
@r###"
Config error: Invalid `revsets.short-prefixes`
Caused by: --> 1:1
|
1 | !nval!d
| ^---
|
= expected <expression>
For help, see https://github.com/martinvonz/jj/blob/main/docs/config.md.
"###);
}
#[test]
fn test_log_prefix_highlight_styled() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
fn prefix_format(len: Option<usize>) -> String {
format!(
r###"
separate(" ",
"Change",
change_id.shortest({0}),
description.first_line(),
commit_id.shortest({0}),
branches,
)
"###,
len.map(|l| l.to_string()).unwrap_or_default()
)
}
std::fs::write(repo_path.join("file"), "original file\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "initial"]);
test_env.jj_cmd_ok(&repo_path, &["branch", "c", "original"]);
insta::assert_snapshot!(
test_env.jj_cmd_success(&repo_path, &["log", "-r", "original", "-T", &prefix_format(Some(12))]),
@r###"
@ Change qpvuntsmwlqt initial ba1a30916d29 original
~
"###
);
// Create a chain of 10 commits
for i in 1..10 {
test_env.jj_cmd_ok(&repo_path, &["new", "-m", &format!("commit{i}")]);
std::fs::write(repo_path.join("file"), format!("file {i}\n")).unwrap();
}
// Create 2^3 duplicates of the chain
for _ in 0..3 {
test_env.jj_cmd_ok(&repo_path, &["duplicate", "description(commit)"]);
}
insta::assert_snapshot!(
test_env.jj_cmd_success(&repo_path, &["log", "-r", "original", "-T", &prefix_format(Some(12))]),
@r###"
◉ Change qpvuntsmwlqt initial ba1a30916d29 original
~
"###
);
let stdout = test_env.jj_cmd_success(
&repo_path,
&[
"--color=always",
"log",
"-r",
"@-----------..@",
"-T",
&prefix_format(Some(12)),
],
);
insta::assert_snapshot!(stdout,
@r###"
@ Change wqnwkozpkust commit9 03f51310b83e
◉ Change kmkuslswpqwq commit8 f77fb1909080
◉ Change kpqxywonksrl commit7 e715ad5db646
◉ Change znkkpsqqskkl commit6 38622e54e2e5
◉ Change yostqsxwqrlt commit5 0cf42f60199c
◉ Change vruxwmqvtpmx commit4 9e6015e4e622
◉ Change yqosqzytrlsw commit3 06f34d9b1475
◉ Change royxmykxtrkr commit2 1f99a5e19891
◉ Change mzvwutvlkqwt commit1 7b1f7dee65b4
◉ Change qpvuntsmwlqt initial ba1a30916d29 original
◉ Change zzzzzzzzzzzz 000000000000
"###
);
let stdout = test_env.jj_cmd_success(
&repo_path,
&[
"--color=always",
"log",
"-r",
"@-----------..@",
"-T",
&prefix_format(Some(3)),
],
);
insta::assert_snapshot!(stdout,
@r###"
@ Change wqn commit9 03f
◉ Change kmk commit8 f77
◉ Change kpq commit7 e71
◉ Change znk commit6 386
◉ Change yos commit5 0cf
◉ Change vru commit4 9e6
◉ Change yqo commit3 06f
◉ Change roy commit2 1f9
◉ Change mzv commit1 7b1
◉ Change qpv initial ba1 original
◉ Change zzz 000
"###
);
let stdout = test_env.jj_cmd_success(
&repo_path,
&[
"--color=always",
"log",
"-r",
"@-----------..@",
"-T",
&prefix_format(None),
],
);
insta::assert_snapshot!(stdout,
@r###"
@ Change wq commit9 03
◉ Change km commit8 f7
◉ Change kp commit7 e7
◉ Change zn commit6 38
◉ Change yo commit5 0cf
◉ Change vr commit4 9e
◉ Change yq commit3 06
◉ Change ro commit2 1f
◉ Change mz commit1 7b
◉ Change qpv initial ba1 original
◉ Change zzz 00
"###
);
}
#[test]
fn test_log_prefix_highlight_counts_hidden_commits() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
test_env.add_config(
r#"
[revsets]
short-prefixes = "" # Disable short prefixes
[template-aliases]
'format_id(id)' = 'id.shortest(12).prefix() ++ "[" ++ id.shortest(12).rest() ++ "]"'
"#,
);
let prefix_format = r#"
separate(" ",
"Change",
format_id(change_id),
description.first_line(),
format_id(commit_id),
branches,
)
"#;
std::fs::write(repo_path.join("file"), "original file\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "initial"]);
test_env.jj_cmd_ok(&repo_path, &["branch", "c", "original"]);
insta::assert_snapshot!(
test_env.jj_cmd_success(&repo_path, &["log", "-r", "all()", "-T", prefix_format]),
@r###"
@ Change q[pvuntsmwlqt] initial b[a1a30916d29] original
◉ Change z[zzzzzzzzzzz] 0[00000000000]
"###
);
// Create 2^7 hidden commits
test_env.jj_cmd_ok(&repo_path, &["new", "root()", "-m", "extra"]);
for _ in 0..7 {
test_env.jj_cmd_ok(&repo_path, &["duplicate", "description(extra)"]);
}
test_env.jj_cmd_ok(&repo_path, &["abandon", "description(extra)"]);
// The unique prefixes became longer.
insta::assert_snapshot!(
test_env.jj_cmd_success(&repo_path, &["log", "-T", prefix_format]),
@r###"
@ Change wq[nwkozpkust] 44[4c3c5066d3]
│ ◉ Change qpv[untsmwlqt] initial ba[1a30916d29] original
├─╯
◉ Change zzz[zzzzzzzzz] 00[0000000000]
"###
);
insta::assert_snapshot!(
test_env.jj_cmd_failure(&repo_path, &["log", "-r", "4", "-T", prefix_format]),
@r###"
Error: Commit ID prefix "4" is ambiguous
"###
);
insta::assert_snapshot!(
test_env.jj_cmd_success(&repo_path, &["log", "-r", "44", "-T", prefix_format]),
@r###"
@ Change wq[nwkozpkust] 44[4c3c5066d3]
~
"###
);
}
#[test]
fn test_log_short_shortest_length_parameter() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
let render = |template| test_env.jj_cmd_success(&repo_path, &["log", "-T", template]);
insta::assert_snapshot!(
render(r#"commit_id.short(0) ++ "|" ++ commit_id.shortest(0)"#), @r###"
@ |2
◉ |0
"###);
insta::assert_snapshot!(
render(r#"commit_id.short(-0) ++ "|" ++ commit_id.shortest(-0)"#), @r###"
@ |2
◉ |0
"###);
insta::assert_snapshot!(
render(r#"commit_id.short(-100) ++ "|" ++ commit_id.shortest(-100)"#), @r###"
@ <Error: out of range integral type conversion attempted>|<Error: out of range integral type conversion attempted>
◉ <Error: out of range integral type conversion attempted>|<Error: out of range integral type conversion attempted>
"###);
insta::assert_snapshot!(
render(r#"commit_id.short(100) ++ "|" ++ commit_id.shortest(100)"#), @r###"
@ 230dd059e1b059aefc0da06a2e5a7dbf22362f22|230dd059e1b059aefc0da06a2e5a7dbf22362f22
◉ 0000000000000000000000000000000000000000|0000000000000000000000000000000000000000
"###);
}
#[test]
fn test_log_author_format() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
insta::assert_snapshot!(
test_env.jj_cmd_success(&repo_path, &["log", "--revisions=@"]),
@r###"
@ qpvuntsm test.user@example.com 2001-02-03 08:05:07 230dd059
│ (empty) (no description set)
~
"###
);
let decl = "template-aliases.'format_short_signature(signature)'";
insta::assert_snapshot!(
test_env.jj_cmd_success(
&repo_path,
&[
"--config-toml",
&format!("{decl}='signature.username()'"),
"log",
"--revisions=@",
],
),
@r###"
@ qpvuntsm test.user 2001-02-03 08:05:07 230dd059
│ (empty) (no description set)
~
"###
);
}
#[test]
fn test_log_divergence() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
let template = r#"description.first_line() ++ if(divergent, " !divergence!")"#;
std::fs::write(repo_path.join("file"), "foo\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "description 1"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", template]);
// No divergence
insta::assert_snapshot!(stdout, @r###"
@ description 1
"###);
// Create divergence
test_env.jj_cmd_ok(
&repo_path,
&["describe", "-m", "description 2", "--at-operation", "@-"],
);
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "-T", template]);
insta::assert_snapshot!(stdout, @r###"
◉ description 2 !divergence!
│ @ description 1 !divergence!
├─╯
"###);
insta::assert_snapshot!(stderr, @r###"
Concurrent modification detected, resolving automatically.
"###);
}
#[test]
fn test_log_reversed() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "first"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "second"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "--reversed"]);
insta::assert_snapshot!(stdout, @r###"
◉ first
@ second
"###);
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "--reversed", "--no-graph"],
);
insta::assert_snapshot!(stdout, @r###"
first
second
"###);
}
#[test]
fn test_log_filtered_by_path() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "first"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "second"]);
std::fs::write(repo_path.join("file1"), "foo\nbar\n").unwrap();
std::fs::write(repo_path.join("file2"), "baz\n").unwrap();
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "file1"]);
insta::assert_snapshot!(stdout, @r###"
@ second
◉ first
~
"###);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "file2"]);
insta::assert_snapshot!(stdout, @r###"
@ second
~
"###);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "-s", "file1"]);
insta::assert_snapshot!(stdout, @r###"
@ second
│ M file1
◉ first
│ A file1
~
"###);
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "-s", "file2", "--no-graph"],
);
insta::assert_snapshot!(stdout, @r###"
second
A file2
"###);
// Fileset/pattern syntax is disabled by default.
let stderr = test_env.jj_cmd_failure(
test_env.env_root(),
&["log", "-R", repo_path.to_str().unwrap(), "all()"],
);
insta::assert_snapshot!(stderr.replace('\\', "/"), @r###"
Error: Path "all()" is not in the repo "repo"
Caused by: Invalid component ".." in repo-relative path "../all()"
"###);
test_env.add_config("ui.allow-filesets = true");
// empty revisions are filtered out by "all()" fileset.
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-Tdescription", "-s", "all()"]);
insta::assert_snapshot!(stdout, @r###"
@ second
│ M file1
│ A file2
◉ first
│ A file1
~
"###);
// "root:<path>" is resolved relative to the workspace root.
let stdout = test_env.jj_cmd_success(
test_env.env_root(),
&[
"log",
"-R",
repo_path.to_str().unwrap(),
"-Tdescription",
"-s",
"root:file1",
],
);
insta::assert_snapshot!(stdout.replace('\\', "/"), @r###"
@ second
│ M repo/file1
◉ first
│ A repo/file1
~
"###);
// file() revset doesn't filter the diff.
let stdout = test_env.jj_cmd_success(
&repo_path,
&[
"log",
"-T",
"description",
"-s",
"-rfile(file2)",
"--no-graph",
],
);
insta::assert_snapshot!(stdout, @r###"
second
M file1
A file2
"###);
}
#[test]
fn test_log_limit() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "a"]);
std::fs::write(repo_path.join("a"), "").unwrap();
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "b"]);
std::fs::write(repo_path.join("b"), "").unwrap();
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "c", "description(a)"]);
std::fs::write(repo_path.join("c"), "").unwrap();
test_env.jj_cmd_ok(
&repo_path,
&["new", "-m", "d", "description(c)", "description(b)"],
);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "--limit=3"]);
insta::assert_snapshot!(stdout, @r###"
@ d
├─╮
│ ◉ b
◉ │ c
├─╯
"###);
// Applied on sorted DAG
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", "description", "--limit=2"]);
insta::assert_snapshot!(stdout, @r###"
@ d
├─╮
│ ◉ b
"###);
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "--limit=2", "--no-graph"],
);
insta::assert_snapshot!(stdout, @r###"
d
c
"###);
// Applied on reversed DAG
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "--limit=3", "--reversed"],
);
insta::assert_snapshot!(stdout, @r###"
◉ a
├─╮
│ ◉ c
"###);
let stdout = test_env.jj_cmd_success(
&repo_path,
&[
"log",
"-T",
"description",
"--limit=3",
"--reversed",
"--no-graph",
],
);
insta::assert_snapshot!(stdout, @r###"
a
b
"###);
// Applied on filtered commits
let stdout = test_env.jj_cmd_success(
&repo_path,
&["log", "-T", "description", "--limit=1", "b", "c"],
);
insta::assert_snapshot!(stdout, @r###"
◉ c
~
"###);
}
#[test]
fn test_log_warn_path_might_be_revset() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
// Don't warn if the file actually exists.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "file1", "-T", "description"]);
insta::assert_snapshot!(stdout, @r###"
@
~
"###);
insta::assert_snapshot!(stderr, @"");
// Warn for `jj log .` specifically, for former Mercurial users.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", ".", "-T", "description"]);
insta::assert_snapshot!(stdout, @r###"
@
~
"###);
insta::assert_snapshot!(stderr, @r###"
Warning: The argument "." is being interpreted as a path, but this is often not useful because all non-empty commits touch '.'. If you meant to show the working copy commit, pass -r '@' instead.
"###);
// ...but checking `jj log .` makes sense in a subdirectory.
let subdir = repo_path.join("dir");
std::fs::create_dir_all(&subdir).unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(&subdir, &["log", "."]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @"");
// Warn for `jj log @` instead of `jj log -r @`.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "@", "-T", "description"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Warning: The argument "@" is being interpreted as a path. To specify a revset, pass -r "@" instead.
"###);
// Warn when there's no path with the provided name.
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["log", "file2", "-T", "description"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Warning: The argument "file2" is being interpreted as a path. To specify a revset, pass -r "file2" instead.
"###);
// If an explicit revision is provided, then suppress the warning.
let (stdout, stderr) =
test_env.jj_cmd_ok(&repo_path, &["log", "@", "-r", "@", "-T", "description"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
"###);
}
#[test]
fn test_default_revset() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "add a file"]);
// Set configuration to only show the root commit.
test_env.add_config(r#"revsets.log = "root()""#);
// Log should only contain one line (for the root commit), and not show the
// commit created above.
assert_eq!(
1,
test_env
.jj_cmd_success(&repo_path, &["log", "-T", "commit_id"])
.lines()
.count()
);
// The default revset is not used if a path is specified
insta::assert_snapshot!(
test_env.jj_cmd_success(&repo_path, &["log", "file1", "-T", "description"]),
@r###"
@ add a file
~
"###);
}
#[test]
fn test_default_revset_per_repo() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
std::fs::write(repo_path.join("file1"), "foo\n").unwrap();
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "add a file"]);
// Set configuration to only show the root commit.
std::fs::write(
repo_path.join(".jj/repo/config.toml"),
r#"revsets.log = "root()""#,
)
.unwrap();
// Log should only contain one line (for the root commit), and not show the
// commit created above.
assert_eq!(
1,
test_env
.jj_cmd_success(&repo_path, &["log", "-T", "commit_id"])
.lines()
.count()
);
}
#[test]
fn test_multiple_revsets() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
for name in ["foo", "bar", "baz"] {
test_env.jj_cmd_ok(&repo_path, &["new", "-m", name]);
test_env.jj_cmd_ok(&repo_path, &["branch", "create", name]);
}
// Default revset should be overridden if one or more -r options are specified.
test_env.add_config(r#"revsets.log = "root()""#);
insta::assert_snapshot!(
test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches", "-rfoo"]),
@r###"
◉ foo
~
"###);
insta::assert_snapshot!(
test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches", "-rfoo", "-rbar", "-rbaz"]),
@r###"
@ baz
◉ bar
◉ foo
~
"###);
insta::assert_snapshot!(
test_env.jj_cmd_success(&repo_path, &["log", "-T", "branches", "-rfoo", "-rfoo"]),
@r###"
◉ foo
~
"###);
}
#[test]
fn test_graph_template_color() {
// Test that color codes from a multi-line template don't span the graph lines.
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
test_env.jj_cmd_ok(
&repo_path,
&["describe", "-m", "first line\nsecond line\nthird line"],
);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "single line"]);
test_env.add_config(
r#"[colors]
description = "red"
"working_copy description" = "green"
"#,
);
// First test without color for comparison
let template = r#"label(if(current_working_copy, "working_copy"), description)"#;
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T", template]);
insta::assert_snapshot!(stdout, @r###"
@ single line
◉ first line
│ second line
│ third line
"###);
let stdout = test_env.jj_cmd_success(&repo_path, &["--color=always", "log", "-T", template]);
insta::assert_snapshot!(stdout, @r###"
@ single line
first line
second line
third line
"###);
}
#[test]
fn test_graph_styles() {
// Test that different graph styles are available.
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "initial"]);
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "main branch 1"]);
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "main branch 2"]);
test_env.jj_cmd_ok(
&repo_path,
&["new", "-m", "side branch\nwith\nlong\ndescription"],
);
test_env.jj_cmd_ok(
&repo_path,
&["new", "-m", "merge", r#"description("main branch 1")"#, "@"],
);
// Default (curved) style
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T=description"]);
insta::assert_snapshot!(stdout, @r###"
@ merge
├─╮
│ ◉ side branch
│ │ with
│ │ long
│ │ description
│ ◉ main branch 2
├─╯
◉ main branch 1
◉ initial
"###);
// ASCII style
test_env.add_config(r#"ui.graph.style = "ascii""#);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T=description"]);
insta::assert_snapshot!(stdout, @r###"
@ merge
|\
| o side branch
| | with
| | long
| | description
| o main branch 2
|/
o main branch 1
o initial
o
"###);
// Large ASCII style
test_env.add_config(r#"ui.graph.style = "ascii-large""#);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T=description"]);
insta::assert_snapshot!(stdout, @r###"
@ merge
|\
| \
| o side branch
| | with
| | long
| | description
| o main branch 2
| /
|/
o main branch 1
o initial
o
"###);
// Curved style
test_env.add_config(r#"ui.graph.style = "curved""#);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T=description"]);
insta::assert_snapshot!(stdout, @r###"
@ merge
├─╮
│ ◉ side branch
│ │ with
│ │ long
│ │ description
│ ◉ main branch 2
├─╯
◉ main branch 1
◉ initial
"###);
// Square style
test_env.add_config(r#"ui.graph.style = "square""#);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-T=description"]);
insta::assert_snapshot!(stdout, @r###"
@ merge
├─┐
│ ◉ side branch
│ │ with
│ │ long
│ │ description
│ ◉ main branch 2
├─┘
◉ main branch 1
◉ initial
"###);
}
#[test]
fn test_log_word_wrap() {
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
let render = |args: &[&str], columns: u32, word_wrap: bool| {
let mut args = args.to_vec();
if word_wrap {
args.push("--config-toml=ui.log-word-wrap=true");
}
let assert = test_env
.jj_cmd(&repo_path, &args)
.env("COLUMNS", columns.to_string())
.assert()
.success()
.stderr("");
get_stdout_string(&assert)
};
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "main branch 1"]);
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "main branch 2"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "side"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "merge", "@--", "@"]);
// ui.log-word-wrap option applies to both graph/no-graph outputs
insta::assert_snapshot!(render(&["log", "-r@"], 40, false), @r###"
@ mzvwutvl test.user@example.com 2001-02-03 08:05:11 68518a7e
│ (empty) merge
~
"###);
insta::assert_snapshot!(render(&["log", "-r@"], 40, true), @r###"
@ mzvwutvl test.user@example.com
│ 2001-02-03 08:05:11 68518a7e
~ (empty) merge
"###);
insta::assert_snapshot!(render(&["log", "--no-graph", "-r@"], 40, false), @r###"
mzvwutvl test.user@example.com 2001-02-03 08:05:11 68518a7e
(empty) merge
"###);
insta::assert_snapshot!(render(&["log", "--no-graph", "-r@"], 40, true), @r###"
mzvwutvl test.user@example.com
2001-02-03 08:05:11 68518a7e
(empty) merge
"###);
// Color labels should be preserved
insta::assert_snapshot!(render(&["log", "-r@", "--color=always"], 40, true), @r###"
@ mzvwutvl test.user@example.com
2001-02-03 08:05:11 68518a7e
~ (empty) merge
"###);
// Graph width should be subtracted from the term width
let template = r#""0 1 2 3 4 5 6 7 8 9""#;
insta::assert_snapshot!(render(&["log", "-T", template], 10, true), @r###"
@ 0 1 2
├─╮ 3 4 5
│ │ 6 7 8
│ │ 9
│ ◉ 0 1 2
│ │ 3 4 5
│ │ 6 7 8
│ │ 9
│ ◉ 0 1 2
├─╯ 3 4 5
│ 6 7 8
│ 9
◉ 0 1 2 3
│ 4 5 6 7
│ 8 9
◉ 0 1 2 3
4 5 6 7
8 9
"###);
// Shouldn't panic with $COLUMNS < graph_width
insta::assert_snapshot!(render(&["log", "-r@"], 0, true), @r###"
@ mzvwutvl
│ test.user@example.com
~ 2001-02-03
08:05:11
68518a7e
(empty)
merge
"###);
insta::assert_snapshot!(render(&["log", "-r@"], 1, true), @r###"
@ mzvwutvl
│ test.user@example.com
~ 2001-02-03
08:05:11
68518a7e
(empty)
merge
"###);
}
#[test]
fn test_elided() {
// Test that elided commits are shown as synthetic nodes.
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "initial"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "main branch 1"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "main branch 2"]);
test_env.jj_cmd_ok(&repo_path, &["new", "@--", "-m", "side branch 1"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "side branch 2"]);
test_env.jj_cmd_ok(
&repo_path,
&["new", "-m", "merge", r#"description("main branch 2")"#, "@"],
);
let get_log = |revs: &str| -> String {
test_env.jj_cmd_success(
&repo_path,
&["log", "-T", r#"description ++ "\n""#, "-r", revs],
)
};
// Test the setup
insta::assert_snapshot!(get_log("::"), @r###"
@ merge
├─╮
│ ◉ side branch 2
│ │
│ ◉ side branch 1
│ │
◉ │ main branch 2
│ │
◉ │ main branch 1
├─╯
◉ initial
"###);
// Elide some commits from each side of the merge. It's unclear that a revision
// was skipped on the left side.
test_env.add_config("ui.log-synthetic-elided-nodes = false");
insta::assert_snapshot!(get_log("@ | @- | description(initial)"), @r###"
@ merge
├─╮
│ ◉ side branch 2
│ ╷
◉ ╷ main branch 2
├─╯
◉ initial
~
"###);
// Elide shared commits. It's unclear that a revision was skipped on the right
// side (#1252).
insta::assert_snapshot!(get_log("@-- | root()"), @r###"
◉ side branch 1
╷ ◉ main branch 1
╭─╯
"###);
// Now test the same thing with synthetic nodes for elided commits
// Elide some commits from each side of the merge
test_env.add_config("ui.log-synthetic-elided-nodes = true");
insta::assert_snapshot!(get_log("@ | @- | description(initial)"), @r###"
@ merge
├─╮
│ ◉ side branch 2
│ │
│ ◌ (elided revisions)
◉ │ main branch 2
│ │
◌ │ (elided revisions)
├─╯
◉ initial
~
"###);
// Elide shared commits. To keep the implementation simple, it still gets
// rendered as two synthetic nodes.
insta::assert_snapshot!(get_log("@-- | root()"), @r###"
◉ side branch 1
◌ (elided revisions)
│ ◉ main branch 1
│ │
│ ◌ (elided revisions)
├─╯
"###);
}
#[test]
fn test_log_with_custom_symbols() {
// Test that elided commits are shown as synthetic nodes.
let test_env = TestEnvironment::default();
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "initial"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "main branch 1"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "main branch 2"]);
test_env.jj_cmd_ok(&repo_path, &["new", "@--", "-m", "side branch 1"]);
test_env.jj_cmd_ok(&repo_path, &["new", "-m", "side branch 2"]);
test_env.jj_cmd_ok(
&repo_path,
&["new", "-m", "merge", r#"description("main branch 2")"#, "@"],
);
let get_log = |revs: &str| -> String {
test_env.jj_cmd_success(
&repo_path,
&["log", "-T", r#"description ++ "\n""#, "-r", revs],
)
};
// Simple test with showing default and elided nodes.
test_env.add_config(
r###"
ui.log-synthetic-elided-nodes = true
templates.log_node = 'if(self, if(current_working_copy, "$", if(root, "┴", "┝")), "🮀")'
"###,
);
insta::assert_snapshot!(get_log("@ | @- | description(initial) | root()"), @r###"
$ merge
├─╮
│ ┝ side branch 2
│ │
│ 🮀 (elided revisions)
┝ │ main branch 2
│ │
🮀 │ (elided revisions)
├─╯
┝ initial
"###);
// Simple test with showing default and elided nodes, ascii style.
test_env.add_config(
r###"
ui.log-synthetic-elided-nodes = true
ui.graph.style = 'ascii'
templates.log_node = 'if(self, if(current_working_copy, "$", if(root, "^", "*")), ":")'
"###,
);
insta::assert_snapshot!(get_log("@ | @- | description(initial) | root()"), @r###"
$ merge
|\
| * side branch 2
| |
| : (elided revisions)
* | main branch 2
| |
: | (elided revisions)
|/
* initial
|
^
"###);
}