mbox series
Message ID20260604121203.2955783-14-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 F31B21BC434F
	for <pw@patches.jarry.cc>; Thu, 04 Jun 2026 14:12:10 +0200 (CEST)
From: Robin Jarry <robin@jarry.cc>
To: pw@patches.jarry.cc
Subject: [PATCH 13/15] forge: forward mailing list comments to forge pull
 requests
Date: Thu,  4 Jun 2026 14:12:01 +0200
Message-ID: <20260604121203.2955783-14-robin@jarry.cc>
X-Mailer: git-send-email 2.54.0
In-Reply-To: <20260604121203.2955783-1-robin@jarry.cc>
References: <20260604121203.2955783-1-robin@jarry.cc>
MIME-Version: 1.0
List-ID: <pw.jarry.cc>
Content-Transfer-Encoding: 8bit
Series
Implement forge bidirectional sync

Commit Message

Robin JarryJun. 4, 2026, 14:12. UTC
[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>
---
 patchwork/forge/__init__.py        | 48 ++++++++++++++++++++++++++++++
 patchwork/forge/github/__init__.py |  4 +++
 patchwork/forge/github/from_ml.py  | 47 ++++++++++++++++++++++-------
 3 files changed, 88 insertions(+), 11 deletions(-)

Patch

mbox series
diff --git a/patchwork/forge/__init__.py b/patchwork/forge/__init__.py
index 25c76c26e1c1..8ac13d747874 100644
--- a/patchwork/forge/__init__.py
+++ b/patchwork/forge/__init__.py
@@ -182,6 +182,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 = {}
 
@@ -219,6 +229,43 @@ 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
 
@@ -229,3 +276,4 @@ def load_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/from_ml.py b/patchwork/forge/github/from_ml.py
index f648711ff0f3..1db2acc5463b 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 json
 import logging
 import urllib.error
 import urllib.request
 
 from patchwork.forge.git import GitMirror
+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
@@ -60,6 +62,17 @@ def create_pr(gh, forge_config, title, body, head, base):
     return result['number']
 
 
+def post_comment(gh, forge_config, pr_number, body):
+    owner, repo = forge_config.repo.split('/', 1)
+    gh_api_request(
+        gh,
+        forge_config,
+        'POST',
+        f'/repos/{owner}/{repo}/issues/{pr_number}/comments',
+        {'body': body},
+    )
+
+
 def base_branch(gh, forge_config):
     """
     Return the default branch of the GitHub repository.
@@ -74,17 +87,6 @@ def base_branch(gh, forge_config):
     return result['default_branch']
 
 
-def post_comment(gh, forge_config, pr_number, body):
-    owner, repo = forge_config.repo.split('/', 1)
-    gh_api_request(
-        gh,
-        forge_config,
-        'POST',
-        f'/repos/{owner}/{repo}/issues/{pr_number}/comments',
-        {'body': body},
-    )
-
-
 def create_or_update_pr(gh, forge_config, series):
     if not forge_config.sync_ml_to_forge:
         return
@@ -172,3 +174,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)