From 800ffd4b6d47fff4947c9a209c41625efa0e96b7 Mon Sep 17 00:00:00 2001 From: Zachary Dremann Date: Wed, 13 Sep 2023 13:50:12 -0400 Subject: [PATCH] Allow \0 escape for nulls This allows safely getting e.g. multiple descriptions, and knowing where the boundaries are --- CHANGELOG.md | 5 +++++ cli/src/template.pest | 2 +- cli/src/template_parser.rs | 17 +++++++++-------- cli/tests/test_log_command.rs | 36 +++++++++++++++++++++++++++++++++++ cli/tests/test_operations.rs | 21 ++++++++++++++++++++ cli/tests/test_templater.rs | 2 ++ 6 files changed, 74 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b5348c85d..f16fb610d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj op log` now supports `--no-graph`. +* Templates now support an additional escape: `\0`. This will output a literal + null byte. This may be useful for e.g. + `jj log -T 'description ++ "\0"' --no-graph` to output descriptions only, but + be able to tell where the boundaries are + ### Fixed bugs ## [0.9.0] - 2023-09-06 diff --git a/cli/src/template.pest b/cli/src/template.pest index 232bd45429..261fc0438d 100644 --- a/cli/src/template.pest +++ b/cli/src/template.pest @@ -19,7 +19,7 @@ whitespace = _{ " " | "\t" | "\r" | "\n" | "\x0c" } -escape = @{ "\\" ~ ("t" | "r" | "n" | "\"" | "\\") } +escape = @{ "\\" ~ ("t" | "r" | "n" | "0" | "\"" | "\\") } literal_char = @{ !("\"" | "\\") ~ ANY } raw_literal = @{ literal_char+ } literal = { "\"" ~ (raw_literal | escape)* ~ "\"" } diff --git a/cli/src/template_parser.rs b/cli/src/template_parser.rs index 81c1e2e2dd..33186410cf 100644 --- a/cli/src/template_parser.rs +++ b/cli/src/template_parser.rs @@ -256,12 +256,13 @@ fn parse_string_literal(pair: Pair) -> String { Rule::raw_literal => { result.push_str(part.as_str()); } - Rule::escape => match part.as_str().as_bytes()[1] as char { - '"' => result.push('"'), - '\\' => result.push('\\'), - 't' => result.push('\t'), - 'r' => result.push('\r'), - 'n' => result.push('\n'), + Rule::escape => match &part.as_str()[1..] { + "\"" => result.push('"'), + "\\" => result.push('\\'), + "t" => result.push('\t'), + "r" => result.push('\r'), + "n" => result.push('\n'), + "0" => result.push('\0'), char => panic!("invalid escape: \\{char:?}"), }, _ => panic!("unexpected part of string: {part:?}"), @@ -963,8 +964,8 @@ mod tests { fn test_string_literal() { // "\" escapes assert_eq!( - parse_into_kind(r#" "\t\r\n\"\\" "#), - Ok(ExpressionKind::String("\t\r\n\"\\".to_owned())), + parse_into_kind(r#" "\t\r\n\"\\\0" "#), + Ok(ExpressionKind::String("\t\r\n\"\\\0".to_owned())), ); // Invalid "\" escape diff --git a/cli/tests/test_log_command.rs b/cli/tests/test_log_command.rs index aae4ab68ba..65439836c8 100644 --- a/cli/tests/test_log_command.rs +++ b/cli/tests/test_log_command.rs @@ -314,6 +314,42 @@ fn test_log_with_or_without_diff() { "###); } +#[test] +fn test_log_null_terminate_multiline_descriptions() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + + test_env.jj_cmd_success( + &repo_path, + &["commit", "-m", "commit 1 line 1", "-m", "commit 1 line 2"], + ); + test_env.jj_cmd_success( + &repo_path, + &["commit", "-m", "commit 2 line 1", "-m", "commit 2 line 2"], + ); + test_env.jj_cmd_success( + &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(); diff --git a/cli/tests/test_operations.rs b/cli/tests/test_operations.rs index 08340c208a..5636aff885 100644 --- a/cli/tests/test_operations.rs +++ b/cli/tests/test_operations.rs @@ -146,6 +146,27 @@ fn test_op_log_no_graph() { "###); } +#[test] +fn test_op_log_no_graph_null_terminated() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + test_env.jj_cmd_success(&repo_path, &["commit", "-m", "message1"]); + test_env.jj_cmd_success(&repo_path, &["commit", "-m", "message2"]); + + let stdout = test_env.jj_cmd_success( + &repo_path, + &[ + "op", + "log", + "--no-graph", + "--template", + r#"id.short(4) ++ "\0""#, + ], + ); + insta::assert_debug_snapshot!(stdout, @r###""c8b0\07277\019b8\0f1c4\0""###); +} + #[test] fn test_op_log_template() { let test_env = TestEnvironment::default(); diff --git a/cli/tests/test_templater.rs b/cli/tests/test_templater.rs index 21a3753e76..377921c6e8 100644 --- a/cli/tests/test_templater.rs +++ b/cli/tests/test_templater.rs @@ -279,6 +279,8 @@ fn test_templater_list_method() { insta::assert_snapshot!(render(r#""".lines().join("|")"#), @""); insta::assert_snapshot!(render(r#""a\nb\nc".lines().join("|")"#), @"a|b|c"); + // Null separator + insta::assert_snapshot!(render(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c"); // Keyword as separator insta::assert_snapshot!(render(r#""a\nb\nc".lines().join(commit_id.short(2))"#), @"a00b00c");