| 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.extensions.api.GerritApi; |
| import com.google.gerrit.extensions.client.ListChangesOption; |
| import com.google.gerrit.extensions.common.ChangeInfo; |
| import com.google.gerrit.extensions.common.RevisionInfo; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.inject.ImplementedBy; |
| import com.google.inject.Inject; |
| import com.googlesource.gerrit.plugins.its.base.its.ItsConfig; |
| import java.util.EnumSet; |
| 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 PatchSetDb db; |
| private final ItsConfig itsConfig; |
| |
| @ImplementedBy(PatchSetDbImpl.class) |
| public interface PatchSetDb { |
| public String getRevision(PatchSet.Id patchSetId); |
| } |
| |
| public static class PatchSetDbImpl implements PatchSetDb { |
| private final GerritApi gApi; |
| |
| @Inject |
| public PatchSetDbImpl(GerritApi gApi) { |
| this.gApi = gApi; |
| } |
| |
| @Override |
| public String getRevision(PatchSet.Id patchSetId) { |
| try { |
| ChangeInfo info = |
| gApi.changes() |
| .id(patchSetId.getParentKey().get()) |
| .get(EnumSet.of(ListChangesOption.ALL_REVISIONS)); |
| for (Map.Entry<String, RevisionInfo> e : info.revisions.entrySet()) { |
| if (e.getValue()._number == patchSetId.get()) { |
| return e.getKey(); |
| } |
| } |
| return null; |
| } catch (RestApiException e) { |
| // previous is still empty to indicate that there was no previous |
| // accessible patch set. We treat every occurrence as added. |
| } |
| return null; |
| } |
| } |
| |
| @Inject |
| IssueExtractor(ItsConfig itsConfig, CommitMessageFetcher commitMessageFetcher, PatchSetDb 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); |
| String previousPatchSet = db.getRevision(previousPatchSetId); |
| if (previousPatchSet != null) { |
| previous = getIssueIds(projectName, previousPatchSet); |
| } |
| } |
| |
| 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; |
| } |
| } |