@Override
  public OntrackSVNRevisionInfo getOntrackRevisionInfo(SVNRepository repository, long revision) {

    // Gets information about the revision
    SVNRevisionInfo basicInfo = svnService.getRevisionInfo(repository, revision);
    SVNChangeLogRevision changeLogRevision =
        svnService.createChangeLogRevision(repository, basicInfo);

    // Gets the first copy event on this path after this revision
    SVNLocation firstCopy = svnService.getFirstCopyAfter(repository, basicInfo.toLocation());

    // Data to collect
    Collection<BuildView> buildViews = new ArrayList<>();
    Collection<BranchStatusView> branchStatusViews = new ArrayList<>();
    // Loops over all authorised branches
    for (Project project : structureService.getProjectList()) {
      // Filter on SVN configuration: must be present and equal to the one the revision info is
      // looked into
      Property<SVNProjectConfigurationProperty> projectSvnConfig =
          propertyService.getProperty(project, SVNProjectConfigurationPropertyType.class);
      if (!projectSvnConfig.isEmpty()
          && repository
              .getConfiguration()
              .getName()
              .equals(projectSvnConfig.getValue().getConfiguration().getName())) {
        for (Branch branch : structureService.getBranchesForProject(project.getId())) {
          // Filter on branch type
          // Filter on SVN configuration: must be present
          if (branch.getType() != BranchType.TEMPLATE_DEFINITION
              && propertyService.hasProperty(branch, SVNBranchConfigurationPropertyType.class)) {
            // Identifies a possible build given the path/revision and the first copy
            Optional<Build> build = lookupBuild(basicInfo.toLocation(), firstCopy, branch);
            // Build found
            if (build.isPresent()) {
              // Gets the build view
              BuildView buildView = structureService.getBuildView(build.get());
              // Adds it to the list
              buildViews.add(buildView);
              // Collects the promotions for the branch
              branchStatusViews.add(structureService.getEarliestPromotionsAfterBuild(build.get()));
            }
          }
        }
      }
    }

    // OK
    return new OntrackSVNRevisionInfo(
        repository.getConfiguration(), changeLogRevision, buildViews, branchStatusViews);
  }
 @Override
 public Form getEditionForm(ProjectEntity entity, AutoPromotionProperty value) {
   PromotionLevel promotionLevel = (PromotionLevel) entity;
   return Form.create()
       .with(
           MultiSelection.of("validationStamps")
               .label("Validation stamps")
               .items(
                   structureService
                       .getValidationStampListForBranch(promotionLevel.getBranch().getId())
                       .stream()
                       .map(
                           vs ->
                               new ValidationStampSelection(
                                   vs, value != null && value.containsDirectValidationStamp(vs)))
                       .collect(Collectors.toList()))
               .help(
                   "When all the selected validation stamps have passed for a build, the promotion will automatically be granted."))
       .with(
           Text.of("include")
               .label("Include")
               .optional()
               .value(value != null ? value.getInclude() : "")
               .help("Regular expression to select validation stamps by name"))
       .with(
           Text.of("exclude")
               .label("Exclude")
               .optional()
               .value(value != null ? value.getExclude() : "")
               .help("Regular expression to exclude validation stamps by name"));
 }
 private List<ValidationStamp> readValidationStamps(JsonNode validationStampIds) {
   List<ValidationStamp> validationStampList;
   if (validationStampIds.isArray()) {
     List<Integer> ids = new ArrayList<>();
     validationStampIds.forEach(id -> ids.add(id.asInt()));
     // Reading the validation stamps and then the names
     validationStampList =
         ids.stream()
             .map(id -> structureService.getValidationStamp(ID.of(id)))
             .collect(Collectors.toList());
   } else {
     throw new AutoPromotionPropertyCannotParseException(
         "Cannot get the list of validation stamps");
   }
   return validationStampList;
 }
 @Override
 public AutoPromotionProperty copy(
     ProjectEntity sourceEntity,
     AutoPromotionProperty value,
     ProjectEntity targetEntity,
     Function<String, String> replacementFn) {
   PromotionLevel targetPromotionLevel = (PromotionLevel) targetEntity;
   return new AutoPromotionProperty(
       value
           .getValidationStamps()
           .stream()
           .map(
               vs ->
                   structureService.findValidationStampByName(
                       targetPromotionLevel.getBranch().getProject().getName(),
                       targetPromotionLevel.getBranch().getName(),
                       vs.getName()))
           .filter(Optional::isPresent)
           .map(Optional::get)
           .collect(Collectors.toList()),
       value.getInclude(),
       value.getExclude());
 }
  @Override
  public OntrackSVNIssueInfo getIssueInfo(String configurationName, String issueKey) {
    // Repository
    SVNRepository repository = svnService.getRepository(configurationName);
    // Issue service
    ConfiguredIssueService configuredIssueService = repository.getConfiguredIssueService();
    if (configuredIssueService == null) {
      // No issue service configured
      return OntrackSVNIssueInfo.empty(repository.getConfiguration());
    }
    // Gets the details about the issue
    Issue issue = configuredIssueService.getIssue(issueKey);

    // For each configured branch
    Map<String, BranchRevision> branchRevisions = new HashMap<>();
    svnService.forEachConfiguredBranch(
        config -> Objects.equals(configurationName, config.getConfiguration().getName()),
        (branch, branchConfig) -> {
          String branchPath = branchConfig.getCuredBranchPath();
          // List of linked issues
          Collection<String> linkedIssues =
              configuredIssueService
                  .getLinkedIssues(branch.getProject(), issue)
                  .stream()
                  .map(Issue::getKey)
                  .collect(Collectors.toList());
          // Gets the last raw revision on this branch
          issueRevisionDao
              .findLastRevisionByIssuesAndBranch(repository.getId(), linkedIssues, branchPath)
              .ifPresent(
                  revision ->
                      branchRevisions.put(
                          branchPath, new BranchRevision(branchPath, revision, false)));
        });

    // Until all revisions are complete in respect of their merges...
    while (!BranchRevision.areComplete(branchRevisions.values())) {
      // Gets the incomplete revisions
      Collection<BranchRevision> incompleteRevisions =
          branchRevisions
              .values()
              .stream()
              .filter(br -> !br.isComplete())
              .collect(Collectors.toList());
      // For each of them, gets the list of revisions it was merged to
      incompleteRevisions.forEach(
          br -> {
            List<Long> merges =
                revisionDao.getMergesForRevision(repository.getId(), br.getRevision());
            // Marks the current revision as complete
            branchRevisions.put(br.getPath(), br.complete());
            // Gets the revision info for each merged revision
            List<TRevision> revisions =
                merges
                    .stream()
                    .map(r -> revisionDao.get(repository.getId(), r))
                    .collect(Collectors.toList());
            // For each revision path, compares with current stored revision
            revisions.forEach(
                t -> {
                  String branch = t.getBranch();
                  // Existing branch revision?
                  BranchRevision existingBranchRevision = branchRevisions.get(branch);
                  if (existingBranchRevision == null
                      || t.getRevision() > existingBranchRevision.getRevision()) {
                    branchRevisions.put(branch, new BranchRevision(branch, t.getRevision(), true));
                  }
                });
          });
    }

    // We now have the last revision for this issue on each branch...
    List<OntrackSVNIssueRevisionInfo> issueRevisionInfos = new ArrayList<>();
    branchRevisions
        .values()
        .forEach(
            br -> {
              // Loads the revision info
              SVNRevisionInfo basicInfo = svnService.getRevisionInfo(repository, br.getRevision());
              SVNChangeLogRevision changeLogRevision =
                  svnService.createChangeLogRevision(repository, basicInfo);
              // Info to collect
              OntrackSVNIssueRevisionInfo issueRevisionInfo =
                  OntrackSVNIssueRevisionInfo.of(changeLogRevision);
              // Gets the branch from the branch path
              AtomicReference<Branch> rBranch = new AtomicReference<>();
              svnService.forEachConfiguredBranch(
                  config -> Objects.equals(configurationName, config.getConfiguration().getName()),
                  (candidate, branchConfig) -> {
                    String branchPath = branchConfig.getCuredBranchPath();
                    if (Objects.equals(br.getPath(), branchPath)) {
                      rBranch.set(candidate);
                    }
                  });
              Branch branch = rBranch.get();
              if (branch != null) {
                // Collects branch info
                SCMIssueCommitBranchInfo branchInfo = SCMIssueCommitBranchInfo.of(branch);
                // Gets the first copy event on this path after this revision
                SVNLocation firstCopy =
                    svnService.getFirstCopyAfter(repository, basicInfo.toLocation());
                // Identifies a possible build given the path/revision and the first copy
                Optional<Build> buildAfterCommit =
                    lookupBuild(basicInfo.toLocation(), firstCopy, branch);
                if (buildAfterCommit.isPresent()) {
                  Build build = buildAfterCommit.get();
                  // Gets the build view
                  BuildView buildView = structureService.getBuildView(build);
                  // Adds it to the list
                  branchInfo = branchInfo.withBuildView(buildView);
                  // Collects the promotions for the branch
                  branchInfo =
                      branchInfo.withBranchStatusView(
                          structureService.getEarliestPromotionsAfterBuild(build));
                }
                // OK
                issueRevisionInfo.add(branchInfo);
              }
              // OK
              issueRevisionInfos.add(issueRevisionInfo);
            });

    // Gets the list of revisions & their basic info (order from latest to oldest)
    List<SVNChangeLogRevision> revisions =
        svnService
            .getRevisionsForIssueKey(repository, issueKey)
            .stream()
            .map(
                revision ->
                    svnService.createChangeLogRevision(
                        repository, svnService.getRevisionInfo(repository, revision)))
            .collect(Collectors.toList());

    // OK
    return new OntrackSVNIssueInfo(
        repository.getConfiguration(),
        repository.getConfiguredIssueService().getIssueServiceConfigurationRepresentation(),
        issue,
        issueRevisionInfos,
        revisions);
  }