Reduce allocations in decorator
[egit/eclipse.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / resources / ResourceStateFactory.java
blobd65b477f4f530d35d72682e1ff471c9721a1e791
1 /*******************************************************************************
2 * Copyright (C) 2007, IBM Corporation and others
3 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
4 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
5 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
6 * Copyright (C) 2008, Google Inc.
7 * Copyright (C) 2008, Tor Arne Vestbø <torarnv@gmail.com>
8 * Copyright (C) 2011, Dariusz Luksza <dariusz@luksza.org>
9 * Copyright (C) 2011, Christian Halstrick <christian.halstrick@sap.com>
10 * Copyright (C) 2015, Thomas Wolf <thomas.wolf@paranor.ch>
12 * All rights reserved. This program and the accompanying materials
13 * are made available under the terms of the Eclipse Public License v1.0
14 * which accompanies this distribution, and is available at
15 * http://www.eclipse.org/legal/epl-v10.html
17 * Contributors:
18 * Thomas Wolf <thomas.wolf@paranor.ch> - Factored out from DecoratableResourceAdapter
19 * and GitLightweightDecorator
20 *******************************************************************************/
21 package org.eclipse.egit.ui.internal.resources;
23 import java.io.File;
24 import java.io.IOException;
25 import java.nio.file.FileVisitResult;
26 import java.nio.file.FileVisitor;
27 import java.nio.file.Files;
28 import java.nio.file.Path;
29 import java.nio.file.Paths;
30 import java.nio.file.attribute.BasicFileAttributes;
31 import java.util.HashSet;
32 import java.util.Set;
34 import org.eclipse.core.resources.IContainer;
35 import org.eclipse.core.resources.IResource;
36 import org.eclipse.core.runtime.CoreException;
37 import org.eclipse.core.runtime.IPath;
38 import org.eclipse.egit.core.Activator;
39 import org.eclipse.egit.core.internal.indexdiff.IndexDiffCacheEntry;
40 import org.eclipse.egit.core.internal.indexdiff.IndexDiffData;
41 import org.eclipse.egit.core.internal.util.ResourceUtil;
42 import org.eclipse.egit.ui.internal.resources.IResourceState.StagingState;
43 import org.eclipse.jgit.annotations.NonNull;
44 import org.eclipse.jgit.annotations.Nullable;
45 import org.eclipse.jgit.lib.Constants;
46 import org.eclipse.jgit.lib.Repository;
48 /**
49 * Factory for creating {@link IResourceState}s.
51 public class ResourceStateFactory {
53 /**
54 * {@link IResourceState} returned when no information can be retrieved. All
55 * boolean getters return {@code false}, and the
56 * {@link IResourceState.StagingState StagingState} is
57 * {@link IResourceState.StagingState#NOT_STAGED NOT_STAGED}.
59 @NonNull
60 public static final IResourceState UNKNOWN_STATE = new ResourceState();
62 /**
63 * Singleton {@link IResourceState} returned for ignored files.
65 @NonNull
66 private static final IResourceState IGNORED = new ResourceState() {
68 @Override
69 public boolean isIgnored() {
70 return true;
74 @NonNull
75 private static final ResourceStateFactory INSTANCE = new ResourceStateFactory();
77 /**
78 * Retrieves the singleton instance of the {@link ResourceStateFactory}.
80 * @return the factory singleton
82 @NonNull
83 public static ResourceStateFactory getInstance() {
84 return INSTANCE;
87 /**
88 * Returns the {@link IndexDiffData} for a given {@link IResource}, provided
89 * the resource exists and belongs to a git-tracked project.
91 * @param resource
92 * context to get the repository to get the index diff data from
93 * @return the IndexDiffData, or {@code null} if none.
95 @Nullable
96 public IndexDiffData getIndexDiffDataOrNull(@Nullable IResource resource) {
97 if (resource == null || resource.getType() == IResource.ROOT
98 || !ResourceUtil.isSharedWithGit(resource)) {
99 return null;
101 Repository repository = ResourceUtil.getRepository(resource);
102 return getIndexDiffDataOrNull(repository);
106 * Returns the {@link IndexDiffData} for a given {@link File}, provided the
107 * file is in a git repository working tree.
109 * @param file
110 * context to get the repository to get the index diff data from
111 * @return the IndexDiffData, or {@code null} if none.
113 @Nullable
114 public IndexDiffData getIndexDiffDataOrNull(@Nullable File file) {
115 if (file == null) {
116 return null;
118 File absoluteFile = file.getAbsoluteFile();
119 IPath path = new org.eclipse.core.runtime.Path(absoluteFile.getPath());
120 Repository repository = ResourceUtil.getRepository(path);
121 return getIndexDiffDataOrNull(repository);
125 * Returns the {@link IndexDiffData} for a given {@link Repository}.
127 * @param repository
128 * to get the index diff data from
129 * @return the IndexDiffData, or {@code null} if none.
131 @Nullable
132 private IndexDiffData getIndexDiffDataOrNull(
133 @Nullable Repository repository) {
134 if (repository == null) {
135 return null;
136 } else if (repository.isBare()) {
137 // For bare repository just return empty data
138 return new IndexDiffData();
140 IndexDiffCacheEntry diffCacheEntry = Activator.getDefault()
141 .getIndexDiffCache().getIndexDiffCacheEntry(repository);
142 if (diffCacheEntry == null) {
143 return null;
145 return diffCacheEntry.getIndexDiff();
149 * Determines the repository state of the given {@link IResource}.
151 * @param resource
152 * to get the state for
153 * @return the state, {@link #UNKNOWN_STATE} if none can be determined.
155 @NonNull
156 public IResourceState get(@Nullable IResource resource) {
157 IndexDiffData indexDiffData = getIndexDiffDataOrNull(resource);
158 if (indexDiffData == null || resource == null) {
159 return UNKNOWN_STATE;
161 return get(indexDiffData, resource);
165 * Determines the repository state of the given {@link File}.
167 * @param file
168 * to get the state for
169 * @return the state, {@link #UNKNOWN_STATE} if none can be determined.
171 @NonNull
172 public IResourceState get(@Nullable File file) {
173 IndexDiffData indexDiffData = getIndexDiffDataOrNull(file);
174 if (indexDiffData == null || file == null) {
175 return UNKNOWN_STATE;
177 return get(indexDiffData, file);
181 * Computes an {@link IResourceState} for the given {@link IResource} from
182 * the given {@link IndexDiffData}.
184 * @param indexDiffData
185 * to compute the state from
186 * @param resource
187 * to get the state of
188 * @return the state
190 @NonNull
191 public IResourceState get(@NonNull IndexDiffData indexDiffData,
192 @NonNull IResource resource) {
193 IPath path = resource.getLocation();
194 if (path != null) {
195 return get(indexDiffData, new ResourceItem(resource));
197 return UNKNOWN_STATE;
201 * Computes an {@link IResourceState} for the given {@link File} from the
202 * given {@link IndexDiffData}.
204 * @param indexDiffData
205 * to compute the state from
206 * @param file
207 * to get the state of
208 * @return the state
210 @NonNull
211 public IResourceState get(@NonNull IndexDiffData indexDiffData,
212 @NonNull File file) {
213 return get(indexDiffData, new FileItem(file));
217 * Computes an {@link IResourceState} for the given {@link FileSystemItem}
218 * from the given {@link IndexDiffData}.
220 * @param indexDiffData
221 * to compute the state from
222 * @param file
223 * to get the state of
224 * @return the state
226 @NonNull
227 private IResourceState get(@NonNull IndexDiffData indexDiffData,
228 @NonNull FileSystemItem file) {
229 IPath path = file.getAbsolutePath();
230 if (path == null) {
231 return UNKNOWN_STATE;
233 Repository repository = file.getRepository();
234 if (repository == null || repository.isBare()) {
235 return UNKNOWN_STATE;
237 File workTree = repository.getWorkTree();
238 String repoRelativePath = path.makeRelativeTo(
239 new org.eclipse.core.runtime.Path(workTree.getAbsolutePath()))
240 .toString();
241 if (repoRelativePath.equals(path.toString())) {
242 // Could not be made relative.
243 return UNKNOWN_STATE;
245 if (file.isContainer()) {
246 if (!repoRelativePath.endsWith("/")) { //$NON-NLS-1$
247 repoRelativePath += '/';
249 if (ResourceUtil.isSymbolicLink(repository, repoRelativePath)) {
250 // The Eclipse resource model handles a symlink to a folder like
251 // the container it refers to but git status handles the symlink
252 // source like a special file.
253 return extractFileProperties(indexDiffData, repoRelativePath);
254 } else {
255 return extractContainerProperties(indexDiffData,
256 repoRelativePath, file);
258 } else {
259 return extractFileProperties(indexDiffData, repoRelativePath);
263 private @NonNull IResourceState extractFileProperties(
264 @NonNull IndexDiffData indexDiffData,
265 @NonNull String repoRelativePath) {
266 Set<String> ignoredFiles = indexDiffData.getIgnoredNotInIndex();
267 boolean ignored = ignoredFiles.contains(repoRelativePath)
268 || containsPrefixPath(ignoredFiles, repoRelativePath);
269 if (ignored) {
270 // Leave the rest at the default (false, NOT_STAGED)
271 return IGNORED;
273 ResourceState state = new ResourceState();
274 Set<String> untracked = indexDiffData.getUntracked();
275 state.setTracked(!untracked.contains(repoRelativePath));
277 Set<String> added = indexDiffData.getAdded();
278 Set<String> removed = indexDiffData.getRemoved();
279 Set<String> changed = indexDiffData.getChanged();
280 if (added.contains(repoRelativePath)) {
281 state.setStagingState(StagingState.ADDED);
282 } else if (removed.contains(repoRelativePath)) {
283 state.setStagingState(StagingState.REMOVED);
284 } else if (changed.contains(repoRelativePath)) {
285 state.setStagingState(StagingState.MODIFIED);
286 } else {
287 state.setStagingState(StagingState.NOT_STAGED);
290 // conflicting
291 Set<String> conflicting = indexDiffData.getConflicting();
292 state.setConflicts(conflicting.contains(repoRelativePath));
294 // locally modified
295 Set<String> modified = indexDiffData.getModified();
296 state.setDirty(modified.contains(repoRelativePath));
298 // locally deleted
299 Set<String> missing = indexDiffData.getMissing();
300 state.setMissing(missing.contains(repoRelativePath));
302 Set<String> assumeUnchanged = indexDiffData.getAssumeUnchanged();
303 state.setAssumeUnchanged(assumeUnchanged.contains(repoRelativePath));
304 return state;
307 private @NonNull IResourceState extractContainerProperties(
308 @NonNull IndexDiffData indexDiffData,
309 @NonNull String repoRelativePath,
310 @NonNull FileSystemItem directory) {
311 Set<String> ignoredFiles = indexDiffData.getIgnoredNotInIndex();
312 boolean ignored = containsPrefixPath(ignoredFiles, repoRelativePath)
313 || !directory.hasContainerAnyFiles();
314 if (ignored) {
315 return IGNORED;
317 ResourceState state = new ResourceState();
318 Set<String> untrackedFolders = indexDiffData.getUntrackedFolders();
319 state.setTracked(
320 !containsPrefixPath(untrackedFolders, repoRelativePath));
322 // containers are marked as staged whenever file was added, removed or
323 // changed
324 Set<String> changed = new HashSet<>(indexDiffData.getChanged());
325 changed.addAll(indexDiffData.getAdded());
326 changed.addAll(indexDiffData.getRemoved());
327 if (containsPrefix(changed, repoRelativePath)) {
328 state.setStagingState(StagingState.MODIFIED);
329 } else {
330 state.setStagingState(StagingState.NOT_STAGED);
332 // conflicting
333 Set<String> conflicting = indexDiffData.getConflicting();
334 state.setConflicts(containsPrefix(conflicting, repoRelativePath));
336 // locally modified / untracked
337 Set<String> modified = indexDiffData.getModified();
338 Set<String> untracked = indexDiffData.getUntracked();
339 Set<String> missing = indexDiffData.getMissing();
340 state.setDirty(containsPrefix(modified, repoRelativePath)
341 || containsPrefix(untracked, repoRelativePath)
342 || containsPrefix(missing, repoRelativePath));
343 return state;
346 private boolean containsPrefix(Set<String> collection, String prefix) {
347 // when prefix is empty we are handling repository root, therefore we
348 // should return true whenever collection isn't empty
349 if (prefix.length() == 1 && !collection.isEmpty())
350 return true;
352 for (String path : collection)
353 if (path.startsWith(prefix))
354 return true;
355 return false;
358 private boolean containsPrefixPath(Set<String> collection, String path) {
359 for (String entry : collection) {
360 String entryPath;
361 if (entry.endsWith("/")) //$NON-NLS-1$
362 entryPath = entry;
363 else
364 entryPath = entry + "/"; //$NON-NLS-1$
365 if (path.startsWith(entryPath))
366 return true;
368 return false;
371 private interface FileSystemItem {
372 boolean hasContainerAnyFiles();
374 boolean isContainer();
376 @Nullable
377 IPath getAbsolutePath();
379 @Nullable
380 Repository getRepository();
383 private static class FileItem implements FileSystemItem {
385 @NonNull
386 private final File file;
388 public FileItem(@NonNull File file) {
389 this.file = file;
392 @Override
393 @NonNull
394 public IPath getAbsolutePath() {
395 return new org.eclipse.core.runtime.Path(file.getAbsolutePath());
398 @Override
399 public Repository getRepository() {
400 return ResourceUtil.getRepository(getAbsolutePath());
403 @Override
404 public boolean isContainer() {
405 return file.isDirectory();
408 @Override
409 public boolean hasContainerAnyFiles() {
410 if (!isContainer()) {
411 throw new IllegalArgumentException("Container expected"); //$NON-NLS-1$
413 try {
414 final boolean[] result = new boolean[] { false };
415 final Path dotGit = Paths.get(Constants.DOT_GIT);
416 Files.walkFileTree(file.toPath(), new FileVisitor<Path>() {
417 @Override
418 public FileVisitResult preVisitDirectory(Path dir,
419 BasicFileAttributes attrs) throws IOException {
420 if (dotGit.equals(dir.getFileName())) {
421 return FileVisitResult.SKIP_SUBTREE;
423 return FileVisitResult.CONTINUE;
426 @Override
427 public FileVisitResult visitFile(Path path,
428 BasicFileAttributes attrs) throws IOException {
429 if (!attrs.isDirectory()) {
430 result[0] = true;
431 return FileVisitResult.TERMINATE;
433 return FileVisitResult.CONTINUE;
436 @Override
437 public FileVisitResult visitFileFailed(Path path,
438 IOException exc) throws IOException {
439 return FileVisitResult.CONTINUE;
442 @Override
443 public FileVisitResult postVisitDirectory(Path dir,
444 IOException exc) throws IOException {
445 return FileVisitResult.CONTINUE;
448 return result[0];
449 } catch (IOException e) {
450 // if can't get any info, treat as with file
451 return true;
456 private static class ResourceItem implements FileSystemItem {
458 @NonNull
459 private final IResource resource;
461 public ResourceItem(@NonNull IResource resource) {
462 this.resource = resource;
465 @Override
466 @Nullable
467 public IPath getAbsolutePath() {
468 return resource.getLocation();
471 @Override
472 public Repository getRepository() {
473 return ResourceUtil.getRepository(resource);
476 @Override
477 public boolean isContainer() {
478 return isContainer(resource);
481 @Override
482 public boolean hasContainerAnyFiles() {
483 return containsFiles(resource);
486 private boolean isContainer(IResource rsc) {
487 int type = rsc.getType();
488 return type == IResource.FOLDER || type == IResource.PROJECT
489 || type == IResource.ROOT;
492 private boolean containsFiles(IResource rsc) {
493 if (rsc instanceof IContainer) {
494 IContainer container = (IContainer) rsc;
495 try {
496 return anyFile(container.members());
497 } catch (CoreException e) {
498 // if can't get any info, treat as with file
499 return true;
502 throw new IllegalArgumentException(
503 "Expected a container resource."); //$NON-NLS-1$
506 private boolean anyFile(IResource[] members) {
507 for (IResource member : members) {
508 if (member.getType() == IResource.FILE) {
509 return true;
510 } else if (isContainer(member) && containsFiles(member)) {
511 return true;
514 return false;