@Immutable
public class ShortBranchIssue implements Trackable {
+ private final String key;
private final Integer line;
private final String message;
private final String lineHash;
private final RuleKey ruleKey;
private final String status;
- private final String resolution;
- public ShortBranchIssue(@Nullable Integer line, String message, @Nullable String lineHash, RuleKey ruleKey, String status, @Nullable String resolution) {
+ public ShortBranchIssue(String key, @Nullable Integer line, String message, @Nullable String lineHash, RuleKey ruleKey, String status) {
+ this.key = key;
this.line = line;
this.message = message;
this.lineHash = lineHash;
this.ruleKey = ruleKey;
this.status = status;
- this.resolution = resolution;
+ }
+
+ public String getKey() {
+ return key;
}
@CheckForNull
return status;
}
- public String getResolution() {
- return resolution;
+ @Override
+ public int hashCode() {
+ return key.hashCode();
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ShortBranchIssue other = (ShortBranchIssue) obj;
+ return key.equals(other.key);
+ }
+
}
return executeLargeInputs(issueKeys, issueKeys1 -> mapper(session).selectByIssuesAndType(issueKeys1, changeType));
}
+ public List<IssueChangeDto> selectByIssueKeys(DbSession session, Collection<String> issueKeys) {
+ return executeLargeInputs(issueKeys, issueKeys1 -> mapper(session).selectByIssues(issueKeys1));
+ }
+
public Optional<IssueChangeDto> selectCommentByKey(DbSession session, String commentKey) {
return Optional.ofNullable(mapper(session).selectByKeyAndType(commentKey, IssueChangeDto.TYPE_COMMENT));
}
List<IssueChangeDto> selectByIssuesAndType(@Param("issueKeys") List<String> issueKeys,
@Param("changeType") String changeType);
+ List<IssueChangeDto> selectByIssues(@Param("issueKeys") List<String> issueKeys);
+
List<IssueChangeDto> selectChangelogOfNonClosedIssuesByComponent(@Param("componentUuid") String componentUuid, @Param("changeType") String changeType);
}
*/
package org.sonar.db.issue;
-import com.google.common.base.Preconditions;
import java.io.Serializable;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.apache.commons.lang.builder.ToStringStyle;
import org.sonar.api.rule.RuleKey;
import org.sonar.core.issue.ShortBranchIssue;
-import org.sonar.db.rule.RuleDefinitionDto;
-
-import static com.google.common.base.Preconditions.checkArgument;
public final class ShortBranchIssueDto implements Serializable {
private Integer line;
private String checksum;
private String status;
- private String resolution;
// joins
private String ruleKey;
return this;
}
- @CheckForNull
- public String getResolution() {
- return resolution;
- }
-
- public ShortBranchIssueDto setResolution(@Nullable String s) {
- this.resolution = s;
- return this;
- }
-
@CheckForNull
public String getChecksum() {
return checksum;
}
public static ShortBranchIssue toShortBranchIssue(ShortBranchIssueDto dto) {
- return new ShortBranchIssue(dto.getLine(), dto.getMessage(), dto.getChecksum(), dto.getRuleKey(), dto.getStatus(), dto.getResolution());
+ return new ShortBranchIssue(dto.getKey(), dto.getLine(), dto.getMessage(), dto.getChecksum(), dto.getRuleKey(), dto.getStatus());
}
}
</foreach>
order by c.created_at
</select>
+
+ <select id="selectByIssues" parameterType="map" resultType="IssueChange">
+ select
+ <include refid="issueChangeColumns"/>
+ from issue_changes c
+ where c.issue_key in
+ <foreach collection="issueKeys" open="(" close=")" item="key" separator=",">
+ #{key,jdbcType=VARCHAR}
+ </foreach>
+ </select>
<select id="selectByKeyAndType" parameterType="map" resultType="IssueChange">
select
i.message as message,
i.line as line,
i.status as status,
- i.resolution as resolution,
i.checksum as checksum,
r.plugin_rule_key as ruleKey,
r.plugin_name as ruleRepo
*/
package org.sonar.db.issue;
+import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.junit.Rule;
assertThat(changelog.get(0).diffs().get("severity").oldValue()).isEqualTo("MAJOR");
}
+ @Test
+ public void select_issue_changes_from_issues_key() {
+ db.prepareDbUnit(getClass(), "shared.xml");
+
+ List<IssueChangeDto> changelog = underTest.selectByIssueKeys(db.getSession(), Arrays.asList("1000", "1001"));
+ assertThat(changelog).hasSize(5);
+ }
+
@Test
public void selectChangelogOfNonClosedIssuesByComponent() {
db.prepareDbUnit(getClass(), "selectChangelogOfNonClosedIssuesByComponent.xml");
import org.junit.rules.ExpectedException;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.utils.System2;
-import org.sonar.core.issue.ShortBranchIssue;
import org.sonar.db.DbTester;
import org.sonar.db.RowNotFoundException;
import org.sonar.db.component.ComponentDto;
assertThat(fp.getChecksum()).isEqualTo(fpIssue.getChecksum());
assertThat(fp.getRuleKey()).isEqualTo(fpIssue.getRuleKey());
assertThat(fp.getStatus()).isEqualTo(fpIssue.getStatus());
- assertThat(fp.getResolution()).isEqualTo(fpIssue.getResolution());
assertThat(fp.getLine()).isNotNull();
assertThat(fp.getLine()).isNotZero();
assertThat(fp.getChecksum()).isNotEmpty();
assertThat(fp.getRuleKey()).isNotNull();
assertThat(fp.getStatus()).isNotNull();
- assertThat(fp.getResolution()).isNotNull();
}
private static IssueDto newIssueDto(String key) {
}
}
- private void fillNewOpenIssues(Component component, Iterable<DefaultIssue> issues, DiskCache<DefaultIssue>.DiskAppender cacheAppender) {
+ private void fillNewOpenIssues(Component component, Iterable<DefaultIssue> newIssues, DiskCache<DefaultIssue>.DiskAppender cacheAppender) {
List<DefaultIssue> list = new ArrayList<>();
- issues.forEach(issue -> {
+ newIssues.forEach(issue -> {
issueLifecycle.initNewOpenIssue(issue);
list.add(issue);
});
for (Map.Entry<DefaultIssue, DefaultIssue> entry : matched.entrySet()) {
DefaultIssue raw = entry.getKey();
DefaultIssue base = entry.getValue();
- issueLifecycle.copyExistingOpenIssue(raw, base);
+ issueLifecycle.copyExistingOpenIssueFromLongLivingBranch(raw, base);
process(component, raw, cacheAppender);
}
}
import com.google.common.annotations.VisibleForTesting;
import java.util.Date;
-import javax.annotation.Nullable;
import org.sonar.api.issue.Issue;
import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.DefaultIssueComment;
+import org.sonar.core.issue.FieldDiffs;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.util.Uuids;
import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder;
issue.setEffort(debtCalculator.calculate(issue));
}
- public void copyExistingOpenIssue(DefaultIssue raw, DefaultIssue base) {
+ public void copyExistingOpenIssueFromLongLivingBranch(DefaultIssue raw, DefaultIssue base) {
raw.setKey(Uuids.create());
raw.setNew(false);
raw.setCopied(true);
}
}
- public void copyResolution(DefaultIssue raw, String status, @Nullable String resolution) {
- raw.setStatus(status);
- raw.setResolution(resolution);
+ public void mergeIssueFromShortLivingBranch(DefaultIssue raw, DefaultIssue fromShortLiving) {
+ raw.setCopied(true);
+ raw.setType(fromShortLiving.type());
+ raw.setResolution(fromShortLiving.resolution());
+ raw.setStatus(fromShortLiving.status());
+ raw.setAssignee(fromShortLiving.assignee());
+ raw.setAuthorLogin(fromShortLiving.authorLogin());
+ raw.setTags(fromShortLiving.tags());
+ raw.setAttributes(fromShortLiving.attributes());
+ if (fromShortLiving.manualSeverity()) {
+ raw.setManualSeverity(true);
+ raw.setSeverity(fromShortLiving.severity());
+ }
+ fromShortLiving.comments().forEach(c -> raw.addComment(DefaultIssueComment.copy(raw.key(), c)));
+ fromShortLiving.changes().forEach(c -> raw.addChange(FieldDiffs.copy(raw.key(), c)));
}
public void mergeExistingOpenIssue(DefaultIssue raw, DefaultIssue base) {
import org.sonar.server.computation.task.projectanalysis.component.Component;
public class ShortBranchIssueStatusCopier {
- private final ShortBranchIssuesLoader resolvedShortBranchIssuesLoader;
+ private final ShortBranchIssuesLoader shortBranchIssuesLoader;
private final SimpleTracker<DefaultIssue, ShortBranchIssue> tracker;
private final IssueLifecycle issueLifecycle;
this(resolvedShortBranchIssuesLoader, new SimpleTracker<>(), issueLifecycle);
}
- public ShortBranchIssueStatusCopier(ShortBranchIssuesLoader resolvedShortBranchIssuesLoader, SimpleTracker<DefaultIssue, ShortBranchIssue> tracker,
- IssueLifecycle issueLifecycle) {
- this.resolvedShortBranchIssuesLoader = resolvedShortBranchIssuesLoader;
+ public ShortBranchIssueStatusCopier(ShortBranchIssuesLoader shortBranchIssuesLoader, SimpleTracker<DefaultIssue, ShortBranchIssue> tracker, IssueLifecycle issueLifecycle) {
+ this.shortBranchIssuesLoader = shortBranchIssuesLoader;
this.tracker = tracker;
this.issueLifecycle = issueLifecycle;
}
public void updateStatus(Component component, Collection<DefaultIssue> newIssues) {
- Collection<ShortBranchIssue> shortBranchIssues = resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component);
+ Collection<ShortBranchIssue> shortBranchIssues = shortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component);
Tracking<DefaultIssue, ShortBranchIssue> tracking = tracker.track(newIssues, shortBranchIssues);
- for (Map.Entry<DefaultIssue, ShortBranchIssue> e : tracking.getMatchedRaws().entrySet()) {
+ Map<DefaultIssue, ShortBranchIssue> matchedRaws = tracking.getMatchedRaws();
+
+ Map<ShortBranchIssue, DefaultIssue> defaultIssues = shortBranchIssuesLoader.loadDefaultIssuesWithChanges(matchedRaws.values());
+
+ for (Map.Entry<DefaultIssue, ShortBranchIssue> e : matchedRaws.entrySet()) {
ShortBranchIssue issue = e.getValue();
- issueLifecycle.copyResolution(e.getKey(), issue.getStatus(), issue.getResolution());
+ issueLifecycle.mergeIssueFromShortLivingBranch(e.getKey(), defaultIssues.get(issue));
}
}
}
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
+import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.ShortBranchIssue;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueChangeDto;
+import org.sonar.db.issue.IssueDto;
import org.sonar.db.issue.ShortBranchIssueDto;
import org.sonar.server.computation.task.projectanalysis.component.Component;
import org.sonar.server.computation.task.projectanalysis.component.ShortBranchComponentsWithIssues;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toMap;
+
public class ShortBranchIssuesLoader {
private final ShortBranchComponentsWithIssues shortBranchComponentsWithIssues;
.collect(Collectors.toList());
}
}
+
+ public Map<ShortBranchIssue, DefaultIssue> loadDefaultIssuesWithChanges(Collection<ShortBranchIssue> lightIssues) {
+ if (lightIssues.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<String, ShortBranchIssue> issuesByKey = lightIssues.stream().collect(Collectors.toMap(ShortBranchIssue::getKey, i -> i));
+ try (DbSession session = dbClient.openSession(false)) {
+
+ Map<String, List<IssueChangeDto>> changeDtoByIssueKey = dbClient.issueChangeDao()
+ .selectByIssueKeys(session, issuesByKey.keySet()).stream().collect(groupingBy(IssueChangeDto::getIssueKey));
+
+ return dbClient.issueDao().selectByKeys(session, issuesByKey.keySet())
+ .stream()
+ .map(IssueDto::toDefaultIssue)
+ .peek(i -> setChanges(changeDtoByIssueKey, i))
+ .collect(toMap(i -> issuesByKey.get(i.key()), i -> i));
+ }
+ }
+
+ private static void setChanges(Map<String, List<IssueChangeDto>> changeDtoByIssueKey, DefaultIssue i) {
+ changeDtoByIssueKey.get(i.key()).forEach(c -> {
+ switch (c.getChangeType()) {
+ case IssueChangeDto.TYPE_FIELD_CHANGE:
+ i.addChange(c.toFieldDiffs());
+ break;
+ case IssueChangeDto.TYPE_COMMENT:
+ i.addComment(c.toComment());
+ break;
+ default:
+ throw new IllegalStateException("Unknow change type: " + c.getChangeType());
+ }
+ });
+ }
}
*/
package org.sonar.server.computation.task.projectanalysis.step;
-import org.sonar.api.issue.IssueComment;
import org.sonar.api.utils.System2;
import org.sonar.core.issue.DefaultIssue;
-import org.sonar.core.issue.DefaultIssueComment;
-import org.sonar.core.issue.FieldDiffs;
import org.sonar.core.util.CloseableIterator;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
-import org.sonar.db.issue.IssueChangeDto;
import org.sonar.db.issue.IssueChangeMapper;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.issue.IssueMapper;
import org.sonar.server.computation.task.projectanalysis.issue.RuleRepository;
import org.sonar.server.computation.task.projectanalysis.issue.UpdateConflictResolver;
import org.sonar.server.computation.task.step.ComputationStep;
+import org.sonar.server.issue.IssueStorage;
public class PersistIssuesStep implements ComputationStep {
DefaultIssue issue = issues.next();
boolean saved = persistIssueIfRequired(mapper, issue);
if (saved) {
- insertChanges(changeMapper, issue);
+ IssueStorage.insertChanges(changeMapper, issue);
}
}
dbSession.flushStatements();
}
}
- private static void insertChanges(IssueChangeMapper mapper, DefaultIssue issue) {
- for (IssueComment comment : issue.comments()) {
- DefaultIssueComment c = (DefaultIssueComment) comment;
- if (c.isNew()) {
- IssueChangeDto changeDto = IssueChangeDto.of(c);
- mapper.insert(changeDto);
- }
- }
- FieldDiffs diffs = issue.currentChange();
- if (!issue.isNew() && diffs != null) {
- IssueChangeDto changeDto = IssueChangeDto.of(issue.key(), diffs);
- mapper.insert(changeDto);
- }
- }
-
@Override
public String getDescription() {
return "Persist issues";
protected abstract IssueDto doUpdate(DbSession batchSession, long now, DefaultIssue issue);
- private void insertChanges(IssueChangeMapper mapper, DefaultIssue issue) {
+ public static void insertChanges(IssueChangeMapper mapper, DefaultIssue issue) {
for (IssueComment comment : issue.comments()) {
DefaultIssueComment c = (DefaultIssueComment) comment;
if (c.isNew()) {
if (!issue.isNew() && diffs != null) {
IssueChangeDto changeDto = IssueChangeDto.of(issue.key(), diffs);
mapper.insert(changeDto);
+ } else if (issue.isCopied()) {
+ for (FieldDiffs d : issue.changes()) {
+ IssueChangeDto changeDto = IssueChangeDto.of(issue.key(), d);
+ mapper.insert(changeDto);
+ }
}
}
}
@Test
- public void copyResolution() {
- DefaultIssue issue = new DefaultIssue();
- underTest.copyResolution(issue, "status", "resolution");
- assertThat(issue.resolution()).isEqualTo("resolution");
- assertThat(issue.status()).isEqualTo("status");
+ public void mergeIssueFromShortLivingBranch() {
+ DefaultIssue raw = new DefaultIssue();
+ DefaultIssue fromShort = new DefaultIssue();
+ fromShort.setResolution("resolution");
+ fromShort.setStatus("status");
+ underTest.mergeIssueFromShortLivingBranch(raw, fromShort);
+ assertThat(raw.resolution()).isEqualTo("resolution");
+ assertThat(raw.status()).isEqualTo("status");
}
@Test
when(debtCalculator.calculate(raw)).thenReturn(DEFAULT_DURATION);
- underTest.copyExistingOpenIssue(raw, base);
+ underTest.copyExistingOpenIssueFromLongLivingBranch(raw, base);
assertThat(raw.isNew()).isFalse();
assertThat(raw.isCopied()).isTrue();
*/
package org.sonar.server.computation.task.projectanalysis.issue;
+import com.google.common.collect.ImmutableMap;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.sonar.api.issue.Issue;
import org.sonar.core.issue.tracking.SimpleTracker;
import org.sonar.server.computation.task.projectanalysis.component.Component;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyListOf;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
@Test
public void update_status_on_matches() {
- ShortBranchIssue shortBranchIssue = newShortBranchIssue(createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null));
+ DefaultIssue issue1 = createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null);
+ ShortBranchIssue shortBranchIssue = newShortBranchIssue(issue1);
DefaultIssue newIssue = createIssue("issue2", "rule1", Issue.STATUS_OPEN, null);
when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Collections.singleton(shortBranchIssue));
+ when(resolvedShortBranchIssuesLoader.loadDefaultIssuesWithChanges(anyListOf(ShortBranchIssue.class))).thenReturn(ImmutableMap.of(shortBranchIssue, issue1));
copier.updateStatus(component, Collections.singleton(newIssue));
- verify(issueLifecycle).copyResolution(newIssue, shortBranchIssue.getStatus(), shortBranchIssue.getResolution());
+ ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+ verify(resolvedShortBranchIssuesLoader).loadDefaultIssuesWithChanges(captor.capture());
+ assertThat(captor.getValue()).containsOnly(shortBranchIssue);
+ verify(issueLifecycle).mergeIssueFromShortLivingBranch(newIssue, issue1);
}
@Test
public void prefer_resolved_issues() {
ShortBranchIssue shortBranchIssue1 = newShortBranchIssue(createIssue("issue1", "rule1", Issue.STATUS_CONFIRMED, null));
ShortBranchIssue shortBranchIssue2 = newShortBranchIssue(createIssue("issue2", "rule1", Issue.STATUS_CONFIRMED, null));
- ShortBranchIssue shortBranchIssue3 = newShortBranchIssue(createIssue("issue3", "rule1", Issue.STATUS_RESOLVED, Issue.RESOLUTION_FALSE_POSITIVE));
+ DefaultIssue issue3 = createIssue("issue3", "rule1", Issue.STATUS_RESOLVED, Issue.RESOLUTION_FALSE_POSITIVE);
+ ShortBranchIssue shortBranchIssue3 = newShortBranchIssue(issue3);
DefaultIssue newIssue = createIssue("newIssue", "rule1", Issue.STATUS_OPEN, null);
when(resolvedShortBranchIssuesLoader.loadCandidateIssuesForMergingInTargetBranch(component)).thenReturn(Arrays.asList(shortBranchIssue1, shortBranchIssue2, shortBranchIssue3));
+ when(resolvedShortBranchIssuesLoader.loadDefaultIssuesWithChanges(anyListOf(ShortBranchIssue.class))).thenReturn(ImmutableMap.of(shortBranchIssue3, issue3));
copier.updateStatus(component, Collections.singleton(newIssue));
- verify(issueLifecycle).copyResolution(newIssue, Issue.STATUS_RESOLVED, Issue.RESOLUTION_FALSE_POSITIVE);
+ verify(issueLifecycle).mergeIssueFromShortLivingBranch(newIssue, issue3);
}
private static DefaultIssue createIssue(String key, String ruleKey, String status, @Nullable String resolution) {
}
private ShortBranchIssue newShortBranchIssue(DefaultIssue i) {
- return new ShortBranchIssue(i.line(), i.message(), i.getLineHash(), i.ruleKey(), i.status(), i.resolution());
+ return new ShortBranchIssue(i.key(), i.line(), i.message(), i.getLineHash(), i.ruleKey(), i.status());
}
}
// true if the issue did not exist in the previous scan.
private boolean isNew = true;
- // true if the issue is being copied to a different branch
+ // true if the issue is being copied between branch
private boolean isCopied = false;
// True if the issue did exist in the previous scan but not in the current one. That means
return comment;
}
+ /**
+ * Copy a comment from another issue
+ */
+ public static DefaultIssueComment copy(String issueKey, IssueComment c) {
+ DefaultIssueComment comment = new DefaultIssueComment();
+ comment.setIssueKey(issueKey);
+ comment.setKey(Uuids.create());
+ comment.setUserLogin(c.userLogin());
+ comment.setMarkdownText(c.markdownText());
+ comment.setCreatedAt(c.createdAt()).setUpdatedAt(c.updatedAt());
+ comment.setNew(true);
+ return comment;
+ }
+
@Override
public String markdownText() {
return markdownText;
isNew = b;
return this;
}
+
}
private final Map<String, Diff> diffs = Maps.newLinkedHashMap();
+ /**
+ * Copy a diff from another issue
+ */
+ public static FieldDiffs copy(String issueKey, FieldDiffs c) {
+ FieldDiffs result = new FieldDiffs();
+ result.issueKey = issueKey;
+ result.userLogin = c.userLogin;
+ result.creationDate = c.creationDate;
+ result.diffs.putAll(c.diffs);
+ return result;
+ }
+
public Map<String, Diff> diffs() {
return diffs;
}
return sb.toString();
}
}
+
}