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