1use std::io;
4use std::path::{Path, PathBuf};
5
6use crate::error::{Error, Result};
7use crate::objects::odb::Odb;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum GitDirPathOperation {
13 WriteObject,
15 UpdateRef,
17 AppendReflog,
19 WriteIndex,
21 GcPack,
23 CreateDir,
25}
26
27impl GitDirPathOperation {
28 fn phrase(self) -> &'static str {
29 match self {
30 Self::WriteObject => "add an object to the object database",
31 Self::UpdateRef => "update the ref store",
32 Self::AppendReflog => "append to the reflog",
33 Self::WriteIndex => "write the index",
34 Self::GcPack => "run repository maintenance",
35 Self::CreateDir => "create a directory in the repository",
36 }
37 }
38}
39
40fn layout_uses_dot_git_prefix(git_dir: &Path) -> bool {
42 git_dir.file_name().is_some_and(|n| n == ".git")
43}
44
45fn normalize_repo_relative_suffix(rel: &str) -> String {
47 rel.trim_start_matches(['/', '\\']).replace('\\', "/")
48}
49
50fn format_layout_path(layout_git_dir: &Path, rel: &str) -> String {
52 if rel.is_empty() {
53 if layout_uses_dot_git_prefix(layout_git_dir) {
54 return ".git".to_owned();
55 }
56 return layout_git_dir
57 .file_name()
58 .map(|s| s.to_string_lossy().into_owned())
59 .unwrap_or_else(|| ".".into());
60 }
61 if layout_uses_dot_git_prefix(layout_git_dir) {
62 format!(".git/{rel}")
63 } else {
64 rel.to_owned()
65 }
66}
67
68#[must_use]
70#[allow(dead_code)]
71pub fn display_storage_path(layout_git_dir: &Path, storage_root: &Path, path: &Path) -> String {
72 if let Ok(rel) = path.strip_prefix(storage_root) {
73 let trimmed = normalize_repo_relative_suffix(&rel.to_string_lossy());
74 return format_layout_path(layout_git_dir, &trimmed);
75 }
76 display_git_dir_path(layout_git_dir, path)
77}
78
79#[must_use]
81pub fn display_ref_store_path(common_dir: &Path, checkout_git_dir: &Path, path: &Path) -> String {
82 let layout = if layout_uses_dot_git_prefix(common_dir) {
83 common_dir
84 } else {
85 checkout_git_dir
86 };
87 let rel = if let Ok(r) = path.strip_prefix(common_dir) {
88 normalize_repo_relative_suffix(&r.to_string_lossy())
89 } else if let Ok(tail) = path.strip_prefix(checkout_git_dir) {
90 let middle = checkout_git_dir
91 .strip_prefix(common_dir)
92 .map(|p| normalize_repo_relative_suffix(&p.to_string_lossy()))
93 .unwrap_or_default();
94 let tail = normalize_repo_relative_suffix(&tail.to_string_lossy());
95 if middle.is_empty() {
96 tail
97 } else {
98 format!("{middle}/{tail}")
99 }
100 } else {
101 return path.display().to_string();
102 };
103 format_layout_path(layout, &rel)
104}
105
106#[must_use]
108pub fn display_git_dir_path(git_dir: &Path, path: &Path) -> String {
109 if let Ok(rel) = path.strip_prefix(git_dir) {
110 let trimmed = normalize_repo_relative_suffix(&rel.to_string_lossy());
111 return format_layout_path(git_dir, &trimmed);
112 }
113 if layout_uses_dot_git_prefix(git_dir) {
114 if let Some(worktree) = git_dir.parent() {
115 if let Ok(rel) = path.strip_prefix(worktree) {
116 return normalize_repo_relative_suffix(&rel.to_string_lossy());
117 }
118 }
119 }
120 path.display().to_string()
121}
122
123#[must_use]
125pub fn git_dir_for_pack_io(odb: &Odb, pack_area_path: &Path) -> PathBuf {
126 if let Some(git_dir) = odb.config_git_dir() {
127 return git_dir.to_path_buf();
128 }
129 pack_area_path
130 .parent()
131 .and_then(|objects| objects.parent())
132 .map(Path::to_path_buf)
133 .unwrap_or_else(|| pack_area_path.to_path_buf())
134}
135
136#[must_use]
138pub fn git_dir_from_objects_dir(objects_dir: &Path) -> PathBuf {
139 objects_dir
140 .parent()
141 .map(Path::to_path_buf)
142 .unwrap_or_else(|| objects_dir.to_path_buf())
143}
144
145#[must_use]
147pub fn git_dir_from_pack_dir(pack_dir: &Path) -> PathBuf {
148 pack_dir
149 .parent()
150 .and_then(|objects| objects.parent())
151 .map(Path::to_path_buf)
152 .unwrap_or_else(|| pack_dir.to_path_buf())
153}
154
155#[must_use]
157pub fn format_git_dir_io_message(
158 path: &str,
159 operation: GitDirPathOperation,
160 source: &io::Error,
161) -> String {
162 if source.kind() == io::ErrorKind::PermissionDenied {
163 format!(
164 "insufficient permission to {} ({path}): {source}",
165 operation.phrase()
166 )
167 } else {
168 format!("cannot {} ({path}): {source}", operation.phrase())
169 }
170}
171
172#[must_use]
174pub fn map_git_dir_io(
175 git_dir: &Path,
176 path: &Path,
177 operation: GitDirPathOperation,
178 source: io::Error,
179) -> Error {
180 Error::GitDirPathIo {
181 path: display_git_dir_path(git_dir, path),
182 operation,
183 source,
184 }
185}
186
187#[must_use]
189#[allow(dead_code)]
190pub fn map_storage_git_dir_io(
191 layout_git_dir: &Path,
192 storage_root: &Path,
193 path: &Path,
194 operation: GitDirPathOperation,
195 source: io::Error,
196) -> Error {
197 Error::GitDirPathIo {
198 path: display_storage_path(layout_git_dir, storage_root, path),
199 operation,
200 source,
201 }
202}
203
204#[must_use]
206pub fn map_ref_store_path_io(
207 common_dir: &Path,
208 checkout_git_dir: &Path,
209 path: &Path,
210 operation: GitDirPathOperation,
211 source: io::Error,
212) -> Error {
213 Error::GitDirPathIo {
214 path: display_ref_store_path(common_dir, checkout_git_dir, path),
215 operation,
216 source,
217 }
218}
219
220pub fn io_with_git_path<T>(
222 git_dir: &Path,
223 path: &Path,
224 operation: GitDirPathOperation,
225 result: io::Result<T>,
226) -> Result<T> {
227 result.map_err(|source| map_git_dir_io(git_dir, path, operation, source))
228}
229
230#[must_use]
232pub fn map_index_write_error(git_dir: &Path, index_path: &Path, err: Error) -> Error {
233 let display_path = if let Some(common) = crate::refs::common_dir(git_dir) {
234 display_ref_store_path(&common, git_dir, index_path)
235 } else {
236 display_git_dir_path(git_dir, index_path)
237 };
238 match err {
239 Error::Io(source) => Error::GitDirPathIo {
240 path: display_path,
241 operation: GitDirPathOperation::WriteIndex,
242 source,
243 },
244 other => other,
245 }
246}
247
248#[allow(dead_code)]
250pub fn io_with_storage_git_path<T>(
251 layout_git_dir: &Path,
252 storage_root: &Path,
253 path: &Path,
254 operation: GitDirPathOperation,
255 result: io::Result<T>,
256) -> Result<T> {
257 result.map_err(|source| {
258 map_storage_git_dir_io(layout_git_dir, storage_root, path, operation, source)
259 })
260}
261
262pub fn io_with_ref_store_path<T>(
264 common_dir: &Path,
265 checkout_git_dir: &Path,
266 path: &Path,
267 operation: GitDirPathOperation,
268 result: io::Result<T>,
269) -> Result<T> {
270 result.map_err(|source| {
271 map_ref_store_path_io(common_dir, checkout_git_dir, path, operation, source)
272 })
273}
274
275#[must_use]
277pub fn map_objects_dir_io(
278 objects_dir: &Path,
279 path: &Path,
280 operation: GitDirPathOperation,
281 source: io::Error,
282) -> Error {
283 let git_dir = git_dir_from_objects_dir(objects_dir);
284 map_git_dir_io(&git_dir, path, operation, source)
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use std::path::PathBuf;
291
292 #[test]
293 fn display_path_under_dot_git() {
294 let git = PathBuf::from("/tmp/pe/.git");
295 let path = git.join("objects/ab/cdef");
296 assert_eq!(display_git_dir_path(&git, &path), ".git/objects/ab/cdef");
297 }
298
299 #[test]
300 fn strip_prefix_suffix_normalizes_windows_separators() {
301 let git = PathBuf::from("/tmp/pe/.git");
302 let rel = normalize_repo_relative_suffix(r"\objects\ab\cdef");
303 assert_eq!(rel, "objects/ab/cdef");
304 assert_eq!(format_layout_path(&git, &rel), ".git/objects/ab/cdef");
305 }
306
307 #[cfg(windows)]
308 #[test]
309 fn display_path_under_dot_git_on_windows() {
310 let git = PathBuf::from(r"C:\Users\pe\repo\.git");
311 let path = PathBuf::from(r"C:\Users\pe\repo\.git\objects\ab\cdef");
312 let display = display_git_dir_path(&git, &path);
313 assert_eq!(display, ".git/objects/ab/cdef");
314 assert!(
315 !display.contains('\\'),
316 "git-dir display paths must use forward slashes: {display}"
317 );
318 }
319
320 #[test]
321 fn display_path_under_bare_repo() {
322 let git = PathBuf::from("/tmp/foo.git");
323 let path = git.join("objects/pack/tmp_repack_1");
324 assert_eq!(
325 display_git_dir_path(&git, &path),
326 "objects/pack/tmp_repack_1"
327 );
328 }
329
330 #[test]
331 fn storage_path_uses_dot_git_for_worktree_layout() {
332 let git = PathBuf::from("/tmp/wt/.git");
333 let common = PathBuf::from("/shared/commondir");
334 let path = common.join("refs/heads/main");
335 assert_eq!(
336 display_storage_path(&git, &common, &path),
337 ".git/refs/heads/main"
338 );
339 }
340
341 #[test]
342 fn ref_store_path_for_linked_worktree_head_lock() {
343 let common = PathBuf::from("/tmp/main/.git");
344 let admin = common.join("worktrees/linked");
345 let lock = admin.join("HEAD.lock");
346 assert_eq!(
347 display_ref_store_path(&common, &admin, &lock),
348 ".git/worktrees/linked/HEAD.lock"
349 );
350 }
351
352 #[test]
353 fn ref_store_path_for_bare_pack_staging() {
354 let bare = PathBuf::from("/tmp/repo.git");
355 let path = bare.join("objects/pack/tmp_repack_1");
356 assert_eq!(
357 display_ref_store_path(&bare, &bare, &path),
358 "objects/pack/tmp_repack_1"
359 );
360 }
361
362 #[test]
363 fn permission_message_names_git_path() {
364 let err = io::Error::from_raw_os_error(libc::EACCES as i32);
365 let msg = format_git_dir_io_message(".git/objects", GitDirPathOperation::WriteObject, &err);
366 assert!(msg.contains(".git/objects"));
367 assert!(msg.contains("insufficient permission"));
368 }
369}