Skip to content

Commit

Permalink
Merge pull request #46 from koordinates/exit_codes
Browse files Browse the repository at this point in the history
Add exit codes that indicate what went wrong
  • Loading branch information
olsen232 authored Apr 15, 2020
2 parents 7452101 + f2af66c commit 35f38ad
Show file tree
Hide file tree
Showing 18 changed files with 220 additions and 81 deletions.
3 changes: 2 additions & 1 deletion sno/branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pygit2

from .cli_util import do_json_option
from .exceptions import InvalidOperation
from .exec import execvp


Expand Down Expand Up @@ -33,7 +34,7 @@ def branch(ctx, do_json, args):
if sargs & {"-d", "--delete", "-D"}:
branch = repo.head.shorthand
if branch in sargs:
raise click.ClickException(
raise InvalidOperation(
f"Cannot delete the branch '{branch}' which you are currently on."
)

Expand Down
7 changes: 4 additions & 3 deletions sno/checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import click
import pygit2

from .exceptions import InvalidOperation, NotFound, NO_WORKING_COPY
from .structure import RepositoryStructure
from .working_copy import WorkingCopy

Expand Down Expand Up @@ -66,7 +67,7 @@ def checkout(ctx, branch, fmt, force, path, datasets, refish):
wc = repo_structure.working_copy
if wc:
if path is not None:
raise click.ClickException(
raise InvalidOperation(
f"This repository already has a working copy at: {wc.path}",
)

Expand Down Expand Up @@ -221,7 +222,7 @@ def restore(ctx, source, pathspec):
repo_structure = RepositoryStructure(repo)
working_copy = repo_structure.working_copy
if not working_copy:
raise click.ClickException("You don't have a working copy")
raise NotFound("You don't have a working copy", exit_code=NO_WORKING_COPY)

head_commit = repo.head.peel(pygit2.Commit)

Expand All @@ -246,7 +247,7 @@ def workingcopy_set_path(ctx, new):

repo_cfg = repo.config
if "sno.workingcopy.path" not in repo_cfg:
raise click.ClickException("No working copy? Try `sno checkout`")
raise NotFound("No working copy? Try `sno checkout`", exit_code=NO_WORKING_COPY)

new = Path(new)
# TODO: This doesn't seem to do anything?
Expand Down
17 changes: 10 additions & 7 deletions sno/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from . import is_windows
from .core import check_git_user
from .diff import Diff
from .exceptions import NotFound, SubprocessError, NO_CHANGES, NO_DATA, NO_WORKING_COPY
from .status import (
get_branch_status_message,
get_diff_status_message,
Expand Down Expand Up @@ -64,8 +65,9 @@ def commit(ctx, message, message_file, allow_empty, do_json):
repo = ctx.obj.repo

if repo.is_empty:
raise click.UsageError(
'Empty repository.\n (use "sno import" to add some data)'
raise NotFound(
'Empty repository.\n (use "sno import" to add some data)',
exit_code=NO_DATA,
)

check_git_user(repo)
Expand All @@ -75,7 +77,7 @@ def commit(ctx, message, message_file, allow_empty, do_json):

working_copy = WorkingCopy.open(repo)
if not working_copy:
raise click.UsageError("No working copy, use 'checkout'")
raise NotFound("No working copy, use 'checkout'", exit_code=NO_WORKING_COPY)

working_copy.assert_db_tree_match(tree)

Expand All @@ -88,7 +90,7 @@ def commit(ctx, message, message_file, allow_empty, do_json):
wc_changes[dataset.path] = diff.counts(dataset)

if not wcdiff and not allow_empty:
raise click.ClickException("No changes to commit")
raise NotFound("No changes to commit", exit_code=NO_CHANGES)

if message_file:
commit_msg = message_file.read().strip()
Expand All @@ -98,7 +100,7 @@ def commit(ctx, message, message_file, allow_empty, do_json):
commit_msg = get_commit_message(repo, wc_changes, quiet=do_json)

if not commit_msg:
raise click.Abort()
raise click.UsageError("No commit message")

rs.commit(wcdiff, commit_msg, allow_empty=allow_empty)

Expand Down Expand Up @@ -152,8 +154,9 @@ def get_commit_message(repo, wc_changes, quiet=False):
try:
subprocess.check_call(editor_cmd, shell=True)
except subprocess.CalledProcessError as e:
raise click.ClickException(
f"There was a problem with the editor '{editor}': {e}"
raise SubprocessError(
f"There was a problem with the editor '{editor}': {e}",
called_process_error=e,
) from e

with open(commit_editmsg, "rt", encoding="utf-8") as f:
Expand Down
16 changes: 9 additions & 7 deletions sno/context.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from pathlib import Path

import click
import pygit2

from .exceptions import NotFound, NO_REPOSITORY


class Context(object):
DEFAULT_REPO_PATH = Path()
Expand Down Expand Up @@ -41,11 +42,12 @@ def repo(self):

if not self._repo or not self._repo.is_bare:
if self.has_repo_path:
raise click.BadParameter(
"Not an existing repository", param_hint="--repo"
)
message = "Not an existing repository"
param_hint = "repo"
else:
raise click.UsageError(
"Current directory is not an existing repository"
)
message = "Current directory is not an existing repository"
param_hint = None

raise NotFound(message, exit_code=NO_REPOSITORY, param_hint=param_hint)

return self._repo
6 changes: 2 additions & 4 deletions sno/core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import os

import pygit2
from click import ClickException
from .exceptions import NotFound, NO_USER


def walk_tree(top, path="", topdown=True):
Expand Down Expand Up @@ -99,4 +97,4 @@ def check_git_user(repo=None):

msg.append("\n(sno uses the same credentials and configuration as git)")

raise ClickException("\n".join(msg))
raise NotFound("\n".join(msg), exit_code=NO_USER)
30 changes: 22 additions & 8 deletions sno/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@

from . import gpkg
from .cli_util import MutexOption
from .exceptions import (
InvalidOperation,
NotYetImplemented,
NotFound,
NO_WORKING_COPY,
NO_COMMIT,
UNCATEGORIZED_ERROR,
)


L = logging.getLogger("sno.diff")
Expand Down Expand Up @@ -435,14 +443,18 @@ def diff(ctx, output_format, output_path, exit_code, args):
commit_target = repo.revparse_single(commit_parts[2] or "HEAD")
L.debug("commit_target=%s", commit_target.id)
except KeyError:
raise click.BadParameter("Invalid commit spec", param_hint="commit")
raise NotFound(
"Invalid commit spec", param_hint="commit", exit_code=NO_COMMIT
)
else:
path_list.pop(0)
else:
try:
commit_base = repo.revparse_single(commit_parts[0])
except KeyError:
raise click.BadParameter("Invalid commit spec", param_hint="commit")
raise NotFound(
"Invalid commit spec", param_hint="commit", exit_code=NO_COMMIT
)
else:
path_list.pop(0)

Expand All @@ -456,7 +468,9 @@ def diff(ctx, output_format, output_path, exit_code, args):
L.debug("commit_target=working-copy")
working_copy = WorkingCopy.open(repo)
if not working_copy:
raise click.UsageError("No working copy, use 'checkout'")
raise NotFound(
"No working copy, use 'checkout'", exit_code=NO_WORKING_COPY
)

working_copy.assert_db_tree_match(commit_head.peel(pygit2.Tree))

Expand All @@ -469,12 +483,12 @@ def diff(ctx, output_format, output_path, exit_code, args):

if not merge_base:
# there is no relation between the commits
raise click.ClickException(
raise InvalidOperation(
f"Commits {commit_base.id} and {c_target.id} aren't related."
)
elif merge_base not in (commit_base.id, c_target.id):
# this needs a 3-way diff and we don't support them yet
raise click.ClickException(f"Sorry, 3-way diffs aren't supported yet.")
raise NotYetImplemented(f"Sorry, 3-way diffs aren't supported yet.")

base_rs = RepositoryStructure(repo, commit_base)
all_datasets = {ds.path for ds in base_rs}
Expand Down Expand Up @@ -537,14 +551,14 @@ def diff(ctx, output_format, output_path, exit_code, args):
w(dataset, diff[dataset])
except click.ClickException as e:
L.debug("Caught ClickException: %s", e)
if exit_code:
e.exit_code = 2
if exit_code and e.exit_code == 1:
e.exit_code = UNCATEGORIZED_ERROR
raise
except Exception as e:
L.debug("Caught non-ClickException: %s", e)
if exit_code:
click.secho(f"Error: {e}", fg="red", file=sys.stderr)
raise SystemExit(2) from e
raise SystemExit(UNCATEGORIZED_ERROR) from e
else:
raise
else:
Expand Down
91 changes: 91 additions & 0 deletions sno/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import click

# Exit codes

SUCCESS = 0
SUCCESS_WITH_FLAG = 1

INVALID_ARGUMENT = 2

# We could use 1 for this, except in --exit-code mode.
# So we always use 11 for consistency.
UNCATEGORIZED_ERROR = 11

INVALID_OPERATION = 20

NOT_YET_IMPLEMENTED = 30

NOT_FOUND = 40
NO_REPOSITORY = 41
NO_DATA = 42
NO_BRANCH = 43
NO_CHANGES = 44
NO_WORKING_COPY = 45
NO_USER = 46
NO_COMMIT = 47
NO_IMPORT_SOURCE = 48
NO_TABLE = 49

SUBPROCESS_ERROR_FLAG = 128
DEFAULT_SUBPROCESS_ERROR = 129


class BaseException(click.ClickException):
"""
A ClickException that can easily be constructed with any exit code,
and which can also optionally give a hint about which param lead to
the problem.
Providing a param hint or not can be done for any type of error -
an unparseable import path and an import path that points to a
corrupted database might both provide the same hint, but be
considered completely different types of errors.
"""

exit_code = UNCATEGORIZED_ERROR

def __init__(self, message, exit_code=None, param=None, param_hint=None):
super(BaseException, self).__init__(message)

if exit_code is not None:
self.exit_code = exit_code

self.param_hint = None
if param_hint is not None:
self.param_hint = param_hint
elif param is not None:
self.param_hint = param.get_error_hint(None)

def format_message(self):
if self.param_hint is not None:
return f"Invalid value for {self.param_hint}: {self.message}"
return self.message


class InvalidOperation(BaseException):
exit_code = INVALID_OPERATION


class NotYetImplemented(BaseException):
exit_code = NOT_YET_IMPLEMENTED


class NotFound(BaseException):
exit_code = NOT_FOUND


class SubprocessError(BaseException):
exit_code = DEFAULT_SUBPROCESS_ERROR

def __init__(
self,
message,
exit_code=None,
param=None,
param_hint=None,
called_process_error=None,
):
super(SubprocessError, self).__init__(
message, exit_code=exit_code, param=param, param_hint=param_hint
)
if called_process_error and not exit_code:
self.exit_code = SUBPROCESS_ERROR_FLAG + called_process_error.return_code
8 changes: 5 additions & 3 deletions sno/fsck.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import pygit2
from osgeo import gdal

from . import core, gpkg
from . import gpkg
from .exceptions import NotFound, NO_WORKING_COPY
from .structure import RepositoryStructure


Expand Down Expand Up @@ -87,8 +88,9 @@ def fsck(ctx, reset_datasets, fsck_args):

working_copy_path = repo.config["sno.workingcopy.path"]
if not os.path.isfile(working_copy_path):
raise click.ClickException(
click.style(f"Working copy missing: {working_copy_path}", fg="red")
raise NotFound(
click.style(f"Working copy missing: {working_copy_path}", fg="red"),
exit_code=NO_WORKING_COPY,
)
working_copy = rs.working_copy

Expand Down
Loading

0 comments on commit 35f38ad

Please sign in to comment.