| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | use std::{ |
| 8 | collections::HashMap, |
| 9 | sync::{Arc, Mutex}, |
| 10 | time::SystemTime, |
| 11 | }; |
| 12 | |
| 13 | use crate::{ |
| 14 | application::{ |
| 15 | blame::Blame, |
| 16 | port::{Blob, CommitCount, GitQuery, GitQueryError, MergePreview, RawDiff}, |
| 17 | }, |
| 18 | domain::{ |
| 19 | BranchRow, CommitDetail, CommitEntry, CommitSummary, EntryCommit, GitRef, GrepHit, |
| 20 | ObjectId, OrgName, RefKind, RefName, RepoName, RepoPath, TagRow, TagSummary, TreeEntry, |
| 21 | }, |
| 22 | }; |
| 23 | |
| 24 | use super::FOLLOWED_LOG_SCAN; |
| 25 | |
| 26 | |
| 27 | |
| 28 | |
| 29 | |
| 30 | |
| 31 | #[derive(Debug, Default, Clone)] |
| 32 | pub struct InMemoryGitQuery { |
| 33 | default_branch: Option<RefName>, |
| 34 | |
| 35 | trees: HashMap<String, Vec<TreeEntry>>, |
| 36 | blobs: HashMap<String, Vec<u8>>, |
| 37 | commits: Vec<CommitSummary>, |
| 38 | |
| 39 | |
| 40 | refs: Vec<GitRef>, |
| 41 | |
| 42 | |
| 43 | commit_count: Option<u64>, |
| 44 | latest_tag: Option<TagSummary>, |
| 45 | |
| 46 | |
| 47 | branch_rows: Vec<BranchRow>, |
| 48 | tag_rows: Vec<TagRow>, |
| 49 | |
| 50 | |
| 51 | grep_hits: Vec<GrepHit>, |
| 52 | |
| 53 | |
| 54 | slow_grep: bool, |
| 55 | |
| 56 | |
| 57 | detail: Option<CommitDetail>, |
| 58 | |
| 59 | diff: Option<RawDiff>, |
| 60 | |
| 61 | merge_base: Option<ObjectId>, |
| 62 | |
| 63 | |
| 64 | |
| 65 | merge_preview: Option<MergePreview>, |
| 66 | |
| 67 | |
| 68 | |
| 69 | snapshot: Arc<Mutex<Vec<(RefName, ObjectId)>>>, |
| 70 | |
| 71 | |
| 72 | |
| 73 | blames: HashMap<String, Blame>, |
| 74 | |
| 75 | |
| 76 | |
| 77 | |
| 78 | bodies: HashMap<String, String>, |
| 79 | |
| 80 | |
| 81 | last_commits: HashMap<String, HashMap<String, EntryCommit>>, |
| 82 | |
| 83 | |
| 84 | |
| 85 | |
| 86 | |
| 87 | |
| 88 | path_logs: HashMap<String, Vec<CommitSummary>>, |
| 89 | |
| 90 | |
| 91 | symlink_targets: HashMap<ObjectId, String>, |
| 92 | |
| 93 | |
| 94 | |
| 95 | |
| 96 | |
| 97 | |
| 98 | resolved: HashMap<String, ObjectId>, |
| 99 | } |
| 100 | |
| 101 | impl InMemoryGitQuery { |
| 102 | |
| 103 | pub fn empty() -> Self { |
| 104 | Self::default() |
| 105 | } |
| 106 | |
| 107 | |
| 108 | pub fn new() -> Self { |
| 109 | Self { |
| 110 | default_branch: Some(RefName::from_trusted("main")), |
| 111 | ..Self::default() |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | fn key(rev: &RefName, path: &RepoPath) -> String { |
| 116 | format!("{}\0{}", rev.as_str(), path.as_str()) |
| 117 | } |
| 118 | |
| 119 | pub fn with_tree(mut self, rev: &str, path: &str, entries: Vec<TreeEntry>) -> Self { |
| 120 | self.trees.insert( |
| 121 | Self::key(&RefName::from_trusted(rev), &RepoPath::from_trusted(path)), |
| 122 | entries, |
| 123 | ); |
| 124 | self |
| 125 | } |
| 126 | |
| 127 | pub fn with_blob(mut self, rev: &str, path: &str, content: impl Into<Vec<u8>>) -> Self { |
| 128 | self.blobs.insert( |
| 129 | Self::key(&RefName::from_trusted(rev), &RepoPath::from_trusted(path)), |
| 130 | content.into(), |
| 131 | ); |
| 132 | self |
| 133 | } |
| 134 | |
| 135 | |
| 136 | pub fn with_last_commits( |
| 137 | mut self, |
| 138 | rev: &str, |
| 139 | path: &str, |
| 140 | commits: Vec<(&str, EntryCommit)>, |
| 141 | ) -> Self { |
| 142 | self.last_commits.insert( |
| 143 | Self::key(&RefName::from_trusted(rev), &RepoPath::from_trusted(path)), |
| 144 | commits |
| 145 | .into_iter() |
| 146 | .map(|(name, commit)| (name.to_owned(), commit)) |
| 147 | .collect(), |
| 148 | ); |
| 149 | self |
| 150 | } |
| 151 | |
| 152 | pub fn with_log(mut self, commits: Vec<CommitSummary>) -> Self { |
| 153 | self.commits = commits; |
| 154 | self |
| 155 | } |
| 156 | |
| 157 | |
| 158 | pub fn with_path_log(mut self, rev: &str, path: &str, commits: Vec<CommitSummary>) -> Self { |
| 159 | self.path_logs.insert( |
| 160 | Self::key(&RefName::from_trusted(rev), &RepoPath::from_trusted(path)), |
| 161 | commits, |
| 162 | ); |
| 163 | self |
| 164 | } |
| 165 | |
| 166 | |
| 167 | pub fn with_symlink_target(mut self, id: &ObjectId, target: &str) -> Self { |
| 168 | self.symlink_targets.insert(id.clone(), target.to_owned()); |
| 169 | self |
| 170 | } |
| 171 | |
| 172 | pub fn with_branch(self, name: &str) -> Self { |
| 173 | self.with_ref(name, RefKind::Branch) |
| 174 | } |
| 175 | |
| 176 | pub fn with_tag(self, name: &str) -> Self { |
| 177 | self.with_ref(name, RefKind::Tag) |
| 178 | } |
| 179 | |
| 180 | |
| 181 | pub fn with_resolved(mut self, rev: &str, id: &str) -> Self { |
| 182 | self.resolved |
| 183 | .insert(rev.to_owned(), ObjectId::from_trusted(format!("{id:0>40}"))); |
| 184 | self |
| 185 | } |
| 186 | |
| 187 | pub fn with_commit_count(mut self, count: u64) -> Self { |
| 188 | self.commit_count = Some(count); |
| 189 | self |
| 190 | } |
| 191 | |
| 192 | pub fn with_latest_tag(mut self, name: &str, created_at: SystemTime) -> Self { |
| 193 | self.latest_tag = Some(TagSummary { |
| 194 | name: RefName::from_trusted(name), |
| 195 | created_at, |
| 196 | }); |
| 197 | self |
| 198 | } |
| 199 | |
| 200 | |
| 201 | pub fn with_branch_row( |
| 202 | mut self, |
| 203 | name: &str, |
| 204 | is_default: bool, |
| 205 | committed_at: SystemTime, |
| 206 | ) -> Self { |
| 207 | self.branch_rows.push(BranchRow { |
| 208 | name: RefName::from_trusted(name), |
| 209 | is_default, |
| 210 | commit: ObjectId::from_trusted("2".repeat(40)), |
| 211 | summary: format!("work on {name}"), |
| 212 | committed_at, |
| 213 | }); |
| 214 | self |
| 215 | } |
| 216 | |
| 217 | |
| 218 | |
| 219 | pub fn with_tag_row(mut self, name: &str, annotated: bool, created_at: SystemTime) -> Self { |
| 220 | self.tag_rows.push(TagRow { |
| 221 | name: RefName::from_trusted(name), |
| 222 | commit: ObjectId::from_trusted("3".repeat(40)), |
| 223 | message: annotated.then(|| format!("release {name}")), |
| 224 | annotated, |
| 225 | created_at, |
| 226 | }); |
| 227 | self |
| 228 | } |
| 229 | |
| 230 | |
| 231 | |
| 232 | pub fn with_grep_hits(mut self, hits: Vec<GrepHit>) -> Self { |
| 233 | self.grep_hits = hits; |
| 234 | self |
| 235 | } |
| 236 | |
| 237 | |
| 238 | pub fn with_slow_grep(mut self) -> Self { |
| 239 | self.slow_grep = true; |
| 240 | self |
| 241 | } |
| 242 | |
| 243 | |
| 244 | pub fn with_commit_body(mut self, id: &str, body: &str) -> Self { |
| 245 | self.bodies.insert(id.to_owned(), body.to_owned()); |
| 246 | self |
| 247 | } |
| 248 | |
| 249 | pub fn with_commit(mut self, detail: CommitDetail) -> Self { |
| 250 | self.detail = Some(detail); |
| 251 | self |
| 252 | } |
| 253 | |
| 254 | pub fn with_diff(mut self, patch: impl Into<Vec<u8>>, numstat: impl Into<Vec<u8>>) -> Self { |
| 255 | self.diff = Some(RawDiff { |
| 256 | numstat: numstat.into(), |
| 257 | patch: patch.into(), |
| 258 | truncated: false, |
| 259 | }); |
| 260 | self |
| 261 | } |
| 262 | |
| 263 | pub fn with_merge_base(mut self, id: &str) -> Self { |
| 264 | self.merge_base = Some(ObjectId::from_trusted(id)); |
| 265 | self |
| 266 | } |
| 267 | |
| 268 | |
| 269 | pub fn with_conflicts(mut self, paths: &[&str]) -> Self { |
| 270 | self.merge_preview = Some(MergePreview::Conflicted { |
| 271 | paths: paths.iter().map(|path| (*path).to_owned()).collect(), |
| 272 | }); |
| 273 | self |
| 274 | } |
| 275 | |
| 276 | pub fn with_blame(mut self, rev: &str, path: &str, blame: Blame) -> Self { |
| 277 | self.blames.insert( |
| 278 | Self::key(&RefName::from_trusted(rev), &RepoPath::from_trusted(path)), |
| 279 | blame, |
| 280 | ); |
| 281 | self |
| 282 | } |
| 283 | |
| 284 | |
| 285 | pub fn with_ref_snapshot(self, refs: &[(&str, &str)]) -> Self { |
| 286 | self.set_ref_snapshot(refs); |
| 287 | self |
| 288 | } |
| 289 | |
| 290 | |
| 291 | |
| 292 | |
| 293 | |
| 294 | pub fn set_ref_snapshot(&self, refs: &[(&str, &str)]) { |
| 295 | *self.snapshot.lock().expect("lock poisoned") = refs |
| 296 | .iter() |
| 297 | .map(|(name, id)| { |
| 298 | ( |
| 299 | RefName::from_trusted(*name), |
| 300 | ObjectId::from_trusted(format!("{id:0>40}")), |
| 301 | ) |
| 302 | }) |
| 303 | .collect(); |
| 304 | } |
| 305 | |
| 306 | fn with_ref(mut self, name: &str, kind: RefKind) -> Self { |
| 307 | self.refs.push(GitRef { |
| 308 | name: RefName::from_trusted(name), |
| 309 | kind, |
| 310 | }); |
| 311 | self |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | impl GitQuery for InMemoryGitQuery { |
| 316 | async fn default_branch( |
| 317 | &self, |
| 318 | _handle: &OrgName, |
| 319 | _name: &RepoName, |
| 320 | ) -> Result<Option<RefName>, GitQueryError> { |
| 321 | Ok(self.default_branch.clone()) |
| 322 | } |
| 323 | |
| 324 | async fn resolve( |
| 325 | &self, |
| 326 | _handle: &OrgName, |
| 327 | _name: &RepoName, |
| 328 | rev: &RefName, |
| 329 | ) -> Result<Option<ObjectId>, GitQueryError> { |
| 330 | if let Some(id) = self.resolved.get(rev.as_str()) { |
| 331 | return Ok(Some(id.clone())); |
| 332 | } |
| 333 | |
| 334 | Ok(self |
| 335 | .default_branch |
| 336 | .as_ref() |
| 337 | .map(|_| ObjectId::from_trusted("0".repeat(40)))) |
| 338 | } |
| 339 | |
| 340 | async fn list_tree( |
| 341 | &self, |
| 342 | _handle: &OrgName, |
| 343 | _name: &RepoName, |
| 344 | rev: &RefName, |
| 345 | path: &RepoPath, |
| 346 | ) -> Result<Option<Vec<TreeEntry>>, GitQueryError> { |
| 347 | Ok(self.trees.get(&Self::key(rev, path)).cloned()) |
| 348 | } |
| 349 | |
| 350 | async fn last_commits( |
| 351 | &self, |
| 352 | _handle: &OrgName, |
| 353 | _name: &RepoName, |
| 354 | rev: &RefName, |
| 355 | path: &RepoPath, |
| 356 | entries: &[String], |
| 357 | ) -> Result<HashMap<String, EntryCommit>, GitQueryError> { |
| 358 | let Some(seeded) = self.last_commits.get(&Self::key(rev, path)) else { |
| 359 | return Ok(HashMap::new()); |
| 360 | }; |
| 361 | |
| 362 | |
| 363 | |
| 364 | Ok(seeded |
| 365 | .iter() |
| 366 | .filter(|(name, _)| entries.iter().any(|entry| entry == *name)) |
| 367 | .map(|(name, commit)| (name.clone(), commit.clone())) |
| 368 | .collect()) |
| 369 | } |
| 370 | |
| 371 | async fn read_blob( |
| 372 | &self, |
| 373 | _handle: &OrgName, |
| 374 | _name: &RepoName, |
| 375 | rev: &RefName, |
| 376 | path: &RepoPath, |
| 377 | max_bytes: u64, |
| 378 | ) -> Result<Option<Blob>, GitQueryError> { |
| 379 | Ok(self.blobs.get(&Self::key(rev, path)).map(|content| { |
| 380 | let size = content.len() as u64; |
| 381 | |
| 382 | Blob { |
| 383 | id: ObjectId::from_trusted("1".repeat(40)), |
| 384 | size, |
| 385 | |
| 386 | |
| 387 | content: (size <= max_bytes).then(|| content.clone()), |
| 388 | } |
| 389 | })) |
| 390 | } |
| 391 | |
| 392 | async fn log( |
| 393 | &self, |
| 394 | _handle: &OrgName, |
| 395 | _name: &RepoName, |
| 396 | _rev: &RefName, |
| 397 | offset: usize, |
| 398 | limit: usize, |
| 399 | ) -> Result<Vec<CommitSummary>, GitQueryError> { |
| 400 | Ok(self |
| 401 | .commits |
| 402 | .iter() |
| 403 | .skip(offset) |
| 404 | .take(limit) |
| 405 | .cloned() |
| 406 | .collect()) |
| 407 | } |
| 408 | |
| 409 | async fn log_entries( |
| 410 | &self, |
| 411 | _handle: &OrgName, |
| 412 | _name: &RepoName, |
| 413 | _rev: &RefName, |
| 414 | limit: usize, |
| 415 | ) -> Result<Vec<CommitEntry>, GitQueryError> { |
| 416 | Ok(self |
| 417 | .commits |
| 418 | .iter() |
| 419 | .take(limit) |
| 420 | .map(|commit| CommitEntry { |
| 421 | body: self |
| 422 | .bodies |
| 423 | .get(commit.id.as_str()) |
| 424 | .cloned() |
| 425 | .unwrap_or_default(), |
| 426 | commit: commit.clone(), |
| 427 | }) |
| 428 | .collect()) |
| 429 | } |
| 430 | |
| 431 | async fn list_refs( |
| 432 | &self, |
| 433 | _handle: &OrgName, |
| 434 | _name: &RepoName, |
| 435 | ) -> Result<Vec<GitRef>, GitQueryError> { |
| 436 | Ok(self.refs.clone()) |
| 437 | } |
| 438 | |
| 439 | async fn count_commits( |
| 440 | &self, |
| 441 | _handle: &OrgName, |
| 442 | _name: &RepoName, |
| 443 | _rev: &RefName, |
| 444 | ) -> Result<u64, GitQueryError> { |
| 445 | Ok(self.commit_count.unwrap_or(self.commits.len() as u64)) |
| 446 | } |
| 447 | |
| 448 | async fn latest_tag( |
| 449 | &self, |
| 450 | _handle: &OrgName, |
| 451 | _name: &RepoName, |
| 452 | ) -> Result<Option<TagSummary>, GitQueryError> { |
| 453 | Ok(self.latest_tag.clone()) |
| 454 | } |
| 455 | |
| 456 | async fn branches( |
| 457 | &self, |
| 458 | _handle: &OrgName, |
| 459 | _name: &RepoName, |
| 460 | ) -> Result<Vec<BranchRow>, GitQueryError> { |
| 461 | Ok(self.branch_rows.clone()) |
| 462 | } |
| 463 | |
| 464 | async fn tags( |
| 465 | &self, |
| 466 | _handle: &OrgName, |
| 467 | _name: &RepoName, |
| 468 | ) -> Result<Vec<TagRow>, GitQueryError> { |
| 469 | Ok(self.tag_rows.clone()) |
| 470 | } |
| 471 | |
| 472 | async fn grep( |
| 473 | &self, |
| 474 | _handle: &OrgName, |
| 475 | _name: &RepoName, |
| 476 | _commit: &ObjectId, |
| 477 | _query: &str, |
| 478 | limit: usize, |
| 479 | ) -> Result<Vec<GrepHit>, GitQueryError> { |
| 480 | if self.slow_grep { |
| 481 | return Err(GitQueryError::timed_out(std::time::Duration::from_secs(20))); |
| 482 | } |
| 483 | |
| 484 | Ok(self.grep_hits.iter().take(limit).cloned().collect()) |
| 485 | } |
| 486 | |
| 487 | async fn commit( |
| 488 | &self, |
| 489 | _handle: &OrgName, |
| 490 | _name: &RepoName, |
| 491 | _rev: &RefName, |
| 492 | ) -> Result<Option<CommitDetail>, GitQueryError> { |
| 493 | Ok(self.detail.clone()) |
| 494 | } |
| 495 | |
| 496 | async fn diff( |
| 497 | &self, |
| 498 | _handle: &OrgName, |
| 499 | _name: &RepoName, |
| 500 | _base: Option<&ObjectId>, |
| 501 | _head: &ObjectId, |
| 502 | _max_bytes: u64, |
| 503 | ) -> Result<RawDiff, GitQueryError> { |
| 504 | Ok(self.diff.clone().unwrap_or_default()) |
| 505 | } |
| 506 | |
| 507 | async fn merge_base( |
| 508 | &self, |
| 509 | _handle: &OrgName, |
| 510 | _name: &RepoName, |
| 511 | _base: &ObjectId, |
| 512 | _head: &ObjectId, |
| 513 | ) -> Result<Option<ObjectId>, GitQueryError> { |
| 514 | Ok(self.merge_base.clone()) |
| 515 | } |
| 516 | |
| 517 | async fn log_between( |
| 518 | &self, |
| 519 | _handle: &OrgName, |
| 520 | _name: &RepoName, |
| 521 | _base: Option<&ObjectId>, |
| 522 | _head: &ObjectId, |
| 523 | offset: usize, |
| 524 | limit: usize, |
| 525 | ) -> Result<Vec<CommitSummary>, GitQueryError> { |
| 526 | Ok(self |
| 527 | .commits |
| 528 | .iter() |
| 529 | .skip(offset) |
| 530 | .take(limit) |
| 531 | .cloned() |
| 532 | .collect()) |
| 533 | } |
| 534 | |
| 535 | async fn count_between( |
| 536 | &self, |
| 537 | _handle: &OrgName, |
| 538 | _name: &RepoName, |
| 539 | _base: Option<&ObjectId>, |
| 540 | _head: &ObjectId, |
| 541 | ) -> Result<usize, GitQueryError> { |
| 542 | Ok(self.commits.len()) |
| 543 | } |
| 544 | |
| 545 | async fn blame( |
| 546 | &self, |
| 547 | _handle: &OrgName, |
| 548 | _name: &RepoName, |
| 549 | rev: &RefName, |
| 550 | path: &RepoPath, |
| 551 | ) -> Result<Option<Blame>, GitQueryError> { |
| 552 | Ok(self.blames.get(&Self::key(rev, path)).cloned()) |
| 553 | } |
| 554 | |
| 555 | async fn ref_snapshot( |
| 556 | &self, |
| 557 | _handle: &OrgName, |
| 558 | _name: &RepoName, |
| 559 | ) -> Result<Vec<(RefName, ObjectId)>, GitQueryError> { |
| 560 | Ok(self.snapshot.lock().expect("lock poisoned").clone()) |
| 561 | } |
| 562 | |
| 563 | async fn log_at_path( |
| 564 | &self, |
| 565 | _handle: &OrgName, |
| 566 | _name: &RepoName, |
| 567 | rev: &RefName, |
| 568 | path: &RepoPath, |
| 569 | _follow: bool, |
| 570 | offset: usize, |
| 571 | limit: usize, |
| 572 | ) -> Result<Vec<CommitSummary>, GitQueryError> { |
| 573 | let Some(commits) = self.path_logs.get(&Self::key(rev, path)) else { |
| 574 | return Ok(Vec::new()); |
| 575 | }; |
| 576 | |
| 577 | Ok(commits.iter().skip(offset).take(limit).cloned().collect()) |
| 578 | } |
| 579 | |
| 580 | async fn count_commits_at_path( |
| 581 | &self, |
| 582 | _handle: &OrgName, |
| 583 | _name: &RepoName, |
| 584 | rev: &RefName, |
| 585 | path: &RepoPath, |
| 586 | follow: bool, |
| 587 | ) -> Result<CommitCount, GitQueryError> { |
| 588 | let counted = self |
| 589 | .path_logs |
| 590 | .get(&Self::key(rev, path)) |
| 591 | .map(Vec::len) |
| 592 | .unwrap_or(0); |
| 593 | |
| 594 | |
| 595 | |
| 596 | |
| 597 | Ok(match follow { |
| 598 | true => CommitCount { |
| 599 | total: counted.min(FOLLOWED_LOG_SCAN) as u64, |
| 600 | capped: counted > FOLLOWED_LOG_SCAN, |
| 601 | }, |
| 602 | false => CommitCount { |
| 603 | total: counted as u64, |
| 604 | capped: false, |
| 605 | }, |
| 606 | }) |
| 607 | } |
| 608 | |
| 609 | async fn symlink_targets( |
| 610 | &self, |
| 611 | _handle: &OrgName, |
| 612 | _name: &RepoName, |
| 613 | ids: &[ObjectId], |
| 614 | ) -> Result<HashMap<ObjectId, String>, GitQueryError> { |
| 615 | Ok(ids |
| 616 | .iter() |
| 617 | .filter_map(|id| Some((id.clone(), self.symlink_targets.get(id)?.clone()))) |
| 618 | .collect()) |
| 619 | } |
| 620 | |
| 621 | async fn merge_preview( |
| 622 | &self, |
| 623 | _handle: &OrgName, |
| 624 | _name: &RepoName, |
| 625 | _target: &ObjectId, |
| 626 | _source: &ObjectId, |
| 627 | ) -> Result<MergePreview, GitQueryError> { |
| 628 | |
| 629 | |
| 630 | Ok(self |
| 631 | .merge_preview |
| 632 | .clone() |
| 633 | .unwrap_or_else(|| MergePreview::Clean { |
| 634 | tree: ObjectId::from_trusted("7".repeat(40)), |
| 635 | })) |
| 636 | } |
| 637 | } |