Skip to main content

git_spawn/command/
grep.rs

1//! `git grep` — print lines matching a pattern.
2
3use crate::command::{CommandExecutor, CommandOutput, GitCommand};
4use crate::error::{Error, Result};
5use async_trait::async_trait;
6
7/// Builder for `git grep`.
8#[derive(Debug, Clone, Default)]
9pub struct GrepCommand {
10    /// Shared executor.
11    pub executor: CommandExecutor,
12    /// Pattern to search for.
13    pub pattern: Option<String>,
14    /// Tree-ishes to search (defaults to working tree if empty).
15    pub trees: Vec<String>,
16    /// Pathspecs.
17    pub paths: Vec<String>,
18    /// `-i` case-insensitive.
19    pub ignore_case: bool,
20    /// `-w` match whole word.
21    pub word_regexp: bool,
22    /// `-v` invert match.
23    pub invert: bool,
24    /// `-n` show line numbers.
25    pub line_number: bool,
26    /// `-c` count matches per file.
27    pub count: bool,
28    /// `-l` / `--files-with-matches`.
29    pub files_with_matches: bool,
30    /// `-L` / `--files-without-match`.
31    pub files_without_match: bool,
32    /// `--name-only`.
33    pub name_only: bool,
34    /// `-E` extended regex.
35    pub extended_regexp: bool,
36    /// `-F` fixed string.
37    pub fixed_strings: bool,
38    /// `-P` Perl regex.
39    pub perl_regexp: bool,
40    /// `--cached`.
41    pub cached: bool,
42    /// `--untracked`.
43    pub untracked: bool,
44    /// `--no-index`.
45    pub no_index: bool,
46    /// `--recurse-submodules`.
47    pub recurse_submodules: bool,
48}
49
50impl GrepCommand {
51    /// New grep with the given pattern.
52    pub fn new(pattern: impl Into<String>) -> Self {
53        Self {
54            pattern: Some(pattern.into()),
55            ..Self::default()
56        }
57    }
58
59    /// Search a tree-ish (e.g. `HEAD`, a commit, a branch).
60    pub fn tree(&mut self, t: impl Into<String>) -> &mut Self {
61        self.trees.push(t.into());
62        self
63    }
64
65    /// Filter by path.
66    pub fn path(&mut self, p: impl Into<String>) -> &mut Self {
67        self.paths.push(p.into());
68        self
69    }
70
71    /// `-i`.
72    pub fn ignore_case(&mut self) -> &mut Self {
73        self.ignore_case = true;
74        self
75    }
76
77    /// `-w`.
78    pub fn word_regexp(&mut self) -> &mut Self {
79        self.word_regexp = true;
80        self
81    }
82
83    /// `-v`.
84    pub fn invert(&mut self) -> &mut Self {
85        self.invert = true;
86        self
87    }
88
89    /// `-n`.
90    pub fn line_number(&mut self) -> &mut Self {
91        self.line_number = true;
92        self
93    }
94
95    /// `-c`.
96    pub fn count(&mut self) -> &mut Self {
97        self.count = true;
98        self
99    }
100
101    /// `-l`.
102    pub fn files_with_matches(&mut self) -> &mut Self {
103        self.files_with_matches = true;
104        self
105    }
106
107    /// `-L`.
108    pub fn files_without_match(&mut self) -> &mut Self {
109        self.files_without_match = true;
110        self
111    }
112
113    /// `--name-only`.
114    pub fn name_only(&mut self) -> &mut Self {
115        self.name_only = true;
116        self
117    }
118
119    /// `-E`.
120    pub fn extended_regexp(&mut self) -> &mut Self {
121        self.extended_regexp = true;
122        self
123    }
124
125    /// `-F`.
126    pub fn fixed_strings(&mut self) -> &mut Self {
127        self.fixed_strings = true;
128        self
129    }
130
131    /// `-P`.
132    pub fn perl_regexp(&mut self) -> &mut Self {
133        self.perl_regexp = true;
134        self
135    }
136
137    /// `--cached`.
138    pub fn cached(&mut self) -> &mut Self {
139        self.cached = true;
140        self
141    }
142
143    /// `--untracked`.
144    pub fn untracked(&mut self) -> &mut Self {
145        self.untracked = true;
146        self
147    }
148
149    /// `--no-index`.
150    pub fn no_index(&mut self) -> &mut Self {
151        self.no_index = true;
152        self
153    }
154
155    /// `--recurse-submodules`.
156    pub fn recurse_submodules(&mut self) -> &mut Self {
157        self.recurse_submodules = true;
158        self
159    }
160}
161
162#[async_trait]
163impl GitCommand for GrepCommand {
164    type Output = CommandOutput;
165    fn get_executor(&self) -> &CommandExecutor {
166        &self.executor
167    }
168    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
169        &mut self.executor
170    }
171    fn build_command_args(&self) -> Vec<String> {
172        let mut args = vec!["grep".to_string()];
173        if self.ignore_case {
174            args.push("-i".into());
175        }
176        if self.word_regexp {
177            args.push("-w".into());
178        }
179        if self.invert {
180            args.push("-v".into());
181        }
182        if self.line_number {
183            args.push("-n".into());
184        }
185        if self.count {
186            args.push("-c".into());
187        }
188        if self.files_with_matches {
189            args.push("-l".into());
190        }
191        if self.files_without_match {
192            args.push("-L".into());
193        }
194        if self.name_only {
195            args.push("--name-only".into());
196        }
197        if self.extended_regexp {
198            args.push("-E".into());
199        }
200        if self.fixed_strings {
201            args.push("-F".into());
202        }
203        if self.perl_regexp {
204            args.push("-P".into());
205        }
206        if self.cached {
207            args.push("--cached".into());
208        }
209        if self.untracked {
210            args.push("--untracked".into());
211        }
212        if self.no_index {
213            args.push("--no-index".into());
214        }
215        if self.recurse_submodules {
216            args.push("--recurse-submodules".into());
217        }
218        if let Some(p) = &self.pattern {
219            args.push("-e".into());
220            args.push(p.clone());
221        }
222        args.extend(self.trees.iter().cloned());
223        if !self.paths.is_empty() {
224            args.push("--".into());
225            args.extend(self.paths.iter().cloned());
226        }
227        args
228    }
229    async fn execute(&self) -> Result<CommandOutput> {
230        if self.pattern.is_none() {
231            return Err(Error::invalid_config("grep requires a pattern"));
232        }
233        // `git grep` exits 1 on "no matches" which we surface as CommandFailed.
234        // Callers that want to distinguish should match on Error::CommandFailed.
235        self.execute_raw().await
236    }
237}