Source code for reuse.vcs

# SPDX-FileCopyrightText: 2017 Free Software Foundation Europe e.V. <https://fsfe.org>
# SPDX-FileCopyrightText: © 2020 Liferay, Inc. <https://liferay.com>
# SPDX-FileCopyrightText: 2020 John Mulligan <jmulligan@redhat.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""This module deals with version control systems."""

from __future__ import annotations

import logging
import os
from abc import ABC, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Set

from ._util import GIT_EXE, HG_EXE, StrPath, execute_command

if TYPE_CHECKING:
    from .project import Project

_LOGGER = logging.getLogger(__name__)


[docs]class VCSStrategy(ABC): """Strategy pattern for version control systems.""" @abstractmethod def __init__(self, project: Project): self.project = project
[docs] @abstractmethod def is_ignored(self, path: StrPath) -> bool: """Is *path* ignored by the VCS?"""
[docs] @classmethod @abstractmethod def in_repo(cls, directory: StrPath) -> bool: """Is *directory* inside of the VCS repository? :raises NotADirectoryError: if directory is not a directory. """
[docs] @classmethod @abstractmethod def find_root(cls, 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. """
[docs]class VCSStrategyNone(VCSStrategy): """Strategy that is used when there is no VCS.""" def __init__(self, project: Project): # pylint: disable=useless-super-delegation super().__init__(project)
[docs] def is_ignored(self, path: StrPath) -> bool: return False
[docs] @classmethod def in_repo(cls, directory: StrPath) -> bool: return False
[docs] @classmethod def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]: return None
[docs]class VCSStrategyGit(VCSStrategy): """Strategy that is used for Git.""" def __init__(self, project: Project): super().__init__(project) if not GIT_EXE: raise FileNotFoundError("Could not find binary for Git") self._all_ignored_files = self._find_all_ignored_files() def _find_all_ignored_files(self) -> Set[Path]: """Return a set of all files ignored by git. If a whole directory is ignored, don't return all files inside of it. """ command = [ str(GIT_EXE), "ls-files", "--exclude-standard", "--ignored", "--others", "--directory", # TODO: This flag is unexpected. I reported it as a bug in Git. # This flag---counter-intuitively---lists untracked directories # that contain ignored files. "--no-empty-directory", # Separate output with \0 instead of \n. "-z", ] result = execute_command(command, _LOGGER, cwd=self.project.root) all_files = result.stdout.decode("utf-8").split("\0") return {Path(file_) for file_ in all_files}
[docs] def is_ignored(self, path: StrPath) -> bool: path = self.project.relative_from_root(path) return path in self._all_ignored_files
[docs] @classmethod def in_repo(cls, directory: StrPath) -> bool: if directory is None: directory = Path.cwd() if not Path(directory).is_dir(): raise NotADirectoryError() command = [str(GIT_EXE), "status"] result = execute_command(command, _LOGGER, cwd=directory) return not result.returncode
[docs] @classmethod def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]: if cwd is None: cwd = Path.cwd() if not Path(cwd).is_dir(): raise NotADirectoryError() command = [str(GIT_EXE), "rev-parse", "--show-toplevel"] result = execute_command(command, _LOGGER, cwd=cwd) if not result.returncode: path = result.stdout.decode("utf-8")[:-1] return Path(os.path.relpath(path, cwd)) return None
[docs]class VCSStrategyHg(VCSStrategy): """Strategy that is used for Mercurial.""" def __init__(self, project: Project): super().__init__(project) if not HG_EXE: raise FileNotFoundError("Could not find binary for Mercurial") self._all_ignored_files = self._find_all_ignored_files() def _find_all_ignored_files(self) -> Set[Path]: """Return a set of all files ignored by mercurial. If a whole directory is ignored, don't return all files inside of it. """ command = [ str(HG_EXE), "status", "--ignored", # terse is marked 'experimental' in the hg help but is documented # in the man page. It collapses the output of a dir containing only # ignored files to the ignored name like the git command does. # TODO: Re-enable this flag in the future. # "--terse=i", "--no-status", "--print0", ] result = execute_command(command, _LOGGER, cwd=self.project.root) all_files = result.stdout.decode("utf-8").split("\0") return {Path(file_) for file_ in all_files}
[docs] def is_ignored(self, path: StrPath) -> bool: path = self.project.relative_from_root(path) return path in self._all_ignored_files
[docs] @classmethod def in_repo(cls, directory: StrPath) -> bool: if directory is None: directory = Path.cwd() if not Path(directory).is_dir(): raise NotADirectoryError() command = [str(HG_EXE), "root"] result = execute_command(command, _LOGGER, cwd=directory) return not result.returncode
[docs] @classmethod def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]: if cwd is None: cwd = Path.cwd() if not Path(cwd).is_dir(): raise NotADirectoryError() command = [str(HG_EXE), "root"] result = execute_command(command, _LOGGER, cwd=cwd) if not result.returncode: path = result.stdout.decode("utf-8")[:-1] return Path(os.path.relpath(path, cwd)) return None
[docs]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 return None