use std::io;
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::objects::odb::Odb;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum GitDirPathOperation {
WriteObject,
UpdateRef,
AppendReflog,
WriteIndex,
GcPack,
CreateDir,
}
impl GitDirPathOperation {
fn phrase(self) -> &'static str {
match self {
Self::WriteObject => "add an object to the object database",
Self::UpdateRef => "update the ref store",
Self::AppendReflog => "append to the reflog",
Self::WriteIndex => "write the index",
Self::GcPack => "run repository maintenance",
Self::CreateDir => "create a directory in the repository",
}
}
}
fn layout_uses_dot_git_prefix(git_dir: &Path) -> bool {
git_dir.file_name().is_some_and(|n| n == ".git")
}
fn normalize_repo_relative_suffix(rel: &str) -> String {
rel.trim_start_matches(['/', '\\']).replace('\\', "/")
}
fn format_layout_path(layout_git_dir: &Path, rel: &str) -> String {
if rel.is_empty() {
if layout_uses_dot_git_prefix(layout_git_dir) {
return ".git".to_owned();
}
return layout_git_dir
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| ".".into());
}
if layout_uses_dot_git_prefix(layout_git_dir) {
format!(".git/{rel}")
} else {
rel.to_owned()
}
}
#[must_use]
#[allow(dead_code)]
pub fn display_storage_path(layout_git_dir: &Path, storage_root: &Path, path: &Path) -> String {
if let Ok(rel) = path.strip_prefix(storage_root) {
let trimmed = normalize_repo_relative_suffix(&rel.to_string_lossy());
return format_layout_path(layout_git_dir, &trimmed);
}
display_git_dir_path(layout_git_dir, path)
}
#[must_use]
pub fn display_ref_store_path(common_dir: &Path, checkout_git_dir: &Path, path: &Path) -> String {
let layout = if layout_uses_dot_git_prefix(common_dir) {
common_dir
} else {
checkout_git_dir
};
let rel = if let Ok(r) = path.strip_prefix(common_dir) {
normalize_repo_relative_suffix(&r.to_string_lossy())
} else if let Ok(tail) = path.strip_prefix(checkout_git_dir) {
let middle = checkout_git_dir
.strip_prefix(common_dir)
.map(|p| normalize_repo_relative_suffix(&p.to_string_lossy()))
.unwrap_or_default();
let tail = normalize_repo_relative_suffix(&tail.to_string_lossy());
if middle.is_empty() {
tail
} else {
format!("{middle}/{tail}")
}
} else {
return path.display().to_string();
};
format_layout_path(layout, &rel)
}
#[must_use]
pub fn display_git_dir_path(git_dir: &Path, path: &Path) -> String {
if let Ok(rel) = path.strip_prefix(git_dir) {
let trimmed = normalize_repo_relative_suffix(&rel.to_string_lossy());
return format_layout_path(git_dir, &trimmed);
}
if layout_uses_dot_git_prefix(git_dir) {
if let Some(worktree) = git_dir.parent() {
if let Ok(rel) = path.strip_prefix(worktree) {
return normalize_repo_relative_suffix(&rel.to_string_lossy());
}
}
}
path.display().to_string()
}
#[must_use]
pub fn git_dir_for_pack_io(odb: &Odb, pack_area_path: &Path) -> PathBuf {
if let Some(git_dir) = odb.config_git_dir() {
return git_dir.to_path_buf();
}
pack_area_path
.parent()
.and_then(|objects| objects.parent())
.map(Path::to_path_buf)
.unwrap_or_else(|| pack_area_path.to_path_buf())
}
#[must_use]
pub fn git_dir_from_objects_dir(objects_dir: &Path) -> PathBuf {
objects_dir
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| objects_dir.to_path_buf())
}
#[must_use]
pub fn git_dir_from_pack_dir(pack_dir: &Path) -> PathBuf {
pack_dir
.parent()
.and_then(|objects| objects.parent())
.map(Path::to_path_buf)
.unwrap_or_else(|| pack_dir.to_path_buf())
}
#[must_use]
pub fn format_git_dir_io_message(
path: &str,
operation: GitDirPathOperation,
source: &io::Error,
) -> String {
if source.kind() == io::ErrorKind::PermissionDenied {
format!(
"insufficient permission to {} ({path}): {source}",
operation.phrase()
)
} else {
format!("cannot {} ({path}): {source}", operation.phrase())
}
}
#[must_use]
pub fn map_git_dir_io(
git_dir: &Path,
path: &Path,
operation: GitDirPathOperation,
source: io::Error,
) -> Error {
Error::GitDirPathIo {
path: display_git_dir_path(git_dir, path),
operation,
source,
}
}
#[must_use]
#[allow(dead_code)]
pub fn map_storage_git_dir_io(
layout_git_dir: &Path,
storage_root: &Path,
path: &Path,
operation: GitDirPathOperation,
source: io::Error,
) -> Error {
Error::GitDirPathIo {
path: display_storage_path(layout_git_dir, storage_root, path),
operation,
source,
}
}
#[must_use]
pub fn map_ref_store_path_io(
common_dir: &Path,
checkout_git_dir: &Path,
path: &Path,
operation: GitDirPathOperation,
source: io::Error,
) -> Error {
Error::GitDirPathIo {
path: display_ref_store_path(common_dir, checkout_git_dir, path),
operation,
source,
}
}
pub fn io_with_git_path<T>(
git_dir: &Path,
path: &Path,
operation: GitDirPathOperation,
result: io::Result<T>,
) -> Result<T> {
result.map_err(|source| map_git_dir_io(git_dir, path, operation, source))
}
#[must_use]
pub fn map_index_write_error(git_dir: &Path, index_path: &Path, err: Error) -> Error {
let display_path = if let Some(common) = crate::refs::common_dir(git_dir) {
display_ref_store_path(&common, git_dir, index_path)
} else {
display_git_dir_path(git_dir, index_path)
};
match err {
Error::Io(source) => Error::GitDirPathIo {
path: display_path,
operation: GitDirPathOperation::WriteIndex,
source,
},
other => other,
}
}
#[allow(dead_code)]
pub fn io_with_storage_git_path<T>(
layout_git_dir: &Path,
storage_root: &Path,
path: &Path,
operation: GitDirPathOperation,
result: io::Result<T>,
) -> Result<T> {
result.map_err(|source| {
map_storage_git_dir_io(layout_git_dir, storage_root, path, operation, source)
})
}
pub fn io_with_ref_store_path<T>(
common_dir: &Path,
checkout_git_dir: &Path,
path: &Path,
operation: GitDirPathOperation,
result: io::Result<T>,
) -> Result<T> {
result.map_err(|source| {
map_ref_store_path_io(common_dir, checkout_git_dir, path, operation, source)
})
}
#[must_use]
pub fn map_objects_dir_io(
objects_dir: &Path,
path: &Path,
operation: GitDirPathOperation,
source: io::Error,
) -> Error {
let git_dir = git_dir_from_objects_dir(objects_dir);
map_git_dir_io(&git_dir, path, operation, source)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn display_path_under_dot_git() {
let git = PathBuf::from("/tmp/pe/.git");
let path = git.join("objects/ab/cdef");
assert_eq!(display_git_dir_path(&git, &path), ".git/objects/ab/cdef");
}
#[test]
fn strip_prefix_suffix_normalizes_windows_separators() {
let git = PathBuf::from("/tmp/pe/.git");
let rel = normalize_repo_relative_suffix(r"\objects\ab\cdef");
assert_eq!(rel, "objects/ab/cdef");
assert_eq!(format_layout_path(&git, &rel), ".git/objects/ab/cdef");
}
#[cfg(windows)]
#[test]
fn display_path_under_dot_git_on_windows() {
let git = PathBuf::from(r"C:\Users\pe\repo\.git");
let path = PathBuf::from(r"C:\Users\pe\repo\.git\objects\ab\cdef");
let display = display_git_dir_path(&git, &path);
assert_eq!(display, ".git/objects/ab/cdef");
assert!(
!display.contains('\\'),
"git-dir display paths must use forward slashes: {display}"
);
}
#[test]
fn display_path_under_bare_repo() {
let git = PathBuf::from("/tmp/foo.git");
let path = git.join("objects/pack/tmp_repack_1");
assert_eq!(
display_git_dir_path(&git, &path),
"objects/pack/tmp_repack_1"
);
}
#[test]
fn storage_path_uses_dot_git_for_worktree_layout() {
let git = PathBuf::from("/tmp/wt/.git");
let common = PathBuf::from("/shared/commondir");
let path = common.join("refs/heads/main");
assert_eq!(
display_storage_path(&git, &common, &path),
".git/refs/heads/main"
);
}
#[test]
fn ref_store_path_for_linked_worktree_head_lock() {
let common = PathBuf::from("/tmp/main/.git");
let admin = common.join("worktrees/linked");
let lock = admin.join("HEAD.lock");
assert_eq!(
display_ref_store_path(&common, &admin, &lock),
".git/worktrees/linked/HEAD.lock"
);
}
#[test]
fn ref_store_path_for_bare_pack_staging() {
let bare = PathBuf::from("/tmp/repo.git");
let path = bare.join("objects/pack/tmp_repack_1");
assert_eq!(
display_ref_store_path(&bare, &bare, &path),
"objects/pack/tmp_repack_1"
);
}
#[test]
fn permission_message_names_git_path() {
let err = io::Error::from_raw_os_error(libc::EACCES as i32);
let msg = format_git_dir_io_message(".git/objects", GitDirPathOperation::WriteObject, &err);
assert!(msg.contains(".git/objects"));
assert!(msg.contains("insufficient permission"));
}
}