diff --git a/crates/lib/src/api/client/repositories.rs b/crates/lib/src/api/client/repositories.rs index 67acf625f..5de3a3ba7 100644 --- a/crates/lib/src/api/client/repositories.rs +++ b/crates/lib/src/api/client/repositories.rs @@ -113,7 +113,7 @@ pub async fn get_by_remote(remote: &Remote) -> Result Result { latest_commit = Some(commit); } } - latest_commit.ok_or(OxenError::no_commits_found()) + latest_commit.ok_or_else(|| OxenError::NoCommitsFound) } fn head_commit_id(repo: &LocalRepository) -> Result { let commit_id = with_ref_manager(repo, |manager| manager.head_commit_id())?; match commit_id { Some(commit_id) => Ok(commit_id.parse()?), - None => Err(OxenError::head_not_found()), + None => Err(OxenError::HeadNotFound), } } @@ -257,7 +257,7 @@ pub fn create_empty_commit( ) -> Result { let branch_name = branch_name.as_ref(); let Some(existing_commit) = repositories::revisions::get(repo, branch_name)? else { - return Err(OxenError::revision_not_found(branch_name.into())); + return Err(OxenError::RevisionNotFound(branch_name.into())); }; let existing_commit_id = existing_commit.id.parse()?; let existing_node = @@ -677,9 +677,9 @@ pub fn list_from_paginated_impl( let base = split[0]; let head = split[1]; let base_commit = repositories::commits::get_by_id(repo, base)? - .ok_or(OxenError::revision_not_found(base.into()))?; + .ok_or(OxenError::RevisionNotFound(base.into()))?; let head_commit = repositories::commits::get_by_id(repo, head)? - .ok_or(OxenError::revision_not_found(head.into()))?; + .ok_or(OxenError::RevisionNotFound(head.into()))?; let (commits, total_count) = list_recursive_paginated(repo, head_commit, skip, limit, Some(&base_commit), None)?; @@ -791,7 +791,7 @@ pub fn count_from( let revision = revision.as_ref(); let commit = repositories::revisions::get(repo, revision)? - .ok_or_else(|| OxenError::revision_not_found(revision.into()))?; + .ok_or_else(|| OxenError::RevisionNotFound(revision.into()))?; let db = open_commit_count_db(repo)?; diff --git a/crates/lib/src/core/v_latest/entries.rs b/crates/lib/src/core/v_latest/entries.rs index 80680e021..f67d2624c 100644 --- a/crates/lib/src/core/v_latest/entries.rs +++ b/crates/lib/src/core/v_latest/entries.rs @@ -72,7 +72,7 @@ pub fn list_directory_with_depth( let commit = parsed_resource .commit .clone() - .ok_or(OxenError::revision_not_found(revision.into()))?; + .ok_or(OxenError::RevisionNotFound(revision.into()))?; log::debug!("list_directory commit {commit}"); @@ -537,7 +537,7 @@ pub fn list_for_commit( pub fn update_metadata(repo: &LocalRepository, revision: impl AsRef) -> Result<(), OxenError> { let commit = repositories::revisions::get(repo, revision.as_ref())? - .ok_or_else(|| OxenError::revision_not_found(revision.as_ref().to_string().into()))?; + .ok_or_else(|| OxenError::RevisionNotFound(revision.as_ref().to_string().into()))?; let tree: CommitMerkleTree = CommitMerkleTree::from_commit(repo, &commit)?; let mut node = tree.root; diff --git a/crates/lib/src/core/v_latest/pull.rs b/crates/lib/src/core/v_latest/pull.rs index 58b06643e..809c61c64 100644 --- a/crates/lib/src/core/v_latest/pull.rs +++ b/crates/lib/src/core/v_latest/pull.rs @@ -48,7 +48,7 @@ pub async fn pull_remote_branch( let remote_branch = fetch::fetch_remote_branch(repo, &remote_repo, &fetch_opts).await?; let new_head_commit = repositories::revisions::get(repo, &remote_branch.commit_id)?.ok_or( - OxenError::revision_not_found(remote_branch.commit_id.to_owned().into()), + OxenError::RevisionNotFound(remote_branch.commit_id.to_owned().into()), )?; match previous_head_commit { @@ -72,8 +72,8 @@ pub async fn pull_remote_branch( } Ok(None) => { // Merge conflict, keep the previous commit - return Err(OxenError::merge_conflict( - "There was a merge conflict, please resolve it before pulling", + return Err(OxenError::UpstreamMergeConflict( + "There was a merge conflict, please resolve it before pulling.".into(), )); } Err(e) => return Err(e), diff --git a/crates/lib/src/core/v_latest/push.rs b/crates/lib/src/core/v_latest/push.rs index c3edee1b2..6a451b48a 100644 --- a/crates/lib/src/core/v_latest/push.rs +++ b/crates/lib/src/core/v_latest/push.rs @@ -72,7 +72,7 @@ async fn push_local_branch_to_remote_repo( ) -> Result<(), OxenError> { // Get the commit from the branch let Some(commit) = repositories::commits::get_by_id(repo, &local_branch.commit_id)? else { - return Err(OxenError::revision_not_found( + return Err(OxenError::RevisionNotFound( local_branch.commit_id.clone().into(), )); }; @@ -136,7 +136,7 @@ async fn push_to_existing_branch( let latest_remote_commit = repositories::commits::get_by_id(repo, &remote_branch.commit_id)?.ok_or_else( - || OxenError::revision_not_found(remote_branch.commit_id.clone().into()), + || OxenError::RevisionNotFound(remote_branch.commit_id.clone().into()), )?; let mut commits = diff --git a/crates/lib/src/core/v_latest/workspaces/commit.rs b/crates/lib/src/core/v_latest/workspaces/commit.rs index 28ab599a5..4a142ffe9 100644 --- a/crates/lib/src/core/v_latest/workspaces/commit.rs +++ b/crates/lib/src/core/v_latest/workspaces/commit.rs @@ -61,7 +61,7 @@ pub async fn commit( let conflicts = list_conflicts(workspace, &dir_entries, &branch)?; if !conflicts.is_empty() { - return Err(OxenError::workspace_behind(workspace)); + return Err(OxenError::WorkspaceBehind(Box::new(workspace.clone()))); } let dir_entries = export_tabular_data_frames(workspace, dir_entries).await?; @@ -110,16 +110,14 @@ pub fn mergeability( let branch_name = branch_name.as_ref(); let Some(branch) = repositories::branches::get_by_name(&workspace.base_repo, branch_name)? else { - return Err(OxenError::revision_not_found( - branch_name.to_string().into(), - )); + return Err(OxenError::BranchNotFound(branch_name.into())); }; let base = &workspace.commit; let Some(head) = repositories::commits::get_by_id(&workspace.base_repo, &branch.commit_id)? else { - return Err(OxenError::revision_not_found( - branch.commit_id.clone().into(), + return Err(OxenError::RevisionNotFound( + branch.commit_id.as_str().into(), )); }; @@ -174,8 +172,8 @@ fn list_conflicts( let Some(branch_commit) = repositories::commits::get_by_id(&workspace.base_repo, &branch.commit_id)? else { - return Err(OxenError::revision_not_found( - branch.commit_id.clone().into(), + return Err(OxenError::RevisionNotFound( + branch.commit_id.as_str().into(), )); }; log::debug!( @@ -195,22 +193,22 @@ fn list_conflicts( let Some(branch_commit) = repositories::commits::get_by_id(&workspace.base_repo, &branch.commit_id)? else { - return Err(OxenError::revision_not_found( - branch.commit_id.clone().into(), + return Err(OxenError::RevisionNotFound( + branch.commit_id.as_str().into(), )); }; let Some(branch_tree) = repositories::tree::get_root_with_children(&workspace.base_repo, &branch_commit)? else { - return Err(OxenError::revision_not_found( - branch.commit_id.clone().into(), + return Err(OxenError::RevisionNotFound( + branch.commit_id.as_str().into(), )); }; let Some(workspace_tree) = repositories::tree::get_root_with_children(&workspace.base_repo, workspace_commit)? else { - return Err(OxenError::revision_not_found( - workspace.commit.id.clone().into(), + return Err(OxenError::RevisionNotFound( + workspace.commit.id.as_str().into(), )); }; diff --git a/crates/lib/src/core/v_latest/workspaces/data_frames.rs b/crates/lib/src/core/v_latest/workspaces/data_frames.rs index b5996f87f..ee67fd2a5 100644 --- a/crates/lib/src/core/v_latest/workspaces/data_frames.rs +++ b/crates/lib/src/core/v_latest/workspaces/data_frames.rs @@ -31,7 +31,7 @@ pub fn is_queryable_data_frame_indexed( match get_queryable_data_frame_workspace(repo, path, commit) { Ok(_workspace) => Ok(true), Err(e) => match e { - OxenError::QueryableWorkspaceNotFound() => Ok(false), + OxenError::QueryableWorkspaceNotFound => Ok(false), _ => Err(e), }, } @@ -47,7 +47,7 @@ pub fn is_queryable_data_frame_indexed_from_file_node( { Ok(_workspace) => Ok(true), Err(e) => match e { - OxenError::QueryableWorkspaceNotFound() => Ok(false), + OxenError::QueryableWorkspaceNotFound => Ok(false), _ => Err(e), }, } @@ -78,7 +78,7 @@ pub fn get_queryable_data_frame_workspace_from_file_node( } } - Err(OxenError::QueryableWorkspaceNotFound()) + Err(OxenError::QueryableWorkspaceNotFound) } pub fn get_queryable_data_frame_workspace( diff --git a/crates/lib/src/core/v_latest/workspaces/diff.rs b/crates/lib/src/core/v_latest/workspaces/diff.rs index bf907b8cb..b92c16870 100644 --- a/crates/lib/src/core/v_latest/workspaces/diff.rs +++ b/crates/lib/src/core/v_latest/workspaces/diff.rs @@ -25,7 +25,7 @@ pub fn diff(workspace: &Workspace, path: impl AsRef) -> Result bool { false } +#[derive(Debug, thiserror::Error)] +enum InvalidUrlError { + #[error("URL has no host")] + NoHost, + + #[error("DNS resolution failed for {host}: {source}")] + DnsResolutionFailed { + host: String, + source: std::io::Error, + }, + + #[error("URL resolves to a private/reserved IP address: {0}")] + PrivateIp(std::net::IpAddr), +} + /// Resolves a URL's hostname via DNS and rejects it if any resolved address is /// private/reserved. This prevents SSRF attacks where a user-supplied URL could reach /// internal services (e.g. cloud metadata at 169.254.169.254, internal APIs, etc.). -async fn validate_url_target(url: &Url) -> Result<(), OxenError> { - let host = url - .host_str() - .ok_or_else(|| OxenError::file_import_error("URL has no host"))?; +async fn validate_url_target(url: &Url) -> Result<(), InvalidUrlError> { + let host = url.host_str().ok_or(InvalidUrlError::NoHost)?; let port = url.port_or_known_default().unwrap_or(443); let addr = format!("{host}:{port}"); - let resolved = tokio::net::lookup_host(&addr).await.map_err(|e| { - OxenError::file_import_error(format!("DNS resolution failed for {host}: {e}")) - })?; + let resolved = + tokio::net::lookup_host(&addr) + .await + .map_err(|e| InvalidUrlError::DnsResolutionFailed { + host: host.to_string(), + source: e, + })?; for socket_addr in resolved { if is_private_ip(&socket_addr.ip()) { - return Err(OxenError::file_import_error(format!( - "URL resolves to a private/reserved IP address: {}", - socket_addr.ip() - ))); + return Err(InvalidUrlError::PrivateIp(socket_addr.ip())); } } @@ -353,7 +367,9 @@ pub async fn import( )); } - validate_url_target(&parsed_url).await?; + validate_url_target(&parsed_url) + .await + .map_err(|e| OxenError::ImportFileError(format!("{e}").into()))?; let auth_header_value = HeaderValue::from_str(auth) .map_err(|_e| OxenError::file_import_error(format!("Invalid header auth value {auth}")))?; @@ -407,12 +423,12 @@ pub async fn upload_zip( log::debug!("workspace::commit ✅ success! commit {commit:?}"); Ok(commit) } - Err(OxenError::WorkspaceBehind(workspace)) => { + workspace_behind_error @ Err(OxenError::WorkspaceBehind(_)) => { log::error!( "unable to commit branch {:?}. Workspace behind", branch.name ); - Err(OxenError::WorkspaceBehind(workspace)) + workspace_behind_error } Err(err) => { log::error!("unable to commit branch {:?}. Err: {}", branch.name, err); @@ -479,7 +495,9 @@ async fn fetch_file( "Redirect to non-HTTP(S) URL is not allowed", )); } - validate_url_target(&next_url).await?; + validate_url_target(&next_url) + .await + .map_err(|e| OxenError::ImportFileError(format!("{e}").into()))?; current_url = next_url; continue; diff --git a/crates/lib/src/core/v_old/v0_19_0/pull.rs b/crates/lib/src/core/v_old/v0_19_0/pull.rs index cd4e69e1b..6c8803028 100644 --- a/crates/lib/src/core/v_old/v0_19_0/pull.rs +++ b/crates/lib/src/core/v_old/v0_19_0/pull.rs @@ -44,7 +44,7 @@ pub async fn pull_remote_branch( fetch::fetch_remote_branch(repo, &remote_repo, fetch_opts).await?; let new_head_commit = repositories::revisions::get(repo, branch)? - .ok_or(OxenError::revision_not_found(branch.to_owned().into()))?; + .ok_or(OxenError::RevisionNotFound(branch.to_owned().into()))?; // Merge if there are changes if let Some(previous_head_commit) = &previous_head_commit { diff --git a/crates/lib/src/error.rs b/crates/lib/src/error.rs index 353fb34e5..0397aa1ed 100644 --- a/crates/lib/src/error.rs +++ b/crates/lib/src/error.rs @@ -8,13 +8,12 @@ use std::io; use std::num::ParseIntError; use std::path::Path; use std::path::PathBuf; -use std::path::StripPrefixError; use tokio::task::JoinError; +use crate::model::ParsedResource; use crate::model::RepoNew; use crate::model::Schema; use crate::model::Workspace; -use crate::model::{Commit, ParsedResource}; pub mod path_buf_error; pub mod string_error; @@ -22,179 +21,293 @@ pub mod string_error; pub use crate::error::path_buf_error::PathBufError; pub use crate::error::string_error::StringError; -pub const HEAD_NOT_FOUND: &str = "HEAD not found"; - -pub const EMAIL_AND_NAME_NOT_FOUND: &str = "oxen not configured, set email and name with:\n\noxen config --name YOUR_NAME --email YOUR_EMAIL\n"; - pub const AUTH_TOKEN_NOT_FOUND: &str = "oxen authentication token not found, obtain one from your administrator and configure with:\n\noxen config --auth \n"; #[derive(thiserror::Error, Debug)] pub enum OxenError { - // User - #[error("{0}")] - UserConfigNotFound(Box), - + // + // Configuration + // + /// The user configuration file cannot be found at $HOME/.config/oxen/user_config.toml + #[error( + "oxen not configured, set email and name with:\n\noxen config --name YOUR_NAME --email YOUR_EMAIL\n" + )] + UserConfigNotFound, + + // // Repo + // + /// When an operation assumes a repository exists but it cannot be found. #[error("Repository '{0}' not found")] RepoNotFound(Box), + + /// When a local repository cannot be found at the given path. #[error("No oxen repository found at {0}")] - LocalRepoNotFound(Box), + LocalRepoNotFound(PathBufError), + + /// Error during repository creation: attempt to create a repository that already exists. #[error("Repository '{0}' already exists")] RepoAlreadyExists(Box), - #[error("Repository already exists at destination: {0}")] - RepoAlreadyExistsAtDestination(Box), + + /// Error when creating a repository: repo names are restricted. #[error("Invalid repository or namespace name '{0}'. Must match [a-zA-Z0-9][a-zA-Z0-9_.-]+")] InvalidRepoName(StringError), - // Fork - #[error("{0}")] - ForkStatusNotFound(StringError), + /// When `get_fork_status` cannot obtain the fork status for a repository. + #[error("No fork status found.")] + ForkStatusNotFound, + // // Remotes + // + /// A remote repository with a given name was not located on the server. #[error("Remote repository not found: {0}")] - RemoteRepoNotFound(Box), - #[error("{0}")] - RemoteAheadOfLocal(StringError), - #[error("{0}")] - IncompleteLocalHistory(StringError), - #[error("{0}")] - RemoteBranchLocked(StringError), + RemoteRepoNotFound(StringError), + + /// A merge cannot occur because there's a conflict with its upstream tracking branch. #[error("{0}")] UpstreamMergeConflict(StringError), + // // Branches/Commits + // + /// A branch with a given name was not found in the repository. #[error("{0}")] - BranchNotFound(Box), + BranchNotFound(StringError), + + /// A given revision (commit hash) was not found in the repository. #[error("Revision not found: {0}")] - RevisionNotFound(Box), - #[error("Root commit does not match: {0}")] - RootCommitDoesNotMatch(Box), - #[error("{0}")] - NothingToCommit(StringError), - #[error("{0}")] - NoCommitsFound(StringError), - #[error("{0}")] - HeadNotFound(StringError), + RevisionNotFound(StringError), + /// The repository is empty: it has no commits. + #[error("No commits found.")] + NoCommitsFound, + + /// The repository's current branch (head) cannot be located. + #[error("HEAD not found.")] + HeadNotFound, + + // // Workspaces + // + /// The workspace wasn't found (either locally or on a remote server). #[error("Workspace not found: {0}")] - WorkspaceNotFound(Box), + WorkspaceNotFound(StringError), + + /// No queryable workspace was found. #[error("No queryable workspace found")] - QueryableWorkspaceNotFound(), + QueryableWorkspaceNotFound, + + /// The workspace is behind the remote repository and cannot be automatically updated. #[error("Workspace is behind: {0}")] WorkspaceBehind(Box), + // // Resources (paths, uris, etc.) + // + /// A resource (entry, path, commit, etc.) was not found. #[error("Resource not found: {0}")] ResourceNotFound(StringError), + + /// The given path was not found, either in the repository or in the filesystem. #[error("Path does not exist: {0}")] - PathDoesNotExist(Box), + PathDoesNotExist(PathBufError), + + /// A parsed resource was not found. #[error("Resource not found: {0}")] - ParsedResourceNotFound(Box), + ParsedResourceNotFound(PathBufError), + // // Versioning + // + /// The repository must be migrated before it can be used. + /// This is due to the repository being at an older version than the oxen server or client + /// being used on it. #[error("{0}")] MigrationRequired(StringError), + + /// The oxen client or server must be updated before it can be used. #[error("{0}")] OxenUpdateRequired(StringError), + + /// The version is invalid or unsupported. #[error("Invalid version: {0}")] InvalidVersion(StringError), - // Entry + /// A commit entry is not present in the repository. #[error("{0}")] CommitEntryNotFound(StringError), - // Schema + // + // Schema (dataframes) + // + /// The schema is invalid or unsupported for dataframe operations. #[error("Invalid schema: {0}")] InvalidSchema(Box), + + /// The schemas of the data frames are incompatible. #[error("Incompatible schemas: {0}")] IncompatibleSchemas(Box), + + /// The file type is unsupported for data frame operations. #[error("{0}")] InvalidFileType(StringError), + + /// A column name already exists in the dataframe's schema and cannot be added again. #[error("{0}")] ColumnNameAlreadyExists(StringError), + + /// A column name was requested in a dataframe, but no such column exists. #[error("{0}")] ColumnNameNotFound(StringError), + + /// An operation is not supported for the the dataframe. #[error("{0}")] UnsupportedOperation(StringError), + // // Metadata + // + /// Thumbnails can only be created when the ffmpeg feature is enabled. + #[error( + "Video thumbnail generation requires the 'ffmpeg' feature to be enabled. Build with --features liboxen/ffmpeg to enable this functionality." + )] + ThumbnailingNotEnabled, + + // + // Dataframes + // + /// No rows were found for a given SQL query. + #[error("Query returned no rows")] + NoRowsFound, + + /// An error encountered during dataframe operations. + /// Contains a human-readable description of the error. #[error("{0}")] - ImageMetadataParseError(StringError), - #[error("{0}")] - ThumbnailingNotEnabled(StringError), - - // SQL - #[error("SQL parse error: {0}")] - SQLParseError(StringError), - #[error("{0}")] - NoRowsFound(StringError), - - // CLI Interaction - #[error("{0}")] - OperationCancelled(StringError), + DataFrameError(StringError), - // fs / io + /// Adding a file into a workspace #[error("{0}")] - StripPrefixError(StringError), + ImportFileError(StringError), - // Dataframe Errors + /// An error encountered during SQL parsing. #[error("{0}")] - DataFrameError(StringError), + SQLParseError(StringError), - // File Import Error - #[error("{0}")] - ImportFileError(StringError), + // + // + // Wrappers + // + // + /// Wraps the error from std::path::strip_prefix. + #[error("Error stripping prefix: {0}")] + StripPrefixError(#[from] std::path::StripPrefixError), - // External Library Errors + /// Wraps errors encotunered from file reading & writing operations. #[error("{0}")] IO(#[from] io::Error), + + /// Encountered when authentication fails. Contains the authentication error message. #[error("Authentication failed: {0}")] Authentication(StringError), + + /// Wraps errors from the Arrow library, which are encountered in dataframe operations. #[error("{0}")] ArrowError(#[from] ArrowError), + + /// Wraps errors from bincode when serializing and deserializing Rust objects into binary data. #[error("{0}")] BinCodeError(#[from] bincode::Error), + + /// Wraps errors encountered when trying to serialize TOML data. #[error("Configuration error: {0}")] TomlSer(#[from] toml::ser::Error), + + /// Wraps errors encountered when deserializing invalid TOML data. #[error("Configuration error: {0}")] TomlDe(#[from] toml::de::Error), + + /// Wraps errors encountered when parsing malformed URIs. #[error("Invalid URI: {0}")] URI(#[from] http::uri::InvalidUri), + + /// Wraps errors encountered when parsing malformed URLs. #[error("Invalid URL: {0}")] URL(#[from] url::ParseError), + + /// Wraps JSON serialization and deserialization errors. #[error("JSON error: {0}")] JSON(#[from] serde_json::Error), + + /// Wraps any HTTP client errors we encounter. #[error("Network error: {0}")] HTTP(#[from] reqwest::Error), + + /// Wraps any error we encounter from handling non-UTF-8 strings. + /// + /// Most often occurs when interacting with filesystem paths as much + /// of the oxen codebase relies on paths being valid UTF-8 strings. #[error("UTF-8 encoding error: {0}")] UTF8Error(#[from] std::str::Utf8Error), + + /// Wraps any error we encounter from converting a byte slice to a UTF-8 string. + #[error("UTF-8 conversion error: {0}")] + Utf8ConvError(#[from] std::string::FromUtf8Error), + + /// Wraps any error we encounter from interacting with RocksDB. #[error("Database error: {0}")] DB(#[from] rocksdb::Error), + + /// Wraps any error we encounter from interacting with DuckDB. #[error("Query error: {0}")] DUCKDB(#[from] duckdb::Error), + + /// Wraps any error we encounter from interacting with environment variables. #[error("Environment variable error: {0}")] ENV(#[from] std::env::VarError), + + /// Wraps any error that we get from using the image crate (image processing). #[error("Image processing error: {0}")] ImageError(#[from] image::ImageError), + + /// Wraps any error that we get from Redis client use. #[error("Redis error: {0}")] RedisError(#[from] redis::RedisError), + + /// Wraps any error that we get from using r2d2 (the connection pool). #[error("Connection pool error: {0}")] R2D2Error(#[from] r2d2::Error), + + /// Wraps any error that we get from using jwalk (directory traversal). #[error("Directory traversal error: {0}")] JwalkError(#[from] jwalk::Error), + + /// Wraps any error that we get from parsing malformed glob patterns. #[error("Pattern error: {0}")] PatternError(#[from] glob::PatternError), + + /// Wraps any error that we encounter when walking paths emitted from a glob pattern. #[error("Glob error: {0}")] GlobError(#[from] glob::GlobError), + + /// Wraps any error that we get from using polars (dataframe operations). #[error("DataFrame error: {0}")] PolarsError(#[from] polars::prelude::PolarsError), + + /// Wraps any error that we get from parsing integers from strings. #[error("Invalid integer: {0}")] ParseIntError(#[from] ParseIntError), + + /// Wraps any error that we get from decoding message pack data. #[error("Decode error: {0}")] RmpDecodeError(#[from] rmp_serde::decode::Error), + /// Wraps any error that we get from joining tasks. + #[error("{0}")] + JoinError(#[from] JoinError), + // Fallback + // TODO: remove all uses of `Basic` and replace with specific errors. #[error("{0}")] Basic(StringError), } @@ -217,8 +330,7 @@ impl OxenError { RevisionNotFound(_) => { "Check available branches with `oxen branch --all` or commits with `oxen log`." } - NothingToCommit(_) => "Stage changes with `oxen add ` before committing.", - HeadNotFound(_) | NoCommitsFound(_) => { + HeadNotFound | NoCommitsFound => { "This repository has no commits yet. Add files and create your first commit." } PathDoesNotExist(_) @@ -255,18 +367,10 @@ impl OxenError { OxenError::Basic(StringError::from(s.as_ref())) } - pub fn thumbnailing_not_enabled(s: impl AsRef) -> Self { - OxenError::ThumbnailingNotEnabled(StringError::from(s.as_ref())) - } - pub fn authentication(s: impl AsRef) -> Self { OxenError::Authentication(StringError::from(s.as_ref())) } - pub fn migration_required(s: impl AsRef) -> Self { - OxenError::MigrationRequired(StringError::from(s.as_ref())) - } - pub fn invalid_version(s: impl AsRef) -> Self { OxenError::InvalidVersion(StringError::from(s.as_ref())) } @@ -275,10 +379,6 @@ impl OxenError { OxenError::OxenUpdateRequired(StringError::from(s.as_ref())) } - pub fn user_config_not_found(value: StringError) -> Self { - OxenError::UserConfigNotFound(Box::new(value)) - } - pub fn repo_not_found(repo: RepoNew) -> Self { OxenError::RepoNotFound(Box::new(repo)) } @@ -294,60 +394,16 @@ impl OxenError { )) } - pub fn remote_ahead_of_local() -> Self { - OxenError::RemoteAheadOfLocal(StringError::from( - "\nRemote ahead of local, must pull changes. To fix run:\n\n oxen pull\n", - )) - } - - pub fn upstream_merge_conflict() -> Self { - OxenError::UpstreamMergeConflict(StringError::from( - "\nRemote has conflicts with local branch. To fix run:\n\n oxen pull\n\nThen resolve conflicts and commit changes.\n", - )) - } - - pub fn merge_conflict(desc: impl AsRef) -> Self { - OxenError::UpstreamMergeConflict(StringError::from(desc.as_ref())) - } - - pub fn incomplete_local_history() -> Self { - OxenError::IncompleteLocalHistory(StringError::from( - "\nCannot push to an empty repository with an incomplete local history. To fix, pull the complete history from your remote:\n\n oxen pull --all\n", - )) - } - - pub fn remote_branch_locked() -> Self { - OxenError::RemoteBranchLocked(StringError::from( - "\nRemote branch is locked - another push is in progress. Wait a bit before pushing again, or try pushing to a new branch.\n", - )) - } - - pub fn operation_cancelled() -> Self { - OxenError::OperationCancelled(StringError::from("\nOperation cancelled.\n")) - } - pub fn resource_not_found(value: impl AsRef) -> Self { OxenError::ResourceNotFound(StringError::from(value.as_ref())) } pub fn path_does_not_exist(path: impl AsRef) -> Self { - OxenError::PathDoesNotExist(Box::new(path.as_ref().into())) - } - - pub fn image_metadata_error(s: impl AsRef) -> Self { - OxenError::ImageMetadataParseError(StringError::from(s.as_ref())) - } - - pub fn sql_parse_error(s: impl AsRef) -> Self { - OxenError::SQLParseError(StringError::from(s.as_ref())) + OxenError::PathDoesNotExist(path.as_ref().into()) } pub fn parsed_resource_not_found(resource: ParsedResource) -> Self { - OxenError::ParsedResourceNotFound(Box::new(resource.resource.into())) - } - - pub fn invalid_repo_name(s: impl AsRef) -> Self { - OxenError::InvalidRepoName(StringError::from(s.as_ref())) + OxenError::ParsedResourceNotFound(resource.resource.into()) } pub fn is_auth_error(&self) -> bool { @@ -363,52 +419,12 @@ impl OxenError { ) } - pub fn repo_already_exists(repo: RepoNew) -> Self { - OxenError::RepoAlreadyExists(Box::new(repo)) - } - - pub fn repo_already_exists_at_destination(value: StringError) -> Self { - OxenError::RepoAlreadyExistsAtDestination(Box::new(value)) - } - - pub fn fork_status_not_found() -> Self { - OxenError::ForkStatusNotFound(StringError::from("No fork status found")) - } - - pub fn revision_not_found(value: StringError) -> Self { - OxenError::RevisionNotFound(Box::new(value)) - } - - pub fn workspace_not_found(value: StringError) -> Self { - OxenError::WorkspaceNotFound(Box::new(value)) - } - - pub fn workspace_behind(workspace: &Workspace) -> Self { - OxenError::WorkspaceBehind(Box::new(workspace.clone())) - } - - pub fn root_commit_does_not_match(commit: Commit) -> Self { - OxenError::RootCommitDoesNotMatch(Box::new(commit)) - } - - pub fn no_commits_found() -> Self { - OxenError::NoCommitsFound(StringError::from("\n No commits found.\n")) - } - pub fn local_repo_not_found(dir: impl AsRef) -> OxenError { - OxenError::LocalRepoNotFound(Box::new(dir.as_ref().into())) + OxenError::LocalRepoNotFound(dir.as_ref().into()) } pub fn email_and_name_not_set() -> OxenError { - OxenError::user_config_not_found(EMAIL_AND_NAME_NOT_FOUND.to_string().into()) - } - - pub fn remote_repo_not_found(url: impl AsRef) -> OxenError { - OxenError::RemoteRepoNotFound(Box::new(StringError::from(url.as_ref()))) - } - - pub fn head_not_found() -> OxenError { - OxenError::HeadNotFound(StringError::from(HEAD_NOT_FOUND)) + OxenError::UserConfigNotFound } pub fn home_dir_not_found() -> OxenError { @@ -460,12 +476,12 @@ impl OxenError { pub fn remote_branch_not_found(name: impl AsRef) -> OxenError { let err = format!("Remote branch '{}' not found", name.as_ref()); - OxenError::BranchNotFound(Box::new(StringError::from(err))) + OxenError::BranchNotFound(err.into()) } pub fn local_branch_not_found(name: impl AsRef) -> OxenError { let err = format!("Branch '{}' not found", name.as_ref()); - OxenError::BranchNotFound(Box::new(StringError::from(err))) + OxenError::BranchNotFound(err.into()) } pub fn commit_db_corrupted(commit_id: impl AsRef) -> OxenError { @@ -487,7 +503,7 @@ impl OxenError { } pub fn entry_does_not_exist(path: impl AsRef) -> OxenError { - OxenError::ParsedResourceNotFound(Box::new(path.as_ref().into())) + OxenError::ParsedResourceNotFound(path.as_ref().into()) } pub fn file_error(path: impl AsRef, error: std::io::Error) -> OxenError { @@ -679,21 +695,3 @@ impl From for OxenError { OxenError::Basic(StringError::from(error)) } } - -impl From for OxenError { - fn from(error: StripPrefixError) -> Self { - OxenError::basic_str(format!("Error stripping prefix: {error}")) - } -} - -impl From for OxenError { - fn from(error: JoinError) -> Self { - OxenError::basic_str(error.to_string()) - } -} - -impl From for OxenError { - fn from(error: std::string::FromUtf8Error) -> Self { - OxenError::basic_str(format!("UTF8 conversion error: {error}")) - } -} diff --git a/crates/lib/src/repositories.rs b/crates/lib/src/repositories.rs index a3693a81b..3241bdd83 100644 --- a/crates/lib/src/repositories.rs +++ b/crates/lib/src/repositories.rs @@ -218,12 +218,12 @@ pub async fn create( ) -> Result { // Validate repo name if !is_valid_repo_name(&new_repo.name) { - return Err(OxenError::invalid_repo_name(&new_repo.name)); + return Err(OxenError::InvalidRepoName(new_repo.name.into())); } // Validate namespace if !is_valid_repo_name(&new_repo.namespace) { - return Err(OxenError::invalid_repo_name(&new_repo.namespace)); + return Err(OxenError::InvalidRepoName(new_repo.namespace.into())); } let repo_dir = root_dir @@ -231,7 +231,7 @@ pub async fn create( .join(Path::new(&new_repo.name)); if repo_dir.exists() { log::error!("Repository already exists {repo_dir:?}"); - return Err(OxenError::repo_already_exists(new_repo)); + return Err(OxenError::RepoAlreadyExists(Box::new(new_repo))); } // Create the repo dir diff --git a/crates/lib/src/repositories/checkout.rs b/crates/lib/src/repositories/checkout.rs index 562563213..51e718be2 100644 --- a/crates/lib/src/repositories/checkout.rs +++ b/crates/lib/src/repositories/checkout.rs @@ -28,7 +28,7 @@ pub async fn checkout( println!("Checkout branch: {value}"); let commit = repositories::revisions::get(repo, value)? - .ok_or(OxenError::revision_not_found(value.into()))?; + .ok_or(OxenError::RevisionNotFound(value.into()))?; let subtree_paths = match repo.subtree_paths() { Some(paths_vec) => paths_vec, // If Some(vec), take the inner vector None => vec![Path::new("").to_path_buf()], @@ -46,7 +46,7 @@ pub async fn checkout( } let commit = repositories::revisions::get(repo, value)? - .ok_or(OxenError::revision_not_found(value.into()))?; + .ok_or(OxenError::RevisionNotFound(value.into()))?; let previous_head_commit = repositories::commits::head_commit_maybe(repo)?; repositories::branches::checkout_commit_from_commit(repo, &commit, &previous_head_commit) diff --git a/crates/lib/src/repositories/diffs.rs b/crates/lib/src/repositories/diffs.rs index 6c28e947d..4d3be8c9e 100644 --- a/crates/lib/src/repositories/diffs.rs +++ b/crates/lib/src/repositories/diffs.rs @@ -174,7 +174,7 @@ pub async fn diff_uncommitted( let unstaged_files = status.unstaged_files(); log::debug!("unstaged_files: {unstaged_files:?}"); let commit_1 = repositories::revisions::get(repo, rev_1)? - .ok_or_else(|| OxenError::revision_not_found(rev_1.to_string().into()))?; + .ok_or_else(|| OxenError::RevisionNotFound(rev_1.into()))?; log::debug!("commit_1: {commit_1:?}"); let mut diff_result = Vec::new(); log::debug!("diff_result: {diff_result:?}"); @@ -224,9 +224,9 @@ pub async fn diff_revs( path_2.display() ); let commit_1 = repositories::revisions::get(repo, rev_1)? - .ok_or_else(|| OxenError::revision_not_found(rev_1.to_string().into()))?; + .ok_or_else(|| OxenError::RevisionNotFound(rev_1.into()))?; let commit_2 = repositories::revisions::get(repo, rev_2)? - .ok_or_else(|| OxenError::revision_not_found(rev_2.to_string().into()))?; + .ok_or_else(|| OxenError::RevisionNotFound(rev_2.into()))?; let dir_diff = diff_path(repo, &commit_1, &commit_2, path_1, path_2, opts).await?; log::debug!( diff --git a/crates/lib/src/repositories/fork.rs b/crates/lib/src/repositories/fork.rs index 51c82673d..4a9c07314 100644 --- a/crates/lib/src/repositories/fork.rs +++ b/crates/lib/src/repositories/fork.rs @@ -102,7 +102,7 @@ pub fn start_fork( } pub fn get_fork_status(repo_path: &Path) -> Result { - let status = read_status(repo_path)?.ok_or_else(OxenError::fork_status_not_found)?; + let status = read_status(repo_path)?.ok_or_else(|| OxenError::ForkStatusNotFound)?; Ok(ForkStatusResponse { repository: repo_path.to_string_lossy().to_string(), @@ -222,7 +222,7 @@ mod tests { current_status = match get_fork_status(&forked_repo_path) { Ok(status) => status.status, Err(e) => { - if let OxenError::ForkStatusNotFound(_) = e { + if matches!(e, OxenError::ForkStatusNotFound) { "in_progress".to_string() } else { return Err(e); diff --git a/crates/lib/src/repositories/tree.rs b/crates/lib/src/repositories/tree.rs index d62a0ce01..6657b7b21 100644 --- a/crates/lib/src/repositories/tree.rs +++ b/crates/lib/src/repositories/tree.rs @@ -550,7 +550,7 @@ pub async fn list_missing_file_hashes_from_commits( let commit_id_str = commit_id.to_string(); let Some(commit) = repositories::commits::get_by_id(repo, &commit_id_str)? else { log::error!("list_missing_file_hashes_from_commits Commit {commit_id_str} not found"); - return Err(OxenError::revision_not_found(commit_id_str.into())); + return Err(OxenError::RevisionNotFound(commit_id_str.into())); }; // Handle the case where we are given a list of subtrees to check // It is much faster to check the subtree directly than to walk the entire tree diff --git a/crates/lib/src/repositories/workspaces.rs b/crates/lib/src/repositories/workspaces.rs index f22c9f73e..47470fe2d 100644 --- a/crates/lib/src/repositories/workspaces.rs +++ b/crates/lib/src/repositories/workspaces.rs @@ -307,7 +307,7 @@ pub fn delete(workspace: &Workspace) -> Result<(), OxenError> { let workspace_id = workspace.id.to_string(); let workspace_dir = workspace.dir(); if !workspace_dir.exists() { - return Err(OxenError::workspace_not_found(workspace_id.into())); + return Err(OxenError::WorkspaceNotFound(workspace_id.into())); } log::debug!("workspace::delete cleaning up workspace dir: {workspace_dir:?}"); @@ -338,7 +338,7 @@ pub fn update_commit(workspace: &Workspace, new_commit_id: &str) -> Result<(), O if !config_path.exists() { log::error!("Workspace config not found: {config_path:?}"); - return Err(OxenError::workspace_not_found(workspace.id.clone().into())); + return Err(OxenError::WorkspaceNotFound(workspace.id.as_str().into())); } let config_contents = util::fs::read_from_path(&config_path)?; diff --git a/crates/lib/src/repositories/workspaces/data_frames/embeddings.rs b/crates/lib/src/repositories/workspaces/data_frames/embeddings.rs index f1b7ab23d..bbf84c918 100644 --- a/crates/lib/src/repositories/workspaces/data_frames/embeddings.rs +++ b/crates/lib/src/repositories/workspaces/data_frames/embeddings.rs @@ -563,7 +563,7 @@ fn get_avg_embedding(result_set: Vec) -> Result, OxenError } if embeddings.is_empty() { - return Err(OxenError::NoRowsFound("Query returned no rows".into())); + return Err(OxenError::NoRowsFound); } if vector_length == 0 { diff --git a/crates/lib/src/util/fs.rs b/crates/lib/src/util/fs.rs index 27faefba5..502707059 100644 --- a/crates/lib/src/util/fs.rs +++ b/crates/lib/src/util/fs.rs @@ -1719,10 +1719,7 @@ pub async fn handle_video_thumbnail( #[cfg(not(feature = "ffmpeg"))] { let _ = (version_store, file_hash, video_thumbnail); - Err(OxenError::thumbnailing_not_enabled( - "Video thumbnail generation requires the 'ffmpeg' feature to be enabled. \ - Build with --features liboxen/ffmpeg to enable this functionality.", - )) + Err(OxenError::ThumbnailingNotEnabled) } #[cfg(feature = "ffmpeg")] diff --git a/crates/oxen-py/src/py_remote_data_frame.rs b/crates/oxen-py/src/py_remote_data_frame.rs index 08b6f339f..aed90dc6d 100644 --- a/crates/oxen-py/src/py_remote_data_frame.rs +++ b/crates/oxen-py/src/py_remote_data_frame.rs @@ -26,7 +26,7 @@ impl PyRemoteDataFrame { fn size(&self) -> Result<(usize, usize), PyOxenError> { let Some(revision) = &self.repo.revision else { - return Err(OxenError::no_commits_found().into()); + return Err(OxenError::NoCommitsFound.into()); }; pyo3_async_runtimes::tokio::get_runtime().block_on(async { @@ -50,7 +50,7 @@ impl PyRemoteDataFrame { fn get_row_by_index(&self, row: usize) -> Result { let Some(revision) = &self.repo.revision else { - return Err(OxenError::no_commits_found().into()); + return Err(OxenError::NoCommitsFound.into()); }; let data = pyo3_async_runtimes::tokio::get_runtime().block_on(async { @@ -78,7 +78,7 @@ impl PyRemoteDataFrame { columns: Vec, ) -> Result { let Some(revision) = &self.repo.revision else { - return Err(OxenError::no_commits_found().into()); + return Err(OxenError::NoCommitsFound.into()); }; let data = pyo3_async_runtimes::tokio::get_runtime().block_on(async { diff --git a/crates/server/src/controllers/commits.rs b/crates/server/src/controllers/commits.rs index 3222302dc..979d1b440 100644 --- a/crates/server/src/controllers/commits.rs +++ b/crates/server/src/controllers/commits.rs @@ -306,7 +306,7 @@ pub async fn list_missing_files( }; let head_commit = repositories::commits::get_by_id(&repo, &query.head)? - .ok_or(OxenError::revision_not_found(query.head.clone().into()))?; + .ok_or(OxenError::RevisionNotFound(query.head.as_str().into()))?; let missing_files = repositories::entries::list_missing_files_in_commit_range( &repo, @@ -399,7 +399,7 @@ pub async fn show(req: HttpRequest) -> actix_web::Result actix_web::Result { - log::error!("Err create_commit: RootCommitDoesNotMatch {commit_id}"); - Err(OxenHttpError::BadRequest("Remote commit history does not match local commit history. Make sure you are pushing to the correct remote.".into())) - } Err(err) => { log::error!("Err create_commit: {err}"); Err(OxenHttpError::InternalServerError) diff --git a/crates/server/src/controllers/diff.rs b/crates/server/src/controllers/diff.rs index 74180081a..63a23fd7f 100644 --- a/crates/server/src/controllers/diff.rs +++ b/crates/server/src/controllers/diff.rs @@ -73,8 +73,8 @@ pub async fn commits( let (base, head) = parse_base_head(&base_head)?; let (base_commit, head_commit) = resolve_base_head(&repository, &base, &head)?; - let base_commit = base_commit.ok_or(OxenError::revision_not_found(base.into()))?; - let head_commit = head_commit.ok_or(OxenError::revision_not_found(head.into()))?; + let base_commit = base_commit.ok_or_else(|| OxenError::RevisionNotFound(base.into()))?; + let head_commit = head_commit.ok_or_else(|| OxenError::RevisionNotFound(head.into()))?; let commits = repositories::commits::list_between(&repository, &base_commit, &head_commit)?; let (paginated, pagination) = util::paginate(commits, page, page_size); @@ -132,8 +132,8 @@ pub async fn entries( let (base, head) = parse_base_head(&base_head)?; let (base_commit, head_commit) = resolve_base_head(&repository, &base, &head)?; - let base_commit = base_commit.ok_or(OxenError::revision_not_found(base.into()))?; - let head_commit = head_commit.ok_or(OxenError::revision_not_found(head.into()))?; + let base_commit = base_commit.ok_or_else(|| OxenError::RevisionNotFound(base.into()))?; + let head_commit = head_commit.ok_or_else(|| OxenError::RevisionNotFound(head.into()))?; let entries_diff = repositories::diffs::list_diff_entries( &repository, @@ -207,8 +207,8 @@ pub async fn dir_tree(req: HttpRequest) -> actix_web::Result { - log::debug!("Repo already exists: {path:?}"); - Ok(HttpResponse::Conflict() - .json(StatusMessage::error("Repo already exists at destination."))) - } Err(err) => { log::error!("Failed to fork repository: {err:?}"); Err(OxenHttpError::from(err)) @@ -90,7 +85,7 @@ pub async fn get_status(req: HttpRequest) -> Result match repositories::fork::get_fork_status(&repo_path) { Ok(status) => Ok(HttpResponse::Ok().json(status)), - Err(OxenError::ForkStatusNotFound(_)) => { + Err(OxenError::ForkStatusNotFound) => { Ok(HttpResponse::NotFound().json(StatusMessage::error("Fork status not found"))) } Err(e) => { diff --git a/crates/server/src/controllers/merger.rs b/crates/server/src/controllers/merger.rs index 9f35bc628..dcacdb65e 100644 --- a/crates/server/src/controllers/merger.rs +++ b/crates/server/src/controllers/merger.rs @@ -39,8 +39,8 @@ pub async fn show(req: HttpRequest) -> actix_web::Result actix_web::Result actix_web::Result { log::debug!("Merge has conflicts"); - Err(OxenError::merge_conflict(format!( - "Unable to merge {head} into {base} due to conflicts" - )))? + Err(OxenError::UpstreamMergeConflict( + format!("Unable to merge {head} into {base} due to conflicts.").into(), + ))? } Err(err) => { log::debug!("Err merging branches {err:?}"); diff --git a/crates/server/src/controllers/metadata.rs b/crates/server/src/controllers/metadata.rs index c42ba5e90..902eae456 100644 --- a/crates/server/src/controllers/metadata.rs +++ b/crates/server/src/controllers/metadata.rs @@ -51,7 +51,7 @@ pub async fn file(req: HttpRequest) -> actix_web::Result '{}'", diff --git a/crates/server/src/controllers/repositories.rs b/crates/server/src/controllers/repositories.rs index 2358eacb8..1420a8798 100644 --- a/crates/server/src/controllers/repositories.rs +++ b/crates/server/src/controllers/repositories.rs @@ -307,7 +307,7 @@ async fn handle_json_creation( }, metadata_entries: None, })), - Err(OxenError::NoCommitsFound(_)) => { + Err(OxenError::NoCommitsFound) => { Ok(HttpResponse::Ok().json(RepositoryCreationResponse { status: STATUS_SUCCESS.to_string(), status_message: MSG_RESOURCE_FOUND.to_string(), @@ -453,7 +453,7 @@ async fn handle_multipart_creation( }, metadata_entries: repo.entries, })), - Err(OxenError::NoCommitsFound(_)) => { + Err(OxenError::NoCommitsFound) => { Ok(HttpResponse::Ok().json(RepositoryCreationResponse { status: STATUS_SUCCESS.to_string(), status_message: MSG_RESOURCE_FOUND.to_string(), diff --git a/crates/server/src/controllers/schemas.rs b/crates/server/src/controllers/schemas.rs index eb0a49aff..12bac687d 100644 --- a/crates/server/src/controllers/schemas.rs +++ b/crates/server/src/controllers/schemas.rs @@ -59,7 +59,7 @@ pub async fn list_or_get(req: HttpRequest) -> actix_web::Result { - log::error!("Remote ahead of local: {desc}"); - HttpResponse::BadRequest() - .json(StatusMessageDescription::bad_request(format!("{desc}"))) - } - OxenError::IncompleteLocalHistory(desc) => { - log::error!("Cannot push repo with incomplete local history: {desc}"); - - HttpResponse::BadRequest() - .json(StatusMessageDescription::bad_request(format!("{desc}"))) - } OxenError::IncompatibleSchemas(schema) => { log::error!("Incompatible schemas: {schema}"); @@ -505,13 +494,13 @@ impl error::ResponseError for OxenHttpError { }); HttpResponse::InternalServerError().json(error_json) } - OxenError::ThumbnailingNotEnabled(error) => { - log::error!("Thumbnailing not enabled: {error}"); + thumbnail_error @ OxenError::ThumbnailingNotEnabled => { + log::error!("Thumbnailing not enabled: {thumbnail_error}"); let error_json = json!({ "error": { "type": "thumbnailing_not_enabled", "title": "Thumbnailing Not Enabled", - "detail": format!("{}", error), + "detail": format!("{error}"), }, "status": STATUS_ERROR, "status_message": MSG_INTERNAL_SERVER_ERROR, @@ -529,13 +518,13 @@ impl error::ResponseError for OxenHttpError { }); HttpResponse::InternalServerError().json(error_json) } - OxenError::NoRowsFound(msg) => { - log::error!("No rows found: {msg}"); + e @ OxenError::NoRowsFound => { + log::error!("No rows found: {e}"); let error_json = json!({ "error": { "type": "no_rows_found", "title": "No rows found", - "detail": format!("{}", msg), + "detail": format!("{e}"), }, "status": STATUS_ERROR, "status_message": MSG_INTERNAL_SERVER_ERROR,