a012c477b30647f5fffbe8efa876c01a89910f90
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / ChangeListWorker.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.changes;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.project.Project;
20 import com.intellij.openapi.util.Comparing;
21 import com.intellij.openapi.util.Pair;
22 import com.intellij.openapi.vcs.AbstractVcs;
23 import com.intellij.openapi.vcs.FilePath;
24 import com.intellij.openapi.vcs.FileStatus;
25 import com.intellij.openapi.vcs.VcsKey;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.util.IncorrectOperationException;
28 import com.intellij.util.containers.MultiMap;
29 import org.jetbrains.annotations.NotNull;
30 import org.jetbrains.annotations.Nullable;
31
32 import java.io.File;
33 import java.util.*;
34
35 /** should work under _external_ lock
36 * just logic here: do modifications to group of change lists
37 */
38 public class ChangeListWorker implements ChangeListsWriteOperations {
39   private final static Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.ChangeListWorker");
40
41   private final Project myProject;
42   private final Map<String, LocalChangeList> myMap;
43   // in fact, a kind of local change
44   private final DeletedFilesHolder myLocallyDeleted;
45   private final SwitchedFileHolder mySwitchedHolder;
46   private LocalChangeList myDefault;
47
48   private ChangeListsIndexes myIdx;
49   private final ChangesDelta myDelta;
50
51   public ChangeListWorker(final Project project, final PlusMinus<Pair<String, AbstractVcs>> deltaListener) {
52     myProject = project;
53     myMap = new HashMap<String, LocalChangeList>();
54     myIdx = new ChangeListsIndexes();
55     myLocallyDeleted = new DeletedFilesHolder();
56     mySwitchedHolder = new SwitchedFileHolder(project, FileHolder.HolderType.SWITCHED);
57
58     myDelta = new ChangesDelta(project, deltaListener);
59   }
60
61   private ChangeListWorker(final ChangeListWorker worker) {
62     myProject = worker.myProject;
63     myMap = new HashMap<String, LocalChangeList>();
64     myIdx = new ChangeListsIndexes(worker.myIdx);
65     myLocallyDeleted = worker.myLocallyDeleted.copy();
66     mySwitchedHolder = (SwitchedFileHolder) worker.mySwitchedHolder.copy();
67     myDelta = worker.myDelta;
68     
69     LocalChangeList defaultList = null;
70     for (LocalChangeList changeList : worker.myMap.values()) {
71       final LocalChangeList copy = changeList.copy();
72       
73       final String changeListName = copy.getName();
74       myMap.put(changeListName, copy);
75       if (copy.isDefault()) {
76         defaultList = copy;
77       }
78     }
79     if (defaultList == null) {
80       LOG.info("default list not found when copy");
81       defaultList = myMap.get(worker.getDefaultListName());
82     }
83
84     if (defaultList == null) {
85       LOG.info("default list not found when copy in original object too");
86       if (! myMap.isEmpty()) {
87         defaultList = myMap.values().iterator().next();
88       } else {
89         // can be when there's no vcs configured
90         ///LOG.error("no changelists at all");
91       }
92     }
93     myDefault = defaultList;
94   }
95
96   public void takeData(@NotNull final ChangeListWorker worker) {
97     myMap.clear();
98     myMap.putAll(worker.myMap);
99     myDefault = worker.myDefault;
100     
101     myDelta.step(myIdx, worker.myIdx);
102     myIdx = new ChangeListsIndexes(worker.myIdx);
103     // todo +-
104     myLocallyDeleted.takeFrom(worker.myLocallyDeleted);
105     mySwitchedHolder.takeFrom(worker.mySwitchedHolder);
106   }
107
108   public ChangeListWorker copy() {
109     return new ChangeListWorker(this);
110   }
111
112   public boolean findListByName(@NotNull final String name) {
113     return myMap.containsKey(name);
114   }
115
116   @Nullable
117   public LocalChangeList getCopyByName(final String name) {
118     return myMap.get(name);
119   }
120
121   @Nullable
122   public LocalChangeList getChangeList(String id) {
123     for (LocalChangeList changeList : myMap.values()) {
124       if (changeList.getId().equals(id)) {
125         return changeList.copy();
126       }
127     }
128     return null;
129   }
130
131   /**
132    * @return if list with name exists, return previous default list name or null of there wasn't previous
133    */
134   @Nullable
135   public String setDefault(final String name) {
136     final LocalChangeList newDefault = myMap.get(name);
137     if (newDefault == null) {
138       return null;
139     }
140     String previousName = null;
141     if (myDefault != null) {
142       ((LocalChangeListImpl) myDefault).setDefault(false);
143       correctChangeListEditHandler(myDefault);
144       previousName = myDefault.getName();
145     }
146
147     ((LocalChangeListImpl) newDefault).setDefault(true);
148     myDefault = newDefault;
149
150     return previousName;
151   }
152
153   public boolean setReadOnly(final String name, final boolean value) {
154     final LocalChangeList list = myMap.get(name);
155     if (list != null) {
156       list.setReadOnly(value);
157     }
158     return list != null;
159   }
160
161   public LocalChangeList addChangeList(@NotNull final String name, @Nullable final String description) {
162     return addChangeList(null, name, description, false);
163   }
164
165   LocalChangeList addChangeList(String id, @NotNull final String name, @Nullable final String description, final boolean inUpdate) {
166     final boolean contains = myMap.containsKey(name);
167     LOG.assertTrue(! contains, "Attempt to create duplicate changelist " + name);
168     final LocalChangeListImpl newList = (LocalChangeListImpl) LocalChangeList.createEmptyChangeList(myProject, name);
169
170     if (description != null) {
171       newList.setCommentImpl(description);
172     }
173     if (id != null) {
174       newList.setId(id);
175     }
176     myMap.put(name, newList);
177     if (inUpdate) {
178       // scope is not important: nothing had been added jet, nothing to move to "old state" members
179       newList.startProcessingChanges(myProject, null);
180     }
181     return newList.copy();
182   }
183
184   public boolean addChangeToList(@NotNull final String name, final Change change, final VcsKey vcsKey) {
185     final LocalChangeList changeList = myMap.get(name);
186     if (changeList != null) {
187       ((LocalChangeListImpl) changeList).addChange(change);
188       myIdx.changeAdded(change, vcsKey);
189       correctChangeListEditHandler(changeList);
190     }
191     return changeList != null;
192   }
193
194   public void addChangeToCorrespondingList(final Change change, final VcsKey vcsKey) {
195     assert myDefault != null;
196     for (LocalChangeList list : myMap.values()) {
197       if (list.isDefault()) continue;
198       if (((LocalChangeListImpl) list).processChange(change)) {
199         myIdx.changeAdded(change, vcsKey);
200         correctChangeListEditHandler(list);
201         return;
202       }
203     }
204     ((LocalChangeListImpl) myDefault).processChange(change);
205     myIdx.changeAdded(change, vcsKey);
206     correctChangeListEditHandler(myDefault);
207   }
208
209   public boolean removeChangeList(@NotNull String name) {
210     final LocalChangeList list = myMap.get(name);
211     if (list == null) {
212       return false;
213     }
214     if (list.isDefault()) {
215       throw new RuntimeException(new IncorrectOperationException("Cannot remove default changelist"));
216     }
217     final String listName = list.getName();
218
219     for (Change change : list.getChanges()) {
220       ((LocalChangeListImpl) myDefault).addChange(change);
221     }
222
223     myMap.remove(listName);
224     return true;
225   }
226
227   void initialized() {
228     for (LocalChangeList list : myMap.values()) {
229       correctChangeListEditHandler(list);
230     }
231   }
232
233   // since currently it is only for P4, "composite" edit handler is not created - it does not make sence
234   private void correctChangeListEditHandler(final LocalChangeList list) {
235   }
236
237   @Nullable
238   public MultiMap<LocalChangeList, Change> moveChangesTo(final String name, final Change[] changes) {
239     final LocalChangeListImpl changeList = (LocalChangeListImpl) myMap.get(name);
240     if (changeList != null) {
241       final MultiMap<LocalChangeList, Change> result = new MultiMap<LocalChangeList, Change>();
242       for (LocalChangeList list : myMap.values()) {
243         if (list.equals(changeList)) continue;
244         for (Change change : changes) {
245           final Change removedChange = ((LocalChangeListImpl)list).removeChange(change);
246           if (removedChange != null) {
247             correctChangeListEditHandler(list);
248
249             changeList.addChange(removedChange);
250             result.putValue(list, removedChange);
251           }
252         }
253       }
254       correctChangeListEditHandler(changeList);
255       return result;
256     }
257     return null;
258   }
259
260   public boolean editName(@NotNull final String fromName, @NotNull final String toName) {
261     if (fromName.equals(toName)) return false;
262     final LocalChangeList list = myMap.get(fromName);
263     final boolean canEdit = list != null && (!list.isReadOnly());
264     if (canEdit) {
265       final LocalChangeListImpl listImpl = (LocalChangeListImpl) list;
266       listImpl.setNameImpl(toName);
267       myMap.remove(fromName);
268       myMap.put(toName, list);
269       final ChangeListEditHandler editHandler = listImpl.getEditHandler();
270       if (editHandler != null) {
271         listImpl.setCommentImpl(editHandler.changeCommentOnChangeName(toName, listImpl.getComment()));
272       }
273     }
274     return canEdit;
275   }
276
277   @Nullable
278   public String editComment(@NotNull final String fromName, final String newComment) {
279     final LocalChangeList list = myMap.get(fromName);
280     if (list != null) {
281       final String oldComment = list.getComment();
282       if (! Comparing.equal(oldComment, newComment)) {
283         final LocalChangeListImpl listImpl = (LocalChangeListImpl) list;
284         listImpl.setCommentImpl(newComment);
285         final ChangeListEditHandler editHandler = listImpl.getEditHandler();
286         if (editHandler != null) {
287           listImpl.setNameImpl(editHandler.changeNameOnChangeComment(listImpl.getName(), listImpl.getComment()));
288           if (! fromName.equals(listImpl.getName())) {
289             myMap.remove(fromName);
290             myMap.put(listImpl.getName(), list);
291           }
292         }
293       }
294       return oldComment;
295     }
296     return null;
297   }
298
299   public boolean isEmpty() {
300     return myMap.isEmpty();
301   }
302
303   @Nullable
304   public LocalChangeList getDefaultListCopy() {
305     return myDefault == null ? null : myDefault.copy();
306   }
307
308   public boolean isDefaultList(LocalChangeList list) {
309     return myDefault != null && list.getId().equals(myDefault.getId());  
310   }
311
312   public Project getProject() {
313     return myProject;
314   }
315
316   // called NOT under ChangeListManagerImpl lock
317   public void notifyStartProcessingChanges(final VcsAppendableDirtyScope scope) {
318     final Collection<Change> oldChanges = new ArrayList<Change>();
319     for (LocalChangeList list : myMap.values()) {
320       final Collection<Change> affectedChanges = ((LocalChangeListImpl)list).startProcessingChanges(myProject, scope);
321       if (! affectedChanges.isEmpty()) {
322         oldChanges.addAll(affectedChanges);
323       }
324     }
325     for (Change change : oldChanges) {
326       myIdx.changeRemoved(change);
327     }
328     // scope should be modified for correct moves tracking
329     correctScopeForMoves(scope, oldChanges);
330
331     myLocallyDeleted.cleanScope(scope);
332     mySwitchedHolder.cleanScope(scope);
333   }
334
335   private void correctScopeForMoves(final VcsAppendableDirtyScope scope, final Collection<Change> changes) {
336     if (scope == null) return;
337     for (Change change : changes) {
338       if (change.isMoved() || change.isRenamed()) {
339         scope.addDirtyFile(change.getBeforeRevision().getFile());
340         scope.addDirtyFile(change.getAfterRevision().getFile());
341       }
342     }
343   }
344
345   public void notifyDoneProcessingChanges(final ChangeListListener dispatcher) {
346     List<ChangeList> changedLists = new ArrayList<ChangeList>();
347     final Map<LocalChangeListImpl, List<Change>> removedChanges = new HashMap<LocalChangeListImpl, List<Change>>();
348       for (LocalChangeList list : myMap.values()) {
349         final List<Change> removed = new ArrayList<Change>();
350         final LocalChangeListImpl listImpl = (LocalChangeListImpl)list;
351         if (listImpl.doneProcessingChanges(removed)) {
352           changedLists.add(list);
353         }
354         if (! removed.isEmpty()) {
355           removedChanges.put(listImpl, removed);
356         }
357       }
358     for (Map.Entry<LocalChangeListImpl, List<Change>> entry : removedChanges.entrySet()) {
359       dispatcher.changesRemoved(entry.getValue(), entry.getKey());
360     }
361     for(ChangeList changeList: changedLists) {
362       dispatcher.changeListChanged(changeList);
363     }
364     mySwitchedHolder.calculateChildren();
365   }
366
367   public List<LocalChangeList> getListsCopy() {
368     final List<LocalChangeList> result = new ArrayList<LocalChangeList>();
369     for (LocalChangeList list : myMap.values()) {
370       result.add(list.copy());
371     }
372     return result;
373   }
374
375   public String getDefaultListName() {
376     return myDefault == null ? null : myDefault.getName();
377   }
378
379   public List<File> getAffectedPaths() {
380     return myIdx.getAffectedPaths();
381   }
382
383   @NotNull
384   public List<VirtualFile> getAffectedFiles() {
385     final List<VirtualFile> result = new ArrayList<VirtualFile>();
386     for (LocalChangeList list : myMap.values()) {
387       for (Change change : list.getChanges()) {
388         final ContentRevision before = change.getBeforeRevision();
389         final ContentRevision after = change.getAfterRevision();
390         if (before != null) {
391           final VirtualFile file = before.getFile().getVirtualFile();
392           if (file != null) {
393             result.add(file);
394           }
395         }
396         if (after != null) {
397           final VirtualFile file = after.getFile().getVirtualFile();
398           if (file != null) {
399             result.add(file);
400           }
401         }
402       }
403     }
404     return result;
405   }
406
407   public LocalChangeList getListCopy(@NotNull final VirtualFile file) {
408     for (LocalChangeList list : myMap.values()) {
409       for (Change change : list.getChanges()) {
410         if (change.getAfterRevision() != null &&
411             Comparing.equal(change.getAfterRevision().getFile().getVirtualFile(), file)) {
412           return list.copy();
413         }
414         if (change.getBeforeRevision() != null &&
415             Comparing.equal(change.getBeforeRevision().getFile().getVirtualFile(), file)) {
416           return list.copy();
417         }
418       }
419     }
420     return null;
421   }
422
423   @Nullable
424   public Change getChangeForPath(final FilePath file) {
425     for (LocalChangeList list : myMap.values()) {
426       for (Change change : list.getChanges()) {
427         final ContentRevision afterRevision = change.getAfterRevision();
428         if (afterRevision != null && afterRevision.getFile().equals(file)) {
429           return change;
430         }
431         final ContentRevision beforeRevision = change.getBeforeRevision();
432         if (beforeRevision != null && beforeRevision.getFile().equals(file)) {
433           return change;
434         }
435       }
436     }
437     return null;
438   }
439
440   public FileStatus getStatus(final VirtualFile file) {
441     return myIdx.getStatus(file);
442   }
443
444   public DeletedFilesHolder getLocallyDeleted() {
445     return myLocallyDeleted.copy();
446   }
447
448   public SwitchedFileHolder getSwitchedHolder() {
449     return mySwitchedHolder.copy();
450   }
451
452   public void addSwitched(final VirtualFile file, @NotNull String branchName, final boolean recursive) {
453     mySwitchedHolder.addFile(file, branchName, recursive);
454   }
455
456   public void removeSwitched(final VirtualFile file) {
457     mySwitchedHolder.removeFile(file);
458   }
459
460   public String getBranchForFile(final VirtualFile file) {
461     return mySwitchedHolder.getBranchForFile(file);
462   }
463
464   public boolean isSwitched(final VirtualFile file) {
465     return mySwitchedHolder.containsFile(file);
466   }
467
468   public void addLocallyDeleted(final LocallyDeletedChange change) {
469     myLocallyDeleted.addFile(change);
470   }
471
472   public boolean isContainedInLocallyDeleted(final FilePath filePath) {
473     return myLocallyDeleted.isContainedInLocallyDeleted(filePath);
474   }
475
476   private abstract class ExternalVsInternalChangesIntersection {
477     protected final Collection<Change> myInChanges;
478     protected final Map<Pair<String, String>, LocalChangeList> myInternalMap;
479     protected final LocalChangeList myDefaultCopy;
480     protected final Map<String, LocalChangeList> myIncludedListsCopies;
481
482     protected ExternalVsInternalChangesIntersection(final Collection<Change> inChanges) {
483       myInChanges = inChanges;
484       myInternalMap = new HashMap<Pair<String, String>, LocalChangeList>();
485       myDefaultCopy = myDefault.copy();
486       myIncludedListsCopies = new HashMap<String, LocalChangeList>();
487     }
488
489     private Pair<String, String> keyForChange(final Change change) {
490       final FilePath beforePath = ChangesUtil.getBeforePath(change);
491       final String beforeKey = beforePath == null ? null : beforePath.getIOFile().getAbsolutePath();
492       final FilePath afterPath = ChangesUtil.getAfterPath(change);
493       final String afterKey = afterPath == null ? null : afterPath.getIOFile().getAbsolutePath();
494       return new Pair<String, String>(beforeKey, afterKey);
495     }
496
497     private void preparation() {
498       for (LocalChangeList list : myMap.values()) {
499         final Collection<Change> managerChanges = list.getChanges();
500         final LocalChangeList copy = list.copy();
501         for (Change change : managerChanges) {
502           myInternalMap.put(keyForChange(change), copy);
503         }
504       }
505     }
506
507     protected abstract void processInChange(final Pair<String, String> key, final Change change);
508
509     public void run() {
510       preparation();
511
512       for (Change change : myInChanges) {
513         final Pair<String, String> key = keyForChange(change);
514         processInChange(key, change);
515       }
516     }
517
518     public Map<String, LocalChangeList> getIncludedListsCopies() {
519       return myIncludedListsCopies;
520     }
521   }
522
523   private class GatherChangesVsListsInfo extends ExternalVsInternalChangesIntersection {
524     private final Map<String, List<Change>> myListToChangesMap;
525
526     private GatherChangesVsListsInfo(final Collection<Change> inChanges) {
527       super(inChanges);
528       myListToChangesMap = new HashMap<String, List<Change>>();
529     }
530
531     protected void processInChange(Pair<String, String> key, Change change) {
532       LocalChangeList tmpList = myInternalMap.get(key);
533       if (tmpList == null) {
534         tmpList = myDefaultCopy;
535       }
536       final String tmpName = tmpList.getName();
537       List<Change> list = myListToChangesMap.get(tmpName);
538       if (list == null) {
539         list = new ArrayList<Change>();
540         myListToChangesMap.put(tmpName, list);
541         myIncludedListsCopies.put(tmpName, tmpList);
542       }
543       list.add(change);
544     }
545
546     public Map<String, List<Change>> getListToChangesMap() {
547       return myListToChangesMap;
548     }
549   }
550
551   private class GatherListsFilterValidChanges extends ExternalVsInternalChangesIntersection {
552     private final List<Change> myValidChanges;
553
554     private GatherListsFilterValidChanges(final Collection<Change> inChanges) {
555       super(inChanges);
556       myValidChanges = new ArrayList<Change>();
557     }
558
559     protected void processInChange(Pair<String, String> key, Change change) {
560       final LocalChangeList list = myInternalMap.get(key);
561       if (list != null) {
562         myIncludedListsCopies.put(list.getName(), list);
563         myValidChanges.add(change);
564       }
565     }
566
567     public List<Change> getValidChanges() {
568       return myValidChanges;
569     }
570   }
571
572   @NotNull
573   public Map<String, List<Change>> listsForChanges(final Collection<Change> changes, final Map<String, LocalChangeList> lists) {
574     final GatherChangesVsListsInfo info = new GatherChangesVsListsInfo(changes);
575     info.run();
576     lists.putAll(info.getIncludedListsCopies());
577     return info.getListToChangesMap();
578   }
579
580   @NotNull
581   public Collection<LocalChangeList> getInvolvedListsFilterChanges(final Collection<Change> changes, final List<Change> validChanges) {
582     final GatherListsFilterValidChanges worker = new GatherListsFilterValidChanges(changes);
583     worker.run();
584     validChanges.addAll(worker.getValidChanges());
585     return worker.getIncludedListsCopies().values();
586   }
587
588   @Nullable
589   public LocalChangeList listForChange(final Change change) {
590     for (LocalChangeList list : myMap.values()) {
591       if (list.getChanges().contains(change)) return list.copy();
592     }
593     return null;
594   }
595
596   @Nullable
597   public String listNameIfOnlyOne(final @Nullable Change[] changes) {
598     if (changes == null || changes.length == 0) {
599       return null;
600     }
601
602     final Change first = changes[0];
603
604     for (LocalChangeList list : myMap.values()) {
605       final Collection<Change> listChanges = list.getChanges();
606       if (listChanges.contains(first)) {
607         // must contain all other
608         for (int i = 1; i < changes.length; i++) {
609           final Change change = changes[i];
610           if (! listChanges.contains(change)) {
611             return null;
612           }
613         }
614         return list.getName();
615       }
616     }
617     return null;
618   }
619
620   @NotNull
621   public Collection<Change> getChangesIn(final FilePath dirPath) {
622     List<Change> changes = new ArrayList<Change>();
623     for (ChangeList list : myMap.values()) {
624       for (Change change : list.getChanges()) {
625         final ContentRevision afterRevision = change.getAfterRevision();
626         if (afterRevision != null && afterRevision.getFile().isUnder(dirPath, false)) {
627           changes.add(change);
628           continue;
629         }
630
631         final ContentRevision beforeRevision = change.getBeforeRevision();
632         if (beforeRevision != null && beforeRevision.getFile().isUnder(dirPath, false)) {
633           changes.add(change);
634         }
635       }
636     }
637     return changes;
638   }
639
640   ChangeListManagerGate createSelfGate() {
641     return new MyGate(this);
642   }
643
644   private static class MyGate implements ChangeListManagerGate {
645     private final ChangeListWorker myWorker;
646
647     private MyGate(final ChangeListWorker worker) {
648       myWorker = worker;
649     }
650
651     public List<LocalChangeList> getListsCopy() {
652       return myWorker.getListsCopy();
653     }
654
655     @Nullable
656     public LocalChangeList findChangeList(final String name) {
657       return myWorker.getCopyByName(name);
658     }
659
660     public LocalChangeList addChangeList(final String name, final String comment) {
661       return myWorker.addChangeList(null, name, comment, true);
662     }
663
664     public LocalChangeList findOrCreateList(final String name, final String comment) {
665       LocalChangeList list = myWorker.getCopyByName(name);
666       if (list == null) {
667         list = addChangeList(name, comment);
668       }
669       return list;
670     }
671
672     public void editComment(final String name, final String comment) {
673       myWorker.editComment(name, comment);
674     }
675
676     public void editName(String oldName, String newName) {
677       myWorker.editName(oldName, newName);
678     }
679
680     // todo usage allowed only when..
681     public void moveChanges(String toList, Collection<Change> changes) {
682       myWorker.moveChangesTo(toList, changes.toArray(new Change[changes.size()]));
683     }
684
685     public void deleteIfEmpty(String name) {
686       final LocalChangeList list = myWorker.getCopyByName(name);
687       if ((list != null) && (list.getChanges().isEmpty()) && (! list.isDefault())) {
688         myWorker.removeChangeList(name);
689       }
690     }
691   }
692 }