]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4309 Support concurrent modifications on issues made by batch and simultaneousl...
authorSimon Brandhof <simon.brandhof@gmail.com>
Tue, 4 Jun 2013 17:04:44 +0000 (19:04 +0200)
committerSimon Brandhof <simon.brandhof@gmail.com>
Tue, 4 Jun 2013 17:05:21 +0000 (19:05 +0200)
13 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensor.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java
sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml
sonar-core/src/test/java/org/sonar/core/issue/db/IssueMapperTest.java
sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/testUpdate.xml
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict-result.xml [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict.xml [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java

index 9796c39293b8830d9d8430558629b8ed0f2d53ed..cff5ec73080a1bf0337b985b486595268fcabce2 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.plugins.core.issue;
 
+import org.apache.commons.lang.time.DateUtils;
 import org.apache.ibatis.session.ResultContext;
 import org.apache.ibatis.session.ResultHandler;
 import org.sonar.api.batch.Sensor;
@@ -27,6 +28,9 @@ import org.sonar.api.resources.Project;
 import org.sonar.core.issue.db.IssueDao;
 import org.sonar.core.issue.db.IssueDto;
 
+import java.util.Calendar;
+import java.util.Date;
+
 /**
  * Load all the issues referenced during the previous scan.
  */
@@ -47,10 +51,15 @@ public class InitialOpenIssuesSensor implements Sensor {
 
   @Override
   public void analyse(Project project, SensorContext context) {
+    // Adding one second is a hack for resolving conflicts with concurrent user
+    // changes during issue persistence
+    final Date now = DateUtils.addSeconds(DateUtils.truncate(new Date(), Calendar.MILLISECOND), 1);
+
     issueDao.selectNonClosedIssuesByModule(project.getId(), new ResultHandler() {
       @Override
       public void handleResult(ResultContext rc) {
         IssueDto dto = (IssueDto) rc.getResultObject();
+        dto.setSelectedAt(now);
         initialOpenIssuesStack.addIssue(dto);
       }
     });
index 1728af0ffd3c7ad418d9463ed70b0f23db4bc8d3..539dd16224feec9391acd575da756678596a4cb8 100644 (file)
@@ -133,6 +133,7 @@ public class IssueTrackingDecorator implements Decorator {
       issue.setNew(false);
       issue.setEndOfLife(false);
       issue.setOnDisabledRule(false);
+      issue.setSelectedAt(ref.getSelectedAt());
 
       // fields to update with old values
       issue.setActionPlanKey(ref.getActionPlanKey());
index 412d4deb26fb9b7c85bdf5f40e16c75733ef3bd1..b0d87a27c9b58106c659b0df17717f825e12fa70 100644 (file)
@@ -29,7 +29,6 @@ import org.sonar.api.utils.KeyValueFormat;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
-
 import java.io.Serializable;
 import java.util.Date;
 
@@ -66,6 +65,11 @@ public final class IssueDto implements Serializable {
   private Date createdAt;
   private Date updatedAt;
 
+  /**
+   * Temporary date used only during scan
+   */
+  private Date selectedAt;
+
   // joins
   private String ruleKey;
   private String ruleRepo;
@@ -305,6 +309,16 @@ public final class IssueDto implements Serializable {
     return rootComponentKey;
   }
 
+  @CheckForNull
+  public Date getSelectedAt() {
+    return selectedAt;
+  }
+
+  public IssueDto setSelectedAt(Date d) {
+    this.selectedAt = d;
+    return this;
+  }
+
   /**
    * Only for unit tests
    */
@@ -357,7 +371,7 @@ public final class IssueDto implements Serializable {
       .setIssueCreationDate(issue.creationDate())
       .setIssueCloseDate(issue.closeDate())
       .setIssueUpdateDate(issue.updateDate())
-
+      .setSelectedAt(issue.selectedAt())
       .setCreatedAt(now)
       .setUpdatedAt(now);
   }
@@ -382,6 +396,7 @@ public final class IssueDto implements Serializable {
       .setIssueCreationDate(issue.creationDate())
       .setIssueCloseDate(issue.closeDate())
       .setIssueUpdateDate(issue.updateDate())
+      .setSelectedAt(issue.selectedAt())
       .setUpdatedAt(now);
   }
 
@@ -407,6 +422,7 @@ public final class IssueDto implements Serializable {
     issue.setCreationDate(issueCreationDate);
     issue.setCloseDate(issueCloseDate);
     issue.setUpdateDate(issueUpdateDate);
+    issue.setSelectedAt(selectedAt);
     return issue;
   }
 }
index d0f81df029d225ff7f1dae0a0f6a5d41b96f88fc..42c9b0913403843ef332c23ea244edf1f77d9352 100644 (file)
@@ -40,4 +40,5 @@ public interface IssueMapper {
 
   int update(IssueDto issue);
 
+  int updateIfBeforeSelectedDate(IssueDto issue);
 }
index b8287a836559b9dac758d974ec4da139eec10cb1..bf4050312c0166859e340b3ae820b5670d3d6e3f 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.core.issue.db;
 
-import com.google.common.collect.Lists;
 import org.apache.ibatis.session.SqlSession;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueComment;
@@ -32,12 +31,12 @@ import org.sonar.core.persistence.MyBatis;
 
 import java.util.Arrays;
 import java.util.Date;
-import java.util.List;
 
 public abstract class IssueStorage {
 
   private final MyBatis mybatis;
   private final RuleFinder ruleFinder;
+  private final UpdateConflictResolver conflictResolver = new UpdateConflictResolver();
 
   protected IssueStorage(MyBatis mybatis, RuleFinder ruleFinder) {
     this.mybatis = mybatis;
@@ -49,37 +48,50 @@ public abstract class IssueStorage {
   }
 
   public void save(Iterable<DefaultIssue> issues) {
-    SqlSession session = mybatis.openBatchSession();
+    SqlSession session = mybatis.openSession();
     IssueMapper issueMapper = session.getMapper(IssueMapper.class);
     IssueChangeMapper issueChangeMapper = session.getMapper(IssueChangeMapper.class);
     Date now = new Date();
     try {
-      List<DefaultIssue> conflicts = Lists.newArrayList();
       for (DefaultIssue issue : issues) {
         if (issue.isNew()) {
-          long componentId = componentId(issue);
-          long projectId = projectId(issue);
-          int ruleId = ruleId(issue);
-          IssueDto dto = IssueDto.toDtoForInsert(issue, componentId, projectId, ruleId, now);
-          issueMapper.insert(dto);
-
+          insert(issueMapper, now, issue);
         } else if (issue.isChanged()) {
-          IssueDto dto = IssueDto.toDtoForUpdate(issue, now);
-          int count = issueMapper.update(dto);
-          if (count < 1) {
-            conflicts.add(issue);
-          }
+          update(issueMapper, now, issue);
         }
         insertChanges(issueChangeMapper, issue);
 
       }
       session.commit();
-      // TODO log and fix conflicts
     } finally {
       MyBatis.closeQuietly(session);
     }
   }
 
+  private void insert(IssueMapper issueMapper, Date now, DefaultIssue issue) {
+    long componentId = componentId(issue);
+    long projectId = projectId(issue);
+    int ruleId = ruleId(issue);
+    IssueDto dto = IssueDto.toDtoForInsert(issue, componentId, projectId, ruleId, now);
+    issueMapper.insert(dto);
+  }
+
+  private void update(IssueMapper issueMapper, Date now, DefaultIssue issue) {
+    IssueDto dto = IssueDto.toDtoForUpdate(issue, now);
+    if (Issue.STATUS_CLOSED.equals(issue.status()) || issue.selectedAt() == null) {
+      // Issue is closed by scan or changed by end-user
+      issueMapper.update(dto);
+
+    } else {
+      int count = issueMapper.updateIfBeforeSelectedDate(dto);
+      if (count == 0) {
+        // End-user and scan changed the issue at the same time.
+        // See https://jira.codehaus.org/browse/SONAR-4309
+        conflictResolver.resolve(issue, issueMapper);
+      }
+    }
+  }
+
   private void insertChanges(IssueChangeMapper mapper, DefaultIssue issue) {
     for (IssueComment comment : issue.comments()) {
       DefaultIssueComment c = (DefaultIssueComment) comment;
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java b/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java
new file mode 100644 (file)
index 0000000..5ebe79b
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 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.core.issue.db;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.issue.internal.DefaultIssue;
+
+import java.util.Date;
+
+/**
+ * See https://jira.codehaus.org/browse/SONAR-4309
+ *
+ * @since 3.6
+ */
+class UpdateConflictResolver {
+
+  private final Logger LOG = LoggerFactory.getLogger(IssueStorage.class);
+
+  public void resolve(DefaultIssue issue, IssueMapper mapper) {
+    LOG.debug("Resolve conflict on issue " + issue.key());
+
+    IssueDto dbIssue = mapper.selectByKey(issue.key());
+    if (dbIssue != null) {
+      mergeFields(dbIssue, issue);
+      mapper.update(IssueDto.toDtoForUpdate(issue, new Date()));
+    }
+  }
+
+  @VisibleForTesting
+  void mergeFields(IssueDto dbIssue, DefaultIssue issue) {
+    resolveAssignee(dbIssue, issue);
+    resolvePlan(dbIssue, issue);
+    resolveSeverity(dbIssue, issue);
+    resolveEffortToFix(dbIssue, issue);
+    resolveResolution(dbIssue, issue);
+    resolveStatus(dbIssue, issue);
+  }
+
+  private void resolveStatus(IssueDto dbIssue, DefaultIssue issue) {
+    issue.setStatus(dbIssue.getStatus());
+  }
+
+  private void resolveResolution(IssueDto dbIssue, DefaultIssue issue) {
+    issue.setResolution(dbIssue.getResolution());
+  }
+
+  private void resolveEffortToFix(IssueDto dbIssue, DefaultIssue issue) {
+    issue.setEffortToFix(dbIssue.getEffortToFix());
+  }
+
+  private void resolveSeverity(IssueDto dbIssue, DefaultIssue issue) {
+    if (dbIssue.isManualSeverity()) {
+      issue.setManualSeverity(true);
+      issue.setSeverity(dbIssue.getSeverity());
+    }
+    // else keep severity as declared in quality profile
+  }
+
+  private void resolvePlan(IssueDto dbIssue, DefaultIssue issue) {
+    issue.setActionPlanKey(dbIssue.getActionPlanKey());
+  }
+
+  private void resolveAssignee(IssueDto dbIssue, DefaultIssue issue) {
+    issue.setAssignee(dbIssue.getAssignee());
+  }
+}
index 855800149a5258df84451f418facada7876601cc..e52ab7b86481cc71b95aa74ea46d61d5565aed61 100644 (file)
     where kee = #{kee}
   </update>
 
+  <!--
+   IMPORTANT - invariant columns can't be updated. See IssueDto#toDtoForUpdate()
+ -->
+  <update id="updateIfBeforeSelectedDate" parameterType="Issue">
+    update issues set
+    action_plan_key=#{actionPlanKey},
+    severity=#{severity},
+    manual_severity=#{manualSeverity},
+    message=#{message},
+    line=#{line},
+    effort_to_fix=#{effortToFix},
+    status=#{status},
+    resolution=#{resolution},
+    checksum=#{checksum},
+    reporter=#{reporter},
+    assignee=#{assignee},
+    author_login=#{authorLogin},
+    issue_attributes=#{issueAttributes},
+    issue_creation_date=#{issueCreationDate},
+    issue_update_date=#{issueUpdateDate},
+    issue_close_date=#{issueCloseDate},
+    updated_at=#{updatedAt}
+    where kee = #{kee} and updated_at &lt;= #{selectedAt}
+  </update>
+
   <select id="selectByKey" parameterType="String" resultType="Issue">
     select
     <include refid="issueColumns"/>
index 8311558009f4369b5d437c8d3d65e6d10f297fc0..7476d5b98fceaf7d7383c70a693a7f590bdac4f7 100644 (file)
@@ -25,6 +25,10 @@ import org.junit.Test;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.core.persistence.AbstractDaoTestCase;
 
+import java.util.Date;
+
+import static org.fest.assertions.Assertions.assertThat;
+
 public class IssueMapperTest extends AbstractDaoTestCase {
 
   SqlSession session;
@@ -101,4 +105,78 @@ public class IssueMapperTest extends AbstractDaoTestCase {
 
     checkTables("testUpdate", new String[]{"id"}, "issues");
   }
+
+  @Test
+  public void updateBeforeSelectedDate_without_conflict() throws Exception {
+    setupData("testUpdate");
+
+    IssueDto dto = new IssueDto();
+    dto.setComponentId(123l);
+    dto.setRootComponentId(100l);
+    dto.setRuleId(200);
+    dto.setKee("ABCDE");
+    dto.setLine(500);
+    dto.setEffortToFix(3.14);
+    dto.setResolution("FIXED");
+    dto.setStatus("RESOLVED");
+    dto.setSeverity("BLOCKER");
+    dto.setReporter("emmerik");
+    dto.setAuthorLogin("morgan");
+    dto.setAssignee("karadoc");
+    dto.setActionPlanKey("current_sprint");
+    dto.setIssueAttributes("JIRA=FOO-1234");
+    dto.setChecksum("123456789");
+    dto.setMessage("the message");
+    dto.setIssueCreationDate(DateUtils.parseDate("2013-05-18"));
+    dto.setIssueUpdateDate(DateUtils.parseDate("2013-05-19"));
+    dto.setIssueCloseDate(DateUtils.parseDate("2013-05-20"));
+    dto.setCreatedAt(DateUtils.parseDate("2013-05-21"));
+    dto.setUpdatedAt(DateUtils.parseDate("2013-05-22"));
+
+    // selected after last update -> ok
+    dto.setSelectedAt(DateUtils.parseDate("2015-01-01"));
+
+    int count = mapper.updateIfBeforeSelectedDate(dto);
+    assertThat(count).isEqualTo(1);
+    session.commit();
+
+    checkTables("testUpdate", new String[]{"id"}, "issues");
+  }
+
+  @Test
+  public void updateBeforeSelectedDate_with_conflict() throws Exception {
+    setupData("updateBeforeSelectedDate_with_conflict");
+
+    IssueDto dto = new IssueDto();
+    dto.setComponentId(123l);
+    dto.setRootComponentId(100l);
+    dto.setRuleId(200);
+    dto.setKee("ABCDE");
+    dto.setLine(500);
+    dto.setEffortToFix(3.14);
+    dto.setResolution("FIXED");
+    dto.setStatus("RESOLVED");
+    dto.setSeverity("BLOCKER");
+    dto.setReporter("emmerik");
+    dto.setAuthorLogin("morgan");
+    dto.setAssignee("karadoc");
+    dto.setActionPlanKey("current_sprint");
+    dto.setIssueAttributes("JIRA=FOO-1234");
+    dto.setChecksum("123456789");
+    dto.setMessage("the message");
+    dto.setIssueCreationDate(DateUtils.parseDate("2013-05-18"));
+    dto.setIssueUpdateDate(DateUtils.parseDate("2013-05-19"));
+    dto.setIssueCloseDate(DateUtils.parseDate("2013-05-20"));
+    dto.setCreatedAt(DateUtils.parseDate("2013-05-21"));
+    dto.setUpdatedAt(DateUtils.parseDate("2013-05-22"));
+
+    // selected before last update -> ko
+    dto.setSelectedAt(DateUtils.parseDate("2009-01-01"));
+
+    int count = mapper.updateIfBeforeSelectedDate(dto);
+    assertThat(count).isEqualTo(0);
+    session.commit();
+
+    checkTables("updateBeforeSelectedDate_with_conflict", new String[]{"id"}, "issues");
+  }
 }
diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java
new file mode 100644 (file)
index 0000000..09458da
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 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.core.issue.db;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.utils.DateUtils;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+public class UpdateConflictResolverTest {
+
+  @Test
+  public void should_reload_issue_and_resolve_conflict() throws Exception {
+    DefaultIssue issue = new DefaultIssue()
+      .setKey("ABCDE")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycles"))
+      .setComponentKey("struts:org.apache.struts.Action")
+      .setNew(false)
+      .setStatus(Issue.STATUS_OPEN);
+
+    // Issue as seen and changed by end-user
+    IssueMapper mapper = mock(IssueMapper.class);
+    when(mapper.selectByKey("ABCDE")).thenReturn(
+      new IssueDto()
+        .setKee("ABCDE")
+        .setRuleId(10)
+        .setRuleKey_unit_test_only("squid", "AvoidCycles")
+        .setComponentId(100L)
+        .setComponentKey_unit_test_only("struts:org.apache.struts.Action")
+        .setLine(10)
+        .setStatus(Issue.STATUS_OPEN)
+
+          // field changed by user
+        .setAssignee("arthur")
+    );
+
+    new UpdateConflictResolver().resolve(issue, mapper);
+
+    ArgumentCaptor<IssueDto> argument = ArgumentCaptor.forClass(IssueDto.class);
+    verify(mapper).update(argument.capture());
+    IssueDto updatedIssue = argument.getValue();
+    assertThat(updatedIssue.getKee()).isEqualTo("ABCDE");
+    assertThat(updatedIssue.getAssignee()).isEqualTo("arthur");
+  }
+
+  @Test
+  public void should_keep_changes_made_by_user() throws Exception {
+    DefaultIssue issue = new DefaultIssue()
+      .setKey("ABCDE")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycles"))
+      .setComponentKey("struts:org.apache.struts.Action")
+      .setNew(false);
+
+    // Before starting scan
+    issue.setAssignee(null);
+    issue.setActionPlanKey("PLAN-1");
+    issue.setCreationDate(DateUtils.parseDate("2012-01-01"));
+    issue.setUpdateDate(DateUtils.parseDate("2012-02-02"));
+
+    // Changed by scan
+    issue.setLine(200);
+    issue.setSeverity(Severity.BLOCKER);
+    issue.setManualSeverity(false);
+    issue.setAuthorLogin("simon");
+    issue.setChecksum("CHECKSUM-ABCDE");
+    issue.setResolution(null);
+    issue.setStatus(Issue.STATUS_REOPENED);
+
+    // Issue as seen and changed by end-user
+    IssueDto dbIssue = new IssueDto()
+      .setKee("ABCDE")
+      .setRuleId(10)
+      .setRuleKey_unit_test_only("squid", "AvoidCycles")
+      .setComponentId(100L)
+      .setComponentKey_unit_test_only("struts:org.apache.struts.Action")
+      .setLine(10)
+      .setResolution(Issue.RESOLUTION_FALSE_POSITIVE)
+      .setStatus(Issue.STATUS_RESOLVED)
+      .setAssignee("arthur")
+      .setActionPlanKey("PLAN-2")
+      .setSeverity(Severity.MAJOR)
+      .setManualSeverity(false);
+
+    new UpdateConflictResolver().mergeFields(dbIssue, issue);
+
+    assertThat(issue.key()).isEqualTo("ABCDE");
+    assertThat(issue.componentKey()).isEqualTo("struts:org.apache.struts.Action");
+
+    // Scan wins on :
+    assertThat(issue.line()).isEqualTo(200);
+    assertThat(issue.severity()).isEqualTo(Severity.BLOCKER);
+    assertThat(issue.manualSeverity()).isFalse();
+
+    // User wins on :
+    assertThat(issue.assignee()).isEqualTo("arthur");
+    assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FALSE_POSITIVE);
+    assertThat(issue.status()).isEqualTo(Issue.STATUS_RESOLVED);
+    assertThat(issue.actionPlanKey()).isEqualTo("PLAN-2");
+  }
+
+  @Test
+  public void severity_changed_by_user_should_be_kept() throws Exception {
+    DefaultIssue issue = new DefaultIssue()
+      .setKey("ABCDE")
+      .setRuleKey(RuleKey.of("squid", "AvoidCycles"))
+      .setComponentKey("struts:org.apache.struts.Action")
+      .setNew(false)
+      .setStatus(Issue.STATUS_OPEN);
+
+    // Changed by scan
+    issue.setSeverity(Severity.BLOCKER);
+    issue.setManualSeverity(false);
+
+    // Issue as seen and changed by end-user
+    IssueDto dbIssue = new IssueDto()
+      .setKee("ABCDE")
+      .setStatus(Issue.STATUS_OPEN)
+      .setSeverity(Severity.INFO)
+      .setManualSeverity(true);
+
+    new UpdateConflictResolver().mergeFields(dbIssue, issue);
+
+    assertThat(issue.severity()).isEqualTo(Severity.INFO);
+    assertThat(issue.manualSeverity()).isTrue();
+  }
+}
index c1473018a536a0741a530a2dc792cc971444053f..a4df3d603dd3dcc79870f52b81bd29437ef7d2f5 100644 (file)
@@ -21,7 +21,7 @@
       issue_update_date="[null]"
       issue_close_date="[null]"
       created_at="[null]"
-      updated_at="[null]"
+      updated_at="2009-01-01"
       action_plan_key="[null]"
       />
 </dataset>
\ No newline at end of file
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict-result.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict-result.xml
new file mode 100644 (file)
index 0000000..3bd12af
--- /dev/null
@@ -0,0 +1,28 @@
+<dataset>
+  <!-- not updated -->
+  <issues
+      id="100"
+      kee="ABCDE"
+      component_id="123"
+      root_component_id="100"
+      rule_id="200"
+      severity="INFO"
+      manual_severity="[false]"
+      message="old"
+      line="[null]"
+      effort_to_fix="[null]"
+      status="OPEN"
+      resolution="[null]"
+      checksum="[null]"
+      reporter="[null]"
+      author_login="[null]"
+      assignee="[null]"
+      issue_attributes="[null]"
+      issue_creation_date="[null]"
+      issue_update_date="[null]"
+      issue_close_date="[null]"
+      created_at="[null]"
+      updated_at="2013-06-01"
+      action_plan_key="[null]"
+      />
+</dataset>
\ No newline at end of file
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueMapperTest/updateBeforeSelectedDate_with_conflict.xml
new file mode 100644 (file)
index 0000000..d598d02
--- /dev/null
@@ -0,0 +1,27 @@
+<dataset>
+  <issues
+      id="100"
+      kee="ABCDE"
+      component_id="123"
+      root_component_id="100"
+      rule_id="200"
+      severity="INFO"
+      manual_severity="[false]"
+      message="old"
+      line="[null]"
+      effort_to_fix="[null]"
+      status="OPEN"
+      resolution="[null]"
+      checksum="[null]"
+      reporter="[null]"
+      author_login="[null]"
+      assignee="[null]"
+      issue_attributes="[null]"
+      issue_creation_date="[null]"
+      issue_update_date="[null]"
+      issue_close_date="[null]"
+      created_at="[null]"
+      updated_at="2013-06-01"
+      action_plan_key="[null]"
+      />
+</dataset>
\ No newline at end of file
index e594b75414539027cb3714855ead55417a237cdd..8bd1e6b322bd2484a0d404f9e387842e4d6cdf4a 100644 (file)
@@ -89,6 +89,9 @@ public class DefaultIssue implements Issue {
   // true if some fields have been changed since the previous scan
   private boolean isChanged = false;
 
+  // Date when issue was loaded from db (only when isNew=false)
+  private Date selectedAt;
+
   public String key() {
     return key;
   }
@@ -380,6 +383,15 @@ public class DefaultIssue implements Issue {
     return Objects.firstNonNull(comments, Collections.<IssueComment>emptyList());
   }
 
+  @CheckForNull
+  public Date selectedAt() {
+    return selectedAt;
+  }
+
+  public void setSelectedAt(@Nullable Date d) {
+    this.selectedAt = d;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
@@ -404,4 +416,6 @@ public class DefaultIssue implements Issue {
   public String toString() {
     return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
   }
+
+
 }