/******************************************************************************* * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> * Copyright (c) 2010, Stefan Lay <stefan.lay@sap.com> * Copyright (C) 2012, Robin Stocker <robin@nibor.org> * * 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.ui.internal.history; import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import org.eclipse.core.resources.IResource; import org.eclipse.egit.core.internal.util.ResourceUtil; import org.eclipse.egit.ui.UIUtils; import org.eclipse.egit.ui.internal.DecorationOverlayDescriptor; import org.eclipse.egit.ui.internal.UIIcons; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.viewers.IDecoration; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.EditList; import org.eclipse.jgit.diff.MyersDiff; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.diff.RenameDetector; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.treewalk.EmptyTreeIterator; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.treewalk.filter.TreeFilterMarker; import org.eclipse.ui.model.WorkbenchAdapter; /** * A class with information about the changes to a file introduced in a * commit. */ public class FileDiff extends WorkbenchAdapter { /** * Comparator for sorting FileDiffs based on getPath(). Compares first the * directory part, if those are equal, the filename part. */ public static final Comparator<FileDiff> PATH_COMPARATOR = new Comparator<FileDiff>() { @Override public int compare(FileDiff left, FileDiff right) { String leftPath = left.getPath(); String rightPath = right.getPath(); int i = leftPath.lastIndexOf('/'); int j = rightPath.lastIndexOf('/'); int p = leftPath.substring(0, i + 1) .compareTo(rightPath.substring(0, j + 1)); if (p != 0) { return p; } return leftPath.compareTo(rightPath); } }; private final RevCommit commit; private DiffEntry diffEntry; static ObjectId[] trees(final RevCommit commit, final RevCommit[] parents) { final ObjectId[] r = new ObjectId[parents.length + 1]; for (int i = 0; i < r.length - 1; i++) r[i] = parents[i].getTree().getId(); r[r.length - 1] = commit.getTree().getId(); return r; } /** * Computer file diffs for specified tree walk and commit * * @param repository * @param walk * @param commit * @param markTreeFilters * optional filters for marking entries, see * {@link #isMarked(int)} * @return non-null but possibly empty array of file diffs * @throws MissingObjectException * @throws IncorrectObjectTypeException * @throws CorruptObjectException * @throws IOException */ public static FileDiff[] compute(final Repository repository, final TreeWalk walk, final RevCommit commit, final TreeFilter... markTreeFilters) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { return compute(repository, walk, commit, commit.getParents(), markTreeFilters); } /** * Computer file diffs for specified tree walk and commit * * @param repository * @param walk * @param commit * @param parents * @param markTreeFilters * optional filters for marking entries, see * {@link #isMarked(int)} * @return non-null but possibly empty array of file diffs * @throws MissingObjectException * @throws IncorrectObjectTypeException * @throws CorruptObjectException * @throws IOException */ public static FileDiff[] compute(final Repository repository, final TreeWalk walk, final RevCommit commit, final RevCommit[] parents, final TreeFilter... markTreeFilters) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { final ArrayList<FileDiff> r = new ArrayList<>(); if (parents.length > 0) { walk.reset(trees(commit, parents)); } else { walk.reset(); walk.addTree(new EmptyTreeIterator()); walk.addTree(commit.getTree()); } if (walk.getTreeCount() <= 2) { List<DiffEntry> entries = DiffEntry.scan(walk, false, markTreeFilters); List<DiffEntry> xentries = new LinkedList<>(entries); RenameDetector detector = new RenameDetector(repository); detector.addAll(entries); List<DiffEntry> renames = detector.compute(walk.getObjectReader(), org.eclipse.jgit.lib.NullProgressMonitor.INSTANCE); for (DiffEntry m : renames) { final FileDiff d = new FileDiff(commit, m); r.add(d); for (Iterator<DiffEntry> i = xentries.iterator(); i.hasNext();) { DiffEntry n = i.next(); if (m.getOldPath().equals(n.getOldPath())) i.remove(); else if (m.getNewPath().equals(n.getNewPath())) i.remove(); } } for (DiffEntry m : xentries) { final FileDiff d = new FileDiff(commit, m); r.add(d); } } else { // DiffEntry does not support walks with more than two trees final int nTree = walk.getTreeCount(); final int myTree = nTree - 1; TreeFilterMarker treeFilterMarker = new TreeFilterMarker( markTreeFilters); while (walk.next()) { if (matchAnyParent(walk, myTree)) continue; int treeFilterMarks = treeFilterMarker.getMarks(walk); final FileDiffForMerges d = new FileDiffForMerges(commit, treeFilterMarks); d.path = walk.getPathString(); int m0 = 0; for (int i = 0; i < myTree; i++) m0 |= walk.getRawMode(i); final int m1 = walk.getRawMode(myTree); d.change = ChangeType.MODIFY; if (m0 == 0 && m1 != 0) d.change = ChangeType.ADD; else if (m0 != 0 && m1 == 0) d.change = ChangeType.DELETE; else if (m0 != m1 && walk.idEqual(0, myTree)) d.change = ChangeType.MODIFY; // there is no ChangeType.TypeChanged d.blobs = new ObjectId[nTree]; d.modes = new FileMode[nTree]; for (int i = 0; i < nTree; i++) { d.blobs[i] = walk.getObjectId(i); d.modes[i] = walk.getFileMode(i); } r.add(d); } } final FileDiff[] tmp = new FileDiff[r.size()]; r.toArray(tmp); return tmp; } private static boolean matchAnyParent(final TreeWalk walk, final int myTree) { final int m = walk.getRawMode(myTree); for (int i = 0; i < myTree; i++) if (walk.getRawMode(i) == m && walk.idEqual(i, myTree)) return true; return false; } /** * Creates a textual diff together with meta information. * TODO So far this works only in case of one parent commit. * * @param d * the StringBuilder where the textual diff is added to * @param db * the Repo * @param diffFmt * the DiffFormatter used to create the textual diff * @param gitFormat * if false, do not show any source or destination prefix, * and the paths are calculated relative to the eclipse * project, otherwise relative to the git repository * @throws IOException */ public void outputDiff(final StringBuilder d, final Repository db, final DiffFormatter diffFmt, boolean gitFormat) throws IOException { if (gitFormat) { diffFmt.setRepository(db); diffFmt.format(diffEntry); return; } try (ObjectReader reader = db.newObjectReader()) { outputEclipseDiff(d, db, reader, diffFmt); } } private void outputEclipseDiff(final StringBuilder d, final Repository db, final ObjectReader reader, final DiffFormatter diffFmt) throws IOException { if (!(getBlobs().length == 2)) throw new UnsupportedOperationException( "Not supported yet if the number of parents is different from one"); //$NON-NLS-1$ String projectRelativeNewPath = getProjectRelativePath(db, getNewPath()); String projectRelativeOldPath = getProjectRelativePath(db, getOldPath()); d.append("diff --git ").append(projectRelativeOldPath).append(" ") //$NON-NLS-1$ //$NON-NLS-2$ .append(projectRelativeNewPath).append("\n"); //$NON-NLS-1$ final ObjectId id1 = getBlobs()[0]; final ObjectId id2 = getBlobs()[1]; final FileMode mode1 = getModes()[0]; final FileMode mode2 = getModes()[1]; if (id1.equals(ObjectId.zeroId())) { d.append("new file mode " + mode2).append("\n"); //$NON-NLS-1$//$NON-NLS-2$ } else if (id2.equals(ObjectId.zeroId())) { d.append("deleted file mode " + mode1).append("\n"); //$NON-NLS-1$//$NON-NLS-2$ } else if (!mode1.equals(mode2)) { d.append("old mode " + mode1); //$NON-NLS-1$ d.append("new mode " + mode2).append("\n"); //$NON-NLS-1$//$NON-NLS-2$ } d.append("index ").append(reader.abbreviate(id1).name()). //$NON-NLS-1$ append("..").append(reader.abbreviate(id2).name()). //$NON-NLS-1$ append(mode1.equals(mode2) ? " " + mode1 : "").append("\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ if (id1.equals(ObjectId.zeroId())) d.append("--- /dev/null\n"); //$NON-NLS-1$ else { d.append("--- "); //$NON-NLS-1$ d.append(getProjectRelativePath(db, getOldPath())); d.append("\n"); //$NON-NLS-1$ } if (id2.equals(ObjectId.zeroId())) d.append("+++ /dev/null\n"); //$NON-NLS-1$ else { d.append("+++ "); //$NON-NLS-1$ d.append(getProjectRelativePath(db, getNewPath())); d.append("\n"); //$NON-NLS-1$ } final RawText a = getRawText(id1, reader); final RawText b = getRawText(id2, reader); EditList editList = MyersDiff.INSTANCE .diff(RawTextComparator.DEFAULT, a, b); diffFmt.format(editList, a, b); } private String getProjectRelativePath(Repository db, String repoPath) { IResource resource = ResourceUtil.getFileForLocation(db, repoPath, false); if (resource == null) return null; return resource.getProjectRelativePath().toString(); } private RawText getRawText(ObjectId id, ObjectReader reader) throws IOException { if (id.equals(ObjectId.zeroId())) return new RawText(new byte[] {}); ObjectLoader ldr = reader.open(id, Constants.OBJ_BLOB); return new RawText(ldr.getCachedBytes(Integer.MAX_VALUE)); } /** * Get commit * * @return commit */ public RevCommit getCommit() { return commit; } /** * @return the old path in case of a delete, the new path otherwise, but * never null or <code>/dev/null</code> * @see #getNewPath() * @see #getOldPath() */ public String getPath() { if (ChangeType.DELETE.equals(diffEntry.getChangeType())) return diffEntry.getOldPath(); return diffEntry.getNewPath(); } /** * @return the old path or <code>/dev/null</code> for a completely new file * @see #getPath() for getting the new or old path depending on change type */ public String getOldPath() { return diffEntry.getOldPath(); } /** * @return the new path or <code>/dev/null</code> for a deleted file * @see #getPath() for getting the new or old path depending on change type */ public String getNewPath() { return diffEntry.getNewPath(); } /** * Get change type * * @return type */ public ChangeType getChange() { return diffEntry.getChangeType(); } /** * Get blob object ids * * @return non-null but possibly empty array of object ids */ public ObjectId[] getBlobs() { List<ObjectId> objectIds = new ArrayList<>(); if (diffEntry.getOldId() != null) objectIds.add(diffEntry.getOldId().toObjectId()); if (diffEntry.getNewId() != null) objectIds.add(diffEntry.getNewId().toObjectId()); return objectIds.toArray(new ObjectId[]{}); } /** * Get file modes * * @return non-null but possibly empty array of file modes */ public FileMode[] getModes() { List<FileMode> modes = new ArrayList<>(); if (diffEntry.getOldMode() != null) modes.add(diffEntry.getOldMode()); if (diffEntry.getOldMode() != null) modes.add(diffEntry.getOldMode()); return modes.toArray(new FileMode[]{}); } /** * Whether the mark tree filter with the specified index matched during scan * or not, see * {@link #compute(Repository, TreeWalk, RevCommit, RevCommit[], TreeFilter...)} * . * * @param index * the tree filter index to check * @return true if it was marked, false otherwise */ public boolean isMarked(int index) { return diffEntry != null && diffEntry.isMarked(index); } /** * Create a file diff for a specified {@link RevCommit} and * {@link DiffEntry} * * @param c * @param entry */ public FileDiff(final RevCommit c, final DiffEntry entry) { diffEntry = entry; commit = c; } /** * Is this diff a submodule? * * @return true if submodule, false otherwise */ public boolean isSubmodule() { if (diffEntry == null) return false; return diffEntry.getOldMode() == FileMode.GITLINK || diffEntry.getNewMode() == FileMode.GITLINK; } @Override public ImageDescriptor getImageDescriptor(Object object) { final ImageDescriptor base; if (!isSubmodule()) base = UIUtils.getEditorImage(getPath()); else base = UIIcons.REPOSITORY; switch (getChange()) { case ADD: return new DecorationOverlayDescriptor(base, UIIcons.OVR_STAGED_ADD, IDecoration.BOTTOM_RIGHT); case DELETE: return new DecorationOverlayDescriptor(base, UIIcons.OVR_STAGED_REMOVE, IDecoration.BOTTOM_RIGHT); case RENAME: return new DecorationOverlayDescriptor(base, UIIcons.OVR_STAGED_RENAME, IDecoration.BOTTOM_RIGHT); default: return base; } } @Override public String getLabel(Object object) { return getPath(); } private static class FileDiffForMerges extends FileDiff { private String path; private ChangeType change; private ObjectId[] blobs; private FileMode[] modes; private final int treeFilterMarks; private FileDiffForMerges(final RevCommit c, int treeFilterMarks) { super (c, null); this.treeFilterMarks = treeFilterMarks; } @Override public String getPath() { return path; } @Override public String getNewPath() { return path; } @Override public ChangeType getChange() { return change; } @Override public ObjectId[] getBlobs() { return blobs; } @Override public FileMode[] getModes() { return modes; } @Override public boolean isMarked(int index) { return (treeFilterMarks & (1L << index)) != 0; } } }