2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.openapi.vcs.changes.committed;
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;
43 import static com.intellij.openapi.vcs.changes.committed.IncomingChangeState.State.*;
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;
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;
74 public ChangesCacheFile(Project project, File path, AbstractVcs vcs, VirtualFile root, RepositoryLocation location) {
79 myIndexPath = new File(myPath.toString() + INDEX_EXTENSION);
81 myChangesProvider = (CachingCommittedChangesProvider) vcs.getCommittedChangesProvider();
82 myVcsManager = ProjectLevelVcsManager.getInstance(project);
83 myRootPath = VcsUtil.getFilePath(root);
84 myLocation = location;
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();
94 myLastCachedChangelist = -1;
95 myFirstCachedChangelist = Long.MAX_VALUE;
96 myHaveCompleteHistory = false;
97 myHeaderLoaded = false;
100 public RepositoryLocation getLocation() {
104 public CachingCommittedChangesProvider getProvider() {
105 return myChangesProvider;
108 public boolean isEmpty() throws IOException {
109 if (!myPath.exists()) {
115 catch(VersionMismatchException ex) {
117 myIndexPath.delete();
120 catch(EOFException ex) {
122 myIndexPath.delete();
129 public void delete() {
130 FileUtil.delete(myPath);
131 FileUtil.delete(myIndexPath);
135 catch (IOException e) {
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);
146 public List<CommittedChangeList> writeChanges(final List<CommittedChangeList> changes, @Nullable final List<Boolean> present) throws IOException {
147 assert present == null || present.size() == changes.size();
149 List<CommittedChangeList> result = new ArrayList<CommittedChangeList>(changes.size());
150 boolean wasEmpty = isEmpty();
154 myHeaderLoaded = true;
157 myStream.seek(myStream.length());
158 IndexEntry[] entries = readLastIndexEntries(0, changes.size());
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) {
170 debug("Skipping duplicate changelist " + list.getNumber());
173 debug("Writing incoming changelist " + list.getNumber());
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());
183 myHeaderLoaded = true;
191 private static void debug(@NonNls String message) {
195 private void updateCachedRange(final CommittedChangeList list) {
196 if (list.getCommitDate().getTime() > myLastCachedDate.getTime()) {
197 myLastCachedDate = list.getCommitDate();
199 if (list.getCommitDate().getTime() < myFirstCachedDate.getTime()) {
200 myFirstCachedDate = list.getCommitDate();
202 if (list.getNumber() < myFirstCachedChangelist) {
203 myFirstCachedChangelist = list.getNumber();
205 if (list.getNumber() > myLastCachedChangelist) {
206 myLastCachedChangelist = list.getNumber();
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);
217 private void openStreams() throws FileNotFoundException {
218 myStream = new RandomAccessFile(myPath, "rw");
219 myIndexStream = new RandomAccessFile(myIndexPath, "rw");
220 myStreamsOpen = true;
223 private void closeStreams() throws IOException {
224 myStreamsOpen = false;
226 if (myStream != null) {
231 if (myIndexStream != null) {
232 myIndexStream.close();
237 private void writeHeader() throws IOException {
238 assert myStreamsOpen && myHeaderLoaded;
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);
252 private IndexEntry[] readIndexEntriesByOffset(final long offsetFromStart, int count) throws IOException {
253 if (!myIndexPath.exists()) {
256 long totalCount = myIndexStream.length() / INDEX_ENTRY_SIZE;
257 if (count > (totalCount - offsetFromStart)) {
258 count = (int) (totalCount - offsetFromStart);
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]);
273 private IndexEntry[] readLastIndexEntries(int offset, int count) throws IOException {
274 if (!myIndexPath.exists()) {
277 long totalCount = myIndexStream.length() / INDEX_ENTRY_SIZE;
278 if (count > totalCount - offset) {
279 count = (int)totalCount - offset;
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]);
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);
300 public Date getLastCachedDate() throws IOException {
302 return myLastCachedDate;
305 public Date getFirstCachedDate() throws IOException {
307 return myFirstCachedDate;
310 public long getFirstCachedChangelist() throws IOException {
312 return myFirstCachedChangelist;
315 public long getLastCachedChangelist() throws IOException {
317 return myLastCachedChangelist;
320 private void loadHeader() throws IOException {
321 if (!myHeaderLoaded) {
322 RandomAccessFile stream = new RandomAccessFile(myPath, "r");
324 int version = stream.readInt();
325 if (version != VERSION) {
326 throw new VersionMismatchException();
328 int providerVersion = stream.readInt();
329 if (providerVersion != myChangesProvider.getFormatVersion()) {
330 throw new VersionMismatchException();
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;
343 myHeaderLoaded = true;
347 public Iterator<ChangesBunch> getBackBunchedIterator(final int bunchSize) {
348 return new BackIterator(bunchSize);
351 private List<Boolean> loadAllData(final List<CommittedChangeList> lists) throws IOException {
352 List<Boolean> idx = new ArrayList<Boolean>();
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();
364 final CommittedChangeList list = loadChangeListAt(e.offset);
366 idx.add(e.completelyDownloaded);
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);
384 Collections.reverse(lists);
385 Collections.reverse(present);
386 writeChanges(lists, present);
389 private class BackIterator implements Iterator<ChangesBunch> {
390 private final int bunchSize;
391 private long myOffset;
393 private BackIterator(final int bunchSize) {
394 this.bunchSize = bunchSize;
398 myOffset = (myIndexStream.length() / INDEX_ENTRY_SIZE);
403 catch (IOException e) {
408 public boolean hasNext() {
413 public ChangesBunch next() {
416 if (myOffset < bunchSize) {
417 size = (int) myOffset;
420 myOffset -= bunchSize;
423 return new ChangesBunch(readChangesInterval(myOffset, size), true);
425 catch (IOException e) {
431 public void remove() {
432 throw new UnsupportedOperationException();
436 private List<CommittedChangeList> readChangesInterval(final long indexOffset, final int number) throws IOException {
440 IndexEntry[] entries = readIndexEntriesByOffset(indexOffset, number);
441 if (entries.length == 0) {
442 return Collections.emptyList();
445 final List<CommittedChangeList> result = new ArrayList<CommittedChangeList>();
446 for (IndexEntry entry : entries) {
447 final CommittedChangeList changeList = loadChangeListAt(entry.offset);
448 result.add(changeList);
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();
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);
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));
479 while(result.size() < maxCount) {
480 IndexEntry[] entries = readLastIndexEntries(offset, 1);
481 if (entries.length == 0) {
484 CommittedChangeList changeList = loadChangeListAt(entries [0].offset);
485 if (filter.accepts(changeList)) {
486 result.add(0, changeList);
498 public boolean hasCompleteHistory() {
499 return myHaveCompleteHistory;
502 public void setHaveCompleteHistory(final boolean haveCompleteHistory) {
503 if (myHaveCompleteHistory != haveCompleteHistory) {
504 myHaveCompleteHistory = haveCompleteHistory;
514 catch(IOException ex) {
520 public List<CommittedChangeList> loadIncomingChanges() throws IOException {
521 List<CommittedChangeList> result = new ArrayList<CommittedChangeList>();
526 IndexEntry[] entries = readLastIndexEntries(offset, 1);
527 if (entries.length == 0) {
530 if (!entries [0].completelyDownloaded) {
531 IncomingChangeListData data = readIncomingChangeListData(offset, entries [0]);
532 if (data.accountedChanges.size() == 0) {
533 result.add(data.changeList);
536 ReceivedChangeList changeList = new ReceivedChangeList(data.changeList);
537 for(Change change: data.changeList.getChanges()) {
538 if (!data.accountedChanges.contains(change)) {
539 changeList.addChange(change);
542 result.add(changeList);
544 if (result.size() == myIncomingCount) break;
548 debug("Loaded " + result.size() + " incoming changelists");
556 private CommittedChangeList loadChangeListAt(final long clOffset) throws IOException {
557 myStream.seek(clOffset);
558 return myChangesProvider.readChangeList(myLocation, myStream);
561 public boolean processUpdatedFiles(UpdatedFiles updatedFiles, Collection<CommittedChangeList> receivedChanges) throws IOException {
562 boolean haveUnaccountedUpdatedFiles = false;
565 ReceivedChangeListTracker tracker = new ReceivedChangeListTracker();
567 final List<IncomingChangeListData> incomingData = loadIncomingChangeListData();
568 for(FileGroup group: updatedFiles.getTopLevelGroups()) {
569 haveUnaccountedUpdatedFiles |= processGroup(group, incomingData, tracker);
571 if (!haveUnaccountedUpdatedFiles) {
572 for(IncomingChangeListData data: incomingData) {
573 saveIncoming(data, false);
581 receivedChanges.addAll(tracker.getChangeLists());
582 return haveUnaccountedUpdatedFiles;
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);
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) {
605 if (group.getId().equals(FileGroup.REMOVED_FROM_REPOSITORY_ID)) {
606 haveUnaccountedUpdatedFiles |= processDeletedFile(path, incomingData, tracker);
609 haveUnaccountedUpdatedFiles |= processFile(path, pair.second, incomingData, tracker);
612 for(FileGroup childGroup: group.getChildren()) {
613 haveUnaccountedUpdatedFiles |= processGroup(childGroup, incomingData, tracker);
615 return haveUnaccountedUpdatedFiles;
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());
630 foundRevision = true;
633 tracker.addChange(data.changeList, change);
634 data.accountedChanges.add(change);
639 debug(foundRevision ? "All changes for file found" : "Some of changes for file not found");
640 return !foundRevision;
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;
659 return !foundRevision;
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();
671 if (!e.completelyDownloaded) {
672 incomingData.add(readIncomingChangeListData(indexOffset, e));
673 if (incomingData.size() == myIncomingCount) {
678 debug("Loaded " + incomingData.size() + " incoming changelist pointers");
682 private IncomingChangeListData readIncomingChangeListData(final long indexOffset, final IndexEntry e) throws IOException {
683 IncomingChangeListData data = new IncomingChangeListData();
684 data.indexOffset = indexOffset;
686 data.changeList = loadChangeListAt(e.offset);
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();
697 else if (accounted > 0) {
698 RandomAccessFile file = new RandomAccessFile(partialFile, "rw");
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;
709 file.writeByte(isAfterRevision ? 1 : 0);
710 file.writeUTF(revision.getFile().getIOFile().toString());
719 private void readPartial(IncomingChangeListData data) {
720 HashSet<Change> result = new HashSet<Change>();
722 File partialFile = getPartialPath(data.indexEntry.offset);
723 if (partialFile.exists()) {
724 RandomAccessFile file = new RandomAccessFile(partialFile, "r");
726 int count = file.readInt();
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);
735 if (change.getAfterRevision() != null) {
736 afterPaths.put(FilePathsHelper.convertPath(change.getAfterRevision().getFile()), change);
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);
744 if (isAfterRevision) {
745 change = afterPaths.get(converted);
747 change = beforePaths.get(converted);
749 if (change != null) {
760 catch(IOException ex) {
763 data.accountedChanges = result;
767 private File getPartialPath(final long offset) {
768 return new File(myPath + "." + offset + ".partial");
771 public boolean refreshIncomingChanges() throws IOException, VcsException {
772 if (myProject.isDisposed()) return false;
774 DiffProvider diffProvider = myVcs.getDiffProvider();
775 if (diffProvider == null) return false;
777 return new RefreshIncomingChangesOperation(this, myProject, diffProvider).invoke();
780 public AbstractVcs getVcs() {
784 public FilePath getRootPath() {
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;
801 RefreshIncomingChangesOperation(ChangesCacheFile changesCacheFile, Project project, final DiffProvider diffProvider) {
802 myChangesCacheFile = changesCacheFile;
804 myDiffProvider = diffProvider;
805 myClManager = ChangeListManagerImpl.getInstanceImpl(project);
808 public boolean invoke() throws VcsException, IOException {
809 myChangesCacheFile.myLocation.onBeforeBatch();
810 final Collection<FilePath> incomingFiles = myChangesCacheFile.myChangesProvider.getIncomingFiles(myChangesCacheFile.myLocation);
812 myAnyChanges = false;
813 myChangesCacheFile.openStreams();
814 myChangesCacheFile.loadHeader();
816 IncomingChangeState.header(myChangesCacheFile.myLocation.toPresentableString());
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);
827 shouldChangeHeader = refreshIncomingInFile(incomingFiles, list);
830 IncomingChangeState.footer();
831 if (shouldChangeHeader) {
832 myChangesCacheFile.writeHeader();
836 myChangesCacheFile.myLocation.onAfterBatch();
837 myChangesCacheFile.closeStreams();
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
846 Map<Pair<IncomingChangeListData, Change>, VirtualFile> revisionDependentFiles = ContainerUtil.newHashMap();
847 Map<Pair<IncomingChangeListData, Change>, ProcessingResult> results = ContainerUtil.newHashMap();
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());
854 for(Change change: data.getChangesToProcess()) {
855 final ProcessingResult result = processIncomingChange(change, data, incomingFiles);
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);
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);
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))));
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) {
896 data.accountedChanges.add(change);
898 anyChangeFound = true;
901 if (updated || ! anyChangeFound) {
903 myChangesCacheFile.saveIncoming(data, !anyChangeFound);
906 return myAnyChanges || !list.isEmpty();
909 private static class ProcessingResult {
910 final boolean changeFound;
911 final IncomingChangeState.State state;
912 final VirtualFile file;
913 final Function<VcsRevisionNumber, ProcessingResult> revisionDependentProcessing;
915 private ProcessingResult(boolean changeFound, IncomingChangeState.State state) {
916 this.changeFound = changeFound;
919 this.revisionDependentProcessing = null;
922 private ProcessingResult(VirtualFile file, Function<VcsRevisionNumber, ProcessingResult> revisionDependentProcessing) {
924 this.revisionDependentProcessing = revisionDependentProcessing;
925 this.changeFound = false;
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);
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());
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);
952 debug("Checking file " + afterRevision.getFile().getPath());
953 FilePath localPath = ChangesUtil.getLocalPath(myProject, afterRevision.getFile());
955 if (! FileUtil.isAncestor(myChangesCacheFile.myRootPath.getIOFile(), localPath.getIOFile(), false)) {
956 // alien change in list; skip
957 debug("Alien path " +
958 localPath.getPresentableUrl() +
960 myChangesCacheFile.myRootPath.getPresentableUrl() +
962 return new ProcessingResult(true, AFTER_DOES_NOT_MATTER_ALIEN_PATH);
965 final VirtualFile file = localPath.getVirtualFile();
966 if (isDeletedFile(myDeletedFiles, afterRevision, myReplacedFiles)) {
967 debug("Found deleted file");
968 return new ProcessingResult(true, AFTER_DOES_NOT_MATTER_DELETED_FOUND_IN_INCOMING_LIST);
970 else if (file != null) {
971 return new ProcessingResult(file, new Function<VcsRevisionNumber, ProcessingResult>() {
973 public ProcessingResult fun(VcsRevisionNumber revision) {
974 if (revision != null) {
975 debug("Current revision is " + revision + ", changelist revision is " + afterRevision.getRevisionNumber());
976 //noinspection unchecked
977 if (myChangesCacheFile.myChangesProvider
978 .isChangeLocallyAvailable(afterRevision.getFile(), revision, afterRevision.getRevisionNumber(), changeList)) {
979 return new ProcessingResult(true, AFTER_EXISTS_LOCALLY_AVAILABLE);
981 return new ProcessingResult(false, AFTER_EXISTS_NOT_LOCALLY_AVAILABLE);
983 debug("Failed to fetch revision");
984 return new ProcessingResult(false, AFTER_EXISTS_REVISION_NOT_LOADED);
989 //noinspection unchecked
990 if (myChangesCacheFile.myChangesProvider.isChangeLocallyAvailable(afterRevision.getFile(), null, afterRevision.getRevisionNumber(), changeList)) {
991 return new ProcessingResult(true, AFTER_NOT_EXISTS_LOCALLY_AVAILABLE);
993 if (fileMarkedForDeletion(localPath)) {
994 debug("File marked for deletion and not committed jet.");
995 return new ProcessingResult(true, AFTER_NOT_EXISTS_MARKED_FOR_DELETION);
997 if (wasSubsequentlyDeleted(afterRevision.getFile(), changeListData.indexOffset)) {
998 return new ProcessingResult(true, AFTER_NOT_EXISTS_SUBSEQUENTLY_DELETED);
1000 debug("Could not find local file for change " + afterRevision.getFile().getPath());
1001 return new ProcessingResult(false, AFTER_NOT_EXISTS_OTHER);
1005 final ContentRevision beforeRevision = change.getBeforeRevision();
1006 assert beforeRevision != null;
1007 debug("Checking deleted file " + beforeRevision.getFile());
1008 myDeletedFiles.add(beforeRevision.getFile());
1009 if (incomingFiles != null && !incomingFiles.contains(beforeRevision.getFile())) {
1010 debug("Skipping deleted file outside of incoming files: " + beforeRevision.getFile());
1011 return new ProcessingResult(true, BEFORE_DOES_NOT_MATTER_OUTSIDE);
1013 if (beforeRevision.getFile().getVirtualFile() == null || myCreatedFiles.contains(beforeRevision.getFile())) {
1014 // if not deleted from vcs, mark as incoming, otherwise file already deleted
1015 final boolean locallyDeleted = myClManager.isContainedInLocallyDeleted(beforeRevision.getFile());
1016 debug(locallyDeleted ? "File deleted locally, change marked as incoming" : "File already deleted");
1017 return new ProcessingResult(!locallyDeleted, locallyDeleted ? BEFORE_NOT_EXISTS_DELETED_LOCALLY : BEFORE_NOT_EXISTS_ALREADY_DELETED);
1019 else if (!myChangesCacheFile.myVcs.fileExistsInVcs(beforeRevision.getFile())) {
1020 debug("File exists locally and is unversioned");
1021 return new ProcessingResult(true, BEFORE_UNVERSIONED_INSTEAD_OF_VERS_DELETED);
1024 final VirtualFile file = beforeRevision.getFile().getVirtualFile();
1025 return new ProcessingResult(file, new Function<VcsRevisionNumber, ProcessingResult>() {
1027 public ProcessingResult fun(VcsRevisionNumber currentRevision) {
1028 if ((currentRevision != null) && (currentRevision.compareTo(beforeRevision.getRevisionNumber()) > 0)) {
1029 // revived in newer revision - possibly was added file with same name
1030 debug("File with same name was added after file deletion");
1031 return new ProcessingResult(true, BEFORE_SAME_NAME_ADDED_AFTER_DELETION);
1033 debug("File exists locally and no 'create' change found for it");
1034 return new ProcessingResult(false, BEFORE_EXISTS_BUT_SHOULD_NOT);
1041 private boolean fileMarkedForDeletion(final FilePath localPath) {
1042 final List<LocalChangeList> changeLists = myClManager.getChangeListsCopy();
1043 for (LocalChangeList list : changeLists) {
1044 final Collection<Change> changes = list.getChanges();
1045 for (Change change : changes) {
1046 if (change.getBeforeRevision() != null && change.getBeforeRevision().getFile() != null &&
1047 change.getBeforeRevision().getFile().getPath().equals(localPath.getPath())) {
1048 if (FileStatus.DELETED.equals(change.getFileStatus()) || change.isMoved() || change.isRenamed()) {
1057 // If we have an incoming add, we may have already processed the subsequent delete of the same file during
1058 // a previous incoming changes refresh. So we try to search for the deletion of this file through all
1059 // subsequent committed changelists, regardless of whether they are in "incoming" status.
1060 private boolean wasSubsequentlyDeleted(final FilePath file, long indexOffset) {
1062 indexOffset += INDEX_ENTRY_SIZE;
1063 while(indexOffset < myIndexStreamCachedLength) {
1064 IndexEntry e = getIndexEntryAtOffset(indexOffset);
1066 final CommittedChangeList changeList = getChangeListAtOffset(e.offset);
1067 for(Change c: changeList.getChanges()) {
1068 final ContentRevision beforeRevision = c.getBeforeRevision();
1069 if ((beforeRevision != null) && (c.getAfterRevision() == null)) {
1070 if (isFileDeleted(file, beforeRevision.getFile())) {
1073 } else if ((beforeRevision != null) && (c.getAfterRevision() != null)) {
1074 if (isParentReplacedOrFileMoved(file, c, beforeRevision.getFile())) {
1079 indexOffset += INDEX_ENTRY_SIZE;
1082 catch (IOException e) {
1088 private static boolean isParentReplacedOrFileMoved(@NotNull FilePath file, @NotNull Change change, @NotNull FilePath beforeFile) {
1089 boolean isParentReplaced = change.isIsReplaced() && (!file.equals(beforeFile));
1090 boolean isMovedRenamed = change.isMoved() || change.isRenamed();
1091 // call FilePath.isUnder() only if change is either "parent replaced" or moved/renamed - as many calls to FilePath.isUnder()
1092 // could take a lot of time
1093 boolean underBefore = (isParentReplaced || isMovedRenamed) && file.isUnder(beforeFile, false);
1095 if (underBefore && isParentReplaced) {
1096 debug("For " + file + "some of parents is replaced: " + beforeFile);
1099 else if (underBefore && isMovedRenamed) {
1100 debug("For " + file + "some of parents was renamed/moved: " + beforeFile);
1106 private static boolean isFileDeleted(@NotNull FilePath file, @NotNull FilePath beforeFile) {
1107 if (file.getIOFile().getAbsolutePath().equals(beforeFile.getIOFile().getAbsolutePath()) ||
1108 file.isUnder(beforeFile, false)) {
1109 debug("Found subsequent deletion for file " + file);
1115 private IndexEntry getIndexEntryAtOffset(final long indexOffset) throws IOException {
1116 IndexEntry e = myIndexEntryCache.get(indexOffset);
1118 myChangesCacheFile.myIndexStream.seek(indexOffset);
1119 e = new IndexEntry();
1120 myChangesCacheFile.readIndexEntry(e);
1121 myIndexEntryCache.put(indexOffset, e);
1126 private CommittedChangeList getChangeListAtOffset(final long offset) throws IOException {
1127 CommittedChangeList changeList = myPreviousChangeListsCache.get(offset);
1128 if (changeList == null) {
1129 changeList = myChangesCacheFile.loadChangeListAt(offset);
1130 myPreviousChangeListsCache.put(offset, changeList);
1135 private static boolean isDeletedFile(final Set<FilePath> deletedFiles,
1136 final ContentRevision afterRevision,
1137 final Set<FilePath> replacedFiles) {
1138 FilePath file = afterRevision.getFile();
1139 while(file != null) {
1140 if (deletedFiles.contains(file)) {
1143 file = file.getParentPath();
1144 if (file != null && replacedFiles.contains(file)) {
1152 private static class IndexEntry {
1156 boolean completelyDownloaded;
1159 private static class IncomingChangeListData {
1160 public long indexOffset;
1161 public IndexEntry indexEntry;
1162 public CommittedChangeList changeList;
1163 public Set<Change> accountedChanges;
1165 List<Change> getChangesToProcess() {
1166 return ContainerUtil.filter(changeList.getChanges(), new Condition<Change>() {
1168 public boolean value(Change change) {
1169 return !accountedChanges.contains(change);
1175 private static final IndexEntry[] NO_ENTRIES = new IndexEntry[0];
1177 private static class VersionMismatchException extends RuntimeException {
1180 private static class ReceivedChangeListTracker {
1181 private final Map<CommittedChangeList, ReceivedChangeList> myMap = new HashMap<CommittedChangeList, ReceivedChangeList>();
1183 public void addChange(CommittedChangeList changeList, Change change) {
1184 ReceivedChangeList list = myMap.get(changeList);
1186 list = new ReceivedChangeList(changeList);
1187 myMap.put(changeList, list);
1189 list.addChange(change);
1192 public Collection<? extends CommittedChangeList> getChangeLists() {
1193 return myMap.values();