use std::path::PathBuf; use crate::rpcgit::commit::types::{TreeEntry, TreeEntryMode}; use crate::rpcgit::tree::types::WalkTreeOptions; use serde::{Deserialize, Serialize}; use crate::context::facade::GitContext; use crate::service::GitService; use crate::service::cache::{self, IMMUTABLE_TTL}; use crate::share::errors::{GitError, map_baby}; use crate::share::validate; #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct PathLastUpdated { pub path: String, pub object_type: i8, pub last_updated: i64, pub commit_id: String, } fn entry_full_path(entry: &TreeEntry) -> String { let mut s = entry.name.clone(); if entry.mode == TreeEntryMode::Tree { s.push('/'); } s } impl GitService { pub async fn tree_read( &self, ctx: &GitContext, revision: String, path: PathBuf, ) -> Result, GitError> { validate::validate_revision(&revision)?; let baby = self.baby(&ctx); let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?; let key = format!( "repo.{}.tree_read.{}.{}", ctx.model.id, rev_oid, path.display() ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { baby.get_tree_entries(&revision, &path) .await .map_err(map_baby) }) .await } pub async fn tree_resolve_path( &self, ctx: &GitContext, revision: String, path: String, ) -> Result { validate::validate_revision(&revision)?; let baby = self.baby(&ctx); let name = path.trim().trim_start_matches("./").to_string(); let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?; let key = format!("repo.{}.tree_resolve.{}.{}", ctx.model.id, rev_oid, name); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { baby.tree_entry_at(&revision, std::path::Path::new(&name)) .await .map_err(map_baby)? .ok_or(GitError::NotFound) }) .await } pub async fn tree_walk( &self, ctx: &GitContext, revision: String, recurse: bool, show_trees: bool, ) -> Result, GitError> { validate::validate_revision(&revision)?; let baby = self.baby(&ctx); let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?; let key = format!( "repo.{}.tree_walk.{}:{}:{}", ctx.model.id, rev_oid, recurse, show_trees ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { baby.walk_tree( &revision, std::path::Path::new(""), WalkTreeOptions { recurse, show_trees, }, ) .await .map_err(map_baby) }) .await } pub async fn tree_follow( &self, ctx: &GitContext, revision: String, path: String, ) -> Result<(TreeEntry, Option, Option), GitError> { validate::validate_revision(&revision)?; let entry = self.tree_resolve_path(ctx, revision.clone(), path).await?; if entry.mode != TreeEntryMode::Symlink { return Ok((entry, None, None)); } let baby = self.baby(&ctx); let blob = baby .get_blob( entry.oid.clone(), crate::rpcgit::blob::types::GetBlobOptions { limit: Some(8192) }, ) .await .map_err(map_baby)?; let target = String::from_utf8_lossy(&blob.data).trim().to_string(); let mut target_path = PathBuf::from(&target); if target_path.is_relative() { let parent = std::path::Path::new(&entry.name) .parent() .map(|p| p.to_path_buf()) .unwrap_or_default(); target_path = parent.join(target_path); } let target_entry = baby .tree_entry_at(&revision, &target_path) .await .ok() .flatten() .filter(|_| !target.is_empty()); Ok((entry, Some(target), target_entry)) } pub async fn tree_last_updated( &self, ctx: &GitContext, revision: String, path: Option, ) -> Result, GitError> { validate::validate_revision(&revision)?; let baby = self.baby(&ctx); let rev_oid = baby.rev_parse(&revision).await.map_err(map_baby)?; let path_str = path.as_deref().unwrap_or(""); let key = format!( "repo.{}.tree_last_updated.{}.{}", ctx.model.id, rev_oid, path_str ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let rows = baby .tree_entries_with_latest(&revision, path.as_deref()) .await .map_err(map_baby)?; Ok(rows .into_iter() .map(|row| PathLastUpdated { path: entry_full_path(&row.entry), object_type: mode_to_i8(row.entry.mode), last_updated: row.latest.author.time.unix_timestamp(), commit_id: row.latest.id.to_string(), }) .collect()) }) .await } pub async fn tree_entries_with_latest_html( &self, ctx: &std::sync::Arc, revision: &str, ) -> Result, GitError> { validate::validate_revision(revision)?; if ctx.repo_uninitialized() { return Ok(Vec::new()); } let baby = self.baby(ctx); let rev_oid = baby.rev_parse(revision).await.map_err(map_baby)?; let key = format!("repo.{}.tree_entries_latest_html.{}", ctx.model.id, rev_oid); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { baby.tree_entries_with_latest(revision, None) .await .map_err(map_baby) }) .await } pub async fn tree_with_commits_html( &self, ctx: &std::sync::Arc, revision: &str, path: &str, ) -> Result, GitError> { validate::validate_revision(revision)?; if path.contains("..") || path.contains('\\') || path.starts_with('/') || path.contains('\0') { return Err(GitError::InvalidArgument("invalid path".into())); } if ctx.repo_uninitialized() { return Ok(Vec::new()); } let baby = self.baby(ctx); let rev_oid = baby.rev_parse(revision).await.map_err(map_baby)?; let key = format!( "repo.{}.tree_dir_entries_html.{}.{}", ctx.model.id, rev_oid, path ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let p = if path.is_empty() { None } else { Some(path) }; baby.tree_dir_entries_with_latest(revision, p) .await .map_err(map_baby) }) .await } } fn mode_to_i8(mode: TreeEntryMode) -> i8 { match mode { TreeEntryMode::Blob => 0, TreeEntryMode::Exec => 1, TreeEntryMode::Symlink => 2, TreeEntryMode::Tree => 3, TreeEntryMode::Submodule => 4, TreeEntryMode::Commit => 5, } }