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

Add support for the Pijul VCS #858

Merged
merged 6 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/license_list_up_to_date.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name: Verify that the license lists are up-to-date

on:
schedule:
- cron: "0 9 * * *"
- cron: "0 9 * * 1"

jobs:
license-list-up-to-date:
Expand All @@ -16,7 +16,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Verify that the license lists are up-to-date
Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/pijul.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# SPDX-FileCopyrightText: © 2020 Liferay, Inc. <https://liferay.com>
# SPDX-FileCopyrightText: 2023 Free Software Foundation Europe e.V.
#
# SPDX-License-Identifier: GPL-3.0-or-later

name: Test with Paijul

# These tests are run exclusively on the main branch to reduce CPU time wasted
# on every single PR that very likely does not affect Pijul functionality.
on:
push:
branches:
- main
paths:
- "src/reuse/**.py"
- "tests/**.py"
jobs:
test-pijul:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.x
- name: Install dependencies
run: |
pip install poetry~=1.2.0
poetry install --no-interaction --only main,test
# TODO: As soon as a binary is available for Ubuntu 22.04, use it instead
# of manually building it.
- name: Set up Pijul
run: |
sudo apt install make libsodium-dev libclang-dev pkg-config libssl-dev libxxhash-dev libzstd-dev clang
cargo install --locked pijul --version "1.0.0-beta.6"
pijul identity new --no-prompt --display-name 'Jane Doe' --email 'jdoe@example.com' 'jdoe'
- name: Run tests with pytest
run: |
poetry run pytest --cov=reuse
2 changes: 2 additions & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ Contributors

- Robin Vobruba

- Markus Haug <korrat@proton.me>

Translators
-----------

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ CLI command and its behaviour. There are no guarantees of stability for the
- Julia (`.jl`) (#815)
- Modern Fortran (`.f90`) (#836)
- Display recommendations for steps to fix found issues during a lint. (#698)
- Add support for Pijul VCS. Pijul support is not added to the Docker image.
(#858)

### Changed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ For full functionality, the following pieces of software are recommended:

- Git
- Mercurial 4.3+
- Pijul

### Installation via pip

Expand Down
1 change: 1 addition & 0 deletions src/reuse/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@

GIT_EXE = shutil.which("git")
HG_EXE = shutil.which("hg")
PIJUL_EXE = shutil.which("pijul")

REUSE_IGNORE_START = "REUSE-IgnoreStart"
REUSE_IGNORE_END = "REUSE-IgnoreEnd"
Expand Down
12 changes: 5 additions & 7 deletions src/reuse/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@
from ._util import (
_HEADER_BYTES,
_LICENSEREF_PATTERN,
GIT_EXE,
HG_EXE,
StrPath,
_contains_snippet,
_copyright_from_dep5,
Expand All @@ -46,7 +44,7 @@
decoded_text_from_binary,
extract_reuse_info,
)
from .vcs import VCSStrategy, VCSStrategyGit, VCSStrategyHg, VCSStrategyNone
from .vcs import VCSStrategy, VCSStrategyNone, all_vcs_strategies

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -448,10 +446,10 @@ def _detect_vcs_strategy(cls, root: StrPath) -> Type[VCSStrategy]:
"""For each supported VCS, check if the software is available and if the
directory is a repository. If not, return :class:`VCSStrategyNone`.
"""
if GIT_EXE and VCSStrategyGit.in_repo(root):
return VCSStrategyGit
if HG_EXE and VCSStrategyHg.in_repo(root):
return VCSStrategyHg
for strategy in all_vcs_strategies():
if strategy.EXE and strategy.in_repo(root):
return strategy

_LOGGER.info(
_(
"project '{}' is not a VCS repository or required VCS"
Expand Down
118 changes: 99 additions & 19 deletions src/reuse/vcs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 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-FileCopyrightText: 2023 Markus Haug <korrat@proton.me>
#
# SPDX-License-Identifier: GPL-3.0-or-later

Expand All @@ -11,10 +12,11 @@
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, StrPath, execute_command
from ._util import GIT_EXE, HG_EXE, PIJUL_EXE, StrPath, execute_command

if TYPE_CHECKING:
from .project import Project
Expand All @@ -25,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 @@ -82,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 @@ -94,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 @@ -113,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 @@ -150,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 @@ -163,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 @@ -176,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 @@ -187,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 @@ -218,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 @@ -231,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 @@ -241,19 +249,91 @@ def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]:
return None


class VCSStrategyPijul(VCSStrategy):
"""Strategy that is used for Pijul."""

EXE = PIJUL_EXE

def __init__(self, project: Project):
super().__init__(project)
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(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}

def is_ignored(self, path: StrPath) -> bool:
path = self.project.relative_from_root(path)
return path not in self._all_tracked_files

def is_submodule(self, path: StrPath) -> bool:
# not supported in pijul yet
return False

@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(cls.EXE), "diff", "--short"]
result = execute_command(command, _LOGGER, cwd=directory)

return not result.returncode

@classmethod
def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]:
if cwd is None:
cwd = Path.cwd()

# TODO this duplicates pijul's logic.
# Maybe it should be replaced by calling pijul,
# but there is no matching subcommand yet.
path = Path(cwd).resolve()

if not path.is_dir():
raise NotADirectoryError()

while True:
if (path / ".pijul").is_dir():
return path

parent = path.parent
if parent == path:
# We reached the filesystem root
return None

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
for strategy in all_vcs_strategies():
if strategy.EXE:
root = strategy.find_root(cwd=cwd)
if root:
return root
return None
Loading
Loading