ok/jj
1
0
Fork 0
forked from mirrors/jj
jj/src/template_parser.rs
Martin von Zweigbergk ba4ac44719 cli: replace committer email by author timestamp in log template
I've often missed not having the timestamp there. It gets too long
with both email and timestamp for both author and committer, so I
removed the committer email to make room for the author timestamp.
2021-04-26 21:30:14 -07:00

424 lines
16 KiB
Rust

// Copyright 2020 Google LLC
//
// 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.
extern crate pest;
use chrono::{FixedOffset, TimeZone, Utc};
use jujube_lib::commit::Commit;
use jujube_lib::repo::RepoRef;
use jujube_lib::store::{CommitId, Signature};
use pest::iterators::{Pair, Pairs};
use pest::Parser;
use crate::styler::PlainTextStyler;
use crate::templater::{
AuthorProperty, ChangeIdProperty, CommitIdKeyword, CommitterProperty, ConditionalTemplate,
ConflictProperty, ConstantTemplateProperty, CurrentCheckoutProperty, DescriptionProperty,
DivergentProperty, DynamicLabelTemplate, GitRefsProperty, LabelTemplate, ListTemplate,
LiteralTemplate, ObsoleteProperty, OpenProperty, OrphanProperty, PrunedProperty,
StringPropertyTemplate, Template, TemplateFunction, TemplateProperty,
};
#[derive(Parser)]
#[grammar = "template.pest"]
pub struct TemplateParser;
fn parse_string_literal(pair: Pair<Rule>) -> String {
assert_eq!(pair.as_rule(), Rule::literal);
let mut result = String::new();
for part in pair.into_inner() {
match part.as_rule() {
Rule::raw_literal => {
result.push_str(part.as_str());
}
Rule::escape => match part.as_str().as_bytes()[1] as char {
'"' => result.push('"'),
'\\' => result.push('\\'),
'n' => result.push('\n'),
char => panic!("invalid escape: \\{:?}", char),
},
_ => panic!("unexpected part of string: {:?}", part),
}
}
result
}
struct StringShort;
impl TemplateProperty<String, String> for StringShort {
fn extract(&self, context: &String) -> String {
context.chars().take(12).collect()
}
}
struct StringFirstLine;
impl TemplateProperty<String, String> for StringFirstLine {
fn extract(&self, context: &String) -> String {
context.lines().next().unwrap().to_string()
}
}
struct CommitIdShortest;
impl TemplateProperty<CommitId, String> for CommitIdShortest {
fn extract(&self, context: &CommitId) -> String {
CommitIdKeyword::shortest_format(context.clone())
}
}
struct SignatureName;
impl TemplateProperty<Signature, String> for SignatureName {
fn extract(&self, context: &Signature) -> String {
context.name.clone()
}
}
struct SignatureEmail;
impl TemplateProperty<Signature, String> for SignatureEmail {
fn extract(&self, context: &Signature) -> String {
context.email.clone()
}
}
struct SignatureTimestamp;
impl TemplateProperty<Signature, String> for SignatureTimestamp {
fn extract(&self, context: &Signature) -> String {
let utc = Utc
.timestamp(
context.timestamp.timestamp.0 as i64 / 1000,
(context.timestamp.timestamp.0 % 1000) as u32 * 1000000,
)
.with_timezone(&FixedOffset::east(context.timestamp.tz_offset * 60));
utc.format("%Y-%m-%d %H:%M:%S.%3f %:z").to_string()
}
}
fn parse_method_chain<'a, I: 'a>(
pair: Pair<Rule>,
input_property: Property<'a, I>,
) -> Property<'a, I> {
assert_eq!(pair.as_rule(), Rule::maybe_method);
if pair.as_str().is_empty() {
input_property
} else {
let method = pair.into_inner().next().unwrap();
match input_property {
Property::String(property) => {
let next_method = parse_string_method(method);
next_method.after(property)
}
Property::Boolean(property) => {
let next_method = parse_boolean_method(method);
next_method.after(property)
}
Property::CommitId(property) => {
let next_method = parse_commit_id_method(method);
next_method.after(property)
}
Property::Signature(property) => {
let next_method = parse_signature_method(method);
next_method.after(property)
}
}
}
}
fn parse_string_method<'a>(method: Pair<Rule>) -> Property<'a, String> {
assert_eq!(method.as_rule(), Rule::method);
let mut inner = method.into_inner();
let name = inner.next().unwrap();
// TODO: validate arguments
let this_function = match name.as_str() {
"short" => Property::String(Box::new(StringShort)),
"first_line" => Property::String(Box::new(StringFirstLine)),
name => panic!("no such string method: {}", name),
};
let chain_method = inner.last().unwrap();
parse_method_chain(chain_method, this_function)
}
fn parse_boolean_method<'a>(method: Pair<Rule>) -> Property<'a, bool> {
assert_eq!(method.as_rule(), Rule::maybe_method);
let mut inner = method.into_inner();
let name = inner.next().unwrap();
// TODO: validate arguments
panic!("no such boolean method: {}", name.as_str());
}
// TODO: pass a context to the returned function (we need the repo to find the
// shortest unambiguous prefix)
fn parse_commit_id_method<'a>(method: Pair<Rule>) -> Property<'a, CommitId> {
assert_eq!(method.as_rule(), Rule::method);
let mut inner = method.into_inner();
let name = inner.next().unwrap();
// TODO: validate arguments
let this_function = match name.as_str() {
"short" => Property::String(Box::new(CommitIdShortest)),
name => panic!("no such commit id method: {}", name),
};
let chain_method = inner.last().unwrap();
parse_method_chain(chain_method, this_function)
}
fn parse_signature_method<'a>(method: Pair<Rule>) -> Property<'a, Signature> {
assert_eq!(method.as_rule(), Rule::method);
let mut inner = method.into_inner();
let name = inner.next().unwrap();
// TODO: validate arguments
let this_function: Property<'a, Signature> = match name.as_str() {
// TODO: Automatically label these too (so author.name() gets
// labels "author" *and" "name". Perhaps drop parentheses
// from syntax for that? Or maybe this should be using
// syntax for nested records (e.g.
// `author % (name "<" email ">")`)?
"name" => Property::String(Box::new(SignatureName)),
"email" => Property::String(Box::new(SignatureEmail)),
"timestamp" => Property::String(Box::new(SignatureTimestamp)),
name => panic!("no such commit id method: {}", name),
};
let chain_method = inner.last().unwrap();
parse_method_chain(chain_method, this_function)
}
enum Property<'a, I> {
String(Box<dyn TemplateProperty<I, String> + 'a>),
Boolean(Box<dyn TemplateProperty<I, bool> + 'a>),
CommitId(Box<dyn TemplateProperty<I, CommitId> + 'a>),
Signature(Box<dyn TemplateProperty<I, Signature> + 'a>),
}
impl<'a, I: 'a> Property<'a, I> {
fn after<C: 'a>(self, first: Box<dyn TemplateProperty<C, I> + 'a>) -> Property<'a, C> {
match self {
Property::String(property) => Property::String(Box::new(TemplateFunction::new(
first,
Box::new(move |value| property.extract(&value)),
))),
Property::Boolean(property) => Property::Boolean(Box::new(TemplateFunction::new(
first,
Box::new(move |value| property.extract(&value)),
))),
Property::CommitId(property) => Property::CommitId(Box::new(TemplateFunction::new(
first,
Box::new(move |value| property.extract(&value)),
))),
Property::Signature(property) => Property::Signature(Box::new(TemplateFunction::new(
first,
Box::new(move |value| property.extract(&value)),
))),
}
}
}
fn parse_commit_keyword<'a>(repo: RepoRef<'a>, pair: Pair<Rule>) -> (Property<'a, Commit>, String) {
assert_eq!(pair.as_rule(), Rule::identifier);
let property = match pair.as_str() {
"description" => Property::String(Box::new(DescriptionProperty)),
"change_id" => Property::String(Box::new(ChangeIdProperty)),
"commit_id" => Property::CommitId(Box::new(CommitIdKeyword)),
"author" => Property::Signature(Box::new(AuthorProperty)),
"committer" => Property::Signature(Box::new(CommitterProperty)),
"open" => Property::Boolean(Box::new(OpenProperty)),
"pruned" => Property::Boolean(Box::new(PrunedProperty)),
"current_checkout" => Property::Boolean(Box::new(CurrentCheckoutProperty { repo })),
"git_refs" => Property::String(Box::new(GitRefsProperty { repo })),
"obsolete" => Property::Boolean(Box::new(ObsoleteProperty { repo })),
"orphan" => Property::Boolean(Box::new(OrphanProperty { repo })),
"divergent" => Property::Boolean(Box::new(DivergentProperty { repo })),
"conflict" => Property::Boolean(Box::new(ConflictProperty)),
name => panic!("unexpected identifier: {}", name),
};
(property, pair.as_str().to_string())
}
fn coerce_to_string<'a, I: 'a>(
property: Property<'a, I>,
) -> Box<dyn TemplateProperty<I, String> + 'a> {
match property {
Property::String(property) => property,
Property::Boolean(property) => Box::new(TemplateFunction::new(
property,
Box::new(|value| String::from(if value { "true" } else { "false" })),
)),
Property::CommitId(property) => Box::new(TemplateFunction::new(
property,
Box::new(CommitIdKeyword::default_format),
)),
Property::Signature(property) => Box::new(TemplateFunction::new(
property,
Box::new(|signature| signature.name),
)),
}
}
fn parse_boolean_commit_property<'a>(
repo: RepoRef<'a>,
pair: Pair<Rule>,
) -> Box<dyn TemplateProperty<Commit, bool> + 'a> {
let mut inner = pair.into_inner();
let pair = inner.next().unwrap();
let _method = inner.next().unwrap();
assert!(inner.next().is_none());
match pair.as_rule() {
Rule::identifier => match parse_commit_keyword(repo, pair.clone()).0 {
Property::Boolean(property) => property,
_ => panic!("cannot yet use this as boolean: {:?}", pair),
},
_ => panic!("cannot yet use this as boolean: {:?}", pair),
}
}
fn parse_commit_term<'a>(repo: RepoRef<'a>, pair: Pair<Rule>) -> Box<dyn Template<Commit> + 'a> {
assert_eq!(pair.as_rule(), Rule::term);
if pair.as_str().is_empty() {
Box::new(LiteralTemplate(String::new()))
} else {
let mut inner = pair.into_inner();
let expr = inner.next().unwrap();
let maybe_method = inner.next().unwrap();
assert!(inner.next().is_none());
match expr.as_rule() {
Rule::literal => {
let text = parse_string_literal(expr);
if maybe_method.as_str().is_empty() {
Box::new(LiteralTemplate(text))
} else {
let input_property =
Property::String(Box::new(ConstantTemplateProperty { output: text }));
let property = parse_method_chain(maybe_method, input_property);
let string_property = coerce_to_string(property);
Box::new(StringPropertyTemplate {
property: string_property,
})
}
}
Rule::identifier => {
let (term_property, labels) = parse_commit_keyword(repo, expr);
let property = parse_method_chain(maybe_method, term_property);
let string_property = coerce_to_string(property);
Box::new(LabelTemplate::new(
Box::new(StringPropertyTemplate {
property: string_property,
}),
labels,
))
}
Rule::function => {
let mut inner = expr.into_inner();
let name = inner.next().unwrap().as_str();
match name {
"label" => {
let label_pair = inner.next().unwrap();
let label_template = parse_commit_template_rule(
repo,
label_pair.into_inner().next().unwrap(),
);
let arg_template = match inner.next() {
None => panic!("label() requires two arguments"),
Some(pair) => pair,
};
if inner.next().is_some() {
panic!("label() accepts only two arguments")
}
let content: Box<dyn Template<Commit> + 'a> =
parse_commit_template_rule(repo, arg_template);
let get_labels = move |commit: &Commit| -> String {
let mut buf: Vec<u8> = vec![];
{
let writer = Box::new(&mut buf);
let mut styler = PlainTextStyler::new(writer);
label_template.format(commit, &mut styler).unwrap();
}
String::from_utf8(buf).unwrap()
};
Box::new(DynamicLabelTemplate::new(content, Box::new(get_labels)))
}
"if" => {
let condition_pair = inner.next().unwrap();
let condition_template = condition_pair.into_inner().next().unwrap();
let condition = parse_boolean_commit_property(repo, condition_template);
let true_template = match inner.next() {
None => panic!("if() requires at least two arguments"),
Some(pair) => parse_commit_template_rule(repo, pair),
};
let false_template = inner
.next()
.map(|pair| parse_commit_template_rule(repo, pair));
if inner.next().is_some() {
panic!("if() accepts at most three arguments")
}
Box::new(ConditionalTemplate::new(
condition,
true_template,
false_template,
))
}
name => panic!("function {} not implemented", name),
}
}
other => panic!("unexpected term: {:?}", other),
}
}
}
fn parse_commit_template_rule<'a>(
repo: RepoRef<'a>,
pair: Pair<Rule>,
) -> Box<dyn Template<Commit> + 'a> {
match pair.as_rule() {
Rule::template => {
let mut inner = pair.into_inner();
let formatter = parse_commit_template_rule(repo, inner.next().unwrap());
assert!(inner.next().is_none());
formatter
}
Rule::term => parse_commit_term(repo, pair),
Rule::list => {
let mut formatters: Vec<Box<dyn Template<Commit>>> = vec![];
for inner_pair in pair.into_inner() {
formatters.push(parse_commit_template_rule(repo, inner_pair));
}
Box::new(ListTemplate(formatters))
}
_ => Box::new(LiteralTemplate(String::new())),
}
}
pub fn parse_commit_template<'a>(
repo: RepoRef<'a>,
template_text: &str,
) -> Box<dyn Template<Commit> + 'a> {
let mut pairs: Pairs<Rule> = TemplateParser::parse(Rule::template, template_text).unwrap();
let first_pair = pairs.next().unwrap();
assert!(pairs.next().is_none());
if first_pair.as_span().end() != template_text.len() {
panic!(
"failed to parse template past position {}",
first_pair.as_span().end()
);
}
parse_commit_template_rule(repo, first_pair)
}