From 5aa358fb41ce325af310da985f5a15d525715db9 Mon Sep 17 00:00:00 2001 From: Shihao Date: Thu, 1 Oct 2026 21:49:54 -0600 Subject: [PATCH 1/2] Search pgsql-bugs too when attaching a thread The "Attach thread" search only looked at pgsql-hackers, so a patch for a bug report could only be attached by pasting a message id. Search every list in ARCHIVES_SEARCH_LISTS, which is pgsql-hackers and pgsql-bugs by default, and merge the results. Fixes #117 --- pgcommitfest/commitfest/ajax.py | 12 ++- .../commitfest/tests/test_get_threads.py | 73 +++++++++++++++++++ pgcommitfest/settings.py | 2 + 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 pgcommitfest/commitfest/tests/test_get_threads.py diff --git a/pgcommitfest/commitfest/ajax.py b/pgcommitfest/commitfest/ajax.py index 8137dfdf..fa1eac69 100644 --- a/pgcommitfest/commitfest/ajax.py +++ b/pgcommitfest/commitfest/ajax.py @@ -87,13 +87,19 @@ def getThreads(request): else: attachonly = 0 - # Make a JSON api call to the archives server + # Make a JSON api call to the archives server for each list. A message + # that was sent to more than one list is only returned once. params = {"n": 100, "a": attachonly} if search: params["s"] = search - r = _archivesAPI("/list/pgsql-hackers/latest.json", params) - return sorted(r, key=lambda x: x["date"], reverse=True) + messages = {} + for listname in settings.ARCHIVES_SEARCH_LISTS: + for m in _archivesAPI("/list/%s/latest.json" % listname, params): + messages.setdefault(m["msgid"], m) + + r = sorted(messages.values(), key=lambda x: x["date"], reverse=True) + return r[: params["n"]] def getMessages(request): diff --git a/pgcommitfest/commitfest/tests/test_get_threads.py b/pgcommitfest/commitfest/tests/test_get_threads.py new file mode 100644 index 00000000..df240976 --- /dev/null +++ b/pgcommitfest/commitfest/tests/test_get_threads.py @@ -0,0 +1,73 @@ +from django.test import RequestFactory + +from unittest.mock import patch + +from pgcommitfest.commitfest.ajax import getThreads + + +def _msg(msgid, date, subj): + return {"msgid": msgid, "date": date, "from": "a@example.com", "subj": subj} + + +HACKERS = [ + _msg("h1@example.com", "2026-01-03T00:00:00", "hackers thread"), + _msg("both@example.com", "2026-01-01T00:00:00", "sent to both lists"), +] +BUGS = [ + _msg("b1@example.com", "2026-01-02T00:00:00", "BUG #1: bugs thread"), + _msg("both@example.com", "2026-01-01T00:00:00", "sent to both lists"), +] + + +def _fake_archives(suburl, params=None): + return { + "/list/pgsql-hackers/latest.json": HACKERS, + "/list/pgsql-bugs/latest.json": BUGS, + }[suburl] + + +def test_get_threads_searches_hackers_and_bugs(): + """The thread search should return threads from pgsql-bugs too.""" + request = RequestFactory().get("/ajax/getThreads/", {"s": "thread", "a": "1"}) + + with patch( + "pgcommitfest.commitfest.ajax._archivesAPI", side_effect=_fake_archives + ) as api: + result = getThreads(request) + + # Both lists are searched with the same parameters + assert [c.args for c in api.call_args_list] == [ + ("/list/pgsql-hackers/latest.json", {"n": 100, "a": 1, "s": "thread"}), + ("/list/pgsql-bugs/latest.json", {"n": 100, "a": 1, "s": "thread"}), + ] + + # Results are merged newest first, and the message sent to both lists + # shows up only once + assert [m["msgid"] for m in result] == [ + "h1@example.com", + "b1@example.com", + "both@example.com", + ] + + +def test_get_threads_limits_merged_result(): + """Merging two lists should not return more than one list would.""" + hackers = [ + _msg("h%d@example.com" % i, "2026-01-01T00:%02d:00" % (i % 60), "h") + for i in range(100) + ] + bugs = [ + _msg("b%d@example.com" % i, "2026-01-02T00:%02d:00" % (i % 60), "b") + for i in range(100) + ] + + def fake(suburl, params=None): + return hackers if "pgsql-hackers" in suburl else bugs + + request = RequestFactory().get("/ajax/getThreads/") + with patch("pgcommitfest.commitfest.ajax._archivesAPI", side_effect=fake): + result = getThreads(request) + + assert len(result) == 100 + # The bugs messages are all newer, so they are the ones that are kept + assert all(m["msgid"].startswith("b") for m in result) diff --git a/pgcommitfest/settings.py b/pgcommitfest/settings.py index 07e4adaf..cdcd3121 100644 --- a/pgcommitfest/settings.py +++ b/pgcommitfest/settings.py @@ -157,6 +157,8 @@ ARCHIVES_PORT = "8001" ARCHIVES_HOST = "archives.postgresql.org" # Host: header to send ARCHIVES_APIKEY = None +# Mailing lists searched when attaching a thread to a patch +ARCHIVES_SEARCH_LISTS = ["pgsql-hackers", "pgsql-bugs"] # Email address to pgsql-hackers. Set to something local to test maybe? HACKERS_EMAIL = "pgsql-hackers-testing@localhost" From fc254257e292fca1ff88174aa1cdbb2469d5d030 Mon Sep 17 00:00:00 2001 From: Jelterminator FennemAI-Neo Date: Sun, 18 Oct 2026 09:59:15 +0200 Subject: [PATCH 2/2] Hardcode the searched mailing lists and simplify the test There's no real need for deployments to configure which lists are searched when attaching a thread, so the `ARCHIVES_SEARCH_LISTS` setting is replaced by an inline tuple in `getThreads`. The two tests are folded into one that covers merging, deduplication, ordering and the 100 result cap at once. --- pgcommitfest/commitfest/ajax.py | 2 +- .../commitfest/tests/test_get_threads.py | 75 +++++-------------- pgcommitfest/settings.py | 2 - 3 files changed, 21 insertions(+), 58 deletions(-) diff --git a/pgcommitfest/commitfest/ajax.py b/pgcommitfest/commitfest/ajax.py index fa1eac69..1592e800 100644 --- a/pgcommitfest/commitfest/ajax.py +++ b/pgcommitfest/commitfest/ajax.py @@ -94,7 +94,7 @@ def getThreads(request): params["s"] = search messages = {} - for listname in settings.ARCHIVES_SEARCH_LISTS: + for listname in ("pgsql-hackers", "pgsql-bugs"): for m in _archivesAPI("/list/%s/latest.json" % listname, params): messages.setdefault(m["msgid"], m) diff --git a/pgcommitfest/commitfest/tests/test_get_threads.py b/pgcommitfest/commitfest/tests/test_get_threads.py index df240976..6ca209d6 100644 --- a/pgcommitfest/commitfest/tests/test_get_threads.py +++ b/pgcommitfest/commitfest/tests/test_get_threads.py @@ -5,69 +5,34 @@ from pgcommitfest.commitfest.ajax import getThreads -def _msg(msgid, date, subj): - return {"msgid": msgid, "date": date, "from": "a@example.com", "subj": subj} - - -HACKERS = [ - _msg("h1@example.com", "2026-01-03T00:00:00", "hackers thread"), - _msg("both@example.com", "2026-01-01T00:00:00", "sent to both lists"), -] -BUGS = [ - _msg("b1@example.com", "2026-01-02T00:00:00", "BUG #1: bugs thread"), - _msg("both@example.com", "2026-01-01T00:00:00", "sent to both lists"), -] - - -def _fake_archives(suburl, params=None): - return { - "/list/pgsql-hackers/latest.json": HACKERS, - "/list/pgsql-bugs/latest.json": BUGS, - }[suburl] +def _msg(msgid, date): + return {"msgid": msgid, "date": date, "from": "a@example.com", "subj": msgid} def test_get_threads_searches_hackers_and_bugs(): - """The thread search should return threads from pgsql-bugs too.""" - request = RequestFactory().get("/ajax/getThreads/", {"s": "thread", "a": "1"}) - - with patch( - "pgcommitfest.commitfest.ajax._archivesAPI", side_effect=_fake_archives - ) as api: - result = getThreads(request) - - # Both lists are searched with the same parameters - assert [c.args for c in api.call_args_list] == [ - ("/list/pgsql-hackers/latest.json", {"n": 100, "a": 1, "s": "thread"}), - ("/list/pgsql-bugs/latest.json", {"n": 100, "a": 1, "s": "thread"}), - ] - - # Results are merged newest first, and the message sent to both lists - # shows up only once - assert [m["msgid"] for m in result] == [ - "h1@example.com", - "b1@example.com", - "both@example.com", - ] - - -def test_get_threads_limits_merged_result(): - """Merging two lists should not return more than one list would.""" hackers = [ - _msg("h%d@example.com" % i, "2026-01-01T00:%02d:00" % (i % 60), "h") - for i in range(100) - ] - bugs = [ - _msg("b%d@example.com" % i, "2026-01-02T00:%02d:00" % (i % 60), "b") + _msg("h%d" % i, "2026-01-01T%02d:%02d:00" % (i // 60, i % 60)) for i in range(100) ] + # b1 is newer than everything on hackers, h99 was sent to both lists + bugs = [_msg("b1", "2026-01-02T00:00:00"), hackers[99]] def fake(suburl, params=None): - return hackers if "pgsql-hackers" in suburl else bugs + return { + "/list/pgsql-hackers/latest.json": hackers, + "/list/pgsql-bugs/latest.json": bugs, + }[suburl] - request = RequestFactory().get("/ajax/getThreads/") - with patch("pgcommitfest.commitfest.ajax._archivesAPI", side_effect=fake): + request = RequestFactory().get("/ajax/getThreads/", {"s": "x", "a": "1"}) + with patch("pgcommitfest.commitfest.ajax._archivesAPI", side_effect=fake) as api: result = getThreads(request) - assert len(result) == 100 - # The bugs messages are all newer, so they are the ones that are kept - assert all(m["msgid"].startswith("b") for m in result) + assert [c.args for c in api.call_args_list] == [ + ("/list/pgsql-hackers/latest.json", {"n": 100, "a": 1, "s": "x"}), + ("/list/pgsql-bugs/latest.json", {"n": 100, "a": 1, "s": "x"}), + ] + # Merged newest first, h99 deduplicated, and capped at 100 so the oldest + # message (h0) is dropped. + assert [m["msgid"] for m in result] == ["b1"] + [ + "h%d" % i for i in range(99, 0, -1) + ] diff --git a/pgcommitfest/settings.py b/pgcommitfest/settings.py index cdcd3121..07e4adaf 100644 --- a/pgcommitfest/settings.py +++ b/pgcommitfest/settings.py @@ -157,8 +157,6 @@ ARCHIVES_PORT = "8001" ARCHIVES_HOST = "archives.postgresql.org" # Host: header to send ARCHIVES_APIKEY = None -# Mailing lists searched when attaching a thread to a patch -ARCHIVES_SEARCH_LISTS = ["pgsql-hackers", "pgsql-bugs"] # Email address to pgsql-hackers. Set to something local to test maybe? HACKERS_EMAIL = "pgsql-hackers-testing@localhost"