blob: a10d03aad1b33088873ee27fd16c473a0b5feb2c [file] [log] [blame]
/*
* Copyright 2000-2009 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.vcs.changes.committed;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.diff.DiffProvider;
import com.intellij.openapi.vcs.diff.DiffProviderEx;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.update.FileGroup;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import com.intellij.openapi.vcs.versionBrowser.ChangeBrowserSettings;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.util.*;
import static com.intellij.openapi.vcs.changes.committed.IncomingChangeState.State.*;
/**
* @author yole
*/
public class ChangesCacheFile {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.committed.ChangesCacheFile");
private static final int VERSION = 7;
private final File myPath;
private final File myIndexPath;
private RandomAccessFile myStream;
private RandomAccessFile myIndexStream;
private boolean myStreamsOpen;
private final Project myProject;
private final AbstractVcs myVcs;
private final CachingCommittedChangesProvider myChangesProvider;
private final ProjectLevelVcsManager myVcsManager;
private final FilePath myRootPath;
private final RepositoryLocation myLocation;
private Date myFirstCachedDate;
private Date myLastCachedDate;
private long myFirstCachedChangelist;
private long myLastCachedChangelist;
private int myIncomingCount;
private boolean myHaveCompleteHistory;
private boolean myHeaderLoaded;
@NonNls private static final String INDEX_EXTENSION = ".index";
private static final int INDEX_ENTRY_SIZE = 3*8+2;
private static final int HEADER_SIZE = 46;
public ChangesCacheFile(Project project, File path, AbstractVcs vcs, VirtualFile root, RepositoryLocation location) {
reset();
myProject = project;
myPath = path;
myIndexPath = new File(myPath.toString() + INDEX_EXTENSION);
myVcs = vcs;
myChangesProvider = (CachingCommittedChangesProvider) vcs.getCommittedChangesProvider();
myVcsManager = ProjectLevelVcsManager.getInstance(project);
myRootPath = new FilePathImpl(root);
myLocation = location;
}
private void reset() {
final Calendar date = Calendar.getInstance();
date.set(2020, Calendar.FEBRUARY, 2);
myFirstCachedDate = date.getTime();
date.set(1970, Calendar.FEBRUARY, 2);
myLastCachedDate = date.getTime();
myIncomingCount = 0;
myLastCachedChangelist = -1;
myFirstCachedChangelist = Long.MAX_VALUE;
myHaveCompleteHistory = false;
myHeaderLoaded = false;
}
public RepositoryLocation getLocation() {
return myLocation;
}
public CachingCommittedChangesProvider getProvider() {
return myChangesProvider;
}
public boolean isEmpty() throws IOException {
if (!myPath.exists()) {
return true;
}
try {
loadHeader();
}
catch(VersionMismatchException ex) {
myPath.delete();
myIndexPath.delete();
return true;
}
catch(EOFException ex) {
myPath.delete();
myIndexPath.delete();
return true;
}
return false;
}
public void delete() {
FileUtil.delete(myPath);
FileUtil.delete(myIndexPath);
try {
closeStreams();
}
catch (IOException e) {
//
}
}
public List<CommittedChangeList> writeChanges(final List<CommittedChangeList> changes) throws IOException {
// the list and index are sorted in direct chronological order
Collections.sort(changes, new Comparator<CommittedChangeList>() {
public int compare(final CommittedChangeList o1, final CommittedChangeList o2) {
return Comparing.compare(o1.getCommitDate(), o2.getCommitDate());
}
});
return writeChanges(changes, null);
}
public List<CommittedChangeList> writeChanges(final List<CommittedChangeList> changes, @Nullable final List<Boolean> present) throws IOException {
assert present == null || present.size() == changes.size();
List<CommittedChangeList> result = new ArrayList<CommittedChangeList>(changes.size());
boolean wasEmpty = isEmpty();
openStreams();
try {
if (wasEmpty) {
myHeaderLoaded = true;
writeHeader();
}
myStream.seek(myStream.length());
IndexEntry[] entries = readLastIndexEntries(0, changes.size());
final Iterator<Boolean> iterator = present == null ? null : present.iterator();
for(CommittedChangeList list: changes) {
boolean duplicate = false;
for(IndexEntry entry: entries) {
if (list.getCommitDate().getTime() == entry.date && list.getNumber() == entry.number) {
duplicate = true;
break;
}
}
if (duplicate) {
debug("Skipping duplicate changelist " + list.getNumber());
continue;
}
debug("Writing incoming changelist " + list.getNumber());
result.add(list);
long position = myStream.getFilePointer();
//noinspection unchecked
myChangesProvider.writeChangeList(myStream, list);
updateCachedRange(list);
writeIndexEntry(list.getNumber(), list.getCommitDate().getTime(), position, present == null ? false : iterator.next());
myIncomingCount++;
}
writeHeader();
myHeaderLoaded = true;
}
finally {
closeStreams();
}
return result;
}
private static void debug(@NonNls String message) {
LOG.debug(message);
}
private void updateCachedRange(final CommittedChangeList list) {
if (list.getCommitDate().getTime() > myLastCachedDate.getTime()) {
myLastCachedDate = list.getCommitDate();
}
if (list.getCommitDate().getTime() < myFirstCachedDate.getTime()) {
myFirstCachedDate = list.getCommitDate();
}
if (list.getNumber() < myFirstCachedChangelist) {
myFirstCachedChangelist = list.getNumber();
}
if (list.getNumber() > myLastCachedChangelist) {
myLastCachedChangelist = list.getNumber();
}
}
private void writeIndexEntry(long number, long date, long offset, boolean completelyDownloaded) throws IOException {
myIndexStream.writeLong(number);
myIndexStream.writeLong(date);
myIndexStream.writeLong(offset);
myIndexStream.writeShort(completelyDownloaded ? 1 : 0);
}
private void openStreams() throws FileNotFoundException {
myStream = new RandomAccessFile(myPath, "rw");
myIndexStream = new RandomAccessFile(myIndexPath, "rw");
myStreamsOpen = true;
}
private void closeStreams() throws IOException {
myStreamsOpen = false;
try {
if (myStream != null) {
myStream.close();
}
}
finally {
if (myIndexStream != null) {
myIndexStream.close();
}
}
}
private void writeHeader() throws IOException {
assert myStreamsOpen && myHeaderLoaded;
myStream.seek(0);
myStream.writeInt(VERSION);
myStream.writeInt(myChangesProvider.getFormatVersion());
myStream.writeLong(myLastCachedDate.getTime());
myStream.writeLong(myFirstCachedDate.getTime());
myStream.writeLong(myFirstCachedChangelist);
myStream.writeLong(myLastCachedChangelist);
myStream.writeShort(myHaveCompleteHistory ? 1 : 0);
myStream.writeInt(myIncomingCount);
debug("Saved header for cache of " + myLocation + ": last cached date=" + myLastCachedDate +
", last cached number=" + myLastCachedChangelist + ", incoming count=" + myIncomingCount);
}
private IndexEntry[] readIndexEntriesByOffset(final long offsetFromStart, int count) throws IOException {
if (!myIndexPath.exists()) {
return NO_ENTRIES;
}
long totalCount = myIndexStream.length() / INDEX_ENTRY_SIZE;
if (count > (totalCount - offsetFromStart)) {
count = (int) (totalCount - offsetFromStart);
}
if (count == 0) {
return NO_ENTRIES;
}
// offset from start
myIndexStream.seek(INDEX_ENTRY_SIZE * offsetFromStart);
IndexEntry[] result = new IndexEntry[count];
for(int i = (count - 1); i >= 0; --i) {
result [i] = new IndexEntry();
readIndexEntry(result [i]);
}
return result;
}
private IndexEntry[] readLastIndexEntries(int offset, int count) throws IOException {
if (!myIndexPath.exists()) {
return NO_ENTRIES;
}
long totalCount = myIndexStream.length() / INDEX_ENTRY_SIZE;
if (count > totalCount - offset) {
count = (int)totalCount - offset;
}
if (count == 0) {
return NO_ENTRIES;
}
myIndexStream.seek(myIndexStream.length() - INDEX_ENTRY_SIZE * (count + offset));
IndexEntry[] result = new IndexEntry[count];
for(int i=0; i<count; i++) {
result [i] = new IndexEntry();
readIndexEntry(result [i]);
}
return result;
}
private void readIndexEntry(final IndexEntry result) throws IOException {
result.number = myIndexStream.readLong();
result.date = myIndexStream.readLong();
result.offset = myIndexStream.readLong();
result.completelyDownloaded = (myIndexStream.readShort() != 0);
}
public Date getLastCachedDate() throws IOException {
loadHeader();
return myLastCachedDate;
}
public Date getFirstCachedDate() throws IOException {
loadHeader();
return myFirstCachedDate;
}
public long getFirstCachedChangelist() throws IOException {
loadHeader();
return myFirstCachedChangelist;
}
public long getLastCachedChangelist() throws IOException {
loadHeader();
return myLastCachedChangelist;
}
private void loadHeader() throws IOException {
if (!myHeaderLoaded) {
RandomAccessFile stream = new RandomAccessFile(myPath, "r");
try {
int version = stream.readInt();
if (version != VERSION) {
throw new VersionMismatchException();
}
int providerVersion = stream.readInt();
if (providerVersion != myChangesProvider.getFormatVersion()) {
throw new VersionMismatchException();
}
myLastCachedDate = new Date(stream.readLong());
myFirstCachedDate = new Date(stream.readLong());
myFirstCachedChangelist = stream.readLong();
myLastCachedChangelist = stream.readLong();
myHaveCompleteHistory = (stream.readShort() != 0);
myIncomingCount = stream.readInt();
assert stream.getFilePointer() == HEADER_SIZE;
}
finally {
stream.close();
}
myHeaderLoaded = true;
}
}
public Iterator<ChangesBunch> getBackBunchedIterator(final int bunchSize) {
return new BackIterator(bunchSize);
}
private List<Boolean> loadAllData(final List<CommittedChangeList> lists) throws IOException {
List<Boolean> idx = new ArrayList<Boolean>();
openStreams();
try {
loadHeader();
final long length = myIndexStream.length();
long totalCount = length / INDEX_ENTRY_SIZE;
for(int i=0; i<totalCount; i++) {
final long indexOffset = length - (i + 1) * INDEX_ENTRY_SIZE;
myIndexStream.seek(indexOffset);
IndexEntry e = new IndexEntry();
readIndexEntry(e);
final CommittedChangeList list = loadChangeListAt(e.offset);
lists.add(list);
idx.add(e.completelyDownloaded);
}
} finally {
closeStreams();
}
return idx;
}
public void editChangelist(long number, String message) throws IOException {
final List<CommittedChangeList> lists = new ArrayList<CommittedChangeList>();
final List<Boolean> present = loadAllData(lists);
for (CommittedChangeList list : lists) {
if (list.getNumber() == number) {
list.setDescription(message);
break;
}
}
delete();
Collections.reverse(lists);
Collections.reverse(present);
writeChanges(lists, present);
}
private class BackIterator implements Iterator<ChangesBunch> {
private final int bunchSize;
private long myOffset;
private BackIterator(final int bunchSize) {
this.bunchSize = bunchSize;
try {
try {
openStreams();
myOffset = (myIndexStream.length() / INDEX_ENTRY_SIZE);
} finally {
closeStreams();
}
}
catch (IOException e) {
myOffset = -1;
}
}
public boolean hasNext() {
return myOffset > 0;
}
@Nullable
public ChangesBunch next() {
try {
final int size;
if (myOffset < bunchSize) {
size = (int) myOffset;
myOffset = 0;
} else {
myOffset -= bunchSize;
size = bunchSize;
}
return new ChangesBunch(readChangesInterval(myOffset, size), true);
}
catch (IOException e) {
LOG.error(e);
return null;
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
private List<CommittedChangeList> readChangesInterval(final long indexOffset, final int number) throws IOException {
openStreams();
try {
IndexEntry[] entries = readIndexEntriesByOffset(indexOffset, number);
if (entries.length == 0) {
return Collections.emptyList();
}
final List<CommittedChangeList> result = new ArrayList<CommittedChangeList>();
for (IndexEntry entry : entries) {
final CommittedChangeList changeList = loadChangeListAt(entry.offset);
result.add(changeList);
}
return result;
} finally {
closeStreams();
}
}
public List<CommittedChangeList> readChanges(final ChangeBrowserSettings settings, final int maxCount) throws IOException {
final List<CommittedChangeList> result = new ArrayList<CommittedChangeList>();
final ChangeBrowserSettings.Filter filter = settings.createFilter();
openStreams();
try {
if (maxCount == 0) {
myStream.seek(HEADER_SIZE); // skip header
while(myStream.getFilePointer() < myStream.length()) {
CommittedChangeList changeList = myChangesProvider.readChangeList(myLocation, myStream);
if (filter.accepts(changeList)) {
result.add(changeList);
}
}
}
else if (!settings.isAnyFilterSpecified()) {
IndexEntry[] entries = readLastIndexEntries(0, maxCount);
for(IndexEntry entry: entries) {
myStream.seek(entry.offset);
result.add(myChangesProvider.readChangeList(myLocation, myStream));
}
}
else {
int offset = 0;
while(result.size() < maxCount) {
IndexEntry[] entries = readLastIndexEntries(offset, 1);
if (entries.length == 0) {
break;
}
CommittedChangeList changeList = loadChangeListAt(entries [0].offset);
if (filter.accepts(changeList)) {
result.add(0, changeList);
}
offset++;
}
}
return result;
}
finally {
closeStreams();
}
}
public boolean hasCompleteHistory() {
return myHaveCompleteHistory;
}
public void setHaveCompleteHistory(final boolean haveCompleteHistory) {
if (myHaveCompleteHistory != haveCompleteHistory) {
myHaveCompleteHistory = haveCompleteHistory;
try {
openStreams();
try {
writeHeader();
}
finally {
closeStreams();
}
}
catch(IOException ex) {
LOG.error(ex);
}
}
}
public List<CommittedChangeList> loadIncomingChanges() throws IOException {
List<CommittedChangeList> result = new ArrayList<CommittedChangeList>();
int offset = 0;
openStreams();
try {
while(true) {
IndexEntry[] entries = readLastIndexEntries(offset, 1);
if (entries.length == 0) {
break;
}
if (!entries [0].completelyDownloaded) {
IncomingChangeListData data = readIncomingChangeListData(offset, entries [0]);
if (data.accountedChanges.size() == 0) {
result.add(data.changeList);
}
else {
ReceivedChangeList changeList = new ReceivedChangeList(data.changeList);
for(Change change: data.changeList.getChanges()) {
if (!data.accountedChanges.contains(change)) {
changeList.addChange(change);
}
}
result.add(changeList);
}
if (result.size() == myIncomingCount) break;
}
offset++;
}
debug("Loaded " + result.size() + " incoming changelists");
}
finally {
closeStreams();
}
return result;
}
private CommittedChangeList loadChangeListAt(final long clOffset) throws IOException {
myStream.seek(clOffset);
return myChangesProvider.readChangeList(myLocation, myStream);
}
public boolean processUpdatedFiles(UpdatedFiles updatedFiles, Collection<CommittedChangeList> receivedChanges) throws IOException {
boolean haveUnaccountedUpdatedFiles = false;
openStreams();
loadHeader();
ReceivedChangeListTracker tracker = new ReceivedChangeListTracker();
try {
final List<IncomingChangeListData> incomingData = loadIncomingChangeListData();
for(FileGroup group: updatedFiles.getTopLevelGroups()) {
haveUnaccountedUpdatedFiles |= processGroup(group, incomingData, tracker);
}
if (!haveUnaccountedUpdatedFiles) {
for(IncomingChangeListData data: incomingData) {
saveIncoming(data, false);
}
writeHeader();
}
}
finally {
closeStreams();
}
receivedChanges.addAll(tracker.getChangeLists());
return haveUnaccountedUpdatedFiles;
}
private void saveIncoming(final IncomingChangeListData data, boolean haveNoMoreIncoming) throws IOException {
writePartial(data, haveNoMoreIncoming);
if (data.accountedChanges.size() == data.changeList.getChanges().size() || haveNoMoreIncoming) {
debug("Removing changelist " + data.changeList.getNumber() + " from incoming changelists");
myIndexStream.seek(data.indexOffset);
writeIndexEntry(data.indexEntry.number, data.indexEntry.date, data.indexEntry.offset, true);
myIncomingCount--;
}
}
private boolean processGroup(final FileGroup group, final List<IncomingChangeListData> incomingData,
final ReceivedChangeListTracker tracker) {
boolean haveUnaccountedUpdatedFiles = false;
final List<Pair<String,VcsRevisionNumber>> list = group.getFilesAndRevisions(myVcsManager);
for(Pair<String, VcsRevisionNumber> pair: list) {
final String file = pair.first;
FilePath path = new FilePathImpl(new File(file), false);
if (!path.isUnder(myRootPath, false) || pair.second == null) {
continue;
}
if (group.getId().equals(FileGroup.REMOVED_FROM_REPOSITORY_ID)) {
haveUnaccountedUpdatedFiles |= processDeletedFile(path, incomingData, tracker);
}
else {
haveUnaccountedUpdatedFiles |= processFile(path, pair.second, incomingData, tracker);
}
}
for(FileGroup childGroup: group.getChildren()) {
haveUnaccountedUpdatedFiles |= processGroup(childGroup, incomingData, tracker);
}
return haveUnaccountedUpdatedFiles;
}
private static boolean processFile(final FilePath path,
final VcsRevisionNumber number,
final List<IncomingChangeListData> incomingData,
final ReceivedChangeListTracker tracker) {
boolean foundRevision = false;
debug("Processing updated file " + path + ", revision " + number);
for(IncomingChangeListData data: incomingData) {
for(Change change: data.changeList.getChanges()) {
ContentRevision afterRevision = change.getAfterRevision();
if (afterRevision != null && afterRevision.getFile().equals(path)) {
int rc = number.compareTo(afterRevision.getRevisionNumber());
if (rc == 0) {
foundRevision = true;
}
if (rc >= 0) {
tracker.addChange(data.changeList, change);
data.accountedChanges.add(change);
}
}
}
}
debug(foundRevision ? "All changes for file found" : "Some of changes for file not found");
return !foundRevision;
}
private static boolean processDeletedFile(final FilePath path,
final List<IncomingChangeListData> incomingData,
final ReceivedChangeListTracker tracker) {
boolean foundRevision = false;
for(IncomingChangeListData data: incomingData) {
for(Change change: data.changeList.getChanges()) {
ContentRevision beforeRevision = change.getBeforeRevision();
if (beforeRevision != null && beforeRevision.getFile().equals(path)) {
tracker.addChange(data.changeList, change);
data.accountedChanges.add(change);
if (change.getAfterRevision() == null) {
foundRevision = true;
}
}
}
}
return !foundRevision;
}
private List<IncomingChangeListData> loadIncomingChangeListData() throws IOException {
final long length = myIndexStream.length();
long totalCount = length / INDEX_ENTRY_SIZE;
List<IncomingChangeListData> incomingData = new ArrayList<IncomingChangeListData>();
for(int i=0; i<totalCount; i++) {
final long indexOffset = length - (i + 1) * INDEX_ENTRY_SIZE;
myIndexStream.seek(indexOffset);
IndexEntry e = new IndexEntry();
readIndexEntry(e);
if (!e.completelyDownloaded) {
incomingData.add(readIncomingChangeListData(indexOffset, e));
if (incomingData.size() == myIncomingCount) {
break;
}
}
}
debug("Loaded " + incomingData.size() + " incoming changelist pointers");
return incomingData;
}
private IncomingChangeListData readIncomingChangeListData(final long indexOffset, final IndexEntry e) throws IOException {
IncomingChangeListData data = new IncomingChangeListData();
data.indexOffset = indexOffset;
data.indexEntry = e;
data.changeList = loadChangeListAt(e.offset);
readPartial(data);
return data;
}
private void writePartial(final IncomingChangeListData data, boolean haveNoMoreIncoming) throws IOException {
File partialFile = getPartialPath(data.indexEntry.offset);
final int accounted = data.accountedChanges.size();
if (haveNoMoreIncoming || accounted == data.changeList.getChanges().size()) {
partialFile.delete();
}
else if (accounted > 0) {
RandomAccessFile file = new RandomAccessFile(partialFile, "rw");
try {
file.writeInt(accounted);
for(Change c: data.accountedChanges) {
boolean isAfterRevision = true;
ContentRevision revision = c.getAfterRevision();
if (revision == null) {
isAfterRevision = false;
revision = c.getBeforeRevision();
assert revision != null;
}
file.writeByte(isAfterRevision ? 1 : 0);
file.writeUTF(revision.getFile().getIOFile().toString());
}
}
finally {
file.close();
}
}
}
private void readPartial(IncomingChangeListData data) {
HashSet<Change> result = new HashSet<Change>();
try {
File partialFile = getPartialPath(data.indexEntry.offset);
if (partialFile.exists()) {
RandomAccessFile file = new RandomAccessFile(partialFile, "r");
try {
int count = file.readInt();
if (count > 0) {
final Collection<Change> changes = data.changeList.getChanges();
final Map<String, Change> beforePaths = new HashMap<String, Change>();
final Map<String, Change> afterPaths = new HashMap<String, Change>();
for (Change change : changes) {
if (change.getBeforeRevision() != null) {
beforePaths.put(FilePathsHelper.convertPath(change.getBeforeRevision().getFile()), change);
}
if (change.getAfterRevision() != null) {
afterPaths.put(FilePathsHelper.convertPath(change.getAfterRevision().getFile()), change);
}
}
for(int i=0; i<count; i++) {
boolean isAfterRevision = (file.readByte() != 0);
String path = file.readUTF();
final String converted = FilePathsHelper.convertPath(path);
final Change change;
if (isAfterRevision) {
change = afterPaths.get(converted);
} else {
change = beforePaths.get(converted);
}
if (change != null) {
result.add(change);
}
}
}
}
finally {
file.close();
}
}
}
catch(IOException ex) {
LOG.error(ex);
}
data.accountedChanges = result;
}
@NonNls
private File getPartialPath(final long offset) {
return new File(myPath + "." + offset + ".partial");
}
public boolean refreshIncomingChanges() throws IOException, VcsException {
if (myProject.isDisposed()) return false;
DiffProvider diffProvider = myVcs.getDiffProvider();
if (diffProvider == null) return false;
return new RefreshIncomingChangesOperation(this, myProject, diffProvider).invoke();
}
public AbstractVcs getVcs() {
return myVcs;
}
public FilePath getRootPath() {
return myRootPath;
}
private static class RefreshIncomingChangesOperation {
private final Set<FilePath> myDeletedFiles = new HashSet<FilePath>();
private final Set<FilePath> myCreatedFiles = new HashSet<FilePath>();
private final Set<FilePath> myReplacedFiles = new HashSet<FilePath>();
private final Map<Long, IndexEntry> myIndexEntryCache = new HashMap<Long, IndexEntry>();
private final Map<Long, CommittedChangeList> myPreviousChangeListsCache = new HashMap<Long, CommittedChangeList>();
private final ChangeListManagerImpl myClManager;
private final ChangesCacheFile myChangesCacheFile;
private final Project myProject;
private final DiffProvider myDiffProvider;
private boolean myAnyChanges;
private long myIndexStreamCachedLength;
RefreshIncomingChangesOperation(ChangesCacheFile changesCacheFile, Project project, final DiffProvider diffProvider) {
myChangesCacheFile = changesCacheFile;
myProject = project;
myDiffProvider = diffProvider;
myClManager = ChangeListManagerImpl.getInstanceImpl(project);
}
public boolean invoke() throws VcsException, IOException {
myChangesCacheFile.myLocation.onBeforeBatch();
final Collection<FilePath> incomingFiles = myChangesCacheFile.myChangesProvider.getIncomingFiles(myChangesCacheFile.myLocation);
myAnyChanges = false;
myChangesCacheFile.openStreams();
myChangesCacheFile.loadHeader();
try {
IncomingChangeState.header(myChangesCacheFile.myLocation.toPresentableString());
final List<IncomingChangeListData> list = myChangesCacheFile.loadIncomingChangeListData();
boolean shouldChangeHeader;
if (incomingFiles != null && incomingFiles.isEmpty()) {
// we should just delete any partial files
shouldChangeHeader = ! list.isEmpty();
for (IncomingChangeListData data : list) {
myChangesCacheFile.saveIncoming(data, true);
}
} else {
shouldChangeHeader = refreshIncomingInFile(incomingFiles, list);
}
IncomingChangeState.footer();
if (shouldChangeHeader) {
myChangesCacheFile.writeHeader();
}
}
finally {
myChangesCacheFile.myLocation.onAfterBatch();
myChangesCacheFile.closeStreams();
}
return myAnyChanges;
}
private boolean refreshIncomingInFile(Collection<FilePath> incomingFiles, List<IncomingChangeListData> list) throws IOException {
// the incoming changelist pointers are actually sorted in reverse chronological order,
// so we process file delete changes before changes made to deleted files before they were deleted
Map<Pair<IncomingChangeListData, Change>, VirtualFile> revisionDependentFiles = ContainerUtil.newHashMap();
Map<Pair<IncomingChangeListData, Change>, ProcessingResult> results = ContainerUtil.newHashMap();
myIndexStreamCachedLength = myChangesCacheFile.myIndexStream.length();
// try to process changelists in a light way, remember which files need revisions
for(IncomingChangeListData data: list) {
debug("Checking incoming changelist " + data.changeList.getNumber());
for(Change change: data.getChangesToProcess()) {
final ProcessingResult result = processIncomingChange(change, data, incomingFiles);
Pair<IncomingChangeListData, Change> key = Pair.create(data, change);
results.put(key, result);
if (result.revisionDependentProcessing != null) {
revisionDependentFiles.put(key, result.file);
}
}
}
if (!revisionDependentFiles.isEmpty()) {
// lots of same files could be collected - make set of unique files
HashSet<VirtualFile> uniqueFiles = ContainerUtil.newHashSet(revisionDependentFiles.values());
// bulk-get all needed revisions at once
Map<VirtualFile, VcsRevisionNumber> revisions = myDiffProvider instanceof DiffProviderEx
? ((DiffProviderEx)myDiffProvider).getCurrentRevisions(uniqueFiles)
: DiffProviderEx.getCurrentRevisions(uniqueFiles, myDiffProvider);
// perform processing requiring those revisions
for(IncomingChangeListData data: list) {
for (Change change : data.getChangesToProcess()) {
Pair<IncomingChangeListData, Change> key = Pair.create(data, change);
Function<VcsRevisionNumber, ProcessingResult> revisionHandler = results.get(key).revisionDependentProcessing;
if (revisionHandler != null) {
results.put(key, revisionHandler.fun(revisions.get(revisionDependentFiles.get(key))));
}
}
}
}
// collect and save processing results
for(IncomingChangeListData data: list) {
boolean updated = false;
boolean anyChangeFound = false;
for (Change change : data.getChangesToProcess()) {
final ContentRevision revision = (change.getAfterRevision() == null) ? change.getBeforeRevision() : change.getAfterRevision();
assert revision != null;
ProcessingResult result = results.get(Pair.create(data, change));
new IncomingChangeState(change, revision.getRevisionNumber().asString(), result.state).logSelf();
if (result.changeFound) {
updated = true;
data.accountedChanges.add(change);
} else {
anyChangeFound = true;
}
}
if (updated || ! anyChangeFound) {
myAnyChanges = true;
myChangesCacheFile.saveIncoming(data, !anyChangeFound);
}
}
return myAnyChanges || !list.isEmpty();
}
private static class ProcessingResult {
final boolean changeFound;
final IncomingChangeState.State state;
final VirtualFile file;
final Function<VcsRevisionNumber, ProcessingResult> revisionDependentProcessing;
private ProcessingResult(boolean changeFound, IncomingChangeState.State state) {
this.changeFound = changeFound;
this.state = state;
this.file = null;
this.revisionDependentProcessing = null;
}
private ProcessingResult(VirtualFile file, Function<VcsRevisionNumber, ProcessingResult> revisionDependentProcessing) {
this.file = file;
this.revisionDependentProcessing = revisionDependentProcessing;
this.changeFound = false;
this.state = null;
}
}
private ProcessingResult processIncomingChange(final Change change,
final IncomingChangeListData changeListData,
@Nullable final Collection<FilePath> incomingFiles) {
final CommittedChangeList changeList = changeListData.changeList;
final ContentRevision afterRevision = change.getAfterRevision();
if (afterRevision != null) {
if (afterRevision.getFile().isNonLocal()) {
// don't bother to search for non-local paths on local disk
return new ProcessingResult(true, AFTER_DOES_NOT_MATTER_NON_LOCAL);
}
if (change.getBeforeRevision() == null) {
final FilePath path = afterRevision.getFile();
debug("Marking created file " + path);
myCreatedFiles.add(path);
} else if (change.getBeforeRevision().getFile().getIOFile().getAbsolutePath().equals(
afterRevision.getFile().getIOFile().getAbsolutePath()) && change.isIsReplaced()) {
myReplacedFiles.add(afterRevision.getFile());
}
if (incomingFiles != null && !incomingFiles.contains(afterRevision.getFile())) {
debug("Skipping new/changed file outside of incoming files: " + afterRevision.getFile());
return new ProcessingResult(true, AFTER_DOES_NOT_MATTER_OUTSIDE_INCOMING);
}
debug("Checking file " + afterRevision.getFile().getPath());
FilePath localPath = ChangesUtil.getLocalPath(myProject, afterRevision.getFile());
if (! FileUtil.isAncestor(myChangesCacheFile.myRootPath.getIOFile(), localPath.getIOFile(), false)) {
// alien change in list; skip
debug("Alien path " +
localPath.getPresentableUrl() +
" under root " +
myChangesCacheFile.myRootPath.getPresentableUrl() +
"; skipping.");
return new ProcessingResult(true, AFTER_DOES_NOT_MATTER_ALIEN_PATH);
}
localPath.refresh();
final VirtualFile file = localPath.getVirtualFile();
if (isDeletedFile(myDeletedFiles, afterRevision, myReplacedFiles)) {
debug("Found deleted file");
return new ProcessingResult(true, AFTER_DOES_NOT_MATTER_DELETED_FOUND_IN_INCOMING_LIST);
}
else if (file != null) {
return new ProcessingResult(file, new Function<VcsRevisionNumber, ProcessingResult>() {
@Override
public ProcessingResult fun(VcsRevisionNumber revision) {
if (revision != null) {
debug("Current revision is " + revision + ", changelist revision is " + afterRevision.getRevisionNumber());
//noinspection unchecked
if (myChangesCacheFile.myChangesProvider
.isChangeLocallyAvailable(afterRevision.getFile(), revision, afterRevision.getRevisionNumber(), changeList)) {
return new ProcessingResult(true, AFTER_EXISTS_LOCALLY_AVAILABLE);
}
return new ProcessingResult(false, AFTER_EXISTS_NOT_LOCALLY_AVAILABLE);
}
debug("Failed to fetch revision");
return new ProcessingResult(false, AFTER_EXISTS_REVISION_NOT_LOADED);
}
});
}
else {
//noinspection unchecked
if (myChangesCacheFile.myChangesProvider.isChangeLocallyAvailable(afterRevision.getFile(), null, afterRevision.getRevisionNumber(), changeList)) {
return new ProcessingResult(true, AFTER_NOT_EXISTS_LOCALLY_AVAILABLE);
}
if (fileMarkedForDeletion(localPath)) {
debug("File marked for deletion and not committed jet.");
return new ProcessingResult(true, AFTER_NOT_EXISTS_MARKED_FOR_DELETION);
}
if (wasSubsequentlyDeleted(afterRevision.getFile(), changeListData.indexOffset)) {
return new ProcessingResult(true, AFTER_NOT_EXISTS_SUBSEQUENTLY_DELETED);
}
debug("Could not find local file for change " + afterRevision.getFile().getPath());
return new ProcessingResult(false, AFTER_NOT_EXISTS_OTHER);
}
}
else {
final ContentRevision beforeRevision = change.getBeforeRevision();
assert beforeRevision != null;
debug("Checking deleted file " + beforeRevision.getFile());
myDeletedFiles.add(beforeRevision.getFile());
if (incomingFiles != null && !incomingFiles.contains(beforeRevision.getFile())) {
debug("Skipping deleted file outside of incoming files: " + beforeRevision.getFile());
return new ProcessingResult(true, BEFORE_DOES_NOT_MATTER_OUTSIDE);
}
beforeRevision.getFile().refresh();
if (beforeRevision.getFile().getVirtualFile() == null || myCreatedFiles.contains(beforeRevision.getFile())) {
// if not deleted from vcs, mark as incoming, otherwise file already deleted
final boolean locallyDeleted = myClManager.isContainedInLocallyDeleted(beforeRevision.getFile());
debug(locallyDeleted ? "File deleted locally, change marked as incoming" : "File already deleted");
return new ProcessingResult(!locallyDeleted, locallyDeleted ? BEFORE_NOT_EXISTS_DELETED_LOCALLY : BEFORE_NOT_EXISTS_ALREADY_DELETED);
}
else if (!myChangesCacheFile.myVcs.fileExistsInVcs(beforeRevision.getFile())) {
debug("File exists locally and is unversioned");
return new ProcessingResult(true, BEFORE_UNVERSIONED_INSTEAD_OF_VERS_DELETED);
}
else {
final VirtualFile file = beforeRevision.getFile().getVirtualFile();
return new ProcessingResult(file, new Function<VcsRevisionNumber, ProcessingResult>() {
@Override
public ProcessingResult fun(VcsRevisionNumber currentRevision) {
if ((currentRevision != null) && (currentRevision.compareTo(beforeRevision.getRevisionNumber()) > 0)) {
// revived in newer revision - possibly was added file with same name
debug("File with same name was added after file deletion");
return new ProcessingResult(true, BEFORE_SAME_NAME_ADDED_AFTER_DELETION);
}
debug("File exists locally and no 'create' change found for it");
return new ProcessingResult(false, BEFORE_EXISTS_BUT_SHOULD_NOT);
}
});
}
}
}
private boolean fileMarkedForDeletion(final FilePath localPath) {
final List<LocalChangeList> changeLists = myClManager.getChangeListsCopy();
for (LocalChangeList list : changeLists) {
final Collection<Change> changes = list.getChanges();
for (Change change : changes) {
if (change.getBeforeRevision() != null && change.getBeforeRevision().getFile() != null &&
change.getBeforeRevision().getFile().getPath().equals(localPath.getPath())) {
if (FileStatus.DELETED.equals(change.getFileStatus()) || change.isMoved() || change.isRenamed()) {
return true;
}
}
}
}
return false;
}
// If we have an incoming add, we may have already processed the subsequent delete of the same file during
// a previous incoming changes refresh. So we try to search for the deletion of this file through all
// subsequent committed changelists, regardless of whether they are in "incoming" status.
private boolean wasSubsequentlyDeleted(final FilePath file, long indexOffset) {
try {
indexOffset += INDEX_ENTRY_SIZE;
while(indexOffset < myIndexStreamCachedLength) {
IndexEntry e = getIndexEntryAtOffset(indexOffset);
final CommittedChangeList changeList = getChangeListAtOffset(e.offset);
for(Change c: changeList.getChanges()) {
final ContentRevision beforeRevision = c.getBeforeRevision();
if ((beforeRevision != null) && (c.getAfterRevision() == null)) {
if (isFileDeleted(file, beforeRevision.getFile())) {
return true;
}
} else if ((beforeRevision != null) && (c.getAfterRevision() != null)) {
if (isParentReplacedOrFileMoved(file, c, beforeRevision.getFile())) {
return true;
}
}
}
indexOffset += INDEX_ENTRY_SIZE;
}
}
catch (IOException e) {
LOG.error(e);
}
return false;
}
private static boolean isParentReplacedOrFileMoved(@NotNull FilePath file, @NotNull Change change, @NotNull FilePath beforeFile) {
boolean isParentReplaced = change.isIsReplaced() && (!file.equals(beforeFile));
boolean isMovedRenamed = change.isMoved() || change.isRenamed();
// call FilePath.isUnder() only if change is either "parent replaced" or moved/renamed - as many calls to FilePath.isUnder()
// could take a lot of time
boolean underBefore = (isParentReplaced || isMovedRenamed) && file.isUnder(beforeFile, false);
if (underBefore && isParentReplaced) {
debug("For " + file + "some of parents is replaced: " + beforeFile);
return true;
}
else if (underBefore && isMovedRenamed) {
debug("For " + file + "some of parents was renamed/moved: " + beforeFile);
return true;
}
return false;
}
private static boolean isFileDeleted(@NotNull FilePath file, @NotNull FilePath beforeFile) {
if (file.getIOFile().getAbsolutePath().equals(beforeFile.getIOFile().getAbsolutePath()) ||
file.isUnder(beforeFile, false)) {
debug("Found subsequent deletion for file " + file);
return true;
}
return false;
}
private IndexEntry getIndexEntryAtOffset(final long indexOffset) throws IOException {
IndexEntry e = myIndexEntryCache.get(indexOffset);
if (e == null) {
myChangesCacheFile.myIndexStream.seek(indexOffset);
e = new IndexEntry();
myChangesCacheFile.readIndexEntry(e);
myIndexEntryCache.put(indexOffset, e);
}
return e;
}
private CommittedChangeList getChangeListAtOffset(final long offset) throws IOException {
CommittedChangeList changeList = myPreviousChangeListsCache.get(offset);
if (changeList == null) {
changeList = myChangesCacheFile.loadChangeListAt(offset);
myPreviousChangeListsCache.put(offset, changeList);
}
return changeList;
}
private static boolean isDeletedFile(final Set<FilePath> deletedFiles,
final ContentRevision afterRevision,
final Set<FilePath> replacedFiles) {
FilePath file = afterRevision.getFile();
while(file != null) {
if (deletedFiles.contains(file)) {
return true;
}
file = file.getParentPath();
if (file != null && replacedFiles.contains(file)) {
return true;
}
}
return false;
}
}
private static class IndexEntry {
long number;
long date;
long offset;
boolean completelyDownloaded;
}
private static class IncomingChangeListData {
public long indexOffset;
public IndexEntry indexEntry;
public CommittedChangeList changeList;
public Set<Change> accountedChanges;
List<Change> getChangesToProcess() {
return ContainerUtil.filter(changeList.getChanges(), new Condition<Change>() {
@Override
public boolean value(Change change) {
return !accountedChanges.contains(change);
}
});
}
}
private static final IndexEntry[] NO_ENTRIES = new IndexEntry[0];
private static class VersionMismatchException extends RuntimeException {
}
private static class ReceivedChangeListTracker {
private final Map<CommittedChangeList, ReceivedChangeList> myMap = new HashMap<CommittedChangeList, ReceivedChangeList>();
public void addChange(CommittedChangeList changeList, Change change) {
ReceivedChangeList list = myMap.get(changeList);
if (list == null) {
list = new ReceivedChangeList(changeList);
myMap.put(changeList, list);
}
list.addChange(change);
}
public Collection<? extends CommittedChangeList> getChangeLists() {
return myMap.values();
}
}
}