mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-18 02:04:19 +00:00
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:
parent
eafbd977a3
commit
b5f1728ffb
10 changed files with 240 additions and 80 deletions
|
@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Breaking changes
|
### 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
|
### New features
|
||||||
|
|
||||||
* `jj git push --deleted` will remove all locally deleted branches from the remote.
|
* `jj git push --deleted` will remove all locally deleted branches from the remote.
|
||||||
|
|
|
@ -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
|
`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
|
```toml
|
||||||
[ui]
|
[template-aliases]
|
||||||
oplog-relative-timestamps=false
|
'format_time_range(time_range)' = 'time_range.start() " - " time_range.end()'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Author format
|
### Author format
|
||||||
|
|
|
@ -158,12 +158,6 @@ impl UserSettings {
|
||||||
.unwrap_or(false)
|
.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 {
|
pub fn config(&self) -> &config::Config {
|
||||||
&self.config
|
&self.config
|
||||||
}
|
}
|
||||||
|
|
|
@ -811,6 +811,10 @@ impl WorkspaceCommandHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn template_aliases_map(&self) -> &TemplateAliasesMap {
|
||||||
|
&self.template_aliases_map
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_commit_template(
|
pub fn parse_commit_template(
|
||||||
&self,
|
&self,
|
||||||
template_text: &str,
|
template_text: &str,
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use jujutsu_lib::dag_walk::topo_order_reverse;
|
use jujutsu_lib::dag_walk::topo_order_reverse;
|
||||||
use jujutsu_lib::operation::Operation;
|
use jujutsu_lib::operation::Operation;
|
||||||
|
|
||||||
use crate::cli_util::{user_error, CommandError, CommandHelper};
|
use crate::cli_util::{user_error, CommandError, CommandHelper};
|
||||||
use crate::formatter::Formatter;
|
|
||||||
use crate::graphlog::{get_graphlog, Edge};
|
use crate::graphlog::{get_graphlog, Edge};
|
||||||
use crate::templater::{Template, TimestampRange};
|
use crate::operation_templater;
|
||||||
use crate::time_util::format_timestamp_relative_to_now;
|
use crate::templater::Template as _;
|
||||||
use crate::ui::Ui;
|
use crate::ui::Ui;
|
||||||
|
|
||||||
/// Commands for working with the operation log
|
/// Commands for working with the operation log
|
||||||
|
@ -50,59 +47,17 @@ fn cmd_op_log(
|
||||||
let repo = workspace_command.repo();
|
let repo = workspace_command.repo();
|
||||||
let head_op = repo.operation().clone();
|
let head_op = repo.operation().clone();
|
||||||
let head_op_id = head_op.id().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();
|
ui.request_pager();
|
||||||
let mut formatter = ui.stdout_formatter();
|
let mut formatter = ui.stdout_formatter();
|
||||||
let formatter = formatter.as_mut();
|
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());
|
let mut graph = get_graphlog(command.settings(), formatter.raw());
|
||||||
for op in topo_order_reverse(
|
for op in topo_order_reverse(
|
||||||
vec![head_op],
|
vec![head_op],
|
||||||
|
@ -115,16 +70,8 @@ fn cmd_op_log(
|
||||||
}
|
}
|
||||||
let is_head_op = op.id() == &head_op_id;
|
let is_head_op = op.id() == &head_op_id;
|
||||||
let mut buffer = vec![];
|
let mut buffer = vec![];
|
||||||
{
|
ui.new_formatter(&mut buffer)
|
||||||
let mut formatter = ui.new_formatter(&mut buffer);
|
.with_label("op-log", |formatter| template.format(&op, formatter))?;
|
||||||
formatter.with_label("op-log", |formatter| {
|
|
||||||
if is_head_op {
|
|
||||||
formatter.with_label("head", |formatter| template.format(&op, formatter))
|
|
||||||
} else {
|
|
||||||
template.format(&op, formatter)
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
if !buffer.ends_with(b"\n") {
|
if !buffer.ends_with(b"\n") {
|
||||||
buffer.push(b'\n');
|
buffer.push(b'\n');
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,11 +51,6 @@
|
||||||
"description": "Whether to allow initializing a repo with the native backend",
|
"description": "Whether to allow initializing a repo with the native backend",
|
||||||
"default": false
|
"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": {
|
"default-revset": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Default set of revisions to show when no explicit revset is given for jj log and similar commands",
|
"description": "Default set of revisions to show when no explicit revset is given for jj log and similar commands",
|
||||||
|
|
|
@ -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'
|
# Defined as alias to allow 'log -T show'
|
||||||
show = 'show'
|
show = 'show'
|
||||||
|
|
||||||
|
@ -35,6 +48,8 @@ show = 'show'
|
||||||
# Hook points for users to customize the default templates:
|
# Hook points for users to customize the default templates:
|
||||||
'format_short_id(id)' = 'id.shortest(12)'
|
'format_short_id(id)' = 'id.shortest(12)'
|
||||||
'format_short_signature(signature)' = 'signature.email()'
|
'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'
|
'format_timestamp(timestamp)' = 'timestamp'
|
||||||
|
|
||||||
# TODO: Add branches, tags, etc
|
# TODO: Add branches, tags, etc
|
||||||
|
|
|
@ -23,6 +23,7 @@ pub mod diff_util;
|
||||||
pub mod formatter;
|
pub mod formatter;
|
||||||
pub mod graphlog;
|
pub mod graphlog;
|
||||||
pub mod merge_tools;
|
pub mod merge_tools;
|
||||||
|
pub mod operation_templater;
|
||||||
mod progress;
|
mod progress;
|
||||||
pub mod template_parser;
|
pub mod template_parser;
|
||||||
pub mod templater;
|
pub mod templater;
|
||||||
|
|
200
src/operation_templater.rs
Normal file
200
src/operation_templater.rs
Normal 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())
|
||||||
|
}
|
|
@ -33,7 +33,7 @@ fn test_op_log() {
|
||||||
"op",
|
"op",
|
||||||
"log",
|
"log",
|
||||||
"--config-toml",
|
"--config-toml",
|
||||||
"ui.oplog-relative-timestamps=false",
|
"template-aliases.'format_time_range(x)' = 'x'",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
insta::assert_snapshot!(&stdout, @r###"
|
insta::assert_snapshot!(&stdout, @r###"
|
||||||
|
|
Loading…
Reference in a new issue