diff --git a/patchwork/admin.py b/patchwork/admin.py
index a264878..12dfa46 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
@@ -43,12 +44,27 @@ admin.site.register(User, UserAdmin)
class DelegationRuleInline(admin.TabularInline):
model = DelegationRule
fields = ('path', 'user', 'priority')
+ extra = 0
+
+
+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 0000000..36f041d
--- /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 0b23b3b..8bdb0c6 100644
--- a/patchwork/models.py
+++ b/patchwork/models.py
@@ -130,6 +130,54 @@ 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='',
+ blank=True,
+ 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(