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 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 edits, final int i) { int end = i + 1; while (end < edits.size() diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java index f2f2f2e74..0c24fc6be 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/FileHeader.java @@ -134,6 +134,25 @@ public static enum PatchType { /** 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; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java index bfb20b64e..a6ea74eb1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/HunkHeader.java @@ -116,6 +116,8 @@ public int getLinesAdded() { /** 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 @@ -131,6 +133,21 @@ public AbbreviatedObjectId getId() { 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; @@ -173,48 +190,50 @@ public int getLinesContext() { /** @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; + 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; + 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); + 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() { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/patch/Patch.java b/org.eclipse.jgit/src/org/eclipse/jgit/patch/Patch.java index ce006dadb..cf42b665a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/patch/Patch.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/patch/Patch.java @@ -60,7 +60,7 @@ /** 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 ");