Prune Code to only what's used by the UI

Also adjust the `Code` documentation to clarify this - otherwise
we will have more and more variants and nobody actually cares.

The frontend code is adjusted as well, as its `Code` counterpart
contained unsused variants which are now removed.
This commit is contained in:
Sebastian Thiel 2024-05-30 10:44:42 +02:00
parent d689f36e7f
commit 20d84247e9
No known key found for this signature in database
GPG Key ID: 9CB5EE7895E8268B
15 changed files with 185 additions and 551 deletions

View File

@ -5,11 +5,7 @@ import type { EventCallback, EventName } from '@tauri-apps/api/event';
export enum Code {
Unknown = 'errors.unknown',
Validation = 'errors.validation',
Projects = 'errors.projects',
ProjectsGitAuth = 'errors.projects.git.auth',
ProjectsGitRemote = 'errors.projects.git.remote',
ProjectHead = 'errors.projects.head',
ProjectConflict = 'errors.projects.conflict'
ProjectsGitAuth = 'errors.projects.git.auth'
}
export class UserError extends Error {

View File

@ -123,49 +123,44 @@ use std::borrow::Cow;
use std::fmt::{self, Debug, Display};
/// A unique code that consumers of the API may rely on to identify errors.
///
/// ### Important
///
/// **Only add variants if a consumer, like the *frontend*, is actually using them**.
/// Remove variants when no longer in use.
///
/// In practice, it should match its [frontend counterpart](https://github.com/gitbutlerapp/gitbutler/blob/fa973fd8f1ae8807621f47601803d98b8a9cf348/app/src/lib/backend/ipc.ts#L5).
#[derive(Debug, Default, Copy, Clone, PartialOrd, PartialEq)]
pub enum Code {
/// Much like a catch-all error code. It shouldn't be attached explicitly unless
/// a message is provided as well as part of a [`Context`].
#[default]
Unknown,
Validation,
Projects,
Branches,
ProjectGitAuth,
ProjectGitRemote,
/// The push operation failed, specifically because the remote rejected it.
ProjectGitPush,
// TODO(ST): try to remove this and replace it with downcasting or thiserror pattern matching
ProjectConflict,
ProjectHead,
Menu,
PreCommitHook,
CommitMsgHook,
}
impl std::fmt::Display for Code {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let code = match self {
Code::Menu => "errors.menu",
Code::Unknown => "errors.unknown",
Code::Validation => "errors.validation",
Code::Projects => "errors.projects",
Code::Branches => "errors.branches",
Code::ProjectGitAuth => "errors.projects.git.auth",
Code::ProjectGitRemote => "errors.projects.git.remote",
Code::ProjectGitPush => "errors.projects.git.push",
Code::ProjectHead => "errors.projects.head",
Code::ProjectConflict => "errors.projects.conflict",
//TODO: rename js side to be more precise what kind of hook error this is
Code::PreCommitHook => "errors.hook",
Code::CommitMsgHook => "errors.hooks.commit.msg",
};
f.write_str(code)
}
}
/// A context to wrap around lower errors to allow its classification, along with a message for the user.
/// A context for classifying errors.
///
/// It provides a [`Code`], which may be [unknown](Code::Unknown), and a `message` which explains
/// more about the problem at hand.
#[derive(Default, Debug, Clone)]
pub struct Context {
/// The identifier of the error.
/// The classification of the error.
pub code: Code,
/// A description of what went wrong, if available.
pub message: Option<Cow<'static, str>>,
@ -188,9 +183,9 @@ impl From<Code> for Context {
impl Context {
/// Create a new instance with `code` and an owned `message`.
pub fn new(code: Code, message: impl Into<String>) -> Self {
pub fn new(message: impl Into<String>) -> Self {
Context {
code,
code: Code::Unknown,
message: Some(Cow::Owned(message.into())),
}
}
@ -202,6 +197,12 @@ impl Context {
message: Some(Cow::Borrowed(message)),
}
}
/// Adjust the `code` of this instance to the given one.
pub fn with_code(mut self, code: Code) -> Self {
self.code = code;
self
}
}
mod private {

View File

@ -1,7 +1,6 @@
use std::path::PathBuf;
use crate::error::{AnyhowContextExt, Code, Context, ErrorWithContext};
use crate::{error, keys, project_repository, projects, users};
use crate::{keys, project_repository, projects, users};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SshCredential {
@ -87,19 +86,6 @@ pub enum HelpError {
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for HelpError {
fn context(&self) -> Option<Context> {
Some(match self {
HelpError::NoUrlSet => {
error::Context::new_static(Code::ProjectGitRemote, "no url set for remote")
}
HelpError::UrlConvertError(_) => Code::ProjectGitRemote.into(),
HelpError::Git(_) => return None,
HelpError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}
impl Helper {
pub fn new(
keys: keys::Controller,

View File

@ -3,6 +3,6 @@ pub mod conflicts;
mod repository;
pub use config::Config;
pub use repository::{LogUntil, OpenError, RemoteError, Repository};
pub use repository::{LogUntil, RemoteError, Repository};
pub mod signatures;

View File

@ -4,7 +4,7 @@ use std::{
sync::{atomic::AtomicUsize, Arc},
};
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use super::conflicts;
use crate::{
@ -24,82 +24,63 @@ pub struct Repository {
project: projects::Project,
}
#[derive(Debug, thiserror::Error)]
pub enum OpenError {
#[error("repository not found at {0}")]
NotFound(path::PathBuf),
#[error(transparent)]
Other(anyhow::Error),
}
impl ErrorWithContext for OpenError {
fn context(&self) -> Option<crate::error::Context> {
match self {
OpenError::NotFound(path) => {
error::Context::new(Code::Projects, format!("{} not found", path.display())).into()
}
OpenError::Other(error) => error.custom_context_or_root_cause().into(),
}
}
}
impl Repository {
pub fn open(project: &projects::Project) -> Result<Self, OpenError> {
git::Repository::open(&project.path)
.map_err(|error| match error {
git::Error::NotFound(_) => OpenError::NotFound(project.path.clone()),
other => OpenError::Other(other.into()),
})
.map(|git_repository| {
// XXX(qix-): This is a temporary measure to disable GC on the project repository.
// XXX(qix-): We do this because the internal repository we use to store the "virtual"
// XXX(qix-): refs and information use Git's alternative-objects mechanism to refer
// XXX(qix-): to the project repository's objects. However, the project repository
// XXX(qix-): has no knowledge of these refs, and will GC them away (usually after
// XXX(qix-): about 2 weeks) which will corrupt the internal repository.
// XXX(qix-):
// XXX(qix-): We will ultimately move away from an internal repository for a variety
// XXX(qix-): of reasons, but for now, this is a simple, short-term solution that we
// XXX(qix-): can clean up later on. We're aware this isn't ideal.
if let Ok(config) = git_repository.config().as_mut() {
let should_set = match config.get_bool("gitbutler.didSetPrune") {
Ok(None | Some(false)) => true,
Ok(Some(true)) => false,
Err(error) => {
tracing::warn!(
pub fn open(project: &projects::Project) -> Result<Self> {
let repo = git::Repository::open(&project.path).or_else(|err| match err {
git::Error::NotFound(_) => Err(anyhow::Error::from(err)).context(format!(
"repository not found at \"{}\"",
project.path.display()
)),
other => Err(other.into()),
})?;
// XXX(qix-): This is a temporary measure to disable GC on the project repository.
// XXX(qix-): We do this because the internal repository we use to store the "virtual"
// XXX(qix-): refs and information use Git's alternative-objects mechanism to refer
// XXX(qix-): to the project repository's objects. However, the project repository
// XXX(qix-): has no knowledge of these refs, and will GC them away (usually after
// XXX(qix-): about 2 weeks) which will corrupt the internal repository.
// XXX(qix-):
// XXX(qix-): We will ultimately move away from an internal repository for a variety
// XXX(qix-): of reasons, but for now, this is a simple, short-term solution that we
// XXX(qix-): can clean up later on. We're aware this isn't ideal.
if let Ok(config) = repo.config().as_mut() {
let should_set = match config.get_bool("gitbutler.didSetPrune") {
Ok(None | Some(false)) => true,
Ok(Some(true)) => false,
Err(err) => {
tracing::warn!(
"failed to get gitbutler.didSetPrune for repository at {}; cannot disable gc: {}",
project.path.display(),
error
err
);
false
}
};
false
}
};
if should_set {
if let Err(error) = config
.set_str("gc.pruneExpire", "never")
.and_then(|()| config.set_bool("gitbutler.didSetPrune", true))
{
tracing::warn!(
if should_set {
if let Err(error) = config
.set_str("gc.pruneExpire", "never")
.and_then(|()| config.set_bool("gitbutler.didSetPrune", true))
{
tracing::warn!(
"failed to set gc.auto to false for repository at {}; cannot disable gc: {}",
project.path.display(),
error
);
}
}
} else {
tracing::warn!(
"failed to get config for repository at {}; cannot disable gc",
project.path.display()
);
}
}
} else {
tracing::warn!(
"failed to get config for repository at {}; cannot disable gc",
project.path.display()
);
}
git_repository
})
.map(|git_repository| Self {
git_repository,
project: project.clone(),
})
Ok(Self {
git_repository: repo,
project: project.clone(),
})
}
pub fn is_resolving(&self) -> bool {
@ -517,9 +498,7 @@ impl Repository {
}
Err(err) => {
if let Some(err) = update_refs_error.as_ref() {
return Err(RemoteError::Other(
anyhow::anyhow!(err.to_string()).context(Code::ProjectGitPush),
));
return Err(RemoteError::Other(anyhow!(err.to_string())));
}
return Err(RemoteError::Other(err.into()));
}
@ -630,17 +609,13 @@ pub enum RemoteError {
impl ErrorWithContext for RemoteError {
fn context(&self) -> Option<error::Context> {
Some(match self {
RemoteError::Help(error) => return error.context(),
RemoteError::Network => {
error::Context::new_static(Code::ProjectGitRemote, "Network error occurred")
RemoteError::Help(_) | RemoteError::Network | RemoteError::Git(_) => {
error::Context::default()
}
RemoteError::Auth => error::Context::new_static(
Code::ProjectGitAuth,
"Project remote authentication error",
),
RemoteError::Git(_) => {
error::Context::new_static(Code::ProjectGitRemote, "Git command failed")
}
RemoteError::Other(error) => {
return error.custom_context_or_root_cause().into();
}

View File

@ -7,11 +7,8 @@ use anyhow::{Context, Result};
use async_trait::async_trait;
use super::{storage, storage::UpdateRequest, Project, ProjectId};
use crate::{error, project_repository};
use crate::{
error::{AnyhowContextExt, Code, Error, ErrorWithContext},
projects::AuthKey,
};
use crate::project_repository;
use crate::{error::Error, projects::AuthKey};
#[async_trait]
pub trait Watchers {
@ -160,40 +157,28 @@ impl Controller {
Ok(updated)
}
pub fn get(&self, id: ProjectId) -> Result<Project, GetError> {
let project = self.projects_storage.get(id).map_err(|error| match error {
super::storage::Error::NotFound => GetError::NotFound,
error => GetError::Other(error.into()),
});
if let Ok(project) = &project {
if !project.gb_dir().exists() {
if let Err(error) = std::fs::create_dir_all(project.gb_dir()) {
tracing::error!(project_id = %project.id, ?error, "failed to create {:?} on project get", project.gb_dir());
}
pub fn get(&self, id: ProjectId) -> Result<Project> {
let project = self.projects_storage.get(id)?;
if !project.gb_dir().exists() {
if let Err(error) = std::fs::create_dir_all(project.gb_dir()) {
tracing::error!(project_id = %project.id, ?error, "failed to create \"{}\" on project get", project.gb_dir().display());
}
// Clean up old virtual_branches.toml that was never used
if project
.path
.join(".git")
.join("virtual_branches.toml")
.exists()
{
if let Err(error) =
std::fs::remove_file(project.path.join(".git").join("virtual_branches.toml"))
{
tracing::error!(project_id = %project.id, ?error, "failed to remove old virtual_branches.toml");
}
}
// Clean up old virtual_branches.toml that was never used
let old_virtual_branches_path = project.path.join(".git").join("virtual_branches.toml");
if old_virtual_branches_path.exists() {
if let Err(error) = std::fs::remove_file(old_virtual_branches_path) {
tracing::error!(project_id = %project.id, ?error, "failed to remove old virtual_branches.toml");
}
}
// FIXME(qix-): On windows, we have to force to system executable
#[cfg(windows)]
let project = project.map(|mut p| {
p.preferred_key = AuthKey::SystemExecutable;
p
});
{
project.preferred_key = AuthKey::SystemExecutable;
}
project
Ok(project)
}
pub fn list(&self) -> Result<Vec<Project>> {
@ -246,8 +231,7 @@ impl Controller {
error => ConfigError::Other(error.into()),
})?;
let repo = project_repository::Repository::open(&project)
.map_err(|e| ConfigError::Other(e.into()))?;
let repo = project_repository::Repository::open(&project).map_err(ConfigError::Other)?;
repo.config()
.get_local(key)
.map_err(|e| ConfigError::Other(e.into()))
@ -264,8 +248,7 @@ impl Controller {
error => ConfigError::Other(error.into()),
})?;
let repo = project_repository::Repository::open(&project)
.map_err(|e| ConfigError::Other(e.into()))?;
let repo = project_repository::Repository::open(&project).map_err(ConfigError::Other)?;
repo.config()
.set_local(key, value)
.map_err(|e| ConfigError::Other(e.into()))?;
@ -290,17 +273,6 @@ pub enum GetError {
Other(#[from] anyhow::Error),
}
impl error::ErrorWithContext for GetError {
fn context(&self) -> Option<error::Context> {
match self {
GetError::NotFound => {
error::Context::new_static(Code::Projects, "Project not found").into()
}
GetError::Other(error) => error.custom_context_or_root_cause().into(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum UpdateError {
#[error("project not found")]
@ -311,26 +283,6 @@ pub enum UpdateError {
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for UpdateError {
fn context(&self) -> Option<error::Context> {
Some(match self {
UpdateError::Validation(UpdateValidationError::KeyNotFound(path)) => {
error::Context::new(Code::Projects, format!("'{}' not found", path.display()))
}
UpdateError::Validation(UpdateValidationError::KeyNotFile(path)) => {
error::Context::new(
Code::Projects,
format!("'{}' is not a file", path.display()),
)
}
UpdateError::NotFound => {
error::Context::new_static(Code::Projects, "Project not found")
}
UpdateError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum UpdateValidationError {
#[error("{0} not found")]
@ -343,47 +295,18 @@ pub enum UpdateValidationError {
pub enum AddError {
#[error("not a directory")]
NotADirectory,
#[error("not a git repository")]
#[error("must be a Git repository")]
NotAGitRepository(#[from] Box<gix::open::Error>),
#[error("bare repositories are not supported")]
#[error("bare repositories are unsupported")]
BareUnsupported,
#[error("worktrees unsupported")]
#[error("can only work in main worktrees")]
WorktreeNotSupported,
#[error("path not found")]
PathNotFound,
#[error("project already exists")]
AlreadyExists,
#[error("submodules not supported")]
#[error("repositories with git submodules are not supported")]
SubmodulesNotSupported,
#[error(transparent)]
OpenProjectRepository(#[from] project_repository::OpenError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for AddError {
fn context(&self) -> Option<error::Context> {
Some(match self {
AddError::NotAGitRepository(_) => {
error::Context::new_static(Code::Projects, "Must be a git directory")
}
AddError::BareUnsupported => {
error::Context::new_static(Code::Projects, "Bare repositories are unsupported")
}
AddError::AlreadyExists => {
error::Context::new_static(Code::Projects, "Project already exists")
}
AddError::OpenProjectRepository(error) => return error.context(),
AddError::NotADirectory => error::Context::new(Code::Projects, "Not a directory"),
AddError::WorktreeNotSupported => {
error::Context::new(Code::Projects, "Can only work in main worktrees")
}
AddError::PathNotFound => error::Context::new(Code::Projects, "Path not found"),
AddError::SubmodulesNotSupported => error::Context::new_static(
Code::Projects,
"Repositories with git submodules are not supported",
),
AddError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}

View File

@ -455,7 +455,7 @@ impl ControllerInner {
user,
run_hooks,
)
.map_err(Into::into);
.map_err(Error::from_err);
let _ = snapshot_tree.and_then(|snapshot_tree| {
project_repository.project().snapshot_commit_creation(
snapshot_tree,
@ -475,10 +475,7 @@ impl ControllerInner {
) -> Result<bool, Error> {
let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?;
Ok(super::is_remote_branch_mergeable(
&project_repository,
branch_name,
)?)
super::is_remote_branch_mergeable(&project_repository, branch_name).map_err(Error::from_err)
}
pub fn can_apply_virtual_branch(
@ -523,9 +520,8 @@ impl ControllerInner {
let _permit = self.semaphore.acquire().await;
self.with_verify_branch(project_id, |project_repository, user| {
let result =
super::create_virtual_branch_from_branch(project_repository, branch, user)?;
Ok(result)
super::create_virtual_branch_from_branch(project_repository, branch, user)
.map_err(Error::from_err)
})
}
@ -543,7 +539,7 @@ impl ControllerInner {
let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?;
super::list_remote_commit_files(&project_repository.git_repository, commit_oid)
.map_err(Into::into)
.map_err(Error::from_err)
}
pub fn set_base_branch(
@ -556,8 +552,7 @@ impl ControllerInner {
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::SetBaseBranch));
let result = super::set_base_branch(&project_repository, target_branch)?;
Ok(result)
super::set_base_branch(&project_repository, target_branch).map_err(Error::from_err)
}
pub fn set_target_push_remote(
@ -567,8 +562,7 @@ impl ControllerInner {
) -> Result<(), Error> {
let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?;
super::set_target_push_remote(&project_repository, push_remote)?;
Ok(())
super::set_target_push_remote(&project_repository, push_remote).map_err(Error::from_err)
}
pub async fn integrate_upstream_commits(
@ -633,7 +627,7 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, user| {
let snapshot_tree = project_repository.project().prepare_snapshot();
let result =
super::apply_branch(project_repository, branch_id, user).map_err(Into::into);
super::apply_branch(project_repository, branch_id, user).map_err(Error::from_err);
let _ = snapshot_tree.and_then(|snapshot_tree| {
project_repository
@ -687,7 +681,8 @@ impl ControllerInner {
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::AmendCommit));
super::amend(project_repository, branch_id, commit_oid, ownership).map_err(Into::into)
super::amend(project_repository, branch_id, commit_oid, ownership)
.map_err(Error::from_err)
})
}
@ -712,7 +707,7 @@ impl ControllerInner {
to_commit_oid,
ownership,
)
.map_err(Into::into)
.map_err(Error::from_err)
})
}
@ -727,7 +722,7 @@ impl ControllerInner {
self.with_verify_branch(project_id, |project_repository, _| {
let snapshot_tree = project_repository.project().prepare_snapshot();
let result: Result<(), Error> =
super::undo_commit(project_repository, branch_id, commit_oid).map_err(Into::into);
super::undo_commit(project_repository, branch_id, commit_oid).map_err(Error::from_err);
let _ = snapshot_tree.and_then(|snapshot_tree| {
project_repository.project().snapshot_commit_undo(
snapshot_tree,
@ -753,7 +748,7 @@ impl ControllerInner {
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::InsertBlankCommit));
super::insert_blank_commit(project_repository, branch_id, commit_oid, user, offset)
.map_err(Into::into)
.map_err(Error::from_err)
})
}
@ -771,7 +766,7 @@ impl ControllerInner {
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::ReorderCommit));
super::reorder_commit(project_repository, branch_id, commit_oid, offset)
.map_err(Into::into)
.map_err(Error::from_err)
})
}
@ -788,7 +783,7 @@ impl ControllerInner {
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::UndoCommit));
super::reset_branch(project_repository, branch_id, target_commit_oid)
.map_err(Into::into)
.map_err(Error::from_err)
})
}
@ -845,7 +840,7 @@ impl ControllerInner {
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::CherryPick));
super::cherry_pick(project_repository, branch_id, commit_oid).map_err(Into::into)
super::cherry_pick(project_repository, branch_id, commit_oid).map_err(Error::from_err)
})
}
@ -880,7 +875,7 @@ impl ControllerInner {
let _ = project_repository
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::SquashCommit));
super::squash(project_repository, branch_id, commit_oid).map_err(Into::into)
super::squash(project_repository, branch_id, commit_oid).map_err(Error::from_err)
})
}
@ -897,7 +892,7 @@ impl ControllerInner {
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::UpdateCommitMessage));
super::update_commit_message(project_repository, branch_id, commit_oid, message)
.map_err(Into::into)
.map_err(Error::from_err)
})
}
@ -977,7 +972,7 @@ impl ControllerInner {
.project()
.create_snapshot(SnapshotDetails::new(OperationKind::MoveCommit));
super::move_commit(project_repository, target_branch_id, commit_oid, user)
.map_err(Into::into)
.map_err(Error::from_err)
})
}
}
@ -991,7 +986,7 @@ impl ControllerInner {
let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?;
let user = self.users.get_user()?;
super::integration::verify_branch(&project_repository)?;
super::integration::verify_branch(&project_repository).map_err(Error::from_err)?;
action(&project_repository, user.as_ref())
}
@ -1005,7 +1000,7 @@ impl ControllerInner {
let project = self.projects.get(project_id)?;
let project_repository = project_repository::Repository::open(&project)?;
let user = self.users.get_user()?;
super::integration::verify_branch(&project_repository)?;
super::integration::verify_branch(&project_repository).map_err(Error::from_err)?;
Ok(tokio::task::spawn_blocking(move || {
action(&project_repository, user.as_ref())
}))

View File

@ -25,49 +25,21 @@ pub enum VirtualBranchError {
RebaseFailed,
#[error("force push not allowed")]
ForcePushNotAllowed(ForcePushNotAllowed),
#[error("branch has no commits")]
#[error("Branch has no commits - there is nothing to amend to")]
BranchHasNoCommits,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for VirtualBranchError {
fn context(&self) -> Option<Context> {
Some(match self {
VirtualBranchError::Conflict(ctx) => ctx.to_context(),
VirtualBranchError::BranchNotFound(ctx) => ctx.to_context(),
VirtualBranchError::DefaultTargetNotSet(ctx) => ctx.to_context(),
VirtualBranchError::TargetOwnerhshipNotFound(_) => {
error::Context::new_static(Code::Branches, "target ownership not found")
}
VirtualBranchError::GitObjectNotFound(oid) => {
error::Context::new(Code::Branches, format!("git object {oid} not found"))
}
VirtualBranchError::CommitFailed => {
error::Context::new_static(Code::Branches, "commit failed")
}
VirtualBranchError::RebaseFailed => {
error::Context::new_static(Code::Branches, "rebase failed")
}
VirtualBranchError::BranchHasNoCommits => error::Context::new_static(
Code::Branches,
"Branch has no commits - there is nothing to amend to",
),
VirtualBranchError::ForcePushNotAllowed(ctx) => ctx.to_context(),
VirtualBranchError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum VerifyError {
#[error("head is detached")]
#[error("project in detached head state. Please checkout {} to continue", GITBUTLER_INTEGRATION_REFERENCE.branch())]
DetachedHead,
#[error("head is {0}")]
#[error("project is on {0}. Please checkout {} to continue", GITBUTLER_INTEGRATION_REFERENCE.branch())]
InvalidHead(String),
#[error("head not found")]
#[error("Repo HEAD is unavailable")]
HeadNotFound,
#[error("integration commit not found")]
#[error("GibButler's integration commit not found on head.")]
NoIntegrationCommit,
#[error(transparent)]
GitError(#[from] git::Error),
@ -75,39 +47,6 @@ pub enum VerifyError {
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for VerifyError {
fn context(&self) -> Option<Context> {
Some(match self {
VerifyError::DetachedHead => error::Context::new(
Code::ProjectHead,
format!(
"Project in detached head state. Please checkout {0} to continue.",
GITBUTLER_INTEGRATION_REFERENCE.branch()
),
),
VerifyError::InvalidHead(head) => error::Context::new(
Code::ProjectHead,
format!(
"Project is on {}. Please checkout {} to continue.",
head,
GITBUTLER_INTEGRATION_REFERENCE.branch()
),
),
VerifyError::NoIntegrationCommit => error::Context::new_static(
Code::ProjectHead,
"GibButler's integration commit not found on head.",
),
VerifyError::HeadNotFound => {
error::Context::new_static(Code::Validation, "Repo HEAD is unavailable")
}
VerifyError::GitError(error) => {
error::Context::new(Code::Validation, error.to_string())
}
VerifyError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum ResetBranchError {
#[error("commit {0} not in the branch")]
@ -122,27 +61,13 @@ pub enum ResetBranchError {
Git(#[from] git::Error),
}
impl ErrorWithContext for ResetBranchError {
fn context(&self) -> Option<Context> {
Some(match self {
ResetBranchError::BranchNotFound(ctx) => ctx.to_context(),
ResetBranchError::DefaultTargetNotSet(ctx) => ctx.to_context(),
ResetBranchError::CommitNotFoundInBranch(oid) => {
error::Context::new(Code::Branches, format!("commit {} not found", oid))
}
ResetBranchError::Other(error) => return error.custom_context_or_root_cause().into(),
ResetBranchError::Git(_err) => return None,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum ApplyBranchError {
#[error("project")]
Conflict(ProjectConflict),
#[error("branch not found")]
BranchNotFound(BranchNotFound),
#[error("branch being applied conflicts with other branch: {0}")]
#[error("branch {0} is in a conflicting state")]
BranchConflicts(BranchId),
#[error("default target not set")]
DefaultTargetNotSet(DefaultTargetNotSet),
@ -152,22 +77,6 @@ pub enum ApplyBranchError {
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for ApplyBranchError {
fn context(&self) -> Option<Context> {
Some(match self {
ApplyBranchError::DefaultTargetNotSet(ctx) => ctx.to_context(),
ApplyBranchError::Conflict(ctx) => ctx.to_context(),
ApplyBranchError::BranchNotFound(ctx) => ctx.to_context(),
ApplyBranchError::BranchConflicts(id) => error::Context::new(
Code::Branches,
format!("Branch {} is in a conflicting state", id),
),
ApplyBranchError::GitError(_) => return None,
ApplyBranchError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum UnapplyOwnershipError {
#[error("default target not set")]
@ -252,31 +161,14 @@ pub enum CommitError {
DefaultTargetNotSet(DefaultTargetNotSet),
#[error("will not commit conflicted files")]
Conflicted(ProjectConflict),
#[error("commit hook rejected")]
#[error("commit hook rejected: {0}")]
CommitHookRejected(String),
#[error("commit msg hook rejected")]
#[error("commit-msg hook rejected: {0}")]
CommitMsgHookRejected(String),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for CommitError {
fn context(&self) -> Option<Context> {
Some(match self {
CommitError::BranchNotFound(ctx) => ctx.to_context(),
CommitError::DefaultTargetNotSet(ctx) => ctx.to_context(),
CommitError::Conflicted(ctx) => ctx.to_context(),
CommitError::CommitHookRejected(error) => {
error::Context::new(Code::PreCommitHook, error)
}
CommitError::CommitMsgHookRejected(error) => {
error::Context::new(Code::CommitMsgHook, error)
}
CommitError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum PushError {
#[error("default target not set")]
@ -304,26 +196,12 @@ impl ErrorWithContext for PushError {
pub enum IsRemoteBranchMergableError {
#[error("default target not set")]
DefaultTargetNotSet(DefaultTargetNotSet),
#[error("branch not found")]
#[error("Remote branch {0} not found")]
BranchNotFound(git::RemoteRefname),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for IsRemoteBranchMergableError {
fn context(&self) -> Option<Context> {
Some(match self {
IsRemoteBranchMergableError::BranchNotFound(name) => {
error::Context::new(Code::Branches, format!("Remote branch {} not found", name))
}
IsRemoteBranchMergableError::DefaultTargetNotSet(ctx) => ctx.to_context(),
IsRemoteBranchMergableError::Other(error) => {
return error.custom_context_or_root_cause().into()
}
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum IsVirtualBranchMergeable {
#[error("default target not set")]
@ -354,7 +232,7 @@ pub struct ForcePushNotAllowed {
impl ForcePushNotAllowed {
fn to_context(&self) -> error::Context {
error::Context::new_static(
Code::Branches,
Code::Unknown,
"Action will lead to force pushing, which is not allowed for this",
)
}
@ -362,7 +240,7 @@ impl ForcePushNotAllowed {
#[derive(Debug, thiserror::Error)]
pub enum CherryPickError {
#[error("target commit {0} not found ")]
#[error("commit {0} not found ")]
CommitNotFound(git::Oid),
#[error("can not cherry pick not applied branch")]
NotApplied,
@ -372,21 +250,6 @@ pub enum CherryPickError {
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for CherryPickError {
fn context(&self) -> Option<Context> {
Some(match self {
CherryPickError::NotApplied => {
error::Context::new_static(Code::Branches, "can not cherry pick non applied branch")
}
CherryPickError::Conflict(ctx) => ctx.to_context(),
CherryPickError::CommitNotFound(oid) => {
error::Context::new(Code::Branches, format!("commit {oid} not found"))
}
CherryPickError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum SquashError {
#[error("force push not allowed")]
@ -405,25 +268,6 @@ pub enum SquashError {
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for SquashError {
fn context(&self) -> Option<Context> {
Some(match self {
SquashError::ForcePushNotAllowed(ctx) => ctx.to_context(),
SquashError::DefaultTargetNotSet(ctx) => ctx.to_context(),
SquashError::BranchNotFound(ctx) => ctx.to_context(),
SquashError::Conflict(ctx) => ctx.to_context(),
SquashError::CantSquashRootCommit => {
error::Context::new_static(Code::Branches, "can not squash root branch commit")
}
SquashError::CommitNotFound(oid) => error::Context::new(
crate::error::Code::Branches,
format!("commit {oid} not found"),
),
SquashError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum FetchFromTargetError {
#[error("default target not set")]
@ -448,7 +292,7 @@ impl ErrorWithContext for FetchFromTargetError {
pub enum UpdateCommitMessageError {
#[error("force push not allowed")]
ForcePushNotAllowed(ForcePushNotAllowed),
#[error("empty message")]
#[error("Commit message can not be empty")]
EmptyMessage,
#[error("default target not set")]
DefaultTargetNotSet(DefaultTargetNotSet),
@ -462,51 +306,16 @@ pub enum UpdateCommitMessageError {
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for UpdateCommitMessageError {
fn context(&self) -> Option<Context> {
Some(match self {
UpdateCommitMessageError::ForcePushNotAllowed(ctx) => ctx.to_context(),
UpdateCommitMessageError::EmptyMessage => {
error::Context::new_static(Code::Branches, "Commit message can not be empty")
}
UpdateCommitMessageError::DefaultTargetNotSet(ctx) => ctx.to_context(),
UpdateCommitMessageError::CommitNotFound(oid) => {
error::Context::new(Code::Branches, format!("Commit {} not found", oid))
}
UpdateCommitMessageError::BranchNotFound(ctx) => ctx.to_context(),
UpdateCommitMessageError::Conflict(ctx) => ctx.to_context(),
UpdateCommitMessageError::Other(error) => {
return error.custom_context_or_root_cause().into()
}
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum SetBaseBranchError {
#[error("wd is dirty")]
#[error("Current HEAD is dirty.")]
DirtyWorkingDirectory,
#[error("branch {0} not found")]
#[error("remote branch '{0}' not found")]
BranchNotFound(git::RemoteRefname),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for SetBaseBranchError {
fn context(&self) -> Option<Context> {
Some(match self {
SetBaseBranchError::DirtyWorkingDirectory => {
error::Context::new(Code::ProjectConflict, "Current HEAD is dirty.")
}
SetBaseBranchError::BranchNotFound(name) => error::Context::new(
Code::Branches,
format!("remote branch '{}' not found", name),
),
SetBaseBranchError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum UpdateBaseBranchError {
#[error("project is in conflicting state")]
@ -539,65 +348,26 @@ pub enum MoveCommitError {
DefaultTargetNotSet(DefaultTargetNotSet),
#[error("branch not found")]
BranchNotFound(BranchNotFound),
#[error("commit not found")]
#[error("commit {0} not found")]
CommitNotFound(git::Oid),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for MoveCommitError {
fn context(&self) -> Option<Context> {
Some(match self {
MoveCommitError::SourceLocked => error::Context::new_static(
Code::Branches,
"Source branch contains hunks locked to the target commit",
),
MoveCommitError::Conflicted(ctx) => ctx.to_context(),
MoveCommitError::DefaultTargetNotSet(ctx) => ctx.to_context(),
MoveCommitError::BranchNotFound(ctx) => ctx.to_context(),
MoveCommitError::CommitNotFound(oid) => {
error::Context::new(Code::Branches, format!("Commit {} not found", oid))
}
MoveCommitError::Other(error) => return error.custom_context_or_root_cause().into(),
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum CreateVirtualBranchFromBranchError {
#[error("failed to apply")]
ApplyBranch(ApplyBranchError),
#[error("can't make branch from default target")]
#[error("can not create a branch from default target")]
CantMakeBranchFromDefaultTarget,
#[error("default target not set")]
DefaultTargetNotSet(DefaultTargetNotSet),
#[error("{0} not found")]
#[error("branch {0} not found")]
BranchNotFound(git::Refname),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for CreateVirtualBranchFromBranchError {
fn context(&self) -> Option<Context> {
Some(match self {
CreateVirtualBranchFromBranchError::ApplyBranch(err) => return err.context(),
CreateVirtualBranchFromBranchError::CantMakeBranchFromDefaultTarget => {
error::Context::new_static(
Code::Branches,
"Can not create a branch from default target",
)
}
CreateVirtualBranchFromBranchError::DefaultTargetNotSet(ctx) => ctx.to_context(),
CreateVirtualBranchFromBranchError::BranchNotFound(name) => {
error::Context::new(Code::Branches, format!("Branch {} not found", name))
}
CreateVirtualBranchFromBranchError::Other(error) => {
return error.custom_context_or_root_cause().into()
}
})
}
}
#[derive(Debug)]
pub struct ProjectConflict {
pub project_id: ProjectId,
@ -605,10 +375,10 @@ pub struct ProjectConflict {
impl ProjectConflict {
fn to_context(&self) -> error::Context {
error::Context::new(
Code::ProjectConflict,
format!("project {} is in a conflicted state", self.project_id),
)
error::Context::new(format!(
"project {} is in a conflicted state",
self.project_id
))
}
}
@ -619,13 +389,10 @@ pub struct DefaultTargetNotSet {
impl DefaultTargetNotSet {
fn to_context(&self) -> error::Context {
error::Context::new(
Code::ProjectConflict,
format!(
"project {} does not have a default target set",
self.project_id
),
)
error::Context::new(format!(
"project {} does not have a default target set",
self.project_id
))
}
}
@ -637,10 +404,7 @@ pub struct BranchNotFound {
impl BranchNotFound {
fn to_context(&self) -> error::Context {
error::Context::new(
Code::Branches,
format!("branch {} not found", self.branch_id),
)
error::Context::new(format!("branch {} not found", self.branch_id))
}
}
@ -666,23 +430,12 @@ impl ErrorWithContext for UpdateBranchError {
#[derive(Debug, thiserror::Error)]
pub enum ListRemoteCommitFilesError {
#[error("failed to find commit {0}")]
#[error("commit {0} not found")]
CommitNotFound(git::Oid),
#[error("failed to find commit")]
Other(#[from] anyhow::Error),
}
impl ErrorWithContext for ListRemoteCommitFilesError {
fn context(&self) -> Option<Context> {
match self {
ListRemoteCommitFilesError::CommitNotFound(oid) => {
error::Context::new(Code::Branches, format!("Commit {} not found", oid)).into()
}
ListRemoteCommitFilesError::Other(error) => error.custom_context_or_root_cause().into(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ListRemoteBranchesError {
#[error("default target not set")]

View File

@ -1233,7 +1233,7 @@ pub fn integrate_upstream_commits(
if has_rebased_commits && !can_use_force {
let message = "Aborted because force push is disallowed and commits have been rebased.";
return Err(anyhow!("Cannot merge rebased commits without force push")
.context(error::Context::new(Code::ProjectConflict, message)));
.context(error::Context::new(message)));
}
let integration_result = match can_use_force {
@ -1243,7 +1243,7 @@ pub fn integrate_upstream_commits(
let message =
"Aborted because force push is disallowed and commits have been rebased.";
return Err(anyhow!("Cannot merge rebased commits without force push")
.context(error::Context::new(Code::ProjectConflict, message)));
.context(error::Context::new(message)));
}
integrate_with_merge(
project_repository,

View File

@ -13,9 +13,9 @@ mod into_anyhow {
}
}
let err = into_anyhow(Error(Code::Projects));
let err = into_anyhow(Error(Code::Validation));
let ctx = err.downcast_ref::<Context>().unwrap();
assert_eq!(ctx.code, Code::Projects, "the context is attached");
assert_eq!(ctx.code, Code::Validation, "the context is attached");
assert_eq!(
ctx.message, None,
"there is no message when context was created from bare code"
@ -38,11 +38,11 @@ mod into_anyhow {
}
}
let err = into_anyhow(Outer::from(Inner(Code::Projects)));
let err = into_anyhow(Outer::from(Inner(Code::Validation)));
let ctx = err.downcast_ref::<Context>().unwrap();
assert_eq!(
ctx.code,
Code::Projects,
Code::Validation,
"there is no magic here, it's all about manually implementing the nesting :/"
);
}

View File

@ -93,7 +93,7 @@ mod add {
let worktree = repo.worktree("feature", &worktree_dir, None).unwrap();
let err = controller.add(worktree.path()).unwrap_err();
assert_eq!(err.to_string(), "worktrees unsupported");
assert_eq!(err.to_string(), "can only work in main worktrees");
}
fn create_initial_commit(repo: &git2::Repository) -> git2::Oid {

View File

@ -2095,7 +2095,7 @@ fn verify_branch_not_integration() -> Result<()> {
assert!(verify_result.is_err());
assert_eq!(
verify_result.unwrap_err().to_string(),
"head is refs/heads/master"
"project is on refs/heads/master. Please checkout gitbutler/integration to continue"
);
Ok(())

View File

@ -48,6 +48,17 @@ mod frontend {
pub fn from_error_with_context(err: impl ErrorWithContext + Send + Sync + 'static) -> Self {
Self(into_anyhow(err))
}
/// Convert an error without context to our type.
///
/// For now, we avoid using a conversion as it would be so general, we'd miss errors with context
/// which need [`from_error_with_context`](Self::from_error_with_context) for the context to be
/// picked up.
pub fn from_error_without_context(
err: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self(err.into())
}
}
impl Serialize for Error {
@ -92,30 +103,30 @@ mod frontend {
#[test]
fn find_code() {
let err = anyhow!("err msg").context(Code::Projects);
let err = anyhow!("err msg").context(Code::Validation);
assert_eq!(
json(err),
"{\"code\":\"errors.projects\",\"message\":\"err msg\"}",
"{\"code\":\"errors.validation\",\"message\":\"err msg\"}",
"the 'code' is available as string, but the message is taken from the source error"
);
}
#[test]
fn find_context() {
let err = anyhow!("err msg").context(Context::new_static(Code::Projects, "ctx msg"));
let err = anyhow!("err msg").context(Context::new_static(Code::Validation, "ctx msg"));
assert_eq!(
json(err),
"{\"code\":\"errors.projects\",\"message\":\"ctx msg\"}",
"{\"code\":\"errors.validation\",\"message\":\"ctx msg\"}",
"Contexts often provide their own message, so the error message is ignored"
);
}
#[test]
fn find_context_without_message() {
let err = anyhow!("err msg").context(Context::from(Code::Projects));
let err = anyhow!("err msg").context(Context::from(Code::Validation));
assert_eq!(
json(err),
"{\"code\":\"errors.projects\",\"message\":\"err msg\"}",
"{\"code\":\"errors.validation\",\"message\":\"err msg\"}",
"Contexts without a message show the error's message as well"
);
}
@ -124,10 +135,10 @@ mod frontend {
fn find_nested_code() {
let err = anyhow!("bottom msg")
.context("top msg")
.context(Code::Projects);
.context(Code::Validation);
assert_eq!(
json(err),
"{\"code\":\"errors.projects\",\"message\":\"top msg\"}",
"{\"code\":\"errors.validation\",\"message\":\"top msg\"}",
"the 'code' gets the message of the error that it provides context to, and it finds it down the chain"
);
}
@ -135,12 +146,12 @@ mod frontend {
#[test]
fn multiple_codes() {
let err = anyhow!("bottom msg")
.context(Code::Menu)
.context(Code::ProjectGitAuth)
.context("top msg")
.context(Code::Projects);
.context(Code::Validation);
assert_eq!(
json(err),
"{\"code\":\"errors.projects\",\"message\":\"top msg\"}",
"{\"code\":\"errors.validation\",\"message\":\"top msg\"}",
"it finds the most recent 'code' (and the same would be true for contexts, of course)"
);
}

View File

@ -27,9 +27,7 @@ pub async fn menu_item_set_enabled(
let menu_item = window
.menu_handle()
.try_get_item(menu_item_id)
.with_context(|| {
error::Context::new(Code::Menu, format!("menu item not found: {}", menu_item_id))
})?;
.with_context(|| error::Context::new(format!("menu item not found: {}", menu_item_id)))?;
menu_item.set_enabled(enabled).context(Code::Unknown)?;

View File

@ -2,7 +2,6 @@ pub mod commands {
use anyhow::Context;
use std::path;
use gitbutler_core::error::Code;
use gitbutler_core::projects::{self, controller::Controller, ProjectId};
use tauri::Manager;
use tracing::instrument;
@ -20,7 +19,7 @@ pub mod commands {
.state::<Controller>()
.update(&project)
.await
.map_err(Error::from_error_with_context)
.map_err(Error::from_error_without_context)
}
#[tauri::command(async)]
@ -32,7 +31,7 @@ pub mod commands {
handle
.state::<Controller>()
.add(path)
.map_err(Error::from_error_with_context)
.map_err(Error::from_error_without_context)
}
#[tauri::command(async)]
@ -41,10 +40,7 @@ pub mod commands {
handle: tauri::AppHandle,
id: ProjectId,
) -> Result<projects::Project, Error> {
handle
.state::<Controller>()
.get(id)
.map_err(Error::from_error_with_context)
Ok(handle.state::<Controller>().get(id)?)
}
#[tauri::command(async)]
@ -83,10 +79,10 @@ pub mod commands {
id: ProjectId,
key: &str,
) -> Result<Option<String>, Error> {
Ok(handle
handle
.state::<Controller>()
.get_local_config(id, key)
.context(Code::Projects)?)
.map_err(Error::from_error_without_context)
}
#[tauri::command(async)]
@ -97,9 +93,9 @@ pub mod commands {
key: &str,
value: &str,
) -> Result<(), Error> {
Ok(handle
handle
.state::<Controller>()
.set_local_config(id, key, value)
.context(Code::Projects)?)
.map_err(Error::from_error_without_context)
}
}