Пример #1
0
def renderBranches(req, db, user):
    offset = req.getParameter("offset", 0, filter=int)
    count = req.getParameter("count", 50, filter=int)

    cursor = db.cursor()

    repository = req.getParameter("repository", None, gitutils.Repository.FromParameter(db))

    if not repository:
        repository = user.getDefaultRepository(db)

    all_branches = []
    commit_times = []

    if repository:
        cursor.execute(
            """SELECT branches.id, branches.name, commits.commit_time
                            FROM branches
                            JOIN repositories ON (repositories.id=branches.repository)
                            JOIN commits ON (commits.id=branches.head)
                           WHERE branches.type='normal'
                             AND branches.name NOT LIKE 'replay/%%'
                             AND repositories.id=%s
                        ORDER BY commits.commit_time DESC LIMIT %s""",
            (repository.id, count),
        )

        for branch_id, branch_name, commit_time in cursor.fetchall():
            all_branches.append(branch_id)
            commit_times.append(commit_time.timetuple())

    document = htmlutils.Document(req)

    if repository:
        document.setTitle("Branches in %s" % repository.name)
    else:
        document.setTitle("Branches")

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

    page.utils.generateHeader(body, db, user, current_page="branches")

    document.addExternalScript("resource/branches.js")

    if repository:
        document.addInternalScript(repository.getJS())

    extraColumns = [ExtraColumn("when", "When", lambda target, index: log.html.renderWhen(target, commit_times[index]))]

    render(db, body.div("main"), "All Branches", repository, all_branches[offset:], extraColumns=extraColumns)

    return document
Пример #2
0
def renderSelectSource(req, db, user):
    cursor = db.cursor()

    document = htmlutils.Document(req)
    html = document.html()
    head = html.head()
    body = html.body()

    page.utils.generateHeader(body, db, user, current_page="createreview")

    document.addExternalStylesheet("resource/createreview.css")
    document.addExternalScript("resource/createreview.js")

    document.addInternalScript(user.getJS(db))
    document.setTitle("Create Review")

    target = body.div("main")
    table = page.utils.PaleYellowTable(target, "Create Review")
    table.titleRight.innerHTML("Step 1")

    default_repository = user.getPreference(db, "defaultRepository")
    default_remotes = {}
    default_branches = {}

    def renderLocalRepository(target):
        repositories = target.select("repository")

        cursor.execute(
            """SELECT repositories.id, repositories.name, repositories.path, branches.name
                            FROM repositories
                 LEFT OUTER JOIN branches ON (branches.id=repositories.branch)
                        ORDER BY id"""
        )

        for repository_id, name, path, branch_name in cursor:
            option = repositories.option(
                "repository", value=name, selected="selected" if name == default_repository else None
            )
            option.text("%s [%s:%s]" % (name, configuration.base.HOSTNAME, path))

            local_names = ["*"]

            if branch_name:
                local_names.append(branch_name)

            cursor.execute(
                """SELECT remote
                                FROM trackedbranches
                               WHERE repository=%s
                                 AND local_name=ANY (%s)
                            ORDER BY local_name
                               LIMIT 1""",
                (repository_id, local_names),
            )

            def splitRemote(remote):
                if remote.startswith("git://"):
                    host, path = remote[6:].split("/", 1)
                    host = "git://" + host
                else:
                    host, path = remote.split(":", 1)
                return host, path

            row = cursor.fetchone()

            if row:
                default_remotes[name] = splitRemote(row[0])
            else:
                default_remotes[name] = None

            default_branches[name] = branch_name

        document.addInternalScript("var default_remotes = %s;" % json_encode(default_remotes))
        document.addInternalScript("var default_branches = %s;" % json_encode(default_branches))

    def renderRemoteRepository(target):
        host = target.p("remotehost")
        host.text("Host: ")
        hosts = host.select("remotehost")

        cursor.execute("SELECT name, path FROM knownhosts ORDER BY id")

        default_remote = default_remotes.get(default_repository)

        for name, path in cursor:
            option = hosts.option(
                "remotehost",
                value=name,
                critic_default_path=path,
                selected="selected" if default_remote and default_remote[0] == name else None,
            )
            option.text(name)

        path = target.p("remotepath")
        path.text("Path: ")
        path.input("remotepath", value=default_remote[1] if default_remote else None)

    def renderWorkBranch(target):
        target.input("workbranch")

    def renderUpstreamBranch(target):
        target.input("upstreambranch", value=default_branches[default_repository])

    table.addItem("Local Repository", renderLocalRepository, "Critic repository to create review in.")
    table.addItem("Remote Repository", renderRemoteRepository, "Remote repository to fetch commits from.")
    table.addItem("Work Branch", renderWorkBranch, "Work branch containing commits to create review of.")
    table.addItem("Upstream Branch", renderUpstreamBranch, "Upstream branch off of which the work branch was branched.")

    def renderButtons(target):
        target.button("fetchbranch").text("Fetch Branch")

    table.addCentered(renderButtons)

    return document
Пример #3
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
Пример #4
0
def renderSelectSource(req, db, user):
    cursor = db.cursor()

    document = htmlutils.Document(req)
    html = document.html()
    head = html.head()
    body = html.body()

    page.utils.generateHeader(body, db, user, current_page="createreview")

    document.addExternalStylesheet("resource/createreview.css")
    document.addExternalScript("resource/createreview.js")
    document.addExternalScript("resource/autocomplete.js")

    document.addInternalScript(user.getJS(db))
    document.setTitle("Create Review")

    target = body.div("main")
    table = page.utils.PaleYellowTable(target, "Create Review")
    table.titleRight.innerHTML("Step 1")

    default_repository = user.getPreference(db, "defaultRepository")
    default_remotes = {}
    default_branches = {}

    def renderLocalRepository(target):
        page.utils.generateRepositorySelect(db, user, target)

        cursor.execute("""SELECT repositories.id, repositories.name, repositories.path
                            FROM repositories
                        ORDER BY repositories.id""")

        for repository_id, name, path in cursor.fetchall():
            def findRemote(local_name):
                cursor.execute("""SELECT remote
                                    FROM trackedbranches
                                   WHERE repository=%s
                                     AND local_name=%s""",
                               (repository_id, local_name))
                row = cursor.fetchone()
                if row:
                    return row[0]

            repository = gitutils.Repository.fromId(db, repository_id)
            remote = branch_name = None

            for branch in repository.getSignificantBranches(db):
                remote = findRemote(branch.name)
                if remote:
                    branch_name = branch.name
                    break

            if not remote:
                remote = findRemote("*")

            default_remotes[name] = remote
            default_branches[name] = branch_name

        document.addInternalScript("var default_remotes = %s;" % json_encode(default_remotes))
        document.addInternalScript("var default_branches = %s;" % json_encode(default_branches))

    def renderRemoteRepository(target):
        target.input("remote", value=default_remotes.get(default_repository))

    def renderWorkBranch(target):
        target.text("refs/heads/")
        target.input("workbranch")

    def renderUpstreamCommit(target):
        default_branch = default_branches.get(default_repository)
        target.input("upstreamcommit", value=("refs/heads/%s" % default_branch) if default_branch else "")

    table.addItem("Local Repository", renderLocalRepository, "Critic repository to create review in.")
    table.addItem("Remote Repository", renderRemoteRepository, "Remote repository to fetch commits from.")
    table.addItem("Work Branch", renderWorkBranch, "Work branch (in remote repository) containing commits to create review of.")
    table.addItem("Upstream Commit", renderUpstreamCommit, "Upstream commit from which the work branch was branched.")

    def renderButtons(target):
        target.button("fetchbranch").text("Fetch Branch")

    table.addCentered(renderButtons)

    return document
Пример #5
0
def renderSelectSource(req, db, user):
    cursor = db.cursor()

    document = htmlutils.Document(req)
    html = document.html()
    head = html.head()
    body = html.body()

    page.utils.generateHeader(body, db, user, current_page="createreview")

    document.addExternalStylesheet("resource/createreview.css")
    document.addExternalScript("resource/createreview.js")

    document.addInternalScript(user.getJS(db))
    document.setTitle("Create Review")

    target = body.div("main")
    table = page.utils.PaleYellowTable(target, "Create Review")
    table.titleRight.innerHTML("Step 1")

    default_repository = user.getPreference(db, "defaultRepository")
    default_remotes = {}
    default_branches = {}

    def renderLocalRepository(target):
        repositories = target.select("repository")

        cursor.execute(
            """SELECT repositories.id, repositories.name, repositories.path, branches.name
                            FROM repositories
                 LEFT OUTER JOIN branches ON (branches.id=repositories.branch)
                        ORDER BY id""")

        for repository_id, name, path, branch_name in cursor:
            option = repositories.option(
                "repository",
                value=name,
                selected="selected" if name == default_repository else None)
            option.text("%s [%s:%s]" %
                        (name, configuration.base.HOSTNAME, path))

            local_names = ["*"]

            if branch_name:
                local_names.append(branch_name)

            cursor.execute(
                """SELECT remote
                                FROM trackedbranches
                               WHERE repository=%s
                                 AND local_name=ANY (%s)
                            ORDER BY local_name
                               LIMIT 1""", (repository_id, local_names))

            def splitRemote(remote):
                if remote.startswith("git://"):
                    host, path = remote[6:].split("/", 1)
                    host = "git://" + host
                else:
                    host, path = remote.split(":", 1)
                return host, path

            row = cursor.fetchone()

            if row: default_remotes[name] = splitRemote(row[0])
            else: default_remotes[name] = None

            default_branches[name] = branch_name

        document.addInternalScript("var default_remotes = %s;" %
                                   json_encode(default_remotes))
        document.addInternalScript("var default_branches = %s;" %
                                   json_encode(default_branches))

    def renderRemoteRepository(target):
        host = target.p("remotehost")
        host.text("Host: ")
        hosts = host.select("remotehost")

        cursor.execute("SELECT name, path FROM knownhosts ORDER BY id")

        default_remote = default_remotes.get(default_repository)

        for name, path in cursor:
            option = hosts.option("remotehost",
                                  value=name,
                                  critic_default_path=path,
                                  selected="selected" if default_remote
                                  and default_remote[0] == name else None)
            option.text(name)

        path = target.p("remotepath")
        path.text("Path: ")
        path.input("remotepath",
                   value=default_remote[1] if default_remote else None)

    def renderWorkBranch(target):
        target.input("workbranch")

    def renderUpstreamCommit(target):
        default_branch = default_branches.get(default_repository)
        target.input("upstreamcommit",
                     value=("refs/heads/%s" %
                            default_branch) if default_branch else "")

    table.addItem("Local Repository", renderLocalRepository,
                  "Critic repository to create review in.")
    table.addItem("Remote Repository", renderRemoteRepository,
                  "Remote repository to fetch commits from.")
    table.addItem(
        "Work Branch", renderWorkBranch,
        "Work branch (in remote repository) containing commits to create review of."
    )
    table.addItem("Upstream Commit", renderUpstreamCommit,
                  "Upstream commit from which the work branch was branched.")

    def renderButtons(target):
        target.button("fetchbranch").text("Fetch Branch")

    table.addCentered(renderButtons)

    return document
Пример #6
0
def renderConfirmMerge(req, db, user):
    confirmation_id = req.getParameter("id", filter=int)
    tail_sha1 = req.getParameter("tail", None)
    do_confirm = req.getParameter("confirm", "no") == "yes"
    do_cancel = req.getParameter("cancel", "no") == "yes"

    cursor = db.cursor()

    cursor.execute(
        "SELECT review, uid, merge, confirmed, tail FROM reviewmergeconfirmations WHERE id=%s",
        (confirmation_id, ))
    row = cursor.fetchone()
    if not row:
        raise page.utils.DisplayMessage("No pending merge with that id.")
    review_id, user_id, merge_id, confirmed, tail_id = row

    review = dbutils.Review.fromId(db, review_id)
    merge = gitutils.Commit.fromId(db, review.repository, merge_id)

    if confirmed and tail_id is not None:
        tail_sha1 = gitutils.Commit.fromId(db, review.repository, tail_id).sha1

    cursor.execute("SELECT merged FROM reviewmergecontributions WHERE id=%s",
                   (confirmation_id, ))

    merged = [
        gitutils.Commit.fromId(db, review.repository, merged_id)
        for (merged_id, ) in cursor
    ]
    merged_set = log.commitset.CommitSet(merged)

    if tail_sha1 is not None:
        tail = gitutils.Commit.fromSHA1(db, review.repository, tail_sha1)
        tail_id = tail.getId(db)

        cut = [
            gitutils.Commit.fromSHA1(db, review.repository, sha1)
            for sha1 in tail.parents if sha1 in merged_set
        ]
        merged_set = merged_set.without(cut)
        merged = list(merged_set)
    else:
        tail_id = None

    if do_confirm:
        cursor.execute(
            "UPDATE reviewmergeconfirmations SET confirmed=TRUE, tail=%s WHERE id=%s",
            (tail_id, confirmation_id))
        db.commit()
    elif do_cancel:
        cursor.execute("DELETE FROM reviewmergeconfirmations WHERE id=%s",
                       (confirmation_id, ))
        db.commit()

    document = htmlutils.Document(req)

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

    def renderButtons(target):
        if not do_confirm and not do_cancel:
            target.button("confirmAll").text("Confirm (merge + contributed)")
            target.button("confirmNone").text("Confirm (merge only)")
            target.button("cancel").text("Cancel")

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

    document.addExternalStylesheet("resource/confirmmerge.css")
    document.addExternalScript("resource/log.js")
    document.addExternalScript("resource/confirmmerge.js")
    document.addInternalScript(user.getJS())
    document.addInternalScript(review.getJS())
    document.addInternalScript("var confirmation_id = %d;" % confirmation_id)
    document.addInternalScript("var merge_sha1 = %s;" %
                               htmlutils.jsify(merge.sha1))

    if tail_sha1 is not None:
        document.addInternalScript("var tail_sha1 = %s;" %
                                   htmlutils.jsify(tail_sha1))

    if not do_confirm and not do_cancel:
        heads = merged_set.getHeads()
        if heads:
            document.addInternalScript("var automaticAnchorCommit = %s;" %
                                       htmlutils.jsify(heads.pop().sha1))
        else:
            document.addInternalScript("var automaticAnchorCommit = null;")

    if do_confirm:
        document.addInternalScript("var confirmed = true;")
    else:
        document.addInternalScript("var confirmed = false;")

    target = body.div("main")

    basic = target.table("paleyellow")
    basic.col(width='15%')
    basic.col(width='55%')
    basic.col(width='30%')
    h1 = basic.tr().td('h1', colspan=3).h1()

    if do_confirm:
        h1.text("CONFIRMED MERGE")
    elif do_cancel:
        h1.text("CANCELLED MERGE")
    else:
        h1.text("Confirm Merge")

    row = basic.tr("sha1")
    row.td("heading").text("SHA-1:")
    row.td("value").preformatted().text(merge.sha1)
    row.td().text()

    row = basic.tr("message")
    row.td("heading").text("Message:")
    row.td("value").preformatted().text(merge.message)
    row.td().text()

    if not do_confirm and not do_cancel:
        row = basic.tr("instructions")
        row.td("heading").text("Instructions:")
        row.td("value").preformatted().text("""\
Use the top right buttons to confirm the merge with or without the contributed commits that it brings.

By clicking 'Confirm (merge + contributed)' you will bring the merge commit plus all commits that it contributes into the this code review.

By clicking 'Confirm (merge only)' you will bring only the merge commit itself into the code review and not the contributed commits."

By clicking 'Cancel' you will abort the merge. The code review will not be modified at all from its current state."""
                                            )
        row.td().text()

    if merged:
        columns = [(10, log.html.WhenColumn()), (60, log.html.SummaryColumn()),
                   (16, log.html.AuthorColumn())]

        log.html.render(db,
                        target,
                        "Contributed Commits",
                        commits=merged,
                        columns=columns)

    return document
Пример #7
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")

    repository = review.repository

    prefetch_commits = {}

    cursor.execute("""SELECT DISTINCT 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, profiler=profiler)

    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/reviewfilters.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 inset").text(review.branch.name, linkify=linkify.Context())

        if repository.name != user.getPreference(db, "defaultRepository"):
            target.text(" in ")
            target.code("repository inset").text(repository.getURL(db, user))

        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 inset").text(remote_name, linkify=linkify.Context(remote=remote))
            target.text(" in ")
            target.code("repository inset").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 or user.hasRole(db, "administrator"):
                buttons = target.div("buttons")

                if review.state == "open":
                    if disabled:
                        button = buttons.button("enabletracking",
                                                onclick=("enableTracking(%d, %s, %s);"
                                                         % (trackedbranch_id,
                                                            htmlutils.jsify(remote),
                                                            htmlutils.jsify(remote_name))))
                        button.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")

                    buttons.button("rebasereview", onclick="location.assign('/rebasetrackingreview?review=%d');" % review.id).text("Rebase Review")

    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.path
                            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, path in rows:
                filter_user = dbutils.User.fromId(db, user_id)
                path = path or '/'
                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 not review.applyfilters:
            buttons.button("applyfilters", onclick="applyFilters('global');").text("Apply Global Filters")

        if review.applyfilters and review.repository.parent and not review.applyparentfilters:
            buttons.button("applyparentfilters", onclick="applyFilters('upstream');").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.path
                            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, path in rows:
                filter_user = dbutils.User.fromId(db, user_id)
                path = path or '/'
                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
                 LEFT OUTER 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 is None: 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 not in review.owners and (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!")

        if review.repository.hasMainBranch():
            main_branch = review.repository.getMainBranch(db)
            if review.branch.getHead(db).isAncestorOf(main_branch.getHead(db)):
                remark = progress_h1.div().span("remark")
                remark.text("Merged to ")
                remark.a(href="/log?repository=%s&branch=%s" % (review.repository.name, main_branch.name)).text(main_branch.name)
                remark.text(".")
    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 as exc:
        document.setTitle(traceback.format_exception_only(type(exc), exc)[0].strip())

    profiler.check("progress")

    check = profiler.start("ApprovalColumn.fillCache")

    def linkToCommit(commit, overrides={}):
        if "rebase_conflicts" in overrides:
            return "%s..%s?review=%d&conflicts=yes" % (overrides["rebase_conflicts"].sha1[:8], commit.sha1[:8], review.id)
        else:
            return "%s?review=%d" % (commit.sha1[:8], review.id)

    approval_cache = {}

    ApprovalColumn.fillCache(db, user, review, approval_cache, profiler)

    check.stop()

    summary_column = SummaryColumn(review, linkToCommit)
    summary_column.fillCache(db, review)

    profiler.check("SummaryColumn.fillCache")

    columns = [(10, log.html.WhenColumn()),
               (60, summary_column),
               (16, log.html.AuthorColumn()),
               (7, ApprovalColumn(user, review, ApprovalColumn.APPROVED, approval_cache)),
               (7, ApprovalColumn(user, review, ApprovalColumn.TOTAL, approval_cache))]

    def renderReviewPending(db, target):
        if not user.isAnonymous():
            target.text("Filter: ")

            if hasPendingChanges:
                target.a(href="showcommit?review=%d&filter=pending" % review.id, title="All changes you need to review.").text("[pending]")
                target.text()
            if user in review.reviewers:
                target.a(href="showcommit?review=%d&filter=reviewable" % review.id, title="All changes you can review, including what you've already reviewed.").text("[reviewable]")
                target.text()

            target.a(href="showcommit?review=%d&filter=relevant" % review.id, title="All changes that match your filters.").text("[relevant]")
            target.text()

        target.text("Manual: ")
        target.a(href="filterchanges?review=%d" % review.id, title="Manually select what files to display of the changes from all commits.").text("[full]")
        target.text()
        target.a(href="javascript:void(filterPartialChanges());", title="Manually select what files to display of the changes in a selection of commits.").text("[partial]")

    req.addResponseHeader("ETag", review.getETag(db, user))

    if user.getPreference(db, "review.useMustRevalidate"):
        req.addResponseHeader("Cache-Control", "must-revalidate")

    yield flush(target)

    try:
        if prefetch_commits.error is not None:
            raise base.ImplementationError(
                "failed to prefetch commits:\n%s" % prefetch_commits.error)

        prefetch_commits.getCommits(db)

        profiler.check("FetchCommits.getCommits()")

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

        commits = set()
        conflicts = {}

        for changeset_type, parent_id, child_id in cursor:
            child = gitutils.Commit.fromId(db, repository, child_id)
            if changeset_type == "conflicts":
                conflicts[child] = gitutils.Commit.fromId(db, repository, parent_id)
            else:
                commits.add(child)

        commits = list(commits)

        cursor.execute("""SELECT id, old_head, new_head, new_upstream, uid, branch
                            FROM reviewrebases
                           WHERE review=%s""",
                       (review.id,))

        all_rebases = [(rebase_id,
                        gitutils.Commit.fromId(db, repository, old_head),
                        gitutils.Commit.fromId(db, repository, new_head) if new_head else None,
                        dbutils.User.fromId(db, user_id),
                        gitutils.Commit.fromId(db, repository, new_upstream) if new_upstream is not None else None,
                        branch_name)
                       for rebase_id, old_head, new_head, new_upstream, user_id, branch_name in cursor]

        bottom_right = None

        finished_rebases = filter(lambda item: item[2] is not None, all_rebases)
        current_rebases = filter(lambda item: item[2] is None, all_rebases)

        if current_rebases:
            assert len(current_rebases) == 1

            def renderCancelRebase(db, target):
                target.button("cancelrebase").text("Cancel Rebase")

            if user == current_rebases[0][3]:
                bottom_right = renderCancelRebase
        else:
            def renderPrepareRebase(db, target):
                target.button("preparerebase").text("Prepare Rebase")

            bottom_right = renderPrepareRebase

        if finished_rebases:
            cursor.execute("""SELECT commit
                                FROM reachable
                               WHERE branch=%s""",
                           (review.branch.id,))

            actual_commits = [gitutils.Commit.fromId(db, repository, commit_id) for (commit_id,) in cursor]
        else:
            actual_commits = []

        log.html.render(db, target, "Commits (%d)", commits=commits, columns=columns, title_right=renderReviewPending, rebases=finished_rebases, branch_name=review.branch.name, bottom_right=bottom_right, review=review, highlight=highlight, profiler=profiler, user=user, extra_commits=actual_commits, conflicts=conflicts)

        yield flush(target)

        profiler.check("log")
    except gitutils.GitReferenceError as error:
        div = target.div("error")
        div.h1().text("Error!")
        div.text("The commit %s is missing from the repository." % error.sha1)
    except gitutils.GitError as error:
        div = target.div("error")
        div.h1().text("Error!")
        div.text("Failed to read commits from the repository: %s" % error.message)

    all_chains = review_comment.CommentChain.fromReview(db, review, user)

    profiler.check("chains (load)")

    if all_chains:
        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", note_chains)
        open_notes = filter(lambda chain: chain.state != "draft" and chain.state != "empty", note_chains)
    else:
        open_issues = []
        open_notes = []

    chains = target.table("paleyellow comments", align="center", cellspacing=0)
    h1 = chains.tr("h1").td("h1", colspan=3).h1().text("Comments")
    links = h1.span("links")

    if all_chains:
        links.a(href="showcomments?review=%d&filter=all" % review.id).text("[display all]")

        if not user.isAnonymous():
            links.a(href="showcomments?review=%d&filter=all&blame=%s" % (review.id, user.name)).text("[in my commits]")

            cursor.execute("""SELECT count(commentstoread.comment) > 0
                                FROM commentchains
                                JOIN comments ON (comments.chain=commentchains.id)
                                JOIN commentstoread ON (commentstoread.comment=comments.id)
                               WHERE commentchains.review=%s
                                 AND commentstoread.uid=%s""",
                           [review.id, user.id])

            if cursor.fetchone()[0]:
                links.a(href="showcomments?review=%d&filter=toread" % review.id).text("[display unread]")

        def renderChains(target, chains):
            for chain in chains:
                row = target.tr("comment %s %s" % (chain.type, chain.state))
                row.td("author").text(chain.user.fullname)
                row.td("title").a(href="showcomment?chain=%d" % chain.id).innerHTML(chain.leader())

                ncomments = chain.countComments()
                nunread = chain.countUnread()

                cell = row.td("when")
                if ncomments == 1:
                    if nunread: cell.b().text("Unread")
                    else: cell.text("No replies")
                else:
                    if nunread: cell.b().text("%d of %d unread" % (nunread, ncomments))
                    else: cell.text("%d repl%s" % (ncomments - 1, "ies" if ncomments > 2 else "y"))

        if draft_issues:
            h2 = chains.tr("h2", id="draft-issues").td("h2", colspan=3).h2().text("Draft Issues")
            h2.a(href="showcomments?review=%d&filter=draft-issues" % review.id).text("[display all]")
            h2.a(href="showcomments?review=%d&filter=draft-issues&blame=%s" % (review.id, user.name)).text("[in my commits]")
            renderChains(chains, draft_issues)

        if open_issues:
            h2 = chains.tr("h2", id="open-issues").td("h2", colspan=3).h2().text("Open Issues")
            h2.a(href="showcomments?review=%d&filter=open-issues" % review.id).text("[display all]")
            h2.a(href="showcomments?review=%d&filter=open-issues&blame=%s" % (review.id, user.name)).text("[in my commits]")
            renderChains(chains, open_issues)

        if addressed_issues:
            h2 = chains.tr("h2", id="addressed-issues").td("h2", colspan=3).h2().text("Addressed Issues")
            h2.a(href="showcomments?review=%d&filter=addressed-issues" % review.id).text("[display all]")
            h2.a(href="showcomments?review=%d&filter=addressed-issues&blame=%s" % (review.id, user.name)).text("[in my commits]")
            renderChains(chains, addressed_issues)

        if closed_issues:
            h2 = chains.tr("h2", id="closed-issues").td("h2", colspan=3).h2().text("Resolved Issues")
            h2.a(href="showcomments?review=%d&filter=closed-issues" % review.id).text("[display all]")
            h2.a(href="showcomments?review=%d&filter=closed-issues&blame=%s" % (review.id, user.name)).text("[in my commits]")
            renderChains(chains, closed_issues)

        if draft_notes:
            h2 = chains.tr("h2", id="draft-notes").td("h2", colspan=3).h2().text("Draft Notes")
            h2.a(href="showcomments?review=%d&filter=draft-notes" % review.id).text("[display all]")
            h2.a(href="showcomments?review=%d&filter=draft-notes&blame=%s" % (review.id, user.name)).text("[in my commits]")
            renderChains(chains, draft_notes)

        if open_notes:
            h2 = chains.tr("h2", id="notes").td("h2", colspan=3).h2().text("Notes")
            h2.a(href="showcomments?review=%d&filter=open-notes" % review.id).text("[display all]")
            h2.a(href="showcomments?review=%d&filter=open-notes&blame=%s" % (review.id, user.name)).text("[in my commits]")
            renderChains(chains, open_notes)

    buttons = chains.tr("buttons").td("buttons", colspan=3)
    buttons.button(onclick="CommentChain.create('issue');").text("Raise Issue")
    buttons.button(onclick="CommentChain.create('note');").text("Write Note")

    profiler.check("chains (render)")

    yield flush(target)

    cursor.execute("""SELECT DISTINCT reviewfiles.file, theirs.uid
                        FROM reviewfiles
                        JOIN reviewuserfiles AS yours ON (yours.file=reviewfiles.id)
                        JOIN reviewuserfiles AS theirs ON (theirs.file=yours.file AND theirs.uid!=yours.uid)
                       WHERE reviewfiles.review=%s
                         AND yours.uid=%s""",
                   (review.id, user.id))
    rows = cursor.fetchall()

    profiler.check("shared assignments (query)")

    if rows:
        reviewers = {}

        for file_id, user_id in rows:
            reviewers.setdefault(file_id, {})[user_id] = set()

        shared = target.table('paleyellow shared', align='center', cellspacing=0)
        row = shared.tr('h1')
        shared_header = row.td('h1', colspan=2).h1()
        shared_header.text("Shared Assignments")
        shared_buttons = row.td('buttons', colspan=2).span(style="display: none")
        shared_buttons.button("confirm").text("Confirm")
        shared_buttons.button("cancel").text("Cancel")

        granularity = "module"

        def moduleFromFile(file_id):
            filename = dbutils.describe_file(db, file_id)
            return getModuleFromFile(repository, filename) or filename

        def formatFiles(files):
            paths = sorted([dbutils.describe_file(db, file_id) for file_id in files])
            if granularity == "file":
                return diff.File.eliminateCommonPrefixes(paths)
            else:
                modules = set()
                files = []
                for path in paths:
                    module = getModuleFromFile(path)
                    if module: modules.add(module)
                    else: files.append(path)
                return sorted(modules) + diff.File.eliminateCommonPrefixes(files)

        files_per_team = review_utils.collectReviewTeams(reviewers)
        teams_per_modules = {}

        profiler.check("shared assignments (collect teams)")

        for team, files in files_per_team.items():
            modules = set()
            for file_id in files:
                modules.add(moduleFromFile(file_id))
            teams_per_modules.setdefault(frozenset(modules), set()).update(team)

        for modules, team in teams_per_modules.items():
            row = shared.tr("reviewers")

            cell = row.td("reviewers")
            members = sorted([dbutils.User.fromId(db, user_id).fullname for user_id in team])
            for member in members: cell.text(member).br()
            row.td("willreview").innerHTML("<span class='also'>also</span>&nbsp;review&nbsp;changes&nbsp;in")

            cell = row.td("files")
            for path in diff.File.eliminateCommonPrefixes(sorted(modules)):
                cell.span("file").innerHTML(path).br()

            paths = json_encode(list(modules))
            user_ids = json_encode(list(team))

            cell = row.td("buttons")
            cell.button("accept", critic_paths=paths, critic_user_ids=user_ids).text("I will review this!")
            cell.button("deny", critic_paths=paths, critic_user_ids=user_ids).text("They will review this!")

    yield flush(target)

    profiler.check("shared assignments")

    cursor.execute("SELECT batches.id, users.fullname, batches.comment, batches.time FROM batches JOIN users ON (users.id=batches.uid) WHERE batches.review=%s ORDER BY batches.id DESC", [review.id])
    rows = cursor.fetchall()

    if rows:
        notes = dict([(chain.id, chain) for chain in open_notes])

        batches = target.table("paleyellow batches", align="center", cellspacing=0)
        batches.tr().td("h1", colspan=3).h1().text("Work Log")

        for batch_id, user_fullname, chain_id, when in rows:
            row = batches.tr("batch")
            row.td("author").text(user_fullname)
            title = "<i>No comment</i>"
            if chain_id:
                if chain_id in notes:
                    title = notes[chain_id].leader()
                else:
                    for chain in all_chains:
                        if chain.id == chain_id:
                            title = chain.leader()
                            break
            row.td("title").a(href="showbatch?batch=%d" % batch_id).innerHTML(title)
            row.td("when").text(user.formatTimestamp(db, when))

    profiler.check("batches")
    profiler.output(db, user, target)

    yield flush()

    if review.branch.head:
        try: head_according_to_git = repository.revparse(review.branch.name)
        except: head_according_to_git = None

        head_according_to_us = review.branch.head.sha1

        if head_according_to_git != head_according_to_us:
            # The git repository disagrees with us.  Potentially harmful updates
            # to the branch will be rejected by the git hook while this is the
            # case, but this means that "our" head might not be referenced at
            # all and thus that it might be GC:ed by the git repository at some
            # point.  To avoid that, add a keepalive reference.
            repository.keepalive(head_according_to_us)

            yield "\n<!-- branch head mismatch: git=%s, us=%s (corrected) -->" % (head_according_to_git[:8] if head_according_to_git else "N/A", head_according_to_us[:8])
Пример #8
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 = { 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
Пример #9
0
def renderShowReviewLog(req, db, user):
    review_id = page.utils.getParameter(req, "review", filter=int)
    granularity = page.utils.getParameter(req, "granularity")
    unassigned = page.utils.getParameter(req, "unassigned", "no") == "yes"

    cursor = db.cursor()

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

    document = htmlutils.Document(req)
    html = document.html()
    head = html.head()
    body = html.body()

    head.title("Review Log: %s" % review.branch.name)

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

    document.addExternalStylesheet("resource/showreviewlog.css")
    document.addExternalStylesheet("resource/review.css")
    document.addExternalScript("resource/review.js")
    document.addInternalScript(review.getJS())

    target = body.div("main")

    reviewed_reviewers = review_utils.getReviewedReviewers(db, review)

    def formatFiles(files):
        paths = sorted(
            [dbutils.describe_file(db, file_id) for file_id in files])
        if granularity == "file":
            return diff.File.eliminateCommonPrefixes(paths)
        else:
            modules = set()
            files = []
            for path in paths:
                match = re_module.match(path)
                if match: modules.add(match.group(1))
                else: files.append(path)
            return sorted(modules) + diff.File.eliminateCommonPrefixes(files)

    if reviewed_reviewers and not unassigned:
        reviewed = target.table('changes', align='center')
        reviewed.col(width="30%")
        reviewed.col(width="10%")
        reviewed.col(width="60%")
        reviewed.tr().td('h1', colspan=3).h1().text("Reviewed Changes")

        teams = review_utils.collectReviewTeams(reviewed_reviewers)

        for team in teams:
            row = reviewed.tr("reviewers")

            cell = row.td("reviewers")
            users = sorted([
                dbutils.User.fromId(db, user_id).fullname for user_id in team
            ])
            for user in users:
                cell.text(user).br()
            row.td("willreview").innerHTML("reviewed")

            cell = row.td("files")
            for file in formatFiles(teams[team]):
                cell.span("file").innerHTML(file).br()

    pending_reviewers = review_utils.getPendingReviewers(db, review)

    if pending_reviewers:
        pending = target.table('changes', align='center')
        pending.col(width="30%")
        pending.col(width="10%")
        pending.col(width="60%")
        pending.tr().td('h1', colspan=3).h1().text("Pending Changes")

        teams = review_utils.collectReviewTeams(pending_reviewers)

        if not unassigned:
            for team in teams:
                if team is not None:
                    row = pending.tr("reviewers")

                    cell = row.td("reviewers")
                    users = sorted([
                        dbutils.User.fromId(db, user_id).fullname
                        for user_id in team
                    ])
                    for user in users:
                        cell.text(user).br()
                    row.td("willreview").innerHTML("should&nbsp;review")

                    cell = row.td("files")
                    for file in formatFiles(teams[team]):
                        cell.span("file").innerHTML(file).br()

        if None in teams:
            row = pending.tr("reviewers")
            row.td("no-one",
                   colspan=2).text("No reviewers found for changes in")

            cell = row.td("files")
            for file in formatFiles(teams[None]):
                cell.span("file").innerHTML(file).br()

    return document
Пример #10
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())
Пример #11
0
def renderBranches(req, db, user):
    offset = req.getParameter("offset", 0, filter=int)
    count = req.getParameter("count", 50, filter=int)

    document = htmlutils.Document(req)

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

    page.utils.generateHeader(body, db, user, current_page="branches")

    document.addExternalScript("resource/branches.js")
    document.addExternalStylesheet("resource/branches.css")

    cursor = db.cursor()

    repository = req.getParameter("repository", None,
                                  gitutils.Repository.FromParameter(db))

    if not repository:
        repository = user.getDefaultRepository(db)

    if repository:
        title = "Branches in %s" % repository.name
        selected = repository.name
    else:
        title = "Branches"
        selected = None

    document.setTitle(title)

    table = body.div("main").table("paleyellow branches",
                                   align="center",
                                   cellspacing="0")

    row = table.tr("title")
    row.td("h1", colspan=2).h1().text(title)

    page.utils.generateRepositorySelect(db,
                                        user,
                                        row.td("repositories", colspan=2),
                                        selected=selected)

    if repository:
        document.addInternalScript(repository.getJS())

        cursor.execute(
            """SELECT branches.id, branches.name, branches.base, branches.review,
                                 branches.commit_time, COUNT(reachable.branch)
                            FROM (SELECT branches.id AS id, branches.name AS name, bases.name AS base,
                                         branches.review AS review, commits.commit_time AS commit_time
                                    FROM branches
                                    JOIN commits ON (commits.id=branches.head)
                         LEFT OUTER JOIN branches AS bases ON (branches.base=bases.id)
                                   WHERE branches.type='normal'
                                     AND branches.repository=%s
                                ORDER BY commits.commit_time DESC
                                   LIMIT %s
                                   OFFSET %s) AS branches
                 LEFT OUTER JOIN reachable ON (reachable.branch=branches.id)
                        GROUP BY branches.id, branches.name, branches.base, branches.review,
                                 branches.commit_time
                        ORDER BY branches.commit_time DESC""",
            (repository.id, count, offset))

        branches_found = False

        for branch_id, branch_name, base_name, review_id, commit_time, count in cursor:
            if not branches_found:
                row = table.tr("headings")
                row.td("name").text("Name")
                row.td("base").text("Base")
                row.td("commits").text("Commits")
                row.td("when").text("When")
                branches_found = True

            row = table.tr("branch")

            def link_to_branch(target, repository, name):
                url = htmlutils.URL("/log",
                                    branch=name,
                                    repository=repository.id)
                target.a(href=url).text(name)

            td_name = row.td("name")
            link_to_branch(td_name, repository, branch_name)

            if review_id is not None:
                span = td_name.span("review").preformatted()
                span.a(href="r/%d" % review_id).text("r/%d" % review_id)
            elif base_name:
                url = htmlutils.URL("/checkbranch",
                                    repository=repository.id,
                                    commit=branch_name,
                                    upstream=base_name,
                                    fetch="no")
                span = td_name.span("check").preformatted().a(
                    href=url).text("check")

            td_base = row.td("base")
            if base_name:
                link_to_branch(td_base, repository, base_name)

            row.td("commits").text(count)

            log.html.renderWhen(row.td("when"), commit_time.timetuple())

        if not branches_found:
            row = table.tr("nothing")
            row.td("nothing", colspan=4).text("No branches")
    else:
        row = table.tr("nothing")
        row.td("nothing", colspan=4).text("No repository selected")

    return document
Пример #12
0
def renderShowReviewLog(req, db, user):
    review_id = page.utils.getParameter(req, "review", filter=int)
    granularity = page.utils.getParameter(req, "granularity")
    unassigned = page.utils.getParameter(req, "unassigned", "no") == "yes"

    cursor = db.cursor()

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

    document = htmlutils.Document(req)
    html = document.html()
    head = html.head()
    body = html.body()

    head.title("Review Log: %s" % review.branch.name)

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

    document.addExternalStylesheet("resource/showreviewlog.css")
    document.addExternalStylesheet("resource/review.css")
    document.addExternalScript("resource/review.js")
    document.addInternalScript(review.getJS())

    target = body.div("main")

    reviewed_reviewers = review_utils.getReviewedReviewers(db, review)

    def formatFiles(files):
        paths = sorted([dbutils.describe_file(db, file_id) for file_id in files])
        if granularity == "file":
            return diff.File.eliminateCommonPrefixes(paths)
        else:
            modules = set()
            files = []
            for path in paths:
                match = re_module.match(path)
                if match: modules.add(match.group(1))
                else: files.append(path)
            return sorted(modules) + diff.File.eliminateCommonPrefixes(files)

    if reviewed_reviewers and not unassigned:
        reviewed = target.table('changes', align='center')
        reviewed.col(width="30%")
        reviewed.col(width="10%")
        reviewed.col(width="60%")
        reviewed.tr().td('h1', colspan=3).h1().text("Reviewed Changes")

        teams = review_utils.collectReviewTeams(reviewed_reviewers)

        for team in teams:
            row = reviewed.tr("reviewers")

            cell = row.td("reviewers")
            users = sorted([dbutils.User.fromId(db, user_id).fullname for user_id in team])
            for user in users: cell.text(user).br()
            row.td("willreview").innerHTML("reviewed")

            cell = row.td("files")
            for file in formatFiles(teams[team]):
                cell.span("file").innerHTML(file).br()

    pending_reviewers = review_utils.getPendingReviewers(db, review)

    if pending_reviewers:
        pending = target.table('changes', align='center')
        pending.col(width="30%")
        pending.col(width="10%")
        pending.col(width="60%")
        pending.tr().td('h1', colspan=3).h1().text("Pending Changes")

        teams = review_utils.collectReviewTeams(pending_reviewers)

        if not unassigned:
            for team in teams:
                if team is not None:
                    row = pending.tr("reviewers")

                    cell = row.td("reviewers")
                    users = sorted([dbutils.User.fromId(db, user_id).fullname for user_id in team])
                    for user in users: cell.text(user).br()
                    row.td("willreview").innerHTML("should&nbsp;review")

                    cell = row.td("files")
                    for file in formatFiles(teams[team]):
                        cell.span("file").innerHTML(file).br()

        if None in teams:
            row = pending.tr("reviewers")
            row.td("no-one", colspan=2).text("No reviewers found for changes in")

            cell = row.td("files")
            for file in formatFiles(teams[None]):
                cell.span("file").innerHTML(file).br()

    return document
Пример #13
0
def renderSelectSource(req, db, user):
    cursor = db.cursor()

    document = htmlutils.Document(req)
    html = document.html()
    head = html.head()
    body = html.body()

    page.utils.generateHeader(body, db, user, current_page="createreview")

    document.addExternalStylesheet("resource/createreview.css")
    document.addExternalScript("resource/createreview.js")
    document.addExternalScript("resource/autocomplete.js")

    document.addInternalScript(user.getJS(db))
    document.setTitle("Create Review")

    target = body.div("main")
    table = page.utils.PaleYellowTable(target, "Create Review")
    table.titleRight.innerHTML("Step 1")

    default_repository = user.getDefaultRepository(db)
    default_remotes = {}
    default_branches = {}

    def renderLocalRepository(target):
        page.utils.generateRepositorySelect(db, user, target, access_type="modify")

        cursor.execute("""SELECT repositories.id, repositories.name, repositories.path
                            FROM repositories
                        ORDER BY repositories.id""")

        for repository_id, name, path in cursor.fetchall():
            def findRemote(local_name):
                cursor.execute("""SELECT remote
                                    FROM trackedbranches
                                   WHERE repository=%s
                                     AND local_name=%s""",
                               (repository_id, local_name))
                row = cursor.fetchone()
                if row:
                    return row[0]

            try:
                repository = gitutils.Repository.fromId(db, repository_id)
            except auth.AccessDenied:
                continue

            remote = branch_name = None

            for branch in repository.getSignificantBranches(db):
                remote = findRemote(branch.name)
                if remote:
                    branch_name = branch.name
                    break

            if not remote:
                remote = findRemote("*")

            default_remotes[name] = remote
            default_branches[name] = branch_name

        document.addInternalScript("var default_remotes = %s;" % json_encode(default_remotes))
        document.addInternalScript("var default_branches = %s;" % json_encode(default_branches))

    def renderRemoteRepository(target):
        value = default_remotes.get(default_repository.name) if default_repository else None
        target.input("remote", value=value)

    def renderWorkBranch(target):
        target.text("refs/heads/")
        target.input("workbranch")

    def renderUpstreamCommit(target):
        default_branch = default_branches.get(default_repository.name) if default_repository else None
        target.input("upstreamcommit", value=("refs/heads/%s" % default_branch) if default_branch else "")

    table.addItem("Local Repository", renderLocalRepository, "Critic repository to create review in.")
    table.addItem("Remote Repository", renderRemoteRepository, "Remote repository to fetch commits from.")
    table.addItem("Work Branch", renderWorkBranch, "Work branch (in remote repository) containing commits to create review of.")
    table.addItem("Upstream Commit", renderUpstreamCommit, "Upstream commit from which the work branch was branched.")

    def renderButtons(target):
        target.button("fetchbranch").text("Fetch Branch")

    table.addCentered(renderButtons)

    return document
Пример #14
0
def renderConfirmMerge(req, db, user):
    confirmation_id = req.getParameter("id", filter=int)
    tail_sha1 = req.getParameter("tail", None)
    do_confirm = req.getParameter("confirm", "no") == "yes"
    do_cancel = req.getParameter("cancel", "no") == "yes"

    cursor = db.cursor()

    cursor.execute("SELECT review, uid, merge, confirmed, tail FROM reviewmergeconfirmations WHERE id=%s", (confirmation_id,))
    row = cursor.fetchone()
    if not row:
        raise page.utils.DisplayMessage("No pending merge with that id.")
    review_id, user_id, merge_id, confirmed, tail_id = row

    review = dbutils.Review.fromId(db, review_id)
    merge = gitutils.Commit.fromId(db, review.repository, merge_id)

    if confirmed and tail_id is not None:
        tail_sha1 = gitutils.Commit.fromId(db, review.repository, tail_id).sha1

    cursor.execute("SELECT merged FROM reviewmergecontributions WHERE id=%s", (confirmation_id,))

    merged = [gitutils.Commit.fromId(db, review.repository, merged_id) for (merged_id,) in cursor]
    merged_set = log.commitset.CommitSet(merged)

    if tail_sha1 is not None:
        tail = gitutils.Commit.fromSHA1(db, review.repository, tail_sha1)
        tail_id = tail.getId(db)

        cut = [gitutils.Commit.fromSHA1(db, review.repository, sha1)
               for sha1 in tail.parents if sha1 in merged_set]
        merged_set = merged_set.without(cut)
        merged = list(merged_set)
    else:
        tail_id = None

    if do_confirm:
        cursor.execute("UPDATE reviewmergeconfirmations SET confirmed=TRUE, tail=%s WHERE id=%s", (tail_id, confirmation_id))
        db.commit()
    elif do_cancel:
        cursor.execute("DELETE FROM reviewmergeconfirmations WHERE id=%s", (confirmation_id,))
        db.commit()

    document = htmlutils.Document(req)

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

    def renderButtons(target):
        if not do_confirm and not do_cancel:
            target.button("confirmAll").text("Confirm (merge + contributed)")
            target.button("confirmNone").text("Confirm (merge only)")
            target.button("cancel").text("Cancel")

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

    document.addExternalStylesheet("resource/confirmmerge.css")
    document.addExternalScript("resource/log.js")
    document.addExternalScript("resource/confirmmerge.js")
    document.addInternalScript(user.getJS())
    document.addInternalScript(review.getJS())
    document.addInternalScript("var confirmation_id = %d;" % confirmation_id)
    document.addInternalScript("var merge_sha1 = %s;" % htmlutils.jsify(merge.sha1))

    if tail_sha1 is not None:
        document.addInternalScript("var tail_sha1 = %s;" % htmlutils.jsify(tail_sha1))

    if not do_confirm and not do_cancel:
        heads = merged_set.getHeads()
        if heads:
            document.addInternalScript("var automaticAnchorCommit = %s;" % htmlutils.jsify(heads.pop().sha1))
        else:
            document.addInternalScript("var automaticAnchorCommit = null;")

    if do_confirm:
        document.addInternalScript("var confirmed = true;")
    else:
        document.addInternalScript("var confirmed = false;")

    target = body.div("main")

    basic = target.table("paleyellow")
    basic.col(width='15%')
    basic.col(width='55%')
    basic.col(width='30%')
    h1 = basic.tr().td('h1', colspan=3).h1()

    if do_confirm:
        h1.text("CONFIRMED MERGE")
    elif do_cancel:
        h1.text("CANCELLED MERGE")
    else:
        h1.text("Confirm Merge")

    row = basic.tr("sha1")
    row.td("heading").text("SHA-1:")
    row.td("value").preformatted().text(merge.sha1)
    row.td().text()

    row = basic.tr("message")
    row.td("heading").text("Message:")
    row.td("value").preformatted().text(merge.message)
    row.td().text()

    if not do_confirm and not do_cancel:
        row = basic.tr("instructions")
        row.td("heading").text("Instructions:")
        row.td("value").preformatted().text("""\
Use the top right buttons to confirm the merge with or without the contributed commits that it brings.

By clicking 'Confirm (merge + contributed)' you will bring the merge commit plus all commits that it contributes into the this code review.

By clicking 'Confirm (merge only)' you will bring only the merge commit itself into the code review and not the contributed commits."

By clicking 'Cancel' you will abort the merge. The code review will not be modified at all from its current state.""")
        row.td().text()


    if merged:
        columns = [(10, log.html.WhenColumn()),
                   (60, log.html.SummaryColumn()),
                   (16, log.html.AuthorColumn())]

        log.html.render(db, target, "Contributed Commits", commits=merged, columns=columns)

    return document
Пример #15
0
def renderConfirmMerge(req, db, user):
    confirmation_id = req.getParameter("id", filter=int)
    tail_sha1 = req.getParameter("tail", None)
    do_confirm = req.getParameter("confirm", "no") == "yes"
    do_cancel = req.getParameter("cancel", "no") == "yes"

    cursor = db.cursor()

    user = dbutils.User.fromName(db, req.user)

    cursor.execute(
        "SELECT review, uid, merge, confirmed, tail FROM reviewmergeconfirmations WHERE id=%s", (confirmation_id,)
    )

    review_id, user_id, merge_id, confirmed, tail_id = cursor.fetchone()

    review = dbutils.Review.fromId(db, review_id)
    merge = gitutils.Commit.fromId(db, review.repository, merge_id)

    if confirmed and tail_id is not None:
        tail_sha1 = gitutils.Commit.fromId(db, review.repository, tail_id).sha1

    cursor.execute("SELECT merged FROM reviewmergecontributions WHERE id=%s", (confirmation_id,))

    merged = [gitutils.Commit.fromId(db, review.repository, merged_id) for (merged_id,) in cursor]
    merged_set = log.commitset.CommitSet(merged)

    if tail_sha1 is not None:
        tail = gitutils.Commit.fromSHA1(db, review.repository, tail_sha1)
        tail_id = tail.getId(db)

        cut = [gitutils.Commit.fromSHA1(db, review.repository, sha1) for sha1 in tail.parents if sha1 in merged_set]
        merged_set = merged_set.without(cut)
        merged = list(merged_set)
    else:
        tail_id = None

    if do_confirm:
        cursor.execute(
            "UPDATE reviewmergeconfirmations SET confirmed=TRUE, tail=%s WHERE id=%s", (tail_id, confirmation_id)
        )
        db.commit()
    elif do_cancel:
        cursor.execute("DELETE FROM reviewmergeconfirmations WHERE id=%s", (confirmation_id,))
        db.commit()

    document = htmlutils.Document(req)

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

    def renderButtons(target):
        if not do_confirm and not do_cancel:
            target.button("confirmAll").text("Confirm (All)")
            target.button("confirmNone").text("Confirm (None)")
            target.button("cancel").text("Cancel")

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

    document.addExternalStylesheet("resource/confirmmerge.css")
    document.addExternalScript("resource/log.js")
    document.addExternalScript("resource/confirmmerge.js")
    document.addInternalScript(user.getJS())
    document.addInternalScript(review.getJS())
    document.addInternalScript("var confirmation_id = %d;" % confirmation_id)
    document.addInternalScript("var merge_sha1 = %s;" % htmlutils.jsify(merge.sha1))

    if tail_sha1 is not None:
        document.addInternalScript("var tail_sha1 = %s;" % htmlutils.jsify(tail_sha1))

    if not do_confirm and not do_cancel:
        heads = merged_set.getHeads()
        if heads:
            document.addInternalScript("var automaticAnchorCommit = %s;" % htmlutils.jsify(heads.pop().sha1))
        else:
            document.addInternalScript("var automaticAnchorCommit = null;")

    if do_confirm:
        document.addInternalScript("var confirmed = true;")
    else:
        document.addInternalScript("var confirmed = false;")

    target = body.div("main")

    basic = target.table("confirm", align="center")
    basic.col(width="10%")
    basic.col(width="60%")
    basic.col(width="30%")
    h1 = basic.tr().td("h1", colspan=3).h1()

    if do_confirm:
        h1.text("CONFIRMED MERGE")
    elif do_cancel:
        h1.text("CANCELLED MERGE")
    else:
        h1.text("Confirm Merge")

    row = basic.tr("sha1")
    row.td("heading").text("SHA-1:")
    row.td("value").preformatted().text(merge.sha1)
    row.td().text()

    row = basic.tr("message")
    row.td("heading").text("Message:")
    row.td("value").preformatted().text(merge.message)
    row.td().text()

    if merged:
        columns = [(10, log.html.WhenColumn()), (60, log.html.SummaryColumn()), (16, log.html.AuthorColumn())]

        log.html.render(db, target, "Contributed Commits", commits=merged, columns=columns)

    return document
Пример #16
0
def renderBranches(req, db, user):
    offset = req.getParameter("offset", 0, filter=int)
    count = req.getParameter("count", 50, filter=int)

    document = htmlutils.Document(req)

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

    page.utils.generateHeader(body, db, user, current_page="branches")

    document.addExternalScript("resource/branches.js")
    document.addExternalStylesheet("resource/branches.css")

    cursor = db.cursor()

    repository = req.getParameter("repository", None, gitutils.Repository.FromParameter(db))

    if not repository:
        repository = user.getDefaultRepository(db)

    if repository:
        title = "Branches in %s" % repository.name
        selected = repository.name
    else:
        title = "Branches"
        selected = None

    document.setTitle(title)

    table = body.div("main").table("paleyellow branches", align="center", cellspacing="0")

    row = table.tr("title")
    row.td("h1", colspan=2).h1().text(title)

    page.utils.generateRepositorySelect(db, user, row.td("repositories", colspan=2), selected=selected)

    if repository:
        document.addInternalScript(repository.getJS())

        cursor.execute("""SELECT branches.id, branches.name, branches.base, branches.review,
                                 branches.commit_time, COUNT(reachable.branch)
                            FROM (SELECT branches.id AS id, branches.name AS name, bases.name AS base,
                                         branches.review AS review, commits.commit_time AS commit_time
                                    FROM branches
                                    JOIN commits ON (commits.id=branches.head)
                         LEFT OUTER JOIN branches AS bases ON (branches.base=bases.id)
                                   WHERE branches.type='normal'
                                     AND branches.repository=%s
                                ORDER BY commits.commit_time DESC
                                   LIMIT %s
                                   OFFSET %s) AS branches
                 LEFT OUTER JOIN reachable ON (reachable.branch=branches.id)
                        GROUP BY branches.id, branches.name, branches.base, branches.review,
                                 branches.commit_time
                        ORDER BY branches.commit_time DESC""",
                       (repository.id, count, offset))

        branches_found = False

        for branch_id, branch_name, base_name, review_id, commit_time, count in cursor:
            if not branches_found:
                row = table.tr("headings")
                row.td("name").text("Name")
                row.td("base").text("Base")
                row.td("commits").text("Commits")
                row.td("when").text("When")
                branches_found = True

            row = table.tr("branch")

            def link_to_branch(target, repository, name):
                url = htmlutils.URL("/log", branch=name, repository=repository.id)
                target.a(href=url).text(name)

            td_name = row.td("name")
            link_to_branch(td_name, repository, branch_name)

            if review_id is not None:
                span = td_name.span("review").preformatted()
                span.a(href="r/%d" % review_id).text("r/%d" % review_id)
            elif base_name:
                url = htmlutils.URL("/checkbranch",
                                    repository=repository.id,
                                    commit=branch_name,
                                    upstream=base_name,
                                    fetch="no")
                span = td_name.span("check").preformatted().a(href=url).text("check")

            td_base = row.td("base")
            if base_name:
                link_to_branch(td_base, repository, base_name)

            row.td("commits").text(count)

            log.html.renderWhen(row.td("when"), commit_time.timetuple())

        if not branches_found:
            row = table.tr("nothing")
            row.td("nothing", colspan=4).text("No branches")
    else:
        row = table.tr("nothing")
        row.td("nothing", colspan=4).text("No repository selected")

    return document
Пример #17
0
def renderSelectSource(req, db, user):
    cursor = db.cursor()

    document = htmlutils.Document(req)
    html = document.html()
    head = html.head()
    body = html.body()

    page.utils.generateHeader(body, db, user, current_page="createreview")

    document.addExternalStylesheet("resource/createreview.css")
    document.addExternalScript("resource/createreview.js")
    document.addExternalScript("resource/autocomplete.js")

    document.addInternalScript(user.getJS(db))
    document.setTitle("Create Review")

    target = body.div("main")
    table = page.utils.PaleYellowTable(target, "Create Review")
    table.titleRight.innerHTML("Step 1")

    default_repository = user.getPreference(db, "defaultRepository")
    default_remotes = {}
    default_branches = {}

    def renderLocalRepository(target):
        page.utils.generateRepositorySelect(db, user, target)

        cursor.execute("""SELECT repositories.id, repositories.name, repositories.path, branches.name
                            FROM repositories
                 LEFT OUTER JOIN branches ON (branches.id=repositories.branch)
                        ORDER BY id""")

        for repository_id, name, path, branch_name in cursor.fetchall():
            local_names = ["*"]

            if branch_name:
                local_names.append(branch_name)

            cursor.execute("""SELECT remote
                                FROM trackedbranches
                               WHERE repository=%s
                                 AND local_name=ANY (%s)
                            ORDER BY local_name
                               LIMIT 1""",
                           (repository_id, local_names))

            row = cursor.fetchone()

            if row: default_remotes[name] = row[0]
            else: default_remotes[name] = None

            default_branches[name] = branch_name

        document.addInternalScript("var default_remotes = %s;" % json_encode(default_remotes))
        document.addInternalScript("var default_branches = %s;" % json_encode(default_branches))

    def renderRemoteRepository(target):
        target.input("remote", value=default_remotes.get(default_repository))

    def renderWorkBranch(target):
        target.text("refs/heads/")
        target.input("workbranch")

    def renderUpstreamCommit(target):
        default_branch = default_branches.get(default_repository)
        target.input("upstreamcommit", value=("refs/heads/%s" % default_branch) if default_branch else "")

    table.addItem("Local Repository", renderLocalRepository, "Critic repository to create review in.")
    table.addItem("Remote Repository", renderRemoteRepository, "Remote repository to fetch commits from.")
    table.addItem("Work Branch", renderWorkBranch, "Work branch (in remote repository) containing commits to create review of.")
    table.addItem("Upstream Commit", renderUpstreamCommit, "Upstream commit from which the work branch was branched.")

    def renderButtons(target):
        target.button("fetchbranch").text("Fetch Branch")

    table.addCentered(renderButtons)

    return document
Пример #18
0
def renderBranches(req, db, user):
    offset = req.getParameter("offset", 0, filter=int)
    count = req.getParameter("count", 50, filter=int)

    cursor = db.cursor()

    repository = req.getParameter("repository", None,
                                  gitutils.Repository.FromParameter(db))

    if not repository:
        repository = user.getDefaultRepository(db)

    all_branches = []
    commit_times = []

    if repository:
        cursor.execute(
            """SELECT branches.id, branches.name, commits.commit_time
                            FROM branches
                            JOIN repositories ON (repositories.id=branches.repository)
                            JOIN commits ON (commits.id=branches.head)
                           WHERE branches.type='normal'
                             AND branches.name NOT LIKE 'replay/%%'
                             AND repositories.id=%s
                        ORDER BY commits.commit_time DESC LIMIT %s""",
            (repository.id, count))

        for branch_id, branch_name, commit_time in cursor.fetchall():
            all_branches.append(branch_id)
            commit_times.append(commit_time.timetuple())

    document = htmlutils.Document(req)

    if repository:
        document.setTitle("Branches in %s" % repository.name)
    else:
        document.setTitle("Branches")

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

    page.utils.generateHeader(body, db, user, current_page="branches")

    document.addExternalScript("resource/branches.js")

    if repository:
        document.addInternalScript(repository.getJS())

    extraColumns = [
        ExtraColumn(
            "when", "When", lambda target, index: log.html.renderWhen(
                target, commit_times[index]))
    ]

    render(db,
           body.div("main"),
           "All Branches",
           repository,
           all_branches[offset:],
           extraColumns=extraColumns)

    return document
Пример #19
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.path
                            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, path in rows:
                filter_user = dbutils.User.fromId(db, user_id)
                reviewfilters.setdefault(filter_user.fullname, []).append(path or "/")
                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.path
                            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, path in rows:
                filter_user = dbutils.User.fromId(db, user_id)
                reviewfilters.setdefault(filter_user.fullname, []).append(path or "/")
                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())
Пример #20
0
def renderConfirmMerge(req, db, user):
    confirmation_id = req.getParameter("id", filter=int)
    tail_sha1 = req.getParameter("tail", None)
    do_confirm = req.getParameter("confirm", "no") == "yes"
    do_cancel = req.getParameter("cancel", "no") == "yes"

    cursor = db.cursor()

    user = dbutils.User.fromName(db, req.user)

    cursor.execute(
        "SELECT review, uid, merge, confirmed, tail FROM reviewmergeconfirmations WHERE id=%s",
        (confirmation_id, ))

    review_id, user_id, merge_id, confirmed, tail_id = cursor.fetchone()

    review = dbutils.Review.fromId(db, review_id)
    merge = gitutils.Commit.fromId(db, review.repository, merge_id)

    if confirmed and tail_id is not None:
        tail_sha1 = gitutils.Commit.fromId(db, review.repository, tail_id).sha1

    cursor.execute("SELECT merged FROM reviewmergecontributions WHERE id=%s",
                   (confirmation_id, ))

    merged = [
        gitutils.Commit.fromId(db, review.repository, merged_id)
        for (merged_id, ) in cursor
    ]
    merged_set = log.commitset.CommitSet(merged)

    if tail_sha1 is not None:
        tail = gitutils.Commit.fromSHA1(db, review.repository, tail_sha1)
        tail_id = tail.getId(db)

        cut = [
            gitutils.Commit.fromSHA1(db, review.repository, sha1)
            for sha1 in tail.parents if sha1 in merged_set
        ]
        merged_set = merged_set.without(cut)
        merged = list(merged_set)
    else:
        tail_id = None

    if do_confirm:
        cursor.execute(
            "UPDATE reviewmergeconfirmations SET confirmed=TRUE, tail=%s WHERE id=%s",
            (tail_id, confirmation_id))
        db.commit()
    elif do_cancel:
        cursor.execute("DELETE FROM reviewmergeconfirmations WHERE id=%s",
                       (confirmation_id, ))
        db.commit()

    document = htmlutils.Document(req)

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

    def renderButtons(target):
        if not do_confirm and not do_cancel:
            target.button("confirmAll").text("Confirm (All)")
            target.button("confirmNone").text("Confirm (None)")
            target.button("cancel").text("Cancel")

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

    document.addExternalStylesheet("resource/confirmmerge.css")
    document.addExternalScript("resource/log.js")
    document.addExternalScript("resource/confirmmerge.js")
    document.addInternalScript(user.getJS())
    document.addInternalScript(review.getJS())
    document.addInternalScript("var confirmation_id = %d;" % confirmation_id)
    document.addInternalScript("var merge_sha1 = %s;" %
                               htmlutils.jsify(merge.sha1))

    if tail_sha1 is not None:
        document.addInternalScript("var tail_sha1 = %s;" %
                                   htmlutils.jsify(tail_sha1))

    if not do_confirm and not do_cancel:
        heads = merged_set.getHeads()
        if heads:
            document.addInternalScript("var automaticAnchorCommit = %s;" %
                                       htmlutils.jsify(heads.pop().sha1))
        else:
            document.addInternalScript("var automaticAnchorCommit = null;")

    if do_confirm:
        document.addInternalScript("var confirmed = true;")
    else:
        document.addInternalScript("var confirmed = false;")

    target = body.div("main")

    basic = target.table('confirm', align='center')
    basic.col(width='10%')
    basic.col(width='60%')
    basic.col(width='30%')
    h1 = basic.tr().td('h1', colspan=3).h1()

    if do_confirm:
        h1.text("CONFIRMED MERGE")
    elif do_cancel:
        h1.text("CANCELLED MERGE")
    else:
        h1.text("Confirm Merge")

    row = basic.tr("sha1")
    row.td("heading").text("SHA-1:")
    row.td("value").preformatted().text(merge.sha1)
    row.td().text()

    row = basic.tr("message")
    row.td("heading").text("Message:")
    row.td("value").preformatted().text(merge.message)
    row.td().text()

    if merged:
        columns = [(10, log.html.WhenColumn()), (60, log.html.SummaryColumn()),
                   (16, log.html.AuthorColumn())]

        log.html.render(db,
                        target,
                        "Contributed Commits",
                        commits=merged,
                        columns=columns)

    return document