~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/hashcache.py

  • Committer: Aaron Bentley
  • Date: 2005-07-15 13:43:54 UTC
  • mto: This revision was merged to the branch mainline in revision 923.
  • Revision ID: abentley@panoramicfeedback.com-20050715134354-78f2bca607acb415
Fixed log -v (mostly)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# (C) 2005 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
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.
 
24
 
 
25
# TODO: Perhaps return more details on the file to avoid statting it
 
26
# again: nonexistent, file type, size, etc
 
27
 
 
28
 
 
29
 
 
30
 
 
31
CACHE_HEADER = "### bzr hashcache v5\n"
 
32
 
 
33
 
 
34
def _fingerprint(abspath):
 
35
    import os, stat
 
36
 
 
37
    try:
 
38
        fs = os.lstat(abspath)
 
39
    except OSError:
 
40
        # might be missing, etc
 
41
        return None
 
42
 
 
43
    if stat.S_ISDIR(fs.st_mode):
 
44
        return None
 
45
 
 
46
    # we discard any high precision because it's not reliable; perhaps we
 
47
    # could do better on some systems?
 
48
    return (fs.st_size, long(fs.st_mtime),
 
49
            long(fs.st_ctime), fs.st_ino, fs.st_dev)
 
50
 
 
51
 
 
52
class HashCache(object):
 
53
    """Cache for looking up file SHA-1.
 
54
 
 
55
    Files are considered to match the cached value if the fingerprint
 
56
    of the file has not changed.  This includes its mtime, ctime,
 
57
    device number, inode number, and size.  This should catch
 
58
    modifications or replacement of the file by a new one.
 
59
 
 
60
    This may not catch modifications that do not change the file's
 
61
    size and that occur within the resolution window of the
 
62
    timestamps.  To handle this we specifically do not cache files
 
63
    which have changed since the start of the present second, since
 
64
    they could undetectably change again.
 
65
 
 
66
    This scheme may fail if the machine's clock steps backwards.
 
67
    Don't do that.
 
68
 
 
69
    This does not canonicalize the paths passed in; that should be
 
70
    done by the caller.
 
71
 
 
72
    _cache
 
73
        Indexed by path, points to a two-tuple of the SHA-1 of the file.
 
74
        and its fingerprint.
 
75
 
 
76
    stat_count
 
77
        number of times files have been statted
 
78
 
 
79
    hit_count
 
80
        number of times files have been retrieved from the cache, avoiding a
 
81
        re-read
 
82
        
 
83
    miss_count
 
84
        number of misses (times files have been completely re-read)
 
85
    """
 
86
    needs_write = False
 
87
 
 
88
    def __init__(self, basedir):
 
89
        self.basedir = basedir
 
90
        self.hit_count = 0
 
91
        self.miss_count = 0
 
92
        self.stat_count = 0
 
93
        self.danger_count = 0
 
94
        self._cache = {}
 
95
 
 
96
 
 
97
 
 
98
    def cache_file_name(self):
 
99
        import os.path
 
100
        return os.path.join(self.basedir, '.bzr', 'stat-cache')
 
101
 
 
102
 
 
103
 
 
104
 
 
105
    def clear(self):
 
106
        """Discard all cached information.
 
107
 
 
108
        This does not reset the counters."""
 
109
        if self._cache:
 
110
            self.needs_write = True
 
111
            self._cache = {}
 
112
 
 
113
 
 
114
    def get_sha1(self, path):
 
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???
 
118
        """
 
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
 
 
132
        self.stat_count += 1
 
133
 
 
134
        if not fp:
 
135
            # not a regular file
 
136
            return None
 
137
        elif cache_fp and (cache_fp == fp):
 
138
            self.hit_count += 1
 
139
            return cache_sha1
 
140
        else:
 
141
            self.miss_count += 1
 
142
            digest = sha_file(file(abspath, 'rb'))
 
143
 
 
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,))
 
158
                self.needs_write = True
 
159
                self._cache[path] = (digest, fp)
 
160
 
 
161
            return digest
 
162
 
 
163
 
 
164
 
 
165
    def write(self):
 
166
        """Write contents of cache to file."""
 
167
        from atomicfile import AtomicFile
 
168
 
 
169
        outf = AtomicFile(self.cache_file_name(), 'wb')
 
170
        try:
 
171
            print >>outf, CACHE_HEADER,
 
172
 
 
173
            for path, c  in self._cache.iteritems():
 
174
                assert '//' not in path, path
 
175
                outf.write(path.encode('utf-8'))
 
176
                outf.write('// ')
 
177
                print >>outf, c[0],     # hex sha1
 
178
                for fld in c[1]:
 
179
                    print >>outf, "%d" % fld,
 
180
                print >>outf
 
181
 
 
182
            outf.commit()
 
183
            self.needs_write = False
 
184
        finally:
 
185
            if not outf.closed:
 
186
                outf.abort()
 
187
        
 
188
 
 
189
 
 
190
    def read(self):
 
191
        """Reinstate cache from file.
 
192
 
 
193
        Overwrites existing cache.
 
194
 
 
195
        If the cache file has the wrong version marker, this just clears 
 
196
        the cache."""
 
197
        from bzrlib.trace import mutter, warning
 
198
 
 
199
        self._cache = {}
 
200
 
 
201
        fn = self.cache_file_name()
 
202
        try:
 
203
            inf = file(fn, 'rb')
 
204
        except IOError, e:
 
205
            mutter("failed to open %s: %s" % (fn, e))
 
206
            return
 
207
 
 
208
 
 
209
        hdr = inf.readline()
 
210
        if hdr != CACHE_HEADER:
 
211
            mutter('cache header marker not found at top of %s; discarding cache'
 
212
                   % fn)
 
213
            return
 
214
 
 
215
        for l in inf:
 
216
            pos = l.index('// ')
 
217
            path = l[:pos].decode('utf-8')
 
218
            if path in self._cache:
 
219
                warning('duplicated path %r in cache' % path)
 
220
                continue
 
221
 
 
222
            pos += 3
 
223
            fields = l[pos:].split(' ')
 
224
            if len(fields) != 6:
 
225
                warning("bad line in hashcache: %r" % l)
 
226
                continue
 
227
 
 
228
            sha1 = fields[0]
 
229
            if len(sha1) != 40:
 
230
                warning("bad sha1 in hashcache: %r" % sha1)
 
231
                continue
 
232
 
 
233
            fp = tuple(map(long, fields[1:]))
 
234
 
 
235
            self._cache[path] = (sha1, fp)
 
236
 
 
237
        self.needs_write = False
 
238
           
 
239
 
 
240
 
 
241