~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/hashcache.py

  • Committer: Robert Collins
  • Date: 2005-09-28 05:25:54 UTC
  • mfrom: (1185.1.42)
  • mto: (1092.2.18)
  • mto: This revision was merged to the branch mainline in revision 1397.
  • Revision ID: robertc@robertcollins.net-20050928052554-beb985505f77ea6a
update symlink branch to integration

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
2
 
#
 
1
# (C) 2005 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
23
23
# TODO: Perhaps return more details on the file to avoid statting it
24
24
# again: nonexistent, file type, size, etc
25
25
 
26
 
# TODO: Perhaps use a Python pickle instead of a text file; might be faster.
27
 
 
28
26
 
29
27
 
30
28
CACHE_HEADER = "### bzr hashcache v5\n"
32
30
import os, stat, time
33
31
import sha
34
32
 
35
 
from bzrlib.osutils import sha_file, pathjoin, safe_unicode
 
33
from bzrlib.osutils import sha_file
36
34
from bzrlib.trace import mutter, warning
37
 
from bzrlib.atomicfile import AtomicFile
38
 
from bzrlib.errors import BzrError
39
 
 
40
 
 
41
 
FP_MTIME_COLUMN = 1
42
 
FP_CTIME_COLUMN = 2
 
35
 
43
36
FP_MODE_COLUMN = 5
44
37
 
 
38
def _fingerprint(abspath):
 
39
    try:
 
40
        fs = os.lstat(abspath)
 
41
    except OSError:
 
42
        # might be missing, etc
 
43
        return None
 
44
 
 
45
    if stat.S_ISDIR(fs.st_mode):
 
46
        return None
 
47
 
 
48
    # we discard any high precision because it's not reliable; perhaps we
 
49
    # could do better on some systems?
 
50
    return (fs.st_size, long(fs.st_mtime),
 
51
            long(fs.st_ctime), fs.st_ino, fs.st_dev, fs.st_mode)
45
52
 
46
53
 
47
54
class HashCache(object):
80
87
    """
81
88
    needs_write = False
82
89
 
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 ?
 
90
    def __init__(self, basedir):
 
91
        self.basedir = basedir
87
92
        self.hit_count = 0
88
93
        self.miss_count = 0
89
94
        self.stat_count = 0
91
96
        self.removed_count = 0
92
97
        self.update_count = 0
93
98
        self._cache = {}
94
 
        self._mode = mode
95
 
        self._cache_file_name = safe_unicode(cache_file_name)
 
99
 
96
100
 
97
101
    def cache_file_name(self):
98
 
        return self._cache_file_name
 
102
        return os.sep.join([self.basedir, '.bzr', 'stat-cache'])
 
103
 
 
104
 
 
105
 
99
106
 
100
107
    def clear(self):
101
108
        """Discard all cached information.
105
112
            self.needs_write = True
106
113
            self._cache = {}
107
114
 
 
115
 
108
116
    def scan(self):
109
117
        """Scan all files and remove entries where the cache entry is obsolete.
110
118
        
111
119
        Obsolete entries are those where the file has been modified or deleted
112
120
        since the entry was inserted.        
113
121
        """
114
 
        # FIXME optimisation opportunity, on linux [and check other oses]:
115
 
        # rather than iteritems order, stat in inode order.
116
122
        prep = [(ce[1][3], path, ce) for (path, ce) in self._cache.iteritems()]
117
123
        prep.sort()
118
124
        
119
125
        for inum, path, cache_entry in prep:
120
 
            abspath = pathjoin(self.root, path)
121
 
            fp = self._fingerprint(abspath)
 
126
            abspath = os.sep.join([self.basedir, path])
 
127
            fp = _fingerprint(abspath)
122
128
            self.stat_count += 1
123
129
            
124
130
            cache_fp = cache_entry[1]
129
135
                self.needs_write = True
130
136
                del self._cache[path]
131
137
 
132
 
    def get_sha1(self, path, stat_value=None):
 
138
 
 
139
 
 
140
    def get_sha1(self, path):
133
141
        """Return the sha1 of a file.
134
142
        """
135
 
        if path.__class__ is str:
136
 
            abspath = pathjoin(self.root_utf8, path)
137
 
        else:
138
 
            abspath = pathjoin(self.root, path)
 
143
        abspath = os.sep.join([self.basedir, path])
139
144
        self.stat_count += 1
140
 
        file_fp = self._fingerprint(abspath, stat_value)
 
145
        file_fp = _fingerprint(abspath)
141
146
        
142
147
        if not file_fp:
143
148
            # not a regular file or not existing
153
158
            cache_sha1, cache_fp = None, None
154
159
 
155
160
        if cache_fp == file_fp:
156
 
            ## mutter("hashcache hit for %s %r -> %s", path, file_fp, cache_sha1)
157
 
            ## mutter("now = %s", time.time())
158
161
            self.hit_count += 1
159
162
            return cache_sha1
160
163
        
161
164
        self.miss_count += 1
162
165
 
 
166
 
163
167
        mode = file_fp[FP_MODE_COLUMN]
164
168
        if stat.S_ISREG(mode):
165
 
            digest = self._really_sha1_file(abspath)
 
169
            digest = sha_file(file(abspath, 'rb', buffering=65000))
166
170
        elif stat.S_ISLNK(mode):
 
171
            link_target = os.readlink(abspath)
167
172
            digest = sha.new(os.readlink(abspath)).hexdigest()
168
173
        else:
169
174
            raise BzrError("file %r: unknown file stat mode: %o"%(abspath,mode))
170
175
 
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:
 
176
        now = int(time.time())
 
177
        if file_fp[1] >= now or file_fp[2] >= now:
176
178
            # changed too recently; can't be cached.  we can
177
179
            # return the result and it could possibly be cached
178
180
            # next time.
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
 
181
            self.danger_count += 1 
189
182
            if cache_fp:
190
183
                self.removed_count += 1
191
184
                self.needs_write = True
192
185
                del self._cache[path]
193
186
        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])
197
187
            self.update_count += 1
198
188
            self.needs_write = True
199
189
            self._cache[path] = (digest, file_fp)
200
190
        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))
205
191
        
206
192
    def write(self):
207
193
        """Write contents of cache to file."""
208
 
        outf = AtomicFile(self.cache_file_name(), 'wb', new_mode=self._mode)
 
194
        from atomicfile import AtomicFile
 
195
 
 
196
        outf = AtomicFile(self.cache_file_name(), 'wb')
209
197
        try:
210
 
            outf.write(CACHE_HEADER)
 
198
            print >>outf, CACHE_HEADER,
211
199
 
212
200
            for path, c  in self._cache.iteritems():
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))
 
201
                assert '//' not in path, path
 
202
                outf.write(path.encode('utf-8'))
 
203
                outf.write('// ')
 
204
                print >>outf, c[0],     # hex sha1
 
205
                for fld in c[1]:
 
206
                    print >>outf, "%d" % fld,
 
207
                print >>outf
 
208
 
217
209
            outf.commit()
218
210
            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)
223
211
        finally:
224
 
            outf.close()
 
212
            if not outf.closed:
 
213
                outf.abort()
 
214
        
 
215
 
225
216
 
226
217
    def read(self):
227
218
        """Reinstate cache from file.
236
227
        try:
237
228
            inf = file(fn, 'rb', buffering=65000)
238
229
        except IOError, e:
239
 
            mutter("failed to open %s: %s", fn, e)
 
230
            mutter("failed to open %s: %s" % (fn, e))
240
231
            # better write it now so it is valid
241
232
            self.needs_write = True
242
233
            return
243
234
 
 
235
 
244
236
        hdr = inf.readline()
245
237
        if hdr != CACHE_HEADER:
246
 
            mutter('cache header marker not found at top of %s;'
247
 
                   ' discarding cache', fn)
 
238
            mutter('cache header marker not found at top of %s; discarding cache'
 
239
                   % fn)
248
240
            self.needs_write = True
249
241
            return
250
242
 
271
263
            self._cache[path] = (sha1, fp)
272
264
 
273
265
        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
282
266
           
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)
 
267
 
 
268
 
 
269