From 6627c74eede55f37304fc218cc88fa3485064908 Mon Sep 17 00:00:00 2001 From: Antti Kaihola <13725+akaihola@users.noreply.github.com> Date: Sun, 3 Mar 2024 19:39:45 +0200 Subject: [PATCH] Move some Git helpers to Darkgraylib --- src/darker/__main__.py | 12 +- src/darker/git.py | 256 +---------------- src/darker/tests/test_git.py | 517 +--------------------------------- src/darker/tests/test_main.py | 4 +- 4 files changed, 26 insertions(+), 763 deletions(-) diff --git a/src/darker/__main__.py b/src/darker/__main__.py index e0fbaff80..76c3702d6 100644 --- a/src/darker/__main__.py +++ b/src/darker/__main__.py @@ -24,13 +24,9 @@ from darker.exceptions import DependencyError, MissingPackageError from darker.fstring import apply_flynt, flynt from darker.git import ( - PRE_COMMIT_FROM_TO_REFS, - STDIN, - WORKTREE, EditedLinenumsDiffer, get_missing_at_revision, get_path_in_repo, - git_get_content_at_revision, git_get_modified_python_files, git_is_repository, ) @@ -40,7 +36,13 @@ from darker.verification import ASTVerifier, BinarySearch, NotEquivalentError from darkgraylib.black_compat import find_project_root from darkgraylib.config import show_config_if_debug -from darkgraylib.git import RevisionRange +from darkgraylib.git import ( + PRE_COMMIT_FROM_TO_REFS, + STDIN, + WORKTREE, + RevisionRange, + git_get_content_at_revision, +) from darkgraylib.highlighting import colorize, should_use_color from darkgraylib.log import setup_logging from darkgraylib.main import resolve_paths diff --git a/src/darker/git.py b/src/darker/git.py index 1fac38ac0..fff2eb1d6 100644 --- a/src/darker/git.py +++ b/src/darker/git.py @@ -1,35 +1,22 @@ """Helpers for listing modified files and getting unmodified content from Git""" import logging -import os -import re -import shlex -import sys -from contextlib import contextmanager from dataclasses import dataclass -from datetime import datetime from functools import lru_cache from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, check_output, run # nosec -from typing import ( - Dict, - Iterable, - Iterator, - List, - Match, - Optional, - Set, - Tuple, - Union, - cast, - overload, -) +from subprocess import DEVNULL, CalledProcessError, run # nosec +from typing import Iterable, List, Set from darker.diff import opcodes_to_edit_linenums from darker.multiline_strings import get_multiline_string_ranges -from darker.utils import GIT_DATEFORMAT from darkgraylib.diff import diff_and_get_opcodes -from darkgraylib.git import RevisionRange +from darkgraylib.git import ( + WORKTREE, + RevisionRange, + git_check_output_lines, + git_get_content_at_revision, + make_git_env, +) from darkgraylib.utils import TextDocument logger = logging.getLogger(__name__) @@ -39,55 +26,18 @@ # Handles these cases: # .. .. .. # ... ... ... -COMMIT_RANGE_RE = re.compile(r"(.*?)(\.{2,3})(.*)$") # A colon is an invalid character in tag/branch names. Use that in the special value for # - denoting the working tree as one of the "revisions" in revision ranges # - referring to the `PRE_COMMIT_FROM_REF` and `PRE_COMMIT_TO_REF` environment variables # for determining the revision range -WORKTREE = ":WORKTREE:" -STDIN = ":STDIN:" -PRE_COMMIT_FROM_TO_REFS = ":PRE-COMMIT:" - - -def git_get_version() -> Tuple[int, ...]: - """Return the Git version as a tuple of integers - - Ignores any suffixes to the dot-separated parts of the version string. - - :return: The version number of Git installed on the system - :raise: ``RuntimeError`` if unable to parse the Git version - - """ - output_lines = _git_check_output_lines(["--version"], Path(".")) - version_string = output_lines[0].rsplit(None, 1)[-1] - # The version string might be e.g. - # - "2.39.0.windows.1" - # - "2.36.2" - part_matches = [re.match(r"\d+", part) for part in version_string.split(".")][:3] - if all(part_matches): - return tuple( - int(match.group(0)) for match in cast(List[Match[str]], part_matches) - ) - raise RuntimeError(f"Unable to parse Git version: {output_lines!r}") - - -def git_rev_parse(revision: str, cwd: Path) -> str: - """Return the commit hash for the given revision - - :param revision: The revision to get the commit hash for - :param cwd: The root of the Git repository - :return: The commit hash for ``revision`` as parsed from Git output - - """ - return _git_check_output_lines(["rev-parse", revision], cwd)[0] def git_is_repository(path: Path) -> bool: """Return ``True`` if ``path`` is inside a Git working tree""" try: - lines = _git_check_output_lines( + lines = git_check_output_lines( ["rev-parse", "--is-inside-work-tree"], path, exit_on_error=False ) return lines[:1] == ["true"] @@ -99,52 +49,6 @@ def git_is_repository(path: Path) -> bool: return False -def git_get_mtime_at_commit(path: Path, revision: str, cwd: Path) -> str: - """Return the committer date of the given file at the given revision - - :param path: The relative path of the file in the Git repository - :param revision: The Git revision for which to get the file modification time - :param cwd: The root of the Git repository - - """ - cmd = ["log", "-1", "--format=%ct", revision, "--", path.as_posix()] - lines = _git_check_output_lines(cmd, cwd) - return datetime.utcfromtimestamp(int(lines[0])).strftime(GIT_DATEFORMAT) - - -def git_get_content_at_revision(path: Path, revision: str, cwd: Path) -> TextDocument: - """Get unmodified text lines of a file at a Git revision - - :param path: The relative path of the file in the Git repository - :param revision: The Git revision for which to get the file content, or ``WORKTREE`` - to get what's on disk right now. - :param cwd: The root of the Git repository - - """ - if path.is_absolute(): - raise ValueError( - f"the 'path' parameter must receive a relative path, got {path!r} instead" - ) - - if revision == WORKTREE: - abspath = cwd / path - return TextDocument.from_file(abspath) - cmd = ["show", f"{revision}:./{path.as_posix()}"] - try: - return TextDocument.from_bytes( - _git_check_output(cmd, cwd, exit_on_error=False), - mtime=git_get_mtime_at_commit(path, revision, cwd), - ) - except CalledProcessError as exc_info: - if exc_info.returncode != 128: - for error_line in exc_info.stderr.splitlines(): - logger.error(error_line) - raise - # The file didn't exist at the given revision. Act as if it was an empty - # file, so all current lines appear as edited. - return TextDocument() - - def get_path_in_repo(path: Path) -> Path: """Return the relative path to the file in the old revision @@ -172,79 +76,6 @@ def should_reformat_file(path: Path) -> bool: return path.exists() and get_path_in_repo(path).suffix == ".py" -@lru_cache(maxsize=1) -def _make_git_env() -> Dict[str, str]: - """Create custom minimal environment variables to use when invoking Git - - This makes sure that - - Git always runs in English - - ``$PATH`` is preserved (essential on NixOS) - - the environment is otherwise cleared - - """ - return {"LC_ALL": "C", "PATH": os.environ["PATH"]} - - -def _git_check_output_lines( - cmd: List[str], cwd: Path, exit_on_error: bool = True -) -> List[str]: - """Log command line, run Git, split stdout to lines, exit with 123 on error""" - return _git_check_output( - cmd, - cwd, - exit_on_error=exit_on_error, - encoding="utf-8", - ).splitlines() - - -@overload -def _git_check_output( - cmd: List[str], cwd: Path, *, exit_on_error: bool = ..., encoding: None = ... -) -> bytes: - ... - - -@overload -def _git_check_output( - cmd: List[str], cwd: Path, *, exit_on_error: bool = ..., encoding: str -) -> str: - ... - - -def _git_check_output( - cmd: List[str], - cwd: Path, - *, - exit_on_error: bool = True, - encoding: Optional[str] = None, -) -> Union[str, bytes]: - """Log command line, run Git, return stdout, exit with 123 on error""" - logger.debug("[%s]$ git %s", cwd, shlex.join(cmd)) - try: - return check_output( # nosec - ["git"] + cmd, - cwd=str(cwd), - encoding=encoding, - stderr=PIPE, - env=_make_git_env(), - ) - except CalledProcessError as exc_info: - if not exit_on_error: - raise - if exc_info.returncode != 128: - if encoding: - sys.stderr.write(exc_info.stderr) - else: - sys.stderr.buffer.write(exc_info.stderr) - raise - - # Bad revision or another Git failure. Follow Black's example and return the - # error status 123. - for error_line in exc_info.stderr.splitlines(): - logger.error(error_line) - sys.exit(123) - - def _git_exists_in_revision(path: Path, rev2: str, cwd: Path) -> bool: """Return ``True`` if the given path exists in the given Git revision @@ -266,7 +97,7 @@ def _git_exists_in_revision(path: Path, rev2: str, cwd: Path) -> bool: cwd=str(cwd), check=False, stderr=DEVNULL, - env=_make_git_env(), + env=make_git_env(), ) return result.returncode == 0 @@ -312,7 +143,7 @@ def _git_diff_name_only( ] if rev2 != WORKTREE: diff_cmd.insert(diff_cmd.index("--"), rev2) - lines = _git_check_output_lines(diff_cmd, cwd) + lines = git_check_output_lines(diff_cmd, cwd) return {Path(line) for line in lines} @@ -334,7 +165,7 @@ def _git_ls_files_others(relative_paths: Iterable[Path], cwd: Path) -> Set[Path] "--", *{path.as_posix() for path in relative_paths}, ] - lines = _git_check_output_lines(ls_files_cmd, cwd) + lines = git_check_output_lines(ls_files_cmd, cwd) return {Path(line) for line in lines} @@ -358,67 +189,6 @@ def git_get_modified_python_files( return {path for path in changed_paths if should_reformat_file(cwd / path)} -@contextmanager -def git_clone_local( - source_repository: Path, revision: str, destination: Path -) -> Iterator[Path]: - """Clone a local repository and check out the given revision - - :param source_repository: Path to the root of the local repository checkout - :param revision: The revision to check out, or ``HEAD`` - :param destination: Directory to create for the clone - :return: A context manager which yields the path to the clone - - """ - opts = [ - # By default, `add` refuses to create a new worktree when `` is - # a branch name and is already checked out by another worktree, or if - # `` is already assigned to some worktree but is missing (for - # instance, if `` was deleted manually). This option overrides these - # safeguards. To add a missing but locked worktree path, specify `--force` - # twice. - # `remove` refuses to remove an unclean worktree unless `--force` is used. - # To remove a locked worktree, specify `--force` twice. - # https://git-scm.com/docs/git-worktree#_options - "--force", - "--force", - str(destination), - ] - _ = _git_check_output( - ["worktree", "add", "--quiet", *opts, revision], cwd=source_repository - ) - yield destination - _ = _git_check_output(["worktree", "remove", *opts], cwd=source_repository) - - -def git_get_root(path: Path) -> Optional[Path]: - """Get the root directory of a local Git repository clone based on a path inside it - - :param path: A file or directory path inside the Git repository clone - :return: The root of the clone, or ``None`` if none could be found - :raises CalledProcessError: if Git exits with an unexpected error - - """ - try: - return Path( - _git_check_output( - ["rev-parse", "--show-toplevel"], - cwd=path if path.is_dir() else path.parent, - encoding="utf-8", - exit_on_error=False, - ).rstrip() - ) - except CalledProcessError as exc_info: - if exc_info.returncode == 128 and exc_info.stderr.splitlines()[0].startswith( - "fatal: not a git repository (or any " - ): - # The error string differs a bit in different Git versions, but up to the - # point above it's identical in recent versions. - return None - sys.stderr.write(exc_info.stderr) - raise - - def _revision_vs_lines( root: Path, path_in_repo: Path, rev1: str, content: TextDocument, context_lines: int ) -> List[int]: diff --git a/src/darker/tests/test_git.py b/src/darker/tests/test_git.py index 72b2594f5..8601faff3 100644 --- a/src/darker/tests/test_git.py +++ b/src/darker/tests/test_git.py @@ -4,187 +4,19 @@ # pylint: disable=too-many-lines,use-dict-literal import os -import re -from datetime import datetime, timedelta from pathlib import Path -from subprocess import DEVNULL, PIPE, CalledProcessError, check_call # nosec +from subprocess import DEVNULL, check_call # nosec from textwrap import dedent # nosec -from typing import List, Union -from unittest.mock import ANY, Mock, call, patch +from unittest.mock import patch import pytest from darker import git -from darker.utils import GIT_DATEFORMAT -from darkgraylib.git import RevisionRange +from darkgraylib.git import WORKTREE, RevisionRange from darkgraylib.testtools.git_repo_plugin import GitRepoFixture -from darkgraylib.testtools.helpers import raises_or_matches from darkgraylib.utils import TextDocument -def test_tmp_path_sanity(tmp_path): - """Make sure Pytest temporary directories aren't inside a Git repository""" - try: - result = git._git_check_output_lines( - ["rev-parse", "--absolute-git-dir"], tmp_path, exit_on_error=False - ) - except CalledProcessError as exc_info: - if exc_info.returncode != 128 or not exc_info.stderr.startswith( - "fatal: not a git repository" - ): - raise - else: - output = "\n".join(result) - raise AssertionError( - f"Temporary directory {tmp_path} for tests is not clean." - f" There is a Git directory in {output}" - ) - - -@pytest.mark.parametrize( - "revision_range, expect", - [ - ("", None), - ("..", ("", "..", "")), - ("...", ("", "...", "")), - ("a..", ("a", "..", "")), - ("a...", ("a", "...", "")), - ("a..b", ("a", "..", "b")), - ("a...b", ("a", "...", "b")), - ("..b", ("", "..", "b")), - ("...b", ("", "...", "b")), - ], -) -def test_commit_range_re(revision_range, expect): - """Test for ``COMMIT_RANGE_RE``""" - match = git.COMMIT_RANGE_RE.match(revision_range) - if expect is None: - assert match is None - else: - assert match is not None - assert match.groups() == expect - - -def test_worktree_symbol(): - """Test for the ``WORKTREE`` symbol""" - assert git.WORKTREE == ":WORKTREE:" - - -def test_git_get_mtime_at_commit(): - """darker.git.git_get_mtime_at_commit()""" - with patch.object(git, "_git_check_output_lines"): - git._git_check_output_lines.return_value = ["1609104839"] # type: ignore - - result = git.git_get_mtime_at_commit( - Path("dummy path"), "dummy revision", Path("dummy cwd") - ) - assert result == "2020-12-27 21:33:59.000000 +0000" - - -@pytest.mark.kwparametrize( - dict( - revision=":WORKTREE:", - expect_lines=("new content",), - expect_mtime=lambda: datetime(2001, 9, 9, 1, 46, 40), - ), - dict( - revision="HEAD", - expect_lines=("modified content",), - expect_mtime=datetime.utcnow, - ), - dict( - revision="HEAD^", - expect_lines=("original content",), - expect_mtime=datetime.utcnow, - ), - dict(revision="HEAD~2", expect_lines=(), expect_mtime=False), -) -def test_git_get_content_at_revision(git_repo, revision, expect_lines, expect_mtime): - """darker.git.git_get_content_at_revision()""" - git_repo.add({"my.txt": "original content"}, commit="Initial commit") - paths = git_repo.add({"my.txt": "modified content"}, commit="Initial commit") - paths["my.txt"].write_bytes(b"new content") - os.utime(paths["my.txt"], (1000000000, 1000000000)) - - result = git.git_get_content_at_revision( - Path("my.txt"), revision, cwd=Path(git_repo.root) - ) - - assert result.lines == expect_lines - if expect_mtime: - mtime_then = datetime.strptime(result.mtime, GIT_DATEFORMAT) - difference = expect_mtime() - mtime_then - assert timedelta(0) <= difference < timedelta(seconds=6) - else: - assert result.mtime == "" - assert result.encoding == "utf-8" - - -def git_call(cmd, encoding=None): - """Returns a mocked call to git""" - return call( - cmd.split(), - cwd=str(Path("/path")), - encoding=encoding, - stderr=PIPE, - env={"LC_ALL": "C", "PATH": os.environ["PATH"]}, - ) - - -@pytest.mark.kwparametrize( - dict( - revision=":WORKTREE:", - expect_textdocument_calls=[call.from_file(Path("/path/my.txt"))], - ), - dict( - revision="HEAD", - expect_git_calls=[ - git_call("git show HEAD:./my.txt"), - git_call("git log -1 --format=%ct HEAD -- my.txt", encoding="utf-8"), - ], - expect_textdocument_calls=[ - call.from_bytes(b"1627107028", mtime="2021-07-24 06:10:28.000000 +0000") - ], - ), - dict( - revision="HEAD^", - expect_git_calls=[ - git_call("git show HEAD^:./my.txt"), - git_call("git log -1 --format=%ct HEAD^ -- my.txt", encoding="utf-8"), - ], - expect_textdocument_calls=[ - call.from_bytes(b"1627107028", mtime="2021-07-24 06:10:28.000000 +0000") - ], - ), - dict( - revision="master", - expect_git_calls=[ - git_call("git show master:./my.txt"), - git_call("git log -1 --format=%ct master -- my.txt", encoding="utf-8"), - ], - expect_textdocument_calls=[ - call.from_bytes(b"1627107028", mtime="2021-07-24 06:10:28.000000 +0000") - ], - ), - expect_git_calls=[], -) -def test_git_get_content_at_revision_obtain_file_content( - revision, expect_git_calls, expect_textdocument_calls -): - """``git_get_content_at_revision`` calls Git or reads files based on revision""" - with patch("darker.git.check_output") as check_output, patch( - "darker.git.TextDocument" - ) as text_document_class: - # this dummy value acts both as a dummy Unix timestamp for the file as well as - # the contents of the file: - check_output.return_value = b"1627107028" - - git.git_get_content_at_revision(Path("my.txt"), revision, Path("/path")) - - assert check_output.call_args_list == expect_git_calls - assert text_document_class.method_calls == expect_textdocument_calls - - @pytest.mark.kwparametrize( dict(path="file.py", expect="file.py"), dict(path="subdir/file.py", expect="subdir/file.py"), @@ -224,185 +56,6 @@ def test_should_reformat_file(tmpdir, path, create, expect): assert result == expect -@pytest.mark.kwparametrize( - dict(cmd=[], exit_on_error=True, expect_template=CalledProcessError(1, "")), - dict( - cmd=["status", "-sb"], - exit_on_error=True, - expect_template=[ - "## branch", - "A add_index.py", - "D del_index.py", - " D del_worktree.py", - "A mod_index.py", - "?? add_worktree.py", - "?? mod_worktree.py", - ], - ), - dict( - cmd=["diff"], - exit_on_error=True, - expect_template=[ - "diff --git a/del_worktree.py b/del_worktree.py", - "deleted file mode 100644", - "index 94f3610..0000000", - "--- a/del_worktree.py", - "+++ /dev/null", - "@@ -1 +0,0 @@", - "-original", - "\\ No newline at end of file", - ], - ), - dict( - cmd=["merge-base", "master"], - exit_on_error=True, - expect_template=CalledProcessError(129, ""), - ), - dict( - cmd=["merge-base", "master", "HEAD"], - exit_on_error=True, - expect_template=[""], - ), - dict( - cmd=["show", "missing.file"], - exit_on_error=True, - expect_template=SystemExit(123), - ), - dict( - cmd=["show", "missing.file"], - exit_on_error=False, - expect_template=CalledProcessError(128, ""), - ), -) -def test_git_check_output_lines(branched_repo, cmd, exit_on_error, expect_template): - """Unit test for :func:`_git_check_output_lines`""" - if isinstance(expect_template, BaseException): - expect: Union[List[str], BaseException] = expect_template - else: - replacements = {"": branched_repo.get_hash("master^")} - expect = [replacements.get(line, line) for line in expect_template] - with raises_or_matches(expect, ["returncode", "code"]) as check: - - check(git._git_check_output_lines(cmd, branched_repo.root, exit_on_error)) - - -@pytest.mark.kwparametrize( - dict( - cmd=["show", "{initial}:/.file2"], - exit_on_error=True, - expect_exc=SystemExit, - expect_log=( - r"(?:^|\n)ERROR darker\.git:git\.py:\d+ fatal: " - r"[pP]ath '/\.file2' does not exist in '{initial}'\n" - ), - ), - dict( - cmd=["show", "{initial}:/.file2"], - exit_on_error=False, - expect_exc=CalledProcessError, - ), - dict( - cmd=["non-existing", "command"], - exit_on_error=True, - expect_exc=CalledProcessError, - expect_stderr="git: 'non-existing' is not a git command. See 'git --help'.\n", - ), - dict( - cmd=["non-existing", "command"], - exit_on_error=False, - expect_exc=CalledProcessError, - ), - expect_stderr="", - expect_log=r"$", -) -def test_git_check_output_lines_stderr_and_log( - git_repo, capfd, caplog, cmd, exit_on_error, expect_exc, expect_stderr, expect_log -): - """Git non-existing file error is logged and suppressed from stderr""" - git_repo.add({"file1": "file1"}, commit="Initial commit") - initial = git_repo.get_hash()[:7] - git_repo.add({"file2": "file2"}, commit="Second commit") - capfd.readouterr() # flush captured stdout and stderr - cmdline = [s.format(initial=initial) for s in cmd] - with pytest.raises(expect_exc): - - git._git_check_output_lines(cmdline, git_repo.root, exit_on_error) - - outerr = capfd.readouterr() - assert outerr.out == "" - assert outerr.err == expect_stderr - expect_log_re = expect_log.format(initial=initial) - assert re.search(expect_log_re, caplog.text), repr(caplog.text) - - -def test_git_get_content_at_revision_stderr(git_repo, capfd, caplog): - """No stderr or log output from ``git_get_content_at_revision`` for missing file""" - git_repo.add({"file1": "file1"}, commit="Initial commit") - initial = git_repo.get_hash()[:7] - git_repo.add({"file2": "file2"}, commit="Second commit") - capfd.readouterr() # flush captured stdout and stderr - - result = git.git_get_content_at_revision(Path("file2"), initial, git_repo.root) - - assert result == TextDocument() - outerr = capfd.readouterr() - assert outerr.out == "" - assert outerr.err == "" - assert [record for record in caplog.records if record.levelname != "DEBUG"] == [] - - -@pytest.fixture(scope="module") -def encodings_repo(tmp_path_factory): - """Create an example Git repository using various encodings for the same file""" - tmpdir = tmp_path_factory.mktemp("branched_repo") - git_repo = GitRepoFixture.create_repository(tmpdir) - # Commit without an encoding cookie, defaults to utf-8 - git_repo.add({"file.py": "darker = 'plus foncé'\n"}, commit="Default encoding") - git_repo.create_tag("default") - # Commit without an encoding cookie but with a utf-8 signature - content = "darker = 'plus foncé'\n".encode("utf-8-sig") - git_repo.add({"file.py": content}, commit="utf-8-sig") - git_repo.create_tag("utf-8-sig") - # Commit with an iso-8859-1 encoding cookie - content = "# coding: iso-8859-1\ndarker = 'plus foncé'\n".encode("iso-8859-1") - git_repo.add({"file.py": content}, commit="iso-8859-1") - git_repo.create_tag("iso-8859-1") - # Commit with a utf-8 encoding cookie - content = "# coding: utf-8\npython = 'パイソン'\n".encode() - git_repo.add({"file.py": content}, commit="utf-8") - git_repo.create_tag("utf-8") - # Current worktree content (not committed) with a shitfjs encoding cookie - content = "# coding: shiftjis\npython = 'パイソン'\n".encode("shiftjis") - git_repo.add({"file.py": content}) - return git_repo - - -@pytest.mark.kwparametrize( - dict(commit="default", encoding="utf-8", lines=("darker = 'plus foncé'",)), - dict(commit="utf-8-sig", encoding="utf-8-sig", lines=("darker = 'plus foncé'",)), - dict( - commit="iso-8859-1", - encoding="iso-8859-1", - lines=("# coding: iso-8859-1", "darker = 'plus foncé'"), - ), - dict( - commit="utf-8", encoding="utf-8", lines=("# coding: utf-8", "python = 'パイソン'") - ), - dict( - commit=":WORKTREE:", - encoding="shiftjis", - lines=("# coding: shiftjis", "python = 'パイソン'"), - ), -) -def test_git_get_content_at_revision_encoding(encodings_repo, commit, encoding, lines): - """Git file is loaded using its historical encoding""" - result = git.git_get_content_at_revision( - Path("file.py"), commit, encodings_repo.root - ) - assert result.encoding == encoding - assert result.lines == lines - - @pytest.mark.kwparametrize( dict(retval=0, expect=True), dict(retval=1, expect=False), @@ -483,7 +136,7 @@ def test_get_missing_at_revision_worktree(git_repo): paths["dir/b.py"].unlink() result = git.get_missing_at_revision( - {Path("dir"), Path("dir/a.py"), Path("dir/b.py")}, git.WORKTREE, git_repo.root + {Path("dir"), Path("dir/a.py"), Path("dir/b.py")}, WORKTREE, git_repo.root ) assert result == {Path("dir/a.py"), Path("dir/b.py")} @@ -577,66 +230,6 @@ def test_git_get_modified_python_files(git_repo, modify_paths, paths, expect): assert result == {Path(p) for p in expect} -@pytest.fixture(scope="module") -def branched_repo(tmp_path_factory): - """Create an example Git repository with a master branch and a feature branch - - The history created is:: - - . worktree - . index - * branch - | * master - |/ - * Initial commit - - """ - tmpdir = tmp_path_factory.mktemp("branched_repo") - git_repo = GitRepoFixture.create_repository(tmpdir) - git_repo.add( - { - "del_master.py": "original", - "del_branch.py": "original", - "del_index.py": "original", - "del_worktree.py": "original", - "mod_master.py": "original", - "mod_branch.py": "original", - "mod_both.py": "original", - "mod_same.py": "original", - "keep.py": "original", - }, - commit="Initial commit", - ) - branch_point = git_repo.get_hash() - git_repo.add( - { - "del_master.py": None, - "add_master.py": "master", - "mod_master.py": "master", - "mod_both.py": "master", - "mod_same.py": "same", - }, - commit="master", - ) - git_repo.create_branch("branch", branch_point) - git_repo.add( - { - "del_branch.py": None, - "mod_branch.py": "branch", - "mod_both.py": "branch", - "mod_same.py": "same", - }, - commit="branch", - ) - git_repo.add( - {"del_index.py": None, "add_index.py": "index", "mod_index.py": "index"} - ) - (git_repo.root / "del_worktree.py").unlink() - (git_repo.root / "add_worktree.py").write_bytes(b"worktree") - (git_repo.root / "mod_worktree.py").write_bytes(b"worktree") - return git_repo - - @pytest.mark.kwparametrize( dict( _description="from latest commit in branch to worktree and index", @@ -734,108 +327,6 @@ def test_git_get_modified_python_files_revision_range( assert {path.name for path in result} == expect -@pytest.mark.kwparametrize( - dict(branch="first", expect="first"), - dict(branch="second", expect="second"), - dict(branch="third", expect="third"), - dict(branch="HEAD", expect="third"), -) -def test_git_clone_local_branch(git_repo, tmp_path, branch, expect): - """``git_clone_local()`` checks out the specified branch""" - git_repo.add({"a.py": "first"}, commit="first") - git_repo.create_branch("first", "HEAD") - git_repo.create_branch("second", "HEAD") - git_repo.add({"a.py": "second"}, commit="second") - git_repo.create_branch("third", "HEAD") - git_repo.add({"a.py": "third"}, commit="third") - - with git.git_clone_local(git_repo.root, branch, tmp_path / "clone") as clone: - - assert (clone / "a.py").read_text() == expect - - -@pytest.mark.kwparametrize( - dict(branch="HEAD"), - dict(branch="mybranch"), -) -def test_git_clone_local_command(git_repo, tmp_path, branch): - """``git_clone_local()`` issues the correct Git command and options""" - git_repo.add({"a.py": "first"}, commit="first") - git_repo.create_branch("mybranch", "HEAD") - check_output = Mock(wraps=git.check_output) # type: ignore[attr-defined] - clone = tmp_path / "clone" - check_output_opts = dict( - cwd=str(git_repo.root), encoding=None, stderr=PIPE, env=ANY - ) - pre_call = call( - ["git", "worktree", "add", "--quiet", "--force", "--force", str(clone), branch], - **check_output_opts, - ) - post_call = call( - ["git", "worktree", "remove", "--force", "--force", str(clone)], - **check_output_opts, - ) - with patch.object(git, "check_output", check_output): - - with git.git_clone_local(git_repo.root, branch, clone) as result: - - assert result == clone - - check_output.assert_has_calls([pre_call]) - check_output.reset_mock() - check_output.assert_has_calls([post_call]) - - -@pytest.mark.parametrize( - "path", - [ - ".", - "root.py", - "subdir", - "subdir/sub.py", - "subdir/subsubdir", - "subdir/subsubdir/subsub.py", - ], -) -def test_git_get_root(git_repo, path): - """``git_get_root()`` returns repository root for any file or directory inside""" - git_repo.add( - { - "root.py": "root", - "subdir/sub.py": "sub", - "subdir/subsubdir/subsub.py": "subsub", - }, - commit="Initial commit", - ) - - root = git.git_get_root(git_repo.root / path) - - assert root == git_repo.root - - -@pytest.mark.parametrize( - "path", - [ - ".", - "root.py", - "subdir", - "subdir/sub.py", - "subdir/subsubdir", - "subdir/subsubdir/subsub.py", - ], -) -def test_git_get_root_not_found(tmp_path, path): - """``git_get_root()`` returns ``None`` for any file or directory outside of Git""" - (tmp_path / "subdir" / "subsubdir").mkdir(parents=True) - (tmp_path / "root.py").touch() - (tmp_path / "subdir" / "sub.py").touch() - (tmp_path / "subdir" / "subsubdir" / "subsub.py").touch() - - root = git.git_get_root(tmp_path / path) - - assert root is None - - edited_linenums_differ_cases = pytest.mark.kwparametrize( dict(context_lines=0, expect=[3, 7]), dict(context_lines=1, expect=[2, 3, 4, 6, 7, 8]), diff --git a/src/darker/tests/test_main.py b/src/darker/tests/test_main.py index f9217b46f..293c97659 100644 --- a/src/darker/tests/test_main.py +++ b/src/darker/tests/test_main.py @@ -21,12 +21,12 @@ import darker.import_sorting from darker.config import Exclusions from darker.exceptions import MissingPackageError -from darker.git import WORKTREE, EditedLinenumsDiffer +from darker.git import EditedLinenumsDiffer from darker.tests.helpers import isort_present from darker.tests.test_fstring import FLYNTED_SOURCE, MODIFIED_SOURCE, ORIGINAL_SOURCE from darker.utils import joinlines from darker.verification import NotEquivalentError -from darkgraylib.git import RevisionRange +from darkgraylib.git import WORKTREE, RevisionRange from darkgraylib.testtools.highlighting_helpers import BLUE, CYAN, RESET, WHITE, YELLOW from darkgraylib.utils import TextDocument