/* license-start * * Copyright (C) 2008 - 2013 Crispico, <http://www.crispico.com/>. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details, at <http://www.gnu.org/licenses/>. * * Contributors: * Crispico - Initial API and implementation * * license-end */ package org.flowerplatform.web.git.history.internal; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; 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.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; /** * @author Cristina Constantinescu */ public class FileDiff { private final RevCommit commit; private DiffEntry diffEntry; private static ObjectId[] trees(final RevCommit commit) { final ObjectId[] r = new ObjectId[commit.getParentCount() + 1]; for (int i = 0; i < r.length - 1; i++) r[i] = commit.getParent(i).getTree().getId(); r[r.length - 1] = commit.getTree().getId(); return r; } /** * Computer file diffs for specified tree walk and commit * * @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 TreeWalk walk, final RevCommit commit, final TreeFilter... markTreeFilters) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { final ArrayList<FileDiff> r = new ArrayList<FileDiff>(); if (commit.getParentCount() > 0) walk.reset(trees(commit)); else { walk.reset(); walk.addTree(new EmptyTreeIterator()); walk.addTree(commit.getTree()); } if (walk.getTreeCount() <= 2) { List<DiffEntry> entries = DiffEntry.scan(walk, false, markTreeFilters); for (DiffEntry entry : entries) { final FileDiff d = new FileDiff(commit, entry); 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; } ObjectReader reader = db.newObjectReader(); try { outputEclipseDiff(d, db, reader, diffFmt); } finally { reader.release(); } } 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 projectRelativePath = getProjectRelativePath(db, getPath()); d.append("diff --git ").append(projectRelativePath).append(" ") //$NON-NLS-1$ //$NON-NLS-2$ .append(projectRelativePath).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, getPath())); 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, getPath())); 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) { IWorkspace workspace = ResourcesPlugin.getWorkspace(); IWorkspaceRoot root = workspace.getRoot(); IPath absolutePath = new Path(db.getWorkTree().getAbsolutePath()).append(repoPath); IResource resource = root.getFileForLocation(absolutePath); 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; } /** * Get path * * @return path */ public String getPath() { if (ChangeType.DELETE.equals(diffEntry.getChangeType())) return diffEntry.getOldPath(); 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(TreeWalk, 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; } 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 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; } } }