{"thread":{"id":"19913","subject":"[JGIT PATCH 1/2] Add getEncoding() to RevCommit to discover the encoding","startedAt":"2009-06-24T03:11:48Z","lastAt":"2009-06-24T17:30:29Z","messageCount":3,"participants":["Shawn O. Pearce"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"116843","messageId":"1245813109-29763-1-git-send-email-spearce@spearce.org","threadId":"19913","inReplyTo":null,"subject":"[JGIT PATCH 1/2] Add getEncoding() to RevCommit to discover the encoding","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2009-06-24T03:11:48Z","receivedAt":"2009-06-24T03:11:48Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"If an application needs to parse the raw buffer by hand it might\nbenefit from knowing the encoding of the commit.  We can make it\navailable to them through a getEncoding() method, using the same\nlogic we already use for getFullMessage() and getShortMessage(),\nbut this is still only an estimate based on the \"encoding\" header\nand may not be reality if the commit is horribly malformed.\n\nSigned-off-by: Shawn O. Pearce <spearce@spearce.org>\n---\n .../spearce/jgit/revwalk/RevCommitParseTest.java   |    6 ++++++\n .../src/org/spearce/jgit/revwalk/RevCommit.java    |   16 ++++++++++++++++\n 2 files changed, 22 insertions(+), 0 deletions(-)\n\ndiff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/RevCommitParseTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/RevCommitParseTest.java\nindex 9b95924..62a4ab5 100644\n--- a/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/RevCommitParseTest.java\n+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/RevCommitParseTest.java\n@@ -39,6 +39,7 @@\n \n import java.io.ByteArrayOutputStream;\n \n+import org.spearce.jgit.lib.Constants;\n import org.spearce.jgit.lib.ObjectId;\n import org.spearce.jgit.lib.PersonIdent;\n import org.spearce.jgit.lib.RepositoryTestCase;\n@@ -145,6 +146,7 @@ public void testParse_implicit_UTF8_encoded() throws Exception {\n \t\tc = new RevCommit(id(\"9473095c4cb2f12aefe1db8a355fe3fafba42f67\")); // bogus id\n \t\tc.parseCanonical(new RevWalk(db), b.toByteArray());\n \n+\t\tassertSame(Constants.CHARSET, c.getEncoding());\n \t\tassertEquals(\"F\\u00f6r fattare\", c.getAuthorIdent().getName());\n \t\tassertEquals(\"Sm\\u00f6rg\\u00e5sbord\", c.getShortMessage());\n \t\tassertEquals(\"Sm\\u00f6rg\\u00e5sbord\\n\\n\\u304d\\u308c\\u3044\\n\", c.getFullMessage());\n@@ -163,6 +165,7 @@ public void testParse_implicit_mixed_encoded() throws Exception {\n \t\tc = new RevCommit(id(\"9473095c4cb2f12aefe1db8a355fe3fafba42f67\")); // bogus id\n \t\tc.parseCanonical(new RevWalk(db), b.toByteArray());\n \n+\t\tassertSame(Constants.CHARSET, c.getEncoding());\n \t\tassertEquals(\"F\\u00f6r fattare\", c.getAuthorIdent().getName());\n \t\tassertEquals(\"Sm\\u00f6rg\\u00e5sbord\", c.getShortMessage());\n \t\tassertEquals(\"Sm\\u00f6rg\\u00e5sbord\\n\\n\\u304d\\u308c\\u3044\\n\", c.getFullMessage());\n@@ -187,6 +190,7 @@ public void testParse_explicit_encoded() throws Exception {\n \t\tc = new RevCommit(id(\"9473095c4cb2f12aefe1db8a355fe3fafba42f67\")); // bogus id\n \t\tc.parseCanonical(new RevWalk(db), b.toByteArray());\n \n+\t\tassertEquals(\"EUC-JP\", c.getEncoding().name());\n \t\tassertEquals(\"F\\u00f6r fattare\", c.getAuthorIdent().getName());\n \t\tassertEquals(\"\\u304d\\u308c\\u3044\", c.getShortMessage());\n \t\tassertEquals(\"\\u304d\\u308c\\u3044\\n\\nHi\\n\", c.getFullMessage());\n@@ -215,6 +219,7 @@ public void testParse_explicit_bad_encoded() throws Exception {\n \t\tc = new RevCommit(id(\"9473095c4cb2f12aefe1db8a355fe3fafba42f67\")); // bogus id\n \t\tc.parseCanonical(new RevWalk(db), b.toByteArray());\n \n+\t\tassertEquals(\"EUC-JP\", c.getEncoding().name());\n \t\tassertEquals(\"F\\u00f6r fattare\", c.getAuthorIdent().getName());\n \t\tassertEquals(\"\\u304d\\u308c\\u3044\", c.getShortMessage());\n \t\tassertEquals(\"\\u304d\\u308c\\u3044\\n\\nHi\\n\", c.getFullMessage());\n@@ -244,6 +249,7 @@ public void testParse_explicit_bad_encoded2() throws Exception {\n \t\tc = new RevCommit(id(\"9473095c4cb2f12aefe1db8a355fe3fafba42f67\")); // bogus id\n \t\tc.parseCanonical(new RevWalk(db), b.toByteArray());\n \n+\t\tassertEquals(\"ISO-8859-1\", c.getEncoding().name());\n \t\tassertEquals(\"F\\u00f6r fattare\", c.getAuthorIdent().getName());\n \t\tassertEquals(\"\\u304d\\u308c\\u3044\", c.getShortMessage());\n \t\tassertEquals(\"\\u304d\\u308c\\u3044\\n\\nHi\\n\", c.getFullMessage());\ndiff --git a/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java b/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java\nindex f211dfd..284a183 100644\n--- a/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java\n+++ b/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java\n@@ -1,4 +1,5 @@\n /*\n+ * Copyright (C) 2009, Google Inc.\n  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>\n  *\n  * All rights reserved.\n@@ -377,6 +378,21 @@ static boolean hasLF(final byte[] r, int b, final int e) {\n \t}\n \n \t/**\n+\t * Determine the encoding of the commit message buffer.\n+\t * <p>\n+\t * Locates the \"encoding\" header (if present) and then returns the proper\n+\t * character set to apply to this buffer to evaluate its contents as\n+\t * character data.\n+\t * <p>\n+\t * If no encoding header is present, {@link Constants#CHARSET} is assumed.\n+\t *\n+\t * @return the preferred encoding of {@link #getRawBuffer()}.\n+\t */\n+\tpublic final Charset getEncoding() {\n+\t\treturn RawParseUtils.parseEncoding(buffer);\n+\t}\n+\n+\t/**\n \t * Reset this commit to allow another RevWalk with the same instances.\n \t * <p>\n \t * Subclasses <b>must</b> call <code>super.reset()</code> to ensure the\n-- \n1.6.3.3.420.gd4b46\n"},{"id":"116844","messageId":"1245813109-29763-2-git-send-email-spearce@spearce.org","threadId":"19913","inReplyTo":"1245813109-29763-1-git-send-email-spearce@spearce.org","subject":"[JGIT PATCH 2/2] Add parsing support for Signed-off-by lines in commit messages","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2009-06-24T03:11:49Z","receivedAt":"2009-06-24T03:11:49Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"The RevCommit.getFooterLines() methods now give us direct access to\nthe Signed-off-by, Acked-by, etc. footer lines in a commit message\nbody, making it easier to process this information in code.\n\nThe value of a SOB line is treated as an opaque string value.  JGit\ndoes not attempt to break it down into a sort of micro PersonIdent,\neven though it is very typical for a SOB line to have a name and an\nemail address.  Since this is very free-form text that a human can\nmodify when creating a commit object we can't make nearly the same\nassumptions about uniformity of value as we can make when parsing\nthe machine generated commit headers.\n\nTo avoid unnecessary byte[] to String conversions we delay the\nconversion of a key or a value section of a footer line until the\nlast possible moment in the application code.  This way we don't\nspend time converting text for footer lines that the application\ndoesn't care to see.  We also can avoid allocating unnecessary\nString objects if the application won't care about that particular\nfooter line value.\n\nThe FooterKey object can be used to more efficiently filter out\nthe footer lines, by using an optimized byte equality test and\navoiding String conversions.  Standard instances for the common\nfooters like Signed-off-by are supplied as constants, making it\neasier for applications to read footers.\n\nSigned-off-by: Shawn O. Pearce <spearce@spearce.org>\n---\n\n I think this is ready to go as-is.  We really should start to look\n at how one might replace Commit with RevCommit for the purposes\n of *creating* a new commit.  Because tools like the FooterKey and\n FooterLine start to make it easier to add/remove the SOB from within\n a GUI editor a bit easier.  But that's all for some future date.\n\n .../org/spearce/jgit/revwalk/FooterLineTest.java   |  277 ++++++++++++++++++++\n .../src/org/spearce/jgit/revwalk/FooterKey.java    |   77 ++++++\n .../src/org/spearce/jgit/revwalk/FooterLine.java   |  121 +++++++++\n .../src/org/spearce/jgit/revwalk/RevCommit.java    |  102 +++++++\n .../src/org/spearce/jgit/util/RawParseUtils.java   |   44 +++\n 5 files changed, 621 insertions(+), 0 deletions(-)\n create mode 100644 org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/FooterLineTest.java\n create mode 100644 org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterKey.java\n create mode 100644 org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterLine.java\n\ndiff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/FooterLineTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/FooterLineTest.java\nnew file mode 100644\nindex 0000000..342346f\n--- /dev/null\n+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/FooterLineTest.java\n@@ -0,0 +1,277 @@\n+/*\n+ * Copyright (C) 2009, Google Inc.\n+ *\n+ * All rights reserved.\n+ *\n+ * Redistribution and use in source and binary forms, with or\n+ * without modification, are permitted provided that the following\n+ * conditions are met:\n+ *\n+ * - Redistributions of source code must retain the above copyright\n+ *   notice, this list of conditions and the following disclaimer.\n+ *\n+ * - Redistributions in binary form must reproduce the above\n+ *   copyright notice, this list of conditions and the following\n+ *   disclaimer in the documentation and/or other materials provided\n+ *   with the distribution.\n+ *\n+ * - Neither the name of the Git Development Community nor the\n+ *   names of its contributors may be used to endorse or promote\n+ *   products derived from this software without specific prior\n+ *   written permission.\n+ *\n+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND\n+ * CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\n+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\n+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n+ */\n+\n+package org.spearce.jgit.revwalk;\n+\n+import java.util.List;\n+\n+import org.spearce.jgit.lib.Constants;\n+import org.spearce.jgit.lib.ObjectId;\n+import org.spearce.jgit.lib.RepositoryTestCase;\n+\n+public class FooterLineTest extends RepositoryTestCase {\n+\tpublic void testNoFooters_EmptyBody() {\n+\t\tfinal RevCommit commit = parse(\"\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(0, footers.size());\n+\t}\n+\n+\tpublic void testNoFooters_NewlineOnlyBody1() {\n+\t\tfinal RevCommit commit = parse(\"\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(0, footers.size());\n+\t}\n+\n+\tpublic void testNoFooters_NewlineOnlyBody5() {\n+\t\tfinal RevCommit commit = parse(\"\\n\\n\\n\\n\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(0, footers.size());\n+\t}\n+\n+\tpublic void testNoFooters_OneLineBodyNoLF() {\n+\t\tfinal RevCommit commit = parse(\"this is a commit\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(0, footers.size());\n+\t}\n+\n+\tpublic void testNoFooters_OneLineBodyWithLF() {\n+\t\tfinal RevCommit commit = parse(\"this is a commit\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(0, footers.size());\n+\t}\n+\n+\tpublic void testNoFooters_ShortBodyNoLF() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(0, footers.size());\n+\t}\n+\n+\tpublic void testNoFooters_ShortBodyWithLF() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(0, footers.size());\n+\t}\n+\n+\tpublic void testSignedOffBy_OneUserNoLF() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\" + \"\\n\"\n+\t\t\t\t+ \"Signed-off-by: A. U. Thor <a@example.com>\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tFooterLine f;\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(1, footers.size());\n+\n+\t\tf = footers.get(0);\n+\t\tassertEquals(\"Signed-off-by\", f.getKey());\n+\t\tassertEquals(\"A. U. Thor <a@example.com>\", f.getValue());\n+\t}\n+\n+\tpublic void testSignedOffBy_OneUserWithLF() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\" + \"\\n\"\n+\t\t\t\t+ \"Signed-off-by: A. U. Thor <a@example.com>\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tFooterLine f;\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(1, footers.size());\n+\n+\t\tf = footers.get(0);\n+\t\tassertEquals(\"Signed-off-by\", f.getKey());\n+\t\tassertEquals(\"A. U. Thor <a@example.com>\", f.getValue());\n+\t}\n+\n+\tpublic void testSignedOffBy_IgnoreWhitespace() {\n+\t\t// We only ignore leading whitespace on the value, trailing\n+\t\t// is assumed part of the value.\n+\t\t//\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\" + \"\\n\"\n+\t\t\t\t+ \"Signed-off-by:   A. U. Thor <a@example.com>  \\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tFooterLine f;\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(1, footers.size());\n+\n+\t\tf = footers.get(0);\n+\t\tassertEquals(\"Signed-off-by\", f.getKey());\n+\t\tassertEquals(\"A. U. Thor <a@example.com>  \", f.getValue());\n+\t}\n+\n+\tpublic void testEmptyValueNoLF() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\" + \"\\n\"\n+\t\t\t\t+ \"Signed-off-by:\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tFooterLine f;\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(1, footers.size());\n+\n+\t\tf = footers.get(0);\n+\t\tassertEquals(\"Signed-off-by\", f.getKey());\n+\t\tassertEquals(\"\", f.getValue());\n+\t}\n+\n+\tpublic void testEmptyValueWithLF() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\" + \"\\n\"\n+\t\t\t\t+ \"Signed-off-by:\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tFooterLine f;\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(1, footers.size());\n+\n+\t\tf = footers.get(0);\n+\t\tassertEquals(\"Signed-off-by\", f.getKey());\n+\t\tassertEquals(\"\", f.getValue());\n+\t}\n+\n+\tpublic void testShortKey() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\" + \"\\n\"\n+\t\t\t\t+ \"K:V\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tFooterLine f;\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(1, footers.size());\n+\n+\t\tf = footers.get(0);\n+\t\tassertEquals(\"K\", f.getKey());\n+\t\tassertEquals(\"V\", f.getValue());\n+\t}\n+\n+\tpublic void testSignedOffBy_ManyUsers() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\"\n+\t\t\t\t+ \"Not-A-Footer-Line: this line must not be read as a footer\\n\"\n+\t\t\t\t+ \"\\n\" // paragraph break, now footers appear in final block\n+\t\t\t\t+ \"Signed-off-by: A. U. Thor <a@example.com>\\n\"\n+\t\t\t\t+ \"CC:            <some.mailing.list@example.com>\\n\"\n+\t\t\t\t+ \"Acked-by: Some Reviewer <sr@example.com>\\n\"\n+\t\t\t\t+ \"Signed-off-by: Main Tain Er <mte@example.com>\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tFooterLine f;\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(4, footers.size());\n+\n+\t\tf = footers.get(0);\n+\t\tassertEquals(\"Signed-off-by\", f.getKey());\n+\t\tassertEquals(\"A. U. Thor <a@example.com>\", f.getValue());\n+\n+\t\tf = footers.get(1);\n+\t\tassertEquals(\"CC\", f.getKey());\n+\t\tassertEquals(\"<some.mailing.list@example.com>\", f.getValue());\n+\n+\t\tf = footers.get(2);\n+\t\tassertEquals(\"Acked-by\", f.getKey());\n+\t\tassertEquals(\"Some Reviewer <sr@example.com>\", f.getValue());\n+\n+\t\tf = footers.get(3);\n+\t\tassertEquals(\"Signed-off-by\", f.getKey());\n+\t\tassertEquals(\"Main Tain Er <mte@example.com>\", f.getValue());\n+\t}\n+\n+\tpublic void testSignedOffBy_SkipNonFooter() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\"\n+\t\t\t\t+ \"Not-A-Footer-Line: this line must not be read as a footer\\n\"\n+\t\t\t\t+ \"\\n\" // paragraph break, now footers appear in final block\n+\t\t\t\t+ \"Signed-off-by: A. U. Thor <a@example.com>\\n\"\n+\t\t\t\t+ \"CC:            <some.mailing.list@example.com>\\n\"\n+\t\t\t\t+ \"not really a footer line but we'll skip it anyway\\n\"\n+\t\t\t\t+ \"Acked-by: Some Reviewer <sr@example.com>\\n\"\n+\t\t\t\t+ \"Signed-off-by: Main Tain Er <mte@example.com>\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tFooterLine f;\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(4, footers.size());\n+\n+\t\tf = footers.get(0);\n+\t\tassertEquals(\"Signed-off-by\", f.getKey());\n+\t\tassertEquals(\"A. U. Thor <a@example.com>\", f.getValue());\n+\n+\t\tf = footers.get(1);\n+\t\tassertEquals(\"CC\", f.getKey());\n+\t\tassertEquals(\"<some.mailing.list@example.com>\", f.getValue());\n+\n+\t\tf = footers.get(2);\n+\t\tassertEquals(\"Acked-by\", f.getKey());\n+\t\tassertEquals(\"Some Reviewer <sr@example.com>\", f.getValue());\n+\n+\t\tf = footers.get(3);\n+\t\tassertEquals(\"Signed-off-by\", f.getKey());\n+\t\tassertEquals(\"Main Tain Er <mte@example.com>\", f.getValue());\n+\t}\n+\n+\tpublic void testFilterFootersIgnoreCase() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\"\n+\t\t\t\t+ \"Not-A-Footer-Line: this line must not be read as a footer\\n\"\n+\t\t\t\t+ \"\\n\" // paragraph break, now footers appear in final block\n+\t\t\t\t+ \"Signed-Off-By: A. U. Thor <a@example.com>\\n\"\n+\t\t\t\t+ \"CC:            <some.mailing.list@example.com>\\n\"\n+\t\t\t\t+ \"Acked-by: Some Reviewer <sr@example.com>\\n\"\n+\t\t\t\t+ \"signed-off-by: Main Tain Er <mte@example.com>\\n\");\n+\t\tfinal List<String> footers = commit.getFooterLines(\"signed-off-by\");\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(2, footers.size());\n+\n+\t\tassertEquals(\"A. U. Thor <a@example.com>\", footers.get(0));\n+\t\tassertEquals(\"Main Tain Er <mte@example.com>\", footers.get(1));\n+\t}\n+\n+\tprivate RevCommit parse(final String msg) {\n+\t\tfinal StringBuilder buf = new StringBuilder();\n+\t\tbuf.append(\"tree \" + ObjectId.zeroId().name() + \"\\n\");\n+\t\tbuf.append(\"author A. U. Thor <a@example.com> 1 +0000\\n\");\n+\t\tbuf.append(\"committer A. U. Thor <a@example.com> 1 +0000\\n\");\n+\t\tbuf.append(\"\\n\");\n+\t\tbuf.append(msg);\n+\n+\t\tfinal RevWalk walk = new RevWalk(db);\n+\t\twalk.setRetainBody(true);\n+\t\tfinal RevCommit c = new RevCommit(ObjectId.zeroId());\n+\t\tc.parseCanonical(walk, Constants.encode(buf.toString()));\n+\t\treturn c;\n+\t}\n+}\ndiff --git a/org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterKey.java b/org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterKey.java\nnew file mode 100644\nindex 0000000..1e651ff\n--- /dev/null\n+++ b/org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterKey.java\n@@ -0,0 +1,77 @@\n+/*\n+ * Copyright (C) 2009, Google Inc.\n+ *\n+ * All rights reserved.\n+ *\n+ * Redistribution and use in source and binary forms, with or\n+ * without modification, are permitted provided that the following\n+ * conditions are met:\n+ *\n+ * - Redistributions of source code must retain the above copyright\n+ *   notice, this list of conditions and the following disclaimer.\n+ *\n+ * - Redistributions in binary form must reproduce the above\n+ *   copyright notice, this list of conditions and the following\n+ *   disclaimer in the documentation and/or other materials provided\n+ *   with the distribution.\n+ *\n+ * - Neither the name of the Git Development Community nor the\n+ *   names of its contributors may be used to endorse or promote\n+ *   products derived from this software without specific prior\n+ *   written permission.\n+ *\n+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND\n+ * CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\n+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\n+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n+ */\n+\n+package org.spearce.jgit.revwalk;\n+\n+import org.spearce.jgit.lib.Constants;\n+\n+/** Case insensitive key for a {@link FooterLine}. */\n+public final class FooterKey {\n+\t/** Standard {@code Signed-off-by} */\n+\tpublic static final FooterKey SIGNED_OFF_BY = new FooterKey(\"Signed-off-by\");\n+\n+\t/** Standard {@code Acked-by} */\n+\tpublic static final FooterKey ACKED_BY = new FooterKey(\"Acked-by\");\n+\n+\t/** Standard {@code CC} */\n+\tpublic static final FooterKey CC = new FooterKey(\"CC\");\n+\n+\tprivate final String name;\n+\n+\tfinal byte[] raw;\n+\n+\t/**\n+\t * Create a key for a specific footer line.\n+\t *\n+\t * @param keyName\n+\t *            name of the footer line.\n+\t */\n+\tpublic FooterKey(final String keyName) {\n+\t\tname = keyName;\n+\t\traw = Constants.encode(keyName.toLowerCase());\n+\t}\n+\n+\t/** @return name of this footer line. */\n+\tpublic String getName() {\n+\t\treturn name;\n+\t}\n+\n+\t@Override\n+\tpublic String toString() {\n+\t\treturn \"FooterKey[\" + name + \"]\";\n+\t}\n+}\ndiff --git a/org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterLine.java b/org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterLine.java\nnew file mode 100644\nindex 0000000..1fef47d\n--- /dev/null\n+++ b/org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterLine.java\n@@ -0,0 +1,121 @@\n+/*\n+ * Copyright (C) 2009, Google Inc.\n+ *\n+ * All rights reserved.\n+ *\n+ * Redistribution and use in source and binary forms, with or\n+ * without modification, are permitted provided that the following\n+ * conditions are met:\n+ *\n+ * - Redistributions of source code must retain the above copyright\n+ *   notice, this list of conditions and the following disclaimer.\n+ *\n+ * - Redistributions in binary form must reproduce the above\n+ *   copyright notice, this list of conditions and the following\n+ *   disclaimer in the documentation and/or other materials provided\n+ *   with the distribution.\n+ *\n+ * - Neither the name of the Git Development Community nor the\n+ *   names of its contributors may be used to endorse or promote\n+ *   products derived from this software without specific prior\n+ *   written permission.\n+ *\n+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND\n+ * CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\n+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF\n+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n+ */\n+\n+package org.spearce.jgit.revwalk;\n+\n+import java.nio.charset.Charset;\n+\n+import org.spearce.jgit.util.RawParseUtils;\n+\n+/**\n+ * Single line at the end of a message, such as a \"Signed-off-by: someone\".\n+ * <p>\n+ * These footer lines tend to be used to represent additional information about\n+ * a commit, like the path it followed through reviewers before finally being\n+ * accepted into the project's main repository as an immutable commit.\n+ *\n+ * @see RevCommit#getFooterLines()\n+ */\n+public final class FooterLine {\n+\tprivate final byte[] buffer;\n+\n+\tprivate final Charset enc;\n+\n+\tprivate final int keyStart;\n+\n+\tprivate final int keyEnd;\n+\n+\tprivate final int valStart;\n+\n+\tprivate final int valEnd;\n+\n+\tFooterLine(final byte[] b, final Charset e, final int ks, final int ke,\n+\t\t\tfinal int vs, final int ve) {\n+\t\tbuffer = b;\n+\t\tenc = e;\n+\t\tkeyStart = ks;\n+\t\tkeyEnd = ke;\n+\t\tvalStart = vs;\n+\t\tvalEnd = ve;\n+\t}\n+\n+\t/**\n+\t * @param key\n+\t *            key to test this line's key name against.\n+\t * @return true if {@code key.getName().equalsIgnorecase(getKey())}.\n+\t */\n+\tpublic boolean matches(final FooterKey key) {\n+\t\tfinal byte[] kRaw = key.raw;\n+\t\tfinal int len = kRaw.length;\n+\t\tint bPtr = keyStart;\n+\t\tif (keyEnd - bPtr != len)\n+\t\t\treturn false;\n+\t\tfor (int kPtr = 0; bPtr < len;) {\n+\t\t\tbyte b = buffer[bPtr++];\n+\t\t\tif ('A' <= b && b <= 'Z')\n+\t\t\t\tb += 'a' - 'A';\n+\t\t\tif (b != kRaw[kPtr++])\n+\t\t\t\treturn false;\n+\t\t}\n+\t\treturn true;\n+\t}\n+\n+\t/**\n+\t * @return key name of this footer; that is the text before the \":\" on the\n+\t *         line footer's line. The text is decoded according to the commit's\n+\t *         specified (or assumed) character encoding.\n+\t */\n+\tpublic String getKey() {\n+\t\treturn RawParseUtils.decode(enc, buffer, keyStart, keyEnd);\n+\t}\n+\n+\t/**\n+\t * @return value of this footer; that is the text after the \":\" and any\n+\t *         leading whitespace has been skipped. May be the empty string if\n+\t *         the footer has no value (line ended with \":\"). The text is\n+\t *         decoded according to the commit's specified (or assumed)\n+\t *         character encoding.\n+\t */\n+\tpublic String getValue() {\n+\t\treturn RawParseUtils.decode(enc, buffer, valStart, valEnd);\n+\t}\n+\n+\t@Override\n+\tpublic String toString() {\n+\t\treturn getKey() + \": \" + getValue();\n+\t}\n+}\ndiff --git a/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java b/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java\nindex 284a183..44515ec 100644\n--- a/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java\n+++ b/org.spearce.jgit/src/org/spearce/jgit/revwalk/RevCommit.java\n@@ -40,6 +40,9 @@\n \n import java.io.IOException;\n import java.nio.charset.Charset;\n+import java.util.ArrayList;\n+import java.util.Collections;\n+import java.util.List;\n \n import org.spearce.jgit.errors.IncorrectObjectTypeException;\n import org.spearce.jgit.errors.MissingObjectException;\n@@ -393,6 +396,105 @@ public final Charset getEncoding() {\n \t}\n \n \t/**\n+\t * Parse the footer lines (e.g. \"Signed-off-by\") for machine processing.\n+\t * <p>\n+\t * This method splits all of the footer lines out of the last paragraph of\n+\t * the commit message, providing each line as a key-value pair, ordered by\n+\t * the order of the line's appearance in the commit message itself.\n+\t * <p>\n+\t * A footer line's key must match the pattern {@code ^[A-Za-z0-9-]+:}, while\n+\t * the value is free-form, but must not contain an LF. Very common keys seen\n+\t * in the wild are:\n+\t * <ul>\n+\t * <li>{@code Signed-off-by} (agrees to Developer Certificate of Origin)\n+\t * <li>{@code Acked-by} (thinks change looks sane in context)\n+\t * <li>{@code Reported-by} (originally found the issue this change fixes)\n+\t * <li>{@code Tested-by} (validated change fixes the issue for them)\n+\t * <li>{@code CC}, {@code Cc} (copy on all email related to this change)\n+\t * <li>{@code Bug} (link to project's bug tracking system)\n+\t * </ul>\n+\t *\n+\t * @return ordered list of footer lines; empty list if no footers found.\n+\t */\n+\tpublic final List<FooterLine> getFooterLines() {\n+\t\tfinal byte[] raw = buffer;\n+\t\tint ptr = raw.length - 1;\n+\t\twhile (raw[ptr] == '\\n') // trim any trailing LFs, not interesting\n+\t\t\tptr--;\n+\n+\t\tfinal int msgB = RawParseUtils.commitMessage(raw, 0);\n+\t\tfinal ArrayList<FooterLine> r = new ArrayList<FooterLine>(4);\n+\t\tfinal Charset enc = getEncoding();\n+\t\tfor (;;) {\n+\t\t\tptr = RawParseUtils.prevLF(raw, ptr);\n+\t\t\tif (ptr <= msgB)\n+\t\t\t\tbreak; // Don't parse commit headers as footer lines.\n+\n+\t\t\tfinal int keyStart = ptr + 2;\n+\t\t\tif (raw[keyStart] == '\\n')\n+\t\t\t\tbreak; // Stop at first paragraph break, no footers above it.\n+\n+\t\t\tfinal int keyEnd = RawParseUtils.endOfFooterLineKey(raw, keyStart);\n+\t\t\tif (keyEnd < 0)\n+\t\t\t\tcontinue; // Not a well formed footer line, skip it.\n+\n+\t\t\t// Skip over the ': *' at the end of the key before the value.\n+\t\t\t//\n+\t\t\tint valStart = keyEnd + 1;\n+\t\t\twhile (valStart < raw.length && raw[valStart] == ' ')\n+\t\t\t\tvalStart++;\n+\n+\t\t\t// Value ends at the LF, and does not include it.\n+\t\t\t//\n+\t\t\tint valEnd = RawParseUtils.nextLF(raw, valStart);\n+\t\t\tif (raw[valEnd - 1] == '\\n')\n+\t\t\t\tvalEnd--;\n+\n+\t\t\tr.add(new FooterLine(raw, enc, keyStart, keyEnd, valStart, valEnd));\n+\t\t}\n+\t\tCollections.reverse(r);\n+\t\treturn r;\n+\t}\n+\n+\t/**\n+\t * Get the values of all footer lines with the given key.\n+\t *\n+\t * @param keyName\n+\t *            footer key to find values of, case insensitive.\n+\t * @return values of footers with key of {@code keyName}, ordered by their\n+\t *         order of appearance. Duplicates may be returned if the same\n+\t *         footer appeared more than once. Empty list if no footers appear\n+\t *         with the specified key, or there are no footers at all.\n+\t * @see #getFooterLines()\n+\t */\n+\tpublic final List<String> getFooterLines(final String keyName) {\n+\t\treturn getFooterLines(new FooterKey(keyName));\n+\t}\n+\n+\t/**\n+\t * Get the values of all footer lines with the given key.\n+\t *\n+\t * @param keyName\n+\t *            footer key to find values of, case insensitive.\n+\t * @return values of footers with key of {@code keyName}, ordered by their\n+\t *         order of appearance. Duplicates may be returned if the same\n+\t *         footer appeared more than once. Empty list if no footers appear\n+\t *         with the specified key, or there are no footers at all.\n+\t * @see #getFooterLines()\n+\t */\n+\tpublic final List<String> getFooterLines(final FooterKey keyName) {\n+\t\tfinal List<FooterLine> src = getFooterLines();\n+\t\tif (src.isEmpty())\n+\t\t\treturn Collections.emptyList();\n+\t\tfinal ArrayList<String> r = new ArrayList<String>(src.size());\n+\t\tfor (final FooterLine f : src) {\n+\t\t\tif (f.matches(keyName))\n+\t\t\t\tr.add(f.getValue());\n+\t\t}\n+\t\treturn r;\n+\t}\n+\n+\t/**\n \t * Reset this commit to allow another RevWalk with the same instances.\n \t * <p>\n \t * Subclasses <b>must</b> call <code>super.reset()</code> to ensure the\ndiff --git a/org.spearce.jgit/src/org/spearce/jgit/util/RawParseUtils.java b/org.spearce.jgit/src/org/spearce/jgit/util/RawParseUtils.java\nindex 0de7eb8..43c3250 100644\n--- a/org.spearce.jgit/src/org/spearce/jgit/util/RawParseUtils.java\n+++ b/org.spearce.jgit/src/org/spearce/jgit/util/RawParseUtils.java\n@@ -58,6 +58,8 @@\n \n \tprivate static final byte[] digits16;\n \n+\tprivate static final byte[] footerLineKeyChars;\n+\n \tstatic {\n \t\tdigits10 = new byte['9' + 1];\n \t\tArrays.fill(digits10, (byte) -1);\n@@ -72,6 +74,15 @@\n \t\t\tdigits16[i] = (byte) ((i - 'a') + 10);\n \t\tfor (char i = 'A'; i <= 'F'; i++)\n \t\t\tdigits16[i] = (byte) ((i - 'A') + 10);\n+\n+\t\tfooterLineKeyChars = new byte['z' + 1];\n+\t\tfooterLineKeyChars['-'] = 1;\n+\t\tfor (char i = '0'; i <= '9'; i++)\n+\t\t\tfooterLineKeyChars[i] = 1;\n+\t\tfor (char i = 'A'; i <= 'Z'; i++)\n+\t\t\tfooterLineKeyChars[i] = 1;\n+\t\tfor (char i = 'a'; i <= 'z'; i++)\n+\t\t\tfooterLineKeyChars[i] = 1;\n \t}\n \n \t/**\n@@ -713,6 +724,39 @@ public static PersonIdent parsePersonIdentOnly(final byte[] raw, final int nameB\n \t}\n \n \t/**\n+\t * Locate the end of a footer line key string.\n+\t * <p>\n+\t * If the region at {@code raw[ptr]} matches {@code ^[A-Za-z0-9-]+:} (e.g.\n+\t * \"Signed-off-by: A. U. Thor\\n\") then this method returns the position of\n+\t * the first ':'.\n+\t * <p>\n+\t * If the region at {@code raw[ptr]} does not match {@code ^[A-Za-z0-9-]+:}\n+\t * then this method returns -1.\n+\t *\n+\t * @param raw\n+\t *            buffer to scan.\n+\t * @param ptr\n+\t *            first position within raw to consider as a footer line key.\n+\t * @return position of the ':' which terminates the footer line key if this\n+\t *         is otherwise a valid footer line key; otherwise -1.\n+\t */\n+\tpublic static int endOfFooterLineKey(final byte[] raw, int ptr) {\n+\t\ttry {\n+\t\t\tfor (;;) {\n+\t\t\t\tfinal byte c = raw[ptr];\n+\t\t\t\tif (footerLineKeyChars[c] == 0) {\n+\t\t\t\t\tif (c == ':')\n+\t\t\t\t\t\treturn ptr;\n+\t\t\t\t\treturn -1;\n+\t\t\t\t}\n+\t\t\t\tptr++;\n+\t\t\t}\n+\t\t} catch (ArrayIndexOutOfBoundsException e) {\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n+\t/**\n \t * Decode a buffer under UTF-8, if possible.\n \t *\n \t * If the byte stream cannot be decoded that way, the platform default is tried\n-- \n1.6.3.3.420.gd4b46\n"},{"id":"116872","messageId":"20090624173029.GF11191@spearce.org","threadId":"19913","inReplyTo":"1245813109-29763-2-git-send-email-spearce@spearce.org","subject":"[JGIT PATCH 3/2] Support extracting emails from Signed-off-by lines","fromName":"Shawn O. Pearce","fromEmail":"spearce@spearce.org","sentAt":"2009-06-24T17:30:29Z","receivedAt":"2009-06-24T17:30:29Z","isPatch":true,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"A typical Signed-off-by footer line can be seen below in this\nmessage, and it clearly contains an email address (or at least\na user@domain sort of reference to the identity).  This identity\nmay need to be parsed out of a footer line by application code,\nto match the Signed-off-by line back to the author or the committer\nof the commit, or to some external datastore.\n\nSigned-off-by: Shawn O. Pearce <spearce@spearce.org>\n---\n .../org/spearce/jgit/revwalk/FooterLineTest.java   |   40 ++++++++++++++++++++\n .../src/org/spearce/jgit/revwalk/FooterLine.java   |   24 ++++++++++++\n 2 files changed, 64 insertions(+), 0 deletions(-)\n\ndiff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/FooterLineTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/FooterLineTest.java\nindex 342346f..431c170 100644\n--- a/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/FooterLineTest.java\n+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/revwalk/FooterLineTest.java\n@@ -105,6 +105,7 @@ public void testSignedOffBy_OneUserNoLF() {\n \t\tf = footers.get(0);\n \t\tassertEquals(\"Signed-off-by\", f.getKey());\n \t\tassertEquals(\"A. U. Thor <a@example.com>\", f.getValue());\n+\t\tassertEquals(\"a@example.com\", f.getEmailAddress());\n \t}\n \n \tpublic void testSignedOffBy_OneUserWithLF() {\n@@ -119,6 +120,7 @@ public void testSignedOffBy_OneUserWithLF() {\n \t\tf = footers.get(0);\n \t\tassertEquals(\"Signed-off-by\", f.getKey());\n \t\tassertEquals(\"A. U. Thor <a@example.com>\", f.getValue());\n+\t\tassertEquals(\"a@example.com\", f.getEmailAddress());\n \t}\n \n \tpublic void testSignedOffBy_IgnoreWhitespace() {\n@@ -136,6 +138,7 @@ public void testSignedOffBy_IgnoreWhitespace() {\n \t\tf = footers.get(0);\n \t\tassertEquals(\"Signed-off-by\", f.getKey());\n \t\tassertEquals(\"A. U. Thor <a@example.com>  \", f.getValue());\n+\t\tassertEquals(\"a@example.com\", f.getEmailAddress());\n \t}\n \n \tpublic void testEmptyValueNoLF() {\n@@ -150,6 +153,7 @@ public void testEmptyValueNoLF() {\n \t\tf = footers.get(0);\n \t\tassertEquals(\"Signed-off-by\", f.getKey());\n \t\tassertEquals(\"\", f.getValue());\n+\t\tassertNull(f.getEmailAddress());\n \t}\n \n \tpublic void testEmptyValueWithLF() {\n@@ -164,6 +168,7 @@ public void testEmptyValueWithLF() {\n \t\tf = footers.get(0);\n \t\tassertEquals(\"Signed-off-by\", f.getKey());\n \t\tassertEquals(\"\", f.getValue());\n+\t\tassertNull(f.getEmailAddress());\n \t}\n \n \tpublic void testShortKey() {\n@@ -178,6 +183,37 @@ public void testShortKey() {\n \t\tf = footers.get(0);\n \t\tassertEquals(\"K\", f.getKey());\n \t\tassertEquals(\"V\", f.getValue());\n+\t\tassertNull(f.getEmailAddress());\n+\t}\n+\n+\tpublic void testNonDelimtedEmail() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\" + \"\\n\"\n+\t\t\t\t+ \"Acked-by: re@example.com\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tFooterLine f;\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(1, footers.size());\n+\n+\t\tf = footers.get(0);\n+\t\tassertEquals(\"Acked-by\", f.getKey());\n+\t\tassertEquals(\"re@example.com\", f.getValue());\n+\t\tassertEquals(\"re@example.com\", f.getEmailAddress());\n+\t}\n+\n+\tpublic void testNotEmail() {\n+\t\tfinal RevCommit commit = parse(\"subject\\n\\nbody of commit\\n\" + \"\\n\"\n+\t\t\t\t+ \"Acked-by: Main Tain Er\\n\");\n+\t\tfinal List<FooterLine> footers = commit.getFooterLines();\n+\t\tFooterLine f;\n+\n+\t\tassertNotNull(footers);\n+\t\tassertEquals(1, footers.size());\n+\n+\t\tf = footers.get(0);\n+\t\tassertEquals(\"Acked-by\", f.getKey());\n+\t\tassertEquals(\"Main Tain Er\", f.getValue());\n+\t\tassertNull(f.getEmailAddress());\n \t}\n \n \tpublic void testSignedOffBy_ManyUsers() {\n@@ -197,18 +233,22 @@ public void testSignedOffBy_ManyUsers() {\n \t\tf = footers.get(0);\n \t\tassertEquals(\"Signed-off-by\", f.getKey());\n \t\tassertEquals(\"A. U. Thor <a@example.com>\", f.getValue());\n+\t\tassertEquals(\"a@example.com\", f.getEmailAddress());\n \n \t\tf = footers.get(1);\n \t\tassertEquals(\"CC\", f.getKey());\n \t\tassertEquals(\"<some.mailing.list@example.com>\", f.getValue());\n+\t\tassertEquals(\"some.mailing.list@example.com\", f.getEmailAddress());\n \n \t\tf = footers.get(2);\n \t\tassertEquals(\"Acked-by\", f.getKey());\n \t\tassertEquals(\"Some Reviewer <sr@example.com>\", f.getValue());\n+\t\tassertEquals(\"sr@example.com\", f.getEmailAddress());\n \n \t\tf = footers.get(3);\n \t\tassertEquals(\"Signed-off-by\", f.getKey());\n \t\tassertEquals(\"Main Tain Er <mte@example.com>\", f.getValue());\n+\t\tassertEquals(\"mte@example.com\", f.getEmailAddress());\n \t}\n \n \tpublic void testSignedOffBy_SkipNonFooter() {\ndiff --git a/org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterLine.java b/org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterLine.java\nindex 1fef47d..b28f52d 100644\n--- a/org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterLine.java\n+++ b/org.spearce.jgit/src/org/spearce/jgit/revwalk/FooterLine.java\n@@ -114,6 +114,30 @@ public String getValue() {\n \t\treturn RawParseUtils.decode(enc, buffer, valStart, valEnd);\n \t}\n \n+\t/**\n+\t * Extract the email address (if present) from the footer.\n+\t * <p>\n+\t * If there is an email address looking string inside of angle brackets\n+\t * (e.g. \"<a@b>\"), the return value is the part extracted from inside the\n+\t * brackets. If no brackets are found, then {@link #getValue()} is returned\n+\t * if the value contains an '@' sign. Otherwise, null.\n+\t *\n+\t * @return email address appearing in the value of this footer, or null.\n+\t */\n+\tpublic String getEmailAddress() {\n+\t\tfinal int lt = RawParseUtils.nextLF(buffer, valStart, '<');\n+\t\tif (valEnd <= lt) {\n+\t\t\tfinal int at = RawParseUtils.nextLF(buffer, valStart, '@');\n+\t\t\tif (valStart < at && at < valEnd)\n+\t\t\t\treturn getValue();\n+\t\t\treturn null;\n+\t\t}\n+\t\tfinal int gt = RawParseUtils.nextLF(buffer, lt, '>');\n+\t\tif (valEnd < gt)\n+\t\t\treturn null;\n+\t\treturn RawParseUtils.decode(enc, buffer, lt, gt - 1);\n+\t}\n+\n \t@Override\n \tpublic String toString() {\n \t\treturn getKey() + \": \" + getValue();\n-- \n1.6.3.3.420.gd4b46\n"}]}