diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java new file mode 100644 index 000000000..996ee35a1 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java @@ -0,0 +1,186 @@ +/* + * 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(); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java index e41ec5670..4d25dbd02 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java @@ -81,6 +81,13 @@ public static enum ChangeType { COPY; } + /** + * Create an empty DiffEntry + */ + protected DiffEntry(){ + // reduce the visibility of the default constructor + } + /** * Convert the TreeWalk into DiffEntry headers. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java index c12c54b05..cdcc5e63e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java @@ -48,18 +48,24 @@ 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. @@ -185,87 +191,10 @@ public void format(List extends DiffEntry> entries) throws IOException { * be written to. */ public void format(DiffEntry ent) throws IOException { - String oldName = quotePath("a/" + ent.getOldName()); - String newName = quotePath("b/" + ent.getNewName()); - out.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'); - break; - - case DELETE: - out.write(encodeASCII("deleted file mode ")); - ent.getOldMode().copyTo(out); - out.write('\n'); - break; - - case RENAME: - out.write(encodeASCII("similarity index " + ent.getScore() + "%")); - out.write('\n'); - - out.write(encode("rename from " + quotePath(ent.getOldName()))); - out.write('\n'); - - out.write(encode("rename to " + quotePath(ent.getNewName()))); - out.write('\n'); - break; - - case COPY: - out.write(encodeASCII("similarity index " + ent.getScore() + "%")); - out.write('\n'); - - out.write(encode("copy from " + quotePath(ent.getOldName()))); - out.write('\n'); - - out.write(encode("copy to " + quotePath(ent.getNewName()))); - out.write('\n'); - - if (!ent.getOldMode().equals(ent.getNewMode())) { - out.write(encodeASCII("new file mode ")); - ent.getNewMode().copyTo(out); - out.write('\n'); - } - break; - } - - switch (ent.getChangeType()) { - case RENAME: - case MODIFY: - if (!ent.getOldMode().equals(ent.getNewMode())) { - out.write(encodeASCII("old mode ")); - ent.getOldMode().copyTo(out); - out.write('\n'); - - out.write(encodeASCII("new mode ")); - ent.getNewMode().copyTo(out); - out.write('\n'); - } - } - - out.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')); + writeDiffHeader(out, ent); 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")); - } + writeGitLinkDiffText(out, ent); } else { byte[] aRaw = open(ent.getOldMode(), ent.getOldId()); byte[] bRaw = open(ent.getNewMode(), ent.getNewId()); @@ -281,6 +210,93 @@ public void format(DiffEntry ent) throws IOException { } } + 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()); + 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.getOldName()))); + o.write('\n'); + + o.write(encode("rename to " + quotePath(ent.getNewName()))); + o.write('\n'); + break; + + case COPY: + o.write(encodeASCII("similarity index " + ent.getScore() + "%")); + o.write('\n'); + + o.write(encode("copy from " + quotePath(ent.getOldName()))); + o.write('\n'); + + o.write(encode("copy to " + quotePath(ent.getNewName()))); + o.write('\n'); + + if (!ent.getOldMode().equals(ent.getNewMode())) { + o.write(encodeASCII("new file mode ")); + ent.getNewMode().copyTo(o); + 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 oldId) { if (oldId.isComplete() && db != null) oldId = oldId.toObjectId().abbreviate(db, abbreviationLength); @@ -513,6 +529,58 @@ protected void writeLine(final char prefix, final RawText text, out.write('\n'); } + /** + * Creates a {@link FileHeader} representing the given {@link DiffEntry} + *
+ * 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