a73x

src/server/http/repo/tree.rs

Ref:   Size: 7.0 KiB

use std::sync::Arc;

use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response};

use super::{AppState, collab_counts, head_branch_name, internal_error, list_branches, not_found, open_repo};

#[derive(Debug)]
pub struct TreeEntry {
    pub name: String,
    pub full_path: String,
    pub is_dir: bool,
}

#[derive(askama::Template, askama_web::WebTemplate)]
#[template(path = "tree.html")]
pub struct TreeTemplate {
    pub site_title: String,
    pub repo_name: String,
    pub active_section: String,
    pub open_patches: usize,
    pub open_issues: usize,
    pub ref_name: String,
    pub branches: Vec<String>,
    pub path_display: String,
    pub show_parent: bool,
    pub parent_path: String,
    pub entries: Vec<TreeEntry>,
}

#[derive(askama::Template, askama_web::WebTemplate)]
#[template(path = "blob.html")]
pub struct BlobTemplate {
    pub site_title: String,
    pub repo_name: String,
    pub active_section: String,
    pub open_patches: usize,
    pub open_issues: usize,
    pub ref_name: String,
    pub branches: Vec<String>,
    pub file_path: String,
    pub content: String,
    pub is_binary: bool,
    pub size_display: String,
}

/// Split "ref/path/to/file" — first segment is ref, rest joined is path.
/// Returns ("HEAD", "") for empty input.
fn split_ref_path(input: &str) -> (String, String) {
    if input.is_empty() {
        return ("HEAD".to_string(), String::new());
    }
    let parts: Vec<&str> = input.splitn(2, '/').collect();
    let ref_name = parts[0].to_string();
    let path = if parts.len() > 1 { parts[1].to_string() } else { String::new() };
    (ref_name, path)
}

fn build_tree_response(
    state: &AppState,
    repo_name: String,
    repo: &git2::Repository,
    ref_name: String,
    path: String,
) -> Response {
    let (open_patches, open_issues) = collab_counts(repo);
    let branches = list_branches(repo);
    let ref_name = if ref_name == "HEAD" { head_branch_name(repo) } else { ref_name };

    let obj = match repo.revparse_single(&ref_name) {
        Ok(o) => o,
        Err(_) => return not_found(state, format!("Ref '{}' not found.", ref_name)),
    };

    let commit = match obj.peel_to_commit() {
        Ok(c) => c,
        Err(_) => return not_found(state, "Could not resolve ref to a commit."),
    };

    let root_tree = match commit.tree() {
        Ok(t) => t,
        Err(_) => return internal_error(state, "Failed to get commit tree."),
    };

    let tree = if path.is_empty() {
        root_tree
    } else {
        let entry = match root_tree.get_path(std::path::Path::new(&path)) {
            Ok(e) => e,
            Err(_) => return not_found(state, format!("Path '{}' not found.", path)),
        };
        let obj = match entry.to_object(repo) {
            Ok(o) => o,
            Err(_) => return internal_error(state, "Failed to resolve path object."),
        };
        match obj.into_tree() {
            Ok(t) => t,
            Err(_) => return not_found(state, "Path is not a directory."),
        }
    };

    let mut entries: Vec<TreeEntry> = tree
        .iter()
        .map(|e| {
            let name = e.name().unwrap_or("").to_string();
            let full_path = if path.is_empty() {
                name.clone()
            } else {
                format!("{}/{}", path, name)
            };
            let is_dir = e.kind() == Some(git2::ObjectType::Tree);
            TreeEntry { name, full_path, is_dir }
        })
        .collect();

    entries.sort_by(|a, b| {
        match (a.is_dir, b.is_dir) {
            (true, false) => std::cmp::Ordering::Less,
            (false, true) => std::cmp::Ordering::Greater,
            _ => a.name.cmp(&b.name),
        }
    });

    let (show_parent, parent_path) = if path.is_empty() {
        (false, String::new())
    } else {
        let parent = path.rfind('/').map(|i| &path[..i]).unwrap_or("").to_string();
        (true, parent)
    };

    TreeTemplate {
        site_title: state.site_title.clone(),
        repo_name,
        active_section: "tree".to_string(),
        open_patches,
        open_issues,
        ref_name,
        branches,
        path_display: path,
        show_parent,
        parent_path,
        entries,
    }
    .into_response()
}

pub async fn tree_root(
    Path(repo_name): Path<String>,
    State(state): State<Arc<AppState>>,
) -> Response {
    let (_entry, repo) = match open_repo(&state, &repo_name) {
        Ok(r) => r,
        Err(resp) => return resp,
    };

    build_tree_response(&state, repo_name, &repo, "HEAD".to_string(), String::new())
}

pub async fn tree(
    Path((repo_name, rest)): Path<(String, String)>,
    State(state): State<Arc<AppState>>,
) -> Response {
    let (_entry, repo) = match open_repo(&state, &repo_name) {
        Ok(r) => r,
        Err(resp) => return resp,
    };

    let (ref_name, path) = split_ref_path(&rest);
    build_tree_response(&state, repo_name, &repo, ref_name, path)
}

pub async fn blob(
    Path((repo_name, rest)): Path<(String, String)>,
    State(state): State<Arc<AppState>>,
) -> Response {
    let (_entry, repo) = match open_repo(&state, &repo_name) {
        Ok(r) => r,
        Err(resp) => return resp,
    };

    let (open_patches, open_issues) = collab_counts(&repo);
    let branches = list_branches(&repo);
    let (ref_name, file_path) = split_ref_path(&rest);
    let ref_name = if ref_name == "HEAD" { head_branch_name(&repo) } else { ref_name };

    let obj = match repo.revparse_single(&ref_name) {
        Ok(o) => o,
        Err(_) => return not_found(&state, format!("Ref '{}' not found.", ref_name)),
    };

    let commit = match obj.peel_to_commit() {
        Ok(c) => c,
        Err(_) => return not_found(&state, "Could not resolve ref to a commit."),
    };

    let tree = match commit.tree() {
        Ok(t) => t,
        Err(_) => return internal_error(&state, "Failed to get commit tree."),
    };

    let entry_obj = match tree.get_path(std::path::Path::new(&file_path)) {
        Ok(e) => e,
        Err(_) => return not_found(&state, format!("File '{}' not found.", file_path)),
    };

    let obj = match entry_obj.to_object(&repo) {
        Ok(o) => o,
        Err(_) => return internal_error(&state, "Failed to resolve file object."),
    };

    let blob = match obj.into_blob() {
        Ok(b) => b,
        Err(_) => return not_found(&state, "Path is not a file."),
    };

    let is_binary = blob.is_binary();
    let size = blob.size();
    let size_display = if size < 1024 {
        format!("{} B", size)
    } else if size < 1024 * 1024 {
        format!("{:.1} KiB", size as f64 / 1024.0)
    } else {
        format!("{:.1} MiB", size as f64 / (1024.0 * 1024.0))
    };

    let content = if is_binary {
        String::new()
    } else {
        String::from_utf8_lossy(blob.content()).into_owned()
    };

    BlobTemplate {
        site_title: state.site_title.clone(),
        repo_name,
        active_section: "tree".to_string(),
        open_patches,
        open_issues,
        ref_name,
        branches,
        file_path,
        content,
        is_binary,
        size_display,
    }
    .into_response()
}