Skip to content

Commit

Permalink
Lots of progress:
Browse files Browse the repository at this point in the history
  - Refactored interaction between CLI args and rest of xrep.
  - Filling in a lot more options, including file type filtering.
  - Fixing some bugs in globbing/ignoring.
  - More documentation.
  • Loading branch information
BurntSushi committed Sep 5, 2016
1 parent 0bf278e commit 812cdb1
Show file tree
Hide file tree
Showing 9 changed files with 1,564 additions and 405 deletions.
551 changes: 551 additions & 0 deletions src/args.rs

Large diffs are not rendered by default.

46 changes: 34 additions & 12 deletions src/gitignore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl From<io::Error> for Error {
}

/// Gitignore is a matcher for the glob patterns in a single gitignore file.
#[derive(Clone, Debug)]
pub struct Gitignore {
set: glob::Set,
root: PathBuf,
Expand Down Expand Up @@ -136,22 +137,26 @@ impl Gitignore {
pub fn matched_utf8(&self, path: &str, is_dir: bool) -> Match {
// A single regex with a bunch of alternations of glob patterns is
// unfortunately typically faster than a regex, so we use it as a
// first pass filter. We still need to run the RegexSet to most
// first pass filter. We still need to run the RegexSet to get the most
// recently defined glob that matched.
if !self.set.is_match(path) {
return Match::None;
}
let pat = match self.set.matches(path).iter().last() {
None => return Match::None,
Some(i) => &self.patterns[i],
};
if pat.whitelist {
Match::Whitelist(&pat)
} else if !pat.only_dir || is_dir {
Match::Ignored(&pat)
} else {
Match::None
// The regex set can't actually pick the right glob that matched all
// on its own. In particular, some globs require that only directories
// can match. Thus, only accept a match from the regex set if the given
// path satisfies the corresponding glob's directory criteria.
for i in self.set.matches(path).iter().rev() {
let pat = &self.patterns[i];
if !pat.only_dir || is_dir {
return if pat.whitelist {
Match::Whitelist(pat)
} else {
Match::Ignored(pat)
};
}
}
Match::None
}
}

Expand All @@ -177,6 +182,24 @@ impl<'a> Match<'a> {
Match::None | Match::Whitelist(_) => false,
}
}

/// Returns true if the match result didn't match any globs.
pub fn is_none(&self) -> bool {
match *self {
Match::None => true,
Match::Ignored(_) | Match::Whitelist(_) => false,
}
}

/// Inverts the match so that Ignored becomes Whitelisted and Whitelisted
/// becomes Ignored. A non-match remains the same.
pub fn invert(self) -> Match<'a> {
match self {
Match::None => Match::None,
Match::Ignored(pat) => Match::Whitelist(pat),
Match::Whitelist(pat) => Match::Ignored(pat),
}
}
}

/// GitignoreBuilder constructs a matcher for a single set of globs from a
Expand Down Expand Up @@ -231,7 +254,6 @@ impl GitignoreBuilder {
/// Add each pattern line from the file path given.
pub fn add_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
let rdr = io::BufReader::new(try!(File::open(&path)));
// println!("adding ignores from: {}", path.as_ref().display());
for line in rdr.lines() {
try!(self.add(&path, &try!(line)));
}
Expand Down
2 changes: 2 additions & 0 deletions src/glob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ impl Set {
/// Returns every glob pattern (by sequence number) that matches the given
/// path.
pub fn matches<T: AsRef<[u8]>>(&self, path: T) -> SetMatches {
// TODO(burntsushi): If we split this out into a separate crate, don't
// expose the regex::SetMatches type in the public API.
self.set.matches(path.as_ref())
}

Expand Down
82 changes: 74 additions & 8 deletions src/ignore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use std::fmt;
use std::path::{Path, PathBuf};

use gitignore::{self, Gitignore, GitignoreBuilder, Match};
use types::Types;

/// Represents an error that can occur when parsing a gitignore file.
#[derive(Debug)]
Expand Down Expand Up @@ -56,15 +57,24 @@ pub struct Ignore {
/// A stack of ignore patterns at each directory level of traversal.
/// A directory that contributes no ignore patterns is `None`.
stack: Vec<Option<IgnoreDir>>,
/// A set of override globs that are always checked first. A match (whether
/// it's whitelist or blacklist) trumps anything in stack.
overrides: Option<Gitignore>,
/// A file type matcher.
types: Option<Types>,
ignore_hidden: bool,
no_ignore: bool,
}

impl Ignore {
/// Create an empty set of ignore patterns.
pub fn new() -> Ignore {
Ignore {
stack: vec![],
overrides: None,
types: None,
ignore_hidden: true,
no_ignore: false,
}
}

Expand All @@ -74,11 +84,34 @@ impl Ignore {
self
}

/// When set, ignore files are ignored.
pub fn no_ignore(&mut self, yes: bool) -> &mut Ignore {
self.no_ignore = yes;
self
}

/// Add a set of globs that overrides all other match logic.
pub fn add_override(&mut self, gi: Gitignore) -> &mut Ignore {
self.overrides = Some(gi);
self
}

/// Add a file type matcher. The file type matcher has the lowest
/// precedence.
pub fn add_types(&mut self, types: Types) -> &mut Ignore {
self.types = Some(types);
self
}

/// Add a directory to the stack.
///
/// Note that even if this returns an error, the directory is added to the
/// stack (and therefore should be popped).
pub fn push<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
if self.no_ignore {
self.stack.push(None);
return Ok(());
}
match IgnoreDir::new(path) {
Ok(id) => {
self.stack.push(id);
Expand All @@ -102,24 +135,57 @@ impl Ignore {
/// Returns true if and only if the given file path should be ignored.
pub fn ignored<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> bool {
let path = path.as_ref();
if let Some(ref overrides) = self.overrides {
let mat = overrides.matched(path, is_dir).invert();
if let Some(is_ignored) = self.ignore_match(path, mat) {
return is_ignored;
}
}
if self.ignore_hidden && is_hidden(&path) {
debug!("{} ignored because it is hidden", path.display());
return true;
}
for id in self.stack.iter().rev().filter_map(|id| id.as_ref()) {
match id.matched(path, is_dir) {
Match::Whitelist(ref pat) => {
debug!("{} whitelisted by {:?}", path.display(), pat);
return false;
}
Match::Ignored(ref pat) => {
debug!("{} ignored by {:?}", path.display(), pat);
let mat = id.matched(path, is_dir);
if let Some(is_ignored) = self.ignore_match(path, mat) {
if is_ignored {
return true;
}
Match::None => {}
// If this path is whitelisted by an ignore, then fallthrough
// and let the file type matcher have a say.
break;
}
}
if let Some(ref types) = self.types {
let mat = types.matched(path, is_dir);
if let Some(is_ignored) = self.ignore_match(path, mat) {
return is_ignored;
}
}
false
}

/// Returns true if the given match says the given pattern should be
/// ignored or false if the given pattern should be explicitly whitelisted.
/// Returns None otherwise.
pub fn ignore_match<P: AsRef<Path>>(
&self,
path: P,
mat: Match,
) -> Option<bool> {
let path = path.as_ref();
match mat {
Match::Whitelist(ref pat) => {
debug!("{} whitelisted by {:?}", path.display(), pat);
Some(false)
}
Match::Ignored(ref pat) => {
debug!("{} ignored by {:?}", path.display(), pat);
Some(true)
}
Match::None => None,
}
}
}

/// IgnoreDir represents a set of ignore patterns retrieved from a single
Expand Down

0 comments on commit 812cdb1

Please sign in to comment.