package zielu.gittoolbox.cache; import com.google.common.collect.Maps; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.messages.Topic; import git4idea.GitUtil; import git4idea.repo.GitRepository; import git4idea.repo.GitRepositoryChangeListener; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.NotNull; import zielu.gittoolbox.ProjectAware; import zielu.gittoolbox.status.GitStatusCalculator; public class PerRepoInfoCache implements GitRepositoryChangeListener, Disposable, ProjectAware { public static final Topic<PerRepoStatusCacheListener> CACHE_CHANGE = Topic.create("Status cache change", PerRepoStatusCacheListener.class); private final Logger LOG = Logger.getInstance(getClass()); private ExecutorService myUpdateExecutor; private final AtomicBoolean myActive = new AtomicBoolean(); private final ConcurrentMap<GitRepository, CachedStatus> myBehindStatuses = Maps.newConcurrentMap(); private final ConcurrentMap<GitRepository, Boolean> myScheduledRepositories = Maps.newConcurrentMap(); private final Application myApplication; private final Project myProject; private final GitStatusCalculator myCalculator; private final MessageBusConnection myRepoChangeConnection; private PerRepoInfoCache(@NotNull Application application, @NotNull Project project) { myApplication = application; myProject = project; myCalculator = GitStatusCalculator.create(project); myRepoChangeConnection = myProject.getMessageBus().connect(); myRepoChangeConnection.subscribe(GitRepository.GIT_REPO_CHANGE, this); } public static PerRepoInfoCache create(@NotNull Project project) { return new PerRepoInfoCache(ApplicationManager.getApplication(), project); } private CachedStatus get(final GitRepository repository) { CachedStatus cachedStatus = myBehindStatuses.get(repository); if (cachedStatus == null) { CachedStatus newStatus = CachedStatus.create(repository); CachedStatus foundStatus = myBehindStatuses.putIfAbsent(repository, newStatus); cachedStatus = foundStatus != null ? foundStatus : newStatus; if (cachedStatus.isNew()) { scheduleUpdate(repository); } } return cachedStatus; } private void update(GitRepository repository) { if (myActive.get()) { myApplication.runReadAction(() -> get(repository).update(repository, myCalculator, info -> onRepoChanged(repository, info))); } } @NotNull public RepoInfo getInfo(GitRepository repository) { CachedStatus cachedStatus = get(repository); return cachedStatus.get(); } @Override public void dispose() { myRepoChangeConnection.disconnect(); myBehindStatuses.clear(); myUpdateExecutor = null; myScheduledRepositories.clear(); } private void scheduleRefresh(@NotNull GitRepository repository) { final boolean debug = LOG.isDebugEnabled(); if (myActive.get()) { if (debug) { LOG.debug("Scheduled refresh for: " + repository); } scheduleTask(new RefreshTask(repository)); } else { if (debug) { LOG.debug("Inactive - ignored scheduling refresh for " + repository); } } } private void scheduleUpdate(@NotNull GitRepository repository) { final boolean debug = LOG.isDebugEnabled(); if (myActive.get()) { if (debug) { LOG.debug("Scheduled update for: " + repository); } scheduleTask(new UpdateTask(repository)); } else { if (debug) { LOG.debug("Inactive - ignored updating refresh for " + repository); } } } private void scheduleTask(CacheTask task) { if (myScheduledRepositories.putIfAbsent(task.myRepository, Boolean.TRUE) == null) { myUpdateExecutor.submit(task); LOG.debug("Scheduled ", task); } else { LOG.debug("Task for ", task.myRepository, " already scheduled"); } } @Override public void repositoryChanged(@NotNull GitRepository repository) { if (LOG.isDebugEnabled()) { LOG.debug("Got repo changed event: " + repository); } scheduleRefresh(repository); } private void onRepoChanged(GitRepository repo, RepoInfo info) { if (myActive.get()) { myProject.getMessageBus().syncPublisher(CACHE_CHANGE).stateChanged(info, repo); if (LOG.isDebugEnabled()) { LOG.debug("Published cache changed event: " + repo); } } } private void refreshSync(GitRepository repository) { get(repository).invalidate(); update(repository); } private void updateSync(GitRepository repository) { update(repository); } public void refreshAll() { LOG.info("Refreshing repositories statuses"); refresh(GitUtil.getRepositories(myProject)); } public void refresh(Iterable<GitRepository> repositories) { LOG.info("Refreshing repositories statuses"); repositories.forEach(this::scheduleRefresh); } @Override public void opened() { if (myActive.compareAndSet(false,true)) { ThreadFactoryBuilder threadBuilder = new ThreadFactoryBuilder(); myUpdateExecutor = Executors.newSingleThreadExecutor( threadBuilder.setNameFormat(getClass().getSimpleName()+"-["+myProject.getName()+"]-%d").build() ); } } @Override public void closed() { if (myActive.compareAndSet(true, false)) { myUpdateExecutor.shutdown(); } } private class RefreshTask extends CacheTask { private RefreshTask(@NotNull GitRepository myRepository) { super(myRepository); } @Override public void runImpl() { refreshSync(myRepository); } } private class UpdateTask extends CacheTask { private UpdateTask(@NotNull GitRepository myRepository) { super(myRepository); } @Override public void runImpl() { updateSync(myRepository); } } private abstract class CacheTask implements Runnable { final GitRepository myRepository; CacheTask(GitRepository repository) { this.myRepository = repository; } @Override public void run() { if (myActive.get()) { runImpl(); myScheduledRepositories.remove(myRepository); } } abstract void runImpl(); } }