--- /dev/null
+/*
+ * Copyright (C) 2010, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.diff;
+
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RepositoryTestCase;
+import org.eclipse.jgit.patch.FileHeader;
+import org.eclipse.jgit.patch.HunkHeader;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+
+public class DiffFormatterTest extends RepositoryTestCase {
+ private static final String DIFF = "diff --git ";
+
+ private static final String REGULAR_FILE = "100644";
+
+ private static final String GITLINK = "160000";
+
+ private static final String PATH_A = "src/a";
+
+ private static final String PATH_B = "src/b";
+
+ private DiffFormatter df;
+
+ private TestRepository testDb;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ testDb = new TestRepository(db);
+ df = new DiffFormatter(DisabledOutputStream.INSTANCE);
+ df.setRepository(db);
+ }
+
+ public void testCreateFileHeader_Modify() throws Exception {
+ ObjectId adId = blob("a\nd\n");
+ ObjectId abcdId = blob("a\nb\nc\nd\n");
+
+ String diffHeader = makeDiffHeader(PATH_A, PATH_A, adId, abcdId);
+
+ DiffEntry ad = DiffEntry.delete(PATH_A, adId);
+ DiffEntry abcd = DiffEntry.add(PATH_A, abcdId);
+
+ DiffEntry mod = DiffEntry.pair(ChangeType.MODIFY, ad, abcd, 0);
+
+ FileHeader fh = df.createFileHeader(mod);
+
+ assertEquals(diffHeader, RawParseUtils.decode(fh.getBuffer()));
+ assertEquals(0, fh.getStartOffset());
+ assertEquals(fh.getBuffer().length, fh.getEndOffset());
+ assertEquals(FileHeader.PatchType.UNIFIED, fh.getPatchType());
+
+ assertEquals(1, fh.getHunks().size());
+
+ HunkHeader hh = fh.getHunks().get(0);
+ assertEquals(1, hh.toEditList().size());
+
+ EditList el = hh.toEditList();
+ assertEquals(1, el.size());
+
+ Edit e = el.get(0);
+ assertEquals(1, e.getBeginA());
+ assertEquals(1, e.getEndA());
+ assertEquals(1, e.getBeginB());
+ assertEquals(3, e.getEndB());
+ assertEquals(Edit.Type.INSERT, e.getType());
+ }
+
+ public void testCreateFileHeader_Binary() throws Exception {
+ ObjectId adId = blob("a\nd\n");
+ ObjectId binId = blob("a\nb\nc\n\0\0\0\0d\n");
+
+ String diffHeader = makeDiffHeader(PATH_A, PATH_B, adId, binId)
+ + "Binary files differ\n";
+
+ DiffEntry ad = DiffEntry.delete(PATH_A, adId);
+ DiffEntry abcd = DiffEntry.add(PATH_B, binId);
+
+ DiffEntry mod = DiffEntry.pair(ChangeType.MODIFY, ad, abcd, 0);
+
+ FileHeader fh = df.createFileHeader(mod);
+
+ assertEquals(diffHeader, RawParseUtils.decode(fh.getBuffer()));
+ assertEquals(FileHeader.PatchType.BINARY, fh.getPatchType());
+
+ assertEquals(1, fh.getHunks().size());
+
+ HunkHeader hh = fh.getHunks().get(0);
+ assertEquals(0, hh.toEditList().size());
+ }
+
+ public void testCreateFileHeader_GitLink() throws Exception {
+ ObjectId aId = blob("a\n");
+ ObjectId bId = blob("b\n");
+
+ String diffHeader = makeDiffHeaderModeChange(PATH_A, PATH_A, aId, bId,
+ GITLINK, REGULAR_FILE)
+ + "-Subproject commit " + aId.name() + "\n";
+
+ DiffEntry ad = DiffEntry.delete(PATH_A, aId);
+ ad.oldMode = FileMode.GITLINK;
+ DiffEntry abcd = DiffEntry.add(PATH_A, bId);
+
+ DiffEntry mod = DiffEntry.pair(ChangeType.MODIFY, ad, abcd, 0);
+
+ FileHeader fh = df.createFileHeader(mod);
+
+ assertEquals(diffHeader, RawParseUtils.decode(fh.getBuffer()));
+
+ assertEquals(1, fh.getHunks().size());
+
+ HunkHeader hh = fh.getHunks().get(0);
+ assertEquals(0, hh.toEditList().size());
+ }
+
+ private String makeDiffHeader(String pathA, String pathB, ObjectId aId,
+ ObjectId bId) {
+ String a = aId.abbreviate(db).name();
+ String b = bId.abbreviate(db).name();
+ return DIFF + "a/" + pathA + " " + "b/" + pathB + "\n" + //
+ "index " + a + ".." + b + " " + REGULAR_FILE + "\n" + //
+ "--- a/" + pathA + "\n" + //
+ "+++ b/" + pathB + "\n";
+ }
+
+ private String makeDiffHeaderModeChange(String pathA, String pathB,
+ ObjectId aId, ObjectId bId, String modeA, String modeB) {
+ String a = aId.abbreviate(db).name();
+ String b = bId.abbreviate(db).name();
+ return DIFF + "a/" + pathA + " " + "b/" + pathB + "\n" + //
+ "old mode " + modeA + "\n" + //
+ "new mode " + modeB + "\n" + //
+ "index " + a + ".." + b + "\n" + //
+ "--- a/" + pathA + "\n" + //
+ "+++ b/" + pathB + "\n";
+ }
+
+ private ObjectId blob(String content) throws Exception {
+ return testDb.blob(content).copy();
+ }
+
+}
COPY;
}
+ /**
+ * Create an empty DiffEntry
+ */
+ protected DiffEntry(){
+ // reduce the visibility of the default constructor
+ }
+
/**
* Convert the TreeWalk into DiffEntry headers.
*
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.List;
import org.eclipse.jgit.JGitText;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.patch.FileHeader;
+import org.eclipse.jgit.patch.HunkHeader;
+import org.eclipse.jgit.patch.FileHeader.PatchType;
import org.eclipse.jgit.util.QuotedString;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
/**
* Format an {@link EditList} as a Git style unified patch script.
* be written to.
*/
public void format(DiffEntry ent) throws IOException {
+ writeDiffHeader(out, ent);
+
+ if (ent.getOldMode() == GITLINK || ent.getNewMode() == GITLINK) {
+ writeGitLinkDiffText(out, ent);
+ } else {
+ byte[] aRaw = open(ent.getOldMode(), ent.getOldId());
+ byte[] bRaw = open(ent.getNewMode(), ent.getNewId());
+
+ if (RawText.isBinary(aRaw) || RawText.isBinary(bRaw)) {
+ out.write(encodeASCII("Binary files differ\n"));
+
+ } else {
+ RawText a = rawTextFactory.create(aRaw);
+ RawText b = rawTextFactory.create(bRaw);
+ formatEdits(a, b, new MyersDiff(a, b).getEdits());
+ }
+ }
+ }
+
+ 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.getOldName());
String newName = quotePath("b/" + ent.getNewName());
- out.write(encode("diff --git " + oldName + " " + newName + "\n"));
+ o.write(encode("diff --git " + oldName + " " + newName + "\n"));
switch (ent.getChangeType()) {
case ADD:
- out.write(encodeASCII("new file mode "));
- ent.getNewMode().copyTo(out);
- out.write('\n');
+ o.write(encodeASCII("new file mode "));
+ ent.getNewMode().copyTo(o);
+ o.write('\n');
break;
case DELETE:
- out.write(encodeASCII("deleted file mode "));
- ent.getOldMode().copyTo(out);
- out.write('\n');
+ o.write(encodeASCII("deleted file mode "));
+ ent.getOldMode().copyTo(o);
+ o.write('\n');
break;
case RENAME:
- out.write(encodeASCII("similarity index " + ent.getScore() + "%"));
- out.write('\n');
+ o.write(encodeASCII("similarity index " + ent.getScore() + "%"));
+ o.write('\n');
- out.write(encode("rename from " + quotePath(ent.getOldName())));
- out.write('\n');
+ o.write(encode("rename from " + quotePath(ent.getOldName())));
+ o.write('\n');
- out.write(encode("rename to " + quotePath(ent.getNewName())));
- out.write('\n');
+ o.write(encode("rename to " + quotePath(ent.getNewName())));
+ o.write('\n');
break;
case COPY:
- out.write(encodeASCII("similarity index " + ent.getScore() + "%"));
- out.write('\n');
+ o.write(encodeASCII("similarity index " + ent.getScore() + "%"));
+ o.write('\n');
- out.write(encode("copy from " + quotePath(ent.getOldName())));
- out.write('\n');
+ o.write(encode("copy from " + quotePath(ent.getOldName())));
+ o.write('\n');
- out.write(encode("copy to " + quotePath(ent.getNewName())));
- out.write('\n');
+ o.write(encode("copy to " + quotePath(ent.getNewName())));
+ o.write('\n');
if (!ent.getOldMode().equals(ent.getNewMode())) {
- out.write(encodeASCII("new file mode "));
- ent.getNewMode().copyTo(out);
- out.write('\n');
+ o.write(encodeASCII("new file mode "));
+ ent.getNewMode().copyTo(o);
+ o.write('\n');
}
break;
}
case RENAME:
case MODIFY:
if (!ent.getOldMode().equals(ent.getNewMode())) {
- out.write(encodeASCII("old mode "));
- ent.getOldMode().copyTo(out);
- out.write('\n');
+ o.write(encodeASCII("old mode "));
+ ent.getOldMode().copyTo(o);
+ o.write('\n');
- out.write(encodeASCII("new mode "));
- ent.getNewMode().copyTo(out);
- out.write('\n');
+ o.write(encodeASCII("new mode "));
+ ent.getNewMode().copyTo(o);
+ o.write('\n');
}
}
- out.write(encodeASCII("index " //
+ o.write(encodeASCII("index " //
+ format(ent.getOldId()) //
+ ".." //
+ format(ent.getNewId())));
if (ent.getOldMode().equals(ent.getNewMode())) {
- out.write(' ');
- ent.getNewMode().copyTo(out);
- }
- out.write('\n');
- out.write(encode("--- " + oldName + '\n'));
- out.write(encode("+++ " + newName + '\n'));
-
- if (ent.getOldMode() == GITLINK || ent.getNewMode() == GITLINK) {
- if (ent.getOldMode() == GITLINK) {
- out.write(encodeASCII("-Subproject commit "
- + ent.getOldId().name() + "\n"));
- }
- if (ent.getNewMode() == GITLINK) {
- out.write(encodeASCII("+Subproject commit "
- + ent.getNewId().name() + "\n"));
- }
- } else {
- byte[] aRaw = open(ent.getOldMode(), ent.getOldId());
- byte[] bRaw = open(ent.getNewMode(), ent.getNewId());
-
- if (RawText.isBinary(aRaw) || RawText.isBinary(bRaw)) {
- out.write(encodeASCII("Binary files differ\n"));
-
- } else {
- RawText a = rawTextFactory.create(aRaw);
- RawText b = rawTextFactory.create(bRaw);
- formatEdits(a, b, new MyersDiff(a, b).getEdits());
- }
+ o.write(' ');
+ ent.getNewMode().copyTo(o);
}
+ o.write('\n');
+ o.write(encode("--- " + oldName + '\n'));
+ o.write(encode("+++ " + newName + '\n'));
}
private String format(AbbreviatedObjectId oldId) {
out.write('\n');
}
+ /**
+ * Creates a {@link FileHeader} representing the given {@link DiffEntry}
+ * <p>
+ * This method does not use the OutputStream associated with this
+ * DiffFormatter instance. It is therefore safe to instantiate this
+ * DiffFormatter instance with a {@link DisabledOutputStream} if this method
+ * is the only one that will be used.
+ *
+ * @param ent
+ * the DiffEntry to create the FileHeader for
+ * @return a FileHeader representing the DiffEntry. The FileHeader's buffer
+ * will contain only the header of the diff output. It will also
+ * contain one {@link HunkHeader}.
+ * @throws IOException
+ * the stream threw an exception while writing to it, or one of
+ * the blobs referenced by the DiffEntry could not be read.
+ * @throws CorruptObjectException
+ * one of the blobs referenced by the DiffEntry is corrupt.
+ * @throws MissingObjectException
+ * one of the blobs referenced by the DiffEntry is missing.
+ */
+ public FileHeader createFileHeader(DiffEntry ent) throws IOException,
+ CorruptObjectException, MissingObjectException {
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ final EditList editList;
+ final FileHeader.PatchType type;
+
+ writeDiffHeader(buf, ent);
+
+ if (ent.getOldMode() == GITLINK || ent.getNewMode() == GITLINK) {
+ writeGitLinkDiffText(buf, ent);
+ editList = new EditList();
+ type = PatchType.UNIFIED;
+ } else {
+ byte[] aRaw = open(ent.getOldMode(), ent.getOldId());
+ byte[] bRaw = open(ent.getNewMode(), ent.getNewId());
+
+ if (RawText.isBinary(aRaw) || RawText.isBinary(bRaw)) {
+ buf.write(encodeASCII("Binary files differ\n"));
+ editList = new EditList();
+ type = PatchType.BINARY;
+ } else {
+ RawText a = rawTextFactory.create(aRaw);
+ RawText b = rawTextFactory.create(bRaw);
+ editList = new MyersDiff(a, b).getEdits();
+ type = PatchType.UNIFIED;
+ }
+ }
+
+ return new FileHeader(buf.toByteArray(), editList, type);
+ }
+
private int findCombinedEnd(final List<Edit> edits, final int i) {
int end = i + 1;
while (end < edits.size()
/** If {@link #patchType} is {@link PatchType#GIT_BINARY}, the old image */
BinaryHunk reverseBinaryHunk;
+ /**
+ * Constructs a new FileHeader
+ *
+ * @param headerLines
+ * buffer holding the diff header for this file
+ * @param edits
+ * the edits for this file
+ * @param type
+ * the type of patch used to modify this file
+ */
+ public FileHeader(final byte[] headerLines, EditList edits, PatchType type) {
+ this(headerLines, 0);
+ endOffset = headerLines.length;
+ int ptr = parseGitFileName(Patch.DIFF_GIT.length, headerLines.length);
+ ptr = parseGitHeaders(ptr, headerLines.length);
+ this.patchType = type;
+ addHunk(new HunkHeader(this, edits));
+ }
+
FileHeader(final byte[] b, final int offset) {
buf = b;
startOffset = offset;
/** Total number of lines of context appearing in this hunk */
int nContext;
+ private EditList editList;
+
HunkHeader(final FileHeader fh, final int offset) {
this(fh, offset, new OldImage() {
@Override
old = oi;
}
+ HunkHeader(final FileHeader fh, final EditList editList) {
+ this(fh, fh.buf.length);
+ this.editList = editList;
+ endOffset = startOffset;
+ nContext = 0;
+ if (editList.isEmpty()) {
+ newStartLine = 0;
+ newLineCount = 0;
+ } else {
+ newStartLine = editList.get(0).getBeginB();
+ Edit last = editList.get(editList.size() - 1);
+ newLineCount = last.getEndB() - newStartLine;
+ }
+ }
+
/** @return header for the file this hunk applies to */
public FileHeader getFileHeader() {
return file;
/** @return a list describing the content edits performed within the hunk. */
public EditList toEditList() {
- final EditList r = new EditList();
- final byte[] buf = file.buf;
- int c = nextLF(buf, startOffset);
- int oLine = old.startLine;
- int nLine = newStartLine;
- Edit in = null;
-
- SCAN: for (; c < endOffset; c = nextLF(buf, c)) {
- switch (buf[c]) {
- case ' ':
- case '\n':
- in = null;
- oLine++;
- nLine++;
- continue;
-
- case '-':
- if (in == null) {
- in = new Edit(oLine - 1, nLine - 1);
- r.add(in);
+ if (editList == null) {
+ editList = new EditList();
+ final byte[] buf = file.buf;
+ int c = nextLF(buf, startOffset);
+ int oLine = old.startLine;
+ int nLine = newStartLine;
+ Edit in = null;
+
+ SCAN: for (; c < endOffset; c = nextLF(buf, c)) {
+ switch (buf[c]) {
+ case ' ':
+ case '\n':
+ in = null;
+ oLine++;
+ nLine++;
+ continue;
+
+ case '-':
+ if (in == null) {
+ in = new Edit(oLine - 1, nLine - 1);
+ editList.add(in);
+ }
+ oLine++;
+ in.extendA();
+ continue;
+
+ case '+':
+ if (in == null) {
+ in = new Edit(oLine - 1, nLine - 1);
+ editList.add(in);
+ }
+ nLine++;
+ in.extendB();
+ continue;
+
+ case '\\': // Matches "\ No newline at end of file"
+ continue;
+
+ default:
+ break SCAN;
}
- oLine++;
- in.extendA();
- continue;
-
- case '+':
- if (in == null) {
- in = new Edit(oLine - 1, nLine - 1);
- r.add(in);
- }
- nLine++;
- in.extendB();
- continue;
-
- case '\\': // Matches "\ No newline at end of file"
- continue;
-
- default:
- break SCAN;
}
}
- return r;
+ return editList;
}
void parseHeader() {
/** A parsed collection of {@link FileHeader}s from a unified diff patch file */
public class Patch {
- private static final byte[] DIFF_GIT = encodeASCII("diff --git ");
+ static final byte[] DIFF_GIT = encodeASCII("diff --git ");
private static final byte[] DIFF_CC = encodeASCII("diff --cc ");