1#![cfg(unix)]
17
18use super::repo_read_config_scan::{ScopeEnvironment, content_key, entries_avoid_keys, outer_scopes_avoid_keys};
19use super::repo_read_config_text::{CONFIG_TEXT_LIMIT, parse_config_text};
20use super::repo_read_diff::{PathFilter, diff_sorted, name_lines, parse_diff_filter, raw_lines, select};
21use super::repo_read_index::{INTENT_TO_ADD, IndexEntry, parse_index};
22use super::repo_read_layout::{RepoLayout, env_config_avoids, read_layout_with};
23use super::repo_read_object::{ObjectKind, from_hex, name_length, to_hex};
24
25use super::repo_read_odb::ObjectDatabase;
26use super::repo_read_query_refs::{config_list, for_each_ref, remote_url, rev_parse, symbolic_ref};
27use super::repo_read_tree::{FlatEntry, flatten_tree, parse_commit, subject_of};
28use std::ffi::{OsStr, OsString};
29use std::path::{Path, PathBuf};
30
31pub struct Query<'a> {
33 pub arguments: &'a [OsString],
35 pub directory: Option<&'a Path>,
37 pub variables: &'a [(OsString, OsString)],
39 pub input: Option<&'a [u8]>,
41}
42
43#[derive(Clone, Debug, Eq, PartialEq)]
45pub struct Answer {
46 pub stdout: Vec<u8>,
48 pub stderr: Vec<u8>,
50 pub code: i32,
52}
53
54pub(super) struct Parsed<'a> {
56 pub(super) command: &'a str,
58 pub(super) flags: Vec<&'a str>,
60 pub(super) operands: Vec<&'a [u8]>,
62 pub(super) specs: Vec<&'a [u8]>,
64}
65
66fn parse<'a>(arguments: &'a [OsString], start: &mut PathBuf) -> Option<Parsed<'a>> {
68 let mut index: usize = 0;
69 while index < arguments.len() {
70 let word: &OsStr = arguments[index].as_os_str();
71 if word == "-C" {
72 let target: &OsString = arguments.get(index + 1)?;
73 if !target.is_empty() {
74 *start = start.join(target);
75 }
76 if !std::fs::metadata(&*start).is_ok_and(|found| { return found.is_dir(); }) {
78 return None;
79 }
80 index += 2;
81 } else if word == "--no-literal-pathspecs" {
82 index += 1;
83 } else {
84 break;
85 }
86 }
87 let command: &str = arguments.get(index)?.to_str()?;
88 let mut parsed: Parsed<'a> = Parsed { command, flags: Vec::new(), operands: Vec::new(), specs: Vec::new() };
89 let mut after_separator: bool = false;
90 for argument in &arguments[index + 1..] {
91 let bytes: &[u8] = argument.as_encoded_bytes();
92 if after_separator {
93 parsed.specs.push(bytes);
94 } else if bytes == b"--" {
95 after_separator = true;
96 } else if bytes.starts_with(b"-") {
97 parsed.flags.push(argument.to_str()?);
98 } else {
99 parsed.operands.push(bytes);
100 }
101 }
102 return Some(parsed);
103}
104
105pub(super) fn flags_are(flags: &[&str], required: &[&str], allowed: &[&str]) -> bool {
107 return required.iter().all(|flag| { return flags.contains(flag); })
108 && flags.iter().all(|flag| { return allowed.contains(flag) || required.contains(flag); })
109 && flags.iter().enumerate().all(|(position, flag)| { return !flags[..position].contains(flag); });
110}
111
112fn environment(
114 base: Vec<(OsString, OsString)>,
115 variables: &[(OsString, OsString)],
116) -> (Vec<(OsString, OsString)>, Option<OsString>, Option<OsString>) {
117 let mut all: Vec<(OsString, OsString)> = base
118 .into_iter()
119 .filter(|(name, _)| { return !variables.iter().any(|(added, _)| { return added == name; }); })
120 .collect();
121 all.extend(variables.iter().cloned());
122 let find = |wanted: &str| -> Option<OsString> {
123 return all.iter().rev().find(|(name, _)| { return name == wanted; }).map(|(_, value)| { return value.clone(); });
125 };
126 let index: Option<OsString> = find("GIT_INDEX_FILE");
127 let objects: Option<OsString> = find("GIT_OBJECT_DIRECTORY");
128 all.retain(|(name, _)| { return name != "GIT_INDEX_FILE" && name != "GIT_OBJECT_DIRECTORY"; });
129 return (all, index, objects);
130}
131
132fn content_config_is_plain(layout: &RepoLayout, scope: &ScopeEnvironment) -> Option<bool> {
134 let path: PathBuf = layout.common_dir.join("config");
135 let bytes: Vec<u8> = super::private_storage::read_regular_file(path.as_path(), CONFIG_TEXT_LIMIT as u64).ok()?;
136 let entries = parse_config_text(bytes.as_slice())?;
137 if !entries_avoid_keys(entries.as_slice(), path.as_path(), scope.home.as_deref(), 0, &content_key)? {
138 return Some(false);
139 }
140 return outer_scopes_avoid_keys(scope, &content_key);
141}
142
143pub(super) struct Context {
145 pub(super) layout: RepoLayout,
147 index_path: PathBuf,
149 objects: Option<PathBuf>,
151 pub(super) scope: ScopeEnvironment,
153 pub(super) variables: Vec<(OsString, OsString)>,
155}
156
157impl Context {
158 pub(super) fn database(&self) -> Option<ObjectDatabase> {
160 return ObjectDatabase::open(self.layout.common_dir.as_path(), self.objects.as_deref(), self.layout.object_format);
161 }
162
163 fn index(&self) -> Option<Vec<IndexEntry>> {
165 let bytes: Vec<u8> = super::private_storage::read_regular_file(self.index_path.as_path(), 1 << 30).ok()?;
166 return parse_index(bytes.as_slice(), self.layout.object_format);
167 }
168
169 fn resolved_index(&self) -> Option<Vec<FlatEntry>> {
171 let entries: Vec<IndexEntry> = self.index()?;
172 if entries.iter().any(|entry| { return entry.stage != 0 || entry.extended & INTENT_TO_ADD != 0; }) {
173 return None;
174 }
175 return Some(entries.into_iter().map(|entry| { return FlatEntry { path: entry.path, mode: entry.mode, id: entry.id }; }).collect());
176 }
177
178 fn tree_of(&self, database: &ObjectDatabase, hex: &[u8]) -> Option<Vec<u8>> {
180 let name: Vec<u8> = from_hex(hex, self.layout.object_format)?;
181 let object = database.read(name.as_slice())?;
182 return match object.kind {
183 ObjectKind::Tree => Some(name),
184 ObjectKind::Commit => Some(parse_commit(object.bytes.as_slice(), self.layout.object_format)?.tree),
185 ObjectKind::Blob | ObjectKind::Tag => None,
186 };
187 }
188
189 fn zero(&self) -> String {
191 return "0".repeat(self.layout.object_format.hex_length());
192 }
193}
194
195fn list_files(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
197 let unmerged: bool = parsed.flags.contains(&"--unmerged");
198 let required: [&str; 2] = [if unmerged { "--unmerged" } else { "--stage" }, "-z"];
199 if !flags_are(&parsed.flags, &required, &["--full-name"]) || !parsed.operands.is_empty() || !context.layout.prefix.is_empty() {
200 return None;
201 }
202 let filter: PathFilter = PathFilter::parse(parsed.specs.as_slice(), true)?;
203 let mut stdout: Vec<u8> = Vec::new();
204 for entry in context.index()? {
205 if (unmerged && entry.stage == 0) || !filter.matches(entry.path.as_slice()) {
206 continue;
207 }
208 stdout.extend_from_slice(format!("{:06o} {} {}\t", entry.mode, to_hex(entry.id.as_slice()), entry.stage).as_bytes());
209 stdout.extend_from_slice(entry.path.as_slice());
210 stdout.push(0);
211 }
212 return Some(Answer { stdout, stderr: Vec::new(), code: 0 });
213}
214
215fn diff_cached(context: &Context, parsed: &Parsed<'_>, porcelain: bool) -> Option<Answer> {
217 let raw: bool = parsed.flags.contains(&"--raw");
218 let mut required: Vec<&str> = vec!["--cached", "-z"];
219 required.push(if raw { "--raw" } else { "--name-only" });
220 if raw {
221 required.push("--no-abbrev");
222 }
223 let mut statuses: Option<Vec<u8>> = None;
224 let mut others: Vec<&str> = Vec::new();
225 for flag in &parsed.flags {
226 match flag.strip_prefix("--diff-filter=") {
227 Some(letters) if !porcelain && statuses.is_none() => statuses = Some(parse_diff_filter(letters)?),
229 Some(_) => return None,
230 _ => others.push(flag),
231 }
232 }
233 let allowed: &[&str] = if porcelain { &[] } else { &["--no-renames"] };
234 if (porcelain && raw) || !flags_are(&others, &required, allowed) || parsed.operands.len() != 1 || (porcelain && !parsed.specs.is_empty()) {
235 return None;
236 }
237 let filter: PathFilter = PathFilter::parse(parsed.specs.as_slice(), context.layout.prefix.is_empty())?;
238 let database: ObjectDatabase = context.database()?;
239 let tree: Vec<u8> = context.tree_of(&database, parsed.operands[0])?;
240 let old: Vec<FlatEntry> = flatten_tree(&database, tree.as_slice(), context.layout.object_format)?;
241 let changes = diff_sorted(old.as_slice(), context.resolved_index()?.as_slice())?;
242 if porcelain && changes.iter().any(|change| { return change.status == b'A'; }) && changes.iter().any(|change| { return change.status == b'D'; }) {
244 return None;
245 }
246 let kept = select(changes, &filter, statuses.as_deref());
247 let stdout: Vec<u8> = if raw { raw_lines(kept.as_slice(), context.zero().as_str()) } else { name_lines(kept.as_slice()) };
248 return Some(Answer { stdout, stderr: Vec::new(), code: 0 });
249}
250
251fn diff_tree(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
253 let required: [&str; 7] = ["--root", "--no-commit-id", "-r", "-z", "-m", "--no-renames", "--no-abbrev"];
254 if !flags_are(&parsed.flags, &required, &[]) || parsed.operands.len() != 1 || !parsed.specs.is_empty() {
255 return None;
256 }
257 let database: ObjectDatabase = context.database()?;
258 let name: Vec<u8> = from_hex(parsed.operands[0], context.layout.object_format)?;
259 let object = database.read(name.as_slice())?;
260 if object.kind != ObjectKind::Commit {
261 return None;
262 }
263 let commit = parse_commit(object.bytes.as_slice(), context.layout.object_format)?;
264 if commit.parents.len() > 1 {
265 return None;
266 }
267 let new: Vec<FlatEntry> = flatten_tree(&database, commit.tree.as_slice(), context.layout.object_format)?;
268 let old: Vec<FlatEntry> = match commit.parents.first() {
269 Some(parent) => {
270 let parent_object = database.read(parent.as_slice())?;
271 if parent_object.kind != ObjectKind::Commit {
272 return None;
273 }
274 let parent_tree = parse_commit(parent_object.bytes.as_slice(), context.layout.object_format)?.tree;
275 flatten_tree(&database, parent_tree.as_slice(), context.layout.object_format)?
276 }
277 None => Vec::new(),
278 };
279 let changes = diff_sorted(old.as_slice(), new.as_slice())?;
280 return Some(Answer { stdout: raw_lines(changes.as_slice(), context.zero().as_str()), stderr: Vec::new(), code: 0 });
281}
282
283fn show_subject(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
285 if !flags_are(&parsed.flags, &["--no-patch", "--format=%s"], &[]) || parsed.operands.len() != 1 || !parsed.specs.is_empty() {
286 return None;
287 }
288 let database: ObjectDatabase = context.database()?;
289 let name: Vec<u8> = from_hex(parsed.operands[0], context.layout.object_format)?;
290 let object = database.read(name.as_slice())?;
291 if object.kind != ObjectKind::Commit {
292 return None;
293 }
294 let commit = parse_commit(object.bytes.as_slice(), context.layout.object_format)?;
295 let mut stdout: Vec<u8> = subject_of(commit.message.as_slice())?;
296 stdout.push(b'\n');
297 return Some(Answer { stdout, stderr: Vec::new(), code: 0 });
298}
299
300fn batch_objects(context: &Context, parsed: &Parsed<'_>, input: Option<&[u8]>) -> Option<Answer> {
302 if !flags_are(&parsed.flags, &["--batch"], &[]) || !parsed.operands.is_empty() || !parsed.specs.is_empty() {
303 return None;
304 }
305 let database: ObjectDatabase = context.database()?;
306 let mut stdout: Vec<u8> = Vec::new();
307 let text: &[u8] = input?.strip_suffix(b"\n")?;
308 for line in text.split(|byte| { return *byte == b'\n'; }) {
309 let name: Vec<u8> = from_hex(line, context.layout.object_format)?;
310 let object = database.read(name.as_slice())?;
312 stdout.extend_from_slice(format!("{} {} {}\n", to_hex(name.as_slice()), object.kind.word(), object.bytes.len()).as_bytes());
313 stdout.extend_from_slice(object.bytes.as_slice());
314 stdout.push(b'\n');
315 }
316 return Some(Answer { stdout, stderr: Vec::new(), code: 0 });
317}
318
319pub fn answer(query: &Query<'_>) -> Option<Answer> {
330 let mut base: Vec<(OsString, OsString)> = std::env::vars_os().collect();
331 base.extend(query.variables.iter().cloned());
332 let scope: ScopeEnvironment = ScopeEnvironment::from_variables(base.as_slice());
333 return answer_with(query, std::env::vars_os().collect(), &scope);
334}
335
336pub fn answer_with(query: &Query<'_>, base: Vec<(OsString, OsString)>, scope: &ScopeEnvironment) -> Option<Answer> {
338 let mut start: PathBuf = match query.directory {
339 Some(directory) => directory.to_path_buf(),
340 None => std::env::current_dir().ok()?,
341 };
342 let parsed: Parsed<'_> = parse(query.arguments, &mut start)?;
343 if !matches!(parsed.command, "ls-files" | "diff-index" | "diff" | "diff-tree" | "show" | "config" | "remote" | "rev-parse" | "for-each-ref" | "symbolic-ref" | "cat-file") {
344 return None;
345 }
346 let (variables, index_variable, objects_variable) = environment(base, query.variables);
347 let layout: RepoLayout = read_layout_with(start.as_path(), variables.as_slice(), scope)?;
348 let index_path: PathBuf = match index_variable {
349 Some(file) => start.join(file),
350 None => layout.git_dir.join("index"),
351 };
352 let objects: Option<PathBuf> = objects_variable.map(|directory| { return start.join(directory); });
353 let content_command: bool = matches!(parsed.command, "ls-files" | "diff-index" | "diff" | "diff-tree" | "show");
354 if content_command && !content_config_is_plain(&layout, scope)? {
355 return None;
356 }
357 if content_command && !env_config_avoids(variables.as_slice(), &|key: &str| { return content_key(key.to_ascii_lowercase().as_str()); }) {
359 return None;
360 }
361 let context: Context = Context { layout, index_path, objects, scope: scope.clone(), variables };
362 if name_length(context.layout.object_format) == 0 {
363 return None;
364 }
365 return match parsed.command {
366 "ls-files" => list_files(&context, &parsed),
367 "diff-index" => diff_cached(&context, &parsed, false),
368 "diff" => diff_cached(&context, &parsed, true),
369 "diff-tree" => diff_tree(&context, &parsed),
370 "show" => show_subject(&context, &parsed),
371 "config" => config_list(&context, &parsed),
372 "remote" => remote_url(&context, &parsed),
373 "for-each-ref" => for_each_ref(&context, &parsed),
374 "rev-parse" => rev_parse(&context, &parsed),
375 "symbolic-ref" => symbolic_ref(&context, &parsed),
376 "cat-file" => batch_objects(&context, &parsed, query.input),
377 _ => None,
378 };
379}
380
381#[cfg(test)]
382#[path = "repo_read_query_tests.rs"]
383pub(crate) mod tests;