diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 3b82cf7d1..3aaa8a45e 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 000000000..b3f6bc97f Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/.gitignore differ 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 000000000..09b8574b0 Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/.gitignore differ 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 000000000..e69de29bb 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 000000000..82b0f5d46 Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/.gitignore differ 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 000000000..e69de29bb 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 000000000..3c6cf10b1 Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/.gitignore differ 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 000000000..e69de29bb 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 000000000..e69de29bb 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 000000000..b314092d1 Binary files /dev/null and b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/src/.gitignore differ 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 000000000..e69de29bb 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 000000000..e69de29bb 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 000000000..4083dcb09 --- /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 toDelete = new ArrayList(); + + //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: + * + * + * new/a/b2/.gitignore: + * + * + * 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: + * + * + * new/a/b2/.gitignore: + * + * + * 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 000000000..cacad7555 --- /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 258e6781b..51d440e58 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 000000000..f29fa1e01 --- /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. + *

+ * At the root of the repository, there may be an additional source of + * ignore information (the exclude file) + *

+ * 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 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(); + 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 getRules() { + return rules; + } + + + /** + * + * Returns whether or not a target is matched as being ignored by + * any patterns in this directory. + *
+ * Will return false if the file is not a descendant of this directory. + *
+ * + * @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 000000000..982ce06c6 --- /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. + *
+ * 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 isMatch(String, boolean) 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 000000000..be37a9ad8 --- /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. + *

+ * 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 + *
+ * e.g: path/to/directory is a valid String + */ + private HashMap 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. + *

+ * 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(); + 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. + *

+ * 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 toAdd = new HashSet(); + 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. + *
+ * Will check for the presence of a .gitignore using {@link FileTreeIterator#hasGitIgnore()}. + * If no .gitignore file exists, nothing will be done. + *
+ * 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. + *
+ * 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. + *

+ * 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 03ab62979..a5b3d95d7 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 90cea0f1b..178657a42 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 @@ protected AbstractTreeIterator() { */ protected AbstractTreeIterator(final String prefix) { parent = null; + gitIgnoreTimeStamp = 0l; if (prefix != null && prefix.length() > 0) { final ByteBuffer b; @@ -196,6 +205,7 @@ protected AbstractTreeIterator(final String prefix) { */ protected AbstractTreeIterator(final byte[] prefix) { parent = null; + gitIgnoreTimeStamp = 0l; if (prefix != null && prefix.length > 0) { pathLen = prefix.length; @@ -220,6 +230,8 @@ protected AbstractTreeIterator(final AbstractTreeIterator p) { parent = p; path = p.path; pathOffset = p.pathLen + 1; + gitIgnoreTimeStamp = 0l; + try { path[pathOffset - 1] = '/'; } catch (ArrayIndexOutOfBoundsException e) { @@ -249,6 +261,7 @@ protected AbstractTreeIterator(final AbstractTreeIterator p, parent = p; path = childPath; pathOffset = childPathOffset; + gitIgnoreTimeStamp = 0l; } /** @@ -592,4 +605,22 @@ public int getNameLength() { 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 8dfab8aa5..aab25ee80 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 @@ * 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 AbstractTreeIterator createSubtreeIterator(final Repository repo) } 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 File getFile() { return file; } } + + /** + * @return + * The root directory of this iterator + */ + public File getDirectory() { + return directory; + } }