def sendReviewRebased(db, from_user, to_user, recipients, review, new_upstream, rebased_commits, onto_branch=None): # First check if we can/should send emails to the user at all. try: checkEmailEnabled(db, to_user) subject = generateSubjectLine(db, to_user, review, "updatedReview.reviewRebased") except MailDisabled: return [] if from_user == to_user and to_user.getPreference(db, "email.ignoreOwnChanges"): return [] line_length = to_user.getPreference(db, "email.lineLength") hr = "-" * line_length data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'review.branch.name': review.branch.name, 'review.branch.repository': review.repository.getURL(db, to_user), 'from.fullname': from_user.fullname, 'hr': hr } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data if new_upstream: data["new_upstream"] = new_upstream.oneline(db, decorate=True) text = """\ %(from.fullname)s has rebased the review branch onto: %(new_upstream)s""" % data else: text = "%(from.fullname)s has rewritten the history on the review branch." % data body += """%s """ % textutils.reflow(text, line_length) body += """The new branch log is: """ for commit in rebased_commits: body += commit.oneline(db) + "\n" files = [] parent_message_id = getReviewMessageId(db, to_user, review, files) message_id = generateMessageId(len(files) + 1) files.append(sendMail( db, review, message_id, from_user, to_user, recipients, subject, body, parent_message_id=parent_message_id)) return files
def sendFiltersApplied(db, from_user, to_user, review, globalfilters, parentfilters, assigned): # First check if the user has activated email sending at all. if not to_user.getPreference(db, "email.activated"): return [] line_length = to_user.getPreference(db, "email.lineLength") hr = "-" * line_length data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'review.branch.name': review.branch.name, 'review.branch.repository': review.repository.getURL(db, to_user), 'from.fullname': from_user.fullname, 'hr': hr } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data if globalfilters: what = "global filters" else: what = "global filters from upstream repositories" text = ("%s has modified the assignments in the review by making %s apply, " "which they previously did not. This had the effect that you are " "now a %s the review." % (from_user.fullname, what, "reviewer of changes in" if assigned else "watcher of")) body += """%s """ % textutils.reflow(text, line_length) if assigned: body += renderFiles(db, to_user, review, "The following changes are now assigned to you:", assigned) files = [] parent_message_id = None cursor = db.cursor() cursor.execute("SELECT messageid FROM reviewmessageids WHERE uid=%s AND review=%s", [to_user.id, review.id]) row = cursor.fetchone() if not row: files.extend(sendReviewPlaceholder(db, to_user, review)) cursor.execute("SELECT messageid FROM reviewmessageids WHERE uid=%s AND review=%s", [to_user.id, review.id]) row = cursor.fetchone() if row: parent_message_id = "<%s@%s>" % (row[0], configuration.base.HOSTNAME) files.append(sendMail(db, review, generateMessageId(len(files) + 1), from_user, to_user, [to_user], generateSubjectLine(db, to_user, review, "updatedReview.parentFiltersApplied"), body, parent_message_id=parent_message_id)) return files
def reflow(text, indent): try: import textutils return textutils.reflow(text, indent=indent) except Exception: # The 'textutils' module depends on 'configuration', so make our # dependency on it conditional. return text
def sendVerificationMail(db, user, email_id=None): cursor = db.cursor() if email_id is None: cursor.execute("""SELECT email FROM users WHERE id=%s""", (user.id,)) email_id, = cursor.fetchone() cursor.execute("""SELECT email, verification_token FROM useremails WHERE id=%s""", (email_id,)) email, verification_token = cursor.fetchone() if verification_token is None: verification_token = auth.getToken(encode=base64.b16encode) with db.updating_cursor("useremails") as cursor: cursor.execute("""UPDATE useremails SET verification_token=%s WHERE id=%s""", (verification_token, email_id)) if configuration.base.ACCESS_SCHEME == "http": protocol = "http" else: protocol = "https" administrators = dbutils.getAdministratorContacts(db, indent=2) if administrators: administrators = ":\n\n%s" % administrators else: administrators = "." recipients = [mailutils.User(user.name, email, user.fullname)] subject = "[Critic] Please verify your email: %s" % email body = textutils.reflow(""" This is a message from the Critic code review system at %(hostname)s. The user '%(username)s' on this system has added this email address to his/her account. If this is you, please confirm this by following this link: %(url_prefix)s/verifyemail?email=%(email)s&token=%(verification_token)s If this is not you, you can safely ignore this email. If you wish to report abuse, please contact the Critic system's administrators%(administrators)s """ % { "hostname": configuration.base.HOSTNAME, "username": user.name, "email": email, "url_prefix": "%s://%s" % (protocol, configuration.base.HOSTNAME), "verification_token": verification_token, "administrators": administrators }) mailutils.sendMessage(recipients, subject, body)
def sendVerificationMail(db, user, email_id=None): cursor = db.cursor() if email_id is None: cursor.execute("""SELECT email FROM users WHERE id=%s""", (user.id,)) email_id, = cursor.fetchone() cursor.execute("""SELECT email, verification_token FROM useremails WHERE id=%s""", (email_id,)) email, verification_token = cursor.fetchone() if verification_token is None: verification_token = auth.getToken(encode=base64.b16encode) cursor.execute("""UPDATE useremails SET verification_token=%s WHERE id=%s""", (verification_token, email_id)) if configuration.base.ACCESS_SCHEME == "http": protocol = "http" else: protocol = "https" administrators = dbutils.getAdministratorContacts(db, indent=2) if administrators: administrators = ":\n\n%s" % administrators else: administrators = "." recipients = [mailutils.User(user.name, email, user.fullname)] subject = "[Critic] Please verify your email: %s" % email body = textutils.reflow(""" This is a message from the Critic code review system at %(hostname)s. The user '%(username)s' on this system has added this email address to his/her account. If this is you, please confirm this by following this link: %(url_prefix)s/verifyemail?email=%(email)s&token=%(verification_token)s If this is not you, you can safely ignore this email. If you wish to report abuse, please contact the Critic system's administrators%(administrators)s """ % { "hostname": configuration.base.HOSTNAME, "username": user.name, "email": email, "url_prefix": "%s://%s" % (protocol, configuration.base.HOSTNAME), "verification_token": verification_token, "administrators": administrators }) mailutils.sendMessage(recipients, subject, body)
def sendFiltersApplied(db, from_user, to_user, review, globalfilters, parentfilters, assigned): # First check if we can/should send emails to the user at all. try: checkEmailEnabled(db, to_user) subject = generateSubjectLine(db, to_user, review, "updatedReview.parentFiltersApplied") except MailDisabled: return [] line_length = to_user.getPreference(db, "email.lineLength") hr = "-" * line_length data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'review.branch.name': review.branch.name, 'review.branch.repository': review.repository.getURL(db, to_user), 'from.fullname': from_user.fullname, 'hr': hr } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data if globalfilters: what = "global filters" else: what = "global filters from upstream repositories" text = ("%s has modified the assignments in the review by making %s apply, " "which they previously did not. This had the effect that you are " "now a %s the review." % (from_user.fullname, what, "reviewer of changes in" if assigned else "watcher of")) body += """%s """ % textutils.reflow(text, line_length) if assigned: body += renderFiles(db, to_user, review, "The following changes are now assigned to you:", assigned) files = [] parent_message_id = getReviewMessageId(db, to_user, review, files) message_id = generateMessageId(len(files) + 1) files.append(sendMail( db, review, message_id, from_user, to_user, [to_user], subject, body, parent_message_id=parent_message_id)) return files
def sendReviewRebased(db, from_user, to_user, recipients, review, new_upstream, rebased_commits, onto_branch=None): # First check if the user has activated email sending at all. if not to_user.getPreference(db, "email.activated"): return [] if from_user == to_user and to_user.getPreference(db, "email.ignoreOwnChanges"): return [] line_length = to_user.getPreference(db, "email.lineLength") hr = "-" * line_length data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'review.branch.name': review.branch.name, 'review.branch.repository': "%s:%s" % (configuration.base.HOSTNAME, review.repository.path), 'from.fullname': from_user.fullname, 'hr': hr } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data if new_upstream or onto_branch: if onto_branch is None: data['target'] = "the commit '%s'" % new_upstream else: data['target'] = "the branch '%s'" % onto_branch text = "%(from.fullname)s has rebased the review branch onto %(target)s." % data else: text = "%(from.fullname)s has rewritten the history on the review branch." % data body += """%s """ % textutils.reflow(text, line_length) body += """The new branch log is: """ for commit in rebased_commits: body += "%s %s\n" % (commit.sha1[:8], commit.niceSummary()) cursor = db.cursor() cursor.execute("SELECT messageid FROM reviewmessageids WHERE uid=%s AND review=%s", [to_user.id, review.id]) row = cursor.fetchone() if row: parent_message_id = "<%s@%s>" % (row[0], configuration.base.HOSTNAME) else: parent_message_id = None return [sendMail(db, review, generateMessageId(), from_user, to_user, recipients, generateSubjectLine(db, to_user, review, "updatedReview.reviewRebased"), body, parent_message_id=parent_message_id)]
def sendExtensionOutput(db, user_id, batch_id, output): to_user = dbutils.User.fromId(db, user_id) cursor = db.cursor() cursor.execute("SELECT review, uid FROM batches WHERE id=%s", (batch_id,)) review_id, batch_user_id = cursor.fetchone() review = dbutils.Review.fromId(db, review_id) batch_user = dbutils.User.fromId(db, batch_user_id) # First check if we can/should send emails to the user at all. try: checkEmailEnabled(db, to_user) subject = generateSubjectLine(db, to_user, review, "extensionOutput") except MailDisabled: return [] line_length = to_user.getPreference(db, "email.lineLength") hr = "-" * line_length data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'batch.user.fullname': batch_user.fullname, 'hr': hr } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data text = "A batch of changes submitted by %(batch.user.fullname)s has been processed by your installed extensions." % data body += """%s """ % textutils.reflow(text, line_length) body += "The extensions generated the following output:\n%s" % output files = [] parent_message_id = getReviewMessageId(db, to_user, review, files) message_id = generateMessageId(len(files) + 1) files.append(sendMail( db, review, message_id, to_user, to_user, [to_user], subject, body, parent_message_id=parent_message_id)) return files
def sendExtensionOutput(db, user_id, batch_id, output): user = dbutils.User.fromId(db, user_id) # Explicitly *don't* check if the user has activated email sending. This # allows a user to disable email sending and then install extensions whose # output is still sent. #if not user.getPreference(db, "email.activated"): return [] line_length = user.getPreference(db, "email.lineLength") hr = "-" * line_length cursor = db.cursor() cursor.execute("SELECT review, uid FROM batches WHERE id=%s", (batch_id,)) review_id, batch_user_id = cursor.fetchone() review = dbutils.Review.fromId(db, review_id) batch_user = dbutils.User.fromId(db, batch_user_id) data = { 'review.id': review.id, 'review.url': review.getURL(db, user, 2), 'batch.user.fullname': batch_user.fullname, 'hr': hr } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data text = "A batch of changes submitted by %(batch.user.fullname)s has been processed by your installed extensions." % data body += """%s """ % textutils.reflow(text, line_length) body += "The extensions generated the following output:\n%s" % output cursor.execute("SELECT messageid FROM reviewmessageids WHERE uid=%s AND review=%s", [user.id, review.id]) row = cursor.fetchone() if row: parent_message_id = "<%s@%s>" % (row[0], configuration.base.HOSTNAME) else: parent_message_id = None return [sendMail(db, review, generateMessageId(), user, user, [user], generateSubjectLine(db, user, review, "extensionOutput"), body, parent_message_id=parent_message_id)]
def updateBranch(user_name, repository_name, name, old, new, multiple): repository = gitutils.Repository.fromName(db, repository_name) processCommits(repository_name, new) try: branch = dbutils.Branch.fromName(db, repository, name) base_branch_id = branch.base.id if branch.base else None except: raise IndexException, "The branch '%s' is not in the database! (This should never happen.)" % name if branch.head.sha1 != old: if new == branch.head.sha1: # This is what we think the ref ought to be already. Do nothing, # and let the repository "catch up." return else: data = { "name": name, "old": old[:8], "new": new[:8], "current": branch.head.sha1[:8] } message = """CONFUSED! Git thinks %(name)s points to %(old)s, but Critic thinks it points to %(current)s. Rejecting push since it would only makes matters worse. To resolve this problem, use git push critic %(current)s:%(name)s to resynchronize the Git repository with Critic's database.""" % data raise IndexException, textutils.reflow(message, line_length=80 - len("remote: ")) cursor = db.cursor() cursor.execute( "SELECT remote, remote_name, forced FROM trackedbranches WHERE repository=%s AND local_name=%s AND NOT disabled", (repository.id, name)) row = cursor.fetchone() if row: remote, remote_name, forced = row tracked_branch = "%s in %s" % (remote_name, remote) assert not forced or not name.startswith("r/") if user_name != configuration.base.SYSTEM_USER_NAME: raise IndexException, """\ The branch '%s' is set up to track '%s' in %s Please don't push it manually to this repository.""" % (name, remote_name, remote) elif not name.startswith("r/"): conflicting = repository.revlist([branch.head.sha1], [new]) added = repository.revlist([new], [branch.head.sha1]) if conflicting: if forced: if branch.base is None: cursor.executemany( """DELETE FROM reachable USING commits WHERE reachable.branch=%s AND reachable.commit=commits.id AND commits.sha1=%s""", [(branch.id, sha1) for sha1 in conflicting]) else: output = "Non-fast-forward update detected; deleting and recreating branch." deleteBranch(repository.name, branch.name) createBranch(None, repository, branch.name, new) return output else: raise IndexException, """\ Rejecting non-fast-forward update of branch. To perform the update, you can delete the branch using git push critic :%s first, and then repeat this push.""" % name cursor.executemany( """INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE sha1=%s""", [(branch.id, sha1) for sha1 in added]) new_head = gitutils.Commit.fromSHA1(db, repository, new) cursor.execute("UPDATE branches SET head=%s WHERE id=%s", (new_head.getId(db), branch.id)) output = "" if conflicting: output += "Pruned %d conflicting commits." % len(conflicting) if added: output += "\nAdded %d new commits." % len(added) return output.strip() if output else None else: tracked_branch = False user = getUser(db, user_name) if isinstance(user, str): if not tracked_branch: commit = gitutils.Commit.fromSHA1(db, repository, new) user = dbutils.User.fromId(db, commit.committer.getUserId(db)) else: user = dbutils.User(0, configuration.base.SYSTEM_USER_NAME, configuration.base.SYSTEM_USER_EMAIL, "Critic System", "current") cursor.execute("SELECT id FROM reviews WHERE branch=%s", (branch.id, )) row = cursor.fetchone() is_review = bool(row) if is_review: if multiple: raise IndexException, """\ Refusing to update review in push of multiple refs. Please push one review branch at a time.""" review_id = row[0] cursor.execute( """SELECT id, old_head, old_upstream, new_upstream, uid, branch FROM reviewrebases WHERE review=%s AND new_head IS NULL""", (review_id, )) row = cursor.fetchone() if row: if tracked_branch: raise IndexException, "Refusing to perform a review rebase via an automatic update." rebase_id, old_head_id, old_upstream_id, new_upstream_id, rebaser_id, onto_branch = row review = dbutils.Review.fromId(db, review_id) rebaser = dbutils.User.fromId(db, rebaser_id) if isinstance(user, dbutils.User): if rebaser.id != user.id: if user_name == configuration.base.SYSTEM_USER_NAME: user = rebaser else: raise IndexException, """\ This review is currently being rebased by %s <%s> and can't be otherwise updated right now.""" % (rebaser.fullname, rebaser.email) else: assert user == configuration.base.SYSTEM_USER_NAME user = rebaser old_head = gitutils.Commit.fromId(db, repository, old_head_id) old_commitset = log.commitset.CommitSet(review.branch.commits) if old_head.sha1 != old: raise IndexException, """\ Unexpected error. The branch appears to have been updated since your rebase was prepared. You need to cancel the rebase via the review front-page and then try again, and/or report a bug about this error.""" if old_upstream_id is not None: new_head = gitutils.Commit.fromSHA1(db, repository, new) old_upstream = gitutils.Commit.fromId(db, repository, old_upstream_id) if new_upstream_id is not None: new_upstream = gitutils.Commit.fromId( db, repository, new_upstream_id) else: if len(new_head.parents) != 1: raise IndexException, "Invalid rebase: New head can't be a merge commit." new_upstream = gitutils.Commit.fromSHA1( db, repository, new_head.parents[0]) if new_upstream in old_commitset.getTails(): old_upstream = new_upstream = None else: old_upstream = None if old_upstream: if old_upstream.sha1 != repository.mergebase( [old_upstream.sha1, new_upstream.sha1]): raise IndexException, """\ Invalid rebase: The new upstream commit is not a descendant of the old upstream commit. You may want to cancel the rebase via the review front-page, and prepare another one specifying the correct new upstream commit; or rebase the branch onto the new upstream specified and then push that instead.""" if new_upstream.sha1 != repository.mergebase( [new_upstream.sha1, new]): raise IndexException, """\ Invalid rebase: The new upstream commit you specified when the rebase was prepared is not an ancestor of the commit now pushed. You may want to cancel the rebase via the review front-page, and prepare another one specifying the correct new upstream commit; or rebase the branch onto the new upstream specified and then push that instead.""" old_upstream_name = repository.findInterestingTag( db, old_upstream.sha1) or old_upstream.sha1 new_upstream_name = repository.findInterestingTag( db, new_upstream.sha1) or new_upstream.sha1 if onto_branch: merged_thing = "branch '%s'" % onto_branch else: merged_thing = "commit '%s'" % new_upstream_name merge_sha1 = repository.run('commit-tree', new_head.tree, '-p', old_head.sha1, '-p', new_upstream.sha1, input="""\ Merge %s into %s This commit was generated automatically by Critic as an equivalent merge to the rebase of the commits %s..%s onto the %s.""" % (merged_thing, review.branch.name, old_upstream_name, old_head.sha1, merged_thing), env={ 'GIT_AUTHOR_NAME': user.fullname, 'GIT_AUTHOR_EMAIL': user.email, 'GIT_COMMITTER_NAME': user.fullname, 'GIT_COMMITTER_EMAIL': user.email }).strip() merge = gitutils.Commit.fromSHA1(db, repository, merge_sha1) gituser_id = merge.author.getGitUserId(db) cursor.execute( """INSERT INTO commits (sha1, author_gituser, commit_gituser, author_time, commit_time) VALUES (%s, %s, %s, %s, %s) RETURNING id""", (merge_sha1, gituser_id, gituser_id, timestamp( merge.author.time), timestamp(merge.committer.time))) merge.id = cursor.fetchone()[0] cursor.executemany( "INSERT INTO edges (parent, child) VALUES (%s, %s)", [(old_head.getId(db), merge.id), (new_upstream.getId(db), merge.id)]) # Have to commit to make the new commit available to other DB # sessions right away, specifically so that the changeset # creation server can see it. db.commit() cursor.execute( """UPDATE reviewrebases SET old_head=%s, new_head=%s, new_upstream=%s WHERE review=%s AND new_head IS NULL""", (merge.id, new_head.getId(db), new_upstream.getId(db), review.id)) new_sha1s = repository.revlist([new], [new_upstream.sha1], '--topo-order') rebased_commits = [ gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in new_sha1s ] reachable_values = [(review.branch.id, sha1) for sha1 in new_sha1s] cursor.execute( "INSERT INTO previousreachable (rebase, commit) SELECT %s, commit FROM reachable WHERE branch=%s", (rebase_id, review.branch.id)) cursor.execute("DELETE FROM reachable WHERE branch=%s", (review.branch.id, )) cursor.executemany( "INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE commits.sha1=%s", reachable_values) cursor.execute( "UPDATE branches SET head=%s WHERE id=%s", (gitutils.Commit.fromSHA1( db, repository, new).getId(db), review.branch.id)) pending_mails = [] cursor.execute("SELECT uid FROM reviewusers WHERE review=%s", (review.id, )) recipients = [] for (user_id, ) in cursor.fetchall(): recipients.append(dbutils.User.fromId(db, user_id)) for to_user in recipients: pending_mails.extend( review_mail.sendReviewRebased(db, user, to_user, recipients, review, new_upstream_name, rebased_commits, onto_branch)) print "Rebase performed." review_utils.addCommitsToReview(db, user, review, [merge], pending_mails=pending_mails, silent_if_empty=set([merge]), full_merges=set([merge])) repository.keepalive(merge) else: old_commitset = log.commitset.CommitSet(review.branch.commits) new_sha1s = repository.revlist([new], old_commitset.getTails(), '--topo-order') if old_head.sha1 in new_sha1s: raise IndexException, """\ Invalid history rewrite: Old head of the branch reachable from the pushed ref; no history rewrite performed. (Cancel the rebase via the review front-page if you've changed your mind.)""" for new_sha1 in new_sha1s: new_head = gitutils.Commit.fromSHA1( db, repository, new_sha1) if new_head.tree == old_head.tree: break else: raise IndexException, """\ Invalid history rewrite: No commit on the rebased branch references the same tree as the old head of the branch.""" cursor.execute( """UPDATE reviewrebases SET new_head=%s WHERE review=%s AND new_head IS NULL""", (new_head.getId(db), review.id)) rebased_commits = [ gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in repository.revlist( [new_head], old_commitset.getTails(), '--topo-order') ] new_commits = [ gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in repository.revlist([new], [new_head], '--topo-order') ] reachable_values = [(review.branch.id, sha1) for sha1 in new_sha1s] cursor.execute( "INSERT INTO previousreachable (rebase, commit) SELECT %s, commit FROM reachable WHERE branch=%s", (rebase_id, review.branch.id)) cursor.execute("DELETE FROM reachable WHERE branch=%s", (review.branch.id, )) cursor.executemany( "INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE commits.sha1=%s", reachable_values) cursor.execute( "UPDATE branches SET head=%s WHERE id=%s", (gitutils.Commit.fromSHA1( db, repository, new).getId(db), review.branch.id)) pending_mails = [] cursor.execute("SELECT uid FROM reviewusers WHERE review=%s", (review.id, )) recipients = [] for (user_id, ) in cursor.fetchall(): recipients.append(dbutils.User.fromId(db, user_id)) for to_user in recipients: pending_mails.extend( review_mail.sendReviewRebased(db, user, to_user, recipients, review, None, rebased_commits)) print "History rewrite performed." if new_commits: review_utils.addCommitsToReview( db, user, review, new_commits, pending_mails=pending_mails) else: review_mail.sendPendingMails(pending_mails) repository.run('update-ref', 'refs/keepalive/%s' % old, old) return True elif old != repository.mergebase([old, new]): raise IndexException, "Rejecting non-fast-forward update of review branch." elif old != repository.mergebase([old, new]): raise IndexException, """\ Rejecting non-fast-forward update of branch. To perform the update, you can delete the branch using git push critic :%s first, and then repeat this push.""" % name cursor.execute( "SELECT id FROM branches WHERE repository=%s AND base IS NULL ORDER BY id ASC LIMIT 1", (repository.id, )) root_branch_id = cursor.fetchone()[0] def isreachable(sha1): #if rescan: return False #if is_review: cursor.execute("SELECT 1 FROM commits, reachable, branches WHERE commits.sha1=%s AND commits.id=reachable.commit AND reachable.branch=branches.id AND branches.repository=%s", [sha1, repository.id]) if is_review and sha1 == branch.tail: return True if base_branch_id: cursor.execute( "SELECT 1 FROM commits, reachable WHERE commits.sha1=%s AND commits.id=reachable.commit AND reachable.branch IN (%s, %s, %s)", [sha1, branch.id, base_branch_id, root_branch_id]) else: cursor.execute( "SELECT 1 FROM commits, reachable WHERE commits.sha1=%s AND commits.id=reachable.commit AND reachable.branch IN (%s, %s)", [sha1, branch.id, root_branch_id]) return cursor.fetchone() is not None stack = [new] commits = set() commit_list = [] processed = set() count = 0 while stack: sha1 = stack.pop() count += 1 if (count % 1000) == 0: stdout.write(".") if (count % 10000) == 0: stdout.write("\n") stdout.flush() if sha1 not in commits and not isreachable(sha1): commits.add(sha1) commit_list.append(sha1) #if is_review: # stack.append(gitutils.Commit.fromSHA1(repository, sha1).parents[0]) #else: stack.extend([ parent_sha1 for parent_sha1 in gitutils.Commit.fromSHA1( db, repository, sha1).parents if parent_sha1 not in processed ]) processed.add(sha1) branch = dbutils.Branch.fromName(db, repository, name) review = dbutils.Review.fromBranch(db, branch) if review: if review.state != "open": raise IndexException, """\ The review is closed and can't be extended. You need to reopen it at %s before you can add commits to it.""" % review.getURL(db, user, 2) all_commits = [ gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in reversed(commit_list) ] tails = CommitSet(all_commits).getTails() if old not in tails: raise IndexException, """\ Push rejected; would break the review. It looks like some of the pushed commits are reachable from the repository's main branch, and thus consequently the commits currently included in the review are too. Perhaps you should request a new review of the follow-up commits?""" review_utils.addCommitsToReview(db, user, review, all_commits, commitset=commits, tracked_branch=tracked_branch) reachable_values = [(branch.id, sha1) for sha1 in reversed(commit_list) if sha1 in commits] cursor.executemany( "INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE commits.sha1=%s", reachable_values) cursor.execute( "UPDATE branches SET head=%s WHERE id=%s", (gitutils.Commit.fromSHA1(db, repository, new).getId(db), branch.id)) db.commit() if configuration.extensions.ENABLED and review: extensions.executeProcessCommits( db, user, review, all_commits, gitutils.Commit.fromSHA1(db, repository, old), gitutils.Commit.fromSHA1(db, repository, new), stdout)
def createBranch(user, repository, name, head): processCommits(repository.name, head) cursor = db.cursor() def commit_id(sha1): cursor.execute("SELECT id FROM commits WHERE sha1=%s", [sha1]) return cursor.fetchone()[0] components = name.split("/") for index in range(1, len(components)): try: repository.revparse("refs/heads/%s" % "/".join(components[:index])) except: continue message = ( "Cannot create branch with name '%s' since there is already a branch named '%s' in the repository." % (name, "/".join(components[:index]))) raise IndexException, textutils.reflow(message, line_length=80 - len("remote: ")) if name.startswith("r/"): try: review_id = int(name[2:]) cursor.execute( "SELECT branches.name FROM reviews JOIN branches ON (branches.id=reviews.branch) WHERE reviews.id=%s", (review_id, )) row = cursor.fetchone() message = "Refusing to create review named as a number." if row: message += "\nDid you mean to push to the branch '%s', perhaps?" % row[ 0] raise IndexException, message except ValueError: pass if user.getPreference(db, "review.createViaPush"): the_commit = gitutils.Commit.fromSHA1(db, repository, head, commit_id(head)) all_commits = [the_commit] review = review_utils.createReview(db, user, repository, all_commits, name, the_commit.summary(), None, via_push=True) print "Submitted review: %s/r/%d" % (dbutils.getURLPrefix(db), review.id) if review.reviewers: print " Reviewers:" for reviewer in review.reviewers: print " %s <%s>" % (reviewer.fullname, reviewer.email) if review.watchers: print " Watchers:" for watcher in review.watchers: print " %s <%s>" % (watcher.fullname, watcher.email) if configuration.extensions.ENABLED: if extensions.executeProcessCommits(db, user, review, all_commits, None, the_commit, stdout): print print "Thank you!" return True else: raise IndexException, "Refusing to create review; user preference 'review.createViaPush' is not enabled." sha1 = head base = None tail = None cursor.execute( """SELECT 1 FROM reachable JOIN branches ON (branches.id=reachable.branch) JOIN repositories ON (repositories.id=branches.repository) WHERE repositories.id=%s LIMIT 1""", (repository.id, )) if cursor.fetchone(): def reachable(sha1): cursor.execute( """SELECT branches.id FROM branches JOIN reachable ON (reachable.branch=branches.id) JOIN commits ON (commits.id=reachable.commit) WHERE branches.repository=%s AND branches.type='normal' AND commits.sha1=%s ORDER BY reachable.branch ASC LIMIT 1""", (repository.id, sha1)) return cursor.fetchone() else: def reachable(sha1): return None commit_map = {} commit_list = [] row = reachable(sha1) if row: # Head of branch is reachable from an existing branch. Could be because # this branch is actually empty (just created with no "own" commits) or # it could have been merged into some other already existing branch. We # can't tell, so we just record it as empty. base = row[0] tail = sha1 else: stack = [] while True: if sha1 not in commit_map: commit = gitutils.Commit.fromSHA1(db, repository, sha1) commit_map[sha1] = commit commit_list.append(commit) for sha1 in commit.parents: if sha1 not in commit_map: row = reachable(sha1) if not row: stack.append(sha1) elif base is None: base = row[0] tail = sha1 base_chain = [base] while True: cursor.execute( "SELECT base FROM branches WHERE id=%s", (base_chain[-1], )) next = cursor.fetchone()[0] if next is None: break else: base_chain.append(next) def reachable(sha1): cursor.execute( """SELECT 1 FROM reachable JOIN commits ON (commits.id=reachable.commit) WHERE reachable.branch=ANY (%s) AND commits.sha1=%s""", (base_chain, sha1)) return cursor.fetchone() if stack: sha1 = stack.pop(0) else: break if len(commit_list) % 10000 > 1000: stdout.write("\n") stdout.flush() if not base: cursor.execute( "INSERT INTO branches (repository, name, head) VALUES (%s, %s, %s) RETURNING id", (repository.id, name, commit_id(head))) branch_id = cursor.fetchone()[0] else: cursor.execute( "INSERT INTO branches (repository, name, head, base, tail) VALUES (%s, %s, %s, %s, %s) RETURNING id", (repository.id, name, commit_id(head), base, commit_id(tail))) branch_id = cursor.fetchone()[0] cursor.execute("SELECT name FROM branches WHERE id=%s", [base]) print "Added branch based on %s containing %d commit%s:" % ( cursor.fetchone()[0], len(commit_list), "s" if len(commit_list) > 1 else "") print " %s/log?repository=%d&branch=%s" % (dbutils.getURLPrefix(db), repository.id, name) if len(commit_list) > 1: print "To create a review of all %d commits:" % len(commit_list) else: print "To create a review of the commit:" print " %s/createreview?repository=%d&branch=%s" % ( dbutils.getURLPrefix(db), repository.id, name) reachable_values = [(branch_id, commit.sha1) for commit in commit_list] cursor.executemany( "INSERT INTO reachable (branch, commit) SELECT %s, id FROM commits WHERE sha1=%s", reachable_values) if isinstance(user, str): user_name = user else: user_name = user.name if not repository.hasMainBranch( ) and user_name == configuration.base.SYSTEM_USER_NAME: cursor.execute("UPDATE repositories SET branch=%s WHERE id=%s", (branch_id, repository.id))
def sendPing(db, from_user, to_user, recipients, review, note): # First check if the user has activated email sending at all. if not to_user.getPreference(db, "email.activated"): return [] line_length = to_user.getPreference(db, "email.lineLength") hr = "-" * line_length data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'review.branch.name': review.branch.name, 'review.branch.repository': "%s:%s" % (configuration.base.HOSTNAME, review.repository.path), 'from.fullname': from_user.fullname, 'hr': hr } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data body += """%(from.fullname)s has pinged the review! """ % data if note: body += """Additional information from %s: %s """ % (from_user.getFirstName(), textutils.reflow(note, line_length, indent=2)) cursor = db.cursor() cursor.execute("""SELECT reviewfiles.file, SUM(reviewfiles.deleted), SUM(reviewfiles.inserted) FROM reviewfiles JOIN reviewuserfiles ON (reviewuserfiles.file=reviewfiles.id) WHERE reviewfiles.review=%s AND reviewfiles.state='pending' AND reviewuserfiles.uid=%s GROUP BY reviewfiles.file""", (review.id, to_user.id)) pending_files_lines = cursor.fetchall() cursor.execute("""SELECT DISTINCT changesets.child FROM reviewfiles JOIN reviewuserfiles ON (reviewuserfiles.file=reviewfiles.id) JOIN changesets ON (changesets.id=reviewfiles.changeset) WHERE reviewfiles.review=%s AND reviewfiles.state='pending' AND reviewuserfiles.uid=%s""", (review.id, to_user.id)) pending_commits = cursor.fetchall() body += renderFiles(db, to_user, review, "These pending changes are assigned to you:", pending_files_lines, pending_commits, showcommit_link=True) cursor.execute("SELECT messageid FROM reviewmessageids WHERE uid=%s AND review=%s", [to_user.id, review.id]) row = cursor.fetchone() if row: parent_message_id = "<%s@%s>" % (row[0], configuration.base.HOSTNAME) else: parent_message_id = None return [sendMail(db, review, generateMessageId(), from_user, to_user, recipients, generateSubjectLine(db, to_user, review, "pingedReview"), body, parent_message_id=parent_message_id)]
def sendReviewAddedCommits(db, from_user, to_user, recipients, review, changesets, tracked_branch=False): # First check if the user has activated email sending at all. if not to_user.getPreference(db, "email.activated"): return [] if from_user == to_user and to_user.getPreference(db, "email.ignoreOwnChanges"): return [] line_length = to_user.getPreference(db, "email.lineLength") hr = "-" * line_length relevant_only = to_user not in review.owners and to_user != from_user and to_user.getPreference(db, "email.updatedReview.relevantChangesOnly") cursor = db.cursor() if relevant_only: cursor.execute("SELECT type FROM reviewusers WHERE review=%s AND uid=%s", (review.id, to_user.id)) if cursor.fetchone()[0] == 'manual': relevant_only = False if relevant_only: relevant_files = review.getRelevantFiles(db, to_user) relevant_commits = set() for changeset in changesets: for file in changeset.files: if file.id in relevant_files: relevant_commits.add(changeset.child.getId(db)) break else: cursor.execute("SELECT id FROM commentchains WHERE review=%s AND state='addressed' AND addressed_by=%s", (review.id, changeset.child.getId(db))) for chain_id in cursor.fetchall(): cursor.execute("SELECT 1 FROM commentchainusers WHERE chain=%s AND uid=%s", (chain_id, to_user.id)) if cursor.fetchone(): relevant_commits.add(changeset.child.getId(db)) break if not relevant_commits: return [] else: relevant_commits = None data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'review.branch.name': review.branch.name, 'review.branch.repository': "%s:%s" % (configuration.base.HOSTNAME, review.repository.path), 'hr': hr } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data commits = [] for index, changeset in enumerate(changesets): if changeset.parent.sha1 == changeset.child.parents[0]: commits.append(changeset.child) commitset = log_commitset.CommitSet(commits) if tracked_branch: body += "The automatic tracking of\n %s\n" % tracked_branch body += textutils.reflow("has updated the review by pushing %sadditional commit%s to the branch" % ("an " if len(commits) == 1 else "", "s" if len(commits) > 1 else ""), line_length) else: body += textutils.reflow("%s has updated the review by pushing %sadditional commit%s to the branch" % (from_user.fullname, "an " if len(commits) == 1 else "", "s" if len(commits) > 1 else ""), line_length) body += "\n %s\n" % review.branch.name body += textutils.reflow("in the repository", line_length) body += "\n %s:%s\n\n\n" % (configuration.base.HOSTNAME, review.repository.path) cursor.execute("""SELECT file, SUM(deleted), SUM(inserted) FROM fullreviewuserfiles WHERE review=%%s AND changeset IN (%s) AND state='pending' AND assignee=%%s GROUP BY file""" % ",".join(["%s"] * len(changesets)), [review.id] + [changeset.id for changeset in changesets] + [to_user.id]) pending_files_lines = cursor.fetchall() if pending_files_lines: heads = commitset.getHeads() tails = commitset.getFilteredTails(review.repository) if len(heads) == 1 and len(tails) == 1: showcommit_link = (tails.pop()[:8], heads.pop().sha1[:8]) else: showcommit_link = False body += renderFiles(db, to_user, review, "These changes were assigned to you:", pending_files_lines, showcommit_link=showcommit_link) all_commits = to_user.getPreference(db, "email.updatedReview.displayCommits") context_lines = to_user.getPreference(db, "email.comment.contextLines") if all_commits: body += "The additional commit%s requested to be reviewed are:\n\n" % ("s" if len(commits) > 1 else "") contextLines = to_user.getPreference(db, "email.updatedReview.diff.contextLines") diffMaxLines = to_user.getPreference(db, "email.updatedReview.diff.maxLines") displayStats = to_user.getPreference(db, "email.updatedReview.displayStats") statsMaxLines = to_user.getPreference(db, "email.updatedReview.stats.maxLines") if contextLines < 0: contextLines = 0 if diffMaxLines == 0: diffs = None else: diffs = {} lines = 0 for commit in commits: if len(commit.parents) == 1 and (relevant_commits is None or commit.getId(db) in relevant_commits): cursor.execute("""SELECT id FROM reviewchangesets JOIN changesets ON (id=changeset) WHERE review=%s AND child=%s""", (review.id, commit.getId(db))) (changeset_id,) = cursor.fetchone() diff = changeset_text.unified(db, changeset_load.loadChangeset(db, review.repository, changeset_id), contextLines) diffs[commit] = diff lines += diff.count("\n") if lines > diffMaxLines: diffs = None break if not displayStats or statsMaxLines == 0: stats = None else: stats = {} lines = 0 for commit in commits: commit_stats = review.repository.run("show", "--oneline", "--stat", commit.sha1).split('\n', 1)[1] stats[commit] = commit_stats lines += commit_stats.count('\n') if lines > statsMaxLines: stats = None break for index, commit in enumerate(commits): if index > 0: body += "\n\n\n" body += """Commit: %(sha1)s Author: %(author.fullname)s <%(author.email)s> at %(author.time)s %(message)s """ % { 'sha1': commit.sha1, 'author.fullname': commit.author.getFullname(db), 'author.email': commit.author.email, 'author.time': time.strftime("%Y-%m-%d %H:%M:%S", commit.author.time), 'message': textutils.reflow(commit.message.strip(), line_length, indent=2) } if stats and commit in stats: body += "---\n" + stats[commit] if diffs and commit in diffs: body += "\n" + diffs[commit] cursor.execute("SELECT id FROM commentchains WHERE review=%s AND state='addressed' AND addressed_by=%s", (review.id, commit.getId(db))) rows = cursor.fetchall() if rows: for (chain_id,) in rows: chain = review_comment.CommentChain.fromId(db, chain_id, to_user, review=review) chain.loadComments(db, to_user, include_draft_comments=False) body += "\n\n" + renderChainInMail(db, to_user, chain, None, "addressed", None, line_length, context_lines) cursor.execute("SELECT messageid FROM reviewmessageids WHERE uid=%s AND review=%s", [to_user.id, review.id]) row = cursor.fetchone() files = [] if not row: files = sendReviewPlaceholder(db, to_user, review) cursor.execute("SELECT messageid FROM reviewmessageids WHERE uid=%s AND review=%s", [to_user.id, review.id]) row = cursor.fetchone() if row: parent_message_id = "<%s@%s>" % (row[0], configuration.base.HOSTNAME) else: parent_message_id = None return files + [sendMail(db, review, generateMessageId(), from_user, to_user, recipients, generateSubjectLine(db, to_user, review, "updatedReview.commitsPushed"), body, parent_message_id=parent_message_id)]
def sendReviewBatch(db, from_user, to_user, recipients, review, batch_id, was_accepted, is_accepted, profiler=None): if profiler: profiler.check("generate mail: start") # First check if the user has activated email sending at all. if not to_user.getPreference(db, "email.activated"): return [] if from_user == to_user and to_user.getPreference(db, "email.ignoreOwnChanges"): return [] cursor = db.cursor() line_length = to_user.getPreference(db, "email.lineLength") relevant_only = to_user not in review.owners and to_user != from_user and to_user.getPreference(db, "email.updatedReview.relevantChangesOnly") mail_index = [0] if relevant_only: cursor.execute("SELECT type FROM reviewusers WHERE review=%s AND uid=%s", (review.id, to_user.id)) if cursor.fetchone()[0] == 'manual': relevant_only = False def localGenerateMessageId(): mail_index[0] += 1 return generateMessageId(mail_index[0]) if profiler: profiler.check("generate mail: prologue") if relevant_only: relevant_files = review.getRelevantFiles(db, to_user) else: relevant_files = None if profiler: profiler.check("generate mail: get relevant files") cursor.execute("SELECT comment FROM batches WHERE id=%s", [batch_id]) batch_chain_id = cursor.fetchone()[0] if profiler: profiler.check("generate mail: batch chain") cursor.execute("""SELECT reviewfiles.file, SUM(reviewfiles.deleted), SUM(reviewfiles.inserted) FROM reviewfiles JOIN reviewfilechanges ON (reviewfilechanges.file=reviewfiles.id) WHERE reviewfilechanges.batch=%s AND reviewfilechanges.to='reviewed' GROUP BY reviewfiles.file""", (batch_id,)) reviewed_files_lines = cursor.fetchall() if profiler: profiler.check("generate mail: reviewed files/lines") cursor.execute("""SELECT DISTINCT changesets.child FROM reviewfiles JOIN reviewfilechanges ON (reviewfilechanges.file=reviewfiles.id) JOIN changesets ON (changesets.id=reviewfiles.changeset) WHERE reviewfilechanges.batch=%s AND reviewfilechanges.to='reviewed'""", (batch_id,)) reviewed_commits = cursor.fetchall() if profiler: profiler.check("generate mail: reviewed commits") cursor.execute("""SELECT reviewfiles.file, SUM(reviewfiles.deleted), SUM(reviewfiles.inserted) FROM reviewfiles JOIN reviewfilechanges ON (reviewfilechanges.file=reviewfiles.id) WHERE reviewfilechanges.batch=%s AND reviewfilechanges.to='pending' GROUP BY reviewfiles.file""", (batch_id,)) unreviewed_files_lines = cursor.fetchall() if profiler: profiler.check("generate mail: unreviewed files/lines") cursor.execute("""SELECT DISTINCT changesets.child FROM reviewfiles JOIN reviewfilechanges ON (reviewfilechanges.file=reviewfiles.id) JOIN changesets ON (changesets.id=reviewfiles.changeset) WHERE reviewfilechanges.batch=%s AND reviewfilechanges.to='pending'""", (batch_id,)) unreviewed_commits = cursor.fetchall() if profiler: profiler.check("generate mail: unreviewed commits") reviewed_files = renderFiles(db, to_user, review, "Reviewed Files:", reviewed_files_lines, reviewed_commits, relevant_only, relevant_files) unreviewed_files = renderFiles(db, to_user, review, "Unreviewed Files:", unreviewed_files_lines, unreviewed_commits, relevant_only, relevant_files) if profiler: profiler.check("generate mail: render files") context_lines = to_user.getPreference(db, "email.comment.contextLines") comment_ids = set() def isRelevantComment(chain): if chain.file_id is None or chain.file_id in relevant_files: return True cursor.execute("SELECT 1 FROM commentchainusers WHERE chain=%s AND uid=%s", (chain.id, to_user.id)) return cursor.fetchone() is not None def fetchNewCommentChains(): chains = [] for (chain_id,) in cursor.fetchall(): if chain_id != batch_chain_id: chain = review_comment.CommentChain.fromId(db, chain_id, from_user, review=review) if not relevant_only or isRelevantComment(chain): chain.loadComments(db, from_user) chains.append((chain, None, None)) return chains def fetchAdditionalCommentChains(): chains = [] for chain_id, comment_id, new_state, new_type in cursor.fetchall(): if comment_id is not None or new_state is not None or new_type is not None: chain = review_comment.CommentChain.fromId(db, chain_id, from_user, review=review) if not relevant_only or isRelevantComment(chain): chain.loadComments(db, from_user) chains.append((chain, new_state, new_type)) return chains cursor.execute("SELECT id FROM commentchains WHERE batch=%s AND type='issue' ORDER BY id ASC", [batch_id]) new_issues = fetchNewCommentChains() if profiler: profiler.check("generate mail: new issues") cursor.execute("SELECT id FROM commentchains WHERE batch=%s AND type='note' ORDER BY id ASC", [batch_id]) new_notes = fetchNewCommentChains() if profiler: profiler.check("generate mail: new notes") cursor.execute("""SELECT commentchains.id, comments.id, commentchainchanges.to_state, commentchainchanges.to_type FROM commentchains LEFT OUTER JOIN comments ON (commentchains.id=comments.chain AND comments.batch=%s) LEFT OUTER JOIN commentchainchanges ON (commentchains.id=commentchainchanges.chain AND commentchainchanges.batch=%s) WHERE commentchains.review=%s AND commentchains.batch!=%s""", [batch_id, batch_id, review.id, batch_id]) additional_comments = fetchAdditionalCommentChains() if profiler: profiler.check("generate mail: additional comments") if is_accepted != was_accepted and not reviewed_files and not unreviewed_files and not new_issues and not new_notes and not additional_comments: return [] data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'review.branch.name': review.branch.name, 'review.branch.repository': "%s:%s" % (configuration.base.HOSTNAME, review.repository.path), 'hr': "-" * line_length } header = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data if batch_chain_id is not None: batch_chain = review_comment.CommentChain.fromId(db, batch_chain_id, from_user, review=review) else: batch_chain = None data["batch.author.fullname"] = from_user.fullname first_name = from_user.getFirstName() if batch_chain is not None: batch_chain.loadComments(db, from_user) comment_ids.add(batch_chain.comments[0].id) remark = """%s'%s comment: %s """ % (first_name, first_name[-1] != 's' and 's' or '', textutils.reflow(batch_chain.comments[0].comment, line_length, indent=2)) else: remark = "" body = header body += textutils.reflow("%(batch.author.fullname)s has submitted a batch of changes to the review." % data, line_length) body += "\n\n\n" body += remark if not was_accepted and is_accepted: state_change = textutils.reflow("The review is now ACCEPTED!", line_length) + "\n\n\n" elif was_accepted and not is_accepted: state_change = textutils.reflow("The review is NO LONGER ACCEPTED!", line_length) + "\n\n\n" else: state_change = "" body += state_change body += reviewed_files body += unreviewed_files subject = generateSubjectLine(db, to_user, review, "updatedReview.submittedChanges") def renderCommentChains(chains): result = "" if chains: for chain, new_state, new_type in chains: for focus_comment in chain.comments: if focus_comment.batch_id == batch_id: break else: focus_comment = None if focus_comment is not None or new_state is not None or new_type is not None: result += renderChainInMail(db, to_user, chain, focus_comment, new_state, new_type, line_length, context_lines) + "\n\n" if focus_comment is not None: comment_ids.add(focus_comment.id) return result body += renderCommentChains(new_issues) body += renderCommentChains(new_notes) if profiler: profiler.check("generate mail: render new comment chains") comment_threading = to_user.getPreference(db, "email.updatedReview.commentThreading") send_main_mail = state_change or reviewed_files or unreviewed_files or new_issues or new_notes if not comment_threading: send_main_mail = send_main_mail or additional_comments body += renderCommentChains(additional_comments) if profiler: profiler.check("generate mail: render additional comments") review_message_id = [None] files = [] def getReviewMessageId(): if review_message_id[0] is None: cursor.execute("SELECT messageid FROM reviewmessageids WHERE uid=%s AND review=%s", [to_user.id, review.id]) row = cursor.fetchone() if not row: files.extend(sendReviewPlaceholder(db, to_user, review)) mail_index[0] += 1 cursor.execute("SELECT messageid FROM reviewmessageids WHERE uid=%s AND review=%s", [to_user.id, review.id]) row = cursor.fetchone() if row: review_message_id[0] = "<%s@%s>" % (row[0], configuration.base.HOSTNAME) return review_message_id[0] if send_main_mail: message_id = localGenerateMessageId() cursor.executemany("INSERT INTO commentmessageids (uid, comment, messageid) VALUES (%s, %s, %s)", [(to_user.id, comment_id, message_id) for comment_id in comment_ids]) files.append(sendMail(db, review, message_id, from_user, to_user, recipients, subject, body, parent_message_id=getReviewMessageId())) if comment_threading: threads = {} for chain, new_state, new_type in additional_comments: if chain.comments[-1].batch_id == batch_id: parent_comment_id = chain.comments[-2].id else: parent_comment_id = chain.comments[-1].id cursor.execute("""SELECT messageid FROM commentmessageids WHERE comment=%s AND uid=%s""", [parent_comment_id, to_user.id]) row = cursor.fetchone() if row: parent_message_id = "<%s@%s>" % (row[0], configuration.base.HOSTNAME) else: parent_message_id = getReviewMessageId() threads.setdefault(parent_message_id, []).append((chain, new_state, new_type)) for parent_message_id, chains in threads.items(): comment_ids = set() body = header + remark + renderCommentChains(chains) message_id = localGenerateMessageId() cursor.executemany("INSERT INTO commentmessageids (uid, comment, messageid) VALUES (%s, %s, %s)", [(to_user.id, comment_id, message_id) for comment_id in comment_ids]) files.append(sendMail(db, review, message_id, from_user, to_user, recipients, subject, body, parent_message_id=parent_message_id)) if profiler: profiler.check("generate mail: finished") return files
def reflow(message): return textutils.reflow(message, line_length=80 - len("remote: "))
def updateBranch(user_name, repository_name, name, old, new, multiple): repository = gitutils.Repository.fromName(db, repository_name) processCommits(repository_name, new) try: branch = dbutils.Branch.fromName(db, repository, name) base_branch_id = branch.base.id if branch.base else None except: raise IndexException, "The branch '%s' is not in the database! (This should never happen.)" % name if branch.head.sha1 != old: if new == branch.head.sha1: # This is what we think the ref ought to be already. Do nothing, # and let the repository "catch up." return else: data = { "name": name, "old": old[:8], "new": new[:8], "current": branch.head.sha1[:8] } message = """CONFUSED! Git thinks %(name)s points to %(old)s, but Critic thinks it points to %(current)s. Rejecting push since it would only makes matters worse. To resolve this problem, use git push critic %(current)s:%(name)s to resynchronize the Git repository with Critic's database.""" % data raise IndexException, textutils.reflow(message, line_length=80 - len("remote: ")) cursor = db.cursor() cursor.execute("SELECT remote, remote_name, forced FROM trackedbranches WHERE repository=%s AND local_name=%s AND NOT disabled", (repository.id, name)) row = cursor.fetchone() if row: remote, remote_name, forced = row tracked_branch = "%s in %s" % (remote_name, remote) assert not forced or not name.startswith("r/") if user_name != configuration.base.SYSTEM_USER_NAME: raise IndexException, """\ The branch '%s' is set up to track '%s' in %s Please don't push it manually to this repository.""" % (name, remote_name, remote) elif not name.startswith("r/"): conflicting = repository.revlist([branch.head.sha1], [new]) added = repository.revlist([new], [branch.head.sha1]) if conflicting: if forced: if branch.base is None: cursor.executemany("""DELETE FROM reachable USING commits WHERE reachable.branch=%s AND reachable.commit=commits.id AND commits.sha1=%s""", [(branch.id, sha1) for sha1 in conflicting]) else: output = "Non-fast-forward update detected; deleting and recreating branch." deleteBranch(repository.name, branch.name) createBranch(None, repository, branch.name, new) return output else: raise IndexException, """\ Rejecting non-fast-forward update of branch. To perform the update, you can delete the branch using git push critic :%s first, and then repeat this push.""" % name cursor.executemany("""INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE sha1=%s""", [(branch.id, sha1) for sha1 in added]) new_head = gitutils.Commit.fromSHA1(db, repository, new) cursor.execute("UPDATE branches SET head=%s WHERE id=%s", (new_head.getId(db), branch.id)) output = "" if conflicting: output += "Pruned %d conflicting commits." % len(conflicting) if added: output += "\nAdded %d new commits." % len(added) return output.strip() if output else None else: tracked_branch = False user = getUser(db, user_name) if isinstance(user, str): if not tracked_branch: commit = gitutils.Commit.fromSHA1(db, repository, new) user = dbutils.User.fromId(db, commit.committer.getUserId(db)) else: user = dbutils.User(0, configuration.base.SYSTEM_USER_NAME, configuration.base.SYSTEM_USER_EMAIL, "Critic System", "current") cursor.execute("SELECT id FROM reviews WHERE branch=%s", (branch.id,)) row = cursor.fetchone() is_review = bool(row) if is_review: if multiple: raise IndexException, """\ Refusing to update review in push of multiple refs. Please push one review branch at a time.""" review_id = row[0] cursor.execute("""SELECT id, old_head, old_upstream, new_upstream, uid, branch FROM reviewrebases WHERE review=%s AND new_head IS NULL""", (review_id,)) row = cursor.fetchone() if row: if tracked_branch: raise IndexException, "Refusing to perform a review rebase via an automatic update." rebase_id, old_head_id, old_upstream_id, new_upstream_id, rebaser_id, onto_branch = row review = dbutils.Review.fromId(db, review_id) rebaser = dbutils.User.fromId(db, rebaser_id) if isinstance(user, dbutils.User): if rebaser.id != user.id: if user_name == configuration.base.SYSTEM_USER_NAME: user = rebaser else: raise IndexException, """\ This review is currently being rebased by %s <%s> and can't be otherwise updated right now.""" % (rebaser.fullname, rebaser.email) else: assert user == configuration.base.SYSTEM_USER_NAME user = rebaser old_head = gitutils.Commit.fromId(db, repository, old_head_id) old_commitset = log.commitset.CommitSet(review.branch.commits) if old_head.sha1 != old: raise IndexException, """\ Unexpected error. The branch appears to have been updated since your rebase was prepared. You need to cancel the rebase via the review front-page and then try again, and/or report a bug about this error.""" if old_upstream_id is not None: new_head = gitutils.Commit.fromSHA1(db, repository, new) old_upstream = gitutils.Commit.fromId(db, repository, old_upstream_id) if new_upstream_id is not None: new_upstream = gitutils.Commit.fromId(db, repository, new_upstream_id) else: if len(new_head.parents) != 1: raise IndexException, "Invalid rebase: New head can't be a merge commit." new_upstream = gitutils.Commit.fromSHA1(db, repository, new_head.parents[0]) if new_upstream in old_commitset.getTails(): old_upstream = new_upstream = None else: old_upstream = None if old_upstream: if old_upstream.sha1 != repository.mergebase([old_upstream.sha1, new_upstream.sha1]): raise IndexException, """\ Invalid rebase: The new upstream commit is not a descendant of the old upstream commit. You may want to cancel the rebase via the review front-page, and prepare another one specifying the correct new upstream commit; or rebase the branch onto the new upstream specified and then push that instead.""" if new_upstream.sha1 != repository.mergebase([new_upstream.sha1, new]): raise IndexException, """\ Invalid rebase: The new upstream commit you specified when the rebase was prepared is not an ancestor of the commit now pushed. You may want to cancel the rebase via the review front-page, and prepare another one specifying the correct new upstream commit; or rebase the branch onto the new upstream specified and then push that instead.""" old_upstream_name = repository.findInterestingTag(db, old_upstream.sha1) or old_upstream.sha1 new_upstream_name = repository.findInterestingTag(db, new_upstream.sha1) or new_upstream.sha1 if onto_branch: merged_thing = "branch '%s'" % onto_branch else: merged_thing = "commit '%s'" % new_upstream_name merge_sha1 = repository.run('commit-tree', new_head.tree, '-p', old_head.sha1, '-p', new_upstream.sha1, input="""\ Merge %s into %s This commit was generated automatically by Critic as an equivalent merge to the rebase of the commits %s..%s onto the %s.""" % (merged_thing, review.branch.name, old_upstream_name, old_head.sha1, merged_thing), env={ 'GIT_AUTHOR_NAME': user.fullname, 'GIT_AUTHOR_EMAIL': user.email, 'GIT_COMMITTER_NAME': user.fullname, 'GIT_COMMITTER_EMAIL': user.email }).strip() merge = gitutils.Commit.fromSHA1(db, repository, merge_sha1) gituser_id = merge.author.getGitUserId(db) cursor.execute("""INSERT INTO commits (sha1, author_gituser, commit_gituser, author_time, commit_time) VALUES (%s, %s, %s, %s, %s) RETURNING id""", (merge_sha1, gituser_id, gituser_id, timestamp(merge.author.time), timestamp(merge.committer.time))) merge.id = cursor.fetchone()[0] cursor.executemany("INSERT INTO edges (parent, child) VALUES (%s, %s)", [(old_head.getId(db), merge.id), (new_upstream.getId(db), merge.id)]) # Have to commit to make the new commit available to other DB # sessions right away, specifically so that the changeset # creation server can see it. db.commit() cursor.execute("""UPDATE reviewrebases SET old_head=%s, new_head=%s, new_upstream=%s WHERE review=%s AND new_head IS NULL""", (merge.id, new_head.getId(db), new_upstream.getId(db), review.id)) new_sha1s = repository.revlist([new], [new_upstream.sha1], '--topo-order') rebased_commits = [gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in new_sha1s] reachable_values = [(review.branch.id, sha1) for sha1 in new_sha1s] cursor.execute("INSERT INTO previousreachable (rebase, commit) SELECT %s, commit FROM reachable WHERE branch=%s", (rebase_id, review.branch.id)) cursor.execute("DELETE FROM reachable WHERE branch=%s", (review.branch.id,)) cursor.executemany("INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE commits.sha1=%s", reachable_values) cursor.execute("UPDATE branches SET head=%s WHERE id=%s", (gitutils.Commit.fromSHA1(db, repository, new).getId(db), review.branch.id)) pending_mails = [] cursor.execute("SELECT uid FROM reviewusers WHERE review=%s", (review.id,)) recipients = [] for (user_id,) in cursor.fetchall(): recipients.append(dbutils.User.fromId(db, user_id)) for to_user in recipients: pending_mails.extend(review_mail.sendReviewRebased(db, user, to_user, recipients, review, new_upstream_name, rebased_commits, onto_branch)) print "Rebase performed." review_utils.addCommitsToReview(db, user, review, [merge], pending_mails=pending_mails, silent_if_empty=set([merge]), full_merges=set([merge])) repository.keepalive(merge) else: old_commitset = log.commitset.CommitSet(review.branch.commits) new_sha1s = repository.revlist([new], old_commitset.getTails(), '--topo-order') if old_head.sha1 in new_sha1s: raise IndexException, """\ Invalid history rewrite: Old head of the branch reachable from the pushed ref; no history rewrite performed. (Cancel the rebase via the review front-page if you've changed your mind.)""" for new_sha1 in new_sha1s: new_head = gitutils.Commit.fromSHA1(db, repository, new_sha1) if new_head.tree == old_head.tree: break else: raise IndexException, """\ Invalid history rewrite: No commit on the rebased branch references the same tree as the old head of the branch.""" cursor.execute("""UPDATE reviewrebases SET new_head=%s WHERE review=%s AND new_head IS NULL""", (new_head.getId(db), review.id)) rebased_commits = [gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in repository.revlist([new_head], old_commitset.getTails(), '--topo-order')] new_commits = [gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in repository.revlist([new], [new_head], '--topo-order')] reachable_values = [(review.branch.id, sha1) for sha1 in new_sha1s] cursor.execute("INSERT INTO previousreachable (rebase, commit) SELECT %s, commit FROM reachable WHERE branch=%s", (rebase_id, review.branch.id)) cursor.execute("DELETE FROM reachable WHERE branch=%s", (review.branch.id,)) cursor.executemany("INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE commits.sha1=%s", reachable_values) cursor.execute("UPDATE branches SET head=%s WHERE id=%s", (gitutils.Commit.fromSHA1(db, repository, new).getId(db), review.branch.id)) pending_mails = [] cursor.execute("SELECT uid FROM reviewusers WHERE review=%s", (review.id,)) recipients = [] for (user_id,) in cursor.fetchall(): recipients.append(dbutils.User.fromId(db, user_id)) for to_user in recipients: pending_mails.extend(review_mail.sendReviewRebased(db, user, to_user, recipients, review, None, rebased_commits)) print "History rewrite performed." if new_commits: review_utils.addCommitsToReview(db, user, review, new_commits, pending_mails=pending_mails) else: review_mail.sendPendingMails(pending_mails) repository.run('update-ref', 'refs/keepalive/%s' % old, old) return True elif old != repository.mergebase([old, new]): raise IndexException, "Rejecting non-fast-forward update of review branch." elif old != repository.mergebase([old, new]): raise IndexException, """\ Rejecting non-fast-forward update of branch. To perform the update, you can delete the branch using git push critic :%s first, and then repeat this push.""" % name cursor.execute("SELECT id FROM branches WHERE repository=%s AND base IS NULL ORDER BY id ASC LIMIT 1", (repository.id,)) root_branch_id = cursor.fetchone()[0] def isreachable(sha1): #if rescan: return False #if is_review: cursor.execute("SELECT 1 FROM commits, reachable, branches WHERE commits.sha1=%s AND commits.id=reachable.commit AND reachable.branch=branches.id AND branches.repository=%s", [sha1, repository.id]) if is_review and sha1 == branch.tail: return True if base_branch_id: cursor.execute("SELECT 1 FROM commits, reachable WHERE commits.sha1=%s AND commits.id=reachable.commit AND reachable.branch IN (%s, %s, %s)", [sha1, branch.id, base_branch_id, root_branch_id]) else: cursor.execute("SELECT 1 FROM commits, reachable WHERE commits.sha1=%s AND commits.id=reachable.commit AND reachable.branch IN (%s, %s)", [sha1, branch.id, root_branch_id]) return cursor.fetchone() is not None stack = [new] commits = set() commit_list = [] processed = set() count = 0 while stack: sha1 = stack.pop() count += 1 if (count % 1000) == 0: stdout.write(".") if (count % 10000) == 0: stdout.write("\n") stdout.flush() if sha1 not in commits and not isreachable(sha1): commits.add(sha1) commit_list.append(sha1) #if is_review: # stack.append(gitutils.Commit.fromSHA1(repository, sha1).parents[0]) #else: stack.extend([parent_sha1 for parent_sha1 in gitutils.Commit.fromSHA1(db, repository, sha1).parents if parent_sha1 not in processed]) processed.add(sha1) branch = dbutils.Branch.fromName(db, repository, name) review = dbutils.Review.fromBranch(db, branch) if review: if review.state != "open": raise IndexException, """\ The review is closed and can't be extended. You need to reopen it at %s before you can add commits to it.""" % review.getURL(db, user, 2) all_commits = [gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in reversed(commit_list)] tails = CommitSet(all_commits).getTails() if old not in tails: raise IndexException, """\ Push rejected; would break the review. It looks like some of the pushed commits are reachable from the repository's main branch, and thus consequently the commits currently included in the review are too. Perhaps you should request a new review of the follow-up commits?""" review_utils.addCommitsToReview(db, user, review, all_commits, commitset=commits, tracked_branch=tracked_branch) reachable_values = [(branch.id, sha1) for sha1 in reversed(commit_list) if sha1 in commits] cursor.executemany("INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE commits.sha1=%s", reachable_values) cursor.execute("UPDATE branches SET head=%s WHERE id=%s", (gitutils.Commit.fromSHA1(db, repository, new).getId(db), branch.id)) db.commit() if configuration.extensions.ENABLED and review: extensions.executeProcessCommits(db, user, review, all_commits, gitutils.Commit.fromSHA1(db, repository, old), gitutils.Commit.fromSHA1(db, repository, new), stdout)
def process_request(environ, start_response): request_start = time.time() critic = api.critic.startSession() db = critic.database user = None try: try: req = request.Request(db, environ, start_response) req.setUser(db) if req.user is None: if configuration.base.AUTHENTICATION_MODE == "host": user = dbutils.User.makeAnonymous() elif configuration.base.SESSION_TYPE == "httpauth": req.requestHTTPAuthentication() return [] elif req.path.startswith("externalauth/"): provider_name = req.path[len("externalauth/"):] raise request.DoExternalAuthentication(provider_name) elif req.path.startswith("oauth/"): provider_name = req.path[len("oauth/"):] if provider_name in auth.PROVIDERS: provider = auth.PROVIDERS[provider_name] if isinstance(provider, auth.OAuthProvider): if finishOAuth(db, req, provider): return [] elif configuration.base.SESSION_TYPE == "cookie": if req.cookies.get("has_sid") == "1": req.ensureSecure() if configuration.base.ALLOW_ANONYMOUS_USER \ or req.path in request.INSECURE_PATHS \ or req.path.startswith("static-resource/"): user = dbutils.User.makeAnonymous() # Don't try to redirect POST requests to the login page. elif req.method == "GET": if configuration.base.AUTHENTICATION_MODE == "critic": raise request.NeedLogin(req) else: raise request.DoExternalAuthentication( configuration.base.AUTHENTICATION_MODE, req.getTargetURL()) if not user: req.setStatus(403) req.start() return [] else: try: user = dbutils.User.fromName(db, req.user) except dbutils.NoSuchUser: if configuration.base.AUTHENTICATION_MODE == "host": email = getUserEmailAddress(req.user) user = dbutils.User.create(db, req.user, req.user, email, email_verified=None) db.commit() else: # This can't really happen. raise if not user.isAnonymous(): critic.setActualUser(api.user.fetch(critic, user_id=user.id)) user.loadPreferences(db) if user.status == 'retired': cursor = db.cursor() cursor.execute("UPDATE users SET status='current' WHERE id=%s", (user.id, )) user = dbutils.User.fromId(db, user.id) db.commit() if not user.getPreference(db, "debug.profiling.databaseQueries"): db.disableProfiling() if not req.path: if user.isAnonymous(): location = "tutorial" else: location = user.getPreference(db, "defaultPage") if req.query: location += "?" + req.query req.setStatus(307) req.addResponseHeader("Location", location) req.start() return [] if req.path == "redirect": target = req.getParameter("target", "/") if req.method == "POST": # Don't use HTTP redirect for POST requests. req.setContentType("text/html") req.start() return [ "<meta http-equiv='refresh' content='0; %s'>" % htmlify(target) ] else: raise request.MovedTemporarily(target) # Require a .git suffix on HTTP(S) repository URLs unless the user- # agent starts with "git/" (as Git's normally does.) # # Our objectives are: # # 1) Not to require Git's user-agent to be its default value, since # the user might have to override it to get through firewalls. # 2) Never to send regular user requests to 'git http-backend' by # mistake. # # This is a compromise. if req.getRequestHeader("User-Agent", "").startswith("git/"): suffix = None else: suffix = ".git" if handleRepositoryPath(db, req, user, suffix): db = None return [] if req.path.startswith("!/"): req.path = req.path[2:] elif configuration.extensions.ENABLED: handled = extensions.role.page.execute(db, req, user) if isinstance(handled, basestring): req.start() return [handled] if req.path.startswith("static-resource/"): return handleStaticResource(req) if req.path.startswith("r/"): req.updateQuery({"id": [req.path[2:]]}) req.path = "showreview" if configuration.extensions.ENABLED: match = RE_EXTENSION_RESOURCE.match(req.path) if match: content_type, resource = extensions.resource.get( req, db, user, match.group(1)) if resource: req.setContentType(content_type) if content_type.startswith("image/"): req.addResponseHeader("Cache-Control", "max-age=3600") req.start() return [resource] else: req.setStatus(404) req.start() return [] if req.path.startswith("download/"): operationfn = download elif req.path == "api" or req.path.startswith("api/"): try: result = jsonapi.handle(critic, req) except jsonapi.Error as error: req.setStatus(error.http_status) result = { "error": { "title": error.title, "message": error.message } } else: req.setStatus(200) accept_header = req.getRequestHeader("Accept") if accept_header == "application/vnd.api+json": default_indent = None else: default_indent = 2 indent = req.getParameter("indent", default_indent, filter=int) if indent == 0: # json.encode(..., indent=0) still gives line-breaks, just # no indentation. This is not so useful, so set indent to # None instead, which disables formatting entirely. indent = None req.setContentType("application/vnd.api+json") req.start() return [json_encode(result, indent=indent)] else: operationfn = OPERATIONS.get(req.path) if operationfn: result = operationfn(req, db, user) if isinstance(result, (OperationResult, OperationError)): req.setContentType("text/json") if isinstance(result, OperationResult): if db.profiling: result.set("__profiling__", formatDBProfiling(db)) result.set("__time__", time.time() - request_start) elif not req.hasContentType(): req.setContentType("text/plain") req.start() if isinstance(result, unicode): return [result.encode("utf8")] else: return [str(result)] override_user = req.getParameter("user", None) while True: pagefn = PAGES.get(req.path) if pagefn: try: if not user.isAnonymous() and override_user: user = dbutils.User.fromName(db, override_user) req.setContentType("text/html") result = pagefn(req, db, user) if db.profiling and not (isinstance(result, str) or isinstance(result, Document)): source = "" for fragment in result: source += fragment result = source if isinstance(result, str) or isinstance( result, Document): req.start() result = str(result) result += ("<!-- total request time: %.2f ms -->" % ((time.time() - request_start) * 1000)) if db.profiling: result += ("<!--\n\n%s\n\n -->" % formatDBProfiling(db)) return [result] else: result = WrappedResult(db, req, user, result) req.start() # Prevent the finally clause below from closing the # connection. WrappedResult does it instead. db = None return result except gitutils.NoSuchRepository as error: raise page.utils.DisplayMessage( title="Invalid URI Parameter!", body=error.message) except gitutils.GitReferenceError as error: if error.ref: raise page.utils.DisplayMessage( title="Specified ref not found", body=("There is no ref named \"%s\" in %s." % (error.ref, error.repository))) elif error.sha1: raise page.utils.DisplayMessage( title="SHA-1 not found", body=error.message) else: raise except dbutils.NoSuchUser as error: raise page.utils.DisplayMessage( title="Invalid URI Parameter!", body=error.message) except dbutils.NoSuchReview as error: raise page.utils.DisplayMessage( title="Invalid URI Parameter!", body=error.message) path = req.path if "/" in path: repository = gitutils.Repository.fromName( db, path.split("/", 1)[0]) if repository: path = path.split("/", 1)[1] else: repository = None def revparsePlain(item): try: return gitutils.getTaggedCommit( repository, repository.revparse(item)) except: raise revparse = revparsePlain if repository is None: review_id = req.getParameter("review", None, filter=int) if review_id: cursor = db.cursor() cursor.execute( """SELECT repository FROM branches JOIN reviews ON (reviews.branch=branches.id) WHERE reviews.id=%s""", (review_id, )) row = cursor.fetchone() if row: repository = gitutils.Repository.fromId(db, row[0]) def revparseWithReview(item): if re.match("^[0-9a-f]+$", item): cursor.execute( """SELECT sha1 FROM commits JOIN changesets ON (changesets.child=commits.id) JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id) WHERE reviewchangesets.review=%s AND commits.sha1 LIKE %s""", (review_id, item + "%")) row = cursor.fetchone() if row: return row[0] else: return revparsePlain(item) revparse = revparseWithReview if repository is None: repository = gitutils.Repository.fromName( db, user.getPreference(db, "defaultRepository")) if gitutils.re_sha1.match(path): if repository and not repository.iscommit(path): repository = None if not repository: try: repository = gitutils.Repository.fromSHA1( db, path) except gitutils.GitReferenceError: repository = None if repository: try: items = filter(None, map(revparse, path.split(".."))) updated_query = {} if len(items) == 1: updated_query["repository"] = [repository.name] updated_query["sha1"] = [items[0]] elif len(items) == 2: updated_query["repository"] = [repository.name] updated_query["from"] = [items[0]] updated_query["to"] = [items[1]] if updated_query: req.updateQuery(updated_query) req.path = "showcommit" continue except gitutils.GitReferenceError: pass break raise page.utils.DisplayMessage(title="Not found!", body="Page not handled: /%s" % path, status=404) except GeneratorExit: raise except page.utils.NotModified: req.setStatus(304) req.start() return [] except request.MovedTemporarily as redirect: req.setStatus(307) req.addResponseHeader("Location", redirect.location) if redirect.no_cache: req.addResponseHeader("Cache-Control", "no-cache") req.start() return [] except request.DoExternalAuthentication as command: command.execute(db, req) return [] except request.MissingWSGIRemoteUser as error: # req object is not initialized yet. start_response("200 OK", [("Content-Type", "text/html")]) return [ """\ <pre>error: Critic was configured with '--auth-mode host' but there was no REMOTE_USER variable in the WSGI environ dict provided by the web server. To fix this you can either reinstall Critic using '--auth-mode critic' (to let Critic handle user authentication automatically), or you can configure user authentication properly in the web server. For apache2, the latter can be done by adding the something like the following to the apache site configuration for Critic: <Location /> AuthType Basic AuthName "Authentication Required" AuthUserFile "/path/to/critic-main.htpasswd.users" Require valid-user </Location> If you need more dynamic http authentication you can instead setup mod_wsgi with a custom WSGIAuthUserScript directive. This will cause the provided credentials to be passed to a Python function called check_password() that you can implement yourself. This way you can validate the user/pass via any existing database or for example an LDAP server. For more information on setting up such authentication in apache2, see: <a href="%(url)s">%(url)s</a></pre>""" % { "url": "http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider" } ] except page.utils.DisplayMessage as message: if user is None: user = dbutils.User.makeAnonymous() document = page.utils.displayMessage(db, req, user, title=message.title, message=message.body, review=message.review, is_html=message.html) req.setContentType("text/html") req.setStatus(message.status) req.start() return [str(document)] except page.utils.DisplayFormattedText as formatted_text: if user is None: user = dbutils.User.makeAnonymous() document = page.utils.displayFormattedText(db, req, user, formatted_text.source) req.setContentType("text/html") req.start() return [str(document)] except Exception: # crash might be psycopg2.ProgrammingError so rollback to avoid # "InternalError: current transaction is aborted" inside handleException() if db and db.closed(): db = None elif db: db.rollback() error_title, error_body = handleException(db, req, user) error_body = reflow("\n\n".join(error_body)) error_message = "\n".join( [error_title, "=" * len(error_title), "", error_body]) assert not req.isStarted() req.setStatus(500) req.setContentType("text/plain") req.start() return [error_message] finally: if db: db.close()
def createBranch(user, repository, name, head, flags): processCommits(repository.name, head) try: update(repository.path, "refs/heads/" + name, None, head) except Reject as rejected: raise IndexException(str(rejected)) except Exception: pass cursor = db.cursor() def commit_id(sha1): cursor.execute("SELECT id FROM commits WHERE sha1=%s", [sha1]) return cursor.fetchone()[0] components = name.split("/") for index in range(1, len(components)): try: repository.revparse("refs/heads/%s" % "/".join(components[:index])) except: continue message = ("Cannot create branch with name '%s' since there is already a branch named '%s' in the repository." % (name, "/".join(components[:index]))) raise IndexException(textutils.reflow(message, line_length=80 - len("remote: "))) if name.startswith("r/"): try: review_id = int(name[2:]) cursor.execute("SELECT branches.name FROM reviews JOIN branches ON (branches.id=reviews.branch) WHERE reviews.id=%s", (review_id,)) row = cursor.fetchone() message = "Refusing to create review named as a number." if row: message += "\nDid you mean to push to the branch '%s', perhaps?" % row[0] raise IndexException(message) except ValueError: pass if user.getPreference(db, "review.createViaPush"): the_commit = gitutils.Commit.fromSHA1(db, repository, head, commit_id(head)) all_commits = [the_commit] review = reviewing.utils.createReview( db, user, repository, all_commits, name, the_commit.niceSummary(include_tag=False), None, via_push=True) print "Submitted review:" print review.getURL(db, user, indent=2) if review.reviewers: print " Reviewers:" for reviewer in review.reviewers: print " %s <%s>" % (reviewer.fullname, reviewer.email) if review.watchers: print " Watchers:" for watcher in review.watchers: print " %s <%s>" % (watcher.fullname, watcher.email) if configuration.extensions.ENABLED: if extensions.role.processcommits.execute(db, user, review, all_commits, None, the_commit, sys.stdout): print print "Thank you!" return True else: raise IndexException("Refusing to create review; user preference 'review.createViaPush' is not enabled.") sha1 = head base = None tail = None cursor.execute("""SELECT 1 FROM reachable JOIN branches ON (branches.id=reachable.branch) JOIN repositories ON (repositories.id=branches.repository) WHERE repositories.id=%s LIMIT 1""", (repository.id,)) if cursor.fetchone(): def reachable(sha1): cursor.execute("""SELECT branches.id FROM branches JOIN reachable ON (reachable.branch=branches.id) JOIN commits ON (commits.id=reachable.commit) WHERE branches.repository=%s AND branches.type='normal' AND commits.sha1=%s ORDER BY reachable.branch ASC LIMIT 1""", (repository.id, sha1)) return cursor.fetchone() else: def reachable(sha1): return None commit_map = {} commit_list = [] row = reachable(sha1) if row: # Head of branch is reachable from an existing branch. Could be because # this branch is actually empty (just created with no "own" commits) or # it could have been merged into some other already existing branch. We # can't tell, so we just record it as empty. base = row[0] tail = sha1 else: stack = [] while True: if sha1 not in commit_map: commit = gitutils.Commit.fromSHA1(db, repository, sha1) commit_map[sha1] = commit commit_list.append(commit) for sha1 in commit.parents: if sha1 not in commit_map: row = reachable(sha1) if not row: stack.append(sha1) elif base is None: base = row[0] tail = sha1 base_chain = [base] while True: cursor.execute("SELECT base FROM branches WHERE id=%s", (base_chain[-1],)) next = cursor.fetchone()[0] if next is None: break else: base_chain.append(next) def reachable(sha1): cursor.execute("""SELECT 1 FROM reachable JOIN commits ON (commits.id=reachable.commit) WHERE reachable.branch=ANY (%s) AND commits.sha1=%s""", (base_chain, sha1)) return cursor.fetchone() if stack: sha1 = stack.pop(0) else: break if isinstance(user, dbutils.User): # Push by regular user. user_name = user.name else: # Push by the Critic system user, i.e. by the branch tracker service or # other internal mechanism. user_name = user if not base: cursor.execute("INSERT INTO branches (repository, name, head) VALUES (%s, %s, %s) RETURNING id", (repository.id, name, commit_id(head))) branch_id = cursor.fetchone()[0] else: cursor.execute("INSERT INTO branches (repository, name, head, base, tail) VALUES (%s, %s, %s, %s, %s) RETURNING id", (repository.id, name, commit_id(head), base, commit_id(tail))) branch_id = cursor.fetchone()[0] # Suppress the "user friendly" feedback if the push is performed by the # Critic system user, since there wouldn't be a human being reading it. # # Also, the calls to user.getCriticURLs() obvious don't work if 'user' # isn't a dbutils.User object, which it isn't in that case. if user_name != configuration.base.SYSTEM_USER_NAME: cursor.execute("SELECT name FROM branches WHERE id=%s", [base]) print "Added branch based on %s containing %d commit%s:" % (cursor.fetchone()[0], len(commit_list), "s" if len(commit_list) > 1 else "") for url_prefix in user.getCriticURLs(db): print " %s/log?repository=%d&branch=%s" % (url_prefix, repository.id, name) if len(commit_list) > 1: print "To create a review of all %d commits:" % len(commit_list) else: print "To create a review of the commit:" for url_prefix in user.getCriticURLs(db): print " %s/createreview?repository=%d&branch=%s" % (url_prefix, repository.id, name) reachable_values = [(branch_id, commit.sha1) for commit in commit_list] cursor.executemany("INSERT INTO reachable (branch, commit) SELECT %s, id FROM commits WHERE sha1=%s", reachable_values) if not repository.hasMainBranch() and user_name == configuration.base.SYSTEM_USER_NAME: cursor.execute("UPDATE repositories SET branch=%s WHERE id=%s", (branch_id, repository.id))
def process_request(environ, start_response): request_start = time.time() db = dbutils.Database() user = None try: try: req = request.Request(db, environ, start_response) req.setUser(db) if req.user is None: if configuration.base.AUTHENTICATION_MODE == "host": user = dbutils.User.makeAnonymous() elif configuration.base.SESSION_TYPE == "httpauth": req.requestHTTPAuthentication() return [] elif req.path.startswith("externalauth/"): provider_name = req.path[len("externalauth/"):] raise request.DoExternalAuthentication(provider_name) elif req.path.startswith("oauth/"): provider_name = req.path[len("oauth/"):] if provider_name in auth.PROVIDERS: provider = auth.PROVIDERS[provider_name] if isinstance(provider, auth.OAuthProvider): if finishOAuth(db, req, provider): return [] elif configuration.base.SESSION_TYPE == "cookie": if req.cookies.get("has_sid") == "1": req.ensureSecure() if configuration.base.ALLOW_ANONYMOUS_USER \ or req.path in request.INSECURE_PATHS \ or req.path.startswith("static-resource/"): user = dbutils.User.makeAnonymous() # Don't try to redirect POST requests to the login page. elif req.method == "GET": if configuration.base.AUTHENTICATION_MODE == "critic": raise request.NeedLogin(req) else: raise request.DoExternalAuthentication( configuration.base.AUTHENTICATION_MODE, req.getTargetURL()) if not user: req.setStatus(403) req.start() return [] else: try: user = dbutils.User.fromName(db, req.user) except dbutils.NoSuchUser: if configuration.base.AUTHENTICATION_MODE == "host": email = getUserEmailAddress(req.user) user = dbutils.User.create( db, req.user, req.user, email, email_verified=None) db.commit() else: # This can't really happen. raise user.loadPreferences(db) if user.status == 'retired': cursor = db.cursor() cursor.execute("UPDATE users SET status='current' WHERE id=%s", (user.id,)) user = dbutils.User.fromId(db, user.id) db.commit() if not user.getPreference(db, "debug.profiling.databaseQueries"): db.disableProfiling() if not req.path: if user.isAnonymous(): location = "tutorial" else: location = user.getPreference(db, "defaultPage") if req.query: location += "?" + req.query req.setStatus(307) req.addResponseHeader("Location", location) req.start() return [] if req.path == "redirect": target = req.getParameter("target", "/") if req.method == "POST": # Don't use HTTP redirect for POST requests. req.setContentType("text/html") req.start() return ["<meta http-equiv='refresh' content='0; %s'>" % htmlify(target)] else: raise request.MovedTemporarily(target) # Require a .git suffix on HTTP(S) repository URLs unless the user- # agent starts with "git/" (as Git's normally does.) # # Our objectives are: # # 1) Not to require Git's user-agent to be its default value, since # the user might have to override it to get through firewalls. # 2) Never to send regular user requests to 'git http-backend' by # mistake. # # This is a compromise. if req.getRequestHeader("User-Agent", "").startswith("git/"): suffix = None else: suffix = ".git" if handleRepositoryPath(db, req, user, suffix): db = None return [] if req.path.startswith("!/"): req.path = req.path[2:] elif configuration.extensions.ENABLED: handled = extensions.role.page.execute(db, req, user) if isinstance(handled, basestring): req.start() return [handled] if req.path.startswith("static-resource/"): return handleStaticResource(req) if req.path.startswith("r/"): req.query = "id=" + req.path[2:] + ("&" + req.query if req.query else "") req.path = "showreview" if configuration.extensions.ENABLED: match = RE_EXTENSION_RESOURCE.match(req.path) if match: content_type, resource = extensions.resource.get(req, db, user, match.group(1)) if resource: req.setContentType(content_type) if content_type.startswith("image/"): req.addResponseHeader("Cache-Control", "max-age=3600") req.start() return [resource] else: req.setStatus(404) req.start() return [] if req.path.startswith("download/"): operationfn = download else: operationfn = OPERATIONS.get(req.path) if operationfn: result = operationfn(req, db, user) if isinstance(result, (OperationResult, OperationError)): req.setContentType("text/json") if isinstance(result, OperationResult): if db.profiling: result.set("__profiling__", formatDBProfiling(db)) result.set("__time__", time.time() - request_start) elif not req.hasContentType(): req.setContentType("text/plain") req.start() if isinstance(result, unicode): return [result.encode("utf8")] else: return [str(result)] override_user = req.getParameter("user", None) while True: pagefn = PAGES.get(req.path) if pagefn: try: if not user.isAnonymous() and override_user: user = dbutils.User.fromName(db, override_user) req.setContentType("text/html") result = pagefn(req, db, user) if db.profiling and not (isinstance(result, str) or isinstance(result, Document)): source = "" for fragment in result: source += fragment result = source if isinstance(result, str) or isinstance(result, Document): req.start() result = str(result) result += ("<!-- total request time: %.2f ms -->" % ((time.time() - request_start) * 1000)) if db.profiling: result += ("<!--\n\n%s\n\n -->" % formatDBProfiling(db)) return [result] else: result = WrappedResult(db, req, user, result) req.start() # Prevent the finally clause below from closing the # connection. WrappedResult does it instead. db = None return result except gitutils.NoSuchRepository as error: raise page.utils.DisplayMessage( title="Invalid URI Parameter!", body=error.message) except gitutils.GitReferenceError as error: if error.ref: raise page.utils.DisplayMessage( title="Specified ref not found", body=("There is no ref named \"%s\" in %s." % (error.ref, error.repository))) elif error.sha1: raise page.utils.DisplayMessage( title="SHA-1 not found", body=error.message) else: raise except dbutils.NoSuchUser as error: raise page.utils.DisplayMessage( title="Invalid URI Parameter!", body=error.message) except dbutils.NoSuchReview as error: raise page.utils.DisplayMessage( title="Invalid URI Parameter!", body=error.message) path = req.path if "/" in path: repository = gitutils.Repository.fromName(db, path.split("/", 1)[0]) if repository: path = path.split("/", 1)[1] else: repository = None def revparsePlain(item): try: return gitutils.getTaggedCommit(repository, repository.revparse(item)) except: raise revparse = revparsePlain if repository is None: review_id = req.getParameter("review", None, filter=int) if review_id: cursor = db.cursor() cursor.execute("""SELECT repository FROM branches JOIN reviews ON (reviews.branch=branches.id) WHERE reviews.id=%s""", (review_id,)) row = cursor.fetchone() if row: repository = gitutils.Repository.fromId(db, row[0]) def revparseWithReview(item): if re.match("^[0-9a-f]+$", item): cursor.execute("""SELECT sha1 FROM commits JOIN changesets ON (changesets.child=commits.id) JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id) WHERE reviewchangesets.review=%s AND commits.sha1 LIKE %s""", (review_id, item + "%")) row = cursor.fetchone() if row: return row[0] else: return revparsePlain(item) revparse = revparseWithReview if repository is None: repository = gitutils.Repository.fromName( db, user.getPreference(db, "defaultRepository")) if gitutils.re_sha1.match(path): if repository and not repository.iscommit(path): repository = None if not repository: try: repository = gitutils.Repository.fromSHA1(db, path) except gitutils.GitReferenceError: repository = None if repository: try: items = filter(None, map(revparse, path.split(".."))) query = None if len(items) == 1: query = ("repository=%d&sha1=%s" % (repository.id, items[0])) elif len(items) == 2: query = ("repository=%d&from=%s&to=%s" % (repository.id, items[0], items[1])) if query: if req.query: query += "&" + req.query req.query = query req.path = "showcommit" continue except gitutils.GitReferenceError: pass break req.setStatus(404) raise page.utils.DisplayMessage( title="Not found!", body="Page not handled: /%s" % path) except GeneratorExit: raise except page.utils.NotModified: req.setStatus(304) req.start() return [] except request.MovedTemporarily as redirect: req.setStatus(307) req.addResponseHeader("Location", redirect.location) if redirect.no_cache: req.addResponseHeader("Cache-Control", "no-cache") req.start() return [] except request.DoExternalAuthentication as command: command.execute(db, req) return [] except request.MissingWSGIRemoteUser as error: # req object is not initialized yet. start_response("200 OK", [("Content-Type", "text/html")]) return ["""\ <pre>error: Critic was configured with '--auth-mode host' but there was no REMOTE_USER variable in the WSGI environ dict provided by the web server. To fix this you can either reinstall Critic using '--auth-mode critic' (to let Critic handle user authentication automatically), or you can configure user authentication properly in the web server. For apache2, the latter can be done by adding the something like the following to the apache site configuration for Critic: <Location /> AuthType Basic AuthName "Authentication Required" AuthUserFile "/path/to/critic-main.htpasswd.users" Require valid-user </Location> If you need more dynamic http authentication you can instead setup mod_wsgi with a custom WSGIAuthUserScript directive. This will cause the provided credentials to be passed to a Python function called check_password() that you can implement yourself. This way you can validate the user/pass via any existing database or for example an LDAP server. For more information on setting up such authentication in apache2, see: <a href="%(url)s">%(url)s</a></pre>""" % { "url": "http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider" }] except page.utils.DisplayMessage as message: if user is None: user = dbutils.User.makeAnonymous() document = page.utils.displayMessage( db, req, user, title=message.title, message=message.body, review=message.review, is_html=message.html) req.setContentType("text/html") req.start() return [str(document)] except Exception: # crash might be psycopg2.ProgrammingError so rollback to avoid # "InternalError: current transaction is aborted" inside handleException() if db and db.closed(): db = None elif db: db.rollback() error_title, error_body = handleException(db, req, user) error_body = reflow("\n\n".join(error_body)) error_message = "\n".join([error_title, "=" * len(error_title), "", error_body]) assert not req.isStarted() req.setStatus(500) req.setContentType("text/plain") req.start() return [error_message] finally: if db: db.close()
def sendReviewAddedCommits(db, from_user, to_user, recipients, review, commits, changesets, tracked_branch=False): # First check if we can send emails to the user at all. if not checkEmailEnabled(db, to_user): return [] if from_user == to_user and to_user.getPreference(db, "email.ignoreOwnChanges"): return [] line_length = to_user.getPreference(db, "email.lineLength") hr = "-" * line_length relevant_only = to_user not in review.owners and to_user != from_user and to_user.getPreference(db, "email.updatedReview.relevantChangesOnly") cursor = db.cursor() if relevant_only: cursor.execute("SELECT type FROM reviewusers WHERE review=%s AND uid=%s", (review.id, to_user.id)) if cursor.fetchone()[0] == 'manual': relevant_only = False all_commits = dict((commit.sha1, commit) for commit in commits) changeset_for_commit = {} for changeset in changesets: # We don't include diffs for merge commits in mails. if len(changeset.child.parents) == 1: if changeset.child in all_commits: changeset_for_commit[changeset.child] = changeset else: # An added changeset where the child isn't part of the added # commits will be a changeset between a "replayed rebase" commit # and the new head commit, generated when doing a non-fast- # forward rebase. The relevant commit from such a changeset is # the first (and only) parent. changeset_for_commit[changeset.parent] = changeset if relevant_only: relevant_files = review.getRelevantFiles(db, to_user) relevant_commits = set() for changeset in changesets: for file in changeset.files: if file.id in relevant_files: if changeset.child in all_commits: relevant_commits.add(changeset.child) else: # "Replayed rebase" commit; see comment above. relevant_commits.add(all_commits[changeset.parent]) break else: cursor.execute("SELECT id FROM commentchains WHERE review=%s AND state='addressed' AND addressed_by=%s", (review.id, changeset.child.getId(db))) for chain_id in cursor.fetchall(): cursor.execute("SELECT 1 FROM commentchainusers WHERE chain=%s AND uid=%s", (chain_id, to_user.id)) if cursor.fetchone(): relevant_commits.add(changeset.child) break if not relevant_commits: return [] else: relevant_commits = None data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'review.branch.name': review.branch.name, 'review.branch.repository': review.repository.getURL(db, to_user), 'hr': hr } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data commitset = log_commitset.CommitSet(commits) if tracked_branch: body += "The automatic tracking of\n %s\n" % tracked_branch body += textutils.reflow("has updated the review by pushing %sadditional commit%s to the branch" % ("an " if len(commits) == 1 else "", "s" if len(commits) > 1 else ""), line_length) else: body += textutils.reflow("%s has updated the review by pushing %sadditional commit%s to the branch" % (from_user.fullname, "an " if len(commits) == 1 else "", "s" if len(commits) > 1 else ""), line_length) body += "\n %s\n" % review.branch.name body += textutils.reflow("in the repository", line_length) body += "\n %s\n\n\n" % review.repository.getURL(db, to_user) cursor.execute("""SELECT file, SUM(deleted), SUM(inserted) FROM fullreviewuserfiles WHERE review=%%s AND changeset IN (%s) AND state='pending' AND assignee=%%s GROUP BY file""" % ",".join(["%s"] * len(changesets)), [review.id] + [changeset.id for changeset in changesets] + [to_user.id]) pending_files_lines = cursor.fetchall() if pending_files_lines: heads = commitset.getHeads() tails = commitset.getFilteredTails(review.repository) if len(heads) == 1 and len(tails) == 1: showcommit_link = (tails.pop()[:8], heads.pop().sha1[:8]) else: showcommit_link = False body += renderFiles(db, to_user, review, "These changes were assigned to you:", pending_files_lines, showcommit_link=showcommit_link) all_commits = to_user.getPreference(db, "email.updatedReview.displayCommits") context_lines = to_user.getPreference(db, "email.comment.contextLines") if all_commits: body += "The additional commit%s requested to be reviewed are:\n\n" % ("s" if len(commits) > 1 else "") contextLines = to_user.getPreference(db, "email.updatedReview.diff.contextLines") diffMaxLines = to_user.getPreference(db, "email.updatedReview.diff.maxLines") displayStats = to_user.getPreference(db, "email.updatedReview.displayStats") statsMaxLines = to_user.getPreference(db, "email.updatedReview.stats.maxLines") if contextLines < 0: contextLines = 0 if diffMaxLines == 0: diffs = None else: diffs = {} lines = 0 for commit in commits: if commit in changeset_for_commit: diff = changeset_text.unified(db, changeset_for_commit[commit], contextLines) diffs[commit] = diff lines += diff.count("\n") if lines > diffMaxLines: diffs = None break if not displayStats or statsMaxLines == 0: stats = None else: stats = {} lines = 0 for commit in commits: commit_stats = review.repository.run("show", "--oneline", "--stat", commit.sha1).split('\n', 1)[1] stats[commit] = commit_stats lines += commit_stats.count('\n') if lines > statsMaxLines: stats = None break for index, commit in enumerate(commits): if index > 0: body += "\n\n\n" body += """Commit: %(sha1)s Author: %(author.fullname)s <%(author.email)s> at %(author.time)s %(message)s """ % { 'sha1': commit.sha1, 'author.fullname': commit.author.getFullname(db), 'author.email': commit.author.email, 'author.time': time.strftime("%Y-%m-%d %H:%M:%S", commit.author.time), 'message': textutils.reflow(commit.message.strip(), line_length, indent=2) } if stats and commit in stats: body += "---\n" + stats[commit] if diffs and commit in diffs: body += "\n" + diffs[commit] cursor.execute("SELECT id FROM commentchains WHERE review=%s AND state='addressed' AND addressed_by=%s", (review.id, commit.getId(db))) rows = cursor.fetchall() if rows: for (chain_id,) in rows: chain = review_comment.CommentChain.fromId(db, chain_id, to_user, review=review) chain.loadComments(db, to_user, include_draft_comments=False) body += "\n\n" + renderChainInMail(db, to_user, chain, None, "addressed", None, line_length, context_lines) files = [] parent_message_id = getReviewMessageId(db, to_user, review, files) message_id = generateMessageId(len(files) + 1) subject = generateSubjectLine(db, to_user, review, "updatedReview.commitsPushed") files.append(sendMail( db, review, message_id, from_user, to_user, recipients, subject, body, parent_message_id=parent_message_id)) return files
def createBranch(user, repository, name, head): processCommits(repository.name, head) cursor = db.cursor() def commit_id(sha1): cursor.execute("SELECT id FROM commits WHERE sha1=%s", [sha1]) return cursor.fetchone()[0] components = name.split("/") for index in range(1, len(components)): try: repository.revparse("refs/heads/%s" % "/".join(components[:index])) except: continue message = ("Cannot create branch with name '%s' since there is already a branch named '%s' in the repository." % (name, "/".join(components[:index]))) raise IndexException, textutils.reflow(message, line_length=80 - len("remote: ")) if name.startswith("r/"): try: review_id = int(name[2:]) cursor.execute("SELECT branches.name FROM reviews JOIN branches ON (branches.id=reviews.branch) WHERE reviews.id=%s", (review_id,)) row = cursor.fetchone() message = "Refusing to create review named as a number." if row: message += "\nDid you mean to push to the branch '%s', perhaps?" % row[0] raise IndexException, message except ValueError: pass if user.getPreference(db, "review.createViaPush"): the_commit = gitutils.Commit.fromSHA1(db, repository, head, commit_id(head)) all_commits = [the_commit] review = review_utils.createReview(db, user, repository, all_commits, name, the_commit.summary(), None, via_push=True) print "Submitted review: %s/r/%d" % (dbutils.getURLPrefix(db), review.id) if review.reviewers: print " Reviewers:" for reviewer in review.reviewers: print " %s <%s>" % (reviewer.fullname, reviewer.email) if review.watchers: print " Watchers:" for watcher in review.watchers: print " %s <%s>" % (watcher.fullname, watcher.email) if configuration.extensions.ENABLED: if extensions.executeProcessCommits(db, user, review, all_commits, None, the_commit, stdout): print print "Thank you!" return True else: raise IndexException, "Refusing to create review; user preference 'review.createViaPush' is not enabled." sha1 = head base = None tail = None cursor.execute("""SELECT 1 FROM reachable JOIN branches ON (branches.id=reachable.branch) JOIN repositories ON (repositories.id=branches.repository) WHERE repositories.id=%s LIMIT 1""", (repository.id,)) if cursor.fetchone(): def reachable(sha1): cursor.execute("""SELECT branches.id FROM branches JOIN reachable ON (reachable.branch=branches.id) JOIN commits ON (commits.id=reachable.commit) WHERE branches.repository=%s AND branches.type='normal' AND commits.sha1=%s ORDER BY reachable.branch ASC LIMIT 1""", (repository.id, sha1)) return cursor.fetchone() else: def reachable(sha1): return None commit_map = {} commit_list = [] row = reachable(sha1) if row: # Head of branch is reachable from an existing branch. Could be because # this branch is actually empty (just created with no "own" commits) or # it could have been merged into some other already existing branch. We # can't tell, so we just record it as empty. base = row[0] tail = sha1 else: stack = [] while True: if sha1 not in commit_map: commit = gitutils.Commit.fromSHA1(db, repository, sha1) commit_map[sha1] = commit commit_list.append(commit) for sha1 in commit.parents: if sha1 not in commit_map: row = reachable(sha1) if not row: stack.append(sha1) elif base is None: base = row[0] tail = sha1 base_chain = [base] while True: cursor.execute("SELECT base FROM branches WHERE id=%s", (base_chain[-1],)) next = cursor.fetchone()[0] if next is None: break else: base_chain.append(next) def reachable(sha1): cursor.execute("""SELECT 1 FROM reachable JOIN commits ON (commits.id=reachable.commit) WHERE reachable.branch=ANY (%s) AND commits.sha1=%s""", (base_chain, sha1)) return cursor.fetchone() if stack: sha1 = stack.pop(0) else: break if len(commit_list) % 10000 > 1000: stdout.write("\n") stdout.flush() if not base: cursor.execute("INSERT INTO branches (repository, name, head) VALUES (%s, %s, %s) RETURNING id", (repository.id, name, commit_id(head))) branch_id = cursor.fetchone()[0] else: cursor.execute("INSERT INTO branches (repository, name, head, base, tail) VALUES (%s, %s, %s, %s, %s) RETURNING id", (repository.id, name, commit_id(head), base, commit_id(tail))) branch_id = cursor.fetchone()[0] cursor.execute("SELECT name FROM branches WHERE id=%s", [base]) print "Added branch based on %s containing %d commit%s:" % (cursor.fetchone()[0], len(commit_list), "s" if len(commit_list) > 1 else "") print " %s/log?repository=%d&branch=%s" % (dbutils.getURLPrefix(db), repository.id, name) if len(commit_list) > 1: print "To create a review of all %d commits:" % len(commit_list) else: print "To create a review of the commit:" print " %s/createreview?repository=%d&branch=%s" % (dbutils.getURLPrefix(db), repository.id, name) reachable_values = [(branch_id, commit.sha1) for commit in commit_list] cursor.executemany("INSERT INTO reachable (branch, commit) SELECT %s, id FROM commits WHERE sha1=%s", reachable_values) if isinstance(user, str): user_name = user else: user_name = user.name if not repository.hasMainBranch() and user_name == configuration.base.SYSTEM_USER_NAME: cursor.execute("UPDATE repositories SET branch=%s WHERE id=%s", (branch_id, repository.id))
def formatComment(comment): return "%s at %s:\n%s\n" % (comment.user.fullname, comment.when(), textutils.reflow(comment.comment, line_length, indent=2))
def updateBranch(db, user, repository, name, old, new, multiple, flags): try: update(repository.path, "refs/heads/" + name, old, new) except Reject as rejected: raise IndexException(str(rejected)) except Exception: pass try: branch = dbutils.Branch.fromName(db, repository, name, for_update=dbutils.NOWAIT) except dbutils.FailedToLock: raise IndexException(reflow( "The branch '%s' is currently locked since it is being updated " "by another push. Please fetch and try again." % name)) else: if not branch: # FIXME: We should handle this better. Maybe just redirect to # createBranch()? raise IndexException("The branch '%s' is not in the database!" % name) base_branch_id = branch.base.id if branch.base else None if branch.head_sha1 != old: if new == branch.head_sha1: # This is what we think the ref ought to be already. Do nothing, # and let the repository "catch up." return else: data = { "name": name, "old": old[:8], "new": new[:8], "current": branch.head_sha1[:8] } message = """CONFUSED! Git thinks %(name)s points to %(old)s, but Critic thinks it points to %(current)s. Rejecting push since it would only makes matters worse. To resolve this problem, use git push -f critic %(current)s:%(name)s to resynchronize the Git repository with Critic's database. Note that 'critic' above must be replaced by the actual name of your Critic remote, if not 'critic'.""" % data raise IndexException(textutils.reflow(message, line_length=80 - len("remote: "))) cursor = db.cursor() cursor.execute("""SELECT id, remote, remote_name, forced, updating FROM trackedbranches WHERE repository=%s AND local_name=%s AND NOT disabled""", (repository.id, name)) row = cursor.fetchone() if row: trackedbranch_id, remote, remote_name, forced, updating = row tracked_branch = "%s in %s" % (remote_name, remote) assert not forced or not name.startswith("r/") if not user.isSystem() \ or flags.get("trackedbranch_id") != str(trackedbranch_id): raise IndexException("""\ The branch '%s' is set up to track '%s' in %s Please don't push it manually to this repository.""" % (name, remote_name, remote)) assert updating if not name.startswith("r/"): conflicting = repository.revlist([branch.head_sha1], [new]) added = repository.revlist([new], [branch.head_sha1]) if conflicting: if forced: if branch.base is None: cursor.executemany("""DELETE FROM reachable WHERE branch=%s AND commit IN (SELECT id FROM commits WHERE sha1=%s)""", [(branch.id, sha1) for sha1 in conflicting]) else: print "Non-fast-forward update detected; deleting and recreating branch." deleteBranch(db, user, repository, branch.name, old) createBranches(db, user, repository, [(branch.name, new)], flags) return else: raise IndexException("""\ Rejecting non-fast-forward update of branch. To perform the update, you can delete the branch using git push critic :%s first, and then repeat this push.""" % name) cursor.executemany("""INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE sha1=%s""", [(branch.id, sha1) for sha1 in added]) new_head = gitutils.Commit.fromSHA1(db, repository, new) cursor.execute("UPDATE branches SET head=%s WHERE id=%s", (new_head.getId(db), branch.id)) output = [] if conflicting: output.append("Pruned %d conflicting commits." % len(conflicting)) if added: output.append("Added %d new commits." % len(added)) if output: print "\n".join(output) return else: tracked_branch = False cursor.execute("SELECT id FROM reviews WHERE branch=%s", (branch.id,)) row = cursor.fetchone() is_review = bool(row) if is_review: if multiple: raise IndexException("""\ Refusing to update review in push of multiple refs. Please push one review branch at a time.""") review_id = row[0] cursor.execute("""SELECT id, old_head, old_upstream, new_upstream, uid, branch FROM reviewrebases WHERE review=%s AND new_head IS NULL""", (review_id,)) row = cursor.fetchone() if row: if tracked_branch: raise IndexException("Refusing to perform a review rebase via an automatic update.") rebase_id, old_head_id, old_upstream_id, new_upstream_id, rebaser_id, onto_branch = row review = dbutils.Review.fromId(db, review_id) rebaser = dbutils.User.fromId(db, rebaser_id) if rebaser.id != user.id: if user.isSystem(): user = rebaser else: raise IndexException("""\ This review is currently being rebased by %s <%s> and can't be otherwise updated right now.""" % (rebaser.fullname, rebaser.email)) old_head = gitutils.Commit.fromId(db, repository, old_head_id) old_commitset = log.commitset.CommitSet(review.branch.getCommits(db)) if old_head.sha1 != old: raise IndexException("""\ Unexpected error. The branch appears to have been updated since your rebase was prepared. You need to cancel the rebase via the review front-page and then try again, and/or report a bug about this error.""") if old_upstream_id is not None: new_head = gitutils.Commit.fromSHA1(db, repository, new) old_upstream = gitutils.Commit.fromId(db, repository, old_upstream_id) if new_upstream_id is not None: new_upstream = gitutils.Commit.fromId(db, repository, new_upstream_id) else: if len(new_head.parents) != 1: raise IndexException("Invalid rebase: New head can't be a merge commit.") new_upstream = gitutils.Commit.fromSHA1(db, repository, new_head.parents[0]) if new_upstream in old_commitset.getTails(): old_upstream = new_upstream = None else: old_upstream = None if old_upstream: unrelated_move = False if not new_upstream.isAncestorOf(new): raise IndexException("""\ Invalid rebase: The new upstream commit you specified when the rebase was prepared is not an ancestor of the commit now pushed. You may want to cancel the rebase via the review front-page, and prepare another one specifying the correct new upstream commit; or rebase the branch onto the new upstream specified and then push that instead.""") if not old_upstream.isAncestorOf(new_upstream): unrelated_move = True equivalent_merge = replayed_rebase = None if unrelated_move: replayed_rebase = reviewing.rebase.replayRebase( db, review, user, old_head, old_upstream, new_head, new_upstream, onto_branch) else: equivalent_merge = reviewing.rebase.createEquivalentMergeCommit( db, review, user, old_head, old_upstream, new_head, new_upstream, onto_branch) new_sha1s = repository.revlist([new_head.sha1], [new_upstream.sha1], '--topo-order') rebased_commits = [gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in new_sha1s] reachable_values = [(review.branch.id, sha1) for sha1 in new_sha1s] pending_mails = [] recipients = review.getRecipients(db) for to_user in recipients: pending_mails.extend(reviewing.mail.sendReviewRebased( db, user, to_user, recipients, review, new_upstream, rebased_commits, onto_branch)) print "Rebase performed." review.setPerformedRebase(old_head, new_head, old_upstream, new_upstream, user, equivalent_merge, replayed_rebase) if unrelated_move: reviewing.utils.addCommitsToReview( db, user, review, [replayed_rebase], pending_mails=pending_mails, silent_if_empty=set([replayed_rebase]), replayed_rebases={ replayed_rebase: new_head }) repository.keepalive(old_head) repository.keepalive(replayed_rebase) cursor.execute("""UPDATE reviewrebases SET replayed_rebase=%s WHERE id=%s""", (replayed_rebase.getId(db), rebase_id)) else: reviewing.utils.addCommitsToReview( db, user, review, [equivalent_merge], pending_mails=pending_mails, silent_if_empty=set([equivalent_merge]), full_merges=set([equivalent_merge])) repository.keepalive(equivalent_merge) cursor.execute("""UPDATE reviewrebases SET equivalent_merge=%s WHERE id=%s""", (equivalent_merge.getId(db), rebase_id)) cursor.execute("""UPDATE reviewrebases SET new_head=%s, new_upstream=%s WHERE id=%s""", (new_head.getId(db), new_upstream.getId(db), rebase_id)) cursor.execute("""INSERT INTO previousreachable (rebase, commit) SELECT %s, commit FROM reachable WHERE branch=%s""", (rebase_id, review.branch.id)) cursor.execute("DELETE FROM reachable WHERE branch=%s", (review.branch.id,)) cursor.executemany("""INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE commits.sha1=%s""", reachable_values) cursor.execute("UPDATE branches SET head=%s WHERE id=%s", (new_head.getId(db), review.branch.id)) else: old_commitset = log.commitset.CommitSet(review.branch.getCommits(db)) new_sha1s = repository.revlist([new], old_commitset.getTails(), '--topo-order') if old_head.sha1 in new_sha1s: raise IndexException("""\ Invalid history rewrite: Old head of the branch reachable from the pushed ref; no history rewrite performed. (Cancel the rebase via the review front-page if you've changed your mind.)""") for new_sha1 in new_sha1s: new_head = gitutils.Commit.fromSHA1(db, repository, new_sha1) if new_head.tree == old_head.tree: break else: raise IndexException("""\ Invalid history rewrite: The rebase introduced unexpected code changes. Use git diff between the review branch in Critic's repository and the rebased local branch to see what those changes are.""") rebased_commits = [gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in repository.revlist([new_head], old_commitset.getTails(), '--topo-order')] new_commits = [gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in repository.revlist([new], [new_head], '--topo-order')] reachable_values = [(review.branch.id, sha1) for sha1 in new_sha1s] pending_mails = [] recipients = review.getRecipients(db) for to_user in recipients: pending_mails.extend(reviewing.mail.sendReviewRebased(db, user, to_user, recipients, review, None, rebased_commits)) print "History rewrite performed." if new_commits: reviewing.utils.addCommitsToReview(db, user, review, new_commits, pending_mails=pending_mails) else: reviewing.mail.sendPendingMails(pending_mails) cursor.execute("""UPDATE reviewrebases SET new_head=%s WHERE id=%s""", (new_head.getId(db), rebase_id)) cursor.execute("""INSERT INTO previousreachable (rebase, commit) SELECT %s, commit FROM reachable WHERE branch=%s""", (rebase_id, review.branch.id)) cursor.execute("DELETE FROM reachable WHERE branch=%s", (review.branch.id,)) cursor.executemany("""INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE commits.sha1=%s""", reachable_values) cursor.execute("UPDATE branches SET head=%s WHERE id=%s", (gitutils.Commit.fromSHA1(db, repository, new).getId(db), review.branch.id)) repository.keepalive(old) review.incrementSerial(db) return True elif old != repository.mergebase([old, new]): raise IndexException("Rejecting non-fast-forward update of review branch.") elif old != repository.mergebase([old, new]): raise IndexException("""\ Rejecting non-fast-forward update of branch. To perform the update, you can delete the branch using git push critic :%s first, and then repeat this push.""" % name) cursor.execute("SELECT id FROM branches WHERE repository=%s AND base IS NULL ORDER BY id ASC LIMIT 1", (repository.id,)) root_branch_id = cursor.fetchone()[0] def isreachable(sha1): if is_review and sha1 == branch.tail_sha1: return True if base_branch_id: cursor.execute("""SELECT 1 FROM commits JOIN reachable ON (reachable.commit=commits.id) WHERE commits.sha1=%s AND reachable.branch IN (%s, %s, %s)""", (sha1, branch.id, base_branch_id, root_branch_id)) else: cursor.execute("""SELECT 1 FROM commits JOIN reachable ON (reachable.commit=commits.id) WHERE commits.sha1=%s AND reachable.branch IN (%s, %s)""", (sha1, branch.id, root_branch_id)) return cursor.fetchone() is not None stack = [new] commits = set() commit_list = [] processed = set() while stack: sha1 = stack.pop() if sha1 not in commits and not isreachable(sha1): commits.add(sha1) commit_list.append(sha1) stack.extend([parent_sha1 for parent_sha1 in gitutils.Commit.fromSHA1(db, repository, sha1).parents if parent_sha1 not in processed]) processed.add(sha1) branch = dbutils.Branch.fromName(db, repository, name) review = dbutils.Review.fromBranch(db, branch) if review: if review.state != "open": raise IndexException("""\ The review is closed and can't be extended. You need to reopen it at %s before you can add commits to it.""" % review.getURL(db, user, 2)) all_commits = [gitutils.Commit.fromSHA1(db, repository, sha1) for sha1 in reversed(commit_list)] tails = CommitSet(all_commits).getTails() if old not in tails: raise IndexException("""\ Push rejected; would break the review. It looks like some of the pushed commits are reachable from the repository's main branch, and thus consequently the commits currently included in the review are too. Perhaps you should request a new review of the follow-up commits?""") reviewing.utils.addCommitsToReview(db, user, review, all_commits, commitset=commits, tracked_branch=tracked_branch) reachable_values = [(branch.id, sha1) for sha1 in reversed(commit_list) if sha1 in commits] cursor.executemany("INSERT INTO reachable (branch, commit) SELECT %s, commits.id FROM commits WHERE commits.sha1=%s", reachable_values) cursor.execute("UPDATE branches SET head=%s WHERE id=%s", (gitutils.Commit.fromSHA1(db, repository, new).getId(db), branch.id)) db.commit() if configuration.extensions.ENABLED and review: extensions.role.processcommits.execute(db, user, review, all_commits, gitutils.Commit.fromSHA1(db, repository, old), gitutils.Commit.fromSHA1(db, repository, new), sys.stdout)
def sendReviewCreated(db, from_user, to_user, recipients, review): # First check if the user has activated email sending at all. if not to_user.getPreference(db, "email.activated"): return [] line_length = to_user.getPreference(db, "email.lineLength") hr = "-" * line_length data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'review.owner.fullname': review.owners[0].fullname, 'review.branch.name': review.branch.name, 'review.branch.repository': "%s:%s" % (configuration.base.HOSTNAME, review.repository.path), 'hr': hr } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s """ % data body += """%(review.owner.fullname)s has requested a review of the changes on the branch %(review.branch.name)s in the repository %(review.branch.repository)s """ % data all_reviewers = to_user.getPreference(db, "email.newReview.displayReviewers") all_watchers = to_user.getPreference(db, "email.newReview.displayWatchers") if all_reviewers or all_watchers: if all_reviewers: if review.reviewers: body += "The users assigned to review the changes on the review branch are:\n" for reviewer in review.reviewers: body += " " + reviewer.fullname + "\n" body += "\n" else: body += """No reviewers have been identified for the changes in this review. This means the review is currently stuck; it cannot finish unless there are reviewers. """ if all_watchers and review.watchers: body += "The following additional users are following the review:\n" for watcher in review.watchers: body += " " + watcher.fullname + "\n" body += "\n" body += "\n" if review.description: body += """Description: %s """ % textutils.reflow(review.description, line_length, indent=2) cursor = db.cursor() cursor.execute("""SELECT file, SUM(deleted), SUM(inserted) FROM fullreviewuserfiles WHERE review=%s AND assignee=%s GROUP BY file""", (review.id, to_user.id)) pending_files_lines = cursor.fetchall() if pending_files_lines: body += renderFiles(db, to_user, review, "These changes were assigned to you:", pending_files_lines, showcommit_link=True) all_commits = to_user.getPreference(db, "email.newReview.displayCommits") if all_commits: body += "The commits requested to be reviewed are:\n\n" contextLines = to_user.getPreference(db, "email.newReview.diff.contextLines") diffMaxLines = to_user.getPreference(db, "email.newReview.diff.maxLines") displayStats = to_user.getPreference(db, "email.newReview.displayStats") statsMaxLines = to_user.getPreference(db, "email.newReview.stats.maxLines") if contextLines < 0: contextLines = 0 commits = list(reversed(review.branch.commits)) if diffMaxLines == 0: diffs = None else: diffs = {} lines = 0 for commit in commits: if len(commit.parents) == 1: cursor.execute("""SELECT id FROM reviewchangesets JOIN changesets ON (id=changeset) WHERE review=%s AND child=%s""", (review.id, commit.getId(db))) (changeset_id,) = cursor.fetchone() diff = changeset_text.unified(db, changeset_load.loadChangeset(db, review.repository, changeset_id), contextLines) diffs[commit] = diff lines += diff.count("\n") if lines > diffMaxLines: diffs = None break if not displayStats or statsMaxLines == 0: stats = None else: stats = {} lines = 0 for commit in commits: commit_stats = review.repository.run("show", "--oneline", "--stat", commit.sha1).split('\n', 1)[1] stats[commit] = commit_stats lines += commit_stats.count('\n') if lines > statsMaxLines: stats = None break for index, commit in enumerate(commits): if index > 0: body += "\n\n\n" body += """Commit: %(sha1)s Author: %(author.fullname)s <%(author.email)s> at %(author.time)s %(message)s """ % { 'sha1': commit.sha1, 'author.fullname': commit.author.getFullname(db), 'author.email': commit.author.email, 'author.time': time.strftime("%Y-%m-%d %H:%M:%S", commit.author.time), 'message': textutils.reflow(commit.message.strip(), line_length, indent=2) } if stats and commit in stats: body += "---\n" + stats[commit] if diffs and commit in diffs: body += "\n" + diffs[commit] message_id = generateMessageId() cursor.execute("INSERT INTO reviewmessageids (uid, review, messageid) VALUES (%s, %s, %s)", [to_user.id, review.id, message_id]) return [sendMail(db, review, message_id, from_user, to_user, recipients, generateSubjectLine(db, to_user, review, 'newReview'), body)]
def process_request(environ, start_response): request_start = time.time() critic = api.critic.startSession(for_user=True) db = critic.database user = None try: try: req = request.Request(db, environ, start_response) # Handle static resources very early. We don't bother with checking # for an authenticated user; static resources aren't sensitive, and # are referenced from special-case resources like the login page and # error messages that, that we want to display even if something # went wrong with the authentication. if req.path.startswith("static-resource/"): return handleStaticResource(req) if req.path.startswith("externalauth/"): provider_name = req.path[len("externalauth/"):] if provider_name in auth.PROVIDERS: provider = auth.PROVIDERS[provider_name] authorize_url = provider.start(db, req) if authorize_url: raise request.Found(authorize_url) if req.path.startswith("oauth/"): provider_name = req.path[len("oauth/"):] if provider_name in auth.PROVIDERS: provider = auth.PROVIDERS[provider_name] if isinstance(provider, auth.OAuthProvider): finishOAuth(db, req, provider) auth.checkSession(db, req) auth.AccessControl.accessHTTP(db, req) user = req.user user.loadPreferences(db) if user.status == 'retired': # If a retired user accesses the system, change the status back # to 'current' again. with db.updating_cursor("users") as cursor: cursor.execute("""UPDATE users SET status='current' WHERE id=%s""", (user.id,)) user.status = 'current' if not user.getPreference(db, "debug.profiling.databaseQueries"): db.disableProfiling() original_path = req.path if not req.path: if user.isAnonymous(): location = "tutorial" else: location = user.getPreference(db, "defaultPage") if req.query: location += "?" + req.query raise request.MovedTemporarily(location) if req.path == "redirect": target = req.getParameter("target", "/") raise request.SeeOther(target) if req.path == "findreview": # This raises either DisplayMessage or MovedTemporarily. findreview(req, db) # Require a .git suffix on HTTP(S) repository URLs unless the user- # agent starts with "git/" (as Git's normally does.) # # Our objectives are: # # 1) Not to require Git's user-agent to be its default value, since # the user might have to override it to get through firewalls. # 2) Never to send regular user requests to 'git http-backend' by # mistake. # # This is a compromise. if req.getRequestHeader("User-Agent", "").startswith("git/"): suffix = None else: suffix = ".git" if handleRepositoryPath(db, req, user, suffix): db = None return [] # Extension "page" roles. Prefixing a path with "!/" bypasses all # extensions. # # Also bypass extensions if the user is anonymous unless general # anonymous access is allowed. If it's not and the user is still # anonymous, access was allowed because of a path-based exception, # which was not intended to allow access to an extension. if req.path.startswith("!/"): req.path = req.path[2:] elif configuration.extensions.ENABLED: handled = extensions.role.page.execute(db, req, user) if isinstance(handled, basestring): req.start() return [handled] if req.path.startswith("r/"): req.updateQuery({ "id": [req.path[2:]] }) req.path = "showreview" if configuration.extensions.ENABLED: match = RE_EXTENSION_RESOURCE.match(req.path) if match: content_type, resource = extensions.resource.get(req, db, user, match.group(1)) if resource: req.setContentType(content_type) if content_type.startswith("image/"): req.addResponseHeader("Cache-Control", "max-age=3600") req.start() return [resource] else: req.setStatus(404) req.start() return [] if req.path.startswith("download/"): return handleDownload(db, req, user) if req.path == "api" or req.path.startswith("api/"): try: result = jsonapi.handleRequest(critic, req) except jsonapi.Error as error: req.setStatus(error.http_status) result = { "error": { "title": error.title, "message": error.message }} else: req.setStatus(200) accept_header = req.getRequestHeader("Accept") if accept_header == "application/vnd.api+json": default_indent = None else: default_indent = 2 indent = req.getParameter("indent", default_indent, filter=int) if indent == 0: # json.encode(..., indent=0) still gives line-breaks, just # no indentation. This is not so useful, so set indent to # None instead, which disables formatting entirely. indent = None req.setContentType("application/vnd.api+json") req.start() return [json_encode(result, indent=indent)] operationfn = OPERATIONS.get(req.path) if operationfn: result = operationfn(req, db, user) if isinstance(result, (OperationResult, OperationError)): req.setContentType("text/json") if isinstance(result, OperationResult): if db.profiling: result.set("__profiling__", formatDBProfiling(db)) result.set("__time__", time.time() - request_start) elif not req.hasContentType(): req.setContentType("text/plain") req.start() if isinstance(result, unicode): return [result.encode("utf8")] else: return [str(result)] impersonate_user = user if not user.isAnonymous(): user_parameter = req.getParameter("user", None) if user_parameter: impersonate_user = dbutils.User.fromName(db, user_parameter) while True: pagefn = PAGES.get(req.path) if pagefn: try: result = pagefn(req, db, impersonate_user) if db.profiling and not (isinstance(result, str) or isinstance(result, Document)): source = "" for fragment in result: source += fragment result = source if isinstance(result, page.utils.ResponseBody): req.setContentType(result.content_type) req.start() return [result.data] if isinstance(result, str) or isinstance(result, Document): req.setContentType("text/html") req.start() result = str(result) result += ("<!-- total request time: %.2f ms -->" % ((time.time() - request_start) * 1000)) if db.profiling: result += ("<!--\n\n%s\n\n -->" % formatDBProfiling(db)) return [result] result = WrappedResult(db, req, user, result) req.setContentType("text/html") req.start() # Prevent the finally clause below from closing the # connection. WrappedResult does it instead. db = None return result except gitutils.NoSuchRepository as error: raise page.utils.DisplayMessage( title="Invalid URI Parameter!", body=error.message) except gitutils.GitReferenceError as error: if error.ref: raise page.utils.DisplayMessage( title="Specified ref not found", body=("There is no ref named \"%s\" in %s." % (error.ref, error.repository))) elif error.sha1: raise page.utils.DisplayMessage( title="SHA-1 not found", body=error.message) else: raise except dbutils.NoSuchUser as error: raise page.utils.DisplayMessage( title="Invalid URI Parameter!", body=error.message) except dbutils.NoSuchReview as error: raise page.utils.DisplayMessage( title="Invalid URI Parameter!", body=error.message) if "/" in req.path: repository_name, _, rest = req.path.partition("/") repository = gitutils.Repository.fromName(db, repository_name) if repository: req.path = rest else: repository = None def revparsePlain(item): try: return gitutils.getTaggedCommit(repository, repository.revparse(item)) except: raise revparse = revparsePlain if repository is None: review_id = req.getParameter("review", None, filter=int) if review_id: cursor = db.cursor() cursor.execute("""SELECT repository FROM branches JOIN reviews ON (reviews.branch=branches.id) WHERE reviews.id=%s""", (review_id,)) row = cursor.fetchone() if row: repository = gitutils.Repository.fromId(db, row[0]) def revparseWithReview(item): if re.match("^[0-9a-f]+$", item): cursor.execute("""SELECT sha1 FROM commits JOIN changesets ON (changesets.child=commits.id) JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id) WHERE reviewchangesets.review=%s AND commits.sha1 LIKE %s""", (review_id, item + "%")) row = cursor.fetchone() if row: return row[0] else: return revparsePlain(item) revparse = revparseWithReview if repository is None: repository = user.getDefaultRepository(db) if gitutils.re_sha1.match(req.path): if repository and not repository.iscommit(req.path): repository = None if not repository: try: repository = gitutils.Repository.fromSHA1(db, req.path) except gitutils.GitReferenceError: repository = None if repository: try: items = filter(None, map(revparse, req.path.split(".."))) updated_query = {} if len(items) == 1: updated_query["repository"] = [repository.name] updated_query["sha1"] = [items[0]] elif len(items) == 2: updated_query["repository"] = [repository.name] updated_query["from"] = [items[0]] updated_query["to"] = [items[1]] if updated_query: req.updateQuery(updated_query) req.path = "showcommit" continue except gitutils.GitReferenceError: pass break raise page.utils.DisplayMessage( title="Not found!", body="Page not handled: /%s" % original_path, status=404) except GeneratorExit: raise except auth.AccessDenied as error: return handleDisplayMessage( db, req, request.DisplayMessage( title="Access denied", body=error.message, status=403)) except request.HTTPResponse as response: return response.execute(db, req) except request.MissingWSGIRemoteUser as error: return handleMissingWSGIRemoteUser(db, req) except page.utils.DisplayMessage as message: return handleDisplayMessage(db, req, message) except page.utils.DisplayFormattedText as formatted_text: return handleDisplayFormattedText(db, req, formatted_text) except Exception: # crash might be psycopg2.ProgrammingError so rollback to avoid # "InternalError: current transaction is aborted" inside handleException() if db and db.closed(): db = None elif db: db.rollback() error_title, error_body = handleException(db, req, user) error_body = reflow("\n\n".join(error_body)) error_message = "\n".join([error_title, "=" * len(error_title), "", error_body]) assert not req.isStarted() req.setStatus(500) req.setContentType("text/plain") req.start() return [error_message] finally: if db: db.close()
def sendReviewPlaceholder(db, to_user, review): # First check if the user has activated email sending at all. if not to_user.getPreference(db, "email.activated"): return [] line_length = to_user.getPreference(db, "email.lineLength") hr = "-" * line_length why = "This message is sent to you when you become associated with a review after the review was initially requested. It is then sent instead of the regular \"New Review\" message, for the purpose of using as the reference/in-reply-to message for other messages sent about this review." data = { 'review.id': review.id, 'review.url': review.getURL(db, to_user, 2), 'review.owner.fullname': review.owners[0].fullname, 'review.branch.name': review.branch.name, 'review.branch.repository': "%s:%s" % (configuration.base.HOSTNAME, review.repository.path), 'hr': hr, 'why': textutils.reflow(why, line_length) } body = """%(hr)s This is an automatic message generated by the review at: %(review.url)s %(hr)s %(why)s %(hr)s """ % data body += """%(review.owner.fullname)s has requested a review of the changes on the branch %(review.branch.name)s in the repository %(review.branch.repository)s """ % data all_reviewers = to_user.getPreference(db, "email.newReview.displayReviewers") all_watchers = to_user.getPreference(db, "email.newReview.displayWatchers") if all_reviewers or all_watchers: if all_reviewers: if review.reviewers: body += "The users assigned to review the changes on the review branch are:\n" for reviewer in review.reviewers: body += " " + reviewer.fullname + "\n" body += "\n" else: body += """No reviewers have been identified for the changes in this review. This means the review is currently stuck; it cannot finish unless there are reviewers. """ if all_watchers and review.watchers: body += "The following additional users are following the review:\n" for watcher in review.watchers: body += " " + watcher.fullname + "\n" body += "\n" body += "\n" if review.description: body += """Description: %s """ % textutils.reflow(review.description, line_length, indent=2) message_id = generateMessageId() cursor = db.cursor() cursor.execute("INSERT INTO reviewmessageids (uid, review, messageid) VALUES (%s, %s, %s)", [to_user.id, review.id, message_id]) return [sendMail(db, review, message_id, review.owners[0], to_user, [to_user], generateSubjectLine(db, to_user, review, "newishReview"), body)]