2 * Copyright 2000-2018 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package jetbrains.buildServer.buildTriggers.vcs.git.agent;
19 import com.intellij.openapi.util.io.FileUtil;
20 import jetbrains.buildServer.agent.AgentRunningBuild;
21 import jetbrains.buildServer.agent.SmartDirectoryCleaner;
22 import jetbrains.buildServer.buildTriggers.vcs.git.GitUtils;
23 import jetbrains.buildServer.buildTriggers.vcs.git.MirrorManager;
24 import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitExecTimeout;
25 import jetbrains.buildServer.util.StringUtil;
26 import jetbrains.buildServer.vcs.CheckoutRules;
27 import jetbrains.buildServer.vcs.VcsException;
28 import jetbrains.buildServer.vcs.VcsRoot;
29 import com.intellij.openapi.diagnostic.Logger;
30 import org.eclipse.jgit.lib.Ref;
31 import org.eclipse.jgit.lib.Repository;
32 import org.eclipse.jgit.lib.RepositoryBuilder;
33 import org.eclipse.jgit.transport.URIish;
34 import org.jetbrains.annotations.NotNull;
37 import java.io.IOException;
38 import java.net.URISyntaxException;
39 import java.util.List;
43 * @author dmitry.neverov
45 public class UpdaterWithMirror extends UpdaterImpl {
47 private final static Logger LOG = Logger.getInstance(UpdaterWithMirror.class.getName());
49 public UpdaterWithMirror(@NotNull FS fs,
50 @NotNull AgentPluginConfig pluginConfig,
51 @NotNull MirrorManager mirrorManager,
52 @NotNull SmartDirectoryCleaner directoryCleaner,
53 @NotNull GitFactory gitFactory,
54 @NotNull AgentRunningBuild build,
55 @NotNull VcsRoot root,
56 @NotNull String version,
57 @NotNull File targetDir,
58 @NotNull CheckoutRules rules,
59 @NotNull CheckoutMode mode) throws VcsException {
60 super(fs, pluginConfig, mirrorManager, directoryCleaner, gitFactory, build, root, version, targetDir, rules, mode);
64 protected void doUpdate() throws VcsException {
69 private void updateLocalMirror() throws VcsException {
70 String message = "Update git mirror (" + myRoot.getRepositoryDir() + ")";
71 myLogger.activityStarted(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
73 updateLocalMirror(true);
74 //prepare refs for copying into working dir repository
75 myGitFactory.create(myRoot.getRepositoryDir()).packRefs().call();
77 myLogger.activityFinished(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
81 private void updateLocalMirror(boolean repeatFetchAttempt) throws VcsException {
82 File bareRepositoryDir = myRoot.getRepositoryDir();
83 String mirrorDescription = "local mirror of root " + myRoot.getName() + " at " + bareRepositoryDir;
84 LOG.info("Update " + mirrorDescription);
85 boolean fetchRequired = true;
86 if (isValidGitRepo(bareRepositoryDir)) {
87 removeOrphanedIdxFiles(bareRepositoryDir);
89 FileUtil.delete(bareRepositoryDir);
91 boolean newMirror = false;
92 if (!bareRepositoryDir.exists()) {
93 LOG.info("Init " + mirrorDescription);
94 bareRepositoryDir.mkdirs();
95 GitFacade git = myGitFactory.create(bareRepositoryDir);
96 git.init().setBare(true).call();
97 configureRemoteUrl(bareRepositoryDir);
100 configureRemoteUrl(bareRepositoryDir);
101 boolean outdatedTagsFound = removeOutdatedRefs(bareRepositoryDir);
102 if (!outdatedTagsFound) {
103 LOG.debug("Try to find revision " + myRevision + " in " + mirrorDescription);
104 Ref ref = getRef(bareRepositoryDir, GitUtils.createRemoteRef(myFullBranchName));
105 if (ref != null && myRevision.equals(ref.getObjectId().name())) {
106 LOG.info("No fetch required for revision '" + myRevision + "' in " + mirrorDescription);
107 fetchRequired = false;
111 FetchHeadsMode fetchHeadsMode = myPluginConfig.getFetchHeadsMode();
112 Ref ref = getRef(bareRepositoryDir, myFullBranchName);
114 fetchRequired = true;
115 if (!fetchRequired && fetchHeadsMode != FetchHeadsMode.ALWAYS)
117 if (!newMirror && optimizeMirrorBeforeFetch()) {
118 GitFacade git = myGitFactory.create(bareRepositoryDir);
123 switch (fetchHeadsMode) {
125 String msg = getForcedHeadsFetchMessage();
127 myLogger.message(msg);
128 fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+refs/heads/*:refs/heads/*");
129 if (!myFullBranchName.startsWith("refs/heads/") && !hasRevision(bareRepositoryDir, myRevision))
130 fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+" + myFullBranchName + ":" + GitUtils.expandRef(myFullBranchName));
132 case BEFORE_BUILD_BRANCH:
133 fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+refs/heads/*:refs/heads/*");
134 if (!myFullBranchName.startsWith("refs/heads/") && !hasRevision(bareRepositoryDir, myRevision))
135 fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+" + myFullBranchName + ":" + GitUtils.expandRef(myFullBranchName));
137 case AFTER_BUILD_BRANCH:
138 fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+" + myFullBranchName + ":" + GitUtils.expandRef(myFullBranchName));
139 if (!hasRevision(bareRepositoryDir, myRevision))
140 fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+refs/heads/*:refs/heads/*");
143 throw new VcsException("Unknown FetchHeadsMode: " + fetchHeadsMode);
148 private boolean optimizeMirrorBeforeFetch() {
149 return "true".equals(myBuild.getSharedConfigParameters().get("teamcity.git.optimizeMirrorBeforeFetch"));
153 private void fetchMirror(boolean repeatFetchAttempt,
154 @NotNull File repositoryDir,
155 @NotNull String refspec) throws VcsException {
156 removeRefLocks(repositoryDir);
158 final int[] retryTimeouts = getRetryTimeouts();
159 for (int i = 0; i <= retryTimeouts.length; i++) {
161 fetch(repositoryDir, refspec, false);
163 } catch (GitExecTimeout e) {
165 } catch (VcsException e) {
166 if (!repeatFetchAttempt) throw e;
167 // Throw exception after latest attempt
168 if (i == retryTimeouts.length) throw e;
169 int wait = retryTimeouts[i];
170 LOG.warnAndDebugDetails("Failed to fetch mirror, will retry after " + wait + " seconds.", e);
172 Thread.sleep(wait * 1000);
173 } catch (InterruptedException e1) {
174 throw new VcsException("Failed to fetch mirror", e1);
178 } catch (VcsException e) {
179 if (myPluginConfig.isFailOnCleanCheckout() || !repeatFetchAttempt || !shouldFetchFromScratch(e))
181 LOG.warnAndDebugDetails("Failed to fetch mirror", e);
182 if (cleanDir(repositoryDir)) {
183 GitFacade git = myGitFactory.create(repositoryDir);
184 git.init().setBare(true).call();
185 configureRemoteUrl(repositoryDir);
186 fetch(repositoryDir, refspec, false);
188 LOG.info("Failed to delete repository " + repositoryDir + " after failed checkout, clone repository in another directory");
189 myMirrorManager.invalidate(repositoryDir);
190 updateLocalMirror(false);
196 private boolean shouldFetchFromScratch(@NotNull VcsException e) {
197 if (e instanceof GitExecTimeout)
199 String msg = e.getMessage();
200 if (msg.contains("Couldn't find remote ref") ||
201 msg.contains("Could not read from remote repository")) {
208 private boolean cleanDir(final @NotNull File repositoryDir) {
209 return myFS.delete(repositoryDir) && myFS.mkdirs(repositoryDir);
213 private boolean isValidGitRepo(@NotNull File gitDir) {
215 new RepositoryBuilder().setGitDir(gitDir).setMustExist(true).build();
217 } catch (IOException e) {
224 protected void setupExistingRepository() throws VcsException {
231 protected void setupNewRepository() throws VcsException {
237 protected void ensureCommitLoaded(boolean fetchRequired) throws VcsException {
238 if (myPluginConfig.isUseShallowClone()) {
239 File mirrorRepositoryDir = myRoot.getRepositoryDir();
240 if (GitUtils.isTag(myFullBranchName)) {
241 //handle tags specially: if we fetch a temporary branch which points to a commit
242 //tags points to, git fetches both branch and tag, tries to make a local
243 //branch to track both of them and fails.
244 String refspec = "+" + myFullBranchName + ":" + myFullBranchName;
245 fetch(myTargetDirectory, refspec, true);
247 String tmpBranchName = createTmpBranch(mirrorRepositoryDir, myRevision);
248 String tmpBranchRef = "refs/heads/" + tmpBranchName;
249 String refspec = "+" + tmpBranchRef + ":" + GitUtils.createRemoteRef(myFullBranchName);
250 fetch(myTargetDirectory, refspec, true);
251 myGitFactory.create(mirrorRepositoryDir).deleteBranch().setName(tmpBranchName).call();
254 super.ensureCommitLoaded(fetchRequired);
260 private String readRemoteUrl() throws VcsException {
261 Repository repository = null;
263 repository = new RepositoryBuilder().setWorkTree(myTargetDirectory).build();
264 return repository.getConfig().getString("remote", "origin", "url");
265 } catch (IOException e) {
266 throw new VcsException("Error while reading remote repository url", e);
268 if (repository != null)
274 private void setUseLocalMirror() throws VcsException {
275 //read remote url from config instead of VCS root, they can be different
276 //e.g. due to username exclusion from http(s) urls
277 String remoteUrl = readRemoteUrl();
278 String localMirrorUrl = getLocalMirrorUrl();
279 GitFacade git = myGitFactory.create(myTargetDirectory);
281 .setPropertyName("url." + localMirrorUrl + ".insteadOf")
285 .setPropertyName("url." + remoteUrl + ".pushInsteadOf")
290 private String getLocalMirrorUrl() throws VcsException {
292 return new URIish(myRoot.getRepositoryDir().toURI().toASCIIString()).toString();
293 } catch (URISyntaxException e) {
294 throw new VcsException("Cannot create uri for local mirror " + myRoot.getRepositoryDir().getAbsolutePath(), e);
298 private String createTmpBranch(@NotNull File repositoryDir, @NotNull String branchStartingPoint) throws VcsException {
299 String tmpBranchName = getUnusedBranchName(repositoryDir);
300 myGitFactory.create(repositoryDir)
302 .setName(tmpBranchName)
303 .setStartPoint(branchStartingPoint)
305 return tmpBranchName;
308 private String getUnusedBranchName(@NotNull File repositoryDir) {
309 final String tmpBranchName = "tmp_branch_for_build";
310 String branchName = tmpBranchName;
311 Map<String, Ref> existingRefs = myGitFactory.create(repositoryDir).showRef().call().getValidRefs();
313 while (existingRefs.containsKey("refs/heads/" + branchName)) {
314 branchName = tmpBranchName + i;
320 private int[] getRetryTimeouts() {
321 String value = myBuild.getSharedConfigParameters().get("teamcity.git.fetchMirrorRetryTimeouts");
322 if (value == null) return new int[]{5, 10, 15, 30}; // total 60 seconds
324 List<String> split = StringUtil.split(value, true, ',');
325 int[] result = new int[split.size()];
326 for (int i = 0; i < result.length; i++) {
329 parsed = Integer.parseInt(split.get(i));
330 } catch (NumberFormatException ignored) {