Browse Source

Move ignore node handling into WorkingTreeIterator

The working tree iterator has perfect knowledge of the path structure
as well as immediate information about whether or not an ignore file
even exists at this level.  We can exploit that to simplify the
logic and running time for testing ignored file status by pushing
all of the checks down into the iterator itself.

Change-Id: I22ff534853e8c5672cc5c2d9444aeb14e294070e
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
CC: Charley Wang <chwang@redhat.com>
CC: Chris Aniszczyk <caniszczyk@gmail.com>
CC: Stefan Lay <stefan.lay@sap.com>
CC: Matthias Sohn <matthias.sohn@sap.com>
stable-0.9
Shawn O. Pearce 15 years ago
parent
commit
09910ffa32
  1. 1
      org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/.gitignore
  2. 1
      org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/.gitignore
  3. 0
      org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/.gitignore
  4. 1
      org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/.gitignore
  5. 0
      org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/test.stp
  6. 1
      org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/.gitignore
  7. 0
      org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/test.stp
  8. 0
      org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/notignored
  9. 0
      org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/test.stp
  10. 403
      org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreCacheTest.java
  11. 184
      org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java
  12. 3
      org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReadTreeTest.java
  13. 3
      org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
  14. 214
      org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java
  15. 3
      org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreRule.java
  16. 291
      org.eclipse.jgit/src/org/eclipse/jgit/ignore/SimpleIgnoreCache.java
  17. 11
      org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java
  18. 4
      org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
  19. 135
      org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java

1
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/.gitignore vendored

@ -1 +0,0 @@
!/notignored

1
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/.gitignore vendored

@ -1 +0,0 @@
notarealfile

0
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/.gitignore vendored

1
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/.gitignore vendored

@ -1 +0,0 @@
/c

0
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b1/test.stp

1
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/.gitignore vendored

@ -1 +0,0 @@
/notarealfile2

0
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/new/a/b2/c/test.stp

0
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/notignored

0
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/test/resources/excludeTest/test.stp

403
org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreCacheTest.java

@ -1,403 +0,0 @@
/*
* 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");
}
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");
}
}
}

184
org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java

@ -0,0 +1,184 @@
/*
* 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 org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.RepositoryTestCase;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
/**
* Tests ignore node behavior on the local filesystem.
*/
public class IgnoreNodeTest extends RepositoryTestCase {
private static final FileMode D = FileMode.TREE;
private static final FileMode F = FileMode.REGULAR_FILE;
private static final boolean ignored = true;
private static final boolean tracked = false;
private TreeWalk walk;
public void testRules() throws IOException {
writeIgnoreFile(".git/info/exclude", "*~", "/out");
writeIgnoreFile(".gitignore", "*.o", "/config");
writeTrashFile("config/secret", "");
writeTrashFile("mylib.c", "");
writeTrashFile("mylib.c~", "");
writeTrashFile("mylib.o", "");
writeTrashFile("out/object/foo.exe", "");
writeIgnoreFile("src/config/.gitignore", "lex.out");
writeTrashFile("src/config/lex.out", "");
writeTrashFile("src/config/config.c", "");
writeTrashFile("src/config/config.c~", "");
writeTrashFile("src/config/old/lex.out", "");
beginWalk();
assertEntry(F, tracked, ".gitignore");
assertEntry(D, ignored, "config");
assertEntry(F, ignored, "config/secret");
assertEntry(F, tracked, "mylib.c");
assertEntry(F, ignored, "mylib.c~");
assertEntry(F, ignored, "mylib.o");
assertEntry(D, ignored, "out");
assertEntry(D, ignored, "out/object");
assertEntry(F, ignored, "out/object/foo.exe");
assertEntry(D, tracked, "src");
assertEntry(D, tracked, "src/config");
assertEntry(F, tracked, "src/config/.gitignore");
assertEntry(F, tracked, "src/config/config.c");
assertEntry(F, ignored, "src/config/config.c~");
assertEntry(F, ignored, "src/config/lex.out");
assertEntry(D, tracked, "src/config/old");
assertEntry(F, ignored, "src/config/old/lex.out");
}
public void testNegation() throws IOException {
writeIgnoreFile(".gitignore", "*.o");
writeIgnoreFile("src/a/b/.gitignore", "!keep.o");
writeTrashFile("src/a/b/keep.o", "");
writeTrashFile("src/a/b/nothere.o", "");
beginWalk();
assertEntry(F, tracked, ".gitignore");
assertEntry(D, tracked, "src");
assertEntry(D, tracked, "src/a");
assertEntry(D, tracked, "src/a/b");
assertEntry(F, tracked, "src/a/b/.gitignore");
assertEntry(F, tracked, "src/a/b/keep.o");
assertEntry(F, ignored, "src/a/b/nothere.o");
}
public void testSlashOnlyMatchesDirectory() throws IOException {
writeIgnoreFile(".gitignore", "out/");
writeTrashFile("out", "");
beginWalk();
assertEntry(F, tracked, ".gitignore");
assertEntry(F, tracked, "out");
new File(trash, "out").delete();
writeTrashFile("out/foo", "");
beginWalk();
assertEntry(F, tracked, ".gitignore");
assertEntry(D, ignored, "out");
assertEntry(F, ignored, "out/foo");
}
public void testWithSlashDoesNotMatchInSubDirectory() throws IOException {
writeIgnoreFile(".gitignore", "a/b");
writeTrashFile("a/a", "");
writeTrashFile("a/b", "");
writeTrashFile("src/a/a", "");
writeTrashFile("src/a/b", "");
beginWalk();
assertEntry(F, tracked, ".gitignore");
assertEntry(D, tracked, "a");
assertEntry(F, tracked, "a/a");
assertEntry(F, ignored, "a/b");
assertEntry(D, tracked, "src");
assertEntry(D, tracked, "src/a");
assertEntry(F, tracked, "src/a/a");
assertEntry(F, tracked, "src/a/b");
}
private void beginWalk() throws CorruptObjectException {
walk = new TreeWalk(db);
walk.reset();
walk.addTree(new FileTreeIterator(db));
}
private void assertEntry(FileMode type, boolean entryIgnored,
String pathName) throws IOException {
assertTrue("walk has entry", walk.next());
assertEquals(pathName, walk.getPathString());
assertEquals(type, walk.getFileMode(0));
WorkingTreeIterator itr = walk.getTree(0, WorkingTreeIterator.class);
assertNotNull("has tree", itr);
assertEquals("is ignored", entryIgnored, itr.isEntryIgnored());
if (D.equals(type))
walk.enterSubtree();
}
private void writeIgnoreFile(String name, String... rules)
throws IOException {
StringBuilder data = new StringBuilder();
for (String line : rules)
data.append(line + "\n");
writeTrashFile(name, data.toString());
}
}

3
org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReadTreeTest.java

@ -58,7 +58,6 @@ import org.eclipse.jgit.errors.CheckoutConflictException;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FS;
public abstract class ReadTreeTest extends RepositoryTestCase {
protected Tree theHead;
@ -613,7 +612,7 @@ public abstract class ReadTreeTest extends RepositoryTestCase {
TreeWalk walk = new TreeWalk(db);
walk.reset();
walk.setRecursive(true);
walk.addTree(new FileTreeIterator(db.getWorkDir(), FS.DETECTED));
walk.addTree(new FileTreeIterator(db));
String expectedValue;
String path;
int nrFiles = 0;

3
org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java

@ -119,8 +119,7 @@ public class AddCommand extends GitCommand<DirCache> {
final TreeWalk tw = new TreeWalk(repo);
tw.reset();
tw.addTree(new DirCacheBuildIterator(builder));
FileTreeIterator fileTreeIterator = new FileTreeIterator(
repo.getWorkDir(), repo.getFS());
FileTreeIterator fileTreeIterator = new FileTreeIterator(repo);
tw.addTree(fileTreeIterator);
tw.setRecursive(true);
tw.setFilter(PathFilterGroup.createFromStrings(filepatterns));

214
org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java

@ -43,194 +43,104 @@
package org.eclipse.jgit.ignore;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.lib.Constants;
/**
* 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.
*
* This class is not thread safe, it maintains state about the last match.
*/
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;
/** Result from {@link IgnoreNode#isIgnored(String, boolean)}. */
public static enum MatchResult {
/** The file is not ignored, due to a rule saying its not ignored. */
NOT_IGNORED,
/** The file is ignored due to a rule in this node. */
IGNORED,
/** The ignore status is unknown, check inherited rules. */
CHECK_PARENT;
}
/** The rules that have been parsed into this node. */
private final List<IgnoreRule> rules;
/** Create an empty ignore node with no rules. */
public IgnoreNode() {
rules = new ArrayList<IgnoreRule>();
}
/**
* 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.
* Create an ignore node with given rules.
*
* @param baseDir
* base directory of this ignore node
*/
public IgnoreNode(File baseDir) {
this.baseDir = baseDir;
rules = new ArrayList<IgnoreRule>();
secondaryFile = null;
lastModified = 0l;
* @param rules
* list of rules.
**/
public IgnoreNode(List<IgnoreRule> rules) {
this.rules = rules;
}
/**
* Parse files according to gitignore standards.
*
* @param in
* input stream holding the standard ignore format. The caller is
* responsible for closing the stream.
* @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));
public void parse(InputStream in) throws IOException {
BufferedReader br = asReader(in);
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;
private static BufferedReader asReader(InputStream in) {
return new BufferedReader(new InputStreamReader(in, Constants.CHARSET));
}
/**
*
* @return
* List of all ignore rules held by this node
*/
public ArrayList<IgnoreRule> getRules() {
return rules;
/** @return list of all ignore rules held by this node. */
public List<IgnoreRule> getRules() {
return Collections.unmodifiableList(rules);
}
/**
* Determine if an entry path matches an ignore rule.
*
* 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
*
* @param entryPath
* the path to test. The path must be relative to this ignore
* node's own repository path, and in repository path format
* (uses '/' and not '\').
* @param isDirectory
* true if the target item is a directory.
* @return status of the path.
*/
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();
}
public MatchResult isIgnored(String entryPath, boolean isDirectory) {
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;
return MatchResult.CHECK_PARENT;
// Parse rules in the reverse order that they were read
for (int i = rules.size() - 1; i > -1; i--) {
IgnoreRule rule = rules.get(i);
if (rule.isMatch(entryPath, isDirectory)) {
if (rule.getResult())
return MatchResult.IGNORED;
else
return MatchResult.NOT_IGNORED;
}
/**
* 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;
return MatchResult.CHECK_PARENT;
}
}

3
org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreRule.java

@ -91,10 +91,11 @@ public class IgnoreRule {
endIndex --;
dirOnly = true;
}
boolean hasSlash = pattern.contains("/");
pattern = pattern.substring(startIndex, endIndex);
if (!pattern.contains("/"))
if (!hasSlash)
nameOnly = true;
else if (!pattern.startsWith("/")) {
//Contains "/" but does not start with one

291
org.eclipse.jgit/src/org/eclipse/jgit/ignore/SimpleIgnoreCache.java

@ -1,291 +0,0 @@
/*
* 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 org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.PathSuffixFilter;
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.setFilter(PathSuffixFilter.create("/" + Constants.DOT_GIT_IGNORE));
tw.setRecursive(true);
while (tw.next())
addNodeFromTree(tw.getTree(0, FileTreeIterator.class));
//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
File f = new File(repository.getWorkDir(), Constants.DOT_GIT_IGNORE);
String path = f.getAbsolutePath();
IgnoreNode n = new IgnoreNode(f.getParentFile());
//Add exclude rules
//TODO: Get /info directory without string concat
path = new File(repository.getDirectory(), "info/exclude").getAbsolutePath();
f = new File(path);
if (f.canRead())
n.addSecondarySource(f);
ignoreMap.put("", n);
}
/**
* Adds a node located at the FileTreeIterator's current position.
*
* @param t
* FileTreeIterator to check for ignore info. The name of the
* entry should be ".gitignore".
*/
protected void addNodeFromTree(FileTreeIterator t) {
IgnoreNode n = ignoreMap.get(relativize(t.getDirectory()));
long time = t.getEntryLastModified();
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;
}
}

11
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/FileTreeIterator.java

@ -76,6 +76,17 @@ public class FileTreeIterator extends WorkingTreeIterator {
*/
protected final FS fs;
/**
* Create a new iterator to traverse the work tree and its children.
*
* @param repo
* the repository whose working tree will be scanned.
*/
public FileTreeIterator(Repository repo) {
this(repo.getWorkDir(), repo.getFS());
initRootIterator(repo);
}
/**
* Create a new iterator to traverse the given directory and its children.
*

4
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java

@ -918,4 +918,8 @@ public class TreeWalk {
static String pathOf(final AbstractTreeIterator t) {
return RawParseUtils.decode(Constants.CHARSET, t.path, 0, t.pathLen);
}
static String pathOf(final byte[] buf, int pos, int end) {
return RawParseUtils.decode(Constants.CHARSET, buf, pos, end);
}
}

135
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java

@ -45,6 +45,8 @@
package org.eclipse.jgit.treewalk;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
@ -54,14 +56,18 @@ import java.nio.charset.CharsetEncoder;
import java.security.MessageDigest;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import org.eclipse.jgit.JGitText;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.ignore.IgnoreNode;
import org.eclipse.jgit.ignore.IgnoreRule;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.FS;
/**
@ -106,6 +112,9 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
/** Current position within {@link #entries}. */
private int ptr;
/** If there is a .gitignore file present, the parsed rules from it. */
private IgnoreNode ignoreNode;
/** Create a new iterator with no parent. */
protected WorkingTreeIterator() {
super();
@ -143,6 +152,24 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
nameEncoder = p.nameEncoder;
}
/**
* Initialize this iterator for the root level of a repository.
* <p>
* This method should only be invoked after calling {@link #init(Entry[])},
* and only for the root iterator.
*
* @param repo
* the repository.
*/
protected void initRootIterator(Repository repo) {
Entry entry;
if (ignoreNode instanceof PerDirectoryIgnoreNode)
entry = ((PerDirectoryIgnoreNode) ignoreNode).entry;
else
entry = null;
ignoreNode = new RootIgnoreNode(entry, repo);
}
@Override
public byte[] idBuffer() {
if (contentIdFromPtr == ptr)
@ -295,6 +322,57 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
return current().getLastModified();
}
/**
* Determine if the current entry path is ignored by an ignore rule.
*
* @return true if the entry was ignored by an ignore rule file.
* @throws IOException
* a relevant ignore rule file exists but cannot be read.
*/
public boolean isEntryIgnored() throws IOException {
return isEntryIgnored(pathLen);
}
/**
* Determine if the entry path is ignored by an ignore rule.
*
* @param pLen
* the length of the path in the path buffer.
* @return true if the entry is ignored by an ignore rule.
* @throws IOException
* a relevant ignore rule file exists but cannot be read.
*/
protected boolean isEntryIgnored(final int pLen) throws IOException {
IgnoreNode rules = getIgnoreNode();
if (rules != null) {
// The ignore code wants path to start with a '/' if possible.
// If we have the '/' in our path buffer because we are inside
// a subdirectory include it in the range we convert to string.
//
int pOff = pathOffset;
if (0 < pOff)
pOff--;
String p = TreeWalk.pathOf(path, pOff, pLen);
switch (rules.isIgnored(p, FileMode.TREE.equals(mode))) {
case IGNORED:
return true;
case NOT_IGNORED:
return false;
case CHECK_PARENT:
break;
}
}
if (parent instanceof WorkingTreeIterator)
return ((WorkingTreeIterator) parent).isEntryIgnored(pLen);
return false;
}
private IgnoreNode getIgnoreNode() throws IOException {
if (ignoreNode instanceof PerDirectoryIgnoreNode)
ignoreNode = ((PerDirectoryIgnoreNode) ignoreNode).load();
return ignoreNode;
}
private static final Comparator<Entry> ENTRY_CMP = new Comparator<Entry>() {
public int compare(final Entry o1, final Entry o2) {
final byte[] a = o1.encodedName;
@ -345,6 +423,8 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
continue;
if (Constants.DOT_GIT.equals(name))
continue;
if (Constants.DOT_GIT_IGNORE.equals(name))
ignoreNode = new PerDirectoryIgnoreNode(e);
if (i != o)
entries[o] = e;
e.encodeName(nameEncoder);
@ -570,4 +650,59 @@ public abstract class WorkingTreeIterator extends AbstractTreeIterator {
*/
public abstract InputStream openInputStream() throws IOException;
}
/** Magic type indicating we know rules exist, but they aren't loaded. */
private static class PerDirectoryIgnoreNode extends IgnoreNode {
final Entry entry;
PerDirectoryIgnoreNode(Entry entry) {
super(Collections.<IgnoreRule> emptyList());
this.entry = entry;
}
IgnoreNode load() throws IOException {
IgnoreNode r = new IgnoreNode();
InputStream in = entry.openInputStream();
try {
r.parse(in);
} finally {
in.close();
}
return r.getRules().isEmpty() ? null : r;
}
}
/** Magic type indicating there may be rules for the top level. */
private static class RootIgnoreNode extends PerDirectoryIgnoreNode {
final Repository repository;
RootIgnoreNode(Entry entry, Repository repository) {
super(entry);
this.repository = repository;
}
@Override
IgnoreNode load() throws IOException {
IgnoreNode r;
if (entry != null) {
r = super.load();
if (r == null)
r = new IgnoreNode();
} else {
r = new IgnoreNode();
}
File exclude = new File(repository.getDirectory(), "info/exclude");
if (exclude.exists()) {
FileInputStream in = new FileInputStream(exclude);
try {
r.parse(in);
} finally {
in.close();
}
}
return r.getRules().isEmpty() ? null : r;
}
}
}

Loading…
Cancel
Save