From 408bdff2df01dc6cf059bef61bbd3ced6e0c90aa Mon Sep 17 00:00:00 2001 From: Simen Strange Date: Sat, 10 Jun 2023 10:45:22 +0200 Subject: [PATCH 01/10] Support creating hard links --- gitman/models/source.py | 41 ++++++++++++++++++++++--------- gitman/shell.py | 49 ++++++++++++++++++++++++++++++++++---- gitman/tests/test_shell.py | 4 ++-- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/gitman/models/source.py b/gitman/models/source.py index 886fbf7..9cdcfb6 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_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...") @@ -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): + + 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) diff --git a/gitman/shell.py b/gitman/shell.py index d1ddfb0..b1d5e59 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -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): + 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) def rm(path): diff --git a/gitman/tests/test_shell.py b/gitman/tests/test_shell.py index 365a7ae..baad891 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", True) mock_symlink.assert_called_once_with("mock/target", "mock/source") check_calls(mock_call, []) @@ -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"]) From 93c96435a7545d499a3f134f7cc6770cf1e2d378 Mon Sep 17 00:00:00 2001 From: Simen Strange Date: Thu, 22 Jun 2023 07:24:43 +0000 Subject: [PATCH 02/10] I think we should remove "sym" from the name if this will create two kinds of links Co-authored-by: Jace Browning --- gitman/models/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitman/models/source.py b/gitman/models/source.py index 9cdcfb6..ddabd9a 100644 --- a/gitman/models/source.py +++ b/gitman/models/source.py @@ -344,7 +344,7 @@ def _invalid_repository(self): return exceptions.InvalidRepository(msg) -def create_sym_link(source: str, target: str, symbolic, *, force: bool): +def create_link(source: str, target: str, *, force: bool, symbolic:bool = True): if symbolic: log.info("Creating a symbolic link...") From 38be3ef8b09c1b98f9365e2fb803c285ec4df2b3 Mon Sep 17 00:00:00 2001 From: Simen Strange Date: Thu, 22 Jun 2023 07:25:43 +0000 Subject: [PATCH 03/10] symbolic should be a required flag Co-authored-by: Jace Browning --- gitman/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitman/shell.py b/gitman/shell.py index b1d5e59..7796a94 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -118,7 +118,7 @@ def pwd(_show=True): return cwd -def ln(source, target, symbolic): +def ln(source, target, *, symbolic:bool): if symbolic: dirpath = os.path.dirname(target) if not os.path.isdir(dirpath): From baa9a416251187dfe9e7a0073563f45d56552b95 Mon Sep 17 00:00:00 2001 From: Simen Strange Date: Thu, 22 Jun 2023 11:48:12 +0200 Subject: [PATCH 04/10] added initial mock for hard links, and optimised deletion of directories not present in source from target --- gitman/models/source.py | 6 +++--- gitman/shell.py | 18 +++++++----------- gitman/tests/test_shell.py | 11 +++++++++-- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/gitman/models/source.py b/gitman/models/source.py index ddabd9a..3b45ad7 100644 --- a/gitman/models/source.py +++ b/gitman/models/source.py @@ -211,7 +211,7 @@ def create_links(self, root: str, *, force: bool = False): 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) + 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...") @@ -344,7 +344,7 @@ def _invalid_repository(self): return exceptions.InvalidRepository(msg) -def create_link(source: str, target: str, *, force: bool, symbolic:bool = True): +def create_link(source: str, target: str, *, force: bool, symbolic: bool = True): if symbolic: log.info("Creating a symbolic link...") @@ -367,4 +367,4 @@ def create_link(source: str, target: str, *, force: bool, symbolic:bool = True): msg = "Preexisting file location at {}".format(target) raise exceptions.UncommittedChanges(msg) - shell.ln(source, target, symbolic) + shell.ln(source, target, symbolic=symbolic) diff --git a/gitman/shell.py b/gitman/shell.py index 7796a94..93c6e8a 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -118,7 +118,7 @@ def pwd(_show=True): return cwd -def ln(source, target, *, symbolic:bool): +def ln(source, target, *, symbolic: bool): if symbolic: dirpath = os.path.dirname(target) if not os.path.isdir(dirpath): @@ -147,21 +147,17 @@ def ln(source, target, *, symbolic:bool): 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) + rm(target_dir) - # delete directories from target not present in source - for dir in target_dirs: - rm(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 baad891..bc39994 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", True) + 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,20 @@ 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", True) + 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_hardlink, mock_call): + """Verify the commands to create hard links.""" + shell.ln("mock/target", "mock/source", symbolic=False) + mock_hardlink.assert_called_once_with("mock/target", "mock/source") + 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.""" From 51db31bb64a1068ad4c069d7e211b6cbeeb81cec Mon Sep 17 00:00:00 2001 From: Simen Strange Date: Thu, 22 Jun 2023 11:55:26 +0200 Subject: [PATCH 05/10] removed some uneeded code that the linter did not want --- gitman/shell.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gitman/shell.py b/gitman/shell.py index 93c6e8a..6211927 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -141,7 +141,6 @@ def ln(source, target, *, symbolic: bool): 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)) From 80148afd65ee8e00a385a3f38efee7469df89a74 Mon Sep 17 00:00:00 2001 From: Simen Strange Date: Fri, 30 Jun 2023 12:19:40 +0200 Subject: [PATCH 06/10] fixed test_hln --- gitman/tests/test_shell.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gitman/tests/test_shell.py b/gitman/tests/test_shell.py index bc39994..ee0b452 100644 --- a/gitman/tests/test_shell.py +++ b/gitman/tests/test_shell.py @@ -83,10 +83,9 @@ def test_ln_missing_parent(self, mock_symlink, mock_call): check_calls(mock_call, ["mkdir -p mock"]) @patch("os.path.isdir", Mock(return_value=True)) - def test_hln(self, mock_hardlink, mock_call): + def test_hln(self, mock_call): """Verify the commands to create hard links.""" - shell.ln("mock/target", "mock/source", symbolic=False) - mock_hardlink.assert_called_once_with("mock/target", "mock/source") + shell.ln("mock/target", "mock/source", symbolic=True) check_calls(mock_call, []) @patch("os.path.isfile", Mock(return_value=True)) From f9c7fdd7ece51626d8c41fa8df11e6476f63fd68 Mon Sep 17 00:00:00 2001 From: Simen Strange Date: Fri, 30 Jun 2023 12:28:03 +0200 Subject: [PATCH 07/10] small bug in test_hln --- gitman/tests/test_shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitman/tests/test_shell.py b/gitman/tests/test_shell.py index ee0b452..31b2b5e 100644 --- a/gitman/tests/test_shell.py +++ b/gitman/tests/test_shell.py @@ -85,7 +85,7 @@ def test_ln_missing_parent(self, mock_symlink, mock_call): @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=True) + shell.ln("mock/target", "mock/source", symbolic=False) check_calls(mock_call, []) @patch("os.path.isfile", Mock(return_value=True)) From 9e58f8db5e1eaa299a53c36d8158aa94956b385f Mon Sep 17 00:00:00 2001 From: Simen Strange Date: Fri, 30 Jun 2023 12:38:20 +0200 Subject: [PATCH 08/10] small indent reformat --- gitman/shell.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gitman/shell.py b/gitman/shell.py index 6211927..d5da805 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -143,15 +143,12 @@ def ln(source, target, *, symbolic: bool): # 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)) - ) - + 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) From e7b6a3e7c75ef2126c1d27c30702771d295b9c56 Mon Sep 17 00:00:00 2001 From: Simen Strange Date: Fri, 30 Jun 2023 16:06:50 +0200 Subject: [PATCH 09/10] small indent bug --- gitman/shell.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gitman/shell.py b/gitman/shell.py index d5da805..4f0fb0a 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -130,8 +130,7 @@ def ln(source, target, *, symbolic: bool): # 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)) - ) + 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: From 1d629942971170c6755fe5aecfb46d578401aa99 Mon Sep 17 00:00:00 2001 From: Simen Strange Date: Fri, 30 Jun 2023 19:32:34 +0200 Subject: [PATCH 10/10] small indent reformat --- gitman/shell.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gitman/shell.py b/gitman/shell.py index 4f0fb0a..93f6242 100644 --- a/gitman/shell.py +++ b/gitman/shell.py @@ -129,8 +129,7 @@ def ln(source, target, *, symbolic: bool): 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))) + 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: @@ -141,8 +140,7 @@ def ln(source, target, *, symbolic: bool): # 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))) + 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)