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)
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)
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)