mbox series
Message IDff3dd0bb37af5dd582b51bbc67694e1d0b552c32.1780583783.git.pw@patches.jarry.cc
StateNew
Delegate
ArchivedNo
Headers
show
Message-ID: 
 <ff3dd0bb37af5dd582b51bbc67694e1d0b552c32.1780583783.git.pw@patches.jarry.cc>
In-Reply-To: <cover.1780583783.git.pw@patches.jarry.cc>
References: <cover.1780583783.git.pw@patches.jarry.cc>
From: Robin Jarry <robin@jarry.cc>
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: <pw.jarry.cc>
X-Patchwork-Hint: ignore
To: pw@patches.jarry.cc
Cc: Robin Jarry <rjarry@redhat.com>,
    Robin Jarry <robin@jarry.cc>
Series
Forge ml sync
github_prhttps://github.com/rjarry/patchwork/pull/3
github_branchforge

Commit Message

Robin JarryJun. 2, 2026, 14:10. UTC
[v4,13/15] 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>
---

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(-)

Patch

mbox series
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)