/*
* Copyright (c) 2011, 2012 Roberto Tyley
*
* This file is part of 'Agit' - an Android Git client.
*
* Agit 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, either version 3 of the License, or
* (at your option) any later version.
*
* Agit 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/ .
*/
package com.madgag.agit.diff;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.util.Collections.emptyList;
import static org.eclipse.jgit.diff.RawTextComparator.DEFAULT;
import static org.eclipse.jgit.lib.Constants.encode;
import static org.eclipse.jgit.lib.Constants.encodeASCII;
import static org.eclipse.jgit.lib.FileMode.GITLINK;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.List;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.diff.MyersDiff;
import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
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.util.QuotedString;
/**
* Format an {@link EditList} as a Git style unified patch script.
*/
public class LineContextDiffer {
private static final byte[] noNewLine = encodeASCII("\\ No newline at end of file\n");
private int context;
private int abbreviationLength;
private int bigFileThreshold = 1 * 1024 * 1024;
private final ObjectReader objectReader;
/**
* Create a new formatter with a default level of context.
*/
public LineContextDiffer(ObjectReader objectReader) {
this.objectReader = objectReader;
setContext(3);
setAbbreviationLength(7);
}
/**
* Change the number of lines of context to display.
*
* @param lineCount number of lines of context to see before the first
* modification and after the last modification within a hunk of
* the modified file.
*/
public void setContext(final int lineCount) {
if (lineCount < 0)
throw new IllegalArgumentException(
JGitText.get().contextMustBeNonNegative);
context = lineCount;
}
/**
* Change the number of digits to show in an ObjectId.
*
* @param count number of digits to show in an ObjectId.
*/
public void setAbbreviationLength(final int count) {
if (count < 0)
throw new IllegalArgumentException(
JGitText.get().abbreviationLengthMustBeNonNegative);
abbreviationLength = count;
}
/**
* Set the maximum file size that should be considered for diff output.
* <p/>
* Text files that are larger than this size will not have a difference
* generated during output.
*
* @param bigFileThreshold the limit, in bytes.
*/
public void setBigFileThreshold(int bigFileThreshold) {
this.bigFileThreshold = bigFileThreshold;
}
/**
* Format a patch script from a list of difference entries.
*
* @param entries entries describing the affected files.
* @throws IOException a file's content cannot be read, or the output stream cannot
* be written to.
*/
public void format(List<? extends DiffEntry> entries) throws IOException {
for (DiffEntry ent : entries)
format(ent);
}
/**
* Format a patch script for one file entry.
*
* @param ent the entry to be formatted.
* @throws IOException a file's content cannot be read, or the output stream cannot
* be written to.
*/
public List<Hunk> format(DiffEntry ent) throws IOException {
//writeDiffHeader(out, ent);
if (ent.getOldMode() == GITLINK || ent.getNewMode() == GITLINK) {
// writeGitLinkDiffText(out, ent);
return emptyList();
} else {
byte[] aRaw, bRaw;
try {
aRaw = open(objectReader, ent.getOldMode(), ent.getOldId());
bRaw = open(objectReader, ent.getNewMode(), ent.getNewId());
} finally {
// objectReader.release();
}
if (RawText.isBinary(aRaw) || RawText.isBinary(bRaw)) {
//out.write(encodeASCII("Binary files differ\n"));
return emptyList();
} else {
RawText a = new RawText(aRaw);
RawText b = new RawText(bRaw);
return formatEdits(a, b, MyersDiff.INSTANCE.diff(DEFAULT, a, b));
}
}
}
private void writeGitLinkDiffText(OutputStream o, DiffEntry ent)
throws IOException {
if (ent.getOldMode() == GITLINK) {
o.write(encodeASCII("-Subproject commit " + ent.getOldId().name()
+ "\n"));
}
if (ent.getNewMode() == GITLINK) {
o.write(encodeASCII("+Subproject commit " + ent.getNewId().name()
+ "\n"));
}
}
private void writeDiffHeader(OutputStream o, DiffEntry ent)
throws IOException {
String oldName = quotePath("a/" + ent.getOldPath());
String newName = quotePath("b/" + ent.getNewPath());
o.write(encode("diff --git " + oldName + " " + newName + "\n"));
switch (ent.getChangeType()) {
case ADD:
o.write(encodeASCII("new file mode "));
ent.getNewMode().copyTo(o);
o.write('\n');
break;
case DELETE:
o.write(encodeASCII("deleted file mode "));
ent.getOldMode().copyTo(o);
o.write('\n');
break;
case RENAME:
o.write(encodeASCII("similarity index " + ent.getScore() + "%"));
o.write('\n');
o.write(encode("rename from " + quotePath(ent.getOldPath())));
o.write('\n');
o.write(encode("rename to " + quotePath(ent.getNewPath())));
o.write('\n');
break;
case COPY:
o.write(encodeASCII("similarity index " + ent.getScore() + "%"));
o.write('\n');
o.write(encode("copy from " + quotePath(ent.getOldPath())));
o.write('\n');
o.write(encode("copy to " + quotePath(ent.getNewPath())));
o.write('\n');
if (!ent.getOldMode().equals(ent.getNewMode())) {
o.write(encodeASCII("new file mode "));
ent.getNewMode().copyTo(o);
o.write('\n');
}
break;
case MODIFY:
int score = ent.getScore();
if (0 < score && score <= 100) {
o.write(encodeASCII("dissimilarity index " + (100 - score)
+ "%"));
o.write('\n');
}
break;
}
switch (ent.getChangeType()) {
case RENAME:
case MODIFY:
if (!ent.getOldMode().equals(ent.getNewMode())) {
o.write(encodeASCII("old mode "));
ent.getOldMode().copyTo(o);
o.write('\n');
o.write(encodeASCII("new mode "));
ent.getNewMode().copyTo(o);
o.write('\n');
}
}
o.write(encodeASCII("index " //
+ format(ent.getOldId()) //
+ ".." //
+ format(ent.getNewId())));
if (ent.getOldMode().equals(ent.getNewMode())) {
o.write(' ');
ent.getNewMode().copyTo(o);
}
o.write('\n');
o.write(encode("--- " + oldName + '\n'));
o.write(encode("+++ " + newName + '\n'));
}
private String format(AbbreviatedObjectId id) {
if (id.isComplete()) {
try {
id = objectReader.abbreviate(id.toObjectId(), abbreviationLength);
} catch (IOException cannotAbbreviate) {
// Ignore this. We'll report the full identity.
} finally {
// reader.release();
}
}
return id.name();
}
private static String quotePath(String name) {
String q = QuotedString.GIT_PATH.quote(name);
return ('"' + name + '"').equals(q) ? name : q;
}
private byte[] open(ObjectReader reader, FileMode mode,
AbbreviatedObjectId id) throws IOException {
if (mode == FileMode.MISSING)
return new byte[] { };
if (mode.getObjectType() != Constants.OBJ_BLOB)
return new byte[] { };
if (!id.isComplete()) {
Collection<ObjectId> ids = reader.resolve(id);
if (ids.size() == 1)
id = AbbreviatedObjectId.fromObjectId(ids.iterator().next());
else if (ids.size() == 0)
throw new MissingObjectException(id, Constants.OBJ_BLOB);
else
throw new AmbiguousObjectException(id, ids);
}
ObjectLoader ldr = reader.open(id.toObjectId());
return ldr.getCachedBytes(bigFileThreshold);
}
public List<Hunk> formatEdits(final RawText a, final RawText b, final EditList edits) throws IOException {
List<Hunk> hunks = newArrayList();
for (int curIdx = 0; curIdx < edits.size(); ) {
Edit curEdit = edits.get(curIdx);
final int endIdx = findCombinedEnd(edits, curIdx);
// Log.i("BUCK", "Will do edits "+curIdx+" - "+endIdx);
final Edit endEdit = edits.get(endIdx);
int aCur = max(0, curEdit.getBeginA() - context);
int bCur = max(0, curEdit.getBeginB() - context);
final int aEnd = min(a.size(), endEdit.getEndA() + context);
final int bEnd = min(b.size(), endEdit.getEndB() + context);
String before = extractHunk(a, aCur, aEnd), after = extractHunk(b, bCur, bEnd);
hunks.add(new Hunk(before, after));
curIdx = endIdx + 1;
}
return hunks;
}
private String extractHunk(RawText rawText, int startLine, int endLine) {
try {
ByteArrayOutputStream bas = new ByteArrayOutputStream();
for (int line = startLine; line < endLine; ++line) {
rawText.writeLine(bas, line);
bas.write('\n');
}
return new String(bas.toByteArray(), "utf-8");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private int findCombinedEnd(final List<Edit> edits, final int i) {
int end = i + 1;
while (end < edits.size()
&& (combineA(edits, end) || combineB(edits, end)))
end++;
return end - 1;
}
private boolean combineA(final List<Edit> e, final int i) {
return e.get(i).getBeginA() - e.get(i - 1).getEndA() <= 2 * context;
}
private boolean combineB(final List<Edit> e, final int i) {
return e.get(i).getBeginB() - e.get(i - 1).getEndB() <= 2 * context;
}
private static boolean end(final Edit edit, final int a, final int b) {
return edit.getEndA() <= a && edit.getEndB() <= b;
}
}