Skip to main content

grit_lib/worktree/
io_error.rs

1//! Map filesystem failures on repository-relative paths to typed [`Error::WorktreePathIo`].
2//!
3//! Callers pass the index-relative path and what they were doing so Windows sharing
4//! violations and Unix `EBUSY` produce a stable, actionable message without copying Git.
5
6use std::io;
7
8use crate::error::{Error, Result};
9
10/// Worktree filesystem operation that failed on a single path.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub enum WorktreePathOperation {
14    /// Reading file bytes or a symlink target.
15    Read,
16    /// Replacing an existing file (checkout, hard reset, apply).
17    Replace,
18    /// Removing a file or symlink before rewrite.
19    Remove,
20    /// Creating a new path (file or symlink).
21    Create,
22    /// `stat` / metadata probe without opening for read.
23    Metadata,
24}
25
26impl WorktreePathOperation {
27    fn verb(self) -> &'static str {
28        match self {
29            Self::Read => "read",
30            Self::Replace => "replace",
31            Self::Remove => "remove",
32            Self::Create => "create",
33            Self::Metadata => "inspect",
34        }
35    }
36}
37
38/// Whether `err` usually means another process holds the file open.
39#[must_use]
40pub fn is_file_in_use(err: &io::Error) -> bool {
41    if err.kind() == io::ErrorKind::WouldBlock {
42        return true;
43    }
44    if let Some(raw) = err.raw_os_error() {
45        #[cfg(windows)]
46        if raw == 32 {
47            return true;
48        }
49        if raw == libc::EBUSY {
50            return true;
51        }
52        #[cfg(windows)]
53        if raw == 33 {
54            // ERROR_LOCK_VIOLATION
55            return true;
56        }
57    }
58    false
59}
60
61/// Human-readable summary for [`Error::WorktreePathIo`].
62#[must_use]
63pub fn format_worktree_path_io_message(
64    path: &str,
65    operation: WorktreePathOperation,
66    source: &io::Error,
67) -> String {
68    if is_file_in_use(source) {
69        format!(
70            "cannot {} '{path}': file is in use by another process (close it and retry)",
71            operation.verb()
72        )
73    } else {
74        format!("cannot {} '{path}': {source}", operation.verb())
75    }
76}
77
78/// Wrap a raw I/O failure with path and operation context.
79#[must_use]
80pub fn map_worktree_io(
81    path: impl Into<String>,
82    operation: WorktreePathOperation,
83    source: io::Error,
84) -> Error {
85    Error::WorktreePathIo {
86        path: path.into(),
87        operation,
88        source,
89    }
90}
91
92/// Attach path context to [`std::io::Result`].
93pub fn io_with_path<T>(
94    path: impl Into<String>,
95    operation: WorktreePathOperation,
96    result: io::Result<T>,
97) -> Result<T> {
98    result.map_err(|source| map_worktree_io(path, operation, source))
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    #[cfg(windows)]
107    fn windows_sharing_violation_maps_to_in_use_message() {
108        let err = io::Error::from_raw_os_error(32);
109        let msg = format_worktree_path_io_message("b.txt", WorktreePathOperation::Replace, &err);
110        assert!(msg.contains("b.txt"));
111        assert!(msg.contains("in use by another process"));
112        assert!(!msg.contains("os error 32"));
113    }
114
115    #[test]
116    fn ebusy_maps_to_in_use_message() {
117        let err = io::Error::from_raw_os_error(libc::EBUSY as i32);
118        let msg = format_worktree_path_io_message("a.txt", WorktreePathOperation::Read, &err);
119        assert!(msg.contains("in use by another process"));
120    }
121
122    #[test]
123    fn other_io_keeps_source_kind() {
124        let err = io::Error::new(io::ErrorKind::NotFound, "gone");
125        let msg = format_worktree_path_io_message("x", WorktreePathOperation::Remove, &err);
126        assert!(msg.contains("cannot remove 'x'"));
127        assert!(msg.contains("gone"));
128    }
129}