Skip to content

Commit ad6f28a

Browse files
authored
Store base and head revision references (#1600)
1 parent d3fca60 commit ad6f28a

35 files changed

+561
-181
lines changed

backend/code_review_backend/issues/admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ class DiffInline(admin.TabularInline):
2222

2323

2424
class RevisionAdmin(admin.ModelAdmin):
25-
list_display = ("id", "title", "bugzilla_id", "repository")
26-
list_filter = ("repository",)
25+
list_display = ("id", "title", "bugzilla_id", "base_repository", "head_repository")
26+
list_filter = ("base_repository", "head_repository")
2727
inlines = (DiffInline,)
2828

2929

backend/code_review_backend/issues/api.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ def get_queryset(self):
109109
# Because of the perf. hit filter issues that are not older than today - 3 months.
110110
.filter(created__gte=date.today() - timedelta(days=90))
111111
.prefetch_related(
112-
"issues", "revision", "revision__repository", "repository"
112+
"issues",
113+
"revision",
114+
"revision__base_repository",
115+
"revision__head_repository",
116+
"repository",
113117
)
114118
.annotate(nb_issues=Count("issues"))
115119
.annotate(nb_errors=Count("issues", filter=Q(issues__level="error")))
@@ -127,7 +131,8 @@ def get_queryset(self):
127131
repository = self.request.query_params.get("repository")
128132
if repository is not None:
129133
diffs = diffs.filter(
130-
Q(revision__repository__slug=repository)
134+
Q(revision__base_repository__slug=repository)
135+
| Q(revision__head_repository__slug=repository)
131136
| Q(repository__slug=repository)
132137
)
133138

@@ -211,14 +216,16 @@ def get_queryset(self):
211216
repo = self.kwargs["repository"]
212217

213218
queryset = (
214-
Issue.objects.filter(revisions__repository__slug=repo)
219+
Issue.objects.filter(revisions__head_repository__slug=repo)
215220
.filter(analyzer=self.kwargs["analyzer"])
216221
.filter(analyzer_check=self.kwargs["check"])
217222
.prefetch_related(
218223
"diffs__repository",
219224
Prefetch(
220225
"diffs__revision",
221-
queryset=Revision.objects.select_related("repository"),
226+
queryset=Revision.objects.select_related(
227+
"base_repository", "head_repository"
228+
),
222229
),
223230
)
224231
.order_by("-created")
@@ -257,14 +264,14 @@ class IssueCheckStats(CachedView, generics.ListAPIView):
257264
def get_queryset(self):
258265
queryset = (
259266
Issue.objects.values(
260-
"revisions__repository__slug", "analyzer", "analyzer_check"
267+
"revisions__head_repository__slug", "analyzer", "analyzer_check"
261268
)
262269
# We want to count distinct issues because they can be referenced on multiple diffs
263270
.annotate(total=Count("id", distinct=True))
264271
.annotate(
265272
publishable=Count("id", filter=Q(in_patch=True) | Q(level=LEVEL_ERROR))
266273
)
267-
.distinct("revisions__repository__slug", "analyzer", "analyzer_check")
274+
.distinct("revisions__head_repository__slug", "analyzer", "analyzer_check")
268275
)
269276

270277
# Filter issues by date
@@ -281,7 +288,7 @@ def get_queryset(self):
281288
queryset = queryset.filter(revisions__created__gte=since).distinct()
282289

283290
return queryset.order_by(
284-
"-total", "revisions__repository__slug", "analyzer", "analyzer_check"
291+
"-total", "revisions__head_repository__slug", "analyzer", "analyzer_check"
285292
)
286293

287294

@@ -311,7 +318,10 @@ def get_queryset(self):
311318
# Filter by repository
312319
repository = self.request.query_params.get("repository")
313320
if repository:
314-
queryset = queryset.filter(diffs__revision__repository__slug=repository)
321+
queryset = queryset.filter(
322+
Q(diffs__revision__base_repository__slug=repository)
323+
| Q(diffs__revision__head_repository__slug=repository)
324+
)
315325

316326
# Filter by analyzer
317327
analyzer = self.request.query_params.get("analyzer")
@@ -362,7 +372,7 @@ def get_queryset(self):
362372
"invalid repo_slug path argument - No repository match this slug"
363373
)
364374
else:
365-
qs = qs.filter(revisions__repository=repo)
375+
qs = qs.filter(revisions__head_repository=repo)
366376

367377
# Always filter by path when the parameter is set
368378
if path := self.request.query_params.get("path"):

backend/code_review_backend/issues/management/commands/cleanup_issues.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def add_arguments(self, parser):
3131
def handle(self, *args, **options):
3232
clean_until = timezone.now() - timedelta(days=options["nb_days"])
3333
rev_to_delete = Revision.objects.filter(
34-
repository__slug__in=["autoland", "mozilla-central"],
34+
base_repository__slug__in=["autoland", "mozilla-central"],
35+
head_repository__slug__in=["autoland", "mozilla-central"],
3536
created__lte=clean_until,
3637
)
3738

backend/code_review_backend/issues/management/commands/load_issues.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,30 +179,39 @@ def load_local_reports(self):
179179
def build_revision_and_diff(self, data, task_id):
180180
"""Build or retrieve a revision and diff in current repo from report's data"""
181181
try:
182-
repository = Repository.objects.get(url=data["repository"])
182+
head_repository = Repository.objects.get(url=data["repository"])
183183
except Repository.DoesNotExist:
184184
logger.warning(
185185
f"No repository found with URL {data['repository']}, skipping."
186186
)
187187
return None, None
188-
revision, _ = repository.revisions.get_or_create(
188+
189+
try:
190+
base_repository = Repository.objects.get(url=data["target_repository"])
191+
except Repository.DoesNotExist:
192+
logger.warning(
193+
f"No repository found with URL {data['target_repository']}, skipping."
194+
)
195+
return None, None
196+
197+
revision, _ = head_repository.head_revisions.get_or_create(
189198
id=data["id"],
190199
defaults={
191200
"phid": data["phid"],
192201
"title": data["title"],
193202
"bugzilla_id": int(data["bugzilla_id"])
194203
if data["bugzilla_id"]
195204
else None,
205+
"base_repository": base_repository,
196206
},
197207
)
198208
diff, _ = revision.diffs.get_or_create(
199209
id=data["diff_id"],
200210
defaults={
201-
"repository": repository,
211+
"repository": head_repository,
202212
"phid": data["diff_phid"],
203213
"review_task_id": task_id,
204214
"mercurial_hash": data["mercurial_revision"],
205-
"repository": repository,
206215
},
207216
)
208217
return revision, diff
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# -*- coding: utf-8 -*-
2+
# This Source Code Form is subject to the terms of the Mozilla Public
3+
# License, v. 2.0. If a copy of the MPL was not distributed with this
4+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
import django.db.models.deletion
7+
from django.db import migrations
8+
from django.db import models
9+
from django.db.models import Count
10+
from django.db.models import F
11+
from django.db.models import OuterRef
12+
from django.db.models import Q
13+
from django.db.models import Subquery
14+
15+
16+
def update_revisions(apps, schema_editor):
17+
"""
18+
Detect the head repository depending on the current data:
19+
- all revisions without any diff use their current head repo
20+
- all revisions with a only diffs on autoland use it as their head repo
21+
"""
22+
Repository = apps.get_model("issues", "Repository")
23+
Revision = apps.get_model("issues", "Revision")
24+
Diff = apps.get_model("issues", "Diff")
25+
26+
# No need to run the following lines if the db is empty (e.g. during tests)
27+
if not Revision.objects.exists():
28+
return
29+
30+
# all revisions without any diff get their head repo as revision.repository_id
31+
Revision.objects.filter(diffs__isnull=True).update(
32+
head_repository_id=F("base_repository_id")
33+
)
34+
35+
# all revisions with only diff on autoland use it as their head repo
36+
autoland = Repository.objects.get(slug="autoland")
37+
others = Repository.objects.exclude(id=autoland.id)
38+
Revision.objects.annotate(
39+
other_diffs=Count("diffs", filter=Q(diffs__repository__in=others))
40+
).filter(other_diffs=0, diffs__repository_id=autoland.id).update(
41+
head_repository_id=autoland.id
42+
)
43+
44+
# all revisions without any diff excluding diffs with autoland repo
45+
# get their head repo as diff.repository_id
46+
rev_repos = (
47+
Diff.objects.filter(revision_id=OuterRef("id"))
48+
.exclude(repository_id=autoland.id)
49+
.values("repository_id")[:1]
50+
)
51+
Revision.objects.filter(head_repository_id__isnull=True).update(
52+
head_repository_id=Subquery(rev_repos)
53+
)
54+
55+
# Check no revisions miss their head repos
56+
assert not Revision.objects.filter(
57+
head_repository__isnull=True
58+
).exists(), "Some revisions miss their head repos"
59+
60+
61+
class Migration(migrations.Migration):
62+
dependencies = [
63+
("issues", "0007_issuelink_swap"),
64+
]
65+
66+
operations = [
67+
migrations.RenameField(
68+
model_name="revision",
69+
old_name="repository",
70+
new_name="base_repository",
71+
),
72+
migrations.AlterField(
73+
model_name="revision",
74+
name="base_repository",
75+
field=models.ForeignKey(
76+
help_text="Target repository where the revision has been produced and will land in the end",
77+
on_delete=django.db.models.deletion.CASCADE,
78+
related_name="base_revisions",
79+
to="issues.repository",
80+
),
81+
),
82+
migrations.AddField(
83+
model_name="revision",
84+
name="head_repository",
85+
# Make the field temporarily nullable so it can be set on existing revisions
86+
field=models.ForeignKey(
87+
to="issues.repository",
88+
on_delete=django.db.models.deletion.CASCADE,
89+
null=True,
90+
),
91+
),
92+
# Revision mercurial changeset reference is set to null on existing revisions
93+
migrations.AddField(
94+
model_name="revision",
95+
name="base_changeset",
96+
field=models.CharField(
97+
max_length=40,
98+
help_text="Mercurial hash identifier on the base repository",
99+
null=True,
100+
blank=True,
101+
),
102+
preserve_default=False,
103+
),
104+
# Head revision is set to null on existing revisions
105+
migrations.AddField(
106+
model_name="revision",
107+
name="head_changeset",
108+
field=models.CharField(
109+
help_text="Mercurial hash identifier on the analyze repository (only set for try pushes)",
110+
max_length=40,
111+
null=True,
112+
blank=True,
113+
),
114+
),
115+
migrations.RunPython(
116+
update_revisions,
117+
reverse_code=migrations.RunPython.noop,
118+
elidable=True,
119+
),
120+
# Make head_repository a required field
121+
migrations.AlterField(
122+
model_name="revision",
123+
name="head_repository",
124+
field=models.ForeignKey(
125+
help_text="Repository where the revision is actually analyzed (e.g. Try for patches analysis)",
126+
on_delete=django.db.models.deletion.CASCADE,
127+
related_name="head_revisions",
128+
to="issues.repository",
129+
null=False,
130+
),
131+
),
132+
]

backend/code_review_backend/issues/models.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,30 @@ def __str__(self):
4242

4343

4444
class Revision(PhabricatorModel):
45-
# The target repository where the revision will land in the end
46-
repository = models.ForeignKey(
47-
Repository, related_name="revisions", on_delete=models.CASCADE
45+
base_repository = models.ForeignKey(
46+
Repository,
47+
related_name="base_revisions",
48+
on_delete=models.CASCADE,
49+
help_text="Target repository where the revision has been produced and will land in the end",
50+
)
51+
head_repository = models.ForeignKey(
52+
Repository,
53+
related_name="head_revisions",
54+
on_delete=models.CASCADE,
55+
help_text="Repository where the revision is actually analyzed (e.g. Try for patches analysis)",
56+
)
57+
58+
base_changeset = models.CharField(
59+
max_length=40,
60+
null=True,
61+
blank=True,
62+
help_text="Mercurial hash identifier on the base repository",
63+
)
64+
head_changeset = models.CharField(
65+
max_length=40,
66+
null=True,
67+
blank=True,
68+
help_text="Mercurial hash identifier on the analyze repository (only set for try pushes)",
4869
)
4970

5071
title = models.CharField(max_length=250)

backend/code_review_backend/issues/serializers.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ class RevisionSerializer(serializers.ModelSerializer):
2828
Serialize a Revision in a Repository
2929
"""
3030

31-
repository = serializers.SlugRelatedField(
31+
base_repository = serializers.SlugRelatedField(
32+
queryset=Repository.objects.all(), slug_field="url"
33+
)
34+
head_repository = serializers.SlugRelatedField(
3235
queryset=Repository.objects.all(), slug_field="url"
3336
)
3437
diffs_url = serializers.HyperlinkedIdentityField(
@@ -43,7 +46,10 @@ class Meta:
4346
model = Revision
4447
fields = (
4548
"id",
46-
"repository",
49+
"base_repository",
50+
"head_repository",
51+
"base_changeset",
52+
"head_changeset",
4753
"phid",
4854
"title",
4955
"bugzilla_id",
@@ -58,14 +64,26 @@ class RevisionLightSerializer(serializers.ModelSerializer):
5864
Serialize a Revision in a Diff light serializer
5965
"""
6066

61-
repository = serializers.SlugRelatedField(
67+
base_repository = serializers.SlugRelatedField(
68+
queryset=Repository.objects.all(), slug_field="url"
69+
)
70+
head_repository = serializers.SlugRelatedField(
6271
queryset=Repository.objects.all(), slug_field="url"
6372
)
6473
phabricator_url = serializers.URLField(read_only=True)
6574

6675
class Meta:
6776
model = Revision
68-
fields = ("id", "repository", "title", "bugzilla_id", "phabricator_url")
77+
fields = (
78+
"id",
79+
"base_repository",
80+
"head_repository",
81+
"base_changeset",
82+
"head_changeset",
83+
"title",
84+
"bugzilla_id",
85+
"phabricator_url",
86+
)
6987

7088

7189
class DiffSerializer(serializers.ModelSerializer):
@@ -224,7 +242,7 @@ class IssueCheckStatsSerializer(serializers.Serializer):
224242
"""
225243

226244
# The view aggregates issues depending on their reference to a repository (via IssueLink M2M)
227-
repository = serializers.SlugField(source="revisions__repository__slug")
245+
repository = serializers.SlugField(source="revisions__head_repository__slug")
228246
analyzer = serializers.CharField()
229247
check = serializers.CharField(source="analyzer_check")
230248
total = serializers.IntegerField()

0 commit comments

Comments
 (0)