just playing with tangled
at gvimdiff 89 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::io; 20use std::rc::Rc; 21 22use bstr::BString; 23use futures::stream::BoxStream; 24use futures::StreamExt as _; 25use futures::TryStreamExt as _; 26use itertools::Itertools as _; 27use jj_lib::backend::BackendResult; 28use jj_lib::backend::ChangeId; 29use jj_lib::backend::CommitId; 30use jj_lib::backend::TreeValue; 31use jj_lib::commit::Commit; 32use jj_lib::conflicts::ConflictMarkerStyle; 33use jj_lib::copies::CopiesTreeDiffEntry; 34use jj_lib::copies::CopiesTreeDiffEntryPath; 35use jj_lib::copies::CopyRecords; 36use jj_lib::extensions_map::ExtensionsMap; 37use jj_lib::fileset; 38use jj_lib::fileset::FilesetDiagnostics; 39use jj_lib::fileset::FilesetExpression; 40use jj_lib::id_prefix::IdPrefixContext; 41use jj_lib::id_prefix::IdPrefixIndex; 42use jj_lib::matchers::Matcher; 43use jj_lib::merge::MergedTreeValue; 44use jj_lib::merged_tree::MergedTree; 45use jj_lib::object_id::ObjectId as _; 46use jj_lib::op_store::RefTarget; 47use jj_lib::op_store::RemoteRef; 48use jj_lib::ref_name::WorkspaceName; 49use jj_lib::ref_name::WorkspaceNameBuf; 50use jj_lib::repo::Repo; 51use jj_lib::repo_path::RepoPathBuf; 52use jj_lib::repo_path::RepoPathUiConverter; 53use jj_lib::revset; 54use jj_lib::revset::Revset; 55use jj_lib::revset::RevsetContainingFn; 56use jj_lib::revset::RevsetDiagnostics; 57use jj_lib::revset::RevsetModifier; 58use jj_lib::revset::RevsetParseContext; 59use jj_lib::revset::UserRevsetExpression; 60use jj_lib::settings::UserSettings; 61use jj_lib::signing::SigStatus; 62use jj_lib::signing::SignError; 63use jj_lib::signing::SignResult; 64use jj_lib::signing::Verification; 65use jj_lib::store::Store; 66use once_cell::unsync::OnceCell; 67use pollster::FutureExt as _; 68 69use crate::diff_util; 70use crate::diff_util::DiffStats; 71use crate::formatter::Formatter; 72use crate::revset_util; 73use crate::template_builder; 74use crate::template_builder::merge_fn_map; 75use crate::template_builder::BuildContext; 76use crate::template_builder::CoreTemplateBuildFnTable; 77use crate::template_builder::CoreTemplatePropertyKind; 78use crate::template_builder::IntoTemplateProperty; 79use crate::template_builder::TemplateBuildMethodFnMap; 80use crate::template_builder::TemplateLanguage; 81use crate::template_parser; 82use crate::template_parser::ExpressionNode; 83use crate::template_parser::FunctionCallNode; 84use crate::template_parser::TemplateDiagnostics; 85use crate::template_parser::TemplateParseError; 86use crate::template_parser::TemplateParseResult; 87use crate::templater; 88use crate::templater::PlainTextFormattedProperty; 89use crate::templater::SizeHint; 90use crate::templater::Template; 91use crate::templater::TemplateFormatter; 92use crate::templater::TemplateProperty; 93use crate::templater::TemplatePropertyError; 94use crate::templater::TemplatePropertyExt as _; 95use crate::text_util; 96 97pub trait CommitTemplateLanguageExtension { 98 fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo>; 99 100 fn build_cache_extensions(&self, extensions: &mut ExtensionsMap); 101} 102 103pub struct CommitTemplateLanguage<'repo> { 104 repo: &'repo dyn Repo, 105 path_converter: &'repo RepoPathUiConverter, 106 workspace_name: WorkspaceNameBuf, 107 // RevsetParseContext doesn't borrow a repo, but we'll need 'repo lifetime 108 // anyway to capture it to evaluate dynamically-constructed user expression 109 // such as `revset("ancestors(" ++ commit_id ++ ")")`. 110 // TODO: Maybe refactor context structs? RepoPathUiConverter and 111 // WorkspaceName are contained in RevsetParseContext for example. 112 revset_parse_context: RevsetParseContext<'repo>, 113 id_prefix_context: &'repo IdPrefixContext, 114 immutable_expression: Rc<UserRevsetExpression>, 115 conflict_marker_style: ConflictMarkerStyle, 116 build_fn_table: CommitTemplateBuildFnTable<'repo>, 117 keyword_cache: CommitKeywordCache<'repo>, 118 cache_extensions: ExtensionsMap, 119} 120 121impl<'repo> CommitTemplateLanguage<'repo> { 122 /// Sets up environment where commit template will be transformed to 123 /// evaluation tree. 124 #[expect(clippy::too_many_arguments)] 125 pub fn new( 126 repo: &'repo dyn Repo, 127 path_converter: &'repo RepoPathUiConverter, 128 workspace_name: &WorkspaceName, 129 revset_parse_context: RevsetParseContext<'repo>, 130 id_prefix_context: &'repo IdPrefixContext, 131 immutable_expression: Rc<UserRevsetExpression>, 132 conflict_marker_style: ConflictMarkerStyle, 133 extensions: &[impl AsRef<dyn CommitTemplateLanguageExtension>], 134 ) -> Self { 135 let mut build_fn_table = CommitTemplateBuildFnTable::builtin(); 136 let mut cache_extensions = ExtensionsMap::empty(); 137 138 for extension in extensions { 139 build_fn_table.merge(extension.as_ref().build_fn_table()); 140 extension 141 .as_ref() 142 .build_cache_extensions(&mut cache_extensions); 143 } 144 145 CommitTemplateLanguage { 146 repo, 147 path_converter, 148 workspace_name: workspace_name.to_owned(), 149 revset_parse_context, 150 id_prefix_context, 151 immutable_expression, 152 conflict_marker_style, 153 build_fn_table, 154 keyword_cache: CommitKeywordCache::default(), 155 cache_extensions, 156 } 157 } 158} 159 160impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { 161 type Property = CommitTemplatePropertyKind<'repo>; 162 163 template_builder::impl_core_wrap_property_fns!('repo, CommitTemplatePropertyKind::Core); 164 165 fn settings(&self) -> &UserSettings { 166 self.repo.base_repo().settings() 167 } 168 169 fn build_function( 170 &self, 171 diagnostics: &mut TemplateDiagnostics, 172 build_ctx: &BuildContext<Self::Property>, 173 function: &FunctionCallNode, 174 ) -> TemplateParseResult<Self::Property> { 175 let table = &self.build_fn_table.core; 176 table.build_function(self, diagnostics, build_ctx, function) 177 } 178 179 fn build_method( 180 &self, 181 diagnostics: &mut TemplateDiagnostics, 182 build_ctx: &BuildContext<Self::Property>, 183 property: Self::Property, 184 function: &FunctionCallNode, 185 ) -> TemplateParseResult<Self::Property> { 186 let type_name = property.type_name(); 187 match property { 188 CommitTemplatePropertyKind::Core(property) => { 189 let table = &self.build_fn_table.core; 190 table.build_method(self, diagnostics, build_ctx, property, function) 191 } 192 CommitTemplatePropertyKind::Commit(property) => { 193 let table = &self.build_fn_table.commit_methods; 194 let build = template_parser::lookup_method(type_name, table, function)?; 195 build(self, diagnostics, build_ctx, property, function) 196 } 197 CommitTemplatePropertyKind::CommitOpt(property) => { 198 let type_name = "Commit"; 199 let table = &self.build_fn_table.commit_methods; 200 let build = template_parser::lookup_method(type_name, table, function)?; 201 let inner_property = property.try_unwrap(type_name); 202 build( 203 self, 204 diagnostics, 205 build_ctx, 206 Box::new(inner_property), 207 function, 208 ) 209 } 210 CommitTemplatePropertyKind::CommitList(property) => { 211 // TODO: migrate to table? 212 template_builder::build_unformattable_list_method( 213 self, 214 diagnostics, 215 build_ctx, 216 property, 217 function, 218 Self::wrap_commit, 219 Self::wrap_commit_list, 220 ) 221 } 222 CommitTemplatePropertyKind::CommitRef(property) => { 223 let table = &self.build_fn_table.commit_ref_methods; 224 let build = template_parser::lookup_method(type_name, table, function)?; 225 build(self, diagnostics, build_ctx, property, function) 226 } 227 CommitTemplatePropertyKind::CommitRefOpt(property) => { 228 let type_name = "CommitRef"; 229 let table = &self.build_fn_table.commit_ref_methods; 230 let build = template_parser::lookup_method(type_name, table, function)?; 231 let inner_property = property.try_unwrap(type_name); 232 build( 233 self, 234 diagnostics, 235 build_ctx, 236 Box::new(inner_property), 237 function, 238 ) 239 } 240 CommitTemplatePropertyKind::CommitRefList(property) => { 241 // TODO: migrate to table? 242 template_builder::build_formattable_list_method( 243 self, 244 diagnostics, 245 build_ctx, 246 property, 247 function, 248 Self::wrap_commit_ref, 249 Self::wrap_commit_ref_list, 250 ) 251 } 252 CommitTemplatePropertyKind::RepoPath(property) => { 253 let table = &self.build_fn_table.repo_path_methods; 254 let build = template_parser::lookup_method(type_name, table, function)?; 255 build(self, diagnostics, build_ctx, property, function) 256 } 257 CommitTemplatePropertyKind::RepoPathOpt(property) => { 258 let type_name = "RepoPath"; 259 let table = &self.build_fn_table.repo_path_methods; 260 let build = template_parser::lookup_method(type_name, table, function)?; 261 let inner_property = property.try_unwrap(type_name); 262 build( 263 self, 264 diagnostics, 265 build_ctx, 266 Box::new(inner_property), 267 function, 268 ) 269 } 270 CommitTemplatePropertyKind::CommitOrChangeId(property) => { 271 let table = &self.build_fn_table.commit_or_change_id_methods; 272 let build = template_parser::lookup_method(type_name, table, function)?; 273 build(self, diagnostics, build_ctx, property, function) 274 } 275 CommitTemplatePropertyKind::ShortestIdPrefix(property) => { 276 let table = &self.build_fn_table.shortest_id_prefix_methods; 277 let build = template_parser::lookup_method(type_name, table, function)?; 278 build(self, diagnostics, build_ctx, property, function) 279 } 280 CommitTemplatePropertyKind::TreeDiff(property) => { 281 let table = &self.build_fn_table.tree_diff_methods; 282 let build = template_parser::lookup_method(type_name, table, function)?; 283 build(self, diagnostics, build_ctx, property, function) 284 } 285 CommitTemplatePropertyKind::TreeDiffEntry(property) => { 286 let table = &self.build_fn_table.tree_diff_entry_methods; 287 let build = template_parser::lookup_method(type_name, table, function)?; 288 build(self, diagnostics, build_ctx, property, function) 289 } 290 CommitTemplatePropertyKind::TreeDiffEntryList(property) => { 291 // TODO: migrate to table? 292 template_builder::build_unformattable_list_method( 293 self, 294 diagnostics, 295 build_ctx, 296 property, 297 function, 298 Self::wrap_tree_diff_entry, 299 Self::wrap_tree_diff_entry_list, 300 ) 301 } 302 CommitTemplatePropertyKind::TreeEntry(property) => { 303 let table = &self.build_fn_table.tree_entry_methods; 304 let build = template_parser::lookup_method(type_name, table, function)?; 305 build(self, diagnostics, build_ctx, property, function) 306 } 307 CommitTemplatePropertyKind::DiffStats(property) => { 308 let table = &self.build_fn_table.diff_stats_methods; 309 let build = template_parser::lookup_method(type_name, table, function)?; 310 // Strip off formatting parameters which are needed only for the 311 // default template output. 312 let property = Box::new(property.map(|formatted| formatted.stats)); 313 build(self, diagnostics, build_ctx, property, function) 314 } 315 CommitTemplatePropertyKind::CryptographicSignatureOpt(property) => { 316 let type_name = "CryptographicSignature"; 317 let table = &self.build_fn_table.cryptographic_signature_methods; 318 let build = template_parser::lookup_method(type_name, table, function)?; 319 let inner_property = property.try_unwrap(type_name); 320 build( 321 self, 322 diagnostics, 323 build_ctx, 324 Box::new(inner_property), 325 function, 326 ) 327 } 328 CommitTemplatePropertyKind::AnnotationLine(property) => { 329 let type_name = "AnnotationLine"; 330 let table = &self.build_fn_table.annotation_line_methods; 331 let build = template_parser::lookup_method(type_name, table, function)?; 332 build(self, diagnostics, build_ctx, property, function) 333 } 334 } 335 } 336} 337 338// If we need to add multiple languages that support Commit types, this can be 339// turned into a trait which extends TemplateLanguage. 340impl<'repo> CommitTemplateLanguage<'repo> { 341 pub fn repo(&self) -> &'repo dyn Repo { 342 self.repo 343 } 344 345 pub fn workspace_name(&self) -> &WorkspaceName { 346 &self.workspace_name 347 } 348 349 pub fn keyword_cache(&self) -> &CommitKeywordCache<'repo> { 350 &self.keyword_cache 351 } 352 353 pub fn cache_extension<T: Any>(&self) -> Option<&T> { 354 self.cache_extensions.get::<T>() 355 } 356 357 pub fn wrap_commit( 358 property: impl TemplateProperty<Output = Commit> + 'repo, 359 ) -> CommitTemplatePropertyKind<'repo> { 360 CommitTemplatePropertyKind::Commit(Box::new(property)) 361 } 362 363 pub fn wrap_commit_opt( 364 property: impl TemplateProperty<Output = Option<Commit>> + 'repo, 365 ) -> CommitTemplatePropertyKind<'repo> { 366 CommitTemplatePropertyKind::CommitOpt(Box::new(property)) 367 } 368 369 pub fn wrap_commit_list( 370 property: impl TemplateProperty<Output = Vec<Commit>> + 'repo, 371 ) -> CommitTemplatePropertyKind<'repo> { 372 CommitTemplatePropertyKind::CommitList(Box::new(property)) 373 } 374 375 pub fn wrap_commit_ref( 376 property: impl TemplateProperty<Output = Rc<CommitRef>> + 'repo, 377 ) -> CommitTemplatePropertyKind<'repo> { 378 CommitTemplatePropertyKind::CommitRef(Box::new(property)) 379 } 380 381 pub fn wrap_commit_ref_opt( 382 property: impl TemplateProperty<Output = Option<Rc<CommitRef>>> + 'repo, 383 ) -> CommitTemplatePropertyKind<'repo> { 384 CommitTemplatePropertyKind::CommitRefOpt(Box::new(property)) 385 } 386 387 pub fn wrap_commit_ref_list( 388 property: impl TemplateProperty<Output = Vec<Rc<CommitRef>>> + 'repo, 389 ) -> CommitTemplatePropertyKind<'repo> { 390 CommitTemplatePropertyKind::CommitRefList(Box::new(property)) 391 } 392 393 pub fn wrap_repo_path( 394 property: impl TemplateProperty<Output = RepoPathBuf> + 'repo, 395 ) -> CommitTemplatePropertyKind<'repo> { 396 CommitTemplatePropertyKind::RepoPath(Box::new(property)) 397 } 398 399 pub fn wrap_repo_path_opt( 400 property: impl TemplateProperty<Output = Option<RepoPathBuf>> + 'repo, 401 ) -> CommitTemplatePropertyKind<'repo> { 402 CommitTemplatePropertyKind::RepoPathOpt(Box::new(property)) 403 } 404 405 pub fn wrap_commit_or_change_id( 406 property: impl TemplateProperty<Output = CommitOrChangeId> + 'repo, 407 ) -> CommitTemplatePropertyKind<'repo> { 408 CommitTemplatePropertyKind::CommitOrChangeId(Box::new(property)) 409 } 410 411 pub fn wrap_shortest_id_prefix( 412 property: impl TemplateProperty<Output = ShortestIdPrefix> + 'repo, 413 ) -> CommitTemplatePropertyKind<'repo> { 414 CommitTemplatePropertyKind::ShortestIdPrefix(Box::new(property)) 415 } 416 417 pub fn wrap_tree_diff( 418 property: impl TemplateProperty<Output = TreeDiff> + 'repo, 419 ) -> CommitTemplatePropertyKind<'repo> { 420 CommitTemplatePropertyKind::TreeDiff(Box::new(property)) 421 } 422 423 pub fn wrap_tree_diff_entry( 424 property: impl TemplateProperty<Output = TreeDiffEntry> + 'repo, 425 ) -> CommitTemplatePropertyKind<'repo> { 426 CommitTemplatePropertyKind::TreeDiffEntry(Box::new(property)) 427 } 428 429 pub fn wrap_tree_diff_entry_list( 430 property: impl TemplateProperty<Output = Vec<TreeDiffEntry>> + 'repo, 431 ) -> CommitTemplatePropertyKind<'repo> { 432 CommitTemplatePropertyKind::TreeDiffEntryList(Box::new(property)) 433 } 434 435 pub fn wrap_tree_entry( 436 property: impl TemplateProperty<Output = TreeEntry> + 'repo, 437 ) -> CommitTemplatePropertyKind<'repo> { 438 CommitTemplatePropertyKind::TreeEntry(Box::new(property)) 439 } 440 441 pub fn wrap_diff_stats( 442 property: impl TemplateProperty<Output = DiffStatsFormatted<'repo>> + 'repo, 443 ) -> CommitTemplatePropertyKind<'repo> { 444 CommitTemplatePropertyKind::DiffStats(Box::new(property)) 445 } 446 447 fn wrap_cryptographic_signature_opt( 448 property: impl TemplateProperty<Output = Option<CryptographicSignature>> + 'repo, 449 ) -> CommitTemplatePropertyKind<'repo> { 450 CommitTemplatePropertyKind::CryptographicSignatureOpt(Box::new(property)) 451 } 452 453 pub fn wrap_annotation_line( 454 property: impl TemplateProperty<Output = AnnotationLine> + 'repo, 455 ) -> CommitTemplatePropertyKind<'repo> { 456 CommitTemplatePropertyKind::AnnotationLine(Box::new(property)) 457 } 458} 459 460pub enum CommitTemplatePropertyKind<'repo> { 461 Core(CoreTemplatePropertyKind<'repo>), 462 Commit(Box<dyn TemplateProperty<Output = Commit> + 'repo>), 463 CommitOpt(Box<dyn TemplateProperty<Output = Option<Commit>> + 'repo>), 464 CommitList(Box<dyn TemplateProperty<Output = Vec<Commit>> + 'repo>), 465 CommitRef(Box<dyn TemplateProperty<Output = Rc<CommitRef>> + 'repo>), 466 CommitRefOpt(Box<dyn TemplateProperty<Output = Option<Rc<CommitRef>>> + 'repo>), 467 CommitRefList(Box<dyn TemplateProperty<Output = Vec<Rc<CommitRef>>> + 'repo>), 468 RepoPath(Box<dyn TemplateProperty<Output = RepoPathBuf> + 'repo>), 469 RepoPathOpt(Box<dyn TemplateProperty<Output = Option<RepoPathBuf>> + 'repo>), 470 CommitOrChangeId(Box<dyn TemplateProperty<Output = CommitOrChangeId> + 'repo>), 471 ShortestIdPrefix(Box<dyn TemplateProperty<Output = ShortestIdPrefix> + 'repo>), 472 TreeDiff(Box<dyn TemplateProperty<Output = TreeDiff> + 'repo>), 473 TreeDiffEntry(Box<dyn TemplateProperty<Output = TreeDiffEntry> + 'repo>), 474 TreeDiffEntryList(Box<dyn TemplateProperty<Output = Vec<TreeDiffEntry>> + 'repo>), 475 TreeEntry(Box<dyn TemplateProperty<Output = TreeEntry> + 'repo>), 476 DiffStats(Box<dyn TemplateProperty<Output = DiffStatsFormatted<'repo>> + 'repo>), 477 CryptographicSignatureOpt( 478 Box<dyn TemplateProperty<Output = Option<CryptographicSignature>> + 'repo>, 479 ), 480 AnnotationLine(Box<dyn TemplateProperty<Output = AnnotationLine> + 'repo>), 481} 482 483impl<'repo> IntoTemplateProperty<'repo> for CommitTemplatePropertyKind<'repo> { 484 fn type_name(&self) -> &'static str { 485 match self { 486 CommitTemplatePropertyKind::Core(property) => property.type_name(), 487 CommitTemplatePropertyKind::Commit(_) => "Commit", 488 CommitTemplatePropertyKind::CommitOpt(_) => "Option<Commit>", 489 CommitTemplatePropertyKind::CommitList(_) => "List<Commit>", 490 CommitTemplatePropertyKind::CommitRef(_) => "CommitRef", 491 CommitTemplatePropertyKind::CommitRefOpt(_) => "Option<CommitRef>", 492 CommitTemplatePropertyKind::CommitRefList(_) => "List<CommitRef>", 493 CommitTemplatePropertyKind::RepoPath(_) => "RepoPath", 494 CommitTemplatePropertyKind::RepoPathOpt(_) => "Option<RepoPath>", 495 CommitTemplatePropertyKind::CommitOrChangeId(_) => "CommitOrChangeId", 496 CommitTemplatePropertyKind::ShortestIdPrefix(_) => "ShortestIdPrefix", 497 CommitTemplatePropertyKind::TreeDiff(_) => "TreeDiff", 498 CommitTemplatePropertyKind::TreeDiffEntry(_) => "TreeDiffEntry", 499 CommitTemplatePropertyKind::TreeDiffEntryList(_) => "List<TreeDiffEntry>", 500 CommitTemplatePropertyKind::TreeEntry(_) => "TreeEntry", 501 CommitTemplatePropertyKind::DiffStats(_) => "DiffStats", 502 CommitTemplatePropertyKind::CryptographicSignatureOpt(_) => { 503 "Option<CryptographicSignature>" 504 } 505 CommitTemplatePropertyKind::AnnotationLine(_) => "AnnotationLine", 506 } 507 } 508 509 fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'repo>> { 510 match self { 511 CommitTemplatePropertyKind::Core(property) => property.try_into_boolean(), 512 CommitTemplatePropertyKind::Commit(_) => None, 513 CommitTemplatePropertyKind::CommitOpt(property) => { 514 Some(Box::new(property.map(|opt| opt.is_some()))) 515 } 516 CommitTemplatePropertyKind::CommitList(property) => { 517 Some(Box::new(property.map(|l| !l.is_empty()))) 518 } 519 CommitTemplatePropertyKind::CommitRef(_) => None, 520 CommitTemplatePropertyKind::CommitRefOpt(property) => { 521 Some(Box::new(property.map(|opt| opt.is_some()))) 522 } 523 CommitTemplatePropertyKind::CommitRefList(property) => { 524 Some(Box::new(property.map(|l| !l.is_empty()))) 525 } 526 CommitTemplatePropertyKind::RepoPath(_) => None, 527 CommitTemplatePropertyKind::RepoPathOpt(property) => { 528 Some(Box::new(property.map(|opt| opt.is_some()))) 529 } 530 CommitTemplatePropertyKind::CommitOrChangeId(_) => None, 531 CommitTemplatePropertyKind::ShortestIdPrefix(_) => None, 532 // TODO: boolean cast could be implemented, but explicit 533 // diff.empty() method might be better. 534 CommitTemplatePropertyKind::TreeDiff(_) => None, 535 CommitTemplatePropertyKind::TreeDiffEntry(_) => None, 536 CommitTemplatePropertyKind::TreeDiffEntryList(property) => { 537 Some(Box::new(property.map(|l| !l.is_empty()))) 538 } 539 CommitTemplatePropertyKind::TreeEntry(_) => None, 540 CommitTemplatePropertyKind::DiffStats(_) => None, 541 CommitTemplatePropertyKind::CryptographicSignatureOpt(property) => { 542 Some(Box::new(property.map(|sig| sig.is_some()))) 543 } 544 CommitTemplatePropertyKind::AnnotationLine(_) => None, 545 } 546 } 547 548 fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'repo>> { 549 match self { 550 CommitTemplatePropertyKind::Core(property) => property.try_into_integer(), 551 _ => None, 552 } 553 } 554 555 fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'repo>> { 556 match self { 557 CommitTemplatePropertyKind::Core(property) => property.try_into_plain_text(), 558 _ => { 559 let template = self.try_into_template()?; 560 Some(Box::new(PlainTextFormattedProperty::new(template))) 561 } 562 } 563 } 564 565 fn try_into_template(self) -> Option<Box<dyn Template + 'repo>> { 566 match self { 567 CommitTemplatePropertyKind::Core(property) => property.try_into_template(), 568 CommitTemplatePropertyKind::Commit(_) => None, 569 CommitTemplatePropertyKind::CommitOpt(_) => None, 570 CommitTemplatePropertyKind::CommitList(_) => None, 571 CommitTemplatePropertyKind::CommitRef(property) => Some(property.into_template()), 572 CommitTemplatePropertyKind::CommitRefOpt(property) => Some(property.into_template()), 573 CommitTemplatePropertyKind::CommitRefList(property) => Some(property.into_template()), 574 CommitTemplatePropertyKind::RepoPath(property) => Some(property.into_template()), 575 CommitTemplatePropertyKind::RepoPathOpt(property) => Some(property.into_template()), 576 CommitTemplatePropertyKind::CommitOrChangeId(property) => { 577 Some(property.into_template()) 578 } 579 CommitTemplatePropertyKind::ShortestIdPrefix(property) => { 580 Some(property.into_template()) 581 } 582 CommitTemplatePropertyKind::TreeDiff(_) => None, 583 CommitTemplatePropertyKind::TreeDiffEntry(_) => None, 584 CommitTemplatePropertyKind::TreeDiffEntryList(_) => None, 585 CommitTemplatePropertyKind::TreeEntry(_) => None, 586 CommitTemplatePropertyKind::DiffStats(property) => Some(property.into_template()), 587 CommitTemplatePropertyKind::CryptographicSignatureOpt(_) => None, 588 CommitTemplatePropertyKind::AnnotationLine(_) => None, 589 } 590 } 591 592 fn try_into_eq(self, other: Self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'repo>> { 593 match (self, other) { 594 (CommitTemplatePropertyKind::Core(lhs), CommitTemplatePropertyKind::Core(rhs)) => { 595 lhs.try_into_eq(rhs) 596 } 597 (CommitTemplatePropertyKind::Core(_), _) => None, 598 (CommitTemplatePropertyKind::Commit(_), _) => None, 599 (CommitTemplatePropertyKind::CommitOpt(_), _) => None, 600 (CommitTemplatePropertyKind::CommitList(_), _) => None, 601 (CommitTemplatePropertyKind::CommitRef(_), _) => None, 602 (CommitTemplatePropertyKind::CommitRefOpt(_), _) => None, 603 (CommitTemplatePropertyKind::CommitRefList(_), _) => None, 604 (CommitTemplatePropertyKind::RepoPath(_), _) => None, 605 (CommitTemplatePropertyKind::RepoPathOpt(_), _) => None, 606 (CommitTemplatePropertyKind::CommitOrChangeId(_), _) => None, 607 (CommitTemplatePropertyKind::ShortestIdPrefix(_), _) => None, 608 (CommitTemplatePropertyKind::TreeDiff(_), _) => None, 609 (CommitTemplatePropertyKind::TreeDiffEntry(_), _) => None, 610 (CommitTemplatePropertyKind::TreeDiffEntryList(_), _) => None, 611 (CommitTemplatePropertyKind::TreeEntry(_), _) => None, 612 (CommitTemplatePropertyKind::DiffStats(_), _) => None, 613 (CommitTemplatePropertyKind::CryptographicSignatureOpt(_), _) => None, 614 (CommitTemplatePropertyKind::AnnotationLine(_), _) => None, 615 } 616 } 617 618 fn try_into_cmp( 619 self, 620 other: Self, 621 ) -> Option<Box<dyn TemplateProperty<Output = Ordering> + 'repo>> { 622 match (self, other) { 623 (CommitTemplatePropertyKind::Core(lhs), CommitTemplatePropertyKind::Core(rhs)) => { 624 lhs.try_into_cmp(rhs) 625 } 626 (CommitTemplatePropertyKind::Core(_), _) => None, 627 (CommitTemplatePropertyKind::Commit(_), _) => None, 628 (CommitTemplatePropertyKind::CommitOpt(_), _) => None, 629 (CommitTemplatePropertyKind::CommitList(_), _) => None, 630 (CommitTemplatePropertyKind::CommitRef(_), _) => None, 631 (CommitTemplatePropertyKind::CommitRefOpt(_), _) => None, 632 (CommitTemplatePropertyKind::CommitRefList(_), _) => None, 633 (CommitTemplatePropertyKind::RepoPath(_), _) => None, 634 (CommitTemplatePropertyKind::RepoPathOpt(_), _) => None, 635 (CommitTemplatePropertyKind::CommitOrChangeId(_), _) => None, 636 (CommitTemplatePropertyKind::ShortestIdPrefix(_), _) => None, 637 (CommitTemplatePropertyKind::TreeDiff(_), _) => None, 638 (CommitTemplatePropertyKind::TreeDiffEntry(_), _) => None, 639 (CommitTemplatePropertyKind::TreeDiffEntryList(_), _) => None, 640 (CommitTemplatePropertyKind::TreeEntry(_), _) => None, 641 (CommitTemplatePropertyKind::DiffStats(_), _) => None, 642 (CommitTemplatePropertyKind::CryptographicSignatureOpt(_), _) => None, 643 (CommitTemplatePropertyKind::AnnotationLine(_), _) => None, 644 } 645 } 646} 647 648/// Table of functions that translate method call node of self type `T`. 649pub type CommitTemplateBuildMethodFnMap<'repo, T> = 650 TemplateBuildMethodFnMap<'repo, CommitTemplateLanguage<'repo>, T>; 651 652/// Symbol table of methods available in the commit template. 653pub struct CommitTemplateBuildFnTable<'repo> { 654 pub core: CoreTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>, 655 pub commit_methods: CommitTemplateBuildMethodFnMap<'repo, Commit>, 656 pub commit_ref_methods: CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>>, 657 pub repo_path_methods: CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf>, 658 pub commit_or_change_id_methods: CommitTemplateBuildMethodFnMap<'repo, CommitOrChangeId>, 659 pub shortest_id_prefix_methods: CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix>, 660 pub tree_diff_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiff>, 661 pub tree_diff_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry>, 662 pub tree_entry_methods: CommitTemplateBuildMethodFnMap<'repo, TreeEntry>, 663 pub diff_stats_methods: CommitTemplateBuildMethodFnMap<'repo, DiffStats>, 664 pub cryptographic_signature_methods: 665 CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature>, 666 pub annotation_line_methods: CommitTemplateBuildMethodFnMap<'repo, AnnotationLine>, 667} 668 669impl<'repo> CommitTemplateBuildFnTable<'repo> { 670 /// Creates new symbol table containing the builtin methods. 671 fn builtin() -> Self { 672 CommitTemplateBuildFnTable { 673 core: CoreTemplateBuildFnTable::builtin(), 674 commit_methods: builtin_commit_methods(), 675 commit_ref_methods: builtin_commit_ref_methods(), 676 repo_path_methods: builtin_repo_path_methods(), 677 commit_or_change_id_methods: builtin_commit_or_change_id_methods(), 678 shortest_id_prefix_methods: builtin_shortest_id_prefix_methods(), 679 tree_diff_methods: builtin_tree_diff_methods(), 680 tree_diff_entry_methods: builtin_tree_diff_entry_methods(), 681 tree_entry_methods: builtin_tree_entry_methods(), 682 diff_stats_methods: builtin_diff_stats_methods(), 683 cryptographic_signature_methods: builtin_cryptographic_signature_methods(), 684 annotation_line_methods: builtin_annotation_line_methods(), 685 } 686 } 687 688 pub fn empty() -> Self { 689 CommitTemplateBuildFnTable { 690 core: CoreTemplateBuildFnTable::empty(), 691 commit_methods: HashMap::new(), 692 commit_ref_methods: HashMap::new(), 693 repo_path_methods: HashMap::new(), 694 commit_or_change_id_methods: HashMap::new(), 695 shortest_id_prefix_methods: HashMap::new(), 696 tree_diff_methods: HashMap::new(), 697 tree_diff_entry_methods: HashMap::new(), 698 tree_entry_methods: HashMap::new(), 699 diff_stats_methods: HashMap::new(), 700 cryptographic_signature_methods: HashMap::new(), 701 annotation_line_methods: HashMap::new(), 702 } 703 } 704 705 fn merge(&mut self, extension: CommitTemplateBuildFnTable<'repo>) { 706 let CommitTemplateBuildFnTable { 707 core, 708 commit_methods, 709 commit_ref_methods, 710 repo_path_methods, 711 commit_or_change_id_methods, 712 shortest_id_prefix_methods, 713 tree_diff_methods, 714 tree_diff_entry_methods, 715 tree_entry_methods, 716 diff_stats_methods, 717 cryptographic_signature_methods, 718 annotation_line_methods, 719 } = extension; 720 721 self.core.merge(core); 722 merge_fn_map(&mut self.commit_methods, commit_methods); 723 merge_fn_map(&mut self.commit_ref_methods, commit_ref_methods); 724 merge_fn_map(&mut self.repo_path_methods, repo_path_methods); 725 merge_fn_map( 726 &mut self.commit_or_change_id_methods, 727 commit_or_change_id_methods, 728 ); 729 merge_fn_map( 730 &mut self.shortest_id_prefix_methods, 731 shortest_id_prefix_methods, 732 ); 733 merge_fn_map(&mut self.tree_diff_methods, tree_diff_methods); 734 merge_fn_map(&mut self.tree_diff_entry_methods, tree_diff_entry_methods); 735 merge_fn_map(&mut self.tree_entry_methods, tree_entry_methods); 736 merge_fn_map(&mut self.diff_stats_methods, diff_stats_methods); 737 merge_fn_map( 738 &mut self.cryptographic_signature_methods, 739 cryptographic_signature_methods, 740 ); 741 merge_fn_map(&mut self.annotation_line_methods, annotation_line_methods); 742 } 743} 744 745#[derive(Default)] 746pub struct CommitKeywordCache<'repo> { 747 // Build index lazily, and Rc to get away from &self lifetime. 748 bookmarks_index: OnceCell<Rc<CommitRefsIndex>>, 749 tags_index: OnceCell<Rc<CommitRefsIndex>>, 750 git_refs_index: OnceCell<Rc<CommitRefsIndex>>, 751 is_immutable_fn: OnceCell<Rc<RevsetContainingFn<'repo>>>, 752} 753 754impl<'repo> CommitKeywordCache<'repo> { 755 pub fn bookmarks_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> { 756 self.bookmarks_index 757 .get_or_init(|| Rc::new(build_bookmarks_index(repo))) 758 } 759 760 pub fn tags_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> { 761 self.tags_index 762 .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().tags()))) 763 } 764 765 pub fn git_refs_index(&self, repo: &dyn Repo) -> &Rc<CommitRefsIndex> { 766 self.git_refs_index 767 .get_or_init(|| Rc::new(build_commit_refs_index(repo.view().git_refs()))) 768 } 769 770 pub fn is_immutable_fn( 771 &self, 772 language: &CommitTemplateLanguage<'repo>, 773 span: pest::Span<'_>, 774 ) -> TemplateParseResult<&Rc<RevsetContainingFn<'repo>>> { 775 // Alternatively, a negated (i.e. visible mutable) set could be computed. 776 // It's usually smaller than the immutable set. The revset engine can also 777 // optimize "::<recent_heads>" query to use bitset-based implementation. 778 self.is_immutable_fn.get_or_try_init(|| { 779 let expression = &language.immutable_expression; 780 let revset = evaluate_revset_expression(language, span, expression)?; 781 Ok(revset.containing_fn().into()) 782 }) 783 } 784} 785 786fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Commit> { 787 type L<'repo> = CommitTemplateLanguage<'repo>; 788 // Not using maplit::hashmap!{} or custom declarative macro here because 789 // code completion inside macro is quite restricted. 790 let mut map = CommitTemplateBuildMethodFnMap::<Commit>::new(); 791 map.insert( 792 "description", 793 |_language, _diagnostics, _build_ctx, self_property, function| { 794 function.expect_no_arguments()?; 795 let out_property = 796 self_property.map(|commit| text_util::complete_newline(commit.description())); 797 Ok(L::wrap_string(out_property)) 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 = 805 self_property.map(|commit| CommitOrChangeId::Change(commit.change_id().to_owned())); 806 Ok(L::wrap_commit_or_change_id(out_property)) 807 }, 808 ); 809 map.insert( 810 "commit_id", 811 |_language, _diagnostics, _build_ctx, self_property, function| { 812 function.expect_no_arguments()?; 813 let out_property = 814 self_property.map(|commit| CommitOrChangeId::Commit(commit.id().to_owned())); 815 Ok(L::wrap_commit_or_change_id(out_property)) 816 }, 817 ); 818 map.insert( 819 "parents", 820 |_language, _diagnostics, _build_ctx, self_property, function| { 821 function.expect_no_arguments()?; 822 let out_property = 823 self_property.and_then(|commit| Ok(commit.parents().try_collect()?)); 824 Ok(L::wrap_commit_list(out_property)) 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(L::wrap_signature(out_property)) 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(L::wrap_signature(out_property)) 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(L::wrap_boolean(out_property)) 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(L::wrap_cryptographic_signature_opt(out_property)) 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(L::wrap_string(out_property)) 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(L::wrap_boolean(out_property)) 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() 895 }); 896 Ok(L::wrap_commit_ref_list(out_property)) 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() 914 }); 915 Ok(L::wrap_commit_ref_list(out_property)) 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() 933 }); 934 Ok(L::wrap_commit_ref_list(out_property)) 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(L::wrap_commit_ref_list(out_property)) 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(L::wrap_commit_ref_list(out_property)) 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(L::wrap_boolean(out_property)) 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(L::wrap_boolean(out_property)) 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(L::wrap_boolean(out_property)) 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(L::wrap_boolean(out_property)) 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(L::wrap_boolean(out_property)) 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(L::wrap_boolean(out_property)) 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(L::wrap_boolean(out_property)) 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(L::wrap_tree_diff(out_property)) 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(L::wrap_boolean(out_property)) 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 /// Local name. 1141 name: String, 1142 /// Remote name if this is a remote or Git-tracking ref. 1143 remote: Option<String>, 1144 /// Target commit ids. 1145 target: RefTarget, 1146 /// Local ref metadata which tracks this remote ref. 1147 tracking_ref: Option<TrackingRef>, 1148 /// Local ref is synchronized with all tracking remotes, or tracking remote 1149 /// ref is synchronized with the local. 1150 synced: bool, 1151} 1152 1153#[derive(Debug)] 1154struct TrackingRef { 1155 /// Local ref target which tracks the other remote ref. 1156 target: RefTarget, 1157 /// Number of commits ahead of the tracking `target`. 1158 ahead_count: OnceCell<SizeHint>, 1159 /// Number of commits behind of the tracking `target`. 1160 behind_count: OnceCell<SizeHint>, 1161} 1162 1163impl CommitRef { 1164 // CommitRef is wrapped by Rc<T> to make it cheaply cloned and share 1165 // lazy-evaluation results across clones. 1166 1167 /// Creates local ref representation which might track some of the 1168 /// `remote_refs`. 1169 pub fn local<'a>( 1170 name: impl Into<String>, 1171 target: RefTarget, 1172 remote_refs: impl IntoIterator<Item = &'a RemoteRef>, 1173 ) -> Rc<Self> { 1174 let synced = remote_refs 1175 .into_iter() 1176 .all(|remote_ref| !remote_ref.is_tracked() || remote_ref.target == target); 1177 Rc::new(CommitRef { 1178 name: name.into(), 1179 remote: None, 1180 target, 1181 tracking_ref: None, 1182 synced, 1183 }) 1184 } 1185 1186 /// Creates local ref representation which doesn't track any remote refs. 1187 pub fn local_only(name: impl Into<String>, target: RefTarget) -> Rc<Self> { 1188 Self::local(name, target, []) 1189 } 1190 1191 /// Creates remote ref representation which might be tracked by a local ref 1192 /// pointing to the `local_target`. 1193 pub fn remote( 1194 name: impl Into<String>, 1195 remote_name: impl Into<String>, 1196 remote_ref: RemoteRef, 1197 local_target: &RefTarget, 1198 ) -> Rc<Self> { 1199 let synced = remote_ref.is_tracked() && remote_ref.target == *local_target; 1200 let tracking_ref = remote_ref.is_tracked().then(|| { 1201 let count = if synced { 1202 OnceCell::from((0, Some(0))) // fast path for synced remotes 1203 } else { 1204 OnceCell::new() 1205 }; 1206 TrackingRef { 1207 target: local_target.clone(), 1208 ahead_count: count.clone(), 1209 behind_count: count, 1210 } 1211 }); 1212 Rc::new(CommitRef { 1213 name: name.into(), 1214 remote: Some(remote_name.into()), 1215 target: remote_ref.target, 1216 tracking_ref, 1217 synced, 1218 }) 1219 } 1220 1221 /// Creates remote ref representation which isn't tracked by a local ref. 1222 pub fn remote_only( 1223 name: impl Into<String>, 1224 remote_name: impl Into<String>, 1225 target: RefTarget, 1226 ) -> Rc<Self> { 1227 Rc::new(CommitRef { 1228 name: name.into(), 1229 remote: Some(remote_name.into()), 1230 target, 1231 tracking_ref: None, 1232 synced: false, // has no local counterpart 1233 }) 1234 } 1235 1236 /// Local name. 1237 pub fn name(&self) -> &str { 1238 &self.name 1239 } 1240 1241 /// Remote name if this is a remote or Git-tracking ref. 1242 pub fn remote_name(&self) -> Option<&str> { 1243 self.remote.as_deref() 1244 } 1245 1246 /// Target commit ids. 1247 pub fn target(&self) -> &RefTarget { 1248 &self.target 1249 } 1250 1251 /// Returns true if this is a local ref. 1252 pub fn is_local(&self) -> bool { 1253 self.remote.is_none() 1254 } 1255 1256 /// Returns true if this is a remote ref. 1257 pub fn is_remote(&self) -> bool { 1258 self.remote.is_some() 1259 } 1260 1261 /// Returns true if this ref points to no commit. 1262 pub fn is_absent(&self) -> bool { 1263 self.target.is_absent() 1264 } 1265 1266 /// Returns true if this ref points to any commit. 1267 pub fn is_present(&self) -> bool { 1268 self.target.is_present() 1269 } 1270 1271 /// Whether the ref target has conflicts. 1272 pub fn has_conflict(&self) -> bool { 1273 self.target.has_conflict() 1274 } 1275 1276 /// Returns true if this ref is tracked by a local ref. The local ref might 1277 /// have been deleted (but not pushed yet.) 1278 pub fn is_tracked(&self) -> bool { 1279 self.tracking_ref.is_some() 1280 } 1281 1282 /// Returns true if this ref is tracked by a local ref, and if the local ref 1283 /// is present. 1284 pub fn is_tracking_present(&self) -> bool { 1285 self.tracking_ref 1286 .as_ref() 1287 .is_some_and(|tracking| tracking.target.is_present()) 1288 } 1289 1290 /// Number of commits ahead of the tracking local ref. 1291 fn tracking_ahead_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> { 1292 let Some(tracking) = &self.tracking_ref else { 1293 return Err(TemplatePropertyError("Not a tracked remote ref".into())); 1294 }; 1295 tracking 1296 .ahead_count 1297 .get_or_try_init(|| { 1298 let self_ids = self.target.added_ids().cloned().collect_vec(); 1299 let other_ids = tracking.target.added_ids().cloned().collect_vec(); 1300 Ok(revset::walk_revs(repo, &self_ids, &other_ids)?.count_estimate()?) 1301 }) 1302 .copied() 1303 } 1304 1305 /// Number of commits behind of the tracking local ref. 1306 fn tracking_behind_count(&self, repo: &dyn Repo) -> Result<SizeHint, TemplatePropertyError> { 1307 let Some(tracking) = &self.tracking_ref else { 1308 return Err(TemplatePropertyError("Not a tracked remote ref".into())); 1309 }; 1310 tracking 1311 .behind_count 1312 .get_or_try_init(|| { 1313 let self_ids = self.target.added_ids().cloned().collect_vec(); 1314 let other_ids = tracking.target.added_ids().cloned().collect_vec(); 1315 Ok(revset::walk_revs(repo, &other_ids, &self_ids)?.count_estimate()?) 1316 }) 1317 .copied() 1318 } 1319} 1320 1321// If wrapping with Rc<T> becomes common, add generic impl for Rc<T>. 1322impl Template for Rc<CommitRef> { 1323 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1324 write!(formatter.labeled("name"), "{}", self.name)?; 1325 if let Some(remote) = &self.remote { 1326 write!(formatter, "@")?; 1327 write!(formatter.labeled("remote"), "{remote}")?; 1328 } 1329 // Don't show both conflict and unsynced sigils as conflicted ref wouldn't 1330 // be pushed. 1331 if self.has_conflict() { 1332 write!(formatter, "??")?; 1333 } else if self.is_local() && !self.synced { 1334 write!(formatter, "*")?; 1335 } 1336 Ok(()) 1337 } 1338} 1339 1340impl Template for Vec<Rc<CommitRef>> { 1341 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1342 templater::format_joined(formatter, self, " ") 1343 } 1344} 1345 1346fn builtin_commit_ref_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Rc<CommitRef>> { 1347 type L<'repo> = CommitTemplateLanguage<'repo>; 1348 // Not using maplit::hashmap!{} or custom declarative macro here because 1349 // code completion inside macro is quite restricted. 1350 let mut map = CommitTemplateBuildMethodFnMap::<Rc<CommitRef>>::new(); 1351 map.insert( 1352 "name", 1353 |_language, _diagnostics, _build_ctx, self_property, function| { 1354 function.expect_no_arguments()?; 1355 let out_property = self_property.map(|commit_ref| commit_ref.name.clone()); 1356 Ok(L::wrap_string(out_property)) 1357 }, 1358 ); 1359 map.insert( 1360 "remote", 1361 |_language, _diagnostics, _build_ctx, self_property, function| { 1362 function.expect_no_arguments()?; 1363 let out_property = 1364 self_property.map(|commit_ref| commit_ref.remote.clone().unwrap_or_default()); 1365 Ok(L::wrap_string(out_property)) 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(L::wrap_boolean(out_property)) 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(L::wrap_boolean(out_property)) 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(L::wrap_commit_opt(out_property)) 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 Ok(ids.map(|id| repo.store().get_commit(id)).try_collect()?) 1404 }); 1405 Ok(L::wrap_commit_list(out_property)) 1406 }, 1407 ); 1408 map.insert( 1409 "added_targets", 1410 |language, _diagnostics, _build_ctx, self_property, function| { 1411 function.expect_no_arguments()?; 1412 let repo = language.repo; 1413 let out_property = self_property.and_then(|commit_ref| { 1414 let ids = commit_ref.target.added_ids(); 1415 Ok(ids.map(|id| repo.store().get_commit(id)).try_collect()?) 1416 }); 1417 Ok(L::wrap_commit_list(out_property)) 1418 }, 1419 ); 1420 map.insert( 1421 "tracked", 1422 |_language, _diagnostics, _build_ctx, self_property, function| { 1423 function.expect_no_arguments()?; 1424 let out_property = self_property.map(|commit_ref| commit_ref.is_tracked()); 1425 Ok(L::wrap_boolean(out_property)) 1426 }, 1427 ); 1428 map.insert( 1429 "tracking_present", 1430 |_language, _diagnostics, _build_ctx, self_property, function| { 1431 function.expect_no_arguments()?; 1432 let out_property = self_property.map(|commit_ref| commit_ref.is_tracking_present()); 1433 Ok(L::wrap_boolean(out_property)) 1434 }, 1435 ); 1436 map.insert( 1437 "tracking_ahead_count", 1438 |language, _diagnostics, _build_ctx, self_property, function| { 1439 function.expect_no_arguments()?; 1440 let repo = language.repo; 1441 let out_property = 1442 self_property.and_then(|commit_ref| commit_ref.tracking_ahead_count(repo)); 1443 Ok(L::wrap_size_hint(out_property)) 1444 }, 1445 ); 1446 map.insert( 1447 "tracking_behind_count", 1448 |language, _diagnostics, _build_ctx, self_property, function| { 1449 function.expect_no_arguments()?; 1450 let repo = language.repo; 1451 let out_property = 1452 self_property.and_then(|commit_ref| commit_ref.tracking_behind_count(repo)); 1453 Ok(L::wrap_size_hint(out_property)) 1454 }, 1455 ); 1456 map 1457} 1458 1459/// Cache for reverse lookup refs. 1460#[derive(Clone, Debug, Default)] 1461pub struct CommitRefsIndex { 1462 index: HashMap<CommitId, Vec<Rc<CommitRef>>>, 1463} 1464 1465impl CommitRefsIndex { 1466 fn insert<'a>(&mut self, ids: impl IntoIterator<Item = &'a CommitId>, name: Rc<CommitRef>) { 1467 for id in ids { 1468 let commit_refs = self.index.entry(id.clone()).or_default(); 1469 commit_refs.push(name.clone()); 1470 } 1471 } 1472 1473 pub fn get(&self, id: &CommitId) -> &[Rc<CommitRef>] { 1474 self.index.get(id).map_or(&[], |refs: &Vec<_>| refs) 1475 } 1476} 1477 1478fn build_bookmarks_index(repo: &dyn Repo) -> CommitRefsIndex { 1479 let mut index = CommitRefsIndex::default(); 1480 for (bookmark_name, bookmark_target) in repo.view().bookmarks() { 1481 let local_target = bookmark_target.local_target; 1482 let remote_refs = bookmark_target.remote_refs; 1483 if local_target.is_present() { 1484 let commit_ref = CommitRef::local( 1485 bookmark_name, 1486 local_target.clone(), 1487 remote_refs.iter().map(|&(_, remote_ref)| remote_ref), 1488 ); 1489 index.insert(local_target.added_ids(), commit_ref); 1490 } 1491 for &(remote_name, remote_ref) in &remote_refs { 1492 let commit_ref = 1493 CommitRef::remote(bookmark_name, remote_name, remote_ref.clone(), local_target); 1494 index.insert(remote_ref.target.added_ids(), commit_ref); 1495 } 1496 } 1497 index 1498} 1499 1500fn build_commit_refs_index<'a, K: Into<String>>( 1501 ref_pairs: impl IntoIterator<Item = (K, &'a RefTarget)>, 1502) -> CommitRefsIndex { 1503 let mut index = CommitRefsIndex::default(); 1504 for (name, target) in ref_pairs { 1505 let commit_ref = CommitRef::local_only(name, target.clone()); 1506 index.insert(target.added_ids(), commit_ref); 1507 } 1508 index 1509} 1510 1511impl Template for RepoPathBuf { 1512 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1513 write!(formatter, "{}", self.as_internal_file_string()) 1514 } 1515} 1516 1517fn builtin_repo_path_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, RepoPathBuf> { 1518 type L<'repo> = CommitTemplateLanguage<'repo>; 1519 // Not using maplit::hashmap!{} or custom declarative macro here because 1520 // code completion inside macro is quite restricted. 1521 let mut map = CommitTemplateBuildMethodFnMap::<RepoPathBuf>::new(); 1522 map.insert( 1523 "display", 1524 |language, _diagnostics, _build_ctx, self_property, function| { 1525 function.expect_no_arguments()?; 1526 let path_converter = language.path_converter; 1527 let out_property = self_property.map(|path| path_converter.format_file_path(&path)); 1528 Ok(L::wrap_string(out_property)) 1529 }, 1530 ); 1531 map.insert( 1532 "parent", 1533 |_language, _diagnostics, _build_ctx, self_property, function| { 1534 function.expect_no_arguments()?; 1535 let out_property = self_property.map(|path| Some(path.parent()?.to_owned())); 1536 Ok(L::wrap_repo_path_opt(out_property)) 1537 }, 1538 ); 1539 map 1540} 1541 1542#[derive(Clone, Debug, Eq, PartialEq)] 1543pub enum CommitOrChangeId { 1544 Commit(CommitId), 1545 Change(ChangeId), 1546} 1547 1548impl CommitOrChangeId { 1549 pub fn hex(&self) -> String { 1550 match self { 1551 CommitOrChangeId::Commit(id) => id.hex(), 1552 CommitOrChangeId::Change(id) => id.reverse_hex(), 1553 } 1554 } 1555 1556 pub fn short(&self, total_len: usize) -> String { 1557 let mut hex = self.hex(); 1558 hex.truncate(total_len); 1559 hex 1560 } 1561 1562 /// The length of the id printed will be the maximum of `total_len` and the 1563 /// length of the shortest unique prefix 1564 pub fn shortest( 1565 &self, 1566 repo: &dyn Repo, 1567 index: &IdPrefixIndex, 1568 total_len: usize, 1569 ) -> ShortestIdPrefix { 1570 let mut hex = self.hex(); 1571 let prefix_len = match self { 1572 CommitOrChangeId::Commit(id) => index.shortest_commit_prefix_len(repo, id), 1573 CommitOrChangeId::Change(id) => index.shortest_change_prefix_len(repo, id), 1574 }; 1575 hex.truncate(max(prefix_len, total_len)); 1576 let rest = hex.split_off(prefix_len); 1577 ShortestIdPrefix { prefix: hex, rest } 1578 } 1579} 1580 1581impl Template for CommitOrChangeId { 1582 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1583 write!(formatter, "{}", self.hex()) 1584 } 1585} 1586 1587fn builtin_commit_or_change_id_methods<'repo>( 1588) -> CommitTemplateBuildMethodFnMap<'repo, CommitOrChangeId> { 1589 type L<'repo> = CommitTemplateLanguage<'repo>; 1590 // Not using maplit::hashmap!{} or custom declarative macro here because 1591 // code completion inside macro is quite restricted. 1592 let mut map = CommitTemplateBuildMethodFnMap::<CommitOrChangeId>::new(); 1593 map.insert( 1594 "normal_hex", 1595 |_language, _diagnostics, _build_ctx, self_property, function| { 1596 function.expect_no_arguments()?; 1597 Ok(L::wrap_string(self_property.map(|id| { 1598 // Note: this is _not_ the same as id.hex() for ChangeId, which 1599 // returns the "reverse" hex (z-k), instead of the "forward" / 1600 // normal hex (0-9a-f) we want here. 1601 match id { 1602 CommitOrChangeId::Commit(id) => id.hex(), 1603 CommitOrChangeId::Change(id) => id.hex(), 1604 } 1605 }))) 1606 }, 1607 ); 1608 map.insert( 1609 "short", 1610 |language, diagnostics, build_ctx, self_property, function| { 1611 let ([], [len_node]) = function.expect_arguments()?; 1612 let len_property = len_node 1613 .map(|node| { 1614 template_builder::expect_usize_expression( 1615 language, 1616 diagnostics, 1617 build_ctx, 1618 node, 1619 ) 1620 }) 1621 .transpose()?; 1622 let out_property = 1623 (self_property, len_property).map(|(id, len)| id.short(len.unwrap_or(12))); 1624 Ok(L::wrap_string(out_property)) 1625 }, 1626 ); 1627 map.insert( 1628 "shortest", 1629 |language, diagnostics, build_ctx, self_property, function| { 1630 let ([], [len_node]) = function.expect_arguments()?; 1631 let len_property = len_node 1632 .map(|node| { 1633 template_builder::expect_usize_expression( 1634 language, 1635 diagnostics, 1636 build_ctx, 1637 node, 1638 ) 1639 }) 1640 .transpose()?; 1641 let repo = language.repo; 1642 let index = match language.id_prefix_context.populate(repo) { 1643 Ok(index) => index, 1644 Err(err) => { 1645 // Not an error because we can still produce somewhat 1646 // reasonable output. 1647 diagnostics.add_warning( 1648 TemplateParseError::expression( 1649 "Failed to load short-prefixes index", 1650 function.name_span, 1651 ) 1652 .with_source(err), 1653 ); 1654 IdPrefixIndex::empty() 1655 } 1656 }; 1657 let out_property = (self_property, len_property) 1658 .map(move |(id, len)| id.shortest(repo, &index, len.unwrap_or(0))); 1659 Ok(L::wrap_shortest_id_prefix(out_property)) 1660 }, 1661 ); 1662 map 1663} 1664 1665pub struct ShortestIdPrefix { 1666 pub prefix: String, 1667 pub rest: String, 1668} 1669 1670impl Template for ShortestIdPrefix { 1671 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1672 write!(formatter.labeled("prefix"), "{}", self.prefix)?; 1673 write!(formatter.labeled("rest"), "{}", self.rest)?; 1674 Ok(()) 1675 } 1676} 1677 1678impl ShortestIdPrefix { 1679 fn to_upper(&self) -> Self { 1680 Self { 1681 prefix: self.prefix.to_ascii_uppercase(), 1682 rest: self.rest.to_ascii_uppercase(), 1683 } 1684 } 1685 fn to_lower(&self) -> Self { 1686 Self { 1687 prefix: self.prefix.to_ascii_lowercase(), 1688 rest: self.rest.to_ascii_lowercase(), 1689 } 1690 } 1691} 1692 1693fn builtin_shortest_id_prefix_methods<'repo>( 1694) -> CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix> { 1695 type L<'repo> = CommitTemplateLanguage<'repo>; 1696 // Not using maplit::hashmap!{} or custom declarative macro here because 1697 // code completion inside macro is quite restricted. 1698 let mut map = CommitTemplateBuildMethodFnMap::<ShortestIdPrefix>::new(); 1699 map.insert( 1700 "prefix", 1701 |_language, _diagnostics, _build_ctx, self_property, function| { 1702 function.expect_no_arguments()?; 1703 let out_property = self_property.map(|id| id.prefix); 1704 Ok(L::wrap_string(out_property)) 1705 }, 1706 ); 1707 map.insert( 1708 "rest", 1709 |_language, _diagnostics, _build_ctx, self_property, function| { 1710 function.expect_no_arguments()?; 1711 let out_property = self_property.map(|id| id.rest); 1712 Ok(L::wrap_string(out_property)) 1713 }, 1714 ); 1715 map.insert( 1716 "upper", 1717 |_language, _diagnostics, _build_ctx, self_property, function| { 1718 function.expect_no_arguments()?; 1719 let out_property = self_property.map(|id| id.to_upper()); 1720 Ok(L::wrap_shortest_id_prefix(out_property)) 1721 }, 1722 ); 1723 map.insert( 1724 "lower", 1725 |_language, _diagnostics, _build_ctx, self_property, function| { 1726 function.expect_no_arguments()?; 1727 let out_property = self_property.map(|id| id.to_lower()); 1728 Ok(L::wrap_shortest_id_prefix(out_property)) 1729 }, 1730 ); 1731 map 1732} 1733 1734/// Pair of trees to be diffed. 1735#[derive(Debug)] 1736pub struct TreeDiff { 1737 from_tree: MergedTree, 1738 to_tree: MergedTree, 1739 matcher: Rc<dyn Matcher>, 1740 copy_records: CopyRecords, 1741} 1742 1743impl TreeDiff { 1744 fn from_commit( 1745 repo: &dyn Repo, 1746 commit: &Commit, 1747 matcher: Rc<dyn Matcher>, 1748 ) -> BackendResult<Self> { 1749 let mut copy_records = CopyRecords::default(); 1750 for parent in commit.parent_ids() { 1751 let records = 1752 diff_util::get_copy_records(repo.store(), parent, commit.id(), &*matcher)?; 1753 copy_records.add_records(records)?; 1754 } 1755 Ok(TreeDiff { 1756 from_tree: commit.parent_tree(repo)?, 1757 to_tree: commit.tree()?, 1758 matcher, 1759 copy_records, 1760 }) 1761 } 1762 1763 fn diff_stream(&self) -> BoxStream<'_, CopiesTreeDiffEntry> { 1764 self.from_tree 1765 .diff_stream_with_copies(&self.to_tree, &*self.matcher, &self.copy_records) 1766 } 1767 1768 async fn collect_entries(&self) -> BackendResult<Vec<TreeDiffEntry>> { 1769 self.diff_stream() 1770 .map(TreeDiffEntry::from_backend_entry_with_copies) 1771 .try_collect() 1772 .await 1773 } 1774 1775 fn into_formatted<F, E>(self, show: F) -> TreeDiffFormatted<F> 1776 where 1777 F: Fn(&mut dyn Formatter, &Store, BoxStream<CopiesTreeDiffEntry>) -> Result<(), E>, 1778 E: Into<TemplatePropertyError>, 1779 { 1780 TreeDiffFormatted { diff: self, show } 1781 } 1782} 1783 1784/// Tree diff to be rendered by predefined function `F`. 1785struct TreeDiffFormatted<F> { 1786 diff: TreeDiff, 1787 show: F, 1788} 1789 1790impl<F, E> Template for TreeDiffFormatted<F> 1791where 1792 F: Fn(&mut dyn Formatter, &Store, BoxStream<CopiesTreeDiffEntry>) -> Result<(), E>, 1793 E: Into<TemplatePropertyError>, 1794{ 1795 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1796 let show = &self.show; 1797 let store = self.diff.from_tree.store(); 1798 let tree_diff = self.diff.diff_stream(); 1799 show(formatter.as_mut(), store, tree_diff).or_else(|err| formatter.handle_error(err.into())) 1800 } 1801} 1802 1803fn builtin_tree_diff_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiff> { 1804 type L<'repo> = CommitTemplateLanguage<'repo>; 1805 // Not using maplit::hashmap!{} or custom declarative macro here because 1806 // code completion inside macro is quite restricted. 1807 let mut map = CommitTemplateBuildMethodFnMap::<TreeDiff>::new(); 1808 map.insert( 1809 "files", 1810 |_language, _diagnostics, _build_ctx, self_property, function| { 1811 function.expect_no_arguments()?; 1812 // TODO: cache and reuse diff entries within the current evaluation? 1813 let out_property = 1814 self_property.and_then(|diff| Ok(diff.collect_entries().block_on()?)); 1815 Ok(L::wrap_tree_diff_entry_list(out_property)) 1816 }, 1817 ); 1818 map.insert( 1819 "color_words", 1820 |language, diagnostics, build_ctx, self_property, function| { 1821 let ([], [context_node]) = function.expect_arguments()?; 1822 let context_property = context_node 1823 .map(|node| { 1824 template_builder::expect_usize_expression( 1825 language, 1826 diagnostics, 1827 build_ctx, 1828 node, 1829 ) 1830 }) 1831 .transpose()?; 1832 let path_converter = language.path_converter; 1833 let options = diff_util::ColorWordsDiffOptions::from_settings(language.settings()) 1834 .map_err(|err| { 1835 let message = "Failed to load diff settings"; 1836 TemplateParseError::expression(message, function.name_span).with_source(err) 1837 })?; 1838 let conflict_marker_style = language.conflict_marker_style; 1839 let template = (self_property, context_property) 1840 .map(move |(diff, context)| { 1841 let mut options = options.clone(); 1842 if let Some(context) = context { 1843 options.context = context; 1844 } 1845 diff.into_formatted(move |formatter, store, tree_diff| { 1846 diff_util::show_color_words_diff( 1847 formatter, 1848 store, 1849 tree_diff, 1850 path_converter, 1851 &options, 1852 conflict_marker_style, 1853 ) 1854 }) 1855 }) 1856 .into_template(); 1857 Ok(L::wrap_template(template)) 1858 }, 1859 ); 1860 map.insert( 1861 "git", 1862 |language, diagnostics, build_ctx, self_property, function| { 1863 let ([], [context_node]) = function.expect_arguments()?; 1864 let context_property = context_node 1865 .map(|node| { 1866 template_builder::expect_usize_expression( 1867 language, 1868 diagnostics, 1869 build_ctx, 1870 node, 1871 ) 1872 }) 1873 .transpose()?; 1874 let options = diff_util::UnifiedDiffOptions::from_settings(language.settings()) 1875 .map_err(|err| { 1876 let message = "Failed to load diff settings"; 1877 TemplateParseError::expression(message, function.name_span).with_source(err) 1878 })?; 1879 let conflict_marker_style = language.conflict_marker_style; 1880 let template = (self_property, context_property) 1881 .map(move |(diff, context)| { 1882 let mut options = options.clone(); 1883 if let Some(context) = context { 1884 options.context = context; 1885 } 1886 diff.into_formatted(move |formatter, store, tree_diff| { 1887 diff_util::show_git_diff( 1888 formatter, 1889 store, 1890 tree_diff, 1891 &options, 1892 conflict_marker_style, 1893 ) 1894 }) 1895 }) 1896 .into_template(); 1897 Ok(L::wrap_template(template)) 1898 }, 1899 ); 1900 map.insert( 1901 "stat", 1902 |language, diagnostics, build_ctx, self_property, function| { 1903 let ([], [width_node]) = function.expect_arguments()?; 1904 let width_property = width_node 1905 .map(|node| { 1906 template_builder::expect_usize_expression( 1907 language, 1908 diagnostics, 1909 build_ctx, 1910 node, 1911 ) 1912 }) 1913 .transpose()?; 1914 let path_converter = language.path_converter; 1915 // No user configuration exists for diff stat. 1916 let options = diff_util::DiffStatOptions::default(); 1917 let conflict_marker_style = language.conflict_marker_style; 1918 // TODO: cache and reuse stats within the current evaluation? 1919 let out_property = (self_property, width_property).and_then(move |(diff, width)| { 1920 let store = diff.from_tree.store(); 1921 let tree_diff = diff.diff_stream(); 1922 let stats = DiffStats::calculate(store, tree_diff, &options, conflict_marker_style) 1923 .block_on()?; 1924 Ok(DiffStatsFormatted { 1925 stats, 1926 path_converter, 1927 // TODO: fall back to current available width 1928 width: width.unwrap_or(80), 1929 }) 1930 }); 1931 Ok(L::wrap_diff_stats(out_property)) 1932 }, 1933 ); 1934 map.insert( 1935 "summary", 1936 |language, _diagnostics, _build_ctx, self_property, function| { 1937 function.expect_no_arguments()?; 1938 let path_converter = language.path_converter; 1939 let template = self_property 1940 .map(move |diff| { 1941 diff.into_formatted(move |formatter, _store, tree_diff| { 1942 diff_util::show_diff_summary(formatter, tree_diff, path_converter) 1943 }) 1944 }) 1945 .into_template(); 1946 Ok(L::wrap_template(template)) 1947 }, 1948 ); 1949 // TODO: add support for external tools 1950 map 1951} 1952 1953/// [`MergedTree`] diff entry. 1954#[derive(Clone, Debug)] 1955pub struct TreeDiffEntry { 1956 pub path: CopiesTreeDiffEntryPath, 1957 pub source_value: MergedTreeValue, 1958 pub target_value: MergedTreeValue, 1959} 1960 1961impl TreeDiffEntry { 1962 fn from_backend_entry_with_copies(entry: CopiesTreeDiffEntry) -> BackendResult<Self> { 1963 let (source_value, target_value) = entry.values?; 1964 Ok(TreeDiffEntry { 1965 path: entry.path, 1966 source_value, 1967 target_value, 1968 }) 1969 } 1970 1971 fn status_label(&self) -> &'static str { 1972 let (label, _sigil) = diff_util::diff_status_label_and_char( 1973 &self.path, 1974 &self.source_value, 1975 &self.target_value, 1976 ); 1977 label 1978 } 1979 1980 fn into_source_entry(self) -> TreeEntry { 1981 TreeEntry { 1982 path: self.path.source.map_or(self.path.target, |(path, _)| path), 1983 value: self.source_value, 1984 } 1985 } 1986 1987 fn into_target_entry(self) -> TreeEntry { 1988 TreeEntry { 1989 path: self.path.target, 1990 value: self.target_value, 1991 } 1992 } 1993} 1994 1995fn builtin_tree_diff_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeDiffEntry> 1996{ 1997 type L<'repo> = CommitTemplateLanguage<'repo>; 1998 // Not using maplit::hashmap!{} or custom declarative macro here because 1999 // code completion inside macro is quite restricted. 2000 let mut map = CommitTemplateBuildMethodFnMap::<TreeDiffEntry>::new(); 2001 map.insert( 2002 "path", 2003 |_language, _diagnostics, _build_ctx, self_property, function| { 2004 function.expect_no_arguments()?; 2005 let out_property = self_property.map(|entry| entry.path.target); 2006 Ok(L::wrap_repo_path(out_property)) 2007 }, 2008 ); 2009 map.insert( 2010 "status", 2011 |_language, _diagnostics, _build_ctx, self_property, function| { 2012 function.expect_no_arguments()?; 2013 let out_property = self_property.map(|entry| entry.status_label().to_owned()); 2014 Ok(L::wrap_string(out_property)) 2015 }, 2016 ); 2017 // TODO: add status_code() or status_char()? 2018 map.insert( 2019 "source", 2020 |_language, _diagnostics, _build_ctx, self_property, function| { 2021 function.expect_no_arguments()?; 2022 let out_property = self_property.map(TreeDiffEntry::into_source_entry); 2023 Ok(L::wrap_tree_entry(out_property)) 2024 }, 2025 ); 2026 map.insert( 2027 "target", 2028 |_language, _diagnostics, _build_ctx, self_property, function| { 2029 function.expect_no_arguments()?; 2030 let out_property = self_property.map(TreeDiffEntry::into_target_entry); 2031 Ok(L::wrap_tree_entry(out_property)) 2032 }, 2033 ); 2034 map 2035} 2036 2037/// [`MergedTree`] entry. 2038#[derive(Clone, Debug)] 2039pub struct TreeEntry { 2040 pub path: RepoPathBuf, 2041 pub value: MergedTreeValue, 2042} 2043 2044fn builtin_tree_entry_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, TreeEntry> { 2045 type L<'repo> = CommitTemplateLanguage<'repo>; 2046 // Not using maplit::hashmap!{} or custom declarative macro here because 2047 // code completion inside macro is quite restricted. 2048 let mut map = CommitTemplateBuildMethodFnMap::<TreeEntry>::new(); 2049 map.insert( 2050 "path", 2051 |_language, _diagnostics, _build_ctx, self_property, function| { 2052 function.expect_no_arguments()?; 2053 let out_property = self_property.map(|entry| entry.path); 2054 Ok(L::wrap_repo_path(out_property)) 2055 }, 2056 ); 2057 map.insert( 2058 "conflict", 2059 |_language, _diagnostics, _build_ctx, self_property, function| { 2060 function.expect_no_arguments()?; 2061 let out_property = self_property.map(|entry| !entry.value.is_resolved()); 2062 Ok(L::wrap_boolean(out_property)) 2063 }, 2064 ); 2065 map.insert( 2066 "file_type", 2067 |_language, _diagnostics, _build_ctx, self_property, function| { 2068 function.expect_no_arguments()?; 2069 let out_property = 2070 self_property.map(|entry| describe_file_type(&entry.value).to_owned()); 2071 Ok(L::wrap_string(out_property)) 2072 }, 2073 ); 2074 map.insert( 2075 "executable", 2076 |_language, _diagnostics, _build_ctx, self_property, function| { 2077 function.expect_no_arguments()?; 2078 let out_property = 2079 self_property.map(|entry| is_executable_file(&entry.value).unwrap_or_default()); 2080 Ok(L::wrap_boolean(out_property)) 2081 }, 2082 ); 2083 map 2084} 2085 2086fn describe_file_type(value: &MergedTreeValue) -> &'static str { 2087 match value.as_resolved() { 2088 Some(Some(TreeValue::File { .. })) => "file", 2089 Some(Some(TreeValue::Symlink(_))) => "symlink", 2090 Some(Some(TreeValue::Tree(_))) => "tree", 2091 Some(Some(TreeValue::GitSubmodule(_))) => "git-submodule", 2092 Some(None) => "", // absent 2093 None | Some(Some(TreeValue::Conflict(_))) => "conflict", 2094 } 2095} 2096 2097fn is_executable_file(value: &MergedTreeValue) -> Option<bool> { 2098 let executable = value.to_executable_merge()?; 2099 executable.resolve_trivial().copied() 2100} 2101 2102/// [`DiffStats`] with rendering parameters. 2103#[derive(Clone, Debug)] 2104pub struct DiffStatsFormatted<'a> { 2105 stats: DiffStats, 2106 path_converter: &'a RepoPathUiConverter, 2107 width: usize, 2108} 2109 2110impl Template for DiffStatsFormatted<'_> { 2111 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 2112 diff_util::show_diff_stats( 2113 formatter.as_mut(), 2114 &self.stats, 2115 self.path_converter, 2116 self.width, 2117 ) 2118 } 2119} 2120 2121fn builtin_diff_stats_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, DiffStats> { 2122 type L<'repo> = CommitTemplateLanguage<'repo>; 2123 // Not using maplit::hashmap!{} or custom declarative macro here because 2124 // code completion inside macro is quite restricted. 2125 let mut map = CommitTemplateBuildMethodFnMap::<DiffStats>::new(); 2126 // TODO: add files() -> List<DiffStatEntry> ? 2127 map.insert( 2128 "total_added", 2129 |_language, _diagnostics, _build_ctx, self_property, function| { 2130 function.expect_no_arguments()?; 2131 let out_property = 2132 self_property.and_then(|stats| Ok(stats.count_total_added().try_into()?)); 2133 Ok(L::wrap_integer(out_property)) 2134 }, 2135 ); 2136 map.insert( 2137 "total_removed", 2138 |_language, _diagnostics, _build_ctx, self_property, function| { 2139 function.expect_no_arguments()?; 2140 let out_property = 2141 self_property.and_then(|stats| Ok(stats.count_total_removed().try_into()?)); 2142 Ok(L::wrap_integer(out_property)) 2143 }, 2144 ); 2145 map 2146} 2147 2148#[derive(Debug)] 2149pub struct CryptographicSignature { 2150 commit: Commit, 2151} 2152 2153impl CryptographicSignature { 2154 fn new(commit: Commit) -> Option<Self> { 2155 commit.is_signed().then_some(Self { commit }) 2156 } 2157 2158 fn verify(&self) -> SignResult<Verification> { 2159 self.commit 2160 .verification() 2161 .transpose() 2162 .expect("must have signature") 2163 } 2164 2165 fn status(&self) -> SignResult<SigStatus> { 2166 self.verify().map(|verification| verification.status) 2167 } 2168 2169 /// Defaults to empty string if key is not present. 2170 fn key(&self) -> SignResult<String> { 2171 self.verify() 2172 .map(|verification| verification.key.unwrap_or_default()) 2173 } 2174 2175 /// Defaults to empty string if display is not present. 2176 fn display(&self) -> SignResult<String> { 2177 self.verify() 2178 .map(|verification| verification.display.unwrap_or_default()) 2179 } 2180} 2181 2182pub fn builtin_cryptographic_signature_methods<'repo>( 2183) -> CommitTemplateBuildMethodFnMap<'repo, CryptographicSignature> { 2184 type L<'repo> = CommitTemplateLanguage<'repo>; 2185 // Not using maplit::hashmap!{} or custom declarative macro here because 2186 // code completion inside macro is quite restricted. 2187 let mut map = CommitTemplateBuildMethodFnMap::<CryptographicSignature>::new(); 2188 map.insert( 2189 "status", 2190 |_language, _diagnostics, _build_ctx, self_property, function| { 2191 function.expect_no_arguments()?; 2192 let out_property = self_property.and_then(|sig| match sig.status() { 2193 Ok(status) => Ok(status.to_string()), 2194 Err(SignError::InvalidSignatureFormat) => Ok("invalid".to_string()), 2195 Err(err) => Err(err.into()), 2196 }); 2197 Ok(L::wrap_string(out_property)) 2198 }, 2199 ); 2200 map.insert( 2201 "key", 2202 |_language, _diagnostics, _build_ctx, self_property, function| { 2203 function.expect_no_arguments()?; 2204 let out_property = self_property.and_then(|sig| Ok(sig.key()?)); 2205 Ok(L::wrap_string(out_property)) 2206 }, 2207 ); 2208 map.insert( 2209 "display", 2210 |_language, _diagnostics, _build_ctx, self_property, function| { 2211 function.expect_no_arguments()?; 2212 let out_property = self_property.and_then(|sig| Ok(sig.display()?)); 2213 Ok(L::wrap_string(out_property)) 2214 }, 2215 ); 2216 map 2217} 2218 2219#[derive(Debug, Clone)] 2220pub struct AnnotationLine { 2221 pub commit: Commit, 2222 pub content: BString, 2223 pub line_number: usize, 2224 pub first_line_in_hunk: bool, 2225} 2226 2227pub fn builtin_annotation_line_methods<'repo>( 2228) -> CommitTemplateBuildMethodFnMap<'repo, AnnotationLine> { 2229 type L<'repo> = CommitTemplateLanguage<'repo>; 2230 let mut map = CommitTemplateBuildMethodFnMap::<AnnotationLine>::new(); 2231 map.insert( 2232 "commit", 2233 |_language, _diagnostics, _build_ctx, self_property, function| { 2234 function.expect_no_arguments()?; 2235 let out_property = self_property.map(|line| line.commit); 2236 Ok(L::wrap_commit(out_property)) 2237 }, 2238 ); 2239 map.insert( 2240 "content", 2241 |_language, _diagnostics, _build_ctx, self_property, function| { 2242 function.expect_no_arguments()?; 2243 let out_property = self_property.map(|line| line.content); 2244 // TODO: Add Bytes or BString template type? 2245 Ok(L::wrap_template(out_property.into_template())) 2246 }, 2247 ); 2248 map.insert( 2249 "line_number", 2250 |_language, _diagnostics, _build_ctx, self_property, function| { 2251 function.expect_no_arguments()?; 2252 let out_property = self_property.and_then(|line| Ok(line.line_number.try_into()?)); 2253 Ok(L::wrap_integer(out_property)) 2254 }, 2255 ); 2256 map.insert( 2257 "first_line_in_hunk", 2258 |_language, _diagnostics, _build_ctx, self_property, function| { 2259 function.expect_no_arguments()?; 2260 let out_property = self_property.map(|line| line.first_line_in_hunk); 2261 Ok(L::wrap_boolean(out_property)) 2262 }, 2263 ); 2264 map 2265}