path-sync 0.7.4__tar.gz → 0.7.6__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.7.4 → path_sync-0.7.6}/PKG-INFO +1 -1
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/__init__.py +1 -1
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/auto_merge.py +28 -10
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/cmd_copy.py +22 -21
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/cmd_dep_update.py +4 -27
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/git_ops.py +2 -1
- path_sync-0.7.6/path_sync/_internal/repo_utils.py +37 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/pyproject.toml +1 -1
- {path_sync-0.7.4 → path_sync-0.7.6}/.gitignore +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/LICENSE +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/README.md +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/__main__.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/__init__.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/cmd_boot.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/cmd_options.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/cmd_validate.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/file_utils.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/header.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/log_capture.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/models.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/models_dep.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/prompt_utils.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/typer_app.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/validation.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/verify.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/_internal/yaml_utils.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/config.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/copy.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/dep_update.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/sections.py +0 -0
- {path_sync-0.7.4 → path_sync-0.7.6}/path_sync/validate_no_changes.py +0 -0
|
@@ -70,15 +70,16 @@ class PRMergeResult(BaseModel):
|
|
|
70
70
|
return [c for c in self.checks if c.pending]
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
def enable_auto_merge(repo_path: Path, pr_ref: str, config: AutoMergeConfig) -> None:
|
|
73
|
+
def enable_auto_merge(repo_path: Path, pr_ref: str, config: AutoMergeConfig, dest_name: str = "") -> None:
|
|
74
|
+
label = dest_name or pr_ref
|
|
74
75
|
cmd = ["gh", "pr", "merge", "--auto", f"--{config.method}", pr_ref]
|
|
75
76
|
if config.delete_branch:
|
|
76
77
|
cmd.append("--delete-branch")
|
|
77
78
|
result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
|
|
78
79
|
if result.returncode != 0:
|
|
79
|
-
logger.warning(f"
|
|
80
|
+
logger.warning(f" {label}: auto-merge enable failed: {result.stderr.strip()}")
|
|
80
81
|
else:
|
|
81
|
-
logger.info(f"
|
|
82
|
+
logger.info(f" {label}: enabled auto-merge ({config.method})")
|
|
82
83
|
|
|
83
84
|
|
|
84
85
|
def get_pr_checks(repo_path: Path, pr_ref: str) -> list[CheckRun]:
|
|
@@ -103,6 +104,7 @@ def get_pr_url(repo_path: Path, pr_ref: str) -> str:
|
|
|
103
104
|
cmd = ["gh", "pr", "view", pr_ref, "--json", "url", "-q", ".url"]
|
|
104
105
|
result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
|
|
105
106
|
if result.returncode != 0:
|
|
107
|
+
logger.warning(f"Failed to get PR URL for {pr_ref}: {result.stderr.strip()}")
|
|
106
108
|
return pr_ref
|
|
107
109
|
return result.stdout.strip() or pr_ref
|
|
108
110
|
|
|
@@ -110,21 +112,31 @@ def get_pr_url(repo_path: Path, pr_ref: str) -> str:
|
|
|
110
112
|
def wait_for_merge(repo_path: Path, pr_ref: str, config: AutoMergeConfig, dest_name: str = "") -> PRMergeResult:
|
|
111
113
|
pr_url = get_pr_url(repo_path, pr_ref)
|
|
112
114
|
deadline = time.monotonic() + config.timeout_seconds
|
|
115
|
+
label = dest_name or pr_ref
|
|
116
|
+
poll_count = 0
|
|
113
117
|
|
|
114
118
|
while time.monotonic() < deadline:
|
|
115
119
|
state = get_pr_state(repo_path, pr_ref)
|
|
116
120
|
if state == PRState.MERGED:
|
|
121
|
+
logger.info(f"{label}: merged ({pr_url})")
|
|
117
122
|
return PRMergeResult(dest_name=dest_name, pr_url=pr_url, branch=pr_ref, state=PRState.MERGED)
|
|
118
123
|
if state == PRState.CLOSED:
|
|
119
124
|
checks = get_pr_checks(repo_path, pr_ref)
|
|
125
|
+
logger.warning(f"{label}: PR closed without merging ({pr_url})")
|
|
120
126
|
return PRMergeResult(dest_name=dest_name, pr_url=pr_url, branch=pr_ref, state=PRState.CLOSED, checks=checks)
|
|
127
|
+
poll_count += 1
|
|
128
|
+
elapsed = int(config.timeout_seconds - (deadline - time.monotonic()))
|
|
129
|
+
logger.info(f"{label}: still open (poll #{poll_count}, {elapsed}s elapsed)")
|
|
121
130
|
time.sleep(config.poll_interval_seconds)
|
|
122
131
|
|
|
123
132
|
checks = get_pr_checks(repo_path, pr_ref)
|
|
124
|
-
logger.warning(f"Timeout waiting for {
|
|
133
|
+
logger.warning(f"Timeout waiting for {label} after {config.timeout_seconds}s ({pr_url})")
|
|
125
134
|
return PRMergeResult(dest_name=dest_name, pr_url=pr_url, branch=pr_ref, state=PRState.OPEN, checks=checks)
|
|
126
135
|
|
|
127
136
|
|
|
137
|
+
SEPARATOR_WIDTH = 40
|
|
138
|
+
|
|
139
|
+
|
|
128
140
|
def handle_auto_merge(
|
|
129
141
|
pr_refs: list[PRRef],
|
|
130
142
|
config: AutoMergeConfig,
|
|
@@ -133,22 +145,27 @@ def handle_auto_merge(
|
|
|
133
145
|
if not pr_refs:
|
|
134
146
|
return []
|
|
135
147
|
|
|
148
|
+
line = "─" * SEPARATOR_WIDTH
|
|
149
|
+
logger.info(f"\n{line}")
|
|
150
|
+
logger.info(" Auto-merge")
|
|
151
|
+
logger.info(line)
|
|
152
|
+
|
|
136
153
|
pending_refs: list[PRRef] = []
|
|
137
154
|
for ref in pr_refs:
|
|
138
155
|
state = get_pr_state(ref.repo_path, ref.branch_or_url)
|
|
139
156
|
if state == PRState.MERGED:
|
|
140
|
-
logger.info(f"{ref.dest_name}: already merged")
|
|
157
|
+
logger.info(f" {ref.dest_name}: already merged")
|
|
141
158
|
continue
|
|
142
|
-
enable_auto_merge(ref.repo_path, ref.branch_or_url, config)
|
|
159
|
+
enable_auto_merge(ref.repo_path, ref.branch_or_url, config, dest_name=ref.dest_name)
|
|
143
160
|
pending_refs.append(ref)
|
|
144
161
|
|
|
145
162
|
if no_wait:
|
|
146
|
-
logger.info("--no-wait: skipping merge polling")
|
|
163
|
+
logger.info(" --no-wait: skipping merge polling")
|
|
147
164
|
return []
|
|
148
165
|
|
|
149
166
|
results: list[PRMergeResult] = []
|
|
150
167
|
for ref in pending_refs:
|
|
151
|
-
logger.info(f"Waiting for {ref.dest_name} to merge...")
|
|
168
|
+
logger.info(f" Waiting for {ref.dest_name} to merge...")
|
|
152
169
|
result = wait_for_merge(ref.repo_path, ref.branch_or_url, config, dest_name=ref.dest_name)
|
|
153
170
|
results.append(result)
|
|
154
171
|
|
|
@@ -160,9 +177,10 @@ def _log_summary(results: list[PRMergeResult]) -> None:
|
|
|
160
177
|
if not results:
|
|
161
178
|
return
|
|
162
179
|
max_name = max(len(r.dest_name) for r in results)
|
|
163
|
-
|
|
180
|
+
max_url = max(len(r.pr_url) for r in results)
|
|
181
|
+
header = f"{'Repo':<{max_name}} {'PR':<{max_url}} State Failed Checks"
|
|
164
182
|
logger.info(header)
|
|
165
183
|
logger.info("-" * len(header))
|
|
166
184
|
for r in results:
|
|
167
185
|
failed = ", ".join(c.name for c in r.failed_checks)
|
|
168
|
-
logger.info(f"{r.dest_name:<{max_name}} {r.state:<8} {failed}")
|
|
186
|
+
logger.info(f"{r.dest_name:<{max_name}} {r.pr_url:<{max_url}} {r.state:<8} {failed}")
|
|
@@ -23,6 +23,7 @@ from path_sync._internal.models import (
|
|
|
23
23
|
pr_already_synced,
|
|
24
24
|
resolve_config_path,
|
|
25
25
|
)
|
|
26
|
+
from path_sync._internal.repo_utils import ensure_repo, resolve_repo_path
|
|
26
27
|
from path_sync._internal.typer_app import app
|
|
27
28
|
from path_sync._internal.verify import StepFailure, VerifyResult, VerifyStatus
|
|
28
29
|
from path_sync._internal.yaml_utils import load_yaml_model
|
|
@@ -57,6 +58,7 @@ class CopyOptions(BaseModel):
|
|
|
57
58
|
skip_verify: bool = False
|
|
58
59
|
no_wait: bool = False
|
|
59
60
|
no_auto_merge: bool = False
|
|
61
|
+
work_dir: str = ""
|
|
60
62
|
pr_title: str = ""
|
|
61
63
|
labels: list[str] | None = None
|
|
62
64
|
reviewers: list[str] | None = None
|
|
@@ -78,6 +80,7 @@ def copy(
|
|
|
78
80
|
help="Source repo root (default: find git root from cwd)",
|
|
79
81
|
),
|
|
80
82
|
dest_filter: str = typer.Option("", "-d", "--dest", help="Filter destinations (comma-separated)"),
|
|
83
|
+
work_dir: str = typer.Option("", "--work-dir", help="Clone repos here (overrides dest_path_relative)"),
|
|
81
84
|
dry_run: bool = typer.Option(False, "--dry-run", help="Preview without writing"),
|
|
82
85
|
force_overwrite: bool = typer.Option(
|
|
83
86
|
False,
|
|
@@ -174,6 +177,7 @@ def copy(
|
|
|
174
177
|
skip_verify=skip_verify,
|
|
175
178
|
no_wait=no_wait,
|
|
176
179
|
no_auto_merge=no_auto_merge,
|
|
180
|
+
work_dir=work_dir,
|
|
177
181
|
pr_title=pr_title or config.pr_defaults.title,
|
|
178
182
|
labels=cmd_options.split_csv(pr_labels) or config.pr_defaults.labels,
|
|
179
183
|
reviewers=cmd_options.split_csv(pr_reviewers) or config.pr_defaults.reviewers,
|
|
@@ -251,15 +255,13 @@ def _sync_destination(
|
|
|
251
255
|
opts: CopyOptions,
|
|
252
256
|
read_log: Callable[[], str],
|
|
253
257
|
) -> tuple[int, PRRef | None]:
|
|
254
|
-
dest_root = (src_root
|
|
255
|
-
|
|
256
|
-
if opts.dry_run and not dest_root.exists():
|
|
257
|
-
raise ValueError(f"Destination repo not found: {dest_root}. Clone it first or run without --dry-run.")
|
|
258
|
-
|
|
259
|
-
dest_repo = _ensure_dest_repo(dest, dest_root, opts.dry_run)
|
|
258
|
+
dest_root = resolve_repo_path(dest, src_root, opts.work_dir)
|
|
259
|
+
dest_repo = ensure_repo(dest, dest_root, dry_run=opts.dry_run)
|
|
260
260
|
copy_branch = dest.resolved_copy_branch(config.name)
|
|
261
|
+
_print_dest_header(dest)
|
|
261
262
|
|
|
262
263
|
if _skip_already_synced(dest.name, dest_root, copy_branch, commit_ts, opts, config):
|
|
264
|
+
typer.echo(" (already synced, skipped)", err=True)
|
|
263
265
|
return 0, None
|
|
264
266
|
|
|
265
267
|
if not opts.no_checkout and prompt_utils.prompt_confirm(f"Switch {dest.name} to {copy_branch}?", opts.no_prompt):
|
|
@@ -270,10 +272,10 @@ def _sync_destination(
|
|
|
270
272
|
from_default=opts.checkout_from_default,
|
|
271
273
|
)
|
|
272
274
|
result = _sync_paths(config, dest, src_root, dest_root, opts)
|
|
273
|
-
_print_sync_summary(
|
|
275
|
+
_print_sync_summary(result)
|
|
274
276
|
|
|
275
277
|
if result.total == 0:
|
|
276
|
-
|
|
278
|
+
typer.echo(" No changes", err=True)
|
|
277
279
|
_close_stale_pr(dest_root, copy_branch, opts, config)
|
|
278
280
|
return 0, None
|
|
279
281
|
|
|
@@ -304,24 +306,23 @@ def _sync_destination(
|
|
|
304
306
|
return result.total, pr_ref
|
|
305
307
|
|
|
306
308
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
+
SEPARATOR_WIDTH = 40
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _print_dest_header(dest: Destination) -> None:
|
|
313
|
+
line = "─" * SEPARATOR_WIDTH
|
|
314
|
+
typer.echo(f"\n{line}", err=True)
|
|
315
|
+
typer.echo(f" {dest.name}", err=True)
|
|
316
|
+
typer.echo(line, err=True)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _print_sync_summary(result: SyncResult) -> None:
|
|
309
320
|
if result.content_changes > 0:
|
|
310
321
|
typer.echo(f" [{result.content_changes} files synced]", err=True)
|
|
311
322
|
if result.orphans_deleted > 0:
|
|
312
323
|
typer.echo(f" [-] {result.orphans_deleted} orphans deleted", err=True)
|
|
313
324
|
if result.total > 0:
|
|
314
|
-
typer.echo(f"
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def _ensure_dest_repo(dest: Destination, dest_root: Path, dry_run: bool):
|
|
318
|
-
if not dest_root.exists():
|
|
319
|
-
if dry_run:
|
|
320
|
-
raise ValueError(f"Destination repo not found: {dest_root}. Clone it first or run without --dry-run.")
|
|
321
|
-
if not dest.repo_url:
|
|
322
|
-
raise ValueError(f"Dest {dest.name} not found and no repo_url configured")
|
|
323
|
-
git_ops.clone_repo(dest.repo_url, dest_root)
|
|
324
|
-
return git_ops.get_repo(dest_root)
|
|
325
|
+
typer.echo(f" {result.total} changes ready.", err=True)
|
|
325
326
|
|
|
326
327
|
|
|
327
328
|
def _sync_paths(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import shutil
|
|
5
4
|
import subprocess
|
|
6
5
|
from dataclasses import dataclass, field
|
|
7
6
|
from enum import StrEnum
|
|
@@ -10,7 +9,7 @@ from pathlib import Path
|
|
|
10
9
|
import typer
|
|
11
10
|
from git import Repo
|
|
12
11
|
|
|
13
|
-
from path_sync._internal import cmd_options, git_ops,
|
|
12
|
+
from path_sync._internal import cmd_options, git_ops, verify
|
|
14
13
|
from path_sync._internal.auto_merge import PRRef, handle_auto_merge
|
|
15
14
|
from path_sync._internal.log_capture import capture_log
|
|
16
15
|
from path_sync._internal.models import Destination, OnFailStrategy, find_repo_root
|
|
@@ -19,6 +18,7 @@ from path_sync._internal.models_dep import (
|
|
|
19
18
|
UpdateEntry,
|
|
20
19
|
resolve_dep_config_path,
|
|
21
20
|
)
|
|
21
|
+
from path_sync._internal.repo_utils import ensure_repo, resolve_repo_path
|
|
22
22
|
from path_sync._internal.typer_app import app
|
|
23
23
|
from path_sync._internal.verify import StepFailure, VerifyStatus
|
|
24
24
|
from path_sync._internal.yaml_utils import load_yaml_model
|
|
@@ -154,8 +154,8 @@ def _process_single_repo_inner(
|
|
|
154
154
|
opts: DepUpdateOptions,
|
|
155
155
|
) -> RepoResult:
|
|
156
156
|
logger.info(f"Processing {dest.name}...")
|
|
157
|
-
repo_path =
|
|
158
|
-
repo =
|
|
157
|
+
repo_path = resolve_repo_path(dest, src_root, work_dir)
|
|
158
|
+
repo = ensure_repo(dest, repo_path)
|
|
159
159
|
git_ops.prepare_copy_branch(repo, dest.default_branch, config.pr.branch, from_default=True)
|
|
160
160
|
|
|
161
161
|
if failure := _run_updates(config.updates, repo_path):
|
|
@@ -219,29 +219,6 @@ def _create_prs(config: DepConfig, results: list[RepoResult], opts: DepUpdateOpt
|
|
|
219
219
|
return pr_refs
|
|
220
220
|
|
|
221
221
|
|
|
222
|
-
def _resolve_repo_path(dest: Destination, src_root: Path, work_dir: str) -> Path:
|
|
223
|
-
if work_dir:
|
|
224
|
-
return Path(work_dir) / dest.name
|
|
225
|
-
if dest.dest_path_relative:
|
|
226
|
-
return (src_root / dest.dest_path_relative).resolve()
|
|
227
|
-
raise typer.BadParameter(f"No dest_path_relative for {dest.name}, --work-dir required")
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _ensure_repo(dest: Destination, repo_path: Path, default_branch: str) -> Repo:
|
|
231
|
-
if repo_path.exists():
|
|
232
|
-
if git_ops.is_git_repo(repo_path):
|
|
233
|
-
repo = git_ops.get_repo(repo_path)
|
|
234
|
-
git_ops.fetch_and_reset_to_default(repo, default_branch)
|
|
235
|
-
return repo
|
|
236
|
-
logger.warning(f"Invalid git repo at {repo_path}")
|
|
237
|
-
if not prompt_utils.prompt_confirm(f"Remove {repo_path} and re-clone?"):
|
|
238
|
-
raise typer.Abort()
|
|
239
|
-
shutil.rmtree(repo_path)
|
|
240
|
-
if not dest.repo_url:
|
|
241
|
-
raise ValueError(f"Dest {dest.name} not found at {repo_path} and no repo_url configured")
|
|
242
|
-
return git_ops.clone_repo(dest.repo_url, repo_path)
|
|
243
|
-
|
|
244
|
-
|
|
245
222
|
def _build_pr_body(log_content: str, failures: list[StepFailure]) -> str:
|
|
246
223
|
body = "Automated dependency update."
|
|
247
224
|
|
|
@@ -202,7 +202,8 @@ def update_pr_body(repo_path: Path, branch: str, body: str) -> bool:
|
|
|
202
202
|
if result.returncode != 0:
|
|
203
203
|
logger.warning(f"Failed to update PR body: {result.stderr}")
|
|
204
204
|
return False
|
|
205
|
-
|
|
205
|
+
pr_url = f"https://github.com/{repo_full}/pull/{pr_number}"
|
|
206
|
+
logger.info(f"Updated PR body: {pr_url}")
|
|
206
207
|
return True
|
|
207
208
|
|
|
208
209
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from git import Repo
|
|
8
|
+
|
|
9
|
+
from path_sync._internal import git_ops, prompt_utils
|
|
10
|
+
from path_sync._internal.models import Destination
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_repo_path(dest: Destination, src_root: Path, work_dir: str) -> Path:
|
|
16
|
+
if work_dir:
|
|
17
|
+
return Path(work_dir) / dest.name
|
|
18
|
+
if dest.dest_path_relative:
|
|
19
|
+
return (src_root / dest.dest_path_relative).resolve()
|
|
20
|
+
raise ValueError(f"No dest_path_relative for {dest.name}, use --work-dir")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ensure_repo(dest: Destination, repo_path: Path, dry_run: bool = False) -> Repo:
|
|
24
|
+
if repo_path.exists():
|
|
25
|
+
if git_ops.is_git_repo(repo_path):
|
|
26
|
+
return git_ops.get_repo(repo_path)
|
|
27
|
+
logger.warning(f"Invalid git repo at {repo_path}")
|
|
28
|
+
if dry_run:
|
|
29
|
+
raise ValueError(f"Invalid git repo at {repo_path}, cannot re-clone in dry-run mode")
|
|
30
|
+
if not prompt_utils.prompt_confirm(f"Remove {repo_path} and re-clone?"):
|
|
31
|
+
raise ValueError(f"Aborted: user declined to remove invalid repo at {repo_path}")
|
|
32
|
+
shutil.rmtree(repo_path)
|
|
33
|
+
if dry_run:
|
|
34
|
+
raise ValueError(f"Destination repo not found: {repo_path}. Clone it first or run without --dry-run.")
|
|
35
|
+
if not dest.repo_url:
|
|
36
|
+
raise ValueError(f"Dest {dest.name} not found at {repo_path} and no repo_url configured")
|
|
37
|
+
return git_ops.clone_repo(dest.repo_url, repo_path)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|