Initial EGit contribution to eclipse.org
[egit.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / decorators / DecoratableResourceAdapter.java
blob46b2f729f51e10e807fb17e82d65b57e117cc240
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 * which accompanies this distribution, and is available at
12 * http://www.eclipse.org/legal/epl-v10.html
13 *******************************************************************************/
15 package org.eclipse.egit.ui.internal.decorators;
17 import java.io.File;
18 import java.io.IOException;
19 import java.util.Collections;
20 import java.util.Set;
22 import org.eclipse.core.resources.IProject;
23 import org.eclipse.core.resources.IResource;
24 import org.eclipse.core.resources.IWorkspaceRoot;
25 import org.eclipse.egit.core.AdaptableFileTreeIterator;
26 import org.eclipse.egit.core.ContainerTreeIterator;
27 import org.eclipse.egit.core.ContainerTreeIterator.ResourceEntry;
28 import org.eclipse.egit.core.project.RepositoryMapping;
29 import org.eclipse.egit.ui.Activator;
30 import org.eclipse.egit.ui.UIPreferences;
31 import org.eclipse.jface.preference.IPreferenceStore;
32 import org.eclipse.team.core.Team;
33 import org.eclipse.jgit.dircache.DirCache;
34 import org.eclipse.jgit.dircache.DirCacheEntry;
35 import org.eclipse.jgit.dircache.DirCacheIterator;
36 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
37 import org.eclipse.jgit.errors.MissingObjectException;
38 import org.eclipse.jgit.lib.Constants;
39 import org.eclipse.jgit.lib.FileMode;
40 import org.eclipse.jgit.lib.ObjectId;
41 import org.eclipse.jgit.lib.Repository;
42 import org.eclipse.jgit.revwalk.RevWalk;
43 import org.eclipse.jgit.treewalk.EmptyTreeIterator;
44 import org.eclipse.jgit.treewalk.TreeWalk;
45 import org.eclipse.jgit.treewalk.WorkingTreeIterator;
46 import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
47 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
48 import org.eclipse.jgit.treewalk.filter.TreeFilter;
50 class DecoratableResourceAdapter implements IDecoratableResource {
52 private final IResource resource;
54 private final RepositoryMapping mapping;
56 private final Repository repository;
58 private final ObjectId headId;
60 private final IPreferenceStore store;
62 private String branch = "";
64 private boolean tracked = false;
66 private boolean ignored = false;
68 private boolean dirty = false;
70 private boolean conflicts = false;
72 private boolean assumeValid = false;
74 private Staged staged = Staged.NOT_STAGED;
76 static final int T_HEAD = 0;
78 static final int T_INDEX = 1;
80 static final int T_WORKSPACE = 2;
82 @SuppressWarnings("fallthrough")
83 public DecoratableResourceAdapter(IResource resourceToWrap)
84 throws IOException {
85 resource = resourceToWrap;
86 mapping = RepositoryMapping.getMapping(resource);
87 repository = mapping.getRepository();
88 headId = repository.resolve(Constants.HEAD);
90 store = Activator.getDefault().getPreferenceStore();
92 // TODO: Add option to shorten branch name to 6 chars if it's a SHA
93 branch = repository.getBranch();
95 TreeWalk treeWalk = createThreeWayTreeWalk();
96 if (treeWalk == null)
97 return;
99 switch (resource.getType()) {
100 case IResource.FILE:
101 if (!treeWalk.next())
102 return;
103 extractResourceProperties(treeWalk);
104 break;
105 case IResource.PROJECT:
106 tracked = true;
107 case IResource.FOLDER:
108 extractContainerProperties(treeWalk);
109 break;
113 private void extractResourceProperties(TreeWalk treeWalk) {
114 final ContainerTreeIterator workspaceIterator = treeWalk.getTree(
115 T_WORKSPACE, ContainerTreeIterator.class);
116 final ResourceEntry resourceEntry = workspaceIterator != null ? workspaceIterator
117 .getResourceEntry() : null;
119 if (resourceEntry == null)
120 return;
122 if (isIgnored(resourceEntry.getResource())) {
123 ignored = true;
124 return;
127 final int mHead = treeWalk.getRawMode(T_HEAD);
128 final int mIndex = treeWalk.getRawMode(T_INDEX);
130 if (mHead == FileMode.MISSING.getBits()
131 && mIndex == FileMode.MISSING.getBits())
132 return;
134 tracked = true;
136 if (mHead == FileMode.MISSING.getBits()) {
137 staged = Staged.ADDED;
138 } else if (mIndex == FileMode.MISSING.getBits()) {
139 staged = Staged.REMOVED;
140 } else if (mHead != mIndex
141 || (mIndex != FileMode.TREE.getBits() && !treeWalk.idEqual(
142 T_HEAD, T_INDEX))) {
143 staged = Staged.MODIFIED;
144 } else {
145 staged = Staged.NOT_STAGED;
148 final DirCacheIterator indexIterator = treeWalk.getTree(T_INDEX,
149 DirCacheIterator.class);
150 final DirCacheEntry indexEntry = indexIterator != null ? indexIterator
151 .getDirCacheEntry() : null;
153 if (indexEntry == null)
154 return;
156 if (indexEntry.getStage() > 0)
157 conflicts = true;
159 if (indexEntry.isAssumeValid()) {
160 dirty = false;
161 assumeValid = true;
162 } else {
163 if (!timestampMatches(indexEntry, resourceEntry))
164 dirty = true;
166 // TODO: Consider doing a content check here, to rule out false
167 // positives, as we might get mismatch between timestamps, even
168 // if the content is the same.
172 private class RecursiveStateFilter extends TreeFilter {
174 private int filesChecked = 0;
176 private int targetDepth = -1;
178 private final int recurseLimit;
180 public RecursiveStateFilter() {
181 recurseLimit = store
182 .getInt(UIPreferences.DECORATOR_RECURSIVE_LIMIT);
185 @Override
186 public boolean include(TreeWalk treeWalk)
187 throws MissingObjectException, IncorrectObjectTypeException,
188 IOException {
190 if (treeWalk.getFileMode(T_HEAD) == FileMode.MISSING
191 && treeWalk.getFileMode(T_INDEX) == FileMode.MISSING)
192 return false;
194 if (FileMode.TREE.equals(treeWalk.getRawMode(T_WORKSPACE)))
195 return shouldRecurse(treeWalk);
197 // Backup current state so far
198 Staged wasStaged = staged;
199 boolean wasDirty = dirty;
200 boolean hadConflicts = conflicts;
202 extractResourceProperties(treeWalk);
203 filesChecked++;
205 // Merge results with old state
206 ignored = false;
207 assumeValid = false;
208 dirty = wasDirty || dirty;
209 conflicts = hadConflicts || conflicts;
210 if (staged != wasStaged && filesChecked > 1)
211 staged = Staged.MODIFIED;
213 return false;
216 private boolean shouldRecurse(TreeWalk treeWalk) {
217 final WorkingTreeIterator workspaceIterator = treeWalk.getTree(
218 T_WORKSPACE, WorkingTreeIterator.class);
220 if (workspaceIterator instanceof AdaptableFileTreeIterator)
221 return true;
223 ResourceEntry resourceEntry = null;
224 if (workspaceIterator != null)
225 resourceEntry = ((ContainerTreeIterator) workspaceIterator)
226 .getResourceEntry();
228 if (resourceEntry == null)
229 return true;
231 IResource visitingResource = resourceEntry.getResource();
232 if (targetDepth == -1) {
233 if (visitingResource.equals(resource)
234 || visitingResource.getParent().equals(resource))
235 targetDepth = treeWalk.getDepth();
236 else
237 return true;
240 if ((treeWalk.getDepth() - targetDepth) >= recurseLimit) {
241 if (visitingResource.equals(resource))
242 extractResourceProperties(treeWalk);
244 return false;
247 return true;
250 @Override
251 public TreeFilter clone() {
252 RecursiveStateFilter clone = new RecursiveStateFilter();
253 clone.filesChecked = this.filesChecked;
254 return clone;
257 @Override
258 public boolean shouldBeRecursive() {
259 return true;
263 private void extractContainerProperties(TreeWalk treeWalk) throws IOException {
265 if (isIgnored(resource)) {
266 ignored = true;
267 return;
270 treeWalk.setFilter(AndTreeFilter.create(treeWalk.getFilter(),
271 new RecursiveStateFilter()));
272 treeWalk.setRecursive(true);
274 treeWalk.next();
278 * Adds a filter to the specified tree walk limiting the results to only
279 * those matching the resource specified by <code>resourceToFilterBy</code>
280 * <p>
281 * If the resource does not exists in the current repository, no filter is
282 * added and the method returns <code>false</code>. If the resource is a
283 * project, no filter is added, but the operation is considered a success.
285 * @param treeWalk
286 * the tree walk to add the filter to
287 * @param resourceToFilterBy
288 * the resource to filter by
290 * @return <code>true</code> if the filter could be added,
291 * <code>false</code> otherwise
293 private boolean addResourceFilter(final TreeWalk treeWalk,
294 final IResource resourceToFilterBy) {
295 Set<String> repositoryPaths = Collections.singleton(mapping
296 .getRepoRelativePath(resourceToFilterBy));
297 if (repositoryPaths.isEmpty())
298 return false;
300 if (repositoryPaths.contains(""))
301 return true; // Project filter
303 treeWalk.setFilter(PathFilterGroup.createFromStrings(repositoryPaths));
304 return true;
308 * Helper method to create a new tree walk between the repository, the
309 * index, and the working tree.
311 * @return the created tree walk, or null if it could not be created
312 * @throws IOException
313 * if there were errors when creating the tree walk
315 private TreeWalk createThreeWayTreeWalk() throws IOException {
316 final TreeWalk treeWalk = new TreeWalk(repository);
317 if (!addResourceFilter(treeWalk, resource))
318 return null;
320 treeWalk.setRecursive(treeWalk.getFilter().shouldBeRecursive());
321 treeWalk.reset();
323 // Repository
324 if (headId != null)
325 treeWalk.addTree(new RevWalk(repository).parseTree(headId));
326 else
327 treeWalk.addTree(new EmptyTreeIterator());
329 // Index
330 treeWalk.addTree(new DirCacheIterator(DirCache.read(repository)));
332 // Working directory
333 IProject project = resource.getProject();
334 IWorkspaceRoot workspaceRoot = resource.getWorkspace().getRoot();
335 File repoRoot = repository.getWorkDir();
337 if (repoRoot.equals(project.getLocation().toFile()))
338 treeWalk.addTree(new ContainerTreeIterator(project));
339 else if (repoRoot.equals(workspaceRoot.getLocation().toFile()))
340 treeWalk.addTree(new ContainerTreeIterator(workspaceRoot));
341 else
342 treeWalk.addTree(new AdaptableFileTreeIterator(repoRoot,
343 workspaceRoot));
345 return treeWalk;
348 private static boolean timestampMatches(DirCacheEntry indexEntry,
349 ResourceEntry resourceEntry) {
350 long tIndex = indexEntry.getLastModified();
351 long tWorkspaceResource = resourceEntry.getLastModified();
354 // C-Git under Windows stores timestamps with 1-seconds resolution,
355 // so we need to check to see if this is the case here, and possibly
356 // fix the timestamp of the resource to match the resolution of the
357 // index.
358 // It also appears the timestamp in Java on Linux may also be rounded
359 // in which case the index timestamp may have subseconds, but not
360 // the timestamp from the workspace resource.
361 // If either timestamp looks rounded we skip the subscond part.
362 if (tIndex % 1000 == 0 || tWorkspaceResource % 1000 == 0) {
363 return tIndex / 1000 == tWorkspaceResource / 1000;
364 } else {
365 return tIndex == tWorkspaceResource;
369 private static boolean isIgnored(IResource resource) {
370 // TODO: Also read ignores from .git/info/excludes et al.
371 return Team.isIgnoredHint(resource);
374 public String getName() {
375 return resource.getName();
378 public int getType() {
379 return resource.getType();
382 public String getBranch() {
383 return branch;
386 public boolean isTracked() {
387 return tracked;
390 public boolean isIgnored() {
391 return ignored;
394 public boolean isDirty() {
395 return dirty;
398 public Staged staged() {
399 return staged;
402 public boolean hasConflicts() {
403 return conflicts;
406 public boolean isAssumeValid() {
407 return assumeValid;