Skip to main content

ephemeris_files/
error.rs

1use std::path::PathBuf;
2use std::{fmt, io};
3
4use git2::Error as GitError;
5
6use crate::FormatterError;
7use crate::path::{PathError, PathSelector, RepositoryPath};
8
9/// Error returned by file reconciliation planning and application.
10#[derive(Debug, thiserror::Error)]
11pub enum FilesError {
12    /// The repository root is not a Git repository `git2` can open.
13    #[error("cannot open the Git repository at {}", path.display())]
14    Open {
15        /// The path that failed to open as a Git repository.
16        path: PathBuf,
17        /// The underlying `git2` failure.
18        #[source]
19        source: GitError,
20    },
21    /// The repository has no checked-out worktree.
22    #[error("Git repository has no worktree")]
23    NoWorktree,
24    /// A repository path is malformed, unsafe, or cannot be inspected.
25    #[error(transparent)]
26    Path(#[from] PathError),
27    /// A path rule's selector rejects every segment it could match.
28    #[error("invalid path selector {selector:?}")]
29    InvalidSelector {
30        /// The selector that failed validation.
31        selector: PathSelector,
32    },
33    /// Two path rules disagree on `managed` for the same path and priority.
34    #[error("conflicting managed policy for `{path}` at priority {priority}")]
35    ManagedConflict {
36        /// The path both rules matched.
37        path: RepositoryPath,
38        /// The shared priority at which the rules disagree.
39        priority: u32,
40    },
41    /// Two paths differ only by case, which collides on case-insensitive
42    /// filesystems.
43    #[error(
44        "`{first}` and `{second}` differ only by case, which collides on case-insensitive \
45         filesystems"
46    )]
47    CaseInsensitivePath {
48        /// The first path seen with this case-insensitive form.
49        first: RepositoryPath,
50        /// The later path that collides with it.
51        second: RepositoryPath,
52    },
53    /// A document failed validation, rendering, or reconciliation.
54    #[error("invalid document {path}")]
55    Document {
56        /// The file the document was declared for.
57        path: RepositoryPath,
58        /// The underlying document failure.
59        #[source]
60        source: ephemeris_documents::DocumentError,
61    },
62    /// The Git index could not be read.
63    #[error("cannot read the Git index")]
64    ReadIndex {
65        /// The underlying `git2` failure.
66        #[source]
67        source: GitError,
68    },
69    /// The Git working tree status could not be read.
70    #[error("cannot read the Git working tree status")]
71    ReadStatus {
72        /// The underlying `git2` failure.
73        #[source]
74        source: GitError,
75    },
76    /// The Git index could not be written.
77    #[error("cannot write the Git index")]
78    WriteIndex {
79        /// The underlying `git2` failure.
80        #[source]
81        source: GitError,
82    },
83    /// A changed path could not be staged in the Git index.
84    #[error("cannot stage `{path}`")]
85    Stage {
86        /// The path that failed to stage.
87        path: RepositoryPath,
88        /// The underlying `git2` failure.
89        #[source]
90        source: GitError,
91    },
92    /// A path's current worktree content cannot be recovered from HEAD or
93    /// the index, so changing it would be unsafe.
94    #[error(
95        "unsafe change to `{path}`: current worktree state is not recoverable from HEAD or index"
96    )]
97    Unsafe {
98        /// The path whose current state is not recoverable.
99        path: RepositoryPath,
100    },
101    /// Tracked files in managed paths that Ephemeris did not generate.
102    #[error(
103        "tracked files in managed paths are not generated by Ephemeris: {}. Delete them or \
104         declare the path unmanaged through ephemeris.paths",
105        PathList(paths)
106    )]
107    Foreign {
108        /// The foreign paths.
109        paths: Vec<RepositoryPath>,
110    },
111    /// A path changed after the plan was made.
112    #[error("`{path}` changed since the plan was made")]
113    Stale {
114        /// The path whose state no longer matches the plan.
115        path: RepositoryPath,
116    },
117    /// [`crate::Plan::apply`] was called on a plan built for a snapshot.
118    #[error("a snapshot plan cannot be applied to a worktree")]
119    SnapshotPlan,
120    /// A planned update list names the same path more than once.
121    #[error("duplicate path `{0}` in planned updates")]
122    DuplicatePath(RepositoryPath),
123    /// A formatter could not be applied.
124    #[error(transparent)]
125    Formatter(#[from] FormatterError),
126}
127
128/// Paths separated by commas for an error message.
129struct PathList<'a>(&'a [RepositoryPath]);
130
131impl fmt::Display for PathList<'_> {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        for (index, path) in self.0.iter().enumerate() {
134            if index > 0 {
135                f.write_str(", ")?;
136            }
137            write!(f, "`{path}`")?;
138        }
139        Ok(())
140    }
141}
142
143/// Attaches a path to an I/O error, turning it into [`PathError::Io`].
144pub(crate) trait IoResultExt<T> {
145    /// Turn the I/O error, if any, into a [`PathError::Io`] naming `path`.
146    fn at_path(self, path: impl Into<PathBuf>) -> Result<T, FilesError>;
147}
148
149impl<T> IoResultExt<T> for io::Result<T> {
150    fn at_path(self, path: impl Into<PathBuf>) -> Result<T, FilesError> {
151        self.map_err(|source| {
152            PathError::Io {
153                path: path.into(),
154                source,
155            }
156            .into()
157        })
158    }
159}