markdown-table-fixer 0.1.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.
- markdown_table_fixer/__init__.py +13 -0
- markdown_table_fixer/_version.py +34 -0
- markdown_table_fixer/cli.py +703 -0
- markdown_table_fixer/exceptions.py +90 -0
- markdown_table_fixer/github_client.py +282 -0
- markdown_table_fixer/graphql_queries.py +258 -0
- markdown_table_fixer/models.py +250 -0
- markdown_table_fixer/pr_fixer.py +825 -0
- markdown_table_fixer/pr_scanner.py +462 -0
- markdown_table_fixer/progress_tracker.py +472 -0
- markdown_table_fixer/py.typed +0 -0
- markdown_table_fixer/table_fixer.py +344 -0
- markdown_table_fixer/table_parser.py +233 -0
- markdown_table_fixer/table_validator.py +249 -0
- markdown_table_fixer-0.1.0.dist-info/METADATA +271 -0
- markdown_table_fixer-0.1.0.dist-info/RECORD +19 -0
- markdown_table_fixer-0.1.0.dist-info/WHEEL +4 -0
- markdown_table_fixer-0.1.0.dist-info/entry_points.txt +2 -0
- markdown_table_fixer-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
|
3
|
+
|
|
4
|
+
"""Command-line interface for markdown-table-fixer."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.logging import RichHandler
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
|
|
21
|
+
from ._version import __version__
|
|
22
|
+
from .exceptions import (
|
|
23
|
+
FileAccessError,
|
|
24
|
+
TableParseError,
|
|
25
|
+
)
|
|
26
|
+
from .github_client import GitHubClient
|
|
27
|
+
from .models import (
|
|
28
|
+
BlockedPR,
|
|
29
|
+
FileResult,
|
|
30
|
+
OutputFormat,
|
|
31
|
+
PRInfo,
|
|
32
|
+
ScanResult,
|
|
33
|
+
TableViolation,
|
|
34
|
+
)
|
|
35
|
+
from .pr_fixer import PRFixer
|
|
36
|
+
from .pr_scanner import PRScanner
|
|
37
|
+
from .progress_tracker import ProgressTracker
|
|
38
|
+
from .table_fixer import FileFixer
|
|
39
|
+
from .table_parser import MarkdownFileScanner, TableParser
|
|
40
|
+
from .table_validator import TableValidator
|
|
41
|
+
|
|
42
|
+
console = Console()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def version_callback(value: bool) -> None:
|
|
46
|
+
"""Show version and exit."""
|
|
47
|
+
if value:
|
|
48
|
+
console.print(f"🏷️ markdown-table-fixer version {__version__}")
|
|
49
|
+
console.print()
|
|
50
|
+
raise typer.Exit()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def setup_logging(
|
|
54
|
+
log_level: str = "INFO", quiet: bool = False, verbose: bool = False
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Configure logging with Rich handler."""
|
|
57
|
+
if quiet:
|
|
58
|
+
log_level = "ERROR"
|
|
59
|
+
elif verbose:
|
|
60
|
+
log_level = "DEBUG"
|
|
61
|
+
|
|
62
|
+
logging.basicConfig(
|
|
63
|
+
level=log_level,
|
|
64
|
+
format="%(message)s",
|
|
65
|
+
handlers=[
|
|
66
|
+
RichHandler(console=console, show_time=False, show_path=False)
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Silence httpx INFO logs to prevent Rich display interruption
|
|
71
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Create Typer app with version in help
|
|
75
|
+
app = typer.Typer(
|
|
76
|
+
name="markdown-table-fixer",
|
|
77
|
+
help=f"markdown-table-fixer version {__version__}\n\nFix markdown table formatting issues",
|
|
78
|
+
add_completion=False,
|
|
79
|
+
rich_markup_mode="rich",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Update command docstrings with version at module load time
|
|
84
|
+
def _add_version_to_docstring(
|
|
85
|
+
cmd_func: Callable[..., None],
|
|
86
|
+
) -> Callable[..., None]:
|
|
87
|
+
"""Add version info to command docstring."""
|
|
88
|
+
if cmd_func.__doc__:
|
|
89
|
+
cmd_func.__doc__ = (
|
|
90
|
+
f"markdown-table-fixer version {__version__}\n\n" + cmd_func.__doc__
|
|
91
|
+
)
|
|
92
|
+
return cmd_func
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@app.callback() # type: ignore[misc]
|
|
96
|
+
def main(
|
|
97
|
+
version: bool = typer.Option(
|
|
98
|
+
False,
|
|
99
|
+
"--version",
|
|
100
|
+
callback=version_callback,
|
|
101
|
+
is_eager=True,
|
|
102
|
+
help="Show version and exit",
|
|
103
|
+
),
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Markdown table formatter and linter with GitHub integration."""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.command() # type: ignore[misc]
|
|
110
|
+
@_add_version_to_docstring # type: ignore[misc]
|
|
111
|
+
def lint(
|
|
112
|
+
path: Path = typer.Argument(
|
|
113
|
+
Path("."),
|
|
114
|
+
help="Path to scan for markdown files",
|
|
115
|
+
exists=True,
|
|
116
|
+
),
|
|
117
|
+
output_format: OutputFormat = typer.Option(
|
|
118
|
+
OutputFormat.TEXT,
|
|
119
|
+
"--format",
|
|
120
|
+
help="Output format: text, json",
|
|
121
|
+
case_sensitive=False,
|
|
122
|
+
),
|
|
123
|
+
quiet: bool = typer.Option(
|
|
124
|
+
False,
|
|
125
|
+
"--quiet",
|
|
126
|
+
"-q",
|
|
127
|
+
help="Suppress output except errors",
|
|
128
|
+
),
|
|
129
|
+
check: bool = typer.Option(
|
|
130
|
+
False,
|
|
131
|
+
"--check",
|
|
132
|
+
help="Exit with error if issues found (CI mode)",
|
|
133
|
+
),
|
|
134
|
+
_version: bool = typer.Option(
|
|
135
|
+
False,
|
|
136
|
+
"--version",
|
|
137
|
+
help="Show version and exit",
|
|
138
|
+
callback=version_callback,
|
|
139
|
+
is_eager=True,
|
|
140
|
+
),
|
|
141
|
+
max_line_length: int = typer.Option(
|
|
142
|
+
80,
|
|
143
|
+
"--max-line-length",
|
|
144
|
+
help="Maximum line length before adding MD013 disable comments",
|
|
145
|
+
),
|
|
146
|
+
auto_fix: bool = typer.Option(
|
|
147
|
+
False,
|
|
148
|
+
"--auto-fix/--no-auto-fix",
|
|
149
|
+
help="Automatically fix issues found (default: disabled)",
|
|
150
|
+
),
|
|
151
|
+
fail_on_error: bool = typer.Option(
|
|
152
|
+
True,
|
|
153
|
+
"--fail-on-error/--no-fail-on-error",
|
|
154
|
+
help="Exit with error if issues found (default: enabled)",
|
|
155
|
+
),
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Scan and optionally fix markdown table formatting issues.
|
|
158
|
+
|
|
159
|
+
By default, scans the current directory and reports issues without fixing.
|
|
160
|
+
Use --auto-fix to automatically fix issues found.
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
markdown-table-fixer lint # Scan current directory
|
|
164
|
+
markdown-table-fixer lint --auto-fix # Scan and fix issues
|
|
165
|
+
markdown-table-fixer lint /path/to/docs # Scan specific path
|
|
166
|
+
markdown-table-fixer lint --check # CI mode: fail if issues found
|
|
167
|
+
"""
|
|
168
|
+
# Use the positional path argument
|
|
169
|
+
scan_path = path
|
|
170
|
+
|
|
171
|
+
# Use auto_fix flag to determine if we should fix issues
|
|
172
|
+
should_fix = auto_fix
|
|
173
|
+
|
|
174
|
+
# Don't print status messages in JSON mode or when quiet
|
|
175
|
+
if not quiet and output_format != OutputFormat.JSON:
|
|
176
|
+
console.print(f"🔍 Scanning: {scan_path}")
|
|
177
|
+
if should_fix:
|
|
178
|
+
console.print("🔧 Auto-fix enabled")
|
|
179
|
+
console.print()
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
scanner = MarkdownFileScanner(scan_path)
|
|
183
|
+
markdown_files = scanner.find_markdown_files()
|
|
184
|
+
|
|
185
|
+
if not markdown_files:
|
|
186
|
+
if not quiet:
|
|
187
|
+
console.print(
|
|
188
|
+
f"[yellow]No markdown files found in {scan_path}[/yellow]"
|
|
189
|
+
)
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
# Process files
|
|
193
|
+
results = []
|
|
194
|
+
for md_file in markdown_files:
|
|
195
|
+
result = _process_file(md_file, should_fix, max_line_length)
|
|
196
|
+
results.append(result)
|
|
197
|
+
|
|
198
|
+
# Create scan result
|
|
199
|
+
scan_result = ScanResult()
|
|
200
|
+
for result in results:
|
|
201
|
+
scan_result.add_file_result(result)
|
|
202
|
+
|
|
203
|
+
# Output results
|
|
204
|
+
if output_format == OutputFormat.JSON:
|
|
205
|
+
_output_json_results(scan_result)
|
|
206
|
+
else:
|
|
207
|
+
_output_text_results(scan_result, quiet)
|
|
208
|
+
|
|
209
|
+
# Exit with error if issues found and check mode
|
|
210
|
+
if (check or fail_on_error) and scan_result.files_with_issues > 0:
|
|
211
|
+
raise typer.Exit(1)
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
215
|
+
raise typer.Exit(1) from e
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.command() # type: ignore[misc]
|
|
219
|
+
@_add_version_to_docstring # type: ignore[misc]
|
|
220
|
+
def github(
|
|
221
|
+
target: str = typer.Argument(
|
|
222
|
+
...,
|
|
223
|
+
help="GitHub organization name/URL or PR URL (e.g., https://github.com/org or https://github.com/owner/repo/pull/123)",
|
|
224
|
+
),
|
|
225
|
+
token: str | None = typer.Option(
|
|
226
|
+
None,
|
|
227
|
+
"--token",
|
|
228
|
+
"-t",
|
|
229
|
+
help="GitHub token (or set GITHUB_TOKEN env var)",
|
|
230
|
+
envvar="GITHUB_TOKEN",
|
|
231
|
+
),
|
|
232
|
+
sync_strategy: str = typer.Option(
|
|
233
|
+
"none",
|
|
234
|
+
"--sync-strategy",
|
|
235
|
+
help="How to sync PR with base branch: 'rebase', 'merge', or 'none' (default: none)",
|
|
236
|
+
case_sensitive=False,
|
|
237
|
+
),
|
|
238
|
+
conflict_strategy: str = typer.Option(
|
|
239
|
+
"fail",
|
|
240
|
+
"--conflict-strategy",
|
|
241
|
+
help="How to resolve conflicts: 'fail', 'ours' (keep PR changes), or 'theirs' (keep base changes)",
|
|
242
|
+
case_sensitive=False,
|
|
243
|
+
),
|
|
244
|
+
dry_run: bool = typer.Option(
|
|
245
|
+
False,
|
|
246
|
+
"--dry-run",
|
|
247
|
+
help="Preview changes without applying them",
|
|
248
|
+
),
|
|
249
|
+
include_drafts: bool = typer.Option(
|
|
250
|
+
False,
|
|
251
|
+
"--include-drafts",
|
|
252
|
+
help="Include draft PRs in scan",
|
|
253
|
+
),
|
|
254
|
+
debug_org: bool = typer.Option(
|
|
255
|
+
False,
|
|
256
|
+
"--debug-org",
|
|
257
|
+
help="Debug mode: only process first PR found (useful for testing org scans)",
|
|
258
|
+
),
|
|
259
|
+
_workers: int = typer.Option(
|
|
260
|
+
4,
|
|
261
|
+
"--workers",
|
|
262
|
+
"-j",
|
|
263
|
+
min=1,
|
|
264
|
+
max=32,
|
|
265
|
+
help="Number of parallel workers (default: 4)",
|
|
266
|
+
),
|
|
267
|
+
verbose: bool = typer.Option(
|
|
268
|
+
False,
|
|
269
|
+
"--verbose",
|
|
270
|
+
"-v",
|
|
271
|
+
help="Enable verbose output",
|
|
272
|
+
),
|
|
273
|
+
quiet: bool = typer.Option(
|
|
274
|
+
False,
|
|
275
|
+
"--quiet",
|
|
276
|
+
"-q",
|
|
277
|
+
help="Suppress all output except errors",
|
|
278
|
+
),
|
|
279
|
+
log_level: str = typer.Option(
|
|
280
|
+
"INFO",
|
|
281
|
+
"--log-level",
|
|
282
|
+
help="Set logging level",
|
|
283
|
+
),
|
|
284
|
+
_version: bool = typer.Option(
|
|
285
|
+
False,
|
|
286
|
+
"--version",
|
|
287
|
+
help="Show version and exit",
|
|
288
|
+
callback=version_callback,
|
|
289
|
+
is_eager=True,
|
|
290
|
+
),
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Fix markdown tables in GitHub PRs.
|
|
293
|
+
|
|
294
|
+
Can process either:
|
|
295
|
+
- An entire organization (scans all PRs for table issues)
|
|
296
|
+
- A specific PR by URL
|
|
297
|
+
|
|
298
|
+
Sync strategies:
|
|
299
|
+
- 'none': Fix tables on PR branch as-is (may have conflicts)
|
|
300
|
+
- 'rebase': Rebase PR onto base branch before fixing (cleaner history)
|
|
301
|
+
- 'merge': Merge base branch into PR before fixing (safer)
|
|
302
|
+
|
|
303
|
+
Conflict resolution strategies (when sync causes conflicts):
|
|
304
|
+
- 'fail': Abort if conflicts occur (default, safest)
|
|
305
|
+
- 'ours': Keep PR changes, discard conflicting base changes
|
|
306
|
+
- 'theirs': Keep base changes, discard conflicting PR changes
|
|
307
|
+
|
|
308
|
+
Examples:
|
|
309
|
+
markdown-table-fixer github myorg --token ghp_xxx
|
|
310
|
+
markdown-table-fixer github https://github.com/owner/repo/pull/123
|
|
311
|
+
markdown-table-fixer github https://github.com/owner/repo/pull/123 --sync-strategy rebase
|
|
312
|
+
markdown-table-fixer github https://github.com/owner/repo/pull/123 --sync-strategy merge --conflict-strategy ours
|
|
313
|
+
"""
|
|
314
|
+
setup_logging(log_level=log_level, quiet=quiet, verbose=verbose)
|
|
315
|
+
|
|
316
|
+
# Validate sync strategy
|
|
317
|
+
if sync_strategy.lower() not in ["none", "rebase", "merge"]:
|
|
318
|
+
console.print(
|
|
319
|
+
f"[red]Error:[/red] Invalid sync strategy '{sync_strategy}'. "
|
|
320
|
+
"Use 'none', 'rebase', or 'merge'"
|
|
321
|
+
)
|
|
322
|
+
raise typer.Exit(1)
|
|
323
|
+
|
|
324
|
+
# Validate conflict strategy
|
|
325
|
+
if conflict_strategy.lower() not in ["fail", "ours", "theirs"]:
|
|
326
|
+
console.print(
|
|
327
|
+
f"[red]Error:[/red] Invalid conflict strategy '{conflict_strategy}'. "
|
|
328
|
+
"Use 'fail', 'ours', or 'theirs'"
|
|
329
|
+
)
|
|
330
|
+
raise typer.Exit(1)
|
|
331
|
+
|
|
332
|
+
if not token:
|
|
333
|
+
console.print(
|
|
334
|
+
"[red]Error:[/red] GitHub token required. "
|
|
335
|
+
"Provide --token or set GITHUB_TOKEN environment variable"
|
|
336
|
+
)
|
|
337
|
+
raise typer.Exit(1)
|
|
338
|
+
|
|
339
|
+
# Check if target is a PR URL or organization name
|
|
340
|
+
if "/pull/" in target or "/pulls/" in target:
|
|
341
|
+
# Single PR
|
|
342
|
+
asyncio.run(
|
|
343
|
+
_fix_single_pr(
|
|
344
|
+
target,
|
|
345
|
+
token,
|
|
346
|
+
sync_strategy=sync_strategy.lower(),
|
|
347
|
+
conflict_strategy=conflict_strategy.lower(),
|
|
348
|
+
dry_run=dry_run,
|
|
349
|
+
quiet=quiet,
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
# Scan organization
|
|
354
|
+
asyncio.run(
|
|
355
|
+
_scan_organization(
|
|
356
|
+
target,
|
|
357
|
+
token,
|
|
358
|
+
sync_strategy=sync_strategy.lower(),
|
|
359
|
+
conflict_strategy=conflict_strategy.lower(),
|
|
360
|
+
dry_run=dry_run,
|
|
361
|
+
include_drafts=include_drafts,
|
|
362
|
+
debug_org=debug_org,
|
|
363
|
+
quiet=quiet,
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
async def _fix_single_pr(
|
|
369
|
+
pr_url: str,
|
|
370
|
+
token: str,
|
|
371
|
+
sync_strategy: str = "none",
|
|
372
|
+
conflict_strategy: str = "fail",
|
|
373
|
+
dry_run: bool = False,
|
|
374
|
+
quiet: bool = False,
|
|
375
|
+
) -> None:
|
|
376
|
+
"""Fix markdown tables in a single PR."""
|
|
377
|
+
if not quiet:
|
|
378
|
+
console.print(f"🔍 Processing PR: {pr_url}")
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
async with GitHubClient(token) as client: # type: ignore[attr-defined]
|
|
382
|
+
fixer = PRFixer(client)
|
|
383
|
+
result = await fixer.fix_pr_by_url( # type: ignore[attr-defined]
|
|
384
|
+
pr_url,
|
|
385
|
+
sync_strategy=sync_strategy,
|
|
386
|
+
conflict_strategy=conflict_strategy,
|
|
387
|
+
dry_run=dry_run,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if result.success:
|
|
391
|
+
if not quiet:
|
|
392
|
+
console.print(f"[green]✅ {result.message}[/green]")
|
|
393
|
+
if result.files_modified:
|
|
394
|
+
for file in result.files_modified:
|
|
395
|
+
console.print(f" - {file}")
|
|
396
|
+
elif not quiet:
|
|
397
|
+
console.print(f"[yellow]⚠️ {result.message}[/yellow]")
|
|
398
|
+
if result.error:
|
|
399
|
+
console.print(f" Error: {result.error}")
|
|
400
|
+
|
|
401
|
+
except Exception as e:
|
|
402
|
+
console.print(f"[red]Error processing PR:[/red] {e}")
|
|
403
|
+
raise typer.Exit(1) from e
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
async def _scan_organization(
|
|
407
|
+
org: str,
|
|
408
|
+
token: str,
|
|
409
|
+
sync_strategy: str = "none",
|
|
410
|
+
conflict_strategy: str = "fail",
|
|
411
|
+
dry_run: bool = False,
|
|
412
|
+
include_drafts: bool = False,
|
|
413
|
+
debug_org: bool = False,
|
|
414
|
+
quiet: bool = False,
|
|
415
|
+
) -> None:
|
|
416
|
+
"""Scan organization for PRs with markdown table issues."""
|
|
417
|
+
# Remove github.com prefix if present
|
|
418
|
+
if "github.com" in org:
|
|
419
|
+
org = org.split("github.com/")[-1].strip("/")
|
|
420
|
+
|
|
421
|
+
if not quiet:
|
|
422
|
+
console.print(f"🔍 Scanning organization: {org}")
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
async with GitHubClient(token) as client: # type: ignore[attr-defined]
|
|
426
|
+
# Create progress tracker for visual feedback
|
|
427
|
+
progress_tracker = (
|
|
428
|
+
None if quiet else ProgressTracker(org, show_pr_stats=True)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
scanner = PRScanner(client, progress_tracker=progress_tracker)
|
|
432
|
+
fixer = PRFixer(client)
|
|
433
|
+
|
|
434
|
+
# Collect PRs from async generator
|
|
435
|
+
prs_to_process = []
|
|
436
|
+
|
|
437
|
+
if progress_tracker:
|
|
438
|
+
progress_tracker.start()
|
|
439
|
+
|
|
440
|
+
# Scanner yields only PRs that are blocked by markdown/lint check failures
|
|
441
|
+
# (it counts repos internally using GraphQL)
|
|
442
|
+
try:
|
|
443
|
+
async for (
|
|
444
|
+
owner,
|
|
445
|
+
repo_name,
|
|
446
|
+
pr_data,
|
|
447
|
+
) in scanner.scan_organization(
|
|
448
|
+
org, include_drafts=include_drafts
|
|
449
|
+
):
|
|
450
|
+
pr_info = PRInfo(
|
|
451
|
+
number=pr_data.get("number", 0),
|
|
452
|
+
title=pr_data.get("title", ""),
|
|
453
|
+
repository=f"{owner}/{repo_name}",
|
|
454
|
+
url=pr_data.get("html_url", ""),
|
|
455
|
+
author=pr_data.get("user", {}).get("login", ""),
|
|
456
|
+
is_draft=pr_data.get("draft", False),
|
|
457
|
+
head_ref=pr_data.get("head", {}).get("ref", ""),
|
|
458
|
+
head_sha=pr_data.get("head", {}).get("sha", ""),
|
|
459
|
+
base_ref=pr_data.get("base", {}).get("ref", ""),
|
|
460
|
+
mergeable=pr_data.get("mergeable_state", ""),
|
|
461
|
+
merge_state_status=pr_data.get(
|
|
462
|
+
"merge_state_status", ""
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
blocked_pr = BlockedPR(
|
|
467
|
+
pr_info=pr_info,
|
|
468
|
+
blocking_reasons=["Failing markdown/lint checks"],
|
|
469
|
+
has_markdown_issues=True, # Will be verified when we try to fix
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
prs_to_process.append(blocked_pr)
|
|
473
|
+
except Exception as scan_error:
|
|
474
|
+
# Stop progress tracker on error
|
|
475
|
+
if progress_tracker:
|
|
476
|
+
progress_tracker.stop()
|
|
477
|
+
console.print(
|
|
478
|
+
f"\n[yellow]⚠️ Scanning interrupted: {scan_error}[/yellow]"
|
|
479
|
+
)
|
|
480
|
+
console.print("[yellow]Processing PRs found so far...[/yellow]")
|
|
481
|
+
|
|
482
|
+
# Stop progress tracker
|
|
483
|
+
if progress_tracker:
|
|
484
|
+
progress_tracker.stop()
|
|
485
|
+
|
|
486
|
+
if not quiet:
|
|
487
|
+
console.print(
|
|
488
|
+
f"\n📊 Found {len(prs_to_process)} blocked PRs with potential markdown issues"
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
if not prs_to_process:
|
|
492
|
+
console.print(
|
|
493
|
+
"\n[green]✅ No blocked PRs with markdown table issues found![/green]"
|
|
494
|
+
)
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
# Display PRs
|
|
498
|
+
if not quiet:
|
|
499
|
+
console.print(
|
|
500
|
+
"\n🔍 Blocked PRs with potential markdown issues:"
|
|
501
|
+
)
|
|
502
|
+
for blocked_pr in prs_to_process:
|
|
503
|
+
console.print(
|
|
504
|
+
f" • {blocked_pr.pr_info.repository}#{blocked_pr.pr_info.number}: {blocked_pr.pr_info.title}"
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Debug mode: limit to first PR only
|
|
508
|
+
if debug_org and prs_to_process:
|
|
509
|
+
if not quiet:
|
|
510
|
+
console.print(
|
|
511
|
+
"\n[yellow]🐛 GitHub Organisation DEBUG mode: only processing first pull request[/yellow]"
|
|
512
|
+
)
|
|
513
|
+
prs_to_process = prs_to_process[:1]
|
|
514
|
+
elif not quiet:
|
|
515
|
+
# Only show this message if NOT in debug mode
|
|
516
|
+
if dry_run:
|
|
517
|
+
console.print(
|
|
518
|
+
f"\n🔍 [DRY RUN] Would fix {len(prs_to_process)} PRs (no changes will be made)..."
|
|
519
|
+
)
|
|
520
|
+
else:
|
|
521
|
+
console.print(f"\n🔧 Fixing {len(prs_to_process)} PRs...")
|
|
522
|
+
|
|
523
|
+
prs_fixed = 0
|
|
524
|
+
prs_with_issues = 0
|
|
525
|
+
fixed_pr_urls = []
|
|
526
|
+
|
|
527
|
+
# Process PRs sequentially to avoid overwhelming the system
|
|
528
|
+
for blocked_pr in prs_to_process:
|
|
529
|
+
try:
|
|
530
|
+
if not quiet:
|
|
531
|
+
console.print(
|
|
532
|
+
f"\n🔄 Processing: {blocked_pr.pr_info.repository}#{blocked_pr.pr_info.number}"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
result = await fixer.fix_pr_by_url(
|
|
536
|
+
blocked_pr.pr_info.url,
|
|
537
|
+
sync_strategy=sync_strategy,
|
|
538
|
+
conflict_strategy=conflict_strategy,
|
|
539
|
+
dry_run=dry_run,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
if result.success:
|
|
543
|
+
if len(result.files_modified) > 0:
|
|
544
|
+
prs_fixed += 1
|
|
545
|
+
fixed_pr_urls.append(blocked_pr.pr_info.url)
|
|
546
|
+
if not quiet:
|
|
547
|
+
console.print(f"[green]✅ {result.message}[/green]")
|
|
548
|
+
elif not quiet:
|
|
549
|
+
console.print(f"[yellow]⚠️ {result.message}[/yellow]")
|
|
550
|
+
|
|
551
|
+
if len(result.files_modified) > 0:
|
|
552
|
+
prs_with_issues += 1
|
|
553
|
+
|
|
554
|
+
except Exception as e:
|
|
555
|
+
if not quiet:
|
|
556
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
|
557
|
+
|
|
558
|
+
if dry_run:
|
|
559
|
+
console.print(
|
|
560
|
+
f"\n[green]✅ [DRY RUN] Found issues in {prs_with_issues} PR(s) (no changes made)[/green]"
|
|
561
|
+
)
|
|
562
|
+
elif prs_fixed > 0:
|
|
563
|
+
console.print(f"\n[green]✅ Fixed {prs_fixed} PR(s):[/green]")
|
|
564
|
+
for pr_url in fixed_pr_urls:
|
|
565
|
+
console.print(f" • {pr_url}")
|
|
566
|
+
else:
|
|
567
|
+
console.print("\n[green]✅ No PRs needed fixing[/green]")
|
|
568
|
+
|
|
569
|
+
except Exception as e:
|
|
570
|
+
console.print(f"[red]Error scanning organization:[/red] {e}")
|
|
571
|
+
if not quiet:
|
|
572
|
+
import traceback
|
|
573
|
+
|
|
574
|
+
console.print("[dim]" + traceback.format_exc() + "[/dim]")
|
|
575
|
+
raise typer.Exit(1) from e
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _process_file(
|
|
579
|
+
file_path: Path, fix: bool = False, max_line_length: int = 80
|
|
580
|
+
) -> FileResult:
|
|
581
|
+
"""Process a single markdown file.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
file_path: Path to the file
|
|
585
|
+
fix: Whether to fix issues
|
|
586
|
+
max_line_length: Maximum line length before adding MD013 disable
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
File processing result
|
|
590
|
+
"""
|
|
591
|
+
result = FileResult(file_path=file_path)
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
# Parse tables from file
|
|
595
|
+
parser = TableParser(file_path)
|
|
596
|
+
tables = parser.parse_file()
|
|
597
|
+
|
|
598
|
+
result.tables_found = len(tables)
|
|
599
|
+
|
|
600
|
+
# Validate each table
|
|
601
|
+
for table in tables:
|
|
602
|
+
validator = TableValidator(table)
|
|
603
|
+
violations = validator.validate()
|
|
604
|
+
result.violations.extend(violations)
|
|
605
|
+
|
|
606
|
+
# Fix if requested
|
|
607
|
+
# Always run fixer if fix=True to add MD013 comments even when no violations
|
|
608
|
+
if fix:
|
|
609
|
+
fixer = FileFixer(file_path, max_line_length=max_line_length)
|
|
610
|
+
fixes = fixer.fix_file(tables)
|
|
611
|
+
result.fixes_applied.extend(fixes)
|
|
612
|
+
|
|
613
|
+
except (FileAccessError, TableParseError) as e:
|
|
614
|
+
result.error = str(e)
|
|
615
|
+
|
|
616
|
+
return result
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _output_json_results(result: ScanResult) -> None:
|
|
620
|
+
"""Output results in JSON format."""
|
|
621
|
+
output = {
|
|
622
|
+
"files_scanned": result.files_scanned,
|
|
623
|
+
"files_with_issues": result.files_with_issues,
|
|
624
|
+
"files_fixed": result.files_fixed,
|
|
625
|
+
"total_violations": result.total_violations,
|
|
626
|
+
"total_fixes": result.total_fixes,
|
|
627
|
+
"files": [
|
|
628
|
+
{
|
|
629
|
+
"path": str(fr.file_path),
|
|
630
|
+
"tables_found": fr.tables_found,
|
|
631
|
+
"violations": len(fr.violations),
|
|
632
|
+
"fixes_applied": len(fr.fixes_applied),
|
|
633
|
+
"error": fr.error,
|
|
634
|
+
}
|
|
635
|
+
for fr in result.file_results
|
|
636
|
+
],
|
|
637
|
+
}
|
|
638
|
+
console.print(json.dumps(output, indent=2))
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _output_text_results(result: ScanResult, quiet: bool) -> None:
|
|
642
|
+
"""Output results in text format."""
|
|
643
|
+
if quiet:
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
if result.files_with_issues == 0:
|
|
647
|
+
console.print("[green]✅ No issues found![/green]")
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
# Summary
|
|
651
|
+
console.print("\n[bold]Summary:[/bold]")
|
|
652
|
+
console.print(f" Files scanned: {result.files_scanned}")
|
|
653
|
+
console.print(f" Files with issues: {result.files_with_issues}")
|
|
654
|
+
if result.files_fixed > 0:
|
|
655
|
+
console.print(f" Files fixed: {result.files_fixed}")
|
|
656
|
+
console.print(f" Total violations: {result.total_violations}")
|
|
657
|
+
if result.total_fixes > 0:
|
|
658
|
+
console.print(f" Total fixes: {result.total_fixes}")
|
|
659
|
+
console.print()
|
|
660
|
+
|
|
661
|
+
# Show errors
|
|
662
|
+
if result.errors:
|
|
663
|
+
console.print("[bold red]Errors:[/bold red]")
|
|
664
|
+
for error in result.errors[:5]:
|
|
665
|
+
console.print(f" {error}")
|
|
666
|
+
if len(result.errors) > 5:
|
|
667
|
+
console.print(f" ... and {len(result.errors) - 5} more errors")
|
|
668
|
+
console.print()
|
|
669
|
+
|
|
670
|
+
# Show sample violations
|
|
671
|
+
if result.files_with_issues > 0:
|
|
672
|
+
console.print("[bold yellow]Files with issues:[/bold yellow]")
|
|
673
|
+
for file_result in result.file_results[:3]:
|
|
674
|
+
if file_result.has_violations:
|
|
675
|
+
console.print(f"{file_result.file_path}")
|
|
676
|
+
|
|
677
|
+
# Group violations by table
|
|
678
|
+
violations_by_table: dict[int, list[TableViolation]] = {}
|
|
679
|
+
for violation in file_result.violations:
|
|
680
|
+
table_line = violation.table_start_line
|
|
681
|
+
if table_line not in violations_by_table:
|
|
682
|
+
violations_by_table[table_line] = []
|
|
683
|
+
violations_by_table[table_line].append(violation)
|
|
684
|
+
|
|
685
|
+
# Show summary per table
|
|
686
|
+
for table_line in sorted(violations_by_table.keys()):
|
|
687
|
+
violations = violations_by_table[table_line]
|
|
688
|
+
# Count unique rows with violations
|
|
689
|
+
unique_rows = len({v.line_number for v in violations})
|
|
690
|
+
console.print(
|
|
691
|
+
f" Markdown table at line {table_line} has {unique_rows} "
|
|
692
|
+
f"row(s) with formatting issues"
|
|
693
|
+
)
|
|
694
|
+
console.print()
|
|
695
|
+
|
|
696
|
+
if result.files_with_issues > 3:
|
|
697
|
+
console.print(
|
|
698
|
+
f"... and {result.files_with_issues - 3} more files with issues"
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
if __name__ == "__main__":
|
|
703
|
+
app()
|