Skip to content

Commit

Permalink
SONAR-9837 Detect files changed in the current branch with SCM
Browse files Browse the repository at this point in the history
  • Loading branch information
dbmeneses committed Sep 28, 2017
1 parent 0a201a0 commit 5abe7f2
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 39 deletions.
Expand Up @@ -21,7 +21,7 @@
package org.sonar.api.batch.scm; package org.sonar.api.batch.scm;


import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Set;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.sonar.api.ExtensionPoint; import org.sonar.api.ExtensionPoint;
import org.sonar.api.batch.InstantiationStrategy; import org.sonar.api.batch.InstantiationStrategy;
Expand All @@ -37,11 +37,11 @@
public abstract class ScmBranchProvider extends ScmProvider { public abstract class ScmBranchProvider extends ScmProvider {


/** /**
* Return absolute path of files changed in the current branch, compared to the provided target branch. * Return absolute path of the files changed in the current branch, compared to the provided target branch.
* @return null if SCM provider was not able to compute the list of files. * @return null if SCM provider was not able to compute the list of files.
*/ */
@Nullable @Nullable
public Collection<Path> branchChangedFiles(String targetBranchName, Path rootBaseDir) { public Set<Path> branchChangedFiles(String targetBranchName, Path rootBaseDir) {
return null; return null;
} }
} }
Expand Up @@ -23,29 +23,28 @@
import org.sonar.api.batch.fs.internal.DefaultFileSystem; import org.sonar.api.batch.fs.internal.DefaultFileSystem;
import org.sonar.api.batch.fs.internal.DefaultInputModule; import org.sonar.api.batch.fs.internal.DefaultInputModule;
import org.sonar.scanner.analysis.DefaultAnalysisMode; import org.sonar.scanner.analysis.DefaultAnalysisMode;
import org.sonar.scanner.repository.ProjectRepositories;


public class DefaultModuleFileSystem extends DefaultFileSystem { public class DefaultModuleFileSystem extends DefaultFileSystem {


public DefaultModuleFileSystem(ModuleInputComponentStore moduleInputFileCache, DefaultInputModule module, ModuleFileSystemInitializer initializer, DefaultAnalysisMode mode, public DefaultModuleFileSystem(ModuleInputComponentStore moduleInputFileCache, DefaultInputModule module, ModuleFileSystemInitializer initializer, DefaultAnalysisMode mode,
ProjectRepositories projectRepositories) { StatusDetection statusDetection) {
super(module.getBaseDir(), moduleInputFileCache); super(module.getBaseDir(), moduleInputFileCache);
setFields(module, initializer, mode, projectRepositories); setFields(module, initializer, mode, statusDetection);
} }


@VisibleForTesting @VisibleForTesting
public DefaultModuleFileSystem(DefaultInputModule module, ModuleFileSystemInitializer initializer, DefaultAnalysisMode mode, ProjectRepositories projectRepositories) { public DefaultModuleFileSystem(DefaultInputModule module, ModuleFileSystemInitializer initializer, DefaultAnalysisMode mode, StatusDetection statusDetection) {
super(module.getBaseDir()); super(module.getBaseDir());
setFields(module, initializer, mode, projectRepositories); setFields(module, initializer, mode, statusDetection);
} }


private void setFields(DefaultInputModule module, ModuleFileSystemInitializer initializer, DefaultAnalysisMode mode, ProjectRepositories projectRepositories) { private void setFields(DefaultInputModule module, ModuleFileSystemInitializer initializer, DefaultAnalysisMode mode, StatusDetection statusDetection) {
setWorkDir(module.getWorkDir()); setWorkDir(module.getWorkDir());
setEncoding(initializer.defaultEncoding()); setEncoding(initializer.defaultEncoding());


// filter the files sensors have access to // filter the files sensors have access to
if (!mode.scanAllFiles()) { if (!mode.scanAllFiles()) {
setDefaultPredicate(p -> new SameInputFilePredicate(p, projectRepositories, module.definition().getKeyWithBranch())); setDefaultPredicate(p -> new SameInputFilePredicate(p, statusDetection, module.definition().getKeyWithBranch()));
} }
} }


Expand Down
Expand Up @@ -20,45 +20,39 @@
package org.sonar.scanner.scan.filesystem; package org.sonar.scanner.scan.filesystem;


import java.util.function.Predicate; import java.util.function.Predicate;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.batch.fs.FilePredicate; import org.sonar.api.batch.fs.FilePredicate;
import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.InputFile.Status;
import org.sonar.api.batch.fs.internal.DefaultInputFile;
import org.sonar.api.batch.fs.internal.OperatorPredicate; import org.sonar.api.batch.fs.internal.OperatorPredicate;
import org.sonar.api.batch.fs.internal.StatusPredicate; import org.sonar.api.batch.fs.internal.StatusPredicate;
import org.sonar.scanner.repository.FileData;
import org.sonar.scanner.repository.ProjectRepositories;


public class SameInputFilePredicate implements Predicate<InputFile> { public class SameInputFilePredicate implements Predicate<InputFile> {
private final ProjectRepositories projectRepositories; private final StatusDetection statusDetection;
private final String moduleKeyWithBranch; private final String moduleKeyWithBranch;
private final FilePredicate currentPredicate; private final FilePredicate currentPredicate;


public SameInputFilePredicate(FilePredicate currentPredicate, ProjectRepositories projectRepositories, String moduleKeyWithBranch) { public SameInputFilePredicate(FilePredicate currentPredicate, StatusDetection statusDetection, String moduleKeyWithBranch) {
this.currentPredicate = currentPredicate; this.currentPredicate = currentPredicate;
this.projectRepositories = projectRepositories; this.statusDetection = statusDetection;
this.moduleKeyWithBranch = moduleKeyWithBranch; this.moduleKeyWithBranch = moduleKeyWithBranch;
} }


@Override @Override
public boolean test(InputFile inputFile) { public boolean test(InputFile inputFile) {
if (hasExplicitFilterOnStatus(currentPredicate)) { if (hasExplicitFilterOnStatus(currentPredicate)) {
// If user explicitely requested a given status, don't change the result // If user explicitly requested a given status, don't change the result
return true; return true;
} }
// Try to avoid initializing metadata
FileData fileDataPerPath = projectRepositories.fileData(moduleKeyWithBranch, inputFile.relativePath()); // TODO: the inputFile could try to calculate the status itself without generating metadata
if (fileDataPerPath == null) { Status status = statusDetection.getStatusWithoutMetadata(moduleKeyWithBranch, (DefaultInputFile) inputFile);
// ADDED if (status != null) {
return true; return status != Status.SAME;
}
String previousHash = fileDataPerPath.hash();
if (StringUtils.isEmpty(previousHash)) {
// ADDED
return true;
} }


// this will trigger computation of metadata // this will trigger computation of metadata
return inputFile.status() != InputFile.Status.SAME; return inputFile.status() != Status.SAME;
} }


static boolean hasExplicitFilterOnStatus(FilePredicate predicate) { static boolean hasExplicitFilterOnStatus(FilePredicate predicate) {
Expand Down
Expand Up @@ -19,6 +19,7 @@
*/ */
package org.sonar.scanner.scan.filesystem; package org.sonar.scanner.scan.filesystem;


import javax.annotation.CheckForNull;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.sonar.api.batch.fs.InputFile; import org.sonar.api.batch.fs.InputFile;
Expand All @@ -27,6 +28,8 @@
import org.sonar.scanner.repository.ProjectRepositories; import org.sonar.scanner.repository.ProjectRepositories;
import org.sonar.scanner.scm.ScmChangedFiles; import org.sonar.scanner.scm.ScmChangedFiles;


import static org.sonar.api.batch.fs.InputFile.Status.*;

@Immutable @Immutable
public class StatusDetection { public class StatusDetection {


Expand All @@ -41,18 +44,39 @@ public StatusDetection(ProjectRepositories projectSettings, ScmChangedFiles scmC
InputFile.Status status(String projectKeyWithBranch, DefaultInputFile inputFile, String hash) { InputFile.Status status(String projectKeyWithBranch, DefaultInputFile inputFile, String hash) {
FileData fileDataPerPath = projectRepositories.fileData(projectKeyWithBranch, inputFile.relativePath()); FileData fileDataPerPath = projectRepositories.fileData(projectKeyWithBranch, inputFile.relativePath());
if (fileDataPerPath == null) { if (fileDataPerPath == null) {
return InputFile.Status.ADDED; return checkChanged(ADDED, inputFile);
} }
String previousHash = fileDataPerPath.hash(); String previousHash = fileDataPerPath.hash();
if (StringUtils.equals(hash, previousHash)) { if (StringUtils.equals(hash, previousHash)) {
return InputFile.Status.SAME; return SAME;
} }
if (StringUtils.isEmpty(previousHash)) { if (StringUtils.isEmpty(previousHash)) {
return InputFile.Status.ADDED; return checkChanged(ADDED, inputFile);
}
return checkChanged(CHANGED, inputFile);
}

/**
* If possible, get the status of the provided file without initializing metadata of the file.
* @return null if it was not possible to get the status without calculating metadata
*/
@CheckForNull
public InputFile.Status getStatusWithoutMetadata(String projectKeyWithBranch, DefaultInputFile inputFile) {
FileData fileDataPerPath = projectRepositories.fileData(projectKeyWithBranch, inputFile.relativePath());
if (fileDataPerPath == null) {
return checkChanged(ADDED, inputFile);
} }
String previousHash = fileDataPerPath.hash();
if (StringUtils.isEmpty(previousHash)) {
return checkChanged(ADDED, inputFile);
}
return null;
}

private InputFile.Status checkChanged(InputFile.Status status, DefaultInputFile inputFile) {
if (!scmChangedFiles.verifyChanged(inputFile.path())) { if (!scmChangedFiles.verifyChanged(inputFile.path())) {
return InputFile.Status.SAME; return SAME;
} }
return InputFile.Status.CHANGED; return status;
} }
} }
Expand Up @@ -24,15 +24,17 @@
import javax.annotation.CheckForNull; import javax.annotation.CheckForNull;
import org.picocontainer.annotations.Nullable; import org.picocontainer.annotations.Nullable;
import org.picocontainer.injectors.ProviderAdapter; import org.picocontainer.injectors.ProviderAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.batch.fs.internal.InputModuleHierarchy; import org.sonar.api.batch.fs.internal.InputModuleHierarchy;
import org.sonar.api.batch.scm.ScmBranchProvider; import org.sonar.api.batch.scm.ScmBranchProvider;
import org.sonar.api.batch.scm.ScmProvider; import org.sonar.api.batch.scm.ScmProvider;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.log.Profiler;
import org.sonar.scanner.scan.branch.BranchConfiguration; import org.sonar.scanner.scan.branch.BranchConfiguration;


public class ScmChangedFilesProvider extends ProviderAdapter { public class ScmChangedFilesProvider extends ProviderAdapter {
private static final Logger LOG = LoggerFactory.getLogger(ScmChangedFilesProvider.class); private static final Logger LOG = Loggers.get(ScmChangedFilesProvider.class);
private static final String LOG_MSG = "SCM collecting changed files in the branch";


private ScmChangedFiles scmBranchChangedFiles; private ScmChangedFiles scmBranchChangedFiles;


Expand All @@ -46,21 +48,30 @@ public ScmChangedFiles provide(@Nullable ScmConfiguration scmConfiguration, Bran
} else { } else {
Path rootBaseDir = inputModuleHierarchy.root().getBaseDir(); Path rootBaseDir = inputModuleHierarchy.root().getBaseDir();
Collection<Path> changedFiles = loadChangedFilesIfNeeded(scmConfiguration, branchConfiguration, rootBaseDir); Collection<Path> changedFiles = loadChangedFilesIfNeeded(scmConfiguration, branchConfiguration, rootBaseDir);
validatePaths(changedFiles);
scmBranchChangedFiles = new ScmChangedFiles(changedFiles); scmBranchChangedFiles = new ScmChangedFiles(changedFiles);
} }
} }
return scmBranchChangedFiles; return scmBranchChangedFiles;
} }


private static void validatePaths(@javax.annotation.Nullable Collection<Path> paths) {
if (paths != null && paths.stream().anyMatch(p -> !p.isAbsolute())) {
throw new IllegalStateException("SCM provider returned a changed file with a relative path but paths must be absolute. Please fix the provider.");
}
}

@CheckForNull @CheckForNull
private static Collection<Path> loadChangedFilesIfNeeded(ScmConfiguration scmConfiguration, BranchConfiguration branchConfiguration, Path rootBaseDir) { private static Collection<Path> loadChangedFilesIfNeeded(ScmConfiguration scmConfiguration, BranchConfiguration branchConfiguration, Path rootBaseDir) {
if (branchConfiguration.isShortLivingBranch()) { if (branchConfiguration.isShortLivingBranch()) {
ScmProvider scmProvider = scmConfiguration.provider(); ScmProvider scmProvider = scmConfiguration.provider();
if (scmProvider != null && (scmProvider instanceof ScmBranchProvider)) { if (scmProvider != null && (scmProvider instanceof ScmBranchProvider)) {
Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
ScmBranchProvider scmBranchProvider = (ScmBranchProvider) scmProvider; ScmBranchProvider scmBranchProvider = (ScmBranchProvider) scmProvider;
Collection<Path> changedFiles = scmBranchProvider.branchChangedFiles(branchConfiguration.branchTarget(), rootBaseDir); Collection<Path> changedFiles = scmBranchProvider.branchChangedFiles(branchConfiguration.branchTarget(), rootBaseDir);
profiler.stopInfo();
if (changedFiles != null) { if (changedFiles != null) {
LOG.debug("SCM reported {} files changed in the branch", changedFiles.size()); LOG.debug("SCM reported {} {} changed in the branch", changedFiles.size(), pluralize("file", changedFiles.size()));
return changedFiles; return changedFiles;
} }
} }
Expand All @@ -70,4 +81,11 @@ private static Collection<Path> loadChangedFilesIfNeeded(ScmConfiguration scmCon
return null; return null;
} }


private static String pluralize(String str, int i) {
if (i == 1) {
return str;
}
return str + "s";
}

} }
Expand Up @@ -33,6 +33,10 @@
import org.sonar.scanner.scm.ScmChangedFiles; import org.sonar.scanner.scm.ScmChangedFiles;


import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;


public class StatusDetectionTest { public class StatusDetectionTest {
@Test @Test
Expand All @@ -54,6 +58,26 @@ public void detect_status_branches_exclude() {


// normally changed // normally changed
assertThat(statusDetection.status("foo", createFile("src/Foo.java"), "XXXXX")).isEqualTo(InputFile.Status.SAME); assertThat(statusDetection.status("foo", createFile("src/Foo.java"), "XXXXX")).isEqualTo(InputFile.Status.SAME);

// normally added
assertThat(statusDetection.status("foo", createFile("src/Other.java"), "QWERT")).isEqualTo(InputFile.Status.SAME);
}

@Test
public void detect_status_without_metadata() {
DefaultInputFile mockedFile = mock(DefaultInputFile.class);
when(mockedFile.relativePath()).thenReturn("module/src/Foo.java");
when(mockedFile.path()).thenReturn(Paths.get("module", "src", "Foo.java"));

ProjectRepositories ref = new ProjectRepositories(ImmutableTable.of(), createTable(), null);
ScmChangedFiles changedFiles = new ScmChangedFiles(Collections.singletonList(Paths.get("module", "src", "Foo.java")));
StatusDetection statusDetection = new StatusDetection(ref, changedFiles);

assertThat(statusDetection.getStatusWithoutMetadata("foo", mockedFile)).isEqualTo(InputFile.Status.ADDED);

verify(mockedFile).path();
verify(mockedFile).relativePath();
verifyNoMoreInteractions(mockedFile);
} }


@Test @Test
Expand Down
Expand Up @@ -23,7 +23,9 @@
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Collections; import java.util.Collections;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.sonar.api.batch.fs.internal.DefaultInputModule; import org.sonar.api.batch.fs.internal.DefaultInputModule;
Expand All @@ -48,6 +50,9 @@ public class ScmChangedFilesProviderTest {
@Mock @Mock
private ScmBranchProvider scmProvider; private ScmBranchProvider scmProvider;


@Rule
public ExpectedException exception = ExpectedException.none();

private Path rootBaseDir = Paths.get("root"); private Path rootBaseDir = Paths.get("root");
private ScmChangedFilesProvider provider; private ScmChangedFilesProvider provider;


Expand All @@ -69,6 +74,18 @@ public void testNoScmProvider() {
verify(scmConfiguration).provider(); verify(scmConfiguration).provider();
} }


@Test
public void testFailIfRelativePath() {
when(branchConfiguration.branchTarget()).thenReturn("target");
when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
when(scmConfiguration.provider()).thenReturn(scmProvider);
when(scmProvider.branchChangedFiles("target", rootBaseDir)).thenReturn(Collections.singleton(Paths.get("changedFile")));

exception.expect(IllegalStateException.class);
exception.expectMessage("changed file with a relative path");
provider.provide(scmConfiguration, branchConfiguration, inputModuleHierarchy);
}

@Test @Test
public void testProviderDoesntSupport() { public void testProviderDoesntSupport() {
when(branchConfiguration.branchTarget()).thenReturn("target"); when(branchConfiguration.branchTarget()).thenReturn("target");
Expand Down Expand Up @@ -108,10 +125,10 @@ public void testReturnChangedFiles() {
when(branchConfiguration.branchTarget()).thenReturn("target"); when(branchConfiguration.branchTarget()).thenReturn("target");
when(branchConfiguration.isShortLivingBranch()).thenReturn(true); when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
when(scmConfiguration.provider()).thenReturn(scmProvider); when(scmConfiguration.provider()).thenReturn(scmProvider);
when(scmProvider.branchChangedFiles("target", rootBaseDir)).thenReturn(Collections.singletonList(Paths.get("changedFile"))); when(scmProvider.branchChangedFiles("target", rootBaseDir)).thenReturn(Collections.singleton(Paths.get("changedFile").toAbsolutePath()));
ScmChangedFiles scmChangedFiles = provider.provide(scmConfiguration, branchConfiguration, inputModuleHierarchy); ScmChangedFiles scmChangedFiles = provider.provide(scmConfiguration, branchConfiguration, inputModuleHierarchy);


assertThat(scmChangedFiles.get()).containsOnly(Paths.get("changedFile")); assertThat(scmChangedFiles.get()).containsOnly(Paths.get("changedFile").toAbsolutePath());
verify(scmProvider).branchChangedFiles("target", rootBaseDir); verify(scmProvider).branchChangedFiles("target", rootBaseDir);
} }


Expand Down

0 comments on commit 5abe7f2

Please sign in to comment.