This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new a1eaef0  Allow users to set any revision as the latest
a1eaef0 is described below

commit a1eaef046af6b53c47abfe7aa186a7b06949a438
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Apr 9 17:12:48 2025 +0100

    Allow users to set any revision as the latest
---
 atr/revision.py                    | 11 +++++----
 atr/routes/draft.py                | 48 +++++++++++++++++++++++++++++++++++++-
 atr/templates/draft-evaluate.html  |  5 +++-
 atr/templates/draft-revisions.html | 20 ++++++++++++----
 4 files changed, 72 insertions(+), 12 deletions(-)

diff --git a/atr/revision.py b/atr/revision.py
index 6a58875..d3b1b66 100644
--- a/atr/revision.py
+++ b/atr/revision.py
@@ -109,8 +109,9 @@ async def create_and_manage(
                     await aioshutil.rmtree(new_revision_dir)  # type: 
ignore[call-arg]
 
 
-async def latest_info(project_name: str, version_name: str) -> tuple[str | 
None, datetime.datetime | None]:
-    """Get the editor and timestamp of the latest revision from the 
filesystem."""
+async def latest_info(project_name: str, version_name: str) -> tuple[str | 
None, str | None, datetime.datetime | None]:
+    """Get the name, editor, and timestamp of the latest revision."""
+    revision_name_from_link: str | None = None
     editor: str | None = None
     timestamp: datetime.datetime | None = None
 
@@ -120,14 +121,14 @@ async def latest_info(project_name: str, version_name: 
str) -> tuple[str | None,
         latest_symlink_path = release_dir / "latest"
 
         if await aiofiles.os.path.islink(latest_symlink_path):
-            revision_name = await 
aiofiles.os.readlink(str(latest_symlink_path))
-            parts = revision_name.split("@", 1)
+            revision_name_from_link = await 
aiofiles.os.readlink(str(latest_symlink_path))
+            parts = revision_name_from_link.split("@", 1)
             if len(parts) == 2:
                 editor = parts[0]
                 dt_obj = datetime.datetime.strptime(parts[1][:-1], 
"%Y-%m-%dT%H.%M.%S.%f")
                 timestamp = dt_obj.replace(tzinfo=datetime.UTC)
 
-    return editor, timestamp
+    return revision_name_from_link, editor, timestamp
 
 
 async def _manage_draft_revision_find_parent(
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index 375d566..57a4695 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -413,7 +413,7 @@ async def evaluate(session: routes.CommitterSession, 
project_name: str, version_
     #         if (latest_check_result is None) or (check_result.created > 
latest_check_result):
     #             latest_check_result = check_result.created
 
-    revision_editor, revision_time = await revision.latest_info(project_name, 
version_name)
+    revision_name_from_link, revision_editor, revision_time = await 
revision.latest_info(project_name, version_name)
 
     delete_file_form = await DeleteFileForm.create_form()
     return await quart.render_template(
@@ -438,6 +438,7 @@ async def evaluate(session: routes.CommitterSession, 
project_name: str, version_
         delete_file_form=delete_file_form,
         revision_editor=revision_editor,
         revision_time=revision_time,
+        revision_name_from_link=revision_name_from_link,
     )
 
 
@@ -614,6 +615,45 @@ async def promote(session: routes.CommitterSession) -> str 
| response.Response:
     )
 
 
[email protected]("/draft/revision/set/<project_name>/<version_name>", 
methods=["POST"])
+async def revision_set(session: routes.CommitterSession, project_name: str, 
version_name: str) -> response.Response:
+    """Set a specific revision as the latest for a candidate draft."""
+    if not any((p.name == project_name) for p in (await 
session.user_projects)):
+        raise base.ASFQuartException("You do not have access to this project", 
errorcode=403)
+
+    form_data = await quart.request.form
+    revision_name = form_data.get("revision_name")
+    if not revision_name:
+        raise base.ASFQuartException("Missing revision name", errorcode=400)
+
+    release_dir = util.get_release_candidate_draft_dir() / project_name / 
version_name
+    target_revision_dir = release_dir / revision_name
+    latest_symlink_path = release_dir / "latest"
+
+    # Check that the target revision directory exists
+    if not await aiofiles.os.path.isdir(target_revision_dir):
+        raise base.ASFQuartException("Target revision directory not found", 
errorcode=404)
+
+    try:
+        # Target must be relative for the symlink
+        await util.update_atomic_symlink(latest_symlink_path, revision_name)
+    except Exception as e:
+        logging.exception("Error updating latest symlink:")
+        return await session.redirect(
+            revisions,
+            error=f"Failed to set revision {revision_name} as latest: {e!s}",
+            project_name=project_name,
+            version_name=version_name,
+        )
+
+    return await session.redirect(
+        revisions,
+        success=f"Revision {revision_name} set as latest",
+        project_name=project_name,
+        version_name=version_name,
+    )
+
+
 @routes.committer("/draft/revisions/<project_name>/<version_name>")
 async def revisions(session: routes.CommitterSession, project_name: str, 
version_name: str) -> str:
     """Show the revision history for a release candidate draft."""
@@ -657,6 +697,11 @@ async def revisions(session: routes.CommitterSession, 
project_name: str, version
         parent_links_result = await data.execute(query)
         parent_map = {link.key: link.value for link in 
parent_links_result.scalars().all()}
 
+        # Determine the current latest revision
+        latest_revision_name: str | None = None
+        with contextlib.suppress(FileNotFoundError, OSError):
+            latest_revision_name = await aiofiles.os.readlink(str(release_dir 
/ "latest"))
+
         revision_history = []
         prev_revision_files: set[pathlib.Path] | None = None
         prev_revision_name: str | None = None
@@ -681,6 +726,7 @@ async def revisions(session: routes.CommitterSession, 
project_name: str, version
         version_name=version_name,
         release=release,
         revision_history=list(reversed(revision_history)),
+        latest_revision_name=latest_revision_name,
     )
 
 
diff --git a/atr/templates/draft-evaluate.html 
b/atr/templates/draft-evaluate.html
index c392491..6ac89d9 100644
--- a/atr/templates/draft-evaluate.html
+++ b/atr/templates/draft-evaluate.html
@@ -50,7 +50,10 @@
           </p>
           {% if revision_time %}
             <p>
-              <strong>Revision:</strong> {{ revision_time.strftime("%Y-%m-%d 
%H:%M:%S") }}
+              <strong>Revision:</strong>
+              <a href="{{ as_url(routes.draft.revisions, 
project_name=project_name, version_name=version_name) }}#{{ 
revision_name_from_link }}">
+                {{ revision_time.strftime("%Y-%m-%d %H:%M:%S") }}
+              </a>
             </p>
           {% endif %}
         </div>
diff --git a/atr/templates/draft-revisions.html 
b/atr/templates/draft-revisions.html
index 71c2145..22ac1a6 100644
--- a/atr/templates/draft-revisions.html
+++ b/atr/templates/draft-revisions.html
@@ -17,9 +17,10 @@
     {% for revision in revision_history %}
       <div id="{{ revision.name }}" class="card mb-3">
         <div class="card-header d-flex justify-content-between 
align-items-center">
-          <h2 class="fs-6 my-2 mx-0 p-0 border-0">
+          <h2 class="fs-6 my-2 mx-0 p-0 border-0 atr-sans">
             <a href="#{{ revision.name }}"
-               class="fw-bold atr-sans text-decoration-none text-body">{{ 
revision.name }}</a>
+               class="fw-bold text-decoration-none text-body">{{ revision.name 
}}</a>
+            {% if revision.name == latest_revision_name %}<span class="badge 
bg-primary ms-2">Latest</span>{% endif %}
           </h2>
           <span class="fs-6 text-muted">
             {% if revision.timestamp %}
@@ -43,7 +44,7 @@
             <p class="fst-italic text-muted">No file changes detected in this 
revision.</p>
           {% else %}
             {% if revision.added %}
-              <p class="fs-6 fw-bold mt-3">Added files</p>
+              <h3 class="fs-6 fw-semibold mt-3 atr-sans">Added files</h3>
               <ul class="list-group list-group-flush mb-2">
                 {% for file in revision.added %}
                   <li class="list-group-item list-group-item-success py-1 px-3 
small rounded-2">{{ file }}</li>
@@ -52,7 +53,7 @@
             {% endif %}
 
             {% if revision.removed %}
-              <p class="fs-6 fw-bold mt-3">Removed files</p>
+              <h3 class="fs-6 fw-semibold mt-3 atr-sans">Removed files</h3>
               <ul class="list-group list-group-flush mb-2">
                 {% for file in revision.removed %}
                   <li class="list-group-item list-group-item-danger py-1 px-3 
small rounded-2">{{ file }}</li>
@@ -61,7 +62,7 @@
             {% endif %}
 
             {% if revision.modified %}
-              <p class="fs-6 fw-bold mt-3">Modified files</p>
+              <h3 class="fs-6 fw-semibold mt-3 atr-sans">Modified files</h3>
               <ul class="list-group list-group-flush mb-2">
                 {% for file in revision.modified %}
                   <li class="list-group-item list-group-item-warning py-1 px-3 
small rounded-2">{{ file }}</li>
@@ -69,6 +70,15 @@
               </ul>
             {% endif %}
           {% endif %}
+
+          <h3 class="fs-6 fw-semibold mt-3 atr-sans">Actions</h3>
+          <div class="mt-3">
+            <form method="post"
+                  action="{{ as_url(routes.draft.revision_set, 
project_name=project_name, version_name=version_name) }}">
+              <input type="hidden" name="revision_name" value="{{ 
revision.name }}" />
+              <button type="submit" class="btn btn-sm btn-outline-danger">Set 
this revision as latest</button>
+            </form>
+          </div>
         </div>
       </div>
     {% endfor %}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to