* [JGIT PATCH 0/5] Patch parsing API
@ 2008-12-11 4:58 Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 1/5] Add toByteArray() to TemporaryBuffer Shawn O. Pearce
0 siblings, 1 reply; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 4:58 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
This is an API to parse a Git style patch file and extract the
critical metadata from the header lines, including the hunk headers
and what lines they correspond to in the pre and post image files.
It requires the two other series I already sent out today for
QuotedString and AbbreviatedObjectId.
There's TODO markers left where we still need to insert code to
create some sort of warning object, and then hang the warnings off
the Patch class. Given the size of the code I'm inclined to do that
as yet an additional patch, rather than squash it into this series.
My short-term roadmap related to this part of JGit:
* Compute and collect warnings from malformed git-style patches
* Correctly parse "git diff --cc" style output
* Get it into egit.git
I'm stopping development once I have the diff --cc output parsing
correctly. My rationale is right now I need the patch metadata
parsing in Gerrit 2, so that's what I'm teaching JGit to do. Maybe
later in the month or early next I'll add patch application support,
because I also want that in Gerrit 2. Patch application is not
currently a blocking item for me; but reading the patch metadata is.
Traditional patch support is really stubbed out too; there's a very
small subset of traditional (non-git) style patches this code can
scan the metadata from, but no tests to verify it. Gerrit 2 gets
all of its data from a "git diff" process, so I only need support
for git diffs right now. Yes, I'd like to add traditional patch
support too, but it won't be until later in 2009 that I would even
think about working on that myself.
Shawn O. Pearce (5):
Add toByteArray() to TemporaryBuffer
Add copy(InputStream) to TemporaryBuffer
Define FileHeader to parse the header block of a git diff
Define Patch to parse a sequence of patch FileHeaders
Add HunkHeader to represent a single hunk of a file within a patch
.../tst/org/spearce/jgit/patch/FileHeaderTest.java | 395 ++++++++++++++++
.../tst/org/spearce/jgit/patch/PatchTest.java | 155 +++++++
.../patch/testParse_ConfigCaseInsensitive.patch | 67 +++
.../src/org/spearce/jgit/patch/FileHeader.java | 480 ++++++++++++++++++++
.../src/org/spearce/jgit/patch/HunkHeader.java | 185 ++++++++
.../src/org/spearce/jgit/patch/Patch.java | 267 +++++++++++
.../src/org/spearce/jgit/util/TemporaryBuffer.java | 51 ++
7 files changed, 1600 insertions(+), 0 deletions(-)
create mode 100644 org.spearce.jgit.test/tst/org/spearce/jgit/patch/FileHeaderTest.java
create mode 100644 org.spearce.jgit.test/tst/org/spearce/jgit/patch/PatchTest.java
create mode 100644 org.spearce.jgit.test/tst/org/spearce/jgit/patch/testParse_ConfigCaseInsensitive.patch
create mode 100644 org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java
create mode 100644 org.spearce.jgit/src/org/spearce/jgit/patch/HunkHeader.java
create mode 100644 org.spearce.jgit/src/org/spearce/jgit/patch/Patch.java
^ permalink raw reply [flat|nested] 15+ messages in thread
* [JGIT PATCH 1/5] Add toByteArray() to TemporaryBuffer
2008-12-11 4:58 [JGIT PATCH 0/5] Patch parsing API Shawn O. Pearce
@ 2008-12-11 4:58 ` Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 2/5] Add copy(InputStream) " Shawn O. Pearce
0 siblings, 1 reply; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 4:58 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
It can be more useful to convert a buffered output stream into
a single byte array, without paying the penalties associated
with ByteArrayOutputStream to do the same action.
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
---
.../src/org/spearce/jgit/util/TemporaryBuffer.java | 34 ++++++++++++++++++++
1 files changed, 34 insertions(+), 0 deletions(-)
diff --git a/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java b/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
index d597c38..b1ffd6e 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
@@ -182,6 +182,40 @@ public long length() {
}
/**
+ * Convert this buffer's contents into a contiguous byte array.
+ * <p>
+ * The buffer is only complete after {@link #close()} has been invoked.
+ *
+ * @return the complete byte array; length matches {@link #length()}.
+ * @throws IOException
+ * an error occurred reading from a local temporary file
+ * @throws OutOfMemoryError
+ * the buffer cannot fit in memory
+ */
+ public byte[] toByteArray() throws IOException {
+ final long len = length();
+ if (Integer.MAX_VALUE < len)
+ throw new OutOfMemoryError("Length exceeds maximum array size");
+
+ final byte[] out = new byte[(int) len];
+ if (blocks != null) {
+ int outPtr = 0;
+ for (final Block b : blocks) {
+ System.arraycopy(b.buffer, 0, out, outPtr, b.count);
+ outPtr += b.count;
+ }
+ } else {
+ final FileInputStream in = new FileInputStream(onDiskFile);
+ try {
+ NB.readFully(in, out, 0, (int) len);
+ } finally {
+ in.close();
+ }
+ }
+ return out;
+ }
+
+ /**
* Send this buffer to an output stream.
* <p>
* This method may only be invoked after {@link #close()} has completed
--
1.6.1.rc2.299.gead4c
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [JGIT PATCH 2/5] Add copy(InputStream) to TemporaryBuffer
2008-12-11 4:58 ` [JGIT PATCH 1/5] Add toByteArray() to TemporaryBuffer Shawn O. Pearce
@ 2008-12-11 4:58 ` Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 3/5] Define FileHeader to parse the header block of a git diff Shawn O. Pearce
2008-12-11 15:40 ` [JGIT PATCH 2/5] Add copy(InputStream) to TemporaryBuffer Robin Rosenberg
0 siblings, 2 replies; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 4:58 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
In some places we may find it ourselves with an InputStream we
need to copy into a TemporaryBuffer, so we can flatten out the
entire stream to a single byte[]. Putting the copy loop here
is more useful then duplicating it in application level code.
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
---
.../src/org/spearce/jgit/util/TemporaryBuffer.java | 17 +++++++++++++++++
1 files changed, 17 insertions(+), 0 deletions(-)
diff --git a/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java b/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
index b1ffd6e..8f91246 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
@@ -42,6 +42,7 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
@@ -135,6 +136,22 @@ public void write(final byte[] b, int off, int len) throws IOException {
diskOut.write(b, off, len);
}
+ /**
+ * Copy all bytes remaining on the input stream into this buffer.
+ *
+ * @param in
+ * the stream to read from, until EOF is reached.
+ * @throws IOException
+ * an error occurred reading from the input stream, or while
+ * writing to a local temporary file.
+ */
+ public void copy(final InputStream in) throws IOException {
+ final byte[] b = new byte[2048];
+ int n;
+ while ((n = in.read(b)) > 0)
+ write(b, 0, n);
+ }
+
private Block last() {
return blocks.get(blocks.size() - 1);
}
--
1.6.1.rc2.299.gead4c
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [JGIT PATCH 3/5] Define FileHeader to parse the header block of a git diff
2008-12-11 4:58 ` [JGIT PATCH 2/5] Add copy(InputStream) " Shawn O. Pearce
@ 2008-12-11 4:58 ` Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders Shawn O. Pearce
2008-12-11 15:40 ` [JGIT PATCH 2/5] Add copy(InputStream) to TemporaryBuffer Robin Rosenberg
1 sibling, 1 reply; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 4:58 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
This class parses the top header lines of a git style diff, such as:
diff --git a/SUBMITTING_PATCHES b/Q
similarity index 100%
copy from SUBMITTING_PATCHES
copy to Q
or:
diff --git a/Q b/Q
new file mode 100644
index 0000000..e4a135e
--- /dev/null
+++ b/Q
and makes the information available in an object form. Unit tests
cover the different styles of headers that are commonly created by
C git, including both rename formats.
The hunk header information is not handled by this class, and it
does not have a public API. It is my intention to wrap this into
a larger container class that handles multiple FileHeaders at once,
with the base case of course being a single FileHeader describing
a patch that impacts only one file.
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
---
| 391 ++++++++++++++++++
| 430 ++++++++++++++++++++
2 files changed, 821 insertions(+), 0 deletions(-)
create mode 100644 org.spearce.jgit.test/tst/org/spearce/jgit/patch/FileHeaderTest.java
create mode 100644 org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java
--git a/org.spearce.jgit.test/tst/org/spearce/jgit/patch/FileHeaderTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/patch/FileHeaderTest.java
new file mode 100644
index 0000000..1d87bc0
--- /dev/null
+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/patch/FileHeaderTest.java
@@ -0,0 +1,391 @@
+/*
+ * 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.patch;
+
+import junit.framework.TestCase;
+
+import org.spearce.jgit.lib.Constants;
+import org.spearce.jgit.lib.FileMode;
+import org.spearce.jgit.lib.ObjectId;
+
+public class FileHeaderTest extends TestCase {
+ public void testParseGitFileName_Empty() {
+ assertEquals(-1, data("").parseGitFileName(0));
+ }
+
+ public void testParseGitFileName_NoLF() {
+ assertEquals(-1, data("a/ b/").parseGitFileName(0));
+ }
+
+ public void testParseGitFileName_NoSecondLine() {
+ assertEquals(-1, data("\n").parseGitFileName(0));
+ }
+
+ public void testParseGitFileName_EmptyHeader() {
+ assertEquals(1, data("\n\n").parseGitFileName(0));
+ }
+
+ public void testParseGitFileName_Foo() {
+ final String name = "foo";
+ final FileHeader fh = header(name);
+ assertEquals(gitLine(name).length(), fh.parseGitFileName(0));
+ assertEquals(name, fh.getOldName());
+ assertSame(fh.getOldName(), fh.getNewName());
+ }
+
+ public void testParseGitFileName_FailFooBar() {
+ final FileHeader fh = data("a/foo b/bar\n-");
+ assertTrue(fh.parseGitFileName(0) > 0);
+ assertNull(fh.getOldName());
+ assertNull(fh.getNewName());
+ }
+
+ public void testParseGitFileName_FooSpBar() {
+ final String name = "foo bar";
+ final FileHeader fh = header(name);
+ assertEquals(gitLine(name).length(), fh.parseGitFileName(0));
+ assertEquals(name, fh.getOldName());
+ assertSame(fh.getOldName(), fh.getNewName());
+ }
+
+ public void testParseGitFileName_DqFooTabBar() {
+ final String name = "foo\tbar";
+ final String dqName = "foo\\tbar";
+ final FileHeader fh = dqHeader(dqName);
+ assertEquals(dqGitLine(dqName).length(), fh.parseGitFileName(0));
+ assertEquals(name, fh.getOldName());
+ assertSame(fh.getOldName(), fh.getNewName());
+ }
+
+ public void testParseGitFileName_DqFooSpLfNulBar() {
+ final String name = "foo \n\0bar";
+ final String dqName = "foo \\n\\0bar";
+ final FileHeader fh = dqHeader(dqName);
+ assertEquals(dqGitLine(dqName).length(), fh.parseGitFileName(0));
+ assertEquals(name, fh.getOldName());
+ assertSame(fh.getOldName(), fh.getNewName());
+ }
+
+ public void testParseGitFileName_SrcFooC() {
+ final String name = "src/foo/bar/argh/code.c";
+ final FileHeader fh = header(name);
+ assertEquals(gitLine(name).length(), fh.parseGitFileName(0));
+ assertEquals(name, fh.getOldName());
+ assertSame(fh.getOldName(), fh.getNewName());
+ }
+
+ public void testParseGitFileName_SrcFooCNonStandardPrefix() {
+ final String name = "src/foo/bar/argh/code.c";
+ final String header = "project-v-1.0/" + name + " mydev/" + name + "\n";
+ final FileHeader fh = data(header + "-");
+ assertEquals(header.length(), fh.parseGitFileName(0));
+ assertEquals(name, fh.getOldName());
+ assertSame(fh.getOldName(), fh.getNewName());
+ }
+
+ public void testParseUnicodeName_NewFile() {
+ final FileHeader fh = data("diff --git \"a/\\303\\205ngstr\\303\\266m\" \"b/\\303\\205ngstr\\303\\266m\"\n"
+ + "new file mode 100644\n"
+ + "index 0000000..7898192\n"
+ + "--- /dev/null\n"
+ + "+++ \"b/\\303\\205ngstr\\303\\266m\"\n"
+ + "@@ -0,0 +1 @@\n" + "+a\n");
+ assertParse(fh);
+
+ assertEquals("/dev/null", fh.getOldName());
+ assertSame(FileHeader.DEV_NULL, fh.getOldName());
+ assertEquals("\u00c5ngstr\u00f6m", fh.getNewName());
+
+ assertSame(FileHeader.ChangeType.ADD, fh.getChangeType());
+
+ assertNull(fh.getOldMode());
+ assertSame(FileMode.REGULAR_FILE, fh.getNewMode());
+
+ assertEquals("0000000", fh.getOldId().name());
+ assertEquals("7898192", fh.getNewId().name());
+ assertEquals(0, fh.getScore());
+ }
+
+ public void testParseUnicodeName_DeleteFile() {
+ final FileHeader fh = data("diff --git \"a/\\303\\205ngstr\\303\\266m\" \"b/\\303\\205ngstr\\303\\266m\"\n"
+ + "deleted file mode 100644\n"
+ + "index 7898192..0000000\n"
+ + "--- \"a/\\303\\205ngstr\\303\\266m\"\n"
+ + "+++ /dev/null\n"
+ + "@@ -1 +0,0 @@\n" + "-a\n");
+ assertParse(fh);
+
+ assertEquals("\u00c5ngstr\u00f6m", fh.getOldName());
+ assertEquals("/dev/null", fh.getNewName());
+ assertSame(FileHeader.DEV_NULL, fh.getNewName());
+
+ assertSame(FileHeader.ChangeType.DELETE, fh.getChangeType());
+
+ assertSame(FileMode.REGULAR_FILE, fh.getOldMode());
+ assertNull(fh.getNewMode());
+
+ assertEquals("7898192", fh.getOldId().name());
+ assertEquals("0000000", fh.getNewId().name());
+ assertEquals(0, fh.getScore());
+ }
+
+ public void testParseModeChange() {
+ final FileHeader fh = data("diff --git a/a b b/a b\n"
+ + "old mode 100644\n" + "new mode 100755\n");
+ assertParse(fh);
+ assertEquals("a b", fh.getOldName());
+ assertEquals("a b", fh.getNewName());
+
+ assertSame(FileHeader.ChangeType.MODIFY, fh.getChangeType());
+
+ assertNull(fh.getOldId());
+ assertNull(fh.getNewId());
+
+ assertSame(FileMode.REGULAR_FILE, fh.getOldMode());
+ assertSame(FileMode.EXECUTABLE_FILE, fh.getNewMode());
+ assertEquals(0, fh.getScore());
+ }
+
+ public void testParseRename100_NewStyle() {
+ final FileHeader fh = data("diff --git a/a b/ c/\\303\\205ngstr\\303\\266m\n"
+ + "similarity index 100%\n"
+ + "rename from a\n"
+ + "rename to \" c/\\303\\205ngstr\\303\\266m\"\n");
+ int ptr = fh.parseGitFileName(0);
+ assertTrue(ptr > 0);
+ assertNull(fh.getOldName()); // can't parse names on a rename
+ assertNull(fh.getNewName());
+
+ ptr = fh.parseGitHeaders(ptr);
+ assertTrue(ptr > 0);
+
+ assertEquals("a", fh.getOldName());
+ assertEquals(" c/\u00c5ngstr\u00f6m", fh.getNewName());
+
+ assertSame(FileHeader.ChangeType.RENAME, fh.getChangeType());
+
+ assertNull(fh.getOldId());
+ assertNull(fh.getNewId());
+
+ assertNull(fh.getOldMode());
+ assertNull(fh.getNewMode());
+
+ assertEquals(100, fh.getScore());
+ }
+
+ public void testParseRename100_OldStyle() {
+ final FileHeader fh = data("diff --git a/a b/ c/\\303\\205ngstr\\303\\266m\n"
+ + "similarity index 100%\n"
+ + "rename old a\n"
+ + "rename new \" c/\\303\\205ngstr\\303\\266m\"\n");
+ int ptr = fh.parseGitFileName(0);
+ assertTrue(ptr > 0);
+ assertNull(fh.getOldName()); // can't parse names on a rename
+ assertNull(fh.getNewName());
+
+ ptr = fh.parseGitHeaders(ptr);
+ assertTrue(ptr > 0);
+
+ assertEquals("a", fh.getOldName());
+ assertEquals(" c/\u00c5ngstr\u00f6m", fh.getNewName());
+
+ assertSame(FileHeader.ChangeType.RENAME, fh.getChangeType());
+
+ assertNull(fh.getOldId());
+ assertNull(fh.getNewId());
+
+ assertNull(fh.getOldMode());
+ assertNull(fh.getNewMode());
+
+ assertEquals(100, fh.getScore());
+ }
+ public void testParseCopy100() {
+ final FileHeader fh = data("diff --git a/a b/ c/\\303\\205ngstr\\303\\266m\n"
+ + "similarity index 100%\n"
+ + "copy from a\n"
+ + "copy to \" c/\\303\\205ngstr\\303\\266m\"\n");
+ int ptr = fh.parseGitFileName(0);
+ assertTrue(ptr > 0);
+ assertNull(fh.getOldName()); // can't parse names on a copy
+ assertNull(fh.getNewName());
+
+ ptr = fh.parseGitHeaders(ptr);
+ assertTrue(ptr > 0);
+
+ assertEquals("a", fh.getOldName());
+ assertEquals(" c/\u00c5ngstr\u00f6m", fh.getNewName());
+
+ assertSame(FileHeader.ChangeType.COPY, fh.getChangeType());
+
+ assertNull(fh.getOldId());
+ assertNull(fh.getNewId());
+
+ assertNull(fh.getOldMode());
+ assertNull(fh.getNewMode());
+
+ assertEquals(100, fh.getScore());
+ }
+
+ public void testParseFullIndexLine_WithMode() {
+ final String oid = "78981922613b2afb6025042ff6bd878ac1994e85";
+ final String nid = "61780798228d17af2d34fce4cfbdf35556832472";
+ final FileHeader fh = data("diff --git a/a b/a\n" + "index " + oid
+ + ".." + nid + " 100644\n" + "--- a/a\n" + "+++ b/a\n");
+ assertParse(fh);
+
+ assertEquals("a", fh.getOldName());
+ assertEquals("a", fh.getNewName());
+
+ assertSame(FileMode.REGULAR_FILE, fh.getOldMode());
+ assertSame(FileMode.REGULAR_FILE, fh.getNewMode());
+
+ assertNotNull(fh.getOldId());
+ assertNotNull(fh.getNewId());
+
+ assertTrue(fh.getOldId().isComplete());
+ assertTrue(fh.getNewId().isComplete());
+
+ assertEquals(ObjectId.fromString(oid), fh.getOldId().toObjectId());
+ assertEquals(ObjectId.fromString(nid), fh.getNewId().toObjectId());
+ }
+
+ public void testParseFullIndexLine_NoMode() {
+ final String oid = "78981922613b2afb6025042ff6bd878ac1994e85";
+ final String nid = "61780798228d17af2d34fce4cfbdf35556832472";
+ final FileHeader fh = data("diff --git a/a b/a\n" + "index " + oid
+ + ".." + nid + "\n" + "--- a/a\n" + "+++ b/a\n");
+ assertParse(fh);
+
+ assertEquals("a", fh.getOldName());
+ assertEquals("a", fh.getNewName());
+
+ assertNull(fh.getOldMode());
+ assertNull(fh.getNewMode());
+
+ assertNotNull(fh.getOldId());
+ assertNotNull(fh.getNewId());
+
+ assertTrue(fh.getOldId().isComplete());
+ assertTrue(fh.getNewId().isComplete());
+
+ assertEquals(ObjectId.fromString(oid), fh.getOldId().toObjectId());
+ assertEquals(ObjectId.fromString(nid), fh.getNewId().toObjectId());
+ }
+
+ public void testParseAbbrIndexLine_WithMode() {
+ final int a = 7;
+ final String oid = "78981922613b2afb6025042ff6bd878ac1994e85";
+ final String nid = "61780798228d17af2d34fce4cfbdf35556832472";
+ final FileHeader fh = data("diff --git a/a b/a\n" + "index "
+ + oid.substring(0, a - 1) + ".." + nid.substring(0, a - 1)
+ + " 100644\n" + "--- a/a\n" + "+++ b/a\n");
+ assertParse(fh);
+
+ assertEquals("a", fh.getOldName());
+ assertEquals("a", fh.getNewName());
+
+ assertSame(FileMode.REGULAR_FILE, fh.getOldMode());
+ assertSame(FileMode.REGULAR_FILE, fh.getNewMode());
+
+ assertNotNull(fh.getOldId());
+ assertNotNull(fh.getNewId());
+
+ assertFalse(fh.getOldId().isComplete());
+ assertFalse(fh.getNewId().isComplete());
+
+ assertEquals(oid.substring(0, a - 1), fh.getOldId().name());
+ assertEquals(nid.substring(0, a - 1), fh.getNewId().name());
+
+ assertTrue(ObjectId.fromString(oid).startsWith(fh.getOldId()));
+ assertTrue(ObjectId.fromString(nid).startsWith(fh.getNewId()));
+ }
+
+ public void testParseAbbrIndexLine_NoMode() {
+ final int a = 7;
+ final String oid = "78981922613b2afb6025042ff6bd878ac1994e85";
+ final String nid = "61780798228d17af2d34fce4cfbdf35556832472";
+ final FileHeader fh = data("diff --git a/a b/a\n" + "index "
+ + oid.substring(0, a - 1) + ".." + nid.substring(0, a - 1)
+ + "\n" + "--- a/a\n" + "+++ b/a\n");
+ assertParse(fh);
+
+ assertEquals("a", fh.getOldName());
+ assertEquals("a", fh.getNewName());
+
+ assertNull(fh.getOldMode());
+ assertNull(fh.getNewMode());
+
+ assertNotNull(fh.getOldId());
+ assertNotNull(fh.getNewId());
+
+ assertFalse(fh.getOldId().isComplete());
+ assertFalse(fh.getNewId().isComplete());
+
+ assertEquals(oid.substring(0, a - 1), fh.getOldId().name());
+ assertEquals(nid.substring(0, a - 1), fh.getNewId().name());
+
+ assertTrue(ObjectId.fromString(oid).startsWith(fh.getOldId()));
+ assertTrue(ObjectId.fromString(nid).startsWith(fh.getNewId()));
+ }
+
+ private static void assertParse(final FileHeader fh) {
+ int ptr = fh.parseGitFileName(0);
+ assertTrue(ptr > 0);
+ ptr = fh.parseGitHeaders(ptr);
+ assertTrue(ptr > 0);
+ }
+
+ private static FileHeader data(final String in) {
+ return new FileHeader(Constants.encodeASCII(in), 0);
+ }
+
+ private static FileHeader header(final String path) {
+ return data(gitLine(path) + "--- " + path + "\n");
+ }
+
+ private static String gitLine(final String path) {
+ return "a/" + path + " b/" + path + "\n";
+ }
+
+ private static FileHeader dqHeader(final String path) {
+ return data(dqGitLine(path) + "--- " + path + "\n");
+ }
+
+ private static String dqGitLine(final String path) {
+ return "\"a/" + path + "\" \"b/" + path + "\"\n";
+ }
+}
--git a/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java b/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java
new file mode 100644
index 0000000..5d1454b
--- /dev/null
+++ b/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java
@@ -0,0 +1,430 @@
+/*
+ * 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.patch;
+
+import static org.spearce.jgit.lib.Constants.encodeASCII;
+import static org.spearce.jgit.util.RawParseUtils.decode;
+import static org.spearce.jgit.util.RawParseUtils.match;
+import static org.spearce.jgit.util.RawParseUtils.nextLF;
+import static org.spearce.jgit.util.RawParseUtils.parseBase10;
+
+import org.spearce.jgit.lib.AbbreviatedObjectId;
+import org.spearce.jgit.lib.Constants;
+import org.spearce.jgit.lib.FileMode;
+import org.spearce.jgit.util.QuotedString;
+
+/** Patch header describing an action for a single file path. */
+public class FileHeader {
+ /** Magical file name used for file adds or deletes. */
+ public static final String DEV_NULL = "/dev/null";
+
+ private static final byte[] OLD_MODE = encodeASCII("old mode ");
+
+ private static final byte[] NEW_MODE = encodeASCII("new mode ");
+
+ private static final byte[] DELETED_FILE_MODE = encodeASCII("deleted file mode ");
+
+ private static final byte[] NEW_FILE_MODE = encodeASCII("new file mode ");
+
+ private static final byte[] COPY_FROM = encodeASCII("copy from ");
+
+ private static final byte[] COPY_TO = encodeASCII("copy to ");
+
+ private static final byte[] RENAME_OLD = encodeASCII("rename old ");
+
+ private static final byte[] RENAME_NEW = encodeASCII("rename new ");
+
+ private static final byte[] RENAME_FROM = encodeASCII("rename from ");
+
+ private static final byte[] RENAME_TO = encodeASCII("rename to ");
+
+ private static final byte[] SIMILARITY_INDEX = encodeASCII("similarity index ");
+
+ private static final byte[] DISSIMILARITY_INDEX = encodeASCII("dissimilarity index ");
+
+ private static final byte[] INDEX = encodeASCII("index ");
+
+ static final byte[] OLD_NAME = encodeASCII("--- ");
+
+ static final byte[] NEW_NAME = encodeASCII("+++ ");
+
+ static final byte[] HUNK_HDR = encodeASCII("@@ -");
+
+ /** General type of change a single file-level patch describes. */
+ public static enum ChangeType {
+ /** Add a new file to the project */
+ ADD,
+
+ /** Modify an existing file in the project (content and/or mode) */
+ MODIFY,
+
+ /** Delete an existing file from the project */
+ DELETE,
+
+ /** Rename an existing file to a new location */
+ RENAME,
+
+ /** Copy an existing file to a new location, keeping the original */
+ COPY;
+ }
+
+ /** Buffer holding the patch data for this file. */
+ final byte[] buf;
+
+ /** Offset within {@link #buf} to the "diff ..." line. */
+ final int startOffset;
+
+ /** Position 1 past the end of this file within {@link #buf}. */
+ int endOffset;
+
+ /** File name of the old (pre-image). */
+ private String oldName;
+
+ /** File name of the new (post-image). */
+ private String newName;
+
+ /** Old mode of the file, if described by the patch, else null. */
+ private FileMode oldMode;
+
+ /** New mode of the file, if described by the patch, else null. */
+ private FileMode newMode;
+
+ /** General type of change indicated by the patch. */
+ private ChangeType changeType;
+
+ /** Similarity score if {@link #changeType} is a copy or rename. */
+ private int score;
+
+ /** ObjectId listed on the index line for the old (pre-image) */
+ private AbbreviatedObjectId oldId;
+
+ /** ObjectId listed on the index line for the new (post-image) */
+ private AbbreviatedObjectId newId;
+
+ FileHeader(final byte[] b, final int offset) {
+ buf = b;
+ startOffset = offset;
+ changeType = ChangeType.MODIFY; // unless otherwise designated
+ }
+
+ /**
+ * Get the old name associated with this file.
+ * <p>
+ * The meaning of the old name can differ depending on the semantic meaning
+ * of this patch:
+ * <ul>
+ * <li><i>file add</i>: always <code>/dev/null</code></li>
+ * <li><i>file modify</i>: always {@link #getNewName()}</li>
+ * <li><i>file delete</i>: always the file being deleted</li>
+ * <li><i>file copy</i>: source file the copy originates from</li>
+ * <li><i>file rename</i>: source file the rename originates from</li>
+ * </ul>
+ *
+ * @return old name for this file.
+ */
+ public String getOldName() {
+ return oldName;
+ }
+
+ /**
+ * Get the new name associated with this file.
+ * <p>
+ * The meaning of the new name can differ depending on the semantic meaning
+ * of this patch:
+ * <ul>
+ * <li><i>file add</i>: always the file being created</li>
+ * <li><i>file modify</i>: always {@link #getOldName()}</li>
+ * <li><i>file delete</i>: always <code>/dev/null</code></li>
+ * <li><i>file copy</i>: destination file the copy ends up at</li>
+ * <li><i>file rename</i>: destination file the rename ends up at/li>
+ * </ul>
+ *
+ * @return new name for this file.
+ */
+ public String getNewName() {
+ return newName;
+ }
+
+ /** @return the old file mode, if described in the patch */
+ public FileMode getOldMode() {
+ return oldMode;
+ }
+
+ /** @return the new file mode, if described in the patch */
+ public FileMode getNewMode() {
+ return newMode;
+ }
+
+ /** @return the type of change this patch makes on {@link #getNewName()} */
+ public ChangeType getChangeType() {
+ return changeType;
+ }
+
+ /**
+ * @return similarity score between {@link #getOldName()} and
+ * {@link #getNewName()} if {@link #getChangeType()} is
+ * {@link ChangeType#COPY} or {@link ChangeType#RENAME}.
+ */
+ public int getScore() {
+ return score;
+ }
+
+ /**
+ * Get the old object id from the <code>index</code>.
+ *
+ * @return the object id; null if there is no index line
+ */
+ public AbbreviatedObjectId getOldId() {
+ return oldId;
+ }
+
+ /**
+ * Get the new object id from the <code>index</code>.
+ *
+ * @return the object id; null if there is no index line
+ */
+ public AbbreviatedObjectId getNewId() {
+ return newId;
+ }
+
+ /**
+ * Parse a "diff --git" or "diff --cc" line.
+ *
+ * @param ptr
+ * first character after the "diff --git " or "diff --cc " part.
+ * @return first character after the LF at the end of the line; -1 on error.
+ */
+ int parseGitFileName(int ptr) {
+ final int eol = nextLF(buf, ptr);
+ final int bol = ptr;
+ if (eol >= buf.length) {
+ return -1;
+ }
+
+ // buffer[ptr..eol] looks like "a/foo b/foo\n". After the first
+ // A regex to match this is "^[^/]+/(.*?) [^/+]+/\1\n$". There
+ // is only one way to split the line such that text to the left
+ // of the space matches the text to the right, excluding the part
+ // before the first slash.
+ //
+
+ final int aStart = nextLF(buf, ptr, '/');
+ if (aStart >= eol)
+ return eol;
+
+ while (ptr < eol) {
+ final int sp = nextLF(buf, ptr, ' ');
+ if (sp >= eol) {
+ // We can't split the header, it isn't valid.
+ // This may be OK if this is a rename patch.
+ //
+ return eol;
+ }
+ final int bStart = nextLF(buf, sp, '/');
+ if (bStart >= eol)
+ return eol;
+
+ // If buffer[aStart..sp - 1] = buffer[bStart..eol - 1]
+ // we have a valid split.
+ //
+ if (eq(aStart, sp - 1, bStart, eol - 1)) {
+ if (buf[bol] == '"') {
+ // We're a double quoted name. The region better end
+ // in a double quote too, and we need to decode the
+ // characters before reading the name.
+ //
+ if (buf[sp - 2] != '"') {
+ return eol;
+ }
+ oldName = QuotedString.GIT_PATH.dequote(buf, bol, sp - 1);
+ oldName = p1(oldName);
+ } else {
+ oldName = decode(Constants.CHARSET, buf, aStart, sp - 1);
+ }
+ newName = oldName;
+ return eol;
+ }
+
+ // This split wasn't correct. Move past the space and try
+ // another split as the space must be part of the file name.
+ //
+ ptr = sp;
+ }
+
+ return eol;
+ }
+
+ int parseGitHeaders(int ptr) {
+ final int sz = buf.length;
+ while (ptr < sz) {
+ final int eol = nextLF(buf, ptr);
+ if (match(buf, ptr, HUNK_HDR) >= 0) {
+ // First hunk header; break out and parse them later.
+ break;
+
+ } else if (match(buf, ptr, OLD_NAME) >= 0) {
+ oldName = p1(parseName(oldName, ptr + OLD_NAME.length, eol));
+ if (oldName == DEV_NULL)
+ changeType = ChangeType.ADD;
+
+ } else if (match(buf, ptr, NEW_NAME) >= 0) {
+ newName = p1(parseName(newName, ptr + NEW_NAME.length, eol));
+ if (newName == DEV_NULL)
+ changeType = ChangeType.DELETE;
+
+ } else if (match(buf, ptr, OLD_MODE) >= 0) {
+ oldMode = parseFileMode(ptr + OLD_MODE.length, eol);
+
+ } else if (match(buf, ptr, NEW_MODE) >= 0) {
+ newMode = parseFileMode(ptr + NEW_MODE.length, eol);
+
+ } else if (match(buf, ptr, DELETED_FILE_MODE) >= 0) {
+ oldMode = parseFileMode(ptr + DELETED_FILE_MODE.length, eol);
+ changeType = ChangeType.DELETE;
+
+ } else if (match(buf, ptr, NEW_FILE_MODE) >= 0) {
+ newMode = parseFileMode(ptr + NEW_FILE_MODE.length, eol);
+ changeType = ChangeType.ADD;
+
+ } else if (match(buf, ptr, COPY_FROM) >= 0) {
+ oldName = parseName(oldName, ptr + COPY_FROM.length, eol);
+ changeType = ChangeType.COPY;
+
+ } else if (match(buf, ptr, COPY_TO) >= 0) {
+ newName = parseName(newName, ptr + COPY_TO.length, eol);
+ changeType = ChangeType.COPY;
+
+ } else if (match(buf, ptr, RENAME_OLD) >= 0) {
+ oldName = parseName(oldName, ptr + RENAME_OLD.length, eol);
+ changeType = ChangeType.RENAME;
+
+ } else if (match(buf, ptr, RENAME_NEW) >= 0) {
+ newName = parseName(newName, ptr + RENAME_NEW.length, eol);
+ changeType = ChangeType.RENAME;
+
+ } else if (match(buf, ptr, RENAME_FROM) >= 0) {
+ oldName = parseName(oldName, ptr + RENAME_FROM.length, eol);
+ changeType = ChangeType.RENAME;
+
+ } else if (match(buf, ptr, RENAME_TO) >= 0) {
+ newName = parseName(newName, ptr + RENAME_TO.length, eol);
+ changeType = ChangeType.RENAME;
+
+ } else if (match(buf, ptr, SIMILARITY_INDEX) >= 0) {
+ score = parseBase10(buf, ptr + SIMILARITY_INDEX.length, null);
+
+ } else if (match(buf, ptr, DISSIMILARITY_INDEX) >= 0) {
+ score = parseBase10(buf, ptr + DISSIMILARITY_INDEX.length, null);
+
+ } else if (match(buf, ptr, INDEX) >= 0) {
+ parseIndexLine(ptr + INDEX.length, eol);
+
+ } else {
+ // Probably an empty patch (stat dirty).
+ break;
+ }
+
+ ptr = eol;
+ }
+ return ptr;
+ }
+
+ private String parseName(final String expect, int ptr, final int end) {
+ if (ptr == end)
+ return expect;
+
+ String r;
+ if (buf[ptr] == '"') {
+ // New style GNU diff format
+ //
+ r = QuotedString.GIT_PATH.dequote(buf, ptr, end - 1);
+ } else {
+ // Older style GNU diff format, an optional tab ends the name.
+ //
+ int tab = end;
+ while (ptr < tab && buf[tab - 1] != '\t')
+ tab--;
+ if (ptr == tab)
+ tab = end;
+ r = decode(Constants.CHARSET, buf, ptr, tab - 1);
+ }
+
+ if (r.equals(DEV_NULL))
+ r = DEV_NULL;
+ return r;
+ }
+
+ private static String p1(final String r) {
+ final int s = r.indexOf('/');
+ return s > 0 ? r.substring(s + 1) : r;
+ }
+
+ private FileMode parseFileMode(int ptr, final int end) {
+ int tmp = 0;
+ while (ptr < end - 1) {
+ tmp <<= 3;
+ tmp += buf[ptr++] - '0';
+ }
+ return FileMode.fromBits(tmp);
+ }
+
+ private void parseIndexLine(int ptr, final int end) {
+ // "index $asha1..$bsha1[ $mode]" where $asha1 and $bsha1
+ // can be unique abbreviations
+ //
+ final int dot2 = nextLF(buf, ptr, '.');
+ final int mode = nextLF(buf, dot2, ' ');
+
+ oldId = AbbreviatedObjectId.fromString(buf, ptr, dot2 - 1);
+ newId = AbbreviatedObjectId.fromString(buf, dot2 + 1, mode - 1);
+
+ if (mode < end)
+ newMode = oldMode = parseFileMode(mode, end);
+ }
+
+ private boolean eq(int aPtr, int aEnd, int bPtr, int bEnd) {
+ if (aEnd - aPtr != bEnd - bPtr) {
+ return false;
+ }
+ while (aPtr < aEnd) {
+ if (buf[aPtr++] != buf[bPtr++])
+ return false;
+ }
+ return true;
+ }
+}
--
1.6.1.rc2.299.gead4c
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders
2008-12-11 4:58 ` [JGIT PATCH 3/5] Define FileHeader to parse the header block of a git diff Shawn O. Pearce
@ 2008-12-11 4:58 ` Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 5/5] Add HunkHeader to represent a single hunk of a file within a patch Shawn O. Pearce
2008-12-11 18:34 ` [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders Robin Rosenberg
0 siblings, 2 replies; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 4:58 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
Most patch scripts impact more than one file at a time, so we need
to support parsing multiple FileHeaders from the same input stream
and collect them into a larger entity representing the entire script.
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
---
.../tst/org/spearce/jgit/patch/PatchTest.java | 97 +++++++++
.../patch/testParse_ConfigCaseInsensitive.patch | 67 ++++++
| 28 +++
.../src/org/spearce/jgit/patch/Patch.java | 217 ++++++++++++++++++++
4 files changed, 409 insertions(+), 0 deletions(-)
create mode 100644 org.spearce.jgit.test/tst/org/spearce/jgit/patch/PatchTest.java
create mode 100644 org.spearce.jgit.test/tst/org/spearce/jgit/patch/testParse_ConfigCaseInsensitive.patch
create mode 100644 org.spearce.jgit/src/org/spearce/jgit/patch/Patch.java
diff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/patch/PatchTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/patch/PatchTest.java
new file mode 100644
index 0000000..f389d89
--- /dev/null
+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/patch/PatchTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.patch;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import junit.framework.TestCase;
+
+import org.spearce.jgit.lib.FileMode;
+
+public class PatchTest extends TestCase {
+ public void testEmpty() {
+ final Patch p = new Patch();
+ assertTrue(p.getFiles().isEmpty());
+ }
+
+ public void testParse_ConfigCaseInsensitive() throws IOException {
+ final Patch p = parseTestPatchFile();
+ assertEquals(2, p.getFiles().size());
+
+ final FileHeader fRepositoryConfigTest = p.getFiles().get(0);
+ final FileHeader fRepositoryConfig = p.getFiles().get(1);
+
+ assertEquals(
+ "org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryConfigTest.java",
+ fRepositoryConfigTest.getNewName());
+
+ assertEquals(
+ "org.spearce.jgit/src/org/spearce/jgit/lib/RepositoryConfig.java",
+ fRepositoryConfig.getNewName());
+
+ assertEquals(572, fRepositoryConfigTest.startOffset);
+ assertEquals(1490, fRepositoryConfig.startOffset);
+
+ assertEquals("da7e704", fRepositoryConfigTest.getOldId().name());
+ assertEquals("34ce04a", fRepositoryConfigTest.getNewId().name());
+ assertSame(FileMode.REGULAR_FILE, fRepositoryConfigTest.getOldMode());
+ assertSame(FileMode.REGULAR_FILE, fRepositoryConfigTest.getNewMode());
+
+ assertEquals("45c2f8a", fRepositoryConfig.getOldId().name());
+ assertEquals("3291bba", fRepositoryConfig.getNewId().name());
+ assertSame(FileMode.REGULAR_FILE, fRepositoryConfig.getOldMode());
+ assertSame(FileMode.REGULAR_FILE, fRepositoryConfig.getNewMode());
+ }
+
+ private Patch parseTestPatchFile() throws IOException {
+ final String patchFile = getName() + ".patch";
+ final InputStream in = getClass().getResourceAsStream(patchFile);
+ if (in == null) {
+ fail("No " + patchFile + " test vector");
+ return null; // Never happens
+ }
+ try {
+ final Patch p = new Patch();
+ p.parse(in);
+ return p;
+ } finally {
+ in.close();
+ }
+ }
+}
diff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/patch/testParse_ConfigCaseInsensitive.patch b/org.spearce.jgit.test/tst/org/spearce/jgit/patch/testParse_ConfigCaseInsensitive.patch
new file mode 100644
index 0000000..b30418e
--- /dev/null
+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/patch/testParse_ConfigCaseInsensitive.patch
@@ -0,0 +1,67 @@
+From ce9b593ddf2530613f6da9d7f7e4a5ff93da8b36 Mon Sep 17 00:00:00 2001
+From: Robin Rosenberg <robin.rosenberg@dewire.com>
+Date: Mon, 13 Oct 2008 00:50:59 +0200
+Subject: [PATCH] git config file is case insensitive
+
+Keys are now always compared with ignore case rules.
+
+Signed-off-by: Robin Rosenberg <robin.rosenberg@dewire.com>
+Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
+---
+ .../org/spearce/jgit/lib/RepositoryConfigTest.java | 7 +++++++
+ .../src/org/spearce/jgit/lib/RepositoryConfig.java | 8 ++++----
+ 2 files changed, 11 insertions(+), 4 deletions(-)
+
+diff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryConfigTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryConfigTest.java
+index da7e704..34ce04a 100644
+--- a/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryConfigTest.java
++++ b/org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryConfigTest.java
+@@ -109,4 +109,11 @@ assertTrue(Arrays.equals(values.toArray(), repositoryConfig
+ .getStringList("my", null, "somename")));
+ checkFile(cfgFile, "[my]\n\tsomename = value1\n\tsomename = value2\n");
+ }
++
++ public void test006_readCaseInsensitive() throws IOException {
++ final File path = writeTrashFile("config_001", "[Foo]\nBar\n");
++ RepositoryConfig repositoryConfig = new RepositoryConfig(null, path);
++ assertEquals(true, repositoryConfig.getBoolean("foo", null, "bar", false));
++ assertEquals("", repositoryConfig.getString("foo", null, "bar"));
++ }
+ }
+diff --git a/org.spearce.jgit/src/org/spearce/jgit/lib/RepositoryConfig.java b/org.spearce.jgit/src/org/spearce/jgit/lib/RepositoryConfig.java
+index 45c2f8a..3291bba 100644
+--- a/org.spearce.jgit/src/org/spearce/jgit/lib/RepositoryConfig.java
++++ b/org.spearce.jgit/src/org/spearce/jgit/lib/RepositoryConfig.java
+@@ -236,9 +236,9 @@ protected boolean getBoolean(final String section, String subsection,
+ return defaultValue;
+
+ n = n.toLowerCase();
+- if (MAGIC_EMPTY_VALUE.equals(n) || "yes".equals(n) || "true".equals(n) || "1".equals(n)) {
++ if (MAGIC_EMPTY_VALUE.equals(n) || "yes".equalsIgnoreCase(n) || "true".equalsIgnoreCase(n) || "1".equals(n)) {
+ return true;
+- } else if ("no".equals(n) || "false".equals(n) || "0".equals(n)) {
++ } else if ("no".equalsIgnoreCase(n) || "false".equalsIgnoreCase(n) || "0".equalsIgnoreCase(n)) {
+ return false;
+ } else {
+ throw new IllegalArgumentException("Invalid boolean value: "
+@@ -300,7 +300,7 @@ public String getString(final String section, String subsection, final String na
+ final Set<String> result = new HashSet<String>();
+
+ for (final Entry e : entries) {
+- if (section.equals(e.base) && e.extendedBase != null)
++ if (section.equalsIgnoreCase(e.base) && e.extendedBase != null)
+ result.add(e.extendedBase);
+ }
+ if (baseConfig != null)
+@@ -954,7 +954,7 @@ private static boolean eq(final String a, final String b) {
+ return true;
+ if (a == null || b == null)
+ return false;
+- return a.equals(b);
++ return a.equalsIgnoreCase(b);
+ }
+ }
+ }
+--
+1.6.1.rc2.299.gead4c
+
--git a/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java b/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java
index 5d1454b..f57a0ff 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java
@@ -364,6 +364,34 @@ int parseGitHeaders(int ptr) {
return ptr;
}
+ int parseTraditionalHeaders(int ptr) {
+ final int sz = buf.length;
+ while (ptr < sz) {
+ final int eol = nextLF(buf, ptr);
+ if (match(buf, ptr, HUNK_HDR) >= 0) {
+ // First hunk header; break out and parse them later.
+ break;
+
+ } else if (match(buf, ptr, OLD_NAME) >= 0) {
+ oldName = p1(parseName(oldName, ptr + OLD_NAME.length, eol));
+ if (oldName == DEV_NULL)
+ changeType = ChangeType.ADD;
+
+ } else if (match(buf, ptr, NEW_NAME) >= 0) {
+ newName = p1(parseName(newName, ptr + NEW_NAME.length, eol));
+ if (newName == DEV_NULL)
+ changeType = ChangeType.DELETE;
+
+ } else {
+ // Possibly an empty patch.
+ break;
+ }
+
+ ptr = eol;
+ }
+ return ptr;
+ }
+
private String parseName(final String expect, int ptr, final int end) {
if (ptr == end)
return expect;
diff --git a/org.spearce.jgit/src/org/spearce/jgit/patch/Patch.java b/org.spearce.jgit/src/org/spearce/jgit/patch/Patch.java
new file mode 100644
index 0000000..30d12a5
--- /dev/null
+++ b/org.spearce.jgit/src/org/spearce/jgit/patch/Patch.java
@@ -0,0 +1,217 @@
+/*
+ * 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.patch;
+
+import static org.spearce.jgit.lib.Constants.encodeASCII;
+import static org.spearce.jgit.patch.FileHeader.HUNK_HDR;
+import static org.spearce.jgit.patch.FileHeader.NEW_NAME;
+import static org.spearce.jgit.patch.FileHeader.OLD_NAME;
+import static org.spearce.jgit.util.RawParseUtils.match;
+import static org.spearce.jgit.util.RawParseUtils.nextLF;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.spearce.jgit.util.TemporaryBuffer;
+
+/** 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 ");
+
+ private static final byte[] DIFF_CC = encodeASCII("diff --cc ");
+
+ /** The files, in the order they were parsed out of the input. */
+ private final List<FileHeader> files;
+
+ /** Create an empty patch. */
+ public Patch() {
+ files = new ArrayList<FileHeader>();
+ }
+
+ /**
+ * Add a single file to this patch.
+ * <p>
+ * Typically files should be added by parsing the text through one of this
+ * class's parse methods.
+ *
+ * @param fh
+ * the header of the file.
+ */
+ public void addFile(final FileHeader fh) {
+ files.add(fh);
+ }
+
+ /** @return list of files described in the patch, in occurrence order. */
+ public List<FileHeader> getFiles() {
+ return files;
+ }
+
+ /**
+ * Parse a patch received from an InputStream.
+ * <p>
+ * Multiple parse calls on the same instance will concatenate the patch
+ * data, but each parse input must start with a valid file header (don't
+ * split a single file across parse calls).
+ *
+ * @param is
+ * the stream to read the patch data from. The stream is read
+ * until EOF is reached.
+ * @throws IOException
+ * there was an error reading from the input stream.
+ */
+ public void parse(final InputStream is) throws IOException {
+ final byte[] buf = readFully(is);
+ parse(buf, 0, buf.length);
+ }
+
+ private static byte[] readFully(final InputStream is) throws IOException {
+ final TemporaryBuffer b = new TemporaryBuffer();
+ b.copy(is);
+ final byte[] buf = b.toByteArray();
+ return buf;
+ }
+
+ /**
+ * Parse a patch stored in a byte[].
+ * <p>
+ * Multiple parse calls on the same instance will concatenate the patch
+ * data, but each parse input must start with a valid file header (don't
+ * split a single file across parse calls).
+ *
+ * @param buf
+ * the buffer to parse.
+ * @param ptr
+ * starting position to parse from.
+ * @param end
+ * 1 past the last position to end parsing. The total length to
+ * be parsed is <code>end - ptr</code>.
+ */
+ public void parse(final byte[] buf, int ptr, final int end) {
+ while (ptr < end)
+ ptr = parseFile(buf, ptr);
+ }
+
+ private int parseFile(final byte[] buf, int c) {
+ final int sz = buf.length;
+ while (c < sz) {
+ // Valid git style patch?
+ //
+ if (match(buf, c, DIFF_GIT) >= 0)
+ return parseDiffGit(buf, c);
+ if (match(buf, c, DIFF_CC) >= 0)
+ return parseDiffCC(buf, c);
+
+ // Junk between files? Leading junk? Traditional
+ // (non-git generated) patch?
+ //
+ final int n = nextLF(buf, c);
+ if (n >= sz) {
+ // Patches cannot be only one line long. This must be
+ // trailing junk that we should ignore.
+ //
+ return sz;
+ }
+
+ if (n - c < 6) {
+ // A valid header must be at least 6 bytes on the
+ // first line, e.g. "--- a/b\n".
+ //
+ c = n;
+ continue;
+ }
+
+ if (match(buf, c, OLD_NAME) >= 0 && match(buf, n, NEW_NAME) >= 0) {
+ // Probably a traditional patch. Ensure we have at least
+ // a "@@ -0,0" smelling line next. We only check the "@@ -".
+ //
+ final int f = nextLF(buf, n);
+ if (f >= sz)
+ return sz;
+ if (match(buf, f, HUNK_HDR) >= 0)
+ return parseTraditionalPatch(buf, c);
+ }
+
+ c = n;
+ }
+ return c;
+ }
+
+ private int parseDiffGit(final byte[] buf, final int startOffset) {
+ final FileHeader fh = new FileHeader(buf, startOffset);
+ int ptr = fh.parseGitFileName(startOffset + DIFF_GIT.length);
+ if (ptr < 0)
+ return skipFile(buf, startOffset);
+
+ ptr = fh.parseGitHeaders(ptr);
+ // TODO parse hunks
+ fh.endOffset = ptr;
+ addFile(fh);
+ return ptr;
+ }
+
+ private int parseDiffCC(final byte[] buf, final int startOffset) {
+ final FileHeader fh = new FileHeader(buf, startOffset);
+ int ptr = fh.parseGitFileName(startOffset + DIFF_CC.length);
+ if (ptr < 0)
+ return skipFile(buf, startOffset);
+
+ // TODO Support parsing diff --cc headers
+ // TODO parse diff --cc hunks
+ fh.endOffset = ptr;
+ addFile(fh);
+ return ptr;
+ }
+
+ private int parseTraditionalPatch(final byte[] buf, final int startOffset) {
+ final FileHeader fh = new FileHeader(buf, startOffset);
+ int ptr = fh.parseTraditionalHeaders(startOffset);
+ // TODO parse hunks
+ fh.endOffset = ptr;
+ addFile(fh);
+ return ptr;
+ }
+
+ private static int skipFile(final byte[] buf, int ptr) {
+ ptr = nextLF(buf, ptr);
+ if (match(buf, ptr, OLD_NAME) >= 0)
+ ptr = nextLF(buf, ptr);
+ return ptr;
+ }
+}
--
1.6.1.rc2.299.gead4c
^ permalink raw reply related [flat|nested] 15+ messages in thread
* [JGIT PATCH 5/5] Add HunkHeader to represent a single hunk of a file within a patch
2008-12-11 4:58 ` [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders Shawn O. Pearce
@ 2008-12-11 4:58 ` Shawn O. Pearce
2008-12-11 18:34 ` [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders Robin Rosenberg
1 sibling, 0 replies; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 4:58 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
The hunk header parses the lines of the hunk, counting up the body
to ensure it matches with the header line. We store the hunks on
on the FileHeader, providing public access to them after a patch
has been parsed.
If the line counts in the body differ from the header we don't make
that an immediate error. This permits a future implementation of
something like "git apply --recount", where the hunk headers can be
recomputed based on the actual hunk contents after a human has done
manual editing. We may also need to parse a corrupt patch and show
it in a visual tool for a human to repair, so aborting with any sort
of exception isn't useful.
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
---
| 6 +-
.../tst/org/spearce/jgit/patch/PatchTest.java | 60 ++++++-
| 22 +++
| 185 ++++++++++++++++++++
.../src/org/spearce/jgit/patch/Patch.java | 54 ++++++-
5 files changed, 323 insertions(+), 4 deletions(-)
create mode 100644 org.spearce.jgit/src/org/spearce/jgit/patch/HunkHeader.java
--git a/org.spearce.jgit.test/tst/org/spearce/jgit/patch/FileHeaderTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/patch/FileHeaderTest.java
index 1d87bc0..4094a5c 100644
--- a/org.spearce.jgit.test/tst/org/spearce/jgit/patch/FileHeaderTest.java
+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/patch/FileHeaderTest.java
@@ -44,7 +44,10 @@
public class FileHeaderTest extends TestCase {
public void testParseGitFileName_Empty() {
- assertEquals(-1, data("").parseGitFileName(0));
+ final FileHeader fh = data("");
+ assertEquals(-1, fh.parseGitFileName(0));
+ assertNotNull(fh.getHunks());
+ assertTrue(fh.getHunks().isEmpty());
}
public void testParseGitFileName_NoLF() {
@@ -233,6 +236,7 @@ public void testParseRename100_OldStyle() {
assertEquals(100, fh.getScore());
}
+
public void testParseCopy100() {
final FileHeader fh = data("diff --git a/a b/ c/\\303\\205ngstr\\303\\266m\n"
+ "similarity index 100%\n"
diff --git a/org.spearce.jgit.test/tst/org/spearce/jgit/patch/PatchTest.java b/org.spearce.jgit.test/tst/org/spearce/jgit/patch/PatchTest.java
index f389d89..8ddaadc 100644
--- a/org.spearce.jgit.test/tst/org/spearce/jgit/patch/PatchTest.java
+++ b/org.spearce.jgit.test/tst/org/spearce/jgit/patch/PatchTest.java
@@ -72,11 +72,69 @@ assertEquals(
assertEquals("34ce04a", fRepositoryConfigTest.getNewId().name());
assertSame(FileMode.REGULAR_FILE, fRepositoryConfigTest.getOldMode());
assertSame(FileMode.REGULAR_FILE, fRepositoryConfigTest.getNewMode());
+ assertEquals(1, fRepositoryConfigTest.getHunks().size());
+ {
+ final HunkHeader h = fRepositoryConfigTest.getHunks().get(0);
+ assertEquals(921, h.startOffset);
+ assertEquals(109, h.getOldStartLine());
+ assertEquals(4, h.getOldLineCount());
+ assertEquals(109, h.getNewStartLine());
+ assertEquals(11, h.getNewLineCount());
+
+ assertEquals(4, h.getLinesContext());
+ assertEquals(7, h.getLinesAdded());
+ assertEquals(0, h.getLinesDeleted());
+
+ assertEquals(1490, h.endOffset);
+ }
assertEquals("45c2f8a", fRepositoryConfig.getOldId().name());
assertEquals("3291bba", fRepositoryConfig.getNewId().name());
assertSame(FileMode.REGULAR_FILE, fRepositoryConfig.getOldMode());
assertSame(FileMode.REGULAR_FILE, fRepositoryConfig.getNewMode());
+ assertEquals(3, fRepositoryConfig.getHunks().size());
+ {
+ final HunkHeader h = fRepositoryConfig.getHunks().get(0);
+ assertEquals(1803, h.startOffset);
+ assertEquals(236, h.getOldStartLine());
+ assertEquals(9, h.getOldLineCount());
+ assertEquals(236, h.getNewStartLine());
+ assertEquals(9, h.getNewLineCount());
+
+ assertEquals(7, h.getLinesContext());
+ assertEquals(2, h.getLinesAdded());
+ assertEquals(2, h.getLinesDeleted());
+
+ assertEquals(2434, h.endOffset);
+ }
+ {
+ final HunkHeader h = fRepositoryConfig.getHunks().get(1);
+ assertEquals(2434, h.startOffset);
+ assertEquals(300, h.getOldStartLine());
+ assertEquals(7, h.getOldLineCount());
+ assertEquals(300, h.getNewStartLine());
+ assertEquals(7, h.getNewLineCount());
+
+ assertEquals(6, h.getLinesContext());
+ assertEquals(1, h.getLinesAdded());
+ assertEquals(1, h.getLinesDeleted());
+
+ assertEquals(2816, h.endOffset);
+ }
+ {
+ final HunkHeader h = fRepositoryConfig.getHunks().get(2);
+ assertEquals(2816, h.startOffset);
+ assertEquals(954, h.getOldStartLine());
+ assertEquals(7, h.getOldLineCount());
+ assertEquals(954, h.getNewStartLine());
+ assertEquals(7, h.getNewLineCount());
+
+ assertEquals(6, h.getLinesContext());
+ assertEquals(1, h.getLinesAdded());
+ assertEquals(1, h.getLinesDeleted());
+
+ assertEquals(3035, h.endOffset);
+ }
}
private Patch parseTestPatchFile() throws IOException {
@@ -84,7 +142,7 @@ private Patch parseTestPatchFile() throws IOException {
final InputStream in = getClass().getResourceAsStream(patchFile);
if (in == null) {
fail("No " + patchFile + " test vector");
- return null; // Never happens
+ return null; // Never happens
}
try {
final Patch p = new Patch();
--git a/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java b/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java
index f57a0ff..b49cb7b 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/patch/FileHeader.java
@@ -43,6 +43,10 @@
import static org.spearce.jgit.util.RawParseUtils.nextLF;
import static org.spearce.jgit.util.RawParseUtils.parseBase10;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
import org.spearce.jgit.lib.AbbreviatedObjectId;
import org.spearce.jgit.lib.Constants;
import org.spearce.jgit.lib.FileMode;
@@ -136,6 +140,9 @@
/** ObjectId listed on the index line for the new (post-image) */
private AbbreviatedObjectId newId;
+ /** The hunks of this file */
+ private List<HunkHeader> hunks;
+
FileHeader(final byte[] b, final int offset) {
buf = b;
startOffset = offset;
@@ -222,6 +229,21 @@ public AbbreviatedObjectId getNewId() {
return newId;
}
+ /** @return hunks altering this file; in order of appearance in patch */
+ public List<HunkHeader> getHunks() {
+ if (hunks == null)
+ return Collections.emptyList();
+ return hunks;
+ }
+
+ void addHunk(final HunkHeader h) {
+ if (h.getFileHeader() != this)
+ throw new IllegalArgumentException("Hunk belongs to another file");
+ if (hunks == null)
+ hunks = new ArrayList<HunkHeader>();
+ hunks.add(h);
+ }
+
/**
* Parse a "diff --git" or "diff --cc" line.
*
--git a/org.spearce.jgit/src/org/spearce/jgit/patch/HunkHeader.java b/org.spearce.jgit/src/org/spearce/jgit/patch/HunkHeader.java
new file mode 100644
index 0000000..fc606c3
--- /dev/null
+++ b/org.spearce.jgit/src/org/spearce/jgit/patch/HunkHeader.java
@@ -0,0 +1,185 @@
+/*
+ * 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.patch;
+
+import static org.spearce.jgit.util.RawParseUtils.match;
+import static org.spearce.jgit.util.RawParseUtils.nextLF;
+import static org.spearce.jgit.util.RawParseUtils.parseBase10;
+
+import org.spearce.jgit.util.MutableInteger;
+
+/** Hunk header describing the layout of a single block of lines */
+public class HunkHeader {
+ private final FileHeader file;
+
+ /** Offset within {@link #file}.buf to the "@@ -" line. */
+ final int startOffset;
+
+ /** Position 1 past the end of this hunk within {@link #file}'s buf. */
+ int endOffset;
+
+ /** First line number in the pre-image file where the hunk starts */
+ int oldStartLine;
+
+ /** Total number of pre-image lines this hunk covers (context + deleted) */
+ int oldLineCount;
+
+ /** First line number in the post-image file where the hunk starts */
+ int newStartLine;
+
+ /** Total number of post-image lines this hunk covers (context + inserted) */
+ int newLineCount;
+
+ /** Total number of lines of context appearing in this hunk */
+ int nContext;
+
+ /** Number of lines removed by this hunk */
+ int nDeleted;
+
+ /** Number of lines added by this hunk */
+ int nAdded;
+
+ HunkHeader(final FileHeader fh, final int offset) {
+ file = fh;
+ startOffset = offset;
+ }
+
+ /** @return header for the file this hunk applies to */
+ public FileHeader getFileHeader() {
+ return file;
+ }
+
+ /** @return first line number in the pre-image file where the hunk starts */
+ public int getOldStartLine() {
+ return oldStartLine;
+ }
+
+ /** @return total number of pre-image lines this hunk covers */
+ public int getOldLineCount() {
+ return oldLineCount;
+ }
+
+ /** @return first line number in the post-image file where the hunk starts */
+ public int getNewStartLine() {
+ return newStartLine;
+ }
+
+ /** @return Total number of post-image lines this hunk covers */
+ public int getNewLineCount() {
+ return newLineCount;
+ }
+
+ /** @return total number of lines of context appearing in this hunk */
+ public int getLinesContext() {
+ return nContext;
+ }
+
+ /** @return number of lines removed by this hunk */
+ public int getLinesDeleted() {
+ return nDeleted;
+ }
+
+ /** @return number of lines added by this hunk */
+ public int getLinesAdded() {
+ return nAdded;
+ }
+
+ void parseHeader() {
+ // Parse "@@ -236,9 +236,9 @@ protected boolean"
+ //
+ final byte[] buf = file.buf;
+ final MutableInteger ptr = new MutableInteger();
+ ptr.value = nextLF(buf, startOffset, ' ');
+ oldStartLine = -parseBase10(buf, ptr.value, ptr);
+ oldLineCount = parseBase10(buf, ptr.value + 1, ptr);
+
+ newStartLine = parseBase10(buf, ptr.value + 1, ptr);
+ newLineCount = parseBase10(buf, ptr.value + 1, ptr);
+ }
+
+ int parseBody() {
+ final byte[] buf = file.buf;
+ final int sz = buf.length;
+ int c = nextLF(buf, startOffset), last = c;
+
+ nDeleted = 0;
+ nAdded = 0;
+
+ SCAN: for (; c < sz; last = c, c = nextLF(buf, c)) {
+ switch (buf[c]) {
+ case ' ':
+ case '\n':
+ nContext++;
+ continue;
+
+ case '-':
+ nDeleted++;
+ continue;
+
+ case '+':
+ nAdded++;
+ continue;
+
+ case '\\': // Matches "\ No newline at end of file"
+ continue;
+
+ default:
+ break SCAN;
+ }
+ }
+
+ if (last < sz && nContext + nDeleted - 1 == oldLineCount
+ && nContext + nAdded == newLineCount
+ && match(buf, last, Patch.SIG_FOOTER) >= 0) {
+ // This is an extremely common occurrence of "corruption".
+ // Users add footers with their signatures after this mark,
+ // and git diff adds the git executable version number.
+ // Let it slide; the hunk otherwise looked sound.
+ //
+ nDeleted--;
+ return last;
+ }
+
+ if (nContext + nDeleted != oldLineCount
+ || nContext + nAdded != newLineCount) {
+ // TODO report on truncated hunk
+ }
+
+ return c;
+ }
+}
diff --git a/org.spearce.jgit/src/org/spearce/jgit/patch/Patch.java b/org.spearce.jgit/src/org/spearce/jgit/patch/Patch.java
index 30d12a5..165058d 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/patch/Patch.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/patch/Patch.java
@@ -57,6 +57,8 @@
private static final byte[] DIFF_CC = encodeASCII("diff --cc ");
+ static final byte[] SIG_FOOTER = encodeASCII("-- \n");
+
/** The files, in the order they were parsed out of the input. */
private final List<FileHeader> files;
@@ -131,6 +133,17 @@ public void parse(final byte[] buf, int ptr, final int end) {
private int parseFile(final byte[] buf, int c) {
final int sz = buf.length;
while (c < sz) {
+ if (match(buf, c, HUNK_HDR) >= 0) {
+ // If we find a disconnected hunk header we might
+ // have missed a file header previously. The hunk
+ // isn't valid without knowing where it comes from.
+ //
+
+ // TODO handle a disconnected hunk fragment
+ c = nextLF(buf, c);
+ continue;
+ }
+
// Valid git style patch?
//
if (match(buf, c, DIFF_GIT) >= 0)
@@ -180,7 +193,7 @@ private int parseDiffGit(final byte[] buf, final int startOffset) {
return skipFile(buf, startOffset);
ptr = fh.parseGitHeaders(ptr);
- // TODO parse hunks
+ ptr = parseHunks(fh, buf, ptr);
fh.endOffset = ptr;
addFile(fh);
return ptr;
@@ -202,7 +215,7 @@ private int parseDiffCC(final byte[] buf, final int startOffset) {
private int parseTraditionalPatch(final byte[] buf, final int startOffset) {
final FileHeader fh = new FileHeader(buf, startOffset);
int ptr = fh.parseTraditionalHeaders(startOffset);
- // TODO parse hunks
+ ptr = parseHunks(fh, buf, ptr);
fh.endOffset = ptr;
addFile(fh);
return ptr;
@@ -214,4 +227,41 @@ private static int skipFile(final byte[] buf, int ptr) {
ptr = nextLF(buf, ptr);
return ptr;
}
+
+ private int parseHunks(final FileHeader fh, final byte[] buf, int c) {
+ final int sz = buf.length;
+ while (c < sz) {
+ // If we see a file header at this point, we have all of the
+ // hunks for our current file. We should stop and report back
+ // with this position so it can be parsed again later.
+ //
+ if (match(buf, c, DIFF_GIT) >= 0)
+ return c;
+ if (match(buf, c, DIFF_CC) >= 0)
+ return c;
+ if (match(buf, c, OLD_NAME) >= 0)
+ return c;
+ if (match(buf, c, NEW_NAME) >= 0)
+ return c;
+
+ if (match(buf, c, HUNK_HDR) >= 0) {
+ final HunkHeader h = new HunkHeader(fh, c);
+ h.parseHeader();
+ c = h.parseBody();
+ h.endOffset = c;
+ fh.addHunk(h);
+ if (c < sz && buf[c] != '@' && buf[c] != 'd'
+ && match(buf, c, SIG_FOOTER) < 0) {
+ // TODO report on noise between hunks, might be an error
+ }
+ continue;
+ }
+
+ // Skip this line and move to the next. Its probably garbage
+ // after the last hunk of a file.
+ //
+ c = nextLF(buf, c);
+ }
+ return c;
+ }
}
--
1.6.1.rc2.299.gead4c
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [JGIT PATCH 2/5] Add copy(InputStream) to TemporaryBuffer
2008-12-11 4:58 ` [JGIT PATCH 2/5] Add copy(InputStream) " Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 3/5] Define FileHeader to parse the header block of a git diff Shawn O. Pearce
@ 2008-12-11 15:40 ` Robin Rosenberg
2008-12-11 15:52 ` Shawn O. Pearce
1 sibling, 1 reply; 15+ messages in thread
From: Robin Rosenberg @ 2008-12-11 15:40 UTC (permalink / raw)
To: Shawn O. Pearce; +Cc: git
torsdag 11 december 2008 05:58:39 skrev Shawn O. Pearce:
> In some places we may find it ourselves with an InputStream we
> need to copy into a TemporaryBuffer, so we can flatten out the
> entire stream to a single byte[]. Putting the copy loop here
> is more useful then duplicating it in application level code.
> + public void copy(final InputStream in) throws IOException {
> + final byte[] b = new byte[2048];
Why not 8192 here too?
-- robin
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [JGIT PATCH 2/5] Add copy(InputStream) to TemporaryBuffer
2008-12-11 15:40 ` [JGIT PATCH 2/5] Add copy(InputStream) to TemporaryBuffer Robin Rosenberg
@ 2008-12-11 15:52 ` Shawn O. Pearce
2008-12-11 16:53 ` [JGIT PATCH 2/5 v2] " Shawn O. Pearce
0 siblings, 1 reply; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 15:52 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
Robin Rosenberg <robin.rosenberg@dewire.com> wrote:
> torsdag 11 december 2008 05:58:39 skrev Shawn O. Pearce:
> > In some places we may find it ourselves with an InputStream we
> > need to copy into a TemporaryBuffer, so we can flatten out the
> > entire stream to a single byte[]. Putting the copy loop here
> > is more useful then duplicating it in application level code.
> > + public void copy(final InputStream in) throws IOException {
> > + final byte[] b = new byte[2048];
>
> Why not 8192 here too?
Blargh, you're right. Actually what I should do is look to see
if blocks != null, in which case I should alloc a block and read
directly into it. That would avoid one copy of the data.
--
Shawn.
^ permalink raw reply [flat|nested] 15+ messages in thread
* [JGIT PATCH 2/5 v2] Add copy(InputStream) to TemporaryBuffer
2008-12-11 15:52 ` Shawn O. Pearce
@ 2008-12-11 16:53 ` Shawn O. Pearce
0 siblings, 0 replies; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 16:53 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
In some places we may find ourselves with an InputStream we
need to copy into a TemporaryBuffer, so we can flatten out the
entire stream to a single byte[]. Putting the copy loop here
is more useful then duplicating it in application level code.
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
---
"Shawn O. Pearce" <spearce@spearce.org> wrote:
> Robin Rosenberg <robin.rosenberg@dewire.com> wrote:
> > torsdag 11 december 2008 05:58:39 skrev Shawn O. Pearce:
> > > + final byte[] b = new byte[2048];
> >
> > Why not 8192 here too?
>
> Blargh, you're right. Actually what I should do is look to see
> if blocks != null, in which case I should alloc a block and read
> directly into it. That would avoid one copy of the data.
And now we do that...
diff --git a/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java b/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
index b1ffd6e..761f359 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
@@ -42,6 +42,7 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
@@ -135,6 +136,39 @@ public void write(final byte[] b, int off, int len) throws IOException {
diskOut.write(b, off, len);
}
+ /**
+ * Copy all bytes remaining on the input stream into this buffer.
+ *
+ * @param in
+ * the stream to read from, until EOF is reached.
+ * @throws IOException
+ * an error occurred reading from the input stream, or while
+ * writing to a local temporary file.
+ */
+ public void copy(final InputStream in) throws IOException {
+ if (blocks != null) {
+ for (;;) {
+ Block s = last();
+ if (s.isFull()) {
+ if (reachedInCoreLimit())
+ break;
+ s = new Block();
+ blocks.add(s);
+ }
+
+ final int n = in.read(s.buffer, s.count, Block.SZ - s.count);
+ if (n < 1)
+ return;
+ s.count += n;
+ }
+ }
+
+ final byte[] tmp = new byte[Block.SZ];
+ int n;
+ while ((n = in.read(tmp)) > 0)
+ diskOut.write(tmp, 0, n);
+ }
+
private Block last() {
return blocks.get(blocks.size() - 1);
}
--
1.6.1.rc2.306.ge5d5e
--
Shawn.
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders
2008-12-11 4:58 ` [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 5/5] Add HunkHeader to represent a single hunk of a file within a patch Shawn O. Pearce
@ 2008-12-11 18:34 ` Robin Rosenberg
2008-12-11 18:39 ` Shawn O. Pearce
1 sibling, 1 reply; 15+ messages in thread
From: Robin Rosenberg @ 2008-12-11 18:34 UTC (permalink / raw)
To: Shawn O. Pearce; +Cc: git
torsdag 11 december 2008 05:58:41 skrev Shawn O. Pearce:
> Most patch scripts impact more than one file at a time, so we need
> to support parsing multiple FileHeaders from the same input stream
> and collect them into a larger entity representing the entire script.
...
> + public void testParse_ConfigCaseInsensitive() throws IOException {
> + final Patch p = parseTestPatchFile();
> + assertEquals(2, p.getFiles().size());
> +
> + final FileHeader fRepositoryConfigTest = p.getFiles().get(0);
> + final FileHeader fRepositoryConfig = p.getFiles().get(1);
> +
> + assertEquals(
> + "org.spearce.jgit.test/tst/org/spearce/jgit/lib/RepositoryConfigTest.java",
> + fRepositoryConfigTest.getNewName());
> +
> + assertEquals(
> + "org.spearce.jgit/src/org/spearce/jgit/lib/RepositoryConfig.java",
> + fRepositoryConfig.getNewName());
> +
> + assertEquals(572, fRepositoryConfigTest.startOffset);
> + assertEquals(1490, fRepositoryConfig.startOffset);
1487 here
-- robin
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders
2008-12-11 18:34 ` [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders Robin Rosenberg
@ 2008-12-11 18:39 ` Shawn O. Pearce
2008-12-11 20:23 ` Robin Rosenberg
2008-12-11 20:39 ` Robin Rosenberg
0 siblings, 2 replies; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 18:39 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
Robin Rosenberg <robin.rosenberg.lists@dewire.com> wrote:
> > + assertEquals(572, fRepositoryConfigTest.startOffset);
> > + assertEquals(1490, fRepositoryConfig.startOffset);
>
> 1487 here
Really? 1490 is the only value that the test vector passes with.
What's the 3 bytes you think I'm off by?
--
Shawn.
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders
2008-12-11 18:39 ` Shawn O. Pearce
@ 2008-12-11 20:23 ` Robin Rosenberg
2008-12-11 20:27 ` Shawn O. Pearce
2008-12-11 20:39 ` Robin Rosenberg
1 sibling, 1 reply; 15+ messages in thread
From: Robin Rosenberg @ 2008-12-11 20:23 UTC (permalink / raw)
To: Shawn O. Pearce; +Cc: git
torsdag 11 december 2008 19:39:54 skrev Shawn O. Pearce:
> Robin Rosenberg <robin.rosenberg.lists@dewire.com> wrote:
> > > + assertEquals(572, fRepositoryConfigTest.startOffset);
> > > + assertEquals(1490, fRepositoryConfig.startOffset);
> >
> > 1487 here
>
> Really? 1490 is the only value that the test vector passes with.
> What's the 3 bytes you think I'm off by?
Ah, --whitespace=fix did that.
-- robin
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders
2008-12-11 20:23 ` Robin Rosenberg
@ 2008-12-11 20:27 ` Shawn O. Pearce
0 siblings, 0 replies; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 20:27 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
Robin Rosenberg <robin.rosenberg.lists@dewire.com> wrote:
> torsdag 11 december 2008 19:39:54 skrev Shawn O. Pearce:
> > Robin Rosenberg <robin.rosenberg.lists@dewire.com> wrote:
> > > > + assertEquals(572, fRepositoryConfigTest.startOffset);
> > > > + assertEquals(1490, fRepositoryConfig.startOffset);
> > >
> > > 1487 here
> >
> > Really? 1490 is the only value that the test vector passes with.
> > What's the 3 bytes you think I'm off by?
>
> Ah, --whitespace=fix did that.
Ok. I know you like to apply with --whitespace=fix, but I would
prefer to leave these *.patch test input files[*1*] exactly as they
were created by git format-patch or git diff, so I'm sure we are
parsing the same thing git would have produced and sent to us.
*1*: I have more patches coming which add 2 more test inputs
to the same PatchTest suite.
--
Shawn.
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders
2008-12-11 18:39 ` Shawn O. Pearce
2008-12-11 20:23 ` Robin Rosenberg
@ 2008-12-11 20:39 ` Robin Rosenberg
2008-12-11 20:41 ` Shawn O. Pearce
1 sibling, 1 reply; 15+ messages in thread
From: Robin Rosenberg @ 2008-12-11 20:39 UTC (permalink / raw)
To: Shawn O. Pearce; +Cc: git
torsdag 11 december 2008 19:39:54 skrev Shawn O. Pearce:
> Robin Rosenberg <robin.rosenberg.lists@dewire.com> wrote:
> > > + assertEquals(572, fRepositoryConfigTest.startOffset);
> > > + assertEquals(1490, fRepositoryConfig.startOffset);
> >
> > 1487 here
>
> Really? 1490 is the only value that the test vector passes with.
> What's the 3 bytes you think I'm off by?
...Quick, quick, find something else to pick on.. :->
Yes. Very little of the code in TemporaryBuffer is covered by the unit tests
and number of conditionals in there are rather large. I tried messing with
the constants in there to improve that and then PatchTest started to fail.
Here are the changes I tried with. I think it should still work with thes
changes. Rather than changing the other tests, we might want to create
a special test for only the buffer class.
-- robin
diff --git a/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java b/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
index 27f6444..556ab71 100644
--- a/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
+++ b/org.spearce.jgit/src/org/spearce/jgit/util/TemporaryBuffer.java
@@ -60,7 +60,7 @@
* after this stream has been properly closed by {@link #close()}.
*/
public class TemporaryBuffer extends OutputStream {
- private static final int DEFAULT_IN_CORE_LIMIT = 1024 * 1024;
+ private static final int DEFAULT_IN_CORE_LIMIT = 1024;
/** Chain of data, if we are still completely in-core; otherwise null. */
private ArrayList<Block> blocks;
@@ -315,7 +315,7 @@ public void destroy() {
}
private static class Block {
- static final int SZ = 8 * 1024;
+ static final int SZ = 512;
final byte[] buffer = new byte[SZ];
^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders
2008-12-11 20:39 ` Robin Rosenberg
@ 2008-12-11 20:41 ` Shawn O. Pearce
0 siblings, 0 replies; 15+ messages in thread
From: Shawn O. Pearce @ 2008-12-11 20:41 UTC (permalink / raw)
To: Robin Rosenberg; +Cc: git
Robin Rosenberg <robin.rosenberg.lists@dewire.com> wrote:
> ...Quick, quick, find something else to pick on.. :->
Heh, you'll get more patches again from me today, so there'll still
be more to pick on. :)
> Yes. Very little of the code in TemporaryBuffer is covered by the unit tests
> and number of conditionals in there are rather large. I tried messing with
> the constants in there to improve that and then PatchTest started to fail.
>
> Here are the changes I tried with. I think it should still work with thes
> changes. Rather than changing the other tests, we might want to create
> a special test for only the buffer class.
Ok. I was thinking the same thing actually, that I should spend a
bit of time today and try to get coverage on TemporaryBuffer. I'll
write a unit test for it.
--
Shawn.
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2008-12-11 20:43 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2008-12-11 4:58 [JGIT PATCH 0/5] Patch parsing API Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 1/5] Add toByteArray() to TemporaryBuffer Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 2/5] Add copy(InputStream) " Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 3/5] Define FileHeader to parse the header block of a git diff Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders Shawn O. Pearce
2008-12-11 4:58 ` [JGIT PATCH 5/5] Add HunkHeader to represent a single hunk of a file within a patch Shawn O. Pearce
2008-12-11 18:34 ` [JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders Robin Rosenberg
2008-12-11 18:39 ` Shawn O. Pearce
2008-12-11 20:23 ` Robin Rosenberg
2008-12-11 20:27 ` Shawn O. Pearce
2008-12-11 20:39 ` Robin Rosenberg
2008-12-11 20:41 ` Shawn O. Pearce
2008-12-11 15:40 ` [JGIT PATCH 2/5] Add copy(InputStream) to TemporaryBuffer Robin Rosenberg
2008-12-11 15:52 ` Shawn O. Pearce
2008-12-11 16:53 ` [JGIT PATCH 2/5 v2] " Shawn O. Pearce
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).