mbox series
Message ID20260604141826.2998337-6-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 AF8C31BC4350
	for <pw@patches.jarry.cc>; Thu, 04 Jun 2026 16:18:29 +0200 (CEST)
From: Robin Jarry <robin@jarry.cc>
To: pw@patches.jarry.cc
Subject: [PATCH v3 05/16] forge: add per-project configuration model
Date: Thu,  4 Jun 2026 16:18:14 +0200
Message-ID: <20260604141826.2998337-6-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_prhttps://github.com/rjarry/patchwork/pull/4
github_branchpatchwork/implement-forge-bidirectional-sync-c

Commit Message

Robin JarryJun. 4, 2026, 16:18. UTC
[v3,05/16] forge: add per-project configuration model

Add a ForgeConfig model that links a patchwork project to a forge
repository. Each project can have one configuration per backend type,
with independent sync direction flags (ml-to-forge and forge-to-ml)
and a thread_respins toggle to control whether respin series are
threaded under the original version.

A from_email field specifies the sender address for forge-originated
emails, with a sender_email property that falls back to
DEFAULT_FROM_EMAIL when unset.

A tabular inline on the Project admin page allows maintainers to
configure forge coupling directly.

Signed-off-by: Robin Jarry <robin@jarry.cc>
---
 patchwork/admin.py                       | 15 ++++++++
 patchwork/migrations/0051_forgeconfig.py | 46 +++++++++++++++++++++++
 patchwork/models.py                      | 47 ++++++++++++++++++++++++
 3 files changed, 108 insertions(+)
 create mode 100644 patchwork/migrations/0051_forgeconfig.py

Patch

mbox series
diff --git a/patchwork/admin.py b/patchwork/admin.py
index a26487850936..e138ad4f2f8e 100644
--- a/patchwork/admin.py
+++ b/patchwork/admin.py
@@ -13,6 +13,7 @@ from patchwork.models import Check
 from patchwork.models import Cover
 from patchwork.models import CoverComment
 from patchwork.models import DelegationRule
+from patchwork.models import ForgeConfig
 from patchwork.models import Patch
 from patchwork.models import PatchComment
 from patchwork.models import PatchRelation
@@ -45,10 +46,24 @@ class DelegationRuleInline(admin.TabularInline):
     fields = ('path', 'user', 'priority')
 
 
+class ForgeConfigInline(admin.TabularInline):
+    model = ForgeConfig
+    fields = (
+        'backend',
+        'repo',
+        'from_email',
+        'sync_ml_to_forge',
+        'sync_forge_to_ml',
+        'thread_respins',
+    )
+    extra = 0
+
+
 class ProjectAdmin(admin.ModelAdmin):
     list_display = ('name', 'linkname', 'listid', 'listemail')
     inlines = [
         DelegationRuleInline,
+        ForgeConfigInline,
     ]
 
 
diff --git a/patchwork/migrations/0051_forgeconfig.py b/patchwork/migrations/0051_forgeconfig.py
new file mode 100644
index 000000000000..36f041d24021
--- /dev/null
+++ b/patchwork/migrations/0051_forgeconfig.py
@@ -0,0 +1,46 @@
+# Generated by Django 5.1.15 on 2026-05-30 00:32
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ('patchwork', '0050_series_metadata'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ForgeConfig',
+            fields=[
+                (
+                    'id',
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name='ID',
+                    ),
+                ),
+                ('backend', models.CharField(max_length=50)),
+                ('repo', models.CharField(max_length=255)),
+                ('from_email', models.CharField(default='', max_length=255)),
+                ('sync_ml_to_forge', models.BooleanField(default=True)),
+                ('sync_forge_to_ml', models.BooleanField(default=True)),
+                ('thread_respins', models.BooleanField(default=False)),
+                (
+                    'project',
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name='forges',
+                        to='patchwork.project',
+                    ),
+                ),
+            ],
+            options={
+                'verbose_name': 'forge configuration',
+                'verbose_name_plural': 'forge configurations',
+                'unique_together': {('project', 'backend')},
+            },
+        ),
+    ]
diff --git a/patchwork/models.py b/patchwork/models.py
index 0b23b3bc495f..3f7974d92ccb 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -130,6 +130,53 @@ class Project(models.Model):
         ordering = ['linkname']
 
 
+class ForgeConfig(models.Model):
+    project = models.ForeignKey(
+        Project, related_name='forges', on_delete=models.CASCADE
+    )
+    backend = models.CharField(
+        max_length=50,
+        help_text='Forge backend name (e.g. "github").',
+    )
+    repo = models.CharField(
+        max_length=255,
+        help_text='Repository identifier (e.g. "owner/repo").',
+    )
+    from_email = models.CharField(
+        max_length=255,
+        default='',
+        help_text='Sender address for forge-originated emails.',
+    )
+    sync_ml_to_forge = models.BooleanField(
+        default=True,
+        help_text='Create forge PRs from mailing list patch series.',
+    )
+    sync_forge_to_ml = models.BooleanField(
+        default=True,
+        help_text='Send patch emails from forge PRs to the mailing list.',
+    )
+    thread_respins = models.BooleanField(
+        default=False,
+        help_text='Thread respin series as replies to the original version.',
+    )
+
+    @property
+    def sender_email(self):
+        """
+        Return from_email if set, otherwise fall back to
+        DEFAULT_FROM_EMAIL.
+        """
+        return self.from_email or settings.DEFAULT_FROM_EMAIL
+
+    def __str__(self):
+        return '%s (%s)' % (self.repo, self.backend)
+
+    class Meta:
+        verbose_name = 'forge configuration'
+        verbose_name_plural = 'forge configurations'
+        unique_together = [('project', 'backend')]
+
+
 class DelegationRule(models.Model):
     project = models.ForeignKey(Project, on_delete=models.CASCADE)
     user = models.ForeignKey(