gitacross 2.0.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.0.0/src/gitacross.egg-info → gitacross-2.2.0}/PKG-INFO +26 -16
- {gitacross-2.0.0 → gitacross-2.2.0}/README.md +24 -15
- {gitacross-2.0.0 → gitacross-2.2.0}/pyproject.toml +14 -1
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/__init__.py +6 -5
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/__main__.py +0 -1
- gitacross-2.2.0/src/gitacross/cli.py +105 -0
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/config.py +12 -14
- gitacross-2.2.0/src/gitacross/linter/__init__.py +26 -0
- gitacross-2.2.0/src/gitacross/linter/fixer.py +282 -0
- gitacross-2.2.0/src/gitacross/linter/keys.py +39 -0
- gitacross-2.2.0/src/gitacross/linter/models.py +145 -0
- gitacross-2.0.0/src/gitacross/linter.py → gitacross-2.2.0/src/gitacross/linter/validator.py +106 -532
- gitacross-2.2.0/src/gitacross/main.py +562 -0
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/providers/__init__.py +16 -3
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/providers/base.py +8 -1
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/providers/gitea.py +10 -5
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/providers/github.py +0 -1
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/renderer.py +11 -13
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/source.py +10 -9
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/target.py +6 -6
- {gitacross-2.0.0 → gitacross-2.2.0/src/gitacross.egg-info}/PKG-INFO +26 -16
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross.egg-info/SOURCES.txt +16 -2
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross.egg-info/requires.txt +1 -0
- gitacross-2.2.0/tests/test_config.py +488 -0
- gitacross-2.2.0/tests/test_git.py +341 -0
- gitacross-2.2.0/tests/test_integration.py +1196 -0
- gitacross-2.2.0/tests/test_linter.py +1031 -0
- gitacross-2.2.0/tests/test_providers.py +643 -0
- gitacross-2.2.0/tests/test_renderer.py +648 -0
- gitacross-2.2.0/tests/test_source.py +779 -0
- gitacross-2.2.0/tests/test_state.py +77 -0
- gitacross-2.2.0/tests/test_sync_api.py +1142 -0
- gitacross-2.2.0/tests/test_target.py +223 -0
- gitacross-2.0.0/src/gitacross/main.py +0 -614
- gitacross-2.0.0/tests/test_sync.py +0 -4560
- {gitacross-2.0.0 → gitacross-2.2.0}/LICENSE +0 -0
- {gitacross-2.0.0 → gitacross-2.2.0}/setup.cfg +0 -0
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/git.py +0 -0
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/retry.py +0 -0
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross/state.py +0 -0
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross.egg-info/dependency_links.txt +0 -0
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross.egg-info/entry_points.txt +0 -0
- {gitacross-2.0.0 → gitacross-2.2.0}/src/gitacross.egg-info/top_level.txt +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
|
|
@@ -22,6 +22,7 @@ License-File: LICENSE
|
|
|
22
22
|
Requires-Dist: PyYAML<7.0,>=6.0
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
25
26
|
Requires-Dist: build; extra == "dev"
|
|
26
27
|
Requires-Dist: twine; extra == "dev"
|
|
27
28
|
Dynamic: license-file
|
|
@@ -77,6 +78,15 @@ For development, install the local checkout:
|
|
|
77
78
|
pip install -e .
|
|
78
79
|
```
|
|
79
80
|
|
|
81
|
+
### Tests
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install -e .[dev] # pytest + pytest-cov
|
|
85
|
+
pytest # run the test suite
|
|
86
|
+
pytest --cov=gitacross # run with a coverage floor (90% line + branch)
|
|
87
|
+
ruff check src tests # lint
|
|
88
|
+
```
|
|
89
|
+
|
|
80
90
|
### Config
|
|
81
91
|
|
|
82
92
|
GitAcross reads a YAML file listing the mirrors you want. Each entry in the `projects` list is a **project**: it has a `source` (where releases come from) and a `target` (where they go).
|
|
@@ -102,7 +112,7 @@ gitacross --config config.yml --lint
|
|
|
102
112
|
gitacross --config config.yml
|
|
103
113
|
```
|
|
104
114
|
|
|
105
|
-
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.
|
|
106
116
|
|
|
107
117
|
## How it works
|
|
108
118
|
|
|
@@ -166,11 +176,11 @@ Copy the source release notes/body to the target release. Default `true`. Set at
|
|
|
166
176
|
|
|
167
177
|
#### `release_description`
|
|
168
178
|
|
|
169
|
-
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}`.
|
|
170
180
|
|
|
171
181
|
#### `commit_message`
|
|
172
182
|
|
|
173
|
-
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}`.
|
|
174
184
|
|
|
175
185
|
#### `sync_assets`
|
|
176
186
|
|
|
@@ -426,13 +436,13 @@ GitAcross can be driven from the command line or called directly from Python.
|
|
|
426
436
|
### CLI
|
|
427
437
|
|
|
428
438
|
```
|
|
429
|
-
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]
|
|
430
440
|
```
|
|
431
441
|
|
|
432
442
|
| Flag | Description |
|
|
433
443
|
|---|---|
|
|
434
444
|
| `--config PATH` | Config file to use (required) |
|
|
435
|
-
| `--project NAME` | Sync only
|
|
445
|
+
| `--project-name NAME` | Sync only the project with this name |
|
|
436
446
|
| `--workdir PATH` | Where state and cache live (default: `.gitsync`) |
|
|
437
447
|
| `--dry-run` | Preview changes without committing or pushing |
|
|
438
448
|
| `--reset` | Clear saved state and cache before running (fresh start) |
|
|
@@ -467,7 +477,7 @@ for r in results:
|
|
|
467
477
|
# Preview only — nothing is committed or pushed
|
|
468
478
|
results = gitacross.run(
|
|
469
479
|
"config.yml",
|
|
470
|
-
|
|
480
|
+
project_name="my-project",
|
|
471
481
|
dry_run=True,
|
|
472
482
|
work_dir="/data/custom_dir",
|
|
473
483
|
)
|
|
@@ -491,9 +501,9 @@ results = gitacross.run("config.yml", reset=True)
|
|
|
491
501
|
```python
|
|
492
502
|
config = gitacross.Config("config.yml")
|
|
493
503
|
|
|
494
|
-
for
|
|
495
|
-
if
|
|
496
|
-
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)
|
|
497
507
|
```
|
|
498
508
|
|
|
499
509
|
</details>
|
|
@@ -503,7 +513,7 @@ for project in config.projects:
|
|
|
503
513
|
|
|
504
514
|
```python
|
|
505
515
|
# ${VAR} tokens still resolve from the environment
|
|
506
|
-
|
|
516
|
+
project_config = gitacross.ProjectConfig({
|
|
507
517
|
"name": "my-project",
|
|
508
518
|
"source": {
|
|
509
519
|
"type": "gitea",
|
|
@@ -518,7 +528,7 @@ project = gitacross.ProjectConfig({
|
|
|
518
528
|
"token": "${GITHUB_TOKEN}",
|
|
519
529
|
},
|
|
520
530
|
})
|
|
521
|
-
gitacross.sync_project(
|
|
531
|
+
gitacross.sync_project(project_config, ".gitsync")
|
|
522
532
|
```
|
|
523
533
|
|
|
524
534
|
</details>
|
|
@@ -563,16 +573,16 @@ All public symbols are importable directly from `gitacross`:
|
|
|
563
573
|
|
|
564
574
|
| Symbol | What it does |
|
|
565
575
|
|---|---|
|
|
566
|
-
| `run(config,
|
|
567
|
-
| `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` |
|
|
568
578
|
| `lint_config(config, print_output=True)` | Lint a config (path or open file object) → `LintReport` |
|
|
569
579
|
| `fix_config(config, write_back=True, print_output=True)` | Fix misplaced/redundant options (path only — writes back to the file) → `FixReport` |
|
|
570
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 |
|
|
571
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` |
|
|
572
582
|
| `ConfigLinter()` | Collect lint issues programmatically: `lint_file(config)`, `lint_yaml_string(content)`; results accumulate in `.issues` |
|
|
573
583
|
| `ConfigFixer()` | Fix a config programmatically: `fix_yaml_string(content)` → `FixReport`; actions recorded in `.fixes` |
|
|
574
|
-
| `LintIssue(severity, message,
|
|
575
|
-
| `FixIssue(message,
|
|
584
|
+
| `LintIssue(severity, message, project_name=None, key=None)` | One lint finding |
|
|
585
|
+
| `FixIssue(message, project_name=None)` | One applied fix |
|
|
576
586
|
| `LintReport(issues)` | Lint results: `.issues`, `.errors`, `.warnings`, `.redundant`, `.is_valid`, `.format_text()` |
|
|
577
587
|
| `FixReport(fixes, content, is_valid, error=None)` | Fix results: `.fixes`, `.content`, `.is_valid`, `.error`, `.format_text()` |
|
|
578
588
|
| `LintSeverity` | Severity levels used by `LintIssue`: `LintSeverity.ERROR`, `LintSeverity.WARNING`, `LintSeverity.REDUNDANT` |
|
|
@@ -49,6 +49,15 @@ For development, install the local checkout:
|
|
|
49
49
|
pip install -e .
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
### Tests
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e .[dev] # pytest + pytest-cov
|
|
56
|
+
pytest # run the test suite
|
|
57
|
+
pytest --cov=gitacross # run with a coverage floor (90% line + branch)
|
|
58
|
+
ruff check src tests # lint
|
|
59
|
+
```
|
|
60
|
+
|
|
52
61
|
### Config
|
|
53
62
|
|
|
54
63
|
GitAcross reads a YAML file listing the mirrors you want. Each entry in the `projects` list is a **project**: it has a `source` (where releases come from) and a `target` (where they go).
|
|
@@ -74,7 +83,7 @@ gitacross --config config.yml --lint
|
|
|
74
83
|
gitacross --config config.yml
|
|
75
84
|
```
|
|
76
85
|
|
|
77
|
-
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.
|
|
78
87
|
|
|
79
88
|
## How it works
|
|
80
89
|
|
|
@@ -138,11 +147,11 @@ Copy the source release notes/body to the target release. Default `true`. Set at
|
|
|
138
147
|
|
|
139
148
|
#### `release_description`
|
|
140
149
|
|
|
141
|
-
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}`.
|
|
142
151
|
|
|
143
152
|
#### `commit_message`
|
|
144
153
|
|
|
145
|
-
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}`.
|
|
146
155
|
|
|
147
156
|
#### `sync_assets`
|
|
148
157
|
|
|
@@ -398,13 +407,13 @@ GitAcross can be driven from the command line or called directly from Python.
|
|
|
398
407
|
### CLI
|
|
399
408
|
|
|
400
409
|
```
|
|
401
|
-
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]
|
|
402
411
|
```
|
|
403
412
|
|
|
404
413
|
| Flag | Description |
|
|
405
414
|
|---|---|
|
|
406
415
|
| `--config PATH` | Config file to use (required) |
|
|
407
|
-
| `--project NAME` | Sync only
|
|
416
|
+
| `--project-name NAME` | Sync only the project with this name |
|
|
408
417
|
| `--workdir PATH` | Where state and cache live (default: `.gitsync`) |
|
|
409
418
|
| `--dry-run` | Preview changes without committing or pushing |
|
|
410
419
|
| `--reset` | Clear saved state and cache before running (fresh start) |
|
|
@@ -439,7 +448,7 @@ for r in results:
|
|
|
439
448
|
# Preview only — nothing is committed or pushed
|
|
440
449
|
results = gitacross.run(
|
|
441
450
|
"config.yml",
|
|
442
|
-
|
|
451
|
+
project_name="my-project",
|
|
443
452
|
dry_run=True,
|
|
444
453
|
work_dir="/data/custom_dir",
|
|
445
454
|
)
|
|
@@ -463,9 +472,9 @@ results = gitacross.run("config.yml", reset=True)
|
|
|
463
472
|
```python
|
|
464
473
|
config = gitacross.Config("config.yml")
|
|
465
474
|
|
|
466
|
-
for
|
|
467
|
-
if
|
|
468
|
-
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)
|
|
469
478
|
```
|
|
470
479
|
|
|
471
480
|
</details>
|
|
@@ -475,7 +484,7 @@ for project in config.projects:
|
|
|
475
484
|
|
|
476
485
|
```python
|
|
477
486
|
# ${VAR} tokens still resolve from the environment
|
|
478
|
-
|
|
487
|
+
project_config = gitacross.ProjectConfig({
|
|
479
488
|
"name": "my-project",
|
|
480
489
|
"source": {
|
|
481
490
|
"type": "gitea",
|
|
@@ -490,7 +499,7 @@ project = gitacross.ProjectConfig({
|
|
|
490
499
|
"token": "${GITHUB_TOKEN}",
|
|
491
500
|
},
|
|
492
501
|
})
|
|
493
|
-
gitacross.sync_project(
|
|
502
|
+
gitacross.sync_project(project_config, ".gitsync")
|
|
494
503
|
```
|
|
495
504
|
|
|
496
505
|
</details>
|
|
@@ -535,16 +544,16 @@ All public symbols are importable directly from `gitacross`:
|
|
|
535
544
|
|
|
536
545
|
| Symbol | What it does |
|
|
537
546
|
|---|---|
|
|
538
|
-
| `run(config,
|
|
539
|
-
| `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` |
|
|
540
549
|
| `lint_config(config, print_output=True)` | Lint a config (path or open file object) → `LintReport` |
|
|
541
550
|
| `fix_config(config, write_back=True, print_output=True)` | Fix misplaced/redundant options (path only — writes back to the file) → `FixReport` |
|
|
542
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 |
|
|
543
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` |
|
|
544
553
|
| `ConfigLinter()` | Collect lint issues programmatically: `lint_file(config)`, `lint_yaml_string(content)`; results accumulate in `.issues` |
|
|
545
554
|
| `ConfigFixer()` | Fix a config programmatically: `fix_yaml_string(content)` → `FixReport`; actions recorded in `.fixes` |
|
|
546
|
-
| `LintIssue(severity, message,
|
|
547
|
-
| `FixIssue(message,
|
|
555
|
+
| `LintIssue(severity, message, project_name=None, key=None)` | One lint finding |
|
|
556
|
+
| `FixIssue(message, project_name=None)` | One applied fix |
|
|
548
557
|
| `LintReport(issues)` | Lint results: `.issues`, `.errors`, `.warnings`, `.redundant`, `.is_valid`, `.format_text()` |
|
|
549
558
|
| `FixReport(fixes, content, is_valid, error=None)` | Fix results: `.fixes`, `.content`, `.is_valid`, `.error`, `.format_text()` |
|
|
550
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"
|
|
@@ -33,6 +33,7 @@ dependencies = [
|
|
|
33
33
|
[project.optional-dependencies]
|
|
34
34
|
dev = [
|
|
35
35
|
"pytest>=7.0",
|
|
36
|
+
"pytest-cov>=4.0",
|
|
36
37
|
"build",
|
|
37
38
|
"twine",
|
|
38
39
|
]
|
|
@@ -46,3 +47,15 @@ where = ["src"]
|
|
|
46
47
|
[tool.pytest.ini_options]
|
|
47
48
|
pythonpath = ["src"]
|
|
48
49
|
testpaths = ["tests"]
|
|
50
|
+
|
|
51
|
+
[tool.coverage.run]
|
|
52
|
+
source = ["gitacross"]
|
|
53
|
+
# Keep branch measurement on for the report, but don't gate on it —
|
|
54
|
+
# a 100% branch gate incentivizes line-touching tests over behavior tests.
|
|
55
|
+
branch = true
|
|
56
|
+
|
|
57
|
+
[tool.coverage.report]
|
|
58
|
+
# Enforcement floor with headroom: catches regressions without demanding
|
|
59
|
+
# 100% (which encourages tests-for-the-sake-of-it on defensive branches).
|
|
60
|
+
fail_under = 90
|
|
61
|
+
show_missing = true
|
|
@@ -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")
|
|
@@ -28,6 +28,7 @@ from importlib.metadata import PackageNotFoundError
|
|
|
28
28
|
from importlib.metadata import version as _installed_version
|
|
29
29
|
from pathlib import Path
|
|
30
30
|
|
|
31
|
+
from .cli import main # the CLI lives in cli.py; main.py re-exports it too
|
|
31
32
|
from .config import Config, ProjectConfig
|
|
32
33
|
from .linter import (
|
|
33
34
|
ConfigFixer,
|
|
@@ -40,7 +41,7 @@ from .linter import (
|
|
|
40
41
|
fix_config,
|
|
41
42
|
lint_config,
|
|
42
43
|
)
|
|
43
|
-
from .main import
|
|
44
|
+
from .main import run, sync_project
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def _package_version() -> str:
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Command-line interface for GitAcross.
|
|
2
|
+
|
|
3
|
+
The sync logic lives in :mod:`gitacross.main`; this module only parses
|
|
4
|
+
``argv`` and maps results to process exit codes.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import cast
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _setup_logging(verbose):
|
|
16
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
17
|
+
logging.basicConfig(
|
|
18
|
+
level=level,
|
|
19
|
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
20
|
+
datefmt="%H:%M:%S",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
from .main import DEFAULT_WORK_DIR, run
|
|
26
|
+
|
|
27
|
+
parser = argparse.ArgumentParser(description="Sync releases from source to target")
|
|
28
|
+
_ = parser.add_argument("--config", required=True, help="Path to config.yml")
|
|
29
|
+
_ = parser.add_argument("--project-name", help="Sync only this project (by name)")
|
|
30
|
+
_ = parser.add_argument(
|
|
31
|
+
"--dry-run", action="store_true", help="Print changes without pushing"
|
|
32
|
+
)
|
|
33
|
+
_ = parser.add_argument(
|
|
34
|
+
"--reset",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Clear state and cache before running (fresh start)",
|
|
37
|
+
)
|
|
38
|
+
_ = parser.add_argument(
|
|
39
|
+
"--lint",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help="Lint config file for YAML errors, invalid settings, and redundant options",
|
|
42
|
+
)
|
|
43
|
+
_ = parser.add_argument(
|
|
44
|
+
"--fix",
|
|
45
|
+
action="store_true",
|
|
46
|
+
help="Automatically fix misplaced keys and remove redundant options in config file",
|
|
47
|
+
)
|
|
48
|
+
_ = parser.add_argument(
|
|
49
|
+
"--workdir",
|
|
50
|
+
default=DEFAULT_WORK_DIR,
|
|
51
|
+
help="Directory for state.yml and cache/ (default: .gitsync)",
|
|
52
|
+
)
|
|
53
|
+
_ = parser.add_argument("-v", "--verbose", action="store_true", help="Debug logging")
|
|
54
|
+
args = parser.parse_args()
|
|
55
|
+
|
|
56
|
+
# Convert argparse's Any values into concrete types (cast is the escape hatch).
|
|
57
|
+
verbose = cast(bool, args.verbose)
|
|
58
|
+
config_path = Path(cast(str, args.config))
|
|
59
|
+
fix_flag = cast(bool, args.fix)
|
|
60
|
+
lint_flag = cast(bool, args.lint)
|
|
61
|
+
project_name = None if args.project_name is None else cast(str, args.project_name)
|
|
62
|
+
dry_run_flag = cast(bool, args.dry_run)
|
|
63
|
+
reset_flag = cast(bool, args.reset)
|
|
64
|
+
workdir = cast(str, args.workdir)
|
|
65
|
+
|
|
66
|
+
_setup_logging(verbose)
|
|
67
|
+
|
|
68
|
+
if fix_flag:
|
|
69
|
+
if not config_path.exists():
|
|
70
|
+
sys.exit(f"Config file not found: {config_path}")
|
|
71
|
+
from .linter import fix_config, lint_config
|
|
72
|
+
|
|
73
|
+
fix_report = fix_config(
|
|
74
|
+
config_path, write_back=not dry_run_flag, dry_run=dry_run_flag
|
|
75
|
+
)
|
|
76
|
+
if not fix_report.is_valid:
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
if lint_flag:
|
|
79
|
+
print()
|
|
80
|
+
lint_report = lint_config(config_path)
|
|
81
|
+
sys.exit(0 if lint_report.is_valid else 1)
|
|
82
|
+
sys.exit(0)
|
|
83
|
+
|
|
84
|
+
if lint_flag:
|
|
85
|
+
if not config_path.exists():
|
|
86
|
+
sys.exit(f"Config file not found: {config_path}")
|
|
87
|
+
from .linter import lint_config
|
|
88
|
+
|
|
89
|
+
report = lint_config(config_path)
|
|
90
|
+
sys.exit(0 if report.is_valid else 1)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
results = run(
|
|
94
|
+
config_path,
|
|
95
|
+
project_name=project_name,
|
|
96
|
+
dry_run=dry_run_flag,
|
|
97
|
+
reset=reset_flag,
|
|
98
|
+
work_dir=workdir,
|
|
99
|
+
)
|
|
100
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
101
|
+
sys.exit(str(exc))
|
|
102
|
+
|
|
103
|
+
if any(not r["synced"] for r in results):
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
|
|
@@ -7,6 +7,8 @@ from typing import final
|
|
|
7
7
|
|
|
8
8
|
import yaml
|
|
9
9
|
|
|
10
|
+
from .providers import REMOTE_TYPES
|
|
11
|
+
|
|
10
12
|
logger = logging.getLogger(__name__)
|
|
11
13
|
|
|
12
14
|
VALID_PROJECT_KEYS = {
|
|
@@ -20,10 +22,7 @@ VALID_PROJECT_KEYS = {
|
|
|
20
22
|
"sync_assets",
|
|
21
23
|
"stream_assets",
|
|
22
24
|
"commit_message",
|
|
23
|
-
"commit_template",
|
|
24
25
|
"release_description",
|
|
25
|
-
"release_notes_template",
|
|
26
|
-
"description_template",
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
KNOWN_SOURCE_KEYS = {
|
|
@@ -164,7 +163,12 @@ class _EndpointConfig:
|
|
|
164
163
|
|
|
165
164
|
@property
|
|
166
165
|
def is_remote(self):
|
|
167
|
-
return self.type in
|
|
166
|
+
return self.type in REMOTE_TYPES
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def repo_slug(self):
|
|
170
|
+
"""Filesystem-safe repo identifier (``owner/name`` → ``owner_name``)."""
|
|
171
|
+
return self.repo.replace("/", "_")
|
|
168
172
|
|
|
169
173
|
@property
|
|
170
174
|
def owner(self):
|
|
@@ -249,15 +253,10 @@ class ProjectConfig:
|
|
|
249
253
|
self.stream_assets = bool(raw.get("stream_assets", False))
|
|
250
254
|
|
|
251
255
|
# commit_message — project-level commit message template (e.g. "chore(sync): {tag}")
|
|
252
|
-
self.commit_message = raw.get("commit_message") or
|
|
256
|
+
self.commit_message = raw.get("commit_message") or None
|
|
253
257
|
|
|
254
258
|
# release_description — project-level release description template
|
|
255
|
-
self.release_description = (
|
|
256
|
-
raw.get("release_description")
|
|
257
|
-
or raw.get("release_notes_template")
|
|
258
|
-
or raw.get("description_template")
|
|
259
|
-
or None
|
|
260
|
-
)
|
|
259
|
+
self.release_description = raw.get("release_description") or None
|
|
261
260
|
|
|
262
261
|
def __repr__(self):
|
|
263
262
|
return (
|
|
@@ -302,9 +301,8 @@ def _cascade(raw_project, raw_source, raw_target, keys, default):
|
|
|
302
301
|
"""Return the first value found for any of *keys* across project, source, target.
|
|
303
302
|
|
|
304
303
|
Lookup order: project-level first (highest priority), then source-level,
|
|
305
|
-
then target-level, then *default*.
|
|
306
|
-
level
|
|
307
|
-
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.
|
|
308
306
|
"""
|
|
309
307
|
for raw in (raw_project, raw_source, raw_target):
|
|
310
308
|
for k in keys:
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Config linter and fixer for GitAcross.
|
|
2
|
+
|
|
3
|
+
``lint_config`` validates a config; ``fix_config`` rewrites it in place to
|
|
4
|
+
remove redundant and misplaced options.
|
|
5
|
+
|
|
6
|
+
Implementation lives in three modules:
|
|
7
|
+
|
|
8
|
+
* :mod:`gitacross.linter.models` — report/issue data types and YAML dump helper
|
|
9
|
+
* :mod:`gitacross.linter.validator` — :class:`ConfigLinter` (the checks)
|
|
10
|
+
* :mod:`gitacross.linter.fixer` — :class:`ConfigFixer` (the rewrites)
|
|
11
|
+
"""
|
|
12
|
+
from .fixer import ConfigFixer, fix_config
|
|
13
|
+
from .models import FixIssue, FixReport, LintIssue, LintReport, LintSeverity
|
|
14
|
+
from .validator import ConfigLinter, lint_config
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ConfigFixer",
|
|
18
|
+
"ConfigLinter",
|
|
19
|
+
"FixIssue",
|
|
20
|
+
"FixReport",
|
|
21
|
+
"LintIssue",
|
|
22
|
+
"LintReport",
|
|
23
|
+
"LintSeverity",
|
|
24
|
+
"fix_config",
|
|
25
|
+
"lint_config",
|
|
26
|
+
]
|