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());
}
}