1use std::path::PathBuf;
4
5use tracing::debug;
6
7use crate::error::{Result, SpsError};
8
9pub struct Tap {
11 pub user: String,
13
14 pub repo: String,
16
17 pub path: PathBuf,
19}
20
21impl Tap {
22 pub fn new(name: &str) -> Result<Self> {
24 let parts: Vec<&str> = name.split('/').collect();
25 if parts.len() != 2 {
26 return Err(SpsError::Generic(format!("Invalid tap name: {name}")));
27 }
28 let user = parts[0].to_string();
29 let repo = parts[1].to_string();
30 let prefix = if cfg!(target_arch = "aarch64") {
31 PathBuf::from("/opt/homebrew")
32 } else {
33 PathBuf::from("/usr/local")
34 };
35 let path = prefix
36 .join("Library/Taps")
37 .join(&user)
38 .join(format!("homebrew-{repo}"));
39 Ok(Self { user, repo, path })
40 }
41
42 pub fn update(&self) -> Result<()> {
44 use git2::{FetchOptions, Repository};
45
46 let repo = Repository::open(&self.path)
47 .map_err(|e| SpsError::Generic(format!("Failed to open tap repository: {e}")))?;
48
49 let mut remote = repo
51 .find_remote("origin")
52 .map_err(|e| SpsError::Generic(format!("Failed to find remote 'origin': {e}")))?;
53
54 let mut fetch_options = FetchOptions::new();
55 remote
56 .fetch(
57 &["refs/heads/*:refs/heads/*"],
58 Some(&mut fetch_options),
59 None,
60 )
61 .map_err(|e| SpsError::Generic(format!("Failed to fetch updates: {e}")))?;
62
63 let fetch_head = repo
65 .find_reference("FETCH_HEAD")
66 .map_err(|e| SpsError::Generic(format!("Failed to find FETCH_HEAD: {e}")))?;
67
68 let fetch_commit = repo
69 .reference_to_annotated_commit(&fetch_head)
70 .map_err(|e| SpsError::Generic(format!("Failed to get commit from FETCH_HEAD: {e}")))?;
71
72 let analysis = repo
73 .merge_analysis(&[&fetch_commit])
74 .map_err(|e| SpsError::Generic(format!("Failed to analyze merge: {e}")))?;
75
76 if analysis.0.is_up_to_date() {
77 debug!("Already up-to-date");
78 return Ok(());
79 }
80
81 if analysis.0.is_fast_forward() {
82 let mut reference = repo
83 .find_reference("refs/heads/master")
84 .map_err(|e| SpsError::Generic(format!("Failed to find master branch: {e}")))?;
85 reference
86 .set_target(fetch_commit.id(), "Fast-forward")
87 .map_err(|e| SpsError::Generic(format!("Failed to fast-forward: {e}")))?;
88 repo.set_head("refs/heads/master")
89 .map_err(|e| SpsError::Generic(format!("Failed to set HEAD: {e}")))?;
90 repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))
91 .map_err(|e| SpsError::Generic(format!("Failed to checkout: {e}")))?;
92 } else {
93 return Err(SpsError::Generic(
94 "Tap requires merge but automatic merging is not implemented".to_string(),
95 ));
96 }
97
98 Ok(())
99 }
100
101 pub fn remove(&self) -> Result<()> {
103 if !self.path.exists() {
104 return Err(SpsError::NotFound(format!(
105 "Tap {} is not installed",
106 self.full_name()
107 )));
108 }
109 debug!("Removing tap {}", self.full_name());
110 std::fs::remove_dir_all(&self.path).map_err(|e| {
111 SpsError::Generic(format!("Failed to remove tap {}: {}", self.full_name(), e))
112 })
113 }
114
115 pub fn full_name(&self) -> String {
117 format!("{}/{}", self.user, self.repo)
118 }
119
120 pub fn is_installed(&self) -> bool {
122 self.path.exists()
123 }
124}