##// END OF EJS Templates
Fix dirstate.changes for ignored directories....
Bryan O'Sullivan -
r1276:25e5b108 default
parent child Browse files
Show More
@@ -1,408 +1,419
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 self.markdirty()
120 self.markdirty()
121 self.pl = p1, p2
121 self.pl = p1, p2
122
122
123 def state(self, key):
123 def state(self, key):
124 try:
124 try:
125 return self[key][0]
125 return self[key][0]
126 except KeyError:
126 except KeyError:
127 return "?"
127 return "?"
128
128
129 def read(self):
129 def read(self):
130 if self.map is not None: return self.map
130 if self.map is not None: return self.map
131
131
132 self.map = {}
132 self.map = {}
133 self.pl = [nullid, nullid]
133 self.pl = [nullid, nullid]
134 try:
134 try:
135 st = self.opener("dirstate").read()
135 st = self.opener("dirstate").read()
136 if not st: return
136 if not st: return
137 except: return
137 except: return
138
138
139 self.pl = [st[:20], st[20: 40]]
139 self.pl = [st[:20], st[20: 40]]
140
140
141 pos = 40
141 pos = 40
142 while pos < len(st):
142 while pos < len(st):
143 e = struct.unpack(">cllll", st[pos:pos+17])
143 e = struct.unpack(">cllll", st[pos:pos+17])
144 l = e[4]
144 l = e[4]
145 pos += 17
145 pos += 17
146 f = st[pos:pos + l]
146 f = st[pos:pos + l]
147 if '\0' in f:
147 if '\0' in f:
148 f, c = f.split('\0')
148 f, c = f.split('\0')
149 self.copies[f] = c
149 self.copies[f] = c
150 self.map[f] = e[:4]
150 self.map[f] = e[:4]
151 pos += l
151 pos += l
152
152
153 def copy(self, source, dest):
153 def copy(self, source, dest):
154 self.read()
154 self.read()
155 self.markdirty()
155 self.markdirty()
156 self.copies[dest] = source
156 self.copies[dest] = source
157
157
158 def copied(self, file):
158 def copied(self, file):
159 return self.copies.get(file, None)
159 return self.copies.get(file, None)
160
160
161 def update(self, files, state, **kw):
161 def update(self, files, state, **kw):
162 ''' current states:
162 ''' current states:
163 n normal
163 n normal
164 m needs merging
164 m needs merging
165 r marked for removal
165 r marked for removal
166 a marked for addition'''
166 a marked for addition'''
167
167
168 if not files: return
168 if not files: return
169 self.read()
169 self.read()
170 self.markdirty()
170 self.markdirty()
171 for f in files:
171 for f in files:
172 if state == "r":
172 if state == "r":
173 self.map[f] = ('r', 0, 0, 0)
173 self.map[f] = ('r', 0, 0, 0)
174 else:
174 else:
175 s = os.lstat(os.path.join(self.root, f))
175 s = os.lstat(os.path.join(self.root, f))
176 st_size = kw.get('st_size', s.st_size)
176 st_size = kw.get('st_size', s.st_size)
177 st_mtime = kw.get('st_mtime', s.st_mtime)
177 st_mtime = kw.get('st_mtime', s.st_mtime)
178 self.map[f] = (state, s.st_mode, st_size, st_mtime)
178 self.map[f] = (state, s.st_mode, st_size, st_mtime)
179 if self.copies.has_key(f):
179 if self.copies.has_key(f):
180 del self.copies[f]
180 del self.copies[f]
181
181
182 def forget(self, files):
182 def forget(self, files):
183 if not files: return
183 if not files: return
184 self.read()
184 self.read()
185 self.markdirty()
185 self.markdirty()
186 for f in files:
186 for f in files:
187 try:
187 try:
188 del self.map[f]
188 del self.map[f]
189 except KeyError:
189 except KeyError:
190 self.ui.warn("not in dirstate: %s!\n" % f)
190 self.ui.warn("not in dirstate: %s!\n" % f)
191 pass
191 pass
192
192
193 def clear(self):
193 def clear(self):
194 self.map = {}
194 self.map = {}
195 self.markdirty()
195 self.markdirty()
196
196
197 def write(self):
197 def write(self):
198 st = self.opener("dirstate", "w")
198 st = self.opener("dirstate", "w")
199 st.write("".join(self.pl))
199 st.write("".join(self.pl))
200 for f, e in self.map.items():
200 for f, e in self.map.items():
201 c = self.copied(f)
201 c = self.copied(f)
202 if c:
202 if c:
203 f = f + "\0" + c
203 f = f + "\0" + c
204 e = struct.pack(">cllll", e[0], e[1], e[2], e[3], len(f))
204 e = struct.pack(">cllll", e[0], e[1], e[2], e[3], len(f))
205 st.write(e + f)
205 st.write(e + f)
206 self.dirty = 0
206 self.dirty = 0
207
207
208 def filterfiles(self, files):
208 def filterfiles(self, files):
209 ret = {}
209 ret = {}
210 unknown = []
210 unknown = []
211
211
212 for x in files:
212 for x in files:
213 if x is '.':
213 if x is '.':
214 return self.map.copy()
214 return self.map.copy()
215 if x not in self.map:
215 if x not in self.map:
216 unknown.append(x)
216 unknown.append(x)
217 else:
217 else:
218 ret[x] = self.map[x]
218 ret[x] = self.map[x]
219
219
220 if not unknown:
220 if not unknown:
221 return ret
221 return ret
222
222
223 b = self.map.keys()
223 b = self.map.keys()
224 b.sort()
224 b.sort()
225 blen = len(b)
225 blen = len(b)
226
226
227 for x in unknown:
227 for x in unknown:
228 bs = bisect.bisect(b, x)
228 bs = bisect.bisect(b, x)
229 if bs != 0 and b[bs-1] == x:
229 if bs != 0 and b[bs-1] == x:
230 ret[x] = self.map[x]
230 ret[x] = self.map[x]
231 continue
231 continue
232 while bs < blen:
232 while bs < blen:
233 s = b[bs]
233 s = b[bs]
234 if len(s) > len(x) and s.startswith(x) and s[len(x)] == '/':
234 if len(s) > len(x) and s.startswith(x) and s[len(x)] == '/':
235 ret[s] = self.map[s]
235 ret[s] = self.map[s]
236 else:
236 else:
237 break
237 break
238 bs += 1
238 bs += 1
239 return ret
239 return ret
240
240
241 def walk(self, files=None, match=util.always, dc=None):
241 def walk(self, files=None, match=util.always, dc=None):
242 self.read()
242 self.read()
243
243
244 # walk all files by default
244 # walk all files by default
245 if not files:
245 if not files:
246 files = [self.root]
246 files = [self.root]
247 if not dc:
247 if not dc:
248 dc = self.map.copy()
248 dc = self.map.copy()
249 elif not dc:
249 elif not dc:
250 dc = self.filterfiles(files)
250 dc = self.filterfiles(files)
251
251
252 def statmatch(file, stat):
252 def statmatch(file, stat):
253 file = util.pconvert(file)
253 file = util.pconvert(file)
254 if file not in dc and self.ignore(file):
254 if file not in dc and self.ignore(file):
255 return False
255 return False
256 return match(file)
256 return match(file)
257
257
258 return self.walkhelper(files=files, statmatch=statmatch, dc=dc)
258 return self.walkhelper(files=files, statmatch=statmatch, dc=dc)
259
259
260 # walk recursively through the directory tree, finding all files
260 # walk recursively through the directory tree, finding all files
261 # matched by the statmatch function
261 # matched by the statmatch function
262 #
262 #
263 # results are yielded in a tuple (src, filename), where src is one of:
263 # results are yielded in a tuple (src, filename), where src is one of:
264 # 'f' the file was found in the directory tree
264 # 'f' the file was found in the directory tree
265 # 'm' the file was only in the dirstate and not in the tree
265 # 'm' the file was only in the dirstate and not in the tree
266 #
266 #
267 # dc is an optional arg for the current dirstate. dc is not modified
267 # 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.
268 # directly by this function, but might be modified by your statmatch call.
269 #
269 #
270 def walkhelper(self, files, statmatch, dc):
270 def walkhelper(self, files, statmatch, dc):
271 # recursion free walker, faster than os.walk.
271 # recursion free walker, faster than os.walk.
272 def findfiles(s):
272 def findfiles(s):
273 retfiles = []
273 retfiles = []
274 work = [s]
274 work = [s]
275 while work:
275 while work:
276 top = work.pop()
276 top = work.pop()
277 names = os.listdir(top)
277 names = os.listdir(top)
278 names.sort()
278 names.sort()
279 # nd is the top of the repository dir tree
279 # nd is the top of the repository dir tree
280 nd = util.normpath(top[len(self.root) + 1:])
280 nd = util.normpath(top[len(self.root) + 1:])
281 if nd == '.': nd = ''
281 if nd == '.': nd = ''
282 for f in names:
282 for f in names:
283 np = os.path.join(nd, f)
283 np = os.path.join(nd, f)
284 if seen(np):
284 if seen(np):
285 continue
285 continue
286 p = os.path.join(top, f)
286 p = os.path.join(top, f)
287 # don't trip over symlinks
287 # don't trip over symlinks
288 st = os.lstat(p)
288 st = os.lstat(p)
289 if stat.S_ISDIR(st.st_mode):
289 if stat.S_ISDIR(st.st_mode):
290 ds = os.path.join(nd, f +'/')
290 ds = os.path.join(nd, f +'/')
291 if statmatch(ds, st):
291 if statmatch(ds, st):
292 work.append(p)
292 work.append(p)
293 else:
293 else:
294 if statmatch(np, st):
294 if statmatch(np, st):
295 yield util.pconvert(np)
295 yield util.pconvert(np)
296
296
297 known = {'.hg': 1}
297 known = {'.hg': 1}
298 def seen(fn):
298 def seen(fn):
299 if fn in known: return True
299 if fn in known: return True
300 known[fn] = 1
300 known[fn] = 1
301
301
302 # step one, find all files that match our criteria
302 # step one, find all files that match our criteria
303 files.sort()
303 files.sort()
304 for ff in util.unique(files):
304 for ff in util.unique(files):
305 f = os.path.join(self.root, ff)
305 f = os.path.join(self.root, ff)
306 try:
306 try:
307 st = os.lstat(f)
307 st = os.lstat(f)
308 except OSError, inst:
308 except OSError, inst:
309 if ff not in dc: self.ui.warn('%s: %s\n' % (
309 if ff not in dc: self.ui.warn('%s: %s\n' % (
310 util.pathto(self.getcwd(), ff),
310 util.pathto(self.getcwd(), ff),
311 inst.strerror))
311 inst.strerror))
312 continue
312 continue
313 if stat.S_ISDIR(st.st_mode):
313 if stat.S_ISDIR(st.st_mode):
314 sorted = [ x for x in findfiles(f) ]
314 sorted = [ x for x in findfiles(f) ]
315 sorted.sort()
315 sorted.sort()
316 for fl in sorted:
316 for fl in sorted:
317 yield 'f', fl
317 yield 'f', fl
318 elif stat.S_ISREG(st.st_mode):
318 elif stat.S_ISREG(st.st_mode):
319 ff = util.normpath(ff)
319 ff = util.normpath(ff)
320 if seen(ff):
320 if seen(ff):
321 continue
321 continue
322 found = False
322 found = False
323 self.blockignore = True
323 self.blockignore = True
324 if statmatch(ff, st):
324 if statmatch(ff, st):
325 found = True
325 found = True
326 self.blockignore = False
326 self.blockignore = False
327 if found:
327 if found:
328 yield 'f', ff
328 yield 'f', ff
329 else:
329 else:
330 kind = 'unknown'
330 kind = 'unknown'
331 if stat.S_ISCHR(st.st_mode): kind = 'character device'
331 if stat.S_ISCHR(st.st_mode): kind = 'character device'
332 elif stat.S_ISBLK(st.st_mode): kind = 'block device'
332 elif stat.S_ISBLK(st.st_mode): kind = 'block device'
333 elif stat.S_ISFIFO(st.st_mode): kind = 'fifo'
333 elif stat.S_ISFIFO(st.st_mode): kind = 'fifo'
334 elif stat.S_ISLNK(st.st_mode): kind = 'symbolic link'
334 elif stat.S_ISLNK(st.st_mode): kind = 'symbolic link'
335 elif stat.S_ISSOCK(st.st_mode): kind = 'socket'
335 elif stat.S_ISSOCK(st.st_mode): kind = 'socket'
336 self.ui.warn('%s: unsupported file type (type is %s)\n' % (
336 self.ui.warn('%s: unsupported file type (type is %s)\n' % (
337 util.pathto(self.getcwd(), ff),
337 util.pathto(self.getcwd(), ff),
338 kind))
338 kind))
339
339
340 # step two run through anything left in the dc hash and yield
340 # step two run through anything left in the dc hash and yield
341 # if we haven't already seen it
341 # if we haven't already seen it
342 ks = dc.keys()
342 ks = dc.keys()
343 ks.sort()
343 ks.sort()
344 for k in ks:
344 for k in ks:
345 if not seen(k) and (statmatch(k, None)):
345 if not seen(k) and (statmatch(k, None)):
346 yield 'm', k
346 yield 'm', k
347
347
348 def changes(self, files=None, match=util.always):
348 def changes(self, files=None, match=util.always):
349 self.read()
349 self.read()
350 if not files:
350 if not files:
351 files = [self.root]
351 files = [self.root]
352 dc = self.map.copy()
352 dc = self.map.copy()
353 else:
353 else:
354 dc = self.filterfiles(files)
354 dc = self.filterfiles(files)
355 lookup, modified, added, unknown = [], [], [], []
355 lookup, modified, added, unknown = [], [], [], []
356 removed, deleted = [], []
356 removed, deleted = [], []
357
357
358 # statmatch function to eliminate entries from the dirstate copy
358 # statmatch function to eliminate entries from the dirstate copy
359 # and put files into the appropriate array. This gets passed
359 # and put files into the appropriate array. This gets passed
360 # to the walking code
360 # to the walking code
361 def statmatch(fn, s):
361 def statmatch(fn, s):
362 fn = util.pconvert(fn)
362 fn = util.pconvert(fn)
363 def checkappend(l, fn):
363 def checkappend(l, fn):
364 if match is util.always or match(fn):
364 if match is util.always or match(fn):
365 l.append(fn)
365 l.append(fn)
366
366
367 if not s or stat.S_ISDIR(s.st_mode):
367 if not s or stat.S_ISDIR(s.st_mode):
368 if self.ignore(fn): return False
368 if self.ignore(fn): return False
369 return match(fn)
369 return match(fn)
370
370
371 if not stat.S_ISREG(s.st_mode):
371 if not stat.S_ISREG(s.st_mode):
372 return False
372 return False
373 c = dc.pop(fn, None)
373 c = dc.pop(fn, None)
374 if c:
374 if c:
375 type, mode, size, time = c
375 type, mode, size, time = c
376 # check the common case first
376 # check the common case first
377 if type == 'n':
377 if type == 'n':
378 if size != s.st_size or (mode ^ s.st_mode) & 0100:
378 if size != s.st_size or (mode ^ s.st_mode) & 0100:
379 checkappend(modified, fn)
379 checkappend(modified, fn)
380 elif time != s.st_mtime:
380 elif time != s.st_mtime:
381 checkappend(lookup, fn)
381 checkappend(lookup, fn)
382 elif type == 'm':
382 elif type == 'm':
383 checkappend(modified, fn)
383 checkappend(modified, fn)
384 elif type == 'a':
384 elif type == 'a':
385 checkappend(added, fn)
385 checkappend(added, fn)
386 elif type == 'r':
386 elif type == 'r':
387 checkappend(unknown, fn)
387 checkappend(unknown, fn)
388 elif not self.ignore(fn) and match(fn):
388 elif not self.ignore(fn) and match(fn):
389 unknown.append(fn)
389 unknown.append(fn)
390 # return false because we've already handled all cases above.
390 # return false because we've already handled all cases above.
391 # there's no need for the walking code to process the file
391 # there's no need for the walking code to process the file
392 # any further.
392 # any further.
393 return False
393 return False
394
394
395 # because our statmatch always returns false, self.walk will only
395 # because our statmatch always returns false, self.walk will only
396 # return files in the dirstate map that are not present in the FS.
396 # return files in the dirstate map that are not present in the FS.
397 # But, we still need to iterate through the results to force the
397 # But, we still need to iterate through the results to force the
398 # walk to complete
398 # walk to complete
399 for src, fn in self.walkhelper(files, statmatch, dc):
399 for src, fn in self.walkhelper(files, statmatch, dc):
400 pass
400 pass
401
401
402 # there may be patterns in the .hgignore file that prevent us
403 # from examining entire directories in the dirstate map, so we
404 # go back and explicitly examine any matching files we've
405 # ignored
406 unexamined = [fn for fn in dc.iterkeys()
407 if self.ignore(fn) and match(fn)]
408
409 for src, fn in self.walkhelper(unexamined, statmatch, dc):
410 pass
411
402 # anything left in dc didn't exist in the filesystem
412 # anything left in dc didn't exist in the filesystem
403 for fn, c in [(fn, c) for fn, c in dc.items() if match(fn)]:
413 for fn, c in dc.iteritems():
414 if not match(fn): continue
404 if c[0] == 'r':
415 if c[0] == 'r':
405 removed.append(fn)
416 removed.append(fn)
406 else:
417 else:
407 deleted.append(fn)
418 deleted.append(fn)
408 return (lookup, modified, added, removed + deleted, unknown)
419 return (lookup, modified, added, removed + deleted, unknown)
General Comments 0
You need to be logged in to leave comments. Login now