jj/cli/tests/test_commit_template.rs
Siva Mahadevan 24cd079307 templates: create new git_format_patch_email_headers template
With this template, a 'git format-patch' compatible
email message can be generated using something like:

jj show --git --template git_format_patch_email_headers <rev>
2025-04-02 13:16:47 +00:00

1658 lines
63 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 2023 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 indoc::indoc;
use regex::Regex;
use testutils::git;
use crate::common::TestEnvironment;
#[test]
fn test_log_parents() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["new"]).success();
work_dir.run_jj(["new", "@-"]).success();
work_dir.run_jj(["new", "@", "@-"]).success();
let template =
r#"commit_id ++ "\nP: " ++ parents.len() ++ " " ++ parents.map(|c| c.commit_id()) ++ "\n""#;
let output = work_dir.run_jj(["log", "-T", template]);
insta::assert_snapshot!(output, @r"
@ c067170d4ca1bc6162b64f7550617ec809647f84
├─╮ P: 2 4db490c88528133d579540b6900b8098f0c17701 230dd059e1b059aefc0da06a2e5a7dbf22362f22
○ │ 4db490c88528133d579540b6900b8098f0c17701
├─╯ P: 1 230dd059e1b059aefc0da06a2e5a7dbf22362f22
○ 230dd059e1b059aefc0da06a2e5a7dbf22362f22
│ P: 1 0000000000000000000000000000000000000000
◆ 0000000000000000000000000000000000000000
P: 0
[EOF]
");
// List<Commit> can be filtered
let template =
r#""P: " ++ parents.filter(|c| !c.root()).map(|c| c.commit_id().short()) ++ "\n""#;
let output = work_dir.run_jj(["log", "-T", template]);
insta::assert_snapshot!(output, @r"
@ P: 4db490c88528 230dd059e1b0
├─╮
○ │ P: 230dd059e1b0
├─╯
○ P:
◆ P:
[EOF]
");
let template = r#"parents.map(|c| c.commit_id().shortest(4))"#;
let output = work_dir.run_jj(["log", "-T", template, "-r@", "--color=always"]);
insta::assert_snapshot!(output, @r"
@ 4db4 230d
~
[EOF]
");
// Commit object isn't printable
let output = work_dir.run_jj(["log", "-T", "parents"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Error: Failed to parse template: Expected expression of type `Template`, but actual type is `List<Commit>`
Caused by: --> 1:1
|
1 | parents
| ^-----^
|
= Expected expression of type `Template`, but actual type is `List<Commit>`
[EOF]
[exit status: 1]
");
// Redundant argument passed to keyword method
let template = r#"parents.map(|c| c.commit_id(""))"#;
let output = work_dir.run_jj(["log", "-T", template]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Failed to parse template: Function `commit_id`: Expected 0 arguments
Caused by: --> 1:29
|
1 | parents.map(|c| c.commit_id(""))
| ^^
|
= Function `commit_id`: Expected 0 arguments
[EOF]
[exit status: 1]
"#);
}
#[test]
fn test_log_author_timestamp() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["describe", "-m", "first"]).success();
work_dir.run_jj(["new", "-m", "second"]).success();
let output = work_dir.run_jj(["log", "-T", "author.timestamp()"]);
insta::assert_snapshot!(output, @r"
@ 2001-02-03 04:05:09.000 +07:00
○ 2001-02-03 04:05:08.000 +07:00
◆ 1970-01-01 00:00:00.000 +00:00
[EOF]
");
}
#[test]
fn test_log_author_timestamp_ago() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["describe", "-m", "first"]).success();
work_dir.run_jj(["new", "-m", "second"]).success();
let template = r#"author.timestamp().ago() ++ "\n""#;
let output = work_dir
.run_jj(&["log", "--no-graph", "-T", template])
.success();
let line_re = Regex::new(r"[0-9]+ years ago").unwrap();
assert!(
output.stdout.raw().lines().all(|x| line_re.is_match(x)),
"expected every line to match regex"
);
}
#[test]
fn test_log_author_timestamp_utc() {
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(["log", "-T", "author.timestamp().utc()"]);
insta::assert_snapshot!(output, @r"
@ 2001-02-02 21:05:07.000 +00:00
◆ 1970-01-01 00:00:00.000 +00:00
[EOF]
");
}
#[cfg(unix)]
#[test]
fn test_log_author_timestamp_local() {
let mut test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
test_env.add_env_var("TZ", "UTC-05:30");
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["log", "-T", "author.timestamp().local()"]);
insta::assert_snapshot!(output, @r"
@ 2001-02-03 08:05:07.000 +11:00
◆ 1970-01-01 11:00:00.000 +11:00
[EOF]
");
test_env.add_env_var("TZ", "UTC+10:00");
let work_dir = test_env.work_dir("repo");
let output = work_dir.run_jj(["log", "-T", "author.timestamp().local()"]);
insta::assert_snapshot!(output, @r"
@ 2001-02-03 08:05:07.000 +11:00
◆ 1970-01-01 11:00:00.000 +11:00
[EOF]
");
}
#[test]
fn test_log_author_timestamp_after_before() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["describe", "-m", "first"]).success();
let template = r#"
separate(" ",
author.timestamp(),
":",
if(author.timestamp().after("1969"), "(after 1969)", "(before 1969)"),
if(author.timestamp().before("1975"), "(before 1975)", "(after 1975)"),
if(author.timestamp().before("now"), "(before now)", "(after now)")
) ++ "\n""#;
let output = work_dir.run_jj(["log", "--no-graph", "-T", template]);
insta::assert_snapshot!(output, @r"
2001-02-03 04:05:08.000 +07:00 : (after 1969) (after 1975) (before now)
1970-01-01 00:00:00.000 +00:00 : (after 1969) (before 1975) (before now)
[EOF]
");
// Should display error with invalid date.
let template = r#"author.timestamp().after("invalid date")"#;
let output = work_dir.run_jj(["log", "-r@", "--no-graph", "-T", template]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Failed to parse template: Invalid date pattern
Caused by:
1: --> 1:26
|
1 | author.timestamp().after("invalid date")
| ^------------^
|
= Invalid date pattern
2: expected unsupported identifier as position 0..7
[EOF]
[exit status: 1]
"#);
}
#[test]
fn test_mine_is_true_when_author_is_user() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir
.run_jj([
"--config=user.email=johndoe@example.com",
"--config=user.name=John Doe",
"new",
])
.success();
let output = work_dir.run_jj([
"log",
"-T",
r#"coalesce(if(mine, "mine"), author.email(), email_placeholder)"#,
]);
insta::assert_snapshot!(output, @r"
@ johndoe@example.com
○ mine
◆ (no email set)
[EOF]
");
}
#[test]
fn test_log_default() {
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("file1", "foo\n");
work_dir.run_jj(["describe", "-m", "add a file"]).success();
work_dir.run_jj(["new", "-m", "description 1"]).success();
work_dir
.run_jj(["bookmark", "create", "-r@", "my-bookmark"])
.success();
// Test default log output format
let output = work_dir.run_jj(["log"]);
insta::assert_snapshot!(output, @r"
@ kkmpptxz test.user@example.com 2001-02-03 08:05:09 my-bookmark bac9ff9e
│ (empty) description 1
○ qpvuntsm test.user@example.com 2001-02-03 08:05:08 aa2015d7
│ add a file
◆ zzzzzzzz root() 00000000
[EOF]
");
// Color
let output = work_dir.run_jj(["log", "--color=always"]);
insta::assert_snapshot!(output, @r"
@ kkmpptxz test.user@example.com 2001-02-03 08:05:09 my-bookmark bac9ff9e
(empty) description 1
qpvuntsm test.user@example.com 2001-02-03 08:05:08 aa2015d7
│ add a file
◆ zzzzzzzz root() 00000000
[EOF]
");
// Color without graph
let output = work_dir.run_jj(["log", "--color=always", "--no-graph"]);
insta::assert_snapshot!(output, @r"
kkmpptxz test.user@example.com 2001-02-03 08:05:09 my-bookmark bac9ff9e
(empty) description 1
qpvuntsm test.user@example.com 2001-02-03 08:05:08 aa2015d7
add a file
zzzzzzzz root() 00000000
[EOF]
");
}
#[test]
fn test_log_default_without_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.run_jj(["workspace", "forget"]).success();
let output = work_dir.run_jj(["log"]);
insta::assert_snapshot!(output, @r"
◆ zzzzzzzz root() 00000000
[EOF]
");
}
#[test]
fn test_log_builtin_templates() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
// Render without graph to test line ending
let render = |template| work_dir.run_jj(["log", "-T", template, "--no-graph"]);
work_dir
.run_jj(["--config=user.email=''", "--config=user.name=''", "new"])
.success();
work_dir
.run_jj(["bookmark", "create", "-r@", "my-bookmark"])
.success();
insta::assert_snapshot!(render(r#"builtin_log_oneline"#), @r"
rlvkpnrz (no email set) 2001-02-03 08:05:08 my-bookmark dc315397 (empty) (no description set)
qpvuntsm test.user 2001-02-03 08:05:07 230dd059 (empty) (no description set)
zzzzzzzz root() 00000000
[EOF]
");
insta::assert_snapshot!(render(r#"builtin_log_compact"#), @r"
rlvkpnrz (no email set) 2001-02-03 08:05:08 my-bookmark dc315397
(empty) (no description set)
qpvuntsm test.user@example.com 2001-02-03 08:05:07 230dd059
(empty) (no description set)
zzzzzzzz root() 00000000
[EOF]
");
insta::assert_snapshot!(render(r#"builtin_log_comfortable"#), @r"
rlvkpnrz (no email set) 2001-02-03 08:05:08 my-bookmark dc315397
(empty) (no description set)
qpvuntsm test.user@example.com 2001-02-03 08:05:07 230dd059
(empty) (no description set)
zzzzzzzz root() 00000000
[EOF]
");
insta::assert_snapshot!(render(r#"builtin_log_detailed"#), @r"
Commit ID: dc31539712c7294d1d712cec63cef4504b94ca74
Change ID: rlvkpnrzqnoowoytxnquwvuryrwnrmlp
Bookmarks: my-bookmark
Author : (no name set) <(no email set)> (2001-02-03 08:05:08)
Committer: (no name set) <(no email set)> (2001-02-03 08:05:08)
(no description set)
Commit ID: 230dd059e1b059aefc0da06a2e5a7dbf22362f22
Change ID: qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu
Author : Test User <test.user@example.com> (2001-02-03 08:05:07)
Committer: Test User <test.user@example.com> (2001-02-03 08:05:07)
(no description set)
Commit ID: 0000000000000000000000000000000000000000
Change ID: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
Author : (no name set) <(no email set)> (1970-01-01 11:00:00)
Committer: (no name set) <(no email set)> (1970-01-01 11:00:00)
(no description set)
[EOF]
");
}
#[test]
fn test_log_builtin_templates_colored() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let render = |template| work_dir.run_jj(["--color=always", "log", "-T", template]);
work_dir
.run_jj(["--config=user.email=''", "--config=user.name=''", "new"])
.success();
work_dir
.run_jj(["bookmark", "create", "-r@", "my-bookmark"])
.success();
insta::assert_snapshot!(render(r#"builtin_log_oneline"#), @r"
@ rlvkpnrz (no email set) 2001-02-03 08:05:08 my-bookmark dc315397 (empty) (no description set)
qpvuntsm test.user 2001-02-03 08:05:07 230dd059 (empty) (no description set)
◆ zzzzzzzz root() 00000000
[EOF]
");
insta::assert_snapshot!(render(r#"builtin_log_compact"#), @r"
@ rlvkpnrz (no email set) 2001-02-03 08:05:08 my-bookmark dc315397
(empty) (no description set)
qpvuntsm test.user@example.com 2001-02-03 08:05:07 230dd059
(empty) (no description set)
◆ zzzzzzzz root() 00000000
[EOF]
");
insta::assert_snapshot!(render(r#"builtin_log_comfortable"#), @r"
@ rlvkpnrz (no email set) 2001-02-03 08:05:08 my-bookmark dc315397
(empty) (no description set)
qpvuntsm test.user@example.com 2001-02-03 08:05:07 230dd059
(empty) (no description set)
◆ zzzzzzzz root() 00000000
[EOF]
");
insta::assert_snapshot!(render(r#"builtin_log_detailed"#), @r"
@ Commit ID: dc31539712c7294d1d712cec63cef4504b94ca74
│ Change ID: rlvkpnrzqnoowoytxnquwvuryrwnrmlp
│ Bookmarks: my-bookmark
│ Author : (no name set) <(no email set)> (2001-02-03 08:05:08)
│ Committer: (no name set) <(no email set)> (2001-02-03 08:05:08)
 (no description set)
○ Commit ID: 230dd059e1b059aefc0da06a2e5a7dbf22362f22
│ Change ID: qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu
│ Author : Test User <test.user@example.com> (2001-02-03 08:05:07)
│ Committer: Test User <test.user@example.com> (2001-02-03 08:05:07)
 (no description set)
◆ Commit ID: 0000000000000000000000000000000000000000
Change ID: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
Author : (no name set) <(no email set)> (1970-01-01 11:00:00)
Committer: (no name set) <(no email set)> (1970-01-01 11:00:00)
 (no description set)
[EOF]
");
}
#[test]
fn test_log_builtin_templates_colored_debug() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
let render = |template| work_dir.run_jj(["--color=debug", "log", "-T", template]);
work_dir
.run_jj(["--config=user.email=''", "--config=user.name=''", "new"])
.success();
work_dir
.run_jj(["bookmark", "create", "-r@", "my-bookmark"])
.success();
insta::assert_snapshot!(render(r#"builtin_log_oneline"#), @r"
<<node working_copy::@>> <<log working_copy change_id shortest prefix::r>><<log working_copy change_id shortest rest::lvkpnrz>><<log working_copy:: >><<log working_copy email placeholder::(no email set)>><<log working_copy:: >><<log working_copy committer timestamp local format::2001-02-03 08:05:08>><<log working_copy:: >><<log working_copy bookmarks name::my-bookmark>><<log working_copy:: >><<log working_copy commit_id shortest prefix::d>><<log working_copy commit_id shortest rest::c315397>><<log working_copy:: >><<log working_copy empty::(empty)>><<log working_copy:: >><<log working_copy empty description placeholder::(no description set)>><<log working_copy::>>
<<node::○>> <<log change_id shortest prefix::q>><<log change_id shortest rest::pvuntsm>><<log:: >><<log author email local::test.user>><<log:: >><<log committer timestamp local format::2001-02-03 08:05:07>><<log:: >><<log commit_id shortest prefix::2>><<log commit_id shortest rest::30dd059>><<log:: >><<log empty::(empty)>><<log:: >><<log empty description placeholder::(no description set)>><<log::>>
<<node immutable::◆>> <<log change_id shortest prefix::z>><<log change_id shortest rest::zzzzzzz>><<log:: >><<log root::root()>><<log:: >><<log commit_id shortest prefix::0>><<log commit_id shortest rest::0000000>><<log::>>
[EOF]
");
insta::assert_snapshot!(render(r#"builtin_log_compact"#), @r"
<<node working_copy::@>> <<log working_copy change_id shortest prefix::r>><<log working_copy change_id shortest rest::lvkpnrz>><<log working_copy:: >><<log working_copy email placeholder::(no email set)>><<log working_copy:: >><<log working_copy committer timestamp local format::2001-02-03 08:05:08>><<log working_copy:: >><<log working_copy bookmarks name::my-bookmark>><<log working_copy:: >><<log working_copy commit_id shortest prefix::d>><<log working_copy commit_id shortest rest::c315397>><<log working_copy::>>
<<log working_copy empty::(empty)>><<log working_copy:: >><<log working_copy empty description placeholder::(no description set)>><<log working_copy::>>
<<node::○>> <<log change_id shortest prefix::q>><<log change_id shortest rest::pvuntsm>><<log:: >><<log author email local::test.user>><<log author email::@>><<log author email domain::example.com>><<log:: >><<log committer timestamp local format::2001-02-03 08:05:07>><<log:: >><<log commit_id shortest prefix::2>><<log commit_id shortest rest::30dd059>><<log::>>
<<log empty::(empty)>><<log:: >><<log empty description placeholder::(no description set)>><<log::>>
<<node immutable::◆>> <<log change_id shortest prefix::z>><<log change_id shortest rest::zzzzzzz>><<log:: >><<log root::root()>><<log:: >><<log commit_id shortest prefix::0>><<log commit_id shortest rest::0000000>><<log::>>
[EOF]
");
insta::assert_snapshot!(render(r#"builtin_log_comfortable"#), @r"
<<node working_copy::@>> <<log working_copy change_id shortest prefix::r>><<log working_copy change_id shortest rest::lvkpnrz>><<log working_copy:: >><<log working_copy email placeholder::(no email set)>><<log working_copy:: >><<log working_copy committer timestamp local format::2001-02-03 08:05:08>><<log working_copy:: >><<log working_copy bookmarks name::my-bookmark>><<log working_copy:: >><<log working_copy commit_id shortest prefix::d>><<log working_copy commit_id shortest rest::c315397>><<log working_copy::>>
<<log working_copy empty::(empty)>><<log working_copy:: >><<log working_copy empty description placeholder::(no description set)>><<log working_copy::>>
│ <<log::>>
<<node::○>> <<log change_id shortest prefix::q>><<log change_id shortest rest::pvuntsm>><<log:: >><<log author email local::test.user>><<log author email::@>><<log author email domain::example.com>><<log:: >><<log committer timestamp local format::2001-02-03 08:05:07>><<log:: >><<log commit_id shortest prefix::2>><<log commit_id shortest rest::30dd059>><<log::>>
<<log empty::(empty)>><<log:: >><<log empty description placeholder::(no description set)>><<log::>>
│ <<log::>>
<<node immutable::◆>> <<log change_id shortest prefix::z>><<log change_id shortest rest::zzzzzzz>><<log:: >><<log root::root()>><<log:: >><<log commit_id shortest prefix::0>><<log commit_id shortest rest::0000000>><<log::>>
<<log::>>
[EOF]
");
insta::assert_snapshot!(render(r#"builtin_log_detailed"#), @r"
<<node working_copy::@>> <<log::Commit ID: >><<log commit_id::dc31539712c7294d1d712cec63cef4504b94ca74>><<log::>>
│ <<log::Change ID: >><<log change_id::rlvkpnrzqnoowoytxnquwvuryrwnrmlp>><<log::>>
│ <<log::Bookmarks: >><<log local_bookmarks name::my-bookmark>><<log::>>
│ <<log::Author : >><<log name placeholder::(no name set)>><<log:: <>><<log email placeholder::(no email set)>><<log::> (>><<log author timestamp local format::2001-02-03 08:05:08>><<log::)>>
│ <<log::Committer: >><<log name placeholder::(no name set)>><<log:: <>><<log email placeholder::(no email set)>><<log::> (>><<log committer timestamp local format::2001-02-03 08:05:08>><<log::)>>
│ <<log::>>
<<log empty description placeholder:: (no description set)>><<log::>>
│ <<log::>>
<<node::○>> <<log::Commit ID: >><<log commit_id::230dd059e1b059aefc0da06a2e5a7dbf22362f22>><<log::>>
│ <<log::Change ID: >><<log change_id::qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu>><<log::>>
│ <<log::Author : >><<log author name::Test User>><<log:: <>><<log author email local::test.user>><<log author email::@>><<log author email domain::example.com>><<log::> (>><<log author timestamp local format::2001-02-03 08:05:07>><<log::)>>
│ <<log::Committer: >><<log committer name::Test User>><<log:: <>><<log committer email local::test.user>><<log committer email::@>><<log committer email domain::example.com>><<log::> (>><<log committer timestamp local format::2001-02-03 08:05:07>><<log::)>>
│ <<log::>>
<<log empty description placeholder:: (no description set)>><<log::>>
│ <<log::>>
<<node immutable::◆>> <<log::Commit ID: >><<log commit_id::0000000000000000000000000000000000000000>><<log::>>
<<log::Change ID: >><<log change_id::zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz>><<log::>>
<<log::Author : >><<log name placeholder::(no name set)>><<log:: <>><<log email placeholder::(no email set)>><<log::> (>><<log author timestamp local format::1970-01-01 11:00:00>><<log::)>>
<<log::Committer: >><<log name placeholder::(no name set)>><<log:: <>><<log email placeholder::(no email set)>><<log::> (>><<log committer timestamp local format::1970-01-01 11:00:00>><<log::)>>
<<log::>>
<<log empty description placeholder:: (no description set)>><<log::>>
<<log::>>
[EOF]
");
}
#[test]
fn test_log_evolog_divergence() {
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", "foo\n");
work_dir
.run_jj(["describe", "-m", "description 1"])
.success();
// No divergence
let output = work_dir.run_jj(["log"]);
insta::assert_snapshot!(output, @r"
@ qpvuntsm test.user@example.com 2001-02-03 08:05:08 ff309c29
│ description 1
◆ zzzzzzzz root() 00000000
[EOF]
");
// Create divergence
work_dir
.run_jj(["describe", "-m", "description 2", "--at-operation", "@-"])
.success();
let output = work_dir.run_jj(["log"]);
insta::assert_snapshot!(output, @r"
@ qpvuntsm?? test.user@example.com 2001-02-03 08:05:08 ff309c29
│ description 1
│ ○ qpvuntsm?? test.user@example.com 2001-02-03 08:05:10 6ba70e00
├─╯ description 2
◆ zzzzzzzz root() 00000000
[EOF]
------- stderr -------
Concurrent modification detected, resolving automatically.
[EOF]
");
// Color
let output = work_dir.run_jj(["log", "--color=always"]);
insta::assert_snapshot!(output, @r"
@ qpvuntsm?? test.user@example.com 2001-02-03 08:05:08 ff309c29
description 1
│ ○ qpvuntsm?? test.user@example.com 2001-02-03 08:05:10 6ba70e00
├─╯ description 2
◆ zzzzzzzz root() 00000000
[EOF]
");
// Evolog and hidden divergent
let output = work_dir.run_jj(["evolog"]);
insta::assert_snapshot!(output, @r"
@ qpvuntsm?? test.user@example.com 2001-02-03 08:05:08 ff309c29
│ description 1
○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 485d52a9
│ (no description set)
○ qpvuntsm hidden test.user@example.com 2001-02-03 08:05:07 230dd059
(empty) (no description set)
[EOF]
");
// Colored evolog
let output = work_dir.run_jj(["evolog", "--color=always"]);
insta::assert_snapshot!(output, @r"
@ qpvuntsm?? test.user@example.com 2001-02-03 08:05:08 ff309c29
description 1
qpvuntsm hidden test.user@example.com 2001-02-03 08:05:08 485d52a9
(no description set)
qpvuntsm hidden test.user@example.com 2001-02-03 08:05:07 230dd059
(empty) (no description set)
[EOF]
");
}
#[test]
fn test_log_bookmarks() {
let test_env = TestEnvironment::default();
test_env.add_config("git.auto-local-bookmark = true");
test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#);
test_env.run_jj_in(".", ["git", "init", "origin"]).success();
let origin_dir = test_env.work_dir("origin");
let origin_git_repo_path = origin_dir
.root()
.join(".jj")
.join("repo")
.join("store")
.join("git");
// Created some bookmarks on the remote
origin_dir
.run_jj(["describe", "-m=description 1"])
.success();
origin_dir
.run_jj(["bookmark", "create", "-r@", "bookmark1"])
.success();
origin_dir
.run_jj(["new", "root()", "-m=description 2"])
.success();
origin_dir
.run_jj(["bookmark", "create", "-r@", "bookmark2", "unchanged"])
.success();
origin_dir
.run_jj(["new", "root()", "-m=description 3"])
.success();
origin_dir
.run_jj(["bookmark", "create", "-r@", "bookmark3"])
.success();
origin_dir.run_jj(["git", "export"]).success();
test_env
.run_jj_in(
".",
[
"git",
"clone",
origin_git_repo_path.to_str().unwrap(),
"local",
],
)
.success();
let work_dir = test_env.work_dir("local");
// Rewrite bookmark1, move bookmark2 forward, create conflict in bookmark3, add
// new-bookmark
work_dir
.run_jj(["describe", "bookmark1", "-m", "modified bookmark1 commit"])
.success();
work_dir.run_jj(["new", "bookmark2"]).success();
work_dir
.run_jj(["bookmark", "set", "bookmark2", "--to=@"])
.success();
work_dir
.run_jj(["bookmark", "create", "-r@", "new-bookmark"])
.success();
work_dir
.run_jj(["describe", "bookmark3", "-m=local"])
.success();
origin_dir
.run_jj(["describe", "bookmark3", "-m=origin"])
.success();
origin_dir.run_jj(["git", "export"]).success();
work_dir.run_jj(["git", "fetch"]).success();
let template = r#"commit_id.short() ++ " " ++ if(bookmarks, bookmarks, "(no bookmarks)")"#;
let output = work_dir.run_jj(["log", "-T", template]);
insta::assert_snapshot!(output, @r"
@ a5b4d15489cc bookmark2* new-bookmark
○ 8476341eb395 bookmark2@origin unchanged
│ ○ fed794e2ba44 bookmark3?? bookmark3@origin
├─╯
│ ○ b1bb3766d584 bookmark3??
├─╯
│ ○ 4a7e4246fc4d bookmark1*
├─╯
◆ 000000000000 (no bookmarks)
[EOF]
");
let template = r#"bookmarks.map(|b| separate("/", b.remote(), b.name())).join(", ")"#;
let output = work_dir.run_jj(["log", "-T", template]);
insta::assert_snapshot!(output, @r"
@ bookmark2, new-bookmark
○ origin/bookmark2, unchanged
│ ○ bookmark3, origin/bookmark3
├─╯
│ ○ bookmark3
├─╯
│ ○ bookmark1
├─╯
[EOF]
");
let template = r#"separate(" ", "L:", local_bookmarks, "R:", remote_bookmarks)"#;
let output = work_dir.run_jj(["log", "-T", template]);
insta::assert_snapshot!(output, @r"
@ L: bookmark2* new-bookmark R:
○ L: unchanged R: bookmark2@origin unchanged@origin
│ ○ L: bookmark3?? R: bookmark3@origin
├─╯
│ ○ L: bookmark3?? R:
├─╯
│ ○ L: bookmark1* R:
├─╯
◆ L: R:
[EOF]
");
let template = r#"
remote_bookmarks.map(|ref| concat(
ref,
if(ref.tracked(),
"(+" ++ ref.tracking_ahead_count().lower()
++ "/-" ++ ref.tracking_behind_count().lower() ++ ")"),
))
"#;
let output = work_dir.run_jj(["log", "-r::remote_bookmarks()", "-T", template]);
insta::assert_snapshot!(output, @r"
○ bookmark3@origin(+0/-1)
│ ○ bookmark2@origin(+0/-1) unchanged@origin(+0/-0)
├─╯
│ ○ bookmark1@origin(+1/-1)
├─╯
[EOF]
");
}
#[test]
fn test_log_git_head() {
let test_env = TestEnvironment::default();
let work_dir = test_env.work_dir("repo");
git::init(work_dir.root());
work_dir.run_jj(["git", "init", "--git-repo=."]).success();
work_dir.run_jj(["new", "-m=initial"]).success();
work_dir.write_file("file", "foo\n");
let output = work_dir.run_jj(["log", "-T", "git_head"]);
insta::assert_snapshot!(output, @r"
@ false
○ true
◆ false
[EOF]
");
let output = work_dir.run_jj(["log", "--color=always"]);
insta::assert_snapshot!(output, @r"
@ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 50aaf475
initial
qpvuntsm test.user@example.com 2001-02-03 08:05:07 git_head() 230dd059
(empty) (no description set)
◆ zzzzzzzz root() 00000000
[EOF]
");
}
#[test]
fn test_log_commit_id_normal_hex() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["new", "-m", "first"]).success();
work_dir.run_jj(["new", "-m", "second"]).success();
let output = work_dir.run_jj([
"log",
"-T",
r#"commit_id ++ ": " ++ commit_id.normal_hex()"#,
]);
insta::assert_snapshot!(output, @r"
@ 6572f22267c6f0f2bf7b8a37969ee5a7d54b8aae: 6572f22267c6f0f2bf7b8a37969ee5a7d54b8aae
○ 222fa9f0b41347630a1371203b8aad3897d34e5f: 222fa9f0b41347630a1371203b8aad3897d34e5f
○ 230dd059e1b059aefc0da06a2e5a7dbf22362f22: 230dd059e1b059aefc0da06a2e5a7dbf22362f22
◆ 0000000000000000000000000000000000000000: 0000000000000000000000000000000000000000
[EOF]
");
}
#[test]
fn test_log_change_id_normal_hex() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["new", "-m", "first"]).success();
work_dir.run_jj(["new", "-m", "second"]).success();
let output = work_dir.run_jj([
"log",
"-T",
r#"change_id ++ ": " ++ change_id.normal_hex()"#,
]);
insta::assert_snapshot!(output, @r"
@ kkmpptxzrspxrzommnulwmwkkqwworpl: ffdaa62087a280bddc5e3d3ff933b8ae
○ rlvkpnrzqnoowoytxnquwvuryrwnrmlp: 8e4fac809cbb3b162c953458183c8dea
○ qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu: 9a45c67d3e96a7e5007c110ede34dec5
◆ zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz: 00000000000000000000000000000000
[EOF]
");
}
#[test]
fn test_log_customize_short_id() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["describe", "-m", "first"]).success();
// Customize both the commit and the change id
let decl = "template-aliases.'format_short_id(id)'";
let output = work_dir.run_jj([
"log",
"--config",
&format!(r#"{decl}='id.shortest(5).prefix().upper() ++ "_" ++ id.shortest(5).rest()'"#),
]);
insta::assert_snapshot!(output, @r"
@ Q_pvun test.user@example.com 2001-02-03 08:05:08 F_a156
│ (empty) first
◆ Z_zzzz root() 0_0000
[EOF]
");
// Customize only the change id
let output = work_dir.run_jj([
"log",
"--config=template-aliases.'format_short_change_id(id)'='format_short_id(id).upper()'",
]);
insta::assert_snapshot!(output, @r"
@ QPVUNTSM test.user@example.com 2001-02-03 08:05:08 fa15625b
│ (empty) first
◆ ZZZZZZZZ root() 00000000
[EOF]
");
}
#[test]
fn test_log_immutable() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["new", "-mA", "root()"]).success();
work_dir.run_jj(["new", "-mB"]).success();
work_dir
.run_jj(["bookmark", "create", "-r@", "main"])
.success();
work_dir.run_jj(["new", "-mC"]).success();
work_dir.run_jj(["new", "-mD", "root()"]).success();
let template = r#"
separate(" ",
description.first_line(),
bookmarks,
if(immutable, "[immutable]"),
) ++ "\n"
"#;
test_env.add_config("revset-aliases.'immutable_heads()' = 'main'");
let output = work_dir.run_jj(["log", "-r::", "-T", template]);
insta::assert_snapshot!(output, @r"
@ D
│ ○ C
│ ◆ B main [immutable]
│ ◆ A [immutable]
├─╯
◆ [immutable]
[EOF]
");
// Suppress error that could be detected earlier
test_env.add_config("revsets.short-prefixes = ''");
test_env.add_config("revset-aliases.'immutable_heads()' = 'unknown_fn()'");
let output = work_dir.run_jj(["log", "-r::", "-T", template]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Config error: Invalid `revset-aliases.immutable_heads()`
Caused by: --> 1:1
|
1 | unknown_fn()
| ^--------^
|
= Function `unknown_fn` doesn't exist
For help, see https://jj-vcs.github.io/jj/latest/config/ or use `jj help -k config`.
[EOF]
[exit status: 1]
");
test_env.add_config("revset-aliases.'immutable_heads()' = 'unknown_symbol'");
let output = work_dir.run_jj(["log", "-r::", "-T", template]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Failed to parse template: Failed to evaluate revset
Caused by:
1: --> 5:10
|
5 | if(immutable, "[immutable]"),
| ^-------^
|
= Failed to evaluate revset
2: Revision `unknown_symbol` doesn't exist
[EOF]
[exit status: 1]
"#);
}
#[test]
fn test_log_contained_in() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["new", "-mA", "root()"]).success();
work_dir.run_jj(["new", "-mB"]).success();
work_dir
.run_jj(["bookmark", "create", "-r@", "main"])
.success();
work_dir.run_jj(["new", "-mC"]).success();
work_dir.run_jj(["new", "-mD", "root()"]).success();
let template_for_revset = |revset: &str| {
format!(
r#"
separate(" ",
description.first_line(),
bookmarks,
if(self.contained_in("{revset}"), "[contained_in]"),
) ++ "\n"
"#
)
};
let output = work_dir.run_jj([
"log",
"-r::",
"-T",
&template_for_revset(r#"description(A)::"#),
]);
insta::assert_snapshot!(output, @r"
@ D
│ ○ C [contained_in]
│ ○ B main [contained_in]
│ ○ A [contained_in]
├─╯
[EOF]
");
let output = work_dir.run_jj([
"log",
"-r::",
"-T",
&template_for_revset(r#"visible_heads()"#),
]);
insta::assert_snapshot!(output, @r"
@ D [contained_in]
│ ○ C [contained_in]
│ ○ B main
│ ○ A
├─╯
[EOF]
");
// Suppress error that could be detected earlier
let output = work_dir.run_jj(["log", "-r::", "-T", &template_for_revset("unknown_fn()")]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Failed to parse template: In revset expression
Caused by:
1: --> 5:28
|
5 | if(self.contained_in("unknown_fn()"), "[contained_in]"),
| ^------------^
|
= In revset expression
2: --> 1:1
|
1 | unknown_fn()
| ^--------^
|
= Function `unknown_fn` doesn't exist
[EOF]
[exit status: 1]
"#);
let output = work_dir.run_jj(["log", "-r::", "-T", &template_for_revset("author(x:'y')")]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Failed to parse template: In revset expression
Caused by:
1: --> 5:28
|
5 | if(self.contained_in("author(x:'y')"), "[contained_in]"),
| ^-------------^
|
= In revset expression
2: --> 1:8
|
1 | author(x:'y')
| ^---^
|
= Invalid string pattern
3: Invalid string pattern kind `x:`
Hint: Try prefixing with one of `exact:`, `glob:`, `regex:`, `substring:`, or one of these with `-i` suffix added (e.g. `glob-i:`) for case-insensitive matching
[EOF]
[exit status: 1]
"#);
let output = work_dir.run_jj(["log", "-r::", "-T", &template_for_revset("maine")]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Failed to parse template: Failed to evaluate revset
Caused by:
1: --> 5:28
|
5 | if(self.contained_in("maine"), "[contained_in]"),
| ^-----^
|
= Failed to evaluate revset
2: Revision `maine` doesn't exist
Hint: Did you mean `main`?
[EOF]
[exit status: 1]
"#);
}
#[test]
fn test_short_prefix_in_transaction() {
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(r#"
[revsets]
log = '::description(test)'
[templates]
log = 'summary ++ "\n"'
commit_summary = 'summary'
[template-aliases]
'format_id(id)' = 'id.shortest(12).prefix() ++ "[" ++ id.shortest(12).rest() ++ "]"'
'summary' = 'separate(" ", format_id(change_id), format_id(commit_id), description.first_line())'
"#);
work_dir.write_file("file", "original file\n");
work_dir.run_jj(["describe", "-m", "initial"]).success();
// Create a chain of 5 commits
for i in 0..5 {
work_dir
.run_jj(["new", "-m", &format!("commit{i}")])
.success();
work_dir.write_file("file", format!("file {i}\n"));
}
// Create 2^4 duplicates of the chain
for _ in 0..4 {
work_dir
.run_jj(["duplicate", "description(commit)"])
.success();
}
// Short prefix should be used for commit summary inside the transaction
let parent_id = "58731d"; // Force id lookup to build index before mutation.
// If the cached index wasn't invalidated, the
// newly created commit wouldn't be found in it.
let output = work_dir.run_jj(["new", parent_id, "--no-edit", "-m", "test"]);
insta::assert_snapshot!(output, @r"
------- stderr -------
Created new commit km[kuslswpqwq] 7[4ac55dd119b] test
[EOF]
");
// Should match log's short prefixes
let output = work_dir.run_jj(["log", "--no-graph"]);
insta::assert_snapshot!(output, @r"
km[kuslswpqwq] 7[4ac55dd119b] test
y[qosqzytrlsw] 5[8731db5875e] commit4
r[oyxmykxtrkr] 9[95cc897bca7] commit3
m[zvwutvlkqwt] 3[74534c54448] commit2
zs[uskulnrvyr] d[e304c281bed] commit1
kk[mpptxzrspx] 05[2755155952] commit0
q[pvuntsmwlqt] e[0e22b9fae75] initial
zz[zzzzzzzzzz] 00[0000000000]
[EOF]
");
test_env.add_config(r#"revsets.short-prefixes = """#);
let output = work_dir.run_jj(["log", "--no-graph"]);
insta::assert_snapshot!(output, @r"
kmk[uslswpqwq] 74ac[55dd119b] test
yq[osqzytrlsw] 587[31db5875e] commit4
ro[yxmykxtrkr] 99[5cc897bca7] commit3
mz[vwutvlkqwt] 374[534c54448] commit2
zs[uskulnrvyr] de[304c281bed] commit1
kk[mpptxzrspx] 052[755155952] commit0
qp[vuntsmwlqt] e0[e22b9fae75] initial
zz[zzzzzzzzzz] 00[0000000000]
[EOF]
");
}
#[test]
fn test_log_diff_predefined_formats() {
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("file1", "a\nb\n");
work_dir.write_file("file2", "a\n");
work_dir.write_file("rename-source", "rename");
work_dir.run_jj(["new"]).success();
work_dir.write_file("file1", "a\nb\nc\n");
work_dir.write_file("file2", "b\nc\n");
std::fs::rename(
work_dir.root().join("rename-source"),
work_dir.root().join("rename-target"),
)
.unwrap();
let template = r#"
concat(
"=== color_words ===\n",
diff.color_words(),
"=== git ===\n",
diff.git(),
"=== stat ===\n",
diff.stat(80),
"=== summary ===\n",
diff.summary(),
)
"#;
// color, without paths
let output = work_dir.run_jj(["log", "--no-graph", "--color=always", "-r@", "-T", template]);
insta::assert_snapshot!(output, @r"
=== color_words ===
Modified regular file file1:
 1  1: a
 2  2: b
 3: c
Modified regular file file2:
 1  1: ab
 2: c
Modified regular file rename-target (rename-source => rename-target):
=== git ===
diff --git a/file1 b/file1
index 422c2b7ab3..de980441c3 100644
--- a/file1
+++ b/file1
@@ -1,2 +1,3 @@
a
b
+c
diff --git a/file2 b/file2
index 7898192261..9ddeb5c484 100644
--- a/file2
+++ b/file2
@@ -1,1 +1,2 @@
-a
+b
+c
diff --git a/rename-source b/rename-target
rename from rename-source
rename to rename-target
=== stat ===
file1 | 1 +
file2 | 3 ++-
{rename-source => rename-target} | 0
3 files changed, 3 insertions(+), 1 deletion(-)
=== summary ===
M file1
M file2
R {rename-source => rename-target}
[EOF]
");
// color labels
let output = work_dir.run_jj(["log", "--no-graph", "--color=debug", "-r@", "-T", template]);
insta::assert_snapshot!(output, @r"
<<log::=== color_words ===>>
<<log diff color_words header::Modified regular file file1:>>
<<log diff color_words removed line_number:: 1>><<log diff color_words:: >><<log diff color_words added line_number:: 1>><<log diff color_words::: a>>
<<log diff color_words removed line_number:: 2>><<log diff color_words:: >><<log diff color_words added line_number:: 2>><<log diff color_words::: b>>
<<log diff color_words:: >><<log diff color_words added line_number:: 3>><<log diff color_words::: >><<log diff color_words added token::c>>
<<log diff color_words header::Modified regular file file2:>>
<<log diff color_words removed line_number:: 1>><<log diff color_words:: >><<log diff color_words added line_number:: 1>><<log diff color_words::: >><<log diff color_words removed token::a>><<log diff color_words added token::b>><<log diff color_words::>>
<<log diff color_words:: >><<log diff color_words added line_number:: 2>><<log diff color_words::: >><<log diff color_words added token::c>>
<<log diff color_words header::Modified regular file rename-target (rename-source => rename-target):>>
<<log::=== git ===>>
<<log diff git file_header::diff --git a/file1 b/file1>>
<<log diff git file_header::index 422c2b7ab3..de980441c3 100644>>
<<log diff git file_header::--- a/file1>>
<<log diff git file_header::+++ b/file1>>
<<log diff git hunk_header::@@ -1,2 +1,3 @@>>
<<log diff git context:: a>>
<<log diff git context:: b>>
<<log diff git added::+>><<log diff git added token::c>>
<<log diff git file_header::diff --git a/file2 b/file2>>
<<log diff git file_header::index 7898192261..9ddeb5c484 100644>>
<<log diff git file_header::--- a/file2>>
<<log diff git file_header::+++ b/file2>>
<<log diff git hunk_header::@@ -1,1 +1,2 @@>>
<<log diff git removed::->><<log diff git removed token::a>><<log diff git removed::>>
<<log diff git added::+>><<log diff git added token::b>><<log diff git added::>>
<<log diff git added::+>><<log diff git added token::c>>
<<log diff git file_header::diff --git a/rename-source b/rename-target>>
<<log diff git file_header::rename from rename-source>>
<<log diff git file_header::rename to rename-target>>
<<log::=== stat ===>>
<<log diff stat::file1 | 1 >><<log diff stat added::+>><<log diff stat removed::>>
<<log diff stat::file2 | 3 >><<log diff stat added::++>><<log diff stat removed::->>
<<log diff stat::{rename-source => rename-target} | 0>><<log diff stat removed::>>
<<log diff stat stat-summary::3 files changed, 3 insertions(+), 1 deletion(-)>>
<<log::=== summary ===>>
<<log diff summary modified::M file1>>
<<log diff summary modified::M file2>>
<<log diff summary renamed::R {rename-source => rename-target}>>
[EOF]
");
// cwd != workspace root
let output = test_env.run_jj_in(".", ["log", "-Rrepo", "--no-graph", "-r@", "-T", template]);
insta::assert_snapshot!(output.normalize_backslash(), @r"
=== color_words ===
Modified regular file repo/file1:
1 1: a
2 2: b
3: c
Modified regular file repo/file2:
1 1: ab
2: c
Modified regular file repo/rename-target (repo/rename-source => repo/rename-target):
=== git ===
diff --git a/file1 b/file1
index 422c2b7ab3..de980441c3 100644
--- a/file1
+++ b/file1
@@ -1,2 +1,3 @@
a
b
+c
diff --git a/file2 b/file2
index 7898192261..9ddeb5c484 100644
--- a/file2
+++ b/file2
@@ -1,1 +1,2 @@
-a
+b
+c
diff --git a/rename-source b/rename-target
rename from rename-source
rename to rename-target
=== stat ===
repo/file1 | 1 +
repo/file2 | 3 ++-
repo/{rename-source => rename-target} | 0
3 files changed, 3 insertions(+), 1 deletion(-)
=== summary ===
M repo/file1
M repo/file2
R repo/{rename-source => rename-target}
[EOF]
");
// with non-default config
std::fs::write(
test_env.env_root().join("config-good.toml"),
indoc! {"
diff.color-words.context = 0
diff.color-words.max-inline-alternation = 0
diff.git.context = 1
"},
)
.unwrap();
let output = work_dir.run_jj([
"log",
"--config-file=../config-good.toml",
"--no-graph",
"-r@",
"-T",
template,
]);
insta::assert_snapshot!(output, @r"
=== color_words ===
Modified regular file file1:
...
3: c
Modified regular file file2:
1 : a
1: b
2: c
Modified regular file rename-target (rename-source => rename-target):
=== git ===
diff --git a/file1 b/file1
index 422c2b7ab3..de980441c3 100644
--- a/file1
+++ b/file1
@@ -2,1 +2,2 @@
b
+c
diff --git a/file2 b/file2
index 7898192261..9ddeb5c484 100644
--- a/file2
+++ b/file2
@@ -1,1 +1,2 @@
-a
+b
+c
diff --git a/rename-source b/rename-target
rename from rename-source
rename to rename-target
=== stat ===
file1 | 1 +
file2 | 3 ++-
{rename-source => rename-target} | 0
3 files changed, 3 insertions(+), 1 deletion(-)
=== summary ===
M file1
M file2
R {rename-source => rename-target}
[EOF]
");
// bad config
std::fs::write(
test_env.env_root().join("config-bad.toml"),
"diff.git.context = 'not an integer'\n",
)
.unwrap();
let output = work_dir.run_jj([
"log",
"--config-file=../config-bad.toml",
"-Tself.diff().git()",
]);
insta::assert_snapshot!(output, @r#"
------- stderr -------
Error: Failed to parse template: Failed to load diff settings
Caused by:
1: --> 1:13
|
1 | self.diff().git()
| ^-^
|
= Failed to load diff settings
2: Invalid type or value for diff.git.context
3: invalid type: string "not an integer", expected usize
Hint: Check the config file: ../config-bad.toml
[EOF]
[exit status: 1]
"#);
// color_words() with parameters
let template = "self.diff('file1').color_words(0)";
let output = work_dir.run_jj(["log", "--no-graph", "-r@", "-T", template]);
insta::assert_snapshot!(output, @r"
Modified regular file file1:
...
3: c
[EOF]
");
// git() with parameters
let template = "self.diff('file1').git(1)";
let output = work_dir.run_jj(["log", "--no-graph", "-r@", "-T", template]);
insta::assert_snapshot!(output, @r"
diff --git a/file1 b/file1
index 422c2b7ab3..de980441c3 100644
--- a/file1
+++ b/file1
@@ -2,1 +2,2 @@
b
+c
[EOF]
");
// custom template with files()
let template = indoc! {r#"
concat(
"=== " ++ commit_id.short() ++ " ===\n",
diff.files().map(|e| separate(" ",
e.path(),
"[" ++ e.status() ++ "]",
"source=" ++ e.source().path() ++ " [" ++ e.source().file_type() ++ "]",
"target=" ++ e.target().path() ++ " [" ++ e.target().file_type() ++ "]",
) ++ "\n").join(""),
"* " ++ separate(" ",
if(diff.files(), "non-empty", "empty"),
"len=" ++ diff.files().len(),
) ++ "\n",
)
"#};
let output = work_dir.run_jj(["log", "--no-graph", "-T", template]);
insta::assert_snapshot!(output, @r"
=== fbad2dd53d06 ===
file1 [modified] source=file1 [file] target=file1 [file]
file2 [modified] source=file2 [file] target=file2 [file]
rename-target [renamed] source=rename-source [file] target=rename-target [file]
* non-empty len=3
=== 3c9b3178609b ===
file1 [added] source=file1 [] target=file1 [file]
file2 [added] source=file2 [] target=file2 [file]
rename-source [added] source=rename-source [] target=rename-source [file]
* non-empty len=3
=== 000000000000 ===
* empty len=0
[EOF]
");
// custom diff stat template
let template = indoc! {r#"
concat(
"=== " ++ commit_id.short() ++ " ===\n",
"* " ++ separate(" ",
"total_added=" ++ diff.stat().total_added(),
"total_removed=" ++ diff.stat().total_removed(),
) ++ "\n",
)
"#};
let output = work_dir.run_jj(["log", "--no-graph", "-T", template]);
insta::assert_snapshot!(output, @r"
=== fbad2dd53d06 ===
* total_added=3 total_removed=1
=== 3c9b3178609b ===
* total_added=4 total_removed=0
=== 000000000000 ===
* total_added=0 total_removed=0
[EOF]
");
}
#[test]
fn test_file_list_entries() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.create_dir("dir");
work_dir.write_file("dir/file", "content1");
work_dir.write_file("exec-file", "content1");
work_dir.write_file("conflict-exec-file", "content1");
work_dir.write_file("conflict-file", "content1");
work_dir
.run_jj(["file", "chmod", "x", "exec-file", "conflict-exec-file"])
.success();
work_dir.run_jj(["new", "root()"]).success();
work_dir.write_file("conflict-exec-file", "content2");
work_dir.write_file("conflict-file", "content2");
work_dir
.run_jj(["file", "chmod", "x", "conflict-exec-file"])
.success();
work_dir.run_jj(["new", "all:visible_heads()"]).success();
let template = indoc! {r#"
separate(" ",
path,
"[" ++ file_type ++ "]",
"conflict=" ++ conflict,
"executable=" ++ executable,
) ++ "\n"
"#};
let output = work_dir.run_jj(["file", "list", "-T", template]);
insta::assert_snapshot!(output, @r"
conflict-exec-file [conflict] conflict=true executable=true
conflict-file [conflict] conflict=true executable=false
dir/file [file] conflict=false executable=false
exec-file [file] conflict=false executable=true
[EOF]
");
}
#[cfg(unix)]
#[test]
fn test_file_list_symlink() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
std::os::unix::fs::symlink("symlink_target", work_dir.root().join("symlink")).unwrap();
let template = r#"separate(" ", path, "[" ++ file_type ++ "]") ++ "\n""#;
let output = work_dir.run_jj(["file", "list", "-T", template]);
insta::assert_snapshot!(output, @r"
symlink [symlink]
[EOF]
");
}
#[test]
fn test_repo_path() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.create_dir("dir");
work_dir.write_file("dir/file", "content1");
work_dir.write_file("file", "content1");
let template = indoc! {r#"
separate(" ",
path,
"display=" ++ path.display(),
"parent=" ++ if(path.parent(), path.parent(), "<none>"),
"parent^2=" ++ if(path.parent().parent(), path.parent().parent(), "<none>"),
) ++ "\n"
"#};
let output = work_dir.run_jj(["file", "list", "-T", template]);
insta::assert_snapshot!(output.normalize_backslash(), @r"
dir/file display=dir/file parent=dir parent^2=
file display=file parent= parent^2=<none>
[EOF]
");
let template = r#"separate(" ", path, "display=" ++ path.display()) ++ "\n""#;
let output = test_env.run_jj_in(
work_dir.root().join("dir"),
["file", "list", "-T", template],
);
insta::assert_snapshot!(output.normalize_backslash(), @r"
dir/file display=file
file display=../file
[EOF]
");
}
#[test]
fn test_signature_templates() {
let test_env = TestEnvironment::default();
test_env.run_jj_in(".", ["git", "init", "repo"]).success();
let work_dir = test_env.work_dir("repo");
work_dir.run_jj(["commit", "-m", "unsigned"]).success();
test_env.add_config("signing.behavior = 'own'");
test_env.add_config("signing.backend = 'test'");
work_dir.run_jj(["describe", "-m", "signed"]).success();
let template = r#"
if(signature,
signature.status() ++ " " ++ signature.display(),
"no",
) ++ " signature""#;
// show that signatures can render
let output = work_dir.run_jj(["log", "-T", template]);
insta::assert_snapshot!(output, @r"
@ good test-display signature
○ no signature
◆ no signature
[EOF]
");
let output = work_dir.run_jj(["show", "-T", template]);
insta::assert_snapshot!(output, @"good test-display signature[EOF]");
// builtin templates
test_env.add_config("ui.show-cryptographic-signatures = true");
let args = ["log", "-r", "..", "-T"];
let output = work_dir.run_jj_with(|cmd| cmd.args(args).arg("builtin_log_oneline"));
insta::assert_snapshot!(output, @r"
@ rlvkpnrz test.user 2001-02-03 08:05:09 a0909ee9 [✓︎] (empty) signed
○ qpvuntsm test.user 2001-02-03 08:05:08 879d5d20 (empty) unsigned
~
[EOF]
");
let output = work_dir.run_jj_with(|cmd| cmd.args(args).arg("builtin_log_compact"));
insta::assert_snapshot!(output, @r"
@ rlvkpnrz test.user@example.com 2001-02-03 08:05:09 a0909ee9 [✓︎]
│ (empty) signed
○ qpvuntsm test.user@example.com 2001-02-03 08:05:08 879d5d20
│ (empty) unsigned
~
[EOF]
");
let output = work_dir.run_jj_with(|cmd| cmd.args(args).arg("builtin_log_detailed"));
insta::assert_snapshot!(output, @r"
@ Commit ID: a0909ee96bb5c66311a0c579dc8ebed4456dfc1b
│ Change ID: rlvkpnrzqnoowoytxnquwvuryrwnrmlp
│ Author : Test User <test.user@example.com> (2001-02-03 08:05:09)
│ Committer: Test User <test.user@example.com> (2001-02-03 08:05:09)
│ Signature: good signature by test-display
│ signed
○ Commit ID: 879d5d20fea5930f053e0817033ad4aba924a361
│ Change ID: qpvuntsmwlqtpsluzzsnyyzlmlwvmlnu
~ Author : Test User <test.user@example.com> (2001-02-03 08:05:08)
Committer: Test User <test.user@example.com> (2001-02-03 08:05:08)
Signature: (no signature)
unsigned
[EOF]
");
// customization point
let config_val = r#"template-aliases."format_short_cryptographic_signature(signature)"="'status: ' ++ signature.status()""#;
let output = work_dir.run_jj_with(|cmd| {
cmd.args(args)
.arg("builtin_log_oneline")
.args(["--config", config_val])
});
insta::assert_snapshot!(output, @r"
@ rlvkpnrz test.user 2001-02-03 08:05:09 a0909ee9 status: good (empty) signed
○ qpvuntsm test.user 2001-02-03 08:05:08 879d5d20 status: <Error: No CryptographicSignature available> (empty) unsigned
~
[EOF]
");
}
#[test]
fn test_log_git_format_patch_template() {
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("file1", "foo\n");
work_dir.write_file("file2", "bar\n");
work_dir
.run_jj([
"new",
"-m",
"some change\n\nmultiline desc\nsecond line\n\nwith blanks\n",
])
.success();
work_dir.remove_file("file1");
work_dir.write_file("file2", "modified\n");
work_dir.write_file("file3", "new\n");
let output = work_dir.run_jj([
"log",
"--no-graph",
"-T",
"git_format_patch_email_headers",
"-r@",
]);
insta::assert_snapshot!(output, @r"
From 993219c0b219b5eeac6303b5cb2bf943ea719672 Mon Sep 17 00:00:00 2001
From: Test User <test.user@example.com>
Date: Sat, 3 Feb 2001 04:05:08 +0700
Subject: [PATCH] some change
multiline desc
second line
with blanks
---
file1 | 1 -
file2 | 2 +-
file3 | 1 +
3 files changed, 2 insertions(+), 2 deletions(-)
[EOF]
");
}