class PhabricatorActions(object): ''' Common Phabricator actions shared across clients ''' def __init__(self, url, api_key, retries=5, sleep=10): self.api = PhabricatorAPI(url=url, api_key=api_key) # Phabricator secure revision retries configuration assert isinstance(retries, int) assert isinstance(sleep, int) self.retries = collections.defaultdict(lambda: (retries, None)) self.sleep = sleep logger.info('Will retry Phabricator secure revision queries', retries=retries, sleep=sleep) # Load secure projects projects = self.api.search_projects(slugs=['secure-revision']) self.secure_projects = { p['phid']: p['fields']['name'] for p in projects } logger.info('Loaded secure projects', projects=self.secure_projects.values()) def update_state(self, build): ''' Check the visibility of the revision, by retrying N times with a specified time This method is executed regularly by the client application to check on the status evolution as the BMO daemon can take several minutes to update the status ''' assert isinstance(build, PhabricatorBuild) # Only when queued if build.state != PhabricatorBuildState.Queued: return # Check this build has some retries left retries_left, last_try = self.retries[build.target_phid] if retries_left <= 0: return # Check this build has been awaited between tries now = time.time() if last_try is not None and now - last_try < self.sleep: return # Now we can check if this revision is public retries_left -= 1 self.retries[build.target_phid] = (retries_left, now) logger.info('Checking visibility status', build=str(build), retries_left=retries_left) if self.is_visible(build): build.state = PhabricatorBuildState.Public logger.info('Revision is public', build=str(build)) elif retries_left <= 0: # Mark as secured when no retries are left build.state = PhabricatorBuildState.Secured logger.info('Revision is marked as secure', build=str(build)) else: # Enqueue back to retry later build.state = PhabricatorBuildState.Queued def is_visible(self, build): ''' Check the visibility of the revision by loading its details ''' assert isinstance(build, PhabricatorBuild) assert build.state == PhabricatorBuildState.Queued try: # Load revision with projects build.revision = self.api.load_revision(rev_id=build.revision_id, attachments={ 'projects': True, 'reviewers': True }) if not build.revision: raise Exception('Not found') # Check against secure projects projects = set( build.revision['attachments']['projects']['projectPHIDs']) if projects.intersection(self.secure_projects): raise Exception('Secure revision') except Exception as e: logger.info('Revision not accessible', build=str(build), error=str(e)) return False return True def load_patches_stack(self, build): ''' Load a stack of patches for a public Phabricator build without hitting a local mercurial repository ''' build.stack = self.api.load_patches_stack(build.diff_id, build.diff) def load_reviewers(self, build): ''' Load details for reviewers found on a build ''' assert isinstance(build, PhabricatorBuild) assert build.state == PhabricatorBuildState.Public assert build.revision is not None reviewers = build.revision['attachments']['reviewers']['reviewers'] build.reviewers = [ self.api.load_user(user_phid=reviewer['reviewerPHID']) for reviewer in reviewers ]
class PhabricatorActions(object): """ Common Phabricator actions shared across clients """ def __init__(self, url, api_key, retries=5, sleep=10): self.api = PhabricatorAPI(url=url, api_key=api_key) # Phabricator secure revision retries configuration assert isinstance(retries, int) assert isinstance(sleep, int) self.max_retries = retries self.retries = collections.defaultdict(lambda: (retries, None)) self.sleep = sleep logger.info( "Will retry Phabricator secure revision queries", retries=retries, sleep=sleep, ) # Load secure projects projects = self.api.search_projects(slugs=["secure-revision"]) self.secure_projects = { p["phid"]: p["fields"]["name"] for p in projects } logger.info("Loaded secure projects", projects=self.secure_projects.values()) def update_state(self, build): """ Check the visibility of the revision, by retrying N times with an exponential backoff time This method is executed regularly by the client application to check on the status evolution as the BMO daemon can take several minutes to update the status """ assert isinstance(build, PhabricatorBuild) # Only when queued if build.state != PhabricatorBuildState.Queued: return # Check this build has some retries left retries_left, last_try = self.retries[build.target_phid] if retries_left <= 0: return # Check this build has been awaited between tries exp_backoff = (2**(self.max_retries - retries_left)) * self.sleep now = time.time() if last_try is not None and now - last_try < exp_backoff: return # Now we can check if this revision is public retries_left -= 1 self.retries[build.target_phid] = (retries_left, now) logger.info("Checking visibility status", build=str(build), retries_left=retries_left) if self.is_visible(build): build.state = PhabricatorBuildState.Public build.revision_url = self.build_revision_url(build) logger.info("Revision is public", build=str(build)) elif retries_left <= 0: # Mark as secured when no retries are left build.state = PhabricatorBuildState.Secured logger.info("Revision is marked as secure", build=str(build)) else: # Enqueue back to retry later build.state = PhabricatorBuildState.Queued def is_visible(self, build): """ Check the visibility of the revision by loading its details """ assert isinstance(build, PhabricatorBuild) assert build.state == PhabricatorBuildState.Queued try: # Load revision with projects build.revision = self.api.load_revision( rev_id=build.revision_id, attachments={ "projects": True, "reviewers": True }, ) if not build.revision: raise Exception("Not found") # Check against secure projects projects = set( build.revision["attachments"]["projects"]["projectPHIDs"]) if projects.intersection(self.secure_projects): raise Exception("Secure revision") except Exception as e: logger.info("Revision not accessible", build=str(build), error=str(e)) return False return True def load_patches_stack(self, build): """ Load a stack of patches for a public Phabricator build without hitting a local mercurial repository """ build.stack = self.api.load_patches_stack(build.diff_id, build.diff) def load_reviewers(self, build): """ Load details for reviewers found on a build """ assert isinstance(build, PhabricatorBuild) assert build.state == PhabricatorBuildState.Public assert build.revision is not None def load_user(phid): if phid.startswith("PHID-USER"): return self.api.load_user(user_phid=phid) elif phid.startswith("PHID-PROJ"): logger.info(f"Skipping group reviewer {phid}") else: raise Exception(f"Unsupported reviewer {phid}") reviewers = build.revision["attachments"]["reviewers"]["reviewers"] build.reviewers = list( filter(None, [ load_user(reviewer["reviewerPHID"]) for reviewer in reviewers ])) def build_revision_url(self, build): """ Build a Phabricator frontend url for a build's revision """ return "https://{}/D{}".format(self.api.hostname, build.revision_id)