use models::pull_request::{pull_request_event_line, pull_requests}; use models::repo::repos; use sea_orm::{ ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, }; use time::OffsetDateTime; use crate::sync::error::SyncResult; use crate::sync::event::PostReceiveTask; use export::AppDatabase; pub const EVENT_HEAD_UPDATED: &str = "head_updated"; pub const EVENT_HEAD_DELETED: &str = "head_deleted"; pub const EVENT_BASE_UPDATED: &str = "base_updated"; pub const ZERO_OID_LEN: usize = 40; /// push 之后把分支最新提交同步到受影响的开放合并请求。 pub async fn refresh_pulls_after_sync( db: &AppDatabase, repo: &repos::Model, task: &PostReceiveTask, ) -> SyncResult { let mut touched = 0u64; for update in &task.refs { let Some(branch) = update.name.strip_prefix("refs/heads/") else { continue; }; let deleted = update.new_oid.chars().all(|c| c == '0'); if let Err(error) = refresh_for_branch(db, repo, branch, &update.new_oid, deleted, task.actor_id).await { tracing::warn!( error = %error, repo_id = %repo.id, branch, "pull request sync skipped" ); } else { touched += 1; } } if touched > 0 { recount_open_pulls(db, repo.id).await?; } Ok(touched) } async fn refresh_for_branch( db: &AppDatabase, repo: &repos::Model, branch: &str, new_oid: &str, deleted: bool, actor: Option, ) -> SyncResult<()> { let open = pull_requests::Entity::find() .filter(pull_requests::Column::RepoId.eq(repo.id)) .filter(pull_requests::Column::HasMerged.eq(false)) .filter(pull_requests::Column::ClosedAt.is_null()) .filter(pull_requests::Column::HeadRepoId.is_null()) .all(db.read()) .await?; for pull in open { if pull.head_branch == branch { if deleted { record_event(db, &pull, actor, EVENT_HEAD_DELETED, None).await?; continue; } if pull.head_sha != new_oid { let mut model: pull_requests::ActiveModel = pull.clone().into(); model.head_sha = Set(new_oid.to_string()); model.updated_at = Set(Some(OffsetDateTime::now_utc())); model.update(db.write()).await?; record_event(db, &pull, actor, EVENT_HEAD_UPDATED, Some(new_oid)).await?; } } else if pull.base_branch == branch && !deleted && pull.base_sha != new_oid { let mut model: pull_requests::ActiveModel = pull.clone().into(); model.base_sha = Set(new_oid.to_string()); model.update(db.write()).await?; record_event(db, &pull, actor, EVENT_BASE_UPDATED, Some(new_oid)).await?; } } Ok(()) } async fn record_event( db: &AppDatabase, pull: &pull_requests::Model, actor: Option, event_type: &str, commit_sha: Option<&str>, ) -> SyncResult<()> { let row = pull_request_event_line::ActiveModel { id: Set(uuid::Uuid::now_v7()), pull_request_id: Set(pull.id), user_id: Set(actor.unwrap_or(pull.author_id)), event_type: Set(event_type.to_string()), commit_sha: Set(commit_sha.map(|sha| sha.to_string())), content: Set(None), created_at: Set(OffsetDateTime::now_utc()), }; row.insert(db.write()).await?; Ok(()) } /// 重新统计开放合并请求数量,写回 `repos.nums_pr`。 pub async fn recount_open_pulls(db: &AppDatabase, repo_id: uuid::Uuid) -> SyncResult<()> { let open = pull_requests::Entity::find() .filter(pull_requests::Column::RepoId.eq(repo_id)) .filter(pull_requests::Column::HasMerged.eq(false)) .filter(pull_requests::Column::ClosedAt.is_null()) .count(db.read()) .await?; let model = repos::ActiveModel { id: Set(repo_id), nums_pr: Set(i32::try_from(open).unwrap_or(i32::MAX)), // A partial update leaves every other column `NotSet`. ..Default::default() }; model.update(db.write()).await?; Ok(()) }