def test_previously_landed_but_landed_since_still_warns(
    db, client, phabdouble, transfactory, auth0_mock
):
    diff1 = phabdouble.diff()
    revision = phabdouble.revision(diff=diff1, repo=phabdouble.repo())
    diff2 = phabdouble.diff(revision=revision)

    db.session.add(
        Transplant(
            request_id=1,
            revision_to_diff_id={str(revision['id']): diff1['id']},
            revision_order=[str(revision['id'])],
            requester_email='*****@*****.**',
            tree='mozilla-central',
            repository_url='http://hg.test',
            status=TransplantStatus.landed,
            result=('X' * 40)
        )
    )
    db.session.commit()

    db.session.add(
        Transplant(
            request_id=2,
            revision_to_diff_id={str(revision['id']): diff2['id']},
            revision_order=[str(revision['id'])],
            requester_email='*****@*****.**',
            tree='mozilla-central',
            repository_url='http://hg.test',
            status=TransplantStatus.failed,
            result=('X' * 40)
        )
    )
    db.session.commit()

    response = client.post(
        '/landings/dryrun',
        json=dict(
            revision_id='D{}'.format(revision['id']), diff_id=diff2['id']
        ),
        headers=auth0_mock.mock_headers,
        content_type='application/json',
    )
    assert response.status_code == 200
    assert response.json['warnings'][0] == {
        'id': 'W002',
        'message': (
            'Another diff ({landed_diff_id}) of this revision has already '
            'landed as commit {commit_sha}. Unless this change has been '
            'backed out, new changes should use a new revision.'.format(
                landed_diff_id=diff1['id'], commit_sha='X'*40
            )
        ),
    }  # yapf: disable
Exemple #2
0
def test_transplant_failure_update_notifies(db, client, monkeypatch):
    db.session.add(
        Transplant(
            request_id=1,
            revision_to_diff_id={str(1): 1},
            revision_order=[str(1)],
            requester_email="*****@*****.**",
            tree="mozilla-central",
            repository_url="http://hg.test",
            status=TransplantStatus.submitted,
        ))
    db.session.commit()

    mock_notify = MagicMock(notify_user_of_landing_failure)
    monkeypatch.setattr("landoapi.api.landings.notify_user_of_landing_failure",
                        mock_notify)

    # Send a message that looks like a transplant failure to land.
    response = client.post(
        "/landings/update",
        json={
            "request_id": 1,
            "landed": False,
            "error_msg": "This failed!"
        },
        headers=[("API-Key", "someapikey")],
    )

    assert response.status_code == 200
    assert mock_notify.called
def test_notify_user_of_landing_failure(check_celery, app, celery_worker,
                                        smtp):
    # Happy-path test for all objects that collaborate to send emails. We don't check
    # for an observable effect of sending emails in this test because the
    # celery_worker fixture causes the test to cross threads.  We only ensure the
    # happy-path runs cleanly.
    notify_user_of_landing_failure(Transplant(revision_order=["1"]))
def get_list(stack_revision_id):
    """Return a list of Transplant objects"""
    revision_id = revision_id_to_int(stack_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 revision does not exist or you lack permission to see it.",
            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"))
    revision_phids = list(nodes)
    revs = phab.call_conduit(
        "differential.revision.search",
        constraints={"phids": revision_phids},
        limit=len(revision_phids),
    )

    transplants = Transplant.revisions_query(
        [phab.expect(r, "id") for r in phab.expect(revs, "data")]).all()
    return [t.serialize() for t in transplants], 200
Exemple #5
0
def test_land_failed_revision(db, client, auth0_mock, phabdouble, s3,
                              transfactory, status):
    diff = phabdouble.diff()
    revision = phabdouble.revision(diff=diff, repo=phabdouble.repo())
    phabdouble.reviewer(revision, phabdouble.user(username='******'))
    _create_transplant(
        db,
        revision_id=revision['id'],
        diff_id=diff['id'],
        status=status,
    )
    transfactory.mock_successful_response(2)

    response = client.post('/landings',
                           data=json.dumps({
                               'revision_id':
                               'D{}'.format(revision['id']),
                               'diff_id':
                               diff['id'],
                           }),
                           headers=auth0_mock.mock_headers,
                           content_type='application/json')
    assert response.status_code == 202

    # Ensure DB access isn't using uncommitted data.
    db.session.close()

    assert Transplant.is_revision_submitted(revision['id'])
Exemple #6
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,
        )
    )
Exemple #7
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()
Exemple #8
0
def get_list(stack_revision_id):
    """Return a list of Transplant objects"""
    revision_id = revision_id_to_int(stack_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 revision does not exist or you lack permission to see it.",
            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"))
    revision_phids = list(nodes)
    revs = phab.call_conduit(
        "differential.revision.search",
        constraints={"phids": revision_phids},
        limit=len(revision_phids),
    )

    # Return both transplants and landing jobs, since for repos that were switched
    # both or either of these could be populated.

    rev_ids = [phab.expect(r, "id") for r in phab.expect(revs, "data")]

    transplants = Transplant.revisions_query(rev_ids).all()
    landing_jobs = LandingJob.revisions_query(rev_ids).all()

    if transplants and landing_jobs:
        logger.warning(
            "Both {} transplants and {} landing jobs found for this revision".format(
                str(len(transplants)), str(len(landing_jobs))
            )
        )

    return (
        [t.serialize() for t in transplants] + [j.serialize() for j in landing_jobs],
        200,
    )
Exemple #9
0
    def check(cls, *, revision_id, diff_id, **kwargs):
        already_submitted = Transplant.is_revision_submitted(revision_id)
        if not already_submitted:
            return None

        submit_diff = already_submitted.revision_to_diff_id[str(revision_id)]

        if diff_id == submit_diff:
            return cls(
                'This revision is already queued for landing with '
                'the same diff.'
            )
        else:
            return cls(
                'This revision is already queued for landing with '
                'diff {}'.format(submit_diff)
            )
Exemple #10
0
def _create_transplant(db,
                       request_id=1,
                       revision_id=1,
                       diff_id=1,
                       requester_email='*****@*****.**',
                       tree='mozilla-central',
                       repository_url='http://hg.test',
                       status=TransplantStatus.submitted):
    transplant = Transplant(request_id=request_id,
                            revision_to_diff_id={str(revision_id): diff_id},
                            revision_order=[str(revision_id)],
                            requester_email=requester_email,
                            tree=tree,
                            repository_url=repository_url,
                            status=status)
    db.session.add(transplant)
    db.session.commit()
    return transplant
Exemple #11
0
def get_list(revision_id):
    """API endpoint at GET /landings to return a list of Landing objects."""
    # Verify that the client is permitted to see the associated revision.
    revision_id = revision_id_to_int(revision_id)
    revision = g.phabricator.call_conduit(
        'differential.revision.search',
        constraints={'ids': [revision_id]},
    )
    revision = g.phabricator.expect(revision, 'data')
    revision = g.phabricator.single(revision, none_when_empty=True)
    if not revision:
        return problem(
            404,
            'Revision not found',
            'The revision does not exist or you lack permission to see it.',
            type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404'
        )

    transplants = Transplant.revisions_query([revision_id]).all()
    return [t.legacy_serialize() for t in transplants], 200
def _create_transplant(db,
                       *,
                       request_id=1,
                       landing_path=((1, 1), ),
                       requester_email="*****@*****.**",
                       tree="mozilla-central",
                       repository_url="http://hg.test",
                       status=TransplantStatus.submitted):
    transplant = Transplant(
        request_id=request_id,
        revision_to_diff_id={str(r_id): d_id
                             for r_id, d_id in landing_path},
        revision_order=[str(r_id) for r_id, _ in landing_path],
        requester_email=requester_email,
        tree=tree,
        repository_url=repository_url,
        status=status,
    )
    db.session.add(transplant)
    db.session.commit()
    return transplant
Exemple #13
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
        )
    )
Exemple #14
0
def test_revision_already_submitted(db, status, considered_submitted):
    landing = _create_transplant(db, status=status, diff_id=2)
    if considered_submitted:
        assert Transplant.is_revision_submitted(1) == landing
    else:
        assert not Transplant.is_revision_submitted(1)
Exemple #15
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)

    if not all((to_land, landing_repo, stack_data)):
        raise ValueError(
            "One or more values missing in access transplant request: "
            f"{to_land}, {landing_repo}, {stack_data}"
        )

    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()

    revisions = [r[0] for r in to_land]

    for revision in revisions:
        involved_phids.update(gather_involved_phids(revision))

    involved_phids = list(involved_phids)
    users = user_search(phab, involved_phids)
    projects = project_search(phab, involved_phids)

    secure_project_phid = get_secure_project_phid(phab)

    # Take note of any revisions that the checkin project tag must be
    # removed from.
    checkin_phid = get_checkin_project_phid(phab)
    checkin_revision_phids = [
        r["phid"]
        for r in revisions
        if checkin_phid in phab.expect(r, "attachments", "projects", "projectPHIDs")
    ]

    sec_approval_project_phid = get_sec_approval_project_phid(phab)

    # Build the patches to land.
    patch_urls = []
    for revision, diff in to_land:
        reviewers = get_collated_reviewers(revision)
        accepted_reviewers = reviewers_for_commit_message(
            reviewers, users, projects, sec_approval_project_phid
        )

        secure = revision_is_secure(revision, secure_project_phid)
        commit_description = find_title_and_summary_for_landing(phab, revision, secure)

        commit_message = format_commit_message(
            commit_description.title,
            get_bugzilla_bug(revision),
            accepted_reviewers,
            commit_description.summary,
            urllib.parse.urljoin(
                current_app.config["PHABRICATOR_URL"], "D{}".format(revision["id"])
            ),
        )[1]
        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)

    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 revisions]
    stack_ids = [r["id"] for r in stack_data.revisions.values()]

    submitted_assessment = TransplantAssessment(
        blocker=(
            "This stack was submitted for landing by another user at the same time."
        )
    )

    if landing_repo.transplant_locally:
        with db.session.begin_nested():
            _lock_table_for(db.session, model=LandingJob)
            if (
                LandingJob.revisions_query(stack_ids)
                .filter(
                    LandingJob.status.in_(
                        [LandingJobStatus.SUBMITTED, LandingJobStatus.IN_PROGRESS]
                    )
                )
                .count()
                != 0
            ):
                submitted_assessment.raise_if_blocked_or_unacknowledged(None)

            # Trigger a local transplant
            job = LandingJob(
                status=LandingJobStatus.SUBMITTED,
                requester_email=ldap_username,
                repository_name=landing_repo.tree,
                repository_url=landing_repo.url,
                revision_to_diff_id=revision_to_diff_id,
                revision_order=revision_order,
            )

            db.session.add(job)

        db.session.commit()
        logger.info("New landing job {job.id} created for {landing_repo.tree} repo")

        # NOTE: the response body is not being used anywhere.
        return {"id": job.id}, 202

    trans = TransplantClient(
        current_app.config["TRANSPLANT_URL"],
        current_app.config["TRANSPLANT_USERNAME"],
        current_app.config["TRANSPLANT_PASSWORD"],
    )

    # 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():
            _lock_table_for(db.session, model=Transplant)
            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},
    )

    # Asynchronously remove the checkin project from any of the landing
    # revisions that had it.
    for r_phid in checkin_revision_phids:
        try:
            admin_remove_phab_project.apply_async(
                args=(r_phid, checkin_phid),
                kwargs=dict(comment=f"#{CHECKIN_PROJ_SLUG} handled, landing queued."),
            )
        except kombu.exceptions.OperationalError:
            # Best effort is acceptable here, Transplant *is* going to land
            # these changes so it's better to return properly from the request.
            pass

    return {"id": transplant.id}, 202
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 test_display_branch_head():
    assert Transplant(revision_order=["1", "2"]).head_revision == "D2"
Exemple #18
0
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
Exemple #19
0
def test_revision_not_submitted(db, status):
    _create_transplant(db, status=status)
    assert not Transplant.is_revision_submitted(1)