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