Change singletons to enumeration literals
[egit/eclipse.git] / org.eclipse.egit.core / src / org / eclipse / egit / core / internal / ResourceRefreshHandler.java
blobdc0f5a9c87b1207a566188401621b6717c1423e7
1 /*******************************************************************************
2 * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
3 * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com>
5 * All rights reserved. This program and the accompanying materials
6 * are made available under the terms of the Eclipse Public License 2.0
7 * which accompanies this distribution, and is available at
8 * https://www.eclipse.org/legal/epl-2.0/
10 * SPDX-License-Identifier: EPL-2.0
11 *******************************************************************************/
12 package org.eclipse.egit.core.internal;
14 import java.io.File;
15 import java.net.URI;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.stream.Stream;
27 import org.eclipse.core.filesystem.URIUtil;
28 import org.eclipse.core.resources.IContainer;
29 import org.eclipse.core.resources.IFile;
30 import org.eclipse.core.resources.IProject;
31 import org.eclipse.core.resources.IProjectDescription;
32 import org.eclipse.core.resources.IResource;
33 import org.eclipse.core.resources.IWorkspace;
34 import org.eclipse.core.resources.IWorkspaceRoot;
35 import org.eclipse.core.resources.IWorkspaceRunnable;
36 import org.eclipse.core.resources.ResourcesPlugin;
37 import org.eclipse.core.runtime.CoreException;
38 import org.eclipse.core.runtime.IPath;
39 import org.eclipse.core.runtime.IProgressMonitor;
40 import org.eclipse.core.runtime.OperationCanceledException;
41 import org.eclipse.core.runtime.Path;
42 import org.eclipse.core.runtime.SubMonitor;
43 import org.eclipse.egit.core.Activator;
44 import org.eclipse.egit.core.internal.indexdiff.IndexDiffCache;
45 import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
46 import org.eclipse.egit.core.internal.job.RuleUtil;
47 import org.eclipse.egit.core.internal.trace.GitTraceLocation;
48 import org.eclipse.jgit.annotations.NonNull;
49 import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
50 import org.eclipse.jgit.events.WorkingTreeModifiedListener;
51 import org.eclipse.jgit.lib.Repository;
53 /**
54 * Refreshes parts of the workspace changed by JGit operations. This will
55 * not refresh any git-ignored resources since those are not reported in the
56 * {@link WorkingTreeModifiedEvent}.
58 public class ResourceRefreshHandler implements WorkingTreeModifiedListener {
60 @Override
61 public void onWorkingTreeModified(WorkingTreeModifiedEvent event) {
62 if (event.isEmpty()) {
63 return;
65 Repository repo = event.getRepository();
66 if (repo == null || repo.isBare()) {
67 return; // Should never occur
69 if (GitTraceLocation.REFRESH.isActive()) {
70 GitTraceLocation.getTrace().trace(
71 GitTraceLocation.REFRESH.getLocation(),
72 "Triggered refresh for repo: " + repo); //$NON-NLS-1$
74 try {
75 refreshRepository(event,
76 event.getRepository().getWorkTree().getAbsoluteFile(),
77 null);
78 } catch (OperationCanceledException oe) {
79 return;
80 } catch (CoreException e) {
81 Activator.error(CoreText.Activator_refreshFailed, e);
85 /**
86 * Refresh the Eclipse workspace resources in response to a
87 * {@link WorkingTreeModifiedEvent}.
89 * @param event
90 * describing changes
91 * @param workTree
92 * of the repository this event relates to
93 * @param monitor
94 * for progress reporting and cancellation, may be {@code null}
95 * if neither is desired
96 * @throws CoreException
98 public void refreshRepository(WorkingTreeModifiedEvent event,
99 File workTree, IProgressMonitor monitor)
100 throws CoreException {
101 SubMonitor progress = SubMonitor.convert(monitor, 2);
102 if (progress.isCanceled()) {
103 throw new OperationCanceledException();
105 if (event.isEmpty()) {
106 progress.done();
107 return; // Should actually not occur
109 Map<IPath, IProject> roots = getProjectLocations(workTree);
110 if (roots.isEmpty()) {
111 // No open projects from this repository in the workspace
112 if (!event.getModified().isEmpty()
113 || !event.getDeleted().isEmpty()) {
114 Set<String> toRefresh = new HashSet<>(event.getModified());
115 toRefresh.addAll(event.getDeleted());
116 refreshIndex(event.getRepository(), toRefresh);
118 progress.done();
119 return;
121 IPath wt = new Path(workTree.getPath());
122 List<String> needRefresh = new ArrayList<>();
123 Map<IResource, Boolean> toRefresh = computeResources(
124 event.getModified(), event.getDeleted(), wt, roots, needRefresh,
125 progress.newChild(1));
126 if (toRefresh.isEmpty()) {
127 if (!needRefresh.isEmpty()) {
128 refreshIndex(event.getRepository(), needRefresh);
130 return;
132 IWorkspace workspace = ResourcesPlugin.getWorkspace();
133 IWorkspaceRunnable operation = innerMonitor -> {
134 SubMonitor innerProgress = SubMonitor.convert(innerMonitor,
135 toRefresh.size());
136 if (GitTraceLocation.REFRESH.isActive()) {
137 GitTraceLocation.getTrace().trace(
138 GitTraceLocation.REFRESH.getLocation(),
139 "Refreshing repository " + workTree + ' ' //$NON-NLS-1$
140 + toRefresh.size());
142 for (Map.Entry<IResource, Boolean> entry : toRefresh.entrySet()) {
143 if (innerProgress.isCanceled()) {
144 break;
146 entry.getKey().refreshLocal(
147 entry.getValue().booleanValue()
148 ? IResource.DEPTH_INFINITE
149 : IResource.DEPTH_ONE,
150 innerProgress.newChild(1));
152 if (GitTraceLocation.REFRESH.isActive()) {
153 GitTraceLocation.getTrace().trace(
154 GitTraceLocation.REFRESH.getLocation(),
155 "Refreshed repository " + workTree + ' ' //$NON-NLS-1$
156 + toRefresh.size());
159 // No scheduling rule needed; IResource.refreshLocal() gets its own
160 // rule. This workspace operation serves only to batch resource
161 // update notifications.
162 workspace.run(operation, null, IWorkspace.AVOID_UPDATE,
163 progress.newChild(1));
164 if (!needRefresh.isEmpty()) {
165 refreshIndex(event.getRepository(), needRefresh);
169 private void refreshIndex(Repository repository,
170 Collection<String> toRefresh) {
171 IndexDiffCacheEntry cache = IndexDiffCache.INSTANCE
172 .getIndexDiffCacheEntry(repository);
173 if (cache != null) {
174 cache.refreshFiles(toRefresh);
178 private Map<IPath, IProject> getProjectLocations(File workTree) {
179 IProject[] projects = RuleUtil.getProjects(workTree);
180 if (projects == null) {
181 return Collections.emptyMap();
183 Map<IPath, IProject> result = new HashMap<>();
184 for (IProject project : projects) {
185 if (project.isAccessible()) {
186 IPath path = project.getLocation();
187 if (path != null) {
188 IPath projectFilePath = path.append(
189 IProjectDescription.DESCRIPTION_FILE_NAME);
190 if (projectFilePath.toFile().exists()) {
191 result.put(path, project);
196 return result;
199 private Map<IResource, Boolean> computeResources(
200 Collection<String> modified, Collection<String> deleted,
201 IPath workTree, Map<IPath, IProject> roots,
202 Collection<String> needRefresh, IProgressMonitor monitor) {
203 // Attempt to minimize the refreshes by returning IContainers if
204 // more than one file in a container has changed.
205 if (GitTraceLocation.REFRESH.isActive()) {
206 GitTraceLocation.getTrace().trace(
207 GitTraceLocation.REFRESH.getLocation(),
208 "Calculating refresh for repository " + workTree + ' ' //$NON-NLS-1$
209 + modified.size() + ' ' + deleted.size());
211 SubMonitor progress = SubMonitor.convert(monitor,
212 modified.size() + deleted.size());
213 Set<IPath> fullRefreshes = new HashSet<>();
214 Map<IPath, IFile> handled = new HashMap<>();
215 Map<IResource, Boolean> result = new HashMap<>();
216 Stream.concat(modified.stream(), deleted.stream()).forEach(path -> {
217 if (progress.isCanceled()) {
218 throw new OperationCanceledException();
220 IPath filePath = "/".equals(path) ? workTree //$NON-NLS-1$
221 : workTree.append(path);
222 IProject project = roots.get(filePath);
223 if (project != null) {
224 // Eclipse knows this as a project. Make sure it gets
225 // refreshed as such. One can refresh a folder via an IFile,
226 // but not an IProject.
227 handled.put(filePath, null);
228 result.put(project, Boolean.FALSE);
229 progress.worked(1);
230 return;
232 if (fullRefreshes.stream()
233 .anyMatch(full -> full.isPrefixOf(filePath))) {
234 // Covered by a full container refresh
235 progress.worked(1);
236 return;
238 if (!roots.keySet().stream()
239 .anyMatch(root -> root.isPrefixOf(filePath))) {
240 // Not in workspace.
241 needRefresh.add(path);
242 progress.worked(1);
243 return;
245 IPath containerPath;
246 boolean isFile;
247 if (path.endsWith("/")) { //$NON-NLS-1$
248 // It's already a directory
249 isFile = false;
250 containerPath = filePath.removeTrailingSeparator();
251 } else {
252 isFile = true;
253 containerPath = filePath.removeLastSegments(1);
255 if (!handled.containsKey(containerPath)) {
256 if (!isFile && containerPath != null) {
257 IContainer container = getContainerForLocation(
258 containerPath);
259 if (container != null) {
260 IFile file = handled.get(containerPath);
261 handled.put(containerPath, null);
262 if (file != null) {
263 result.remove(file);
265 result.put(container, Boolean.FALSE);
267 } else if (isFile) {
268 // First file in this container. Find the deepest
269 // existing container and record its child.
270 String lastPart = filePath.lastSegment();
271 while (containerPath != null
272 && workTree.isPrefixOf(containerPath)) {
273 IContainer container = getContainerForLocation(
274 containerPath);
275 if (container == null) {
276 lastPart = containerPath.lastSegment();
277 containerPath = containerPath
278 .removeLastSegments(1);
279 isFile = false;
280 continue;
282 if (container.getType() == IResource.ROOT) {
283 // Missing project... ignore it and anything
284 // beneath. The user or our own branch project
285 // tracker will have to properly add/import the
286 // project.
287 containerPath = containerPath.append(lastPart);
288 fullRefreshes.add(containerPath);
289 handled.put(containerPath, null);
290 } else if (isFile) {
291 IFile file = container
292 .getFile(new Path(lastPart));
293 handled.put(containerPath, file);
294 result.put(file, Boolean.FALSE);
295 } else {
296 // New or deleted folder.
297 container = container
298 .getFolder(new Path(lastPart));
299 containerPath = containerPath.append(lastPart);
300 fullRefreshes.add(containerPath);
301 handled.put(containerPath, null);
302 result.put(container, Boolean.TRUE);
304 break;
307 } else {
308 IFile file = handled.get(containerPath);
309 if (file != null) {
310 // Second file in this container: replace file by
311 // its container.
312 handled.put(containerPath, null);
313 result.remove(file);
314 result.put(file.getParent(), Boolean.FALSE);
316 // Otherwise we already have this container.
318 progress.worked(1);
321 if (GitTraceLocation.REFRESH.isActive()) {
322 GitTraceLocation.getTrace().trace(
323 GitTraceLocation.REFRESH.getLocation(),
324 "Calculated refresh for repository " + workTree); //$NON-NLS-1$
326 return result;
329 private static IContainer getContainerForLocation(@NonNull IPath location) {
330 IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
331 IContainer dir = root.getContainerForLocation(location);
332 if (dir == null) {
333 return null;
335 if (isValid(dir)) {
336 return dir;
338 URI uri = URIUtil.toURI(location);
339 IContainer[] containers = root.findContainersForLocationURI(uri);
340 return Arrays.stream(containers).filter(ResourceRefreshHandler::isValid)
341 .findFirst().orElse(null);
344 private static boolean isValid(@NonNull IResource resource) {
345 return resource.isAccessible()
346 && !resource.isLinked(IResource.CHECK_ANCESTORS);