diff --git a/CHANGES.md b/CHANGES.md index d7a4c0a51..2e3cfa987 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,9 @@ 2.3b5 (unreleased) ------------------ -- Nothing changed yet. +- Do not clobber git clones by default. Add new option `clobber` to allow + specifying that a git clone may clobber the target directory. + ([#298](https://github.com/flyingcircusio/batou/issues/298)) 2.3b4 (2022-08-22) diff --git a/src/batou/lib/git.py b/src/batou/lib/git.py index 88ca8d315..281c8d855 100644 --- a/src/batou/lib/git.py +++ b/src/batou/lib/git.py @@ -28,6 +28,7 @@ class Clone(Component): revision = None branch = None vcs_update = True + clobber = False def configure(self): if (not self.revision_or_branch) or (self.revision and self.branch): @@ -64,13 +65,25 @@ def verify(self): ) if self.has_changes(): - output.annotate( - "Git clone at {} is dirty, going to lose changes.".format( - self.target - ), - red=True, - ) - raise UpdateNeeded() + if self.clobber: + output.annotate( + "Git clone at {} is dirty, going to lose changes.".format( + self.target + ), + red=True, + ) + raise UpdateNeeded() + else: + output.annotate( + "Git clone at {} is dirty - refusing to clobber. " + "Use `clobber=True` if this is intentional .".format( + self.target + ), + red=True, + ) + raise RuntimeError( + "Refusing to clobber dirty work directory." + ) if self.revision and self.current_revision() != self.revision: raise UpdateNeeded() diff --git a/src/batou/lib/tests/test_git.py b/src/batou/lib/tests/test_git.py index 77b873a6b..88b117728 100644 --- a/src/batou/lib/tests/test_git.py +++ b/src/batou/lib/tests/test_git.py @@ -159,7 +159,7 @@ def test_clean_clone_updates_on_incoming_changes(root, repos_path): @pytest.mark.slow -def test_changes_lost_on_update_with_incoming(root, repos_path): +def test_no_clobber_changes_protected_on_update_with_incoming(root, repos_path): root.component += batou.lib.git.Clone( repos_path, target="clone", branch="master" ) @@ -170,6 +170,59 @@ def test_changes_lost_on_update_with_incoming(root, repos_path): ) ) cmd("cd {dir}/clone; echo foobar >foo".format(dir=root.workdir)) + with pytest.raises(RuntimeError) as e: + root.component.deploy() + assert e.value.args[0] == "Refusing to clobber dirty work directory." + with open(root.component.map("clone/foo")) as f: + assert f.read() == "foobar\n" + + +@pytest.mark.slow +def test_no_clobber_changes_protected_on_update_without_incoming( + root, repos_path +): + root.component += batou.lib.git.Clone( + repos_path, target="clone", branch="master" + ) + root.component.deploy() + cmd("cd {dir}/clone; echo foobar >foo".format(dir=root.workdir)) + with pytest.raises(RuntimeError) as e: + root.component.deploy() + assert e.value.args[0] == "Refusing to clobber dirty work directory." + with open(root.component.map("clone/foo")) as f: + assert f.read() == "foobar\n" + + +@pytest.mark.slow +def test_no_clobber_untracked_files_are_kept_on_update(root, repos_path): + root.component += batou.lib.git.Clone( + repos_path, target="clone", branch="master" + ) + root.component.deploy() + cmd( + "cd {dir}/clone; mkdir bar; echo foobar >bar/baz".format( + dir=root.workdir + ) + ) + with pytest.raises(RuntimeError) as e: + root.component.deploy() + assert e.value.args[0] == "Refusing to clobber dirty work directory." + with open(root.component.map("clone/bar/baz")) as f: + assert f.read() == "foobar\n" + + +@pytest.mark.slow +def test_clobber_changes_lost_on_update_with_incoming(root, repos_path): + root.component += batou.lib.git.Clone( + repos_path, target="clone", branch="master", clobber=True + ) + root.component.deploy() + cmd( + 'cd {dir}; touch bar; git add .; git commit -m "commit"'.format( + dir=repos_path + ) + ) + cmd("cd {dir}/clone; echo foobar >foo".format(dir=root.workdir)) root.component.deploy() assert os.path.exists(root.component.map("clone/bar")) with open(root.component.map("clone/foo")) as f: @@ -177,9 +230,9 @@ def test_changes_lost_on_update_with_incoming(root, repos_path): @pytest.mark.slow -def test_changes_lost_on_update_without_incoming(root, repos_path): +def test_clobber_changes_lost_on_update_without_incoming(root, repos_path): root.component += batou.lib.git.Clone( - repos_path, target="clone", branch="master" + repos_path, target="clone", branch="master", clobber=True ) root.component.deploy() cmd("cd {dir}/clone; echo foobar >foo".format(dir=root.workdir)) @@ -189,9 +242,9 @@ def test_changes_lost_on_update_without_incoming(root, repos_path): @pytest.mark.slow -def test_untracked_files_are_removed_on_update(root, repos_path): +def test_clobber_untracked_files_are_removed_on_update(root, repos_path): root.component += batou.lib.git.Clone( - repos_path, target="clone", branch="master" + repos_path, target="clone", branch="master", clobber=True ) root.component.deploy() cmd(