path-sync 0.7.6__tar.gz → 0.7.7__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.6 → path_sync-0.7.7}/PKG-INFO +1 -1
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/__init__.py +1 -1
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/auto_merge.py +3 -1
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/cmd_copy.py +4 -2
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/cmd_dep_update.py +5 -2
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/git_ops.py +33 -2
- {path_sync-0.7.6 → path_sync-0.7.7}/pyproject.toml +24 -6
- {path_sync-0.7.6 → path_sync-0.7.7}/.gitignore +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/LICENSE +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/README.md +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/__main__.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/__init__.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/cmd_boot.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/cmd_options.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/cmd_validate.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/file_utils.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/header.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/log_capture.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/models.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/models_dep.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/prompt_utils.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/repo_utils.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/typer_app.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/validation.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/verify.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/_internal/yaml_utils.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/config.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/copy.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/dep_update.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/sections.py +0 -0
- {path_sync-0.7.6 → path_sync-0.7.7}/path_sync/validate_no_changes.py +0 -0
|
@@ -38,6 +38,8 @@ COMPLETED_CHECK_STATES: frozenset[str] = frozenset(FAILED_CHECK_STATES | {"SUCCE
|
|
|
38
38
|
class CheckRun(BaseModel):
|
|
39
39
|
name: str
|
|
40
40
|
state: str
|
|
41
|
+
workflow: str = ""
|
|
42
|
+
link: str = ""
|
|
41
43
|
|
|
42
44
|
@property
|
|
43
45
|
def failed(self) -> bool:
|
|
@@ -83,7 +85,7 @@ def enable_auto_merge(repo_path: Path, pr_ref: str, config: AutoMergeConfig, des
|
|
|
83
85
|
|
|
84
86
|
|
|
85
87
|
def get_pr_checks(repo_path: Path, pr_ref: str) -> list[CheckRun]:
|
|
86
|
-
cmd = ["gh", "pr", "checks", pr_ref, "--json", "name,state"]
|
|
88
|
+
cmd = ["gh", "pr", "checks", pr_ref, "--json", "name,state,workflow,link"]
|
|
87
89
|
result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
|
|
88
90
|
if result.returncode != 0:
|
|
89
91
|
logger.warning(f"Failed to get checks for {pr_ref}: {result.stderr.strip()}")
|
|
@@ -607,8 +607,10 @@ def _push_and_pr(
|
|
|
607
607
|
if not prompt_utils.prompt_confirm(f"Push {dest.name} to origin?", opts.no_prompt):
|
|
608
608
|
return None
|
|
609
609
|
|
|
610
|
-
git_ops.push_branch(repo, copy_branch, force=True)
|
|
611
|
-
|
|
610
|
+
if git_ops.push_branch(repo, copy_branch, force=True):
|
|
611
|
+
typer.echo(f" Pushed: {copy_branch} (force)", err=True)
|
|
612
|
+
else:
|
|
613
|
+
typer.echo(f" Skipped push for {copy_branch}: content unchanged on remote", err=True)
|
|
612
614
|
|
|
613
615
|
if opts.no_pr or not prompt_utils.prompt_confirm(f"Create PR for {dest.name}?", opts.no_prompt):
|
|
614
616
|
return None
|
|
@@ -201,9 +201,12 @@ def _create_prs(config: DepConfig, results: list[RepoResult], opts: DepUpdateOpt
|
|
|
201
201
|
continue
|
|
202
202
|
|
|
203
203
|
repo = git_ops.get_repo(result.repo_path)
|
|
204
|
-
git_ops.push_branch(repo, config.pr.branch, force=True)
|
|
205
|
-
|
|
206
204
|
body = _build_pr_body(result.log_content, result.failures)
|
|
205
|
+
|
|
206
|
+
if not git_ops.push_branch(repo, config.pr.branch, force=True):
|
|
207
|
+
git_ops.update_pr_body(result.repo_path, config.pr.branch, body)
|
|
208
|
+
continue
|
|
209
|
+
|
|
207
210
|
pr_url = git_ops.create_or_update_pr(
|
|
208
211
|
result.repo_path,
|
|
209
212
|
config.pr.branch,
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import subprocess
|
|
6
7
|
from contextlib import suppress
|
|
7
8
|
from pathlib import Path
|
|
@@ -10,6 +11,20 @@ from git import GitCommandError, InvalidGitRepositoryError, NoSuchPathError, Rep
|
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
14
|
+
GH_PR_BODY_MAX_CHARS = 64536 # real limit is 65536, but we leave some buffer
|
|
15
|
+
_TRUNCATION_NOTICE = "\n\n... (truncated, output too long for PR body)"
|
|
16
|
+
_CODE_FENCE_RE = re.compile(r"^`{3,}", re.MULTILINE)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _truncate_body(body: str) -> str:
|
|
20
|
+
if len(body) <= GH_PR_BODY_MAX_CHARS:
|
|
21
|
+
return body
|
|
22
|
+
cut = GH_PR_BODY_MAX_CHARS - len(_TRUNCATION_NOTICE)
|
|
23
|
+
truncated = body[:cut]
|
|
24
|
+
fence_count = len(_CODE_FENCE_RE.findall(truncated))
|
|
25
|
+
suffix = "\n```" + _TRUNCATION_NOTICE if fence_count % 2 else _TRUNCATION_NOTICE
|
|
26
|
+
return truncated[: GH_PR_BODY_MAX_CHARS - len(suffix)] + suffix
|
|
27
|
+
|
|
13
28
|
|
|
14
29
|
def _auth_url(url: str) -> str:
|
|
15
30
|
"""Inject GH_TOKEN into HTTPS URL for authentication."""
|
|
@@ -145,10 +160,24 @@ def _ensure_git_user(repo: Repo) -> None:
|
|
|
145
160
|
repo.config_writer().set_value("user", "email", "path-sync[bot]@users.noreply.github.com").release()
|
|
146
161
|
|
|
147
162
|
|
|
148
|
-
def
|
|
163
|
+
def remote_branch_has_same_content(repo: Repo, branch: str) -> bool:
|
|
164
|
+
"""Check if origin/{branch} has identical file content (tree) as local {branch}."""
|
|
165
|
+
with suppress(GitCommandError):
|
|
166
|
+
local_tree = repo.git.rev_parse(f"{branch}^{{tree}}")
|
|
167
|
+
remote_tree = repo.git.rev_parse(f"origin/{branch}^{{tree}}")
|
|
168
|
+
return local_tree == remote_tree
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def push_branch(repo: Repo, branch: str, force: bool = True) -> bool:
|
|
173
|
+
"""Push branch to origin. Returns False if skipped (content unchanged on remote)."""
|
|
174
|
+
if force and remote_branch_has_same_content(repo, branch):
|
|
175
|
+
logger.info(f"Skipping push for {branch}: content unchanged on remote")
|
|
176
|
+
return False
|
|
149
177
|
logger.info(f"Pushing {branch}" + (" (force)" if force else ""))
|
|
150
178
|
args = ["--force", "-u", "origin", branch] if force else ["-u", "origin", branch]
|
|
151
179
|
repo.git.push(*args)
|
|
180
|
+
return True
|
|
152
181
|
|
|
153
182
|
|
|
154
183
|
def _get_repo_full_name(repo_path: Path) -> str | None:
|
|
@@ -189,6 +218,7 @@ def update_pr_body(repo_path: Path, branch: str, body: str) -> bool:
|
|
|
189
218
|
if not pr_number:
|
|
190
219
|
return False
|
|
191
220
|
|
|
221
|
+
body = _truncate_body(body)
|
|
192
222
|
cmd = [
|
|
193
223
|
"gh",
|
|
194
224
|
"api",
|
|
@@ -243,8 +273,9 @@ def create_or_update_pr(
|
|
|
243
273
|
reviewers: list[str] | None = None,
|
|
244
274
|
assignees: list[str] | None = None,
|
|
245
275
|
) -> str:
|
|
276
|
+
body = _truncate_body(body) if body else ""
|
|
246
277
|
cmd = ["gh", "pr", "create", "--head", branch, "--title", title]
|
|
247
|
-
cmd.extend(["--body", body
|
|
278
|
+
cmd.extend(["--body", body])
|
|
248
279
|
if labels:
|
|
249
280
|
cmd.extend(["--label", ",".join(labels)])
|
|
250
281
|
if reviewers:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[project]
|
|
4
4
|
name = "path-sync"
|
|
5
|
-
version = "0.7.
|
|
5
|
+
version = "0.7.7"
|
|
6
6
|
description = "Sync files from a source repo to multiple destination repos"
|
|
7
7
|
requires-python = ">=3.13"
|
|
8
8
|
license = "MIT"
|
|
@@ -68,12 +68,13 @@ target-version = "py313"
|
|
|
68
68
|
# UP046: use type parameters on functions instead of TypeVar
|
|
69
69
|
# UP047: use type parameters instead of `Generic[T]` subclass
|
|
70
70
|
extend-ignore = ["E501", "UP006", "UP007", "UP035", "UP040", "UP046", "UP047"]
|
|
71
|
-
extend-select = ["Q", "RUF100", "C90", "UP", "I", "T"]
|
|
71
|
+
extend-select = ["Q", "RUF100", "C90", "UP", "I", "T", "FURB"]
|
|
72
72
|
# === OK_EDIT: path-sync ruff ===
|
|
73
73
|
|
|
74
74
|
# === DO_NOT_EDIT: path-sync pytest ===
|
|
75
75
|
[tool.pytest.ini_options]
|
|
76
|
-
addopts = "-s -vv --log-cli-level=INFO"
|
|
76
|
+
addopts = "-s -vv --log-cli-level=INFO -m 'not manual'"
|
|
77
|
+
markers = ["manual: test requires manual env setup (not run in CI)"]
|
|
77
78
|
python_files = ["*_test.py", "test_*.py"]
|
|
78
79
|
asyncio_mode = "auto"
|
|
79
80
|
# === OK_EDIT: path-sync pytest ===
|
|
@@ -84,16 +85,33 @@ omit = ["*_test.py"]
|
|
|
84
85
|
exclude_lines = ["no cov", "if __name__ == .__main__.:"]
|
|
85
86
|
# === OK_EDIT: path-sync coverage ===
|
|
86
87
|
|
|
87
|
-
# === DO_NOT_EDIT: path-sync
|
|
88
|
+
# === DO_NOT_EDIT: path-sync vulture ===
|
|
89
|
+
[tool.vulture]
|
|
90
|
+
min_confidence = 80
|
|
91
|
+
ignore_names = ["mock_*", "Mock*", "*_mock"]
|
|
92
|
+
exclude = [".venv/", "*test.py"]
|
|
93
|
+
# === OK_EDIT: path-sync vulture ===
|
|
94
|
+
|
|
95
|
+
# === DO_NOT_EDIT: path-sync dev-lint ===
|
|
88
96
|
[dependency-groups]
|
|
89
|
-
dev = [
|
|
97
|
+
dev-lint = [
|
|
90
98
|
"pyright>=1.1",
|
|
99
|
+
"ruff>=0.14",
|
|
100
|
+
"vulture>=2.14",
|
|
101
|
+
]
|
|
102
|
+
# === OK_EDIT: path-sync dev-lint ===
|
|
103
|
+
|
|
104
|
+
# === DO_NOT_EDIT: path-sync dev-test ===
|
|
105
|
+
dev-test = [
|
|
91
106
|
"pytest>=9.0",
|
|
92
107
|
"pytest-asyncio>=0.24",
|
|
93
108
|
"pytest-cov>=7.0",
|
|
94
109
|
"pytest-regressions>=2.6",
|
|
95
|
-
"ruff>=0.14",
|
|
96
110
|
]
|
|
111
|
+
# === OK_EDIT: path-sync dev-test ===
|
|
112
|
+
|
|
113
|
+
# === DO_NOT_EDIT: path-sync dev ===
|
|
114
|
+
dev = [{include-group = "dev-lint"}, {include-group = "dev-test"}]
|
|
97
115
|
# === OK_EDIT: path-sync dev ===
|
|
98
116
|
|
|
99
117
|
# === DO_NOT_EDIT: path-sync docs ===
|
|
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
|
|
File without changes
|