##// END OF EJS Templates
dirstate: filecacheify _ignore (issue3278)...
Idan Kamara -
r16202:53e2cd30 2.1.1 stable
parent child Browse files
Show More
@@ -1,743 +1,748 b''
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 import errno
8 8
9 9 from node import nullid
10 10 from i18n import _
11 11 import scmutil, util, ignore, osutil, parsers, encoding
12 12 import struct, os, stat, errno
13 13 import cStringIO
14 14
15 15 _format = ">cllll"
16 16 propertycache = util.propertycache
17 17 filecache = scmutil.filecache
18 18
19 19 class repocache(filecache):
20 20 """filecache for files in .hg/"""
21 21 def join(self, obj, fname):
22 22 return obj._opener.join(fname)
23 23
24 class rootcache(filecache):
25 """filecache for files in the repository root"""
26 def join(self, obj, fname):
27 return obj._join(fname)
28
24 29 def _finddirs(path):
25 30 pos = path.rfind('/')
26 31 while pos != -1:
27 32 yield path[:pos]
28 33 pos = path.rfind('/', 0, pos)
29 34
30 35 def _incdirs(dirs, path):
31 36 for base in _finddirs(path):
32 37 if base in dirs:
33 38 dirs[base] += 1
34 39 return
35 40 dirs[base] = 1
36 41
37 42 def _decdirs(dirs, path):
38 43 for base in _finddirs(path):
39 44 if dirs[base] > 1:
40 45 dirs[base] -= 1
41 46 return
42 47 del dirs[base]
43 48
44 49 class dirstate(object):
45 50
46 51 def __init__(self, opener, ui, root, validate):
47 52 '''Create a new dirstate object.
48 53
49 54 opener is an open()-like callable that can be used to open the
50 55 dirstate file; root is the root of the directory tracked by
51 56 the dirstate.
52 57 '''
53 58 self._opener = opener
54 59 self._validate = validate
55 60 self._root = root
56 61 self._rootdir = os.path.join(root, '')
57 62 self._dirty = False
58 63 self._dirtypl = False
59 64 self._lastnormaltime = 0
60 65 self._ui = ui
61 66 self._filecache = {}
62 67
63 68 @propertycache
64 69 def _map(self):
65 70 '''Return the dirstate contents as a map from filename to
66 71 (state, mode, size, time).'''
67 72 self._read()
68 73 return self._map
69 74
70 75 @propertycache
71 76 def _copymap(self):
72 77 self._read()
73 78 return self._copymap
74 79
75 80 @propertycache
76 81 def _normroot(self):
77 82 return util.normcase(self._root)
78 83
79 84 @propertycache
80 85 def _foldmap(self):
81 86 f = {}
82 87 for name in self._map:
83 88 f[util.normcase(name)] = name
84 89 f['.'] = '.' # prevents useless util.fspath() invocation
85 90 return f
86 91
87 92 @repocache('branch')
88 93 def _branch(self):
89 94 try:
90 95 return self._opener.read("branch").strip() or "default"
91 96 except IOError, inst:
92 97 if inst.errno != errno.ENOENT:
93 98 raise
94 99 return "default"
95 100
96 101 @propertycache
97 102 def _pl(self):
98 103 try:
99 104 fp = self._opener("dirstate")
100 105 st = fp.read(40)
101 106 fp.close()
102 107 l = len(st)
103 108 if l == 40:
104 109 return st[:20], st[20:40]
105 110 elif l > 0 and l < 40:
106 111 raise util.Abort(_('working directory state appears damaged!'))
107 112 except IOError, err:
108 113 if err.errno != errno.ENOENT:
109 114 raise
110 115 return [nullid, nullid]
111 116
112 117 @propertycache
113 118 def _dirs(self):
114 119 dirs = {}
115 120 for f, s in self._map.iteritems():
116 121 if s[0] != 'r':
117 122 _incdirs(dirs, f)
118 123 return dirs
119 124
120 125 def dirs(self):
121 126 return self._dirs
122 127
123 @propertycache
128 @rootcache('.hgignore')
124 129 def _ignore(self):
125 130 files = [self._join('.hgignore')]
126 131 for name, path in self._ui.configitems("ui"):
127 132 if name == 'ignore' or name.startswith('ignore.'):
128 133 files.append(util.expandpath(path))
129 134 return ignore.ignore(self._root, files, self._ui.warn)
130 135
131 136 @propertycache
132 137 def _slash(self):
133 138 return self._ui.configbool('ui', 'slash') and os.sep != '/'
134 139
135 140 @propertycache
136 141 def _checklink(self):
137 142 return util.checklink(self._root)
138 143
139 144 @propertycache
140 145 def _checkexec(self):
141 146 return util.checkexec(self._root)
142 147
143 148 @propertycache
144 149 def _checkcase(self):
145 150 return not util.checkcase(self._join('.hg'))
146 151
147 152 def _join(self, f):
148 153 # much faster than os.path.join()
149 154 # it's safe because f is always a relative path
150 155 return self._rootdir + f
151 156
152 157 def flagfunc(self, buildfallback):
153 158 if self._checklink and self._checkexec:
154 159 def f(x):
155 160 p = self._join(x)
156 161 if os.path.islink(p):
157 162 return 'l'
158 163 if util.isexec(p):
159 164 return 'x'
160 165 return ''
161 166 return f
162 167
163 168 fallback = buildfallback()
164 169 if self._checklink:
165 170 def f(x):
166 171 if os.path.islink(self._join(x)):
167 172 return 'l'
168 173 if 'x' in fallback(x):
169 174 return 'x'
170 175 return ''
171 176 return f
172 177 if self._checkexec:
173 178 def f(x):
174 179 if 'l' in fallback(x):
175 180 return 'l'
176 181 if util.isexec(self._join(x)):
177 182 return 'x'
178 183 return ''
179 184 return f
180 185 else:
181 186 return fallback
182 187
183 188 def getcwd(self):
184 189 cwd = os.getcwd()
185 190 if cwd == self._root:
186 191 return ''
187 192 # self._root ends with a path separator if self._root is '/' or 'C:\'
188 193 rootsep = self._root
189 194 if not util.endswithsep(rootsep):
190 195 rootsep += os.sep
191 196 if cwd.startswith(rootsep):
192 197 return cwd[len(rootsep):]
193 198 else:
194 199 # we're outside the repo. return an absolute path.
195 200 return cwd
196 201
197 202 def pathto(self, f, cwd=None):
198 203 if cwd is None:
199 204 cwd = self.getcwd()
200 205 path = util.pathto(self._root, cwd, f)
201 206 if self._slash:
202 207 return util.normpath(path)
203 208 return path
204 209
205 210 def __getitem__(self, key):
206 211 '''Return the current state of key (a filename) in the dirstate.
207 212
208 213 States are:
209 214 n normal
210 215 m needs merging
211 216 r marked for removal
212 217 a marked for addition
213 218 ? not tracked
214 219 '''
215 220 return self._map.get(key, ("?",))[0]
216 221
217 222 def __contains__(self, key):
218 223 return key in self._map
219 224
220 225 def __iter__(self):
221 226 for x in sorted(self._map):
222 227 yield x
223 228
224 229 def parents(self):
225 230 return [self._validate(p) for p in self._pl]
226 231
227 232 def p1(self):
228 233 return self._validate(self._pl[0])
229 234
230 235 def p2(self):
231 236 return self._validate(self._pl[1])
232 237
233 238 def branch(self):
234 239 return encoding.tolocal(self._branch)
235 240
236 241 def setparents(self, p1, p2=nullid):
237 242 self._dirty = self._dirtypl = True
238 243 self._pl = p1, p2
239 244
240 245 def setbranch(self, branch):
241 246 if branch in ['tip', '.', 'null']:
242 247 raise util.Abort(_('the name \'%s\' is reserved') % branch)
243 248 self._branch = encoding.fromlocal(branch)
244 249 self._opener.write("branch", self._branch + '\n')
245 250
246 251 def _read(self):
247 252 self._map = {}
248 253 self._copymap = {}
249 254 try:
250 255 st = self._opener.read("dirstate")
251 256 except IOError, err:
252 257 if err.errno != errno.ENOENT:
253 258 raise
254 259 return
255 260 if not st:
256 261 return
257 262
258 263 p = parsers.parse_dirstate(self._map, self._copymap, st)
259 264 if not self._dirtypl:
260 265 self._pl = p
261 266
262 267 def invalidate(self):
263 268 for a in ("_map", "_copymap", "_foldmap", "_branch", "_pl", "_dirs",
264 269 "_ignore"):
265 270 if a in self.__dict__:
266 271 delattr(self, a)
267 272 self._lastnormaltime = 0
268 273 self._dirty = False
269 274
270 275 def copy(self, source, dest):
271 276 """Mark dest as a copy of source. Unmark dest if source is None."""
272 277 if source == dest:
273 278 return
274 279 self._dirty = True
275 280 if source is not None:
276 281 self._copymap[dest] = source
277 282 elif dest in self._copymap:
278 283 del self._copymap[dest]
279 284
280 285 def copied(self, file):
281 286 return self._copymap.get(file, None)
282 287
283 288 def copies(self):
284 289 return self._copymap
285 290
286 291 def _droppath(self, f):
287 292 if self[f] not in "?r" and "_dirs" in self.__dict__:
288 293 _decdirs(self._dirs, f)
289 294
290 295 def _addpath(self, f, check=False):
291 296 oldstate = self[f]
292 297 if check or oldstate == "r":
293 298 scmutil.checkfilename(f)
294 299 if f in self._dirs:
295 300 raise util.Abort(_('directory %r already in dirstate') % f)
296 301 # shadows
297 302 for d in _finddirs(f):
298 303 if d in self._dirs:
299 304 break
300 305 if d in self._map and self[d] != 'r':
301 306 raise util.Abort(
302 307 _('file %r in dirstate clashes with %r') % (d, f))
303 308 if oldstate in "?r" and "_dirs" in self.__dict__:
304 309 _incdirs(self._dirs, f)
305 310
306 311 def normal(self, f):
307 312 '''Mark a file normal and clean.'''
308 313 self._dirty = True
309 314 self._addpath(f)
310 315 s = os.lstat(self._join(f))
311 316 mtime = int(s.st_mtime)
312 317 self._map[f] = ('n', s.st_mode, s.st_size, mtime)
313 318 if f in self._copymap:
314 319 del self._copymap[f]
315 320 if mtime > self._lastnormaltime:
316 321 # Remember the most recent modification timeslot for status(),
317 322 # to make sure we won't miss future size-preserving file content
318 323 # modifications that happen within the same timeslot.
319 324 self._lastnormaltime = mtime
320 325
321 326 def normallookup(self, f):
322 327 '''Mark a file normal, but possibly dirty.'''
323 328 if self._pl[1] != nullid and f in self._map:
324 329 # if there is a merge going on and the file was either
325 330 # in state 'm' (-1) or coming from other parent (-2) before
326 331 # being removed, restore that state.
327 332 entry = self._map[f]
328 333 if entry[0] == 'r' and entry[2] in (-1, -2):
329 334 source = self._copymap.get(f)
330 335 if entry[2] == -1:
331 336 self.merge(f)
332 337 elif entry[2] == -2:
333 338 self.otherparent(f)
334 339 if source:
335 340 self.copy(source, f)
336 341 return
337 342 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
338 343 return
339 344 self._dirty = True
340 345 self._addpath(f)
341 346 self._map[f] = ('n', 0, -1, -1)
342 347 if f in self._copymap:
343 348 del self._copymap[f]
344 349
345 350 def otherparent(self, f):
346 351 '''Mark as coming from the other parent, always dirty.'''
347 352 if self._pl[1] == nullid:
348 353 raise util.Abort(_("setting %r to other parent "
349 354 "only allowed in merges") % f)
350 355 self._dirty = True
351 356 self._addpath(f)
352 357 self._map[f] = ('n', 0, -2, -1)
353 358 if f in self._copymap:
354 359 del self._copymap[f]
355 360
356 361 def add(self, f):
357 362 '''Mark a file added.'''
358 363 self._dirty = True
359 364 self._addpath(f, True)
360 365 self._map[f] = ('a', 0, -1, -1)
361 366 if f in self._copymap:
362 367 del self._copymap[f]
363 368
364 369 def remove(self, f):
365 370 '''Mark a file removed.'''
366 371 self._dirty = True
367 372 self._droppath(f)
368 373 size = 0
369 374 if self._pl[1] != nullid and f in self._map:
370 375 # backup the previous state
371 376 entry = self._map[f]
372 377 if entry[0] == 'm': # merge
373 378 size = -1
374 379 elif entry[0] == 'n' and entry[2] == -2: # other parent
375 380 size = -2
376 381 self._map[f] = ('r', 0, size, 0)
377 382 if size == 0 and f in self._copymap:
378 383 del self._copymap[f]
379 384
380 385 def merge(self, f):
381 386 '''Mark a file merged.'''
382 387 self._dirty = True
383 388 s = os.lstat(self._join(f))
384 389 self._addpath(f)
385 390 self._map[f] = ('m', s.st_mode, s.st_size, int(s.st_mtime))
386 391 if f in self._copymap:
387 392 del self._copymap[f]
388 393
389 394 def drop(self, f):
390 395 '''Drop a file from the dirstate'''
391 396 if f in self._map:
392 397 self._dirty = True
393 398 self._droppath(f)
394 399 del self._map[f]
395 400
396 401 def _normalize(self, path, isknown):
397 402 normed = util.normcase(path)
398 403 folded = self._foldmap.get(normed, None)
399 404 if folded is None:
400 405 if isknown or not os.path.lexists(os.path.join(self._root, path)):
401 406 folded = path
402 407 else:
403 408 folded = self._foldmap.setdefault(normed,
404 409 util.fspath(normed, self._normroot))
405 410 return folded
406 411
407 412 def normalize(self, path, isknown=False):
408 413 '''
409 414 normalize the case of a pathname when on a casefolding filesystem
410 415
411 416 isknown specifies whether the filename came from walking the
412 417 disk, to avoid extra filesystem access
413 418
414 419 The normalized case is determined based on the following precedence:
415 420
416 421 - version of name already stored in the dirstate
417 422 - version of name stored on disk
418 423 - version provided via command arguments
419 424 '''
420 425
421 426 if self._checkcase:
422 427 return self._normalize(path, isknown)
423 428 return path
424 429
425 430 def clear(self):
426 431 self._map = {}
427 432 if "_dirs" in self.__dict__:
428 433 delattr(self, "_dirs")
429 434 self._copymap = {}
430 435 self._pl = [nullid, nullid]
431 436 self._lastnormaltime = 0
432 437 self._dirty = True
433 438
434 439 def rebuild(self, parent, files):
435 440 self.clear()
436 441 for f in files:
437 442 if 'x' in files.flags(f):
438 443 self._map[f] = ('n', 0777, -1, 0)
439 444 else:
440 445 self._map[f] = ('n', 0666, -1, 0)
441 446 self._pl = (parent, nullid)
442 447 self._dirty = True
443 448
444 449 def write(self):
445 450 if not self._dirty:
446 451 return
447 452 st = self._opener("dirstate", "w", atomictemp=True)
448 453
449 454 # use the modification time of the newly created temporary file as the
450 455 # filesystem's notion of 'now'
451 456 now = int(util.fstat(st).st_mtime)
452 457
453 458 cs = cStringIO.StringIO()
454 459 copymap = self._copymap
455 460 pack = struct.pack
456 461 write = cs.write
457 462 write("".join(self._pl))
458 463 for f, e in self._map.iteritems():
459 464 if e[0] == 'n' and e[3] == now:
460 465 # The file was last modified "simultaneously" with the current
461 466 # write to dirstate (i.e. within the same second for file-
462 467 # systems with a granularity of 1 sec). This commonly happens
463 468 # for at least a couple of files on 'update'.
464 469 # The user could change the file without changing its size
465 470 # within the same second. Invalidate the file's stat data in
466 471 # dirstate, forcing future 'status' calls to compare the
467 472 # contents of the file. This prevents mistakenly treating such
468 473 # files as clean.
469 474 e = (e[0], 0, -1, -1) # mark entry as 'unset'
470 475 self._map[f] = e
471 476
472 477 if f in copymap:
473 478 f = "%s\0%s" % (f, copymap[f])
474 479 e = pack(_format, e[0], e[1], e[2], e[3], len(f))
475 480 write(e)
476 481 write(f)
477 482 st.write(cs.getvalue())
478 483 st.close()
479 484 self._lastnormaltime = 0
480 485 self._dirty = self._dirtypl = False
481 486
482 487 def _dirignore(self, f):
483 488 if f == '.':
484 489 return False
485 490 if self._ignore(f):
486 491 return True
487 492 for p in _finddirs(f):
488 493 if self._ignore(p):
489 494 return True
490 495 return False
491 496
492 497 def walk(self, match, subrepos, unknown, ignored):
493 498 '''
494 499 Walk recursively through the directory tree, finding all files
495 500 matched by match.
496 501
497 502 Return a dict mapping filename to stat-like object (either
498 503 mercurial.osutil.stat instance or return value of os.stat()).
499 504 '''
500 505
501 506 def fwarn(f, msg):
502 507 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
503 508 return False
504 509
505 510 def badtype(mode):
506 511 kind = _('unknown')
507 512 if stat.S_ISCHR(mode):
508 513 kind = _('character device')
509 514 elif stat.S_ISBLK(mode):
510 515 kind = _('block device')
511 516 elif stat.S_ISFIFO(mode):
512 517 kind = _('fifo')
513 518 elif stat.S_ISSOCK(mode):
514 519 kind = _('socket')
515 520 elif stat.S_ISDIR(mode):
516 521 kind = _('directory')
517 522 return _('unsupported file type (type is %s)') % kind
518 523
519 524 ignore = self._ignore
520 525 dirignore = self._dirignore
521 526 if ignored:
522 527 ignore = util.never
523 528 dirignore = util.never
524 529 elif not unknown:
525 530 # if unknown and ignored are False, skip step 2
526 531 ignore = util.always
527 532 dirignore = util.always
528 533
529 534 matchfn = match.matchfn
530 535 badfn = match.bad
531 536 dmap = self._map
532 537 normpath = util.normpath
533 538 listdir = osutil.listdir
534 539 lstat = os.lstat
535 540 getkind = stat.S_IFMT
536 541 dirkind = stat.S_IFDIR
537 542 regkind = stat.S_IFREG
538 543 lnkkind = stat.S_IFLNK
539 544 join = self._join
540 545 work = []
541 546 wadd = work.append
542 547
543 548 exact = skipstep3 = False
544 549 if matchfn == match.exact: # match.exact
545 550 exact = True
546 551 dirignore = util.always # skip step 2
547 552 elif match.files() and not match.anypats(): # match.match, no patterns
548 553 skipstep3 = True
549 554
550 555 if self._checkcase:
551 556 normalize = self._normalize
552 557 skipstep3 = False
553 558 else:
554 559 normalize = lambda x, y: x
555 560
556 561 files = sorted(match.files())
557 562 subrepos.sort()
558 563 i, j = 0, 0
559 564 while i < len(files) and j < len(subrepos):
560 565 subpath = subrepos[j] + "/"
561 566 if files[i] < subpath:
562 567 i += 1
563 568 continue
564 569 while i < len(files) and files[i].startswith(subpath):
565 570 del files[i]
566 571 j += 1
567 572
568 573 if not files or '.' in files:
569 574 files = ['']
570 575 results = dict.fromkeys(subrepos)
571 576 results['.hg'] = None
572 577
573 578 # step 1: find all explicit files
574 579 for ff in files:
575 580 nf = normalize(normpath(ff), False)
576 581 if nf in results:
577 582 continue
578 583
579 584 try:
580 585 st = lstat(join(nf))
581 586 kind = getkind(st.st_mode)
582 587 if kind == dirkind:
583 588 skipstep3 = False
584 589 if nf in dmap:
585 590 #file deleted on disk but still in dirstate
586 591 results[nf] = None
587 592 match.dir(nf)
588 593 if not dirignore(nf):
589 594 wadd(nf)
590 595 elif kind == regkind or kind == lnkkind:
591 596 results[nf] = st
592 597 else:
593 598 badfn(ff, badtype(kind))
594 599 if nf in dmap:
595 600 results[nf] = None
596 601 except OSError, inst:
597 602 if nf in dmap: # does it exactly match a file?
598 603 results[nf] = None
599 604 else: # does it match a directory?
600 605 prefix = nf + "/"
601 606 for fn in dmap:
602 607 if fn.startswith(prefix):
603 608 match.dir(nf)
604 609 skipstep3 = False
605 610 break
606 611 else:
607 612 badfn(ff, inst.strerror)
608 613
609 614 # step 2: visit subdirectories
610 615 while work:
611 616 nd = work.pop()
612 617 skip = None
613 618 if nd == '.':
614 619 nd = ''
615 620 else:
616 621 skip = '.hg'
617 622 try:
618 623 entries = listdir(join(nd), stat=True, skip=skip)
619 624 except OSError, inst:
620 625 if inst.errno == errno.EACCES:
621 626 fwarn(nd, inst.strerror)
622 627 continue
623 628 raise
624 629 for f, kind, st in entries:
625 630 nf = normalize(nd and (nd + "/" + f) or f, True)
626 631 if nf not in results:
627 632 if kind == dirkind:
628 633 if not ignore(nf):
629 634 match.dir(nf)
630 635 wadd(nf)
631 636 if nf in dmap and matchfn(nf):
632 637 results[nf] = None
633 638 elif kind == regkind or kind == lnkkind:
634 639 if nf in dmap:
635 640 if matchfn(nf):
636 641 results[nf] = st
637 642 elif matchfn(nf) and not ignore(nf):
638 643 results[nf] = st
639 644 elif nf in dmap and matchfn(nf):
640 645 results[nf] = None
641 646
642 647 # step 3: report unseen items in the dmap hash
643 648 if not skipstep3 and not exact:
644 649 visit = sorted([f for f in dmap if f not in results and matchfn(f)])
645 650 for nf, st in zip(visit, util.statfiles([join(i) for i in visit])):
646 651 if not st is None and not getkind(st.st_mode) in (regkind, lnkkind):
647 652 st = None
648 653 results[nf] = st
649 654 for s in subrepos:
650 655 del results[s]
651 656 del results['.hg']
652 657 return results
653 658
654 659 def status(self, match, subrepos, ignored, clean, unknown):
655 660 '''Determine the status of the working copy relative to the
656 661 dirstate and return a tuple of lists (unsure, modified, added,
657 662 removed, deleted, unknown, ignored, clean), where:
658 663
659 664 unsure:
660 665 files that might have been modified since the dirstate was
661 666 written, but need to be read to be sure (size is the same
662 667 but mtime differs)
663 668 modified:
664 669 files that have definitely been modified since the dirstate
665 670 was written (different size or mode)
666 671 added:
667 672 files that have been explicitly added with hg add
668 673 removed:
669 674 files that have been explicitly removed with hg remove
670 675 deleted:
671 676 files that have been deleted through other means ("missing")
672 677 unknown:
673 678 files not in the dirstate that are not ignored
674 679 ignored:
675 680 files not in the dirstate that are ignored
676 681 (by _dirignore())
677 682 clean:
678 683 files that have definitely not been modified since the
679 684 dirstate was written
680 685 '''
681 686 listignored, listclean, listunknown = ignored, clean, unknown
682 687 lookup, modified, added, unknown, ignored = [], [], [], [], []
683 688 removed, deleted, clean = [], [], []
684 689
685 690 dmap = self._map
686 691 ladd = lookup.append # aka "unsure"
687 692 madd = modified.append
688 693 aadd = added.append
689 694 uadd = unknown.append
690 695 iadd = ignored.append
691 696 radd = removed.append
692 697 dadd = deleted.append
693 698 cadd = clean.append
694 699
695 700 lnkkind = stat.S_IFLNK
696 701
697 702 for fn, st in self.walk(match, subrepos, listunknown,
698 703 listignored).iteritems():
699 704 if fn not in dmap:
700 705 if (listignored or match.exact(fn)) and self._dirignore(fn):
701 706 if listignored:
702 707 iadd(fn)
703 708 elif listunknown:
704 709 uadd(fn)
705 710 continue
706 711
707 712 state, mode, size, time = dmap[fn]
708 713
709 714 if not st and state in "nma":
710 715 dadd(fn)
711 716 elif state == 'n':
712 717 # The "mode & lnkkind != lnkkind or self._checklink"
713 718 # lines are an expansion of "islink => checklink"
714 719 # where islink means "is this a link?" and checklink
715 720 # means "can we check links?".
716 721 mtime = int(st.st_mtime)
717 722 if (size >= 0 and
718 723 (size != st.st_size
719 724 or ((mode ^ st.st_mode) & 0100 and self._checkexec))
720 725 and (mode & lnkkind != lnkkind or self._checklink)
721 726 or size == -2 # other parent
722 727 or fn in self._copymap):
723 728 madd(fn)
724 729 elif (mtime != time
725 730 and (mode & lnkkind != lnkkind or self._checklink)):
726 731 ladd(fn)
727 732 elif mtime == self._lastnormaltime:
728 733 # fn may have been changed in the same timeslot without
729 734 # changing its size. This can happen if we quickly do
730 735 # multiple commits in a single transaction.
731 736 # Force lookup, so we don't miss such a racy file change.
732 737 ladd(fn)
733 738 elif listclean:
734 739 cadd(fn)
735 740 elif state == 'm':
736 741 madd(fn)
737 742 elif state == 'a':
738 743 aadd(fn)
739 744 elif state == 'r':
740 745 radd(fn)
741 746
742 747 return (lookup, modified, added, removed, deleted, unknown, ignored,
743 748 clean)
@@ -1,242 +1,257 b''
1 1 import sys, os, struct, subprocess, cStringIO, re, shutil
2 2
3 3 def connect(path=None):
4 4 cmdline = ['hg', 'serve', '--cmdserver', 'pipe']
5 5 if path:
6 6 cmdline += ['-R', path]
7 7
8 8 server = subprocess.Popen(cmdline, stdin=subprocess.PIPE,
9 9 stdout=subprocess.PIPE)
10 10
11 11 return server
12 12
13 13 def writeblock(server, data):
14 14 server.stdin.write(struct.pack('>I', len(data)))
15 15 server.stdin.write(data)
16 16 server.stdin.flush()
17 17
18 18 def readchannel(server):
19 19 data = server.stdout.read(5)
20 20 if not data:
21 21 raise EOFError()
22 22 channel, length = struct.unpack('>cI', data)
23 23 if channel in 'IL':
24 24 return channel, length
25 25 else:
26 26 return channel, server.stdout.read(length)
27 27
28 28 def runcommand(server, args, output=sys.stdout, error=sys.stderr, input=None):
29 29 print ' runcommand', ' '.join(args)
30 30 sys.stdout.flush()
31 31 server.stdin.write('runcommand\n')
32 32 writeblock(server, '\0'.join(args))
33 33
34 34 if not input:
35 35 input = cStringIO.StringIO()
36 36
37 37 while True:
38 38 ch, data = readchannel(server)
39 39 if ch == 'o':
40 40 output.write(data)
41 41 output.flush()
42 42 elif ch == 'e':
43 43 error.write(data)
44 44 error.flush()
45 45 elif ch == 'I':
46 46 writeblock(server, input.read(data))
47 47 elif ch == 'L':
48 48 writeblock(server, input.readline(data))
49 49 elif ch == 'r':
50 50 return struct.unpack('>i', data)[0]
51 51 else:
52 52 print "unexpected channel %c: %r" % (ch, data)
53 53 if ch.isupper():
54 54 return
55 55
56 56 def check(func, repopath=None):
57 57 print
58 58 print 'testing %s:' % func.__name__
59 59 print
60 60 sys.stdout.flush()
61 61 server = connect(repopath)
62 62 try:
63 63 return func(server)
64 64 finally:
65 65 server.stdin.close()
66 66 server.wait()
67 67
68 68 def unknowncommand(server):
69 69 server.stdin.write('unknowncommand\n')
70 70
71 71 def hellomessage(server):
72 72 ch, data = readchannel(server)
73 73 # escaping python tests output not supported
74 74 print '%c, %r' % (ch, re.sub('encoding: [a-zA-Z0-9-]+', 'encoding: ***', data))
75 75
76 76 # run an arbitrary command to make sure the next thing the server sends
77 77 # isn't part of the hello message
78 78 runcommand(server, ['id'])
79 79
80 80 def checkruncommand(server):
81 81 # hello block
82 82 readchannel(server)
83 83
84 84 # no args
85 85 runcommand(server, [])
86 86
87 87 # global options
88 88 runcommand(server, ['id', '--quiet'])
89 89
90 90 # make sure global options don't stick through requests
91 91 runcommand(server, ['id'])
92 92
93 93 # --config
94 94 runcommand(server, ['id', '--config', 'ui.quiet=True'])
95 95
96 96 # make sure --config doesn't stick
97 97 runcommand(server, ['id'])
98 98
99 99 def inputeof(server):
100 100 readchannel(server)
101 101 server.stdin.write('runcommand\n')
102 102 # close stdin while server is waiting for input
103 103 server.stdin.close()
104 104
105 105 # server exits with 1 if the pipe closed while reading the command
106 106 print 'server exit code =', server.wait()
107 107
108 108 def serverinput(server):
109 109 readchannel(server)
110 110
111 111 patch = """
112 112 # HG changeset patch
113 113 # User test
114 114 # Date 0 0
115 115 # Node ID c103a3dec114d882c98382d684d8af798d09d857
116 116 # Parent 0000000000000000000000000000000000000000
117 117 1
118 118
119 119 diff -r 000000000000 -r c103a3dec114 a
120 120 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
121 121 +++ b/a Thu Jan 01 00:00:00 1970 +0000
122 122 @@ -0,0 +1,1 @@
123 123 +1
124 124 """
125 125
126 126 runcommand(server, ['import', '-'], input=cStringIO.StringIO(patch))
127 127 runcommand(server, ['log'])
128 128
129 129 def cwd(server):
130 130 """ check that --cwd doesn't persist between requests """
131 131 readchannel(server)
132 132 os.mkdir('foo')
133 133 f = open('foo/bar', 'wb')
134 134 f.write('a')
135 135 f.close()
136 136 runcommand(server, ['--cwd', 'foo', 'st', 'bar'])
137 137 runcommand(server, ['st', 'foo/bar'])
138 138 os.remove('foo/bar')
139 139
140 140 def localhgrc(server):
141 141 """ check that local configs for the cached repo aren't inherited when -R
142 142 is used """
143 143 readchannel(server)
144 144
145 145 # the cached repo local hgrc contains ui.foo=bar, so showconfig should show it
146 146 runcommand(server, ['showconfig'])
147 147
148 148 # but not for this repo
149 149 runcommand(server, ['init', 'foo'])
150 150 runcommand(server, ['-R', 'foo', 'showconfig', 'ui', 'defaults'])
151 151 shutil.rmtree('foo')
152 152
153 153 def hook(**args):
154 154 print 'hook talking'
155 155 print 'now try to read something: %r' % sys.stdin.read()
156 156
157 157 def hookoutput(server):
158 158 readchannel(server)
159 159 runcommand(server, ['--config',
160 160 'hooks.pre-identify=python:test-commandserver.hook', 'id'],
161 161 input=cStringIO.StringIO('some input'))
162 162
163 163 def outsidechanges(server):
164 164 readchannel(server)
165 165 f = open('a', 'ab')
166 166 f.write('a\n')
167 167 f.close()
168 168 runcommand(server, ['status'])
169 169 os.system('hg ci -Am2')
170 170 runcommand(server, ['tip'])
171 171 runcommand(server, ['status'])
172 172
173 173 def bookmarks(server):
174 174 readchannel(server)
175 175 runcommand(server, ['bookmarks'])
176 176
177 177 # changes .hg/bookmarks
178 178 os.system('hg bookmark -i bm1')
179 179 os.system('hg bookmark -i bm2')
180 180 runcommand(server, ['bookmarks'])
181 181
182 182 # changes .hg/bookmarks.current
183 183 os.system('hg upd bm1 -q')
184 184 runcommand(server, ['bookmarks'])
185 185
186 186 runcommand(server, ['bookmarks', 'bm3'])
187 187 f = open('a', 'ab')
188 188 f.write('a\n')
189 189 f.close()
190 190 runcommand(server, ['commit', '-Amm'])
191 191 runcommand(server, ['bookmarks'])
192 192
193 193 def tagscache(server):
194 194 readchannel(server)
195 195 runcommand(server, ['id', '-t', '-r', '0'])
196 196 os.system('hg tag -r 0 foo')
197 197 runcommand(server, ['id', '-t', '-r', '0'])
198 198
199 199 def setphase(server):
200 200 readchannel(server)
201 201 runcommand(server, ['phase', '-r', '.'])
202 202 os.system('hg phase -r . -p')
203 203 runcommand(server, ['phase', '-r', '.'])
204 204
205 205 def rollback(server):
206 206 readchannel(server)
207 207 runcommand(server, ['phase', '-r', '.', '-p'])
208 208 f = open('a', 'ab')
209 209 f.write('a\n')
210 210 f.close()
211 211 runcommand(server, ['commit', '-Am.'])
212 212 runcommand(server, ['rollback'])
213 213 runcommand(server, ['phase', '-r', '.'])
214 214
215 215 def branch(server):
216 216 readchannel(server)
217 217 runcommand(server, ['branch'])
218 218 os.system('hg branch foo')
219 219 runcommand(server, ['branch'])
220 220 os.system('hg branch default')
221 221
222 def hgignore(server):
223 readchannel(server)
224 f = open('.hgignore', 'ab')
225 f.write('')
226 f.close()
227 runcommand(server, ['commit', '-Am.'])
228 f = open('ignored-file', 'ab')
229 f.write('')
230 f.close()
231 f = open('.hgignore', 'ab')
232 f.write('ignored-file')
233 f.close()
234 runcommand(server, ['status', '-i', '-u'])
235
222 236 if __name__ == '__main__':
223 237 os.system('hg init')
224 238
225 239 check(hellomessage)
226 240 check(unknowncommand)
227 241 check(checkruncommand)
228 242 check(inputeof)
229 243 check(serverinput)
230 244 check(cwd)
231 245
232 246 hgrc = open('.hg/hgrc', 'a')
233 247 hgrc.write('[ui]\nfoo=bar\n')
234 248 hgrc.close()
235 249 check(localhgrc)
236 250 check(hookoutput)
237 251 check(outsidechanges)
238 252 check(bookmarks)
239 253 check(tagscache)
240 254 check(setphase)
241 255 check(rollback)
242 256 check(branch)
257 check(hgignore)
@@ -1,158 +1,165 b''
1 1
2 2 testing hellomessage:
3 3
4 4 o, 'capabilities: getencoding runcommand\nencoding: ***'
5 5 runcommand id
6 6 000000000000 tip
7 7
8 8 testing unknowncommand:
9 9
10 10 abort: unknown command unknowncommand
11 11
12 12 testing checkruncommand:
13 13
14 14 runcommand
15 15 Mercurial Distributed SCM
16 16
17 17 basic commands:
18 18
19 19 add add the specified files on the next commit
20 20 annotate show changeset information by line for each file
21 21 clone make a copy of an existing repository
22 22 commit commit the specified files or all outstanding changes
23 23 diff diff repository (or selected files)
24 24 export dump the header and diffs for one or more changesets
25 25 forget forget the specified files on the next commit
26 26 init create a new repository in the given directory
27 27 log show revision history of entire repository or files
28 28 merge merge working directory with another revision
29 29 phase set or show the current phase name
30 30 pull pull changes from the specified source
31 31 push push changes to the specified destination
32 32 remove remove the specified files on the next commit
33 33 serve start stand-alone webserver
34 34 status show changed files in the working directory
35 35 summary summarize working directory state
36 36 update update working directory (or switch revisions)
37 37
38 38 use "hg help" for the full list of commands or "hg -v" for details
39 39 runcommand id --quiet
40 40 000000000000
41 41 runcommand id
42 42 000000000000 tip
43 43 runcommand id --config ui.quiet=True
44 44 000000000000
45 45 runcommand id
46 46 000000000000 tip
47 47
48 48 testing inputeof:
49 49
50 50 server exit code = 1
51 51
52 52 testing serverinput:
53 53
54 54 runcommand import -
55 55 applying patch from stdin
56 56 runcommand log
57 57 changeset: 0:eff892de26ec
58 58 tag: tip
59 59 user: test
60 60 date: Thu Jan 01 00:00:00 1970 +0000
61 61 summary: 1
62 62
63 63
64 64 testing cwd:
65 65
66 66 runcommand --cwd foo st bar
67 67 ? bar
68 68 runcommand st foo/bar
69 69 ? foo/bar
70 70
71 71 testing localhgrc:
72 72
73 73 runcommand showconfig
74 74 bundle.mainreporoot=$TESTTMP
75 75 defaults.backout=-d "0 0"
76 76 defaults.commit=-d "0 0"
77 77 defaults.tag=-d "0 0"
78 78 ui.slash=True
79 79 ui.foo=bar
80 80 runcommand init foo
81 81 runcommand -R foo showconfig ui defaults
82 82 defaults.backout=-d "0 0"
83 83 defaults.commit=-d "0 0"
84 84 defaults.tag=-d "0 0"
85 85 ui.slash=True
86 86
87 87 testing hookoutput:
88 88
89 89 runcommand --config hooks.pre-identify=python:test-commandserver.hook id
90 90 hook talking
91 91 now try to read something: 'some input'
92 92 eff892de26ec tip
93 93
94 94 testing outsidechanges:
95 95
96 96 runcommand status
97 97 M a
98 98 runcommand tip
99 99 changeset: 1:d3a0a68be6de
100 100 tag: tip
101 101 user: test
102 102 date: Thu Jan 01 00:00:00 1970 +0000
103 103 summary: 2
104 104
105 105 runcommand status
106 106
107 107 testing bookmarks:
108 108
109 109 runcommand bookmarks
110 110 no bookmarks set
111 111 runcommand bookmarks
112 112 bm1 1:d3a0a68be6de
113 113 bm2 1:d3a0a68be6de
114 114 runcommand bookmarks
115 115 * bm1 1:d3a0a68be6de
116 116 bm2 1:d3a0a68be6de
117 117 runcommand bookmarks bm3
118 118 runcommand commit -Amm
119 119 runcommand bookmarks
120 120 bm1 1:d3a0a68be6de
121 121 bm2 1:d3a0a68be6de
122 122 * bm3 2:aef17e88f5f0
123 123
124 124 testing tagscache:
125 125
126 126 runcommand id -t -r 0
127 127
128 128 runcommand id -t -r 0
129 129 foo
130 130
131 131 testing setphase:
132 132
133 133 runcommand phase -r .
134 134 3: draft
135 135 runcommand phase -r .
136 136 3: public
137 137
138 138 testing rollback:
139 139
140 140 runcommand phase -r . -p
141 141 no phases changed
142 142 runcommand commit -Am.
143 143 runcommand rollback
144 144 repository tip rolled back to revision 3 (undo commit)
145 145 working directory now based on revision 3
146 146 runcommand phase -r .
147 147 3: public
148 148
149 149 testing branch:
150 150
151 151 runcommand branch
152 152 default
153 153 marked working directory as branch foo
154 154 (branches are permanent and global, did you want a bookmark?)
155 155 runcommand branch
156 156 foo
157 157 marked working directory as branch default
158 158 (branches are permanent and global, did you want a bookmark?)
159
160 testing hgignore:
161
162 runcommand commit -Am.
163 adding .hgignore
164 runcommand status -i -u
165 I ignored-file
General Comments 0
You need to be logged in to leave comments. Login now