| /******************************************************************************* |
| * 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(). |
| */ |
| public static final Comparator<FileDiff> PATH_COMPARATOR = new Comparator<FileDiff>() { |
| @Override |
| public int compare(FileDiff o1, FileDiff o2) { |
| return o1.getPath().compareTo(o2.getPath()); |
| } |
| }; |
| |
| 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<FileDiff>(); |
| |
| 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<DiffEntry>(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<ObjectId>(); |
| 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<FileMode>(); |
| 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; |
| } |
| } |
| } |