]> source.dussan.org Git - jgit.git/commitdiff
Add compatibility with gitignore specifications 83/683/20
authorCharley Wang <chwang@redhat.com>
Mon, 12 Jul 2010 22:34:15 +0000 (00:34 +0200)
committerMatthias Sohn <matthias.sohn@sap.com>
Mon, 12 Jul 2010 22:34:15 +0000 (00:34 +0200)
This patch adds ignore compatibility to jgit. It encompasses
exclude files as well as .gitignore. Uses TreeWalk and
FileTreeIterator to find nodes and parses .gitignore
files when required. The patch includes a simple cache that
can be used to save results and avoid excessive gitignore
parsing.

CQ: 4302
Bug: 303925
Change-Id: Iebd7e5bb534accca4bf00d25bbc1f561d7cad11b
Signed-off-by: Chris Aniszczyk <caniszczyk@gmail.com>
Signed-off-by: Stefan Lay <stefan.lay@sap.com>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
21 files changed:
org.eclipse.jgit.test/META-INF/MANIFEST.MF
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/.gitignore [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/.gitignore [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/.gitignore [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/.gitignore [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/test.stp [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/.gitignore [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/test.stp [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/notignored [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/.gitignore [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/test.stp [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/test.stp [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreCacheTest.java [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreMatcherTest.java [new file with mode: 0644]
org.eclipse.jgit/META-INF/MANIFEST.MF
org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreRule.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/ignore/SimpleIgnoreCache.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java

index 3b82cf7d1d5858c180dd774914babcbe39af5eac..3aaa8a45e87f70f1dd3856ee43d8d950967c8a7e 100644 (file)
@@ -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 (file)
index 0000000..b3f6bc9
--- /dev/null
@@ -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 (file)
index 0000000..09b8574
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
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 (file)
index 0000000..82b0f5d
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
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 (file)
index 0000000..3c6cf10
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..b314092
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..4083dcb
--- /dev/null
@@ -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 (file)
index 0000000..cacad75
--- /dev/null
@@ -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("/"));
+       }
+}
index 258e6781b5adba18d185da533980e7db42ad3159..51d440e58edea4d4ed7b3cfb93be592e98ed40f4 100644 (file)
@@ -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 (file)
index 0000000..f29fa1e
--- /dev/null
@@ -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 (file)
index 0000000..982ce06
--- /dev/null
@@ -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 (file)
index 0000000..be37a9a
--- /dev/null
@@ -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;
+       }
+
+}
index 03ab629790ddda7b8f7aac051d96ff9a77eb216a..a5b3d95d76f9a21ea9c5d07d0dd46ef56a4dccf4 100644 (file)
@@ -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.
         *
index 90cea0f1be68dea14d0cf983c14d91a69b84c562..178657a42988d3c235a83469f49db0dab8a80a6f 100644 (file)
@@ -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;
+       }
 }
index 8dfab8aa5775be84aaf72087593248355993c0ec..aab25ee8052a556ad02bdcadc340ef65f4c892e9 100644 (file)
@@ -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;
+       }
 }