just playing with tangled
at diffedit3 1082 lines 42 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::collections::HashMap; 18use std::io; 19use std::rc::Rc; 20 21use itertools::Itertools as _; 22use jj_lib::backend::{ChangeId, CommitId}; 23use jj_lib::commit::Commit; 24use jj_lib::extensions_map::ExtensionsMap; 25use jj_lib::hex_util::to_reverse_hex; 26use jj_lib::id_prefix::IdPrefixContext; 27use jj_lib::object_id::ObjectId as _; 28use jj_lib::op_store::{RefTarget, WorkspaceId}; 29use jj_lib::repo::Repo; 30use jj_lib::revset::{self, Revset, RevsetExpression, RevsetParseContext}; 31use jj_lib::{git, rewrite}; 32use once_cell::unsync::OnceCell; 33 34use crate::template_builder::{ 35 self, merge_fn_map, BuildContext, CoreTemplateBuildFnTable, CoreTemplatePropertyKind, 36 IntoTemplateProperty, TemplateBuildMethodFnMap, TemplateLanguage, 37}; 38use crate::template_parser::{self, FunctionCallNode, TemplateParseError, TemplateParseResult}; 39use crate::templater::{ 40 self, IntoTemplate, PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty, 41 TemplatePropertyError, TemplatePropertyExt as _, 42}; 43use crate::{revset_util, text_util}; 44 45pub trait CommitTemplateLanguageExtension { 46 fn build_fn_table<'repo>(&self) -> CommitTemplateBuildFnTable<'repo>; 47 48 fn build_cache_extensions(&self, extensions: &mut ExtensionsMap); 49} 50 51pub struct CommitTemplateLanguage<'repo> { 52 repo: &'repo dyn Repo, 53 workspace_id: WorkspaceId, 54 // RevsetParseContext doesn't borrow a repo, but we'll need 'repo lifetime 55 // anyway to capture it to evaluate dynamically-constructed user expression 56 // such as `revset("ancestors(" ++ commit_id ++ ")")`. 57 // TODO: Maybe refactor context structs? WorkspaceId is contained in 58 // RevsetParseContext for example. 59 revset_parse_context: RevsetParseContext<'repo>, 60 id_prefix_context: &'repo IdPrefixContext, 61 build_fn_table: CommitTemplateBuildFnTable<'repo>, 62 keyword_cache: CommitKeywordCache<'repo>, 63 cache_extensions: ExtensionsMap, 64} 65 66impl<'repo> CommitTemplateLanguage<'repo> { 67 /// Sets up environment where commit template will be transformed to 68 /// evaluation tree. 69 pub fn new( 70 repo: &'repo dyn Repo, 71 workspace_id: &WorkspaceId, 72 revset_parse_context: RevsetParseContext<'repo>, 73 id_prefix_context: &'repo IdPrefixContext, 74 extensions: &[impl AsRef<dyn CommitTemplateLanguageExtension>], 75 ) -> Self { 76 let mut build_fn_table = CommitTemplateBuildFnTable::builtin(); 77 let mut cache_extensions = ExtensionsMap::empty(); 78 79 for extension in extensions { 80 build_fn_table.merge(extension.as_ref().build_fn_table()); 81 extension 82 .as_ref() 83 .build_cache_extensions(&mut cache_extensions); 84 } 85 86 CommitTemplateLanguage { 87 repo, 88 workspace_id: workspace_id.clone(), 89 revset_parse_context, 90 id_prefix_context, 91 build_fn_table, 92 keyword_cache: CommitKeywordCache::default(), 93 cache_extensions, 94 } 95 } 96} 97 98impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { 99 type Property = CommitTemplatePropertyKind<'repo>; 100 101 template_builder::impl_core_wrap_property_fns!('repo, CommitTemplatePropertyKind::Core); 102 103 fn build_function( 104 &self, 105 build_ctx: &BuildContext<Self::Property>, 106 function: &FunctionCallNode, 107 ) -> TemplateParseResult<Self::Property> { 108 let table = &self.build_fn_table.core; 109 table.build_function(self, build_ctx, function) 110 } 111 112 fn build_method( 113 &self, 114 build_ctx: &BuildContext<Self::Property>, 115 property: Self::Property, 116 function: &FunctionCallNode, 117 ) -> TemplateParseResult<Self::Property> { 118 let type_name = property.type_name(); 119 match property { 120 CommitTemplatePropertyKind::Core(property) => { 121 let table = &self.build_fn_table.core; 122 table.build_method(self, build_ctx, property, function) 123 } 124 CommitTemplatePropertyKind::Commit(property) => { 125 let table = &self.build_fn_table.commit_methods; 126 let build = template_parser::lookup_method(type_name, table, function)?; 127 build(self, build_ctx, property, function) 128 } 129 CommitTemplatePropertyKind::CommitOpt(property) => { 130 let type_name = "Commit"; 131 let table = &self.build_fn_table.commit_methods; 132 let build = template_parser::lookup_method(type_name, table, function)?; 133 let inner_property = property.and_then(|opt| { 134 opt.ok_or_else(|| TemplatePropertyError("No commit available".into())) 135 }); 136 build(self, build_ctx, Box::new(inner_property), function) 137 } 138 CommitTemplatePropertyKind::CommitList(property) => { 139 // TODO: migrate to table? 140 template_builder::build_unformattable_list_method( 141 self, 142 build_ctx, 143 property, 144 function, 145 Self::wrap_commit, 146 ) 147 } 148 CommitTemplatePropertyKind::RefName(property) => { 149 let table = &self.build_fn_table.ref_name_methods; 150 let build = template_parser::lookup_method(type_name, table, function)?; 151 build(self, build_ctx, property, function) 152 } 153 CommitTemplatePropertyKind::RefNameOpt(property) => { 154 let type_name = "RefName"; 155 let table = &self.build_fn_table.ref_name_methods; 156 let build = template_parser::lookup_method(type_name, table, function)?; 157 let inner_property = property.and_then(|opt| { 158 opt.ok_or_else(|| TemplatePropertyError("No RefName available".into())) 159 }); 160 build(self, build_ctx, Box::new(inner_property), function) 161 } 162 CommitTemplatePropertyKind::RefNameList(property) => { 163 // TODO: migrate to table? 164 template_builder::build_formattable_list_method( 165 self, 166 build_ctx, 167 property, 168 function, 169 Self::wrap_ref_name, 170 ) 171 } 172 CommitTemplatePropertyKind::CommitOrChangeId(property) => { 173 let table = &self.build_fn_table.commit_or_change_id_methods; 174 let build = template_parser::lookup_method(type_name, table, function)?; 175 build(self, build_ctx, property, function) 176 } 177 CommitTemplatePropertyKind::ShortestIdPrefix(property) => { 178 let table = &self.build_fn_table.shortest_id_prefix_methods; 179 let build = template_parser::lookup_method(type_name, table, function)?; 180 build(self, build_ctx, property, function) 181 } 182 } 183 } 184} 185 186// If we need to add multiple languages that support Commit types, this can be 187// turned into a trait which extends TemplateLanguage. 188impl<'repo> CommitTemplateLanguage<'repo> { 189 pub fn repo(&self) -> &'repo dyn Repo { 190 self.repo 191 } 192 193 pub fn workspace_id(&self) -> &WorkspaceId { 194 &self.workspace_id 195 } 196 197 pub fn keyword_cache(&self) -> &CommitKeywordCache<'repo> { 198 &self.keyword_cache 199 } 200 201 pub fn cache_extension<T: Any>(&self) -> Option<&T> { 202 self.cache_extensions.get::<T>() 203 } 204 205 pub fn wrap_commit( 206 property: impl TemplateProperty<Output = Commit> + 'repo, 207 ) -> CommitTemplatePropertyKind<'repo> { 208 CommitTemplatePropertyKind::Commit(Box::new(property)) 209 } 210 211 pub fn wrap_commit_opt( 212 property: impl TemplateProperty<Output = Option<Commit>> + 'repo, 213 ) -> CommitTemplatePropertyKind<'repo> { 214 CommitTemplatePropertyKind::CommitOpt(Box::new(property)) 215 } 216 217 pub fn wrap_commit_list( 218 property: impl TemplateProperty<Output = Vec<Commit>> + 'repo, 219 ) -> CommitTemplatePropertyKind<'repo> { 220 CommitTemplatePropertyKind::CommitList(Box::new(property)) 221 } 222 223 pub fn wrap_ref_name( 224 property: impl TemplateProperty<Output = RefName> + 'repo, 225 ) -> CommitTemplatePropertyKind<'repo> { 226 CommitTemplatePropertyKind::RefName(Box::new(property)) 227 } 228 229 pub fn wrap_ref_name_opt( 230 property: impl TemplateProperty<Output = Option<RefName>> + 'repo, 231 ) -> CommitTemplatePropertyKind<'repo> { 232 CommitTemplatePropertyKind::RefNameOpt(Box::new(property)) 233 } 234 235 pub fn wrap_ref_name_list( 236 property: impl TemplateProperty<Output = Vec<RefName>> + 'repo, 237 ) -> CommitTemplatePropertyKind<'repo> { 238 CommitTemplatePropertyKind::RefNameList(Box::new(property)) 239 } 240 241 pub fn wrap_commit_or_change_id( 242 property: impl TemplateProperty<Output = CommitOrChangeId> + 'repo, 243 ) -> CommitTemplatePropertyKind<'repo> { 244 CommitTemplatePropertyKind::CommitOrChangeId(Box::new(property)) 245 } 246 247 pub fn wrap_shortest_id_prefix( 248 property: impl TemplateProperty<Output = ShortestIdPrefix> + 'repo, 249 ) -> CommitTemplatePropertyKind<'repo> { 250 CommitTemplatePropertyKind::ShortestIdPrefix(Box::new(property)) 251 } 252} 253 254pub enum CommitTemplatePropertyKind<'repo> { 255 Core(CoreTemplatePropertyKind<'repo>), 256 Commit(Box<dyn TemplateProperty<Output = Commit> + 'repo>), 257 CommitOpt(Box<dyn TemplateProperty<Output = Option<Commit>> + 'repo>), 258 CommitList(Box<dyn TemplateProperty<Output = Vec<Commit>> + 'repo>), 259 RefName(Box<dyn TemplateProperty<Output = RefName> + 'repo>), 260 RefNameOpt(Box<dyn TemplateProperty<Output = Option<RefName>> + 'repo>), 261 RefNameList(Box<dyn TemplateProperty<Output = Vec<RefName>> + 'repo>), 262 CommitOrChangeId(Box<dyn TemplateProperty<Output = CommitOrChangeId> + 'repo>), 263 ShortestIdPrefix(Box<dyn TemplateProperty<Output = ShortestIdPrefix> + 'repo>), 264} 265 266impl<'repo> IntoTemplateProperty<'repo> for CommitTemplatePropertyKind<'repo> { 267 fn type_name(&self) -> &'static str { 268 match self { 269 CommitTemplatePropertyKind::Core(property) => property.type_name(), 270 CommitTemplatePropertyKind::Commit(_) => "Commit", 271 CommitTemplatePropertyKind::CommitOpt(_) => "Option<Commit>", 272 CommitTemplatePropertyKind::CommitList(_) => "List<Commit>", 273 CommitTemplatePropertyKind::RefName(_) => "RefName", 274 CommitTemplatePropertyKind::RefNameOpt(_) => "Option<RefName>", 275 CommitTemplatePropertyKind::RefNameList(_) => "List<RefName>", 276 CommitTemplatePropertyKind::CommitOrChangeId(_) => "CommitOrChangeId", 277 CommitTemplatePropertyKind::ShortestIdPrefix(_) => "ShortestIdPrefix", 278 } 279 } 280 281 fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Output = bool> + 'repo>> { 282 match self { 283 CommitTemplatePropertyKind::Core(property) => property.try_into_boolean(), 284 CommitTemplatePropertyKind::Commit(_) => None, 285 CommitTemplatePropertyKind::CommitOpt(property) => { 286 Some(Box::new(property.map(|opt| opt.is_some()))) 287 } 288 CommitTemplatePropertyKind::CommitList(property) => { 289 Some(Box::new(property.map(|l| !l.is_empty()))) 290 } 291 CommitTemplatePropertyKind::RefName(_) => None, 292 CommitTemplatePropertyKind::RefNameOpt(property) => { 293 Some(Box::new(property.map(|opt| opt.is_some()))) 294 } 295 CommitTemplatePropertyKind::RefNameList(property) => { 296 Some(Box::new(property.map(|l| !l.is_empty()))) 297 } 298 CommitTemplatePropertyKind::CommitOrChangeId(_) => None, 299 CommitTemplatePropertyKind::ShortestIdPrefix(_) => None, 300 } 301 } 302 303 fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Output = i64> + 'repo>> { 304 match self { 305 CommitTemplatePropertyKind::Core(property) => property.try_into_integer(), 306 _ => None, 307 } 308 } 309 310 fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<Output = String> + 'repo>> { 311 match self { 312 CommitTemplatePropertyKind::Core(property) => property.try_into_plain_text(), 313 _ => { 314 let template = self.try_into_template()?; 315 Some(Box::new(PlainTextFormattedProperty::new(template))) 316 } 317 } 318 } 319 320 fn try_into_template(self) -> Option<Box<dyn Template + 'repo>> { 321 match self { 322 CommitTemplatePropertyKind::Core(property) => property.try_into_template(), 323 CommitTemplatePropertyKind::Commit(_) => None, 324 CommitTemplatePropertyKind::CommitOpt(_) => None, 325 CommitTemplatePropertyKind::CommitList(_) => None, 326 CommitTemplatePropertyKind::RefName(property) => Some(property.into_template()), 327 CommitTemplatePropertyKind::RefNameOpt(property) => Some(property.into_template()), 328 CommitTemplatePropertyKind::RefNameList(property) => Some(property.into_template()), 329 CommitTemplatePropertyKind::CommitOrChangeId(property) => { 330 Some(property.into_template()) 331 } 332 CommitTemplatePropertyKind::ShortestIdPrefix(property) => { 333 Some(property.into_template()) 334 } 335 } 336 } 337} 338 339/// Table of functions that translate method call node of self type `T`. 340pub type CommitTemplateBuildMethodFnMap<'repo, T> = 341 TemplateBuildMethodFnMap<'repo, CommitTemplateLanguage<'repo>, T>; 342 343/// Symbol table of methods available in the commit template. 344pub struct CommitTemplateBuildFnTable<'repo> { 345 pub core: CoreTemplateBuildFnTable<'repo, CommitTemplateLanguage<'repo>>, 346 pub commit_methods: CommitTemplateBuildMethodFnMap<'repo, Commit>, 347 pub ref_name_methods: CommitTemplateBuildMethodFnMap<'repo, RefName>, 348 pub commit_or_change_id_methods: CommitTemplateBuildMethodFnMap<'repo, CommitOrChangeId>, 349 pub shortest_id_prefix_methods: CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix>, 350} 351 352impl<'repo> CommitTemplateBuildFnTable<'repo> { 353 /// Creates new symbol table containing the builtin methods. 354 fn builtin() -> Self { 355 CommitTemplateBuildFnTable { 356 core: CoreTemplateBuildFnTable::builtin(), 357 commit_methods: builtin_commit_methods(), 358 ref_name_methods: builtin_ref_name_methods(), 359 commit_or_change_id_methods: builtin_commit_or_change_id_methods(), 360 shortest_id_prefix_methods: builtin_shortest_id_prefix_methods(), 361 } 362 } 363 364 pub fn empty() -> Self { 365 CommitTemplateBuildFnTable { 366 core: CoreTemplateBuildFnTable::empty(), 367 commit_methods: HashMap::new(), 368 ref_name_methods: HashMap::new(), 369 commit_or_change_id_methods: HashMap::new(), 370 shortest_id_prefix_methods: HashMap::new(), 371 } 372 } 373 374 fn merge(&mut self, extension: CommitTemplateBuildFnTable<'repo>) { 375 let CommitTemplateBuildFnTable { 376 core, 377 commit_methods, 378 ref_name_methods, 379 commit_or_change_id_methods, 380 shortest_id_prefix_methods, 381 } = extension; 382 383 self.core.merge(core); 384 merge_fn_map(&mut self.commit_methods, commit_methods); 385 merge_fn_map(&mut self.ref_name_methods, ref_name_methods); 386 merge_fn_map( 387 &mut self.commit_or_change_id_methods, 388 commit_or_change_id_methods, 389 ); 390 merge_fn_map( 391 &mut self.shortest_id_prefix_methods, 392 shortest_id_prefix_methods, 393 ); 394 } 395} 396 397#[derive(Default)] 398pub struct CommitKeywordCache<'repo> { 399 // Build index lazily, and Rc to get away from &self lifetime. 400 branches_index: OnceCell<Rc<RefNamesIndex>>, 401 tags_index: OnceCell<Rc<RefNamesIndex>>, 402 git_refs_index: OnceCell<Rc<RefNamesIndex>>, 403 is_immutable_fn: OnceCell<Rc<RevsetContainingFn<'repo>>>, 404} 405 406impl<'repo> CommitKeywordCache<'repo> { 407 pub fn branches_index(&self, repo: &dyn Repo) -> &Rc<RefNamesIndex> { 408 self.branches_index 409 .get_or_init(|| Rc::new(build_branches_index(repo))) 410 } 411 412 pub fn tags_index(&self, repo: &dyn Repo) -> &Rc<RefNamesIndex> { 413 self.tags_index 414 .get_or_init(|| Rc::new(build_ref_names_index(repo.view().tags()))) 415 } 416 417 pub fn git_refs_index(&self, repo: &dyn Repo) -> &Rc<RefNamesIndex> { 418 self.git_refs_index 419 .get_or_init(|| Rc::new(build_ref_names_index(repo.view().git_refs()))) 420 } 421 422 pub fn is_immutable_fn( 423 &self, 424 language: &CommitTemplateLanguage<'repo>, 425 span: pest::Span<'_>, 426 ) -> TemplateParseResult<&Rc<RevsetContainingFn<'repo>>> { 427 self.is_immutable_fn.get_or_try_init(|| { 428 let revset = evaluate_immutable_revset(language, span)?; 429 Ok(revset.containing_fn().into()) 430 }) 431 } 432} 433 434fn builtin_commit_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, Commit> { 435 type L<'repo> = CommitTemplateLanguage<'repo>; 436 // Not using maplit::hashmap!{} or custom declarative macro here because 437 // code completion inside macro is quite restricted. 438 let mut map = CommitTemplateBuildMethodFnMap::<Commit>::new(); 439 map.insert( 440 "description", 441 |_language, _build_ctx, self_property, function| { 442 template_parser::expect_no_arguments(function)?; 443 let out_property = 444 self_property.map(|commit| text_util::complete_newline(commit.description())); 445 Ok(L::wrap_string(out_property)) 446 }, 447 ); 448 map.insert( 449 "change_id", 450 |_language, _build_ctx, self_property, function| { 451 template_parser::expect_no_arguments(function)?; 452 let out_property = 453 self_property.map(|commit| CommitOrChangeId::Change(commit.change_id().to_owned())); 454 Ok(L::wrap_commit_or_change_id(out_property)) 455 }, 456 ); 457 map.insert( 458 "commit_id", 459 |_language, _build_ctx, self_property, function| { 460 template_parser::expect_no_arguments(function)?; 461 let out_property = 462 self_property.map(|commit| CommitOrChangeId::Commit(commit.id().to_owned())); 463 Ok(L::wrap_commit_or_change_id(out_property)) 464 }, 465 ); 466 map.insert( 467 "parents", 468 |_language, _build_ctx, self_property, function| { 469 template_parser::expect_no_arguments(function)?; 470 let out_property = self_property.map(|commit| commit.parents()); 471 Ok(L::wrap_commit_list(out_property)) 472 }, 473 ); 474 map.insert( 475 "author", 476 |_language, _build_ctx, self_property, function| { 477 template_parser::expect_no_arguments(function)?; 478 let out_property = self_property.map(|commit| commit.author().clone()); 479 Ok(L::wrap_signature(out_property)) 480 }, 481 ); 482 map.insert( 483 "committer", 484 |_language, _build_ctx, self_property, function| { 485 template_parser::expect_no_arguments(function)?; 486 let out_property = self_property.map(|commit| commit.committer().clone()); 487 Ok(L::wrap_signature(out_property)) 488 }, 489 ); 490 map.insert("mine", |language, _build_ctx, self_property, function| { 491 template_parser::expect_no_arguments(function)?; 492 let user_email = language.revset_parse_context.user_email.clone(); 493 let out_property = self_property.map(move |commit| commit.author().email == user_email); 494 Ok(L::wrap_boolean(out_property)) 495 }); 496 map.insert( 497 "working_copies", 498 |language, _build_ctx, self_property, function| { 499 template_parser::expect_no_arguments(function)?; 500 let repo = language.repo; 501 let out_property = self_property.map(|commit| extract_working_copies(repo, &commit)); 502 Ok(L::wrap_string(out_property)) 503 }, 504 ); 505 map.insert( 506 "current_working_copy", 507 |language, _build_ctx, self_property, function| { 508 template_parser::expect_no_arguments(function)?; 509 let repo = language.repo; 510 let workspace_id = language.workspace_id.clone(); 511 let out_property = self_property.map(move |commit| { 512 Some(commit.id()) == repo.view().get_wc_commit_id(&workspace_id) 513 }); 514 Ok(L::wrap_boolean(out_property)) 515 }, 516 ); 517 map.insert( 518 "branches", 519 |language, _build_ctx, self_property, function| { 520 template_parser::expect_no_arguments(function)?; 521 let index = language.keyword_cache.branches_index(language.repo).clone(); 522 let out_property = self_property.map(move |commit| { 523 index 524 .get(commit.id()) 525 .iter() 526 .filter(|ref_name| ref_name.is_local() || !ref_name.synced) 527 .cloned() 528 .collect() 529 }); 530 Ok(L::wrap_ref_name_list(out_property)) 531 }, 532 ); 533 map.insert( 534 "local_branches", 535 |language, _build_ctx, self_property, function| { 536 template_parser::expect_no_arguments(function)?; 537 let index = language.keyword_cache.branches_index(language.repo).clone(); 538 let out_property = self_property.map(move |commit| { 539 index 540 .get(commit.id()) 541 .iter() 542 .filter(|ref_name| ref_name.is_local()) 543 .cloned() 544 .collect() 545 }); 546 Ok(L::wrap_ref_name_list(out_property)) 547 }, 548 ); 549 map.insert( 550 "remote_branches", 551 |language, _build_ctx, self_property, function| { 552 template_parser::expect_no_arguments(function)?; 553 let index = language.keyword_cache.branches_index(language.repo).clone(); 554 let out_property = self_property.map(move |commit| { 555 index 556 .get(commit.id()) 557 .iter() 558 .filter(|ref_name| ref_name.is_remote()) 559 .cloned() 560 .collect() 561 }); 562 Ok(L::wrap_ref_name_list(out_property)) 563 }, 564 ); 565 map.insert("tags", |language, _build_ctx, self_property, function| { 566 template_parser::expect_no_arguments(function)?; 567 let index = language.keyword_cache.tags_index(language.repo).clone(); 568 let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec()); 569 Ok(L::wrap_ref_name_list(out_property)) 570 }); 571 map.insert( 572 "git_refs", 573 |language, _build_ctx, self_property, function| { 574 template_parser::expect_no_arguments(function)?; 575 let index = language.keyword_cache.git_refs_index(language.repo).clone(); 576 let out_property = self_property.map(move |commit| index.get(commit.id()).to_vec()); 577 Ok(L::wrap_ref_name_list(out_property)) 578 }, 579 ); 580 map.insert( 581 "git_head", 582 |language, _build_ctx, self_property, function| { 583 template_parser::expect_no_arguments(function)?; 584 let repo = language.repo; 585 let out_property = self_property.map(|commit| extract_git_head(repo, &commit)); 586 Ok(L::wrap_ref_name_opt(out_property)) 587 }, 588 ); 589 map.insert( 590 "divergent", 591 |language, _build_ctx, self_property, function| { 592 template_parser::expect_no_arguments(function)?; 593 let repo = language.repo; 594 let out_property = self_property.map(|commit| { 595 // The given commit could be hidden in e.g. obslog. 596 let maybe_entries = repo.resolve_change_id(commit.change_id()); 597 maybe_entries.map_or(0, |entries| entries.len()) > 1 598 }); 599 Ok(L::wrap_boolean(out_property)) 600 }, 601 ); 602 map.insert("hidden", |language, _build_ctx, self_property, function| { 603 template_parser::expect_no_arguments(function)?; 604 let repo = language.repo; 605 let out_property = self_property.map(|commit| { 606 let maybe_entries = repo.resolve_change_id(commit.change_id()); 607 maybe_entries.map_or(true, |entries| !entries.contains(commit.id())) 608 }); 609 Ok(L::wrap_boolean(out_property)) 610 }); 611 map.insert( 612 "immutable", 613 |language, _build_ctx, self_property, function| { 614 template_parser::expect_no_arguments(function)?; 615 let is_immutable = language 616 .keyword_cache 617 .is_immutable_fn(language, function.name_span)? 618 .clone(); 619 let out_property = self_property.map(move |commit| is_immutable(commit.id())); 620 Ok(L::wrap_boolean(out_property)) 621 }, 622 ); 623 map.insert( 624 "contained_in", 625 |language, _build_ctx, self_property, function| { 626 let [revset_node] = template_parser::expect_exact_arguments(function)?; 627 628 let is_contained = 629 template_parser::expect_string_literal_with(revset_node, |revset, span| { 630 Ok(evaluate_user_revset(language, span, revset)?.containing_fn()) 631 })?; 632 633 let out_property = self_property.map(move |commit| is_contained(commit.id())); 634 Ok(L::wrap_boolean(out_property)) 635 }, 636 ); 637 map.insert( 638 "conflict", 639 |_language, _build_ctx, self_property, function| { 640 template_parser::expect_no_arguments(function)?; 641 let out_property = self_property.and_then(|commit| Ok(commit.has_conflict()?)); 642 Ok(L::wrap_boolean(out_property)) 643 }, 644 ); 645 map.insert("empty", |language, _build_ctx, self_property, function| { 646 template_parser::expect_no_arguments(function)?; 647 let repo = language.repo; 648 let out_property = self_property.and_then(|commit| { 649 if let [parent] = &commit.parents()[..] { 650 return Ok(parent.tree_id() == commit.tree_id()); 651 } 652 let parent_tree = rewrite::merge_commit_trees(repo, &commit.parents())?; 653 Ok(*commit.tree_id() == parent_tree.id()) 654 }); 655 Ok(L::wrap_boolean(out_property)) 656 }); 657 map.insert("root", |language, _build_ctx, self_property, function| { 658 template_parser::expect_no_arguments(function)?; 659 let repo = language.repo; 660 let out_property = self_property.map(|commit| commit.id() == repo.store().root_commit_id()); 661 Ok(L::wrap_boolean(out_property)) 662 }); 663 map 664} 665 666// TODO: return Vec<String> 667fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String { 668 let wc_commit_ids = repo.view().wc_commit_ids(); 669 if wc_commit_ids.len() <= 1 { 670 return "".to_string(); 671 } 672 let mut names = vec![]; 673 for (workspace_id, wc_commit_id) in wc_commit_ids.iter().sorted() { 674 if wc_commit_id == commit.id() { 675 names.push(format!("{}@", workspace_id.as_str())); 676 } 677 } 678 names.join(" ") 679} 680 681type RevsetContainingFn<'repo> = dyn Fn(&CommitId) -> bool + 'repo; 682 683fn evaluate_revset_expression<'repo>( 684 language: &CommitTemplateLanguage<'repo>, 685 span: pest::Span<'_>, 686 expression: Rc<RevsetExpression>, 687) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> { 688 let symbol_resolver = revset_util::default_symbol_resolver( 689 language.repo, 690 language.revset_parse_context.extensions.symbol_resolvers(), 691 language.id_prefix_context, 692 ); 693 let revset = 694 revset_util::evaluate(language.repo, &symbol_resolver, expression).map_err(|err| { 695 TemplateParseError::expression("Failed to evaluate revset", span).with_source(err) 696 })?; 697 Ok(revset) 698} 699 700fn evaluate_immutable_revset<'repo>( 701 language: &CommitTemplateLanguage<'repo>, 702 span: pest::Span<'_>, 703) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> { 704 // Alternatively, a negated (i.e. visible mutable) set could be computed. 705 // It's usually smaller than the immutable set. The revset engine can also 706 // optimize "::<recent_heads>" query to use bitset-based implementation. 707 let expression = revset_util::parse_immutable_expression(&language.revset_parse_context) 708 .map_err(|err| { 709 TemplateParseError::expression("Failed to parse revset", span).with_source(err) 710 })?; 711 712 evaluate_revset_expression(language, span, expression) 713} 714 715fn evaluate_user_revset<'repo>( 716 language: &CommitTemplateLanguage<'repo>, 717 span: pest::Span<'_>, 718 revset: &str, 719) -> Result<Box<dyn Revset + 'repo>, TemplateParseError> { 720 let expression = revset::parse(revset, &language.revset_parse_context).map_err(|err| { 721 TemplateParseError::expression("Failed to parse revset", span).with_source(err) 722 })?; 723 724 evaluate_revset_expression(language, span, expression) 725} 726 727/// Branch or tag name with metadata. 728#[derive(Clone, Debug)] 729pub struct RefName { 730 /// Local name. 731 name: String, 732 /// Remote name if this is a remote or Git-tracking ref. 733 remote: Option<String>, 734 /// Target commit ids. 735 target: RefTarget, 736 /// Local ref is synchronized with all tracking remotes, or tracking remote 737 /// ref is synchronized with the local. 738 synced: bool, 739} 740 741impl RefName { 742 /// Creates local ref representation which doesn't track any remote refs. 743 pub fn local_only(name: impl Into<String>, target: RefTarget) -> Self { 744 RefName { 745 name: name.into(), 746 remote: None, 747 target, 748 synced: true, // has no tracking remotes 749 } 750 } 751 752 fn is_local(&self) -> bool { 753 self.remote.is_none() 754 } 755 756 fn is_remote(&self) -> bool { 757 self.remote.is_some() 758 } 759 760 fn is_present(&self) -> bool { 761 self.target.is_present() 762 } 763 764 /// Whether the ref target has conflicts. 765 fn has_conflict(&self) -> bool { 766 self.target.has_conflict() 767 } 768} 769 770impl Template for RefName { 771 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 772 write!(formatter.labeled("name"), "{}", self.name)?; 773 if let Some(remote) = &self.remote { 774 write!(formatter, "@")?; 775 write!(formatter.labeled("remote"), "{remote}")?; 776 } 777 // Don't show both conflict and unsynced sigils as conflicted ref wouldn't 778 // be pushed. 779 if self.has_conflict() { 780 write!(formatter, "??")?; 781 } else if self.is_local() && !self.synced { 782 write!(formatter, "*")?; 783 } 784 Ok(()) 785 } 786} 787 788impl Template for Vec<RefName> { 789 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 790 templater::format_joined(formatter, self, " ") 791 } 792} 793 794fn builtin_ref_name_methods<'repo>() -> CommitTemplateBuildMethodFnMap<'repo, RefName> { 795 type L<'repo> = CommitTemplateLanguage<'repo>; 796 // Not using maplit::hashmap!{} or custom declarative macro here because 797 // code completion inside macro is quite restricted. 798 let mut map = CommitTemplateBuildMethodFnMap::<RefName>::new(); 799 map.insert("name", |_language, _build_ctx, self_property, function| { 800 template_parser::expect_no_arguments(function)?; 801 let out_property = self_property.map(|ref_name| ref_name.name); 802 Ok(L::wrap_string(out_property)) 803 }); 804 map.insert( 805 "remote", 806 |_language, _build_ctx, self_property, function| { 807 template_parser::expect_no_arguments(function)?; 808 let out_property = self_property.map(|ref_name| ref_name.remote.unwrap_or_default()); 809 Ok(L::wrap_string(out_property)) 810 }, 811 ); 812 map.insert( 813 "present", 814 |_language, _build_ctx, self_property, function| { 815 template_parser::expect_no_arguments(function)?; 816 let out_property = self_property.map(|ref_name| ref_name.is_present()); 817 Ok(L::wrap_boolean(out_property)) 818 }, 819 ); 820 map.insert( 821 "conflict", 822 |_language, _build_ctx, self_property, function| { 823 template_parser::expect_no_arguments(function)?; 824 let out_property = self_property.map(|ref_name| ref_name.has_conflict()); 825 Ok(L::wrap_boolean(out_property)) 826 }, 827 ); 828 map.insert( 829 "normal_target", 830 |language, _build_ctx, self_property, function| { 831 template_parser::expect_no_arguments(function)?; 832 let repo = language.repo; 833 let out_property = self_property.and_then(|ref_name| { 834 let maybe_id = ref_name.target.as_normal(); 835 Ok(maybe_id.map(|id| repo.store().get_commit(id)).transpose()?) 836 }); 837 Ok(L::wrap_commit_opt(out_property)) 838 }, 839 ); 840 map.insert( 841 "removed_targets", 842 |language, _build_ctx, self_property, function| { 843 template_parser::expect_no_arguments(function)?; 844 let repo = language.repo; 845 let out_property = self_property.and_then(|ref_name| { 846 let ids = ref_name.target.removed_ids(); 847 Ok(ids.map(|id| repo.store().get_commit(id)).try_collect()?) 848 }); 849 Ok(L::wrap_commit_list(out_property)) 850 }, 851 ); 852 map.insert( 853 "added_targets", 854 |language, _build_ctx, self_property, function| { 855 template_parser::expect_no_arguments(function)?; 856 let repo = language.repo; 857 let out_property = self_property.and_then(|ref_name| { 858 let ids = ref_name.target.added_ids(); 859 Ok(ids.map(|id| repo.store().get_commit(id)).try_collect()?) 860 }); 861 Ok(L::wrap_commit_list(out_property)) 862 }, 863 ); 864 map 865} 866 867/// Cache for reverse lookup refs. 868#[derive(Clone, Debug, Default)] 869pub struct RefNamesIndex { 870 index: HashMap<CommitId, Vec<RefName>>, 871} 872 873impl RefNamesIndex { 874 fn insert<'a>(&mut self, ids: impl IntoIterator<Item = &'a CommitId>, name: RefName) { 875 for id in ids { 876 let ref_names = self.index.entry(id.clone()).or_default(); 877 ref_names.push(name.clone()); 878 } 879 } 880 881 #[allow(unknown_lints)] // XXX FIXME (aseipp): nightly bogons; re-test this occasionally 882 #[allow(clippy::manual_unwrap_or_default)] 883 pub fn get(&self, id: &CommitId) -> &[RefName] { 884 if let Some(names) = self.index.get(id) { 885 names 886 } else { 887 &[] 888 } 889 } 890} 891 892fn build_branches_index(repo: &dyn Repo) -> RefNamesIndex { 893 let mut index = RefNamesIndex::default(); 894 for (branch_name, branch_target) in repo.view().branches() { 895 let local_target = branch_target.local_target; 896 let remote_refs = branch_target.remote_refs; 897 if local_target.is_present() { 898 let ref_name = RefName { 899 name: branch_name.to_owned(), 900 remote: None, 901 target: local_target.clone(), 902 synced: remote_refs.iter().all(|&(_, remote_ref)| { 903 !remote_ref.is_tracking() || remote_ref.target == *local_target 904 }), 905 }; 906 index.insert(local_target.added_ids(), ref_name); 907 } 908 for &(remote_name, remote_ref) in &remote_refs { 909 let ref_name = RefName { 910 name: branch_name.to_owned(), 911 remote: Some(remote_name.to_owned()), 912 target: remote_ref.target.clone(), 913 synced: remote_ref.is_tracking() && remote_ref.target == *local_target, 914 }; 915 index.insert(remote_ref.target.added_ids(), ref_name); 916 } 917 } 918 index 919} 920 921fn build_ref_names_index<'a>( 922 ref_pairs: impl IntoIterator<Item = (&'a String, &'a RefTarget)>, 923) -> RefNamesIndex { 924 let mut index = RefNamesIndex::default(); 925 for (name, target) in ref_pairs { 926 let ref_name = RefName::local_only(name, target.clone()); 927 index.insert(target.added_ids(), ref_name); 928 } 929 index 930} 931 932fn extract_git_head(repo: &dyn Repo, commit: &Commit) -> Option<RefName> { 933 let target = repo.view().git_head(); 934 target.added_ids().contains(commit.id()).then(|| { 935 RefName { 936 name: "HEAD".to_owned(), 937 remote: Some(git::REMOTE_NAME_FOR_LOCAL_GIT_REPO.to_owned()), 938 target: target.clone(), 939 synced: false, // has no local counterpart 940 } 941 }) 942} 943 944#[derive(Clone, Debug, Eq, PartialEq)] 945pub enum CommitOrChangeId { 946 Commit(CommitId), 947 Change(ChangeId), 948} 949 950impl CommitOrChangeId { 951 pub fn hex(&self) -> String { 952 match self { 953 CommitOrChangeId::Commit(id) => id.hex(), 954 CommitOrChangeId::Change(id) => { 955 // TODO: We can avoid the unwrap() and make this more efficient by converting 956 // straight from bytes. 957 to_reverse_hex(&id.hex()).unwrap() 958 } 959 } 960 } 961 962 pub fn short(&self, total_len: usize) -> String { 963 let mut hex = self.hex(); 964 hex.truncate(total_len); 965 hex 966 } 967 968 /// The length of the id printed will be the maximum of `total_len` and the 969 /// length of the shortest unique prefix 970 pub fn shortest( 971 &self, 972 repo: &dyn Repo, 973 id_prefix_context: &IdPrefixContext, 974 total_len: usize, 975 ) -> ShortestIdPrefix { 976 let mut hex = self.hex(); 977 let prefix_len = match self { 978 CommitOrChangeId::Commit(id) => id_prefix_context.shortest_commit_prefix_len(repo, id), 979 CommitOrChangeId::Change(id) => id_prefix_context.shortest_change_prefix_len(repo, id), 980 }; 981 hex.truncate(max(prefix_len, total_len)); 982 let rest = hex.split_off(prefix_len); 983 ShortestIdPrefix { prefix: hex, rest } 984 } 985} 986 987impl Template for CommitOrChangeId { 988 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 989 write!(formatter, "{}", self.hex()) 990 } 991} 992 993fn builtin_commit_or_change_id_methods<'repo>( 994) -> CommitTemplateBuildMethodFnMap<'repo, CommitOrChangeId> { 995 type L<'repo> = CommitTemplateLanguage<'repo>; 996 // Not using maplit::hashmap!{} or custom declarative macro here because 997 // code completion inside macro is quite restricted. 998 let mut map = CommitTemplateBuildMethodFnMap::<CommitOrChangeId>::new(); 999 map.insert("short", |language, build_ctx, self_property, function| { 1000 let ([], [len_node]) = template_parser::expect_arguments(function)?; 1001 let len_property = len_node 1002 .map(|node| template_builder::expect_usize_expression(language, build_ctx, node)) 1003 .transpose()?; 1004 let out_property = 1005 (self_property, len_property).map(|(id, len)| id.short(len.unwrap_or(12))); 1006 Ok(L::wrap_string(out_property)) 1007 }); 1008 map.insert( 1009 "shortest", 1010 |language, build_ctx, self_property, function| { 1011 let id_prefix_context = &language.id_prefix_context; 1012 let ([], [len_node]) = template_parser::expect_arguments(function)?; 1013 let len_property = len_node 1014 .map(|node| template_builder::expect_usize_expression(language, build_ctx, node)) 1015 .transpose()?; 1016 let out_property = (self_property, len_property) 1017 .map(|(id, len)| id.shortest(language.repo, id_prefix_context, len.unwrap_or(0))); 1018 Ok(L::wrap_shortest_id_prefix(out_property)) 1019 }, 1020 ); 1021 map 1022} 1023 1024pub struct ShortestIdPrefix { 1025 pub prefix: String, 1026 pub rest: String, 1027} 1028 1029impl Template for ShortestIdPrefix { 1030 fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { 1031 write!(formatter.labeled("prefix"), "{}", self.prefix)?; 1032 write!(formatter.labeled("rest"), "{}", self.rest)?; 1033 Ok(()) 1034 } 1035} 1036 1037impl ShortestIdPrefix { 1038 fn to_upper(&self) -> Self { 1039 Self { 1040 prefix: self.prefix.to_ascii_uppercase(), 1041 rest: self.rest.to_ascii_uppercase(), 1042 } 1043 } 1044 fn to_lower(&self) -> Self { 1045 Self { 1046 prefix: self.prefix.to_ascii_lowercase(), 1047 rest: self.rest.to_ascii_lowercase(), 1048 } 1049 } 1050} 1051 1052fn builtin_shortest_id_prefix_methods<'repo>( 1053) -> CommitTemplateBuildMethodFnMap<'repo, ShortestIdPrefix> { 1054 type L<'repo> = CommitTemplateLanguage<'repo>; 1055 // Not using maplit::hashmap!{} or custom declarative macro here because 1056 // code completion inside macro is quite restricted. 1057 let mut map = CommitTemplateBuildMethodFnMap::<ShortestIdPrefix>::new(); 1058 map.insert( 1059 "prefix", 1060 |_language, _build_ctx, self_property, function| { 1061 template_parser::expect_no_arguments(function)?; 1062 let out_property = self_property.map(|id| id.prefix); 1063 Ok(L::wrap_string(out_property)) 1064 }, 1065 ); 1066 map.insert("rest", |_language, _build_ctx, self_property, function| { 1067 template_parser::expect_no_arguments(function)?; 1068 let out_property = self_property.map(|id| id.rest); 1069 Ok(L::wrap_string(out_property)) 1070 }); 1071 map.insert("upper", |_language, _build_ctx, self_property, function| { 1072 template_parser::expect_no_arguments(function)?; 1073 let out_property = self_property.map(|id| id.to_upper()); 1074 Ok(L::wrap_shortest_id_prefix(out_property)) 1075 }); 1076 map.insert("lower", |_language, _build_ctx, self_property, function| { 1077 template_parser::expect_no_arguments(function)?; 1078 let out_property = self_property.map(|id| id.to_lower()); 1079 Ok(L::wrap_shortest_id_prefix(out_property)) 1080 }); 1081 map 1082}