use crate::private_storage::read_regular_file;
use crate::repo_read_hook::enabled;
use crate::repo_read_config_scan::{ScopeEnvironment, discovery_key, entries_avoid_keys, outer_scopes_avoid_keys, read_following};
use crate::repo_read_config_text::{CONFIG_TEXT_LIMIT, ConfigEntry, last_entry, parse_bool, parse_config_text};
use crate::repo_read_config_typed::{entries_are_accepted, environment_is_accepted, outer_scopes_are_accepted};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
pub use crate::repo_read_layout_environment_rules::{WRAPPER_ENVIRONMENT_PREFIX, env_config_avoids};
pub(crate) use crate::repo_read_layout_environment_rules::{effective_environment, environment_is_plain};
pub(crate) use crate::repo_read_layout_disk_and_directory_facts::start_directory;
use crate::repo_read_layout_disk_and_directory_facts::{device_of, has_git_directory_entries, has_git_directory_entries_split, is_hex_byte, owned_by_current_user, small_file};
const WORKTREE_CONFIG_NAME: &str = "config.worktree";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ObjectFormat {
Sha1,
Sha256,
}
impl ObjectFormat {
pub fn name(self) -> &'static str {
return match self {
ObjectFormat::Sha1 => "sha1",
ObjectFormat::Sha256 => "sha256",
};
}
pub fn hex_length(self) -> usize {
return match self {
ObjectFormat::Sha1 => 40,
ObjectFormat::Sha256 => 64,
};
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RepoLayout {
pub git_dir: PathBuf,
pub common_dir: PathBuf,
pub worktree_root: PathBuf,
pub prefix: Vec<u8>,
pub object_format: ObjectFormat,
}
fn head_is_valid(text: &str, format: ObjectFormat) -> bool {
let Some(line) = text.strip_suffix('\n') else {
return false;
};
if let Some(name) = line.strip_prefix("ref: ") {
return name.starts_with("refs/") && !name.contains('\n');
}
return line.len() == format.hex_length() && line.bytes().all(is_hex_byte);
}
pub fn dot_git_target(worktree: &Path) -> Option<Option<(PathBuf, bool)>> {
let dot_git: PathBuf = worktree.join(".git");
let metadata = match std::fs::symlink_metadata(&dot_git) {
Ok(found) => found,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Some(None),
Err(_) => return None,
};
if metadata.is_dir() {
return Some(Some((dot_git, false)));
}
if !metadata.is_file() {
return None;
}
let text: String = small_file(dot_git.as_path())?;
let target: &str = text.strip_prefix("gitdir: ")?.trim_end_matches(['\r', '\n']);
if target.is_empty() || target.contains('\n') {
return None;
}
return Some(Some((worktree.join(target), true)));
}
fn format_of(entries: &[ConfigEntry]) -> Option<(ObjectFormat, bool)> {
let version: Option<&[u8]> = match last_entry(entries, "core.repositoryformatversion") {
Some(entry) => Some(entry.value.as_deref()?),
None => None,
};
let version_one: bool = match version {
None | Some(b"0") => false,
Some(b"1") => true,
Some(_) => return None,
};
let mut format: ObjectFormat = ObjectFormat::Sha1;
let mut worktree_config: bool = false;
for entry in entries {
if !entry.key.starts_with("extensions.") {
continue;
}
if entry.key == "extensions.worktreeconfig" {
worktree_config = parse_bool(entry.value.as_deref())?;
continue;
}
if !version_one {
return None;
}
let value: &[u8] = entry.value.as_deref().unwrap_or_default();
match (entry.key.as_str(), value) {
("extensions.noop", _) => {}
("extensions.preciousobjects", _) if parse_bool(entry.value.as_deref()).is_some() => {}
("extensions.objectformat", b"sha1") => format = ObjectFormat::Sha1,
("extensions.objectformat", b"sha256") => format = ObjectFormat::Sha256,
("extensions.refstorage", b"files") => {}
_ => return None,
}
}
return Some((format, worktree_config && version.is_some()));
}
fn worktree_entries(git_dir: &Path, enabled: bool) -> Option<Vec<ConfigEntry>> {
if !enabled {
return Some(Vec::new());
}
return match read_following(git_dir.join(WORKTREE_CONFIG_NAME).as_path())? {
Some(bytes) => parse_config_text(bytes.as_slice()),
None => Some(Vec::new()),
};
}
fn without_false_bare(files: [&[ConfigEntry]; 2]) -> Option<[Vec<ConfigEntry>; 2]> {
let mut last: Option<bool> = None;
for entries in files {
for entry in entries {
if entry.key == "core.bare" {
last = Some(parse_bool(entry.value.as_deref())?);
}
}
}
if last == Some(true) {
return None;
}
let [common, per_worktree] = files;
return Some([without_bare(common), without_bare(per_worktree)]);
}
fn without_bare(entries: &[ConfigEntry]) -> Vec<ConfigEntry> {
let mut kept: Vec<ConfigEntry> = Vec::new();
for entry in entries {
if entry.key != "core.bare" {
kept.push(entry.clone());
}
}
return kept;
}
fn finish(start: &Path, worktree: &Path, entry: (PathBuf, bool), scope: &ScopeEnvironment, typed: bool) -> Option<RepoLayout> {
let (target, from_file) = entry;
let git_dir: PathBuf = std::fs::canonicalize(target).ok()?;
let dot_git: PathBuf = worktree.join(".git");
let owners_ok: bool = owned_by_current_user(worktree)
&& owned_by_current_user(git_dir.as_path())
&& (!from_file || owned_by_current_user(dot_git.as_path()));
if !owners_ok {
return None;
}
let common_dir: PathBuf = match small_file(git_dir.join("commondir").as_path()) {
Some(text) => std::fs::canonicalize(git_dir.join(text.trim_end_matches(['\r', '\n']))).ok()?,
None if git_dir.join("commondir").exists() => return None,
None => git_dir.clone(),
};
if git_dir.join("reftable").exists() || common_dir.join("reftable").exists() || !has_git_directory_entries_split(&git_dir, &common_dir) {
return None;
}
let config_bytes: Vec<u8> = read_regular_file(common_dir.join("config").as_path(), CONFIG_TEXT_LIMIT as u64).ok()?;
let entries: Vec<ConfigEntry> = parse_config_text(config_bytes.as_slice())?;
let (object_format, worktree_config): (ObjectFormat, bool) = format_of(entries.as_slice())?;
let worktree_path: PathBuf = git_dir.join(WORKTREE_CONFIG_NAME);
let per_worktree: Vec<ConfigEntry> = worktree_entries(git_dir.as_path(), worktree_config)?;
let [discoverable, worktree_discoverable] = without_false_bare([entries.as_slice(), per_worktree.as_slice()])?;
let local_ok: bool = entries_avoid_keys(discoverable.as_slice(), common_dir.join("config").as_path(), scope.home.as_deref(), 0, &discovery_key)?
&& entries_avoid_keys(worktree_discoverable.as_slice(), worktree_path.as_path(), scope.home.as_deref(), 0, &discovery_key)?;
if !local_ok || !outer_scopes_avoid_keys(scope, &discovery_key)? {
return None;
}
let typed_ok: bool = !typed
|| (entries_are_accepted(entries.as_slice(), common_dir.join("config").as_path(), scope.home.as_deref(), 0)?
&& entries_are_accepted(per_worktree.as_slice(), worktree_path.as_path(), scope.home.as_deref(), 0)?
&& outer_scopes_are_accepted(scope)?);
if !typed_ok {
return None;
}
let head: String = small_file(git_dir.join("HEAD").as_path())?;
if !head_is_valid(head.as_str(), object_format) {
return None;
}
let relative: &Path = start.strip_prefix(worktree).ok()?;
let mut prefix: Vec<u8> = Vec::new();
for component in relative.components() {
prefix.extend_from_slice(component.as_os_str().as_encoded_bytes());
prefix.push(b'/');
}
return Some(RepoLayout { git_dir, common_dir, worktree_root: worktree.to_path_buf(), prefix, object_format });
}
pub fn read_layout(global_prefix: &[OsString], overlay: &[(OsString, OsString)]) -> Option<RepoLayout> {
if !enabled(overlay) {
return None;
}
let variables: Vec<(OsString, OsString)> = effective_environment(overlay);
if !environment_is_plain(variables.as_slice()) || !environment_is_accepted(variables.as_slice()) {
return None;
}
let start: PathBuf = start_directory(global_prefix)?;
return walk_up(start.as_path(), &ScopeEnvironment::from_variables(variables.as_slice()), true);
}
pub fn read_layout_with(start: &Path, variables: &[(OsString, OsString)], scope: &ScopeEnvironment) -> Option<RepoLayout> {
if !environment_is_plain(variables) || !environment_is_accepted(variables) {
return None;
}
return walk_up(std::fs::canonicalize(start).ok()?.as_path(), scope, true);
}
pub fn read_layout_in(directory: &Path, overlay: &[(OsString, OsString)]) -> Option<RepoLayout> {
if !enabled(overlay) {
return None;
}
let variables: Vec<(OsString, OsString)> = effective_environment(overlay);
return read_layout_with(directory, variables.as_slice(), &ScopeEnvironment::from_variables(variables.as_slice()));
}
pub fn read_layout_after_success(directory: &Path, overlay: &[(OsString, OsString)]) -> Option<RepoLayout> {
if !enabled(overlay) {
return None;
}
let variables: Vec<(OsString, OsString)> = effective_environment(overlay);
if !environment_is_plain(variables.as_slice()) {
return None;
}
return walk_up(std::fs::canonicalize(directory).ok()?.as_path(), &ScopeEnvironment::from_variables(variables.as_slice()), false);
}
pub fn read_layout_after_success_with(start: &Path, variables: &[(OsString, OsString)], scope: &ScopeEnvironment) -> Option<RepoLayout> {
if !environment_is_plain(variables) {
return None;
}
return walk_up(std::fs::canonicalize(start).ok()?.as_path(), scope, false);
}
pub fn read_layout_after_success_for(global_prefix: &[OsString], overlay: &[(OsString, OsString)]) -> Option<RepoLayout> {
if !enabled(overlay) {
return None;
}
let variables: Vec<(OsString, OsString)> = effective_environment(overlay);
if !environment_is_plain(variables.as_slice()) {
return None;
}
let start: PathBuf = start_directory(global_prefix)?;
return walk_up(start.as_path(), &ScopeEnvironment::from_variables(variables.as_slice()), false);
}
pub(crate) fn walk_up(start: &Path, scope: &ScopeEnvironment, typed: bool) -> Option<RepoLayout> {
let mut directory: PathBuf = start.to_path_buf();
loop {
if let Some(entry) = dot_git_target(directory.as_path())? {
return finish(start, directory.as_path(), entry, scope, typed);
}
if has_git_directory_entries(directory.as_path()) {
return None;
}
let parent: PathBuf = directory.parent()?.to_path_buf();
if device_of(parent.as_path())? != device_of(directory.as_path())? {
return None;
}
directory = parent;
}
}
pub fn git_path(layout: &RepoLayout, name: &str) -> Option<PathBuf> {
const KNOWN: [&str; 9] = [
"index",
"objects",
"HEAD",
"MERGE_HEAD",
"CHERRY_PICK_HEAD",
"REVERT_HEAD",
"cli-git-transactions",
"packed-refs",
"config",
];
if layout.git_dir != layout.common_dir || !KNOWN.contains(&name) {
return None;
}
let path: PathBuf = layout.git_dir.join(name);
if let Ok(metadata) = std::fs::symlink_metadata(path.as_path())
&& metadata.file_type().is_symlink()
{
return None;
}
return Some(path);
}
#[cfg(test)]
#[path = "repo_read_layout_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "repo_read_layout_worktree_tests.rs"]
mod worktree_tests;