From 5d65ebd915433479490e55feec606e0e7d95aac7 Mon Sep 17 00:00:00 2001 From: Skyler Grey Date: Sun, 14 Jul 2024 18:37:48 +0000 Subject: [PATCH] feat(vcs): Add Jujutsu support Although it uses .gitignore, Jujutsu has support for repositories without git ("non-colocated"), which we can't use "git ls-files" on jj has its own command for listing out tracked files ("jj files"), and we can use it to determine ignored files in a similar way to Pijul Fixes: fsfe/reuse-tool#711 --- .github/workflows/jujutsu.yaml | 37 ++++++++++++++++ AUTHORS.rst | 1 + README.md | 1 + changelog.d/added/jujutsu.md | 1 + src/reuse/_util.py | 2 + src/reuse/vcs.py | 77 +++++++++++++++++++++++++++++++++- tests/conftest.py | 30 ++++++++++++- tests/test_main.py | 16 ++++++- tests/test_project.py | 35 ++++++++++++++++ tests/test_vcs.py | 13 +++++- 10 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/jujutsu.yaml create mode 100644 changelog.d/added/jujutsu.md diff --git a/.github/workflows/jujutsu.yaml b/.github/workflows/jujutsu.yaml new file mode 100644 index 00000000..b59d9d89 --- /dev/null +++ b/.github/workflows/jujutsu.yaml @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2023 Free Software Foundation Europe e.V. +# SPDX-FileCopyrightText: 2024 Skyler Grey +# +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Test with Jujutsu + +# These tests are run exclusively on the main branch to reduce CPU time wasted +# on every single PR that very likely does not affect Jujutsu functionality. +on: + push: + branches: + - main + paths: + - "src/reuse/**.py" + - "tests/**.py" +jobs: + test-jujutsu: + 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.3.0 + poetry install --no-interaction --only main,test + - name: Set up Jujutsu + run: | + cargo install cargo-binstall + cargo binstall --strategies crate-meta-data jj-cli --no-confirm + export PATH=~/.cargo/bin:$PATH + - name: Run tests with pytest + run: | + poetry run pytest --cov=reuse diff --git a/AUTHORS.rst b/AUTHORS.rst index 6bb97ef2..1a40a48b 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -150,3 +150,4 @@ Contributors - rajivsunar07 <56905029+rajivsunar07@users.noreply.github.com> - Сергій - Mersho +- Skyler Grey diff --git a/README.md b/README.md index 4eb501bf..1908df36 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ For full functionality, the following pieces of software are recommended: - Git - Mercurial 4.3+ - Pijul +- Jujutsu ### Installation via pip diff --git a/changelog.d/added/jujutsu.md b/changelog.d/added/jujutsu.md new file mode 100644 index 00000000..c005e592 --- /dev/null +++ b/changelog.d/added/jujutsu.md @@ -0,0 +1 @@ +- Add Jujutsu VCS support. (#TODO) diff --git a/src/reuse/_util.py b/src/reuse/_util.py index 66c2c8fd..d10044ce 100644 --- a/src/reuse/_util.py +++ b/src/reuse/_util.py @@ -7,6 +7,7 @@ # SPDX-FileCopyrightText: 2023 DB Systel GmbH # SPDX-FileCopyrightText: 2023 Johannes Zarl-Zierl # SPDX-FileCopyrightText: 2024 Rivos Inc. +# SPDX-FileCopyrightText: 2024 Skyler Grey # SPDX-FileCopyrightText: © 2020 Liferay, Inc. # # SPDX-License-Identifier: GPL-3.0-or-later @@ -61,6 +62,7 @@ GIT_EXE = shutil.which("git") HG_EXE = shutil.which("hg") +JUJUTSU_EXE = shutil.which("jj") PIJUL_EXE = shutil.which("pijul") REUSE_IGNORE_START = "REUSE-IgnoreStart" diff --git a/src/reuse/vcs.py b/src/reuse/vcs.py index b5bb7326..92470307 100644 --- a/src/reuse/vcs.py +++ b/src/reuse/vcs.py @@ -1,7 +1,8 @@ # SPDX-FileCopyrightText: 2017 Free Software Foundation Europe e.V. -# SPDX-FileCopyrightText: © 2020 Liferay, Inc. # SPDX-FileCopyrightText: 2020 John Mulligan # SPDX-FileCopyrightText: 2023 Markus Haug +# SPDX-FileCopyrightText: 2024 Skyler Grey +# SPDX-FileCopyrightText: © 2020 Liferay, Inc. # # SPDX-License-Identifier: GPL-3.0-or-later @@ -16,7 +17,14 @@ from pathlib import Path from typing import TYPE_CHECKING, Generator, Optional, Set, Type -from ._util import GIT_EXE, HG_EXE, PIJUL_EXE, StrPath, execute_command +from ._util import ( + GIT_EXE, + HG_EXE, + JUJUTSU_EXE, + PIJUL_EXE, + StrPath, + execute_command, +) if TYPE_CHECKING: from .project import Project @@ -243,6 +251,71 @@ def find_root(cls, cwd: Optional[StrPath] = None) -> Optional[Path]: return None +class VCSStrategyJujutsu(VCSStrategy): + """Strategy that is used for Jujutsu.""" + + EXE = JUJUTSU_EXE + + def __init__(self, project: Project): + super().__init__(project) + if not self.EXE: + raise FileNotFoundError("Could not find binary for Jujutsu") + self._all_tracked_files = self._find_all_tracked_files() + + def _find_all_tracked_files(self) -> Set[Path]: + """ + Return a set of all files tracked in the current jj revision + """ + command = [str(self.EXE), "files"] + result = execute_command(command, _LOGGER, cwd=self.project.root) + all_files = result.stdout.decode("utf-8").split("\n") + return {Path(file_) for file_ in all_files if file_} + + def is_ignored(self, path: StrPath) -> bool: + path = self.project.relative_from_root(path) + + for tracked in self._all_tracked_files: + if tracked.parts[: len(path.parts)] == path.parts: + # We can't check only if the path is in our tracked files as we + # must support directories as well as files + # + # We'll consider a directory "tracked" if there are any tracked + # files inside it + return False + + return True + + def is_submodule(self, path: StrPath) -> bool: + return False + + @classmethod + def in_repo(cls, directory: StrPath) -> bool: + if not Path(directory).is_dir(): + raise NotADirectoryError() + + command = [str(cls.EXE), "root"] + 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() + + if not Path(cwd).is_dir(): + raise NotADirectoryError() + + command = [str(cls.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 + + class VCSStrategyPijul(VCSStrategy): """Strategy that is used for Pijul.""" diff --git a/tests/conftest.py b/tests/conftest.py index 94b3b095..d9b14592 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ # SPDX-FileCopyrightText: 2022 Carmen Bianca Bakker # SPDX-FileCopyrightText: 2022 Florian Snow # SPDX-FileCopyrightText: 2023 Matthias Riße +# SPDX-FileCopyrightText: 2024 Skyler Grey # # SPDX-License-Identifier: GPL-3.0-or-later @@ -37,7 +38,13 @@ except ImportError: sys.path.append(os.path.join(Path(__file__).parent.parent, "src")) finally: - from reuse._util import GIT_EXE, HG_EXE, PIJUL_EXE, setup_logging + from reuse._util import ( + GIT_EXE, + HG_EXE, + JUJUTSU_EXE, + PIJUL_EXE, + setup_logging, + ) from reuse.global_licensing import ReuseDep5 CWD = Path.cwd() @@ -108,6 +115,14 @@ def hg_exe() -> str: return str(HG_EXE) +@pytest.fixture() +def jujutsu_exe() -> str: + """Run the test with Jujutsu.""" + if not JUJUTSU_EXE: + pytest.skip("cannot run this test without jujutsu") + return str(JUJUTSU_EXE) + + @pytest.fixture() def pijul_exe() -> str: """Run the test with Pijul.""" @@ -275,6 +290,19 @@ def hg_repository(fake_repository: Path, hg_exe: str) -> Path: return fake_repository +@pytest.fixture() +def jujutsu_repository(fake_repository: Path, jujutsu_exe: str) -> Path: + """Create a jujutsu repository with ignored files.""" + os.chdir(fake_repository) + _repo_contents(fake_repository) + + subprocess.run( + [jujutsu_exe, "git", "init", str(fake_repository)], check=True + ) + + return fake_repository + + @pytest.fixture() def pijul_repository(fake_repository: Path, pijul_exe: str) -> Path: """Create a pijul repository with ignored files.""" diff --git a/tests/test_main.py b/tests/test_main.py index 3068b330..461a15ca 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,9 +1,10 @@ # SPDX-FileCopyrightText: 2019 Free Software Foundation Europe e.V. # SPDX-FileCopyrightText: 2019 Stefan Bakker -# SPDX-FileCopyrightText: © 2020 Liferay, Inc. # SPDX-FileCopyrightText: 2022 Florian Snow # SPDX-FileCopyrightText: 2022 Pietro Albini # SPDX-FileCopyrightText: 2024 Carmen Bianca BAKKER +# SPDX-FileCopyrightText: 2024 Skyler Grey +# SPDX-FileCopyrightText: © 2020 Liferay, Inc. # # SPDX-License-Identifier: GPL-3.0-or-later @@ -29,7 +30,7 @@ from reuse import download from reuse._main import main -from reuse._util import GIT_EXE, HG_EXE, PIJUL_EXE, cleandoc_nl +from reuse._util import GIT_EXE, HG_EXE, JUJUTSU_EXE, PIJUL_EXE, cleandoc_nl from reuse.report import LINT_VERSION # REUSE-IgnoreStart @@ -57,6 +58,17 @@ def optional_hg_exe( yield exe +@pytest.fixture(params=[True, False]) +def optional_jujutsu_exe( + request, monkeypatch +) -> Generator[Optional[str], None, None]: + """Run the test with or without Jujutsu.""" + exe = JUJUTSU_EXE if request.param else "" + monkeypatch.setattr("reuse.vcs.JUJUTSU_EXE", exe) + monkeypatch.setattr("reuse._util.JUJUTSU_EXE", exe) + yield exe + + @pytest.fixture(params=[True, False]) def optional_pijul_exe( request, monkeypatch diff --git a/tests/test_project.py b/tests/test_project.py index 0041466e..fceba03e 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2017 Free Software Foundation Europe e.V. # SPDX-FileCopyrightText: 2022 Florian Snow # SPDX-FileCopyrightText: 2023 Carmen Bianca BAKKER +# SPDX-FileCopyrightText: 2024 Skyler Grey # SPDX-FileCopyrightText: © 2020 Liferay, Inc. # # SPDX-License-Identifier: GPL-3.0-or-later @@ -240,6 +241,40 @@ def test_all_files_hg_ignored_contains_newline(hg_repository): assert Path("hello\nworld.pyc").absolute() not in project.all_files() +def test_all_files_jujutsu_ignored(jujutsu_repository): + """Given a jujutsu repository where some files are ignored, do not yield + those files. + """ + project = Project.from_directory(jujutsu_repository) + assert Path("build/hello.py").absolute() not in project.all_files() + + +def test_all_files_jujutsu_ignored_different_cwd(jujutsu_repository): + """Given a jujutsu repository where some files are ignored, do not yield + those files. + + Be in a different CWD during the above. + """ + os.chdir(jujutsu_repository / "LICENSES") + project = Project.from_directory(jujutsu_repository) + assert Path("build/hello.py").absolute() not in project.all_files() + + +def test_all_files_jujutsu_ignored_contains_space(jujutsu_repository): + """File names that contain spaces are also ignored.""" + (jujutsu_repository / "I contain spaces.pyc").touch() + project = Project.from_directory(jujutsu_repository) + assert Path("I contain spaces.pyc").absolute() not in project.all_files() + + +@posix +def test_all_files_jujutsu_ignored_contains_newline(jujutsu_repository): + """File names that contain newlines are also ignored.""" + (jujutsu_repository / "hello\nworld.pyc").touch() + project = Project.from_directory(jujutsu_repository) + assert Path("hello\nworld.pyc").absolute() not in project.all_files() + + def test_all_files_pijul_ignored(pijul_repository): """Given a pijul repository where some files are ignored, do not yield those files. diff --git a/tests/test_vcs.py b/tests/test_vcs.py index 97bebb6c..3712d1b0 100644 --- a/tests/test_vcs.py +++ b/tests/test_vcs.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2017 Free Software Foundation Europe e.V. -# SPDX-FileCopyrightText: © 2020 Liferay, Inc. # SPDX-FileCopyrightText: 2022 Florian Snow +# SPDX-FileCopyrightText: 2024 Skyler Grey +# SPDX-FileCopyrightText: © 2020 Liferay, Inc. # # SPDX-License-Identifier: GPL-3.0-or-later @@ -33,6 +34,16 @@ def test_find_root_in_hg_repo(hg_repository): assert Path(result).absolute().resolve() == hg_repository +def test_find_root_in_jujutsu_repo(jujutsu_repository): + """When using reuse from a child directory in a Jujutsu repo, always find + the root directory. + """ + os.chdir("src") + result = vcs.find_root() + + assert Path(result).absolute().resolve() == jujutsu_repository + + def test_find_root_in_pijul_repo(pijul_repository): """When using reuse from a child directory in a Pijul repo, always find the root directory.