VCS: roots autodetection mentioned in IDEADEV-42024 (Subversion: Update is not workin...
[fedora-idea.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / impl / projectlevelman / NewMappings.java
blobcca8b1bb7fb9ce7f287c7ddb46d0004673ea4a0d
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 com.intellij.openapi.vcs.impl.projectlevelman;
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.project.DumbAwareRunnable;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.startup.StartupManager;
22 import com.intellij.openapi.ui.MessageType;
23 import com.intellij.openapi.util.EmptyRunnable;
24 import com.intellij.openapi.util.Pair;
25 import com.intellij.openapi.util.Ref;
26 import com.intellij.openapi.util.io.FileUtil;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.openapi.vcs.*;
29 import com.intellij.openapi.vcs.changes.ui.ChangesViewBalloonProblemNotifier;
30 import com.intellij.openapi.vcs.impl.DefaultVcsRootPolicy;
31 import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl;
32 import com.intellij.openapi.vcs.impl.VcsInitObject;
33 import com.intellij.openapi.vfs.LocalFileSystem;
34 import com.intellij.openapi.vfs.VirtualFile;
35 import com.intellij.util.EventDispatcher;
36 import com.intellij.util.containers.Convertor;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
40 import java.util.*;
42 public class NewMappings {
43 private final static Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.impl.projectlevelman.NewMappings");
44 private final Object myLock;
46 // vcs to mappings
47 private final Map<String, List<VcsDirectoryMapping>> myVcsToPaths;
48 private AbstractVcs[] myActiveVcses;
49 private VcsDirectoryMapping[] mySortedMappings;
50 private final Map<VcsDirectoryMapping, LocalFileSystem.WatchRequest> myDirectoryMappingWatches;
52 private final DefaultVcsRootPolicy myDefaultVcsRootPolicy;
53 private final EventDispatcher<VcsListener> myEventDispatcher;
54 private final Project myProject;
56 private boolean myActivated;
58 public NewMappings(final Project project, final EventDispatcher<VcsListener> eventDispatcher,
59 final ProjectLevelVcsManagerImpl vcsManager) {
60 myProject = project;
61 myLock = new Object();
62 myVcsToPaths = new HashMap<String, List<VcsDirectoryMapping>>();
63 myDirectoryMappingWatches = new HashMap<VcsDirectoryMapping, LocalFileSystem.WatchRequest>();
64 myDefaultVcsRootPolicy = DefaultVcsRootPolicy.getInstance(project);
65 myActiveVcses = new AbstractVcs[0];
66 myEventDispatcher = eventDispatcher;
68 final ArrayList<VcsDirectoryMapping> listStr = new ArrayList<VcsDirectoryMapping>();
69 final VcsDirectoryMapping mapping = new VcsDirectoryMapping("", "");
70 listStr.add(mapping);
71 myVcsToPaths.put("", listStr);
72 mySortedMappings = new VcsDirectoryMapping[] {mapping};
73 myActivated = false;
75 vcsManager.addInitializationRequest(VcsInitObject.MAPPINGS, new DumbAwareRunnable() {
76 public void run() {
77 activateActiveVcses();
79 });
82 public AbstractVcs[] getActiveVcses() {
83 synchronized (myLock) {
84 final AbstractVcs[] result = new AbstractVcs[myActiveVcses.length];
85 System.arraycopy(myActiveVcses, 0, result, 0, myActiveVcses.length);
86 return result;
90 public void activateActiveVcses() {
91 synchronized (myLock) {
92 if (myActivated) return;
93 myActivated = true;
95 keepActiveVcs(EmptyRunnable.getInstance());
96 mappingsChanged();
99 @Modification
100 public void setMapping(final String path, final String activeVcsName) {
101 LOG.debug("setMapping path = '" + path + "' vcs = " + activeVcsName);
102 final VcsDirectoryMapping newMapping = new VcsDirectoryMapping(path, activeVcsName);
103 // do not add duplicates
104 synchronized (myLock) {
105 if (myVcsToPaths.containsKey(activeVcsName)) {
106 final List<VcsDirectoryMapping> vcsDirectoryMappings = myVcsToPaths.get(activeVcsName);
107 if ((vcsDirectoryMappings != null) && (vcsDirectoryMappings.contains(newMapping))) {
108 return;
113 final LocalFileSystem.WatchRequest request = addWatchRequest(newMapping);
115 final Ref<Boolean> switched = new Ref<Boolean>(Boolean.FALSE);
116 keepActiveVcs(new Runnable() {
117 public void run() {
118 // sorted -> map. sorted mappings are NOT changed;
119 switched.set(trySwitchVcs(path, activeVcsName));
120 if (! switched.get().booleanValue()) {
121 final List<VcsDirectoryMapping> newList = listForVcsFromMap(newMapping.getVcs());
122 newList.add(newMapping);
123 sortedMappingsByMap();
125 if (request != null) {
126 myDirectoryMappingWatches.put(newMapping, request);
132 if (switched.get().booleanValue() && (request != null)) {
133 LocalFileSystem.getInstance().removeWatchedRoot(request);
136 mappingsChanged();
139 private void keepActiveVcs(final Runnable runnable) {
140 final MyVcsActivator activator;
141 synchronized (myLock) {
142 if (! myActivated) {
143 runnable.run();
144 return;
146 final HashSet<String> old = new HashSet<String>();
147 for (AbstractVcs activeVcs : myActiveVcses) {
148 old.add(activeVcs.getName());
150 activator = new MyVcsActivator(old);
151 runnable.run();
152 restoreActiveVcses();
154 activator.activate(myVcsToPaths.keySet(), AllVcses.getInstance(myProject));
157 private void restoreActiveVcses() {
158 synchronized (myLock) {
159 final Set<String> set = myVcsToPaths.keySet();
160 final List<AbstractVcs> list = new ArrayList<AbstractVcs>(set.size());
161 for (String s : set) {
162 if (s.trim().length() == 0) continue;
163 final AbstractVcs vcs = AllVcses.getInstance(myProject).getByName(s);
164 if (vcs != null) {
165 list.add(vcs);
168 myActiveVcses = list.toArray(new AbstractVcs[list.size()]);
172 public void mappingsChanged() {
173 myEventDispatcher.getMulticaster().directoryMappingChanged();
176 @Modification
177 public void setDirectoryMappings(final List<VcsDirectoryMapping> items) {
178 LOG.debug("setDirectoryMappings, size: " + items.size());
179 MySetMappingsPreProcessor setMappingsPreProcessor = new MySetMappingsPreProcessor(items);
180 setMappingsPreProcessor.invoke();
181 final List<VcsDirectoryMapping> itemsCopy = setMappingsPreProcessor.getItemsCopy();
182 final Map<VcsDirectoryMapping, LocalFileSystem.WatchRequest> requests = setMappingsPreProcessor.getRequests();
184 final Collection<LocalFileSystem.WatchRequest> toRemove = new ArrayList<LocalFileSystem.WatchRequest>();
186 keepActiveVcs(new Runnable() {
187 public void run() {
188 // a copy!
189 toRemove.addAll(myDirectoryMappingWatches.values());
190 myDirectoryMappingWatches.clear();
192 myVcsToPaths.clear();
193 for (VcsDirectoryMapping mapping : itemsCopy) {
194 listForVcsFromMap(mapping.getVcs()).add(mapping);
196 sortedMappingsByMap();
200 // do not remove what added and is active
201 toRemove.removeAll(requests.values());
202 // tracked by request object reference so OK to first add and then remove
203 LocalFileSystem.getInstance().removeWatchedRoots(toRemove);
205 mappingsChanged();
208 @Nullable
209 public VcsDirectoryMapping getMappingFor(VirtualFile file) {
210 if (file == null) return null;
211 if (! file.isInLocalFileSystem()) {
212 return null;
215 return getMappingFor(file, myDefaultVcsRootPolicy.getMatchContext(file));
218 @Nullable
219 public VcsDirectoryMapping getMappingFor(final VirtualFile file, final Object matchContext) {
220 // performance: calculate file path just once, rather than once per mapping
221 String path = file.getPath();
222 final String systemIndependPath = FileUtil.toSystemIndependentName((file.isDirectory() && (! path.endsWith("/"))) ? (path + "/") : path);
224 synchronized (myLock) {
225 for (int i = mySortedMappings.length - 1; i >= 0; -- i) {
226 final VcsDirectoryMapping mapping = mySortedMappings[i];
227 if (fileMatchesMapping(file, matchContext, systemIndependPath, mapping)) {
228 return mapping;
231 return null;
235 @Nullable
236 public String getVcsFor(@NotNull VirtualFile file) {
237 VcsDirectoryMapping mapping = getMappingFor(file);
238 if (mapping == null) {
239 return null;
241 return mapping.getVcs();
244 private boolean fileMatchesMapping(final VirtualFile file, final Object matchContext, final String systemIndependPath, final VcsDirectoryMapping mapping) {
245 if (mapping.getDirectory().length() == 0) {
246 return myDefaultVcsRootPolicy.matchesDefaultMapping(file, matchContext);
248 return FileUtil.startsWith(systemIndependPath, mapping.systemIndependentPath());
251 List<VirtualFile> getMappingsAsFilesUnderVcs(final AbstractVcs vcs) {
252 final List<VirtualFile> result = new ArrayList<VirtualFile>();
253 final String vcsName = vcs.getName();
255 final List<VcsDirectoryMapping> mappings;
256 synchronized (myLock) {
257 final List<VcsDirectoryMapping> vcsMappings = myVcsToPaths.get(vcsName);
258 if (vcsMappings == null) return result;
259 mappings = new ArrayList<VcsDirectoryMapping>(vcsMappings);
262 for (VcsDirectoryMapping mapping : mappings) {
263 if (mapping.isDefaultMapping()) {
264 // todo callback here; don't like it
265 myDefaultVcsRootPolicy.addDefaultVcsRoots(this, vcs, result);
266 } else {
267 final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(mapping.getDirectory());
268 if (file != null) {
269 result.add(file);
273 return result;
276 @Modification
277 public void disposeMe() {
278 LOG.debug("dipose me");
279 clearImpl();
282 @Modification
283 public void clear() {
284 LOG.debug("clear");
285 clearImpl();
287 mappingsChanged();
290 private void clearImpl() {
291 // if vcses were not mapped, there's nothing to clear
292 if ((myActiveVcses == null) || (myActiveVcses.length == 0)) return;
294 final Collection<LocalFileSystem.WatchRequest> toRemove = new ArrayList<LocalFileSystem.WatchRequest>();
295 keepActiveVcs(new Runnable() {
296 public void run() {
297 // a copy!
298 toRemove.addAll(myDirectoryMappingWatches.values());
299 myDirectoryMappingWatches.clear();
301 myVcsToPaths.clear();
302 myActiveVcses = new AbstractVcs[0];
303 mySortedMappings = new VcsDirectoryMapping[0];
306 if (! toRemove.isEmpty()) {
307 LocalFileSystem.getInstance().removeWatchedRoots(toRemove);
311 public List<VcsDirectoryMapping> getDirectoryMappings() {
312 synchronized (myLock) {
313 return Arrays.asList(mySortedMappings);
317 public List<VcsDirectoryMapping> getDirectoryMappings(String vcsName) {
318 synchronized (myLock) {
319 final List<VcsDirectoryMapping> mappings = myVcsToPaths.get(vcsName);
320 return mappings == null ? new ArrayList<VcsDirectoryMapping>() : new ArrayList<VcsDirectoryMapping>(mappings);
324 public void cleanupMappings() {
325 final List<LocalFileSystem.WatchRequest> watchRequestList;
326 synchronized (myLock) {
327 watchRequestList = removeRedundantMappings();
329 LocalFileSystem.getInstance().removeWatchedRoots(watchRequestList);
332 @Nullable
333 public String haveDefaultMapping() {
334 synchronized (myLock) {
335 // empty mapping MUST be first
336 if (mySortedMappings.length == 0) return null;
337 return mySortedMappings[0].isDefaultMapping() ? mySortedMappings[0].getVcs() : null;
341 public boolean isEmpty() {
342 synchronized (myLock) {
343 return mySortedMappings.length == 0;
347 @Modification
348 public void removeDirectoryMapping(final VcsDirectoryMapping mapping) {
349 LOG.debug("remove mapping: " + mapping.getDirectory());
350 final Ref<LocalFileSystem.WatchRequest> request = new Ref<LocalFileSystem.WatchRequest>();
352 keepActiveVcs(new Runnable() {
353 public void run() {
354 if (removeVcsFromMap(mapping, mapping.getVcs())) {
355 sortedMappingsByMap();
356 request.set(myDirectoryMappingWatches.remove(mapping));
361 if (! request.isNull()) {
362 LocalFileSystem.getInstance().removeWatchedRoot(request.get());
365 mappingsChanged();
368 private class MyMappingsFilter extends AbstractFilterChildren<VcsDirectoryMapping> {
369 private final List<LocalFileSystem.WatchRequest> myRemovedRequests;
371 private MyMappingsFilter() {
372 myRemovedRequests = new ArrayList<LocalFileSystem.WatchRequest>();
375 protected void sortAscending(List<VcsDirectoryMapping> vcsDirectoryMappings) {
376 // todo ordering is actually here
377 Collections.sort(vcsDirectoryMappings, MyMappingsComparator.getInstance());
380 @Override
381 protected void onRemove(final VcsDirectoryMapping vcsDirectoryMapping) {
382 final LocalFileSystem.WatchRequest request = myDirectoryMappingWatches.remove(vcsDirectoryMapping);
383 if (request != null) {
384 myRemovedRequests.add(request);
388 protected boolean isAncestor(VcsDirectoryMapping parent, VcsDirectoryMapping child) {
389 if (! parent.getVcs().equals(child.getVcs())) return false;
391 final String parentPath = parent.systemIndependentPath();
392 final String fixedParentPath = (parentPath.endsWith("/")) ? parentPath : (parentPath + "/");
394 if (child.systemIndependentPath().length() < fixedParentPath.length()) {
395 return child.systemIndependentPath().equals(parentPath);
397 return child.systemIndependentPath().startsWith(fixedParentPath);
400 public List<LocalFileSystem.WatchRequest> getRemovedRequests() {
401 return myRemovedRequests;
405 // todo area for optimization
406 private List<LocalFileSystem.WatchRequest> removeRedundantMappings() {
407 final Set<Map.Entry<String, List<VcsDirectoryMapping>>> entries = myVcsToPaths.entrySet();
409 final LocalFileSystem lfs = LocalFileSystem.getInstance();
410 final AllVcsesI allVcses = AllVcses.getInstance(myProject);
412 final List<LocalFileSystem.WatchRequest> removedRequests = new LinkedList<LocalFileSystem.WatchRequest>();
414 for (Iterator<String> iterator = myVcsToPaths.keySet().iterator(); iterator.hasNext();) {
415 final String vcsName = iterator.next();
416 final List<VcsDirectoryMapping> mappings = myVcsToPaths.get(vcsName);
418 final List<Pair<VirtualFile, VcsDirectoryMapping>> objects = ObjectsConvertor.convert(mappings,
419 new Convertor<VcsDirectoryMapping, Pair<VirtualFile, VcsDirectoryMapping>>() {
420 public Pair<VirtualFile, VcsDirectoryMapping> convert(final VcsDirectoryMapping dm) {
421 VirtualFile vf = lfs.findFileByPath(dm.getDirectory());
422 if (vf == null) {
423 vf = lfs.refreshAndFindFileByPath(dm.getDirectory());
425 return vf == null ? null : new Pair<VirtualFile, VcsDirectoryMapping>(vf, dm);
427 }, ObjectsConvertor.NOT_NULL);
429 final List<Pair<VirtualFile, VcsDirectoryMapping>> filteredFiles;
430 // todo static
431 final Convertor<Pair<VirtualFile, VcsDirectoryMapping>, VirtualFile> fileConvertor =
432 new Convertor<Pair<VirtualFile, VcsDirectoryMapping>, VirtualFile>() {
433 public VirtualFile convert(Pair<VirtualFile, VcsDirectoryMapping> o) {
434 return o.getFirst();
437 if (StringUtil.isEmptyOrSpaces(vcsName)) {
438 filteredFiles = AbstractVcs.filterUniqueRootsDefault(objects, fileConvertor);
439 } else {
440 final AbstractVcs vcs = allVcses.getByName(vcsName);
441 if (vcs == null) {
442 ChangesViewBalloonProblemNotifier.showMe(myProject, "VCS plugin not found for mapping to : '" + vcsName + "'", MessageType.ERROR);
443 continue;
445 filteredFiles = vcs.filterUniqueRoots(objects, fileConvertor);
448 final List<VcsDirectoryMapping> filteredMappings =
449 ObjectsConvertor.convert(filteredFiles, new Convertor<Pair<VirtualFile, VcsDirectoryMapping>, VcsDirectoryMapping>() {
450 public VcsDirectoryMapping convert(final Pair<VirtualFile, VcsDirectoryMapping> o) {
451 return o.getSecond();
455 // to calculate what had been removed
456 mappings.removeAll(filteredMappings);
457 for (VcsDirectoryMapping mapping : mappings) {
458 removedRequests.add(myDirectoryMappingWatches.remove(mapping));
461 if (filteredMappings.isEmpty()) {
462 iterator.remove();
463 } else {
464 mappings.clear();
465 mappings.addAll(filteredMappings);
469 sortedMappingsByMap();
470 return removedRequests;
473 private boolean trySwitchVcs(final String path, final String activeVcsName) {
474 final String fixedPath = FileUtil.toSystemIndependentName(path);
475 for (VcsDirectoryMapping mapping : mySortedMappings) {
476 if (mapping.systemIndependentPath().equals(fixedPath)) {
477 final String oldVcs = mapping.getVcs();
478 if (! oldVcs.equals(activeVcsName)) {
479 migrateVcs(activeVcsName, mapping, oldVcs);
481 return true;
484 return false;
487 private void sortedMappingsByMap() {
488 final List<VcsDirectoryMapping> list = new ArrayList<VcsDirectoryMapping>();
489 for (List<VcsDirectoryMapping> mappingList : myVcsToPaths.values()) {
490 list.addAll(mappingList);
492 mySortedMappings = list.toArray(new VcsDirectoryMapping[list.size()]);
493 Arrays.sort(mySortedMappings, MyMappingsComparator.getInstance());
496 private void migrateVcs(String activeVcsName, VcsDirectoryMapping mapping, String oldVcs) {
497 mapping.setVcs(activeVcsName);
499 removeVcsFromMap(mapping, oldVcs);
501 final List<VcsDirectoryMapping> newList = listForVcsFromMap(activeVcsName);
502 newList.add(mapping);
505 private boolean removeVcsFromMap(VcsDirectoryMapping mapping, String oldVcs) {
506 final List<VcsDirectoryMapping> oldList = myVcsToPaths.get(oldVcs);
507 if (oldList == null) return false;
509 final boolean result = oldList.remove(mapping);
510 if (oldList.isEmpty()) {
511 myVcsToPaths.remove(oldVcs);
513 return result;
516 // todo don't like it
517 private List<VcsDirectoryMapping> listForVcsFromMap(String activeVcsName) {
518 List<VcsDirectoryMapping> newList = myVcsToPaths.get(activeVcsName);
519 if (newList == null) {
520 newList = new ArrayList<VcsDirectoryMapping>();
521 myVcsToPaths.put(activeVcsName, newList);
523 return newList;
526 @Nullable
527 private static LocalFileSystem.WatchRequest addWatchRequest(final VcsDirectoryMapping mapping) {
528 if (! mapping.isDefaultMapping()) {
529 return LocalFileSystem.getInstance().addRootToWatch(mapping.getDirectory(), true);
531 return null;
534 private static class MyMappingsComparator implements Comparator<VcsDirectoryMapping> {
535 private static final MyMappingsComparator ourInstance = new MyMappingsComparator();
537 public static MyMappingsComparator getInstance() {
538 return ourInstance;
541 public int compare(VcsDirectoryMapping m1, VcsDirectoryMapping m2) {
542 return m1.getDirectory().compareTo(m2.getDirectory());
546 private static class MyVcsActivator {
547 private final Set<String> myOld;
549 public MyVcsActivator(final Set<String> old) {
550 myOld = old;
553 public void activate(final Set<String> newOne, final AllVcsesI vcsesI) {
554 final Set<String> toAdd = notInBottom(newOne, myOld);
555 final Set<String> toRemove = notInBottom(myOld, newOne);
556 if (toAdd != null) {
557 for (String s : toAdd) {
558 final AbstractVcs vcs = vcsesI.getByName(s);
559 if (vcs != null) {
560 try {
561 vcs.doActivate();
563 catch (VcsException e) {
564 // actually is not thrown (AbstractVcs#actualActivate())
566 } else {
567 LOG.info("Error: activating non existing vcs: " + s);
571 if (toRemove != null) {
572 for (String s : toRemove) {
573 final AbstractVcs vcs = vcsesI.getByName(s);
574 if (vcs != null) {
575 try {
576 vcs.doDeactivate();
578 catch (VcsException e) {
579 // actually is not thrown (AbstractVcs#actualDeactivate())
581 } else {
582 LOG.info("Error: removing non existing vcs: " + s);
588 @Nullable
589 private Set<String> notInBottom(final Set<String> top, final Set<String> bottom) {
590 Set<String> notInBottom = null;
591 for (String topItem : top) {
592 // omit empty vcs: not a vcs
593 if (topItem.trim().length() == 0) continue;
595 if (! bottom.contains(topItem)) {
596 if (notInBottom == null) {
597 notInBottom = new HashSet<String>();
599 notInBottom.add(topItem);
602 return notInBottom;
606 public boolean haveActiveVcs(final String name) {
607 synchronized (myLock) {
608 return myVcsToPaths.containsKey(name);
612 @Modification
613 public void beingUnregistered(final String name) {
614 synchronized (myLock) {
615 keepActiveVcs(new Runnable() {
616 public void run() {
617 final List<VcsDirectoryMapping> removed = myVcsToPaths.remove(name);
618 sortedMappingsByMap();
623 mappingsChanged();
626 private static class MySetMappingsPreProcessor {
627 private List<VcsDirectoryMapping> myItems;
628 private List<VcsDirectoryMapping> myItemsCopy;
629 private Map<VcsDirectoryMapping, LocalFileSystem.WatchRequest> myRequests;
631 public MySetMappingsPreProcessor(final List<VcsDirectoryMapping> items) {
632 myItems = items;
635 public List<VcsDirectoryMapping> getItemsCopy() {
636 return myItemsCopy;
639 public Map<VcsDirectoryMapping, LocalFileSystem.WatchRequest> getRequests() {
640 return myRequests;
643 public void invoke() {
644 if (myItems.isEmpty()) {
645 myItemsCopy = Collections.singletonList(new VcsDirectoryMapping("", ""));
646 myRequests = Collections.emptyMap();
647 } else {
648 myRequests = new HashMap<VcsDirectoryMapping, LocalFileSystem.WatchRequest>();
650 for (VcsDirectoryMapping item : myItems) {
651 final LocalFileSystem.WatchRequest request = addWatchRequest(item);
652 if (request != null) {
653 myRequests.put(item, request);
656 myItemsCopy = myItems;
661 private @interface Modification {