#![cfg(unix)]
use super::repo_read_config_list::{ListedEntry, list_config, listing_bytes};
use super::repo_read_object::{ObjectKind, from_hex};
use super::repo_read_odb::ObjectDatabase;
use super::repo_read_query::{Answer, Context, Parsed, flags_are};
use super::repo_read_refs::{Lookup, RefValue, Refs, is_plain_ref_name};
use std::ffi::OsString;
const LISTING_FORMAT: &str = "--format=%(refname)%00%(objectname)%00%(symref)";
const PEEL_LIMIT: usize = 8;
fn shared_refs(context: &Context) -> Option<Refs> {
if context.layout.git_dir != context.layout.common_dir {
return None;
}
return Some(Refs::new(context.layout.common_dir.as_path(), context.layout.object_format.hex_length()));
}
fn listing(context: &Context) -> Option<Vec<ListedEntry>> {
return list_config(context.layout.common_dir.as_path(), context.layout.git_dir.as_path(), &context.scope, context.variables.as_slice());
}
fn values_of<'a>(listed: &'a [ListedEntry], key: &str) -> Vec<&'a Option<Vec<u8>>> {
return listed.iter().filter(|item| { return item.entry.key == key; }).map(|item| { return &item.entry.value; }).collect();
}
fn is_terminal(refs: &Refs, name: &str) -> bool {
return matches!(refs.lookup(name), Lookup::Missing | Lookup::Found(RefValue::Direct(_)));
}
fn messages_are_english(variables: &[(OsString, OsString)]) -> bool {
let find = |name: &str| -> Option<String> {
return variables.iter().rev().find(|(candidate, _)| { return candidate == name; }).and_then(|(_, value)| { return value.to_str().map(str::to_string); }).filter(|value| { return !value.is_empty(); });
};
let english = |value: &str| -> bool { return value == "C" || value == "POSIX" || value.starts_with("en"); };
let locale: Option<String> = find("LC_ALL").or_else(|| { return find("LC_MESSAGES"); }).or_else(|| { return find("LANG"); });
return locale.as_deref().is_none_or(english) && find("LANGUAGE").as_deref().is_none_or(english);
}
pub(super) fn config_list(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
if !flags_are(&parsed.flags, &["--list", "-z", "--show-scope"], &[]) || !parsed.operands.is_empty() || !parsed.specs.is_empty() {
return None;
}
let listed: Vec<ListedEntry> = listing(context)?;
return Some(Answer { stdout: listing_bytes(listed.as_slice()), stderr: Vec::new(), code: 0 });
}
pub(super) fn remote_url(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
if !parsed.flags.is_empty() || !parsed.specs.is_empty() || parsed.operands.len() != 2 || parsed.operands[0] != b"get-url" {
return None;
}
let name: &str = std::str::from_utf8(parsed.operands[1]).ok()?;
if name.is_empty() || !name.bytes().all(|byte| { return byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); }) {
return None;
}
let listed: Vec<ListedEntry> = listing(context)?;
if listed.iter().any(|item| { return item.entry.key.starts_with("url.") && (item.entry.key.ends_with(".insteadof") || item.entry.key.ends_with(".pushinsteadof")); }) {
return None;
}
let key: String = format!("remote.{name}.url");
let urls: Vec<&ListedEntry> = listed.iter().filter(|item| { return item.entry.key == key; }).collect();
if urls.is_empty() {
let prefix: String = format!("remote.{name}.");
let legacy: bool = ["remotes", "branches"].iter().any(|directory| { return std::fs::symlink_metadata(context.layout.common_dir.join(directory).join(name)).is_ok(); });
if legacy || listed.iter().any(|item| { return item.entry.key.starts_with(prefix.as_str()); }) {
return None;
}
if !messages_are_english(context.variables.as_slice()) {
return None;
}
let message: String = format!("error: No such remote '{name}'\n");
return Some(Answer { stdout: Vec::new(), stderr: message.into_bytes(), code: 2 });
}
let [only] = urls.as_slice() else {
return None;
};
let mut stdout: Vec<u8> = only.entry.value.clone().filter(|value| { return !value.is_empty(); })?;
stdout.push(b'\n');
return Some(Answer { stdout, stderr: Vec::new(), code: 0 });
}
fn peel_to_commit(database: &ObjectDatabase, context: &Context, start: &str) -> Option<String> {
let mut current: Vec<u8> = from_hex(start.as_bytes(), context.layout.object_format)?;
for _ in 0..PEEL_LIMIT {
let object = database.read(current.as_slice())?;
match object.kind {
ObjectKind::Commit => return Some(super::repo_read_object::to_hex(current.as_slice())),
ObjectKind::Tag => {
let first: &[u8] = object.bytes.split(|byte| { return *byte == b'\n'; }).next()?;
current = from_hex(first.strip_prefix(b"object ")?, context.layout.object_format)?;
}
ObjectKind::Tree | ObjectKind::Blob => return None,
}
}
return None;
}
pub(super) fn for_each_ref(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
if parsed.flags != [LISTING_FORMAT] || !parsed.operands.is_empty() || !parsed.specs.is_empty() {
return None;
}
let refs: Refs = shared_refs(context)?;
let database: ObjectDatabase = context.database()?;
let mut stdout: Vec<u8> = Vec::new();
for (name, value) in refs.list()? {
let (object, symref): (String, &str) = match &value {
RefValue::Direct(object) => (object.clone(), ""),
RefValue::Symbolic(target) if is_terminal(&refs, target.as_str()) => (refs.resolve(name.as_str())?, target.as_str()),
RefValue::Symbolic(_) => return None,
};
if !database.contains(from_hex(object.as_bytes(), context.layout.object_format)?.as_slice()) {
return None;
}
stdout.extend_from_slice(format!("{name}\0{object}\0{symref}\n").as_bytes());
}
return Some(Answer { stdout, stderr: Vec::new(), code: 0 });
}
fn shadowed(refs: &Refs, prefix: &str, short: &str) -> Option<bool> {
let name: String = format!("{prefix}{short}");
if !is_plain_ref_name(name.as_str()) {
return Some(false);
}
return match refs.lookup(name.as_str()) {
Lookup::Missing => Some(false),
Lookup::Found(_) => Some(true),
Lookup::Unsure => None,
};
}
pub(super) fn tracking_ref(listed: &[ListedEntry], remote: &str, merge: &str) -> Option<String> {
let key: String = format!("remote.{remote}.fetch");
for item in listed.iter().filter(|item| { return item.entry.key == key; }) {
let spec: &str = std::str::from_utf8(item.entry.value.as_deref()?).ok()?;
let (source, destination) = spec.strip_prefix('+').unwrap_or(spec).split_once(':')?;
match (source.split_once('*'), destination.split_once('*')) {
(Some((head, tail)), Some((to_head, to_tail))) => {
if let Some(middle) = merge.strip_prefix(head).and_then(|rest| { return rest.strip_suffix(tail); }) {
return Some(format!("{to_head}{middle}{to_tail}"));
}
}
(None, None) if source == merge => return Some(destination.to_string()),
_ => {}
}
}
return None;
}
fn upstream(context: &Context) -> Option<Answer> {
let refs: Refs = shared_refs(context)?;
let head = match refs.lookup_head(context.layout.git_dir.as_path()) {
Some(RefValue::Symbolic(target)) => target,
_ => return None,
};
let branch: &str = head.strip_prefix("refs/heads/")?;
if !is_terminal(&refs, head.as_str()) {
return None;
}
let listed: Vec<ListedEntry> = listing(context)?;
let remotes = values_of(listed.as_slice(), &format!("branch.{branch}.remote"));
let merges = values_of(listed.as_slice(), &format!("branch.{branch}.merge"));
let (remote_value, merge_value) = (remotes.as_slice(), merges.as_slice());
let ([Some(remote)], [Some(merge)]) = (remote_value, merge_value) else {
if remote_value.is_empty() && merge_value.is_empty() && messages_are_english(context.variables.as_slice()) {
let message: String = format!("fatal: no upstream configured for branch '{branch}'\n");
return Some(Answer { stdout: Vec::new(), stderr: message.into_bytes(), code: 128 });
}
return None;
};
let remote_name: &str = std::str::from_utf8(remote.as_slice()).ok()?;
let merge_name: &str = std::str::from_utf8(merge.as_slice()).ok()?;
if remote_name == "." || !is_plain_ref_name(merge_name) {
return None;
}
let tracking: String = tracking_ref(listed.as_slice(), remote_name, merge_name)?;
let short: &str = tracking.strip_prefix("refs/remotes/")?;
refs.resolve(tracking.as_str())?;
if std::fs::symlink_metadata(context.layout.common_dir.join(short)).is_ok() {
return None;
}
for prefix in ["refs/", "refs/tags/", "refs/heads/"] {
if shadowed(&refs, prefix, short)? {
return None;
}
}
if shadowed(&refs, "refs/remotes/", &format!("{short}/HEAD"))? {
return None;
}
return Some(Answer { stdout: format!("{short}\n").into_bytes(), stderr: Vec::new(), code: 0 });
}
pub(super) fn symbolic_ref(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
if !flags_are(&parsed.flags, &["--quiet"], &[]) || parsed.operands != [b"HEAD".as_slice()] || !parsed.specs.is_empty() {
return None;
}
let refs: Refs = shared_refs(context)?;
return match refs.lookup_head(context.layout.git_dir.as_path())? {
RefValue::Symbolic(target) if !is_terminal(&refs, target.as_str()) => None,
RefValue::Symbolic(target) => Some(Answer { stdout: format!("{target}\n").into_bytes(), stderr: Vec::new(), code: 0 }),
RefValue::Direct(_) => Some(Answer { stdout: Vec::new(), stderr: Vec::new(), code: 1 }),
};
}
pub(super) fn rev_parse(context: &Context, parsed: &Parsed<'_>) -> Option<Answer> {
if !parsed.specs.is_empty() || parsed.operands.len() != 1 {
return None;
}
if parsed.operands[0] == b"@{upstream}" {
if !flags_are(&parsed.flags, &["--abbrev-ref", "--symbolic-full-name"], &[]) {
return None;
}
return upstream(context);
}
if !flags_are(&parsed.flags, &["--verify", "--quiet"], &[]) {
return None;
}
let name: &str = std::str::from_utf8(parsed.operands[0]).ok()?.strip_suffix("^{commit}")?;
let refs: Refs = shared_refs(context)?;
let object: String = refs.resolve(name)?;
let database: ObjectDatabase = context.database()?;
let commit: String = peel_to_commit(&database, context, object.as_str())?;
return Some(Answer { stdout: format!("{commit}\n").into_bytes(), stderr: Vec::new(), code: 0 });
}
#[cfg(test)]
#[path = "repo_read_query_refs_tests.rs"]
mod tests;