fix(requests): rework handling of requests

This commit changes several things about how we were handling
package requests.

Modifications (requests):
-------------
- `/requests/{id}/close` no longer provides an Accepted selection.
  All manual request closures will cause a rejection.
- Relevent `pkgbase` actions now trigger request closures:
  `/pkgbase/{name}/delete` (deletion), `/pkgbase/{name}/merge` (merge)
  and `/pkgbase/{name}/disown` (orphan).
- Comment fields have been added to
  `/pkgbase/{name}/{delete,merge,disown}`, which is used to set the
  `PackageRequest.ClosureComment` on pending requests. If the comment
  field is left blank, a closure comment is autogenerated.
- Autogenerated request notifications are only sent out once
  as a closure notification.
- Some markup has been fixed.

Modifications (disown/orphan):
-----------------------------
- Orphan requests are now handled through the same path as
  deletion/merge.
- We now check for due date when disowning as non-maintainer;
  previously, this was only done for display and not functionally.
  This check applies to Trusted Users' disowning of a package.

This style of notification flow does reduce our visibility, but
accounting can still be done via the close request; it includes
the action, pkgbase name and the user who accepted it.

Closes #204

Signed-off-by: Kevin Morris <kevr@0cost.org>
This commit is contained in:
Kevin Morris 2021-12-08 17:34:44 -08:00
parent bad57ba502
commit 26b1674c9e
No known key found for this signature in database
GPG key ID: F7E46DED420788F3
10 changed files with 1044 additions and 354 deletions

240
aurweb/packages/requests.py Normal file
View file

@ -0,0 +1,240 @@
from datetime import datetime
from typing import List, Optional, Set
from fastapi import Request
from sqlalchemy import and_, orm
from aurweb import config, db, l10n, 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} "
"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`. """
is_maint = user == pkgbase.Maintainer
if is_maint:
return True
requests = pkgbase.requests.filter(
PackageRequest.ReqTypeID == ORPHAN_ID)
for pkgreq in requests:
idle_time = config.getint("options", "request_idle_time")
time_delta = int(datetime.utcnow().timestamp()) - 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 = int(datetime.utcnow().timestamp())
pkgreq.Status = status
pkgreq.Closer = closer
pkgreq.ClosureComment = (
pkgreq.ClosureComment or ClosureFactory().get_closure(
pkgreq.ReqTypeID, closer, pkgbase, target, status)
)
pkgreq.ClosedTS = now
def handle_request(request: Request, reqtype_id: int,
pkgbase: PackageBase,
target: PackageBase = None) -> 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:
with db.begin():
pkgreq = db.create(PackageRequest,
ReqTypeID=reqtype_id,
User=request.user,
PackageBase=pkgbase,
PackageBaseName=pkgbase.Name,
Comments="Autogenerated by aurweb.",
ClosureComment=str())
# 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.
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))
# 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

View file

@ -4,19 +4,20 @@ from typing import Any, Dict, List
from fastapi import APIRouter, Form, HTTPException, Query, Request, Response
from fastapi.responses import JSONResponse, RedirectResponse
from sqlalchemy import case
from sqlalchemy import and_, case
import aurweb.filters
import aurweb.packages.util
from aurweb import db, defaults, l10n, logging, models, util
from aurweb.auth import auth_required, creds
from aurweb.exceptions import ValidationError
from aurweb.exceptions import InvariantError, ValidationError
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID
from aurweb.models.request_type import DELETION_ID, MERGE, MERGE_ID
from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID
from aurweb.packages import util as pkgutil
from aurweb.packages import validate
from aurweb.packages.requests import handle_request, update_closure_comment
from aurweb.packages.search import PackageSearch
from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment, get_pkgreq_by_id, query_notified, query_voted
from aurweb.scripts import notify, popupdate
@ -115,66 +116,30 @@ async def packages(request: Request) -> Response:
return await packages_get(request, context)
def create_request_if_missing(requests: List[models.PackageRequest],
reqtype: models.RequestType,
user: models.User,
package: models.Package):
now = int(datetime.utcnow().timestamp())
pkgreq = db.query(models.PackageRequest).filter(
models.PackageRequest.PackageBaseName == package.PackageBase.Name
).first()
if not pkgreq:
# No PackageRequest existed. Create one.
comments = "Automatically generated by aurweb."
closure_comment = "Deleted by aurweb."
pkgreq = db.create(models.PackageRequest,
RequestType=reqtype,
PackageBase=package.PackageBase,
PackageBaseName=package.PackageBase.Name,
User=user,
Status=ACCEPTED_ID,
Comments=comments,
ClosureComment=closure_comment,
ClosedTS=now,
Closer=user)
requests.append(pkgreq)
return pkgreq
def delete_package(deleter: models.User, package: models.Package):
notifications = []
requests = []
def delete_package(request: Request, package: models.Package,
merge_into: models.PackageBase = None,
comments: str = str()):
bases_to_delete = []
target = db.query(models.PackageBase).filter(
models.PackageBase.Name == merge_into
).first()
notifs = []
# In all cases, though, just delete the Package in question.
if package.PackageBase.packages.count() == 1:
reqtype = db.query(models.RequestType).filter(
models.RequestType.ID == DELETION_ID
).first()
with db.begin():
pkgreq = create_request_if_missing(
requests, reqtype, deleter, package)
pkgreq.Status = ACCEPTED_ID
notifs = handle_request(request, DELETION_ID, package.PackageBase,
target=target)
bases_to_delete.append(package.PackageBase)
# Prepare DeleteNotification.
notifications.append(
notify.DeleteNotification(deleter.ID, package.PackageBase.ID)
)
with db.begin():
update_closure_comment(package.PackageBase, DELETION_ID, comments,
target=target)
# For each PackageRequest created, mock up an open and close notification.
basename = package.PackageBase.Name
for pkgreq in requests:
notifications.append(
notify.RequestOpenNotification(
deleter.ID, pkgreq.ID, reqtype.Name,
pkgreq.PackageBase.ID, merge_into=basename or None)
)
notifications.append(
notify.RequestCloseNotification(
deleter.ID, pkgreq.ID, pkgreq.status_display())
# Prepare DeleteNotification.
notifs.append(
notify.DeleteNotification(request.user.ID, package.PackageBase.ID)
)
# Perform all the deletions.
@ -183,7 +148,7 @@ def delete_package(deleter: models.User, package: models.Package):
db.delete_all(bases_to_delete)
# Send out all the notifications.
util.apply_all(notifications, lambda n: n.send())
util.apply_all(notifs, lambda n: n.send())
async def make_single_context(request: Request,
@ -676,22 +641,26 @@ async def pkgbase_request_post(request: Request, name: str,
auto_orphan_age = aurweb.config.getint("options", "auto_orphan_age")
auto_delete_age = aurweb.config.getint("options", "auto_delete_age")
flagged = pkgbase.OutOfDateTS and pkgbase.OutOfDateTS >= auto_orphan_age
ood_ts = pkgbase.OutOfDateTS or 0
flagged = ood_ts and (now - ood_ts) >= auto_orphan_age
is_maintainer = pkgbase.Maintainer == request.user
outdated = now - pkgbase.SubmittedTS <= auto_delete_age
outdated = (now - pkgbase.SubmittedTS) <= auto_delete_age
if type == "orphan" and flagged:
# This request should be auto-accepted.
with db.begin():
pkgbase.Maintainer = None
pkgreq.Status = ACCEPTED_ID
db.refresh(pkgreq)
notif = notify.RequestCloseNotification(
request.user.ID, pkgreq.ID, pkgreq.status_display())
notif.send()
logger.debug(f"New request #{pkgreq.ID} is marked for auto-orphan.")
elif type == "deletion" and is_maintainer and outdated:
# This request should be auto-accepted.
packages = pkgbase.packages.all()
for package in packages:
delete_package(request.user, package)
delete_package(request, package)
logger.debug(f"New request #{pkgreq.ID} is marked for auto-deletion.")
# Redirect the submitting user to /packages.
return RedirectResponse("/packages", status_code=HTTPStatus.SEE_OTHER)
@ -713,31 +682,24 @@ async def requests_close(request: Request, id: int):
@router.post("/requests/{id}/close")
@auth_required()
async def requests_close_post(request: Request, id: int,
reason: int = Form(default=0),
comments: str = Form(default=str())):
pkgreq = get_pkgreq_by_id(id)
if not request.user.is_elevated() and request.user != pkgreq.User:
# `pkgreq`.User can close their own request.
approved = [pkgreq.User]
if not request.user.has_credential(creds.PKGREQ_CLOSE, approved=approved):
# Request user doesn't have permission here: redirect to '/'.
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)
context = make_context(request, "Close Request")
context["pkgreq"] = pkgreq
if reason not in {ACCEPTED_ID, REJECTED_ID}:
# If the provided reason is not valid, send the user back to
# the closure form with a BAD_REQUEST status.
return render_template(request, "requests/close.html", context,
status_code=HTTPStatus.BAD_REQUEST)
if not request.user.is_elevated():
# If we're closing the request as the user who created it,
# the reason should just be a REJECTION.
reason = REJECTED_ID
now = int(datetime.utcnow().timestamp())
with db.begin():
pkgreq.Closer = request.user
pkgreq.Status = reason
pkgreq.ClosureComment = comments
pkgreq.ClosedTS = now
pkgreq.Status = REJECTED_ID
notify_ = notify.RequestCloseNotification(
request.user.ID, pkgreq.ID, pkgreq.status_display())
@ -932,7 +894,8 @@ async def pkgbase_unvote(request: Request, name: str):
def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase):
disowner = request.user
notif = notify.DisownNotification(disowner.ID, pkgbase.ID)
notifs = [notify.DisownNotification(disowner.ID, pkgbase.ID)]
notifs += handle_request(request, ORPHAN_ID, pkgbase)
if disowner != pkgbase.Maintainer:
with db.begin():
@ -949,7 +912,7 @@ def pkgbase_disown_instance(request: Request, pkgbase: models.PackageBase):
else:
pkgbase.Maintainer = None
notif.send()
util.apply_all(notifs, lambda n: n.send())
@router.get("/pkgbase/{name}/disown")
@ -971,6 +934,7 @@ async def pkgbase_disown_get(request: Request, name: str):
@router.post("/pkgbase/{name}/disown")
@auth_required()
async def pkgbase_disown_post(request: Request, name: str,
comments: str = Form(default=str()),
confirm: bool = Form(default=False)):
pkgbase = get_pkg_or_base(name, models.PackageBase)
@ -980,15 +944,24 @@ async def pkgbase_disown_post(request: Request, name: str,
return RedirectResponse(f"/pkgbase/{name}",
HTTPStatus.SEE_OTHER)
context = make_context(request, "Disown Package")
context["pkgbase"] = pkgbase
if not confirm:
context = make_context(request, "Disown Package")
context["pkgbase"] = pkgbase
context["errors"] = [("The selected packages have not been disowned, "
"check the confirmation checkbox.")]
return render_template(request, "packages/disown.html", context,
status_code=HTTPStatus.BAD_REQUEST)
pkgbase_disown_instance(request, pkgbase)
with db.begin():
update_closure_comment(pkgbase, ORPHAN_ID, comments)
try:
pkgbase_disown_instance(request, pkgbase)
except InvariantError as exc:
context["errors"] = [str(exc)]
return render_template(request, "packages/disown.html", context,
status_code=HTTPStatus.BAD_REQUEST)
return RedirectResponse(f"/pkgbase/{name}",
status_code=HTTPStatus.SEE_OTHER)
@ -1032,7 +1005,8 @@ async def pkgbase_delete_get(request: Request, name: str):
@router.post("/pkgbase/{name}/delete")
@auth_required()
async def pkgbase_delete_post(request: Request, name: str,
confirm: bool = Form(default=False)):
confirm: bool = Form(default=False),
comments: str = Form(default=str())):
pkgbase = get_pkg_or_base(name, models.PackageBase)
if not request.user.has_credential(creds.PKGBASE_DELETE):
@ -1047,10 +1021,20 @@ async def pkgbase_delete_post(request: Request, name: str,
return render_template(request, "packages/delete.html", context,
status_code=HTTPStatus.BAD_REQUEST)
if comments:
# Update any existing deletion requests' ClosureComment.
with db.begin():
requests = pkgbase.requests.filter(
and_(models.PackageRequest.Status == PENDING_ID,
models.PackageRequest.ReqTypeID == DELETION_ID)
)
for pkgreq in requests:
pkgreq.ClosureComment = comments
# Obtain deletion locks and delete the packages.
packages = pkgbase.packages.all()
for package in packages:
delete_package(request.user, package)
delete_package(request, package, comments=comments)
return RedirectResponse("/packages", status_code=HTTPStatus.SEE_OTHER)
@ -1190,6 +1174,17 @@ async def packages_adopt(request: Request, package_ids: List[int] = [],
return (True, ["The selected packages have been adopted."])
def disown_all(request: Request, pkgbases: List[models.PackageBase]) \
-> List[str]:
errors = []
for pkgbase in pkgbases:
try:
pkgbase_disown_instance(request, pkgbase)
except InvariantError as exc:
errors.append(str(exc))
return errors
async def packages_disown(request: Request, package_ids: List[int] = [],
confirm: bool = False, **kwargs):
if not package_ids:
@ -1217,9 +1212,9 @@ async def packages_disown(request: Request, package_ids: List[int] = [],
return (False, ["You are not allowed to disown one "
"of the packages you selected."])
# Now, really disown the bases.
for pkgbase in bases:
pkgbase_disown_instance(request, pkgbase)
# Now, disown all the bases if we can.
if errors := disown_all(request, bases):
return (False, errors)
return (True, ["The selected packages have been disowned."])
@ -1256,7 +1251,7 @@ async def packages_delete(request: Request, package_ids: List[int] = [],
# using the same method we use in our /pkgbase/{name}/delete route.
for pkg in packages:
deleted_pkgs.append(pkg.Name)
delete_package(request.user, pkg)
delete_package(request, pkg)
# Log out the fact that this happened for accountability.
logger.info(f"Privileged user '{request.user.Username}' deleted the "
@ -1341,83 +1336,46 @@ async def pkgbase_merge_get(request: Request, name: str,
def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase,
target: models.PackageBase):
target: models.PackageBase, comments: str = str()):
pkgbasename = str(pkgbase.Name)
# Collect requests related to this merge.
query = pkgbase.requests.filter(
models.PackageRequest.ReqTypeID == MERGE_ID
)
# Create notifications.
notifs = handle_request(request, MERGE_ID, pkgbase, target)
requests = query.filter(
models.PackageRequest.MergeBaseName == target.Name).all()
reject_requests = query.filter(
models.PackageRequest.MergeBaseName != target.Name).all()
# Target votes and notifications sets of user IDs that are
# looking to be migrated.
target_votes = set(v.UsersID for v in target.package_votes)
target_notifs = set(n.UserID for n in target.notifications)
notifs = [] # Used to keep track of notifications over the function.
closure_comment = (f"Merged into package base {target.Name} by "
f"{request.user.Username}.")
rejected_closure_comment = ("Rejected because another merge request "
"for the same package base was accepted.")
with db.begin():
# Merge pkgbase's comments.
for comment in pkgbase.comments:
comment.PackageBase = target
if not requests:
# If there are no requests, create one owned by request.user.
with db.begin():
pkgreq = db.create(models.PackageRequest,
ReqTypeID=MERGE_ID,
User=request.user,
PackageBase=pkgbase,
PackageBaseName=pkgbasename,
MergeBaseName=target.Name,
Comments="Generated by aurweb.",
Status=ACCEPTED_ID,
ClosureComment=closure_comment)
requests.append(pkgreq)
# Add a notification about the opening to our notifs array.
notif = notify.RequestOpenNotification(
request.user.ID, pkgreq.ID, MERGE,
pkgbase.ID, merge_into=target.Name)
notifs.append(notif)
with db.begin():
# Merge pkgbase's comments, notifications and votes into target.
for comment in pkgbase.comments:
comment.PackageBase = target
for notif in pkgbase.notifications:
# Merge notifications that don't yet exist in the target.
for notif in pkgbase.notifications:
if notif.UserID not in target_notifs:
notif.PackageBase = target
for vote in pkgbase.package_votes:
# Merge votes that don't yet exist in the target.
for vote in pkgbase.package_votes:
if vote.UsersID not in target_votes:
vote.PackageBase = target
with db.begin():
# Delete pkgbase and its packages now that everything's merged.
for pkg in pkgbase.packages:
db.delete(pkg)
db.delete(pkgbase)
# Run popupdate.
popupdate.run_single(target)
# Accept merge requests related to this pkgbase and target.
for pkgreq in requests:
pkgreq.Status = ACCEPTED_ID
pkgreq.ClosureComment = closure_comment
pkgreq.Closer = request.user
for pkgreq in reject_requests:
pkgreq.Status = REJECTED_ID
pkgreq.ClosureComment = rejected_closure_comment
pkgreq.Closer = request.user
all_requests = requests + reject_requests
for pkgreq in all_requests:
# Create notifications for request closure.
notif = notify.RequestCloseNotification(
request.user.ID, pkgreq.ID, pkgreq.status_display())
notifs.append(notif)
with db.begin():
# Delete pkgbase and its packages now that everything's merged.
for pkg in pkgbase.packages:
db.delete(pkg)
db.delete(pkgbase)
# Log this out for accountability purposes.
logger.info(f"Trusted User '{request.user.Username}' merged "
f"'{pkgbasename}' into '{target.Name}'.")
# Send our notifications array.
# Send notifications.
util.apply_all(notifs, lambda n: n.send())
@ -1425,6 +1383,7 @@ def pkgbase_merge_instance(request: Request, pkgbase: models.PackageBase,
@auth_required()
async def pkgbase_merge_post(request: Request, name: str,
into: str = Form(default=str()),
comments: str = Form(default=str()),
confirm: bool = Form(default=False),
next: str = Form(default=str())):
@ -1458,8 +1417,11 @@ async def pkgbase_merge_post(request: Request, name: str,
return render_template(request, "pkgbase/merge.html", context,
status_code=HTTPStatus.BAD_REQUEST)
with db.begin():
update_closure_comment(pkgbase, MERGE_ID, comments, target=target)
# Merge pkgbase into target.
pkgbase_merge_instance(request, pkgbase, target)
pkgbase_merge_instance(request, pkgbase, target, comments=comments)
# Run popupdate on the target.
popupdate.run_single(target)