You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/09/03 11:11:01 UTC
(tooling-trusted-releases) branch arm updated (b9d90193 -> dbd8dce9)
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a change to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
discard b9d90193 #1499 - Empty dirs in file list
add 52dbbc11 Remove CRC-24 checking
add 7aaa110a Split key blocks into certificates more efficiently
add 040e0d43 Prevent resource saturation from KEYS files, but leave big margins
add 680bafc0 Remove code for the entirely unused KEYS import task
add 838f9ffe Improve the error handling for editing metadata
add c118b3ba Allow admins to modify metadata directly
add 1ad82a79 Place the standard limit on KEYS file sizes read from SVN
add 803189cc Use lenient decode when reading KEYS files
new dbd8dce9 #1499 - Empty dirs in file list
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (b9d90193)
\
N -- N -- N refs/heads/arm (dbd8dce9)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
atr/constants.py | 2 +
atr/docs/tasks.md | 2 -
atr/models/args.py | 8 --
atr/pgp.py | 50 ++++++------
atr/post/keys.py | 4 +-
atr/post/projects.py | 130 +++++++++++++++++---------------
atr/shared/keys.py | 3 +-
atr/storage/datatypes.py | 1 -
atr/storage/writers/keys.py | 105 +++++++++++++++-----------
atr/svn/keys_reflect.py | 6 +-
atr/tasks/__init__.py | 30 +-------
atr/tasks/keys.py | 16 ----
tests/unit/test_keys_post.py | 22 ++++++
tests/unit/test_keys_reflect.py | 35 +++++++++
tests/unit/test_keys_writer.py | 60 ++++++++++++---
tests/unit/test_pgp.py | 37 +++++++++
tests/unit/test_projects_post_access.py | 76 +++++++++++++++++++
17 files changed, 385 insertions(+), 202 deletions(-)
create mode 100644 tests/unit/test_projects_post_access.py
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org
(tooling-trusted-releases) 01/01: #1499 - Empty dirs in file list
Posted by ar...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
alitheg pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit dbd8dce940e4067933b9f8170baab7946014f36a
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Fri Aug 14 14:40:11 2026 +0100
#1499 - Empty dirs in file list
---
atr/classify.py | 1 +
atr/get/compose.py | 17 +++++++-----
atr/post/draft.py | 14 +++++-----
atr/storage/readers/releases.py | 15 ++++++-----
atr/storage/writers/release.py | 23 +++++++++++++---
atr/templates/check-selected-path-table.html | 40 +++++++++++++++++++---------
atr/util.py | 15 ++++++++---
7 files changed, 88 insertions(+), 37 deletions(-)
diff --git a/atr/classify.py b/atr/classify.py
index fae6c339..7e94ba9e 100644
--- a/atr/classify.py
+++ b/atr/classify.py
@@ -126,6 +126,7 @@ _SBOM_XML_RE: Final[re.Pattern[bytes]] = re.compile(rb"http://cyclonedx\.org/sch
class FileType(enum.Enum):
BINARY = "binary"
+ DIRECTORY = "directory"
DISALLOWED = "disallowed"
DOCS = "docs"
METADATA = "metadata"
diff --git a/atr/get/compose.py b/atr/get/compose.py
index 1879d7ab..ddf2e9b8 100644
--- a/atr/get/compose.py
+++ b/atr/get/compose.py
@@ -78,7 +78,7 @@ async def selected(
# TODO: This takes 180ms for providers
# We could cache it
- all_paths = [path async for path in util.paths_recursive(base_path)]
+ all_paths = [path async for path in util.paths_recursive(base_path, include_empty_dirs=True)]
all_paths.sort()
async with storage.read(session) as read:
@@ -392,6 +392,15 @@ async def _quarantine_alerts(
async def _render_delete_file_form(release: sql.Release, path: pathlib.Path | safe.RelPath) -> htm.Element:
+ # Empty dirs come through as RelDirPath, so the wording drops the metadata bit
+ if isinstance(path, safe.RelDirPath):
+ confirm = "Are you sure you want to delete this directory? This cannot be undone."
+ else:
+ confirm = (
+ "Are you sure you want to delete this file? "
+ "This will also delete any associated metadata files. "
+ "This cannot be undone."
+ )
return await form.render(
model_cls=draft.DeleteFileForm,
action=util.as_url(
@@ -404,11 +413,7 @@ async def _render_delete_file_form(release: sql.Release, path: pathlib.Path | sa
submit_label="Delete",
empty=True,
defaults={"file_path": str(path)},
- confirm=(
- "Are you sure you want to delete this file? "
- "This will also delete any associated metadata files. "
- "This cannot be undone."
- ),
+ confirm=confirm,
)
diff --git a/atr/post/draft.py b/atr/post/draft.py
index fe1324ff..fc1d5ba3 100644
--- a/atr/post/draft.py
+++ b/atr/post/draft.py
@@ -132,15 +132,17 @@ async def delete_file(
try:
async with storage.write(session) as write:
wacp = await write.as_project_committee_participant(project_key)
- metadata_files_deleted = await wacp.release.delete_file(project_key, version_key, path_to_delete)
+ result = await wacp.release.delete_file(project_key, version_key, path_to_delete)
except Exception as e:
- log.exception("Error deleting file:")
- await quart.flash(f"Error deleting file: {e!s}", "error")
+ noun = "directory" if isinstance(rel_path_to_delete, safe.RelDirPath) else "file"
+ log.exception(f"Error deleting {noun}:")
+ await quart.flash(f"Error deleting {noun}: {e!s}", "error")
return await session.redirect(get.compose.selected, project_key=str(project_key), version_key=str(version_key))
- success_message = f"File '{path_to_delete.name}' deleted successfully"
- if metadata_files_deleted:
- success_message += f", and {util.plural(metadata_files_deleted, 'associated metadata file')} deleted"
+ noun = "Directory" if result.was_directory else "File"
+ success_message = f"{noun} '{path_to_delete.name}' deleted successfully"
+ if result.metadata_files_deleted:
+ success_message += f", and {util.plural(result.metadata_files_deleted, 'associated metadata file')} deleted"
return await session.redirect(
get.compose.selected, success=success_message, project_key=str(project_key), version_key=str(version_key)
)
diff --git a/atr/storage/readers/releases.py b/atr/storage/readers/releases.py
index d8c00f36..e885d63d 100644
--- a/atr/storage/readers/releases.py
+++ b/atr/storage/readers/releases.py
@@ -68,13 +68,16 @@ class GeneralPublic:
base_path,
)
for path in all_paths:
- db_value = db_classifications.get(str(path))
- if db_value is not None:
- info.file_types[path] = classify.FileType(db_value)
+ if isinstance(path, safe.RelDirPath):
+ info.file_types[path] = classify.FileType.DIRECTORY
else:
- info.file_types[path] = await classify.classify(
- path, base_path=base_path, source_matcher=source_matcher, binary_matcher=binary_matcher
- )
+ db_value = db_classifications.get(str(path))
+ if db_value is not None:
+ info.file_types[path] = classify.FileType(db_value)
+ else:
+ info.file_types[path] = await classify.classify(
+ path, base_path=base_path, source_matcher=source_matcher, binary_matcher=binary_matcher
+ )
self.__pair_sboms(info, all_paths)
self.__compute_checker_stats(info, all_paths)
return info
diff --git a/atr/storage/writers/release.py b/atr/storage/writers/release.py
index 5920bdff..72f5eefd 100644
--- a/atr/storage/writers/release.py
+++ b/atr/storage/writers/release.py
@@ -98,6 +98,15 @@ class ArtifactInput:
sbom_path: str | None = None
+@dataclasses.dataclass(frozen=True)
+class DeleteFileResult:
+ # What the delete removed - a directory has no companion metadata, so the
+ # count only ever moves for a real file. The handler needs the flag to word
+ # its success message
+ was_directory: bool
+ metadata_files_deleted: int
+
+
async def _archive_release(
data: db.Session,
write_as: storage.WriteAs,
@@ -604,12 +613,13 @@ class CommitteeParticipant(FoundationCommitter):
async def delete_file(
self, project_key: safe.ProjectKey, version: safe.VersionKey, rel_path_to_delete: pathlib.Path
- ) -> int:
+ ) -> DeleteFileResult:
metadata_files_deleted = 0
+ was_directory = False
description = "File deletion through web interface"
async def modify(path: safe.StatePath, _old_rev: sql.Revision | None) -> None:
- nonlocal metadata_files_deleted
+ nonlocal metadata_files_deleted, was_directory
# Uses new_revision_number for logging only
# Path to delete within the new revision directory
path_in_new_revision = path / rel_path_to_delete
@@ -624,6 +634,13 @@ class CommitteeParticipant(FoundationCommitter):
log.error(f"SEVERE ERROR! File {rel_path_to_delete} not found in new revision before deletion")
raise storage.AccessError("File to delete was not found in the new revision", status=500)
+ # An empty directory shown in the file list needs rmdir, not remove
+ # This will *only* succeed on an empty directory
+ if await aiofiles.os.path.isdir(path_in_new_revision):
+ await aiofiles.os.rmdir(path_in_new_revision)
+ was_directory = True
+ return
+
# Check whether the file is an artifact
if analysis.is_artifact(path_in_new_revision.path):
# If so, delete all associated metadata files in the new revision
@@ -645,7 +662,7 @@ class CommitteeParticipant(FoundationCommitter):
description=description,
modify=modify,
)
- return metadata_files_deleted
+ return DeleteFileResult(was_directory=was_directory, metadata_files_deleted=metadata_files_deleted)
async def generate_hash_file(
self, project_key: safe.ProjectKey, version_key: safe.VersionKey, rel_path: pathlib.Path
diff --git a/atr/templates/check-selected-path-table.html b/atr/templates/check-selected-path-table.html
index f61a9131..70562ff0 100644
--- a/atr/templates/check-selected-path-table.html
+++ b/atr/templates/check-selected-path-table.html
@@ -63,6 +63,8 @@
<span class="atr-classification-badge" title="Disallowed file">bad</span>
{% elif file_type == "source" %}
<span class="atr-classification-badge" title="Source artifact">src</span>
+ {% elif file_type == "directory" %}
+ <span class="atr-classification-badge" title="Directory">dir</span>
{% elif file_type == "metadata" %}
<span class="atr-classification-badge" title="Metadata file">meta</span>
{% elif file_type == "sbom" %}
@@ -73,15 +75,23 @@
{% endif %}
</td>
<td class="py-2">
- <a href="{{ as_url(get.file.selected_path, project_key=project_key|string, version_key=version_key|string, file_path=path) }}"
- title="View file {{ path }}"
- class="text-decoration-none text-reset">
+ {% if file_type != "directory" %}
+ <a href="{{ as_url(get.file.selected_path, project_key=project_key|string, version_key=version_key|string, file_path=path) }}"
+ title="View file {{ path }}"
+ class="text-decoration-none text-reset">
+ {% if path_style_class %}
+ <strong><code class="{{ path_style_class }}">{{ path }}</code></strong>
+ {% else %}
+ <code>{{ path }}</code>
+ {% endif %}
+ </a>
+ {% else %}
{% if path_style_class %}
- <strong><code class="{{ path_style_class }}">{{ path }}</code></strong>
+ <strong><code class="{{ path_style_class }}">{{ path }} (empty)</code></strong>
{% else %}
- <code>{{ path }}</code>
+ <code>{{ path }} (empty)</code>
{% endif %}
- </a>
+ {% endif %}
</td>
<td class="py-2 text-center">
{% if has_suggestions %}
@@ -136,7 +146,9 @@
class="btn btn-sm btn-outline-secondary"
title="Show report for {{ path }}">Notes</a>
{% else %}
- <span class="btn btn-sm btn-outline-secondary disabled">No checks</span>
+ {% if file_type != "directory" %}
+ <span class="btn btn-sm btn-outline-secondary disabled">No checks</span>
+ {% endif %}
{% endif %}
{% if phase == "release_candidate_draft" %}
@@ -163,12 +175,14 @@
<div class="d-flex justify-content-end align-items-center gap-2"
role="group"
aria-label="More file actions for {{ path }}">
- <a href="{{ as_url(get.download.path, project_key=release.project.key, version_key=release.version, file_path=path) }}"
- title="Download file {{ path }}"
- class="btn btn-sm btn-outline-secondary">Download</a>
- <a href="{{ as_url(get.draft.tools, project_key=release.project.key, version_key=release.version, file_path=path) }}"
- title="Tools for file {{ path }}"
- class="btn btn-sm btn-outline-secondary">Tools</a>
+ {% if file_type != "directory" %}
+ <a href="{{ as_url(get.download.path, project_key=release.project.key, version_key=release.version, file_path=path) }}"
+ title="Download file {{ path }}"
+ class="btn btn-sm btn-outline-secondary">Download</a>
+ <a href="{{ as_url(get.draft.tools, project_key=release.project.key, version_key=release.version, file_path=path) }}"
+ title="Tools for file {{ path }}"
+ class="btn btn-sm btn-outline-secondary">Tools</a>
+ {% endif %}
{{ delete_file_forms[path|string] }}
</div>
</div>
diff --git a/atr/util.py b/atr/util.py
index 762eae4d..b17f9938 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -1175,16 +1175,25 @@ def parse_npm_pack_info(raw: bytes, filename_basename: str | None = None) -> tup
return NpmPackInfo(name=name, version=version, filename_match=filename_match), None
-async def paths_recursive(base_path: pathlib.Path | safe.StatePath) -> AsyncGenerator[safe.RelPath]:
+async def paths_recursive(
+ base_path: pathlib.Path | safe.StatePath, include_empty_dirs: bool = False
+) -> AsyncGenerator[safe.RelPath]:
"""Yield all file paths recursively within a base path, relative to the base path."""
if (resolved_base_path := await is_dir_resolve(base_path)) is None:
return
async for rel_path in paths_recursive_all(base_path):
abs_path_to_check = resolved_base_path / rel_path
with contextlib.suppress(FileNotFoundError, OSError):
- if await aiofiles.os.path.isfile(abs_path_to_check):
+ is_file = await aiofiles.os.path.isfile(abs_path_to_check)
+ # An empty dir has no files to imply it, so we keep it to preserve the tree shape
+ is_empty_dir = (
+ include_empty_dirs
+ and await aiofiles.os.path.isdir(abs_path_to_check)
+ and not await aiofiles.os.listdir(abs_path_to_check)
+ )
+ if is_file or is_empty_dir:
try:
- yield safe.RelPath.from_path(rel_path)
+ yield safe.RelDirPath.from_path(rel_path) if is_empty_dir else safe.RelPath.from_path(rel_path)
except ValueError as err:
msg = f"Unsafe relative path {str(rel_path)!r}: {err}"
raise ValueError(msg) from err
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org