示例#1
0
文件: client.py 项目: Asana/SGTM
def get_review_for_database_id(pull_request_id: str,
                               review_db_id: str) -> Optional[Review]:
    """Get the PullRequestReview given a pull request and the NUMERIC id id of the review.

    NOTE: `pull_request_id` and `review_db_id are DIFFERENT types of ids.

    The github API has two ids for each object:
        `id`: a base64-encoded string (also known as "node_id").
        `databaseId`: the primary key from the database.

    In this function:
        @pull_request_id is the `id` for the pull request.
        @review_db_id is the `databaseId` for the review.

    Unfortunately, this requires iterating through all reviews on the given pull request.

    See https://developer.github.com/v4/object/repository/#fields
    """
    data = _execute_graphql_query(IterateReviews,
                                  {"pullRequestId": pull_request_id})
    while data["node"]["reviews"]["edges"]:
        try:
            match = next((e["node"] for e in data["node"]["reviews"]["edges"]
                          if e["node"]["databaseId"] == review_db_id))
            return Review(match)
        except StopIteration:
            # no matching reviews, continue.
            data = _execute_graphql_query(
                IterateReviews,
                {
                    "pullRequestId": pull_request_id,
                    "cursor": data["node"]["reviews"]["edges"][-1]["cursor"],
                },
            )
    return None
示例#2
0
文件: client.py 项目: Asana/SGTM
def get_pull_request_and_review(pull_request_id: str,
                                review_id: str) -> Tuple[PullRequest, Review]:
    data = _execute_graphql_query(
        GetPullRequestAndReview,
        {
            "pullRequestId": pull_request_id,
            "reviewId": review_id
        },
    )
    return PullRequest(data["pullRequest"]), Review(data["review"])
示例#3
0
文件: webhook.py 项目: Asana/SGTM
def _handle_pull_request_review_comment(payload: dict):
    """Handle when a pull request review comment is edited or removed.
    When comments are added it either hits:
        1 _handle_issue_comment_webhook (if the comment is on PR itself)
        2 _handle_pull_request_review_webhook (if the comment is on the "Files Changed" tab)
    Note that it hits (2) even if the comment is inline, and doesn't contain a review;
        in those cases Github still creates a review object for it.

    Unfortunately, this payload doesn't contain the node id of the review.
    Instead, it includes a separate, numeric id
    which is stored as `databaseId` on each GraphQL object.

    To get the review, we either:
        (1) query for the comment, and use the `review` edge in GraphQL.
        (2) Iterate through all reviews on the pull request, and find the one whose databaseId matches.
            See get_review_for_database_id()

    We do (1) for comments that were added or edited, but if a comment was just deleted, we have to do (2).

    See https://developer.github.com/v4/object/repository/#fields.
    """
    pull_request_id = payload["pull_request"]["node_id"]
    action = payload["action"]
    comment_id = payload["comment"]["node_id"]

    # This is NOT the node_id, but is a numeric string (the databaseId field).
    review_database_id = payload["comment"]["pull_request_review_id"]

    with dynamodb_lock(pull_request_id):
        if action in ("created", "edited"):
            pull_request, comment = graphql_client.get_pull_request_and_comment(
                pull_request_id, comment_id)
            if not isinstance(comment, PullRequestReviewComment):
                raise Exception(
                    f"Unexpected comment type {type(PullRequestReviewComment)} for pull request review"
                )
            review: Optional[Review] = Review.from_comment(comment)
        elif action == "deleted":
            pull_request = graphql_client.get_pull_request(pull_request_id)
            review = graphql_client.get_review_for_database_id(
                pull_request_id, review_database_id)
            if review is None:
                # If we deleted the last comment from a review, Github might have deleted the review.
                # If so, we should delete the Asana comment.
                logger.info(
                    "No review found in Github. Deleting the Asana comment.")
                github_controller.delete_comment(comment_id)
        else:
            raise ValueError(f"Unexpected action: {action}")

        if review is not None:
            github_controller.upsert_review(pull_request, review)

        return HttpResponse("200")
示例#4
0
文件: controller.py 项目: Asana/SGTM
def upsert_github_review_to_task(review: Review, task_id: str):
    github_review_id = review.id()
    asana_comment_id = dynamodb_client.get_asana_id_from_github_node_id(
        github_review_id)
    if asana_comment_id is None:
        logger.info(f"Adding review {github_review_id} to task {task_id}")
        asana_comment_id = asana_client.add_comment(
            task_id, asana_helpers.asana_comment_from_github_review(review))
        dynamodb_client.insert_github_node_to_asana_id_mapping(
            github_review_id, asana_comment_id)
    else:
        logger.info(
            f"Review {github_review_id} already synced to task {task_id}. Updating."
        )
        asana_client.update_comment(
            asana_comment_id,
            asana_helpers.asana_comment_from_github_review(review))

    dynamodb_client.bulk_insert_github_node_to_asana_id_mapping([
        (c.id(), asana_comment_id) for c in review.comments()
    ])
示例#5
0
文件: controller.py 项目: Asana/SGTM
def upsert_review(pull_request: PullRequest, review: Review):
    pull_request_id = pull_request.id()
    task_id = dynamodb_client.get_asana_id_from_github_node_id(pull_request_id)
    if task_id is None:
        logger.info(
            f"Task not found for pull request {pull_request_id}. Running a full sync!"
        )
        # TODO: Full sync
    else:
        logger.info(
            f"Found task id {task_id} for pull_request {pull_request_id}. Adding review now."
        )
        asana_controller.upsert_github_review_to_task(review, task_id)
        if review.is_approval_or_changes_requested():
            assign_pull_request_to_author(pull_request)
        asana_controller.update_task(pull_request, task_id)
示例#6
0
def asana_comment_from_github_review(review: Review) -> str:
    """
    Extracts the GitHub author and comments from a GitHub Review, and transforms them into
    a suitable html comment string for Asana. This will involve looking up the GitHub author in
    DynamoDb to determine the Asana domain user id of the review author and any @mentioned GitHub
    users.
    """
    user_display_name = _asana_display_name_for_github_user(review.author())

    if review.is_just_comments():
        # When a user replies to an inline comment,
        # or writes inline comments without a review,
        # github still creates a Review object,
        # even though nothing in github looks like a review
        # If that's the case, there is no meaningful review state ("commented" isn't helpful)
        # and the link to it will either not point anywhere, or be less useful than the individual links on each comment.
        review_action = _wrap_in_tag("strong")("left inline comments:\n")
    else:
        review_action = _wrap_in_tag("A", attrs={"href": review.url()})(
            _review_action_to_text_map.get(review.state(), "commented"))

    review_body = _format_github_text_for_asana(review.body())
    if review_body:
        header = (_wrap_in_tag("strong")
                  (f"{user_display_name} {review_action} :\n") + review_body)
    else:
        header = _wrap_in_tag("strong")(f"{user_display_name} {review_action}")

    # For each comment, prefix its text with a bracketed number that is a link to the Github comment.
    inline_comments = [
        _wrap_in_tag("li")
        (_wrap_in_tag("A", attrs={"href": comment.url()})(f"[{i}] ") +
         _format_github_text_for_asana(comment.body()))
        for i, comment in enumerate(review.comments(), start=1)
    ]
    if inline_comments:
        comments_html = _wrap_in_tag("ul")("".join(inline_comments))
        if not review.is_just_comments():
            # If this was an inline reply, we already added "and left inline comments" above.
            comments_html = (
                _wrap_in_tag("strong")("\n\nand left inline comments:\n") +
                comments_html)
    else:
        comments_html = ""

    return _wrap_in_tag("body")(header + comments_html)
示例#7
0
文件: test_review.py 项目: Asana/SGTM
 def test_with_status_of_approved__is_just_comments_is_false(self):
     raw_review = {"state": "APPROVED", "body": ""}
     review = Review(raw_review)
     self.assertEqual(review.is_just_comments(), False)
示例#8
0
文件: test_review.py 项目: Asana/SGTM
 def test_with_status_of_commented_and_populated_body__is_just_comments_is_false(
     self, ):
     raw_review = {"state": "COMMENTED", "body": "Here's a body!"}
     review = Review(raw_review)
     self.assertEqual(review.is_just_comments(), False)
示例#9
0
文件: test_review.py 项目: Asana/SGTM
 def test_with_status_of_commented_and_empty_body__is_just_comments_is_true(
         self):
     raw_review = {"state": "COMMENTED", "body": ""}
     review = Review(raw_review)
     self.assertEqual(review.is_just_comments(), True)
示例#10
0
文件: test_review.py 项目: Asana/SGTM
 def test_with_status_of_changes_requested__is_just_comments_is_false(self):
     raw_review = {"state": "CHANGES_REQUESTED", "body": ""}
     review = Review(raw_review)
     self.assertEqual(review.is_just_comments(), False)
示例#11
0
 def build(self) -> Review:
     return Review(self.raw_review)