don't
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(knot): improve error type for git operations

Signed-off-by: tjh <did:plc:65gha4t3avpfpzmvpbwovss7>

+45 -59
+45 -59
crates/knot/src/public/git.rs
··· 50 50 const RECEIVE_PACK_ADVERTISEMENT: &str = "application/x-git-receive-pack-advertisement"; 51 51 const RECEIVE_PACK_RESULT: &str = "application/x-git-receive-pack-result"; 52 52 53 - pub struct Error { 54 - code: StatusCode, 55 - error: String, 56 - } 53 + pub struct GitError(StatusCode, String); 57 54 58 - impl Error { 59 - fn new<E: std::fmt::Display>(error: E) -> Self { 60 - Self { 61 - code: StatusCode::INTERNAL_SERVER_ERROR, 62 - error: error.to_string(), 63 - } 64 - } 65 - 66 - fn not_found() -> Self { 67 - Self { 68 - code: StatusCode::NOT_FOUND, 69 - error: String::new(), 70 - } 71 - } 72 - 73 - #[allow(unused)] 74 - fn forbidden<E: std::fmt::Display>(error: E) -> Self { 75 - Self { 76 - code: StatusCode::FORBIDDEN, 77 - error: error.to_string(), 78 - } 79 - } 80 - } 81 - 82 - impl IntoResponse for Error { 55 + impl IntoResponse for GitError { 83 56 fn into_response(self) -> Response { 84 - (self.code, self.error).into_response() 57 + let Self(status, error) = self; 58 + Response::builder() 59 + .status(status) 60 + .header(CONTENT_TYPE, "text/html; charset=utf-8") 61 + .body(Body::from(error)) 62 + .expect("Failed to build body") 85 63 } 86 64 } 87 65 88 - impl From<std::io::Error> for Error { 89 - fn from(value: std::io::Error) -> Self { 90 - Self::new(value) 66 + pub struct NotFound<E: std::fmt::Display>(E); 67 + 68 + impl<E: std::fmt::Display> From<NotFound<E>> for GitError { 69 + fn from(value: NotFound<E>) -> Self { 70 + Self(StatusCode::NOT_FOUND, value.0.to_string()) 91 71 } 92 72 } 93 73 94 - impl From<tokio::task::JoinError> for Error { 95 - fn from(error: tokio::task::JoinError) -> Self { 96 - Self::new(error) 74 + pub struct Forbidden<E: std::fmt::Display>(E); 75 + 76 + impl<E: std::fmt::Display> From<Forbidden<E>> for GitError { 77 + fn from(value: Forbidden<E>) -> Self { 78 + Self(StatusCode::FORBIDDEN, value.0.to_string()) 97 79 } 98 80 } 99 81 100 - impl From<anyhow::Error> for Error { 101 - fn from(error: anyhow::Error) -> Self { 102 - Self::new(error) 103 - } 82 + macro_rules! internal_error { 83 + ($typ:ty, $status:expr) => { 84 + impl From<$typ> for GitError { 85 + fn from(value: $typ) -> Self { 86 + Self($status, value.to_string()) 87 + } 88 + } 89 + }; 90 + ($typ:ty) => { 91 + internal_error!($typ, StatusCode::INTERNAL_SERVER_ERROR); 92 + }; 104 93 } 105 94 106 - impl From<axum::Error> for Error { 107 - fn from(error: axum::Error) -> Self { 108 - Self::new(error) 109 - } 110 - } 95 + internal_error!(std::io::Error); 96 + internal_error!(tokio::task::JoinError); 97 + internal_error!(anyhow::Error); 98 + internal_error!(axum::Error); 111 99 112 - type Result<T> = std::result::Result<T, Error>; 100 + type Result<T> = std::result::Result<T, GitError>; 113 101 114 102 pub fn router() -> Router<Knot> { 115 103 use axum::routing::{get, post}; ··· 128 140 repo: RepoSpec, 129 141 protocol: Option<GitProtocol>, 130 142 ) -> Result<Response> { 131 - let repo = knot.open(&repo).map_err(|_| Error::not_found())?; 143 + let repo = knot.open(&repo).map_err(|e| NotFound(e.message))?; 132 144 133 145 let (mut stdout_tx, stdout_rx) = async_pipe()?; 134 146 ··· 173 185 repo: RepoSpec, 174 186 protocol: Option<GitProtocol>, 175 187 ) -> Result<Response> { 176 - let repo = knot.reopen(&repo).map_err(|_| Error::not_found())?; 188 + let repo = knot.reopen(&repo).map_err(|e| NotFound(e.message))?; 177 189 check_push_status(&repo)?; 178 190 179 191 let (mut stdout_tx, stdout_rx) = async_pipe()?; ··· 217 229 protocol: Option<GitProtocol>, 218 230 body: Body, 219 231 ) -> Result<Response> { 220 - let repo = knot.open(&repo).map_err(|_| Error::not_found())?; 232 + let repo = knot.open(&repo).map_err(|e| NotFound(e.message))?; 221 233 222 234 // The request body for git-upload-pack should be quite small, so just 223 235 // buffer it. ··· 266 278 protocol: Option<GitProtocol>, 267 279 body: Body, 268 280 ) -> Result<impl IntoResponse> { 269 - let repo = knot.reopen(&repo).map_err(|_| Error::not_found())?; 281 + let repo = knot.reopen(&repo).map_err(|e| NotFound(e.message))?; 270 282 check_push_status(&repo)?; 271 283 272 284 let mut child = Command::new(GIT_COMMAND) ··· 309 321 .boolean("receive.advertisePushOptions") 310 322 .unwrap_or_default() 311 323 { 312 - return Err(Error::forbidden( 313 - "'receive.advertisePushOptions is not set or 'false'", 314 - )); 324 + Err(Forbidden( 325 + "'receive.advertisePushOptions' is not set or 'false'", 326 + ))?; 315 327 } 316 328 317 329 if config ··· 319 331 .unwrap_or_default() 320 332 .is_empty() 321 333 { 322 - return Err(Error::forbidden( 323 - "'receive.certNonceSeed is not set or empty", 324 - )); 334 + Err(Forbidden("'receive.certNonceSeed' is not set or empty"))?; 325 335 } 326 336 327 337 // Verify a pre-receive hook is present. 328 338 let pre_receive_path = repo.path().join("hooks/pre-receive"); 329 339 let Ok(metadata) = std::fs::metadata(&pre_receive_path) else { 330 - return Err(Error::forbidden("Failed to stat 'hooks/pre-receive'")); 340 + return Err(Forbidden("Failed to stat 'hooks/pre-receive'"))?; 331 341 }; 332 342 333 343 if !(metadata.is_file() || metadata.is_symlink()) 334 344 || (metadata.permissions().mode() & 0o111 == 0) 335 345 { 336 - return Err(Error::forbidden( 346 + Err(Forbidden( 337 347 "'hooks/pre-receive' is not an executable file/symlink", 338 - )); 348 + ))?; 339 349 } 340 350 341 351 Ok(())