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