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