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