We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
dvc repro reproduces directories all the time
The directory was just created by `dvc run` and next `dvc repro` reproduces the directory one more time.

```
$ dvc repro -v dir1.dvc
Data /home/ubuntu/src/myrepo_test/data/file.txt with cache /home/ubuntu/src/myrepo_test/.dvc/cache/2db7e560e41eb16f6a78cb13d2645836 didn't change
/home/ubuntu/src/myrepo_test/data/file.txt.dvc didn't change
Data /home/ubuntu/src/myrepo_test/dir1 with cache /home/ubuntu/src/myrepo_test/.dvc/cache/3908923f1bc06b1f1279aad916f5cdbf changed
/home/ubuntu/src/myrepo_test/dir1.dvc changed
Data /home/ubuntu/src/myrepo_test/dir1 with cache /home/ubuntu/src/myrepo_test/.dvc/cache/3908923f1bc06b1f1279aad916f5cdbf changed
/home/ubuntu/src/myrepo_test/dir1.dvc changed
Removing '/home/ubuntu/src/myrepo_test/dir1/.head'
Removing '/home/ubuntu/src/myrepo_test/dir1/.tail'
Reproducing 'dir1.dvc':
	./headdir.sh data/file.txt dir1
...
```

Details:
```
# headdir.sh
f=`basename $2`
head -n 3 $1 > $2/${f}.head
tail -n 3 $1 > $2/${f}.tail
```

`dvc run -d data/file.txt -o dir1 ./headdir.sh data/file.txt dir1`

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `dvc/output.py`
Content:
```
1 import os
2 import stat
3 import yaml
4 from checksumdir import dirhash
5 
6 from dvc.system import System
7 from dvc.utils import file_md5
8 from dvc.exceptions import DvcException
9 
10 
11 class OutputError(DvcException):
12     pass
13 
14 
15 class CmdOutputError(DvcException):
16     def __init__(self, path, msg):
17         super(CmdOutputError, self).__init__('Output file \'{}\' error: {}'.format(path, msg))
18 
19 
20 class CmdOutputNoCacheError(CmdOutputError):
21     def __init__(self, path):
22         super(CmdOutputNoCacheError, self).__init__(path, 'no cache')
23 
24 
25 class CmdOutputOutsideOfRepoError(CmdOutputError):
26     def __init__(self, path):
27         super(CmdOutputOutsideOfRepoError, self).__init__(path, 'outside of repository')
28 
29 
30 class CmdOutputDoesNotExistError(CmdOutputError):
31     def __init__(self, path):
32         super(CmdOutputDoesNotExistError, self).__init__(path, 'does not exist')
33 
34 
35 class CmdOutputIsNotFileOrDirError(CmdOutputError):
36     def __init__(self, path):
37         super(CmdOutputIsNotFileOrDirError, self).__init__(path, 'not a file or directory')
38 
39 
40 class CmdOutputAlreadyTrackedError(CmdOutputError):
41     def __init__(self, path):
42         super(CmdOutputAlreadyTrackedError, self).__init__(path, 'already tracked by scm(e.g. git)')
43 
44 
45 class Dependency(object):
46     PARAM_PATH = 'path'
47     PARAM_MD5 = 'md5'
48 
49     def __init__(self, project, path, md5=None):
50         self.project = project
51         self.path = os.path.abspath(os.path.realpath(path))
52 
53         if not self.path.startswith(self.project.root_dir):
54             raise CmdOutputOutsideOfRepoError(self.rel_path)
55 
56         self.md5 = md5
57 
58     @property
59     def dvc_path(self):
60         return os.path.relpath(self.path, self.project.root_dir)
61 
62     @property
63     def rel_path(self):
64         return os.path.relpath(self.path, '.')
65 
66     def _changed_md5(self):
67         if not os.path.exists(self.path):
68             return True
69 
70         state = self.project.state.get(self.path)
71         if state and state.mtime == self.mtime() and state.inode == self.inode():
72             md5 = state.md5
73         else:
74             md5 = self.compute_md5()
75 
76         return self.md5 != md5
77 
78     def changed(self):
79         return self._changed_md5()
80 
81     def compute_md5(self):
82         if os.path.isdir(self.path):
83             return dirhash(self.path, hashfunc='md5')
84         else:
85             return file_md5(self.path)[0]
86 
87     def mtime(self):
88         return os.path.getmtime(self.path)
89 
90     def inode(self):
91         return os.stat(self.path).st_ino
92 
93     def save(self):
94         if not os.path.exists(self.path):
95             raise CmdOutputDoesNotExistError(self.rel_path)
96 
97         if not os.path.isfile(self.path) and not os.path.isdir(self.path):
98             raise CmdOutputIsNotFileOrDirError(self.rel_path)
99 
100         state = self.project.state.get(self.path)
101         if state and state.mtime == self.mtime() and state.inode == self.inode():
102             md5 = state.md5
103             msg = '{} using md5 {} from state file'
104             self.project.logger.debug(msg.format(self.path, md5))
105             self.md5 = md5
106         else:
107             self.md5 = self.compute_md5()
108             self.project.state.update(self.path, self.md5, self.mtime(), self.inode())
109 
110     def dumpd(self, cwd):
111         return {
112             Output.PARAM_PATH: os.path.relpath(self.path, cwd),
113             Output.PARAM_MD5: self.md5,
114         }
115 
116     @classmethod
117     def loadd(cls, project, d, cwd=os.curdir):
118         path = os.path.join(cwd, d[Output.PARAM_PATH])
119         md5 = d.get(Output.PARAM_MD5, None)
120         return cls(project, path, md5=md5)
121 
122     @classmethod
123     def loadd_from(cls, project, d_list, cwd=os.curdir):
124         return [cls.loadd(project, x, cwd=cwd) for x in d_list]
125 
126     @classmethod
127     def loads(cls, project, s, cwd=os.curdir):
128         return cls(project, os.path.join(cwd, s), md5=None)
129 
130     @classmethod
131     def loads_from(cls, project, s_list, cwd=os.curdir):
132         return [cls.loads(project, x, cwd=cwd) for x in s_list]
133 
134     def stage(self):
135         for stage in self.project.stages():
136             for out in stage.outs:
137                 if self.path == out.path:
138                     return stage
139         return None
140 
141 
142 class Output(Dependency):
143     PARAM_CACHE = 'cache'
144 
145     def __init__(self, project, path, md5=None, use_cache=True):
146         super(Output, self).__init__(project, path, md5=md5)
147         self.use_cache = use_cache
148 
149     @property
150     def cache(self):
151         return self.project.cache.get(self.md5)
152 
153     def dumpd(self, cwd):
154         ret = super(Output, self).dumpd(cwd)
155         ret[Output.PARAM_CACHE] = self.use_cache
156         return ret
157 
158     @classmethod
159     def loadd(cls, project, d, cwd=os.curdir):
160         ret = super(Output, cls).loadd(project, d, cwd=cwd)
161         ret.use_cache = d.get(Output.PARAM_CACHE, True)
162         return ret
163 
164     @classmethod
165     def loads(cls, project, s, use_cache=True, cwd=os.curdir):
166         ret = super(Output, cls).loads(project, s, cwd=cwd)
167         ret.use_cache = use_cache
168         return ret
169 
170     @classmethod
171     def loads_from(cls, project, s_list, use_cache=False, cwd=os.curdir):
172         return [cls.loads(project, x, use_cache=use_cache, cwd=cwd) for x in s_list]
173 
174     def changed(self):
175         ret = True
176 
177         if not self.use_cache:
178             ret = super(Output, self).changed()
179         elif os.path.exists(self.path) and \
180            os.path.exists(self.cache) and \
181            System.samefile(self.path, self.cache) and \
182            os.stat(self.cache).st_mode & stat.S_IREAD:
183             ret = False
184 
185         msg = "Data {} with cache {} "
186         if ret:
187             msg += "changed"
188         else:
189             msg += "didn't change"
190         self.project.logger.debug(msg.format(self.path, self.cache))
191 
192         return ret
193 
194     def hardlink(self, src, link):
195         self.project.logger.debug("creating hardlink {} -> {}".format(src, link))
196         System.hardlink(src, link)
197         os.chmod(src, stat.S_IREAD)
198 
199     def dir_cache(self):
200         res = {}
201         for root, dirs, files in os.walk(self.cache):
202             for fname in files:
203                 path = os.path.join(root, fname)
204                 relpath = os.path.relpath(path, self.cache)
205                 with open(path, 'r') as fd:
206                     d = yaml.safe_load(fd)
207                 md5 = d[Output.PARAM_MD5]
208                 res[relpath] = self.project.cache.get(md5)
209         return res
210 
211     def checkout(self):
212         if not self.use_cache:
213             return
214 
215         self.project.logger.debug("Checking out {} with cache {}".format(self.path, self.cache))
216 
217         if not self.changed():
218             msg = "Data {} with cache {} didn't change, skipping checkout."
219             self.project.logger.debug(msg.format(self.path, self.cache))
220             return
221 
222         if not os.path.exists(self.cache):
223             self.project.logger.warn(u'\'{}\': cache file not found'.format(self.dvc_path))
224             self.remove()
225             return
226 
227         if os.path.exists(self.path):
228             msg = "Data {} exists. Removing before checkout"
229             self.project.logger.debug(msg.format(self.path))
230             self.remove()
231 
232         if os.path.isfile(self.cache):
233             self.hardlink(self.cache, self.path)
234             return
235 
236         for relpath, cache in self.dir_cache().items():
237             path = os.path.join(self.path, relpath)
238             dname = os.path.dirname(path)
239 
240             if not os.path.exists(dname):
241                 os.makedirs(dname)
242 
243             self.hardlink(cache, path)
244 
245     def save(self):
246         super(Output, self).save()
247 
248         if not self.use_cache:
249             return
250 
251         self.project.logger.debug("Saving {} to {}".format(self.path, self.cache))
252 
253         if self.project.scm.is_tracked(self.path):
254             raise CmdOutputAlreadyTrackedError(self.rel_path)
255 
256         if not self.changed():
257              return
258 
259         if os.path.exists(self.cache):
260             # This means that we already have cache for this data.
261             # We remove data and link it to existing cache to save
262             # some space.
263             msg = "Cache {} already exists, performing checkout for {}"
264             self.project.logger.debug(msg.format(self.cache, self.path))
265             self.checkout()
266             return
267 
268         if os.path.isfile(self.path):
269             self.hardlink(self.path, self.cache)
270             return
271 
272         for root, dirs, files in os.walk(self.path):
273             for fname in files:
274                 path = os.path.join(root, fname)
275                 relpath = os.path.relpath(path, self.path)
276                 md5 = file_md5(path)[0]
277                 cache = self.project.cache.get(md5)
278                 cache_info = os.path.join(self.cache, relpath)
279                 cache_dir = os.path.dirname(cache_info)
280 
281                 if os.path.exists(cache):
282                     self._remove(path, None)
283                     self.hardlink(cache, path)
284                 else:
285                     self.hardlink(path, cache)
286 
287                 if not os.path.exists(cache_dir):
288                     os.makedirs(cache_dir)
289 
290                 with open(cache_info, 'w') as fd:
291                     yaml.safe_dump({self.PARAM_MD5: md5}, fd, default_flow_style=False)
292 
293     def _remove(self, path, cache):
294         self.project.logger.debug("Removing '{}'".format(path))
295         os.chmod(path, stat.S_IWUSR)
296         os.unlink(path)
297         if cache != None and os.path.exists(cache):
298             os.chmod(cache, stat.S_IREAD)
299 
300     def remove(self):
301         if not os.path.exists(self.path):
302             return
303 
304         if os.path.isfile(self.path):
305             self._remove(self.path, self.cache)
306             return
307 
308         caches = self.dir_cache()
309         for root, dirs, files in os.walk(self.path, topdown=False):
310             for d in dirs:
311                 path = os.path.join(root, d)
312                 os.rmdir(path)
313             for f in files:
314                 path = os.path.join(root, f)
315                 relpath = os.path.relpath(path, self.path)
316                 cache = caches.get(relpath, None)
317                 self._remove(path, cache)
318         os.rmdir(self.path)
319
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/dvc/output.py b/dvc/output.py
--- a/dvc/output.py
+++ b/dvc/output.py
@@ -171,16 +171,48 @@
     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:

{"golden_diff": "diff --git a/dvc/output.py b/dvc/output.py\n--- a/dvc/output.py\n+++ b/dvc/output.py\n@@ -171,16 +171,48 @@\n     def loads_from(cls, project, s_list, use_cache=False, cwd=os.curdir):\n         return [cls.loads(project, x, use_cache=use_cache, cwd=cwd) for x in s_list]\n \n+    def _changed_file(self, path, cache):\n+        if os.path.isfile(path) and \\\n+           os.path.isfile(cache) and \\\n+           System.samefile(path, cache) and \\\n+           os.stat(cache).st_mode & stat.S_IREAD:\n+            return False\n+\n+        return True\n+\n+    def _changed_dir(self):\n+        if not os.path.isdir(self.path) or not os.path.isdir(self.cache):\n+            return True\n+\n+        for root, dirs, files in os.walk(self.path):\n+            for fname in files:\n+                path = os.path.join(root, fname)\n+                mtime = os.path.getmtime(path)\n+                inode = os.stat(path).st_ino\n+\n+                state = self.project.state.get(path)\n+                if state and state.mtime == mtime and state.inode == inode:\n+                    md5 = state.md5\n+                else:\n+                    md5 = file_md5(path)[0]\n+\n+                cache = self.project.cache.get(md5)\n+                if self._changed_file(path, cache):\n+                    return True\n+\n+        return False\n+\n     def changed(self):\n         ret = True\n \n         if not self.use_cache:\n             ret = super(Output, self).changed()\n-        elif os.path.exists(self.path) and \\\n-           os.path.exists(self.cache) and \\\n-           System.samefile(self.path, self.cache) and \\\n-           os.stat(self.cache).st_mode & stat.S_IREAD:\n-            ret = False\n+        elif os.path.isfile(self.path) and \\\n+             os.path.isfile(self.cache):\n+            ret = self._changed_file(self.path, self.cache)\n+        elif os.path.isdir(self.path) and \\\n+             os.path.isdir(self.cache):\n+            ret = self._changed_dir()\n \n         msg = \"Data {} with cache {} \"\n         if ret:\n", "issue": "dvc repro reproduces directories all the time\nThe directory was just created by `dvc run` and next `dvc repro` reproduces the directory one more time.\r\n\r\n```\r\n$ dvc repro -v dir1.dvc\r\nData /home/ubuntu/src/myrepo_test/data/file.txt with cache /home/ubuntu/src/myrepo_test/.dvc/cache/2db7e560e41eb16f6a78cb13d2645836 didn't change\r\n/home/ubuntu/src/myrepo_test/data/file.txt.dvc didn't change\r\nData /home/ubuntu/src/myrepo_test/dir1 with cache /home/ubuntu/src/myrepo_test/.dvc/cache/3908923f1bc06b1f1279aad916f5cdbf changed\r\n/home/ubuntu/src/myrepo_test/dir1.dvc changed\r\nData /home/ubuntu/src/myrepo_test/dir1 with cache /home/ubuntu/src/myrepo_test/.dvc/cache/3908923f1bc06b1f1279aad916f5cdbf changed\r\n/home/ubuntu/src/myrepo_test/dir1.dvc changed\r\nRemoving '/home/ubuntu/src/myrepo_test/dir1/.head'\r\nRemoving '/home/ubuntu/src/myrepo_test/dir1/.tail'\r\nReproducing 'dir1.dvc':\r\n\t./headdir.sh data/file.txt dir1\r\n...\r\n```\r\n\r\nDetails:\r\n```\r\n# headdir.sh\r\nf=`basename $2`\r\nhead -n 3 $1 > $2/${f}.head\r\ntail -n 3 $1 > $2/${f}.tail\r\n```\r\n\r\n`dvc run -d data/file.txt -o dir1 ./headdir.sh data/file.txt dir1`\n", "before_files": [{"content": "import os\nimport stat\nimport yaml\nfrom checksumdir import dirhash\n\nfrom dvc.system import System\nfrom dvc.utils import file_md5\nfrom dvc.exceptions import DvcException\n\n\nclass OutputError(DvcException):\n    pass\n\n\nclass CmdOutputError(DvcException):\n    def __init__(self, path, msg):\n        super(CmdOutputError, self).__init__('Output file \\'{}\\' error: {}'.format(path, msg))\n\n\nclass CmdOutputNoCacheError(CmdOutputError):\n    def __init__(self, path):\n        super(CmdOutputNoCacheError, self).__init__(path, 'no cache')\n\n\nclass CmdOutputOutsideOfRepoError(CmdOutputError):\n    def __init__(self, path):\n        super(CmdOutputOutsideOfRepoError, self).__init__(path, 'outside of repository')\n\n\nclass CmdOutputDoesNotExistError(CmdOutputError):\n    def __init__(self, path):\n        super(CmdOutputDoesNotExistError, self).__init__(path, 'does not exist')\n\n\nclass CmdOutputIsNotFileOrDirError(CmdOutputError):\n    def __init__(self, path):\n        super(CmdOutputIsNotFileOrDirError, self).__init__(path, 'not a file or directory')\n\n\nclass CmdOutputAlreadyTrackedError(CmdOutputError):\n    def __init__(self, path):\n        super(CmdOutputAlreadyTrackedError, self).__init__(path, 'already tracked by scm(e.g. git)')\n\n\nclass Dependency(object):\n    PARAM_PATH = 'path'\n    PARAM_MD5 = 'md5'\n\n    def __init__(self, project, path, md5=None):\n        self.project = project\n        self.path = os.path.abspath(os.path.realpath(path))\n\n        if not self.path.startswith(self.project.root_dir):\n            raise CmdOutputOutsideOfRepoError(self.rel_path)\n\n        self.md5 = md5\n\n    @property\n    def dvc_path(self):\n        return os.path.relpath(self.path, self.project.root_dir)\n\n    @property\n    def rel_path(self):\n        return os.path.relpath(self.path, '.')\n\n    def _changed_md5(self):\n        if not os.path.exists(self.path):\n            return True\n\n        state = self.project.state.get(self.path)\n        if state and state.mtime == self.mtime() and state.inode == self.inode():\n            md5 = state.md5\n        else:\n            md5 = self.compute_md5()\n\n        return self.md5 != md5\n\n    def changed(self):\n        return self._changed_md5()\n\n    def compute_md5(self):\n        if os.path.isdir(self.path):\n            return dirhash(self.path, hashfunc='md5')\n        else:\n            return file_md5(self.path)[0]\n\n    def mtime(self):\n        return os.path.getmtime(self.path)\n\n    def inode(self):\n        return os.stat(self.path).st_ino\n\n    def save(self):\n        if not os.path.exists(self.path):\n            raise CmdOutputDoesNotExistError(self.rel_path)\n\n        if not os.path.isfile(self.path) and not os.path.isdir(self.path):\n            raise CmdOutputIsNotFileOrDirError(self.rel_path)\n\n        state = self.project.state.get(self.path)\n        if state and state.mtime == self.mtime() and state.inode == self.inode():\n            md5 = state.md5\n            msg = '{} using md5 {} from state file'\n            self.project.logger.debug(msg.format(self.path, md5))\n            self.md5 = md5\n        else:\n            self.md5 = self.compute_md5()\n            self.project.state.update(self.path, self.md5, self.mtime(), self.inode())\n\n    def dumpd(self, cwd):\n        return {\n            Output.PARAM_PATH: os.path.relpath(self.path, cwd),\n            Output.PARAM_MD5: self.md5,\n        }\n\n    @classmethod\n    def loadd(cls, project, d, cwd=os.curdir):\n        path = os.path.join(cwd, d[Output.PARAM_PATH])\n        md5 = d.get(Output.PARAM_MD5, None)\n        return cls(project, path, md5=md5)\n\n    @classmethod\n    def loadd_from(cls, project, d_list, cwd=os.curdir):\n        return [cls.loadd(project, x, cwd=cwd) for x in d_list]\n\n    @classmethod\n    def loads(cls, project, s, cwd=os.curdir):\n        return cls(project, os.path.join(cwd, s), md5=None)\n\n    @classmethod\n    def loads_from(cls, project, s_list, cwd=os.curdir):\n        return [cls.loads(project, x, cwd=cwd) for x in s_list]\n\n    def stage(self):\n        for stage in self.project.stages():\n            for out in stage.outs:\n                if self.path == out.path:\n                    return stage\n        return None\n\n\nclass Output(Dependency):\n    PARAM_CACHE = 'cache'\n\n    def __init__(self, project, path, md5=None, use_cache=True):\n        super(Output, self).__init__(project, path, md5=md5)\n        self.use_cache = use_cache\n\n    @property\n    def cache(self):\n        return self.project.cache.get(self.md5)\n\n    def dumpd(self, cwd):\n        ret = super(Output, self).dumpd(cwd)\n        ret[Output.PARAM_CACHE] = self.use_cache\n        return ret\n\n    @classmethod\n    def loadd(cls, project, d, cwd=os.curdir):\n        ret = super(Output, cls).loadd(project, d, cwd=cwd)\n        ret.use_cache = d.get(Output.PARAM_CACHE, True)\n        return ret\n\n    @classmethod\n    def loads(cls, project, s, use_cache=True, cwd=os.curdir):\n        ret = super(Output, cls).loads(project, s, cwd=cwd)\n        ret.use_cache = use_cache\n        return ret\n\n    @classmethod\n    def loads_from(cls, project, s_list, use_cache=False, cwd=os.curdir):\n        return [cls.loads(project, x, use_cache=use_cache, cwd=cwd) for x in s_list]\n\n    def changed(self):\n        ret = True\n\n        if not self.use_cache:\n            ret = super(Output, self).changed()\n        elif os.path.exists(self.path) and \\\n           os.path.exists(self.cache) and \\\n           System.samefile(self.path, self.cache) and \\\n           os.stat(self.cache).st_mode & stat.S_IREAD:\n            ret = False\n\n        msg = \"Data {} with cache {} \"\n        if ret:\n            msg += \"changed\"\n        else:\n            msg += \"didn't change\"\n        self.project.logger.debug(msg.format(self.path, self.cache))\n\n        return ret\n\n    def hardlink(self, src, link):\n        self.project.logger.debug(\"creating hardlink {} -> {}\".format(src, link))\n        System.hardlink(src, link)\n        os.chmod(src, stat.S_IREAD)\n\n    def dir_cache(self):\n        res = {}\n        for root, dirs, files in os.walk(self.cache):\n            for fname in files:\n                path = os.path.join(root, fname)\n                relpath = os.path.relpath(path, self.cache)\n                with open(path, 'r') as fd:\n                    d = yaml.safe_load(fd)\n                md5 = d[Output.PARAM_MD5]\n                res[relpath] = self.project.cache.get(md5)\n        return res\n\n    def checkout(self):\n        if not self.use_cache:\n            return\n\n        self.project.logger.debug(\"Checking out {} with cache {}\".format(self.path, self.cache))\n\n        if not self.changed():\n            msg = \"Data {} with cache {} didn't change, skipping checkout.\"\n            self.project.logger.debug(msg.format(self.path, self.cache))\n            return\n\n        if not os.path.exists(self.cache):\n            self.project.logger.warn(u'\\'{}\\': cache file not found'.format(self.dvc_path))\n            self.remove()\n            return\n\n        if os.path.exists(self.path):\n            msg = \"Data {} exists. Removing before checkout\"\n            self.project.logger.debug(msg.format(self.path))\n            self.remove()\n\n        if os.path.isfile(self.cache):\n            self.hardlink(self.cache, self.path)\n            return\n\n        for relpath, cache in self.dir_cache().items():\n            path = os.path.join(self.path, relpath)\n            dname = os.path.dirname(path)\n\n            if not os.path.exists(dname):\n                os.makedirs(dname)\n\n            self.hardlink(cache, path)\n\n    def save(self):\n        super(Output, self).save()\n\n        if not self.use_cache:\n            return\n\n        self.project.logger.debug(\"Saving {} to {}\".format(self.path, self.cache))\n\n        if self.project.scm.is_tracked(self.path):\n            raise CmdOutputAlreadyTrackedError(self.rel_path)\n\n        if not self.changed():\n             return\n\n        if os.path.exists(self.cache):\n            # This means that we already have cache for this data.\n            # We remove data and link it to existing cache to save\n            # some space.\n            msg = \"Cache {} already exists, performing checkout for {}\"\n            self.project.logger.debug(msg.format(self.cache, self.path))\n            self.checkout()\n            return\n\n        if os.path.isfile(self.path):\n            self.hardlink(self.path, self.cache)\n            return\n\n        for root, dirs, files in os.walk(self.path):\n            for fname in files:\n                path = os.path.join(root, fname)\n                relpath = os.path.relpath(path, self.path)\n                md5 = file_md5(path)[0]\n                cache = self.project.cache.get(md5)\n                cache_info = os.path.join(self.cache, relpath)\n                cache_dir = os.path.dirname(cache_info)\n\n                if os.path.exists(cache):\n                    self._remove(path, None)\n                    self.hardlink(cache, path)\n                else:\n                    self.hardlink(path, cache)\n\n                if not os.path.exists(cache_dir):\n                    os.makedirs(cache_dir)\n\n                with open(cache_info, 'w') as fd:\n                    yaml.safe_dump({self.PARAM_MD5: md5}, fd, default_flow_style=False)\n\n    def _remove(self, path, cache):\n        self.project.logger.debug(\"Removing '{}'\".format(path))\n        os.chmod(path, stat.S_IWUSR)\n        os.unlink(path)\n        if cache != None and os.path.exists(cache):\n            os.chmod(cache, stat.S_IREAD)\n\n    def remove(self):\n        if not os.path.exists(self.path):\n            return\n\n        if os.path.isfile(self.path):\n            self._remove(self.path, self.cache)\n            return\n\n        caches = self.dir_cache()\n        for root, dirs, files in os.walk(self.path, topdown=False):\n            for d in dirs:\n                path = os.path.join(root, d)\n                os.rmdir(path)\n            for f in files:\n                path = os.path.join(root, f)\n                relpath = os.path.relpath(path, self.path)\n                cache = caches.get(relpath, None)\n                self._remove(path, cache)\n        os.rmdir(self.path)\n", "path": "dvc/output.py"}], "after_files": [{"content": "import os\nimport stat\nimport yaml\nfrom checksumdir import dirhash\n\nfrom dvc.system import System\nfrom dvc.utils import file_md5\nfrom dvc.exceptions import DvcException\n\n\nclass OutputError(DvcException):\n    pass\n\n\nclass CmdOutputError(DvcException):\n    def __init__(self, path, msg):\n        super(CmdOutputError, self).__init__('Output file \\'{}\\' error: {}'.format(path, msg))\n\n\nclass CmdOutputNoCacheError(CmdOutputError):\n    def __init__(self, path):\n        super(CmdOutputNoCacheError, self).__init__(path, 'no cache')\n\n\nclass CmdOutputOutsideOfRepoError(CmdOutputError):\n    def __init__(self, path):\n        super(CmdOutputOutsideOfRepoError, self).__init__(path, 'outside of repository')\n\n\nclass CmdOutputDoesNotExistError(CmdOutputError):\n    def __init__(self, path):\n        super(CmdOutputDoesNotExistError, self).__init__(path, 'does not exist')\n\n\nclass CmdOutputIsNotFileOrDirError(CmdOutputError):\n    def __init__(self, path):\n        super(CmdOutputIsNotFileOrDirError, self).__init__(path, 'not a file or directory')\n\n\nclass CmdOutputAlreadyTrackedError(CmdOutputError):\n    def __init__(self, path):\n        super(CmdOutputAlreadyTrackedError, self).__init__(path, 'already tracked by scm(e.g. git)')\n\n\nclass Dependency(object):\n    PARAM_PATH = 'path'\n    PARAM_MD5 = 'md5'\n\n    def __init__(self, project, path, md5=None):\n        self.project = project\n        self.path = os.path.abspath(os.path.realpath(path))\n\n        if not self.path.startswith(self.project.root_dir):\n            raise CmdOutputOutsideOfRepoError(self.rel_path)\n\n        self.md5 = md5\n\n    @property\n    def dvc_path(self):\n        return os.path.relpath(self.path, self.project.root_dir)\n\n    @property\n    def rel_path(self):\n        return os.path.relpath(self.path, '.')\n\n    def _changed_md5(self):\n        if not os.path.exists(self.path):\n            return True\n\n        state = self.project.state.get(self.path)\n        if state and state.mtime == self.mtime() and state.inode == self.inode():\n            md5 = state.md5\n        else:\n            md5 = self.compute_md5()\n\n        return self.md5 != md5\n\n    def changed(self):\n        return self._changed_md5()\n\n    def compute_md5(self):\n        if os.path.isdir(self.path):\n            return dirhash(self.path, hashfunc='md5')\n        else:\n            return file_md5(self.path)[0]\n\n    def mtime(self):\n        return os.path.getmtime(self.path)\n\n    def inode(self):\n        return os.stat(self.path).st_ino\n\n    def save(self):\n        if not os.path.exists(self.path):\n            raise CmdOutputDoesNotExistError(self.rel_path)\n\n        if not os.path.isfile(self.path) and not os.path.isdir(self.path):\n            raise CmdOutputIsNotFileOrDirError(self.rel_path)\n\n        state = self.project.state.get(self.path)\n        if state and state.mtime == self.mtime() and state.inode == self.inode():\n            md5 = state.md5\n            msg = '{} using md5 {} from state file'\n            self.project.logger.debug(msg.format(self.path, md5))\n            self.md5 = md5\n        else:\n            self.md5 = self.compute_md5()\n            self.project.state.update(self.path, self.md5, self.mtime(), self.inode())\n\n    def dumpd(self, cwd):\n        return {\n            Output.PARAM_PATH: os.path.relpath(self.path, cwd),\n            Output.PARAM_MD5: self.md5,\n        }\n\n    @classmethod\n    def loadd(cls, project, d, cwd=os.curdir):\n        path = os.path.join(cwd, d[Output.PARAM_PATH])\n        md5 = d.get(Output.PARAM_MD5, None)\n        return cls(project, path, md5=md5)\n\n    @classmethod\n    def loadd_from(cls, project, d_list, cwd=os.curdir):\n        return [cls.loadd(project, x, cwd=cwd) for x in d_list]\n\n    @classmethod\n    def loads(cls, project, s, cwd=os.curdir):\n        return cls(project, os.path.join(cwd, s), md5=None)\n\n    @classmethod\n    def loads_from(cls, project, s_list, cwd=os.curdir):\n        return [cls.loads(project, x, cwd=cwd) for x in s_list]\n\n    def stage(self):\n        for stage in self.project.stages():\n            for out in stage.outs:\n                if self.path == out.path:\n                    return stage\n        return None\n\n\nclass Output(Dependency):\n    PARAM_CACHE = 'cache'\n\n    def __init__(self, project, path, md5=None, use_cache=True):\n        super(Output, self).__init__(project, path, md5=md5)\n        self.use_cache = use_cache\n\n    @property\n    def cache(self):\n        return self.project.cache.get(self.md5)\n\n    def dumpd(self, cwd):\n        ret = super(Output, self).dumpd(cwd)\n        ret[Output.PARAM_CACHE] = self.use_cache\n        return ret\n\n    @classmethod\n    def loadd(cls, project, d, cwd=os.curdir):\n        ret = super(Output, cls).loadd(project, d, cwd=cwd)\n        ret.use_cache = d.get(Output.PARAM_CACHE, True)\n        return ret\n\n    @classmethod\n    def loads(cls, project, s, use_cache=True, cwd=os.curdir):\n        ret = super(Output, cls).loads(project, s, cwd=cwd)\n        ret.use_cache = use_cache\n        return ret\n\n    @classmethod\n    def loads_from(cls, project, s_list, use_cache=False, cwd=os.curdir):\n        return [cls.loads(project, x, use_cache=use_cache, cwd=cwd) for x in s_list]\n\n    def _changed_file(self, path, cache):\n        if os.path.isfile(path) and \\\n           os.path.isfile(cache) and \\\n           System.samefile(path, cache) and \\\n           os.stat(cache).st_mode & stat.S_IREAD:\n            return False\n\n        return True\n\n    def _changed_dir(self):\n        if not os.path.isdir(self.path) or not os.path.isdir(self.cache):\n            return True\n\n        for root, dirs, files in os.walk(self.path):\n            for fname in files:\n                path = os.path.join(root, fname)\n                mtime = os.path.getmtime(path)\n                inode = os.stat(path).st_ino\n\n                state = self.project.state.get(path)\n                if state and state.mtime == mtime and state.inode == inode:\n                    md5 = state.md5\n                else:\n                    md5 = file_md5(path)[0]\n\n                cache = self.project.cache.get(md5)\n                if self._changed_file(path, cache):\n                    return True\n\n        return False\n\n    def changed(self):\n        ret = True\n\n        if not self.use_cache:\n            ret = super(Output, self).changed()\n        elif os.path.isfile(self.path) and \\\n             os.path.isfile(self.cache):\n            ret = self._changed_file(self.path, self.cache)\n        elif os.path.isdir(self.path) and \\\n             os.path.isdir(self.cache):\n            ret = self._changed_dir()\n\n        msg = \"Data {} with cache {} \"\n        if ret:\n            msg += \"changed\"\n        else:\n            msg += \"didn't change\"\n        self.project.logger.debug(msg.format(self.path, self.cache))\n\n        return ret\n\n    def hardlink(self, src, link):\n        self.project.logger.debug(\"creating hardlink {} -> {}\".format(src, link))\n        System.hardlink(src, link)\n        os.chmod(src, stat.S_IREAD)\n\n    def dir_cache(self):\n        res = {}\n        for root, dirs, files in os.walk(self.cache):\n            for fname in files:\n                path = os.path.join(root, fname)\n                relpath = os.path.relpath(path, self.cache)\n                with open(path, 'r') as fd:\n                    d = yaml.safe_load(fd)\n                md5 = d[Output.PARAM_MD5]\n                res[relpath] = self.project.cache.get(md5)\n        return res\n\n    def checkout(self):\n        if not self.use_cache:\n            return\n\n        self.project.logger.debug(\"Checking out {} with cache {}\".format(self.path, self.cache))\n\n        if not self.changed():\n            msg = \"Data {} with cache {} didn't change, skipping checkout.\"\n            self.project.logger.debug(msg.format(self.path, self.cache))\n            return\n\n        if not os.path.exists(self.cache):\n            self.project.logger.warn(u'\\'{}\\': cache file not found'.format(self.dvc_path))\n            self.remove()\n            return\n\n        if os.path.exists(self.path):\n            msg = \"Data {} exists. Removing before checkout\"\n            self.project.logger.debug(msg.format(self.path))\n            self.remove()\n\n        if os.path.isfile(self.cache):\n            self.hardlink(self.cache, self.path)\n            return\n\n        for relpath, cache in self.dir_cache().items():\n            path = os.path.join(self.path, relpath)\n            dname = os.path.dirname(path)\n\n            if not os.path.exists(dname):\n                os.makedirs(dname)\n\n            self.hardlink(cache, path)\n\n    def save(self):\n        super(Output, self).save()\n\n        if not self.use_cache:\n            return\n\n        self.project.logger.debug(\"Saving {} to {}\".format(self.path, self.cache))\n\n        if self.project.scm.is_tracked(self.path):\n            raise CmdOutputAlreadyTrackedError(self.rel_path)\n\n        if not self.changed():\n             return\n\n        if os.path.exists(self.cache):\n            # This means that we already have cache for this data.\n            # We remove data and link it to existing cache to save\n            # some space.\n            msg = \"Cache {} already exists, performing checkout for {}\"\n            self.project.logger.debug(msg.format(self.cache, self.path))\n            self.checkout()\n            return\n\n        if os.path.isfile(self.path):\n            self.hardlink(self.path, self.cache)\n            return\n\n        for root, dirs, files in os.walk(self.path):\n            for fname in files:\n                path = os.path.join(root, fname)\n                relpath = os.path.relpath(path, self.path)\n                md5 = file_md5(path)[0]\n                cache = self.project.cache.get(md5)\n                cache_info = os.path.join(self.cache, relpath)\n                cache_dir = os.path.dirname(cache_info)\n\n                if os.path.exists(cache):\n                    self._remove(path, None)\n                    self.hardlink(cache, path)\n                else:\n                    self.hardlink(path, cache)\n\n                if not os.path.exists(cache_dir):\n                    os.makedirs(cache_dir)\n\n                with open(cache_info, 'w') as fd:\n                    yaml.safe_dump({self.PARAM_MD5: md5}, fd, default_flow_style=False)\n\n    def _remove(self, path, cache):\n        self.project.logger.debug(\"Removing '{}'\".format(path))\n        os.chmod(path, stat.S_IWUSR)\n        os.unlink(path)\n        if cache != None and os.path.exists(cache):\n            os.chmod(cache, stat.S_IREAD)\n\n    def remove(self):\n        if not os.path.exists(self.path):\n            return\n\n        if os.path.isfile(self.path):\n            self._remove(self.path, self.cache)\n            return\n\n        caches = self.dir_cache()\n        for root, dirs, files in os.walk(self.path, topdown=False):\n            for d in dirs:\n                path = os.path.join(root, d)\n                os.rmdir(path)\n            for f in files:\n                path = os.path.join(root, f)\n                relpath = os.path.relpath(path, self.path)\n                cache = caches.get(relpath, None)\n                self._remove(path, cache)\n        os.rmdir(self.path)\n", "path": "dvc/output.py"}]}