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