Пример #1
0
    def renderBranchName(target):
        target.code("branch").text(review.branch.name, linkify=linkify.Context())

        if repository.name != user.getPreference(db, "defaultRepository"):
            target.text(" in ")
            target.code("repository").text("%s:%s" % (configuration.base.HOSTNAME, repository.path))

        cursor.execute("""SELECT id, remote, remote_name, disabled, previous
                            FROM trackedbranches
                           WHERE repository=%s
                             AND local_name=%s""",
                       (repository.id, review.branch.name))

        row = cursor.fetchone()
        if row:
            trackedbranch_id, remote, remote_name, disabled, previous = row

            target.p("tracking disabled" if disabled else "tracking").text("tracking")

            target.code("branch").text(remote_name, linkify=linkify.Context(remote=remote))
            target.text(" in ")
            target.code("repository").text(remote, linkify=linkify.Context())

            if previous:
                target.span("lastupdate").script(type="text/javascript").text("document.write('(last fetched: ' + shortDate(new Date(%d)) + ')');" % (calendar.timegm(previous.utctimetuple()) * 1000))

            if user in review.owners:
                buttons = target.div("buttons")

                if disabled:
                    buttons.button("enabletracking", onclick="enableTracking(%d);" % trackedbranch_id).text("Enable Tracking")
                else:
                    buttons.button("disabletracking", onclick="triggerUpdate(%d);" % trackedbranch_id).text("Update Now")
                    buttons.button("disabletracking", onclick="disableTracking(%d);" % trackedbranch_id).text("Disable Tracking")
Пример #2
0
def renderShowComment(req, db, user):
    chain_id = req.getParameter("chain", filter=int)
    context_lines = req.getParameter("context",
                                     user.getPreference(
                                         db, "comment.diff.contextLines"),
                                     filter=int)

    default_compact = "yes" if user.getPreference(
        db, "commit.diff.compactMode") else "no"
    compact = req.getParameter("compact", default_compact) == "yes"

    default_tabify = "yes" if user.getPreference(
        db, "commit.diff.visualTabs") else "no"
    tabify = req.getParameter("tabify", default_tabify) == "yes"

    original = req.getParameter("original", "no") == "yes"

    chain = review_comment.CommentChain.fromId(db, chain_id, user)

    if chain is None or chain.state == "empty":
        raise page.utils.DisplayMessage("Invalid comment chain ID: %d" %
                                        chain_id)

    review = chain.review

    document = htmlutils.Document(req)

    html = document.html()
    head = html.head()
    body = html.body()

    document.setTitle("%s in review %s" %
                      (chain.title(False), review.branch.name))

    def renderHeaderItems(target):
        review_utils.renderDraftItems(db, user, review, target)
        target.div("buttons").span("buttonscope buttonscope-global")

    page.utils.generateHeader(body,
                              db,
                              user,
                              renderHeaderItems,
                              extra_links=[("r/%d" % review.id,
                                            "Back to Review")])

    document.addExternalScript("resource/showcomment.js")
    document.addInternalScript(user.getJS(db))
    document.addInternalScript(review.repository.getJS())
    document.addInternalScript(review.getJS())
    document.addInternalScript("var contextLines = %d;" % context_lines)
    document.addInternalScript(
        "var keyboardShortcuts = %s;" %
        (user.getPreference(db, "ui.keyboardShortcuts") and "true" or "false"))

    if not user.isAnonymous() and user.name == req.user:
        document.addInternalScript(
            "$(function () { markChainsAsRead([%d]); });" % chain_id)

    review_html.renderCommentChain(db,
                                   body.div("main"),
                                   user,
                                   review,
                                   chain,
                                   context_lines=context_lines,
                                   compact=compact,
                                   tabify=tabify,
                                   original=original,
                                   linkify=linkify.Context(db=db,
                                                           request=req,
                                                           review=review))

    if user.getPreference(db, "ui.keyboardShortcuts"):
        page.utils.renderShortcuts(body, "showcomment", review=review)

    yield document.render(pretty=not compact)
Пример #3
0
def renderShowComments(req, db, user):
    context_lines = req.getParameter("context",
                                     user.getPreference(
                                         db, "comment.diff.contextLines"),
                                     filter=int)

    default_compact = "yes" if user.getPreference(
        db, "commit.diff.compactMode") else "no"
    compact = req.getParameter("compact", default_compact) == "yes"

    default_tabify = "yes" if user.getPreference(
        db, "commit.diff.visualTabs") else "no"
    tabify = req.getParameter("tabify", default_tabify) == "yes"

    original = req.getParameter("original", "no") == "yes"

    review_id = req.getParameter("review", filter=int)
    batch_id = req.getParameter("batch", None, filter=int)
    filter = req.getParameter("filter", "all")
    blame = req.getParameter("blame", None)

    profiler = profiling.Profiler()

    review = dbutils.Review.fromId(db, review_id)
    review.repository.enableBlobCache()

    cursor = db.cursor()

    profiler.check("create review")

    if blame is not None:
        blame_user = dbutils.User.fromName(db, blame)

        cursor.execute(
            """SELECT commentchains.id
                            FROM commentchains
                            JOIN commentchainlines ON (commentchainlines.chain=commentchains.id)
                            JOIN fileversions ON (fileversions.new_sha1=commentchainlines.sha1)
                            JOIN changesets ON (changesets.id=fileversions.changeset)
                            JOIN commits ON (commits.id=changesets.child)
                            JOIN gitusers ON (gitusers.id=commits.author_gituser)
                            JOIN usergitemails USING (email)
                            JOIN reviewchangesets ON (reviewchangesets.changeset=changesets.id AND reviewchangesets.review=commentchains.review)
                           WHERE commentchains.review=%s
                             AND usergitemails.uid=%s
                             AND commentchains.state!='empty'
                             AND (commentchains.state!='draft' OR commentchains.uid=%s)
                        ORDER BY commentchains.file, commentchainlines.first_line""",
            (review.id, blame_user.id, user.id))

        include_chain_ids = set([chain_id for (chain_id, ) in cursor])

        profiler.check("initial blame filtering")
    else:
        include_chain_ids = None

    if filter == "toread":
        query = """SELECT commentchains.id
                     FROM commentchains
                     JOIN comments ON (comments.chain=commentchains.id)
                     JOIN commentstoread ON (commentstoread.comment=comments.id)
          LEFT OUTER JOIN commentchainlines ON (commentchainlines.chain=commentchains.id)
                    WHERE review=%s
                      AND commentstoread.uid=%s
                 ORDER BY file, first_line"""

        cursor.execute(query, (review.id, user.id))
    else:
        query = """SELECT commentchains.id
                     FROM commentchains
          LEFT OUTER JOIN commentchainlines ON (chain=id)
                    WHERE review=%s
                      AND commentchains.state!='empty'"""

        arguments = [review.id]

        if filter == "issues":
            query += " AND type='issue' AND (commentchains.state!='draft' OR commentchains.uid=%s)"
            arguments.append(user.id)
        elif filter == "draft-issues":
            query += " AND type='issue' AND commentchains.state='draft' AND commentchains.uid=%s"
            arguments.append(user.id)
        elif filter == "open-issues":
            query += " AND type='issue' AND commentchains.state='open'"
        elif filter == "addressed-issues":
            query += " AND type='issue' AND commentchains.state='addressed'"
        elif filter == "closed-issues":
            query += " AND type='issue' AND commentchains.state='closed'"
        elif filter == "notes":
            query += " AND type='note' AND (commentchains.state!='draft' OR commentchains.uid=%s)"
            arguments.append(user.id)
        elif filter == "draft-notes":
            query += " AND type='note' AND commentchains.state='draft' AND commentchains.uid=%s"
            arguments.append(user.id)
        elif filter == "open-notes":
            query += " AND type='note' AND commentchains.state='open'"
        else:
            query += " AND (commentchains.state!='draft' OR commentchains.uid=%s)"
            arguments.append(user.id)

        if batch_id is not None:
            query += " AND batch=%s"
            arguments.append(batch_id)

        # This ordering is inaccurate if comments apply to the same file but
        # different commits, but then, in that case there isn't really a
        # well-defined natural order either.  Two comments that apply to the
        # same file and commit will at least be order by line number, and that's
        # better than nothing.
        query += " ORDER BY file, first_line"

        cursor.execute(query, arguments)

    profiler.check("main query")

    if include_chain_ids is None:
        chain_ids = [chain_id for (chain_id, ) in cursor]
    else:
        chain_ids = [
            chain_id for (chain_id, ) in cursor
            if chain_id in include_chain_ids
        ]

    profiler.check("query result")

    document = htmlutils.Document(req)

    html = document.html()
    head = html.head()
    body = html.body()

    document.addInternalScript(user.getJS(db))
    document.addInternalScript(review.getJS())

    page.utils.generateHeader(
        body,
        db,
        user,
        lambda target: review_utils.renderDraftItems(db, user, review, target),
        extra_links=[("r/%d" % review.id, "Back to Review")])

    profiler.check("page header")

    target = body.div("main")

    if chain_ids and not user.isAnonymous() and user.name == req.user:
        document.addInternalScript(
            "$(function () { markChainsAsRead([%s]); });" %
            ", ".join(map(str, chain_ids)))

    if chain_ids:
        processed = set()

        chains = []
        file_ids = set()
        changesets_files = {}
        changesets = {}

        if blame is not None:
            annotators = {}
            review.branch.loadCommits(db)
            commits = log.commitset.CommitSet(review.branch.commits)

        for chain_id in chain_ids:
            if chain_id in processed:
                continue
            else:
                processed.add(chain_id)

                chain = review_comment.CommentChain.fromId(db,
                                                           chain_id,
                                                           user,
                                                           review=review)
                chains.append(chain)

                if chain.file_id is not None:
                    file_ids.add(chain.file_id)
                    parent, child = review_html.getCodeCommentChainChangeset(
                        db, chain, original)
                    if parent and child:
                        changesets_files.setdefault((parent, child),
                                                    set()).add(chain.file_id)

        profiler.check("load chains")

        changeset_cache = {}

        for (from_commit,
             to_commit), filtered_file_ids in changesets_files.items():
            changesets[(from_commit,
                        to_commit)] = changeset_utils.createChangeset(
                            db,
                            user,
                            review.repository,
                            from_commit=from_commit,
                            to_commit=to_commit,
                            filtered_file_ids=filtered_file_ids)[0]
            profiler.check("create changesets")

            if blame is not None:
                annotators[(from_commit,
                            to_commit)] = operation.blame.LineAnnotator(
                                db,
                                from_commit,
                                to_commit,
                                file_ids=file_ids,
                                commits=commits,
                                changeset_cache=changeset_cache)
                profiler.check("create annotators")

        for chain in chains:
            if blame is not None and chain.file_id is not None:
                try:
                    changeset = changesets[(chain.first_commit,
                                            chain.last_commit)]
                    annotator = annotators[(chain.first_commit,
                                            chain.last_commit)]
                except KeyError:
                    # Most likely a comment created via /showfile.  Such a
                    # comment could be in code that 'blame_user' modified in the
                    # review, but for now, let's skip the comment.
                    continue
                else:
                    file_in_changeset = changeset.getFile(chain.file_id)

                    if not file_in_changeset:
                        continue

                    try:
                        offset, count = chain.lines_by_sha1[
                            file_in_changeset.new_sha1]
                    except KeyError:
                        # Probably a chain raised against the "old" side of the diff.
                        continue
                    else:
                        if not annotator.annotate(chain.file_id,
                                                  offset,
                                                  offset + count - 1,
                                                  check_user=blame_user):
                            continue

            profiler.check("detailed blame filtering")

            if chain.file_id is not None:
                from_commit, to_commit = review_html.getCodeCommentChainChangeset(
                    db, chain, original)
                changeset = changesets.get((from_commit, to_commit))
            else:
                changeset = None

            review_html.renderCommentChain(db,
                                           target,
                                           user,
                                           review,
                                           chain,
                                           context_lines=context_lines,
                                           compact=compact,
                                           tabify=tabify,
                                           original=original,
                                           changeset=changeset,
                                           linkify=linkify.Context(
                                               db=db,
                                               request=req,
                                               review=review))

            profiler.check("rendering")

            yield document.render(
                stop=target, pretty=not compact
            ) + "<script>console.log((new Date).toString());</script>"

            profiler.check("transfer")

        page.utils.renderShortcuts(target, "showcomments", review=review)
    else:
        target.h1(align="center").text("No comments.")

    profiler.output(db, user, document)

    yield document.render(pretty=not compact)
Пример #4
0
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
Пример #5
0
def renderShowReview(req, db, user):
    profiler = profiling.Profiler()

    cursor = db.cursor()

    if user.getPreference(db, "commit.diff.compactMode"): default_compact = "yes"
    else: default_compact = "no"

    compact = req.getParameter("compact", default_compact) == "yes"
    highlight = req.getParameter("highlight", None)

    review_id = req.getParameter("id", filter=int)
    review = dbutils.Review.fromId(db, review_id, load_commits=False, profiler=profiler)

    profiler.check("create review")

    if not review:
        raise page.utils.DisplayMessage, ("Invalid Review ID", "%d is not a valid review ID." % review_id)

    if review.getETag(db, user) == req.getRequestHeader("If-None-Match"):
        raise page.utils.NotModified

    profiler.check("ETag")

    # if usesExperimentalFeature(req, db, review):
    #     def renderMessage(target):
    #         url = "%s/r/%d" % (configuration.URL_PER_TYPE['development'], review.id)

    #         p = target.p(style="padding-top: 1em")
    #         p.text("Sorry, this review uses experimental features currently only available in the development version of Critic.  Because of that, it can only be displayed there.")
    #         p = target.p(style="padding-top: 1em")
    #         p.b().a(href=url).text(url)

    #         yield page.utils.displayMessage(db, req, user, "Experimental Feature Alert!", message=renderMessage)
    #         return

    repository = review.repository

    prefetch_commits = {}

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

    prefetch_commits.update(dict(cursor))

    profiler.check("commits (query)")

    cursor.execute("""SELECT old_head, commits1.sha1, new_head, commits2.sha1, new_upstream, commits3.sha1
                        FROM reviewrebases
             LEFT OUTER JOIN commits AS commits1 ON (commits1.id=old_head)
             LEFT OUTER JOIN commits AS commits2 ON (commits2.id=new_head)
             LEFT OUTER JOIN commits AS commits3 ON (commits3.id=new_upstream)
                       WHERE review=%s""",
                   (review.id,))

    rebases = cursor.fetchall()

    if rebases:
        has_finished_rebases = False

        for old_head_id, old_head_sha1, new_head_id, new_head_sha1, new_upstream_id, new_upstream_sha1 in rebases:
            if old_head_id:
                prefetch_commits[old_head_sha1] = old_head_id
            if new_head_id:
                prefetch_commits[new_head_sha1] = new_head_id
                has_finished_rebases = True
            if new_upstream_id:
                prefetch_commits[new_upstream_sha1] = new_upstream_id

        profiler.check("auxiliary commits (query)")

        if has_finished_rebases:
            cursor.execute("""SELECT commits.sha1, commits.id
                                FROM commits
                                JOIN reachable ON (reachable.commit=commits.id)
                               WHERE branch=%s""",
                           (review.branch.id,))

            prefetch_commits.update(dict(cursor))

            profiler.check("actual commits (query)")

    prefetch_commits = gitutils.FetchCommits(repository, prefetch_commits)

    document = htmlutils.Document(req)

    html = document.html()
    head = html.head()
    body = html.body(onunload="void(0);")

    def flush(target=None):
        return document.render(stop=target, pretty=not compact)

    def renderHeaderItems(target):
        has_draft_items = review_utils.renderDraftItems(db, user, review, target)

        target = target.div("buttons")

        if not has_draft_items:
            if review.state == "open":
                if review.accepted(db):
                    target.button(id="closeReview", onclick="closeReview();").text("Close Review")
                else:
                    if user in review.owners or user.getPreference(db, "review.pingAnyReview"):
                        target.button(id="pingReview", onclick="pingReview();").text("Ping Review")
                    if user in review.owners or user.getPreference(db, "review.dropAnyReview"):
                        target.button(id="dropReview", onclick="dropReview();").text("Drop Review")

                if user in review.owners and not review.description:
                    target.button(id="writeDescription", onclick="editDescription();").text("Write Description")
            else:
                target.button(id="reopenReview", onclick="reopenReview();").text("Reopen Review")

        target.span("buttonscope buttonscope-global")

    profiler.check("prologue")

    page.utils.generateHeader(body, db, user, renderHeaderItems)

    cursor.execute("SELECT 1 FROM fullreviewuserfiles WHERE review=%s AND state='pending' AND assignee=%s", (review.id, user.id))
    hasPendingChanges = bool(cursor.fetchone())

    if hasPendingChanges:
        head.setLink("next", "showcommit?review=%d&filter=pending" % review.id)

    profiler.check("header")

    document.addExternalStylesheet("resource/showreview.css")
    document.addExternalStylesheet("resource/review.css")
    document.addExternalStylesheet("resource/comment.css")
    document.addExternalScript("resource/showreview.js")
    document.addExternalScript("resource/review.js")
    document.addExternalScript("resource/comment.js")
    document.addExternalScript("resource/autocomplete.js")
    document.addInternalScript(user.getJS())
    document.addInternalScript("var owners = [ %s ];" % ", ".join(owner.getJSConstructor() for owner in review.owners))
    document.addInternalScript("var updateCheckInterval = %d;" % user.getPreference(db, "review.updateCheckInterval"));

    log.html.addResources(document)

    document.addInternalScript(review.getJS())

    target = body.div("main")

    basic = target.table('paleyellow basic', align='center')
    basic.col(width='10%')
    basic.col(width='60%')
    basic.col(width='30%')
    h1 = basic.tr().td('h1', colspan=3).h1()
    h1.text("r/%d: " % review.id)
    h1.span(id="summary").text("%s" % review.summary, linkify=linkify.Context(db=db, review=review))
    h1.a("edit", href="javascript:editSummary();").text("[edit]")

    def linkToCommit(commit):
        cursor.execute("SELECT 1 FROM commits JOIN changesets ON (child=commits.id) JOIN reviewchangesets ON (changeset=changesets.id) WHERE sha1=%s AND review=%s", (commit.sha1, review.id))
        if cursor.fetchone():
            return "%s/%s?review=%d" % (review.repository.name, commit.sha1, review.id)
        return "%s/%s" % (review.repository.name, commit.sha1)

    def row(heading, value, help, right=None, linkify=False, cellId=None):
        main_row = basic.tr('line')
        main_row.td('heading').text("%s:" % heading)
        if right is False: colspan = 2
        else: colspan = None
        if callable(value): value(main_row.td('value', id=cellId, colspan=colspan).preformatted())
        else: main_row.td('value', id=cellId, colspan=colspan).preformatted().text(value, linkify=linkify, repository=review.repository)
        if right is False: pass
        elif callable(right): right(main_row.td('right', valign='bottom'))
        else: main_row.td('right').text()
        if help: basic.tr('help').td('help', colspan=3).text(help)

    def renderBranchName(target):
        target.code("branch").text(review.branch.name, linkify=linkify.Context())

        if repository.name != user.getPreference(db, "defaultRepository"):
            target.text(" in ")
            target.code("repository").text("%s:%s" % (configuration.base.HOSTNAME, repository.path))

        cursor.execute("""SELECT id, remote, remote_name, disabled, previous
                            FROM trackedbranches
                           WHERE repository=%s
                             AND local_name=%s""",
                       (repository.id, review.branch.name))

        row = cursor.fetchone()
        if row:
            trackedbranch_id, remote, remote_name, disabled, previous = row

            target.p("tracking disabled" if disabled else "tracking").text("tracking")

            target.code("branch").text(remote_name, linkify=linkify.Context(remote=remote))
            target.text(" in ")
            target.code("repository").text(remote, linkify=linkify.Context())

            if previous:
                target.span("lastupdate").script(type="text/javascript").text("document.write('(last fetched: ' + shortDate(new Date(%d)) + ')');" % (calendar.timegm(previous.utctimetuple()) * 1000))

            if user in review.owners:
                buttons = target.div("buttons")

                if disabled:
                    buttons.button("enabletracking", onclick="enableTracking(%d);" % trackedbranch_id).text("Enable Tracking")
                else:
                    buttons.button("disabletracking", onclick="triggerUpdate(%d);" % trackedbranch_id).text("Update Now")
                    buttons.button("disabletracking", onclick="disableTracking(%d);" % trackedbranch_id).text("Disable Tracking")

    def renderReviewers(target):
        if review.reviewers:
            for index, reviewer in enumerate(review.reviewers):
                if index != 0: target.text(", ")
                span = target.span("user %s" % reviewer.status)
                span.span("name").text(reviewer.fullname)
                if reviewer.status == 'absent':
                    span.span("status").text(" (%s)" % reviewer.getAbsence(db))
                elif reviewer.status == 'retired':
                    span.span("status").text(" (retired)")
        else:
            target.i().text("No reviewers.")

        cursor.execute("""SELECT reviewfilters.id, reviewfilters.uid, reviewfilters.directory, reviewfilters.file
                            FROM reviewfilters
                            JOIN users ON (reviewfilters.uid=users.id)
                           WHERE reviewfilters.review=%s
                             AND reviewfilters.type='reviewer'
                             AND users.status!='retired'""",
                       (review.id,))

        rows = cursor.fetchall()
        reviewer_filters_hidden = []

        if rows:
            table = target.table("reviewfilters reviewers")

            row = table.thead().tr("h1")
            row.th("h1", colspan=4).text("Custom filters:")

            filter_data = {}
            reviewfilters = {}

            for filter_id, user_id, directory_id, file_id in rows:
                filter_user = dbutils.User.fromId(db, user_id)

                if file_id: path = dbutils.describe_file(db, file_id)
                else: path = dbutils.describe_directory(db, directory_id) + "/"

                reviewfilters.setdefault(filter_user.fullname, []).append(path)
                filter_data[(filter_user.fullname, path)] = (filter_id, filter_user)

            count = 0
            tbody = table.tbody()

            for fullname in sorted(reviewfilters.keys()):
                original_paths = sorted(reviewfilters[fullname])
                trimmed_paths = diff.File.eliminateCommonPrefixes(original_paths[:])

                first = True

                for original_path, trimmed_path in zip(original_paths, trimmed_paths):
                    row = tbody.tr("filter")

                    if first:
                        row.td("username", rowspan=len(original_paths)).text(fullname)
                        row.td("reviews", rowspan=len(original_paths)).text("reviews")
                        first = False

                    row.td("path").span().innerHTML(trimmed_path)

                    filter_id, filter_user = filter_data[(fullname, original_path)]

                    href = "javascript:removeReviewFilter(%d, %s, 'reviewer', %s, %s);" % (filter_id, filter_user.getJSConstructor(), htmlutils.jsify(original_path), "true" if filter_user != user else "false")
                    row.td("remove").a(href=href).text("[remove]")

                    count += 1

            tfoot = table.tfoot()
            tfoot.tr().td(colspan=4).text("%d line%s hidden" % (count, "s" if count > 1 else ""))

            if count > 10:
                tbody.setAttribute("class", "hidden")
                reviewer_filters_hidden.append(True)
            else:
                tfoot.setAttribute("class", "hidden")
                reviewer_filters_hidden.append(False)

        buttons = target.div("buttons")

        if reviewer_filters_hidden:
            buttons.button("showfilters", onclick="toggleReviewFilters('reviewers', $(this));").text("%s Custom Filters" % ("Show" if reviewer_filters_hidden[0] else "Hide"))

        if review.applyfilters and review.repository.parent and not review.applyparentfilters:
            buttons.button("applyparentfilters", onclick="applyParentFilters();").text("Apply Upstream Filters")

        buttons.button("addreviewer", onclick="addReviewer();").text("Add Reviewer")
        buttons.button("manage", onclick="location.href='managereviewers?review=%d';" % review.id).text("Manage Assignments")

    def renderWatchers(target):
        if review.watchers:
            for index, watcher in enumerate(review.watchers):
                if index != 0: target.text(", ")
                span = target.span("user %s" % watcher.status)
                span.span("name").text(watcher.fullname)
                if watcher.status == 'absent':
                    span.span("status").text(" (%s)" % watcher.getAbsence(db))
                elif watcher.status == 'retired':
                    span.span("status").text(" (retired)")
        else:
            target.i().text("No watchers.")

        cursor.execute("""SELECT reviewfilters.id, reviewfilters.uid, reviewfilters.directory, reviewfilters.file
                            FROM reviewfilters
                            JOIN users ON (reviewfilters.uid=users.id)
                           WHERE reviewfilters.review=%s
                             AND reviewfilters.type='watcher'
                             AND users.status!='retired'""",
                       (review.id,))

        rows = cursor.fetchall()
        watcher_filters_hidden = []

        if rows:
            table = target.table("reviewfilters watchers")

            row = table.thead().tr("h1")
            row.th("h1", colspan=4).text("Custom filters:")

            filter_data = {}
            reviewfilters = {}

            for filter_id, user_id, directory_id, file_id in rows:
                filter_user = dbutils.User.fromId(db, user_id)

                if file_id: path = dbutils.describe_file(db, file_id)
                else: path = dbutils.describe_directory(db, directory_id) + "/"

                reviewfilters.setdefault(filter_user.fullname, []).append(path)
                filter_data[(filter_user.fullname, path)] = (filter_id, filter_user)

            count = 0
            tbody = table.tbody()

            for fullname in sorted(reviewfilters.keys()):
                original_paths = sorted(reviewfilters[fullname])
                trimmed_paths = diff.File.eliminateCommonPrefixes(original_paths[:])

                first = True

                for original_path, trimmed_path in zip(original_paths, trimmed_paths):
                    row = tbody.tr("filter")

                    if first:
                        row.td("username", rowspan=len(original_paths)).text(fullname)
                        row.td("reviews", rowspan=len(original_paths)).text("watches")
                        first = False

                    row.td("path").span().innerHTML(trimmed_path)

                    filter_id, filter_user = filter_data[(fullname, original_path)]

                    href = "javascript:removeReviewFilter(%d, %s, 'watcher', %s, %s);" % (filter_id, filter_user.getJSConstructor(), htmlutils.jsify(original_path), "true" if filter_user != user else "false")
                    row.td("remove").a(href=href).text("[remove]")

                    count += 1

            tfoot = table.tfoot()
            tfoot.tr().td(colspan=4).text("%d line%s hidden" % (count, "s" if count > 1 else ""))

            if count > 10:
                tbody.setAttribute("class", "hidden")
                watcher_filters_hidden.append(True)
            else:
                tfoot.setAttribute("class", "hidden")
                watcher_filters_hidden.append(False)

        buttons = target.div("buttons")

        if watcher_filters_hidden:
            buttons.button("showfilters", onclick="toggleReviewFilters('watchers', $(this));").text("%s Custom Filters" % ("Show" if watcher_filters_hidden[0] else "Hide"))

        buttons.button("addwatcher", onclick="addWatcher();").text("Add Watcher")

        if user not in review.reviewers and user not in review.owners:
            if user not in review.watchers:
                buttons.button("watch", onclick="watchReview();").text("Watch Review")
            elif review.watchers[user] == "manual":
                buttons.button("watch", onclick="unwatchReview();").text("Stop Watching Review")

    def renderEditOwners(target):
        target.button("description", onclick="editOwners();").text("Edit Owners")

    def renderEditDescription(target):
        target.button("description", onclick="editDescription();").text("Edit Description")

    def renderRecipientList(target):
        cursor.execute("SELECT uid, fullname, include FROM reviewrecipientfilters JOIN users ON (uid=id) WHERE review=%s", (review.id,))

        default_include = True
        included = dict((owner.fullname, owner.id) for owner in review.owners)
        excluded = {}

        for user_id, fullname, include in cursor:
            if user_id == 0: default_include = include
            elif include: included[fullname] = user_id
            elif user_id not in review.owners: excluded[fullname] = user_id

        mode = None
        users = None

        buttons = []
        opt_in_button = False
        opt_out_button = False

        if default_include:
            if excluded:
                mode = "Everyone except "
                users = excluded
                opt_out_button = user.fullname not in excluded
                opt_in_button = not opt_out_button
            else:
                mode = "Everyone."
                opt_out_button = True
        else:
            if included:
                mode = "No-one except "
                users = included
                opt_in_button = user.fullname not in included
                opt_out_button = not opt_in_button
            else:
                mode = "No-one at all."
                opt_in_button = True

        if user in review.owners or user in review.reviewers or user in review.watchers:
            if opt_in_button:
                buttons.append(("Include me, please!", "includeRecipient(%d);" % user.id))
            if opt_out_button:
                buttons.append(("Exclude me, please!", "excludeRecipient(%d);" % user.id))

        target.span("mode").text(mode)

        if users:
            container = target.span("users")

            first = True
            for fullname in sorted(users.keys()):
                if first: first = False
                else: container.text(", ")

                container.span("user", critic_user_id=users[fullname]).text(fullname)

            container.text(".")

        if buttons:
            container = target.div("buttons")

            for label, onclick in buttons:
                container.button(onclick=onclick).text(label)

    row("Branch", renderBranchName, "The branch containing the commits to review.", right=False)
    row("Owner%s" % ("s" if len(review.owners) > 1 else ""), ", ".join(owner.fullname for owner in review.owners), "The users who created and/or owns the review.", right=renderEditOwners)
    if review.description:
        row("Description", review.description, "A longer description of the changes to be reviewed.", linkify=linkToCommit, cellId="description", right=renderEditDescription)
    row("Reviewers", renderReviewers, "Users responsible for reviewing the changes in this review.", right=False)
    row("Watchers", renderWatchers, "Additional users who receive e-mails about updates to this review.", right=False)
    row("Recipient List", renderRecipientList, "Users (among the reviewers and watchers) who will receive any e-mails about the review.", right=False)

    profiler.check("basic")

    review_state = review.getReviewState(db)

    profiler.check("review state")

    progress = target.table('paleyellow progress', align='center')
    progress_header = progress.tr().td('h1', colspan=3).h1()
    progress_header.text("Review Progress")
    progress_header_right = progress_header.span("right")
    progress_header_right.text("Display log: ")
    progress_header_right.a(href="showreviewlog?review=%d&granularity=module" % review.id).text("[per module]")
    progress_header_right.text()
    progress_header_right.a(href="showreviewlog?review=%d&granularity=file" % review.id).text("[per file]")
    progress_h1 = progress.tr().td('percent', colspan=3).h1()

    title_data = { 'id': 'r/%d' % review.id,
                   'summary': review.summary,
                   'progress': str(review_state) }

    if review.state == "closed":
        progress_h1.img(src=htmlutils.getStaticResourceURI("seal-of-approval-left.png"),
                        style="position: absolute; margin-left: -80px; margin-top: -100px")
        progress_h1.text("Finished!")
    elif review.state == "dropped":
        progress_h1.text("Dropped...")
    elif review.state == "open" and review_state.accepted:
        progress_h1.img(src=htmlutils.getStaticResourceURI("seal-of-approval-left.png"),
                        style="position: absolute; margin-left: -80px; margin-top: -100px")
        progress_h1.text("Accepted!")
        progress_h1.div().span("remark").text("Hurry up and close it before anyone has a change of heart.")
    else:
        progress_h1.text(review_state.getProgress())

        if review_state.issues:
            progress_h1.span("comments").text(" and ")
            progress_h1.text("%d" % review_state.issues)
            progress_h1.span("comments").text(" issue%s" % (review_state.issues > 1 and "s" or ""))

        if review_state.getPercentReviewed() != 100.0:
            cursor = db.cursor()
            cursor.execute("""SELECT 1
                                FROM reviewfiles
                     LEFT OUTER JOIN reviewuserfiles ON (reviewuserfiles.file=reviewfiles.id)
                               WHERE reviewfiles.review=%s
                                 AND reviewfiles.state='pending'
                                 AND reviewuserfiles.uid IS NULL""",
                           (review.id,))

            if cursor.fetchone():
                progress.tr().td('stuck', colspan=3).a(href="showreviewlog?review=%d&granularity=file&unassigned=yes" % review.id).text("Not all changes have a reviewer assigned!")

            cursor.execute("""SELECT uid, MIN(reviewuserfiles.time)
                                FROM reviewfiles
                                JOIN reviewuserfiles ON (reviewuserfiles.file=reviewfiles.id)
                               WHERE reviewfiles.review=%s
                                 AND reviewfiles.state='pending'
                            GROUP BY reviewuserfiles.uid""",
                           (review.id,))

            def total_seconds(delta):
                return delta.days * 60 * 60 * 24 + delta.seconds

            now = datetime.datetime.now()
            pending_reviewers = [(dbutils.User.fromId(db, user_id), total_seconds(now - timestamp)) for (user_id, timestamp) in cursor.fetchall() if total_seconds(now - timestamp) > 60 * 60 * 8]

            if pending_reviewers:
                progress.tr().td('stragglers', colspan=3).text("Needs review from")
                for reviewer, seconds in pending_reviewers:
                    if reviewer.status == 'retired': continue
                    elif reviewer.status == 'absent': warning = " absent"
                    elif not reviewer.getPreference(db, "email.activated"): warning = " no-email"
                    else: warning = ""

                    if seconds < 60 * 60 * 24:
                        hours = seconds / (60 * 60)
                        duration = " (%d hour%s)" % (hours, "s" if hours > 1 else "")
                    elif seconds < 60 * 60 * 24 * 7:
                        days = seconds / (60 * 60 * 24)
                        duration = " (%d day%s)" % (days, "s" if days > 1 else "")
                    elif seconds < 60 * 60 * 24 * 30:
                        weeks = seconds / (60 * 60 * 24 * 7)
                        duration = " (%d week%s)" % (weeks, "s" if weeks > 1 else "")
                    else:
                        duration = " (wake up!)"

                    progress.tr().td('straggler' + warning, colspan=3).text("%s%s" % (reviewer.fullname, duration))
                if user in review.owners:
                    progress.tr().td('pinging', colspan=3).span().text("Send a message to these users by pinging the review.")

    title_format = user.getPreference(db, 'ui.title.showReview')

    try:
        document.setTitle(title_format % title_data)
    except Exception, exc:
        document.setTitle(traceback.format_exception_only(type(exc), exc)[0].strip())