use git::context::facade::GitContext; /// 组装好的模型上下文:提示词正文 + 是否发生截断。 #[derive(Debug, Clone)] pub struct BuiltContext { pub text: String, pub truncated: bool, } const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; impl super::AgentService { /// PR 评审上下文:merge-base..head 的三点差异。 pub async fn build_pull_diff( &self, space_name: &str, repo_name: &str, head_sha: &str, base_ref: &str, ) -> Result<(BuiltContext, String), git::share::errors::GitError> { let ctx = self .git .context(space_name.to_string(), repo_name.to_string()) .await?; let plan = self .git .merge_plan(&ctx, &format!("refs/heads/{base_ref}"), head_sha) .await?; let from = plan .merge_base .clone() .unwrap_or_else(|| plan.base_sha.clone()); let diff = self .git .diff_raw( space_name.to_string(), repo_name.to_string(), from.clone(), plan.head_sha.clone(), ) .await?; let label = format!("{}..{}", short(&from), short(&plan.head_sha)); Ok((self.truncate_diff(diff), label)) } /// Commit 评审上下文:父提交..该提交的差异。 pub async fn build_commit_diff( &self, space_name: &str, repo_name: &str, commit_sha: &str, ) -> Result<(BuiltContext, String), git::share::errors::GitError> { let ctx = self .git .context(space_name.to_string(), repo_name.to_string()) .await?; let baby = self.git.baby(&ctx); let info = baby .get_commit(commit_sha) .await .map_err(git::share::errors::map_baby)?; let parent = info .parents .first() .map(|p| p.to_string()) .unwrap_or_else(|| EMPTY_TREE.to_string()); let sha = info.id.to_string(); let diff = self .git .diff_raw( space_name.to_string(), repo_name.to_string(), parent.clone(), sha.clone(), ) .await?; let label = format!("{}..{}", short(&parent), short(&sha)); Ok((self.truncate_diff(diff), label)) } /// 提交列表(评审用不到,但总结/release 复用同一格式化)。 pub(crate) fn format_commits(commits: &[git::service::merge::MergeCommitDto]) -> String { let mut out = String::new(); for commit in commits { out.push_str(&format!( "- {} {} ({}, {})\n", short(&commit.sha), commit.subject.trim(), commit.author_name, commit.author_at )); } out } /// 按字节预算截断差异:超限时逐文件裁剪并标记 truncated。 pub(crate) fn truncate_diff(&self, diff: String) -> BuiltContext { let limit = self.config().ai.context_max_bytes; if diff.len() <= limit { return BuiltContext { text: diff, truncated: false, }; } let mut text = String::with_capacity(limit); let mut truncated = false; for file in split_diff_files(&diff) { if text.len() + file.len() > limit { truncated = true; break; } text.push_str(&file); } if text.is_empty() { // 单个文件本身就超限:按字符边界硬截断。 text = diff.chars().take(limit).collect(); truncated = true; } BuiltContext { text, truncated } } } /// 按 `diff --git` 切分,保留每段完整补丁文本。 pub fn split_diff_files(diff: &str) -> Vec { let mut files: Vec = Vec::new(); let mut current = String::new(); for line in diff.split_inclusive('\n') { if line.starts_with("diff --git ") && !current.is_empty() { files.push(std::mem::take(&mut current)); } current.push_str(line); } if !current.is_empty() { files.push(current); } files } pub(crate) fn short(sha: &str) -> String { sha.chars().take(8).collect() } /// 让 `GitContext` 在本 crate 内可用(避免调用方各自引入 git crate)。 pub type RepoContext = GitContext;