def get_list(stack_revision_id):
    """Return a list of Transplant objects"""
    revision_id = revision_id_to_int(stack_revision_id)

    phab = g.phabricator
    revision = phab.call_conduit("differential.revision.search",
                                 constraints={"ids": [revision_id]})
    revision = phab.single(revision, "data", none_when_empty=True)
    if revision is None:
        return problem(
            404,
            "Revision not found",
            "The revision does not exist or you lack permission to see it.",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404",
        )

    # TODO: This assumes that all revisions and related objects in the stack
    # have uniform view permissions for the requesting user. Some revisions
    # being restricted could cause this to fail.
    nodes, edges = build_stack_graph(phab, phab.expect(revision, "phid"))
    revision_phids = list(nodes)
    revs = phab.call_conduit(
        "differential.revision.search",
        constraints={"phids": revision_phids},
        limit=len(revision_phids),
    )

    transplants = Transplant.revisions_query(
        [phab.expect(r, "id") for r in phab.expect(revs, "data")]).all()
    return [t.serialize() for t in transplants], 200
Exemple #2
0
def _unmarshal_transplant_request(data):
    try:
        path = [
            (revision_id_to_int(item["revision_id"]), int(item["diff_id"]))
            for item in data["landing_path"]
        ]
    except (ValueError, TypeError):
        raise ProblemException(
            400,
            "Landing Path Malformed",
            "The provided landing_path was malformed.",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400",
        )

    if not path:
        raise ProblemException(
            400,
            "Landing Path Required",
            "A non empty landing_path is required.",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400",
        )

    # Confirmation token is optional. Convert usage of an empty
    # string to None as well to make using the API easier.
    confirmation_token = data.get("confirmation_token") or None

    return path, confirmation_token
Exemple #3
0
def get_list(stack_revision_id):
    """Return a list of Transplant objects"""
    revision_id = revision_id_to_int(stack_revision_id)

    phab = g.phabricator
    revision = phab.call_conduit(
        "differential.revision.search", constraints={"ids": [revision_id]}
    )
    revision = phab.single(revision, "data", none_when_empty=True)
    if revision is None:
        return problem(
            404,
            "Revision not found",
            "The revision does not exist or you lack permission to see it.",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404",
        )

    # TODO: This assumes that all revisions and related objects in the stack
    # have uniform view permissions for the requesting user. Some revisions
    # being restricted could cause this to fail.
    nodes, edges = build_stack_graph(phab, phab.expect(revision, "phid"))
    revision_phids = list(nodes)
    revs = phab.call_conduit(
        "differential.revision.search",
        constraints={"phids": revision_phids},
        limit=len(revision_phids),
    )

    # Return both transplants and landing jobs, since for repos that were switched
    # both or either of these could be populated.

    rev_ids = [phab.expect(r, "id") for r in phab.expect(revs, "data")]

    transplants = Transplant.revisions_query(rev_ids).all()
    landing_jobs = LandingJob.revisions_query(rev_ids).all()

    if transplants and landing_jobs:
        logger.warning(
            "Both {} transplants and {} landing jobs found for this revision".format(
                str(len(transplants)), str(len(landing_jobs))
            )
        )

    return (
        [t.serialize() for t in transplants] + [j.serialize() for j in landing_jobs],
        200,
    )
Exemple #4
0
def get_list(revision_id):
    """API endpoint at GET /landings to return a list of Landing objects."""
    # Verify that the client is permitted to see the associated revision.
    revision_id = revision_id_to_int(revision_id)
    revision = g.phabricator.call_conduit(
        'differential.revision.search',
        constraints={'ids': [revision_id]},
    )
    revision = g.phabricator.expect(revision, 'data')
    revision = g.phabricator.single(revision, none_when_empty=True)
    if not revision:
        return problem(
            404,
            'Revision not found',
            'The revision does not exist or you lack permission to see it.',
            type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404'
        )

    transplants = Transplant.revisions_query([revision_id]).all()
    return [t.legacy_serialize() for t in transplants], 200
Exemple #5
0
def _parse_transplant_request(data):
    """Extract confirmation token, flags, and the landing path from provided data.

    Args
        data (dict): A dictionary representing the transplant request.

    Returns:
        dict: A dictionary containing the landing path, confirmation token and flags.
    """
    try:
        landing_path = [(revision_id_to_int(item["revision_id"]),
                         int(item["diff_id"]))
                        for item in data["landing_path"]]
    except (ValueError, TypeError):
        raise ProblemException(
            400,
            "Landing Path Malformed",
            "The provided landing_path was malformed.",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400",
        )

    if not landing_path:
        raise ProblemException(
            400,
            "Landing Path Required",
            "A non empty landing_path is required.",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400",
        )

    flags = data.get("flags", [])

    # Confirmation token is optional. Convert usage of an empty
    # string to None as well to make using the API easier.
    confirmation_token = data.get("confirmation_token") or None

    return {
        "landing_path": landing_path,
        "confirmation_token": confirmation_token,
        "flags": flags,
    }
Exemple #6
0
def _unmarshal_transplant_request(data):
    try:
        path = [(revision_id_to_int(item['revision_id']), int(item['diff_id']))
                for item in data['landing_path']]
    except (ValueError, TypeError):
        raise ProblemException(
            400,
            'Landing Path Malformed',
            'The provided landing_path was malformed.',
            type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400'
        )

    if not path:
        raise ProblemException(
            400,
            'Landing Path Required',
            'A non empty landing_path is required.',
            type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400'
        )

    # Confirmation token is optional.
    confirmation_token = data.get('confirmation_token')

    return path, confirmation_token
Exemple #7
0
def get(revision_id, diff_id=None):
    """Gets revision from Phabricator.

    Args:
        revision_id: (string) ID of the revision in 'D{number}' format
        diff_id: (integer) Id of the diff to return with the revision. By
            default the active diff will be returned.
    """
    revision_id = revision_id_to_int(revision_id)

    phab = g.phabricator
    revision = phab.call_conduit('differential.revision.search',
                                 constraints={'ids': [revision_id]},
                                 attachments={
                                     'reviewers': True,
                                     'reviewers-extra': True,
                                 })
    revision = phab.single(revision, 'data', none_when_empty=True)
    if revision is None:
        return problem(
            404,
            'Revision not found',
            'The requested revision does not exist',
            type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404'
        )

    latest_diff = phab.single(
        phab.call_conduit(
            'differential.diff.search',
            constraints={
                'phids': [phab.expect(revision, 'fields', 'diffPHID')]
            },
            attachments={'commits': True},
        ), 'data')
    latest_diff_id = phab.expect(latest_diff, 'id')
    if diff_id is not None and diff_id != latest_diff_id:
        diff = phab.single(phab.call_conduit(
            'differential.diff.search',
            constraints={'ids': [diff_id]},
            attachments={'commits': True},
        ),
                           'data',
                           none_when_empty=True)
    else:
        diff = latest_diff

    if diff is None:
        return problem(
            404,
            'Diff not found',
            'The requested diff does not exist',
            type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404'
        )

    revision_phid = phab.expect(revision, 'phid')
    if phab.expect(diff, 'fields', 'revisionPHID') != revision_phid:
        return problem(
            400,
            'Diff not related to the revision',
            'The requested diff is not related to the requested revision.',
            type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400'
        )

    author_phid = phab.expect(revision, 'fields', 'authorPHID')
    reviewers = get_collated_reviewers(revision)

    # Immediately execute the lazy functions.
    users = lazy_user_search(phab, list(reviewers.keys()) + [author_phid])()
    projects = lazy_project_search(phab, list(reviewers.keys()))()

    accepted_reviewers = [
        reviewer_identity(phid, users, projects).identifier
        for phid, r in reviewers.items()
        if r['status'] is ReviewerStatus.ACCEPTED
    ]

    title = phab.expect(revision, 'fields', 'title')
    summary = phab.expect(revision, 'fields', 'summary')
    bug_id = get_bugzilla_bug(revision)
    human_revision_id = 'D{}'.format(revision_id)
    revision_url = urllib.parse.urljoin(current_app.config['PHABRICATOR_URL'],
                                        human_revision_id)
    commit_message_title, commit_message = format_commit_message(
        title, bug_id, accepted_reviewers, summary, revision_url)

    reviewers_response = serialize_reviewers(reviewers, users, projects,
                                             phab.expect(diff, 'phid'))
    author_response = serialize_author(author_phid, users)
    diff_response = serialize_diff(diff)

    return {
        'id': human_revision_id,
        'phid': phab.expect(revision, 'phid'),
        'bug_id': bug_id,
        'title': title,
        'url': revision_url,
        'date_created': PhabricatorClient.to_datetime(
            phab.expect(revision, 'fields', 'dateCreated')
        ).isoformat(),
        'date_modified': PhabricatorClient.to_datetime(
            phab.expect(revision, 'fields', 'dateModified')
        ).isoformat(),
        'summary': summary,
        'commit_message_title': commit_message_title,
        'commit_message': commit_message,
        'diff': diff_response,
        'latest_diff_id': latest_diff_id,
        'author': author_response,
        'reviewers': reviewers_response,
    }, 200  # yapf: disable
Exemple #8
0
def get(revision_id):
    """Get the stack a revision is part of.

    Args:
        revision_id: (string) ID of the revision in 'D{number}' format
    """
    revision_id = revision_id_to_int(revision_id)

    phab = g.phabricator
    revision = phab.call_conduit("differential.revision.search",
                                 constraints={"ids": [revision_id]})
    revision = phab.single(revision, "data", none_when_empty=True)
    if revision is None:
        return problem(
            404,
            "Revision not found",
            "The requested revision does not exist",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404",
        )

    # TODO: This assumes that all revisions and related objects in the stack
    # have uniform view permissions for the requesting user. Some revisions
    # being restricted could cause this to fail.
    nodes, edges = build_stack_graph(phab, phab.expect(revision, "phid"))
    stack_data = request_extended_revision_data(phab, [phid for phid in nodes])

    supported_repos = get_repos_for_env(current_app.config.get("ENVIRONMENT"))
    landable_repos = get_landable_repos_for_revision_data(
        stack_data, supported_repos)

    other_checks = get_blocker_checks(
        repositories=supported_repos,
        relman_group_phid=get_relman_group_phid(phab))

    landable, blocked = calculate_landable_subgraphs(stack_data,
                                                     edges,
                                                     landable_repos,
                                                     other_checks=other_checks)
    uplift_repos = [
        name for name, repo in supported_repos.items()
        if repo.approval_required
    ]

    involved_phids = set()
    for revision in stack_data.revisions.values():
        involved_phids.update(gather_involved_phids(revision))

    involved_phids = list(involved_phids)

    users = user_search(phab, involved_phids)
    projects = project_search(phab, involved_phids)

    secure_project_phid = get_secure_project_phid(phab)
    sec_approval_project_phid = get_sec_approval_project_phid(phab)

    revisions_response = []
    for _phid, revision in stack_data.revisions.items():
        revision_phid = PhabricatorClient.expect(revision, "phid")
        fields = PhabricatorClient.expect(revision, "fields")
        diff_phid = PhabricatorClient.expect(fields, "diffPHID")
        diff = stack_data.diffs[diff_phid]
        human_revision_id = "D{}".format(
            PhabricatorClient.expect(revision, "id"))
        revision_url = urllib.parse.urljoin(
            current_app.config["PHABRICATOR_URL"], human_revision_id)
        secure = revision_is_secure(revision, secure_project_phid)
        commit_description = find_title_and_summary_for_display(
            phab, revision, secure)
        bug_id = get_bugzilla_bug(revision)
        reviewers = get_collated_reviewers(revision)
        accepted_reviewers = reviewers_for_commit_message(
            reviewers, users, projects, sec_approval_project_phid)
        commit_message_title, commit_message = format_commit_message(
            commit_description.title,
            bug_id,
            accepted_reviewers,
            commit_description.summary,
            revision_url,
        )
        author_response = serialize_author(phab.expect(fields, "authorPHID"),
                                           users)

        revisions_response.append({
            "id":
            human_revision_id,
            "phid":
            revision_phid,
            "status":
            serialize_status(revision),
            "blocked_reason":
            blocked.get(revision_phid, ""),
            "bug_id":
            bug_id,
            "title":
            commit_description.title,
            "url":
            revision_url,
            "date_created":
            PhabricatorClient.to_datetime(
                PhabricatorClient.expect(revision, "fields",
                                         "dateCreated")).isoformat(),
            "date_modified":
            PhabricatorClient.to_datetime(
                PhabricatorClient.expect(revision, "fields",
                                         "dateModified")).isoformat(),
            "summary":
            commit_description.summary,
            "commit_message_title":
            commit_message_title,
            "commit_message":
            commit_message,
            "repo_phid":
            PhabricatorClient.expect(fields, "repositoryPHID"),
            "diff":
            serialize_diff(diff),
            "author":
            author_response,
            "reviewers":
            serialize_reviewers(reviewers, users, projects, diff_phid),
            "is_secure":
            secure,
            "is_using_secure_commit_message":
            commit_description.sanitized,
        })

    repositories = []
    for phid in stack_data.repositories.keys():
        short_name = PhabricatorClient.expect(stack_data.repositories[phid],
                                              "fields", "shortName")
        repo = supported_repos.get(short_name)
        if repo is None:
            landing_supported, approval_required = False, None
        else:
            landing_supported, approval_required = True, repo.approval_required
        url = ("{phabricator_url}/source/{short_name}".format(
            phabricator_url=current_app.config["PHABRICATOR_URL"],
            short_name=short_name,
        ) if not landing_supported else supported_repos[short_name].url)
        repositories.append({
            "phid": phid,
            "short_name": short_name,
            "url": url,
            "landing_supported": landing_supported,
            "approval_required": approval_required,
        })

    return {
        "repositories": repositories,
        "revisions": revisions_response,
        "edges": [e for e in edges],
        "landable_paths": landable,
        "uplift_repositories": uplift_repos,
    }
Exemple #9
0
def get(revision_id):
    """Get the stack a revision is part of.

    Args:
        revision_id: (string) ID of the revision in 'D{number}' format
    """
    revision_id = revision_id_to_int(revision_id)

    phab = g.phabricator
    revision = phab.call_conduit(
        'differential.revision.search',
        constraints={'ids': [revision_id]},
    )
    revision = phab.single(revision, 'data', none_when_empty=True)
    if revision is None:
        return problem(
            404,
            'Revision not found',
            'The requested revision does not exist',
            type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404'
        )

    # TODO: This assumes that all revisions and related objects in the stack
    # have uniform view permissions for the requesting user. Some revisions
    # being restricted could cause this to fail.
    nodes, edges = build_stack_graph(phab, phab.expect(revision, 'phid'))
    stack_data = request_extended_revision_data(phab, [phid for phid in nodes])

    supported_repos = get_repos_for_env(current_app.config.get('ENVIRONMENT'))
    landable_repos = get_landable_repos_for_revision_data(
        stack_data, supported_repos
    )
    landable, blocked = calculate_landable_subgraphs(
        stack_data,
        edges,
        landable_repos,
        other_checks=DEFAULT_OTHER_BLOCKER_CHECKS
    )

    involved_phids = set()
    for revision in stack_data.revisions.values():
        involved_phids.update(gather_involved_phids(revision))

    involved_phids = list(involved_phids)

    users = lazy_user_search(phab, involved_phids)()
    projects = lazy_project_search(phab, involved_phids)()

    revisions_response = []
    for phid, revision in stack_data.revisions.items():
        revision_phid = PhabricatorClient.expect(revision, 'phid')
        fields = PhabricatorClient.expect(revision, 'fields')
        diff_phid = PhabricatorClient.expect(fields, 'diffPHID')
        diff = stack_data.diffs[diff_phid]
        human_revision_id = 'D{}'.format(
            PhabricatorClient.expect(revision, 'id')
        )
        revision_url = urllib.parse.urljoin(
            current_app.config['PHABRICATOR_URL'], human_revision_id
        )
        title = PhabricatorClient.expect(fields, 'title')
        summary = PhabricatorClient.expect(fields, 'summary')
        bug_id = get_bugzilla_bug(revision)
        reviewers = get_collated_reviewers(revision)
        accepted_reviewers = [
            reviewer_identity(phid, users, projects).identifier
            for phid, r in reviewers.items()
            if r['status'] is ReviewerStatus.ACCEPTED
        ]
        commit_message_title, commit_message = format_commit_message(
            title, bug_id, accepted_reviewers, summary, revision_url
        )
        author_response = serialize_author(
            phab.expect(fields, 'authorPHID'), users
        )

        revisions_response.append({
            'id': human_revision_id,
            'phid': revision_phid,
            'status': serialize_status(revision),
            'blocked_reason': blocked.get(revision_phid, ''),
            'bug_id': bug_id,
            'title': title,
            'url': revision_url,
            'date_created': PhabricatorClient.to_datetime(
                PhabricatorClient.expect(revision, 'fields', 'dateCreated')
            ).isoformat(),
            'date_modified': PhabricatorClient.to_datetime(
                PhabricatorClient.expect(revision, 'fields', 'dateModified')
            ).isoformat(),
            'summary': summary,
            'commit_message_title': commit_message_title,
            'commit_message': commit_message,
            'diff': serialize_diff(diff),
            'author': author_response,
            'reviewers': serialize_reviewers(
                reviewers, users, projects, diff_phid
            ),
        })  # yapf: disable

    return {
        'revisions': revisions_response,
        'edges': [e for e in edges],
        'landable_paths': landable,
    }
Exemple #10
0
def test_convertion_success():
    assert revision_id_to_int('D123') == 123
Exemple #11
0
def test_convertion_failure_integer():
    with pytest.raises(TypeError):
        revision_id_to_int(123)
Exemple #12
0
def test_convertion_failure_string(id):
    with pytest.raises(ProblemException):
        revision_id_to_int(id)
Exemple #13
0
def request_sec_approval(data=None):
    """Update a Revision with a sanitized commit message.

    Kicks off the sec-approval process.

    See https://wiki.mozilla.org/Security/Bug_Approval_Process.

    Args:
        revision_id: The ID of the revision that will have a sanitized commit
            message. e.g. D1234.
        sanitized_message: The sanitized commit message.
    """
    phab = g.phabricator

    revision_id = revision_id_to_int(data["revision_id"])
    alt_message = data["sanitized_message"]

    logger.info(
        "Got request for sec-approval review of revision",
        extra=dict(revision_phid=revision_id),
    )

    if not alt_message:
        return problem(
            400,
            "Empty commit message text",
            "The sanitized commit message text cannot be empty",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400",
        )

    # FIXME: this is repeated in numerous places in the code. Needs refactoring!
    revision = phab.call_conduit(
        "differential.revision.search",
        constraints={"ids": [revision_id]},
        attachments={"projects": True},
    )
    revision = phab.single(revision, "data", none_when_empty=True)
    if revision is None:
        return problem(
            404,
            "Revision not found",
            "The requested revision does not exist",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404",
        )

    # Only secure revisions are allowed to follow the sec-approval process.
    if not revision_is_secure(revision, get_secure_project_phid(phab)):
        return problem(
            400,
            "Operation only allowed for secure revisions",
            "Only security-sensitive revisions can be given sanitized commit messages",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400",
        )

    resulting_transactions = send_sanitized_commit_message_for_review(
        revision["phid"], alt_message, phab
    )

    # Save the transactions that added the sec-approval comment so we can
    # quickly fetch the comment from Phabricator later in the process.
    #
    # NOTE: Each call to Phabricator returns two transactions: one for adding the
    # comment and one for adding the reviewer.  We don't know which transaction is
    # which at this point so we record both of them.
    sa_request = SecApprovalRequest.build(revision, resulting_transactions)
    db.session.add(sa_request)
    db.session.commit()

    return {}, 200
Exemple #14
0
def unmarshal_landing_request(data):
    return (revision_id_to_int(data['revision_id']), data['diff_id'])
Exemple #15
0
def get(revision_id):
    """Get the stack a revision is part of.

    Args:
        revision_id: (string) ID of the revision in 'D{number}' format
    """
    revision_id = revision_id_to_int(revision_id)

    phab = g.phabricator
    revision = phab.call_conduit("differential.revision.search",
                                 constraints={"ids": [revision_id]})
    revision = phab.single(revision, "data", none_when_empty=True)
    if revision is None:
        return not_found_problem

    try:
        nodes, edges = build_stack_graph(phab, phab.expect(revision, "phid"))
    except PhabricatorAPIException:
        # If a revision within the stack causes an API exception, treat the whole stack
        # as not found.
        return not_found_problem
    stack_data = request_extended_revision_data(phab, [phid for phid in nodes])

    supported_repos = get_repos_for_env(current_app.config.get("ENVIRONMENT"))
    landable_repos = get_landable_repos_for_revision_data(
        stack_data, supported_repos)

    other_checks = get_blocker_checks(
        repositories=supported_repos,
        relman_group_phid=get_relman_group_phid(phab))

    landable, blocked = calculate_landable_subgraphs(stack_data,
                                                     edges,
                                                     landable_repos,
                                                     other_checks=other_checks)
    uplift_repos = [
        name for name, repo in supported_repos.items()
        if repo.approval_required
    ]

    involved_phids = set()
    for revision in stack_data.revisions.values():
        involved_phids.update(gather_involved_phids(revision))

    involved_phids = list(involved_phids)

    users = user_search(phab, involved_phids)
    projects = project_search(phab, involved_phids)

    secure_project_phid = get_secure_project_phid(phab)
    sec_approval_project_phid = get_sec_approval_project_phid(phab)

    revisions_response = []
    for _phid, revision in stack_data.revisions.items():
        revision_phid = PhabricatorClient.expect(revision, "phid")
        fields = PhabricatorClient.expect(revision, "fields")
        diff_phid = PhabricatorClient.expect(fields, "diffPHID")
        diff = stack_data.diffs[diff_phid]
        human_revision_id = "D{}".format(
            PhabricatorClient.expect(revision, "id"))
        revision_url = urllib.parse.urljoin(
            current_app.config["PHABRICATOR_URL"], human_revision_id)
        secure = revision_is_secure(revision, secure_project_phid)
        commit_description = find_title_and_summary_for_display(
            phab, revision, secure)
        bug_id = get_bugzilla_bug(revision)
        reviewers = get_collated_reviewers(revision)
        accepted_reviewers = reviewers_for_commit_message(
            reviewers, users, projects, sec_approval_project_phid)
        commit_message_title, commit_message = format_commit_message(
            commit_description.title,
            bug_id,
            accepted_reviewers,
            commit_description.summary,
            revision_url,
        )
        author_response = serialize_author(phab.expect(fields, "authorPHID"),
                                           users)

        revisions_response.append({
            "id":
            human_revision_id,
            "phid":
            revision_phid,
            "status":
            serialize_status(revision),
            "blocked_reason":
            blocked.get(revision_phid, ""),
            "bug_id":
            bug_id,
            "title":
            commit_description.title,
            "url":
            revision_url,
            "date_created":
            PhabricatorClient.to_datetime(
                PhabricatorClient.expect(revision, "fields",
                                         "dateCreated")).isoformat(),
            "date_modified":
            PhabricatorClient.to_datetime(
                PhabricatorClient.expect(revision, "fields",
                                         "dateModified")).isoformat(),
            "summary":
            commit_description.summary,
            "commit_message_title":
            commit_message_title,
            "commit_message":
            commit_message,
            "repo_phid":
            PhabricatorClient.expect(fields, "repositoryPHID"),
            "diff":
            serialize_diff(diff),
            "author":
            author_response,
            "reviewers":
            serialize_reviewers(reviewers, users, projects, diff_phid),
            "is_secure":
            secure,
            "is_using_secure_commit_message":
            commit_description.sanitized,
        })

    repositories = []
    for phid in stack_data.repositories.keys():
        short_name = PhabricatorClient.expect(stack_data.repositories[phid],
                                              "fields", "shortName")

        repo = supported_repos.get(short_name)
        landing_supported = repo is not None
        url = (repo.url if landing_supported else
               f"{current_app.config['PHABRICATOR_URL']}/source/{short_name}")

        repositories.append({
            "approval_required": landing_supported and repo.approval_required,
            "commit_flags": repo.commit_flags if repo else [],
            "landing_supported": landing_supported,
            "phid": phid,
            "short_name": short_name,
            "url": url,
        })

    return {
        "repositories": repositories,
        "revisions": revisions_response,
        "edges": [e for e in edges],
        "landable_paths": landable,
        "uplift_repositories": uplift_repos,
    }