#![cfg(unix)]
use crate::repo_read_config_scan::{ScopeEnvironment, content_key, entries_avoid_keys, file_avoids_keys, last_variable, outer_scopes_avoid_keys};
use crate::repo_read_config_text::{CONFIG_TEXT_LIMIT, parse_config_text};
use crate::repo_read_diff::{PathFilter, diff_sorted, name_lines, parse_diff_filter, raw_lines, select};
use crate::repo_read_layout::{RepoLayout, env_config_avoids, read_layout_with};
use crate::repo_read_object::{ObjectKind, from_hex, name_length, to_hex};
use crate::repo_read_odb::ObjectDatabase;
use crate::repo_read_query_refs::{config_list, for_each_ref, remote_url, rev_parse, symbolic_ref};
use crate::index_cache_tree::CacheTree;
use crate::repo_read_tree_cached::changes_with_cache;
use crate::repo_read_tree::{FlatEntry, flatten_pair, flatten_tree_shared, parse_commit, subject_of};
use std::ffi::OsString;
use std::path::PathBuf;
use std::rc::Rc;
pub use crate::repo_read_query_request_answer_and_context::{Answer, Query};
use crate::repo_read_query_request_answer_and_context::{Context, Parsed, flags_are, parse};
fn is_newline(byte: &u8) -> bool {
return *byte == b'\n';
}
fn content_key_of_any_case(key: &str) -> bool {
return content_key(key.to_ascii_lowercase().as_str());
}
fn environment(
base: Vec<(OsString, OsString)>,
variables: &[(OsString, OsString)],
) -> (Vec<(OsString, OsString)>, Option<OsString>, Option<OsString>) {
let mut all: Vec<(OsString, OsString)> = Vec::new();
for (name, value) in base {
let mut replaced: bool = false;
for (added, _) in variables {
if *added == name {
replaced = true;
break;
}
}
if !replaced {
all.push((name, value));
}
}
all.extend(variables.iter().cloned());
let index: Option<OsString> = last_variable(all.as_slice(), "GIT_INDEX_FILE");
let objects: Option<OsString> = last_variable(all.as_slice(), "GIT_OBJECT_DIRECTORY");
let mut kept: Vec<(OsString, OsString)> = Vec::new();
for (name, value) in all {
if name != "GIT_INDEX_FILE" && name != "GIT_OBJECT_DIRECTORY" {
kept.push((name, value));
}
}
return (kept, index, objects);
}
fn content_config_is_plain(layout: &RepoLayout, scope: &ScopeEnvironment) -> Option<bool> {
let path: PathBuf = layout.common_dir.join("config");
let bytes: Vec<u8> = crate::private_storage::read_regular_file(path.as_path(), CONFIG_TEXT_LIMIT as u64).ok()?;
let entries = parse_config_text(bytes.as_slice())?;
if !entries_avoid_keys(entries.as_slice(), path.as_path(), scope.home.as_deref(), 0, &content_key)? {
return Some(false);
}
if !file_avoids_keys(layout.git_dir.join("config.worktree").as_path(), scope.home.as_deref(), 0, &content_key)? {
return Some(false);
}
return outer_scopes_avoid_keys(scope, &content_key);
}
fn list_files(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
let unmerged: bool = parsed.flags.contains(&"--unmerged");
let required: [&str; 2] = [if unmerged { "--unmerged" } else { "--stage" }, "-z"];
if !flags_are(&parsed.flags, &required, &["--full-name"]) || !parsed.operands.is_empty() || !context.layout.prefix.is_empty() {
return None;
}
let filter: PathFilter = PathFilter::parse(parsed.specs.as_slice(), true)?;
let mut stdout: Vec<u8> = Vec::new();
for entry in context.index()? {
if (unmerged && entry.stage == 0) || !filter.matches(entry.path.as_slice()) {
continue;
}
stdout.extend_from_slice(format!("{:06o} {} {}\t", entry.mode, to_hex(entry.id.as_slice()), entry.stage).as_bytes());
stdout.extend_from_slice(entry.path.as_slice());
stdout.push(0);
}
return Some(Answer { stdout, stderr: Vec::new(), code: 0 });
}
fn diff_cached(context: &Context, parsed: &Parsed<'_>, porcelain: bool) -> Option<Answer> {
if !porcelain && parsed.flags.contains(&"--quiet") {
return crate::repo_read_query_quiet::diff_index_quiet(context, parsed);
}
let raw: bool = parsed.flags.contains(&"--raw");
let mut required: Vec<&str> = vec!["--cached", "-z"];
required.push(if raw { "--raw" } else { "--name-only" });
if raw {
required.push("--no-abbrev");
}
let mut statuses: Option<Vec<u8>> = None;
let mut others: Vec<&str> = Vec::new();
for flag in &parsed.flags {
match flag.strip_prefix("--diff-filter=") {
Some(letters) if !porcelain && statuses.is_none() => statuses = Some(parse_diff_filter(letters)?),
Some(_) => return None,
_ => others.push(flag),
}
}
let allowed: &[&str] = if porcelain { &[] } else { &["--no-renames"] };
if (porcelain && raw) || !flags_are(&others, &required, allowed) || parsed.operands.len() != 1 || (porcelain && !parsed.specs.is_empty()) {
return None;
}
let filter: PathFilter = PathFilter::parse(parsed.specs.as_slice(), context.layout.prefix.is_empty())?;
let database: ObjectDatabase = context.database()?;
let tree: Vec<u8> = context.tree_of(&database, parsed.operands[0])?;
let (index, cache): (Vec<FlatEntry>, Option<CacheTree>) = context.resolved_index_with_tree()?;
let cached = match cache.as_ref() {
Some(nodes) => changes_with_cache(&database, tree.as_slice(), context.layout.object_format, nodes, index.as_slice()),
None => None,
};
let changes = match cached {
Some(found) => found,
None => {
let old: Rc<Vec<FlatEntry>> = flatten_tree_shared(&database, tree.as_slice(), context.layout.object_format)?;
diff_sorted(old.as_slice(), index.as_slice())?
}
};
if porcelain {
let mut added: bool = false;
let mut deleted: bool = false;
for change in &changes {
added = added || change.status == b'A';
deleted = deleted || change.status == b'D';
}
if added && deleted {
return None;
}
}
let kept = select(changes, &filter, statuses.as_deref());
let stdout: Vec<u8> = if raw { raw_lines(kept.as_slice(), context.zero().as_str()) } else { name_lines(kept.as_slice()) };
return Some(Answer { stdout, stderr: Vec::new(), code: 0 });
}
fn diff_tree(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
let required: [&str; 7] = ["--root", "--no-commit-id", "-r", "-z", "-m", "--no-renames", "--no-abbrev"];
if !flags_are(&parsed.flags, &required, &[]) || parsed.operands.len() != 1 || !parsed.specs.is_empty() {
return None;
}
let database: ObjectDatabase = context.database()?;
let name: Vec<u8> = from_hex(parsed.operands[0], context.layout.object_format)?;
let object = database.read(name.as_slice())?;
if object.kind != ObjectKind::Commit {
return None;
}
let commit = parse_commit(object.bytes.as_slice(), context.layout.object_format)?;
if commit.parents.len() > 1 {
return None;
}
let changes = match commit.parents.first() {
Some(parent) => {
let parent_object = database.read(parent.as_slice())?;
if parent_object.kind != ObjectKind::Commit {
return None;
}
let parent_tree = parse_commit(parent_object.bytes.as_slice(), context.layout.object_format)?.tree;
let (before, after): (Vec<FlatEntry>, Vec<FlatEntry>) = flatten_pair(&database, parent_tree.as_slice(), commit.tree.as_slice(), context.layout.object_format)?;
diff_sorted(before.as_slice(), after.as_slice())?
}
None => {
let new: Rc<Vec<FlatEntry>> = flatten_tree_shared(&database, commit.tree.as_slice(), context.layout.object_format)?;
diff_sorted(&[], new.as_slice())?
}
};
return Some(Answer { stdout: raw_lines(changes.as_slice(), context.zero().as_str()), stderr: Vec::new(), code: 0 });
}
fn diff_tree_names(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
if !parsed.specs.is_empty() {
return None;
}
let database: ObjectDatabase = context.database()?;
let (old, new): (Vec<u8>, Vec<u8>) = match parsed.operands.as_slice() {
[pair_old, pair_new] => {
if !flags_are(&parsed.flags, &["-r", "--name-only", "-z"], &["--no-renames"]) {
return None;
}
(context.tree_of(&database, pair_old)?, context.tree_of(&database, pair_new)?)
}
[single] => {
if !flags_are(&parsed.flags, &["--no-commit-id", "--name-only", "-r", "-z"], &[]) {
return None;
}
let name: Vec<u8> = from_hex(single, context.layout.object_format)?;
let object = database.read(name.as_slice())?;
if object.kind != ObjectKind::Commit {
return None;
}
let commit = parse_commit(object.bytes.as_slice(), context.layout.object_format)?;
let [parent] = commit.parents.as_slice() else { return None };
let parent_object = database.read(parent.as_slice())?;
if parent_object.kind != ObjectKind::Commit {
return None;
}
(parse_commit(parent_object.bytes.as_slice(), context.layout.object_format)?.tree, commit.tree)
}
_ => return None,
};
let (before, after): (Vec<FlatEntry>, Vec<FlatEntry>) = flatten_pair(&database, old.as_slice(), new.as_slice(), context.layout.object_format)?;
let changes = diff_sorted(before.as_slice(), after.as_slice())?;
return Some(Answer { stdout: name_lines(changes.as_slice()), stderr: Vec::new(), code: 0 });
}
fn show_subject(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
if !flags_are(&parsed.flags, &["--no-patch", "--format=%s"], &[]) || parsed.operands.len() != 1 || !parsed.specs.is_empty() {
return None;
}
let database: ObjectDatabase = context.database()?;
let name: Vec<u8> = from_hex(parsed.operands[0], context.layout.object_format)?;
let object = database.read(name.as_slice())?;
if object.kind != ObjectKind::Commit {
return None;
}
let commit = parse_commit(object.bytes.as_slice(), context.layout.object_format)?;
let mut stdout: Vec<u8> = subject_of(commit.message.as_slice())?;
stdout.push(b'\n');
return Some(Answer { stdout, stderr: Vec::new(), code: 0 });
}
fn batch_objects(context: &Context, parsed: &Parsed<'_>, input: Option<&[u8]>) -> Option<Answer> {
if !flags_are(&parsed.flags, &["--batch"], &[]) || !parsed.operands.is_empty() || !parsed.specs.is_empty() {
return None;
}
let database: ObjectDatabase = context.database()?;
let mut stdout: Vec<u8> = Vec::new();
let text: &[u8] = input?.strip_suffix(b"\n")?;
for line in text.split(is_newline) {
let name: Vec<u8> = from_hex(line, context.layout.object_format)?;
let object = database.read(name.as_slice())?;
stdout.extend_from_slice(format!("{} {} {}\n", to_hex(name.as_slice()), object.kind.word(), object.bytes.len()).as_bytes());
stdout.extend_from_slice(object.bytes.as_slice());
stdout.push(b'\n');
}
return Some(Answer { stdout, stderr: Vec::new(), code: 0 });
}
pub fn answer(query: &Query<'_>) -> Option<Answer> {
let mut base: Vec<(OsString, OsString)> = std::env::vars_os().collect();
base.extend(query.variables.iter().cloned());
let scope: ScopeEnvironment = ScopeEnvironment::from_variables(base.as_slice());
return answer_with(query, std::env::vars_os().collect(), &scope);
}
pub fn answer_with(query: &Query<'_>, base: Vec<(OsString, OsString)>, scope: &ScopeEnvironment) -> Option<Answer> {
let mut start: PathBuf = match query.directory {
Some(directory) => directory.to_path_buf(),
None => std::env::current_dir().ok()?,
};
let parsed: Parsed<'_> = parse(query.arguments, &mut start)?;
if !matches!(parsed.command, "ls-files" | "diff-index" | "diff" | "diff-tree" | "show" | "config" | "remote" | "rev-parse" | "for-each-ref" | "symbolic-ref" | "cat-file") {
return None;
}
let (variables, index_variable, objects_variable) = environment(base, query.variables);
let layout: RepoLayout = read_layout_with(start.as_path(), variables.as_slice(), scope)?;
let index_path: PathBuf = match index_variable {
Some(file) => start.join(file),
None => layout.git_dir.join("index"),
};
let mut objects: Option<PathBuf> = None;
if let Some(directory) = objects_variable {
objects = Some(start.join(directory));
}
let content_command: bool = matches!(parsed.command, "ls-files" | "diff-index" | "diff" | "diff-tree" | "show");
if content_command && !content_config_is_plain(&layout, scope)? {
return None;
}
if content_command && !env_config_avoids(variables.as_slice(), &content_key_of_any_case) {
return None;
}
let context: Context = Context { layout, index_path, objects, scope: scope.clone(), variables };
if name_length(context.layout.object_format) == 0 {
return None;
}
return match parsed.command {
"ls-files" => list_files(&context, &parsed),
"diff-index" => diff_cached(&context, &parsed, false),
"diff" => diff_cached(&context, &parsed, true),
"diff-tree" if parsed.flags.contains(&"--root") => diff_tree(&context, &parsed),
"diff-tree" => diff_tree_names(&context, &parsed),
"show" => show_subject(&context, &parsed),
"config" => config_list(&context, &parsed),
"remote" => remote_url(&context, &parsed),
"for-each-ref" => for_each_ref(&context, &parsed),
"rev-parse" => rev_parse(&context, &parsed),
"symbolic-ref" => symbolic_ref(&context, &parsed),
"cat-file" => batch_objects(&context, &parsed, query.input),
_ => None,
};
}
#[cfg(test)]
#[path = "repo_read_query_tests.rs"]
pub(crate) mod tests;
#[cfg(test)]
#[path = "repo_read_pair_tests.rs"]
mod pair_tests;