From patchwork Thu Jun 4 14:18:14 2026 Return-Path: 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 ; Thu, 04 Jun 2026 16:18:29 +0200 (CEST) From: Robin Jarry 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: X-Patchwork-Submitter: Robin Jarry X-Patchwork-Id: 102 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit 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 --- 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 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(