Skip to content

Commit

Permalink
Add better plumbing for adding more VCSStrategies
Browse files Browse the repository at this point in the history
Signed-off-by: Carmen Bianca BAKKER <carmenbianca@fsfe.org>
  • Loading branch information
carmenbianca committed Oct 31, 2023
1 parent 58e5fc9 commit cfae922
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 45 deletions.
23 changes: 5 additions & 18 deletions src/reuse/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@
from ._util import (
_HEADER_BYTES,
_LICENSEREF_PATTERN,
GIT_EXE,
HG_EXE,
PIJUL_EXE,
StrPath,
_contains_snippet,
_copyright_from_dep5,
Expand All @@ -45,14 +42,7 @@
decoded_text_from_binary,
extract_reuse_info,
)
from .vcs import (
VCSStrategy,
VCSStrategyGit,
VCSStrategyHg,
VCSStrategyNone,
VCSStrategyPijul,
find_root,
)
from .vcs import VCSStrategy, VCSStrategyNone, all_vcs_strategies, find_root

_LOGGER = logging.getLogger(__name__)

Expand All @@ -72,13 +62,10 @@ def __init__(
if not self._root.is_dir():
raise NotADirectoryError(f"{self._root} is no valid path")

if GIT_EXE and VCSStrategyGit.in_repo(self._root):
self.vcs_strategy: VCSStrategy = VCSStrategyGit(self)
elif HG_EXE and VCSStrategyHg.in_repo(self._root):
self.vcs_strategy = VCSStrategyHg(self)
elif PIJUL_EXE and VCSStrategyPijul.in_repo(self._root):
self.vcs_strategy = VCSStrategyPijul(self)

for strategy in all_vcs_strategies():
if strategy.EXE and strategy.in_repo(self._root):
self.vcs_strategy: VCSStrategy = strategy(self)
break
else:
_LOGGER.info(
_(
Expand Down
65 changes: 39 additions & 26 deletions src/reuse/vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
import logging
import os
from abc import ABC, abstractmethod
from inspect import isclass
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Set
from typing import TYPE_CHECKING, Generator, Optional, Set, Type

from ._util import GIT_EXE, HG_EXE, PIJUL_EXE, StrPath, execute_command

Expand All @@ -26,6 +27,8 @@
class VCSStrategy(ABC):
"""Strategy pattern for version control systems."""

EXE: str | None = None

@abstractmethod
def __init__(self, project: Project):
self.project = project
Expand Down Expand Up @@ -83,9 +86,11 @@ def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]:
class VCSStrategyGit(VCSStrategy):
"""Strategy that is used for Git."""

EXE = GIT_EXE

def __init__(self, project: Project):
super().__init__(project)
if not GIT_EXE:
if not self.EXE:
raise FileNotFoundError("Could not find binary for Git")
self._all_ignored_files = self._find_all_ignored_files()
self._submodules = self._find_submodules()
Expand All @@ -95,7 +100,7 @@ def _find_all_ignored_files(self) -> Set[Path]:
ignored, don't return all files inside of it.
"""
command = [
str(GIT_EXE),
str(self.EXE),
"ls-files",
"--exclude-standard",
"--ignored",
Expand All @@ -114,7 +119,7 @@ def _find_all_ignored_files(self) -> Set[Path]:

def _find_submodules(self) -> Set[Path]:
command = [
str(GIT_EXE),
str(self.EXE),
"config",
"-z",
"--file",
Expand Down Expand Up @@ -151,7 +156,7 @@ def in_repo(cls, directory: StrPath) -> bool:
if not Path(directory).is_dir():
raise NotADirectoryError()

command = [str(GIT_EXE), "status"]
command = [str(cls.EXE), "status"]
result = execute_command(command, _LOGGER, cwd=directory)

return not result.returncode
Expand All @@ -164,7 +169,7 @@ def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]:
if not Path(cwd).is_dir():
raise NotADirectoryError()

command = [str(GIT_EXE), "rev-parse", "--show-toplevel"]
command = [str(cls.EXE), "rev-parse", "--show-toplevel"]
result = execute_command(command, _LOGGER, cwd=cwd)

if not result.returncode:
Expand All @@ -177,9 +182,11 @@ def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]:
class VCSStrategyHg(VCSStrategy):
"""Strategy that is used for Mercurial."""

EXE = HG_EXE

def __init__(self, project: Project):
super().__init__(project)
if not HG_EXE:
if not self.EXE:
raise FileNotFoundError("Could not find binary for Mercurial")
self._all_ignored_files = self._find_all_ignored_files()

Expand All @@ -188,7 +195,7 @@ def _find_all_ignored_files(self) -> Set[Path]:
is ignored, don't return all files inside of it.
"""
command = [
str(HG_EXE),
str(self.EXE),
"status",
"--ignored",
# terse is marked 'experimental' in the hg help but is documented
Expand Down Expand Up @@ -219,7 +226,7 @@ def in_repo(cls, directory: StrPath) -> bool:
if not Path(directory).is_dir():
raise NotADirectoryError()

command = [str(HG_EXE), "root"]
command = [str(cls.EXE), "root"]
result = execute_command(command, _LOGGER, cwd=directory)

return not result.returncode
Expand All @@ -232,7 +239,7 @@ def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]:
if not Path(cwd).is_dir():
raise NotADirectoryError()

command = [str(HG_EXE), "root"]
command = [str(cls.EXE), "root"]
result = execute_command(command, _LOGGER, cwd=cwd)

if not result.returncode:
Expand All @@ -245,15 +252,17 @@ def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]:
class VCSStrategyPijul(VCSStrategy):
"""Strategy that is used for Pijul."""

EXE = PIJUL_EXE

def __init__(self, project: Project):
super().__init__(project)
if not PIJUL_EXE:
raise FileNotFoundError("Could not find binary for Mercurial")
if not self.EXE:
raise FileNotFoundError("Could not find binary for Pijul")
self._all_tracked_files = self._find_all_tracked_files()

def _find_all_tracked_files(self) -> Set[Path]:
"""Return a set of all files tracked by pijul."""
command = [str(PIJUL_EXE), "list"]
command = [str(self.EXE), "list"]
result = execute_command(command, _LOGGER, cwd=self.project.root)
all_files = result.stdout.decode("utf-8").splitlines()
return {Path(file_) for file_ in all_files}
Expand All @@ -274,7 +283,7 @@ def in_repo(cls, directory: StrPath) -> bool:
if not Path(directory).is_dir():
raise NotADirectoryError()

command = [str(PIJUL_EXE), "diff", "--short"]
command = [str(cls.EXE), "diff", "--short"]
result = execute_command(command, _LOGGER, cwd=directory)

return not result.returncode
Expand Down Expand Up @@ -304,23 +313,27 @@ def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]:
path = parent


def all_vcs_strategies() -> Generator[Type[VCSStrategy], None, None]:
"""Yield all VCSStrategy classes that aren't the abstract base class."""
for value in globals().values():
if (
isclass(value)
and issubclass(value, VCSStrategy)
and value is not VCSStrategy
):
yield value


def find_root(cwd: Optional[StrPath] = None) -> Optional[Path]:
"""Try to find the root of the project from *cwd*. If none is found,
return None.
Raises:
NotADirectoryError: if directory is not a directory.
"""
if GIT_EXE:
root = VCSStrategyGit.find_root(cwd=cwd)
if root:
return root
if HG_EXE:
root = VCSStrategyHg.find_root(cwd=cwd)
if root:
return root
if PIJUL_EXE:
root = VCSStrategyPijul.find_root(cwd=cwd)
if root:
return root
for strategy in all_vcs_strategies():
if strategy.EXE:
root = strategy.find_root(cwd=cwd)
if root:
return root
return None
10 changes: 9 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
except ImportError:
sys.path.append(os.path.join(Path(__file__).parent.parent, "src"))
finally:
from reuse._util import GIT_EXE, HG_EXE, setup_logging
from reuse._util import GIT_EXE, HG_EXE, PIJUL_EXE, setup_logging

CWD = Path.cwd()

Expand Down Expand Up @@ -84,6 +84,14 @@ def hg_exe() -> str:
return str(HG_EXE)


@pytest.fixture()
def pijul_exe() -> str:
"""Run the test with Pijul."""
if not PIJUL_EXE:
pytest.skip("cannot run this test without pijul")
return str(PIJUL_EXE)


@pytest.fixture(params=[True, False])
def multiprocessing(request, monkeypatch) -> Generator[bool, None, None]:
"""Run the test with or without multiprocessing."""
Expand Down

0 comments on commit cfae922

Please sign in to comment.