From e6dafb0f4d456062c6e574bef878fd22c46bcacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20=C3=84lgmyr?= Date: Mon, 11 Mar 2024 23:16:24 +0100 Subject: [PATCH] Make node symbols templatable in the graphs. Adds config options * templates.log_graph_node_commit * templates.log_graph_node_op * templates.log_graph_node_elided --- CHANGELOG.md | 6 +++++- cli/src/cli_util.rs | 8 ++++++++ cli/src/commands/log.rs | 26 ++++++++++++++----------- cli/src/commands/obslog.rs | 17 ++++++++-------- cli/src/commands/operation.rs | 23 ++++++++++++---------- cli/src/config-schema.json | 12 ++++++++---- cli/tests/test_log_command.rs | 24 ++++++++++++----------- cli/tests/test_obslog_command.rs | 33 ++++++++++++++++++++++++++++++++ cli/tests/test_operations.rs | 32 +++++++++++++++++++++++++++++++ docs/config.md | 23 ++++++++++++++++++++++ lib/src/settings.rs | 22 ++++++++++++++++----- 11 files changed, 175 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c271987f..ea61daf574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `ui.default-command` now accepts multiple string arguments, for more complex default `jj` commands. -* Graph node symbols are now configurable via `ui.graph.default_node` and `ui.graph.elided_node`. +* Graph node symbols are now configurable via templates + * `templates.log_graph_node_commit` + * `templates.log_graph_node_op` + * `templates.log_graph_node_elided` + * `jj log` now includes synthetic nodes in the graph where some revisions were elided. diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 7d69e30380..ce9233964b 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -2311,6 +2311,14 @@ pub fn parse_args( Ok((matches, args)) } +pub fn format_template(ui: &Ui, arg: &T, template: &dyn Template) -> String { + let mut output = vec![]; + template + .format(arg, ui.new_formatter(&mut output).as_mut()) + .expect("write() to vec backed formatter should never fail"); + String::from_utf8(output).expect("template output should be utf-8 bytes") +} + /// CLI command builder and runner. #[must_use] pub struct CliRunner { diff --git a/cli/src/commands/log.rs b/cli/src/commands/log.rs index d7d9e4f4ca..7da046e206 100644 --- a/cli/src/commands/log.rs +++ b/cli/src/commands/log.rs @@ -21,10 +21,12 @@ use jj_lib::revset_graph::{ }; use tracing::instrument; -use crate::cli_util::{CommandHelper, LogContentFormat, RevisionArg}; +use crate::cli_util::{format_template, CommandHelper, LogContentFormat, RevisionArg}; use crate::command_error::CommandError; use crate::diff_util::{self, DiffFormatArgs}; +use crate::generic_templater::GenericTemplateLanguage; use crate::graphlog::{get_graphlog, Edge}; +use crate::templater::Template; use crate::ui::Ui; /// Show revision history @@ -99,7 +101,6 @@ pub(crate) fn cmd_log( expression }; let repo = workspace_command.repo(); - let wc_commit_id = workspace_command.get_wc_commit_id(); let matcher = workspace_command.matcher_from_values(&args.paths)?; let revset = workspace_command.evaluate_revset(revset_expression)?; @@ -118,6 +119,14 @@ pub(crate) fn cmd_log( let template = workspace_command.parse_commit_template(&template_string)?; let with_content_format = LogContentFormat::new(ui, command.settings())?; + let commit_node_template = + workspace_command.parse_commit_template(&command.settings().commit_node_template())?; + + let elided_node_template = workspace_command.parse_template( + &GenericTemplateLanguage::new(), + &command.settings().elided_node_template(), + )?; + { ui.request_pager(); let mut formatter = ui.stdout_formatter(); @@ -125,8 +134,6 @@ pub(crate) fn cmd_log( if !args.no_graph { let mut graph = get_graphlog(command.settings(), formatter.raw()); - let default_node_symbol = command.settings().default_node_symbol(); - let elided_node_symbol = command.settings().elided_node_symbol(); let forward_iter = TopoGroupedRevsetGraphIterator::new(revset.iter_graph()); let iter: Box> = if args.reversed { Box::new(ReverseRevsetGraphIterator::new(forward_iter)) @@ -184,16 +191,12 @@ pub(crate) fn cmd_log( &diff_formats, )?; } - let node_symbol = if Some(&key.0) == wc_commit_id { - "@" - } else { - &default_node_symbol - }; + let node_symbol = format_template(ui, &commit, commit_node_template.as_ref()); graph.add_node( &key, &graphlog_edges, - node_symbol, + &node_symbol, &String::from_utf8_lossy(&buffer), )?; for elided_target in elided_targets { @@ -206,10 +209,11 @@ pub(crate) fn cmd_log( |formatter| writeln!(formatter.labeled("elided"), "(elided revisions)"), || graph.width(&elided_key, &edges), )?; + let node_symbol = format_template(ui, &(), elided_node_template.as_ref()); graph.add_node( &elided_key, &edges, - &elided_node_symbol, + &node_symbol, &String::from_utf8_lossy(&buffer), )?; } diff --git a/cli/src/commands/obslog.rs b/cli/src/commands/obslog.rs index a77b439b4e..581892af16 100644 --- a/cli/src/commands/obslog.rs +++ b/cli/src/commands/obslog.rs @@ -18,7 +18,9 @@ use jj_lib::matchers::EverythingMatcher; use jj_lib::rewrite::rebase_to_dest_parent; use tracing::instrument; -use crate::cli_util::{CommandHelper, LogContentFormat, RevisionArg, WorkspaceCommandHelper}; +use crate::cli_util::{ + format_template, CommandHelper, LogContentFormat, RevisionArg, WorkspaceCommandHelper, +}; use crate::command_error::CommandError; use crate::diff_util::{self, DiffFormat, DiffFormatArgs}; use crate::formatter::Formatter; @@ -63,7 +65,6 @@ pub(crate) fn cmd_obslog( let workspace_command = command.workspace_helper(ui)?; let start_commit = workspace_command.resolve_single_rev(&args.revision)?; - let wc_commit_id = workspace_command.get_wc_commit_id(); let diff_formats = diff_util::diff_formats_for_log(command.settings(), &args.diff_format, args.patch)?; @@ -80,6 +81,9 @@ pub(crate) fn cmd_obslog( let formatter = formatter.as_mut(); formatter.push_label("log")?; + let commit_node_template = + workspace_command.parse_commit_template(&command.settings().commit_node_template())?; + let mut commits = topo_order_reverse( vec![start_commit], |commit: &Commit| commit.id().clone(), @@ -90,7 +94,6 @@ pub(crate) fn cmd_obslog( } if !args.no_graph { let mut graph = get_graphlog(command.settings(), formatter.raw()); - let default_node_symbol = command.settings().default_node_symbol(); for commit in commits { let mut edges = vec![]; for predecessor in &commit.predecessors() { @@ -115,15 +118,11 @@ pub(crate) fn cmd_obslog( &diff_formats, )?; } - let node_symbol = if Some(commit.id()) == wc_commit_id { - "@" - } else { - &default_node_symbol - }; + let node_symbol = format_template(ui, &commit, commit_node_template.as_ref()); graph.add_node( commit.id(), &edges, - node_symbol, + &node_symbol, &String::from_utf8_lossy(&buffer), )?; } diff --git a/cli/src/commands/operation.rs b/cli/src/commands/operation.rs index 0e4fd85362..3abb60efdf 100644 --- a/cli/src/commands/operation.rs +++ b/cli/src/commands/operation.rs @@ -23,11 +23,11 @@ use jj_lib::op_walk; use jj_lib::operation::Operation; use jj_lib::repo::Repo; -use crate::cli_util::{short_operation_hash, CommandHelper, LogContentFormat}; +use crate::cli_util::{format_template, short_operation_hash, CommandHelper, LogContentFormat}; use crate::command_error::{user_error, user_error_with_hint, CommandError}; use crate::graphlog::{get_graphlog, Edge}; use crate::operation_templater::OperationTemplateLanguage; -use crate::templater::Template as _; +use crate::templater::Template; use crate::ui::Ui; /// Commands for working with the operation log @@ -172,20 +172,27 @@ fn cmd_op_log( }; let with_content_format = LogContentFormat::new(ui, command.settings())?; + let op_node_template = { + let language = OperationTemplateLanguage::new( + repo_loader.op_store().root_operation_id(), + current_op_id, + command.operation_template_extension(), + ); + command.parse_template(ui, &language, &command.settings().op_node_template())? + }; + ui.request_pager(); let mut formatter = ui.stdout_formatter(); let formatter = formatter.as_mut(); let iter = op_walk::walk_ancestors(&head_ops).take(args.limit.unwrap_or(usize::MAX)); if !args.no_graph { let mut graph = get_graphlog(command.settings(), formatter.raw()); - let default_node_symbol = command.settings().default_node_symbol(); for op in iter { let op = op?; let mut edges = vec![]; for id in op.parent_ids() { edges.push(Edge::Direct(id.clone())); } - let is_current_op = Some(op.id()) == current_op_id; let mut buffer = vec![]; with_content_format.write_graph_text( ui.new_formatter(&mut buffer).as_mut(), @@ -197,15 +204,11 @@ fn cmd_op_log( if !buffer.ends_with(b"\n") { buffer.push(b'\n'); } - let node_symbol = if is_current_op { - "@" - } else { - &default_node_symbol - }; + let node_symbol = format_template(ui, &op, &op_node_template); graph.add_node( op.id(), &edges, - node_symbol, + &node_symbol, &String::from_utf8_lossy(&buffer), )?; } diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json index 8bbc7fa68e..32b767e763 100644 --- a/cli/src/config-schema.json +++ b/cli/src/config-schema.json @@ -123,13 +123,17 @@ ], "default": "curved" }, - "default_node": { + "commit-node-template": { "type": "string", - "description": "The symbol used as the default symbol for nodes." + "description": "Template for rendering commit nodes." }, - "elided_node": { + "op-node-template": { + "type": "string", + "description": "Template for rendering operation nodes." + }, + "elided-node-template": { "type": "string", - "description": "The symbol used for elided nodes." + "description": "Template for rendering elided nodes." } } }, diff --git a/cli/tests/test_log_command.rs b/cli/tests/test_log_command.rs index 5601b53610..3d26256860 100644 --- a/cli/tests/test_log_command.rs +++ b/cli/tests/test_log_command.rs @@ -1413,7 +1413,7 @@ fn test_elided() { } #[test] -fn test_custom_symbols() { +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"]); @@ -1439,11 +1439,12 @@ fn test_custom_symbols() { // Simple test with showing default and elided nodes. test_env.add_config(concat!( "ui.log-synthetic-elided-nodes = true\n", - "ui.graph.default_node = '┝'\n", - "ui.graph.elided_node = '🮀'", + "templates.log_graph_node_commit = 'if(current_working_copy, \"$\", if(root, \"┴\", \ + \"┝\"))'\n", + "templates.log_graph_node_elided = '\"🮀\"'", )); - insta::assert_snapshot!(get_log("@ | @- | description(initial)"), @r###" - @ merge + insta::assert_snapshot!(get_log("@ | @- | description(initial) | root()"), @r###" + $ merge ├─╮ │ ┝ side branch 2 │ │ @@ -1454,18 +1455,19 @@ fn test_custom_symbols() { ├─╯ ┝ initial │ - ~ + ┴ "###); // Simple test with showing default and elided nodes, ascii style. test_env.add_config(concat!( "ui.log-synthetic-elided-nodes = true\n", "ui.graph.style = 'ascii'\n", - "ui.graph.default_node = '*'\n", - "ui.graph.elided_node = ':'", + "templates.log_graph_node_commit = 'if(current_working_copy, \"$\", if(root, \"^\", \ + \"*\"))'\n", + "templates.log_graph_node_elided = '\":\"'", )); - insta::assert_snapshot!(get_log("@ | @- | description(initial)"), @r###" - @ merge + insta::assert_snapshot!(get_log("@ | @- | description(initial) | root()"), @r###" + $ merge |\ | * side branch 2 | | @@ -1476,6 +1478,6 @@ fn test_custom_symbols() { |/ * initial | - ~ + ^ "###); } diff --git a/cli/tests/test_obslog_command.rs b/cli/tests/test_obslog_command.rs index af20ec0572..24c13f1d25 100644 --- a/cli/tests/test_obslog_command.rs +++ b/cli/tests/test_obslog_command.rs @@ -142,6 +142,39 @@ fn test_obslog_with_or_without_diff() { "###); } +#[test] +fn test_obslog_with_custom_symbols() { + 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, &["new", "-m", "my description"]); + std::fs::write(repo_path.join("file1"), "foo\nbar\n").unwrap(); + std::fs::write(repo_path.join("file2"), "foo\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "@", "-d", "root()"]); + std::fs::write(repo_path.join("file1"), "resolved\n").unwrap(); + + let toml = concat!( + "templates.log_graph_node_elided = true\n", + "templates.log_graph_node_commit = 'if(current_working_copy, \"$\", if(root, \"┴\", \ + \"┝\"))'\n", + ); + + let stdout = test_env.jj_cmd_success(&repo_path, &["obslog", "--config-toml", toml]); + + insta::assert_snapshot!(stdout, @r###" + $ rlvkpnrz test.user@example.com 2001-02-03 04:05:10.000 +07:00 66b42ad3 + │ my description + ┝ rlvkpnrz hidden test.user@example.com 2001-02-03 04:05:09.000 +07:00 ebc23d4b conflict + │ my description + ┝ rlvkpnrz hidden test.user@example.com 2001-02-03 04:05:09.000 +07:00 6fbba7bc + │ my description + ┝ rlvkpnrz hidden test.user@example.com 2001-02-03 04:05:08.000 +07:00 eac0d0da + (empty) my description + "###); +} + #[test] fn test_obslog_word_wrap() { let test_env = TestEnvironment::default(); diff --git a/cli/tests/test_operations.rs b/cli/tests/test_operations.rs index ff00f1089b..774fe905ae 100644 --- a/cli/tests/test_operations.rs +++ b/cli/tests/test_operations.rs @@ -93,6 +93,38 @@ fn test_op_log() { "###); } +#[test] +fn test_op_log_with_custom_symbols() { + 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", "description 0"]); + + let stdout = test_env.jj_cmd_success( + &repo_path, + &[ + "op", + "log", + "--config-toml", + concat!( + "template-aliases.'format_time_range(x)' = 'x'\n", + "templates.log_graph_node_op = 'if(current_operation, \"$\", if(root, \"┴\", \ + \"┝\"))'", + ), + ], + ); + insta::assert_snapshot!(&stdout, @r###" + $ 52ac15d375ba test-username@host.example.com 2001-02-03 04:05:08.000 +07:00 - 2001-02-03 04:05:08.000 +07:00 + │ describe commit 230dd059e1b059aefc0da06a2e5a7dbf22362f22 + │ args: jj describe -m 'description 0' + ┝ b51416386f26 test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00 + │ add workspace 'default' + ┝ 9a7d829846af test-username@host.example.com 2001-02-03 04:05:07.000 +07:00 - 2001-02-03 04:05:07.000 +07:00 + │ initialize repo + ┴ 000000000000 root() + "###); +} + #[test] fn test_op_log_with_no_template() { let test_env = TestEnvironment::default(); diff --git a/docs/config.md b/docs/config.md index c30dd49c54..4555955a4c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -223,6 +223,29 @@ revsets.log = "main@origin.." ui.graph.style = "square" ``` +#### Node style +The symbols used to represent commits or operations can be customized via +templates. + + * `templates.log_graph_node_commit` for commits (with `Commit` keywords) + * `templates.log_graph_node_op` for operations (with `Operation` keywords) + * `templates.log_graph_node_elided` for elided nodes + +For example: +```toml +[templates] +log_graph_node_commit = ''' + if(current_working_copy, "@", + if(immutable, "●", + if(root, "┴", "○") + ) + ) +''' +log_graph_node_op = 'if(current_operation, "@", "○")' +log_graph_node_elided = '"🮀"' + +``` + ### Wrap log content If enabled, `log`/`obslog`/`op log` content will be wrapped based on diff --git a/lib/src/settings.rs b/lib/src/settings.rs index d35d08412b..e47ca24530 100644 --- a/lib/src/settings.rs +++ b/lib/src/settings.rs @@ -242,12 +242,24 @@ impl UserSettings { .unwrap_or_else(|_| "curved".to_string()) } - pub fn default_node_symbol(&self) -> String { - self.node_symbol_for_key("ui.graph.default_node", "◉", "o") + pub fn commit_node_template(&self) -> String { + self.node_template_for_key( + "templates.log_graph_node_commit", + r#"if(current_working_copy, "@", if(immutable, "◉", "◉"))"#, + r#"if(current_working_copy, "@", if(immutable, "o", "o"))"#, + ) } - pub fn elided_node_symbol(&self) -> String { - self.node_symbol_for_key("ui.graph.elided_node", "◌", ".") + pub fn op_node_template(&self) -> String { + self.node_template_for_key( + "templates.log_graph_node_op", + r#"if(current_operation, "@", "◉")"#, + r#"if(current_operation, "@", "o")"#, + ) + } + + pub fn elided_node_template(&self) -> String { + self.node_template_for_key("templates.log_graph_node_elided", r#""◌""#, r#"".""#) } pub fn max_new_file_size(&self) -> Result { @@ -273,7 +285,7 @@ impl UserSettings { SignSettings::from_settings(self) } - fn node_symbol_for_key(&self, key: &str, fallback: &str, ascii_fallback: &str) -> String { + fn node_template_for_key(&self, key: &str, fallback: &str, ascii_fallback: &str) -> String { let symbol = self.config.get_string(key); match self.graph_style().as_str() { "ascii" | "ascii-large" => symbol.unwrap_or_else(|_| ascii_fallback.to_owned()),