From c2ca356028fc55e25b46a8617073eb7b09bbdf96 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Feb 2025 10:33:27 -0600 Subject: [PATCH 1/6] feat(git): enhance git init support with all options and tests - Add support for all git-init options (template, separate_git_dir, object_format, etc.) - Add comprehensive tests for each option - Fix path handling for separate_git_dir - Fix string formatting for bytes paths - Update docstrings with examples for all options --- src/libvcs/cmd/git.py | 102 ++++++++++++++++++++++++++------------ tests/cmd/test_git.py | 113 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 31 deletions(-) diff --git a/src/libvcs/cmd/git.py b/src/libvcs/cmd/git.py index f32ce9c4..4a536b45 100644 --- a/src/libvcs/cmd/git.py +++ b/src/libvcs/cmd/git.py @@ -1031,7 +1031,7 @@ def init( object_format: t.Literal["sha1", "sha256"] | None = None, branch: str | None = None, initial_branch: str | None = None, - shared: bool | None = None, + shared: bool | str | None = None, quiet: bool | None = None, bare: bool | None = None, # libvcs special behavior @@ -1042,60 +1042,100 @@ def init( Parameters ---------- - quiet : bool - ``--quiet`` - bare : bool - ``--bare`` - object_format : - Hash algorithm used for objects. SHA-256 is still experimental as of git - 2.36.0. + template : str, optional + Directory from which templates will be used. The template directory + contains files and directories that will be copied to the $GIT_DIR + after it is created. + separate_git_dir : :attr:`libvcs._internal.types.StrOrBytesPath`, optional + Instead of placing the git repository in /.git/, place it in + the specified path. + object_format : "sha1" | "sha256", optional + Specify the hash algorithm to use. The default is sha1. Note that + sha256 is still experimental in git. + branch : str, optional + Use the specified name for the initial branch. If not specified, fall + back to the default name (currently "master"). + initial_branch : str, optional + Alias for branch parameter. Specify the name for the initial branch. + shared : bool | str, optional + Specify that the git repository is to be shared amongst several users. + Can be 'false', 'true', 'umask', 'group', 'all', 'world', + 'everybody', or an octal number. + quiet : bool, optional + Only print error and warning messages; all other output will be + suppressed. + bare : bool, optional + Create a bare repository. If GIT_DIR environment is not set, it is set + to the current working directory. Examples -------- - >>> new_repo = tmp_path / 'example' - >>> new_repo.mkdir() - >>> git = Git(path=new_repo) + >>> git = Git(path=tmp_path) >>> git.init() 'Initialized empty Git repository in ...' - >>> pathlib.Path(new_repo / 'test').write_text('foo', 'utf-8') - 3 - >>> git.run(['add', '.']) - '' - Bare: + Create with a specific initial branch name: - >>> new_repo = tmp_path / 'example1' + >>> new_repo = tmp_path / 'branch_example' >>> new_repo.mkdir() >>> git = Git(path=new_repo) + >>> git.init(branch='main') + 'Initialized empty Git repository in ...' + + Create a bare repository: + + >>> bare_repo = tmp_path / 'bare_example' + >>> bare_repo.mkdir() + >>> git = Git(path=bare_repo) >>> git.init(bare=True) 'Initialized empty Git repository in ...' - >>> pathlib.Path(new_repo / 'HEAD').exists() - True - Existing repo: + Create with a separate git directory: - >>> git = Git(path=new_repo) - >>> git = Git(path=example_git_repo.path) - >>> git_remote_repo = create_git_remote_repo() - >>> git.init() - 'Reinitialized existing Git repository in ...' + >>> repo_path = tmp_path / 'repo' + >>> git_dir = tmp_path / 'git_dir' + >>> repo_path.mkdir() + >>> git_dir.mkdir() + >>> git = Git(path=repo_path) + >>> git.init(separate_git_dir=str(git_dir.absolute())) + 'Initialized empty Git repository in ...' + + Create with shared permissions: + >>> shared_repo = tmp_path / 'shared_example' + >>> shared_repo.mkdir() + >>> git = Git(path=shared_repo) + >>> git.init(shared='group') + 'Initialized empty shared Git repository in ...' + + Create with a template directory: + + >>> template_repo = tmp_path / 'template_example' + >>> template_repo.mkdir() + >>> git = Git(path=template_repo) + >>> git.init(template=str(tmp_path)) + 'Initialized empty Git repository in ...' """ - required_flags: list[str] = [str(self.path)] local_flags: list[str] = [] + required_flags: list[str] = [str(self.path)] if template is not None: local_flags.append(f"--template={template}") if separate_git_dir is not None: - local_flags.append(f"--separate-git-dir={separate_git_dir!r}") + if isinstance(separate_git_dir, pathlib.Path): + separate_git_dir = str(separate_git_dir.absolute()) + local_flags.append(f"--separate-git-dir={separate_git_dir!s}") if object_format is not None: local_flags.append(f"--object-format={object_format}") if branch is not None: - local_flags.extend(["--branch", branch]) - if initial_branch is not None: + local_flags.extend(["--initial-branch", branch]) + elif initial_branch is not None: local_flags.extend(["--initial-branch", initial_branch]) - if shared is True: - local_flags.append("--shared") + if shared is not None: + if isinstance(shared, bool): + local_flags.append("--shared") + else: + local_flags.append(f"--shared={shared}") if quiet is True: local_flags.append("--quiet") if bare is True: diff --git a/tests/cmd/test_git.py b/tests/cmd/test_git.py index 1aa15560..2445b461 100644 --- a/tests/cmd/test_git.py +++ b/tests/cmd/test_git.py @@ -19,3 +19,116 @@ def test_git_constructor( repo = git.Git(path=path_type(tmp_path)) assert repo.path == tmp_path + + +def test_git_init_basic(tmp_path: pathlib.Path) -> None: + """Test basic git init functionality.""" + repo = git.Git(path=tmp_path) + result = repo.init() + assert "Initialized empty Git repository" in result + assert (tmp_path / ".git").is_dir() + + +def test_git_init_bare(tmp_path: pathlib.Path) -> None: + """Test git init with bare repository.""" + repo = git.Git(path=tmp_path) + result = repo.init(bare=True) + assert "Initialized empty Git repository" in result + # Bare repos have files directly in the directory + assert (tmp_path / "HEAD").exists() + + +def test_git_init_template(tmp_path: pathlib.Path) -> None: + """Test git init with template directory.""" + template_dir = tmp_path / "template" + template_dir.mkdir() + (template_dir / "hooks").mkdir() + (template_dir / "hooks" / "pre-commit").write_text("#!/bin/sh\nexit 0\n") + + repo_dir = tmp_path / "repo" + repo_dir.mkdir() + repo = git.Git(path=repo_dir) + result = repo.init(template=str(template_dir)) + + assert "Initialized empty Git repository" in result + assert (repo_dir / ".git" / "hooks" / "pre-commit").exists() + + +def test_git_init_separate_git_dir(tmp_path: pathlib.Path) -> None: + """Test git init with separate git directory.""" + repo_dir = tmp_path / "repo" + git_dir = tmp_path / "git_dir" + repo_dir.mkdir() + git_dir.mkdir() + + repo = git.Git(path=repo_dir) + result = repo.init(separate_git_dir=str(git_dir.absolute())) + + assert "Initialized empty Git repository" in result + assert git_dir.is_dir() + assert (git_dir / "HEAD").exists() + + +def test_git_init_initial_branch(tmp_path: pathlib.Path) -> None: + """Test git init with custom initial branch name.""" + repo = git.Git(path=tmp_path) + result = repo.init(branch="main") + + assert "Initialized empty Git repository" in result + # Check if HEAD points to the correct branch + head_content = (tmp_path / ".git" / "HEAD").read_text() + assert "ref: refs/heads/main" in head_content + + +def test_git_init_shared(tmp_path: pathlib.Path) -> None: + """Test git init with shared repository settings.""" + repo = git.Git(path=tmp_path) + + # Test boolean shared + result = repo.init(shared=True) + assert "Initialized empty shared Git repository" in result + + # Test string shared value + repo_dir = tmp_path / "shared_group" + repo_dir.mkdir() + repo = git.Git(path=repo_dir) + result = repo.init(shared="group") + assert "Initialized empty shared Git repository" in result + + +def test_git_init_quiet(tmp_path: pathlib.Path) -> None: + """Test git init with quiet flag.""" + repo = git.Git(path=tmp_path) + result = repo.init(quiet=True) + # Quiet mode should suppress normal output + assert result == "" or "Initialized empty Git repository" not in result + + +def test_git_init_object_format(tmp_path: pathlib.Path) -> None: + """Test git init with different object formats.""" + repo = git.Git(path=tmp_path) + + # Test with sha1 (default) + result = repo.init(object_format="sha1") + assert "Initialized empty Git repository" in result + + # Note: sha256 test is commented out as it might not be supported in all + # git versions + # repo_dir = tmp_path / "sha256" + # repo_dir.mkdir() + # repo = git.Git(path=repo_dir) + # result = repo.init(object_format="sha256") + # assert "Initialized empty Git repository" in result + + +def test_git_reinit(tmp_path: pathlib.Path) -> None: + """Test reinitializing an existing repository.""" + repo = git.Git(path=tmp_path) + + # Initial init + first_result = repo.init() + assert "Initialized empty Git repository" in first_result + + # Reinit + second_result = repo.init() + assert "Reinitialized existing Git repository" in second_result From 7ef3b25d3f0238ea59e87f92b58841cb9999a373 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Feb 2025 10:43:11 -0600 Subject: [PATCH 2/6] docs: improve Git.init documentation and validation tests - Enhance Git.init docstrings with detailed parameter descriptions - Add comprehensive examples including SHA-256 object format - Add return value and exception documentation - Improve type hints for shared parameter with Literal types - Add extensive validation tests for all parameters --- src/libvcs/cmd/git.py | 58 ++++++++++++++++++++++++++++++++++++------- tests/cmd/test_git.py | 39 +++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/libvcs/cmd/git.py b/src/libvcs/cmd/git.py index 4a536b45..d3314f94 100644 --- a/src/libvcs/cmd/git.py +++ b/src/libvcs/cmd/git.py @@ -1031,7 +1031,10 @@ def init( object_format: t.Literal["sha1", "sha256"] | None = None, branch: str | None = None, initial_branch: str | None = None, - shared: bool | str | None = None, + shared: bool + | Literal[false, true, umask, group, all, world, everybody] + | str + | None = None, quiet: bool | None = None, bare: bool | None = None, # libvcs special behavior @@ -1045,28 +1048,58 @@ def init( template : str, optional Directory from which templates will be used. The template directory contains files and directories that will be copied to the $GIT_DIR - after it is created. + after it is created. The template directory will be one of the + following (in order): + - The argument given with the --template option + - The contents of the $GIT_TEMPLATE_DIR environment variable + - The init.templateDir configuration variable + - The default template directory: /usr/share/git-core/templates separate_git_dir : :attr:`libvcs._internal.types.StrOrBytesPath`, optional Instead of placing the git repository in /.git/, place it in - the specified path. + the specified path. The .git file at /.git will contain a + gitfile that points to the separate git dir. This is useful when you + want to store the git directory on a different disk or filesystem. object_format : "sha1" | "sha256", optional Specify the hash algorithm to use. The default is sha1. Note that - sha256 is still experimental in git. + sha256 is still experimental in git and requires git version >= 2.29.0. + Once the repository is created with a specific hash algorithm, it cannot + be changed. branch : str, optional Use the specified name for the initial branch. If not specified, fall - back to the default name (currently "master"). + back to the default name (currently "master", but may change based on + init.defaultBranch configuration). initial_branch : str, optional Alias for branch parameter. Specify the name for the initial branch. + This is provided for compatibility with newer git versions. shared : bool | str, optional Specify that the git repository is to be shared amongst several users. - Can be 'false', 'true', 'umask', 'group', 'all', 'world', - 'everybody', or an octal number. + Valid values are: + - false: Turn off sharing (default) + - true: Same as group + - umask: Use permissions specified by umask + - group: Make the repository group-writable + - all, world, everybody: Same as world, make repo readable by all users + - An octal number: Explicit mode specification (e.g., "0660") quiet : bool, optional Only print error and warning messages; all other output will be - suppressed. + suppressed. Useful for scripting. bare : bool, optional Create a bare repository. If GIT_DIR environment is not set, it is set - to the current working directory. + to the current working directory. Bare repositories have no working + tree and are typically used as central repositories. + check_returncode : bool, optional + If True, check the return code of the git command and raise a + CalledProcessError if it is non-zero. + + Returns + ------- + str + The output of the git init command. + + Raises + ------ + CalledProcessError + If the git command fails and check_returncode is True. Examples -------- @@ -1115,6 +1148,13 @@ def init( >>> git = Git(path=template_repo) >>> git.init(template=str(tmp_path)) 'Initialized empty Git repository in ...' + + Create with SHA-256 object format (requires git >= 2.29.0): + + >>> sha256_repo = tmp_path / 'sha256_example' + >>> sha256_repo.mkdir() + >>> git = Git(path=sha256_repo) + >>> git.init(object_format='sha256') # doctest: +SKIP """ local_flags: list[str] = [] required_flags: list[str] = [str(self.path)] diff --git a/tests/cmd/test_git.py b/tests/cmd/test_git.py index 2445b461..47d44cae 100644 --- a/tests/cmd/test_git.py +++ b/tests/cmd/test_git.py @@ -132,3 +132,42 @@ def test_git_reinit(tmp_path: pathlib.Path) -> None: # Reinit second_result = repo.init() assert "Reinitialized existing Git repository" in second_result + + +def test_git_init_validation_errors(tmp_path: pathlib.Path) -> None: + """Test validation errors in git init.""" + repo = git.Git(path=tmp_path) + + # Test invalid template type + with pytest.raises(TypeError, match="template must be a string or Path"): + repo.init(template=123) # type: ignore + + # Test non-existent template directory + with pytest.raises(ValueError, match="template directory does not exist"): + repo.init(template=str(tmp_path / "nonexistent")) + + # Test invalid object format + with pytest.raises( + ValueError, + match="object_format must be either 'sha1' or 'sha256'", + ): + repo.init(object_format="invalid") # type: ignore + + # Test specifying both branch and initial_branch + with pytest.raises( + ValueError, + match="Cannot specify both branch and initial_branch", + ): + repo.init(branch="main", initial_branch="master") + + # Test branch name with whitespace + with pytest.raises(ValueError, match="Branch name cannot contain whitespace"): + repo.init(branch="main branch") + + # Test invalid shared value + with pytest.raises(ValueError, match="Invalid shared value"): + repo.init(shared="invalid") + + # Test invalid octal number for shared + with pytest.raises(ValueError, match="Invalid shared value"): + repo.init(shared="8888") # Invalid octal number From cec0848d67f231777b8c9dd2c8d193fa587e79b4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Feb 2025 10:44:47 -0600 Subject: [PATCH 3/6] feat: add parameter validation for git init - Add validation for template parameter type and existence - Add validation for object_format parameter values - Improve type formatting for shared parameter - Complete docstring example output --- src/libvcs/cmd/git.py | 54 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/libvcs/cmd/git.py b/src/libvcs/cmd/git.py index d3314f94..4070fdfb 100644 --- a/src/libvcs/cmd/git.py +++ b/src/libvcs/cmd/git.py @@ -5,6 +5,7 @@ import datetime import pathlib import shlex +import string import typing as t from collections.abc import Sequence @@ -1155,26 +1156,71 @@ def init( >>> sha256_repo.mkdir() >>> git = Git(path=sha256_repo) >>> git.init(object_format='sha256') # doctest: +SKIP + 'Initialized empty Git repository in ...' """ local_flags: list[str] = [] required_flags: list[str] = [str(self.path)] if template is not None: + if not isinstance(template, (str, pathlib.Path)): + msg = "template must be a string or Path" + raise TypeError(msg) + template_path = pathlib.Path(template) + if not template_path.is_dir(): + msg = f"template directory does not exist: {template}" + raise ValueError(msg) local_flags.append(f"--template={template}") + if separate_git_dir is not None: if isinstance(separate_git_dir, pathlib.Path): separate_git_dir = str(separate_git_dir.absolute()) local_flags.append(f"--separate-git-dir={separate_git_dir!s}") + if object_format is not None: + if object_format not in {"sha1", "sha256"}: + msg = "object_format must be either 'sha1' or 'sha256'" + raise ValueError(msg) local_flags.append(f"--object-format={object_format}") - if branch is not None: - local_flags.extend(["--initial-branch", branch]) - elif initial_branch is not None: - local_flags.extend(["--initial-branch", initial_branch]) + + if branch is not None and initial_branch is not None: + msg = "Cannot specify both branch and initial_branch" + raise ValueError(msg) + + branch_name = branch or initial_branch + if branch_name is not None: + if any(c.isspace() for c in branch_name): + msg = "Branch name cannot contain whitespace" + raise ValueError(msg) + local_flags.extend(["--initial-branch", branch_name]) + if shared is not None: + valid_shared_values = { + "false", + "true", + "umask", + "group", + "all", + "world", + "everybody", + } if isinstance(shared, bool): local_flags.append("--shared") else: + shared_str = str(shared).lower() + # Check if it's a valid string value or an octal number + if not ( + shared_str in valid_shared_values + or ( + shared_str.isdigit() + and len(shared_str) <= 4 + and all(c in string.octdigits for c in shared_str) + ) + ): + msg = ( + f"Invalid shared value. Must be one of {valid_shared_values} " + "or an octal number" + ) + raise ValueError(msg) local_flags.append(f"--shared={shared}") if quiet is True: local_flags.append("--quiet") From 9a21fe0b74d61ec05643653e898a45a381111679 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Feb 2025 11:03:51 -0600 Subject: [PATCH 4/6] 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 --- src/libvcs/cmd/git.py | 50 ++++++++++++++++++++++---- tests/cmd/test_git.py | 82 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/src/libvcs/cmd/git.py b/src/libvcs/cmd/git.py index 4070fdfb..c885fd58 100644 --- a/src/libvcs/cmd/git.py +++ b/src/libvcs/cmd/git.py @@ -1027,26 +1027,29 @@ def pull( def init( self, *, - template: str | None = None, + template: str | pathlib.Path | None = None, separate_git_dir: StrOrBytesPath | None = None, object_format: t.Literal["sha1", "sha256"] | None = None, branch: str | None = None, initial_branch: str | None = None, shared: bool - | Literal[false, true, umask, group, all, world, everybody] - | str + | t.Literal["false", "true", "umask", "group", "all", "world", "everybody"] + | str # Octal number string (e.g., "0660") | None = None, quiet: bool | None = None, bare: bool | None = None, + ref_format: t.Literal["files", "reftable"] | None = None, + default: bool | None = None, # libvcs special behavior check_returncode: bool | None = None, + make_parents: bool = True, **kwargs: t.Any, ) -> str: """Create empty repo. Wraps `git init `_. Parameters ---------- - template : str, optional + template : str | pathlib.Path, optional Directory from which templates will be used. The template directory contains files and directories that will be copied to the $GIT_DIR after it is created. The template directory will be one of the @@ -1080,7 +1083,7 @@ def init( - umask: Use permissions specified by umask - group: Make the repository group-writable - all, world, everybody: Same as world, make repo readable by all users - - An octal number: Explicit mode specification (e.g., "0660") + - An octal number string: Explicit mode specification (e.g., "0660") quiet : bool, optional Only print error and warning messages; all other output will be suppressed. Useful for scripting. @@ -1088,9 +1091,19 @@ def init( Create a bare repository. If GIT_DIR environment is not set, it is set to the current working directory. Bare repositories have no working tree and are typically used as central repositories. + ref_format : "files" | "reftable", optional + Specify the reference storage format. Requires git version >= 2.37.0. + - files: Classic format with packed-refs and loose refs (default) + - reftable: New format that is more efficient for large repositories + default : bool, optional + Use default permissions for directories and files. This is the same as + running git init without any options. check_returncode : bool, optional If True, check the return code of the git command and raise a CalledProcessError if it is non-zero. + make_parents : bool, default: True + If True, create the target directory if it doesn't exist. If False, + raise an error if the directory doesn't exist. Returns ------- @@ -1101,6 +1114,10 @@ def init( ------ CalledProcessError If the git command fails and check_returncode is True. + ValueError + If invalid parameters are provided. + FileNotFoundError + If make_parents is False and the target directory doesn't exist. Examples -------- @@ -1142,6 +1159,14 @@ def init( >>> git.init(shared='group') 'Initialized empty shared Git repository in ...' + Create with octal permissions: + + >>> shared_repo = tmp_path / 'shared_octal_example' + >>> shared_repo.mkdir() + >>> git = Git(path=shared_repo) + >>> git.init(shared='0660') + 'Initialized empty shared Git repository in ...' + Create with a template directory: >>> template_repo = tmp_path / 'template_example' @@ -1214,18 +1239,31 @@ def init( shared_str.isdigit() and len(shared_str) <= 4 and all(c in string.octdigits for c in shared_str) + and int(shared_str, 8) <= 0o777 # Validate octal range ) ): msg = ( f"Invalid shared value. Must be one of {valid_shared_values} " - "or an octal number" + "or a valid octal number between 0000 and 0777" ) raise ValueError(msg) local_flags.append(f"--shared={shared}") + if quiet is True: local_flags.append("--quiet") if bare is True: local_flags.append("--bare") + if ref_format is not None: + local_flags.append(f"--ref-format={ref_format}") + if default is True: + local_flags.append("--default") + + # libvcs special behavior + if make_parents and not self.path.exists(): + self.path.mkdir(parents=True) + elif not self.path.exists(): + msg = f"Directory does not exist: {self.path}" + raise FileNotFoundError(msg) return self.run( ["init", *local_flags, "--", *required_flags], diff --git a/tests/cmd/test_git.py b/tests/cmd/test_git.py index 47d44cae..243f723c 100644 --- a/tests/cmd/test_git.py +++ b/tests/cmd/test_git.py @@ -171,3 +171,85 @@ def test_git_init_validation_errors(tmp_path: pathlib.Path) -> None: # Test invalid octal number for shared with pytest.raises(ValueError, match="Invalid shared value"): repo.init(shared="8888") # Invalid octal number + + # Test octal number out of range + with pytest.raises(ValueError, match="Invalid shared value"): + repo.init(shared="1000") # Octal number > 0777 + + # Test non-existent directory with make_parents=False + non_existent = tmp_path / "non_existent" + with pytest.raises(FileNotFoundError, match="Directory does not exist"): + repo = git.Git(path=non_existent) + repo.init(make_parents=False) + + +def test_git_init_shared_octal(tmp_path: pathlib.Path) -> None: + """Test git init with shared octal permissions.""" + repo = git.Git(path=tmp_path) + + # Test valid octal numbers + for octal in ["0660", "0644", "0755"]: + repo_dir = tmp_path / f"shared_{octal}" + repo_dir.mkdir() + repo = git.Git(path=repo_dir) + result = repo.init(shared=octal) + assert "Initialized empty shared Git repository" in result + + +def test_git_init_shared_values(tmp_path: pathlib.Path) -> None: + """Test git init with all valid shared values.""" + valid_values = ["false", "true", "umask", "group", "all", "world", "everybody"] + + for value in valid_values: + repo_dir = tmp_path / f"shared_{value}" + repo_dir.mkdir() + repo = git.Git(path=repo_dir) + result = repo.init(shared=value) + # The output message varies between git versions and shared values + assert any( + msg in result + for msg in [ + "Initialized empty Git repository", + "Initialized empty shared Git repository", + ] + ) + + +def test_git_init_ref_format(tmp_path: pathlib.Path) -> None: + """Test git init with different ref formats.""" + repo = git.Git(path=tmp_path) + + # Test with files format (default) + result = repo.init() + assert "Initialized empty Git repository" in result + + # Test with reftable format (requires git >= 2.37.0) + repo_dir = tmp_path / "reftable" + repo_dir.mkdir() + repo = git.Git(path=repo_dir) + try: + result = repo.init(ref_format="reftable") + assert "Initialized empty Git repository" in result + except Exception as e: + if "unknown option" in str(e): + pytest.skip("ref-format option not supported in this git version") + raise + + +def test_git_init_make_parents(tmp_path: pathlib.Path) -> None: + """Test git init with make_parents flag.""" + deep_path = tmp_path / "a" / "b" / "c" + + # Test with make_parents=True (default) + repo = git.Git(path=deep_path) + result = repo.init() + assert "Initialized empty Git repository" in result + assert deep_path.exists() + assert (deep_path / ".git").is_dir() + + # Test with make_parents=False on existing directory + existing_path = tmp_path / "existing" + existing_path.mkdir() + repo = git.Git(path=existing_path) + result = repo.init(make_parents=False) + assert "Initialized empty Git repository" in result From 900fcc92eecdfd3e715e2061d6c1e5b369fa613d Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Feb 2025 11:45:36 -0600 Subject: [PATCH 5/6] tests(git[cmd]) Assert behavior of `bare=True` --- tests/cmd/test_git.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/cmd/test_git.py b/tests/cmd/test_git.py index 243f723c..8c504e4d 100644 --- a/tests/cmd/test_git.py +++ b/tests/cmd/test_git.py @@ -34,8 +34,13 @@ def test_git_init_bare(tmp_path: pathlib.Path) -> None: repo = git.Git(path=tmp_path) result = repo.init(bare=True) assert "Initialized empty Git repository" in result - # Bare repos have files directly in the directory + + # Verify bare repository structure and configuration assert (tmp_path / "HEAD").exists() + config_path = tmp_path / "config" + assert config_path.exists(), "Config file does not exist in bare repository" + config_text = config_path.read_text() + assert "bare = true" in config_text, "Repository core.bare flag not set to true" def test_git_init_template(tmp_path: pathlib.Path) -> None: From 03acb4cf2cef772f7db0912694608e77c0e7a31f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 22 Feb 2025 11:42:05 -0600 Subject: [PATCH 6/6] docs(CHANGES) Note `Git.init` updates --- CHANGES | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES b/CHANGES index 64c7c3cd..5cefa1bc 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,18 @@ $ pip install --user --upgrade --pre libvcs +### New features + +#### cmd: Enhanced Git.init() with comprehensive validation and features (#487) + +- Added support for all git init parameters with full validation: + - `template`: Support for both string and Path objects with directory validation + - `separate_git_dir`: Support for custom git directory locations + - `object_format`: SHA-1/SHA-256 hash algorithm selection with validation + - `shared`: Extended support for all git-supported sharing modes including octal permissions + - `ref_format`: Support for 'files' and 'reftable' formats + - `make_parents`: Option to create parent directories automatically + ### Development - Cursor rules for development loop and git commit messages (#488)