5891b340dc97f8be0347226bd9857d72837dd3a0
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / committed / ChangesCacheFile.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.committed;
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.Condition;
22 import com.intellij.openapi.util.Pair;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.vcs.*;
25 import com.intellij.openapi.vcs.changes.*;
26 import com.intellij.openapi.vcs.diff.DiffProvider;
27 import com.intellij.openapi.vcs.diff.DiffProviderEx;
28 import com.intellij.openapi.vcs.history.VcsRevisionNumber;
29 import com.intellij.openapi.vcs.update.FileGroup;
30 import com.intellij.openapi.vcs.update.UpdatedFiles;
31 import com.intellij.openapi.vcs.versionBrowser.ChangeBrowserSettings;
32 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.util.Function;
35 import com.intellij.util.containers.ContainerUtil;
36 import com.intellij.vcsUtil.VcsUtil;
37 import org.jetbrains.annotations.NonNls;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import java.io.*;
42 import java.util.*;
43
44 import static com.intellij.openapi.vcs.changes.committed.IncomingChangeState.State.*;
45
46 /**
47  * @author yole
48  */
49 public class ChangesCacheFile {
50   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.committed.ChangesCacheFile");
51   private static final int VERSION = 7;
52
53   private final File myPath;
54   private final File myIndexPath;
55   private RandomAccessFile myStream;
56   private RandomAccessFile myIndexStream;
57   private boolean myStreamsOpen;
58   private final Project myProject;
59   private final AbstractVcs myVcs;
60   private final CachingCommittedChangesProvider myChangesProvider;
61   private final ProjectLevelVcsManager myVcsManager;
62   private final FilePath myRootPath;
63   private final RepositoryLocation myLocation;
64   private Date myFirstCachedDate;
65   private Date myLastCachedDate;
66   private long myFirstCachedChangelist;
67   private long myLastCachedChangelist;
68   private int myIncomingCount;
69   private boolean myHaveCompleteHistory;
70   private boolean myHeaderLoaded;
71   @NonNls private static final String INDEX_EXTENSION = ".index";
72   private static final int INDEX_ENTRY_SIZE = 3*8+2;
73   private static final int HEADER_SIZE = 46;
74
75   public ChangesCacheFile(Project project, File path, AbstractVcs vcs, VirtualFile root, RepositoryLocation location) {
76     reset();
77
78     myProject = project;
79     myPath = path;
80     myIndexPath = new File(myPath.toString() + INDEX_EXTENSION);
81     myVcs = vcs;
82     myChangesProvider = (CachingCommittedChangesProvider) vcs.getCommittedChangesProvider();
83     myVcsManager = ProjectLevelVcsManager.getInstance(project);
84     myRootPath = VcsUtil.getFilePath(root);
85     myLocation = location;
86   }
87
88   private void reset() {
89     final Calendar date = Calendar.getInstance();
90     date.set(2020, Calendar.FEBRUARY, 2);
91     myFirstCachedDate = date.getTime();
92     date.set(1970, Calendar.FEBRUARY, 2);
93     myLastCachedDate = date.getTime();
94     myIncomingCount = 0;
95     myLastCachedChangelist = -1;
96     myFirstCachedChangelist = Long.MAX_VALUE;
97     myHaveCompleteHistory = false;
98     myHeaderLoaded = false;
99   }
100
101   public RepositoryLocation getLocation() {
102     return myLocation;
103   }
104
105   public CachingCommittedChangesProvider getProvider() {
106     return myChangesProvider;
107   }
108
109   public boolean isEmpty() throws IOException {
110     if (!myPath.exists()) {
111       return true;
112     }
113     try {
114       loadHeader();
115     }
116     catch(VersionMismatchException ex) {
117       myPath.delete();
118       myIndexPath.delete();
119       return true;
120     }
121     catch(EOFException ex) {
122       myPath.delete();
123       myIndexPath.delete();
124       return true;
125     }
126
127     return false;
128   }
129
130   public void delete() {
131     FileUtil.delete(myPath);
132     FileUtil.delete(myIndexPath);
133     try {
134       closeStreams();
135     }
136     catch (IOException e) {
137       //
138     }
139   }
140
141   public List<CommittedChangeList> writeChanges(final List<CommittedChangeList> changes) throws IOException {
142     // the list and index are sorted in direct chronological order
143     Collections.sort(changes, new Comparator<CommittedChangeList>() {
144       public int compare(final CommittedChangeList o1, final CommittedChangeList o2) {
145         return Comparing.compare(o1.getCommitDate(), o2.getCommitDate());
146       }
147     });
148     return writeChanges(changes, null);
149   }
150
151   public List<CommittedChangeList> writeChanges(final List<CommittedChangeList> changes, @Nullable final List<Boolean> present) throws IOException {
152     assert present == null || present.size() == changes.size();
153
154     List<CommittedChangeList> result = new ArrayList<CommittedChangeList>(changes.size());
155     boolean wasEmpty = isEmpty();
156     openStreams();
157     try {
158       if (wasEmpty) {
159         myHeaderLoaded = true;
160         writeHeader();
161       }
162       myStream.seek(myStream.length());
163       IndexEntry[] entries = readLastIndexEntries(0, changes.size());
164
165       final Iterator<Boolean> iterator = present == null ? null : present.iterator();
166       for(CommittedChangeList list: changes) {
167         boolean duplicate = false;
168         for(IndexEntry entry: entries) {
169           if (list.getCommitDate().getTime() == entry.date && list.getNumber() == entry.number) {
170             duplicate = true;
171             break;
172           }
173         }
174         if (duplicate) {
175           debug("Skipping duplicate changelist " + list.getNumber());
176           continue;
177         }
178         debug("Writing incoming changelist " + list.getNumber());
179         result.add(list);
180         long position = myStream.getFilePointer();
181         //noinspection unchecked
182         myChangesProvider.writeChangeList(myStream, list);
183         updateCachedRange(list);
184         writeIndexEntry(list.getNumber(), list.getCommitDate().getTime(), position, present == null ? false : iterator.next());
185         myIncomingCount++;
186       }
187       writeHeader();
188       myHeaderLoaded = true;
189     }
190     finally {
191       closeStreams();
192     }
193     return result;
194   }
195
196   private static void debug(@NonNls String message) {
197     LOG.debug(message);
198   }
199
200   private void updateCachedRange(final CommittedChangeList list) {
201     if (list.getCommitDate().getTime() > myLastCachedDate.getTime()) {
202       myLastCachedDate = list.getCommitDate();
203     }
204     if (list.getCommitDate().getTime() < myFirstCachedDate.getTime()) {
205       myFirstCachedDate = list.getCommitDate();
206     }
207     if (list.getNumber() < myFirstCachedChangelist) {
208       myFirstCachedChangelist = list.getNumber();
209     }
210     if (list.getNumber() > myLastCachedChangelist) {
211       myLastCachedChangelist = list.getNumber();
212     }
213   }
214
215   private void writeIndexEntry(long number, long date, long offset, boolean completelyDownloaded) throws IOException {
216     myIndexStream.writeLong(number);
217     myIndexStream.writeLong(date);
218     myIndexStream.writeLong(offset);
219     myIndexStream.writeShort(completelyDownloaded ? 1 : 0);
220   }
221
222   private void openStreams() throws FileNotFoundException {
223     myStream = new RandomAccessFile(myPath, "rw");
224     myIndexStream = new RandomAccessFile(myIndexPath, "rw");
225     myStreamsOpen = true;
226   }
227
228   private void closeStreams() throws IOException {
229     myStreamsOpen = false;
230     try {
231       if (myStream != null) {
232         myStream.close();
233       }
234     }
235     finally {
236       if (myIndexStream != null) {
237         myIndexStream.close();
238       }
239     }
240   }
241
242   private void writeHeader() throws IOException {
243     assert myStreamsOpen && myHeaderLoaded;
244     myStream.seek(0);
245     myStream.writeInt(VERSION);
246     myStream.writeInt(myChangesProvider.getFormatVersion());
247     myStream.writeLong(myLastCachedDate.getTime());
248     myStream.writeLong(myFirstCachedDate.getTime());
249     myStream.writeLong(myFirstCachedChangelist);
250     myStream.writeLong(myLastCachedChangelist);
251     myStream.writeShort(myHaveCompleteHistory ? 1 : 0);
252     myStream.writeInt(myIncomingCount);
253     debug("Saved header for cache of " + myLocation + ": last cached date=" + myLastCachedDate +
254              ", last cached number=" + myLastCachedChangelist + ", incoming count=" + myIncomingCount);
255   }
256
257   private IndexEntry[] readIndexEntriesByOffset(final long offsetFromStart, int count) throws IOException {
258     if (!myIndexPath.exists()) {
259       return NO_ENTRIES;
260     }
261     long totalCount = myIndexStream.length() / INDEX_ENTRY_SIZE;
262     if (count > (totalCount - offsetFromStart)) {
263       count = (int) (totalCount - offsetFromStart);
264     }
265     if (count == 0) {
266       return NO_ENTRIES;
267     }
268     // offset from start
269     myIndexStream.seek(INDEX_ENTRY_SIZE * offsetFromStart);
270     IndexEntry[] result = new IndexEntry[count];
271     for(int i = (count - 1); i >= 0; --i) {
272       result [i] = new IndexEntry();
273       readIndexEntry(result [i]);
274     }
275     return result;
276   }
277
278   private IndexEntry[] readLastIndexEntries(int offset, int count) throws IOException {
279     if (!myIndexPath.exists()) {
280       return NO_ENTRIES;
281     }
282     long totalCount = myIndexStream.length() / INDEX_ENTRY_SIZE;
283     if (count > totalCount - offset) {
284       count = (int)totalCount - offset;
285     }
286     if (count == 0) {
287       return NO_ENTRIES;
288     }
289     myIndexStream.seek(myIndexStream.length() - INDEX_ENTRY_SIZE * (count + offset));
290     IndexEntry[] result = new IndexEntry[count];
291     for(int i=0; i<count; i++) {
292       result [i] = new IndexEntry();
293       readIndexEntry(result [i]);
294     }
295     return result;
296   }
297
298   private void readIndexEntry(final IndexEntry result) throws IOException {
299     result.number = myIndexStream.readLong();
300     result.date = myIndexStream.readLong();
301     result.offset = myIndexStream.readLong();
302     result.completelyDownloaded = (myIndexStream.readShort() != 0);
303   }
304
305   public Date getLastCachedDate() throws IOException {
306     loadHeader();
307     return myLastCachedDate;
308   }
309
310   public Date getFirstCachedDate() throws IOException {
311     loadHeader();
312     return myFirstCachedDate;
313   }
314
315   public long getFirstCachedChangelist() throws IOException {
316     loadHeader();
317     return myFirstCachedChangelist;
318   }
319
320   public long getLastCachedChangelist() throws IOException {
321     loadHeader();
322     return myLastCachedChangelist;
323   }
324
325   private void loadHeader() throws IOException {
326     if (!myHeaderLoaded) {
327       RandomAccessFile stream = new RandomAccessFile(myPath, "r");
328       try {
329         int version = stream.readInt();
330         if (version != VERSION) {
331           throw new VersionMismatchException();
332         }
333         int providerVersion = stream.readInt();
334         if (providerVersion != myChangesProvider.getFormatVersion()) {
335           throw new VersionMismatchException();
336         }
337         myLastCachedDate = new Date(stream.readLong());
338         myFirstCachedDate = new Date(stream.readLong());
339         myFirstCachedChangelist = stream.readLong();
340         myLastCachedChangelist = stream.readLong();
341         myHaveCompleteHistory = (stream.readShort() != 0);
342         myIncomingCount = stream.readInt();
343         assert stream.getFilePointer() == HEADER_SIZE;
344       }
345       finally {
346         stream.close();
347       }
348       myHeaderLoaded = true;
349     }
350   }
351
352   public Iterator<ChangesBunch> getBackBunchedIterator(final int bunchSize) {
353     return new BackIterator(bunchSize);
354   }
355
356   private List<Boolean> loadAllData(final List<CommittedChangeList> lists) throws IOException {
357     List<Boolean> idx = new ArrayList<Boolean>();
358     openStreams();
359
360     try {
361       loadHeader();
362       final long length = myIndexStream.length();
363       long totalCount = length / INDEX_ENTRY_SIZE;
364       for(int i=0; i<totalCount; i++) {
365         final long indexOffset = length - (i + 1) * INDEX_ENTRY_SIZE;
366         myIndexStream.seek(indexOffset);
367         IndexEntry e = new IndexEntry();
368         readIndexEntry(e);
369         final CommittedChangeList list = loadChangeListAt(e.offset);
370         lists.add(list);
371         idx.add(e.completelyDownloaded);
372       }
373     } finally {
374       closeStreams();
375     }
376     return idx;
377   }
378
379   public void editChangelist(long number, String message) throws IOException {
380     final List<CommittedChangeList> lists = new ArrayList<CommittedChangeList>();
381     final List<Boolean> present = loadAllData(lists);
382     for (CommittedChangeList list : lists) {
383       if (list.getNumber() == number) {
384         list.setDescription(message);
385         break;
386       }
387     }
388     delete();
389     Collections.reverse(lists);
390     Collections.reverse(present);
391     writeChanges(lists, present);
392   }
393
394   private class BackIterator implements Iterator<ChangesBunch> {
395     private final int bunchSize;
396     private long myOffset;
397
398     private BackIterator(final int bunchSize) {
399       this.bunchSize = bunchSize;
400       try {
401         try {
402           openStreams();
403           myOffset = (myIndexStream.length() / INDEX_ENTRY_SIZE);
404         } finally {
405           closeStreams();
406         }
407       }
408       catch (IOException e) {
409         myOffset = -1;
410       }
411     }
412
413     public boolean hasNext() {
414       return myOffset > 0;
415     }
416
417     @Nullable
418     public ChangesBunch next() {
419       try {
420         final int size;
421         if (myOffset < bunchSize) {
422           size = (int) myOffset;
423           myOffset = 0;
424         } else {
425           myOffset -= bunchSize;
426           size = bunchSize;
427         }
428         return new ChangesBunch(readChangesInterval(myOffset, size), true);
429       }
430       catch (IOException e) {
431         LOG.error(e);
432         return null;
433       }
434     }
435
436     public void remove() {
437       throw new UnsupportedOperationException();
438     }
439   }
440
441   private List<CommittedChangeList> readChangesInterval(final long indexOffset, final int number) throws IOException {
442     openStreams();
443
444     try {
445       IndexEntry[] entries = readIndexEntriesByOffset(indexOffset, number);
446       if (entries.length == 0) {
447         return Collections.emptyList();
448       }
449
450       final List<CommittedChangeList> result = new ArrayList<CommittedChangeList>();
451       for (IndexEntry entry : entries) {
452         final CommittedChangeList changeList = loadChangeListAt(entry.offset);
453         result.add(changeList);
454       }
455       return result;
456     } finally {
457       closeStreams();
458     }
459   }
460
461   public List<CommittedChangeList> readChanges(final ChangeBrowserSettings settings, final int maxCount) throws IOException {
462     final List<CommittedChangeList> result = new ArrayList<CommittedChangeList>();
463     final ChangeBrowserSettings.Filter filter = settings.createFilter();
464     openStreams();
465     try {
466       if (maxCount == 0) {
467         myStream.seek(HEADER_SIZE);  // skip header
468         while(myStream.getFilePointer() < myStream.length()) {
469           CommittedChangeList changeList = myChangesProvider.readChangeList(myLocation, myStream);
470           if (filter.accepts(changeList)) {
471             result.add(changeList);
472           }
473         }
474       }
475       else if (!settings.isAnyFilterSpecified()) {
476         IndexEntry[] entries = readLastIndexEntries(0, maxCount);
477         for(IndexEntry entry: entries) {
478           myStream.seek(entry.offset);
479           result.add(myChangesProvider.readChangeList(myLocation, myStream));
480         }
481       }
482       else {
483         int offset = 0;
484         while(result.size() < maxCount) {
485           IndexEntry[] entries = readLastIndexEntries(offset, 1);
486           if (entries.length == 0) {
487             break;
488           }
489           CommittedChangeList changeList = loadChangeListAt(entries [0].offset);
490           if (filter.accepts(changeList)) {
491             result.add(0, changeList);
492           }
493           offset++;
494         }
495       }
496       return result;
497     }
498     finally {
499       closeStreams();
500     }
501   }
502
503   public boolean hasCompleteHistory() {
504     return myHaveCompleteHistory;
505   }
506
507   public void setHaveCompleteHistory(final boolean haveCompleteHistory) {
508     if (myHaveCompleteHistory != haveCompleteHistory) {
509       myHaveCompleteHistory = haveCompleteHistory;
510       try {
511         openStreams();
512         try {
513           writeHeader();
514         }
515         finally {
516           closeStreams();
517         }
518       }
519       catch(IOException ex) {
520         LOG.error(ex);
521       }
522     }
523   }
524
525   public List<CommittedChangeList> loadIncomingChanges() throws IOException {
526     List<CommittedChangeList> result = new ArrayList<CommittedChangeList>();
527     int offset = 0;
528     openStreams();
529     try {
530       while(true) {
531         IndexEntry[] entries = readLastIndexEntries(offset, 1);
532         if (entries.length == 0) {
533           break;
534         }
535         if (!entries [0].completelyDownloaded) {
536           IncomingChangeListData data = readIncomingChangeListData(offset, entries [0]);
537           if (data.accountedChanges.size() == 0) {
538             result.add(data.changeList);
539           }
540           else {
541             ReceivedChangeList changeList = new ReceivedChangeList(data.changeList);
542             for(Change change: data.changeList.getChanges()) {
543               if (!data.accountedChanges.contains(change)) {
544                 changeList.addChange(change);
545               }
546             }
547             result.add(changeList);
548           }
549           if (result.size() == myIncomingCount) break;
550         }
551         offset++;
552       }
553       debug("Loaded " + result.size() + " incoming changelists");
554     }
555     finally {
556       closeStreams();
557     }
558     return result;
559   }
560
561   private CommittedChangeList loadChangeListAt(final long clOffset) throws IOException {
562     myStream.seek(clOffset);
563     return myChangesProvider.readChangeList(myLocation, myStream);
564   }
565
566   public boolean processUpdatedFiles(UpdatedFiles updatedFiles, Collection<CommittedChangeList> receivedChanges) throws IOException {
567     boolean haveUnaccountedUpdatedFiles = false;
568     openStreams();
569     loadHeader();
570     ReceivedChangeListTracker tracker = new ReceivedChangeListTracker();
571     try {
572       final List<IncomingChangeListData> incomingData = loadIncomingChangeListData();
573       for(FileGroup group: updatedFiles.getTopLevelGroups()) {
574         haveUnaccountedUpdatedFiles |= processGroup(group, incomingData, tracker);
575       }
576       if (!haveUnaccountedUpdatedFiles) {
577         for(IncomingChangeListData data: incomingData) {
578           saveIncoming(data, false);
579         }
580         writeHeader();
581       }
582     }
583     finally {
584       closeStreams();
585     }
586     receivedChanges.addAll(tracker.getChangeLists());
587     return haveUnaccountedUpdatedFiles;
588   }
589
590   private void saveIncoming(final IncomingChangeListData data, boolean haveNoMoreIncoming) throws IOException {
591     writePartial(data, haveNoMoreIncoming);
592     if (data.accountedChanges.size() == data.changeList.getChanges().size() || haveNoMoreIncoming) {
593       debug("Removing changelist " + data.changeList.getNumber() + " from incoming changelists");
594       myIndexStream.seek(data.indexOffset);
595       writeIndexEntry(data.indexEntry.number, data.indexEntry.date, data.indexEntry.offset, true);
596       myIncomingCount--;
597     }
598   }
599
600   private boolean processGroup(final FileGroup group, final List<IncomingChangeListData> incomingData,
601                                final ReceivedChangeListTracker tracker) {
602     boolean haveUnaccountedUpdatedFiles = false;
603     final List<Pair<String,VcsRevisionNumber>> list = group.getFilesAndRevisions(myVcsManager);
604     for(Pair<String, VcsRevisionNumber> pair: list) {
605       final String file = pair.first;
606       FilePath path = VcsUtil.getFilePath(file, false);
607       if (!path.isUnder(myRootPath, false) || pair.second == null) {
608         continue;
609       }
610       if (group.getId().equals(FileGroup.REMOVED_FROM_REPOSITORY_ID)) {
611         haveUnaccountedUpdatedFiles |= processDeletedFile(path, incomingData, tracker);
612       }
613       else {
614         haveUnaccountedUpdatedFiles |= processFile(path, pair.second, incomingData, tracker);
615       }
616     }
617     for(FileGroup childGroup: group.getChildren()) {
618       haveUnaccountedUpdatedFiles |= processGroup(childGroup, incomingData, tracker);
619     }
620     return haveUnaccountedUpdatedFiles;
621   }
622
623   private static boolean processFile(final FilePath path,
624                                      final VcsRevisionNumber number,
625                                      final List<IncomingChangeListData> incomingData,
626                                      final ReceivedChangeListTracker tracker) {
627     boolean foundRevision = false;
628     debug("Processing updated file " + path + ", revision " + number);
629     for(IncomingChangeListData data: incomingData) {
630       for(Change change: data.changeList.getChanges()) {
631         ContentRevision afterRevision = change.getAfterRevision();
632         if (afterRevision != null && afterRevision.getFile().equals(path)) {
633           int rc = number.compareTo(afterRevision.getRevisionNumber());
634           if (rc == 0) {
635             foundRevision = true;
636           }
637           if (rc >= 0) {
638             tracker.addChange(data.changeList, change);
639             data.accountedChanges.add(change);
640           }
641         }
642       }
643     }
644     debug(foundRevision ? "All changes for file found" : "Some of changes for file not found");
645     return !foundRevision;
646   }
647
648   private static boolean processDeletedFile(final FilePath path,
649                                             final List<IncomingChangeListData> incomingData,
650                                             final ReceivedChangeListTracker tracker) {
651     boolean foundRevision = false;
652     for(IncomingChangeListData data: incomingData) {
653       for(Change change: data.changeList.getChanges()) {
654         ContentRevision beforeRevision = change.getBeforeRevision();
655         if (beforeRevision != null && beforeRevision.getFile().equals(path)) {
656           tracker.addChange(data.changeList, change);
657           data.accountedChanges.add(change);
658           if (change.getAfterRevision() == null) {
659             foundRevision = true;
660           }
661         }
662       }
663     }
664     return !foundRevision;
665   }
666
667   private List<IncomingChangeListData> loadIncomingChangeListData() throws IOException {
668     final long length = myIndexStream.length();
669     long totalCount = length / INDEX_ENTRY_SIZE;
670     List<IncomingChangeListData> incomingData = new ArrayList<IncomingChangeListData>();
671     for(int i=0; i<totalCount; i++) {
672       final long indexOffset = length - (i + 1) * INDEX_ENTRY_SIZE;
673       myIndexStream.seek(indexOffset);
674       IndexEntry e = new IndexEntry();
675       readIndexEntry(e);
676       if (!e.completelyDownloaded) {
677         incomingData.add(readIncomingChangeListData(indexOffset, e));
678         if (incomingData.size() == myIncomingCount) {
679           break;
680         }
681       }
682     }
683     debug("Loaded " + incomingData.size() + " incoming changelist pointers");
684     return incomingData;
685   }
686
687   private IncomingChangeListData readIncomingChangeListData(final long indexOffset, final IndexEntry e) throws IOException {
688     IncomingChangeListData data = new IncomingChangeListData();
689     data.indexOffset = indexOffset;
690     data.indexEntry = e;
691     data.changeList = loadChangeListAt(e.offset);
692     readPartial(data);
693     return data;
694   }
695
696   private void writePartial(final IncomingChangeListData data, boolean haveNoMoreIncoming) throws IOException {
697     File partialFile = getPartialPath(data.indexEntry.offset);
698     final int accounted = data.accountedChanges.size();
699     if (haveNoMoreIncoming || accounted == data.changeList.getChanges().size()) {
700       partialFile.delete();
701     }
702     else if (accounted > 0) {
703       RandomAccessFile file = new RandomAccessFile(partialFile, "rw");
704       try {
705         file.writeInt(accounted);
706         for(Change c: data.accountedChanges) {
707           boolean isAfterRevision = true;
708           ContentRevision revision = c.getAfterRevision();
709           if (revision == null) {
710             isAfterRevision = false;
711             revision = c.getBeforeRevision();
712             assert revision != null;
713           }
714           file.writeByte(isAfterRevision ? 1 : 0);
715           file.writeUTF(revision.getFile().getIOFile().toString());
716         }
717       }
718       finally {
719         file.close();
720       }
721     }
722   }
723
724   private void readPartial(IncomingChangeListData data) {
725     HashSet<Change> result = new HashSet<Change>();
726     try {
727       File partialFile = getPartialPath(data.indexEntry.offset);
728       if (partialFile.exists()) {
729         RandomAccessFile file = new RandomAccessFile(partialFile, "r");
730         try {
731           int count = file.readInt();
732           if (count > 0) {
733             final Collection<Change> changes = data.changeList.getChanges();
734             final Map<String, Change> beforePaths = new HashMap<String, Change>();
735             final Map<String, Change> afterPaths = new HashMap<String, Change>();
736             for (Change change : changes) {
737               if (change.getBeforeRevision() != null) {
738                 beforePaths.put(FilePathsHelper.convertPath(change.getBeforeRevision().getFile()), change);
739               }
740               if (change.getAfterRevision() != null) {
741                 afterPaths.put(FilePathsHelper.convertPath(change.getAfterRevision().getFile()), change);
742               }
743             }
744             for(int i=0; i<count; i++) {
745               boolean isAfterRevision = (file.readByte() != 0);
746               String path = file.readUTF();
747               final String converted = FilePathsHelper.convertPath(path);
748               final Change change;
749               if (isAfterRevision) {
750                 change = afterPaths.get(converted);
751               } else {
752                 change = beforePaths.get(converted);
753               }
754               if (change != null) {
755                 result.add(change);
756               }
757             }
758           }
759         }
760         finally {
761           file.close();
762         }
763       }
764     }
765     catch(IOException ex) {
766       LOG.error(ex);
767     }
768     data.accountedChanges = result;
769   }
770
771   @NonNls
772   private File getPartialPath(final long offset) {
773     return new File(myPath + "." + offset + ".partial");
774   }
775
776   public boolean refreshIncomingChanges() throws IOException, VcsException {
777     if (myProject.isDisposed()) return false;
778     
779     DiffProvider diffProvider = myVcs.getDiffProvider();
780     if (diffProvider == null) return false;
781     
782     return new RefreshIncomingChangesOperation(this, myProject, diffProvider).invoke();
783   }
784
785   public AbstractVcs getVcs() {
786     return myVcs;
787   }
788
789   public FilePath getRootPath() {
790     return myRootPath;
791   }
792
793   private static class RefreshIncomingChangesOperation {
794     private final Set<FilePath> myDeletedFiles = new HashSet<FilePath>();
795     private final Set<FilePath> myCreatedFiles = new HashSet<FilePath>();
796     private final Set<FilePath> myReplacedFiles = new HashSet<FilePath>();
797     private final Map<Long, IndexEntry> myIndexEntryCache = new HashMap<Long, IndexEntry>();
798     private final Map<Long, CommittedChangeList> myPreviousChangeListsCache = new HashMap<Long, CommittedChangeList>();
799     private final ChangeListManagerImpl myClManager;
800     private final ChangesCacheFile myChangesCacheFile;
801     private final Project myProject;
802     private final DiffProvider myDiffProvider;
803     private boolean myAnyChanges;
804     private long myIndexStreamCachedLength;
805
806     RefreshIncomingChangesOperation(ChangesCacheFile changesCacheFile, Project project, final DiffProvider diffProvider) {
807       myChangesCacheFile = changesCacheFile;
808       myProject = project;
809       myDiffProvider = diffProvider;
810       myClManager = ChangeListManagerImpl.getInstanceImpl(project);
811     }
812
813     public boolean invoke() throws VcsException, IOException {
814       myChangesCacheFile.myLocation.onBeforeBatch();
815       final Collection<FilePath> incomingFiles = myChangesCacheFile.myChangesProvider.getIncomingFiles(myChangesCacheFile.myLocation);
816
817       myAnyChanges = false;
818       myChangesCacheFile.openStreams();
819       myChangesCacheFile.loadHeader();
820       try {
821         IncomingChangeState.header(myChangesCacheFile.myLocation.toPresentableString());
822
823         final List<IncomingChangeListData> list = myChangesCacheFile.loadIncomingChangeListData();
824         boolean shouldChangeHeader;
825         if (incomingFiles != null && incomingFiles.isEmpty()) {
826           // we should just delete any partial files
827           shouldChangeHeader = ! list.isEmpty();
828           for (IncomingChangeListData data : list) {
829             myChangesCacheFile.saveIncoming(data, true);
830           }
831         } else {
832           shouldChangeHeader = refreshIncomingInFile(incomingFiles, list);
833         }
834
835         IncomingChangeState.footer();
836         if (shouldChangeHeader) {
837           myChangesCacheFile.writeHeader();
838         }
839       }
840       finally {
841         myChangesCacheFile.myLocation.onAfterBatch();
842         myChangesCacheFile.closeStreams();
843       }
844       return myAnyChanges;
845     }
846
847     private boolean refreshIncomingInFile(Collection<FilePath> incomingFiles, List<IncomingChangeListData> list) throws IOException {
848       // the incoming changelist pointers are actually sorted in reverse chronological order,
849       // so we process file delete changes before changes made to deleted files before they were deleted
850       
851       Map<Pair<IncomingChangeListData, Change>, VirtualFile> revisionDependentFiles = ContainerUtil.newHashMap();
852       Map<Pair<IncomingChangeListData, Change>, ProcessingResult> results = ContainerUtil.newHashMap();
853
854       myIndexStreamCachedLength = myChangesCacheFile.myIndexStream.length();
855       // try to process changelists in a light way, remember which files need revisions
856       for(IncomingChangeListData data: list) {
857         debug("Checking incoming changelist " + data.changeList.getNumber());
858
859         for(Change change: data.getChangesToProcess()) {
860           final ProcessingResult result = processIncomingChange(change, data, incomingFiles);
861           
862           Pair<IncomingChangeListData, Change> key = Pair.create(data, change);
863           results.put(key, result);
864           if (result.revisionDependentProcessing != null) {
865             revisionDependentFiles.put(key, result.file);
866           }
867         }
868       }
869
870       if (!revisionDependentFiles.isEmpty()) {
871         // lots of same files could be collected - make set of unique files
872         HashSet<VirtualFile> uniqueFiles = ContainerUtil.newHashSet(revisionDependentFiles.values());
873         // bulk-get all needed revisions at once
874         Map<VirtualFile, VcsRevisionNumber> revisions = myDiffProvider instanceof DiffProviderEx
875                                                         ? ((DiffProviderEx)myDiffProvider).getCurrentRevisions(uniqueFiles)
876                                                         : DiffProviderEx.getCurrentRevisions(uniqueFiles, myDiffProvider);
877
878         // perform processing requiring those revisions
879         for(IncomingChangeListData data: list) {
880           for (Change change : data.getChangesToProcess()) {
881             Pair<IncomingChangeListData, Change> key = Pair.create(data, change);
882             Function<VcsRevisionNumber, ProcessingResult> revisionHandler = results.get(key).revisionDependentProcessing;
883             if (revisionHandler != null) {
884               results.put(key, revisionHandler.fun(revisions.get(revisionDependentFiles.get(key))));
885             }
886           }
887         }
888       }
889
890       // collect and save processing results
891       for(IncomingChangeListData data: list) {
892         boolean updated = false;
893         boolean anyChangeFound = false;
894         for (Change change : data.getChangesToProcess()) {
895           final ContentRevision revision = (change.getAfterRevision() == null) ? change.getBeforeRevision() : change.getAfterRevision();
896           assert revision != null;
897           ProcessingResult result = results.get(Pair.create(data, change));
898           new IncomingChangeState(change, revision.getRevisionNumber().asString(), result.state).logSelf();
899           if (result.changeFound) {
900             updated = true;
901             data.accountedChanges.add(change);
902           } else {
903             anyChangeFound = true;
904           }
905         }
906         if (updated || ! anyChangeFound) {
907           myAnyChanges = true;
908           myChangesCacheFile.saveIncoming(data, !anyChangeFound);
909         }
910       }
911       return myAnyChanges || !list.isEmpty();
912     }
913     
914     private static class ProcessingResult {
915       final boolean changeFound; 
916       final IncomingChangeState.State state;
917       final VirtualFile file;
918       final Function<VcsRevisionNumber, ProcessingResult> revisionDependentProcessing;
919
920       private ProcessingResult(boolean changeFound, IncomingChangeState.State state) {
921         this.changeFound = changeFound;
922         this.state = state;
923         this.file = null;
924         this.revisionDependentProcessing = null;
925       }
926
927       private ProcessingResult(VirtualFile file, Function<VcsRevisionNumber, ProcessingResult> revisionDependentProcessing) {
928         this.file = file;
929         this.revisionDependentProcessing = revisionDependentProcessing;
930         this.changeFound = false;
931         this.state = null;
932       }
933     }
934
935     private ProcessingResult processIncomingChange(final Change change,
936                                           final IncomingChangeListData changeListData,
937                                           @Nullable final Collection<FilePath> incomingFiles) {
938       final CommittedChangeList changeList = changeListData.changeList;
939       final ContentRevision afterRevision = change.getAfterRevision();
940       if (afterRevision != null) {
941         if (afterRevision.getFile().isNonLocal()) {
942           // don't bother to search for non-local paths on local disk
943           return new ProcessingResult(true, AFTER_DOES_NOT_MATTER_NON_LOCAL);
944         }
945         if (change.getBeforeRevision() == null) {
946           final FilePath path = afterRevision.getFile();
947           debug("Marking created file " + path);
948           myCreatedFiles.add(path);
949         } else if (change.getBeforeRevision().getFile().getIOFile().getAbsolutePath().equals(
950           afterRevision.getFile().getIOFile().getAbsolutePath()) && change.isIsReplaced()) {
951           myReplacedFiles.add(afterRevision.getFile());
952         }
953         if (incomingFiles != null && !incomingFiles.contains(afterRevision.getFile())) {
954           debug("Skipping new/changed file outside of incoming files: " + afterRevision.getFile());
955           return new ProcessingResult(true, AFTER_DOES_NOT_MATTER_OUTSIDE_INCOMING);
956         }
957         debug("Checking file " + afterRevision.getFile().getPath());
958         FilePath localPath = ChangesUtil.getLocalPath(myProject, afterRevision.getFile());
959
960         if (! FileUtil.isAncestor(myChangesCacheFile.myRootPath.getIOFile(), localPath.getIOFile(), false)) {
961           // alien change in list; skip
962           debug("Alien path " +
963                 localPath.getPresentableUrl() +
964                 " under root " +
965                 myChangesCacheFile.myRootPath.getPresentableUrl() +
966                 "; skipping.");
967           return new ProcessingResult(true, AFTER_DOES_NOT_MATTER_ALIEN_PATH);
968         }
969
970         final VirtualFile file = localPath.getVirtualFile();
971         if (isDeletedFile(myDeletedFiles, afterRevision, myReplacedFiles)) {
972           debug("Found deleted file");
973           return new ProcessingResult(true, AFTER_DOES_NOT_MATTER_DELETED_FOUND_IN_INCOMING_LIST);
974         }
975         else if (file != null) {
976           return new ProcessingResult(file, new Function<VcsRevisionNumber, ProcessingResult>() {
977             @Override
978             public ProcessingResult fun(VcsRevisionNumber revision) {
979               if (revision != null) {
980                 debug("Current revision is " + revision + ", changelist revision is " + afterRevision.getRevisionNumber());
981                 //noinspection unchecked
982                 if (myChangesCacheFile.myChangesProvider
983                   .isChangeLocallyAvailable(afterRevision.getFile(), revision, afterRevision.getRevisionNumber(), changeList)) {
984                   return new ProcessingResult(true, AFTER_EXISTS_LOCALLY_AVAILABLE);
985                 }
986                 return new ProcessingResult(false, AFTER_EXISTS_NOT_LOCALLY_AVAILABLE);
987               }
988               debug("Failed to fetch revision");
989               return new ProcessingResult(false, AFTER_EXISTS_REVISION_NOT_LOADED);
990             }
991           });
992         }
993         else {
994           //noinspection unchecked
995           if (myChangesCacheFile.myChangesProvider.isChangeLocallyAvailable(afterRevision.getFile(), null, afterRevision.getRevisionNumber(), changeList)) {
996             return new ProcessingResult(true, AFTER_NOT_EXISTS_LOCALLY_AVAILABLE);
997           }
998           if (fileMarkedForDeletion(localPath)) {
999             debug("File marked for deletion and not committed jet.");
1000             return new ProcessingResult(true, AFTER_NOT_EXISTS_MARKED_FOR_DELETION);
1001           }
1002           if (wasSubsequentlyDeleted(afterRevision.getFile(), changeListData.indexOffset)) {
1003             return new ProcessingResult(true, AFTER_NOT_EXISTS_SUBSEQUENTLY_DELETED);
1004           }
1005           debug("Could not find local file for change " + afterRevision.getFile().getPath());
1006           return new ProcessingResult(false, AFTER_NOT_EXISTS_OTHER);
1007         }
1008       }
1009       else {
1010         final ContentRevision beforeRevision = change.getBeforeRevision();
1011         assert beforeRevision != null;
1012         debug("Checking deleted file " + beforeRevision.getFile());
1013         myDeletedFiles.add(beforeRevision.getFile());
1014         if (incomingFiles != null && !incomingFiles.contains(beforeRevision.getFile())) {
1015           debug("Skipping deleted file outside of incoming files: " + beforeRevision.getFile());
1016           return new ProcessingResult(true, BEFORE_DOES_NOT_MATTER_OUTSIDE);
1017         }
1018         if (beforeRevision.getFile().getVirtualFile() == null || myCreatedFiles.contains(beforeRevision.getFile())) {
1019           // if not deleted from vcs, mark as incoming, otherwise file already deleted
1020           final boolean locallyDeleted = myClManager.isContainedInLocallyDeleted(beforeRevision.getFile());
1021           debug(locallyDeleted ? "File deleted locally, change marked as incoming" : "File already deleted");
1022           return new ProcessingResult(!locallyDeleted, locallyDeleted ? BEFORE_NOT_EXISTS_DELETED_LOCALLY : BEFORE_NOT_EXISTS_ALREADY_DELETED);
1023         }
1024         else if (!myChangesCacheFile.myVcs.fileExistsInVcs(beforeRevision.getFile())) {
1025           debug("File exists locally and is unversioned");
1026           return new ProcessingResult(true, BEFORE_UNVERSIONED_INSTEAD_OF_VERS_DELETED);
1027         }
1028         else {
1029           final VirtualFile file = beforeRevision.getFile().getVirtualFile();
1030           return new ProcessingResult(file, new Function<VcsRevisionNumber, ProcessingResult>() {
1031             @Override
1032             public ProcessingResult fun(VcsRevisionNumber currentRevision) {
1033               if ((currentRevision != null) && (currentRevision.compareTo(beforeRevision.getRevisionNumber()) > 0)) {
1034                 // revived in newer revision - possibly was added file with same name
1035                 debug("File with same name was added after file deletion");
1036                 return new ProcessingResult(true, BEFORE_SAME_NAME_ADDED_AFTER_DELETION);
1037               }
1038               debug("File exists locally and no 'create' change found for it");
1039               return new ProcessingResult(false, BEFORE_EXISTS_BUT_SHOULD_NOT);
1040             }
1041           });
1042         }
1043       }
1044     }
1045
1046     private boolean fileMarkedForDeletion(final FilePath localPath) {
1047       final List<LocalChangeList> changeLists =  myClManager.getChangeListsCopy();
1048       for (LocalChangeList list : changeLists) {
1049         final Collection<Change> changes = list.getChanges();
1050         for (Change change : changes) {
1051           if (change.getBeforeRevision() != null && change.getBeforeRevision().getFile() != null &&
1052               change.getBeforeRevision().getFile().getPath().equals(localPath.getPath())) {
1053             if (FileStatus.DELETED.equals(change.getFileStatus()) || change.isMoved() || change.isRenamed()) {
1054               return true;
1055             }
1056           }
1057         }
1058       }
1059       return false;
1060     }
1061
1062     // If we have an incoming add, we may have already processed the subsequent delete of the same file during
1063     // a previous incoming changes refresh. So we try to search for the deletion of this file through all
1064     // subsequent committed changelists, regardless of whether they are in "incoming" status.
1065     private boolean wasSubsequentlyDeleted(final FilePath file, long indexOffset) {
1066       try {
1067         indexOffset += INDEX_ENTRY_SIZE;
1068         while(indexOffset < myIndexStreamCachedLength) {
1069           IndexEntry e = getIndexEntryAtOffset(indexOffset);
1070
1071           final CommittedChangeList changeList = getChangeListAtOffset(e.offset);
1072           for(Change c: changeList.getChanges()) {
1073             final ContentRevision beforeRevision = c.getBeforeRevision();
1074             if ((beforeRevision != null) && (c.getAfterRevision() == null)) {
1075               if (isFileDeleted(file, beforeRevision.getFile())) {
1076                 return true;
1077               }
1078             } else if ((beforeRevision != null) && (c.getAfterRevision() != null)) {
1079               if (isParentReplacedOrFileMoved(file, c, beforeRevision.getFile())) {
1080                 return true;
1081               }
1082             }
1083           }
1084           indexOffset += INDEX_ENTRY_SIZE;
1085         }
1086       }
1087       catch (IOException e) {
1088         LOG.error(e);
1089       }
1090       return false;
1091     }
1092
1093     private static boolean isParentReplacedOrFileMoved(@NotNull FilePath file, @NotNull Change change, @NotNull FilePath beforeFile) {
1094       boolean isParentReplaced = change.isIsReplaced() && (!file.equals(beforeFile));
1095       boolean isMovedRenamed = change.isMoved() || change.isRenamed();
1096       // call FilePath.isUnder() only if change is either "parent replaced" or moved/renamed - as many calls to FilePath.isUnder()
1097       // could take a lot of time
1098       boolean underBefore = (isParentReplaced || isMovedRenamed) && file.isUnder(beforeFile, false);
1099
1100       if (underBefore && isParentReplaced) {
1101         debug("For " + file + "some of parents is replaced: " + beforeFile);
1102         return true;
1103       }
1104       else if (underBefore && isMovedRenamed) {
1105         debug("For " + file + "some of parents was renamed/moved: " + beforeFile);
1106         return true;
1107       }
1108       return false;
1109     }
1110
1111     private static boolean isFileDeleted(@NotNull FilePath file, @NotNull FilePath beforeFile) {
1112       if (file.getIOFile().getAbsolutePath().equals(beforeFile.getIOFile().getAbsolutePath()) ||
1113           file.isUnder(beforeFile, false)) {
1114         debug("Found subsequent deletion for file " + file);
1115         return true;
1116       }
1117       return false;
1118     }
1119
1120     private IndexEntry getIndexEntryAtOffset(final long indexOffset) throws IOException {
1121       IndexEntry e = myIndexEntryCache.get(indexOffset);
1122       if (e == null) {
1123         myChangesCacheFile.myIndexStream.seek(indexOffset);
1124         e = new IndexEntry();
1125         myChangesCacheFile.readIndexEntry(e);
1126         myIndexEntryCache.put(indexOffset, e);
1127       }
1128       return e;
1129     }
1130
1131     private CommittedChangeList getChangeListAtOffset(final long offset) throws IOException {
1132       CommittedChangeList changeList = myPreviousChangeListsCache.get(offset);
1133       if (changeList == null) {
1134         changeList = myChangesCacheFile.loadChangeListAt(offset);
1135         myPreviousChangeListsCache.put(offset, changeList);
1136       }
1137       return changeList; 
1138     }
1139
1140     private static boolean isDeletedFile(final Set<FilePath> deletedFiles,
1141                                          final ContentRevision afterRevision,
1142                                          final Set<FilePath> replacedFiles) {
1143       FilePath file = afterRevision.getFile();
1144       while(file != null) {
1145         if (deletedFiles.contains(file)) {
1146           return true;
1147         }
1148         file = file.getParentPath();
1149         if (file != null && replacedFiles.contains(file)) {
1150           return true;
1151         }
1152       }
1153       return false;
1154     }
1155   }
1156
1157   private static class IndexEntry {
1158     long number;
1159     long date;
1160     long offset;
1161     boolean completelyDownloaded;
1162   }
1163
1164   private static class IncomingChangeListData {
1165     public long indexOffset;
1166     public IndexEntry indexEntry;
1167     public CommittedChangeList changeList;
1168     public Set<Change> accountedChanges;
1169
1170     List<Change> getChangesToProcess() {
1171       return ContainerUtil.filter(changeList.getChanges(), new Condition<Change>() {
1172         @Override
1173         public boolean value(Change change) {
1174           return !accountedChanges.contains(change);
1175         }
1176       });
1177     }
1178   }
1179
1180   private static final IndexEntry[] NO_ENTRIES = new IndexEntry[0];
1181
1182   private static class VersionMismatchException extends RuntimeException {
1183   }
1184
1185   private static class ReceivedChangeListTracker {
1186     private final Map<CommittedChangeList, ReceivedChangeList> myMap = new HashMap<CommittedChangeList, ReceivedChangeList>();
1187
1188     public void addChange(CommittedChangeList changeList, Change change) {
1189       ReceivedChangeList list = myMap.get(changeList);
1190       if (list == null) {
1191         list = new ReceivedChangeList(changeList);
1192         myMap.put(changeList, list);
1193       }
1194       list.addChange(change);
1195     }
1196
1197     public Collection<? extends CommittedChangeList> getChangeLists() {
1198       return myMap.values();
1199     }
1200   }
1201 }