pull-request-fixer 0.1.1__tar.gz → 0.1.2__tar.gz
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.
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/PKG-INFO +21 -19
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/README.md +4 -4
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/pyproject.toml +92 -16
- pull_request_fixer-0.1.2/src/pull_request_fixer/_version.py +24 -0
- pull_request_fixer-0.1.2/src/pull_request_fixer/blocked_pr_scanner.py +216 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/cli.py +175 -71
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/exceptions.py +6 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/git_config.py +1 -1
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/github_client.py +15 -4
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/pr_file_fixer.py +143 -30
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/pr_scanner.py +56 -14
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/progress_tracker.py +8 -8
- pull_request_fixer-0.1.1/src/pull_request_fixer/_version.py +0 -34
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/.gitignore +0 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/.pre-commit-hooks.yaml +0 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/LICENSE +0 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/LICENSES/Apache-2.0.txt +0 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/scripts/README.md +0 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/__init__.py +0 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/file_fixer.py +0 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/graphql_queries.py +0 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/models.py +0 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/pr_fixer.py +0 -0
- {pull_request_fixer-0.1.1 → pull_request_fixer-0.1.2}/src/pull_request_fixer/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pull-request-fixer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Pull request formatter and fixer with GitHub integration
|
|
5
5
|
Project-URL: Homepage, https://github.com/lfit/pull-request-fixer
|
|
6
6
|
Project-URL: Repository, https://github.com/lfit/pull-request-fixer.git
|
|
@@ -19,23 +19,25 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
23
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
24
|
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
24
|
-
Requires-Python:
|
|
25
|
-
Requires-Dist: aiolimiter>=1.2.
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-Dist:
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: aiolimiter>=1.2.1
|
|
27
|
+
Requires-Dist: dependamerge>=0.1.19
|
|
28
|
+
Requires-Dist: httpx[http2]>=0.28.1
|
|
29
|
+
Requires-Dist: pydantic>=2.12.5
|
|
30
|
+
Requires-Dist: rich>=14.2.0
|
|
31
|
+
Requires-Dist: tenacity>=9.1.2
|
|
32
|
+
Requires-Dist: typer>=0.20.0
|
|
31
33
|
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: mypy>=1.
|
|
33
|
-
Requires-Dist: pre-commit>=4.0
|
|
34
|
-
Requires-Dist: pytest-asyncio>=
|
|
35
|
-
Requires-Dist: pytest-cov>=
|
|
36
|
-
Requires-Dist: pytest-mock>=3.
|
|
37
|
-
Requires-Dist: pytest>=
|
|
38
|
-
Requires-Dist: ruff>=0.8
|
|
34
|
+
Requires-Dist: mypy>=1.19.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pre-commit>=4.5.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-mock>=3.15.1; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
40
|
+
Requires-Dist: ruff>=0.14.8; extra == 'dev'
|
|
39
41
|
Description-Content-Type: text/markdown
|
|
40
42
|
|
|
41
43
|
<!--
|
|
@@ -313,7 +315,7 @@ pull-request-fixer myorg --fix-title --blocked-only
|
|
|
313
315
|
```
|
|
314
316
|
<!-- write-good-enable -->
|
|
315
317
|
|
|
316
|
-
**Fix files
|
|
318
|
+
**Fix files across PRs:**
|
|
317
319
|
|
|
318
320
|
<!-- write-good-disable -->
|
|
319
321
|
```bash
|
|
@@ -355,6 +357,7 @@ comment.
|
|
|
355
357
|
## Options
|
|
356
358
|
|
|
357
359
|
<!-- markdownlint-disable MD013 -->
|
|
360
|
+
<!-- write-good-disable -->
|
|
358
361
|
|
|
359
362
|
| Flag | Short | Default | Description |
|
|
360
363
|
| ------------------ | ----- | --------------- | ----------------------------------------------------- |
|
|
@@ -379,6 +382,7 @@ comment.
|
|
|
379
382
|
| `--log-level` | | `INFO` | Set logging level |
|
|
380
383
|
| `--version` | | | Show version and exit |
|
|
381
384
|
|
|
385
|
+
<!-- write-good-enable -->
|
|
382
386
|
<!-- markdownlint-enable MD013 -->
|
|
383
387
|
|
|
384
388
|
## How It Works
|
|
@@ -521,15 +525,13 @@ pull-request-fixer https://github.com/lfreleng-actions/make-action/pull/40 \
|
|
|
521
525
|
|
|
522
526
|
Output:
|
|
523
527
|
|
|
524
|
-
<!-- write-good-disable -->
|
|
525
528
|
```text
|
|
526
529
|
🔍 Processing PR: https://github.com/lfreleng-actions/make-action/pull/40
|
|
527
530
|
🔧 Will fix: files
|
|
528
|
-
🏃 Dry run mode:
|
|
531
|
+
🏃 Dry run mode: the tool will not apply changes
|
|
529
532
|
|
|
530
533
|
📝 Fixing files in PR...
|
|
531
534
|
```
|
|
532
|
-
<!-- write-good-enable -->
|
|
533
535
|
|
|
534
536
|
✅ Would fix 1 file
|
|
535
537
|
📂 action.yaml
|
|
@@ -273,7 +273,7 @@ pull-request-fixer myorg --fix-title --blocked-only
|
|
|
273
273
|
```
|
|
274
274
|
<!-- write-good-enable -->
|
|
275
275
|
|
|
276
|
-
**Fix files
|
|
276
|
+
**Fix files across PRs:**
|
|
277
277
|
|
|
278
278
|
<!-- write-good-disable -->
|
|
279
279
|
```bash
|
|
@@ -315,6 +315,7 @@ comment.
|
|
|
315
315
|
## Options
|
|
316
316
|
|
|
317
317
|
<!-- markdownlint-disable MD013 -->
|
|
318
|
+
<!-- write-good-disable -->
|
|
318
319
|
|
|
319
320
|
| Flag | Short | Default | Description |
|
|
320
321
|
| ------------------ | ----- | --------------- | ----------------------------------------------------- |
|
|
@@ -339,6 +340,7 @@ comment.
|
|
|
339
340
|
| `--log-level` | | `INFO` | Set logging level |
|
|
340
341
|
| `--version` | | | Show version and exit |
|
|
341
342
|
|
|
343
|
+
<!-- write-good-enable -->
|
|
342
344
|
<!-- markdownlint-enable MD013 -->
|
|
343
345
|
|
|
344
346
|
## How It Works
|
|
@@ -481,15 +483,13 @@ pull-request-fixer https://github.com/lfreleng-actions/make-action/pull/40 \
|
|
|
481
483
|
|
|
482
484
|
Output:
|
|
483
485
|
|
|
484
|
-
<!-- write-good-disable -->
|
|
485
486
|
```text
|
|
486
487
|
🔍 Processing PR: https://github.com/lfreleng-actions/make-action/pull/40
|
|
487
488
|
🔧 Will fix: files
|
|
488
|
-
🏃 Dry run mode:
|
|
489
|
+
🏃 Dry run mode: the tool will not apply changes
|
|
489
490
|
|
|
490
491
|
📝 Fixing files in PR...
|
|
491
492
|
```
|
|
492
|
-
<!-- write-good-enable -->
|
|
493
493
|
|
|
494
494
|
✅ Would fix 1 file
|
|
495
495
|
📂 action.yaml
|
|
@@ -10,7 +10,7 @@ name = "pull-request-fixer"
|
|
|
10
10
|
dynamic = ["version"]
|
|
11
11
|
description = "Pull request formatter and fixer with GitHub integration"
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.10
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
14
|
license = {text = "Apache-2.0"}
|
|
15
15
|
authors = [
|
|
16
16
|
{name = "The Linux Foundation", email = "releng@linuxfoundation.org"},
|
|
@@ -27,27 +27,29 @@ classifiers = [
|
|
|
27
27
|
"Programming Language :: Python :: 3.11",
|
|
28
28
|
"Programming Language :: Python :: 3.12",
|
|
29
29
|
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Programming Language :: Python :: 3.14",
|
|
30
31
|
"Topic :: Software Development :: Quality Assurance",
|
|
31
32
|
"Topic :: Text Processing :: Markup :: Markdown",
|
|
32
33
|
]
|
|
33
34
|
dependencies = [
|
|
34
|
-
"typer>=0.
|
|
35
|
-
"rich>=
|
|
36
|
-
"httpx[http2]>=0.28.
|
|
37
|
-
"pydantic>=2.
|
|
38
|
-
"aiolimiter>=1.2.
|
|
39
|
-
"tenacity>=9.
|
|
35
|
+
"typer>=0.20.0",
|
|
36
|
+
"rich>=14.2.0",
|
|
37
|
+
"httpx[http2]>=0.28.1",
|
|
38
|
+
"pydantic>=2.12.5",
|
|
39
|
+
"aiolimiter>=1.2.1",
|
|
40
|
+
"tenacity>=9.1.2",
|
|
41
|
+
"dependamerge>=0.1.19",
|
|
40
42
|
]
|
|
41
43
|
|
|
42
44
|
[project.optional-dependencies]
|
|
43
45
|
dev = [
|
|
44
|
-
"pytest>=
|
|
45
|
-
"pytest-cov>=
|
|
46
|
-
"pytest-asyncio>=
|
|
47
|
-
"pytest-mock>=3.
|
|
48
|
-
"mypy>=1.
|
|
49
|
-
"ruff>=0.8
|
|
50
|
-
"pre-commit>=4.0
|
|
46
|
+
"pytest>=9.0.2",
|
|
47
|
+
"pytest-cov>=7.0.0",
|
|
48
|
+
"pytest-asyncio>=1.3.0",
|
|
49
|
+
"pytest-mock>=3.15.1",
|
|
50
|
+
"mypy>=1.19.0",
|
|
51
|
+
"ruff>=0.14.8",
|
|
52
|
+
"pre-commit>=4.5.0",
|
|
51
53
|
]
|
|
52
54
|
|
|
53
55
|
[project.urls]
|
|
@@ -60,6 +62,7 @@ pull-request-fixer = "pull_request_fixer.cli:cli"
|
|
|
60
62
|
|
|
61
63
|
[tool.hatch.version]
|
|
62
64
|
source = "vcs"
|
|
65
|
+
fallback-version = "0.0.0"
|
|
63
66
|
|
|
64
67
|
[tool.hatch.build.hooks.vcs]
|
|
65
68
|
version-file = "src/pull_request_fixer/_version.py"
|
|
@@ -71,7 +74,7 @@ packages = ["src/pull_request_fixer"]
|
|
|
71
74
|
include = ["src", "README.md", "LICENSE*", "pyproject.toml", ".pre-commit-hooks.yaml"]
|
|
72
75
|
|
|
73
76
|
[tool.ruff]
|
|
74
|
-
target-version = "
|
|
77
|
+
target-version = "py312"
|
|
75
78
|
line-length = 80
|
|
76
79
|
exclude = [
|
|
77
80
|
"src/pull_request_fixer/_version.py",
|
|
@@ -114,8 +117,81 @@ indent-style = "space"
|
|
|
114
117
|
skip-magic-trailing-comma = false
|
|
115
118
|
line-ending = "auto"
|
|
116
119
|
|
|
120
|
+
[tool.basedpyright]
|
|
121
|
+
pythonVersion = "3.12"
|
|
122
|
+
pythonPlatform = "All"
|
|
123
|
+
typeCheckingMode = "standard"
|
|
124
|
+
include = ["src", "tests", "scripts", "test_blocked_pr_logic.py", "test_integration.py"]
|
|
125
|
+
exclude = [
|
|
126
|
+
"src/pull_request_fixer/_version.py",
|
|
127
|
+
".venv",
|
|
128
|
+
"build",
|
|
129
|
+
"dist",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# --- Errors we want to catch and fix ---
|
|
133
|
+
reportMissingTypeArgument = "warning"
|
|
134
|
+
reportOptionalMemberAccess = "warning"
|
|
135
|
+
reportCallIssue = "warning"
|
|
136
|
+
reportAssignmentType = "warning"
|
|
137
|
+
reportReturnType = "warning"
|
|
138
|
+
reportGeneralTypeIssues = "warning"
|
|
139
|
+
reportUninitializedInstanceVariable = "warning"
|
|
140
|
+
|
|
141
|
+
# --- Intentional patterns we suppress ---
|
|
142
|
+
# Third-party imports cannot be resolved in pre-commit.ci sandbox
|
|
143
|
+
reportMissingImports = false
|
|
144
|
+
# Typer uses Option() calls in parameter defaults by design
|
|
145
|
+
reportCallInDefaultInitializer = false
|
|
146
|
+
# Typer parameters are consumed by the framework, not the function body
|
|
147
|
+
reportUnusedParameter = false
|
|
148
|
+
# Test files legitimately access private members for testing
|
|
149
|
+
reportPrivateUsage = false
|
|
150
|
+
# Test files and CLI create values that are intentionally unused
|
|
151
|
+
reportUnusedCallResult = false
|
|
152
|
+
# Many third-party and dynamic types produce Any
|
|
153
|
+
reportAny = false
|
|
154
|
+
reportExplicitAny = false
|
|
155
|
+
reportUnknownParameterType = false
|
|
156
|
+
reportUnknownMemberType = false
|
|
157
|
+
reportUnknownVariableType = false
|
|
158
|
+
reportUnknownLambdaType = false
|
|
159
|
+
reportUnknownArgumentType = false
|
|
160
|
+
# Missing type annotations are tracked separately (mypy)
|
|
161
|
+
reportMissingParameterType = false
|
|
162
|
+
# String concatenation in GraphQL queries and Rich output is intentional
|
|
163
|
+
reportImplicitStringConcatenation = false
|
|
164
|
+
# Class attributes in tests don't need annotations
|
|
165
|
+
reportUnannotatedClassAttribute = false
|
|
166
|
+
# Unused imports are handled by ruff (F401)
|
|
167
|
+
reportUnusedImport = false
|
|
168
|
+
# Unused functions are typically CLI helpers or test utilities
|
|
169
|
+
reportUnusedFunction = false
|
|
170
|
+
# Unused variables are handled by ruff (F841)
|
|
171
|
+
reportUnusedVariable = false
|
|
172
|
+
# Deprecated typing imports in auto-generated files
|
|
173
|
+
reportDeprecated = false
|
|
174
|
+
# Override decorators are not required
|
|
175
|
+
reportImplicitOverride = false
|
|
176
|
+
# isinstance checks used for runtime safety
|
|
177
|
+
reportUnnecessaryIsInstance = false
|
|
178
|
+
# Comparison checks used for runtime safety
|
|
179
|
+
reportUnnecessaryComparison = false
|
|
180
|
+
# Unreachable code used for defensive programming
|
|
181
|
+
reportUnreachable = false
|
|
182
|
+
# Missing type stubs for third-party packages
|
|
183
|
+
reportMissingTypeStubs = false
|
|
184
|
+
# Constant redefinition patterns (e.g. try/except fallback classes)
|
|
185
|
+
reportConstantRedefinition = false
|
|
186
|
+
# Attribute access issues in tests (mock objects)
|
|
187
|
+
reportAttributeAccessIssue = false
|
|
188
|
+
# Operator issues in tests (mock assertions)
|
|
189
|
+
reportOperatorIssue = false
|
|
190
|
+
# Index issues in tests (mock return values)
|
|
191
|
+
reportIndexIssue = false
|
|
192
|
+
|
|
117
193
|
[tool.mypy]
|
|
118
|
-
python_version = "3.
|
|
194
|
+
python_version = "3.12"
|
|
119
195
|
strict = true
|
|
120
196
|
warn_return_any = true
|
|
121
197
|
warn_unused_configs = true
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 2)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Blocked PR scanner using dependamerge's GitHubService.
|
|
6
|
+
|
|
7
|
+
This module wraps dependamerge's PR scanning functionality to provide
|
|
8
|
+
consistent blocked PR detection across both tools.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from collections.abc import AsyncIterator
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from dependamerge.github_service import GitHubService
|
|
17
|
+
from dependamerge.models import UnmergeablePR
|
|
18
|
+
|
|
19
|
+
from .progress_tracker import ProgressTracker
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BlockedPRScanner:
|
|
23
|
+
"""Scanner for blocked/unmergeable pull requests using dependamerge logic."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
token: str,
|
|
28
|
+
progress_tracker: ProgressTracker | None = None,
|
|
29
|
+
max_repo_tasks: int = 10,
|
|
30
|
+
):
|
|
31
|
+
"""Initialize the blocked PR scanner.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
token: GitHub authentication token
|
|
35
|
+
progress_tracker: Optional progress tracker for UI updates
|
|
36
|
+
max_repo_tasks: Maximum number of repositories to process concurrently
|
|
37
|
+
"""
|
|
38
|
+
self.token = token
|
|
39
|
+
self.progress_tracker = progress_tracker
|
|
40
|
+
self.max_repo_tasks = max_repo_tasks
|
|
41
|
+
self.logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
# Initialize the dependamerge GitHubService
|
|
44
|
+
self.service = GitHubService(
|
|
45
|
+
token=token,
|
|
46
|
+
progress_tracker=progress_tracker,
|
|
47
|
+
max_repo_tasks=max_repo_tasks,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def close(self) -> None:
|
|
51
|
+
"""Close the underlying HTTP client."""
|
|
52
|
+
await self.service.close()
|
|
53
|
+
|
|
54
|
+
async def scan_organization_for_blocked_prs(
|
|
55
|
+
self,
|
|
56
|
+
organization: str,
|
|
57
|
+
include_drafts: bool = False,
|
|
58
|
+
) -> AsyncIterator[tuple[str, str, dict[str, Any], UnmergeablePR]]:
|
|
59
|
+
"""Scan an organization for blocked/unmergeable pull requests.
|
|
60
|
+
|
|
61
|
+
This method uses dependamerge's GitHubService to analyze PRs and only
|
|
62
|
+
yields those that are truly blocked (have merge conflicts, failing checks,
|
|
63
|
+
behind base, etc.). This ensures consistency with dependamerge's reporting.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
organization: Organization name to scan
|
|
67
|
+
include_drafts: Whether to include draft PRs (default False)
|
|
68
|
+
|
|
69
|
+
Yields:
|
|
70
|
+
Tuples of (owner, repo_name, pr_data, unmergeable_pr) where:
|
|
71
|
+
- owner: Repository owner
|
|
72
|
+
- repo_name: Repository name
|
|
73
|
+
- pr_data: Raw PR data dict from GraphQL
|
|
74
|
+
- unmergeable_pr: UnmergeablePR model with blocking reasons
|
|
75
|
+
"""
|
|
76
|
+
self.logger.debug(
|
|
77
|
+
f"Starting blocked PR scan of organization: {organization}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Start progress tracker if available
|
|
81
|
+
if self.progress_tracker:
|
|
82
|
+
self.progress_tracker.start()
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Use dependamerge's scan_organization which already:
|
|
86
|
+
# 1. Counts repositories
|
|
87
|
+
# 2. Paginates through PRs
|
|
88
|
+
# 3. Analyzes blocking conditions
|
|
89
|
+
# 4. Tracks progress correctly
|
|
90
|
+
scan_result = await self.service.scan_organization(
|
|
91
|
+
organization, include_drafts=include_drafts
|
|
92
|
+
)
|
|
93
|
+
finally:
|
|
94
|
+
# Stop progress tracker before yielding results
|
|
95
|
+
if self.progress_tracker:
|
|
96
|
+
self.progress_tracker.stop()
|
|
97
|
+
|
|
98
|
+
# Display summary
|
|
99
|
+
if self.progress_tracker:
|
|
100
|
+
from rich.console import Console
|
|
101
|
+
|
|
102
|
+
console = Console()
|
|
103
|
+
console.print(
|
|
104
|
+
f"\n✅ Scan completed\n"
|
|
105
|
+
f"📊 Found {len(scan_result.unmergeable_prs)} blocked PRs "
|
|
106
|
+
f"out of {scan_result.total_prs} total PRs across {scan_result.scanned_repositories} repositories\n"
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
self.logger.info(
|
|
110
|
+
f"Scan complete: found {len(scan_result.unmergeable_prs)} blocked PRs "
|
|
111
|
+
f"out of {scan_result.total_prs} total PRs across {scan_result.scanned_repositories} repositories"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Now we need to fetch the full PR data for each unmergeable PR
|
|
115
|
+
# to provide it in the same format as the old scanner
|
|
116
|
+
for unmergeable_pr in scan_result.unmergeable_prs:
|
|
117
|
+
# Split repository name
|
|
118
|
+
parts = unmergeable_pr.repository.split("/", 1)
|
|
119
|
+
if len(parts) != 2:
|
|
120
|
+
self.logger.warning(
|
|
121
|
+
f"Invalid repository format: {unmergeable_pr.repository}"
|
|
122
|
+
)
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
owner, repo_name = parts
|
|
126
|
+
|
|
127
|
+
# Create a minimal pr_data dict that matches what the old scanner returned
|
|
128
|
+
# This is primarily for compatibility with existing code
|
|
129
|
+
pr_data = {
|
|
130
|
+
"number": unmergeable_pr.pr_number,
|
|
131
|
+
"title": unmergeable_pr.title,
|
|
132
|
+
"author": {"login": unmergeable_pr.author},
|
|
133
|
+
"url": unmergeable_pr.url,
|
|
134
|
+
"createdAt": unmergeable_pr.created_at,
|
|
135
|
+
"updatedAt": unmergeable_pr.updated_at,
|
|
136
|
+
# Include blocking information for backward compatibility
|
|
137
|
+
"_unmergeable_reasons": [
|
|
138
|
+
{
|
|
139
|
+
"type": reason.type,
|
|
140
|
+
"description": reason.description,
|
|
141
|
+
"details": reason.details,
|
|
142
|
+
}
|
|
143
|
+
for reason in unmergeable_pr.reasons
|
|
144
|
+
],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
yield owner, repo_name, pr_data, unmergeable_pr
|
|
148
|
+
|
|
149
|
+
async def scan_organization_all_prs(
|
|
150
|
+
self,
|
|
151
|
+
organization: str,
|
|
152
|
+
include_drafts: bool = False,
|
|
153
|
+
) -> AsyncIterator[tuple[str, str, dict[str, Any], bool]]:
|
|
154
|
+
"""Scan an organization for ALL pull requests, marking which are blocked.
|
|
155
|
+
|
|
156
|
+
This is useful when you want to process all PRs but need to know which
|
|
157
|
+
ones are blocked.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
organization: Organization name to scan
|
|
161
|
+
include_drafts: Whether to include draft PRs
|
|
162
|
+
|
|
163
|
+
Yields:
|
|
164
|
+
Tuples of (owner, repo_name, pr_data, is_blocked) where:
|
|
165
|
+
- owner: Repository owner
|
|
166
|
+
- repo_name: Repository name
|
|
167
|
+
- pr_data: Raw PR data dict from GraphQL
|
|
168
|
+
- is_blocked: Boolean indicating if PR is blocked
|
|
169
|
+
"""
|
|
170
|
+
# For this use case, we need to fetch ALL PRs, not just blocked ones
|
|
171
|
+
# This requires using the lower-level iteration methods
|
|
172
|
+
# For now, we'll note this is a TODO and focus on the blocked-only case
|
|
173
|
+
raise NotImplementedError(
|
|
174
|
+
"Scanning all PRs (not just blocked) is not yet implemented. "
|
|
175
|
+
"Use scan_organization_for_blocked_prs() for blocked-only scanning."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def is_pr_blocked(self, pr: dict[str, Any]) -> tuple[bool, list[str]]:
|
|
179
|
+
"""Check if a PR is blocked from merging (synchronous wrapper).
|
|
180
|
+
|
|
181
|
+
This method provides backward compatibility with the old scanner interface.
|
|
182
|
+
It uses the same logic as dependamerge's _analyze_pr_node.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
pr: PR data from GraphQL
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Tuple of (is_blocked, list of blocking reasons as strings)
|
|
189
|
+
"""
|
|
190
|
+
# Use asyncio to call the async version
|
|
191
|
+
return asyncio.run(self._is_pr_blocked_async(pr))
|
|
192
|
+
|
|
193
|
+
async def _is_pr_blocked_async(
|
|
194
|
+
self, pr: dict[str, Any]
|
|
195
|
+
) -> tuple[bool, list[str]]:
|
|
196
|
+
"""Async version of is_pr_blocked.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
pr: PR data from GraphQL
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Tuple of (is_blocked, list of blocking reasons as strings)
|
|
203
|
+
"""
|
|
204
|
+
# Use dependamerge's _analyze_pr_node to check if PR is blocked
|
|
205
|
+
result = await self.service._analyze_pr_node(
|
|
206
|
+
repo_full_name="temp/repo", # Not used for just checking blocked status
|
|
207
|
+
pr=pr,
|
|
208
|
+
include_drafts=False,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if result is None:
|
|
212
|
+
return False, []
|
|
213
|
+
|
|
214
|
+
# Extract reason descriptions
|
|
215
|
+
reasons = [reason.description for reason in result.reasons]
|
|
216
|
+
return True, reasons
|