summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.test/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/.gitignore1
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/.gitignore1
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/.gitignore0
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/.gitignore1
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/test.stp0
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/.gitignore1
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/test.stp0
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/notignored0
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/.gitignore4
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/test.stp0
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/test.stp0
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreCacheTest.java404
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreMatcherTest.java364
-rw-r--r--org.eclipse.jgit/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java236
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreRule.java237
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/ignore/SimpleIgnoreCache.java308
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java31
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java26
21 files changed, 1616 insertions, 3 deletions
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 3b82cf7d1d..3aaa8a45e8 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -18,6 +18,7 @@ Import-Package: junit.framework;version="[3.8.2,4.0.0)",
org.eclipse.jgit.errors;version="[0.9.0,0.10.0)",
org.eclipse.jgit.fnmatch;version="[0.9.0,0.10.0)",
org.eclipse.jgit.http.server;version="[0.9.0,0.10.0)",
+ org.eclipse.jgit.ignore;version="[0.9.0,0.10.0)",
org.eclipse.jgit.iplog;version="[0.9.0,0.10.0)",
org.eclipse.jgit.junit;version="[0.9.0,0.10.0)",
org.eclipse.jgit.lib;version="[0.9.0,0.10.0)",
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/.gitignore b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/.gitignore
new file mode 100644
index 0000000000..b3f6bc97fb
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/.gitignore
@@ -0,0 +1 @@
+!/notignored
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/.gitignore b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/.gitignore
new file mode 100644
index 0000000000..09b8574b00
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/.gitignore
@@ -0,0 +1 @@
+notarealfile
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/.gitignore b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/.gitignore
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/.gitignore
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/.gitignore b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/.gitignore
new file mode 100644
index 0000000000..82b0f5d464
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/.gitignore
@@ -0,0 +1 @@
+/c
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/test.stp b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/test.stp
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/test.stp
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/.gitignore b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/.gitignore
new file mode 100644
index 0000000000..3c6cf10b1d
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/.gitignore
@@ -0,0 +1 @@
+/notarealfile2
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/test.stp b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/test.stp
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/test.stp
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/notignored b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/notignored
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/notignored
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/.gitignore b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/.gitignore
new file mode 100644
index 0000000000..b314092d16
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/.gitignore
@@ -0,0 +1,4 @@
+/*.st?
+!/test.stp
+!/a.c
+/a.c
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/test.stp b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/test.stp
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/test.stp
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/test.stp b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/test.stp
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/test.stp
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreCacheTest.java
new file mode 100644
index 0000000000..4083dcb09b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreCacheTest.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2010, Red Hat Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * 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 Eclipse Foundation, Inc. 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.eclipse.jgit.ignore;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import org.eclipse.jgit.lib.RepositoryTestCase;
+import org.eclipse.jgit.util.JGitTestUtil;
+
+/**
+ * Tests for the ignore cache
+ */
+public class IgnoreCacheTest extends RepositoryTestCase {
+
+ private File ignoreTestDir = JGitTestUtil.getTestResourceFile("excludeTest");
+ private SimpleIgnoreCache cache;
+ private final ArrayList<File> toDelete = new ArrayList<File>();
+
+ //TODO: Do not use OS dependent strings to encode file paths
+
+ public void tearDown() throws Exception {
+ super.tearDown();
+ deleteIgnoreFiles();
+ cache.clear();
+ toDelete.clear();
+ }
+
+ public void setUp() throws Exception {
+ super.setUp();
+ ignoreTestDir = JGitTestUtil.getTestResourceFile("excludeTest");
+ assertTrue("Test resource directory is not a directory",ignoreTestDir.isDirectory());
+
+ db = createWorkRepository();
+ recursiveCopy(ignoreTestDir, db.getDirectory().getParentFile());
+ cache = new SimpleIgnoreCache(db);
+ initCache();
+ }
+
+ protected void recursiveCopy(File src, File parent) throws IOException {
+ for (File file : src.listFiles()) {
+ String rel = file.getName();
+ File dst = new File(parent.toURI().resolve(rel));
+ copyFileOrDirectory(file, dst);
+ if (file.isDirectory())
+ recursiveCopy(file, dst);
+ }
+ }
+
+ protected static void copyFileOrDirectory(File src, File dst) throws IOException {
+ if (src.isDirectory())
+ dst.mkdir();
+ else
+ copyFile(src, dst);
+ }
+
+ public void testInitialization() {
+ File test = new File(db.getDirectory().getParentFile() + "/new/a/b1/test.stp");
+ assertTrue("Missing file " + test.getAbsolutePath(), test.exists());
+
+ /*
+ * Every folder along the path has a .gitignore file. Therefore every
+ * folder should have been added and initialized
+ */
+ boolean result = isIgnored(getRelativePath(test));
+ assertFalse("Unexpected match for " + test.toString(), result);
+
+ /*
+ * Check that every .gitignore along the path has been initialized
+ */
+ File folder = test.getParentFile();
+ IgnoreNode rules = null;
+ String fp = folder.getAbsolutePath();
+ while (!folder.equals(db.getDirectory().getParentFile()) && fp.length() > 0) {
+ rules = cache.getRules(getRelativePath(folder));
+ assertNotNull("Ignore file not initialized for " + fp, rules);
+ if (getRelativePath(folder).endsWith("new/a"))
+ //The /new/a directory has an empty ignore file
+ assertEquals("Ignore file not initialized for " + fp, 0, rules.getRules().size());
+ else
+ assertEquals("Ignore file not initialized for " + fp, 1, rules.getRules().size());
+
+ folder = folder.getParentFile();
+ fp = folder.getAbsolutePath();
+ }
+ if (rules != null)
+ assertEquals(1, rules.getRules().size());
+ else
+ fail("Base directory not initialized");
+
+ test = new File("/tmp/not/part/of/repo/path");
+ }
+
+ public void testRules() {
+ ignoreTestDir = JGitTestUtil.getTestResourceFile("excludeTest");
+ assertTrue("Test resource directory is not a directory", ignoreTestDir.isDirectory());
+ createExcludeFile();
+ initCache();
+
+ File test = new File(db.getDirectory().getParentFile(), "test.stp");
+ String path = test.getAbsolutePath();
+ assertTrue("Could not find test file " + path, test.exists());
+
+ IgnoreNode baseRules = cache.getRules("");
+ assertNotNull("Could not find base rules", baseRules);
+
+ /*
+ * .git/info/excludes:
+ * /test.stp
+ * /notignored
+ *
+ * new/.gitignore:
+ * notarealfile
+ *
+ * new/a/.gitignore:
+ * <empty>
+ *
+ * new/a/b2/.gitignore:
+ * <does not exist>
+ *
+ * new/a/b1/.gitignore:
+ * /c
+ *
+ * new/a/b1/c/.gitignore:
+ * !/shouldbeignored.txt
+ *
+ * .gitignore:
+ * !/notignored
+ * /commentNotIgnored.tx#t
+ * /commentIgnored.txt#comment
+ * /commentIgnored.txt #comment
+ */
+ boolean result = isIgnored(getRelativePath(test));
+ assertEquals(3, baseRules.getRules().size());
+ assertTrue(db.getDirectory().getParentFile().toURI().equals(baseRules.getBaseDir().toURI()));
+ //Test basic exclude file
+ assertTrue("Did not match file " + test.toString(), result);
+ //Test exclude file priority
+ assertNotIgnored("notignored");
+ //Test that /src/test.stp is not matched by /test.stp in exclude file (Do not reinitialize)
+ assertNotIgnored("/src/test.stp");
+ //Test file that is not mentioned -- should just return unmatched
+ assertNotIgnored("not/mentioned/file.txt");
+
+ //Test adding nonexistent node
+ test = new File(db.getDirectory().getParentFile(), "new/a/b2/d/test.stp");
+ assertNotIgnored("new/a/b2/d/test.stp");
+ assertNotIgnored("new/a/b2/d/");
+ assertNotIgnored("new/a/b2/d");
+
+ //Test folder
+ test = new File(db.getDirectory().getParentFile(), "new/a/b1/c");
+ assertIgnored("new/a/b1/c");
+ assertIgnored("new/a/b1/c/anything.c");
+ assertIgnored("new/a/b1/c/and.o");
+ assertIgnored("new/a/b1/c/everything.d");
+ assertIgnored("new/a/b1/c/everything.d");
+ //Special case -- the normally higher priority negation in c/.gitignore is cancelled by the folder being ignored
+ assertIgnored("new/a/b1/c/shouldbeignored.txt");
+
+ //Test name-only (use non-existent folders)
+ assertNotIgnored("notarealfile");
+ assertNotIgnored("/notarealfile");
+ assertIgnored("new/notarealfile");
+ assertIgnored("new/notarealfile/fake");
+ assertIgnored("new/a/notarealfile");
+ assertIgnored("new/a/b1/notarealfile");
+
+ //Test clearing node -- create empty .gitignore
+ createIgnoreFile(db.getDirectory().getParentFile() + "/new/a/b2/.gitignore", new String[0]);
+ test = new File(db.getDirectory().getParentFile(), "new/a/b2/c");
+ initCache();
+ baseRules = cache.getRules("new/a/b2");
+ assertNotNull(baseRules);
+ baseRules.clear();
+ assertEquals(baseRules.getRules().size(), 0);
+ try {
+ assertFalse("Node not properly cleared", baseRules.isIgnored(getRelativePath(test)));
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail("IO exception when testing base rules");
+ }
+
+ //Test clearing entire cache, and isEmpty
+ assertNotNull(cache.getRules(""));
+ assertFalse(cache.isEmpty());
+ cache.clear();
+ assertNull(cache.getRules(""));
+ assertTrue(cache.isEmpty());
+ assertNotIgnored("/anything");
+ assertNotIgnored("/new/anything");
+ assertNotIgnored("/src/anything");
+ }
+
+ public void testPriorities() {
+ ignoreTestDir = JGitTestUtil.getTestResourceFile("excludeTest");
+ assertTrue("Test resource directory is not a directory",ignoreTestDir.isDirectory());
+ createExcludeFile();
+ initCache();
+
+ File test = new File(db.getDirectory().getParentFile(), "/src/test.stp");
+ assertTrue("Resource file " + test.getName() + " is missing", test.exists());
+
+ //Test basic exclude file
+ IgnoreNode node = cache.getRules("src");
+ assertNotNull("Excludes file was not initialized", node);
+
+ /*
+ * src/.gitignore:
+ * /*.st?
+ * !/test.stp
+ * !/a.c
+ * /a.c
+ *
+ * ./.gitignore:
+ * !/notignored
+ *
+ * .git/info/exclude:
+ * /test.stp
+ * /notignored
+ */
+ assertIgnored("src/a.c");
+ assertIgnored("test.stp");
+ assertIgnored("src/blank.stp");
+ assertNotIgnored("notignored");
+ assertNotIgnored("src/test.stp");
+
+ assertEquals(4, node.getRules().size());
+
+ /*
+ * new/.gitignore:
+ * notarealfile
+ *
+ * new/a/.gitignore:
+ * <empty>
+ *
+ * new/a/b2/.gitignore:
+ * <does not exist>
+ *
+ * new/a/b2/c/.gitignore:
+ * /notarealfile2
+ */
+ assertIgnored("new/a/b2/c/notarealfile2");
+ assertIgnored("new/notarealfile");
+ assertIgnored("new/a/notarealfile");
+ assertNotIgnored("new/a/b2/c/test.stp");
+ assertNotIgnored("new/a/b2/c");
+ assertNotIgnored("new/a/b2/nonexistent");
+ }
+
+ /**
+ * Check if a file is not matched as ignored
+ * @param relativePath
+ * Path to file, relative to db.getDirectory. Use "/" as a separator,
+ * this method will replace all instances of "/" with File.separator
+ */
+ private void assertNotIgnored(String relativePath) {
+ File test = new File(db.getDirectory().getParentFile(), relativePath);
+ assertFalse("Should not match " + test.toString(), isIgnored(getRelativePath(test)));
+ }
+
+ /**
+ * Check if a file is matched as ignored
+ * @param relativePath
+ * Path to file, relative to db.getDirectory. Use "/" as a separator,
+ * this method will replace all instances of "/" with File.separator.
+ */
+ private void assertIgnored(String relativePath) {
+ File test = new File(db.getDirectory().getParentFile(), relativePath);
+ assertTrue("Failed to match " + test.toString(), isIgnored(getRelativePath(test)));
+ }
+
+ /**
+ * Attempt to write an ignore file at the given location
+ * @param path
+ * Will create file at this path
+ * @param contents
+ * Each entry in contents will be entered on its own line
+ */
+ private void createIgnoreFile(String path, String[] contents) {
+ File ignoreFile = new File(path);
+ ignoreFile.delete();
+ ignoreFile.deleteOnExit(); //Hope to catch in the event of crash
+ toDelete.add(ignoreFile); //For teardown purposes
+
+ //Jump through some hoops to create the exclude file
+ try {
+ if (!ignoreFile.createNewFile())
+ fail("Could not create ignore file" + ignoreFile.getAbsolutePath());
+
+ BufferedWriter bw = new BufferedWriter(new FileWriter (ignoreFile));
+ for (String s : contents)
+ bw.write(s + System.getProperty("line.separator"));
+ bw.flush();
+ bw.close();
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ fail("Could not create exclude file");
+ }
+ }
+
+ private void createExcludeFile() {
+ String[] content = new String[2];
+ content[0] = "/test.stp";
+ content[1] = "/notignored";
+
+ //We can do this because we explicitly delete parent directories later in deleteIgnoreFiles.
+ File parent= new File(db.getDirectory().getParentFile(), ".git/info");
+ if (!parent.exists())
+ parent.mkdirs();
+
+ createIgnoreFile(db.getDirectory().getParentFile() + "/.git/info/exclude", content);
+ }
+
+ private void deleteIgnoreFiles() {
+ for (File f : toDelete)
+ f.delete();
+
+ //Systematically delete exclude parent dirs
+ File f = new File(ignoreTestDir.getAbsoluteFile(), ".git/info");
+ f.delete();
+ f = new File(ignoreTestDir.getAbsoluteFile(), ".git");
+ f.delete();
+ }
+
+ /**
+ * @param path
+ * Filepath relative to the git directory
+ * @return
+ * Results of cache.isIgnored(path) -- true if ignored, false if
+ * a negation is encountered or if no rules apply
+ */
+ private boolean isIgnored(String path) {
+ try {
+ return cache.isIgnored(path);
+ } catch (IOException e) {
+ fail("IOException when attempting to check ignored status");
+ }
+ return false;
+ }
+
+ private String getRelativePath(File file) {
+ String retVal = db.getDirectory().getParentFile().toURI().relativize(file.toURI()).getPath();
+ if (retVal.length() == file.getAbsolutePath().length())
+ fail("Not a child of the git directory");
+ if (retVal.endsWith("/"))
+ retVal = retVal.substring(0, retVal.length() - 1);
+
+ return retVal;
+ }
+
+ private void initCache() {
+ try {
+ cache.initialize();
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail("Could not initialize cache");
+ }
+ }
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreMatcherTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreMatcherTest.java
new file mode 100644
index 0000000000..cacad75558
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreMatcherTest.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2010, Red Hat Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * 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 Eclipse Foundation, Inc. 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.eclipse.jgit.ignore;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+
+/**
+ * Tests ignore pattern matches
+ */
+public class IgnoreMatcherTest extends TestCase{
+
+ public void testBasic() {
+ String pattern = "/test.stp";
+ assertMatched(pattern, "/test.stp");
+
+ pattern = "#/test.stp";
+ assertNotMatched(pattern, "/test.stp");
+ }
+
+ public void testFileNameWildcards() {
+ //Test basic * and ? for any pattern + any character
+ String pattern = "*.st?";
+ assertMatched(pattern, "/test.stp");
+ assertMatched(pattern, "/anothertest.stg");
+ assertMatched(pattern, "/anothertest.st0");
+ assertNotMatched(pattern, "/anothertest.sta1");
+ //Check that asterisk does not expand to "/"
+ assertNotMatched(pattern, "/another/test.sta1");
+
+ //Same as above, with a leading slash to ensure that doesn't cause problems
+ pattern = "/*.st?";
+ assertMatched(pattern, "/test.stp");
+ assertMatched(pattern, "/anothertest.stg");
+ assertMatched(pattern, "/anothertest.st0");
+ assertNotMatched(pattern, "/anothertest.sta1");
+ //Check that asterisk does not expand to "/"
+ assertNotMatched(pattern, "/another/test.sta1");
+
+ //Test for numbers
+ pattern = "*.sta[0-5]";
+ assertMatched(pattern, "/test.sta5");
+ assertMatched(pattern, "/test.sta4");
+ assertMatched(pattern, "/test.sta3");
+ assertMatched(pattern, "/test.sta2");
+ assertMatched(pattern, "/test.sta1");
+ assertMatched(pattern, "/test.sta0");
+ assertMatched(pattern, "/anothertest.sta2");
+ assertNotMatched(pattern, "test.stag");
+ assertNotMatched(pattern, "test.sta6");
+
+ //Test for letters
+ pattern = "/[tv]est.sta[a-d]";
+ assertMatched(pattern, "/test.staa");
+ assertMatched(pattern, "/test.stab");
+ assertMatched(pattern, "/test.stac");
+ assertMatched(pattern, "/test.stad");
+ assertMatched(pattern, "/vest.stac");
+ assertNotMatched(pattern, "test.stae");
+ assertNotMatched(pattern, "test.sta9");
+
+ //Test child directory/file is matched
+ pattern = "/src/ne?";
+ assertMatched(pattern, "/src/new/");
+ assertMatched(pattern, "/src/new");
+ assertMatched(pattern, "/src/new/a.c");
+ assertMatched(pattern, "/src/new/a/a.c");
+ assertNotMatched(pattern, "/src/new.c");
+
+ //Test name-only fnmatcher matches
+ pattern = "ne?";
+ assertMatched(pattern, "/src/new/");
+ assertMatched(pattern, "/src/new");
+ assertMatched(pattern, "/src/new/a.c");
+ assertMatched(pattern, "/src/new/a/a.c");
+ assertMatched(pattern, "/neb");
+ assertNotMatched(pattern, "/src/new.c");
+ }
+
+ public void testTargetWithoutLeadingSlash() {
+ //Test basic * and ? for any pattern + any character
+ String pattern = "/*.st?";
+ assertMatched(pattern, "test.stp");
+ assertMatched(pattern, "anothertest.stg");
+ assertMatched(pattern, "anothertest.st0");
+ assertNotMatched(pattern, "anothertest.sta1");
+ //Check that asterisk does not expand to ""
+ assertNotMatched(pattern, "another/test.sta1");
+
+ //Same as above, with a leading slash to ensure that doesn't cause problems
+ pattern = "/*.st?";
+ assertMatched(pattern, "test.stp");
+ assertMatched(pattern, "anothertest.stg");
+ assertMatched(pattern, "anothertest.st0");
+ assertNotMatched(pattern, "anothertest.sta1");
+ //Check that asterisk does not expand to ""
+ assertNotMatched(pattern, "another/test.sta1");
+
+ //Test for numbers
+ pattern = "/*.sta[0-5]";
+ assertMatched(pattern, "test.sta5");
+ assertMatched(pattern, "test.sta4");
+ assertMatched(pattern, "test.sta3");
+ assertMatched(pattern, "test.sta2");
+ assertMatched(pattern, "test.sta1");
+ assertMatched(pattern, "test.sta0");
+ assertMatched(pattern, "anothertest.sta2");
+ assertNotMatched(pattern, "test.stag");
+ assertNotMatched(pattern, "test.sta6");
+
+ //Test for letters
+ pattern = "/[tv]est.sta[a-d]";
+ assertMatched(pattern, "test.staa");
+ assertMatched(pattern, "test.stab");
+ assertMatched(pattern, "test.stac");
+ assertMatched(pattern, "test.stad");
+ assertMatched(pattern, "vest.stac");
+ assertNotMatched(pattern, "test.stae");
+ assertNotMatched(pattern, "test.sta9");
+
+ //Test child directory/file is matched
+ pattern = "/src/ne?";
+ assertMatched(pattern, "src/new/");
+ assertMatched(pattern, "src/new");
+ assertMatched(pattern, "src/new/a.c");
+ assertMatched(pattern, "src/new/a/a.c");
+ assertNotMatched(pattern, "src/new.c");
+
+ //Test name-only fnmatcher matches
+ pattern = "ne?";
+ assertMatched(pattern, "src/new/");
+ assertMatched(pattern, "src/new");
+ assertMatched(pattern, "src/new/a.c");
+ assertMatched(pattern, "src/new/a/a.c");
+ assertMatched(pattern, "neb");
+ assertNotMatched(pattern, "src/new.c");
+ }
+
+ public void testParentDirectoryGitIgnores() {
+ //Contains git ignore patterns such as might be seen in a parent directory
+
+ //Test for wildcards
+ String pattern = "/*/*.c";
+ assertMatched(pattern, "/file/a.c");
+ assertMatched(pattern, "/src/a.c");
+ assertNotMatched(pattern, "/src/new/a.c");
+
+ //Test child directory/file is matched
+ pattern = "/src/new";
+ assertMatched(pattern, "/src/new/");
+ assertMatched(pattern, "/src/new");
+ assertMatched(pattern, "/src/new/a.c");
+ assertMatched(pattern, "/src/new/a/a.c");
+ assertNotMatched(pattern, "/src/new.c");
+
+ //Test child directory is matched, slash after name
+ pattern = "/src/new/";
+ assertMatched(pattern, "/src/new/");
+ assertMatched(pattern, "/src/new/a.c");
+ assertMatched(pattern, "/src/new/a/a.c");
+ assertNotMatched(pattern, "/src/new");
+ assertNotMatched(pattern, "/src/new.c");
+
+ //Test directory is matched by name only
+ pattern = "b1";
+ assertMatched(pattern, "/src/new/a/b1/a.c");
+ assertNotMatched(pattern, "/src/new/a/b2/file.c");
+ assertNotMatched(pattern, "/src/new/a/bb1/file.c");
+ assertNotMatched(pattern, "/src/new/a/file.c");
+ }
+
+ public void testTrailingSlash() {
+ String pattern = "/src/";
+ assertMatched(pattern, "/src/");
+ assertMatched(pattern, "/src/new");
+ assertMatched(pattern, "/src/new/a.c");
+ assertMatched(pattern, "/src/a.c");
+ assertNotMatched(pattern, "/src");
+ assertNotMatched(pattern, "/srcA/");
+ }
+
+ public void testNameOnlyMatches() {
+ /*
+ * Name-only matches do not contain any path separators
+ */
+ //Test matches for file extension
+ String pattern = "*.stp";
+ assertMatched(pattern, "/test.stp");
+ assertMatched(pattern, "/src/test.stp");
+ assertNotMatched(pattern, "/test.stp1");
+ assertNotMatched(pattern, "/test.astp");
+
+ //Test matches for name-only, applies to file name or folder name
+ pattern = "src";
+ assertMatched(pattern, "/src/a.c");
+ assertMatched(pattern, "/src/new/a.c");
+ assertMatched(pattern, "/new/src/a.c");
+ assertMatched(pattern, "/file/src");
+ assertMatched(pattern, "/src/");
+
+ //Test matches for name-only, applies to file name or folder name
+ //With a small wildcard
+ pattern = "?rc";
+ assertMatched(pattern, "/src/a.c");
+ assertMatched(pattern, "/src/new/a.c");
+ assertMatched(pattern, "/new/src/a.c");
+ assertMatched(pattern, "/file/src");
+ assertMatched(pattern, "/src/");
+
+ //Test matches for name-only, applies to file name or folder name
+ //With a small wildcard
+ pattern = "?r[a-c]";
+ assertMatched(pattern, "/src/a.c");
+ assertMatched(pattern, "/src/new/a.c");
+ assertMatched(pattern, "/new/src/a.c");
+ assertMatched(pattern, "/file/src");
+ assertMatched(pattern, "/src/");
+ assertMatched(pattern, "/srb/a.c");
+ assertMatched(pattern, "/grb/new/a.c");
+ assertMatched(pattern, "/new/crb/a.c");
+ assertMatched(pattern, "/file/3rb");
+ assertMatched(pattern, "/xrb/");
+ assertMatched(pattern, "/3ra/a.c");
+ assertMatched(pattern, "/5ra/new/a.c");
+ assertMatched(pattern, "/new/1ra/a.c");
+ assertMatched(pattern, "/file/dra");
+ assertMatched(pattern, "/era/");
+ assertNotMatched(pattern, "/crg");
+ assertNotMatched(pattern, "/cr3");
+ }
+
+ public void testNegation() {
+ String pattern = "!/test.stp";
+ assertMatched(pattern, "/test.stp");
+ }
+
+ public void testGetters() {
+ IgnoreRule r = new IgnoreRule("/pattern/");
+ assertFalse(r.getNameOnly());
+ assertTrue(r.dirOnly());
+ assertFalse(r.getNegation());
+ assertEquals(r.getPattern(), "/pattern");
+
+ r = new IgnoreRule("/patter?/");
+ assertFalse(r.getNameOnly());
+ assertTrue(r.dirOnly());
+ assertFalse(r.getNegation());
+ assertEquals(r.getPattern(), "/patter?");
+
+ r = new IgnoreRule("patt*");
+ assertTrue(r.getNameOnly());
+ assertFalse(r.dirOnly());
+ assertFalse(r.getNegation());
+ assertEquals(r.getPattern(), "patt*");
+
+ r = new IgnoreRule("pattern");
+ assertTrue(r.getNameOnly());
+ assertFalse(r.dirOnly());
+ assertFalse(r.getNegation());
+ assertEquals(r.getPattern(), "pattern");
+
+ r = new IgnoreRule("!pattern");
+ assertTrue(r.getNameOnly());
+ assertFalse(r.dirOnly());
+ assertTrue(r.getNegation());
+ assertEquals(r.getPattern(), "pattern");
+
+ r = new IgnoreRule("!/pattern");
+ assertFalse(r.getNameOnly());
+ assertFalse(r.dirOnly());
+ assertTrue(r.getNegation());
+ assertEquals(r.getPattern(), "/pattern");
+
+ r = new IgnoreRule("!/patter?");
+ assertFalse(r.getNameOnly());
+ assertFalse(r.dirOnly());
+ assertTrue(r.getNegation());
+ assertEquals(r.getPattern(), "/patter?");
+ }
+
+ /**
+ * Check for a match. If target ends with "/", match will assume that the
+ * target is meant to be a directory.
+ * @param pattern
+ * Pattern as it would appear in a .gitignore file
+ * @param target
+ * Target file path relative to repository's GIT_DIR
+ */
+ public void assertMatched(String pattern, String target) {
+ boolean value = match(pattern, target);
+ Assert.assertTrue("Expected a match for: " + pattern + " with: " + target, value);
+ }
+
+ /**
+ * Check for a match. If target ends with "/", match will assume that the
+ * target is meant to be a directory.
+ * @param pattern
+ * Pattern as it would appear in a .gitignore file
+ * @param target
+ * Target file path relative to repository's GIT_DIR
+ */
+ public void assertNotMatched(String pattern, String target) {
+ boolean value = match(pattern, target);
+ Assert.assertFalse("Expected no match for: " + pattern + " with: " + target, value);
+ }
+
+ /**
+ * Check for a match. If target ends with "/", match will assume that the
+ * target is meant to be a directory.
+ * @param pattern
+ * Pattern as it would appear in a .gitignore file
+ * @param target
+ * Target file path relative to repository's GIT_DIR
+ * @return
+ * Result of {@link IgnoreRule#isMatch(String, boolean)}
+ */
+ private boolean match(String pattern, String target) {
+ IgnoreRule r = new IgnoreRule(pattern);
+ //If speed of this test is ever an issue, we can use a presetRule field
+ //to avoid recompiling a pattern each time.
+ return r.isMatch(target, target.endsWith("/"));
+ }
+}
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index 258e6781b5..51d440e58e 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -11,6 +11,7 @@ Export-Package: org.eclipse.jgit;version="0.9.0",
org.eclipse.jgit.dircache;version="0.9.0",
org.eclipse.jgit.errors;version="0.9.0",
org.eclipse.jgit.fnmatch;version="0.9.0",
+ org.eclipse.jgit.ignore;version="0.9.0",
org.eclipse.jgit.lib;version="0.9.0",
org.eclipse.jgit.merge;version="0.9.0",
org.eclipse.jgit.nls;version="0.9.0",
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java
new file mode 100644
index 0000000000..f29fa1e015
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2010, Red Hat Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * 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 Eclipse Foundation, Inc. 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.eclipse.jgit.ignore;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Represents a bundle of ignore rules inherited from a base directory.
+ * Each IgnoreNode corresponds to one directory. Most IgnoreNodes will have
+ * at most one source of ignore information -- its .gitignore file.
+ * <br><br>
+ * At the root of the repository, there may be an additional source of
+ * ignore information (the exclude file)
+ * <br><br>
+ * It is recommended that implementers call the {@link #isIgnored(String)} method
+ * rather than try to use the rules manually. The method will handle rule priority
+ * automatically.
+ *
+ */
+public class IgnoreNode {
+ //The base directory will be used to find the .gitignore file
+ private File baseDir;
+ //Only used for root node.
+ private File secondaryFile;
+ private ArrayList<IgnoreRule> rules;
+ //Indicates whether a match was made. Necessary to terminate early when a negation is encountered
+ private boolean matched;
+ //Indicates whether a match was made. Necessary to terminate early when a negation is encountered
+ private long lastModified;
+
+ /**
+ * Create a new ignore node based on the given directory. The node's
+ * ignore file will be the .gitignore file in the directory (if any)
+ * Rules contained within this node will only be applied to files
+ * which are descendants of this directory.
+ *
+ * @param baseDir
+ * base directory of this ignore node
+ */
+ public IgnoreNode(File baseDir) {
+ this.baseDir = baseDir;
+ rules = new ArrayList<IgnoreRule>();
+ secondaryFile = null;
+ lastModified = 0l;
+ }
+
+ /**
+ * Parse files according to gitignore standards.
+ *
+ * @throws IOException
+ * Error thrown when reading an ignore file.
+ */
+ private void parse() throws IOException {
+ if (secondaryFile != null && secondaryFile.exists())
+ parse(secondaryFile);
+
+ parse(new File(baseDir.getAbsolutePath(), ".gitignore"));
+ }
+
+ private void parse(File targetFile) throws IOException {
+ if (!targetFile.exists())
+ return;
+
+ BufferedReader br = new BufferedReader(new FileReader(targetFile));
+ String txt;
+ try {
+ while ((txt = br.readLine()) != null) {
+ txt = txt.trim();
+ if (txt.length() > 0 && !txt.startsWith("#"))
+ rules.add(new IgnoreRule(txt));
+ }
+ } finally {
+ br.close();
+ }
+ }
+
+ /**
+ * @return
+ * Base directory to which these rules apply, absolute path
+ */
+ public File getBaseDir() {
+ return baseDir;
+ }
+
+
+ /**
+ *
+ * @return
+ * List of all ignore rules held by this node
+ */
+ public ArrayList<IgnoreRule> getRules() {
+ return rules;
+ }
+
+
+ /**
+ *
+ * Returns whether or not a target is matched as being ignored by
+ * any patterns in this directory.
+ * <br>
+ * Will return false if the file is not a descendant of this directory.
+ * <br>
+ *
+ * @param target
+ * Absolute path to the file. This makes stripping common path elements easier.
+ * @return
+ * true if target is ignored, false if the target is explicitly not
+ * ignored or if no rules exist for the target.
+ * @throws IOException
+ * Failed to parse rules
+ *
+ */
+ public boolean isIgnored(String target) throws IOException {
+ matched = false;
+ File targetFile = new File(target);
+ String tar = baseDir.toURI().relativize(targetFile.toURI()).getPath();
+
+ if (tar.length() == target.length())
+ //target is not a derivative of baseDir, this node has no jurisdiction
+ return false;
+
+ if (rules.isEmpty()) {
+ //Either we haven't parsed yet, or the file is empty.
+ //Empty file should be very fast to parse
+ parse();
+ }
+ if (rules.isEmpty())
+ return false;
+
+ /*
+ * Boolean matched is necessary because we may have encountered
+ * a negation ("!/test.c").
+ */
+
+ int i;
+ //Parse rules in the reverse order that they were read
+ for (i = rules.size() -1; i > -1; i--) {
+ matched = rules.get(i).isMatch(tar, targetFile.isDirectory());
+ if (matched)
+ break;
+ }
+
+ if (i > -1 && rules.get(i) != null)
+ return rules.get(i).getResult();
+
+ return false;
+ }
+
+ /**
+ * @return
+ * True if the previous call to isIgnored resulted in a match,
+ * false otherwise.
+ */
+ public boolean wasMatched() {
+ return matched;
+ }
+
+ /**
+ * Adds another file as a source of ignore rules for this file. The
+ * secondary file will have a lower priority than the first file, and
+ * the parent directory of this node will be regarded as firstFile.getParent()
+ *
+ * @param f
+ * Secondary source of gitignore information for this node
+ */
+ public void addSecondarySource(File f) {
+ secondaryFile = f;
+ }
+
+ /**
+ * Clear all rules in this node.
+ */
+ public void clear() {
+ rules.clear();
+ }
+
+ /**
+ * @param val
+ * Set the last modified time of this node.
+ */
+ public void setLastModified(long val) {
+ lastModified = val;
+ }
+
+ /**
+ * @return
+ * Last modified time of this node.
+ */
+ public long getLastModified() {
+ return lastModified;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreRule.java
new file mode 100644
index 0000000000..982ce06c66
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreRule.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2010, Red Hat Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * 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 Eclipse Foundation, Inc. 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.eclipse.jgit.ignore;
+
+import org.eclipse.jgit.errors.InvalidPatternException;
+import org.eclipse.jgit.fnmatch.FileNameMatcher;
+
+/**
+ * A single ignore rule corresponding to one line in a .gitignore or
+ * ignore file. Parses the ignore pattern
+ *
+ * Inspiration from: Ferry Huberts
+ */
+public class IgnoreRule {
+ private String pattern;
+ private boolean negation;
+ private boolean nameOnly;
+ private boolean dirOnly;
+ private FileNameMatcher matcher;
+
+ /**
+ * Create a new ignore rule with the given pattern. Assumes that
+ * the pattern is already trimmed.
+ *
+ * @param pattern
+ * Base pattern for the ignore rule. This pattern will
+ * be parsed to generate rule parameters.
+ */
+ public IgnoreRule (String pattern) {
+ this.pattern = pattern;
+ negation = false;
+ nameOnly = false;
+ dirOnly = false;
+ matcher = null;
+ setup();
+ }
+
+ /**
+ * Remove leading/trailing characters as needed. Set up
+ * rule variables for later matching.
+ */
+ private void setup() {
+ int startIndex = 0;
+ int endIndex = pattern.length();
+ if (pattern.startsWith("!")) {
+ startIndex++;
+ negation = true;
+ }
+
+ if (pattern.endsWith("/")) {
+ endIndex --;
+ dirOnly = true;
+ }
+
+ pattern = pattern.substring(startIndex, endIndex);
+
+ if (!pattern.contains("/"))
+ nameOnly = true;
+ else if (!pattern.startsWith("/")) {
+ //Contains "/" but does not start with one
+ //Adding / to the start should not interfere with matching
+ pattern = "/" + pattern;
+ }
+
+ if (pattern.contains("*") || pattern.contains("?") || pattern.contains("[")) {
+ try {
+ matcher = new FileNameMatcher(pattern, new Character('/'));
+ } catch (InvalidPatternException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+
+ /**
+ * @return
+ * True if the pattern is just a file name and not a path
+ */
+ public boolean getNameOnly() {
+ return nameOnly;
+ }
+
+ /**
+ *
+ * @return
+ * True if the pattern should match directories only
+ */
+ public boolean dirOnly() {
+ return dirOnly;
+ }
+
+ /**
+ *
+ * @return
+ * True if the pattern had a "!" in front of it
+ */
+ public boolean getNegation() {
+ return negation;
+ }
+
+ /**
+ * @return
+ * The blob pattern to be used as a matcher
+ */
+ public String getPattern() {
+ return pattern;
+ }
+
+ /**
+ * Returns true if a match was made.
+ * <br>
+ * This function does NOT return the actual ignore status of the
+ * target! Please consult {@link #getResult()} for the ignore status. The actual
+ * ignore status may be true or false depending on whether this rule is
+ * an ignore rule or a negation rule.
+ *
+ * @param target
+ * Name pattern of the file, relative to the base directory of this rule
+ * @param isDirectory
+ * Whether the target file is a directory or not
+ * @return
+ * True if a match was made. This does not necessarily mean that
+ * the target is ignored. Call {@link IgnoreRule#getResult() getResult()} for the result.
+ */
+ public boolean isMatch(String target, boolean isDirectory) {
+ if (!target.startsWith("/"))
+ target = "/" + target;
+
+ if (matcher == null) {
+ if (target.equals(pattern)) {
+ //Exact match
+ if (dirOnly && !isDirectory)
+ //Directory expectations not met
+ return false;
+ else
+ //Directory expectations met
+ return true;
+ }
+
+ /*
+ * Add slashes for startsWith check. This avoids matching e.g.
+ * "/src/new" to /src/newfile" but allows "/src/new" to match
+ * "/src/new/newfile", as is the git standard
+ */
+ if ((target).startsWith(pattern + "/"))
+ return true;
+
+ if (nameOnly) {
+ //Iterate through each sub-name
+ for (String folderName : target.split("/")) {
+ if (folderName.equals(pattern))
+ return true;
+ }
+ }
+
+ } else {
+ matcher.append(target);
+ if (matcher.isMatch())
+ return true;
+
+ if (nameOnly) {
+ for (String folderName : target.split("/")) {
+ //Iterate through each sub-directory
+ matcher.reset();
+ matcher.append(folderName);
+ if (matcher.isMatch())
+ return true;
+ }
+ } else {
+ //TODO: This is the slowest operation
+ //This matches e.g. "/src/ne?" to "/src/new/file.c"
+ matcher.reset();
+ for (String folderName : target.split("/")) {
+ if (folderName.length() > 0)
+ matcher.append("/" + folderName);
+
+ if (matcher.isMatch())
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * If a call to <code>isMatch(String, boolean)</code> was previously
+ * made, this will return whether or not the target was ignored. Otherwise
+ * this just indicates whether the rule is non-negation or negation.
+ *
+ * @return
+ * True if the target is to be ignored, false otherwise.
+ */
+ public boolean getResult() {
+ return !negation;
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/SimpleIgnoreCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/SimpleIgnoreCache.java
new file mode 100644
index 0000000000..be37a9ad8d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/SimpleIgnoreCache.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2010, Red Hat Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * 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 Eclipse Foundation, Inc. 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.eclipse.jgit.ignore;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.HashSet;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.FS;
+
+/**
+ * A simple ignore cache. Stores ignore information on .gitignore and exclude files.
+ * <br><br>
+ * The cache can be initialized by calling {@link #initialize()} on a
+ * target file.
+ *
+ * Inspiration from: Ferry Huberts
+ */
+public class SimpleIgnoreCache {
+
+ /**
+ * Map of ignore nodes, indexed by base directory. By convention, the
+ * base directory string should NOT start or end with a "/". Use
+ * {@link #relativize(File)} before appending nodes to the ignoreMap
+ * <br>
+ * e.g: path/to/directory is a valid String
+ */
+ private HashMap<String, IgnoreNode> ignoreMap;
+
+ //Repository associated with this cache
+ private Repository repository;
+
+ //Base directory of this cache
+ private URI rootFileURI;
+
+ /**
+ * Creates a base implementation of an ignore cache. This default implementation
+ * will search for all .gitignore files in all children of the base directory,
+ * and grab the exclude file from baseDir/.git/info/exclude.
+ * <br><br>
+ * Call {@link #initialize()} to fetch the ignore information relevant
+ * to a target file.
+ * @param repository
+ * Repository to associate this cache with. The cache's base directory will
+ * be set to this repository's GIT_DIR
+ *
+ */
+ public SimpleIgnoreCache(Repository repository) {
+ ignoreMap = new HashMap<String, IgnoreNode>();
+ this.repository = repository;
+ this.rootFileURI = repository.getWorkDir().toURI();
+ }
+
+ /**
+ * Initializes the ignore map for the target file and all parents.
+ * This will delete existing ignore information for all folders
+ * on the partial initialization path. Will only function for files
+ * that are children of the cache's basePath.
+ * <br><br>
+ * Note that this does not initialize the ignore rules. Ignore rules will
+ * be parsed when needed during a call to {@link #isIgnored(String)}
+ *
+ * @throws IOException
+ * The tree could not be walked.
+ */
+ public void initialize() throws IOException {
+ TreeWalk tw = new TreeWalk(repository);
+ tw.reset();
+ tw.addTree(new FileTreeIterator(repository.getWorkDir(), FS.DETECTED));
+ tw.setRecursive(true);
+
+ //Don't waste time trying to add iterators that already exist
+ HashSet<FileTreeIterator> toAdd = new HashSet<FileTreeIterator>();
+ while (tw.next()) {
+ FileTreeIterator t = tw.getTree(0, FileTreeIterator.class);
+ if (t.hasGitIgnore()) {
+ toAdd.add(t);
+ //TODO: Account for and test the removal of .gitignore files
+ }
+ }
+ for (FileTreeIterator t : toAdd)
+ addNodeFromTree(t);
+
+ //The base is special
+ //TODO: Test alternate locations for GIT_DIR
+ readRulesAtBase();
+ }
+
+ /**
+ * Creates rules for .git/info/exclude and .gitignore to the base node.
+ * It will overwrite the existing base ignore node. There will always
+ * be a base ignore node, even if there is no .gitignore file
+ */
+ private void readRulesAtBase() {
+ //Add .gitignore rules
+ String path = new File(repository.getWorkDir(), ".gitignore").getAbsolutePath();
+ File f = new File(path);
+ IgnoreNode n = new IgnoreNode(f.getParentFile());
+
+ //Add exclude rules
+ //TODO: Get /info directory without string concat
+ path = new File(repository.getWorkDir(), ".git/info/exclude").getAbsolutePath();
+ f = new File(path);
+ if (f.canRead())
+ n.addSecondarySource(f);
+
+ ignoreMap.put("", n);
+ }
+
+ /**
+ * Adds a node located at the FileTreeIterator's root directory.
+ * <br>
+ * Will check for the presence of a .gitignore using {@link FileTreeIterator#hasGitIgnore()}.
+ * If no .gitignore file exists, nothing will be done.
+ * <br>
+ * Will check the last time of modification using {@link FileTreeIterator#hasGitIgnore()}.
+ * If a node already exists and the time stamp has not changed, do nothing.
+ * <br>
+ * Note: This can be extended later if necessary to AbstractTreeIterator by using
+ * byte[] path instead of File directory.
+ *
+ * @param t
+ * AbstractTreeIterator to check for ignore info. The name of the node
+ * should be .gitignore
+ */
+ protected void addNodeFromTree(FileTreeIterator t) {
+ IgnoreNode n = ignoreMap.get(relativize(t.getDirectory()));
+ long time = t.getGitIgnoreLastModified();
+ if (n != null) {
+ if (n.getLastModified() == time)
+ //TODO: Test and optimize
+ return;
+ }
+ n = addIgnoreNode(t.getDirectory());
+ n.setLastModified(time);
+ }
+
+ /**
+ * Maps the directory to an IgnoreNode, but does not initialize
+ * the IgnoreNode. If a node already exists it will be emptied. Empty nodes
+ * will be initialized when needed, see {@link #isIgnored(String)}
+ *
+ * @param dir
+ * directory to load rules from
+ * @return
+ * true if set successfully, false if directory does not exist
+ * or if directory does not contain a .gitignore file.
+ */
+ protected IgnoreNode addIgnoreNode(File dir) {
+ String relativeDir = relativize(dir);
+ IgnoreNode n = ignoreMap.get(relativeDir);
+ if (n != null)
+ n.clear();
+ else {
+ n = new IgnoreNode(dir);
+ ignoreMap.put(relativeDir, n);
+ }
+ return n;
+ }
+
+ /**
+ * Returns the ignored status of the file based on the current state
+ * of the ignore nodes. Ignore nodes will not be updated and new ignore
+ * nodes will not be created.
+ * <br><br>
+ * Traverses from highest to lowest priority and quits as soon as a match
+ * is made. If no match is made anywhere, the file is assumed
+ * to be not ignored.
+ *
+ * @param file
+ * Path string relative to Repository.getWorkDir();
+ * @return true
+ * True if file is ignored, false if the file matches a negation statement
+ * or if there are no rules pertaining to the file.
+ * @throws IOException
+ * Failed to check ignore status
+ */
+ public boolean isIgnored(String file) throws IOException{
+ String currentPriority = file;
+
+ boolean ignored = false;
+ String target = rootFileURI.getPath() + file;
+ while (currentPriority.length() > 1) {
+ currentPriority = getParent(currentPriority);
+ IgnoreNode n = ignoreMap.get(currentPriority);
+
+ if (n != null) {
+ ignored = n.isIgnored(target);
+
+ if (n.wasMatched()) {
+ if (ignored)
+ return ignored;
+ else
+ target = getParent(target);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * String manipulation to get the parent directory of the given path.
+ * It may be more efficient to make a file and call File.getParent().
+ * This function is only called in {@link #initialize}
+ *
+ * @param filePath
+ * Will seek parent directory for this path. Returns empty string
+ * if the filePath does not contain a File.separator
+ * @return
+ * Parent of the filePath, or blank string if non-existent
+ */
+ private String getParent(String filePath) {
+ int lastSlash = filePath.lastIndexOf("/");
+ if (filePath.length() > 0 && lastSlash != -1)
+ return filePath.substring(0, lastSlash);
+ else
+ //This line should be unreachable with the current partiallyInitialize
+ return "";
+ }
+
+ /**
+ * @param relativePath
+ * Directory to find rules for, should be relative to the repository root
+ * @return
+ * Ignore rules for given base directory, contained in an IgnoreNode
+ */
+ public IgnoreNode getRules(String relativePath) {
+ return ignoreMap.get(relativePath);
+ }
+
+ /**
+ * @return
+ * True if there are no ignore rules in this cache
+ */
+ public boolean isEmpty() {
+ return ignoreMap.isEmpty();
+ }
+
+ /**
+ * Clears the cache
+ */
+ public void clear() {
+ ignoreMap.clear();
+ }
+
+ /**
+ * Returns the relative path versus the repository root.
+ *
+ * @param directory
+ * Directory to find relative path for.
+ * @return
+ * Relative path versus the repository root. This function will
+ * strip the last trailing "/" from its return string
+ */
+ private String relativize(File directory) {
+ String retVal = rootFileURI.relativize(directory.toURI()).getPath();
+ if (retVal.endsWith("/"))
+ retVal = retVal.substring(0, retVal.length() - 1);
+ return retVal;
+ }
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
index 03ab629790..a5b3d95d76 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
@@ -316,6 +316,9 @@ public final class Constants {
/** A bare repository typically ends with this string */
public static final String DOT_GIT_EXT = ".git";
+ /** Name of the ignore file */
+ public static final String DOT_GIT_IGNORE = ".gitignore";
+
/**
* Create a new digest function for objects.
*
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
index 90cea0f1be..178657a429 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
@@ -138,11 +138,19 @@ public abstract class AbstractTreeIterator {
*/
protected int pathLen;
+ /**
+ * Last modified time of the .gitignore file. Greater than 0 if a .gitignore
+ * file exists.
+ *
+ */
+ protected long gitIgnoreTimeStamp;
+
/** Create a new iterator with no parent. */
protected AbstractTreeIterator() {
parent = null;
path = new byte[DEFAULT_PATH_SIZE];
pathOffset = 0;
+ gitIgnoreTimeStamp = 0l;
}
/**
@@ -162,6 +170,7 @@ public abstract class AbstractTreeIterator {
*/
protected AbstractTreeIterator(final String prefix) {
parent = null;
+ gitIgnoreTimeStamp = 0l;
if (prefix != null && prefix.length() > 0) {
final ByteBuffer b;
@@ -196,6 +205,7 @@ public abstract class AbstractTreeIterator {
*/
protected AbstractTreeIterator(final byte[] prefix) {
parent = null;
+ gitIgnoreTimeStamp = 0l;
if (prefix != null && prefix.length > 0) {
pathLen = prefix.length;
@@ -220,6 +230,8 @@ public abstract class AbstractTreeIterator {
parent = p;
path = p.path;
pathOffset = p.pathLen + 1;
+ gitIgnoreTimeStamp = 0l;
+
try {
path[pathOffset - 1] = '/';
} catch (ArrayIndexOutOfBoundsException e) {
@@ -249,6 +261,7 @@ public abstract class AbstractTreeIterator {
parent = p;
path = childPath;
pathOffset = childPathOffset;
+ gitIgnoreTimeStamp = 0l;
}
/**
@@ -592,4 +605,22 @@ public abstract class AbstractTreeIterator {
public void getName(byte[] buffer, int offset) {
System.arraycopy(path, pathOffset, buffer, offset, pathLen - pathOffset);
}
+
+ /**
+ * @return
+ * True if this iterator encountered a .gitignore file when initializing entries.
+ * Checks if the gitIgnoreTimeStamp > 0.
+ */
+ public boolean hasGitIgnore() {
+ return gitIgnoreTimeStamp > 0;
+ }
+
+ /**
+ * @return
+ * Last modified time of the .gitignore file, if any. Will be > 0 if a .gitignore
+ * exists.
+ */
+ public long getGitIgnoreLastModified() {
+ return gitIgnoreTimeStamp;
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
index 8dfab8aa57..aab25ee805 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
@@ -64,8 +64,16 @@ import org.eclipse.jgit.util.FS;
* specified working directory as part of a {@link TreeWalk}.
*/
public class FileTreeIterator extends WorkingTreeIterator {
- private final File directory;
- private final FS fs;
+ /**
+ * the starting directory. This directory should correspond to
+ * the root of the repository.
+ */
+ protected final File directory;
+ /**
+ * the file system abstraction which will be necessary to
+ * perform certain file system operations.
+ */
+ protected final FS fs;
/**
* Create a new iterator to traverse the given directory and its children.
@@ -109,12 +117,16 @@ public class FileTreeIterator extends WorkingTreeIterator {
}
private Entry[] entries() {
+ gitIgnoreTimeStamp = 0l;
final File[] all = directory.listFiles();
if (all == null)
return EOF;
final Entry[] r = new Entry[all.length];
- for (int i = 0; i < r.length; i++)
+ for (int i = 0; i < r.length; i++) {
r[i] = new FileEntry(all[i], fs);
+ if (all[i].getName().equals(Constants.DOT_GIT_IGNORE))
+ gitIgnoreTimeStamp = r[i].getLastModified();
+ }
return r;
}
@@ -182,4 +194,12 @@ public class FileTreeIterator extends WorkingTreeIterator {
return file;
}
}
+
+ /**
+ * @return
+ * The root directory of this iterator
+ */
+ public File getDirectory() {
+ return directory;
+ }
}