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