IDEA-26360 (Performance and inconsistency issues with svn:externals and "Detect neste...
[fedora-idea.git] / plugins / svn4idea / src / org / jetbrains / idea / svn / SvnFileUrlMappingImpl.java
blob17e730c6f1cf1f98f8c8f9f90e3f09049c5672c2
1 /*
2 * Copyright 2000-2009 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.
16 package org.jetbrains.idea.svn;
18 import com.intellij.lifecycle.AtomicSectionsAware;
19 import com.intellij.openapi.components.PersistentStateComponent;
20 import com.intellij.openapi.components.ProjectComponent;
21 import com.intellij.openapi.components.State;
22 import com.intellij.openapi.components.Storage;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.progress.ProcessCanceledException;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.util.Getter;
27 import com.intellij.openapi.util.Ref;
28 import com.intellij.openapi.util.io.FileUtil;
29 import com.intellij.openapi.vcs.ObjectsConvertor;
30 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
31 import com.intellij.openapi.vcs.changes.ChangeListManager;
32 import com.intellij.openapi.vcs.changes.InvokeAfterUpdateMode;
33 import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
34 import com.intellij.openapi.vcs.impl.StringLenComparator;
35 import com.intellij.openapi.vfs.LocalFileSystem;
36 import com.intellij.openapi.vfs.VfsUtil;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.util.Consumer;
39 import com.intellij.util.containers.Convertor;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42 import org.tmatesoft.svn.core.SVNDepth;
43 import org.tmatesoft.svn.core.SVNException;
44 import org.tmatesoft.svn.core.SVNURL;
45 import org.tmatesoft.svn.core.internal.util.SVNPathUtil;
46 import org.tmatesoft.svn.core.internal.util.SVNURLUtil;
47 import org.tmatesoft.svn.core.wc.*;
49 import java.io.File;
50 import java.util.*;
52 @State(
53 name = "SvnFileUrlMappingImpl",
54 storages = {
55 @Storage(
56 id ="other",
57 file = "$WORKSPACE_FILE$"
60 class SvnFileUrlMappingImpl implements SvnFileUrlMapping, PersistentStateComponent<SvnMappingSavedPart>, ProjectComponent {
61 private static final Logger LOG = Logger.getInstance("#org.jetbrains.idea.svn.SvnFileUrlMappingImpl");
63 private final SvnCompatibilityChecker myChecker;
64 private final Object myMonitor = new Object();
65 // strictly: what real roots are under what vcs mappings
66 private final SvnMapping myMapping;
67 // grouped; if there are several mappings one under another, will return the upmost
68 private final SvnMapping myMoreRealMapping;
69 private final MyRootsHelper myHelper;
70 private final Project myProject;
71 private final NestedCopiesSink myTempSink;
72 private boolean myInitialized;
74 private class MyRootsHelper extends ThreadLocalDefendedInvoker<VirtualFile[]> {
75 private final ProjectLevelVcsManager myPlVcsManager;
77 private MyRootsHelper(final ProjectLevelVcsManager vcsManager) {
78 myPlVcsManager = vcsManager;
81 protected VirtualFile[] execute() {
82 return myPlVcsManager.getRootsUnderVcs(SvnVcs.getInstance(myProject));
86 public static SvnFileUrlMappingImpl getInstance(final Project project) {
87 return project.getComponent(SvnFileUrlMappingImpl.class);
90 private SvnFileUrlMappingImpl(final Project project, final ProjectLevelVcsManager vcsManager) {
91 myProject = project;
92 myMapping = new SvnMapping();
93 myMoreRealMapping = new SvnMapping();
94 myHelper = new MyRootsHelper(vcsManager);
95 myChecker = new SvnCompatibilityChecker(project);
96 myTempSink = new NestedCopiesSink();
99 @Nullable
100 public SVNURL getUrlForFile(final File file) {
101 final RootUrlInfo rootUrlInfo = getWcRootForFilePath(file);
102 if (rootUrlInfo == null) {
103 return null;
106 final String absolutePath = file.getAbsolutePath();
107 final String rootAbsPath = rootUrlInfo.getIoFile().getAbsolutePath();
108 if (absolutePath.length() < rootAbsPath.length()) {
109 // remove last separator from etalon name
110 if (absolutePath.equals(rootAbsPath.substring(0, rootAbsPath.length() - 1))) {
111 return rootUrlInfo.getAbsoluteUrlAsUrl();
113 return null;
115 final String relativePath = absolutePath.substring(rootAbsPath.length());
116 try {
117 return rootUrlInfo.getAbsoluteUrlAsUrl().appendPath(FileUtil.toSystemIndependentName(relativePath), true);
119 catch (SVNException e) {
120 LOG.info(e);
121 return null;
125 @Nullable
126 public String getLocalPath(final String url) {
127 synchronized (myMonitor) {
128 final String rootUrl = getUrlRootForUrl(url);
129 if (rootUrl == null) {
130 return null;
132 final RootUrlInfo parentInfo = myMapping.byUrl(rootUrl);
133 if (parentInfo == null) {
134 return null;
137 return fileByUrl(parentInfo.getIoFile().getAbsolutePath(), rootUrl, url).getAbsolutePath();
141 public static File fileByUrl(final String parentPath, final String parentUrl, final String childUrl) {
142 return new File(parentPath, childUrl.substring(parentUrl.length()));
145 @Nullable
146 public RootUrlInfo getWcRootForFilePath(final File file) {
147 synchronized (myMonitor) {
148 final String root = getRootForPath(file);
149 if (root == null) {
150 return null;
153 return myMapping.byFile(root);
157 public boolean rootsDiffer() {
158 synchronized (myMonitor) {
159 return myMapping.isRootsDifferFromSettings();
163 @Nullable
164 public RootUrlInfo getWcRootForUrl(final String url) {
165 synchronized (myMonitor) {
166 final String rootUrl = getUrlRootForUrl(url);
167 if (rootUrl == null) {
168 return null;
171 final RootUrlInfo result = myMapping.byUrl(rootUrl);
172 if (result == null) {
173 LOG.info("Inconsistent maps for url:" + url + " found root url: " + rootUrl);
174 return null;
176 return result;
181 * Returns real working copies roots - if there is <Project Root> -> Subversion setting,
182 * and there is one working copy, will return one root
184 public List<RootUrlInfo> getAllWcInfos() {
185 synchronized (myMonitor) {
186 // a copy is created inside
187 return myMoreRealMapping.getAllCopies();
191 public List<VirtualFile> convertRoots(final List<VirtualFile> result) {
192 if (myHelper.isInside()) return result;
194 synchronized (myMonitor) {
195 final List<VirtualFile> cachedRoots = myMapping.getUnderVcsRoots();
196 final List<VirtualFile> lonelyRoots = myMapping.getLonelyRoots();
197 if (! lonelyRoots.isEmpty()) {
198 myChecker.reportNoRoots(lonelyRoots);
200 if (cachedRoots.isEmpty()) {
201 // todo +-
202 return result;
204 return cachedRoots;
208 public void acceptNestedData(final Set<NestedCopiesBuilder.MyPointInfo> set) {
209 myTempSink.add(set);
212 private boolean init() {
213 synchronized (myMonitor) {
214 final boolean result = myInitialized;
215 myInitialized = true;
216 return result;
220 public void realRefresh(final AtomicSectionsAware atomicSectionsAware) {
221 final SvnVcs vcs = SvnVcs.getInstance(myProject);
222 final VirtualFile[] roots = myHelper.executeDefended();
224 final CopiesApplier copiesApplier = new CopiesApplier();
225 final CopiesDetector copiesDetector = new CopiesDetector(atomicSectionsAware, vcs, copiesApplier, new Getter<NestedCopiesData>() {
226 public NestedCopiesData get() {
227 return myTempSink.receive();
230 // do not send additional request for nested copies when in init state
231 copiesDetector.detectCopyRoots(roots, init());
234 private class CopiesApplier {
235 public void apply(final SvnVcs vcs, final AtomicSectionsAware atomicSectionsAware, final List<RootUrlInfo> roots,
236 final List<VirtualFile> lonelyRoots) {
237 final SvnMapping mapping = new SvnMapping();
238 mapping.addAll(roots);
239 mapping.reportLonelyRoots(lonelyRoots);
241 final SvnMapping groupedMapping = new SvnMapping();
242 final List<RootUrlInfo> filtered = new ArrayList<RootUrlInfo>();
243 ForNestedRootChecker.filterOutSuperfluousChildren(vcs, roots, filtered);
245 groupedMapping.addAll(filtered);
247 // apply
248 try {
249 atomicSectionsAware.enter();
250 synchronized (myMonitor) {
251 myMapping.copyFrom(mapping);
252 myMoreRealMapping.copyFrom(groupedMapping);
254 } finally {
255 atomicSectionsAware.exit();
257 myProject.getMessageBus().syncPublisher(SvnVcs.ROOTS_RELOADED).run();
261 private static class CopiesDetector {
262 private final AtomicSectionsAware myAtomicSectionsAware;
263 private final SvnVcs myVcs;
264 private final CopiesApplier myApplier;
265 private final List<VirtualFile> myLonelyRoots;
266 private final List<RootUrlInfo> myTopRoots;
267 private final RepositoryRoots myRepositoryRoots;
268 private final Getter<NestedCopiesData> myGate;
270 private CopiesDetector(final AtomicSectionsAware atomicSectionsAware, final SvnVcs vcs, final CopiesApplier applier,
271 final Getter<NestedCopiesData> gate) {
272 myAtomicSectionsAware = atomicSectionsAware;
273 myVcs = vcs;
274 myApplier = applier;
275 myGate = gate;
276 myTopRoots = new ArrayList<RootUrlInfo>();
277 myLonelyRoots = new ArrayList<VirtualFile>();
278 myRepositoryRoots = new RepositoryRoots(myVcs);
281 public void detectCopyRoots(final VirtualFile[] roots, final boolean clearState) {
282 final Getter<Boolean> cancelGetter = new Getter<Boolean>() {
283 public Boolean get() {
284 return myAtomicSectionsAware.shouldExitAsap();
288 for (final VirtualFile vcsRoot : roots) {
289 final List<Real> foundRoots = ForNestedRootChecker.getAllNestedWorkingCopies(vcsRoot, myVcs, false, cancelGetter);
290 if (foundRoots.isEmpty()) {
291 myLonelyRoots.add(vcsRoot);
293 // filter out bad(?) items
294 for (Real foundRoot : foundRoots) {
295 final SVNURL repoRoot = foundRoot.getInfo().getRepositoryRootURL();
296 if (repoRoot == null) {
297 LOG.info("Error: cannot find repository URL for versioned folder: " + foundRoot.getFile().getPath());
298 } else {
299 myRepositoryRoots.register(repoRoot);
300 myTopRoots.add(new RootUrlInfo(repoRoot, foundRoot.getInfo().getURL(),
301 SvnFormatSelector.getWorkingCopyFormat(foundRoot.getInfo().getFile()), foundRoot.getFile(), vcsRoot));
306 if (! SvnConfiguration.getInstance(myVcs.getProject()).DETECT_NESTED_COPIES) {
307 myApplier.apply(myVcs, myAtomicSectionsAware, myTopRoots, myLonelyRoots);
308 } else {
309 addNestedRoots(clearState);
313 private void addNestedRoots(final boolean clearState) {
314 final List<VirtualFile> basicVfRoots = ObjectsConvertor.convert(myTopRoots, new Convertor<RootUrlInfo, VirtualFile>() {
315 public VirtualFile convert(final RootUrlInfo real) {
316 return real.getVirtualFile();
320 final ChangeListManager clManager = ChangeListManager.getInstance(myVcs.getProject());
322 if (clearState) {
323 // clear what was reported before (could be for currently-not-existing roots)
324 myGate.get();
326 clManager.invokeAfterUpdate(new Runnable() {
327 public void run() {
328 final List<RootUrlInfo> nestedRoots = new ArrayList<RootUrlInfo>();
330 final NestedCopiesData data = myGate.get();
331 for (NestedCopiesBuilder.MyPointInfo info : data.getSet()) {
332 if (NestedCopyType.external.equals(info.getType()) || NestedCopyType.switched.equals(info.getType())) {
333 final File infoFile = new File(info.getFile().getPath());
334 boolean copyFound = false;
335 for (RootUrlInfo topRoot : myTopRoots) {
336 if (topRoot.getIoFile().equals(infoFile)) {
337 topRoot.setType(info.getType());
338 copyFound = true;
339 break;
342 if (copyFound) {
343 continue;
345 try {
346 final SVNStatus svnStatus = SvnUtil.getStatus(myVcs, infoFile);
347 if (svnStatus.getURL() == null) continue;
348 info.setUrl(svnStatus.getURL());
349 info.setFormat(WorkingCopyFormat.getInstance(svnStatus.getWorkingCopyFormat()));
351 catch (Exception e) {
352 continue;
355 for (RootUrlInfo topRoot : myTopRoots) {
356 if (VfsUtil.isAncestor(topRoot.getVirtualFile(), info.getFile(), true)) {
357 final SVNURL repoRoot = myRepositoryRoots.ask(info.getUrl());
358 if (repoRoot != null) {
359 final RootUrlInfo rootInfo = new RootUrlInfo(repoRoot, info.getUrl(), info.getFormat(), info.getFile(), topRoot.getRoot());
360 rootInfo.setType(info.getType());
361 nestedRoots.add(rootInfo);
363 break;
367 // check those top roots which ARE externals, but that was not detected due to they itself were the status request target
368 //new SvnNestedTypeRechecker(myVcs.getProject(), myTopRoots).run();
370 myTopRoots.addAll(nestedRoots);
371 myApplier.apply(myVcs, myAtomicSectionsAware, myTopRoots, myLonelyRoots);
373 }, InvokeAfterUpdateMode.SILENT_CALLBACK_POOLED, null, new Consumer<VcsDirtyScopeManager>() {
374 public void consume(VcsDirtyScopeManager vcsDirtyScopeManager) {
375 if (clearState) {
376 vcsDirtyScopeManager.filesDirty(null, basicVfRoots);
379 }, null);
383 private static SVNStatus getExternalItemStatus(final SvnVcs vcs, final File file) {
384 final SVNStatusClient statusClient = vcs.createStatusClient();
385 try {
386 if (file.isDirectory()) {
387 return statusClient.doStatus(file, false);
388 } else {
389 final File parent = file.getParentFile();
390 if (parent != null) {
391 statusClient.setFilesProvider(new ISVNStatusFileProvider() {
392 public Map getChildrenFiles(File parent) {
393 return Collections.singletonMap(file.getAbsolutePath(), file);
396 final Ref<SVNStatus> refStatus = new Ref<SVNStatus>();
397 statusClient.doStatus(parent, SVNRevision.WORKING, SVNDepth.FILES, false, true, false, false, new ISVNStatusHandler() {
398 public void handleStatus(final SVNStatus status) throws SVNException {
399 if (file.equals(status.getFile())) {
400 refStatus.set(status);
403 }, null);
404 return refStatus.get();
408 catch (SVNException e) {
411 return null;
414 private static class RepositoryRoots {
415 private final SvnVcs myVcs;
416 private final Set<SVNURL> myRoots;
418 private RepositoryRoots(final SvnVcs vcs) {
419 myVcs = vcs;
420 myRoots = new HashSet<SVNURL>();
423 public void register(final SVNURL url) {
424 myRoots.add(url);
427 public SVNURL ask(final SVNURL url) {
428 for (SVNURL root : myRoots) {
429 if (root.equals(SVNURLUtil.getCommonURLAncestor(root, url))) {
430 return root;
433 final SVNURL newRoot = SvnUtil.getRepositoryRoot(myVcs, url);
434 if (newRoot != null) {
435 myRoots.add(newRoot);
437 return newRoot;
441 @Nullable
442 public String getUrlRootForUrl(final String currentUrl) {
443 for (String url : myMapping.getUrls()) {
444 if (SVNPathUtil.isAncestor(url, currentUrl)) {
445 return url;
448 return null;
451 @Nullable
452 public String getRootForPath(final File currentPath) {
453 String convertedPath = currentPath.getAbsolutePath();
454 convertedPath = (currentPath.isDirectory() && (! convertedPath.endsWith(File.separator))) ? convertedPath + File.separator :
455 convertedPath;
456 final List<String> paths;
457 synchronized (myMonitor) {
458 paths = new ArrayList<String>(myMapping.getFileRoots());
460 Collections.sort(paths, StringLenComparator.getDescendingInstance());
462 for (String path : paths) {
463 if (FileUtil.startsWith(convertedPath, path)) {
464 return path;
467 return null;
470 public VirtualFile[] getNotFilteredRoots() {
471 return myHelper.executeDefended();
474 public boolean isEmpty() {
475 synchronized (myMonitor) {
476 return myMapping.isEmpty();
480 public SvnMappingSavedPart getState() {
481 final SvnMappingSavedPart result = new SvnMappingSavedPart();
483 final SvnMapping mapping = new SvnMapping();
484 final SvnMapping realMapping = new SvnMapping();
485 synchronized (myMonitor) {
486 mapping.copyFrom(myMapping);
487 realMapping.copyFrom(myMoreRealMapping);
490 for (RootUrlInfo info : mapping.getAllCopies()) {
491 result.add(convert(info));
493 for (RootUrlInfo info : realMapping.getAllCopies()) {
494 result.addReal(convert(info));
496 return result;
499 private SvnCopyRootSimple convert(final RootUrlInfo info) {
500 final SvnCopyRootSimple copy = new SvnCopyRootSimple();
501 copy.myVcsRoot = FileUtil.toSystemDependentName(info.getRoot().getPath());
502 copy.myCopyRoot = info.getIoFile().getAbsolutePath();
503 return copy;
506 public void loadState(final SvnMappingSavedPart state) {
507 final SvnMapping mapping = new SvnMapping();
508 final SvnMapping realMapping = new SvnMapping();
510 try {
511 fillMapping(mapping, state.getMappingRoots());
512 fillMapping(realMapping, state.getMoreRealMappingRoots());
513 } catch (ProcessCanceledException e) {
514 throw e;
515 } catch (Throwable t) {
516 LOG.info(t);
517 return;
520 synchronized (myMonitor) {
521 myMapping.copyFrom(mapping);
522 myMoreRealMapping.copyFrom(realMapping);
526 private void fillMapping(final SvnMapping mapping, final List<SvnCopyRootSimple> list) {
527 final LocalFileSystem lfs = LocalFileSystem.getInstance();
529 for (SvnCopyRootSimple simple : list) {
530 final VirtualFile copyRoot = lfs.findFileByIoFile(new File(simple.myCopyRoot));
531 final VirtualFile vcsRoot = lfs.findFileByIoFile(new File(simple.myVcsRoot));
533 if (copyRoot == null || vcsRoot == null) continue;
535 final SvnVcs vcs = SvnVcs.getInstance(myProject);
536 final SVNInfo svnInfo = vcs.getInfo(copyRoot);
537 if ((svnInfo == null) || (svnInfo.getRepositoryRootURL() == null)) continue;
539 mapping.add(new RootUrlInfo(svnInfo.getRepositoryRootURL(), svnInfo.getURL(),
540 SvnFormatSelector.getWorkingCopyFormat(svnInfo.getFile()), copyRoot, vcsRoot));
544 public void projectOpened() {
547 public void projectClosed() {
550 @NotNull
551 public String getComponentName() {
552 return "SvnFileUrlMappingImpl";
555 public void initComponent() {
558 public void disposeComponent() {