diff --git a/CHANGES b/CHANGES index 3242cd546..199295145 100644 --- a/CHANGES +++ b/CHANGES @@ -29,12 +29,17 @@ URL renamings (#417): ### Improvements -- URLs (#423): - - `hg`: Add `HgBaseURL`, `HgPipURL` - - `svn`: Add `SvnBaseURL`, `SvnPipURL` - - `URLProtocol`: Fix `is_valid` to use `classmethod` - - All: Fix `is_valid` to use default of `None` to avoid implicitly filtering - - Reduce duplicated code in methods by using `super()` +Sync: + +- `git`: Fix `update_repo` when there are only untracked files (#425, credit: @jfpedroza) + +URLs (#423): + +- `hg`: Add `HgBaseURL`, `HgPipURL` +- `svn`: Add `SvnBaseURL`, `SvnPipURL` +- `URLProtocol`: Fix `is_valid` to use `classmethod` +- All: Fix `is_valid` to use default of `None` to avoid implicitly filtering +- Reduce duplicated code in methods by using `super()` ### Packaging diff --git a/poetry.lock b/poetry.lock index cae14f4ff..29a4a0fe5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -195,7 +195,7 @@ sphinx-basic-ng = "*" [[package]] name = "gp-libs" -version = "0.0.1a11" +version = "0.0.1a12" description = "Internal utilities for projects following git-pull python package spec" category = "dev" optional = false @@ -629,7 +629,7 @@ python-versions = ">=3.6" [[package]] name = "Sphinx" -version = "5.2.0.post0" +version = "5.2.1" description = "Python documentation generator" category = "dev" optional = false @@ -1042,8 +1042,8 @@ furo = [ {file = "furo-2022.9.15.tar.gz", hash = "sha256:4a7ef1c8a3b615171592da4d2ad8a53cc4aacfbe111958890f5f9ff7279066ab"}, ] gp-libs = [ - {file = "gp-libs-0.0.1a11.tar.gz", hash = "sha256:2fa1d886aae88f17614c052652509ce6347adc5f39e1b35223ef0ee2b56069e5"}, - {file = "gp_libs-0.0.1a11-py3-none-any.whl", hash = "sha256:75248e0409e8af142cd2ccdbf3382300d26eb73f0155b0de721368c3543e5c35"}, + {file = "gp-libs-0.0.1a12.tar.gz", hash = "sha256:3a9a3018fa524a0008dd2a88197b2ab503a769bfa780337bf00f5753e1b95552"}, + {file = "gp_libs-0.0.1a12-py3-none-any.whl", hash = "sha256:7115eb6f65de812352fd08da1316a31458d3ceedede3fb9f7f4d2236aae0ca27"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -1279,8 +1279,8 @@ soupsieve = [ {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, ] Sphinx = [ - {file = "Sphinx-5.2.0.post0.tar.gz", hash = "sha256:68e7833263a961521f45302fa87285f9395ecf385f1eefd85cd61ddff0b15bc1"}, - {file = "sphinx-5.2.0.post0-py3-none-any.whl", hash = "sha256:db93dc52cc90d12ef38c9f506eab9171813041204d8270e30ffad2be511e7ced"}, + {file = "Sphinx-5.2.1.tar.gz", hash = "sha256:c009bb2e9ac5db487bcf53f015504005a330ff7c631bb6ab2604e0d65bae8b54"}, + {file = "sphinx-5.2.1-py3-none-any.whl", hash = "sha256:3dcf00fcf82cf91118db9b7177edea4fc01998976f893928d0ab0c58c54be2ca"}, ] sphinx-autobuild = [ {file = "sphinx-autobuild-2021.3.14.tar.gz", hash = "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"}, diff --git a/pyproject.toml b/pyproject.toml index ef2b65834..db35e0d07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "libvcs" -version = "0.17.0a0" +version = "0.17.0a1" description = "Lite, typed, python utilities for Git, SVN, Mercurial, etc." license = "MIT" authors = ["Tony Narlock "] diff --git a/src/libvcs/__about__.py b/src/libvcs/__about__.py index 94866e464..64d53ef9f 100644 --- a/src/libvcs/__about__.py +++ b/src/libvcs/__about__.py @@ -1,7 +1,7 @@ __title__ = "libvcs" __package_name__ = "libvcs" __description__ = "Lite, typed, python utilities for Git, SVN, Mercurial, etc." -__version__ = "0.17.0a0" +__version__ = "0.17.0a1" __author__ = "Tony Narlock" __github__ = "https://github.com/vcs-python/libvcs" __docs__ = "https://libvcs.git-pull.com" diff --git a/src/libvcs/pytest_plugin.py b/src/libvcs/pytest_plugin.py index d84891f53..303171547 100644 --- a/src/libvcs/pytest_plugin.py +++ b/src/libvcs/pytest_plugin.py @@ -5,7 +5,7 @@ import random import shutil import textwrap -from typing import Any, Optional, Protocol +from typing import TYPE_CHECKING, Any, Optional, Protocol import pytest @@ -14,6 +14,9 @@ from libvcs.sync.hg import HgSync from libvcs.sync.svn import SvnSync +if TYPE_CHECKING: + from typing_extensions import TypeAlias + skip_if_git_missing = pytest.mark.skipif( not shutil.which("git"), reason="git is not available" ) @@ -181,6 +184,9 @@ def unique_repo_name(remote_repos_path: pathlib.Path, max_retries: int = 15) -> return remote_repo_name +InitCmdArgs: "TypeAlias" = Optional[list[str]] + + class CreateProjectCallbackProtocol(Protocol): def __call__(self, remote_repo_path: pathlib.Path) -> None: ... @@ -192,6 +198,7 @@ def __call__( remote_repos_path: pathlib.Path = ..., remote_repo_name: Optional[str] = ..., remote_repo_post_init: Optional[CreateProjectCallbackProtocol] = ..., + init_cmd_args: InitCmdArgs = ..., ) -> pathlib.Path: ... @@ -200,9 +207,12 @@ def _create_git_remote_repo( remote_repos_path: pathlib.Path, remote_repo_name: str, remote_repo_post_init: Optional[CreateProjectCallbackProtocol] = None, + init_cmd_args: InitCmdArgs = ["--bare"], ) -> pathlib.Path: + if init_cmd_args is None: + init_cmd_args = [] remote_repo_path = remote_repos_path / remote_repo_name - run(["git", "init", remote_repo_name], cwd=remote_repos_path) + run(["git", "init", remote_repo_name, *init_cmd_args], cwd=remote_repos_path) if remote_repo_post_init is not None and callable(remote_repo_post_init): remote_repo_post_init(remote_repo_path=remote_repo_path) @@ -221,6 +231,7 @@ def fn( remote_repos_path: pathlib.Path = remote_repos_path, remote_repo_name: Optional[str] = None, remote_repo_post_init: Optional[CreateProjectCallbackProtocol] = None, + init_cmd_args: InitCmdArgs = ["--bare"], ) -> pathlib.Path: return _create_git_remote_repo( remote_repos_path=remote_repos_path, @@ -228,6 +239,7 @@ def fn( if remote_repo_name is not None else unique_repo_name(remote_repos_path=remote_repos_path), remote_repo_post_init=remote_repo_post_init, + init_cmd_args=init_cmd_args, ) return fn @@ -249,6 +261,7 @@ def git_remote_repo(remote_repos_path: pathlib.Path) -> pathlib.Path: remote_repos_path=remote_repos_path, remote_repo_name="dummyrepo", remote_repo_post_init=git_remote_repo_single_commit_post_init, + init_cmd_args=None, # Don't do --bare ) @@ -256,11 +269,14 @@ def _create_svn_remote_repo( remote_repos_path: pathlib.Path, remote_repo_name: str, remote_repo_post_init: Optional[CreateProjectCallbackProtocol] = None, + init_cmd_args: InitCmdArgs = None, ) -> pathlib.Path: """Create a test SVN repo to for checkout / commit purposes""" + if init_cmd_args is None: + init_cmd_args = [] remote_repo_path = remote_repos_path / remote_repo_name - run(["svnadmin", "create", remote_repo_path]) + run(["svnadmin", "create", remote_repo_path, *init_cmd_args]) if remote_repo_post_init is not None and callable(remote_repo_post_init): remote_repo_post_init(remote_repo_path=remote_repo_path) @@ -279,6 +295,7 @@ def fn( remote_repos_path: pathlib.Path = remote_repos_path, remote_repo_name: Optional[str] = None, remote_repo_post_init: Optional[CreateProjectCallbackProtocol] = None, + init_cmd_args: InitCmdArgs = None, ) -> pathlib.Path: return _create_svn_remote_repo( remote_repos_path=remote_repos_path, @@ -286,6 +303,7 @@ def fn( if remote_repo_name is not None else unique_repo_name(remote_repos_path=remote_repos_path), remote_repo_post_init=remote_repo_post_init, + init_cmd_args=init_cmd_args, ) return fn @@ -309,10 +327,14 @@ def _create_hg_remote_repo( remote_repos_path: pathlib.Path, remote_repo_name: str, remote_repo_post_init: Optional[CreateProjectCallbackProtocol] = None, + init_cmd_args: InitCmdArgs = None, ) -> pathlib.Path: """Create a test hg repo to for checkout / commit purposes""" + if init_cmd_args is None: + init_cmd_args = [] + remote_repo_path = remote_repos_path / remote_repo_name - run(["hg", "init", remote_repo_name], cwd=remote_repos_path) + run(["hg", "init", remote_repo_name, *init_cmd_args], cwd=remote_repos_path) if remote_repo_post_init is not None and callable(remote_repo_post_init): remote_repo_post_init(remote_repo_path=remote_repo_path) @@ -340,6 +362,7 @@ def fn( remote_repos_path: pathlib.Path = remote_repos_path, remote_repo_name: Optional[str] = None, remote_repo_post_init: Optional[CreateProjectCallbackProtocol] = None, + init_cmd_args: InitCmdArgs = None, ) -> pathlib.Path: return _create_hg_remote_repo( remote_repos_path=remote_repos_path, @@ -347,6 +370,7 @@ def fn( if remote_repo_name is not None else unique_repo_name(remote_repos_path=remote_repos_path), remote_repo_post_init=remote_repo_post_init, + init_cmd_args=init_cmd_args, ) return fn @@ -427,6 +451,7 @@ def add_doctest_fixtures( doctest_namespace["create_git_remote_repo"] = functools.partial( create_git_remote_repo, remote_repo_post_init=git_remote_repo_single_commit_post_init, + init_cmd_args=None, ) doctest_namespace["create_git_remote_repo_bare"] = create_git_remote_repo doctest_namespace["git_local_clone"] = git_repo diff --git a/src/libvcs/sync/git.py b/src/libvcs/sync/git.py index c54a70d63..3c019e2ad 100644 --- a/src/libvcs/sync/git.py +++ b/src/libvcs/sync/git.py @@ -408,7 +408,7 @@ def update_repo(self, set_remotes: bool = False, *args: Any, **kwargs: Any) -> N if is_remote_ref: # Check if stash is needed try: - process = self.run(["status", "--porcelain"]) + process = self.run(["status", "--porcelain", "--untracked-files=no"]) except exc.CommandError: self.log.error("Failed to get the status") return @@ -435,7 +435,7 @@ def update_repo(self, set_remotes: bool = False, *args: Any, **kwargs: Any) -> N try: process = self.run(["rebase", git_remote_name + "/" + git_tag]) except exc.CommandError as e: - if "invalid_upstream" in str(e): + if any(msg in str(e) for msg in ["invalid_upstream", "Aborting"]): self.log.error(e) else: # Rebase failed: Restore previous state. diff --git a/tests/sync/test_git.py b/tests/sync/test_git.py index 7c1fb4cbe..fe2597683 100644 --- a/tests/sync/test_git.py +++ b/tests/sync/test_git.py @@ -2,6 +2,7 @@ import datetime import os import pathlib +import random import shutil import textwrap from typing import Callable, TypedDict @@ -141,6 +142,7 @@ def test_repo_update_handle_cases( ) -> None: git_repo: GitSync = constructor(**lazy_constructor_options(**locals())) git_repo.obtain() # clone initial repo + mocka = mocker.spy(git_repo, "run") git_repo.update_repo() @@ -154,6 +156,69 @@ def test_repo_update_handle_cases( assert mocker.call(["symbolic-ref", "--short", "HEAD"]) not in mocka.mock_calls +@pytest.mark.parametrize( + "has_untracked_files,needs_stash,has_remote_changes", + [ + [True, True, True], + [True, True, False], + [True, False, True], + [True, False, False], + [False, True, True], + [False, True, False], + [False, False, True], + [False, False, False], + ], +) +def test_repo_update_stash_cases( + tmp_path: pathlib.Path, + create_git_remote_repo: CreateProjectCallbackFixtureProtocol, + mocker: MockerFixture, + has_untracked_files: bool, + needs_stash: bool, + has_remote_changes: bool, +) -> None: + git_remote_repo = create_git_remote_repo() + + git_repo: GitSync = GitSync( + url=f"file://{git_remote_repo}", + dir=tmp_path / "myrepo", + vcs="git", + ) + git_repo.obtain() # clone initial repo + + def make_file(filename: str) -> pathlib.Path: + some_file = git_repo.dir.joinpath(filename) + with open(some_file, "w") as file: + file.write("some content: " + str(random.random())) + + return some_file + + # Make an initial commit so we can reset + some_file = make_file("initial_file") + git_repo.run(["add", some_file]) + git_repo.run(["commit", "-m", "a commit"]) + git_repo.run(["push"]) + + if has_remote_changes: + some_file = make_file("some_file") + git_repo.run(["add", some_file]) + git_repo.run(["commit", "-m", "a commit"]) + git_repo.run(["push"]) + git_repo.run(["reset", "--hard", "HEAD^"]) + + if has_untracked_files: + make_file("some_file") + + if needs_stash: + some_file = make_file("some_stashed_file") + git_repo.run(["add", some_file]) + + mocka = mocker.spy(git_repo, "run") + git_repo.update_repo() + + mocka.assert_any_call(["symbolic-ref", "--short", "HEAD"]) + + @pytest.mark.parametrize( # Postpone evaluation of options so fixture variables can interpolate "constructor,lazy_constructor_options",