Beispiel #1
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."
Beispiel #2
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
Beispiel #3
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'))
Beispiel #4
0
def serialize_status(revision):
    status_value = PhabricatorClient.expect(revision, "fields", "status", "value")
    status = RevisionStatus.from_status(status_value)

    if status is RevisionStatus.UNEXPECTED_STATUS:
        logger.warning(
            "Revision had unexpected status",
            extra={
                "id": PhabricatorClient.expection(revision, "id"),
                "value": status_value,
            },
        )
        return {"closed": False, "value": None, "display": "Unknown"}

    return {
        "closed": status.closed,
        "value": status.value,
        "display": status.output_name,
    }
Beispiel #5
0
def serialize_status(revision):
    status_value = PhabricatorClient.expect(revision, 'fields', 'status',
                                            'value')
    status = RevisionStatus.from_status(status_value)

    if status is RevisionStatus.UNEXPECTED_STATUS:
        logger.warning('Revision had unexpected status',
                       extra={
                           'id': PhabricatorClient.expection(revision, 'id'),
                           'value': status_value,
                       })
        return {
            'closed': False,
            'value': None,
            'display': 'Unknown',
        }

    return {
        'closed': status.closed,
        'value': status.value,
        'display': status.output_name,
    }
def calculate_landable_subgraphs(
    revision_data, edges, landable_repos, *, other_checks=[]
):
    """Return a list of landable DAG paths.

    Args:
        revision_data: A RevisionData with data for all phids present
            in `edges`.
        edges: a set of tuples (child, parent) each representing an edge
            between two nodes. `child` and `parent` are also string
            PHIDs.
        landable_repos: a set of string PHIDs for repositories that
            are supported for landing.
        other_checks: An iterable of callables which will be executed
            for each revision to determine if it should be blocked. These
            checks must not rely on the structure the stack graph takes,
            instead they may check only properties specific to a single
            revision. If a revision is blocked for other reasons it's
            possible that a check may not be called for that revision. The
            callables must have the following signature:
                Args:
                    revision: A dictionary of revision data.
                    diff: A dictionary of diff data for the diff of the
                        given revision.
                    repo: A dictionary of repository data for the repository
                        the revision applies to.
                Returns:
                    None if the check passed and doesn't block the revision,
                    a string containing the reason if the check fails and
                    should block.

    Returns:
        A 2-tuple of (landable_paths, blockers). landable_paths is
        a list of lists with each sub-list being a series of PHID strings
        which identify a DAG path. These paths are the set of landable
        paths in the revision stack. blockers is a dictionary mapping
        revision phid to a string reason for that revision being blocked.
        Revisions appearing in landable_paths will not have an entry
        in blockers.
    """
    # We need to indicate why a revision isn't landable, so keep track
    # of the reason whenever we remove a revision from consideration.
    blocked = {}

    def block(node, reason):
        if node not in blocked:
            blocked[node] = reason

    # We won't land anything that has a repository we don't support, so make
    # a pass over all the revisions and block these.
    for phid, revision in revision_data.revisions.items():
        if (
            PhabricatorClient.expect(revision, "fields", "repositoryPHID")
            not in landable_repos
        ):
            block(phid, "Repository is not supported by Lando.")

    # We only want to consider paths starting from the open revisions
    # do grab the status for all revisions.
    statuses = {
        phid: RevisionStatus.from_status(
            PhabricatorClient.expect(revision, "fields", "status", "value")
        )
        for phid, revision in revision_data.revisions.items()
    }

    # Mark all closed revisions as blocked.
    for phid, status in statuses.items():
        if status.closed:
            block(phid, "Revision is closed.")

    # We need to walk from the roots to the heads to build the landable
    # subgraphs so identify the roots and insantiate a RevisionStack
    # to use its adjacency lists.
    stack = RevisionStack(set(revision_data.revisions.keys()), edges)
    roots = {phid for phid in stack.nodes if not stack.parents[phid]}

    # All of the roots may not be open so we need to walk from them
    # and find the first open revision along each path.
    to_process = roots
    roots = set()
    while to_process:
        phid = to_process.pop()
        if not statuses[phid].closed:
            roots.add(phid)
            continue

        to_process.update(stack.children[phid])

    # Because `roots` may no longer contain just true roots of the DAG,
    # a "root" could be the descendent of another. Filter out these "roots".
    to_process = set()
    for root in roots:
        to_process.update(stack.children[root])
    while to_process:
        phid = to_process.pop()
        roots.discard(phid)
        to_process.update(stack.children[phid])

    # Filter out roots that we have blocked already.
    roots = roots - blocked.keys()

    # Do a pass over the roots to check if they're blocked, so we only
    # start landable paths with unblocked roots.
    to_process = roots
    roots = set()
    for root in to_process:
        reason = _blocked_by(
            root, revision_data, statuses, stack, blocked, other_checks=other_checks
        )
        if reason is None:
            roots.add(root)
        else:
            block(root, reason)

    # Now walk from the unblocked roots to identify landable paths.
    landable = roots.copy()
    paths = []
    to_process = [[root] for root in roots]
    while to_process:
        path = to_process.pop()

        valid_children = []
        for child in stack.children[path[-1]]:
            if statuses[child].closed:
                continue

            reason = _blocked_by(
                child,
                revision_data,
                statuses,
                stack,
                blocked,
                other_checks=other_checks,
            )
            if reason is None:
                valid_children.append(child)
                landable.add(child)
            else:
                block(child, reason)

        if valid_children:
            to_process.extend([path + [child] for child in valid_children])
        else:
            paths.append(path)

    # Do one final pass to set blocked for anything that's not landable and
    # and hasn't already been marked blocked. These are the descendents we
    # never managed to reach walking the landable paths.
    for phid in stack.nodes - landable - set(blocked.keys()):
        block(phid, "Has an open ancestor revision that is blocked.")

    return paths, blocked
def test_revision_status_uknown_values(v):
    assert RevisionStatus.from_status(v) is RevisionStatus.UNEXPECTED_STATUS