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."
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
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'))
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 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