Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

222: Implement backport issue creation #346

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -34,6 +34,8 @@
import java.nio.file.Path;
import java.util.*;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.*;

public class IssueUpdater implements RepositoryUpdateConsumer, PullRequestUpdateConsumer {
private final IssueProject issueProject;
@@ -55,6 +57,148 @@ public class IssueUpdater implements RepositoryUpdateConsumer, PullRequestUpdate
this.fixVersion = fixVersion;
}

private final static Set<String> primaryTypes = Set.of("Bug", "New Feature", "Enhancement", "Task", "Sub-task");

private boolean isPrimaryIssue(Issue issue) {
var properties = issue.properties();
if (!properties.containsKey("type")) {
throw new RuntimeException("Unknown type for issue " + issue);
}
var type = properties.get("type");
return primaryTypes.contains(type);
}

private final static Pattern majorVersionPattern = Pattern.compile("([0-9]+)(u[0-9]+)?");

/**
* Extracts the major version part of the string, if possible.
*/
private Optional<String> majorVersion(String version) {
var matcher = majorVersionPattern.matcher(version);
if (matcher.matches()) {
return Optional.of(matcher.group(1));
} else {
return Optional.empty();
}
}

private List<Issue> findBackports(Issue primary) {
var links = primary.links();
return links.stream()
.filter(l -> l.issue().isPresent())
.map(l -> l.issue().get())
.filter(i -> i.properties().containsKey("type"))
.filter(i -> i.properties().get("type").equals("Backport"))
.collect(Collectors.toList());
}

private boolean isNonScratchVersion(String version) {
return !version.startsWith("tbd") && !version.toLowerCase().equals("unknown");
}

/**
* Return true if the issue's fixVersionList matches fixVersion.
*
* fixVersionsList must contain one entry that is an exact match for fixVersions; any
* other entries must be scratch values.
*/
private boolean matchVersion(Issue issue, String fixVersion) {
var nonScratch = issue.fixVersions().stream()
.filter(this::isNonScratchVersion)
.collect(Collectors.toList());
return nonScratch.size() == 1 && nonScratch.get(0).equals(fixVersion);
}

/**
* Return true if the issue's fixVersionList is a match for fixVersion, using "-pool" or "-open".
*
* If fixVersion has a major release of <N>, it matches the fixVersionList has an
* <N>-pool or <N>-open entry and all other entries are scratch values.
*/
private boolean matchPoolVersion(Issue issue, String fixVersion) {
var majorVersion = majorVersion(fixVersion);
if (majorVersion.isEmpty()) {
return false;
}
var poolVersion = majorVersion.get() + "-pool";
var openVersion = majorVersion.get() + "-open";

var nonScratch = issue.fixVersions().stream()
.filter(this::isNonScratchVersion)
.collect(Collectors.toList());
return nonScratch.size() == 1 && (nonScratch.get(0).equals(poolVersion) || nonScratch.get(0).equals(openVersion));
}

/**
* Return true if fixVersionList is empty or contains only scratch values.
*/
private boolean matchScratchVersion(Issue issue) {
var nonScratch = issue.fixVersions().stream()
.filter(this::isNonScratchVersion)
.collect(Collectors.toList());
return nonScratch.size() == 0;
}

/**
* Create a backport of issue.
*/
private Issue createBackportIssue(Issue primary) {
var properties = primary.properties();
properties.put("type", "Backport");

var backport = primary.project().createIssue(primary.title(), primary.body().lines().collect(Collectors.toList()), properties);

var backportLink = Link.create(backport, "backported by").build();
primary.addLink(backportLink);;
return backport;
}

/**
* Return issue or one of its backports that applies to fixVersion.
*
* If the main issue has the correct fixVersion, use it.
* If an existing Backport has the correct fixVersion, use it.
* If the main issue has a matching <N>-pool/open fixVersion, use it.
* If an existing Backport has a matching <N>-pool/open fixVersion, use it.
* If the main issue has a "scratch" fixVersion, use it.
* If an existing Backport has a "scratch" fixVersion, use it.
*
* Otherwise, create a new Backport.
*
* A "scratch" fixVersion is empty, "tbd.*", or "unknown".
*/
private Issue findIssue(Issue primary, String fixVersion) {
log.info("Searching for properly versioned issue for primary issue " + primary.id());
var candidates = Stream.concat(Stream.of(primary), findBackports(primary).stream()).collect(Collectors.toList());
candidates.forEach(c -> log.fine("Candidate: " + c.id() + " with versions: " + String.join(",", c.fixVersions())));
var matchingVersionIssue = candidates.stream()
.filter(i -> matchVersion(i, fixVersion))
.findFirst();
if (matchingVersionIssue.isPresent()) {
log.info("Issue " + matchingVersionIssue.get().id() + " has a correct fixVersion");
return matchingVersionIssue.get();
}

var matchingPoolVersionIssue = candidates.stream()
.filter(i -> matchPoolVersion(i, fixVersion))
.findFirst();
if (matchingPoolVersionIssue.isPresent()) {
log.info("Issue " + matchingPoolVersionIssue.get().id() + " has a matching pool version");
return matchingPoolVersionIssue.get();
}

var matchingScratchVersionIssue = candidates.stream()
.filter(this::matchScratchVersion)
.findFirst();
if (matchingScratchVersionIssue.isPresent()) {
log.info("Issue " + matchingScratchVersionIssue.get().id() + " has a scratch fixVersion");
return matchingScratchVersionIssue.get();
}

log.info("Creating new backport for " + primary.id());
return createBackportIssue(primary);
}

@Override
public void handleCommits(HostedRepository repository, Repository localRepository, List<Commit> commits, Branch branch) {
for (var commit : commits) {
@@ -68,6 +212,36 @@ public void handleCommits(HostedRepository repository, Repository localRepositor
continue;
}
var issue = optionalIssue.get();

// We only update primary type issues
if (!isPrimaryIssue(issue)) {
log.severe("Issue " + issue.id() + " isn't of a primary type - ignoring");
// TODO: search for the primary issue
continue;
}

// The actual issue to be updated can change depending on the fix version
String requestedVersion = null;
if (setFixVersion) {
requestedVersion = fixVersion;
if (requestedVersion == null) {
try {
var conf = localRepository.lines(Path.of(".jcheck/conf"), commit.hash());
if (conf.isPresent()) {
var parsed = JCheckConfiguration.parse(conf.get());
var version = parsed.general().version();
requestedVersion = version.orElse(null);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

if (requestedVersion != null) {
issue = findIssue(issue, requestedVersion);
}
}

var existingComments = issue.comments();
var hashUrl = repository.webUrl(commit.hash()).toString();
var alreadyPostedComment = existingComments.stream()
@@ -89,21 +263,11 @@ public void handleCommits(HostedRepository repository, Repository localRepositor
}

if (setFixVersion) {
var requestedVersion = fixVersion;
if (requestedVersion == null) {
try {
var conf = localRepository.lines(Path.of(".jcheck/conf"), commit.hash());
if (conf.isPresent()) {
var parsed = JCheckConfiguration.parse(conf.get());
var version = parsed.general().version();
requestedVersion = version.orElse(null);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

if (requestedVersion != null) {
// Remove any previously set versions (can only be scratch versions)
for (var oldVersion : issue.fixVersions()) {
issue.removeFixVersion(oldVersion);
}
issue.addFixVersion(requestedVersion);
}
}
@@ -159,6 +323,7 @@ public void handleRemovedIssue(PullRequest pr, org.openjdk.skara.vcs.openjdk.Iss
return;
}

realIssue.get().removeLink(pr.webUrl());
var link = Link.create(pr.webUrl(), "").build();
realIssue.get().removeLink(link);
}
}