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"]), )