markdown-table-fixer 0.1.0__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.
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/PKG-INFO +29 -1
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/README.md +28 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/pyproject.toml +12 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/_version.py +2 -2
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/cli.py +119 -50
- markdown_table_fixer-0.1.2/src/markdown_table_fixer/git_config.py +303 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/github_client.py +2 -2
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/pr_fixer.py +183 -33
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/.gitignore +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/.pre-commit-hooks.yaml +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/LICENSE +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/LICENSES/Apache-2.0.txt +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/scripts/README.md +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/__init__.py +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/exceptions.py +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/graphql_queries.py +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/models.py +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/pr_scanner.py +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/progress_tracker.py +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/py.typed +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/table_fixer.py +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/table_parser.py +0 -0
- {markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/table_validator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-table-fixer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Markdown table formatter and linter with GitHub integration
|
|
5
5
|
Project-URL: Homepage, https://github.com/lfit/markdown-table-fixer
|
|
6
6
|
Project-URL: Repository, https://github.com/lfit/markdown-table-fixer.git
|
|
@@ -123,6 +123,34 @@ Dry run to preview changes without applying them:
|
|
|
123
123
|
markdown-table-fixer github https://github.com/owner/repo/pull/123 --dry-run
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
#### Update Methods
|
|
127
|
+
|
|
128
|
+
The tool supports two methods for applying fixes:
|
|
129
|
+
|
|
130
|
+
**API Method (default)** - Uses GitHub API to create new commits:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
markdown-table-fixer github https://github.com/owner/repo/pull/123
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- Creates new commits via GitHub API
|
|
137
|
+
- Shows as "Verified" by GitHub
|
|
138
|
+
- No Git operations required
|
|
139
|
+
- Faster and simpler
|
|
140
|
+
- Does not support sync strategies (rebase/merge)
|
|
141
|
+
|
|
142
|
+
**Git Method** - Clones repo, amends commit, force-pushes:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
markdown-table-fixer github https://github.com/owner/repo/pull/123 \
|
|
146
|
+
--update-method git
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- Respects your local Git signing configuration
|
|
150
|
+
- Amends the existing commit (preserves commit history)
|
|
151
|
+
- Requires Git operations (clone, amend, push)
|
|
152
|
+
- Use when you need to amend commits or use sync strategies
|
|
153
|
+
|
|
126
154
|
#### Fix PRs Across an Organization
|
|
127
155
|
|
|
128
156
|
Scan and fix tables in PRs across an entire GitHub organization:
|
|
@@ -82,6 +82,34 @@ Dry run to preview changes without applying them:
|
|
|
82
82
|
markdown-table-fixer github https://github.com/owner/repo/pull/123 --dry-run
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
#### Update Methods
|
|
86
|
+
|
|
87
|
+
The tool supports two methods for applying fixes:
|
|
88
|
+
|
|
89
|
+
**API Method (default)** - Uses GitHub API to create new commits:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
markdown-table-fixer github https://github.com/owner/repo/pull/123
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- Creates new commits via GitHub API
|
|
96
|
+
- Shows as "Verified" by GitHub
|
|
97
|
+
- No Git operations required
|
|
98
|
+
- Faster and simpler
|
|
99
|
+
- Does not support sync strategies (rebase/merge)
|
|
100
|
+
|
|
101
|
+
**Git Method** - Clones repo, amends commit, force-pushes:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
markdown-table-fixer github https://github.com/owner/repo/pull/123 \
|
|
105
|
+
--update-method git
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- Respects your local Git signing configuration
|
|
109
|
+
- Amends the existing commit (preserves commit history)
|
|
110
|
+
- Requires Git operations (clone, amend, push)
|
|
111
|
+
- Use when you need to amend commits or use sync strategies
|
|
112
|
+
|
|
85
113
|
#### Fix PRs Across an Organization
|
|
86
114
|
|
|
87
115
|
Scan and fix tables in PRs across an entire GitHub organization:
|
|
@@ -136,6 +136,18 @@ module = ["tests.*"]
|
|
|
136
136
|
disallow_untyped_decorators = false
|
|
137
137
|
disallow_untyped_defs = false
|
|
138
138
|
|
|
139
|
+
[[tool.mypy.overrides]]
|
|
140
|
+
module = ["typer.*"]
|
|
141
|
+
disallow_untyped_decorators = false
|
|
142
|
+
|
|
143
|
+
[[tool.mypy.overrides]]
|
|
144
|
+
module = ["tenacity.*"]
|
|
145
|
+
disallow_untyped_decorators = false
|
|
146
|
+
|
|
147
|
+
[[tool.mypy.overrides]]
|
|
148
|
+
module = ["wcwidth"]
|
|
149
|
+
ignore_missing_imports = true
|
|
150
|
+
|
|
139
151
|
[tool.pytest.ini_options]
|
|
140
152
|
testpaths = ["tests"]
|
|
141
153
|
python_files = ["test_*.py", "*_test.py"]
|
{markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/_version.py
RENAMED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 2)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -9,20 +9,17 @@ import asyncio
|
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import TYPE_CHECKING
|
|
13
12
|
|
|
14
13
|
from rich.console import Console
|
|
15
14
|
from rich.logging import RichHandler
|
|
16
15
|
import typer
|
|
17
16
|
|
|
18
|
-
if TYPE_CHECKING:
|
|
19
|
-
from collections.abc import Callable
|
|
20
|
-
|
|
21
17
|
from ._version import __version__
|
|
22
18
|
from .exceptions import (
|
|
23
19
|
FileAccessError,
|
|
24
20
|
TableParseError,
|
|
25
21
|
)
|
|
22
|
+
from .git_config import GitConfigMode
|
|
26
23
|
from .github_client import GitHubClient
|
|
27
24
|
from .models import (
|
|
28
25
|
BlockedPR,
|
|
@@ -42,10 +39,15 @@ from .table_validator import TableValidator
|
|
|
42
39
|
console = Console()
|
|
43
40
|
|
|
44
41
|
|
|
42
|
+
def get_version_string() -> str:
|
|
43
|
+
"""Get the formatted version string."""
|
|
44
|
+
return f"🏷️ markdown-table-fixer version {__version__}"
|
|
45
|
+
|
|
46
|
+
|
|
45
47
|
def version_callback(value: bool) -> None:
|
|
46
48
|
"""Show version and exit."""
|
|
47
49
|
if value:
|
|
48
|
-
console.print(
|
|
50
|
+
console.print(get_version_string())
|
|
49
51
|
console.print()
|
|
50
52
|
raise typer.Exit()
|
|
51
53
|
|
|
@@ -71,28 +73,30 @@ def setup_logging(
|
|
|
71
73
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
72
74
|
|
|
73
75
|
|
|
74
|
-
# Create Typer app with
|
|
75
|
-
|
|
76
|
+
# Create Typer app with custom help formatter
|
|
77
|
+
class CustomTyper(typer.Typer):
|
|
78
|
+
"""Custom Typer class to add version to help output."""
|
|
79
|
+
|
|
80
|
+
def __call__(self, *args: object, **kwargs: object) -> object:
|
|
81
|
+
"""Override to inject version string in help output."""
|
|
82
|
+
# Check if help is being requested (for any command or subcommand)
|
|
83
|
+
import sys
|
|
84
|
+
|
|
85
|
+
if "--help" in sys.argv or "-h" in sys.argv:
|
|
86
|
+
console.print(get_version_string())
|
|
87
|
+
return super().__call__(*args, **kwargs)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Create Typer app
|
|
91
|
+
app = CustomTyper(
|
|
76
92
|
name="markdown-table-fixer",
|
|
77
|
-
help=
|
|
93
|
+
help="Fix markdown table formatting issues",
|
|
78
94
|
add_completion=False,
|
|
79
95
|
rich_markup_mode="rich",
|
|
80
96
|
)
|
|
81
97
|
|
|
82
98
|
|
|
83
|
-
|
|
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]
|
|
99
|
+
@app.callback()
|
|
96
100
|
def main(
|
|
97
101
|
version: bool = typer.Option(
|
|
98
102
|
False,
|
|
@@ -106,8 +110,7 @@ def main(
|
|
|
106
110
|
pass
|
|
107
111
|
|
|
108
112
|
|
|
109
|
-
@app.command(
|
|
110
|
-
@_add_version_to_docstring # type: ignore[misc]
|
|
113
|
+
@app.command(help="Scan and optionally fix markdown table formatting issues")
|
|
111
114
|
def lint(
|
|
112
115
|
path: Path = typer.Argument(
|
|
113
116
|
Path("."),
|
|
@@ -131,13 +134,6 @@ def lint(
|
|
|
131
134
|
"--check",
|
|
132
135
|
help="Exit with error if issues found (CI mode)",
|
|
133
136
|
),
|
|
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
137
|
max_line_length: int = typer.Option(
|
|
142
138
|
80,
|
|
143
139
|
"--max-line-length",
|
|
@@ -215,8 +211,7 @@ def lint(
|
|
|
215
211
|
raise typer.Exit(1) from e
|
|
216
212
|
|
|
217
213
|
|
|
218
|
-
@app.command(
|
|
219
|
-
@_add_version_to_docstring # type: ignore[misc]
|
|
214
|
+
@app.command(help="Fix markdown tables in GitHub pull requests")
|
|
220
215
|
def github(
|
|
221
216
|
target: str = typer.Argument(
|
|
222
217
|
...,
|
|
@@ -241,6 +236,22 @@ def github(
|
|
|
241
236
|
help="How to resolve conflicts: 'fail', 'ours' (keep PR changes), or 'theirs' (keep base changes)",
|
|
242
237
|
case_sensitive=False,
|
|
243
238
|
),
|
|
239
|
+
update_method: str = typer.Option(
|
|
240
|
+
"api",
|
|
241
|
+
"--update-method",
|
|
242
|
+
help="Method to apply fixes: 'git' (clone, amend, push) or 'api' (GitHub API commits, default)",
|
|
243
|
+
case_sensitive=False,
|
|
244
|
+
),
|
|
245
|
+
no_user_signing: bool = typer.Option(
|
|
246
|
+
False,
|
|
247
|
+
"--no-user-signing",
|
|
248
|
+
help="Use user identity but disable commit signing (only applies to 'git' method)",
|
|
249
|
+
),
|
|
250
|
+
bot_identity: bool = typer.Option(
|
|
251
|
+
False,
|
|
252
|
+
"--bot-identity",
|
|
253
|
+
help="Use bot identity without signing (only applies to 'git' method)",
|
|
254
|
+
),
|
|
244
255
|
dry_run: bool = typer.Option(
|
|
245
256
|
False,
|
|
246
257
|
"--dry-run",
|
|
@@ -281,13 +292,6 @@ def github(
|
|
|
281
292
|
"--log-level",
|
|
282
293
|
help="Set logging level",
|
|
283
294
|
),
|
|
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
295
|
) -> None:
|
|
292
296
|
"""Fix markdown tables in GitHub PRs.
|
|
293
297
|
|
|
@@ -295,7 +299,16 @@ def github(
|
|
|
295
299
|
- An entire organization (scans all PRs for table issues)
|
|
296
300
|
- A specific PR by URL
|
|
297
301
|
|
|
298
|
-
|
|
302
|
+
Update Methods:
|
|
303
|
+
- 'api' (default): Use GitHub API to create new commits (shows as verified by GitHub)
|
|
304
|
+
- 'git': Clone repo, amend commit, force-push (respects signing)
|
|
305
|
+
|
|
306
|
+
Git Identity & Signing (only applies to 'git' update method):
|
|
307
|
+
- By default, uses your git user.name, user.email, and commit signing settings
|
|
308
|
+
- --no-user-signing: Use your identity but disable commit signing
|
|
309
|
+
- --bot-identity: Use bot identity without signing
|
|
310
|
+
|
|
311
|
+
Sync strategies (only applies to 'git' update method):
|
|
299
312
|
- 'none': Fix tables on PR branch as-is (may have conflicts)
|
|
300
313
|
- 'rebase': Rebase PR onto base branch before fixing (cleaner history)
|
|
301
314
|
- 'merge': Merge base branch into PR before fixing (safer)
|
|
@@ -313,16 +326,41 @@ def github(
|
|
|
313
326
|
"""
|
|
314
327
|
setup_logging(log_level=log_level, quiet=quiet, verbose=verbose)
|
|
315
328
|
|
|
316
|
-
#
|
|
317
|
-
|
|
329
|
+
# Normalize and validate update method
|
|
330
|
+
update_method = update_method.lower()
|
|
331
|
+
if update_method not in ["git", "api"]:
|
|
332
|
+
console.print(
|
|
333
|
+
f"[red]Error:[/red] Invalid update method '{update_method}'. "
|
|
334
|
+
"Use 'git' or 'api'"
|
|
335
|
+
)
|
|
336
|
+
raise typer.Exit(1)
|
|
337
|
+
|
|
338
|
+
# Determine git config mode from CLI flags (only relevant for git method)
|
|
339
|
+
if bot_identity and no_user_signing:
|
|
340
|
+
console.print(
|
|
341
|
+
"[red]Error:[/red] Cannot use both --bot-identity and --no-user-signing"
|
|
342
|
+
)
|
|
343
|
+
raise typer.Exit(1)
|
|
344
|
+
|
|
345
|
+
if bot_identity:
|
|
346
|
+
git_config_mode = GitConfigMode.BOT_IDENTITY
|
|
347
|
+
elif no_user_signing:
|
|
348
|
+
git_config_mode = GitConfigMode.USER_NO_SIGN
|
|
349
|
+
else:
|
|
350
|
+
git_config_mode = GitConfigMode.USER_INHERIT
|
|
351
|
+
|
|
352
|
+
# Normalize and validate sync strategy
|
|
353
|
+
sync_strategy = sync_strategy.lower()
|
|
354
|
+
if sync_strategy not in ["none", "rebase", "merge"]:
|
|
318
355
|
console.print(
|
|
319
356
|
f"[red]Error:[/red] Invalid sync strategy '{sync_strategy}'. "
|
|
320
357
|
"Use 'none', 'rebase', or 'merge'"
|
|
321
358
|
)
|
|
322
359
|
raise typer.Exit(1)
|
|
323
360
|
|
|
324
|
-
#
|
|
325
|
-
|
|
361
|
+
# Normalize and validate conflict strategy
|
|
362
|
+
conflict_strategy = conflict_strategy.lower()
|
|
363
|
+
if conflict_strategy not in ["fail", "ours", "theirs"]:
|
|
326
364
|
console.print(
|
|
327
365
|
f"[red]Error:[/red] Invalid conflict strategy '{conflict_strategy}'. "
|
|
328
366
|
"Use 'fail', 'ours', or 'theirs'"
|
|
@@ -343,10 +381,12 @@ def github(
|
|
|
343
381
|
_fix_single_pr(
|
|
344
382
|
target,
|
|
345
383
|
token,
|
|
346
|
-
sync_strategy=sync_strategy
|
|
347
|
-
conflict_strategy=conflict_strategy
|
|
384
|
+
sync_strategy=sync_strategy,
|
|
385
|
+
conflict_strategy=conflict_strategy,
|
|
348
386
|
dry_run=dry_run,
|
|
349
387
|
quiet=quiet,
|
|
388
|
+
git_config_mode=git_config_mode,
|
|
389
|
+
update_method=update_method,
|
|
350
390
|
)
|
|
351
391
|
)
|
|
352
392
|
else:
|
|
@@ -355,12 +395,14 @@ def github(
|
|
|
355
395
|
_scan_organization(
|
|
356
396
|
target,
|
|
357
397
|
token,
|
|
358
|
-
sync_strategy=sync_strategy
|
|
359
|
-
conflict_strategy=conflict_strategy
|
|
398
|
+
sync_strategy=sync_strategy,
|
|
399
|
+
conflict_strategy=conflict_strategy,
|
|
360
400
|
dry_run=dry_run,
|
|
361
401
|
include_drafts=include_drafts,
|
|
362
402
|
debug_org=debug_org,
|
|
363
403
|
quiet=quiet,
|
|
404
|
+
git_config_mode=git_config_mode,
|
|
405
|
+
update_method=update_method,
|
|
364
406
|
)
|
|
365
407
|
)
|
|
366
408
|
|
|
@@ -372,6 +414,8 @@ async def _fix_single_pr(
|
|
|
372
414
|
conflict_strategy: str = "fail",
|
|
373
415
|
dry_run: bool = False,
|
|
374
416
|
quiet: bool = False,
|
|
417
|
+
git_config_mode: str = GitConfigMode.USER_INHERIT,
|
|
418
|
+
update_method: str = "api",
|
|
375
419
|
) -> None:
|
|
376
420
|
"""Fix markdown tables in a single PR."""
|
|
377
421
|
if not quiet:
|
|
@@ -379,12 +423,13 @@ async def _fix_single_pr(
|
|
|
379
423
|
|
|
380
424
|
try:
|
|
381
425
|
async with GitHubClient(token) as client: # type: ignore[attr-defined]
|
|
382
|
-
fixer = PRFixer(client)
|
|
426
|
+
fixer = PRFixer(client, git_config_mode=git_config_mode)
|
|
383
427
|
result = await fixer.fix_pr_by_url( # type: ignore[attr-defined]
|
|
384
428
|
pr_url,
|
|
385
429
|
sync_strategy=sync_strategy,
|
|
386
430
|
conflict_strategy=conflict_strategy,
|
|
387
431
|
dry_run=dry_run,
|
|
432
|
+
update_method=update_method,
|
|
388
433
|
)
|
|
389
434
|
|
|
390
435
|
if result.success:
|
|
@@ -412,6 +457,8 @@ async def _scan_organization(
|
|
|
412
457
|
include_drafts: bool = False,
|
|
413
458
|
debug_org: bool = False,
|
|
414
459
|
quiet: bool = False,
|
|
460
|
+
git_config_mode: str = GitConfigMode.USER_INHERIT,
|
|
461
|
+
update_method: str = "api",
|
|
415
462
|
) -> None:
|
|
416
463
|
"""Scan organization for PRs with markdown table issues."""
|
|
417
464
|
# Remove github.com prefix if present
|
|
@@ -429,7 +476,28 @@ async def _scan_organization(
|
|
|
429
476
|
)
|
|
430
477
|
|
|
431
478
|
scanner = PRScanner(client, progress_tracker=progress_tracker)
|
|
432
|
-
fixer = PRFixer(client)
|
|
479
|
+
fixer = PRFixer(client, git_config_mode=git_config_mode)
|
|
480
|
+
|
|
481
|
+
# Inform about update method
|
|
482
|
+
if not quiet:
|
|
483
|
+
method_desc = (
|
|
484
|
+
"Git clone/amend/push"
|
|
485
|
+
if update_method.lower() == "git"
|
|
486
|
+
else "GitHub API commits"
|
|
487
|
+
)
|
|
488
|
+
console.print(f"Update method: {method_desc}")
|
|
489
|
+
if update_method.lower() == "git":
|
|
490
|
+
if git_config_mode == GitConfigMode.BOT_IDENTITY:
|
|
491
|
+
console.print(
|
|
492
|
+
"Git identity: Bot (markdown-table-fixer)"
|
|
493
|
+
)
|
|
494
|
+
elif git_config_mode == GitConfigMode.USER_NO_SIGN:
|
|
495
|
+
console.print("Git identity: User (signing disabled)")
|
|
496
|
+
else:
|
|
497
|
+
console.print(
|
|
498
|
+
"Git identity: User (inheriting signing config)"
|
|
499
|
+
)
|
|
500
|
+
console.print()
|
|
433
501
|
|
|
434
502
|
# Collect PRs from async generator
|
|
435
503
|
prs_to_process = []
|
|
@@ -537,6 +605,7 @@ async def _scan_organization(
|
|
|
537
605
|
sync_strategy=sync_strategy,
|
|
538
606
|
conflict_strategy=conflict_strategy,
|
|
539
607
|
dry_run=dry_run,
|
|
608
|
+
update_method=update_method,
|
|
540
609
|
)
|
|
541
610
|
|
|
542
611
|
if result.success:
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
|
3
|
+
|
|
4
|
+
"""Git configuration management for commit signing and user identity."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path # noqa: TC003
|
|
11
|
+
import subprocess
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GitConfigMode(str, Enum):
|
|
17
|
+
"""Git configuration modes."""
|
|
18
|
+
|
|
19
|
+
USER_INHERIT = (
|
|
20
|
+
"user_inherit" # Inherit user's full config including signing
|
|
21
|
+
)
|
|
22
|
+
USER_NO_SIGN = "user_no_sign" # Use user identity but disable signing
|
|
23
|
+
BOT_IDENTITY = "bot_identity" # Use bot identity without signing
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_global_git_config(key: str) -> str | None:
|
|
27
|
+
"""Get a value from global git config.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
key: Git config key (e.g., "user.name")
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Config value or None if not set
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["git", "config", "--global", key],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
check=False,
|
|
41
|
+
)
|
|
42
|
+
if result.returncode == 0:
|
|
43
|
+
value = result.stdout.strip()
|
|
44
|
+
return value if value else None
|
|
45
|
+
return None
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.debug(f"Could not get git config {key}: {e}")
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _set_repo_git_config(repo_dir: Path, key: str, value: str) -> bool:
|
|
52
|
+
"""Set a git config value in the repository.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
repo_dir: Repository directory path
|
|
56
|
+
key: Git config key
|
|
57
|
+
value: Value to set
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
True if successful, False otherwise
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
subprocess.run(
|
|
64
|
+
["git", "config", key, value],
|
|
65
|
+
cwd=repo_dir,
|
|
66
|
+
check=True,
|
|
67
|
+
capture_output=True,
|
|
68
|
+
)
|
|
69
|
+
return True
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.debug(f"Could not set git config {key}={value}: {e}")
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def configure_git_identity(
|
|
76
|
+
repo_dir: Path,
|
|
77
|
+
mode: str = GitConfigMode.USER_INHERIT,
|
|
78
|
+
*,
|
|
79
|
+
bot_name: str = "markdown-table-fixer",
|
|
80
|
+
bot_email: str = "noreply@linuxfoundation.org",
|
|
81
|
+
) -> dict[str, str]:
|
|
82
|
+
"""Configure git identity and signing settings in a repository.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
repo_dir: Repository directory path
|
|
86
|
+
mode: Configuration mode (USER_INHERIT, USER_NO_SIGN, or BOT_IDENTITY)
|
|
87
|
+
bot_name: Bot name for BOT_IDENTITY mode
|
|
88
|
+
bot_email: Bot email for BOT_IDENTITY mode
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dict with applied configuration details
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If mode is invalid
|
|
95
|
+
"""
|
|
96
|
+
valid_modes = [m.value for m in GitConfigMode]
|
|
97
|
+
if mode not in valid_modes:
|
|
98
|
+
msg = f"Invalid mode: {mode}. Must be one of {valid_modes}"
|
|
99
|
+
raise ValueError(msg)
|
|
100
|
+
|
|
101
|
+
applied_config: dict[str, str] = {}
|
|
102
|
+
|
|
103
|
+
if mode == GitConfigMode.BOT_IDENTITY:
|
|
104
|
+
# Use bot identity without signing
|
|
105
|
+
logger.debug("Configuring git with bot identity (no signing)")
|
|
106
|
+
if _set_repo_git_config(repo_dir, "user.name", bot_name):
|
|
107
|
+
applied_config["user.name"] = bot_name
|
|
108
|
+
else:
|
|
109
|
+
logger.warning(f"Failed to set user.name to {bot_name}")
|
|
110
|
+
|
|
111
|
+
if _set_repo_git_config(repo_dir, "user.email", bot_email):
|
|
112
|
+
applied_config["user.email"] = bot_email
|
|
113
|
+
else:
|
|
114
|
+
logger.warning(f"Failed to set user.email to {bot_email}")
|
|
115
|
+
|
|
116
|
+
applied_config["mode"] = "bot_identity"
|
|
117
|
+
return applied_config
|
|
118
|
+
|
|
119
|
+
# For USER_INHERIT and USER_NO_SIGN modes, try to get user's config
|
|
120
|
+
user_name = _get_global_git_config("user.name")
|
|
121
|
+
user_email = _get_global_git_config("user.email")
|
|
122
|
+
|
|
123
|
+
if not user_name or not user_email:
|
|
124
|
+
logger.warning(
|
|
125
|
+
"User git config not found, falling back to bot identity"
|
|
126
|
+
)
|
|
127
|
+
if _set_repo_git_config(repo_dir, "user.name", bot_name):
|
|
128
|
+
applied_config["user.name"] = bot_name
|
|
129
|
+
else:
|
|
130
|
+
logger.warning(f"Failed to set user.name to {bot_name}")
|
|
131
|
+
|
|
132
|
+
if _set_repo_git_config(repo_dir, "user.email", bot_email):
|
|
133
|
+
applied_config["user.email"] = bot_email
|
|
134
|
+
else:
|
|
135
|
+
logger.warning(f"Failed to set user.email to {bot_email}")
|
|
136
|
+
|
|
137
|
+
applied_config["mode"] = "bot_identity_fallback"
|
|
138
|
+
return applied_config
|
|
139
|
+
|
|
140
|
+
# Set user identity
|
|
141
|
+
if _set_repo_git_config(repo_dir, "user.name", user_name):
|
|
142
|
+
applied_config["user.name"] = user_name
|
|
143
|
+
else:
|
|
144
|
+
logger.warning(f"Failed to set user.name to {user_name}")
|
|
145
|
+
|
|
146
|
+
if _set_repo_git_config(repo_dir, "user.email", user_email):
|
|
147
|
+
applied_config["user.email"] = user_email
|
|
148
|
+
else:
|
|
149
|
+
logger.warning(f"Failed to set user.email to {user_email}")
|
|
150
|
+
|
|
151
|
+
if mode == GitConfigMode.USER_NO_SIGN:
|
|
152
|
+
# Explicitly disable signing
|
|
153
|
+
logger.debug("Configuring git with user identity (signing disabled)")
|
|
154
|
+
if _set_repo_git_config(repo_dir, "commit.gpgsign", "false"):
|
|
155
|
+
applied_config["commit.gpgsign"] = "false"
|
|
156
|
+
else:
|
|
157
|
+
logger.warning("Failed to set commit.gpgsign to false")
|
|
158
|
+
|
|
159
|
+
applied_config["mode"] = "user_no_sign"
|
|
160
|
+
return applied_config
|
|
161
|
+
|
|
162
|
+
# USER_INHERIT mode: copy signing configuration
|
|
163
|
+
logger.debug("Configuring git with user identity (inheriting signing)")
|
|
164
|
+
|
|
165
|
+
# Check if user has signing enabled
|
|
166
|
+
gpgsign = _get_global_git_config("commit.gpgsign")
|
|
167
|
+
if gpgsign == "true":
|
|
168
|
+
if _set_repo_git_config(repo_dir, "commit.gpgsign", "true"):
|
|
169
|
+
applied_config["commit.gpgsign"] = "true"
|
|
170
|
+
else:
|
|
171
|
+
logger.warning("Failed to set commit.gpgsign to true")
|
|
172
|
+
|
|
173
|
+
# Get GPG format (ssh, openpgp, x509) - default to openpgp if not set
|
|
174
|
+
gpg_format = _get_global_git_config("gpg.format") or "openpgp"
|
|
175
|
+
if _set_repo_git_config(repo_dir, "gpg.format", gpg_format):
|
|
176
|
+
applied_config["gpg.format"] = gpg_format
|
|
177
|
+
else:
|
|
178
|
+
logger.warning(f"Failed to set gpg.format to {gpg_format}")
|
|
179
|
+
|
|
180
|
+
# Handle SSH-specific configuration
|
|
181
|
+
if gpg_format == "ssh":
|
|
182
|
+
# Copy SSH signing key
|
|
183
|
+
ssh_key = _get_global_git_config("user.signingkey")
|
|
184
|
+
if ssh_key:
|
|
185
|
+
if _set_repo_git_config(repo_dir, "user.signingkey", ssh_key):
|
|
186
|
+
applied_config["user.signingkey"] = ssh_key
|
|
187
|
+
else:
|
|
188
|
+
logger.warning(
|
|
189
|
+
f"Failed to set user.signingkey to {ssh_key}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Copy allowed signers file
|
|
193
|
+
allowed_signers = _get_global_git_config(
|
|
194
|
+
"gpg.ssh.allowedSignersFile"
|
|
195
|
+
)
|
|
196
|
+
if allowed_signers:
|
|
197
|
+
if _set_repo_git_config(
|
|
198
|
+
repo_dir, "gpg.ssh.allowedSignersFile", allowed_signers
|
|
199
|
+
):
|
|
200
|
+
applied_config["gpg.ssh.allowedSignersFile"] = (
|
|
201
|
+
allowed_signers
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
logger.warning(
|
|
205
|
+
f"Failed to set gpg.ssh.allowedSignersFile to {allowed_signers}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Copy default key ID if set
|
|
209
|
+
default_key = _get_global_git_config("gpg.ssh.defaultKeyCommand")
|
|
210
|
+
if default_key:
|
|
211
|
+
if _set_repo_git_config(
|
|
212
|
+
repo_dir, "gpg.ssh.defaultKeyCommand", default_key
|
|
213
|
+
):
|
|
214
|
+
applied_config["gpg.ssh.defaultKeyCommand"] = default_key
|
|
215
|
+
else:
|
|
216
|
+
logger.warning(
|
|
217
|
+
f"Failed to set gpg.ssh.defaultKeyCommand to {default_key}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Handle GPG-specific configuration (openpgp, x509)
|
|
221
|
+
elif gpg_format in ["openpgp", "x509"]:
|
|
222
|
+
# Copy GPG signing key
|
|
223
|
+
signing_key = _get_global_git_config("user.signingkey")
|
|
224
|
+
if signing_key:
|
|
225
|
+
if _set_repo_git_config(
|
|
226
|
+
repo_dir, "user.signingkey", signing_key
|
|
227
|
+
):
|
|
228
|
+
applied_config["user.signingkey"] = signing_key
|
|
229
|
+
else:
|
|
230
|
+
logger.warning(
|
|
231
|
+
f"Failed to set user.signingkey to {signing_key}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Copy GPG program if specified
|
|
235
|
+
gpg_program = _get_global_git_config("gpg.program")
|
|
236
|
+
if gpg_program:
|
|
237
|
+
if _set_repo_git_config(repo_dir, "gpg.program", gpg_program):
|
|
238
|
+
applied_config["gpg.program"] = gpg_program
|
|
239
|
+
else:
|
|
240
|
+
logger.warning(
|
|
241
|
+
f"Failed to set gpg.program to {gpg_program}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
logger.info(
|
|
245
|
+
f"Git signing enabled: format={gpg_format}, "
|
|
246
|
+
f"key={'configured' if applied_config.get('user.signingkey') else 'default'}"
|
|
247
|
+
)
|
|
248
|
+
else:
|
|
249
|
+
logger.debug("User does not have commit signing enabled")
|
|
250
|
+
|
|
251
|
+
applied_config["mode"] = "user_inherit"
|
|
252
|
+
return applied_config
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_signing_info(repo_dir: Path) -> dict[str, str | bool]:
|
|
256
|
+
"""Get current signing configuration from a repository.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
repo_dir: Repository directory path
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Dict with signing configuration details
|
|
263
|
+
"""
|
|
264
|
+
info: dict[str, str | bool] = {}
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
result = subprocess.run(
|
|
268
|
+
["git", "config", "commit.gpgsign"],
|
|
269
|
+
cwd=repo_dir,
|
|
270
|
+
capture_output=True,
|
|
271
|
+
text=True,
|
|
272
|
+
check=False,
|
|
273
|
+
)
|
|
274
|
+
info["signing_enabled"] = result.stdout.strip() == "true"
|
|
275
|
+
|
|
276
|
+
if info["signing_enabled"]:
|
|
277
|
+
# Get format
|
|
278
|
+
result = subprocess.run(
|
|
279
|
+
["git", "config", "gpg.format"],
|
|
280
|
+
cwd=repo_dir,
|
|
281
|
+
capture_output=True,
|
|
282
|
+
text=True,
|
|
283
|
+
check=False,
|
|
284
|
+
)
|
|
285
|
+
gpg_format = result.stdout.strip()
|
|
286
|
+
info["format"] = gpg_format if gpg_format else "openpgp"
|
|
287
|
+
|
|
288
|
+
# Get signing key
|
|
289
|
+
result = subprocess.run(
|
|
290
|
+
["git", "config", "user.signingkey"],
|
|
291
|
+
cwd=repo_dir,
|
|
292
|
+
capture_output=True,
|
|
293
|
+
text=True,
|
|
294
|
+
check=False,
|
|
295
|
+
)
|
|
296
|
+
signing_key = result.stdout.strip()
|
|
297
|
+
if signing_key:
|
|
298
|
+
info["signing_key"] = signing_key
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
logger.debug(f"Could not get signing info: {e}")
|
|
302
|
+
|
|
303
|
+
return info
|
{markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/github_client.py
RENAMED
|
@@ -40,7 +40,7 @@ class GitHubClient:
|
|
|
40
40
|
"""Async context manager exit."""
|
|
41
41
|
pass
|
|
42
42
|
|
|
43
|
-
@retry(
|
|
43
|
+
@retry(
|
|
44
44
|
stop=stop_after_attempt(3),
|
|
45
45
|
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
46
46
|
)
|
|
@@ -83,7 +83,7 @@ class GitHubClient:
|
|
|
83
83
|
msg = f"Request failed: {e}"
|
|
84
84
|
raise FileAccessError(msg) from e
|
|
85
85
|
|
|
86
|
-
@retry(
|
|
86
|
+
@retry(
|
|
87
87
|
stop=stop_after_attempt(3),
|
|
88
88
|
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
89
89
|
)
|
{markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/pr_fixer.py
RENAMED
|
@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from .github_client import GitHubClient
|
|
18
18
|
|
|
19
|
+
from .git_config import GitConfigMode, configure_git_identity
|
|
19
20
|
from .models import GitHubFixResult, PRInfo
|
|
20
21
|
from .table_fixer import FileFixer, TableFixer
|
|
21
22
|
from .table_parser import MarkdownFileScanner, TableParser
|
|
@@ -25,13 +26,19 @@ from .table_validator import TableValidator
|
|
|
25
26
|
class PRFixer:
|
|
26
27
|
"""Fix markdown tables in pull requests."""
|
|
27
28
|
|
|
28
|
-
def __init__(
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
client: GitHubClient,
|
|
32
|
+
git_config_mode: str = GitConfigMode.USER_INHERIT,
|
|
33
|
+
):
|
|
29
34
|
"""Initialize PR fixer.
|
|
30
35
|
|
|
31
36
|
Args:
|
|
32
37
|
client: GitHub API client
|
|
38
|
+
git_config_mode: Git configuration mode (USER_INHERIT, USER_NO_SIGN, or BOT_IDENTITY)
|
|
33
39
|
"""
|
|
34
40
|
self.client = client
|
|
41
|
+
self.git_config_mode = git_config_mode
|
|
35
42
|
self.logger = logging.getLogger("markdown_table_fixer.pr_fixer")
|
|
36
43
|
|
|
37
44
|
def _sanitize_message(self, message: str) -> str:
|
|
@@ -58,25 +65,114 @@ class PRFixer:
|
|
|
58
65
|
|
|
59
66
|
return sanitized
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
def _create_error_pr_info(self, pr_url: str) -> PRInfo:
|
|
69
|
+
"""Create a dummy PRInfo for error responses.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
pr_url: PR URL for error messages
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
PRInfo with dummy values for error cases
|
|
76
|
+
"""
|
|
77
|
+
return PRInfo(
|
|
78
|
+
number=0,
|
|
79
|
+
title="",
|
|
80
|
+
repository="",
|
|
81
|
+
url=pr_url,
|
|
82
|
+
author="",
|
|
83
|
+
is_draft=False,
|
|
84
|
+
head_ref="",
|
|
85
|
+
head_sha="",
|
|
86
|
+
base_ref="",
|
|
87
|
+
mergeable="",
|
|
88
|
+
merge_state_status="",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _validate_parameters(
|
|
92
|
+
self,
|
|
93
|
+
pr_url: str,
|
|
94
|
+
update_method: str,
|
|
95
|
+
sync_strategy: str,
|
|
96
|
+
conflict_strategy: str,
|
|
97
|
+
) -> GitHubFixResult | None:
|
|
98
|
+
"""Validate input parameters.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
pr_url: PR URL for error messages
|
|
102
|
+
update_method: Update method (already normalized)
|
|
103
|
+
sync_strategy: Sync strategy (already normalized)
|
|
104
|
+
conflict_strategy: Conflict strategy (already normalized)
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
GitHubFixResult with error if validation fails, None if valid
|
|
108
|
+
"""
|
|
109
|
+
# Validate update_method
|
|
110
|
+
if update_method not in ["git", "api"]:
|
|
111
|
+
return GitHubFixResult(
|
|
112
|
+
pr_info=self._create_error_pr_info(pr_url),
|
|
113
|
+
success=False,
|
|
114
|
+
message=f"Invalid update_method '{update_method}'. Use 'git' or 'api'",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Validate sync_strategy (only relevant for git method)
|
|
118
|
+
if update_method == "git" and sync_strategy not in [
|
|
119
|
+
"none",
|
|
120
|
+
"rebase",
|
|
121
|
+
"merge",
|
|
122
|
+
]:
|
|
123
|
+
return GitHubFixResult(
|
|
124
|
+
pr_info=self._create_error_pr_info(pr_url),
|
|
125
|
+
success=False,
|
|
126
|
+
message=f"Invalid sync_strategy '{sync_strategy}'. Use 'none', 'rebase', or 'merge'",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Validate conflict_strategy (only relevant for git method)
|
|
130
|
+
if update_method == "git" and conflict_strategy not in [
|
|
131
|
+
"fail",
|
|
132
|
+
"ours",
|
|
133
|
+
"theirs",
|
|
134
|
+
]:
|
|
135
|
+
return GitHubFixResult(
|
|
136
|
+
pr_info=self._create_error_pr_info(pr_url),
|
|
137
|
+
success=False,
|
|
138
|
+
message=f"Invalid conflict_strategy '{conflict_strategy}'. Use 'fail', 'ours', or 'theirs'",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
async def fix_pr_by_url( # noqa: PLR0911
|
|
62
144
|
self,
|
|
63
145
|
pr_url: str,
|
|
64
146
|
*,
|
|
65
147
|
sync_strategy: str = "none",
|
|
66
148
|
conflict_strategy: str = "fail",
|
|
67
149
|
dry_run: bool = False,
|
|
150
|
+
update_method: str = "api",
|
|
68
151
|
) -> GitHubFixResult:
|
|
69
|
-
"""Fix markdown tables in a PR by URL
|
|
152
|
+
"""Fix markdown tables in a PR by URL.
|
|
70
153
|
|
|
71
154
|
Args:
|
|
72
155
|
pr_url: GitHub PR URL (e.g., https://github.com/owner/repo/pull/123)
|
|
73
|
-
sync_strategy: How to sync with base branch: 'none', 'rebase', or 'merge'
|
|
74
|
-
conflict_strategy: How to resolve conflicts: 'fail', 'ours', or 'theirs'
|
|
156
|
+
sync_strategy: How to sync with base branch: 'none', 'rebase', or 'merge' (git method only)
|
|
157
|
+
conflict_strategy: How to resolve conflicts: 'fail', 'ours', or 'theirs' (git method only)
|
|
75
158
|
dry_run: If True, don't actually push changes
|
|
159
|
+
update_method: Method to apply fixes: 'git' (clone, amend, push) or 'api' (GitHub API commits)
|
|
76
160
|
|
|
77
161
|
Returns:
|
|
78
162
|
GitHubFixResult with operation details
|
|
79
163
|
"""
|
|
164
|
+
# Normalize parameters to lowercase for case-insensitive comparison
|
|
165
|
+
update_method = update_method.lower()
|
|
166
|
+
sync_strategy = sync_strategy.lower()
|
|
167
|
+
conflict_strategy = conflict_strategy.lower()
|
|
168
|
+
|
|
169
|
+
# Validate parameters
|
|
170
|
+
validation_error = self._validate_parameters(
|
|
171
|
+
pr_url, update_method, sync_strategy, conflict_strategy
|
|
172
|
+
)
|
|
173
|
+
if validation_error:
|
|
174
|
+
return validation_error
|
|
175
|
+
|
|
80
176
|
# Parse PR URL
|
|
81
177
|
match = re.match(
|
|
82
178
|
r"https?://github\.com/([^/]+)/([^/]+)/pull/(\d+)", pr_url
|
|
@@ -150,23 +246,34 @@ class PRFixer:
|
|
|
150
246
|
merge_state_status=pr_data.get("mergeable_state", "unknown"),
|
|
151
247
|
)
|
|
152
248
|
|
|
153
|
-
if not clone_url:
|
|
249
|
+
if not clone_url and update_method == "git":
|
|
154
250
|
return GitHubFixResult(
|
|
155
251
|
pr_info=pr_info,
|
|
156
252
|
success=False,
|
|
157
253
|
message="PR head repository not accessible",
|
|
158
254
|
)
|
|
159
255
|
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
256
|
+
# Route to appropriate update method
|
|
257
|
+
if update_method == "api":
|
|
258
|
+
# Use GitHub API to update files
|
|
259
|
+
return await self._fix_pr_with_api(
|
|
260
|
+
pr_info,
|
|
261
|
+
owner,
|
|
262
|
+
repo,
|
|
263
|
+
pr_data,
|
|
264
|
+
dry_run=dry_run,
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
# Clone and fix using Git operations
|
|
268
|
+
return await self._fix_pr_with_git(
|
|
269
|
+
pr_info,
|
|
270
|
+
clone_url,
|
|
271
|
+
owner,
|
|
272
|
+
repo,
|
|
273
|
+
sync_strategy=sync_strategy,
|
|
274
|
+
conflict_strategy=conflict_strategy,
|
|
275
|
+
dry_run=dry_run,
|
|
276
|
+
)
|
|
170
277
|
|
|
171
278
|
except Exception as e:
|
|
172
279
|
self.logger.error(f"Error fixing PR: {e}", exc_info=True)
|
|
@@ -199,6 +306,7 @@ class PRFixer:
|
|
|
199
306
|
sync_strategy: str = "none",
|
|
200
307
|
conflict_strategy: str = "fail",
|
|
201
308
|
dry_run: bool = False,
|
|
309
|
+
git_config_mode: str | None = None,
|
|
202
310
|
) -> GitHubFixResult:
|
|
203
311
|
"""Fix PR using Git operations (clone, fix, amend, push).
|
|
204
312
|
|
|
@@ -210,10 +318,13 @@ class PRFixer:
|
|
|
210
318
|
sync_strategy: How to sync with base branch: 'none', 'rebase', or 'merge'
|
|
211
319
|
conflict_strategy: How to resolve conflicts: 'fail', 'ours', or 'theirs'
|
|
212
320
|
dry_run: If True, don't push changes
|
|
321
|
+
git_config_mode: Override git config mode for this operation
|
|
213
322
|
|
|
214
323
|
Returns:
|
|
215
324
|
GitHubFixResult with operation details
|
|
216
325
|
"""
|
|
326
|
+
# Use provided mode or fall back to instance default
|
|
327
|
+
config_mode = git_config_mode or self.git_config_mode
|
|
217
328
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
218
329
|
repo_dir = Path(tmpdir) / "repo"
|
|
219
330
|
self.logger.debug(f"Cloning {clone_url} to {repo_dir}")
|
|
@@ -318,24 +429,14 @@ class PRFixer:
|
|
|
318
429
|
files_modified=result_files,
|
|
319
430
|
)
|
|
320
431
|
|
|
321
|
-
# Configure git
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
)
|
|
328
|
-
subprocess.run(
|
|
329
|
-
[
|
|
330
|
-
"git",
|
|
331
|
-
"config",
|
|
332
|
-
"user.email",
|
|
333
|
-
"noreply@linuxfoundation.org",
|
|
334
|
-
],
|
|
335
|
-
cwd=repo_dir,
|
|
336
|
-
check=True,
|
|
337
|
-
capture_output=True,
|
|
432
|
+
# Configure git identity and signing
|
|
433
|
+
git_config = configure_git_identity(
|
|
434
|
+
repo_dir,
|
|
435
|
+
mode=config_mode,
|
|
436
|
+
bot_name="markdown-table-fixer",
|
|
437
|
+
bot_email="noreply@linuxfoundation.org",
|
|
338
438
|
)
|
|
439
|
+
self.logger.debug(f"Git config applied: {git_config}")
|
|
339
440
|
|
|
340
441
|
# Stage the changes
|
|
341
442
|
for file_path in files_modified:
|
|
@@ -575,6 +676,55 @@ class PRFixer:
|
|
|
575
676
|
f"Merge had conflicts, attempting to resolve with strategy '{conflict_strategy}'"
|
|
576
677
|
)
|
|
577
678
|
|
|
679
|
+
async def _fix_pr_with_api(
|
|
680
|
+
self,
|
|
681
|
+
pr_info: PRInfo,
|
|
682
|
+
owner: str,
|
|
683
|
+
repo: str,
|
|
684
|
+
pr_data: dict[str, Any],
|
|
685
|
+
*,
|
|
686
|
+
dry_run: bool = False,
|
|
687
|
+
) -> GitHubFixResult:
|
|
688
|
+
"""Fix PR using GitHub API (creates new commits, shows as verified by GitHub).
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
pr_info: PR information
|
|
692
|
+
owner: Repository owner
|
|
693
|
+
repo: Repository name
|
|
694
|
+
pr_data: Full PR data from API
|
|
695
|
+
dry_run: If True, don't actually push changes
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
GitHubFixResult with operation details
|
|
699
|
+
"""
|
|
700
|
+
result = await self.fix_pr_tables(
|
|
701
|
+
owner,
|
|
702
|
+
repo,
|
|
703
|
+
pr_data,
|
|
704
|
+
dry_run=dry_run,
|
|
705
|
+
create_comment=True,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
if result.get("success"):
|
|
709
|
+
files_modified = [
|
|
710
|
+
Path(f["filename"]) for f in result.get("fixed_files", [])
|
|
711
|
+
]
|
|
712
|
+
message = f"Fixed {result.get('tables_fixed', 0)} table(s) in {result.get('files_fixed', 0)} file(s)"
|
|
713
|
+
|
|
714
|
+
return GitHubFixResult(
|
|
715
|
+
pr_info=pr_info,
|
|
716
|
+
success=True,
|
|
717
|
+
message=message,
|
|
718
|
+
files_modified=files_modified,
|
|
719
|
+
)
|
|
720
|
+
else:
|
|
721
|
+
return GitHubFixResult(
|
|
722
|
+
pr_info=pr_info,
|
|
723
|
+
success=False,
|
|
724
|
+
message=result.get("error", "Failed to fix tables via API"),
|
|
725
|
+
error=result.get("error"),
|
|
726
|
+
)
|
|
727
|
+
|
|
578
728
|
async def fix_pr_tables(
|
|
579
729
|
self,
|
|
580
730
|
owner: str,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/__init__.py
RENAMED
|
File without changes
|
{markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
{markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/models.py
RENAMED
|
File without changes
|
{markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/pr_scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/table_fixer.py
RENAMED
|
File without changes
|
{markdown_table_fixer-0.1.0 → markdown_table_fixer-0.1.2}/src/markdown_table_fixer/table_parser.py
RENAMED
|
File without changes
|
|
File without changes
|