Skip to main content

git_vet/
error.rs

1use std::fmt;
2
3use thiserror::Error;
4
5use crate::channel::ChannelTransferError;
6use crate::path::{PathError, RepoPath, RepoPathScope};
7use crate::remote::RemoteError;
8
9#[derive(Debug, Error)]
10pub enum AppError {
11    #[error("git operation failed while {operation}: {details}")]
12    Git {
13        operation: &'static str,
14        details: String,
15    },
16    #[error("repository has no worktree")]
17    MissingWorktree,
18    #[error("path is not valid UTF-8")]
19    NonUtf8Path,
20    #[error("path escapes the repository root")]
21    PathOutsideRepo,
22    #[error("path must be absolute")]
23    NonAbsolutePath,
24    #[error("empty paths are not valid tracked files")]
25    EmptyPath,
26    #[error("repo path is invalid: {details}")]
27    InvalidRepoPath { details: &'static str },
28    #[error("path is not tracked at HEAD: {0}")]
29    PathNotTracked(RepoPath),
30    #[error("pathspec did not match any tracked files at HEAD: {0}")]
31    PathspecNotMatched(RepoPathScope),
32    #[error("path is a submodule/gitlink and is out of scope: {0}")]
33    PathIsSubmodule(RepoPath),
34    #[error("failed to read vetignore file: {0}")]
35    Vetignore(String),
36    #[error("missing git config user.name")]
37    MissingUserName,
38    #[error("missing git config user.email")]
39    MissingUserEmail,
40    #[error("git config {key} is not valid UTF-8: {details}")]
41    NonUtf8GitConfig { key: &'static str, details: String },
42    #[error(
43        "target paths have uncommitted working-tree changes; rerun with --allow-dirty to proceed with committed HEAD contents"
44    )]
45    DirtyPathsRequireAllowDirty,
46    #[error("aborted because target paths have uncommitted working-tree changes")]
47    DirtyPathsDeclined,
48    #[error("invalid review channel {channel:?}: {details}")]
49    InvalidChannel { channel: String, details: String },
50    #[error("source and destination channels must differ")]
51    SameChannelTransfer,
52    #[error("source channel {channel:?} has no local review notes")]
53    MissingSourceChannelNotes { channel: String },
54    #[error("review channel {channel:?} does not exist locally")]
55    MissingChannelNotes { channel: String },
56    #[error("destination channel {channel:?} already has local review notes")]
57    ExistingDestinationChannelNotes { channel: String },
58    #[error("source channel {channel:?} has a symbolic notes ref; expected a direct ref")]
59    SymbolicChannelNotesRef { channel: String },
60    #[error("--channel cannot be used with `{command}`; pass SOURCE and DESTINATION explicitly")]
61    ChannelOptionNotAllowed { command: &'static str },
62    #[error("--channel cannot be used with `channel list`; listing is not channel-scoped")]
63    ChannelOptionNotAllowedForList,
64    #[error("--channel cannot be used with `channel remove`; pass CHANNEL explicitly")]
65    ChannelOptionNotAllowedForRemove,
66    #[error(
67        "--channel cannot be used with `install-man`; manual installation is not channel-scoped"
68    )]
69    ChannelOptionNotAllowedForInstallMan,
70    #[error("failed to install manual: {details}")]
71    ManPageInstall { details: String },
72    #[error("aborted removing review channel")]
73    ChannelRemovalDeclined,
74    #[error("non-interactive channel removal requires --force")]
75    ChannelRemovalRequiresForce,
76    #[error("sync remote error: {0}")]
77    Remote(#[from] RemoteError),
78    #[error("terminal interaction failed: {0}")]
79    Dialog(#[from] dialoguer::Error),
80    #[error("I/O error: {0}")]
81    Io(#[from] std::io::Error),
82    #[error("JSON error: {0}")]
83    Json(#[from] serde_json::Error),
84}
85
86impl From<ChannelTransferError> for AppError {
87    fn from(error: ChannelTransferError) -> Self {
88        match error {
89            ChannelTransferError::SameChannel => Self::SameChannelTransfer,
90        }
91    }
92}
93
94impl From<PathError> for AppError {
95    fn from(error: PathError) -> Self {
96        match error {
97            PathError::NonUtf8 => Self::NonUtf8Path,
98            PathError::OutsideRepo => Self::PathOutsideRepo,
99            PathError::NonAbsolute => Self::NonAbsolutePath,
100            PathError::EmptyPath => Self::EmptyPath,
101            PathError::EmptyComponent => Self::InvalidRepoPath {
102                details: "path contains an empty component",
103            },
104            PathError::CurrentDirComponent => Self::InvalidRepoPath {
105                details: "path contains a current-directory component",
106            },
107            PathError::NulByte => Self::InvalidRepoPath {
108                details: "path contains a NUL byte",
109            },
110        }
111    }
112}
113
114pub(crate) fn git_error(operation: &'static str, source: impl fmt::Display) -> AppError {
115    AppError::Git {
116        operation,
117        details: source.to_string(),
118    }
119}