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)