TW-56415 Try to update local git mirror several times instead of cloning from scratch
[teamcity/git-plugin.git] / git-agent / src / jetbrains / buildServer / buildTriggers / vcs / git / agent / UpdaterWithMirror.java
1 /*
2  * Copyright 2000-2018 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package jetbrains.buildServer.buildTriggers.vcs.git.agent;
18
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.util.ThreadUtil;
27 import jetbrains.buildServer.vcs.CheckoutRules;
28 import jetbrains.buildServer.vcs.VcsException;
29 import jetbrains.buildServer.vcs.VcsRoot;
30 import com.intellij.openapi.diagnostic.Logger;
31 import org.eclipse.jgit.lib.Ref;
32 import org.eclipse.jgit.lib.Repository;
33 import org.eclipse.jgit.lib.RepositoryBuilder;
34 import org.eclipse.jgit.transport.URIish;
35 import org.jetbrains.annotations.NotNull;
36
37 import java.io.File;
38 import java.io.IOException;
39 import java.net.URISyntaxException;
40 import java.util.List;
41 import java.util.Map;
42
43 /**
44  * @author dmitry.neverov
45  */
46 public class UpdaterWithMirror extends UpdaterImpl {
47
48   private final static Logger LOG = Logger.getInstance(UpdaterWithMirror.class.getName());
49
50   public UpdaterWithMirror(@NotNull FS fs,
51                            @NotNull AgentPluginConfig pluginConfig,
52                            @NotNull MirrorManager mirrorManager,
53                            @NotNull SmartDirectoryCleaner directoryCleaner,
54                            @NotNull GitFactory gitFactory,
55                            @NotNull AgentRunningBuild build,
56                            @NotNull VcsRoot root,
57                            @NotNull String version,
58                            @NotNull File targetDir,
59                            @NotNull CheckoutRules rules,
60                            @NotNull CheckoutMode mode) throws VcsException {
61     super(fs, pluginConfig, mirrorManager, directoryCleaner, gitFactory, build, root, version, targetDir, rules, mode);
62   }
63
64   @Override
65   protected void doUpdate() throws VcsException {
66     updateLocalMirror();
67     super.doUpdate();
68   }
69
70   private void updateLocalMirror() throws VcsException {
71     String message = "Update git mirror (" + myRoot.getRepositoryDir() + ")";
72     myLogger.activityStarted(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
73     try {
74       updateLocalMirror(true);
75       //prepare refs for copying into working dir repository
76       myGitFactory.create(myRoot.getRepositoryDir()).packRefs().call();
77     } finally {
78       myLogger.activityFinished(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
79     }
80   }
81
82   private void updateLocalMirror(boolean repeatFetchAttempt) throws VcsException {
83     File bareRepositoryDir = myRoot.getRepositoryDir();
84     String mirrorDescription = "local mirror of root " + myRoot.getName() + " at " + bareRepositoryDir;
85     LOG.info("Update " + mirrorDescription);
86     boolean fetchRequired = true;
87     if (isValidGitRepo(bareRepositoryDir)) {
88       removeOrphanedIdxFiles(bareRepositoryDir);
89     } else {
90       FileUtil.delete(bareRepositoryDir);
91     }
92     boolean newMirror = false;
93     if (!bareRepositoryDir.exists()) {
94       LOG.info("Init " + mirrorDescription);
95       bareRepositoryDir.mkdirs();
96       GitFacade git = myGitFactory.create(bareRepositoryDir);
97       git.init().setBare(true).call();
98       configureRemoteUrl(bareRepositoryDir);
99       newMirror = true;
100     } else {
101       configureRemoteUrl(bareRepositoryDir);
102       boolean outdatedTagsFound = removeOutdatedRefs(bareRepositoryDir);
103       if (!outdatedTagsFound) {
104         LOG.debug("Try to find revision " + myRevision + " in " + mirrorDescription);
105         Ref ref = getRef(bareRepositoryDir, GitUtils.createRemoteRef(myFullBranchName));
106         if (ref != null && myRevision.equals(ref.getObjectId().name())) {
107           LOG.info("No fetch required for revision '" + myRevision + "' in " + mirrorDescription);
108           fetchRequired = false;
109         }
110       }
111     }
112     FetchHeadsMode fetchHeadsMode = myPluginConfig.getFetchHeadsMode();
113     Ref ref = getRef(bareRepositoryDir, myFullBranchName);
114     if (ref == null)
115       fetchRequired = true;
116     if (!fetchRequired && fetchHeadsMode != FetchHeadsMode.ALWAYS)
117       return;
118     if (!newMirror && optimizeMirrorBeforeFetch()) {
119       GitFacade git = myGitFactory.create(bareRepositoryDir);
120       git.gc().call();
121       git.repack().call();
122     }
123
124     switch (fetchHeadsMode) {
125       case ALWAYS:
126         String msg = getForcedHeadsFetchMessage();
127         LOG.info(msg);
128         myLogger.message(msg);
129         fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+refs/heads/*:refs/heads/*");
130         if (!myFullBranchName.startsWith("refs/heads/") && !hasRevision(bareRepositoryDir, myRevision))
131           fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+" + myFullBranchName + ":" + GitUtils.expandRef(myFullBranchName));
132         break;
133       case BEFORE_BUILD_BRANCH:
134         fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+refs/heads/*:refs/heads/*");
135         if (!myFullBranchName.startsWith("refs/heads/") && !hasRevision(bareRepositoryDir, myRevision))
136           fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+" + myFullBranchName + ":" + GitUtils.expandRef(myFullBranchName));
137         break;
138       case AFTER_BUILD_BRANCH:
139         fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+" + myFullBranchName + ":" + GitUtils.expandRef(myFullBranchName));
140         if (!hasRevision(bareRepositoryDir, myRevision))
141           fetchMirror(repeatFetchAttempt, bareRepositoryDir, "+refs/heads/*:refs/heads/*");
142         break;
143       default:
144         throw new VcsException("Unknown FetchHeadsMode: " + fetchHeadsMode);
145     }
146   }
147
148
149   private boolean optimizeMirrorBeforeFetch() {
150     return "true".equals(myBuild.getSharedConfigParameters().get("teamcity.git.optimizeMirrorBeforeFetch"));
151   }
152
153
154   private void fetchMirror(boolean repeatFetchAttempt,
155                            @NotNull File repositoryDir,
156                            @NotNull String refspec) throws VcsException {
157     removeRefLocks(repositoryDir);
158     try {
159       final int[] retryTimeouts = getRetryTimeouts();
160       for (int i = 0; i <= retryTimeouts.length; i++) {
161         try {
162           fetch(repositoryDir, refspec, false);
163           break;
164         } catch (VcsException e) {
165           // Throw exception after latest attempt
166           if (i == retryTimeouts.length) throw e;
167           int wait = retryTimeouts[i];
168           LOG.warnAndDebugDetails("Failed to fetch mirror, will retry after " + wait + " seconds.", e);
169           ThreadUtil.sleep(wait * 1000);
170         }
171       }
172     } catch (VcsException e) {
173       if (myPluginConfig.isFailOnCleanCheckout() || !repeatFetchAttempt || !shouldFetchFromScratch(e))
174         throw e;
175       LOG.warnAndDebugDetails("Failed to fetch mirror", e);
176       if (cleanDir(repositoryDir)) {
177         GitFacade git = myGitFactory.create(repositoryDir);
178         git.init().setBare(true).call();
179         configureRemoteUrl(repositoryDir);
180         fetch(repositoryDir, refspec, false);
181       } else {
182         LOG.info("Failed to delete repository " + repositoryDir + " after failed checkout, clone repository in another directory");
183         myMirrorManager.invalidate(repositoryDir);
184         updateLocalMirror(false);
185       }
186     }
187   }
188
189
190   private boolean shouldFetchFromScratch(@NotNull VcsException e) {
191     if (e instanceof GitExecTimeout)
192       return false;
193     String msg = e.getMessage();
194     if (msg.contains("Couldn't find remote ref") ||
195         msg.contains("Could not read from remote repository")) {
196       return false;
197     }
198     return true;
199   }
200
201
202   private boolean cleanDir(final @NotNull File repositoryDir) {
203     return myFS.delete(repositoryDir) && myFS.mkdirs(repositoryDir);
204   }
205
206
207   private boolean isValidGitRepo(@NotNull File gitDir) {
208     try {
209       new RepositoryBuilder().setGitDir(gitDir).setMustExist(true).build();
210       return true;
211     } catch (IOException e) {
212       return false;
213     }
214   }
215
216
217   @Override
218   protected void setupExistingRepository() throws VcsException {
219     removeUrlSections();
220     setUseLocalMirror();
221     disableAlternates();
222   }
223
224   @Override
225   protected void setupNewRepository() throws VcsException {
226     setUseLocalMirror();
227     disableAlternates();
228   }
229
230   @Override
231   protected void ensureCommitLoaded(boolean fetchRequired) throws VcsException {
232     if (myPluginConfig.isUseShallowClone()) {
233       File mirrorRepositoryDir = myRoot.getRepositoryDir();
234       if (GitUtils.isTag(myFullBranchName)) {
235         //handle tags specially: if we fetch a temporary branch which points to a commit
236         //tags points to, git fetches both branch and tag, tries to make a local
237         //branch to track both of them and fails.
238         String refspec = "+" + myFullBranchName + ":" + myFullBranchName;
239         fetch(myTargetDirectory, refspec, true);
240       } else {
241         String tmpBranchName = createTmpBranch(mirrorRepositoryDir, myRevision);
242         String tmpBranchRef = "refs/heads/" + tmpBranchName;
243         String refspec = "+" + tmpBranchRef + ":" + GitUtils.createRemoteRef(myFullBranchName);
244         fetch(myTargetDirectory, refspec, true);
245         myGitFactory.create(mirrorRepositoryDir).deleteBranch().setName(tmpBranchName).call();
246       }
247     } else {
248       super.ensureCommitLoaded(fetchRequired);
249     }
250   }
251
252
253   @NotNull
254   private String readRemoteUrl() throws VcsException {
255     Repository repository = null;
256     try {
257       repository = new RepositoryBuilder().setWorkTree(myTargetDirectory).build();
258       return repository.getConfig().getString("remote", "origin", "url");
259     } catch (IOException e) {
260       throw new VcsException("Error while reading remote repository url", e);
261     } finally {
262       if (repository != null)
263         repository.close();
264     }
265   }
266
267
268   private void setUseLocalMirror() throws VcsException {
269     //read remote url from config instead of VCS root, they can be different
270     //e.g. due to username exclusion from http(s) urls
271     String remoteUrl = readRemoteUrl();
272     String localMirrorUrl = getLocalMirrorUrl();
273     GitFacade git = myGitFactory.create(myTargetDirectory);
274     git.setConfig()
275       .setPropertyName("url." + localMirrorUrl + ".insteadOf")
276       .setValue(remoteUrl)
277       .call();
278     git.setConfig()
279       .setPropertyName("url." + remoteUrl + ".pushInsteadOf")
280       .setValue(remoteUrl)
281       .call();
282   }
283
284   private String getLocalMirrorUrl() throws VcsException {
285     try {
286       return new URIish(myRoot.getRepositoryDir().toURI().toASCIIString()).toString();
287     } catch (URISyntaxException e) {
288       throw new VcsException("Cannot create uri for local mirror " + myRoot.getRepositoryDir().getAbsolutePath(), e);
289     }
290   }
291
292   private String createTmpBranch(@NotNull File repositoryDir, @NotNull String branchStartingPoint) throws VcsException {
293     String tmpBranchName = getUnusedBranchName(repositoryDir);
294     myGitFactory.create(repositoryDir)
295       .createBranch()
296       .setName(tmpBranchName)
297       .setStartPoint(branchStartingPoint)
298       .call();
299     return tmpBranchName;
300   }
301
302   private String getUnusedBranchName(@NotNull File repositoryDir) {
303     final String tmpBranchName = "tmp_branch_for_build";
304     String branchName = tmpBranchName;
305     Map<String, Ref> existingRefs = myGitFactory.create(repositoryDir).showRef().call().getValidRefs();
306     int i = 0;
307     while (existingRefs.containsKey("refs/heads/" + branchName)) {
308       branchName = tmpBranchName + i;
309       i++;
310     }
311     return branchName;
312   }
313
314   private int[] getRetryTimeouts() {
315     String value = myBuild.getSharedConfigParameters().get("teamcity.git.fetchMirrorRetryTimeouts");
316     if (value == null) return new int[]{5, 10, 15, 30}; // total 60 seconds
317
318     List<String> split = StringUtil.split(value, true, ',');
319     int[] result = new int[split.size()];
320     for (int i = 0; i < result.length; i++) {
321       int parsed = 1;
322       try {
323         parsed = Integer.parseInt(split.get(i));
324       } catch (NumberFormatException ignored) {
325       }
326       result[i] = parsed;
327     }
328     return result;
329   }
330 }