##// END OF EJS Templates
Fix issue 562: .hgignore requires newline at end....
Patrick Mezard -
r4440:2d32e3ae default
parent child Browse files
Show More
@@ -1,550 +1,552
1 1 """
2 2 dirstate.py - working directory tracking for mercurial
3 3
4 4 Copyright 2005, 2006 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 from node import *
11 11 from i18n import gettext as _
12 12 from demandload import *
13 13 demandload(globals(), "struct os time bisect stat strutil util re errno")
14 14
15 15 class dirstate(object):
16 16 format = ">cllll"
17 17
18 18 def __init__(self, opener, ui, root):
19 19 self.opener = opener
20 20 self.root = root
21 21 self.dirty = 0
22 22 self.ui = ui
23 23 self.map = None
24 24 self.pl = None
25 25 self.dirs = None
26 26 self.copymap = {}
27 27 self.ignorefunc = None
28 28 self._branch = None
29 29
30 30 def wjoin(self, f):
31 31 return os.path.join(self.root, f)
32 32
33 33 def getcwd(self):
34 34 cwd = os.getcwd()
35 35 if cwd == self.root: return ''
36 36 # self.root ends with a path separator if self.root is '/' or 'C:\'
37 37 rootsep = self.root
38 38 if not rootsep.endswith(os.sep):
39 39 rootsep += os.sep
40 40 if cwd.startswith(rootsep):
41 41 return cwd[len(rootsep):]
42 42 else:
43 43 # we're outside the repo. return an absolute path.
44 44 return cwd
45 45
46 46 def hgignore(self):
47 47 '''return the contents of .hgignore files as a list of patterns.
48 48
49 49 the files parsed for patterns include:
50 50 .hgignore in the repository root
51 51 any additional files specified in the [ui] section of ~/.hgrc
52 52
53 53 trailing white space is dropped.
54 54 the escape character is backslash.
55 55 comments start with #.
56 56 empty lines are skipped.
57 57
58 58 lines can be of the following formats:
59 59
60 60 syntax: regexp # defaults following lines to non-rooted regexps
61 61 syntax: glob # defaults following lines to non-rooted globs
62 62 re:pattern # non-rooted regular expression
63 63 glob:pattern # non-rooted glob
64 64 pattern # pattern of the current default type'''
65 65 syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:'}
66 66 def parselines(fp):
67 67 for line in fp:
68 if not line.endswith('\n'):
69 line += '\n'
68 70 escape = False
69 71 for i in xrange(len(line)):
70 72 if escape: escape = False
71 73 elif line[i] == '\\': escape = True
72 74 elif line[i] == '#': break
73 75 line = line[:i].rstrip()
74 76 if line: yield line
75 77 repoignore = self.wjoin('.hgignore')
76 78 files = [repoignore]
77 79 files.extend(self.ui.hgignorefiles())
78 80 pats = {}
79 81 for f in files:
80 82 try:
81 83 pats[f] = []
82 84 fp = open(f)
83 85 syntax = 'relre:'
84 86 for line in parselines(fp):
85 87 if line.startswith('syntax:'):
86 88 s = line[7:].strip()
87 89 try:
88 90 syntax = syntaxes[s]
89 91 except KeyError:
90 92 self.ui.warn(_("%s: ignoring invalid "
91 93 "syntax '%s'\n") % (f, s))
92 94 continue
93 95 pat = syntax + line
94 96 for s in syntaxes.values():
95 97 if line.startswith(s):
96 98 pat = line
97 99 break
98 100 pats[f].append(pat)
99 101 except IOError, inst:
100 102 if f != repoignore:
101 103 self.ui.warn(_("skipping unreadable ignore file"
102 104 " '%s': %s\n") % (f, inst.strerror))
103 105 return pats
104 106
105 107 def ignore(self, fn):
106 108 '''default match function used by dirstate and
107 109 localrepository. this honours the repository .hgignore file
108 110 and any other files specified in the [ui] section of .hgrc.'''
109 111 if not self.ignorefunc:
110 112 ignore = self.hgignore()
111 113 allpats = []
112 114 [allpats.extend(patlist) for patlist in ignore.values()]
113 115 if allpats:
114 116 try:
115 117 files, self.ignorefunc, anypats = (
116 118 util.matcher(self.root, inc=allpats, src='.hgignore'))
117 119 except util.Abort:
118 120 # Re-raise an exception where the src is the right file
119 121 for f, patlist in ignore.items():
120 122 files, self.ignorefunc, anypats = (
121 123 util.matcher(self.root, inc=patlist, src=f))
122 124 else:
123 125 self.ignorefunc = util.never
124 126 return self.ignorefunc(fn)
125 127
126 128 def __del__(self):
127 129 if self.dirty:
128 130 self.write()
129 131
130 132 def __getitem__(self, key):
131 133 try:
132 134 return self.map[key]
133 135 except TypeError:
134 136 self.lazyread()
135 137 return self[key]
136 138
137 139 def __contains__(self, key):
138 140 self.lazyread()
139 141 return key in self.map
140 142
141 143 def parents(self):
142 144 self.lazyread()
143 145 return self.pl
144 146
145 147 def branch(self):
146 148 if not self._branch:
147 149 try:
148 150 self._branch = self.opener("branch").read().strip()\
149 151 or "default"
150 152 except IOError:
151 153 self._branch = "default"
152 154 return self._branch
153 155
154 156 def markdirty(self):
155 157 if not self.dirty:
156 158 self.dirty = 1
157 159
158 160 def setparents(self, p1, p2=nullid):
159 161 self.lazyread()
160 162 self.markdirty()
161 163 self.pl = p1, p2
162 164
163 165 def setbranch(self, branch):
164 166 self._branch = branch
165 167 self.opener("branch", "w").write(branch + '\n')
166 168
167 169 def state(self, key):
168 170 try:
169 171 return self[key][0]
170 172 except KeyError:
171 173 return "?"
172 174
173 175 def lazyread(self):
174 176 if self.map is None:
175 177 self.read()
176 178
177 179 def parse(self, st):
178 180 self.pl = [st[:20], st[20: 40]]
179 181
180 182 # deref fields so they will be local in loop
181 183 map = self.map
182 184 copymap = self.copymap
183 185 format = self.format
184 186 unpack = struct.unpack
185 187
186 188 pos = 40
187 189 e_size = struct.calcsize(format)
188 190
189 191 while pos < len(st):
190 192 newpos = pos + e_size
191 193 e = unpack(format, st[pos:newpos])
192 194 l = e[4]
193 195 pos = newpos
194 196 newpos = pos + l
195 197 f = st[pos:newpos]
196 198 if '\0' in f:
197 199 f, c = f.split('\0')
198 200 copymap[f] = c
199 201 map[f] = e[:4]
200 202 pos = newpos
201 203
202 204 def read(self):
203 205 self.map = {}
204 206 self.pl = [nullid, nullid]
205 207 try:
206 208 st = self.opener("dirstate").read()
207 209 if st:
208 210 self.parse(st)
209 211 except IOError, err:
210 212 if err.errno != errno.ENOENT: raise
211 213
212 214 def copy(self, source, dest):
213 215 self.lazyread()
214 216 self.markdirty()
215 217 self.copymap[dest] = source
216 218
217 219 def copied(self, file):
218 220 return self.copymap.get(file, None)
219 221
220 222 def copies(self):
221 223 return self.copymap
222 224
223 225 def initdirs(self):
224 226 if self.dirs is None:
225 227 self.dirs = {}
226 228 for f in self.map:
227 229 self.updatedirs(f, 1)
228 230
229 231 def updatedirs(self, path, delta):
230 232 if self.dirs is not None:
231 233 for c in strutil.findall(path, '/'):
232 234 pc = path[:c]
233 235 self.dirs.setdefault(pc, 0)
234 236 self.dirs[pc] += delta
235 237
236 238 def checkinterfering(self, files):
237 239 def prefixes(f):
238 240 for c in strutil.rfindall(f, '/'):
239 241 yield f[:c]
240 242 self.lazyread()
241 243 self.initdirs()
242 244 seendirs = {}
243 245 for f in files:
244 246 # shadows
245 247 if self.dirs.get(f):
246 248 raise util.Abort(_('directory named %r already in dirstate') %
247 249 f)
248 250 for d in prefixes(f):
249 251 if d in seendirs:
250 252 break
251 253 if d in self.map:
252 254 raise util.Abort(_('file named %r already in dirstate') %
253 255 d)
254 256 seendirs[d] = True
255 257 # disallowed
256 258 if '\r' in f or '\n' in f:
257 259 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames"))
258 260
259 261 def update(self, files, state, **kw):
260 262 ''' current states:
261 263 n normal
262 264 m needs merging
263 265 r marked for removal
264 266 a marked for addition'''
265 267
266 268 if not files: return
267 269 self.lazyread()
268 270 self.markdirty()
269 271 if state == "a":
270 272 self.initdirs()
271 273 self.checkinterfering(files)
272 274 for f in files:
273 275 if state == "r":
274 276 self.map[f] = ('r', 0, 0, 0)
275 277 self.updatedirs(f, -1)
276 278 else:
277 279 if state == "a":
278 280 self.updatedirs(f, 1)
279 281 s = os.lstat(self.wjoin(f))
280 282 st_size = kw.get('st_size', s.st_size)
281 283 st_mtime = kw.get('st_mtime', s.st_mtime)
282 284 self.map[f] = (state, s.st_mode, st_size, st_mtime)
283 285 if self.copymap.has_key(f):
284 286 del self.copymap[f]
285 287
286 288 def forget(self, files):
287 289 if not files: return
288 290 self.lazyread()
289 291 self.markdirty()
290 292 self.initdirs()
291 293 for f in files:
292 294 try:
293 295 del self.map[f]
294 296 self.updatedirs(f, -1)
295 297 except KeyError:
296 298 self.ui.warn(_("not in dirstate: %s!\n") % f)
297 299 pass
298 300
299 301 def clear(self):
300 302 self.map = {}
301 303 self.copymap = {}
302 304 self.dirs = None
303 305 self.markdirty()
304 306
305 307 def rebuild(self, parent, files):
306 308 self.clear()
307 309 for f in files:
308 310 if files.execf(f):
309 311 self.map[f] = ('n', 0777, -1, 0)
310 312 else:
311 313 self.map[f] = ('n', 0666, -1, 0)
312 314 self.pl = (parent, nullid)
313 315 self.markdirty()
314 316
315 317 def write(self):
316 318 if not self.dirty:
317 319 return
318 320 st = self.opener("dirstate", "w", atomictemp=True)
319 321 st.write("".join(self.pl))
320 322 for f, e in self.map.items():
321 323 c = self.copied(f)
322 324 if c:
323 325 f = f + "\0" + c
324 326 e = struct.pack(self.format, e[0], e[1], e[2], e[3], len(f))
325 327 st.write(e + f)
326 328 st.rename()
327 329 self.dirty = 0
328 330
329 331 def filterfiles(self, files):
330 332 ret = {}
331 333 unknown = []
332 334
333 335 for x in files:
334 336 if x == '.':
335 337 return self.map.copy()
336 338 if x not in self.map:
337 339 unknown.append(x)
338 340 else:
339 341 ret[x] = self.map[x]
340 342
341 343 if not unknown:
342 344 return ret
343 345
344 346 b = self.map.keys()
345 347 b.sort()
346 348 blen = len(b)
347 349
348 350 for x in unknown:
349 351 bs = bisect.bisect(b, "%s%s" % (x, '/'))
350 352 while bs < blen:
351 353 s = b[bs]
352 354 if len(s) > len(x) and s.startswith(x):
353 355 ret[s] = self.map[s]
354 356 else:
355 357 break
356 358 bs += 1
357 359 return ret
358 360
359 361 def supported_type(self, f, st, verbose=False):
360 362 if stat.S_ISREG(st.st_mode):
361 363 return True
362 364 if verbose:
363 365 kind = 'unknown'
364 366 if stat.S_ISCHR(st.st_mode): kind = _('character device')
365 367 elif stat.S_ISBLK(st.st_mode): kind = _('block device')
366 368 elif stat.S_ISFIFO(st.st_mode): kind = _('fifo')
367 369 elif stat.S_ISLNK(st.st_mode): kind = _('symbolic link')
368 370 elif stat.S_ISSOCK(st.st_mode): kind = _('socket')
369 371 elif stat.S_ISDIR(st.st_mode): kind = _('directory')
370 372 self.ui.warn(_('%s: unsupported file type (type is %s)\n') % (
371 373 util.pathto(self.root, self.getcwd(), f),
372 374 kind))
373 375 return False
374 376
375 377 def walk(self, files=None, match=util.always, badmatch=None):
376 378 # filter out the stat
377 379 for src, f, st in self.statwalk(files, match, badmatch=badmatch):
378 380 yield src, f
379 381
380 382 def statwalk(self, files=None, match=util.always, ignored=False,
381 383 badmatch=None):
382 384 '''
383 385 walk recursively through the directory tree, finding all files
384 386 matched by the match function
385 387
386 388 results are yielded in a tuple (src, filename, st), where src
387 389 is one of:
388 390 'f' the file was found in the directory tree
389 391 'm' the file was only in the dirstate and not in the tree
390 392 'b' file was not found and matched badmatch
391 393
392 394 and st is the stat result if the file was found in the directory.
393 395 '''
394 396 self.lazyread()
395 397
396 398 # walk all files by default
397 399 if not files:
398 400 files = ['.']
399 401 dc = self.map.copy()
400 402 else:
401 403 files = util.unique(files)
402 404 dc = self.filterfiles(files)
403 405
404 406 def imatch(file_):
405 407 if file_ not in dc and self.ignore(file_):
406 408 return False
407 409 return match(file_)
408 410
409 411 if ignored: imatch = match
410 412
411 413 # self.root may end with a path separator when self.root == '/'
412 414 common_prefix_len = len(self.root)
413 415 if not self.root.endswith(os.sep):
414 416 common_prefix_len += 1
415 417 # recursion free walker, faster than os.walk.
416 418 def findfiles(s):
417 419 work = [s]
418 420 while work:
419 421 top = work.pop()
420 422 names = os.listdir(top)
421 423 names.sort()
422 424 # nd is the top of the repository dir tree
423 425 nd = util.normpath(top[common_prefix_len:])
424 426 if nd == '.':
425 427 nd = ''
426 428 else:
427 429 # do not recurse into a repo contained in this
428 430 # one. use bisect to find .hg directory so speed
429 431 # is good on big directory.
430 432 hg = bisect.bisect_left(names, '.hg')
431 433 if hg < len(names) and names[hg] == '.hg':
432 434 if os.path.isdir(os.path.join(top, '.hg')):
433 435 continue
434 436 for f in names:
435 437 np = util.pconvert(os.path.join(nd, f))
436 438 if seen(np):
437 439 continue
438 440 p = os.path.join(top, f)
439 441 # don't trip over symlinks
440 442 st = os.lstat(p)
441 443 if stat.S_ISDIR(st.st_mode):
442 444 ds = util.pconvert(os.path.join(nd, f +'/'))
443 445 if imatch(ds):
444 446 work.append(p)
445 447 if imatch(np) and np in dc:
446 448 yield 'm', np, st
447 449 elif imatch(np):
448 450 if self.supported_type(np, st):
449 451 yield 'f', np, st
450 452 elif np in dc:
451 453 yield 'm', np, st
452 454
453 455 known = {'.hg': 1}
454 456 def seen(fn):
455 457 if fn in known: return True
456 458 known[fn] = 1
457 459
458 460 # step one, find all files that match our criteria
459 461 files.sort()
460 462 for ff in files:
461 463 nf = util.normpath(ff)
462 464 f = self.wjoin(ff)
463 465 try:
464 466 st = os.lstat(f)
465 467 except OSError, inst:
466 468 found = False
467 469 for fn in dc:
468 470 if nf == fn or (fn.startswith(nf) and fn[len(nf)] == '/'):
469 471 found = True
470 472 break
471 473 if not found:
472 474 if inst.errno != errno.ENOENT or not badmatch:
473 475 self.ui.warn('%s: %s\n' % (
474 476 util.pathto(self.root, self.getcwd(), ff),
475 477 inst.strerror))
476 478 elif badmatch and badmatch(ff) and imatch(nf):
477 479 yield 'b', ff, None
478 480 continue
479 481 if stat.S_ISDIR(st.st_mode):
480 482 cmp1 = (lambda x, y: cmp(x[1], y[1]))
481 483 sorted_ = [ x for x in findfiles(f) ]
482 484 sorted_.sort(cmp1)
483 485 for e in sorted_:
484 486 yield e
485 487 else:
486 488 if not seen(nf) and match(nf):
487 489 if self.supported_type(ff, st, verbose=True):
488 490 yield 'f', nf, st
489 491 elif ff in dc:
490 492 yield 'm', nf, st
491 493
492 494 # step two run through anything left in the dc hash and yield
493 495 # if we haven't already seen it
494 496 ks = dc.keys()
495 497 ks.sort()
496 498 for k in ks:
497 499 if not seen(k) and imatch(k):
498 500 yield 'm', k, None
499 501
500 502 def status(self, files=None, match=util.always, list_ignored=False,
501 503 list_clean=False):
502 504 lookup, modified, added, unknown, ignored = [], [], [], [], []
503 505 removed, deleted, clean = [], [], []
504 506
505 507 for src, fn, st in self.statwalk(files, match, ignored=list_ignored):
506 508 try:
507 509 type_, mode, size, time = self[fn]
508 510 except KeyError:
509 511 if list_ignored and self.ignore(fn):
510 512 ignored.append(fn)
511 513 else:
512 514 unknown.append(fn)
513 515 continue
514 516 if src == 'm':
515 517 nonexistent = True
516 518 if not st:
517 519 try:
518 520 st = os.lstat(self.wjoin(fn))
519 521 except OSError, inst:
520 522 if inst.errno != errno.ENOENT:
521 523 raise
522 524 st = None
523 525 # We need to re-check that it is a valid file
524 526 if st and self.supported_type(fn, st):
525 527 nonexistent = False
526 528 # XXX: what to do with file no longer present in the fs
527 529 # who are not removed in the dirstate ?
528 530 if nonexistent and type_ in "nm":
529 531 deleted.append(fn)
530 532 continue
531 533 # check the common case first
532 534 if type_ == 'n':
533 535 if not st:
534 536 st = os.lstat(self.wjoin(fn))
535 537 if size >= 0 and (size != st.st_size
536 538 or (mode ^ st.st_mode) & 0100):
537 539 modified.append(fn)
538 540 elif time != int(st.st_mtime):
539 541 lookup.append(fn)
540 542 elif list_clean:
541 543 clean.append(fn)
542 544 elif type_ == 'm':
543 545 modified.append(fn)
544 546 elif type_ == 'a':
545 547 added.append(fn)
546 548 elif type_ == 'r':
547 549 removed.append(fn)
548 550
549 551 return (lookup, modified, added, removed, deleted, unknown, ignored,
550 552 clean)
General Comments 0
You need to be logged in to leave comments. Login now