diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e7ceab6a2..f2000e640 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,7 @@ Release 0.15.0 (unreleased) ============================== * Add ``replay-patches`` command to step through patch contributions interactively (#1290) +* Fix a root-level directory matching a license glob being vendored alongside ``src:`` (#1428) Release 0.14.4 (released 2026-08-28) ==================================== diff --git a/dfetch/vcs/git.py b/dfetch/vcs/git.py index a2e42dce5..edd675435 100644 --- a/dfetch/vcs/git.py +++ b/dfetch/vcs/git.py @@ -1,6 +1,7 @@ """Git specific implementation.""" import contextlib +import fnmatch import functools import os import re @@ -12,6 +13,7 @@ from dfetch.log import get_logger from dfetch.util.cmdline import SubprocessCommandError, run_on_cmdline +from dfetch.util.license import is_license_file from dfetch.util.ssh import InvalidSshCommandError, sanitize_ssh_cmd from dfetch.util.util import in_directory, safe_rm from dfetch.vcs import git_submodule @@ -487,6 +489,30 @@ def _configure_sparse_checkout( f.write("\n".join(map(str, patterns)) + "\n") + @staticmethod + def _drop_directories_matching_license_globs(src: str | None) -> None: + """Undo the license keep patterns matching root-level directories. + + The sparse-checkout keep patterns for license files (see + ``LICENSE_GLOBS``) are written without a trailing slash so plain + ``fnmatch`` can also recognise them elsewhere; in a sparse-checkout + pattern file that same lack of a trailing slash makes them match + directories too, e.g. a root-level ``licensecore/`` folder ends up + vendored whole (#1428). Only files should ever be kept as license + files, so any root-level directory that merely shares the name is + removed again here, unless it is the directory requested via + ``src:`` itself -- whose leading path component may itself be a + glob (e.g. ``licen*/``), so it is matched with ``fnmatch`` too. + """ + keep_root = Path(src).parts[0] if src else None + for entry in Path(".").iterdir(): + if ( + entry.is_dir() + and is_license_file(entry.name) + and not (keep_root and fnmatch.fnmatch(entry.name, keep_root)) + ): + safe_rm(entry, within=".") + def checkout_version( self, options: CheckoutOptions, @@ -516,6 +542,9 @@ def checkout_version( ) run_on_cmdline(logger, ["git", "reset", "--hard", "FETCH_HEAD"]) + if options.src or options.ignore: + self._drop_directories_matching_license_globs(options.src) + if options.eol is not None: self._renormalize_eol() diff --git a/features/keep-license-in-project.feature b/features/keep-license-in-project.feature index 2e993236b..66ac3a0f1 100644 --- a/features/keep-license-in-project.feature +++ b/features/keep-license-in-project.feature @@ -36,6 +36,59 @@ Feature: Keep license in project dfetch.yaml """ + Scenario: Root-level folder matching a license glob is not vendored alongside 'src:' + Given the manifest 'dfetch.yaml' in MyProject + """ + manifest: + version: 0.0 + projects: + - name: SomeProjectWithLicenseLikeFolder + url: some-remote-server/SomeProjectWithLicenseLikeFolder.git + src: SomeFolder/ + tag: v1 + """ + And a git-repository "SomeProjectWithLicenseLikeFolder.git" with the files + | path | + | LICENSE | + | licensecore/SomeUnrelatedFile.txt | + | SomeFolder/SomeFile.txt | + When I run "dfetch update" + Then 'MyProject' looks like: + """ + MyProject/ + SomeProjectWithLicenseLikeFolder/ + .dfetch_data.yaml + LICENSE + SomeFile.txt + dfetch.yaml + """ + + Scenario: A 'src:' glob whose root component looks like a license file is kept + Given the manifest 'dfetch.yaml' in MyProject + """ + manifest: + version: 0.0 + projects: + - name: SomeProjectWithLicenseLikeSrc + url: some-remote-server/SomeProjectWithLicenseLikeSrc.git + src: licen*/ + tag: v1 + """ + And a git-repository "SomeProjectWithLicenseLikeSrc.git" with the files + | path | + | LICENSE | + | licensecore/SomeFile.txt | + When I run "dfetch update" + Then 'MyProject' looks like: + """ + MyProject/ + SomeProjectWithLicenseLikeSrc/ + .dfetch_data.yaml + LICENSE + SomeFile.txt + dfetch.yaml + """ + Scenario: License is preserved in svn repo sparse checkout and cannot be ignored Given the manifest 'dfetch.yaml' in MyProject """