feat(core): Implement infrastructure layer for CLI operations
Establishes foundational modules that all other components depend on. src/core/config.rs - Configuration management: - JSON-based config file with Zod-like validation via serde - GitLab settings: base URL, token environment variable - Project list with paths to track - Sync settings: backfill days, stale lock timeout, cursor rewind - Storage settings: database path, payload compression toggle - XDG-compliant config path resolution via dirs crate - Loads GITLAB_TOKEN from configured environment variable src/core/db.rs - Database connection and migrations: - Opens or creates SQLite database with WAL mode for concurrency - Embeds migration SQL as const strings (001-005) - Runs migrations idempotently with checksum verification - Provides thread-safe connection management src/core/error.rs - Unified error handling: - GiError enum with variants for all failure modes - Config, Database, GitLab, Ingestion, Lock, IO, Parse errors - thiserror derive for automatic Display/Error impls - Result type alias for ergonomic error propagation src/core/lock.rs - Distributed sync locking: - File-based locks to prevent concurrent syncs - Stale lock detection with configurable timeout - Force override for recovery scenarios - Lock file contains PID and timestamp for debugging src/core/paths.rs - Path resolution: - XDG Base Directory Specification compliance - Config: ~/.config/gi/config.json - Data: ~/.local/share/gi/gi.db - Creates parent directories on first access src/core/payloads.rs - Raw payload storage: - Optional gzip compression for storage efficiency - SHA-256 content addressing for deduplication - Type-prefixed keys (issue:, discussion:, note:) - Batch insert with UPSERT for idempotent ingestion src/core/time.rs - Timestamp utilities: - Relative time parsing (7d, 2w, 1m) for --since flag - ISO 8601 date parsing for absolute dates - Human-friendly relative time formatting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
137
src/core/error.rs
Normal file
137
src/core/error.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
//! Custom error types for gitlab-inbox.
|
||||
//!
|
||||
//! Uses thiserror for ergonomic error definitions with structured error codes.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Error codes for programmatic error handling.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ErrorCode {
|
||||
ConfigNotFound,
|
||||
ConfigInvalid,
|
||||
GitLabAuthFailed,
|
||||
GitLabNotFound,
|
||||
GitLabRateLimited,
|
||||
GitLabNetworkError,
|
||||
DatabaseLocked,
|
||||
DatabaseError,
|
||||
MigrationFailed,
|
||||
TokenNotSet,
|
||||
TransformError,
|
||||
IoError,
|
||||
InternalError,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ErrorCode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let code = match self {
|
||||
Self::ConfigNotFound => "CONFIG_NOT_FOUND",
|
||||
Self::ConfigInvalid => "CONFIG_INVALID",
|
||||
Self::GitLabAuthFailed => "GITLAB_AUTH_FAILED",
|
||||
Self::GitLabNotFound => "GITLAB_NOT_FOUND",
|
||||
Self::GitLabRateLimited => "GITLAB_RATE_LIMITED",
|
||||
Self::GitLabNetworkError => "GITLAB_NETWORK_ERROR",
|
||||
Self::DatabaseLocked => "DB_LOCKED",
|
||||
Self::DatabaseError => "DB_ERROR",
|
||||
Self::MigrationFailed => "MIGRATION_FAILED",
|
||||
Self::TokenNotSet => "TOKEN_NOT_SET",
|
||||
Self::TransformError => "TRANSFORM_ERROR",
|
||||
Self::IoError => "IO_ERROR",
|
||||
Self::InternalError => "INTERNAL_ERROR",
|
||||
};
|
||||
write!(f, "{code}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Main error type for gitlab-inbox.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum GiError {
|
||||
#[error("Config file not found at {path}. Run \"gi init\" first.")]
|
||||
ConfigNotFound { path: String },
|
||||
|
||||
#[error("Invalid config: {details}")]
|
||||
ConfigInvalid { details: String },
|
||||
|
||||
#[error("GitLab authentication failed. Check your token has read_api scope.")]
|
||||
GitLabAuthFailed,
|
||||
|
||||
#[error("GitLab resource not found: {resource}")]
|
||||
GitLabNotFound { resource: String },
|
||||
|
||||
#[error("Rate limited. Retry after {retry_after}s")]
|
||||
GitLabRateLimited { retry_after: u64 },
|
||||
|
||||
#[error("Cannot connect to GitLab at {base_url}")]
|
||||
GitLabNetworkError {
|
||||
base_url: String,
|
||||
#[source]
|
||||
source: Option<reqwest::Error>,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"Another sync is running (owner: {owner}, started: {started_at}). Use --force to override if stale."
|
||||
)]
|
||||
DatabaseLocked { owner: String, started_at: String },
|
||||
|
||||
#[error("Migration {version} failed: {message}")]
|
||||
MigrationFailed {
|
||||
version: i32,
|
||||
message: String,
|
||||
#[source]
|
||||
source: Option<rusqlite::Error>,
|
||||
},
|
||||
|
||||
#[error("GitLab token not set. Export {env_var} environment variable.")]
|
||||
TokenNotSet { env_var: String },
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] rusqlite::Error),
|
||||
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Transform error: {0}")]
|
||||
Transform(#[from] crate::gitlab::transformers::issue::TransformError),
|
||||
|
||||
#[error("Not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Ambiguous: {0}")]
|
||||
Ambiguous(String),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl GiError {
|
||||
/// Get the error code for programmatic handling.
|
||||
pub fn code(&self) -> ErrorCode {
|
||||
match self {
|
||||
Self::ConfigNotFound { .. } => ErrorCode::ConfigNotFound,
|
||||
Self::ConfigInvalid { .. } => ErrorCode::ConfigInvalid,
|
||||
Self::GitLabAuthFailed => ErrorCode::GitLabAuthFailed,
|
||||
Self::GitLabNotFound { .. } => ErrorCode::GitLabNotFound,
|
||||
Self::GitLabRateLimited { .. } => ErrorCode::GitLabRateLimited,
|
||||
Self::GitLabNetworkError { .. } => ErrorCode::GitLabNetworkError,
|
||||
Self::DatabaseLocked { .. } => ErrorCode::DatabaseLocked,
|
||||
Self::MigrationFailed { .. } => ErrorCode::MigrationFailed,
|
||||
Self::TokenNotSet { .. } => ErrorCode::TokenNotSet,
|
||||
Self::Database(_) => ErrorCode::DatabaseError,
|
||||
Self::Http(_) => ErrorCode::GitLabNetworkError,
|
||||
Self::Json(_) => ErrorCode::InternalError,
|
||||
Self::Io(_) => ErrorCode::IoError,
|
||||
Self::Transform(_) => ErrorCode::TransformError,
|
||||
Self::NotFound(_) => ErrorCode::GitLabNotFound,
|
||||
Self::Ambiguous(_) => ErrorCode::InternalError,
|
||||
Self::Other(_) => ErrorCode::InternalError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, GiError>;
|
||||
Reference in New Issue
Block a user