| /* |
| * Copyright 2000-2014 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.concurrency.JobScheduler; |
| import com.intellij.lifecycle.PeriodicalTasksCloser; |
| import com.intellij.openapi.Disposable; |
| import com.intellij.openapi.application.Application; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.components.PersistentStateComponent; |
| import com.intellij.openapi.components.State; |
| import com.intellij.openapi.components.Storage; |
| import com.intellij.openapi.components.StoragePathMacros; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.progress.ProcessCanceledException; |
| import com.intellij.openapi.progress.ProgressManagerQueue; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.MessageType; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.openapi.util.Disposer; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.vcs.*; |
| import com.intellij.openapi.vcs.changes.Change; |
| import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl; |
| import com.intellij.openapi.vcs.impl.VcsInitObject; |
| import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier; |
| 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.Consumer; |
| import com.intellij.util.MessageBusUtil; |
| import com.intellij.util.NotNullFunction; |
| import com.intellij.util.containers.ConcurrentHashMap; |
| import com.intellij.util.containers.ContainerUtil; |
| import com.intellij.util.containers.MultiMap; |
| import com.intellij.util.messages.MessageBus; |
| import com.intellij.util.messages.MessageBusConnection; |
| import com.intellij.util.messages.Topic; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.annotations.TestOnly; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.*; |
| import java.util.concurrent.ScheduledFuture; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * @author yole |
| */ |
| @State( |
| name = "CommittedChangesCache", |
| storages = {@Storage(file = StoragePathMacros.WORKSPACE_FILE)} |
| ) |
| public class CommittedChangesCache implements PersistentStateComponent<CommittedChangesCache.State> { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.committed.CommittedChangesCache"); |
| |
| private final Project myProject; |
| private final MessageBus myBus; |
| private final ProgressManagerQueue myTaskQueue; |
| private final MessageBusConnection myConnection; |
| private boolean myRefreshingIncomingChanges = false; |
| private int myPendingUpdateCount = 0; |
| private State myState = new State(); |
| private ScheduledFuture myFuture; |
| private List<CommittedChangeList> myCachedIncomingChangeLists; |
| private final Set<CommittedChangeList> myNewIncomingChanges = new LinkedHashSet<CommittedChangeList>(); |
| private final ProjectLevelVcsManager myVcsManager; |
| |
| public static final Change[] ALL_CHANGES = new Change[0]; |
| private MyRefreshRunnable myRefresnRunnable; |
| |
| private final Map<String, Pair<Long, List<CommittedChangeList>>> myExternallyLoadedChangeLists; |
| private final CachesHolder myCachesHolder; |
| private final RepositoryLocationCache myLocationCache; |
| |
| public static class State { |
| private int myInitialCount = 500; |
| private int myInitialDays = 90; |
| private int myRefreshInterval = 30; |
| private boolean myRefreshEnabled = false; |
| |
| public int getInitialCount() { |
| return myInitialCount; |
| } |
| |
| public void setInitialCount(final int initialCount) { |
| myInitialCount = initialCount; |
| } |
| |
| public int getInitialDays() { |
| return myInitialDays; |
| } |
| |
| public void setInitialDays(final int initialDays) { |
| myInitialDays = initialDays; |
| } |
| |
| public int getRefreshInterval() { |
| return myRefreshInterval; |
| } |
| |
| public void setRefreshInterval(final int refreshInterval) { |
| myRefreshInterval = refreshInterval; |
| } |
| |
| public boolean isRefreshEnabled() { |
| return myRefreshEnabled; |
| } |
| |
| public void setRefreshEnabled(final boolean refreshEnabled) { |
| myRefreshEnabled = refreshEnabled; |
| } |
| } |
| |
| public static final Topic<CommittedChangesListener> COMMITTED_TOPIC = new Topic<CommittedChangesListener>("committed changes updates", |
| CommittedChangesListener.class); |
| |
| public static CommittedChangesCache getInstance(Project project) { |
| return PeriodicalTasksCloser.getInstance().safeGetComponent(project, CommittedChangesCache.class); |
| } |
| |
| public CommittedChangesCache(final Project project, final MessageBus bus, final ProjectLevelVcsManager vcsManager) { |
| myProject = project; |
| myBus = bus; |
| myConnection = myBus.connect(); |
| final VcsListener vcsListener = new VcsListener() { |
| @Override |
| public void directoryMappingChanged() { |
| myLocationCache.reset(); |
| refreshAllCachesAsync(false, true); |
| refreshIncomingChangesAsync(); |
| myTaskQueue.run(new Runnable() { |
| @Override |
| public void run() { |
| final List<ChangesCacheFile> files = myCachesHolder.getAllCaches(); |
| for (ChangesCacheFile file : files) { |
| final RepositoryLocation location = file.getLocation(); |
| fireChangesLoaded(location, Collections.<CommittedChangeList>emptyList()); |
| } |
| fireIncomingReloaded(); |
| } |
| }); |
| } |
| }; |
| myLocationCache = new RepositoryLocationCache(project); |
| myCachesHolder = new CachesHolder(project, myLocationCache); |
| myTaskQueue = new ProgressManagerQueue(project, VcsBundle.message("committed.changes.refresh.progress")); |
| ((ProjectLevelVcsManagerImpl) vcsManager).addInitializationRequest(VcsInitObject.COMMITTED_CHANGES_CACHE, new Runnable() { |
| @Override |
| public void run() { |
| ApplicationManager.getApplication().runReadAction(new Runnable() { |
| @Override |
| public void run() { |
| if (myProject.isDisposed()) return; |
| myTaskQueue.start(); |
| myConnection.subscribe(ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED, vcsListener); |
| myConnection.subscribe(ProjectLevelVcsManager.VCS_CONFIGURATION_CHANGED_IN_PLUGIN, vcsListener); |
| } |
| }); |
| } |
| }); |
| myVcsManager = vcsManager; |
| Disposer.register(project, new Disposable() { |
| @Override |
| public void dispose() { |
| cancelRefreshTimer(); |
| myConnection.disconnect(); |
| } |
| }); |
| myExternallyLoadedChangeLists = new ConcurrentHashMap<String, Pair<Long, List<CommittedChangeList>>>(); |
| } |
| |
| public MessageBus getMessageBus() { |
| return myBus; |
| } |
| |
| @Override |
| public State getState() { |
| return myState; |
| } |
| |
| @Override |
| public void loadState(State state) { |
| myState = state; |
| updateRefreshTimer(); |
| } |
| |
| @Nullable |
| public CommittedChangesProvider getProviderForProject() { |
| final AbstractVcs[] vcss = myVcsManager.getAllActiveVcss(); |
| List<AbstractVcs> vcsWithProviders = new ArrayList<AbstractVcs>(); |
| for(AbstractVcs vcs: vcss) { |
| if (vcs.getCommittedChangesProvider() != null) { |
| vcsWithProviders.add(vcs); |
| } |
| } |
| if (vcsWithProviders.isEmpty()) { |
| return null; |
| } |
| if (vcsWithProviders.size() == 1) { |
| return vcsWithProviders.get(0).getCommittedChangesProvider(); |
| } |
| return new CompositeCommittedChangesProvider(myProject, vcsWithProviders.toArray(new AbstractVcs[vcsWithProviders.size()])); |
| } |
| |
| public boolean isMaxCountSupportedForProject() { |
| for(AbstractVcs vcs: myVcsManager.getAllActiveVcss()) { |
| final CommittedChangesProvider provider = vcs.getCommittedChangesProvider(); |
| if (provider instanceof CachingCommittedChangesProvider) { |
| final CachingCommittedChangesProvider cachingProvider = (CachingCommittedChangesProvider)provider; |
| if (!cachingProvider.isMaxCountSupported()) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| private class MyProjectChangesLoader implements Runnable { |
| private final ChangeBrowserSettings mySettings; |
| private final int myMaxCount; |
| private final boolean myCacheOnly; |
| private final Consumer<List<CommittedChangeList>> myConsumer; |
| private final Consumer<List<VcsException>> myErrorConsumer; |
| |
| private final LinkedHashSet<CommittedChangeList> myResult = new LinkedHashSet<CommittedChangeList>(); |
| private final List<VcsException> myExceptions = new ArrayList<VcsException>(); |
| private boolean myDisposed = false; |
| |
| private MyProjectChangesLoader(ChangeBrowserSettings settings, int maxCount, boolean cacheOnly, |
| Consumer<List<CommittedChangeList>> consumer, Consumer<List<VcsException>> errorConsumer) { |
| mySettings = settings; |
| myMaxCount = maxCount; |
| myCacheOnly = cacheOnly; |
| myConsumer = consumer; |
| myErrorConsumer = errorConsumer; |
| } |
| |
| @Override |
| public void run() { |
| for(AbstractVcs vcs: myVcsManager.getAllActiveVcss()) { |
| final CommittedChangesProvider provider = vcs.getCommittedChangesProvider(); |
| if (provider == null) continue; |
| |
| final VcsCommittedListsZipper vcsZipper = provider.getZipper(); |
| CommittedListsSequencesZipper zipper = null; |
| if (vcsZipper != null) { |
| zipper = new CommittedListsSequencesZipper(vcsZipper); |
| } |
| boolean zipSupported = zipper != null; |
| |
| final Map<VirtualFile, RepositoryLocation> map = myCachesHolder.getAllRootsUnderVcs(vcs); |
| |
| for (VirtualFile root : map.keySet()) { |
| if (myProject.isDisposed()) return; |
| |
| final RepositoryLocation location = map.get(root); |
| |
| try { |
| final List<CommittedChangeList> lists = getChanges(mySettings, root, vcs, myMaxCount, myCacheOnly, provider, location); |
| if (lists != null) { |
| if (zipSupported) { |
| zipper.add(location, lists); |
| } else { |
| myResult.addAll(lists); |
| } |
| } |
| } |
| catch (VcsException e) { |
| myExceptions.add(e); |
| } |
| catch(ProcessCanceledException e) { |
| myDisposed = true; |
| } |
| } |
| |
| if (zipSupported) { |
| myResult.addAll(zipper.execute()); |
| } |
| } |
| |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| LOG.info("FINISHED CommittedChangesCache.getProjectChangesAsync - execution in queue"); |
| if (myProject.isDisposed()) { |
| return; |
| } |
| if (myExceptions.size() > 0) { |
| myErrorConsumer.consume(myExceptions); |
| } |
| else if (!myDisposed) { |
| myConsumer.consume(new ArrayList<CommittedChangeList>(myResult)); |
| } |
| } |
| }, ModalityState.NON_MODAL); |
| } |
| } |
| |
| public void getProjectChangesAsync(final ChangeBrowserSettings settings, |
| final int maxCount, |
| final boolean cacheOnly, |
| final Consumer<List<CommittedChangeList>> consumer, |
| final Consumer<List<VcsException>> errorConsumer) { |
| final MyProjectChangesLoader loader = new MyProjectChangesLoader(settings, maxCount, cacheOnly, consumer, errorConsumer); |
| myTaskQueue.run(loader); |
| } |
| |
| @Nullable |
| public List<CommittedChangeList> getChanges(ChangeBrowserSettings settings, final VirtualFile file, @NotNull final AbstractVcs vcs, |
| final int maxCount, final boolean cacheOnly, final CommittedChangesProvider provider, |
| final RepositoryLocation location) throws VcsException { |
| if (settings instanceof CompositeCommittedChangesProvider.CompositeChangeBrowserSettings) { |
| settings = ((CompositeCommittedChangesProvider.CompositeChangeBrowserSettings) settings).get(vcs); |
| } |
| if (provider instanceof CachingCommittedChangesProvider) { |
| try { |
| if (cacheOnly) { |
| ChangesCacheFile cacheFile = myCachesHolder.getCacheFile(vcs, file, location); |
| if (!cacheFile.isEmpty()) { |
| |
| final RepositoryLocation fileLocation = cacheFile.getLocation(); |
| fileLocation.onBeforeBatch(); |
| final List<CommittedChangeList> committedChangeLists = cacheFile.readChanges(settings, maxCount); |
| fileLocation.onAfterBatch(); |
| return committedChangeLists; |
| } |
| return null; |
| } |
| else { |
| if (canGetFromCache(vcs, settings, file, location, maxCount)) { |
| return getChangesWithCaching(vcs, settings, file, location, maxCount); |
| } |
| } |
| } |
| catch (IOException e) { |
| LOG.info(e); |
| } |
| } |
| //noinspection unchecked |
| return provider.getCommittedChanges(settings, location, maxCount); |
| } |
| |
| private boolean canGetFromCache(final AbstractVcs vcs, final ChangeBrowserSettings settings, |
| final VirtualFile root, final RepositoryLocation location, final int maxCount) throws IOException { |
| ChangesCacheFile cacheFile = myCachesHolder.getCacheFile(vcs, root, location); |
| if (cacheFile.isEmpty()) { |
| return true; // we'll initialize the cache and check again after that |
| } |
| if (settings.USE_DATE_BEFORE_FILTER && !settings.USE_DATE_AFTER_FILTER) { |
| return cacheFile.hasCompleteHistory(); |
| } |
| if (settings.USE_CHANGE_BEFORE_FILTER && !settings.USE_CHANGE_AFTER_FILTER) { |
| return cacheFile.hasCompleteHistory(); |
| } |
| |
| boolean hasDateFilter = settings.USE_DATE_AFTER_FILTER || settings.USE_DATE_BEFORE_FILTER || settings.USE_CHANGE_AFTER_FILTER || settings.USE_CHANGE_BEFORE_FILTER; |
| boolean hasNonDateFilter = settings.isNonDateFilterSpecified(); |
| if (!hasDateFilter && hasNonDateFilter) { |
| return cacheFile.hasCompleteHistory(); |
| } |
| if (settings.USE_DATE_AFTER_FILTER && settings.getDateAfter().getTime() < cacheFile.getFirstCachedDate().getTime()) { |
| return cacheFile.hasCompleteHistory(); |
| } |
| if (settings.USE_CHANGE_AFTER_FILTER && settings.getChangeAfterFilter().longValue() < cacheFile.getFirstCachedChangelist()) { |
| return cacheFile.hasCompleteHistory(); |
| } |
| return true; |
| } |
| |
| public void hasCachesForAnyRoot(@Nullable final Consumer<Boolean> continuation) { |
| myTaskQueue.run(new Runnable() { |
| @Override |
| public void run() { |
| final Ref<Boolean> success = new Ref<Boolean>(); |
| try { |
| success.set(hasCachesWithEmptiness(false)); |
| } |
| catch (ProcessCanceledException e) { |
| success.set(true); |
| } |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| continuation.consume(success.get()); |
| } |
| }, myProject.getDisposed()); |
| } |
| }); |
| } |
| |
| public boolean hasEmptyCaches() { |
| try { |
| return hasCachesWithEmptiness(true); |
| } |
| catch (ProcessCanceledException e) { |
| return false; |
| } |
| } |
| |
| private boolean hasCachesWithEmptiness(final boolean emptiness) { |
| final Ref<Boolean> resultRef = new Ref<Boolean>(Boolean.FALSE); |
| myCachesHolder.iterateAllCaches(new NotNullFunction<ChangesCacheFile, Boolean>() { |
| @Override |
| @NotNull |
| public Boolean fun(final ChangesCacheFile changesCacheFile) { |
| try { |
| if (changesCacheFile.isEmpty() == emptiness) { |
| resultRef.set(true); |
| return true; |
| } |
| } |
| catch (IOException e) { |
| LOG.info(e); |
| } |
| return false; |
| } |
| }); |
| return resultRef.get(); |
| } |
| |
| @Nullable |
| public Iterator<ChangesBunch> getBackBunchedIterator(final AbstractVcs vcs, final VirtualFile root, final RepositoryLocation location, final int bunchSize) { |
| final ChangesCacheFile cacheFile = myCachesHolder.getCacheFile(vcs, root, location); |
| try { |
| if (! cacheFile.isEmpty()) { |
| return cacheFile.getBackBunchedIterator(bunchSize); |
| } |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| return null; |
| } |
| |
| private List<CommittedChangeList> getChangesWithCaching(final AbstractVcs vcs, |
| final ChangeBrowserSettings settings, |
| final VirtualFile root, |
| final RepositoryLocation location, |
| final int maxCount) throws VcsException, IOException { |
| ChangesCacheFile cacheFile = myCachesHolder.getCacheFile(vcs, root, location); |
| if (cacheFile.isEmpty()) { |
| List<CommittedChangeList> changes = initCache(cacheFile); |
| if (canGetFromCache(vcs, settings, root, location, maxCount)) { |
| settings.filterChanges(changes); |
| return trimToSize(changes, maxCount); |
| } |
| //noinspection unchecked |
| return cacheFile.getProvider().getCommittedChanges(settings, location, maxCount); |
| } |
| else { |
| // we take location instance that would be used for deserialization |
| final RepositoryLocation fileLocation = cacheFile.getLocation(); |
| fileLocation.onBeforeBatch(); |
| final List<CommittedChangeList> changes = cacheFile.readChanges(settings, maxCount); |
| fileLocation.onAfterBatch(); |
| List<CommittedChangeList> newChanges = refreshCache(cacheFile); |
| settings.filterChanges(newChanges); |
| changes.addAll(newChanges); |
| return trimToSize(changes, maxCount); |
| } |
| } |
| |
| @TestOnly |
| public void refreshAllCaches() throws IOException, VcsException { |
| final Collection<ChangesCacheFile> files = myCachesHolder.getAllCaches(); |
| for(ChangesCacheFile file: files) { |
| if (file.isEmpty()) { |
| initCache(file); |
| } |
| else { |
| refreshCache(file); |
| } |
| } |
| } |
| |
| private List<CommittedChangeList> initCache(final ChangesCacheFile cacheFile) throws VcsException, IOException { |
| debug("Initializing cache for " + cacheFile.getLocation()); |
| final CachingCommittedChangesProvider provider = cacheFile.getProvider(); |
| final RepositoryLocation location = cacheFile.getLocation(); |
| final ChangeBrowserSettings settings = provider.createDefaultSettings(); |
| int maxCount = 0; |
| if (isMaxCountSupportedForProject()) { |
| maxCount = myState.getInitialCount(); |
| } |
| else { |
| settings.USE_DATE_AFTER_FILTER = true; |
| Calendar calendar = Calendar.getInstance(); |
| calendar.add(Calendar.DAY_OF_YEAR, -myState.getInitialDays()); |
| settings.setDateAfter(calendar.getTime()); |
| } |
| //noinspection unchecked |
| final List<CommittedChangeList> changes = provider.getCommittedChanges(settings, location, maxCount); |
| // when initially initializing cache, assume all changelists are locally available |
| writeChangesInReadAction(cacheFile, changes); // this sorts changes in chronological order |
| if (maxCount > 0 && changes.size() < myState.getInitialCount()) { |
| cacheFile.setHaveCompleteHistory(true); |
| } |
| if (changes.size() > 0) { |
| fireChangesLoaded(location, changes); |
| } |
| return changes; |
| } |
| |
| private void fireChangesLoaded(final RepositoryLocation location, final List<CommittedChangeList> changes) { |
| MessageBusUtil.invokeLaterIfNeededOnSyncPublisher(myProject, COMMITTED_TOPIC, new Consumer<CommittedChangesListener>() { |
| @Override |
| public void consume(CommittedChangesListener listener) { |
| listener.changesLoaded(location, changes); |
| } |
| }); |
| } |
| |
| private void fireIncomingReloaded() { |
| MessageBusUtil.invokeLaterIfNeededOnSyncPublisher(myProject, COMMITTED_TOPIC, new Consumer<CommittedChangesListener>() { |
| @Override |
| public void consume(CommittedChangesListener listener) { |
| listener.incomingChangesUpdated(Collections.<CommittedChangeList>emptyList()); |
| } |
| }); |
| } |
| |
| // todo: fix - would externally loaded nesseccerily for file? i.e. just not efficient now |
| private List<CommittedChangeList> refreshCache(final ChangesCacheFile cacheFile) throws VcsException, IOException { |
| final List<CommittedChangeList> newLists = new ArrayList<CommittedChangeList>(); |
| |
| final CachingCommittedChangesProvider provider = cacheFile.getProvider(); |
| final RepositoryLocation location = cacheFile.getLocation(); |
| |
| final Pair<Long, List<CommittedChangeList>> externalLists = myExternallyLoadedChangeLists.get(location.getKey()); |
| final long latestChangeList = getLatestListForFile(cacheFile); |
| if ((externalLists != null) && (latestChangeList == externalLists.first.longValue())) { |
| newLists.addAll(appendLoadedChanges(cacheFile, location, externalLists.second)); |
| myExternallyLoadedChangeLists.clear(); |
| } |
| |
| final ChangeBrowserSettings defaultSettings = provider.createDefaultSettings(); |
| int maxCount = 0; |
| if (provider.refreshCacheByNumber()) { |
| final long number = cacheFile.getLastCachedChangelist(); |
| debug("Refreshing cache for " + location + " since #" + number); |
| if (number >= 0) { |
| defaultSettings.CHANGE_AFTER = Long.toString(number); |
| defaultSettings.USE_CHANGE_AFTER_FILTER = true; |
| } |
| else { |
| maxCount = myState.getInitialCount(); |
| } |
| } |
| else { |
| final Date date = cacheFile.getLastCachedDate(); |
| debug("Refreshing cache for " + location + " since " + date); |
| defaultSettings.setDateAfter(date); |
| defaultSettings.USE_DATE_AFTER_FILTER = true; |
| } |
| final List<CommittedChangeList> newChanges = provider.getCommittedChanges(defaultSettings, location, maxCount); |
| debug("Loaded " + newChanges.size() + " new changelists"); |
| newLists.addAll(appendLoadedChanges(cacheFile, location, newChanges)); |
| |
| return newLists; |
| } |
| |
| private static void debug(@NonNls String message) { |
| LOG.debug(message); |
| } |
| |
| private List<CommittedChangeList> appendLoadedChanges(final ChangesCacheFile cacheFile, final RepositoryLocation location, |
| final List<CommittedChangeList> newChanges) throws IOException { |
| final List<CommittedChangeList> savedChanges = writeChangesInReadAction(cacheFile, newChanges); |
| if (savedChanges.size() > 0) { |
| fireChangesLoaded(location, savedChanges); |
| } |
| return savedChanges; |
| } |
| |
| private static List<CommittedChangeList> writeChangesInReadAction(final ChangesCacheFile cacheFile, |
| final List<CommittedChangeList> newChanges) throws IOException { |
| // ensure that changes are loaded before taking read action, to avoid stalling UI |
| for(CommittedChangeList changeList: newChanges) { |
| changeList.getChanges(); |
| } |
| final Ref<IOException> ref = new Ref<IOException>(); |
| final List<CommittedChangeList> savedChanges = ApplicationManager.getApplication().runReadAction(new Computable<List<CommittedChangeList>>() { |
| @Override |
| public List<CommittedChangeList> compute() { |
| try { |
| return cacheFile.writeChanges(newChanges); // skip duplicates; |
| } |
| catch (IOException e) { |
| ref.set(e); |
| return null; |
| } |
| } |
| }); |
| if (!ref.isNull()) { |
| throw ref.get(); |
| } |
| return savedChanges; |
| } |
| |
| private static List<CommittedChangeList> trimToSize(final List<CommittedChangeList> changes, final int maxCount) { |
| if (maxCount > 0) { |
| while(changes.size() > maxCount) { |
| changes.remove(0); |
| } |
| } |
| return changes; |
| } |
| |
| public List<CommittedChangeList> loadIncomingChanges(boolean inBackground) { |
| final List<CommittedChangeList> result = new ArrayList<CommittedChangeList>(); |
| final Collection<ChangesCacheFile> caches = myCachesHolder.getAllCaches(); |
| |
| final MultiMap<AbstractVcs, Pair<RepositoryLocation, List<CommittedChangeList>>> byVcs = |
| new MultiMap<AbstractVcs, Pair<RepositoryLocation, List<CommittedChangeList>>>(); |
| |
| for(ChangesCacheFile cache: caches) { |
| try { |
| if (inBackground && (! cache.getVcs().isVcsBackgroundOperationsAllowed(cache.getRootPath().getVirtualFile()))) continue; |
| if (!cache.isEmpty()) { |
| debug("Loading incoming changes for " + cache.getLocation()); |
| final List<CommittedChangeList> incomingChanges = cache.loadIncomingChanges(); |
| byVcs.putValue(cache.getVcs(), Pair.create(cache.getLocation(), incomingChanges)); |
| } |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| } |
| |
| for (AbstractVcs vcs : byVcs.keySet()) { |
| final CommittedChangesProvider committedChangesProvider = vcs.getCommittedChangesProvider(); |
| VcsCommittedListsZipper vcsZipper = committedChangesProvider.getZipper(); |
| if (vcsZipper != null) { |
| final VcsCommittedListsZipper incomingZipper = new IncomingListsZipper(vcsZipper); |
| final CommittedListsSequencesZipper zipper = new CommittedListsSequencesZipper(incomingZipper); |
| for (Pair<RepositoryLocation, List<CommittedChangeList>> pair : byVcs.get(vcs)) { |
| zipper.add(pair.getFirst(), pair.getSecond()); |
| } |
| result.addAll(zipper.execute()); |
| } else { |
| for (Pair<RepositoryLocation, List<CommittedChangeList>> pair : byVcs.get(vcs)) { |
| result.addAll(pair.getSecond()); |
| } |
| } |
| } |
| |
| myCachedIncomingChangeLists = result; |
| debug("Incoming changes loaded"); |
| notifyIncomingChangesUpdated(result); |
| return result; |
| } |
| |
| private static class IncomingListsZipper extends VcsCommittedListsZipperAdapter { |
| private final VcsCommittedListsZipper myVcsZipper; |
| |
| private IncomingListsZipper(final VcsCommittedListsZipper vcsZipper) { |
| super(null); |
| myVcsZipper = vcsZipper; |
| } |
| |
| @Override |
| public Pair<List<RepositoryLocationGroup>, List<RepositoryLocation>> groupLocations(final List<RepositoryLocation> in) { |
| return myVcsZipper.groupLocations(in); |
| } |
| |
| @Override |
| public CommittedChangeList zip(final RepositoryLocationGroup group, final List<CommittedChangeList> lists) { |
| if (lists.size() == 1) { |
| return lists.get(0); |
| } |
| final CommittedChangeList victim = ReceivedChangeList.unwrap(lists.get(0)); |
| final ReceivedChangeList result = new ReceivedChangeList(victim); |
| result.setForcePartial(false); |
| final Set<Change> baseChanges = new HashSet<Change>(); |
| |
| for (CommittedChangeList list : lists) { |
| baseChanges.addAll(ReceivedChangeList.unwrap(list).getChanges()); |
| |
| final Collection<Change> changes = list.getChanges(); |
| for (Change change : changes) { |
| if (! result.getChanges().contains(change)) { |
| result.addChange(change); |
| } |
| } |
| } |
| result.setForcePartial(baseChanges.size() != result.getChanges().size()); |
| return result; |
| } |
| |
| @Override |
| public long getNumber(final CommittedChangeList list) { |
| return myVcsZipper.getNumber(list); |
| } |
| } |
| |
| public void commitMessageChanged(final AbstractVcs vcs, |
| final RepositoryLocation location, final long number, final String newMessage) { |
| myTaskQueue.run(new Runnable() { |
| @Override |
| public void run() { |
| final ChangesCacheFile file = myCachesHolder.haveCache(location); |
| if (file != null) { |
| try { |
| if (file.isEmpty()) return; |
| file.editChangelist(number, newMessage); |
| loadIncomingChanges(true); |
| fireChangesLoaded(location, Collections.<CommittedChangeList>emptyList()); |
| } |
| catch (IOException e) { |
| VcsBalloonProblemNotifier.showOverChangesView(myProject, "Didn't update Repository changes with new message due to error: " + e.getMessage(), |
| MessageType.ERROR); |
| } |
| } |
| } |
| }); |
| } |
| |
| public void loadIncomingChangesAsync(@Nullable final Consumer<List<CommittedChangeList>> consumer, final boolean inBackground) { |
| debug("Loading incoming changes"); |
| final Runnable task = new Runnable() { |
| @Override |
| public void run() { |
| final List<CommittedChangeList> list = loadIncomingChanges(inBackground); |
| if (consumer != null) { |
| consumer.consume(new ArrayList<CommittedChangeList>(list)); |
| } |
| } |
| }; |
| myTaskQueue.run(task); |
| } |
| |
| public void clearCaches(final Runnable continuation) { |
| myTaskQueue.run(new Runnable() { |
| @Override |
| public void run() { |
| myCachesHolder.clearAllCaches(); |
| myCachedIncomingChangeLists = null; |
| continuation.run(); |
| MessageBusUtil.invokeLaterIfNeededOnSyncPublisher(myProject, COMMITTED_TOPIC, new Consumer<CommittedChangesListener>() { |
| @Override |
| public void consume(CommittedChangesListener listener) { |
| listener.changesCleared(); |
| } |
| }); |
| } |
| }); |
| } |
| |
| @Nullable |
| public List<CommittedChangeList> getCachedIncomingChanges() { |
| return myCachedIncomingChangeLists; |
| } |
| |
| public void processUpdatedFiles(final UpdatedFiles updatedFiles) { |
| processUpdatedFiles(updatedFiles, null); |
| } |
| |
| public void processUpdatedFiles(final UpdatedFiles updatedFiles, |
| @Nullable final Consumer<List<CommittedChangeList>> incomingChangesConsumer) { |
| final Runnable task = new Runnable() { |
| @Override |
| public void run() { |
| debug("Processing updated files"); |
| final Collection<ChangesCacheFile> caches = myCachesHolder.getAllCaches(); |
| myPendingUpdateCount += caches.size(); |
| for(final ChangesCacheFile cache: caches) { |
| try { |
| if (cache.isEmpty()) { |
| pendingUpdateProcessed(incomingChangesConsumer); |
| continue; |
| } |
| debug("Processing updated files in " + cache.getLocation()); |
| boolean needRefresh = cache.processUpdatedFiles(updatedFiles, myNewIncomingChanges); |
| if (needRefresh) { |
| debug("Found unaccounted files, requesting refresh"); |
| // todo do we need double-queueing here??? |
| processUpdatedFilesAfterRefresh(cache, updatedFiles, incomingChangesConsumer); |
| } |
| else { |
| debug("Clearing cached incoming changelists"); |
| myCachedIncomingChangeLists = null; |
| pendingUpdateProcessed(incomingChangesConsumer); |
| } |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| } |
| } |
| }; |
| myTaskQueue.run(task); |
| } |
| |
| private void pendingUpdateProcessed(@Nullable Consumer<List<CommittedChangeList>> incomingChangesConsumer) { |
| myPendingUpdateCount--; |
| if (myPendingUpdateCount == 0) { |
| notifyIncomingChangesUpdated(myNewIncomingChanges); |
| if (incomingChangesConsumer != null) { |
| incomingChangesConsumer.consume(ContainerUtil.newArrayList(myNewIncomingChanges)); |
| } |
| myNewIncomingChanges.clear(); |
| } |
| } |
| |
| private void processUpdatedFilesAfterRefresh(final ChangesCacheFile cache, |
| final UpdatedFiles updatedFiles, |
| @Nullable final Consumer<List<CommittedChangeList>> incomingChangesConsumer) { |
| refreshCacheAsync(cache, false, new RefreshResultConsumer() { |
| @Override |
| public void receivedChanges(final List<CommittedChangeList> committedChangeLists) { |
| try { |
| debug("Processing updated files after refresh in " + cache.getLocation()); |
| boolean result = true; |
| if (committedChangeLists.size() > 0) { |
| // received some new changelists, try to process updated files again |
| result = cache.processUpdatedFiles(updatedFiles, myNewIncomingChanges); |
| } |
| debug(result ? "Still have unaccounted files" : "No more unaccounted files"); |
| // for svn, we won't get exact revision numbers in updatedFiles, so we have to double-check by |
| // checking revisions we have locally |
| if (result) { |
| cache.refreshIncomingChanges(); |
| debug("Clearing cached incoming changelists"); |
| myCachedIncomingChangeLists = null; |
| } |
| pendingUpdateProcessed(incomingChangesConsumer); |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| catch(VcsException e) { |
| notifyRefreshError(e); |
| } |
| } |
| |
| @Override |
| public void receivedError(VcsException ex) { |
| notifyRefreshError(ex); |
| } |
| }); |
| } |
| |
| private void fireIncomingChangesUpdated(final List<CommittedChangeList> lists) { |
| MessageBusUtil.invokeLaterIfNeededOnSyncPublisher(myProject, COMMITTED_TOPIC, new Consumer<CommittedChangesListener>() { |
| @Override |
| public void consume(CommittedChangesListener listener) { |
| listener.incomingChangesUpdated(new ArrayList<CommittedChangeList>(lists)); |
| } |
| }); |
| } |
| |
| private void notifyIncomingChangesUpdated(@Nullable final Collection<CommittedChangeList> receivedChanges) { |
| final Collection<CommittedChangeList> changes = receivedChanges == null ? myCachedIncomingChangeLists : receivedChanges; |
| if (changes == null) { |
| final Application application = ApplicationManager.getApplication(); |
| final Runnable runnable = new Runnable() { |
| @Override |
| public void run() { |
| final List<CommittedChangeList> lists = loadIncomingChanges(true); |
| fireIncomingChangesUpdated(lists); |
| } |
| }; |
| if (application.isDispatchThread()) { |
| myTaskQueue.run(runnable); |
| } else { |
| runnable.run(); |
| } |
| return; |
| } |
| final ArrayList<CommittedChangeList> listCopy = new ArrayList<CommittedChangeList>(changes); |
| fireIncomingChangesUpdated(listCopy); |
| } |
| |
| private void notifyRefreshError(final VcsException e) { |
| MessageBusUtil.invokeLaterIfNeededOnSyncPublisher(myProject, COMMITTED_TOPIC, new Consumer<CommittedChangesListener>() { |
| @Override |
| public void consume(CommittedChangesListener listener) { |
| listener.refreshErrorStatusChanged(e); |
| } |
| }); |
| } |
| |
| private CommittedChangesListener getPublisher(final Consumer<CommittedChangesListener> listener) { |
| return ApplicationManager.getApplication().runReadAction(new Computable<CommittedChangesListener>() { |
| @Override |
| public CommittedChangesListener compute() { |
| if (myProject.isDisposed()) throw new ProcessCanceledException(); |
| return myBus.syncPublisher(COMMITTED_TOPIC); |
| } |
| }); |
| } |
| |
| public boolean isRefreshingIncomingChanges() { |
| return myRefreshingIncomingChanges; |
| } |
| |
| public boolean refreshIncomingChanges() { |
| boolean hasChanges = false; |
| final Collection<ChangesCacheFile> caches = myCachesHolder.getAllCaches(); |
| for(ChangesCacheFile file: caches) { |
| try { |
| if (file.isEmpty()) { |
| continue; |
| } |
| debug("Refreshing incoming changes for " + file.getLocation()); |
| boolean changesForCache = file.refreshIncomingChanges(); |
| hasChanges |= changesForCache; |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| catch(VcsException e) { |
| notifyRefreshError(e); |
| } |
| } |
| return hasChanges; |
| } |
| |
| public void refreshIncomingChangesAsync() { |
| debug("Refreshing incoming changes in background"); |
| myRefreshingIncomingChanges = true; |
| final Runnable task = new Runnable() { |
| @Override |
| public void run() { |
| refreshIncomingChanges(); |
| |
| refreshIncomingUi(); |
| } |
| }; |
| myTaskQueue.run(task); |
| } |
| |
| private void refreshIncomingUi() { |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| myRefreshingIncomingChanges = false; |
| debug("Incoming changes refresh complete, clearing cached incoming changes"); |
| notifyReloadIncomingChanges(); |
| } |
| }, ModalityState.NON_MODAL, myProject.getDisposed()); |
| } |
| |
| public void refreshAllCachesAsync(final boolean initIfEmpty, final boolean inBackground) { |
| final Runnable task = new Runnable() { |
| @Override |
| public void run() { |
| final List<ChangesCacheFile> files = myCachesHolder.getAllCaches(); |
| final RefreshResultConsumer notifyConsumer = new RefreshResultConsumer() { |
| private VcsException myError = null; |
| private int myCount = 0; |
| private int totalChangesCount = 0; |
| |
| @Override |
| public void receivedChanges(List<CommittedChangeList> changes) { |
| totalChangesCount += changes.size(); |
| checkDone(); |
| } |
| |
| @Override |
| public void receivedError(VcsException ex) { |
| myError = ex; |
| checkDone(); |
| } |
| |
| private void checkDone() { |
| myCount++; |
| if (myCount == files.size()) { |
| myTaskQueue.run(new Runnable() { |
| @Override |
| public void run() { |
| if (totalChangesCount > 0) { |
| notifyReloadIncomingChanges(); |
| } else { |
| myProject.getMessageBus().syncPublisher(CommittedChangesTreeBrowser.ITEMS_RELOADED).emptyRefresh(); |
| } |
| } |
| }); |
| notifyRefreshError(myError); |
| } |
| } |
| }; |
| for(ChangesCacheFile file: files) { |
| if ((! inBackground) || file.getVcs().isVcsBackgroundOperationsAllowed(file.getRootPath().getVirtualFile())) { |
| refreshCacheAsync(file, initIfEmpty, notifyConsumer, false); |
| } |
| } |
| } |
| }; |
| myTaskQueue.run(task); |
| } |
| |
| private void notifyReloadIncomingChanges() { |
| myCachedIncomingChangeLists = null; |
| notifyIncomingChangesUpdated(null); |
| } |
| |
| private void refreshCacheAsync(final ChangesCacheFile cache, final boolean initIfEmpty, |
| @Nullable final RefreshResultConsumer consumer) { |
| refreshCacheAsync(cache, initIfEmpty, consumer, true); |
| } |
| |
| private void refreshCacheAsync(final ChangesCacheFile cache, final boolean initIfEmpty, |
| @Nullable final RefreshResultConsumer consumer, final boolean asynch) { |
| try { |
| if (!initIfEmpty && cache.isEmpty()) { |
| return; |
| } |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| return; |
| } |
| final Runnable task = new Runnable() { |
| @Override |
| public void run() { |
| try { |
| final List<CommittedChangeList> list; |
| if (initIfEmpty && cache.isEmpty()) { |
| list = initCache(cache); |
| } |
| else { |
| list = refreshCache(cache); |
| } |
| if (consumer != null) { |
| consumer.receivedChanges(list); |
| } |
| } |
| catch(ProcessCanceledException ex) { |
| // ignore |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| catch (VcsException e) { |
| if (consumer != null) { |
| consumer.receivedError(e); |
| } |
| } |
| } |
| }; |
| if (asynch) { |
| myTaskQueue.run(task); |
| } else { |
| task.run(); |
| } |
| } |
| |
| private void updateRefreshTimer() { |
| cancelRefreshTimer(); |
| if (myState.isRefreshEnabled()) { |
| myRefresnRunnable = new MyRefreshRunnable(this); |
| // if "schedule with fixed rate" is used, then after waking up from stand-by mode, events are generated for inactive period |
| // it does not make sense |
| myFuture = JobScheduler.getScheduler().scheduleWithFixedDelay(myRefresnRunnable, |
| myState.getRefreshInterval()*60, myState.getRefreshInterval()*60, |
| TimeUnit.SECONDS); |
| } |
| } |
| |
| private void cancelRefreshTimer() { |
| if (myRefresnRunnable != null) { |
| myRefresnRunnable.cancel(); |
| myRefresnRunnable = null; |
| } |
| if (myFuture != null) { |
| myFuture.cancel(false); |
| myFuture = null; |
| } |
| } |
| |
| @Nullable |
| public Pair<CommittedChangeList, Change> getIncomingChangeList(final VirtualFile file) { |
| if (myCachedIncomingChangeLists != null) { |
| File ioFile = new File(file.getPath()); |
| for(CommittedChangeList changeList: myCachedIncomingChangeLists) { |
| for(Change change: changeList.getChanges()) { |
| if (change.affectsFile(ioFile)) { |
| return Pair.create(changeList, change); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| private long getLatestListForFile(final ChangesCacheFile file) { |
| try { |
| if ((file == null) || (file.isEmpty())) { |
| return -1; |
| } |
| return file.getLastCachedChangelist(); |
| } |
| catch (IOException e) { |
| return -1; |
| } |
| } |
| |
| public CachesHolder getCachesHolder() { |
| return myCachesHolder; |
| } |
| |
| public void submitExternallyLoaded(final RepositoryLocation location, final long myLastCl, final List<CommittedChangeList> lists) { |
| myExternallyLoadedChangeLists.put(location.getKey(), new Pair<Long, List<CommittedChangeList>>(myLastCl, lists)); |
| } |
| |
| private interface RefreshResultConsumer { |
| void receivedChanges(List<CommittedChangeList> changes); |
| void receivedError(VcsException ex); |
| } |
| |
| private static class MyRefreshRunnable implements Runnable { |
| private CommittedChangesCache myCache; |
| |
| private MyRefreshRunnable(final CommittedChangesCache cache) { |
| myCache = cache; |
| } |
| |
| private void cancel() { |
| myCache = null; |
| } |
| |
| @Override |
| public void run() { |
| final CommittedChangesCache cache = myCache; |
| if (cache == null) return; |
| cache.refreshAllCachesAsync(false, true); |
| final List<ChangesCacheFile> list = cache.getCachesHolder().getAllCaches(); |
| for(ChangesCacheFile file: list) { |
| if (file.getVcs().isVcsBackgroundOperationsAllowed(file.getRootPath().getVirtualFile())) { |
| if (file.getProvider().refreshIncomingWithCommitted()) { |
| cache.refreshIncomingChangesAsync(); |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| public RepositoryLocationCache getLocationCache() { |
| return myLocationCache; |
| } |
| } |