def warning_blocking_reviews(*, revision, diff, reviewers, users, projects, **kwargs): reviewer_extra_state = { phid: calculate_review_extra_state(diff["phid"], r["status"], r["diffPHID"], r["voidedPHID"]) for phid, r in reviewers.items() } blocking_phids = [ phid for phid, state in reviewer_extra_state.items() if state["blocking_landing"] ] if not blocking_phids: return None blocking_reviewers = [ "@{}".format(reviewer_identity(phid, users, projects).identifier) for phid in blocking_phids ] if len(blocking_reviewers) > 1: return ("Reviews from {all_but_last_reviewer}, and {last_reviewer} " "are in a state which is intended to prevent landings.".format( all_but_last_reviewer=", ".join(blocking_reviewers[:-1]), last_reviewer=blocking_reviewers[-1], )) return ("The review from {username} is in a state which is " "intended to prevent landings.".format( username=blocking_reviewers[0]))
def _render_reviewers_response( collated_reviewers, user_search_data, project_search_data, diff_phid ): reviewers = [] for phid, r in collated_reviewers.items(): identity = reviewer_identity( phid, user_search_data, project_search_data ) state = calculate_review_extra_state( diff_phid, r['status'], r['diffPHID'], r['voidedPHID'] ) reviewers.append( { 'phid': phid, 'status': r['status'].value, 'for_other_diff': state['for_other_diff'], 'blocking_landing': state['blocking_landing'], 'identifier': identity.identifier, 'full_name': identity.full_name, # Deprecated, remove after lando UI stops use. 'username': identity.identifier, 'real_name': identity.full_name, } ) return reviewers
def check(cls, *, get_reviewers_extra_state, get_reviewer_info, **kwargs): blocking_phids = [ phid for phid, state in get_reviewers_extra_state().items() if state['blocking_landing'] ] if not blocking_phids: return None users, projects = get_reviewer_info() blocking_reviewers = [ '@' + reviewer_identity(phid, users, projects).identifier for phid in blocking_phids ] # yapf: disable if len(blocking_reviewers) > 1: return cls( 'Reviews from {all_but_last_reviewer}, and {last_reviewer} ' 'are in a state which is intended to prevent landings.'.format( all_but_last_reviewer=', '.join(blocking_reviewers[:-1]), last_reviewer=blocking_reviewers[-1], ) ) return cls( 'The review from {username} is in a state which is ' 'intended to prevent landings.'.format( username=blocking_reviewers[0], ) )
def warning_blocking_reviews( *, revision, diff, reviewers, users, projects, **kwargs ): reviewer_extra_state = { phid: calculate_review_extra_state( diff['phid'], r['status'], r['diffPHID'], r['voidedPHID'] ) for phid, r in reviewers.items() } blocking_phids = [ phid for phid, state in reviewer_extra_state.items() if state['blocking_landing'] ] if not blocking_phids: return None blocking_reviewers = [ '@{}'.format(reviewer_identity(phid, users, projects).identifier) for phid in blocking_phids ] if len(blocking_reviewers) > 1: return ( 'Reviews from {all_but_last_reviewer}, and {last_reviewer} ' 'are in a state which is intended to prevent landings.'.format( all_but_last_reviewer=', '.join(blocking_reviewers[:-1]), last_reviewer=blocking_reviewers[-1], ) ) return ( 'The review from {username} is in a state which is ' 'intended to prevent landings.'.format( username=blocking_reviewers[0], ) )
def post(data): phab = g.phabricator landing_path, confirmation_token = _unmarshal_transplant_request(data) logger.info( "transplant requested by user", extra={ "has_confirmation_token": confirmation_token is not None, "landing_path": landing_path, }, ) assessment, to_land, landing_repo, stack_data = _assess_transplant_request( phab, landing_path) assessment.raise_if_blocked_or_unacknowledged(confirmation_token) assert to_land is not None assert landing_repo is not None assert stack_data is not None if assessment.warnings: # Log any warnings that were acknowledged, for auditing. logger.info( "Transplant with acknowledged warnings is being requested", extra={ "landing_path": landing_path, "warnings": [{ "i": w.i, "revision_id": w.revision_id, "details": w.details } for w in assessment.warnings], }, ) involved_phids = set() for revision, _ in to_land: involved_phids.update(gather_involved_phids(revision)) involved_phids = list(involved_phids) users = user_search(phab, involved_phids) projects = project_search(phab, involved_phids) # Build the patches to land. patch_urls = [] for revision, diff in to_land: 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 = format_commit_message( phab.expect(revision, "fields", "title"), get_bugzilla_bug(revision), accepted_reviewers, phab.expect(revision, "fields", "summary"), urllib.parse.urljoin(current_app.config["PHABRICATOR_URL"], "D{}".format(revision["id"])), ) author_name, author_email = select_diff_author(diff) date_modified = phab.expect(revision, "fields", "dateModified") # Construct the patch that will be sent to transplant. raw_diff = phab.call_conduit("differential.getrawdiff", diffID=diff["id"]) patch = build_patch_for_revision(raw_diff, author_name, author_email, commit_message, date_modified) # Upload the patch to S3 patch_url = upload( revision["id"], diff["id"], patch, current_app.config["PATCH_BUCKET_NAME"], aws_access_key=current_app.config["AWS_ACCESS_KEY"], aws_secret_key=current_app.config["AWS_SECRET_KEY"], ) patch_urls.append(patch_url) trans = TransplantClient( current_app.config["TRANSPLANT_URL"], current_app.config["TRANSPLANT_USERNAME"], current_app.config["TRANSPLANT_PASSWORD"], ) submitted_assessment = TransplantAssessment(blocker=( "This stack was submitted for landing by another user at the same time." )) ldap_username = g.auth0_user.email revision_to_diff_id = {str(r["id"]): d["id"] for r, d in to_land} revision_order = [str(r["id"]) for r, _ in to_land] stack_ids = [r["id"] for r in stack_data.revisions.values()] # We pass the revision id of the base of our landing path to # transplant in rev as it must be unique until the request # has been serviced. While this doesn't use Autoland Transplant # to enforce not requesting from the same stack again, Lando # ensures this itself. root_revision_id = to_land[0][0]["id"] try: # WARNING: Entering critical section, do not add additional # code unless absolutely necessary. Acquires a lock on the # transplants table which gives exclusive write access and # prevents readers who are entering this critical section. # See https://www.postgresql.org/docs/9.3/static/explicit-locking.html # for more details on the specifics of the lock mode. with db.session.begin_nested(): db.session.execute( "LOCK TABLE transplants IN SHARE ROW EXCLUSIVE MODE;") if (Transplant.revisions_query(stack_ids).filter_by( status=TransplantStatus.submitted).first() is not None): submitted_assessment.raise_if_blocked_or_unacknowledged(None) transplant_request_id = trans.land( revision_id=root_revision_id, ldap_username=ldap_username, patch_urls=patch_urls, tree=landing_repo.tree, pingback=current_app.config["PINGBACK_URL"], push_bookmark=landing_repo.push_bookmark, ) transplant = Transplant( request_id=transplant_request_id, revision_to_diff_id=revision_to_diff_id, revision_order=revision_order, requester_email=ldap_username, tree=landing_repo.tree, repository_url=landing_repo.url, status=TransplantStatus.submitted, ) db.session.add(transplant) except TransplantError: logger.exception("error creating transplant", extra={"landing_path": landing_path}) return problem( 502, "Transplant not created", "The requested landing_path is valid, but transplant failed." "Please retry your request at a later time.", type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502", ) # Transaction succeeded, commit the session. db.session.commit() logger.info( "transplant created", extra={ "landing_path": landing_path, "transplant_id": transplant.id }, ) return {"id": transplant.id}, 202
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
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 = user_search(phab, involved_phids) projects = 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, "repo_phid": PhabricatorClient.expect(fields, "repositoryPHID"), "diff": serialize_diff(diff), "author": author_response, "reviewers": serialize_reviewers(reviewers, users, projects, diff_phid), } ) repositories = [] for phid in stack_data.repositories.keys(): short_name = PhabricatorClient.expect( stack_data.repositories[phid], "fields", "shortName" ) landing_supported = short_name in supported_repos 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, } ) return { "repositories": repositories, "revisions": revisions_response, "edges": [e for e in edges], "landable_paths": landable, }
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, }
def post(data): """API endpoint at POST /landings to land revision.""" logger.info( 'landing requested by user', extra={ 'path': request.path, 'method': request.method, 'data': data, } ) revision_id, diff_id = unmarshal_landing_request(data) confirmation_token = data.get('confirmation_token') or None phab = g.phabricator get_revision = lazy_get_revision(phab, revision_id) get_latest_diff = lazy_get_latest_diff(phab, get_revision) get_diff = lazy_get_diff(phab, diff_id, get_latest_diff) get_diff_author = lazy_get_diff_author(get_diff) get_latest_landed = lazy(Transplant.legacy_latest_landed)(revision_id) get_repository = lazy_get_repository(phab, get_revision) get_landing_repo = lazy_get_landing_repo( get_repository, current_app.config.get('ENVIRONMENT') ) get_open_parents = lazy_get_open_parents(phab, get_revision) get_reviewers = lazy_get_reviewers(get_revision) get_reviewer_info = lazy_reviewers_search(phab, get_reviewers) get_reviewers_extra_state = lazy_get_reviewers_extra_state( get_reviewers, get_diff ) get_revision_status = lazy_get_revision_status(get_revision) assessment = check_landing_conditions( g.auth0_user, revision_id, diff_id, get_revision, get_latest_diff, get_latest_landed, get_repository, get_landing_repo, get_diff, get_diff_author, get_open_parents, get_reviewers, get_reviewer_info, get_reviewers_extra_state, get_revision_status, short_circuit=True, ) assessment.raise_if_blocked_or_unacknowledged(confirmation_token) if assessment.warnings: # Log any warnings that were acknowledged, for auditing. logger.info( 'Landing with acknowledged warnings is being requested', extra={ 'revision_id': revision_id, 'warnings': [w.serialize() for w in assessment.warnings], } ) # These are guaranteed to return proper data since we're # running after checking_landing_conditions(). revision = get_revision() landing_repo = get_landing_repo() author_name, author_email = get_diff_author() # Collect the usernames of reviewers who have accepted. reviewers = get_reviewers() users, projects = get_reviewer_info() accepted_reviewers = [ reviewer_identity(phid, users, projects).identifier for phid, r in reviewers.items() if r['status'] is ReviewerStatus.ACCEPTED ] # Seconds since Unix Epoch, UTC. date_modified = phab.expect(revision, 'fields', 'dateModified') 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 = format_commit_message( title, bug_id, accepted_reviewers, summary, revision_url ) # Construct the patch that will be sent to transplant. raw_diff = phab.call_conduit('differential.getrawdiff', diffID=diff_id) patch = build_patch_for_revision( raw_diff, author_name, author_email, commit_message[1], date_modified ) # Upload the patch to S3 patch_url = upload( revision_id, diff_id, patch, current_app.config['PATCH_BUCKET_NAME'], aws_access_key=current_app.config['AWS_ACCESS_KEY'], aws_secret_key=current_app.config['AWS_SECRET_KEY'], ) trans = TransplantClient( current_app.config['TRANSPLANT_URL'], current_app.config['TRANSPLANT_USERNAME'], current_app.config['TRANSPLANT_PASSWORD'], ) submitted_assessment = LandingAssessment( blockers=[ LandingInProgress( 'This revision was submitted for landing by another user at ' 'the same time.' ) ] ) ldap_username = g.auth0_user.email try: # WARNING: Entering critical section, do not add additional # code unless absolutely necessary. Acquires a lock on the # transplants table which gives exclusive write access and # prevents readers who are entering this critical section. # See https://www.postgresql.org/docs/9.3/static/explicit-locking.html # for more details on the specifics of the lock mode. with db.session.begin_nested(): db.session.execute( 'LOCK TABLE transplants IN SHARE ROW EXCLUSIVE MODE;' ) if Transplant.is_revision_submitted(revision_id): submitted_assessment.raise_if_blocked_or_unacknowledged(None) transplant_request_id = trans.land( revision_id=revision_id, ldap_username=ldap_username, patch_urls=[patch_url], tree=landing_repo.tree, pingback=current_app.config['PINGBACK_URL'], push_bookmark=landing_repo.push_bookmark ) transplant = Transplant( request_id=transplant_request_id, revision_to_diff_id={str(revision_id): diff_id}, revision_order=[str(revision_id)], requester_email=ldap_username, tree=landing_repo.tree, repository_url=landing_repo.url, status=TransplantStatus.submitted ) db.session.add(transplant) except TransplantError as exc: logger.info( 'error creating transplant', extra={'revision': revision_id}, exc_info=exc ) return problem( 502, 'Landing not created', 'The requested revision does exist, but landing failed.' 'Please retry your request at a later time.', type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502' ) # Transaction succeeded, commit the session. db.session.commit() logger.info( 'transplant created', extra={ 'revision_id': revision_id, 'transplant_id': transplant.id, } ) return {'id': transplant.id}, 202