~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/hashcache.py

  • Committer: Martin Pool
  • Date: 2008-06-05 03:27:37 UTC
  • mto: This revision was merged to the branch mainline in revision 3491.
  • Revision ID: mbp@sourcefrog.net-20080605032737-0eytu5y1k9k5a72t
Add plugin and integration guides to doc indexes

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# (C) 2005 Canonical Ltd
2
 
 
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
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
30
30
CACHE_HEADER = "### bzr hashcache v5\n"
31
31
 
32
32
import os, stat, time
 
33
import sha
33
34
 
34
 
from bzrlib.osutils import sha_file
 
35
from bzrlib.osutils import sha_file, pathjoin, safe_unicode
35
36
from bzrlib.trace import mutter, warning
36
37
from bzrlib.atomicfile import AtomicFile
37
 
 
38
 
 
39
 
 
40
 
 
41
 
def _fingerprint(abspath):
42
 
    try:
43
 
        fs = os.lstat(abspath)
44
 
    except OSError:
45
 
        # might be missing, etc
46
 
        return None
47
 
 
48
 
    if stat.S_ISDIR(fs.st_mode):
49
 
        return None
50
 
 
51
 
    # we discard any high precision because it's not reliable; perhaps we
52
 
    # could do better on some systems?
53
 
    return (fs.st_size, long(fs.st_mtime),
54
 
            long(fs.st_ctime), fs.st_ino, fs.st_dev)
 
38
from bzrlib.errors import BzrError
 
39
 
 
40
 
 
41
FP_MTIME_COLUMN = 1
 
42
FP_CTIME_COLUMN = 2
 
43
FP_MODE_COLUMN = 5
 
44
 
55
45
 
56
46
 
57
47
class HashCache(object):
90
80
    """
91
81
    needs_write = False
92
82
 
93
 
    def __init__(self, basedir):
94
 
        self.basedir = basedir
 
83
    def __init__(self, root, cache_file_name, mode=None):
 
84
        """Create a hash cache in base dir, and set the file mode to mode."""
 
85
        self.root = safe_unicode(root)
 
86
        self.root_utf8 = self.root.encode('utf8') # where is the filesystem encoding ?
95
87
        self.hit_count = 0
96
88
        self.miss_count = 0
97
89
        self.stat_count = 0
99
91
        self.removed_count = 0
100
92
        self.update_count = 0
101
93
        self._cache = {}
102
 
 
 
94
        self._mode = mode
 
95
        self._cache_file_name = safe_unicode(cache_file_name)
103
96
 
104
97
    def cache_file_name(self):
105
 
        return os.sep.join([self.basedir, '.bzr', 'stat-cache'])
106
 
 
107
 
 
108
 
 
 
98
        return self._cache_file_name
109
99
 
110
100
    def clear(self):
111
101
        """Discard all cached information.
115
105
            self.needs_write = True
116
106
            self._cache = {}
117
107
 
118
 
 
119
108
    def scan(self):
120
109
        """Scan all files and remove entries where the cache entry is obsolete.
121
110
        
122
111
        Obsolete entries are those where the file has been modified or deleted
123
112
        since the entry was inserted.        
124
113
        """
 
114
        # FIXME optimisation opportunity, on linux [and check other oses]:
 
115
        # rather than iteritems order, stat in inode order.
125
116
        prep = [(ce[1][3], path, ce) for (path, ce) in self._cache.iteritems()]
126
117
        prep.sort()
127
118
        
128
119
        for inum, path, cache_entry in prep:
129
 
            abspath = os.sep.join([self.basedir, path])
130
 
            fp = _fingerprint(abspath)
 
120
            abspath = pathjoin(self.root, path)
 
121
            fp = self._fingerprint(abspath)
131
122
            self.stat_count += 1
132
123
            
133
124
            cache_fp = cache_entry[1]
138
129
                self.needs_write = True
139
130
                del self._cache[path]
140
131
 
141
 
 
142
 
 
143
 
    def get_sha1(self, path):
 
132
    def get_sha1(self, path, stat_value=None):
144
133
        """Return the sha1 of a file.
145
134
        """
146
 
        abspath = os.sep.join([self.basedir, path])
 
135
        if path.__class__ is str:
 
136
            abspath = pathjoin(self.root_utf8, path)
 
137
        else:
 
138
            abspath = pathjoin(self.root, path)
147
139
        self.stat_count += 1
148
 
        file_fp = _fingerprint(abspath)
 
140
        file_fp = self._fingerprint(abspath, stat_value)
149
141
        
150
142
        if not file_fp:
151
143
            # not a regular file or not existing
161
153
            cache_sha1, cache_fp = None, None
162
154
 
163
155
        if cache_fp == file_fp:
 
156
            ## mutter("hashcache hit for %s %r -> %s", path, file_fp, cache_sha1)
 
157
            ## mutter("now = %s", time.time())
164
158
            self.hit_count += 1
165
159
            return cache_sha1
166
160
        
167
161
        self.miss_count += 1
168
 
        digest = sha_file(file(abspath, 'rb', buffering=65000))
169
 
 
170
 
        now = int(time.time())
171
 
        if file_fp[1] >= now or file_fp[2] >= now:
 
162
 
 
163
        mode = file_fp[FP_MODE_COLUMN]
 
164
        if stat.S_ISREG(mode):
 
165
            digest = self._really_sha1_file(abspath)
 
166
        elif stat.S_ISLNK(mode):
 
167
            digest = sha.new(os.readlink(abspath)).hexdigest()
 
168
        else:
 
169
            raise BzrError("file %r: unknown file stat mode: %o"%(abspath,mode))
 
170
 
 
171
        # window of 3 seconds to allow for 2s resolution on windows,
 
172
        # unsynchronized file servers, etc.
 
173
        cutoff = self._cutoff_time()
 
174
        if file_fp[FP_MTIME_COLUMN] >= cutoff \
 
175
                or file_fp[FP_CTIME_COLUMN] >= cutoff:
172
176
            # changed too recently; can't be cached.  we can
173
177
            # return the result and it could possibly be cached
174
178
            # next time.
175
 
            self.danger_count += 1 
 
179
            #
 
180
            # the point is that we only want to cache when we are sure that any
 
181
            # subsequent modifications of the file can be detected.  If a
 
182
            # modification neither changes the inode, the device, the size, nor
 
183
            # the mode, then we can only distinguish it by time; therefore we
 
184
            # need to let sufficient time elapse before we may cache this entry
 
185
            # again.  If we didn't do this, then, for example, a very quick 1
 
186
            # byte replacement in the file might go undetected.
 
187
            ## mutter('%r modified too recently; not caching', path)
 
188
            self.danger_count += 1
176
189
            if cache_fp:
177
190
                self.removed_count += 1
178
191
                self.needs_write = True
179
192
                del self._cache[path]
180
193
        else:
 
194
            ## mutter('%r added to cache: now=%f, mtime=%d, ctime=%d',
 
195
            ##        path, time.time(), file_fp[FP_MTIME_COLUMN],
 
196
            ##        file_fp[FP_CTIME_COLUMN])
181
197
            self.update_count += 1
182
198
            self.needs_write = True
183
199
            self._cache[path] = (digest, file_fp)
184
 
        
185
200
        return digest
 
201
 
 
202
    def _really_sha1_file(self, abspath):
 
203
        """Calculate the SHA1 of a file by reading the full text"""
 
204
        return sha_file(file(abspath, 'rb', buffering=65000))
186
205
        
187
 
 
188
 
 
189
 
 
190
206
    def write(self):
191
207
        """Write contents of cache to file."""
192
 
        outf = AtomicFile(self.cache_file_name(), 'wb')
 
208
        outf = AtomicFile(self.cache_file_name(), 'wb', new_mode=self._mode)
193
209
        try:
194
 
            print >>outf, CACHE_HEADER,
 
210
            outf.write(CACHE_HEADER)
195
211
 
196
212
            for path, c  in self._cache.iteritems():
197
 
                assert '//' not in path, path
198
 
                outf.write(path.encode('utf-8'))
199
 
                outf.write('// ')
200
 
                print >>outf, c[0],     # hex sha1
201
 
                for fld in c[1]:
202
 
                    print >>outf, "%d" % fld,
203
 
                print >>outf
204
 
 
 
213
                line_info = [path.encode('utf-8'), '// ', c[0], ' ']
 
214
                line_info.append(' '.join([str(fld) for fld in c[1]]))
 
215
                line_info.append('\n')
 
216
                outf.write(''.join(line_info))
205
217
            outf.commit()
206
218
            self.needs_write = False
 
219
            ## mutter("write hash cache: %s hits=%d misses=%d stat=%d recent=%d updates=%d",
 
220
            ##        self.cache_file_name(), self.hit_count, self.miss_count,
 
221
            ##        self.stat_count,
 
222
            ##        self.danger_count, self.update_count)
207
223
        finally:
208
 
            if not outf.closed:
209
 
                outf.abort()
210
 
        
211
 
 
 
224
            outf.close()
212
225
 
213
226
    def read(self):
214
227
        """Reinstate cache from file.
223
236
        try:
224
237
            inf = file(fn, 'rb', buffering=65000)
225
238
        except IOError, e:
226
 
            mutter("failed to open %s: %s" % (fn, e))
 
239
            mutter("failed to open %s: %s", fn, e)
227
240
            # better write it now so it is valid
228
241
            self.needs_write = True
229
242
            return
230
243
 
231
 
 
232
244
        hdr = inf.readline()
233
245
        if hdr != CACHE_HEADER:
234
 
            mutter('cache header marker not found at top of %s; discarding cache'
235
 
                   % fn)
 
246
            mutter('cache header marker not found at top of %s;'
 
247
                   ' discarding cache', fn)
236
248
            self.needs_write = True
237
249
            return
238
250
 
245
257
 
246
258
            pos += 3
247
259
            fields = l[pos:].split(' ')
248
 
            if len(fields) != 6:
 
260
            if len(fields) != 7:
249
261
                warning("bad line in hashcache: %r" % l)
250
262
                continue
251
263
 
259
271
            self._cache[path] = (sha1, fp)
260
272
 
261
273
        self.needs_write = False
 
274
 
 
275
    def _cutoff_time(self):
 
276
        """Return cutoff time.
 
277
 
 
278
        Files modified more recently than this time are at risk of being
 
279
        undetectably modified and so can't be cached.
 
280
        """
 
281
        return int(time.time()) - 3
262
282
           
263
 
 
264
 
 
265
 
        
 
283
    def _fingerprint(self, abspath, stat_value=None):
 
284
        if stat_value is None:
 
285
            try:
 
286
                stat_value = os.lstat(abspath)
 
287
            except OSError:
 
288
                # might be missing, etc
 
289
                return None
 
290
        if stat.S_ISDIR(stat_value.st_mode):
 
291
            return None
 
292
        # we discard any high precision because it's not reliable; perhaps we
 
293
        # could do better on some systems?
 
294
        return (stat_value.st_size, long(stat_value.st_mtime),
 
295
                long(stat_value.st_ctime), stat_value.st_ino, 
 
296
                stat_value.st_dev, stat_value.st_mode)