backend/code_review_backend/issues/models.py (161 lines of code) (raw):
# 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/.
import urllib.parse
import uuid
from django.conf import settings
from django.db import models
from django.db.models import Q
LEVEL_WARNING = "warning"
LEVEL_ERROR = "error"
ISSUE_LEVELS = ((LEVEL_WARNING, "Warning"), (LEVEL_ERROR, "Error"))
class Repository(models.Model):
id = models.AutoField(primary_key=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
slug = models.SlugField(unique=True)
url = models.URLField(unique=True)
class Meta:
verbose_name_plural = "repositories"
ordering = ("id",)
def __str__(self):
return self.slug
class Revision(models.Model):
id = models.BigAutoField(primary_key=True)
# Phabricator references will be left empty when ingesting a decision task (e.g. from MC or autoland)
phabricator_id = models.PositiveIntegerField(unique=True, null=True, blank=True)
phabricator_phid = models.CharField(
max_length=40, unique=True, null=True, blank=True
)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
base_repository = models.ForeignKey(
Repository,
related_name="base_revisions",
on_delete=models.CASCADE,
help_text="Target repository where the revision has been produced and will land in the end",
)
head_repository = models.ForeignKey(
Repository,
related_name="head_revisions",
on_delete=models.CASCADE,
help_text="Repository where the revision is actually analyzed (e.g. Try for patches analysis)",
)
base_changeset = models.CharField(
max_length=40,
null=True,
blank=True,
help_text="Mercurial hash identifier on the base repository",
)
head_changeset = models.CharField(
max_length=40,
null=True,
blank=True,
help_text="Mercurial hash identifier on the analyze repository (only set for try pushes)",
)
title = models.CharField(max_length=250)
bugzilla_id = models.PositiveIntegerField(null=True)
class Meta:
ordering = ("phabricator_id", "id")
indexes = (models.Index(fields=["head_repository", "head_changeset"]),)
constraints = [
models.UniqueConstraint(
fields=["phabricator_id"],
name="revision_unique_phab_id",
condition=Q(phabricator_id__isnull=False),
),
models.UniqueConstraint(
fields=["phabricator_phid"],
name="revision_unique_phab_phabid",
condition=Q(phabricator_phid__isnull=False),
),
]
def __str__(self):
if self.phabricator_id is not None:
return f"D{self.phabricator_id} - {self.title}"
return f"#{self.id} - {self.title}"
@property
def phabricator_url(self):
if self.phabricator_id is None:
return
parser = urllib.parse.urlparse(settings.PHABRICATOR_HOST)
return f"{parser.scheme}://{parser.netloc}/D{self.phabricator_id}"
class Diff(models.Model):
"""Reference of a specific code patch (diff) in Phabricator.
A revision can be linked to multiple successive diffs, or none in case of a repository push.
"""
# Phabricator's attributes
id = models.PositiveIntegerField(primary_key=True)
phid = models.CharField(max_length=40, unique=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
revision = models.ForeignKey(
Revision, related_name="diffs", on_delete=models.CASCADE
)
review_task_id = models.CharField(max_length=30, unique=True)
mercurial_hash = models.CharField(max_length=40)
# The repository hosting this specific mercurial revision (try, autoland, ...)
repository = models.ForeignKey(
Repository, related_name="diffs", on_delete=models.CASCADE
)
def __str__(self):
return f"Diff {self.id}"
class Meta:
ordering = ("id",)
class IssueLink(models.Model):
"""Many-to-many relationship between an Issue and a Revision.
A Diff can be set to track issues evolution on a revision with multiple diffs.
"""
id = models.BigAutoField(primary_key=True)
revision = models.ForeignKey(
"issues.Revision",
on_delete=models.CASCADE,
related_name="issue_links",
)
issue = models.ForeignKey(
"issues.Issue",
on_delete=models.CASCADE,
related_name="issue_links",
)
diff = models.ForeignKey(
"issues.Diff",
on_delete=models.CASCADE,
related_name="issue_links",
null=True,
blank=True,
)
# Is this issue new for this revision ?
# Can be null (not set by API) when a revision is not linked to a diff
new_for_revision = models.BooleanField(null=True)
# Is this issue present in the patch ?
# Can be null (not set by API) when a revision is not linked to a diff
in_patch = models.BooleanField(null=True)
# Issue position on the file
line = models.PositiveIntegerField(null=True)
nb_lines = models.PositiveIntegerField(null=True)
char = models.PositiveIntegerField(null=True)
class Meta:
constraints = [
# Two constraints are required as Null values are not compared for unicity
models.UniqueConstraint(
fields=["issue", "revision", "line", "nb_lines", "char"],
name="issue_link_unique_revision",
condition=Q(diff__isnull=True),
),
models.UniqueConstraint(
fields=["issue", "revision", "diff", "line", "nb_lines", "char"],
name="issue_link_unique_diff",
condition=Q(diff__isnull=False),
),
]
@property
def publishable(self):
"""Is that issue publishable on Phabricator to developers"""
return self.in_patch is True or self.issue.level == LEVEL_ERROR
class Issue(models.Model):
"""An issue detected on a Phabricator patch"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
revisions = models.ManyToManyField(
"issues.Revision",
through="issues.IssueLink",
related_name="issues",
)
diffs = models.ManyToManyField(
"issues.Diff",
through="issues.IssueLink",
related_name="issues",
)
# Raw issue data
path = models.CharField(max_length=250)
level = models.CharField(max_length=20, choices=ISSUE_LEVELS)
analyzer_check = models.CharField(max_length=250, null=True)
message = models.TextField(null=True)
analyzer = models.CharField(max_length=50)
# Calculated hash identifying issue
hash = models.CharField(max_length=32, unique=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ("created",)
indexes = (
models.Index(fields=["hash"], name="issue_hash_idx"),
models.Index(fields=["path"]),
)