use crate::AppService; use crate::error::AppError; use crate::git::{TreeEntry, TreeInfo}; use redis::AsyncCommands; use serde::{Deserialize, Serialize}; use session::Session; #[derive(Debug, Clone, Deserialize)] pub struct TreeGetQuery { #[serde(default)] pub oid: String, } #[derive(Debug, Clone, Deserialize)] pub struct TreeEntryQuery { #[serde(default)] pub oid: String, pub index: usize, } #[derive(Debug, Clone, Deserialize)] pub struct TreeEntryByPathQuery { #[serde(default)] pub oid: String, pub path: String, } #[derive(Debug, Clone, Deserialize)] pub struct TreeEntryByCommitPathQuery { pub commit: String, pub path: String, } #[derive(Debug, Clone, Deserialize)] pub struct TreeDiffQuery { pub old_tree: String, pub new_tree: String, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct TreeInfoResponse { pub oid: String, pub entry_count: usize, pub is_empty: bool, } impl From for TreeInfoResponse { fn from(t: TreeInfo) -> Self { Self { oid: t.oid.to_string(), entry_count: t.entry_count, is_empty: t.is_empty, } } } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct TreeEntryResponse { pub name: String, pub oid: String, pub kind: String, pub filemode: u32, pub is_binary: bool, } impl From for TreeEntryResponse { fn from(e: TreeEntry) -> Self { Self { name: e.name, oid: e.oid.to_string(), kind: e.kind, filemode: e.filemode, is_binary: e.is_binary, } } } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct TreeExistsResponse { pub oid: String, pub exists: bool, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct TreeEntryCountResponse { pub oid: String, pub count: usize, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct TreeIsEmptyResponse { pub oid: String, pub is_empty: bool, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct TreeDiffStatsResponse { pub old_tree: String, pub new_tree: String, pub files_changed: usize, pub insertions: usize, pub deletions: usize, } impl AppService { pub async fn git_tree_get( &self, namespace: String, repo_name: String, query: TreeGetQuery, ctx: &Session, ) -> Result { let repo = self.utils_find_repo(namespace, repo_name, ctx).await?; let oid_str = query.oid.clone(); let info = tokio::task::spawn_blocking(move || { let domain = git::GitDomain::from_model(repo)?; let oid = git::CommitOid::new(&oid_str); domain.tree_get(&oid) }) .await .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))? .map_err(AppError::from)?; Ok(TreeInfoResponse::from(info)) } pub async fn git_tree_exists( &self, namespace: String, repo_name: String, query: TreeGetQuery, ctx: &Session, ) -> Result { let repo = self.utils_find_repo(namespace, repo_name, ctx).await?; let oid_str = query.oid.clone(); let exists = tokio::task::spawn_blocking(move || { let domain = git::GitDomain::from_model(repo)?; let oid = git::CommitOid::new(&oid_str); Ok::<_, git::GitError>(domain.tree_exists(&oid)) }) .await .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))? .map_err(AppError::from)?; Ok(TreeExistsResponse { oid: query.oid, exists, }) } pub async fn git_tree_list( &self, namespace: String, repo_name: String, query: TreeGetQuery, ctx: &Session, ) -> Result, AppError> { let repo = self .utils_find_repo(namespace.clone(), repo_name.clone(), ctx) .await?; let cache_key = format!("git:tree:list:{}:{}:{}", namespace, repo_name, query.oid); if let Ok(mut conn) = self.cache.conn().await { if let Ok(cached) = conn.get::<_, String>(cache_key.clone()).await { if let Ok(cached) = serde_json::from_str(&cached) { return Ok(cached); } } } let oid_str = query.oid.clone(); let entries = tokio::task::spawn_blocking(move || { let domain = git::GitDomain::from_model(repo)?; let oid = git::CommitOid::new(&oid_str); domain.tree_list(&oid) }) .await .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))? .map_err(AppError::from)?; let response: Vec = entries.into_iter().map(TreeEntryResponse::from).collect(); if let Ok(mut conn) = self.cache.conn().await { if let Err(e) = conn .set_ex::( cache_key, serde_json::to_string(&response).unwrap_or_default(), 60 * 60, ) .await { slog::debug!(self.logs, "cache set failed (non-fatal): {}", e); } } Ok(response) } pub async fn git_tree_entry( &self, namespace: String, repo_name: String, query: TreeEntryQuery, ctx: &Session, ) -> Result { let repo = self.utils_find_repo(namespace, repo_name, ctx).await?; let oid_str = query.oid.clone(); let index = query.index; let entry = tokio::task::spawn_blocking(move || { let domain = git::GitDomain::from_model(repo)?; let oid = git::CommitOid::new(&oid_str); domain.tree_entry(&oid, index) }) .await .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))? .map_err(AppError::from)?; Ok(TreeEntryResponse::from(entry)) } pub async fn git_tree_entry_by_path( &self, namespace: String, repo_name: String, query: TreeEntryByPathQuery, ctx: &Session, ) -> Result { let repo = self.utils_find_repo(namespace, repo_name, ctx).await?; let oid_str = query.oid.clone(); let path = query.path.clone(); let entry = tokio::task::spawn_blocking(move || { let domain = git::GitDomain::from_model(repo)?; let oid = git::CommitOid::new(&oid_str); domain.tree_entry_by_path(&oid, &path) }) .await .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))? .map_err(AppError::from)?; Ok(TreeEntryResponse::from(entry)) } pub async fn git_tree_entry_by_commit_path( &self, namespace: String, repo_name: String, query: TreeEntryByCommitPathQuery, ctx: &Session, ) -> Result { let repo = self.utils_find_repo(namespace, repo_name, ctx).await?; let commit_str = query.commit.clone(); let path = query.path.clone(); let entry = tokio::task::spawn_blocking(move || { let domain = git::GitDomain::from_model(repo)?; let commit = git::CommitOid::new(&commit_str); domain.tree_entry_by_path_from_commit(&commit, &path) }) .await .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))? .map_err(AppError::from)?; Ok(TreeEntryResponse::from(entry)) } pub async fn git_tree_entry_count( &self, namespace: String, repo_name: String, query: TreeGetQuery, ctx: &Session, ) -> Result { let repo = self.utils_find_repo(namespace, repo_name, ctx).await?; let oid_str = query.oid.clone(); let count = tokio::task::spawn_blocking(move || { let domain = git::GitDomain::from_model(repo)?; let oid = git::CommitOid::new(&oid_str); domain.tree_entry_count(&oid) }) .await .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))? .map_err(AppError::from)?; Ok(TreeEntryCountResponse { oid: query.oid, count, }) } pub async fn git_tree_is_empty( &self, namespace: String, repo_name: String, query: TreeGetQuery, ctx: &Session, ) -> Result { let repo = self.utils_find_repo(namespace, repo_name, ctx).await?; let oid_str = query.oid.clone(); let is_empty = tokio::task::spawn_blocking(move || { let domain = git::GitDomain::from_model(repo)?; let oid = git::CommitOid::new(&oid_str); domain.tree_is_empty(&oid) }) .await .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))? .map_err(AppError::from)?; Ok(TreeIsEmptyResponse { oid: query.oid, is_empty, }) } pub async fn git_tree_diffstats( &self, namespace: String, repo_name: String, query: TreeDiffQuery, ctx: &Session, ) -> Result { let repo = self.utils_find_repo(namespace, repo_name, ctx).await?; let old_tree_str = query.old_tree.clone(); let new_tree_str = query.new_tree.clone(); let stats = tokio::task::spawn_blocking(move || { let domain = git::GitDomain::from_model(repo)?; let old_tree = git::CommitOid::new(&old_tree_str); let new_tree = git::CommitOid::new(&new_tree_str); domain.tree_diffstats(&old_tree, &new_tree) }) .await .map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))? .map_err(AppError::from)?; Ok(TreeDiffStatsResponse { old_tree: query.old_tree, new_tree: query.new_tree, files_changed: stats.files_changed, insertions: stats.insertions, deletions: stats.deletions, }) } }