use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use super::{Error, FileMatches, Searcher};
use crate::attributes::{Lookup, State};
use crate::ignore::InputKind;
use crate::index::{Limits as IndexLimits, Mode};
use crate::pathspec::{Pathspec, Prune};
use crate::workflow::Environment;
use crate::{EntryMode, ObjectId, Objects, Repository};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Source {
Worktree,
WorktreeAndUntracked {
ignored: bool,
},
Index,
Object(ObjectId),
}
#[derive(Clone, Debug, Default)]
pub struct Scope {
pathspec: Pathspec,
max_depth: Option<usize>,
environment: Environment,
}
impl Scope {
pub fn new(pathspec: Pathspec) -> Self {
Self {
pathspec,
..Self::default()
}
}
#[must_use]
pub fn with_max_depth(mut self, levels: Option<usize>) -> Self {
self.max_depth = levels;
self
}
#[must_use]
pub fn with_environment(mut self, environment: Environment) -> Self {
self.environment = environment;
self
}
fn selects(&self, path: &[u8], lookup: &mut Option<Lookup>) -> Result<bool, Error> {
if self
.max_depth
.is_some_and(|levels| !self.pathspec.within_depth(path, levels))
{
return Ok(false);
}
if !self.pathspec.uses_attributes() {
return Ok(self.pathspec.matches_unchecked(path));
}
let Some(lookup) = lookup else {
return Ok(false);
};
self.pathspec.matches_with(path, |requirement| {
let state: State<'_> =
lookup
.get(path, requirement.name())
.map_err(|source| Error::Attributes {
path: path.to_vec(),
source: Box::new(source),
})?;
Ok(requirement.accepts(state))
})
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Found<'a> {
pub path: &'a [u8],
pub content: &'a [u8],
pub matches: &'a FileMatches,
pub searcher: &'a Searcher,
}
enum Content {
Blob(ObjectId),
File,
}
impl Repository {
pub fn grep(
&self,
source: Source,
scope: &Scope,
searcher: &mut Searcher,
mut visit: impl FnMut(Found<'_>) -> ControlFlow<()>,
) -> Result<(), Error> {
let objects = self.objects().map_err(Error::object)?;
let mut lookup = if scope.pathspec.uses_attributes() {
let directories = self
.attribute_source(&scope.environment)
.map_err(|source| Error::Attributes {
path: Vec::new(),
source: Box::new(source),
})?;
Some(
self.attributes(&scope.environment, directories)
.map_err(|source| Error::Attributes {
path: Vec::new(),
source: Box::new(source),
})?,
)
} else {
None
};
let candidates = match source {
Source::Object(id) => {
let object = objects.read(id).map_err(Error::object)?;
if let Some(object) =
object.filter(|object| object.kind() == crate::ObjectKind::Blob)
{
let matches = searcher.search(b"", object.data())?;
let _ = visit(Found {
path: b"",
content: object.data(),
matches: &matches,
searcher,
});
return Ok(());
}
let mut candidates = Vec::new();
tree_files(
&objects,
id,
Vec::new(),
scope,
&mut lookup,
&mut candidates,
)?;
candidates
}
Source::Index => self.index_files(&objects, scope, &mut lookup, false)?,
Source::Worktree => self.index_files(&objects, scope, &mut lookup, true)?,
Source::WorktreeAndUntracked { ignored } => {
let mut candidates = self.index_files(&objects, scope, &mut lookup, true)?;
self.add_untracked(&mut candidates, scope, &mut lookup, ignored)?;
candidates
}
};
let root = self.worktree().map(Path::to_owned);
let mut checked = CheckedDirectories::default();
for (path, content) in candidates {
let bytes = match content {
Content::Blob(id) => objects.read_blob(id).map_err(Error::object)?,
Content::File => {
let Some(root) = &root else { continue };
match read_worktree_file(root, &path, &mut checked)? {
Some(bytes) => bytes,
None => continue,
}
}
};
let matches = searcher.search(&path, &bytes)?;
let found = Found {
path: &path,
content: &bytes,
matches: &matches,
searcher,
};
if visit(found).is_break() {
break;
}
}
Ok(())
}
fn index_files(
&self,
objects: &Objects,
scope: &Scope,
lookup: &mut Option<Lookup>,
worktree: bool,
) -> Result<Vec<(Vec<u8>, Content)>, Error> {
let index = self
.read_index(IndexLimits::default())
.map_err(Error::object)?;
let Some(index) = index else {
return Ok(Vec::new());
};
let mut candidates: Vec<(Vec<u8>, Content)> = Vec::new();
for entry in index.entries() {
if candidates
.last()
.is_some_and(|(path, _)| *path == entry.path)
{
continue;
}
let content = match entry.mode {
Mode::SparseDirectory => {
let directory = entry.path.strip_suffix(b"/").unwrap_or(&entry.path);
tree_files(
objects,
entry.id,
directory.to_vec(),
scope,
lookup,
&mut candidates,
)?;
continue;
}
Mode::Gitlink => continue,
Mode::Symlink if worktree => continue,
_ if worktree && !entry.skip_worktree => Content::File,
_ => Content::Blob(entry.id),
};
if scope.selects(&entry.path, lookup)? {
candidates.push((entry.path.clone(), content));
}
}
Ok(candidates)
}
fn add_untracked(
&self,
candidates: &mut Vec<(Vec<u8>, Content)>,
scope: &Scope,
lookup: &mut Option<Lookup>,
ignored: bool,
) -> Result<(), Error> {
let untracked = |source| Error::Untracked(Box::new(source));
let never = AtomicBool::new(false);
let paths: Vec<Vec<u8>> = if ignored {
self.raw_status_with(
crate::status::Baseline::Tree(None),
crate::status::Untracked::RawFilesWithoutIgnores,
&scope.pathspec,
crate::status::Limits::default(),
&never,
)
.map_err(untracked)?
.raw_untracked
} else {
let mut rules = self
.ignore_rules(&scope.environment)
.map_err(|source| Error::Ignore(Box::new(source)))?;
let mut kept = Vec::with_capacity(candidates.len());
for candidate in candidates.drain(..) {
let hidden = rules
.check(&candidate.0, InputKind::File)
.map_err(|source| Error::Ignore(Box::new(source)))?
.is_some_and(|found| found.ignored);
if !hidden {
kept.push(candidate);
}
}
*candidates = kept;
self.status_with(
crate::status::Baseline::Tree(None),
crate::status::UntrackedFiles::All,
&rules,
&scope.pathspec,
crate::status::Limits::default(),
&never,
)
.map_err(untracked)?
.untracked
.into_iter()
.filter(|entry| entry.kind == crate::status::UntrackedKind::File)
.map(|entry| entry.path)
.collect()
};
let tracked: std::collections::HashSet<Vec<u8>> =
candidates.iter().map(|(path, _)| path.clone()).collect();
for path in paths {
if !tracked.contains(&path) && scope.selects(&path, lookup)? {
candidates.push((path, Content::File));
}
}
candidates.sort_by(|a, b| a.0.cmp(&b.0));
Ok(())
}
}
pub fn grep_directory(
root: &Path,
scope: &Scope,
searcher: &mut Searcher,
mut visit: impl FnMut(Found<'_>) -> ControlFlow<()>,
) -> Result<(), Error> {
let mut files = Vec::new();
walk_directory(root, &[], scope, &mut files)?;
files.sort();
for path in files {
let native = native_path(root, &path)?;
let bytes = std::fs::read(&native).map_err(|source| Error::Read {
path: native.clone(),
source,
})?;
let matches = searcher.search(&path, &bytes)?;
let found = Found {
path: &path,
content: &bytes,
matches: &matches,
searcher,
};
if visit(found).is_break() {
break;
}
}
Ok(())
}
fn walk_directory(
root: &Path,
directory: &[u8],
scope: &Scope,
files: &mut Vec<Vec<u8>>,
) -> Result<(), Error> {
let native = native_path(root, directory)?;
let read = |source| Error::Read {
path: native.clone(),
source,
};
for entry in std::fs::read_dir(&native).map_err(read)? {
let entry = entry.map_err(read)?;
let name = entry.file_name();
let Some(name) = name_bytes(&name) else {
continue;
};
if name == b".git" {
continue;
}
let mut path = directory.to_vec();
if !path.is_empty() {
path.push(b'/');
}
path.extend_from_slice(&name);
let kind = entry.file_type().map_err(read)?;
if kind.is_dir() {
if scope.pathspec.directory(&path) != Prune::Skip {
walk_directory(root, &path, scope, files)?;
}
} else if kind.is_file() && scope.selects(&path, &mut None)? {
files.push(path);
}
}
Ok(())
}
fn name_bytes(name: &std::ffi::OsStr) -> Option<Vec<u8>> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Some(name.as_bytes().to_vec())
}
#[cfg(not(unix))]
{
name.to_str().map(|name| name.as_bytes().to_vec())
}
}
fn native_path(root: &Path, path: &[u8]) -> Result<PathBuf, Error> {
if path.is_empty() {
return Ok(root.to_owned());
}
crate::source_files::native(root, path).ok_or_else(|| Error::Path(path.to_vec()))
}
fn tree_files(
objects: &Objects,
id: ObjectId,
directory: Vec<u8>,
scope: &Scope,
lookup: &mut Option<Lookup>,
out: &mut Vec<(Vec<u8>, Content)>,
) -> Result<(), Error> {
let tree = objects.read_tree(id).map_err(Error::object)?;
for entry in tree.entries() {
let mut path = directory.clone();
if !path.is_empty() {
path.push(b'/');
}
path.extend_from_slice(&entry.name);
match entry.mode {
EntryMode::Tree => {
if scope.pathspec.directory(&path) != Prune::Skip {
tree_files(objects, entry.id, path, scope, lookup, out)?;
}
}
EntryMode::Gitlink => {}
_ => {
if scope.selects(&path, lookup)? {
out.push((path, Content::Blob(entry.id)));
}
}
}
}
Ok(())
}
#[derive(Default)]
struct CheckedDirectories(std::collections::HashSet<Vec<u8>>);
fn read_worktree_file(
root: &Path,
path: &[u8],
checked: &mut CheckedDirectories,
) -> Result<Option<Vec<u8>>, Error> {
let mut end = 0;
while let Some(slash) = path
.get(end..)
.and_then(|rest| rest.iter().position(|&byte| byte == b'/'))
{
end += slash;
let directory = path.get(..end).unwrap_or_default();
if !checked.0.contains(directory) {
let native = native_path(root, directory)?;
match std::fs::symlink_metadata(&native) {
Ok(metadata) if metadata.is_dir() => {
checked.0.insert(directory.to_vec());
}
_ => return Ok(None),
}
}
end += 1;
}
let native = native_path(root, path)?;
match std::fs::symlink_metadata(&native) {
Ok(metadata) if metadata.is_file() => {}
_ => return Ok(None),
}
match std::fs::read(&native) {
Ok(bytes) => Ok(Some(bytes)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(source) => Err(Error::Read {
path: native,
source,
}),
}
}