just playing with tangled
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at export 513 lines 20 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::cmp::max; 16use std::io; 17 18use itertools::Itertools as _; 19use jujutsu_lib::backend::{ChangeId, CommitId, ObjectId as _}; 20use jujutsu_lib::commit::Commit; 21use jujutsu_lib::hex_util::to_reverse_hex; 22use jujutsu_lib::op_store::WorkspaceId; 23use jujutsu_lib::repo::Repo; 24use jujutsu_lib::rewrite; 25 26use crate::formatter::Formatter; 27use crate::template_builder::{ 28 self, BuildContext, CoreTemplatePropertyKind, IntoTemplateProperty, TemplateLanguage, 29}; 30use crate::template_parser::{ 31 self, FunctionCallNode, TemplateAliasesMap, TemplateParseError, TemplateParseResult, 32}; 33use crate::templater::{ 34 IntoTemplate, PlainTextFormattedProperty, Template, TemplateFunction, TemplateProperty, 35 TemplatePropertyFn, 36}; 37use crate::text_util; 38 39struct CommitTemplateLanguage<'repo, 'b> { 40 repo: &'repo dyn Repo, 41 workspace_id: &'b WorkspaceId, 42} 43 44impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo, '_> { 45 type Context = Commit; 46 type Property = CommitTemplatePropertyKind<'repo>; 47 48 template_builder::impl_core_wrap_property_fns!('repo, CommitTemplatePropertyKind::Core); 49 50 fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult<Self::Property> { 51 build_commit_keyword(self, name, span) 52 } 53 54 fn build_method( 55 &self, 56 build_ctx: &BuildContext<Self::Property>, 57 property: Self::Property, 58 function: &FunctionCallNode, 59 ) -> TemplateParseResult<Self::Property> { 60 match property { 61 CommitTemplatePropertyKind::Core(property) => { 62 template_builder::build_core_method(self, build_ctx, property, function) 63 } 64 CommitTemplatePropertyKind::Commit(property) => { 65 build_commit_method(self, build_ctx, property, function) 66 } 67 CommitTemplatePropertyKind::CommitList(property) => { 68 template_builder::build_unformattable_list_method( 69 self, 70 build_ctx, 71 property, 72 function, 73 |item| self.wrap_commit(item), 74 ) 75 } 76 CommitTemplatePropertyKind::CommitOrChangeId(property) => { 77 build_commit_or_change_id_method(self, build_ctx, property, function) 78 } 79 CommitTemplatePropertyKind::ShortestIdPrefix(property) => { 80 build_shortest_id_prefix_method(self, build_ctx, property, function) 81 } 82 } 83 } 84} 85 86// If we need to add multiple languages that support Commit types, this can be 87// turned into a trait which extends TemplateLanguage. 88impl<'repo> CommitTemplateLanguage<'repo, '_> { 89 fn wrap_commit( 90 &self, 91 property: impl TemplateProperty<Commit, Output = Commit> + 'repo, 92 ) -> CommitTemplatePropertyKind<'repo> { 93 CommitTemplatePropertyKind::Commit(Box::new(property)) 94 } 95 96 fn wrap_commit_list( 97 &self, 98 property: impl TemplateProperty<Commit, Output = Vec<Commit>> + 'repo, 99 ) -> CommitTemplatePropertyKind<'repo> { 100 CommitTemplatePropertyKind::CommitList(Box::new(property)) 101 } 102 103 fn wrap_commit_or_change_id( 104 &self, 105 property: impl TemplateProperty<Commit, Output = CommitOrChangeId> + 'repo, 106 ) -> CommitTemplatePropertyKind<'repo> { 107 CommitTemplatePropertyKind::CommitOrChangeId(Box::new(property)) 108 } 109 110 fn wrap_shortest_id_prefix( 111 &self, 112 property: impl TemplateProperty<Commit, Output = ShortestIdPrefix> + 'repo, 113 ) -> CommitTemplatePropertyKind<'repo> { 114 CommitTemplatePropertyKind::ShortestIdPrefix(Box::new(property)) 115 } 116} 117 118enum CommitTemplatePropertyKind<'repo> { 119 Core(CoreTemplatePropertyKind<'repo, Commit>), 120 Commit(Box<dyn TemplateProperty<Commit, Output = Commit> + 'repo>), 121 CommitList(Box<dyn TemplateProperty<Commit, Output = Vec<Commit>> + 'repo>), 122 CommitOrChangeId(Box<dyn TemplateProperty<Commit, Output = CommitOrChangeId> + 'repo>), 123 ShortestIdPrefix(Box<dyn TemplateProperty<Commit, Output = ShortestIdPrefix> + 'repo>), 124} 125 126impl<'repo> IntoTemplateProperty<'repo, Commit> for CommitTemplatePropertyKind<'repo> { 127 fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<Commit, Output = bool> + 'repo>> { 128 match self { 129 CommitTemplatePropertyKind::Core(property) => property.try_into_boolean(), 130 // TODO: should we allow implicit cast of List type? 131 _ => None, 132 } 133 } 134 135 fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<Commit, Output = i64> + 'repo>> { 136 match self { 137 CommitTemplatePropertyKind::Core(property) => property.try_into_integer(), 138 _ => None, 139 } 140 } 141 142 fn try_into_plain_text( 143 self, 144 ) -> Option<Box<dyn TemplateProperty<Commit, Output = String> + 'repo>> { 145 match self { 146 CommitTemplatePropertyKind::Core(property) => property.try_into_plain_text(), 147 _ => { 148 let template = self.try_into_template()?; 149 Some(Box::new(PlainTextFormattedProperty::new(template))) 150 } 151 } 152 } 153 154 fn try_into_template(self) -> Option<Box<dyn Template<Commit> + 'repo>> { 155 match self { 156 CommitTemplatePropertyKind::Core(property) => property.try_into_template(), 157 CommitTemplatePropertyKind::Commit(_) => None, 158 CommitTemplatePropertyKind::CommitList(_) => None, 159 CommitTemplatePropertyKind::CommitOrChangeId(property) => { 160 Some(property.into_template()) 161 } 162 CommitTemplatePropertyKind::ShortestIdPrefix(property) => { 163 Some(property.into_template()) 164 } 165 } 166 } 167} 168 169fn build_commit_keyword<'repo>( 170 language: &CommitTemplateLanguage<'repo, '_>, 171 name: &str, 172 span: pest::Span, 173) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> { 174 // Commit object is lightweight (a few Arc + CommitId), so just clone it 175 // to turn into a property type. Abstraction over "for<'a> (&'a T) -> &'a T" 176 // and "(&T) -> T" wouldn't be simple. If we want to remove Clone/Rc/Arc, 177 // maybe we can add an abstraction that takes "Fn(&Commit) -> O" and returns 178 // "TemplateProperty<Commit, Output = O>". 179 let property = TemplatePropertyFn(|commit: &Commit| commit.clone()); 180 build_commit_keyword_opt(language, property, name) 181 .ok_or_else(|| TemplateParseError::no_such_keyword(name, span)) 182} 183 184fn build_commit_method<'repo>( 185 language: &CommitTemplateLanguage<'repo, '_>, 186 _build_ctx: &BuildContext<CommitTemplatePropertyKind<'repo>>, 187 self_property: impl TemplateProperty<Commit, Output = Commit> + 'repo, 188 function: &FunctionCallNode, 189) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> { 190 if let Some(property) = build_commit_keyword_opt(language, self_property, function.name) { 191 template_parser::expect_no_arguments(function)?; 192 Ok(property) 193 } else { 194 Err(TemplateParseError::no_such_method("Commit", function)) 195 } 196} 197 198fn build_commit_keyword_opt<'repo>( 199 language: &CommitTemplateLanguage<'repo, '_>, 200 property: impl TemplateProperty<Commit, Output = Commit> + 'repo, 201 name: &str, 202) -> Option<CommitTemplatePropertyKind<'repo>> { 203 fn wrap_fn<'repo, O>( 204 property: impl TemplateProperty<Commit, Output = Commit> + 'repo, 205 f: impl Fn(&Commit) -> O + 'repo, 206 ) -> impl TemplateProperty<Commit, Output = O> + 'repo { 207 TemplateFunction::new(property, move |commit| f(&commit)) 208 } 209 fn wrap_repo_fn<'repo, O>( 210 repo: &'repo dyn Repo, 211 property: impl TemplateProperty<Commit, Output = Commit> + 'repo, 212 f: impl Fn(&dyn Repo, &Commit) -> O + 'repo, 213 ) -> impl TemplateProperty<Commit, Output = O> + 'repo { 214 TemplateFunction::new(property, move |commit| f(repo, &commit)) 215 } 216 217 let repo = language.repo; 218 let property = match name { 219 "description" => language.wrap_string(wrap_fn(property, |commit| { 220 text_util::complete_newline(commit.description()) 221 })), 222 "change_id" => language.wrap_commit_or_change_id(wrap_fn(property, |commit| { 223 CommitOrChangeId::Change(commit.change_id().to_owned()) 224 })), 225 "commit_id" => language.wrap_commit_or_change_id(wrap_fn(property, |commit| { 226 CommitOrChangeId::Commit(commit.id().to_owned()) 227 })), 228 "parents" => language.wrap_commit_list(wrap_fn(property, |commit| commit.parents())), 229 "author" => language.wrap_signature(wrap_fn(property, |commit| commit.author().clone())), 230 "committer" => { 231 language.wrap_signature(wrap_fn(property, |commit| commit.committer().clone())) 232 } 233 "working_copies" => { 234 language.wrap_string(wrap_repo_fn(repo, property, extract_working_copies)) 235 } 236 "current_working_copy" => { 237 let workspace_id = language.workspace_id.clone(); 238 language.wrap_boolean(wrap_fn(property, move |commit| { 239 Some(commit.id()) == repo.view().get_wc_commit_id(&workspace_id) 240 })) 241 } 242 "branches" => language.wrap_string(wrap_repo_fn(repo, property, extract_branches)), 243 "tags" => language.wrap_string(wrap_repo_fn(repo, property, extract_tags)), 244 "git_refs" => language.wrap_string(wrap_repo_fn(repo, property, extract_git_refs)), 245 "git_head" => language.wrap_string(wrap_repo_fn(repo, property, extract_git_head)), 246 "divergent" => language.wrap_boolean(wrap_fn(property, |commit| { 247 // The given commit could be hidden in e.g. obslog. 248 let maybe_entries = repo.resolve_change_id(commit.change_id()); 249 maybe_entries.map_or(0, |entries| entries.len()) > 1 250 })), 251 "hidden" => language.wrap_boolean(wrap_fn(property, |commit| { 252 let maybe_entries = repo.resolve_change_id(commit.change_id()); 253 maybe_entries.map_or(true, |entries| !entries.contains(commit.id())) 254 })), 255 "conflict" => { 256 language.wrap_boolean(wrap_fn(property, |commit| commit.tree().has_conflict())) 257 } 258 "empty" => language.wrap_boolean(wrap_fn(property, |commit| { 259 commit.tree().id() == rewrite::merge_commit_trees(repo, &commit.parents()).id() 260 })), 261 _ => return None, 262 }; 263 Some(property) 264} 265 266// TODO: return Vec<String> 267fn extract_working_copies(repo: &dyn Repo, commit: &Commit) -> String { 268 let wc_commit_ids = repo.view().wc_commit_ids(); 269 if wc_commit_ids.len() <= 1 { 270 return "".to_string(); 271 } 272 let mut names = vec![]; 273 for (workspace_id, wc_commit_id) in wc_commit_ids.iter().sorted() { 274 if wc_commit_id == commit.id() { 275 names.push(format!("{}@", workspace_id.as_str())); 276 } 277 } 278 names.join(" ") 279} 280 281// TODO: return Vec<Branch>? 282fn extract_branches(repo: &dyn Repo, commit: &Commit) -> String { 283 let mut names = vec![]; 284 for (branch_name, branch_target) in repo.view().branches() { 285 let local_target = branch_target.local_target.as_ref(); 286 if let Some(local_target) = local_target { 287 if local_target.has_add(commit.id()) { 288 if local_target.is_conflict() { 289 names.push(format!("{branch_name}??")); 290 } else if branch_target 291 .remote_targets 292 .values() 293 .any(|remote_target| remote_target != local_target) 294 { 295 names.push(format!("{branch_name}*")); 296 } else { 297 names.push(branch_name.clone()); 298 } 299 } 300 } 301 for (remote_name, remote_target) in &branch_target.remote_targets { 302 if Some(remote_target) != local_target && remote_target.has_add(commit.id()) { 303 if remote_target.is_conflict() { 304 names.push(format!("{branch_name}@{remote_name}?")); 305 } else { 306 names.push(format!("{branch_name}@{remote_name}")); 307 } 308 } 309 } 310 } 311 names.join(" ") 312} 313 314// TODO: return Vec<NameRef>? 315fn extract_tags(repo: &dyn Repo, commit: &Commit) -> String { 316 let mut names = vec![]; 317 for (tag_name, target) in repo.view().tags() { 318 if target.has_add(commit.id()) { 319 if target.is_conflict() { 320 names.push(format!("{tag_name}?")); 321 } else { 322 names.push(tag_name.clone()); 323 } 324 } 325 } 326 names.join(" ") 327} 328 329// TODO: return Vec<NameRef>? 330fn extract_git_refs(repo: &dyn Repo, commit: &Commit) -> String { 331 // TODO: We should keep a map from commit to ref names so we don't have to walk 332 // all refs here. 333 let mut names = vec![]; 334 for (name, target) in repo.view().git_refs() { 335 if target.has_add(commit.id()) { 336 if target.is_conflict() { 337 names.push(format!("{name}?")); 338 } else { 339 names.push(name.clone()); 340 } 341 } 342 } 343 names.join(" ") 344} 345 346// TODO: return NameRef? 347fn extract_git_head(repo: &dyn Repo, commit: &Commit) -> String { 348 match repo.view().git_head() { 349 Some(ref_target) if ref_target.has_add(commit.id()) => { 350 if ref_target.is_conflict() { 351 "HEAD@git?".to_string() 352 } else { 353 "HEAD@git".to_string() 354 } 355 } 356 _ => "".to_string(), 357 } 358} 359 360#[derive(Clone, Debug, Eq, PartialEq)] 361enum CommitOrChangeId { 362 Commit(CommitId), 363 Change(ChangeId), 364} 365 366impl CommitOrChangeId { 367 pub fn hex(&self) -> String { 368 match self { 369 CommitOrChangeId::Commit(id) => id.hex(), 370 CommitOrChangeId::Change(id) => { 371 // TODO: We can avoid the unwrap() and make this more efficient by converting 372 // straight from bytes. 373 to_reverse_hex(&id.hex()).unwrap() 374 } 375 } 376 } 377 378 pub fn short(&self, total_len: usize) -> String { 379 let mut hex = self.hex(); 380 hex.truncate(total_len); 381 hex 382 } 383 384 /// The length of the id printed will be the maximum of `total_len` and the 385 /// length of the shortest unique prefix 386 pub fn shortest(&self, repo: &dyn Repo, total_len: usize) -> ShortestIdPrefix { 387 let mut hex = self.hex(); 388 let prefix_len = match self { 389 CommitOrChangeId::Commit(id) => repo.index().shortest_unique_commit_id_prefix_len(id), 390 CommitOrChangeId::Change(id) => repo.shortest_unique_change_id_prefix_len(id), 391 }; 392 hex.truncate(max(prefix_len, total_len)); 393 let rest = hex.split_off(prefix_len); 394 ShortestIdPrefix { prefix: hex, rest } 395 } 396} 397 398impl Template<()> for CommitOrChangeId { 399 fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { 400 formatter.write_str(&self.hex()) 401 } 402} 403 404fn build_commit_or_change_id_method<'repo>( 405 language: &CommitTemplateLanguage<'repo, '_>, 406 build_ctx: &BuildContext<CommitTemplatePropertyKind<'repo>>, 407 self_property: impl TemplateProperty<Commit, Output = CommitOrChangeId> + 'repo, 408 function: &FunctionCallNode, 409) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> { 410 let parse_optional_integer = |function| -> Result<Option<_>, TemplateParseError> { 411 let ([], [len_node]) = template_parser::expect_arguments(function)?; 412 len_node 413 .map(|node| template_builder::expect_integer_expression(language, build_ctx, node)) 414 .transpose() 415 }; 416 let property = match function.name { 417 "short" => { 418 let len_property = parse_optional_integer(function)?; 419 language.wrap_string(TemplateFunction::new( 420 (self_property, len_property), 421 |(id, len)| id.short(len.and_then(|l| l.try_into().ok()).unwrap_or(12)), 422 )) 423 } 424 "shortest" => { 425 let repo = language.repo; 426 let len_property = parse_optional_integer(function)?; 427 language.wrap_shortest_id_prefix(TemplateFunction::new( 428 (self_property, len_property), 429 |(id, len)| id.shortest(repo, len.and_then(|l| l.try_into().ok()).unwrap_or(0)), 430 )) 431 } 432 _ => { 433 return Err(TemplateParseError::no_such_method( 434 "CommitOrChangeId", 435 function, 436 )) 437 } 438 }; 439 Ok(property) 440} 441 442struct ShortestIdPrefix { 443 pub prefix: String, 444 pub rest: String, 445} 446 447impl Template<()> for ShortestIdPrefix { 448 fn format(&self, _: &(), formatter: &mut dyn Formatter) -> io::Result<()> { 449 formatter.with_label("prefix", |fmt| fmt.write_str(&self.prefix))?; 450 formatter.with_label("rest", |fmt| fmt.write_str(&self.rest)) 451 } 452} 453 454impl ShortestIdPrefix { 455 fn to_upper(&self) -> Self { 456 Self { 457 prefix: self.prefix.to_ascii_uppercase(), 458 rest: self.rest.to_ascii_uppercase(), 459 } 460 } 461 fn to_lower(&self) -> Self { 462 Self { 463 prefix: self.prefix.to_ascii_lowercase(), 464 rest: self.rest.to_ascii_lowercase(), 465 } 466 } 467} 468 469fn build_shortest_id_prefix_method<'repo>( 470 language: &CommitTemplateLanguage<'repo, '_>, 471 _build_ctx: &BuildContext<CommitTemplatePropertyKind<'repo>>, 472 self_property: impl TemplateProperty<Commit, Output = ShortestIdPrefix> + 'repo, 473 function: &FunctionCallNode, 474) -> TemplateParseResult<CommitTemplatePropertyKind<'repo>> { 475 let property = match function.name { 476 "prefix" => { 477 template_parser::expect_no_arguments(function)?; 478 language.wrap_string(TemplateFunction::new(self_property, |id| id.prefix)) 479 } 480 "rest" => { 481 template_parser::expect_no_arguments(function)?; 482 language.wrap_string(TemplateFunction::new(self_property, |id| id.rest)) 483 } 484 "upper" => { 485 template_parser::expect_no_arguments(function)?; 486 language 487 .wrap_shortest_id_prefix(TemplateFunction::new(self_property, |id| id.to_upper())) 488 } 489 "lower" => { 490 template_parser::expect_no_arguments(function)?; 491 language 492 .wrap_shortest_id_prefix(TemplateFunction::new(self_property, |id| id.to_lower())) 493 } 494 _ => { 495 return Err(TemplateParseError::no_such_method( 496 "ShortestIdPrefix", 497 function, 498 )) 499 } 500 }; 501 Ok(property) 502} 503 504pub fn parse<'repo>( 505 repo: &'repo dyn Repo, 506 workspace_id: &WorkspaceId, 507 template_text: &str, 508 aliases_map: &TemplateAliasesMap, 509) -> TemplateParseResult<Box<dyn Template<Commit> + 'repo>> { 510 let language = CommitTemplateLanguage { repo, workspace_id }; 511 let node = template_parser::parse(template_text, aliases_map)?; 512 template_builder::build(&language, &node) 513}