// Copyright 2020-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. //! Template environment for `jj log`, `jj evolog` and similar. use std::any::Any; use std::cmp::Ordering; use std::cmp::max; use std::collections::HashMap; use std::fmt; use std::fmt::Display; use std::io; use std::rc::Rc; use std::sync::Arc; use bstr::BString; use futures::StreamExt as _; use futures::TryStreamExt as _; use futures::stream::BoxStream; use itertools::Itertools as _; use jj_lib::backend::BackendResult; use jj_lib::backend::ChangeId; use jj_lib::backend::CommitId; use jj_lib::backend::Timestamp; use jj_lib::backend::TreeValue; use jj_lib::commit::Commit; use jj_lib::conflict_labels::ConflictLabels; use jj_lib::conflicts; use jj_lib::conflicts::ConflictMarkerStyle; use jj_lib::copies::CopiesTreeDiffEntry; use jj_lib::copies::CopiesTreeDiffEntryPath; use jj_lib::copies::CopyRecords; use jj_lib::evolution::CommitEvolutionEntry; use jj_lib::extensions_map::ExtensionsMap; use jj_lib::fileset; use jj_lib::fileset::FilesetDiagnostics; use jj_lib::fileset::FilesetExpression; use jj_lib::fileset::FilesetParseContext; use jj_lib::id_prefix::IdPrefixContext; use jj_lib::id_prefix::IdPrefixIndex; use jj_lib::index::IndexResult; use jj_lib::matchers::Matcher; use jj_lib::merge::Diff; use jj_lib::merge::MergedTreeValue; use jj_lib::merged_tree::MergedTree; use jj_lib::object_id::ObjectId as _; use jj_lib::op_store::LocalRemoteRefTarget; use jj_lib::op_store::OperationId; use jj_lib::op_store::RefTarget; use jj_lib::op_store::RemoteRef; use jj_lib::ref_name::RefName; use jj_lib::ref_name::WorkspaceName; use jj_lib::ref_name::WorkspaceNameBuf; use jj_lib::repo::Repo; use jj_lib::repo::RepoLoader; use jj_lib::repo_path::RepoPathBuf; use jj_lib::repo_path::RepoPathUiConverter; use jj_lib::revset; use jj_lib::revset::Revset; use jj_lib::revset::RevsetContainingFn; use jj_lib::revset::RevsetDiagnostics; use jj_lib::revset::RevsetParseContext; use jj_lib::revset::UserRevsetExpression; use jj_lib::rewrite::rebase_to_dest_parent; use jj_lib::settings::UserSettings; use jj_lib::signing::SigStatus; use jj_lib::signing::SignError; use jj_lib::signing::SignResult; use jj_lib::signing::Verification; use jj_lib::store::Store; use jj_lib::trailer; use jj_lib::trailer::Trailer; use jj_lib::workspace::DefaultWorkspaceLoaderFactory; use jj_lib::workspace::WorkspaceLoaderFactory as _; use jj_lib::workspace_store::SimpleWorkspaceStore; use jj_lib::workspace_store::WorkspaceStore as _; use once_cell::unsync::OnceCell; use pollster::FutureExt as _; use serde::Serialize as _; use crate::diff_util; use crate::diff_util::DiffStatEntry; use crate::diff_util::DiffStats; use crate::formatter::Formatter; use crate::git_util; use crate::operation_templater; use crate::operation_templater::OperationTemplateBuildFnTable; use crate::operation_templater::OperationTemplateEnvironment; use crate::operation_templater::OperationTemplatePropertyKind; use crate::operation_templater::OperationTemplatePropertyVar; use crate::revset_util; use crate::template_builder; use crate::template_builder::BuildContext; use crate::template_builder::CoreTemplateBuildFnTable; use crate::template_builder::CoreTemplatePropertyKind; use crate::template_builder::CoreTemplatePropertyVar; use crate::template_builder::TemplateBuildFunctionFnMap; use crate::template_builder::TemplateBuildMethodFnMap; use crate::template_builder::TemplateLanguage; use crate::template_builder::expect_stringify_expression; use crate::template_builder::merge_fn_map; use crate::template_parser; use crate::template_parser::ExpressionNode; use crate::template_parser::FunctionCallNode; use crate::template_parser::TemplateDiagnostics; use crate::template_parser::TemplateParseError; use crate::template_parser::TemplateParseResult; use crate::templater; use crate::templater::BoxedAnyProperty; use crate::templater::BoxedSerializeProperty; use crate::templater::BoxedTemplateProperty; use crate::templater::Literal; use crate::templater::SizeHint; use crate::templater::Template; use crate::templater::TemplateFormatter; use crate::templater::TemplatePropertyError; use crate::templater::TemplatePropertyExt as _; pub trait CommitTemplateLanguageExtension { fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo>; fn build_cache_extensions(&self, extensions: &mut ExtensionsMap); } /// Template environment for `jj log` and `jj evolog`. pub struct CommitTemplateLanguage<'repo> { repo: &'repo dyn Repo, path_converter: &'repo RepoPathUiConverter, workspace_name: WorkspaceNameBuf, // RevsetParseContext doesn't borrow a repo, but we'll need 'repo lifetime // anyway to capture it to evaluate dynamically-constructed user expression // such as `revset("ancestors(" ++ commit_id ++ ")")`. // TODO: Maybe refactor context structs in a way that // Revset/FilesetParseContext are constructed from a common resource? // RepoPathUiConverter and WorkspaceName are contained in RevsetParseContext // for example. revset_parse_context: RevsetParseContext<'repo>, id_prefix_context: &'repo IdPrefixContext, immutable_expression: Arc, conflict_marker_style: ConflictMarkerStyle, build_fn_table: CommitTemplateBuildFnTable<'repo>, keyword_cache: CommitKeywordCache<'repo>, cache_extensions: ExtensionsMap, } impl<'repo> CommitTemplateLanguage<'repo> { /// Sets up environment where commit template will be transformed to /// evaluation tree. #[expect(clippy::too_many_arguments)] pub fn new( repo: &'repo dyn Repo, path_converter: &'repo RepoPathUiConverter, workspace_name: &WorkspaceName, revset_parse_context: RevsetParseContext<'repo>, id_prefix_context: &'repo IdPrefixContext, immutable_expression: Arc, conflict_marker_style: ConflictMarkerStyle, extensions: &[impl AsRef], ) -> Self { let mut build_fn_table = CommitTemplateBuildFnTable::builtin(); let mut cache_extensions = ExtensionsMap::empty(); for extension in extensions { build_fn_table.merge(extension.as_ref().build_fn_table()); extension .as_ref() .build_cache_extensions(&mut cache_extensions); } CommitTemplateLanguage { repo, path_converter, workspace_name: workspace_name.to_owned(), revset_parse_context, id_prefix_context, immutable_expression, conflict_marker_style, build_fn_table, keyword_cache: CommitKeywordCache::default(), cache_extensions, } } fn fileset_parse_context(&self) -> FilesetParseContext<'repo> { FilesetParseContext { aliases_map: self.revset_parse_context.fileset_aliases_map, path_converter: self.path_converter, } } } impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { type Property = CommitTemplatePropertyKind<'repo>; fn settings(&self) -> &UserSettings { self.repo.base_repo().settings() } fn build_function( &self, diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, function: &FunctionCallNode, ) -> TemplateParseResult { let table = &self.build_fn_table.core; table.build_function(self, diagnostics, build_ctx, function) } fn build_method( &self, diagnostics: &mut TemplateDiagnostics, build_ctx: &BuildContext, property: Self::Property, function: &FunctionCallNode, ) -> TemplateParseResult { let type_name = property.type_name(); match property { CommitTemplatePropertyKind::Core(property) => { let table = &self.build_fn_table.core; table.build_method(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::Operation(property) => { let table = &self.build_fn_table.operation; table.build_method(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::Commit(property) => { let table = &self.build_fn_table.commit_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::CommitOpt(property) => { let type_name = "Commit"; let table = &self.build_fn_table.commit_methods; let build = template_parser::lookup_method(type_name, table, function)?; let inner_property = property.try_unwrap(type_name).into_dyn(); build(self, diagnostics, build_ctx, inner_property, function) } CommitTemplatePropertyKind::CommitList(property) => { let table = &self.build_fn_table.commit_list_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::CommitEvolutionEntry(property) => { let table = &self.build_fn_table.commit_evolution_entry_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::CommitRef(property) => { let table = &self.build_fn_table.commit_ref_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::CommitRefOpt(property) => { let type_name = "CommitRef"; let table = &self.build_fn_table.commit_ref_methods; let build = template_parser::lookup_method(type_name, table, function)?; let inner_property = property.try_unwrap(type_name).into_dyn(); build(self, diagnostics, build_ctx, inner_property, function) } CommitTemplatePropertyKind::CommitRefList(property) => { let table = &self.build_fn_table.commit_ref_list_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::WorkspaceRef(property) => { let table = &self.build_fn_table.workspace_ref_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::WorkspaceRefOpt(property) => { let type_name = "WorkspaceRef"; let table = &self.build_fn_table.workspace_ref_methods; let build = template_parser::lookup_method(type_name, table, function)?; let inner_property = property.try_unwrap(type_name).into_dyn(); build(self, diagnostics, build_ctx, inner_property, function) } CommitTemplatePropertyKind::WorkspaceRefList(property) => { let table = &self.build_fn_table.workspace_ref_list_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::RefSymbol(property) => { let table = &self.build_fn_table.core.string_methods; let build = template_parser::lookup_method(type_name, table, function)?; let inner_property = property.map(|RefSymbolBuf(s)| s).into_dyn(); build(self, diagnostics, build_ctx, inner_property, function) } CommitTemplatePropertyKind::RefSymbolOpt(property) => { let type_name = "RefSymbol"; let table = &self.build_fn_table.core.string_methods; let build = template_parser::lookup_method(type_name, table, function)?; let inner_property = property .try_unwrap(type_name) .map(|RefSymbolBuf(s)| s) .into_dyn(); build(self, diagnostics, build_ctx, inner_property, function) } CommitTemplatePropertyKind::RepoPath(property) => { let table = &self.build_fn_table.repo_path_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::RepoPathOpt(property) => { let type_name = "RepoPath"; let table = &self.build_fn_table.repo_path_methods; let build = template_parser::lookup_method(type_name, table, function)?; let inner_property = property.try_unwrap(type_name).into_dyn(); build(self, diagnostics, build_ctx, inner_property, function) } CommitTemplatePropertyKind::ChangeId(property) => { let table = &self.build_fn_table.change_id_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::CommitId(property) => { let table = &self.build_fn_table.commit_id_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::ShortestIdPrefix(property) => { let table = &self.build_fn_table.shortest_id_prefix_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::TreeDiff(property) => { let table = &self.build_fn_table.tree_diff_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::TreeDiffEntry(property) => { let table = &self.build_fn_table.tree_diff_entry_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::TreeDiffEntryList(property) => { let table = &self.build_fn_table.tree_diff_entry_list_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::TreeEntry(property) => { let table = &self.build_fn_table.tree_entry_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::TreeEntryList(property) => { let table = &self.build_fn_table.tree_entry_list_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::DiffStats(property) => { let table = &self.build_fn_table.diff_stats_methods; let build = template_parser::lookup_method(type_name, table, function)?; // Strip off formatting parameters which are needed only for the // default template output. let property = property.map(|formatted| formatted.stats).into_dyn(); build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::DiffStatEntry(property) => { let table = &self.build_fn_table.diff_stat_entry_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::DiffStatEntryList(property) => { let table = &self.build_fn_table.diff_stat_entry_list_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::CryptographicSignatureOpt(property) => { let type_name = "CryptographicSignature"; let table = &self.build_fn_table.cryptographic_signature_methods; let build = template_parser::lookup_method(type_name, table, function)?; let inner_property = property.try_unwrap(type_name).into_dyn(); build(self, diagnostics, build_ctx, inner_property, function) } CommitTemplatePropertyKind::AnnotationLine(property) => { let type_name = "AnnotationLine"; let table = &self.build_fn_table.annotation_line_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::Trailer(property) => { let table = &self.build_fn_table.trailer_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } CommitTemplatePropertyKind::TrailerList(property) => { let table = &self.build_fn_table.trailer_list_methods; let build = template_parser::lookup_method(type_name, table, function)?; build(self, diagnostics, build_ctx, property, function) } } } } // If we need to add multiple languages that support Commit types, this can be // turned into a trait which extends TemplateLanguage. impl<'repo> CommitTemplateLanguage<'repo> { pub fn repo(&self) -> &'repo dyn Repo { self.repo } pub fn workspace_name(&self) -> &WorkspaceName { &self.workspace_name } pub fn keyword_cache(&self) -> &CommitKeywordCache<'repo> { &self.keyword_cache } pub fn cache_extension(&self) -> Option<&T> { self.cache_extensions.get::() } } impl OperationTemplateEnvironment for CommitTemplateLanguage<'_> { fn repo_loader(&self) -> &RepoLoader { self.repo.base_repo().loader() } fn current_op_id(&self) -> Option<&OperationId> { // TODO: Maybe return None if the repo is a MutableRepo? Some(self.repo.base_repo().op_id()) } } pub enum CommitTemplatePropertyKind<'repo> { Core(CoreTemplatePropertyKind<'repo>), Operation(OperationTemplatePropertyKind<'repo>), Commit(BoxedTemplateProperty<'repo, Commit>), CommitOpt(BoxedTemplateProperty<'repo, Option>), CommitList(BoxedTemplateProperty<'repo, Vec>), CommitEvolutionEntry(BoxedTemplateProperty<'repo, CommitEvolutionEntry>), CommitRef(BoxedTemplateProperty<'repo, Rc>), CommitRefOpt(BoxedTemplateProperty<'repo, Option>>), CommitRefList(BoxedTemplateProperty<'repo, Vec>>), WorkspaceRef(BoxedTemplateProperty<'repo, WorkspaceRef>), WorkspaceRefOpt(BoxedTemplateProperty<'repo, Option>), WorkspaceRefList(BoxedTemplateProperty<'repo, Vec>), RefSymbol(BoxedTemplateProperty<'repo, RefSymbolBuf>), RefSymbolOpt(BoxedTemplateProperty<'repo, Option>), RepoPath(BoxedTemplateProperty<'repo, RepoPathBuf>), RepoPathOpt(BoxedTemplateProperty<'repo, Option>), ChangeId(BoxedTemplateProperty<'repo, ChangeId>), CommitId(BoxedTemplateProperty<'repo, CommitId>), ShortestIdPrefix(BoxedTemplateProperty<'repo, ShortestIdPrefix>), TreeDiff(BoxedTemplateProperty<'repo, TreeDiff>), TreeDiffEntry(BoxedTemplateProperty<'repo, TreeDiffEntry>), TreeDiffEntryList(BoxedTemplateProperty<'repo, Vec>), TreeEntry(BoxedTemplateProperty<'repo, TreeEntry>), TreeEntryList(BoxedTemplateProperty<'repo, Vec>), DiffStats(BoxedTemplateProperty<'repo, DiffStatsFormatted<'repo>>), DiffStatEntry(BoxedTemplateProperty<'repo, DiffStatEntry>), DiffStatEntryList(BoxedTemplateProperty<'repo, Vec>), CryptographicSignatureOpt(BoxedTemplateProperty<'repo, Option>), AnnotationLine(BoxedTemplateProperty<'repo, AnnotationLine>), Trailer(BoxedTemplateProperty<'repo, Trailer>), TrailerList(BoxedTemplateProperty<'repo, Vec>), } template_builder::impl_core_property_wrappers!(<'repo> CommitTemplatePropertyKind<'repo> => Core); operation_templater::impl_operation_property_wrappers!(<'repo> CommitTemplatePropertyKind<'repo> => Operation); template_builder::impl_property_wrappers!(<'repo> CommitTemplatePropertyKind<'repo> { Commit(Commit), CommitOpt(Option), CommitList(Vec), CommitEvolutionEntry(CommitEvolutionEntry), CommitRef(Rc), CommitRefOpt(Option>), CommitRefList(Vec>), WorkspaceRef(WorkspaceRef), WorkspaceRefOpt(Option), WorkspaceRefList(Vec), RefSymbol(RefSymbolBuf), RefSymbolOpt(Option), RepoPath(RepoPathBuf), RepoPathOpt(Option), ChangeId(ChangeId), CommitId(CommitId), ShortestIdPrefix(ShortestIdPrefix), TreeDiff(TreeDiff), TreeDiffEntry(TreeDiffEntry), TreeDiffEntryList(Vec), TreeEntry(TreeEntry), TreeEntryList(Vec), DiffStats(DiffStatsFormatted<'repo>), DiffStatEntry(DiffStatEntry), DiffStatEntryList(Vec), CryptographicSignatureOpt(Option), AnnotationLine(AnnotationLine), Trailer(Trailer), TrailerList(Vec), }); impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> { fn wrap_template(template: Box) -> Self { Self::Core(CoreTemplatePropertyKind::wrap_template(template)) } fn wrap_any(property: BoxedAnyProperty<'repo>) -> Self { Self::Core(CoreTemplatePropertyKind::wrap_any(property)) } fn wrap_any_list(property: BoxedAnyProperty<'repo>) -> Self { Self::Core(CoreTemplatePropertyKind::wrap_any_list(property)) } fn type_name(&self) -> &'static str { match self { Self::Core(property) => property.type_name(), Self::Operation(property) => property.type_name(), Self::Commit(_) => "Commit", Self::CommitOpt(_) => "Option", Self::CommitList(_) => "List", Self::CommitEvolutionEntry(_) => "CommitEvolutionEntry", Self::CommitRef(_) => "CommitRef", Self::CommitRefOpt(_) => "Option", Self::CommitRefList(_) => "List", Self::WorkspaceRef(_) => "WorkspaceRef", Self::WorkspaceRefOpt(_) => "Option", Self::WorkspaceRefList(_) => "List", Self::RefSymbol(_) => "RefSymbol", Self::RefSymbolOpt(_) => "Option", Self::RepoPath(_) => "RepoPath", Self::RepoPathOpt(_) => "Option", Self::ChangeId(_) => "ChangeId", Self::CommitId(_) => "CommitId", Self::ShortestIdPrefix(_) => "ShortestIdPrefix", Self::TreeDiff(_) => "TreeDiff", Self::TreeDiffEntry(_) => "TreeDiffEntry", Self::TreeDiffEntryList(_) => "List", Self::TreeEntry(_) => "TreeEntry", Self::TreeEntryList(_) => "List", Self::DiffStats(_) => "DiffStats", Self::DiffStatEntry(_) => "DiffStatEntry", Self::DiffStatEntryList(_) => "List", Self::CryptographicSignatureOpt(_) => "Option", Self::AnnotationLine(_) => "AnnotationLine", Self::Trailer(_) => "Trailer", Self::TrailerList(_) => "List", } } fn try_into_byte_string(self) -> Result, Self> { match self { Self::Core(property) => property.try_into_byte_string().map_err(Self::Core), Self::Operation(property) => property.try_into_byte_string().map_err(Self::Operation), _ => Err(self), } } fn try_into_string(self) -> Result, Self> { match self { Self::Core(property) => property.try_into_string().map_err(Self::Core), Self::Operation(property) => property.try_into_string().map_err(Self::Operation), Self::RefSymbol(property) => Ok(property.map(|RefSymbolBuf(s)| s).into_dyn()), Self::RefSymbolOpt(property) => Ok(property .map(|opt| opt.map_or_else(String::new, |RefSymbolBuf(s)| s)) .into_dyn()), _ => Err(self), } } fn try_into_boolean(self) -> Result, Self> { match self { Self::Core(property) => property.try_into_boolean().map_err(Self::Core), Self::Operation(property) => property.try_into_boolean().map_err(Self::Operation), Self::Commit(_) => Err(self), Self::CommitOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()), Self::CommitList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()), Self::CommitEvolutionEntry(_) => Err(self), Self::CommitRef(_) => Err(self), Self::CommitRefOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()), Self::CommitRefList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()), Self::WorkspaceRef(_) => Err(self), Self::WorkspaceRefOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()), Self::WorkspaceRefList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()), Self::RefSymbol(_) => Err(self), Self::RefSymbolOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()), Self::RepoPath(_) => Err(self), Self::RepoPathOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()), Self::ChangeId(_) => Err(self), Self::CommitId(_) => Err(self), Self::ShortestIdPrefix(_) => Err(self), // TODO: boolean cast could be implemented, but explicit // diff.empty() method might be better. Self::TreeDiff(_) => Err(self), Self::TreeDiffEntry(_) => Err(self), Self::TreeDiffEntryList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()), Self::TreeEntry(_) => Err(self), Self::TreeEntryList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()), Self::DiffStats(_) => Err(self), Self::DiffStatEntry(_) => Err(self), Self::DiffStatEntryList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()), Self::CryptographicSignatureOpt(property) => { Ok(property.map(|sig| sig.is_some()).into_dyn()) } Self::AnnotationLine(_) => Err(self), Self::Trailer(_) => Err(self), Self::TrailerList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()), } } fn try_into_integer(self) -> Result, Self> { match self { Self::Core(property) => property.try_into_integer().map_err(Self::Core), Self::Operation(property) => property.try_into_integer().map_err(Self::Operation), _ => Err(self), } } fn try_into_timestamp(self) -> Result, Self> { match self { Self::Core(property) => property.try_into_timestamp().map_err(Self::Core), Self::Operation(property) => property.try_into_timestamp().map_err(Self::Operation), _ => Err(self), } } fn try_into_serialize(self) -> Option> { match self { Self::Core(property) => property.try_into_serialize(), Self::Operation(property) => property.try_into_serialize(), Self::Commit(property) => Some(property.into_serialize()), Self::CommitOpt(property) => Some(property.into_serialize()), Self::CommitList(property) => Some(property.into_serialize()), Self::CommitEvolutionEntry(property) => Some(property.into_serialize()), Self::CommitRef(property) => Some(property.into_serialize()), Self::CommitRefOpt(property) => Some(property.into_serialize()), Self::CommitRefList(property) => Some(property.into_serialize()), Self::WorkspaceRef(property) => Some(property.into_serialize()), Self::WorkspaceRefOpt(property) => Some(property.into_serialize()), Self::WorkspaceRefList(property) => Some(property.into_serialize()), Self::RefSymbol(property) => Some(property.into_serialize()), Self::RefSymbolOpt(property) => Some(property.into_serialize()), Self::RepoPath(property) => Some(property.into_serialize()), Self::RepoPathOpt(property) => Some(property.into_serialize()), Self::ChangeId(property) => Some(property.into_serialize()), Self::CommitId(property) => Some(property.into_serialize()), Self::ShortestIdPrefix(property) => Some(property.into_serialize()), Self::TreeDiff(_) => None, Self::TreeDiffEntry(_) => None, Self::TreeDiffEntryList(_) => None, Self::TreeEntry(_) => None, Self::TreeEntryList(_) => None, Self::DiffStats(_) => None, Self::DiffStatEntry(_) => None, Self::DiffStatEntryList(_) => None, Self::CryptographicSignatureOpt(_) => None, Self::AnnotationLine(_) => None, Self::Trailer(_) => None, Self::TrailerList(_) => None, } } fn try_into_template(self) -> Option> { match self { Self::Core(property) => property.try_into_template(), Self::Operation(property) => property.try_into_template(), Self::Commit(_) => None, Self::CommitOpt(_) => None, Self::CommitList(_) => None, Self::CommitEvolutionEntry(_) => None, Self::CommitRef(property) => Some(property.into_template()), Self::CommitRefOpt(property) => Some(property.into_template()), Self::CommitRefList(property) => Some(property.into_template()), Self::WorkspaceRef(property) => Some(property.into_template()), Self::WorkspaceRefOpt(property) => Some(property.into_template()), Self::WorkspaceRefList(property) => Some(property.into_template()), Self::RefSymbol(property) => Some(property.into_template()), Self::RefSymbolOpt(property) => Some(property.into_template()), Self::RepoPath(property) => Some(property.into_template()), Self::RepoPathOpt(property) => Some(property.into_template()), Self::ChangeId(property) => Some(property.into_template()), Self::CommitId(property) => Some(property.into_template()), Self::ShortestIdPrefix(property) => Some(property.into_template()), Self::TreeDiff(_) => None, Self::TreeDiffEntry(_) => None, Self::TreeDiffEntryList(_) => None, Self::TreeEntry(_) => None, Self::TreeEntryList(_) => None, Self::DiffStats(property) => Some(property.into_template()), Self::DiffStatEntry(_) => None, Self::DiffStatEntryList(_) => None, Self::CryptographicSignatureOpt(_) => None, Self::AnnotationLine(_) => None, Self::Trailer(property) => Some(property.into_template()), Self::TrailerList(property) => Some(property.into_template()), } } fn try_into_eq(self, other: Self) -> Option> { type Core<'repo> = CoreTemplatePropertyKind<'repo>; match (self, other) { (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_eq(rhs), (Self::Core(lhs), Self::Operation(rhs)) => rhs.try_into_eq_core(lhs), (Self::Core(Core::String(lhs)), Self::RefSymbol(rhs)) => { Some((lhs, rhs).map(|(l, r)| RefSymbolBuf(l) == r).into_dyn()) } (Self::Core(Core::String(lhs)), Self::RefSymbolOpt(rhs)) => Some( (lhs, rhs) .map(|(l, r)| Some(RefSymbolBuf(l)) == r) .into_dyn(), ), (Self::Operation(lhs), Self::Core(rhs)) => lhs.try_into_eq_core(rhs), (Self::Operation(lhs), Self::Operation(rhs)) => lhs.try_into_eq(rhs), (Self::RefSymbol(lhs), Self::Core(Core::String(rhs))) => { Some((lhs, rhs).map(|(l, r)| l == RefSymbolBuf(r)).into_dyn()) } (Self::RefSymbol(lhs), Self::RefSymbol(rhs)) => { Some((lhs, rhs).map(|(l, r)| l == r).into_dyn()) } (Self::RefSymbol(lhs), Self::RefSymbolOpt(rhs)) => { Some((lhs, rhs).map(|(l, r)| Some(l) == r).into_dyn()) } (Self::RefSymbolOpt(lhs), Self::Core(Core::String(rhs))) => Some( (lhs, rhs) .map(|(l, r)| l == Some(RefSymbolBuf(r))) .into_dyn(), ), (Self::RefSymbolOpt(lhs), Self::RefSymbol(rhs)) => { Some((lhs, rhs).map(|(l, r)| l == Some(r)).into_dyn()) } (Self::RefSymbolOpt(lhs), Self::RefSymbolOpt(rhs)) => { Some((lhs, rhs).map(|(l, r)| l == r).into_dyn()) } (Self::Core(_), _) => None, (Self::Operation(_), _) => None, (Self::Commit(_), _) => None, (Self::CommitOpt(_), _) => None, (Self::CommitList(_), _) => None, (Self::CommitEvolutionEntry(_), _) => None, (Self::CommitRef(_), _) => None, (Self::CommitRefOpt(_), _) => None, (Self::CommitRefList(_), _) => None, (Self::WorkspaceRef(_), _) => None, (Self::WorkspaceRefOpt(_), _) => None, (Self::WorkspaceRefList(_), _) => None, (Self::RefSymbol(_), _) => None, (Self::RefSymbolOpt(_), _) => None, (Self::RepoPath(_), _) => None, (Self::RepoPathOpt(_), _) => None, (Self::ChangeId(_), _) => None, (Self::CommitId(_), _) => None, (Self::ShortestIdPrefix(_), _) => None, (Self::TreeDiff(_), _) => None, (Self::TreeDiffEntry(_), _) => None, (Self::TreeDiffEntryList(_), _) => None, (Self::TreeEntry(_), _) => None, (Self::TreeEntryList(_), _) => None, (Self::DiffStats(_), _) => None, (Self::DiffStatEntry(_), _) => None, (Self::DiffStatEntryList(_), _) => None, (Self::CryptographicSignatureOpt(_), _) => None, (Self::AnnotationLine(_), _) => None, (Self::Trailer(_), _) => None, (Self::TrailerList(_), _) => None, } } fn try_into_cmp(self, other: Self) -> Option> { match (self, other) { (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_cmp(rhs), (Self::Core(lhs), Self::Operation(rhs)) => rhs .try_into_cmp_core(lhs) .map(|property| property.map(Ordering::reverse).into_dyn()), (Self::Operation(lhs), Self::Core(rhs)) => lhs.try_into_cmp_core(rhs), (Self::Operation(lhs), Self::Operation(rhs)) => lhs.try_into_cmp(rhs), (Self::Core(_), _) => None, (Self::Operation(_), _) => None, (Self::Commit(_), _) => None, (Self::CommitOpt(_), _) => None, (Self::CommitList(_), _) => None, (Self::CommitEvolutionEntry(_), _) => None, (Self::CommitRef(_), _) => None, (Self::CommitRefOpt(_), _) => None, (Self::CommitRefList(_), _) => None, (Self::WorkspaceRef(_), _) => None, (Self::WorkspaceRefOpt(_), _) => None, (Self::WorkspaceRefList(_), _) => None, (Self::RefSymbol(_), _) => None, (Self::RefSymbolOpt(_), _) => None, (Self::RepoPath(_), _) => None, (Self::RepoPathOpt(_), _) => None, (Self::ChangeId(_), _) => None, (Self::CommitId(_), _) => None, (Self::ShortestIdPrefix(_), _) => None, (Self::TreeDiff(_), _) => None, (Self::TreeDiffEntry(_), _) => None, (Self::TreeDiffEntryList(_), _) => None, (Self::TreeEntry(_), _) => None, (Self::TreeEntryList(_), _) => None, (Self::DiffStats(_), _) => None, (Self::DiffStatEntry(_), _) => None, (Self::DiffStatEntryList(_), _) => None, (Self::CryptographicSignatureOpt(_), _) => None, (Self::AnnotationLine(_), _) => None, (Self::Trailer(_), _) => None, (Self::TrailerList(_), _) => None, } } } impl<'repo> OperationTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> {} /// Table of functions that translate method call node of self type `T`. pub type CommitTemplateBuildMethodFnMap<'repo, T> = TemplateBuildMethodFnMap<'repo, CommitTemplateLanguage<'repo>, T>; /// Symbol table of methods available in the commit template. pub struct CommitTemplateBuildFnTable<'repo> { pub core: CoreTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>, pub operation: OperationTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>, pub commit_methods: CommitTemplateBuildMethodFnMap<'repo, Commit>, pub commit_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec>, pub commit_evolution_entry_methods: CommitTemplateBuildMethodFnMap<'repo, CommitEvolutionEntry>, pub commit_ref_methods: CommitTemplateBuildMethodFnMap<'repo, Rc>, pub commit_ref_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec>>, pub workspace_ref_methods: CommitTemplateBuildMethodFnMap<'repo, WorkspaceRef>, pub workspace_ref_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec>, pub repo_path_methods: CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf>, pub change_id_methods: CommitTemplateBuildMethodFnMap<'repo, ChangeId>, pub commit_id_methods: CommitTemplateBuildMethodFnMap<'repo, CommitId>, pub shortest_id_prefix_methods: CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix>, pub tree_diff_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiff>, pub tree_diff_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry>, pub tree_diff_entry_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec>, pub tree_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeEntry>, pub tree_entry_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec>, pub diff_stats_methods: CommitTemplateBuildMethodFnMap<'repo, DiffStats>, pub diff_stat_entry_methods: CommitTemplateBuildMethodFnMap<'repo, DiffStatEntry>, pub diff_stat_entry_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec>, pub cryptographic_signature_methods: CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature>, pub annotation_line_methods: CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>, pub trailer_methods: CommitTemplateBuildMethodFnMap<'repo, Trailer>, pub trailer_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec>, } impl CommitTemplateBuildFnTable<'_> { pub fn empty() -> Self { Self { core: CoreTemplateBuildFnTable::empty(), operation: OperationTemplateBuildFnTable::empty(), commit_methods: HashMap::new(), commit_list_methods: HashMap::new(), commit_evolution_entry_methods: HashMap::new(), commit_ref_methods: HashMap::new(), commit_ref_list_methods: HashMap::new(), workspace_ref_methods: HashMap::new(), workspace_ref_list_methods: HashMap::new(), repo_path_methods: HashMap::new(), change_id_methods: HashMap::new(), commit_id_methods: HashMap::new(), shortest_id_prefix_methods: HashMap::new(), tree_diff_methods: HashMap::new(), tree_diff_entry_methods: HashMap::new(), tree_diff_entry_list_methods: HashMap::new(), tree_entry_methods: HashMap::new(), tree_entry_list_methods: HashMap::new(), diff_stats_methods: HashMap::new(), diff_stat_entry_methods: HashMap::new(), diff_stat_entry_list_methods: HashMap::new(), cryptographic_signature_methods: HashMap::new(), annotation_line_methods: HashMap::new(), trailer_methods: HashMap::new(), trailer_list_methods: HashMap::new(), } } fn merge(&mut self, other: Self) { let Self { core, operation, commit_methods, commit_list_methods, commit_evolution_entry_methods, commit_ref_methods, commit_ref_list_methods, workspace_ref_methods, workspace_ref_list_methods, repo_path_methods, change_id_methods, commit_id_methods, shortest_id_prefix_methods, tree_diff_methods, tree_diff_entry_methods, tree_diff_entry_list_methods, tree_entry_methods, tree_entry_list_methods, diff_stats_methods, diff_stat_entry_methods, diff_stat_entry_list_methods, cryptographic_signature_methods, annotation_line_methods, trailer_methods, trailer_list_methods, } = other; self.core.merge(core); self.operation.merge(operation); merge_fn_map(&mut self.commit_methods, commit_methods); merge_fn_map(&mut self.commit_list_methods, commit_list_methods); merge_fn_map( &mut self.commit_evolution_entry_methods, commit_evolution_entry_methods, ); merge_fn_map(&mut self.commit_ref_methods, commit_ref_methods); merge_fn_map(&mut self.commit_ref_list_methods, commit_ref_list_methods); merge_fn_map(&mut self.workspace_ref_methods, workspace_ref_methods); merge_fn_map( &mut self.workspace_ref_list_methods, workspace_ref_list_methods, ); merge_fn_map(&mut self.repo_path_methods, repo_path_methods); merge_fn_map(&mut self.change_id_methods, change_id_methods); merge_fn_map(&mut self.commit_id_methods, commit_id_methods); merge_fn_map( &mut self.shortest_id_prefix_methods, shortest_id_prefix_methods, ); merge_fn_map(&mut self.tree_diff_methods, tree_diff_methods); merge_fn_map(&mut self.tree_diff_entry_methods, tree_diff_entry_methods); merge_fn_map( &mut self.tree_diff_entry_list_methods, tree_diff_entry_list_methods, ); merge_fn_map(&mut self.tree_entry_methods, tree_entry_methods); merge_fn_map(&mut self.tree_entry_list_methods, tree_entry_list_methods); merge_fn_map(&mut self.diff_stats_methods, diff_stats_methods); merge_fn_map(&mut self.diff_stat_entry_methods, diff_stat_entry_methods); merge_fn_map( &mut self.diff_stat_entry_list_methods, diff_stat_entry_list_methods, ); merge_fn_map( &mut self.cryptographic_signature_methods, cryptographic_signature_methods, ); merge_fn_map(&mut self.annotation_line_methods, annotation_line_methods); merge_fn_map(&mut self.trailer_methods, trailer_methods); merge_fn_map(&mut self.trailer_list_methods, trailer_list_methods); } /// Creates new symbol table containing the builtin methods. fn builtin() -> Self { let mut core = CoreTemplateBuildFnTable::builtin(); merge_fn_map(&mut core.functions, builtin_commit_template_functions()); Self { core, operation: OperationTemplateBuildFnTable::builtin(), commit_methods: builtin_commit_methods(), commit_list_methods: template_builder::builtin_unformattable_list_methods(), commit_evolution_entry_methods: builtin_commit_evolution_entry_methods(), commit_ref_methods: builtin_commit_ref_methods(), commit_ref_list_methods: template_builder::builtin_formattable_list_methods(), workspace_ref_methods: builtin_workspace_ref_methods(), workspace_ref_list_methods: template_builder::builtin_formattable_list_methods(), repo_path_methods: builtin_repo_path_methods(), change_id_methods: builtin_change_id_methods(), commit_id_methods: builtin_commit_id_methods(), shortest_id_prefix_methods: builtin_shortest_id_prefix_methods(), tree_diff_methods: builtin_tree_diff_methods(), tree_diff_entry_methods: builtin_tree_diff_entry_methods(), tree_diff_entry_list_methods: template_builder::builtin_unformattable_list_methods(), tree_entry_methods: builtin_tree_entry_methods(), tree_entry_list_methods: template_builder::builtin_unformattable_list_methods(), diff_stats_methods: builtin_diff_stats_methods(), diff_stat_entry_methods: builtin_diff_stat_entry_methods(), diff_stat_entry_list_methods: template_builder::builtin_unformattable_list_methods(), cryptographic_signature_methods: builtin_cryptographic_signature_methods(), annotation_line_methods: builtin_annotation_line_methods(), trailer_methods: builtin_trailer_methods(), trailer_list_methods: builtin_trailer_list_methods(), } } } #[derive(Default)] pub struct CommitKeywordCache<'repo> { // Build index lazily, and Rc to get away from &self lifetime. bookmarks_index: OnceCell>, tags_index: OnceCell>, git_refs_index: OnceCell>, is_immutable_fn: OnceCell>>, } impl<'repo> CommitKeywordCache<'repo> { pub fn bookmarks_index(&self, repo: &dyn Repo) -> &Rc { self.bookmarks_index .get_or_init(|| Rc::new(build_local_remote_refs_index(repo.view().bookmarks()))) } pub fn tags_index(&self, repo: &dyn Repo) -> &Rc { self.tags_index .get_or_init(|| Rc::new(build_local_remote_refs_index(repo.view().tags()))) } pub fn git_refs_index(&self, repo: &dyn Repo) -> &Rc { self.git_refs_index .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().git_refs()))) } pub fn is_immutable_fn( &self, language: &CommitTemplateLanguage<'repo>, span: pest::Span<'_>, ) -> TemplateParseResult<&Rc>> { // Alternatively, a negated (i.e. visible mutable) set could be computed. // It's usually smaller than the immutable set. The revset engine can also // optimize "::" query to use bitset-based implementation. self.is_immutable_fn.get_or_try_init(|| { let expression = &language.immutable_expression; let revset = evaluate_revset_expression(language, span, expression)?; Ok(revset.containing_fn().into()) }) } } /// Builtin functions for the commit template language. fn builtin_commit_template_functions<'repo>() -> TemplateBuildFunctionFnMap<'repo, CommitTemplateLanguage<'repo>> { let mut map = TemplateBuildFunctionFnMap::::new(); map.insert( "git_web_url", |language, diagnostics, build_ctx, function| { let ([], [remote_node]) = function.expect_named_arguments(&["remote"])?; let remote_property: BoxedTemplateProperty = match remote_node { Some(node) => expect_stringify_expression(language, diagnostics, build_ctx, node)?, None => Box::new(Literal("origin".to_owned())), }; let repo = language.repo; let out_property = remote_property.map(move |remote_name| { git_util::get_remote_web_url(repo.base_repo(), &remote_name).unwrap_or_default() }); Ok(out_property.into_dyn_wrapped()) }, ); map } fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Commit> { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "description", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| commit.description().to_owned()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "trailers", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property .map(|commit| trailer::parse_description_trailers(commit.description())); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "change_id", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| commit.change_id().to_owned()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "commit_id", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| commit.id().to_owned()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "parents", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|commit| { let commits = commit.parents().block_on()?; Ok(commits) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "author", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| commit.author().clone()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "committer", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| commit.committer().clone()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "mine", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let user_email = language.revset_parse_context.user_email.to_owned(); let out_property = self_property.map(move |commit| commit.author().email == user_email); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "signature", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(CryptographicSignature::new); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "working_copies", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.map(|commit| extract_working_copies(repo, &commit)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "current_working_copy", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let name = language.workspace_name.clone(); let out_property = self_property .map(move |commit| Some(commit.id()) == repo.view().get_wc_commit_id(&name)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "bookmarks", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let index = language .keyword_cache .bookmarks_index(language.repo) .clone(); let out_property = self_property.map(move |commit| collect_distinct_refs(index.get(commit.id()))); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "local_bookmarks", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let index = language .keyword_cache .bookmarks_index(language.repo) .clone(); let out_property = self_property.map(move |commit| collect_local_refs(index.get(commit.id()))); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "remote_bookmarks", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let index = language .keyword_cache .bookmarks_index(language.repo) .clone(); let out_property = self_property.map(move |commit| collect_remote_refs(index.get(commit.id()))); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "tags", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let index = language.keyword_cache.tags_index(language.repo).clone(); let out_property = self_property.map(move |commit| collect_distinct_refs(index.get(commit.id()))); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "local_tags", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let index = language.keyword_cache.tags_index(language.repo).clone(); let out_property = self_property.map(move |commit| collect_local_refs(index.get(commit.id()))); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "remote_tags", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let index = language.keyword_cache.tags_index(language.repo).clone(); let out_property = self_property.map(move |commit| collect_remote_refs(index.get(commit.id()))); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "divergent", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|commit| { // The given commit could be hidden in e.g. `jj evolog`. let maybe_targets = repo.resolve_change_id(commit.change_id())?; let divergent = maybe_targets.is_some_and(|targets| targets.is_divergent()); Ok(divergent) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "hidden", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|commit| Ok(commit.is_hidden(repo)?)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "change_offset", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|commit| { // The given commit could be hidden in e.g. `jj evolog`. let maybe_targets = repo.resolve_change_id(commit.change_id())?; let offset = maybe_targets .and_then(|targets| targets.find_offset(commit.id())) .map(i64::try_from) .transpose()?; Ok(offset) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "immutable", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let is_immutable = language .keyword_cache .is_immutable_fn(language, function.name_span)? .clone(); let out_property = self_property.and_then(move |commit| Ok(is_immutable(commit.id())?)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "contained_in", |language, diagnostics, _build_ctx, self_property, function| { let [revset_node] = function.expect_exact_arguments()?; let is_contained = template_parser::catch_aliases(diagnostics, revset_node, |diagnostics, node| { let text = template_parser::expect_string_literal(node)?; let revset = evaluate_user_revset(language, diagnostics, node.span, text)?; Ok(revset.containing_fn()) })?; let out_property = self_property.and_then(move |commit| Ok(is_contained(commit.id())?)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "conflict", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit| commit.has_conflict()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "empty", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|commit| Ok(commit.is_empty(repo).block_on()?)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "diff", |language, diagnostics, _build_ctx, self_property, function| { let ([], [files_node]) = function.expect_arguments()?; let files = if let Some(node) = files_node { expect_fileset_literal(diagnostics, node, &language.fileset_parse_context())? } else { // TODO: defaults to CLI path arguments? // https://github.com/jj-vcs/jj/issues/2933#issuecomment-1925870731 FilesetExpression::all() }; let repo = language.repo; let matcher: Rc = files.to_matcher().into(); let out_property = self_property.and_then(move |commit| { Ok(TreeDiff::from_commit(repo, &commit, matcher.clone()).block_on()?) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "files", |language, diagnostics, _build_ctx, self_property, function| { let ([], [files_node]) = function.expect_arguments()?; let files = if let Some(node) = files_node { expect_fileset_literal(diagnostics, node, &language.fileset_parse_context())? } else { // TODO: defaults to CLI path arguments? // https://github.com/jj-vcs/jj/issues/2933#issuecomment-1925870731 FilesetExpression::all() }; let matcher = files.to_matcher(); let out_property = self_property.and_then(move |commit| { let tree = commit.tree(); let entries: Vec<_> = tree .entries_matching(&*matcher) .map(|(path, value)| value.map(|value| TreeEntry { path, value })) .try_collect()?; Ok(entries) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "conflicted_files", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|commit| { let tree = commit.tree(); let entries: Vec<_> = tree .conflicts() .map(|(path, value)| value.map(|value| TreeEntry { path, value })) .try_collect()?; Ok(entries) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "root", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.map(|commit| commit.id() == repo.store().root_commit_id()); Ok(out_property.into_dyn_wrapped()) }, ); map } fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> Vec { if repo.view().wc_commit_ids().len() <= 1 { // No non-default working copies, return empty list. return vec![]; } repo.view() .wc_commit_ids() .iter() .filter(|(_, wc_commit_id)| *wc_commit_id == commit.id()) .map(|(name, _)| WorkspaceRef::new(name.to_owned(), commit.to_owned())) .collect() } fn expect_fileset_literal( diagnostics: &mut TemplateDiagnostics, node: &ExpressionNode, context: &FilesetParseContext, ) -> Result { template_parser::catch_aliases(diagnostics, node, |diagnostics, node| { let text = template_parser::expect_string_literal(node)?; let mut inner_diagnostics = FilesetDiagnostics::new(); let expression = fileset::parse(&mut inner_diagnostics, text, context).map_err(|err| { TemplateParseError::expression("In fileset expression", node.span).with_source(err) })?; diagnostics.extend_with(inner_diagnostics, |diag| { TemplateParseError::expression("In fileset expression", node.span).with_source(diag) }); Ok(expression) }) } fn evaluate_revset_expression<'repo>( language: &CommitTemplateLanguage<'repo>, span: pest::Span<'_>, expression: &UserRevsetExpression, ) -> Result, TemplateParseError> { let make_error = || TemplateParseError::expression("Failed to evaluate revset", span); let repo = language.repo; let symbol_resolver = revset_util::default_symbol_resolver( repo, language.revset_parse_context.extensions.symbol_resolvers(), language.id_prefix_context, ); let revset = expression .resolve_user_expression(repo, &symbol_resolver) .map_err(|err| make_error().with_source(err))? .evaluate(repo) .map_err(|err| make_error().with_source(err))?; Ok(revset) } fn evaluate_user_revset<'repo>( language: &CommitTemplateLanguage<'repo>, diagnostics: &mut TemplateDiagnostics, span: pest::Span<'_>, revset: &str, ) -> Result, TemplateParseError> { let mut inner_diagnostics = RevsetDiagnostics::new(); let expression = revset::parse( &mut inner_diagnostics, revset, &language.revset_parse_context, ) .map_err(|err| TemplateParseError::expression("In revset expression", span).with_source(err))?; diagnostics.extend_with(inner_diagnostics, |diag| { TemplateParseError::expression("In revset expression", span).with_source(diag) }); evaluate_revset_expression(language, span, &expression) } fn builtin_commit_evolution_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, CommitEvolutionEntry> { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "commit", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| entry.commit); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "operation", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| entry.operation); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "predecessors", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|entry| { let commits = entry.predecessors().block_on()?; Ok(commits) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "inter_diff", |language, diagnostics, _build_ctx, self_property, function| { let ([], [files_node]) = function.expect_arguments()?; let files = if let Some(node) = files_node { expect_fileset_literal(diagnostics, node, &language.fileset_parse_context())? } else { FilesetExpression::all() }; let repo = language.repo; let matcher: Rc = files.to_matcher().into(); let out_property = self_property.and_then(move |entry| { let predecessors = entry.predecessors().block_on()?; let from_tree = rebase_to_dest_parent(repo, &predecessors, &entry.commit).block_on()?; let to_tree = entry.commit.tree(); Ok(TreeDiff { from_tree, to_tree, matcher: matcher.clone(), copy_records: CopyRecords::default(), // TODO: copy tracking }) }); Ok(out_property.into_dyn_wrapped()) }, ); map } /// Bookmark or tag name with metadata. #[derive(Debug, serde::Serialize)] pub struct CommitRef { // Not using Ref/GitRef/RemoteName types here because it would be overly // complex to generalize the name type as T: RefName|GitRefName. /// Local name. name: RefSymbolBuf, /// Remote name if this is a remote or Git-tracking ref. #[serde(skip_serializing_if = "Option::is_none")] // local ref shouldn't have this field remote: Option, /// Target commit ids. target: RefTarget, /// Local ref metadata which tracks this remote ref. #[serde(rename = "tracking_target")] #[serde(skip_serializing_if = "Option::is_none")] // local ref shouldn't have this field #[serde(serialize_with = "serialize_tracking_target")] tracking_ref: Option, /// Local ref is synchronized with all tracking remotes, or tracking remote /// ref is synchronized with the local. #[serde(skip)] // internal state used mainly for Template impl synced: bool, } #[derive(Debug)] struct TrackingRef { /// Local ref target which tracks the other remote ref. target: RefTarget, /// Number of commits ahead of the tracking `target`. ahead_count: OnceCell, /// Number of commits behind of the tracking `target`. behind_count: OnceCell, } impl CommitRef { // CommitRef is wrapped by Rc to make it cheaply cloned and share // lazy-evaluation results across clones. /// Creates local ref representation which might track some of the /// `remote_refs`. pub fn local<'a>( name: impl Into, target: RefTarget, remote_refs: impl IntoIterator, ) -> Rc { let synced = remote_refs .into_iter() .all(|remote_ref| !remote_ref.is_tracked() || remote_ref.target == target); Rc::new(Self { name: RefSymbolBuf(name.into()), remote: None, target, tracking_ref: None, synced, }) } /// Creates local ref representation which doesn't track any remote refs. pub fn local_only(name: impl Into, target: RefTarget) -> Rc { Self::local(name, target, []) } /// Creates remote ref representation which might be tracked by a local ref /// pointing to the `local_target`. pub fn remote( name: impl Into, remote_name: impl Into, remote_ref: RemoteRef, local_target: &RefTarget, ) -> Rc { let synced = remote_ref.is_tracked() && remote_ref.target == *local_target; let tracking_ref = remote_ref.is_tracked().then(|| { let count = if synced { OnceCell::from((0, Some(0))) // fast path for synced remotes } else { OnceCell::new() }; TrackingRef { target: local_target.clone(), ahead_count: count.clone(), behind_count: count, } }); Rc::new(Self { name: RefSymbolBuf(name.into()), remote: Some(RefSymbolBuf(remote_name.into())), target: remote_ref.target, tracking_ref, synced, }) } /// Creates remote ref representation which isn't tracked by a local ref. pub fn remote_only( name: impl Into, remote_name: impl Into, target: RefTarget, ) -> Rc { Rc::new(Self { name: RefSymbolBuf(name.into()), remote: Some(RefSymbolBuf(remote_name.into())), target, tracking_ref: None, synced: false, // has no local counterpart }) } /// Local name. pub fn name(&self) -> &str { self.name.as_ref() } /// Remote name if this is a remote or Git-tracking ref. pub fn remote_name(&self) -> Option<&str> { self.remote.as_ref().map(AsRef::as_ref) } /// Target commit ids. pub fn target(&self) -> &RefTarget { &self.target } /// Returns true if this is a local ref. pub fn is_local(&self) -> bool { self.remote.is_none() } /// Returns true if this is a remote ref. pub fn is_remote(&self) -> bool { self.remote.is_some() } /// Returns true if this ref points to no commit. pub fn is_absent(&self) -> bool { self.target.is_absent() } /// Returns true if this ref points to any commit. pub fn is_present(&self) -> bool { self.target.is_present() } /// Whether the ref target has conflicts. pub fn has_conflict(&self) -> bool { self.target.has_conflict() } /// Returns true if this ref is tracked by a local ref. The local ref might /// have been deleted (but not pushed yet.) pub fn is_tracked(&self) -> bool { self.tracking_ref.is_some() } /// Returns true if this ref is tracked by a local ref, and if the local ref /// is present. pub fn is_tracking_present(&self) -> bool { self.tracking_ref .as_ref() .is_some_and(|tracking| tracking.target.is_present()) } /// Number of commits ahead of the tracking local ref. fn tracking_ahead_count(&self, repo: &dyn Repo) -> Result { let Some(tracking) = &self.tracking_ref else { return Err(TemplatePropertyError("Not a tracked remote ref".into())); }; tracking .ahead_count .get_or_try_init(|| { let self_ids = self.target.added_ids().cloned().collect_vec(); let other_ids = tracking.target.added_ids().cloned().collect_vec(); Ok(revset::walk_revs(repo, &self_ids, &other_ids)?.count_estimate()?) }) .copied() } /// Number of commits behind of the tracking local ref. fn tracking_behind_count(&self, repo: &dyn Repo) -> Result { let Some(tracking) = &self.tracking_ref else { return Err(TemplatePropertyError("Not a tracked remote ref".into())); }; tracking .behind_count .get_or_try_init(|| { let self_ids = self.target.added_ids().cloned().collect_vec(); let other_ids = tracking.target.added_ids().cloned().collect_vec(); Ok(revset::walk_revs(repo, &other_ids, &self_ids)?.count_estimate()?) }) .copied() } } // If wrapping with Rc becomes common, add generic impl for Rc. impl Template for Rc { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter.labeled("name"), "{}", self.name)?; if let Some(remote) = &self.remote { write!(formatter, "@")?; write!(formatter.labeled("remote"), "{remote}")?; } // Don't show both conflict and unsynced sigils as conflicted ref wouldn't // be pushed. if self.has_conflict() { write!(formatter, "??")?; } else if self.is_local() && !self.synced { write!(formatter, "*")?; } Ok(()) } } impl Template for Vec> { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { templater::format_joined(formatter, self, " ") } } /// Workspace name together with its working-copy commit for templating. #[derive(Debug, Clone, serde::Serialize)] pub struct WorkspaceRef { /// Workspace name as a symbol. name: WorkspaceNameBuf, /// Working-copy commit of this workspace. target: Commit, } impl WorkspaceRef { /// Creates a new workspace reference from the workspace name and commit. pub fn new(name: WorkspaceNameBuf, target: Commit) -> Self { Self { name, target } } /// Returns the workspace name symbol. pub fn name(&self) -> &WorkspaceName { self.name.as_ref() } /// Returns the working-copy commit of this workspace. pub fn target(&self) -> &Commit { &self.target } /// Returns the root path of the workspace. fn root(&self, path_converter: &RepoPathUiConverter) -> Result { let RepoPathUiConverter::Fs { cwd: _, base } = path_converter; // TODO: Stop reconstructing the workspace loader here once we've // decided which object should own the workspace store. let workspace_loader = DefaultWorkspaceLoaderFactory.create(base)?; let repo_path = workspace_loader.repo_path().to_owned(); let workspace_store = SimpleWorkspaceStore::load(&repo_path)?; let workspace_path = workspace_store .get_workspace_path(self.name())? .ok_or_else(|| { TemplatePropertyError( format!( "Workspace has no recorded path: {}", self.name().as_symbol() ) .into(), ) })?; let full_path = repo_path.join(workspace_path); let path = dunce::canonicalize(&full_path).map_err(|err| { TemplatePropertyError( format!( "Failed to resolve workspace root: {}: {}: {err}", self.name().as_symbol(), full_path.display() ) .into(), ) })?; // TODO: Return PathBuf once the templater has a filesystem path type. path.into_os_string() .into_string() .map_err(|_| TemplatePropertyError("Invalid UTF-8 sequence in path".into())) } } impl Template for WorkspaceRef { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter, "{}@", self.name.as_symbol()) } } impl Template for Vec { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { templater::format_joined(formatter, self, " ") } } fn builtin_workspace_ref_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, WorkspaceRef> { let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "name", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|ws_ref| RefSymbolBuf(ws_ref.name.into())); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "target", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|ws_ref| ws_ref.target); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "root", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let path_converter = language.path_converter; let out_property = self_property.and_then(move |ws_ref| ws_ref.root(path_converter)); Ok(out_property.into_dyn_wrapped()) }, ); map } #[expect(clippy::ref_option)] fn serialize_tracking_target( tracking_ref: &Option, serializer: S, ) -> Result where S: serde::Serializer, { let target = tracking_ref.as_ref().map(|tracking| &tracking.target); target.serialize(serializer) } fn builtin_commit_ref_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc> { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::>::new(); map.insert( "name", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit_ref| commit_ref.name.clone()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "remote", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit_ref| commit_ref.remote.clone()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "present", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit_ref| commit_ref.is_present()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "conflict", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit_ref| commit_ref.has_conflict()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "normal_target", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|commit_ref| { let maybe_id = commit_ref.target.as_normal(); Ok(maybe_id.map(|id| repo.store().get_commit(id)).transpose()?) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "removed_targets", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|commit_ref| { let ids = commit_ref.target.removed_ids(); let commits: Vec<_> = ids.map(|id| repo.store().get_commit(id)).try_collect()?; Ok(commits) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "added_targets", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|commit_ref| { let ids = commit_ref.target.added_ids(); let commits: Vec<_> = ids.map(|id| repo.store().get_commit(id)).try_collect()?; Ok(commits) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "tracked", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit_ref| commit_ref.is_tracked()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "tracking_present", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit_ref| commit_ref.is_tracking_present()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "tracking_ahead_count", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|commit_ref| commit_ref.tracking_ahead_count(repo)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "tracking_behind_count", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let repo = language.repo; let out_property = self_property.and_then(|commit_ref| commit_ref.tracking_behind_count(repo)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "synced", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|commit_ref| commit_ref.synced); Ok(out_property.into_dyn_wrapped()) }, ); map } /// Cache for reverse lookup refs. #[derive(Clone, Debug, Default)] pub struct CommitRefsIndex { index: HashMap>>, } impl CommitRefsIndex { fn insert<'a>(&mut self, ids: impl IntoIterator, name: Rc) { for id in ids { let commit_refs = self.index.entry(id.clone()).or_default(); commit_refs.push(name.clone()); } } pub fn get(&self, id: &CommitId) -> &[Rc] { self.index.get(id).map_or(&[], |refs: &Vec<_>| refs) } } fn build_local_remote_refs_index<'a>( local_remote_refs: impl IntoIterator)>, ) -> CommitRefsIndex { let mut index = CommitRefsIndex::default(); for (name, target) in local_remote_refs { let local_target = target.local_target; let remote_refs = target.remote_refs; if local_target.is_present() { let commit_ref = CommitRef::local( name, local_target.clone(), remote_refs.iter().map(|&(_, remote_ref)| remote_ref), ); index.insert(local_target.added_ids(), commit_ref); } for &(remote_name, remote_ref) in &remote_refs { let commit_ref = CommitRef::remote(name, remote_name, remote_ref.clone(), local_target); index.insert(remote_ref.target.added_ids(), commit_ref); } } index } fn build_commit_refs_index<'a, K: Into>( ref_pairs: impl IntoIterator, ) -> CommitRefsIndex { let mut index = CommitRefsIndex::default(); for (name, target) in ref_pairs { let commit_ref = CommitRef::local_only(name, target.clone()); index.insert(target.added_ids(), commit_ref); } index } fn collect_distinct_refs(commit_refs: &[Rc]) -> Vec> { commit_refs .iter() .filter(|commit_ref| commit_ref.is_local() || !commit_ref.synced) .cloned() .collect() } fn collect_local_refs(commit_refs: &[Rc]) -> Vec> { commit_refs .iter() .filter(|commit_ref| commit_ref.is_local()) .cloned() .collect() } fn collect_remote_refs(commit_refs: &[Rc]) -> Vec> { commit_refs .iter() .filter(|commit_ref| commit_ref.is_remote()) .cloned() .collect() } /// Wrapper to render ref/remote name in revset syntax. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] #[serde(transparent)] pub struct RefSymbolBuf(String); impl AsRef for RefSymbolBuf { fn as_ref(&self) -> &str { &self.0 } } impl Display for RefSymbolBuf { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.pad(&revset::format_symbol(&self.0)) } } impl Template for RefSymbolBuf { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter, "{self}") } } impl Template for RepoPathBuf { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter, "{}", self.as_internal_file_string()) } } fn builtin_repo_path_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf> { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "absolute", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let path_converter = language.path_converter; // We handle the absolute path here instead of in a wrapper in // `RepoPathUiConverter` because absolute paths only make sense for // filesystem paths. Other cases should fail here. let out_property = self_property.and_then(move |path| match path_converter { RepoPathUiConverter::Fs { cwd: _, base } => path .to_fs_path(base)? .into_os_string() .into_string() .map_err(|_| TemplatePropertyError("Invalid UTF-8 sequence in path".into())), }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "display", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let path_converter = language.path_converter; let out_property = self_property.map(|path| path_converter.format_file_path(&path)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "parent", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|path| Some(path.parent()?.to_owned())); Ok(out_property.into_dyn_wrapped()) }, ); map } trait ShortestIdPrefixLen { fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> IndexResult; } impl ShortestIdPrefixLen for ChangeId { fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> IndexResult { index.shortest_change_prefix_len(repo, self) } } impl Template for ChangeId { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter, "{self}") } } fn builtin_change_id_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, ChangeId> { let mut map = builtin_commit_or_change_id_methods::(); map.insert( "normal_hex", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; // Note: this is _not_ the same as id.to_string(), which returns the // "reverse" hex (z-k), instead of the "forward" / normal hex // (0-9a-f) we want here. let out_property = self_property.map(|id| id.hex()); Ok(out_property.into_dyn_wrapped()) }, ); map } impl ShortestIdPrefixLen for CommitId { fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> IndexResult { index.shortest_commit_prefix_len(repo, self) } } impl Template for CommitId { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter, "{self}") } } fn builtin_commit_id_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, CommitId> { builtin_commit_or_change_id_methods::() } fn builtin_commit_or_change_id_methods<'repo, O>() -> CommitTemplateBuildMethodFnMap<'repo, O> where O: Display + ShortestIdPrefixLen + 'repo, { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "short", |language, diagnostics, build_ctx, self_property, function| { let ([], [len_node]) = function.expect_arguments()?; let len_property = len_node .map(|node| { template_builder::expect_usize_expression( language, diagnostics, build_ctx, node, ) }) .transpose()?; let out_property = (self_property, len_property) .map(|(id, len)| format!("{id:.len$}", len = len.unwrap_or(12))); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "shortest", |language, diagnostics, build_ctx, self_property, function| { let ([], [len_node]) = function.expect_arguments()?; let len_property = len_node .map(|node| { template_builder::expect_usize_expression( language, diagnostics, build_ctx, node, ) }) .transpose()?; let repo = language.repo; let index = match language.id_prefix_context.populate(repo) { Ok(index) => index, Err(err) => { // Not an error because we can still produce somewhat // reasonable output. diagnostics.add_warning( TemplateParseError::expression( "Failed to load short-prefixes index", function.name_span, ) .with_source(err), ); IdPrefixIndex::empty() } }; // The length of the id printed will be the maximum of the minimum // `len` and the length of the shortest unique prefix. let out_property = (self_property, len_property).and_then(move |(id, len)| { let prefix_len = id.shortest_prefix_len(repo, &index)?; let mut hex = format!("{id:.len$}", len = max(prefix_len, len.unwrap_or(0))); let rest = hex.split_off(prefix_len); Ok(ShortestIdPrefix { prefix: hex, rest }) }); Ok(out_property.into_dyn_wrapped()) }, ); map } #[derive(Clone, Debug, serde::Serialize)] pub struct ShortestIdPrefix { pub prefix: String, pub rest: String, } impl Template for ShortestIdPrefix { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter.labeled("prefix"), "{}", self.prefix)?; write!(formatter.labeled("rest"), "{}", self.rest)?; Ok(()) } } impl ShortestIdPrefix { fn to_upper(&self) -> Self { Self { prefix: self.prefix.to_ascii_uppercase(), rest: self.rest.to_ascii_uppercase(), } } fn to_lower(&self) -> Self { Self { prefix: self.prefix.to_ascii_lowercase(), rest: self.rest.to_ascii_lowercase(), } } } fn builtin_shortest_id_prefix_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix> { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "prefix", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|id| id.prefix); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "rest", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|id| id.rest); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "upper", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|id| id.to_upper()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "lower", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|id| id.to_lower()); Ok(out_property.into_dyn_wrapped()) }, ); map } /// Pair of trees to be diffed. #[derive(Debug)] pub struct TreeDiff { from_tree: MergedTree, to_tree: MergedTree, matcher: Rc, copy_records: CopyRecords, } impl TreeDiff { async fn from_commit( repo: &dyn Repo, commit: &Commit, matcher: Rc, ) -> BackendResult { let mut copy_records = CopyRecords::default(); for parent in commit.parent_ids() { let records = diff_util::get_copy_records(repo.store(), parent, commit.id(), &*matcher).await?; copy_records.add_records(records); } Ok(Self { from_tree: commit.parent_tree(repo).await?, to_tree: commit.tree(), matcher, copy_records, }) } fn diff_stream(&self) -> BoxStream<'_, CopiesTreeDiffEntry> { self.from_tree .diff_stream_with_copies(&self.to_tree, &*self.matcher, &self.copy_records) } async fn collect_entries(&self) -> BackendResult> { self.diff_stream() .map(TreeDiffEntry::from_backend_entry_with_copies) .try_collect() .await } fn into_formatted(self, show: F) -> TreeDiffFormatted where F: Fn( &mut dyn Formatter, &Store, BoxStream, Diff<&ConflictLabels>, ) -> Result<(), E>, E: Into, { TreeDiffFormatted { diff: self, show } } } /// Tree diff to be rendered by predefined function `F`. struct TreeDiffFormatted { diff: TreeDiff, show: F, } impl Template for TreeDiffFormatted where F: Fn( &mut dyn Formatter, &Store, BoxStream, Diff<&ConflictLabels>, ) -> Result<(), E>, E: Into, { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { let show = &self.show; let store = self.diff.from_tree.store(); let tree_diff = self.diff.diff_stream(); let conflict_labels = Diff::new(self.diff.from_tree.labels(), self.diff.to_tree.labels()); show(formatter.as_mut(), store, tree_diff, conflict_labels) .or_else(|err| formatter.handle_error(err.into())) } } fn builtin_tree_diff_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiff> { type P<'repo> = CommitTemplatePropertyKind<'repo>; // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "files", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; // TODO: cache and reuse diff entries within the current evaluation? let out_property = self_property.and_then(|diff| Ok(diff.collect_entries().block_on()?)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "color_words", |language, diagnostics, build_ctx, self_property, function| { let ([], [context_node]) = function.expect_arguments()?; let context_property = context_node .map(|node| { template_builder::expect_usize_expression( language, diagnostics, build_ctx, node, ) }) .transpose()?; let path_converter = language.path_converter; let options = diff_util::ColorWordsDiffOptions::from_settings(language.settings()) .map_err(|err| { let message = "Failed to load diff settings"; TemplateParseError::expression(message, function.name_span).with_source(err) })?; let conflict_marker_style = language.conflict_marker_style; let template = (self_property, context_property) .map(move |(diff, context)| { let mut options = options.clone(); if let Some(context) = context { options.context = context; } diff.into_formatted(move |formatter, store, tree_diff, conflict_labels| { diff_util::show_color_words_diff( formatter, store, tree_diff, conflict_labels, path_converter, &options, conflict_marker_style, ) .block_on() }) }) .into_template(); Ok(P::wrap_template(template)) }, ); map.insert( "git", |language, diagnostics, build_ctx, self_property, function| { let ([], [context_node]) = function.expect_arguments()?; let context_property = context_node .map(|node| { template_builder::expect_usize_expression( language, diagnostics, build_ctx, node, ) }) .transpose()?; let options = diff_util::UnifiedDiffOptions::from_settings(language.settings()) .map_err(|err| { let message = "Failed to load diff settings"; TemplateParseError::expression(message, function.name_span).with_source(err) })?; let conflict_marker_style = language.conflict_marker_style; let template = (self_property, context_property) .map(move |(diff, context)| { let mut options = options.clone(); if let Some(context) = context { options.context = context; } diff.into_formatted(move |formatter, store, trees, tree_diff| { diff_util::show_git_diff( formatter, store, trees, tree_diff, &options, conflict_marker_style, ) .block_on() }) }) .into_template(); Ok(P::wrap_template(template)) }, ); map.insert( "stat", |language, diagnostics, build_ctx, self_property, function| { let ([], [width_node]) = function.expect_arguments()?; let width_property = width_node .map(|node| { template_builder::expect_usize_expression( language, diagnostics, build_ctx, node, ) }) .transpose()?; let path_converter = language.path_converter; // No user configuration exists for diff stat. let options = diff_util::DiffStatOptions::default(); let conflict_marker_style = language.conflict_marker_style; // TODO: cache and reuse stats within the current evaluation? let out_property = (self_property, width_property).and_then(move |(diff, width)| { let store = diff.from_tree.store(); let tree_diff = diff.diff_stream(); let stats = DiffStats::calculate(store, tree_diff, &options, conflict_marker_style) .block_on()?; Ok(DiffStatsFormatted { stats, path_converter, // TODO: fall back to current available width width: width.unwrap_or(80), }) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "summary", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let path_converter = language.path_converter; let template = self_property .map(move |diff| { diff.into_formatted(move |formatter, _store, tree_diff, _conflict_labels| { diff_util::show_diff_summary(formatter, tree_diff, path_converter) .block_on() }) }) .into_template(); Ok(P::wrap_template(template)) }, ); // TODO: add support for external tools map } /// [`MergedTree`] diff entry. #[derive(Clone, Debug)] pub struct TreeDiffEntry { pub path: CopiesTreeDiffEntryPath, pub values: Diff, } impl TreeDiffEntry { pub fn from_backend_entry_with_copies(entry: CopiesTreeDiffEntry) -> BackendResult { Ok(Self { path: entry.path, values: entry.values?, }) } fn status(&self) -> diff_util::DiffEntryStatus { diff_util::diff_status(&self.path, &self.values) } fn into_source_entry(self) -> TreeEntry { TreeEntry { path: self.path.source.map_or(self.path.target, |(path, _)| path), value: self.values.before, } } fn into_target_entry(self) -> TreeEntry { TreeEntry { path: self.path.target, value: self.values.after, } } } fn format_diff_path( path: &CopiesTreeDiffEntryPath, path_converter: &RepoPathUiConverter, ) -> String { match path.to_diff() { Some(paths) => path_converter.format_copied_path(paths), None => path_converter.format_file_path(path.target()), } } fn builtin_tree_diff_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry> { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "path", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| entry.path.target); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "display_diff_path", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let path_converter = language.path_converter; let out_property = self_property.map(move |entry| format_diff_path(&entry.path, path_converter)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "status", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| entry.status().label().to_owned()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "status_char", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| entry.status().char().to_string()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "source", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(TreeDiffEntry::into_source_entry); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "target", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(TreeDiffEntry::into_target_entry); Ok(out_property.into_dyn_wrapped()) }, ); map } /// [`MergedTree`] entry. #[derive(Clone, Debug)] pub struct TreeEntry { pub path: RepoPathBuf, pub value: MergedTreeValue, } fn builtin_tree_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeEntry> { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "path", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| entry.path); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "conflict", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| !entry.value.is_resolved()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "conflict_side_count", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property .and_then(|entry| Ok(i64::try_from(entry.value.simplify().num_sides())?)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "file_type", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| describe_file_type(&entry.value).to_owned()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "executable", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| is_executable_file(&entry.value).unwrap_or_default()); Ok(out_property.into_dyn_wrapped()) }, ); map } fn describe_file_type(value: &MergedTreeValue) -> &'static str { match value.as_resolved() { Some(Some(TreeValue::File { .. })) => "file", Some(Some(TreeValue::Symlink(_))) => "symlink", Some(Some(TreeValue::Tree(_))) => "tree", Some(Some(TreeValue::GitSubmodule(_))) => "git-submodule", Some(None) => "", // absent None => "conflict", } } fn is_executable_file(value: &MergedTreeValue) -> Option { let executable = value.to_executable_merge()?; conflicts::resolve_file_executable(&executable) } /// [`DiffStats`] with rendering parameters. #[derive(Clone, Debug)] pub struct DiffStatsFormatted<'a> { stats: DiffStats, path_converter: &'a RepoPathUiConverter, width: usize, } impl Template for DiffStatsFormatted<'_> { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { diff_util::show_diff_stats( formatter.as_mut(), &self.stats, self.path_converter, self.width, ) } } fn builtin_diff_stats_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, DiffStats> { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "files", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|diff| Ok(diff.entries().to_vec())); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "total_added", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|stats| Ok(i64::try_from(stats.count_total_added())?)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "total_removed", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|stats| Ok(i64::try_from(stats.count_total_removed())?)); Ok(out_property.into_dyn_wrapped()) }, ); map } fn builtin_diff_stat_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, DiffStatEntry> { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "path", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| entry.path.target); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "display_diff_path", |language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let path_converter = language.path_converter; let out_property = self_property.map(move |entry| format_diff_path(&entry.path, path_converter)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "status", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| entry.status.label().to_owned()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "status_char", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|entry| entry.status.char().to_string()); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "lines_added", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|entry| { Ok(i64::try_from( entry.added_removed.map_or(0, |(added, _)| added), )?) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "lines_removed", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|entry| { Ok(i64::try_from( entry.added_removed.map_or(0, |(_, removed)| removed), )?) }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "bytes_delta", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|entry| Ok(i64::try_from(entry.bytes_delta)?)); Ok(out_property.into_dyn_wrapped()) }, ); map } #[derive(Debug)] pub struct CryptographicSignature { commit: Commit, } impl CryptographicSignature { fn new(commit: Commit) -> Option { commit.is_signed().then_some(Self { commit }) } fn verify(&self) -> SignResult { self.commit .verification() .transpose() .expect("must have signature") } fn status(&self) -> SignResult { self.verify().map(|verification| verification.status) } /// Defaults to empty string if key is not present. fn key(&self) -> SignResult { self.verify() .map(|verification| verification.key.unwrap_or_default()) } /// Defaults to empty string if display is not present. fn display(&self) -> SignResult { self.verify() .map(|verification| verification.display.unwrap_or_default()) } } fn builtin_cryptographic_signature_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature> { // Not using maplit::hashmap!{} or custom declarative macro here because // code completion inside macro is quite restricted. let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "status", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|sig| match sig.status() { Ok(status) => Ok(status.to_string()), Err(SignError::InvalidSignatureFormat) => Ok("invalid".to_string()), Err(err) => Err(err.into()), }); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "key", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|sig| Ok(sig.key()?)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "display", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|sig| Ok(sig.display()?)); Ok(out_property.into_dyn_wrapped()) }, ); map } #[derive(Debug, Clone)] pub struct AnnotationLine { pub commit: Commit, pub content: BString, pub line_number: usize, pub original_line_number: usize, pub first_line_in_hunk: bool, } fn builtin_annotation_line_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, AnnotationLine> { let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "commit", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|line| line.commit); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "content", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|line| line.content); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "line_number", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|line| Ok(i64::try_from(line.line_number)?)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "original_line_number", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.and_then(|line| Ok(i64::try_from(line.original_line_number)?)); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "first_line_in_hunk", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|line| line.first_line_in_hunk); Ok(out_property.into_dyn_wrapped()) }, ); map } impl Template for Trailer { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter, "{}: {}", self.key, self.value) } } impl Template for Vec { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { templater::format_joined(formatter, self, "\n") } } fn builtin_trailer_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Trailer> { let mut map = CommitTemplateBuildMethodFnMap::::new(); map.insert( "key", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|trailer| trailer.key); Ok(out_property.into_dyn_wrapped()) }, ); map.insert( "value", |_language, _diagnostics, _build_ctx, self_property, function| { function.expect_no_arguments()?; let out_property = self_property.map(|trailer| trailer.value); Ok(out_property.into_dyn_wrapped()) }, ); map } fn builtin_trailer_list_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Vec> { let mut map: CommitTemplateBuildMethodFnMap> = template_builder::builtin_formattable_list_methods(); map.insert( "contains_key", |language, diagnostics, build_ctx, self_property, function| { let [key_node] = function.expect_exact_arguments()?; let key_property = expect_stringify_expression(language, diagnostics, build_ctx, key_node)?; let out_property = (self_property, key_property) .map(|(trailers, key)| trailers.iter().any(|t| t.key == key)); Ok(out_property.into_dyn_wrapped()) }, ); map } #[cfg(test)] mod tests { use std::path::Component; use std::path::Path; use std::path::PathBuf; use jj_lib::config::ConfigLayer; use jj_lib::config::ConfigSource; use jj_lib::fileset::FilesetAliasesMap; use jj_lib::revset::RevsetAliasesMap; use jj_lib::revset::RevsetExpression; use jj_lib::revset::RevsetExtensions; use jj_lib::revset::RevsetWorkspaceContext; use testutils::TestRepoBackend; use testutils::TestWorkspace; use testutils::repo_path_buf; use super::*; use crate::template_parser::TemplateAliasesMap; use crate::templater::TemplateRenderer; use crate::templater::WrapTemplateProperty; // TemplateBuildFunctionFn defined for<'a> type BuildFunctionFn = for<'a> fn( &CommitTemplateLanguage<'a>, &mut TemplateDiagnostics, &BuildContext>, &FunctionCallNode, ) -> TemplateParseResult>; struct CommitTemplateTestEnv { test_workspace: TestWorkspace, path_converter: RepoPathUiConverter, revset_extensions: Arc, id_prefix_context: IdPrefixContext, fileset_aliases_map: FilesetAliasesMap, revset_aliases_map: RevsetAliasesMap, template_aliases_map: TemplateAliasesMap, immutable_expression: Arc, extra_functions: HashMap<&'static str, BuildFunctionFn>, } impl CommitTemplateTestEnv { fn init() -> Self { // Stabilize commit id of the initialized working copy let settings = stable_settings(); let test_workspace = TestWorkspace::init_with_backend_and_settings(TestRepoBackend::Git, &settings); let path_converter = RepoPathUiConverter::Fs { cwd: test_workspace.workspace.workspace_root().to_owned(), base: test_workspace.workspace.workspace_root().to_owned(), }; let revset_extensions = Arc::new(RevsetExtensions::new()); let id_prefix_context = IdPrefixContext::new(revset_extensions.clone()); Self { test_workspace, path_converter, revset_extensions, id_prefix_context, fileset_aliases_map: FilesetAliasesMap::new(), revset_aliases_map: RevsetAliasesMap::new(), template_aliases_map: TemplateAliasesMap::new(), immutable_expression: RevsetExpression::none(), extra_functions: HashMap::new(), } } fn set_base_and_cwd(&mut self, base: PathBuf, cwd: impl AsRef) { self.path_converter = RepoPathUiConverter::Fs { cwd: base.join(cwd), base, }; } fn add_function(&mut self, name: &'static str, f: BuildFunctionFn) { self.extra_functions.insert(name, f); } fn new_language(&self) -> CommitTemplateLanguage<'_> { let revset_parse_context = RevsetParseContext { aliases_map: &self.revset_aliases_map, local_variables: HashMap::new(), user_email: "test.user@example.com", date_pattern_context: chrono::DateTime::UNIX_EPOCH.fixed_offset().into(), default_ignored_remote: None, fileset_aliases_map: &self.fileset_aliases_map, extensions: &self.revset_extensions, workspace: Some(RevsetWorkspaceContext { path_converter: &self.path_converter, workspace_name: self.test_workspace.workspace.workspace_name(), }), }; let mut language = CommitTemplateLanguage::new( self.test_workspace.repo.as_ref(), &self.path_converter, self.test_workspace.workspace.workspace_name(), revset_parse_context, &self.id_prefix_context, self.immutable_expression.clone(), ConflictMarkerStyle::Diff, &[] as &[Box], ); // Not using .extend() to infer lifetime of f for (&name, &f) in &self.extra_functions { language.build_fn_table.core.functions.insert(name, f); } language } fn parse<'a, C>(&'a self, text: &str) -> TemplateParseResult> where C: Clone + 'a, CommitTemplatePropertyKind<'a>: WrapTemplateProperty<'a, C>, { let language = self.new_language(); let mut diagnostics = TemplateDiagnostics::new(); template_builder::parse( &language, &mut diagnostics, text, &self.template_aliases_map, ) } fn render_ok<'a, C>(&'a self, text: &str, context: &C) -> BString where C: Clone + 'a, CommitTemplatePropertyKind<'a>: WrapTemplateProperty<'a, C>, { let template = self.parse(text).unwrap(); template.format_plain_text(context).into() } } fn stable_settings() -> UserSettings { let mut config = testutils::base_user_config(); let mut layer = ConfigLayer::empty(ConfigSource::User); layer .set_value("debug.commit-timestamp", "2001-02-03T04:05:06+07:00") .unwrap(); config.add_layer(layer); UserSettings::from_config(config).unwrap() } #[test] fn test_ref_symbol_type() { let mut env = CommitTemplateTestEnv::init(); env.add_function("sym", |language, diagnostics, build_ctx, function| { let [value_node] = function.expect_exact_arguments()?; let value = expect_stringify_expression(language, diagnostics, build_ctx, value_node)?; let out_property = value.map(RefSymbolBuf); Ok(out_property.into_dyn_wrapped()) }); let sym = |s: &str| RefSymbolBuf(s.to_owned()); // default formatting insta::assert_snapshot!(env.render_ok("self", &sym("")), @r#""""#); insta::assert_snapshot!(env.render_ok("self", &sym("foo")), @"foo"); insta::assert_snapshot!(env.render_ok("self", &sym("foo bar")), @r#""foo bar""#); // comparison insta::assert_snapshot!(env.render_ok("self == 'foo'", &sym("foo")), @"true"); insta::assert_snapshot!(env.render_ok("'bar' == self", &sym("foo")), @"false"); insta::assert_snapshot!(env.render_ok("self == self", &sym("foo")), @"true"); insta::assert_snapshot!(env.render_ok("self == sym('bar')", &sym("foo")), @"false"); insta::assert_snapshot!(env.render_ok("self == 'bar'", &Some(sym("foo"))), @"false"); insta::assert_snapshot!(env.render_ok("self == sym('foo')", &Some(sym("foo"))), @"true"); insta::assert_snapshot!(env.render_ok("'foo' == self", &Some(sym("foo"))), @"true"); insta::assert_snapshot!(env.render_ok("sym('bar') == self", &Some(sym("foo"))), @"false"); insta::assert_snapshot!(env.render_ok("self == self", &Some(sym("foo"))), @"true"); insta::assert_snapshot!(env.render_ok("self == ''", &None::), @"false"); insta::assert_snapshot!(env.render_ok("sym('') == self", &None::), @"false"); insta::assert_snapshot!(env.render_ok("self == self", &None::), @"true"); // string cast != formatting: it would be weird if function argument of // string type were quoted/escaped. (e.g. `"foo".contains(bookmark)`) insta::assert_snapshot!(env.render_ok("stringify(self)", &sym("a b")), @"a b"); insta::assert_snapshot!(env.render_ok("stringify(self)", &Some(sym("a b"))), @"a b"); insta::assert_snapshot!(env.render_ok("stringify(self)", &None::), @""); // string methods insta::assert_snapshot!(env.render_ok("self.len()", &sym("a b")), @"3"); // JSON insta::assert_snapshot!(env.render_ok("json(self)", &sym("foo bar")), @r#""foo bar""#); } #[test] fn test_repo_path_type() { let mut env = CommitTemplateTestEnv::init(); let mut base = PathBuf::from(Component::RootDir.as_os_str()); base.extend(["path", "to", "repo"]); env.set_base_and_cwd(base, "dir"); // slash-separated by default insta::assert_snapshot!( env.render_ok("self", &repo_path_buf("dir/file")), @"dir/file"); // .absolute() to convert to absolute path. if cfg!(windows) { insta::assert_snapshot!( env.render_ok("self.absolute()", &repo_path_buf("file")), @"\\path\\to\\repo\\file"); insta::assert_snapshot!( env.render_ok("self.absolute()", &repo_path_buf("dir/file")), @"\\path\\to\\repo\\dir\\file"); } else { insta::assert_snapshot!( env.render_ok("self.absolute()", &repo_path_buf("file")), @"/path/to/repo/file"); insta::assert_snapshot!( env.render_ok("self.absolute()", &repo_path_buf("dir/file")), @"/path/to/repo/dir/file"); } // .display() to convert to filesystem path insta::assert_snapshot!( env.render_ok("self.display()", &repo_path_buf("dir/file")), @"file"); if cfg!(windows) { insta::assert_snapshot!( env.render_ok("self.display()", &repo_path_buf("file")), @"..\\file"); } else { insta::assert_snapshot!( env.render_ok("self.display()", &repo_path_buf("file")), @"../file"); } let template = "if(self.parent(), self.parent(), '')"; insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("")), @""); insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("file")), @""); insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("dir/file")), @"dir"); // JSON insta::assert_snapshot!( env.render_ok("json(self)", &repo_path_buf("dir/file")), @r#""dir/file""#); insta::assert_snapshot!( env.render_ok("json(self)", &None::), @"null"); } #[test] fn test_commit_id_type() { let env = CommitTemplateTestEnv::init(); let id = CommitId::from_hex("08a70ab33d7143b7130ed8594d8216ef688623c0"); insta::assert_snapshot!( env.render_ok("self", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0"); insta::assert_snapshot!(env.render_ok("self.short()", &id), @"08a70ab33d71"); insta::assert_snapshot!(env.render_ok("self.short(0)", &id), @""); insta::assert_snapshot!(env.render_ok("self.short(-0)", &id), @""); insta::assert_snapshot!( env.render_ok("self.short(100)", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0"); insta::assert_snapshot!( env.render_ok("self.short(-100)", &id), @""); insta::assert_snapshot!(env.render_ok("self.shortest()", &id), @"08"); insta::assert_snapshot!(env.render_ok("self.shortest(0)", &id), @"08"); insta::assert_snapshot!(env.render_ok("self.shortest(-0)", &id), @"08"); insta::assert_snapshot!( env.render_ok("self.shortest(100)", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0"); insta::assert_snapshot!( env.render_ok("self.shortest(-100)", &id), @""); // JSON insta::assert_snapshot!( env.render_ok("json(self)", &id), @r#""08a70ab33d7143b7130ed8594d8216ef688623c0""#); } #[test] fn test_change_id_type() { let env = CommitTemplateTestEnv::init(); let id = ChangeId::from_hex("ffdaa62087a280bddc5e3d3ff933b8ae"); insta::assert_snapshot!( env.render_ok("self", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl"); insta::assert_snapshot!( env.render_ok("self.normal_hex()", &id), @"ffdaa62087a280bddc5e3d3ff933b8ae"); insta::assert_snapshot!(env.render_ok("self.short()", &id), @"kkmpptxzrspx"); insta::assert_snapshot!(env.render_ok("self.short(0)", &id), @""); insta::assert_snapshot!(env.render_ok("self.short(-0)", &id), @""); insta::assert_snapshot!( env.render_ok("self.short(100)", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl"); insta::assert_snapshot!( env.render_ok("self.short(-100)", &id), @""); insta::assert_snapshot!(env.render_ok("self.shortest()", &id), @"k"); insta::assert_snapshot!(env.render_ok("self.shortest(0)", &id), @"k"); insta::assert_snapshot!(env.render_ok("self.shortest(-0)", &id), @"k"); insta::assert_snapshot!( env.render_ok("self.shortest(100)", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl"); insta::assert_snapshot!( env.render_ok("self.shortest(-100)", &id), @""); // JSON insta::assert_snapshot!( env.render_ok("json(self)", &id), @r#""kkmpptxzrspxrzommnulwmwkkqwworpl""#); } #[test] fn test_shortest_id_prefix_type() { let env = CommitTemplateTestEnv::init(); let id = ShortestIdPrefix { prefix: "012".to_owned(), rest: "3abcdef".to_owned(), }; // JSON insta::assert_snapshot!( env.render_ok("json(self)", &id), @r#"{"prefix":"012","rest":"3abcdef"}"#); } }