These methods make the RevTag API more like the RevCommit API, such that it is more consistent for applications to access the "fields" of a tag object in the same way that they access the fields of any commit object. Signed-off-by: Shawn O. Pearce <spearce@xxxxxxxxxxx> --- Robin Rosenberg <robin.rosenberg.lists@xxxxxxxxxx> wrote: > Where are the unit tests? New test file included. > I'm kind of surprised you did not optimized skipping past the object header, > like the parents are skipped when parsing commits. Also included. Otherwise unmodified from v1. .../org/spearce/jgit/revwalk/RevTagParseTest.java | 350 ++++++++++++++++++++ .../src/org/spearce/jgit/revwalk/RevCommit.java | 2 +- .../src/org/spearce/jgit/revwalk/RevTag.java | 74 ++++- .../src/org/spearce/jgit/util/RawParseUtils.java | 51 +++- 4 files changed, 473 insertions(+), 4 deletions(-) create mode 100644 org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/RevTagParseTest.java diff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/RevTagParseTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/RevTagParseTest.java new file mode 100644 index 0000000..82680f3 --- /dev/null +++ b/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/RevTagParseTest.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2008, Google Inc. + * + * 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 Git Development Community 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.spearce.jgit.revwalk; + +import java.io.ByteArrayOutputStream; + +import org.spearce.jgit.lib.Constants; +import org.spearce.jgit.lib.ObjectId; +import org.spearce.jgit.lib.PersonIdent; +import org.spearce.jgit.lib.RepositoryTestCase; + +public class RevTagParseTest extends RepositoryTestCase { + public void testTagBlob() throws Exception { + testOneType(Constants.OBJ_BLOB); + } + + public void testTagTree() throws Exception { + testOneType(Constants.OBJ_TREE); + } + + public void testTagCommit() throws Exception { + testOneType(Constants.OBJ_COMMIT); + } + + public void testTagTag() throws Exception { + testOneType(Constants.OBJ_TAG); + } + + private void testOneType(final int typeCode) throws Exception { + final ObjectId id = id("9788669ad918b6fcce64af8882fc9a81cb6aba67"); + final StringBuilder b = new StringBuilder(); + b.append("object " + id.name() + "\n"); + b.append("type " + Constants.typeString(typeCode) + "\n"); + b.append("tag v1.2.3.4.5\n"); + b.append("tagger A U. Thor <a_u_thor@xxxxxxxxxxx> 1218123387 +0700\n"); + b.append("\n"); + + final RevWalk rw = new RevWalk(db); + final RevTag c; + + c = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + assertNull(c.getObject()); + assertNull(c.getName()); + + c.parseCanonical(rw, b.toString().getBytes("UTF-8")); + assertNotNull(c.getObject()); + assertEquals(id, c.getObject().getId()); + assertSame(rw.lookupAny(id, typeCode), c.getObject()); + } + + public void testParseAllFields() throws Exception { + final ObjectId treeId = id("9788669ad918b6fcce64af8882fc9a81cb6aba67"); + final String name = "v1.2.3.4.5"; + final String taggerName = "A U. Thor"; + final String taggerEmail = "a_u_thor@xxxxxxxxxxx"; + final int taggerTime = 1218123387; + + final StringBuilder body = new StringBuilder(); + + body.append("object "); + body.append(treeId.name()); + body.append("\n"); + + body.append("type tree\n"); + + body.append("tag "); + body.append(name); + body.append("\n"); + + body.append("tagger "); + body.append(taggerName); + body.append(" <"); + body.append(taggerEmail); + body.append("> "); + body.append(taggerTime); + body.append(" +0700\n"); + + body.append("\n"); + + final RevWalk rw = new RevWalk(db); + final RevTag c; + + c = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + assertNull(c.getObject()); + assertNull(c.getName()); + + c.parseCanonical(rw, body.toString().getBytes("UTF-8")); + assertNotNull(c.getObject()); + assertEquals(treeId, c.getObject().getId()); + assertSame(rw.lookupTree(treeId), c.getObject()); + + assertNotNull(c.getName()); + assertEquals(name, c.getName()); + assertEquals("", c.getFullMessage()); + + final PersonIdent cTagger = c.getTaggerIdent(); + assertNotNull(cTagger); + assertEquals(taggerName, cTagger.getName()); + assertEquals(taggerEmail, cTagger.getEmailAddress()); + } + + private RevTag create(final String msg) throws Exception { + final StringBuilder b = new StringBuilder(); + b.append("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"); + b.append("type tree\n"); + b.append("tag v1.2.3.4.5\n"); + b.append("tagger A U. Thor <a_u_thor@xxxxxxxxxxx> 1218123387 +0700\n"); + b.append("\n"); + b.append(msg); + + final RevTag c; + c = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + c.parseCanonical(new RevWalk(db), b.toString().getBytes("UTF-8")); + return c; + } + + public void testParse_implicit_UTF8_encoded() throws Exception { + final ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n" + .getBytes("UTF-8")); + b.write("type tree\n".getBytes("UTF-8")); + b.write("tag v1.2.3.4.5\n".getBytes("UTF-8")); + + b + .write("tagger F\u00f6r fattare <a_u_thor@xxxxxxxxxxx> 1218123387 +0700\n" + .getBytes("UTF-8")); + b.write("\n".getBytes("UTF-8")); + b.write("Sm\u00f6rg\u00e5sbord\n".getBytes("UTF-8")); + b.write("\n".getBytes("UTF-8")); + b.write("\u304d\u308c\u3044\n".getBytes("UTF-8")); + final RevTag c; + c = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + c.parseCanonical(new RevWalk(db), b.toByteArray()); + + assertEquals("F\u00f6r fattare", c.getTaggerIdent().getName()); + assertEquals("Sm\u00f6rg\u00e5sbord", c.getShortMessage()); + assertEquals("Sm\u00f6rg\u00e5sbord\n\n\u304d\u308c\u3044\n", c + .getFullMessage()); + } + + public void testParse_implicit_mixed_encoded() throws Exception { + final ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n" + .getBytes("UTF-8")); + b.write("type tree\n".getBytes("UTF-8")); + b.write("tag v1.2.3.4.5\n".getBytes("UTF-8")); + b + .write("tagger F\u00f6r fattare <a_u_thor@xxxxxxxxxxx> 1218123387 +0700\n" + .getBytes("ISO-8859-1")); + b.write("\n".getBytes("UTF-8")); + b.write("Sm\u00f6rg\u00e5sbord\n".getBytes("UTF-8")); + b.write("\n".getBytes("UTF-8")); + b.write("\u304d\u308c\u3044\n".getBytes("UTF-8")); + final RevTag c; + c = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + c.parseCanonical(new RevWalk(db), b.toByteArray()); + + assertEquals("F\u00f6r fattare", c.getTaggerIdent().getName()); + assertEquals("Sm\u00f6rg\u00e5sbord", c.getShortMessage()); + assertEquals("Sm\u00f6rg\u00e5sbord\n\n\u304d\u308c\u3044\n", c + .getFullMessage()); + } + + /** + * Test parsing of a commit whose encoding is given and works. + * + * @throws Exception + */ + public void testParse_explicit_encoded() throws Exception { + final ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n" + .getBytes("EUC-JP")); + b.write("type tree\n".getBytes("EUC-JP")); + b.write("tag v1.2.3.4.5\n".getBytes("EUC-JP")); + b + .write("tagger F\u00f6r fattare <a_u_thor@xxxxxxxxxxx> 1218123387 +0700\n" + .getBytes("EUC-JP")); + b.write("encoding euc_JP\n".getBytes("EUC-JP")); + b.write("\n".getBytes("EUC-JP")); + b.write("\u304d\u308c\u3044\n".getBytes("EUC-JP")); + b.write("\n".getBytes("EUC-JP")); + b.write("Hi\n".getBytes("EUC-JP")); + final RevTag c; + c = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + c.parseCanonical(new RevWalk(db), b.toByteArray()); + + assertEquals("F\u00f6r fattare", c.getTaggerIdent().getName()); + assertEquals("\u304d\u308c\u3044", c.getShortMessage()); + assertEquals("\u304d\u308c\u3044\n\nHi\n", c.getFullMessage()); + } + + /** + * This is a twisted case, but show what we expect here. We can revise the + * expectations provided this case is updated. + * + * What happens here is that an encoding us given, but data is not encoded + * that way (and we can detect it), so we try other encodings. + * + * @throws Exception + */ + public void testParse_explicit_bad_encoded() throws Exception { + final ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n" + .getBytes("UTF-8")); + b.write("type tree\n".getBytes("UTF-8")); + b.write("tag v1.2.3.4.5\n".getBytes("UTF-8")); + b + .write("tagger F\u00f6r fattare <a_u_thor@xxxxxxxxxxx> 1218123387 +0700\n" + .getBytes("ISO-8859-1")); + b.write("encoding EUC-JP\n".getBytes("UTF-8")); + b.write("\n".getBytes("UTF-8")); + b.write("\u304d\u308c\u3044\n".getBytes("UTF-8")); + b.write("\n".getBytes("UTF-8")); + b.write("Hi\n".getBytes("UTF-8")); + final RevTag c; + c = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + c.parseCanonical(new RevWalk(db), b.toByteArray()); + + assertEquals("F\u00f6r fattare", c.getTaggerIdent().getName()); + assertEquals("\u304d\u308c\u3044", c.getShortMessage()); + assertEquals("\u304d\u308c\u3044\n\nHi\n", c.getFullMessage()); + } + + /** + * This is a twisted case too, but show what we expect here. We can revise + * the expectations provided this case is updated. + * + * What happens here is that an encoding us given, but data is not encoded + * that way (and we can detect it), so we try other encodings. Here data + * could actually be decoded in the stated encoding, but we override using + * UTF-8. + * + * @throws Exception + */ + public void testParse_explicit_bad_encoded2() throws Exception { + final ByteArrayOutputStream b = new ByteArrayOutputStream(); + b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n" + .getBytes("UTF-8")); + b.write("type tree\n".getBytes("UTF-8")); + b.write("tag v1.2.3.4.5\n".getBytes("UTF-8")); + b + .write("tagger F\u00f6r fattare <a_u_thor@xxxxxxxxxxx> 1218123387 +0700\n" + .getBytes("UTF-8")); + b.write("encoding ISO-8859-1\n".getBytes("UTF-8")); + b.write("\n".getBytes("UTF-8")); + b.write("\u304d\u308c\u3044\n".getBytes("UTF-8")); + b.write("\n".getBytes("UTF-8")); + b.write("Hi\n".getBytes("UTF-8")); + final RevTag c; + c = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67")); + c.parseCanonical(new RevWalk(db), b.toByteArray()); + + assertEquals("F\u00f6r fattare", c.getTaggerIdent().getName()); + assertEquals("\u304d\u308c\u3044", c.getShortMessage()); + assertEquals("\u304d\u308c\u3044\n\nHi\n", c.getFullMessage()); + } + + public void testParse_NoMessage() throws Exception { + final String msg = ""; + final RevTag c = create(msg); + assertEquals(msg, c.getFullMessage()); + assertEquals(msg, c.getShortMessage()); + } + + public void testParse_OnlyLFMessage() throws Exception { + final RevTag c = create("\n"); + assertEquals("\n", c.getFullMessage()); + assertEquals("", c.getShortMessage()); + } + + public void testParse_ShortLineOnlyNoLF() throws Exception { + final String shortMsg = "This is a short message."; + final RevTag c = create(shortMsg); + assertEquals(shortMsg, c.getFullMessage()); + assertEquals(shortMsg, c.getShortMessage()); + } + + public void testParse_ShortLineOnlyEndLF() throws Exception { + final String shortMsg = "This is a short message."; + final String fullMsg = shortMsg + "\n"; + final RevTag c = create(fullMsg); + assertEquals(fullMsg, c.getFullMessage()); + assertEquals(shortMsg, c.getShortMessage()); + } + + public void testParse_ShortLineOnlyEmbeddedLF() throws Exception { + final String fullMsg = "This is a\nshort message."; + final String shortMsg = fullMsg.replace('\n', ' '); + final RevTag c = create(fullMsg); + assertEquals(fullMsg, c.getFullMessage()); + assertEquals(shortMsg, c.getShortMessage()); + } + + public void testParse_ShortLineOnlyEmbeddedAndEndingLF() throws Exception { + final String fullMsg = "This is a\nshort message.\n"; + final String shortMsg = "This is a short message."; + final RevTag c = create(fullMsg); + assertEquals(fullMsg, c.getFullMessage()); + assertEquals(shortMsg, c.getShortMessage()); + } + + public void testParse_GitStyleMessage() throws Exception { + final String shortMsg = "This fixes a bug."; + final String body = "We do it with magic and pixie dust and stuff.\n" + + "\n" + "Signed-off-by: A U. Thor <author@xxxxxxxxxxx>\n"; + final String fullMsg = shortMsg + "\n" + "\n" + body; + final RevTag c = create(fullMsg); + assertEquals(fullMsg, c.getFullMessage()); + assertEquals(shortMsg, c.getShortMessage()); + } + + private static ObjectId id(final String str) { + return ObjectId.fromString(str); + } +} diff --git a/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java b/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java index 7454d8e..de11c39 100644 --- a/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java +++ b/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java @@ -367,7 +367,7 @@ public final String getShortMessage() { return str; } - private static boolean hasLF(final byte[] r, int b, final int e) { + static boolean hasLF(final byte[] r, int b, final int e) { while (b < e) if (r[b++] == '\n') return true; diff --git a/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevTag.java b/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevTag.java index 77a55cd..82f0009 100644 --- a/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevTag.java +++ b/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevTag.java @@ -38,6 +38,7 @@ package org.spearce.jgit.revwalk; import java.io.IOException; +import java.nio.charset.Charset; import org.spearce.jgit.errors.CorruptObjectException; import org.spearce.jgit.errors.IncorrectObjectTypeException; @@ -45,6 +46,7 @@ import org.spearce.jgit.lib.AnyObjectId; import org.spearce.jgit.lib.Constants; import org.spearce.jgit.lib.ObjectLoader; +import org.spearce.jgit.lib.PersonIdent; import org.spearce.jgit.lib.Tag; import org.spearce.jgit.util.MutableInteger; import org.spearce.jgit.util.RawParseUtils; @@ -100,7 +102,77 @@ void parseCanonical(final RevWalk walk, final byte[] rawTag) public int getType() { return Constants.OBJ_TAG; } - + + /** + * Parse the tagger identity from the raw buffer. + * <p> + * This method parses and returns the content of the tagger line, after + * taking the tag's character set into account and decoding the tagger + * name and email address. This method is fairly expensive and produces a + * new PersonIdent instance on each invocation. Callers should invoke this + * method only if they are certain they will be outputting the result, and + * should cache the return value for as long as necessary to use all + * information from it. + * + * @return identity of the tagger (name, email) and the time the tag + * was made by the tagger; null if no tagger line was found. + */ + public final PersonIdent getTaggerIdent() { + final byte[] raw = buffer; + final int nameB = RawParseUtils.tagger(raw, 0); + if (nameB < 0) + return null; + return RawParseUtils.parsePersonIdent(raw, nameB); + } + + /** + * Parse the complete tag message and decode it to a string. + * <p> + * This method parses and returns the message portion of the tag buffer, + * after taking the tag's character set into account and decoding the buffer + * using that character set. This method is a fairly expensive operation and + * produces a new string on each invocation. + * + * @return decoded tag message as a string. Never null. + */ + public final String getFullMessage() { + final byte[] raw = buffer; + final int msgB = RawParseUtils.tagMessage(raw, 0); + if (msgB < 0) + return ""; + final Charset enc = RawParseUtils.parseEncoding(raw); + return RawParseUtils.decode(enc, raw, msgB, raw.length); + } + + /** + * Parse the tag message and return the first "line" of it. + * <p> + * The first line is everything up to the first pair of LFs. This is the + * "oneline" format, suitable for output in a single line display. + * <p> + * This method parses and returns the message portion of the tag buffer, + * after taking the tag's character set into account and decoding the buffer + * using that character set. This method is a fairly expensive operation and + * produces a new string on each invocation. + * + * @return decoded tag message as a string. Never null. The returned string + * does not contain any LFs, even if the first paragraph spanned + * multiple lines. Embedded LFs are converted to spaces. + */ + public final String getShortMessage() { + final byte[] raw = buffer; + final int msgB = RawParseUtils.tagMessage(raw, 0); + if (msgB < 0) + return ""; + + final Charset enc = RawParseUtils.parseEncoding(raw); + final int msgE = RawParseUtils.endOfParagraph(raw, msgB); + String str = RawParseUtils.decode(enc, raw, msgB, msgE); + if (RevCommit.hasLF(raw, msgB, msgE)) + str = str.replace('\n', ' '); + return str; + } + /** * Parse this tag buffer for display. * diff --git a/org.spearce.jgit/src/org/spearce/jgit/util/RawParseUtils.java b/org.spearce.jgit/src/org/spearce/jgit/util/RawParseUtils.java index 758e7af..1b8bf94 100644 --- a/org.spearce.jgit/src/org/spearce/jgit/util/RawParseUtils.java +++ b/org.spearce.jgit/src/org/spearce/jgit/util/RawParseUtils.java @@ -40,6 +40,7 @@ import static org.spearce.jgit.lib.ObjectChecker.author; import static org.spearce.jgit.lib.ObjectChecker.committer; import static org.spearce.jgit.lib.ObjectChecker.encoding; +import static org.spearce.jgit.lib.ObjectChecker.tagger; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; @@ -397,6 +398,34 @@ public static final int committer(final byte[] b, int ptr) { } /** + * Locate the "tagger " header line data. + * + * @param b + * buffer to scan. + * @param ptr + * position in buffer to start the scan at. Most callers should + * pass 0 to ensure the scan starts from the beginning of the tag + * buffer and does not accidentally look at message body. + * @return position just after the space in "tagger ", so the first + * character of the tagger's name. If no tagger header can be + * located -1 is returned. + */ + public static final int tagger(final byte[] b, int ptr) { + final int sz = b.length; + if (ptr == 0) + ptr += 48; // skip the "object ..." line. + while (ptr < sz) { + if (b[ptr] == '\n') + return -1; + final int m = match(b, ptr, tagger); + if (m >= 0) + return m; + ptr = nextLF(b, ptr); + } + return -1; + } + + /** * Locate the "encoding " header line. * * @param b @@ -648,9 +677,27 @@ public static final int commitMessage(final byte[] b, int ptr) { while (ptr < sz && b[ptr] == 'p') ptr += 48; // skip this parent. - // skip any remaining header lines, ignoring what their actual - // header line type is. + // Skip any remaining header lines, ignoring what their actual + // header line type is. This is identical to the logic for a tag. // + return tagMessage(b, ptr); + } + + /** + * Locate the position of the tag message body. + * + * @param b + * buffer to scan. + * @param ptr + * position in buffer to start the scan at. Most callers should + * pass 0 to ensure the scan starts from the beginning of the tag + * buffer. + * @return position of the user's message buffer. + */ + public static final int tagMessage(final byte[] b, int ptr) { + final int sz = b.length; + if (ptr == 0) + ptr += 48; // skip the "object ..." line. while (ptr < sz && b[ptr] != '\n') ptr = nextLF(b, ptr); if (ptr < sz && b[ptr] == '\n') -- 1.6.1.2.418.gd79e6 -- Shawn. -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html