From d606672801582d123f5304386219b5bc9bbafc51 Mon Sep 17 00:00:00 2001 From: Mike Davies Date: Wed, 30 Apr 2025 14:09:40 -0700 Subject: [PATCH 1/4] Add ignore_repositories config for PR filtering What Changed? * Added support to ignore PRs/MRs from specific repositories in GitHub, Bitbucket, and GitLab webhook logic * Updated configuration.toml to include ignore_repositories option * Added unit tests for ignore_repositories across all supported providers --- pr_agent/servers/bitbucket_app.py | 16 +++++ pr_agent/servers/github_app.py | 8 +++ pr_agent/servers/gitlab_webhook.py | 8 +++ pr_agent/settings/configuration.toml | 1 + tests/unittest/test_ignore_repositories.py | 79 ++++++++++++++++++++++ 5 files changed, 112 insertions(+) create mode 100644 tests/unittest/test_ignore_repositories.py diff --git a/pr_agent/servers/bitbucket_app.py b/pr_agent/servers/bitbucket_app.py index a641adeb..56921328 100644 --- a/pr_agent/servers/bitbucket_app.py +++ b/pr_agent/servers/bitbucket_app.py @@ -127,11 +127,22 @@ def should_process_pr_logic(data) -> bool: source_branch = pr_data.get("source", {}).get("branch", {}).get("name", "") target_branch = pr_data.get("destination", {}).get("branch", {}).get("name", "") sender = _get_username(data) + repo_full_name = pr_data.get("destination", {}).get("repository", {}).get("full_name", "") + + print(f"DEBUG: repo_full_name={repo_full_name}, ignore_repos={get_settings().get('CONFIG.IGNORE_REPOSITORIES', [])}") + # logic to ignore PRs from specific repositories + ignore_repos = get_settings().get("CONFIG.IGNORE_REPOSITORIES", []) + if ignore_repos and repo_full_name: + if repo_full_name in ignore_repos: + print(f"DEBUG: Ignoring due to repo match: {repo_full_name}") + get_logger().info(f"Ignoring PR from repository '{repo_full_name}' due to 'config.ignore_repositories' setting") + return False # logic to ignore PRs from specific users ignore_pr_users = get_settings().get("CONFIG.IGNORE_PR_AUTHORS", []) if ignore_pr_users and sender: if sender in ignore_pr_users: + print(f"DEBUG: Ignoring due to user match: {sender}") get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' setting") return False @@ -141,6 +152,7 @@ def should_process_pr_logic(data) -> bool: if not isinstance(ignore_pr_title_re, list): ignore_pr_title_re = [ignore_pr_title_re] if ignore_pr_title_re and any(re.search(regex, title) for regex in ignore_pr_title_re): + print(f"DEBUG: Ignoring due to title match: {title}") get_logger().info(f"Ignoring PR with title '{title}' due to config.ignore_pr_title setting") return False @@ -148,15 +160,19 @@ def should_process_pr_logic(data) -> bool: ignore_pr_target_branches = get_settings().get("CONFIG.IGNORE_PR_TARGET_BRANCHES", []) if (ignore_pr_source_branches or ignore_pr_target_branches): if any(re.search(regex, source_branch) for regex in ignore_pr_source_branches): + print(f"DEBUG: Ignoring due to source branch match: {source_branch}") get_logger().info( f"Ignoring PR with source branch '{source_branch}' due to config.ignore_pr_source_branches settings") return False if any(re.search(regex, target_branch) for regex in ignore_pr_target_branches): + print(f"DEBUG: Ignoring due to target branch match: {target_branch}") get_logger().info( f"Ignoring PR with target branch '{target_branch}' due to config.ignore_pr_target_branches settings") return False except Exception as e: + print(f"DEBUG: Exception in should_process_pr_logic: {e}") get_logger().error(f"Failed 'should_process_pr_logic': {e}") + print("DEBUG: Returning True from should_process_pr_logic") return True diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index 4576bd9d..6354b2b9 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -258,6 +258,14 @@ def should_process_pr_logic(body) -> bool: source_branch = pull_request.get("head", {}).get("ref", "") target_branch = pull_request.get("base", {}).get("ref", "") sender = body.get("sender", {}).get("login") + repo_full_name = body.get("repository", {}).get("full_name", "") + + # logic to ignore PRs from specific repositories + ignore_repos = get_settings().get("CONFIG.IGNORE_REPOSITORIES", []) + if ignore_repos and repo_full_name: + if repo_full_name in ignore_repos: + get_logger().info(f"Ignoring PR from repository '{repo_full_name}' due to 'config.ignore_repositories' setting") + return False # logic to ignore PRs from specific users ignore_pr_users = get_settings().get("CONFIG.IGNORE_PR_AUTHORS", []) diff --git a/pr_agent/servers/gitlab_webhook.py b/pr_agent/servers/gitlab_webhook.py index 0aef933d..7d178702 100644 --- a/pr_agent/servers/gitlab_webhook.py +++ b/pr_agent/servers/gitlab_webhook.py @@ -103,6 +103,14 @@ def should_process_pr_logic(data) -> bool: return False title = data['object_attributes'].get('title') sender = data.get("user", {}).get("username", "") + repo_full_name = data.get('project', {}).get('path_with_namespace', "") + + # logic to ignore PRs from specific repositories + ignore_repos = get_settings().get("CONFIG.IGNORE_REPOSITORIES", []) + if ignore_repos and repo_full_name: + if repo_full_name in ignore_repos: + get_logger().info(f"Ignoring MR from repository '{repo_full_name}' due to 'config.ignore_repositories' setting") + return False # logic to ignore PRs from specific users ignore_pr_users = get_settings().get("CONFIG.IGNORE_PR_AUTHORS", []) diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index e63b7ea8..cd7ebd16 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -55,6 +55,7 @@ ignore_pr_target_branches = [] # a list of regular expressions of target branche ignore_pr_source_branches = [] # a list of regular expressions of source branches to ignore from PR agent when an PR is created ignore_pr_labels = [] # labels to ignore from PR agent when an PR is created ignore_pr_authors = [] # authors to ignore from PR agent when an PR is created +ignore_repositories = [] # list of repository full names (e.g. "org/repo") to ignore from PR agent processing # is_auto_command = false # will be auto-set to true if the command is triggered by an automation enable_ai_metadata = false # will enable adding ai metadata diff --git a/tests/unittest/test_ignore_repositories.py b/tests/unittest/test_ignore_repositories.py new file mode 100644 index 00000000..b8b02009 --- /dev/null +++ b/tests/unittest/test_ignore_repositories.py @@ -0,0 +1,79 @@ +import pytest +from pr_agent.servers.github_app import should_process_pr_logic as github_should_process_pr_logic +from pr_agent.servers.bitbucket_app import should_process_pr_logic as bitbucket_should_process_pr_logic +from pr_agent.servers.gitlab_webhook import should_process_pr_logic as gitlab_should_process_pr_logic +from pr_agent.config_loader import get_settings + +def make_bitbucket_payload(full_name): + return { + "data": { + "pullrequest": { + "title": "Test PR", + "source": {"branch": {"name": "feature/test"}}, + "destination": { + "branch": {"name": "main"}, + "repository": {"full_name": full_name} + } + }, + "actor": {"username": "user", "type": "user"} + } + } + +def make_github_body(full_name): + return { + "pull_request": {}, + "repository": {"full_name": full_name}, + "sender": {"login": "user"} + } + +def make_gitlab_body(full_name): + return { + "object_attributes": {"title": "Test MR"}, + "project": {"path_with_namespace": full_name} + } + +PROVIDERS = [ + ("github", github_should_process_pr_logic, make_github_body), + ("bitbucket", bitbucket_should_process_pr_logic, make_bitbucket_payload), + ("gitlab", gitlab_should_process_pr_logic, make_gitlab_body), +] + +class TestIgnoreRepositories: + def setup_method(self): + get_settings().set("CONFIG.IGNORE_REPOSITORIES", []) + + @pytest.mark.parametrize("provider_name, provider_func, body_func", PROVIDERS) + def test_should_ignore_matching_repository(self, provider_name, provider_func, body_func): + get_settings().set("CONFIG.IGNORE_REPOSITORIES", ["org/repo-to-ignore"]) + body = { + "pull_request": {}, + "repository": {"full_name": "org/repo-to-ignore"}, + "sender": {"login": "user"} + } + result = provider_func(body_func(body["repository"]["full_name"])) + print(f"DEBUG: Provider={provider_name}, test_should_ignore_matching_repository, result={result}") + assert result is False, f"{provider_name}: PR from ignored repository should be ignored (return False)" + + @pytest.mark.parametrize("provider_name, provider_func, body_func", PROVIDERS) + def test_should_not_ignore_non_matching_repository(self, provider_name, provider_func, body_func): + get_settings().set("CONFIG.IGNORE_REPOSITORIES", ["org/repo-to-ignore"]) + body = { + "pull_request": {}, + "repository": {"full_name": "org/other-repo"}, + "sender": {"login": "user"} + } + result = provider_func(body_func(body["repository"]["full_name"])) + print(f"DEBUG: Provider={provider_name}, test_should_not_ignore_non_matching_repository, result={result}") + assert result is True, f"{provider_name}: PR from non-ignored repository should not be ignored (return True)" + + @pytest.mark.parametrize("provider_name, provider_func, body_func", PROVIDERS) + def test_should_not_ignore_when_config_empty(self, provider_name, provider_func, body_func): + get_settings().set("CONFIG.IGNORE_REPOSITORIES", []) + body = { + "pull_request": {}, + "repository": {"full_name": "org/repo-to-ignore"}, + "sender": {"login": "user"} + } + result = provider_func(body_func(body["repository"]["full_name"])) + print(f"DEBUG: Provider={provider_name}, test_should_not_ignore_when_config_empty, result={result}") + assert result is True, f"{provider_name}: PR should not be ignored if ignore_repositories config is empty" \ No newline at end of file From 42557feb9711d106f664df89e4d6993407c225c3 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Fri, 16 May 2025 17:20:54 +0300 Subject: [PATCH 2/4] Enhance repository filtering with regex pattern matching for ignore_repositories --- .../usage-guide/additional_configurations.md | 17 +++++++++++++++-- pr_agent/servers/bitbucket_app.py | 11 ++--------- pr_agent/servers/github_app.py | 2 +- pr_agent/servers/gitlab_webhook.py | 2 +- pr_agent/settings/configuration.toml | 2 +- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/docs/usage-guide/additional_configurations.md b/docs/docs/usage-guide/additional_configurations.md index c345e0c6..eb46bbaa 100644 --- a/docs/docs/usage-guide/additional_configurations.md +++ b/docs/docs/usage-guide/additional_configurations.md @@ -164,6 +164,7 @@ Qodo Merge allows you to automatically ignore certain PRs based on various crite - PRs with specific titles (using regex matching) - PRs between specific branches (using regex matching) +- PRs from specific repositories (using regex matching) - PRs not from specific folders - PRs containing specific labels - PRs opened by specific users @@ -172,7 +173,7 @@ Qodo Merge allows you to automatically ignore certain PRs based on various crite To ignore PRs with a specific title such as "[Bump]: ...", you can add the following to your `configuration.toml` file: -``` +```toml [config] ignore_pr_title = ["\\[Bump\\]"] ``` @@ -183,7 +184,7 @@ Where the `ignore_pr_title` is a list of regex patterns to match the PR title yo To ignore PRs from specific source or target branches, you can add the following to your `configuration.toml` file: -``` +```toml [config] ignore_pr_source_branches = ['develop', 'main', 'master', 'stage'] ignore_pr_target_branches = ["qa"] @@ -192,6 +193,18 @@ ignore_pr_target_branches = ["qa"] Where the `ignore_pr_source_branches` and `ignore_pr_target_branches` are lists of regex patterns to match the source and target branches you want to ignore. They are not mutually exclusive, you can use them together or separately. +### Ignoring PRs from specific repositories + +To ignore PRs from specific repositories, you can add the following to your `configuration.toml` file: + +```toml +[config] +ignore_repositories = ["my-org/my-repo1", "my-org/my-repo2"] +``` + +Where the `ignore_repositories` is a list of regex patterns to match the repositories you want to ignore. This is useful when you have multiple repositories and want to exclude certain ones from analysis. + + ### Ignoring PRs not from specific folders To allow only specific folders (often needed in large monorepos), set: diff --git a/pr_agent/servers/bitbucket_app.py b/pr_agent/servers/bitbucket_app.py index 56921328..c55a335e 100644 --- a/pr_agent/servers/bitbucket_app.py +++ b/pr_agent/servers/bitbucket_app.py @@ -129,12 +129,10 @@ def should_process_pr_logic(data) -> bool: sender = _get_username(data) repo_full_name = pr_data.get("destination", {}).get("repository", {}).get("full_name", "") - print(f"DEBUG: repo_full_name={repo_full_name}, ignore_repos={get_settings().get('CONFIG.IGNORE_REPOSITORIES', [])}") # logic to ignore PRs from specific repositories ignore_repos = get_settings().get("CONFIG.IGNORE_REPOSITORIES", []) - if ignore_repos and repo_full_name: - if repo_full_name in ignore_repos: - print(f"DEBUG: Ignoring due to repo match: {repo_full_name}") + if repo_full_name and ignore_repos: + if any(re.search(regex, repo_full_name) for regex in ignore_repos): get_logger().info(f"Ignoring PR from repository '{repo_full_name}' due to 'config.ignore_repositories' setting") return False @@ -142,7 +140,6 @@ def should_process_pr_logic(data) -> bool: ignore_pr_users = get_settings().get("CONFIG.IGNORE_PR_AUTHORS", []) if ignore_pr_users and sender: if sender in ignore_pr_users: - print(f"DEBUG: Ignoring due to user match: {sender}") get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' setting") return False @@ -152,7 +149,6 @@ def should_process_pr_logic(data) -> bool: if not isinstance(ignore_pr_title_re, list): ignore_pr_title_re = [ignore_pr_title_re] if ignore_pr_title_re and any(re.search(regex, title) for regex in ignore_pr_title_re): - print(f"DEBUG: Ignoring due to title match: {title}") get_logger().info(f"Ignoring PR with title '{title}' due to config.ignore_pr_title setting") return False @@ -160,17 +156,14 @@ def should_process_pr_logic(data) -> bool: ignore_pr_target_branches = get_settings().get("CONFIG.IGNORE_PR_TARGET_BRANCHES", []) if (ignore_pr_source_branches or ignore_pr_target_branches): if any(re.search(regex, source_branch) for regex in ignore_pr_source_branches): - print(f"DEBUG: Ignoring due to source branch match: {source_branch}") get_logger().info( f"Ignoring PR with source branch '{source_branch}' due to config.ignore_pr_source_branches settings") return False if any(re.search(regex, target_branch) for regex in ignore_pr_target_branches): - print(f"DEBUG: Ignoring due to target branch match: {target_branch}") get_logger().info( f"Ignoring PR with target branch '{target_branch}' due to config.ignore_pr_target_branches settings") return False except Exception as e: - print(f"DEBUG: Exception in should_process_pr_logic: {e}") get_logger().error(f"Failed 'should_process_pr_logic': {e}") print("DEBUG: Returning True from should_process_pr_logic") return True diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index 6354b2b9..36db3c69 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -263,7 +263,7 @@ def should_process_pr_logic(body) -> bool: # logic to ignore PRs from specific repositories ignore_repos = get_settings().get("CONFIG.IGNORE_REPOSITORIES", []) if ignore_repos and repo_full_name: - if repo_full_name in ignore_repos: + if any(re.search(regex, repo_full_name) for regex in ignore_repos): get_logger().info(f"Ignoring PR from repository '{repo_full_name}' due to 'config.ignore_repositories' setting") return False diff --git a/pr_agent/servers/gitlab_webhook.py b/pr_agent/servers/gitlab_webhook.py index 7d178702..9036f15d 100644 --- a/pr_agent/servers/gitlab_webhook.py +++ b/pr_agent/servers/gitlab_webhook.py @@ -108,7 +108,7 @@ def should_process_pr_logic(data) -> bool: # logic to ignore PRs from specific repositories ignore_repos = get_settings().get("CONFIG.IGNORE_REPOSITORIES", []) if ignore_repos and repo_full_name: - if repo_full_name in ignore_repos: + if any(re.search(regex, repo_full_name) for regex in ignore_repos): get_logger().info(f"Ignoring MR from repository '{repo_full_name}' due to 'config.ignore_repositories' setting") return False diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index cd7ebd16..ce097c95 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -55,7 +55,7 @@ ignore_pr_target_branches = [] # a list of regular expressions of target branche ignore_pr_source_branches = [] # a list of regular expressions of source branches to ignore from PR agent when an PR is created ignore_pr_labels = [] # labels to ignore from PR agent when an PR is created ignore_pr_authors = [] # authors to ignore from PR agent when an PR is created -ignore_repositories = [] # list of repository full names (e.g. "org/repo") to ignore from PR agent processing +ignore_repositories = [] # a list of regular expressions of repository full names (e.g. "org/repo") to ignore from PR agent processing # is_auto_command = false # will be auto-set to true if the command is triggered by an automation enable_ai_metadata = false # will enable adding ai metadata From f47da75e6fe553b0ca429eeb79934a004fbeee17 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Fri, 16 May 2025 17:23:27 +0300 Subject: [PATCH 3/4] Remove debug print statement from should_process_pr_logic function --- pr_agent/servers/bitbucket_app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pr_agent/servers/bitbucket_app.py b/pr_agent/servers/bitbucket_app.py index c55a335e..0d75d143 100644 --- a/pr_agent/servers/bitbucket_app.py +++ b/pr_agent/servers/bitbucket_app.py @@ -165,7 +165,6 @@ def should_process_pr_logic(data) -> bool: return False except Exception as e: get_logger().error(f"Failed 'should_process_pr_logic': {e}") - print("DEBUG: Returning True from should_process_pr_logic") return True From 52ce74a31a5f83960e7f67ab019a473d9951d9f1 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Fri, 16 May 2025 17:25:10 +0300 Subject: [PATCH 4/4] Remove debug print statements from repository filtering tests --- tests/unittest/test_ignore_repositories.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unittest/test_ignore_repositories.py b/tests/unittest/test_ignore_repositories.py index b8b02009..e05447d5 100644 --- a/tests/unittest/test_ignore_repositories.py +++ b/tests/unittest/test_ignore_repositories.py @@ -51,7 +51,7 @@ class TestIgnoreRepositories: "sender": {"login": "user"} } result = provider_func(body_func(body["repository"]["full_name"])) - print(f"DEBUG: Provider={provider_name}, test_should_ignore_matching_repository, result={result}") + # print(f"DEBUG: Provider={provider_name}, test_should_ignore_matching_repository, result={result}") assert result is False, f"{provider_name}: PR from ignored repository should be ignored (return False)" @pytest.mark.parametrize("provider_name, provider_func, body_func", PROVIDERS) @@ -63,7 +63,7 @@ class TestIgnoreRepositories: "sender": {"login": "user"} } result = provider_func(body_func(body["repository"]["full_name"])) - print(f"DEBUG: Provider={provider_name}, test_should_not_ignore_non_matching_repository, result={result}") + # print(f"DEBUG: Provider={provider_name}, test_should_not_ignore_non_matching_repository, result={result}") assert result is True, f"{provider_name}: PR from non-ignored repository should not be ignored (return True)" @pytest.mark.parametrize("provider_name, provider_func, body_func", PROVIDERS) @@ -75,5 +75,5 @@ class TestIgnoreRepositories: "sender": {"login": "user"} } result = provider_func(body_func(body["repository"]["full_name"])) - print(f"DEBUG: Provider={provider_name}, test_should_not_ignore_when_config_empty, result={result}") + # print(f"DEBUG: Provider={provider_name}, test_should_not_ignore_when_config_empty, result={result}") assert result is True, f"{provider_name}: PR should not be ignored if ignore_repositories config is empty" \ No newline at end of file