Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
@Extension
public class GitBlamerFactory extends BlamerFactory {
@Override
public Optional<Blamer> createBlamer(final SCM scm, final Run<?, ?> build,
final FilePath workTree, final TaskListener listener, final FilteredLog logger) {
var validator = new GitRepositoryValidator(scm, build, workTree, listener, logger);
if (validator.isGitRepository()) {
if (validator.isFullGitRepository()) {

Check warning on line 29 in plugin/src/main/java/io/jenkins/plugins/forensics/git/blame/GitBlamerFactory.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>public Optional&lt;Blamer&gt; createBlamer(final SCM scm, final Run&lt;?, ?&gt; build, final FilePath workTree, final TaskListener listener, final FilteredLog logger) { var validator &#61; new GitRepositoryValidator(scm, build, workTree, listener, logger); if (validator.isFullGitRepository()) {<!-- --></code></pre>
var client = validator.createClient();
logger.logInfo("-> Git blamer successfully created in working tree '%s'",
new PathUtil().getAbsolutePath(client.getWorkTree().getRemote()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class GitDeltaCalculatorFactory extends DeltaCalculatorFactory {
public Optional<DeltaCalculator> createDeltaCalculator(final SCM scm, final Run<?, ?> run, final FilePath workspace,
final TaskListener listener, final FilteredLog logger) {
var validator = new GitRepositoryValidator(scm, run, workspace, listener, logger);
if (validator.isGitRepository()) {
if (validator.isFullGitRepository()) {
var client = validator.createClient();
logger.logInfo("-> Git delta calculator successfully created for SCM '%s' in working tree '%s'",
scm, new PathUtil().getAbsolutePath(client.getWorkTree().getRemote()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public void perform(@NonNull final Run<?, ?> run, @NonNull final FilePath worksp
logHandler.log(logger);

var validator = new GitRepositoryValidator(repository, run, workspace, listener, logger);
if (validator.isGitRepository()) {
if (validator.isFullGitRepository()) {
try {
computeStats(run, logger, repository, validator);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
@Extension
public class GitMinerFactory extends MinerFactory {
@Override
public Optional<RepositoryMiner> createMiner(final SCM scm, final Run<?, ?> build, final FilePath workTree,
final TaskListener listener, final FilteredLog logger) {
var validator = new GitRepositoryValidator(scm, build, workTree, listener, logger);
if (validator.isGitRepository()) {
if (validator.isFullGitRepository()) {

Check warning on line 28 in plugin/src/main/java/io/jenkins/plugins/forensics/git/miner/GitMinerFactory.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>public Optional&lt;Blamer&gt; createBlamer(final SCM scm, final Run&lt;?, ?&gt; build, final FilePath workTree, final TaskListener listener, final FilteredLog logger) { var validator &#61; new GitRepositoryValidator(scm, build, workTree, listener, logger); if (validator.isFullGitRepository()) {<!-- --></code></pre>
logger.logInfo("-> Git miner successfully created in working tree '%s'", workTree);

return Optional.of(new GitRepositoryMiner(validator.createClient()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@
* @author Ullrich Hafner
*/
public class GitRepositoryValidator {
/** Error message. */
/** Info message when a shallow clone is detected and blame/mining is skipped. */
@VisibleForTesting
public static final String INFO_SHALLOW_CLONE = "Skipping issues blame since Git has been configured with shallow clone";

/** Info message when a shallow clone is detected but commit recording is still performed. */
@VisibleForTesting
public static final String INFO_SHALLOW_CLONE_COMMIT_RECORDING = "Git has been configured with shallow clone - commit recording will be limited to the available commits";

private static final String HEAD = "HEAD";

private final SCM scm;
Expand Down Expand Up @@ -57,24 +61,52 @@ public GitRepositoryValidator(final SCM scm, final Run<?, ?> build,
}

/**
* Returns whether the specified working tree contains a valid Git repository that can be used to run one of the
* forensics analyzers.
* Returns whether the specified working tree contains a valid Git repository. Shallow clones are accepted
* for operations that do not require full history (e.g., commit recording).
*
* @return {@code true} if the working tree contains a valid repository, {@code false} otherwise
* @return {@code true} if the working tree contains a valid repository (including shallow clones),
* {@code false} otherwise
*/
public boolean isGitRepository() {
if (scm instanceof GitSCM) {
return isValidGitRoot((GitSCM) scm);
return isValidGitRoot((GitSCM) scm, false);
}
logger.logInfo("SCM '%s' is not of type GitSCM", scm.getType());
return false;
}

private boolean isValidGitRoot(final GitSCM git) {
if (isShallow(git)) {
logger.logInfo(INFO_SHALLOW_CLONE);
/**
* Returns whether the specified working tree contains a valid Git repository with full history (no shallow
* clone). This is required for operations that need full commit history, such as blame analysis and
* repository mining.
*
* @return {@code true} if the working tree contains a valid non-shallow repository, {@code false} otherwise
*/
public boolean isFullGitRepository() {
if (scm instanceof GitSCM) {
return isValidGitRoot((GitSCM) scm, true);
}
logger.logInfo("SCM '%s' is not of type GitSCM", scm.getType());
return false;
}

return false;
/**
* Returns whether the Git repository is configured as a shallow clone.
*
* @return {@code true} if the repository is a shallow clone, {@code false} otherwise
*/
public boolean isShallowClone() {
return scm instanceof GitSCM
&& isShallow((GitSCM) scm);
}

private boolean isValidGitRoot(final GitSCM git, final boolean rejectShallowClone) {
if (isShallow(git)) {
if (rejectShallowClone) {
logger.logInfo(INFO_SHALLOW_CLONE);
return false;
}
logger.logInfo(INFO_SHALLOW_CLONE_COMMIT_RECORDING);
}

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import hudson.model.FreeStyleProject;
import hudson.plugins.git.GitSCM;
import hudson.plugins.git.extensions.impl.CloneOption;

import io.jenkins.plugins.forensics.git.util.GitCommitTextDecorator;
import io.jenkins.plugins.forensics.git.util.GitITest;
import io.jenkins.plugins.forensics.git.util.GitRepositoryValidator;

import static io.jenkins.plugins.forensics.git.assertions.Assertions.*;

Expand Down Expand Up @@ -165,6 +168,34 @@ private void verifyAction(final GitCommitsRecord record, final String repository
"-> Git commit decorator successfully obtained 'hudson.plugins.git.browser.GithubWeb"));
}

/**
* Verifies that a build using a shallow clone (depth=1) still records a {@link GitCommitsRecord}.
* Previously, the {@link io.jenkins.plugins.forensics.git.util.GitRepositoryValidator#isGitRepository()}
* method incorrectly returned {@code false} for shallow clones, preventing commit recording and thus
* breaking {@code discoverGitReferenceBuild}.
*
* @throws Exception
* in case of an IO exception
*/
@Test
void shouldRecordCommitsForShallowClone() throws Exception {
createAndCommitFile("First.java", "first commit");
createAndCommitFile("Second.java", "second commit");

var job = createFreeStyleProjectWithShallowClone("shallow-listener");

var record = buildSuccessfully(job).getAction(GitCommitsRecord.class);
assertThat(record)
.as("GitCommitsRecord must be present even for shallow-clone builds (JENKINS-74921)")
.isNotNull()
.isNotEmpty()
.hasNoErrorMessages()
.hasInfoMessages(
GitRepositoryValidator.INFO_SHALLOW_CLONE_COMMIT_RECORDING,
"Found no previous build with recorded Git commits",
"-> Starting initial recording of commits");
}

private void createAndCommitFile(final String fileName, final String content) {
writeFile(fileName, content);
addFile(fileName);
Expand All @@ -176,4 +207,13 @@ private FreeStyleProject createFreeStyleProject(final String name) throws IOExce
project.setScm(new GitSCM(getGitRepositoryPath()));
return project;
}

private FreeStyleProject createFreeStyleProjectWithShallowClone(final String name) throws IOException {
var project = createProject(FreeStyleProject.class, name);
var cloneOption = new CloneOption(true, null, null);
var scm = new GitSCM(GitSCM.createRepoList(getGitRepositoryPath(), null),
Collections.emptyList(), null, null, Collections.singletonList(cloneOption));
project.setScm(scm);
return project;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package io.jenkins.plugins.forensics.git.util;

import org.assertj.core.util.Lists;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.jupiter.api.Test;

import edu.hm.hafner.util.FilteredLog;

import java.io.File;
import java.io.IOException;

import org.jenkinsci.plugins.gitclient.GitClient;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.model.Run;
import hudson.model.Saveable;
import hudson.model.TaskListener;
import hudson.plugins.git.GitSCM;
import hudson.plugins.git.extensions.impl.CloneOption;
import hudson.scm.NullSCM;
import hudson.util.DescribableList;

import static io.jenkins.plugins.forensics.assertions.Assertions.*;
import static org.mockito.Mockito.*;

/**
* Tests the class {@link GitRepositoryValidator}.
*
* @author Akash Manna
*/
class GitRepositoryValidatorTest {
private static final TaskListener NULL_LISTENER = TaskListener.NULL;

@Test
void isGitRepositoryShouldReturnFalseForNonGitScm() {
var logger = createLogger();
var validator = new GitRepositoryValidator(new NullSCM(), null, createWorkTree(), NULL_LISTENER, logger);

assertThat(validator.isGitRepository()).isFalse();
assertThat(logger.getInfoMessages()).contains("SCM 'hudson.scm.NullSCM' is not of type GitSCM");
}

@Test
void isGitRepositoryShouldReturnTrueForNonShallowGit() throws IOException, InterruptedException {
GitSCM gitSCM = createNonShallowGitScm();
Run<?, ?> run = mock(Run.class);
var envVars = new EnvVars();
when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars);
var workspace = createWorkTree();
GitClient gitClient = mock(GitClient.class);
when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient);
when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class));

var logger = createLogger();
var validator = new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger);

assertThat(validator.isGitRepository()).isTrue();

Check warning on line 57 in plugin/src/test/java/io/jenkins/plugins/forensics/git/util/GitRepositoryValidatorTest.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>void isGitRepositoryShouldReturnTrueForNonShallowGit() throws IOException, InterruptedException { GitSCM gitSCM &#61; createNonShallowGitScm(); Run&lt;?, ?&gt; run &#61; mock(Run.class); var envVars &#61; new EnvVars(); when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars); var workspace &#61; createWorkTree(); GitClient gitClient &#61; mock(GitClient.class); when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient); when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class)); var logger &#61; createLogger(); var validator &#61; new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger); assertThat(validator.isGitRepository()).isTrue();</code></pre>
assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE);

Check warning on line 58 in plugin/src/test/java/io/jenkins/plugins/forensics/git/util/GitRepositoryValidatorTest.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>GitSCM gitSCM &#61; createNonShallowGitScm(); Run&lt;?, ?&gt; run &#61; mock(Run.class); var envVars &#61; new EnvVars(); when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars); var workspace &#61; createWorkTree(); GitClient gitClient &#61; mock(GitClient.class); when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient); when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class)); var logger &#61; createLogger(); var validator &#61; new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger); assertThat(validator.isGitRepository()).isTrue(); assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE);</code></pre>
assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE_COMMIT_RECORDING);
}

@Test
void isGitRepositoryShouldReturnTrueForShallowClone() throws IOException, InterruptedException {
GitSCM gitSCM = createShallowGitScm();
Run<?, ?> run = mock(Run.class);
var envVars = new EnvVars();
when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars);
var workspace = createWorkTree();
GitClient gitClient = mock(GitClient.class);
when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient);
when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class));

var logger = createLogger();
var validator = new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger);

assertThat(validator.isGitRepository()).isTrue();

Check warning on line 76 in plugin/src/test/java/io/jenkins/plugins/forensics/git/util/GitRepositoryValidatorTest.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>GitSCM gitSCM &#61; createShallowGitScm(); Run&lt;?, ?&gt; run &#61; mock(Run.class); var envVars &#61; new EnvVars(); when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars); var workspace &#61; createWorkTree(); GitClient gitClient &#61; mock(GitClient.class); when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient); when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class)); var logger &#61; createLogger(); var validator &#61; new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger); assertThat(validator.isGitRepository()).isTrue();</code></pre>
assertThat(logger.getInfoMessages()).contains(GitRepositoryValidator.INFO_SHALLOW_CLONE_COMMIT_RECORDING);

Check warning on line 77 in plugin/src/test/java/io/jenkins/plugins/forensics/git/util/GitRepositoryValidatorTest.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>GitSCM gitSCM &#61; createNonShallowGitScm(); Run&lt;?, ?&gt; run &#61; mock(Run.class); var envVars &#61; new EnvVars(); when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars); var workspace &#61; createWorkTree(); GitClient gitClient &#61; mock(GitClient.class); when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient); when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class)); var logger &#61; createLogger(); var validator &#61; new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger); assertThat(validator.isGitRepository()).isTrue(); assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE);</code></pre>
assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE);
}

@Test
void isFullGitRepositoryShouldReturnFalseForNonGitScm() {
var logger = createLogger();
var validator = new GitRepositoryValidator(new NullSCM(), null, createWorkTree(), NULL_LISTENER, logger);

assertThat(validator.isFullGitRepository()).isFalse();
assertThat(logger.getInfoMessages()).contains("SCM 'hudson.scm.NullSCM' is not of type GitSCM");
}

@Test
void isFullGitRepositoryShouldReturnTrueForNonShallowGit() throws IOException, InterruptedException {
GitSCM gitSCM = createNonShallowGitScm();
Run<?, ?> run = mock(Run.class);
var envVars = new EnvVars();
when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars);
var workspace = createWorkTree();
GitClient gitClient = mock(GitClient.class);
when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient);
when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class));

var logger = createLogger();
var validator = new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger);

assertThat(validator.isFullGitRepository()).isTrue();

Check warning on line 104 in plugin/src/test/java/io/jenkins/plugins/forensics/git/util/GitRepositoryValidatorTest.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>void isGitRepositoryShouldReturnTrueForNonShallowGit() throws IOException, InterruptedException { GitSCM gitSCM &#61; createNonShallowGitScm(); Run&lt;?, ?&gt; run &#61; mock(Run.class); var envVars &#61; new EnvVars(); when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars); var workspace &#61; createWorkTree(); GitClient gitClient &#61; mock(GitClient.class); when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient); when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class)); var logger &#61; createLogger(); var validator &#61; new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger); assertThat(validator.isGitRepository()).isTrue();</code></pre>

Check warning on line 104 in plugin/src/test/java/io/jenkins/plugins/forensics/git/util/GitRepositoryValidatorTest.java

View check run for this annotation

ci.jenkins.io / CPD

CPD

LOW: Found duplicated code.
Raw output
<pre><code>GitSCM gitSCM &#61; createShallowGitScm(); Run&lt;?, ?&gt; run &#61; mock(Run.class); var envVars &#61; new EnvVars(); when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars); var workspace &#61; createWorkTree(); GitClient gitClient &#61; mock(GitClient.class); when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient); when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class)); var logger &#61; createLogger(); var validator &#61; new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger); assertThat(validator.isGitRepository()).isTrue();</code></pre>
assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE);
}

@Test
void isFullGitRepositoryShouldReturnFalseForShallowClone() {
CloneOption shallowOption = mock(CloneOption.class);
when(shallowOption.isShallow()).thenReturn(true);

GitSCM gitSCM = mock(GitSCM.class);
when(gitSCM.getExtensions()).thenReturn(new DescribableList<>(Saveable.NOOP, Lists.list(shallowOption)));

var logger = createLogger();
var validator = new GitRepositoryValidator(gitSCM, mock(Run.class), createWorkTree(), NULL_LISTENER, logger);

assertThat(validator.isFullGitRepository()).isFalse();
assertThat(logger.getInfoMessages()).contains(GitRepositoryValidator.INFO_SHALLOW_CLONE);
assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE_COMMIT_RECORDING);
}

@Test
void isShallowCloneShouldReturnFalseForNonGitScm() {
var validator = new GitRepositoryValidator(new NullSCM(), null, createWorkTree(), NULL_LISTENER, createLogger());

assertThat(validator.isShallowClone()).isFalse();
}

@Test
void isShallowCloneShouldReturnFalseForNonShallowGit() {
var validator = new GitRepositoryValidator(createNonShallowGitScm(), null, createWorkTree(), NULL_LISTENER, createLogger());

assertThat(validator.isShallowClone()).isFalse();
}

@Test
void isShallowCloneShouldReturnTrueForShallowGit() {
var validator = new GitRepositoryValidator(createShallowGitScm(), null, createWorkTree(), NULL_LISTENER, createLogger());

assertThat(validator.isShallowClone()).isTrue();
}

private GitSCM createNonShallowGitScm() {
GitSCM git = mock(GitSCM.class);
when(git.getExtensions()).thenReturn(new DescribableList<>(Saveable.NOOP));
return git;
}

private GitSCM createShallowGitScm() {
CloneOption shallowOption = mock(CloneOption.class);
when(shallowOption.isShallow()).thenReturn(true);

GitSCM git = mock(GitSCM.class);
when(git.getExtensions()).thenReturn(new DescribableList<>(Saveable.NOOP, Lists.list(shallowOption)));
return git;
}

private FilePath createWorkTree() {
File mock = mock(File.class);
when(mock.getPath()).thenReturn("/");
return new FilePath(mock);
}

private FilteredLog createLogger() {
return new FilteredLog("errors");
}
}
Loading