e52d3cc9e1fd6da9b72ca84eb55559d9e85a1a79
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / impl / projectlevelman / NewMappings.java
1 /*
2  * Copyright 2000-2009 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 package com.intellij.openapi.vcs.impl.projectlevelman;
17
18 import com.intellij.openapi.application.ApplicationManager;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.project.DumbAwareRunnable;
21 import com.intellij.openapi.project.Project;
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.impl.DefaultVcsRootPolicy;
30 import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl;
31 import com.intellij.openapi.vcs.impl.VcsInitObject;
32 import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
33 import com.intellij.openapi.vfs.LocalFileSystem;
34 import com.intellij.openapi.vfs.VirtualFile;
35 import com.intellij.util.containers.Convertor;
36 import com.intellij.util.messages.MessageBus;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39
40 import java.util.*;
41
42 public class NewMappings {
43   private final static Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.impl.projectlevelman.NewMappings");
44   private final Object myLock;
45
46   // vcs to mappings
47   private final Map<String, List<VcsDirectoryMapping>> myVcsToPaths;
48   private AbstractVcs[] myActiveVcses;
49   private VcsDirectoryMapping[] mySortedMappings;
50   private FileWatchRequestsManager myFileWatchRequestsManager;
51
52   private final DefaultVcsRootPolicy myDefaultVcsRootPolicy;
53   private final MessageBus myMessageBus;
54   private final FileStatusManager myFileStatusManager;
55   private final Project myProject;
56
57   private boolean myActivated;
58
59   public NewMappings(final Project project, final MessageBus messageBus,
60                      final ProjectLevelVcsManagerImpl vcsManager, FileStatusManager fileStatusManager) {
61     myProject = project;
62     myMessageBus = messageBus;
63     myFileStatusManager = fileStatusManager;
64     myLock = new Object();
65     myVcsToPaths = new HashMap<String, List<VcsDirectoryMapping>>();
66     myFileWatchRequestsManager = new FileWatchRequestsManager(myProject, this, LocalFileSystem.getInstance());
67     myDefaultVcsRootPolicy = DefaultVcsRootPolicy.getInstance(project);
68     myActiveVcses = new AbstractVcs[0];
69
70     if (! myProject.isDefault()) {
71       final ArrayList<VcsDirectoryMapping> listStr = new ArrayList<VcsDirectoryMapping>();
72       final VcsDirectoryMapping mapping = new VcsDirectoryMapping("", "");
73       listStr.add(mapping);
74       myVcsToPaths.put("", listStr);
75       mySortedMappings = new VcsDirectoryMapping[] {mapping};
76     } else {
77       mySortedMappings = VcsDirectoryMapping.EMPTY_ARRAY;
78     }
79     myActivated = false;
80
81     vcsManager.addInitializationRequest(VcsInitObject.MAPPINGS, new DumbAwareRunnable() {
82       public void run() {
83         activateActiveVcses();
84       }
85     });
86   }
87
88   // for tests
89   public void setFileWatchRequestsManager(FileWatchRequestsManager fileWatchRequestsManager) {
90     assert ApplicationManager.getApplication().isUnitTestMode();
91     myFileWatchRequestsManager = fileWatchRequestsManager;
92   }
93
94   public AbstractVcs[] getActiveVcses() {
95     synchronized (myLock) {
96       final AbstractVcs[] result = new AbstractVcs[myActiveVcses.length];
97       System.arraycopy(myActiveVcses, 0, result, 0, myActiveVcses.length);
98       return result;
99     }
100   }
101
102   public boolean hasActiveVcss() {
103     synchronized (myLock) {
104       return myActiveVcses.length > 0;
105     }
106   }
107
108   public void activateActiveVcses() {
109     synchronized (myLock) {
110       if (myActivated) return;
111       myActivated = true;
112     }
113     keepActiveVcs(EmptyRunnable.getInstance());
114     mappingsChanged();
115   }
116
117   @Modification
118   public void setMapping(final String path, final String activeVcsName) {
119     LOG.debug("setMapping path = '" + path + "' vcs = " + activeVcsName);
120     final VcsDirectoryMapping newMapping = new VcsDirectoryMapping(path, activeVcsName);
121     // do not add duplicates
122     synchronized (myLock) {
123       if (myVcsToPaths.containsKey(activeVcsName)) {
124         final List<VcsDirectoryMapping> vcsDirectoryMappings = myVcsToPaths.get(activeVcsName);
125         if ((vcsDirectoryMappings != null) && (vcsDirectoryMappings.contains(newMapping))) {
126           return;
127         }
128       }
129
130     }
131
132     final Ref<Boolean> switched = new Ref<Boolean>(Boolean.FALSE);
133     keepActiveVcs(new Runnable() {
134       public void run() {
135         // sorted -> map. sorted mappings are NOT changed;
136         switched.set(trySwitchVcs(path, activeVcsName));
137         if (! switched.get().booleanValue()) {
138           final List<VcsDirectoryMapping> newList = listForVcsFromMap(newMapping.getVcs());
139           newList.add(newMapping);
140           sortedMappingsByMap();
141         }
142       }
143     });
144
145     mappingsChanged();
146   }
147
148   private void keepActiveVcs(final Runnable runnable) {
149     final MyVcsActivator activator;
150     synchronized (myLock) {
151       if (! myActivated) {
152         runnable.run();
153         return;
154       }
155       final HashSet<String> old = new HashSet<String>();
156       for (AbstractVcs activeVcs : myActiveVcses) {
157         old.add(activeVcs.getName());
158       }
159       activator = new MyVcsActivator(old);
160       runnable.run();
161       restoreActiveVcses();
162     }
163     activator.activate(myVcsToPaths.keySet(), AllVcses.getInstance(myProject));
164   }
165
166   private void restoreActiveVcses() {
167     synchronized (myLock) {
168       final Set<String> set = myVcsToPaths.keySet();
169       final List<AbstractVcs> list = new ArrayList<AbstractVcs>(set.size());
170       for (String s : set) {
171         if (s.trim().length() == 0) continue;
172         final AbstractVcs vcs = AllVcses.getInstance(myProject).getByName(s);
173         if (vcs != null) {
174           list.add(vcs);
175         }
176       }
177       myActiveVcses = list.toArray(new AbstractVcs[list.size()]);
178     }
179   }
180
181   public void mappingsChanged() {
182     myMessageBus.syncPublisher(ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED).directoryMappingChanged();
183     myFileStatusManager.fileStatusesChanged();
184     myFileWatchRequestsManager.ping();
185   }
186
187   @Modification
188   public void setDirectoryMappings(final List<VcsDirectoryMapping> items) {
189     LOG.debug("setDirectoryMappings, size: " + items.size());
190     MySetMappingsPreProcessor setMappingsPreProcessor = new MySetMappingsPreProcessor(items);
191     setMappingsPreProcessor.invoke();
192
193     final List<VcsDirectoryMapping> itemsCopy;
194     if (items.isEmpty()) {
195       itemsCopy = Collections.singletonList(new VcsDirectoryMapping("", ""));
196     } else {
197       itemsCopy = items;
198     }
199
200     keepActiveVcs(new Runnable() {
201       public void run() {
202         myVcsToPaths.clear();
203         for (VcsDirectoryMapping mapping : itemsCopy) {
204           listForVcsFromMap(mapping.getVcs()).add(mapping);
205         }
206         sortedMappingsByMap();
207       }
208     });
209
210     mappingsChanged();
211   }
212
213   @Nullable
214   public VcsDirectoryMapping getMappingFor(VirtualFile file) {
215     if (file == null) return null;
216     if (! file.isInLocalFileSystem()) {
217       return null;
218     }
219
220     return getMappingFor(file, myDefaultVcsRootPolicy.getMatchContext(file));
221   }
222
223   @Nullable
224   public VcsDirectoryMapping getMappingFor(final VirtualFile file, final Object matchContext) {
225     // performance: calculate file path just once, rather than once per mapping
226     String path = file.getPath();
227     final String systemIndependPath = FileUtil.toSystemIndependentName((file.isDirectory() && (! path.endsWith("/"))) ? (path + "/") : path);
228
229     synchronized (myLock) {
230       for (int i = mySortedMappings.length - 1; i >= 0; -- i) {
231         final VcsDirectoryMapping mapping = mySortedMappings[i];
232         if (fileMatchesMapping(file, matchContext, systemIndependPath, mapping)) {
233           return mapping;
234         }
235       }
236       return null;
237     }
238   }
239
240   @Nullable
241   public String getVcsFor(@NotNull VirtualFile file) {
242     VcsDirectoryMapping mapping = getMappingFor(file);
243     if (mapping == null) {
244       return null;
245     }
246     return mapping.getVcs();
247   }
248
249   private boolean fileMatchesMapping(final VirtualFile file, final Object matchContext, final String systemIndependPath, final VcsDirectoryMapping mapping) {
250     if (mapping.getDirectory().length() == 0) {
251       return myDefaultVcsRootPolicy.matchesDefaultMapping(file, matchContext);
252     }
253     return FileUtil.startsWith(systemIndependPath, mapping.systemIndependentPath());
254   }
255
256   public List<VirtualFile> getMappingsAsFilesUnderVcs(final AbstractVcs vcs) {
257     final List<VirtualFile> result = new ArrayList<VirtualFile>();
258     final String vcsName = vcs.getName();
259
260     final List<VcsDirectoryMapping> mappings;
261     synchronized (myLock) {
262       final List<VcsDirectoryMapping> vcsMappings = myVcsToPaths.get(vcsName);
263       if (vcsMappings == null) return result;
264       mappings = new ArrayList<VcsDirectoryMapping>(vcsMappings);
265     }
266
267     for (VcsDirectoryMapping mapping : mappings) {
268       if (mapping.isDefaultMapping()) {
269         // todo callback here; don't like it
270         myDefaultVcsRootPolicy.addDefaultVcsRoots(this, vcsName, result);
271       } else {
272         final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(mapping.getDirectory());
273         if (file != null) {
274           result.add(file);
275         }
276       }
277     }
278     return result;
279   }
280
281   @Modification
282   public void disposeMe() {
283     LOG.debug("dipose me");
284     clearImpl();
285   }
286
287   @Modification
288   public void clear() {
289     LOG.debug("clear");
290     clearImpl();
291
292     mappingsChanged();
293   }
294
295   private void clearImpl() {
296     // if vcses were not mapped, there's nothing to clear
297     if ((myActiveVcses ==  null) || (myActiveVcses.length == 0)) return;
298
299     keepActiveVcs(new Runnable() {
300       public void run() {
301         myVcsToPaths.clear();
302         myActiveVcses = new AbstractVcs[0];
303         mySortedMappings = VcsDirectoryMapping.EMPTY_ARRAY;
304       }
305     });
306     myFileWatchRequestsManager.ping();
307   }
308
309   public List<VcsDirectoryMapping> getDirectoryMappings() {
310     synchronized (myLock) {
311       return Arrays.asList(mySortedMappings);
312     }
313   }
314
315   public List<VcsDirectoryMapping> getDirectoryMappings(String vcsName) {
316     synchronized (myLock) {
317       final List<VcsDirectoryMapping> mappings = myVcsToPaths.get(vcsName);
318       return mappings == null ? new ArrayList<VcsDirectoryMapping>() : new ArrayList<VcsDirectoryMapping>(mappings);
319     }
320   }
321
322   public void cleanupMappings() {
323     synchronized (myLock) {
324       removeRedundantMappings();
325     }
326     myFileWatchRequestsManager.ping();
327   }
328
329   @Nullable
330   public String haveDefaultMapping() {
331     synchronized (myLock) {
332       // empty mapping MUST be first
333       if (mySortedMappings.length == 0) return null;
334       return mySortedMappings[0].isDefaultMapping() ? mySortedMappings[0].getVcs() : null;
335     }
336   }
337
338   public boolean isEmpty() {
339     synchronized (myLock) {
340       return mySortedMappings.length == 0;
341     }
342   }
343
344   @Modification
345   public void removeDirectoryMapping(final VcsDirectoryMapping mapping) {
346     LOG.debug("remove mapping: " + mapping.getDirectory());
347
348     keepActiveVcs(new Runnable() {
349       public void run() {
350         if (removeVcsFromMap(mapping, mapping.getVcs())) {
351           sortedMappingsByMap();
352         }
353       }
354     });
355
356     mappingsChanged();
357   }
358
359   // todo area for optimization
360   private void removeRedundantMappings() {
361     final LocalFileSystem lfs = LocalFileSystem.getInstance();
362     final AllVcsesI allVcses = AllVcses.getInstance(myProject);
363
364     for (Iterator<String> iterator = myVcsToPaths.keySet().iterator(); iterator.hasNext();) {
365       final String vcsName = iterator.next();
366       final List<VcsDirectoryMapping> mappings = myVcsToPaths.get(vcsName);
367
368       final List<Pair<VirtualFile, VcsDirectoryMapping>> objects = ObjectsConvertor.convert(mappings,
369         new Convertor<VcsDirectoryMapping, Pair<VirtualFile, VcsDirectoryMapping>>() {
370         public Pair<VirtualFile, VcsDirectoryMapping> convert(final VcsDirectoryMapping dm) {
371           VirtualFile vf = lfs.findFileByPath(dm.getDirectory());
372           if (vf == null) {
373             vf = lfs.refreshAndFindFileByPath(dm.getDirectory());
374           }
375           return vf == null ? null : new Pair<VirtualFile, VcsDirectoryMapping>(vf, dm);
376         }
377       }, ObjectsConvertor.NOT_NULL);
378
379       final List<Pair<VirtualFile, VcsDirectoryMapping>> filteredFiles;
380       // todo static
381       final Convertor<Pair<VirtualFile, VcsDirectoryMapping>, VirtualFile> fileConvertor =
382         new Convertor<Pair<VirtualFile, VcsDirectoryMapping>, VirtualFile>() {
383           public VirtualFile convert(Pair<VirtualFile, VcsDirectoryMapping> o) {
384             return o.getFirst();
385           }
386         };
387       if (StringUtil.isEmptyOrSpaces(vcsName)) {
388         filteredFiles = AbstractVcs.filterUniqueRootsDefault(objects, fileConvertor);
389       } else {
390         final AbstractVcs vcs = allVcses.getByName(vcsName);
391         if (vcs == null) {
392           VcsBalloonProblemNotifier.showOverChangesView(myProject, "VCS plugin not found for mapping to : '" + vcsName + "'", MessageType.ERROR);
393           continue;
394         }
395         filteredFiles = vcs.filterUniqueRoots(objects, fileConvertor);
396       }
397
398       final List<VcsDirectoryMapping> filteredMappings =
399         ObjectsConvertor.convert(filteredFiles, new Convertor<Pair<VirtualFile, VcsDirectoryMapping>, VcsDirectoryMapping>() {
400           public VcsDirectoryMapping convert(final Pair<VirtualFile, VcsDirectoryMapping> o) {
401             return o.getSecond();
402           }
403         });
404
405       // to calculate what had been removed
406       mappings.removeAll(filteredMappings);
407
408       if (filteredMappings.isEmpty()) {
409         iterator.remove();
410       } else {
411         mappings.clear();
412         mappings.addAll(filteredMappings);
413       }
414     }
415
416     sortedMappingsByMap();
417   }
418
419   private boolean trySwitchVcs(final String path, final String activeVcsName) {
420     final String fixedPath = FileUtil.toSystemIndependentName(path);
421     for (VcsDirectoryMapping mapping : mySortedMappings) {
422       if (mapping.systemIndependentPath().equals(fixedPath)) {
423         final String oldVcs = mapping.getVcs();
424         if (! oldVcs.equals(activeVcsName)) {
425           migrateVcs(activeVcsName, mapping, oldVcs);
426         }
427         return true;
428       }
429     }
430     return false;
431   }
432
433   private void sortedMappingsByMap() {
434     final List<VcsDirectoryMapping> list = new ArrayList<VcsDirectoryMapping>();
435     for (List<VcsDirectoryMapping> mappingList : myVcsToPaths.values()) {
436       list.addAll(mappingList);
437     }
438     mySortedMappings = list.toArray(new VcsDirectoryMapping[list.size()]);
439     Arrays.sort(mySortedMappings, MyMappingsComparator.getInstance());
440   }
441
442   private void migrateVcs(String activeVcsName, VcsDirectoryMapping mapping, String oldVcs) {
443     mapping.setVcs(activeVcsName);
444
445     removeVcsFromMap(mapping, oldVcs);
446
447     final List<VcsDirectoryMapping> newList = listForVcsFromMap(activeVcsName);
448     newList.add(mapping);
449   }
450
451   private boolean removeVcsFromMap(VcsDirectoryMapping mapping, String oldVcs) {
452     final List<VcsDirectoryMapping> oldList = myVcsToPaths.get(oldVcs);
453     if (oldList == null) return false;
454
455     final boolean result = oldList.remove(mapping);
456     if (oldList.isEmpty()) {
457       myVcsToPaths.remove(oldVcs);
458     }
459     return result;
460   }
461
462   // todo don't like it
463   private List<VcsDirectoryMapping> listForVcsFromMap(String activeVcsName) {
464     List<VcsDirectoryMapping> newList = myVcsToPaths.get(activeVcsName);
465     if (newList == null) {
466       newList = new ArrayList<VcsDirectoryMapping>();
467       myVcsToPaths.put(activeVcsName, newList);
468     }
469     return newList;
470   }
471
472   private static class MyMappingsComparator implements Comparator<VcsDirectoryMapping> {
473     private static final MyMappingsComparator ourInstance = new MyMappingsComparator();
474
475     public static MyMappingsComparator getInstance() {
476       return ourInstance;
477     }
478
479     public int compare(VcsDirectoryMapping m1, VcsDirectoryMapping m2) {
480       return m1.getDirectory().compareTo(m2.getDirectory());
481     }
482   }
483
484   private static class MyVcsActivator {
485     private final Set<String> myOld;
486
487     public MyVcsActivator(final Set<String> old) {
488       myOld = old;
489     }
490
491     public void activate(final Set<String> newOne, final AllVcsesI vcsesI) {
492       final Set<String> toAdd = notInBottom(newOne, myOld);
493       final Set<String> toRemove = notInBottom(myOld, newOne);
494       if (toAdd != null) {
495         for (String s : toAdd) {
496           final AbstractVcs vcs = vcsesI.getByName(s);
497           if (vcs != null) {
498             try {
499               vcs.doActivate();
500             }
501             catch (VcsException e) {
502               // actually is not thrown (AbstractVcs#actualActivate())
503             }
504           } else {
505             LOG.info("Error: activating non existing vcs: " + s);
506           }
507         }
508       }
509       if (toRemove != null) {
510         for (String s : toRemove) {
511           final AbstractVcs vcs = vcsesI.getByName(s);
512           if (vcs != null) {
513             try {
514               vcs.doDeactivate();
515             }
516             catch (VcsException e) {
517               // actually is not thrown (AbstractVcs#actualDeactivate())
518             }
519           } else {
520             LOG.info("Error: removing non existing vcs: " + s);
521           }
522         }
523       }
524     }
525
526     @Nullable
527     private Set<String> notInBottom(final Set<String> top, final Set<String> bottom) {
528       Set<String> notInBottom = null;
529       for (String topItem : top) {
530         // omit empty vcs: not a vcs
531         if (topItem.trim().length() == 0) continue;
532
533         if (! bottom.contains(topItem)) {
534           if (notInBottom == null) {
535             notInBottom = new HashSet<String>();
536           }
537           notInBottom.add(topItem);
538         }
539       }
540       return notInBottom;
541     }
542   }
543
544   public boolean haveActiveVcs(final String name) {
545     synchronized (myLock) {
546       return myVcsToPaths.containsKey(name);
547     }
548   }
549
550   @Modification
551   public void beingUnregistered(final String name) {
552     synchronized (myLock) {
553       keepActiveVcs(new Runnable() {
554         public void run() {
555           final List<VcsDirectoryMapping> removed = myVcsToPaths.remove(name);
556           sortedMappingsByMap();
557         }
558       });
559     }
560
561     mappingsChanged();
562   }
563
564   private static class MySetMappingsPreProcessor {
565     private final List<VcsDirectoryMapping> myItems;
566     private List<VcsDirectoryMapping> myItemsCopy;
567
568     public MySetMappingsPreProcessor(final List<VcsDirectoryMapping> items) {
569       myItems = items;
570     }
571
572     public List<VcsDirectoryMapping> getItemsCopy() {
573       return myItemsCopy;
574     }
575
576     public void invoke() {
577       if (myItems.isEmpty()) {
578         myItemsCopy = Collections.singletonList(new VcsDirectoryMapping("", ""));
579       } else {
580         myItemsCopy = myItems;
581       }
582     }
583   }
584
585   private @interface Modification {
586   }
587
588   public List<VirtualFile> getDefaultRoots() {
589     synchronized (myLock) {
590       final String defaultVcs = haveDefaultMapping();
591       if (defaultVcs == null) return Collections.emptyList();
592       final List<VirtualFile> list = new ArrayList<VirtualFile>();
593       myDefaultVcsRootPolicy.addDefaultVcsRoots(this, defaultVcs, list);
594       if (StringUtil.isEmptyOrSpaces(defaultVcs)) {
595         return AbstractVcs.filterUniqueRootsDefault(list, Convertor.SELF);
596       } else {
597         final AbstractVcs vcs = AllVcses.getInstance(myProject).getByName(defaultVcs);
598         if (vcs == null) {
599           return AbstractVcs.filterUniqueRootsDefault(list, Convertor.SELF);
600         }
601         return vcs.filterUniqueRoots(list, Convertor.SELF);
602       }
603     }
604   }
605 }