From b5f1728ffb6952befa8e718d4e30add80f1541d6 Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Sun, 19 Feb 2023 17:05:50 +0900 Subject: [PATCH] templater: migrate op log to template language The outermost "op-log" label isn't moved to the default template. I think it belongs to the command's formatter rather than the template. Old bikeshedding items: - "current_head", "is_head", or "is_head_op" => renamed to "current_operation" - "templates.op-log" vs "templates.op_log" (the whole template is labeled as "op-log") => renamed to "op_log" - "template-aliases.'format_operation_duration(time_range)'" => renamed to 'format_time_range(time_range)' --- CHANGELOG.md | 4 + docs/config.md | 6 +- lib/src/settings.rs | 6 -- src/cli_util.rs | 4 + src/commands/operation.rs | 77 +++----------- src/config-schema.json | 5 - src/config/templates.toml | 15 +++ src/lib.rs | 1 + src/operation_templater.rs | 200 +++++++++++++++++++++++++++++++++++++ tests/test_operations.rs | 2 +- 10 files changed, 240 insertions(+), 80 deletions(-) create mode 100644 src/operation_templater.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 779c1c4ac..53b77ef89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking changes +* The `ui.oplog-relative-timestamps` option has been removed. Use the + `format_time_range()` template alias instead. For details, see + [the documentation](docs/config.md). + ### New features * `jj git push --deleted` will remove all locally deleted branches from the remote. diff --git a/docs/config.md b/docs/config.md index 978c7428d..2d0885960 100644 --- a/docs/config.md +++ b/docs/config.md @@ -165,11 +165,11 @@ Can be customized by the `format_timestamp()` template alias. ``` `jj op log` defaults to relative timestamps. To use absolute timestamps, you -will need to modify an option. +will need to modify the `format_time_range()` template alias. ```toml -[ui] -oplog-relative-timestamps=false +[template-aliases] +'format_time_range(time_range)' = 'time_range.start() " - " time_range.end()' ``` ### Author format diff --git a/lib/src/settings.rs b/lib/src/settings.rs index cb73b2afd..2c109ecc2 100644 --- a/lib/src/settings.rs +++ b/lib/src/settings.rs @@ -158,12 +158,6 @@ impl UserSettings { .unwrap_or(false) } - pub fn oplog_relative_timestamps(&self) -> bool { - self.config - .get_bool("ui.oplog-relative-timestamps") - .unwrap_or(true) - } - pub fn config(&self) -> &config::Config { &self.config } diff --git a/src/cli_util.rs b/src/cli_util.rs index 45410f62a..7a4c93dd1 100644 --- a/src/cli_util.rs +++ b/src/cli_util.rs @@ -811,6 +811,10 @@ impl WorkspaceCommandHelper { } } + pub fn template_aliases_map(&self) -> &TemplateAliasesMap { + &self.template_aliases_map + } + pub fn parse_commit_template( &self, template_text: &str, diff --git a/src/commands/operation.rs b/src/commands/operation.rs index 9781ca564..ba363349d 100644 --- a/src/commands/operation.rs +++ b/src/commands/operation.rs @@ -1,14 +1,11 @@ -use std::io; - use clap::Subcommand; use jujutsu_lib::dag_walk::topo_order_reverse; use jujutsu_lib::operation::Operation; use crate::cli_util::{user_error, CommandError, CommandHelper}; -use crate::formatter::Formatter; use crate::graphlog::{get_graphlog, Edge}; -use crate::templater::{Template, TimestampRange}; -use crate::time_util::format_timestamp_relative_to_now; +use crate::operation_templater; +use crate::templater::Template as _; use crate::ui::Ui; /// Commands for working with the operation log @@ -50,59 +47,17 @@ fn cmd_op_log( let repo = workspace_command.repo(); let head_op = repo.operation().clone(); let head_op_id = head_op.id().clone(); + + let template_string = command.settings().config().get_string("templates.op_log")?; + let template = operation_templater::parse( + repo, + &template_string, + workspace_command.template_aliases_map(), + )?; + ui.request_pager(); let mut formatter = ui.stdout_formatter(); let formatter = formatter.as_mut(); - struct OpTemplate { - relative_timestamps: bool, - } - impl Template for OpTemplate { - fn format(&self, op: &Operation, formatter: &mut dyn Formatter) -> io::Result<()> { - // TODO: Make this templated - write!(formatter.labeled("id"), "{}", &op.id().hex()[0..12])?; - formatter.write_str(" ")?; - let metadata = &op.store_operation().metadata; - write!( - formatter.labeled("user"), - "{}@{}", - metadata.username, - metadata.hostname - )?; - formatter.write_str(" ")?; - let time_range = TimestampRange { - start: metadata.start_time.clone(), - end: metadata.end_time.clone(), - }; - if self.relative_timestamps { - let start = format_timestamp_relative_to_now(&time_range.start); - write!( - formatter.labeled("time"), - "{start}, lasted {duration}", - duration = time_range.duration() - )?; - } else { - time_range.format(&(), formatter)?; - } - formatter.write_str("\n")?; - write!( - formatter.labeled("description"), - "{}", - &metadata.description - )?; - for (key, value) in &metadata.tags { - write!(formatter.labeled("tags"), "\n{key}: {value}")?; - } - Ok(()) - } - - fn has_content(&self, _: &Operation) -> bool { - true - } - } - let template = OpTemplate { - relative_timestamps: command.settings().oplog_relative_timestamps(), - }; - let mut graph = get_graphlog(command.settings(), formatter.raw()); for op in topo_order_reverse( vec![head_op], @@ -115,16 +70,8 @@ fn cmd_op_log( } let is_head_op = op.id() == &head_op_id; let mut buffer = vec![]; - { - let mut formatter = ui.new_formatter(&mut buffer); - formatter.with_label("op-log", |formatter| { - if is_head_op { - formatter.with_label("head", |formatter| template.format(&op, formatter)) - } else { - template.format(&op, formatter) - } - })?; - } + ui.new_formatter(&mut buffer) + .with_label("op-log", |formatter| template.format(&op, formatter))?; if !buffer.ends_with(b"\n") { buffer.push(b'\n'); } diff --git a/src/config-schema.json b/src/config-schema.json index 2987719aa..56ed4f166 100644 --- a/src/config-schema.json +++ b/src/config-schema.json @@ -51,11 +51,6 @@ "description": "Whether to allow initializing a repo with the native backend", "default": false }, - "oplog-relative-timestamps": { - "type": "boolean", - "description": "Whether to change timestamps in the op log to be rendered as a relative description instead of a full timestamp", - "default": true - }, "default-revset": { "type": "string", "description": "Default set of revisions to show when no explicit revset is given for jj log and similar commands", diff --git a/src/config/templates.toml b/src/config/templates.toml index 59b64bffb..884953997 100644 --- a/src/config/templates.toml +++ b/src/config/templates.toml @@ -26,6 +26,19 @@ label(if(current_working_copy, "working_copy"), ) ''' +op_log = ''' +label(if(current_operation, "head"), + separate(" ", + id.short(), + user, + format_time_range(time), + ) + "\n" + description.first_line() "\n" + tags +) +''' + # Defined as alias to allow 'log -T show' show = 'show' @@ -35,6 +48,8 @@ show = 'show' # Hook points for users to customize the default templates: 'format_short_id(id)' = 'id.shortest(12)' 'format_short_signature(signature)' = 'signature.email()' +'format_time_range(time_range)' = ''' + time_range.start().ago() label("time", ", lasted ") time_range.duration()''' 'format_timestamp(timestamp)' = 'timestamp' # TODO: Add branches, tags, etc diff --git a/src/lib.rs b/src/lib.rs index 2e5aa81f6..e29de0e6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ pub mod diff_util; pub mod formatter; pub mod graphlog; pub mod merge_tools; +pub mod operation_templater; mod progress; pub mod template_parser; pub mod templater; diff --git a/src/operation_templater.rs b/src/operation_templater.rs new file mode 100644 index 000000000..240119003 --- /dev/null +++ b/src/operation_templater.rs @@ -0,0 +1,200 @@ +// 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 std::io; + +use itertools::Itertools as _; +use jujutsu_lib::op_store::{OperationId, OperationMetadata}; +use jujutsu_lib::operation::Operation; +use jujutsu_lib::repo::ReadonlyRepo; + +use crate::formatter::Formatter; +use crate::template_parser::{ + self, CoreTemplatePropertyKind, FunctionCallNode, IntoTemplateProperty, TemplateAliasesMap, + TemplateLanguage, TemplateParseError, TemplateParseResult, +}; +use crate::templater::{ + IntoTemplate, PlainTextFormattedProperty, Template, TemplateProperty, TemplatePropertyFn, + TimestampRange, +}; + +struct OperationTemplateLanguage<'b> { + head_op_id: &'b OperationId, +} + +impl TemplateLanguage<'static> for OperationTemplateLanguage<'_> { + type Context = Operation; + type Property = OperationTemplatePropertyKind; + + template_parser::impl_core_wrap_property_fns!('static, OperationTemplatePropertyKind::Core); + + fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult { + build_operation_keyword(self, name, span) + } + + fn build_method( + &self, + property: Self::Property, + function: &FunctionCallNode, + ) -> TemplateParseResult { + match property { + OperationTemplatePropertyKind::Core(property) => { + template_parser::build_core_method(self, property, function) + } + OperationTemplatePropertyKind::OperationId(property) => { + build_operation_id_method(self, property, function) + } + } + } +} + +impl OperationTemplateLanguage<'_> { + fn wrap_operation_id( + &self, + property: Box>, + ) -> OperationTemplatePropertyKind { + OperationTemplatePropertyKind::OperationId(property) + } +} + +enum OperationTemplatePropertyKind { + Core(CoreTemplatePropertyKind<'static, Operation>), + OperationId(Box>), +} + +impl IntoTemplateProperty<'static, Operation> for OperationTemplatePropertyKind { + fn try_into_boolean(self) -> Option>> { + match self { + OperationTemplatePropertyKind::Core(property) => property.try_into_boolean(), + _ => None, + } + } + + fn try_into_integer(self) -> Option>> { + match self { + OperationTemplatePropertyKind::Core(property) => property.try_into_integer(), + _ => None, + } + } + + fn into_plain_text(self) -> Box> { + match self { + OperationTemplatePropertyKind::Core(property) => property.into_plain_text(), + _ => Box::new(PlainTextFormattedProperty::new(self.into_template())), + } + } +} + +impl IntoTemplate<'static, Operation> for OperationTemplatePropertyKind { + fn into_template(self) -> Box> { + match self { + OperationTemplatePropertyKind::Core(property) => property.into_template(), + OperationTemplatePropertyKind::OperationId(property) => property.into_template(), + } + } +} + +fn build_operation_keyword( + language: &OperationTemplateLanguage, + name: &str, + span: pest::Span, +) -> TemplateParseResult { + fn wrap_fn( + f: impl Fn(&Operation) -> O + 'static, + ) -> Box> { + Box::new(TemplatePropertyFn(f)) + } + fn wrap_metadata_fn( + f: impl Fn(&OperationMetadata) -> O + 'static, + ) -> Box> { + wrap_fn(move |op| f(&op.store_operation().metadata)) + } + + let property = match name { + "current_operation" => { + let head_op_id = language.head_op_id.clone(); + language.wrap_boolean(wrap_fn(move |op| op.id() == &head_op_id)) + } + "description" => { + language.wrap_string(wrap_metadata_fn(|metadata| metadata.description.clone())) + } + "id" => language.wrap_operation_id(wrap_fn(|op| op.id().clone())), + "tags" => language.wrap_string(wrap_metadata_fn(|metadata| { + // TODO: introduce map type + metadata + .tags + .iter() + .map(|(key, value)| format!("{key}: {value}")) + .join("\n") + })), + "time" => language.wrap_timestamp_range(wrap_metadata_fn(|metadata| TimestampRange { + start: metadata.start_time.clone(), + end: metadata.end_time.clone(), + })), + "user" => language.wrap_string(wrap_metadata_fn(|metadata| { + // TODO: introduce dedicated type and provide accessors? + format!("{}@{}", metadata.username, metadata.hostname) + })), + _ => return Err(TemplateParseError::no_such_keyword(name, span)), + }; + Ok(property) +} + +impl Template<()> for OperationId { + fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { + formatter.write_str(&self.hex()) + } + + fn has_content(&self, _: &()) -> bool { + !self.as_bytes().is_empty() + } +} + +fn build_operation_id_method( + language: &OperationTemplateLanguage, + self_property: impl TemplateProperty + 'static, + function: &FunctionCallNode, +) -> TemplateParseResult { + let property = match function.name { + "short" => { + let ([], [len_node]) = template_parser::expect_arguments(function)?; + let len_property = len_node + .map(|node| template_parser::expect_integer_expression(language, node)) + .transpose()?; + language.wrap_string(template_parser::chain_properties( + (self_property, len_property), + TemplatePropertyFn(|(id, len): &(OperationId, Option)| { + let mut hex = id.hex(); + hex.truncate(len.and_then(|l| l.try_into().ok()).unwrap_or(12)); + hex + }), + )) + } + _ => return Err(TemplateParseError::no_such_method("OperationId", function)), + }; + Ok(property) +} + +pub fn parse( + repo: &ReadonlyRepo, + template_text: &str, + aliases_map: &TemplateAliasesMap, +) -> TemplateParseResult>> { + let head_op_id = repo.op_id(); + let language = OperationTemplateLanguage { head_op_id }; + let node = template_parser::parse_template(template_text)?; + let node = template_parser::expand_aliases(node, aliases_map)?; + let expression = template_parser::build_expression(&language, &node)?; + Ok(expression.into_template()) +} diff --git a/tests/test_operations.rs b/tests/test_operations.rs index 8dbe5cf68..b36f88413 100644 --- a/tests/test_operations.rs +++ b/tests/test_operations.rs @@ -33,7 +33,7 @@ fn test_op_log() { "op", "log", "--config-toml", - "ui.oplog-relative-timestamps=false", + "template-aliases.'format_time_range(x)' = 'x'", ], ); insta::assert_snapshot!(&stdout, @r###"