def getTitle(self, db, html=False): if html: title = "<b>%s</b>" % htmlutils.htmlify(self.getName()) else: title = self.getName() if not self.isSystemExtension(): author = self.getAuthor(db) try: manifest = self.getManifest() except ManifestError: # Can't access information from the manifest, so assume "yes". is_author = True else: is_author = manifest.isAuthor(db, author) if is_author: title += " by " else: title += " hosted by " if html: title += htmlutils.htmlify(author.fullname) else: title += author.fullname return title
def __call__(self, source, output_file, contexts_path): self.output = output_file blocks = re.split("^((?:<<<<<<<|>>>>>>>)[^\n]*\n)", source, flags=re.MULTILINE) in_conflict = False for index, block in enumerate(blocks): if (index & 1) == 0: if in_conflict: blocks = re.split("^(=======[^\n]*\n)", block, flags=re.MULTILINE) else: blocks = [block] for index, block in enumerate(blocks): if (index & 1) == 0: if block: for token, value in self.lexer.get_tokens(block): self.highlightToken(token, value) else: assert block[0] == "=" self.output.write(htmlutils.htmlify(block)) else: assert block[0] == "<" or block[0] == ">" self.output.write(htmlutils.htmlify(block)) in_conflict = block[0] == "<"
def leader(self, max_length=80, text=False): if self.__leader is None: self.__leader = self.comments[0].comment.split("\n", 1)[0] if len(self.__leader) > max_length: if text: return self.__leader[:max_length - 5] + "[...]" else: return htmlutils.htmlify(self.__leader[:max_length - 3]) + "[…]" else: if text: return self.__leader else: return htmlutils.htmlify(self.__leader)
def process(self, db, user, branch_id, new_remote_name=None): cursor = db.cursor() if not user.hasRole(db, "administrator"): cursor.execute("""SELECT 1 FROM trackedbranchusers WHERE branch=%s AND uid=%s""", (branch_id, user.id)) if not cursor.fetchone(): raise OperationFailure(code="notallowed", title="Not allowed!", message="Operation not permitted.") review_state = getTrackedBranchReviewState(db, branch_id) if review_state is not None and review_state != "open": raise OperationFailure(code="reviewnotopen", title="The review is not open!", message="You need to reopen the review before new commits can be added to it.") if new_remote_name is not None: cursor.execute("""SELECT remote FROM trackedbranches WHERE id=%s""", (branch_id,)) remote = cursor.fetchone()[0] if not gitutils.Repository.lsremote(remote, pattern="refs/heads/" + new_remote_name): raise OperationFailure( code="refnotfound", title="Remote ref not found!", message=("Could not find the ref <code>%s</code> in the repository <code>%s</code>." % (htmlutils.htmlify("refs/heads/" + new_remote_name), htmlutils.htmlify(remote))), is_html=True) cursor.execute("""UPDATE trackedbranches SET remote_name=%s, disabled=FALSE, next=NULL WHERE id=%s""", (new_remote_name, branch_id)) else: cursor.execute("""UPDATE trackedbranches SET disabled=FALSE, next=NULL WHERE id=%s""", (branch_id,)) db.commit() pid = int(open(configuration.services.BRANCHTRACKER["pidfile_path"]).read().strip()) os.kill(pid, signal.SIGHUP) return OperationResult()
def leader(self, max_length=80, text=False): if self.__leader is None: self.__leader = self.comments[0].comment.split("\n", 1)[0] if len(self.__leader) > max_length: if text: return self.__leader[:max_length - 5] + "[...]" else: return htmlutils.htmlify( self.__leader[:max_length - 3]) + "[…]" else: if text: return self.__leader else: return htmlutils.htmlify(self.__leader)
def eliminateCommonPrefixes(files, text=False, getpath=None, setpath=None): assert (getpath is None) == (setpath is None) if getpath is None: def defaultGetPath(x): return x getpath = defaultGetPath if setpath is None: def defaultSetPath(x, p): files[index] = p setpath = defaultSetPath def commonPrefixLength(pathA, pathB): componentsA = pathA.split('/') componentsB = pathB.split('/') for index in range(min(len(componentsA), len(componentsB))): if componentsA[index] != componentsB[index]: return sum(map(len, componentsA[:index])) + index if files: previous = None for index in range(len(files)): current = getpath(files[index]) if index > 0: length = commonPrefixLength(previous, current) else: length = 0 if text: if length > 4: updated = (" " * (length - 4) + ".../" + textutils.escape(current[length:])) else: updated = textutils.escape(current) else: if length > 2: updated = (" " * (length - 2) + "…/" + htmlutils.htmlify( textutils.escape(current[length:]))) else: updated = htmlutils.htmlify(textutils.escape(current)) if updated != current: setpath(files[index], updated) previous = current return files
def highlightToken(self, token): if token.iskeyword(): self.output.write("<b class='kw'>" + str(token) + "</b>") elif token.isidentifier(): self.output.write("<b class='id'>" + str(token) + "</b>") elif token.iscomment(): if str(token)[0:2] == "/*": lines = str(token).splitlines() self.output.write("\n".join(["<b class='com'>" + htmlutils.htmlify(line) + "</b>" for line in lines])) else: self.output.write("<b class='com'>" + htmlutils.htmlify(token) + "</b>") elif token.isppdirective(): lines = str(token).split("\n") self.output.write("\n".join(["<b class='pp'>" + htmlutils.htmlify(line) + "</b>" for line in lines])) elif token.isspace(): self.output.write(str(token)) elif token.isconflictmarker(): self.output.write(htmlutils.htmlify(token)) elif str(token)[0] == '"': self.output.write("<b class='str'>" + htmlutils.htmlify(token) + "</b>") elif str(token)[0] == "'": self.output.write("<b class='ch'>" + htmlutils.htmlify(token) + "</b>") elif token.isfloat(): self.output.write("<b class='fp'>" + str(token) + "</b>") elif token.isint(): self.output.write("<b class='int'>" + str(token) + "</b>") elif token.isbyteordermark(): self.output.write(htmlutils.htmlify(u"\ufeff")) else: self.output.write("<b class='op'>" + htmlutils.htmlify(token) + "</b>")
def outputDirectory(base, name, directories, files): if name: level = base.count("/") row = basic.tr("directory", critic_level=level) row.td("select").input(type="checkbox") if level > 1: row.td("path").preformatted().innerHTML((" " * (len(base) - 2)) + "…/" + name + "/") else: row.td("path").preformatted().innerHTML(base + name + "/") row.td().text() else: level = 0 for directory_name in sorted(directories.keys()): outputDirectory(base + name + "/" if name else "", directory_name, directories[directory_name][0], directories[directory_name][1]) for file_name in sorted(files.keys()): row = basic.tr("file", critic_file_id=files[file_name], critic_level=level + 1) row.td("select").input(type="checkbox") row.td("path").preformatted().innerHTML( (" " * (len(base + name) - 1)) + "…/" + htmlutils.htmlify(file_name)) row.td().text()
def highlightToken(self, token, value): def tag(cls, value): return "<b class='%s'>%s</b>" % (cls, htmlutils.htmlify(value)) def tagm(cls, value): if value == "\n": return value else: res = [] for line in value.splitlines(): if line: res.append(tag(cls, line)) else: res.append(line) if value.endswith("\n"): res.append("") return "\n".join(res) value = value.encode("utf-8") if token in pygments.token.Token.Punctuation or token in pygments.token.Token.Operator: self.output.write(tag("op", value)) elif token in pygments.token.Token.Name or token in pygments.token.Token.String.Symbol: self.output.write(tag("id", value)) elif token in pygments.token.Token.Keyword: self.output.write(tag("kw", value)) elif token in pygments.token.Token.String: self.output.write(tagm("str", value)) elif token in pygments.token.Token.Comment: self.output.write(tagm("com", value)) elif token in pygments.token.Token.Number.Integer: self.output.write(tag("int", value)) elif token in pygments.token.Token.Number.Float: self.output.write(tag("fp", value)) else: self.output.write(htmlutils.htmlify(value))
def execute(self, db, req): from htmlutils import htmlify if not req.allowRedirect(self.status): self.status = 403 self.body = [ "Cowardly refusing to redirect %s request." % req.method ] else: req.addResponseHeader("Location", self.location) self.body = [ "<p>Please go here: <a href=%s>%s</a>." % (htmlify(self.location, attributeValue=True), htmlify(self.location)) ] self.content_type = "text/html" return super(Redirect, self).execute(db, req)
def loadNewLines(self, highlighted=False, request_highlight=False): """Load the lines of the new version of the file, optionally highlighted.""" from diff.parse import splitlines if self.new_sha1 is None or self.new_sha1 == '0' * 40: self.new_plain = [] self.new_highlighted = [] return elif self.new_mode and self.new_mode == "160000": self.new_plain = self.new_highlighted = ["Subproject commit %s" % self.new_sha1] return if highlighted: if self.new_highlighted and self.new_is_highlighted: return else: self.new_is_highlighted = True language = self.getLanguage(use_content="new") if language: data = syntaxhighlight.readHighlight(self.repository, self.new_sha1, self.path, language, request=request_highlight) elif self.new_highlighted: return else: data = htmlutils.htmlify(textutils.decode(self.repository.fetch(self.new_sha1).data)) self.new_highlighted = splitlines(data) self.new_eof_eol = data and data[-1] in "\n\r" else: if self.new_plain: return else: data = self.repository.fetch(self.new_sha1).data self.new_plain = splitlines(data) self.new_eof_eol = data and data[-1] in "\n\r"
def outputDirectory(base, name, directories, files): if name: level = base.count("/") row = basic.tr("directory", critic_level=level) row.td("select").input(type="checkbox") if level > 1: row.td("path").preformatted().innerHTML((" " * (len(base) - 2)) + "…/" + name + "/") else: row.td("path").preformatted().innerHTML(base + name + "/") row.td().text() else: row = basic.tr("directory", critic_level=-1) row.td("select").input(type="checkbox") row.td("path").preformatted().i().text("Everything") row.td().text() level = -1 for directory_name in sorted(directories.keys()): outputDirectory(base + name + "/" if name else "", directory_name, directories[directory_name][0], directories[directory_name][1]) for file_name in sorted(files.keys()): row = basic.tr("file", critic_file_id=files[file_name], critic_level=level + 1) row.td("select").input(type="checkbox") if level > -1: row.td("path").preformatted().innerHTML((" " * (len(base + name) - 1)) + "…/" + htmlutils.htmlify(file_name)) else: row.td("path").preformatted().innerHTML(htmlutils.htmlify(file_name)) row.td().text()
def handleException(db, req, user, as_html=False): error_message = traceback.format_exc() environ = req.getEnvironment() environ["wsgi.errors"].write(error_message) if not user or not db or not user.hasRole(db, "developer"): url = wsgiref.util.request_uri(environ) x_forwarded_host = req.getRequestHeader("X-Forwarded-Host") if x_forwarded_host: original_host = x_forwarded_host.split(",")[0].strip() def replace_host(match): return match.group(1) + original_host url = re.sub("^([a-z]+://)[^/]+", replace_host, url) if user and not user.isAnonymous(): user_string = str(user) else: user_string = "<anonymous>" mailutils.sendExceptionMessage(db, "wsgi", "\n".join(["User: %s" % user_string, "Method: %s" % req.method, "URL: %s" % url, "", error_message])) admin_message_sent = True else: admin_message_sent = False if not user or not db or user.hasRole(db, "developer") \ or configuration.debug.IS_DEVELOPMENT \ or configuration.debug.IS_TESTING: error_title = "Unexpected error!" error_message = error_message.strip() if as_html: error_message = "<p class='pre inset'>%s</p>" % htmlify(error_message) error_body = [error_message] if admin_message_sent: admin_message_sent = ("A message has been sent to the system " "administrator(s) with details about the " "problem.") if as_html: admin_message_sent = "<p>%s</p>" % admin_message_sent error_body.append(admin_message_sent) else: error_title = "Request failed!" error_message = ("An unexpected error occurred while handling the " "request. A message has been sent to the system " "administrator(s) with details about the problem. " "Please contact them for further information and/or " "assistance.") if as_html: error_message = "<p>%s</p>" % error_message error_body = [error_message] return error_title, error_body
def eliminateCommonPrefixes(files, text=False, getpath=None, setpath=None): assert (getpath is None) == (setpath is None) if getpath is None: def defaultGetPath(x): return x getpath = defaultGetPath if setpath is None: def defaultSetPath(x, p): files[index] = p setpath = defaultSetPath def commonPrefixLength(pathA, pathB): componentsA = pathA.split('/') componentsB = pathB.split('/') for index in range(min(len(componentsA), len(componentsB))): if componentsA[index] != componentsB[index]: return sum(map(len, componentsA[:index])) + index if files: previous = None for index in range(len(files)): current = getpath(files[index]) if index > 0: length = commonPrefixLength(previous, current) else: length = 0 if text: if length > 4: updated = (" " * (length - 4) + ".../" + textutils.escape(current[length:])) else: updated = textutils.escape(current) else: if length > 2: updated = (" " * (length - 2) + "…/" + htmlutils.htmlify(textutils.escape(current[length:]))) else: updated = htmlutils.htmlify(textutils.escape(current)) if updated != current: setpath(files[index], updated) previous = current return files
def summarize(string, max_length=80, as_html=False): if len(string) <= max_length: return string string = string[:max_length - 5] if as_html: import htmlutils return htmlutils.htmlify(string) + "[…]" else: return string + "[...]"
def linkify(match): config_item, reference_text, reference_name = match.groups() if config_item: url = "/config?highlight=%s" % config_item text = config_item title = None else: reference_name = re.sub(r"\s+", " ", reference_name) assert reference_name in references, reference_name url, title = references[reference_name] text = reference_text link = "<a href=%s" % htmlutils.htmlify(url, True) if title: link += " title=%s" % htmlutils.htmlify(title, True) return link + ">%s</a>" % htmlutils.htmlify(text)
def addItem(self, heading, value, description=None, buttons=None): row = self.table.tr("item") row.td("name").innerHTML(htmlutils.htmlify(heading).replace(" ", " ") + ":") cell = row.td("value", colspan=2).preformatted() if callable(value): value(cell) else: cell.text(str(value)) if buttons: div = cell.div("buttons") for label, onclick in buttons: div.button(onclick=onclick).text(label) if description is not None: self.table.tr("help").td(colspan=len(self.columns)).text(description)
def process(self, db, user, remote, pattern=None): if pattern: regexp = re.compile(pattern.replace("*", ".*")) else: regexp = None try: refs = gitutils.Repository.lsremote(remote, regexp=regexp) except gitutils.GitCommandError, error: if error.output.splitlines()[0].endswith("does not appear to be a git repository"): raise OperationFailure( code="invalidremote", title="Invalid remote!", message=("<code>%s</code> does not appear to be a valid Git repository." % htmlutils.htmlify(remote)), is_html=True) else: raise
def highlightToken(self, token): if token.iskeyword(): self.output.write("<b class='kw'>" + str(token) + "</b>") elif token.isidentifier(): self.output.write("<b class='id'>" + str(token) + "</b>") elif token.iscomment(): if str(token)[0:2] == "/*": lines = str(token).splitlines() self.output.write("\n".join([ "<b class='com'>" + htmlutils.htmlify(line) + "</b>" for line in lines ])) else: self.output.write("<b class='com'>" + htmlutils.htmlify(token) + "</b>") elif token.isppdirective(): lines = str(token).split("\n") self.output.write("\n".join([ "<b class='pp'>" + htmlutils.htmlify(line) + "</b>" for line in lines ])) elif token.isspace(): self.output.write(str(token)) elif token.isconflictmarker(): self.output.write(htmlutils.htmlify(token)) elif str(token)[0] == '"': self.output.write("<b class='str'>" + htmlutils.htmlify(token) + "</b>") elif str(token)[0] == "'": self.output.write("<b class='ch'>" + htmlutils.htmlify(token) + "</b>") elif token.isfloat(): self.output.write("<b class='fp'>" + str(token) + "</b>") elif token.isint(): self.output.write("<b class='int'>" + str(token) + "</b>") elif token.isbyteordermark(): self.output.write(htmlutils.htmlify(u"\ufeff")) else: self.output.write("<b class='op'>" + htmlutils.htmlify(token) + "</b>")
def readHighlight(repository, sha1, path, language, request=False): path = generateHighlightPath(sha1, language) if os.path.isfile(path): os.utime(path, None) source = open(path).read() elif os.path.isfile(path + ".bz2"): os.utime(path + ".bz2", None) source = bz2.BZ2File(path + ".bz2", "r").read() elif request: import request request.requestHighlights(repository, { sha1: (path, language) }) return readHighlight(repository, sha1, path, language) else: source = None if not source: source = htmlutils.htmlify(repository.fetch(sha1)[2]) return source.replace("\r", "")
def readHighlight(repository, sha1, path, language, request=False): path = generateHighlightPath(sha1, language) if os.path.isfile(path): os.utime(path, None) source = open(path).read() elif os.path.isfile(path + ".bz2"): os.utime(path + ".bz2", None) source = bz2.BZ2File(path + ".bz2", "r").read() elif request: import request request.requestHighlights(repository, {sha1: (path, language)}) return readHighlight(repository, sha1, path, language) else: source = None if not source: source = htmlutils.htmlify(repository.fetch(sha1)[2]) return source
def loadNewLines(self, highlighted=False, request_highlight=False): """Load the lines of the new version of the file, optionally highlighted.""" from diff.parse import splitlines if self.new_sha1 is None or self.new_sha1 == '0' * 40: self.new_plain = [] self.new_highlighted = [] return elif self.new_mode and self.new_mode == "160000": self.new_plain = self.new_highlighted = [ "Subproject commit %s" % self.new_sha1 ] return if highlighted: if self.new_highlighted and self.new_is_highlighted: return else: self.new_is_highlighted = True language = self.getLanguage(use_content="new") if language: data = syntaxhighlight.readHighlight( self.repository, self.new_sha1, self.path, language, request=request_highlight) elif self.new_highlighted: return else: data = htmlutils.htmlify( textutils.decode( self.repository.fetch(self.new_sha1).data)) self.new_highlighted = splitlines(data) self.new_eof_eol = data and data[-1] in "\n\r" else: if self.new_plain: return else: data = self.repository.fetch(self.new_sha1).data self.new_plain = splitlines(data) self.new_eof_eol = data and data[-1] in "\n\r"
def process(self, db, user, repository_name, remote, branch, upstream="refs/heads/master"): repository = gitutils.Repository.fromName(db, repository_name) cursor = db.cursor() # Check if any other repository is currently tracking branches from this # remote. If that's the case, then the user most likely either selected # the wrong repository or entered the wrong remote. cursor.execute("""SELECT repositories.name FROM repositories JOIN trackedbranches ON (trackedbranches.repository=repositories.id) WHERE repositories.id!=%s AND trackedbranches.remote=%s""", (repository.id, remote)) for (other_name,) in cursor: raise OperationFailure( code="badremote", title="Bad remote!", message=("The remote <code>%s</code> appears to be related to " "another repository on this server (<code>%s</code>). " "You most likely shouldn't be importing branches from " "it into the selected repository (<code>%s</code>)." % (htmlutils.htmlify(remote), htmlutils.htmlify(other_name), htmlutils.htmlify(repository_name))), is_html=True) if not branch.startswith("refs/"): branch = "refs/heads/%s" % branch try: head_sha1 = repository.fetchTemporaryFromRemote(remote, branch) except gitutils.GitReferenceError: raise OperationFailure( code="refnotfound", title="Remote ref not found!", message=("Could not find the ref <code>%s</code> in the repository <code>%s</code>." % (htmlutils.htmlify(branch), htmlutils.htmlify(remote))), is_html=True) except gitutils.GitCommandError, error: if error.output.splitlines()[0].endswith("does not appear to be a git repository"): raise OperationFailure( code="invalidremote", title="Invalid remote!", message=("<code>%s</code> does not appear to be a valid Git repository." % htmlutils.htmlify(remote)), is_html=True) else: raise
def process(self, db, user, remote, pattern=None): if pattern: regexp = re.compile(pattern.replace("*", ".*")) else: regexp = None try: refs = gitutils.Repository.lsremote(remote, regexp=regexp) except gitutils.GitCommandError as error: if error.output.splitlines()[0].endswith( "does not appear to be a git repository"): raise OperationFailure( code="invalidremote", title="Invalid remote!", message= ("<code>%s</code> does not appear to be a valid Git repository." % htmlutils.htmlify(remote)), is_html=True) else: raise else: branches = dict([(ref[1], ref[0]) for ref in refs]) return OperationResult(branches=branches)
def next(self): if self.failed: raise StopIteration try: if self.first: value = self.first self.first = None else: value = self.result.next() self.db.rollback() return value except StopIteration: self.db.close() raise except Exception: error_title, error_body = handleException( self.db, self.req, self.user) self.db.close() if self.req.getContentType().startswith("text/html"): self.failed = True error_body = "".join("<p>%s</p>" % htmlify(part) for part in error_body) # Close a bunch of tables, in case we're in any. Not # pretty, but probably makes the end result prettier. return ("</table></table></table></table></div>" "<div class='fatal'><table align=center><tr>" "<td><h1>%s</h1>%s</td></tr></table></div>" % (error_title, error_body)) else: raise StopIteration
def main(environ, start_response): request_start = time.time() db = dbutils.Database() user = None try: try: req = request.Request(db, environ, start_response) if req.user is None: if configuration.base.AUTHENTICATION_MODE == "critic": if configuration.base.SESSION_TYPE == "httpauth": req.setStatus(401) req.addResponseHeader("WWW-Authenticate", "Basic realm=\"Critic\"") req.start() return elif configuration.base.ALLOW_ANONYMOUS_USER or req.path in ( "login", "validatelogin"): user = dbutils.User.makeAnonymous() elif req.method == "GET": raise page.utils.NeedLogin, req else: # Don't try to redirect POST requests to the login page. req.setStatus(403) req.start() return else: try: user = dbutils.User.fromName(db, req.user) except dbutils.NoSuchUser: cursor = db.cursor() cursor.execute( """INSERT INTO users (name, email, fullname) VALUES (%s, %s, %s) RETURNING id""", (req.user, getUserEmailAddress(req.user), req.user)) user = dbutils.User.fromId(db, cursor.fetchone()[0]) db.commit() 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() yield "<meta http-equiv='refresh' content='0; %s'>" % htmlify( target) return else: raise page.utils.MovedTemporarily, target if req.path.startswith("!/"): req.path = req.path[2:] elif configuration.extensions.ENABLED: handled = extensions.executePage(db, req, user) if handled: req.start() yield handled return 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.getExtensionResource( req, db, user, match.group(1)) if resource: req.setContentType(content_type) req.start() yield resource return else: req.setStatus(404) req.start() return if req.path.startswith("download/"): operation = download else: operation = operations.get(req.path) if operation: req.setContentType("text/plain") try: result = operation(req, db, user) except OperationError, error: result = error except page.utils.DisplayMessage, message: result = "error:" + message.title if message.body: result += " " + message.body except Exception, exception: result = "error:\n" + "".join( traceback.format_exception(*sys.exc_info()))
def process(self, db, user, filter_type, path, delegates, repository_id=None, repository_name=None, replaced_filter_id=None): path = reviewing.filters.sanitizePath(path) if "*" in path: try: reviewing.filters.validatePattern(path) except reviewing.filters.PatternError as error: raise OperationFailure(code="invalidpattern", title="Invalid path pattern", message="There are invalid wild-cards in the path: %s" % error.message) if filter_type == "reviewer": delegates = filter(None, delegates) invalid_delegates = [] for delegate in delegates: try: dbutils.User.fromName(db, delegate) except dbutils.NoSuchUser: invalid_delegates.append(delegate) if invalid_delegates: raise OperationFailure(code="invaliduser", title="Invalid delegate(s)", message="These user-names are not valid: %s" % ", ".join(invalid_delegates)) else: delegates = [] cursor = db.cursor() if repository_id is None: cursor.execute("""SELECT id FROM repositories WHERE name=%s""", (repository_name,)) repository_id = cursor.fetchone()[0] if replaced_filter_id is not None: cursor.execute("""SELECT 1 FROM filters WHERE id=%s AND uid=%s""", (replaced_filter_id, user.id)) if not cursor.fetchone(): raise OperationFailure(code="invalidoperation", title="Invalid operation", message="Filter to replace does not exist or belongs to another user!") cursor.execute("""DELETE FROM filters WHERE id=%s""", (replaced_filter_id,)) cursor.execute("""SELECT 1 FROM filters WHERE uid=%s AND repository=%s AND path=%s""", (user.id, repository_id, path)) if cursor.fetchone(): raise OperationFailure(code="duplicatefilter", title="Duplicate filter", message=("You already have a filter for the path <code>%s</code> in this repository." % htmlutils.htmlify(path)), is_html=True) cursor.execute("""INSERT INTO filters (uid, repository, path, type, delegate) VALUES (%s, %s, %s, %s, %s) RETURNING id""", (user.id, repository_id, path, filter_type, ",".join(delegates))) filter_id = cursor.fetchone()[0] db.commit() return OperationResult(filter_id=filter_id)
def renderShowBatch(req, db, user): batch_id = page.utils.getParameter(req, "batch", None, filter=int) review_id = page.utils.getParameter(req, "review", None, filter=int) cursor = db.cursor() if batch_id is None and review_id is None: return page.utils.displayMessage(db, req, user, "Missing argument: 'batch'") if batch_id: cursor.execute("SELECT review, uid, comment FROM batches WHERE id=%s", (batch_id,)) row = cursor.fetchone() if not row: raise page.utils.DisplayMessage("Invalid batch ID: %d" % batch_id) review_id, author_id, chain_id = row author = dbutils.User.fromId(db, author_id) else: chain_id = None author = user review = dbutils.Review.fromId(db, review_id) if chain_id: batch_chain = review_comment.CommentChain.fromId(db, chain_id, user, review=review) batch_chain.loadComments(db, user) else: batch_chain = None document = htmlutils.Document(req) html = document.html() head = html.head() body = html.body() page.utils.generateHeader(body, db, user, lambda target: review_utils.renderDraftItems(db, user, review, target), extra_links=[("r/%d" % review.id, "Back to Review")]) document.addExternalStylesheet("resource/showreview.css") document.addExternalStylesheet("resource/showbatch.css") document.addExternalStylesheet("resource/review.css") document.addExternalStylesheet("resource/comment.css") document.addExternalScript("resource/review.js") document.addExternalScript("resource/comment.js") document.addInternalScript(user.getJS()) document.addInternalScript(review.getJS()) if batch_chain: document.addInternalScript("commentChainById[%d] = %s;" % (batch_chain.id, batch_chain.getJSConstructor())) target = body.div("main") table = target.table('paleyellow basic comments', align='center') table.col(width='10%') table.col(width='80%') table.col(width='10%') table.tr().td('h1', colspan=3).h1().text("Review by %s" % htmlify(author.fullname)) if batch_chain: batch_chain.loadComments(db, user) row = table.tr("line") row.td("heading").text("Comment:") row.td("value").preformatted().div("text").text(htmlify(batch_chain.comments[0].comment)) row.td("status").text() def renderFiles(title, cursor): files = [] for file_id, delete_count, insert_count in cursor.fetchall(): files.append((dbutils.describe_file(db, file_id), delete_count, insert_count)) paths = [] deleted = [] inserted = [] for path, delete_count, insert_count in sorted(files): paths.append(path) deleted.append(delete_count) inserted.append(insert_count) if paths: diff.File.eliminateCommonPrefixes(paths) row = table.tr("line") row.td("heading").text(title) files_table = row.td().table("files callout") headers = files_table.thead().tr() headers.th("path").text("Changed Files") headers.th("lines", colspan=2).text("Lines") files = files_table.tbody() for path, delete_count, insert_count in zip(paths, deleted, inserted): file = files.tr() file.td("path").preformatted().innerHTML(path) file.td("lines").preformatted().text("-%d" % delete_count if delete_count else None) file.td("lines").preformatted().text("+%d" % insert_count if insert_count else None) row.td("status").text() def condition(table_name): if batch_id: return "%s.batch=%d" % (table_name, batch_id) else: return "review=%d AND %s.batch IS NULL AND %s.uid=%d" % (review.id, table_name, table_name, author.id) cursor.execute("""SELECT reviewfiles.file, SUM(deleted), SUM(inserted) FROM reviewfiles JOIN reviewfilechanges ON (reviewfilechanges.file=reviewfiles.id) WHERE %s AND reviewfilechanges.to_state='reviewed' GROUP BY reviewfiles.file""" % condition("reviewfilechanges")) renderFiles("Reviewed:", cursor) cursor.execute("""SELECT reviewfiles.file, SUM(deleted), SUM(inserted) FROM reviewfiles JOIN reviewfilechanges ON (reviewfilechanges.file=reviewfiles.id) WHERE %s AND reviewfilechanges.to_state='pending' GROUP BY reviewfiles.file""" % condition("reviewfilechanges")) renderFiles("Unreviewed:", cursor) def renderChains(title, cursor, replies): all_chains = [review_comment.CommentChain.fromId(db, chain_id, user, review=review) for (chain_id,) in cursor] if not all_chains: return for chain in all_chains: chain.loadComments(db, user) issue_chains = filter(lambda chain: chain.type == "issue", all_chains) draft_issues = filter(lambda chain: chain.state == "draft", issue_chains) open_issues = filter(lambda chain: chain.state == "open", issue_chains) addressed_issues = filter(lambda chain: chain.state == "addressed", issue_chains) closed_issues = filter(lambda chain: chain.state == "closed", issue_chains) note_chains = filter(lambda chain: chain.type == "note", all_chains) draft_notes = filter(lambda chain: chain.state == "draft" and chain != batch_chain, note_chains) open_notes = filter(lambda chain: chain.state == "open" and chain != batch_chain, note_chains) def renderChains(target, chains): for chain in chains: row = target.tr("comment") row.td("author").text(chain.user.fullname) row.td("title").a(href="showcomment?chain=%d" % chain.id).innerHTML(chain.leader()) row.td("when").text(chain.when()) def showcomments(filter_param): params = { "review": review.id, "filter": filter_param } if batch_id: params["batch"] = batch_id return htmlutils.URL("/showcomments", **params) if draft_issues or open_issues or addressed_issues or closed_issues: h2 = table.tr().td("h2", colspan=3).h2().text(title) if len(draft_issues) + len(open_issues) + len(addressed_issues) + len(closed_issues) > 1: h2.a(href=showcomments("issues")).text("[display all]") if draft_issues: h3 = table.tr(id="draft-issues").td("h3", colspan=3).h3().text("Draft issues") if len(draft_issues) > 1: h3.a(href=showcomments("draft-issues")).text("[display all]") renderChains(table, draft_issues) if batch_id is not None or replies: if open_issues: h3 = table.tr(id="open-issues").td("h3", colspan=3).h3().text("Still open issues") if batch_id and len(open_issues) > 1: h3.a(href=showcomments("open-issues")).text("[display all]") renderChains(table, open_issues) if addressed_issues: h3 = table.tr(id="addressed-issues").td("h3", colspan=3).h3().text("Now addressed issues") if batch_id and len(addressed_issues) > 1: h3.a(href=showcomments("addressed-issues")).text("[display all]") renderChains(table, addressed_issues) if closed_issues: h3 = table.tr(id="closed-issues").td("h3", colspan=3).h3().text("Now closed issues") if batch_id and len(closed_issues) > 1: h3.a(href=showcomments("closed-issues")).text("[display all]") renderChains(table, closed_issues) if draft_notes or open_notes: h2 = table.tr().td("h2", colspan=3).h2().text(title) if len(draft_notes) + len(open_notes) > 1: h2.a(href=showcomments("notes")).text("[display all]") if draft_notes: h3 = table.tr(id="draft-notes").td("h3", colspan=3).h3().text("Draft notes") if len(draft_notes) > 1: h3.a(href=showcomments("draft-notes")).text("[display all]") renderChains(table, draft_notes) if open_notes: h3 = table.tr(id="notes").td("h3", colspan=3).h3().text("Notes") if batch_id and len(open_notes) > 1: h3.a(href=showcomments("open-notes")).text("[display all]") renderChains(table, open_notes) cursor.execute("SELECT id FROM commentchains WHERE %s AND type='issue'" % condition("commentchains")) renderChains("Raised issues", cursor, False) cursor.execute("""SELECT commentchains.id FROM commentchains JOIN commentchainchanges ON (commentchainchanges.chain=commentchains.id) WHERE %s AND to_state='closed'""" % condition("commentchainchanges")) renderChains("Resolved issues", cursor, False) cursor.execute("""SELECT commentchains.id FROM commentchains JOIN commentchainchanges ON (commentchainchanges.chain=commentchains.id) WHERE %s AND to_state='open'""" % condition("commentchainchanges")) renderChains("Reopened issues", cursor, False) cursor.execute("""SELECT commentchains.id FROM commentchains JOIN commentchainchanges ON (commentchainchanges.chain=commentchains.id) WHERE %s AND to_type='issue'""" % condition("commentchainchanges")) renderChains("Converted into issues", cursor, False) cursor.execute("""SELECT commentchains.id FROM commentchains JOIN commentchainchanges ON (commentchainchanges.chain=commentchains.id) WHERE %s AND to_type='note'""" % condition("commentchainchanges")) renderChains("Converted into notes", cursor, False) cursor.execute("SELECT id FROM commentchains WHERE %s AND type='note'" % condition("commentchains")) renderChains("Written notes", cursor, False) cursor.execute("""SELECT commentchains.id FROM commentchains JOIN comments ON (comments.chain=commentchains.id) WHERE %s AND comments.id!=commentchains.first_comment""" % condition("comments")) renderChains("Replied to", cursor, True) return document
def createReview(db, user, repository, commits, branch_name, summary, description, from_branch_name=None, via_push=False, reviewfilters=None, applyfilters=True, applyparentfilters=False, recipientfilters=None): cursor = db.cursor() if via_push: applyparentfilters = bool(user.getPreference(db, 'review.applyUpstreamFilters')) branch = dbutils.Branch.fromName(db, repository, branch_name) if branch is not None: raise OperationFailure( code="branchexists", title="Invalid review branch name", message="""\ <p>There is already a branch named <code>%s</code> in the repository. You have to select a different name.</p> <p>If you believe the existing branch was created during an earlier (failed) attempt to create this review, you can try to delete it from the repository using the command<p> <pre> git push <remote> :%s</pre> <p>and then press the "Submit Review" button on this page again.""" % (htmlutils.htmlify(branch_name), htmlutils.htmlify(branch_name)), is_html=True) if not commits: raise OperationFailure( code="nocommits", title="No commits specified", message="You need at least one commit to create a review.") commitset = log_commitset.CommitSet(commits) heads = commitset.getHeads() if len(heads) != 1: # There is really no plausible way for this error to occur. raise OperationFailure( code="disconnectedtree", title="Disconnected tree", message=("The specified commits do do not form a single connected " "tree. Creating a review of them is not supported.")) head = heads.pop() if len(commitset.getTails()) != 1: tail_id = None else: tail_id = gitutils.Commit.fromSHA1(db, repository, commitset.getTails().pop()).getId(db) if not via_push: try: repository.createBranch(branch_name, head.sha1) except gitutils.GitCommandError as error: raise OperationFailure( code="branchfailed", title="Failed to create review branch", message=("<p><b>Output from git:</b></p>" "<code style='padding-left: 1em'>%s</code>" % htmlutils.htmlify(error.output)), is_html=True) try: cursor.execute("INSERT INTO branches (repository, name, head, tail, type) VALUES (%s, %s, %s, %s, 'review') RETURNING id", [repository.id, branch_name, head.getId(db), tail_id]) branch_id = cursor.fetchone()[0] reachable_values = [(branch_id, commit.getId(db)) for commit in commits] cursor.executemany("INSERT INTO reachable (branch, commit) VALUES (%s, %s)", reachable_values) cursor.execute("INSERT INTO reviews (type, branch, state, summary, description, applyfilters, applyparentfilters) VALUES ('official', %s, 'open', %s, %s, %s, %s) RETURNING id", (branch_id, summary, description, applyfilters, applyparentfilters)) review = dbutils.Review.fromId(db, cursor.fetchone()[0]) cursor.execute("INSERT INTO reviewusers (review, uid, owner) VALUES (%s, %s, TRUE)", (review.id, user.id)) if reviewfilters is not None: cursor.executemany("""INSERT INTO reviewfilters (review, uid, path, type, creator) VALUES (%s, %s, %s, %s, %s)""", [(review.id, filter_user_id, filter_path, filter_type, user.id) for filter_user_id, filter_path, filter_type, filter_delegate in reviewfilters]) is_opt_in = False if recipientfilters is not None: cursor.executemany("INSERT INTO reviewrecipientfilters (review, uid, include) VALUES (%s, %s, %s)", [(review.id, filter_user_id, filter_include) for filter_user_id, filter_include in recipientfilters]) for filter_user_id, filter_include in recipientfilters: if filter_user_id is None and not filter_include: is_opt_in = True addCommitsToReview(db, user, review, commits, new_review=True) if from_branch_name is not None: cursor.execute("UPDATE branches SET review=%s WHERE repository=%s AND name=%s", (review.id, repository.id, from_branch_name)) # Reload to get list of changesets added by addCommitsToReview(). review = dbutils.Review.fromId(db, review.id) pending_mails = [] recipients = review.getRecipients(db) for to_user in recipients: pending_mails.extend(mail.sendReviewCreated(db, user, to_user, recipients, review)) if not is_opt_in: recipient_by_id = dict((to_user.id, to_user) for to_user in recipients) cursor.execute("""SELECT userpreferences.uid, userpreferences.repository, userpreferences.filter, userpreferences.integer FROM userpreferences LEFT OUTER JOIN filters ON (filters.id=userpreferences.filter) WHERE userpreferences.item='review.defaultOptOut' AND userpreferences.uid=ANY (%s) AND (userpreferences.filter IS NULL OR filters.repository=%s) AND (userpreferences.repository IS NULL OR userpreferences.repository=%s)""", (recipient_by_id.keys(), repository.id, repository.id)) user_settings = {} has_filter_settings = False for user_id, repository_id, filter_id, integer in cursor: settings = user_settings.setdefault(user_id, [None, None, {}]) value = bool(integer) if repository_id is None and filter_id is None: settings[0] = value elif repository_id is not None: settings[1] = value else: settings[2][filter_id] = value has_filter_settings = True if has_filter_settings: filters = Filters() filters.setFiles(db, review=review) for user_id, (global_default, repository_default, filter_settings) in user_settings.items(): to_user = recipient_by_id[user_id] opt_out = None if repository_default is not None: opt_out = repository_default elif global_default is not None: opt_out = global_default if filter_settings: # Policy: # # If all of the user's filters that matched files in the # review have review.defaultOptOut enabled, then opt out. # When determining this, any review filters of the user's # that match files in the review count as filters that don't # have the review.defaultOptOut enabled. # # If any of the user's filters that matched files in the # review have review.defaultOptOut disabled, then don't opt # out. When determining this, review filters are ignored. # # Otherwise, ignore the filter settings, and go with either # the user's per-repository or global setting (as set # above.) filters.load(db, review=review, user=to_user) # A set of filter ids. If None is in the set, the user has # one or more review filters in the review. (These do not # have ids.) active_filters = filters.getActiveFilters(to_user) for filter_id in active_filters: if filter_id is None: continue elif filter_id in filter_settings: if not filter_settings[filter_id]: opt_out = False break else: break else: if None not in active_filters: opt_out = True if opt_out: cursor.execute("""INSERT INTO reviewrecipientfilters (review, uid, include) VALUES (%s, %s, FALSE)""", (review.id, to_user.id)) db.commit() mail.sendPendingMails(pending_mails) return review except: if not via_push: repository.run("branch", "-D", branch_name) raise
def renderShowFile(req, db, user): cursor = db.cursor() sha1 = req.getParameter("sha1") path = req.getParameter("path") line = req.getParameter("line", None) review_id = req.getParameter("review", None, filter=int) default_tabify = "yes" if user.getPreference(db, "commit.diff.visualTabs") else "no" tabify = req.getParameter("tabify", default_tabify) == "yes" if line is None: first, last = None, None else: if "-" in line: first, last = map(int, line.split("-")) else: first = last = int(line) context = req.getParameter("context", user.getPreference(db, "commit.diff.contextLines"), int) first_with_context = max(1, first - context) last_with_context = last + context if user.getPreference(db, "commit.diff.compactMode"): default_compact = "yes" else: default_compact = "no" compact = req.getParameter("compact", default_compact) == "yes" if len(path) == 0 or path[-1:] == "/": raise page.utils.DisplayMessage( title="Invalid path parameter", body="<p>The path must be non-empty and must not end with a <code>/</code>.</p>", html=True) if path[0] == '/': full_path = path if path != "/": path = path[1:] else: full_path = "/" + path if not path: path = "/" if review_id is None: review = None repository_arg = req.getParameter("repository", "") if repository_arg: repository = gitutils.Repository.fromParameter(db, repository_arg) else: repository = gitutils.Repository.fromSHA1(db, sha1) else: review = dbutils.Review.fromId(db, review_id) repository = review.repository document = htmlutils.Document(req) html = document.html() head = html.head() body = html.body() if review: page.utils.generateHeader(body, db, user, lambda target: review_utils.renderDraftItems(db, user, review, target), extra_links=[("r/%d" % review.id, "Back to Review")]) else: page.utils.generateHeader(body, db, user) document.addExternalStylesheet("resource/showfile.css") document.addInternalStylesheet(htmlutils.stripStylesheet(user.getResource(db, "syntax.css")[1], compact)) commit = gitutils.Commit.fromSHA1(db, repository, sha1) file_sha1 = commit.getFileSHA1(full_path) file_id = dbutils.find_file(db, path=path) if file_sha1 is None: raise page.utils.DisplayMessage( title="File does not exist", body=("<p>There is no file named <code>%s</code> in the commit " "<a href='/showcommit?repository=%s&sha1=%s'>" "<code>%s</code></a>.</p>" % (htmlutils.htmlify(textutils.escape(full_path)), htmlutils.htmlify(repository.name), htmlutils.htmlify(sha1), htmlutils.htmlify(sha1[:8]))), html=True) file = diff.File(file_id, path, None, file_sha1, repository) # A new file ID might have been added to the database, so need to commit. db.commit() if file.canHighlight(): requestHighlights(repository, { file.new_sha1: (file.path, file.getLanguage()) }) file.loadNewLines(True, request_highlight=True) if review: document.addInternalScript(user.getJS()) document.addInternalScript(review.getJS()) document.addInternalScript("var changeset = { parent: { id: %(id)d, sha1: %(sha1)r }, child: { id: %(id)d, sha1: %(sha1)r } };" % { 'id': commit.getId(db), 'sha1': commit.sha1 }) document.addInternalScript("var files = { %(id)d: { new_sha1: %(sha1)r }, %(sha1)r: { id: %(id)d, side: 'n' } };" % { 'id': file_id, 'sha1': file_sha1 }) document.addExternalStylesheet("resource/review.css") document.addExternalScript("resource/review.js") cursor.execute("""SELECT DISTINCT commentchains.id FROM commentchains JOIN commentchainlines ON (commentchainlines.chain=commentchains.id) WHERE commentchains.review=%s AND commentchains.file=%s AND commentchainlines.sha1=%s AND ((commentchains.state!='draft' OR commentchains.uid=%s) AND commentchains.state!='empty') GROUP BY commentchains.id""", (review.id, file_id, file_sha1, user.id)) comment_chain_script = "" for (chain_id,) in cursor.fetchall(): chain = review_comment.CommentChain.fromId(db, chain_id, user, review=review) chain.loadComments(db, user) comment_chain_script += "commentChains.push(%s);\n" % chain.getJSConstructor(file_sha1) if comment_chain_script: document.addInternalScript(comment_chain_script) document.addExternalStylesheet("resource/comment.css") document.addExternalScript("resource/comment.js") document.addExternalScript("resource/showfile.js") if tabify: document.addExternalStylesheet("resource/tabify.css") document.addExternalScript("resource/tabify.js") tabwidth = file.getTabWidth() indenttabsmode = file.getIndentTabsMode() if user.getPreference(db, "commit.diff.highlightIllegalWhitespace"): document.addInternalStylesheet(user.getResource(db, "whitespace.css")[1], compact) if first is not None: document.addInternalScript("var firstSelectedLine = %d, lastSelectedLine = %d;" % (first, last)) target = body.div("main") if tabify: target.script(type="text/javascript").text("calculateTabWidth();") table = target.table('file show expanded paleyellow', align='center', cellspacing=0) columns = table.colgroup() columns.col('edge') columns.col('linenr') columns.col('line') columns.col('middle') columns.col('middle') columns.col('line') columns.col('linenr') columns.col('edge') thead = table.thead() cell = thead.tr().td('h1', colspan=8) h1 = cell.h1() def make_url(url_path, path): params = { "sha1": sha1, "path": path } if review is None: params["repository"] = str(repository.id) else: params["review"] = str(review.id) return "%s?%s" % (url_path, urllib.urlencode(params)) h1.a("root", href=make_url("showtree", "/")).text("root") h1.span().text('/') components = path.split("/") for index, component in enumerate(components[:-1]): h1.a(href=make_url("showtree", "/".join(components[:index + 1]))).text(component, escape=True) h1.span().text('/') if first is not None: h1.a(href=make_url("showfile", "/".join(components))).text(components[-1], escape=True) else: h1.text(components[-1], escape=True) h1.span("right").a(href=("/download/%s?repository=%s&sha1=%s" % (urllib.quote(path), repository.name, file_sha1)), download=urllib.quote(path)).text("[download]") h1.span("right").a(href=("/download/%s?repository=%s&sha1=%s" % (urllib.quote(path), repository.name, file_sha1))).text("[view]") table.tbody('spacer top').tr('spacer top').td(colspan=8).text() tbody = table.tbody("lines") yield document.render(stop=tbody, pretty=not compact) for linenr, line in enumerate(file.newLines(True)): linenr = linenr + 1 highlight_class = "" if first is not None: if not (first_with_context <= linenr <= last_with_context): continue if linenr == first: highlight_class += " first-selected" if linenr == last: highlight_class += " last-selected" if tabify: line = htmlutils.tabify(line, tabwidth, indenttabsmode) line = line.replace("\r", "<i class='cr'></i>") row = tbody.tr("line context single", id="f%do%dn%d" % (file.id, linenr, linenr)) row.td("edge").text() row.td("linenr old").text(linenr) row.td("line single whole%s" % highlight_class, id="f%dn%d" % (file.id, linenr), colspan=4).innerHTML(line) row.td("linenr new").text(linenr) row.td("edge").text() if linenr % 500: yield document.render(stop=tbody, pretty=not compact) table.tbody('spacer bottom').tr('spacer bottom').td(colspan=8).text() yield document.render(pretty=not compact)
def createReview(db, user, repository, commits, branch_name, summary, description, from_branch_name=None, via_push=False, reviewfilters=None, applyfilters=True, applyparentfilters=False, recipientfilters=None): cursor = db.cursor() if via_push: applyparentfilters = bool(user.getPreference(db, 'review.applyUpstreamFilters')) branch = dbutils.Branch.fromName(db, repository, branch_name) if branch is not None: raise OperationFailure(code="branchexists", title="Invalid review branch name", message="""\ <p>There is already a branch named <code>%s</code> in the repository. You have to select a different name.</p> <p>If you believe the existing branch was created during an earlier (failed) attempt to create this review, you can try to delete it from the repository using the command<p> <pre> git push <remote> :%s</pre> <p>and then press the "Submit Review" button on this page again.""" % (htmlutils.htmlify(branch_name), htmlutils.htmlify(branch_name)), is_html=True) commitset = log_commitset.CommitSet(commits) if len(commitset.getHeads()) != 1: raise Exception, "invalid commit-set; multiple heads" head = commitset.getHeads().pop() if len(commitset.getTails()) != 1: tail_id = None else: tail_id = gitutils.Commit.fromSHA1(db, repository, commitset.getTails().pop()).getId(db) if not via_push: repository.branch(branch_name, head.sha1) try: cursor.execute("INSERT INTO branches (repository, name, head, tail, type) VALUES (%s, %s, %s, %s, 'review') RETURNING id", [repository.id, branch_name, head.getId(db), tail_id]) branch_id = cursor.fetchone()[0] reachable_values = [(branch_id, commit.getId(db)) for commit in commits] cursor.executemany("INSERT INTO reachable (branch, commit) VALUES (%s, %s)", reachable_values) cursor.execute("INSERT INTO reviews (type, branch, state, summary, description, applyfilters, applyparentfilters) VALUES ('official', %s, 'open', %s, %s, %s, %s) RETURNING id", (branch_id, summary, description, applyfilters, applyparentfilters)) review = dbutils.Review.fromId(db, cursor.fetchone()[0]) cursor.execute("INSERT INTO reviewusers (review, uid, owner) VALUES (%s, %s, TRUE)", (review.id, user.id)) if reviewfilters is not None: cursor.executemany("INSERT INTO reviewfilters (review, uid, directory, file, type, creator) VALUES (%s, %s, %s, %s, %s, %s)", [(review.id, filter_user_id, filter_directory_id, filter_file_id, filter_type, user.id) for filter_directory_id, filter_file_id, filter_type, filter_delegate, filter_user_id in reviewfilters]) if recipientfilters is not None: cursor.executemany("INSERT INTO reviewrecipientfilters (review, uid, include) VALUES (%s, %s, %s)", [(review.id, filter_user_id, filter_include) for filter_user_id, filter_include in recipientfilters]) addCommitsToReview(db, user, review, commits, new_review=True) if from_branch_name is not None: cursor.execute("UPDATE branches SET review=%s WHERE repository=%s AND name=%s", (review.id, repository.id, from_branch_name)) # Reload to get list of changesets added by addCommitsToReview(). review = dbutils.Review.fromId(db, review.id) pending_mails = [] recipients = review.getRecipients(db) for to_user in recipients: pending_mails.extend(mail.sendReviewCreated(db, user, to_user, recipients, review)) db.commit() mail.sendPendingMails(pending_mails) return review except: if not via_push: repository.run("branch", "-D", branch_name) raise
def wrap(raw_source, mode): if mode == "json": return "\n".join( textutils.json_encode([[None, line]]) for line in diff.parse.splitlines(raw_source)) return htmlutils.htmlify(raw_source)
req.setContentType("text/html") req.start() yield str(document) return except: error_message = traceback.format_exc() environ["wsgi.errors"].write(error_message) db.rollback() if not user or user.hasRole(db, "developer"): title = "You broke the system again:" body = error_message body_html = "<pre>%s</pre>" % htmlify(body) else: prefix = dbutils.getURLPrefix(db) x_forwarded_host = req.getRequestHeader("X-Forwarded-Host") if x_forwarded_host: prefix = "https://" + x_forwarded_host url = "%s/%s?%s" % (prefix, req.path, req.query) mailutils.sendExceptionMessage("wsgi", ( "User: %s\nMethod: %s\nPath: %s\nQuery: %s\nURL: %s\n\n%s" % (req.user, req.method, req.path, req.query, url, error_message))) title = "Darn! It seems we have a problem..." body = "A message has been sent to the system administrator(s) with details about the problem."
def renderCreateReview(req, db, user): if user.isAnonymous(): raise page.utils.NeedLogin(req) repository = req.getParameter("repository", filter=gitutils.Repository.FromParameter(db), default=None) applyparentfilters = req.getParameter("applyparentfilters", "yes" if user.getPreference(db, 'review.applyUpstreamFilters') else "no") == "yes" cursor = db.cursor() if req.method == "POST": data = json_decode(req.read()) summary = data.get("summary") description = data.get("description") review_branch_name = data.get("review_branch_name") commit_ids = data.get("commit_ids") commit_sha1s = data.get("commit_sha1s") else: summary = req.getParameter("summary", None) description = req.getParameter("description", None) review_branch_name = req.getParameter("reviewbranchname", None) commit_ids = None commit_sha1s = None commits_arg = req.getParameter("commits", None) remote = req.getParameter("remote", None) upstream = req.getParameter("upstream", "master") branch_name = req.getParameter("branch", None) if commits_arg: try: commit_ids = map(int, commits_arg.split(",")) except: commit_sha1s = [repository.revparse(ref) for ref in commits_arg.split(",")] elif branch_name: cursor.execute("""SELECT commit FROM reachable JOIN branches ON (branch=id) WHERE repository=%s AND name=%s""", (repository.id, branch_name)) commit_ids = [commit_id for (commit_id,) in cursor] if len(commit_ids) > configuration.limits.MAXIMUM_REVIEW_COMMITS: raise page.utils.DisplayMessage( "Too many commits!", (("<p>The branch <code>%s</code> contains %d commits. Reviews can" "be created from branches that contain at most %d commits.</p>" "<p>This limit can be adjusted by modifying the system setting" "<code>configuration.limits.MAXIMUM_REVIEW_COMMITS</code>.</p>") % (htmlutils.htmlify(branch_name), len(commit_ids), configuration.limits.MAXIMUM_REVIEW_COMMITS)), html=True) else: return renderSelectSource(req, db, user) req.content_type = "text/html; charset=utf-8" if commit_ids: commits = [gitutils.Commit.fromId(db, repository, commit_id) for commit_id in commit_ids] elif commit_sha1s: commits = [gitutils.Commit.fromSHA1(db, repository, commit_sha1) for commit_sha1 in commit_sha1s] else: commits = [] if not commit_ids: commit_ids = [commit.getId(db) for commit in commits] if not commit_sha1s: commit_sha1s = [commit.sha1 for commit in commits] if summary is None: if len(commits) == 1: summary = commits[0].summary() else: summary = "" if review_branch_name: invalid_branch_name = "false" default_branch_name = review_branch_name else: invalid_branch_name = htmlutils.jsify(user.name + "/") default_branch_name = user.name + "/" match = re.search("(?:^|[Ff]ix(?:e[ds])?(?: +for)?(?: +bug)? +)([A-Z][A-Z0-9]+-[0-9]+)", summary) if match: invalid_branch_name = "false" default_branch_name = htmlutils.htmlify(match.group(1)) changesets = [] changeset_utils.createChangesets(db, repository, commits) for commit in commits: changesets.extend(changeset_utils.createChangeset(db, None, repository, commit, do_highlight=False)) changeset_ids = [changeset.id for changeset in changesets] all_reviewers, all_watchers = reviewing.utils.getReviewersAndWatchers( db, repository, changesets=changesets, applyparentfilters=applyparentfilters) document = htmlutils.Document(req) html = document.html() head = html.head() document.addInternalScript(user.getJS(db)) if branch_name: document.addInternalScript("var fromBranch = %s;" % htmlutils.jsify(branch_name)) if remote: document.addInternalScript("var trackedbranch = { remote: %s, name: %s };" % (htmlutils.jsify(remote), htmlutils.jsify(branch_name))) head.title().text("Create Review") body = html.body(onload="document.getElementById('branch_name').focus()") page.utils.generateHeader(body, db, user, lambda target: target.button(onclick="submitReview();").text("Submit Review")) document.addExternalStylesheet("resource/createreview.css") document.addExternalScript("resource/createreview.js") document.addExternalScript("resource/reviewfilters.js") document.addExternalScript("resource/autocomplete.js") document.addInternalScript(""" var invalid_branch_name = %s; var review = { commit_ids: %r, commit_sha1s: %r, changeset_ids: %r };""" % (invalid_branch_name, commit_ids, commit_sha1s, changeset_ids)) document.addInternalScript(repository.getJS()) main = body.div("main") table = main.table("basic paleyellow", align="center") table.tr().td("h1", colspan=3).h1().text("Create Review") row = table.tr("line") row.td("heading").text("Branch Name:") row.td("value").text("r/").input("value", id="branch_name", value=default_branch_name) row.td("status") row = table.tr() if not remote: row.td("help", colspan=3).div().text("""\ This is the main identifier of the review. It will be created in the review repository to contain the commits below. Reviewers can fetch it from there, and additional commits can be added to the review later by pushing them to this branch in the review repository.""") else: row.td("help", colspan=3).div().text("""\ This is the main identifier of the review. It will be created in the review repository to contain the commits below, and reviewers can fetch it from there.""") if remote: row = table.tr("line") row.td("heading").text("Tracked Branch:") value = row.td("value") value.code("branch inset").text(branch_name, linkify=linkify.Context(remote=remote)) value.text(" in ") value.code("remote inset").text(remote, linkify=linkify.Context()) row.td("status") row = table.tr() row.td("help", colspan=3).div().text("""\ Rather than pushing directly to the review branch in Critic's repository to add commits to the review, you will be pushing to this branch (in a separate repository,) from which Critic will fetch commits and add them to the review automatically.""") row = table.tr("line") row.td("heading").text("Summary:") row.td("value").input("value", id="summary", value=summary) row.td("status") row = table.tr() row.td("help", colspan=3).div().text("""\ The summary should be a short summary of the changes in the review. It will appear in the subject of all emails sent about the review. """) row = table.tr("line description") row.td("heading").text("Description:") textarea = row.td("value").textarea(id="description", rows=12) textarea.preformatted() if description: textarea.text(description) row.td("status") row = table.tr() row.td("help", colspan=3).div().text("""\ The description should describe the changes to be reviewed. It is usually fine to leave the description empty, since the commit messages are also available in the review. """) generateReviewersAndWatchersTable(db, repository, main, all_reviewers, all_watchers, applyparentfilters=applyparentfilters) row = table.tr("line recipients") row.td("heading").text("Recipient List:") cell = row.td("value", colspan=2).preformatted() cell.span("mode").text("Everyone") cell.span("users") cell.text(".") buttons = cell.div("buttons") buttons.button(onclick="editRecipientList();").text("Edit Recipient List") row = table.tr() row.td("help", colspan=3).div().text("""\ The basic recipient list for e-mails sent about the review. """) log.html.render(db, main, "Commits", commits=commits) return document
def basic(): import htmlutils from operation.basictypes import OperationError, OperationFailure from operation.typechecker import ( Optional, TypeChecker, TypeCheckerContext, BooleanChecker, StringChecker, RestrictedString, SHA1, IntegerChecker, RestrictedInteger, PositiveInteger, NonNegativeInteger, ArrayChecker, EnumerationChecker, VariantChecker, DictionaryChecker) # Check TypeChecker.make()'s handling of basic types. assert type(TypeChecker.make(bool)) is BooleanChecker assert type(TypeChecker.make(str)) is StringChecker assert type(TypeChecker.make(int)) is IntegerChecker assert type(TypeChecker.make([bool])) is ArrayChecker assert type(TypeChecker.make(set(["foo", "bar"]))) is EnumerationChecker assert type(TypeChecker.make(set([bool, str, int]))) is VariantChecker assert type(TypeChecker.make({ "foo": bool })) is DictionaryChecker # Check TypeChecker.make()'s handling of TypeChecker sub-classes and # instances thereof. assert isinstance(TypeChecker.make(BooleanChecker), BooleanChecker) boolean_checker = BooleanChecker() assert TypeChecker.make(boolean_checker) is boolean_checker def check(checker, *values): checker = TypeChecker.make(checker) results = [] for value in values: converted = checker(value, TypeCheckerContext(None, None, None)) results.append(value if converted is None else converted) return results def should_match(checker, *values, **kwargs): results = check(checker, *values) if "result" in kwargs: expected_result = kwargs["result"] for result in results: assert result == expected_result, \ "%r != %r" % (result, expected_result) def should_not_match(checker, *values, **expected): for value in values: try: check(checker, copy.deepcopy(value)) except (OperationError, OperationFailure) as error: error = json.loads(str(error)) for key, value in expected.items(): if isinstance(value, str): value = set([value]) assert error.get(key) in value, \ ("%s: %r not among %r" % (key, error.get(key), value)) else: assert False, "checker allowed value incorrectly: %r" % value # Check some simple things that should be accepted. should_match(bool, True, False) should_match(str, "", "foo") should_match(int, -2**31, -1, 0, 1, 2**31) should_match([bool], [], [True, False]) should_match([str], ["", "foo"]) should_match([int], [-2**31, -1, 0, 1, 2**31]) should_match(set(["foo", "bar"]), "foo", "bar") should_match(set([bool, str, int]), True, False, "", "foo", -2**31, -1, 0, 1, 2**31) # Check some equally simple things that shouldn't be accepted. should_not_match(bool, 10, "foo", error="invalid input: data is not a boolean") should_not_match(str, True, 10, error="invalid input: data is not a string") should_not_match(int, True, "foo", 0.5, error="invalid input: data is not an integer") should_not_match([bool], [True, 10], [False, "foo"], error="invalid input: data[1] is not a boolean") should_not_match([str], ["", True], ["foo", 10], error="invalid input: data[1] is not a string") should_not_match([int], [0, True], [10, "foo"], error="invalid input: data[1] is not an integer") should_not_match(set(["foo", "bar"]), "fie", error="invalid input: data is not valid") should_not_match(set(["foo", "bar"]), True, 10, error="invalid input: data is not a string") should_not_match(set([bool, str, int]), [True], ["foo"], [10], error="data is of invalid type") # Check some dictionary checkers. should_match({ "b": bool, "s": str, "i": int }, { "b": True, "s": "foo", "i": 10 }) should_match({ "req": bool, "opt": Optional(bool) }, { "req": True, "opt": False }, { "req": False }) should_not_match({ "b": bool }, { "b": "foo" }, { "b": 10 }, error="invalid input: data.b is not a boolean") should_not_match({ "b": bool }, { "i": 10 }, error="invalid input: data.b missing") should_not_match({ "b": bool }, { "b": True, "i": 10 }, error="invalid input: data.i was not used") should_not_match({ "b": Optional(bool) }, { "b": "foo" }, { "b": 10 }, error="invalid input: data.b is not a boolean") # Check suffixed variant checker in dictionary. id_or_name = VariantChecker({ "id": int, "name": str }) should_match({ "thing": id_or_name }, { "thing": 10 }, { "thing_id": 10 }, result={ "thing": 10 }) should_match({ "thing": id_or_name }, { "thing": "foo" }, { "thing_name": "foo" }, result={ "thing": "foo" }) should_not_match({ "thing": id_or_name }, { "thing_id": "foo" }, error="invalid input: data.thing_id is not an integer") should_not_match({ "thing": id_or_name }, { "thing_name": 10 }, error="invalid input: data.thing_name is not a string") should_not_match({ "thing": id_or_name }, { "thing_id": 10, "thing_name": "foo" }, error=("invalid input: data.thing_id was not used", "invalid input: data.thing_name was not used")) # Check some RestrictedString types. should_match(RestrictedString, "", "foo") should_match(RestrictedString(minlength=0), "", "foo") should_match(RestrictedString(minlength=3), "foo") should_match(RestrictedString(maxlength=0), "") should_match(RestrictedString(maxlength=3), "", "foo") should_match(RestrictedString(minlength=0, maxlength=3), "", "foo") should_match(RestrictedString(allowed=lambda c: False), "") should_match(RestrictedString(allowed=lambda c: True), "", "foo") should_match(RestrictedString(allowed=lambda c: c in "foo"), "", "foo") should_not_match(RestrictedString(), True, 10, error="invalid input: data is not a string") should_not_match( RestrictedString(minlength=1), "", code="paramtooshort:data", title="Invalid data", message="invalid input: data must be at least 1 characters long") should_not_match( RestrictedString(maxlength=2), "foo", code="paramtoolong:data", title="Invalid data", message="invalid input: data must be at most 2 characters long") should_not_match( RestrictedString(allowed=lambda c: False), "foo", code="paramcontainsillegalchar:data", title="Invalid data", message="invalid input: data may not contain the characters 'f', 'o'") should_not_match( RestrictedString(allowed=lambda c: False, ui_name="gazonk"), "foo", code="paramcontainsillegalchar:data", title="Invalid gazonk", message="invalid input: gazonk may not contain the characters 'f', 'o'") # Check SHA1. sha1 = "0123456789abcdefABCDEF0123456789abcdefAB" should_match(SHA1, *[sha1[:length] for length in range(4, 41)]) should_not_match(SHA1, True, 10, error="invalid input: data is not a string") for ch in range(0, 256): ch = chr(ch) if ch in sha1: continue should_not_match( SHA1, "012" + ch, message=htmlutils.htmlify( "invalid input: data may not contain the character %r" % ch)) should_not_match( SHA1, "012", message="invalid input: data must be at least 4 characters long") should_not_match( SHA1, "0" * 41, message="invalid input: data must be at most 40 characters long") # Check some RestrictedInteger types. should_match(RestrictedInteger, -2**31, -1, 0, 1, 2**31) should_match(RestrictedInteger(minvalue=-2**31), -2**31, -1, 0, 1, 2**31) should_match(RestrictedInteger(minvalue=0), 0, 1, 2**31) should_match(RestrictedInteger(maxvalue=0), -2**31, -1, 0) should_match(RestrictedInteger(maxvalue=2**31), -2**31, -1, 0, 1, 2**31) should_match(RestrictedInteger(minvalue=0, maxvalue=0), 0) should_not_match(RestrictedInteger(), True, "foo", error="invalid input: data is not an integer") should_not_match(RestrictedInteger(minvalue=0), -2**31, -1, code="valuetoolow:data", title="Invalid data parameter", message="invalid input: data must be 0 or higher") should_not_match(RestrictedInteger(maxvalue=0), 1, 2**31, code="valuetoohigh:data", title="Invalid data parameter", message="invalid input: data must be 0 or lower") should_not_match(RestrictedInteger(minvalue=1, ui_name="gazonk"), 0, code="valuetoolow:data", title="Invalid gazonk parameter", message="invalid input: gazonk must be 1 or higher") # Check NonNegativeInteger. should_match(NonNegativeInteger, 0, 1, 2**31) should_not_match(NonNegativeInteger, True, "foo", error="invalid input: data is not an integer") should_not_match(NonNegativeInteger, -2**31, -1, code="valuetoolow:data", title="Invalid data parameter", message="invalid input: data must be 0 or higher") # Check PositiveInteger. should_match(PositiveInteger, 1, 2**31) should_not_match(PositiveInteger, True, "foo", error="invalid input: data is not an integer") should_not_match(PositiveInteger, -2**31, -1, 0, code="valuetoolow:data", title="Invalid data parameter", message="invalid input: data must be 1 or higher")
def renderManageReviewers(req, db, user): review_id = req.getParameter("review", filter=int) cursor = db.cursor() review = dbutils.Review.fromId(db, review_id) root_directories = {} root_files = {} def processFile(file_id): components = dbutils.describe_file(db, file_id).split("/") directories, files = root_directories, root_files for directory_name in components[:-1]: directories, files = directories.setdefault( directory_name, ({}, {})) files[components[-1]] = file_id cursor.execute("SELECT file FROM reviewfiles WHERE review=%s", (review.id, )) for (file_id, ) in cursor: processFile(file_id) cursor.execute("SELECT name FROM users WHERE name IS NOT NULL") users = [user_name for (user_name, ) in cursor if user_name] document = htmlutils.Document(req) html = document.html() head = html.head() body = html.body() page.utils.generateHeader( body, db, user, lambda target: review_utils.renderDraftItems(db, user, review, target), extra_links=[("r/%d" % review.id, "Back to Review")]) document.addExternalStylesheet("resource/managereviewers.css") document.addExternalScript("resource/managereviewers.js") document.addInternalScript(user.getJS()) document.addInternalScript(review.getJS()) document.addInternalScript( "var users = [ %s ];" % ", ".join([htmlutils.jsify(user_name) for user_name in sorted(users)])) target = body.div("main") basic = target.table('manage paleyellow', align='center') basic.col(width='10%') basic.col(width='60%') basic.col(width='30%') basic.tr().td('h1', colspan=3).h1().text("Manage Reviewers") row = basic.tr("current") row.td("select").text("Current:") cell = row.td("value") for index, reviewer in enumerate(review.reviewers): if index != 0: cell.text(", ") cell.span("reviewer", critic_username=reviewer.name).innerHTML( htmlutils.htmlify(reviewer.fullname).replace(" ", " ")) row.td("right").text() row = basic.tr("reviewer") row.td("select").text("Reviewer:") row.td("value").input("reviewer").span("message") row.td("right").button("save").text("Save") row = basic.tr("help") row.td("help", colspan=3).text( "Enter the name of a current reviewer to edit assignments (or unassign.) Enter the name of another user to add a new reviewer." ) row = basic.tr("headings") row.td("select").text("Assigned") row.td("path").text("Path") row.td().text() def outputDirectory(base, name, directories, files): if name: level = base.count("/") row = basic.tr("directory", critic_level=level) row.td("select").input(type="checkbox") if level > 1: row.td("path").preformatted().innerHTML((" " * (len(base) - 2)) + "…/" + name + "/") else: row.td("path").preformatted().innerHTML(base + name + "/") row.td().text() else: level = 0 for directory_name in sorted(directories.keys()): outputDirectory(base + name + "/" if name else "", directory_name, directories[directory_name][0], directories[directory_name][1]) for file_name in sorted(files.keys()): row = basic.tr("file", critic_file_id=files[file_name], critic_level=level + 1) row.td("select").input(type="checkbox") row.td("path").preformatted().innerHTML( (" " * (len(base + name) - 1)) + "…/" + htmlutils.htmlify(file_name)) row.td().text() outputDirectory("", "", root_directories, root_files) return document
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 renderShowTree(req, db, user): cursor = db.cursor() sha1 = req.getParameter("sha1") path = req.getParameter("path", "/") review_id = req.getParameter("review", None, filter=int) if path[0] == '/': full_path = path if path != "/": path = path[1:] else: full_path = "/" + path if not path: path = "/" if review_id is None: review = None repository_arg = req.getParameter("repository", "") if repository_arg: repository = gitutils.Repository.fromParameter(db, repository_arg) else: repository = gitutils.Repository.fromSHA1(db, sha1) else: review = dbutils.Review.fromId(db, review_id) repository = review.repository document = htmlutils.Document(req) html = document.html() head = html.head() body = html.body() extra_links = [] if review: extra_links.append(("r/%d" % review.id, "Back to Review")) page.utils.generateHeader(body, db, user, extra_links=extra_links) document.addExternalStylesheet("resource/showtree.css") if review: document.addInternalScript(review.getJS()) target = body.div("main") table = target.table("tree paleyellow", align="center", cellspacing=0) table.col(width="10%") table.col(width="60%") table.col(width="20%") thead = table.thead() h1 = thead.tr().td('h1', colspan=3).h1() def make_url(url_path, path): params = { "sha1": sha1, "path": path } if review is None: params["repository"] = str(repository.id) else: params["review"] = str(review.id) return "%s?%s" % (url_path, urllib.urlencode(params)) if path == "/": h1.text("/") else: h1.a("root", href=make_url("showtree", "/")).text("root") h1.span().text('/') components = path.split("/") for index, component in enumerate(components[:-1]): h1.a(href=make_url("showtree", "/".join(components[:index + 1]))).text(component, escape=True) h1.span().text('/') h1.text(components[-1], escape=True) row = thead.tr() row.td('mode').text("Mode") row.td('name').text("Name") row.td('size').text("Size") tree = gitutils.Tree.fromPath(gitutils.Commit.fromSHA1(db, repository, sha1), full_path) if tree is None: raise page.utils.DisplayMessage( title="Directory does not exist", body=("<p>There is no directory named <code>%s</code> in the commit " "<a href='/showcommit?repository=%s&sha1=%s'>" "<code>%s</code></a>.</p>" % (htmlutils.htmlify(textutils.escape(full_path)), htmlutils.htmlify(repository.name), htmlutils.htmlify(sha1), htmlutils.htmlify(sha1[:8]))), html=True) def compareEntries(a, b): if a.type != b.type: if a.type == "tree": return -1 else: return 1 else: return cmp(a.name, b.name) tbody = table.tbody() for entry in sorted(tree, cmp=compareEntries): if entry.type in ("blob", "tree"): if entry.type == "blob": url_path = "showfile" else: url_path = "showtree" url = make_url(url_path, os.path.join(path, entry.name)) else: url = None row = tbody.tr(entry.type) row.td('mode').text(str(entry.mode)) if stat.S_ISLNK(entry.mode): cell = row.td('link', colspan=2) cell.span('name').text(entry.name, escape=True) cell.text(' -> ') cell.span('target').text(repository.fetch(entry.sha1).data) elif entry.type == "commit": row.td('name').text("%s (%s)" % (entry.name, entry.sha1), escape=True) row.td('size').text(entry.size) else: row.td('name').a(href=url).text(entry.name, escape=True) row.td('size').text(entry.size) return document
def renderShowBatch(req, db, user): batch_id = page.utils.getParameter(req, "batch", None, filter=int) review_id = page.utils.getParameter(req, "review", None, filter=int) cursor = db.cursor() if batch_id is None and review_id is None: return page.utils.displayMessage(db, req, user, "Missing argument: 'batch'") if batch_id: cursor.execute("SELECT review, uid, comment FROM batches WHERE id=%s", (batch_id, )) row = cursor.fetchone() if not row: raise page.utils.DisplayMessage("Invalid batch ID: %d" % batch_id) review_id, author_id, chain_id = row author = dbutils.User.fromId(db, author_id) else: chain_id = None author = user review = dbutils.Review.fromId(db, review_id) if chain_id: batch_chain = review_comment.CommentChain.fromId(db, chain_id, user, review=review) batch_chain.loadComments(db, user) else: batch_chain = None document = htmlutils.Document(req) html = document.html() head = html.head() body = html.body() page.utils.generateHeader( body, db, user, lambda target: review_utils.renderDraftItems(db, user, review, target), extra_links=[("r/%d" % review.id, "Back to Review")]) document.addExternalStylesheet("resource/showreview.css") document.addExternalStylesheet("resource/showbatch.css") document.addExternalStylesheet("resource/review.css") document.addExternalStylesheet("resource/comment.css") document.addExternalScript("resource/review.js") document.addExternalScript("resource/comment.js") document.addInternalScript(user.getJS()) document.addInternalScript(review.getJS()) if batch_chain: document.addInternalScript( "commentChainById[%d] = %s;" % (batch_chain.id, batch_chain.getJSConstructor())) target = body.div("main") table = target.table('paleyellow basic comments', align='center') table.col(width='10%') table.col(width='80%') table.col(width='10%') table.tr().td('h1', colspan=3).h1().text("Review by %s" % htmlify(author.fullname)) if batch_chain: batch_chain.loadComments(db, user) row = table.tr("line") row.td("heading").text("Comment:") row.td("value").preformatted().div("text").text( htmlify(batch_chain.comments[0].comment)) row.td("status").text() def renderFiles(title, cursor): files = [] for file_id, delete_count, insert_count in cursor.fetchall(): files.append( (dbutils.describe_file(db, file_id), delete_count, insert_count)) paths = [] deleted = [] inserted = [] for path, delete_count, insert_count in sorted(files): paths.append(path) deleted.append(delete_count) inserted.append(insert_count) if paths: diff.File.eliminateCommonPrefixes(paths) row = table.tr("line") row.td("heading").text(title) files_table = row.td().table("files callout") headers = files_table.thead().tr() headers.th("path").text("Changed Files") headers.th("lines", colspan=2).text("Lines") files = files_table.tbody() for path, delete_count, insert_count in zip( paths, deleted, inserted): file = files.tr() file.td("path").preformatted().innerHTML(path) file.td("lines").preformatted().text( "-%d" % delete_count if delete_count else None) file.td("lines").preformatted().text( "+%d" % insert_count if insert_count else None) row.td("status").text() def condition(table_name): if batch_id: return "%s.batch=%d" % (table_name, batch_id) else: return "review=%d AND %s.batch IS NULL AND %s.uid=%d" % ( review.id, table_name, table_name, author.id) cursor.execute("""SELECT reviewfiles.file, SUM(deleted), SUM(inserted) FROM reviewfiles JOIN reviewfilechanges ON (reviewfilechanges.file=reviewfiles.id) WHERE %s AND reviewfilechanges.to_state='reviewed' GROUP BY reviewfiles.file""" % condition("reviewfilechanges")) renderFiles("Reviewed:", cursor) cursor.execute("""SELECT reviewfiles.file, SUM(deleted), SUM(inserted) FROM reviewfiles JOIN reviewfilechanges ON (reviewfilechanges.file=reviewfiles.id) WHERE %s AND reviewfilechanges.to_state='pending' GROUP BY reviewfiles.file""" % condition("reviewfilechanges")) renderFiles("Unreviewed:", cursor) def renderChains(title, cursor, replies): all_chains = [ review_comment.CommentChain.fromId(db, chain_id, user, review=review) for (chain_id, ) in cursor ] if not all_chains: return for chain in all_chains: chain.loadComments(db, user) issue_chains = filter(lambda chain: chain.type == "issue", all_chains) draft_issues = filter(lambda chain: chain.state == "draft", issue_chains) open_issues = filter(lambda chain: chain.state == "open", issue_chains) addressed_issues = filter(lambda chain: chain.state == "addressed", issue_chains) closed_issues = filter(lambda chain: chain.state == "closed", issue_chains) note_chains = filter(lambda chain: chain.type == "note", all_chains) draft_notes = filter( lambda chain: chain.state == "draft" and chain != batch_chain, note_chains) open_notes = filter( lambda chain: chain.state == "open" and chain != batch_chain, note_chains) def renderChains(target, chains): for chain in chains: row = target.tr("comment") row.td("author").text(chain.user.fullname) row.td("title").a(href="showcomment?chain=%d" % chain.id).innerHTML(chain.leader()) row.td("when").text(chain.when()) def showcomments(filter_param): params = {"review": review.id, "filter": filter_param} if batch_id: params["batch"] = batch_id return htmlutils.URL("/showcomments", **params) if draft_issues or open_issues or addressed_issues or closed_issues: h2 = table.tr().td("h2", colspan=3).h2().text(title) if len(draft_issues) + len(open_issues) + len( addressed_issues) + len(closed_issues) > 1: h2.a(href=showcomments("issues")).text("[display all]") if draft_issues: h3 = table.tr(id="draft-issues").td( "h3", colspan=3).h3().text("Draft issues") if len(draft_issues) > 1: h3.a(href=showcomments("draft-issues")).text( "[display all]") renderChains(table, draft_issues) if batch_id is not None or replies: if open_issues: h3 = table.tr(id="open-issues").td( "h3", colspan=3).h3().text("Still open issues") if batch_id and len(open_issues) > 1: h3.a(href=showcomments("open-issues")).text( "[display all]") renderChains(table, open_issues) if addressed_issues: h3 = table.tr(id="addressed-issues").td( "h3", colspan=3).h3().text("Now addressed issues") if batch_id and len(addressed_issues) > 1: h3.a(href=showcomments("addressed-issues")).text( "[display all]") renderChains(table, addressed_issues) if closed_issues: h3 = table.tr(id="closed-issues").td( "h3", colspan=3).h3().text("Now closed issues") if batch_id and len(closed_issues) > 1: h3.a(href=showcomments("closed-issues")).text( "[display all]") renderChains(table, closed_issues) if draft_notes or open_notes: h2 = table.tr().td("h2", colspan=3).h2().text(title) if len(draft_notes) + len(open_notes) > 1: h2.a(href=showcomments("notes")).text("[display all]") if draft_notes: h3 = table.tr(id="draft-notes").td( "h3", colspan=3).h3().text("Draft notes") if len(draft_notes) > 1: h3.a( href=showcomments("draft-notes")).text("[display all]") renderChains(table, draft_notes) if open_notes: h3 = table.tr(id="notes").td("h3", colspan=3).h3().text("Notes") if batch_id and len(open_notes) > 1: h3.a(href=showcomments("open-notes")).text("[display all]") renderChains(table, open_notes) cursor.execute("SELECT id FROM commentchains WHERE %s AND type='issue'" % condition("commentchains")) renderChains("Raised issues", cursor, False) cursor.execute("""SELECT commentchains.id FROM commentchains JOIN commentchainchanges ON (commentchainchanges.chain=commentchains.id) WHERE %s AND to_state='closed'""" % condition("commentchainchanges")) renderChains("Resolved issues", cursor, False) cursor.execute("""SELECT commentchains.id FROM commentchains JOIN commentchainchanges ON (commentchainchanges.chain=commentchains.id) WHERE %s AND to_state='open'""" % condition("commentchainchanges")) renderChains("Reopened issues", cursor, False) cursor.execute("""SELECT commentchains.id FROM commentchains JOIN commentchainchanges ON (commentchainchanges.chain=commentchains.id) WHERE %s AND to_type='issue'""" % condition("commentchainchanges")) renderChains("Converted into issues", cursor, False) cursor.execute("""SELECT commentchains.id FROM commentchains JOIN commentchainchanges ON (commentchainchanges.chain=commentchains.id) WHERE %s AND to_type='note'""" % condition("commentchainchanges")) renderChains("Converted into notes", cursor, False) cursor.execute("SELECT id FROM commentchains WHERE %s AND type='note'" % condition("commentchains")) renderChains("Written notes", cursor, False) cursor.execute("""SELECT commentchains.id FROM commentchains JOIN comments ON (comments.chain=commentchains.id) WHERE %s AND comments.id!=commentchains.first_comment""" % condition("comments")) renderChains("Replied to", cursor, True) return document
def tag(cls, value): return "<b class='%s'>%s</b>" % (cls, htmlutils.htmlify(value)) def tagm(cls, value):
def __init__(self, code, title, message, is_html=False): self.__code = code self.__title = htmlutils.htmlify(title) self.__message = message if is_html else htmlutils.htmlify(message)
def process(self, db, user, repository, branch, summary, commit_ids=None, commit_sha1s=None, applyfilters=True, applyparentfilters=True, reviewfilters=None, recipientfilters=None, description=None, frombranch=None, trackedbranch=None): if not branch.startswith("r/"): raise OperationFailure( code="invalidbranch", title="Invalid review branch name", message= "'%s' is not a valid review branch name; it must have a \"r/\" prefix." % branch) if reviewfilters is None: reviewfilters = [] if recipientfilters is None: recipientfilters = {} components = branch.split("/") for index in range(1, len(components)): try: repository.revparse("refs/heads/%s" % "/".join(components[:index])) except gitutils.GitReferenceError: continue message = ( "Cannot create branch with name<pre>%s</pre>since there is already a branch named<pre>%s</pre>in the repository." % (htmlutils.htmlify(branch), htmlutils.htmlify("/".join(components[:index])))) raise OperationFailure(code="invalidbranch", title="Invalid review branch name", message=message, is_html=True) if commit_sha1s is not None: commits = [ gitutils.Commit.fromSHA1(db, repository, commit_sha1) for commit_sha1 in commit_sha1s ] elif commit_ids is not None: commits = [ gitutils.Commit.fromId(db, repository, commit_id) for commit_id in commit_ids ] else: commits = [] commitset = CommitSet(commits) reviewfilters = parseReviewFilters(db, reviewfilters) recipientfilters = parseRecipientFilters(db, recipientfilters) review = createReview(db, user, repository, commits, branch, summary, description, from_branch_name=frombranch, reviewfilters=reviewfilters, recipientfilters=recipientfilters, applyfilters=applyfilters, applyparentfilters=applyparentfilters) extensions_output = StringIO() kwargs = {} if configuration.extensions.ENABLED: if extensions.role.processcommits.execute( db, user, review, commits, None, commitset.getHeads().pop(), extensions_output): kwargs["extensions_output"] = extensions_output.getvalue( ).lstrip() if trackedbranch: cursor = db.cursor() cursor.execute( """SELECT 1 FROM knownremotes WHERE url=%s AND pushing""", (trackedbranch["remote"], )) if cursor.fetchone(): delay = "1 week" else: delay = "1 hour" cursor.execute( """INSERT INTO trackedbranches (repository, local_name, remote, remote_name, forced, delay) VALUES (%s, %s, %s, %s, false, %s) RETURNING id""", (repository.id, branch, trackedbranch["remote"], trackedbranch["name"], delay)) trackedbranch_id = cursor.fetchone()[0] kwargs["trackedbranch_id"] = trackedbranch_id cursor.execute( """INSERT INTO trackedbranchusers (branch, uid) VALUES (%s, %s)""", (trackedbranch_id, user.id)) db.commit() pid = int( open(configuration.services.BRANCHTRACKER["pidfile_path"]). read().strip()) os.kill(pid, signal.SIGHUP) return OperationResult(review_id=review.id, **kwargs)
def process(self, db, user, repository_name, remote, branch, upstream="refs/heads/master"): repository = gitutils.Repository.fromName(db, repository_name) cursor = db.cursor() # Check if any other repository is currently tracking branches from this # remote. If that's the case, then the user most likely either selected # the wrong repository or entered the wrong remote. cursor.execute("""SELECT repositories.name FROM repositories JOIN trackedbranches ON (trackedbranches.repository=repositories.id) WHERE repositories.id!=%s AND trackedbranches.remote=%s""", (repository.id, remote)) for (other_name,) in cursor: raise OperationFailure( code="badremote", title="Bad remote!", message=("The remote <code>%s</code> appears to be related to " "another repository on this server (<code>%s</code>). " "You most likely shouldn't be importing branches from " "it into the selected repository (<code>%s</code>)." % (htmlutils.htmlify(remote), htmlutils.htmlify(other_name), htmlutils.htmlify(repository_name))), is_html=True) if not branch.startswith("refs/"): branch = "refs/heads/%s" % branch try: head_sha1 = repository.fetchTemporaryFromRemote(remote, branch) except gitutils.GitReferenceError: raise OperationFailure( code="refnotfound", title="Remote ref not found!", message=("Could not find the ref <code>%s</code> in the repository <code>%s</code>." % (htmlutils.htmlify(branch), htmlutils.htmlify(remote))), is_html=True) except gitutils.GitCommandError as error: if error.output.splitlines()[0].endswith("does not appear to be a git repository"): raise OperationFailure( code="invalidremote", title="Invalid remote!", message=("<code>%s</code> does not appear to be a valid Git repository." % htmlutils.htmlify(remote)), is_html=True) else: raise if upstream.startswith("refs/"): try: upstream_sha1 = repository.fetchTemporaryFromRemote(remote, upstream) except gitutils.GitReferenceError: raise OperationFailure( code="refnotfound", title="Remote ref not found!", message=("Could not find the ref <code>%s</code> in the repository <code>%s</code>." % (htmlutils.htmlify(upstream), htmlutils.htmlify(remote))), is_html=True) else: try: upstream_sha1 = repository.revparse(upstream) except gitutils.GitReferenceError: raise OperationFailure( code="refnotfound", title="Local ref not found!", message=("Could not find the ref <code>%s</code> in the repository <code>%s</code>." % (htmlutils.htmlify(upstream), htmlutils.htmlify(str(repository)))), is_html=True) try: resolved_upstream_sha1 = gitutils.getTaggedCommit(repository, upstream_sha1) except gitutils.GitReferenceError: resolved_upstream_sha1 = None if not resolved_upstream_sha1: raise OperationFailure( code="missingcommit", title="Upstream commit is missing!", message=("<p>Could not find the commit <code>%s</code> in the " "repository <code>%s</code>.</p>" "<p>Since it would have been fetched along with the " "branch if it actually was a valid upstream commit, " "this means it's not valid.</p>" % (htmlutils.htmlify(upstream_sha1), htmlutils.htmlify(str(repository)))), is_html=True) commit_sha1s = repository.revlist(included=[head_sha1], excluded=[resolved_upstream_sha1]) if not commit_sha1s: raise OperationFailure( code="emptybranch", title="Branch contains no commits!", message=("All commits referenced by <code>%s</code> are reachable from <code>%s</code>." % (htmlutils.htmlify(branch), htmlutils.htmlify(upstream))), is_html=True) cursor.execute("SELECT id FROM commits WHERE sha1=ANY (%s)", (commit_sha1s,)) return OperationResult(commit_ids=[commit_id for (commit_id,) in cursor], head_sha1=head_sha1, upstream_sha1=resolved_upstream_sha1)
def process(self, db, user, name, path, mirror=None): if not user.hasRole(db, "repositories"): raise OperationFailure(code="notallowed", title="Not allowed!", message="Only users with the 'repositories' role can add new repositories.") if name.endswith(".git"): raise OperationFailure(code="badsuffix_name", title="Invalid short name", message="The short name must not end with .git") if name == "r": raise OperationFailure(code="invalid_name", title="Invalid short name", message="The short name 'r' is not allowed since corresponding /REPOSHORTNAME/{SHA1|BRANCH} URLs would conflict with r/REVIEW_ID URLs.") path = path.strip("/").rsplit("/", 1) if len(path) == 2: base, repository_name = path else: base, repository_name = None, path[0] if base: main_base_path = os.path.join(configuration.paths.GIT_DIR, base) else: main_base_path = configuration.paths.GIT_DIR main_path = os.path.join(main_base_path, repository_name + ".git") cursor = db.cursor() cursor.execute("""SELECT name FROM repositories WHERE path=%s""", (main_path,)) row = cursor.fetchone() if row: raise OperationFailure(code="duplicaterepository", title="Duplicate repository", message="The specified path is already used by repository %s" % row[0]) cursor.execute("""SELECT name FROM repositories WHERE name=%s""", (name,)) row = cursor.fetchone() if row: raise OperationFailure(code="duplicateshortname", title="Duplicate short name", message="The specified short name is already in use, please select a different short name.") if not os.path.isdir(main_base_path): os.makedirs(main_base_path, mode=0775) def git(arguments, cwd): argv = [configuration.executables.GIT] + arguments git = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) stdout, stderr = git.communicate() if git.returncode != 0: raise gitutils.GitError("unexpected output from '%s': %s" % (" ".join(argv), stderr)) if mirror: try: subprocess.check_output([configuration.executables.GIT, "ls-remote", mirror["remote_url"]], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: raise OperationFailure(code="failedreadremote", title="Failed to read source repository", message="Critic failed to read from the specified source repository. The error reported from git " + "(when running as the system user '%s') was: <pre>%s</pre>" % (configuration.base.SYSTEM_USER_NAME, htmlutils.htmlify(e.output)), is_html=True) git(["init", "--bare", "--shared", repository_name + ".git"], cwd=main_base_path) git(["config", "receive.denyNonFastforwards", "false"], cwd=main_path) git(["config", "critic.name", name], cwd=main_path) if configuration.debug.IS_QUICKSTART: git(["config", "critic.socket", os.path.join(configuration.paths.SOCKETS_DIR, "githook.unix")], cwd=main_path) os.symlink(os.path.join(configuration.paths.INSTALL_DIR, "hooks", "pre-receive"), os.path.join(main_path, "hooks", "pre-receive")) cursor.execute("""INSERT INTO repositories (name, path) VALUES (%s, %s) RETURNING id""", (name, main_path)) repository_id = cursor.fetchone()[0] if mirror: cursor.execute("""INSERT INTO trackedbranches (repository, local_name, remote, remote_name, forced, delay) VALUES (%s, '*', %s, '*', true, '1 day')""", (repository_id, mirror["remote_url"])) cursor.execute("""INSERT INTO trackedbranches (repository, local_name, remote, remote_name, forced, delay) VALUES (%s, %s, %s, %s, true, '1 day')""", (repository_id, mirror["local_branch"], mirror["remote_url"], mirror["remote_branch"])) git(["symbolic-ref", "HEAD", "refs/heads/" + mirror["local_branch"]], cwd=main_path) db.commit() if mirror: pid = int(open(configuration.services.BRANCHTRACKER["pidfile_path"]).read().strip()) os.kill(pid, signal.SIGHUP) return OperationResult()
def process(self, db, user, repository, branch, summary, commit_ids=None, commit_sha1s=None, applyfilters=True, applyparentfilters=True, reviewfilters=None, recipientfilters=None, description=None, frombranch=None, trackedbranch=None): if not branch.startswith("r/"): raise OperationFailure(code="invalidbranch", title="Invalid review branch name", message="'%s' is not a valid review branch name; it must have a \"r/\" prefix." % branch) if reviewfilters is None: reviewfilters = [] if recipientfilters is None: recipientfilters = {} components = branch.split("/") for index in range(1, len(components)): try: repository.revparse("refs/heads/%s" % "/".join(components[:index])) except gitutils.GitReferenceError: continue message = ("Cannot create branch with name<pre>%s</pre>since there is already a branch named<pre>%s</pre>in the repository." % (htmlutils.htmlify(branch), htmlutils.htmlify("/".join(components[:index])))) raise OperationFailure(code="invalidbranch", title="Invalid review branch name", message=message, is_html=True) if commit_sha1s is not None: commits = [gitutils.Commit.fromSHA1(db, repository, commit_sha1) for commit_sha1 in commit_sha1s] elif commit_ids is not None: commits = [gitutils.Commit.fromId(db, repository, commit_id) for commit_id in commit_ids] else: commits = [] commitset = CommitSet(commits) reviewfilters = parseReviewFilters(db, reviewfilters) recipientfilters = parseRecipientFilters(db, recipientfilters) review = createReview(db, user, repository, commits, branch, summary, description, from_branch_name=frombranch, reviewfilters=reviewfilters, recipientfilters=recipientfilters, applyfilters=applyfilters, applyparentfilters=applyparentfilters) extensions_output = StringIO() kwargs = {} if configuration.extensions.ENABLED: if extensions.role.processcommits.execute(db, user, review, commits, None, commitset.getHeads().pop(), extensions_output): kwargs["extensions_output"] = extensions_output.getvalue().lstrip() if trackedbranch: cursor = db.cursor() cursor.execute("""SELECT 1 FROM knownremotes WHERE url=%s AND pushing""", (trackedbranch["remote"],)) if cursor.fetchone(): delay = "1 week" else: delay = "1 hour" cursor.execute("""INSERT INTO trackedbranches (repository, local_name, remote, remote_name, forced, delay) VALUES (%s, %s, %s, %s, false, %s) RETURNING id""", (repository.id, branch, trackedbranch["remote"], trackedbranch["name"], delay)) trackedbranch_id = cursor.fetchone()[0] cursor.execute("""INSERT INTO trackedbranchusers (branch, uid) VALUES (%s, %s)""", (trackedbranch_id, user.id)) db.commit() pid = int(open(configuration.services.BRANCHTRACKER["pidfile_path"]).read().strip()) os.kill(pid, signal.SIGHUP) return OperationResult(review_id=review.id, **kwargs)
def basic(): import htmlutils from operation.basictypes import OperationError, OperationFailure from operation.typechecker import ( Optional, TypeChecker, TypeCheckerContext, BooleanChecker, StringChecker, RestrictedString, SHA1, IntegerChecker, RestrictedInteger, PositiveInteger, NonNegativeInteger, ArrayChecker, EnumerationChecker, VariantChecker, DictionaryChecker) # Check TypeChecker.make()'s handling of basic types. assert type(TypeChecker.make(bool)) is BooleanChecker assert type(TypeChecker.make(str)) is StringChecker assert type(TypeChecker.make(int)) is IntegerChecker assert type(TypeChecker.make([bool])) is ArrayChecker assert type(TypeChecker.make(set(["foo", "bar"]))) is EnumerationChecker assert type(TypeChecker.make(set([bool, str, int]))) is VariantChecker assert type(TypeChecker.make({"foo": bool})) is DictionaryChecker # Check TypeChecker.make()'s handling of TypeChecker sub-classes and # instances thereof. assert isinstance(TypeChecker.make(BooleanChecker), BooleanChecker) boolean_checker = BooleanChecker() assert TypeChecker.make(boolean_checker) is boolean_checker def check(checker, *values): checker = TypeChecker.make(checker) results = [] for value in values: converted = checker(value, TypeCheckerContext(None, None, None)) results.append(value if converted is None else converted) return results def should_match(checker, *values, **kwargs): results = check(checker, *values) if "result" in kwargs: expected_result = kwargs["result"] for result in results: assert result == expected_result, \ "%r != %r" % (result, expected_result) def should_not_match(checker, *values, **expected): for value in values: try: check(checker, copy.deepcopy(value)) except (OperationError, OperationFailure) as error: error = json.loads(str(error)) for key, value in expected.items(): if isinstance(value, str): value = set([value]) assert error.get(key) in value, \ ("%s: %r not among %r" % (key, error.get(key), value)) else: assert False, "checker allowed value incorrectly: %r" % value # Check some simple things that should be accepted. should_match(bool, True, False) should_match(str, "", "foo") should_match(int, -2**31, -1, 0, 1, 2**31) should_match([bool], [], [True, False]) should_match([str], ["", "foo"]) should_match([int], [-2**31, -1, 0, 1, 2**31]) should_match(set(["foo", "bar"]), "foo", "bar") should_match(set([bool, str, int]), True, False, "", "foo", -2**31, -1, 0, 1, 2**31) # Check some equally simple things that shouldn't be accepted. should_not_match(bool, 10, "foo", error="invalid input: data is not a boolean") should_not_match(str, True, 10, error="invalid input: data is not a string") should_not_match(int, True, "foo", 0.5, error="invalid input: data is not an integer") should_not_match([bool], [True, 10], [False, "foo"], error="invalid input: data[1] is not a boolean") should_not_match([str], ["", True], ["foo", 10], error="invalid input: data[1] is not a string") should_not_match([int], [0, True], [10, "foo"], error="invalid input: data[1] is not an integer") should_not_match(set(["foo", "bar"]), "fie", error="invalid input: data is not valid") should_not_match(set(["foo", "bar"]), True, 10, error="invalid input: data is not a string") should_not_match(set([bool, str, int]), [True], ["foo"], [10], error="data is of invalid type") # Check some dictionary checkers. should_match({ "b": bool, "s": str, "i": int }, { "b": True, "s": "foo", "i": 10 }) should_match({ "req": bool, "opt": Optional(bool) }, { "req": True, "opt": False }, {"req": False}) should_not_match({"b": bool}, {"b": "foo"}, {"b": 10}, error="invalid input: data.b is not a boolean") should_not_match({"b": bool}, {"i": 10}, error="invalid input: data.b missing") should_not_match({"b": bool}, { "b": True, "i": 10 }, error="invalid input: data.i was not used") should_not_match({"b": Optional(bool)}, {"b": "foo"}, {"b": 10}, error="invalid input: data.b is not a boolean") # Check suffixed variant checker in dictionary. id_or_name = VariantChecker({"id": int, "name": str}) should_match({"thing": id_or_name}, {"thing": 10}, {"thing_id": 10}, result={"thing": 10}) should_match({"thing": id_or_name}, {"thing": "foo"}, {"thing_name": "foo"}, result={"thing": "foo"}) should_not_match({"thing": id_or_name}, {"thing_id": "foo"}, error="invalid input: data.thing_id is not an integer") should_not_match({"thing": id_or_name}, {"thing_name": 10}, error="invalid input: data.thing_name is not a string") should_not_match({"thing": id_or_name}, { "thing_id": 10, "thing_name": "foo" }, error=("invalid input: data.thing_id was not used", "invalid input: data.thing_name was not used")) # Check some RestrictedString types. should_match(RestrictedString, "", "foo") should_match(RestrictedString(minlength=0), "", "foo") should_match(RestrictedString(minlength=3), "foo") should_match(RestrictedString(maxlength=0), "") should_match(RestrictedString(maxlength=3), "", "foo") should_match(RestrictedString(minlength=0, maxlength=3), "", "foo") should_match(RestrictedString(allowed=lambda c: False), "") should_match(RestrictedString(allowed=lambda c: True), "", "foo") should_match(RestrictedString(allowed=lambda c: c in "foo"), "", "foo") should_not_match(RestrictedString(), True, 10, error="invalid input: data is not a string") should_not_match( RestrictedString(minlength=1), "", code="paramtooshort:data", title="Invalid data", message="invalid input: data must be at least 1 characters long") should_not_match( RestrictedString(maxlength=2), "foo", code="paramtoolong:data", title="Invalid data", message="invalid input: data must be at most 2 characters long") should_not_match( RestrictedString(allowed=lambda c: False), "foo", code="paramcontainsillegalchar:data", title="Invalid data", message="invalid input: data may not contain the characters 'f', 'o'") should_not_match( RestrictedString(allowed=lambda c: False, ui_name="gazonk"), "foo", code="paramcontainsillegalchar:data", title="Invalid gazonk", message="invalid input: gazonk may not contain the characters 'f', 'o'" ) # Check SHA1. sha1 = "0123456789abcdefABCDEF0123456789abcdefAB" should_match(SHA1, *[sha1[:length] for length in range(4, 41)]) should_not_match(SHA1, True, 10, error="invalid input: data is not a string") for ch in range(0, 256): ch = chr(ch) if ch in sha1: continue should_not_match( SHA1, "012" + ch, message=htmlutils.htmlify( "invalid input: data may not contain the character %r" % ch)) should_not_match( SHA1, "012", message="invalid input: data must be at least 4 characters long") should_not_match( SHA1, "0" * 41, message="invalid input: data must be at most 40 characters long") # Check some RestrictedInteger types. should_match(RestrictedInteger, -2**31, -1, 0, 1, 2**31) should_match(RestrictedInteger(minvalue=-2**31), -2**31, -1, 0, 1, 2**31) should_match(RestrictedInteger(minvalue=0), 0, 1, 2**31) should_match(RestrictedInteger(maxvalue=0), -2**31, -1, 0) should_match(RestrictedInteger(maxvalue=2**31), -2**31, -1, 0, 1, 2**31) should_match(RestrictedInteger(minvalue=0, maxvalue=0), 0) should_not_match(RestrictedInteger(), True, "foo", error="invalid input: data is not an integer") should_not_match(RestrictedInteger(minvalue=0), -2**31, -1, code="valuetoolow:data", title="Invalid data parameter", message="invalid input: data must be 0 or higher") should_not_match(RestrictedInteger(maxvalue=0), 1, 2**31, code="valuetoohigh:data", title="Invalid data parameter", message="invalid input: data must be 0 or lower") should_not_match(RestrictedInteger(minvalue=1, ui_name="gazonk"), 0, code="valuetoolow:data", title="Invalid gazonk parameter", message="invalid input: gazonk must be 1 or higher") # Check NonNegativeInteger. should_match(NonNegativeInteger, 0, 1, 2**31) should_not_match(NonNegativeInteger, True, "foo", error="invalid input: data is not an integer") should_not_match(NonNegativeInteger, -2**31, -1, code="valuetoolow:data", title="Invalid data parameter", message="invalid input: data must be 0 or higher") # Check PositiveInteger. should_match(PositiveInteger, 1, 2**31) should_not_match(PositiveInteger, True, "foo", error="invalid input: data is not an integer") should_not_match(PositiveInteger, -2**31, -1, 0, code="valuetoolow:data", title="Invalid data parameter", message="invalid input: data must be 1 or higher")
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 createReview(db, user, repository, commits, branch_name, summary, description, from_branch_name=None, via_push=False, reviewfilters=None, applyfilters=True, applyparentfilters=False, recipientfilters=None): cursor = db.cursor() if via_push: applyparentfilters = bool( user.getPreference(db, 'review.applyUpstreamFilters')) branch = dbutils.Branch.fromName(db, repository, branch_name) if branch is not None: raise OperationFailure( code="branchexists", title="Invalid review branch name", message="""\ <p>There is already a branch named <code>%s</code> in the repository. You have to select a different name.</p> <p>If you believe the existing branch was created during an earlier (failed) attempt to create this review, you can try to delete it from the repository using the command<p> <pre> git push <remote> :%s</pre> <p>and then press the "Submit Review" button on this page again.""" % (htmlutils.htmlify(branch_name), htmlutils.htmlify(branch_name)), is_html=True) if not commits: raise OperationFailure( code="nocommits", title="No commits specified", message="You need at least one commit to create a review.") commitset = log_commitset.CommitSet(commits) heads = commitset.getHeads() if len(heads) != 1: # There is really no plausible way for this error to occur. raise OperationFailure( code="disconnectedtree", title="Disconnected tree", message=("The specified commits do do not form a single connected " "tree. Creating a review of them is not supported.")) head = heads.pop() if len(commitset.getTails()) != 1: tail_id = None else: tail_id = gitutils.Commit.fromSHA1( db, repository, commitset.getTails().pop()).getId(db) if not via_push: try: repository.createBranch(branch_name, head.sha1) except gitutils.GitCommandError as error: raise OperationFailure( code="branchfailed", title="Failed to create review branch", message=("<p><b>Output from git:</b></p>" "<code style='padding-left: 1em'>%s</code>" % htmlutils.htmlify(error.output)), is_html=True) createChangesetsForCommits(db, commits) try: cursor.execute( "INSERT INTO branches (repository, name, head, tail, type) VALUES (%s, %s, %s, %s, 'review') RETURNING id", [repository.id, branch_name, head.getId(db), tail_id]) branch_id = cursor.fetchone()[0] reachable_values = [(branch_id, commit.getId(db)) for commit in commits] cursor.executemany( "INSERT INTO reachable (branch, commit) VALUES (%s, %s)", reachable_values) cursor.execute( "INSERT INTO reviews (type, branch, state, summary, description, applyfilters, applyparentfilters) VALUES ('official', %s, 'open', %s, %s, %s, %s) RETURNING id", (branch_id, summary, description, applyfilters, applyparentfilters)) review = dbutils.Review.fromId(db, cursor.fetchone()[0]) cursor.execute( "INSERT INTO reviewusers (review, uid, owner) VALUES (%s, %s, TRUE)", (review.id, user.id)) if reviewfilters is not None: cursor.executemany( """INSERT INTO reviewfilters (review, uid, path, type, creator) VALUES (%s, %s, %s, %s, %s)""", [(review.id, filter_user_id, filter_path, filter_type, user.id) for filter_user_id, filter_path, filter_type, filter_delegate in reviewfilters]) is_opt_in = False if recipientfilters is not None: cursor.executemany( "INSERT INTO reviewrecipientfilters (review, uid, include) VALUES (%s, %s, %s)", [(review.id, filter_user_id, filter_include) for filter_user_id, filter_include in recipientfilters]) for filter_user_id, filter_include in recipientfilters: if filter_user_id is None and not filter_include: is_opt_in = True addCommitsToReview(db, user, review, commits, new_review=True) if from_branch_name is not None: cursor.execute( "UPDATE branches SET review=%s WHERE repository=%s AND name=%s", (review.id, repository.id, from_branch_name)) # Reload to get list of changesets added by addCommitsToReview(). review = dbutils.Review.fromId(db, review.id) pending_mails = [] recipients = review.getRecipients(db) for to_user in recipients: pending_mails.extend( mail.sendReviewCreated(db, user, to_user, recipients, review)) if not is_opt_in: recipient_by_id = dict( (to_user.id, to_user) for to_user in recipients) cursor.execute( """SELECT userpreferences.uid, userpreferences.repository, userpreferences.filter, userpreferences.integer FROM userpreferences LEFT OUTER JOIN filters ON (filters.id=userpreferences.filter) WHERE userpreferences.item='review.defaultOptOut' AND userpreferences.uid=ANY (%s) AND (userpreferences.filter IS NULL OR filters.repository=%s) AND (userpreferences.repository IS NULL OR userpreferences.repository=%s)""", (recipient_by_id.keys(), repository.id, repository.id)) user_settings = {} has_filter_settings = False for user_id, repository_id, filter_id, integer in cursor: settings = user_settings.setdefault(user_id, [None, None, {}]) value = bool(integer) if repository_id is None and filter_id is None: settings[0] = value elif repository_id is not None: settings[1] = value else: settings[2][filter_id] = value has_filter_settings = True if has_filter_settings: filters = Filters() filters.setFiles(db, review=review) for user_id, (global_default, repository_default, filter_settings) in user_settings.items(): to_user = recipient_by_id[user_id] opt_out = None if repository_default is not None: opt_out = repository_default elif global_default is not None: opt_out = global_default if filter_settings: # Policy: # # If all of the user's filters that matched files in the # review have review.defaultOptOut enabled, then opt out. # When determining this, any review filters of the user's # that match files in the review count as filters that don't # have the review.defaultOptOut enabled. # # If any of the user's filters that matched files in the # review have review.defaultOptOut disabled, then don't opt # out. When determining this, review filters are ignored. # # Otherwise, ignore the filter settings, and go with either # the user's per-repository or global setting (as set # above.) filters.load(db, review=review, user=to_user) # A set of filter ids. If None is in the set, the user has # one or more review filters in the review. (These do not # have ids.) active_filters = filters.getActiveFilters(to_user) for filter_id in active_filters: if filter_id is None: continue elif filter_id in filter_settings: if not filter_settings[filter_id]: opt_out = False break else: break else: if None not in active_filters: opt_out = True if opt_out: cursor.execute( """INSERT INTO reviewrecipientfilters (review, uid, include) VALUES (%s, %s, FALSE)""", (review.id, to_user.id)) db.commit() mail.sendPendingMails(pending_mails) return review except: if not via_push: repository.run("branch", "-D", branch_name) raise
def renderShowFile(req, db, user): cursor = db.cursor() sha1 = req.getParameter("sha1") path = req.getParameter("path") line = req.getParameter("line", None) review_id = req.getParameter("review", None, filter=int) default_tabify = "yes" if user.getPreference( db, "commit.diff.visualTabs") else "no" tabify = req.getParameter("tabify", default_tabify) == "yes" if line is None: first, last = None, None else: if "-" in line: first, last = map(int, line.split("-")) else: first = last = int(line) context = req.getParameter( "context", user.getPreference(db, "commit.diff.contextLines"), int) first_with_context = max(1, first - context) last_with_context = last + context if user.getPreference(db, "commit.diff.compactMode"): default_compact = "yes" else: default_compact = "no" compact = req.getParameter("compact", default_compact) == "yes" if len(path) == 0 or path[-1:] == "/": raise page.utils.DisplayMessage( title="Invalid path parameter", body= "<p>The path must be non-empty and must not end with a <code>/</code>.</p>", html=True) if path[0] == '/': full_path = path if path != "/": path = path[1:] else: full_path = "/" + path if not path: path = "/" if review_id is None: review = None repository_arg = req.getParameter("repository", "") if repository_arg: repository = gitutils.Repository.fromParameter(db, repository_arg) else: repository = gitutils.Repository.fromSHA1(db, sha1) else: review = dbutils.Review.fromId(db, review_id) repository = review.repository document = htmlutils.Document(req) html = document.html() head = html.head() body = html.body() if review: page.utils.generateHeader(body, db, user, lambda target: review_utils.renderDraftItems( db, user, review, target), extra_links=[("r/%d" % review.id, "Back to Review")]) else: page.utils.generateHeader(body, db, user) document.addExternalStylesheet("resource/showfile.css") document.addInternalStylesheet( htmlutils.stripStylesheet( user.getResource(db, "syntax.css")[1], compact)) commit = gitutils.Commit.fromSHA1(db, repository, sha1) file_sha1 = commit.getFileSHA1(full_path) file_id = dbutils.find_file(db, path=path) if file_sha1 is None: raise page.utils.DisplayMessage( title="File does not exist", body=("<p>There is no file named <code>%s</code> in the commit " "<a href='/showcommit?repository=%s&sha1=%s'>" "<code>%s</code></a>.</p>" % (htmlutils.htmlify(textutils.escape(full_path)), htmlutils.htmlify(repository.name), htmlutils.htmlify(sha1), htmlutils.htmlify(sha1[:8]))), html=True) file = diff.File(file_id, path, None, file_sha1, repository) # A new file ID might have been added to the database, so need to commit. db.commit() if file.canHighlight(): requestHighlights(repository, {file.new_sha1: (file.path, file.getLanguage())}) file.loadNewLines(True, request_highlight=True) if review: document.addInternalScript(user.getJS()) document.addInternalScript(review.getJS()) document.addInternalScript( "var changeset = { parent: { id: %(id)d, sha1: %(sha1)r }, child: { id: %(id)d, sha1: %(sha1)r } };" % { 'id': commit.getId(db), 'sha1': commit.sha1 }) document.addInternalScript( "var files = { %(id)d: { new_sha1: %(sha1)r }, %(sha1)r: { id: %(id)d, side: 'n' } };" % { 'id': file_id, 'sha1': file_sha1 }) document.addExternalStylesheet("resource/review.css") document.addExternalScript("resource/review.js") cursor.execute( """SELECT DISTINCT commentchains.id FROM commentchains JOIN commentchainlines ON (commentchainlines.chain=commentchains.id) WHERE commentchains.review=%s AND commentchains.file=%s AND commentchainlines.sha1=%s AND ((commentchains.state!='draft' OR commentchains.uid=%s) AND commentchains.state!='empty') GROUP BY commentchains.id""", (review.id, file_id, file_sha1, user.id)) comment_chain_script = "" for (chain_id, ) in cursor.fetchall(): chain = review_comment.CommentChain.fromId(db, chain_id, user, review=review) chain.loadComments(db, user) comment_chain_script += "commentChains.push(%s);\n" % chain.getJSConstructor( file_sha1) if comment_chain_script: document.addInternalScript(comment_chain_script) document.addExternalStylesheet("resource/comment.css") document.addExternalScript("resource/comment.js") document.addExternalScript("resource/showfile.js") if tabify: document.addExternalStylesheet("resource/tabify.css") document.addExternalScript("resource/tabify.js") tabwidth = file.getTabWidth() indenttabsmode = file.getIndentTabsMode() if user.getPreference(db, "commit.diff.highlightIllegalWhitespace"): document.addInternalStylesheet( user.getResource(db, "whitespace.css")[1], compact) if first is not None: document.addInternalScript( "var firstSelectedLine = %d, lastSelectedLine = %d;" % (first, last)) target = body.div("main") if tabify: target.script(type="text/javascript").text("calculateTabWidth();") table = target.table('file show expanded paleyellow', align='center', cellspacing=0) columns = table.colgroup() columns.col('edge') columns.col('linenr') columns.col('line') columns.col('middle') columns.col('middle') columns.col('line') columns.col('linenr') columns.col('edge') thead = table.thead() cell = thead.tr().td('h1', colspan=8) h1 = cell.h1() def make_url(url_path, path): params = {"sha1": sha1, "path": path} if review is None: params["repository"] = str(repository.id) else: params["review"] = str(review.id) return "%s?%s" % (url_path, urllib.urlencode(params)) h1.a("root", href=make_url("showtree", "/")).text("root") h1.span().text('/') components = path.split("/") for index, component in enumerate(components[:-1]): h1.a(href=make_url("showtree", "/".join(components[:index + 1]))).text( component, escape=True) h1.span().text('/') if first is not None: h1.a(href=make_url("showfile", "/".join(components))).text( components[-1], escape=True) else: h1.text(components[-1], escape=True) h1.span("right").a(href=("/download/%s?repository=%s&sha1=%s" % (urllib.quote(path), repository.name, file_sha1)), download=urllib.quote(path)).text("[download]") h1.span("right").a( href=("/download/%s?repository=%s&sha1=%s" % (urllib.quote(path), repository.name, file_sha1))).text("[view]") table.tbody('spacer top').tr('spacer top').td(colspan=8).text() tbody = table.tbody("lines") yield document.render(stop=tbody, pretty=not compact) for linenr, line in enumerate(file.newLines(True)): linenr = linenr + 1 highlight_class = "" if first is not None: if not (first_with_context <= linenr <= last_with_context): continue if linenr == first: highlight_class += " first-selected" if linenr == last: highlight_class += " last-selected" if tabify: line = htmlutils.tabify(line, tabwidth, indenttabsmode) line = line.replace("\r", "<i class='cr'></i>") row = tbody.tr("line context single", id="f%do%dn%d" % (file.id, linenr, linenr)) row.td("edge").text() row.td("linenr old").text(linenr) row.td("line single whole%s" % highlight_class, id="f%dn%d" % (file.id, linenr), colspan=4).innerHTML(line) row.td("linenr new").text(linenr) row.td("edge").text() if linenr % 500: yield document.render(stop=tbody, pretty=not compact) table.tbody('spacer bottom').tr('spacer bottom').td(colspan=8).text() yield document.render(pretty=not compact)
def renderCreateReview(req, db, user): if user.isAnonymous(): raise page.utils.NeedLogin(req) repository = req.getParameter("repository", filter=gitutils.Repository.FromParameter(db), default=None) applyparentfilters = req.getParameter("applyparentfilters", "yes" if user.getPreference(db, 'review.applyUpstreamFilters') else "no") == "yes" cursor = db.cursor() if req.method == "POST": data = json_decode(req.read()) summary = data.get("summary") description = data.get("description") review_branch_name = data.get("review_branch_name") commit_ids = data.get("commit_ids") commit_sha1s = data.get("commit_sha1s") else: summary = req.getParameter("summary", None) description = req.getParameter("description", None) review_branch_name = req.getParameter("reviewbranchname", None) commit_ids = None commit_sha1s = None commits_arg = req.getParameter("commits", None) remote = req.getParameter("remote", None) upstream = req.getParameter("upstream", "master") branch_name = req.getParameter("branch", None) if commits_arg: try: commit_ids = map(int, commits_arg.split(",")) except: commit_sha1s = [repository.revparse(ref) for ref in commits_arg.split(",")] elif branch_name: cursor.execute("""SELECT commit FROM reachable JOIN branches ON (branch=id) WHERE repository=%s AND name=%s""", (repository.id, branch_name)) commit_ids = [commit_id for (commit_id,) in cursor] if len(commit_ids) > configuration.limits.MAXIMUM_REVIEW_COMMITS: raise page.utils.DisplayMessage( "Too many commits!", (("<p>The branch <code>%s</code> contains %d commits. Reviews can" "be created from branches that contain at most %d commits.</p>" "<p>This limit can be adjusted by modifying the system setting" "<code>configuration.limits.MAXIMUM_REVIEW_COMMITS</code>.</p>") % (htmlutils.htmlify(branch_name), len(commit_ids), configuration.limits.MAXIMUM_REVIEW_COMMITS)), html=True) else: return renderSelectSource(req, db, user) req.content_type = "text/html; charset=utf-8" if commit_ids: commits = [gitutils.Commit.fromId(db, repository, commit_id) for commit_id in commit_ids] elif commit_sha1s: commits = [gitutils.Commit.fromSHA1(db, repository, commit_sha1) for commit_sha1 in commit_sha1s] else: commits = [] if not commit_ids: commit_ids = [commit.getId(db) for commit in commits] if not commit_sha1s: commit_sha1s = [commit.sha1 for commit in commits] if summary is None: if len(commits) == 1: summary = commits[0].summary() else: summary = "" if review_branch_name: invalid_branch_name = "false" default_branch_name = review_branch_name else: invalid_branch_name = htmlutils.jsify(user.name + "/") default_branch_name = user.name + "/" match = re.search("(?:^|[Ff]ix(?:e[ds])?(?: +for)?(?: +bug)? +)([A-Z][A-Z0-9]+-[0-9]+)", summary) if match: invalid_branch_name = "false" default_branch_name = htmlutils.htmlify(match.group(1)) changesets = [] changeset_utils.createChangesets(db, repository, commits) for commit in commits: changesets.extend(changeset_utils.createChangeset(db, None, repository, commit, do_highlight=False)) changeset_ids = [changeset.id for changeset in changesets] all_reviewers, all_watchers = reviewing.utils.getReviewersAndWatchers( db, repository, changesets=changesets, applyparentfilters=applyparentfilters) document = htmlutils.Document(req) html = document.html() head = html.head() document.addInternalScript(user.getJS(db)) if branch_name: document.addInternalScript("var fromBranch = %s;" % htmlutils.jsify(branch_name)) if remote: document.addInternalScript("var trackedbranch = { remote: %s, name: %s };" % (htmlutils.jsify(remote), htmlutils.jsify(branch_name))) head.title().text("Create Review") body = html.body(onload="document.getElementById('branch_name').focus()") page.utils.generateHeader(body, db, user, lambda target: target.button(onclick="submitReview();").text("Submit Review")) document.addExternalStylesheet("resource/createreview.css") document.addExternalScript("resource/createreview.js") document.addExternalScript("resource/reviewfilters.js") document.addExternalScript("resource/autocomplete.js") document.addInternalScript(""" var invalid_branch_name = %s; var review_data = { commit_ids: %r, commit_sha1s: %r, changeset_ids: %r };""" % (invalid_branch_name, commit_ids, commit_sha1s, changeset_ids)) document.addInternalScript(repository.getJS()) main = body.div("main") table = main.table("basic paleyellow", align="center") table.tr().td("h1", colspan=3).h1().text("Create Review") row = table.tr("line") row.td("heading").text("Branch Name:") row.td("value").text("r/").input("value", id="branch_name", value=default_branch_name) row.td("status") row = table.tr() if not remote: row.td("help", colspan=3).div().text("""\ This is the main identifier of the review. It will be created in the review repository to contain the commits below. Reviewers can fetch it from there, and additional commits can be added to the review later by pushing them to this branch in the review repository.""") else: row.td("help", colspan=3).div().text("""\ This is the main identifier of the review. It will be created in the review repository to contain the commits below, and reviewers can fetch it from there.""") if remote: row = table.tr("line") row.td("heading").text("Tracked Branch:") value = row.td("value") value.code("branch inset").text(branch_name, linkify=linkify.Context(remote=remote)) value.text(" in ") value.code("remote inset").text(remote, linkify=linkify.Context()) row.td("status") row = table.tr() row.td("help", colspan=3).div().text("""\ Rather than pushing directly to the review branch in Critic's repository to add commits to the review, you will be pushing to this branch (in a separate repository,) from which Critic will fetch commits and add them to the review automatically.""") row = table.tr("line") row.td("heading").text("Summary:") row.td("value").input("value", id="summary", value=summary) row.td("status") row = table.tr() row.td("help", colspan=3).div().text("""\ The summary should be a short summary of the changes in the review. It will appear in the subject of all emails sent about the review. """) row = table.tr("line description") row.td("heading").text("Description:") textarea = row.td("value").textarea(id="description", rows=12) textarea.preformatted() if description: textarea.text(description) row.td("status") row = table.tr() row.td("help", colspan=3).div().text("""\ The description should describe the changes to be reviewed. It is usually fine to leave the description empty, since the commit messages are also available in the review. """) generateReviewersAndWatchersTable(db, repository, main, all_reviewers, all_watchers, applyparentfilters=applyparentfilters) row = table.tr("line recipients") row.td("heading").text("Recipient List:") cell = row.td("value", colspan=2).preformatted() cell.span("mode").text("Everyone") cell.span("users") cell.text(".") buttons = cell.div("buttons") buttons.button(onclick="editRecipientList();").text("Edit Recipient List") row = table.tr() row.td("help", colspan=3).div().text("""\ The basic recipient list for e-mails sent about the review. """) log.html.render(db, main, "Commits", commits=commits) return document
def process(self, db, user, repository_name, remote, branch, upstream="refs/heads/master"): repository = gitutils.Repository.fromName(db, repository_name) cursor = db.cursor() # Check if any other repository is currently tracking branches from this # remote. If that's the case, then the user most likely either selected # the wrong repository or entered the wrong remote. cursor.execute( """SELECT repositories.name FROM repositories JOIN trackedbranches ON (trackedbranches.repository=repositories.id) WHERE repositories.id!=%s AND trackedbranches.remote=%s""", (repository.id, remote)) for (other_name, ) in cursor: raise OperationFailure( code="badremote", title="Bad remote!", message=( "The remote <code>%s</code> appears to be related to " "another repository on this server (<code>%s</code>). " "You most likely shouldn't be importing branches from " "it into the selected repository (<code>%s</code>)." % (htmlutils.htmlify(remote), htmlutils.htmlify(other_name), htmlutils.htmlify(repository_name))), is_html=True) if not branch.startswith("refs/"): branch = "refs/heads/%s" % branch try: with repository.fetchTemporaryFromRemote(remote, branch) as sha1: head_sha1 = repository.keepalive(sha1) except gitutils.GitReferenceError as error: if error.repository: raise OperationFailure( code="refnotfound", title="Remote ref not found!", message= ("Could not find the ref <code>%s</code> in the repository <code>%s</code>." % (htmlutils.htmlify( error.ref), htmlutils.htmlify(error.repository))), is_html=True) else: raise OperationFailure( code="invalidref", title="Invalid ref!", message=("The specified ref is invalid: <code>%s</code>." % htmlutils.htmlify(error.ref)), is_html=True) except gitutils.GitCommandError as error: if error.output.splitlines()[0].endswith( "does not appear to be a git repository"): raise OperationFailure( code="invalidremote", title="Invalid remote!", message= ("<code>%s</code> does not appear to be a valid Git repository." % htmlutils.htmlify(remote)), is_html=True) else: raise if upstream.startswith("refs/"): try: with repository.fetchTemporaryFromRemote(remote, upstream) as sha1: upstream_sha1 = repository.keepalive(sha1) except gitutils.GitReferenceError: raise OperationFailure( code="refnotfound", title="Remote ref not found!", message= ("Could not find the ref <code>%s</code> in the repository <code>%s</code>." % (htmlutils.htmlify(upstream), htmlutils.htmlify(remote))), is_html=True) else: try: upstream_sha1 = repository.revparse(upstream) except gitutils.GitReferenceError: raise OperationFailure( code="refnotfound", title="Local ref not found!", message= ("Could not find the ref <code>%s</code> in the repository <code>%s</code>." % (htmlutils.htmlify(upstream), htmlutils.htmlify(str(repository)))), is_html=True) try: resolved_upstream_sha1 = gitutils.getTaggedCommit( repository, upstream_sha1) except gitutils.GitReferenceError: resolved_upstream_sha1 = None if not resolved_upstream_sha1: raise OperationFailure( code="missingcommit", title="Upstream commit is missing!", message=("<p>Could not find the commit <code>%s</code> in the " "repository <code>%s</code>.</p>" "<p>Since it would have been fetched along with the " "branch if it actually was a valid upstream commit, " "this means it's not valid.</p>" % (htmlutils.htmlify(upstream_sha1), htmlutils.htmlify(str(repository)))), is_html=True) commit_sha1s = repository.revlist(included=[head_sha1], excluded=[resolved_upstream_sha1]) if not commit_sha1s: raise OperationFailure( code="emptybranch", title="Branch contains no commits!", message= ("All commits referenced by <code>%s</code> are reachable from <code>%s</code>." % (htmlutils.htmlify(branch), htmlutils.htmlify(upstream))), is_html=True) cursor.execute("SELECT id FROM commits WHERE sha1=ANY (%s)", (commit_sha1s, )) return OperationResult( commit_ids=[commit_id for (commit_id, ) in cursor], head_sha1=head_sha1, upstream_sha1=resolved_upstream_sha1)