diff --git a/CHANGELOG.md b/CHANGELOG.md index 9565758a75..1249227ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,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_node` + * `templates.op_log_node` + * `templates.log_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 61b7688fcb..b348a82dfa 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -904,14 +904,19 @@ Set which revision the branch points to with `jj branch set {branch_name} -r Result + '_>, CommandError> { - let language = CommitTemplateLanguage::new( + let language = self.commit_template_language()?; + self.parse_template(&language, template_text) + } + + /// Creates commit template language environment for this workspace. + pub fn commit_template_language(&self) -> Result, CommandError> { + Ok(CommitTemplateLanguage::new( self.repo().as_ref(), self.workspace_id(), self.revset_parse_context(), self.id_prefix_context()?, self.commit_template_extension.as_deref(), - ); - self.parse_template(&language, template_text) + )) } /// Template for one-line summary of a commit. @@ -2309,6 +2314,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 0b59bbb6e3..e14a56959b 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 @@ -94,7 +96,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)?; @@ -102,17 +103,30 @@ pub(crate) fn cmd_log( let diff_formats = diff_util::diff_formats_for_log(command.settings(), &args.diff_format, args.patch)?; - let template_string = match &args.template { - Some(value) => value.to_string(), - None => command.settings().config().get_string("templates.log")?, - }; let use_elided_nodes = command .settings() .config() .get_bool("ui.log-synthetic-elided-nodes")?; - let template = workspace_command.parse_commit_template(&template_string)?; let with_content_format = LogContentFormat::new(ui, command.settings())?; + let template; + let commit_node_template; + { + let language = workspace_command.commit_template_language()?; + let template_string = match &args.template { + Some(value) => value.to_string(), + None => command.settings().config().get_string("templates.log")?, + }; + template = workspace_command.parse_template(&language, &template_string)?; + commit_node_template = workspace_command + .parse_template(&language, &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(); @@ -120,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)) @@ -179,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 { @@ -201,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..663829e154 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,18 +65,24 @@ 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)?; - - let template_string = match &args.template { - Some(value) => value.to_string(), - None => command.settings().config().get_string("templates.log")?, - }; - let template = workspace_command.parse_commit_template(&template_string)?; let with_content_format = LogContentFormat::new(ui, command.settings())?; + let template; + let commit_node_template; + { + let language = workspace_command.commit_template_language()?; + let template_string = match &args.template { + Some(value) => value.to_string(), + None => command.settings().config().get_string("templates.log")?, + }; + template = workspace_command.parse_template(&language, &template_string)?; + commit_node_template = workspace_command + .parse_template(&language, &command.settings().commit_node_template())?; + } + ui.request_pager(); let mut formatter = ui.stdout_formatter(); let formatter = formatter.as_mut(); @@ -90,7 +98,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 +122,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..d4051d4f68 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 @@ -157,8 +157,11 @@ fn cmd_op_log( [op] => Some(op.id()), _ => None, }; + let with_content_format = LogContentFormat::new(ui, command.settings())?; - let template = { + let template; + let op_node_template; + { let language = OperationTemplateLanguage::new( repo_loader.op_store().root_operation_id(), current_op_id, @@ -168,9 +171,10 @@ fn cmd_op_log( Some(value) => value.to_owned(), None => command.settings().config().get_string("templates.op_log")?, }; - command.parse_template(ui, &language, &text)? - }; - let with_content_format = LogContentFormat::new(ui, command.settings())?; + template = command.parse_template(ui, &language, &text)?; + op_node_template = + command.parse_template(ui, &language, &command.settings().op_node_template())?; + } ui.request_pager(); let mut formatter = ui.stdout_formatter(); @@ -178,14 +182,12 @@ fn cmd_op_log( 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 +199,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 5d96d03aba..a6bca7fffc 100644 --- a/cli/src/config-schema.json +++ b/cli/src/config-schema.json @@ -122,14 +122,6 @@ "ascii-large" ], "default": "curved" - }, - "default_node": { - "type": "string", - "description": "The symbol used as the default symbol for nodes." - }, - "elided_node": { - "type": "string", - "description": "The symbol used for elided nodes." } } }, diff --git a/cli/tests/test_log_command.rs b/cli/tests/test_log_command.rs index 43748ccaf5..d2887bbcb3 100644 --- a/cli/tests/test_log_command.rs +++ b/cli/tests/test_log_command.rs @@ -1409,7 +1409,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"]); @@ -1435,11 +1435,11 @@ 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_node = 'if(current_working_copy, \"$\", if(root, \"┴\", \"┝\"))'\n", + "templates.log_node_elided = '\"🮀\"'", )); - insta::assert_snapshot!(get_log("@ | @- | description(initial)"), @r###" - @ merge + insta::assert_snapshot!(get_log("@ | @- | description(initial) | root()"), @r###" + $ merge ├─╮ │ ┝ side branch 2 │ │ @@ -1450,18 +1450,18 @@ 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_node = 'if(current_working_copy, \"$\", if(root, \"^\", \"*\"))'\n", + "templates.log_node_elided = '\":\"'", )); - insta::assert_snapshot!(get_log("@ | @- | description(initial)"), @r###" - @ merge + insta::assert_snapshot!(get_log("@ | @- | description(initial) | root()"), @r###" + $ merge |\ | * side branch 2 | | @@ -1472,6 +1472,6 @@ fn test_custom_symbols() { |/ * initial | - ~ + ^ "###); } diff --git a/cli/tests/test_obslog_command.rs b/cli/tests/test_obslog_command.rs index 59a2f333a5..2d0cb29b33 100644 --- a/cli/tests/test_obslog_command.rs +++ b/cli/tests/test_obslog_command.rs @@ -142,6 +142,35 @@ 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_node = 'if(current_working_copy, \"$\", \"┝\")'\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 08:05:10 66b42ad3 + │ my description + ┝ rlvkpnrz hidden test.user@example.com 2001-02-03 08:05:09 ebc23d4b conflict + │ my description + ┝ rlvkpnrz hidden test.user@example.com 2001-02-03 08:05:09 6fbba7bc + │ my description + ┝ rlvkpnrz hidden test.user@example.com 2001-02-03 08:05:08 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..1e85705652 100644 --- a/cli/tests/test_operations.rs +++ b/cli/tests/test_operations.rs @@ -93,6 +93,37 @@ 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.op_log_node = '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 f151337c55..ba499f5242 100644 --- a/docs/config.md +++ b/docs/config.md @@ -229,6 +229,30 @@ 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_node` for commits (with `Commit` keywords) + * `templates.op_log_node` for operations (with `Operation` keywords) + * `templates.log_node_elided` for elided nodes + +For example: +```toml +[templates] +log_node = ''' + if(current_working_copy, "@", + if(root, "┴", + if(immutable, "●", "○") + ) + ) +''' +op_log_node = 'if(current_operation, "@", "○")' +log_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..c01d8d23da 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_node", + r#"if(current_working_copy, "@", "◉")"#, + r#"if(current_working_copy, "@", "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.op_log_node", + r#"if(current_operation, "@", "◉")"#, + r#"if(current_operation, "@", "o")"#, + ) + } + + pub fn elided_node_template(&self) -> String { + self.node_template_for_key("templates.log_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()),