Ability to disable credential helper provided by TeamCity
[teamcity/git-plugin.git] / git-agent / src / jetbrains / buildServer / buildTriggers / vcs / git / agent / GitAgentVcsSupport.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package jetbrains.buildServer.buildTriggers.vcs.git.agent;
18
19 import com.intellij.openapi.util.Pair;
20 import jetbrains.buildServer.agent.AgentRunningBuild;
21 import jetbrains.buildServer.agent.SmartDirectoryCleaner;
22 import jetbrains.buildServer.agent.vcs.AgentCheckoutAbility;
23 import jetbrains.buildServer.agent.vcs.AgentVcsSupport;
24 import jetbrains.buildServer.agent.vcs.UpdateByCheckoutRules2;
25 import jetbrains.buildServer.agent.vcs.UpdatePolicy;
26 import jetbrains.buildServer.buildTriggers.vcs.git.Constants;
27 import jetbrains.buildServer.buildTriggers.vcs.git.GitVcsRoot;
28 import jetbrains.buildServer.buildTriggers.vcs.git.MirrorManager;
29 import jetbrains.buildServer.vcs.*;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32
33 import java.io.File;
34 import java.util.*;
35 import java.util.concurrent.ConcurrentHashMap;
36 import java.util.concurrent.ConcurrentMap;
37 import java.util.concurrent.atomic.AtomicLong;
38
39 /**
40  * The agent support for VCS.
41  */
42 public class GitAgentVcsSupport extends AgentVcsSupport implements UpdateByCheckoutRules2 {
43
44   private final FS myFS;
45   private final SmartDirectoryCleaner myDirectoryCleaner;
46   private final GitAgentSSHService mySshService;
47   private final PluginConfigFactory myConfigFactory;
48   private final MirrorManager myMirrorManager;
49   private final GitMetaFactory myGitMetaFactory;
50
51   //The canCheckout() method should check that roots are not checked out in the same dir (TW-49786).
52   //To do that we need to create AgentPluginConfig for each VCS root which involves 'git version'
53   //command execution. Since we don't have a dedicated API for checking several roots, every root
54   //is checked with all other roots. In order to avoid running n^2 'git version' commands configs
55   //are cached for the build. Cache is reset when we get a new build.
56   private final AtomicLong myConfigsCacheBuildId = new AtomicLong(-1); //buildId for which configs are cached
57   private final ConcurrentMap<VcsRoot, AgentPluginConfig> myConfigsCache = new ConcurrentHashMap<VcsRoot, AgentPluginConfig>();//cached config per root
58   private final ConcurrentMap<VcsRoot, VcsException> myConfigErrorsCache = new ConcurrentHashMap<VcsRoot, VcsException>();//cached error thrown during config creation per root
59
60   public GitAgentVcsSupport(@NotNull FS fs,
61                             @NotNull SmartDirectoryCleaner directoryCleaner,
62                             @NotNull GitAgentSSHService sshService,
63                             @NotNull PluginConfigFactory configFactory,
64                             @NotNull MirrorManager mirrorManager,
65                             @NotNull GitMetaFactory gitMetaFactory) {
66     myFS = fs;
67     myDirectoryCleaner = directoryCleaner;
68     mySshService = sshService;
69     myConfigFactory = configFactory;
70     myMirrorManager = mirrorManager;
71     myGitMetaFactory = gitMetaFactory;
72   }
73
74
75   @NotNull
76   @Override
77   public UpdatePolicy getUpdatePolicy() {
78     return this;
79   }
80
81
82   @NotNull
83   @Override
84   public String getName() {
85     return Constants.VCS_NAME;
86   }
87
88
89   public void updateSources(@NotNull VcsRoot root,
90                             @NotNull CheckoutRules rules,
91                             @NotNull String toVersion,
92                             @NotNull File checkoutDirectory,
93                             @NotNull AgentRunningBuild build,
94                             boolean cleanCheckoutRequested) throws VcsException {
95     AgentPluginConfig config = myConfigFactory.createConfig(build, root);
96     Map<String, String> env = getGitCommandEnv(config, build);
97     GitFactory gitFactory = myGitMetaFactory.createFactory(mySshService, config, getLogger(build, config), build.getBuildTempDirectory(), env, new BuildContext(build, config));
98     Pair<CheckoutMode, File> targetDirAndMode = getTargetDirAndMode(config, rules, checkoutDirectory);
99     CheckoutMode mode = targetDirAndMode.first;
100     File targetDir = targetDirAndMode.second;
101     Updater updater;
102     AgentGitVcsRoot gitRoot = new AgentGitVcsRoot(myMirrorManager, targetDir, root);
103     if (config.isUseAlternates(gitRoot)) {
104       updater = new UpdaterWithAlternates(myFS, config, myMirrorManager, myDirectoryCleaner, gitFactory, build, root, toVersion, targetDir, rules, mode);
105     } else if (config.isUseLocalMirrors(gitRoot)) {
106       updater = new UpdaterWithMirror(myFS, config, myMirrorManager, myDirectoryCleaner, gitFactory, build, root, toVersion, targetDir, rules, mode);
107     } else {
108       updater = new UpdaterImpl(myFS, config, myMirrorManager, myDirectoryCleaner, gitFactory, build, root, toVersion, targetDir, rules, mode);
109     }
110     updater.update();
111   }
112
113
114   @NotNull
115   private Map<String, String> getGitCommandEnv(@NotNull AgentPluginConfig config, @NotNull AgentRunningBuild build) {
116     if (config.isRunGitWithBuildEnv()) {
117       return build.getBuildParameters().getEnvironmentVariables();
118     } else {
119       return new HashMap<String, String>(0);
120     }
121   }
122
123   @NotNull
124   @Override
125   public AgentCheckoutAbility canCheckout(@NotNull final VcsRoot vcsRoot, @NotNull CheckoutRules checkoutRules, @NotNull final AgentRunningBuild build) {
126     AgentPluginConfig config;
127     try {
128       config = getAndCacheConfig(build, vcsRoot);
129     } catch (VcsException e) {
130       return AgentCheckoutAbility.noVcsClientOnAgent(e.getMessage());
131     }
132
133     Pair<CheckoutMode, String> pathAndMode = getTargetPathAndMode(checkoutRules);
134     String targetDir = pathAndMode.second;
135     if (targetDir == null) {
136       return AgentCheckoutAbility.notSupportedCheckoutRules("Unsupported rules for agent-side checkout: " + checkoutRules.getAsString());
137     }
138
139     if (pathAndMode.first == CheckoutMode.SPARSE_CHECKOUT && !canUseSparseCheckout(config)) {
140       return AgentCheckoutAbility.notSupportedCheckoutRules("Cannot perform sparse checkout using git " + config.getGitExec().getVersion());
141     }
142
143     try {
144       GitVcsRoot gitRoot = new GitVcsRoot(myMirrorManager, vcsRoot);
145       UpdaterImpl.checkAuthMethodIsSupported(gitRoot, config);
146     } catch (VcsException e) {
147       return AgentCheckoutAbility.canNotCheckout(e.getMessage());
148     }
149
150     List<VcsRootEntry> gitEntries = getGitRootEntries(build);
151     if (gitEntries.size() > 1) {
152       for (VcsRootEntry entry : gitEntries) {
153         VcsRoot otherRoot = entry.getVcsRoot();
154         if (vcsRoot.equals(otherRoot))
155           continue;
156
157         AgentPluginConfig otherConfig;
158         try {
159           otherConfig = getAndCacheConfig(build, otherRoot);
160         } catch (VcsException e) {
161           continue;//appropriate reason will be returned during otherRoot check
162         }
163         Pair<CheckoutMode, String> otherPathAndMode = getTargetPathAndMode(entry.getCheckoutRules());
164         if (otherPathAndMode.first == CheckoutMode.SPARSE_CHECKOUT && !canUseSparseCheckout(otherConfig)) {
165           continue;//appropriate reason will be returned during otherRoot check
166         }
167         String entryPath = otherPathAndMode.second;
168         if (targetDir.equals(entryPath))
169           return AgentCheckoutAbility.canNotCheckout("Cannot checkout VCS root '" + vcsRoot.getName() + "' into the same directory as VCS root '" + otherRoot.getName() + "'");
170       }
171     }
172
173     return AgentCheckoutAbility.canCheckout();
174   }
175
176
177   private boolean isRequireSparseCheckout(@NotNull CheckoutRules rules) {
178     if (!rules.getExcludeRules().isEmpty())
179       return true;
180     List<IncludeRule> includeRules = rules.getRootIncludeRules();
181     if (includeRules.isEmpty() || includeRules.size() > 1)
182       return true;
183     IncludeRule rule = includeRules.get(0);
184     return !"".equals(rule.getFrom()); //rule of form +:.=>dir doesn't require sparse checkout ('.' is transformed into empty string)
185   }
186
187
188   @NotNull
189   private List<VcsRootEntry> getGitRootEntries(@NotNull AgentRunningBuild build) {
190     List<VcsRootEntry> result = new ArrayList<VcsRootEntry>();
191     for (VcsRootEntry entry : build.getVcsRootEntries()) {
192       if (Constants.VCS_NAME.equals(entry.getVcsRoot().getVcsName()))
193         result.add(entry);
194     }
195     return result;
196   }
197
198
199   @NotNull
200   private GitBuildProgressLogger getLogger(@NotNull AgentRunningBuild build, @NotNull AgentPluginConfig config) {
201     return new GitBuildProgressLogger(build.getBuildLogger().getFlowLogger("-1"), config.getGitProgressMode());
202   }
203
204
205   @NotNull
206   private Pair<CheckoutMode, File> getTargetDirAndMode(@NotNull AgentPluginConfig config,
207                                                        @NotNull CheckoutRules rules,
208                                                        @NotNull File checkoutDir) throws VcsException {
209     Pair<CheckoutMode, String> pathAndMode = getTargetPathAndMode(rules);
210     String path = pathAndMode.second;
211     if (path == null) {
212       throw new VcsException("Unsupported checkout rules for agent-side checkout: " + rules.getAsString());
213     }
214
215     boolean canUseSparseCheckout = canUseSparseCheckout(config);
216     if (pathAndMode.first == CheckoutMode.SPARSE_CHECKOUT && !canUseSparseCheckout) {
217       throw new VcsException("Cannot perform sparse checkout using git " + config.getGitExec().getVersion());
218     }
219
220     File targetDir = path.length() == 0 ? checkoutDir : new File(checkoutDir, path.replace('/', File.separatorChar));
221     if (!targetDir.exists()) {
222       //noinspection ResultOfMethodCallIgnored
223       targetDir.mkdirs();
224       if (!targetDir.exists())
225         throw new VcsException("Cannot create destination directory '" + targetDir + "'");
226     }
227
228     //Use sparse checkout mode if we can, without that switch from rules requiring sparse checkout
229     //to simple rules (e.g. to CheckoutRules.DEFAULT) doesn't work (run AgentSideSparseCheckoutTest.
230     //update_files_after_switching_to_default_rules). Probably it is a rare case when we checked out
231     //a repository using sparse checkout and then cannot use sparse checkout in the next build.
232     CheckoutMode mode = canUseSparseCheckout ? CheckoutMode.SPARSE_CHECKOUT : pathAndMode.first;
233     return Pair.create(mode, targetDir);
234   }
235
236
237   @NotNull
238   private AgentPluginConfig getAndCacheConfig(@NotNull AgentRunningBuild build, @NotNull VcsRoot root) throws VcsException {
239     //reset cache if we get a new build
240     if (build.getBuildId() != myConfigsCacheBuildId.get()) {
241       myConfigsCacheBuildId.set(build.getBuildId());
242       myConfigsCache.clear();
243       myConfigErrorsCache.clear();
244     }
245
246     AgentPluginConfig result = myConfigsCache.get(root);
247     if (result == null) {
248       VcsException error = myConfigErrorsCache.get(root);
249       if (error != null)
250         throw error;
251       try {
252         result = myConfigFactory.createConfig(build, root);
253       } catch (VcsException e) {
254         myConfigErrorsCache.put(root, e);
255         throw e;
256       }
257       myConfigsCache.put(root, result);
258     }
259     return result;
260   }
261
262
263   @NotNull
264   private Pair<CheckoutMode, String> getTargetPathAndMode(@NotNull CheckoutRules rules) {
265     if (isRequireSparseCheckout(rules)) {
266       return Pair.create(CheckoutMode.SPARSE_CHECKOUT, getSingleTargetDirForSparseCheckout(rules));
267     } else {
268       return Pair.create(CheckoutMode.MAP_REPO_TO_DIR, rules.map(""));
269     }
270   }
271
272   private boolean canUseSparseCheckout(@NotNull AgentPluginConfig config) {
273     return config.isUseSparseCheckout() && !config.getGitVersion().isLessThan(UpdaterImpl.GIT_WITH_SPARSE_CHECKOUT) &&
274            !config.getGitVersion().equals(UpdaterImpl.BROKEN_SPARSE_CHECKOUT);
275   }
276
277   @Nullable
278   private String getSingleTargetDirForSparseCheckout(@NotNull CheckoutRules rules) {
279     Set<String> targetDirs = new HashSet<String>();
280     for (IncludeRule rule : rules.getRootIncludeRules()) {
281       String from = rule.getFrom();
282       String to = rule.getTo();
283       if (from.equals("")) {
284         targetDirs.add(to);
285         continue;
286       }
287       if (from.equals(to)) {
288         targetDirs.add("");
289         continue;
290       }
291       int prefixEnd = to.lastIndexOf(from);
292       if (prefixEnd == -1) // rule of form +:a=>b, but we don't support such mapping
293         return null;
294       String prefix = to.substring(0, prefixEnd);
295       if (!prefix.endsWith("/")) //rule of form +:a=>ab, but we don't support such mapping
296         return null;
297       prefix = prefix.substring(0, prefix.length() - 1);
298       targetDirs.add(prefix);
299     }
300     if (targetDirs.isEmpty())
301       return "";
302     if (targetDirs.size() > 1) //no single target dir
303       return null;
304     return targetDirs.iterator().next();
305   }
306 }