/******************************************************************************* * Copyright (C) 2011, 2015 Dariusz Luksza <dariusz@luksza.org> and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package org.eclipse.egit.core.synchronize; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.egit.core.synchronize.dto.GitSynchronizeData; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.MutableObjectId; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.treewalk.TreeWalk; /** * Based on {@link org.eclipse.jgit.diff.DiffEntry}. Represents change to a file * with additional information about change direction. */ public final class ThreeWayDiffEntry { /** Magical SHA1 used for file adds or deletes */ private static final AbbreviatedObjectId A_ZERO = AbbreviatedObjectId .fromObjectId(ObjectId.zeroId()); /** General type of change a single file-level patch describes. */ public static enum ChangeType { /** Add a new file to the project */ ADD, /** Modify an existing file in the project (content and/or mode) */ MODIFY, /** Delete an existing file from the project */ DELETE, /** Resource is in sync */ IN_SYNC; } /** Change direction */ public static enum Direction { /** * */ INCOMING, /** * */ OUTGOING, /** */ CONFLICTING; } ThreeWayDiffEntry() { // reduce the visibility of the default constructor } /** * Convert the TreeWalk into {@link ThreeWayDiffEntry} instances. * * @param walk * the TreeWalk to walk through. Must have 3 or 4 trees in this * order: local, base, remote, optionally a DirCacheIterator, and * can't be recursive. * @return A list, never null but possibly empty, of * {@link ThreeWayDiffEntry} describing the changed file. * @throws IOException * the repository cannot be accessed. * @throws IllegalArgumentException * when {@code walk} doen't have 3 or 4 trees, or when * {@code walk} is recursive */ public static @NonNull List<ThreeWayDiffEntry> scan(TreeWalk walk) throws IOException { return scan(walk, null); } /** * Convert the TreeWalk into {@link ThreeWayDiffEntry} instances. * * @param walk * the TreeWalk to walk through. Must have 3 or 4 trees in this * order: local, base, remote, optionally a DirCacheIterator, and * can't be recursive. * @param gsd * The {@link GitSynchronizeData} that contains info about the * synchronization configuration and scope. * @return A list, never null but possibly empty, of * {@link ThreeWayDiffEntry} describing the changed file. * @throws IOException * the repository cannot be accessed. * @throws IllegalArgumentException * when {@code walk} doen't have 3 or 4 trees, or when * {@code walk} is recursive */ public static @NonNull List<ThreeWayDiffEntry> scan(TreeWalk walk, GitSynchronizeData gsd) throws IOException { if (walk.getTreeCount() != 3 && walk.getTreeCount() != 4) throw new IllegalArgumentException( "TreeWalk need to have three or four trees"); //$NON-NLS-1$ if (walk.isRecursive()) throw new IllegalArgumentException( "TreeWalk shouldn't be recursive."); //$NON-NLS-1$ List<ThreeWayDiffEntry> r = new ArrayList<ThreeWayDiffEntry>(); MutableObjectId idBuf = new MutableObjectId(); NeedEntry needEntry = new NeedEntry(gsd); while (walk.next()) { ThreeWayDiffEntry e = new ThreeWayDiffEntry(); walk.getObjectId(idBuf, 0); e.localId = AbbreviatedObjectId.fromObjectId(idBuf); walk.getObjectId(idBuf, 1); e.baseId = AbbreviatedObjectId.fromObjectId(idBuf); walk.getObjectId(idBuf, 2); e.remoteId = AbbreviatedObjectId.fromObjectId(idBuf); boolean localSameAsBase = e.localId.equals(e.baseId); if (!A_ZERO.equals(e.localId) && localSameAsBase && e.baseId.equals(e.remoteId)) { if (needEntry.apply(walk.getPathString())) { e.direction = Direction.INCOMING; e.changeType = ChangeType.IN_SYNC; e.path = walk.getPathString(); r.add(e); } continue; } e.path = walk.getPathString(); boolean localIsMissing = walk.getFileMode(0) == FileMode.MISSING; boolean baseIsMissing = walk.getFileMode(1) == FileMode.MISSING; boolean remoteIsMissing = walk.getFileMode(2) == FileMode.MISSING; if (localIsMissing || baseIsMissing || remoteIsMissing) { if (!localIsMissing && baseIsMissing && remoteIsMissing) { e.direction = Direction.OUTGOING; e.changeType = ChangeType.ADD; } else if (localIsMissing && baseIsMissing && !remoteIsMissing) { e.direction = Direction.INCOMING; e.changeType = ChangeType.ADD; } else if (!localIsMissing && !baseIsMissing && remoteIsMissing) { e.direction = Direction.INCOMING; e.changeType = ChangeType.DELETE; } else if (localIsMissing && !baseIsMissing && !remoteIsMissing) { e.direction = Direction.OUTGOING; e.changeType = ChangeType.DELETE; } else { e.direction = Direction.CONFLICTING; e.changeType = ChangeType.MODIFY; } } else { if (localSameAsBase && !e.localId.equals(e.remoteId)) e.direction = Direction.INCOMING; else if (e.remoteId.equals(e.baseId) && !e.remoteId.equals(e.localId)) e.direction = Direction.OUTGOING; else e.direction = Direction.CONFLICTING; e.changeType = ChangeType.MODIFY; } r.add(e); if (walk.isSubtree()) { e.isTree = true; walk.enterSubtree(); } } return r; } ChangeType changeType; AbbreviatedObjectId baseId; AbbreviatedObjectId remoteId; private String path; private Direction direction; private AbbreviatedObjectId localId; private boolean isTree = false; /** * @return base id */ public AbbreviatedObjectId getBaseId() { return baseId; } /** * @return path */ public String getPath() { return path; } /** * @return {@code true} if entry represents tree, {@code false} otherwise */ public boolean isTree() { return isTree; } /** @return the type of change this patch makes on {@link #getPath()} */ public ChangeType getChangeType() { return changeType; } /** * Get the old object id from the <code>index</code>. * * @return the object id; null if there is no index line */ public AbbreviatedObjectId getLocalId() { return localId; } /** * Get the new object id from the <code>index</code>. * * @return the object id; null if there is no index line */ public AbbreviatedObjectId getRemoteId() { return remoteId; } /** * @return direction */ public Direction getDirection() { return direction; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("ThreeDiffEntry["); //$NON-NLS-1$ buf.append(changeType).append(" ").append(path); //$NON-NLS-1$ buf.append("]"); //$NON-NLS-1$ return buf.toString(); } private static class NeedEntry { private final GitSynchronizeData gsd; private Set<String> paths; public NeedEntry(GitSynchronizeData gsd) { this.gsd = gsd; } boolean apply(String pathString) { if (gsd == null) { // This means that all paths must be included return true; } if (paths == null) { initPaths(); } return paths.contains(pathString); } private void initPaths() { Set<IResource> resources = gsd.getIncludedResources(); if (resources != null && !resources.isEmpty()) { paths = new HashSet<String>(resources.size()); final Path repositoryPath = new Path(gsd.getRepository() .getWorkTree().getAbsolutePath()); for (IResource resource : gsd.getIncludedResources()) { IPath resourceLocation = resource.getLocation(); if (resourceLocation != null) { paths.add(resourceLocation.makeRelativeTo( repositoryPath).toString()); } } } else { paths = Collections.emptySet(); } } } }