Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 8 additions & 16 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
Release 0.15.0 (unreleased)
Release 0.14.1 (unreleased)
============================

* Implement C-043: add ``pip-audit`` OSV gate to the release workflow; publishing is blocked if
any known vulnerability is found in dfetch's runtime dependencies
* Add CRA Compliance Track B: OSCAL 1.2.2 Component Definition mapping all CRA Annex I Part I
essential requirements (ECR-a–m) through prEN 40000-1-4 Security Objectives to dfetch controls;
covers Part II via prEN 40000-1-3; introduces controls C-043 (release-gate CVE check), C-044
(data minimisation policy), and C-046 (exploit mitigation inventory)
* Upgrade OSCAL artifacts to 1.2.2: both the prEN 40000-1-4 catalog and the Component Definition
now declare ``oscal-version: 1.2.2``; the catalog gains ``parties``, ``roles``, and
``responsible-parties`` in its metadata; the Component Definition adds a ``purpose`` field on
the component, a ``supplier`` party, ``document-ids`` for stable cross-referencing,
``responsible-roles``, and ``evidence`` links on every implemented-requirement pointing to the
concrete code or CI workflow file that implements the control — turning the compliance mapping
into a machine-verifiable security-as-code artifact; the back-matter is enriched with references
to the OpenSSF Scorecard, SLSA Source Provenance, Sigstore attestation, in-toto test results,
CodeQL, and dependency-review workflows
* Implement C-043: add ``pip-audit`` OSV gate to the release workflow
* Add CRA Compliance: OSCAL 1.2.2 Component Definition
* Fix ``dfetch import`` mangling the namespace of a generic VCS URL whose path contains ``.git`` (#1268)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
* Fix ``Version`` comparison raising ``AttributeError`` when compared against a non-``Version`` object (#1268)
* Fix ``dfetch add`` matching remote whose base URL is only string (not path) prefix of the project URL (#1268)
* Fix git ref resolution spuriously matching the first reference for an empty revision (#1268)
* Strip the trailing newline from the git origin URL returned by ``get_remote_url`` (#1268)

Release 0.14.0 (released 2026-06-14)
====================================
Expand Down
2 changes: 1 addition & 1 deletion dfetch/manifest/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ def find_remote_for_url(self, remote_url: str) -> Remote | None:
target = remote_url.rstrip("/")
for remote in self.remotes:
remote_base = remote.url.rstrip("/")
if target.startswith(remote_base):
if target == remote_base or target.startswith(remote_base + "/"):
return remote
return None

Expand Down
8 changes: 7 additions & 1 deletion dfetch/manifest/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@ class Version(NamedTuple):

def __eq__(self, other: Any) -> bool:
"""Check if two versions can be considered as equal."""
if not other:
if not isinstance(other, Version):
return False

if self.tag or other.tag:
return bool(self.tag == other.tag)

return bool(self.branch == other.branch and self.revision == other.revision)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def __hash__(self) -> int:
"""Hash only fields that determine equality."""
if self.tag:
return hash(self.tag)
return hash((self.branch, self.revision))

@property
def field(self) -> tuple[str, str]:
"""Return ``(kind, value)`` for the active field: tag, revision, or branch."""
Expand Down
2 changes: 1 addition & 1 deletion dfetch/util/purl.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def _vcs_namespace_and_name(remote_url: str) -> tuple[str, str, str]:
remote_url = f"ssh://{parsed.path.replace(':', '/')}"
else:
namespace, name = _namespace_and_name_from_domain_and_path(
remote_url, path.replace(".git", "")
remote_url, path.removesuffix(".git")
)
return namespace, name, remote_url

Expand Down
4 changes: 3 additions & 1 deletion dfetch/vcs/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ def _find_branch_tip_or_tag_from_sha(
) -> tuple[str, str]:
"""Check all branch tips and tags and see if the sha is one of them."""
branch, tag = "", ""
if not rev:
return (branch, tag)
for reference, sha in info.items():
if sha.startswith(rev): # Also allow for shorter SHA's
if reference.startswith("refs/tags/"):
Expand Down Expand Up @@ -680,7 +682,7 @@ def get_remote_url() -> str:
"""Get the url of the remote origin."""
try:
result = run_on_cmdline(logger, ["git", "remote", "get-url", "origin"])
decoded_result = str(result.stdout.decode())
decoded_result = str(result.stdout.decode()).strip()
except SubprocessCommandError:
decoded_result = ""

Expand Down
16 changes: 16 additions & 0 deletions tests/test_git_vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,22 @@ def test_ls_remote():
assert info == expected


def test_get_remote_url_strips_trailing_newline():
"""git remote get-url appends a newline that must not leak into the URL."""
with patch("dfetch.vcs.git.run_on_cmdline") as run_on_cmdline_mock:
run_on_cmdline_mock.return_value.stdout = b"https://github.com/org/repo.git\n"
assert GitLocalRepo.get_remote_url() == "https://github.com/org/repo.git"


def test_find_branch_tip_or_tag_from_sha_empty_rev_matches_nothing():
"""An empty revision must not spuriously match the first reference."""
info = {
"refs/heads/master": "33d11e10699bae03ba2a58a280e92494f4fa0d82",
"refs/tags/v1.0": "0e3b216c7ab365b67765e94aeb45085c4db029e0",
}
assert GitRemote._find_branch_tip_or_tag_from_sha(info, "") == ("", "")


@pytest.mark.parametrize(
"name, env_ssh, git_config_ssh, expected",
[
Expand Down
34 changes: 33 additions & 1 deletion tests/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import os
from typing import cast
from unittest.mock import mock_open, patch
from unittest.mock import Mock, mock_open, patch

import pytest

Expand Down Expand Up @@ -686,3 +686,35 @@ def test_builder_blank_line_between_remotes() -> None:
.build()
)
assert "\n\n - name: gitlab" in manifest._doc.as_yaml()


# ---------------------------------------------------------------------------
# Manifest.find_remote_for_url – path-boundary checks
# ---------------------------------------------------------------------------


def _make_remote(name: str, url: str) -> Mock:
r = Mock(spec=Remote)
r.name = name
r.url = url
return r


def test_determine_remote_requires_path_boundary():
"""An org-scoped remote must not match a different org sharing its prefix."""
m = Mock()
m.remotes = [_make_remote("myorg", "https://github.com/myorg")]
result = Manifest.find_remote_for_url(
m, "https://github.com/myorg-private/repo.git"
)
assert result is None


def test_determine_remote_matches_exact_and_subpath():
"""The boundary check still matches the remote itself and any URL beneath it."""
m = Mock()
m.remotes = [_make_remote("myorg", "https://github.com/myorg")]
assert Manifest.find_remote_for_url(m, "https://github.com/myorg") is not None
assert (
Manifest.find_remote_for_url(m, "https://github.com/myorg/repo.git") is not None
)
22 changes: 22 additions & 0 deletions tests/test_project_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,25 @@
def test_version_comparison(name, version_1, version_2, expected_equality):
actual_equality = version_1 == version_2
assert actual_equality == expected_equality


@pytest.mark.parametrize(
"other",
[
("", "main", "123"), # a plain tuple
"main", # a string
42, # an int
],
)
def test_version_comparison_with_non_version(other):
"""Comparing a Version with a non-Version must not crash."""
assert (Version(branch="main", revision="123") == other) is False


def test_version_remains_hashable():
"""Defining __eq__ must not break hashing/set membership."""
v1 = Version(tag="1.0")
v2 = Version(tag="1.0")
assert v1 == v2
assert hash(v1) == hash(v2)
assert v2 in {v1}
5 changes: 5 additions & 0 deletions tests/test_purl.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@
"git://git.git.savannah.gnu.org/automake.git",
"pkg:generic/automake?vcs_url=git://git.git.savannah.gnu.org/automake.git",
),
# A ".git" substring inside the path must not be stripped (only the suffix)
(
"https://gitlab.com/group/foo.github/project.git",
"pkg:generic/group/foo.github/project?vcs_url=https://gitlab.com/group/foo.github/project.git",
),
# Trailing slash – issue #1137
("https://github.com/cpputest/cpputest/", "pkg:github/cpputest/cpputest"),
("https://github.com/dfetch-org/dfetch/", "pkg:github/dfetch-org/dfetch"),
Expand Down
Loading