pkg-30922 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pkg_30922/__init__.py +37 -0
- pkg_30922/cli.py +258 -0
- pkg_30922/config/__init__.py +13 -0
- pkg_30922/config/config.py +213 -0
- pkg_30922/services/__init__.py +0 -0
- pkg_30922/services/gh_ci.py +544 -0
- pkg_30922/services/gh_merge.py +101 -0
- pkg_30922/services/gh_pr.py +166 -0
- pkg_30922/services/gh_repo.py +153 -0
- pkg_30922/services/gh_user.py +94 -0
- pkg_30922-1.0.0.dist-info/METADATA +25 -0
- pkg_30922-1.0.0.dist-info/RECORD +15 -0
- pkg_30922-1.0.0.dist-info/WHEEL +4 -0
- pkg_30922-1.0.0.dist-info/entry_points.txt +2 -0
- pkg_30922-1.0.0.dist-info/licenses/LICENSE +18 -0
pkg_30922/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main package entry point.
|
|
3
|
+
|
|
4
|
+
This package provides automated pull request merging functionality for GitHub,
|
|
5
|
+
including repository discovery, PR filtering, merge readiness evaluation, and
|
|
6
|
+
batch merging operations.
|
|
7
|
+
|
|
8
|
+
Submodules:
|
|
9
|
+
- config: Configuration loading and validation
|
|
10
|
+
- services.gh_ci: Merge readiness evaluation with CI and review checks
|
|
11
|
+
- services.gh_merge: PR merge execution
|
|
12
|
+
- services.gh_pr: Open PR retrieval and filtering
|
|
13
|
+
- services.gh_repo: Repository discovery
|
|
14
|
+
- services.gh_user: GitHub authentication and user info
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .config import Config, load_config, merge_config_with_cli
|
|
18
|
+
from .services.gh_ci import get_merge_readiness
|
|
19
|
+
from .services.gh_merge import put_merge_pr
|
|
20
|
+
from .services.gh_pr import get_open_prs
|
|
21
|
+
from .services.gh_repo import get_repos_from_all, get_repos_from_owner
|
|
22
|
+
from .services.gh_user import get_auth, get_token_user_info
|
|
23
|
+
|
|
24
|
+
__version__ = "1.0.0"
|
|
25
|
+
|
|
26
|
+
__all__ = (
|
|
27
|
+
"Config",
|
|
28
|
+
"load_config",
|
|
29
|
+
"merge_config_with_cli",
|
|
30
|
+
"get_auth",
|
|
31
|
+
"get_merge_readiness",
|
|
32
|
+
"put_merge_pr",
|
|
33
|
+
"get_open_prs",
|
|
34
|
+
"get_repos_from_all",
|
|
35
|
+
"get_repos_from_owner",
|
|
36
|
+
"get_token_user_info",
|
|
37
|
+
)
|
pkg_30922/cli.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI tool to automate pull request merging.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to:
|
|
5
|
+
- Get a list of repositories across organizations and users
|
|
6
|
+
- Filter repositories with open pull requests
|
|
7
|
+
- Filter pull requests by label, base branch, title prefix, and owner
|
|
8
|
+
- Evaluate merge readiness based on CI status and code review approvals
|
|
9
|
+
- Execute batch merge operations across multiple repositories
|
|
10
|
+
|
|
11
|
+
Key Features:
|
|
12
|
+
- Concurrent processing using asyncio for efficiency
|
|
13
|
+
- Support for dry-run mode to preview merges without executing
|
|
14
|
+
- Filtering by multiple criteria (labels, branch, prefix, owner)
|
|
15
|
+
- Merge method selection (merge, rebase, squash)
|
|
16
|
+
- Bypass review count option to merge without sufficient approvals
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
import asyncclick as click
|
|
25
|
+
|
|
26
|
+
from pkg_30922 import (
|
|
27
|
+
__version__,
|
|
28
|
+
get_auth,
|
|
29
|
+
get_merge_readiness,
|
|
30
|
+
get_open_prs,
|
|
31
|
+
get_repos_from_all,
|
|
32
|
+
get_repos_from_owner,
|
|
33
|
+
get_token_user_info,
|
|
34
|
+
put_merge_pr,
|
|
35
|
+
)
|
|
36
|
+
from pkg_30922.config import Config, load_config, merge_config_with_cli
|
|
37
|
+
|
|
38
|
+
ALLOWED_REPO_TYPES = ["all", "private", "public"]
|
|
39
|
+
ALLOWED_MERGE_METHODS = ["merge", "rebase", "squash"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CustomError(Exception):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_owner(owner: str, orgs: list[str], login: str) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Validate that the owner is valid for the authenticated token.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
owner: The owner to validate (username or organization name).
|
|
52
|
+
orgs: List of organizations the token has access to.
|
|
53
|
+
login: The authenticated user's login name.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
CustomError: If owner is not in orgs and doesn't match the token owner.
|
|
57
|
+
"""
|
|
58
|
+
if owner:
|
|
59
|
+
if owner not in orgs and owner != login:
|
|
60
|
+
raise CustomError("CLI option '--owner' is invalid (mismatch what the token suggests)")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def find_config_file() -> Optional[str]:
|
|
64
|
+
"""
|
|
65
|
+
Find configuration file for GitHub Actions users.
|
|
66
|
+
|
|
67
|
+
Searches for config file at the default location (./config/default.yaml).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Path to config file if found, None otherwise.
|
|
71
|
+
"""
|
|
72
|
+
config_locations = ["./config/default.yaml"]
|
|
73
|
+
|
|
74
|
+
for location in config_locations:
|
|
75
|
+
if os.path.exists(location):
|
|
76
|
+
return location
|
|
77
|
+
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def check_repo_type(repo_type: str) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Validate the repository type option.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
repo_type: The repository type to validate.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
CustomError: If repo_type is not one of 'all', 'private', or 'public'.
|
|
90
|
+
"""
|
|
91
|
+
if repo_type:
|
|
92
|
+
if not repo_type.strip().lower() in ALLOWED_REPO_TYPES:
|
|
93
|
+
raise CustomError("CLI option '--repo-type' requires all or private or public")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def check_merge_method(merge_method: str) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Validate the merge method option.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
merge_method: The merge method to validate.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
CustomError: If merge_method is not one of 'merge', 'rebase', or 'squash'.
|
|
105
|
+
"""
|
|
106
|
+
if merge_method:
|
|
107
|
+
if not merge_method.strip().lower() in ALLOWED_MERGE_METHODS:
|
|
108
|
+
raise CustomError("CLI option '--merge-method' requires merge, rebase, or squash")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def print_summary(number_of_open_prs: int, number_of_mergeable_prs: int, number_of_merged_prs: int) -> None:
|
|
112
|
+
"""
|
|
113
|
+
Print summary
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
number_of_open_prs: Number of open pull requests
|
|
117
|
+
number_of_mergeable_prs: Number of mergeable pull requests
|
|
118
|
+
number_of_merged_prs: Number of merged pull requests
|
|
119
|
+
"""
|
|
120
|
+
dict_summary = {}
|
|
121
|
+
dict_summary["open-prs"] = number_of_open_prs
|
|
122
|
+
dict_summary["mergeable-prs"] = number_of_mergeable_prs
|
|
123
|
+
dict_summary["merged-prs"] = number_of_merged_prs
|
|
124
|
+
print()
|
|
125
|
+
print(f"🎉 Merge Pull Request Task Info. :: {dict_summary}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@click.command()
|
|
129
|
+
@click.option("--base-branch", type=str, default="main", help="Restrict to a particular base-branch [case-sensitive]")
|
|
130
|
+
@click.option("--bypass-review-count", type=bool, default=False, help="Bypass required review count (default: false)")
|
|
131
|
+
@click.option("--exclude-labels", type=str, default="", help="Restrict to particular labels to excldue")
|
|
132
|
+
@click.option("--include-labels", type=str, default="", help="Restrict to particular labels to incldue")
|
|
133
|
+
@click.option("--merge-method", type=str, default="merge", help="merge|rebase|squash (default: merge)")
|
|
134
|
+
@click.option("--owner", type=str, default="", help="Restrict to a particular user/org")
|
|
135
|
+
@click.option("--prefix", type=str, default="", help="Restrict to a PR title prefix")
|
|
136
|
+
@click.option("--repo-type", type=str, default="all", help="all|private|public (default: all)")
|
|
137
|
+
@click.option("--dry-run", type=bool, default=True, help="Show PR ready for merge (default: true)")
|
|
138
|
+
@click.version_option(version=__version__)
|
|
139
|
+
async def main(
|
|
140
|
+
base_branch: str,
|
|
141
|
+
dry_run: bool,
|
|
142
|
+
exclude_labels: str,
|
|
143
|
+
include_labels: str,
|
|
144
|
+
merge_method: str,
|
|
145
|
+
bypass_review_count: bool,
|
|
146
|
+
owner: str,
|
|
147
|
+
prefix: str,
|
|
148
|
+
repo_type: str,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Main CLI entry point to merge pull requests
|
|
152
|
+
|
|
153
|
+
\f
|
|
154
|
+
Executes the complete workflow:
|
|
155
|
+
1. Load configuration (YAML file or defaults)
|
|
156
|
+
2. Validate and merge CLI arguments with config
|
|
157
|
+
3. Authenticate with GitHub API
|
|
158
|
+
4. Discover repositories
|
|
159
|
+
5. Fetch and filter open pull requests
|
|
160
|
+
6. Evaluate merge readiness (CI + reviews)
|
|
161
|
+
7. Execute merge operations (or dry-run)
|
|
162
|
+
"""
|
|
163
|
+
config_file = find_config_file()
|
|
164
|
+
if config_file:
|
|
165
|
+
config, _ = load_config(config_file)
|
|
166
|
+
else:
|
|
167
|
+
config = Config()
|
|
168
|
+
|
|
169
|
+
cli_args = {
|
|
170
|
+
"base_branch": base_branch,
|
|
171
|
+
"dry_run": dry_run,
|
|
172
|
+
"exclude_labels": exclude_labels,
|
|
173
|
+
"include_labels": include_labels,
|
|
174
|
+
"merge_method": merge_method,
|
|
175
|
+
"bypass_review_count": bypass_review_count,
|
|
176
|
+
"owner": owner,
|
|
177
|
+
"prefix": prefix,
|
|
178
|
+
"repo_type": repo_type,
|
|
179
|
+
}
|
|
180
|
+
merged_config = merge_config_with_cli(config, cli_args)
|
|
181
|
+
|
|
182
|
+
# Display startup message first
|
|
183
|
+
print("🚀 Starting Merge-Pulls (Merge Pull Requests) on GitHub...\n")
|
|
184
|
+
|
|
185
|
+
# Load config if available (for display purposes)
|
|
186
|
+
if config_file:
|
|
187
|
+
print(f"📚 Loading configuration from: {config_file}")
|
|
188
|
+
|
|
189
|
+
# Display merged configuration
|
|
190
|
+
print("📚 Configuration (merged YAML and CLI):")
|
|
191
|
+
print(f" base-branch: {merged_config['base_branch']}")
|
|
192
|
+
print(f" bypass-review-count: {merged_config['bypass_review_count']}")
|
|
193
|
+
print(f" dry-run: {merged_config['dry_run']}")
|
|
194
|
+
print(f" exclude-labels: {merged_config['exclude_labels']}")
|
|
195
|
+
print(f" include-labels: {merged_config['include_labels']}")
|
|
196
|
+
print(f" merge-method: {merged_config['merge_method']}")
|
|
197
|
+
print(f" owner: {merged_config['owner']}")
|
|
198
|
+
print(f" prefix: {merged_config['prefix']}")
|
|
199
|
+
print(f" repo-type: {merged_config['repo_type']}")
|
|
200
|
+
|
|
201
|
+
print() # Empty line before actual operations
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
# Create a single GitHub client instance for the rest of the process
|
|
205
|
+
gh, gh_token = get_auth()
|
|
206
|
+
|
|
207
|
+
# 1. Get Token User Data
|
|
208
|
+
user = gh.get_user()
|
|
209
|
+
login, orgs = get_token_user_info(gh, user)
|
|
210
|
+
|
|
211
|
+
# 2. Process Click Inputs (use merged config values)
|
|
212
|
+
owner = merged_config["owner"]
|
|
213
|
+
repo_type = merged_config["repo_type"]
|
|
214
|
+
merge_method = merged_config["merge_method"]
|
|
215
|
+
set_exclude_labels = merged_config["exclude_labels"]
|
|
216
|
+
set_include_labels = merged_config["include_labels"]
|
|
217
|
+
base_branch = merged_config["base_branch"]
|
|
218
|
+
prefix = merged_config["prefix"]
|
|
219
|
+
dry_run = merged_config["dry_run"]
|
|
220
|
+
bypass_review_count = merged_config["bypass_review_count"]
|
|
221
|
+
|
|
222
|
+
check_owner(owner=owner, orgs=orgs, login=login)
|
|
223
|
+
check_repo_type(repo_type=repo_type)
|
|
224
|
+
check_merge_method(merge_method=merge_method)
|
|
225
|
+
|
|
226
|
+
# 3. Get Repo Data
|
|
227
|
+
if owner:
|
|
228
|
+
repos_obj, repos = get_repos_from_owner(gh, user, owner, repo_type)
|
|
229
|
+
else:
|
|
230
|
+
repos_obj, repos = get_repos_from_all(gh, user, repo_type)
|
|
231
|
+
|
|
232
|
+
# 4. Get PR Data
|
|
233
|
+
list_open_prs = await get_open_prs(gh, repos_obj, base_branch, set_exclude_labels, set_include_labels, prefix)
|
|
234
|
+
|
|
235
|
+
# 5. Check Merge Readiness
|
|
236
|
+
list_mergeable_prs = await get_merge_readiness(gh, list_open_prs, base_branch, bypass_review_count, merge_method)
|
|
237
|
+
|
|
238
|
+
# 6. Merge PRs
|
|
239
|
+
list_merged_prs = await put_merge_pr(gh, list_mergeable_prs, merge_method, dry_run)
|
|
240
|
+
|
|
241
|
+
print_summary(len(list_open_prs), len(list_mergeable_prs), len(list_merged_prs))
|
|
242
|
+
|
|
243
|
+
except CustomError as err:
|
|
244
|
+
print(f"\n❌ Configuration Error: {err}")
|
|
245
|
+
sys.exit(1)
|
|
246
|
+
except (KeyError, PermissionError) as err:
|
|
247
|
+
print(f"\n❌ Authentication Error: {err}")
|
|
248
|
+
sys.exit(1)
|
|
249
|
+
except ValueError as err:
|
|
250
|
+
print(f"\n❌ Error: {err}")
|
|
251
|
+
sys.exit(1)
|
|
252
|
+
except Exception as err:
|
|
253
|
+
print(f"\n❌ Unexpected Error: {err}")
|
|
254
|
+
sys.exit(1)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
if __name__ == "__main__": # pragma: no cover
|
|
258
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management services.
|
|
3
|
+
|
|
4
|
+
Modules:
|
|
5
|
+
- config:
|
|
6
|
+
- Loading YAML configuration files
|
|
7
|
+
- Validating configuration against schema
|
|
8
|
+
- Merging CLI options with config file settings
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .config import Config, load_config, merge_config_with_cli
|
|
12
|
+
|
|
13
|
+
__all__ = ["Config", "load_config", "merge_config_with_cli"]
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management and validation.
|
|
3
|
+
|
|
4
|
+
This module handles:
|
|
5
|
+
- Loading YAML configuration files from multiple locations
|
|
6
|
+
- Strict validation against a dataclass schema
|
|
7
|
+
- Merging CLI options with config file (CLI takes precedence over YAML)
|
|
8
|
+
- Label conversion (string to set) and boolean string parsing
|
|
9
|
+
- Error handling for invalid configurations
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from dataclasses import dataclass, fields
|
|
14
|
+
from typing import (
|
|
15
|
+
Any,
|
|
16
|
+
Dict,
|
|
17
|
+
List,
|
|
18
|
+
Optional,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Config:
|
|
26
|
+
"""
|
|
27
|
+
Configuration model.
|
|
28
|
+
|
|
29
|
+
All fields are optional with defaults. CLI arguments take precedence over
|
|
30
|
+
this configuration when provided.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
base_branch: Target base branch name (default: "main")
|
|
34
|
+
bypass_review_count: Bypass required review count (default: False)
|
|
35
|
+
dry_run: Preview mode without actual merges (default: True)
|
|
36
|
+
exclude_labels: List of labels to exclude to filter PRs (default: [])
|
|
37
|
+
include_labels: List of labels to include to filter PRs (default: [])
|
|
38
|
+
merge_method: Merge strategy: merge, rebase, or squash (default: "merge")
|
|
39
|
+
owner: Filter to specific user/org (default: "")
|
|
40
|
+
prefix: Filter PRs by title prefix (default: "")
|
|
41
|
+
repo_type: Repository type filter: all, private, or public (default: "all")
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
base_branch: str = "main"
|
|
45
|
+
dry_run: bool = True
|
|
46
|
+
exclude_labels: Optional[List[str]] = None
|
|
47
|
+
include_labels: Optional[List[str]] = None
|
|
48
|
+
merge_method: str = "merge"
|
|
49
|
+
bypass_review_count: bool = False
|
|
50
|
+
owner: str = ""
|
|
51
|
+
prefix: str = ""
|
|
52
|
+
repo_type: str = "all"
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
"""Validate configuration after initialization."""
|
|
56
|
+
if self.exclude_labels is None:
|
|
57
|
+
self.exclude_labels = []
|
|
58
|
+
|
|
59
|
+
if self.include_labels is None:
|
|
60
|
+
self.include_labels = []
|
|
61
|
+
|
|
62
|
+
allowed_merge_methods = {"merge", "rebase", "squash"}
|
|
63
|
+
if self.merge_method not in allowed_merge_methods:
|
|
64
|
+
raise ValueError(f"merge_method must be one of {allowed_merge_methods}, got '{self.merge_method}'")
|
|
65
|
+
|
|
66
|
+
allowed_repo_types = {"all", "private", "public"}
|
|
67
|
+
if self.repo_type not in allowed_repo_types:
|
|
68
|
+
raise ValueError(f"repo_type must be one of {allowed_repo_types}, got '{self.repo_type}'")
|
|
69
|
+
|
|
70
|
+
# Reject extra fields
|
|
71
|
+
field_names = {f.name for f in fields(self)}
|
|
72
|
+
for key in self.__dict__.keys():
|
|
73
|
+
if key not in field_names:
|
|
74
|
+
raise ValueError(f"Invalid config field: '{key}'")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def load_config(config_path: Optional[str] = None) -> tuple[Config, Dict[str, Any]]:
|
|
78
|
+
"""
|
|
79
|
+
Load configuration from YAML file.
|
|
80
|
+
|
|
81
|
+
For GitHub Actions users, config is expected at ./config/default.yaml.
|
|
82
|
+
For CLI users, this file is optional (dataclass defaults are used if not found).
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
config_path: Optional explicit path to config file (default: ./config/default.yaml).
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Tuple of (Config object, raw config dict)
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError: If config validation fails.
|
|
92
|
+
yaml.YAMLError: If YAML parsing fails.
|
|
93
|
+
"""
|
|
94
|
+
config_locations = []
|
|
95
|
+
|
|
96
|
+
if config_path:
|
|
97
|
+
config_locations.append(config_path)
|
|
98
|
+
else:
|
|
99
|
+
# Default location for GA users
|
|
100
|
+
config_locations.append("./config/default.yaml")
|
|
101
|
+
|
|
102
|
+
config_dict: Dict[str, Any] = {}
|
|
103
|
+
|
|
104
|
+
for location in config_locations:
|
|
105
|
+
if os.path.exists(location):
|
|
106
|
+
with open(location, "r") as f:
|
|
107
|
+
config_dict = yaml.safe_load(f) or {}
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
if not config_dict and not config_path:
|
|
111
|
+
config_dict = {}
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Convert kebab-case keys to snake_case
|
|
115
|
+
converted_dict = _convert_kebab_to_snake(config_dict)
|
|
116
|
+
config = Config(**converted_dict)
|
|
117
|
+
return config, config_dict
|
|
118
|
+
except Exception as err:
|
|
119
|
+
source_file = config_locations[0] if config_path else "default configuration"
|
|
120
|
+
raise ValueError(f"Configuration error in {source_file}: {err}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _convert_kebab_to_snake(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
124
|
+
"""Convert kebab-case keys to snake_case for dataclass initialization."""
|
|
125
|
+
conversion_map = {
|
|
126
|
+
"base-branch": "base_branch",
|
|
127
|
+
"bypass-review-count": "bypass_review_count",
|
|
128
|
+
"dry-run": "dry_run",
|
|
129
|
+
"exclude-labels": "exclude_labels",
|
|
130
|
+
"include-labels": "include_labels",
|
|
131
|
+
"merge-method": "merge_method",
|
|
132
|
+
"repo-type": "repo_type",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
result: Dict[str, Any] = {}
|
|
136
|
+
for key, value in data.items():
|
|
137
|
+
if key in conversion_map:
|
|
138
|
+
result[conversion_map[key]] = value
|
|
139
|
+
else:
|
|
140
|
+
# Keep unknown keys as-is (they'll be caught by __post_init__ validation if invalid)
|
|
141
|
+
result[key] = value
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def merge_config_with_cli(config: Config, cli_args: Dict[str, Any]) -> Dict[str, Any]:
|
|
147
|
+
"""
|
|
148
|
+
Merge CLI arguments with config, giving precedence to CLI args.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
config: Config object from YAML file (or defaults).
|
|
152
|
+
cli_args: Dictionary of CLI arguments.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dictionary with merged configuration (CLI overrides YAML).
|
|
156
|
+
"""
|
|
157
|
+
merged = {
|
|
158
|
+
"base_branch": cli_args.get("base_branch") or config.base_branch,
|
|
159
|
+
"bypass_review_count": (
|
|
160
|
+
cli_args.get("bypass_review_count")
|
|
161
|
+
if cli_args.get("bypass_review_count") is not None
|
|
162
|
+
else config.bypass_review_count
|
|
163
|
+
),
|
|
164
|
+
"dry_run": cli_args.get("dry_run") if cli_args.get("dry_run") is not None else config.dry_run,
|
|
165
|
+
"exclude_labels": cli_args.get("exclude_labels"),
|
|
166
|
+
"include_labels": cli_args.get("include_labels"),
|
|
167
|
+
"merge_method": cli_args.get("merge_method") or config.merge_method,
|
|
168
|
+
"owner": cli_args.get("owner") or config.owner,
|
|
169
|
+
"prefix": cli_args.get("prefix") or config.prefix,
|
|
170
|
+
"repo_type": cli_args.get("repo_type") or config.repo_type,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Use config defaults for any None values (labels can be None, others use 'or' above)
|
|
174
|
+
if merged["exclude_labels"] is None:
|
|
175
|
+
merged["exclude_labels"] = config.exclude_labels
|
|
176
|
+
if merged["include_labels"] is None:
|
|
177
|
+
merged["include_labels"] = config.include_labels
|
|
178
|
+
|
|
179
|
+
# Handle exclude_labels: convert to set (CLI string or YAML list)
|
|
180
|
+
if isinstance(merged["exclude_labels"], str):
|
|
181
|
+
# Split on both commas and spaces for flexibility
|
|
182
|
+
# First replace commas with spaces, then split
|
|
183
|
+
exclude_label_str = merged["exclude_labels"].replace(",", " ")
|
|
184
|
+
merged["exclude_labels"] = (
|
|
185
|
+
{item.strip() for item in exclude_label_str.split() if item.strip()} if merged["exclude_labels"] else set()
|
|
186
|
+
)
|
|
187
|
+
elif isinstance(merged["exclude_labels"], list):
|
|
188
|
+
merged["exclude_labels"] = set(merged["exclude_labels"])
|
|
189
|
+
else:
|
|
190
|
+
# Handle None or other types (None falls back to config in merge, so this is defensive)
|
|
191
|
+
merged["exclude_labels"] = set()
|
|
192
|
+
|
|
193
|
+
# Handle include_labels: convert to set (CLI string or YAML list)
|
|
194
|
+
if isinstance(merged["include_labels"], str):
|
|
195
|
+
# Split on both commas and spaces for flexibility
|
|
196
|
+
# First replace commas with spaces, then split
|
|
197
|
+
include_label_str = merged["include_labels"].replace(",", " ")
|
|
198
|
+
merged["include_labels"] = (
|
|
199
|
+
{item.strip() for item in include_label_str.split() if item.strip()} if merged["include_labels"] else set()
|
|
200
|
+
)
|
|
201
|
+
elif isinstance(merged["include_labels"], list):
|
|
202
|
+
merged["include_labels"] = set(merged["include_labels"])
|
|
203
|
+
else:
|
|
204
|
+
# Handle None or other types (None falls back to config in merge, so this is defensive)
|
|
205
|
+
merged["include_labels"] = set()
|
|
206
|
+
|
|
207
|
+
# Convert boolean strings to actual booleans (for CLI args)
|
|
208
|
+
for bool_field in ["dry_run", "bypass_review_count"]:
|
|
209
|
+
if isinstance(merged[bool_field], str):
|
|
210
|
+
value = merged[bool_field].lower()
|
|
211
|
+
merged[bool_field] = value in ("true", "1", "yes")
|
|
212
|
+
|
|
213
|
+
return merged
|
|
File without changes
|