mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
TUs and Devs can delete and merge packages directly. Currently the comments they enter, don't end up in the ML notification. Include the comment in the notifications for direct deletion / merge Signed-off-by: moson-mo <mo-son@mailbox.org>
269 lines
9.2 KiB
Python
269 lines
9.2 KiB
Python
from typing import Optional, Set
|
|
|
|
from fastapi import Request
|
|
from sqlalchemy import and_, orm
|
|
|
|
from aurweb import config, db, l10n, time, util
|
|
from aurweb.exceptions import InvariantError
|
|
from aurweb.models import PackageBase, PackageRequest, User
|
|
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
|
|
from aurweb.models.request_type import (
|
|
DELETION,
|
|
DELETION_ID,
|
|
MERGE,
|
|
MERGE_ID,
|
|
ORPHAN,
|
|
ORPHAN_ID,
|
|
)
|
|
from aurweb.scripts import notify
|
|
|
|
|
|
class ClosureFactory:
|
|
"""A factory class used to autogenerate closure comments."""
|
|
|
|
REQTYPE_NAMES = {DELETION_ID: DELETION, MERGE_ID: MERGE, ORPHAN_ID: ORPHAN}
|
|
|
|
def _deletion_closure(
|
|
self, requester: User, pkgbase: PackageBase, target: PackageBase = None
|
|
):
|
|
return f"[Autogenerated] Accepted deletion for {pkgbase.Name}."
|
|
|
|
def _merge_closure(
|
|
self, requester: User, pkgbase: PackageBase, target: PackageBase = None
|
|
):
|
|
return (
|
|
f"[Autogenerated] Accepted merge for {pkgbase.Name} " f"into {target.Name}."
|
|
)
|
|
|
|
def _orphan_closure(
|
|
self, requester: User, pkgbase: PackageBase, target: PackageBase = None
|
|
):
|
|
return f"[Autogenerated] Accepted orphan for {pkgbase.Name}."
|
|
|
|
def _rejected_merge_closure(
|
|
self, requester: User, pkgbase: PackageBase, target: PackageBase = None
|
|
):
|
|
return (
|
|
f"[Autogenerated] Another request to merge {pkgbase.Name} "
|
|
f"into {target.Name} has rendered this request invalid."
|
|
)
|
|
|
|
def get_closure(
|
|
self,
|
|
reqtype_id: int,
|
|
requester: User,
|
|
pkgbase: PackageBase,
|
|
target: PackageBase = None,
|
|
status: int = ACCEPTED_ID,
|
|
) -> str:
|
|
"""
|
|
Return a closure comment handled by this class.
|
|
|
|
:param reqtype_id: RequestType.ID
|
|
:param requester: User who is closing a request
|
|
:param pkgbase: PackageBase instance related to the request
|
|
:param target: Merge request target PackageBase instance
|
|
:param status: PackageRequest.Status
|
|
"""
|
|
reqtype = ClosureFactory.REQTYPE_NAMES.get(reqtype_id)
|
|
|
|
partial = str()
|
|
if status == REJECTED_ID:
|
|
partial = "_rejected"
|
|
|
|
try:
|
|
handler = getattr(self, f"{partial}_{reqtype}_closure")
|
|
except AttributeError:
|
|
raise NotImplementedError("Unsupported 'reqtype_id' value.")
|
|
return handler(requester, pkgbase, target)
|
|
|
|
|
|
def update_closure_comment(
|
|
pkgbase: PackageBase, reqtype_id: int, comments: str, target: PackageBase = None
|
|
) -> None:
|
|
"""
|
|
Update all pending requests related to `pkgbase` with a closure comment.
|
|
|
|
In order to persist closure comments through `handle_request`'s
|
|
algorithm, we must set `PackageRequest.ClosureComment` before calling
|
|
it. This function can be used to update the closure comment of all
|
|
package requests related to `pkgbase` and `reqtype_id`.
|
|
|
|
If an empty `comments` string is provided, we no-op out of this.
|
|
|
|
:param pkgbase: PackageBase instance
|
|
:param reqtype_id: RequestType.ID
|
|
:param comments: PackageRequest.ClosureComment to update to
|
|
:param target: Merge request target PackageBase instance
|
|
"""
|
|
if not comments:
|
|
return
|
|
|
|
query = pkgbase.requests.filter(
|
|
and_(
|
|
PackageRequest.ReqTypeID == reqtype_id, PackageRequest.Status == PENDING_ID
|
|
)
|
|
)
|
|
if reqtype_id == MERGE_ID:
|
|
query = query.filter(PackageRequest.MergeBaseName == target.Name)
|
|
|
|
for pkgreq in query:
|
|
pkgreq.ClosureComment = comments
|
|
|
|
|
|
def verify_orphan_request(user: User, pkgbase: PackageBase):
|
|
"""Verify that an undue orphan request exists in `requests`."""
|
|
requests = pkgbase.requests.filter(PackageRequest.ReqTypeID == ORPHAN_ID)
|
|
for pkgreq in requests:
|
|
idle_time = config.getint("options", "request_idle_time")
|
|
time_delta = time.utcnow() - pkgreq.RequestTS
|
|
is_due = pkgreq.Status == PENDING_ID and time_delta > idle_time
|
|
if is_due:
|
|
# If the requester is the pkgbase maintainer or the
|
|
# request is already due, we're good to go: return True.
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def close_pkgreq(
|
|
pkgreq: PackageRequest,
|
|
closer: User,
|
|
pkgbase: PackageBase,
|
|
target: Optional[PackageBase],
|
|
status: int,
|
|
) -> None:
|
|
"""
|
|
Close a package request with `pkgreq`.Status == `status`.
|
|
|
|
:param pkgreq: PackageRequest instance
|
|
:param closer: `pkgreq`.Closer User instance to update to
|
|
:param pkgbase: PackageBase instance which `pkgreq` is about
|
|
:param target: Optional PackageBase instance to merge into
|
|
:param status: `pkgreq`.Status value to update to
|
|
"""
|
|
now = time.utcnow()
|
|
pkgreq.Status = status
|
|
pkgreq.Closer = closer
|
|
pkgreq.ClosureComment = pkgreq.ClosureComment or ClosureFactory().get_closure(
|
|
pkgreq.ReqTypeID, closer, pkgbase, target, status
|
|
)
|
|
pkgreq.ClosedTS = now
|
|
|
|
|
|
@db.retry_deadlock
|
|
def handle_request(
|
|
request: Request,
|
|
reqtype_id: int,
|
|
pkgbase: PackageBase,
|
|
target: PackageBase = None,
|
|
comments: str = str(),
|
|
) -> list[notify.Notification]:
|
|
"""
|
|
Handle package requests before performing an action.
|
|
|
|
The actions we're interested in are disown (orphan), delete and
|
|
merge. There is now an automated request generation and closure
|
|
notification when a privileged user performs one of these actions
|
|
without a pre-existing request. They all commit changes to the
|
|
database, and thus before calling, state should be verified to
|
|
avoid leaked database records regarding these requests.
|
|
|
|
Otherwise, we accept and reject requests based on their state
|
|
and send out the relevent notifications.
|
|
|
|
:param requester: User who needs this a `pkgbase` request handled
|
|
:param reqtype_id: RequestType.ID
|
|
:param pkgbase: PackageBase which the request is about
|
|
:param target: Optional target to merge into
|
|
"""
|
|
notifs: list[notify.Notification] = []
|
|
|
|
# If it's an orphan request, perform further verification
|
|
# regarding existing requests.
|
|
if reqtype_id == ORPHAN_ID:
|
|
if not verify_orphan_request(request.user, pkgbase):
|
|
_ = l10n.get_translator_for_request(request)
|
|
raise InvariantError(
|
|
_("No due existing orphan requests to accept for %s.") % pkgbase.Name
|
|
)
|
|
|
|
# Produce a base query for requests related to `pkgbase`, based
|
|
# on ReqTypeID matching `reqtype_id`, pending status and a correct
|
|
# PackagBaseName column.
|
|
query: orm.Query = pkgbase.requests.filter(
|
|
and_(
|
|
PackageRequest.ReqTypeID == reqtype_id,
|
|
PackageRequest.Status == PENDING_ID,
|
|
PackageRequest.PackageBaseName == pkgbase.Name,
|
|
)
|
|
)
|
|
|
|
# Build a query for records we should accept. For merge requests,
|
|
# this is specific to a matching MergeBaseName. For others, this
|
|
# just ends up becoming `query`.
|
|
accept_query: orm.Query = query
|
|
if target:
|
|
# If a `target` was supplied, filter by MergeBaseName
|
|
accept_query = query.filter(PackageRequest.MergeBaseName == target.Name)
|
|
|
|
# Build an accept list out of `accept_query`.
|
|
to_accept: list[PackageRequest] = accept_query.all()
|
|
accepted_ids: Set[int] = set(p.ID for p in to_accept)
|
|
|
|
# Build a reject list out of `query` filtered by IDs not found
|
|
# in `to_accept`. That is, unmatched records of the same base
|
|
# query properties.
|
|
to_reject: list[PackageRequest] = query.filter(
|
|
~PackageRequest.ID.in_(accepted_ids)
|
|
).all()
|
|
|
|
# If we have no requests to accept, create a new one.
|
|
# This is done to increase tracking of actions occurring
|
|
# through the website.
|
|
if not to_accept:
|
|
utcnow = time.utcnow()
|
|
with db.begin():
|
|
pkgreq = db.create(
|
|
PackageRequest,
|
|
ReqTypeID=reqtype_id,
|
|
RequestTS=utcnow,
|
|
User=request.user,
|
|
PackageBase=pkgbase,
|
|
PackageBaseName=pkgbase.Name,
|
|
Comments="Autogenerated by aurweb.",
|
|
ClosureComment=comments,
|
|
)
|
|
|
|
# If it's a merge request, set MergeBaseName to `target`.Name.
|
|
if pkgreq.ReqTypeID == MERGE_ID:
|
|
pkgreq.MergeBaseName = target.Name
|
|
|
|
# Add the new request to `to_accept` and allow standard
|
|
# flow to continue afterward.
|
|
to_accept.append(pkgreq)
|
|
|
|
# Update requests with their new status and closures.
|
|
@db.retry_deadlock
|
|
def retry_closures():
|
|
with db.begin():
|
|
util.apply_all(
|
|
to_accept,
|
|
lambda p: close_pkgreq(p, request.user, pkgbase, target, ACCEPTED_ID),
|
|
)
|
|
util.apply_all(
|
|
to_reject,
|
|
lambda p: close_pkgreq(p, request.user, pkgbase, target, REJECTED_ID),
|
|
)
|
|
|
|
retry_closures()
|
|
|
|
# Create RequestCloseNotifications for all requests involved.
|
|
for pkgreq in to_accept + to_reject:
|
|
notif = notify.RequestCloseNotification(
|
|
request.user.ID, pkgreq.ID, pkgreq.status_display()
|
|
)
|
|
notifs.append(notif)
|
|
|
|
# Return notifications to the caller for sending.
|
|
return notifs
|