path-sync 0.5.0__tar.gz → 0.6.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.
- {path_sync-0.5.0 → path_sync-0.6.0}/PKG-INFO +69 -4
- {path_sync-0.5.0 → path_sync-0.6.0}/README.md +68 -3
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/__init__.py +1 -1
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/cmd_copy.py +73 -29
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/cmd_dep_update.py +12 -54
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/models.py +32 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/models_dep.py +7 -24
- path_sync-0.6.0/path_sync/_internal/verify.py +97 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/dep_update.py +7 -7
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/sections.py +19 -1
- {path_sync-0.5.0 → path_sync-0.6.0}/pyproject.toml +1 -1
- {path_sync-0.5.0 → path_sync-0.6.0}/.gitignore +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/LICENSE +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/__main__.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/__init__.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/cmd_boot.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/cmd_options.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/cmd_validate.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/file_utils.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/git_ops.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/header.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/log_capture.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/prompt_utils.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/typer_app.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/validation.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/_internal/yaml_utils.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/config.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/copy.py +0 -0
- {path_sync-0.5.0 → path_sync-0.6.0}/path_sync/validate_no_changes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: path-sync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Sync files from a source repo to multiple destination repos
|
|
5
5
|
Author-email: EspenAlbert <espen.albert1@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -77,13 +77,14 @@ By default, prompts before each git operation. See [Usage Scenarios](#usage-scen
|
|
|
77
77
|
| `-d dest1,dest2` | Filter specific destinations |
|
|
78
78
|
| `--dry-run` | Preview without writing (requires existing repos) |
|
|
79
79
|
| `-y, --no-prompt` | Skip confirmations (for CI) |
|
|
80
|
-
| `--
|
|
80
|
+
| `--skip-commit` | No git ops after sync (no commit/push/PR). Alias: `--local` |
|
|
81
81
|
| `--no-checkout` | Skip branch switching (assumes already on correct branch) |
|
|
82
82
|
| `--checkout-from-default` | Reset to origin/default before sync |
|
|
83
83
|
| `--no-pr` | Push but skip PR creation |
|
|
84
84
|
| `--force-overwrite` | Overwrite files even if header removed (opted out) |
|
|
85
85
|
| `--detailed-exit-code` | Exit 0=no changes, 1=changes, 2=error |
|
|
86
86
|
| `--skip-orphan-cleanup` | Skip deletion of orphaned synced files |
|
|
87
|
+
| `--skip-verify` | Skip verification steps after syncing |
|
|
87
88
|
| `--pr-title` | Override PR title (supports `{name}`, `{dest_name}`) |
|
|
88
89
|
| `--pr-labels` | Comma-separated PR labels |
|
|
89
90
|
| `--pr-reviewers` | Comma-separated PR reviewers |
|
|
@@ -106,12 +107,12 @@ Options:
|
|
|
106
107
|
| Interactive sync | `copy -n cfg` |
|
|
107
108
|
| CI fresh sync | `copy -n cfg --checkout-from-default -y` |
|
|
108
109
|
| Local preview | `copy -n cfg --dry-run` |
|
|
109
|
-
| Local test files | `copy -n cfg --
|
|
110
|
+
| Local test files | `copy -n cfg --skip-commit` |
|
|
110
111
|
| Already on branch | `copy -n cfg --no-checkout` |
|
|
111
112
|
| Push, manual PR | `copy -n cfg --no-pr -y` |
|
|
112
113
|
| Force opted-out | `copy -n cfg --force-overwrite` |
|
|
113
114
|
|
|
114
|
-
**Interactive prompt behavior**:
|
|
115
|
+
**Interactive prompt behavior**: Each git operation (checkout, commit, push, PR) prompts independently. Use `--no-checkout` to skip the branch switch prompt. Use `--skip-commit` to skip all git operations after sync.
|
|
115
116
|
|
|
116
117
|
## Section Markers
|
|
117
118
|
|
|
@@ -139,6 +140,36 @@ destinations:
|
|
|
139
140
|
justfile: [coverage] # keep local coverage recipe
|
|
140
141
|
```
|
|
141
142
|
|
|
143
|
+
## Wrapping Synced Files
|
|
144
|
+
|
|
145
|
+
For files without section markers, `wrap_synced_files` automatically wraps content in a `synced` section. This lets destinations add content before/after the synced content.
|
|
146
|
+
|
|
147
|
+
**Without wrapping** (default):
|
|
148
|
+
```python
|
|
149
|
+
# path-sync copy -n myconfig
|
|
150
|
+
def hello():
|
|
151
|
+
pass
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**With wrapping** (`wrap_synced_files: true`):
|
|
155
|
+
```python
|
|
156
|
+
# path-sync copy -n myconfig
|
|
157
|
+
# === DO_NOT_EDIT: path-sync synced ===
|
|
158
|
+
def hello():
|
|
159
|
+
pass
|
|
160
|
+
# === OK_EDIT: path-sync synced ===
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Destinations can add content outside the section markers. Per-path override via `wrap: false`:
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
wrap_synced_files: true
|
|
167
|
+
paths:
|
|
168
|
+
- src_path: templates/base.py # wrapped
|
|
169
|
+
- src_path: .editorconfig
|
|
170
|
+
wrap: false # not wrapped
|
|
171
|
+
```
|
|
172
|
+
|
|
142
173
|
## Skipping Files per Destination
|
|
143
174
|
|
|
144
175
|
Use `skip_file_patterns` to exclude files for specific destinations. Patterns match against the **destination path** (after `dest_path` remapping):
|
|
@@ -195,6 +226,8 @@ destinations:
|
|
|
195
226
|
| `destinations` | Target repos with sync settings |
|
|
196
227
|
| `header_config` | Comment style per extension (has defaults) |
|
|
197
228
|
| `pr_defaults` | PR title, labels, reviewers, assignees |
|
|
229
|
+
| `wrap_synced_files` | Wrap synced files in section markers (default: `false`) |
|
|
230
|
+
| `verify` | Verification steps to run after syncing (see [Verify Steps](#verify-steps-in-copy)) |
|
|
198
231
|
|
|
199
232
|
**Path options**:
|
|
200
233
|
|
|
@@ -205,6 +238,7 @@ destinations:
|
|
|
205
238
|
| `sync_mode` | `sync` (default), `replace`, or `scaffold` |
|
|
206
239
|
| `exclude_dirs` | Directory names to skip (defaults: `__pycache__`, `.git`, `.venv`, etc.) |
|
|
207
240
|
| `exclude_file_patterns` | Filename patterns to skip, supports globs (`*.pyc`, `test_*.py`) |
|
|
241
|
+
| `wrap` | Override global `wrap_synced_files` for this path (`true`/`false`) |
|
|
208
242
|
|
|
209
243
|
**Destination options**:
|
|
210
244
|
|
|
@@ -217,6 +251,37 @@ destinations:
|
|
|
217
251
|
| `default_branch` | Default branch to compare against (defaults to `main`) |
|
|
218
252
|
| `skip_sections` | Map of `{dest_path: [section_ids]}` to preserve locally |
|
|
219
253
|
| `skip_file_patterns` | Patterns to skip for this destination (matches dest path, fnmatch syntax) |
|
|
254
|
+
| `verify` | Per-destination verify config (overrides source-level verify) |
|
|
255
|
+
|
|
256
|
+
## Verify Steps in Copy
|
|
257
|
+
|
|
258
|
+
Run verification steps after syncing files. Synced files are committed first, then verify steps run and can make additional commits.
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
name: myconfig
|
|
262
|
+
verify:
|
|
263
|
+
on_fail: warn # default: warn (also: skip, fail)
|
|
264
|
+
steps:
|
|
265
|
+
- run: just fmt
|
|
266
|
+
commit:
|
|
267
|
+
message: "style: format synced files"
|
|
268
|
+
add_paths: ["."]
|
|
269
|
+
on_fail: warn
|
|
270
|
+
- run: just test
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Per-destination override:
|
|
274
|
+
|
|
275
|
+
```yaml
|
|
276
|
+
destinations:
|
|
277
|
+
- name: dest1
|
|
278
|
+
dest_path_relative: ../dest1
|
|
279
|
+
verify:
|
|
280
|
+
steps:
|
|
281
|
+
- run: npm run build
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Use `--skip-verify` to disable verification steps.
|
|
220
285
|
|
|
221
286
|
## Header Format
|
|
222
287
|
|
|
@@ -59,13 +59,14 @@ By default, prompts before each git operation. See [Usage Scenarios](#usage-scen
|
|
|
59
59
|
| `-d dest1,dest2` | Filter specific destinations |
|
|
60
60
|
| `--dry-run` | Preview without writing (requires existing repos) |
|
|
61
61
|
| `-y, --no-prompt` | Skip confirmations (for CI) |
|
|
62
|
-
| `--
|
|
62
|
+
| `--skip-commit` | No git ops after sync (no commit/push/PR). Alias: `--local` |
|
|
63
63
|
| `--no-checkout` | Skip branch switching (assumes already on correct branch) |
|
|
64
64
|
| `--checkout-from-default` | Reset to origin/default before sync |
|
|
65
65
|
| `--no-pr` | Push but skip PR creation |
|
|
66
66
|
| `--force-overwrite` | Overwrite files even if header removed (opted out) |
|
|
67
67
|
| `--detailed-exit-code` | Exit 0=no changes, 1=changes, 2=error |
|
|
68
68
|
| `--skip-orphan-cleanup` | Skip deletion of orphaned synced files |
|
|
69
|
+
| `--skip-verify` | Skip verification steps after syncing |
|
|
69
70
|
| `--pr-title` | Override PR title (supports `{name}`, `{dest_name}`) |
|
|
70
71
|
| `--pr-labels` | Comma-separated PR labels |
|
|
71
72
|
| `--pr-reviewers` | Comma-separated PR reviewers |
|
|
@@ -88,12 +89,12 @@ Options:
|
|
|
88
89
|
| Interactive sync | `copy -n cfg` |
|
|
89
90
|
| CI fresh sync | `copy -n cfg --checkout-from-default -y` |
|
|
90
91
|
| Local preview | `copy -n cfg --dry-run` |
|
|
91
|
-
| Local test files | `copy -n cfg --
|
|
92
|
+
| Local test files | `copy -n cfg --skip-commit` |
|
|
92
93
|
| Already on branch | `copy -n cfg --no-checkout` |
|
|
93
94
|
| Push, manual PR | `copy -n cfg --no-pr -y` |
|
|
94
95
|
| Force opted-out | `copy -n cfg --force-overwrite` |
|
|
95
96
|
|
|
96
|
-
**Interactive prompt behavior**:
|
|
97
|
+
**Interactive prompt behavior**: Each git operation (checkout, commit, push, PR) prompts independently. Use `--no-checkout` to skip the branch switch prompt. Use `--skip-commit` to skip all git operations after sync.
|
|
97
98
|
|
|
98
99
|
## Section Markers
|
|
99
100
|
|
|
@@ -121,6 +122,36 @@ destinations:
|
|
|
121
122
|
justfile: [coverage] # keep local coverage recipe
|
|
122
123
|
```
|
|
123
124
|
|
|
125
|
+
## Wrapping Synced Files
|
|
126
|
+
|
|
127
|
+
For files without section markers, `wrap_synced_files` automatically wraps content in a `synced` section. This lets destinations add content before/after the synced content.
|
|
128
|
+
|
|
129
|
+
**Without wrapping** (default):
|
|
130
|
+
```python
|
|
131
|
+
# path-sync copy -n myconfig
|
|
132
|
+
def hello():
|
|
133
|
+
pass
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**With wrapping** (`wrap_synced_files: true`):
|
|
137
|
+
```python
|
|
138
|
+
# path-sync copy -n myconfig
|
|
139
|
+
# === DO_NOT_EDIT: path-sync synced ===
|
|
140
|
+
def hello():
|
|
141
|
+
pass
|
|
142
|
+
# === OK_EDIT: path-sync synced ===
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Destinations can add content outside the section markers. Per-path override via `wrap: false`:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
wrap_synced_files: true
|
|
149
|
+
paths:
|
|
150
|
+
- src_path: templates/base.py # wrapped
|
|
151
|
+
- src_path: .editorconfig
|
|
152
|
+
wrap: false # not wrapped
|
|
153
|
+
```
|
|
154
|
+
|
|
124
155
|
## Skipping Files per Destination
|
|
125
156
|
|
|
126
157
|
Use `skip_file_patterns` to exclude files for specific destinations. Patterns match against the **destination path** (after `dest_path` remapping):
|
|
@@ -177,6 +208,8 @@ destinations:
|
|
|
177
208
|
| `destinations` | Target repos with sync settings |
|
|
178
209
|
| `header_config` | Comment style per extension (has defaults) |
|
|
179
210
|
| `pr_defaults` | PR title, labels, reviewers, assignees |
|
|
211
|
+
| `wrap_synced_files` | Wrap synced files in section markers (default: `false`) |
|
|
212
|
+
| `verify` | Verification steps to run after syncing (see [Verify Steps](#verify-steps-in-copy)) |
|
|
180
213
|
|
|
181
214
|
**Path options**:
|
|
182
215
|
|
|
@@ -187,6 +220,7 @@ destinations:
|
|
|
187
220
|
| `sync_mode` | `sync` (default), `replace`, or `scaffold` |
|
|
188
221
|
| `exclude_dirs` | Directory names to skip (defaults: `__pycache__`, `.git`, `.venv`, etc.) |
|
|
189
222
|
| `exclude_file_patterns` | Filename patterns to skip, supports globs (`*.pyc`, `test_*.py`) |
|
|
223
|
+
| `wrap` | Override global `wrap_synced_files` for this path (`true`/`false`) |
|
|
190
224
|
|
|
191
225
|
**Destination options**:
|
|
192
226
|
|
|
@@ -199,6 +233,37 @@ destinations:
|
|
|
199
233
|
| `default_branch` | Default branch to compare against (defaults to `main`) |
|
|
200
234
|
| `skip_sections` | Map of `{dest_path: [section_ids]}` to preserve locally |
|
|
201
235
|
| `skip_file_patterns` | Patterns to skip for this destination (matches dest path, fnmatch syntax) |
|
|
236
|
+
| `verify` | Per-destination verify config (overrides source-level verify) |
|
|
237
|
+
|
|
238
|
+
## Verify Steps in Copy
|
|
239
|
+
|
|
240
|
+
Run verification steps after syncing files. Synced files are committed first, then verify steps run and can make additional commits.
|
|
241
|
+
|
|
242
|
+
```yaml
|
|
243
|
+
name: myconfig
|
|
244
|
+
verify:
|
|
245
|
+
on_fail: warn # default: warn (also: skip, fail)
|
|
246
|
+
steps:
|
|
247
|
+
- run: just fmt
|
|
248
|
+
commit:
|
|
249
|
+
message: "style: format synced files"
|
|
250
|
+
add_paths: ["."]
|
|
251
|
+
on_fail: warn
|
|
252
|
+
- run: just test
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Per-destination override:
|
|
256
|
+
|
|
257
|
+
```yaml
|
|
258
|
+
destinations:
|
|
259
|
+
- name: dest1
|
|
260
|
+
dest_path_relative: ../dest1
|
|
261
|
+
verify:
|
|
262
|
+
steps:
|
|
263
|
+
- run: npm run build
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Use `--skip-verify` to disable verification steps.
|
|
202
267
|
|
|
203
268
|
## Header Format
|
|
204
269
|
|
|
@@ -10,7 +10,7 @@ import typer
|
|
|
10
10
|
from pydantic import BaseModel
|
|
11
11
|
|
|
12
12
|
from path_sync import sections
|
|
13
|
-
from path_sync._internal import cmd_options, git_ops, header, prompt_utils
|
|
13
|
+
from path_sync._internal import cmd_options, git_ops, header, prompt_utils, verify
|
|
14
14
|
from path_sync._internal.file_utils import ensure_parents_write_text
|
|
15
15
|
from path_sync._internal.log_capture import capture_log
|
|
16
16
|
from path_sync._internal.models import (
|
|
@@ -22,6 +22,7 @@ from path_sync._internal.models import (
|
|
|
22
22
|
resolve_config_path,
|
|
23
23
|
)
|
|
24
24
|
from path_sync._internal.typer_app import app
|
|
25
|
+
from path_sync._internal.verify import StepFailure, VerifyResult, VerifyStatus
|
|
25
26
|
from path_sync._internal.yaml_utils import load_yaml_model
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
@@ -47,10 +48,11 @@ class CopyOptions(BaseModel):
|
|
|
47
48
|
force_overwrite: bool = False
|
|
48
49
|
no_checkout: bool = False
|
|
49
50
|
checkout_from_default: bool = False
|
|
50
|
-
|
|
51
|
+
skip_commit: bool = False
|
|
51
52
|
no_prompt: bool = False
|
|
52
53
|
no_pr: bool = False
|
|
53
54
|
skip_orphan_cleanup: bool = False
|
|
55
|
+
skip_verify: bool = False
|
|
54
56
|
pr_title: str = ""
|
|
55
57
|
labels: list[str] | None = None
|
|
56
58
|
reviewers: list[str] | None = None
|
|
@@ -93,8 +95,9 @@ def copy(
|
|
|
93
95
|
"--checkout-from-default",
|
|
94
96
|
help="Reset to origin/default before sync (for CI)",
|
|
95
97
|
),
|
|
96
|
-
|
|
98
|
+
skip_commit: bool = typer.Option(
|
|
97
99
|
False,
|
|
100
|
+
"--skip-commit",
|
|
98
101
|
"--local",
|
|
99
102
|
help="No git operations after sync (no commit/push/PR)",
|
|
100
103
|
),
|
|
@@ -122,6 +125,11 @@ def copy(
|
|
|
122
125
|
"--skip-orphan-cleanup",
|
|
123
126
|
help="Skip deletion of orphaned synced files",
|
|
124
127
|
),
|
|
128
|
+
skip_verify: bool = typer.Option(
|
|
129
|
+
False,
|
|
130
|
+
"--skip-verify",
|
|
131
|
+
help="Skip verification steps after syncing",
|
|
132
|
+
),
|
|
125
133
|
) -> None:
|
|
126
134
|
"""Copy files from SRC to DEST repositories."""
|
|
127
135
|
if name and config_path_opt:
|
|
@@ -148,10 +156,11 @@ def copy(
|
|
|
148
156
|
force_overwrite=force_overwrite,
|
|
149
157
|
no_checkout=no_checkout,
|
|
150
158
|
checkout_from_default=checkout_from_default,
|
|
151
|
-
|
|
159
|
+
skip_commit=skip_commit,
|
|
152
160
|
no_prompt=no_prompt,
|
|
153
161
|
no_pr=no_pr,
|
|
154
162
|
skip_orphan_cleanup=skip_orphan_cleanup,
|
|
163
|
+
skip_verify=skip_verify,
|
|
155
164
|
pr_title=pr_title or config.pr_defaults.title,
|
|
156
165
|
labels=cmd_options.split_csv(pr_labels) or config.pr_defaults.labels,
|
|
157
166
|
reviewers=cmd_options.split_csv(pr_reviewers) or config.pr_defaults.reviewers,
|
|
@@ -196,23 +205,14 @@ def _sync_destination(
|
|
|
196
205
|
dest_repo = _ensure_dest_repo(dest, dest_root, opts.dry_run)
|
|
197
206
|
copy_branch = dest.resolved_copy_branch(config.name)
|
|
198
207
|
|
|
199
|
-
# --no-checkout
|
|
200
|
-
|
|
201
|
-
if opts.dry_run:
|
|
202
|
-
skip_git_ops = True
|
|
203
|
-
elif opts.no_checkout:
|
|
204
|
-
skip_git_ops = False
|
|
205
|
-
elif prompt_utils.prompt_confirm(f"Switch {dest.name} to {copy_branch}?", opts.no_prompt):
|
|
208
|
+
# --no-checkout skips branch switching (assumes already on correct branch)
|
|
209
|
+
if not opts.no_checkout and prompt_utils.prompt_confirm(f"Switch {dest.name} to {copy_branch}?", opts.no_prompt):
|
|
206
210
|
git_ops.prepare_copy_branch(
|
|
207
211
|
repo=dest_repo,
|
|
208
212
|
default_branch=dest.default_branch,
|
|
209
213
|
copy_branch=copy_branch,
|
|
210
214
|
from_default=opts.checkout_from_default,
|
|
211
215
|
)
|
|
212
|
-
skip_git_ops = False
|
|
213
|
-
else:
|
|
214
|
-
skip_git_ops = True
|
|
215
|
-
|
|
216
216
|
result = _sync_paths(config, dest, src_root, dest_root, opts)
|
|
217
217
|
_print_sync_summary(dest, result)
|
|
218
218
|
|
|
@@ -220,10 +220,29 @@ def _sync_destination(
|
|
|
220
220
|
logger.info(f"{dest.name}: No changes")
|
|
221
221
|
return 0
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
# --skip-commit and --dry-run skip commit; otherwise prompt
|
|
224
|
+
should_skip_commit = opts.skip_commit or opts.dry_run
|
|
225
|
+
if not should_skip_commit and prompt_utils.prompt_confirm(f"Commit changes to {dest.name}?", opts.no_prompt):
|
|
226
|
+
sync_commit_msg = f"chore: sync {config.name} from {current_sha[:8]}"
|
|
227
|
+
git_ops.commit_changes(dest_repo, sync_commit_msg)
|
|
228
|
+
|
|
229
|
+
verify_result = VerifyResult()
|
|
230
|
+
effective_verify = dest.resolve_verify(config.verify)
|
|
231
|
+
if not opts.skip_verify and effective_verify.steps:
|
|
232
|
+
verify_result = verify.run_verify_steps(
|
|
233
|
+
dest_repo, dest_root, effective_verify, dry_run=opts.dry_run, skip_commit=opts.skip_commit
|
|
234
|
+
)
|
|
235
|
+
verify.log_verify_summary(dest.name, verify_result)
|
|
236
|
+
|
|
237
|
+
if verify_result.status == VerifyStatus.FAILED:
|
|
238
|
+
logger.error(f"{dest.name}: Verification failed, stopping")
|
|
239
|
+
raise typer.Exit(EXIT_ERROR)
|
|
240
|
+
|
|
241
|
+
if verify_result.status == VerifyStatus.SKIPPED:
|
|
242
|
+
logger.warning(f"{dest.name}: Verification skipped due to failure")
|
|
243
|
+
return result.total
|
|
225
244
|
|
|
226
|
-
|
|
245
|
+
_push_and_pr(config, dest_repo, dest_root, dest, current_sha, src_repo_url, opts, read_log, verify_result)
|
|
227
246
|
return result.total
|
|
228
247
|
|
|
229
248
|
|
|
@@ -264,6 +283,7 @@ def _sync_paths(
|
|
|
264
283
|
config.name,
|
|
265
284
|
opts.dry_run,
|
|
266
285
|
opts.force_overwrite,
|
|
286
|
+
config.wrap_synced_files,
|
|
267
287
|
)
|
|
268
288
|
result.content_changes += changes
|
|
269
289
|
result.synced_paths.update(paths)
|
|
@@ -314,10 +334,12 @@ def _sync_path(
|
|
|
314
334
|
config_name: str,
|
|
315
335
|
dry_run: bool,
|
|
316
336
|
force_overwrite: bool,
|
|
337
|
+
wrap_synced_files: bool = False,
|
|
317
338
|
) -> tuple[int, set[Path]]:
|
|
318
339
|
changes = 0
|
|
319
340
|
synced: set[Path] = set()
|
|
320
341
|
|
|
342
|
+
should_wrap = mapping.should_wrap(wrap_synced_files)
|
|
321
343
|
for src_path, dest_key, dest_path in _iter_sync_files(mapping, src_root, dest_root):
|
|
322
344
|
if dest.is_skipped(dest_key):
|
|
323
345
|
continue
|
|
@@ -330,6 +352,7 @@ def _sync_path(
|
|
|
330
352
|
mapping.sync_mode,
|
|
331
353
|
dry_run,
|
|
332
354
|
force_overwrite,
|
|
355
|
+
should_wrap,
|
|
333
356
|
)
|
|
334
357
|
synced.add(dest_path)
|
|
335
358
|
|
|
@@ -345,6 +368,7 @@ def _copy_file(
|
|
|
345
368
|
sync_mode: SyncMode,
|
|
346
369
|
dry_run: bool,
|
|
347
370
|
force_overwrite: bool = False,
|
|
371
|
+
should_wrap: bool = False,
|
|
348
372
|
) -> int:
|
|
349
373
|
try:
|
|
350
374
|
src_content = header.remove_header(src.read_text())
|
|
@@ -358,7 +382,7 @@ def _copy_file(
|
|
|
358
382
|
return _handle_replace(src_content, dest_path, dry_run)
|
|
359
383
|
case SyncMode.SYNC:
|
|
360
384
|
skip_list = dest.skip_sections.get(dest_key, [])
|
|
361
|
-
return _handle_sync(src_content, dest_path, skip_list, config_name, dry_run, force_overwrite)
|
|
385
|
+
return _handle_sync(src_content, dest_path, skip_list, config_name, dry_run, force_overwrite, should_wrap)
|
|
362
386
|
|
|
363
387
|
|
|
364
388
|
def _copy_binary_file(src: Path, dest_path: Path, sync_mode: SyncMode, dry_run: bool) -> int:
|
|
@@ -402,10 +426,15 @@ def _handle_sync(
|
|
|
402
426
|
config_name: str,
|
|
403
427
|
dry_run: bool,
|
|
404
428
|
force_overwrite: bool,
|
|
429
|
+
should_wrap: bool = False,
|
|
405
430
|
) -> int:
|
|
406
431
|
if sections.has_sections(src_content, dest_path):
|
|
407
432
|
return _handle_sync_sections(src_content, dest_path, skip_list, config_name, dry_run, force_overwrite)
|
|
408
433
|
|
|
434
|
+
if should_wrap:
|
|
435
|
+
wrapped = sections.wrap_in_synced_section(src_content, dest_path)
|
|
436
|
+
return _handle_sync_sections(wrapped, dest_path, skip_list, config_name, dry_run, force_overwrite)
|
|
437
|
+
|
|
409
438
|
if dest_path.exists():
|
|
410
439
|
existing = dest_path.read_text()
|
|
411
440
|
has_hdr = header.has_header(existing)
|
|
@@ -436,7 +465,7 @@ def _handle_sync_sections(
|
|
|
436
465
|
dry_run: bool,
|
|
437
466
|
force_overwrite: bool,
|
|
438
467
|
) -> int:
|
|
439
|
-
src_sections = sections.
|
|
468
|
+
src_sections = sections.parse_sections(src_content, dest_path)
|
|
440
469
|
|
|
441
470
|
if dest_path.exists():
|
|
442
471
|
existing = dest_path.read_text()
|
|
@@ -445,6 +474,9 @@ def _handle_sync_sections(
|
|
|
445
474
|
return 0
|
|
446
475
|
dest_body = header.remove_header(existing)
|
|
447
476
|
new_body = sections.replace_sections(dest_body, src_sections, dest_path, skip_list)
|
|
477
|
+
elif skip_list:
|
|
478
|
+
filtered = [s for s in src_sections if s.id not in skip_list]
|
|
479
|
+
new_body = sections.build_sections_content(filtered, dest_path)
|
|
448
480
|
else:
|
|
449
481
|
new_body = src_content
|
|
450
482
|
|
|
@@ -484,7 +516,7 @@ def _find_files_with_config(dest_root: Path, config_name: str) -> list[Path]:
|
|
|
484
516
|
return result
|
|
485
517
|
|
|
486
518
|
|
|
487
|
-
def
|
|
519
|
+
def _push_and_pr(
|
|
488
520
|
config: SrcConfig,
|
|
489
521
|
repo,
|
|
490
522
|
dest_root: Path,
|
|
@@ -493,19 +525,21 @@ def _commit_and_pr(
|
|
|
493
525
|
src_repo_url: str,
|
|
494
526
|
opts: CopyOptions,
|
|
495
527
|
read_log: Callable[[], str],
|
|
528
|
+
verify_result: VerifyResult,
|
|
496
529
|
) -> None:
|
|
497
|
-
if opts.
|
|
498
|
-
logger.info("
|
|
530
|
+
if opts.skip_commit or opts.dry_run:
|
|
531
|
+
logger.info("Skipping push/PR (--skip-commit or --dry-run)")
|
|
499
532
|
return
|
|
500
533
|
|
|
501
534
|
copy_branch = dest.resolved_copy_branch(config.name)
|
|
502
535
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
536
|
+
# Commit any remaining changes from verify steps without their own commit config
|
|
537
|
+
if git_ops.has_changes(repo):
|
|
538
|
+
if not prompt_utils.prompt_confirm(f"Commit remaining changes to {dest.name}?", opts.no_prompt):
|
|
539
|
+
return
|
|
540
|
+
commit_msg = f"chore: post-sync changes for {config.name}"
|
|
541
|
+
git_ops.commit_changes(repo, commit_msg)
|
|
542
|
+
typer.echo(f" Committed: {commit_msg}", err=True)
|
|
509
543
|
|
|
510
544
|
if not prompt_utils.prompt_confirm(f"Push {dest.name} to origin?", opts.no_prompt):
|
|
511
545
|
return
|
|
@@ -524,6 +558,9 @@ def _commit_and_pr(
|
|
|
524
558
|
dest_name=dest.name,
|
|
525
559
|
)
|
|
526
560
|
|
|
561
|
+
if verify_result.failures:
|
|
562
|
+
pr_body = _append_verify_warnings(pr_body, verify_result.failures)
|
|
563
|
+
|
|
527
564
|
title = opts.pr_title.format(name=config.name, dest_name=dest.name)
|
|
528
565
|
pr_url = git_ops.create_or_update_pr(
|
|
529
566
|
dest_root,
|
|
@@ -536,3 +573,10 @@ def _commit_and_pr(
|
|
|
536
573
|
)
|
|
537
574
|
if pr_url:
|
|
538
575
|
typer.echo(f" Created PR: {pr_url}", err=True)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _append_verify_warnings(body: str, failures: list[StepFailure]) -> str:
|
|
579
|
+
body += "\n\n---\n## Verification Warnings\n"
|
|
580
|
+
for f in failures:
|
|
581
|
+
body += f"\n- `{f.step}` failed (exit code {f.returncode}, strategy: {f.on_fail})"
|
|
582
|
+
return body
|
|
@@ -10,17 +10,16 @@ from pathlib import Path
|
|
|
10
10
|
import typer
|
|
11
11
|
from git import Repo
|
|
12
12
|
|
|
13
|
-
from path_sync._internal import cmd_options, git_ops, prompt_utils
|
|
13
|
+
from path_sync._internal import cmd_options, git_ops, prompt_utils, verify
|
|
14
14
|
from path_sync._internal.log_capture import capture_log
|
|
15
|
-
from path_sync._internal.models import Destination, find_repo_root
|
|
15
|
+
from path_sync._internal.models import Destination, OnFailStrategy, find_repo_root
|
|
16
16
|
from path_sync._internal.models_dep import (
|
|
17
17
|
DepConfig,
|
|
18
|
-
OnFailStrategy,
|
|
19
18
|
UpdateEntry,
|
|
20
|
-
VerifyConfig,
|
|
21
19
|
resolve_dep_config_path,
|
|
22
20
|
)
|
|
23
21
|
from path_sync._internal.typer_app import app
|
|
22
|
+
from path_sync._internal.verify import StepFailure, VerifyStatus
|
|
24
23
|
from path_sync._internal.yaml_utils import load_yaml_model
|
|
25
24
|
|
|
26
25
|
logger = logging.getLogger(__name__)
|
|
@@ -33,12 +32,9 @@ class Status(StrEnum):
|
|
|
33
32
|
NO_CHANGES = "no_changes"
|
|
34
33
|
FAILED = "failed"
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
step: str
|
|
40
|
-
returncode: int
|
|
41
|
-
on_fail: OnFailStrategy
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_verify_status(cls, vs: VerifyStatus) -> Status:
|
|
37
|
+
return cls(vs.value)
|
|
42
38
|
|
|
43
39
|
|
|
44
40
|
@dataclass
|
|
@@ -163,15 +159,17 @@ def _process_single_repo_inner(
|
|
|
163
159
|
def _run_updates(updates: list[UpdateEntry], repo_path: Path) -> StepFailure | None:
|
|
164
160
|
try:
|
|
165
161
|
for update in updates:
|
|
166
|
-
|
|
162
|
+
verify.run_command(update.command, repo_path / update.workdir)
|
|
167
163
|
return None
|
|
168
164
|
except subprocess.CalledProcessError as e:
|
|
169
165
|
return StepFailure(step=e.cmd, returncode=e.returncode, on_fail=OnFailStrategy.SKIP)
|
|
170
166
|
|
|
171
167
|
|
|
172
|
-
def _verify_repo(repo: Repo, repo_path: Path,
|
|
173
|
-
|
|
174
|
-
|
|
168
|
+
def _verify_repo(repo: Repo, repo_path: Path, fallback_verify: verify.VerifyConfig, dest: Destination) -> RepoResult:
|
|
169
|
+
effective_verify = dest.resolve_verify(fallback_verify)
|
|
170
|
+
result = verify.run_verify_steps(repo, repo_path, effective_verify)
|
|
171
|
+
status = Status.from_verify_status(result.status)
|
|
172
|
+
return RepoResult(dest=dest, repo_path=repo_path, status=status, failures=result.failures)
|
|
175
173
|
|
|
176
174
|
|
|
177
175
|
def _create_prs(config: DepConfig, results: list[RepoResult], opts: DepUpdateOptions) -> None:
|
|
@@ -223,46 +221,6 @@ def _ensure_repo(dest: Destination, repo_path: Path, default_branch: str) -> Rep
|
|
|
223
221
|
return git_ops.clone_repo(dest.repo_url, repo_path)
|
|
224
222
|
|
|
225
223
|
|
|
226
|
-
def _run_command(cmd: str, cwd: Path) -> None:
|
|
227
|
-
logger.info(f"Running: {cmd}")
|
|
228
|
-
result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True)
|
|
229
|
-
prefix = cmd.split()[0]
|
|
230
|
-
for line in result.stdout.strip().splitlines():
|
|
231
|
-
logger.info(f"[{prefix}] {line}")
|
|
232
|
-
for line in result.stderr.strip().splitlines():
|
|
233
|
-
if result.returncode != 0:
|
|
234
|
-
logger.error(f"[{prefix}] {line}")
|
|
235
|
-
else:
|
|
236
|
-
logger.info(f"[{prefix}] {line}")
|
|
237
|
-
if result.returncode != 0:
|
|
238
|
-
raise subprocess.CalledProcessError(result.returncode, cmd, output=result.stdout, stderr=result.stderr)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def _run_verify_steps(repo: Repo, repo_path: Path, verify: VerifyConfig) -> tuple[Status, list[StepFailure]]:
|
|
242
|
-
failures: list[StepFailure] = []
|
|
243
|
-
|
|
244
|
-
for step in verify.steps:
|
|
245
|
-
on_fail = step.on_fail or verify.on_fail
|
|
246
|
-
|
|
247
|
-
try:
|
|
248
|
-
_run_command(step.run, repo_path)
|
|
249
|
-
except subprocess.CalledProcessError as e:
|
|
250
|
-
failure = StepFailure(step=step.run, returncode=e.returncode, on_fail=on_fail)
|
|
251
|
-
match on_fail:
|
|
252
|
-
case OnFailStrategy.FAIL:
|
|
253
|
-
return (Status.FAILED, [failure])
|
|
254
|
-
case OnFailStrategy.SKIP:
|
|
255
|
-
return (Status.SKIPPED, [failure])
|
|
256
|
-
case OnFailStrategy.WARN:
|
|
257
|
-
failures.append(failure)
|
|
258
|
-
continue
|
|
259
|
-
|
|
260
|
-
if step.commit:
|
|
261
|
-
git_ops.stage_and_commit(repo, step.commit.add_paths, step.commit.message)
|
|
262
|
-
|
|
263
|
-
return (Status.WARN if failures else Status.PASSED, failures)
|
|
264
|
-
|
|
265
|
-
|
|
266
224
|
def _build_pr_body(log_content: str, failures: list[StepFailure]) -> str:
|
|
267
225
|
body = "Automated dependency update."
|
|
268
226
|
|
|
@@ -35,12 +35,38 @@ class SyncMode(StrEnum):
|
|
|
35
35
|
SCAFFOLD = "scaffold"
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
class OnFailStrategy(StrEnum):
|
|
39
|
+
SKIP = "skip"
|
|
40
|
+
FAIL = "fail"
|
|
41
|
+
WARN = "warn"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CommitConfig(BaseModel):
|
|
45
|
+
message: str
|
|
46
|
+
add_paths: list[str] = Field(default_factory=lambda: ["."])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class VerifyStep(BaseModel):
|
|
50
|
+
run: str
|
|
51
|
+
commit: CommitConfig | None = None
|
|
52
|
+
on_fail: OnFailStrategy | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class VerifyConfig(BaseModel):
|
|
56
|
+
on_fail: OnFailStrategy = OnFailStrategy.WARN
|
|
57
|
+
steps: list[VerifyStep] = Field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
38
60
|
class PathMapping(BaseModel):
|
|
39
61
|
src_path: str
|
|
40
62
|
dest_path: str = ""
|
|
41
63
|
sync_mode: SyncMode = SyncMode.SYNC
|
|
42
64
|
exclude_dirs: set[str] = Field(default_factory=_default_exclude_dirs)
|
|
43
65
|
exclude_file_patterns: set[str] = Field(default_factory=set)
|
|
66
|
+
wrap: bool | None = None
|
|
67
|
+
|
|
68
|
+
def should_wrap(self, config_default: bool) -> bool:
|
|
69
|
+
return self.wrap if self.wrap is not None else config_default
|
|
44
70
|
|
|
45
71
|
def resolved_dest_path(self) -> str:
|
|
46
72
|
return self.dest_path or self.src_path
|
|
@@ -127,6 +153,7 @@ class Destination(BaseModel):
|
|
|
127
153
|
default_branch: str = "main"
|
|
128
154
|
skip_sections: dict[str, list[str]] = Field(default_factory=dict)
|
|
129
155
|
skip_file_patterns: set[str] = Field(default_factory=set)
|
|
156
|
+
verify: VerifyConfig | None = None
|
|
130
157
|
|
|
131
158
|
def resolved_copy_branch(self, config_name: str) -> str:
|
|
132
159
|
return self.copy_branch or f"sync/{config_name}"
|
|
@@ -134,6 +161,9 @@ class Destination(BaseModel):
|
|
|
134
161
|
def is_skipped(self, dest_key: str) -> bool:
|
|
135
162
|
return any(fnmatch.fnmatch(dest_key, pat) for pat in self.skip_file_patterns)
|
|
136
163
|
|
|
164
|
+
def resolve_verify(self, fallback: VerifyConfig | None) -> VerifyConfig:
|
|
165
|
+
return self.verify if self.verify is not None else (fallback or VerifyConfig())
|
|
166
|
+
|
|
137
167
|
|
|
138
168
|
class SrcConfig(BaseModel):
|
|
139
169
|
CONFIG_EXT: ClassVar[str] = ".src.yaml"
|
|
@@ -146,6 +176,8 @@ class SrcConfig(BaseModel):
|
|
|
146
176
|
pr_defaults: PRDefaults = Field(default_factory=PRDefaults)
|
|
147
177
|
paths: list[PathMapping] = Field(default_factory=list)
|
|
148
178
|
destinations: list[Destination] = Field(default_factory=list)
|
|
179
|
+
verify: VerifyConfig | None = None
|
|
180
|
+
wrap_synced_files: bool = False
|
|
149
181
|
|
|
150
182
|
def find_destination(self, name: str) -> Destination:
|
|
151
183
|
for dest in self.destinations:
|
|
@@ -1,37 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from enum import StrEnum
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from typing import ClassVar
|
|
6
5
|
|
|
7
6
|
from pydantic import BaseModel, Field
|
|
8
7
|
|
|
9
|
-
from path_sync._internal.models import
|
|
8
|
+
from path_sync._internal.models import (
|
|
9
|
+
Destination,
|
|
10
|
+
PRFieldsBase,
|
|
11
|
+
SrcConfig,
|
|
12
|
+
VerifyConfig,
|
|
13
|
+
resolve_config_path,
|
|
14
|
+
)
|
|
10
15
|
from path_sync._internal.yaml_utils import load_yaml_model
|
|
11
16
|
|
|
12
17
|
|
|
13
|
-
class OnFailStrategy(StrEnum):
|
|
14
|
-
SKIP = "skip"
|
|
15
|
-
FAIL = "fail"
|
|
16
|
-
WARN = "warn"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class CommitConfig(BaseModel):
|
|
20
|
-
message: str
|
|
21
|
-
add_paths: list[str] = Field(default_factory=lambda: ["."])
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class VerifyStep(BaseModel):
|
|
25
|
-
run: str
|
|
26
|
-
commit: CommitConfig | None = None
|
|
27
|
-
on_fail: OnFailStrategy | None = None
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class VerifyConfig(BaseModel):
|
|
31
|
-
on_fail: OnFailStrategy = OnFailStrategy.SKIP
|
|
32
|
-
steps: list[VerifyStep] = Field(default_factory=list)
|
|
33
|
-
|
|
34
|
-
|
|
35
18
|
class UpdateEntry(BaseModel):
|
|
36
19
|
workdir: str = "."
|
|
37
20
|
command: str
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from git import Repo
|
|
10
|
+
|
|
11
|
+
from path_sync._internal import git_ops
|
|
12
|
+
from path_sync._internal.models import OnFailStrategy, VerifyConfig
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VerifyStatus(StrEnum):
|
|
18
|
+
PASSED = "passed"
|
|
19
|
+
SKIPPED = "skipped"
|
|
20
|
+
WARN = "warn"
|
|
21
|
+
FAILED = "failed"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class StepFailure:
|
|
26
|
+
step: str
|
|
27
|
+
returncode: int
|
|
28
|
+
on_fail: OnFailStrategy
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class VerifyResult:
|
|
33
|
+
status: VerifyStatus = VerifyStatus.PASSED
|
|
34
|
+
failures: list[StepFailure] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_command(cmd: str, cwd: Path, dry_run: bool = False) -> None:
|
|
38
|
+
if dry_run:
|
|
39
|
+
logger.info(f"[DRY RUN] Would run: {cmd} from {cwd}")
|
|
40
|
+
return
|
|
41
|
+
logger.info(f"Running: {cmd}")
|
|
42
|
+
result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True)
|
|
43
|
+
prefix = cmd.split()[0]
|
|
44
|
+
for line in result.stdout.strip().splitlines():
|
|
45
|
+
logger.info(f"[{prefix}] {line}")
|
|
46
|
+
for line in result.stderr.strip().splitlines():
|
|
47
|
+
if result.returncode != 0:
|
|
48
|
+
logger.error(f"[{prefix}] {line}")
|
|
49
|
+
else:
|
|
50
|
+
logger.info(f"[{prefix}] {line}")
|
|
51
|
+
if result.returncode != 0:
|
|
52
|
+
raise subprocess.CalledProcessError(result.returncode, cmd, output=result.stdout, stderr=result.stderr)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run_verify_steps(
|
|
56
|
+
repo: Repo, repo_path: Path, verify: VerifyConfig, dry_run: bool = False, skip_commit: bool = False
|
|
57
|
+
) -> VerifyResult:
|
|
58
|
+
if not verify.steps:
|
|
59
|
+
return VerifyResult()
|
|
60
|
+
|
|
61
|
+
failures: list[StepFailure] = []
|
|
62
|
+
|
|
63
|
+
for step in verify.steps:
|
|
64
|
+
on_fail = step.on_fail or verify.on_fail
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
run_command(step.run, repo_path, dry_run=dry_run)
|
|
68
|
+
except subprocess.CalledProcessError as e:
|
|
69
|
+
failure = StepFailure(step=step.run, returncode=e.returncode, on_fail=on_fail)
|
|
70
|
+
match on_fail:
|
|
71
|
+
case OnFailStrategy.FAIL:
|
|
72
|
+
return VerifyResult(status=VerifyStatus.FAILED, failures=[failure])
|
|
73
|
+
case OnFailStrategy.SKIP:
|
|
74
|
+
return VerifyResult(status=VerifyStatus.SKIPPED, failures=[failure])
|
|
75
|
+
case OnFailStrategy.WARN:
|
|
76
|
+
failures.append(failure)
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if step.commit and not dry_run and not skip_commit:
|
|
80
|
+
git_ops.stage_and_commit(repo, step.commit.add_paths, step.commit.message)
|
|
81
|
+
|
|
82
|
+
status = VerifyStatus.WARN if failures else VerifyStatus.PASSED
|
|
83
|
+
return VerifyResult(status=status, failures=failures)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def log_verify_summary(name: str, result: VerifyResult) -> None:
|
|
87
|
+
match result.status:
|
|
88
|
+
case VerifyStatus.PASSED:
|
|
89
|
+
logger.info(f"Verification passed for {name}")
|
|
90
|
+
case VerifyStatus.WARN:
|
|
91
|
+
logger.warning(f"Verification completed with warnings for {name}")
|
|
92
|
+
for f in result.failures:
|
|
93
|
+
logger.warning(f" {f.step} failed (exit {f.returncode})")
|
|
94
|
+
case VerifyStatus.SKIPPED:
|
|
95
|
+
logger.warning(f"Verification skipped for {name}")
|
|
96
|
+
case VerifyStatus.FAILED:
|
|
97
|
+
logger.error(f"Verification failed for {name}")
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Generated by pkg-ext
|
|
2
2
|
from path_sync._internal.cmd_dep_update import dep_update as _dep_update
|
|
3
|
-
from path_sync._internal.
|
|
3
|
+
from path_sync._internal.models import CommitConfig as _CommitConfig
|
|
4
|
+
from path_sync._internal.models import OnFailStrategy as _OnFailStrategy
|
|
5
|
+
from path_sync._internal.models import VerifyConfig as _VerifyConfig
|
|
6
|
+
from path_sync._internal.models import VerifyStep as _VerifyStep
|
|
4
7
|
from path_sync._internal.models_dep import DepConfig as _DepConfig
|
|
5
|
-
from path_sync._internal.models_dep import OnFailStrategy as _OnFailStrategy
|
|
6
8
|
from path_sync._internal.models_dep import PRConfig as _PRConfig
|
|
7
9
|
from path_sync._internal.models_dep import UpdateEntry as _UpdateEntry
|
|
8
|
-
from path_sync._internal.models_dep import VerifyConfig as _VerifyConfig
|
|
9
|
-
from path_sync._internal.models_dep import VerifyStep as _VerifyStep
|
|
10
10
|
|
|
11
11
|
dep_update = _dep_update
|
|
12
12
|
CommitConfig = _CommitConfig
|
|
13
|
-
DepConfig = _DepConfig
|
|
14
13
|
OnFailStrategy = _OnFailStrategy
|
|
15
|
-
PRConfig = _PRConfig
|
|
16
|
-
UpdateEntry = _UpdateEntry
|
|
17
14
|
VerifyConfig = _VerifyConfig
|
|
18
15
|
VerifyStep = _VerifyStep
|
|
16
|
+
DepConfig = _DepConfig
|
|
17
|
+
PRConfig = _PRConfig
|
|
18
|
+
UpdateEntry = _UpdateEntry
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from zero_3rdparty.sections import (
|
|
6
6
|
Section,
|
|
7
7
|
SectionChanges,
|
|
8
|
+
SectionPart,
|
|
8
9
|
get_comment_config,
|
|
9
10
|
)
|
|
10
11
|
from zero_3rdparty.sections import (
|
|
@@ -25,16 +26,22 @@ from zero_3rdparty.sections import (
|
|
|
25
26
|
from zero_3rdparty.sections import (
|
|
26
27
|
wrap_in_default_section as _wrap_in_default_section,
|
|
27
28
|
)
|
|
29
|
+
from zero_3rdparty.sections import (
|
|
30
|
+
wrap_section as _wrap_section,
|
|
31
|
+
)
|
|
28
32
|
|
|
29
33
|
__all__ = [
|
|
30
34
|
"Section",
|
|
31
35
|
"SectionChanges",
|
|
36
|
+
"SectionPart",
|
|
37
|
+
"build_sections_content",
|
|
32
38
|
"changed_sections",
|
|
33
39
|
"extract_sections",
|
|
34
40
|
"has_sections",
|
|
35
41
|
"parse_sections",
|
|
36
42
|
"replace_sections",
|
|
37
43
|
"wrap_in_default_section",
|
|
44
|
+
"wrap_in_synced_section",
|
|
38
45
|
]
|
|
39
46
|
|
|
40
47
|
TOOL_NAME = "path-sync"
|
|
@@ -52,13 +59,17 @@ def wrap_in_default_section(content: str, path: Path) -> str:
|
|
|
52
59
|
return _wrap_in_default_section(content, TOOL_NAME, get_comment_config(path))
|
|
53
60
|
|
|
54
61
|
|
|
62
|
+
def wrap_in_synced_section(content: str, path: Path) -> str:
|
|
63
|
+
return _wrap_section(content, "synced", TOOL_NAME, get_comment_config(path))
|
|
64
|
+
|
|
65
|
+
|
|
55
66
|
def extract_sections(content: str, path: Path) -> dict[str, str]:
|
|
56
67
|
return _extract_sections(content, TOOL_NAME, get_comment_config(path), str(path))
|
|
57
68
|
|
|
58
69
|
|
|
59
70
|
def replace_sections(
|
|
60
71
|
dest_content: str,
|
|
61
|
-
src_sections:
|
|
72
|
+
src_sections: list[Section],
|
|
62
73
|
path: Path,
|
|
63
74
|
skip_sections: list[str] | None = None,
|
|
64
75
|
*,
|
|
@@ -81,3 +92,10 @@ def changed_sections(
|
|
|
81
92
|
skip: set[str] | None = None,
|
|
82
93
|
) -> SectionChanges:
|
|
83
94
|
return _changed_sections(baseline_content, current_content, TOOL_NAME, get_comment_config(path), skip, str(path))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_sections_content(section_list: list[Section], path: Path) -> str:
|
|
98
|
+
"""Build file content from a list of Section objects."""
|
|
99
|
+
config = get_comment_config(path)
|
|
100
|
+
parts = [_wrap_section(s.content, s.id, TOOL_NAME, config) for s in section_list]
|
|
101
|
+
return "\n".join(parts)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|