blob: 75a1cc333274c65e16a94f63b0490dc6d88c9c43 [file] [log] [blame]
package com.googlesource.gerrit.plugins.its.base.util;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.its.base.its.ItsConfig;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IssueExtractor {
private static final Logger log = LoggerFactory.getLogger(IssueExtractor.class);
private final CommitMessageFetcher commitMessageFetcher;
private final ReviewDb db;
private final ItsConfig itsConfig;
@Inject
IssueExtractor(ItsConfig itsConfig, CommitMessageFetcher commitMessageFetcher, ReviewDb db) {
this.commitMessageFetcher = commitMessageFetcher;
this.db = db;
this.itsConfig = itsConfig;
}
/**
* Gets issue ids from a string.
*
* @param haystack String to extract issue ids from
* @return array of {@link String}. Each String being a found issue id.
*/
public String[] getIssueIds(String haystack) {
Pattern pattern = itsConfig.getIssuePattern();
if (pattern == null) return new String[] {};
log.debug("Matching '{}' against {}", haystack, pattern.pattern());
Set<String> issues = Sets.newHashSet();
Matcher matcher = pattern.matcher(haystack);
int groupIdx = itsConfig.getIssuePatternGroupIndex();
while (matcher.find()) {
String issueId = matcher.group(groupIdx);
if (!Strings.isNullOrEmpty(issueId)) {
issues.add(issueId);
}
}
return issues.toArray(new String[issues.size()]);
}
/**
* Helper function for {@link #getIssueIds(String, String)}.
*
* <p>Adds a text's issues for a given occurrence to the map returned by {@link
* #getIssueIds(String, String)}.
*
* @param text The text to extract issues from.
* @param occurrence The occurrence the issues get added at in {@code map}.
* @param map The map that the issues should get added to.
*/
private void addIssuesOccurrence(String text, String occurrence, Map<String, Set<String>> map) {
for (String issue : getIssueIds(text)) {
Set<String> occurrences = map.computeIfAbsent(issue, k -> Sets.newLinkedHashSet());
occurrences.add(occurrence);
}
}
/**
* Gets issues for a commit.
*
* @param projectName The project to fetch {@code commitId} from.
* @param commitId The commit id to fetch issues for.
* @return A mapping, whose keys are issue ids and whose values is a set of places where the issue
* occurs. Each issue occurs at least in "somewhere". Issues from the first line get tagged
* with an occurrence "subject". Issues in the last block get tagged with "footer". Issues
* occurring between "subject" and "footer" get tagged with "body".
*/
public Map<String, Set<String>> getIssueIds(String projectName, String commitId) {
Map<String, Set<String>> ret = Maps.newHashMap();
String commitMessage = commitMessageFetcher.fetchGuarded(projectName, commitId);
addIssuesOccurrence(commitMessage, "somewhere", ret);
String[] lines = commitMessage.split("\n");
if (lines.length > 0) {
// Parsing for "subject"
addIssuesOccurrence(lines[0], "subject", ret);
// Determining footer line numbers
int currentLine = lines.length - 1;
while (currentLine >= 0 && lines[currentLine].isEmpty()) {
currentLine--;
}
int footerEnd = currentLine + 1;
while (currentLine >= 0 && !lines[currentLine].isEmpty()) {
currentLine--;
}
int footerStart = currentLine + 1;
if (footerStart == 0) {
// The first block of non-blank lines is not considered a footer, so
// we adjust that.
footerStart = -1;
}
// Parsing for "body", and "footer"
String body = null;
String footer = null;
if (footerStart == -1) {
// No footer could be found. So all lines after the first one (that's
// the subject) is the body.
//body = String[] templateParameters =
// Arrays.copyOfRange(allParameters, 1, allParameters.length);
if (lines.length > 0) {
body = StringUtils.join(lines, "\n", 1, lines.length);
}
} else {
body = StringUtils.join(lines, "\n", 1, footerStart - 1);
StringBuilder footerBuilder = new StringBuilder();
for (int lineIdx = footerStart; lineIdx < footerEnd; lineIdx++) {
String line = lines[lineIdx];
// Adding occurrences for footer keys
int colonIdx = line.indexOf(':');
if (colonIdx > 0) {
// tag of length at least 1
String tag = line.substring(0, colonIdx);
addIssuesOccurrence(line, "footer-" + tag, ret);
}
// Putting back together the footer to a single String
if (lineIdx > footerStart) {
footerBuilder.append('\n');
}
footerBuilder.append(line);
}
footer = StringUtils.join(lines, "\n", footerStart, footerEnd);
}
if (body != null) {
addIssuesOccurrence(body, "body", ret);
}
if (footer != null) {
addIssuesOccurrence(footer, "footer", ret);
}
}
return ret;
}
/**
* Gets issues for a commit with new issue occurrences marked as "added".
*
* <p>Fetches the patch set's immediate ancestor and compares issue occurrences between them. Any
* new occurrence gets marked as "added." So if for example in patch sets 1, and 2 issue 23 occurs
* in the subject, while in patch set the issue occurs in the body, then patch set 2 has
* occurrences "somewhere", and "subject" for issue 23. Patch set 3 has occurrences "somewhere",
* "body", and "body-added" for issue 23.
*
* @param projectName The project to fetch {@code commitId} from.
* @param commitId The commit id to fetch issues for.
* @param patchSetId The patch set for the {@code commitId}. If it is null, no occurrence can be
* marked as "-added".
* @return A mapping, whose keys are issue ids and whose values is a set of places where the issue
* occurs. Each issue occurs at least in "somewhere". Issues from the first line get tagged
* with an occurrence "subject". Issues in the last block get tagged with "footer". Issues
* occurring between "subject" and "footer" get tagged with "body".
*/
public Map<String, Set<String>> getIssueIds(
String projectName, String commitId, PatchSet.Id patchSetId) {
Map<String, Set<String>> current = getIssueIds(projectName, commitId);
if (patchSetId != null) {
Map<String, Set<String>> previous = Maps.newHashMap();
if (patchSetId.get() != 1) {
PatchSet.Id previousPatchSetId =
new PatchSet.Id(patchSetId.getParentKey(), patchSetId.get() - 1);
try {
PatchSet previousPatchSet = db.patchSets().get(previousPatchSetId);
if (previousPatchSet != null) {
previous = getIssueIds(projectName, previousPatchSet.getRevision().get());
}
} catch (OrmException e) {
// previous is still empty to indicate that there was no previous
// accessible patch set. We treat every occurrence as added.
}
}
for (String issue : current.keySet()) {
Set<String> currentOccurrences = current.get(issue);
Set<String> previousOccurrences = previous.get(issue);
Set<String> newOccurrences;
if (previousOccurrences == null || previousOccurrences.isEmpty()) {
newOccurrences = Sets.newHashSet(currentOccurrences);
} else {
newOccurrences = Sets.newHashSet(currentOccurrences);
newOccurrences.removeAll(previousOccurrences);
}
for (String occurrence : newOccurrences) {
currentOccurrences.add("added@" + occurrence);
}
}
}
return current;
}
}