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

[JGIT PATCH 3/5] Define FileHeader to parse the header block of a git diff

From
Shawn O. Pearce <spearce@spearce.org>
Date
Dec 11, 2008, 04:58 UTC
Message-ID
<1228971522-28764-4-git-send-email-spearce@spearce.org>
In-Reply-To
<1228971522-28764-3-git-send-email-spearce@spearce.org>
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>
---
 .../tst/org/spearce/jgit/patch/FileHeaderTest.java |  391 ++++++++++++++++++
 .../src/org/spearce/jgit/patch/FileHeader.java     |  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
diff --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";
+	}
+}
diff --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
Previous: Shawn O. PearceNext: Shawn O. Pearce
Message 4 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.