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

add: cleanup .gitignore decorator #1673

Merged
merged 2 commits into from
Mar 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions dvc/repo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ def __init__(self, root_dir=None):

self.metrics = Metrics(self)

self.files_to_git_add = []

self._ignore()

self.updater.check()
Expand Down Expand Up @@ -92,17 +90,6 @@ def find_dvc_dir(root=None):
root_dir = Repo.find_root(root)
return os.path.join(root_dir, Repo.DVC_DIR)

def remind_to_git_add(self):
if not self.files_to_git_add:
return

logger.info(
"\n"
"To track the changes with git run:\n"
"\n"
"\tgit add {files}".format(files=" ".join(self.files_to_git_add))
)

@staticmethod
def init(root_dir=os.curdir, no_scm=False, force=False):
from dvc.repo.init import init
Expand Down
6 changes: 3 additions & 3 deletions dvc/repo/add.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import os

from dvc.scm import scm_context
from dvc.stage import Stage
from dvc.utils import walk_files
from dvc.exceptions import RecursiveAddingWhileUsingFilename


@scm_context
pared marked this conversation as resolved.
Show resolved Hide resolved
def add(repo, target, recursive=False, no_commit=False, fname=None):
if recursive and fname:
raise RecursiveAddingWhileUsingFilename()
Expand All @@ -25,9 +28,6 @@ def add(repo, target, recursive=False, no_commit=False, fname=None):

for stage in stages:
stage.dump()

repo.remind_to_git_add()

return stages


Expand Down
8 changes: 4 additions & 4 deletions dvc/repo/imp.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from dvc.scm import scm_context


@scm_context
def imp(self, url, out, resume=False):
from dvc.stage import Stage

Expand All @@ -9,13 +13,9 @@ def imp(self, url, out, resume=False):

self.check_dag(self.stages() + [stage])

self.files_to_git_add = []
pared marked this conversation as resolved.
Show resolved Hide resolved

with self.state:
stage.run(resume=resume)

stage.dump()

self.remind_to_git_add()

return stage
4 changes: 2 additions & 2 deletions dvc/repo/move.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

from dvc.exceptions import MoveNotDataSourceError
from dvc.scm import scm_context


def _expand_target_path(from_path, to_path):
Expand All @@ -9,6 +10,7 @@ def _expand_target_path(from_path, to_path):
return to_path


@scm_context
def move(self, from_path, to_path):
"""
Renames an output file and modifies the stage associated
Expand Down Expand Up @@ -64,5 +66,3 @@ def move(self, from_path, to_path):
out.move(to_out)

stage.dump()

self.remind_to_git_add()
pared marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 2 additions & 4 deletions dvc/repo/reproduce.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import dvc.logger as logger
from dvc.exceptions import ReproductionError
from dvc.scm import scm_context


def _reproduce_stage(stages, node, force, dry, interactive, no_commit):
Expand All @@ -27,6 +28,7 @@ def _reproduce_stage(stages, node, force, dry, interactive, no_commit):
return [stage]


@scm_context
def reproduce(
self,
target=None,
Expand Down Expand Up @@ -65,8 +67,6 @@ def reproduce(
else:
targets.append(target)

self.files_to_git_add = []
pared marked this conversation as resolved.
Show resolved Hide resolved

ret = []
with self.state:
for target in targets:
Expand All @@ -82,8 +82,6 @@ def reproduce(
)
ret.extend(stages)

self.remind_to_git_add()

return ret


Expand Down
6 changes: 3 additions & 3 deletions dvc/repo/run.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import unicode_literals

from dvc.scm import scm_context


@scm_context
def run(
self,
cmd=None,
Expand Down Expand Up @@ -53,13 +56,10 @@ def run(

self.check_dag(self.stages() + [stage])

self.files_to_git_add = []
pared marked this conversation as resolved.
Show resolved Hide resolved
with self.state:
if not no_exec:
stage.run(no_commit=no_commit)

stage.dump()

self.remind_to_git_add()

return stage
18 changes: 18 additions & 0 deletions dvc/scm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from dvc.scm.base import Base
from dvc.scm.git import Git

import dvc


def SCM(root_dir, repo=None): # pylint: disable=invalid-name
"""Returns SCM instance that corresponds to a repo at the specified
Expand All @@ -21,3 +23,19 @@ def SCM(root_dir, repo=None): # pylint: disable=invalid-name
return Git(root_dir, repo=repo)

return Base(root_dir, repo=repo)


def scm_context(method):
def run(*args, **kw):
repo = args[0]
assert isinstance(repo, dvc.repo.Repo)
try:
result = method(*args, **kw)
repo.scm.reset_ignores()
repo.scm.remind_to_track()
return result
except Exception as e:
repo.scm.cleanup_ignores()
raise e

return run
23 changes: 23 additions & 0 deletions dvc/scm/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,26 @@ def list_tags(self): # pylint: disable=no-self-use

def install(self):
"""Adds dvc commands to SCM hooks for the repo."""

def cleanup_ignores(self):
"""
This method should clean up ignores (eg. entries in .gitignore),
use, when method editing ignores (eg. add, run, import) fails to
perform its task.
"""

def reset_ignores(self):
"""
Method to reset in-memory ignore storing mechanism.
"""

def remind_to_track(self):
"""
Method to remind user to track newly created files handled by scm
"""

def track_file(self, path):
"""
Method to add file to mechanism that will remind user
to track new files
"""
33 changes: 29 additions & 4 deletions dvc/scm/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ def __init__(self, root_dir=os.curdir, repo=None):
libpath = env.get("LD_LIBRARY_PATH", None)
self.git.git.update_environment(LD_LIBRARY_PATH=libpath)

self.ignored_paths = []
self.files_to_track = []

@staticmethod
def is_repo(root_dir):
return os.path.isdir(Git._get_git_dir(root_dir))
Expand Down Expand Up @@ -95,8 +98,9 @@ def ignore(self, path):
with open(gitignore, "a") as fobj:
fobj.write(content)

if self.repo is not None:
self.repo.files_to_git_add.append(os.path.relpath(gitignore))
self.track_file(os.path.relpath(gitignore))

self.ignored_paths.append(path)

def ignore_remove(self, path):
entry, gitignore = self._get_gitignore(path)
Expand All @@ -112,8 +116,7 @@ def ignore_remove(self, path):
with open(gitignore, "w") as fobj:
fobj.writelines(filtered)

if self.repo is not None:
self.repo.files_to_git_add.append(os.path.relpath(gitignore))
self.track_file(os.path.relpath(gitignore))

def add(self, paths):
# NOTE: GitPython is not currently able to handle index version >= 3.
Expand Down Expand Up @@ -173,3 +176,25 @@ def _install_hook(self, name, cmd):
def install(self):
self._install_hook("post-checkout", "checkout")
self._install_hook("pre-commit", "status")

def cleanup_ignores(self):
for path in self.ignored_paths:
self.ignore_remove(path)
self.reset_ignores()

def reset_ignores(self):
self.ignored_paths = []

def remind_to_track(self):
if not self.files_to_track:
return

logger.info(
"\n"
"To track the changes with git run:\n"
"\n"
"\tgit add {files}".format(files=" ".join(self.files_to_track))
)

def track_file(self, path):
self.files_to_track.append(path)
2 changes: 1 addition & 1 deletion dvc/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ def dump(self):
with open(fname, "w") as fd:
yaml.safe_dump(d, fd, default_flow_style=False)

self.repo.files_to_git_add.append(os.path.relpath(fname))
self.repo.scm.track_file(os.path.relpath(fname))

def _compute_md5(self):
from dvc.output.local import OutputLOCAL
Expand Down
22 changes: 21 additions & 1 deletion tests/test_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from dvc.repo import Repo as DvcRepo

from tests.basic_env import TestDvc
from tests.utils import spy
from tests.utils import spy, get_gitignore_content
from tests.utils.logger import MockLoggerHandlers, ConsoleFontColorsRemover


Expand Down Expand Up @@ -457,3 +457,23 @@ def test(self):
self.assertEqual(0, ret)
self.assertTrue(os.path.exists("bar.dvc"))
self.assertFalse(os.path.exists("foo.dvc"))


class TestShouldCleanUpAfterFailedAdd(TestDvc):
def test(self):
ret = main(["add", self.FOO])
self.assertEqual(0, ret)

foo_stage_file = self.FOO + Stage.STAGE_FILE_SUFFIX
# corrupt stage file
with open(foo_stage_file, "a+") as file:
file.write("this will break yaml file structure")

ret = main(["add", self.BAR])
self.assertEqual(1, ret)

bar_stage_file = self.BAR + Stage.STAGE_FILE_SUFFIX
self.assertFalse(os.path.exists(bar_stage_file))

gitignore_content = get_gitignore_content()
self.assertFalse(any(self.BAR in line for line in gitignore_content))
9 changes: 4 additions & 5 deletions tests/test_scm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dvc.scm import SCM, Base, Git

from tests.basic_env import TestDir, TestGit, TestGitSubmodule
from tests.utils import get_gitignore_content


class TestSCM(TestDir):
Expand Down Expand Up @@ -43,11 +44,9 @@ def test_commit_in_submodule(self):

class TestIgnore(TestGit):
def _count_gitignore(self):
with open(Git.GITIGNORE, "r") as fd:
lines = fd.readlines()
return len(
list(filter(lambda x: x.strip() == "/" + self.FOO, lines))
)
lines = get_gitignore_content()

return len(list(filter(lambda x: x.strip() == "/" + self.FOO, lines)))

def test(self):
git = Git(self._root_dir)
Expand Down
Empty file added tests/unit/scm/__init__.py
Empty file.
36 changes: 36 additions & 0 deletions tests/unit/scm/scm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from dvc.repo import Repo
from dvc.scm import scm_context, Base
from unittest import TestCase
import mock


class TestScmContext(TestCase):
def setUp(self):
self.repo_mock = mock.Mock(spec=Repo)
self.scm_mock = mock.Mock(spec=Base)
self.repo_mock.scm = self.scm_mock

def test_should_successfully_perform_method(self):
method = mock.Mock()
wrapped = scm_context(method)

wrapped(self.repo_mock)

self.assertEqual(1, method.call_count)
self.assertEqual(1, self.scm_mock.reset_ignores.call_count)
self.assertEqual(1, self.scm_mock.remind_to_track.call_count)

self.assertEqual(0, self.scm_mock.cleanup_ignores.call_count)

def test_should_throw_and_cleanup(self):
method = mock.Mock(side_effect=Exception("some problem"))
wrapped = scm_context(method)

with self.assertRaises(Exception):
wrapped(self.repo_mock)

self.assertEqual(1, method.call_count)
self.assertEqual(1, self.scm_mock.cleanup_ignores.call_count)

self.assertEqual(0, self.scm_mock.reset_ignores.call_count)
self.assertEqual(0, self.scm_mock.remind_to_track.call_count)
6 changes: 6 additions & 0 deletions tests/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import yaml
import os

from dvc.scm import Git
Expand All @@ -21,6 +22,11 @@ def get_gitignore_content():
return gitignore.readlines()


def load_stage_file(path):
with open(path, "r") as fobj:
return yaml.safe_load(fobj)


@contextmanager
def cd(newdir):
prevdir = os.getcwd()
Expand Down