Skip to content

Commit 46d45e2

Browse files
authored
Merge pull request #1265 from akash-manna-sky/JENKINS-74921
Log an error message when Git repositories are checked out with shallow clone
2 parents 07d1606 + 790f51d commit 46d45e2

7 files changed

Lines changed: 254 additions & 13 deletions

File tree

plugin/src/main/java/io/jenkins/plugins/forensics/git/blame/GitBlamerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class GitBlamerFactory extends BlamerFactory {
2626
public Optional<Blamer> createBlamer(final SCM scm, final Run<?, ?> build,
2727
final FilePath workTree, final TaskListener listener, final FilteredLog logger) {
2828
var validator = new GitRepositoryValidator(scm, build, workTree, listener, logger);
29-
if (validator.isGitRepository()) {
29+
if (validator.isFullGitRepository()) {
3030
var client = validator.createClient();
3131
logger.logInfo("-> Git blamer successfully created in working tree '%s'",
3232
new PathUtil().getAbsolutePath(client.getWorkTree().getRemote()));

plugin/src/main/java/io/jenkins/plugins/forensics/git/delta/GitDeltaCalculatorFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class GitDeltaCalculatorFactory extends DeltaCalculatorFactory {
2626
public Optional<DeltaCalculator> createDeltaCalculator(final SCM scm, final Run<?, ?> run, final FilePath workspace,
2727
final TaskListener listener, final FilteredLog logger) {
2828
var validator = new GitRepositoryValidator(scm, run, workspace, listener, logger);
29-
if (validator.isGitRepository()) {
29+
if (validator.isFullGitRepository()) {
3030
var client = validator.createClient();
3131
logger.logInfo("-> Git delta calculator successfully created for SCM '%s' in working tree '%s'",
3232
scm, new PathUtil().getAbsolutePath(client.getWorkTree().getRemote()));

plugin/src/main/java/io/jenkins/plugins/forensics/git/miner/CommitStatisticsStep.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public void perform(@NonNull final Run<?, ?> run, @NonNull final FilePath worksp
102102
logHandler.log(logger);
103103

104104
var validator = new GitRepositoryValidator(repository, run, workspace, listener, logger);
105-
if (validator.isGitRepository()) {
105+
if (validator.isFullGitRepository()) {
106106
try {
107107
computeStats(run, logger, repository, validator);
108108
}

plugin/src/main/java/io/jenkins/plugins/forensics/git/miner/GitMinerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class GitMinerFactory extends MinerFactory {
2525
public Optional<RepositoryMiner> createMiner(final SCM scm, final Run<?, ?> build, final FilePath workTree,
2626
final TaskListener listener, final FilteredLog logger) {
2727
var validator = new GitRepositoryValidator(scm, build, workTree, listener, logger);
28-
if (validator.isGitRepository()) {
28+
if (validator.isFullGitRepository()) {
2929
logger.logInfo("-> Git miner successfully created in working tree '%s'", workTree);
3030

3131
return Optional.of(new GitRepositoryMiner(validator.createClient()));

plugin/src/main/java/io/jenkins/plugins/forensics/git/util/GitRepositoryValidator.java

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@
2121
* @author Ullrich Hafner
2222
*/
2323
public class GitRepositoryValidator {
24-
/** Error message. */
24+
/** Info message when a shallow clone is detected and blame/mining is skipped. */
2525
@VisibleForTesting
2626
public static final String INFO_SHALLOW_CLONE = "Skipping issues blame since Git has been configured with shallow clone";
2727

28+
/** Info message when a shallow clone is detected but commit recording is still performed. */
29+
@VisibleForTesting
30+
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";
31+
2832
private static final String HEAD = "HEAD";
2933

3034
private final SCM scm;
@@ -57,24 +61,52 @@ public GitRepositoryValidator(final SCM scm, final Run<?, ?> build,
5761
}
5862

5963
/**
60-
* Returns whether the specified working tree contains a valid Git repository that can be used to run one of the
61-
* forensics analyzers.
64+
* Returns whether the specified working tree contains a valid Git repository. Shallow clones are accepted
65+
* for operations that do not require full history (e.g., commit recording).
6266
*
63-
* @return {@code true} if the working tree contains a valid repository, {@code false} otherwise
67+
* @return {@code true} if the working tree contains a valid repository (including shallow clones),
68+
* {@code false} otherwise
6469
*/
6570
public boolean isGitRepository() {
6671
if (scm instanceof GitSCM) {
67-
return isValidGitRoot((GitSCM) scm);
72+
return isValidGitRoot((GitSCM) scm, false);
6873
}
6974
logger.logInfo("SCM '%s' is not of type GitSCM", scm.getType());
7075
return false;
7176
}
7277

73-
private boolean isValidGitRoot(final GitSCM git) {
74-
if (isShallow(git)) {
75-
logger.logInfo(INFO_SHALLOW_CLONE);
78+
/**
79+
* Returns whether the specified working tree contains a valid Git repository with full history (no shallow
80+
* clone). This is required for operations that need full commit history, such as blame analysis and
81+
* repository mining.
82+
*
83+
* @return {@code true} if the working tree contains a valid non-shallow repository, {@code false} otherwise
84+
*/
85+
public boolean isFullGitRepository() {
86+
if (scm instanceof GitSCM) {
87+
return isValidGitRoot((GitSCM) scm, true);
88+
}
89+
logger.logInfo("SCM '%s' is not of type GitSCM", scm.getType());
90+
return false;
91+
}
7692

77-
return false;
93+
/**
94+
* Returns whether the Git repository is configured as a shallow clone.
95+
*
96+
* @return {@code true} if the repository is a shallow clone, {@code false} otherwise
97+
*/
98+
public boolean isShallowClone() {
99+
return scm instanceof GitSCM
100+
&& isShallow((GitSCM) scm);
101+
}
102+
103+
private boolean isValidGitRoot(final GitSCM git, final boolean rejectShallowClone) {
104+
if (isShallow(git)) {
105+
if (rejectShallowClone) {
106+
logger.logInfo(INFO_SHALLOW_CLONE);
107+
return false;
108+
}
109+
logger.logInfo(INFO_SHALLOW_CLONE_COMMIT_RECORDING);
78110
}
79111

80112
try {

plugin/src/test/java/io/jenkins/plugins/forensics/git/reference/GitCheckoutListenerITest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

55
import java.io.IOException;
66
import java.util.ArrayList;
7+
import java.util.Collections;
78
import java.util.List;
89

910
import hudson.model.FreeStyleProject;
1011
import hudson.plugins.git.GitSCM;
12+
import hudson.plugins.git.extensions.impl.CloneOption;
1113

1214
import io.jenkins.plugins.forensics.git.util.GitCommitTextDecorator;
1315
import io.jenkins.plugins.forensics.git.util.GitITest;
16+
import io.jenkins.plugins.forensics.git.util.GitRepositoryValidator;
1417

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

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

171+
/**
172+
* Verifies that a build using a shallow clone (depth=1) still records a {@link GitCommitsRecord}.
173+
* Previously, the {@link io.jenkins.plugins.forensics.git.util.GitRepositoryValidator#isGitRepository()}
174+
* method incorrectly returned {@code false} for shallow clones, preventing commit recording and thus
175+
* breaking {@code discoverGitReferenceBuild}.
176+
*
177+
* @throws Exception
178+
* in case of an IO exception
179+
*/
180+
@Test
181+
void shouldRecordCommitsForShallowClone() throws Exception {
182+
createAndCommitFile("First.java", "first commit");
183+
createAndCommitFile("Second.java", "second commit");
184+
185+
var job = createFreeStyleProjectWithShallowClone("shallow-listener");
186+
187+
var record = buildSuccessfully(job).getAction(GitCommitsRecord.class);
188+
assertThat(record)
189+
.as("GitCommitsRecord must be present even for shallow-clone builds (JENKINS-74921)")
190+
.isNotNull()
191+
.isNotEmpty()
192+
.hasNoErrorMessages()
193+
.hasInfoMessages(
194+
GitRepositoryValidator.INFO_SHALLOW_CLONE_COMMIT_RECORDING,
195+
"Found no previous build with recorded Git commits",
196+
"-> Starting initial recording of commits");
197+
}
198+
168199
private void createAndCommitFile(final String fileName, final String content) {
169200
writeFile(fileName, content);
170201
addFile(fileName);
@@ -176,4 +207,13 @@ private FreeStyleProject createFreeStyleProject(final String name) throws IOExce
176207
project.setScm(new GitSCM(getGitRepositoryPath()));
177208
return project;
178209
}
210+
211+
private FreeStyleProject createFreeStyleProjectWithShallowClone(final String name) throws IOException {
212+
var project = createProject(FreeStyleProject.class, name);
213+
var cloneOption = new CloneOption(true, null, null);
214+
var scm = new GitSCM(GitSCM.createRepoList(getGitRepositoryPath(), null),
215+
Collections.emptyList(), null, null, Collections.singletonList(cloneOption));
216+
project.setScm(scm);
217+
return project;
218+
}
179219
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package io.jenkins.plugins.forensics.git.util;
2+
3+
import org.assertj.core.util.Lists;
4+
import org.eclipse.jgit.lib.ObjectId;
5+
import org.junit.jupiter.api.Test;
6+
7+
import edu.hm.hafner.util.FilteredLog;
8+
9+
import java.io.File;
10+
import java.io.IOException;
11+
12+
import org.jenkinsci.plugins.gitclient.GitClient;
13+
import hudson.EnvVars;
14+
import hudson.FilePath;
15+
import hudson.model.Run;
16+
import hudson.model.Saveable;
17+
import hudson.model.TaskListener;
18+
import hudson.plugins.git.GitSCM;
19+
import hudson.plugins.git.extensions.impl.CloneOption;
20+
import hudson.scm.NullSCM;
21+
import hudson.util.DescribableList;
22+
23+
import static io.jenkins.plugins.forensics.assertions.Assertions.*;
24+
import static org.mockito.Mockito.*;
25+
26+
/**
27+
* Tests the class {@link GitRepositoryValidator}.
28+
*
29+
* @author Akash Manna
30+
*/
31+
class GitRepositoryValidatorTest {
32+
private static final TaskListener NULL_LISTENER = TaskListener.NULL;
33+
34+
@Test
35+
void isGitRepositoryShouldReturnFalseForNonGitScm() {
36+
var logger = createLogger();
37+
var validator = new GitRepositoryValidator(new NullSCM(), null, createWorkTree(), NULL_LISTENER, logger);
38+
39+
assertThat(validator.isGitRepository()).isFalse();
40+
assertThat(logger.getInfoMessages()).contains("SCM 'hudson.scm.NullSCM' is not of type GitSCM");
41+
}
42+
43+
@Test
44+
void isGitRepositoryShouldReturnTrueForNonShallowGit() throws IOException, InterruptedException {
45+
GitSCM gitSCM = createNonShallowGitScm();
46+
Run<?, ?> run = mock(Run.class);
47+
var envVars = new EnvVars();
48+
when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars);
49+
var workspace = createWorkTree();
50+
GitClient gitClient = mock(GitClient.class);
51+
when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient);
52+
when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class));
53+
54+
var logger = createLogger();
55+
var validator = new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger);
56+
57+
assertThat(validator.isGitRepository()).isTrue();
58+
assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE);
59+
assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE_COMMIT_RECORDING);
60+
}
61+
62+
@Test
63+
void isGitRepositoryShouldReturnTrueForShallowClone() throws IOException, InterruptedException {
64+
GitSCM gitSCM = createShallowGitScm();
65+
Run<?, ?> run = mock(Run.class);
66+
var envVars = new EnvVars();
67+
when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars);
68+
var workspace = createWorkTree();
69+
GitClient gitClient = mock(GitClient.class);
70+
when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient);
71+
when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class));
72+
73+
var logger = createLogger();
74+
var validator = new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger);
75+
76+
assertThat(validator.isGitRepository()).isTrue();
77+
assertThat(logger.getInfoMessages()).contains(GitRepositoryValidator.INFO_SHALLOW_CLONE_COMMIT_RECORDING);
78+
assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE);
79+
}
80+
81+
@Test
82+
void isFullGitRepositoryShouldReturnFalseForNonGitScm() {
83+
var logger = createLogger();
84+
var validator = new GitRepositoryValidator(new NullSCM(), null, createWorkTree(), NULL_LISTENER, logger);
85+
86+
assertThat(validator.isFullGitRepository()).isFalse();
87+
assertThat(logger.getInfoMessages()).contains("SCM 'hudson.scm.NullSCM' is not of type GitSCM");
88+
}
89+
90+
@Test
91+
void isFullGitRepositoryShouldReturnTrueForNonShallowGit() throws IOException, InterruptedException {
92+
GitSCM gitSCM = createNonShallowGitScm();
93+
Run<?, ?> run = mock(Run.class);
94+
var envVars = new EnvVars();
95+
when(run.getEnvironment(NULL_LISTENER)).thenReturn(envVars);
96+
var workspace = createWorkTree();
97+
GitClient gitClient = mock(GitClient.class);
98+
when(gitSCM.createClient(NULL_LISTENER, envVars, run, workspace)).thenReturn(gitClient);
99+
when(gitClient.revParse(anyString())).thenReturn(mock(ObjectId.class));
100+
101+
var logger = createLogger();
102+
var validator = new GitRepositoryValidator(gitSCM, run, workspace, NULL_LISTENER, logger);
103+
104+
assertThat(validator.isFullGitRepository()).isTrue();
105+
assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE);
106+
}
107+
108+
@Test
109+
void isFullGitRepositoryShouldReturnFalseForShallowClone() {
110+
CloneOption shallowOption = mock(CloneOption.class);
111+
when(shallowOption.isShallow()).thenReturn(true);
112+
113+
GitSCM gitSCM = mock(GitSCM.class);
114+
when(gitSCM.getExtensions()).thenReturn(new DescribableList<>(Saveable.NOOP, Lists.list(shallowOption)));
115+
116+
var logger = createLogger();
117+
var validator = new GitRepositoryValidator(gitSCM, mock(Run.class), createWorkTree(), NULL_LISTENER, logger);
118+
119+
assertThat(validator.isFullGitRepository()).isFalse();
120+
assertThat(logger.getInfoMessages()).contains(GitRepositoryValidator.INFO_SHALLOW_CLONE);
121+
assertThat(logger.getInfoMessages()).doesNotContain(GitRepositoryValidator.INFO_SHALLOW_CLONE_COMMIT_RECORDING);
122+
}
123+
124+
@Test
125+
void isShallowCloneShouldReturnFalseForNonGitScm() {
126+
var validator = new GitRepositoryValidator(new NullSCM(), null, createWorkTree(), NULL_LISTENER, createLogger());
127+
128+
assertThat(validator.isShallowClone()).isFalse();
129+
}
130+
131+
@Test
132+
void isShallowCloneShouldReturnFalseForNonShallowGit() {
133+
var validator = new GitRepositoryValidator(createNonShallowGitScm(), null, createWorkTree(), NULL_LISTENER, createLogger());
134+
135+
assertThat(validator.isShallowClone()).isFalse();
136+
}
137+
138+
@Test
139+
void isShallowCloneShouldReturnTrueForShallowGit() {
140+
var validator = new GitRepositoryValidator(createShallowGitScm(), null, createWorkTree(), NULL_LISTENER, createLogger());
141+
142+
assertThat(validator.isShallowClone()).isTrue();
143+
}
144+
145+
private GitSCM createNonShallowGitScm() {
146+
GitSCM git = mock(GitSCM.class);
147+
when(git.getExtensions()).thenReturn(new DescribableList<>(Saveable.NOOP));
148+
return git;
149+
}
150+
151+
private GitSCM createShallowGitScm() {
152+
CloneOption shallowOption = mock(CloneOption.class);
153+
when(shallowOption.isShallow()).thenReturn(true);
154+
155+
GitSCM git = mock(GitSCM.class);
156+
when(git.getExtensions()).thenReturn(new DescribableList<>(Saveable.NOOP, Lists.list(shallowOption)));
157+
return git;
158+
}
159+
160+
private FilePath createWorkTree() {
161+
File mock = mock(File.class);
162+
when(mock.getPath()).thenReturn("/");
163+
return new FilePath(mock);
164+
}
165+
166+
private FilteredLog createLogger() {
167+
return new FilteredLog("errors");
168+
}
169+
}

0 commit comments

Comments
 (0)