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]
70pub fn display_ref_store_path(common_dir: &Path, checkout_git_dir: &Path, path: &Path) -> String {
71 let layout = if layout_uses_dot_git_prefix(common_dir) {
72 common_dir
73 } else {
74 checkout_git_dir
75 };
76 let rel = if let Ok(r) = path.strip_prefix(common_dir) {
77 normalize_repo_relative_suffix(&r.to_string_lossy())
78 } else if let Ok(tail) = path.strip_prefix(checkout_git_dir) {
79 let middle = checkout_git_dir
80 .strip_prefix(common_dir)
81 .map(|p| normalize_repo_relative_suffix(&p.to_string_lossy()))
82 .unwrap_or_default();
83 let tail = normalize_repo_relative_suffix(&tail.to_string_lossy());
84 if middle.is_empty() {
85 tail
86 } else {
87 format!("{middle}/{tail}")
88 }
89 } else {
90 return path.display().to_string();
91 };
92 format_layout_path(layout, &rel)
93}
94
95#[must_use]
97pub fn display_git_dir_path(git_dir: &Path, path: &Path) -> String {
98 if let Ok(rel) = path.strip_prefix(git_dir) {
99 let trimmed = normalize_repo_relative_suffix(&rel.to_string_lossy());
100 return format_layout_path(git_dir, &trimmed);
101 }
102 if layout_uses_dot_git_prefix(git_dir) {
103 if let Some(worktree) = git_dir.parent() {
104 if let Ok(rel) = path.strip_prefix(worktree) {
105 return normalize_repo_relative_suffix(&rel.to_string_lossy());
106 }
107 }
108 }
109 path.display().to_string()
110}
111
112#[must_use]
114pub fn git_dir_for_pack_io(odb: &Odb, pack_area_path: &Path) -> PathBuf {
115 if let Some(git_dir) = odb.config_git_dir() {
116 return git_dir.to_path_buf();
117 }
118 pack_area_path
119 .parent()
120 .and_then(|objects| objects.parent())
121 .map(Path::to_path_buf)
122 .unwrap_or_else(|| pack_area_path.to_path_buf())
123}
124
125#[must_use]
127pub fn git_dir_from_objects_dir(objects_dir: &Path) -> PathBuf {
128 objects_dir
129 .parent()
130 .map(Path::to_path_buf)
131 .unwrap_or_else(|| objects_dir.to_path_buf())
132}
133
134#[must_use]
136pub fn git_dir_from_pack_dir(pack_dir: &Path) -> PathBuf {
137 pack_dir
138 .parent()
139 .and_then(|objects| objects.parent())
140 .map(Path::to_path_buf)
141 .unwrap_or_else(|| pack_dir.to_path_buf())
142}
143
144#[must_use]
146pub fn format_git_dir_io_message(
147 path: &str,
148 operation: GitDirPathOperation,
149 source: &io::Error,
150) -> String {
151 if source.kind() == io::ErrorKind::PermissionDenied {
152 format!(
153 "insufficient permission to {} ({path}): {source}",
154 operation.phrase()
155 )
156 } else {
157 format!("cannot {} ({path}): {source}", operation.phrase())
158 }
159}
160
161#[must_use]
163pub fn map_git_dir_io(
164 git_dir: &Path,
165 path: &Path,
166 operation: GitDirPathOperation,
167 source: io::Error,
168) -> Error {
169 Error::GitDirPathIo {
170 path: display_git_dir_path(git_dir, path),
171 operation,
172 source,
173 }
174}
175
176#[must_use]
178pub fn map_ref_store_path_io(
179 common_dir: &Path,
180 checkout_git_dir: &Path,
181 path: &Path,
182 operation: GitDirPathOperation,
183 source: io::Error,
184) -> Error {
185 Error::GitDirPathIo {
186 path: display_ref_store_path(common_dir, checkout_git_dir, path),
187 operation,
188 source,
189 }
190}
191
192pub fn io_with_git_path<T>(
194 git_dir: &Path,
195 path: &Path,
196 operation: GitDirPathOperation,
197 result: io::Result<T>,
198) -> Result<T> {
199 result.map_err(|source| map_git_dir_io(git_dir, path, operation, source))
200}
201
202#[must_use]
204pub fn map_index_write_error(git_dir: &Path, index_path: &Path, err: Error) -> Error {
205 let display_path = if let Some(common) = crate::refs::common_dir(git_dir) {
206 display_ref_store_path(&common, git_dir, index_path)
207 } else {
208 display_git_dir_path(git_dir, index_path)
209 };
210 match err {
211 Error::Io(source) => Error::GitDirPathIo {
212 path: display_path,
213 operation: GitDirPathOperation::WriteIndex,
214 source,
215 },
216 other => other,
217 }
218}
219
220pub fn io_with_ref_store_path<T>(
222 common_dir: &Path,
223 checkout_git_dir: &Path,
224 path: &Path,
225 operation: GitDirPathOperation,
226 result: io::Result<T>,
227) -> Result<T> {
228 result.map_err(|source| {
229 map_ref_store_path_io(common_dir, checkout_git_dir, path, operation, source)
230 })
231}
232
233#[must_use]
235pub fn map_objects_dir_io(
236 objects_dir: &Path,
237 path: &Path,
238 operation: GitDirPathOperation,
239 source: io::Error,
240) -> Error {
241 let git_dir = git_dir_from_objects_dir(objects_dir);
242 map_git_dir_io(&git_dir, path, operation, source)
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use std::path::PathBuf;
249
250 #[test]
251 fn display_path_under_dot_git() {
252 let git = PathBuf::from("/tmp/pe/.git");
253 let path = git.join("objects/ab/cdef");
254 assert_eq!(display_git_dir_path(&git, &path), ".git/objects/ab/cdef");
255 }
256
257 #[test]
258 fn strip_prefix_suffix_normalizes_windows_separators() {
259 let git = PathBuf::from("/tmp/pe/.git");
260 let rel = normalize_repo_relative_suffix(r"\objects\ab\cdef");
261 assert_eq!(rel, "objects/ab/cdef");
262 assert_eq!(format_layout_path(&git, &rel), ".git/objects/ab/cdef");
263 }
264
265 #[cfg(windows)]
266 #[test]
267 fn display_path_under_dot_git_on_windows() {
268 let git = PathBuf::from(r"C:\Users\pe\repo\.git");
269 let path = PathBuf::from(r"C:\Users\pe\repo\.git\objects\ab\cdef");
270 let display = display_git_dir_path(&git, &path);
271 assert_eq!(display, ".git/objects/ab/cdef");
272 assert!(
273 !display.contains('\\'),
274 "git-dir display paths must use forward slashes: {display}"
275 );
276 }
277
278 #[test]
279 fn display_path_under_bare_repo() {
280 let git = PathBuf::from("/tmp/foo.git");
281 let path = git.join("objects/pack/tmp_repack_1");
282 assert_eq!(
283 display_git_dir_path(&git, &path),
284 "objects/pack/tmp_repack_1"
285 );
286 }
287
288 #[test]
289 fn storage_path_uses_dot_git_for_worktree_layout() {
290 let git = PathBuf::from("/tmp/wt/.git");
291 let common = PathBuf::from("/shared/commondir");
292 let path = common.join("refs/heads/main");
293 assert_eq!(
294 display_ref_store_path(&common, &git, &path),
295 ".git/refs/heads/main"
296 );
297 }
298
299 #[test]
300 fn ref_store_path_for_linked_worktree_head_lock() {
301 let common = PathBuf::from("/tmp/main/.git");
302 let admin = common.join("worktrees/linked");
303 let lock = admin.join("HEAD.lock");
304 assert_eq!(
305 display_ref_store_path(&common, &admin, &lock),
306 ".git/worktrees/linked/HEAD.lock"
307 );
308 }
309
310 #[test]
311 fn ref_store_path_for_bare_pack_staging() {
312 let bare = PathBuf::from("/tmp/repo.git");
313 let path = bare.join("objects/pack/tmp_repack_1");
314 assert_eq!(
315 display_ref_store_path(&bare, &bare, &path),
316 "objects/pack/tmp_repack_1"
317 );
318 }
319
320 #[test]
321 fn permission_message_names_git_path() {
322 let err = io::Error::from_raw_os_error(libc::EACCES as i32);
323 let msg = format_git_dir_io_message(".git/objects", GitDirPathOperation::WriteObject, &err);
324 assert!(msg.contains(".git/objects"));
325 assert!(msg.contains("insufficient permission"));
326 }
327}