just playing with tangled
at main 2614 lines 106 kB view raw
1// Copyright 2020-2023 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15use std::any::Any; 16use std::cmp::max; 17use std::cmp::Ordering; 18use std::collections::HashMap; 19use std::fmt; 20use std::fmt::Display; 21use std::io; 22use std::rc::Rc; 23 24use bstr::BString; 25use futures::stream::BoxStream; 26use futures::StreamExt as _; 27use futures::TryStreamExt as _; 28use itertools::Itertools as _; 29use jj_lib::backend::BackendResult; 30use jj_lib::backend::ChangeId; 31use jj_lib::backend::CommitId; 32use jj_lib::backend::TreeValue; 33use jj_lib::commit::Commit; 34use jj_lib::conflicts; 35use jj_lib::conflicts::ConflictMarkerStyle; 36use jj_lib::copies::CopiesTreeDiffEntry; 37use jj_lib::copies::CopiesTreeDiffEntryPath; 38use jj_lib::copies::CopyRecords; 39use jj_lib::extensions_map::ExtensionsMap; 40use jj_lib::fileset; 41use jj_lib::fileset::FilesetDiagnostics; 42use jj_lib::fileset::FilesetExpression; 43use jj_lib::id_prefix::IdPrefixContext; 44use jj_lib::id_prefix::IdPrefixIndex; 45use jj_lib::matchers::Matcher; 46use jj_lib::merge::MergedTreeValue; 47use jj_lib::merged_tree::MergedTree; 48use jj_lib::object_id::ObjectId as _; 49use jj_lib::op_store::RefTarget; 50use jj_lib::op_store::RemoteRef; 51use jj_lib::ref_name::WorkspaceName; 52use jj_lib::ref_name::WorkspaceNameBuf; 53use jj_lib::repo::Repo; 54use jj_lib::repo_path::RepoPathBuf; 55use jj_lib::repo_path::RepoPathUiConverter; 56use jj_lib::revset; 57use jj_lib::revset::Revset; 58use jj_lib::revset::RevsetContainingFn; 59use jj_lib::revset::RevsetDiagnostics; 60use jj_lib::revset::RevsetModifier; 61use jj_lib::revset::RevsetParseContext; 62use jj_lib::revset::UserRevsetExpression; 63use jj_lib::settings::UserSettings; 64use jj_lib::signing::SigStatus; 65use jj_lib::signing::SignError; 66use jj_lib::signing::SignResult; 67use jj_lib::signing::Verification; 68use jj_lib::store::Store; 69use jj_lib::trailer; 70use jj_lib::trailer::Trailer; 71use once_cell::unsync::OnceCell; 72use pollster::FutureExt as _; 73 74use crate::diff_util; 75use crate::diff_util::DiffStats; 76use crate::formatter::Formatter; 77use crate::revset_util; 78use crate::template_builder; 79use crate::template_builder::expect_plain_text_expression; 80use crate::template_builder::merge_fn_map; 81use crate::template_builder::BuildContext; 82use crate::template_builder::CoreTemplateBuildFnTable; 83use crate::template_builder::CoreTemplatePropertyKind; 84use crate::template_builder::CoreTemplatePropertyVar; 85use crate::template_builder::TemplateBuildMethodFnMap; 86use crate::template_builder::TemplateLanguage; 87use crate::template_parser; 88use crate::template_parser::ExpressionNode; 89use crate::template_parser::FunctionCallNode; 90use crate::template_parser::TemplateDiagnostics; 91use crate::template_parser::TemplateParseError; 92use crate::template_parser::TemplateParseResult; 93use crate::templater; 94use crate::templater::BoxedTemplateProperty; 95use crate::templater::ListTemplate; 96use crate::templater::PlainTextFormattedProperty; 97use crate::templater::SizeHint; 98use crate::templater::Template; 99use crate::templater::TemplateFormatter; 100use crate::templater::TemplatePropertyError; 101use crate::templater::TemplatePropertyExt as _; 102use crate::text_util; 103 104pub trait CommitTemplateLanguageExtension { 105 fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo>; 106 107 fn build_cache_extensions(&self, extensions: &mut ExtensionsMap); 108} 109 110pub struct CommitTemplateLanguage<'repo> { 111 repo: &'repo dyn Repo, 112 path_converter: &'repo RepoPathUiConverter, 113 workspace_name: WorkspaceNameBuf, 114 // RevsetParseContext doesn't borrow a repo, but we'll need 'repo lifetime 115 // anyway to capture it to evaluate dynamically-constructed user expression 116 // such as `revset("ancestors(" ++ commit_id ++ ")")`. 117 // TODO: Maybe refactor context structs? RepoPathUiConverter and 118 // WorkspaceName are contained in RevsetParseContext for example. 119 revset_parse_context: RevsetParseContext<'repo>, 120 id_prefix_context: &'repo IdPrefixContext, 121 immutable_expression: Rc<UserRevsetExpression>, 122 conflict_marker_style: ConflictMarkerStyle, 123 build_fn_table: CommitTemplateBuildFnTable<'repo>, 124 keyword_cache: CommitKeywordCache<'repo>, 125 cache_extensions: ExtensionsMap, 126} 127 128impl<'repo> CommitTemplateLanguage<'repo> { 129 /// Sets up environment where commit template will be transformed to 130 /// evaluation tree. 131 #[expect(clippy::too_many_arguments)] 132 pub fn new( 133 repo: &'repo dyn Repo, 134 path_converter: &'repo RepoPathUiConverter, 135 workspace_name: &WorkspaceName, 136 revset_parse_context: RevsetParseContext<'repo>, 137 id_prefix_context: &'repo IdPrefixContext, 138 immutable_expression: Rc<UserRevsetExpression>, 139 conflict_marker_style: ConflictMarkerStyle, 140 extensions: &[impl AsRef<dyn CommitTemplateLanguageExtension>], 141 ) -> Self { 142 let mut build_fn_table = CommitTemplateBuildFnTable::builtin(); 143 let mut cache_extensions = ExtensionsMap::empty(); 144 145 for extension in extensions { 146 build_fn_table.merge(extension.as_ref().build_fn_table()); 147 extension 148 .as_ref() 149 .build_cache_extensions(&mut cache_extensions); 150 } 151 152 CommitTemplateLanguage { 153 repo, 154 path_converter, 155 workspace_name: workspace_name.to_owned(), 156 revset_parse_context, 157 id_prefix_context, 158 immutable_expression, 159 conflict_marker_style, 160 build_fn_table, 161 keyword_cache: CommitKeywordCache::default(), 162 cache_extensions, 163 } 164 } 165} 166 167impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { 168 type Property = CommitTemplatePropertyKind<'repo>; 169 170 fn settings(&self) -> &UserSettings { 171 self.repo.base_repo().settings() 172 } 173 174 fn build_function( 175 &self, 176 diagnostics: &mut TemplateDiagnostics, 177 build_ctx: &BuildContext<Self::Property>, 178 function: &FunctionCallNode, 179 ) -> TemplateParseResult<Self::Property> { 180 let table = &self.build_fn_table.core; 181 table.build_function(self, diagnostics, build_ctx, function) 182 } 183 184 fn build_method( 185 &self, 186 diagnostics: &mut TemplateDiagnostics, 187 build_ctx: &BuildContext<Self::Property>, 188 property: Self::Property, 189 function: &FunctionCallNode, 190 ) -> TemplateParseResult<Self::Property> { 191 let type_name = property.type_name(); 192 match property { 193 CommitTemplatePropertyKind::Core(property) => { 194 let table = &self.build_fn_table.core; 195 table.build_method(self, diagnostics, build_ctx, property, function) 196 } 197 CommitTemplatePropertyKind::Commit(property) => { 198 let table = &self.build_fn_table.commit_methods; 199 let build = template_parser::lookup_method(type_name, table, function)?; 200 build(self, diagnostics, build_ctx, property, function) 201 } 202 CommitTemplatePropertyKind::CommitOpt(property) => { 203 let type_name = "Commit"; 204 let table = &self.build_fn_table.commit_methods; 205 let build = template_parser::lookup_method(type_name, table, function)?; 206 let inner_property = property.try_unwrap(type_name).into_dyn(); 207 build(self, diagnostics, build_ctx, inner_property, function) 208 } 209 CommitTemplatePropertyKind::CommitList(property) => { 210 let table = &self.build_fn_table.commit_list_methods; 211 let build = template_parser::lookup_method(type_name, table, function)?; 212 build(self, diagnostics, build_ctx, property, function) 213 } 214 CommitTemplatePropertyKind::CommitRef(property) => { 215 let table = &self.build_fn_table.commit_ref_methods; 216 let build = template_parser::lookup_method(type_name, table, function)?; 217 build(self, diagnostics, build_ctx, property, function) 218 } 219 CommitTemplatePropertyKind::CommitRefOpt(property) => { 220 let type_name = "CommitRef"; 221 let table = &self.build_fn_table.commit_ref_methods; 222 let build = template_parser::lookup_method(type_name, table, function)?; 223 let inner_property = property.try_unwrap(type_name).into_dyn(); 224 build(self, diagnostics, build_ctx, inner_property, function) 225 } 226 CommitTemplatePropertyKind::CommitRefList(property) => { 227 let table = &self.build_fn_table.commit_ref_list_methods; 228 let build = template_parser::lookup_method(type_name, table, function)?; 229 build(self, diagnostics, build_ctx, property, function) 230 } 231 CommitTemplatePropertyKind::RefSymbol(property) => { 232 let table = &self.build_fn_table.core.string_methods; 233 let build = template_parser::lookup_method(type_name, table, function)?; 234 let inner_property = property.map(|RefSymbolBuf(s)| s).into_dyn(); 235 build(self, diagnostics, build_ctx, inner_property, function) 236 } 237 CommitTemplatePropertyKind::RefSymbolOpt(property) => { 238 let type_name = "RefSymbol"; 239 let table = &self.build_fn_table.core.string_methods; 240 let build = template_parser::lookup_method(type_name, table, function)?; 241 let inner_property = property 242 .try_unwrap(type_name) 243 .map(|RefSymbolBuf(s)| s) 244 .into_dyn(); 245 build(self, diagnostics, build_ctx, inner_property, function) 246 } 247 CommitTemplatePropertyKind::RepoPath(property) => { 248 let table = &self.build_fn_table.repo_path_methods; 249 let build = template_parser::lookup_method(type_name, table, function)?; 250 build(self, diagnostics, build_ctx, property, function) 251 } 252 CommitTemplatePropertyKind::RepoPathOpt(property) => { 253 let type_name = "RepoPath"; 254 let table = &self.build_fn_table.repo_path_methods; 255 let build = template_parser::lookup_method(type_name, table, function)?; 256 let inner_property = property.try_unwrap(type_name).into_dyn(); 257 build(self, diagnostics, build_ctx, inner_property, function) 258 } 259 CommitTemplatePropertyKind::ChangeId(property) => { 260 let table = &self.build_fn_table.change_id_methods; 261 let build = template_parser::lookup_method(type_name, table, function)?; 262 build(self, diagnostics, build_ctx, property, function) 263 } 264 CommitTemplatePropertyKind::CommitId(property) => { 265 let table = &self.build_fn_table.commit_id_methods; 266 let build = template_parser::lookup_method(type_name, table, function)?; 267 build(self, diagnostics, build_ctx, property, function) 268 } 269 CommitTemplatePropertyKind::ShortestIdPrefix(property) => { 270 let table = &self.build_fn_table.shortest_id_prefix_methods; 271 let build = template_parser::lookup_method(type_name, table, function)?; 272 build(self, diagnostics, build_ctx, property, function) 273 } 274 CommitTemplatePropertyKind::TreeDiff(property) => { 275 let table = &self.build_fn_table.tree_diff_methods; 276 let build = template_parser::lookup_method(type_name, table, function)?; 277 build(self, diagnostics, build_ctx, property, function) 278 } 279 CommitTemplatePropertyKind::TreeDiffEntry(property) => { 280 let table = &self.build_fn_table.tree_diff_entry_methods; 281 let build = template_parser::lookup_method(type_name, table, function)?; 282 build(self, diagnostics, build_ctx, property, function) 283 } 284 CommitTemplatePropertyKind::TreeDiffEntryList(property) => { 285 let table = &self.build_fn_table.tree_diff_entry_list_methods; 286 let build = template_parser::lookup_method(type_name, table, function)?; 287 build(self, diagnostics, build_ctx, property, function) 288 } 289 CommitTemplatePropertyKind::TreeEntry(property) => { 290 let table = &self.build_fn_table.tree_entry_methods; 291 let build = template_parser::lookup_method(type_name, table, function)?; 292 build(self, diagnostics, build_ctx, property, function) 293 } 294 CommitTemplatePropertyKind::DiffStats(property) => { 295 let table = &self.build_fn_table.diff_stats_methods; 296 let build = template_parser::lookup_method(type_name, table, function)?; 297 // Strip off formatting parameters which are needed only for the 298 // default template output. 299 let property = property.map(|formatted| formatted.stats).into_dyn(); 300 build(self, diagnostics, build_ctx, property, function) 301 } 302 CommitTemplatePropertyKind::CryptographicSignatureOpt(property) => { 303 let type_name = "CryptographicSignature"; 304 let table = &self.build_fn_table.cryptographic_signature_methods; 305 let build = template_parser::lookup_method(type_name, table, function)?; 306 let inner_property = property.try_unwrap(type_name).into_dyn(); 307 build(self, diagnostics, build_ctx, inner_property, function) 308 } 309 CommitTemplatePropertyKind::AnnotationLine(property) => { 310 let type_name = "AnnotationLine"; 311 let table = &self.build_fn_table.annotation_line_methods; 312 let build = template_parser::lookup_method(type_name, table, function)?; 313 build(self, diagnostics, build_ctx, property, function) 314 } 315 CommitTemplatePropertyKind::Trailer(property) => { 316 let table = &self.build_fn_table.trailer_methods; 317 let build = template_parser::lookup_method(type_name, table, function)?; 318 build(self, diagnostics, build_ctx, property, function) 319 } 320 CommitTemplatePropertyKind::TrailerList(property) => { 321 let table = &self.build_fn_table.trailer_list_methods; 322 let build = template_parser::lookup_method(type_name, table, function)?; 323 build(self, diagnostics, build_ctx, property, function) 324 } 325 } 326 } 327} 328 329// If we need to add multiple languages that support Commit types, this can be 330// turned into a trait which extends TemplateLanguage. 331impl<'repo> CommitTemplateLanguage<'repo> { 332 pub fn repo(&self) -> &'repo dyn Repo { 333 self.repo 334 } 335 336 pub fn workspace_name(&self) -> &WorkspaceName { 337 &self.workspace_name 338 } 339 340 pub fn keyword_cache(&self) -> &CommitKeywordCache<'repo> { 341 &self.keyword_cache 342 } 343 344 pub fn cache_extension<T: Any>(&self) -> Option<&T> { 345 self.cache_extensions.get::<T>() 346 } 347} 348 349pub enum CommitTemplatePropertyKind<'repo> { 350 Core(CoreTemplatePropertyKind<'repo>), 351 Commit(BoxedTemplateProperty<'repo, Commit>), 352 CommitOpt(BoxedTemplateProperty<'repo, Option<Commit>>), 353 CommitList(BoxedTemplateProperty<'repo, Vec<Commit>>), 354 CommitRef(BoxedTemplateProperty<'repo, Rc<CommitRef>>), 355 CommitRefOpt(BoxedTemplateProperty<'repo, Option<Rc<CommitRef>>>), 356 CommitRefList(BoxedTemplateProperty<'repo, Vec<Rc<CommitRef>>>), 357 RefSymbol(BoxedTemplateProperty<'repo, RefSymbolBuf>), 358 RefSymbolOpt(BoxedTemplateProperty<'repo, Option<RefSymbolBuf>>), 359 RepoPath(BoxedTemplateProperty<'repo, RepoPathBuf>), 360 RepoPathOpt(BoxedTemplateProperty<'repo, Option<RepoPathBuf>>), 361 ChangeId(BoxedTemplateProperty<'repo, ChangeId>), 362 CommitId(BoxedTemplateProperty<'repo, CommitId>), 363 ShortestIdPrefix(BoxedTemplateProperty<'repo, ShortestIdPrefix>), 364 TreeDiff(BoxedTemplateProperty<'repo, TreeDiff>), 365 TreeDiffEntry(BoxedTemplateProperty<'repo, TreeDiffEntry>), 366 TreeDiffEntryList(BoxedTemplateProperty<'repo, Vec<TreeDiffEntry>>), 367 TreeEntry(BoxedTemplateProperty<'repo, TreeEntry>), 368 DiffStats(BoxedTemplateProperty<'repo, DiffStatsFormatted<'repo>>), 369 CryptographicSignatureOpt(BoxedTemplateProperty<'repo, Option<CryptographicSignature>>), 370 AnnotationLine(BoxedTemplateProperty<'repo, AnnotationLine>), 371 Trailer(BoxedTemplateProperty<'repo, Trailer>), 372 TrailerList(BoxedTemplateProperty<'repo, Vec<Trailer>>), 373} 374 375template_builder::impl_core_property_wrappers!(<'repo> CommitTemplatePropertyKind<'repo> => Core); 376template_builder::impl_property_wrappers!(<'repo> CommitTemplatePropertyKind<'repo> { 377 Commit(Commit), 378 CommitOpt(Option<Commit>), 379 CommitList(Vec<Commit>), 380 CommitRef(Rc<CommitRef>), 381 CommitRefOpt(Option<Rc<CommitRef>>), 382 CommitRefList(Vec<Rc<CommitRef>>), 383 RefSymbol(RefSymbolBuf), 384 RefSymbolOpt(Option<RefSymbolBuf>), 385 RepoPath(RepoPathBuf), 386 RepoPathOpt(Option<RepoPathBuf>), 387 ChangeId(ChangeId), 388 CommitId(CommitId), 389 ShortestIdPrefix(ShortestIdPrefix), 390 TreeDiff(TreeDiff), 391 TreeDiffEntry(TreeDiffEntry), 392 TreeDiffEntryList(Vec<TreeDiffEntry>), 393 TreeEntry(TreeEntry), 394 DiffStats(DiffStatsFormatted<'repo>), 395 CryptographicSignatureOpt(Option<CryptographicSignature>), 396 AnnotationLine(AnnotationLine), 397 Trailer(Trailer), 398 TrailerList(Vec<Trailer>), 399}); 400 401impl<'repo> CoreTemplatePropertyVar<'repo> for CommitTemplatePropertyKind<'repo> { 402 fn wrap_template(template: Box<dyn Template + 'repo>) -> Self { 403 Self::Core(CoreTemplatePropertyKind::wrap_template(template)) 404 } 405 406 fn wrap_list_template(template: Box<dyn ListTemplate + 'repo>) -> Self { 407 Self::Core(CoreTemplatePropertyKind::wrap_list_template(template)) 408 } 409 410 fn type_name(&self) -> &'static str { 411 match self { 412 Self::Core(property) => property.type_name(), 413 Self::Commit(_) => "Commit", 414 Self::CommitOpt(_) => "Option<Commit>", 415 Self::CommitList(_) => "List<Commit>", 416 Self::CommitRef(_) => "CommitRef", 417 Self::CommitRefOpt(_) => "Option<CommitRef>", 418 Self::CommitRefList(_) => "List<CommitRef>", 419 Self::RefSymbol(_) => "RefSymbol", 420 Self::RefSymbolOpt(_) => "Option<RefSymbol>", 421 Self::RepoPath(_) => "RepoPath", 422 Self::RepoPathOpt(_) => "Option<RepoPath>", 423 Self::ChangeId(_) => "ChangeId", 424 Self::CommitId(_) => "CommitId", 425 Self::ShortestIdPrefix(_) => "ShortestIdPrefix", 426 Self::TreeDiff(_) => "TreeDiff", 427 Self::TreeDiffEntry(_) => "TreeDiffEntry", 428 Self::TreeDiffEntryList(_) => "List<TreeDiffEntry>", 429 Self::TreeEntry(_) => "TreeEntry", 430 Self::DiffStats(_) => "DiffStats", 431 Self::CryptographicSignatureOpt(_) => "Option<CryptographicSignature>", 432 Self::AnnotationLine(_) => "AnnotationLine", 433 Self::Trailer(_) => "Trailer", 434 Self::TrailerList(_) => "List<Trailer>", 435 } 436 } 437 438 fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'repo, bool>> { 439 match self { 440 Self::Core(property) => property.try_into_boolean(), 441 Self::Commit(_) => None, 442 Self::CommitOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()), 443 Self::CommitList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()), 444 Self::CommitRef(_) => None, 445 Self::CommitRefOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()), 446 Self::CommitRefList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()), 447 Self::RefSymbol(_) => None, 448 Self::RefSymbolOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()), 449 Self::RepoPath(_) => None, 450 Self::RepoPathOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()), 451 Self::ChangeId(_) => None, 452 Self::CommitId(_) => None, 453 Self::ShortestIdPrefix(_) => None, 454 // TODO: boolean cast could be implemented, but explicit 455 // diff.empty() method might be better. 456 Self::TreeDiff(_) => None, 457 Self::TreeDiffEntry(_) => None, 458 Self::TreeDiffEntryList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()), 459 Self::TreeEntry(_) => None, 460 Self::DiffStats(_) => None, 461 Self::CryptographicSignatureOpt(property) => { 462 Some(property.map(|sig| sig.is_some()).into_dyn()) 463 } 464 Self::AnnotationLine(_) => None, 465 Self::Trailer(_) => None, 466 Self::TrailerList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()), 467 } 468 } 469 470 fn try_into_integer(self) -> Option<BoxedTemplateProperty<'repo, i64>> { 471 match self { 472 Self::Core(property) => property.try_into_integer(), 473 _ => None, 474 } 475 } 476 477 fn try_into_plain_text(self) -> Option<BoxedTemplateProperty<'repo, String>> { 478 match self { 479 Self::Core(property) => property.try_into_plain_text(), 480 Self::RefSymbol(property) => Some(property.map(|RefSymbolBuf(s)| s).into_dyn()), 481 Self::RefSymbolOpt(property) => Some( 482 property 483 .try_unwrap("RefSymbol") 484 .map(|RefSymbolBuf(s)| s) 485 .into_dyn(), 486 ), 487 _ => { 488 let template = self.try_into_template()?; 489 Some(PlainTextFormattedProperty::new(template).into_dyn()) 490 } 491 } 492 } 493 494 fn try_into_template(self) -> Option<Box<dyn Template + 'repo>> { 495 match self { 496 Self::Core(property) => property.try_into_template(), 497 Self::Commit(_) => None, 498 Self::CommitOpt(_) => None, 499 Self::CommitList(_) => None, 500 Self::CommitRef(property) => Some(property.into_template()), 501 Self::CommitRefOpt(property) => Some(property.into_template()), 502 Self::CommitRefList(property) => Some(property.into_template()), 503 Self::RefSymbol(property) => Some(property.into_template()), 504 Self::RefSymbolOpt(property) => Some(property.into_template()), 505 Self::RepoPath(property) => Some(property.into_template()), 506 Self::RepoPathOpt(property) => Some(property.into_template()), 507 Self::ChangeId(property) => Some(property.into_template()), 508 Self::CommitId(property) => Some(property.into_template()), 509 Self::ShortestIdPrefix(property) => Some(property.into_template()), 510 Self::TreeDiff(_) => None, 511 Self::TreeDiffEntry(_) => None, 512 Self::TreeDiffEntryList(_) => None, 513 Self::TreeEntry(_) => None, 514 Self::DiffStats(property) => Some(property.into_template()), 515 Self::CryptographicSignatureOpt(_) => None, 516 Self::AnnotationLine(_) => None, 517 Self::Trailer(property) => Some(property.into_template()), 518 Self::TrailerList(property) => Some(property.into_template()), 519 } 520 } 521 522 fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'repo, bool>> { 523 type Core<'repo> = CoreTemplatePropertyKind<'repo>; 524 match (self, other) { 525 (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_eq(rhs), 526 (Self::Core(Core::String(lhs)), Self::RefSymbol(rhs)) => { 527 Some((lhs, rhs).map(|(l, r)| RefSymbolBuf(l) == r).into_dyn()) 528 } 529 (Self::Core(Core::String(lhs)), Self::RefSymbolOpt(rhs)) => Some( 530 (lhs, rhs) 531 .map(|(l, r)| Some(RefSymbolBuf(l)) == r) 532 .into_dyn(), 533 ), 534 (Self::RefSymbol(lhs), Self::Core(Core::String(rhs))) => { 535 Some((lhs, rhs).map(|(l, r)| l == RefSymbolBuf(r)).into_dyn()) 536 } 537 (Self::RefSymbol(lhs), Self::RefSymbol(rhs)) => { 538 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn()) 539 } 540 (Self::RefSymbol(lhs), Self::RefSymbolOpt(rhs)) => { 541 Some((lhs, rhs).map(|(l, r)| Some(l) == r).into_dyn()) 542 } 543 (Self::RefSymbolOpt(lhs), Self::Core(Core::String(rhs))) => Some( 544 (lhs, rhs) 545 .map(|(l, r)| l == Some(RefSymbolBuf(r))) 546 .into_dyn(), 547 ), 548 (Self::RefSymbolOpt(lhs), Self::RefSymbol(rhs)) => { 549 Some((lhs, rhs).map(|(l, r)| l == Some(r)).into_dyn()) 550 } 551 (Self::RefSymbolOpt(lhs), Self::RefSymbolOpt(rhs)) => { 552 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn()) 553 } 554 (Self::Core(_), _) => None, 555 (Self::Commit(_), _) => None, 556 (Self::CommitOpt(_), _) => None, 557 (Self::CommitList(_), _) => None, 558 (Self::CommitRef(_), _) => None, 559 (Self::CommitRefOpt(_), _) => None, 560 (Self::CommitRefList(_), _) => None, 561 (Self::RefSymbol(_), _) => None, 562 (Self::RefSymbolOpt(_), _) => None, 563 (Self::RepoPath(_), _) => None, 564 (Self::RepoPathOpt(_), _) => None, 565 (Self::ChangeId(_), _) => None, 566 (Self::CommitId(_), _) => None, 567 (Self::ShortestIdPrefix(_), _) => None, 568 (Self::TreeDiff(_), _) => None, 569 (Self::TreeDiffEntry(_), _) => None, 570 (Self::TreeDiffEntryList(_), _) => None, 571 (Self::TreeEntry(_), _) => None, 572 (Self::DiffStats(_), _) => None, 573 (Self::CryptographicSignatureOpt(_), _) => None, 574 (Self::AnnotationLine(_), _) => None, 575 (Self::Trailer(_), _) => None, 576 (Self::TrailerList(_), _) => None, 577 } 578 } 579 580 fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'repo, Ordering>> { 581 match (self, other) { 582 (Self::Core(lhs), Self::Core(rhs)) => lhs.try_into_cmp(rhs), 583 (Self::Core(_), _) => None, 584 (Self::Commit(_), _) => None, 585 (Self::CommitOpt(_), _) => None, 586 (Self::CommitList(_), _) => None, 587 (Self::CommitRef(_), _) => None, 588 (Self::CommitRefOpt(_), _) => None, 589 (Self::CommitRefList(_), _) => None, 590 (Self::RefSymbol(_), _) => None, 591 (Self::RefSymbolOpt(_), _) => None, 592 (Self::RepoPath(_), _) => None, 593 (Self::RepoPathOpt(_), _) => None, 594 (Self::ChangeId(_), _) => None, 595 (Self::CommitId(_), _) => None, 596 (Self::ShortestIdPrefix(_), _) => None, 597 (Self::TreeDiff(_), _) => None, 598 (Self::TreeDiffEntry(_), _) => None, 599 (Self::TreeDiffEntryList(_), _) => None, 600 (Self::TreeEntry(_), _) => None, 601 (Self::DiffStats(_), _) => None, 602 (Self::CryptographicSignatureOpt(_), _) => None, 603 (Self::AnnotationLine(_), _) => None, 604 (Self::Trailer(_), _) => None, 605 (Self::TrailerList(_), _) => None, 606 } 607 } 608} 609 610/// Table of functions that translate method call node of self type `T`. 611pub type CommitTemplateBuildMethodFnMap<'repo, T> = 612 TemplateBuildMethodFnMap<'repo, CommitTemplateLanguage<'repo>, T>; 613 614/// Symbol table of methods available in the commit template. 615pub struct CommitTemplateBuildFnTable<'repo> { 616 pub core: CoreTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>, 617 pub commit_methods: CommitTemplateBuildMethodFnMap<'repo, Commit>, 618 pub commit_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<Commit>>, 619 pub commit_ref_methods: CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>>, 620 pub commit_ref_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<Rc<CommitRef>>>, 621 pub repo_path_methods: CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf>, 622 pub change_id_methods: CommitTemplateBuildMethodFnMap<'repo, ChangeId>, 623 pub commit_id_methods: CommitTemplateBuildMethodFnMap<'repo, CommitId>, 624 pub shortest_id_prefix_methods: CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix>, 625 pub tree_diff_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiff>, 626 pub tree_diff_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry>, 627 pub tree_diff_entry_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<TreeDiffEntry>>, 628 pub tree_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeEntry>, 629 pub diff_stats_methods: CommitTemplateBuildMethodFnMap<'repo, DiffStats>, 630 pub cryptographic_signature_methods: 631 CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature>, 632 pub annotation_line_methods: CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>, 633 pub trailer_methods: CommitTemplateBuildMethodFnMap<'repo, Trailer>, 634 pub trailer_list_methods: CommitTemplateBuildMethodFnMap<'repo, Vec<Trailer>>, 635} 636 637impl<'repo> CommitTemplateBuildFnTable<'repo> { 638 /// Creates new symbol table containing the builtin methods. 639 fn builtin() -> Self { 640 CommitTemplateBuildFnTable { 641 core: CoreTemplateBuildFnTable::builtin(), 642 commit_methods: builtin_commit_methods(), 643 commit_list_methods: template_builder::builtin_unformattable_list_methods(), 644 commit_ref_methods: builtin_commit_ref_methods(), 645 commit_ref_list_methods: template_builder::builtin_formattable_list_methods(), 646 repo_path_methods: builtin_repo_path_methods(), 647 change_id_methods: builtin_change_id_methods(), 648 commit_id_methods: builtin_commit_id_methods(), 649 shortest_id_prefix_methods: builtin_shortest_id_prefix_methods(), 650 tree_diff_methods: builtin_tree_diff_methods(), 651 tree_diff_entry_methods: builtin_tree_diff_entry_methods(), 652 tree_diff_entry_list_methods: template_builder::builtin_unformattable_list_methods(), 653 tree_entry_methods: builtin_tree_entry_methods(), 654 diff_stats_methods: builtin_diff_stats_methods(), 655 cryptographic_signature_methods: builtin_cryptographic_signature_methods(), 656 annotation_line_methods: builtin_annotation_line_methods(), 657 trailer_methods: builtin_trailer_methods(), 658 trailer_list_methods: builtin_trailer_list_methods(), 659 } 660 } 661 662 pub fn empty() -> Self { 663 CommitTemplateBuildFnTable { 664 core: CoreTemplateBuildFnTable::empty(), 665 commit_methods: HashMap::new(), 666 commit_list_methods: HashMap::new(), 667 commit_ref_methods: HashMap::new(), 668 commit_ref_list_methods: HashMap::new(), 669 repo_path_methods: HashMap::new(), 670 change_id_methods: HashMap::new(), 671 commit_id_methods: HashMap::new(), 672 shortest_id_prefix_methods: HashMap::new(), 673 tree_diff_methods: HashMap::new(), 674 tree_diff_entry_methods: HashMap::new(), 675 tree_diff_entry_list_methods: HashMap::new(), 676 tree_entry_methods: HashMap::new(), 677 diff_stats_methods: HashMap::new(), 678 cryptographic_signature_methods: HashMap::new(), 679 annotation_line_methods: HashMap::new(), 680 trailer_methods: HashMap::new(), 681 trailer_list_methods: HashMap::new(), 682 } 683 } 684 685 fn merge(&mut self, extension: CommitTemplateBuildFnTable<'repo>) { 686 let CommitTemplateBuildFnTable { 687 core, 688 commit_methods, 689 commit_list_methods, 690 commit_ref_methods, 691 commit_ref_list_methods, 692 repo_path_methods, 693 change_id_methods, 694 commit_id_methods, 695 shortest_id_prefix_methods, 696 tree_diff_methods, 697 tree_diff_entry_methods, 698 tree_diff_entry_list_methods, 699 tree_entry_methods, 700 diff_stats_methods, 701 cryptographic_signature_methods, 702 annotation_line_methods, 703 trailer_methods, 704 trailer_list_methods, 705 } = extension; 706 707 self.core.merge(core); 708 merge_fn_map(&mut self.commit_methods, commit_methods); 709 merge_fn_map(&mut self.commit_list_methods, commit_list_methods); 710 merge_fn_map(&mut self.commit_ref_methods, commit_ref_methods); 711 merge_fn_map(&mut self.commit_ref_list_methods, commit_ref_list_methods); 712 merge_fn_map(&mut self.repo_path_methods, repo_path_methods); 713 merge_fn_map(&mut self.change_id_methods, change_id_methods); 714 merge_fn_map(&mut self.commit_id_methods, commit_id_methods); 715 merge_fn_map( 716 &mut self.shortest_id_prefix_methods, 717 shortest_id_prefix_methods, 718 ); 719 merge_fn_map(&mut self.tree_diff_methods, tree_diff_methods); 720 merge_fn_map(&mut self.tree_diff_entry_methods, tree_diff_entry_methods); 721 merge_fn_map( 722 &mut self.tree_diff_entry_list_methods, 723 tree_diff_entry_list_methods, 724 ); 725 merge_fn_map(&mut self.tree_entry_methods, tree_entry_methods); 726 merge_fn_map(&mut self.diff_stats_methods, diff_stats_methods); 727 merge_fn_map( 728 &mut self.cryptographic_signature_methods, 729 cryptographic_signature_methods, 730 ); 731 merge_fn_map(&mut self.annotation_line_methods, annotation_line_methods); 732 merge_fn_map(&mut self.trailer_methods, trailer_methods); 733 merge_fn_map(&mut self.trailer_list_methods, trailer_list_methods); 734 } 735} 736 737#[derive(Default)] 738pub struct CommitKeywordCache<'repo> { 739 // Build index lazily, and Rc to get away from &self lifetime. 740 bookmarks_index: OnceCell<Rc<CommitRefsIndex>>, 741 tags_index: OnceCell<Rc<CommitRefsIndex>>, 742 git_refs_index: OnceCell<Rc<CommitRefsIndex>>, 743 is_immutable_fn: OnceCell<Rc<RevsetContainingFn<'repo>>>, 744} 745 746impl<'repo> CommitKeywordCache<'repo> { 747 pub fn bookmarks_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> { 748 self.bookmarks_index 749 .get_or_init(|| Rc::new(build_bookmarks_index(repo))) 750 } 751 752 pub fn tags_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> { 753 self.tags_index 754 .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().tags()))) 755 } 756 757 pub fn git_refs_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> { 758 self.git_refs_index 759 .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().git_refs()))) 760 } 761 762 pub fn is_immutable_fn( 763 &self, 764 language: &CommitTemplateLanguage<'repo>, 765 span: pest::Span<'_>, 766 ) -> TemplateParseResult<&Rc<RevsetContainingFn<'repo>>> { 767 // Alternatively, a negated (i.e. visible mutable) set could be computed. 768 // It's usually smaller than the immutable set. The revset engine can also 769 // optimize "::<recent_heads>" query to use bitset-based implementation. 770 self.is_immutable_fn.get_or_try_init(|| { 771 let expression = &language.immutable_expression; 772 let revset = evaluate_revset_expression(language, span, expression)?; 773 Ok(revset.containing_fn().into()) 774 }) 775 } 776} 777 778fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Commit> { 779 // Not using maplit::hashmap!{} or custom declarative macro here because 780 // code completion inside macro is quite restricted. 781 let mut map = CommitTemplateBuildMethodFnMap::<Commit>::new(); 782 map.insert( 783 "description", 784 |_language, _diagnostics, _build_ctx, self_property, function| { 785 function.expect_no_arguments()?; 786 let out_property = 787 self_property.map(|commit| text_util::complete_newline(commit.description())); 788 Ok(out_property.into_dyn_wrapped()) 789 }, 790 ); 791 map.insert( 792 "trailers", 793 |_language, _diagnostics, _build_ctx, self_property, function| { 794 function.expect_no_arguments()?; 795 let out_property = self_property 796 .map(|commit| trailer::parse_description_trailers(commit.description())); 797 Ok(out_property.into_dyn_wrapped()) 798 }, 799 ); 800 map.insert( 801 "change_id", 802 |_language, _diagnostics, _build_ctx, self_property, function| { 803 function.expect_no_arguments()?; 804 let out_property = self_property.map(|commit| commit.change_id().to_owned()); 805 Ok(out_property.into_dyn_wrapped()) 806 }, 807 ); 808 map.insert( 809 "commit_id", 810 |_language, _diagnostics, _build_ctx, self_property, function| { 811 function.expect_no_arguments()?; 812 let out_property = self_property.map(|commit| commit.id().to_owned()); 813 Ok(out_property.into_dyn_wrapped()) 814 }, 815 ); 816 map.insert( 817 "parents", 818 |_language, _diagnostics, _build_ctx, self_property, function| { 819 function.expect_no_arguments()?; 820 let out_property = self_property.and_then(|commit| { 821 let commits: Vec<_> = commit.parents().try_collect()?; 822 Ok(commits) 823 }); 824 Ok(out_property.into_dyn_wrapped()) 825 }, 826 ); 827 map.insert( 828 "author", 829 |_language, _diagnostics, _build_ctx, self_property, function| { 830 function.expect_no_arguments()?; 831 let out_property = self_property.map(|commit| commit.author().clone()); 832 Ok(out_property.into_dyn_wrapped()) 833 }, 834 ); 835 map.insert( 836 "committer", 837 |_language, _diagnostics, _build_ctx, self_property, function| { 838 function.expect_no_arguments()?; 839 let out_property = self_property.map(|commit| commit.committer().clone()); 840 Ok(out_property.into_dyn_wrapped()) 841 }, 842 ); 843 map.insert( 844 "mine", 845 |language, _diagnostics, _build_ctx, self_property, function| { 846 function.expect_no_arguments()?; 847 let user_email = language.revset_parse_context.user_email.to_owned(); 848 let out_property = self_property.map(move |commit| commit.author().email == user_email); 849 Ok(out_property.into_dyn_wrapped()) 850 }, 851 ); 852 map.insert( 853 "signature", 854 |_language, _diagnostics, _build_ctx, self_property, function| { 855 function.expect_no_arguments()?; 856 let out_property = self_property.map(CryptographicSignature::new); 857 Ok(out_property.into_dyn_wrapped()) 858 }, 859 ); 860 map.insert( 861 "working_copies", 862 |language, _diagnostics, _build_ctx, self_property, function| { 863 function.expect_no_arguments()?; 864 let repo = language.repo; 865 let out_property = self_property.map(|commit| extract_working_copies(repo, &commit)); 866 Ok(out_property.into_dyn_wrapped()) 867 }, 868 ); 869 map.insert( 870 "current_working_copy", 871 |language, _diagnostics, _build_ctx, self_property, function| { 872 function.expect_no_arguments()?; 873 let repo = language.repo; 874 let name = language.workspace_name.clone(); 875 let out_property = self_property 876 .map(move |commit| Some(commit.id()) == repo.view().get_wc_commit_id(&name)); 877 Ok(out_property.into_dyn_wrapped()) 878 }, 879 ); 880 map.insert( 881 "bookmarks", 882 |language, _diagnostics, _build_ctx, self_property, function| { 883 function.expect_no_arguments()?; 884 let index = language 885 .keyword_cache 886 .bookmarks_index(language.repo) 887 .clone(); 888 let out_property = self_property.map(move |commit| { 889 index 890 .get(commit.id()) 891 .iter() 892 .filter(|commit_ref| commit_ref.is_local() || !commit_ref.synced) 893 .cloned() 894 .collect_vec() 895 }); 896 Ok(out_property.into_dyn_wrapped()) 897 }, 898 ); 899 map.insert( 900 "local_bookmarks", 901 |language, _diagnostics, _build_ctx, self_property, function| { 902 function.expect_no_arguments()?; 903 let index = language 904 .keyword_cache 905 .bookmarks_index(language.repo) 906 .clone(); 907 let out_property = self_property.map(move |commit| { 908 index 909 .get(commit.id()) 910 .iter() 911 .filter(|commit_ref| commit_ref.is_local()) 912 .cloned() 913 .collect_vec() 914 }); 915 Ok(out_property.into_dyn_wrapped()) 916 }, 917 ); 918 map.insert( 919 "remote_bookmarks", 920 |language, _diagnostics, _build_ctx, self_property, function| { 921 function.expect_no_arguments()?; 922 let index = language 923 .keyword_cache 924 .bookmarks_index(language.repo) 925 .clone(); 926 let out_property = self_property.map(move |commit| { 927 index 928 .get(commit.id()) 929 .iter() 930 .filter(|commit_ref| commit_ref.is_remote()) 931 .cloned() 932 .collect_vec() 933 }); 934 Ok(out_property.into_dyn_wrapped()) 935 }, 936 ); 937 map.insert( 938 "tags", 939 |language, _diagnostics, _build_ctx, self_property, function| { 940 function.expect_no_arguments()?; 941 let index = language.keyword_cache.tags_index(language.repo).clone(); 942 let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec()); 943 Ok(out_property.into_dyn_wrapped()) 944 }, 945 ); 946 map.insert( 947 "git_refs", 948 |language, _diagnostics, _build_ctx, self_property, function| { 949 function.expect_no_arguments()?; 950 let index = language.keyword_cache.git_refs_index(language.repo).clone(); 951 let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec()); 952 Ok(out_property.into_dyn_wrapped()) 953 }, 954 ); 955 map.insert( 956 "git_head", 957 |language, _diagnostics, _build_ctx, self_property, function| { 958 function.expect_no_arguments()?; 959 let repo = language.repo; 960 let out_property = self_property.map(|commit| { 961 let target = repo.view().git_head(); 962 target.added_ids().contains(commit.id()) 963 }); 964 Ok(out_property.into_dyn_wrapped()) 965 }, 966 ); 967 map.insert( 968 "divergent", 969 |language, _diagnostics, _build_ctx, self_property, function| { 970 function.expect_no_arguments()?; 971 let repo = language.repo; 972 let out_property = self_property.map(|commit| { 973 // The given commit could be hidden in e.g. `jj evolog`. 974 let maybe_entries = repo.resolve_change_id(commit.change_id()); 975 maybe_entries.map_or(0, |entries| entries.len()) > 1 976 }); 977 Ok(out_property.into_dyn_wrapped()) 978 }, 979 ); 980 map.insert( 981 "hidden", 982 |language, _diagnostics, _build_ctx, self_property, function| { 983 function.expect_no_arguments()?; 984 let repo = language.repo; 985 let out_property = self_property.map(|commit| commit.is_hidden(repo)); 986 Ok(out_property.into_dyn_wrapped()) 987 }, 988 ); 989 map.insert( 990 "immutable", 991 |language, _diagnostics, _build_ctx, self_property, function| { 992 function.expect_no_arguments()?; 993 let is_immutable = language 994 .keyword_cache 995 .is_immutable_fn(language, function.name_span)? 996 .clone(); 997 let out_property = self_property.and_then(move |commit| Ok(is_immutable(commit.id())?)); 998 Ok(out_property.into_dyn_wrapped()) 999 }, 1000 ); 1001 map.insert( 1002 "contained_in", 1003 |language, diagnostics, _build_ctx, self_property, function| { 1004 let [revset_node] = function.expect_exact_arguments()?; 1005 1006 let is_contained = 1007 template_parser::expect_string_literal_with(revset_node, |revset, span| { 1008 Ok(evaluate_user_revset(language, diagnostics, span, revset)?.containing_fn()) 1009 })?; 1010 1011 let out_property = self_property.and_then(move |commit| Ok(is_contained(commit.id())?)); 1012 Ok(out_property.into_dyn_wrapped()) 1013 }, 1014 ); 1015 map.insert( 1016 "conflict", 1017 |_language, _diagnostics, _build_ctx, self_property, function| { 1018 function.expect_no_arguments()?; 1019 let out_property = self_property.and_then(|commit| Ok(commit.has_conflict()?)); 1020 Ok(out_property.into_dyn_wrapped()) 1021 }, 1022 ); 1023 map.insert( 1024 "empty", 1025 |language, _diagnostics, _build_ctx, self_property, function| { 1026 function.expect_no_arguments()?; 1027 let repo = language.repo; 1028 let out_property = self_property.and_then(|commit| Ok(commit.is_empty(repo)?)); 1029 Ok(out_property.into_dyn_wrapped()) 1030 }, 1031 ); 1032 map.insert( 1033 "diff", 1034 |language, diagnostics, _build_ctx, self_property, function| { 1035 let ([], [files_node]) = function.expect_arguments()?; 1036 let files = if let Some(node) = files_node { 1037 expect_fileset_literal(diagnostics, node, language.path_converter)? 1038 } else { 1039 // TODO: defaults to CLI path arguments? 1040 // https://github.com/jj-vcs/jj/issues/2933#issuecomment-1925870731 1041 FilesetExpression::all() 1042 }; 1043 let repo = language.repo; 1044 let matcher: Rc<dyn Matcher> = files.to_matcher().into(); 1045 let out_property = self_property 1046 .and_then(move |commit| Ok(TreeDiff::from_commit(repo, &commit, matcher.clone())?)); 1047 Ok(out_property.into_dyn_wrapped()) 1048 }, 1049 ); 1050 map.insert( 1051 "root", 1052 |language, _diagnostics, _build_ctx, self_property, function| { 1053 function.expect_no_arguments()?; 1054 let repo = language.repo; 1055 let out_property = 1056 self_property.map(|commit| commit.id() == repo.store().root_commit_id()); 1057 Ok(out_property.into_dyn_wrapped()) 1058 }, 1059 ); 1060 map 1061} 1062 1063// TODO: return Vec<String> 1064fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String { 1065 let wc_commit_ids = repo.view().wc_commit_ids(); 1066 if wc_commit_ids.len() <= 1 { 1067 return "".to_string(); 1068 } 1069 let mut names = vec![]; 1070 for (name, wc_commit_id) in wc_commit_ids { 1071 if wc_commit_id == commit.id() { 1072 names.push(format!("{}@", name.as_symbol())); 1073 } 1074 } 1075 names.join(" ") 1076} 1077 1078fn expect_fileset_literal( 1079 diagnostics: &mut TemplateDiagnostics, 1080 node: &ExpressionNode, 1081 path_converter: &RepoPathUiConverter, 1082) -> Result<FilesetExpression, TemplateParseError> { 1083 template_parser::expect_string_literal_with(node, |text, span| { 1084 let mut inner_diagnostics = FilesetDiagnostics::new(); 1085 let expression = 1086 fileset::parse(&mut inner_diagnostics, text, path_converter).map_err(|err| { 1087 TemplateParseError::expression("In fileset expression", span).with_source(err) 1088 })?; 1089 diagnostics.extend_with(inner_diagnostics, |diag| { 1090 TemplateParseError::expression("In fileset expression", span).with_source(diag) 1091 }); 1092 Ok(expression) 1093 }) 1094} 1095 1096fn evaluate_revset_expression<'repo>( 1097 language: &CommitTemplateLanguage<'repo>, 1098 span: pest::Span<'_>, 1099 expression: &UserRevsetExpression, 1100) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> { 1101 let make_error = || TemplateParseError::expression("Failed to evaluate revset", span); 1102 let repo = language.repo; 1103 let symbol_resolver = revset_util::default_symbol_resolver( 1104 repo, 1105 language.revset_parse_context.extensions.symbol_resolvers(), 1106 language.id_prefix_context, 1107 ); 1108 let revset = expression 1109 .resolve_user_expression(repo, &symbol_resolver) 1110 .map_err(|err| make_error().with_source(err))? 1111 .evaluate(repo) 1112 .map_err(|err| make_error().with_source(err))?; 1113 Ok(revset) 1114} 1115 1116fn evaluate_user_revset<'repo>( 1117 language: &CommitTemplateLanguage<'repo>, 1118 diagnostics: &mut TemplateDiagnostics, 1119 span: pest::Span<'_>, 1120 revset: &str, 1121) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> { 1122 let mut inner_diagnostics = RevsetDiagnostics::new(); 1123 let (expression, modifier) = revset::parse_with_modifier( 1124 &mut inner_diagnostics, 1125 revset, 1126 &language.revset_parse_context, 1127 ) 1128 .map_err(|err| TemplateParseError::expression("In revset expression", span).with_source(err))?; 1129 diagnostics.extend_with(inner_diagnostics, |diag| { 1130 TemplateParseError::expression("In revset expression", span).with_source(diag) 1131 }); 1132 let (None | Some(RevsetModifier::All)) = modifier; 1133 1134 evaluate_revset_expression(language, span, &expression) 1135} 1136 1137/// Bookmark or tag name with metadata. 1138#[derive(Debug)] 1139pub struct CommitRef { 1140 // Not using Ref/GitRef/RemoteName types here because it would be overly 1141 // complex to generalize the name type as T: RefName|GitRefName. 1142 /// Local name. 1143 name: RefSymbolBuf, 1144 /// Remote name if this is a remote or Git-tracking ref. 1145 remote: Option<RefSymbolBuf>, 1146 /// Target commit ids. 1147 target: RefTarget, 1148 /// Local ref metadata which tracks this remote ref. 1149 tracking_ref: Option<TrackingRef>, 1150 /// Local ref is synchronized with all tracking remotes, or tracking remote 1151 /// ref is synchronized with the local. 1152 synced: bool, 1153} 1154 1155#[derive(Debug)] 1156struct TrackingRef { 1157 /// Local ref target which tracks the other remote ref. 1158 target: RefTarget, 1159 /// Number of commits ahead of the tracking `target`. 1160 ahead_count: OnceCell<SizeHint>, 1161 /// Number of commits behind of the tracking `target`. 1162 behind_count: OnceCell<SizeHint>, 1163} 1164 1165impl CommitRef { 1166 // CommitRef is wrapped by Rc<T> to make it cheaply cloned and share 1167 // lazy-evaluation results across clones. 1168 1169 /// Creates local ref representation which might track some of the 1170 /// `remote_refs`. 1171 pub fn local<'a>( 1172 name: impl Into<String>, 1173 target: RefTarget, 1174 remote_refs: impl IntoIterator<Item = &'a RemoteRef>, 1175 ) -> Rc<Self> { 1176 let synced = remote_refs 1177 .into_iter() 1178 .all(|remote_ref| !remote_ref.is_tracked() || remote_ref.target == target); 1179 Rc::new(CommitRef { 1180 name: RefSymbolBuf(name.into()), 1181 remote: None, 1182 target, 1183 tracking_ref: None, 1184 synced, 1185 }) 1186 } 1187 1188 /// Creates local ref representation which doesn't track any remote refs. 1189 pub fn local_only(name: impl Into<String>, target: RefTarget) -> Rc<Self> { 1190 Self::local(name, target, []) 1191 } 1192 1193 /// Creates remote ref representation which might be tracked by a local ref 1194 /// pointing to the `local_target`. 1195 pub fn remote( 1196 name: impl Into<String>, 1197 remote_name: impl Into<String>, 1198 remote_ref: RemoteRef, 1199 local_target: &RefTarget, 1200 ) -> Rc<Self> { 1201 let synced = remote_ref.is_tracked() && remote_ref.target == *local_target; 1202 let tracking_ref = remote_ref.is_tracked().then(|| { 1203 let count = if synced { 1204 OnceCell::from((0, Some(0))) // fast path for synced remotes 1205 } else { 1206 OnceCell::new() 1207 }; 1208 TrackingRef { 1209 target: local_target.clone(), 1210 ahead_count: count.clone(), 1211 behind_count: count, 1212 } 1213 }); 1214 Rc::new(CommitRef { 1215 name: RefSymbolBuf(name.into()), 1216 remote: Some(RefSymbolBuf(remote_name.into())), 1217 target: remote_ref.target, 1218 tracking_ref, 1219 synced, 1220 }) 1221 } 1222 1223 /// Creates remote ref representation which isn't tracked by a local ref. 1224 pub fn remote_only( 1225 name: impl Into<String>, 1226 remote_name: impl Into<String>, 1227 target: RefTarget, 1228 ) -> Rc<Self> { 1229 Rc::new(CommitRef { 1230 name: RefSymbolBuf(name.into()), 1231 remote: Some(RefSymbolBuf(remote_name.into())), 1232 target, 1233 tracking_ref: None, 1234 synced: false, // has no local counterpart 1235 }) 1236 } 1237 1238 /// Local name. 1239 pub fn name(&self) -> &str { 1240 self.name.as_ref() 1241 } 1242 1243 /// Remote name if this is a remote or Git-tracking ref. 1244 pub fn remote_name(&self) -> Option<&str> { 1245 self.remote.as_ref().map(AsRef::as_ref) 1246 } 1247 1248 /// Target commit ids. 1249 pub fn target(&self) -> &RefTarget { 1250 &self.target 1251 } 1252 1253 /// Returns true if this is a local ref. 1254 pub fn is_local(&self) -> bool { 1255 self.remote.is_none() 1256 } 1257 1258 /// Returns true if this is a remote ref. 1259 pub fn is_remote(&self) -> bool { 1260 self.remote.is_some() 1261 } 1262 1263 /// Returns true if this ref points to no commit. 1264 pub fn is_absent(&self) -> bool { 1265 self.target.is_absent() 1266 } 1267 1268 /// Returns true if this ref points to any commit. 1269 pub fn is_present(&self) -> bool { 1270 self.target.is_present() 1271 } 1272 1273 /// Whether the ref target has conflicts. 1274 pub fn has_conflict(&self) -> bool { 1275 self.target.has_conflict() 1276 } 1277 1278 /// Returns true if this ref is tracked by a local ref. The local ref might 1279 /// have been deleted (but not pushed yet.) 1280 pub fn is_tracked(&self) -> bool { 1281 self.tracking_ref.is_some() 1282 } 1283 1284 /// Returns true if this ref is tracked by a local ref, and if the local ref 1285 /// is present. 1286 pub fn is_tracking_present(&self) -> bool { 1287 self.tracking_ref 1288 .as_ref() 1289 .is_some_and(|tracking| tracking.target.is_present()) 1290 } 1291 1292 /// Number of commits ahead of the tracking local ref. 1293 fn tracking_ahead_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> { 1294 let Some(tracking) = &self.tracking_ref else { 1295 return Err(TemplatePropertyError("Not a tracked remote ref".into())); 1296 }; 1297 tracking 1298 .ahead_count 1299 .get_or_try_init(|| { 1300 let self_ids = self.target.added_ids().cloned().collect_vec(); 1301 let other_ids = tracking.target.added_ids().cloned().collect_vec(); 1302 Ok(revset::walk_revs(repo, &self_ids, &other_ids)?.count_estimate()?) 1303 }) 1304 .copied() 1305 } 1306 1307 /// Number of commits behind of the tracking local ref. 1308 fn tracking_behind_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> { 1309 let Some(tracking) = &self.tracking_ref else { 1310 return Err(TemplatePropertyError("Not a tracked remote ref".into())); 1311 }; 1312 tracking 1313 .behind_count 1314 .get_or_try_init(|| { 1315 let self_ids = self.target.added_ids().cloned().collect_vec(); 1316 let other_ids = tracking.target.added_ids().cloned().collect_vec(); 1317 Ok(revset::walk_revs(repo, &other_ids, &self_ids)?.count_estimate()?) 1318 }) 1319 .copied() 1320 } 1321} 1322 1323// If wrapping with Rc<T> becomes common, add generic impl for Rc<T>. 1324impl Template for Rc<CommitRef> { 1325 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1326 write!(formatter.labeled("name"), "{}", self.name)?; 1327 if let Some(remote) = &self.remote { 1328 write!(formatter, "@")?; 1329 write!(formatter.labeled("remote"), "{remote}")?; 1330 } 1331 // Don't show both conflict and unsynced sigils as conflicted ref wouldn't 1332 // be pushed. 1333 if self.has_conflict() { 1334 write!(formatter, "??")?; 1335 } else if self.is_local() && !self.synced { 1336 write!(formatter, "*")?; 1337 } 1338 Ok(()) 1339 } 1340} 1341 1342impl Template for Vec<Rc<CommitRef>> { 1343 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1344 templater::format_joined(formatter, self, " ") 1345 } 1346} 1347 1348fn builtin_commit_ref_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>> { 1349 // Not using maplit::hashmap!{} or custom declarative macro here because 1350 // code completion inside macro is quite restricted. 1351 let mut map = CommitTemplateBuildMethodFnMap::<Rc<CommitRef>>::new(); 1352 map.insert( 1353 "name", 1354 |_language, _diagnostics, _build_ctx, self_property, function| { 1355 function.expect_no_arguments()?; 1356 let out_property = self_property.map(|commit_ref| commit_ref.name.clone()); 1357 Ok(out_property.into_dyn_wrapped()) 1358 }, 1359 ); 1360 map.insert( 1361 "remote", 1362 |_language, _diagnostics, _build_ctx, self_property, function| { 1363 function.expect_no_arguments()?; 1364 let out_property = self_property.map(|commit_ref| commit_ref.remote.clone()); 1365 Ok(out_property.into_dyn_wrapped()) 1366 }, 1367 ); 1368 map.insert( 1369 "present", 1370 |_language, _diagnostics, _build_ctx, self_property, function| { 1371 function.expect_no_arguments()?; 1372 let out_property = self_property.map(|commit_ref| commit_ref.is_present()); 1373 Ok(out_property.into_dyn_wrapped()) 1374 }, 1375 ); 1376 map.insert( 1377 "conflict", 1378 |_language, _diagnostics, _build_ctx, self_property, function| { 1379 function.expect_no_arguments()?; 1380 let out_property = self_property.map(|commit_ref| commit_ref.has_conflict()); 1381 Ok(out_property.into_dyn_wrapped()) 1382 }, 1383 ); 1384 map.insert( 1385 "normal_target", 1386 |language, _diagnostics, _build_ctx, self_property, function| { 1387 function.expect_no_arguments()?; 1388 let repo = language.repo; 1389 let out_property = self_property.and_then(|commit_ref| { 1390 let maybe_id = commit_ref.target.as_normal(); 1391 Ok(maybe_id.map(|id| repo.store().get_commit(id)).transpose()?) 1392 }); 1393 Ok(out_property.into_dyn_wrapped()) 1394 }, 1395 ); 1396 map.insert( 1397 "removed_targets", 1398 |language, _diagnostics, _build_ctx, self_property, function| { 1399 function.expect_no_arguments()?; 1400 let repo = language.repo; 1401 let out_property = self_property.and_then(|commit_ref| { 1402 let ids = commit_ref.target.removed_ids(); 1403 let commits: Vec<_> = ids.map(|id| repo.store().get_commit(id)).try_collect()?; 1404 Ok(commits) 1405 }); 1406 Ok(out_property.into_dyn_wrapped()) 1407 }, 1408 ); 1409 map.insert( 1410 "added_targets", 1411 |language, _diagnostics, _build_ctx, self_property, function| { 1412 function.expect_no_arguments()?; 1413 let repo = language.repo; 1414 let out_property = self_property.and_then(|commit_ref| { 1415 let ids = commit_ref.target.added_ids(); 1416 let commits: Vec<_> = ids.map(|id| repo.store().get_commit(id)).try_collect()?; 1417 Ok(commits) 1418 }); 1419 Ok(out_property.into_dyn_wrapped()) 1420 }, 1421 ); 1422 map.insert( 1423 "tracked", 1424 |_language, _diagnostics, _build_ctx, self_property, function| { 1425 function.expect_no_arguments()?; 1426 let out_property = self_property.map(|commit_ref| commit_ref.is_tracked()); 1427 Ok(out_property.into_dyn_wrapped()) 1428 }, 1429 ); 1430 map.insert( 1431 "tracking_present", 1432 |_language, _diagnostics, _build_ctx, self_property, function| { 1433 function.expect_no_arguments()?; 1434 let out_property = self_property.map(|commit_ref| commit_ref.is_tracking_present()); 1435 Ok(out_property.into_dyn_wrapped()) 1436 }, 1437 ); 1438 map.insert( 1439 "tracking_ahead_count", 1440 |language, _diagnostics, _build_ctx, self_property, function| { 1441 function.expect_no_arguments()?; 1442 let repo = language.repo; 1443 let out_property = 1444 self_property.and_then(|commit_ref| commit_ref.tracking_ahead_count(repo)); 1445 Ok(out_property.into_dyn_wrapped()) 1446 }, 1447 ); 1448 map.insert( 1449 "tracking_behind_count", 1450 |language, _diagnostics, _build_ctx, self_property, function| { 1451 function.expect_no_arguments()?; 1452 let repo = language.repo; 1453 let out_property = 1454 self_property.and_then(|commit_ref| commit_ref.tracking_behind_count(repo)); 1455 Ok(out_property.into_dyn_wrapped()) 1456 }, 1457 ); 1458 map 1459} 1460 1461/// Cache for reverse lookup refs. 1462#[derive(Clone, Debug, Default)] 1463pub struct CommitRefsIndex { 1464 index: HashMap<CommitId, Vec<Rc<CommitRef>>>, 1465} 1466 1467impl CommitRefsIndex { 1468 fn insert<'a>(&mut self, ids: impl IntoIterator<Item = &'a CommitId>, name: Rc<CommitRef>) { 1469 for id in ids { 1470 let commit_refs = self.index.entry(id.clone()).or_default(); 1471 commit_refs.push(name.clone()); 1472 } 1473 } 1474 1475 pub fn get(&self, id: &CommitId) -> &[Rc<CommitRef>] { 1476 self.index.get(id).map_or(&[], |refs: &Vec<_>| refs) 1477 } 1478} 1479 1480fn build_bookmarks_index(repo: &dyn Repo) -> CommitRefsIndex { 1481 let mut index = CommitRefsIndex::default(); 1482 for (bookmark_name, bookmark_target) in repo.view().bookmarks() { 1483 let local_target = bookmark_target.local_target; 1484 let remote_refs = bookmark_target.remote_refs; 1485 if local_target.is_present() { 1486 let commit_ref = CommitRef::local( 1487 bookmark_name, 1488 local_target.clone(), 1489 remote_refs.iter().map(|&(_, remote_ref)| remote_ref), 1490 ); 1491 index.insert(local_target.added_ids(), commit_ref); 1492 } 1493 for &(remote_name, remote_ref) in &remote_refs { 1494 let commit_ref = 1495 CommitRef::remote(bookmark_name, remote_name, remote_ref.clone(), local_target); 1496 index.insert(remote_ref.target.added_ids(), commit_ref); 1497 } 1498 } 1499 index 1500} 1501 1502fn build_commit_refs_index<'a, K: Into<String>>( 1503 ref_pairs: impl IntoIterator<Item = (K, &'a RefTarget)>, 1504) -> CommitRefsIndex { 1505 let mut index = CommitRefsIndex::default(); 1506 for (name, target) in ref_pairs { 1507 let commit_ref = CommitRef::local_only(name, target.clone()); 1508 index.insert(target.added_ids(), commit_ref); 1509 } 1510 index 1511} 1512 1513/// Wrapper to render ref/remote name in revset syntax. 1514#[derive(Clone, Debug, Eq, PartialEq)] 1515pub struct RefSymbolBuf(String); 1516 1517impl AsRef<str> for RefSymbolBuf { 1518 fn as_ref(&self) -> &str { 1519 &self.0 1520 } 1521} 1522 1523impl Display for RefSymbolBuf { 1524 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1525 f.pad(&revset::format_symbol(&self.0)) 1526 } 1527} 1528 1529impl Template for RefSymbolBuf { 1530 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1531 write!(formatter, "{self}") 1532 } 1533} 1534 1535impl Template for RepoPathBuf { 1536 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1537 write!(formatter, "{}", self.as_internal_file_string()) 1538 } 1539} 1540 1541fn builtin_repo_path_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf> { 1542 // Not using maplit::hashmap!{} or custom declarative macro here because 1543 // code completion inside macro is quite restricted. 1544 let mut map = CommitTemplateBuildMethodFnMap::<RepoPathBuf>::new(); 1545 map.insert( 1546 "display", 1547 |language, _diagnostics, _build_ctx, self_property, function| { 1548 function.expect_no_arguments()?; 1549 let path_converter = language.path_converter; 1550 let out_property = self_property.map(|path| path_converter.format_file_path(&path)); 1551 Ok(out_property.into_dyn_wrapped()) 1552 }, 1553 ); 1554 map.insert( 1555 "parent", 1556 |_language, _diagnostics, _build_ctx, self_property, function| { 1557 function.expect_no_arguments()?; 1558 let out_property = self_property.map(|path| Some(path.parent()?.to_owned())); 1559 Ok(out_property.into_dyn_wrapped()) 1560 }, 1561 ); 1562 map 1563} 1564 1565trait ShortestIdPrefixLen { 1566 fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> usize; 1567} 1568 1569impl ShortestIdPrefixLen for ChangeId { 1570 fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> usize { 1571 index.shortest_change_prefix_len(repo, self) 1572 } 1573} 1574 1575impl Template for ChangeId { 1576 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1577 write!(formatter, "{self}") 1578 } 1579} 1580 1581fn builtin_change_id_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, ChangeId> { 1582 let mut map = builtin_commit_or_change_id_methods::<ChangeId>(); 1583 map.insert( 1584 "normal_hex", 1585 |_language, _diagnostics, _build_ctx, self_property, function| { 1586 function.expect_no_arguments()?; 1587 // Note: this is _not_ the same as id.to_string(), which returns the 1588 // "reverse" hex (z-k), instead of the "forward" / normal hex 1589 // (0-9a-f) we want here. 1590 let out_property = self_property.map(|id| id.hex()); 1591 Ok(out_property.into_dyn_wrapped()) 1592 }, 1593 ); 1594 map 1595} 1596 1597impl ShortestIdPrefixLen for CommitId { 1598 fn shortest_prefix_len(&self, repo: &dyn Repo, index: &IdPrefixIndex) -> usize { 1599 index.shortest_commit_prefix_len(repo, self) 1600 } 1601} 1602 1603impl Template for CommitId { 1604 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1605 write!(formatter, "{self}") 1606 } 1607} 1608 1609fn builtin_commit_id_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, CommitId> { 1610 let mut map = builtin_commit_or_change_id_methods::<CommitId>(); 1611 // TODO: Remove in jj 0.36+ 1612 map.insert( 1613 "normal_hex", 1614 |_language, diagnostics, _build_ctx, self_property, function| { 1615 diagnostics.add_warning(TemplateParseError::expression( 1616 "commit_id.normal_hex() is deprecated; use stringify(commit_id) instead", 1617 function.name_span, 1618 )); 1619 function.expect_no_arguments()?; 1620 let out_property = self_property.map(|id| id.hex()); 1621 Ok(out_property.into_dyn_wrapped()) 1622 }, 1623 ); 1624 map 1625} 1626 1627fn builtin_commit_or_change_id_methods<'repo, O>() -> CommitTemplateBuildMethodFnMap<'repo, O> 1628where 1629 O: Display + ShortestIdPrefixLen + 'repo, 1630{ 1631 // Not using maplit::hashmap!{} or custom declarative macro here because 1632 // code completion inside macro is quite restricted. 1633 let mut map = CommitTemplateBuildMethodFnMap::<O>::new(); 1634 map.insert( 1635 "short", 1636 |language, diagnostics, build_ctx, self_property, function| { 1637 let ([], [len_node]) = function.expect_arguments()?; 1638 let len_property = len_node 1639 .map(|node| { 1640 template_builder::expect_usize_expression( 1641 language, 1642 diagnostics, 1643 build_ctx, 1644 node, 1645 ) 1646 }) 1647 .transpose()?; 1648 let out_property = (self_property, len_property) 1649 .map(|(id, len)| format!("{id:.len$}", len = len.unwrap_or(12))); 1650 Ok(out_property.into_dyn_wrapped()) 1651 }, 1652 ); 1653 map.insert( 1654 "shortest", 1655 |language, diagnostics, build_ctx, self_property, function| { 1656 let ([], [len_node]) = function.expect_arguments()?; 1657 let len_property = len_node 1658 .map(|node| { 1659 template_builder::expect_usize_expression( 1660 language, 1661 diagnostics, 1662 build_ctx, 1663 node, 1664 ) 1665 }) 1666 .transpose()?; 1667 let repo = language.repo; 1668 let index = match language.id_prefix_context.populate(repo) { 1669 Ok(index) => index, 1670 Err(err) => { 1671 // Not an error because we can still produce somewhat 1672 // reasonable output. 1673 diagnostics.add_warning( 1674 TemplateParseError::expression( 1675 "Failed to load short-prefixes index", 1676 function.name_span, 1677 ) 1678 .with_source(err), 1679 ); 1680 IdPrefixIndex::empty() 1681 } 1682 }; 1683 // The length of the id printed will be the maximum of the minimum 1684 // `len` and the length of the shortest unique prefix. 1685 let out_property = (self_property, len_property).map(move |(id, len)| { 1686 let prefix_len = id.shortest_prefix_len(repo, &index); 1687 let mut hex = format!("{id:.len$}", len = max(prefix_len, len.unwrap_or(0))); 1688 let rest = hex.split_off(prefix_len); 1689 ShortestIdPrefix { prefix: hex, rest } 1690 }); 1691 Ok(out_property.into_dyn_wrapped()) 1692 }, 1693 ); 1694 map 1695} 1696 1697pub struct ShortestIdPrefix { 1698 pub prefix: String, 1699 pub rest: String, 1700} 1701 1702impl Template for ShortestIdPrefix { 1703 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1704 write!(formatter.labeled("prefix"), "{}", self.prefix)?; 1705 write!(formatter.labeled("rest"), "{}", self.rest)?; 1706 Ok(()) 1707 } 1708} 1709 1710impl ShortestIdPrefix { 1711 fn to_upper(&self) -> Self { 1712 Self { 1713 prefix: self.prefix.to_ascii_uppercase(), 1714 rest: self.rest.to_ascii_uppercase(), 1715 } 1716 } 1717 fn to_lower(&self) -> Self { 1718 Self { 1719 prefix: self.prefix.to_ascii_lowercase(), 1720 rest: self.rest.to_ascii_lowercase(), 1721 } 1722 } 1723} 1724 1725fn builtin_shortest_id_prefix_methods<'repo>( 1726) -> CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix> { 1727 // Not using maplit::hashmap!{} or custom declarative macro here because 1728 // code completion inside macro is quite restricted. 1729 let mut map = CommitTemplateBuildMethodFnMap::<ShortestIdPrefix>::new(); 1730 map.insert( 1731 "prefix", 1732 |_language, _diagnostics, _build_ctx, self_property, function| { 1733 function.expect_no_arguments()?; 1734 let out_property = self_property.map(|id| id.prefix); 1735 Ok(out_property.into_dyn_wrapped()) 1736 }, 1737 ); 1738 map.insert( 1739 "rest", 1740 |_language, _diagnostics, _build_ctx, self_property, function| { 1741 function.expect_no_arguments()?; 1742 let out_property = self_property.map(|id| id.rest); 1743 Ok(out_property.into_dyn_wrapped()) 1744 }, 1745 ); 1746 map.insert( 1747 "upper", 1748 |_language, _diagnostics, _build_ctx, self_property, function| { 1749 function.expect_no_arguments()?; 1750 let out_property = self_property.map(|id| id.to_upper()); 1751 Ok(out_property.into_dyn_wrapped()) 1752 }, 1753 ); 1754 map.insert( 1755 "lower", 1756 |_language, _diagnostics, _build_ctx, self_property, function| { 1757 function.expect_no_arguments()?; 1758 let out_property = self_property.map(|id| id.to_lower()); 1759 Ok(out_property.into_dyn_wrapped()) 1760 }, 1761 ); 1762 map 1763} 1764 1765/// Pair of trees to be diffed. 1766#[derive(Debug)] 1767pub struct TreeDiff { 1768 from_tree: MergedTree, 1769 to_tree: MergedTree, 1770 matcher: Rc<dyn Matcher>, 1771 copy_records: CopyRecords, 1772} 1773 1774impl TreeDiff { 1775 fn from_commit( 1776 repo: &dyn Repo, 1777 commit: &Commit, 1778 matcher: Rc<dyn Matcher>, 1779 ) -> BackendResult<Self> { 1780 let mut copy_records = CopyRecords::default(); 1781 for parent in commit.parent_ids() { 1782 let records = 1783 diff_util::get_copy_records(repo.store(), parent, commit.id(), &*matcher)?; 1784 copy_records.add_records(records)?; 1785 } 1786 Ok(TreeDiff { 1787 from_tree: commit.parent_tree(repo)?, 1788 to_tree: commit.tree()?, 1789 matcher, 1790 copy_records, 1791 }) 1792 } 1793 1794 fn diff_stream(&self) -> BoxStream<'_, CopiesTreeDiffEntry> { 1795 self.from_tree 1796 .diff_stream_with_copies(&self.to_tree, &*self.matcher, &self.copy_records) 1797 } 1798 1799 async fn collect_entries(&self) -> BackendResult<Vec<TreeDiffEntry>> { 1800 self.diff_stream() 1801 .map(TreeDiffEntry::from_backend_entry_with_copies) 1802 .try_collect() 1803 .await 1804 } 1805 1806 fn into_formatted<F, E>(self, show: F) -> TreeDiffFormatted<F> 1807 where 1808 F: Fn(&mut dyn Formatter, &Store, BoxStream<CopiesTreeDiffEntry>) -> Result<(), E>, 1809 E: Into<TemplatePropertyError>, 1810 { 1811 TreeDiffFormatted { diff: self, show } 1812 } 1813} 1814 1815/// Tree diff to be rendered by predefined function `F`. 1816struct TreeDiffFormatted<F> { 1817 diff: TreeDiff, 1818 show: F, 1819} 1820 1821impl<F, E> Template for TreeDiffFormatted<F> 1822where 1823 F: Fn(&mut dyn Formatter, &Store, BoxStream<CopiesTreeDiffEntry>) -> Result<(), E>, 1824 E: Into<TemplatePropertyError>, 1825{ 1826 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1827 let show = &self.show; 1828 let store = self.diff.from_tree.store(); 1829 let tree_diff = self.diff.diff_stream(); 1830 show(formatter.as_mut(), store, tree_diff).or_else(|err| formatter.handle_error(err.into())) 1831 } 1832} 1833 1834fn builtin_tree_diff_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiff> { 1835 type P<'repo> = CommitTemplatePropertyKind<'repo>; 1836 // Not using maplit::hashmap!{} or custom declarative macro here because 1837 // code completion inside macro is quite restricted. 1838 let mut map = CommitTemplateBuildMethodFnMap::<TreeDiff>::new(); 1839 map.insert( 1840 "files", 1841 |_language, _diagnostics, _build_ctx, self_property, function| { 1842 function.expect_no_arguments()?; 1843 // TODO: cache and reuse diff entries within the current evaluation? 1844 let out_property = 1845 self_property.and_then(|diff| Ok(diff.collect_entries().block_on()?)); 1846 Ok(out_property.into_dyn_wrapped()) 1847 }, 1848 ); 1849 map.insert( 1850 "color_words", 1851 |language, diagnostics, build_ctx, self_property, function| { 1852 let ([], [context_node]) = function.expect_arguments()?; 1853 let context_property = context_node 1854 .map(|node| { 1855 template_builder::expect_usize_expression( 1856 language, 1857 diagnostics, 1858 build_ctx, 1859 node, 1860 ) 1861 }) 1862 .transpose()?; 1863 let path_converter = language.path_converter; 1864 let options = diff_util::ColorWordsDiffOptions::from_settings(language.settings()) 1865 .map_err(|err| { 1866 let message = "Failed to load diff settings"; 1867 TemplateParseError::expression(message, function.name_span).with_source(err) 1868 })?; 1869 let conflict_marker_style = language.conflict_marker_style; 1870 let template = (self_property, context_property) 1871 .map(move |(diff, context)| { 1872 let mut options = options.clone(); 1873 if let Some(context) = context { 1874 options.context = context; 1875 } 1876 diff.into_formatted(move |formatter, store, tree_diff| { 1877 diff_util::show_color_words_diff( 1878 formatter, 1879 store, 1880 tree_diff, 1881 path_converter, 1882 &options, 1883 conflict_marker_style, 1884 ) 1885 }) 1886 }) 1887 .into_template(); 1888 Ok(P::wrap_template(template)) 1889 }, 1890 ); 1891 map.insert( 1892 "git", 1893 |language, diagnostics, build_ctx, self_property, function| { 1894 let ([], [context_node]) = function.expect_arguments()?; 1895 let context_property = context_node 1896 .map(|node| { 1897 template_builder::expect_usize_expression( 1898 language, 1899 diagnostics, 1900 build_ctx, 1901 node, 1902 ) 1903 }) 1904 .transpose()?; 1905 let options = diff_util::UnifiedDiffOptions::from_settings(language.settings()) 1906 .map_err(|err| { 1907 let message = "Failed to load diff settings"; 1908 TemplateParseError::expression(message, function.name_span).with_source(err) 1909 })?; 1910 let conflict_marker_style = language.conflict_marker_style; 1911 let template = (self_property, context_property) 1912 .map(move |(diff, context)| { 1913 let mut options = options.clone(); 1914 if let Some(context) = context { 1915 options.context = context; 1916 } 1917 diff.into_formatted(move |formatter, store, tree_diff| { 1918 diff_util::show_git_diff( 1919 formatter, 1920 store, 1921 tree_diff, 1922 &options, 1923 conflict_marker_style, 1924 ) 1925 }) 1926 }) 1927 .into_template(); 1928 Ok(P::wrap_template(template)) 1929 }, 1930 ); 1931 map.insert( 1932 "stat", 1933 |language, diagnostics, build_ctx, self_property, function| { 1934 let ([], [width_node]) = function.expect_arguments()?; 1935 let width_property = width_node 1936 .map(|node| { 1937 template_builder::expect_usize_expression( 1938 language, 1939 diagnostics, 1940 build_ctx, 1941 node, 1942 ) 1943 }) 1944 .transpose()?; 1945 let path_converter = language.path_converter; 1946 // No user configuration exists for diff stat. 1947 let options = diff_util::DiffStatOptions::default(); 1948 let conflict_marker_style = language.conflict_marker_style; 1949 // TODO: cache and reuse stats within the current evaluation? 1950 let out_property = (self_property, width_property).and_then(move |(diff, width)| { 1951 let store = diff.from_tree.store(); 1952 let tree_diff = diff.diff_stream(); 1953 let stats = DiffStats::calculate(store, tree_diff, &options, conflict_marker_style) 1954 .block_on()?; 1955 Ok(DiffStatsFormatted { 1956 stats, 1957 path_converter, 1958 // TODO: fall back to current available width 1959 width: width.unwrap_or(80), 1960 }) 1961 }); 1962 Ok(out_property.into_dyn_wrapped()) 1963 }, 1964 ); 1965 map.insert( 1966 "summary", 1967 |language, _diagnostics, _build_ctx, self_property, function| { 1968 function.expect_no_arguments()?; 1969 let path_converter = language.path_converter; 1970 let template = self_property 1971 .map(move |diff| { 1972 diff.into_formatted(move |formatter, _store, tree_diff| { 1973 diff_util::show_diff_summary(formatter, tree_diff, path_converter) 1974 }) 1975 }) 1976 .into_template(); 1977 Ok(P::wrap_template(template)) 1978 }, 1979 ); 1980 // TODO: add support for external tools 1981 map 1982} 1983 1984/// [`MergedTree`] diff entry. 1985#[derive(Clone, Debug)] 1986pub struct TreeDiffEntry { 1987 pub path: CopiesTreeDiffEntryPath, 1988 pub source_value: MergedTreeValue, 1989 pub target_value: MergedTreeValue, 1990} 1991 1992impl TreeDiffEntry { 1993 fn from_backend_entry_with_copies(entry: CopiesTreeDiffEntry) -> BackendResult<Self> { 1994 let (source_value, target_value) = entry.values?; 1995 Ok(TreeDiffEntry { 1996 path: entry.path, 1997 source_value, 1998 target_value, 1999 }) 2000 } 2001 2002 fn status_label(&self) -> &'static str { 2003 let (label, _sigil) = diff_util::diff_status_label_and_char( 2004 &self.path, 2005 &self.source_value, 2006 &self.target_value, 2007 ); 2008 label 2009 } 2010 2011 fn into_source_entry(self) -> TreeEntry { 2012 TreeEntry { 2013 path: self.path.source.map_or(self.path.target, |(path, _)| path), 2014 value: self.source_value, 2015 } 2016 } 2017 2018 fn into_target_entry(self) -> TreeEntry { 2019 TreeEntry { 2020 path: self.path.target, 2021 value: self.target_value, 2022 } 2023 } 2024} 2025 2026fn builtin_tree_diff_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry> 2027{ 2028 // Not using maplit::hashmap!{} or custom declarative macro here because 2029 // code completion inside macro is quite restricted. 2030 let mut map = CommitTemplateBuildMethodFnMap::<TreeDiffEntry>::new(); 2031 map.insert( 2032 "path", 2033 |_language, _diagnostics, _build_ctx, self_property, function| { 2034 function.expect_no_arguments()?; 2035 let out_property = self_property.map(|entry| entry.path.target); 2036 Ok(out_property.into_dyn_wrapped()) 2037 }, 2038 ); 2039 map.insert( 2040 "status", 2041 |_language, _diagnostics, _build_ctx, self_property, function| { 2042 function.expect_no_arguments()?; 2043 let out_property = self_property.map(|entry| entry.status_label().to_owned()); 2044 Ok(out_property.into_dyn_wrapped()) 2045 }, 2046 ); 2047 // TODO: add status_code() or status_char()? 2048 map.insert( 2049 "source", 2050 |_language, _diagnostics, _build_ctx, self_property, function| { 2051 function.expect_no_arguments()?; 2052 let out_property = self_property.map(TreeDiffEntry::into_source_entry); 2053 Ok(out_property.into_dyn_wrapped()) 2054 }, 2055 ); 2056 map.insert( 2057 "target", 2058 |_language, _diagnostics, _build_ctx, self_property, function| { 2059 function.expect_no_arguments()?; 2060 let out_property = self_property.map(TreeDiffEntry::into_target_entry); 2061 Ok(out_property.into_dyn_wrapped()) 2062 }, 2063 ); 2064 map 2065} 2066 2067/// [`MergedTree`] entry. 2068#[derive(Clone, Debug)] 2069pub struct TreeEntry { 2070 pub path: RepoPathBuf, 2071 pub value: MergedTreeValue, 2072} 2073 2074fn builtin_tree_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeEntry> { 2075 // Not using maplit::hashmap!{} or custom declarative macro here because 2076 // code completion inside macro is quite restricted. 2077 let mut map = CommitTemplateBuildMethodFnMap::<TreeEntry>::new(); 2078 map.insert( 2079 "path", 2080 |_language, _diagnostics, _build_ctx, self_property, function| { 2081 function.expect_no_arguments()?; 2082 let out_property = self_property.map(|entry| entry.path); 2083 Ok(out_property.into_dyn_wrapped()) 2084 }, 2085 ); 2086 map.insert( 2087 "conflict", 2088 |_language, _diagnostics, _build_ctx, self_property, function| { 2089 function.expect_no_arguments()?; 2090 let out_property = self_property.map(|entry| !entry.value.is_resolved()); 2091 Ok(out_property.into_dyn_wrapped()) 2092 }, 2093 ); 2094 map.insert( 2095 "file_type", 2096 |_language, _diagnostics, _build_ctx, self_property, function| { 2097 function.expect_no_arguments()?; 2098 let out_property = 2099 self_property.map(|entry| describe_file_type(&entry.value).to_owned()); 2100 Ok(out_property.into_dyn_wrapped()) 2101 }, 2102 ); 2103 map.insert( 2104 "executable", 2105 |_language, _diagnostics, _build_ctx, self_property, function| { 2106 function.expect_no_arguments()?; 2107 let out_property = 2108 self_property.map(|entry| is_executable_file(&entry.value).unwrap_or_default()); 2109 Ok(out_property.into_dyn_wrapped()) 2110 }, 2111 ); 2112 map 2113} 2114 2115fn describe_file_type(value: &MergedTreeValue) -> &'static str { 2116 match value.as_resolved() { 2117 Some(Some(TreeValue::File { .. })) => "file", 2118 Some(Some(TreeValue::Symlink(_))) => "symlink", 2119 Some(Some(TreeValue::Tree(_))) => "tree", 2120 Some(Some(TreeValue::GitSubmodule(_))) => "git-submodule", 2121 Some(None) => "", // absent 2122 None | Some(Some(TreeValue::Conflict(_))) => "conflict", 2123 } 2124} 2125 2126fn is_executable_file(value: &MergedTreeValue) -> Option<bool> { 2127 let executable = value.to_executable_merge()?; 2128 conflicts::resolve_file_executable(&executable) 2129} 2130 2131/// [`DiffStats`] with rendering parameters. 2132#[derive(Clone, Debug)] 2133pub struct DiffStatsFormatted<'a> { 2134 stats: DiffStats, 2135 path_converter: &'a RepoPathUiConverter, 2136 width: usize, 2137} 2138 2139impl Template for DiffStatsFormatted<'_> { 2140 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 2141 diff_util::show_diff_stats( 2142 formatter.as_mut(), 2143 &self.stats, 2144 self.path_converter, 2145 self.width, 2146 ) 2147 } 2148} 2149 2150fn builtin_diff_stats_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, DiffStats> { 2151 // Not using maplit::hashmap!{} or custom declarative macro here because 2152 // code completion inside macro is quite restricted. 2153 let mut map = CommitTemplateBuildMethodFnMap::<DiffStats>::new(); 2154 // TODO: add files() -> List<DiffStatEntry> ? 2155 map.insert( 2156 "total_added", 2157 |_language, _diagnostics, _build_ctx, self_property, function| { 2158 function.expect_no_arguments()?; 2159 let out_property = 2160 self_property.and_then(|stats| Ok(i64::try_from(stats.count_total_added())?)); 2161 Ok(out_property.into_dyn_wrapped()) 2162 }, 2163 ); 2164 map.insert( 2165 "total_removed", 2166 |_language, _diagnostics, _build_ctx, self_property, function| { 2167 function.expect_no_arguments()?; 2168 let out_property = 2169 self_property.and_then(|stats| Ok(i64::try_from(stats.count_total_removed())?)); 2170 Ok(out_property.into_dyn_wrapped()) 2171 }, 2172 ); 2173 map 2174} 2175 2176#[derive(Debug)] 2177pub struct CryptographicSignature { 2178 commit: Commit, 2179} 2180 2181impl CryptographicSignature { 2182 fn new(commit: Commit) -> Option<Self> { 2183 commit.is_signed().then_some(Self { commit }) 2184 } 2185 2186 fn verify(&self) -> SignResult<Verification> { 2187 self.commit 2188 .verification() 2189 .transpose() 2190 .expect("must have signature") 2191 } 2192 2193 fn status(&self) -> SignResult<SigStatus> { 2194 self.verify().map(|verification| verification.status) 2195 } 2196 2197 /// Defaults to empty string if key is not present. 2198 fn key(&self) -> SignResult<String> { 2199 self.verify() 2200 .map(|verification| verification.key.unwrap_or_default()) 2201 } 2202 2203 /// Defaults to empty string if display is not present. 2204 fn display(&self) -> SignResult<String> { 2205 self.verify() 2206 .map(|verification| verification.display.unwrap_or_default()) 2207 } 2208} 2209 2210fn builtin_cryptographic_signature_methods<'repo>( 2211) -> CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature> { 2212 // Not using maplit::hashmap!{} or custom declarative macro here because 2213 // code completion inside macro is quite restricted. 2214 let mut map = CommitTemplateBuildMethodFnMap::<CryptographicSignature>::new(); 2215 map.insert( 2216 "status", 2217 |_language, _diagnostics, _build_ctx, self_property, function| { 2218 function.expect_no_arguments()?; 2219 let out_property = self_property.and_then(|sig| match sig.status() { 2220 Ok(status) => Ok(status.to_string()), 2221 Err(SignError::InvalidSignatureFormat) => Ok("invalid".to_string()), 2222 Err(err) => Err(err.into()), 2223 }); 2224 Ok(out_property.into_dyn_wrapped()) 2225 }, 2226 ); 2227 map.insert( 2228 "key", 2229 |_language, _diagnostics, _build_ctx, self_property, function| { 2230 function.expect_no_arguments()?; 2231 let out_property = self_property.and_then(|sig| Ok(sig.key()?)); 2232 Ok(out_property.into_dyn_wrapped()) 2233 }, 2234 ); 2235 map.insert( 2236 "display", 2237 |_language, _diagnostics, _build_ctx, self_property, function| { 2238 function.expect_no_arguments()?; 2239 let out_property = self_property.and_then(|sig| Ok(sig.display()?)); 2240 Ok(out_property.into_dyn_wrapped()) 2241 }, 2242 ); 2243 map 2244} 2245 2246#[derive(Debug, Clone)] 2247pub struct AnnotationLine { 2248 pub commit: Commit, 2249 pub content: BString, 2250 pub line_number: usize, 2251 pub first_line_in_hunk: bool, 2252} 2253 2254fn builtin_annotation_line_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, AnnotationLine> 2255{ 2256 type P<'repo> = CommitTemplatePropertyKind<'repo>; 2257 let mut map = CommitTemplateBuildMethodFnMap::<AnnotationLine>::new(); 2258 map.insert( 2259 "commit", 2260 |_language, _diagnostics, _build_ctx, self_property, function| { 2261 function.expect_no_arguments()?; 2262 let out_property = self_property.map(|line| line.commit); 2263 Ok(out_property.into_dyn_wrapped()) 2264 }, 2265 ); 2266 map.insert( 2267 "content", 2268 |_language, _diagnostics, _build_ctx, self_property, function| { 2269 function.expect_no_arguments()?; 2270 let out_property = self_property.map(|line| line.content); 2271 // TODO: Add Bytes or BString template type? 2272 Ok(P::wrap_template(out_property.into_template())) 2273 }, 2274 ); 2275 map.insert( 2276 "line_number", 2277 |_language, _diagnostics, _build_ctx, self_property, function| { 2278 function.expect_no_arguments()?; 2279 let out_property = self_property.and_then(|line| Ok(i64::try_from(line.line_number)?)); 2280 Ok(out_property.into_dyn_wrapped()) 2281 }, 2282 ); 2283 map.insert( 2284 "first_line_in_hunk", 2285 |_language, _diagnostics, _build_ctx, self_property, function| { 2286 function.expect_no_arguments()?; 2287 let out_property = self_property.map(|line| line.first_line_in_hunk); 2288 Ok(out_property.into_dyn_wrapped()) 2289 }, 2290 ); 2291 map 2292} 2293 2294impl Template for Trailer { 2295 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 2296 write!(formatter, "{}: {}", self.key, self.value) 2297 } 2298} 2299 2300impl Template for Vec<Trailer> { 2301 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 2302 templater::format_joined(formatter, self, "\n") 2303 } 2304} 2305 2306fn builtin_trailer_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Trailer> { 2307 let mut map = CommitTemplateBuildMethodFnMap::<Trailer>::new(); 2308 map.insert( 2309 "key", 2310 |_language, _diagnostics, _build_ctx, self_property, function| { 2311 function.expect_no_arguments()?; 2312 let out_property = self_property.map(|trailer| trailer.key); 2313 Ok(out_property.into_dyn_wrapped()) 2314 }, 2315 ); 2316 map.insert( 2317 "value", 2318 |_language, _diagnostics, _build_ctx, self_property, function| { 2319 function.expect_no_arguments()?; 2320 let out_property = self_property.map(|trailer| trailer.value); 2321 Ok(out_property.into_dyn_wrapped()) 2322 }, 2323 ); 2324 map 2325} 2326 2327fn builtin_trailer_list_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Vec<Trailer>> { 2328 let mut map: CommitTemplateBuildMethodFnMap<Vec<Trailer>> = 2329 template_builder::builtin_formattable_list_methods(); 2330 map.insert( 2331 "contains_key", 2332 |language, diagnostics, build_ctx, self_property, function| { 2333 let [key_node] = function.expect_exact_arguments()?; 2334 let key_property = 2335 expect_plain_text_expression(language, diagnostics, build_ctx, key_node)?; 2336 let out_property = (self_property, key_property) 2337 .map(|(trailers, key)| trailers.iter().any(|t| t.key == key)); 2338 Ok(out_property.into_dyn_wrapped()) 2339 }, 2340 ); 2341 map 2342} 2343 2344#[cfg(test)] 2345mod tests { 2346 use std::path::Path; 2347 use std::sync::Arc; 2348 2349 use jj_lib::config::ConfigLayer; 2350 use jj_lib::config::ConfigSource; 2351 use jj_lib::revset::RevsetAliasesMap; 2352 use jj_lib::revset::RevsetExpression; 2353 use jj_lib::revset::RevsetExtensions; 2354 use jj_lib::revset::RevsetWorkspaceContext; 2355 use testutils::repo_path_buf; 2356 use testutils::TestRepoBackend; 2357 use testutils::TestWorkspace; 2358 2359 use super::*; 2360 use crate::formatter::PlainTextFormatter; 2361 use crate::template_parser::TemplateAliasesMap; 2362 use crate::templater::TemplateRenderer; 2363 use crate::templater::WrapTemplateProperty; 2364 2365 // TemplateBuildFunctionFn defined for<'a> 2366 type BuildFunctionFn = for<'a> fn( 2367 &CommitTemplateLanguage<'a>, 2368 &mut TemplateDiagnostics, 2369 &BuildContext<CommitTemplatePropertyKind<'a>>, 2370 &FunctionCallNode, 2371 ) -> TemplateParseResult<CommitTemplatePropertyKind<'a>>; 2372 2373 struct CommitTemplateTestEnv { 2374 test_workspace: TestWorkspace, 2375 path_converter: RepoPathUiConverter, 2376 revset_extensions: Arc<RevsetExtensions>, 2377 id_prefix_context: IdPrefixContext, 2378 revset_aliases_map: RevsetAliasesMap, 2379 template_aliases_map: TemplateAliasesMap, 2380 immutable_expression: Rc<UserRevsetExpression>, 2381 extra_functions: HashMap<&'static str, BuildFunctionFn>, 2382 } 2383 2384 impl CommitTemplateTestEnv { 2385 fn init() -> Self { 2386 // Stabilize commit id of the initialized working copy 2387 let settings = stable_settings(); 2388 let test_workspace = 2389 TestWorkspace::init_with_backend_and_settings(TestRepoBackend::Git, &settings); 2390 let path_converter = RepoPathUiConverter::Fs { 2391 cwd: test_workspace.workspace.workspace_root().to_owned(), 2392 base: test_workspace.workspace.workspace_root().to_owned(), 2393 }; 2394 // IdPrefixContext::new() expects Arc<RevsetExtensions> 2395 #[expect(clippy::arc_with_non_send_sync)] 2396 let revset_extensions = Arc::new(RevsetExtensions::new()); 2397 let id_prefix_context = IdPrefixContext::new(revset_extensions.clone()); 2398 CommitTemplateTestEnv { 2399 test_workspace, 2400 path_converter, 2401 revset_extensions, 2402 id_prefix_context, 2403 revset_aliases_map: RevsetAliasesMap::new(), 2404 template_aliases_map: TemplateAliasesMap::new(), 2405 immutable_expression: RevsetExpression::none(), 2406 extra_functions: HashMap::new(), 2407 } 2408 } 2409 2410 fn set_cwd(&mut self, path: impl AsRef<Path>) { 2411 self.path_converter = RepoPathUiConverter::Fs { 2412 cwd: self.test_workspace.workspace.workspace_root().join(path), 2413 base: self.test_workspace.workspace.workspace_root().to_owned(), 2414 }; 2415 } 2416 2417 fn add_function(&mut self, name: &'static str, f: BuildFunctionFn) { 2418 self.extra_functions.insert(name, f); 2419 } 2420 2421 fn new_language(&self) -> CommitTemplateLanguage<'_> { 2422 let revset_parse_context = RevsetParseContext { 2423 aliases_map: &self.revset_aliases_map, 2424 local_variables: HashMap::new(), 2425 user_email: "test.user@example.com", 2426 date_pattern_context: chrono::DateTime::UNIX_EPOCH.fixed_offset().into(), 2427 extensions: &self.revset_extensions, 2428 workspace: Some(RevsetWorkspaceContext { 2429 path_converter: &self.path_converter, 2430 workspace_name: self.test_workspace.workspace.workspace_name(), 2431 }), 2432 }; 2433 let mut language = CommitTemplateLanguage::new( 2434 self.test_workspace.repo.as_ref(), 2435 &self.path_converter, 2436 self.test_workspace.workspace.workspace_name(), 2437 revset_parse_context, 2438 &self.id_prefix_context, 2439 self.immutable_expression.clone(), 2440 ConflictMarkerStyle::default(), 2441 &[] as &[Box<dyn CommitTemplateLanguageExtension>], 2442 ); 2443 // Not using .extend() to infer lifetime of f 2444 for (&name, &f) in &self.extra_functions { 2445 language.build_fn_table.core.functions.insert(name, f); 2446 } 2447 language 2448 } 2449 2450 fn parse<'a, C>(&'a self, text: &str) -> TemplateParseResult<TemplateRenderer<'a, C>> 2451 where 2452 C: Clone + 'a, 2453 CommitTemplatePropertyKind<'a>: WrapTemplateProperty<'a, C>, 2454 { 2455 let language = self.new_language(); 2456 let mut diagnostics = TemplateDiagnostics::new(); 2457 template_builder::parse( 2458 &language, 2459 &mut diagnostics, 2460 text, 2461 &self.template_aliases_map, 2462 ) 2463 } 2464 2465 fn render_ok<'a, C>(&'a self, text: &str, context: &C) -> String 2466 where 2467 C: Clone + 'a, 2468 CommitTemplatePropertyKind<'a>: WrapTemplateProperty<'a, C>, 2469 { 2470 let template = self.parse(text).unwrap(); 2471 let mut output = Vec::new(); 2472 let mut formatter = PlainTextFormatter::new(&mut output); 2473 template.format(context, &mut formatter).unwrap(); 2474 String::from_utf8(output).unwrap() 2475 } 2476 } 2477 2478 fn stable_settings() -> UserSettings { 2479 let mut config = testutils::base_user_config(); 2480 let mut layer = ConfigLayer::empty(ConfigSource::User); 2481 layer 2482 .set_value("debug.commit-timestamp", "2001-02-03T04:05:06+07:00") 2483 .unwrap(); 2484 config.add_layer(layer); 2485 UserSettings::from_config(config).unwrap() 2486 } 2487 2488 #[test] 2489 fn test_ref_symbol_type() { 2490 let mut env = CommitTemplateTestEnv::init(); 2491 env.add_function("sym", |language, diagnostics, build_ctx, function| { 2492 let [value_node] = function.expect_exact_arguments()?; 2493 let value = expect_plain_text_expression(language, diagnostics, build_ctx, value_node)?; 2494 let out_property = value.map(RefSymbolBuf); 2495 Ok(out_property.into_dyn_wrapped()) 2496 }); 2497 let sym = |s: &str| RefSymbolBuf(s.to_owned()); 2498 2499 // default formatting 2500 insta::assert_snapshot!(env.render_ok("self", &sym("")), @r#""""#); 2501 insta::assert_snapshot!(env.render_ok("self", &sym("foo")), @"foo"); 2502 insta::assert_snapshot!(env.render_ok("self", &sym("foo bar")), @r#""foo bar""#); 2503 2504 // comparison 2505 insta::assert_snapshot!(env.render_ok("self == 'foo'", &sym("foo")), @"true"); 2506 insta::assert_snapshot!(env.render_ok("'bar' == self", &sym("foo")), @"false"); 2507 insta::assert_snapshot!(env.render_ok("self == self", &sym("foo")), @"true"); 2508 insta::assert_snapshot!(env.render_ok("self == sym('bar')", &sym("foo")), @"false"); 2509 2510 insta::assert_snapshot!(env.render_ok("self == 'bar'", &Some(sym("foo"))), @"false"); 2511 insta::assert_snapshot!(env.render_ok("self == sym('foo')", &Some(sym("foo"))), @"true"); 2512 insta::assert_snapshot!(env.render_ok("'foo' == self", &Some(sym("foo"))), @"true"); 2513 insta::assert_snapshot!(env.render_ok("sym('bar') == self", &Some(sym("foo"))), @"false"); 2514 insta::assert_snapshot!(env.render_ok("self == self", &Some(sym("foo"))), @"true"); 2515 insta::assert_snapshot!(env.render_ok("self == ''", &None::<RefSymbolBuf>), @"false"); 2516 insta::assert_snapshot!(env.render_ok("sym('') == self", &None::<RefSymbolBuf>), @"false"); 2517 insta::assert_snapshot!(env.render_ok("self == self", &None::<RefSymbolBuf>), @"true"); 2518 2519 // string cast != formatting: it would be weird if function argument of 2520 // string type were quoted/escaped. (e.g. `"foo".contains(bookmark)`) 2521 insta::assert_snapshot!(env.render_ok("stringify(self)", &sym("a b")), @"a b"); 2522 insta::assert_snapshot!(env.render_ok("stringify(self)", &Some(sym("a b"))), @"a b"); 2523 insta::assert_snapshot!( 2524 env.render_ok("stringify(self)", &None::<RefSymbolBuf>), 2525 @"<Error: No RefSymbol available>"); 2526 2527 // string methods 2528 insta::assert_snapshot!(env.render_ok("self.len()", &sym("a b")), @"3"); 2529 } 2530 2531 #[test] 2532 fn test_repo_path_type() { 2533 let mut env = CommitTemplateTestEnv::init(); 2534 env.set_cwd("dir"); 2535 2536 // slash-separated by default 2537 insta::assert_snapshot!( 2538 env.render_ok("self", &repo_path_buf("dir/file")), @"dir/file"); 2539 2540 // .display() to convert to filesystem path 2541 insta::assert_snapshot!( 2542 env.render_ok("self.display()", &repo_path_buf("dir/file")), @"file"); 2543 if cfg!(windows) { 2544 insta::assert_snapshot!( 2545 env.render_ok("self.display()", &repo_path_buf("file")), @"..\\file"); 2546 } else { 2547 insta::assert_snapshot!( 2548 env.render_ok("self.display()", &repo_path_buf("file")), @"../file"); 2549 } 2550 2551 let template = "if(self.parent(), self.parent(), '<none>')"; 2552 insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("")), @"<none>"); 2553 insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("file")), @""); 2554 insta::assert_snapshot!(env.render_ok(template, &repo_path_buf("dir/file")), @"dir"); 2555 } 2556 2557 #[test] 2558 fn test_commit_id_type() { 2559 let env = CommitTemplateTestEnv::init(); 2560 2561 let id = CommitId::from_hex("08a70ab33d7143b7130ed8594d8216ef688623c0"); 2562 insta::assert_snapshot!( 2563 env.render_ok("self", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0"); 2564 insta::assert_snapshot!( 2565 env.render_ok("self.normal_hex()", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0"); 2566 2567 insta::assert_snapshot!(env.render_ok("self.short()", &id), @"08a70ab33d71"); 2568 insta::assert_snapshot!(env.render_ok("self.short(0)", &id), @""); 2569 insta::assert_snapshot!(env.render_ok("self.short(-0)", &id), @""); 2570 insta::assert_snapshot!( 2571 env.render_ok("self.short(100)", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0"); 2572 insta::assert_snapshot!( 2573 env.render_ok("self.short(-100)", &id), 2574 @"<Error: out of range integral type conversion attempted>"); 2575 2576 insta::assert_snapshot!(env.render_ok("self.shortest()", &id), @"08"); 2577 insta::assert_snapshot!(env.render_ok("self.shortest(0)", &id), @"08"); 2578 insta::assert_snapshot!(env.render_ok("self.shortest(-0)", &id), @"08"); 2579 insta::assert_snapshot!( 2580 env.render_ok("self.shortest(100)", &id), @"08a70ab33d7143b7130ed8594d8216ef688623c0"); 2581 insta::assert_snapshot!( 2582 env.render_ok("self.shortest(-100)", &id), 2583 @"<Error: out of range integral type conversion attempted>"); 2584 } 2585 2586 #[test] 2587 fn test_change_id_type() { 2588 let env = CommitTemplateTestEnv::init(); 2589 2590 let id = ChangeId::from_hex("ffdaa62087a280bddc5e3d3ff933b8ae"); 2591 insta::assert_snapshot!( 2592 env.render_ok("self", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl"); 2593 insta::assert_snapshot!( 2594 env.render_ok("self.normal_hex()", &id), @"ffdaa62087a280bddc5e3d3ff933b8ae"); 2595 2596 insta::assert_snapshot!(env.render_ok("self.short()", &id), @"kkmpptxzrspx"); 2597 insta::assert_snapshot!(env.render_ok("self.short(0)", &id), @""); 2598 insta::assert_snapshot!(env.render_ok("self.short(-0)", &id), @""); 2599 insta::assert_snapshot!( 2600 env.render_ok("self.short(100)", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl"); 2601 insta::assert_snapshot!( 2602 env.render_ok("self.short(-100)", &id), 2603 @"<Error: out of range integral type conversion attempted>"); 2604 2605 insta::assert_snapshot!(env.render_ok("self.shortest()", &id), @"k"); 2606 insta::assert_snapshot!(env.render_ok("self.shortest(0)", &id), @"k"); 2607 insta::assert_snapshot!(env.render_ok("self.shortest(-0)", &id), @"k"); 2608 insta::assert_snapshot!( 2609 env.render_ok("self.shortest(100)", &id), @"kkmpptxzrspxrzommnulwmwkkqwworpl"); 2610 insta::assert_snapshot!( 2611 env.render_ok("self.shortest(-100)", &id), 2612 @"<Error: out of range integral type conversion attempted>"); 2613 } 2614}