Skip to content

Commit 37bc905

Browse files
committed
add a bit more validation
1 parent 29c1739 commit 37bc905

5 files changed

Lines changed: 227 additions & 10 deletions

File tree

git_sync_filtered/cli.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,52 @@
1+
from fnmatch import translate as glob_translate
2+
from pathlib import Path
3+
14
import click
5+
from pydantic import BaseModel, ConfigDict, FilePath, field_validator
26

37
from git_sync_filtered.sync import sync
48

59

10+
class SyncConfig(BaseModel):
11+
model_config = ConfigDict(frozen=True)
12+
13+
private: str
14+
public: str
15+
keep: tuple[str, ...]
16+
keep_from_file: FilePath | None = None
17+
sync_branch: str = "upstream/sync"
18+
main_branch: str = "main"
19+
private_branch: str = "main"
20+
dry_run: bool = False
21+
merge: bool = False
22+
force: bool = False
23+
24+
@field_validator("keep", mode="before")
25+
@classmethod
26+
def ensure_non_empty(cls, v: tuple[str, ...]) -> tuple[str, ...]:
27+
if not v:
28+
raise ValueError("At least one --keep path required")
29+
return v
30+
31+
@field_validator("keep", mode="after")
32+
@classmethod
33+
def validate_glob_paths(cls, v: tuple[str, ...]) -> tuple[str, ...]:
34+
for path in v:
35+
if not path:
36+
raise ValueError("Keep path cannot be empty")
37+
glob_translate(path)
38+
return v
39+
40+
@field_validator("sync_branch", "main_branch", "private_branch", mode="after")
41+
@classmethod
42+
def validate_branch_name(cls, v: str) -> str:
43+
if not v:
44+
raise ValueError("Branch name cannot be empty")
45+
if v.startswith("/") or ".." in v:
46+
raise ValueError(f"Invalid branch name: {v!r}")
47+
return v
48+
49+
650
@click.command()
751
@click.option("--private", required=True, help="Private repo path or URL")
852
@click.option("--public", required=True, help="Public repo path or URL")
@@ -35,18 +79,30 @@ def main(
3579
"""Sync filtered commits from private to public repository."""
3680

3781
try:
38-
result = sync(
82+
config = SyncConfig(
3983
private=private,
4084
public=public,
4185
keep=keep,
42-
keep_from_file=keep_from_file,
86+
keep_from_file=Path(keep_from_file) if keep_from_file else None,
4387
sync_branch=sync_branch,
4488
main_branch=main_branch,
4589
private_branch=private_branch,
4690
dry_run=dry_run,
4791
merge=merge,
4892
force=force,
4993
)
94+
result = sync(
95+
private=config.private,
96+
public=config.public,
97+
keep=config.keep,
98+
keep_from_file=config.keep_from_file,
99+
sync_branch=config.sync_branch,
100+
main_branch=config.main_branch,
101+
private_branch=config.private_branch,
102+
dry_run=config.dry_run,
103+
merge=config.merge,
104+
force=config.force,
105+
)
50106
except ValueError as e:
51107
raise click.ClickException(str(e))
52108

git_sync_filtered/sync.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ def read_paths_from_file(path: Path) -> list[str]:
2020

2121

2222
def collect_paths_to_keep(
23-
keep: tuple[str, ...], keep_from_file: Optional[str]
23+
keep: tuple[str, ...], keep_from_file: Optional[Path]
2424
) -> list[str]:
25-
paths_to_keep = set(keep)
25+
paths_to_keep: set[str] = set(keep)
2626

2727
if keep_from_file:
28-
paths_to_keep.update(read_paths_from_file(Path(keep_from_file)))
28+
paths_to_keep.update(read_paths_from_file(keep_from_file))
2929

30-
return sorted(list(paths_to_keep))
30+
return sorted(paths_to_keep)
3131

3232

3333
def run_filter_repo(repo_path: Path | str, paths_to_keep: list[str]) -> None:
@@ -95,7 +95,7 @@ def sync(
9595
private: str,
9696
public: str,
9797
keep: tuple[str, ...],
98-
keep_from_file: Optional[str],
98+
keep_from_file: Optional[Path],
9999
sync_branch: str,
100100
main_branch: str,
101101
private_branch: str,

pyproject.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ classifiers = [
2222
"Programming Language :: Python :: 3.14",
2323
]
2424

25-
dependencies = ["click>=8.0", "gitpython>=3.1", "git-filter-repo>=2.0"]
25+
dependencies = [
26+
"click>=8.0",
27+
"gitpython>=3.1",
28+
"git-filter-repo>=2.0",
29+
"pydantic>=2.12.5",
30+
]
2631

2732
[project.optional-dependencies]
2833
dev = ["pytest", "ruff", "mypy"]
@@ -37,7 +42,7 @@ include = ["git_sync_filtered*"]
3742
[tool.ruff]
3843
line-length = 100
3944
target-version = "py310"
40-
select = ["E", "F", "ANN"]
45+
lint.select = ["E", "F", "ANN"]
4146

4247
[dependency-groups]
4348
dev = ["pytest>=9.0.2"]

tests/unit/test_collect_paths.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def test_collect_paths_to_keep_combines_args_and_file(tmp_path: Path) -> None:
1414

1515
result = collect_paths_to_keep(
1616
keep=("src",),
17-
keep_from_file=str(file_path),
17+
keep_from_file=file_path,
1818
)
1919

2020
assert result == ["lib", "src", "tests"]

0 commit comments

Comments
 (0)