/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * 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 * * Contributors: * Codenvy, S.A. - initial API and implementation * SAP - implementation *******************************************************************************/ package org.eclipse.che.git.impl.jgit; import org.eclipse.che.api.git.DiffPage; import org.eclipse.che.api.git.params.DiffParams; import org.eclipse.che.api.git.shared.DiffType; import org.eclipse.jgit.diff.ContentSource; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.RenameDetector; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.EmptyTreeIterator; import org.eclipse.jgit.treewalk.FileTreeIterator; import org.eclipse.jgit.treewalk.filter.AndTreeFilter; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.jgit.treewalk.filter.TreeFilter; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.util.Collections; import java.util.List; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.System.lineSeparator; /** * Contains information about difference between two commits, commit and working tree, * working tree and index, commit and index. * * @author Andrey Parfonov */ class JGitDiffPage extends DiffPage { private final DiffParams params; private final Repository repository; JGitDiffPage(DiffParams params, Repository repository) { this.params = params; this.repository = repository; } @Override public final void writeTo(OutputStream out) throws IOException { DiffFormatter formatter = new DiffFormatter(new BufferedOutputStream(out)); formatter.setRepository(repository); List<String> rawFileFilter = params.getFileFilter(); TreeFilter pathFilter = (rawFileFilter != null && rawFileFilter.size() > 0) ? PathFilterGroup.createFromStrings(rawFileFilter) : TreeFilter.ALL; formatter.setPathFilter(AndTreeFilter.create(TreeFilter.ANY_DIFF, pathFilter)); try { String commitA = params.getCommitA(); String commitB = params.getCommitB(); boolean cached = params.isCached(); List<DiffEntry> diff; if (commitA == null && commitB == null && !cached) { diff = indexToWorkingTree(formatter); } else if (commitA != null && commitB == null && !cached) { diff = commitToWorkingTree(commitA, formatter); } else if (commitA == null && commitB != null) { diff = emptyToCommit(commitB, formatter); } else if (commitB == null) { diff = commitToIndex(commitA, formatter); } else { diff = commitToCommit(commitA, commitB, formatter); } DiffType type = params.getType(); if (type == DiffType.NAME_ONLY) { writeNames(diff, out); } else if (type == DiffType.NAME_STATUS) { writeNamesAndStatus(diff, out); } else { writeRawDiff(diff, formatter); } } finally { formatter.close(); repository.close(); } } /** * Show changes between specified revision and empty tree. * * @param commitId * id of commit * @param formatter * diff formatter * @return list of diff entries * @throws IOException * if any i/o errors occurs */ private List<DiffEntry> emptyToCommit(String commitId, DiffFormatter formatter) throws IOException { ObjectId commit = repository.resolve(commitId); checkArgument(commit != null, "Invalid commit id " + commitId); RevTree tree; try (RevWalk revWalkA = new RevWalk(repository)) { tree = revWalkA.parseTree(commit); } List<DiffEntry> diff; try (ObjectReader reader = repository.newObjectReader()) { CanonicalTreeParser iterator = new CanonicalTreeParser(); iterator.reset(reader, tree); diff = formatter.scan(new EmptyTreeIterator(), iterator); } return diff; } /** * Show changes between index and working tree. * * @param formatter * diff formatter * @return list of diff entries * @throws IOException * if any i/o errors occurs */ private List<DiffEntry> indexToWorkingTree(DiffFormatter formatter) throws IOException { DirCache dirCache = null; ObjectReader reader = repository.newObjectReader(); List<DiffEntry> diff; try { dirCache = repository.lockDirCache(); DirCacheIterator iterA = new DirCacheIterator(dirCache); FileTreeIterator iterB = new FileTreeIterator(repository); // Seems bug in DiffFormatter when work with working. Disable detect // renames by formatter and do it later. formatter.setDetectRenames(false); diff = formatter.scan(iterA, iterB); if (!params.isNoRenames()) { // Detect renames. RenameDetector renameDetector = createRenameDetector(); ContentSource.Pair sourcePairReader = new ContentSource.Pair(ContentSource.create(reader), ContentSource.create(iterB)); renameDetector.addAll(diff); diff = renameDetector.compute(sourcePairReader, NullProgressMonitor.INSTANCE); } } finally { reader.close(); if (dirCache != null) { dirCache.unlock(); } } return diff; } /** * Show changes between specified revision and working tree. * * @param commitId * id of commit * @param formatter * diff formatter * @return list of diff entries * @throws IOException * if any i/o errors occurs */ private List<DiffEntry> commitToWorkingTree(String commitId, DiffFormatter formatter) throws IOException { ObjectId commitA = repository.resolve(commitId); if (commitA == null) { File heads = new File(repository.getWorkTree().getPath() + "/.git/refs/heads"); if (heads.exists() && heads.list().length == 0) { return Collections.emptyList(); } throw new IllegalArgumentException("Invalid commit id " + commitId); } RevTree treeA; try (RevWalk revWalkA = new RevWalk(repository)) { treeA = revWalkA.parseTree(commitA); } List<DiffEntry> diff; try (ObjectReader reader = repository.newObjectReader()) { CanonicalTreeParser iterA = new CanonicalTreeParser(); iterA.reset(reader, treeA); FileTreeIterator iterB = new FileTreeIterator(repository); // Seems bug in DiffFormatter when work with working. Disable detect // renames by formatter and do it later. formatter.setDetectRenames(false); diff = formatter.scan(iterA, iterB); if (!params.isNoRenames()) { // Detect renames. RenameDetector renameDetector = createRenameDetector(); ContentSource.Pair sourcePairReader = new ContentSource.Pair(ContentSource.create(reader), ContentSource.create(iterB)); renameDetector.addAll(diff); diff = renameDetector.compute(sourcePairReader, NullProgressMonitor.INSTANCE); } } return diff; } /** * Show changes between specified revision and index. If * <code>commitId == null</code> then view changes between HEAD and index. * * @param commitId * id of commit, pass <code>null</code> is the same as pass HEAD * @param formatter * diff formatter * @return list of diff entries * @throws IOException * if any i/o errors occurs */ private List<DiffEntry> commitToIndex(String commitId, DiffFormatter formatter) throws IOException { if (commitId == null) { commitId = Constants.HEAD; } ObjectId commitA = repository.resolve(commitId); if (commitA == null) { throw new IllegalArgumentException("Invalid commit id " + commitId); } RevTree treeA; try (RevWalk revWalkA = new RevWalk(repository)) { treeA = revWalkA.parseTree(commitA); } DirCache dirCache = null; List<DiffEntry> diff; try (ObjectReader reader = repository.newObjectReader()) { dirCache = repository.lockDirCache(); CanonicalTreeParser iterA = new CanonicalTreeParser(); iterA.reset(reader, treeA); DirCacheIterator iterB = new DirCacheIterator(dirCache); if (!params.isNoRenames()) { // Use embedded RenameDetector it works well with index and // revision history. formatter.setDetectRenames(true); int renameLimit = params.getRenameLimit(); if (renameLimit > 0) { formatter.getRenameDetector().setRenameLimit(renameLimit); } } diff = formatter.scan(iterA, iterB); } finally { if (dirCache != null) { dirCache.unlock(); } } return diff; } /** * Show changes between specified two revisions and index. If * <code>commitAId == null</code> then view changes between HEAD and revision commitBId. * * @param commitAId * id of commit A, pass <code>null</code> is the same as pass HEAD * @param commitBId * id of commit B * @param formatter * diff formatter * @return list of diff entries * @throws IOException * if any i/o errors occurs */ private List<DiffEntry> commitToCommit(String commitAId, String commitBId, DiffFormatter formatter) throws IOException { if (commitAId == null) { commitAId = Constants.HEAD; } ObjectId commitA = repository.resolve(commitAId); if (commitA == null) { throw new IllegalArgumentException("Invalid commit id " + commitAId); } ObjectId commitB = repository.resolve(commitBId); if (commitB == null) { throw new IllegalArgumentException("Invalid commit id " + commitBId); } RevTree treeA; try (RevWalk revWalkA = new RevWalk(repository)) { treeA = revWalkA.parseTree(commitA); } RevTree treeB; try (RevWalk revWalkB = new RevWalk(repository)) { treeB = revWalkB.parseTree(commitB); } if (!params.isNoRenames()) { // Use embedded RenameDetector it works well with index and revision // history. formatter.setDetectRenames(true); int renameLimit = params.getRenameLimit(); if (renameLimit > 0) { formatter.getRenameDetector().setRenameLimit(renameLimit); } } return formatter.scan(treeA, treeB); } private RenameDetector createRenameDetector() { RenameDetector renameDetector = new RenameDetector(repository); int renameLimit = params.getRenameLimit(); if (renameLimit > 0) { renameDetector.setRenameLimit(renameLimit); } return renameDetector; } private void writeRawDiff(List<DiffEntry> diff, DiffFormatter formatter) throws IOException { formatter.format(diff); formatter.flush(); } private void writeNames(List<DiffEntry> diff, OutputStream out) throws IOException { PrintWriter writer = new PrintWriter(out); for (DiffEntry de : diff) { writer.print((de.getChangeType() == ChangeType.DELETE ? de.getOldPath() : de.getNewPath()) + (diff.size() != diff.indexOf(de) + 1 ? lineSeparator() : "")); } writer.flush(); } private void writeNamesAndStatus(List<DiffEntry> diff, OutputStream out) throws IOException { PrintWriter writer = new PrintWriter(out); int diffSize = diff.size(); for (DiffEntry de : diff) { if (de.getChangeType() == ChangeType.ADD) { writer.print("A\t" + de.getNewPath() + (diffSize != diff.indexOf(de) + 1 ? lineSeparator() : "")); } else if (de.getChangeType() == ChangeType.DELETE) { writer.print("D\t" + de.getOldPath() + (diffSize != diff.indexOf(de) + 1 ? lineSeparator() : "")); } else if (de.getChangeType() == ChangeType.MODIFY) { writer.print("M\t" + de.getNewPath() + (diffSize != diff.indexOf(de) + 1 ? lineSeparator() : "")); } else if (de.getChangeType() == ChangeType.COPY) { writer.print("C\t" + de.getOldPath() + '\t' + de.getNewPath() + (diffSize != diff.indexOf(de) + 1 ? lineSeparator() : "")); } else if (de.getChangeType() == ChangeType.RENAME) { writer.print("R\t" + de.getOldPath() + '\t' + de.getNewPath() + (diffSize != diff.indexOf(de) + 1 ? lineSeparator() : "")); } } writer.flush(); } }