From 4fa8e46ba742c2e1b1ed8fa1f3afb2f2dc1d37b7 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 12 Apr 2026 12:41:39 +0000 Subject: [PATCH 01/15] Fix #1096: print runtime errors in context of failing subproject Replace the pattern of collecting RuntimeErrors across all subprojects and printing them at the end with immediate in-context logging via `logger.print_warning_line(project.name, str(exc))` at the point of failure. This ensures errors like "svn not available on system" appear next to the subproject they belong to instead of at the end of the run. The final `raise RuntimeError()` (no message) still produces a non-zero exit code without double-printing. Also bumps version to 0.14.0 and updates CHANGELOG, feature tests. https://claude.ai/code/session_01RTgPqrX37jFkK843dq65Bu Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.rst | 1 + dfetch/__init__.py | 2 +- dfetch/commands/check.py | 13 +++++++---- dfetch/commands/format_patch.py | 12 ++++++---- dfetch/commands/freeze.py | 7 +++--- dfetch/commands/update.py | 23 ++++++++++++------- dfetch/util/util.py | 12 ---------- features/check-archive.feature | 12 +++++----- features/check-git-repo.feature | 20 ++++++++-------- features/check-specific-projects.feature | 2 +- features/check-svn-repo.feature | 14 +++++------ .../checked-project-has-dependencies.feature | 6 ++--- features/diff-in-git.feature | 4 ++-- features/diff-in-svn.feature | 4 ++-- features/fetch-archive.feature | 7 +++--- features/fetch-checks-destination.feature | 6 ++--- features/fetch-file-pattern-git.feature | 4 ++-- features/fetch-file-pattern-svn.feature | 2 +- .../fetch-git-repo-with-submodule.feature | 8 +++---- features/fetch-git-repo.feature | 4 ++-- features/fetch-single-file-git.feature | 4 ++-- features/fetch-single-file-svn.feature | 2 +- features/fetch-with-ignore-git.feature | 6 ++--- features/fetch-with-ignore-svn.feature | 6 ++--- .../guard-against-overwriting-git.feature | 6 ++--- .../guard-against-overwriting-svn.feature | 6 ++--- features/handle-invalid-metadata.feature | 2 +- features/journey-basic-patching.feature | 2 +- features/journey-basic-usage.feature | 2 +- features/list-projects.feature | 6 ++--- features/patch-after-fetch-git.feature | 10 ++++---- features/patch-after-fetch-svn.feature | 2 +- features/patch-fuzzy-matching-git.feature | 2 +- features/report-sbom.feature | 4 ++-- features/suggest-project-name.feature | 6 ++--- features/update-patch-in-git.feature | 2 +- features/update-patch-in-svn.feature | 2 +- .../updated-project-has-dependencies.feature | 14 +++++------ features/validate-manifest.feature | 10 ++++---- 39 files changed, 128 insertions(+), 129 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 393daddec..c516042c7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,7 @@ Release 0.14.0 (unreleased) * Edit manifest in-place when freezing inside a git or SVN superproject, preserving comments and layout (#1063) * Add new ``remove`` command to remove projects from manifest and disk (#26) * Fix "unsafe symlink target" error for archives containing relative ``..`` symlinks (#1122) +* Print runtime errors (e.g. ``svn not available on system``) directly in context of the failing subproject instead of collecting and showing them at the end (#1096) * Fix ``dfetch add`` crashing with a ``ValueError`` when the remote URL has a trailing slash (#1137) * Fix unhelpful error message when a metadata file is malformed (#1145) * Fix arbitrary file write via malicious tar/zip symlink (#1152) diff --git a/dfetch/__init__.py b/dfetch/__init__.py index 1651dc472..7cf054bbc 100644 --- a/dfetch/__init__.py +++ b/dfetch/__init__.py @@ -1,5 +1,5 @@ """Dfetch.""" -__version__ = "0.13.0" +__version__ = "0.14.0" DEFAULT_MANIFEST_NAME: str = "dfetch.yaml" diff --git a/dfetch/commands/check.py b/dfetch/commands/check.py index 58b5d0627..f6f0a8222 100644 --- a/dfetch/commands/check.py +++ b/dfetch/commands/check.py @@ -46,7 +46,7 @@ from dfetch.reporting.check.sarif_reporter import SarifReporter from dfetch.reporting.check.stdout_reporter import CheckStdoutReporter from dfetch.util.github_version_check import newer_version_available -from dfetch.util.util import catch_runtime_exceptions, in_directory +from dfetch.util.util import in_directory logger = get_logger(__name__) @@ -107,13 +107,16 @@ def __call__(self, args: argparse.Namespace) -> None: reporters = self._get_reporters(args, superproject.manifest) with in_directory(superproject.root_directory): - exceptions: list[str] = [] + had_errors: bool = False for project in superproject.manifest.selected_projects(args.projects): - with catch_runtime_exceptions(exceptions) as exceptions: + try: dfetch.project.create_sub_project(project).check_for_update( reporters, files_to_ignore=superproject.ignored_files(project.destination), ) + except RuntimeError as exc: + logger.print_warning_line(project.name, str(exc)) + had_errors = True if not args.no_recommendations and os.path.isdir(project.destination): with in_directory(project.destination): @@ -122,8 +125,8 @@ def __call__(self, args: argparse.Namespace) -> None: for reporter in reporters: reporter.dump_to_file() - if exceptions: - raise RuntimeError("\n".join(exceptions)) + if had_errors: + raise RuntimeError() @staticmethod def _get_reporters( diff --git a/dfetch/commands/format_patch.py b/dfetch/commands/format_patch.py index 4bc34148e..371ba9153 100644 --- a/dfetch/commands/format_patch.py +++ b/dfetch/commands/format_patch.py @@ -38,7 +38,6 @@ from dfetch.project.subproject import SubProject from dfetch.project.svnsubproject import SvnSubProject from dfetch.util.util import ( - catch_runtime_exceptions, check_no_path_traversal, in_directory, ) @@ -84,7 +83,7 @@ def __call__(self, args: argparse.Namespace) -> None: """Perform the format patch.""" superproject = create_super_project() - exceptions: list[str] = [] + had_errors: bool = False output_dir_path = pathlib.Path(args.output_directory).resolve() @@ -94,7 +93,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: + try: subproject = dfetch.project.create_sub_project(project) # Check if the project has a patch, maybe suggest creating one? @@ -139,9 +138,12 @@ def __call__(self, args: argparse.Namespace) -> None: project.name, f"formatted patch written to {output_patch_file.relative_to(os.getcwd())}", ) + except RuntimeError as exc: + logger.print_warning_line(project.name, str(exc)) + had_errors = True - if exceptions: - raise RuntimeError("\n".join(exceptions)) + if had_errors: + raise RuntimeError() def _determine_target_patch_type(subproject: SubProject) -> PatchType: diff --git a/dfetch/commands/freeze.py b/dfetch/commands/freeze.py index 5e4cb21bd..20e8769ae 100644 --- a/dfetch/commands/freeze.py +++ b/dfetch/commands/freeze.py @@ -68,7 +68,7 @@ from dfetch.log import get_logger from dfetch.project import create_super_project from dfetch.project.superproject import NoVcsSuperProject -from dfetch.util.util import catch_runtime_exceptions, in_directory +from dfetch.util.util import in_directory logger = get_logger(__name__) @@ -99,7 +99,6 @@ def __call__(self, args: argparse.Namespace) -> None: superproject = create_super_project() make_backup = isinstance(superproject, NoVcsSuperProject) - exceptions: list[str] = [] manifest_updated = False with in_directory(superproject.root_directory): @@ -110,7 +109,7 @@ def __call__(self, args: argparse.Namespace) -> None: shutil.copyfile(manifest_path, manifest_path + ".backup") for project in projects_to_freeze: - with catch_runtime_exceptions(exceptions) as exceptions: + try: sub_project = dfetch.project.create_sub_project(project) on_disk_version = sub_project.on_disk_version() @@ -133,6 +132,8 @@ def __call__(self, args: argparse.Namespace) -> None: ) superproject.manifest.update_project_version(project) manifest_updated = True + except RuntimeError as exc: + logger.print_warning_line(project.name, str(exc)) if manifest_updated: superproject.manifest.dump() diff --git a/dfetch/commands/update.py b/dfetch/commands/update.py index 294700a04..6028d6112 100644 --- a/dfetch/commands/update.py +++ b/dfetch/commands/update.py @@ -28,7 +28,6 @@ from dfetch.log import get_logger from dfetch.project import create_super_project from dfetch.util.util import ( - catch_runtime_exceptions, check_no_path_traversal, in_directory, ) @@ -78,20 +77,25 @@ def __call__(self, args: argparse.Namespace) -> None: """Perform the update.""" superproject = create_super_project() - exceptions: list[str] = [] + had_errors: bool = False destinations: list[str] = [ os.path.realpath(project.destination) for project in superproject.manifest.projects ] with in_directory(superproject.root_directory): for project in superproject.manifest.selected_projects(args.projects): - with catch_runtime_exceptions(exceptions) as exceptions: + try: self._check_destination(project, destinations) - destination = project.destination + except RuntimeError: + had_errors = True + continue - def _ignored(dst: str = destination) -> list[str]: - return list(superproject.ignored_files(dst)) + destination = project.destination + def _ignored(dst: str = destination) -> list[str]: + return list(superproject.ignored_files(dst)) + + try: dfetch.project.create_sub_project(project).update( force=args.force, ignored_files_callback=_ignored, @@ -103,9 +107,12 @@ def _ignored(dst: str = destination) -> list[str]: ): with in_directory(project.destination): check_sub_manifests(superproject.manifest, project) + except RuntimeError as exc: + logger.print_warning_line(project.name, str(exc)) + had_errors = True - if exceptions: - raise RuntimeError("\n".join(exceptions)) + if had_errors: + raise RuntimeError() @staticmethod def _check_destination( diff --git a/dfetch/util/util.py b/dfetch/util/util.py index 018ffa126..4954a83fe 100644 --- a/dfetch/util/util.py +++ b/dfetch/util/util.py @@ -188,18 +188,6 @@ def in_directory(path: str | Path) -> Generator[str, None, None]: os.chdir(pwd) -@contextmanager -def catch_runtime_exceptions( - exc_list: list[str] | None = None, -) -> Generator[list[str], None, None]: - """Catch all runtime errors and add it to list of strings.""" - exc_list = exc_list or [] - try: - yield exc_list - except RuntimeError as exc: - exc_list += [str(exc)] - - @contextmanager def prefix_runtime_exceptions( prefix: str, diff --git a/features/check-archive.feature b/features/check-archive.feature index 6919718b5..745e0a424 100644 --- a/features/check-archive.feature +++ b/features/check-archive.feature @@ -25,7 +25,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > up-to-date (some-remote-server/SomeProject.tar.gz) """ @@ -49,7 +49,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > up-to-date (sha256:) """ @@ -70,7 +70,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > wanted (some-remote-server/SomeProject.tar.gz), available (some-remote-server/SomeProject.tar.gz) """ @@ -88,7 +88,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) non-existent-archive: > wanted (https://dfetch.invalid/does-not-exist.tar.gz), but not available at the upstream. """ @@ -114,7 +114,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check SomeProject" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > up-to-date (some-remote-server/SomeProject.tar.gz) """ @@ -137,7 +137,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check SomeProject" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > Local changes were detected, please generate a patch using 'dfetch diff SomeProject' and add it to your manifest using 'patch:'. Alternatively overwrite the local changes with 'dfetch update --force SomeProject' > up-to-date (some-remote-server/SomeProject.tar.gz) diff --git a/features/check-git-repo.feature b/features/check-git-repo.feature index e74086839..374ca4172 100644 --- a/features/check-git-repo.feature +++ b/features/check-git-repo.feature @@ -27,7 +27,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-rev-only: > wanted (e1fda19a57b873eb8e6ae37780594cbb77b70f1a), available (e1fda19a57b873eb8e6ae37780594cbb77b70f1a) ext/test-rev-and-branch: @@ -53,7 +53,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag-v1: > wanted (v1), available (v2.0) """ @@ -83,7 +83,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-rev-only: > up-to-date (e1fda19a57b873eb8e6ae37780594cbb77b70f1a) ext/test-rev-and-branch: @@ -117,7 +117,7 @@ Feature: Checking dependencies from a git repository And I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > wanted (v2.0), current (v1), available (v2.0) """ @@ -139,7 +139,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check SomeProject" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > Local changes were detected, please generate a patch using 'dfetch diff SomeProject' and add it to your manifest using 'patch:'. Alternatively overwrite the local changes with 'dfetch update --force SomeProject' > up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0) @@ -160,7 +160,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check SomeProject" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0) """ @@ -180,7 +180,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) >>>git ls-remote --heads https://giiiiiidhub.com/i-do-not-exist/broken<<< failed! 'https://giiiiiidhub.com/i-do-not-exist/broken' is not a valid URL or unreachable: fatal: unable to access 'https://giiiiiidhub.com/i-do-not-exist/broken/': Could not resolve host: giiiiiidhub.com @@ -209,7 +209,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProjectMissingTag: > wanted (i-dont-exist), but not available at the upstream. SomeProjectNonExistentBranch: @@ -231,7 +231,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output starts with: """ - Dfetch (0.13.0) + Dfetch (0.14.0) >>>git ls-remote --heads --tags https://github.com/dfetch-org/test-repo-private.git<<< returned 128: """ @@ -248,7 +248,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output starts with: """ - Dfetch (0.13.0) + Dfetch (0.14.0) >>>git ls-remote --heads --tags git@github.com:dfetch-org/test-repo-private.git<<< returned 128: """ diff --git a/features/check-specific-projects.feature b/features/check-specific-projects.feature index fe445a6d7..038434dc7 100644 --- a/features/check-specific-projects.feature +++ b/features/check-specific-projects.feature @@ -28,7 +28,7 @@ Feature: Checking specific projects When I run "dfetch check ext/test-rev-and-branch" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-rev-and-branch: > wanted (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a) """ diff --git a/features/check-svn-repo.feature b/features/check-svn-repo.feature index e6b588f1d..573e2552e 100644 --- a/features/check-svn-repo.feature +++ b/features/check-svn-repo.feature @@ -29,7 +29,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) cunit-svn-rev-only: > wanted (176), available (trunk - 176) cunit-svn-rev-and-branch: @@ -56,7 +56,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) cutter-svn-tag: > wanted (1.1.7), available (1.1.8) """ @@ -94,7 +94,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) cunit-svn-rev-only: > wanted (169), current (trunk - 169), available (trunk - 176) cunit-svn-rev-and-branch: @@ -118,7 +118,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > wanted (latest), current (1), available (1) """ @@ -137,7 +137,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) >>>svn --non-interactive info https://giiiiiidhub.com/i-do-not-exist/broken/trunk<<< failed! 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' is not a valid URL or unreachable: svn: E170013: Unable to connect to a repository at URL 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' @@ -162,7 +162,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) cutter-svn-tag: > wanted (non-existent-tag), but not available at the upstream. """ @@ -183,7 +183,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check SomeProject" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > up-to-date (v1) """ diff --git a/features/checked-project-has-dependencies.feature b/features/checked-project-has-dependencies.feature index 5ab556d85..3f89826f5 100644 --- a/features/checked-project-has-dependencies.feature +++ b/features/checked-project-has-dependencies.feature @@ -30,7 +30,7 @@ Feature: Check for dependencies in projects When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > wanted (v1), available (v1) """ @@ -67,7 +67,7 @@ Feature: Check for dependencies in projects When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) Multiple manifests found, using dfetch.yaml SomeProject: > up-to-date (v1) @@ -111,7 +111,7 @@ Feature: Check for dependencies in projects When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) Multiple manifests found, using dfetch.yaml SomeProject: > up-to-date (v1) diff --git a/features/diff-in-git.feature b/features/diff-in-git.feature index 533dce6dd..1c2eb0650 100644 --- a/features/diff-in-git.feature +++ b/features/diff-in-git.feature @@ -66,7 +66,7 @@ Feature: Diff in git When I run "dfetch diff SomeProject" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > No diffs found since 59efb91396fd369eb113b43382783294dc8ed6d2 """ @@ -93,7 +93,7 @@ Feature: Diff in git When I run "dfetch diff SomeProject" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > No diffs found since 59efb91396fd369eb113b43382783294dc8ed6d2 """ diff --git a/features/diff-in-svn.feature b/features/diff-in-svn.feature index 5f1177dae..e6ff06694 100644 --- a/features/diff-in-svn.feature +++ b/features/diff-in-svn.feature @@ -63,7 +63,7 @@ Feature: Diff in svn When I run "dfetch diff SomeProject" in MySvnProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > No diffs found since 1 """ @@ -90,7 +90,7 @@ Feature: Diff in svn When I run "dfetch diff SomeProject" in MySvnProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > No diffs found since 1 """ diff --git a/features/fetch-archive.feature b/features/fetch-archive.feature index 98e3c3e01..855bea504 100644 --- a/features/fetch-archive.feature +++ b/features/fetch-archive.feature @@ -118,8 +118,9 @@ Feature: Fetching dependencies from an archive (tar/zip) When I run "dfetch update" in MyProject Then the output shows """ - Dfetch (0.13.0) - Hash mismatch for SomeProject! sha256 expected 0000000000000000000000000000000000000000000000000000000000000000 + Dfetch (0.14.0) + SomeProject: + > Hash mismatch for SomeProject! sha256 expected 0000000000000000000000000000000000000000000000000000000000000000 """ Scenario: Specific directory from archive can be fetched @@ -195,7 +196,7 @@ Feature: Fetching dependencies from an archive (tar/zip) When I run "dfetch update --force" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > Fetched some-remote-server/SomeProject.tar.gz """ diff --git a/features/fetch-checks-destination.feature b/features/fetch-checks-destination.feature index 999590cb7..045ab707b 100644 --- a/features/fetch-checks-destination.feature +++ b/features/fetch-checks-destination.feature @@ -21,10 +21,9 @@ Feature: Fetch checks destinations When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > Skipping, path "." is not allowed as destination. - Destination must be in a valid subfolder. "." is not valid! """ Scenario: Path traversal is not allowed @@ -43,8 +42,7 @@ Feature: Fetch checks destinations When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > Skipping, path "../../some-higher-folder" is outside manifest directory tree. - Destination must be in the manifests folder or a subfolder. "../../some-higher-folder" is outside this tree! """ diff --git a/features/fetch-file-pattern-git.feature b/features/fetch-file-pattern-git.feature index e969915d9..358f00e35 100644 --- a/features/fetch-file-pattern-git.feature +++ b/features/fetch-file-pattern-git.feature @@ -25,7 +25,7 @@ Feature: Fetch file pattern from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProjectWithAnInterestingFile: > Fetched v1 """ @@ -57,7 +57,7 @@ Feature: Fetch file pattern from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) The 'src:' filter 'SomeFolder/Some*' matches multiple directories from 'some-remote-server/SomeProjectWithAnInterestingFile.git'. Only considering files in 'SomeFolder/SomeOtherSubFolder'. SomeProjectWithAnInterestingFile: > Fetched v1 diff --git a/features/fetch-file-pattern-svn.feature b/features/fetch-file-pattern-svn.feature index a5f3416b8..61f62c7da 100644 --- a/features/fetch-file-pattern-svn.feature +++ b/features/fetch-file-pattern-svn.feature @@ -22,7 +22,7 @@ Feature: Fetch file pattern from svn repo When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProjectWithAnInterestingFile: > Fetched trunk - 1 """ diff --git a/features/fetch-git-repo-with-submodule.feature b/features/fetch-git-repo-with-submodule.feature index c349a3c6e..0bf175a99 100644 --- a/features/fetch-git-repo-with-submodule.feature +++ b/features/fetch-git-repo-with-submodule.feature @@ -25,7 +25,7 @@ Feature: Fetch projects with nested VCS dependencies When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) my-project-with-submodules: > Found & fetched submodule "./ext/test-repo1" (some-remote-server/TestRepo.git @ master - 79698c99152e4a4b7b759c9def50a130bc91a2ff) > Found & fetched submodule "./ext/test-repo2" (some-remote-server/TestRepo.git @ master - 79698c99152e4a4b7b759c9def50a130bc91a2ff) @@ -64,7 +64,7 @@ Feature: Fetch projects with nested VCS dependencies When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) outer-project: > Found & fetched submodule "./ext/middle" (some-remote-server/MiddleProject.git @ master - 79698c99152e4a4b7b759c9def50a130bc91a2ff) > Fetched master - e1fda19a57b873eb8e6ae37780594cbb77b70f1a @@ -96,7 +96,7 @@ Feature: Fetch projects with nested VCS dependencies When I run "dfetch report" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) my-project-with-submodules: - remote : remote url : some-remote-server/SomeInterestingProject.git @@ -139,7 +139,7 @@ Feature: Fetch projects with nested VCS dependencies When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) glob-project: > Found & fetched submodule "./ext/test-repo" (some-remote-server/TestRepo.git @ master - 79698c99152e4a4b7b759c9def50a130bc91a2ff) > Fetched master - e1fda19a57b873eb8e6ae37780594cbb77b70f1a diff --git a/features/fetch-git-repo.feature b/features/fetch-git-repo.feature index 2d3846fb0..4ac7d3c31 100644 --- a/features/fetch-git-repo.feature +++ b/features/fetch-git-repo.feature @@ -64,7 +64,7 @@ Feature: Fetching dependencies from a git repository And I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > Fetched v2.0 """ @@ -85,7 +85,7 @@ Feature: Fetching dependencies from a git repository When I run "dfetch update --force" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > Fetched v1 """ diff --git a/features/fetch-single-file-git.feature b/features/fetch-single-file-git.feature index d8dd06829..fa8187abc 100644 --- a/features/fetch-single-file-git.feature +++ b/features/fetch-single-file-git.feature @@ -22,7 +22,7 @@ Feature: Fetch single file from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProjectWithAnInterestingFile: > Fetched v1 """ @@ -54,7 +54,7 @@ Feature: Fetch single file from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProjectWithAnInterestingFile: > Fetched v1 """ diff --git a/features/fetch-single-file-svn.feature b/features/fetch-single-file-svn.feature index 25d817b08..7850663b7 100644 --- a/features/fetch-single-file-svn.feature +++ b/features/fetch-single-file-svn.feature @@ -22,7 +22,7 @@ Feature: Fetch single file from svn repo When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProjectWithAnInterestingFile: > Fetched trunk - 1 """ diff --git a/features/fetch-with-ignore-git.feature b/features/fetch-with-ignore-git.feature index f05556a08..d3ea701b7 100644 --- a/features/fetch-with-ignore-git.feature +++ b/features/fetch-with-ignore-git.feature @@ -29,7 +29,7 @@ Feature: Fetch with ignore in git When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeInterestingProject: > Fetched v1 """ @@ -59,7 +59,7 @@ Feature: Fetch with ignore in git When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeInterestingProject: > Fetched v1 """ @@ -92,7 +92,7 @@ Feature: Fetch with ignore in git When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeInterestingProject: > Fetched v1 """ diff --git a/features/fetch-with-ignore-svn.feature b/features/fetch-with-ignore-svn.feature index f1423236a..0f94db1fc 100644 --- a/features/fetch-with-ignore-svn.feature +++ b/features/fetch-with-ignore-svn.feature @@ -28,7 +28,7 @@ Feature: Fetch with ignore in svn When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeInterestingProject: > Fetched trunk - 1 """ @@ -57,7 +57,7 @@ Feature: Fetch with ignore in svn When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeInterestingProject: > Fetched trunk - 1 """ @@ -89,7 +89,7 @@ Feature: Fetch with ignore in svn When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeInterestingProject: > Fetched trunk - 1 """ diff --git a/features/guard-against-overwriting-git.feature b/features/guard-against-overwriting-git.feature index 98ad1422b..30ea95e38 100644 --- a/features/guard-against-overwriting-git.feature +++ b/features/guard-against-overwriting-git.feature @@ -24,7 +24,7 @@ Feature: Guard against overwriting in git When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > skipped - local changes after last update (use --force to overwrite) """ @@ -34,7 +34,7 @@ Feature: Guard against overwriting in git When I run "dfetch update --force" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > Fetched v2 """ @@ -45,7 +45,7 @@ Feature: Guard against overwriting in git When I run "dfetch update SomeProject" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > Fetched v2 """ diff --git a/features/guard-against-overwriting-svn.feature b/features/guard-against-overwriting-svn.feature index e9af3238e..a904b901a 100644 --- a/features/guard-against-overwriting-svn.feature +++ b/features/guard-against-overwriting-svn.feature @@ -24,7 +24,7 @@ Feature: Guard against overwriting in svn When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > skipped - local changes after last update (use --force to overwrite) """ @@ -34,7 +34,7 @@ Feature: Guard against overwriting in svn When I run "dfetch update --force" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > Fetched v2 """ @@ -45,7 +45,7 @@ Feature: Guard against overwriting in svn When I run "dfetch update SomeProject" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > Fetched v2 """ diff --git a/features/handle-invalid-metadata.feature b/features/handle-invalid-metadata.feature index e4726ddb2..7f4c94c2c 100644 --- a/features/handle-invalid-metadata.feature +++ b/features/handle-invalid-metadata.feature @@ -72,7 +72,7 @@ Feature: Handle invalid metadata files When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > ext/test-repo-tag/.dfetch_data.yaml is an invalid metadata file, not checking on disk version! > ext/test-repo-tag/.dfetch_data.yaml is an invalid metadata file, not checking local hash! diff --git a/features/journey-basic-patching.feature b/features/journey-basic-patching.feature index 5eb5726a8..8277325c3 100644 --- a/features/journey-basic-patching.feature +++ b/features/journey-basic-patching.feature @@ -56,7 +56,7 @@ Feature: Basic patch journey And I run "dfetch update -f test-repo" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) test-repo: > Fetched v1 > Applying patch "test-repo.patch" diff --git a/features/journey-basic-usage.feature b/features/journey-basic-usage.feature index d719f0e1d..4b88ff450 100644 --- a/features/journey-basic-usage.feature +++ b/features/journey-basic-usage.feature @@ -29,7 +29,7 @@ Feature: Basic usage journey When I run "dfetch check" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > wanted & current (v1), available (v2.0) """ diff --git a/features/list-projects.feature b/features/list-projects.feature index c1421c212..0a23c969d 100644 --- a/features/list-projects.feature +++ b/features/list-projects.feature @@ -28,7 +28,7 @@ Feature: List dependencies When I run "dfetch report" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: - remote : remote url : https://github.com/dfetch-org/test-repo @@ -68,7 +68,7 @@ Feature: List dependencies When I run "dfetch report" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) cutter-svn-tag: - remote : remote url : https://svn.code.sf.net/p/cutter/svn/cutter @@ -85,7 +85,7 @@ Feature: List dependencies When I run "dfetch report" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: - remote : github-com-dfetch-org remote url : https://github.com/dfetch-org/test-repo diff --git a/features/patch-after-fetch-git.feature b/features/patch-after-fetch-git.feature index 53f3a8d60..d2da6f2de 100644 --- a/features/patch-after-fetch-git.feature +++ b/features/patch-after-fetch-git.feature @@ -68,14 +68,14 @@ Feature: Patch after fetching from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > Fetched v2.0 > Applying patch "diff.patch" source/target file does not exist: --- b'README1.md' +++ b'README1.md' - Applying patch "diff.patch" failed + > Applying patch "diff.patch" failed """ Scenario: Multiple patch files are applied after fetching @@ -126,7 +126,7 @@ Feature: Patch after fetching from git repo """ And the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > Fetched v2.0 > Applying patch "001-diff.patch" @@ -170,7 +170,7 @@ Feature: Patch after fetching from git repo """ And the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > Fetched v2.0 > Applying patch "diff.patch" @@ -198,7 +198,7 @@ Feature: Patch after fetching from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) ext/test-repo-tag: > Fetched v2.0 > Skipping patch "../diff.patch" which is outside /some/path. diff --git a/features/patch-after-fetch-svn.feature b/features/patch-after-fetch-svn.feature index 6e7a39ca8..3999e8e06 100644 --- a/features/patch-after-fetch-svn.feature +++ b/features/patch-after-fetch-svn.feature @@ -69,7 +69,7 @@ Feature: Patch after fetching from svn repo When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) cutter: > Fetched 1.1.7 > Applying patch "diff.patch" diff --git a/features/patch-fuzzy-matching-git.feature b/features/patch-fuzzy-matching-git.feature index 02a9bbb37..d6ed2c319 100644 --- a/features/patch-fuzzy-matching-git.feature +++ b/features/patch-fuzzy-matching-git.feature @@ -60,7 +60,7 @@ Feature: Patch application tolerates small upstream changes When I run "dfetch update" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > Fetched master - f47d80c35e14dfa4f9c9c30c9865cbf0f8d50933 > Applying patch "SomeProject.patch" diff --git a/features/report-sbom.feature b/features/report-sbom.feature index c17448d52..415e0986e 100644 --- a/features/report-sbom.feature +++ b/features/report-sbom.feature @@ -123,7 +123,7 @@ Feature: Create an CycloneDX sbom "tools": { "components": [ { - "bom-ref": "dfetch-0.13.0", + "bom-ref": "dfetch-0.14.0", "externalReferences": [ { "type": "build-system", @@ -171,7 +171,7 @@ Feature: Create an CycloneDX sbom "name": "dfetch-org" }, "type": "application", - "version": "0.13.0" + "version": "0.14.0" }, { "description": "Python library for CycloneDX", diff --git a/features/suggest-project-name.feature b/features/suggest-project-name.feature index cb3252efb..e8f65a297 100644 --- a/features/suggest-project-name.feature +++ b/features/suggest-project-name.feature @@ -16,7 +16,7 @@ Feature: Suggest a project name When I run "dfetch check project with space" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) Not all projects found! "project", "with", "space" This manifest contains: "project with space", "project-with-l", "Project-With-Capital" Did you mean: "project with space"? @@ -26,7 +26,7 @@ Feature: Suggest a project name When I run "dfetch check project-with-1" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) Not all projects found! "project-with-1" This manifest contains: "project with space", "project-with-l", "Project-With-Capital" Did you mean: "project-with-l"? @@ -36,7 +36,7 @@ Feature: Suggest a project name When I run "dfetch check project-with-1 project-with-space Project-With-Capital" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) Not all projects found! "project-with-1", "project-with-space" This manifest contains: "project with space", "project-with-l", "Project-With-Capital" Did you mean: "project with space" and "project-with-l"? diff --git a/features/update-patch-in-git.feature b/features/update-patch-in-git.feature index 6f2e31eb6..36d2ea698 100644 --- a/features/update-patch-in-git.feature +++ b/features/update-patch-in-git.feature @@ -51,7 +51,7 @@ Feature: Update an existing patch in git """ And the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > Fetched master - f9b88b8259d9a7fb48327bf23beabe40c150d474 > Updating patch "patches/SomeProject.patch" diff --git a/features/update-patch-in-svn.feature b/features/update-patch-in-svn.feature index 1b665c53e..34ba4b2db 100644 --- a/features/update-patch-in-svn.feature +++ b/features/update-patch-in-svn.feature @@ -52,7 +52,7 @@ Feature: Update an existing patch in svn """ And the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) Update patch is only fully supported in git superprojects! SomeProject: > Fetched trunk - 1 diff --git a/features/updated-project-has-dependencies.feature b/features/updated-project-has-dependencies.feature index 081b05743..4f9036710 100644 --- a/features/updated-project-has-dependencies.feature +++ b/features/updated-project-has-dependencies.feature @@ -41,7 +41,7 @@ Feature: Updated project has dependencies When I run "dfetch update" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProjectWithManifest: > Fetched v1 > "SomeProjectWithManifest" depends on the following project(s) which are not part of your manifest: @@ -89,15 +89,13 @@ Feature: Updated project has dependencies When I run "dfetch update" in MyProject Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) SomeProject: > Fetched v1 - SomeProject/dfetch.yaml: Schema validation failed: - - "very-invalid-manifest\n" - ^ (line: 1) - - found arbitrary text + > SomeProject/dfetch.yaml: Schema validation failed: + "very-invalid-manifest\n" + ^ (line: 1) + found arbitrary text """ And 'MyProject' looks like: """ diff --git a/features/validate-manifest.feature b/features/validate-manifest.feature index 4c36d1bf2..0f13d79d9 100644 --- a/features/validate-manifest.feature +++ b/features/validate-manifest.feature @@ -21,7 +21,7 @@ Feature: Validate a manifest When I run "dfetch validate" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) dfetch.yaml : valid """ @@ -56,7 +56,7 @@ Feature: Validate a manifest When I run "dfetch validate" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) Schema validation failed: manifest-wrong: @@ -94,7 +94,7 @@ Feature: Validate a manifest When I run "dfetch validate" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) dfetch.yaml : valid """ @@ -115,7 +115,7 @@ Feature: Validate a manifest When I run "dfetch validate" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) Schema validation failed: hash: not-a-valid-hash ^ (line: 9) @@ -137,7 +137,7 @@ Feature: Validate a manifest When I run "dfetch validate" Then the output shows """ - Dfetch (0.13.0) + Dfetch (0.14.0) Schema validation failed: Duplicate manifest.projects.name value(s): ext/test-repo-rev-only """ From 33734a899e8ba6238aaeedf04c7ef93d7b3769f9 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 12 Apr 2026 12:41:59 +0000 Subject: [PATCH 02/15] Reduce cyclomatic complexity below 9 in diff and update-patch commands Extract per-project logic from Diff.__call__ (was CC=11) into _diff_project and from UpdatePatch.__call__ (was CC=10) into _process_project. All methods now have CC <= 8. https://claude.ai/code/session_01RTgPqrX37jFkK843dq65Bu Co-Authored-By: Claude Sonnet 4.6 --- dfetch/commands/diff.py | 94 +++++++++++--------- dfetch/commands/update_patch.py | 152 +++++++++++++++++--------------- 2 files changed, 134 insertions(+), 112 deletions(-) diff --git a/dfetch/commands/diff.py b/dfetch/commands/diff.py index 2e529ef27..27cb3b71e 100644 --- a/dfetch/commands/diff.py +++ b/dfetch/commands/diff.py @@ -58,11 +58,12 @@ import pathlib import dfetch.commands.command +import dfetch.manifest.project 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 -from dfetch.util.util import catch_runtime_exceptions, in_directory +from dfetch.project.superproject import NoVcsSuperProject, RevisionRange, SuperProject +from dfetch.util.util import in_directory logger = get_logger(__name__) @@ -115,51 +116,62 @@ def __call__(self, args: argparse.Namespace) -> None: ) with in_directory(superproject.root_directory): - exceptions: list[str] = [] projects = superproject.manifest.selected_projects(args.projects) if not projects: raise RuntimeError( f"No (such) project found! {', '.join(args.projects)}" ) + had_errors: bool = False for project in projects: - with catch_runtime_exceptions(exceptions) as exceptions: - if not os.path.exists(project.destination): - raise RuntimeError( - "You cannot generate a diff of a project that was never fetched" - ) - subproject = superproject.get_sub_project(project) - - 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 = superproject.diff( - project.destination, - revisions=RevisionRange(old_rev, new_rev), - ignore=(Metadata.FILENAME,), - ) - - msg = self._rev_msg(old_rev, new_rev) - if patch: - patch_path = pathlib.Path(f"{project.name}.patch") - logger.print_info_line( - project.name, - f"Generating patch {patch_path} {msg} in {superproject.root_directory}", - ) - patch_path.write_text(patch, encoding="UTF-8") - else: - logger.print_info_line(project.name, f"No diffs found {msg}") - - if exceptions: - raise RuntimeError("\n".join(exceptions)) + try: + self._diff_project(superproject, project, old_rev, new_rev) + except RuntimeError as exc: + logger.print_warning_line(project.name, str(exc)) + had_errors = True + + if had_errors: + raise RuntimeError() + + def _diff_project( + self, + superproject: SuperProject, + project: dfetch.manifest.project.ProjectEntry, + old_rev: str, + new_rev: str, + ) -> None: + """Generate a diff patch for a single project.""" + if not os.path.exists(project.destination): + raise RuntimeError( + "You cannot generate a diff of a project that was never fetched" + ) + subproject = superproject.get_sub_project(project) + + 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 = superproject.diff( + project.destination, + revisions=RevisionRange(old_rev, new_rev), + ignore=(Metadata.FILENAME,), + ) + + msg = self._rev_msg(old_rev, new_rev) + if patch: + patch_path = pathlib.Path(f"{project.name}.patch") + logger.print_info_line( + project.name, + f"Generating patch {patch_path} {msg} in {superproject.root_directory}", + ) + patch_path.write_text(patch, encoding="UTF-8") + else: + logger.print_info_line(project.name, f"No diffs found {msg}") @staticmethod def _parse_revs(revs_arg: str) -> tuple[str, str]: diff --git a/dfetch/commands/update_patch.py b/dfetch/commands/update_patch.py index 5f4a9f066..a7d3e4680 100644 --- a/dfetch/commands/update_patch.py +++ b/dfetch/commands/update_patch.py @@ -40,9 +40,8 @@ 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.project.superproject import NoVcsSuperProject, RevisionRange, SuperProject from dfetch.util.util import ( - catch_runtime_exceptions, check_no_path_traversal, in_directory, ) @@ -76,7 +75,7 @@ def __call__(self, args: argparse.Namespace) -> None: """Perform the update patch.""" superproject = create_super_project() - exceptions: list[str] = [] + had_errors: bool = False if isinstance(superproject, NoVcsSuperProject): raise RuntimeError( @@ -88,74 +87,85 @@ 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.create_sub_project(project) - destination = project.destination - - def _ignored(dst: str = destination) -> list[str]: - return list(superproject.ignored_files(dst)) - - # Check if the project has a patch, maybe suggest creating one? - if not subproject.patch: - logger.print_warning_line( - project.name, - f'skipped - there is no patch file, use "dfetch diff {project.name}"' - " to generate one instead", - ) - continue - - # Check if the project was ever fetched - on_disk_version = subproject.on_disk_version() - if not on_disk_version: - logger.print_warning_line( - project.name, - f'skipped - the project was never fetched before, use "dfetch update {project.name}"', - ) - continue - - # Make sure no uncommitted changes (don't care about ignored files) - if superproject.has_local_changes_in_dir(subproject.local_path): - logger.print_warning_line( - project.name, - f"skipped - Uncommitted changes in {subproject.local_path}", - ) - continue - - # force update to fetched version from metadata without applying patch - subproject.update( - force=True, - ignored_files_callback=_ignored, - patch_count=len(subproject.patch) - 1, - eol_preferences_callback=superproject.eol_preferences, - ) - - # generate reverse patch - patch_text = superproject.diff( - subproject.local_path, - revisions=RevisionRange("", ""), - ignore=(Metadata.FILENAME,), - reverse=True, - ) - - # Select patch to overwrite & make backup - if not self._update_patch( - subproject.patch[-1], - superproject.root_directory, - project.name, - patch_text, - ): - continue - - # force update again to fetched version from metadata but with applying patch - subproject.update( - force=True, - ignored_files_callback=_ignored, - patch_count=-1, - eol_preferences_callback=superproject.eol_preferences, - ) - - if exceptions: - raise RuntimeError("\n".join(exceptions)) + try: + self._process_project(superproject, project) + except RuntimeError as exc: + logger.print_warning_line(project.name, str(exc)) + had_errors = True + + if had_errors: + raise RuntimeError() + + def _process_project( + self, + superproject: SuperProject, + project: dfetch.manifest.project.ProjectEntry, + ) -> None: + """Perform the patch update for a single project.""" + subproject = dfetch.project.create_sub_project(project) + destination = project.destination + + def _ignored(dst: str = destination) -> list[str]: + return list(superproject.ignored_files(dst)) + + # Check if the project has a patch, maybe suggest creating one? + if not subproject.patch: + logger.print_warning_line( + project.name, + f'skipped - there is no patch file, use "dfetch diff {project.name}"' + " to generate one instead", + ) + return + + # Check if the project was ever fetched + on_disk_version = subproject.on_disk_version() + if not on_disk_version: + logger.print_warning_line( + project.name, + f'skipped - the project was never fetched before, use "dfetch update {project.name}"', + ) + return + + # Make sure no uncommitted changes (don't care about ignored files) + if superproject.has_local_changes_in_dir(subproject.local_path): + logger.print_warning_line( + project.name, + f"skipped - Uncommitted changes in {subproject.local_path}", + ) + return + + # force update to fetched version from metadata without applying patch + subproject.update( + force=True, + ignored_files_callback=_ignored, + patch_count=len(subproject.patch) - 1, + eol_preferences_callback=superproject.eol_preferences, + ) + + # generate reverse patch + patch_text = superproject.diff( + subproject.local_path, + revisions=RevisionRange("", ""), + ignore=(Metadata.FILENAME,), + reverse=True, + ) + + # Select patch to overwrite & make backup + if not self._update_patch( + subproject.patch[-1], + superproject.root_directory, + project.name, + patch_text, + ): + return + + # force update again to fetched version from metadata but with applying patch + subproject.update( + force=True, + ignored_files_callback=_ignored, + patch_count=-1, + eol_preferences_callback=superproject.eol_preferences, + ) def _update_patch( self, From 9a563139dbbb00b4bb37832b8e21e7da8d6df609 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 12 Apr 2026 12:46:47 +0000 Subject: [PATCH 03/15] Sort commands --- .claude/settings.json | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index cb6980dcd..22f61dde5 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,24 +1,27 @@ { "permissions": { "allow": [ + "Bash(bandit -r dfetch)", + "Bash(black --check dfetch)", "Bash(codespell)", - "Bash(pre-commit run *)", - "Bash(git stash *)", - "Bash(xenon *)", - "Bash(radon *)", - "Bash(isort --diff dfetch*)", - "Bash(black --check dfetch*)", - "Bash(pylint dfetch*)", - "Bash(ruff check dfetch*)", - "Bash(mypy dfetch*)", - "Bash(python -m mypy dfetch*)", - "Bash(python -m pytest tests/*)", - "Bash(pip show *)", - "Bash(doc8 doc*)", - "Bash(pydocstyle dfetch*)", - "Bash(bandit *)", + "Bash(doc8 doc:*)", + "Bash(git stash:*)", + "Bash(isort --diff dfetch)", + "Bash(lint-imports)", + "Bash(mypy dfetch:*)", + "Bash(pip install:*)", + "Bash(pip show:*)", + "Bash(pre-commit run:*)", + "Bash(pydocstyle dfetch:*)", + "Bash(pylint dfetch:*)", "Bash(pyroma --directory --min=10 .)", - "Bash(xargs pyupgrade *)", + "Bash(pytest tests/test_sbom_reporter.py -q)", + "Bash(python -m behave *)", + "Bash(python -m pytest tests/*)", + "Bash(radon *)", + "Bash(ruff check:*)", + "Bash(xenon *)", + "Bash(xargs pyupgrade:*)", "Bash(lint-imports)", "Bash(pip install *)", "Bash(pytest *)", From 026c9155c12c01854f3be5270353b0407eda316e Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 12 Apr 2026 13:35:45 +0000 Subject: [PATCH 04/15] Fix #1096: print runtime errors in context of failing subproject Replace the pattern of collecting RuntimeErrors across all subprojects and printing them at the end with immediate in-context logging via `logger.print_warning_line(project.name, str(exc))` at the point of failure. This ensures errors like "svn not available on system" appear next to the subproject they belong to instead of at the end of the run. The final `raise RuntimeError()` (no message) still produces a non-zero exit code without double-printing. Also bumps version to 0.14.0 and updates CHANGELOG, feature tests. https://claude.ai/code/session_01RTgPqrX37jFkK843dq65Bu Co-Authored-By: Claude Sonnet 4.6 --- features/check-git-repo.feature | 7 ++++--- features/check-svn-repo.feature | 9 +++++---- features/patch-after-fetch-svn.feature | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/features/check-git-repo.feature b/features/check-git-repo.feature index 374ca4172..e1fe52506 100644 --- a/features/check-git-repo.feature +++ b/features/check-git-repo.feature @@ -181,9 +181,10 @@ Feature: Checking dependencies from a git repository Then the output shows """ Dfetch (0.14.0) - >>>git ls-remote --heads https://giiiiiidhub.com/i-do-not-exist/broken<<< failed! - 'https://giiiiiidhub.com/i-do-not-exist/broken' is not a valid URL or unreachable: - fatal: unable to access 'https://giiiiiidhub.com/i-do-not-exist/broken/': Could not resolve host: giiiiiidhub.com + non-existent-url: + > >>>git ls-remote --heads https://giiiiiidhub.com/i-do-not-exist/broken<<< failed! + 'https://giiiiiidhub.com/i-do-not-exist/broken' is not a valid URL or unreachable: + fatal: unable to access 'https://giiiiiidhub.com/i-do-not-exist/broken/': Could not resolve host: giiiiiidhub.com """ Scenario: A non-existent tag, branch or revision is reported diff --git a/features/check-svn-repo.feature b/features/check-svn-repo.feature index 573e2552e..28adfd0fb 100644 --- a/features/check-svn-repo.feature +++ b/features/check-svn-repo.feature @@ -138,10 +138,11 @@ Feature: Checking dependencies from a svn repository Then the output shows """ Dfetch (0.14.0) - >>>svn --non-interactive info https://giiiiiidhub.com/i-do-not-exist/broken/trunk<<< failed! - 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' is not a valid URL or unreachable: - svn: E170013: Unable to connect to a repository at URL 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' - svn: E670002: Name or service not known + non-existent-url: + > >>>svn --non-interactive info https://giiiiiidhub.com/i-do-not-exist/broken/trunk<<< failed! + 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' is not a valid URL or unreachable: + svn: E170013: Unable to connect to a repository at URL 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' + svn: E670002: Name or service not known """ Scenario: A non-existent tag in svn repo diff --git a/features/patch-after-fetch-svn.feature b/features/patch-after-fetch-svn.feature index 3999e8e06..8a347bcf7 100644 --- a/features/patch-after-fetch-svn.feature +++ b/features/patch-after-fetch-svn.feature @@ -76,5 +76,5 @@ Feature: Patch after fetching from svn repo source/target file does not exist: --- b'build-deb2.sh' +++ b'build-deb2.sh' - Applying patch "diff.patch" failed + > Applying patch "diff.patch" failed """ From 6d1107e9de60ae538d0b0638e285934e57bcf4f4 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 12 Apr 2026 15:56:15 +0000 Subject: [PATCH 05/15] Cleanup commands --- .claude/settings.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 22f61dde5..aab79307c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,15 +1,18 @@ { "permissions": { "allow": [ + "Bash(codespell)", "Bash(bandit -r dfetch)", "Bash(black --check dfetch)", - "Bash(codespell)", "Bash(doc8 doc:*)", + "Bash(git add:*)", + "Bash(git fetch:*)", + "Bash(git pull:*)", "Bash(git stash:*)", + "Bash(git update-index --refresh)", "Bash(isort --diff dfetch)", "Bash(lint-imports)", "Bash(mypy dfetch:*)", - "Bash(pip install:*)", "Bash(pip show:*)", "Bash(pre-commit run:*)", "Bash(pydocstyle dfetch:*)", From 838b26c53bbfb70c891b78c43cc22f0aadfba5f7 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 13 Apr 2026 08:34:49 +0200 Subject: [PATCH 06/15] Revert version bump --- dfetch/__init__.py | 2 +- features/check-archive.feature | 12 +++++------ features/check-git-repo.feature | 20 +++++++++---------- features/check-specific-projects.feature | 2 +- features/check-svn-repo.feature | 14 ++++++------- .../checked-project-has-dependencies.feature | 6 +++--- features/diff-in-git.feature | 4 ++-- features/diff-in-svn.feature | 4 ++-- features/fetch-archive.feature | 4 ++-- features/fetch-checks-destination.feature | 4 ++-- features/fetch-file-pattern-git.feature | 4 ++-- features/fetch-file-pattern-svn.feature | 2 +- .../fetch-git-repo-with-submodule.feature | 8 ++++---- features/fetch-git-repo.feature | 4 ++-- features/fetch-single-file-git.feature | 4 ++-- features/fetch-single-file-svn.feature | 2 +- features/fetch-with-ignore-git.feature | 6 +++--- features/fetch-with-ignore-svn.feature | 6 +++--- .../guard-against-overwriting-git.feature | 6 +++--- .../guard-against-overwriting-svn.feature | 6 +++--- features/handle-invalid-metadata.feature | 2 +- features/journey-basic-patching.feature | 2 +- features/journey-basic-usage.feature | 2 +- features/list-projects.feature | 6 +++--- features/patch-after-fetch-git.feature | 8 ++++---- features/patch-after-fetch-svn.feature | 2 +- features/patch-fuzzy-matching-git.feature | 2 +- features/report-sbom.feature | 4 ++-- features/suggest-project-name.feature | 6 +++--- features/update-patch-in-git.feature | 2 +- features/update-patch-in-svn.feature | 2 +- .../updated-project-has-dependencies.feature | 4 ++-- features/validate-manifest.feature | 10 +++++----- 33 files changed, 86 insertions(+), 86 deletions(-) diff --git a/dfetch/__init__.py b/dfetch/__init__.py index 7cf054bbc..1651dc472 100644 --- a/dfetch/__init__.py +++ b/dfetch/__init__.py @@ -1,5 +1,5 @@ """Dfetch.""" -__version__ = "0.14.0" +__version__ = "0.13.0" DEFAULT_MANIFEST_NAME: str = "dfetch.yaml" diff --git a/features/check-archive.feature b/features/check-archive.feature index 745e0a424..6919718b5 100644 --- a/features/check-archive.feature +++ b/features/check-archive.feature @@ -25,7 +25,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > up-to-date (some-remote-server/SomeProject.tar.gz) """ @@ -49,7 +49,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > up-to-date (sha256:) """ @@ -70,7 +70,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > wanted (some-remote-server/SomeProject.tar.gz), available (some-remote-server/SomeProject.tar.gz) """ @@ -88,7 +88,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) non-existent-archive: > wanted (https://dfetch.invalid/does-not-exist.tar.gz), but not available at the upstream. """ @@ -114,7 +114,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check SomeProject" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > up-to-date (some-remote-server/SomeProject.tar.gz) """ @@ -137,7 +137,7 @@ Feature: Checking dependencies from an archive When I run "dfetch check SomeProject" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Local changes were detected, please generate a patch using 'dfetch diff SomeProject' and add it to your manifest using 'patch:'. Alternatively overwrite the local changes with 'dfetch update --force SomeProject' > up-to-date (some-remote-server/SomeProject.tar.gz) diff --git a/features/check-git-repo.feature b/features/check-git-repo.feature index e1fe52506..e3439b7ca 100644 --- a/features/check-git-repo.feature +++ b/features/check-git-repo.feature @@ -27,7 +27,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-rev-only: > wanted (e1fda19a57b873eb8e6ae37780594cbb77b70f1a), available (e1fda19a57b873eb8e6ae37780594cbb77b70f1a) ext/test-rev-and-branch: @@ -53,7 +53,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag-v1: > wanted (v1), available (v2.0) """ @@ -83,7 +83,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-rev-only: > up-to-date (e1fda19a57b873eb8e6ae37780594cbb77b70f1a) ext/test-rev-and-branch: @@ -117,7 +117,7 @@ Feature: Checking dependencies from a git repository And I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > wanted (v2.0), current (v1), available (v2.0) """ @@ -139,7 +139,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check SomeProject" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Local changes were detected, please generate a patch using 'dfetch diff SomeProject' and add it to your manifest using 'patch:'. Alternatively overwrite the local changes with 'dfetch update --force SomeProject' > up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0) @@ -160,7 +160,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check SomeProject" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0) """ @@ -180,7 +180,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) non-existent-url: > >>>git ls-remote --heads https://giiiiiidhub.com/i-do-not-exist/broken<<< failed! 'https://giiiiiidhub.com/i-do-not-exist/broken' is not a valid URL or unreachable: @@ -210,7 +210,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProjectMissingTag: > wanted (i-dont-exist), but not available at the upstream. SomeProjectNonExistentBranch: @@ -232,7 +232,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output starts with: """ - Dfetch (0.14.0) + Dfetch (0.13.0) >>>git ls-remote --heads --tags https://github.com/dfetch-org/test-repo-private.git<<< returned 128: """ @@ -249,7 +249,7 @@ Feature: Checking dependencies from a git repository When I run "dfetch check" Then the output starts with: """ - Dfetch (0.14.0) + Dfetch (0.13.0) >>>git ls-remote --heads --tags git@github.com:dfetch-org/test-repo-private.git<<< returned 128: """ diff --git a/features/check-specific-projects.feature b/features/check-specific-projects.feature index 038434dc7..fe445a6d7 100644 --- a/features/check-specific-projects.feature +++ b/features/check-specific-projects.feature @@ -28,7 +28,7 @@ Feature: Checking specific projects When I run "dfetch check ext/test-rev-and-branch" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-rev-and-branch: > wanted (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a) """ diff --git a/features/check-svn-repo.feature b/features/check-svn-repo.feature index 28adfd0fb..0924f298b 100644 --- a/features/check-svn-repo.feature +++ b/features/check-svn-repo.feature @@ -29,7 +29,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) cunit-svn-rev-only: > wanted (176), available (trunk - 176) cunit-svn-rev-and-branch: @@ -56,7 +56,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) cutter-svn-tag: > wanted (1.1.7), available (1.1.8) """ @@ -94,7 +94,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) cunit-svn-rev-only: > wanted (169), current (trunk - 169), available (trunk - 176) cunit-svn-rev-and-branch: @@ -118,7 +118,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > wanted (latest), current (1), available (1) """ @@ -137,7 +137,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) non-existent-url: > >>>svn --non-interactive info https://giiiiiidhub.com/i-do-not-exist/broken/trunk<<< failed! 'https://giiiiiidhub.com/i-do-not-exist/broken/trunk' is not a valid URL or unreachable: @@ -163,7 +163,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) cutter-svn-tag: > wanted (non-existent-tag), but not available at the upstream. """ @@ -184,7 +184,7 @@ Feature: Checking dependencies from a svn repository When I run "dfetch check SomeProject" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > up-to-date (v1) """ diff --git a/features/checked-project-has-dependencies.feature b/features/checked-project-has-dependencies.feature index 3f89826f5..5ab556d85 100644 --- a/features/checked-project-has-dependencies.feature +++ b/features/checked-project-has-dependencies.feature @@ -30,7 +30,7 @@ Feature: Check for dependencies in projects When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > wanted (v1), available (v1) """ @@ -67,7 +67,7 @@ Feature: Check for dependencies in projects When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) Multiple manifests found, using dfetch.yaml SomeProject: > up-to-date (v1) @@ -111,7 +111,7 @@ Feature: Check for dependencies in projects When I run "dfetch check" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) Multiple manifests found, using dfetch.yaml SomeProject: > up-to-date (v1) diff --git a/features/diff-in-git.feature b/features/diff-in-git.feature index 1c2eb0650..533dce6dd 100644 --- a/features/diff-in-git.feature +++ b/features/diff-in-git.feature @@ -66,7 +66,7 @@ Feature: Diff in git When I run "dfetch diff SomeProject" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > No diffs found since 59efb91396fd369eb113b43382783294dc8ed6d2 """ @@ -93,7 +93,7 @@ Feature: Diff in git When I run "dfetch diff SomeProject" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > No diffs found since 59efb91396fd369eb113b43382783294dc8ed6d2 """ diff --git a/features/diff-in-svn.feature b/features/diff-in-svn.feature index e6ff06694..5f1177dae 100644 --- a/features/diff-in-svn.feature +++ b/features/diff-in-svn.feature @@ -63,7 +63,7 @@ Feature: Diff in svn When I run "dfetch diff SomeProject" in MySvnProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > No diffs found since 1 """ @@ -90,7 +90,7 @@ Feature: Diff in svn When I run "dfetch diff SomeProject" in MySvnProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > No diffs found since 1 """ diff --git a/features/fetch-archive.feature b/features/fetch-archive.feature index 855bea504..44343f4a3 100644 --- a/features/fetch-archive.feature +++ b/features/fetch-archive.feature @@ -118,7 +118,7 @@ Feature: Fetching dependencies from an archive (tar/zip) When I run "dfetch update" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Hash mismatch for SomeProject! sha256 expected 0000000000000000000000000000000000000000000000000000000000000000 """ @@ -196,7 +196,7 @@ Feature: Fetching dependencies from an archive (tar/zip) When I run "dfetch update --force" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Fetched some-remote-server/SomeProject.tar.gz """ diff --git a/features/fetch-checks-destination.feature b/features/fetch-checks-destination.feature index 045ab707b..c1279ba3c 100644 --- a/features/fetch-checks-destination.feature +++ b/features/fetch-checks-destination.feature @@ -21,7 +21,7 @@ Feature: Fetch checks destinations When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > Skipping, path "." is not allowed as destination. """ @@ -42,7 +42,7 @@ Feature: Fetch checks destinations When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > Skipping, path "../../some-higher-folder" is outside manifest directory tree. """ diff --git a/features/fetch-file-pattern-git.feature b/features/fetch-file-pattern-git.feature index 358f00e35..e969915d9 100644 --- a/features/fetch-file-pattern-git.feature +++ b/features/fetch-file-pattern-git.feature @@ -25,7 +25,7 @@ Feature: Fetch file pattern from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProjectWithAnInterestingFile: > Fetched v1 """ @@ -57,7 +57,7 @@ Feature: Fetch file pattern from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) The 'src:' filter 'SomeFolder/Some*' matches multiple directories from 'some-remote-server/SomeProjectWithAnInterestingFile.git'. Only considering files in 'SomeFolder/SomeOtherSubFolder'. SomeProjectWithAnInterestingFile: > Fetched v1 diff --git a/features/fetch-file-pattern-svn.feature b/features/fetch-file-pattern-svn.feature index 61f62c7da..a5f3416b8 100644 --- a/features/fetch-file-pattern-svn.feature +++ b/features/fetch-file-pattern-svn.feature @@ -22,7 +22,7 @@ Feature: Fetch file pattern from svn repo When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProjectWithAnInterestingFile: > Fetched trunk - 1 """ diff --git a/features/fetch-git-repo-with-submodule.feature b/features/fetch-git-repo-with-submodule.feature index 0bf175a99..c349a3c6e 100644 --- a/features/fetch-git-repo-with-submodule.feature +++ b/features/fetch-git-repo-with-submodule.feature @@ -25,7 +25,7 @@ Feature: Fetch projects with nested VCS dependencies When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) my-project-with-submodules: > Found & fetched submodule "./ext/test-repo1" (some-remote-server/TestRepo.git @ master - 79698c99152e4a4b7b759c9def50a130bc91a2ff) > Found & fetched submodule "./ext/test-repo2" (some-remote-server/TestRepo.git @ master - 79698c99152e4a4b7b759c9def50a130bc91a2ff) @@ -64,7 +64,7 @@ Feature: Fetch projects with nested VCS dependencies When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) outer-project: > Found & fetched submodule "./ext/middle" (some-remote-server/MiddleProject.git @ master - 79698c99152e4a4b7b759c9def50a130bc91a2ff) > Fetched master - e1fda19a57b873eb8e6ae37780594cbb77b70f1a @@ -96,7 +96,7 @@ Feature: Fetch projects with nested VCS dependencies When I run "dfetch report" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) my-project-with-submodules: - remote : remote url : some-remote-server/SomeInterestingProject.git @@ -139,7 +139,7 @@ Feature: Fetch projects with nested VCS dependencies When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) glob-project: > Found & fetched submodule "./ext/test-repo" (some-remote-server/TestRepo.git @ master - 79698c99152e4a4b7b759c9def50a130bc91a2ff) > Fetched master - e1fda19a57b873eb8e6ae37780594cbb77b70f1a diff --git a/features/fetch-git-repo.feature b/features/fetch-git-repo.feature index 4ac7d3c31..2d3846fb0 100644 --- a/features/fetch-git-repo.feature +++ b/features/fetch-git-repo.feature @@ -64,7 +64,7 @@ Feature: Fetching dependencies from a git repository And I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > Fetched v2.0 """ @@ -85,7 +85,7 @@ Feature: Fetching dependencies from a git repository When I run "dfetch update --force" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > Fetched v1 """ diff --git a/features/fetch-single-file-git.feature b/features/fetch-single-file-git.feature index fa8187abc..d8dd06829 100644 --- a/features/fetch-single-file-git.feature +++ b/features/fetch-single-file-git.feature @@ -22,7 +22,7 @@ Feature: Fetch single file from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProjectWithAnInterestingFile: > Fetched v1 """ @@ -54,7 +54,7 @@ Feature: Fetch single file from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProjectWithAnInterestingFile: > Fetched v1 """ diff --git a/features/fetch-single-file-svn.feature b/features/fetch-single-file-svn.feature index 7850663b7..25d817b08 100644 --- a/features/fetch-single-file-svn.feature +++ b/features/fetch-single-file-svn.feature @@ -22,7 +22,7 @@ Feature: Fetch single file from svn repo When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProjectWithAnInterestingFile: > Fetched trunk - 1 """ diff --git a/features/fetch-with-ignore-git.feature b/features/fetch-with-ignore-git.feature index d3ea701b7..f05556a08 100644 --- a/features/fetch-with-ignore-git.feature +++ b/features/fetch-with-ignore-git.feature @@ -29,7 +29,7 @@ Feature: Fetch with ignore in git When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeInterestingProject: > Fetched v1 """ @@ -59,7 +59,7 @@ Feature: Fetch with ignore in git When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeInterestingProject: > Fetched v1 """ @@ -92,7 +92,7 @@ Feature: Fetch with ignore in git When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeInterestingProject: > Fetched v1 """ diff --git a/features/fetch-with-ignore-svn.feature b/features/fetch-with-ignore-svn.feature index 0f94db1fc..f1423236a 100644 --- a/features/fetch-with-ignore-svn.feature +++ b/features/fetch-with-ignore-svn.feature @@ -28,7 +28,7 @@ Feature: Fetch with ignore in svn When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeInterestingProject: > Fetched trunk - 1 """ @@ -57,7 +57,7 @@ Feature: Fetch with ignore in svn When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeInterestingProject: > Fetched trunk - 1 """ @@ -89,7 +89,7 @@ Feature: Fetch with ignore in svn When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeInterestingProject: > Fetched trunk - 1 """ diff --git a/features/guard-against-overwriting-git.feature b/features/guard-against-overwriting-git.feature index 30ea95e38..98ad1422b 100644 --- a/features/guard-against-overwriting-git.feature +++ b/features/guard-against-overwriting-git.feature @@ -24,7 +24,7 @@ Feature: Guard against overwriting in git When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > skipped - local changes after last update (use --force to overwrite) """ @@ -34,7 +34,7 @@ Feature: Guard against overwriting in git When I run "dfetch update --force" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Fetched v2 """ @@ -45,7 +45,7 @@ Feature: Guard against overwriting in git When I run "dfetch update SomeProject" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Fetched v2 """ diff --git a/features/guard-against-overwriting-svn.feature b/features/guard-against-overwriting-svn.feature index a904b901a..e9af3238e 100644 --- a/features/guard-against-overwriting-svn.feature +++ b/features/guard-against-overwriting-svn.feature @@ -24,7 +24,7 @@ Feature: Guard against overwriting in svn When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > skipped - local changes after last update (use --force to overwrite) """ @@ -34,7 +34,7 @@ Feature: Guard against overwriting in svn When I run "dfetch update --force" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Fetched v2 """ @@ -45,7 +45,7 @@ Feature: Guard against overwriting in svn When I run "dfetch update SomeProject" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Fetched v2 """ diff --git a/features/handle-invalid-metadata.feature b/features/handle-invalid-metadata.feature index 7f4c94c2c..e4726ddb2 100644 --- a/features/handle-invalid-metadata.feature +++ b/features/handle-invalid-metadata.feature @@ -72,7 +72,7 @@ Feature: Handle invalid metadata files When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > ext/test-repo-tag/.dfetch_data.yaml is an invalid metadata file, not checking on disk version! > ext/test-repo-tag/.dfetch_data.yaml is an invalid metadata file, not checking local hash! diff --git a/features/journey-basic-patching.feature b/features/journey-basic-patching.feature index 8277325c3..5eb5726a8 100644 --- a/features/journey-basic-patching.feature +++ b/features/journey-basic-patching.feature @@ -56,7 +56,7 @@ Feature: Basic patch journey And I run "dfetch update -f test-repo" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) test-repo: > Fetched v1 > Applying patch "test-repo.patch" diff --git a/features/journey-basic-usage.feature b/features/journey-basic-usage.feature index 4b88ff450..d719f0e1d 100644 --- a/features/journey-basic-usage.feature +++ b/features/journey-basic-usage.feature @@ -29,7 +29,7 @@ Feature: Basic usage journey When I run "dfetch check" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > wanted & current (v1), available (v2.0) """ diff --git a/features/list-projects.feature b/features/list-projects.feature index 0a23c969d..c1421c212 100644 --- a/features/list-projects.feature +++ b/features/list-projects.feature @@ -28,7 +28,7 @@ Feature: List dependencies When I run "dfetch report" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: - remote : remote url : https://github.com/dfetch-org/test-repo @@ -68,7 +68,7 @@ Feature: List dependencies When I run "dfetch report" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) cutter-svn-tag: - remote : remote url : https://svn.code.sf.net/p/cutter/svn/cutter @@ -85,7 +85,7 @@ Feature: List dependencies When I run "dfetch report" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: - remote : github-com-dfetch-org remote url : https://github.com/dfetch-org/test-repo diff --git a/features/patch-after-fetch-git.feature b/features/patch-after-fetch-git.feature index d2da6f2de..e030d8ff0 100644 --- a/features/patch-after-fetch-git.feature +++ b/features/patch-after-fetch-git.feature @@ -68,7 +68,7 @@ Feature: Patch after fetching from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > Fetched v2.0 > Applying patch "diff.patch" @@ -126,7 +126,7 @@ Feature: Patch after fetching from git repo """ And the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > Fetched v2.0 > Applying patch "001-diff.patch" @@ -170,7 +170,7 @@ Feature: Patch after fetching from git repo """ And the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > Fetched v2.0 > Applying patch "diff.patch" @@ -198,7 +198,7 @@ Feature: Patch after fetching from git repo When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) ext/test-repo-tag: > Fetched v2.0 > Skipping patch "../diff.patch" which is outside /some/path. diff --git a/features/patch-after-fetch-svn.feature b/features/patch-after-fetch-svn.feature index 8a347bcf7..f637162af 100644 --- a/features/patch-after-fetch-svn.feature +++ b/features/patch-after-fetch-svn.feature @@ -69,7 +69,7 @@ Feature: Patch after fetching from svn repo When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) cutter: > Fetched 1.1.7 > Applying patch "diff.patch" diff --git a/features/patch-fuzzy-matching-git.feature b/features/patch-fuzzy-matching-git.feature index d6ed2c319..02a9bbb37 100644 --- a/features/patch-fuzzy-matching-git.feature +++ b/features/patch-fuzzy-matching-git.feature @@ -60,7 +60,7 @@ Feature: Patch application tolerates small upstream changes When I run "dfetch update" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Fetched master - f47d80c35e14dfa4f9c9c30c9865cbf0f8d50933 > Applying patch "SomeProject.patch" diff --git a/features/report-sbom.feature b/features/report-sbom.feature index 415e0986e..c17448d52 100644 --- a/features/report-sbom.feature +++ b/features/report-sbom.feature @@ -123,7 +123,7 @@ Feature: Create an CycloneDX sbom "tools": { "components": [ { - "bom-ref": "dfetch-0.14.0", + "bom-ref": "dfetch-0.13.0", "externalReferences": [ { "type": "build-system", @@ -171,7 +171,7 @@ Feature: Create an CycloneDX sbom "name": "dfetch-org" }, "type": "application", - "version": "0.14.0" + "version": "0.13.0" }, { "description": "Python library for CycloneDX", diff --git a/features/suggest-project-name.feature b/features/suggest-project-name.feature index e8f65a297..cb3252efb 100644 --- a/features/suggest-project-name.feature +++ b/features/suggest-project-name.feature @@ -16,7 +16,7 @@ Feature: Suggest a project name When I run "dfetch check project with space" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) Not all projects found! "project", "with", "space" This manifest contains: "project with space", "project-with-l", "Project-With-Capital" Did you mean: "project with space"? @@ -26,7 +26,7 @@ Feature: Suggest a project name When I run "dfetch check project-with-1" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) Not all projects found! "project-with-1" This manifest contains: "project with space", "project-with-l", "Project-With-Capital" Did you mean: "project-with-l"? @@ -36,7 +36,7 @@ Feature: Suggest a project name When I run "dfetch check project-with-1 project-with-space Project-With-Capital" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) Not all projects found! "project-with-1", "project-with-space" This manifest contains: "project with space", "project-with-l", "Project-With-Capital" Did you mean: "project with space" and "project-with-l"? diff --git a/features/update-patch-in-git.feature b/features/update-patch-in-git.feature index 36d2ea698..6f2e31eb6 100644 --- a/features/update-patch-in-git.feature +++ b/features/update-patch-in-git.feature @@ -51,7 +51,7 @@ Feature: Update an existing patch in git """ And the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Fetched master - f9b88b8259d9a7fb48327bf23beabe40c150d474 > Updating patch "patches/SomeProject.patch" diff --git a/features/update-patch-in-svn.feature b/features/update-patch-in-svn.feature index 34ba4b2db..1b665c53e 100644 --- a/features/update-patch-in-svn.feature +++ b/features/update-patch-in-svn.feature @@ -52,7 +52,7 @@ Feature: Update an existing patch in svn """ And the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) Update patch is only fully supported in git superprojects! SomeProject: > Fetched trunk - 1 diff --git a/features/updated-project-has-dependencies.feature b/features/updated-project-has-dependencies.feature index 4f9036710..19a9b00e7 100644 --- a/features/updated-project-has-dependencies.feature +++ b/features/updated-project-has-dependencies.feature @@ -41,7 +41,7 @@ Feature: Updated project has dependencies When I run "dfetch update" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProjectWithManifest: > Fetched v1 > "SomeProjectWithManifest" depends on the following project(s) which are not part of your manifest: @@ -89,7 +89,7 @@ Feature: Updated project has dependencies When I run "dfetch update" in MyProject Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) SomeProject: > Fetched v1 > SomeProject/dfetch.yaml: Schema validation failed: diff --git a/features/validate-manifest.feature b/features/validate-manifest.feature index 0f13d79d9..4c36d1bf2 100644 --- a/features/validate-manifest.feature +++ b/features/validate-manifest.feature @@ -21,7 +21,7 @@ Feature: Validate a manifest When I run "dfetch validate" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) dfetch.yaml : valid """ @@ -56,7 +56,7 @@ Feature: Validate a manifest When I run "dfetch validate" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) Schema validation failed: manifest-wrong: @@ -94,7 +94,7 @@ Feature: Validate a manifest When I run "dfetch validate" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) dfetch.yaml : valid """ @@ -115,7 +115,7 @@ Feature: Validate a manifest When I run "dfetch validate" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) Schema validation failed: hash: not-a-valid-hash ^ (line: 9) @@ -137,7 +137,7 @@ Feature: Validate a manifest When I run "dfetch validate" Then the output shows """ - Dfetch (0.14.0) + Dfetch (0.13.0) Schema validation failed: Duplicate manifest.projects.name value(s): ext/test-repo-rev-only """ From 836bd333c17c85d25d0e1ead955d4e56d5ef4fce Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 13 Apr 2026 17:19:52 +0000 Subject: [PATCH 07/15] Restrict commits in settings.json --- .claude/settings.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index aab79307c..9b35fe923 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,9 +5,7 @@ "Bash(bandit -r dfetch)", "Bash(black --check dfetch)", "Bash(doc8 doc:*)", - "Bash(git add:*)", "Bash(git fetch:*)", - "Bash(git pull:*)", "Bash(git stash:*)", "Bash(git update-index --refresh)", "Bash(isort --diff dfetch)", From f74f467921fc36d576d67e35cd2788b537d7e6ea Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 13 Apr 2026 17:23:12 +0000 Subject: [PATCH 08/15] Review comments --- dfetch/commands/check.py | 9 +++++---- dfetch/commands/freeze.py | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/dfetch/commands/check.py b/dfetch/commands/check.py index f6f0a8222..b01f3d2aa 100644 --- a/dfetch/commands/check.py +++ b/dfetch/commands/check.py @@ -114,14 +114,15 @@ def __call__(self, args: argparse.Namespace) -> None: reporters, files_to_ignore=superproject.ignored_files(project.destination), ) + if not args.no_recommendations and os.path.isdir( + project.destination + ): + with in_directory(project.destination): + check_sub_manifests(superproject.manifest, project) except RuntimeError as exc: logger.print_warning_line(project.name, str(exc)) had_errors = True - if not args.no_recommendations and os.path.isdir(project.destination): - with in_directory(project.destination): - check_sub_manifests(superproject.manifest, project) - for reporter in reporters: reporter.dump_to_file() diff --git a/dfetch/commands/freeze.py b/dfetch/commands/freeze.py index 20e8769ae..2b1e9dd50 100644 --- a/dfetch/commands/freeze.py +++ b/dfetch/commands/freeze.py @@ -100,6 +100,7 @@ def __call__(self, args: argparse.Namespace) -> None: make_backup = isinstance(superproject, NoVcsSuperProject) manifest_updated = False + had_errors = False with in_directory(superproject.root_directory): manifest_path = superproject.manifest.path @@ -134,7 +135,11 @@ def __call__(self, args: argparse.Namespace) -> None: manifest_updated = True except RuntimeError as exc: logger.print_warning_line(project.name, str(exc)) + had_errors = True if manifest_updated: superproject.manifest.dump() logger.info(f"Updated manifest ({manifest_path}) in {os.getcwd()}") + + if had_errors: + raise RuntimeError() From 398675c26a8de2601a739f395c07912c249ba5fd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 16:25:23 +0000 Subject: [PATCH 09/15] Fix glob_within_root root comparison and narrow git fetch permission Resolve root once before the loop in glob_within_root so symlinked or relative root paths compare correctly against resolved match paths. Narrow the Claude Code permission from the wildcard "git fetch:*" to "git fetch origin:*", restricting fetches to the origin remote only. https://claude.ai/code/session_01RTgPqrX37jFkK843dq65Bu --- .claude/settings.json | 2 +- dfetch/util/util.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 9b35fe923..b5c02acfa 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,7 +5,7 @@ "Bash(bandit -r dfetch)", "Bash(black --check dfetch)", "Bash(doc8 doc:*)", - "Bash(git fetch:*)", + "Bash(git fetch origin:*)", "Bash(git stash:*)", "Bash(git update-index --refresh)", "Bash(isort --diff dfetch)", diff --git a/dfetch/util/util.py b/dfetch/util/util.py index 4954a83fe..51c7bb863 100644 --- a/dfetch/util/util.py +++ b/dfetch/util/util.py @@ -364,10 +364,11 @@ def glob_within_root(pattern: str, root: Path) -> tuple[list[str], list[str]]: A ``(safe, escaped)`` tuple where *safe* contains sorted paths that resolve inside *root* and *escaped* contains those that do not. """ + root_resolved = root.resolve() safe: list[str] = [] escaped: list[str] = [] for p in sorted(glob.glob(pattern)): - (safe if Path(p).resolve().is_relative_to(root) else escaped).append(p) + (safe if Path(p).resolve().is_relative_to(root_resolved) else escaped).append(p) return safe, escaped From 4c6c79dc047e65518ff7ec705f1d430055416545 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 14 Jun 2026 14:52:37 +0000 Subject: [PATCH 10/15] Print errors instead of warnings --- dfetch/commands/check.py | 2 +- dfetch/commands/diff.py | 2 +- dfetch/commands/format_patch.py | 2 +- dfetch/commands/freeze.py | 2 +- dfetch/commands/update.py | 2 +- dfetch/commands/update_patch.py | 2 +- dfetch/log.py | 10 ++++++++++ 7 files changed, 16 insertions(+), 6 deletions(-) diff --git a/dfetch/commands/check.py b/dfetch/commands/check.py index b01f3d2aa..73a5549de 100644 --- a/dfetch/commands/check.py +++ b/dfetch/commands/check.py @@ -120,7 +120,7 @@ def __call__(self, args: argparse.Namespace) -> None: with in_directory(project.destination): check_sub_manifests(superproject.manifest, project) except RuntimeError as exc: - logger.print_warning_line(project.name, str(exc)) + logger.print_error_line(project.name, str(exc)) had_errors = True for reporter in reporters: diff --git a/dfetch/commands/diff.py b/dfetch/commands/diff.py index 27cb3b71e..e2e9a92d2 100644 --- a/dfetch/commands/diff.py +++ b/dfetch/commands/diff.py @@ -126,7 +126,7 @@ def __call__(self, args: argparse.Namespace) -> None: try: self._diff_project(superproject, project, old_rev, new_rev) except RuntimeError as exc: - logger.print_warning_line(project.name, str(exc)) + logger.print_error_line(project.name, str(exc)) had_errors = True if had_errors: diff --git a/dfetch/commands/format_patch.py b/dfetch/commands/format_patch.py index 371ba9153..ac7c9c113 100644 --- a/dfetch/commands/format_patch.py +++ b/dfetch/commands/format_patch.py @@ -139,7 +139,7 @@ def __call__(self, args: argparse.Namespace) -> None: f"formatted patch written to {output_patch_file.relative_to(os.getcwd())}", ) except RuntimeError as exc: - logger.print_warning_line(project.name, str(exc)) + logger.print_error_line(project.name, str(exc)) had_errors = True if had_errors: diff --git a/dfetch/commands/freeze.py b/dfetch/commands/freeze.py index 2b1e9dd50..515e2cbc1 100644 --- a/dfetch/commands/freeze.py +++ b/dfetch/commands/freeze.py @@ -134,7 +134,7 @@ def __call__(self, args: argparse.Namespace) -> None: superproject.manifest.update_project_version(project) manifest_updated = True except RuntimeError as exc: - logger.print_warning_line(project.name, str(exc)) + logger.print_error_line(project.name, str(exc)) had_errors = True if manifest_updated: diff --git a/dfetch/commands/update.py b/dfetch/commands/update.py index 6028d6112..6a6b010cf 100644 --- a/dfetch/commands/update.py +++ b/dfetch/commands/update.py @@ -108,7 +108,7 @@ def _ignored(dst: str = destination) -> list[str]: with in_directory(project.destination): check_sub_manifests(superproject.manifest, project) except RuntimeError as exc: - logger.print_warning_line(project.name, str(exc)) + logger.print_error_line(project.name, str(exc)) had_errors = True if had_errors: diff --git a/dfetch/commands/update_patch.py b/dfetch/commands/update_patch.py index a7d3e4680..15dd28185 100644 --- a/dfetch/commands/update_patch.py +++ b/dfetch/commands/update_patch.py @@ -90,7 +90,7 @@ def __call__(self, args: argparse.Namespace) -> None: try: self._process_project(superproject, project) except RuntimeError as exc: - logger.print_warning_line(project.name, str(exc)) + logger.print_error_line(project.name, str(exc)) had_errors = True if had_errors: diff --git a/dfetch/log.py b/dfetch/log.py index 64862608c..f66422a53 100644 --- a/dfetch/log.py +++ b/dfetch/log.py @@ -116,6 +116,16 @@ def print_warning_line(self, name: str, info: str) -> None: line = markup_escape(info).replace("\n", "\n ") self.info(f" [bold bright_yellow]> {line}[/bold bright_yellow]") + def print_error_line(self, name: str, info: str) -> None: + """Print an error line: green name, red value.""" + if name not in DLogger._printed_projects: + safe_name = markup_escape(name) + self.info(f" [bold][bright_green]{safe_name}:[/bright_green][/bold]") + DLogger._printed_projects.add(name) + + line = markup_escape(info).replace("\n", "\n ") + self.info(f" [bold bright_red]> {line}[/bold bright_red]") + def print_overview(self, name: str, title: str, info: dict[str, Any]) -> None: """Print an overview of fields.""" self.print_info_line(name, title) From 9552fb2e422c3e7934a751f2353267287cb42e84 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 14 Jun 2026 15:00:00 +0000 Subject: [PATCH 11/15] Fix fuzz test --- tests/test_fuzzing.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_fuzzing.py b/tests/test_fuzzing.py index ce4bd4519..40796094a 100644 --- a/tests/test_fuzzing.py +++ b/tests/test_fuzzing.py @@ -53,14 +53,15 @@ max_size=64, ) -# VERSION = Int() | Float() | Str(): generate unquoted ints, unquoted floats, -# and arbitrary safe strings to cover all three schema branches. -# Empty strings are excluded: Float()'s validator crashes on them instead of -# raising YAMLValidationError, so the OrValidator cannot fall through to Str(). +# VERSION = Int() | Float() | Str(): generate unquoted ints and floats. +# Arbitrary strings cannot be tested here: strictyaml's Float validator calls +# float() directly and raises ValueError (not YAMLValidationError) for any +# non-float string, so the OrValidator cannot fall through to Str(). +# The Str() branch is exercised implicitly by test_manifest_can_be_created +# which uses yaml.dump (preserving YAML quoting) rather than as_document. SAFE_VERSION = st.one_of( st.integers(), st.floats(allow_nan=False, allow_infinity=False), - SAFE_TEXT.filter(lambda s: s != ""), ) From 615c647af62806ba7d1210338563aa6299ae3af4 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 14 Jun 2026 14:28:53 +0000 Subject: [PATCH 12/15] Improve logging --- dfetch/log.py | 12 +++++++++++- dfetch/project/svnsubproject.py | 1 + dfetch/vcs/svn.py | 5 +++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/dfetch/log.py b/dfetch/log.py index f66422a53..d9ae19981 100644 --- a/dfetch/log.py +++ b/dfetch/log.py @@ -86,6 +86,7 @@ class DLogger(logging.Logger): """Logging class extended with specific log items for dfetch.""" _printed_projects: set[str] = set() + _active_status: Status | None = None def print_report_line(self, name: str, info: str) -> None: """Print a line for a report.""" @@ -219,11 +220,20 @@ def status( self.info(f" [bold][bright_green]{safe_name}:[/bright_green][/bold]") DLogger._printed_projects.add(name) - return Status( + active = Status( f"[bold bright_blue]> {markup_escape(message)}[/bold bright_blue]", spinner=spinner, console=rich_console, ) + DLogger._active_status = active + return active + + def update_status(self, message: str) -> None: + """Update the text of the currently active spinner, if any.""" + if DLogger._active_status is not None: + DLogger._active_status.update( + f"[bold bright_blue]> {markup_escape(message)}[/bold bright_blue]" + ) @classmethod def reset_projects(cls) -> None: diff --git a/dfetch/project/svnsubproject.py b/dfetch/project/svnsubproject.py index e7e1bf488..eafbe194f 100644 --- a/dfetch/project/svnsubproject.py +++ b/dfetch/project/svnsubproject.py @@ -167,6 +167,7 @@ def _fetch_impl( def _fetch_externals(self, complete_path: str, revision: str) -> list[Dependency]: """Detect and log SVN externals that were exported with the project.""" + logger.update_status("Indexing externals") vcs_deps = [] for external in SvnRepo.externals_from_url(complete_path, revision): path_display = "./" + external.path.lstrip("./") diff --git a/dfetch/vcs/svn.py b/dfetch/vcs/svn.py index de7deed16..a8be2972d 100644 --- a/dfetch/vcs/svn.py +++ b/dfetch/vcs/svn.py @@ -317,6 +317,11 @@ def externals(self) -> list[External]: def externals_from_url(url: str, revision: str = "") -> list[External]: """Get list of externals from a remote SVN URL.""" extra = ["--revision", revision] if revision else [] + rev_suffix = f"@{revision}" if revision else "" + logger.debug( + f"Scanning '{url}{rev_suffix}' recursively for svn:externals " + f"(may be slow for large repositories)" + ) output = _run_svn(["propget", "svn:externals", "-R"] + extra + [url], url=url) repo_root = SvnRepo.get_info_from_target(url)["Repository Root"] normalized = SvnRepo._normalize_url_prefix(output, url) From eaaf2b31f327b028881317674120af770aa6662c Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 14 Jun 2026 14:37:13 +0000 Subject: [PATCH 13/15] Log new version check --- dfetch/commands/check.py | 1 + dfetch/commands/environment.py | 1 + 2 files changed, 2 insertions(+) diff --git a/dfetch/commands/check.py b/dfetch/commands/check.py index 73a5549de..eb177f855 100644 --- a/dfetch/commands/check.py +++ b/dfetch/commands/check.py @@ -100,6 +100,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, args: argparse.Namespace) -> None: """Perform the check.""" if not os.environ.get("CI"): + logger.debug("Checking for a newer dfetch version") newer = newer_version_available() if newer: logger.print_newer_version_notice(newer) diff --git a/dfetch/commands/environment.py b/dfetch/commands/environment.py index d81fea00d..84c9753b7 100644 --- a/dfetch/commands/environment.py +++ b/dfetch/commands/environment.py @@ -37,6 +37,7 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None def __call__(self, _: argparse.Namespace) -> None: """Perform listing the environment.""" logger.print_report_line("dfetch", __version__) + logger.debug("Checking for a newer dfetch version") newer = newer_version_available() if newer: logger.print_newer_version_notice(newer) From 2fa5016ab455d298b79538357133be1a0e71e3f3 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 14 Jun 2026 17:29:58 +0000 Subject: [PATCH 14/15] Fuzz test again --- tests/test_fuzzing.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_fuzzing.py b/tests/test_fuzzing.py index 40796094a..777e9e1db 100644 --- a/tests/test_fuzzing.py +++ b/tests/test_fuzzing.py @@ -53,15 +53,18 @@ max_size=64, ) -# VERSION = Int() | Float() | Str(): generate unquoted ints and floats. -# Arbitrary strings cannot be tested here: strictyaml's Float validator calls -# float() directly and raises ValueError (not YAMLValidationError) for any -# non-float string, so the OrValidator cannot fall through to Str(). -# The Str() branch is exercised implicitly by test_manifest_can_be_created -# which uses yaml.dump (preserving YAML quoting) rather than as_document. +# VERSION = Int() | Float() | Str(): generate unquoted ints, unquoted floats, +# and float-like strings. Arbitrary strings cannot be used: strictyaml's Float +# validator calls float() directly and raises ValueError (not +# YAMLValidationError) for non-float strings, so the OrValidator cannot fall +# through to Str(). Float-like strings are safe: as_document serializes them +# via Float() (float("1.5") succeeds), while yaml.dump quotes them as YAML +# string scalars ('1.5'), exercising the Str() branch during parsing in +# test_manifest_can_be_created, test_check, and test_update. SAFE_VERSION = st.one_of( st.integers(), st.floats(allow_nan=False, allow_infinity=False), + st.floats(allow_nan=False, allow_infinity=False).map(str), ) From c80559ee041023325f7a741b5681da216adb4e3b Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 14 Jun 2026 17:42:46 +0000 Subject: [PATCH 15/15] Fix node reuse across multiple ScenarioAppendixPlaceholders Docutils nodes can only have one parent. Creating a single note (HTML) or a single appendix-nodes list (PDF) and inserting it into multiple placeholders caused earlier placeholders to silently lose their node once it was re-parented. Move node creation inside the placeholder loop so each replacement receives its own fresh node tree. Co-Authored-By: Claude Sonnet 4.6 --- doc/_ext/scenario_directive.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/doc/_ext/scenario_directive.py b/doc/_ext/scenario_directive.py index e04bc02dd..deffeb5dc 100644 --- a/doc/_ext/scenario_directive.py +++ b/doc/_ext/scenario_directive.py @@ -495,23 +495,22 @@ def process_scenario_appendix(app, doctree: nodes.document, _fromdocname: str) - return if app.builder.name not in ("latex", "rinoh"): - note = nodes.note() - para = nodes.paragraph() - para += nodes.Text( - "In the HTML edition, feature examples appear as expandable " - "blocks directly within each guide section. " - "In the PDF edition they are collected here, grouped by command." - ) - note += para for placeholder in placeholders: + note = nodes.note() + para = nodes.paragraph() + para += nodes.Text( + "In the HTML edition, feature examples appear as expandable " + "blocks directly within each guide section. " + "In the PDF edition they are collected here, grouped by command." + ) + note += para placeholder.replace_self([note]) return entries = getattr(app.env, "scenario_appendix_entries", {}) - appendix_nodes = _build_appendix_nodes(entries) if entries else [] for placeholder in placeholders: - placeholder.replace_self(appendix_nodes) + placeholder.replace_self(_build_appendix_nodes(entries) if entries else []) def purge_scenario_appendix(_app, env, docname: str) -> None: