use db::sqlx; use git::rpc::{proto as p, proto::merge_service_client::MergeServiceClient}; use serde::Deserialize; use session::Session; use crate::{ AppService, error::AppError, git::rpc_err, metrics::with_op_metric, pull_request::types::PullRequestResponse, session_user, }; #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct MergePullRequest { pub method: Option, pub commit_title: Option, pub commit_message: Option, } impl AppService { pub async fn pr_merge_analysis( &self, ctx: &Session, wk_name: &str, repo_name: &str, number: i64, ) -> Result { let (repo_id, _) = self.pr_resolve_repo(ctx, wk_name, repo_name).await?; let pr = self.pr_resolve(repo_id, number).await?; let mut client = MergeServiceClient::new(self.git.clone()); let resp = client .merge_analysis(tonic::Request::new(p::MergeAnalysisRequest { repo_id: repo_id.to_string(), oid_a: Some(p::ObjectId { value: pr.source_sha.clone(), }), oid_b: Some(p::ObjectId { value: pr.target_sha.clone(), }), })) .await .map_err(rpc_err)? .into_inner(); Ok(resp) } pub async fn pr_merge_base( &self, ctx: &Session, wk_name: &str, repo_name: &str, number: i64, ) -> Result { let (repo_id, _) = self.pr_resolve_repo(ctx, wk_name, repo_name).await?; let pr = self.pr_resolve(repo_id, number).await?; let mut client = MergeServiceClient::new(self.git.clone()); let resp = client .merge_base(tonic::Request::new(p::MergeBaseRequest { repo_id: repo_id.to_string(), oid_a: Some(p::ObjectId { value: pr.source_sha.clone(), }), oid_b: Some(p::ObjectId { value: pr.target_sha.clone(), }), })) .await .map_err(rpc_err)? .into_inner(); Ok(resp) } #[tracing::instrument(skip(self, ctx, params), fields(workspace = %wk_name, repo = %repo_name, pr = %number))] pub async fn pr_merge( &self, ctx: &Session, wk_name: &str, repo_name: &str, number: i64, params: MergePullRequest, ) -> Result { let method = params.method.unwrap_or_else(|| "merge".to_string()); with_op_metric(&self.metrics.pr_merge_total, &[&method], async { let user_uid = session_user(ctx)?; let (repo_id, _) = self.pr_resolve_repo_admin(ctx, wk_name, repo_name).await?; let pr = self.pr_resolve(repo_id, number).await?; if pr.state != "open" { return Err(AppError::BadRequest( "pull request is not open".to_string(), )); } if pr.draft { return Err(AppError::BadRequest( "draft pull request cannot be merged".to_string(), )); } let now = chrono::Utc::now(); let merge_result_sha = match method.as_str() { "merge" => { let mut client = MergeServiceClient::new(self.git.clone()); let resp = client .merge_commit(tonic::Request::new(p::MergeCommitRequest { repo_id: repo_id.to_string(), params: Some(p::MergeCommitParams { their_commit: Some(p::ObjectId { value: pr.source_sha.clone(), }), author: Some(p::CommitSignature { name: format!("merge: {}", pr.title), email: "noreply@gitdata.ai".to_string(), time_secs: now.timestamp(), offset_minutes: 0, }), committer: Some(p::CommitSignature { name: "gitdata".to_string(), email: "noreply@gitdata.ai".to_string(), time_secs: now.timestamp(), offset_minutes: 0, }), message: params.commit_message.unwrap_or_else( || { format!( "Merge pull request #{}: {}", pr.number, pr.title ) }, ), update_ref: Some(format!( "refs/heads/{}", pr.target_branch )), options: None, }), })) .await .map_err(rpc_err)? .into_inner(); resp.oid.map(|oid| oid.value).unwrap_or_default() } "squash" => { let mut client = MergeServiceClient::new(self.git.clone()); let resp = client .squash_commit(tonic::Request::new( p::SquashCommitRequest { repo_id: repo_id.to_string(), params: Some(p::SquashCommitParams { their_commit: Some(p::ObjectId { value: pr.source_sha.clone(), }), options: None, }), }, )) .await .map_err(rpc_err)? .into_inner(); resp.oid.map(|oid| oid.value).unwrap_or_default() } _ => { return Err(AppError::BadRequest( "merge method must be 'merge' or 'squash'".to_string(), )); } }; sqlx::query( "UPDATE pull_request SET state = 'merged', merged_by = $1, merged_at = $2, \ target_sha = $3, updated_at = $2 WHERE id = $4", ) .bind(user_uid) .bind(now) .bind(&merge_result_sha) .bind(pr.id) .execute(self.db.writer()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let pr = self.pr_resolve(repo_id, number).await?; self.pr_build_response(pr).await }).await } pub async fn pr_merge_abort( &self, ctx: &Session, wk_name: &str, repo_name: &str, ) -> Result<(), AppError> { let (repo_id, _) = self.pr_resolve_repo_admin(ctx, wk_name, repo_name).await?; let mut client = MergeServiceClient::new(self.git.clone()); client .merge_abort(tonic::Request::new(p::MergeAbortRequest { repo_id: repo_id.to_string(), })) .await .map_err(rpc_err)?; Ok(()) } pub async fn pr_update_branch( &self, ctx: &Session, wk_name: &str, repo_name: &str, number: i64, ) -> Result { let (repo_id, _) = self.pr_resolve_repo(ctx, wk_name, repo_name).await?; let pr = self.pr_resolve(repo_id, number).await?; if pr.state != "open" { return Err(AppError::BadRequest( "pull request is not open".to_string(), )); } let source_sha = self .branch_head_sha(pr.source_repo, &pr.source_branch) .await?; let target_sha = self.branch_head_sha(repo_id, &pr.target_branch).await?; let now = chrono::Utc::now(); sqlx::query( "UPDATE pull_request SET source_sha = $1, target_sha = $2, updated_at = $3 WHERE id = $4", ) .bind(&source_sha) .bind(&target_sha) .bind(now) .bind(pr.id) .execute(self.db.writer()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let pr = self.pr_resolve(repo_id, number).await?; self.pr_build_response(pr).await } }