Skip to content

Commit

Permalink
Update kart create-workingcopy for workdir...
Browse files Browse the repository at this point in the history
just as it does for tabular working copy
  • Loading branch information
olsen232 committed Aug 24, 2022
1 parent 2847b00 commit 73eb608
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 143 deletions.
131 changes: 0 additions & 131 deletions kart/checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@
from .exceptions import (
NO_BRANCH,
NO_COMMIT,
DbConnectionError,
InvalidOperation,
NotFound,
)
from .key_filters import RepoKeyFilter
from .output_util import InputMode, get_input_mode
from .promisor_utils import get_partial_clone_envelope
from .spatial_filter import SpatialFilterString, spatial_filter_help_text
from .structs import CommitWithReference
from .working_copy import PartType
from kart.cli_util import KartCommand

_DISCARD_CHANGES_HELP_MESSAGE = (
Expand Down Expand Up @@ -428,131 +425,3 @@ def reset(ctx, discard_changes, refish):
repo.set_head(commit.id)

repo.working_copy.reset_to_head()


@click.command("create-workingcopy", cls=KartCommand)
@click.pass_context
@click.option(
"--discard-changes",
"--force",
"-f",
is_flag=True,
help="Discard local changes in working copy if necessary",
)
@click.option(
"--delete-existing/--no-delete-existing",
help="Whether to delete the existing working copy",
required=False,
default=None,
)
@click.argument("new_wc_loc", nargs=1, required=False)
def create_workingcopy(ctx, delete_existing, discard_changes, new_wc_loc):
"""
Create a new working copy - if one already exists it will be deleted.
Usage: kart create-workingcopy [LOCATION]
LOCATION should be one of the following:
- PATH.gpkg for a GPKG file.
- postgresql://HOST/DBNAME/DBSCHEMA for a PostGIS database.
- mssql://HOST/DBNAME/DBSCHEMA for a SQL Server database.
If no location is supplied, the location from the repo config at "kart.workingcopy.location" will be used.
If no location is configured, a GPKG working copy will be created with a default name based on the repository name.
"""

# TODO - this deals with just the tabular WC part, which is probably fine, but it means it is now misnamed.

from kart.tabular.working_copy import TableWorkingCopyStatus
from kart.tabular.working_copy.base import TableWorkingCopy

repo = ctx.obj.repo
if repo.head_is_unborn:
raise InvalidOperation(
"Can't create a working copy for an empty repository — first import some data with `kart import`"
)

old_wc_loc = repo.workingcopy_location
if not new_wc_loc and old_wc_loc is not None:
new_wc_loc = old_wc_loc
elif not new_wc_loc:
new_wc_loc = TableWorkingCopy.default_location(repo)

if new_wc_loc != old_wc_loc:
TableWorkingCopy.check_valid_creation_location(new_wc_loc, repo)

if TableWorkingCopy.clearly_doesnt_exist(old_wc_loc, repo):
old_wc_loc = None

if old_wc_loc:
old_wc = TableWorkingCopy.get_at_location(
repo,
old_wc_loc,
allow_uncreated=True,
allow_invalid_state=True,
allow_unconnectable=True,
)

if delete_existing is None:
if get_input_mode() is not InputMode.INTERACTIVE:
if old_wc_loc == new_wc_loc:
help_message = (
"Specify --delete-existing to delete and recreate it."
)
else:
help_message = "Either delete it with --delete-existing, or just abandon it with --no-delete-existing."
raise click.UsageError(
f"A working copy is already configured at {old_wc}\n{help_message}"
)

click.echo(f"A working copy is already configured at {old_wc}")
delete_existing = click.confirm(
"Delete the existing working copy before creating a new one?",
default=True,
)

check_if_dirty = not discard_changes

if delete_existing is False:
allow_unconnectable = old_wc_loc != new_wc_loc
status = old_wc.status(
allow_unconnectable=allow_unconnectable, check_if_dirty=check_if_dirty
)
if old_wc_loc == new_wc_loc and status & TableWorkingCopyStatus.WC_EXISTS:
raise InvalidOperation(
f"Cannot recreate working copy at same location {old_wc} if --no-delete-existing is set."
)

if not discard_changes and (status & TableWorkingCopyStatus.DIRTY):
raise InvalidOperation(
f"You have uncommitted changes at {old_wc}.\n"
+ _DISCARD_CHANGES_HELP_MESSAGE
)

if delete_existing is True:
try:
status = old_wc.status(check_if_dirty=check_if_dirty)
except DbConnectionError as e:
click.echo(
f"Encountered an error while trying to delete existing working copy at {old_wc}"
)
click.echo(
"To simply abandon the existing working copy, use --no-delete-existing."
)
raise e

if not discard_changes and (status & TableWorkingCopyStatus.DIRTY):
raise InvalidOperation(
f"You have uncommitted changes at {old_wc}.\n"
+ _DISCARD_CHANGES_HELP_MESSAGE
)

if status & TableWorkingCopyStatus.WC_EXISTS:
click.echo(f"Deleting existing working copy at {old_wc}")
keep_db_schema_if_possible = old_wc_loc == new_wc_loc
old_wc.delete(keep_db_schema_if_possible=keep_db_schema_if_possible)

TableWorkingCopy.write_config(repo, new_wc_loc)
repo.working_copy.reset_to_head(create_parts_if_missing=[PartType.TABULAR])

# This command is used in tests and by other commands, so we have to be extra careful to
# tidy up properly - otherwise, tests can fail (on Windows especially) due to PermissionError.
repo.free()
del repo
3 changes: 2 additions & 1 deletion kart/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
"annotations.cli": {"build-annotations"},
"apply": {"apply"},
"branch": {"branch"},
"checkout": {"checkout", "create-workingcopy", "reset", "restore", "switch"},
"checkout": {"checkout", "reset", "restore", "switch"},
"clone": {"clone"},
"conflicts": {"conflicts"},
"commit": {"commit"},
"create_workingcopy": {"create-workingcopy"},
"data": {"data"},
"diff": {"diff"},
"fsck": {"fsck"},
Expand Down
237 changes: 237 additions & 0 deletions kart/create_workingcopy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import click

from kart.cli_util import KartCommand
from kart.exceptions import InvalidOperation, DbConnectionError
from kart.output_util import InputMode, get_input_mode
from kart.working_copy import PartType


_DISCARD_CHANGES_HELP_MESSAGE = (
"Commit these changes first (`kart commit`) or"
" just discard them by adding the option `--discard-changes`."
)


def create_tabular_workingcopy(repo, delete_existing, discard_changes, new_wc_loc):
"""Create or recreate the tabular working copy."""
from kart.tabular.working_copy import TableWorkingCopyStatus
from kart.tabular.working_copy.base import TableWorkingCopy

# This function is relatively complex because it has a few different possibilities -
# - old and new are the same place, old and new are different, old definitely doesn't exist,
# old may exist but we don't know until we try to connect, old is dirty, old is corrupt,
# user wants to delete old or not, user wants to discard changes or not...

# And, we try to handle all these situations without trying to connect to the old database
# too many times, or at all if we don't need to. The old database could be completely gone /
# unavailable, or it could be corrupt, and trying to connect if we don't need to could cause
# this command to fail unnecessarily, or could cause an unnecessary long wait before a timeout.

old_wc_loc = repo.workingcopy_location
if not new_wc_loc and old_wc_loc is not None:
new_wc_loc = old_wc_loc
elif not new_wc_loc:
new_wc_loc = TableWorkingCopy.default_location(repo)

if new_wc_loc != old_wc_loc:
TableWorkingCopy.check_valid_creation_location(new_wc_loc, repo)

if TableWorkingCopy.clearly_doesnt_exist(old_wc_loc, repo):
old_wc_loc = None

if old_wc_loc:
old_wc = TableWorkingCopy.get_at_location(
repo,
old_wc_loc,
allow_uncreated=True,
allow_invalid_state=True,
allow_unconnectable=True,
)

if delete_existing is None:
if get_input_mode() is not InputMode.INTERACTIVE:
if old_wc_loc == new_wc_loc:
help_message = (
"Specify --delete-existing to delete and recreate it."
)
else:
help_message = "Either delete it with --delete-existing, or just abandon it with --no-delete-existing."
raise click.UsageError(
f"A tabular working copy is already configured at {old_wc}\n{help_message}"
)

click.echo(f"A tabular working copy is already configured at {old_wc}")
delete_existing = click.confirm(
"Delete the existing working copy before creating a new one?",
default=True,
)

check_if_dirty = not discard_changes

if delete_existing is False:
allow_unconnectable = old_wc_loc != new_wc_loc
status = old_wc.status(
allow_unconnectable=allow_unconnectable, check_if_dirty=check_if_dirty
)
if old_wc_loc == new_wc_loc and status & TableWorkingCopyStatus.WC_EXISTS:
raise InvalidOperation(
f"Cannot recreate working copy at same location {old_wc} if --no-delete-existing is set."
)

if not discard_changes and (status & TableWorkingCopyStatus.DIRTY):
raise InvalidOperation(
f"You have uncommitted changes at {old_wc}.\n"
+ _DISCARD_CHANGES_HELP_MESSAGE
)

if delete_existing is True:
try:
status = old_wc.status(check_if_dirty=check_if_dirty)
except DbConnectionError as e:
click.echo(
f"Encountered an error while trying to delete existing working copy at {old_wc}"
)
click.echo(
"To simply abandon the existing working copy, use --no-delete-existing."
)
raise e

if not discard_changes and (status & TableWorkingCopyStatus.DIRTY):
raise InvalidOperation(
f"You have uncommitted changes at {old_wc}.\n"
+ _DISCARD_CHANGES_HELP_MESSAGE
)

if status & TableWorkingCopyStatus.WC_EXISTS:
click.echo(f"Deleting existing working copy at {old_wc}")
keep_db_schema_if_possible = old_wc_loc == new_wc_loc
old_wc.delete(keep_db_schema_if_possible=keep_db_schema_if_possible)

TableWorkingCopy.write_config(repo, new_wc_loc)
repo.working_copy.create_parts_if_missing(
[PartType.TABULAR], reset_to=repo.head_commit
)


def create_workdir(repo, delete_existing, discard_changes):
"""Create or recreate the file-system working copy."""

from kart.workdir import FileSystemWorkingCopy, FileSystemWorkingCopyStatus

# This one is much simpler since old and new are always the same place, and,
# we can cheaply check if the files exist or not - we don't need to connect to anything.

old_wc = FileSystemWorkingCopy.get(
repo, allow_uncreated=True, allow_invalid_state=True
)
status = old_wc.status()

if status != FileSystemWorkingCopyStatus.UNCREATED:
if delete_existing is None:
if get_input_mode() is not InputMode.INTERACTIVE:
raise click.UsageError(
f"A file-system working copy already exists. Specify --delete-existing to delete and recreate it."
)

click.echo(f"A file-system working copy already exists.")
delete_existing = click.confirm(
"Delete the existing working copy before creating a new one?",
default=True,
)

if delete_existing is False:
raise InvalidOperation(
"Cannot recreate file-system working copy if --no-delete-existing is set."
)

if (
status == FileSystemWorkingCopyStatus.CREATED
and not discard_changes
and old_wc.is_dirty()
):
raise InvalidOperation(
f"You have uncommitted changes in the file-system working copy.\n"
+ _DISCARD_CHANGES_HELP_MESSAGE
)

if status != FileSystemWorkingCopyStatus.UNCREATED:
assert delete_existing
old_wc.delete()

repo.working_copy.create_parts_if_missing(
[PartType.WORKDIR], reset_to=repo.head_commit
)


@click.command("create-workingcopy", cls=KartCommand)
@click.pass_context
@click.option(
"--discard-changes",
"--force",
"-f",
is_flag=True,
help="Discard local changes in working copy if necessary",
)
@click.option(
"--delete-existing/--no-delete-existing",
help="Whether to delete the existing working copy",
required=False,
default=None,
)
@click.option(
"--parts",
type=click.Choice(["tabular", "file-system", "auto"]),
default="auto",
help=(
"Which parts of the working copy to create / recreate. "
'The default "auto" creates those parts that are required to store the contents of the Kart repo.'
),
)
@click.argument("tabular_location", nargs=1, required=False)
def create_workingcopy(ctx, parts, delete_existing, discard_changes, tabular_location):
"""
Create or recreate a new working copy (or just certain parts of the working copy).
If the required working copy parts already exist, they will be deleted before being recreated.
The parts of the working copy that are required depends on the contents of your Kart repository -
if your repository contains tabular datasets, you need a GPKG file or database in which to view and edit them.
Usage: kart create-workingcopy [TABULAR-LOCATION]
TABULAR-LOCATION specifies where the tabular part of the working copy should be created:
- PATH.gpkg for a GPKG file.
- postgresql://HOST/DBNAME/DBSCHEMA for a PostGIS database.
- mssql://HOST/DBNAME/DBSCHEMA for a SQL Server database.
- mysql://HOST/DBNAME for a MySQL database.
If no such location is supplied, but a tabular part of the working copy is required, the location from the repo
config at "kart.workingcopy.location" will be used. If no such location is configured, a GPKG working copy will be
created with a default name based on the repository name.
"""
repo = ctx.obj.repo
if repo.head_is_unborn:
raise InvalidOperation(
"Can't create a working copy for an empty repository — first import some data with `kart import`"
)

if parts == "auto":
parts = repo.datasets().working_copy_part_types()
if tabular_location:
parts.add(PartType.TABULAR)
else:
if tabular_location and parts != "tabular":
raise click.UsageError(
"The tabular-location should only be specified when creating a tabular working copy (--parts=tabular)"
)
parts = set(
[{"tabular": PartType.TABULAR, "file-system": PartType.WORKDIR}[parts]]
)

if PartType.TABULAR in parts:
create_tabular_workingcopy(
repo, delete_existing, discard_changes, tabular_location
)
if PartType.WORKDIR in parts:
create_workdir(repo, delete_existing, discard_changes)

# This command is used in tests and by other commands, so we have to be extra careful to
# tidy up properly - otherwise, tests can fail (on Windows especially) due to PermissionError.
repo.free()
del repo

0 comments on commit 73eb608

Please sign in to comment.