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