Skip to content

Commit a67b144

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 47ae839 commit a67b144

File tree

2 files changed

+126
-6
lines changed

2 files changed

+126
-6
lines changed

src/libvcs/cmd/git.py

+44-6
Original file line numberDiff line numberDiff line change
@@ -1027,26 +1027,29 @@ def pull(
10271027
def init(
10281028
self,
10291029
*,
1030-
template: str | None = None,
1030+
template: str | pathlib.Path | None = None,
10311031
separate_git_dir: StrOrBytesPath | None = None,
10321032
object_format: t.Literal["sha1", "sha256"] | None = None,
10331033
branch: str | None = None,
10341034
initial_branch: str | None = None,
10351035
shared: bool
1036-
| Literal[false, true, umask, group, all, world, everybody]
1037-
| str
1036+
| t.Literal["false", "true", "umask", "group", "all", "world", "everybody"]
1037+
| str # Octal number string (e.g., "0660")
10381038
| None = None,
10391039
quiet: bool | None = None,
10401040
bare: bool | None = None,
1041+
ref_format: t.Literal["files", "reftable"] | None = None,
1042+
default: bool | None = None,
10411043
# libvcs special behavior
10421044
check_returncode: bool | None = None,
1045+
make_parents: bool = True,
10431046
**kwargs: t.Any,
10441047
) -> str:
10451048
"""Create empty repo. Wraps `git init <https://git-scm.com/docs/git-init>`_.
10461049
10471050
Parameters
10481051
----------
1049-
template : str, optional
1052+
template : str | pathlib.Path, optional
10501053
Directory from which templates will be used. The template directory
10511054
contains files and directories that will be copied to the $GIT_DIR
10521055
after it is created. The template directory will be one of the
@@ -1080,17 +1083,27 @@ def init(
10801083
- umask: Use permissions specified by umask
10811084
- group: Make the repository group-writable
10821085
- all, world, everybody: Same as world, make repo readable by all users
1083-
- An octal number: Explicit mode specification (e.g., "0660")
1086+
- An octal number string: Explicit mode specification (e.g., "0660")
10841087
quiet : bool, optional
10851088
Only print error and warning messages; all other output will be
10861089
suppressed. Useful for scripting.
10871090
bare : bool, optional
10881091
Create a bare repository. If GIT_DIR environment is not set, it is set
10891092
to the current working directory. Bare repositories have no working
10901093
tree and are typically used as central repositories.
1094+
ref_format : "files" | "reftable", optional
1095+
Specify the reference storage format. Requires git version >= 2.37.0.
1096+
- files: Classic format with packed-refs and loose refs (default)
1097+
- reftable: New format that is more efficient for large repositories
1098+
default : bool, optional
1099+
Use default permissions for directories and files. This is the same as
1100+
running git init without any options.
10911101
check_returncode : bool, optional
10921102
If True, check the return code of the git command and raise a
10931103
CalledProcessError if it is non-zero.
1104+
make_parents : bool, default: True
1105+
If True, create the target directory if it doesn't exist. If False,
1106+
raise an error if the directory doesn't exist.
10941107
10951108
Returns
10961109
-------
@@ -1101,6 +1114,10 @@ def init(
11011114
------
11021115
CalledProcessError
11031116
If the git command fails and check_returncode is True.
1117+
ValueError
1118+
If invalid parameters are provided.
1119+
FileNotFoundError
1120+
If make_parents is False and the target directory doesn't exist.
11041121
11051122
Examples
11061123
--------
@@ -1142,6 +1159,14 @@ def init(
11421159
>>> git.init(shared='group')
11431160
'Initialized empty shared Git repository in ...'
11441161
1162+
Create with octal permissions:
1163+
1164+
>>> shared_repo = tmp_path / 'shared_octal_example'
1165+
>>> shared_repo.mkdir()
1166+
>>> git = Git(path=shared_repo)
1167+
>>> git.init(shared='0660')
1168+
'Initialized empty shared Git repository in ...'
1169+
11451170
Create with a template directory:
11461171
11471172
>>> template_repo = tmp_path / 'template_example'
@@ -1214,18 +1239,31 @@ def init(
12141239
shared_str.isdigit()
12151240
and len(shared_str) <= 4
12161241
and all(c in string.octdigits for c in shared_str)
1242+
and int(shared_str, 8) <= 0o777 # Validate octal range
12171243
)
12181244
):
12191245
msg = (
12201246
f"Invalid shared value. Must be one of {valid_shared_values} "
1221-
"or an octal number"
1247+
"or a valid octal number between 0000 and 0777"
12221248
)
12231249
raise ValueError(msg)
12241250
local_flags.append(f"--shared={shared}")
1251+
12251252
if quiet is True:
12261253
local_flags.append("--quiet")
12271254
if bare is True:
12281255
local_flags.append("--bare")
1256+
if ref_format is not None:
1257+
local_flags.append(f"--ref-format={ref_format}")
1258+
if default is True:
1259+
local_flags.append("--default")
1260+
1261+
# libvcs special behavior
1262+
if make_parents and not self.path.exists():
1263+
self.path.mkdir(parents=True)
1264+
elif not self.path.exists():
1265+
msg = f"Directory does not exist: {self.path}"
1266+
raise FileNotFoundError(msg)
12291267

12301268
return self.run(
12311269
["init", *local_flags, "--", *required_flags],

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)