Allow project decorations regardless of repository root location
[egit/torarne.git] / org.spearce.egit.ui / src / org / spearce / egit / ui / internal / decorators / DecoratableResourceAdapter.java
blob5c68d5b2e34bc83f584f2df2e9c48d1ee500e624
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>
9 * All rights reserved. This program and the accompanying materials
10 * are made available under the terms of the Eclipse Public License v1.0
11 * See LICENSE for the full license text, also available.
12 *******************************************************************************/
14 package org.spearce.egit.ui.internal.decorators;
16 import java.io.File;
17 import java.io.IOException;
18 import java.util.Collections;
19 import java.util.Set;
21 import org.eclipse.core.resources.IProject;
22 import org.eclipse.core.resources.IResource;
23 import org.eclipse.core.resources.IWorkspaceRoot;
24 import org.eclipse.jface.preference.IPreferenceStore;
25 import org.eclipse.team.core.Team;
26 import org.spearce.egit.core.AdaptableFileTreeIterator;
27 import org.spearce.egit.core.ContainerTreeIterator;
28 import org.spearce.egit.core.ContainerTreeIterator.ResourceEntry;
29 import org.spearce.egit.core.project.RepositoryMapping;
30 import org.spearce.egit.ui.Activator;
31 import org.spearce.egit.ui.UIPreferences;
32 import org.spearce.jgit.dircache.DirCache;
33 import org.spearce.jgit.dircache.DirCacheEntry;
34 import org.spearce.jgit.dircache.DirCacheIterator;
35 import org.spearce.jgit.errors.IncorrectObjectTypeException;
36 import org.spearce.jgit.errors.MissingObjectException;
37 import org.spearce.jgit.lib.Constants;
38 import org.spearce.jgit.lib.FileMode;
39 import org.spearce.jgit.lib.ObjectId;
40 import org.spearce.jgit.lib.Repository;
41 import org.spearce.jgit.revwalk.RevWalk;
42 import org.spearce.jgit.treewalk.EmptyTreeIterator;
43 import org.spearce.jgit.treewalk.TreeWalk;
44 import org.spearce.jgit.treewalk.WorkingTreeIterator;
45 import org.spearce.jgit.treewalk.filter.AndTreeFilter;
46 import org.spearce.jgit.treewalk.filter.PathFilterGroup;
47 import org.spearce.jgit.treewalk.filter.TreeFilter;
49 class DecoratableResourceAdapter implements IDecoratableResource {
51 private final IResource resource;
53 private final RepositoryMapping mapping;
55 private final Repository repository;
57 private final ObjectId headId;
59 private final IPreferenceStore store;
61 private String branch = "";
63 private boolean tracked = false;
65 private boolean ignored = false;
67 private boolean dirty = false;
69 private boolean conflicts = false;
71 private boolean assumeValid = false;
73 private Staged staged = Staged.NOT_STAGED;
75 static final int T_HEAD = 0;
77 static final int T_INDEX = 1;
79 static final int T_WORKSPACE = 2;
81 @SuppressWarnings("fallthrough")
82 public DecoratableResourceAdapter(IResource resourceToWrap)
83 throws IOException {
84 resource = resourceToWrap;
85 mapping = RepositoryMapping.getMapping(resource);
86 repository = mapping.getRepository();
87 headId = repository.resolve(Constants.HEAD);
89 store = Activator.getDefault().getPreferenceStore();
91 // TODO: Add option to shorten branch name to 6 chars if it's a SHA
92 branch = repository.getBranch();
94 TreeWalk treeWalk = createThreeWayTreeWalk();
95 if (treeWalk == null)
96 return;
98 switch (resource.getType()) {
99 case IResource.FILE:
100 if (!treeWalk.next())
101 return;
102 extractResourceProperties(treeWalk);
103 break;
104 case IResource.PROJECT:
105 tracked = true;
106 case IResource.FOLDER:
107 extractContainerProperties(treeWalk);
108 break;
112 private void extractResourceProperties(TreeWalk treeWalk) {
113 final ContainerTreeIterator workspaceIterator = treeWalk.getTree(
114 T_WORKSPACE, ContainerTreeIterator.class);
115 final ResourceEntry resourceEntry = workspaceIterator != null ? workspaceIterator
116 .getResourceEntry() : null;
118 if (resourceEntry == null)
119 return;
121 if (isIgnored(resourceEntry.getResource())) {
122 ignored = true;
123 return;
126 final int mHead = treeWalk.getRawMode(T_HEAD);
127 final int mIndex = treeWalk.getRawMode(T_INDEX);
129 if (mHead == FileMode.MISSING.getBits()
130 && mIndex == FileMode.MISSING.getBits())
131 return;
133 tracked = true;
135 if (mHead == FileMode.MISSING.getBits()) {
136 staged = Staged.ADDED;
137 } else if (mIndex == FileMode.MISSING.getBits()) {
138 staged = Staged.REMOVED;
139 } else if (mHead != mIndex
140 || (mIndex != FileMode.TREE.getBits() && !treeWalk.idEqual(
141 T_HEAD, T_INDEX))) {
142 staged = Staged.MODIFIED;
143 } else {
144 staged = Staged.NOT_STAGED;
147 final DirCacheIterator indexIterator = treeWalk.getTree(T_INDEX,
148 DirCacheIterator.class);
149 final DirCacheEntry indexEntry = indexIterator != null ? indexIterator
150 .getDirCacheEntry() : null;
152 if (indexEntry == null)
153 return;
155 if (indexEntry.getStage() > 0)
156 conflicts = true;
158 if (indexEntry.isAssumeValid()) {
159 dirty = false;
160 assumeValid = true;
161 } else {
162 if (!timestampMatches(indexEntry, resourceEntry))
163 dirty = true;
165 // TODO: Consider doing a content check here, to rule out false
166 // positives, as we might get mismatch between timestamps, even
167 // if the content is the same.
171 private class RecursiveStateFilter extends TreeFilter {
173 private int filesChecked = 0;
175 private int targetDepth = -1;
177 private final int recurseLimit;
179 public RecursiveStateFilter() {
180 recurseLimit = store
181 .getInt(UIPreferences.DECORATOR_RECURSIVE_LIMIT);
184 @Override
185 public boolean include(TreeWalk treeWalk)
186 throws MissingObjectException, IncorrectObjectTypeException,
187 IOException {
189 if (treeWalk.getFileMode(T_HEAD) == FileMode.MISSING
190 && treeWalk.getFileMode(T_INDEX) == FileMode.MISSING)
191 return false;
193 if (FileMode.TREE.equals(treeWalk.getRawMode(T_WORKSPACE)))
194 return shouldRecurse(treeWalk);
196 // Backup current state so far
197 Staged wasStaged = staged;
198 boolean wasDirty = dirty;
199 boolean hadConflicts = conflicts;
201 extractResourceProperties(treeWalk);
202 filesChecked++;
204 // Merge results with old state
205 ignored = false;
206 assumeValid = false;
207 dirty = wasDirty || dirty;
208 conflicts = hadConflicts || conflicts;
209 if (staged != wasStaged && filesChecked > 1)
210 staged = Staged.MODIFIED;
212 return false;
215 private boolean shouldRecurse(TreeWalk treeWalk) {
216 final WorkingTreeIterator workspaceIterator = treeWalk.getTree(
217 T_WORKSPACE, WorkingTreeIterator.class);
219 if (workspaceIterator instanceof AdaptableFileTreeIterator)
220 return true;
222 ResourceEntry resourceEntry = null;
223 if (workspaceIterator != null)
224 resourceEntry = ((ContainerTreeIterator) workspaceIterator)
225 .getResourceEntry();
227 if (resourceEntry == null)
228 return true;
230 IResource visitingResource = resourceEntry.getResource();
231 if (targetDepth == -1) {
232 if (visitingResource.equals(resource)
233 || visitingResource.getParent().equals(resource))
234 targetDepth = treeWalk.getDepth();
235 else
236 return true;
239 if ((treeWalk.getDepth() - targetDepth) >= recurseLimit) {
240 if (visitingResource.equals(resource))
241 extractResourceProperties(treeWalk);
243 return false;
246 return true;
249 @Override
250 public TreeFilter clone() {
251 RecursiveStateFilter clone = new RecursiveStateFilter();
252 clone.filesChecked = this.filesChecked;
253 return clone;
256 @Override
257 public boolean shouldBeRecursive() {
258 return true;
262 private void extractContainerProperties(TreeWalk treeWalk) throws IOException {
264 if (isIgnored(resource)) {
265 ignored = true;
266 return;
269 treeWalk.setFilter(AndTreeFilter.create(treeWalk.getFilter(),
270 new RecursiveStateFilter()));
271 treeWalk.setRecursive(true);
273 treeWalk.next();
277 * Adds a filter to the specified tree walk limiting the results to only
278 * those matching the resource specified by <code>resourceToFilterBy</code>
279 * <p>
280 * If the resource does not exists in the current repository, no filter is
281 * added and the method returns <code>false</code>. If the resource is a
282 * project, no filter is added, but the operation is considered a success.
284 * @param treeWalk
285 * the tree walk to add the filter to
286 * @param resourceToFilterBy
287 * the resource to filter by
289 * @return <code>true</code> if the filter could be added,
290 * <code>false</code> otherwise
292 private boolean addResourceFilter(final TreeWalk treeWalk,
293 final IResource resourceToFilterBy) {
294 Set<String> repositoryPaths = Collections.singleton(mapping
295 .getRepoRelativePath(resourceToFilterBy));
296 if (repositoryPaths.isEmpty())
297 return false;
299 if (repositoryPaths.contains(""))
300 return true; // Project filter
302 treeWalk.setFilter(PathFilterGroup.createFromStrings(repositoryPaths));
303 return true;
307 * Helper method to create a new tree walk between the repository, the
308 * index, and the working tree.
310 * @return the created tree walk, or null if it could not be created
311 * @throws IOException
312 * if there were errors when creating the tree walk
314 private TreeWalk createThreeWayTreeWalk() throws IOException {
315 final TreeWalk treeWalk = new TreeWalk(repository);
316 if (!addResourceFilter(treeWalk, resource))
317 return null;
319 treeWalk.setRecursive(treeWalk.getFilter().shouldBeRecursive());
320 treeWalk.reset();
322 // Repository
323 if (headId != null)
324 treeWalk.addTree(new RevWalk(repository).parseTree(headId));
325 else
326 treeWalk.addTree(new EmptyTreeIterator());
328 // Index
329 treeWalk.addTree(new DirCacheIterator(DirCache.read(repository)));
331 // Working directory
332 IProject project = resource.getProject();
333 IWorkspaceRoot workspaceRoot = resource.getWorkspace().getRoot();
334 File repoRoot = repository.getWorkDir();
336 if (repoRoot.equals(project.getLocation().toFile()))
337 treeWalk.addTree(new ContainerTreeIterator(project));
338 else if (repoRoot.equals(workspaceRoot.getLocation().toFile()))
339 treeWalk.addTree(new ContainerTreeIterator(workspaceRoot));
340 else
341 treeWalk.addTree(new AdaptableFileTreeIterator(repoRoot,
342 workspaceRoot));
344 return treeWalk;
347 private static boolean timestampMatches(DirCacheEntry indexEntry,
348 ResourceEntry resourceEntry) {
349 long tIndex = indexEntry.getLastModified();
350 long tWorkspaceResource = resourceEntry.getLastModified();
353 // C-Git under Windows stores timestamps with 1-seconds resolution,
354 // so we need to check to see if this is the case here, and possibly
355 // fix the timestamp of the resource to match the resolution of the
356 // index.
357 if (tIndex % 1000 == 0) {
358 return tIndex == (tWorkspaceResource - (tWorkspaceResource % 1000));
359 } else {
360 return tIndex == tWorkspaceResource;
364 private static boolean isIgnored(IResource resource) {
365 // TODO: Also read ignores from .git/info/excludes et al.
366 return Team.isIgnoredHint(resource);
369 public String getName() {
370 return resource.getName();
373 public int getType() {
374 return resource.getType();
377 public String getBranch() {
378 return branch;
381 public boolean isTracked() {
382 return tracked;
385 public boolean isIgnored() {
386 return ignored;
389 public boolean isDirty() {
390 return dirty;
393 public Staged staged() {
394 return staged;
397 public boolean hasConflicts() {
398 return conflicts;
401 public boolean isAssumeValid() {
402 return assumeValid;