diff --git a/dfetch/commands/check.py b/dfetch/commands/check.py index 156fddef..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() + 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 9316bdfa..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 SuperProject +from dfetch.project.superproject import NoVcsSuperProject, RevisionRange from dfetch.util.util import catch_runtime_exceptions, in_directory logger = get_logger(__name__) @@ -96,9 +97,14 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the diff.""" - superproject = SuperProject() + superproject = create_super_project() 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,18 +120,23 @@ 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", - ) - old_rev = old_rev or subproject.metadata_revision() + if not subproject: + raise RuntimeError("No subproject!") + + 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" 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/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 fd55ebe4..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() + 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 8b8b17d9..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() + 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 4f12eb3b..39ff43b8 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 import determine_superproject_vcs 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 = determine_superproject_vcs(".").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/commands/report.py b/dfetch/commands/report.py index 82296f7e..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() + 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 481d519f..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() + 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 c6eca9f0..54ab8346 100644 --- a/dfetch/commands/update_patch.py +++ b/dfetch/commands/update_patch.py @@ -40,7 +40,10 @@ 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.project.gitsuperproject import GitSuperProject +from dfetch.project.metadata import Metadata +from dfetch.project.superproject import NoVcsSuperProject, RevisionRange from dfetch.util.util import catch_runtime_exceptions, in_directory logger = get_logger(__name__) @@ -70,22 +73,22 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the update patch.""" - superproject = SuperProject() + superproject = create_super_project() exceptions: list[str] = [] - if not superproject.in_vcs(): + if isinstance(superproject, NoVcsSuperProject): 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): 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) @@ -123,10 +126,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/__init__.py b/dfetch/project/__init__.py index 24651756..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.project.git import GitSubProject +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.svn import SvnSubProject +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/git.py b/dfetch/project/gitsubproject.py similarity index 71% rename from dfetch/project/git.py rename to dfetch/project/gitsubproject.py index 6f553bbe..53df09a4 100644 --- a/dfetch/project/git.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__) @@ -26,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.""" @@ -44,43 +41,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, - 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.""" @@ -109,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, @@ -117,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 new file mode 100644 index 00000000..4614ad2d --- /dev/null +++ b/dfetch/project/gitsuperproject.py @@ -0,0 +1,151 @@ +"""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.manifest import Manifest +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.""" + + 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.""" + 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 = self._repo.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 = self._repo.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(self._repo.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 = 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 = 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/subproject.py b/dfetch/project/subproject.py index 8d227f69..965771d4 100644 --- a/dfetch/project/subproject.py +++ b/dfetch/project/subproject.py @@ -385,20 +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, - 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.""" @@ -411,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 378b5921..e7c8a199 100644 --- a/dfetch/project/superproject.py +++ b/dfetch/project/superproject.py @@ -11,23 +11,28 @@ import getpass import os 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 -from dfetch.manifest.parse import find_manifest, parse from dfetch.manifest.project import ProjectEntry -from dfetch.project.git import GitSubProject from dfetch.project.subproject import SubProject -from dfetch.project.svn import SvnSubProject from dfetch.util.util import resolve_absolute_path -from dfetch.vcs.git import GitLocalRepo -from dfetch.vcs.svn import SvnRepo logger = get_logger(__name__) -class SuperProject: +@dataclass(frozen=True) +class RevisionRange: + """A revision pair.""" + + old: str + new: str | None + + +class SuperProject(ABC): """Representation of the project containing the manifest. A SuperProject is the repository/directory that contains the dfetch @@ -35,16 +40,10 @@ 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.""" - 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) - ) + self._manifest = manifest + self._root_directory = root_directory @property def root_directory(self) -> pathlib.Path: @@ -56,13 +55,81 @@ 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.""" - if GitLocalRepo(self.root_directory).is_git(): - return GitSubProject(project) - if SvnRepo(self.root_directory).is_svn(): - return SvnSubProject(project) + @abstractmethod + def ignored_files(self, path: str) -> Sequence[str]: + """Return a list of files that can be ignored in a given path.""" + + @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.""" + + def _get_username_fallback(self) -> str: + """Get the username of the superproject VCS.""" + 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.""" + + 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.""" + + @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 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 def ignored_files(self, path: str) -> Sequence[str]: @@ -74,63 +141,42 @@ 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 [] - 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() - ) - - def is_git(self) -> bool: - """Check if this superproject is a git repository.""" - return GitLocalRepo(self.root_directory).is_git() - 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) - - if SvnRepo(self.root_directory).is_svn(): - return SvnRepo.any_changes_or_untracked(path) - return True 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() - - 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.""" - email = "" - if GitLocalRepo(self.root_directory).is_git(): - email = GitLocalRepo(self.root_directory).get_useremail() - - elif SvnRepo(self.root_directory).is_svn(): - email = SvnRepo(self.root_directory).get_useremail() + return self._get_useremail_fallback() + + 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", + ) - username = self.get_username() or "unknown" - return email or f"{username}@example.com" + def diff( + self, + path: str | pathlib.Path, + revisions: RevisionRange, + ignore: Sequence[str], + 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/svn.py b/dfetch/project/svnsubproject.py similarity index 77% rename from dfetch/project/svn.py rename to dfetch/project/svnsubproject.py index 98393b42..0ca4a2ce 100644 --- a/dfetch/project/svn.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__) @@ -36,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.""" @@ -49,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) @@ -94,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) @@ -186,45 +177,9 @@ 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, - 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 + 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 new file mode 100644 index 00000000..706902d6 --- /dev/null +++ b/dfetch/project/svnsuperproject.py @@ -0,0 +1,137 @@ +"""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.manifest import Manifest +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.""" + + 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.""" + 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 = self._repo.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(self._repo.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/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/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 2e049e11..744ea790 100644 --- a/features/import-from-svn.feature +++ b/features/import-from-svn.feature @@ -8,7 +8,7 @@ Feature: Importing externals from an existing svn repository 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 diff --git a/tests/test_check.py b/tests/test_check.py index 73dda675..73423cf6 100644 --- a/tests/test_check.py +++ b/tests/test_check.py @@ -33,11 +33,13 @@ 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.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"): @@ -46,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 933e0c82..9692c50a 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -48,25 +48,35 @@ def test_git_import(name, submodules): import_ = Import() - with patch("dfetch.commands.import_.GitLocalRepo.submodules") as mocked_submodules: - with patch("dfetch.commands.import_.Manifest") as mocked_manifest: - mocked_submodules.return_value = submodules - - if len(submodules) == 0: - with pytest.raises(RuntimeError): - import_(argparse.Namespace()) - else: - import_(argparse.Namespace()) + with patch("dfetch.project.svnsuperproject.SvnRepo.is_svn") as is_svn: + with patch( + "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.gitsuperproject.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()) - 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( @@ -105,10 +115,14 @@ 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.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.commands.import_.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 466f77d6..ef210c89 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.create_super_project", 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_subproject.py b/tests/test_subproject.py index 8b503c26..636cec6e 100644 --- a/tests/test_subproject.py +++ b/tests/test_subproject.py @@ -43,18 +43,6 @@ def wanted_version(self): def _list_of_tags(self): return [] - def metadata_revision(self): - return "1" - - def _diff_impl( - self, - old_revision, - new_revision, - ignore, - reverse=False, - ): - return "" - def get_default_branch(self): return "" 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..487973aa 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -33,11 +33,13 @@ 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.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"): @@ -46,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(): @@ -57,11 +59,13 @@ 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.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"): @@ -74,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=[] )