diff --git a/Cargo.lock b/Cargo.lock index c8efb1a9b80f..852a33b717d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,6 +509,16 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +[[package]] +name = "base64-serde" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba368df5de76a5bea49aaf0cf1b39ccfbbef176924d1ba5db3e4135216cbe3c7" +dependencies = [ + "base64 0.21.5", + "serde", +] + [[package]] name = "base64ct" version = "1.6.0" @@ -573,6 +583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c79ad7fb2dd38f3dabd76b09c6a5a20c038fc0213ef1e9afd30eb777f120f019" dependencies = [ "memchr", + "regex-automata 0.4.6", "serde", ] @@ -1309,13 +1320,22 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1795,6 +1815,85 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "grep" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2b024ec1e686cb64d78beb852030b0e632af93817f1ed25be0173af0e94939" +dependencies = [ + "grep-cli", + "grep-matcher", + "grep-printer", + "grep-regex", + "grep-searcher", +] + +[[package]] +name = "grep-cli" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea40788c059ab8b622c4d074732750bfb3bd2912e2dd58eabc11798a4d5ad725" +dependencies = [ + "bstr", + "globset", + "libc", + "log", + "termcolor", + "winapi-util", +] + +[[package]] +name = "grep-matcher" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a3141a10a43acfedc7c98a60a834d7ba00dfe7bec9071cbfc19b55b292ac02" +dependencies = [ + "memchr", +] + +[[package]] +name = "grep-printer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743c12a03c8aee38b6e5bd0168d8ebb09345751323df4a01c56e792b1f38ceb2" +dependencies = [ + "bstr", + "grep-matcher", + "grep-searcher", + "log", + "serde", + "serde_json", + "termcolor", +] + +[[package]] +name = "grep-regex" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f748bb135ca835da5cbc67ca0e6955f968db9c5df74ca4f56b18e1ddbc68230d" +dependencies = [ + "bstr", + "grep-matcher", + "log", + "regex-automata 0.4.6", + "regex-syntax 0.8.2", +] + +[[package]] +name = "grep-searcher" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba536ae4f69bec62d8839584dd3153d3028ef31bb229f04e09fb5a9e5a193c54" +dependencies = [ + "bstr", + "encoding_rs", + "encoding_rs_io", + "grep-matcher", + "log", + "memchr", + "memmap2", +] + [[package]] name = "h2" version = "0.3.19" @@ -5352,14 +5451,19 @@ dependencies = [ "assert_matches", "async-stream", "axum", + "base64 0.22.0", + "base64-serde", "futures", "git2", + "grep", + "ignore", "mime_guess", "nucleo", "serde", "serde_json", "temp_testdir", "tokio", + "tracing", ] [[package]] @@ -5700,6 +5804,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "text-splitter" version = "0.10.0" @@ -6794,11 +6907,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] diff --git a/crates/tabby-git/Cargo.toml b/crates/tabby-git/Cargo.toml index 9e2c03362b86..8eb35917de5f 100644 --- a/crates/tabby-git/Cargo.toml +++ b/crates/tabby-git/Cargo.toml @@ -17,6 +17,11 @@ mime_guess.workspace = true futures.workspace = true async-stream.workspace = true tokio.workspace = true +tracing.workspace = true +ignore.workspace = true +grep = "0.3.1" +base64-serde = "0.7" +base64 = "0.22" [dev-dependencies] -assert_matches.workspace = true \ No newline at end of file +assert_matches.workspace = true diff --git a/crates/tabby-git/src/file_search.rs b/crates/tabby-git/src/file_search.rs index fdc7ef3cbb57..7ceb14670fd2 100644 --- a/crates/tabby-git/src/file_search.rs +++ b/crates/tabby-git/src/file_search.rs @@ -1,9 +1,12 @@ -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use async_stream::stream; use futures::{Stream, StreamExt}; use git2::TreeWalkResult; +use super::rev_to_commit; +use crate::bytes2path; + pub struct GitFileSearch { pub r#type: &'static str, pub path: String, @@ -27,13 +30,7 @@ fn walk( rev: Option<&str>, tx: tokio::sync::mpsc::Sender<(bool, PathBuf)>, ) -> anyhow::Result<()> { - let commit = if let Some(rev) = rev { - let reference = repository.revparse_single(rev)?; - reference.peel_to_commit()? - } else { - repository.head()?.peel_to_commit()? - }; - + let commit = rev_to_commit(&repository, rev)?; let tree = commit.tree()?; tree.walk(git2::TreeWalkMode::PreOrder, |path, entry| { @@ -107,17 +104,6 @@ pub async fn search( Ok(entries) } -#[cfg(unix)] -pub fn bytes2path(b: &[u8]) -> &Path { - use std::os::unix::prelude::*; - Path::new(std::ffi::OsStr::from_bytes(b)) -} -#[cfg(windows)] -pub fn bytes2path(b: &[u8]) -> &Path { - use std::str; - Path::new(str::from_utf8(b).unwrap()) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/tabby-git/src/grep/mod.rs b/crates/tabby-git/src/grep/mod.rs new file mode 100644 index 000000000000..1c31aff52857 --- /dev/null +++ b/crates/tabby-git/src/grep/mod.rs @@ -0,0 +1,201 @@ +mod output; +mod query; +mod searcher; + +use std::path::PathBuf; + +use anyhow::Context; +use async_stream::stream; +use base64::engine::general_purpose::STANDARD; +use base64_serde::base64_serde_type; +use futures::Stream; +use git2::TreeWalkResult; +pub use query::{GrepQuery, GrepQueryBuilder}; +use serde::Serialize; +use tracing::warn; + +base64_serde_type!(Base64Standard, STANDARD); + +use searcher::GrepSearcher; + +use super::{bytes2path, rev_to_commit}; + +#[derive(Serialize)] +pub struct GrepFile { + pub path: PathBuf, + pub lines: Vec, +} + +#[derive(Serialize)] +pub struct GrepLine { + /// Content of the line. + pub line: GrepTextOrBase64, + + /// Byte offset in the file to the start of the line. + pub byte_offset: usize, + + /// Line number in the file, starting from 1. + pub line_number: usize, + + /// The matches in the line. + pub sub_matches: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +pub enum GrepTextOrBase64 { + Text(String), + Base64(#[serde(with = "Base64Standard")] Vec), +} + +#[derive(Serialize)] +pub struct GrepSubMatch { + // Byte offsets in the line + pub bytes_start: usize, + pub bytes_end: usize, +} + +pub fn grep( + repository: git2::Repository, + rev: Option<&str>, + query: &GrepQuery, +) -> anyhow::Result> { + let (tx, mut rx) = tokio::sync::mpsc::channel(1); + + let rev = rev.map(|s| s.to_owned()); + let query = query.clone(); + let searcher = query.searcher()?; + let task = + tokio::task::spawn_blocking(move || grep_impl(repository, rev.as_deref(), searcher, tx)); + + Ok(stream! { + while let Some(file) = rx.recv().await { + yield file; + } + + if let Err(err) = task.await { + warn!("Error grepping repository: {}", err); + } + }) +} + +fn grep_impl( + repository: git2::Repository, + rev: Option<&str>, + mut searcher: GrepSearcher, + tx: tokio::sync::mpsc::Sender, +) -> anyhow::Result<()> { + let commit = rev_to_commit(&repository, rev)?; + let tree = commit.tree()?; + + tree.walk(git2::TreeWalkMode::PreOrder, |path, entry| { + // Skip non-blob entries + if entry.kind() != Some(git2::ObjectType::Blob) { + return TreeWalkResult::Ok; + } + + match grep_file(&repository, &mut searcher, path, entry, tx.clone()) { + Ok(()) => {} + Err(e) => { + warn!("Error grepping file: {}", e); + } + } + TreeWalkResult::Ok + })?; + Ok(()) +} + +fn grep_file( + repository: &git2::Repository, + searcher: &mut GrepSearcher, + path: &str, + entry: &git2::TreeEntry, + tx: tokio::sync::mpsc::Sender, +) -> anyhow::Result<()> { + let object = entry.to_object(repository)?; + let content = object.as_blob().context("Not a blob")?.content(); + + let path = PathBuf::from(path).join(bytes2path(entry.name_bytes())); + + let mut output = output::GrepOutput::new(path.clone(), tx.clone()); + searcher.search(content, &mut output)?; + output.flush(searcher.require_file_match, searcher.require_content_match); + + Ok(()) +} +#[cfg(test)] +mod tests { + use futures::StreamExt; + + use super::*; + use crate::testutils::TempGitRepository; + + #[tokio::test] + async fn test_grep() { + let root = TempGitRepository::default(); + let query = GrepQuery::builder().pattern("crosscodeeval_data").build(); + + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, PathBuf::from("203_llm_evaluation/README.md")); + + let query = GrepQuery::builder().pattern("ideas").build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, PathBuf::from("README.md")); + + let query = GrepQuery::builder() + .file_type("markdown") + .file_pattern("llm_evaluation") + .build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, PathBuf::from("203_llm_evaluation/README.md")); + + // When positive condition provided, return nothing if no matches. + let query = GrepQuery::builder() + .file_type("markdown") + .pattern("non_exist_pattern") + .build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 0); + + // When no positive condition provided, all + // files not matching negative conditions should be returned. + let query = GrepQuery::builder() + .negative_file_type("rust") + .negative_pattern("non_exist_pattern") + .build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 9); + + let query = GrepQuery::builder() + .pattern("non_exist_pattern") + .negative_file_pattern("non_exist_pattern") + .negative_pattern("ideas") + .build(); + let files: Vec<_> = grep(root.repository(), None, &query) + .unwrap() + .collect() + .await; + assert_eq!(files.len(), 0); + + let query = GrepQuery::builder().build(); + assert!(grep(root.repository(), None, &query).is_err()); + } +} diff --git a/crates/tabby-git/src/grep/output.rs b/crates/tabby-git/src/grep/output.rs new file mode 100644 index 000000000000..1b0d1aa4b129 --- /dev/null +++ b/crates/tabby-git/src/grep/output.rs @@ -0,0 +1,169 @@ +use std::path::{Path, PathBuf}; + +use grep::{matcher::Matcher, regex::RegexMatcher, searcher::Sink}; + +use super::{GrepFile, GrepLine, GrepSubMatch, GrepTextOrBase64}; + +pub struct GrepOutput { + path: PathBuf, + lines: Vec, + + tx: tokio::sync::mpsc::Sender, + + content_matched: bool, + content_negated: bool, + + pub file_matched: bool, + pub file_negated: bool, +} + +impl std::fmt::Debug for GrepOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GrepOutput") + .field("content_matched", &self.content_matched) + .field("content_negated", &self.content_negated) + .field("file_matched", &self.file_matched) + .field("file_negated", &self.file_negated) + .finish() + } +} + +impl GrepOutput { + pub fn new(path: PathBuf, tx: tokio::sync::mpsc::Sender) -> Self { + Self { + path: path.to_owned(), + lines: Vec::new(), + tx, + + file_matched: false, + file_negated: false, + + content_matched: false, + content_negated: false, + } + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn sink<'output, 'a>( + &'output mut self, + matcher: &'a RegexMatcher, + ) -> GrepMatchSink<'output, 'a> { + GrepMatchSink { + output: self, + matcher, + } + } + + pub fn negative_sink(&mut self) -> GrepNegativeMatchSink<'_> { + GrepNegativeMatchSink { output: self } + } + + fn record(&mut self, line: GrepLine) { + self.lines.push(line); + } + + pub fn flush(&mut self, require_file_match: bool, require_content_match: bool) { + // If file or content is negated, we don't want to send the file. + if self.file_negated || self.content_negated { + return; + } + + if require_file_match && !self.file_matched { + return; + } + + if require_content_match && !self.content_matched { + return; + } + + let file = GrepFile { + path: self.path.clone(), + lines: std::mem::take(&mut self.lines), + }; + self.tx.blocking_send(file).expect("Send file"); + } +} + +pub struct GrepMatchSink<'output, 'a> { + output: &'output mut GrepOutput, + matcher: &'a RegexMatcher, +} + +impl<'output, 'a> Sink for GrepMatchSink<'output, 'a> { + type Error = std::io::Error; + + fn matched( + &mut self, + _searcher: &grep::searcher::Searcher, + mat: &grep::searcher::SinkMatch<'_>, + ) -> Result { + self.output.content_matched = true; + + // 1. Search is always done in single-line mode. + let line = mat.lines().next().expect("Have at least one line"); + + // 2. Collect all matches in the line. + let mut matches: Vec = vec![]; + self.matcher.find_iter(line, |m| { + matches.push(GrepSubMatch { + bytes_start: m.start(), + bytes_end: m.end(), + }); + true + })?; + + let line = GrepTextOrBase64::Base64(line.to_owned()); + + // 3. Create a GrepLine object and add it to the file. + let line = GrepLine { + line, + byte_offset: mat.absolute_byte_offset() as usize, + line_number: mat.line_number().expect("Have line number") as usize, + sub_matches: matches, + }; + + self.output.record(line); + Ok(true) + } + + fn context( + &mut self, + _searcher: &grep::searcher::Searcher, + context: &grep::searcher::SinkContext<'_>, + ) -> Result { + let line = context.bytes(); + + let line = match std::str::from_utf8(line) { + Ok(s) => GrepTextOrBase64::Text(s.to_owned()), + Err(_) => GrepTextOrBase64::Base64(line.to_owned()), + }; + + self.output.record(GrepLine { + line, + byte_offset: context.absolute_byte_offset() as usize, + line_number: context.line_number().expect("Have line number") as usize, + sub_matches: vec![], + }); + Ok(true) + } +} + +pub struct GrepNegativeMatchSink<'output> { + output: &'output mut GrepOutput, +} + +impl<'output> Sink for GrepNegativeMatchSink<'output> { + type Error = std::io::Error; + + fn matched( + &mut self, + _searcher: &grep::searcher::Searcher, + _mat: &grep::searcher::SinkMatch<'_>, + ) -> Result { + self.output.content_negated = true; + Ok(false) + } +} diff --git a/crates/tabby-git/src/grep/query.rs b/crates/tabby-git/src/grep/query.rs new file mode 100644 index 000000000000..9d4970e15d5b --- /dev/null +++ b/crates/tabby-git/src/grep/query.rs @@ -0,0 +1,141 @@ +use anyhow::bail; +use grep::{ + regex::RegexMatcher, + searcher::{BinaryDetection, SearcherBuilder}, +}; +use ignore::types::TypesBuilder; + +use super::searcher::GrepSearcher; + +#[derive(Default, Clone)] +pub struct GrepQuery { + patterns: Vec, + negative_patterns: Vec, + + file_patterns: Vec, + negative_file_patterns: Vec, + + file_types: Vec, + negative_file_types: Vec, +} + +impl GrepQuery { + pub fn builder() -> GrepQueryBuilder { + GrepQueryBuilder::default() + } + + pub fn searcher(&self) -> anyhow::Result { + let pattern_matcher = if self.patterns.is_empty() { + None + } else { + Some(RegexMatcher::new_line_matcher(&self.patterns.join("|"))?) + }; + + let negative_pattern_matcher = if self.negative_patterns.is_empty() { + None + } else { + Some(RegexMatcher::new_line_matcher( + &self.negative_patterns.join("|"), + )?) + }; + + let file_pattern_matcher = if self.file_patterns.is_empty() { + None + } else { + Some(RegexMatcher::new_line_matcher( + &self.file_patterns.join("|"), + )?) + }; + + let negative_file_pattern_matcher = if self.negative_file_patterns.is_empty() { + None + } else { + Some(RegexMatcher::new_line_matcher( + &self.negative_file_patterns.join("|"), + )?) + }; + + if pattern_matcher.is_none() + && negative_pattern_matcher.is_none() + && file_pattern_matcher.is_none() + && negative_file_pattern_matcher.is_none() + { + bail!("No patterns specified") + } + + let file_type_matcher = if self.file_types.is_empty() && self.negative_file_types.is_empty() + { + None + } else { + let mut types_builder = TypesBuilder::new(); + types_builder.add_defaults(); + for file_type in &self.file_types { + types_builder.select(file_type); + } + for file_type in &self.negative_file_types { + types_builder.negate(file_type); + } + + Some(types_builder.build()?) + }; + + let searcher = SearcherBuilder::new() + .binary_detection(BinaryDetection::quit(b'\x00')) + .before_context(3) + .line_number(true) + .after_context(3) + .build(); + + Ok(GrepSearcher::new( + !self.file_patterns.is_empty() || !self.file_types.is_empty(), + !self.patterns.is_empty(), + pattern_matcher, + negative_pattern_matcher, + file_pattern_matcher, + negative_file_pattern_matcher, + file_type_matcher, + searcher, + )) + } +} + +#[derive(Default)] +pub struct GrepQueryBuilder { + query: GrepQuery, +} + +impl GrepQueryBuilder { + pub fn pattern>(mut self, pattern: T) -> Self { + self.query.patterns.push(pattern.into()); + self + } + + pub fn negative_pattern>(mut self, pattern: T) -> Self { + self.query.negative_patterns.push(pattern.into()); + self + } + + pub fn file_pattern>(mut self, pattern: T) -> Self { + self.query.file_patterns.push(pattern.into()); + self + } + + pub fn negative_file_pattern>(mut self, pattern: T) -> Self { + self.query.negative_file_patterns.push(pattern.into()); + self + } + + pub fn file_type>(mut self, file_type: T) -> Self { + self.query.file_types.push(file_type.into()); + self + } + + pub fn negative_file_type>(mut self, file_type: T) -> Self { + self.query.negative_file_types.push(file_type.into()); + self + } + + pub fn build(self) -> GrepQuery { + self.query + } +} diff --git a/crates/tabby-git/src/grep/searcher.rs b/crates/tabby-git/src/grep/searcher.rs new file mode 100644 index 000000000000..0b8f0d00b659 --- /dev/null +++ b/crates/tabby-git/src/grep/searcher.rs @@ -0,0 +1,108 @@ +use std::path::Path; + +use grep::{matcher::Matcher, regex::RegexMatcher}; +use ignore::types::Types; + +use super::output::GrepOutput; + +pub struct GrepSearcher { + pub require_file_match: bool, + pub require_content_match: bool, + + pattern_matcher: Option, + negative_pattern_matcher: Option, + + file_pattern_matcher: Option, + negative_file_pattern_matcher: Option, + + file_type_matcher: Option, + searcher: grep::searcher::Searcher, +} + +pub enum GrepFileMatch { + NoPattern, + Matched, + NotMatched, +} + +impl GrepSearcher { + pub fn new( + require_file_match: bool, + require_content_match: bool, + pattern_matcher: Option, + negative_pattern_matcher: Option, + file_pattern_matcher: Option, + negative_file_pattern_matcher: Option, + file_type_matcher: Option, + searcher: grep::searcher::Searcher, + ) -> Self { + Self { + require_file_match, + require_content_match, + pattern_matcher, + negative_pattern_matcher, + file_pattern_matcher, + negative_file_pattern_matcher, + file_type_matcher, + searcher, + } + } + + fn file_matched(&self, path: &Path) -> anyhow::Result { + let path_bytes = path.display().to_string().into_bytes(); + if let Some(ref matcher) = self.negative_file_pattern_matcher { + if matcher.is_match(&path_bytes)? { + return Ok(GrepFileMatch::NotMatched); + } + } + + let mut matched = GrepFileMatch::NoPattern; + if let Some(ref file_type_matcher) = self.file_type_matcher { + match file_type_matcher.matched(path, false) { + ignore::Match::None => { + // Do nothing. + } + ignore::Match::Ignore(_) => { + return Ok(GrepFileMatch::NotMatched); + } + ignore::Match::Whitelist(_glob) => { + matched = GrepFileMatch::Matched; + } + }; + }; + + if let Some(ref matcher) = self.file_pattern_matcher { + if matcher.is_match(&path_bytes)? { + matched = GrepFileMatch::Matched; + } else { + matched = GrepFileMatch::NotMatched; + } + } + + Ok(matched) + } + + pub fn search(&mut self, content: &[u8], output: &mut GrepOutput) -> anyhow::Result<()> { + let file_matched = self.file_matched(output.path())?; + if let GrepFileMatch::NotMatched = file_matched { + output.file_negated = true; + return Ok(()); + } + + if let GrepFileMatch::Matched = file_matched { + output.file_matched = true; + } + + if let Some(ref matcher) = self.pattern_matcher { + self.searcher + .search_reader(matcher, content, output.sink(matcher))?; + }; + + if let Some(ref matcher) = self.negative_pattern_matcher { + self.searcher + .search_reader(matcher, content, output.negative_sink())?; + } + + Ok(()) + } +} diff --git a/crates/tabby-git/src/lib.rs b/crates/tabby-git/src/lib.rs index 221ef436d32f..94303cb75837 100644 --- a/crates/tabby-git/src/lib.rs +++ b/crates/tabby-git/src/lib.rs @@ -1,6 +1,7 @@ mod file_search; mod serve_git; +mod grep; use std::path::Path; use axum::{ @@ -8,6 +9,9 @@ use axum::{ http::{Response, StatusCode}, }; use file_search::GitFileSearch; +pub use grep::{ + grep, GrepFile, GrepLine, GrepQuery, GrepQueryBuilder, GrepSubMatch, GrepTextOrBase64, +}; pub async fn search_files( root: &Path, @@ -38,6 +42,28 @@ pub fn list_refs(root: &Path) -> anyhow::Result> { .collect()) } +fn rev_to_commit<'a>( + repository: &'a git2::Repository, + rev: Option<&str>, +) -> anyhow::Result> { + let commit = match rev { + Some(rev) => repository.revparse_single(rev)?.peel_to_commit()?, + None => repository.head()?.peel_to_commit()?, + }; + Ok(commit) +} + +#[cfg(unix)] +pub fn bytes2path(b: &[u8]) -> &Path { + use std::os::unix::prelude::*; + Path::new(std::ffi::OsStr::from_bytes(b)) +} +#[cfg(windows)] +pub fn bytes2path(b: &[u8]) -> &Path { + use std::str; + Path::new(str::from_utf8(b).unwrap()) +} + #[cfg(test)] mod testutils { use std::process::{Command, Stdio}; diff --git a/crates/tabby-git/src/serve_git.rs b/crates/tabby-git/src/serve_git.rs index 0b690514feb4..70b2fc6557c9 100644 --- a/crates/tabby-git/src/serve_git.rs +++ b/crates/tabby-git/src/serve_git.rs @@ -10,6 +10,8 @@ use git2::{AttrCheckFlags, Blob}; use mime_guess::Mime; use serde::Serialize; +use super::rev_to_commit; + const DIRECTORY_MIME_TYPE: &str = "application/vnd.directory+json"; fn resolve<'a>( @@ -17,12 +19,7 @@ fn resolve<'a>( rev: Option<&str>, relpath_str: Option<&str>, ) -> anyhow::Result> { - let commit = if let Some(rev) = rev { - let reference = repository.revparse_single(rev)?; - reference.peel_to_commit()? - } else { - repository.head()?.peel_to_commit()? - }; + let commit = rev_to_commit(repository, rev)?; let tree = commit.tree()?; let relpath = Path::new(relpath_str.unwrap_or(""));