Skip to main content

ward/github/
contents.rs

1use anyhow::{Context, Result};
2use serde::Deserialize;
3
4use super::Client;
5
6#[derive(Debug, Deserialize)]
7pub struct FileContent {
8    pub name: String,
9    pub path: String,
10    pub sha: String,
11    #[serde(default)]
12    pub content: Option<String>,
13    pub encoding: Option<String>,
14}
15
16impl Client {
17    /// Get a file's content from a repo. Returns None if the file doesn't exist.
18    pub async fn get_file(
19        &self,
20        repo: &str,
21        path: &str,
22        branch: Option<&str>,
23    ) -> Result<Option<FileContent>> {
24        let mut url = format!("/repos/{}/{repo}/contents/{path}", self.org);
25        if let Some(branch) = branch {
26            url.push_str(&format!("?ref={branch}"));
27        }
28
29        let resp = self.get(&url).await?;
30
31        match resp.status().as_u16() {
32            200 => {
33                let content = resp
34                    .json()
35                    .await
36                    .context("Failed to parse file content response")?;
37                Ok(Some(content))
38            }
39            404 => Ok(None),
40            status => {
41                let body = resp.text().await.unwrap_or_default();
42                anyhow::bail!("Failed to get file {path} in {repo} (HTTP {status}): {body}");
43            }
44        }
45    }
46
47    /// Decode base64-encoded file content from the Contents API.
48    pub fn decode_content(content: &FileContent) -> Result<String> {
49        let raw = content.content.as_deref().unwrap_or("");
50        let cleaned: String = raw.chars().filter(|c| !c.is_whitespace()).collect();
51        let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &cleaned)
52            .context("Failed to decode base64 content")?;
53        String::from_utf8(bytes).context("File content is not valid UTF-8")
54    }
55}