big_code_analysis/vcs/error.rs
1//! Error type for the change-history (VCS) metrics pipeline.
2//!
3//! Generic over the backend: backend-specific failures (a `gix` open
4//! error, a rev-walk failure, a blob-diff failure) are mapped to
5//! string-carrying variants here rather than leaking the backend's
6//! concrete error types into the generic surface. This keeps the
7//! generic module tree free of any `gix` reference so a future
8//! backend (`vcs-hg`, `vcs-jj`) can reuse it unchanged (issue #335).
9
10use std::path::PathBuf;
11
12/// Error returned by [`build_history_index`](crate::vcs::build_history_index)
13/// and the surrounding VCS pipeline.
14///
15/// `#[non_exhaustive]` so new variants land additively as backends and
16/// edge cases accrue; match with a trailing `_` arm to stay
17/// forward-compatible.
18#[non_exhaustive]
19#[derive(Debug)]
20pub enum Error {
21 /// The supplied path is not inside the working tree of any
22 /// supported VCS. Distinct from a repository with no tracked text
23 /// files at the target ref, which succeeds with an empty index. (A
24 /// freshly-initialised repository whose `HEAD` is unborn instead
25 /// surfaces as [`Error::ResolveRef`], since there is no commit to
26 /// resolve.)
27 NotARepository(PathBuf),
28 /// Opening or discovering the repository failed for a reason other
29 /// than "no repository here" (corrupt repo, permission denied, …).
30 OpenRepository(String),
31 /// The `--ref` revision could not be resolved to a commit.
32 ResolveRef {
33 /// The revision spec the caller supplied (e.g. `HEAD`, a SHA).
34 reference: String,
35 /// Backend-rendered explanation of why resolution failed.
36 reason: String,
37 },
38 /// Walking commit history failed.
39 Walk(String),
40 /// Computing a tree-to-tree or blob diff failed.
41 Diff(String),
42 /// Loading or applying the repository `.mailmap` failed.
43 Mailmap(String),
44 /// The bot-exclusion pattern is not a valid regular expression.
45 InvalidBotPattern(String),
46 /// A configured time window could not be parsed.
47 InvalidWindow(String),
48 /// The `--as-of` timestamp could not be parsed.
49 InvalidTimestamp(String),
50 /// The risk-formula name is not one of `weighted` / `percentile`.
51 InvalidFormula(String),
52 /// The file-type scope could not be parsed: an empty value, or a
53 /// custom extension list that normalised to nothing (issue #576).
54 InvalidFileTypeScope(String),
55 /// The bus-factor coverage threshold is outside the open interval
56 /// `(0, 1)` (issue #332).
57 InvalidBusFactorThreshold(String),
58 /// The opt-in author-hash key is unusable — empty, or supplied
59 /// without `--emit-author-details` (which it has no effect without)
60 /// (issue #956).
61 InvalidAuthorHashKey(String),
62 /// The historical-trend parameters are out of range — the point
63 /// count is below the two-point minimum or above
64 /// [`MAX_TREND_POINTS`](crate::vcs::trend::MAX_TREND_POINTS) (issue
65 /// #333).
66 InvalidTrend(String),
67 /// Blaming a file for per-function attribution failed (issue #329).
68 Blame(String),
69 /// An arbitrary unified diff supplied to
70 /// [`score_diff`](crate::vcs::score_diff) could not be parsed (issue
71 /// #580). A client-input error: the diff was malformed (a hunk header
72 /// the parser could not read, a body line outside any hunk, …).
73 InvalidDiff(String),
74 /// Reading, writing, or clearing the persistent history cache failed
75 /// (issue #334). A *missing* or *corrupt* cache entry is not an error —
76 /// it is silently ignored and the history is recomputed — so this
77 /// variant is reserved for genuine I/O failures the caller asked to
78 /// surface (e.g. `--clear-cache` on an unwritable directory).
79 Cache(String),
80}
81
82impl Error {
83 /// Whether this error was caused by client-supplied input (a bad
84 /// path, revision, window, timestamp, formula, pattern, threshold,
85 /// trend parameter, file-type scope, or diff) as opposed to an
86 /// environment or backend failure (opening the repository, walking
87 /// history, diffing, `.mailmap`, blame, or the persistent cache).
88 ///
89 /// Front ends use this to choose a status: a web boundary maps
90 /// client-input errors to `400 Bad Request` and the rest to
91 /// `500 Internal Server Error` (see `vcs_error_response` in the web
92 /// crate).
93 ///
94 /// The match is intentionally exhaustive (no wildcard arm): adding a
95 /// new [`Error`] variant is a compile error here until it is
96 /// classified, which prevents the silent fall-through that twice
97 /// mis-mapped client-input variants to `500` (`InvalidFileTypeScope`,
98 /// `InvalidDiff`; see issue #641).
99 #[must_use]
100 pub fn is_client_input(&self) -> bool {
101 match self {
102 Self::NotARepository(_)
103 | Self::ResolveRef { .. }
104 | Self::InvalidBotPattern(_)
105 | Self::InvalidWindow(_)
106 | Self::InvalidTimestamp(_)
107 | Self::InvalidFormula(_)
108 | Self::InvalidFileTypeScope(_)
109 | Self::InvalidBusFactorThreshold(_)
110 | Self::InvalidAuthorHashKey(_)
111 | Self::InvalidTrend(_)
112 | Self::InvalidDiff(_) => true,
113 Self::OpenRepository(_)
114 | Self::Walk(_)
115 | Self::Diff(_)
116 | Self::Mailmap(_)
117 | Self::Blame(_)
118 | Self::Cache(_) => false,
119 }
120 }
121}
122
123impl std::fmt::Display for Error {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 // bca: suppress(cyclomatic)
126 // Exhaustive one-write-per-variant Display match: cyclomatic here
127 // is the variant count, not branching logic, and the arms mirror
128 // the compile-enforced `Error` enum one-to-one. Splitting it into
129 // sub-matches would be an arbitrary partition with no semantic
130 // boundary that would drift out of sync with the enum.
131 match self {
132 Self::NotARepository(path) => {
133 write!(
134 f,
135 "{} is not inside a supported version-control working tree",
136 path.display()
137 )
138 }
139 Self::OpenRepository(reason) => write!(f, "failed to open repository: {reason}"),
140 Self::ResolveRef { reference, reason } => {
141 write!(f, "failed to resolve revision {reference:?}: {reason}")
142 }
143 Self::Walk(reason) => write!(f, "failed to walk commit history: {reason}"),
144 Self::Diff(reason) => write!(f, "failed to compute diff: {reason}"),
145 Self::Mailmap(reason) => write!(f, "failed to apply .mailmap: {reason}"),
146 Self::InvalidBotPattern(reason) => write!(f, "invalid bot pattern: {reason}"),
147 Self::InvalidWindow(reason) => write!(f, "invalid time window: {reason}"),
148 Self::InvalidTimestamp(reason) => write!(f, "invalid timestamp: {reason}"),
149 Self::InvalidFormula(name) => write!(
150 f,
151 "unknown risk formula {name:?} (expected `weighted` or `percentile`)"
152 ),
153 Self::InvalidFileTypeScope(reason) => write!(f, "invalid file-type scope: {reason}"),
154 Self::InvalidBusFactorThreshold(reason) => {
155 write!(f, "invalid bus-factor threshold: {reason}")
156 }
157 Self::InvalidAuthorHashKey(reason) => {
158 write!(f, "invalid author-hash key: {reason}")
159 }
160 Self::InvalidTrend(reason) => write!(f, "invalid trend parameters: {reason}"),
161 Self::Blame(reason) => write!(f, "failed to blame file: {reason}"),
162 Self::InvalidDiff(reason) => write!(f, "invalid unified diff: {reason}"),
163 Self::Cache(reason) => write!(f, "history cache error: {reason}"),
164 }
165 }
166}
167
168impl std::error::Error for Error {}
169
170#[cfg(test)]
171#[path = "error_tests.rs"]
172mod tests;