don't
5
fork

Configure Feed

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

feat(knot): impl builder-style api for `TempWorktree`

Allows temporary worktree name to be customized (eg. to indicate purpose).

Signed-off-by: tjh <x@tjh.dev>

tjh 7f2c9723 d26847d2

+134 -56
+128 -50
crates/knot/src/model/repository.rs
··· 574 574 .env(ENV_PRIVATE_ENDPOINTS, self.knot.private_endpoints()) 575 575 .env(ENV_REPO_DID, self.repo_key.owner_str()) 576 576 .env(ENV_REPO_RKEY, &self.repo_key.rkey); 577 + 577 578 command 578 579 } 579 580 } 580 581 582 + /// A temporary detached worktree generated with a randomised name, and deleted when 583 + /// the object is dropped. 584 + /// 581 585 #[derive(Debug)] 582 586 struct TempWorktree<'repo> { 583 587 repo: &'repo gix::Repository, 588 + 589 + /// Worktree name. 590 + // 591 + // Used to remove the worktree later. 584 592 name: String, 593 + 594 + /// Path to the worktree. 585 595 path: PathBuf, 596 + 597 + /// Path to the global git configuration file. 586 598 config: Option<PathBuf>, 587 599 } 588 600 589 601 impl<'repo> TempWorktree<'repo> { 590 - pub fn new(_repo: &'repo gix::Repository, git_config: Option<&Path>) -> io::Result<Self> { 591 - Self::new_from(_repo, git_config, None) 602 + pub fn builder() -> TempWorktreeBuilder<'repo> { 603 + TempWorktreeBuilder::new() 592 604 } 593 605 594 - pub fn new_from( 595 - repo: &'repo gix::Repository, 596 - git_config: Option<&Path>, 597 - commit: Option<&ObjectId>, 598 - ) -> io::Result<Self> { 599 - let mut name = String::with_capacity("merge-check-".len() + 16); 600 - let random_bytes: [u8; 8] = rand::random(); 601 - 602 - name.push_str("merge-check-"); 603 - data_encoding::BASE32_NOPAD_VISUAL.encode_append(&random_bytes, &mut name); 604 - name = name.to_lowercase(); 605 - 606 - let commit = commit.map(ToString::to_string); 607 - 608 - let output = Command::new("/usr/bin/git") 609 - .env_clear() 610 - .current_dir(repo.path()) 611 - .option_env("GIT_GLOBAL_CONFIG", git_config) 612 - .arg("worktree") 613 - .arg("add") 614 - .arg("--detach") 615 - .arg(&name) 616 - .option_arg(commit) 617 - .stderr(Stdio::inherit()) 618 - .stderr(Stdio::inherit()) 619 - .output()?; 620 - 621 - if !output.status.success() { 622 - return Err(io::Error::other("Failed to create temporary worktree")); 623 - } 624 - 625 - let path = repo.path().join(&name); 626 - 627 - Ok(Self { 628 - repo, 629 - name, 630 - path, 631 - config: git_config.map(Path::to_path_buf), 632 - }) 633 - } 634 - 635 - /// Get the path of the worktree. 636 - fn path(&self) -> &Path { 606 + /// Get the absolute path to the worktree. 607 + pub fn path(&self) -> &Path { 637 608 &self.path 638 609 } 639 610 } ··· 616 645 .env_clear() 617 646 .current_dir(self.repo.path()) 618 647 .option_env("GIT_GLOBAL_CONFIG", self.config.as_deref()) 648 + .arg("-C") 649 + .arg(self.repo.path()) 619 650 .arg("worktree") 620 651 .arg("remove") 621 652 .arg(&self.name) 622 653 .stderr(Stdio::inherit()) 623 - .stderr(Stdio::inherit()) 654 + .stdout(Stdio::null()) 624 655 .output() 625 656 { 626 657 tracing::error!(?self, ?error, "failed to remove temporary worktree"); 627 658 } 659 + } 660 + } 661 + 662 + #[derive(Clone, Debug)] 663 + pub struct TempWorktreeBuilder<'a> { 664 + prefix: Option<&'a str>, 665 + 666 + // Commit object ID to create the worktree from. 667 + commit: Option<&'a ObjectId>, 668 + 669 + /// Path to the global git config. 670 + config: Option<&'a Path>, 671 + } 672 + 673 + impl<'a> TempWorktreeBuilder<'a> { 674 + pub const fn new() -> Self { 675 + Self { 676 + prefix: None, 677 + commit: None, 678 + config: None, 679 + } 680 + } 681 + 682 + /// Set a prefix for the randomly generated worktree name. 683 + pub const fn prefix(&mut self, prefix: &'a str) -> &mut Self { 684 + self.prefix = Some(prefix); 685 + self 686 + } 687 + 688 + /// Set a commit id to create the worktree from. 689 + pub const fn commit(&mut self, commit: &'a ObjectId) -> &mut Self { 690 + self.commit = Some(commit); 691 + self 692 + } 693 + 694 + /// Set the path to the global git config file. 695 + pub const fn config(&mut self, config: &'a Path) -> &mut Self { 696 + self.config = Some(config); 697 + self 698 + } 699 + 700 + /// Build the temporary worktree. 701 + /// 702 + /// # Errors 703 + /// 704 + /// Returns an error if the git subprocess could not be spawned or exits with a non-zero 705 + /// exit code. 706 + /// 707 + /// # Panics 708 + /// 709 + /// Panics if `repo` is not a bare repository. 710 + /// 711 + fn build<'repo>(&self, repo: &'repo gix::Repository) -> io::Result<TempWorktree<'repo>> { 712 + assert!(repo.is_bare(), "repository should be bare"); 713 + 714 + let mut name = 715 + String::with_capacity(self.prefix.map(|val| val.len() + 1).unwrap_or_default() + 13); 716 + 717 + let random_bytes: [u8; 8] = rand::random(); 718 + if let Some(prefix) = self.prefix { 719 + name.push_str(prefix); 720 + name.push('-'); 721 + } 722 + 723 + data_encoding::BASE32_NOPAD_VISUAL.encode_append(&random_bytes, &mut name); 724 + name = name.to_lowercase(); 725 + 726 + let commit = self.commit.map(ToString::to_string); 727 + let config = self.config; 728 + let output = Command::new("/usr/bin/git") 729 + .env_clear() 730 + .current_dir(repo.path()) 731 + .option_env("GIT_GLOBAL_CONFIG", config) 732 + .arg("-C") 733 + .arg(repo.path()) 734 + .arg("worktree") 735 + .arg("add") 736 + .arg("--detach") 737 + .arg(&name) 738 + .option_arg(commit) 739 + .stderr(Stdio::piped()) 740 + .stdout(Stdio::null()) 741 + .output()?; 742 + 743 + if !output.status.success() { 744 + let message = String::from_utf8_lossy(&output.stderr); 745 + return Err(io::Error::other(format!( 746 + "Failed to create temporary worktree: {message}" 747 + ))); 748 + } 749 + 750 + let path = repo.path().join(&name); 751 + Ok(TempWorktree { 752 + repo, 753 + name, 754 + path, 755 + config: config.map(Path::to_path_buf), 756 + }) 628 757 } 629 758 } 630 759 ··· 868 797 fn language_breakdown(&self, at: &ObjectId) -> BTreeMap<String, u64> { 869 798 let mut languages = BTreeMap::new(); 870 799 871 - let Ok(worktree) = TempWorktree::new_from(&self, None, Some(at)) 872 - .inspect_err(|error| tracing::error!(?error, "error creating temporary worktree")) 873 - else { 874 - return languages; 800 + let worktree = TempWorktree::builder() 801 + .prefix("languages-scan") 802 + .commit(at) 803 + .build(&self); 804 + 805 + let worktree = match worktree { 806 + Err(error) => { 807 + tracing::error!(?error, "error creating temporary worktree"); 808 + return languages; 809 + } 810 + Ok(worktree) => worktree, 875 811 }; 876 812 877 813 let mut to_scan = VecDeque::new();
+6 -6
crates/knot/src/model/repository/merge_check.rs
··· 24 24 let ResolvedRevspec { commit, immutable } = 25 25 self.repository.resolve_revspec(&Some(branch.as_ref()))?; 26 26 27 - let worktree = TempWorktree::new_from( 28 - &self.repository, 29 - Some(&self.knot.git_config), 30 - Some(&commit.id), 31 - ) 32 - .map_err(errors::Internal)?; 27 + let worktree = TempWorktree::builder() 28 + .prefix("merge-check") 29 + .config(&self.knot.git_config_path()) 30 + .commit(&commit.id) 31 + .build(&self.repository) 32 + .map_err(errors::Internal)?; 33 33 34 34 let mut child = self 35 35 .git()