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 is_git_dir_transient_contention(operation: GitDirPathOperation, source: &io::Error) -> bool {
147 if crate::worktree::io_error::is_file_in_use(source) {
148 return true;
149 }
150 matches!(
151 operation,
152 GitDirPathOperation::WriteIndex | GitDirPathOperation::UpdateRef
153 ) && crate::atomic_replace::is_replace_contention(source)
154}
155
156#[must_use]
158pub fn format_git_dir_io_message(
159 path: &str,
160 operation: GitDirPathOperation,
161 source: &io::Error,
162) -> String {
163 if is_git_dir_transient_contention(operation, source) {
164 format!(
165 "cannot {} ({path}): file is in use by another process (close it and retry)",
166 operation.phrase()
167 )
168 } else if source.kind() == io::ErrorKind::PermissionDenied {
169 format!(
170 "insufficient permission to {} ({path}): {source}",
171 operation.phrase()
172 )
173 } else {
174 format!("cannot {} ({path}): {source}", operation.phrase())
175 }
176}
177
178#[must_use]
180pub fn map_git_dir_io(
181 git_dir: &Path,
182 path: &Path,
183 operation: GitDirPathOperation,
184 source: io::Error,
185) -> Error {
186 Error::GitDirPathIo {
187 path: display_git_dir_path(git_dir, path),
188 operation,
189 source,
190 }
191}
192
193#[must_use]
195pub fn map_ref_store_path_io(
196 common_dir: &Path,
197 checkout_git_dir: &Path,
198 path: &Path,
199 operation: GitDirPathOperation,
200 source: io::Error,
201) -> Error {
202 Error::GitDirPathIo {
203 path: display_ref_store_path(common_dir, checkout_git_dir, path),
204 operation,
205 source,
206 }
207}
208
209pub fn io_with_git_path<T>(
211 git_dir: &Path,
212 path: &Path,
213 operation: GitDirPathOperation,
214 result: io::Result<T>,
215) -> Result<T> {
216 result.map_err(|source| map_git_dir_io(git_dir, path, operation, source))
217}
218
219#[must_use]
221pub fn map_index_write_error(git_dir: &Path, index_path: &Path, err: Error) -> Error {
222 let display_path = if let Some(common) = crate::refs::common_dir(git_dir) {
223 display_ref_store_path(&common, git_dir, index_path)
224 } else {
225 display_git_dir_path(git_dir, index_path)
226 };
227 match err {
228 Error::Io(source) => Error::GitDirPathIo {
229 path: display_path,
230 operation: GitDirPathOperation::WriteIndex,
231 source,
232 },
233 other => other,
234 }
235}
236
237pub fn io_with_ref_store_path<T>(
239 common_dir: &Path,
240 checkout_git_dir: &Path,
241 path: &Path,
242 operation: GitDirPathOperation,
243 result: io::Result<T>,
244) -> Result<T> {
245 result.map_err(|source| {
246 map_ref_store_path_io(common_dir, checkout_git_dir, path, operation, source)
247 })
248}
249
250#[must_use]
252pub fn map_objects_dir_io(
253 objects_dir: &Path,
254 path: &Path,
255 operation: GitDirPathOperation,
256 source: io::Error,
257) -> Error {
258 let git_dir = git_dir_from_objects_dir(objects_dir);
259 map_git_dir_io(&git_dir, path, operation, source)
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use std::path::PathBuf;
266
267 #[test]
268 fn display_path_under_dot_git() {
269 let git = PathBuf::from("/tmp/pe/.git");
270 let path = git.join("objects/ab/cdef");
271 assert_eq!(display_git_dir_path(&git, &path), ".git/objects/ab/cdef");
272 }
273
274 #[test]
275 fn strip_prefix_suffix_normalizes_windows_separators() {
276 let git = PathBuf::from("/tmp/pe/.git");
277 let rel = normalize_repo_relative_suffix(r"\objects\ab\cdef");
278 assert_eq!(rel, "objects/ab/cdef");
279 assert_eq!(format_layout_path(&git, &rel), ".git/objects/ab/cdef");
280 }
281
282 #[cfg(windows)]
283 #[test]
284 fn display_path_under_dot_git_on_windows() {
285 let git = PathBuf::from(r"C:\Users\pe\repo\.git");
286 let path = PathBuf::from(r"C:\Users\pe\repo\.git\objects\ab\cdef");
287 let display = display_git_dir_path(&git, &path);
288 assert_eq!(display, ".git/objects/ab/cdef");
289 assert!(
290 !display.contains('\\'),
291 "git-dir display paths must use forward slashes: {display}"
292 );
293 }
294
295 #[test]
296 fn display_path_under_bare_repo() {
297 let git = PathBuf::from("/tmp/foo.git");
298 let path = git.join("objects/pack/tmp_repack_1");
299 assert_eq!(
300 display_git_dir_path(&git, &path),
301 "objects/pack/tmp_repack_1"
302 );
303 }
304
305 #[test]
306 fn storage_path_uses_dot_git_for_worktree_layout() {
307 let git = PathBuf::from("/tmp/wt/.git");
308 let common = PathBuf::from("/shared/commondir");
309 let path = common.join("refs/heads/main");
310 assert_eq!(
311 display_ref_store_path(&common, &git, &path),
312 ".git/refs/heads/main"
313 );
314 }
315
316 #[test]
317 fn ref_store_path_for_linked_worktree_head_lock() {
318 let common = PathBuf::from("/tmp/main/.git");
319 let admin = common.join("worktrees/linked");
320 let lock = admin.join("HEAD.lock");
321 assert_eq!(
322 display_ref_store_path(&common, &admin, &lock),
323 ".git/worktrees/linked/HEAD.lock"
324 );
325 }
326
327 #[test]
328 fn ref_store_path_for_bare_pack_staging() {
329 let bare = PathBuf::from("/tmp/repo.git");
330 let path = bare.join("objects/pack/tmp_repack_1");
331 assert_eq!(
332 display_ref_store_path(&bare, &bare, &path),
333 "objects/pack/tmp_repack_1"
334 );
335 }
336
337 #[test]
338 fn permission_message_names_git_path() {
339 let err = io::Error::from_raw_os_error(libc::EACCES as i32);
340 let msg = format_git_dir_io_message(".git/objects", GitDirPathOperation::WriteObject, &err);
341 assert!(msg.contains(".git/objects"));
342 assert!(msg.contains("insufficient permission"));
343 }
344
345 #[test]
346 fn sharing_violation_maps_to_in_use_message() {
347 let err = io::Error::from_raw_os_error(libc::EBUSY as i32);
348 let msg = format_git_dir_io_message(".git/index", GitDirPathOperation::WriteIndex, &err);
349 assert!(msg.contains("in use by another process"));
350 assert!(!msg.contains("insufficient permission"));
351 }
352
353 #[cfg(windows)]
354 #[test]
355 fn access_denied_maps_to_in_use_for_index_write_only() {
356 let err = io::Error::from_raw_os_error(5);
357 let index = format_git_dir_io_message(".git/index", GitDirPathOperation::WriteIndex, &err);
358 assert!(index.contains("in use by another process"));
359 let object =
360 format_git_dir_io_message(".git/objects", GitDirPathOperation::WriteObject, &err);
361 assert!(object.contains("insufficient permission"));
362 }
363}