Example #1
0
def process_queue(queue):
    queue_log = get_queue_logger(queue)
    _, installation_id, owner, repo, queue_base_branch = queue.split("~")

    pull_numbers = _get_pulls(queue)

    queue_log.debug("%d pulls queued",
                    len(pull_numbers),
                    queue=list(pull_numbers))
    if not pull_numbers:
        queue_log.debug("no pull request for this queue")
        return

    pull_number = int(pull_numbers[0])

    try:
        installation = github.get_installation(owner, repo,
                                               int(installation_id))
    except exceptions.MergifyNotInstalled:
        _delete_queue(queue)
        return

    with github.get_client(owner, repo, installation) as client:
        data = client.item(f"pulls/{pull_number}")

        try:
            ctxt = mergify_context.MergifyContext(client, data)
        except exceptions.RateLimited as e:
            log = ctxt.log if ctxt else queue_log
            log.info("rate limited", remaining_seconds=e.countdown)
            return
        except exceptions.MergeableStateUnknown as e:  # pragma: no cover
            e.ctxt.log.warning(
                "pull request with mergeable_state unknown retrying later", )
            _move_pull_at_end(e.ctxt)
            return
        try:
            if ctxt.pull["base"]["ref"] != queue_base_branch:
                ctxt.log.debug(
                    "pull request base branch have changed",
                    old_branch=queue_base_branch,
                    new_branch=ctxt.pull["base"]["ref"],
                )
                _move_pull_to_new_base_branch(ctxt, queue_base_branch)
            elif ctxt.pull["state"] == "closed" or ctxt.is_behind:
                # NOTE(sileht): Pick up this pull request and rebase it again
                # or update its status and remove it from the queue
                ctxt.log.debug(
                    "pull request needs to be updated again or has been closed",
                )
                _handle_first_pull_in_queue(queue, ctxt)
            else:
                # NOTE(sileht): Pull request has not been merged or cancelled
                # yet wait next loop
                ctxt.log.debug("pull request checks are still in progress")
        except Exception:  # pragma: no cover
            ctxt.log.error("Fail to process merge queue", exc_info=True)
            _move_pull_at_end(ctxt)
def test_get_commits_to_cherry_pick_rebase(commits):
    c1 = {"sha": "c1f", "parents": [], "commit": {"message": "foobar"}}
    c2 = {"sha": "c2", "parents": [c1], "commit": {"message": "foobar"}}
    commits.return_value = [c1, c2]

    client = mock.Mock()
    client.auth.get_access_token.return_value = "<token>"
    client.items.side_effect = fake_get_github_pulls_from_sha

    ctxt = mergify_context.MergifyContext(
        client,
        {
            "number": 6,
            "merged": True,
            "state": "closed",
            "html_url": "<html_url>",
            "base": {
                "ref": "ref",
                "repo": {
                    "full_name": "user/ref",
                    "name": "name",
                    "private": False
                },
            },
            "head": {
                "ref": "fork",
                "repo": {
                    "full_name": "fork/other",
                    "name": "other",
                    "private": False
                },
            },
            "user": {
                "login": "******"
            },
            "merged_by": None,
            "merged_at": None,
            "mergeable_state": "clean",
        },
    )

    base_branch = {"sha": "base_branch", "parents": []}
    rebased_c1 = {"sha": "rebased_c1", "parents": [base_branch]}
    rebased_c2 = {"sha": "rebased_c2", "parents": [rebased_c1]}

    assert duplicate_pull._get_commits_to_cherrypick(ctxt, rebased_c2) == [
        rebased_c1,
        rebased_c2,
    ]
Example #3
0
def run_command_async(installation_id,
                      pull_request_raw,
                      sources,
                      comment,
                      user,
                      rerun=False):
    owner = pull_request_raw["base"]["user"]["login"]
    repo = pull_request_raw["base"]["repo"]["name"]

    try:
        installation = github.get_installation(owner, repo, installation_id)
    except exceptions.MergifyNotInstalled:
        return

    with github.get_client(owner, repo, installation) as client:
        pull = mergify_context.MergifyContext(client, pull_request_raw)
        return run_command(pull, sources, comment, user, rerun)
Example #4
0
def PullRequestUrl(v):
    _, owner, repo, _, pull_number = urlsplit(v).path.split("/")
    pull_number = int(pull_number)

    try:
        installation = github.get_installation(owner, repo)
    except exceptions.MergifyNotInstalled:
        raise PullRequestUrlInvalid(
            message="Mergify not installed on repository '%s'" % owner)

    with github.get_client(owner, repo, installation) as client:
        try:
            data = client.item(f"pulls/{pull_number}")
        except httpx.HTTPNotFound:
            raise PullRequestUrlInvalid(
                message=("Pull request '%s' not found" % v))

        return mergify_context.MergifyContext(client, data)
Example #5
0
def test_pull_behind(commits_tree_generator):
    expected, commits = commits_tree_generator
    client = mock.Mock()
    client.items.return_value = commits  # /pulls/X/commits
    client.item.return_value = {"commit": {"sha": "base"}}  # /branch/#foo

    ctxt = mergify_context.MergifyContext(
        client,
        pull={
            "number": 1,
            "mergeable_state": "clean",
            "state": "open",
            "merged": False,
            "merged_at": None,
            "merged_by": None,
            "base": {
                "ref": "#foo"
            },
        },
    )

    assert expected == ctxt.is_behind
Example #6
0
def report(url):
    redis = utils.get_redis_for_cache()
    path = url.replace("https://github.com/", "")
    try:
        owner, repo, _, pull_number = path.split("/")
    except ValueError:
        print(f"Wrong URL: {url}")
        return
    slug = owner + "/" + repo

    try:
        installation = github.get_installation(owner, repo)
    except exceptions.MergifyNotInstalled:
        print("* Mergify is not installed there")
        return

    client = github.get_client(owner, repo, installation)

    print("* INSTALLATION ID: %s" % client.installation["id"])

    cached_sub = sub_utils.get_subscription(redis, client.installation["id"])
    db_sub = sub_utils._retrieve_subscription_from_db(
        client.installation["id"])
    print("* SUBSCRIBED (cache/db): %s / %s" %
          (cached_sub["subscription_active"], db_sub["subscription_active"]))
    report_sub(client.installation["id"], slug, cached_sub, "ENGINE-CACHE")
    report_sub(client.installation["id"], slug, db_sub, "DASHBOARD")

    pull_raw = client.item(f"pulls/{pull_number}")
    ctxt = mergify_context.MergifyContext(client, pull_raw)

    print("* REPOSITORY IS %s" %
          "PRIVATE" if ctxt.pull["base"]["repo"]["private"] else "PUBLIC")

    print("* CONFIGURATION:")
    try:
        mergify_config_content = rules.get_mergify_config_content(ctxt)
    except rules.NoRules:  # pragma: no cover
        print(".mergify.yml is missing")
        pull_request_rules = None
    else:
        print(mergify_config_content.decode())
        try:
            mergify_config = rules.UserConfigurationSchema(
                mergify_config_content)
        except rules.InvalidRules as e:  # pragma: no cover
            print("configuration is invalid %s" % str(e))
        else:
            pull_request_rules_raw = mergify_config[
                "pull_request_rules"].as_dict()
            pull_request_rules_raw["rules"].extend(
                engine.MERGIFY_RULE["rules"])
            pull_request_rules = rules.PullRequestRules(
                **pull_request_rules_raw)

    print("* PULL REQUEST:")
    pprint.pprint(ctxt.to_dict(), width=160)

    print("is_behind: %s" % ctxt.is_behind)

    print("mergeable_state: %s" % ctxt.pull["mergeable_state"])

    print("* MERGIFY LAST CHECKS:")
    checks = list(check_api.get_checks(ctxt, mergify_only=True))
    for c in checks:
        print("[%s]: %s | %s" %
              (c["name"], c["conclusion"], c["output"].get("title")))
        print("> " + "\n> ".join(c["output"].get("summary").split("\n")))

    if pull_request_rules is not None:
        print("* MERGIFY LIVE MATCHES:")
        match = pull_request_rules.get_pull_request_rule(ctxt)
        summary_title, summary = actions_runner.gen_summary(
            ctxt, [{
                "event_type": "refresh",
                "data": {}
            }], match)
        print("> %s" % summary_title)
        print(summary)

    return ctxt
Example #7
0
def test_get_pull_request_rule():

    client = mock.Mock()

    get_reviews = [{
        "user": {
            "login": "******",
            "type": "User"
        },
        "state": "APPROVED",
        "author_association": "MEMBER",
    }]
    get_files = [{"filename": "README.rst"}, {"filename": "setup.py"}]
    get_team_members = [{"login": "******"}, {"login": "******"}]

    get_checks = []
    get_statuses = [{
        "context": "continuous-integration/fake-ci",
        "state": "success"
    }]
    client.item.return_value = {"permission": "write"}  # get review user perm
    client.items.side_effect = [
        get_reviews,
        get_files,
        get_checks,
        get_statuses,
        get_team_members,
    ]

    ctxt = mergify_context.MergifyContext(
        client,
        {
            "number": 1,
            "html_url": "<html_url>",
            "state": "closed",
            "merged_by": None,
            "merged_at": None,
            "merged": False,
            "milestone": None,
            "mergeable_state": "unstable",
            "assignees": [],
            "labels": [],
            "author": "jd",
            "base": {
                "ref": "master",
                "repo": {
                    "name": "name",
                    "private": False
                },
            },
            "head": {
                "ref": "myfeature",
                "sha": "<sha>"
            },
            "locked": False,
            "requested_reviewers": [],
            "requested_teams": [],
            "title": "My awesome job",
            "body": "I rock",
            "user": {
                "login": "******"
            },
        },
    )

    # Don't catch data in these tests
    ctxt.to_dict = ctxt._get_consolidated_data

    # Empty conditions
    pull_request_rules = rules.PullRequestRules([{
        "name": "default",
        "conditions": [],
        "actions": {}
    }])

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["default"]
    assert [r["name"] for r, _ in match.matching_rules] == ["default"]
    assert [(r, []) for r in match.rules] == match.matching_rules
    for rule in match.rules:
        assert rule["actions"] == {}

    pull_request_rules = rules.PullRequestRules([{
        "name": "hello",
        "conditions": ["base:master"],
        "actions": {}
    }])

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["hello"]
    assert [r["name"] for r, _ in match.matching_rules] == ["hello"]
    assert [(r, []) for r in match.rules] == match.matching_rules
    for rule in match.rules:
        assert rule["actions"] == {}

    pull_request_rules = rules.PullRequestRules([
        {
            "name": "hello",
            "conditions": ["base:master"],
            "actions": {}
        },
        {
            "name": "backport",
            "conditions": ["base:master"],
            "actions": {}
        },
    ])

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["hello", "backport"]
    assert [r["name"]
            for r, _ in match.matching_rules] == ["hello", "backport"]
    assert [(r, []) for r in match.rules] == match.matching_rules
    for rule in match.rules:
        assert rule["actions"] == {}

    pull_request_rules = rules.PullRequestRules([
        {
            "name": "hello",
            "conditions": ["#files=3"],
            "actions": {}
        },
        {
            "name": "backport",
            "conditions": ["base:master"],
            "actions": {}
        },
    ])

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["hello", "backport"]
    assert [r["name"] for r, _ in match.matching_rules] == ["backport"]
    for rule in match.rules:
        assert rule["actions"] == {}

    pull_request_rules = rules.PullRequestRules([
        {
            "name": "hello",
            "conditions": ["#files=2"],
            "actions": {}
        },
        {
            "name": "backport",
            "conditions": ["base:master"],
            "actions": {}
        },
    ])

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["hello", "backport"]
    assert [r["name"]
            for r, _ in match.matching_rules] == ["hello", "backport"]
    assert [(r, []) for r in match.rules] == match.matching_rules
    for rule in match.rules:
        assert rule["actions"] == {}

    # No match
    pull_request_rules = rules.PullRequestRules([{
        "name":
        "merge",
        "conditions": [
            "base=xyz",
            "status-success=continuous-integration/fake-ci",
            "#approved-reviews-by>=1",
        ],
        "actions": {},
    }])

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["merge"]
    assert [r["name"] for r, _ in match.matching_rules] == []

    pull_request_rules = rules.PullRequestRules([{
        "name":
        "merge",
        "conditions": [
            "base=master",
            "status-success=continuous-integration/fake-ci",
            "#approved-reviews-by>=1",
        ],
        "actions": {},
    }])

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["merge"]
    assert [r["name"] for r, _ in match.matching_rules] == ["merge"]
    assert [(r, []) for r in match.rules] == match.matching_rules
    for rule in match.rules:
        assert rule["actions"] == {}

    pull_request_rules = rules.PullRequestRules([
        {
            "name":
            "merge",
            "conditions": [
                "base=master",
                "status-success=continuous-integration/fake-ci",
                "#approved-reviews-by>=2",
            ],
            "actions": {},
        },
        {
            "name":
            "fast merge",
            "conditions": [
                "base=master",
                "label=fast-track",
                "status-success=continuous-integration/fake-ci",
                "#approved-reviews-by>=1",
            ],
            "actions": {},
        },
        {
            "name":
            "fast merge with alternate ci",
            "conditions": [
                "base=master",
                "label=fast-track",
                "status-success=continuous-integration/fake-ci-bis",
                "#approved-reviews-by>=1",
            ],
            "actions": {},
        },
        {
            "name":
            "fast merge from a bot",
            "conditions": [
                "base=master",
                "author=mybot",
                "status-success=continuous-integration/fake-ci",
            ],
            "actions": {},
        },
    ])
    match = pull_request_rules.get_pull_request_rule(ctxt)

    assert [r["name"] for r in match.rules] == [
        "merge",
        "fast merge",
        "fast merge with alternate ci",
        "fast merge from a bot",
    ]
    assert [r["name"] for r, _ in match.matching_rules] == [
        "merge",
        "fast merge",
        "fast merge with alternate ci",
    ]
    for rule in match.rules:
        assert rule["actions"] == {}

    assert match.matching_rules[0][0]["name"] == "merge"
    assert len(match.matching_rules[0][1]) == 1
    assert str(match.matching_rules[0][1][0]) == "#approved-reviews-by>=2"

    assert match.matching_rules[1][0]["name"] == "fast merge"
    assert len(match.matching_rules[1][1]) == 1
    assert str(match.matching_rules[1][1][0]) == "label=fast-track"

    assert match.matching_rules[2][0]["name"] == "fast merge with alternate ci"
    assert len(match.matching_rules[2][1]) == 2
    assert str(match.matching_rules[2][1][0]) == "label=fast-track"
    assert (str(match.matching_rules[2][1][1]) ==
            "status-success=continuous-integration/fake-ci-bis")

    # Team conditions with one review missing
    pull_request_rules = rules.PullRequestRules([{
        "name":
        "default",
        "conditions": [
            "approved-reviews-by=@orgs/my-reviewers",
            "#approved-reviews-by>=2",
        ],
        "actions": {},
    }])

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["default"]
    assert [r["name"] for r, _ in match.matching_rules] == ["default"]

    assert match.matching_rules[0][0]["name"] == "default"
    assert len(match.matching_rules[0][1]) == 1
    assert str(match.matching_rules[0][1][0]) == "#approved-reviews-by>=2"

    get_reviews.append({
        "user": {
            "login": "******",
            "type": "User"
        },
        "state": "APPROVED",
        "author_association": "MEMBER",
    })
    client.items.side_effect = [
        get_reviews,
        get_files,
        get_checks,
        get_statuses,
        get_team_members,
    ]
    # Drop caches
    del ctxt.__dict__["checks"]
    del ctxt.__dict__["reviews"]
    del ctxt.__dict__["files"]
    del ctxt.__dict__["consolidated_reviews"]

    # Team conditions with no review missing
    pull_request_rules = rules.PullRequestRules([{
        "name":
        "default",
        "conditions": [
            "approved-reviews-by=@orgs/my-reviewers",
            "#approved-reviews-by>=2",
        ],
        "actions": {},
    }])

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["default"]
    assert [r["name"] for r, _ in match.matching_rules] == ["default"]

    assert match.matching_rules[0][0]["name"] == "default"
    assert len(match.matching_rules[0][1]) == 0

    # Forbidden labels, when no label set
    pull_request_rules = rules.PullRequestRules([{
        "name":
        "default",
        "conditions": ["-label~=^(status/wip|status/blocked|review/need2)$"],
        "actions": {},
    }])

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["default"]
    assert [r["name"] for r, _ in match.matching_rules] == ["default"]
    assert match.matching_rules[0][0]["name"] == "default"
    assert len(match.matching_rules[0][1]) == 0

    # Forbidden labels, when forbiden label set
    ctxt.pull["labels"] = [{"name": "status/wip"}]

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["default"]
    assert [r["name"] for r, _ in match.matching_rules] == ["default"]
    assert match.matching_rules[0][0]["name"] == "default"
    assert len(match.matching_rules[0][1]) == 1
    assert str(match.matching_rules[0][1][0]) == (
        "-label~=^(status/wip|status/blocked|review/need2)$")

    # Forbidden labels, when other label set
    ctxt.pull["labels"] = [{"name": "allowed"}]

    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["default"]
    assert [r["name"] for r, _ in match.matching_rules] == ["default"]
    assert match.matching_rules[0][0]["name"] == "default"
    assert len(match.matching_rules[0][1]) == 0

    # Test team expander
    pull_request_rules = rules.PullRequestRules([{
        "name":
        "default",
        "conditions": ["author~=^(user1|user2|another-jd)$"],
        "actions": {},
    }])
    match = pull_request_rules.get_pull_request_rule(ctxt)
    assert [r["name"] for r in match.rules] == ["default"]
    assert [r["name"] for r, _ in match.matching_rules] == ["default"]
    assert match.matching_rules[0][0]["name"] == "default"
    assert len(match.matching_rules[0][1]) == 0
Example #8
0
def _run(client, event_type, data):
    raw_pull = get_github_pull_from_event(client, event_type, data)

    if not raw_pull:  # pragma: no cover
        LOG.info(
            "No pull request found in the event %s, ignoring",
            event_type,
            gh_owner=client.owner,
            gh_repo=client.repo,
        )
        return

    ctxt = mergify_context.MergifyContext(client, raw_pull)
    # Override pull_request with the updated one
    data["pull_request"] = ctxt.pull

    ctxt.log.info("Pull request found in the event %s", event_type)

    if ("base" not in ctxt.pull or "repo" not in ctxt.pull["base"]
            or len(list(ctxt.pull["base"]["repo"].keys())) < 70):
        ctxt.log.warning(
            "the pull request payload looks suspicious",
            event_type=event_type,
            data=data,
        )

    if (event_type == "status"
            and ctxt.pull["head"]["sha"] != data["sha"]):  # pragma: no cover
        ctxt.log.info(
            "No need to proceed queue (got status of an old commit)", )
        return

    elif (event_type in ["status", "check_suite", "check_run"]
          and ctxt.pull["merged"]):  # pragma: no cover
        ctxt.log.info(
            "No need to proceed queue (got status of a merged pull request)", )
        return
    elif (event_type in ["check_suite", "check_run"]
          and ctxt.pull["head"]["sha"] !=
          data[event_type]["head_sha"]):  # pragma: no cover
        ctxt.log.info(
            "No need to proceed queue (got %s of an old "
            "commit)",
            event_type,
        )
        return

    if check_configuration_changes(ctxt):
        ctxt.log.info("Configuration changed, ignoring", )
        return

    # BRANCH CONFIGURATION CHECKING
    try:
        mergify_config = rules.get_mergify_config(ctxt)
    except rules.NoRules:  # pragma: no cover
        ctxt.log.info("No need to proceed queue (.mergify.yml is missing)", )
        return
    except rules.InvalidRules as e:  # pragma: no cover
        # Not configured, post status check with the error message
        if event_type == "pull_request" and data["action"] in [
                "opened", "synchronize"
        ]:
            check_api.set_check_run(
                ctxt,
                "Summary",
                "completed",
                "failure",
                output={
                    "title": "The Mergify configuration is invalid",
                    "summary": str(e),
                },
            )
        return

    # Add global and mandatory rules
    mergify_config["pull_request_rules"].rules.extend(
        rules.load_pull_request_rules_schema(MERGIFY_RULE["rules"]))

    subscription = sub_utils.get_subscription(utils.get_redis_for_cache(),
                                              client.installation["id"])

    if ctxt.pull["base"]["repo"][
            "private"] and not subscription["subscription_active"]:
        check_api.set_check_run(
            ctxt,
            "Summary",
            "completed",
            "failure",
            output={
                "title": "Mergify is disabled",
                "summary": subscription["subscription_reason"],
            },
        )
        return

    # CheckRun are attached to head sha, so when user add commits or force push
    # we can't directly get the previous Mergify Summary. So we copy it here, then
    # anything that looks at it in next celery tasks will find it.
    if event_type == "pull_request" and data["action"] == "synchronize":
        copy_summary_from_previous_head_sha(ctxt, data["before"])

    sources = [{"event_type": event_type, "data": data}]

    commands_runner.spawn_pending_commands_tasks(ctxt, sources)

    if event_type == "issue_comment":
        commands_runner.run_command(ctxt, sources, data["comment"]["body"],
                                    data["comment"]["user"])
    else:
        actions_runner.handle(mergify_config["pull_request_rules"], ctxt,
                              sources)