Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remote git registry #3

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
34b2dd9
introduce GitRegistryWithRemoteSupport and use it in gto.api.show ins…
Nov 25, 2022
ff6b838
use GitRegistryWithRemoteSupport in gto.api.check_ref
Nov 25, 2022
c778f04
use GitRegistryWithRemoteSupport in gto.api.history
Nov 25, 2022
979b2a8
use GitRegistryWithRemoteSupport in gto.api.get_stages
Nov 25, 2022
fb461dd
get rid of GitRegistryWithRemoteSupport in favor of FromRemoteRepoMix…
Nov 25, 2022
ab7a49a
use FromRemoteRepoMixin for EnrichmentManager
Nov 25, 2022
7884531
push non-working example of api.annotate with removed @clone_on_remote
Nov 25, 2022
7ec9791
fix all tests
Nov 26, 2022
d0ba495
apply style guide-line
Nov 26, 2022
c5d9b98
remove @clone_on_remote_repo where not needed
Nov 27, 2022
bf898f2
remove @clone_on_remote_repo where not needed
Nov 27, 2022
c592fb1
fix tests
aguschin Nov 30, 2022
15e2a7d
moving further, but tests fail
aguschin Nov 30, 2022
d381b39
fixing most tests, improving the code
aguschin Dec 1, 2022
43941e7
fixes, unite two mixins
aguschin Dec 1, 2022
d88f114
fix some tests
Dec 1, 2022
a5e3e31
accept suggestion to increase performance in test_api by not re-creat…
francesco086 Dec 1, 2022
61bc125
fix tests by supporting push
aguschin Dec 4, 2022
a596659
fix test
aguschin Dec 5, 2022
9a4d536
skip tests failing on windows
aguschin Dec 5, 2022
70ad52e
Merge pull request #4 from aguschin/aguschin-fixes
francesco086 Dec 5, 2022
23caff3
delete clone_on_remote_repo
Dec 5, 2022
52ce96c
delete commit_produced_changes_on_commit
Dec 5, 2022
88cef78
delete push_on_push
Dec 5, 2022
5462a3d
delete set_push_on_remote_repo
Dec 5, 2022
383be81
add skip for windows
Dec 5, 2022
c7693c8
fix
aguschin Dec 6, 2022
78000a8
Merge pull request #6 from aguschin/aguschin-suggestions
francesco086 Dec 6, 2022
1491353
Merge pull request #5 from francesco086/delete_auto_push_on_remote
francesco086 Dec 6, 2022
1f0bc45
renaming
Dec 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
348 changes: 162 additions & 186 deletions gto/api.py

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions gto/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
VersionSort,
)
from gto.exceptions import GTOException, NotImplementedInGTO, WrongArgs
from gto.index import RepoIndexManager
from gto.ui import (
EMOJI_FAIL,
EMOJI_GTO,
Expand Down Expand Up @@ -857,10 +858,8 @@ def print_state(repo: str = option_repo):
@gto_command(hidden=True)
def print_index(repo: str = option_repo):
"""Technical cmd: Print repo index."""
index = gto.api._get_index( # pylint: disable=protected-access
repo
).artifact_centric_representation()
format_echo(index, "json")
with RepoIndexManager.from_repo(repo) as index:
format_echo(index.artifact_centric_representation(), "json")


@gto_command(section=CommandGroups.enriching)
Expand Down
223 changes: 72 additions & 151 deletions gto/git_utils.py
Original file line number Diff line number Diff line change
@@ -1,166 +1,84 @@
import inspect
import logging
from contextlib import contextmanager
from functools import wraps
from tempfile import TemporaryDirectory
from typing import Callable, Dict, List, Tuple
from typing import Callable, Dict, List, Tuple, Union

import git
from git import Repo

from gto.commit_message_generator import generate_empty_commit_message
from gto.config import RegistryConfig
from gto.constants import remote_git_repo_regex
from gto.exceptions import GTOException, WrongArgs


def clone_on_remote_repo(f: Callable):
@wraps(f)
def wrapped_f(*args, **kwargs):
kwargs = _turn_args_into_kwargs(f, args, kwargs)
class RemoteRepoMixin:
@classmethod
def from_local_repo(cls, repo: Union[str, Repo], config: RegistryConfig = None):
raise NotImplementedError()

if isinstance(kwargs["repo"], str) and is_url_of_remote_repo(
repo=kwargs["repo"]
):
@classmethod
@contextmanager
def from_repo(cls, repo: Union[str, Repo], config: RegistryConfig = None):
if isinstance(repo, str) and is_url_of_remote_repo(repo_path=repo):
try:
with cloned_git_repo(repo=kwargs["repo"]) as tmp_dir:
kwargs["repo"] = tmp_dir
return f(**kwargs)
with cloned_git_repo(repo=repo) as tmp_dir:
yield cls.from_local_repo(repo=tmp_dir, config=config)
except (NotADirectoryError, PermissionError) as e:
raise e.__class__(
"Are you using windows with python < 3.9? "
"This may be the reason of this error: https://bugs.python.org/issue42796. "
"Consider upgrading python."
) from e

return f(**kwargs)

return wrapped_f


def set_push_on_remote_repo(f: Callable):
@wraps(f)
def wrapped_f(*args, **kwargs):
kwargs = _turn_args_into_kwargs(f, args, kwargs)

if isinstance(kwargs["repo"], str) and is_url_of_remote_repo(
repo=kwargs["repo"]
else:
yield cls.from_local_repo(repo=repo, config=config)

def _call_commit_push(
self, func, commit=False, commit_message=None, push=False, **kwargs
):
if not (commit or push):
return func(**kwargs)
with stashed_changes(repo=self.repo, include_untracked=True) as (
stashed_tracked,
stashed_untracked,
):
kwargs["push"] = True

return f(**kwargs)

return wrapped_f


def commit_produced_changes_on_commit(
message_generator: Callable[..., str] = generate_empty_commit_message
):
"""
The function `message_generator` can use any argument that the decorated function has.

Example: here we are using the argument b of the function f to generate the commit message

def create_message(b: str) -> str:
return "commit message with b={b}"

@commit_produced_changes_on_commit(message_generator=create_message)
def f(a: str, b: str, c: str):
...

"""

def generate_commit_message(**kwargs) -> str:
kwargs_for_message_generator = {
k: kwargs.get(k, None)
for k in inspect.getfullargspec(message_generator).args
}
return message_generator(**kwargs_for_message_generator)

def wrap(f: Callable):
@wraps(f)
def wrapped_f(*args, **kwargs):
kwargs = _turn_args_into_kwargs(f, args, kwargs)

if kwargs.get("commit", False) is True:
if "repo" in kwargs:
with stashed_changes(
repo_path=kwargs["repo"], include_untracked=True
) as (stashed_tracked, stashed_untracked):
result = f(**kwargs)
if are_files_in_repo_changed(
repo_path=kwargs["repo"],
files=stashed_tracked + stashed_untracked,
):
_reset_repo_to_head(repo_path=kwargs["repo"])
raise GTOException(
msg="The command would have changed files that were not committed, "
"automated committing is not possible.\n"
"Suggested action: Commit the changes and re-run this command."
)
git_add_and_commit_all_changes(
repo_path=kwargs["repo"],
message=generate_commit_message(**kwargs),
)
else:
raise ValueError(
"Function decorated with commit_produced_changes_on_commit was called with "
"`commit=True` but `repo` was not provided."
"Argument `repo` is necessary."
)
else:
result = f(**kwargs)

return result

return wrapped_f

return wrap


def push_on_push(f: Callable):
@wraps(f)
def wrapped_f(*args, **kwargs):
kwargs = _turn_args_into_kwargs(f, args, kwargs)
if kwargs.get("push", False) is True:
kwargs["commit"] = True
result = f(**kwargs)
if "repo" in kwargs:
try:
git_push(repo_path=kwargs["repo"])
except Exception as e:
raise GTOException( # pylint: disable=raise-missing-from
"It was not possible to run `git push`. "
"The detailed error message was:\n"
f"{str(e)}"
)
else:
raise ValueError(
"Function decorated with push_on_push was called with "
"`push=True` but `repo` was not provided."
"Argument `repo` is necessary."
result = func(**kwargs)
if are_files_in_repo_changed(
repo=self.repo,
files=stashed_tracked + stashed_untracked,
):
_reset_repo_to_head(repo=self.repo)
raise GTOException(
msg="The command would have changed files that were not committed, "
"automated committing is not possible.\n"
"Suggested action: Commit the changes and re-run this command."
)
else:
result = f(**kwargs)
git_add_and_commit_all_changes(
repo=self.repo,
message=commit_message,
)
if push:
git_push(repo=self.repo)
return result

return wrapped_f


def are_files_in_repo_changed(repo_path: str, files: List[str]) -> bool:
tracked, untracked = _get_repo_changed_tracked_and_untracked_files(
repo_path=repo_path
)
def are_files_in_repo_changed(repo: Union[str, git.Repo], files: List[str]) -> bool:
tracked, untracked = _get_repo_changed_tracked_and_untracked_files(repo=repo)
return (
len(set(files).intersection(tracked)) > 0
or len(set(files).intersection(untracked)) > 0
)


def is_url_of_remote_repo(repo: str) -> bool:
if remote_git_repo_regex.fullmatch(repo) is not None:
logging.debug("%s recognized as remote git repo", repo)
def is_url_of_remote_repo(repo_path: str) -> bool:
if (
isinstance(repo_path, str)
and remote_git_repo_regex.fullmatch(repo_path) is not None
):
logging.debug("%s recognized as remote git repo", repo_path)
return True

logging.debug("%s NOT recognized as remote git repo", repo)
logging.debug("%s NOT recognized as remote git repo", repo_path)
return False


Expand All @@ -180,19 +98,22 @@ def git_clone(repo: str, dir: str) -> None:


def git_push_tag(
repo_path: str, tag_name: str, delete: bool = False, remote_name: str = "origin"
repo: Union[str, git.Repo],
tag_name: str,
delete: bool = False,
remote_name: str = "origin",
) -> None:
repo = git.Repo(path=repo_path)
repo = read_repo(repo)
remote = repo.remote(name=remote_name)
if not hasattr(remote, "url"):
raise WrongArgs(
f"provided repo_path={repo_path} does not appear to have a remote to push to"
f"provided repo={repo} does not appear to have a remote to push to"
)
logging.debug(
"push %s tag %s from directory %s to remote %s with url %s",
"--delete" if delete else "",
tag_name,
repo_path,
repo.working_dir,
remote_name,
remote.url,
)
Expand All @@ -207,33 +128,33 @@ def git_push_tag(
)


def git_push(repo_path: str) -> None:
git.Repo(path=repo_path).git.push()
def git_push(repo: Union[str, git.Repo]) -> None:
read_repo(repo).git.push()


def git_add_and_commit_all_changes(repo_path: str, message: str) -> None:
repo = git.Repo(path=repo_path)
tracked, untracked = _get_repo_changed_tracked_and_untracked_files(
repo_path=repo_path
)
def git_add_and_commit_all_changes(repo: Union[str, git.Repo], message: str) -> None:
repo = read_repo(repo)
tracked, untracked = _get_repo_changed_tracked_and_untracked_files(repo=repo)
if len(tracked) + len(untracked) > 0:
logging.debug("Adding to the index the untracked files %s", untracked)
logging.debug("Add and commit changes to files %s", tracked + untracked)
repo.index.add(items=tracked + untracked)
repo.index.commit(message=message)


def read_repo(repo: Union[str, git.Repo]) -> git.Repo:
return git.Repo(path=repo) if isinstance(repo, str) else repo


@contextmanager
def stashed_changes(repo_path: str, include_untracked: bool = False):
repo = git.Repo(path=repo_path)
def stashed_changes(repo: Union[str, git.Repo], include_untracked: bool = False):
repo = read_repo(repo)
if len(repo.refs) == 0:
raise RuntimeError(
"Cannot stash because repository has no ref. Please create a first commit."
)

tracked, untracked = _get_repo_changed_tracked_and_untracked_files(
repo_path=repo_path
)
tracked, untracked = _get_repo_changed_tracked_and_untracked_files(repo=repo)

stash_arguments = ["push"]
if include_untracked:
Expand All @@ -250,16 +171,16 @@ def stashed_changes(repo_path: str, include_untracked: bool = False):
repo.git.stash("pop")


def _reset_repo_to_head(repo_path: str) -> None:
repo = git.Repo(path=repo_path)
def _reset_repo_to_head(repo: Union[str, git.Repo]) -> None:
repo = read_repo(repo)
repo.git.stash(["push", "--include-untracked"])
repo.git.stash(["drop"])


def _get_repo_changed_tracked_and_untracked_files(
repo_path: str,
repo: Union[str, git.Repo],
) -> Tuple[List[str], List[str]]:
repo = git.Repo(path=repo_path)
repo = read_repo(repo)
return [item.a_path for item in repo.index.diff(None)], repo.untracked_files


Expand Down
Loading