Ability to fetch all heads only if build's commit is not found on the agent
[teamcity/git-plugin.git] / git-agent / src / jetbrains / buildServer / buildTriggers / vcs / git / agent / UpdaterImpl.java
1 /*
2  * Copyright 2000-2014 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.Trinity;
20 import jetbrains.buildServer.agent.AgentRunningBuild;
21 import jetbrains.buildServer.agent.BuildDirectoryCleanerCallback;
22 import jetbrains.buildServer.agent.BuildProgressLogger;
23 import jetbrains.buildServer.agent.SmartDirectoryCleaner;
24 import jetbrains.buildServer.buildTriggers.vcs.git.*;
25 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.*;
26 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.impl.CommandUtil;
27 import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.impl.RefImpl;
28 import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitExecTimeout;
29 import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitIndexCorruptedException;
30 import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitOutdatedIndexException;
31 import jetbrains.buildServer.log.Loggers;
32 import jetbrains.buildServer.util.FileUtil;
33 import jetbrains.buildServer.vcs.*;
34 import org.apache.log4j.Logger;
35 import org.eclipse.jgit.errors.ConfigInvalidException;
36 import org.eclipse.jgit.lib.*;
37 import org.eclipse.jgit.lib.Constants;
38 import org.eclipse.jgit.transport.URIish;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
41
42 import java.io.File;
43 import java.io.FileFilter;
44 import java.io.IOException;
45 import java.net.URISyntaxException;
46 import java.util.Collection;
47 import java.util.HashSet;
48 import java.util.Map;
49 import java.util.Set;
50 import java.util.regex.Matcher;
51
52 import static com.intellij.openapi.util.text.StringUtil.isEmpty;
53 import static jetbrains.buildServer.buildTriggers.vcs.git.GitUtils.*;
54
55 public class UpdaterImpl implements Updater {
56
57   private final static Logger LOG = Logger.getLogger(UpdaterImpl.class);
58   /** Git version which supports --progress option in the fetch command */
59   private final static GitVersion GIT_WITH_PROGRESS_VERSION = new GitVersion(1, 7, 1, 0);
60   //--force option in git submodule update introduced in 1.7.6
61   private final static GitVersion GIT_WITH_FORCE_SUBMODULE_UPDATE = new GitVersion(1, 7, 6);
62   public final static GitVersion GIT_WITH_SPARSE_CHECKOUT = new GitVersion(1, 7, 4);
63   public final static GitVersion BROKEN_SPARSE_CHECKOUT = new GitVersion(2, 7, 0);
64   /**
65    * Git version supporting an empty credential helper - the only way to disable system/global/local cred helper
66    */
67   public final static GitVersion EMPTY_CRED_HELPER = new GitVersion(2, 9, 0);
68   /** Git version supporting [credential] section in config (the first version including a6fc9fd3f4b42cd97b5262026e18bd451c28ee3c) */
69   public final static GitVersion CREDENTIALS_SECTION_VERSION = new GitVersion(1, 7, 10);
70
71   private static final int SILENT_TIMEOUT = 24 * 60 * 60; //24 hours
72
73   protected final FS myFS;
74   private final SmartDirectoryCleaner myDirectoryCleaner;
75   protected final BuildProgressLogger myLogger;
76   protected final AgentPluginConfig myPluginConfig;
77   protected final GitFactory myGitFactory;
78   protected final File myTargetDirectory;
79   protected final String myRevision;
80   protected final AgentGitVcsRoot myRoot;
81   protected final String myFullBranchName;
82   protected final AgentRunningBuild myBuild;
83   private final CheckoutRules myRules;
84   private final CheckoutMode myCheckoutMode;
85   protected final MirrorManager myMirrorManager;
86   //remote repository refs, stored in field in order to not run 'git ls-remote' command twice
87   private Refs myRemoteRefs;
88
89   public UpdaterImpl(@NotNull FS fs,
90                      @NotNull AgentPluginConfig pluginConfig,
91                      @NotNull MirrorManager mirrorManager,
92                      @NotNull SmartDirectoryCleaner directoryCleaner,
93                      @NotNull GitFactory gitFactory,
94                      @NotNull AgentRunningBuild build,
95                      @NotNull VcsRoot root,
96                      @NotNull String version,
97                      @NotNull File targetDir,
98                      @NotNull CheckoutRules rules,
99                      @NotNull CheckoutMode checkoutMode) throws VcsException {
100     myFS = fs;
101     myPluginConfig = pluginConfig;
102     myDirectoryCleaner = directoryCleaner;
103     myGitFactory = gitFactory;
104     myBuild = build;
105     myLogger = build.getBuildLogger();
106     myRevision = GitUtils.versionRevision(version);
107     myTargetDirectory = targetDir;
108     myRoot = new AgentGitVcsRoot(mirrorManager, myTargetDirectory, root);
109     myFullBranchName = getBranch();
110     myRules = rules;
111     myCheckoutMode = checkoutMode;
112     myMirrorManager = mirrorManager;
113   }
114
115
116   private String getBranch() {
117     String defaultBranchName = GitUtils.expandRef(myRoot.getRef());
118     String rootBranchParam = GitUtils.getGitRootBranchParamName(myRoot.getOriginalRoot());
119     String customBranch = myBuild.getSharedConfigParameters().get(rootBranchParam);
120     return customBranch != null ? customBranch : defaultBranchName;
121   }
122
123
124   public void update() throws VcsException {
125     String msg = "Git version: " + myPluginConfig.getGitVersion();
126     myLogger.message(msg);
127     LOG.info(msg);
128     checkAuthMethodIsSupported();
129     doUpdate();
130   }
131
132   protected void doUpdate() throws VcsException {
133     String message = "Update checkout directory (" + myTargetDirectory.getAbsolutePath() + ")";
134     myLogger.activityStarted(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
135     try {
136       logStartUpdating();
137       initGitRepository();
138       removeRefLocks(new File(myTargetDirectory, ".git"));
139       doFetch();
140       updateSources();
141     } finally {
142       myLogger.activityFinished(message, GitBuildProgressLogger.GIT_PROGRESS_ACTIVITY);
143     }
144   }
145
146   private void logStartUpdating() {
147     LOG.info("Starting update of root " + myRoot.getName() + " in " + myTargetDirectory + " to revision " + myRevision);
148     LOG.debug("Updating " + myRoot.debugInfo());
149   }
150
151
152   private void initGitRepository() throws VcsException {
153     if (!new File(myTargetDirectory, ".git").exists()) {
154       initDirectory();
155     } else {
156       try {
157         configureRemoteUrl(new File(myTargetDirectory, ".git"));
158         setupExistingRepository();
159         configureSparseCheckout();
160       } catch (Exception e) {
161         LOG.warn("Do clean checkout due to errors while configure use of local mirrors", e);
162         initDirectory();
163       }
164     }
165     removeOrphanedIdxFiles(new File(myTargetDirectory, ".git"));
166   }
167
168   protected void setupNewRepository() throws VcsException {
169   }
170
171
172   protected void setupExistingRepository() throws VcsException {
173     removeUrlSections();
174     removeLfsStorage();
175     disableAlternates();
176   }
177
178
179   private void updateSources() throws VcsException {
180     final GitFacade git = myGitFactory.create(myTargetDirectory);
181     boolean branchChanged = false;
182     removeIndexLock();
183     if (isRegularBranch(myFullBranchName)) {
184       String branchName = getShortBranchName(myFullBranchName);
185       Branches branches = git.listBranches();
186       if (branches.isCurrentBranch(branchName)) {
187         removeIndexLock();
188         runAndFixIndexErrors(git, new VcsCommand() {
189           @Override
190           public void call() throws VcsException {
191             reset(git).setHard(true).setRevision(myRevision).call();
192           }
193         });
194         git.setUpstream(branchName, GitUtils.createRemoteRef(myFullBranchName)).call();
195       } else {
196         branchChanged = true;
197         if (!branches.contains(branchName)) {
198           git.createBranch()
199             .setName(branchName)
200             .setStartPoint(GitUtils.createRemoteRef(myFullBranchName))
201             .setTrack(true)
202             .call();
203         }
204         git.updateRef().setRef(myFullBranchName).setRevision(myRevision).call();
205         final String finalBranchName = branchName;
206         runAndFixIndexErrors(git, new VcsCommand() {
207           @Override
208           public void call() throws VcsException {
209             checkout(git).setForce(true).setBranch(finalBranchName).setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
210           }
211         });
212         if (branches.contains(branchName)) {
213           git.setUpstream(branchName, GitUtils.createRemoteRef(myFullBranchName)).call();
214         }
215       }
216     } else if (isTag(myFullBranchName)) {
217       final String shortName = myFullBranchName.substring("refs/tags/".length());
218       runAndFixIndexErrors(git, new VcsCommand() {
219         @Override
220         public void call() throws VcsException {
221           checkout(git).setForce(true).setBranch(shortName).setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
222         }
223       });
224       Ref tag = getRef(myTargetDirectory, myFullBranchName);
225       if (tag != null && !tag.getObjectId().name().equals(myRevision)) {
226         runAndFixIndexErrors(git, new VcsCommand() {
227           @Override
228           public void call() throws VcsException {
229             checkout(git).setBranch(myRevision).setForce(true).setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
230           }
231         });
232       }
233       branchChanged = true;
234     } else {
235       runAndFixIndexErrors(git, new VcsCommand() {
236         @Override
237         public void call() throws VcsException {
238           checkout(git).setForce(true).setBranch(myRevision).setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
239         }
240       });
241       branchChanged = true;
242     }
243
244     doClean(branchChanged);
245     if (myRoot.isCheckoutSubmodules()) {
246       checkoutSubmodules(myTargetDirectory);
247     }
248   }
249
250
251   private void runAndFixIndexErrors(@NotNull GitFacade git, @NotNull VcsCommand cmd) throws VcsException {
252     try {
253       cmd.call();
254     } catch (GitIndexCorruptedException e) {
255       File gitIndex = e.getGitIndex();
256       myLogger.message("Git index '" + gitIndex.getAbsolutePath() + "' is corrupted, remove it and repeat the command");
257       FileUtil.delete(gitIndex);
258       cmd.call();
259     } catch (GitOutdatedIndexException e) {
260       myLogger.message("Refresh outdated git index and repeat the command");
261       updateIndex(git).reallyRefresh(true).quiet(true).call();
262       cmd.call();
263     } catch (Exception e) {
264       if (e instanceof VcsException)
265         throw (VcsException) e;
266       throw new VcsException(e);
267     }
268   }
269
270
271   @NotNull
272   private UpdateIndexCommand updateIndex(final GitFacade git) {
273     UpdateIndexCommand result = git.updateIndex()
274       .setAuthSettings(myRoot.getAuthSettings())
275       .setUseNativeSsh(myPluginConfig.isUseNativeSSH());
276     configureLFS(result);
277     return result;
278   }
279
280
281   @NotNull
282   private ResetCommand reset(final GitFacade git) {
283     ResetCommand result = git.reset()
284       .setAuthSettings(myRoot.getAuthSettings())
285       .setUseNativeSsh(myPluginConfig.isUseNativeSSH());
286     configureLFS(result);
287     return result;
288   }
289
290   @NotNull
291   private CheckoutCommand checkout(final GitFacade git) {
292     CheckoutCommand result = git.checkout()
293       .setAuthSettings(myRoot.getAuthSettings())
294       .setUseNativeSsh(myPluginConfig.isUseNativeSSH());
295     configureLFS(result);
296     return result;
297   }
298
299   private void checkoutSubmodules(@NotNull final File repositoryDir) throws VcsException {
300     File dotGitModules = new File(repositoryDir, ".gitmodules");
301     try {
302       Config gitModules = readGitModules(dotGitModules);
303       if (gitModules == null)
304         return;
305
306       myLogger.message("Checkout submodules in " + repositoryDir);
307       GitFacade git = myGitFactory.create(repositoryDir);
308       git.submoduleInit().call();
309       git.submoduleSync().call();
310
311       addSubmoduleUsernames(repositoryDir, gitModules);
312
313       long start = System.currentTimeMillis();
314       SubmoduleUpdateCommand submoduleUpdate = git.submoduleUpdate()
315         .setAuthSettings(myRoot.getAuthSettings())
316         .setUseNativeSsh(myPluginConfig.isUseNativeSSH())
317         .setTimeout(SILENT_TIMEOUT)
318         .setForce(isForceUpdateSupported());
319       configureLFS(submoduleUpdate);
320       submoduleUpdate.call();
321
322       if (recursiveSubmoduleCheckout()) {
323         for (String submodulePath : getSubmodulePaths(gitModules)) {
324           checkoutSubmodules(new File(repositoryDir, submodulePath));
325         }
326       }
327       Loggers.VCS.info("Submodules update in " + repositoryDir.getAbsolutePath() + " is finished in " +
328                        (System.currentTimeMillis() - start) + " ms");
329
330     } catch (IOException e) {
331       Loggers.VCS.error("Submodules checkout failed", e);
332       throw new VcsException("Submodules checkout failed", e);
333     } catch (ConfigInvalidException e) {
334       Loggers.VCS.error("Submodules checkout failed", e);
335       throw new VcsException("Submodules checkout failed", e);
336     }
337   }
338
339
340   private boolean isForceUpdateSupported() {
341     return !GIT_WITH_FORCE_SUBMODULE_UPDATE.isGreaterThan(myPluginConfig.getGitVersion());
342   }
343
344
345   private void addSubmoduleUsernames(@NotNull File repositoryDir, @NotNull Config gitModules)
346     throws IOException, ConfigInvalidException, VcsException {
347     if (!myPluginConfig.isUseMainRepoUserForSubmodules())
348       return;
349
350     Loggers.VCS.info("Update submodules credentials");
351
352     AuthSettings auth = myRoot.getAuthSettings();
353     final String userName = auth.getUserName();
354     if (userName == null) {
355       Loggers.VCS.info("Username is not specified in the main VCS root settings, skip updating credentials");
356       return;
357     }
358
359     Repository r = new RepositoryBuilder().setBare().setGitDir(getGitDir(repositoryDir)).build();
360     try {
361       StoredConfig gitConfig = r.getConfig();
362
363       Set<String> submodules = gitModules.getSubsections("submodule");
364       if (submodules.isEmpty()) {
365         Loggers.VCS.info("No submodule sections found in " + new File(repositoryDir, ".gitmodules").getCanonicalPath()
366                          + ", skip updating credentials");
367         return;
368       }
369       File modulesDir = new File(r.getDirectory(), Constants.MODULES);
370       for (String submoduleName : submodules) {
371         //The 'git submodule sync' command executed before resolves relative submodule urls
372         //from .gitmodules and writes them into .git/config. We should use resolved urls in
373         //order to add parent repository username to submodules with relative urls.
374         String url = gitConfig.getString("submodule", submoduleName, "url");
375         if (url == null) {
376           Loggers.VCS.info(".git/config doesn't contain an url for submodule '" + submoduleName + "', use url from .gitmodules");
377           url = gitModules.getString("submodule", submoduleName, "url");
378         }
379         Loggers.VCS.info("Update credentials for submodule with url " + url);
380         if (url == null || !isRequireAuth(url)) {
381           Loggers.VCS.info("Url " + url + " does not require authentication, skip updating credentials");
382           continue;
383         }
384         try {
385           URIish uri = new URIish(url);
386           String updatedUrl = uri.setUser(userName).toASCIIString();
387           gitConfig.setString("submodule", submoduleName, "url", updatedUrl);
388           String submodulePath = gitModules.getString("submodule", submoduleName, "path");
389           if (submodulePath != null && myPluginConfig.isUpdateSubmoduleOriginUrl()) {
390             File submoduleDir = new File(modulesDir, submodulePath);
391             if (submoduleDir.isDirectory() && new File(submoduleDir, Constants.CONFIG).isFile())
392               updateOriginUrl(submoduleDir, updatedUrl);
393           }
394           Loggers.VCS.debug("Submodule url " + url + " changed to " + updatedUrl);
395         } catch (URISyntaxException e) {
396           Loggers.VCS.warn("Error while parsing an url " + url + ", skip updating submodule credentials", e);
397         } catch (Exception e) {
398           Loggers.VCS.warn("Error while updating the '" + submoduleName + "' submodule url", e);
399         }
400       }
401       gitConfig.save();
402     } finally {
403       r.close();
404     }
405   }
406
407   private void updateOriginUrl(@NotNull File repoDir, @NotNull String url) throws IOException {
408     Repository r = new RepositoryBuilder().setBare().setGitDir(repoDir).build();
409     StoredConfig config = r.getConfig();
410     config.setString("remote", "origin", "url", url);
411     config.save();
412   }
413
414
415   @Nullable
416   private Config readGitModules(@NotNull File dotGitModules) throws IOException, ConfigInvalidException {
417     if (!dotGitModules.exists())
418       return null;
419     String content = FileUtil.readText(dotGitModules);
420     Config config = new Config();
421     config.fromText(content);
422     return config;
423   }
424
425
426   private boolean isRequireAuth(@NotNull String url) {
427     try {
428       URIish uri = new URIish(url);
429       String scheme = uri.getScheme();
430       if (scheme == null || "git".equals(scheme)) //no auth for anonymous protocol and for local repositories
431         return false;
432       String user = uri.getUser();
433       if (user != null) //respect a user specified in config
434         return false;
435       return true;
436     } catch (URISyntaxException e) {
437       return false;
438     }
439   }
440
441
442   private Set<String> getSubmodulePaths(@NotNull Config config) {
443     Set<String> paths = new HashSet<String>();
444     Set<String> submodules = config.getSubsections("submodule");
445     for (String submoduleName : submodules) {
446       String submodulePath = config.getString("submodule", submoduleName, "path");
447       paths.add(submodulePath.replaceAll("/", Matcher.quoteReplacement(File.separator)));
448     }
449     return paths;
450   }
451
452   private boolean recursiveSubmoduleCheckout() {
453     return SubmodulesCheckoutPolicy.CHECKOUT.equals(myRoot.getSubmodulesCheckoutPolicy()) ||
454            SubmodulesCheckoutPolicy.CHECKOUT_IGNORING_ERRORS.equals(myRoot.getSubmodulesCheckoutPolicy());
455   }
456
457
458   private void doClean(boolean branchChanged) throws VcsException {
459     if (myRoot.getCleanPolicy() == AgentCleanPolicy.ALWAYS ||
460         branchChanged && myRoot.getCleanPolicy() == AgentCleanPolicy.ON_BRANCH_CHANGE) {
461       myLogger.message("Cleaning " + myRoot.getName() + " in " + myTargetDirectory + " the file set " + myRoot.getCleanFilesPolicy());
462       myGitFactory.create(myTargetDirectory).clean().setCleanPolicy(myRoot.getCleanFilesPolicy()).call();
463
464       if (myRoot.isCheckoutSubmodules())
465         cleanSubmodules(myTargetDirectory);
466     }
467   }
468
469
470   private void cleanSubmodules(@NotNull File repositoryDir) throws VcsException {
471     File dotGitModules = new File(repositoryDir, ".gitmodules");
472     Config gitModules;
473     try {
474       gitModules = readGitModules(dotGitModules);
475     } catch (Exception e) {
476       Loggers.VCS.error("Error while reading " + dotGitModules.getAbsolutePath() + ": " + e.getMessage());
477       throw new VcsException("Error while reading " + dotGitModules.getAbsolutePath(), e);
478     }
479
480     if (gitModules == null)
481       return;
482
483     for (String submodulePath : getSubmodulePaths(gitModules)) {
484       File submoduleDir = new File(repositoryDir, submodulePath);
485       try {
486         myLogger.message("Cleaning files in " + submoduleDir + " the file set " + myRoot.getCleanFilesPolicy());
487         myGitFactory.create(submoduleDir).clean().setCleanPolicy(myRoot.getCleanFilesPolicy()).call();
488       } catch (Exception e) {
489         Loggers.VCS.error("Error while cleaning files in " + submoduleDir.getAbsolutePath(), e);
490       }
491       if (recursiveSubmoduleCheckout())
492         cleanSubmodules(submoduleDir);
493     }
494   }
495
496
497   protected void removeUrlSections() throws VcsException {
498     Repository r = null;
499     try {
500       r = new RepositoryBuilder().setWorkTree(myTargetDirectory).build();
501       StoredConfig config = r.getConfig();
502       Set<String> urlSubsections = config.getSubsections("url");
503       for (String subsection : urlSubsections) {
504         config.unsetSection("url", subsection);
505       }
506       config.save();
507     } catch (IOException e) {
508       String msg = "Error while remove url.* sections";
509       LOG.error(msg, e);
510       throw new VcsException(msg, e);
511     } finally {
512       if (r != null)
513         r.close();
514     }
515   }
516
517
518   private void removeLfsStorage() throws VcsException {
519     Repository r = null;
520     try {
521       r = new RepositoryBuilder().setWorkTree(myTargetDirectory).build();
522       StoredConfig config = r.getConfig();
523       config.unsetSection("lfs", null);
524       config.save();
525     } catch (IOException e) {
526       String msg = "Error while removing lfs.storage section";
527       LOG.error(msg, e);
528       throw new VcsException(msg, e);
529     } finally {
530       if (r != null)
531         r.close();
532     }
533   }
534
535
536   protected void disableAlternates() {
537     FileUtil.delete(new File(myTargetDirectory, ".git" + File.separator + "objects" + File.separator + "info" + File.separator + "alternates"));
538   }
539
540
541   private String getRemoteUrl() {
542     try {
543       return myGitFactory.create(myTargetDirectory).getConfig().setPropertyName("remote.origin.url").call();
544     } catch (VcsException e) {
545       LOG.debug("Failed to read property", e);
546       return "";
547     }
548   }
549
550
551   @Nullable
552   protected Ref getRef(@NotNull File repositoryDir, @NotNull String ref) {
553     Map<String, Ref> refs = myGitFactory.create(repositoryDir).showRef().setPattern(ref).call().getValidRefs();
554     return refs.isEmpty() ? null : refs.get(ref);
555   }
556
557
558   /**
559    * If some git process crashed in this repository earlier it can leave lock files for index.
560    * This method delete such lock file if it exists (with warning message), otherwise git operation will fail.
561    */
562   private void removeIndexLock() {
563     File indexLock = new File(myTargetDirectory, ".git" + File.separator + "index.lock");
564     if (indexLock.exists()) {
565       myLogger.warning("The .git/index.lock file exists. This probably means a git process crashed in this repository earlier. Deleting lock file");
566       FileUtil.delete(indexLock);
567     }
568   }
569
570
571   private void doFetch() throws VcsException {
572     boolean outdatedRefsFound = removeOutdatedRefs(myTargetDirectory);
573     ensureCommitLoaded(outdatedRefsFound);
574   }
575
576
577   protected void ensureCommitLoaded(boolean fetchRequired) throws VcsException {
578     fetchFromOriginalRepository(fetchRequired);
579   }
580
581
582   protected void fetchFromOriginalRepository(boolean fetchRequired) throws VcsException {
583     Ref remoteRef;
584     FetchHeadsMode fetchHeadsMode = myPluginConfig.getFetchHeadsMode();
585     switch (fetchHeadsMode) {
586       case ALWAYS:
587         String msg = getForcedHeadsFetchMessage();
588         LOG.info(msg);
589         myLogger.message(msg);
590
591         fetchAllBranches();
592         if (!myFullBranchName.startsWith("refs/heads/")) {
593           remoteRef = getRef(myTargetDirectory, GitUtils.createRemoteRef(myFullBranchName));
594           if (fetchRequired || remoteRef == null || !myRevision.equals(remoteRef.getObjectId().name()) || !hasRevision(myTargetDirectory, myRevision))
595             fetchDefaultBranch();
596         }
597         break;
598       case BEFORE_BUILD_BRANCH:
599         remoteRef = getRef(myTargetDirectory, GitUtils.createRemoteRef(myFullBranchName));
600         if (!fetchRequired && remoteRef != null && myRevision.equals(remoteRef.getObjectId().name()) && hasRevision(myTargetDirectory, myRevision))
601           return;
602         myLogger.message("Commit '" + myRevision + "' is not found in local clone. Running 'git fetch'...");
603         fetchAllBranches();
604         if (!myFullBranchName.startsWith("refs/heads/")) {
605           remoteRef = getRef(myTargetDirectory, GitUtils.createRemoteRef(myFullBranchName));
606           if (fetchRequired || remoteRef == null || !myRevision.equals(remoteRef.getObjectId().name()) || !hasRevision(myTargetDirectory, myRevision))
607             fetchDefaultBranch();
608         }
609         break;
610       case AFTER_BUILD_BRANCH:
611         remoteRef = getRef(myTargetDirectory, GitUtils.createRemoteRef(myFullBranchName));
612         if (!fetchRequired && remoteRef != null && myRevision.equals(remoteRef.getObjectId().name()) && hasRevision(myTargetDirectory, myRevision))
613           return;
614         myLogger.message("Commit '" + myRevision + "' is not found in local clone. Running 'git fetch'...");
615         fetchDefaultBranch();
616         if (hasRevision(myTargetDirectory, myRevision))
617           return;
618         myLogger.message("Commit still not found after fetching main branch. Fetching more branches.");
619         fetchAllBranches();
620         break;
621       default:
622         throw new VcsException("Unknown FetchHeadsMode: " + fetchHeadsMode);
623     }
624
625     if (hasRevision(myTargetDirectory, myRevision))
626       return;
627
628     String msg = "Cannot find commit " + myRevision + " in the " + myRoot.getRepositoryFetchURL().toASCIIString() + " repository, " +
629                  "possible reason: " + myFullBranchName + " branch was updated and the commit selected for the build is not reachable anymore";
630     throw new RevisionNotFoundException(msg);
631   }
632
633
634   protected String getForcedHeadsFetchMessage() {
635     return "Forced fetch of all heads (" + PluginConfigImpl.FETCH_ALL_HEADS + "=" + myBuild.getSharedConfigParameters().get(PluginConfigImpl.FETCH_ALL_HEADS) + ")";
636   }
637
638
639   private void fetchDefaultBranch() throws VcsException {
640     fetch(myTargetDirectory, getRefspecForFetch(), false);
641   }
642
643   private String getRefspecForFetch() {
644     return "+" + myFullBranchName + ":" + GitUtils.createRemoteRef(myFullBranchName);
645   }
646
647   private void fetchAllBranches() throws VcsException {
648     fetch(myTargetDirectory, "+refs/heads/*:refs/remotes/origin/*", false);
649   }
650
651   protected boolean hasRevision(@NotNull File repositoryDir, @NotNull String revision) {
652     return getRevision(repositoryDir, revision) != null;
653   }
654
655   private String getRevision(@NotNull File repositoryDir, @NotNull String revision) {
656     return myGitFactory.create(repositoryDir).log()
657       .setCommitsNumber(1)
658       .setPrettyFormat("%H%x20%s")
659       .setStartPoint(revision)
660       .call();
661   }
662
663   protected void fetch(@NotNull File repositoryDir, @NotNull String refspec, boolean shallowClone) throws VcsException {
664     boolean silent = isSilentFetch();
665     int timeout = getTimeout(silent);
666
667     try {
668       getFetch(repositoryDir, refspec, shallowClone, silent, timeout).call();
669     } catch (GitIndexCorruptedException e) {
670       File gitIndex = e.getGitIndex();
671       myLogger.message("Git index '" + gitIndex.getAbsolutePath() + "' is corrupted, remove it and repeat git fetch");
672       FileUtil.delete(gitIndex);
673       getFetch(repositoryDir, refspec, shallowClone, silent, timeout).call();
674     } catch (GitExecTimeout e) {
675       if (!silent) {
676         myLogger.error("No output from git during " + timeout + " seconds. Try increasing idle timeout by setting parameter '"
677                        + PluginConfigImpl.IDLE_TIMEOUT +
678                        "' either in build or in agent configuration.");
679       }
680       throw e;
681     }
682   }
683
684   @NotNull
685   private FetchCommand getFetch(@NotNull File repositoryDir, @NotNull String refspec, boolean shallowClone, boolean silent, int timeout) {
686     FetchCommand result = myGitFactory.create(repositoryDir).fetch()
687       .setAuthSettings(myRoot.getAuthSettings())
688       .setUseNativeSsh(myPluginConfig.isUseNativeSSH())
689       .setTimeout(timeout)
690       .setRefspec(refspec)
691       .setFetchTags(myPluginConfig.isFetchTags());
692
693     if (silent)
694       result.setQuite(true);
695     else
696       result.setShowProgress(true);
697
698     if (shallowClone)
699       result.setDepth(1);
700
701     return result;
702   }
703
704   protected void removeRefLocks(@NotNull File dotGit) {
705     File refs = new File(dotGit, "refs");
706     if (!refs.isDirectory())
707       return;
708     Collection<File> locks = FileUtil.findFiles(new FileFilter() {
709       public boolean accept(File f) {
710         return f.isFile() && f.getName().endsWith(".lock");
711       }
712     }, refs);
713     for (File lock : locks) {
714       LOG.info("Remove a lock file " + lock.getAbsolutePath());
715       FileUtil.delete(lock);
716     }
717   }
718
719   private boolean isSilentFetch() {
720     GitVersion version = myPluginConfig.getGitVersion();
721     return version.isLessThan(GIT_WITH_PROGRESS_VERSION);
722   }
723
724   private int getTimeout(boolean silentFetch) {
725     if (silentFetch)
726       return SILENT_TIMEOUT;
727     else
728       return myPluginConfig.getIdleTimeoutSeconds();
729   }
730
731
732   private void checkAuthMethodIsSupported() throws VcsException {
733     checkAuthMethodIsSupported(myRoot, myPluginConfig);
734   }
735
736
737   static void checkAuthMethodIsSupported(@NotNull GitVcsRoot root, @NotNull AgentPluginConfig config) throws VcsException {
738     if ("git".equals(root.getRepositoryFetchURL().getScheme()))
739       return;//anonymous protocol, don't check anything
740     AuthSettings authSettings = root.getAuthSettings();
741     switch (authSettings.getAuthMethod()) {
742       case PASSWORD:
743         if ("http".equals(root.getRepositoryFetchURL().getScheme()) ||
744             "https".equals(root.getRepositoryFetchURL().getScheme())) {
745           GitVersion actualVersion = config.getGitVersion();
746           GitVersion requiredVersion = getMinVersionForHttpAuth();
747           if (actualVersion.isLessThan(requiredVersion)) {
748             throw new VcsException("Password authentication requires git " + requiredVersion +
749                                    ", found git version is " + actualVersion +
750                                    ". Upgrade git or use different authentication method.");
751           }
752         } else {
753           throw new VcsException("TeamCity doesn't support authentication method '" +
754                                  root.getAuthSettings().getAuthMethod().uiName() +
755                                  "' with agent checkout and non-http protocols. Please use different authentication method.");
756         }
757         break;
758       case PRIVATE_KEY_FILE:
759         throw new VcsException("TeamCity doesn't support authentication method '" +
760                                root.getAuthSettings().getAuthMethod().uiName() +
761                                "' with agent checkout. Please use different authentication method.");
762     }
763   }
764
765   @NotNull
766   private static GitVersion getMinVersionForHttpAuth() {
767     //core.askpass parameter was added in 1.7.1, but
768     //experiments show that it works only in 1.7.3 on linux
769     //and msysgit 1.7.3.1-preview20101002.
770     return new GitVersion(1, 7, 3);
771   }
772
773   /**
774    * Clean and init directory and configure remote origin
775    *
776    * @throws VcsException if there are problems with initializing the directory
777    */
778   private void initDirectory() throws VcsException {
779     BuildDirectoryCleanerCallback c = new BuildDirectoryCleanerCallback(myLogger, LOG);
780     myDirectoryCleaner.cleanFolder(myTargetDirectory, c);
781     //noinspection ResultOfMethodCallIgnored
782     myTargetDirectory.mkdirs();
783     if (c.isHasErrors()) {
784       throw new VcsException("Unable to clean directory " + myTargetDirectory + " for VCS root " + myRoot.getName());
785     }
786     myLogger.message("The .git directory is missing in '" + myTargetDirectory + "'. Running 'git init'...");
787     myGitFactory.create(myTargetDirectory).init().call();
788     validateUrls();
789     configureRemoteUrl(new File(myTargetDirectory, ".git"));
790
791     URIish fetchUrl = myRoot.getRepositoryFetchURL();
792     URIish url = myRoot.getRepositoryPushURL();
793     String pushUrl = url == null ? null : url.toString();
794     if (pushUrl != null && !pushUrl.equals(fetchUrl.toString())) {
795       myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("remote.origin.pushurl").setValue(pushUrl).call();
796     }
797     setupNewRepository();
798     configureSparseCheckout();
799   }
800
801
802   void configureRemoteUrl(@NotNull File gitDir) throws VcsException {
803     RemoteRepositoryConfigurator cfg = new RemoteRepositoryConfigurator();
804     cfg.setGitDir(gitDir);
805     cfg.setExcludeUsernameFromHttpUrls(myPluginConfig.isExcludeUsernameFromHttpUrl() && !myPluginConfig.getGitVersion().isLessThan(UpdaterImpl.CREDENTIALS_SECTION_VERSION));
806     cfg.configure(myRoot);
807   }
808
809
810   private void configureSparseCheckout() throws VcsException {
811     if (myCheckoutMode == CheckoutMode.SPARSE_CHECKOUT) {
812       setupSparseCheckout();
813     } else {
814       myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("core.sparseCheckout").setValue("false").call();
815     }
816   }
817
818   private void setupSparseCheckout() throws VcsException {
819     myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("core.sparseCheckout").setValue("true").call();
820     File sparseCheckout = new File(myTargetDirectory, ".git/info/sparse-checkout");
821     boolean hasIncludeRules = false;
822     StringBuilder sparseCheckoutContent = new StringBuilder();
823     for (IncludeRule rule : myRules.getIncludeRules()) {
824       if (isEmpty(rule.getFrom())) {
825         sparseCheckoutContent.append("/*\n");
826       } else {
827         sparseCheckoutContent.append("/").append(rule.getFrom()).append("\n");
828       }
829       hasIncludeRules = true;
830     }
831     if (!hasIncludeRules) {
832       sparseCheckoutContent.append("/*\n");
833     }
834     for (FileRule rule : myRules.getExcludeRules()) {
835       sparseCheckoutContent.append("!/").append(rule.getFrom()).append("\n");
836     }
837     try {
838       FileUtil.writeFileAndReportErrors(sparseCheckout, sparseCheckoutContent.toString());
839     } catch (IOException e) {
840       LOG.warn("Error while writing sparse checkout config, disable sparse checkout", e);
841       myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("core.sparseCheckout").setValue("false").call();
842     }
843   }
844
845
846   private void validateUrls() {
847     URIish fetch = myRoot.getRepositoryFetchURL();
848     if (isAnonymousGitWithUsername(fetch))
849       LOG.warn("Fetch URL '" + fetch.toString() + "' for root " + myRoot.getName() + " uses an anonymous git protocol and contains a username, fetch will probably fail");
850     URIish push  = myRoot.getRepositoryPushURL();
851     if (!fetch.equals(push) && isAnonymousGitWithUsername(push))
852       LOG.warn("Push URL '" + push.toString() + "'for root " + myRoot.getName() + " uses an anonymous git protocol and contains a username, push will probably fail");
853   }
854
855
856   protected boolean removeOutdatedRefs(@NotNull File workingDir) throws VcsException {
857     boolean outdatedRefsRemoved = false;
858     GitFacade git = myGitFactory.create(workingDir);
859     ShowRefResult showRefResult = git.showRef().call();
860     Refs localRefs = new Refs(showRefResult.getValidRefs());
861     if (localRefs.isEmpty() && showRefResult.getInvalidRefs().isEmpty())
862       return false;
863     for (String invalidRef : showRefResult.getInvalidRefs()) {
864       git.updateRef().setRef(invalidRef).delete().call();
865       outdatedRefsRemoved = true;
866     }
867     final Refs remoteRefs;
868     try {
869       remoteRefs = getRemoteRefs(workingDir);
870     } catch (VcsException e) {
871       if (CommandUtil.isCanceledError(e))
872         throw e;
873       String msg = "Failed to list remote repository refs, outdated local refs will not be cleaned";
874       LOG.warn(msg);
875       myLogger.warning(msg);
876       return false;
877     }
878     //We remove both outdated local refs (e.g. refs/heads/topic) and outdated remote
879     //tracking branches (refs/remote/origin/topic), while git remote origin prune
880     //removes only the latter. We need that because in some cases git cannot handle
881     //rename of the branch (TW-28735).
882     for (Ref localRef : localRefs.list()) {
883       Ref correspondingRemoteRef = createCorrespondingRemoteRef(localRef);
884       if (remoteRefs.isOutdated(correspondingRemoteRef)) {
885         git.updateRef().setRef(localRef.getName()).delete().call();
886         outdatedRefsRemoved = true;
887       }
888     }
889     return outdatedRefsRemoved;
890   }
891
892
893   @NotNull
894   private Refs getRemoteRefs(@NotNull File workingDir) throws VcsException {
895     if (myRemoteRefs != null)
896       return myRemoteRefs;
897     GitFacade git = myGitFactory.create(workingDir);
898     myRemoteRefs = new Refs(git.lsRemote().setAuthSettings(myRoot.getAuthSettings())
899       .setUseNativeSsh(myPluginConfig.isUseNativeSSH())
900       .call());
901     return myRemoteRefs;
902   }
903
904
905   private boolean isRemoteTrackingBranch(@NotNull Ref localRef) {
906     return localRef.getName().startsWith("refs/remotes/origin");
907   }
908
909   @NotNull
910   private Ref createCorrespondingRemoteRef(@NotNull Ref localRef) {
911     if (!isRemoteTrackingBranch(localRef))
912       return localRef;
913     return new RefImpl("refs/heads" + localRef.getName().substring("refs/remotes/origin".length()),
914                        localRef.getObjectId().name());
915   }
916
917
918   private void configureLFS(@NotNull BaseCommand command) {
919     if (!myPluginConfig.isProvideCredHelper())
920       return;
921     Trinity<String, String, String> lfsAuth = getLfsAuth();
922     if (lfsAuth == null)
923       return;
924     File credentialsHelper = null;
925     try {
926       ScriptGen scriptGen = myGitFactory.create(new File(".")).getScriptGen();
927       final File credHelper = scriptGen.generateCredentialsHelper();
928       credentialsHelper = credHelper;
929       if (!myPluginConfig.getGitVersion().isLessThan(UpdaterImpl.EMPTY_CRED_HELPER)) {
930         //Specify an empty helper if it is supported in order to disable
931         //helpers in system-global-local chain. If empty helper is not supported,
932         //then the only workaround is to disable helpers manually in config files.
933         command.addConfig("credential.helper", "");
934       }
935       String path = credHelper.getCanonicalPath();
936       path = path.replaceAll("\\\\", "/");
937       command.addConfig("credential.helper", path);
938       CredentialsHelperConfig config = new CredentialsHelperConfig();
939       config.addCredentials(lfsAuth.first, lfsAuth.second, lfsAuth.third);
940       config.setMatchAllUrls(myPluginConfig.isCredHelperMatchesAllUrls());
941       for (Map.Entry<String, String> e : config.getEnv().entrySet()) {
942         command.setEnv(e.getKey(), e.getValue());
943       }
944       if (myPluginConfig.isCleanCredHelperScript()) {
945         command.addPostAction(new Runnable() {
946           @Override
947           public void run() {
948             FileUtil.delete(credHelper);
949           }
950         });
951       }
952     } catch (Exception e) {
953       if (credentialsHelper != null)
954         FileUtil.delete(credentialsHelper);
955     }
956   }
957
958
959   //returns (url, name, pass) for lfs or null if no authentication is required or
960   //root doesn't use http(s)
961   @Nullable
962   private Trinity<String, String, String> getLfsAuth() {
963     try {
964       URIish uri = new URIish(myRoot.getRepositoryFetchURL().toString());
965       String scheme = uri.getScheme();
966       if (myRoot.getAuthSettings().getAuthMethod() == AuthenticationMethod.PASSWORD &&
967           ("http".equals(scheme) || "https".equals(scheme))) {
968         String lfsUrl = uri.setPass("").setUser("").toASCIIString();
969         if (lfsUrl.endsWith(".git")) {
970           lfsUrl += "/info/lfs";
971         } else {
972           lfsUrl += lfsUrl.endsWith("/") ? ".git/info/lfs" : "/.git/info/lfs";
973         }
974         return Trinity.create(lfsUrl, myRoot.getAuthSettings().getUserName(), myRoot.getAuthSettings().getPassword());
975       }
976     } catch (Exception e) {
977       LOG.debug("Cannot get lfs auth config", e);
978     }
979     return null;
980   }
981
982
983   private interface VcsCommand {
984     void call() throws VcsException;
985   }
986
987
988   /**
989    * Removes .idx files which don't have a corresponding .pack file
990    * @param ditGitDir git dir
991    */
992   void removeOrphanedIdxFiles(@NotNull File ditGitDir) {
993     if ("false".equals(myBuild.getSharedConfigParameters().get("teamcity.git.removeOrphanedIdxFiles"))) {
994       //looks like this logic is always needed, if no problems will be reported we can drop the option
995       return;
996     }
997     File packDir = new File(new File(ditGitDir, "objects"), "pack");
998     File[] files = packDir.listFiles();
999     if (files == null || files.length == 0)
1000       return;
1001
1002     Set<String> packs = new HashSet<String>();
1003     for (File f : files) {
1004       String name = f.getName();
1005       if (name.endsWith(".pack")) {
1006         packs.add(name.substring(0, name.length() - 5));
1007       }
1008     }
1009
1010     for (File f : files) {
1011       String name = f.getName();
1012       if (name.endsWith(".idx")) {
1013         if (!packs.contains(name.substring(0, name.length() - 4)))
1014           FileUtil.delete(f);
1015       }
1016     }
1017   }
1018 }