2 * Copyright 2000-2014 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package jetbrains.buildServer.buildTriggers.vcs.git.agent;
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;
35 import java.util.concurrent.ConcurrentHashMap;
36 import java.util.concurrent.ConcurrentMap;
37 import java.util.concurrent.atomic.AtomicLong;
40 * The agent support for VCS.
42 public class GitAgentVcsSupport extends AgentVcsSupport implements UpdateByCheckoutRules2 {
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;
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
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) {
67 myDirectoryCleaner = directoryCleaner;
68 mySshService = sshService;
69 myConfigFactory = configFactory;
70 myMirrorManager = mirrorManager;
71 myGitMetaFactory = gitMetaFactory;
77 public UpdatePolicy getUpdatePolicy() {
84 public String getName() {
85 return Constants.VCS_NAME;
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;
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);
108 updater = new UpdaterImpl(myFS, config, myMirrorManager, myDirectoryCleaner, gitFactory, build, root, toVersion, targetDir, rules, mode);
115 private Map<String, String> getGitCommandEnv(@NotNull AgentPluginConfig config, @NotNull AgentRunningBuild build) {
116 if (config.isRunGitWithBuildEnv()) {
117 return build.getBuildParameters().getEnvironmentVariables();
119 return new HashMap<String, String>(0);
125 public AgentCheckoutAbility canCheckout(@NotNull final VcsRoot vcsRoot, @NotNull CheckoutRules checkoutRules, @NotNull final AgentRunningBuild build) {
126 AgentPluginConfig config;
128 config = getAndCacheConfig(build, vcsRoot);
129 } catch (VcsException e) {
130 return AgentCheckoutAbility.noVcsClientOnAgent(e.getMessage());
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());
139 if (pathAndMode.first == CheckoutMode.SPARSE_CHECKOUT && !canUseSparseCheckout(config)) {
140 return AgentCheckoutAbility.notSupportedCheckoutRules("Cannot perform sparse checkout using git " + config.getGitExec().getVersion());
144 GitVcsRoot gitRoot = new GitVcsRoot(myMirrorManager, vcsRoot);
145 UpdaterImpl.checkAuthMethodIsSupported(gitRoot, config);
146 } catch (VcsException e) {
147 return AgentCheckoutAbility.canNotCheckout(e.getMessage());
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))
157 AgentPluginConfig otherConfig;
159 otherConfig = getAndCacheConfig(build, otherRoot);
160 } catch (VcsException e) {
161 continue;//appropriate reason will be returned during otherRoot check
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
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() + "'");
173 return AgentCheckoutAbility.canCheckout();
177 private boolean isRequireSparseCheckout(@NotNull CheckoutRules rules) {
178 if (!rules.getExcludeRules().isEmpty())
180 List<IncludeRule> includeRules = rules.getRootIncludeRules();
181 if (includeRules.isEmpty() || includeRules.size() > 1)
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)
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()))
200 private GitBuildProgressLogger getLogger(@NotNull AgentRunningBuild build, @NotNull AgentPluginConfig config) {
201 return new GitBuildProgressLogger(build.getBuildLogger().getFlowLogger("-1"), config.getGitProgressMode());
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;
212 throw new VcsException("Unsupported checkout rules for agent-side checkout: " + rules.getAsString());
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());
220 File targetDir = path.length() == 0 ? checkoutDir : new File(checkoutDir, path.replace('/', File.separatorChar));
221 if (!targetDir.exists()) {
222 //noinspection ResultOfMethodCallIgnored
224 if (!targetDir.exists())
225 throw new VcsException("Cannot create destination directory '" + targetDir + "'");
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);
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();
246 AgentPluginConfig result = myConfigsCache.get(root);
247 if (result == null) {
248 VcsException error = myConfigErrorsCache.get(root);
252 result = myConfigFactory.createConfig(build, root);
253 } catch (VcsException e) {
254 myConfigErrorsCache.put(root, e);
257 myConfigsCache.put(root, result);
264 private Pair<CheckoutMode, String> getTargetPathAndMode(@NotNull CheckoutRules rules) {
265 if (isRequireSparseCheckout(rules)) {
266 return Pair.create(CheckoutMode.SPARSE_CHECKOUT, getSingleTargetDirForSparseCheckout(rules));
268 return Pair.create(CheckoutMode.MAP_REPO_TO_DIR, rules.map(""));
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);
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("")) {
287 if (from.equals(to)) {
291 int prefixEnd = to.lastIndexOf(from);
292 if (prefixEnd == -1) // rule of form +:a=>b, but we don't support such mapping
294 String prefix = to.substring(0, prefixEnd);
295 if (!prefix.endsWith("/")) //rule of form +:a=>ab, but we don't support such mapping
297 prefix = prefix.substring(0, prefix.length() - 1);
298 targetDirs.add(prefix);
300 if (targetDirs.isEmpty())
302 if (targetDirs.size() > 1) //no single target dir
304 return targetDirs.iterator().next();