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