~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/hashcache.py

  • Committer: Martin Pool
  • Date: 2005-07-11 03:42:53 UTC
  • Revision ID: mbp@sourcefrog.net-20050711034253-412281abeb9f56ad
doc

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
# TODO: Up-front, stat all files in order and remove those which are deleted or 
18
 
# out-of-date.  Don't actually re-read them until they're needed.  That ought 
19
 
# to bring all the inodes into core so that future stats to them are fast, and 
20
 
# it preserves the nice property that any caller will always get up-to-date
21
 
# data except in unavoidable cases.
 
17
 
 
18
 
 
19
# TODO: Perhaps have a way to stat all the files in inode order, and
 
20
# then remember that they're all fresh for the lifetime of the object?
 
21
 
 
22
# TODO: Keep track of whether there are in-memory updates that need to
 
23
# be flushed.
22
24
 
23
25
# TODO: Perhaps return more details on the file to avoid statting it
24
26
# again: nonexistent, file type, size, etc
25
27
 
26
 
# TODO: Perhaps use a Python pickle instead of a text file; might be faster.
27
28
 
28
29
 
29
30
 
30
31
CACHE_HEADER = "### bzr hashcache v5\n"
31
32
 
32
 
import os, stat, time
33
 
import sha
34
 
 
35
 
from bzrlib.osutils import sha_file
36
 
from bzrlib.trace import mutter, warning
37
 
from bzrlib.atomicfile import AtomicFile
38
 
 
39
 
 
40
 
FP_MODE_COLUMN = 5
41
33
 
42
34
def _fingerprint(abspath):
 
35
    import os, stat
 
36
 
43
37
    try:
44
38
        fs = os.lstat(abspath)
45
39
    except OSError:
52
46
    # we discard any high precision because it's not reliable; perhaps we
53
47
    # could do better on some systems?
54
48
    return (fs.st_size, long(fs.st_mtime),
55
 
            long(fs.st_ctime), fs.st_ino, fs.st_dev, fs.st_mode)
 
49
            long(fs.st_ctime), fs.st_ino, fs.st_dev)
56
50
 
57
51
 
58
52
class HashCache(object):
97
91
        self.miss_count = 0
98
92
        self.stat_count = 0
99
93
        self.danger_count = 0
100
 
        self.removed_count = 0
101
 
        self.update_count = 0
102
94
        self._cache = {}
103
95
 
104
96
 
 
97
 
105
98
    def cache_file_name(self):
106
 
        return os.sep.join([self.basedir, '.bzr', 'stat-cache'])
 
99
        import os.path
 
100
        return os.path.join(self.basedir, '.bzr', 'stat-cache')
107
101
 
108
102
 
109
103
 
117
111
            self._cache = {}
118
112
 
119
113
 
120
 
    def scan(self):
121
 
        """Scan all files and remove entries where the cache entry is obsolete.
122
 
        
123
 
        Obsolete entries are those where the file has been modified or deleted
124
 
        since the entry was inserted.        
125
 
        """
126
 
        prep = [(ce[1][3], path, ce) for (path, ce) in self._cache.iteritems()]
127
 
        prep.sort()
128
 
        
129
 
        for inum, path, cache_entry in prep:
130
 
            abspath = os.sep.join([self.basedir, path])
131
 
            fp = _fingerprint(abspath)
132
 
            self.stat_count += 1
133
 
            
134
 
            cache_fp = cache_entry[1]
135
 
    
136
 
            if (not fp) or (cache_fp != fp):
137
 
                # not here or not a regular file anymore
138
 
                self.removed_count += 1
139
 
                self.needs_write = True
140
 
                del self._cache[path]
141
 
 
142
 
 
143
114
    def get_sha1(self, path):
144
 
        """Return the sha1 of a file.
 
115
        """Return the hex SHA-1 of the contents of the file at path.
 
116
 
 
117
        XXX: If the file does not exist or is not a plain file???
145
118
        """
146
 
        abspath = os.sep.join([self.basedir, path])
 
119
 
 
120
        import os, time
 
121
        from bzrlib.osutils import sha_file
 
122
        from bzrlib.trace import mutter
 
123
        
 
124
        abspath = os.path.join(self.basedir, path)
 
125
        fp = _fingerprint(abspath)
 
126
        c = self._cache.get(path)
 
127
        if c:
 
128
            cache_sha1, cache_fp = c
 
129
        else:
 
130
            cache_sha1, cache_fp = None, None
 
131
 
147
132
        self.stat_count += 1
148
 
        file_fp = _fingerprint(abspath)
149
 
        
150
 
        if not file_fp:
151
 
            # not a regular file or not existing
152
 
            if path in self._cache:
153
 
                self.removed_count += 1
154
 
                self.needs_write = True
155
 
                del self._cache[path]
156
 
            return None        
157
 
 
158
 
        if path in self._cache:
159
 
            cache_sha1, cache_fp = self._cache[path]
160
 
        else:
161
 
            cache_sha1, cache_fp = None, None
162
 
 
163
 
        if cache_fp == file_fp:
 
133
 
 
134
        if not fp:
 
135
            # not a regular file
 
136
            return None
 
137
        elif cache_fp and (cache_fp == fp):
164
138
            self.hit_count += 1
165
139
            return cache_sha1
166
 
        
167
 
        self.miss_count += 1
168
 
 
169
 
 
170
 
        mode = file_fp[FP_MODE_COLUMN]
171
 
        if stat.S_ISREG(mode):
172
 
            digest = sha_file(file(abspath, 'rb', buffering=65000))
173
 
        elif stat.S_ISLNK(mode):
174
 
            link_target = os.readlink(abspath)
175
 
            digest = sha.new(os.readlink(abspath)).hexdigest()
176
140
        else:
177
 
            raise BzrError("file %r: unknown file stat mode: %o"%(abspath,mode))
 
141
            self.miss_count += 1
 
142
            digest = sha_file(file(abspath, 'rb'))
178
143
 
179
 
        now = int(time.time())
180
 
        if file_fp[1] >= now or file_fp[2] >= now:
181
 
            # changed too recently; can't be cached.  we can
182
 
            # return the result and it could possibly be cached
183
 
            # next time.
184
 
            self.danger_count += 1 
185
 
            if cache_fp:
186
 
                self.removed_count += 1
 
144
            now = int(time.time())
 
145
            if fp[1] >= now or fp[2] >= now:
 
146
                # changed too recently; can't be cached.  we can
 
147
                # return the result and it could possibly be cached
 
148
                # next time.
 
149
                self.danger_count += 1 
 
150
                if cache_fp:
 
151
                    mutter("remove outdated entry for %s" % path)
 
152
                    self.needs_write = True
 
153
                    del self._cache[path]
 
154
            elif (fp != cache_fp) or (digest != cache_sha1):
 
155
                mutter("update entry for %s" % path)
 
156
                mutter("  %r" % (fp,))
 
157
                mutter("  %r" % (cache_fp,))
187
158
                self.needs_write = True
188
 
                del self._cache[path]
189
 
        else:
190
 
            self.update_count += 1
191
 
            self.needs_write = True
192
 
            self._cache[path] = (digest, file_fp)
193
 
        return digest
194
 
        
 
159
                self._cache[path] = (digest, fp)
 
160
 
 
161
            return digest
 
162
 
 
163
 
 
164
 
195
165
    def write(self):
196
166
        """Write contents of cache to file."""
 
167
        from atomicfile import AtomicFile
 
168
 
197
169
        outf = AtomicFile(self.cache_file_name(), 'wb')
198
170
        try:
199
171
            print >>outf, CACHE_HEADER,
222
194
 
223
195
        If the cache file has the wrong version marker, this just clears 
224
196
        the cache."""
 
197
        from bzrlib.trace import mutter, warning
 
198
 
225
199
        self._cache = {}
226
200
 
227
201
        fn = self.cache_file_name()
228
202
        try:
229
 
            inf = file(fn, 'rb', buffering=65000)
 
203
            inf = file(fn, 'rb')
230
204
        except IOError, e:
231
205
            mutter("failed to open %s: %s" % (fn, e))
232
 
            # better write it now so it is valid
233
 
            self.needs_write = True
234
206
            return
235
207
 
236
208
 
238
210
        if hdr != CACHE_HEADER:
239
211
            mutter('cache header marker not found at top of %s; discarding cache'
240
212
                   % fn)
241
 
            self.needs_write = True
242
213
            return
243
214
 
244
215
        for l in inf:
250
221
 
251
222
            pos += 3
252
223
            fields = l[pos:].split(' ')
253
 
            if len(fields) != 7:
 
224
            if len(fields) != 6:
254
225
                warning("bad line in hashcache: %r" % l)
255
226
                continue
256
227