ok/jj
1
0
Fork 0
forked from mirrors/jj
jj/src/template_parser.rs
Martin von Zweigbergk 15132a1166 cli: replace git refs by branches and tags in log output
Now that our own branches and tags are updated when git refs are
updated and the user can use them to specify revisions, we can start
displaying them instead of the git refs. This commit adds new
`branches` and `tags` template keywords and updates the default
templates to use them instead of `git_refs`.
2021-08-04 11:53:37 -07:00

426 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 jujutsu_lib::commit::Commit;
use jujutsu_lib::repo::RepoRef;
use jujutsu_lib::store::{CommitId, Signature};
use pest::iterators::{Pair, Pairs};
use pest::Parser;
use crate::formatter::PlainTextFormatter;
use crate::templater::{
AuthorProperty, BranchProperty, ChangeIdProperty, CommitIdKeyword, CommitterProperty,
ConditionalTemplate, ConflictProperty, ConstantTemplateProperty, CurrentCheckoutProperty,
DescriptionProperty, DivergentProperty, DynamicLabelTemplate, GitRefsProperty, LabelTemplate,
ListTemplate, LiteralTemplate, ObsoleteProperty, OpenProperty, OrphanProperty, PrunedProperty,
StringPropertyTemplate, TagProperty, 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 })),
"branches" => Property::String(Box::new(BranchProperty { repo })),
"tags" => Property::String(Box::new(TagProperty { 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 formatter = PlainTextFormatter::new(writer);
label_template.format(commit, &mut formatter).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)
}