Repository: openvinotoolkit/datumaro

Issue:
datum create might delete your git workspace
I inadvertently just deleted my (dirty :scream:) git workspace - with datumaro v1.4.1:

1. Installed datumaro (omitting (erroneously?) `[default]`):
```sh
jsa@localhost FiftyOneImageAnnotations.jl % conda activate .CondaPkg/env 
(/Users/jsa/work/julia/FiftyOneImageAnnotations.jl/.CondaPkg/env) jsa@localhost FiftyOneImageAnnotations.jl % pip install datumaro
Collecting datumaro
...
Successfully installed ... datumaro-1.4.1 ...
(/Users/jsa/work/julia/FiftyOneImageAnnotations.jl/.CondaPkg/env) jsa@localhost FiftyOneImageAnnotations.jl % which datum
/Users/jsa/work/julia/FiftyOneImageAnnotations.jl/.CondaPkg/env/bin/datum
```

Git workspace intact (messy, but it's there):
```sh
(/Users/jsa/work/julia/FiftyOneImageAnnotations.jl/.CondaPkg/env) jsa@localhost FiftyOneImageAnnotations.jl % git status
On branch feature/importer
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/image_annotations_importer.jl

no changes added to commit (use "git add" and/or "git commit -a")
```

2. Create datumaro project (fails due to buggy install):
```sh
(/Users/jsa/work/julia/FiftyOneImageAnnotations.jl/.CondaPkg/env) jsa@localhost FiftyOneImageAnnotations.jl % datum project create
2023-08-29 14:35:10,934 INFO: Creating project at '/Users/jsa/work/julia/FiftyOneImageAnnotations.jl'
2023-08-29 14:35:11,113 ERROR: Can't import the 'git' package. Make sure GitPython is installed, or install it with 'pip install datumaro[default]'.
Traceback (most recent call last):
  File "/Users/jsa/work/julia/FiftyOneImageAnnotations.jl/.CondaPkg/env/lib/python3.11/site-packages/datumaro/components/project.py", line 998, in module
    import git
ModuleNotFoundError: No module named 'git'
...
ModuleNotFoundError: Can't import the 'git' package. Make sure GitPython is installed, or install it with 'pip install datumaro[default]'.
```

3. Git workspace gone:
```sh
(/Users/jsa/work/julia/FiftyOneImageAnnotations.jl/.CondaPkg/env) jsa@localhost FiftyOneImageAnnotations.jl % git status
fatal: not a git repository (or any of the parent directories): .git
```



Discussion:
Hi @stemann,

I'm really sorry for your inconvenience. We'll patch that command so it doesn't force delete anything anymore.

Resolving patch:
diff --git a/src/datumaro/components/errors.py b/src/datumaro/components/errors.py
--- a/src/datumaro/components/errors.py
+++ b/src/datumaro/components/errors.py
@@ -142,6 +142,14 @@ def __str__(self):
         return f"Can't create project: a project already exists " f"at '{self.path}'"
 
 
+@define(auto_exc=False)
+class VcsAlreadyExists(DatumaroError):
+    path = field()
+
+    def __str__(self):
+        return f"Can't create project: a version control system already exists " f"at '{self.path}'"
+
+
 @define(auto_exc=False)
 class UnknownSourceError(DatumaroError):
     name = field()
diff --git a/src/datumaro/components/project.py b/src/datumaro/components/project.py
--- a/src/datumaro/components/project.py
+++ b/src/datumaro/components/project.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2019-2022 Intel Corporation
+# Copyright (C) 2019-2023 Intel Corporation
 #
 # SPDX-License-Identifier: MIT
 
@@ -70,6 +70,7 @@
     UnknownStageError,
     UnknownTargetError,
     UnsavedChangesError,
+    VcsAlreadyExists,
     VcsError,
 )
 from datumaro.components.launcher import Launcher
@@ -1787,8 +1788,16 @@ def init(cls, path) -> Project:
         os.makedirs(osp.join(path, ProjectLayout.cache_dir))
         os.makedirs(osp.join(path, ProjectLayout.tmp_dir))
 
-        on_error_do(rmtree, osp.join(project_dir, ".git"), ignore_errors=True)
-        on_error_do(rmtree, osp.join(project_dir, ".dvc"), ignore_errors=True)
+        git_dir, dvc_dir = osp.join(project_dir, ".git"), osp.join(project_dir, ".dvc")
+
+        if osp.exists(git_dir):
+            raise VcsAlreadyExists(git_dir)
+        if osp.exists(dvc_dir):
+            raise VcsAlreadyExists(dvc_dir)
+
+        on_error_do(rmtree, git_dir, ignore_errors=True)
+        on_error_do(rmtree, dvc_dir, ignore_errors=True)
+
         project = Project(path)
         project._init_vcs()
 


Test patch:
diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py
--- a/tests/unit/test_project.py
+++ b/tests/unit/test_project.py
@@ -1,3 +1,7 @@
+# Copyright (C) 2019-2023 Intel Corporation
+#
+# SPDX-License-Identifier: MIT
+
 import os
 import os.path as osp
 import shutil
@@ -6,6 +10,7 @@
 from unittest import TestCase
 
 import numpy as np
+import pytest
 
 from datumaro.components.annotation import Annotation, Bbox, Label
 from datumaro.components.config_model import Model, Source
@@ -26,11 +31,13 @@
     SourceUrlInsideProjectError,
     UnexpectedUrlError,
     UnknownTargetError,
+    VcsAlreadyExists,
 )
 from datumaro.components.launcher import Launcher
 from datumaro.components.media import Image
 from datumaro.components.project import DiffStatus, Project
 from datumaro.components.transformer import ItemTransform
+from datumaro.util.os_util import find_files
 from datumaro.util.scope import scope_add, scoped
 
 from ..requirements import Requirements, mark_requirement
@@ -39,6 +46,24 @@
 from tests.utils.test_utils import TestDir, compare_datasets, compare_dirs
 
 
+class ProjectNewTest:
+    @pytest.fixture(params=[".git", ".dvc"])
+    def fxt_vcs_exist_dir(self, test_dir, request):
+        vcs_dir = osp.join(test_dir, request.param)
+        os.makedirs(vcs_dir)
+        with open(osp.join(vcs_dir, "dummy.file"), "w") as fp:
+            fp.write("dummy")
+        yield test_dir
+
+    @mark_requirement(Requirements.DATUM_GENERAL_REQ)
+    def test_project_init_failed_by_vcs_already_exist(self, fxt_vcs_exist_dir):
+        with pytest.raises(VcsAlreadyExists):
+            Project.init(fxt_vcs_exist_dir)
+
+        # Assert Project.init() do not the existing vcs directory
+        assert len(list(find_files(fxt_vcs_exist_dir, ".file", recursive=True))) > 0
+
+
 class ProjectTest(TestCase):
     @mark_requirement(Requirements.DATUM_GENERAL_REQ)
     @scoped