| 130 | } |
| 131 | |
| 132 | fn resolve_change(repo: &Repository, prefix: &str) -> CliResult<Hash> { |
| 133 | // Validate input shape first for a precise error — the shared resolver |
| 134 | // treats malformed input as "no match". |
| 135 | let prefix = prefix.to_ascii_uppercase(); |
| 136 | if prefix.is_empty() |
| 137 | || prefix.len() > 52 |
| 138 | || !prefix |
| 139 | .bytes() |
| 140 | .all(|b| b.is_ascii_uppercase() || (b'2'..=b'7').contains(&b)) |
| 141 | { |
| 142 | return Err(CliError::InvalidArgument { |
| 143 | message: "Expected a Base32 change hash or a non-empty unique prefix".into(), |
| 144 | }); |
| 145 | } |
| 146 | |
| 147 | // The repository's shared, case-insensitive prefix resolver (the same |
| 148 | // one `atomic insert` uses) matches against the whole change store, so a |
| 149 | // hash that exists only on another view still resolves here and then |
| 150 | // hits the repository's explicit membership guard in `unrecord`. |
| 151 | match repo.find_change_by_prefix(&prefix) { |
| 152 | Ok(Some(hash)) => Ok(hash), |
| 153 | Ok(None) => Err(CliError::ChangeNotFound { hash: prefix }), |
| 154 | Err(RepositoryError::AmbiguousHash { prefix, matches }) => Err(CliError::AmbiguousHash { |
| 155 | hash: format!("{prefix} (matches: {})", matches.join(", ")), |
| 156 | }), |
| 157 | Err(e) => Err(CliError::Internal(anyhow::anyhow!("{e}"))), |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | #[cfg(test)] |
| 162 | mod tests { |