Skip to content

Commit 908b72a

Browse files
committed
feat: enhance Git.init with ref-format and improved validation
- Add ref-format parameter support for git init - Add make_parents parameter to control directory creation - Improve type hints and validation for template and shared parameters - Add comprehensive tests for all shared values and octal permissions - Add validation for octal number range in shared parameter
1 parent c2b3361 commit 908b72a

File tree

2 files changed

+128
-8
lines changed

2 files changed

+128
-8
lines changed

src/libvcs/cmd/git.py

+46-8
Original file line numberDiff line numberDiff line change
@@ -1031,26 +1031,29 @@ def pull(
10311031
def init(
10321032
self,
10331033
*,
1034-
template: str | None = None,
1034+
template: str | pathlib.Path | None = None,
10351035
separate_git_dir: StrOrBytesPath | None = None,
10361036
object_format: t.Literal["sha1", "sha256"] | None = None,
10371037
branch: str | None = None,
10381038
initial_branch: str | None = None,
10391039
shared: bool
1040-
| Literal[false, true, umask, group, all, world, everybody]
1041-
| str
1040+
| t.Literal["false", "true", "umask", "group", "all", "world", "everybody"]
1041+
| str # Octal number string (e.g., "0660")
10421042
| None = None,
10431043
quiet: bool | None = None,
10441044
bare: bool | None = None,
1045+
ref_format: t.Literal["files", "reftable"] | None = None,
1046+
default: bool | None = None,
10451047
# libvcs special behavior
10461048
check_returncode: bool | None = None,
1049+
make_parents: bool = True,
10471050
**kwargs: t.Any,
10481051
) -> str:
10491052
"""Create empty repo. Wraps `git init <https://git-scm.com/docs/git-init>`_.
10501053
10511054
Parameters
10521055
----------
1053-
template : str, optional
1056+
template : str | pathlib.Path, optional
10541057
Directory from which templates will be used. The template directory
10551058
contains files and directories that will be copied to the $GIT_DIR
10561059
after it is created. The template directory will be one of the
@@ -1084,17 +1087,27 @@ def init(
10841087
- umask: Use permissions specified by umask
10851088
- group: Make the repository group-writable
10861089
- all, world, everybody: Same as world, make repo readable by all users
1087-
- An octal number: Explicit mode specification (e.g., "0660")
1090+
- An octal number string: Explicit mode specification (e.g., "0660")
10881091
quiet : bool, optional
10891092
Only print error and warning messages; all other output will be
10901093
suppressed. Useful for scripting.
10911094
bare : bool, optional
10921095
Create a bare repository. If GIT_DIR environment is not set, it is set
10931096
to the current working directory. Bare repositories have no working
10941097
tree and are typically used as central repositories.
1098+
ref_format : "files" | "reftable", optional
1099+
Specify the reference storage format. Requires git version >= 2.37.0.
1100+
- files: Classic format with packed-refs and loose refs (default)
1101+
- reftable: New format that is more efficient for large repositories
1102+
default : bool, optional
1103+
Use default permissions for directories and files. This is the same as
1104+
running git init without any options.
10951105
check_returncode : bool, optional
10961106
If True, check the return code of the git command and raise a
10971107
CalledProcessError if it is non-zero.
1108+
make_parents : bool, default: True
1109+
If True, create the target directory if it doesn't exist. If False,
1110+
raise an error if the directory doesn't exist.
10981111
10991112
Returns
11001113
-------
@@ -1105,6 +1118,10 @@ def init(
11051118
------
11061119
CalledProcessError
11071120
If the git command fails and check_returncode is True.
1121+
ValueError
1122+
If invalid parameters are provided.
1123+
FileNotFoundError
1124+
If make_parents is False and the target directory doesn't exist.
11081125
11091126
Examples
11101127
--------
@@ -1146,6 +1163,14 @@ def init(
11461163
>>> git.init(shared='group')
11471164
'Initialized empty shared Git repository in ...'
11481165
1166+
Create with octal permissions:
1167+
1168+
>>> shared_repo = tmp_path / 'shared_octal_example'
1169+
>>> shared_repo.mkdir()
1170+
>>> git = Git(path=shared_repo)
1171+
>>> git.init(shared='0660')
1172+
'Initialized empty shared Git repository in ...'
1173+
11491174
Create with a template directory:
11501175
11511176
>>> template_repo = tmp_path / 'template_example'
@@ -1218,18 +1243,31 @@ def init(
12181243
shared_str.isdigit()
12191244
and len(shared_str) <= 4
12201245
and all(c in string.octdigits for c in shared_str)
1246+
and int(shared_str, 8) <= 0o777 # Validate octal range
12211247
)
12221248
):
12231249
msg = (
12241250
f"Invalid shared value. Must be one of {valid_shared_values} "
1225-
"or an octal number"
1251+
"or a valid octal number between 0000 and 0777"
12261252
)
12271253
raise ValueError(msg)
12281254
local_flags.append(f"--shared={shared}")
1255+
12291256
if quiet is True:
12301257
local_flags.append("--quiet")
12311258
if bare is True:
12321259
local_flags.append("--bare")
1260+
if ref_format is not None:
1261+
local_flags.append(f"--ref-format={ref_format}")
1262+
if default is True:
1263+
local_flags.append("--default")
1264+
1265+
# libvcs special behavior
1266+
if make_parents and not self.path.exists():
1267+
self.path.mkdir(parents=True)
1268+
elif not self.path.exists():
1269+
msg = f"Directory does not exist: {self.path}"
1270+
raise FileNotFoundError(msg)
12331271

12341272
return self.run(
12351273
["init", *local_flags, "--", *required_flags],
@@ -2863,7 +2901,7 @@ def set_url(
28632901
)
28642902

28652903

2866-
GitRemoteManagerLiteral = Literal[
2904+
GitRemoteManagerLiteral = t.Literal[
28672905
"--verbose",
28682906
"add",
28692907
"rename",
@@ -2933,7 +2971,7 @@ def run(
29332971
# Pass-through to run()
29342972
log_in_real_time: bool = False,
29352973
check_returncode: bool | None = None,
2936-
**kwargs: Any,
2974+
**kwargs: t.Any,
29372975
) -> str:
29382976
"""Run a command against a git repository's remotes.
29392977

tests/cmd/test_git.py

+82
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,85 @@ def test_git_init_validation_errors(tmp_path: pathlib.Path) -> None:
171171
# Test invalid octal number for shared
172172
with pytest.raises(ValueError, match="Invalid shared value"):
173173
repo.init(shared="8888") # Invalid octal number
174+
175+
# Test octal number out of range
176+
with pytest.raises(ValueError, match="Invalid shared value"):
177+
repo.init(shared="1000") # Octal number > 0777
178+
179+
# Test non-existent directory with make_parents=False
180+
non_existent = tmp_path / "non_existent"
181+
with pytest.raises(FileNotFoundError, match="Directory does not exist"):
182+
repo = git.Git(path=non_existent)
183+
repo.init(make_parents=False)
184+
185+
186+
def test_git_init_shared_octal(tmp_path: pathlib.Path) -> None:
187+
"""Test git init with shared octal permissions."""
188+
repo = git.Git(path=tmp_path)
189+
190+
# Test valid octal numbers
191+
for octal in ["0660", "0644", "0755"]:
192+
repo_dir = tmp_path / f"shared_{octal}"
193+
repo_dir.mkdir()
194+
repo = git.Git(path=repo_dir)
195+
result = repo.init(shared=octal)
196+
assert "Initialized empty shared Git repository" in result
197+
198+
199+
def test_git_init_shared_values(tmp_path: pathlib.Path) -> None:
200+
"""Test git init with all valid shared values."""
201+
valid_values = ["false", "true", "umask", "group", "all", "world", "everybody"]
202+
203+
for value in valid_values:
204+
repo_dir = tmp_path / f"shared_{value}"
205+
repo_dir.mkdir()
206+
repo = git.Git(path=repo_dir)
207+
result = repo.init(shared=value)
208+
# The output message varies between git versions and shared values
209+
assert any(
210+
msg in result
211+
for msg in [
212+
"Initialized empty Git repository",
213+
"Initialized empty shared Git repository",
214+
]
215+
)
216+
217+
218+
def test_git_init_ref_format(tmp_path: pathlib.Path) -> None:
219+
"""Test git init with different ref formats."""
220+
repo = git.Git(path=tmp_path)
221+
222+
# Test with files format (default)
223+
result = repo.init()
224+
assert "Initialized empty Git repository" in result
225+
226+
# Test with reftable format (requires git >= 2.37.0)
227+
repo_dir = tmp_path / "reftable"
228+
repo_dir.mkdir()
229+
repo = git.Git(path=repo_dir)
230+
try:
231+
result = repo.init(ref_format="reftable")
232+
assert "Initialized empty Git repository" in result
233+
except Exception as e:
234+
if "unknown option" in str(e):
235+
pytest.skip("ref-format option not supported in this git version")
236+
raise
237+
238+
239+
def test_git_init_make_parents(tmp_path: pathlib.Path) -> None:
240+
"""Test git init with make_parents flag."""
241+
deep_path = tmp_path / "a" / "b" / "c"
242+
243+
# Test with make_parents=True (default)
244+
repo = git.Git(path=deep_path)
245+
result = repo.init()
246+
assert "Initialized empty Git repository" in result
247+
assert deep_path.exists()
248+
assert (deep_path / ".git").is_dir()
249+
250+
# Test with make_parents=False on existing directory
251+
existing_path = tmp_path / "existing"
252+
existing_path.mkdir()
253+
repo = git.Git(path=existing_path)
254+
result = repo.init(make_parents=False)
255+
assert "Initialized empty Git repository" in result

0 commit comments

Comments
 (0)