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
;
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
;
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
;
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
{
61 public void onWorkingTreeModified(WorkingTreeModifiedEvent event
) {
62 if (event
.isEmpty()) {
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$
75 refreshRepository(event
,
76 event
.getRepository().getWorkTree().getAbsoluteFile(),
78 } catch (OperationCanceledException oe
) {
80 } catch (CoreException e
) {
81 Activator
.error(CoreText
.Activator_refreshFailed
, e
);
86 * Refresh the Eclipse workspace resources in response to a
87 * {@link WorkingTreeModifiedEvent}.
92 * of the repository this event relates to
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()) {
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
);
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
);
132 IWorkspace workspace
= ResourcesPlugin
.getWorkspace();
133 IWorkspaceRunnable operation
= innerMonitor
-> {
134 SubMonitor innerProgress
= SubMonitor
.convert(innerMonitor
,
136 if (GitTraceLocation
.REFRESH
.isActive()) {
137 GitTraceLocation
.getTrace().trace(
138 GitTraceLocation
.REFRESH
.getLocation(),
139 "Refreshing repository " + workTree
+ ' ' //$NON-NLS-1$
142 for (Map
.Entry
<IResource
, Boolean
> entry
: toRefresh
.entrySet()) {
143 if (innerProgress
.isCanceled()) {
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$
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
);
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();
188 IPath projectFilePath
= path
.append(
189 IProjectDescription
.DESCRIPTION_FILE_NAME
);
190 if (projectFilePath
.toFile().exists()) {
191 result
.put(path
, project
);
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
);
232 if (fullRefreshes
.stream()
233 .anyMatch(full
-> full
.isPrefixOf(filePath
))) {
234 // Covered by a full container refresh
238 if (!roots
.keySet().stream()
239 .anyMatch(root
-> root
.isPrefixOf(filePath
))) {
241 needRefresh
.add(path
);
247 if (path
.endsWith("/")) { //$NON-NLS-1$
248 // It's already a directory
250 containerPath
= filePath
.removeTrailingSeparator();
253 containerPath
= filePath
.removeLastSegments(1);
255 if (!handled
.containsKey(containerPath
)) {
256 if (!isFile
&& containerPath
!= null) {
257 IContainer container
= getContainerForLocation(
259 if (container
!= null) {
260 IFile file
= handled
.get(containerPath
);
261 handled
.put(containerPath
, null);
265 result
.put(container
, Boolean
.FALSE
);
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(
275 if (container
== null) {
276 lastPart
= containerPath
.lastSegment();
277 containerPath
= containerPath
278 .removeLastSegments(1);
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
287 containerPath
= containerPath
.append(lastPart
);
288 fullRefreshes
.add(containerPath
);
289 handled
.put(containerPath
, null);
291 IFile file
= container
292 .getFile(new Path(lastPart
));
293 handled
.put(containerPath
, file
);
294 result
.put(file
, Boolean
.FALSE
);
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
);
308 IFile file
= handled
.get(containerPath
);
310 // Second file in this container: replace file by
312 handled
.put(containerPath
, null);
314 result
.put(file
.getParent(), Boolean
.FALSE
);
316 // Otherwise we already have this container.
321 if (GitTraceLocation
.REFRESH
.isActive()) {
322 GitTraceLocation
.getTrace().trace(
323 GitTraceLocation
.REFRESH
.getLocation(),
324 "Calculated refresh for repository " + workTree
); //$NON-NLS-1$
329 private static IContainer
getContainerForLocation(@NonNull IPath location
) {
330 IWorkspaceRoot root
= ResourcesPlugin
.getWorkspace().getRoot();
331 IContainer dir
= root
.getContainerForLocation(location
);
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
);