use crate::AppService;
use crate::error::AppError;
use crate::git::{BranchDiff, BranchInfo, BranchSummary};
use models::repos::repo;
use models::repos::repo as repo_model;
use models::repos::repo_branch;
use models::repos::repo_branch_protect;
use sea_orm::prelude::Expr;
use sea_orm::{ColumnTrait, EntityTrait, ExprTrait, PaginatorTrait, QueryFilter};
use serde::{Deserialize, Serialize};
use session::Session;
use uuid::Uuid;

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct BranchListQuery {
    #[serde(default)]
    pub remote_only: Option<bool>,
    #[serde(default)]
    pub all: Option<bool>,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchInfoResponse {
    pub name: String,
    pub oid: String,
    pub is_head: bool,
    pub is_remote: bool,
    pub is_current: bool,
    pub upstream: Option<String>,
}

impl From<BranchInfo> for BranchInfoResponse {
    fn from(b: BranchInfo) -> Self {
        Self {
            name: b.name,
            oid: b.oid.to_string(),
            is_head: b.is_head,
            is_remote: b.is_remote,
            is_current: b.is_current,
            upstream: b.upstream,
        }
    }
}

impl From<repo_branch::Model> for BranchInfoResponse {
    fn from(b: repo_branch::Model) -> Self {
        // is_remote: full ref path starts with "refs/remotes/"
        let is_remote = b.name.starts_with("refs/remotes/");
        // shorthand name for display (strip prefix)
        let name = if b.name.starts_with("refs/heads/") {
            b.name
                .strip_prefix("refs/heads/")
                .unwrap_or(&b.name)
                .to_string()
        } else if b.name.starts_with("refs/remotes/") {
            b.name
                .strip_prefix("refs/remotes/")
                .unwrap_or(&b.name)
                .to_string()
        } else {
            b.name.clone()
        };
        Self {
            name,
            oid: b.oid,
            is_head: b.head,
            is_remote,
            // is_current: not stored in DB, always false when from DB
            is_current: false,
            // upstream: stored as pattern "refs/remotes/{}/{}", return as-is
            upstream: b.upstream,
        }
    }
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchSummaryResponse {
    pub local_count: usize,
    pub remote_count: usize,
    pub all_count: usize,
}

impl From<BranchSummary> for BranchSummaryResponse {
    fn from(s: BranchSummary) -> Self {
        Self {
            local_count: s.local_count,
            remote_count: s.remote_count,
            all_count: s.all_count,
        }
    }
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchDiffResponse {
    pub ahead: usize,
    pub behind: usize,
    pub diverged: bool,
}

impl From<BranchDiff> for BranchDiffResponse {
    fn from(d: BranchDiff) -> Self {
        Self {
            ahead: d.ahead,
            behind: d.behind,
            diverged: d.diverged,
        }
    }
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchExistsResponse {
    pub name: String,
    pub exists: bool,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchIsHeadResponse {
    pub name: String,
    pub is_head: bool,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchIsDetachedResponse {
    pub is_detached: bool,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchIsMergedResponse {
    pub branch: String,
    pub into: String,
    pub is_merged: bool,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchMergeBaseResponse {
    pub branch1: String,
    pub branch2: String,
    pub base: String,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchIsAncestorResponse {
    pub child: String,
    pub ancestor: String,
    pub is_ancestor: bool,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchFastForwardResponse {
    pub oid: String,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchTrackingDiffResponse {
    pub name: String,
    pub ahead: usize,
    pub behind: usize,
}

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct BranchIsConflictedResponse {
    pub is_conflicted: bool,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct BranchCreateRequest {
    pub name: String,
    pub oid: Option<String>,
    #[serde(default)]
    pub force: bool,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct BranchRenameRequest {
    pub old_name: String,
    pub new_name: String,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct BranchMoveRequest {
    pub name: String,
    pub new_name: String,
    #[serde(default)]
    pub force: bool,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct BranchSetUpstreamRequest {
    pub name: String,
    pub upstream: Option<String>,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct BranchDiffQuery {
    pub local: String,
    pub remote: String,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct BranchIsMergedQuery {
    pub branch: String,
    pub into: String,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct BranchIsAncestorQuery {
    pub child: String,
    pub ancestor: String,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct BranchMergeBaseQuery {
    pub branch1: String,
    pub branch2: 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 {
    /// Check and enforce branch protection rules before deleting (or renaming/moving away from) a branch.
    async fn check_protection_for_deletion(
        &self,
        repo_id: Uuid,
        branch: &str,
    ) -> Result<(), AppError> {
        let protection = repo_branch_protect::Entity::find()
            .filter(repo_branch_protect::Column::Repo.eq(repo_id))
            .filter(repo_branch_protect::Column::Branch.eq(branch))
            .one(&self.db)
            .await
            .map_err(AppError::from)?;

        if let Some(rule) = protection {
            if rule.forbid_deletion {
                return Err(AppError::Forbidden(format!(
                    "Deletion of protected branch '{}' is forbidden",
                    branch
                )));
            }
        }
        Ok(())
    }

    /// Check and enforce branch protection rules before creating (or renaming/moving to) a branch.
    async fn check_protection_for_push(&self, repo_id: Uuid, branch: &str) -> Result<(), AppError> {
        let protection = repo_branch_protect::Entity::find()
            .filter(repo_branch_protect::Column::Repo.eq(repo_id))
            .filter(repo_branch_protect::Column::Branch.eq(branch))
            .one(&self.db)
            .await
            .map_err(AppError::from)?;

        if let Some(rule) = protection {
            if rule.forbid_push {
                return Err(AppError::Forbidden(format!(
                    "Push to protected branch '{}' is forbidden",
                    branch
                )));
            }
        }
        Ok(())
    }
    pub async fn git_branch_list(
        &self,
        namespace: String,
        repo_name: String,
        query: BranchListQuery,
        ctx: &Session,
    ) -> Result<Vec<BranchInfoResponse>, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        let branches = repo_branch::Entity::find()
            .filter(repo_branch::Column::Repo.eq(repo.id))
            .filter({
                if query.all.unwrap_or(false) {
                    // all: no filter
                    Expr::value(true)
                } else if query.remote_only.unwrap_or(false) {
                    Expr::col(repo_branch::Column::Name).like("refs/remotes/%")
                } else {
                    Expr::col(repo_branch::Column::Name).like("refs/heads/%")
                }
            })
            .all(&self.db)
            .await
            .map_err(AppError::from)?;

        Ok(branches.into_iter().map(BranchInfoResponse::from).collect())
    }

    pub async fn git_branch_summary(
        &self,
        namespace: String,
        repo_name: String,
        ctx: &Session,
    ) -> Result<BranchSummaryResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        let local_count: usize = repo_branch::Entity::find()
            .filter(repo_branch::Column::Repo.eq(repo.id))
            .filter(Expr::col(repo_branch::Column::Name).like("refs/heads/%"))
            .count(&self.db)
            .await
            .map_err(AppError::from)? as usize;

        let remote_count: usize = repo_branch::Entity::find()
            .filter(repo_branch::Column::Repo.eq(repo.id))
            .filter(Expr::col(repo_branch::Column::Name).like("refs/remotes/%"))
            .count(&self.db)
            .await
            .map_err(AppError::from)? as usize;

        Ok(BranchSummaryResponse {
            local_count,
            remote_count,
            all_count: local_count + remote_count,
        })
    }

    pub async fn git_branch_get(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<BranchInfoResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        // Normalize: try shorthand forms, then full ref paths
        let candidates = if name.starts_with("refs/") {
            vec![name.clone()]
        } else if name.starts_with("heads/") {
            vec![format!("refs/{}", name)]
        } else {
            vec![
                format!("refs/heads/{}", name),
                format!("refs/remotes/{}", name),
                name.clone(),
            ]
        };

        for candidate in &candidates {
            if let Some(b) = repo_branch::Entity::find()
                .filter(repo_branch::Column::Repo.eq(repo.id))
                .filter(repo_branch::Column::Name.eq(candidate))
                .one(&self.db)
                .await
                .map_err(AppError::from)?
            {
                return Ok(BranchInfoResponse::from(b));
            }
        }

        // Fallback to git
        let name_clone = name.clone();
        let info = git_spawn!(repo, domain -> {
            domain.branch_get(&name_clone)
        })?;
        Ok(BranchInfoResponse::from(info))
    }

    pub async fn git_branch_current(
        &self,
        namespace: String,
        repo_name: String,
        ctx: &Session,
    ) -> Result<Option<BranchInfoResponse>, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        // Try git first (normal path). Errors (e.g. empty repo) fall through to DB fallback.
        if let Ok(Some(b)) = git_spawn!(repo, domain -> {
            domain.branch_current()
        }) {
            return Ok(Some(BranchInfoResponse::from(b)));
        }

        // Fallback: repo may be empty or sync not yet run, but default_branch
        // is already set in repo metadata. Look it up in DB.
        // repo.default_branch is shorthand (e.g. "main"), DB stores full ref (e.g. "refs/heads/main")
        if !repo.default_branch.is_empty() {
            let full_ref = format!("refs/heads/{}", repo.default_branch);
            if let Some(branch) = repo_branch::Entity::find()
                .filter(repo_branch::Column::Repo.eq(repo.id))
                .filter(repo_branch::Column::Name.eq(&full_ref))
                .one(&self.db)
                .await
                .map_err(AppError::from)?
            {
                return Ok(Some(BranchInfoResponse::from(branch)));
            }
        }

        Ok(None)
    }

    pub async fn git_branch_exists(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<BranchExistsResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        // Try shorthand → full ref path candidates
        let candidates = if name.starts_with("refs/") {
            vec![name.clone()]
        } else {
            vec![
                format!("refs/heads/{}", name),
                format!("refs/remotes/{}", name),
                name.clone(),
            ]
        };

        for candidate in &candidates {
            let found = repo_branch::Entity::find()
                .filter(repo_branch::Column::Repo.eq(repo.id))
                .filter(repo_branch::Column::Name.eq(candidate))
                .one(&self.db)
                .await
                .map_err(AppError::from)?;
            if found.is_some() {
                return Ok(BranchExistsResponse { name, exists: true });
            }
        }

        // Fallback to git
        let name_clone = name.clone();
        let exists = git_spawn!(repo, domain -> {
            Ok::<_, git::GitError>(domain.branch_exists(&name_clone))
        })?;

        Ok(BranchExistsResponse { name, exists })
    }

    pub async fn git_branch_is_head(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<BranchIsHeadResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let name_clone = name.clone();

        let is_head = git_spawn!(repo, domain -> {
            domain.branch_is_head(&name_clone)
        })?;

        Ok(BranchIsHeadResponse { name, is_head })
    }

    pub async fn git_branch_is_detached(
        &self,
        namespace: String,
        repo_name: String,
        ctx: &Session,
    ) -> Result<BranchIsDetachedResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        let is_detached = git_spawn!(repo, domain -> {
            Ok::<_, git::GitError>(domain.branch_is_detached())
        })?;

        Ok(BranchIsDetachedResponse { is_detached })
    }

    pub async fn git_branch_upstream(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<Option<BranchInfoResponse>, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let name_clone = name.clone();

        let upstream = git_spawn!(repo, domain -> {
            domain.branch_upstream(&name_clone)
        })?;

        Ok(upstream.map(BranchInfoResponse::from))
    }

    pub async fn git_branch_diff(
        &self,
        namespace: String,
        repo_name: String,
        query: BranchDiffQuery,
        ctx: &Session,
    ) -> Result<BranchDiffResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let local = query.local.clone();
        let remote = query.remote.clone();

        let diff = git_spawn!(repo, domain -> {
            domain.branch_diff(&local, &remote)
        })?;

        Ok(BranchDiffResponse::from(diff))
    }

    pub async fn git_branch_tracking_difference(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<BranchTrackingDiffResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let name_clone = name.clone();

        let (ahead, behind) = git_spawn!(repo, domain -> {
            domain.branch_tracking_difference(&name_clone)
        })?;

        Ok(BranchTrackingDiffResponse {
            name,
            ahead,
            behind,
        })
    }

    pub async fn git_branch_create(
        &self,
        namespace: String,
        repo_name: String,
        request: BranchCreateRequest,
        ctx: &Session,
    ) -> Result<BranchInfoResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        self.check_protection_for_push(repo.id, &request.name)
            .await?;
        let name = request.name.clone();
        let force = request.force;
        let name_for_spawn = name.clone();

        let info = if let Some(oid) = request.oid {
            let commit_oid = git::CommitOid::new(&oid);
            let name_clone = name_for_spawn.clone();
            git_spawn!(repo, domain -> {
                domain.branch_create(&name_clone, &commit_oid, force)
            })?
        } else {
            git_spawn!(repo, domain -> {
                domain.branch_create_from_head(&name_for_spawn, force)
            })?
        };

        let response = BranchInfoResponse::from(info);

        let project_id = match repo_model::Entity::find_by_id(repo.id).one(&self.db).await {
            Ok(Some(r)) => r.project,
            Ok(None) => Uuid::nil(),
            Err(e) => {
                slog::warn!(
                    self.logs,
                    "failed to look up project_id for activity log: {}",
                    e
                );
                Uuid::nil()
            }
        };
        let user_uid = ctx.user().unwrap_or(Uuid::nil());
        let _ = self
            .project_log_activity(
                project_id,
                Some(repo.id),
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "branch_create".to_string(),
                    title: format!("{} created branch '{}'", user_uid, name),
                    repo_id: Some(repo.id),
                    content: None,
                    event_id: None,
                    event_sub_id: None,
                    metadata: Some(serde_json::json!({"branch_name": name})),
                    is_private: false,
                },
            )
            .await;

        Ok(response)
    }

    pub async fn git_branch_delete(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<(), AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        self.check_protection_for_deletion(repo.id, &name).await?;
        let name_for_spawn = name.clone();

        git_spawn!(repo, domain -> {
            domain.branch_delete(&name_for_spawn)
        })?;

        let project_id = match repo_model::Entity::find_by_id(repo.id).one(&self.db).await {
            Ok(Some(r)) => r.project,
            Ok(None) => Uuid::nil(),
            Err(e) => {
                slog::warn!(
                    self.logs,
                    "failed to look up project_id for activity log: {}",
                    e
                );
                Uuid::nil()
            }
        };
        let user_uid = ctx.user().unwrap_or(Uuid::nil());
        let _ = self
            .project_log_activity(
                project_id,
                Some(repo.id),
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "branch_delete".to_string(),
                    title: format!("{} deleted branch '{}'", user_uid, name),
                    repo_id: Some(repo.id),
                    content: None,
                    event_id: None,
                    event_sub_id: None,
                    metadata: Some(serde_json::json!({"branch_name": name})),
                    is_private: false,
                },
            )
            .await;

        Ok(())
    }

    pub async fn git_branch_delete_remote(
        &self,
        namespace: String,
        repo_name: String,
        name: String,
        ctx: &Session,
    ) -> Result<(), AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        self.check_protection_for_deletion(repo.id, &name).await?;
        let name_clone = name.clone();

        git_spawn!(repo, domain -> {
            domain.branch_delete_remote(&name_clone)
        })?;

        Ok(())
    }

    pub async fn git_branch_rename(
        &self,
        namespace: String,
        repo_name: String,
        request: BranchRenameRequest,
        ctx: &Session,
    ) -> Result<BranchInfoResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        // Check: source branch cannot be deleted if protected
        self.check_protection_for_deletion(repo.id, &request.old_name)
            .await?;
        // Check: target branch cannot be pushed if protected
        self.check_protection_for_push(repo.id, &request.new_name)
            .await?;
        let old_name = request.old_name.clone();
        let new_name = request.new_name.clone();
        let old_name_for_spawn = old_name.clone();
        let new_name_for_spawn = new_name.clone();

        let info = git_spawn!(repo, domain -> {
            domain.branch_rename(&old_name_for_spawn, &new_name_for_spawn)
        })?;

        let response = BranchInfoResponse::from(info);
        let project_id = match repo_model::Entity::find_by_id(repo.id).one(&self.db).await {
            Ok(Some(r)) => r.project,
            Ok(None) => Uuid::nil(),
            Err(e) => {
                slog::warn!(
                    self.logs,
                    "failed to look up project_id for activity log: {}",
                    e
                );
                Uuid::nil()
            }
        };
        let user_uid = ctx.user().unwrap_or(Uuid::nil());
        let _ = self
            .project_log_activity(
                project_id,
                Some(repo.id),
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "branch_rename".to_string(),
                    title: format!(
                        "{} renamed branch '{}' to '{}'",
                        user_uid, old_name, new_name
                    ),
                    repo_id: Some(repo.id),
                    content: None,
                    event_id: None,
                    event_sub_id: None,
                    metadata: Some(serde_json::json!({"old_name": old_name, "new_name": new_name})),
                    is_private: false,
                },
            )
            .await;

        Ok(response)
    }

    pub async fn git_branch_move(
        &self,
        namespace: String,
        repo_name: String,
        request: BranchMoveRequest,
        ctx: &Session,
    ) -> Result<BranchInfoResponse, AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
            .await?;
        // Check: source branch cannot be deleted if protected
        self.check_protection_for_deletion(repo.id, &request.name)
            .await?;
        // Check: target branch cannot be pushed if protected
        self.check_protection_for_push(repo.id, &request.new_name)
            .await?;
        let name = request.name.clone();
        let new_name = request.new_name.clone();
        let force = request.force;
        let name_for_spawn = name.clone();
        let new_name_for_spawn = new_name.clone();

        let info = git_spawn!(repo, domain -> {
            domain.branch_move(&name_for_spawn, &new_name_for_spawn, force)
        })?;

        let response = BranchInfoResponse::from(info);
        let project_id = match repo_model::Entity::find_by_id(repo.id).one(&self.db).await {
            Ok(Some(r)) => r.project,
            Ok(None) => Uuid::nil(),
            Err(e) => {
                slog::warn!(
                    self.logs,
                    "failed to look up project_id for activity log: {}",
                    e
                );
                Uuid::nil()
            }
        };
        let user_uid = ctx.user().unwrap_or(Uuid::nil());
        let _ = self
            .project_log_activity(
                project_id,
                Some(repo.id),
                user_uid,
                super::super::project::activity::ActivityLogParams {
                    event_type: "branch_rename".to_string(),
                    title: format!("{} renamed branch '{}' to '{}'", user_uid, name, new_name),
                    repo_id: Some(repo.id),
                    content: None,
                    event_id: None,
                    event_sub_id: None,
                    metadata: Some(serde_json::json!({"old_name": name, "new_name": new_name})),
                    is_private: false,
                },
            )
            .await;

        Ok(response)
    }

    pub async fn git_branch_set_upstream(
        &self,
        namespace: String,
        repo_name: String,
        request: BranchSetUpstreamRequest,
        ctx: &Session,
    ) -> Result<(), AppError> {
        let repo: repo::Model = self
            .utils_check_repo_admin(namespace, repo_name, ctx)
            .await?;
        let name = request.name.clone();
        let upstream = request.upstream.clone();

        git_spawn!(repo, domain -> {
            domain.branch_set_upstream(&name, upstream.as_deref())
        })?;

        Ok(())
    }

    pub async fn git_branch_is_merged(
        &self,
        namespace: String,
        repo_name: String,
        query: BranchIsMergedQuery,
        ctx: &Session,
    ) -> Result<BranchIsMergedResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let branch = query.branch.clone();
        let into = query.into.clone();

        let is_merged = git_spawn!(repo, domain -> {
            domain.branch_is_merged(&branch, &into)
        })?;

        Ok(BranchIsMergedResponse {
            branch: query.branch,
            into: query.into,
            is_merged,
        })
    }

    pub async fn git_branch_merge_base(
        &self,
        namespace: String,
        repo_name: String,
        query: BranchMergeBaseQuery,
        ctx: &Session,
    ) -> Result<BranchMergeBaseResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let branch1 = query.branch1.clone();
        let branch2 = query.branch2.clone();

        let base = git_spawn!(repo, domain -> {
            domain.branch_merge_base(&branch1, &branch2)
        })?;

        Ok(BranchMergeBaseResponse {
            branch1: query.branch1,
            branch2: query.branch2,
            base: base.to_string(),
        })
    }

    pub async fn git_branch_is_ancestor(
        &self,
        namespace: String,
        repo_name: String,
        query: BranchIsAncestorQuery,
        ctx: &Session,
    ) -> Result<BranchIsAncestorResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let child = query.child.clone();
        let ancestor = query.ancestor.clone();

        let is_ancestor = git_spawn!(repo, domain -> {
            domain.branch_is_ancestor(&child, &ancestor)
        })?;

        Ok(BranchIsAncestorResponse {
            child: query.child,
            ancestor: query.ancestor,
            is_ancestor,
        })
    }

    pub async fn git_branch_fast_forward(
        &self,
        namespace: String,
        repo_name: String,
        target: String,
        force: Option<bool>,
        ctx: &Session,
    ) -> Result<BranchFastForwardResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
        let target_clone = target.clone();

        let new_oid = git_spawn!(repo, domain -> {
            domain.branch_fast_forward(&target_clone, force.unwrap_or(false))
        })?;

        Ok(BranchFastForwardResponse {
            oid: new_oid.to_string(),
        })
    }

    pub async fn git_branch_is_conflicted(
        &self,
        namespace: String,
        repo_name: String,
        ctx: &Session,
    ) -> Result<BranchIsConflictedResponse, AppError> {
        let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;

        let is_conflicted = git_spawn!(repo, domain -> {
            Ok::<_, git::GitError>(domain.branch_is_conflicted())
        })?;

        Ok(BranchIsConflictedResponse { is_conflicted })
    }
}
