gitacross 2.1.0__tar.gz → 2.2.0__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.
- {gitacross-2.1.0/src/gitacross.egg-info → gitacross-2.2.0}/PKG-INFO +16 -16
- {gitacross-2.1.0 → gitacross-2.2.0}/README.md +15 -15
- {gitacross-2.1.0 → gitacross-2.2.0}/pyproject.toml +1 -1
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/__init__.py +4 -4
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/cli.py +3 -3
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/config.py +4 -13
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/linter/fixer.py +26 -26
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/linter/models.py +8 -8
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/linter/validator.py +82 -88
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/main.py +50 -50
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/renderer.py +3 -3
- {gitacross-2.1.0 → gitacross-2.2.0/src/gitacross.egg-info}/PKG-INFO +16 -16
- {gitacross-2.1.0 → gitacross-2.2.0}/tests/test_integration.py +2 -11
- {gitacross-2.1.0 → gitacross-2.2.0}/tests/test_linter.py +4 -3
- {gitacross-2.1.0 → gitacross-2.2.0}/tests/test_sync_api.py +9 -9
- {gitacross-2.1.0 → gitacross-2.2.0}/LICENSE +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/setup.cfg +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/__main__.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/git.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/linter/__init__.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/linter/keys.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/providers/__init__.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/providers/base.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/providers/gitea.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/providers/github.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/retry.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/source.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/state.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross/target.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross.egg-info/SOURCES.txt +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross.egg-info/dependency_links.txt +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross.egg-info/entry_points.txt +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross.egg-info/requires.txt +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/src/gitacross.egg-info/top_level.txt +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/tests/test_config.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/tests/test_git.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/tests/test_providers.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/tests/test_renderer.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/tests/test_source.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/tests/test_state.py +0 -0
- {gitacross-2.1.0 → gitacross-2.2.0}/tests/test_target.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitacross
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Mirror releases and git commits across platforms (Gitea, GitHub, local) with transform pipelines.
|
|
5
5
|
Author: Matthew Deik
|
|
6
6
|
License-Expression: MIT
|
|
@@ -112,7 +112,7 @@ gitacross --config config.yml --lint
|
|
|
112
112
|
gitacross --config config.yml
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
Run it again later — releases that were already synced are skipped, so nothing is duplicated. Use `--project my-project` to sync a single project. See [CLI](#cli) for all flags, or the [Python API](#python-api) to drive GitAcross from code.
|
|
115
|
+
Run it again later — releases that were already synced are skipped, so nothing is duplicated. Use `--project-name my-project` to sync a single project. See [CLI](#cli) for all flags, or the [Python API](#python-api) to drive GitAcross from code.
|
|
116
116
|
|
|
117
117
|
## How it works
|
|
118
118
|
|
|
@@ -176,11 +176,11 @@ Copy the source release notes/body to the target release. Default `true`. Set at
|
|
|
176
176
|
|
|
177
177
|
#### `release_description`
|
|
178
178
|
|
|
179
|
-
Format the target release notes from a template
|
|
179
|
+
Format the target release notes from a template. Placeholders: `{body}`, `{description}`, `{tag}`, `{commit_sha}`, `{short_sha}`, `{project_name}`, `{name}`, `{source_date}`.
|
|
180
180
|
|
|
181
181
|
#### `commit_message`
|
|
182
182
|
|
|
183
|
-
Custom commit message for the target commits
|
|
183
|
+
Custom commit message for the target commits. Default: `"Release {tag}"` or `"Sync commit {short_sha}"`. Placeholders: `{tag}`, `{commit_sha}`, `{short_sha}`, `{project_name}`, `{name}`, `{source_date}`, `{body}`, `{description}`.
|
|
184
184
|
|
|
185
185
|
#### `sync_assets`
|
|
186
186
|
|
|
@@ -436,13 +436,13 @@ GitAcross can be driven from the command line or called directly from Python.
|
|
|
436
436
|
### CLI
|
|
437
437
|
|
|
438
438
|
```
|
|
439
|
-
gitacross --config PATH [--project NAME] [--workdir PATH] [--dry-run] [--reset] [--lint] [--fix] [-v]
|
|
439
|
+
gitacross --config PATH [--project-name NAME] [--workdir PATH] [--dry-run] [--reset] [--lint] [--fix] [-v]
|
|
440
440
|
```
|
|
441
441
|
|
|
442
442
|
| Flag | Description |
|
|
443
443
|
|---|---|
|
|
444
444
|
| `--config PATH` | Config file to use (required) |
|
|
445
|
-
| `--project NAME` | Sync only
|
|
445
|
+
| `--project-name NAME` | Sync only the project with this name |
|
|
446
446
|
| `--workdir PATH` | Where state and cache live (default: `.gitsync`) |
|
|
447
447
|
| `--dry-run` | Preview changes without committing or pushing |
|
|
448
448
|
| `--reset` | Clear saved state and cache before running (fresh start) |
|
|
@@ -477,7 +477,7 @@ for r in results:
|
|
|
477
477
|
# Preview only — nothing is committed or pushed
|
|
478
478
|
results = gitacross.run(
|
|
479
479
|
"config.yml",
|
|
480
|
-
|
|
480
|
+
project_name="my-project",
|
|
481
481
|
dry_run=True,
|
|
482
482
|
work_dir="/data/custom_dir",
|
|
483
483
|
)
|
|
@@ -501,9 +501,9 @@ results = gitacross.run("config.yml", reset=True)
|
|
|
501
501
|
```python
|
|
502
502
|
config = gitacross.Config("config.yml")
|
|
503
503
|
|
|
504
|
-
for
|
|
505
|
-
if
|
|
506
|
-
gitacross.sync_project(
|
|
504
|
+
for project_config in config.projects:
|
|
505
|
+
if project_config.enabled:
|
|
506
|
+
gitacross.sync_project(project_config, ".gitsync", dry_run=False)
|
|
507
507
|
```
|
|
508
508
|
|
|
509
509
|
</details>
|
|
@@ -513,7 +513,7 @@ for project in config.projects:
|
|
|
513
513
|
|
|
514
514
|
```python
|
|
515
515
|
# ${VAR} tokens still resolve from the environment
|
|
516
|
-
|
|
516
|
+
project_config = gitacross.ProjectConfig({
|
|
517
517
|
"name": "my-project",
|
|
518
518
|
"source": {
|
|
519
519
|
"type": "gitea",
|
|
@@ -528,7 +528,7 @@ project = gitacross.ProjectConfig({
|
|
|
528
528
|
"token": "${GITHUB_TOKEN}",
|
|
529
529
|
},
|
|
530
530
|
})
|
|
531
|
-
gitacross.sync_project(
|
|
531
|
+
gitacross.sync_project(project_config, ".gitsync")
|
|
532
532
|
```
|
|
533
533
|
|
|
534
534
|
</details>
|
|
@@ -573,16 +573,16 @@ All public symbols are importable directly from `gitacross`:
|
|
|
573
573
|
|
|
574
574
|
| Symbol | What it does |
|
|
575
575
|
|---|---|
|
|
576
|
-
| `run(config,
|
|
577
|
-
| `sync_project(
|
|
576
|
+
| `run(config, project_name=None, dry_run=False, reset=False, work_dir=".gitsync")` | **Primary entry point.** Sync from a config — a `Config` instance, a path, or an open file object
|
|
577
|
+
| `sync_project(project_config, work_dir=".gitsync", dry_run=False)` | Sync one project's new releases (respects `project_config.enabled`); state and cache live in `work_dir`. Returns dicts with `tag`, `source_commit`, `target_commit`, `source_date` |
|
|
578
578
|
| `lint_config(config, print_output=True)` | Lint a config (path or open file object) → `LintReport` |
|
|
579
579
|
| `fix_config(config, write_back=True, print_output=True)` | Fix misplaced/redundant options (path only — writes back to the file) → `FixReport` |
|
|
580
580
|
| `Config(config_source)` | Load a config from a path or open file object; exposes `.projects`. `Config.from_yaml_string(content)` loads a config from raw YAML text (`str` or `bytes`) — no file or stream needed |
|
|
581
581
|
| `ProjectConfig(raw)` | Build one mirror project from a raw config dict (see the “No config file” example). Fields: `name`, `enabled`, `source`, `target`, `renderer`, `retry`, `preserve_description`, `sync_assets`, `stream_assets`, `commit_message`, `release_description` |
|
|
582
582
|
| `ConfigLinter()` | Collect lint issues programmatically: `lint_file(config)`, `lint_yaml_string(content)`; results accumulate in `.issues` |
|
|
583
583
|
| `ConfigFixer()` | Fix a config programmatically: `fix_yaml_string(content)` → `FixReport`; actions recorded in `.fixes` |
|
|
584
|
-
| `LintIssue(severity, message,
|
|
585
|
-
| `FixIssue(message,
|
|
584
|
+
| `LintIssue(severity, message, project_name=None, key=None)` | One lint finding |
|
|
585
|
+
| `FixIssue(message, project_name=None)` | One applied fix |
|
|
586
586
|
| `LintReport(issues)` | Lint results: `.issues`, `.errors`, `.warnings`, `.redundant`, `.is_valid`, `.format_text()` |
|
|
587
587
|
| `FixReport(fixes, content, is_valid, error=None)` | Fix results: `.fixes`, `.content`, `.is_valid`, `.error`, `.format_text()` |
|
|
588
588
|
| `LintSeverity` | Severity levels used by `LintIssue`: `LintSeverity.ERROR`, `LintSeverity.WARNING`, `LintSeverity.REDUNDANT` |
|
|
@@ -83,7 +83,7 @@ gitacross --config config.yml --lint
|
|
|
83
83
|
gitacross --config config.yml
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
Run it again later — releases that were already synced are skipped, so nothing is duplicated. Use `--project my-project` to sync a single project. See [CLI](#cli) for all flags, or the [Python API](#python-api) to drive GitAcross from code.
|
|
86
|
+
Run it again later — releases that were already synced are skipped, so nothing is duplicated. Use `--project-name my-project` to sync a single project. See [CLI](#cli) for all flags, or the [Python API](#python-api) to drive GitAcross from code.
|
|
87
87
|
|
|
88
88
|
## How it works
|
|
89
89
|
|
|
@@ -147,11 +147,11 @@ Copy the source release notes/body to the target release. Default `true`. Set at
|
|
|
147
147
|
|
|
148
148
|
#### `release_description`
|
|
149
149
|
|
|
150
|
-
Format the target release notes from a template
|
|
150
|
+
Format the target release notes from a template. Placeholders: `{body}`, `{description}`, `{tag}`, `{commit_sha}`, `{short_sha}`, `{project_name}`, `{name}`, `{source_date}`.
|
|
151
151
|
|
|
152
152
|
#### `commit_message`
|
|
153
153
|
|
|
154
|
-
Custom commit message for the target commits
|
|
154
|
+
Custom commit message for the target commits. Default: `"Release {tag}"` or `"Sync commit {short_sha}"`. Placeholders: `{tag}`, `{commit_sha}`, `{short_sha}`, `{project_name}`, `{name}`, `{source_date}`, `{body}`, `{description}`.
|
|
155
155
|
|
|
156
156
|
#### `sync_assets`
|
|
157
157
|
|
|
@@ -407,13 +407,13 @@ GitAcross can be driven from the command line or called directly from Python.
|
|
|
407
407
|
### CLI
|
|
408
408
|
|
|
409
409
|
```
|
|
410
|
-
gitacross --config PATH [--project NAME] [--workdir PATH] [--dry-run] [--reset] [--lint] [--fix] [-v]
|
|
410
|
+
gitacross --config PATH [--project-name NAME] [--workdir PATH] [--dry-run] [--reset] [--lint] [--fix] [-v]
|
|
411
411
|
```
|
|
412
412
|
|
|
413
413
|
| Flag | Description |
|
|
414
414
|
|---|---|
|
|
415
415
|
| `--config PATH` | Config file to use (required) |
|
|
416
|
-
| `--project NAME` | Sync only
|
|
416
|
+
| `--project-name NAME` | Sync only the project with this name |
|
|
417
417
|
| `--workdir PATH` | Where state and cache live (default: `.gitsync`) |
|
|
418
418
|
| `--dry-run` | Preview changes without committing or pushing |
|
|
419
419
|
| `--reset` | Clear saved state and cache before running (fresh start) |
|
|
@@ -448,7 +448,7 @@ for r in results:
|
|
|
448
448
|
# Preview only — nothing is committed or pushed
|
|
449
449
|
results = gitacross.run(
|
|
450
450
|
"config.yml",
|
|
451
|
-
|
|
451
|
+
project_name="my-project",
|
|
452
452
|
dry_run=True,
|
|
453
453
|
work_dir="/data/custom_dir",
|
|
454
454
|
)
|
|
@@ -472,9 +472,9 @@ results = gitacross.run("config.yml", reset=True)
|
|
|
472
472
|
```python
|
|
473
473
|
config = gitacross.Config("config.yml")
|
|
474
474
|
|
|
475
|
-
for
|
|
476
|
-
if
|
|
477
|
-
gitacross.sync_project(
|
|
475
|
+
for project_config in config.projects:
|
|
476
|
+
if project_config.enabled:
|
|
477
|
+
gitacross.sync_project(project_config, ".gitsync", dry_run=False)
|
|
478
478
|
```
|
|
479
479
|
|
|
480
480
|
</details>
|
|
@@ -484,7 +484,7 @@ for project in config.projects:
|
|
|
484
484
|
|
|
485
485
|
```python
|
|
486
486
|
# ${VAR} tokens still resolve from the environment
|
|
487
|
-
|
|
487
|
+
project_config = gitacross.ProjectConfig({
|
|
488
488
|
"name": "my-project",
|
|
489
489
|
"source": {
|
|
490
490
|
"type": "gitea",
|
|
@@ -499,7 +499,7 @@ project = gitacross.ProjectConfig({
|
|
|
499
499
|
"token": "${GITHUB_TOKEN}",
|
|
500
500
|
},
|
|
501
501
|
})
|
|
502
|
-
gitacross.sync_project(
|
|
502
|
+
gitacross.sync_project(project_config, ".gitsync")
|
|
503
503
|
```
|
|
504
504
|
|
|
505
505
|
</details>
|
|
@@ -544,16 +544,16 @@ All public symbols are importable directly from `gitacross`:
|
|
|
544
544
|
|
|
545
545
|
| Symbol | What it does |
|
|
546
546
|
|---|---|
|
|
547
|
-
| `run(config,
|
|
548
|
-
| `sync_project(
|
|
547
|
+
| `run(config, project_name=None, dry_run=False, reset=False, work_dir=".gitsync")` | **Primary entry point.** Sync from a config — a `Config` instance, a path, or an open file object
|
|
548
|
+
| `sync_project(project_config, work_dir=".gitsync", dry_run=False)` | Sync one project's new releases (respects `project_config.enabled`); state and cache live in `work_dir`. Returns dicts with `tag`, `source_commit`, `target_commit`, `source_date` |
|
|
549
549
|
| `lint_config(config, print_output=True)` | Lint a config (path or open file object) → `LintReport` |
|
|
550
550
|
| `fix_config(config, write_back=True, print_output=True)` | Fix misplaced/redundant options (path only — writes back to the file) → `FixReport` |
|
|
551
551
|
| `Config(config_source)` | Load a config from a path or open file object; exposes `.projects`. `Config.from_yaml_string(content)` loads a config from raw YAML text (`str` or `bytes`) — no file or stream needed |
|
|
552
552
|
| `ProjectConfig(raw)` | Build one mirror project from a raw config dict (see the “No config file” example). Fields: `name`, `enabled`, `source`, `target`, `renderer`, `retry`, `preserve_description`, `sync_assets`, `stream_assets`, `commit_message`, `release_description` |
|
|
553
553
|
| `ConfigLinter()` | Collect lint issues programmatically: `lint_file(config)`, `lint_yaml_string(content)`; results accumulate in `.issues` |
|
|
554
554
|
| `ConfigFixer()` | Fix a config programmatically: `fix_yaml_string(content)` → `FixReport`; actions recorded in `.fixes` |
|
|
555
|
-
| `LintIssue(severity, message,
|
|
556
|
-
| `FixIssue(message,
|
|
555
|
+
| `LintIssue(severity, message, project_name=None, key=None)` | One lint finding |
|
|
556
|
+
| `FixIssue(message, project_name=None)` | One applied fix |
|
|
557
557
|
| `LintReport(issues)` | Lint results: `.issues`, `.errors`, `.warnings`, `.redundant`, `.is_valid`, `.format_text()` |
|
|
558
558
|
| `FixReport(fixes, content, is_valid, error=None)` | Fix results: `.fixes`, `.content`, `.is_valid`, `.error`, `.format_text()` |
|
|
559
559
|
| `LintSeverity` | Severity levels used by `LintIssue`: `LintSeverity.ERROR`, `LintSeverity.WARNING`, `LintSeverity.REDUNDANT` |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gitacross"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "Mirror releases and git commits across platforms (Gitea, GitHub, local) with transform pipelines."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -8,13 +8,13 @@ Quickstart (Python API)::
|
|
|
8
8
|
results = gitacross.run("config.yml")
|
|
9
9
|
|
|
10
10
|
# Sync a single project with dry-run mode:
|
|
11
|
-
results = gitacross.run("config.yml",
|
|
11
|
+
results = gitacross.run("config.yml", project_name="my-mirror", dry_run=True)
|
|
12
12
|
|
|
13
13
|
# Lower-level: build objects yourself for full control:
|
|
14
14
|
config = gitacross.Config("config.yml")
|
|
15
|
-
for
|
|
16
|
-
if
|
|
17
|
-
gitacross.sync_project(
|
|
15
|
+
for project_config in config.projects:
|
|
16
|
+
if project_config.enabled:
|
|
17
|
+
gitacross.sync_project(project_config, ".gitsync")
|
|
18
18
|
|
|
19
19
|
# Lint / fix a config file:
|
|
20
20
|
report = gitacross.lint_config("config.yml")
|
|
@@ -26,7 +26,7 @@ def main():
|
|
|
26
26
|
|
|
27
27
|
parser = argparse.ArgumentParser(description="Sync releases from source to target")
|
|
28
28
|
_ = parser.add_argument("--config", required=True, help="Path to config.yml")
|
|
29
|
-
_ = parser.add_argument("--project", help="Sync only this project (by name)")
|
|
29
|
+
_ = parser.add_argument("--project-name", help="Sync only this project (by name)")
|
|
30
30
|
_ = parser.add_argument(
|
|
31
31
|
"--dry-run", action="store_true", help="Print changes without pushing"
|
|
32
32
|
)
|
|
@@ -58,7 +58,7 @@ def main():
|
|
|
58
58
|
config_path = Path(cast(str, args.config))
|
|
59
59
|
fix_flag = cast(bool, args.fix)
|
|
60
60
|
lint_flag = cast(bool, args.lint)
|
|
61
|
-
|
|
61
|
+
project_name = None if args.project_name is None else cast(str, args.project_name)
|
|
62
62
|
dry_run_flag = cast(bool, args.dry_run)
|
|
63
63
|
reset_flag = cast(bool, args.reset)
|
|
64
64
|
workdir = cast(str, args.workdir)
|
|
@@ -92,7 +92,7 @@ def main():
|
|
|
92
92
|
try:
|
|
93
93
|
results = run(
|
|
94
94
|
config_path,
|
|
95
|
-
|
|
95
|
+
project_name=project_name,
|
|
96
96
|
dry_run=dry_run_flag,
|
|
97
97
|
reset=reset_flag,
|
|
98
98
|
work_dir=workdir,
|
|
@@ -22,10 +22,7 @@ VALID_PROJECT_KEYS = {
|
|
|
22
22
|
"sync_assets",
|
|
23
23
|
"stream_assets",
|
|
24
24
|
"commit_message",
|
|
25
|
-
"commit_template",
|
|
26
25
|
"release_description",
|
|
27
|
-
"release_notes_template",
|
|
28
|
-
"description_template",
|
|
29
26
|
}
|
|
30
27
|
|
|
31
28
|
KNOWN_SOURCE_KEYS = {
|
|
@@ -256,15 +253,10 @@ class ProjectConfig:
|
|
|
256
253
|
self.stream_assets = bool(raw.get("stream_assets", False))
|
|
257
254
|
|
|
258
255
|
# commit_message — project-level commit message template (e.g. "chore(sync): {tag}")
|
|
259
|
-
self.commit_message = raw.get("commit_message") or
|
|
256
|
+
self.commit_message = raw.get("commit_message") or None
|
|
260
257
|
|
|
261
258
|
# release_description — project-level release description template
|
|
262
|
-
self.release_description = (
|
|
263
|
-
raw.get("release_description")
|
|
264
|
-
or raw.get("release_notes_template")
|
|
265
|
-
or raw.get("description_template")
|
|
266
|
-
or None
|
|
267
|
-
)
|
|
259
|
+
self.release_description = raw.get("release_description") or None
|
|
268
260
|
|
|
269
261
|
def __repr__(self):
|
|
270
262
|
return (
|
|
@@ -309,9 +301,8 @@ def _cascade(raw_project, raw_source, raw_target, keys, default):
|
|
|
309
301
|
"""Return the first value found for any of *keys* across project, source, target.
|
|
310
302
|
|
|
311
303
|
Lookup order: project-level first (highest priority), then source-level,
|
|
312
|
-
then target-level, then *default*.
|
|
313
|
-
level
|
|
314
|
-
source-level primary key.
|
|
304
|
+
then target-level, then *default*. The first *key* present at the first
|
|
305
|
+
level wins — so a project-level value beats a source-level one.
|
|
315
306
|
"""
|
|
316
307
|
for raw in (raw_project, raw_source, raw_target):
|
|
317
308
|
for k in keys:
|
|
@@ -18,8 +18,8 @@ class ConfigFixer:
|
|
|
18
18
|
def __repr__(self):
|
|
19
19
|
return f"ConfigFixer(fixes={len(self.fixes)})"
|
|
20
20
|
|
|
21
|
-
def _add(self, message: str,
|
|
22
|
-
self.fixes.append(FixIssue(message=message,
|
|
21
|
+
def _add(self, message: str, project_name: str | None = None):
|
|
22
|
+
self.fixes.append(FixIssue(message=message, project_name=project_name))
|
|
23
23
|
|
|
24
24
|
def fix_yaml_string(self, content: str) -> FixReport:
|
|
25
25
|
# Fresh accumulator — reusing an instance must not leak prior fixes
|
|
@@ -68,12 +68,12 @@ class ConfigFixer:
|
|
|
68
68
|
p["source"][k] = val
|
|
69
69
|
self._add(
|
|
70
70
|
f"Moved '{k}: {val}' from project level into 'source:'",
|
|
71
|
-
|
|
71
|
+
project_name=p_name,
|
|
72
72
|
)
|
|
73
73
|
else:
|
|
74
74
|
self._add(
|
|
75
75
|
f"Removed duplicate project-level '{k}' (already present under source)",
|
|
76
|
-
|
|
76
|
+
project_name=p_name,
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
# 2. Misplaced project-level keys inside source -> move to project level
|
|
@@ -85,12 +85,12 @@ class ConfigFixer:
|
|
|
85
85
|
p[k] = val
|
|
86
86
|
self._add(
|
|
87
87
|
f"Moved '{k}' from 'source:' to project level",
|
|
88
|
-
|
|
88
|
+
project_name=p_name,
|
|
89
89
|
)
|
|
90
90
|
else:
|
|
91
91
|
self._add(
|
|
92
92
|
f"Removed duplicate '{k}' from 'source:' (already present at project level)",
|
|
93
|
-
|
|
93
|
+
project_name=p_name,
|
|
94
94
|
)
|
|
95
95
|
|
|
96
96
|
# 3. Misplaced project keys in target -> move to project level
|
|
@@ -103,12 +103,12 @@ class ConfigFixer:
|
|
|
103
103
|
p[k] = val
|
|
104
104
|
self._add(
|
|
105
105
|
f"Moved '{k}' from 'target:' to project level",
|
|
106
|
-
|
|
106
|
+
project_name=p_name,
|
|
107
107
|
)
|
|
108
108
|
else:
|
|
109
109
|
self._add(
|
|
110
110
|
f"Removed duplicate '{k}' from 'target:' (already present at project level)",
|
|
111
|
-
|
|
111
|
+
project_name=p_name,
|
|
112
112
|
)
|
|
113
113
|
elif k in KNOWN_SOURCE_KEYS:
|
|
114
114
|
val = p["target"].pop(k)
|
|
@@ -118,32 +118,32 @@ class ConfigFixer:
|
|
|
118
118
|
p["source"][k] = val
|
|
119
119
|
self._add(
|
|
120
120
|
f"Moved '{k}: {val}' from 'target:' into 'source:'",
|
|
121
|
-
|
|
121
|
+
project_name=p_name,
|
|
122
122
|
)
|
|
123
123
|
else:
|
|
124
124
|
self._add(
|
|
125
125
|
f"Removed duplicate '{k}' from 'target:' (already present under source)",
|
|
126
|
-
|
|
126
|
+
project_name=p_name,
|
|
127
127
|
)
|
|
128
128
|
|
|
129
129
|
# 4. Redundant project-level options
|
|
130
130
|
if p.get("enabled") is True:
|
|
131
131
|
del p["enabled"]
|
|
132
|
-
self._add("Removed redundant 'enabled: true'",
|
|
132
|
+
self._add("Removed redundant 'enabled: true'", project_name=p_name)
|
|
133
133
|
|
|
134
134
|
for key in ("preserve_description",):
|
|
135
135
|
if p.get(key) is True:
|
|
136
136
|
del p[key]
|
|
137
|
-
self._add(f"Removed redundant '{key}: true'",
|
|
137
|
+
self._add(f"Removed redundant '{key}: true'", project_name=p_name)
|
|
138
138
|
|
|
139
139
|
for key in ("sync_assets",):
|
|
140
140
|
if p.get(key) is False:
|
|
141
141
|
del p[key]
|
|
142
|
-
self._add(f"Removed redundant '{key}: false'",
|
|
142
|
+
self._add(f"Removed redundant '{key}: false'", project_name=p_name)
|
|
143
143
|
|
|
144
144
|
if p.get("stream_assets") is False:
|
|
145
145
|
del p["stream_assets"]
|
|
146
|
-
self._add("Removed redundant 'stream_assets: false'",
|
|
146
|
+
self._add("Removed redundant 'stream_assets: false'", project_name=p_name)
|
|
147
147
|
|
|
148
148
|
# 3. Source endpoint fixes
|
|
149
149
|
if isinstance(p.get("source"), dict):
|
|
@@ -151,31 +151,31 @@ class ConfigFixer:
|
|
|
151
151
|
if src.get("mode") == "release":
|
|
152
152
|
del src["mode"]
|
|
153
153
|
self._add(
|
|
154
|
-
"Removed redundant 'source.mode: release'",
|
|
154
|
+
"Removed redundant 'source.mode: release'", project_name=p_name
|
|
155
155
|
)
|
|
156
156
|
if src.get("include_prereleases") is False:
|
|
157
157
|
del src["include_prereleases"]
|
|
158
158
|
self._add(
|
|
159
159
|
"Removed redundant 'source.include_prereleases: false'",
|
|
160
|
-
|
|
160
|
+
project_name=p_name,
|
|
161
161
|
)
|
|
162
162
|
if src.get("include_drafts") is False:
|
|
163
163
|
del src["include_drafts"]
|
|
164
164
|
self._add(
|
|
165
165
|
"Removed redundant 'source.include_drafts: false'",
|
|
166
|
-
|
|
166
|
+
project_name=p_name,
|
|
167
167
|
)
|
|
168
168
|
if src.get("sync_from") == "":
|
|
169
169
|
del src["sync_from"]
|
|
170
170
|
self._add(
|
|
171
171
|
"Removed redundant 'source.sync_from: \"\"'",
|
|
172
|
-
|
|
172
|
+
project_name=p_name,
|
|
173
173
|
)
|
|
174
174
|
if src.get("tag_pattern") == "*":
|
|
175
175
|
del src["tag_pattern"]
|
|
176
176
|
self._add(
|
|
177
177
|
"Removed redundant 'source.tag_pattern: \"*\"'",
|
|
178
|
-
|
|
178
|
+
project_name=p_name,
|
|
179
179
|
)
|
|
180
180
|
|
|
181
181
|
# 4. Target endpoint fixes
|
|
@@ -184,7 +184,7 @@ class ConfigFixer:
|
|
|
184
184
|
if tgt.get("branch") == "main":
|
|
185
185
|
del tgt["branch"]
|
|
186
186
|
self._add(
|
|
187
|
-
"Removed redundant 'target.branch: main'",
|
|
187
|
+
"Removed redundant 'target.branch: main'", project_name=p_name
|
|
188
188
|
)
|
|
189
189
|
|
|
190
190
|
# 5. Retry fixes
|
|
@@ -198,11 +198,11 @@ class ConfigFixer:
|
|
|
198
198
|
del p["retry"]
|
|
199
199
|
self._add(
|
|
200
200
|
"Removed default 'retry' configuration block",
|
|
201
|
-
|
|
201
|
+
project_name=p_name,
|
|
202
202
|
)
|
|
203
203
|
else:
|
|
204
204
|
self._add(
|
|
205
|
-
"Removed redundant retry defaults",
|
|
205
|
+
"Removed redundant retry defaults", project_name=p_name
|
|
206
206
|
)
|
|
207
207
|
|
|
208
208
|
# 6. Renderer fixes
|
|
@@ -221,20 +221,20 @@ class ConfigFixer:
|
|
|
221
221
|
del item["pattern"]
|
|
222
222
|
self._add(
|
|
223
223
|
f"Removed redundant 'pattern: literal' in {op_type} operation",
|
|
224
|
-
|
|
224
|
+
project_name=p_name,
|
|
225
225
|
)
|
|
226
226
|
if op_type == "replace":
|
|
227
227
|
if item.get("case_sensitive") is True:
|
|
228
228
|
del item["case_sensitive"]
|
|
229
229
|
self._add(
|
|
230
230
|
"Removed redundant 'case_sensitive: true' in replace operation",
|
|
231
|
-
|
|
231
|
+
project_name=p_name,
|
|
232
232
|
)
|
|
233
233
|
if item.get("match_case") is False:
|
|
234
234
|
del item["match_case"]
|
|
235
235
|
self._add(
|
|
236
236
|
"Removed redundant 'match_case: false' in replace operation",
|
|
237
|
-
|
|
237
|
+
project_name=p_name,
|
|
238
238
|
)
|
|
239
239
|
if op_type == "validate" and item.get(
|
|
240
240
|
"case_sensitive"
|
|
@@ -242,7 +242,7 @@ class ConfigFixer:
|
|
|
242
242
|
del item["case_sensitive"]
|
|
243
243
|
self._add(
|
|
244
244
|
"Removed redundant 'case_sensitive: true' in validate operation",
|
|
245
|
-
|
|
245
|
+
project_name=p_name,
|
|
246
246
|
)
|
|
247
247
|
|
|
248
248
|
|
|
@@ -21,13 +21,13 @@ class LintSeverity(Enum):
|
|
|
21
21
|
class LintIssue:
|
|
22
22
|
severity: LintSeverity
|
|
23
23
|
message: str
|
|
24
|
-
|
|
24
|
+
project_name: str | None = None
|
|
25
25
|
key: str | None = None
|
|
26
26
|
|
|
27
27
|
def __str__(self) -> str:
|
|
28
28
|
prefix = f"[{self.severity.value}]"
|
|
29
|
-
if self.
|
|
30
|
-
return f" {prefix} (project '{self.
|
|
29
|
+
if self.project_name:
|
|
30
|
+
return f" {prefix} (project '{self.project_name}'): {self.message}"
|
|
31
31
|
return f" {prefix} {self.message}"
|
|
32
32
|
|
|
33
33
|
|
|
@@ -63,7 +63,7 @@ class LintReport:
|
|
|
63
63
|
# Group issues by project (None first)
|
|
64
64
|
by_proj: dict[str | None, list[LintIssue]] = {}
|
|
65
65
|
for issue in self.issues:
|
|
66
|
-
by_proj.setdefault(issue.
|
|
66
|
+
by_proj.setdefault(issue.project_name, []).append(issue)
|
|
67
67
|
|
|
68
68
|
if None in by_proj:
|
|
69
69
|
for issue in by_proj[None]:
|
|
@@ -98,11 +98,11 @@ _CleanDumper.add_representer(str, _str_presenter)
|
|
|
98
98
|
@dataclass
|
|
99
99
|
class FixIssue:
|
|
100
100
|
message: str
|
|
101
|
-
|
|
101
|
+
project_name: str | None = None
|
|
102
102
|
|
|
103
103
|
def __str__(self) -> str:
|
|
104
|
-
if self.
|
|
105
|
-
return f" ✓ (project '{self.
|
|
104
|
+
if self.project_name:
|
|
105
|
+
return f" ✓ (project '{self.project_name}'): {self.message}"
|
|
106
106
|
return f" ✓ {self.message}"
|
|
107
107
|
|
|
108
108
|
|
|
@@ -121,7 +121,7 @@ class FixReport:
|
|
|
121
121
|
out = [f"Applied {len(self.fixes)} fix(es):"]
|
|
122
122
|
by_proj: dict[str | None, list[FixIssue]] = {}
|
|
123
123
|
for fix in self.fixes:
|
|
124
|
-
by_proj.setdefault(fix.
|
|
124
|
+
by_proj.setdefault(fix.project_name, []).append(fix)
|
|
125
125
|
|
|
126
126
|
if None in by_proj:
|
|
127
127
|
for fix in by_proj[None]:
|