use crate::rpcgit::diff::types::{ ChangedPath, ChangedPathDiff, ChangedPathRequest, DiffOptions, DiffRequest, NumStat, RangeDiffSpec, ShortStat, }; use crate::rpcgit::oid::ObjectId; use serde::{Deserialize, Serialize}; use crate::service::GitService; use crate::service::cache::{self, IMMUTABLE_TTL}; use crate::share::errors::{GitError, map_baby}; pub fn parse_oid(hex: &str) -> Result { ObjectId::from_hex(hex.trim().as_bytes()) .map_err(|_| GitError::InvalidArgument(format!("invalid object id `{hex}`"))) } #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct DiffOptionsDto { #[serde(default)] pub paths: Option>, #[serde(default)] pub detect_renames: Option, } impl From for DiffOptions { fn from(d: DiffOptionsDto) -> Self { Self { paths: d.paths, detect_renames: d.detect_renames, ..Default::default() } } } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct ChangedPathDto { pub old_path: Option, pub new_path: String, pub old_mode: u32, pub new_mode: u32, pub old_oid: Option, pub new_oid: Option, pub status: String, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct ChangedPathDiffDto { pub path: ChangedPathDto, pub lines_added: u64, pub lines_removed: u64, pub binary: bool, pub patch: Option, } impl From for ChangedPathDiffDto { fn from(c: ChangedPathDiff) -> Self { let patch = c.patch.and_then(|p| { if c.binary { None } else { Some(String::from_utf8_lossy(&p).into_owned()) } }); Self { path: ChangedPathDto { old_path: c.path.old_path.map(|p| p.to_string_lossy().into_owned()), new_path: c.path.new_path.to_string_lossy().into_owned(), old_mode: c.path.old_mode, new_mode: c.path.new_mode, old_oid: c.path.old_oid.map(|o| o.to_string()), new_oid: c.path.new_oid.map(|o| o.to_string()), status: format!("{:?}", c.path.status), }, lines_added: c.lines_added, lines_removed: c.lines_removed, binary: c.binary, patch, } } } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct DiffTreeRecordDto { pub status: String, pub score: u8, pub head_path: Option, pub base_path: Option, pub head_mode: Option, pub base_mode: Option, pub head_blob_id: Option, pub base_blob_id: Option, } impl GitService { async fn diff_request( &self, ctx: &crate::context::facade::GitContext, base: &str, head: &str, ) -> Result { let baby = self.baby(ctx); let empty_tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; let left = if base.trim() == empty_tree { ObjectId::from_hex(empty_tree.as_bytes()) .map_err(|_| GitError::InvalidArgument("empty tree oid".into()))? } else { baby.rev_parse(base).await.map_err(map_baby)? }; let right = if head.trim() == empty_tree { ObjectId::from_hex(empty_tree.as_bytes()) .map_err(|_| GitError::InvalidArgument("empty tree oid".into()))? } else { baby.rev_parse(head).await.map_err(map_baby)? }; Ok(DiffRequest { left, right }) } pub async fn diff_raw( &self, space_name: String, repo_name: String, base: String, head: String, ) -> Result { let ctx = self.context(space_name, repo_name).await?; let req = self.diff_request(&ctx, &base, &head).await?; let key = format!("repo.{}.diff_raw.{}.{}", ctx.model.id, req.left, req.right); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { self.baby(&ctx).raw_diff(&req, None).await.map_err(map_baby) }) .await } pub async fn diff_files_changed_between( &self, space_name: String, repo_name: String, base: String, head: String, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let baby = self.baby(&ctx); let left = baby.rev_parse(&base).await.map_err(map_baby)?; let right = baby.rev_parse(&head).await.map_err(map_baby)?; let key = format!( "repo.{}.diff_changed_paths.{}.{}", ctx.model.id, left, right ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let paths = baby .find_changed_paths( vec![ChangedPathRequest::TreePair(left, right)], None, None, false, ) .await .map_err(map_baby)?; Ok(paths .into_iter() .map(|p| p.new_path.to_string_lossy().into_owned()) .collect()) }) .await } pub async fn diff_short_stat( &self, space_name: String, repo_name: String, base: String, head: String, ) -> Result { let ctx = self.context(space_name, repo_name).await?; let req = self.diff_request(&ctx, &base, &head).await?; let key = format!( "repo.{}.diff_short_stat.{}.{}", ctx.model.id, req.left, req.right ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { self.baby(&ctx) .shortstat(&req, &DiffOptions::default()) .await .map_err(map_baby) }) .await } pub async fn diff_stats( &self, space_name: String, repo_name: String, base: String, head: String, opts: DiffOptionsDto, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let req = self.diff_request(&ctx, &base, &head).await?; let key = format!( "repo.{}.diff_stats.{}.{}:{:?}", ctx.model.id, req.left, req.right, opts.paths ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { self.baby(&ctx) .diff_stats(&req, &opts.into()) .await .map_err(map_baby) }) .await } pub async fn diff_files( &self, space_name: String, repo_name: String, base: String, head: String, opts: DiffOptionsDto, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let req = self.diff_request(&ctx, &base, &head).await?; let key = format!( "repo.{}.diff_files_dto.{}.{}:{:?}", ctx.model.id, req.left, req.right, opts.paths ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let list = self .baby(&ctx) .commit_diff(&req, &opts.into(), None) .await .map_err(map_baby)? .into_iter() .map(ChangedPathDiffDto::from) .collect::>(); Ok(list) }) .await } pub async fn commit_diff_for_html( &self, ctx: &std::sync::Arc, commit: &str, ) -> Result<(String, Vec), GitError> { const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; let baby = self.baby(ctx); let head_oid = baby.rev_parse(commit).await.map_err(map_baby)?; let info = baby .get_commit(&head_oid.to_string()) .await .map_err(map_baby)?; let base = info .parents .first() .map(|p| p.to_string()) .unwrap_or_else(|| EMPTY_TREE.to_string()); let req = self.diff_request(ctx, &base, &head_oid.to_string()).await?; let key = format!( "repo.{}.diff_files_dto.{}.{:?}", ctx.model.id, req.left, None::> ); let files = cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let list = baby .commit_diff(&req, &DiffOptions::default(), None) .await .map_err(map_baby)? .into_iter() .map(ChangedPathDiffDto::from) .collect::>(); Ok(list) }) .await?; Ok((base, files)) } pub async fn diff_for_commit( &self, space_name: String, repo_name: String, commit: String, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let oid = parse_oid(&commit)?; let key = format!("repo.{}.diff_for_commit.{}", ctx.model.id, oid); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { self.baby(&ctx) .find_changed_paths(vec![ChangedPathRequest::Commit(oid)], None, None, false) .await .map_err(map_baby) }) .await } pub async fn diff_tree( &self, space_name: String, repo_name: String, base: String, head: String, use_merge_base: bool, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let baby = self.baby(&ctx); let left = if use_merge_base { let mb = baby.merge_base(&base, &head).await.map_err(map_baby)?; mb.to_string() } else { baby.rev_parse(&base).await.map_err(map_baby)?.to_string() }; let right = baby.rev_parse(&head).await.map_err(map_baby)?.to_string(); let key = format!( "repo.{}.diff_tree.{}.{}:{}", ctx.model.id, left, right, use_merge_base ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let rows = baby .diff_trees( &left, &right, std::path::Path::new(""), crate::rpcgit::tree::types::DiffTreesOptions { recurse: true, detect_renames: true, ..Default::default() }, ) .await .map_err(map_baby)?; Ok(rows .into_iter() .map(|d| DiffTreeRecordDto { status: format!("{:?}", d.status).to_lowercase(), score: 0, head_path: d.new_path.map(|p| p.to_string_lossy().into_owned()), base_path: d.old_path.map(|p| p.to_string_lossy().into_owned()), head_mode: d.new_mode.map(|m| m as u32), base_mode: d.old_mode.map(|m| m as u32), head_blob_id: d.new_oid.map(|o| o.to_string()), base_blob_id: d.old_oid.map(|o| o.to_string()), }) .collect()) }) .await } pub async fn range_diff( &self, space_name: String, repo_name: String, spec: RangeDiffSpec, ) -> Result { let ctx = self.context(space_name, repo_name).await?; self.baby(&ctx) .range_diff(&spec, &DiffOptions::default(), None) .await .map_err(map_baby) } }