-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[JENKINS-49757] Remove redundant fetch #904
Changes from 32 commits
8d58567
23158ea
9b25935
2cf858e
11a87d4
5d94f2e
1cfb539
e6ac2ed
eaded93
21ead1e
67ced0e
995d803
ae036cc
82ffbbf
3bc0059
57ab0d0
05daa5e
d3a1e22
36e3e7d
deb1b78
c4c47cb
e8d1bca
b54faeb
79cf010
a38f1e2
1be3697
15e2f52
7957017
f2fecc8
a7685db
1623253
09726f1
c7b7ec6
f7b77e6
534818a
c2e097d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,6 @@ | |
import hudson.model.Run; | ||
import hudson.model.Saveable; | ||
import hudson.model.TaskListener; | ||
import hudson.model.queue.Tasks; | ||
import hudson.plugins.git.browser.GitRepositoryBrowser; | ||
import hudson.plugins.git.extensions.GitSCMExtension; | ||
import hudson.plugins.git.extensions.GitSCMExtensionDescriptor; | ||
|
@@ -39,6 +38,7 @@ | |
import hudson.plugins.git.extensions.impl.LocalBranch; | ||
import hudson.plugins.git.extensions.impl.RelativeTargetDirectory; | ||
import hudson.plugins.git.extensions.impl.PreBuildMerge; | ||
import hudson.plugins.git.extensions.impl.CloneOption; | ||
import hudson.plugins.git.opt.PreBuildMergeOptions; | ||
import hudson.plugins.git.util.Build; | ||
import hudson.plugins.git.util.*; | ||
|
@@ -1111,6 +1111,7 @@ public EnvVars getEnvironment() { | |
private void retrieveChanges(Run build, GitClient git, TaskListener listener) throws IOException, InterruptedException { | ||
final PrintStream log = listener.getLogger(); | ||
|
||
boolean removeRedundantFetch = false; | ||
List<RemoteConfig> repos = getParamExpandedRepos(build, listener); | ||
if (repos.isEmpty()) return; // defensive check even though this is an invalid configuration | ||
|
||
|
@@ -1130,13 +1131,20 @@ private void retrieveChanges(Run build, GitClient git, TaskListener listener) th | |
ext.decorateCloneCommand(this, build, git, listener, cmd); | ||
} | ||
cmd.execute(); | ||
// determine if second fetch is required | ||
CloneOption option = extensions.get(CloneOption.class); | ||
removeRedundantFetch = determineRedundantFetch(option, rc); | ||
} catch (GitException ex) { | ||
ex.printStackTrace(listener.error("Error cloning remote repo '" + rc.getName() + "'")); | ||
throw new AbortException("Error cloning remote repo '" + rc.getName() + "'"); | ||
} | ||
} | ||
|
||
for (RemoteConfig remoteRepository : repos) { | ||
if (remoteRepository.equals(repos.get(0)) && removeRedundantFetch){ | ||
log.println("Avoid second fetch"); | ||
continue; | ||
} | ||
try { | ||
fetchFrom(git, build, listener, remoteRepository); | ||
} catch (GitException ex) { | ||
|
@@ -1148,6 +1156,30 @@ private void retrieveChanges(Run build, GitClient git, TaskListener listener) th | |
} | ||
} | ||
|
||
private boolean determineRedundantFetch(CloneOption option, RemoteConfig rc) { | ||
List<RefSpec> initialFetchRefSpecs = rc.getFetchRefSpecs(); | ||
boolean isDefaultRefspec = true; // default refspec is any refspec with "refs/heads/" mapping | ||
boolean removeSecondFetch = true; | ||
if (initialFetchRefSpecs != null) { | ||
for (RefSpec ref : initialFetchRefSpecs) { | ||
if (!ref.toString().contains("refs/heads")) { | ||
isDefaultRefspec = false; // if refspec is not of default type, preserve second fetch | ||
} | ||
} | ||
if (option == null) { | ||
removeSecondFetch = isDefaultRefspec; | ||
} else { | ||
if (!option.isHonorRefspec()) { | ||
removeSecondFetch = isDefaultRefspec; | ||
} else { | ||
removeSecondFetch = true; // avoid second fetch call if honor refspec is enabled | ||
} | ||
} | ||
} | ||
// if initial fetch refspec contains "refs/heads/*" (default refspec), ignore the second fetch call | ||
return removeSecondFetch; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a test that passed in a null refspec but the UserRemoteConfig constructor converts nulls to empty strings. I think that is unreachable code due to the UserRemoteConfig use of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to my assumptions, the need to add the null check was to check for empty refspecs, as Mark as correctly pointed out, the If the UserRemoteConfig was not fixing empty refspecs, the logic presented by me would gladly pass |
||
} | ||
|
||
@Override | ||
public void checkout(Run<?, ?> build, Launcher launcher, FilePath workspace, TaskListener listener, File changelogFile, SCMRevisionState baseline) | ||
throws IOException, InterruptedException { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,8 +127,11 @@ public static void createRepo() throws Exception { | |
String initialImpl = random.nextBoolean() ? "git" : "jgit"; | ||
GitClient gitClient = Git.with(TaskListener.NULL, new EnvVars()).in(repoRoot).using(initialImpl).getClient(); | ||
gitClient.init_().workspace(repoRoot.getAbsolutePath()).execute(); | ||
new CliGitCommand(gitClient, "config", "user.name", "ChangeSet Truncation Test"); | ||
new CliGitCommand(gitClient, "config", "user.email", "[email protected]"); | ||
String[] expectedResult = {""}; | ||
CliGitCommand gitCmd = new CliGitCommand(gitClient, "config", "user.name", "ChangeSet Truncation Test"); | ||
assertThat(gitCmd.run(), is(expectedResult)); | ||
gitCmd = new CliGitCommand(gitClient, "config", "user.email", "[email protected]"); | ||
assertThat(gitCmd.run(), is(expectedResult)); | ||
} | ||
|
||
private ObjectId commitOneFile(GitClient gitClient, final String commitSummary) throws Exception { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -83,6 +83,8 @@ | |
import java.util.*; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import java.util.regex.Pattern; | ||
import java.util.stream.Collectors; | ||
|
||
import org.eclipse.jgit.transport.RemoteConfig; | ||
import static org.hamcrest.MatcherAssert.*; | ||
|
@@ -338,6 +340,142 @@ public void testSpecificRefspecs() throws Exception { | |
build(projectWithFoo, Result.SUCCESS, commitFile1); | ||
} | ||
|
||
/** | ||
* This test confirms the behaviour of avoiding the second fetch in GitSCM checkout() | ||
**/ | ||
@Test | ||
@Issue("JENKINS-56404") | ||
public void testAvoidRedundantFetch() throws Exception { | ||
List<UserRemoteConfig> repos = new ArrayList<>(); | ||
repos.add(new UserRemoteConfig(testRepo.gitDir.getAbsolutePath(), "origin", "+refs/heads/*:refs/remotes/*", null)); | ||
|
||
/* Without honor refspec on initial clone */ | ||
FreeStyleProject projectWithMaster = setupProject(repos, Collections.singletonList(new BranchSpec("master")), null, false, null); | ||
if (random.nextBoolean()) { | ||
/* Randomly enable shallow clone, should not alter test assertions */ | ||
CloneOption cloneOptionMaster = new CloneOption(false, null, null); | ||
cloneOptionMaster.setDepth(1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I fail to understand, why would need a shallow clone to improve coverage when we already have a full normal clone? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question. One branch was not being reached when a cloneOption extension is detected but does not have honor refspec enabled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay. I didn't consider other CloneOptions. If honor refspec is false, with avoiding the second fetch, we will also avoid other clone options like Shallow Clone, Disable tags, Reference a repo and timeout. The good news is that the first fetch is capable to execute all of these options if we miss the second fetch. It should not make any difference to the user's expectation on git repository information after the checkout. |
||
((GitSCM) projectWithMaster.getScm()).getExtensions().add(cloneOptionMaster); | ||
} | ||
|
||
// create initial commit | ||
final String commitFile1 = "commitFile1"; | ||
commit(commitFile1, johnDoe, "Commit in master"); | ||
|
||
FreeStyleBuild build = build(projectWithMaster, Result.SUCCESS); | ||
|
||
assertRedundantFetchIsTrue(build, "+refs/heads/*:refs/remotes/origin/*"); | ||
} | ||
|
||
/** | ||
* After avoiding the second fetch call in retrieveChanges(), this test verifies there is no data loss by fetching a repository | ||
* (git init + git fetch) with a narrow refspec but without CloneOption of honorRefspec = true on initial clone | ||
* First fetch -> wide refspec | ||
* Second fetch -> narrow refspec (avoided) | ||
**/ | ||
@Test | ||
@Issue("JENKINS-56404") | ||
public void testAvoidRedundantFetchWithoutHonorRefSpec() throws Exception { | ||
List<UserRemoteConfig> repos = new ArrayList<>(); | ||
repos.add(new UserRemoteConfig(testRepo.gitDir.getAbsolutePath(), "origin", "+refs/heads/foo:refs/remotes/foo", null)); | ||
|
||
/* Without honor refspec on initial clone */ | ||
FreeStyleProject projectWithMaster = setupProject(repos, Collections.singletonList(new BranchSpec("master")), null, false, null); | ||
if (random.nextBoolean()) { | ||
/* Randomly enable shallow clone, should not alter test assertions */ | ||
CloneOption cloneOptionMaster = new CloneOption(false, null, null); | ||
cloneOptionMaster.setDepth(1); | ||
((GitSCM) projectWithMaster.getScm()).getExtensions().add(cloneOptionMaster); | ||
} | ||
|
||
// create initial commit | ||
final String commitFile1 = "commitFile1"; | ||
commit(commitFile1, johnDoe, "Commit in master"); | ||
// Add another branch 'foo' | ||
git.branch("foo"); | ||
git.checkout().branch("foo"); | ||
commit(commitFile1, johnDoe, "Commit in foo"); | ||
|
||
// Build will be success because the initial clone disregards refspec and fetches all branches | ||
FreeStyleBuild build = build(projectWithMaster, Result.SUCCESS); | ||
FilePath childFile = returnFile(build); | ||
|
||
if (childFile != null) { | ||
// assert that no data is lost by avoidance of second fetch | ||
assertThat("master branch was not fetched", childFile.readToString(), containsString("master")); | ||
assertThat("foo branch was not fetched", childFile.readToString(), containsString("foo")); | ||
} | ||
|
||
String wideRefSpec = "+refs/heads/*:refs/remotes/origin/*"; | ||
assertRedundantFetchIsTrue(build, wideRefSpec); | ||
|
||
assertThat(build.getResult(), is(Result.SUCCESS)); | ||
} | ||
|
||
/** | ||
* After avoiding the second fetch call in retrieveChanges(), this test verifies there is no data loss by fetching a | ||
* repository(git init + git fetch) with a narrow refspec with CloneOption of honorRefspec = true on initial clone | ||
* First fetch -> narrow refspec (since refspec is honored on initial clone) | ||
* Second fetch -> narrow refspec (avoided) | ||
**/ | ||
@Test | ||
@Issue("JENKINS-56404") | ||
public void testAvoidRedundantFetchWithHonorRefSpec() throws Exception { | ||
List<UserRemoteConfig> repos = new ArrayList<>(); | ||
String refSpec = "+refs/heads/foo:refs/remotes/foo"; | ||
repos.add(new UserRemoteConfig(testRepo.gitDir.getAbsolutePath(), "origin", refSpec, null)); | ||
|
||
/* With honor refspec on initial clone */ | ||
FreeStyleProject projectWithMaster = setupProject(repos, Collections.singletonList(new BranchSpec("master")), null, false, null); | ||
CloneOption cloneOptionMaster = new CloneOption(false, null, null); | ||
cloneOptionMaster.setHonorRefspec(true); | ||
((GitSCM)projectWithMaster.getScm()).getExtensions().add(cloneOptionMaster); | ||
|
||
// create initial commit | ||
final String commitFile1 = "commitFile1"; | ||
commit(commitFile1, johnDoe, "Commit in master"); | ||
// Add another branch 'foo' | ||
git.branch("foo"); | ||
git.checkout().branch("foo"); | ||
commit(commitFile1, johnDoe, "Commit in foo"); | ||
|
||
// Build will be failure because the initial clone regards refspec and fetches branch 'foo' only. | ||
FreeStyleBuild build = build(projectWithMaster, Result.FAILURE); | ||
|
||
FilePath childFile = returnFile(build); | ||
assertNotNull(childFile); | ||
// assert that no data is lost by avoidance of second fetch | ||
assertThat(childFile.readToString(), not(containsString("master"))); | ||
assertThat("foo branch was not fetched", childFile.readToString(), containsString("foo")); | ||
assertRedundantFetchIsTrue(build, refSpec); | ||
|
||
assertThat(build.getResult(), is(Result.FAILURE)); | ||
} | ||
|
||
// Checks if the second fetch is being avoided | ||
private void assertRedundantFetchIsTrue(FreeStyleBuild build, String refSpec) throws IOException { | ||
List<String> values = build.getLog(Integer.MAX_VALUE); | ||
|
||
//String fetchArg = " > git fetch --tags --force --progress -- " + testRepo.gitDir.getAbsolutePath() + argRefSpec + " # timeout=10"; | ||
Pattern fetchPattern = Pattern.compile(".* git.* fetch .*"); | ||
List<String> fetchCommands = values.stream().filter(fetchPattern.asPredicate()).collect(Collectors.toList()); | ||
|
||
// After the fix, git fetch is called exactly once | ||
assertThat("Fetch commands were: " + fetchCommands, fetchCommands, hasSize(1)); | ||
} | ||
|
||
// Returns the file FETCH_HEAD found in .git | ||
private FilePath returnFile(FreeStyleBuild build) throws IOException, InterruptedException { | ||
List<FilePath> files = build.getProject().getWorkspace().list(); | ||
FilePath resultFile = null; | ||
for (FilePath s : files) { | ||
if(s.getName().equals(".git")) { | ||
resultFile = s.child("FETCH_HEAD"); | ||
} | ||
} | ||
return resultFile; | ||
} | ||
|
||
/** | ||
* This test and testSpecificRefspecs confirm behaviors of | ||
* refspecs on initial clone. Without the CloneOption to honor refspec, all | ||
|
@@ -837,36 +975,36 @@ public void testBasicExcludedRegion() throws Exception { | |
assertFalse("scm polling should not detect any more changes after build", project.poll(listener).hasChanges()); | ||
} | ||
|
||
private int findLogLineStartsWith(List<String> buildLog, String initialString) { | ||
int logLine = 0; | ||
for (String logString : buildLog) { | ||
if (logString.startsWith(initialString)) { | ||
return logLine; | ||
} | ||
logLine++; | ||
} | ||
return -1; | ||
} | ||
|
||
@Test | ||
public void testCleanBeforeCheckout() throws Exception { | ||
FreeStyleProject p = setupProject("master", false, null, null, "Jane Doe", null); | ||
((GitSCM)p.getScm()).getExtensions().add(new CleanBeforeCheckout()); | ||
|
||
/* First build should not clean, since initial clone is always clean */ | ||
final String commitFile1 = "commitFile1"; | ||
final String commitFile2 = "commitFile2"; | ||
commit(commitFile1, johnDoe, janeDoe, "Commit number 1"); | ||
commit(commitFile2, johnDoe, janeDoe, "Commit number 2"); | ||
final FreeStyleBuild firstBuild = build(p, Result.SUCCESS, commitFile1); | ||
final String branch1 = "Branch1"; | ||
final String branch2 = "Branch2"; | ||
List<BranchSpec> branches = new ArrayList<>(); | ||
branches.add(new BranchSpec("master")); | ||
branches.add(new BranchSpec(branch1)); | ||
branches.add(new BranchSpec(branch2)); | ||
git.branch(branch1); | ||
git.checkout(branch1); | ||
p.poll(listener).hasChanges(); | ||
assertThat(firstBuild.getLog(175), hasItem("Cleaning workspace")); | ||
assertTrue(firstBuild.getLog().indexOf("Cleaning") > firstBuild.getLog().indexOf("Cloning")); //clean should be after clone | ||
assertTrue(firstBuild.getLog().indexOf("Cleaning") < firstBuild.getLog().indexOf("Checking out")); //clean before checkout | ||
assertTrue(firstBuild.getWorkspace().child(commitFile1).exists()); | ||
git.checkout(branch1); | ||
assertThat(firstBuild.getLog(50), not(hasItem("Cleaning workspace"))); | ||
/* Second build should clean, since first build might have modified the workspace */ | ||
final String commitFile2 = "commitFile2"; | ||
commit(commitFile2, johnDoe, janeDoe, "Commit number 2"); | ||
final FreeStyleBuild secondBuild = build(p, Result.SUCCESS, commitFile2); | ||
p.poll(listener).hasChanges(); | ||
assertThat(secondBuild.getLog(175), hasItem("Cleaning workspace")); | ||
assertTrue(secondBuild.getLog().indexOf("Cleaning") < secondBuild.getLog().indexOf("Fetching upstream changes")); | ||
assertTrue(secondBuild.getWorkspace().child(commitFile2).exists()); | ||
|
||
|
||
List<String> secondLog = secondBuild.getLog(50); | ||
assertThat(secondLog, hasItem("Cleaning workspace")); | ||
int cleaningLogLine = findLogLineStartsWith(secondLog, "Cleaning workspace"); | ||
int fetchingLogLine = findLogLineStartsWith(secondLog, "Fetching upstream changes from "); | ||
assertThat("Cleaning should happen before fetch", cleaningLogLine, is(lessThan(fetchingLogLine))); | ||
} | ||
|
||
@Issue("JENKINS-8342") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This if clause didn't help me to see clearer what you meant. While having a "!" in an
if
clause is perfectly fine, the "!" in theif-else
might cause confusion. I would have expectedwhile this piece of code is
My personal preference is to have the first approach in terms of readability. But it's just a matter of personal taste, so take this advice as it is, just an advice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great suggestion @fcojfernandez. I have added this in c2e097d.