解决Stash API分支比较误判:如何获取正确提交差异及作者并发送通知?
解决Stash分支内容一致但Commit ID不同的差异比较问题
问题场景
现有feature、release、develop三个分支,feature分支分别合并至develop和release分支,但两次合并生成的Commit ID不同,导致Stash默认的compare API判定develop与release存在差异,实际两者变更内容完全一致。需要针对多个仓库,准确识别release分支有但develop分支没有的真实变更,获取对应提交的作者信息并发送通知邮件,之前调用Stash的compare REST API返回结果不符合预期。
核心原因
Stash默认的compare API是基于提交历史进行对比,而非代码内容。合并操作会生成新的Commit ID,即使变更内容完全相同,也会被判定为不同提交,导致虚假差异。
解决方案步骤
1. 先判断分支内容是否完全一致
通过获取分支最新提交的treeId(代码内容快照的唯一标识)来判断:
- 调用Stash API
/rest/api/1.0/projects/{projectKey}/repos/{repoSlug}/commits/{branchName},拿到对应分支最新commit的treeId - 若develop和release的
treeId相同,说明内容完全一致,直接跳过该仓库
2. 获取分支的共同祖先提交
若内容不一致,先找到develop和release分支的共同祖先commit,以此为基准对比两边的变更:
- 调用Stash API
/rest/api/1.0/projects/{projectKey}/repos/{repoSlug}/commits/{branch1}/merge-base/{branch2},传入develop和release分支名,获取共同祖先的commit ID
3. 获取两边相对于共同祖先的提交列表
分别获取release和develop分支相对于共同祖先的所有提交:
- 调用
/rest/api/1.0/projects/{projectKey}/repos/{repoSlug}/compare/commits?from={commonAncestor}&to={releaseBranch},拿到release分支的变更提交 - 同理调用该API,将
to参数改为develop分支,获取develop的变更提交
4. 过滤虚假差异(内容重复的提交)
通过提交的treeId判断内容是否一致:
- 收集develop所有提交的
treeId到集合中 - 遍历release的提交,过滤掉
treeId存在于develop集合中的提交,剩下的就是release独有的真实变更
5. 收集作者信息并发送邮件
从过滤后的提交中提取作者的邮箱和姓名,批量发送通知邮件。
Java Spring Boot 实现示例
Stash API 调用服务类
@Service public class StashApiService { private final WebClient webClient; public StashApiService(WebClient.Builder webClientBuilder) { // 配置Stash地址和认证信息 this.webClient = webClientBuilder .baseUrl("https://your-stash-domain/rest/api/1.0") .defaultHeaders(headers -> headers.setBasicAuth("your-username", "your-password")) .build(); } // 获取分支最新提交的treeId public String getBranchLatestTreeId(String projectKey, String repoSlug, String branchName) { return webClient.get() .uri("/projects/{projectKey}/repos/{repoSlug}/commits/{branchName}", projectKey, repoSlug, branchName) .retrieve() .bodyToMono(CommitDTO.class) .map(CommitDTO::getTreeId) .block(); } // 获取两个分支的共同祖先commit ID public String getMergeBaseCommit(String projectKey, String repoSlug, String branch1, String branch2) { return webClient.get() .uri("/projects/{projectKey}/repos/{repoSlug}/commits/{branch1}/merge-base/{branch2}", projectKey, repoSlug, branch1, branch2) .retrieve() .bodyToMono(MergeBaseDTO.class) .map(MergeBaseDTO::getCommitId) .block(); } // 获取两个commit之间的所有提交 public List<CommitDTO> getCommitsBetween(String projectKey, String repoSlug, String fromCommit, String toCommit) { // 处理分页,这里简化处理,实际需循环获取所有页 return webClient.get() .uri(uriBuilder -> uriBuilder .path("/projects/{projectKey}/repos/{repoSlug}/compare/commits") .queryParam("from", fromCommit) .queryParam("to", toCommit) .queryParam("limit", 1000) .build(projectKey, repoSlug)) .retrieve() .bodyToMono(CompareCommitsDTO.class) .map(CompareCommitsDTO::getCommits) .block(); } // 内部DTO类 public static class CommitDTO { private String id; private String treeId; private AuthorDTO author; private String message; // Getters and Setters public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTreeId() { return treeId; } public void setTreeId(String treeId) { this.treeId = treeId; } public AuthorDTO getAuthor() { return author; } public void setAuthor(AuthorDTO author) { this.author = author; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } public static class AuthorDTO { private String name; private String email; // Getters and Setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } public static class MergeBaseDTO { private String commitId; // Getters and Setters public String getCommitId() { return commitId; } public void setCommitId(String commitId) { this.commitId = commitId; } } public static class CompareCommitsDTO { private List<CommitDTO> commits; // Getters and Setters public List<CommitDTO> getCommits() { return commits; } public void setCommits(List<CommitDTO> commits) { this.commits = commits; } } }
业务处理与邮件发送类
@Service public class BranchDiffProcessor { private final StashApiService stashApiService; private final JavaMailSender mailSender; public BranchDiffProcessor(StashApiService stashApiService, JavaMailSender mailSender) { this.stashApiService = stashApiService; this.mailSender = mailSender; } public void processAllRepos(List<RepoConfig> repos) { for (RepoConfig repo : repos) { String projectKey = repo.getProjectKey(); String repoSlug = repo.getRepoSlug(); // 1. 判断分支内容是否完全一致 String developTreeId = stashApiService.getBranchLatestTreeId(projectKey, repoSlug, "develop"); String releaseTreeId = stashApiService.getBranchLatestTreeId(projectKey, repoSlug, "release"); if (developTreeId.equals(releaseTreeId)) { continue; } // 2. 获取共同祖先 String mergeBase = stashApiService.getMergeBaseCommit(projectKey, repoSlug, "develop", "release"); // 3. 获取两边提交列表 List<StashApiService.CommitDTO> releaseCommits = stashApiService.getCommitsBetween(projectKey, repoSlug, mergeBase, "release"); List<StashApiService.CommitDTO> developCommits = stashApiService.getCommitsBetween(projectKey, repoSlug, mergeBase, "develop"); // 4. 过滤release独有的提交 Set<String> developTreeIds = developCommits.stream() .map(StashApiService.CommitDTO::getTreeId) .collect(Collectors.toSet()); List<StashApiService.CommitDTO> uniqueReleaseCommits = releaseCommits.stream() .filter(commit -> !developTreeIds.contains(commit.getTreeId())) .collect(Collectors.toList()); // 5. 收集作者并发送邮件 if (!uniqueReleaseCommits.isEmpty()) { Set<StashApiService.AuthorDTO> authors = uniqueReleaseCommits.stream() .map(StashApiService.CommitDTO::getAuthor) .collect(Collectors.toSet()); sendNotificationMail(repo, uniqueReleaseCommits, authors); } } } private void sendNotificationMail(RepoConfig repo, List<StashApiService.CommitDTO> commits, Set<StashApiService.AuthorDTO> authors) { SimpleMailMessage message = new SimpleMailMessage(); message.setTo(authors.stream().map(StashApiService.AuthorDTO::getEmail).toArray(String[]::new)); message.setSubject(String.format("[提醒] 仓库 %s 的release分支存在未同步至develop的变更", repo.getRepoSlug())); StringBuilder content = new StringBuilder(); content.append("仓库:").append(repo.getRepoSlug()).append("\n"); content.append("以下提交仅存在于release分支,请同步至develop:\n\n"); for (StashApiService.CommitDTO commit : commits) { content.append(String.format("Commit ID: %s\n", commit.getId())); content.append(String.format("作者: %s <%s>\n", commit.getAuthor().getName(), commit.getAuthor().getEmail())); content.append(String.format("提交信息: %s\n\n", commit.getMessage())); } message.setText(content.toString()); mailSender.send(message); } // 仓库配置类 public static class RepoConfig { private String projectKey; private String repoSlug; // Getters and Setters public String getProjectKey() { return projectKey; } public void setProjectKey(String projectKey) { this.projectKey = projectKey; } public String getRepoSlug() { return repoSlug; } public void setRepoSlug(String repoSlug) { this.repoSlug = repoSlug; } } }
注意事项
- Stash API需要正确的权限,确保调用账号能访问目标仓库的提交信息
- 处理分页:若提交数量超过API默认的
limit(通常为25),需循环调用API并递增start参数,获取所有提交 - 异常处理:添加API调用的异常捕获,处理网络错误、权限不足等情况
内容的提问来源于stack exchange,提问作者vicky
相关产品推荐
相关产品推荐

