Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support creating hard links #301 #310

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
41 changes: 30 additions & 11 deletions gitman/models/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
class Link:
source: str = ""
target: str = ""
symbolic: Optional[bool] = None

def __post_init__(self):
if self.symbolic is None:
self.symbolic = True


@dataclass
Expand Down Expand Up @@ -201,9 +206,12 @@ def create_links(self, root: str, *, force: bool = False):

for link in self.links:
target = os.path.join(root, os.path.normpath(link.target))
relpath = os.path.relpath(os.getcwd(), os.path.dirname(target))
source = os.path.join(relpath, os.path.normpath(link.source))
create_sym_link(source, target, force=force)
if link.symbolic:
relpath = os.path.relpath(os.getcwd(), os.path.dirname(target))
source = os.path.join(relpath, os.path.normpath(link.source))
else:
source = os.path.join(os.getcwd(), os.path.normpath(link.source))
create_sym_link(source, target, symbolic=link.symbolic, force=force)

def run_scripts(self, force: bool = False, show_shell_stdout: bool = False):
log.info("Running install scripts...")
Expand Down Expand Up @@ -336,16 +344,27 @@ def _invalid_repository(self):
return exceptions.InvalidRepository(msg)


def create_sym_link(source: str, target: str, *, force: bool):
log.info("Creating a symbolic link...")
def create_sym_link(source: str, target: str, symbolic, *, force: bool):
dxlr8r marked this conversation as resolved.
Show resolved Hide resolved

if symbolic:
log.info("Creating a symbolic link...")
else:
log.info("Creating a hard link...")

if os.path.islink(target):
os.remove(target)
elif os.path.exists(target):
if force:
shell.rm(target)
else:
msg = "Preexisting link location at {}".format(target)
raise exceptions.UncommittedChanges(msg)
if symbolic:
if force:
shell.rm(target)
else:
msg = "Preexisting link location at {}".format(target)
raise exceptions.UncommittedChanges(msg)
elif not os.path.isdir(target):
if force:
shell.rm(target)
else:
msg = "Preexisting file location at {}".format(target)
raise exceptions.UncommittedChanges(msg)

shell.ln(source, target)
shell.ln(source, target, symbolic)
49 changes: 44 additions & 5 deletions gitman/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,50 @@ def pwd(_show=True):
return cwd


def ln(source, target):
dirpath = os.path.dirname(target)
if not os.path.isdir(dirpath):
mkdir(dirpath)
os.symlink(source, target)
def ln(source, target, symbolic):
dxlr8r marked this conversation as resolved.
Show resolved Hide resolved
if symbolic:
dirpath = os.path.dirname(target)
if not os.path.isdir(dirpath):
mkdir(dirpath)
os.symlink(source, target)
else:
if not os.path.isdir(target):
mkdir(target)
# sync files and directories to source not present in target
for (wd_source, dirs, files) in os.walk(source):
wd_target = os.path.normpath(
os.path.join(target, os.path.relpath(wd_source, source))
)
for dir in dirs:
mkdir(os.path.join(wd_target, dir))
for file in files:
file_source = os.path.join(wd_source, file)
file_target = os.path.join(wd_target, file)
if not os.path.exists(file_target):
os.link(file_source, file_target)

# delete files and record directories from target not present in source
target_dirs = []
for (cwd, dirs, files) in os.walk(target):
wd_source = os.path.normpath(
os.path.join(source, os.path.relpath(cwd, target))
)

for file in files:
target_file = os.path.join(cwd, file)
source_file = os.path.join(wd_source, file)
if not os.path.isfile(source_file):
rm(target_file)

for dir in dirs:
target_dir = os.path.join(cwd, dir)
source_dir = os.path.join(wd_source, dir)
if not os.path.isdir(source_dir):
target_dirs.append(target_dir)

# delete directories from target not present in source
for dir in target_dirs:
rm(dir)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to understand why all this code is required. Is just calling os.link(source, target) insufficient?

Copy link
Author

@dxlr8r dxlr8r Jun 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not needed for the initial run, but will most likely be necessary on sequential runs.

Say your source repo has three files, a, b & c. On the initial run you will get links to a, b & c in your target directory.

Now let's say the author of the source repo adds file d and removes a, meaning files b, c & d are the content on the source. Without this extra code, you will however end up with a, b, c & d on the target, instead of b, c & d.



def rm(path):
Expand Down
4 changes: 2 additions & 2 deletions gitman/tests/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_pwd(self, mock_show, mock_call):
@patch("os.symlink")
def test_ln(self, mock_symlink, mock_call):
"""Verify the commands to create symbolic links."""
shell.ln("mock/target", "mock/source")
shell.ln("mock/target", "mock/source", True)
mock_symlink.assert_called_once_with("mock/target", "mock/source")
check_calls(mock_call, [])

Expand All @@ -75,7 +75,7 @@ def test_ln(self, mock_symlink, mock_call):
@patch("os.symlink")
def test_ln_missing_parent(self, mock_symlink, mock_call):
"""Verify the commands to create symbolic links (missing parent)."""
shell.ln("mock/target", "mock/source")
shell.ln("mock/target", "mock/source", True)
mock_symlink.assert_called_once_with("mock/target", "mock/source")
if os.name == "nt":
check_calls(mock_call, ["mkdir mock"])
Expand Down