##// END OF EJS Templates
dirstate: make sure we read the dirstate before setting parents
Matt Mackall -
r1394:b20b683e default
parent child Browse files
Show More
@@ -1,424 +1,426
1 1 """
2 2 dirstate.py - working directory tracking for mercurial
3 3
4 4 Copyright 2005 Matt Mackall <mpm@selenic.com>
5 5
6 6 This software may be used and distributed according to the terms
7 7 of the GNU General Public License, incorporated herein by reference.
8 8 """
9 9
10 10 import struct, os
11 11 from node import *
12 12 from demandload import *
13 13 demandload(globals(), "time bisect stat util re")
14 14
15 15 class dirstate:
16 16 def __init__(self, opener, ui, root):
17 17 self.opener = opener
18 18 self.root = root
19 19 self.dirty = 0
20 20 self.ui = ui
21 21 self.map = None
22 22 self.pl = None
23 23 self.copies = {}
24 24 self.ignorefunc = None
25 25 self.blockignore = False
26 26
27 27 def wjoin(self, f):
28 28 return os.path.join(self.root, f)
29 29
30 30 def getcwd(self):
31 31 cwd = os.getcwd()
32 32 if cwd == self.root: return ''
33 33 return cwd[len(self.root) + 1:]
34 34
35 35 def hgignore(self):
36 36 '''return the contents of .hgignore as a list of patterns.
37 37
38 38 trailing white space is dropped.
39 39 the escape character is backslash.
40 40 comments start with #.
41 41 empty lines are skipped.
42 42
43 43 lines can be of the following formats:
44 44
45 45 syntax: regexp # defaults following lines to non-rooted regexps
46 46 syntax: glob # defaults following lines to non-rooted globs
47 47 re:pattern # non-rooted regular expression
48 48 glob:pattern # non-rooted glob
49 49 pattern # pattern of the current default type'''
50 50 syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:'}
51 51 def parselines(fp):
52 52 for line in fp:
53 53 escape = False
54 54 for i in xrange(len(line)):
55 55 if escape: escape = False
56 56 elif line[i] == '\\': escape = True
57 57 elif line[i] == '#': break
58 58 line = line[:i].rstrip()
59 59 if line: yield line
60 60 pats = []
61 61 try:
62 62 fp = open(self.wjoin('.hgignore'))
63 63 syntax = 'relre:'
64 64 for line in parselines(fp):
65 65 if line.startswith('syntax:'):
66 66 s = line[7:].strip()
67 67 try:
68 68 syntax = syntaxes[s]
69 69 except KeyError:
70 70 self.ui.warn("ignoring invalid syntax '%s'\n" % s)
71 71 continue
72 72 pat = syntax + line
73 73 for s in syntaxes.values():
74 74 if line.startswith(s):
75 75 pat = line
76 76 break
77 77 pats.append(pat)
78 78 except IOError: pass
79 79 return pats
80 80
81 81 def ignore(self, fn):
82 82 '''default match function used by dirstate and localrepository.
83 83 this honours the .hgignore file, and nothing more.'''
84 84 if self.blockignore:
85 85 return False
86 86 if not self.ignorefunc:
87 87 ignore = self.hgignore()
88 88 if ignore:
89 89 files, self.ignorefunc, anypats = util.matcher(self.root,
90 90 inc=ignore)
91 91 else:
92 92 self.ignorefunc = util.never
93 93 return self.ignorefunc(fn)
94 94
95 95 def __del__(self):
96 96 if self.dirty:
97 97 self.write()
98 98
99 99 def __getitem__(self, key):
100 100 try:
101 101 return self.map[key]
102 102 except TypeError:
103 103 self.read()
104 104 return self[key]
105 105
106 106 def __contains__(self, key):
107 107 if not self.map: self.read()
108 108 return key in self.map
109 109
110 110 def parents(self):
111 111 if not self.pl:
112 112 self.read()
113 113 return self.pl
114 114
115 115 def markdirty(self):
116 116 if not self.dirty:
117 117 self.dirty = 1
118 118
119 119 def setparents(self, p1, p2=nullid):
120 if not self.pl:
121 self.read()
120 122 self.markdirty()
121 123 self.pl = p1, p2
122 124
123 125 def state(self, key):
124 126 try:
125 127 return self[key][0]
126 128 except KeyError:
127 129 return "?"
128 130
129 131 def read(self):
130 132 if self.map is not None: return self.map
131 133
132 134 self.map = {}
133 135 self.pl = [nullid, nullid]
134 136 try:
135 137 st = self.opener("dirstate").read()
136 138 if not st: return
137 139 except: return
138 140
139 141 self.pl = [st[:20], st[20: 40]]
140 142
141 143 pos = 40
142 144 while pos < len(st):
143 145 e = struct.unpack(">cllll", st[pos:pos+17])
144 146 l = e[4]
145 147 pos += 17
146 148 f = st[pos:pos + l]
147 149 if '\0' in f:
148 150 f, c = f.split('\0')
149 151 self.copies[f] = c
150 152 self.map[f] = e[:4]
151 153 pos += l
152 154
153 155 def copy(self, source, dest):
154 156 self.read()
155 157 self.markdirty()
156 158 self.copies[dest] = source
157 159
158 160 def copied(self, file):
159 161 return self.copies.get(file, None)
160 162
161 163 def update(self, files, state, **kw):
162 164 ''' current states:
163 165 n normal
164 166 m needs merging
165 167 r marked for removal
166 168 a marked for addition'''
167 169
168 170 if not files: return
169 171 self.read()
170 172 self.markdirty()
171 173 for f in files:
172 174 if state == "r":
173 175 self.map[f] = ('r', 0, 0, 0)
174 176 else:
175 177 s = os.lstat(os.path.join(self.root, f))
176 178 st_size = kw.get('st_size', s.st_size)
177 179 st_mtime = kw.get('st_mtime', s.st_mtime)
178 180 self.map[f] = (state, s.st_mode, st_size, st_mtime)
179 181 if self.copies.has_key(f):
180 182 del self.copies[f]
181 183
182 184 def forget(self, files):
183 185 if not files: return
184 186 self.read()
185 187 self.markdirty()
186 188 for f in files:
187 189 try:
188 190 del self.map[f]
189 191 except KeyError:
190 192 self.ui.warn("not in dirstate: %s!\n" % f)
191 193 pass
192 194
193 195 def clear(self):
194 196 self.map = {}
195 197 self.markdirty()
196 198
197 199 def write(self):
198 200 st = self.opener("dirstate", "w")
199 201 st.write("".join(self.pl))
200 202 for f, e in self.map.items():
201 203 c = self.copied(f)
202 204 if c:
203 205 f = f + "\0" + c
204 206 e = struct.pack(">cllll", e[0], e[1], e[2], e[3], len(f))
205 207 st.write(e + f)
206 208 self.dirty = 0
207 209
208 210 def filterfiles(self, files):
209 211 ret = {}
210 212 unknown = []
211 213
212 214 for x in files:
213 215 if x is '.':
214 216 return self.map.copy()
215 217 if x not in self.map:
216 218 unknown.append(x)
217 219 else:
218 220 ret[x] = self.map[x]
219 221
220 222 if not unknown:
221 223 return ret
222 224
223 225 b = self.map.keys()
224 226 b.sort()
225 227 blen = len(b)
226 228
227 229 for x in unknown:
228 230 bs = bisect.bisect(b, x)
229 231 if bs != 0 and b[bs-1] == x:
230 232 ret[x] = self.map[x]
231 233 continue
232 234 while bs < blen:
233 235 s = b[bs]
234 236 if len(s) > len(x) and s.startswith(x) and s[len(x)] == '/':
235 237 ret[s] = self.map[s]
236 238 else:
237 239 break
238 240 bs += 1
239 241 return ret
240 242
241 243 def walk(self, files=None, match=util.always, dc=None):
242 244 self.read()
243 245
244 246 # walk all files by default
245 247 if not files:
246 248 files = [self.root]
247 249 if not dc:
248 250 dc = self.map.copy()
249 251 elif not dc:
250 252 dc = self.filterfiles(files)
251 253
252 254 def statmatch(file, stat):
253 255 file = util.pconvert(file)
254 256 if file not in dc and self.ignore(file):
255 257 return False
256 258 return match(file)
257 259
258 260 return self.walkhelper(files=files, statmatch=statmatch, dc=dc)
259 261
260 262 # walk recursively through the directory tree, finding all files
261 263 # matched by the statmatch function
262 264 #
263 265 # results are yielded in a tuple (src, filename), where src is one of:
264 266 # 'f' the file was found in the directory tree
265 267 # 'm' the file was only in the dirstate and not in the tree
266 268 #
267 269 # dc is an optional arg for the current dirstate. dc is not modified
268 270 # directly by this function, but might be modified by your statmatch call.
269 271 #
270 272 def walkhelper(self, files, statmatch, dc):
271 273 def supported_type(f, st):
272 274 if stat.S_ISREG(st.st_mode):
273 275 return True
274 276 else:
275 277 kind = 'unknown'
276 278 if stat.S_ISCHR(st.st_mode): kind = 'character device'
277 279 elif stat.S_ISBLK(st.st_mode): kind = 'block device'
278 280 elif stat.S_ISFIFO(st.st_mode): kind = 'fifo'
279 281 elif stat.S_ISLNK(st.st_mode): kind = 'symbolic link'
280 282 elif stat.S_ISSOCK(st.st_mode): kind = 'socket'
281 283 elif stat.S_ISDIR(st.st_mode): kind = 'directory'
282 284 self.ui.warn('%s: unsupported file type (type is %s)\n' % (
283 285 util.pathto(self.getcwd(), f),
284 286 kind))
285 287 return False
286 288
287 289 # recursion free walker, faster than os.walk.
288 290 def findfiles(s):
289 291 retfiles = []
290 292 work = [s]
291 293 while work:
292 294 top = work.pop()
293 295 names = os.listdir(top)
294 296 names.sort()
295 297 # nd is the top of the repository dir tree
296 298 nd = util.normpath(top[len(self.root) + 1:])
297 299 if nd == '.': nd = ''
298 300 for f in names:
299 301 np = os.path.join(nd, f)
300 302 if seen(np):
301 303 continue
302 304 p = os.path.join(top, f)
303 305 # don't trip over symlinks
304 306 st = os.lstat(p)
305 307 if stat.S_ISDIR(st.st_mode):
306 308 ds = os.path.join(nd, f +'/')
307 309 if statmatch(ds, st):
308 310 work.append(p)
309 311 elif supported_type(np, st):
310 312 if statmatch(np, st):
311 313 yield util.pconvert(np)
312 314
313 315
314 316 known = {'.hg': 1}
315 317 def seen(fn):
316 318 if fn in known: return True
317 319 known[fn] = 1
318 320
319 321 # step one, find all files that match our criteria
320 322 files.sort()
321 323 for ff in util.unique(files):
322 324 f = os.path.join(self.root, ff)
323 325 try:
324 326 st = os.lstat(f)
325 327 except OSError, inst:
326 328 if ff not in dc: self.ui.warn('%s: %s\n' % (
327 329 util.pathto(self.getcwd(), ff),
328 330 inst.strerror))
329 331 continue
330 332 if stat.S_ISDIR(st.st_mode):
331 333 sorted = [ x for x in findfiles(f) ]
332 334 sorted.sort()
333 335 for fl in sorted:
334 336 yield 'f', fl
335 337 else:
336 338 ff = util.normpath(ff)
337 339 if seen(ff):
338 340 continue
339 341 found = False
340 342 self.blockignore = True
341 343 if supported_type(ff, st) and statmatch(ff, st):
342 344 found = True
343 345 self.blockignore = False
344 346 if found:
345 347 yield 'f', ff
346 348
347 349 # step two run through anything left in the dc hash and yield
348 350 # if we haven't already seen it
349 351 ks = dc.keys()
350 352 ks.sort()
351 353 for k in ks:
352 354 if not seen(k) and (statmatch(k, None)):
353 355 yield 'm', k
354 356
355 357 def changes(self, files=None, match=util.always):
356 358 self.read()
357 359 if not files:
358 360 files = [self.root]
359 361 dc = self.map.copy()
360 362 else:
361 363 dc = self.filterfiles(files)
362 364 lookup, modified, added, unknown = [], [], [], []
363 365 removed, deleted = [], []
364 366
365 367 # statmatch function to eliminate entries from the dirstate copy
366 368 # and put files into the appropriate array. This gets passed
367 369 # to the walking code
368 370 def statmatch(fn, s):
369 371 fn = util.pconvert(fn)
370 372 def checkappend(l, fn):
371 373 if match is util.always or match(fn):
372 374 l.append(fn)
373 375
374 376 if not s or stat.S_ISDIR(s.st_mode):
375 377 if self.ignore(fn): return False
376 378 return match(fn)
377 379
378 380 c = dc.pop(fn, None)
379 381 if c:
380 382 type, mode, size, time = c
381 383 # check the common case first
382 384 if type == 'n':
383 385 if size != s.st_size or (mode ^ s.st_mode) & 0100:
384 386 checkappend(modified, fn)
385 387 elif time != s.st_mtime:
386 388 checkappend(lookup, fn)
387 389 elif type == 'm':
388 390 checkappend(modified, fn)
389 391 elif type == 'a':
390 392 checkappend(added, fn)
391 393 elif type == 'r':
392 394 checkappend(unknown, fn)
393 395 elif not self.ignore(fn) and match(fn):
394 396 unknown.append(fn)
395 397 # return false because we've already handled all cases above.
396 398 # there's no need for the walking code to process the file
397 399 # any further.
398 400 return False
399 401
400 402 # because our statmatch always returns false, self.walk will only
401 403 # return files in the dirstate map that are not present in the FS.
402 404 # But, we still need to iterate through the results to force the
403 405 # walk to complete
404 406 for src, fn in self.walkhelper(files, statmatch, dc):
405 407 pass
406 408
407 409 # there may be patterns in the .hgignore file that prevent us
408 410 # from examining entire directories in the dirstate map, so we
409 411 # go back and explicitly examine any matching files we've
410 412 # ignored
411 413 unexamined = [fn for fn in dc.iterkeys()
412 414 if self.ignore(fn) and match(fn)]
413 415
414 416 for src, fn in self.walkhelper(unexamined, statmatch, dc):
415 417 pass
416 418
417 419 # anything left in dc didn't exist in the filesystem
418 420 for fn, c in dc.iteritems():
419 421 if not match(fn): continue
420 422 if c[0] == 'r':
421 423 removed.append(fn)
422 424 else:
423 425 deleted.append(fn)
424 426 return (lookup, modified, added, removed + deleted, unknown)
General Comments 0
You need to be logged in to leave comments. Login now