Update org.apache.commons:commons-compress to 1.25.0
[egit/eclipse.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / ExternalRepositoryScanner.java
blob56b130546ca215a253c28707e450c8f21e2c7299
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
11 * Contributors:
12 * Thomas Wolf - factored out of Activator
13 *******************************************************************************/
14 package org.eclipse.egit.ui.internal;
16 import java.io.File;
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;
23 import java.util.Map;
24 import java.util.Set;
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;
59 /**
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;
74 @Override
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) {
82 scanner.schedule();
88 @Activate
89 void startUp() {
90 refreshJob = new ResourceRefreshJob();
91 scanner = new RepositoryChangeScanner(refreshJob, isActive);
92 Activator.getDefault().getPreferenceStore()
93 .addPropertyChangeListener(scanner);
96 @Deactivate
97 void shutDown() {
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);
107 scanner.cancel();
108 refreshJob.cancel();
110 try {
111 scanner.join();
112 refreshJob.join();
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()) {
148 return;
150 Repository repository = event.getRepository();
151 if (repository.isBare()) {
152 return;
154 List<String> directories = new ArrayList<>();
155 for (IProject project : RuleUtil.getProjects(repository)) {
156 if (project.isAccessible()) {
157 RepositoryMapping mapping = RepositoryMapping
158 .getMapping(project);
159 if (mapping != null
160 && repository == mapping.getRepository()) {
161 String repoRelativePath = mapping
162 .getRepoRelativePath(project);
163 if (repoRelativePath == null) {
164 continue;
166 if (GitTraceLocation.REPOSITORYCHANGESCANNER
167 .isActive()) {
168 GitTraceLocation.getTrace().trace(
169 GitTraceLocation.REPOSITORYCHANGESCANNER
170 .getLocation(),
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));
178 } else {
179 directories.add("/"); //$NON-NLS-1$
181 w.setRecursive(false);
182 while (w.next()) {
183 if (w.isSubtree()) {
184 FileTreeIterator iter = w.getTree(0,
185 FileTreeIterator.class);
186 if (iter != null
187 && !iter.isEntryIgnored()) {
188 directories
189 .add(w.getPathString() + '/');
190 w.enterSubtree();
194 } catch (IOException e) {
195 // Ignore.
197 if (GitTraceLocation.REPOSITORYCHANGESCANNER
198 .isActive()) {
199 GitTraceLocation.getTrace().trace(
200 GitTraceLocation.REPOSITORYCHANGESCANNER
201 .getLocation(),
202 "Scanned project " + project.getName()); //$NON-NLS-1$
207 if (directories.isEmpty()) {
208 return;
210 WorkingTreeModifiedEvent evt = new WorkingTreeModifiedEvent(
211 directories, null);
212 evt.setRepository(repository);
213 events.add(evt);
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());
222 setSystem(true);
223 setUser(false);
224 updateRefreshInterval();
227 @Override
228 public boolean shouldSchedule() {
229 return doReschedule;
232 @Override
233 public boolean shouldRun() {
234 return doReschedule;
237 public void setReschedule(boolean reschedule) {
238 doReschedule = reschedule;
241 @Override
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
245 // choice.
246 if (Activator.getDefault().getPreferenceStore()
247 .getBoolean(UIPreferences.REFRESH_ONLY_WHEN_ACTIVE)
248 && !workbenchActive.get()) {
249 monitor.done();
250 return Status.OK_STATUS;
253 Repository[] repos = RepositoryCache.INSTANCE.getAllRepositories();
254 if (repos.length == 0) {
255 schedule(interval);
256 return Status.OK_STATUS;
259 monitor.beginTask(UIText.Activator_scanningRepositories,
260 repos.length);
261 try {
262 events = new ArrayList<>();
263 for (Repository repo : repos) {
264 if (monitor.isCanceled()) {
265 break;
267 if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
268 GitTraceLocation.getTrace()
269 .trace(GitTraceLocation.REPOSITORYCHANGESCANNER
270 .getLocation(),
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
276 // down afterwards
277 ListenerHandle handle = null;
278 try {
279 handle = repo.getListenerList()
280 .addIndexChangedListener(listener);
281 repo.scanForRepoChanges();
282 } finally {
283 if (handle != null) {
284 handle.remove();
288 monitor.worked(1);
290 if (!monitor.isCanceled()) {
291 refresher.trigger(events);
293 events.clear();
294 } catch (IOException e) {
295 if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
296 GitTraceLocation.getTrace().trace(
297 GitTraceLocation.REPOSITORYCHANGESCANNER
298 .getLocation(),
299 "Stopped rescheduling " + getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
301 return Activator.createErrorStatus(UIText.Activator_scanError,
303 } finally {
304 monitor.done();
306 if (GitTraceLocation.REPOSITORYCHANGESCANNER.isActive()) {
307 GitTraceLocation.getTrace().trace(
308 GitTraceLocation.REPOSITORYCHANGESCANNER.getLocation(),
309 "Rescheduling " + getName() + " job"); //$NON-NLS-1$ //$NON-NLS-2$
311 schedule(interval);
312 return Status.OK_STATUS;
315 @Override
316 public void propertyChange(PropertyChangeEvent event) {
317 if (!UIPreferences.REFRESH_INDEX_INTERVAL
318 .equals(event.getProperty())) {
319 return;
321 updateRefreshInterval();
324 private void updateRefreshInterval() {
325 interval = getRefreshIndexInterval();
326 setReschedule(interval > 0);
327 cancel();
328 schedule(interval);
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);
350 setUser(false);
351 setSystem(true);
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() {
373 return gitDir;
376 public Set<String> getModified() {
377 return modified;
380 public Set<String> getDeleted() {
381 return deleted;
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());
393 return this;
397 private Map<File, WorkingTreeChanges> repositoriesChanged = new LinkedHashMap<>();
399 @Override
400 public IStatus run(IProgressMonitor monitor) {
401 try {
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,
412 changes.size());
413 try {
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.
424 progress.worked(1);
425 continue;
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,
438 false);
439 return new Status(IStatus.ERROR, Activator.PLUGIN_ID,
440 e.getMessage());
443 if (!monitor.isCanceled()) {
444 // re-schedule if we got some changes in the meantime
445 synchronized (repositoriesChanged) {
446 if (!repositoriesChanged.isEmpty()) {
447 schedule(100);
451 } finally {
452 monitor.done();
454 return Status.OK_STATUS;
458 * Record which projects have changes. Initiate a resource refresh job
459 * if the user settings allow it.
461 * @param events
462 * The {@link WorkingTreeModifiedEvent}s that triggered this
463 * refresh
465 public void trigger(Collection<WorkingTreeModifiedEvent> events) {
466 boolean haveChanges = false;
467 for (WorkingTreeModifiedEvent event : events) {
468 if (event.isEmpty()) {
469 continue;
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
478 .get(gitDir);
479 if (changes == null) {
480 repositoriesChanged.put(gitDir,
481 new WorkingTreeChanges(event));
482 } else {
483 changes.merge(event);
484 if (changes.isEmpty()) {
485 // Actually, this cannot happen.
486 repositoriesChanged.remove(gitDir);
490 haveChanges = true;
492 if (haveChanges) {
493 schedule();