1 /*******************************************************************************
2 * Copyright (c) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others.
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License 2.0
6 * which accompanies this distribution, and is available at
7 * https://www.eclipse.org/legal/epl-2.0/
9 * SPDX-License-Identifier: EPL-2.0
12 * Thomas Wolf - factored out of Activator
13 *******************************************************************************/
14 package org
.eclipse
.egit
.ui
.internal
;
17 import java
.io
.IOException
;
18 import java
.util
.ArrayList
;
19 import java
.util
.Collection
;
20 import java
.util
.HashSet
;
21 import java
.util
.LinkedHashMap
;
22 import java
.util
.List
;
25 import java
.util
.concurrent
.atomic
.AtomicBoolean
;
27 import org
.eclipse
.core
.resources
.IProject
;
28 import org
.eclipse
.core
.runtime
.CoreException
;
29 import org
.eclipse
.core
.runtime
.IProgressMonitor
;
30 import org
.eclipse
.core
.runtime
.IStatus
;
31 import org
.eclipse
.core
.runtime
.OperationCanceledException
;
32 import org
.eclipse
.core
.runtime
.Status
;
33 import org
.eclipse
.core
.runtime
.SubMonitor
;
34 import org
.eclipse
.core
.runtime
.jobs
.Job
;
35 import org
.eclipse
.e4
.core
.services
.events
.IEventBroker
;
36 import org
.eclipse
.egit
.core
.RepositoryCache
;
37 import org
.eclipse
.egit
.core
.internal
.ResourceRefreshHandler
;
38 import org
.eclipse
.egit
.core
.internal
.job
.RuleUtil
;
39 import org
.eclipse
.egit
.core
.project
.RepositoryMapping
;
40 import org
.eclipse
.egit
.ui
.Activator
;
41 import org
.eclipse
.egit
.ui
.UIPreferences
;
42 import org
.eclipse
.egit
.ui
.internal
.trace
.GitTraceLocation
;
43 import org
.eclipse
.jface
.util
.IPropertyChangeListener
;
44 import org
.eclipse
.jface
.util
.PropertyChangeEvent
;
45 import org
.eclipse
.jgit
.events
.IndexChangedListener
;
46 import org
.eclipse
.jgit
.events
.ListenerHandle
;
47 import org
.eclipse
.jgit
.events
.WorkingTreeModifiedEvent
;
48 import org
.eclipse
.jgit
.lib
.Repository
;
49 import org
.eclipse
.jgit
.treewalk
.FileTreeIterator
;
50 import org
.eclipse
.jgit
.treewalk
.TreeWalk
;
51 import org
.eclipse
.jgit
.treewalk
.filter
.PathFilterGroup
;
52 import org
.osgi
.service
.component
.annotations
.Activate
;
53 import org
.osgi
.service
.component
.annotations
.Component
;
54 import org
.osgi
.service
.component
.annotations
.Deactivate
;
55 import org
.osgi
.service
.event
.Event
;
56 import org
.osgi
.service
.event
.EventConstants
;
57 import org
.osgi
.service
.event
.EventHandler
;
60 * A component that scans for external changes made to git repositories.
61 * Depending on user preference setting, this scanning is done only when the
62 * workbench is active.
64 @Component(property
= EventConstants
.EVENT_TOPIC
+ '='
65 + ApplicationActiveListener
.TOPIC_APPLICATION_ACTIVE
)
66 public class ExternalRepositoryScanner
implements EventHandler
{
68 private AtomicBoolean isActive
= new AtomicBoolean();
70 private ResourceRefreshJob refreshJob
;
72 private RepositoryChangeScanner scanner
;
75 public void handleEvent(Event event
) {
76 if (ApplicationActiveListener
.TOPIC_APPLICATION_ACTIVE
77 .equals(event
.getTopic())) {
78 Object value
= event
.getProperty(IEventBroker
.DATA
);
79 if (value
instanceof Boolean
) {
80 boolean newValue
= ((Boolean
) value
).booleanValue();
81 if (isActive
.compareAndSet(!newValue
, newValue
) && newValue
) {
90 refreshJob
= new ResourceRefreshJob();
91 scanner
= new RepositoryChangeScanner(refreshJob
, isActive
);
92 Activator
.getDefault().getPreferenceStore()
93 .addPropertyChangeListener(scanner
);
98 if (GitTraceLocation
.REPOSITORYCHANGESCANNER
.isActive()) {
99 GitTraceLocation
.getTrace().trace(
100 GitTraceLocation
.REPOSITORYCHANGESCANNER
.getLocation(),
101 "Trying to cancel " + scanner
.getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
104 Activator
.getDefault().getPreferenceStore()
105 .removePropertyChangeListener(scanner
);
106 scanner
.setReschedule(false);
113 if (GitTraceLocation
.REPOSITORYCHANGESCANNER
.isActive()) {
114 GitTraceLocation
.getTrace().trace(
115 GitTraceLocation
.REPOSITORYCHANGESCANNER
.getLocation(),
116 "Jobs terminated"); //$NON-NLS-1$
118 } catch (InterruptedException e
) {
119 if (GitTraceLocation
.REPOSITORYCHANGESCANNER
.isActive()) {
120 GitTraceLocation
.getTrace().trace(
121 GitTraceLocation
.REPOSITORYCHANGESCANNER
.getLocation(),
122 "Jobs termination interrupted"); //$NON-NLS-1$
124 Thread
.currentThread().interrupt();
129 * A Job that looks at the repository meta data and triggers a refresh of
130 * the resources in the affected projects.
132 private static class RepositoryChangeScanner
extends Job
133 implements IPropertyChangeListener
{
135 // volatile in order to ensure thread synchronization
136 private volatile boolean doReschedule
;
138 private volatile int interval
;
140 private final ResourceRefreshJob refresher
;
142 private final AtomicBoolean workbenchActive
;
144 private Collection
<WorkingTreeModifiedEvent
> events
;
146 private final IndexChangedListener listener
= event
-> {
147 if (event
.isInternal()) {
150 Repository repository
= event
.getRepository();
151 if (repository
.isBare()) {
154 List
<String
> directories
= new ArrayList
<>();
155 for (IProject project
: RuleUtil
.getProjects(repository
)) {
156 if (project
.isAccessible()) {
157 RepositoryMapping mapping
= RepositoryMapping
158 .getMapping(project
);
160 && repository
== mapping
.getRepository()) {
161 String repoRelativePath
= mapping
162 .getRepoRelativePath(project
);
163 if (repoRelativePath
== null) {
166 if (GitTraceLocation
.REPOSITORYCHANGESCANNER
168 GitTraceLocation
.getTrace().trace(
169 GitTraceLocation
.REPOSITORYCHANGESCANNER
171 "Scanning project " + project
.getName()); //$NON-NLS-1$
173 try (TreeWalk w
= new TreeWalk(repository
)) {
174 w
.addTree(new FileTreeIterator(repository
));
175 if (!repoRelativePath
.isEmpty()) {
176 w
.setFilter(PathFilterGroup
177 .createFromStrings(repoRelativePath
));
179 directories
.add("/"); //$NON-NLS-1$
181 w
.setRecursive(false);
184 FileTreeIterator iter
= w
.getTree(0,
185 FileTreeIterator
.class);
187 && !iter
.isEntryIgnored()) {
189 .add(w
.getPathString() + '/');
194 } catch (IOException e
) {
197 if (GitTraceLocation
.REPOSITORYCHANGESCANNER
199 GitTraceLocation
.getTrace().trace(
200 GitTraceLocation
.REPOSITORYCHANGESCANNER
202 "Scanned project " + project
.getName()); //$NON-NLS-1$
207 if (directories
.isEmpty()) {
210 WorkingTreeModifiedEvent evt
= new WorkingTreeModifiedEvent(
212 evt
.setRepository(repository
);
216 public RepositoryChangeScanner(ResourceRefreshJob refresher
,
217 AtomicBoolean workbenchActive
) {
218 super(UIText
.Activator_repoScanJobName
);
219 this.refresher
= refresher
;
220 this.workbenchActive
= workbenchActive
;
221 setRule(new RepositoryCacheRule());
224 updateRefreshInterval();
228 public boolean shouldSchedule() {
233 public boolean shouldRun() {
237 public void setReschedule(boolean reschedule
) {
238 doReschedule
= reschedule
;
242 protected IStatus
run(IProgressMonitor monitor
) {
243 // When people use Git from the command line a lot of changes
244 // may happen. Don't scan when inactive depending on the user's
246 if (Activator
.getDefault().getPreferenceStore()
247 .getBoolean(UIPreferences
.REFRESH_ONLY_WHEN_ACTIVE
)
248 && !workbenchActive
.get()) {
250 return Status
.OK_STATUS
;
253 Repository
[] repos
= RepositoryCache
.INSTANCE
.getAllRepositories();
254 if (repos
.length
== 0) {
256 return Status
.OK_STATUS
;
259 monitor
.beginTask(UIText
.Activator_scanningRepositories
,
262 events
= new ArrayList
<>();
263 for (Repository repo
: repos
) {
264 if (monitor
.isCanceled()) {
267 if (GitTraceLocation
.REPOSITORYCHANGESCANNER
.isActive()) {
268 GitTraceLocation
.getTrace()
269 .trace(GitTraceLocation
.REPOSITORYCHANGESCANNER
271 "Scanning " + repo
+ " for changes"); //$NON-NLS-1$ //$NON-NLS-2$
274 if (!repo
.isBare()) {
275 // Set up index change listener for the repo and tear it
277 ListenerHandle handle
= null;
279 handle
= repo
.getListenerList()
280 .addIndexChangedListener(listener
);
281 repo
.scanForRepoChanges();
283 if (handle
!= null) {
290 if (!monitor
.isCanceled()) {
291 refresher
.trigger(events
);
294 } catch (IOException e
) {
295 if (GitTraceLocation
.REPOSITORYCHANGESCANNER
.isActive()) {
296 GitTraceLocation
.getTrace().trace(
297 GitTraceLocation
.REPOSITORYCHANGESCANNER
299 "Stopped rescheduling " + getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
301 return Activator
.createErrorStatus(UIText
.Activator_scanError
,
306 if (GitTraceLocation
.REPOSITORYCHANGESCANNER
.isActive()) {
307 GitTraceLocation
.getTrace().trace(
308 GitTraceLocation
.REPOSITORYCHANGESCANNER
.getLocation(),
309 "Rescheduling " + getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
312 return Status
.OK_STATUS
;
316 public void propertyChange(PropertyChangeEvent event
) {
317 if (!UIPreferences
.REFRESH_INDEX_INTERVAL
318 .equals(event
.getProperty())) {
321 updateRefreshInterval();
324 private void updateRefreshInterval() {
325 interval
= getRefreshIndexInterval();
326 setReschedule(interval
> 0);
332 * @return interval in milliseconds for automatic index check, 0 is if
333 * check should be disabled
335 private static int getRefreshIndexInterval() {
336 return 1000 * Activator
.getDefault().getPreferenceStore()
337 .getInt(UIPreferences
.REFRESH_INDEX_INTERVAL
);
342 * Refreshes parts of the workspace changed by JGit operations. This will
343 * not refresh any git-ignored resources since those are not reported in the
344 * {@link WorkingTreeModifiedEvent}.
346 private static class ResourceRefreshJob
extends Job
{
348 public ResourceRefreshJob() {
349 super(UIText
.Activator_refreshJobName
);
355 * Internal helper class to record batched accumulated results from
356 * several {@link WorkingTreeModifiedEvent}s.
358 private static class WorkingTreeChanges
{
360 private final File gitDir
;
362 private final Set
<String
> modified
;
364 private final Set
<String
> deleted
;
366 public WorkingTreeChanges(WorkingTreeModifiedEvent event
) {
367 gitDir
= event
.getRepository().getDirectory();
368 modified
= new HashSet
<>(event
.getModified());
369 deleted
= new HashSet
<>(event
.getDeleted());
372 public File
getGitDirectory() {
376 public Set
<String
> getModified() {
380 public Set
<String
> getDeleted() {
384 public boolean isEmpty() {
385 return modified
.isEmpty() && deleted
.isEmpty();
388 public WorkingTreeChanges
merge(WorkingTreeModifiedEvent event
) {
389 modified
.removeAll(event
.getDeleted());
390 deleted
.removeAll(event
.getModified());
391 modified
.addAll(event
.getModified());
392 deleted
.addAll(event
.getDeleted());
397 private Map
<File
, WorkingTreeChanges
> repositoriesChanged
= new LinkedHashMap
<>();
400 public IStatus
run(IProgressMonitor monitor
) {
402 List
<WorkingTreeChanges
> changes
;
403 synchronized (repositoriesChanged
) {
404 if (repositoriesChanged
.isEmpty()) {
405 return Status
.OK_STATUS
;
407 changes
= new ArrayList
<>(repositoriesChanged
.values());
408 repositoriesChanged
.clear();
411 SubMonitor progress
= SubMonitor
.convert(monitor
,
414 for (WorkingTreeChanges change
: changes
) {
415 if (progress
.isCanceled()) {
416 return Status
.CANCEL_STATUS
;
418 ResourceRefreshHandler handler
= new ResourceRefreshHandler();
419 Repository repo
= RepositoryCache
.INSTANCE
420 .getRepository(change
.getGitDirectory());
421 if (repo
== null || repo
.isBare()) {
422 // Repo has vanished or suddenly become a bare repo?
423 // No point updating anything.
427 WorkingTreeModifiedEvent event
= new WorkingTreeModifiedEvent(
428 change
.getModified(), change
.getDeleted());
429 event
.setRepository(repo
);
430 handler
.refreshRepository(event
,
431 repo
.getWorkTree().getAbsoluteFile(),
432 progress
.newChild(1));
434 } catch (OperationCanceledException oe
) {
435 return Status
.CANCEL_STATUS
;
436 } catch (CoreException e
) {
437 Activator
.handleError(UIText
.Activator_refreshFailed
, e
,
439 return new Status(IStatus
.ERROR
, Activator
.PLUGIN_ID
,
443 if (!monitor
.isCanceled()) {
444 // re-schedule if we got some changes in the meantime
445 synchronized (repositoriesChanged
) {
446 if (!repositoriesChanged
.isEmpty()) {
454 return Status
.OK_STATUS
;
458 * Record which projects have changes. Initiate a resource refresh job
459 * if the user settings allow it.
462 * The {@link WorkingTreeModifiedEvent}s that triggered this
465 public void trigger(Collection
<WorkingTreeModifiedEvent
> events
) {
466 boolean haveChanges
= false;
467 for (WorkingTreeModifiedEvent event
: events
) {
468 if (event
.isEmpty()) {
471 Repository repo
= event
.getRepository();
472 if (repo
== null || repo
.isBare()) {
473 continue; // Should never occur
475 File gitDir
= repo
.getDirectory();
476 synchronized (repositoriesChanged
) {
477 WorkingTreeChanges changes
= repositoriesChanged
479 if (changes
== null) {
480 repositoriesChanged
.put(gitDir
,
481 new WorkingTreeChanges(event
));
483 changes
.merge(event
);
484 if (changes
.isEmpty()) {
485 // Actually, this cannot happen.
486 repositoriesChanged
.remove(gitDir
);