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
            {
                tracing::debug!(error = ?e, "cache set failed (non-fatal)");
            }
        }

        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().or_else(|| {
            if repo.default_branch.is_empty() {
                None
            } else {
                Some(format!("refs/heads/{}", repo.default_branch))
            }
        });
        let rev_for_count = rev_clone.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 default_ref = if repo.default_branch.is_empty() {
            None
        } else {
            Some(format!("refs/heads/{}", repo.default_branch))
        };

        let count = git_spawn!(repo, domain -> {
            let from_ref = from_clone.as_deref();
            let to_ref = to_clone.as_deref().or(default_ref.as_deref());
            domain.commit_count(from_ref, to_ref)
        })?;

        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
            {
                tracing::debug!(error = ?e, "cache set failed (non-fatal)");
            }
        }

        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?;

        // If refname is None and HEAD is detached, fall back to default_branch
        let refname = refname.or_else(|| {
            if repo.default_branch.is_empty() {
                None
            } else {
                Some(repo.default_branch.clone())
            }
        });

        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().or_else(|| {
            if repo.default_branch.is_empty() {
                None
            } else {
                Some(format!("refs/heads/{}", repo.default_branch))
            }
        });
        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
            {
                tracing::debug!(error = ?e, "cache set failed (non-fatal)");
            }
        }

        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().or_else(|| {
            if repo.default_branch.is_empty() {
                None
            } else {
                Some(format!("refs/heads/{}", repo.default_branch))
            }
        });
        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
            {
                tracing::debug!(error = ?e, "cache set failed (non-fatal)");
            }
        }

        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().or_else(|| {
            if repo.default_branch.is_empty() {
                None
            } else {
                Some(format!("refs/heads/{}", repo.default_branch))
            }
        });
        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
            {
                tracing::debug!(error = ?e, "cache set failed (non-fatal)");
            }
        }

        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
            {
                tracing::debug!(error = ?e, "cache set failed (non-fatal)");
            }
        }

        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
            {
                tracing::debug!(error = ?e, "cache set failed (non-fatal)");
            }
        }

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