TEAMCITY-GIT-CR-7 TW-56415 Fix after review
[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.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;
35
36 import java.io.File;
37 import java.io.IOException;
38 import java.net.URISyntaxException;
39 import java.util.List;
40 import java.util.Map;
41
42 /**
43  * @author dmitry.neverov
44  */
45 public class UpdaterWithMirror extends UpdaterImpl {
46
47   private final static Logger LOG = Logger.getInstance(UpdaterWithMirror.class.getName());
48
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);
61   }
62
63   @Override
64   protected void doUpdate() throws VcsException {
65     updateLocalMirror();
66     super.doUpdate();
67   }
68
69   private void updateLocalMirror() throws VcsException {
70     String message = "Update git mirror (" + myRoot.getRepositoryDir() + ")";
71     myLogger.activityStarted(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
72     try {
73       updateLocalMirror(true);
74       //prepare refs for copying into working dir repository
75       myGitFactory.create(myRoot.getRepositoryDir()).packRefs().call();
76     } finally {
77       myLogger.activityFinished(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
78     }
79   }
80
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);
88     } else {
89       FileUtil.delete(bareRepositoryDir);
90     }
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);
98       newMirror = true;
99     } else {
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;
108         }
109       }
110     }
111     FetchHeadsMode fetchHeadsMode = myPluginConfig.getFetchHeadsMode();
112     Ref ref = getRef(bareRepositoryDir, myFullBranchName);
113     if (ref == null)
114       fetchRequired = true;
115     if (!fetchRequired && fetchHeadsMode != FetchHeadsMode.ALWAYS)
116       return;
117     if (!newMirror && optimizeMirrorBeforeFetch()) {
118       GitFacade git = myGitFactory.create(bareRepositoryDir);
119       git.gc().call();
120       git.repack().call();
121     }
122
123     switch (fetchHeadsMode) {
124       case ALWAYS:
125         String msg = getForcedHeadsFetchMessage();
126         LOG.info(msg);
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));
131         break;
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));
136         break;
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/*");
141         break;
142       default:
143         throw new VcsException("Unknown FetchHeadsMode: " + fetchHeadsMode);
144     }
145   }
146
147
148   private boolean optimizeMirrorBeforeFetch() {
149     return "true".equals(myBuild.getSharedConfigParameters().get("teamcity.git.optimizeMirrorBeforeFetch"));
150   }
151
152
153   private void fetchMirror(boolean repeatFetchAttempt,
154                            @NotNull File repositoryDir,
155                            @NotNull String refspec) throws VcsException {
156     removeRefLocks(repositoryDir);
157     try {
158       final int[] retryTimeouts = getRetryTimeouts();
159       for (int i = 0; i <= retryTimeouts.length; i++) {
160         try {
161           fetch(repositoryDir, refspec, false);
162           break;
163         } catch (GitExecTimeout e) {
164           throw 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);
171           try {
172             Thread.sleep(wait * 1000);
173           } catch (InterruptedException e1) {
174             throw new VcsException("Failed to fetch mirror", e1);
175           }
176         }
177       }
178     } catch (VcsException e) {
179       if (myPluginConfig.isFailOnCleanCheckout() || !repeatFetchAttempt || !shouldFetchFromScratch(e))
180         throw 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);
187       } else {
188         LOG.info("Failed to delete repository " + repositoryDir + " after failed checkout, clone repository in another directory");
189         myMirrorManager.invalidate(repositoryDir);
190         updateLocalMirror(false);
191       }
192     }
193   }
194
195
196   private boolean shouldFetchFromScratch(@NotNull VcsException e) {
197     if (e instanceof GitExecTimeout)
198       return false;
199     String msg = e.getMessage();
200     if (msg.contains("Couldn't find remote ref") ||
201         msg.contains("Could not read from remote repository")) {
202       return false;
203     }
204     return true;
205   }
206
207
208   private boolean cleanDir(final @NotNull File repositoryDir) {
209     return myFS.delete(repositoryDir) && myFS.mkdirs(repositoryDir);
210   }
211
212
213   private boolean isValidGitRepo(@NotNull File gitDir) {
214     try {
215       new RepositoryBuilder().setGitDir(gitDir).setMustExist(true).build();
216       return true;
217     } catch (IOException e) {
218       return false;
219     }
220   }
221
222
223   @Override
224   protected void setupExistingRepository() throws VcsException {
225     removeUrlSections();
226     setUseLocalMirror();
227     disableAlternates();
228   }
229
230   @Override
231   protected void setupNewRepository() throws VcsException {
232     setUseLocalMirror();
233     disableAlternates();
234   }
235
236   @Override
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);
246       } else {
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();
252       }
253     } else {
254       super.ensureCommitLoaded(fetchRequired);
255     }
256   }
257
258
259   @NotNull
260   private String readRemoteUrl() throws VcsException {
261     Repository repository = null;
262     try {
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);
267     } finally {
268       if (repository != null)
269         repository.close();
270     }
271   }
272
273
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);
280     git.setConfig()
281       .setPropertyName("url." + localMirrorUrl + ".insteadOf")
282       .setValue(remoteUrl)
283       .call();
284     git.setConfig()
285       .setPropertyName("url." + remoteUrl + ".pushInsteadOf")
286       .setValue(remoteUrl)
287       .call();
288   }
289
290   private String getLocalMirrorUrl() throws VcsException {
291     try {
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);
295     }
296   }
297
298   private String createTmpBranch(@NotNull File repositoryDir, @NotNull String branchStartingPoint) throws VcsException {
299     String tmpBranchName = getUnusedBranchName(repositoryDir);
300     myGitFactory.create(repositoryDir)
301       .createBranch()
302       .setName(tmpBranchName)
303       .setStartPoint(branchStartingPoint)
304       .call();
305     return tmpBranchName;
306   }
307
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();
312     int i = 0;
313     while (existingRefs.containsKey("refs/heads/" + branchName)) {
314       branchName = tmpBranchName + i;
315       i++;
316     }
317     return branchName;
318   }
319
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
323
324     List<String> split = StringUtil.split(value, true, ',');
325     int[] result = new int[split.size()];
326     for (int i = 0; i < result.length; i++) {
327       int parsed = 1;
328       try {
329         parsed = Integer.parseInt(split.get(i));
330       } catch (NumberFormatException ignored) {
331       }
332       result[i] = parsed;
333     }
334     return result;
335   }
336 }