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}