Skip to content

Commit

Permalink
feat: repository/worktree initialization methods
Browse files Browse the repository at this point in the history
Both `Repo` and `Worktree` have received `init_at()` class methods.
The present implementation is focused on bare necessities:
(re)initialization in an existing directory.

The `Worktree.init_at()` implementation supports the git-init feature
for repository relocation, and it takes care of suitable instance
invalidation. This is a prelimary implementation. The criticial test
will be whether `git-init` can relocate a repository with an annex.
A corresponding tests needs to be added when an `init_annex()` method
becomes available.
  • Loading branch information
mih committed Oct 14, 2024
1 parent c699f04 commit 1cbb796
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 0 deletions.
17 changes: 17 additions & 0 deletions datalad_core/repo/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
get_manager,
)
from datalad_core.repo.gitmanaged import GitManaged
from datalad_core.runners import call_git


class Repo(GitManaged):
Expand Down Expand Up @@ -85,3 +86,19 @@ def config(self) -> ConfigManager:
)
self._config = lman
return self._config

@classmethod
def init_at(cls, path: Path) -> Repo:
"""Initialize a bare repository in an existing directory
There is no test for an existing repository at ``path``. A potential
reinitialization is generally safe. Use cases are described in the
``git init`` documentation.
"""
# TODO: support --shared, needs to establish ENUM for options
call_git(
['init', '--bare'],
cwd=path,
capture_output=True,
)
return cls(path)
11 changes: 11 additions & 0 deletions datalad_core/repo/tests/test_repo.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from shutil import rmtree

import pytest

from ..repo import Repo


Expand All @@ -25,3 +27,12 @@ def test_repo_vanish(baregitrepo):
assert repo.flyweight_valid()
rmtree(baregitrepo)
assert not repo.flyweight_valid()


def test_repo_init_at(tmp_path):
# only existing directories
with pytest.raises((FileNotFoundError, NotADirectoryError)):
Repo.init_at(tmp_path / 'nothere')

repo = Repo.init_at(tmp_path)
assert repo.path == tmp_path.absolute()
47 changes: 47 additions & 0 deletions datalad_core/repo/tests/test_worktree.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from datalad_core.config import ConfigItem
from datalad_core.runners import call_git

Expand Down Expand Up @@ -88,3 +90,48 @@ def test_secondary_worktree(gitrepo):
assert rwd[wt1] == 'wt1'
assert rwd[wt2] == 'wt2'
assert rwd[wt1.repo] == 'repo'


def test_worktree_init_at(tmp_path):
# only existing directories
with pytest.raises((FileNotFoundError, NotADirectoryError)):
Worktree.init_at(tmp_path / 'nothere')

orig_wt_path = tmp_path / 'orig_wt'
orig_wt_path.mkdir()
orig_wt = Worktree.init_at(orig_wt_path)
assert orig_wt.path == orig_wt_path.absolute()
# cursory test that the repo is functional
assert orig_wt.repo.config['core.bare'].value is False
assert orig_wt.config['core.bare'].value is False

# init alternative worktree. This is not a "linked" worktree.
# instead this merely points to the same repository. changes
# made in this worktree will cause unsychronized differences
# at `orig_wt`. Likely not a use case, but we are testing the
# proper functioning of the mechanics anyways
alt_wt_path = tmp_path / 'alt_wt'
alt_wt_path.mkdir()
alt_wt = Worktree.init_at(alt_wt_path, gitdir=orig_wt.repo.path)
assert alt_wt.path == alt_wt_path.absolute()
assert alt_wt.config['core.bare'].value is False
assert orig_wt.repo is alt_wt.repo

# try relocating the repository of a worktree
sep_wt_path = tmp_path / 'sep_wt'
sep_wt_path.mkdir()
sep_repo_path = tmp_path / 'sep_repo'
sep_repo_path.mkdir()
sep_wt = Worktree.init_at(sep_wt_path, gitdir=sep_repo_path)
assert sep_wt.config['core.bare'].value is False
# relocate
assert sep_repo_path.is_dir()
# keep ref for testing
sep_repo_old = sep_wt.repo
sep_repo_path_new = tmp_path / 'sep_repo_new'
sep_wt_new = Worktree.init_at(sep_wt_path, gitdir=sep_repo_path_new)
assert sep_wt_new is sep_wt
assert not sep_repo_path.is_dir()
# we got a new instance for the repo
assert sep_wt.repo is not sep_repo_old
assert sep_wt.repo.path == sep_repo_path_new
31 changes: 31 additions & 0 deletions datalad_core/repo/worktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
)
from datalad_core.repo.gitmanaged import GitManaged
from datalad_core.repo.repo import Repo
from datalad_core.runners import call_git


class Worktree(GitManaged):
Expand Down Expand Up @@ -123,3 +124,33 @@ def repo(self) -> Repo:
if self._repo is None:
self._repo = Repo(self.git_common_dir)
return self._repo

@classmethod
def init_at(cls, path: Path, gitdir: Path | None = None) -> Worktree:
"""Initialize a worktree for a new/existing repository in a directory
A worktree will be (re)initialized at ``path``.
If ``gitdir`` is given it will be passed to
``git init --separate-git-dir``. Depending on whether the location at
``path`` has already been initialized, an existing repository will
be relocated (see ``git init`` documentation).
"""
cmd = ['init']
if gitdir is not None:
cmd.extend(('--separate-git-dir', str(gitdir)))
# TODO: support --shared, needs to establish ENUM for options
call_git(
cmd,
cwd=path,
capture_output=True,
)
wt = cls(path)
if gitdir is not None:
# this call could have relocated the underlying repo.
# drop all previous references and evaluate from scratch.
# we could do upfront inspection instead, but this is
# resonably cheap, and safeer to do unconditionally.
wt.repo.reset()
wt.reset()
return wt

0 comments on commit 1cbb796

Please sign in to comment.