git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[JGIT PATCH 4/5] Define Patch to parse a sequence of patch FileHeaders

From
Shawn O. Pearce <spearce@spearce.org>
Date
Dec 11, 2008, 04:58 UTC
Message-ID
<1228971522-28764-5-git-send-email-spearce@spearce.org>
In-Reply-To
<1228971522-28764-4-git-send-email-spearce@spearce.org>

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 ++++++
 .../src/org/spearce/jgit/patch/FileHeader.java     |   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
+
diff --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
Previous: Shawn O. PearceNext: Shawn O. Pearce
Message 5 of 15 in “Patch parsing API”
  1. 0/5 Patch parsing APIShawn O. Pearce, Dec 11, 2008
  2. 1/5 Add toByteArray() to TemporaryBufferShawn O. Pearce, Dec 11, 2008
  3. 2/5 Add copy(InputStream) to TemporaryBufferShawn O. Pearce, Dec 11, 2008
  4. 3/5 Define FileHeader to parse the header block of a git diffShawn O. Pearce, Dec 11, 2008
  5. 4/5 Define Patch to parse a sequence of patch FileHeadersShawn O. Pearce, Dec 11, 2008
  6. 5/5 Add HunkHeader to represent a single hunk of a file within a patchShawn O. Pearce, Dec 11, 2008
  7. Robin RosenbergDec 11, 2008
  8. Shawn O. PearceDec 11, 2008
  9. Robin RosenbergDec 11, 2008
  10. Shawn O. PearceDec 11, 2008
  11. Robin RosenbergDec 11, 2008
  12. Shawn O. PearceDec 11, 2008
  13. Robin RosenbergDec 11, 2008
  14. Shawn O. PearceDec 11, 2008
  15. 2/5 Add copy(InputStream) to TemporaryBufferShawn O. Pearce, Dec 11, 2008

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.