Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f57aba1
Generic provider fields
Jan 21, 2026
9392557
Fix migration
Jan 27, 2026
20b8770
Fix tests
Jan 27, 2026
a8ae2b6
Rename Diff.phid into provider_id
Jan 27, 2026
57fc291
Fix load_issues
Jan 27, 2026
e46a95f
Update in frontend
Jan 27, 2026
467e6a4
Filter revisions by provider in admin
Jan 27, 2026
3eb053c
Remove other phid
Feb 13, 2026
c1f495d
Run on github revision
Jan 14, 2026
97656e3
github in all
Jan 20, 2026
b31e455
Process github revision
Jan 20, 2026
5592447
Fix tests
Feb 13, 2026
dd717df
Base Github reporter + script to comment on a real issue
vrigal Jan 26, 2026
bf3c908
Add app/installations example
vrigal Feb 2, 2026
5cea762
Use pygithub library to handle API authentication
vrigal Feb 3, 2026
6abd0a7
Remove conflicting jwt library
vrigal Feb 5, 2026
3f0311e
Add a documentation about App authentication
vrigal Feb 12, 2026
eb2ba50
Publish a review with REQUEST_CHANGES
vrigal Feb 12, 2026
c488f11
Rebase on Github revision support
vrigal Feb 13, 2026
38b459a
Approve pull request with no issue
vrigal Feb 13, 2026
edd56b3
Add tests for the github reporter
vrigal Feb 13, 2026
6c10430
Remove the demo script
vrigal Feb 17, 2026
44961f5
Explicitly display the RSA key is fake
vrigal Feb 17, 2026
be9b18b
Fix reporter init when using against real revision
Feb 24, 2026
5de4902
Fix backend publication
Feb 24, 2026
468f615
WIP serialisation
Feb 25, 2026
155f0f4
Add a warning when trying to publish a review from an invalid revision
vrigal Feb 26, 2026
697a2e1
Update the documentation
vrigal Feb 26, 2026
7cf031d
Ignore Mercurial clone for Github revisions
vrigal Feb 26, 2026
e677889
Only publish publishable issues
vrigal Feb 26, 2026
1598779
Publish comments with line attribute rather than position
vrigal Feb 26, 2026
bdec7dd
Move get_file_content logic to revision scope
vrigal Feb 27, 2026
a78d2be
Add a --github-repository argument to the local repo cache
vrigal Feb 27, 2026
82baf3b
Update tests
vrigal Feb 27, 2026
60c9320
Publish revision and issues to the backend
vrigal Mar 2, 2026
3a00ea6
Append github to backend allowed repositories
vrigal Mar 2, 2026
21e445f
Update backend tests
vrigal Mar 3, 2026
7146bc8
Migration to use an UUID4 as Diff PK
vrigal Mar 6, 2026
8a18936
Only rely on provider_id
vrigal Mar 6, 2026
d571dcd
Update migration to preserve the integer Phabricator reference
vrigal Mar 10, 2026
1f53541
Rewrite migration to use an auto increment integer as default PK
vrigal Mar 10, 2026
c04fd42
Allow the bot to publish issues from Github
vrigal Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ repos:
rev: v2.2.4
hooks:
- id: codespell
exclude_types: [json]
exclude_types: [json, pem]
- repo: https://github.com/marco-c/taskcluster_yml_validator
rev: v0.0.11
hooks:
Expand Down
2 changes: 1 addition & 1 deletion backend/code_review_backend/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@
PHABRICATOR_HOST = "https://phabricator.services.mozilla.com"

# Limit the automatic creation of reositories to allowed hosts
ALLOWED_REPOSITORY_HOSTS = ["hg.mozilla.org"]
ALLOWED_REPOSITORY_HOSTS = ["hg.mozilla.org", "github.com"]

DYNO = env("DYNO")
# Heroku settings override to run the web app through dyno
Expand Down
17 changes: 14 additions & 3 deletions backend/code_review_backend/issues/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,30 @@ class RepositoryAdmin(admin.ModelAdmin):
class DiffInline(admin.TabularInline):
# Read only inline
model = Diff
readonly_fields = ("id", "repository", "mercurial_hash", "phid", "review_task_id")
readonly_fields = (
"id",
"repository",
"mercurial_hash",
"provider_id",
"review_task_id",
)


class RevisionAdmin(admin.ModelAdmin):
list_display = (
"id",
"phabricator_id",
"provider",
"provider_id",
"title",
"bugzilla_id",
"base_repository",
"head_repository",
)
list_filter = ("base_repository", "head_repository")
list_filter = (
"base_repository",
"head_repository",
"provider",
)
inlines = (DiffInline,)


Expand Down
24 changes: 16 additions & 8 deletions backend/code_review_backend/issues/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from datetime import date, datetime, timedelta

from django.conf import settings
from django.core.exceptions import BadRequest
from django.db.models import BooleanField, Count, ExpressionWrapper, Prefetch, Q
from django.db.models.functions import TruncDate
from django.shortcuts import get_object_or_404
Expand Down Expand Up @@ -77,10 +78,15 @@ def create(self, request, *args, **kwargs):
# When a revision already exists with that phabricator ID we return its data without creating a new one
# This value is used by the bot to identify a revision and publish new Phabricator diffs.
# The phabricator ID can be null (on mozilla-central) so we must always try to create a revision for that case
phabricator_id = request.data["phabricator_id"]
if phabricator_id is not None:
try:
provider = request.data["provider"]
provider_id = request.data["provider_id"]
except KeyError:
raise BadRequest("Invalid provider identification")
if provider_id is not None:
if revision := Revision.objects.filter(
phabricator_id=phabricator_id
provider=provider,
provider_id=provider_id,
).first():
serializer = RevisionSerializer(
instance=revision, context={"request": request}
Expand Down Expand Up @@ -166,7 +172,7 @@ def get_queryset(self):
if query is not None:
search_query = (
Q(id__icontains=query)
| Q(revision__phabricator_id__icontains=query)
| Q(revision__provider_id__icontains=query)
| Q(revision__bugzilla_id__icontains=query)
| Q(revision__title__icontains=query)
)
Expand Down Expand Up @@ -200,14 +206,14 @@ class IssueViewSet(

def get_queryset(self):
# Required to generate the OpenAPI documentation
if not self.kwargs.get("diff_id"):
if not self.kwargs.get("diff_provider_id"):
return Issue.objects.none()
diff = get_object_or_404(Diff, id=self.kwargs["diff_id"])
diff = get_object_or_404(Diff, provider_id=self.kwargs["diff_provider_id"])
return (
Issue.objects.filter(issue_links__diff=diff)
.annotate(publishable=Q(issue_links__in_patch=True) & Q(level=LEVEL_ERROR))
.values(
"id",
"provider_id",
"hash",
"analyzer",
"analyzer_check",
Expand Down Expand Up @@ -497,7 +503,9 @@ def get_queryset(self):
basename="revision-diffs",
)
router.register(r"diff", DiffViewSet, basename="diffs")
router.register(r"diff/(?P<diff_id>\d+)/issues", IssueViewSet, basename="issues")
router.register(
r"diff/(?P<diff_provider_id>[0-9a-zA-Z-]+)/issues", IssueViewSet, basename="issues"
)
urls = router.urls + [
path(
"revision/<int:revision_id>/issues/",
Expand Down
3 changes: 2 additions & 1 deletion backend/code_review_backend/issues/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def detect_new_for_revision(diff: Diff, path: str, hash: str) -> bool:
assert diff is not None, "Missing diff"
return not IssueLink.objects.filter(
revision_id=diff.revision_id,
diff_id__lt=diff.id,
# Ideally we would rely on the diff integer ID, but that information was lost adding the Github support
diff__created__lt=diff.created,
issue__path=path,
issue__hash=hash,
).exists()
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ def process_diff(diff: Diff):
detect_in_patch(issue_link, lines) for issue_link in diff.issue_links.all()
]
logging.info(
f"Found {len([i for i in issue_links if i.in_patch])} issue link in patch for {diff.id}"
f"Found {len([i for i in issue_links if i.in_patch])} issue link in patch for {diff.provider_id}"
)
IssueLink.objects.bulk_update(issue_links, ["in_patch"])
except Exception as e:
logging.info(f"Failure on diff {diff.id}: {e}")
logging.info(f"Failure on diff {diff.provider_id}: {e}")


class Command(BaseCommand):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ def build_revision_and_diff(self, data, task_id):
return None, None

revision, _ = head_repository.head_revisions.get_or_create(
phabricator_id=data["id"],
provider="phabricator",
provider_id=data["id"],
defaults={
"phabricator_phid": data["phid"],
"title": data["title"],
"bugzilla_id": int(data["bugzilla_id"])
if data["bugzilla_id"]
Expand All @@ -224,10 +224,9 @@ def build_revision_and_diff(self, data, task_id):
},
)
diff, _ = revision.diffs.get_or_create(
id=data["diff_id"],
provider_id=data["diff_phid"],
defaults={
"repository": head_repository,
"phid": data["diff_phid"],
"review_task_id": task_id,
"mercurial_hash": data["mercurial_revision"],
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated by Django 5.1.6 on 2026-01-21 16:01

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("issues", "0015_remove_repository_phid_alter_repository_id"),
]

operations = [
migrations.AlterModelOptions(
name="revision",
options={"ordering": ("provider", "provider_id", "id")},
),
migrations.RemoveConstraint(
model_name="revision",
name="revision_unique_phab_id",
),
migrations.RenameField(
model_name="revision",
old_name="phabricator_id",
new_name="provider_id",
),
migrations.AddField(
model_name="revision",
name="provider",
field=models.CharField(
choices=[("phabricator", "Phabricator"), ("github", "Github")],
default="phabricator",
max_length=20,
),
),
migrations.AddConstraint(
model_name="revision",
constraint=models.UniqueConstraint(
condition=models.Q(("provider_id__isnull", False)),
fields=("provider_id",),
name="revision_unique_phab_id",
),
),
migrations.RemoveConstraint(
model_name="revision",
name="revision_unique_phab_phabid",
),
migrations.RemoveField(
model_name="revision",
name="phabricator_phid",
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.1.6 on 2026-01-27 16:13

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("issues", "0016_alter_revision_options_and_more"),
]

operations = [
migrations.RenameField(
model_name="diff",
old_name="phid",
new_name="provider_id",
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from django.db import migrations, models
from django.db.models import F


def _update_provider_id(apps, schema_editor):
"""
Update all diffs, setting the current ID as the provider ID.
This step is required no to loose the Phabricator integer ID,
which is the reference used by most developers.
The previous value (PHID-DIFF-xxxxxxxxxxxxxxxxxxxx) is dropped,
then the PK is replaced by a random UUID, allowing the support
of other providers without a known unique integer identifier.
"""
Diff = apps.get_model("issues", "Diff")
Diff.objects.update(provider_id=F("id"))


def _reset_postgres_sequense(apps, schema_editor):
"""
PostgreSQL needs to update the auto increment sequence value, otherwise
conflicts would happen when we reach the initial Diff ID.

This command was initially generated with ./manage.py sqlsequencereset issues
"""
if schema_editor.connection.vendor != "postgresql":
# The SQLite backend automatically handles the auto increment update
return
schema_editor.execute(
"""SELECT setval(
pg_get_serial_sequence('"issues_diff"','id'),
coalesce(max("id"), 1),
max("id") IS NOT null
) FROM "issues_diff";
""",
)


class Migration(migrations.Migration):
dependencies = [
("issues", "0017_rename_phid_diff_provider_id"),
]

operations = [
# Save the Phabricator integer ID in Diff.provider_id
migrations.RunPython(
_update_provider_id,
reverse_code=migrations.RunPython.noop,
),
migrations.AlterField(
model_name="diff",
name="id",
field=models.AutoField(primary_key=True, serialize=False),
),
migrations.AlterModelOptions(
name="diff",
options={"ordering": ("created",)},
),
# Restore the sequence with PostgreSQL backend
migrations.RunPython(
_reset_postgres_sequense,
reverse_code=migrations.RunPython.noop,
),
]
Loading