You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 17:11:11