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)'
This commit is contained in:
Yuya Nishihara 2023-02-19 17:05:50 +09:00
parent eafbd977a3
commit b5f1728ffb
10 changed files with 240 additions and 80 deletions

View file

@ -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.

View file

@ -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

View file

@ -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
}

View file

@ -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,

View file

@ -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<Operation> 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');
}

View file

@ -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",

View file

@ -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

View file

@ -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;

200
src/operation_templater.rs Normal file
View file

@ -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<Self::Property> {
build_operation_keyword(self, name, span)
}
fn build_method(
&self,
property: Self::Property,
function: &FunctionCallNode,
) -> TemplateParseResult<Self::Property> {
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<dyn TemplateProperty<Operation, Output = OperationId>>,
) -> OperationTemplatePropertyKind {
OperationTemplatePropertyKind::OperationId(property)
}
}
enum OperationTemplatePropertyKind {
Core(CoreTemplatePropertyKind<'static, Operation>),
OperationId(Box<dyn TemplateProperty<Operation, Output = OperationId>>),
}
impl IntoTemplateProperty<'static, Operation> for OperationTemplatePropertyKind {
fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Operation, Output = bool>>> {
match self {
OperationTemplatePropertyKind::Core(property) => property.try_into_boolean(),
_ => None,
}
}
fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Operation, Output = i64>>> {
match self {
OperationTemplatePropertyKind::Core(property) => property.try_into_integer(),
_ => None,
}
}
fn into_plain_text(self) -> Box<dyn TemplateProperty<Operation, Output = String>> {
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<dyn Template<Operation>> {
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<OperationTemplatePropertyKind> {
fn wrap_fn<O>(
f: impl Fn(&Operation) -> O + 'static,
) -> Box<dyn TemplateProperty<Operation, Output = O>> {
Box::new(TemplatePropertyFn(f))
}
fn wrap_metadata_fn<O>(
f: impl Fn(&OperationMetadata) -> O + 'static,
) -> Box<dyn TemplateProperty<Operation, Output = O>> {
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<Operation, Output = OperationId> + 'static,
function: &FunctionCallNode,
) -> TemplateParseResult<OperationTemplatePropertyKind> {
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<i64>)| {
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<Box<dyn Template<Operation>>> {
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())
}

View file

@ -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###"