sps_common/model/
tap.rs

1// tap/tap.rs - Basic tap functionality // Should probably be in model module
2
3use std::path::PathBuf;
4
5use tracing::debug;
6
7use crate::error::{Result, SpsError};
8
9/// Represents a source of packages (formulas and casks)
10pub struct Tap {
11    /// The user part of the tap name (e.g., "homebrew" in "homebrew/core")
12    pub user: String,
13
14    /// The repository part of the tap name (e.g., "core" in "homebrew/core")
15    pub repo: String,
16
17    /// The full path to the tap directory
18    pub path: PathBuf,
19}
20
21impl Tap {
22    /// Create a new tap from user/repo format
23    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    /// Update this tap by pulling latest changes
43    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        // Fetch updates from origin
50        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        // Merge changes
64        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    /// Remove this tap by deleting its local repository
102    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    /// Get the full name of the tap (user/repo)
116    pub fn full_name(&self) -> String {
117        format!("{}/{}", self.user, self.repo)
118    }
119
120    /// Check if this tap is installed locally
121    pub fn is_installed(&self) -> bool {
122        self.path.exists()
123    }
124}