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