From 4e91204a829c9a8206951df2681ccfec0fa1a72d Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Fri, 6 Feb 2026 23:27:05 +0100 Subject: [PATCH 01/11] Rename git to gitsubproject and svn to svnsubproject --- dfetch/project/__init__.py | 4 ++-- dfetch/project/{git.py => gitsubproject.py} | 0 dfetch/project/superproject.py | 4 ++-- dfetch/project/{svn.py => svnsubproject.py} | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename dfetch/project/{git.py => gitsubproject.py} (100%) rename dfetch/project/{svn.py => svnsubproject.py} (100%) diff --git a/dfetch/project/__init__.py b/dfetch/project/__init__.py index 24651756..108f5d37 100644 --- a/dfetch/project/__init__.py +++ b/dfetch/project/__init__.py @@ -1,9 +1,9 @@ """All Project related items.""" import dfetch.manifest.project -from dfetch.project.git import GitSubProject +from dfetch.project.gitsubproject import GitSubProject from dfetch.project.subproject import SubProject -from dfetch.project.svn import SvnSubProject +from dfetch.project.svnsubproject import SvnSubProject SUPPORTED_PROJECT_TYPES = [GitSubProject, SvnSubProject] diff --git a/dfetch/project/git.py b/dfetch/project/gitsubproject.py similarity index 100% rename from dfetch/project/git.py rename to dfetch/project/gitsubproject.py diff --git a/dfetch/project/superproject.py b/dfetch/project/superproject.py index 378b5921..2dcc98a4 100644 --- a/dfetch/project/superproject.py +++ b/dfetch/project/superproject.py @@ -17,9 +17,9 @@ from dfetch.manifest.manifest import Manifest from dfetch.manifest.parse import find_manifest, parse from dfetch.manifest.project import ProjectEntry -from dfetch.project.git import GitSubProject +from dfetch.project.gitsubproject import GitSubProject from dfetch.project.subproject import SubProject -from dfetch.project.svn import SvnSubProject +from dfetch.project.svnsubproject import SvnSubProject from dfetch.util.util import resolve_absolute_path from dfetch.vcs.git import GitLocalRepo from dfetch.vcs.svn import SvnRepo diff --git a/dfetch/project/svn.py b/dfetch/project/svnsubproject.py similarity index 100% rename from dfetch/project/svn.py rename to dfetch/project/svnsubproject.py From bacea0b18c67f47975966df5b81e31041bd71056 Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Fri, 6 Feb 2026 23:48:20 +0100 Subject: [PATCH 02/11] Split SuperProject into separate VCS types --- dfetch/commands/check.py | 2 +- dfetch/commands/diff.py | 2 +- dfetch/commands/format_patch.py | 2 +- dfetch/commands/freeze.py | 2 +- dfetch/commands/report.py | 2 +- dfetch/commands/update.py | 2 +- dfetch/commands/update_patch.py | 2 +- dfetch/project/superproject.py | 196 +++++++++++++++++++++++++------- dfetch/vcs/svn.py | 5 - tests/test_check.py | 4 +- tests/test_report.py | 4 +- tests/test_svn.py | 2 +- tests/test_update.py | 8 +- 13 files changed, 178 insertions(+), 55 deletions(-) diff --git a/dfetch/commands/check.py b/dfetch/commands/check.py index 156fddef..52f7f2ce 100644 --- a/dfetch/commands/check.py +++ b/dfetch/commands/check.py @@ -90,7 +90,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the check.""" - superproject = SuperProject() + superproject = SuperProject.create() reporters = self._get_reporters(args, superproject.manifest) with in_directory(superproject.root_directory): diff --git a/dfetch/commands/diff.py b/dfetch/commands/diff.py index 9316bdfa..1d8f4e25 100644 --- a/dfetch/commands/diff.py +++ b/dfetch/commands/diff.py @@ -96,7 +96,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the diff.""" - superproject = SuperProject() + superproject = SuperProject.create() old_rev, new_rev = self._parse_revs(args.revs) with in_directory(superproject.root_directory): diff --git a/dfetch/commands/format_patch.py b/dfetch/commands/format_patch.py index fd55ebe4..654da3a8 100644 --- a/dfetch/commands/format_patch.py +++ b/dfetch/commands/format_patch.py @@ -69,7 +69,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the format patch.""" - superproject = SuperProject() + superproject = SuperProject.create() exceptions: list[str] = [] diff --git a/dfetch/commands/freeze.py b/dfetch/commands/freeze.py index 8b8b17d9..61e2dca6 100644 --- a/dfetch/commands/freeze.py +++ b/dfetch/commands/freeze.py @@ -70,7 +70,7 @@ def __call__(self, args: argparse.Namespace) -> None: """Perform the freeze.""" del args # unused - superproject = SuperProject() + superproject = SuperProject.create() exceptions: list[str] = [] projects: list[ProjectEntry] = [] diff --git a/dfetch/commands/report.py b/dfetch/commands/report.py index 82296f7e..cf378229 100644 --- a/dfetch/commands/report.py +++ b/dfetch/commands/report.py @@ -63,7 +63,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Generate the report.""" - superproject = SuperProject() + superproject = SuperProject.create() with dfetch.util.util.in_directory(superproject.root_directory): reporter = REPORTERS[args.type](superproject.manifest) diff --git a/dfetch/commands/update.py b/dfetch/commands/update.py index 481d519f..fdb22b2e 100644 --- a/dfetch/commands/update.py +++ b/dfetch/commands/update.py @@ -74,7 +74,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the update.""" - superproject = SuperProject() + superproject = SuperProject.create() exceptions: list[str] = [] destinations: list[str] = [ diff --git a/dfetch/commands/update_patch.py b/dfetch/commands/update_patch.py index c6eca9f0..3b86027c 100644 --- a/dfetch/commands/update_patch.py +++ b/dfetch/commands/update_patch.py @@ -70,7 +70,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the update patch.""" - superproject = SuperProject() + superproject = SuperProject.create() exceptions: list[str] = [] diff --git a/dfetch/project/superproject.py b/dfetch/project/superproject.py index 2dcc98a4..3d806973 100644 --- a/dfetch/project/superproject.py +++ b/dfetch/project/superproject.py @@ -11,6 +11,7 @@ import getpass import os import pathlib +from abc import ABC, abstractmethod from collections.abc import Sequence from dfetch.log import get_logger @@ -27,7 +28,7 @@ logger = get_logger(__name__) -class SuperProject: +class SuperProject(ABC): """Representation of the project containing the manifest. A SuperProject is the repository/directory that contains the dfetch @@ -35,16 +36,28 @@ class SuperProject: managed by git, svn, or is unversioned. """ - def __init__(self) -> None: + def __init__(self, manifest: Manifest, root_directory: pathlib.Path) -> None: + """Create a SuperProject by looking for a manifest file.""" + self._manifest = manifest + self._root_directory = root_directory + + @staticmethod + def create() -> SuperProject: """Create a SuperProject by looking for a manifest file.""" logger.debug("Looking for manifest") manifest_path = find_manifest() logger.debug(f"Using manifest {manifest_path}") - self._manifest = parse(manifest_path) - self._root_directory = resolve_absolute_path( - os.path.dirname(self._manifest.path) - ) + manifest = parse(manifest_path) + root_directory = resolve_absolute_path(os.path.dirname(manifest.path)) + + if GitLocalRepo(root_directory).is_git(): + return GitSuperProject(manifest, root_directory) + + if SvnRepo(root_directory).is_svn(): + return SvnSuperProject(manifest, root_directory) + + return NoVcsSuperProject(manifest, root_directory) @property def root_directory(self) -> pathlib.Path: @@ -56,14 +69,41 @@ def manifest(self) -> Manifest: """The manifest of the super project.""" return self._manifest + @abstractmethod def get_sub_project(self, project: ProjectEntry) -> SubProject | None: """Get the subproject in the same vcs type as the superproject.""" - if GitLocalRepo(self.root_directory).is_git(): - return GitSubProject(project) - if SvnRepo(self.root_directory).is_svn(): - return SvnSubProject(project) - return None + @abstractmethod + def ignored_files(self, path: str) -> Sequence[str]: + """Return a list of files that can be ignored in a given path.""" + + @abstractmethod + def in_vcs(self) -> bool: + """Check if this superproject is under version control.""" + + @abstractmethod + def is_git(self) -> bool: + """Check if this superproject is a git repository.""" + + @abstractmethod + def has_local_changes_in_dir(self, path: str) -> bool: + """Check if the superproject has local changes.""" + + @abstractmethod + def get_username(self) -> str: + """Get the username of the superproject VCS.""" + + @abstractmethod + def get_useremail(self) -> str: + """Get the user email of the superproject VCS.""" + + +class GitSuperProject(SuperProject): + """A git specific superproject.""" + + def get_sub_project(self, project: ProjectEntry) -> SubProject | None: + """Get the subproject in the same vcs type as the superproject.""" + return GitSubProject(project) def ignored_files(self, path: str) -> Sequence[str]: """Return a list of files that can be ignored in a given path.""" @@ -74,42 +114,77 @@ def ignored_files(self, path: str) -> Sequence[str]: f"{resolved_path} not in superproject {self.root_directory}!" ) - if GitLocalRepo(self.root_directory).is_git(): - return GitLocalRepo.ignored_files(path) - if SvnRepo(self.root_directory).is_svn(): - return SvnRepo.ignored_files(path) - - return [] + return GitLocalRepo.ignored_files(path) def in_vcs(self) -> bool: """Check if this superproject is under version control.""" - return ( - GitLocalRepo(self.root_directory).is_git() - or SvnRepo(self.root_directory).is_svn() - ) + return True def is_git(self) -> bool: """Check if this superproject is a git repository.""" - return GitLocalRepo(self.root_directory).is_git() + return True def has_local_changes_in_dir(self, path: str) -> bool: """Check if the superproject has local changes.""" - if GitLocalRepo(self.root_directory).is_git(): - return GitLocalRepo.any_changes_or_untracked(path) + return GitLocalRepo.any_changes_or_untracked(path) + + def get_username(self) -> str: + """Get the username of the superproject VCS.""" + username = GitLocalRepo(self.root_directory).get_username() - if SvnRepo(self.root_directory).is_svn(): - return SvnRepo.any_changes_or_untracked(path) + if not username: + try: + username = getpass.getuser() + except (ImportError, KeyError, OSError): + username = "" + if not username: + try: + username = os.getlogin() + except OSError: + username = "unknown" + return username + def get_useremail(self) -> str: + """Get the user email of the superproject VCS.""" + email = GitLocalRepo(self.root_directory).get_useremail() + + username = self.get_username() or "unknown" + return email or f"{username}@example.com" + + +class SvnSuperProject(SuperProject): + """A SVN specific superproject.""" + + def get_sub_project(self, project: ProjectEntry) -> SubProject | None: + """Get the subproject in the same vcs type as the superproject.""" + return SvnSubProject(project) + + def ignored_files(self, path: str) -> Sequence[str]: + """Return a list of files that can be ignored in a given path.""" + resolved_path = resolve_absolute_path(path) + + if not resolved_path.is_relative_to(self.root_directory): + raise RuntimeError( + f"{resolved_path} not in superproject {self.root_directory}!" + ) + + return SvnRepo.ignored_files(path) + + def in_vcs(self) -> bool: + """Check if this superproject is under version control.""" return True + def is_git(self) -> bool: + """Check if this superproject is a git repository.""" + return False + + def has_local_changes_in_dir(self, path: str) -> bool: + """Check if the superproject has local changes.""" + return SvnRepo.any_changes_or_untracked(path) + def get_username(self) -> str: """Get the username of the superproject VCS.""" - username = "" - if GitLocalRepo(self.root_directory).is_git(): - username = GitLocalRepo(self.root_directory).get_username() - - elif SvnRepo(self.root_directory).is_svn(): - username = SvnRepo(self.root_directory).get_username() + username = SvnRepo(self.root_directory).get_username() if not username: try: @@ -125,12 +200,57 @@ def get_username(self) -> str: def get_useremail(self) -> str: """Get the user email of the superproject VCS.""" - email = "" - if GitLocalRepo(self.root_directory).is_git(): - email = GitLocalRepo(self.root_directory).get_useremail() + username = self.get_username() or "unknown" + return f"{username}@example.com" - elif SvnRepo(self.root_directory).is_svn(): - email = SvnRepo(self.root_directory).get_useremail() +class NoVcsSuperProject(SuperProject): + """A superproject without any version control.""" + + def get_sub_project(self, project: ProjectEntry) -> SubProject | None: + """Get the subproject in the same vcs type as the superproject.""" + return None + + def ignored_files(self, path: str) -> Sequence[str]: + """Return a list of files that can be ignored in a given path.""" + resolved_path = resolve_absolute_path(path) + + if not resolved_path.is_relative_to(self.root_directory): + raise RuntimeError( + f"{resolved_path} not in superproject {self.root_directory}!" + ) + + return [] + + def in_vcs(self) -> bool: + """Check if this superproject is under version control.""" + return False + + def is_git(self) -> bool: + """Check if this superproject is a git repository.""" + return False + + def has_local_changes_in_dir(self, path: str) -> bool: + """Check if the superproject has local changes.""" + return True + + def get_username(self) -> str: + """Get the username of the superproject VCS.""" + username = "" + + if not username: + try: + username = getpass.getuser() + except (ImportError, KeyError, OSError): + username = "" + if not username: + try: + username = os.getlogin() + except OSError: + username = "unknown" + return username + + def get_useremail(self) -> str: + """Get the user email of the superproject VCS.""" username = self.get_username() or "unknown" - return email or f"{username}@example.com" + return f"{username}@example.com" diff --git a/dfetch/vcs/svn.py b/dfetch/vcs/svn.py index 1b451a87..02d62667 100644 --- a/dfetch/vcs/svn.py +++ b/dfetch/vcs/svn.py @@ -352,8 +352,3 @@ def get_username(self) -> str: return str(result.stdout.decode().strip()) except SubprocessCommandError: return "" - - def get_useremail(self) -> str: - """Get the user email of the local svn repo.""" - # SVN does not have user email concept - return "" diff --git a/tests/test_check.py b/tests/test_check.py index 73dda675..99ffeb8e 100644 --- a/tests/test_check.py +++ b/tests/test_check.py @@ -33,7 +33,9 @@ def test_check(name, projects): fake_superproject.manifest = mock_manifest(projects) fake_superproject.root_directory = Path("/tmp") - with patch("dfetch.commands.check.SuperProject", return_value=fake_superproject): + with patch( + "dfetch.commands.check.SuperProject.create", return_value=fake_superproject + ): with patch( "dfetch.manifest.parse.get_childmanifests" ) as mocked_get_childmanifests: diff --git a/tests/test_report.py b/tests/test_report.py index 466f77d6..6b83db4a 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -33,7 +33,9 @@ def test_report(name, projects): fake_superproject.manifest = mock_manifest(projects) fake_superproject.root_directory = Path("/tmp") - with patch("dfetch.commands.report.SuperProject", return_value=fake_superproject): + with patch( + "dfetch.commands.report.SuperProject.create", return_value=fake_superproject + ): with patch("dfetch.log.DLogger.print_report_line") as mocked_print_report_line: report(DEFAULT_ARGS) diff --git a/tests/test_svn.py b/tests/test_svn.py index 42fac0a2..bc40dd32 100644 --- a/tests/test_svn.py +++ b/tests/test_svn.py @@ -9,7 +9,7 @@ import pytest from dfetch.manifest.project import ProjectEntry -from dfetch.project.svn import SvnSubProject +from dfetch.project.svnsubproject import SvnSubProject from dfetch.util.cmdline import SubprocessCommandError from dfetch.vcs.svn import External, SvnRemote, SvnRepo diff --git a/tests/test_update.py b/tests/test_update.py index c33014cd..2b35003a 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -33,7 +33,9 @@ def test_update(name, projects): fake_superproject.manifest = mock_manifest(projects) fake_superproject.root_directory = Path("/tmp") - with patch("dfetch.commands.update.SuperProject", return_value=fake_superproject): + with patch( + "dfetch.commands.update.SuperProject.create", return_value=fake_superproject + ): with patch( "dfetch.manifest.parse.get_childmanifests" ) as mocked_get_childmanifests: @@ -57,7 +59,9 @@ def test_forced_update(): fake_superproject.root_directory = Path("/tmp") fake_superproject.ignored_files.return_value = [] - with patch("dfetch.commands.update.SuperProject", return_value=fake_superproject): + with patch( + "dfetch.commands.update.SuperProject.create", return_value=fake_superproject + ): with patch( "dfetch.manifest.parse.get_childmanifests" ) as mocked_get_childmanifests: From ed6188be54362de8293a90eb4dacc7c8b9c098f4 Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Fri, 6 Feb 2026 23:52:50 +0100 Subject: [PATCH 03/11] Reduce SuperProject interface --- dfetch/commands/update_patch.py | 6 +++--- dfetch/project/superproject.py | 32 -------------------------------- 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/dfetch/commands/update_patch.py b/dfetch/commands/update_patch.py index 3b86027c..ae0450e4 100644 --- a/dfetch/commands/update_patch.py +++ b/dfetch/commands/update_patch.py @@ -40,7 +40,7 @@ import dfetch.manifest.project import dfetch.project from dfetch.log import get_logger -from dfetch.project.superproject import SuperProject +from dfetch.project.superproject import GitSuperProject, SuperProject, SvnSuperProject from dfetch.util.util import catch_runtime_exceptions, in_directory logger = get_logger(__name__) @@ -74,12 +74,12 @@ def __call__(self, args: argparse.Namespace) -> None: exceptions: list[str] = [] - if not superproject.in_vcs(): + if not isinstance(superproject, (GitSuperProject, SvnSuperProject)): raise RuntimeError( "The project containing the manifest is not under version control," " updating patches is not supported" ) - if not superproject.is_git(): + if not isinstance(superproject, GitSuperProject): logger.warning("Update patch is only fully supported in git superprojects!") with in_directory(superproject.root_directory): diff --git a/dfetch/project/superproject.py b/dfetch/project/superproject.py index 3d806973..87994278 100644 --- a/dfetch/project/superproject.py +++ b/dfetch/project/superproject.py @@ -77,14 +77,6 @@ def get_sub_project(self, project: ProjectEntry) -> SubProject | None: def ignored_files(self, path: str) -> Sequence[str]: """Return a list of files that can be ignored in a given path.""" - @abstractmethod - def in_vcs(self) -> bool: - """Check if this superproject is under version control.""" - - @abstractmethod - def is_git(self) -> bool: - """Check if this superproject is a git repository.""" - @abstractmethod def has_local_changes_in_dir(self, path: str) -> bool: """Check if the superproject has local changes.""" @@ -116,14 +108,6 @@ def ignored_files(self, path: str) -> Sequence[str]: return GitLocalRepo.ignored_files(path) - def in_vcs(self) -> bool: - """Check if this superproject is under version control.""" - return True - - def is_git(self) -> bool: - """Check if this superproject is a git repository.""" - return True - def has_local_changes_in_dir(self, path: str) -> bool: """Check if the superproject has local changes.""" return GitLocalRepo.any_changes_or_untracked(path) @@ -170,14 +154,6 @@ def ignored_files(self, path: str) -> Sequence[str]: return SvnRepo.ignored_files(path) - def in_vcs(self) -> bool: - """Check if this superproject is under version control.""" - return True - - def is_git(self) -> bool: - """Check if this superproject is a git repository.""" - return False - def has_local_changes_in_dir(self, path: str) -> bool: """Check if the superproject has local changes.""" return SvnRepo.any_changes_or_untracked(path) @@ -222,14 +198,6 @@ def ignored_files(self, path: str) -> Sequence[str]: return [] - def in_vcs(self) -> bool: - """Check if this superproject is under version control.""" - return False - - def is_git(self) -> bool: - """Check if this superproject is a git repository.""" - return False - def has_local_changes_in_dir(self, path: str) -> bool: """Check if the superproject has local changes.""" return True From c60f0de61f48f986ec199b246b66a690015d4c6e Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Sat, 7 Feb 2026 00:02:28 +0100 Subject: [PATCH 04/11] Getting revision of local repo should be done from superproject --- dfetch/commands/diff.py | 4 +++- dfetch/project/gitsubproject.py | 4 ---- dfetch/project/subproject.py | 4 ---- dfetch/project/superproject.py | 16 ++++++++++++++++ dfetch/project/svnsubproject.py | 4 ---- tests/test_subproject.py | 3 --- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/dfetch/commands/diff.py b/dfetch/commands/diff.py index 1d8f4e25..ba6c480b 100644 --- a/dfetch/commands/diff.py +++ b/dfetch/commands/diff.py @@ -118,7 +118,9 @@ def __call__(self, args: argparse.Namespace) -> None: raise RuntimeError( "Can only create patch if your project is an SVN or Git repo", ) - old_rev = old_rev or subproject.metadata_revision() + old_rev = old_rev or superproject.get_file_revision( + subproject.metadata_path + ) if not old_rev: raise RuntimeError( "When not providing any revisions, dfetch starts from" diff --git a/dfetch/project/gitsubproject.py b/dfetch/project/gitsubproject.py index 6f553bbe..547dae84 100644 --- a/dfetch/project/gitsubproject.py +++ b/dfetch/project/gitsubproject.py @@ -44,10 +44,6 @@ def _list_of_tags(self) -> list[str]: """Get list of all available tags.""" return [str(tag) for tag in self._remote_repo.list_of_tags()] - def metadata_revision(self) -> str: - """Get the revision of the metadata file.""" - return str(self._local_repo.get_last_file_hash(self.metadata_path)) - def _diff_impl( self, old_revision: str, diff --git a/dfetch/project/subproject.py b/dfetch/project/subproject.py index 8d227f69..37c7b6de 100644 --- a/dfetch/project/subproject.py +++ b/dfetch/project/subproject.py @@ -385,10 +385,6 @@ def _are_there_local_changes(self, files_to_ignore: Sequence[str]) -> bool: def _fetch_impl(self, version: Version) -> Version: """Fetch the given version of the subproject, should be implemented by the child class.""" - @abstractmethod - def metadata_revision(self) -> str: - """Get the revision of the metadata file.""" - @abstractmethod def _diff_impl( self, diff --git a/dfetch/project/superproject.py b/dfetch/project/superproject.py index 87994278..d827e63e 100644 --- a/dfetch/project/superproject.py +++ b/dfetch/project/superproject.py @@ -89,6 +89,10 @@ def get_username(self) -> str: def get_useremail(self) -> str: """Get the user email of the superproject VCS.""" + @abstractmethod + def get_file_revision(self, path: str | pathlib.Path) -> str: + """Get the revision of the given file.""" + class GitSuperProject(SuperProject): """A git specific superproject.""" @@ -135,6 +139,10 @@ def get_useremail(self) -> str: username = self.get_username() or "unknown" return email or f"{username}@example.com" + def get_file_revision(self, path: str | pathlib.Path) -> str: + """Get the revision of the given file.""" + return str(GitLocalRepo(self.root_directory).get_last_file_hash(str(path))) + class SvnSuperProject(SuperProject): """A SVN specific superproject.""" @@ -179,6 +187,10 @@ def get_useremail(self) -> str: username = self.get_username() or "unknown" return f"{username}@example.com" + def get_file_revision(self, path: str | pathlib.Path) -> str: + """Get the revision of the given file.""" + return str(SvnRepo(self.root_directory).get_last_changed_revision(str(path))) + class NoVcsSuperProject(SuperProject): """A superproject without any version control.""" @@ -222,3 +234,7 @@ def get_useremail(self) -> str: """Get the user email of the superproject VCS.""" username = self.get_username() or "unknown" return f"{username}@example.com" + + def get_file_revision(self, path: str | pathlib.Path) -> str: + """Get the revision of the given file.""" + return "" diff --git a/dfetch/project/svnsubproject.py b/dfetch/project/svnsubproject.py index 98393b42..2e5f901e 100644 --- a/dfetch/project/svnsubproject.py +++ b/dfetch/project/svnsubproject.py @@ -186,10 +186,6 @@ def _license_files(url_path: str) -> list[str]: def _get_revision(self, branch: str) -> str: return self._get_info(branch)["Revision"] - def metadata_revision(self) -> str: - """Get the revision of the metadata file.""" - return SvnRepo.get_last_changed_revision(self.metadata_path) - def _diff_impl( self, old_revision: str, diff --git a/tests/test_subproject.py b/tests/test_subproject.py index 8b503c26..17859be6 100644 --- a/tests/test_subproject.py +++ b/tests/test_subproject.py @@ -43,9 +43,6 @@ def wanted_version(self): def _list_of_tags(self): return [] - def metadata_revision(self): - return "1" - def _diff_impl( self, old_revision, From dee6af7d725a80545de8366576cf75dbf9a2e283 Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Sat, 7 Feb 2026 10:13:14 +0100 Subject: [PATCH 05/11] Move import superproject concerns into superproject --- dfetch/commands/import_.py | 80 +--------------------------- dfetch/project/superproject.py | 90 ++++++++++++++++++++++++++++++-- features/import-from-git.feature | 1 + features/import-from-svn.feature | 2 +- tests/test_import.py | 10 ++-- 5 files changed, 95 insertions(+), 88 deletions(-) diff --git a/dfetch/commands/import_.py b/dfetch/commands/import_.py index 4f12eb3b..cb98337e 100644 --- a/dfetch/commands/import_.py +++ b/dfetch/commands/import_.py @@ -92,8 +92,7 @@ from dfetch.manifest.manifest import Manifest from dfetch.manifest.project import ProjectEntry from dfetch.manifest.remote import Remote -from dfetch.vcs.git import GitLocalRepo -from dfetch.vcs.svn import SvnRepo +from dfetch.project.superproject import SuperProject logger = get_logger(__name__) @@ -112,7 +111,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, _: argparse.Namespace) -> None: """Perform the import.""" - projects = _import_projects() + projects = SuperProject.type_from_path(".").import_projects() if not projects: raise RuntimeError(f"No submodules found in {os.getcwd()}!") @@ -135,81 +134,6 @@ def __call__(self, _: argparse.Namespace) -> None: logger.info(f"Created manifest ({DEFAULT_MANIFEST_NAME}) in {os.getcwd()}") -def _import_projects() -> Sequence[ProjectEntry]: - """Find out what type of VCS is used and import projects.""" - if GitLocalRepo().is_git(): - projects = _import_from_git() - elif SvnRepo().is_svn(): - projects = _import_from_svn() - else: - raise RuntimeError( - "Only git or SVN projects can be imported.", - "Run this command within either a git or SVN repository", - ) - return projects - - -def _import_from_svn() -> Sequence[ProjectEntry]: - projects: list[ProjectEntry] = [] - - for external in SvnRepo(os.getcwd()).externals(): - projects.append( - ProjectEntry( - { - "name": external.name, - "revision": external.revision, - "url": external.url, - "dst": external.path, - "branch": external.branch, - "tag": external.tag, - "src": external.src, - } - ) - ) - logger.info(f"Found {external.name}") - - return projects - - -def _import_from_git() -> Sequence[ProjectEntry]: - projects: list[ProjectEntry] = [] - toplevel: str = "" - for submodule in GitLocalRepo.submodules(): - projects.append( - ProjectEntry( - { - "name": submodule.name, - "revision": submodule.sha, - "url": submodule.url, - "dst": submodule.path, - "branch": submodule.branch, - "tag": submodule.tag, - } - ) - ) - logger.info(f"Found {submodule.name}") - - if not toplevel: - toplevel = submodule.toplevel - elif toplevel != submodule.toplevel: - raise RuntimeError( - "Recursive submodules not (yet) supported. Check manifest!" - ) - - if os.path.realpath(toplevel) != os.getcwd(): - logger.warning( - "\n".join( - ( - f'The toplevel directory is in "{toplevel}"', - f'"dfetch import" was called from "{os.getcwd()}"', - "All projects paths will be relative to the current directory dfetch is running!", - ) - ) - ) - - return projects - - def _create_remotes(projects: Sequence[ProjectEntry]) -> Sequence[Remote]: """Create a list of Remotes optimized for least amount of entries and smallest manifest. diff --git a/dfetch/project/superproject.py b/dfetch/project/superproject.py index d827e63e..2382b0be 100644 --- a/dfetch/project/superproject.py +++ b/dfetch/project/superproject.py @@ -50,14 +50,18 @@ def create() -> SuperProject: logger.debug(f"Using manifest {manifest_path}") manifest = parse(manifest_path) root_directory = resolve_absolute_path(os.path.dirname(manifest.path)) + return SuperProject.type_from_path(root_directory)(manifest, root_directory) - if GitLocalRepo(root_directory).is_git(): - return GitSuperProject(manifest, root_directory) + @staticmethod + def type_from_path(path: str | pathlib.Path) -> type[SuperProject]: + """Determine correct VCS type of the superproject in the given path.""" + if GitLocalRepo(path).is_git(): + return GitSuperProject - if SvnRepo(root_directory).is_svn(): - return SvnSuperProject(manifest, root_directory) + if SvnRepo(path).is_svn(): + return SvnSuperProject - return NoVcsSuperProject(manifest, root_directory) + return NoVcsSuperProject @property def root_directory(self) -> pathlib.Path: @@ -93,6 +97,11 @@ def get_useremail(self) -> str: def get_file_revision(self, path: str | pathlib.Path) -> str: """Get the revision of the given file.""" + @staticmethod + @abstractmethod + def import_projects() -> Sequence[ProjectEntry]: + """Import projects from underlying superproject.""" + class GitSuperProject(SuperProject): """A git specific superproject.""" @@ -143,6 +152,46 @@ def get_file_revision(self, path: str | pathlib.Path) -> str: """Get the revision of the given file.""" return str(GitLocalRepo(self.root_directory).get_last_file_hash(str(path))) + @staticmethod + def import_projects() -> Sequence[ProjectEntry]: + """Import projects from underlying superproject.""" + projects: list[ProjectEntry] = [] + toplevel: str = "" + for submodule in GitLocalRepo.submodules(): + projects.append( + ProjectEntry( + { + "name": submodule.name, + "revision": submodule.sha, + "url": submodule.url, + "dst": submodule.path, + "branch": submodule.branch, + "tag": submodule.tag, + } + ) + ) + logger.info(f"Found {submodule.name}") + + if not toplevel: + toplevel = submodule.toplevel + elif toplevel != submodule.toplevel: + raise RuntimeError( + "Recursive submodules not (yet) supported. Check manifest!" + ) + + if os.path.realpath(toplevel) != os.getcwd(): + logger.warning( + "\n".join( + ( + f'The toplevel directory is in "{toplevel}"', + f'Import was done from "{os.getcwd()}"', + "All projects paths will be relative to the current directory dfetch is running!", + ) + ) + ) + + return projects + class SvnSuperProject(SuperProject): """A SVN specific superproject.""" @@ -191,6 +240,29 @@ def get_file_revision(self, path: str | pathlib.Path) -> str: """Get the revision of the given file.""" return str(SvnRepo(self.root_directory).get_last_changed_revision(str(path))) + @staticmethod + def import_projects() -> Sequence[ProjectEntry]: + """Import projects from underlying superproject.""" + projects: list[ProjectEntry] = [] + + for external in SvnRepo(os.getcwd()).externals(): + projects.append( + ProjectEntry( + { + "name": external.name, + "revision": external.revision, + "url": external.url, + "dst": external.path, + "branch": external.branch, + "tag": external.tag, + "src": external.src, + } + ) + ) + logger.info(f"Found {external.name}") + + return projects + class NoVcsSuperProject(SuperProject): """A superproject without any version control.""" @@ -238,3 +310,11 @@ def get_useremail(self) -> str: def get_file_revision(self, path: str | pathlib.Path) -> str: """Get the revision of the given file.""" return "" + + @staticmethod + def import_projects() -> Sequence[ProjectEntry]: + """Import projects from underlying superproject.""" + raise RuntimeError( + "Only git or SVN projects can be imported.", + "Run this command within either a git or SVN repository", + ) diff --git a/features/import-from-git.feature b/features/import-from-git.feature index 84173b01..7b58d5a6 100644 --- a/features/import-from-git.feature +++ b/features/import-from-git.feature @@ -4,6 +4,7 @@ Feature: Importing submodules from an existing git repository as easy as possible, a user should be able to generate a manifest that is filled with the submodules and their pinned versions. + @wip Scenario: Multiple submodules are imported Given a git repo with the following submodules | path | url | revision | diff --git a/features/import-from-svn.feature b/features/import-from-svn.feature index 2e049e11..cbfd96d3 100644 --- a/features/import-from-svn.feature +++ b/features/import-from-svn.feature @@ -4,7 +4,7 @@ Feature: Importing externals from an existing svn repository as easy as possible, a user should be able to generate a manifest that is filled with the externals and their pinned versions. - @remote-svn + @remote-svn @wip Scenario: Multiple externals are imported Given a svn repo with the following externals | path | url | revision | diff --git a/tests/test_import.py b/tests/test_import.py index 933e0c82..8c06372c 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -48,7 +48,9 @@ def test_git_import(name, submodules): import_ = Import() - with patch("dfetch.commands.import_.GitLocalRepo.submodules") as mocked_submodules: + with patch( + "dfetch.project.superproject.GitLocalRepo.submodules" + ) as mocked_submodules: with patch("dfetch.commands.import_.Manifest") as mocked_manifest: mocked_submodules.return_value = submodules @@ -105,10 +107,10 @@ def test_git_import(name, submodules): def test_svn_import(name, externals): import_ = Import() - with patch("dfetch.commands.import_.SvnRepo.is_svn") as is_svn: - with patch("dfetch.commands.import_.SvnRepo.externals") as mocked_externals: + with patch("dfetch.project.superproject.SvnRepo.is_svn") as is_svn: + with patch("dfetch.project.superproject.SvnRepo.externals") as mocked_externals: with patch("dfetch.commands.import_.Manifest") as mocked_manifest: - with patch("dfetch.commands.import_.GitLocalRepo.is_git") as is_git: + with patch("dfetch.project.superproject.GitLocalRepo.is_git") as is_git: is_git.return_value = False is_svn.return_value = True mocked_externals.return_value = externals From 88c3cac3fb137d9a7c1ecfdaa85cb25236626d46 Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Sat, 7 Feb 2026 10:48:29 +0100 Subject: [PATCH 06/11] Move diff into superproject --- dfetch/commands/diff.py | 20 ++++-- dfetch/commands/update_patch.py | 18 ++++-- dfetch/project/gitsubproject.py | 37 +---------- dfetch/project/subproject.py | 16 ----- dfetch/project/superproject.py | 106 ++++++++++++++++++++++++++++++- dfetch/project/svnsubproject.py | 42 +----------- features/import-from-git.feature | 1 - tests/test_subproject.py | 9 --- 8 files changed, 133 insertions(+), 116 deletions(-) diff --git a/dfetch/commands/diff.py b/dfetch/commands/diff.py index ba6c480b..1699abef 100644 --- a/dfetch/commands/diff.py +++ b/dfetch/commands/diff.py @@ -60,7 +60,7 @@ import dfetch.commands.command from dfetch.log import get_logger from dfetch.project.metadata import Metadata -from dfetch.project.superproject import SuperProject +from dfetch.project.superproject import NoVcsSuperProject, RevisionRange, SuperProject from dfetch.util.util import catch_runtime_exceptions, in_directory logger = get_logger(__name__) @@ -99,6 +99,11 @@ def __call__(self, args: argparse.Namespace) -> None: superproject = SuperProject.create() old_rev, new_rev = self._parse_revs(args.revs) + if isinstance(superproject, NoVcsSuperProject): + raise RuntimeError( + "Can only create patch if your project is an SVN or Git repo", + ) + with in_directory(superproject.root_directory): exceptions: list[str] = [] projects = superproject.manifest.selected_projects(args.projects) @@ -114,10 +119,9 @@ def __call__(self, args: argparse.Namespace) -> None: ) subproject = superproject.get_sub_project(project) - if subproject is None: - raise RuntimeError( - "Can only create patch if your project is an SVN or Git repo", - ) + if not subproject: + raise RuntimeError("No subproject!") + old_rev = old_rev or superproject.get_file_revision( subproject.metadata_path ) @@ -127,7 +131,11 @@ def __call__(self, args: argparse.Namespace) -> None: f" the last revision to {Metadata.FILENAME} in {subproject.local_path}." " Please either commit this, or specify a revision to start from with --revs" ) - patch = subproject.diff(old_rev, new_rev) + patch = superproject.diff( + project.destination, + revisions=RevisionRange(old_rev, new_rev), + ignore=(Metadata.FILENAME,), + ) msg = self._rev_msg(old_rev, new_rev) if patch: diff --git a/dfetch/commands/update_patch.py b/dfetch/commands/update_patch.py index ae0450e4..7176c9de 100644 --- a/dfetch/commands/update_patch.py +++ b/dfetch/commands/update_patch.py @@ -40,7 +40,13 @@ import dfetch.manifest.project import dfetch.project from dfetch.log import get_logger -from dfetch.project.superproject import GitSuperProject, SuperProject, SvnSuperProject +from dfetch.project.metadata import Metadata +from dfetch.project.superproject import ( + GitSuperProject, + NoVcsSuperProject, + RevisionRange, + SuperProject, +) from dfetch.util.util import catch_runtime_exceptions, in_directory logger = get_logger(__name__) @@ -74,7 +80,7 @@ def __call__(self, args: argparse.Namespace) -> None: exceptions: list[str] = [] - if not isinstance(superproject, (GitSuperProject, SvnSuperProject)): + if isinstance(superproject, NoVcsSuperProject): raise RuntimeError( "The project containing the manifest is not under version control," " updating patches is not supported" @@ -123,10 +129,10 @@ def __call__(self, args: argparse.Namespace) -> None: ) # generate reverse patch - patch_text = subproject.diff( - old_revision="", - new_revision="", - # ignore=files_to_ignore, + patch_text = superproject.diff( + subproject.local_path, + revisions=RevisionRange("", ""), + ignore=[Metadata.FILENAME], reverse=True, ) diff --git a/dfetch/project/gitsubproject.py b/dfetch/project/gitsubproject.py index 547dae84..7adbc9bc 100644 --- a/dfetch/project/gitsubproject.py +++ b/dfetch/project/gitsubproject.py @@ -2,9 +2,7 @@ import os import pathlib -from collections.abc import Sequence from functools import lru_cache -from typing import Optional from dfetch.log import get_logger from dfetch.manifest.project import ProjectEntry @@ -12,7 +10,7 @@ from dfetch.project.subproject import SubProject from dfetch.util.util import safe_rmtree from dfetch.vcs.git import GitLocalRepo, GitRemote, get_git_version -from dfetch.vcs.patch import PatchInfo, reverse_patch +from dfetch.vcs.patch import PatchInfo logger = get_logger(__name__) @@ -44,39 +42,6 @@ def _list_of_tags(self) -> list[str]: """Get list of all available tags.""" return [str(tag) for tag in self._remote_repo.list_of_tags()] - def _diff_impl( - self, - old_revision: str, - new_revision: Optional[str], - ignore: Sequence[str], - reverse: bool = False, - ) -> str: - """Get the diff of two revisions.""" - diff_since_revision = str( - self._local_repo.create_diff(old_revision, new_revision, ignore, reverse) - ) - - if new_revision: - return diff_since_revision - - combined_diff = [] - - if diff_since_revision: - combined_diff += [diff_since_revision] - - untracked_files_patch = str(self._local_repo.untracked_files_patch(ignore)) - if untracked_files_patch: - if reverse: - reversed_patch = reverse_patch(untracked_files_patch.encode("utf-8")) - if not reversed_patch: - raise RuntimeError( - "Failed to reverse untracked files patch; patch parsing returned empty." - ) - untracked_files_patch = reversed_patch - combined_diff += [untracked_files_patch] - - return "\n".join(combined_diff) - @staticmethod def revision_is_enough() -> bool: """See if this VCS can uniquely distinguish branch with revision only.""" diff --git a/dfetch/project/subproject.py b/dfetch/project/subproject.py index 37c7b6de..965771d4 100644 --- a/dfetch/project/subproject.py +++ b/dfetch/project/subproject.py @@ -385,16 +385,6 @@ def _are_there_local_changes(self, files_to_ignore: Sequence[str]) -> bool: def _fetch_impl(self, version: Version) -> Version: """Fetch the given version of the subproject, should be implemented by the child class.""" - @abstractmethod - def _diff_impl( - self, - old_revision: str, # noqa - new_revision: Optional[str], # noqa - ignore: Sequence[str], - reverse: bool = False, - ) -> str: - """Get the diff of two revisions, should be implemented by the child class.""" - @abstractmethod def get_default_branch(self) -> str: """Get the default branch of this repository.""" @@ -407,12 +397,6 @@ def is_license_file(filename: str) -> bool: for pattern in SubProject.LICENSE_GLOBS ) - def diff(self, old_revision: str, new_revision: str, reverse: bool = False) -> str: - """Generate a relative diff for a subproject.""" - return self._diff_impl( - old_revision, new_revision, ignore=(Metadata.FILENAME,), reverse=reverse - ) - @abstractmethod def create_formatted_patch_header(self, patch_info: PatchInfo) -> str: """Create a formatted patch header for the given patch info.""" diff --git a/dfetch/project/superproject.py b/dfetch/project/superproject.py index 2382b0be..21a0f62b 100644 --- a/dfetch/project/superproject.py +++ b/dfetch/project/superproject.py @@ -13,6 +13,7 @@ import pathlib from abc import ABC, abstractmethod from collections.abc import Sequence +from dataclasses import dataclass from dfetch.log import get_logger from dfetch.manifest.manifest import Manifest @@ -21,13 +22,29 @@ from dfetch.project.gitsubproject import GitSubProject from dfetch.project.subproject import SubProject from dfetch.project.svnsubproject import SvnSubProject -from dfetch.util.util import resolve_absolute_path +from dfetch.util.util import ( + in_directory, + resolve_absolute_path, +) from dfetch.vcs.git import GitLocalRepo +from dfetch.vcs.patch import ( + combine_patches, + create_svn_patch_for_new_file, + reverse_patch, +) from dfetch.vcs.svn import SvnRepo logger = get_logger(__name__) +@dataclass +class RevisionRange: + """A revision pair.""" + + old: str + new: str | None + + class SuperProject(ABC): """Representation of the project containing the manifest. @@ -102,6 +119,16 @@ def get_file_revision(self, path: str | pathlib.Path) -> str: def import_projects() -> Sequence[ProjectEntry]: """Import projects from underlying superproject.""" + @abstractmethod + def diff( + self, + path: str | pathlib.Path, + revisions: RevisionRange, + ignore: Sequence[str], + reverse: bool = False, + ) -> str: + """Get the diff of two revisions.""" + class GitSuperProject(SuperProject): """A git specific superproject.""" @@ -192,6 +219,40 @@ def import_projects() -> Sequence[ProjectEntry]: return projects + def diff( + self, + path: str | pathlib.Path, + revisions: RevisionRange, + ignore: Sequence[str], + reverse: bool = False, + ) -> str: + """Get the diff of two revisions in the given path.""" + local_repo = GitLocalRepo(path) + diff_since_revision = str( + local_repo.create_diff(revisions.old, revisions.new, ignore, reverse) + ) + + if revisions.new: + return diff_since_revision + + combined_diff = [] + + if diff_since_revision: + combined_diff += [diff_since_revision] + + untracked_files_patch = str(local_repo.untracked_files_patch(ignore)) + if untracked_files_patch: + if reverse: + reversed_patch = reverse_patch(untracked_files_patch.encode("utf-8")) + if not reversed_patch: + raise RuntimeError( + "Failed to reverse untracked files patch; patch parsing returned empty." + ) + untracked_files_patch = reversed_patch + combined_diff += [untracked_files_patch] + + return "\n".join(combined_diff) + class SvnSuperProject(SuperProject): """A SVN specific superproject.""" @@ -263,6 +324,39 @@ def import_projects() -> Sequence[ProjectEntry]: return projects + def diff( + self, + path: str | pathlib.Path, + revisions: RevisionRange, + ignore: Sequence[str], + reverse: bool = False, + ) -> str: + """Get the diff between two revisions.""" + repo = SvnRepo(path) + if reverse: + if revisions.new: + revisions.new, revisions.old = revisions.old, revisions.new + + filtered = repo.create_diff(revisions.old, revisions.new, ignore) + + if revisions.new: + return filtered + + patches: list[bytes] = [filtered.encode("utf-8")] if filtered else [] + with in_directory(path): + for file_path in repo.untracked_files(".", ignore): + patch = create_svn_patch_for_new_file(file_path) + if patch: + patches.append(patch.encode("utf-8")) + + patch_str = combine_patches(patches) + + # SVN has no way of producing a reverse working copy patch, reverse ourselves + if reverse and not revisions.new: + patch_str = reverse_patch(patch_str.encode("UTF-8")) + + return patch_str + class NoVcsSuperProject(SuperProject): """A superproject without any version control.""" @@ -318,3 +412,13 @@ def import_projects() -> Sequence[ProjectEntry]: "Only git or SVN projects can be imported.", "Run this command within either a git or SVN repository", ) + + def diff( + self, + path: str | pathlib.Path, + revisions: RevisionRange, + ignore: Sequence[str], + reverse: bool = False, + ) -> str: + """Get the diff between two revisions.""" + return "" diff --git a/dfetch/project/svnsubproject.py b/dfetch/project/svnsubproject.py index 2e5f901e..82947c57 100644 --- a/dfetch/project/svnsubproject.py +++ b/dfetch/project/svnsubproject.py @@ -3,8 +3,6 @@ import os import pathlib import urllib.parse -from collections.abc import Sequence -from typing import Optional from dfetch.log import get_logger from dfetch.manifest.project import ProjectEntry @@ -13,15 +11,9 @@ from dfetch.util.util import ( find_matching_files, find_non_matching_files, - in_directory, safe_rm, ) -from dfetch.vcs.patch import ( - PatchInfo, - combine_patches, - create_svn_patch_for_new_file, - reverse_patch, -) +from dfetch.vcs.patch import PatchInfo from dfetch.vcs.svn import SvnRemote, SvnRepo, get_svn_version logger = get_logger(__name__) @@ -186,38 +178,6 @@ def _license_files(url_path: str) -> list[str]: def _get_revision(self, branch: str) -> str: return self._get_info(branch)["Revision"] - def _diff_impl( - self, - old_revision: str, - new_revision: Optional[str], - ignore: Sequence[str], - reverse: bool = False, - ) -> str: - """Get the diff between two revisions.""" - if reverse: - if new_revision: - new_revision, old_revision = old_revision, new_revision - - filtered = self._repo.create_diff(old_revision, new_revision, ignore) - - if new_revision: - return filtered - - patches: list[bytes] = [filtered.encode("utf-8")] if filtered else [] - with in_directory(self.local_path): - for file_path in self._repo.untracked_files(".", ignore): - patch = create_svn_patch_for_new_file(file_path) - if patch: - patches.append(patch.encode("utf-8")) - - patch_str = combine_patches(patches) - - # SVN has no way of producing a reverse working copy patch, reverse ourselves - if reverse and not new_revision: - patch_str = reverse_patch(patch_str.encode("UTF-8")) - - return patch_str - def get_default_branch(self) -> str: """Get the default branch of this repository.""" return self._repo.DEFAULT_BRANCH diff --git a/features/import-from-git.feature b/features/import-from-git.feature index 7b58d5a6..84173b01 100644 --- a/features/import-from-git.feature +++ b/features/import-from-git.feature @@ -4,7 +4,6 @@ Feature: Importing submodules from an existing git repository as easy as possible, a user should be able to generate a manifest that is filled with the submodules and their pinned versions. - @wip Scenario: Multiple submodules are imported Given a git repo with the following submodules | path | url | revision | diff --git a/tests/test_subproject.py b/tests/test_subproject.py index 17859be6..636cec6e 100644 --- a/tests/test_subproject.py +++ b/tests/test_subproject.py @@ -43,15 +43,6 @@ def wanted_version(self): def _list_of_tags(self): return [] - def _diff_impl( - self, - old_revision, - new_revision, - ignore, - reverse=False, - ): - return "" - def get_default_branch(self): return "" From e28053d2ddb3168962f32493659b33e538656147 Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Sat, 7 Feb 2026 12:22:57 +0100 Subject: [PATCH 07/11] Bump cunit to rev 172 after upstream update --- features/check-svn-repo.feature | 10 +++++----- features/fetch-svn-repo.feature | 2 +- features/freeze-projects.feature | 2 +- features/import-from-svn.feature | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/features/check-svn-repo.feature b/features/check-svn-repo.feature index d864cf40..485f2d72 100644 --- a/features/check-svn-repo.feature +++ b/features/check-svn-repo.feature @@ -15,7 +15,7 @@ Feature: Checking dependencies from a svn repository projects: - name: cunit-svn-rev-only - revision: '170' + revision: '172' vcs: svn dst: ext/cunit-svn-rev-only @@ -31,9 +31,9 @@ Feature: Checking dependencies from a svn repository """ Dfetch (0.11.0) cunit-svn-rev-only: - > wanted (170), available (trunk - 170) + > wanted (172), available (trunk - 172) cunit-svn-rev-and-branch: - > wanted (mingw64 - 156), available (mingw64 - 170) + > wanted (mingw64 - 156), available (mingw64 - 172) """ Scenario: A newer tag is available than in manifest @@ -96,9 +96,9 @@ Feature: Checking dependencies from a svn repository """ Dfetch (0.11.0) cunit-svn-rev-only: - > wanted (169), current (trunk - 169), available (trunk - 170) + > wanted (169), current (trunk - 169), available (trunk - 172) cunit-svn-rev-and-branch: - > wanted & current (mingw64 - 156), available (mingw64 - 170) + > wanted & current (mingw64 - 156), available (mingw64 - 172) ext/test-non-standard-svn: > wanted (latest), current (1), available (1) """ diff --git a/features/fetch-svn-repo.feature b/features/fetch-svn-repo.feature index f547b56d..6220302e 100644 --- a/features/fetch-svn-repo.feature +++ b/features/fetch-svn-repo.feature @@ -25,7 +25,7 @@ Feature: Fetching dependencies from a svn repository projects: - name: cunit-svn-rev-only - revision: '170' + revision: '172' vcs: svn dst: ext/cunit-svn-rev-only diff --git a/features/freeze-projects.feature b/features/freeze-projects.feature index c0cfb9b0..c3c853c8 100644 --- a/features/freeze-projects.feature +++ b/features/freeze-projects.feature @@ -55,7 +55,7 @@ Feature: Freeze dependencies projects: - name: cunit-svn - revision: '170' + revision: '172' url: svn://svn.code.sf.net/p/cunit/code branch: trunk vcs: svn diff --git a/features/import-from-svn.feature b/features/import-from-svn.feature index cbfd96d3..744ea790 100644 --- a/features/import-from-svn.feature +++ b/features/import-from-svn.feature @@ -4,11 +4,11 @@ Feature: Importing externals from an existing svn repository as easy as possible, a user should be able to generate a manifest that is filled with the externals and their pinned versions. - @remote-svn @wip + @remote-svn Scenario: Multiple externals are imported Given a svn repo with the following externals | path | url | revision | - | ext/cunit1 | https://svn.code.sf.net/p/cunit/code/trunk/Man | 170 | + | ext/cunit1 | https://svn.code.sf.net/p/cunit/code/trunk/Man | 172 | | ext/cunit2 | https://svn.code.sf.net/p/cunit/code/branches/mingw64/Man | 150 | | ext/cunit3 | https://svn.code.sf.net/p/cunit/code | | When I run "dfetch import" @@ -23,7 +23,7 @@ Feature: Importing externals from an existing svn repository projects: - name: ext/cunit1 - revision: '170' + revision: '172' src: Man dst: ./ext/cunit1 repo-path: code From 9ef6a88027e42282c086a72e1331a90e91d11c8e Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Sat, 7 Feb 2026 13:26:00 +0100 Subject: [PATCH 08/11] Review comments --- dfetch/project/superproject.py | 73 +++++++++++++++------------------- tests/test_import.py | 38 ++++++++++-------- 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/dfetch/project/superproject.py b/dfetch/project/superproject.py index 21a0f62b..8d1ece8a 100644 --- a/dfetch/project/superproject.py +++ b/dfetch/project/superproject.py @@ -106,6 +106,22 @@ def has_local_changes_in_dir(self, path: str) -> bool: def get_username(self) -> str: """Get the username of the superproject VCS.""" + def _get_username_fallback(self) -> str: + """Get the username of the superproject VCS.""" + username = "" + + if not username: + try: + username = getpass.getuser() + except (ImportError, KeyError, OSError): + username = "" + if not username: + try: + username = os.getlogin() + except OSError: + username = "unknown" + return username + @abstractmethod def get_useremail(self) -> str: """Get the user email of the superproject VCS.""" @@ -156,17 +172,10 @@ def get_username(self) -> str: """Get the username of the superproject VCS.""" username = GitLocalRepo(self.root_directory).get_username() - if not username: - try: - username = getpass.getuser() - except (ImportError, KeyError, OSError): - username = "" - if not username: - try: - username = os.getlogin() - except OSError: - username = "unknown" - return username + if username: + return username + + return self._get_username_fallback() def get_useremail(self) -> str: """Get the user email of the superproject VCS.""" @@ -280,17 +289,10 @@ def get_username(self) -> str: """Get the username of the superproject VCS.""" username = SvnRepo(self.root_directory).get_username() - if not username: - try: - username = getpass.getuser() - except (ImportError, KeyError, OSError): - username = "" - if not username: - try: - username = os.getlogin() - except OSError: - username = "unknown" - return username + if username: + return username + + return self._get_username_fallback() def get_useremail(self) -> str: """Get the user email of the superproject VCS.""" @@ -333,13 +335,14 @@ def diff( ) -> str: """Get the diff between two revisions.""" repo = SvnRepo(path) + new, old = revisions.new, revisions.old if reverse: - if revisions.new: - revisions.new, revisions.old = revisions.old, revisions.new + if new: + new, old = old, new - filtered = repo.create_diff(revisions.old, revisions.new, ignore) + filtered = repo.create_diff(old, new, ignore) - if revisions.new: + if new: return filtered patches: list[bytes] = [filtered.encode("utf-8")] if filtered else [] @@ -352,7 +355,7 @@ def diff( patch_str = combine_patches(patches) # SVN has no way of producing a reverse working copy patch, reverse ourselves - if reverse and not revisions.new: + if reverse and not new: patch_str = reverse_patch(patch_str.encode("UTF-8")) return patch_str @@ -382,19 +385,7 @@ def has_local_changes_in_dir(self, path: str) -> bool: def get_username(self) -> str: """Get the username of the superproject VCS.""" - username = "" - - if not username: - try: - username = getpass.getuser() - except (ImportError, KeyError, OSError): - username = "" - if not username: - try: - username = os.getlogin() - except OSError: - username = "unknown" - return username + return self._get_username_fallback() def get_useremail(self) -> str: """Get the user email of the superproject VCS.""" @@ -409,7 +400,7 @@ def get_file_revision(self, path: str | pathlib.Path) -> str: def import_projects() -> Sequence[ProjectEntry]: """Import projects from underlying superproject.""" raise RuntimeError( - "Only git or SVN projects can be imported.", + "Only git or SVN projects can be imported." "Run this command within either a git or SVN repository", ) diff --git a/tests/test_import.py b/tests/test_import.py index 8c06372c..a10aa145 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -48,27 +48,33 @@ def test_git_import(name, submodules): import_ = Import() - with patch( - "dfetch.project.superproject.GitLocalRepo.submodules" - ) as mocked_submodules: - with patch("dfetch.commands.import_.Manifest") as mocked_manifest: - mocked_submodules.return_value = submodules + with patch("dfetch.project.superproject.SvnRepo.is_svn") as is_svn: + with patch( + "dfetch.project.superproject.GitLocalRepo.submodules" + ) as mocked_submodules: + with patch("dfetch.commands.import_.Manifest") as mocked_manifest: + mocked_submodules.return_value = submodules + with patch("dfetch.project.superproject.GitLocalRepo.is_git") as is_git: + is_git.return_value = True + is_svn.return_value = False - if len(submodules) == 0: - with pytest.raises(RuntimeError): - import_(argparse.Namespace()) - else: - import_(argparse.Namespace()) + if len(submodules) == 0: + with pytest.raises(RuntimeError): + import_(argparse.Namespace()) + else: + import_(argparse.Namespace()) - mocked_manifest.assert_called() + mocked_manifest.assert_called() - args = mocked_manifest.call_args_list[0][0][0] + args = mocked_manifest.call_args_list[0][0][0] - for project_entry in args["projects"]: - assert project_entry.name in [subm.name for subm in submodules] + for project_entry in args["projects"]: + assert project_entry.name in [ + subm.name for subm in submodules + ] - # Manifest should have been dumped - mocked_manifest.return_value.dump.assert_called() + # Manifest should have been dumped + mocked_manifest.return_value.dump.assert_called() FIRST_EXTERNAL = External( From 530faf74d4b0a8616772236870346f9d3c14b9b5 Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Sat, 7 Feb 2026 16:39:08 +0100 Subject: [PATCH 09/11] Split super project --- dfetch/commands/check.py | 6 +- dfetch/commands/diff.py | 5 +- dfetch/commands/environment.py | 4 +- dfetch/commands/format_patch.py | 6 +- dfetch/commands/freeze.py | 8 +- dfetch/commands/import_.py | 4 +- dfetch/commands/report.py | 4 +- dfetch/commands/update.py | 6 +- dfetch/commands/update_patch.py | 13 +- dfetch/project/__init__.py | 43 ++++- dfetch/project/gitsuperproject.py | 145 ++++++++++++++++ dfetch/project/superproject.py | 275 +++--------------------------- dfetch/project/svnsuperproject.py | 131 ++++++++++++++ tests/test_check.py | 6 +- tests/test_import.py | 18 +- tests/test_report.py | 2 +- tests/test_update.py | 12 +- 17 files changed, 387 insertions(+), 301 deletions(-) create mode 100644 dfetch/project/gitsuperproject.py create mode 100644 dfetch/project/svnsuperproject.py diff --git a/dfetch/commands/check.py b/dfetch/commands/check.py index 52f7f2ce..937cac27 100644 --- a/dfetch/commands/check.py +++ b/dfetch/commands/check.py @@ -35,7 +35,7 @@ from dfetch.commands.common import check_child_manifests from dfetch.log import get_logger from dfetch.manifest.manifest import Manifest -from dfetch.project.superproject import SuperProject +from dfetch.project import create_super_project from dfetch.reporting.check.code_climate_reporter import CodeClimateReporter from dfetch.reporting.check.jenkins_reporter import JenkinsReporter from dfetch.reporting.check.reporter import CheckReporter @@ -90,14 +90,14 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the check.""" - superproject = SuperProject.create() + superproject = create_super_project() reporters = self._get_reporters(args, superproject.manifest) with in_directory(superproject.root_directory): exceptions: list[str] = [] for project in superproject.manifest.selected_projects(args.projects): with catch_runtime_exceptions(exceptions) as exceptions: - dfetch.project.make(project).check_for_update( + dfetch.project.create_sub_project(project).check_for_update( reporters, files_to_ignore=superproject.ignored_files(project.destination), ) diff --git a/dfetch/commands/diff.py b/dfetch/commands/diff.py index 1699abef..421e2919 100644 --- a/dfetch/commands/diff.py +++ b/dfetch/commands/diff.py @@ -59,8 +59,9 @@ import dfetch.commands.command from dfetch.log import get_logger +from dfetch.project import create_super_project from dfetch.project.metadata import Metadata -from dfetch.project.superproject import NoVcsSuperProject, RevisionRange, SuperProject +from dfetch.project.superproject import NoVcsSuperProject, RevisionRange from dfetch.util.util import catch_runtime_exceptions, in_directory logger = get_logger(__name__) @@ -96,7 +97,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the diff.""" - superproject = SuperProject.create() + superproject = create_super_project() old_rev, new_rev = self._parse_revs(args.revs) if isinstance(superproject, NoVcsSuperProject): diff --git a/dfetch/commands/environment.py b/dfetch/commands/environment.py index 7ce1e63a..57cb655b 100644 --- a/dfetch/commands/environment.py +++ b/dfetch/commands/environment.py @@ -5,7 +5,7 @@ import dfetch.commands.command from dfetch.log import get_logger -from dfetch.project import SUPPORTED_PROJECT_TYPES +from dfetch.project import SUPPORTED_SUBPROJECT_TYPES logger = get_logger(__name__) @@ -26,5 +26,5 @@ def __call__(self, _: argparse.Namespace) -> None: logger.print_report_line( "platform", f"{platform.system()} {platform.release()}" ) - for project_type in SUPPORTED_PROJECT_TYPES: + for project_type in SUPPORTED_SUBPROJECT_TYPES: project_type.list_tool_info() diff --git a/dfetch/commands/format_patch.py b/dfetch/commands/format_patch.py index 654da3a8..bb5c4cfa 100644 --- a/dfetch/commands/format_patch.py +++ b/dfetch/commands/format_patch.py @@ -33,7 +33,7 @@ import dfetch.manifest.project import dfetch.project from dfetch.log import get_logger -from dfetch.project.superproject import SuperProject +from dfetch.project import create_super_project from dfetch.util.util import catch_runtime_exceptions, in_directory from dfetch.vcs.patch import PatchAuthor, PatchInfo, add_prefix_to_patch @@ -69,7 +69,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the format patch.""" - superproject = SuperProject.create() + superproject = create_super_project() exceptions: list[str] = [] @@ -86,7 +86,7 @@ def __call__(self, args: argparse.Namespace) -> None: with in_directory(superproject.root_directory): for project in superproject.manifest.selected_projects(args.projects): with catch_runtime_exceptions(exceptions) as exceptions: - subproject = dfetch.project.make(project) + subproject = dfetch.project.create_sub_project(project) # Check if the project has a patch, maybe suggest creating one? if not subproject.patch: diff --git a/dfetch/commands/freeze.py b/dfetch/commands/freeze.py index 61e2dca6..c3e38137 100644 --- a/dfetch/commands/freeze.py +++ b/dfetch/commands/freeze.py @@ -49,7 +49,7 @@ from dfetch.log import get_logger from dfetch.manifest.manifest import Manifest from dfetch.manifest.project import ProjectEntry -from dfetch.project.superproject import SuperProject +from dfetch.project import create_super_project from dfetch.util.util import catch_runtime_exceptions, in_directory logger = get_logger(__name__) @@ -70,7 +70,7 @@ def __call__(self, args: argparse.Namespace) -> None: """Perform the freeze.""" del args # unused - superproject = SuperProject.create() + superproject = create_super_project() exceptions: list[str] = [] projects: list[ProjectEntry] = [] @@ -78,7 +78,9 @@ def __call__(self, args: argparse.Namespace) -> None: with in_directory(superproject.root_directory): for project in superproject.manifest.projects: with catch_runtime_exceptions(exceptions) as exceptions: - on_disk_version = dfetch.project.make(project).on_disk_version() + on_disk_version = dfetch.project.create_sub_project( + project + ).on_disk_version() if project.version == on_disk_version: logger.print_info_line( diff --git a/dfetch/commands/import_.py b/dfetch/commands/import_.py index cb98337e..39ff43b8 100644 --- a/dfetch/commands/import_.py +++ b/dfetch/commands/import_.py @@ -92,7 +92,7 @@ from dfetch.manifest.manifest import Manifest from dfetch.manifest.project import ProjectEntry from dfetch.manifest.remote import Remote -from dfetch.project.superproject import SuperProject +from dfetch.project import determine_superproject_vcs logger = get_logger(__name__) @@ -111,7 +111,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, _: argparse.Namespace) -> None: """Perform the import.""" - projects = SuperProject.type_from_path(".").import_projects() + projects = determine_superproject_vcs(".").import_projects() if not projects: raise RuntimeError(f"No submodules found in {os.getcwd()}!") diff --git a/dfetch/commands/report.py b/dfetch/commands/report.py index cf378229..d54ee31c 100644 --- a/dfetch/commands/report.py +++ b/dfetch/commands/report.py @@ -12,9 +12,9 @@ import dfetch.util.util from dfetch.log import get_logger from dfetch.manifest.project import ProjectEntry +from dfetch.project import create_super_project from dfetch.project.metadata import Metadata from dfetch.project.subproject import SubProject -from dfetch.project.superproject import SuperProject from dfetch.reporting import REPORTERS, ReportTypes from dfetch.util.license import License, guess_license_in_file @@ -63,7 +63,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Generate the report.""" - superproject = SuperProject.create() + superproject = create_super_project() with dfetch.util.util.in_directory(superproject.root_directory): reporter = REPORTERS[args.type](superproject.manifest) diff --git a/dfetch/commands/update.py b/dfetch/commands/update.py index fdb22b2e..80d6e700 100644 --- a/dfetch/commands/update.py +++ b/dfetch/commands/update.py @@ -36,7 +36,7 @@ import dfetch.project from dfetch.commands.common import check_child_manifests from dfetch.log import get_logger -from dfetch.project.superproject import SuperProject +from dfetch.project import create_super_project from dfetch.util.util import catch_runtime_exceptions, in_directory logger = get_logger(__name__) @@ -74,7 +74,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the update.""" - superproject = SuperProject.create() + superproject = create_super_project() exceptions: list[str] = [] destinations: list[str] = [ @@ -85,7 +85,7 @@ def __call__(self, args: argparse.Namespace) -> None: for project in superproject.manifest.selected_projects(args.projects): with catch_runtime_exceptions(exceptions) as exceptions: self._check_destination(project, destinations) - dfetch.project.make(project).update( + dfetch.project.create_sub_project(project).update( force=args.force, files_to_ignore=superproject.ignored_files(project.destination), ) diff --git a/dfetch/commands/update_patch.py b/dfetch/commands/update_patch.py index 7176c9de..daba2549 100644 --- a/dfetch/commands/update_patch.py +++ b/dfetch/commands/update_patch.py @@ -40,13 +40,10 @@ import dfetch.manifest.project import dfetch.project from dfetch.log import get_logger +from dfetch.project import create_super_project +from dfetch.project.gitsuperproject import GitSuperProject from dfetch.project.metadata import Metadata -from dfetch.project.superproject import ( - GitSuperProject, - NoVcsSuperProject, - RevisionRange, - SuperProject, -) +from dfetch.project.superproject import NoVcsSuperProject, RevisionRange from dfetch.util.util import catch_runtime_exceptions, in_directory logger = get_logger(__name__) @@ -76,7 +73,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the update patch.""" - superproject = SuperProject.create() + superproject = create_super_project() exceptions: list[str] = [] @@ -91,7 +88,7 @@ def __call__(self, args: argparse.Namespace) -> None: with in_directory(superproject.root_directory): for project in superproject.manifest.selected_projects(args.projects): with catch_runtime_exceptions(exceptions) as exceptions: - subproject = dfetch.project.make(project) + subproject = dfetch.project.create_sub_project(project) files_to_ignore = superproject.ignored_files(project.destination) diff --git a/dfetch/project/__init__.py b/dfetch/project/__init__.py index 108f5d37..7c45da7a 100644 --- a/dfetch/project/__init__.py +++ b/dfetch/project/__init__.py @@ -1,22 +1,57 @@ """All Project related items.""" +import os +import pathlib +from typing import Union + import dfetch.manifest.project +from dfetch.log import get_logger +from dfetch.manifest.parse import find_manifest, parse from dfetch.project.gitsubproject import GitSubProject +from dfetch.project.gitsuperproject import GitSuperProject from dfetch.project.subproject import SubProject +from dfetch.project.superproject import NoVcsSuperProject, SuperProject from dfetch.project.svnsubproject import SvnSubProject +from dfetch.project.svnsuperproject import SvnSuperProject +from dfetch.util.util import resolve_absolute_path + +SUPPORTED_SUBPROJECT_TYPES = [GitSubProject, SvnSubProject] +SUPPORTED_SUPERPROJECT_TYPES = [GitSuperProject, SvnSuperProject] -SUPPORTED_PROJECT_TYPES = [GitSubProject, SvnSubProject] +logger = get_logger(__name__) -def make(project_entry: dfetch.manifest.project.ProjectEntry) -> SubProject: +def create_sub_project( + project_entry: dfetch.manifest.project.ProjectEntry, +) -> SubProject: """Create a new SubProject based on a project from the manifest.""" - for project_type in SUPPORTED_PROJECT_TYPES: + for project_type in SUPPORTED_SUBPROJECT_TYPES: if project_type.NAME == project_entry.vcs: return project_type(project_entry) - for project_type in SUPPORTED_PROJECT_TYPES: + for project_type in SUPPORTED_SUBPROJECT_TYPES: project = project_type(project_entry) if project.check(): return project raise RuntimeError("vcs type unsupported") + + +def create_super_project() -> SuperProject: + """Create a SuperProject by looking for a manifest file.""" + logger.debug("Looking for manifest") + manifest_path = find_manifest() + + logger.debug(f"Using manifest {manifest_path}") + manifest = parse(manifest_path) + root_directory = resolve_absolute_path(os.path.dirname(manifest.path)) + return determine_superproject_vcs(root_directory)(manifest, root_directory) + + +def determine_superproject_vcs(path: Union[str, pathlib.Path]) -> type[SuperProject]: + """Determine correct VCS type of the superproject in the given path.""" + for project_type in SUPPORTED_SUPERPROJECT_TYPES: + if project_type.check(path): + return project_type + + return NoVcsSuperProject diff --git a/dfetch/project/gitsuperproject.py b/dfetch/project/gitsuperproject.py new file mode 100644 index 00000000..c7f2e245 --- /dev/null +++ b/dfetch/project/gitsuperproject.py @@ -0,0 +1,145 @@ +"""Git Super project abstraction. + +This module provides the specific GitSuperProject class which represents +a git project that contains the `dfetch.yaml` manifest file (the "super project"). +""" + +from __future__ import annotations + +import os +import pathlib +from collections.abc import Sequence + +from dfetch.log import get_logger +from dfetch.manifest.project import ProjectEntry +from dfetch.project.gitsubproject import GitSubProject +from dfetch.project.subproject import SubProject +from dfetch.project.superproject import RevisionRange, SuperProject +from dfetch.util.util import resolve_absolute_path +from dfetch.vcs.git import GitLocalRepo +from dfetch.vcs.patch import reverse_patch + +logger = get_logger(__name__) + + +class GitSuperProject(SuperProject): + """A git specific superproject.""" + + @staticmethod + def check(path: str | pathlib.Path) -> bool: + """Check if this path is of the matching VCS.""" + return GitLocalRepo(path).is_git() + + def get_sub_project(self, project: ProjectEntry) -> SubProject | None: + """Get the subproject in the same vcs type as the superproject.""" + return GitSubProject(project) + + def ignored_files(self, path: str) -> Sequence[str]: + """Return a list of files that can be ignored in a given path.""" + resolved_path = resolve_absolute_path(path) + + if not resolved_path.is_relative_to(self.root_directory): + raise RuntimeError( + f"{resolved_path} not in superproject {self.root_directory}!" + ) + + return GitLocalRepo.ignored_files(path) + + def has_local_changes_in_dir(self, path: str) -> bool: + """Check if the superproject has local changes.""" + return GitLocalRepo.any_changes_or_untracked(path) + + def get_username(self) -> str: + """Get the username of the superproject VCS.""" + username = GitLocalRepo(self.root_directory).get_username() + + if username: + return username + + return self._get_username_fallback() + + def get_useremail(self) -> str: + """Get the user email of the superproject VCS.""" + email = GitLocalRepo(self.root_directory).get_useremail() + + if email: + return email + return self._get_useremail_fallback() + + def get_file_revision(self, path: str | pathlib.Path) -> str: + """Get the revision of the given file.""" + return str(GitLocalRepo(self.root_directory).get_last_file_hash(str(path))) + + @staticmethod + def import_projects() -> Sequence[ProjectEntry]: + """Import projects from underlying superproject.""" + projects: list[ProjectEntry] = [] + toplevel: str = "" + for submodule in GitLocalRepo.submodules(): + projects.append( + ProjectEntry( + { + "name": submodule.name, + "revision": submodule.sha, + "url": submodule.url, + "dst": submodule.path, + "branch": submodule.branch, + "tag": submodule.tag, + } + ) + ) + logger.info(f"Found {submodule.name}") + + if not toplevel: + toplevel = submodule.toplevel + elif toplevel != submodule.toplevel: + raise RuntimeError( + "Recursive submodules not (yet) supported. Check manifest!" + ) + + if os.path.realpath(toplevel) != os.getcwd(): + logger.warning( + "\n".join( + ( + f'The toplevel directory is in "{toplevel}"', + f'Import was done from "{os.getcwd()}"', + "All projects paths will be relative to the current directory dfetch is running!", + ) + ) + ) + + return projects + + def diff( + self, + path: str | pathlib.Path, + revisions: RevisionRange, + ignore: Sequence[str], + reverse: bool = False, + ) -> str: + """Get the diff of two revisions in the given path.""" + local_repo = GitLocalRepo(path) + diff_since_revision = str( + local_repo.create_diff(revisions.old, revisions.new, ignore, reverse) + ) + + if revisions.new: + return diff_since_revision + + combined_diff = [] + + if diff_since_revision: + combined_diff += [diff_since_revision] + + untracked_files_patch = str(local_repo.untracked_files_patch(ignore)) + if untracked_files_patch: + if reverse: + reversed_patch = reverse_patch(untracked_files_patch.encode("utf-8")) + if not reversed_patch: + raise RuntimeError( + "Failed to reverse untracked files patch; patch parsing returned empty." + ) + untracked_files_patch = reversed_patch + combined_diff += [untracked_files_patch] + + return "\n".join(combined_diff) diff --git a/dfetch/project/superproject.py b/dfetch/project/superproject.py index 8d1ece8a..ea1fd637 100644 --- a/dfetch/project/superproject.py +++ b/dfetch/project/superproject.py @@ -17,22 +17,9 @@ from dfetch.log import get_logger from dfetch.manifest.manifest import Manifest -from dfetch.manifest.parse import find_manifest, parse from dfetch.manifest.project import ProjectEntry -from dfetch.project.gitsubproject import GitSubProject from dfetch.project.subproject import SubProject -from dfetch.project.svnsubproject import SvnSubProject -from dfetch.util.util import ( - in_directory, - resolve_absolute_path, -) -from dfetch.vcs.git import GitLocalRepo -from dfetch.vcs.patch import ( - combine_patches, - create_svn_patch_for_new_file, - reverse_patch, -) -from dfetch.vcs.svn import SvnRepo +from dfetch.util.util import resolve_absolute_path logger = get_logger(__name__) @@ -58,28 +45,6 @@ def __init__(self, manifest: Manifest, root_directory: pathlib.Path) -> None: self._manifest = manifest self._root_directory = root_directory - @staticmethod - def create() -> SuperProject: - """Create a SuperProject by looking for a manifest file.""" - logger.debug("Looking for manifest") - manifest_path = find_manifest() - - logger.debug(f"Using manifest {manifest_path}") - manifest = parse(manifest_path) - root_directory = resolve_absolute_path(os.path.dirname(manifest.path)) - return SuperProject.type_from_path(root_directory)(manifest, root_directory) - - @staticmethod - def type_from_path(path: str | pathlib.Path) -> type[SuperProject]: - """Determine correct VCS type of the superproject in the given path.""" - if GitLocalRepo(path).is_git(): - return GitSuperProject - - if SvnRepo(path).is_svn(): - return SvnSuperProject - - return NoVcsSuperProject - @property def root_directory(self) -> pathlib.Path: """Return the directory that contains the manifest file.""" @@ -90,6 +55,11 @@ def manifest(self) -> Manifest: """The manifest of the super project.""" return self._manifest + @staticmethod + @abstractmethod + def check(path: str | pathlib.Path) -> bool: + """Check if this path is of the matching VCS.""" + @abstractmethod def get_sub_project(self, project: ProjectEntry) -> SubProject | None: """Get the subproject in the same vcs type as the superproject.""" @@ -126,206 +96,21 @@ def _get_username_fallback(self) -> str: def get_useremail(self) -> str: """Get the user email of the superproject VCS.""" - @abstractmethod - def get_file_revision(self, path: str | pathlib.Path) -> str: - """Get the revision of the given file.""" - - @staticmethod - @abstractmethod - def import_projects() -> Sequence[ProjectEntry]: - """Import projects from underlying superproject.""" - - @abstractmethod - def diff( - self, - path: str | pathlib.Path, - revisions: RevisionRange, - ignore: Sequence[str], - reverse: bool = False, - ) -> str: - """Get the diff of two revisions.""" - - -class GitSuperProject(SuperProject): - """A git specific superproject.""" - - def get_sub_project(self, project: ProjectEntry) -> SubProject | None: - """Get the subproject in the same vcs type as the superproject.""" - return GitSubProject(project) - - def ignored_files(self, path: str) -> Sequence[str]: - """Return a list of files that can be ignored in a given path.""" - resolved_path = resolve_absolute_path(path) - - if not resolved_path.is_relative_to(self.root_directory): - raise RuntimeError( - f"{resolved_path} not in superproject {self.root_directory}!" - ) - - return GitLocalRepo.ignored_files(path) - - def has_local_changes_in_dir(self, path: str) -> bool: - """Check if the superproject has local changes.""" - return GitLocalRepo.any_changes_or_untracked(path) - - def get_username(self) -> str: - """Get the username of the superproject VCS.""" - username = GitLocalRepo(self.root_directory).get_username() - - if username: - return username - - return self._get_username_fallback() - - def get_useremail(self) -> str: - """Get the user email of the superproject VCS.""" - email = GitLocalRepo(self.root_directory).get_useremail() - - username = self.get_username() or "unknown" - return email or f"{username}@example.com" - - def get_file_revision(self, path: str | pathlib.Path) -> str: - """Get the revision of the given file.""" - return str(GitLocalRepo(self.root_directory).get_last_file_hash(str(path))) - - @staticmethod - def import_projects() -> Sequence[ProjectEntry]: - """Import projects from underlying superproject.""" - projects: list[ProjectEntry] = [] - toplevel: str = "" - for submodule in GitLocalRepo.submodules(): - projects.append( - ProjectEntry( - { - "name": submodule.name, - "revision": submodule.sha, - "url": submodule.url, - "dst": submodule.path, - "branch": submodule.branch, - "tag": submodule.tag, - } - ) - ) - logger.info(f"Found {submodule.name}") - - if not toplevel: - toplevel = submodule.toplevel - elif toplevel != submodule.toplevel: - raise RuntimeError( - "Recursive submodules not (yet) supported. Check manifest!" - ) - - if os.path.realpath(toplevel) != os.getcwd(): - logger.warning( - "\n".join( - ( - f'The toplevel directory is in "{toplevel}"', - f'Import was done from "{os.getcwd()}"', - "All projects paths will be relative to the current directory dfetch is running!", - ) - ) - ) - - return projects - - def diff( - self, - path: str | pathlib.Path, - revisions: RevisionRange, - ignore: Sequence[str], - reverse: bool = False, - ) -> str: - """Get the diff of two revisions in the given path.""" - local_repo = GitLocalRepo(path) - diff_since_revision = str( - local_repo.create_diff(revisions.old, revisions.new, ignore, reverse) - ) - - if revisions.new: - return diff_since_revision - - combined_diff = [] - - if diff_since_revision: - combined_diff += [diff_since_revision] - - untracked_files_patch = str(local_repo.untracked_files_patch(ignore)) - if untracked_files_patch: - if reverse: - reversed_patch = reverse_patch(untracked_files_patch.encode("utf-8")) - if not reversed_patch: - raise RuntimeError( - "Failed to reverse untracked files patch; patch parsing returned empty." - ) - untracked_files_patch = reversed_patch - combined_diff += [untracked_files_patch] - - return "\n".join(combined_diff) - - -class SvnSuperProject(SuperProject): - """A SVN specific superproject.""" - - def get_sub_project(self, project: ProjectEntry) -> SubProject | None: - """Get the subproject in the same vcs type as the superproject.""" - return SvnSubProject(project) - - def ignored_files(self, path: str) -> Sequence[str]: - """Return a list of files that can be ignored in a given path.""" - resolved_path = resolve_absolute_path(path) - - if not resolved_path.is_relative_to(self.root_directory): - raise RuntimeError( - f"{resolved_path} not in superproject {self.root_directory}!" - ) - - return SvnRepo.ignored_files(path) - - def has_local_changes_in_dir(self, path: str) -> bool: - """Check if the superproject has local changes.""" - return SvnRepo.any_changes_or_untracked(path) - - def get_username(self) -> str: - """Get the username of the superproject VCS.""" - username = SvnRepo(self.root_directory).get_username() - - if username: - return username - - return self._get_username_fallback() - - def get_useremail(self) -> str: + def _get_useremail_fallback(self) -> str: """Get the user email of the superproject VCS.""" username = self.get_username() or "unknown" return f"{username}@example.com" + @abstractmethod def get_file_revision(self, path: str | pathlib.Path) -> str: """Get the revision of the given file.""" - return str(SvnRepo(self.root_directory).get_last_changed_revision(str(path))) @staticmethod + @abstractmethod def import_projects() -> Sequence[ProjectEntry]: """Import projects from underlying superproject.""" - projects: list[ProjectEntry] = [] - - for external in SvnRepo(os.getcwd()).externals(): - projects.append( - ProjectEntry( - { - "name": external.name, - "revision": external.revision, - "url": external.url, - "dst": external.path, - "branch": external.branch, - "tag": external.tag, - "src": external.src, - } - ) - ) - logger.info(f"Found {external.name}") - - return projects + @abstractmethod def diff( self, path: str | pathlib.Path, @@ -333,37 +118,18 @@ def diff( ignore: Sequence[str], reverse: bool = False, ) -> str: - """Get the diff between two revisions.""" - repo = SvnRepo(path) - new, old = revisions.new, revisions.old - if reverse: - if new: - new, old = old, new - - filtered = repo.create_diff(old, new, ignore) - - if new: - return filtered - - patches: list[bytes] = [filtered.encode("utf-8")] if filtered else [] - with in_directory(path): - for file_path in repo.untracked_files(".", ignore): - patch = create_svn_patch_for_new_file(file_path) - if patch: - patches.append(patch.encode("utf-8")) - - patch_str = combine_patches(patches) - - # SVN has no way of producing a reverse working copy patch, reverse ourselves - if reverse and not new: - patch_str = reverse_patch(patch_str.encode("UTF-8")) - - return patch_str + """Get the diff of two revisions.""" class NoVcsSuperProject(SuperProject): """A superproject without any version control.""" + @staticmethod + def check(path: str | pathlib.Path) -> bool: + """Check if this path is of the matching VCS.""" + del path # unused arg + return True + def get_sub_project(self, project: ProjectEntry) -> SubProject | None: """Get the subproject in the same vcs type as the superproject.""" return None @@ -389,8 +155,7 @@ def get_username(self) -> str: def get_useremail(self) -> str: """Get the user email of the superproject VCS.""" - username = self.get_username() or "unknown" - return f"{username}@example.com" + return self._get_useremail_fallback() def get_file_revision(self, path: str | pathlib.Path) -> str: """Get the revision of the given file.""" @@ -412,4 +177,8 @@ def diff( reverse: bool = False, ) -> str: """Get the diff between two revisions.""" + del path # unused arg + del revisions # unused arg + del ignore # unused arg + del reverse # unused arg return "" diff --git a/dfetch/project/svnsuperproject.py b/dfetch/project/svnsuperproject.py new file mode 100644 index 00000000..3afbeba2 --- /dev/null +++ b/dfetch/project/svnsuperproject.py @@ -0,0 +1,131 @@ +"""Svn Super project abstraction. + +This module provides the specific SvnSuperProject class which represents +a svn project that contains the `dfetch.yaml` manifest file (the "super project"). +""" + +from __future__ import annotations + +import os +import pathlib +from collections.abc import Sequence + +from dfetch.log import get_logger +from dfetch.manifest.project import ProjectEntry +from dfetch.project.subproject import SubProject +from dfetch.project.superproject import RevisionRange, SuperProject +from dfetch.project.svnsubproject import SvnSubProject +from dfetch.util.util import ( + in_directory, + resolve_absolute_path, +) +from dfetch.vcs.patch import ( + combine_patches, + create_svn_patch_for_new_file, + reverse_patch, +) +from dfetch.vcs.svn import SvnRepo + +logger = get_logger(__name__) + + +class SvnSuperProject(SuperProject): + """A SVN specific superproject.""" + + @staticmethod + def check(path: str | pathlib.Path) -> bool: + """Check if this path is of the matching VCS.""" + return SvnRepo(path).is_svn() + + def get_sub_project(self, project: ProjectEntry) -> SubProject | None: + """Get the subproject in the same vcs type as the superproject.""" + return SvnSubProject(project) + + def ignored_files(self, path: str) -> Sequence[str]: + """Return a list of files that can be ignored in a given path.""" + resolved_path = resolve_absolute_path(path) + + if not resolved_path.is_relative_to(self.root_directory): + raise RuntimeError( + f"{resolved_path} not in superproject {self.root_directory}!" + ) + + return SvnRepo.ignored_files(path) + + def has_local_changes_in_dir(self, path: str) -> bool: + """Check if the superproject has local changes.""" + return SvnRepo.any_changes_or_untracked(path) + + def get_username(self) -> str: + """Get the username of the superproject VCS.""" + username = SvnRepo(self.root_directory).get_username() + + if username: + return username + + return self._get_username_fallback() + + def get_useremail(self) -> str: + """Get the user email of the superproject VCS.""" + return self._get_useremail_fallback() + + def get_file_revision(self, path: str | pathlib.Path) -> str: + """Get the revision of the given file.""" + return str(SvnRepo(self.root_directory).get_last_changed_revision(str(path))) + + @staticmethod + def import_projects() -> Sequence[ProjectEntry]: + """Import projects from underlying superproject.""" + projects: list[ProjectEntry] = [] + + for external in SvnRepo(os.getcwd()).externals(): + projects.append( + ProjectEntry( + { + "name": external.name, + "revision": external.revision, + "url": external.url, + "dst": external.path, + "branch": external.branch, + "tag": external.tag, + "src": external.src, + } + ) + ) + logger.info(f"Found {external.name}") + + return projects + + def diff( + self, + path: str | pathlib.Path, + revisions: RevisionRange, + ignore: Sequence[str], + reverse: bool = False, + ) -> str: + """Get the diff between two revisions.""" + repo = SvnRepo(path) + new, old = revisions.new, revisions.old + if reverse: + if new: + new, old = old, new + + filtered = repo.create_diff(old, new, ignore) + + if new: + return filtered + + patches: list[bytes] = [filtered.encode("utf-8")] if filtered else [] + with in_directory(path): + for file_path in repo.untracked_files(".", ignore): + patch = create_svn_patch_for_new_file(file_path) + if patch: + patches.append(patch.encode("utf-8")) + + patch_str = combine_patches(patches) + + # SVN has no way of producing a reverse working copy patch, reverse ourselves + if reverse and not new: + patch_str = reverse_patch(patch_str.encode("UTF-8")) + + return patch_str diff --git a/tests/test_check.py b/tests/test_check.py index 99ffeb8e..73423cf6 100644 --- a/tests/test_check.py +++ b/tests/test_check.py @@ -34,12 +34,12 @@ def test_check(name, projects): fake_superproject.root_directory = Path("/tmp") with patch( - "dfetch.commands.check.SuperProject.create", return_value=fake_superproject + "dfetch.commands.check.create_super_project", return_value=fake_superproject ): with patch( "dfetch.manifest.parse.get_childmanifests" ) as mocked_get_childmanifests: - with patch("dfetch.project.make") as mocked_make: + with patch("dfetch.project.create_sub_project") as mocked_create: with patch("os.path.exists"): with patch("dfetch.commands.check.in_directory"): with patch("dfetch.commands.check.CheckStdoutReporter"): @@ -48,4 +48,4 @@ def test_check(name, projects): check(DEFAULT_ARGS) for _ in projects: - mocked_make.return_value.check_for_update.assert_called() + mocked_create.return_value.check_for_update.assert_called() diff --git a/tests/test_import.py b/tests/test_import.py index a10aa145..9692c50a 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -48,13 +48,15 @@ def test_git_import(name, submodules): import_ = Import() - with patch("dfetch.project.superproject.SvnRepo.is_svn") as is_svn: + with patch("dfetch.project.svnsuperproject.SvnRepo.is_svn") as is_svn: with patch( - "dfetch.project.superproject.GitLocalRepo.submodules" + "dfetch.project.gitsuperproject.GitLocalRepo.submodules" ) as mocked_submodules: with patch("dfetch.commands.import_.Manifest") as mocked_manifest: mocked_submodules.return_value = submodules - with patch("dfetch.project.superproject.GitLocalRepo.is_git") as is_git: + with patch( + "dfetch.project.gitsuperproject.GitLocalRepo.is_git" + ) as is_git: is_git.return_value = True is_svn.return_value = False @@ -113,10 +115,14 @@ def test_git_import(name, submodules): def test_svn_import(name, externals): import_ = Import() - with patch("dfetch.project.superproject.SvnRepo.is_svn") as is_svn: - with patch("dfetch.project.superproject.SvnRepo.externals") as mocked_externals: + with patch("dfetch.project.svnsuperproject.SvnRepo.is_svn") as is_svn: + with patch( + "dfetch.project.svnsuperproject.SvnRepo.externals" + ) as mocked_externals: with patch("dfetch.commands.import_.Manifest") as mocked_manifest: - with patch("dfetch.project.superproject.GitLocalRepo.is_git") as is_git: + with patch( + "dfetch.project.gitsuperproject.GitLocalRepo.is_git" + ) as is_git: is_git.return_value = False is_svn.return_value = True mocked_externals.return_value = externals diff --git a/tests/test_report.py b/tests/test_report.py index 6b83db4a..ef210c89 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -34,7 +34,7 @@ def test_report(name, projects): fake_superproject.root_directory = Path("/tmp") with patch( - "dfetch.commands.report.SuperProject.create", return_value=fake_superproject + "dfetch.commands.report.create_super_project", return_value=fake_superproject ): with patch("dfetch.log.DLogger.print_report_line") as mocked_print_report_line: diff --git a/tests/test_update.py b/tests/test_update.py index 2b35003a..487973aa 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -34,12 +34,12 @@ def test_update(name, projects): fake_superproject.root_directory = Path("/tmp") with patch( - "dfetch.commands.update.SuperProject.create", return_value=fake_superproject + "dfetch.commands.update.create_super_project", return_value=fake_superproject ): with patch( "dfetch.manifest.parse.get_childmanifests" ) as mocked_get_childmanifests: - with patch("dfetch.project.make") as mocked_make: + with patch("dfetch.project.create_sub_project") as mocked_create: with patch("os.path.exists"): with patch("dfetch.commands.update.in_directory"): with patch("dfetch.commands.update.Update._check_destination"): @@ -48,7 +48,7 @@ def test_update(name, projects): update(DEFAULT_ARGS) for _ in projects: - mocked_make.return_value.update.assert_called() + mocked_create.return_value.update.assert_called() def test_forced_update(): @@ -60,12 +60,12 @@ def test_forced_update(): fake_superproject.ignored_files.return_value = [] with patch( - "dfetch.commands.update.SuperProject.create", return_value=fake_superproject + "dfetch.commands.update.create_super_project", return_value=fake_superproject ): with patch( "dfetch.manifest.parse.get_childmanifests" ) as mocked_get_childmanifests: - with patch("dfetch.project.make") as mocked_make: + with patch("dfetch.project.create_sub_project") as mocked_create: with patch("os.path.exists"): with patch("dfetch.commands.update.in_directory"): with patch("dfetch.commands.update.Update._check_destination"): @@ -78,7 +78,7 @@ def test_forced_update(): ) update(args) - mocked_make.return_value.update.assert_called_once_with( + mocked_create.return_value.update.assert_called_once_with( force=True, files_to_ignore=[] ) From ffcd2b532560aaa29084a5a95fbfb6525427dd6d Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Sat, 7 Feb 2026 16:59:09 +0100 Subject: [PATCH 10/11] Review comments --- dfetch/commands/update_patch.py | 2 +- dfetch/project/gitsuperproject.py | 4 ++-- dfetch/project/superproject.py | 14 ++++++-------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/dfetch/commands/update_patch.py b/dfetch/commands/update_patch.py index daba2549..54ab8346 100644 --- a/dfetch/commands/update_patch.py +++ b/dfetch/commands/update_patch.py @@ -129,7 +129,7 @@ def __call__(self, args: argparse.Namespace) -> None: patch_text = superproject.diff( subproject.local_path, revisions=RevisionRange("", ""), - ignore=[Metadata.FILENAME], + ignore=(Metadata.FILENAME,), reverse=True, ) diff --git a/dfetch/project/gitsuperproject.py b/dfetch/project/gitsuperproject.py index c7f2e245..69a951f1 100644 --- a/dfetch/project/gitsuperproject.py +++ b/dfetch/project/gitsuperproject.py @@ -119,8 +119,8 @@ def diff( ) -> str: """Get the diff of two revisions in the given path.""" local_repo = GitLocalRepo(path) - diff_since_revision = str( - local_repo.create_diff(revisions.old, revisions.new, ignore, reverse) + diff_since_revision = local_repo.create_diff( + revisions.old, revisions.new, ignore, reverse ) if revisions.new: diff --git a/dfetch/project/superproject.py b/dfetch/project/superproject.py index ea1fd637..e7c8a199 100644 --- a/dfetch/project/superproject.py +++ b/dfetch/project/superproject.py @@ -24,7 +24,7 @@ logger = get_logger(__name__) -@dataclass +@dataclass(frozen=True) class RevisionRange: """A revision pair.""" @@ -78,13 +78,11 @@ def get_username(self) -> str: def _get_username_fallback(self) -> str: """Get the username of the superproject VCS.""" - username = "" + try: + username = getpass.getuser() + except (ImportError, KeyError, OSError): + username = "" - if not username: - try: - username = getpass.getuser() - except (ImportError, KeyError, OSError): - username = "" if not username: try: username = os.getlogin() @@ -165,7 +163,7 @@ def get_file_revision(self, path: str | pathlib.Path) -> str: def import_projects() -> Sequence[ProjectEntry]: """Import projects from underlying superproject.""" raise RuntimeError( - "Only git or SVN projects can be imported." + "Only git or SVN projects can be imported. " "Run this command within either a git or SVN repository", ) From 62366c5de9105ef320cdc22d6f2cebc965d7da04 Mon Sep 17 00:00:00 2001 From: Ben Spoor Date: Sat, 7 Feb 2026 17:07:28 +0100 Subject: [PATCH 11/11] Cleanup --- dfetch/project/gitsubproject.py | 6 +++--- dfetch/project/gitsuperproject.py | 14 ++++++++++---- dfetch/project/svnsubproject.py | 11 +++++------ dfetch/project/svnsuperproject.py | 10 ++++++++-- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/dfetch/project/gitsubproject.py b/dfetch/project/gitsubproject.py index 7adbc9bc..53df09a4 100644 --- a/dfetch/project/gitsubproject.py +++ b/dfetch/project/gitsubproject.py @@ -24,7 +24,6 @@ def __init__(self, project: ProjectEntry): """Create a Git subproject.""" super().__init__(project) self._remote_repo = GitRemote(self.remote) - self._local_repo = GitLocalRepo(self.local_path) def check(self) -> bool: """Check if is GIT.""" @@ -70,7 +69,8 @@ def _fetch_impl(self, version: Version) -> Version: f"/{name.upper()}" for name in self.LICENSE_GLOBS ] - fetched_sha = self._local_repo.checkout_version( + local_repo = GitLocalRepo(self.local_path) + fetched_sha = local_repo.checkout_version( remote=self.remote, version=rev_or_branch_or_tag, src=self.source, @@ -78,7 +78,7 @@ def _fetch_impl(self, version: Version) -> Version: ignore=self.ignore, ) - safe_rmtree(os.path.join(self.local_path, self._local_repo.METADATA_DIR)) + safe_rmtree(os.path.join(self.local_path, local_repo.METADATA_DIR)) return self._determine_fetched_version(version, fetched_sha) diff --git a/dfetch/project/gitsuperproject.py b/dfetch/project/gitsuperproject.py index 69a951f1..4614ad2d 100644 --- a/dfetch/project/gitsuperproject.py +++ b/dfetch/project/gitsuperproject.py @@ -11,6 +11,7 @@ from collections.abc import Sequence from dfetch.log import get_logger +from dfetch.manifest.manifest import Manifest from dfetch.manifest.project import ProjectEntry from dfetch.project.gitsubproject import GitSubProject from dfetch.project.subproject import SubProject @@ -25,6 +26,11 @@ class GitSuperProject(SuperProject): """A git specific superproject.""" + def __init__(self, manifest: Manifest, root_directory: pathlib.Path) -> None: + """Create a Git Super project.""" + super().__init__(manifest, root_directory) + self._repo = GitLocalRepo(root_directory) + @staticmethod def check(path: str | pathlib.Path) -> bool: """Check if this path is of the matching VCS.""" @@ -51,7 +57,7 @@ def has_local_changes_in_dir(self, path: str) -> bool: def get_username(self) -> str: """Get the username of the superproject VCS.""" - username = GitLocalRepo(self.root_directory).get_username() + username = self._repo.get_username() if username: return username @@ -60,7 +66,7 @@ def get_username(self) -> str: def get_useremail(self) -> str: """Get the user email of the superproject VCS.""" - email = GitLocalRepo(self.root_directory).get_useremail() + email = self._repo.get_useremail() if email: return email @@ -68,7 +74,7 @@ def get_useremail(self) -> str: def get_file_revision(self, path: str | pathlib.Path) -> str: """Get the revision of the given file.""" - return str(GitLocalRepo(self.root_directory).get_last_file_hash(str(path))) + return str(self._repo.get_last_file_hash(str(path))) @staticmethod def import_projects() -> Sequence[ProjectEntry]: @@ -131,7 +137,7 @@ def diff( if diff_since_revision: combined_diff += [diff_since_revision] - untracked_files_patch = str(local_repo.untracked_files_patch(ignore)) + untracked_files_patch = local_repo.untracked_files_patch(ignore) if untracked_files_patch: if reverse: reversed_patch = reverse_patch(untracked_files_patch.encode("utf-8")) diff --git a/dfetch/project/svnsubproject.py b/dfetch/project/svnsubproject.py index 82947c57..0ca4a2ce 100644 --- a/dfetch/project/svnsubproject.py +++ b/dfetch/project/svnsubproject.py @@ -28,7 +28,6 @@ def __init__(self, project: ProjectEntry): """Create a Svn subproject.""" super().__init__(project) self._remote_repo = SvnRemote(self.remote) - self._repo = SvnRepo(self.local_path) def check(self) -> bool: """Check if is SVN.""" @@ -41,7 +40,7 @@ def revision_is_enough() -> bool: def _latest_revision_on_branch(self, branch: str) -> str: """Get the latest revision on a branch.""" - if branch not in (self._repo.DEFAULT_BRANCH, "", " "): + if branch not in (SvnRepo.DEFAULT_BRANCH, "", " "): branch = f"branches/{branch}" return self._get_revision(branch) @@ -86,11 +85,11 @@ def _determine_what_to_fetch(self, version: Version) -> tuple[str, str, str]: branch_path = "" branch = " " else: - branch = version.branch or self._repo.DEFAULT_BRANCH + branch = version.branch or SvnRepo.DEFAULT_BRANCH branch_path = ( f"branches/{branch}" - if branch != self._repo.DEFAULT_BRANCH - else self._repo.DEFAULT_BRANCH + if branch != SvnRepo.DEFAULT_BRANCH + else SvnRepo.DEFAULT_BRANCH ) branch_path = urllib.parse.quote(branch_path) @@ -180,7 +179,7 @@ def _get_revision(self, branch: str) -> str: def get_default_branch(self) -> str: """Get the default branch of this repository.""" - return self._repo.DEFAULT_BRANCH + return SvnRepo.DEFAULT_BRANCH def create_formatted_patch_header(self, patch_info: PatchInfo) -> str: """Create a formatted patch header for the given patch info.""" diff --git a/dfetch/project/svnsuperproject.py b/dfetch/project/svnsuperproject.py index 3afbeba2..706902d6 100644 --- a/dfetch/project/svnsuperproject.py +++ b/dfetch/project/svnsuperproject.py @@ -11,6 +11,7 @@ from collections.abc import Sequence from dfetch.log import get_logger +from dfetch.manifest.manifest import Manifest from dfetch.manifest.project import ProjectEntry from dfetch.project.subproject import SubProject from dfetch.project.superproject import RevisionRange, SuperProject @@ -32,6 +33,11 @@ class SvnSuperProject(SuperProject): """A SVN specific superproject.""" + def __init__(self, manifest: Manifest, root_directory: pathlib.Path) -> None: + """Create a Svn Super project.""" + super().__init__(manifest, root_directory) + self._repo = SvnRepo(root_directory) + @staticmethod def check(path: str | pathlib.Path) -> bool: """Check if this path is of the matching VCS.""" @@ -58,7 +64,7 @@ def has_local_changes_in_dir(self, path: str) -> bool: def get_username(self) -> str: """Get the username of the superproject VCS.""" - username = SvnRepo(self.root_directory).get_username() + username = self._repo.get_username() if username: return username @@ -71,7 +77,7 @@ def get_useremail(self) -> str: def get_file_revision(self, path: str | pathlib.Path) -> str: """Get the revision of the given file.""" - return str(SvnRepo(self.root_directory).get_last_changed_revision(str(path))) + return str(self._repo.get_last_changed_revision(str(path))) @staticmethod def import_projects() -> Sequence[ProjectEntry]: