diff --git a/dvc/output.py b/dvc/output.py index fb6af26f0..dcdf8c83a 100644 --- a/dvc/output.py +++ b/dvc/output.py @@ -171,16 +171,48 @@ class Output(Dependency): def loads_from(cls, project, s_list, use_cache=False, cwd=os.curdir): return [cls.loads(project, x, use_cache=use_cache, cwd=cwd) for x in s_list] + def _changed_file(self, path, cache): + if os.path.isfile(path) and \ + os.path.isfile(cache) and \ + System.samefile(path, cache) and \ + os.stat(cache).st_mode & stat.S_IREAD: + return False + + return True + + def _changed_dir(self): + if not os.path.isdir(self.path) or not os.path.isdir(self.cache): + return True + + for root, dirs, files in os.walk(self.path): + for fname in files: + path = os.path.join(root, fname) + mtime = os.path.getmtime(path) + inode = os.stat(path).st_ino + + state = self.project.state.get(path) + if state and state.mtime == mtime and state.inode == inode: + md5 = state.md5 + else: + md5 = file_md5(path)[0] + + cache = self.project.cache.get(md5) + if self._changed_file(path, cache): + return True + + return False + def changed(self): ret = True if not self.use_cache: ret = super(Output, self).changed() - elif os.path.exists(self.path) and \ - os.path.exists(self.cache) and \ - System.samefile(self.path, self.cache) and \ - os.stat(self.cache).st_mode & stat.S_IREAD: - ret = False + elif os.path.isfile(self.path) and \ + os.path.isfile(self.cache): + ret = self._changed_file(self.path, self.cache) + elif os.path.isdir(self.path) and \ + os.path.isdir(self.cache): + ret = self._changed_dir() msg = "Data {} with cache {} " if ret: diff --git a/tests/test_repro.py b/tests/test_repro.py index b949e7ed4..3171141dd 100644 --- a/tests/test_repro.py +++ b/tests/test_repro.py @@ -104,6 +104,33 @@ class TestReproDataSource(TestReproChangedData): self.assertEqual(stages[0].outs[0].md5, file_md5(self.BAR)[0]) +class TestReproChangedDir(TestDvc): + def test(self): + file_name = 'file' + shutil.copyfile(self.FOO, file_name) + + stage_name = 'dir.dvc' + dir_name = 'dir' + dir_code = 'dir.py' + + with open(dir_code, 'w+') as fd: + fd.write("import os; import shutil; os.mkdir(\"{}\"); shutil.copyfile(\"{}\", os.path.join(\"{}\", \"{}\"))".format(dir_name, file_name, dir_name, file_name)) + + self.dvc.run(fname=stage_name, + outs=[dir_name], + deps=[file_name, dir_code], + cmd="python {}".format(dir_code)) + + stages = self.dvc.reproduce(stage_name) + self.assertEqual(len(stages), 0) + + os.unlink(file_name) + shutil.copyfile(self.BAR, file_name) + + stages = self.dvc.reproduce(stage_name) + self.assertEqual(len(stages), 1) + + class TestCmdRepro(TestRepro): def test(self): ret = main(['repro',