From patchwork Tue Jun 2 12:10:12 2026 Message-ID: In-Reply-To: References: From: Robin Jarry Date: Tue, 2 Jun 2026 14:10:12 +0200 Subject: [PATCH patchwork v4 13/15] forge: forward mailing list comments to forge pull requests Sender: pw@patches.jarry.cc Reply-To: pw@patches.jarry.cc List-ID: X-Patchwork-Hint: ignore To: pw@patches.jarry.cc Cc: Robin Jarry , Robin Jarry X-Patchwork-Submitter: Robin Jarry X-Patchwork-Id: 126 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Add a signal handler for patch and cover letter comment events that forwards them to the linked forge pull request. Comments are posted with the original author name (without email address) and the comment body formatted as a GitHub quote block. Comments that originated from the forge (detected by X-Patchwork-Hint: ignore in the comment headers) are skipped to prevent sync loops. Signed-off-by: Robin Jarry --- Notes: https://github.com/rjarry/patchwork/pull/3/commits/ff3dd0bb37af5dd582b51bbc67694e1d0b552c32 patchwork/forge/__init__.py | 51 ++++++++++++++++++++++++++++-- patchwork/forge/github/__init__.py | 4 +++ patchwork/forge/github/api.py | 3 +- patchwork/forge/github/from_ml.py | 25 +++++++++++++++ 4 files changed, 79 insertions(+), 4 deletions(-) diff --git a/patchwork/forge/__init__.py b/patchwork/forge/__init__.py index cadca53..79a1366 100644 --- a/patchwork/forge/__init__.py +++ b/patchwork/forge/__init__.py @@ -28,6 +28,7 @@ from dataclasses import field from django.conf import settings from django.db import transaction +from django.db.models.signals import post_save from patchwork.models import Event from patchwork.models import ForgeConfig @@ -180,6 +181,16 @@ class ForgeBackend(ABC): """ raise NotImplementedError + @abstractmethod + def handle_comment_created(self, forge_config, comment, series): + """ + Handle a comment on a patch or cover letter. + + Called by the comment-created signal handler. The backend + decides whether to forward the comment to the forge PR. + """ + raise NotImplementedError + _backends = {} @@ -217,12 +228,46 @@ def _on_series_completed(sender, instance, raw, **kwargs): transaction.on_commit(do_sync) -def load_backends(): - from django.db.models.signals import post_save +def _on_comment_created(sender, instance, raw, **kwargs): + if raw: + return + + if instance.category == Event.CATEGORY_PATCH_COMMENT_CREATED: + comment = instance.patch_comment + if not comment: + return + series = comment.patch.series + elif instance.category == Event.CATEGORY_COVER_COMMENT_CREATED: + comment = instance.cover_comment + if not comment: + return + series = comment.cover.series + else: + return + + if not series: + return + + def do_sync(): + for forge_config in ForgeConfig.objects.filter(project=series.project): + backend = get_backend(forge_config.backend) + if not backend: + continue + try: + backend.handle_comment_created(forge_config, comment, series) + except Exception: + logger.exception( + 'forge comment sync failed for series %d on %s', + series.id, + forge_config.backend, + ) + + transaction.on_commit(do_sync) - from patchwork.models import Event +def load_backends(): for module_path in settings.FORGE_BACKENDS: importlib.import_module(module_path) post_save.connect(_on_series_completed, sender=Event) + post_save.connect(_on_comment_created, sender=Event) diff --git a/patchwork/forge/github/__init__.py b/patchwork/forge/github/__init__.py index b2dbf23..18200a4 100644 --- a/patchwork/forge/github/__init__.py +++ b/patchwork/forge/github/__init__.py @@ -15,6 +15,7 @@ import logging from patchwork.forge import ForgeBackend from patchwork.forge import register_backend from patchwork.forge.github.from_ml import create_or_update_pr +from patchwork.forge.github.from_ml import post_pr_comment from patchwork.forge.github.to_ml import handle_check_pending from patchwork.forge.github.to_ml import handle_check_result from patchwork.forge.github.to_ml import handle_issue_comment @@ -99,5 +100,8 @@ class GitHubBackend(ForgeBackend): def handle_series_completed(self, forge_config, series): create_or_update_pr(self, forge_config, series) + def handle_comment_created(self, forge_config, comment, series): + post_pr_comment(self, forge_config, comment, series) + register_backend('github', GitHubBackend()) diff --git a/patchwork/forge/github/api.py b/patchwork/forge/github/api.py index d2332a7..92b4a73 100644 --- a/patchwork/forge/github/api.py +++ b/patchwork/forge/github/api.py @@ -11,6 +11,7 @@ import urllib.request from patchwork.forge import CheckRun from patchwork.forge import ReviewComment +from patchwork.forge.util import COMMENT_MARKER logger = logging.getLogger(__name__) @@ -55,7 +56,7 @@ def fetch_review_comments(gh, forge_config, pr_number, review_id): comments = [] for r in results: body = r.get('body') or '' - if body == '': + if body == '' or COMMENT_MARKER in body: continue comments.append( ReviewComment( diff --git a/patchwork/forge/github/from_ml.py b/patchwork/forge/github/from_ml.py index ccb44c8..4b1acf4 100644 --- a/patchwork/forge/github/from_ml.py +++ b/patchwork/forge/github/from_ml.py @@ -4,12 +4,14 @@ # SPDX-License-Identifier: GPL-2.0-or-later +import email import logging from patchwork.forge.git import GitMirror from patchwork.forge.github.api import base_branch from patchwork.forge.github.api import create_pr from patchwork.forge.github.api import post_comment +from patchwork.forge.util import COMMENT_MARKER from patchwork.forge.util import build_pr_body from patchwork.forge.util import forge_branch_name from patchwork.forge.util import series_from_forge @@ -106,3 +108,26 @@ def store_series_metadata(gh, forge_config, series, pr_ref, branch): key=f'{forge_config.backend}_branch', defaults={'value': branch}, ) + + +def post_pr_comment(gh, forge_config, comment, series): + if not forge_config.sync_ml_to_forge: + return + + if comment.headers: + parsed = email.message_from_string(comment.headers) + hint = parsed.get('X-Patchwork-Hint', '') + if hint.lower() == 'ignore': + return + + pr_meta = SeriesMetadata.objects.filter( + series=series, key=gh.meta_key_pr() + ).first() + if not pr_meta: + return + + pr_number = int(pr_meta.value.rsplit('/', 1)[-1]) + author = comment.submitter.name or comment.submitter.email + quoted = '\n'.join(f'> {line}' for line in comment.content.splitlines()) + body = f'**{author}** commented:\n\n{quoted}\n\n{COMMENT_MARKER}' + post_comment(gh, forge_config, pr_number, body)