use crate::rpcgit::commit::types::CommitInfo; use models::pull_request::pull_requests::MergeMethod; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use uuid::Uuid; use crate::context::facade::GitContext; use crate::service::GitService; use crate::share::errors::{GitError, map_baby}; #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct MergePlanDto { pub base_sha: String, pub head_sha: String, pub merge_base: Option, pub fast_forwardable: bool, /// base 上 head 没有的提交数。 pub base_only: i64, /// head 上 base 没有的提交数。 pub head_only: i64, pub clean: bool, pub conflicts: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct MergeResultDto { /// `fast_forward` | `merge_commit` | `squash` | `rebase` pub method: String, pub base_ref: String, pub old_sha: String, pub new_sha: String, pub head_sha: String, /// rebase 时重写出来的提交(按时间正序)。 pub rewritten: Vec, } #[derive(Debug, Clone)] pub struct ExecuteMergeCmd { pub base_branch: String, pub head_revision: String, pub method: MergeMethod, pub message: Option, pub caller: i64, pub allow_ff_only: bool, pub summary: String, } fn hex(oid: &crate::rpcgit::oid::ObjectId) -> String { oid.to_string() } impl GitService { /// 合并在 KenShell 补齐 MergeService 之前不可用。 pub async fn merge_plan( &self, _ctx: &GitContext, _base: &str, _head: &str, ) -> Result { Err(GitError::Unimplemented( "merge planning requires KenShell MergeService", )) } pub async fn merge_execute( &self, _space_name: String, _repo_name: String, _cmd: ExecuteMergeCmd, ) -> Result { Err(GitError::Unimplemented( "merge execution requires KenShell MergeService", )) } } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct MergeCommitDto { pub sha: String, pub subject: String, pub message: String, pub author_name: String, pub author_email: String, pub author_at: OffsetDateTime, pub parents: Vec, } const MAX_LISTED_COMMITS: u32 = 200; fn commit_dto(info: CommitInfo) -> MergeCommitDto { MergeCommitDto { sha: hex(&info.id), subject: info.message.lines().next().unwrap_or_default().to_string(), message: info.message.clone(), author_name: info.author.name.clone(), author_email: info.author.email.clone(), author_at: info.author.time, parents: info.parents.iter().map(hex).collect(), } } impl GitService { /// `base..head` 的提交列表(正序,最多 200 条)。 pub async fn merge_commits( &self, space_name: String, repo_name: String, base: &str, head: &str, ) -> Result, GitError> { self.commits_between(space_name, repo_name, base, head, MAX_LISTED_COMMITS) .await } pub async fn recent_commits( &self, space_name: String, repo_name: String, revision: Option<&str>, limit: u32, skip: u32, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let reference = match revision.map(str::trim).filter(|value| !value.is_empty()) { Some(value) => value.to_string(), None => format!("refs/heads/{}", ctx.model.default_branch), }; let commits = self .baby(&ctx) .list_commits(crate::rpcgit::commit::types::ListCommitsOptions { revision: reference, max_count: Some(limit.min(MAX_LISTED_COMMITS).saturating_add(skip)), skip: Some(skip), ..Default::default() }) .await .map_err(map_baby)?; Ok(commits.into_iter().map(commit_dto).collect()) } pub async fn commits_between( &self, space_name: String, repo_name: String, base: &str, head: &str, limit: u32, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let baby = self.baby(&ctx); let base_oid = baby.rev_parse(base).await.map_err(map_baby)?; let head_oid = baby.rev_parse(head).await.map_err(map_baby)?; let ids = baby .commit_ids_between_reverse( &hex(&base_oid), &hex(&head_oid), None, Some(limit.min(MAX_LISTED_COMMITS)), ) .await .map_err(map_baby)?; let mut out = Vec::with_capacity(ids.len()); for id in ids { out.push(commit_dto( baby.get_commit(&hex(&id)).await.map_err(map_baby)?, )); } Ok(out) } pub async fn tag_target(&self, repo_id: Uuid, tag_name: &str) -> Option { use models::git::git_tags; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; git_tags::Entity::find() .filter(git_tags::Column::RepoId.eq(repo_id)) .filter(git_tags::Column::Name.eq(tag_name)) .one(self.db_read()) .await .ok() .flatten() .map(|row| row.commit_sha) } pub async fn default_branch_base( &self, space_name: String, repo_name: String, depth: u32, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let baby = self.baby(&ctx); let head = baby .rev_parse(&format!("refs/heads/{}", ctx.model.default_branch)) .await .map_err(map_baby)?; let mut cursor = head; for _ in 0..depth { let info = match baby.get_commit(&hex(&cursor)).await { Ok(info) => info, Err(_) => return Ok(None), }; match info.parents.first() { Some(parent) => cursor = parent.clone(), None => return Ok(None), } } Ok(Some(hex(&cursor))) } }