]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6397 Create repository to load SCM data from report or batch
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Tue, 29 Sep 2015 07:55:17 +0000 (09:55 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Fri, 2 Oct 2015 09:48:28 +0000 (11:48 +0200)
16 files changed:
server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java
server/sonar-server/src/main/java/org/sonar/server/computation/scm/Changeset.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/scm/DbScmInfo.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/scm/ReportScmInfo.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfo.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoRepository.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoRepositoryImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/scm/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/source/LastCommitVisitor.java
server/sonar-server/src/test/java/org/sonar/server/computation/scm/ChangesetTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/scm/DbScmInfoTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/scm/ReportScmInfoTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoRepositoryImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoRepositoryRule.java [new file with mode: 0644]

index 654f601a5c4f54252edce4360c6af81737de8e14..b48d8113d58f607e1c20363ee7df789f7354c590 100644 (file)
@@ -23,9 +23,6 @@ import java.util.Arrays;
 import java.util.List;
 import org.sonar.core.issue.tracking.Tracker;
 import org.sonar.core.platform.ContainerPopulator;
-import org.sonar.server.computation.queue.CeTask;
-import org.sonar.server.computation.step.ComputationStepExecutor;
-import org.sonar.server.computation.filesystem.ComputationTempFolderProvider;
 import org.sonar.server.computation.analysis.ReportAnalysisMetadataHolder;
 import org.sonar.server.computation.batch.BatchReportDirectoryHolderImpl;
 import org.sonar.server.computation.batch.BatchReportReaderImpl;
@@ -34,6 +31,7 @@ import org.sonar.server.computation.component.ReportTreeRootHolderImpl;
 import org.sonar.server.computation.component.SettingsRepositoryImpl;
 import org.sonar.server.computation.debt.DebtModelHolderImpl;
 import org.sonar.server.computation.event.EventRepositoryImpl;
+import org.sonar.server.computation.filesystem.ComputationTempFolderProvider;
 import org.sonar.server.computation.issue.BaseIssuesLoader;
 import org.sonar.server.computation.issue.CloseIssuesOnRemovedComponentsVisitor;
 import org.sonar.server.computation.issue.ComponentIssuesRepositoryImpl;
@@ -76,11 +74,14 @@ import org.sonar.server.computation.qualitygate.EvaluationResultTextConverterImp
 import org.sonar.server.computation.qualitygate.QualityGateHolderImpl;
 import org.sonar.server.computation.qualitygate.QualityGateServiceImpl;
 import org.sonar.server.computation.qualityprofile.ActiveRulesHolderImpl;
+import org.sonar.server.computation.queue.CeTask;
+import org.sonar.server.computation.scm.ScmInfoRepositoryImpl;
 import org.sonar.server.computation.source.LastCommitVisitor;
 import org.sonar.server.computation.source.SourceLinesRepositoryImpl;
 import org.sonar.server.computation.sqale.SqaleMeasuresVisitor;
-import org.sonar.server.computation.sqale.SqaleRatingSettings;
 import org.sonar.server.computation.sqale.SqaleNewMeasuresVisitor;
+import org.sonar.server.computation.sqale.SqaleRatingSettings;
+import org.sonar.server.computation.step.ComputationStepExecutor;
 import org.sonar.server.computation.step.ComputationSteps;
 import org.sonar.server.computation.step.ReportComputationSteps;
 import org.sonar.server.view.index.ViewIndex;
@@ -134,6 +135,7 @@ public final class ReportComputeEngineContainerPopulator implements ContainerPop
       QualityGateServiceImpl.class,
       EvaluationResultTextConverterImpl.class,
       SourceLinesRepositoryImpl.class,
+      ScmInfoRepositoryImpl.class,
 
       // issues
       RuleCacheLoader.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/scm/Changeset.java b/server/sonar-server/src/main/java/org/sonar/server/computation/scm/Changeset.java
new file mode 100644 (file)
index 0000000..32bbe0e
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * 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.scm;
+
+import java.util.Objects;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public final class Changeset {
+
+  private final String revision;
+  private final long date;
+  @CheckForNull
+  private final String author;
+
+  private Changeset(Builder builder) {
+    this.revision = builder.revision;
+    this.author = builder.author;
+    this.date = builder.date;
+  }
+
+  public static Builder newChangesetBuilder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private String revision;
+    private Long date;
+    @CheckForNull
+    private String author;
+
+    private Builder() {
+      // prevents direct instantiation
+    }
+
+    public Builder setRevision(String revision) {
+      this.revision = checkRevision(revision);
+      return this;
+    }
+
+    public Builder setDate(Long date) {
+      this.date = checkDate(date);
+      return this;
+    }
+
+    public Builder setAuthor(@Nullable String author) {
+      this.author = author;
+      return this;
+    }
+
+    public Changeset build() {
+      checkRevision(revision);
+      checkDate(date);
+      return new Changeset(this);
+    }
+
+    private static String checkRevision(String revision){
+      return requireNonNull(revision, "Revision cannot be null");
+    }
+
+    private static long checkDate(Long date){
+      return requireNonNull(date, "Date cannot be null");
+    }
+
+  }
+
+  public String getRevision() {
+    return revision;
+  }
+
+  public long getDate() {
+    return date;
+  }
+
+  @CheckForNull
+  public String getAuthor() {
+    return author;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    Changeset changeset = (Changeset) o;
+    if (date != changeset.date) {
+      return false;
+    }
+    if (!revision.equals(changeset.revision)) {
+      return false;
+    }
+    return Objects.equals(author, changeset.author);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(revision, author, date);
+  }
+
+  @Override
+  public String toString() {
+    return "Changeset{" +
+        "revision='" + revision + '\'' +
+        ", author='" + author + '\'' +
+        ", date=" + date +
+        '}';
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/scm/DbScmInfo.java b/server/sonar-server/src/main/java/org/sonar/server/computation/scm/DbScmInfo.java
new file mode 100644 (file)
index 0000000..4e6bc51
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * 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.scm;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.db.protobuf.DbFileSources;
+import org.sonar.server.computation.component.Component;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Predicates.notNull;
+import static com.google.common.collect.FluentIterable.from;
+import static com.google.common.collect.Iterables.isEmpty;
+import static java.lang.String.format;
+
+/**
+ * ScmInfo implementation based on the lines stored in DB
+ */
+@Immutable
+class DbScmInfo implements ScmInfo {
+
+  private final ScmInfo delegate;
+
+  private DbScmInfo(ScmInfo delegate) {
+    this.delegate = delegate;
+  }
+
+  static Optional<ScmInfo> create(Component component, Iterable<DbFileSources.Line> lines) {
+    LineToChangeset lineToChangeset = new LineToChangeset();
+    Iterable<Changeset> lineChangesets = from(lines)
+      .transform(lineToChangeset)
+      .filter(notNull())
+      .toList();
+    if (isEmpty(lineChangesets)) {
+      return Optional.absent();
+    }
+    checkState(!lineToChangeset.isEncounteredLineWithoutScmInfo(),
+      format("Partial scm information stored in DB for component '%s'. Not all lines have SCM info. Can not proceed", component));
+    return Optional.<ScmInfo>of(new DbScmInfo(new ScmInfoImpl(lineChangesets)));
+  }
+
+  @Override
+  public Changeset getLatestChangeset() {
+    return delegate.getLatestChangeset();
+  }
+
+  @Override
+  public Changeset getChangesetForLine(int lineNumber) {
+    return delegate.getChangesetForLine(lineNumber);
+  }
+
+  @Override
+  public boolean hasChangesetForLine(int lineNumber) {
+    return delegate.hasChangesetForLine(lineNumber);
+  }
+
+  @Override
+  public Iterable<Changeset> getAllChangesets() {
+    return delegate.getAllChangesets();
+  }
+
+  /**
+   * Transforms {@link org.sonar.db.protobuf.DbFileSources.Line} into {@link Changeset} and keep a flag if it encountered
+   * at least one which did not have any SCM information.
+   */
+  private static class LineToChangeset implements Function<DbFileSources.Line, Changeset> {
+    private boolean encounteredLineWithoutScmInfo = false;
+
+    @Override
+    @Nullable
+    public Changeset apply(@Nonnull DbFileSources.Line input) {
+      if (input.hasScmRevision() || input.hasScmAuthor() || input.hasScmDate()) {
+        return Changeset.newChangesetBuilder()
+          .setRevision(input.getScmRevision())
+          .setAuthor(input.getScmAuthor())
+          .setDate(input.getScmDate())
+          .build();
+      }
+
+      this.encounteredLineWithoutScmInfo = true;
+      return null;
+    }
+
+    public boolean isEncounteredLineWithoutScmInfo() {
+      return encounteredLineWithoutScmInfo;
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ReportScmInfo.java b/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ReportScmInfo.java
new file mode 100644 (file)
index 0000000..71f4baf
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * 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.scm;
+
+import com.google.common.base.Function;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.batch.protocol.output.BatchReport;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.FluentIterable.from;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * ScmInfo implementation based on the changeset information from the Report
+ */
+@Immutable
+class ReportScmInfo implements ScmInfo {
+  private final ScmInfo delegate;
+
+  ReportScmInfo(BatchReport.Changesets changesets) {
+    requireNonNull(changesets);
+    this.delegate = convertToScmInfo(changesets);
+  }
+
+  private static ScmInfo convertToScmInfo(BatchReport.Changesets changesets) {
+    return new ScmInfoImpl(
+      from(new IntRangeIterable(changesets.getChangesetIndexByLineCount()))
+        .transform(new LineIndexToChangeset(changesets)));
+  }
+
+  private static Changeset convert(BatchReport.Changesets.Changeset changeset) {
+    checkState(changeset.hasRevision(), "Changeset must have a revision");
+    checkState(changeset.hasDate(), "Changeset must have a date");
+    return Changeset.newChangesetBuilder()
+      .setRevision(changeset.getRevision())
+      .setAuthor(changeset.hasAuthor() ? changeset.getAuthor() : null)
+      .setDate(changeset.getDate())
+      .build();
+  }
+
+  @Override
+  public Changeset getLatestChangeset() {
+    return this.delegate.getLatestChangeset();
+  }
+
+  @Override
+  public Changeset getChangesetForLine(int lineNumber) {
+    return this.delegate.getChangesetForLine(lineNumber);
+  }
+
+  @Override
+  public boolean hasChangesetForLine(int lineNumber) {
+    return delegate.hasChangesetForLine(lineNumber);
+  }
+
+  @Override
+  public Iterable<Changeset> getAllChangesets() {
+    return this.delegate.getAllChangesets();
+  }
+
+  private static class LineIndexToChangeset implements Function<Integer, Changeset> {
+    private final BatchReport.Changesets changesets;
+    private final Map<Integer, Changeset> changeSetCache;
+
+    public LineIndexToChangeset(BatchReport.Changesets changesets) {
+      this.changesets = changesets;
+      changeSetCache = new HashMap<>(changesets.getChangesetCount());
+    }
+
+    @Override
+    @Nonnull
+    public Changeset apply(@Nonnull Integer lineNumber) {
+      int changesetIndex = changesets.getChangesetIndexByLine(lineNumber - 1);
+      if (changeSetCache.containsKey(changesetIndex)) {
+        return changeSetCache.get(changesetIndex);
+      }
+      Changeset res = convert(changesets.getChangeset(changesetIndex));
+      changeSetCache.put(changesetIndex, res);
+      return res;
+    }
+  }
+
+  /**
+   * A simple Iterable which generate integer from 0 (included) to a specific value (excluded).
+   */
+  private static final class IntRangeIterable implements Iterable<Integer> {
+    private final int max;
+
+    private IntRangeIterable(int max) {
+      checkArgument(max >= 0, "Max value must be >= 0");
+      this.max = max;
+    }
+
+    @Override
+    public Iterator<Integer> iterator() {
+      return new IntRangeIterator(max);
+    }
+  }
+
+  private static class IntRangeIterator implements Iterator<Integer> {
+    private final int max;
+
+    private int current = 0;
+
+    public IntRangeIterator(int max) {
+      this.max = max;
+    }
+
+    @Override
+    public boolean hasNext() {
+      return current < max;
+    }
+
+    @Override
+    public Integer next() {
+      if (!hasNext()) {
+        throw new NoSuchElementException();
+      }
+      current++;
+      return current;
+    }
+
+    @Override
+    public void remove() {
+      throw new UnsupportedOperationException("Remove cannot be called");
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfo.java b/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfo.java
new file mode 100644 (file)
index 0000000..a774de4
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * 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.scm;
+
+/**
+ * Represents the Scm information for a specific file.
+ */
+public interface ScmInfo {
+
+  /**
+   * Get most recent ChangeSet of the file. Can never be null
+   */
+  Changeset getLatestChangeset();
+
+  /**
+   * Get ChangeSet of the file for given line
+   *
+   * @throws IllegalArgumentException if there is no Changeset for the specified line, ie. the line number in invalid (either less than 1 or > linecount of the file)
+   */
+  Changeset getChangesetForLine(int lineNumber);
+
+  /**
+   * Check if there's a ChangeSet for given line
+   */
+  boolean hasChangesetForLine(int lineNumber);
+
+  /**
+   * Return all ChangeSets, in order, for all lines of the file
+   */
+  Iterable<Changeset> getAllChangesets();
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoImpl.java
new file mode 100644 (file)
index 0000000..5e124e1
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 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.scm;
+
+import com.google.common.base.Predicate;
+import java.util.Arrays;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.FluentIterable.from;
+import static com.google.common.collect.Iterables.isEmpty;
+import static java.util.Arrays.asList;
+
+@Immutable
+public class ScmInfoImpl implements ScmInfo {
+
+  @CheckForNull
+  private final Changeset latestChangeset;
+  private final Changeset[] lineChangesets;
+
+  public ScmInfoImpl(Iterable<Changeset> lineChangesets) {
+    checkState(!isEmpty(lineChangesets), "A ScmInfo must have at least one Changeset and does not support any null one");
+    this.lineChangesets = from(lineChangesets).filter(CheckNotNull.INSTANCE).toArray(Changeset.class);
+    this.latestChangeset = computeLatestChangeset(lineChangesets);
+  }
+
+  private static Changeset computeLatestChangeset(Iterable<Changeset> lineChangesets) {
+    Changeset latestChangeset = null;
+    for (Changeset lineChangeset : lineChangesets) {
+      if (latestChangeset == null || lineChangeset.getDate() > latestChangeset.getDate()) {
+        latestChangeset = lineChangeset;
+      }
+    }
+    return latestChangeset;
+  }
+
+  @Override
+  public Changeset getLatestChangeset() {
+    return latestChangeset;
+  }
+
+  @Override
+  public Changeset getChangesetForLine(int lineNumber) {
+    checkArgument(lineNumber > 0 && lineNumber <= lineChangesets.length, String.format("There's no changeset on line %s", lineNumber));
+    return lineChangesets[lineNumber - 1];
+  }
+
+  @Override
+  public boolean hasChangesetForLine(int lineNumber) {
+    return lineNumber <= lineChangesets.length;
+  }
+
+  @Override
+  public Iterable<Changeset> getAllChangesets() {
+    return asList(lineChangesets);
+  }
+
+  @Override
+  public String toString() {
+    return "ScmInfoImpl{" +
+      "latestChangeset=" + latestChangeset +
+      ", lineChangesets=" + Arrays.toString(lineChangesets) +
+      '}';
+  }
+
+  private enum CheckNotNull implements Predicate<Changeset> {
+    INSTANCE;
+
+    @Override
+    public boolean apply(@Nullable Changeset input) {
+      checkState(input != null, "Null changeset are not allowed");
+      return true;
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoRepository.java b/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoRepository.java
new file mode 100644 (file)
index 0000000..defd8a9
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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.scm;
+
+import com.google.common.base.Optional;
+import org.sonar.server.computation.component.Component;
+
+/**
+ * Return SCM information of components.
+ *
+ * It will always search in the db if there's nothing in the report.
+ */
+public interface ScmInfoRepository {
+
+  /**
+   * Returns Scm info for the specified component if there is any, first looking into the report, then into the database
+   * It there's nothing in the report and in the db (on first analysis for instance), then it return a {@link Optional#absent()}.
+   *
+   * @throws NullPointerException if argument is {@code null}
+   */
+  Optional<ScmInfo> getScmInfo(Component component);
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoRepositoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/scm/ScmInfoRepositoryImpl.java
new file mode 100644 (file)
index 0000000..7ac532a
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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.scm;
+
+import com.google.common.base.Optional;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.protocol.output.BatchReport;
+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 static com.google.common.base.Preconditions.checkNotNull;
+
+public class ScmInfoRepositoryImpl implements ScmInfoRepository {
+
+  private static final Logger LOGGER = Loggers.get(ScmInfoRepositoryImpl.class);
+
+  private final BatchReportReader batchReportReader;
+  private final DbClient dbClient;
+  private final SourceService sourceService;
+
+  public ScmInfoRepositoryImpl(BatchReportReader batchReportReader, DbClient dbClient, SourceService sourceService) {
+    this.batchReportReader = batchReportReader;
+    this.dbClient = dbClient;
+    this.sourceService = sourceService;
+  }
+
+  @Override
+  public Optional<ScmInfo> getScmInfo(Component component) {
+    checkNotNull(component, "Component cannot be bull");
+    BatchReport.Changesets changesets = batchReportReader.readChangesets(component.getReportAttributes().getRef());
+    if (changesets == null) {
+      LOGGER.trace("Reading SCM info from db for file '{}'", component);
+      return getFromDb(component);
+    } else {
+      LOGGER.trace("Reading SCM info from report for file '{}'", component);
+      return Optional.<ScmInfo>of(new ReportScmInfo(changesets));
+    }
+  }
+
+  private Optional<ScmInfo> getFromDb(Component component){
+    DbSession dbSession = dbClient.openSession(false);
+    try {
+      Optional<Iterable<DbFileSources.Line>> linesOpt = sourceService.getLines(dbSession, component.getUuid(), 1, Integer.MAX_VALUE);
+      if (linesOpt.isPresent()) {
+        return DbScmInfo.create(component, linesOpt.get());
+      }
+      return Optional.absent();
+    } finally {
+      dbClient.closeSession(dbSession);
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/scm/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/computation/scm/package-info.java
new file mode 100644 (file)
index 0000000..ea71454
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+@ParametersAreNonnullByDefault
+package org.sonar.server.computation.scm;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index dc01e3e258bded738ded4431f56ec73cabe8b53b..5c312104dfee99e1f9092e7b5d321dc8c665dd11 100644 (file)
@@ -78,6 +78,10 @@ public class LastCommitVisitor extends PathAwareVisitorAdapter<LastCommitVisitor
     // load SCM blame information from report. It can be absent when the file was not touched
     // since previous analysis (optimization to decrease execution of blame commands). In this case
     // the date is loaded from database, as it did not change from previous analysis.
+
+    // TODO We should use ScmInfoRepository instead of reading the report
+    // (but should only be done when the repo is only used once per component,
+    // as it's done with ComponentIssuesRepository, to not increase number of calls to file_sources)
     BatchReport.Changesets changesets = reportReader.readChangesets(file.getReportAttributes().getRef());
     if (changesets == null) {
       Optional<Measure> baseMeasure = measureRepository.getBaseMeasure(file, lastCommitDateMetric);
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ChangesetTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ChangesetTest.java
new file mode 100644 (file)
index 0000000..426e2b0
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 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.scm;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ChangesetTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void create_changeset() throws Exception {
+    Changeset underTest = Changeset.newChangesetBuilder()
+      .setAuthor("john")
+      .setDate(123456789L)
+      .setRevision("rev-1")
+      .build();
+
+    assertThat(underTest.getAuthor()).isEqualTo("john");
+    assertThat(underTest.getDate()).isEqualTo(123456789L);
+    assertThat(underTest.getRevision()).isEqualTo("rev-1");
+  }
+
+  @Test
+  public void create_changeset_with_minimum_fields() throws Exception {
+    Changeset underTest = Changeset.newChangesetBuilder()
+      .setDate(123456789L)
+      .setRevision("rev-1")
+      .build();
+
+    assertThat(underTest.getAuthor()).isNull();
+    assertThat(underTest.getDate()).isEqualTo(123456789L);
+    assertThat(underTest.getRevision()).isEqualTo("rev-1");
+  }
+
+  @Test
+  public void fail_with_NPE_when_setting_null_revision() throws Exception {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("Revision cannot be null");
+
+    Changeset.newChangesetBuilder().setRevision(null);
+  }
+
+  @Test
+  public void fail_with_NPE_when_building_without_revision() throws Exception {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("Revision cannot be null");
+
+    Changeset.newChangesetBuilder()
+      .setAuthor("john")
+      .setDate(123456789L)
+      .build();
+  }
+
+  @Test
+  public void fail_with_NPE_when_setting_null_date() throws Exception {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("Date cannot be null");
+
+    Changeset.newChangesetBuilder().setDate(null);
+  }
+
+  @Test
+  public void fail_with_NPE_when_building_without_date() throws Exception {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("Date cannot be null");
+
+    Changeset.newChangesetBuilder()
+      .setAuthor("john")
+      .setRevision("rev-1")
+      .build();
+  }
+
+  @Test
+  public void test_to_string() throws Exception {
+    Changeset underTest = Changeset.newChangesetBuilder()
+      .setAuthor("john")
+      .setDate(123456789L)
+      .setRevision("rev-1")
+      .build();
+
+    assertThat(underTest.toString()).isEqualTo("Changeset{revision='rev-1', author='john', date=123456789}");
+  }
+
+  @Test
+  public void test_equals_and_hash_code() throws Exception {
+    Changeset.Builder changesetBuilder = Changeset.newChangesetBuilder()
+      .setAuthor("john")
+      .setDate(123456789L)
+      .setRevision("rev-1");
+
+    Changeset changeset = changesetBuilder.build();
+    Changeset sameChangeset = changesetBuilder.build();
+
+    Changeset anotherChangeset = Changeset.newChangesetBuilder()
+      .setAuthor("henry")
+      .setDate(1234567810L)
+      .setRevision("rev-2")
+      .build();
+
+    assertThat(changeset).isEqualTo(sameChangeset);
+    assertThat(changeset).isEqualTo(changeset);
+    assertThat(changeset).isNotEqualTo(anotherChangeset);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/scm/DbScmInfoTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/scm/DbScmInfoTest.java
new file mode 100644 (file)
index 0000000..8caf16f
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * 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.scm;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.protobuf.DbFileSources;
+import org.sonar.server.computation.component.Component;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.guava.api.Assertions.assertThat;
+import static org.sonar.server.computation.component.ReportComponent.builder;
+import static org.sonar.server.source.index.FileSourceTesting.newFakeData;
+
+public class DbScmInfoTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  static final int FILE_REF = 1;
+  static final Component FILE = builder(Component.Type.FILE, FILE_REF).setKey("FILE_KEY").setUuid("FILE_UUID").build();
+
+  @Test
+  public void create_scm_info_with_some_changesets() throws Exception {
+    ScmInfo scmInfo = DbScmInfo.create(FILE, newFakeData(10).build().getLinesList()).get();
+
+    assertThat(scmInfo.getAllChangesets()).hasSize(10);
+  }
+
+  @Test
+  public void return_changeset_for_a_given_line() throws Exception {
+    DbFileSources.Data.Builder fileDataBuilder = DbFileSources.Data.newBuilder();
+    addLine(fileDataBuilder, 1, "john", 123456789L, "rev-1");
+    addLine(fileDataBuilder, 2, "henry", 1234567810L, "rev-2");
+    addLine(fileDataBuilder, 3, "henry", 1234567810L, "rev-2");
+    addLine(fileDataBuilder, 4, "john", 123456789L, "rev-1");
+    fileDataBuilder.build();
+
+    ScmInfo scmInfo = DbScmInfo.create(FILE, fileDataBuilder.getLinesList()).get();
+
+    assertThat(scmInfo.getAllChangesets()).hasSize(4);
+
+    Changeset changeset = scmInfo.getChangesetForLine(4);
+    assertThat(changeset.getAuthor()).isEqualTo("john");
+    assertThat(changeset.getDate()).isEqualTo(123456789L);
+    assertThat(changeset.getRevision()).isEqualTo("rev-1");
+  }
+
+  @Test
+  public void return_latest_changeset() throws Exception {
+    DbFileSources.Data.Builder fileDataBuilder = DbFileSources.Data.newBuilder();
+    addLine(fileDataBuilder, 1, "john", 123456789L, "rev-1");
+    // Older changeset
+    addLine(fileDataBuilder, 2, "henry", 1234567810L, "rev-2");
+    addLine(fileDataBuilder, 3, "john", 123456789L, "rev-1");
+    fileDataBuilder.build();
+
+    ScmInfo scmInfo = DbScmInfo.create(FILE, fileDataBuilder.getLinesList()).get();
+
+    Changeset latestChangeset = scmInfo.getLatestChangeset();
+    assertThat(latestChangeset.getAuthor()).isEqualTo("henry");
+    assertThat(latestChangeset.getDate()).isEqualTo(1234567810L);
+    assertThat(latestChangeset.getRevision()).isEqualTo("rev-2");
+  }
+
+  @Test
+  public void return_absent_dsm_info_when_no_changeset() throws Exception {
+    DbFileSources.Data.Builder fileDataBuilder = DbFileSources.Data.newBuilder();
+    fileDataBuilder.addLinesBuilder().setLine(1);
+
+    assertThat(DbScmInfo.create(FILE, fileDataBuilder.getLinesList())).isAbsent();
+  }
+
+  @Test
+  public void fail_with_ISE_when_changeset_has_no_field() throws Exception {
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("Partial scm information stored in DB for component 'ReportComponent{ref=1, key='FILE_KEY', type=FILE}'. " +
+      "Not all lines have SCM info. Can not proceed");
+
+    DbFileSources.Data.Builder fileDataBuilder = DbFileSources.Data.newBuilder();
+    fileDataBuilder.addLinesBuilder().setLine(1);
+    fileDataBuilder.addLinesBuilder().setScmAuthor("John").setLine(2);
+    fileDataBuilder.build();
+
+    DbScmInfo.create(FILE, fileDataBuilder.getLinesList()).get().getAllChangesets();
+  }
+
+  private static void addLine(DbFileSources.Data.Builder dataBuilder, Integer line, String author, Long date, String revision) {
+    dataBuilder.addLinesBuilder()
+      .setLine(line)
+      .setScmAuthor(author)
+      .setScmDate(date)
+      .setScmRevision(revision);
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ReportScmInfoTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ReportScmInfoTest.java
new file mode 100644 (file)
index 0000000..b455b33
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * 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.scm;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.batch.protocol.output.BatchReport;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ReportScmInfoTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  static final int FILE_REF = 1;
+
+  @Test
+  public void create_scm_info_with_some_changesets() throws Exception {
+    ScmInfo scmInfo = new ReportScmInfo(BatchReport.Changesets.newBuilder()
+      .setComponentRef(FILE_REF)
+      .addChangeset(BatchReport.Changesets.Changeset.newBuilder()
+        .setAuthor("john")
+        .setDate(123456789L)
+        .setRevision("rev-1")
+        .build())
+      .addChangeset(BatchReport.Changesets.Changeset.newBuilder()
+        .setAuthor("henry")
+        .setDate(1234567810L)
+        .setRevision("rev-2")
+        .build())
+      .addChangesetIndexByLine(0)
+      .addChangesetIndexByLine(1)
+      .addChangesetIndexByLine(0)
+      .addChangesetIndexByLine(0)
+      .build());
+
+    assertThat(scmInfo.getAllChangesets()).hasSize(4);
+  }
+
+  @Test
+  public void return_changeset_for_a_given_line() throws Exception {
+    ScmInfo scmInfo = new ReportScmInfo(BatchReport.Changesets.newBuilder()
+      .setComponentRef(FILE_REF)
+      .addChangeset(BatchReport.Changesets.Changeset.newBuilder()
+        .setAuthor("john")
+        .setDate(123456789L)
+        .setRevision("rev-1")
+        .build())
+      .addChangeset(BatchReport.Changesets.Changeset.newBuilder()
+        .setAuthor("henry")
+        .setDate(1234567810L)
+        .setRevision("rev-2")
+        .build())
+      .addChangesetIndexByLine(0)
+      .addChangesetIndexByLine(1)
+      .addChangesetIndexByLine(1)
+      .addChangesetIndexByLine(0)
+      .build());
+
+    assertThat(scmInfo.getAllChangesets()).hasSize(4);
+
+    Changeset changeset = scmInfo.getChangesetForLine(4);
+    assertThat(changeset.getAuthor()).isEqualTo("john");
+    assertThat(changeset.getDate()).isEqualTo(123456789L);
+    assertThat(changeset.getRevision()).isEqualTo("rev-1");
+  }
+
+  @Test
+  public void return_latest_changeset() throws Exception {
+    ScmInfo scmInfo = new ReportScmInfo(BatchReport.Changesets.newBuilder()
+      .setComponentRef(FILE_REF)
+      .addChangeset(BatchReport.Changesets.Changeset.newBuilder()
+        .setAuthor("john")
+        .setDate(123456789L)
+        .setRevision("rev-1")
+        .build())
+      // Older changeset
+      .addChangeset(BatchReport.Changesets.Changeset.newBuilder()
+        .setAuthor("henry")
+        .setDate(1234567810L)
+        .setRevision("rev-2")
+        .build())
+      .addChangesetIndexByLine(0)
+      .addChangesetIndexByLine(1)
+      .addChangesetIndexByLine(0)
+      .build());
+
+    Changeset latestChangeset = scmInfo.getLatestChangeset();
+    assertThat(latestChangeset.getAuthor()).isEqualTo("henry");
+    assertThat(latestChangeset.getDate()).isEqualTo(1234567810L);
+    assertThat(latestChangeset.getRevision()).isEqualTo("rev-2");
+  }
+
+  @Test
+  public void fail_with_ISE_when_no_changeset() throws Exception {
+    thrown.expect(IllegalStateException.class);
+
+    new ReportScmInfo(BatchReport.Changesets.newBuilder().build());
+  }
+
+  @Test
+  public void fail_with_NPE_when_report_is_null() throws Exception {
+    thrown.expect(NullPointerException.class);
+
+    new ReportScmInfo(null);
+  }
+
+  @Test
+  public void fail_with_ISE_when_changeset_has_no_revision() throws Exception {
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("Changeset must have a revision");
+
+    new ReportScmInfo(BatchReport.Changesets.newBuilder()
+      .setComponentRef(FILE_REF)
+      .addChangeset(BatchReport.Changesets.Changeset.newBuilder()
+        .setAuthor("john")
+        .setDate(123456789L)
+        .build())
+      .addChangesetIndexByLine(0)
+      .build());
+  }
+
+  @Test
+  public void fail_with_ISE_when_changeset_has_no_date() throws Exception {
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("Changeset must have a date");
+
+    new ReportScmInfo(BatchReport.Changesets.newBuilder()
+      .setComponentRef(FILE_REF)
+      .addChangeset(BatchReport.Changesets.Changeset.newBuilder()
+        .setAuthor("john")
+        .setRevision("rev-1")
+        .build())
+      .addChangesetIndexByLine(0)
+      .build());
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoImplTest.java
new file mode 100644 (file)
index 0000000..096b865
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * 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.scm;
+
+import com.google.common.collect.Lists;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ScmInfoImplTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  static final Changeset CHANGESET_1 = Changeset.newChangesetBuilder()
+    .setAuthor("john")
+    .setDate(123456789L)
+    .setRevision("rev-1")
+    .build();
+
+  static final Changeset CHANGESET_2 = Changeset.newChangesetBuilder()
+    .setAuthor("henry")
+    .setDate(1234567810L)
+    .setRevision("rev-2")
+    .build();
+
+  @Test
+  public void get_all_changesets() throws Exception {
+    ScmInfo scmInfo = createScmInfoWithTwoChangestOnFourLines();
+
+    assertThat(scmInfo.getAllChangesets()).containsOnly(CHANGESET_1, CHANGESET_2, CHANGESET_1, CHANGESET_1);
+  }
+
+  @Test
+  public void get_latest_changeset() throws Exception {
+    ScmInfo scmInfo = createScmInfoWithTwoChangestOnFourLines();
+
+    assertThat(scmInfo.getLatestChangeset()).isEqualTo(CHANGESET_2);
+  }
+
+  @Test
+  public void get_changeset_for_given_line() throws Exception {
+    ScmInfo scmInfo = createScmInfoWithTwoChangestOnFourLines();
+
+    assertThat(scmInfo.getChangesetForLine(1)).isEqualTo(CHANGESET_1);
+    assertThat(scmInfo.getChangesetForLine(2)).isEqualTo(CHANGESET_2);
+    assertThat(scmInfo.getChangesetForLine(3)).isEqualTo(CHANGESET_1);
+    assertThat(scmInfo.getChangesetForLine(4)).isEqualTo(CHANGESET_1);
+  }
+
+  @Test
+  public void exists_for_given_line() throws Exception {
+    ScmInfo scmInfo = createScmInfoWithTwoChangestOnFourLines();
+
+    assertThat(scmInfo.hasChangesetForLine(1)).isTrue();
+    assertThat(scmInfo.hasChangesetForLine(5)).isFalse();
+  }
+
+  @Test
+  public void fail_with_ISE_on_empty_changeset() throws Exception {
+    thrown.expect(IllegalStateException.class);
+    thrown.expectMessage("A ScmInfo must have at least one Changeset and does not support any null one");
+
+    new ScmInfoImpl(Lists.<Changeset>newArrayList());
+  }
+
+  @Test
+  public void fail_with_IAE_when_line_is_smaller_than_one() throws Exception {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("There's no changeset on line 0");
+
+    ScmInfo scmInfo = createScmInfoWithTwoChangestOnFourLines();
+    scmInfo.getChangesetForLine(0);
+  }
+
+  @Test
+  public void fail_with_IAE_when_line_is_bigger_than_changetset_size() throws Exception {
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("There's no changeset on line 5");
+
+    ScmInfo scmInfo = createScmInfoWithTwoChangestOnFourLines();
+    scmInfo.getChangesetForLine(5);
+  }
+
+  @Test
+  public void test_to_string() throws Exception {
+    ScmInfo scmInfo = createScmInfoWithTwoChangestOnFourLines();
+
+    assertThat(scmInfo.toString()).isEqualTo("ScmInfoImpl{" +
+      "latestChangeset=Changeset{revision='rev-2', author='henry', date=1234567810}, " +
+      "lineChangesets=[" +
+      "Changeset{revision='rev-1', author='john', date=123456789}, " +
+      "Changeset{revision='rev-2', author='henry', date=1234567810}, " +
+      "Changeset{revision='rev-1', author='john', date=123456789}, " +
+      "Changeset{revision='rev-1', author='john', date=123456789}" +
+      "]}");
+  }
+
+  private static ScmInfo createScmInfoWithTwoChangestOnFourLines() {
+    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();
+
+    ScmInfo scmInfo = new ScmInfoImpl(newArrayList(changeset1, changeset2, changeset1, changeset1));
+    return scmInfo;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoRepositoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoRepositoryImplTest.java
new file mode 100644 (file)
index 0000000..04c0166
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * 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.scm;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.batch.protocol.output.BatchReport;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbTester;
+import org.sonar.db.protobuf.DbFileSources;
+import org.sonar.db.source.FileSourceDto;
+import org.sonar.server.computation.batch.BatchReportReaderRule;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.source.SourceService;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.guava.api.Assertions.assertThat;
+import static org.sonar.server.computation.component.ReportComponent.builder;
+
+public class ScmInfoRepositoryImplTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  static final int FILE_REF = 1;
+  static final Component FILE = builder(Component.Type.FILE, FILE_REF).setKey("FILE_KEY").setUuid("FILE_UUID").build();
+
+  @Rule
+  public BatchReportReaderRule reportReader = new BatchReportReaderRule();
+
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+  DbClient dbClient = dbTester.getDbClient();
+
+  ScmInfoRepositoryImpl underTest = new ScmInfoRepositoryImpl(reportReader, dbClient, new SourceService(dbClient, null));
+
+  @Test
+  public void read_from_report() throws Exception {
+    addChangesetInReport("john", 123456789L, "rev-1");
+
+    ScmInfo scmInfo = underTest.getScmInfo(FILE).get();
+    assertThat(scmInfo.getAllChangesets()).hasSize(1);
+  }
+
+  @Test
+  public void read_from_db() throws Exception {
+    addChangesetInDb("henry", 123456789L, "rev-1");
+
+    ScmInfo scmInfo = underTest.getScmInfo(FILE).get();
+    assertThat(scmInfo.getAllChangesets()).hasSize(1);
+  }
+
+  @Test
+  public void read_from_report_even_if_data_in_db_exists() throws Exception {
+    addChangesetInDb("henry", 123456789L, "rev-1");
+
+    addChangesetInReport("john", 1234567810L, "rev-2");
+
+    ScmInfo scmInfo = underTest.getScmInfo(FILE).get();
+
+    Changeset changeset = scmInfo.getChangesetForLine(1);
+    assertThat(changeset.getAuthor()).isEqualTo("john");
+    assertThat(changeset.getDate()).isEqualTo(1234567810L);
+    assertThat(changeset.getRevision()).isEqualTo("rev-2");
+  }
+
+  @Test
+  public void return_nothing_when_no_data_in_report_and_db() throws Exception {
+    assertThat(underTest.getScmInfo(FILE)).isAbsent();
+  }
+
+  @Test
+  public void return_nothing_when_nothing_in_erpot_and_db_has_no_scm() throws Exception {
+    DbFileSources.Data.Builder fileDataBuilder = DbFileSources.Data.newBuilder();
+    fileDataBuilder.addLinesBuilder()
+      .setLine(1);
+    dbTester.getDbClient().fileSourceDao().insert(new FileSourceDto()
+      .setFileUuid(FILE.getUuid())
+      .setProjectUuid("PROJECT_UUID")
+      .setSourceData(fileDataBuilder.build()));
+
+    assertThat(underTest.getScmInfo(FILE)).isAbsent();
+  }
+
+  @Test
+  public void fail_with_NPE_when_component_is_null() throws Exception {
+    thrown.expect(NullPointerException.class);
+    thrown.expectMessage("Component cannot be bull");
+
+    underTest.getScmInfo(null);
+  }
+
+  private void addChangesetInDb(String author, Long date, String revision) {
+    DbFileSources.Data.Builder fileDataBuilder = DbFileSources.Data.newBuilder();
+    fileDataBuilder.addLinesBuilder()
+      .setLine(1)
+      .setScmAuthor(author)
+      .setScmDate(date)
+      .setScmRevision(revision);
+    dbTester.getDbClient().fileSourceDao().insert(new FileSourceDto()
+      .setFileUuid(FILE.getUuid())
+      .setProjectUuid("PROJECT_UUID")
+      .setSourceData(fileDataBuilder.build()));
+  }
+
+  private void addChangesetInReport(String author, Long date, String revision) {
+    reportReader.putChangesets(BatchReport.Changesets.newBuilder()
+      .setComponentRef(FILE_REF)
+      .addChangeset(BatchReport.Changesets.Changeset.newBuilder()
+        .setAuthor(author)
+        .setDate(date)
+        .setRevision(revision)
+        .build())
+      .addChangesetIndexByLine(0)
+      .build());
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoRepositoryRule.java b/server/sonar-server/src/test/java/org/sonar/server/computation/scm/ScmInfoRepositoryRule.java
new file mode 100644 (file)
index 0000000..678a360
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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.scm;
+
+import com.google.common.base.Optional;
+import org.junit.rules.ExternalResource;
+import org.sonar.server.computation.component.Component;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class ScmInfoRepositoryRule extends ExternalResource implements ScmInfoRepository {
+
+  private Map<Integer, ScmInfo> scmInfoByFileRef = new HashMap<>();
+
+  @Override
+  protected void after() {
+    scmInfoByFileRef.clear();
+  }
+
+  @Override
+  public Optional<ScmInfo> getScmInfo(Component component) {
+    checkNotNull(component, "Component cannot be bull");
+    ScmInfo scmInfo = scmInfoByFileRef.get(component.getReportAttributes().getRef());
+    return Optional.fromNullable(scmInfo);
+  }
+
+  public ScmInfoRepositoryRule setScmInfo(int fileRef, Changeset... changesetList) {
+    scmInfoByFileRef.put(fileRef, new ScmInfoImpl(Arrays.asList(changesetList)));
+    return this;
+  }
+}