gitdataai/libs/service/git/commit.rs
2026-04-15 09:08:09 +08:00

1663 lines
50 KiB
Rust

use crate::AppService;
use crate::error::AppError;
use crate::git::{
CommitDiffFile, CommitDiffHunk, CommitDiffStats, CommitGraph, CommitMeta, CommitRefInfo,
CommitReflogEntry, CommitSignature, CommitSort, CommitWalkOptions,
};
use models::repos::repo;
use redis::AsyncCommands;
use serde::{Deserialize, Serialize};
use session::Session;
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
pub struct CommitGetQuery {
#[serde(default)]
pub oid: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CommitMetaResponse {
pub oid: String,
pub message: String,
pub summary: String,
pub author: CommitSignatureResponse,
pub committer: CommitSignatureResponse,
pub tree_id: String,
pub parent_ids: Vec<String>,
pub encoding: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CommitSignatureResponse {
pub name: String,
pub email: String,
pub time_secs: i64,
pub offset_minutes: i32,
}
impl From<CommitSignature> for CommitSignatureResponse {
fn from(s: CommitSignature) -> Self {
Self {
name: s.name,
email: s.email,
time_secs: s.time_secs,
offset_minutes: s.offset_minutes,
}
}
}
impl From<CommitMeta> for CommitMetaResponse {
fn from(c: CommitMeta) -> Self {
Self {
oid: c.oid.to_string(),
message: c.message,
summary: c.summary,
author: CommitSignatureResponse::from(c.author),
committer: CommitSignatureResponse::from(c.committer),
tree_id: c.tree_id.to_string(),
parent_ids: c.parent_ids.into_iter().map(|p| p.to_string()).collect(),
encoding: c.encoding,
}
}
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitExistsResponse {
pub oid: String,
pub exists: bool,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitIsCommitResponse {
pub oid: String,
pub is_commit: bool,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitMessageResponse {
pub oid: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitSummaryResponse {
pub oid: String,
pub summary: String,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitShortIdResponse {
pub oid: String,
pub short_id: String,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitAuthorResponse {
pub oid: String,
pub author: CommitSignatureResponse,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitTreeIdResponse {
pub oid: String,
pub tree_id: String,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitParentCountResponse {
pub oid: String,
pub parent_count: usize,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitParentIdsResponse {
pub oid: String,
pub parent_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitIsMergeResponse {
pub oid: String,
pub is_merge: bool,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitIsTipResponse {
pub oid: String,
pub is_tip: bool,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitRefCountResponse {
pub oid: String,
pub ref_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CommitCountResponse {
pub count: usize,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitRefInfoResponse {
pub name: String,
pub target: String,
pub is_remote: bool,
pub is_tag: bool,
}
impl From<CommitRefInfo> for CommitRefInfoResponse {
fn from(r: CommitRefInfo) -> Self {
Self {
name: r.name,
target: r.target.to_string(),
is_remote: r.is_remote,
is_tag: r.is_tag,
}
}
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitBranchesResponse {
#[serde(default)]
pub data: std::collections::HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitTagsResponse {
#[serde(default)]
pub data: std::collections::HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitReflogEntryResponse {
pub new_sha: String,
pub old_sha: String,
pub committer_name: String,
pub committer_email: String,
pub time_secs: i64,
pub message: Option<String>,
pub ref_name: String,
}
impl From<CommitReflogEntry> for CommitReflogEntryResponse {
fn from(e: CommitReflogEntry) -> Self {
Self {
new_sha: e.oid_new.to_string(),
old_sha: e.oid_old.to_string(),
committer_name: e.committer_name,
committer_email: e.committer_email,
time_secs: e.time_secs,
message: e.message,
ref_name: e.ref_name,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CommitGraphResponse {
pub lines: Vec<CommitGraphLineResponse>,
pub max_parents: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CommitGraphLineResponse {
pub oid: String,
pub graph_chars: String,
pub refs: String,
pub short_message: String,
}
impl From<git::CommitGraphLine> for CommitGraphLineResponse {
fn from(l: git::CommitGraphLine) -> Self {
Self {
oid: l.oid.to_string(),
graph_chars: l.graph_chars,
refs: l.refs,
short_message: l.short_message,
}
}
}
impl From<CommitGraph> for CommitGraphResponse {
fn from(g: CommitGraph) -> Self {
Self {
lines: g
.lines
.into_iter()
.map(CommitGraphLineResponse::from)
.collect(),
max_parents: g.max_parents,
}
}
}
/// Response for the gitgraph-react compatible API endpoint.
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct LaneInfo {
/// 0-based lane index.
pub lane_index: usize,
/// Branch name if this lane has a branch tip (None for unnamed lanes).
pub branch_name: Option<String>,
/// SHA of the commit where this lane/branch starts.
pub start_oid: String,
/// SHA of the commit where this lane ends (None if lane continues to last commit).
pub end_oid: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CommitGraphReactResponse {
pub commits: Vec<CommitGraphReactCommit>,
pub lanes: Vec<LaneInfo>,
pub max_parents: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CommitGraphReactCommit {
pub oid: String,
pub hash_abbrev: String,
pub subject: String,
pub body: Option<String>,
pub author_name: String,
pub author_email: String,
pub author_timestamp: i64,
pub author_time_offset: i32,
pub committer_name: String,
pub committer_email: String,
pub committer_timestamp: i64,
pub committer_time_offset: i32,
pub parent_hashes: Vec<String>,
pub encoding: Option<String>,
/// 0-based lane index used to assign branch color.
pub lane_index: usize,
/// Raw ASCII graph characters for supplementary rendering.
pub graph_chars: String,
/// Parsed refs string (e.g. "main, v1.0.0").
pub refs: String,
/// Tag names present on this commit.
pub tags: Vec<String>,
/// Branch names this commit is the tip of.
pub branches: Vec<String>,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitDiffStatsResponse {
pub oid: String,
pub files_changed: usize,
pub insertions: usize,
pub deletions: usize,
}
impl From<CommitDiffStats> for CommitDiffStatsResponse {
fn from(s: CommitDiffStats) -> Self {
Self {
oid: s.oid,
files_changed: s.files_changed,
insertions: s.insertions,
deletions: s.deletions,
}
}
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitDiffFileResponse {
pub path: Option<String>,
pub status: String,
pub is_binary: bool,
pub size: u64,
}
impl From<CommitDiffFile> for CommitDiffFileResponse {
fn from(f: CommitDiffFile) -> Self {
Self {
path: f.path,
status: f.status,
is_binary: f.is_binary,
size: f.size,
}
}
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitDiffHunkResponse {
pub old_start: u32,
pub old_lines: u32,
pub new_start: u32,
pub new_lines: u32,
pub header: String,
}
impl From<CommitDiffHunk> for CommitDiffHunkResponse {
fn from(h: CommitDiffHunk) -> Self {
Self {
old_start: h.old_start,
old_lines: h.old_lines,
new_start: h.new_start,
new_lines: h.new_lines,
header: h.header,
}
}
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
pub struct CommitLogQuery {
pub rev: Option<String>,
/// Number of results per page (default 50, max 100).
#[serde(default = "default_per_page")]
pub per_page: usize,
/// Page number (1-indexed, default 1).
#[serde(default)]
pub page: usize,
}
fn default_per_page() -> usize {
50
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitLogResponse {
pub data: Vec<CommitMetaResponse>,
pub total: usize,
pub page: usize,
pub per_page: usize,
pub total_pages: usize,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
pub struct CommitWalkQuery {
pub rev: Option<String>,
pub limit: Option<usize>,
#[serde(default)]
pub first_parent_only: bool,
#[serde(default)]
pub topological: bool,
#[serde(default)]
pub reverse: bool,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
pub struct CommitAncestorsQuery {
pub oid: String,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
pub struct CommitDescendantsQuery {
pub oid: String,
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
pub struct CommitResolveQuery {
pub rev: String,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CommitCherryPickRequest {
pub cherrypick_oid: String,
pub author_name: String,
pub author_email: String,
pub committer_name: String,
pub committer_email: String,
pub message: Option<String>,
pub mainline: Option<u32>,
pub update_ref: Option<String>,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CommitCherryPickAbortRequest {
pub reset_type: Option<String>,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CommitRevertRequest {
pub revert_oid: String,
pub author_name: String,
pub author_email: String,
pub committer_name: String,
pub committer_email: String,
pub message: Option<String>,
pub mainline: Option<u32>,
pub update_ref: Option<String>,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CommitRevertAbortRequest {
pub reset_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct CommitCreateResponse {
pub oid: String,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CommitCreateRequest {
pub author_name: String,
pub author_email: String,
pub committer_name: String,
pub committer_email: String,
pub message: String,
pub tree_id: String,
pub parent_ids: Vec<String>,
pub update_ref: Option<String>,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CommitAmendRequest {
pub oid: String,
pub author_name: Option<String>,
pub author_email: Option<String>,
pub committer_name: Option<String>,
pub committer_email: Option<String>,
pub message: Option<String>,
pub message_encoding: Option<String>,
pub tree_id: Option<String>,
pub update_ref: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CommitDiffQuery {
pub oid: String,
}
macro_rules! git_spawn {
($repo:expr, $domain:ident -> $body:expr) => {{
let repo_clone = $repo.clone();
tokio::task::spawn_blocking(move || {
let $domain = git::GitDomain::from_model(repo_clone)?;
$body
})
.await
.map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
.map_err(AppError::from)
}};
}
impl AppService {
pub async fn git_commit_get(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitMetaResponse, AppError> {
let cache_key = format!("git:commit:get:{}:{}:{}", 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 repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let meta = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_get(&oid)
})?;
let response = CommitMetaResponse::from(meta);
if let Ok(mut conn) = self.cache.conn().await {
if let Err(e) = conn
.set_ex::<String, String, ()>(
cache_key,
serde_json::to_string(&response).unwrap_or_default(),
3600,
)
.await
{
slog::debug!(self.logs, "cache set failed (non-fatal): {}", e);
}
}
Ok(response)
}
pub async fn git_commit_exists(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitExistsResponse, AppError> {
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.commit_exists(&oid))
})
.await
.map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
.map_err(AppError::from)?;
Ok(CommitExistsResponse {
oid: query.oid,
exists,
})
}
pub async fn git_commit_is_commit(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitIsCommitResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let is_commit = tokio::task::spawn_blocking(move || {
let domain = git::GitDomain::from_model(repo)?;
let oid = git::CommitOid::new(&oid_str);
Ok::<_, git::GitError>(domain.commit_is_commit(&oid))
})
.await
.map_err(|e| AppError::InternalServerError(format!("Task join error: {}", e)))?
.map_err(AppError::from)?;
Ok(CommitIsCommitResponse {
oid: query.oid,
is_commit,
})
}
pub async fn git_commit_message(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitMessageResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let message = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_message(&oid)
})?;
Ok(CommitMessageResponse {
oid: query.oid,
message,
})
}
pub async fn git_commit_summary(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitSummaryResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let summary = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_summary(&oid)
})?;
Ok(CommitSummaryResponse {
oid: query.oid,
summary,
})
}
pub async fn git_commit_short_id(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitShortIdResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let short_id = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_short_id(&oid)
})?;
Ok(CommitShortIdResponse {
oid: query.oid,
short_id,
})
}
pub async fn git_commit_author(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitAuthorResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let author = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_author(&oid)
})?;
Ok(CommitAuthorResponse {
oid: query.oid,
author: CommitSignatureResponse::from(author),
})
}
pub async fn git_commit_tree_id(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitTreeIdResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let tree_id = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_tree_id(&oid)
})?;
Ok(CommitTreeIdResponse {
oid: query.oid,
tree_id: tree_id.to_string(),
})
}
pub async fn git_commit_parent_count(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitParentCountResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let parent_count = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_parent_count(&oid)
})?;
Ok(CommitParentCountResponse {
oid: query.oid,
parent_count,
})
}
pub async fn git_commit_parent_ids(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitParentIdsResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let parent_ids = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_parent_ids(&oid)
})?;
Ok(CommitParentIdsResponse {
oid: query.oid,
parent_ids: parent_ids.into_iter().map(|p| p.to_string()).collect(),
})
}
pub async fn git_commit_parent(
&self,
namespace: String,
repo_name: String,
oid: String,
index: usize,
ctx: &Session,
) -> Result<CommitMetaResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = oid.clone();
let parent = git_spawn!(repo, domain -> {
let commit_oid = git::CommitOid::new(&oid_str);
domain.commit_parent(&commit_oid, index)
})?;
Ok(CommitMetaResponse::from(parent))
}
pub async fn git_commit_first_parent(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<Option<CommitMetaResponse>, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let parent = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_first_parent(&oid)
})?;
Ok(parent.map(CommitMetaResponse::from))
}
pub async fn git_commit_is_merge(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitIsMergeResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let is_merge = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_is_merge(&oid)
})?;
Ok(CommitIsMergeResponse {
oid: query.oid,
is_merge,
})
}
pub async fn git_commit_log(
&self,
namespace: String,
repo_name: String,
query: CommitLogQuery,
ctx: &Session,
) -> Result<CommitLogResponse, AppError> {
let page = if query.page == 0 { 1 } else { query.page };
let per_page = query.per_page.clamp(1, 100);
let offset = page.saturating_sub(1) * per_page;
let repo = self
.utils_find_repo(namespace.clone(), repo_name.clone(), ctx)
.await?;
let rev_clone = query.rev.clone();
let rev_for_count = query.rev.clone();
let commits = git_spawn!(repo, domain -> {
domain.commit_log(rev_clone.as_deref(), offset, per_page)
})?;
let data: Vec<CommitMetaResponse> =
commits.into_iter().map(CommitMetaResponse::from).collect();
// Get total count for pagination metadata.
let total_cache_key = format!(
"git:commit:count:{}:{}:{:?}",
namespace, repo_name, rev_for_count,
);
let total: usize = if let Ok(mut conn) = self.cache.conn().await {
if let Ok(cached) = conn.get::<_, String>(total_cache_key.clone()).await {
if let Ok(cached) = serde_json::from_str::<CommitCountResponse>(&cached) {
cached.count
} else {
0
}
} else {
0
}
} else {
0
};
let total_pages = if total == 0 {
0
} else {
(total + per_page - 1) / per_page
};
Ok(CommitLogResponse {
data,
total,
page,
per_page,
total_pages,
})
}
pub async fn git_commit_count(
&self,
namespace: String,
repo_name: String,
from: Option<String>,
to: Option<String>,
ctx: &Session,
) -> Result<CommitCountResponse, AppError> {
let cache_key = format!(
"git:commit:count:{}:{}:{:?}:{:?}",
namespace, repo_name, from, to,
);
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 repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let from_clone = from.clone();
let to_clone = to.clone();
let count = git_spawn!(repo, domain -> {
domain.commit_count(from_clone.as_deref(), to_clone.as_deref())
})?;
let response = CommitCountResponse { count };
if let Ok(mut conn) = self.cache.conn().await {
if let Err(e) = conn
.set_ex::<String, String, ()>(
cache_key,
serde_json::to_string(&response).unwrap_or_default(),
300,
)
.await
{
slog::debug!(self.logs, "cache set failed (non-fatal): {}", e);
}
}
Ok(response)
}
pub async fn git_commit_refs(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<Vec<CommitRefInfoResponse>, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let refs = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_refs(&oid)
})?;
Ok(refs.into_iter().map(CommitRefInfoResponse::from).collect())
}
pub async fn git_commit_branches(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitBranchesResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
if query.oid.is_empty() {
// Batch: fetch all refs, return branches grouped by commit OID
let all_refs = git_spawn!(repo, domain -> {
domain.refs_grouped()
})?;
let data: std::collections::HashMap<String, Vec<String>> = all_refs
.into_iter()
.map(|(oid, (branches, _))| (oid, branches))
.collect();
return Ok(CommitBranchesResponse { data });
}
// Per-commit: existing behavior
let oid_str = query.oid.clone();
let oid_for_spawn = oid_str.clone();
let branches = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_for_spawn);
domain.commit_branches(&oid)
})?;
let mut data = std::collections::HashMap::new();
data.insert(oid_str, branches);
Ok(CommitBranchesResponse { data })
}
pub async fn git_commit_tags(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitTagsResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
if query.oid.is_empty() {
// Batch: fetch all refs, return tags grouped by commit OID
let all_refs = git_spawn!(repo, domain -> {
domain.refs_grouped()
})?;
let data: std::collections::HashMap<String, Vec<String>> = all_refs
.into_iter()
.map(|(oid, (_, tags))| (oid, tags))
.collect();
return Ok(CommitTagsResponse { data });
}
// Per-commit: existing behavior
let oid_str = query.oid.clone();
let oid_for_spawn = oid_str.clone();
let tags = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_for_spawn);
domain.commit_tags(&oid)
})?;
let mut data = std::collections::HashMap::new();
data.insert(oid_str, tags);
Ok(CommitTagsResponse { data })
}
pub async fn git_commit_is_tip(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitIsTipResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let is_tip = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_is_tip(&oid)
})?;
Ok(CommitIsTipResponse {
oid: query.oid,
is_tip,
})
}
pub async fn git_commit_ref_count(
&self,
namespace: String,
repo_name: String,
query: CommitGetQuery,
ctx: &Session,
) -> Result<CommitRefCountResponse, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let ref_count = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_ref_count(&oid)
})?;
Ok(CommitRefCountResponse {
oid: query.oid,
ref_count,
})
}
pub async fn git_commit_reflog(
&self,
namespace: String,
repo_name: String,
_query: CommitGetQuery,
refname: Option<String>,
ctx: &Session,
) -> Result<Vec<CommitReflogEntryResponse>, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let entries = git_spawn!(repo, domain -> {
domain.reflog_entries(refname.as_deref())
})?;
Ok(entries
.into_iter()
.map(CommitReflogEntryResponse::from)
.collect())
}
pub async fn git_commit_graph(
&self,
namespace: String,
repo_name: String,
query: CommitWalkQuery,
ctx: &Session,
) -> Result<CommitGraphResponse, AppError> {
let cache_key = format!(
"git:commit:graph:{}:{}:{:?}:{}",
namespace,
repo_name,
query.rev,
query.limit.unwrap_or(0),
);
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 repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let rev_clone = query.rev.clone();
let limit = query.limit.unwrap_or(0);
let graph = git_spawn!(repo, domain -> {
domain.commit_graph_simple(rev_clone.as_deref(), limit)
})?;
let response = CommitGraphResponse::from(graph);
if let Ok(mut conn) = self.cache.conn().await {
if let Err(e) = conn
.set_ex::<String, String, ()>(
cache_key,
serde_json::to_string(&response).unwrap_or_default(),
300,
)
.await
{
slog::debug!(self.logs, "cache set failed (non-fatal): {}", e);
}
}
Ok(response)
}
pub async fn git_commit_graph_react(
&self,
namespace: String,
repo_name: String,
query: CommitWalkQuery,
ctx: &Session,
) -> Result<CommitGraphReactResponse, AppError> {
let cache_key = format!(
"git:commit:graph:react:{}:{}:{:?}:{}:v2",
namespace,
repo_name,
query.rev,
query.limit.unwrap_or(0),
);
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 repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let rev_clone = query.rev.clone();
let limit = query.limit.unwrap_or(0);
let (graph, refs_grouped) = git_spawn!(repo, domain -> {
let g = domain.commit_graph_simple(rev_clone.as_deref(), limit)?;
let refs = domain.refs_grouped().unwrap_or_default();
Ok::<_, git::GitError>((g, refs))
})?;
let max_parents = graph.max_parents;
let lines = graph.lines;
let oid_to_lane_index: std::collections::HashMap<String, usize> = lines
.iter()
.map(|l| (l.oid.to_string(), l.lane_index))
.collect();
let commits: Vec<CommitGraphReactCommit> = lines
.into_iter()
.map(|line| {
let meta = line.meta;
let oid_str = line.oid.to_string();
let (tags, branches) = refs_grouped.get(&oid_str).cloned().unwrap_or_default();
let tags: Vec<String> = tags
.into_iter()
.map(|s| {
s.trim_start_matches("refs/tags/")
.trim_start_matches("refs/heads/")
.to_string()
})
.collect();
let branches: Vec<String> = branches
.into_iter()
.map(|s| {
s.trim_start_matches("refs/tags/")
.trim_start_matches("refs/heads/")
.to_string()
})
.collect();
CommitGraphReactCommit {
oid: oid_str,
hash_abbrev: line.oid.to_string()[..7.min(line.oid.to_string().len())]
.to_string(),
subject: line.short_message,
body: None,
author_name: meta.author.name,
author_email: meta.author.email,
author_timestamp: meta.author.time_secs,
author_time_offset: meta.author.offset_minutes,
committer_name: meta.committer.name,
committer_email: meta.committer.email,
committer_timestamp: meta.committer.time_secs,
committer_time_offset: meta.committer.offset_minutes,
parent_hashes: meta.parent_ids.into_iter().map(|p| p.to_string()).collect(),
encoding: meta.encoding,
lane_index: line.lane_index,
graph_chars: line.graph_chars,
refs: line.refs,
tags,
branches,
}
})
.collect();
// Compute lane lifecycle.
// Build a map: commit_oid -> Vec<lane_index> (all lanes active at this commit).
let mut oid_lanes: std::collections::HashMap<String, Vec<usize>> =
std::collections::HashMap::new();
for commit in &commits {
oid_lanes
.entry(commit.oid.clone())
.or_default()
.push(commit.lane_index);
}
// Track lane state: lane_index -> Option<branch_name> (when lane starts with a branch).
let mut lane_branch: std::collections::BTreeMap<usize, Option<String>> =
std::collections::BTreeMap::new();
let mut lane_start_oid: std::collections::BTreeMap<usize, String> =
std::collections::BTreeMap::new();
let mut lane_end_oid: std::collections::BTreeMap<usize, Option<String>> =
std::collections::BTreeMap::new();
for commit in &commits {
let lane = commit.lane_index;
if !lane_start_oid.contains_key(&lane) {
lane_start_oid.insert(lane, commit.oid.clone());
// If this commit has branches, assign the first one to this lane.
let branch = commit.branches.first().cloned();
lane_branch.insert(lane, branch);
}
}
// Determine lane end: a lane ends at the last commit that is on that lane
// AND whose children are NOT on the same lane.
for (i, commit) in commits.iter().enumerate() {
let lane = commit.lane_index;
let child_lanes: Vec<usize> = if i + 1 < commits.len() {
commits[i + 1]
.parent_hashes
.iter()
.filter_map(|p| oid_to_lane_index.get(p))
.copied()
.collect()
} else {
vec![]
};
if !child_lanes.contains(&lane) {
// This commit's children don't continue on this lane → lane ends here.
lane_end_oid.insert(lane, Some(commit.oid.clone()));
}
}
// Use BTreeMap ordered iteration instead of sorted keys.
let lanes: Vec<LaneInfo> = lane_start_oid
.into_iter()
.map(|(lane_index, start_oid)| LaneInfo {
lane_index,
branch_name: lane_branch.remove(&lane_index).flatten(),
start_oid,
end_oid: lane_end_oid.remove(&lane_index).flatten(),
})
.collect();
let response = CommitGraphReactResponse {
commits,
lanes,
max_parents,
};
if let Ok(mut conn) = self.cache.conn().await {
if let Err(e) = conn
.set_ex::<String, String, ()>(
cache_key,
serde_json::to_string(&response).unwrap_or_default(),
300,
)
.await
{
slog::debug!(self.logs, "cache set failed (non-fatal): {}", e);
}
}
Ok(response)
}
pub async fn git_commit_walk(
&self,
namespace: String,
repo_name: String,
query: CommitWalkQuery,
ctx: &Session,
) -> Result<Vec<CommitMetaResponse>, AppError> {
let cache_key = format!(
"git:commit:walk:{}:{}:{:?}:{}:{}:{}:{}",
namespace,
repo_name,
query.rev,
query.limit.unwrap_or(0),
query.first_parent_only,
query.topological,
query.reverse,
);
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 repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let rev_clone = query.rev.clone();
let limit = query.limit.unwrap_or(0);
let first_parent_only = query.first_parent_only;
let topological = query.topological;
let reverse = query.reverse;
let sort = if topological && reverse {
CommitSort(CommitSort::TOPOLOGICAL.0 | CommitSort::TIME.0 | CommitSort::REVERSE.0)
} else if topological {
CommitSort(CommitSort::TOPOLOGICAL.0 | CommitSort::TIME.0)
} else if reverse {
CommitSort(CommitSort::TIME.0 | CommitSort::REVERSE.0)
} else {
CommitSort(CommitSort::TOPOLOGICAL.0 | CommitSort::TIME.0)
};
let commits = git_spawn!(repo, domain -> {
domain.commit_walk(CommitWalkOptions {
rev: rev_clone,
sort,
limit,
first_parent_only,
})
})?;
let response: Vec<CommitMetaResponse> =
commits.into_iter().map(CommitMetaResponse::from).collect();
if let Ok(mut conn) = self.cache.conn().await {
if let Err(e) = conn
.set_ex::<String, String, ()>(
cache_key,
serde_json::to_string(&response).unwrap_or_default(),
300,
)
.await
{
slog::debug!(self.logs, "cache set failed (non-fatal): {}", e);
}
}
Ok(response)
}
pub async fn git_commit_ancestors(
&self,
namespace: String,
repo_name: String,
query: CommitAncestorsQuery,
ctx: &Session,
) -> Result<Vec<CommitMetaResponse>, AppError> {
let cache_key = format!(
"git:commit:ancestors:{}:{}:{}:{}",
namespace,
repo_name,
query.oid,
query.limit.unwrap_or(0),
);
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 repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let limit = query.limit.unwrap_or(0);
let commits = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_ancestors(&oid, limit)
})?;
let response: Vec<CommitMetaResponse> =
commits.into_iter().map(CommitMetaResponse::from).collect();
if let Ok(mut conn) = self.cache.conn().await {
if let Err(e) = conn
.set_ex::<String, String, ()>(
cache_key,
serde_json::to_string(&response).unwrap_or_default(),
300,
)
.await
{
slog::debug!(self.logs, "cache set failed (non-fatal): {}", e);
}
}
Ok(response)
}
pub async fn git_commit_descendants(
&self,
namespace: String,
repo_name: String,
query: CommitDescendantsQuery,
ctx: &Session,
) -> Result<Vec<CommitMetaResponse>, AppError> {
let cache_key = format!(
"git:commit:descendants:{}:{}:{}:{}",
namespace,
repo_name,
query.oid,
query.limit.unwrap_or(0),
);
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 repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let oid_str = query.oid.clone();
let limit = query.limit.unwrap_or(0);
let commits = git_spawn!(repo, domain -> {
let oid = git::CommitOid::new(&oid_str);
domain.commit_descendants(&oid, limit)
})?;
let response: Vec<CommitMetaResponse> =
commits.into_iter().map(CommitMetaResponse::from).collect();
if let Ok(mut conn) = self.cache.conn().await {
if let Err(e) = conn
.set_ex::<String, String, ()>(
cache_key,
serde_json::to_string(&response).unwrap_or_default(),
300,
)
.await
{
slog::debug!(self.logs, "cache set failed (non-fatal): {}", e);
}
}
Ok(response)
}
pub async fn git_commit_resolve_rev(
&self,
namespace: String,
repo_name: String,
query: CommitResolveQuery,
ctx: &Session,
) -> Result<String, AppError> {
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
let rev_str = query.rev.clone();
let oid = git_spawn!(repo, domain -> {
domain.resolve_rev(&rev_str)
})?;
Ok(oid.to_string())
}
pub async fn git_commit_create(
&self,
namespace: String,
repo_name: String,
request: CommitCreateRequest,
ctx: &Session,
) -> Result<CommitCreateResponse, AppError> {
let repo: repo::Model = self
.utils_check_repo_admin(namespace, repo_name, ctx)
.await?;
let parent_ids: Vec<_> = request
.parent_ids
.iter()
.map(|p| git::CommitOid::new(p))
.collect();
let author = git::CommitSignature {
name: request.author_name,
email: request.author_email,
time_secs: chrono::Utc::now().timestamp(),
offset_minutes: 0,
};
let committer = git::CommitSignature {
name: request.committer_name,
email: request.committer_email,
time_secs: chrono::Utc::now().timestamp(),
offset_minutes: 0,
};
let tree_id = git::CommitOid::new(&request.tree_id);
let update_ref = request.update_ref.clone();
let oid = git_spawn!(repo, domain -> {
domain.commit_create(
update_ref.as_deref(),
&author,
&committer,
&request.message,
&tree_id,
&parent_ids,
)
})?;
Ok(CommitCreateResponse {
oid: oid.to_string(),
})
}
pub async fn git_commit_amend(
&self,
namespace: String,
repo_name: String,
request: CommitAmendRequest,
ctx: &Session,
) -> Result<CommitCreateResponse, AppError> {
let repo: repo::Model = self
.utils_check_repo_admin(namespace, repo_name, ctx)
.await?;
let oid = git::CommitOid::new(&request.oid);
let author =
if let (Some(name), Some(email)) = (&request.author_name, &request.author_email) {
Some(git::CommitSignature {
name: name.clone(),
email: email.clone(),
time_secs: 0,
offset_minutes: 0,
})
} else {
None
};
let committer = if let (Some(name), Some(email)) =
(&request.committer_name, &request.committer_email)
{
Some(git::CommitSignature {
name: name.clone(),
email: email.clone(),
time_secs: 0,
offset_minutes: 0,
})
} else {
None
};
let tree_id = request.tree_id.as_ref().map(|t| git::CommitOid::new(t));
let update_ref = request.update_ref.clone();
let message_encoding = request.message_encoding.clone();
let message = request.message.clone();
let new_oid = git_spawn!(repo, domain -> {
domain.commit_amend(
&oid,
update_ref.as_deref(),
author.as_ref(),
committer.as_ref(),
message_encoding.as_deref(),
message.as_deref(),
tree_id.as_ref(),
)
})?;
Ok(CommitCreateResponse {
oid: new_oid.to_string(),
})
}
pub async fn git_commit_cherry_pick(
&self,
namespace: String,
repo_name: String,
request: CommitCherryPickRequest,
ctx: &Session,
) -> Result<CommitCreateResponse, AppError> {
let repo: repo::Model = self
.utils_check_repo_admin(namespace, repo_name, ctx)
.await?;
let cherrypick_oid = git::CommitOid::new(&request.cherrypick_oid);
let author = git::CommitSignature {
name: request.author_name,
email: request.author_email,
time_secs: chrono::Utc::now().timestamp(),
offset_minutes: 0,
};
let committer = git::CommitSignature {
name: request.committer_name,
email: request.committer_email,
time_secs: chrono::Utc::now().timestamp(),
offset_minutes: 0,
};
let message = request.message.clone();
let mainline = request.mainline.unwrap_or(0);
let update_ref = request.update_ref.clone();
let oid = git_spawn!(repo, domain -> {
domain.commit_cherry_pick(
&cherrypick_oid,
&author,
&committer,
message.as_deref(),
mainline,
update_ref.as_deref(),
)
})?;
Ok(CommitCreateResponse {
oid: oid.to_string(),
})
}
pub async fn git_commit_cherry_pick_abort(
&self,
namespace: String,
repo_name: String,
request: CommitCherryPickAbortRequest,
ctx: &Session,
) -> Result<(), AppError> {
let repo: repo::Model = self
.utils_check_repo_admin(namespace, repo_name, ctx)
.await?;
let reset_type = request.reset_type.clone();
git_spawn!(repo, domain -> {
domain.commit_cherry_pick_abort(reset_type.as_deref().unwrap_or("hard"))
})?;
Ok(())
}
pub async fn git_commit_revert(
&self,
namespace: String,
repo_name: String,
request: CommitRevertRequest,
ctx: &Session,
) -> Result<CommitCreateResponse, AppError> {
let repo: repo::Model = self
.utils_check_repo_admin(namespace, repo_name, ctx)
.await?;
let revert_oid = git::CommitOid::new(&request.revert_oid);
let author = git::CommitSignature {
name: request.author_name,
email: request.author_email,
time_secs: chrono::Utc::now().timestamp(),
offset_minutes: 0,
};
let committer = git::CommitSignature {
name: request.committer_name,
email: request.committer_email,
time_secs: chrono::Utc::now().timestamp(),
offset_minutes: 0,
};
let message = request.message.clone();
let mainline = request.mainline.unwrap_or(0);
let update_ref = request.update_ref.clone();
let oid = git_spawn!(repo, domain -> {
domain.commit_revert(
&revert_oid,
&author,
&committer,
message.as_deref(),
mainline,
update_ref.as_deref(),
)
})?;
Ok(CommitCreateResponse {
oid: oid.to_string(),
})
}
pub async fn git_commit_revert_abort(
&self,
namespace: String,
repo_name: String,
request: CommitRevertAbortRequest,
ctx: &Session,
) -> Result<(), AppError> {
let repo: repo::Model = self
.utils_check_repo_admin(namespace, repo_name, ctx)
.await?;
let reset_type = request.reset_type.clone();
git_spawn!(repo, domain -> {
domain.commit_revert_abort(reset_type.as_deref().unwrap_or("hard"))
})?;
Ok(())
}
}