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
58 changes: 58 additions & 0 deletions pgcommitfest/commitfest/fixtures/commitfest_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -1351,5 +1351,63 @@
"created": "2025-01-26T22:11:40.000",
"modified": "2025-01-26T22:12:00.000"
}
},
{
"model": "commitfest.patch",
"pk": 9,
"fields": {
"name": "Abandoned patch in closed commitfest",
"topic": 3,
"wikilink": "",
"gitlink": "",
"targetversion": null,
"committer": null,
"created": "2024-10-15T10:00:00",
"modified": "2024-10-15T10:00:00",
"lastmail": "2024-10-01T10:00:00",
"tags": [],
"authors": [],
"reviewers": [],
"subscribers": [],
"mailthread_set": [
9
]
}
},
{
"model": "commitfest.mailthread",
"pk": 9,
"fields": {
"messageid": "abandoned@message-09",
"subject": "Abandoned patch in closed commitfest",
"firstmessage": "2024-10-01T10:00:00",
"firstauthor": "[email protected]",
"latestmessage": "2024-10-01T10:00:00",
"latestauthor": "[email protected]",
"latestsubject": "Abandoned patch in closed commitfest",
"latestmsgid": "abandoned@message-09"
}
},
{
"model": "commitfest.patchoncommitfest",
"pk": 10,
"fields": {
"patch": 9,
"commitfest": 1,
"enterdate": "2024-10-15T10:00:00",
"leavedate": null,
"status": 1
}
},
{
"model": "commitfest.patchhistory",
"pk": 23,
"fields": {
"patch": 9,
"date": "2024-10-15T10:00:00",
"by": 1,
"by_cfbot": false,
"what": "Created patch record"
}
}
]
134 changes: 129 additions & 5 deletions pgcommitfest/commitfest/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from datetime import datetime, timedelta, timezone

from pgcommitfest.mailqueue.util import send_template_mail
from pgcommitfest.userprofile.models import UserProfile

from .util import DiffableModel
Expand Down Expand Up @@ -109,6 +110,119 @@ def to_json(self):
"enddate": self.enddate.isoformat(),
}

def _should_auto_move_patch(self, patch, current_date):
"""Determine if a patch should be automatically moved to the next commitfest.

A patch qualifies for auto-move if it both:
1. Has had email activity within the configured number of days
2. Hasn't been failing CI for longer than the configured threshold
"""
activity_cutoff = current_date - timedelta(
days=settings.AUTO_MOVE_EMAIL_ACTIVITY_DAYS
)
failing_cutoff = current_date - timedelta(
days=settings.AUTO_MOVE_MAX_FAILING_DAYS
)

# Check for recent email activity
if not patch.lastmail or patch.lastmail < activity_cutoff:
return False

# Check if CI has been failing too long
try:
cfbot_branch = patch.cfbot_branch
if (
cfbot_branch.failing_since
and cfbot_branch.failing_since < failing_cutoff
):
return False
except CfbotBranch.DoesNotExist:
# IF no CFBot data exists, the patch is probably very new (i.e. no
# CI run has ever taken place for it yet). So we auto-move it in
# that case.
pass

return True

def auto_move_active_patches(self):
"""Automatically move active patches to the next commitfest.

A patch is moved if it has recent email activity and hasn't been
failing CI for too long.
"""
current_date = datetime.now()

# Get the next open commitfest (must exist, raises IndexError otherwise)
# For draft CFs, find the next draft CF
# For regular CFs, find the next regular CF by start date
if self.draft:
next_cf = CommitFest.objects.filter(
status=CommitFest.STATUS_OPEN,
draft=True,
startdate__gt=self.enddate,
).order_by("startdate")[0]
else:
next_cf = CommitFest.objects.filter(
status=CommitFest.STATUS_OPEN,
draft=False,
startdate__gt=self.enddate,
).order_by("startdate")[0]

# Get all patches with open status in this commitfest
open_pocs = self.patchoncommitfest_set.filter(
status__in=PatchOnCommitFest.OPEN_STATUSES
).select_related("patch")

for poc in open_pocs:
if self._should_auto_move_patch(poc.patch, current_date):
poc.patch.move(self, next_cf, by_user=None, by_cfbot=True)

def send_closure_notifications(self):
"""Send email notifications to authors of patches that are still open."""
# Get patches that still need action (not moved, not closed)
open_pocs = list(
self.patchoncommitfest_set.filter(
status__in=PatchOnCommitFest.OPEN_STATUSES
)
.select_related("patch")
.prefetch_related("patch__authors")
)

if not open_pocs:
return

# Collect unique authors and their patches
authors_patches = {}
for poc in open_pocs:
for author in poc.patch.authors.all():
if author not in authors_patches:
authors_patches[author] = []
authors_patches[author].append(poc)

# Send email to each author who has notifications enabled
for author, patches in authors_patches.items():
try:
if not author.userprofile.notify_all_author:
continue
notifyemail = author.userprofile.notifyemail
except UserProfile.DoesNotExist:
continue

email = notifyemail.email if notifyemail else author.email

send_template_mail(
settings.NOTIFICATION_FROM,
None,
email,
f"Commitfest {self.name} has closed and you have unmoved patches",
"mail/commitfest_closure.txt",
{
"user": author,
"commitfest": self,
"patches": patches,
},
)

@staticmethod
def _are_relevant_commitfests_up_to_date(cfs, current_date):
inprogress_cf = cfs["in_progress"]
Expand Down Expand Up @@ -143,26 +257,33 @@ def _refresh_relevant_commitfests(cls, for_update):
if inprogress_cf and inprogress_cf.enddate < current_date:
inprogress_cf.status = CommitFest.STATUS_CLOSED
inprogress_cf.save()
inprogress_cf.auto_move_active_patches()
inprogress_cf.send_closure_notifications()

open_cf = cfs["open"]

if open_cf.startdate <= current_date:
if open_cf.enddate < current_date:
open_cf.status = CommitFest.STATUS_CLOSED
open_cf.save()
cls.next_open_cf(current_date).save()
open_cf.auto_move_active_patches()
open_cf.send_closure_notifications()
else:
open_cf.status = CommitFest.STATUS_INPROGRESS
open_cf.save()

cls.next_open_cf(current_date).save()
open_cf.save()
cls.next_open_cf(current_date).save()

draft_cf = cfs["draft"]
if not draft_cf:
cls.next_draft_cf(current_date).save()
elif draft_cf.enddate < current_date:
# If the draft commitfest has started, we need to update it
# Create next CF first so auto_move has somewhere to move patches
draft_cf.status = CommitFest.STATUS_CLOSED
draft_cf.save()
cls.next_draft_cf(current_date).save()
draft_cf.auto_move_active_patches()
draft_cf.send_closure_notifications()

return cls.relevant_commitfests(for_update=for_update)

Expand Down Expand Up @@ -456,7 +577,9 @@ def update_lastmail(self):
else:
self.lastmail = max(threads, key=lambda t: t.latestmessage).latestmessage

def move(self, from_cf, to_cf, by_user, allow_move_to_in_progress=False):
def move(
self, from_cf, to_cf, by_user, allow_move_to_in_progress=False, by_cfbot=False
):
"""Returns the new PatchOnCommitFest object, or raises UserInputError"""

current_poc = self.current_patch_on_commitfest()
Expand Down Expand Up @@ -501,6 +624,7 @@ def move(self, from_cf, to_cf, by_user, allow_move_to_in_progress=False):
PatchHistory(
patch=self,
by=by_user,
by_cfbot=by_cfbot,
what=f"Moved from CF {from_cf} to CF {to_cf}",
).save_and_notify()

Expand Down
6 changes: 6 additions & 0 deletions pgcommitfest/commitfest/templates/commitfest.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
{%load commitfest %}
{%block contents%}

{%if cf.is_closed%}
<div class="alert alert-warning" role="alert">
<strong>This commitfest is closed.</strong> Open patches in this commitfest are no longer being picked up by CI. If you want CI to run on your patch, move it to an open commitfest.
</div>
{%endif%}

<button type="button" class="btn btn-secondary active" id="filterButton" onClick="togglePatchFilterButton('filterButton', 'collapseFilters')">Search/filter</button>
<a class="btn btn-secondary{% if request.GET.reviewer == '-2' %} active{% endif %}" href="?reviewer=-2">No reviewers</a>
<a class="btn btn-secondary{% if request.GET.author == '-3' %} active{% endif %}" href="?author=-3">My patches</a>
Expand Down
5 changes: 5 additions & 0 deletions pgcommitfest/commitfest/templates/help.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ <h2>Commitfest</h2>
</p>
<p>There are 5 Commitfests per year. The first one is "In Progress" in <em>July</em> and starts the nine months feature development cycle of PostgreSQL. The next three are "In Progress" in <em>September</em>, <em>November</em> and <em>January</em>. The last Commitfest of the feature development cycle is "In Progress" in <em>March</em>, and ends a when the feature freeze starts. The exact date of the feature freeze depends on the year, but it's usually in early April.</p>

<h3>Commitfest closure</h3>
<p>
When a Commitfest closes, patches that have been active recently are automatically moved to the next Commitfest. A patch is considered "active" if it has had email activity in the past {{auto_move_email_activity_days}} days and has not been failing CI for more than {{auto_move_max_failing_days}} days. Patches that are not automatically moved will stay in the closed Commitfest, where they will no longer be picked up by CI. Authors of such patches that have enabled "Notify on all where author" in their profile settings will receive an email notification asking them to either move the patch to the next Commitfest or close it with an appropriate status.
</p>

<h2>Patches</h2>
<p>
A "patch" is a bit of an overloaded term in the PostgreSQL community. Email threads on the mailing list often contain "patch files" as attachments, such a file is often referred to as a "patch". A single email can even contain multiple related "patch files", which are called a "patchset". However, in the context of a Commitfest app a "patch" usually means a "patch entry" in the Commitfest app. Such a "patch entry" is a reference to a mailinglist thread on which change to PostgreSQL has been proposed, by someone sending an email that contain one or more "patch files". The Commitfest app will automatically detect new versions of the patch files and update the "patch entry" accordingly.
Expand Down
14 changes: 14 additions & 0 deletions pgcommitfest/commitfest/templates/mail/commitfest_closure.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Commitfest {{commitfest.name}} has now closed.

You have {{patches|length}} open patch{{patches|length|pluralize:"es"}} that need{{patches|length|pluralize:"s,"}} attention:

{% for poc in patches %}
- {{poc.patch.name}}
https://commitfest.postgresql.org/patch/{{poc.patch.id}}/
{% endfor %}

Please take action on {{patches|length|pluralize:"these patches,this patch"}} by doing either of the following:

1. If you want to continue working on {{patches|length|pluralize:"them,it"}}, move {{patches|length|pluralize:"them,it"}} to the next commitfest.

2. If you no longer wish to pursue {{patches|length|pluralize:"these patches,this patch"}}, please close {{patches|length|pluralize:"them,it"}} with an appropriate status (Withdrawn, Returned with feedback, etc.)
7 changes: 7 additions & 0 deletions pgcommitfest/commitfest/templates/patch.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
{%extends "base.html"%}
{%load commitfest%}
{%block contents%}
{%with current_poc=patch_commitfests.0%}
{%if cf.is_closed and not current_poc.is_closed%}
<div class="alert alert-warning" role="alert">
<strong>This patch is part of a closed commitfest.</strong> It is no longer being picked up by CI. If you want CI to run on this patch, move it to an open commitfest.
</div>
{%endif%}
{%endwith%}
{%include "patch_commands.inc"%}
<table class="table table-bordered">
<tbody>
Expand Down
7 changes: 5 additions & 2 deletions pgcommitfest/commitfest/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@
import pytest

from pgcommitfest.commitfest.models import CommitFest
from pgcommitfest.userprofile.models import UserProfile


@pytest.fixture
def alice():
"""Create test user Alice."""
return User.objects.create_user(
"""Create test user Alice with notify_all_author enabled."""
user = User.objects.create_user(
username="alice",
first_name="Alice",
last_name="Anderson",
email="[email protected]",
)
UserProfile.objects.create(user=user, notify_all_author=True)
return user


@pytest.fixture
Expand Down
Loading