Esempio n. 1
0
def _render_author_response(phid, user_search_data):
    author = user_search_data.get(phid, {})
    return {
        'phid': PhabricatorClient.expect(author, 'phid'),
        'username': PhabricatorClient.expect(author, 'fields', 'username'),
        'real_name': PhabricatorClient.expect(author, 'fields', 'realName'),
    }
Esempio n. 2
0
def collate_reviewer_attachments(reviewers, reviewers_extra):
    """Return collated reviewer data.

    Args:
        reviewers: Data from the 'reviewers' attachment of
            differential.revision.search.
        reviewers_extra: Data from the 'reviewers-extra'
            attachment of differential.revision.search.
    """
    phids = {}
    for reviewer in reviewers:
        data = {}
        for k in ('reviewerPHID', 'isBlocking', 'actorPHID'):
            data[k] = PhabricatorClient.expect(reviewer, k)

        data['status'] = ReviewerStatus.from_status(
            PhabricatorClient.expect(reviewer, 'status')
        )

        phids[data['reviewerPHID']] = data

    for reviewer in reviewers_extra:
        data = {}
        for k in ('reviewerPHID', 'diffPHID', 'voidedPHID'):
            data[k] = PhabricatorClient.expect(reviewer, k)

        data.update(phids.get(data['reviewerPHID'], {}))
        phids[data['reviewerPHID']] = data

    if len(phids) > min(len(reviewers), len(reviewers_extra)):
        raise PhabricatorCommunicationException(
            'Phabricator responded with unexpected data'
        )

    return phids
Esempio n. 3
0
def warning_previously_landed(*, revision, diff, **kwargs):
    revision_id = PhabricatorClient.expect(revision, "id")
    diff_id = PhabricatorClient.expect(diff, "id")

    landed_transplant = (
        Transplant.revisions_query([revision_id])
        .filter_by(status=TransplantStatus.landed)
        .order_by(Transplant.updated_at.desc())
        .first()
    )

    if landed_transplant is None:
        return None

    landed_diff_id = landed_transplant.revision_to_diff_id[str(revision_id)]
    same = diff_id == landed_diff_id
    only_revision = len(landed_transplant.revision_order) == 1

    return (
        "Already landed with {is_same_string} diff ({landed_diff_id}), "
        "pushed {push_string} {commit_sha}.".format(
            is_same_string=("the same" if same else "an older"),
            landed_diff_id=landed_diff_id,
            push_string=("as" if only_revision else "with new tip"),
            commit_sha=landed_transplant.result,
        )
    )
def get_landable_repos_for_revision_data(revision_data, supported_repos):
    """Return a dictionary mapping string PHID to landable Repo

    Args:
        revision_data: A RevisionData.
        supported_repos: A dictionary mapping repository shortname to
        a Repo for repositories lando supports.

    Returns:
        A dictionary where each key is a string PHID for a repository from
        revision_data and the value is a Repo taken from supported_repos.
        Repositories in revision_data which are unsupported will not be
        present in the dictionary.
    """
    repo_phids = {
        PhabricatorClient.expect(revision, "fields", "repositoryPHID")
        for revision in revision_data.revisions.values()
        if PhabricatorClient.expect(revision, "fields", "repositoryPHID")
    }
    repos = {
        phid: supported_repos.get(
            PhabricatorClient.expect(
                revision_data.repositories[phid], "fields", "shortName"
            )
        )
        for phid in repo_phids
        if phid in revision_data.repositories
    }
    return {phid: repo for phid, repo in repos.items() if repo}
Esempio n. 5
0
def create_approval_request(phab: PhabricatorClient, revision: dict, form_content: str):
    """Update an existing revision with reviewers & form comment"""
    release_managers = get_release_managers(phab)

    rev = phab.call_conduit(
        "differential.revision.edit",
        objectIdentifier=revision["phid"],
        transactions=[
            # Set release managers as reviewers
            {"type": "reviewers.add", "value": [release_managers["phid"]]},
            # Post the form as a comment on the revision
            {"type": "comment", "value": form_content},
        ],
    )
    rev_id = phab.expect(rev, "object", "id")
    rev_phid = phab.expect(rev, "object", "phid")
    assert rev_id == revision["id"], "Revision id mismatch"

    logger.info("Updated Phabricator revision", extra={"id": rev_id, "phid": rev_phid})

    return {
        "mode": "approval",
        "url": f"{phab.url_base}/D{rev_id}",
        "revision_id": rev_id,
        "revision_phid": rev_phid,
    }
Esempio n. 6
0
def check_approval_state(
    phab: PhabricatorClient, revision_id: int, target_repository_name: str
) -> dict:
    """Helper to load the Phabricator revision and check its approval requirement state

    * if the revision's target repository is the same as its current
      repository, it's an approval
    * otherwise it's an uplift request
    """

    # Load target repo from Phabricator
    target_repo = phab.call_conduit(
        "diffusion.repository.search",
        constraints={"shortNames": [target_repository_name]},
    )
    target_repo = phab.single(target_repo, "data")
    target_repo_phid = phab.expect(target_repo, "phid")

    # Load base revision details from Phabricator
    revision = phab.call_conduit(
        "differential.revision.search", constraints={"ids": [revision_id]}
    )
    revision = phab.single(revision, "data")
    revision_repo_phid = phab.expect(revision, "fields", "repositoryPHID")

    # Lookup if this is an uplift or an approval request
    is_approval = target_repo_phid == revision_repo_phid
    return (is_approval, revision, target_repo)
Esempio n. 7
0
def serialize_author(phid, user_search_data):
    out = {"phid": phid, "username": None, "real_name": None}
    author = user_search_data.get(phid)
    if author is not None:
        out["username"] = PhabricatorClient.expect(author, "fields", "username")
        out["real_name"] = PhabricatorClient.expect(author, "fields", "realName")

    return out
Esempio n. 8
0
def check_landing_blockers(
        auth0_user,
        requested_path,
        stack_data,
        landable_paths,
        landable_repos,
        *,
        user_blocks=[user_block_no_auth0_email, user_block_scm_level]):
    revision_path = []
    revision_to_diff_id = {}
    for revision_phid, diff_id in requested_path:
        revision_path.append(revision_phid)
        revision_to_diff_id[revision_phid] = diff_id

    # Check that the provided path is a prefix to, or equal to,
    # a landable path.
    for path in landable_paths:
        if revision_path == path[:len(revision_path)]:
            break
    else:
        return TransplantAssessment(
            blocker="The requested set of revisions are not landable.")

    # Check the requested diffs are the latest.
    for revision_phid in revision_path:
        latest_diff_phid = PhabricatorClient.expect(
            stack_data.revisions[revision_phid], "fields", "diffPHID")
        latest_diff_id = PhabricatorClient.expect(
            stack_data.diffs[latest_diff_phid], "id")

        if latest_diff_id != revision_to_diff_id[revision_phid]:
            return TransplantAssessment(
                blocker="A requested diff is not the latest.")

    # Check if there is already a landing for something in the stack.
    if (Transplant.revisions_query([
            PhabricatorClient.expect(r, "id")
            for r in stack_data.revisions.values()
    ]).filter_by(status=TransplantStatus.submitted).first() is not None):
        return TransplantAssessment(blocker=(
            "A landing for revisions in this stack is already in progress."))

    # To be a landable path the entire path must have the same
    # repository, so we can get away with checking only one.
    repo = landable_repos[stack_data.revisions[revision_path[0]]["fields"]
                          ["repositoryPHID"]]

    # Check anything that would block the current user from
    # landing this.
    for block in user_blocks:
        result = block(auth0_user=auth0_user, landing_repo=repo)
        if result is not None:
            return TransplantAssessment(blocker=result)

    return TransplantAssessment()
Esempio n. 9
0
 def check(cls, *, get_revision, get_diff, **kwargs):
     diff, _ = get_diff()
     revision = get_revision()
     if (PhabricatorClient.expect(revision, 'phid') !=
             PhabricatorClient.expect(diff, 'fields', 'revisionPHID')):
         raise ProblemException(
             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'  # noqa
         )  # yapf: disable
Esempio n. 10
0
def get_raw_comments(transaction):
    """Return a list of 'raw' comment bodies in a Phabricator transaction.

    A single transaction can have multiple comment bodies: e.g. a top-level comment
    and a couple of inline comments along with it.

    See https://phabricator.services.mozilla.com/conduit/method/transaction.search/.
    """
    return [
        PhabricatorClient.expect(comment, "content", "raw")
        for comment in PhabricatorClient.expect(transaction, "comments")
    ]
Esempio n. 11
0
def lazy_get_reviewers(revision):
    """Return a dictionary mapping phid to collated reviewer attachment data.

    Args:
        revision: A dict of the revision data from differential.revision.search
            with the 'reviewers' and 'reviewers-extra' attachments.
    """
    attachments = PhabricatorClient.expect(revision, 'attachments')
    return collate_reviewer_attachments(
        PhabricatorClient.expect(attachments, 'reviewers', 'reviewers'),
        PhabricatorClient.expect(attachments, 'reviewers-extra',
                                 'reviewers-extra'))
Esempio n. 12
0
def serialize_author(phid, user_search_data):
    out = {
        'phid': phid,
        'username': None,
        'real_name': None,
    }
    author = user_search_data.get(phid)
    if author is not None:
        out['username'] = PhabricatorClient.expect(author, 'fields',
                                                   'username')
        out['real_name'] = PhabricatorClient.expect(author, 'fields',
                                                    'realName')

    return out
Esempio n. 13
0
def _convert_path_id_to_phid(path, stack_data):
    mapping = {
        PhabricatorClient.expect(r, "id"): PhabricatorClient.expect(r, "phid")
        for r in stack_data.revisions.values()
    }
    try:
        return [(mapping[r], d) for r, d in path]
    except IndexError:
        ProblemException(
            400,
            "Landing Path Invalid",
            "The provided landing_path is not valid.",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400",
        )
Esempio n. 14
0
def _render_diff_response(querydiffs_data):
    return {
        'id': int(PhabricatorClient.expect(querydiffs_data, 'id')),
        'date_created': _epoch_to_isoformat_time(
            PhabricatorClient.expect(querydiffs_data, 'dateCreated')
        ),
        'date_modified': _epoch_to_isoformat_time(
            PhabricatorClient.expect(querydiffs_data, 'dateModified')
        ),
        'author': {
            'name': querydiffs_data.get('authorName', ''),
            'email': querydiffs_data.get('authorEmail', ''),
        },
    }  # yapf: disable
Esempio n. 15
0
def serialize_diff(diff):
    author_name, author_email = select_diff_author(diff)
    fields = PhabricatorClient.expect(diff, "fields")

    return {
        "id": PhabricatorClient.expect(diff, "id"),
        "phid": PhabricatorClient.expect(diff, "phid"),
        "date_created": PhabricatorClient.to_datetime(
            PhabricatorClient.expect(fields, "dateCreated")
        ).isoformat(),
        "date_modified": PhabricatorClient.to_datetime(
            PhabricatorClient.expect(fields, "dateModified")
        ).isoformat(),
        "author": {"name": author_name or "", "email": author_email or ""},
    }
Esempio n. 16
0
def warning_not_accepted(*, revision, **kwargs):
    status = RevisionStatus.from_status(
        PhabricatorClient.expect(revision, "fields", "status", "value"))
    if status is RevisionStatus.ACCEPTED:
        return None

    return status.output_name
Esempio n. 17
0
def _assess_transplant_request(phab, landing_path):
    nodes, edges = _find_stack_from_landing_path(phab, landing_path)
    stack_data = request_extended_revision_data(phab, [phid for phid in nodes])
    landing_path = _convert_path_id_to_phid(landing_path, stack_data)

    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)

    assessment = check_landing_blockers(
        g.auth0_user,
        landing_path,
        stack_data,
        landable,
        landable_repos,
    )
    if assessment.blocker is not None:
        return (assessment, None, None, None)

    # We have now verified that landable_path is valid and is indeed
    # landable (in the sense that it is a landable_subgraph, with no
    # revisions being blocked). Make this clear by using a different
    # value, and assume it going forward.
    valid_path = landing_path

    # Now that we know this is a valid path we can convert it into a list
    # of (revision, diff) tuples.
    to_land = [stack_data.revisions[r_phid] for r_phid, _ in valid_path]
    to_land = [
        (r, stack_data.diffs[PhabricatorClient.expect(r, 'fields',
                                                      'diffPHID')])
        for r in to_land
    ]

    # To be a landable path the entire path must have the same
    # repository, so we can get away with checking only one.
    repo = stack_data.repositories[to_land[0][0]['fields']['repositoryPHID']]
    landing_repo = landable_repos[repo['phid']]

    involved_phids = set()
    for revision, _ in to_land:
        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)()
    reviewers = {
        revision['phid']: get_collated_reviewers(revision)
        for revision, _ in to_land
    }

    assessment = check_landing_warnings(g.auth0_user, to_land, repo,
                                        landing_repo, reviewers, users,
                                        projects)
    return (assessment, to_land, landing_repo, stack_data)
Esempio n. 18
0
def check_author_planned_changes(*, revision, **kwargs):
    status = RevisionStatus.from_status(
        PhabricatorClient.expect(revision, "fields", "status", "value"))
    if status is not RevisionStatus.CHANGES_PLANNED:
        return None

    return "The author has indicated they are planning changes to this revision."
Esempio n. 19
0
def transaction_search(phabricator,
                       object_identifier,
                       transaction_phids=None,
                       limit=100):
    """Yield the Phabricator transactions related to an object.

    See https://phabricator.services.mozilla.com/conduit/method/transaction.search/.

    If the transaction list is larger that one page of API results then the generator
    will call the Phabricator API successive times to fetch the full transaction list.

    Args:
        phabricator: A PhabricatorClient instance.
        object_identifier: An object identifier (PHID or monogram) whose transactions
            we want to fetch.
        transaction_phids: An optional list of specific transactions PHIDs we want to
            find for the given object_identifier.
        limit: Integer keyword, limit the number of records retrieved per API call.
            Default is 100 records.

    Returns:
        Yields individual transactions.
    """
    next_page_start = None

    if transaction_phids:
        constraints = {"phids": transaction_phids}
    else:
        constraints = {}

    while True:
        transactions = phabricator.call_conduit(
            "transaction.search",
            objectIdentifier=object_identifier,
            constraints=constraints,
            limit=limit,
            after=next_page_start,
        )

        yield from PhabricatorClient.expect(transactions, "data")

        next_page_start = PhabricatorClient.expect(transactions, "cursor",
                                                   "after")

        if next_page_start is None:
            # This was the last page of results.
            return
def reviewer_identity(phid, user_search_data, project_search_data):
    if phid in user_search_data:
        return ReviewerIdentity(
            PhabricatorClient.expect(user_search_data, phid, "fields",
                                     "username"),
            PhabricatorClient.expect(user_search_data, phid, "fields",
                                     "realName"),
        )

    if phid in project_search_data:
        name = PhabricatorClient.expect(project_search_data, phid, "fields",
                                        "name")
        return ReviewerIdentity(name, name)

    logger.info("reviewer was missing from user / project search data",
                extra={"phid": phid})
    return ReviewerIdentity("<unknown>", "Unknown User/Project")
Esempio n. 21
0
def lazy_get_revision_status(revision):
    """Return a landoapi.phabricator.RevisionStatus.

    Args:
        revision: A dict of the revision data just as it is returned
            by Phabricator.
    """
    return RevisionStatus.from_status(
        PhabricatorClient.expect(revision, 'fields', 'status', 'value'))
Esempio n. 22
0
def serialize_diff(diff):
    author_name, author_email = select_diff_author(diff)
    fields = PhabricatorClient.expect(diff, 'fields')

    return {
        'id': PhabricatorClient.expect(diff, 'id'),
        'phid': PhabricatorClient.expect(diff, 'phid'),
        'date_created': PhabricatorClient.to_datetime(
            PhabricatorClient.expect(fields, 'dateCreated')
        ).isoformat(),
        'date_modified': PhabricatorClient.to_datetime(
            PhabricatorClient.expect(fields, 'dateModified')
        ).isoformat(),
        'author': {
            'name': author_name or '',
            'email': author_email or '',
        },
    }  # yapf: disable
Esempio n. 23
0
def select_diff_author(diff):
    commits = PhabricatorClient.expect(diff, "attachments", "commits", "commits")
    if not commits:
        return None, None

    authors = [c.get("author", {}) for c in commits]
    authors = Counter((a.get("name"), a.get("email")) for a in authors)
    authors = authors.most_common(1)
    return authors[0][0] if authors else (None, None)
Esempio n. 24
0
def select_diff_author(diff):
    commits = PhabricatorClient.expect(diff, 'attachments', 'commits',
                                       'commits')
    if not commits:
        return None, None

    authors = [c.get('author', {}) for c in commits]
    authors = Counter((a.get('name'), a.get('email')) for a in authors)
    authors = authors.most_common(1)
    return authors[0][0] if authors else (None, None)
Esempio n. 25
0
def reviewer_identity(phid, user_search_data, project_search_data):
    if phid in user_search_data:
        return ReviewerIdentity(
            PhabricatorClient.expect(user_search_data, phid, 'fields',
                                     'username'),
            PhabricatorClient.expect(user_search_data, phid, 'fields',
                                     'realName'))

    if phid in project_search_data:
        name = PhabricatorClient.expect(project_search_data, phid, 'fields',
                                        'name')
        return ReviewerIdentity(name, name)

    logger.info(
        {
            'msg': 'A reviewer was missing from user / project search data',
            'phid': phid,
        }, 'reviewer.unknown')
    return ReviewerIdentity('<unknown>', 'Unknown User/Project')
Esempio n. 26
0
    def check(cls, *, get_revision, get_landing_repo, **kwargs):
        if not PhabricatorClient.expect(get_revision(), 'fields',
                                        'repositoryPHID'):
            return cls(
                'This revision is not associated with a repository. '
                'In order to land, a revision must be associated with a '
                'repository on Phabricator.')

        return None if get_landing_repo() else cls(
            'The repository this revision is associated with is not '
            'supported by Lando at this time.')
Esempio n. 27
0
 def check(cls, *, get_open_parents, **kwargs):
     open_parents = get_open_parents()
     if open_parents:
         open_text = ', '.join(
             'D{}'.format(PhabricatorClient.expect(r, 'id'))
             for r in open_parents)
         if len(open_parents) > 1:
             return cls('This revision depends on the following revisions '
                        'which are still open: {}'.format(open_text))
         else:
             return cls('This revision depends on the following revision '
                        'which is still open: {}'.format(open_text))
Esempio n. 28
0
    def build(cls, revision, transactions):
        """Build a `SecApprovalRequest` object for a transaction list.

        Args:
            revision: The Phabricator API revision object that we requested
                sec-approval for.
            transactions: A list Phabricator transaction data results related to the
                sec-approval event that we want to save.

        Returns:
            A `SecApprovalRequest` that is ready to be added to the session.
        """
        possible_comment_phids = []
        for transaction in transactions:
            phid = PhabricatorClient.expect(transaction, "phid")
            possible_comment_phids.append(phid)

        return cls(
            revision_id=revision["id"],
            diff_phid=PhabricatorClient.expect(revision, "fields", "diffPHID"),
            comment_candidates=possible_comment_phids,
        )
Esempio n. 29
0
def warning_revision_secure(*, revision, secure_project_phid, **kwargs):
    if secure_project_phid is None:
        return None

    revision_project_tags = PhabricatorClient.expect(revision, "attachments",
                                                     "projects",
                                                     "projectPHIDs")
    if secure_project_phid not in revision_project_tags:
        return None

    return (
        "This revision is tied to a secure bug. Ensure that you are following the "
        "Security Bug Approval Process guidelines before landing this changeset."
    )
Esempio n. 30
0
def lazy_get_reviewers_extra_state(reviewers, diff):
    """Return a dictionary mapping phid to extra reviewer state.

    Args:
        reviewers: a dictionary mapping phid to collated reviewer
            attachment data
        diff: diff data from the Phabricator API
    """
    diff_phid = PhabricatorClient.expect(diff[0], 'phid')
    return {
        phid: calculate_review_extra_state(diff_phid, r['status'],
                                           r['diffPHID'], r['voidedPHID'])
        for phid, r in reviewers.items()
    }