Exemple #1
0
class Review(object):
    def __init__(self, review_id, owners, review_type, branch, state, serial, summary, description, applyfilters, applyparentfilters):
        self.id = review_id
        self.owners = owners
        self.type = review_type
        self.repository = branch.repository
        self.branch = branch
        self.state = state
        self.serial = serial
        self.summary = summary
        self.description = description
        self.reviewers = []
        self.watchers = {}
        self.commentchains = None
        self.applyfilters = applyfilters
        self.applyparentfilters = applyparentfilters
        self.filters = None
        self.relevant_files = None
        self.draft_status = None
        self.performed_rebase = None

    @staticmethod
    def isAccepted(db, review_id):
        cursor = db.cursor()

        cursor.execute("SELECT 1 FROM reviewfiles WHERE review=%s AND state='pending' LIMIT 1", (review_id,))
        if cursor.fetchone(): return False

        cursor.execute("SELECT 1 FROM commentchains WHERE review=%s AND type='issue' AND state='open' LIMIT 1", (review_id,))
        if cursor.fetchone(): return False

        return True

    def accepted(self, db):
        if self.state != 'open': return False
        else: return Review.isAccepted(db, self.id)

    def getReviewState(self, db):
        cursor = db.cursor()

        cursor.execute("""SELECT state, SUM(deleted) + SUM(inserted)
                            FROM reviewfiles
                           WHERE reviewfiles.review=%s
                        GROUP BY state""",
                       (self.id,))

        pending = 0
        reviewed = 0

        for state, count in cursor.fetchall():
            if state == "pending": pending = count
            else: reviewed = count

        cursor.execute("""SELECT count(id)
                            FROM commentchains
                           WHERE review=%s
                             AND type='issue'
                             AND state='open'""",
                       (self.id,))

        issues = cursor.fetchone()[0]

        return ReviewState(self, self.accepted(db), pending, reviewed, issues)

    def setPerformedRebase(self, old_head, new_head, old_upstream, new_upstream, user):
        self.performed_rebase = ReviewRebase(self, old_head, new_head, old_upstream, new_upstream, user)

    def getReviewRebases(self, db):
        return ReviewRebases(db, self)

    def getTrackedBranch(self, db):
        cursor = db.cursor()
        cursor.execute("""SELECT trackedbranches.id, remote, remote_name, disabled
                            FROM trackedbranches
                            JOIN branches ON (trackedbranches.repository=branches.repository
                                          AND trackedbranches.local_name=branches.name)
                            JOIN reviews ON (branches.id=reviews.branch)
                           WHERE reviews.id=%s""",
                       (self.id,))

        for trackedbranch_id, remote, name, disabled in cursor:
            return ReviewTrackedBranch(self, trackedbranch_id, remote, name, disabled)

    def getCommitSet(self, db):
        import gitutils
        import log.commitset

        cursor = db.cursor()
        cursor.execute("""SELECT DISTINCT commits.id, commits.sha1
                            FROM commits
                            JOIN changesets ON (changesets.child=commits.id)
                            JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id)
                           WHERE reviewchangesets.review=%s""",
                       (self.id,))

        commits = []

        for commit_id, commit_sha1 in cursor:
            commits.append(gitutils.Commit.fromSHA1(db, self.repository, commit_sha1, commit_id))

        return log.commitset.CommitSet(commits)

    def containsCommit(self, db, commit, include_head_and_tails=False, include_actual_log=False):
        import gitutils

        commit_id = None
        commit_sha1 = None

        if isinstance(commit, gitutils.Commit):
            commit_id = commit.id
            commit_sha1 = commit.sha1
        elif isinstance(commit, str):
            commit_sha1 = self.repository.revparse(commit)
            commit = None
        elif isinstance(commit, int):
            commit_id = commit
            commit = None
        else:
            raise TypeError

        cursor = db.cursor()

        if commit_id is not None:
            cursor.execute("""SELECT 1
                                FROM reviewchangesets
                                JOIN changesets ON (id=changeset)
                               WHERE reviewchangesets.review=%s
                                 AND changesets.child=%s
                                 AND changesets.type!='conflicts'""",
                           (self.id, commit_id))
        else:
            cursor.execute("""SELECT 1
                                FROM reviewchangesets
                                JOIN changesets ON (changesets.id=reviewchangesets.changeset)
                                JOIN commits ON (commits.id=changesets.child)
                               WHERE reviewchangesets.review=%s
                                 AND changesets.type!='conflicts'
                                 AND commits.sha1=%s""",
                           (self.id, commit_sha1))

        if cursor.fetchone() is not None:
            return True

        if include_head_and_tails:
            head_and_tails = set([self.branch.getHead(db)])

            commitset = self.getCommitSet(db)

            if commitset:
                head_and_tails |= commitset.getTails()

            if commit_sha1 is None:
                if commit is None:
                    commit = gitutils.Commit.fromId(db, self.repository, commit_id)
                commit_sha1 = commit.sha1

            if commit_sha1 in head_and_tails:
                return True

        if include_actual_log:
            if commit_id is not None:
                cursor.execute("""SELECT 1
                                    FROM reachable
                                    JOIN branches ON (branches.id=reachable.branch)
                                    JOIN reviews ON (reviews.branch=branches.id)
                                   WHERE reachable.commit=%s
                                     AND reviews.id=%s""",
                               (commit_id, self.id))
            else:
                cursor.execute("""SELECT 1
                                    FROM commits
                                    JOIN reachable ON (reachable.commit=commits.id)
                                    JOIN branches ON (branches.id=reachable.branch)
                                    JOIN reviews ON (reviews.branch=branches.id)
                                   WHERE commits.sha1=%s
                                     AND reviews.id=%s""",
                               (commit_sha1, self.id))

            if cursor.fetchone() is not None:
                return True

        return False

    def getJS(self):
        return "var review = critic.review = { id: %d, branch: { id: %d, name: %r }, owners: [ %s ], serial: %d };" % (self.id, self.branch.id, self.branch.name, ", ".join(owner.getJSConstructor() for owner in self.owners), self.serial)

    def getETag(self, db, user=None):
        import configuration

        cursor = db.cursor()
        etag = ""

        if configuration.debug.IS_DEVELOPMENT:
            cursor.execute("SELECT installed_at FROM systemidentities WHERE name=%s", (configuration.base.SYSTEM_IDENTITY,))
            installed_at = cursor.fetchone()[0]
            etag += "install%s." % time.mktime(installed_at.timetuple())

        if user and not user.isAnonymous():
            etag += "user%d." % user.id

        etag += "review%d.serial%d" % (self.id, self.serial)

        if user:
            items = self.getDraftStatus(db, user)
            if any(items.values()):
                etag += ".draft%d" % hash(tuple(sorted(items.items())))

            cursor.execute("SELECT id FROM reviewrebases WHERE review=%s AND uid=%s AND new_head IS NULL", (self.id, user.id))
            row = cursor.fetchone()
            if row:
                etag += ".rebase%d" % row[0]

        return '"%s"' % etag

    def getURL(self, db, user=None, indent=0, separator="\n"):
        import dbutils

        indent = " " * indent

        if user:
            url_prefixes = user.getCriticURLs(db)
        else:
            url_prefixes = [dbutils.getURLPrefix(db)]

        return separator.join(["%s%s/r/%d" % (indent, url_prefix, self.id) for url_prefix in url_prefixes])

    def getRecipients(self, db):
        from dbutils import User

        cursor = db.cursor()
        cursor.execute("SELECT uid, include FROM reviewrecipientfilters WHERE review=%s", (self.id,))

        default_include = True
        included = set(owner.id for owner in self.owners)
        excluded = set()

        for uid, include in cursor:
            if uid is None:
                default_include = include
            elif include:
                included.add(uid)
            elif uid not in self.owners:
                excluded.add(uid)

        cursor.execute("SELECT uid FROM reviewusers WHERE review=%s", (self.id,))

        recipients = []
        for (user_id,) in cursor:
            if user_id in excluded:
                continue
            elif user_id not in included and not default_include:
                continue

            user = User.fromId(db, user_id)
            if user.status != "retired":
                recipients.append(user)

        return recipients

    def getDraftStatus(self, db, user):
        if self.draft_status is None:
            self.draft_status = countDraftItems(db, user, self)
        return self.draft_status

    def incrementSerial(self, db):
        self.serial += 1
        db.cursor().execute("UPDATE reviews SET serial=%s WHERE id=%s", [self.serial, self.id])

    def scheduleBranchArchival(self, db, delay=None):
        import dbutils

        # First, cancel current scheduled archival, if there is one.
        self.cancelScheduledBranchArchival(db)

        # If review is not closed or dropped, don't schedule a branch archival.
        # Also don't schedule one if the branch has already been archived.
        if self.state not in ("closed", "dropped") or self.branch.archived:
            return

        if delay is None:
            # Configuration policy:
            #
            # Any owner of a review can, by having changed the relevant
            # preference setting, increase the time before a review branch is
            # archived, or disable archival entirely, but they can't make it
            # happen sooner than the system or repository default, or what any
            # other owner has requested.

            # Find configured value for each owner, and also the per-repository
            # (or per-system) default, in case each owner has changed the
            # setting.
            preference_item = "review.branchArchiveDelay." + self.state
            repository_default = dbutils.User.fetchPreference(
                db, preference_item, repository=self.repository)
            delays = set([repository_default])
            for owner in self.owners:
                delays.add(owner.getPreference(db, preference_item,
                                               repository=self.repository))

            # If configured to zero (by any owner,) don't schedule a branch
            # archival.
            if min(delays) <= 0:
                return

            # Otherwise, use maximum configured value for any owner.
            delay = max(delays)

        cursor = db.cursor()
        cursor.execute("""INSERT INTO scheduledreviewbrancharchivals (review, deadline)
                               VALUES (%s, NOW() + INTERVAL %s)""",
                       (self.id, "%d DAYS" % delay))

        return delay

    def cancelScheduledBranchArchival(self, db):
        cursor = db.cursor()
        cursor.execute("""DELETE FROM scheduledreviewbrancharchivals
                                WHERE review=%s""",
                       (self.id,))

    def close(self, db, user):
        self.serial += 1
        self.state = "closed"
        db.cursor().execute("UPDATE reviews SET state='closed', serial=%s, closed_by=%s WHERE id=%s", (self.serial, user.id, self.id))
        self.scheduleBranchArchival(db)

    def drop(self, db, user):
        self.serial += 1
        self.state = "dropped"
        db.cursor().execute("UPDATE reviews SET state='dropped', serial=%s, closed_by=%s WHERE id=%s", (self.serial, user.id, self.id))
        self.scheduleBranchArchival(db)

    def reopen(self, db, user):
        self.serial += 1
        if self.branch.archived:
            self.branch.resurrect(db)
        db.cursor().execute("UPDATE reviews SET state='open', serial=%s, closed_by=NULL WHERE id=%s", (self.serial, self.id))
        self.cancelScheduledBranchArchival(db)

    def disableTracking(self, db):
        db.cursor().execute("UPDATE trackedbranches SET disabled=TRUE WHERE repository=%s AND local_name=%s", (self.repository.id, self.branch.name))

    def setSummary(self, db, summary):
        self.serial += 1
        self.summary = summary
        db.cursor().execute("UPDATE reviews SET summary=%s, serial=%s WHERE id=%s", [self.summary, self.serial, self.id])

    def setDescription(self, db, description):
        self.serial += 1
        self.description = description
        db.cursor().execute("UPDATE reviews SET description=%s, serial=%s WHERE id=%s", [self.description, self.serial, self.id])

    def addOwner(self, db, owner):
        if not owner in self.owners:
            self.serial += 1
            self.owners.append(owner)

            cursor = db.cursor()
            cursor.execute("SELECT 1 FROM reviewusers WHERE review=%s AND uid=%s", (self.id, owner.id))

            if cursor.fetchone():
                cursor.execute("UPDATE reviewusers SET owner=TRUE WHERE review=%s AND uid=%s", (self.id, owner.id))
            else:
                cursor.execute("INSERT INTO reviewusers (review, uid, owner) VALUES (%s, %s, TRUE)", (self.id, owner.id))

            cursor.execute("SELECT id FROM trackedbranches WHERE repository=%s AND local_name=%s", (self.repository.id, self.branch.name))

            row = cursor.fetchone()
            if row:
                trackedbranch_id = row[0]
                cursor.execute("INSERT INTO trackedbranchusers (branch, uid) VALUES (%s, %s)", (trackedbranch_id, owner.id))

    def removeOwner(self, db, owner):
        if owner in self.owners:
            self.serial += 1
            self.owners.remove(owner)

            cursor = db.cursor()
            cursor.execute("UPDATE reviewusers SET owner=FALSE WHERE review=%s AND uid=%s", (self.id, owner.id))
            cursor.execute("SELECT id FROM trackedbranches WHERE repository=%s AND local_name=%s", (self.repository.id, self.branch.name))

            row = cursor.fetchone()
            if row:
                trackedbranch_id = row[0]
                cursor.execute("DELETE FROM trackedbranchusers WHERE branch=%s AND uid=%s", (trackedbranch_id, owner.id))

    def getReviewFilters(self, db):
        cursor = db.cursor()
        cursor.execute("SELECT uid, path, type, NULL FROM reviewfilters WHERE review=%s", (self.id,))
        return cursor.fetchall() or None

    def getFilteredTails(self, db):
        import log.commitset
        commitset = log.commitset.CommitSet(self.branch.getCommits(db))
        return commitset.getFilteredTails(self.branch.repository)

    def getRelevantFiles(self, db, user):
        if not self.filters:
            from reviewing.filters import Filters

            self.filters = Filters()
            self.filters.setFiles(db, review=self)
            self.filters.load(db, review=self)
            self.relevant_files = self.filters.getRelevantFiles()

            cursor = db.cursor()
            cursor.execute("SELECT assignee, file FROM fullreviewuserfiles WHERE review=%s", (self.id,))
            for user_id, file_id in cursor:
                self.relevant_files.setdefault(user_id, set()).add(file_id)

        return self.relevant_files.get(user.id, set())

    def getUserAssociation(self, db, user):
        cursor = db.cursor()

        association = []

        if user in self.owners:
            association.append("owner")

        cursor.execute("""SELECT 1
                            FROM reviewchangesets
                            JOIN changesets ON (changesets.id=reviewchangesets.changeset)
                            JOIN commits ON (commits.id=changesets.child)
                            JOIN gitusers ON (gitusers.id=commits.author_gituser)
                            JOIN usergitemails USING (email)
                           WHERE reviewchangesets.review=%s
                             AND usergitemails.uid=%s""",
                       (self.id, user.id))
        if cursor.fetchone():
            association.append("author")

        cursor.execute("SELECT COUNT(*) FROM fullreviewuserfiles WHERE review=%s AND assignee=%s", (self.id, user.id))
        if cursor.fetchone()[0] != 0:
            association.append("reviewer")
        elif user not in self.owners:
            cursor.execute("SELECT 1 FROM reviewusers WHERE review=%s AND uid=%s", (self.id, user.id))
            if cursor.fetchone():
                association.append("watcher")

        if not association:
            association.append("none")

        return ", ".join(association)

    @staticmethod
    def fromId(db, review_id, branch=None, profiler=None):
        from dbutils import User

        cursor = db.cursor()
        cursor.execute("SELECT type, branch, state, serial, summary, description, applyfilters, applyparentfilters FROM reviews WHERE id=%s", [review_id])
        row = cursor.fetchone()
        if not row: raise NoSuchReview(review_id)

        type, branch_id, state, serial, summary, description, applyfilters, applyparentfilters = row

        if profiler: profiler.check("Review.fromId: basic")

        if branch is None:
            from dbutils import Branch
            branch = Branch.fromId(db, branch_id, load_review=False, profiler=profiler)

        cursor.execute("SELECT uid FROM reviewusers WHERE review=%s AND owner", (review_id,))

        owners = User.fromIds(db, [user_id for (user_id,) in cursor])

        if profiler: profiler.check("Review.fromId: owners")

        review = Review(review_id, owners, type, branch, state, serial, summary, description, applyfilters, applyparentfilters)
        branch.review = review

        # Reviewers: all users that have at least one review file assigned to them.
        cursor.execute("""SELECT DISTINCT uid, assignee IS NOT NULL, type
                            FROM reviewusers
                 LEFT OUTER JOIN fullreviewuserfiles ON (fullreviewuserfiles.review=reviewusers.review AND assignee=uid)
                           WHERE reviewusers.review=%s""",
                       (review_id,))

        reviewers = []
        watchers = []
        watcher_types = {}

        for user_id, is_reviewer, user_type in cursor.fetchall():
            if is_reviewer:
                reviewers.append(user_id)
            elif user_id not in review.owners:
                watchers.append(user_id)
                watcher_types[user_id] = user_type

        review.reviewers = User.fromIds(db, reviewers)

        for watcher in User.fromIds(db, watchers):
            review.watchers[watcher] = watcher_types[watcher]

        if profiler: profiler.check("Review.fromId: users")

        return review

    @staticmethod
    def fromBranch(db, branch):
        if branch:
            cursor = db.cursor()
            cursor.execute("SELECT id FROM reviews WHERE branch=%s", [branch.id])
            row = cursor.fetchone()
            if not row: return None
            else: return Review.fromId(db, row[0], branch)
        else:
            return None

    @staticmethod
    def fromName(db, repository, name):
        from dbutils import Branch
        return Review.fromBranch(db, Branch.fromName(db, repository, name))

    @staticmethod
    def fromArgument(db, argument):
        try:
            return Review.fromId(db, int(argument))
        except:
            from dbutils import Branch
            branch = Branch.fromName(db, str(argument))
            if not branch: return None
            return Review.fromBranch(db, branch)
Exemple #2
0
class Review(object):
    def __init__(self, review_id, owners, review_type, branch, state, serial, summary, description, applyfilters, applyparentfilters):
        self.id = review_id
        self.owners = owners
        self.type = review_type
        self.repository = branch.repository
        self.branch = branch
        self.state = state
        self.serial = serial
        self.summary = summary
        self.description = description
        self.reviewers = []
        self.watchers = {}
        self.changesets = []
        self.commentchains = None
        self.applyfilters = applyfilters
        self.applyparentfilters = applyparentfilters
        self.filters = None
        self.relevant_files = None
        self.draft_status = None

    @staticmethod
    def isAccepted(db, review_id):
        cursor = db.cursor()

        cursor.execute("SELECT 1 FROM reviewfiles WHERE review=%s AND state='pending' LIMIT 1", (review_id,))
        if cursor.fetchone(): return False

        cursor.execute("SELECT 1 FROM commentchains WHERE review=%s AND type='issue' AND state='open' LIMIT 1", (review_id,))
        if cursor.fetchone(): return False

        return True

    def accepted(self, db):
        if self.state != 'open': return False
        else: return Review.isAccepted(db, self.id)

    def getReviewState(self, db):
        cursor = db.cursor()

        cursor.execute("""SELECT state, SUM(deleted) + SUM(inserted)
                            FROM reviewfiles
                           WHERE reviewfiles.review=%s
                        GROUP BY state""",
                       (self.id,))

        pending = 0
        reviewed = 0

        for state, count in cursor.fetchall():
            if state == "pending": pending = count
            else: reviewed = count

        cursor.execute("""SELECT count(id)
                            FROM commentchains
                           WHERE review=%s
                             AND type='issue'
                             AND state='open'""",
                       (self.id,))

        issues = cursor.fetchone()[0]

        return ReviewState(self, self.accepted(db), pending, reviewed, issues)

    def getReviewRebases(self, db):
        return ReviewRebases(db, self)

    def getCommitSet(self, db):
        import gitutils
        import log.commitset

        cursor = db.cursor()
        cursor.execute("""SELECT DISTINCT commits.id, commits.sha1
                            FROM commits
                            JOIN changesets ON (changesets.child=commits.id)
                            JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id)
                           WHERE reviewchangesets.review=%s""",
                       (self.id,))

        commits = []

        for commit_id, commit_sha1 in cursor:
            commits.append(gitutils.Commit.fromSHA1(db, self.repository, commit_sha1, commit_id))

        return log.commitset.CommitSet(commits)

    def containsCommit(self, db, commit, include_head_and_tails=False):
        import gitutils

        commit_id = None
        commit_sha1 = None

        if isinstance(commit, gitutils.Commit):
            if commit.id: commit_id = commit.id
            else: commit_sha1 = commit.sha1
        elif isinstance(commit, str):
            commit_sha1 = self.repository.revparse(commit)
            commit = None
        elif isinstance(commit, int):
            commit_id = commit
            commit = None
        else:
            raise TypeError

        cursor = db.cursor()

        if commit_id is not None:
            cursor.execute("""SELECT 1
                                FROM reviewchangesets
                                JOIN changesets ON (id=changeset)
                               WHERE reviewchangesets.review=%s
                                 AND changesets.child=%s""",
                           (self.id, commit_id))
        else:
            cursor.execute("""SELECT 1
                                FROM reviewchangesets
                                JOIN changesets ON (changesets.id=reviewchangesets.changeset)
                                JOIN commits ON (commits.id=changesets.child)
                               WHERE reviewchangesets.review=%s
                                 AND commits.sha1=%s""",
                           (self.id, commit_sha1))

        if cursor.fetchone() is not None:
            return True

        if include_head_and_tails:
            head_and_tails = set([self.branch.head])

            commitset = self.getCommitSet(db)

            if commitset:
                head_and_tails |= commitset.getTails()

            if commit_sha1 is None:
                if commit is None:
                    commit = gitutils.Commit.fromId(db, self.repository, commit_id)
                commit_sha1 = commit.sha1

            if commit_sha1 in head_and_tails:
                return True

        return False

    def getJS(self):
        return "var review = critic.review = { id: %d, branch: { id: %d, name: %r }, owners: [ %s ], serial: %d };" % (self.id, self.branch.id, self.branch.name, ", ".join(owner.getJSConstructor() for owner in self.owners), self.serial)

    def getETag(self, db, user=None):
        etag = "review%d.serial%d" % (self.id, self.serial)

        if user:
            items = self.getDraftStatus(db, user)
            if any(items.values()):
                etag += ".draft%d" % hash(tuple(sorted(items.items())))

            cursor = db.cursor()
            cursor.execute("SELECT id FROM reviewrebases WHERE review=%s AND uid=%s AND new_head IS NULL", (self.id, user.id))
            row = cursor.fetchone()
            if row:
                etag += ".rebase%d" % row[0]

        return '"%s"' % etag

    def getURL(self, db, user=None, indent=0):
        import dbutils

        indent = " " * indent

        if db and user:
            url_prefixes = user.getCriticURLs(db)
        else:
            url_prefixes = [dbutils.getURLPrefix(db)]

        return "\n".join(["%s%s/r/%d" % (indent, url_prefix, self.id) for url_prefix in url_prefixes])

    def getRecipients(self, db):
        from dbutils import User

        cursor = db.cursor()
        cursor.execute("SELECT uid, include FROM reviewrecipientfilters WHERE review=%s ORDER BY uid ASC", (self.id,))

        included = set(owner.id for owner in self.owners)
        excluded = set()
        for uid, include in cursor:
            if include: included.add(uid)
            elif uid not in self.owners: excluded.add(uid)

        cursor.execute("SELECT uid FROM reviewusers WHERE review=%s", (self.id,))

        recipients = []
        for (user_id,) in cursor:
            if user_id in excluded: continue
            elif user_id not in included and 0 in excluded: continue

            user = User.fromId(db, user_id)
            if user.status != "retired":
                recipients.append(user)

        return recipients

    def getDraftStatus(self, db, user):
        if self.draft_status is None:
            self.draft_status = countDraftItems(db, user, self)
        return self.draft_status

    def incrementSerial(self, db):
        self.serial += 1
        db.cursor().execute("UPDATE reviews SET serial=%s WHERE id=%s", [self.serial, self.id])

    def close(self, db, user):
        self.serial += 1
        db.cursor().execute("UPDATE reviews SET state='closed', serial=%s, closed_by=%s WHERE id=%s", (self.serial, user.id, self.id))

    def drop(self, db, user):
        self.serial += 1
        db.cursor().execute("UPDATE reviews SET state='dropped', serial=%s, closed_by=%s WHERE id=%s", (self.serial, user.id, self.id))

    def reopen(self, db, user):
        self.serial += 1
        db.cursor().execute("UPDATE reviews SET state='open', serial=%s, closed_by=NULL WHERE id=%s", (self.serial, self.id))

    def disableTracking(self, db):
        db.cursor().execute("UPDATE trackedbranches SET disabled=TRUE WHERE repository=%s AND local_name=%s", (self.repository.id, self.branch.name))

    def setSummary(self, db, summary):
        self.serial += 1
        self.summary = summary
        db.cursor().execute("UPDATE reviews SET summary=%s, serial=%s WHERE id=%s", [self.summary, self.serial, self.id])

    def setDescription(self, db, description):
        self.serial += 1
        self.description = description
        db.cursor().execute("UPDATE reviews SET description=%s, serial=%s WHERE id=%s", [self.description, self.serial, self.id])

    def addOwner(self, db, owner):
        if not owner in self.owners:
            self.serial += 1
            self.owners.append(owner)

            cursor = db.cursor()
            cursor.execute("SELECT 1 FROM reviewusers WHERE review=%s AND uid=%s", (self.id, owner.id))

            if cursor.fetchone():
                cursor.execute("UPDATE reviewusers SET owner=TRUE WHERE review=%s AND uid=%s", (self.id, owner.id))
            else:
                cursor.execute("INSERT INTO reviewusers (review, uid, owner) VALUES (%s, %s, TRUE)", (self.id, owner.id))

            cursor.execute("SELECT id FROM trackedbranches WHERE repository=%s AND local_name=%s", (self.repository.id, self.branch.name))

            row = cursor.fetchone()
            if row:
                trackedbranch_id = row[0]
                cursor.execute("INSERT INTO trackedbranchusers (branch, uid) VALUES (%s, %s)", (trackedbranch_id, owner.id))

    def removeOwner(self, db, owner):
        if owner in self.owners:
            self.serial += 1
            self.owners.remove(owner)

            cursor = db.cursor()
            cursor.execute("UPDATE reviewusers SET owner=FALSE WHERE review=%s AND uid=%s", (self.id, owner.id))
            cursor.execute("SELECT id FROM trackedbranches WHERE repository=%s AND local_name=%s", (self.repository.id, self.branch.name))

            row = cursor.fetchone()
            if row:
                trackedbranch_id = row[0]
                cursor.execute("DELETE FROM trackedbranchusers WHERE branch=%s AND uid=%s", (trackedbranch_id, owner.id))

    def getReviewFilters(self, db):
        cursor = db.cursor()
        cursor.execute("SELECT directory, file, type, NULL, uid FROM reviewfilters WHERE review=%s", (self.id,))
        return cursor.fetchall() or None

    def getFilteredTails(self):
        import log.commitset
        commitset = log.commitset.CommitSet(self.branch.commits)
        return commitset.getFilteredTails(self.branch.repository)

    def getRelevantFiles(self, db, user):
        if not self.filters:
            from reviewing.filters import Filters

            self.filters = Filters()
            self.filters.load(db, review=self)
            self.relevant_files = self.filters.getRelevantFiles(db, self)

            cursor = db.cursor()
            cursor.execute("SELECT assignee, file FROM fullreviewuserfiles WHERE review=%s", (self.id,))
            for user_id, file_id in cursor:
                self.relevant_files.setdefault(user_id, set()).add(file_id)

        return self.relevant_files.get(user.id, set())

    def getUserAssociation(self, db, user):
        cursor = db.cursor()

        association = []

        if user in self.owners:
            association.append("owner")

        cursor.execute("""SELECT 1
                            FROM reviewchangesets
                            JOIN changesets ON (changesets.id=reviewchangesets.changeset)
                            JOIN commits ON (commits.id=changesets.child)
                            JOIN gitusers ON (gitusers.id=commits.author_gituser)
                            JOIN usergitemails USING (email)
                           WHERE reviewchangesets.review=%s
                             AND usergitemails.uid=%s""",
                       (self.id, user.id))
        if cursor.fetchone():
            association.append("author")

        cursor.execute("SELECT COUNT(*) FROM fullreviewuserfiles WHERE review=%s AND assignee=%s", (self.id, user.id))
        if cursor.fetchone()[0] != 0:
            association.append("reviewer")
        elif user not in self.owners:
            cursor.execute("SELECT 1 FROM reviewusers WHERE review=%s AND uid=%s", (self.id, user.id))
            if cursor.fetchone():
                association.append("watcher")

        if not association:
            association.append("none")

        return ", ".join(association)

    @staticmethod
    def fromId(db, review_id, branch=None, load_commits=True, profiler=None):
        from dbutils import User

        cursor = db.cursor()
        cursor.execute("SELECT type, branch, state, serial, summary, description, applyfilters, applyparentfilters FROM reviews WHERE id=%s", [review_id])
        row = cursor.fetchone()
        if not row: return None

        type, branch_id, state, serial, summary, description, applyfilters, applyparentfilters = row

        if profiler: profiler.check("Review.fromId: basic")

        if branch is None:
            from dbutils import Branch
            branch = Branch.fromId(db, branch_id, load_review=False, load_commits=load_commits, profiler=profiler)

        cursor.execute("SELECT uid FROM reviewusers WHERE review=%s AND owner", (review_id,))

        owners = User.fromIds(db, [user_id for (user_id,) in cursor])

        if profiler: profiler.check("Review.fromId: owners")

        review = Review(review_id, owners, type, branch, state, serial, summary, description, applyfilters, applyparentfilters)
        branch.review = review

        # Reviewers: all users that have at least one review file assigned to them.
        cursor.execute("""SELECT DISTINCT uid, assignee IS NOT NULL, type
                            FROM reviewusers
                 LEFT OUTER JOIN fullreviewuserfiles ON (fullreviewuserfiles.review=reviewusers.review AND assignee=uid)
                           WHERE reviewusers.review=%s""",
                       (review_id,))

        reviewers = []
        watchers = []
        watcher_types = {}

        for user_id, is_reviewer, user_type in cursor.fetchall():
            if is_reviewer:
                reviewers.append(user_id)
            elif user_id not in review.owners:
                watchers.append(user_id)
                watcher_types[user_id] = user_type

        review.reviewers = User.fromIds(db, reviewers)

        for watcher in User.fromIds(db, watchers):
            review.watchers[watcher] = watcher_types[watcher]

        if profiler: profiler.check("Review.fromId: users")

        if load_commits:
            review.branch.loadCommits(db)

            cursor.execute("""SELECT id
                                FROM reviewchangesets
                                JOIN changesets ON (id=changeset)
                               WHERE review=%s
                                 AND child=ANY (%s)""", (review_id, [commit.id for commit in review.branch.commits]))

            review.changesets = [changeset_id for (changeset_id,) in cursor.fetchall()]

            if profiler: profiler.check("Review.fromId: load commits")

        return review

    @staticmethod
    def fromBranch(db, branch):
        if branch:
            cursor = db.cursor()
            cursor.execute("SELECT id FROM reviews WHERE branch=%s", [branch.id])
            row = cursor.fetchone()
            if not row: return None
            else: return Review.fromId(db, row[0], branch)
        else:
            return None

    @staticmethod
    def fromName(db, repository, name):
        from dbutils import Branch
        return Review.fromBranch(db, Branch.fromName(db, repository, name))

    @staticmethod
    def fromArgument(db, argument):
        try:
            return Review.fromId(db, int(argument))
        except:
            from dbutils import Branch
            branch = Branch.fromName(db, str(argument))
            if not branch: return None
            return Review.fromBranch(db, branch)
Exemple #3
0
class Review(object):
    def __init__(self, review_id, owners, review_type, branch, state, serial,
                 summary, description, applyfilters, applyparentfilters):
        self.id = review_id
        self.owners = owners
        self.type = review_type
        self.repository = branch.repository
        self.branch = branch
        self.state = state
        self.serial = serial
        self.summary = summary
        self.description = description
        self.reviewers = []
        self.watchers = {}
        self.commentchains = None
        self.applyfilters = applyfilters
        self.applyparentfilters = applyparentfilters
        self.filters = None
        self.relevant_files = None
        self.draft_status = None
        self.performed_rebase = None

    @staticmethod
    def isAccepted(db, review_id):
        cursor = db.cursor()

        cursor.execute(
            "SELECT 1 FROM reviewfiles WHERE review=%s AND state='pending' LIMIT 1",
            (review_id, ))
        if cursor.fetchone(): return False

        cursor.execute(
            "SELECT 1 FROM commentchains WHERE review=%s AND type='issue' AND state='open' LIMIT 1",
            (review_id, ))
        if cursor.fetchone(): return False

        return True

    def accepted(self, db):
        if self.state != 'open': return False
        else: return Review.isAccepted(db, self.id)

    def getReviewState(self, db):
        cursor = db.cursor()

        cursor.execute(
            """SELECT state, SUM(deleted) + SUM(inserted)
                            FROM reviewfiles
                           WHERE reviewfiles.review=%s
                        GROUP BY state""", (self.id, ))

        pending = 0
        reviewed = 0

        for state, count in cursor.fetchall():
            if state == "pending": pending = count
            else: reviewed = count

        cursor.execute(
            """SELECT count(id)
                            FROM commentchains
                           WHERE review=%s
                             AND type='issue'
                             AND state='open'""", (self.id, ))

        issues = cursor.fetchone()[0]

        return ReviewState(self, self.accepted(db), pending, reviewed, issues)

    def setPerformedRebase(self, old_head, new_head, old_upstream,
                           new_upstream, user, equivalent_merge,
                           replayed_rebase):
        self.performed_rebase = ReviewRebase(self, old_head, new_head,
                                             old_upstream, new_upstream, user,
                                             equivalent_merge, replayed_rebase)

    def getReviewRebases(self, db):
        return ReviewRebases(db, self)

    def getTrackedBranch(self, db):
        cursor = db.cursor()
        cursor.execute(
            """SELECT trackedbranches.id, remote, remote_name, disabled
                            FROM trackedbranches
                            JOIN branches ON (trackedbranches.repository=branches.repository
                                          AND trackedbranches.local_name=branches.name)
                            JOIN reviews ON (branches.id=reviews.branch)
                           WHERE reviews.id=%s""", (self.id, ))

        for trackedbranch_id, remote, name, disabled in cursor:
            return ReviewTrackedBranch(self, trackedbranch_id, remote, name,
                                       disabled)

    def getCommitSet(self, db):
        import gitutils
        import log.commitset

        cursor = db.cursor()
        cursor.execute(
            """SELECT DISTINCT commits.id, commits.sha1
                            FROM commits
                            JOIN changesets ON (changesets.child=commits.id)
                            JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id)
                           WHERE reviewchangesets.review=%s""", (self.id, ))

        commits = []

        for commit_id, commit_sha1 in cursor:
            commits.append(
                gitutils.Commit.fromSHA1(db, self.repository, commit_sha1,
                                         commit_id))

        return log.commitset.CommitSet(commits)

    def containsCommit(self,
                       db,
                       commit,
                       include_head_and_tails=False,
                       include_actual_log=False):
        import gitutils

        commit_id = None
        commit_sha1 = None

        if isinstance(commit, gitutils.Commit):
            commit_id = commit.id
            commit_sha1 = commit.sha1
        elif isinstance(commit, str):
            commit_sha1 = self.repository.revparse(commit)
            commit = None
        elif isinstance(commit, int):
            commit_id = commit
            commit = None
        else:
            raise TypeError

        cursor = db.cursor()

        if commit_id is not None:
            cursor.execute(
                """SELECT 1
                                FROM reviewchangesets
                                JOIN changesets ON (id=changeset)
                               WHERE reviewchangesets.review=%s
                                 AND changesets.child=%s
                                 AND changesets.type!='conflicts'""",
                (self.id, commit_id))
        else:
            cursor.execute(
                """SELECT 1
                                FROM reviewchangesets
                                JOIN changesets ON (changesets.id=reviewchangesets.changeset)
                                JOIN commits ON (commits.id=changesets.child)
                               WHERE reviewchangesets.review=%s
                                 AND changesets.type!='conflicts'
                                 AND commits.sha1=%s""",
                (self.id, commit_sha1))

        if cursor.fetchone() is not None:
            return True

        if include_head_and_tails:
            head_and_tails = set([self.branch.getHead(db)])

            commitset = self.getCommitSet(db)

            if commitset:
                head_and_tails |= commitset.getTails()

            if commit_sha1 is None:
                if commit is None:
                    commit = gitutils.Commit.fromId(db, self.repository,
                                                    commit_id)
                commit_sha1 = commit.sha1

            if commit_sha1 in head_and_tails:
                return True

        if include_actual_log:
            if commit_id is not None:
                cursor.execute(
                    """SELECT 1
                                    FROM reachable
                                    JOIN branches ON (branches.id=reachable.branch)
                                    JOIN reviews ON (reviews.branch=branches.id)
                                   WHERE reachable.commit=%s
                                     AND reviews.id=%s""",
                    (commit_id, self.id))
            else:
                cursor.execute(
                    """SELECT 1
                                    FROM commits
                                    JOIN reachable ON (reachable.commit=commits.id)
                                    JOIN branches ON (branches.id=reachable.branch)
                                    JOIN reviews ON (reviews.branch=branches.id)
                                   WHERE commits.sha1=%s
                                     AND reviews.id=%s""",
                    (commit_sha1, self.id))

            if cursor.fetchone() is not None:
                return True

        return False

    def getJS(self):
        return "var review = critic.review = { id: %d, branch: { id: %d, name: %r }, owners: [ %s ], serial: %d };" % (
            self.id, self.branch.id, self.branch.name, ", ".join(
                owner.getJSConstructor()
                for owner in self.owners), self.serial)

    def getETag(self, db, user=None):
        import configuration

        cursor = db.cursor()
        etag = ""

        if configuration.debug.IS_DEVELOPMENT:
            cursor.execute(
                "SELECT installed_at FROM systemidentities WHERE name=%s",
                (configuration.base.SYSTEM_IDENTITY, ))
            installed_at = cursor.fetchone()[0]
            etag += "install%s." % time.mktime(installed_at.timetuple())

        if user and not user.isAnonymous():
            etag += "user%d." % user.id

        etag += "review%d.serial%d" % (self.id, self.serial)

        if user:
            items = self.getDraftStatus(db, user)
            if any(items.values()):
                etag += ".draft%d" % hash(tuple(sorted(items.items())))

            cursor.execute(
                "SELECT id FROM reviewrebases WHERE review=%s AND uid=%s AND new_head IS NULL",
                (self.id, user.id))
            row = cursor.fetchone()
            if row:
                etag += ".rebase%d" % row[0]

        return '"%s"' % etag

    def getURL(self, db, user=None, indent=0, separator="\n"):
        import dbutils

        indent = " " * indent

        if user:
            url_prefixes = user.getCriticURLs(db)
        else:
            url_prefixes = [dbutils.getURLPrefix(db)]

        return separator.join([
            "%s%s/r/%d" % (indent, url_prefix, self.id)
            for url_prefix in url_prefixes
        ])

    def getRecipients(self, db):
        from dbutils import User

        cursor = db.cursor()
        cursor.execute(
            "SELECT uid, include FROM reviewrecipientfilters WHERE review=%s",
            (self.id, ))

        default_include = True
        included = set(owner.id for owner in self.owners)
        excluded = set()

        for uid, include in cursor:
            if uid is None:
                default_include = include
            elif include:
                included.add(uid)
            elif uid not in self.owners:
                excluded.add(uid)

        cursor.execute("SELECT uid FROM reviewusers WHERE review=%s",
                       (self.id, ))

        recipients = []
        for (user_id, ) in cursor:
            if user_id in excluded:
                continue
            elif user_id not in included and not default_include:
                continue

            user = User.fromId(db, user_id)
            if user.status != "retired":
                recipients.append(user)

        return recipients

    def getDraftStatus(self, db, user):
        if self.draft_status is None:
            self.draft_status = countDraftItems(db, user, self)
        return self.draft_status

    def incrementSerial(self, db):
        self.serial += 1
        db.cursor().execute("UPDATE reviews SET serial=%s WHERE id=%s",
                            [self.serial, self.id])

    def scheduleBranchArchival(self, db, delay=None):
        import dbutils

        # First, cancel current scheduled archival, if there is one.
        self.cancelScheduledBranchArchival(db)

        # If review is not closed or dropped, don't schedule a branch archival.
        # Also don't schedule one if the branch has already been archived.
        if self.state not in ("closed", "dropped") or self.branch.archived:
            return

        if delay is None:
            # Configuration policy:
            #
            # Any owner of a review can, by having changed the relevant
            # preference setting, increase the time before a review branch is
            # archived, or disable archival entirely, but they can't make it
            # happen sooner than the system or repository default, or what any
            # other owner has requested.

            # Find configured value for each owner, and also the per-repository
            # (or per-system) default, in case each owner has changed the
            # setting.
            preference_item = "review.branchArchiveDelay." + self.state
            repository_default = dbutils.User.fetchPreference(
                db, preference_item, repository=self.repository)
            delays = set([repository_default])
            for owner in self.owners:
                delays.add(
                    owner.getPreference(db,
                                        preference_item,
                                        repository=self.repository))

            # If configured to zero (by any owner,) don't schedule a branch
            # archival.
            if min(delays) <= 0:
                return

            # Otherwise, use maximum configured value for any owner.
            delay = max(delays)

        cursor = db.cursor()
        cursor.execute(
            """INSERT INTO scheduledreviewbrancharchivals (review, deadline)
                               VALUES (%s, NOW() + INTERVAL %s)""",
            (self.id, "%d DAYS" % delay))

        return delay

    def cancelScheduledBranchArchival(self, db):
        cursor = db.cursor()
        cursor.execute(
            """DELETE FROM scheduledreviewbrancharchivals
                                WHERE review=%s""", (self.id, ))

    def close(self, db, user):
        self.serial += 1
        self.state = "closed"
        db.cursor().execute(
            "UPDATE reviews SET state='closed', serial=%s, closed_by=%s WHERE id=%s",
            (self.serial, user.id, self.id))
        self.scheduleBranchArchival(db)

    def drop(self, db, user):
        self.serial += 1
        self.state = "dropped"
        db.cursor().execute(
            "UPDATE reviews SET state='dropped', serial=%s, closed_by=%s WHERE id=%s",
            (self.serial, user.id, self.id))
        self.scheduleBranchArchival(db)

    def reopen(self, db, user):
        self.serial += 1
        if self.branch.archived:
            self.branch.resurrect(db)
        db.cursor().execute(
            "UPDATE reviews SET state='open', serial=%s, closed_by=NULL WHERE id=%s",
            (self.serial, self.id))
        self.cancelScheduledBranchArchival(db)

    def disableTracking(self, db):
        db.cursor().execute(
            "UPDATE trackedbranches SET disabled=TRUE WHERE repository=%s AND local_name=%s",
            (self.repository.id, self.branch.name))

    def setSummary(self, db, summary):
        self.serial += 1
        self.summary = summary
        db.cursor().execute(
            "UPDATE reviews SET summary=%s, serial=%s WHERE id=%s",
            [self.summary, self.serial, self.id])

    def setDescription(self, db, description):
        self.serial += 1
        self.description = description
        db.cursor().execute(
            "UPDATE reviews SET description=%s, serial=%s WHERE id=%s",
            [self.description, self.serial, self.id])

    def addOwner(self, db, owner):
        if not owner in self.owners:
            self.serial += 1
            self.owners.append(owner)

            cursor = db.cursor()
            cursor.execute(
                "SELECT 1 FROM reviewusers WHERE review=%s AND uid=%s",
                (self.id, owner.id))

            if cursor.fetchone():
                cursor.execute(
                    "UPDATE reviewusers SET owner=TRUE WHERE review=%s AND uid=%s",
                    (self.id, owner.id))
            else:
                cursor.execute(
                    "INSERT INTO reviewusers (review, uid, owner) VALUES (%s, %s, TRUE)",
                    (self.id, owner.id))

            cursor.execute(
                "SELECT id FROM trackedbranches WHERE repository=%s AND local_name=%s",
                (self.repository.id, self.branch.name))

            row = cursor.fetchone()
            if row:
                trackedbranch_id = row[0]
                cursor.execute(
                    "INSERT INTO trackedbranchusers (branch, uid) VALUES (%s, %s)",
                    (trackedbranch_id, owner.id))

    def removeOwner(self, db, owner):
        if owner in self.owners:
            self.serial += 1
            self.owners.remove(owner)

            cursor = db.cursor()
            cursor.execute(
                "UPDATE reviewusers SET owner=FALSE WHERE review=%s AND uid=%s",
                (self.id, owner.id))
            cursor.execute(
                "SELECT id FROM trackedbranches WHERE repository=%s AND local_name=%s",
                (self.repository.id, self.branch.name))

            row = cursor.fetchone()
            if row:
                trackedbranch_id = row[0]
                cursor.execute(
                    "DELETE FROM trackedbranchusers WHERE branch=%s AND uid=%s",
                    (trackedbranch_id, owner.id))

    def getReviewFilters(self, db):
        cursor = db.cursor()
        cursor.execute(
            "SELECT uid, path, type, NULL FROM reviewfilters WHERE review=%s",
            (self.id, ))
        return cursor.fetchall() or None

    def getFilteredTails(self, db):
        import log.commitset
        commitset = log.commitset.CommitSet(self.branch.getCommits(db))
        return commitset.getFilteredTails(self.branch.repository)

    def getRelevantFiles(self, db, user):
        if not self.filters:
            from reviewing.filters import Filters

            self.filters = Filters()
            self.filters.setFiles(db, review=self)
            self.filters.load(db, review=self)
            self.relevant_files = self.filters.getRelevantFiles()

            cursor = db.cursor()
            cursor.execute(
                "SELECT assignee, file FROM fullreviewuserfiles WHERE review=%s",
                (self.id, ))
            for user_id, file_id in cursor:
                self.relevant_files.setdefault(user_id, set()).add(file_id)

        return self.relevant_files.get(user.id, set())

    def getUserAssociation(self, db, user):
        cursor = db.cursor()

        association = []

        if user in self.owners:
            association.append("owner")

        cursor.execute(
            """SELECT 1
                            FROM reviewchangesets
                            JOIN changesets ON (changesets.id=reviewchangesets.changeset)
                            JOIN commits ON (commits.id=changesets.child)
                            JOIN gitusers ON (gitusers.id=commits.author_gituser)
                            JOIN usergitemails USING (email)
                           WHERE reviewchangesets.review=%s
                             AND usergitemails.uid=%s""", (self.id, user.id))
        if cursor.fetchone():
            association.append("author")

        cursor.execute(
            "SELECT COUNT(*) FROM fullreviewuserfiles WHERE review=%s AND assignee=%s",
            (self.id, user.id))
        if cursor.fetchone()[0] != 0:
            association.append("reviewer")
        elif user not in self.owners:
            cursor.execute(
                "SELECT 1 FROM reviewusers WHERE review=%s AND uid=%s",
                (self.id, user.id))
            if cursor.fetchone():
                association.append("watcher")

        if not association:
            association.append("none")

        return ", ".join(association)

    @staticmethod
    def fromId(db, review_id, branch=None, profiler=None):
        from dbutils import User

        cursor = db.cursor()
        cursor.execute(
            "SELECT type, branch, state, serial, summary, description, applyfilters, applyparentfilters FROM reviews WHERE id=%s",
            [review_id])
        row = cursor.fetchone()
        if not row: raise NoSuchReview(review_id)

        type, branch_id, state, serial, summary, description, applyfilters, applyparentfilters = row

        if profiler: profiler.check("Review.fromId: basic")

        if branch is None:
            from dbutils import Branch
            branch = Branch.fromId(db,
                                   branch_id,
                                   load_review=False,
                                   profiler=profiler)

        cursor.execute("SELECT uid FROM reviewusers WHERE review=%s AND owner",
                       (review_id, ))

        owners = User.fromIds(db, [user_id for (user_id, ) in cursor])

        if profiler: profiler.check("Review.fromId: owners")

        review = Review(review_id, owners, type, branch, state, serial,
                        summary, description, applyfilters, applyparentfilters)
        branch.review = review

        # Reviewers: all users that have at least one review file assigned to them.
        cursor.execute(
            """SELECT DISTINCT uid, assignee IS NOT NULL, type
                            FROM reviewusers
                 LEFT OUTER JOIN fullreviewuserfiles ON (fullreviewuserfiles.review=reviewusers.review AND assignee=uid)
                           WHERE reviewusers.review=%s""", (review_id, ))

        reviewers = []
        watchers = []
        watcher_types = {}

        for user_id, is_reviewer, user_type in cursor.fetchall():
            if is_reviewer:
                reviewers.append(user_id)
            elif user_id not in review.owners:
                watchers.append(user_id)
                watcher_types[user_id] = user_type

        review.reviewers = User.fromIds(db, reviewers)

        for watcher in User.fromIds(db, watchers):
            review.watchers[watcher] = watcher_types[watcher]

        if profiler: profiler.check("Review.fromId: users")

        return review

    @staticmethod
    def fromBranch(db, branch):
        if branch:
            cursor = db.cursor()
            cursor.execute("SELECT id FROM reviews WHERE branch=%s",
                           [branch.id])
            row = cursor.fetchone()
            if not row: return None
            else: return Review.fromId(db, row[0], branch)
        else:
            return None

    @staticmethod
    def fromName(db, repository, name):
        from dbutils import Branch
        return Review.fromBranch(db, Branch.fromName(db, repository, name))

    @staticmethod
    def fromArgument(db, argument):
        try:
            return Review.fromId(db, int(argument))
        except:
            from dbutils import Branch
            branch = Branch.fromName(db, str(argument))
            if not branch: return None
            return Review.fromBranch(db, branch)

    @staticmethod
    def fromAPI(api_review):
        return Review.fromId(api_review.critic.database, api_review.id)