Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-ui-web/src/lfs.rs

//! **A file kept in Git LFS, in the file view** (2026-10-15): what the
//! server says about it, the type word for its header, and the raw route's
//! URL for its bytes. Host-testable, unlike `pages::browse`, which draws it.

use serde::Deserialize;

/// A file kept in Git LFS, as the server describes it.
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct LfsFile {
    pub sha256: String,
    /// `image` | `pdf` | `text` | `download`.
    pub shown: String,
    /// The pointer the tree holds — a detail, never the content.
    pub pointer: String,
}

/// The type word in an LFS file's header: what the file IS, not "Plain Text"
/// for its pointer.
pub fn lfs_type_label(shown: &str, path: &str, syntax: Option<&str>) -> String {
    match shown {
        "pdf" => "PDF".to_owned(),
        "image" => {
            let ext = path.rsplit_once('.').map(|(_, e)| e.to_ascii_uppercase());
            format!("{} image", ext.unwrap_or_default())
                .trim()
                .to_owned()
        }
        "text" => syntax.unwrap_or("Text").to_owned(),
        _ => "Binary".to_owned(),
    }
}

/// `/api/repos/{ns}/{repo}/raw?path=…[&rev=…]`: the file's bytes, which the
/// raw route serves (streamed past one gunnar answer) with its own type.
pub fn raw_url(ns: &str, repo: &str, rev: &str, path: &str) -> String {
    let mut url = format!(
        "/api/repos/{}/{}/raw?path={}",
        crate::auth::percent_encode(ns).replace('/', "%2F"),
        crate::auth::percent_encode(repo).replace('/', "%2F"),
        crate::auth::percent_encode(path)
    );
    if !rev.is_empty() {
        url.push_str("&rev=");
        url.push_str(&crate::auth::percent_encode(rev));
    }
    url
}

#[cfg(test)]
mod tests {
    use super::*;

    /// ★ The header says what the FILE is ("186 KB · PDF · stored in Git
    /// LFS"), not "Plain Text" for the pointer (gunnar.rs, 2026-10-15).
    #[test]
    fn an_lfs_file_s_type_is_the_file_s() {
        assert_eq!(lfs_type_label("pdf", "docs/idiot_guide.pdf", None), "PDF");
        assert_eq!(
            lfs_type_label("image", "assets/knut.png", None),
            "PNG image"
        );
        assert_eq!(lfs_type_label("text", "data/a.csv", Some("CSV")), "CSV");
        assert_eq!(lfs_type_label("text", "data/a", None), "Text");
        assert_eq!(lfs_type_label("download", "a.bin", None), "Binary");
    }

    #[test]
    fn the_raw_url_names_the_file_and_the_revision_only_when_there_is_one() {
        assert_eq!(
            raw_url("mikronova", "modgunn", "", "docs/idiot_guide.pdf"),
            format!(
                "/api/repos/mikronova/modgunn/raw?path={}",
                crate::auth::percent_encode("docs/idiot_guide.pdf")
            )
        );
        assert!(raw_url("a", "b", "main", "x.pdf").ends_with("&rev=main"));
    }

    /// The server's `lfs` part reads, and a blob with none (an older server)
    /// leaves it absent rather than failing — the shape `pages::browse::Blob`
    /// carries it in.
    #[test]
    fn the_lfs_part_reads_and_its_absence_is_none() {
        #[derive(serde::Deserialize)]
        struct Carrier {
            #[serde(default)]
            lfs: Option<LfsFile>,
        }
        let with: Carrier = serde_json::from_value(serde_json::json!({
            "lfs": {"sha256": "fd", "shown": "pdf", "pointer": "version …"}
        }))
        .expect("with lfs");
        assert_eq!(with.lfs.expect("lfs").shown, "pdf");
        let without: Carrier = serde_json::from_value(serde_json::json!({})).expect("without");
        assert!(without.lfs.is_none());
    }
}