Skip to main content

facto_core/forges/
manager.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::config::Config;
5use crate::forges::*;
6use crate::http;
7
8pub struct ForgeManager {
9    forges: HashMap<String, Arc<dyn Forge>>,
10}
11
12impl ForgeManager {
13    pub fn new(config: &Config) -> Result<Self, http::HttpClientError> {
14        let mut forges: HashMap<String, Arc<dyn Forge>> = HashMap::new();
15        let default = http::default_client()?;
16
17        for id in &config.forges.enabled {
18            let forge: Option<Arc<dyn Forge>> = match id.as_str() {
19                "github" => {
20                    let c = match &config.forges.github_token {
21                        Some(t) => http::bearer_client(t)?,
22                        None => default.clone(),
23                    };
24                    Some(Arc::new(crate::forges::github::GitHub::new(c)))
25                }
26                "gitlab" => {
27                    let c = match &config.forges.gitlab_token {
28                        Some(t) => http::private_token_client(t)?,
29                        None => default.clone(),
30                    };
31                    Some(Arc::new(crate::forges::gitlab::GitLab::new(
32                        c,
33                        &config.forges.gitlab_base_url,
34                        "gitlab",
35                        "GitLab",
36                    )))
37                }
38                "codeberg" => Some(Arc::new(crate::forges::codeberg::Codeberg::new(
39                    default.clone(),
40                ))),
41                _ => {
42                    tracing::warn!("unknown forge: {}", id);
43                    None
44                }
45            };
46
47            if let Some(f) = forge {
48                forges.insert(id.clone(), f);
49            }
50        }
51
52        Ok(Self { forges })
53    }
54
55    pub fn list_forges(&self) -> Vec<(String, String)> {
56        self.forges
57            .iter()
58            .map(|(id, f)| (id.clone(), f.display_name().to_string()))
59            .collect()
60    }
61
62    pub fn get_forge(&self, id: &str) -> Option<&Arc<dyn Forge>> {
63        self.forges.get(id)
64    }
65
66    /// Resolve a GitHub Action (owner/repo) to a pinned commit SHA. When
67    /// `tag` is None, the latest stable (non-prerelease, non-draft) release
68    /// is used.
69    pub async fn pin_github_action(
70        &self,
71        action: &str,
72        tag: Option<&str>,
73    ) -> Result<crate::models::ActionPin, crate::forges::ForgeError> {
74        let parts: Vec<&str> = action.splitn(2, '/').collect();
75        if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
76            return Err(crate::forges::ForgeError::Parse(
77                "action must be in owner/repo format (e.g. \"actions/checkout\")".into(),
78            ));
79        }
80        let (owner, repo) = (parts[0], parts[1]);
81
82        let forge = self
83            .get_forge("github")
84            .ok_or(crate::forges::ForgeError::NotSupported)?;
85
86        let tag = match tag {
87            Some(t) => t.to_string(),
88            None => {
89                let releases = forge.list_releases(owner, repo, 20).await?;
90                releases
91                    .into_iter()
92                    .find(|r| !r.prerelease && !r.draft)
93                    .map(|r| r.tag)
94                    .ok_or_else(|| {
95                        crate::forges::ForgeError::Parse(format!(
96                            "no stable release found for {action}"
97                        ))
98                    })?
99            }
100        };
101
102        let pin = forge.resolve_tag(owner, repo, &tag).await?;
103        let pinned = format!("{}@{} # {}", action, pin.commit_sha, tag);
104        Ok(crate::models::ActionPin {
105            pinned,
106            action: action.to_string(),
107            tag,
108            commit_sha: pin.commit_sha,
109            url: pin.url,
110        })
111    }
112}