* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
package org.sonar.server.computation.issue;
import com.google.common.base.Optional;
-import java.util.HashMap;
-import java.util.Map;
import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import org.apache.commons.lang.StringUtils;
-import org.sonar.batch.protocol.output.BatchReport;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
import org.sonar.core.issue.DefaultIssue;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.protobuf.DbFileSources;
-import org.sonar.server.computation.batch.BatchReportReader;
import org.sonar.server.computation.component.Component;
-import org.sonar.server.source.SourceService;
+import org.sonar.server.computation.scm.ScmInfo;
+import org.sonar.server.computation.scm.ScmInfoRepository;
+
+import static org.apache.commons.lang.StringUtils.defaultIfEmpty;
/**
* Detect the SCM author and SQ assignee.
*/
public class IssueAssigner extends IssueVisitor {
- private final DbClient dbClient;
- private final SourceService sourceService;
- private final BatchReportReader reportReader;
+ private static final Logger LOGGER = Loggers.get(IssueAssigner.class);
+
+ private final ScmInfoRepository scmInfoRepository;
private final DefaultAssignee defaultAssigne;
private final ScmAccountToUser scmAccountToUser;
- private long lastCommitDate = 0L;
private String lastCommitAuthor = null;
- private BatchReport.Changesets scmChangesets = null;
+ private ScmInfo scmChangesets = null;
- public IssueAssigner(DbClient dbClient, SourceService sourceService, BatchReportReader reportReader,
- ScmAccountToUser scmAccountToUser, DefaultAssignee defaultAssigne) {
- this.dbClient = dbClient;
- this.sourceService = sourceService;
- this.reportReader = reportReader;
+ public IssueAssigner(ScmInfoRepository scmInfoRepository, ScmAccountToUser scmAccountToUser, DefaultAssignee defaultAssigne) {
+ this.scmInfoRepository = scmInfoRepository;
this.scmAccountToUser = scmAccountToUser;
this.defaultAssigne = defaultAssigne;
}
// optimization - do not load SCM data of this component if there are no new issues
loadScmChangesetsIfNeeded(component);
- String scmAuthor = guessScmAuthor(issue.line());
+ String scmAuthor = guessScmAuthor(issue);
issue.setAuthorLogin(scmAuthor);
if (scmAuthor != null) {
String assigneeLogin = scmAccountToUser.getNullable(scmAuthor);
private void loadScmChangesetsIfNeeded(Component component) {
if (scmChangesets == null) {
- scmChangesets = loadScmChangesetsFromReport(component);
- if (scmChangesets == null) {
- scmChangesets = loadScmChangesetsFromDb(component);
+ Optional<ScmInfo> scmInfoOptional = scmInfoRepository.getScmInfo(component);
+ if (scmInfoOptional.isPresent()) {
+ scmChangesets = scmInfoOptional.get();
+ lastCommitAuthor = scmChangesets.getLatestChangeset().getAuthor();
}
- computeLastCommitDateAndAuthor();
}
}
@Override
public void afterComponent(Component component) {
- lastCommitDate = 0L;
lastCommitAuthor = null;
scmChangesets = null;
}
* then get the last committer on the file.
*/
@CheckForNull
- private String guessScmAuthor(@Nullable Integer line) {
+ private String guessScmAuthor(DefaultIssue issue) {
+ Integer line = issue.line();
String author = null;
- if (line != null && line <= scmChangesets.getChangesetIndexByLineCount()) {
- BatchReport.Changesets.Changeset changeset = scmChangesets.getChangeset(scmChangesets.getChangesetIndexByLine(line - 1));
- author = changeset.hasAuthor() ? changeset.getAuthor() : null;
- }
- return StringUtils.defaultIfEmpty(author, lastCommitAuthor);
- }
-
- private BatchReport.Changesets loadScmChangesetsFromReport(Component component) {
- return reportReader.readChangesets(component.getReportAttributes().getRef());
- }
-
- private BatchReport.Changesets loadScmChangesetsFromDb(Component component) {
- DbSession dbSession = dbClient.openSession(false);
- try {
- Optional<Iterable<DbFileSources.Line>> lines = sourceService.getLines(dbSession, component.getUuid(), 1, Integer.MAX_VALUE);
- Map<String, BatchReport.Changesets.Changeset> changesetByRevision = new HashMap<>();
- BatchReport.Changesets.Builder scmBuilder = BatchReport.Changesets.newBuilder()
- .setComponentRef(component.getReportAttributes().getRef());
- if (lines.isPresent()) {
- for (DbFileSources.Line sourceLine : lines.get()) {
- String scmRevision = sourceLine.getScmRevision();
- if (scmRevision == null || changesetByRevision.get(scmRevision) == null) {
- BatchReport.Changesets.Changeset.Builder changeSetBuilder = BatchReport.Changesets.Changeset.newBuilder();
- if (sourceLine.hasScmAuthor()) {
- changeSetBuilder.setAuthor(sourceLine.getScmAuthor());
- }
- if (sourceLine.hasScmDate()) {
- changeSetBuilder.setDate(sourceLine.getScmDate());
- }
- if (scmRevision != null) {
- changeSetBuilder.setRevision(scmRevision);
- }
-
- BatchReport.Changesets.Changeset changeset = changeSetBuilder.build();
- scmBuilder.addChangeset(changeset);
- scmBuilder.addChangesetIndexByLine(scmBuilder.getChangesetCount() - 1);
- if (scmRevision != null) {
- changesetByRevision.put(scmRevision, changeset);
- }
- } else {
- scmBuilder.addChangesetIndexByLine(scmBuilder.getChangesetList().indexOf(changesetByRevision.get(scmRevision)));
- }
- }
- }
- return scmBuilder.build();
- } finally {
- dbClient.closeSession(dbSession);
- }
- }
-
- private void computeLastCommitDateAndAuthor() {
- for (BatchReport.Changesets.Changeset changeset : scmChangesets.getChangesetList()) {
- if (changeset.hasAuthor() && changeset.hasDate() && changeset.getDate() > lastCommitDate) {
- lastCommitDate = changeset.getDate();
- lastCommitAuthor = changeset.getAuthor();
+ if (line != null && scmChangesets != null) {
+ if (scmChangesets.hasChangesetForLine(line)) {
+ author = scmChangesets.getChangesetForLine(line).getAuthor();
+ } else {
+ LOGGER.warn("No SCM info has been found for issue {}", issue);
}
}
+ return defaultIfEmpty(author, lastCommitAuthor);
}
}
--- /dev/null
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.server.computation.issue;
+
+import org.junit.Test;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.scm.Changeset;
+import org.sonar.server.computation.scm.ScmInfoRepositoryRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.computation.component.ReportComponent.builder;
+
+public class IssueAssignerTest {
+
+ static final int FILE_REF = 1;
+ static final Component FILE = builder(Component.Type.FILE, FILE_REF).setKey("FILE_KEY").setUuid("FILE_UUID").build();
+
+ @org.junit.Rule
+ public LogTester logTester = new LogTester();
+
+ @org.junit.Rule
+ public ScmInfoRepositoryRule scmInfoRepository = new ScmInfoRepositoryRule();
+
+ ScmAccountToUser scmAccountToUser = mock(ScmAccountToUser.class);
+ DefaultAssignee defaultAssignee = mock(DefaultAssignee.class);
+
+ IssueAssigner underTest = new IssueAssigner(scmInfoRepository, scmAccountToUser, defaultAssignee);
+
+ @Test
+ public void set_author_to_issue() throws Exception {
+ setSingleChangeset("john", 123456789L, "rev-1");
+ DefaultIssue issue = new DefaultIssue().setNew(true).setLine(1);
+
+ underTest.onIssue(FILE, issue);
+
+ assertThat(issue.authorLogin()).isEqualTo("john");
+ }
+
+ @Test
+ public void nothing_to_do_if_issue_is_not_new() throws Exception {
+ setSingleChangeset("john", 123456789L, "rev-1");
+ DefaultIssue issue = new DefaultIssue().setNew(false).setLine(1);
+
+ underTest.onIssue(FILE, issue);
+
+ assertThat(issue.authorLogin()).isNull();
+ }
+
+ @Test
+ public void nothing_to_do_if_no_changeset() throws Exception {
+ DefaultIssue issue = new DefaultIssue().setNew(true).setLine(1);
+
+ underTest.onIssue(FILE, issue);
+
+ assertThat(issue.authorLogin()).isNull();
+ }
+
+ @Test
+ public void set_assignee_to_issue() throws Exception {
+ addScmUser("john", "John C");
+ setSingleChangeset("john", 123456789L, "rev-1");
+ DefaultIssue issue = new DefaultIssue().setNew(true).setLine(1);
+
+ underTest.onIssue(FILE, issue);
+
+ assertThat(issue.assignee()).isEqualTo("John C");
+ }
+
+ @Test
+ public void set_default_assignee_if_author_not_found() throws Exception {
+ addScmUser("john", null);
+ setSingleChangeset("john", 123456789L, "rev-1");
+ when(defaultAssignee.getLogin()).thenReturn("John C");
+ DefaultIssue issue = new DefaultIssue().setNew(true).setLine(1);
+
+ underTest.onIssue(FILE, issue);
+
+ assertThat(issue.assignee()).isEqualTo("John C");
+ }
+
+ @Test
+ public void set_last_committer_when_line_is_null() throws Exception {
+ addScmUser("henry", "Henry V");
+ Changeset changeset1 = Changeset.newChangesetBuilder()
+ .setAuthor("john")
+ .setDate(123456789L)
+ .setRevision("rev-1")
+ .build();
+ // Latest changeset
+ Changeset changeset2 = Changeset.newChangesetBuilder()
+ .setAuthor("henry")
+ .setDate(1234567810L)
+ .setRevision("rev-2")
+ .build();
+ scmInfoRepository.setScmInfo(FILE_REF, changeset1, changeset2, changeset1);
+
+ DefaultIssue issue = new DefaultIssue()
+ .setNew(true)
+ .setLine(null);
+
+ underTest.onIssue(FILE, issue);
+
+ assertThat(issue.assignee()).isEqualTo("Henry V");
+ }
+
+ @Test
+ public void set_last_committer_when_line_is_bigger_than_changeset_size() throws Exception {
+ addScmUser("john", "John C");
+ Changeset changeset = Changeset.newChangesetBuilder()
+ .setAuthor("john")
+ .setDate(123456789L)
+ .setRevision("rev-1")
+ .build();
+ scmInfoRepository.setScmInfo(FILE_REF, changeset, changeset);
+ DefaultIssue issue = new DefaultIssue().setNew(true).setLine(3);
+
+ underTest.onIssue(FILE, issue);
+
+ assertThat(issue.assignee()).isEqualTo("John C");
+ }
+
+ @Test
+ public void display_warning_when_line_is_above_max_size() throws Exception {
+ setSingleChangeset("john", 123456789L, "rev-1");
+ DefaultIssue issue = new DefaultIssue().setNew(true).setLine(2);
+
+ underTest.onIssue(FILE, issue);
+
+ assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly("No SCM info has been found for issue DefaultIssue[key=<null>,componentUuid=<null>,componentKey=<null>,moduleUuid=<null>,moduleUuidPath=<null>," +
+ "projectUuid=<null>,projectKey=<null>,ruleKey=<null>,language=<null>,severity=<null>,manualSeverity=false,message=<null>,line=2,effortToFix=<null>,debt=<null>," +
+ "status=<null>,resolution=<null>,reporter=<null>,assignee=<null>,checksum=<null>,attributes=<null>,authorLogin=<null>,actionPlanKey=<null>,comments=<null>,tags=<null>,l" +
+ "ocations=<null>,creationDate=<null>,updateDate=<null>,closeDate=<null>,currentChange=<null>,changes=<null>,isNew=true,beingClosed=false,onDisabledRule=false," +
+ "isChanged=false,sendNotifications=false,selectedAt=<null>]");
+ }
+
+ private void setSingleChangeset(String author, Long date, String revision) {
+ scmInfoRepository.setScmInfo(FILE_REF,
+ Changeset.newChangesetBuilder()
+ .setAuthor(author)
+ .setDate(date)
+ .setRevision(revision)
+ .build());
+ }
+
+ private void addScmUser(String scmAccount, String userName) {
+ when(scmAccountToUser.getNullable(scmAccount)).thenReturn(userName);
+ }
+
+}