diff --git a/gitman/models/source.py b/gitman/models/source.py index 886fbf7..3b45ad7 100644 --- a/gitman/models/source.py +++ b/gitman/models/source.py @@ -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 @@ -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_link(source, target, symbolic=bool(link.symbolic), force=force) def run_scripts(self, force: bool = False, show_shell_stdout: bool = False): log.info("Running install scripts...") @@ -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_link(source: str, target: str, *, force: bool, symbolic: bool = True): + + 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=symbolic) diff --git a/gitman/shell.py b/gitman/shell.py index d1ddfb0..93f6242 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -118,11 +118,39 @@ 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: bool): + 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 + for (cwd, dirs, files) in os.walk(target): + wd_source = os.path.normpath(os.path.join(source, os.path.relpath(cwd, target))) + 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): + rm(target_dir) + 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) def rm(path): diff --git a/gitman/tests/test_shell.py b/gitman/tests/test_shell.py index 365a7ae..31b2b5e 100644 --- a/gitman/tests/test_shell.py +++ b/gitman/tests/test_shell.py @@ -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", symbolic=True) mock_symlink.assert_called_once_with("mock/target", "mock/source") check_calls(mock_call, []) @@ -75,13 +75,19 @@ 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", symbolic=True) mock_symlink.assert_called_once_with("mock/target", "mock/source") if os.name == "nt": check_calls(mock_call, ["mkdir mock"]) else: check_calls(mock_call, ["mkdir -p mock"]) + @patch("os.path.isdir", Mock(return_value=True)) + def test_hln(self, mock_call): + """Verify the commands to create hard links.""" + shell.ln("mock/target", "mock/source", symbolic=False) + check_calls(mock_call, []) + @patch("os.path.isfile", Mock(return_value=True)) def test_rm_file(self, mock_call): """Verify the commands to delete files."""