8f1af7b73b9bb64273edf410491a2e41ca7aa86f
[teamcity/git-plugin.git] / git-server / src / jetbrains / buildServer / buildTriggers / vcs / git / GitVcsSupport.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;
18
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.jcraft.jsch.JSchException;
21 import jetbrains.buildServer.ExtensionHolder;
22 import jetbrains.buildServer.buildTriggers.vcs.git.patch.GitPatchBuilderDispatcher;
23 import jetbrains.buildServer.serverSide.PropertiesProcessor;
24 import jetbrains.buildServer.ssh.VcsRootSshKeyManager;
25 import jetbrains.buildServer.util.cache.ResetCacheRegister;
26 import jetbrains.buildServer.vcs.*;
27 import jetbrains.buildServer.vcs.patches.PatchBuilder;
28 import org.eclipse.jgit.errors.NotSupportedException;
29 import org.eclipse.jgit.errors.TransportException;
30 import org.eclipse.jgit.lib.Ref;
31 import org.eclipse.jgit.lib.Repository;
32 import org.eclipse.jgit.storage.file.WindowCacheConfig;
33 import org.eclipse.jgit.transport.FetchConnection;
34 import org.eclipse.jgit.transport.Transport;
35 import org.eclipse.jgit.transport.URIish;
36 import org.jetbrains.annotations.NotNull;
37 import org.jetbrains.annotations.Nullable;
38
39 import java.io.File;
40 import java.io.IOException;
41 import java.util.*;
42 import java.util.stream.Collectors;
43
44 import static jetbrains.buildServer.buildTriggers.vcs.git.GitServerUtil.friendlyNotSupportedException;
45 import static jetbrains.buildServer.buildTriggers.vcs.git.GitServerUtil.friendlyTransportException;
46 import static jetbrains.buildServer.buildTriggers.vcs.git.GitUtils.getRevision;
47 import static jetbrains.buildServer.buildTriggers.vcs.git.GitUtils.isTag;
48 import static jetbrains.buildServer.util.CollectionsUtil.setOf;
49
50
51 /**
52  * Git VCS support
53  */
54 public class GitVcsSupport extends ServerVcsSupport
55   implements VcsBulkSuitabilityChecker, BuildPatchByCheckoutRules,
56              TestConnectionSupport, IncludeRuleBasedMappingProvider {
57
58   private static final Logger LOG = Logger.getInstance(GitVcsSupport.class.getName());
59   private static final Logger PERFORMANCE_LOG = Logger.getInstance(GitVcsSupport.class.getName() + ".Performance");
60   static final String GIT_REPOSITORY_HAS_NO_BRANCHES = "Git repository has no branches";
61
62   private ExtensionHolder myExtensionHolder;
63   private volatile String myDisplayName = null;
64   private final ServerPluginConfig myConfig;
65   private final TransportFactory myTransportFactory;
66   private final RepositoryManager myRepositoryManager;
67   private final GitMapFullPath myMapFullPath;
68   private final CommitLoader myCommitLoader;
69   private final VcsRootSshKeyManager mySshKeyManager;
70   private final VcsOperationProgressProvider myProgressProvider;
71   private final GitTrustStoreProvider myGitTrustStoreProvider;
72   private final TestConnectionSupport myTestConnection;
73   private Collection<GitServerExtension> myExtensions = new ArrayList<GitServerExtension>();
74
75   public GitVcsSupport(@NotNull ServerPluginConfig config,
76                        @NotNull ResetCacheRegister resetCacheManager,
77                        @NotNull TransportFactory transportFactory,
78                        @NotNull RepositoryManager repositoryManager,
79                        @NotNull GitMapFullPath mapFullPath,
80                        @NotNull CommitLoader commitLoader,
81                        @NotNull VcsRootSshKeyManager sshKeyManager,
82                        @NotNull VcsOperationProgressProvider progressProvider,
83                        @NotNull GitResetCacheHandler resetCacheHandler,
84                        @NotNull ResetRevisionsCacheHandler resetRevisionsCacheHandler,
85                        @Nullable TestConnectionSupport customTestConnection) {
86     this(config, resetCacheManager, transportFactory, repositoryManager, mapFullPath, commitLoader, sshKeyManager, progressProvider,
87          resetCacheHandler, resetRevisionsCacheHandler, new GitTrustStoreProviderStatic(null), customTestConnection);
88   }
89
90   public GitVcsSupport(@NotNull ServerPluginConfig config,
91                        @NotNull ResetCacheRegister resetCacheManager,
92                        @NotNull TransportFactory transportFactory,
93                        @NotNull RepositoryManager repositoryManager,
94                        @NotNull GitMapFullPath mapFullPath,
95                        @NotNull CommitLoader commitLoader,
96                        @NotNull VcsRootSshKeyManager sshKeyManager,
97                        @NotNull VcsOperationProgressProvider progressProvider,
98                        @NotNull GitResetCacheHandler resetCacheHandler,
99                        @NotNull ResetRevisionsCacheHandler resetRevisionsCacheHandler,
100                        @NotNull GitTrustStoreProvider gitTrustStoreProvider,
101                        @Nullable TestConnectionSupport customTestConnection) {
102     myConfig = config;
103     myTransportFactory = transportFactory;
104     myRepositoryManager = repositoryManager;
105     myMapFullPath = mapFullPath;
106     myCommitLoader = commitLoader;
107     mySshKeyManager = sshKeyManager;
108     myProgressProvider = progressProvider;
109     setStreamFileThreshold();
110     resetCacheManager.registerHandler(resetCacheHandler);
111     resetCacheManager.registerHandler(resetRevisionsCacheHandler);
112     myGitTrustStoreProvider = gitTrustStoreProvider;
113     myTestConnection = customTestConnection == null ? this : customTestConnection;
114   }
115
116   public void setExtensionHolder(@Nullable ExtensionHolder extensionHolder) {
117     myExtensionHolder = extensionHolder;
118   }
119
120   public void addExtensions(@NotNull Collection<GitServerExtension> extensions) {
121     myExtensions.addAll(extensions);
122   }
123
124   public void addExtension(@NotNull GitServerExtension extension) {
125     myExtensions.add(extension);
126   }
127
128   private void setStreamFileThreshold() {
129     int thresholdBytes = myConfig.getStreamFileThresholdMb() * WindowCacheConfig.MB;
130     if (thresholdBytes <= 0) {
131       //Config returns a threshold > 0, threshold in bytes can became non-positive due to integer overflow.
132       //Since users set a value larger than the max possible one, most likely they wanted a threshold
133       //to be large, so use maximum possible value.
134       thresholdBytes = Integer.MAX_VALUE;
135     }
136     GitServerUtil.configureStreamFileThreshold(thresholdBytes);
137   }
138
139   @NotNull
140   public List<ModificationData> collectChanges(@NotNull VcsRoot fromRoot,
141                                                @NotNull String fromVersion,
142                                                @NotNull VcsRoot toRoot,
143                                                @Nullable String toVersion,
144                                                @NotNull CheckoutRules checkoutRules) throws VcsException {
145     if (toVersion == null)
146       return Collections.emptyList();
147     GitVcsRoot fromGitRoot = new GitVcsRoot(myRepositoryManager, fromRoot);
148     GitVcsRoot toGitRoot = new GitVcsRoot(myRepositoryManager, toRoot);
149     RepositoryStateData fromState = RepositoryStateData.createVersionState(fromGitRoot.getRef(), fromVersion);
150     RepositoryStateData toState = RepositoryStateData.createVersionState(toGitRoot.getRef(), toVersion);
151     return getCollectChangesPolicy().collectChanges(fromRoot, fromState, toRoot, toState, checkoutRules);
152   }
153
154   public List<ModificationData> collectChanges(@NotNull VcsRoot root,
155                                                @NotNull String fromVersion,
156                                                @Nullable String currentVersion,
157                                                @NotNull CheckoutRules checkoutRules) throws VcsException {
158     if (currentVersion == null)
159       return Collections.emptyList();
160     GitVcsRoot gitRoot = new GitVcsRoot(myRepositoryManager, root);
161     RepositoryStateData fromState = RepositoryStateData.createVersionState(gitRoot.getRef(), fromVersion);
162     RepositoryStateData toState = RepositoryStateData.createVersionState(gitRoot.getRef(), currentVersion);
163     return getCollectChangesPolicy().collectChanges(root, fromState, toState, checkoutRules);
164   }
165
166
167   @NotNull
168   public RepositoryStateData getCurrentState(@NotNull VcsRoot root) throws VcsException {
169     GitVcsRoot gitRoot = new GitVcsRoot(myRepositoryManager, root);
170     return getCurrentState(gitRoot);
171   }
172
173   @NotNull
174   public RepositoryStateData getCurrentState(@NotNull GitVcsRoot gitRoot) throws VcsException {
175     return myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
176       String refInRoot = gitRoot.getRef();
177       String fullRef = GitUtils.expandRef(refInRoot);
178       Map<String, String> branchRevisions = new HashMap<String, String>();
179       for (Ref ref : getRemoteRefs(gitRoot.getOriginalRoot()).values()) {
180         if (!ref.getName().startsWith("ref"))
181           continue;
182         if (!gitRoot.isReportTags() && isTag(ref) && !fullRef.equals(ref.getName()))
183           continue;
184         branchRevisions.put(ref.getName(), getRevision(ref));
185       }
186       if (branchRevisions.get(fullRef) == null && !gitRoot.isIgnoreMissingDefaultBranch()) {
187         if (branchRevisions.isEmpty()) {
188           throw new VcsException(GIT_REPOSITORY_HAS_NO_BRANCHES);
189         } else {
190           throw new VcsException("Cannot find revision of the default branch '" + refInRoot + "' of vcs root '" + gitRoot.getName() + "'");
191         }
192       }
193       return RepositoryStateData.createVersionState(fullRef, branchRevisions);
194     });
195   }
196
197   public void buildPatch(@NotNull VcsRoot root,
198                          @Nullable String fromVersion,
199                          @NotNull String toVersion,
200                          @NotNull PatchBuilder builder,
201                          @NotNull CheckoutRules checkoutRules) throws IOException, VcsException {
202     OperationContext context = createContext(root, "patch building");
203     String fromRevision = fromVersion != null ? GitUtils.versionRevision(fromVersion) : null;
204     String toRevision = GitUtils.versionRevision(toVersion);
205     logBuildPatch(root, fromRevision, toRevision);
206     GitVcsRoot gitRoot = context.getGitRoot();
207     myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
208       final File trustedCertificatesDir = myGitTrustStoreProvider.getTrustedCertificatesDir();
209       GitPatchBuilderDispatcher gitPatchBuilder = new GitPatchBuilderDispatcher(myConfig, mySshKeyManager, context, builder, fromRevision,
210                                                                                 toRevision, checkoutRules,
211                                                                                 trustedCertificatesDir == null ? null : trustedCertificatesDir.getAbsolutePath());
212       try {
213         myCommitLoader.loadCommit(context, gitRoot, toRevision);
214         gitPatchBuilder.buildPatch();
215       } catch (Exception e) {
216         throw context.wrapException(e);
217       } finally {
218         context.close();
219       }
220     });
221   }
222
223   private void logBuildPatch(@NotNull VcsRoot root, @Nullable String fromRevision, @NotNull String toRevision) {
224     StringBuilder msg = new StringBuilder();
225     msg.append("Build");
226     if (fromRevision != null)
227       msg.append(" incremental");
228     msg.append(" patch in VCS root ").append(LogUtil.describe(root));
229     if (fromRevision != null)
230       msg.append(" from revision ").append(fromRevision);
231     msg.append(" to revision ").append(toRevision);
232     LOG.info(msg.toString());
233   }
234
235   @NotNull
236   public String getName() {
237     return Constants.VCS_NAME;
238   }
239
240   @NotNull
241   public String getDisplayName() {
242     initDisplayNameIfRequired();
243     return myDisplayName;
244   }
245
246   private void initDisplayNameIfRequired() {
247     if (myDisplayName == null) {
248       if (myExtensionHolder != null) {
249         boolean communityPluginFound = false;
250         final Collection<VcsSupportContext> vcsPlugins = myExtensionHolder.getServices(VcsSupportContext.class);
251         for (VcsSupportContext plugin : vcsPlugins) {
252           if (plugin.getCore().getName().equals("git")) {
253             communityPluginFound = true;
254           }
255         }
256         if (communityPluginFound) {
257           myDisplayName = "Git (JetBrains plugin)";
258         } else {
259           myDisplayName = "Git";
260         }
261       } else {
262         myDisplayName = "Git (JetBrains plugin)";
263       }
264     }
265   }
266
267   public PropertiesProcessor getVcsPropertiesProcessor() {
268     return new VcsPropertiesProcessor();
269   }
270
271   @NotNull
272   public String getVcsSettingsJspFilePath() {
273     return "gitSettings.jsp";
274   }
275
276   @NotNull
277   public String describeVcsRoot(@NotNull VcsRoot root) {
278     final String branch = root.getProperty(Constants.BRANCH_NAME);
279     return root.getProperty(Constants.FETCH_URL) + "#" + (branch == null ? "master" : branch);
280   }
281
282   @NotNull
283   public Map<String, String> getDefaultVcsProperties() {
284     final HashMap<String, String> map = new HashMap<String, String>();
285     map.put(Constants.BRANCH_NAME, "refs/heads/master");
286     map.put(Constants.IGNORE_KNOWN_HOSTS, "true");
287     map.put(Constants.AUTH_METHOD, AuthenticationMethod.ANONYMOUS.name());
288     map.put(Constants.USERNAME_STYLE, GitVcsRoot.UserNameStyle.USERID.name());
289     map.put(Constants.AGENT_CLEAN_POLICY, AgentCleanPolicy.ON_BRANCH_CHANGE.name());
290     map.put(Constants.AGENT_CLEAN_FILES_POLICY, AgentCleanFilesPolicy.ALL_UNTRACKED.name());
291     map.put(Constants.SUBMODULES_CHECKOUT, SubmodulesCheckoutPolicy.CHECKOUT.name());
292     map.put(Constants.USE_AGENT_MIRRORS, "true");
293     return map;
294   }
295
296   @NotNull
297   public String getVersionDisplayName(@NotNull String version, @NotNull VcsRoot root) throws VcsException {
298     return GitServerUtil.displayVersion(version);
299   }
300
301   @NotNull
302   public Comparator<String> getVersionComparator() {
303     return GitUtils.VERSION_COMPARATOR;
304   }
305
306
307   public boolean sourcesUpdatePossibleIfChangesNotFound(@NotNull VcsRoot root) {
308     return false;
309   }
310
311   public String testConnection(@NotNull VcsRoot vcsRoot) throws VcsException {
312     OperationContext context = createContext(vcsRoot, "connection test");
313     GitVcsRoot gitRoot = context.getGitRoot();
314     return myRepositoryManager.runWithDisabledRemove(gitRoot.getRepositoryDir(), () -> {
315       TestConnectionCommand command = new TestConnectionCommand(this, myTransportFactory, myRepositoryManager);
316       try {
317         return command.testConnection(context);
318       } catch (Exception e) {
319         throw context.wrapException(e);
320       } finally {
321         context.close();
322       }
323     });
324   }
325
326
327   @Override
328   public TestConnectionSupport getTestConnectionSupport() {
329     return myTestConnection;
330   }
331
332   public OperationContext createContext(@NotNull String operation) {
333     return createContext(null, operation);
334   }
335
336   public OperationContext createContext(VcsRoot root, String operation) {
337     return createContext(root, operation, GitProgress.NO_OP);
338   }
339
340   public OperationContext createContext(@Nullable VcsRoot root, @NotNull String operation, @NotNull GitProgress progress) {
341     return new OperationContext(myCommitLoader, myRepositoryManager, root, operation, progress, myConfig);
342   }
343
344   @NotNull
345   public LabelingSupport getLabelingSupport() {
346     return new GitLabelingSupport(this, myCommitLoader, myRepositoryManager, myTransportFactory, myConfig);
347   }
348
349   @NotNull
350   public VcsFileContentProvider getContentProvider() {
351     return new GitFileContentDispatcher(this, myCommitLoader, myConfig);
352   }
353
354   @NotNull
355   public GitCollectChangesPolicy getCollectChangesPolicy() {
356     return new GitCollectChangesPolicy(this, myProgressProvider, myCommitLoader, myConfig, myRepositoryManager);
357   }
358
359   @NotNull
360   public BuildPatchPolicy getBuildPatchPolicy() {
361     return this;
362   }
363
364   @Override
365   public VcsPersonalSupport getPersonalSupport() {
366     return this;
367   }
368
369   /**
370    * Expected fullPath format:
371    * <p/>
372    * "<git revision hash>|<repository url>|<file relative path>"
373    *
374    * @param rootEntry indicates the association between VCS root and build configuration
375    * @param fullPath  change path from IDE patch
376    * @return the mapped path
377    */
378   @NotNull
379   public Collection<String> mapFullPath(@NotNull final VcsRootEntry rootEntry, @NotNull final String fullPath) {
380     OperationContext context = createContext(rootEntry.getVcsRoot(), "map full path");
381     try {
382       return myRepositoryManager.runWithDisabledRemove(context.getGitRoot().getRepositoryDir(), () ->
383         myMapFullPath.mapFullPath(context, rootEntry, fullPath));
384     } catch (VcsException e) {
385       LOG.warnAndDebugDetails("Error while mapping path for root " + LogUtil.describe(rootEntry.getVcsRoot()), e);
386       return Collections.emptySet();
387     } catch (Throwable t) {
388       LOG.error("Error while mapping path for root " + LogUtil.describe(rootEntry.getVcsRoot()), t);
389       return Collections.emptySet();
390     } finally {
391       context.close();
392     }
393   }
394
395
396   @NotNull
397   @Override
398   public List<Boolean> checkSuitable(@NotNull List<VcsRootEntry> entries, @NotNull Collection<String> paths) throws VcsException {
399     Set<GitMapFullPath.FullPath> fullPaths = paths.stream().map(GitMapFullPath.FullPath::new).collect(Collectors.toSet());
400
401     //checkout rules do not affect suitability, we can check it for unique root only ignoring different checkout rules
402     Set<VcsRoot> uniqueRoots = entries.stream().map(VcsRootEntry::getVcsRoot).collect(Collectors.toSet());
403
404     //several roots with different settings can be cloned into the same dir,
405     //do not compute suitability for given clone dir more than once
406     Map<File, Boolean> cloneDirResults = new HashMap<>();//clone dir -> result for this dir
407     Map<VcsRoot, Boolean> rootResult = new HashMap<>();
408     for (VcsRoot root : uniqueRoots) {
409       OperationContext context = createContext(root, "checkSuitable");
410       try {
411         GitVcsRoot gitRoot = context.getGitRoot();
412         File cloneDir = gitRoot.getRepositoryDir();
413         Boolean cloneDirResult = cloneDirResults.get(cloneDir);
414         if (cloneDirResult != null) {
415           rootResult.put(root, cloneDirResult);
416           continue;
417         }
418
419         boolean suitable = myRepositoryManager.runWithDisabledRemove(cloneDir, () -> {
420           for (GitMapFullPath.FullPath path : fullPaths) {
421             if (myMapFullPath.repositoryContainsPath(context, gitRoot, path))
422               return true;
423           }
424           return false;
425         });
426
427         rootResult.put(root, suitable);
428         cloneDirResults.put(gitRoot.getRepositoryDir(), suitable);
429       } catch (VcsException e) {
430         //will return false for broken VCS root
431         LOG.warnAndDebugDetails("Error while checking suitability for root " + LogUtil.describe(root) + ", assume root is not suitable", e);
432       } finally {
433         context.close();
434       }
435     }
436
437     List<Boolean> result = new ArrayList<>();
438     for (VcsRootEntry entry : entries) {
439       Boolean suitable = rootResult.get(entry.getVcsRoot());
440       if (suitable != null) {
441         result.add(suitable);
442       } else {
443         //can be null if the root was broken
444         result.add(false);
445       }
446     }
447     return result;
448   }
449
450
451   @Override
452   public boolean isAgentSideCheckoutAvailable() {
453     return true;
454   }
455
456
457   @Override
458   public UrlSupport getUrlSupport() {
459     return null;
460   }
461
462
463   @NotNull
464   public Map<String, Ref> getRemoteRefs(@NotNull final VcsRoot root) throws VcsException {
465     OperationContext context = createContext(root, "list remote refs");
466     GitVcsRoot gitRoot = context.getGitRoot();
467     try {
468       Repository db = context.getRepository();
469       Map<String, Ref> remoteRefs = getRemoteRefs(db, gitRoot);
470       if (LOG.isDebugEnabled() && myConfig.logRemoteRefs())
471         LOG.debug("Remote refs for VCS root " + LogUtil.describe(root) + ": " + remoteRefs);
472       return remoteRefs;
473     } catch (Exception e) {
474       throw context.wrapException(e);
475     } finally {
476       context.close();
477     }
478   }
479
480
481   @NotNull
482   private Map<String, Ref> getRemoteRefs(@NotNull Repository db, @NotNull GitVcsRoot gitRoot) throws Exception {
483     long retryInterval = myConfig.getConnectionRetryIntervalMillis();
484     int attemptsLeft = myConfig.getConnectionRetryAttempts();
485     int timeout = myConfig.getRepositoryStateTimeoutSeconds();
486     while (true) {
487       final long start = System.currentTimeMillis();
488       Transport transport = null;
489       FetchConnection connection = null;
490       try {
491         transport = myTransportFactory.createTransport(db, gitRoot.getRepositoryFetchURL(), gitRoot.getAuthSettings(), timeout);
492         connection = transport.openFetch();
493         return connection.getRefsMap();
494       } catch (NotSupportedException nse) {
495         throw friendlyNotSupportedException(gitRoot, nse);
496       } catch (TransportException te) {
497         attemptsLeft--;
498         if (isRecoverable(te) && attemptsLeft > 0) {
499           LOG.warn("List remote refs failed: " + te.getMessage() + ", " + attemptsLeft + " attempt(s) left");
500         } else {
501           throw friendlyTransportException(te, gitRoot);
502         }
503       } catch (WrongPassphraseException e) {
504         throw new VcsException(e.getMessage(), e);
505       } finally {
506         if (connection != null)
507           connection.close();
508         if (transport != null)
509           transport.close();
510         final long finish = System.currentTimeMillis();
511         PERFORMANCE_LOG.debug("[getRemoteRefs] repository: " + LogUtil.describe(gitRoot) + ", took " + (finish - start) + "ms");
512       }
513       Thread.sleep(retryInterval);
514       retryInterval *= 2;
515     }
516   }
517
518   private boolean isRecoverable(@NotNull TransportException e) {
519     String message = e.getMessage();
520     if (message == null)
521       return false;
522     if (message.contains("Connection timed out") ||
523         message.contains("Connection time out")) {
524       return true;
525     }
526     Throwable cause = e.getCause();
527     if (cause instanceof JSchException) {
528       return message.contains("Session.connect: java.net.SocketException: Connection reset") ||
529              message.contains("Session.connect: java.net.SocketException: Software caused connection abort") ||
530              message.contains("Session.connect: java.net.SocketTimeoutException: Read timed out") ||
531              message.contains("connection is closed by foreign host") ||
532              message.contains("timeout: socket is not established") ||
533              message.contains("java.net.UnknownHostException:") || //TW-31027
534              message.contains("com.jcraft.jsch.JSchException: verify: false"); //TW-31175
535     }
536     return false;
537   }
538
539
540   public Collection<VcsClientMapping> getClientMapping(final @NotNull VcsRoot root, final @NotNull IncludeRule rule) throws VcsException {
541     final OperationContext context = createContext(root, "client-mapping");
542     try {
543       GitVcsRoot gitRoot = context.getGitRoot();
544       URIish uri = gitRoot.getRepositoryFetchURL();
545       return Collections.singletonList(new VcsClientMapping(String.format("|%s|%s", uri.toString(), rule.getFrom()), rule.getTo()));
546     } finally {
547       context.close();
548     }
549   }
550
551   @Override
552   public boolean isDAGBasedVcs() {
553     return true;
554   }
555
556   @Override
557   public ListFilesPolicy getListFilesPolicy() {
558     return new ListFilesDispatcher(this, myCommitLoader, myConfig);
559   }
560
561   @NotNull
562   @Override
563   public Map<String, String> getCheckoutProperties(@NotNull VcsRoot root) throws VcsException {
564     Map<String, String> defaults = getDefaultVcsProperties();
565     Set<String> significantProps = setOf(Constants.FETCH_URL,
566                                          Constants.SUBMODULES_CHECKOUT,
567                                          Constants.AGENT_CLEAN_POLICY,
568                                          Constants.AGENT_CLEAN_FILES_POLICY);
569     Map<String, String> rootProperties = root.getProperties();
570     Map<String, String> repositoryProperties = new HashMap<String, String>();
571     for (String key : significantProps) {
572       String defVal = defaults.get(key);
573       String actualVal = rootProperties.get(key);
574       repositoryProperties.put(key, actualVal == null ? defVal : actualVal);
575     }
576
577     //include autocrlf settings only for non-default value
578     //in order to avoid clean checkout
579     if ("true".equals(rootProperties.get(Constants.SERVER_SIDE_AUTO_CRLF)))
580       repositoryProperties.put(Constants.SERVER_SIDE_AUTO_CRLF, rootProperties.get(Constants.SERVER_SIDE_AUTO_CRLF));
581
582     return repositoryProperties;
583   }
584
585   @Override
586   @Nullable
587   protected <T extends VcsExtension> T getVcsCustomExtension(@NotNull final Class<T> extensionClass) {
588     if (ChangesInfoBuilder.class.equals(extensionClass)) {
589       return extensionClass.cast(getCollectChangesPolicy());
590     }
591
592     if (myExtensions != null) {
593       for (GitServerExtension e : myExtensions) {
594         if (extensionClass.isInstance(e))
595           return extensionClass.cast(e);
596       }
597     }
598     return super.getVcsCustomExtension(extensionClass);
599   }
600
601   @NotNull
602   public CommitLoader getCommitLoader() {
603     return myCommitLoader;
604   }
605
606   @NotNull
607   public RepositoryManager getRepositoryManager() {
608     return myRepositoryManager;
609   }
610 }