mbox series
Message ID20260604141826.2998337-15-robin@jarry.cc
StateNew
Delegate
ArchivedNo
Headers
show
Return-Path: <robin@jarry.cc>
Received: from ringo (2a01cb00021ec0002e23edbec21b0e73.ipv6.abo.wanadoo.fr
 [IPv6:2a01:cb00:21e:c000:2e23:edbe:c21b:e73])
	by patches.jarry.cc (Postfix) with ESMTP id 8F4FC1BC4359
	for <pw@patches.jarry.cc>; Thu, 04 Jun 2026 16:18:30 +0200 (CEST)
From: Robin Jarry <robin@jarry.cc>
To: pw@patches.jarry.cc
Subject: [PATCH v3 14/16] forge: forward mailing list comments to forge pull
 requests
Date: Thu,  4 Jun 2026 16:18:23 +0200
Message-ID: <20260604141826.2998337-15-robin@jarry.cc>
X-Mailer: git-send-email 2.54.0
In-Reply-To: <20260604141826.2998337-1-robin@jarry.cc>
References: <20260604141826.2998337-1-robin@jarry.cc>
MIME-Version: 1.0
List-ID: <pw.jarry.cc>
Content-Transfer-Encoding: 8bit
Series
Implement forge bidirectional sync
github_branchpatchwork/implement-forge-bidirectional-sync-c
github_prhttps://github.com/rjarry/patchwork/pull/4

Commit Message

Robin JarryJun. 4, 2026, 16:18. UTC
[v3,14/16] forge: forward mailing list comments to forge pull requests

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 <robin@jarry.cc>
---
 patchwork/forge/__init__.py        | 53 +++++++++++++++++++++++++++---
 patchwork/forge/github/__init__.py |  4 +++
 patchwork/forge/github/api.py      |  3 +-
 patchwork/forge/github/from_ml.py  | 25 ++++++++++++++
 4 files changed, 80 insertions(+), 5 deletions(-)

Patch

mbox series
diff --git a/patchwork/forge/__init__.py b/patchwork/forge/__init__.py
index cadca530daf8..79a136602d6e 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 _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)
+
+
 def load_backends():
-    from django.db.models.signals import post_save
-
-    from patchwork.models import Event
-
     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 b2dbf23a5ee7..18200a4fdb23 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 d2332a70f0ae..92b4a73bc161 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 ccb44c8a78d3..4b1acf4f424c 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)