##// END OF EJS Templates
rust-parsers: move parser bindings to their own file and Python module...
Raphaël Gomès -
r42992:760a7851 default
parent child Browse files
Show More
@@ -1,1518 +1,1518 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
8 8 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from .node import nullid
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 match as matchmod,
22 22 pathutil,
23 23 policy,
24 24 pycompat,
25 25 scmutil,
26 26 txnutil,
27 27 util,
28 28 )
29 29
30 parsers = policy.importmod(r'parsers')
31 dirstatemod = policy.importrust(r'dirstate', default=parsers)
30 orig_parsers = policy.importmod(r'parsers')
31 parsers = policy.importrust(r'parsers', default=orig_parsers)
32 32
33 33 propertycache = util.propertycache
34 34 filecache = scmutil.filecache
35 35 _rangemask = 0x7fffffff
36 36
37 dirstatetuple = parsers.dirstatetuple
37 dirstatetuple = orig_parsers.dirstatetuple
38 38
39 39 class repocache(filecache):
40 40 """filecache for files in .hg/"""
41 41 def join(self, obj, fname):
42 42 return obj._opener.join(fname)
43 43
44 44 class rootcache(filecache):
45 45 """filecache for files in the repository root"""
46 46 def join(self, obj, fname):
47 47 return obj._join(fname)
48 48
49 49 def _getfsnow(vfs):
50 50 '''Get "now" timestamp on filesystem'''
51 51 tmpfd, tmpname = vfs.mkstemp()
52 52 try:
53 53 return os.fstat(tmpfd)[stat.ST_MTIME]
54 54 finally:
55 55 os.close(tmpfd)
56 56 vfs.unlink(tmpname)
57 57
58 58 class dirstate(object):
59 59
60 60 def __init__(self, opener, ui, root, validate, sparsematchfn):
61 61 '''Create a new dirstate object.
62 62
63 63 opener is an open()-like callable that can be used to open the
64 64 dirstate file; root is the root of the directory tracked by
65 65 the dirstate.
66 66 '''
67 67 self._opener = opener
68 68 self._validate = validate
69 69 self._root = root
70 70 self._sparsematchfn = sparsematchfn
71 71 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
72 72 # UNC path pointing to root share (issue4557)
73 73 self._rootdir = pathutil.normasprefix(root)
74 74 self._dirty = False
75 75 self._lastnormaltime = 0
76 76 self._ui = ui
77 77 self._filecache = {}
78 78 self._parentwriters = 0
79 79 self._filename = 'dirstate'
80 80 self._pendingfilename = '%s.pending' % self._filename
81 81 self._plchangecallbacks = {}
82 82 self._origpl = None
83 83 self._updatedfiles = set()
84 84 self._mapcls = dirstatemap
85 85 # Access and cache cwd early, so we don't access it for the first time
86 86 # after a working-copy update caused it to not exist (accessing it then
87 87 # raises an exception).
88 88 self._cwd
89 89
90 90 @contextlib.contextmanager
91 91 def parentchange(self):
92 92 '''Context manager for handling dirstate parents.
93 93
94 94 If an exception occurs in the scope of the context manager,
95 95 the incoherent dirstate won't be written when wlock is
96 96 released.
97 97 '''
98 98 self._parentwriters += 1
99 99 yield
100 100 # Typically we want the "undo" step of a context manager in a
101 101 # finally block so it happens even when an exception
102 102 # occurs. In this case, however, we only want to decrement
103 103 # parentwriters if the code in the with statement exits
104 104 # normally, so we don't have a try/finally here on purpose.
105 105 self._parentwriters -= 1
106 106
107 107 def pendingparentchange(self):
108 108 '''Returns true if the dirstate is in the middle of a set of changes
109 109 that modify the dirstate parent.
110 110 '''
111 111 return self._parentwriters > 0
112 112
113 113 @propertycache
114 114 def _map(self):
115 115 """Return the dirstate contents (see documentation for dirstatemap)."""
116 116 self._map = self._mapcls(self._ui, self._opener, self._root)
117 117 return self._map
118 118
119 119 @property
120 120 def _sparsematcher(self):
121 121 """The matcher for the sparse checkout.
122 122
123 123 The working directory may not include every file from a manifest. The
124 124 matcher obtained by this property will match a path if it is to be
125 125 included in the working directory.
126 126 """
127 127 # TODO there is potential to cache this property. For now, the matcher
128 128 # is resolved on every access. (But the called function does use a
129 129 # cache to keep the lookup fast.)
130 130 return self._sparsematchfn()
131 131
132 132 @repocache('branch')
133 133 def _branch(self):
134 134 try:
135 135 return self._opener.read("branch").strip() or "default"
136 136 except IOError as inst:
137 137 if inst.errno != errno.ENOENT:
138 138 raise
139 139 return "default"
140 140
141 141 @property
142 142 def _pl(self):
143 143 return self._map.parents()
144 144
145 145 def hasdir(self, d):
146 146 return self._map.hastrackeddir(d)
147 147
148 148 @rootcache('.hgignore')
149 149 def _ignore(self):
150 150 files = self._ignorefiles()
151 151 if not files:
152 152 return matchmod.never()
153 153
154 154 pats = ['include:%s' % f for f in files]
155 155 return matchmod.match(self._root, '', [], pats, warn=self._ui.warn)
156 156
157 157 @propertycache
158 158 def _slash(self):
159 159 return self._ui.configbool('ui', 'slash') and pycompat.ossep != '/'
160 160
161 161 @propertycache
162 162 def _checklink(self):
163 163 return util.checklink(self._root)
164 164
165 165 @propertycache
166 166 def _checkexec(self):
167 167 return util.checkexec(self._root)
168 168
169 169 @propertycache
170 170 def _checkcase(self):
171 171 return not util.fscasesensitive(self._join('.hg'))
172 172
173 173 def _join(self, f):
174 174 # much faster than os.path.join()
175 175 # it's safe because f is always a relative path
176 176 return self._rootdir + f
177 177
178 178 def flagfunc(self, buildfallback):
179 179 if self._checklink and self._checkexec:
180 180 def f(x):
181 181 try:
182 182 st = os.lstat(self._join(x))
183 183 if util.statislink(st):
184 184 return 'l'
185 185 if util.statisexec(st):
186 186 return 'x'
187 187 except OSError:
188 188 pass
189 189 return ''
190 190 return f
191 191
192 192 fallback = buildfallback()
193 193 if self._checklink:
194 194 def f(x):
195 195 if os.path.islink(self._join(x)):
196 196 return 'l'
197 197 if 'x' in fallback(x):
198 198 return 'x'
199 199 return ''
200 200 return f
201 201 if self._checkexec:
202 202 def f(x):
203 203 if 'l' in fallback(x):
204 204 return 'l'
205 205 if util.isexec(self._join(x)):
206 206 return 'x'
207 207 return ''
208 208 return f
209 209 else:
210 210 return fallback
211 211
212 212 @propertycache
213 213 def _cwd(self):
214 214 # internal config: ui.forcecwd
215 215 forcecwd = self._ui.config('ui', 'forcecwd')
216 216 if forcecwd:
217 217 return forcecwd
218 218 return encoding.getcwd()
219 219
220 220 def getcwd(self):
221 221 '''Return the path from which a canonical path is calculated.
222 222
223 223 This path should be used to resolve file patterns or to convert
224 224 canonical paths back to file paths for display. It shouldn't be
225 225 used to get real file paths. Use vfs functions instead.
226 226 '''
227 227 cwd = self._cwd
228 228 if cwd == self._root:
229 229 return ''
230 230 # self._root ends with a path separator if self._root is '/' or 'C:\'
231 231 rootsep = self._root
232 232 if not util.endswithsep(rootsep):
233 233 rootsep += pycompat.ossep
234 234 if cwd.startswith(rootsep):
235 235 return cwd[len(rootsep):]
236 236 else:
237 237 # we're outside the repo. return an absolute path.
238 238 return cwd
239 239
240 240 def pathto(self, f, cwd=None):
241 241 if cwd is None:
242 242 cwd = self.getcwd()
243 243 path = util.pathto(self._root, cwd, f)
244 244 if self._slash:
245 245 return util.pconvert(path)
246 246 return path
247 247
248 248 def __getitem__(self, key):
249 249 '''Return the current state of key (a filename) in the dirstate.
250 250
251 251 States are:
252 252 n normal
253 253 m needs merging
254 254 r marked for removal
255 255 a marked for addition
256 256 ? not tracked
257 257 '''
258 258 return self._map.get(key, ("?",))[0]
259 259
260 260 def __contains__(self, key):
261 261 return key in self._map
262 262
263 263 def __iter__(self):
264 264 return iter(sorted(self._map))
265 265
266 266 def items(self):
267 267 return self._map.iteritems()
268 268
269 269 iteritems = items
270 270
271 271 def parents(self):
272 272 return [self._validate(p) for p in self._pl]
273 273
274 274 def p1(self):
275 275 return self._validate(self._pl[0])
276 276
277 277 def p2(self):
278 278 return self._validate(self._pl[1])
279 279
280 280 def branch(self):
281 281 return encoding.tolocal(self._branch)
282 282
283 283 def setparents(self, p1, p2=nullid):
284 284 """Set dirstate parents to p1 and p2.
285 285
286 286 When moving from two parents to one, 'm' merged entries a
287 287 adjusted to normal and previous copy records discarded and
288 288 returned by the call.
289 289
290 290 See localrepo.setparents()
291 291 """
292 292 if self._parentwriters == 0:
293 293 raise ValueError("cannot set dirstate parent outside of "
294 294 "dirstate.parentchange context manager")
295 295
296 296 self._dirty = True
297 297 oldp2 = self._pl[1]
298 298 if self._origpl is None:
299 299 self._origpl = self._pl
300 300 self._map.setparents(p1, p2)
301 301 copies = {}
302 302 if oldp2 != nullid and p2 == nullid:
303 303 candidatefiles = self._map.nonnormalset.union(
304 304 self._map.otherparentset)
305 305 for f in candidatefiles:
306 306 s = self._map.get(f)
307 307 if s is None:
308 308 continue
309 309
310 310 # Discard 'm' markers when moving away from a merge state
311 311 if s[0] == 'm':
312 312 source = self._map.copymap.get(f)
313 313 if source:
314 314 copies[f] = source
315 315 self.normallookup(f)
316 316 # Also fix up otherparent markers
317 317 elif s[0] == 'n' and s[2] == -2:
318 318 source = self._map.copymap.get(f)
319 319 if source:
320 320 copies[f] = source
321 321 self.add(f)
322 322 return copies
323 323
324 324 def setbranch(self, branch):
325 325 self.__class__._branch.set(self, encoding.fromlocal(branch))
326 326 f = self._opener('branch', 'w', atomictemp=True, checkambig=True)
327 327 try:
328 328 f.write(self._branch + '\n')
329 329 f.close()
330 330
331 331 # make sure filecache has the correct stat info for _branch after
332 332 # replacing the underlying file
333 333 ce = self._filecache['_branch']
334 334 if ce:
335 335 ce.refresh()
336 336 except: # re-raises
337 337 f.discard()
338 338 raise
339 339
340 340 def invalidate(self):
341 341 '''Causes the next access to reread the dirstate.
342 342
343 343 This is different from localrepo.invalidatedirstate() because it always
344 344 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
345 345 check whether the dirstate has changed before rereading it.'''
346 346
347 347 for a in (r"_map", r"_branch", r"_ignore"):
348 348 if a in self.__dict__:
349 349 delattr(self, a)
350 350 self._lastnormaltime = 0
351 351 self._dirty = False
352 352 self._updatedfiles.clear()
353 353 self._parentwriters = 0
354 354 self._origpl = None
355 355
356 356 def copy(self, source, dest):
357 357 """Mark dest as a copy of source. Unmark dest if source is None."""
358 358 if source == dest:
359 359 return
360 360 self._dirty = True
361 361 if source is not None:
362 362 self._map.copymap[dest] = source
363 363 self._updatedfiles.add(source)
364 364 self._updatedfiles.add(dest)
365 365 elif self._map.copymap.pop(dest, None):
366 366 self._updatedfiles.add(dest)
367 367
368 368 def copied(self, file):
369 369 return self._map.copymap.get(file, None)
370 370
371 371 def copies(self):
372 372 return self._map.copymap
373 373
374 374 def _addpath(self, f, state, mode, size, mtime):
375 375 oldstate = self[f]
376 376 if state == 'a' or oldstate == 'r':
377 377 scmutil.checkfilename(f)
378 378 if self._map.hastrackeddir(f):
379 379 raise error.Abort(_('directory %r already in dirstate') %
380 380 pycompat.bytestr(f))
381 381 # shadows
382 382 for d in util.finddirs(f):
383 383 if self._map.hastrackeddir(d):
384 384 break
385 385 entry = self._map.get(d)
386 386 if entry is not None and entry[0] != 'r':
387 387 raise error.Abort(
388 388 _('file %r in dirstate clashes with %r') %
389 389 (pycompat.bytestr(d), pycompat.bytestr(f)))
390 390 self._dirty = True
391 391 self._updatedfiles.add(f)
392 392 self._map.addfile(f, oldstate, state, mode, size, mtime)
393 393
394 394 def normal(self, f, parentfiledata=None):
395 395 '''Mark a file normal and clean.
396 396
397 397 parentfiledata: (mode, size, mtime) of the clean file
398 398
399 399 parentfiledata should be computed from memory (for mode,
400 400 size), as or close as possible from the point where we
401 401 determined the file was clean, to limit the risk of the
402 402 file having been changed by an external process between the
403 403 moment where the file was determined to be clean and now.'''
404 404 if parentfiledata:
405 405 (mode, size, mtime) = parentfiledata
406 406 else:
407 407 s = os.lstat(self._join(f))
408 408 mode = s.st_mode
409 409 size = s.st_size
410 410 mtime = s[stat.ST_MTIME]
411 411 self._addpath(f, 'n', mode, size & _rangemask, mtime & _rangemask)
412 412 self._map.copymap.pop(f, None)
413 413 if f in self._map.nonnormalset:
414 414 self._map.nonnormalset.remove(f)
415 415 if mtime > self._lastnormaltime:
416 416 # Remember the most recent modification timeslot for status(),
417 417 # to make sure we won't miss future size-preserving file content
418 418 # modifications that happen within the same timeslot.
419 419 self._lastnormaltime = mtime
420 420
421 421 def normallookup(self, f):
422 422 '''Mark a file normal, but possibly dirty.'''
423 423 if self._pl[1] != nullid:
424 424 # if there is a merge going on and the file was either
425 425 # in state 'm' (-1) or coming from other parent (-2) before
426 426 # being removed, restore that state.
427 427 entry = self._map.get(f)
428 428 if entry is not None:
429 429 if entry[0] == 'r' and entry[2] in (-1, -2):
430 430 source = self._map.copymap.get(f)
431 431 if entry[2] == -1:
432 432 self.merge(f)
433 433 elif entry[2] == -2:
434 434 self.otherparent(f)
435 435 if source:
436 436 self.copy(source, f)
437 437 return
438 438 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
439 439 return
440 440 self._addpath(f, 'n', 0, -1, -1)
441 441 self._map.copymap.pop(f, None)
442 442
443 443 def otherparent(self, f):
444 444 '''Mark as coming from the other parent, always dirty.'''
445 445 if self._pl[1] == nullid:
446 446 raise error.Abort(_("setting %r to other parent "
447 447 "only allowed in merges") % f)
448 448 if f in self and self[f] == 'n':
449 449 # merge-like
450 450 self._addpath(f, 'm', 0, -2, -1)
451 451 else:
452 452 # add-like
453 453 self._addpath(f, 'n', 0, -2, -1)
454 454 self._map.copymap.pop(f, None)
455 455
456 456 def add(self, f):
457 457 '''Mark a file added.'''
458 458 self._addpath(f, 'a', 0, -1, -1)
459 459 self._map.copymap.pop(f, None)
460 460
461 461 def remove(self, f):
462 462 '''Mark a file removed.'''
463 463 self._dirty = True
464 464 oldstate = self[f]
465 465 size = 0
466 466 if self._pl[1] != nullid:
467 467 entry = self._map.get(f)
468 468 if entry is not None:
469 469 # backup the previous state
470 470 if entry[0] == 'm': # merge
471 471 size = -1
472 472 elif entry[0] == 'n' and entry[2] == -2: # other parent
473 473 size = -2
474 474 self._map.otherparentset.add(f)
475 475 self._updatedfiles.add(f)
476 476 self._map.removefile(f, oldstate, size)
477 477 if size == 0:
478 478 self._map.copymap.pop(f, None)
479 479
480 480 def merge(self, f):
481 481 '''Mark a file merged.'''
482 482 if self._pl[1] == nullid:
483 483 return self.normallookup(f)
484 484 return self.otherparent(f)
485 485
486 486 def drop(self, f):
487 487 '''Drop a file from the dirstate'''
488 488 oldstate = self[f]
489 489 if self._map.dropfile(f, oldstate):
490 490 self._dirty = True
491 491 self._updatedfiles.add(f)
492 492 self._map.copymap.pop(f, None)
493 493
494 494 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
495 495 if exists is None:
496 496 exists = os.path.lexists(os.path.join(self._root, path))
497 497 if not exists:
498 498 # Maybe a path component exists
499 499 if not ignoremissing and '/' in path:
500 500 d, f = path.rsplit('/', 1)
501 501 d = self._normalize(d, False, ignoremissing, None)
502 502 folded = d + "/" + f
503 503 else:
504 504 # No path components, preserve original case
505 505 folded = path
506 506 else:
507 507 # recursively normalize leading directory components
508 508 # against dirstate
509 509 if '/' in normed:
510 510 d, f = normed.rsplit('/', 1)
511 511 d = self._normalize(d, False, ignoremissing, True)
512 512 r = self._root + "/" + d
513 513 folded = d + "/" + util.fspath(f, r)
514 514 else:
515 515 folded = util.fspath(normed, self._root)
516 516 storemap[normed] = folded
517 517
518 518 return folded
519 519
520 520 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
521 521 normed = util.normcase(path)
522 522 folded = self._map.filefoldmap.get(normed, None)
523 523 if folded is None:
524 524 if isknown:
525 525 folded = path
526 526 else:
527 527 folded = self._discoverpath(path, normed, ignoremissing, exists,
528 528 self._map.filefoldmap)
529 529 return folded
530 530
531 531 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
532 532 normed = util.normcase(path)
533 533 folded = self._map.filefoldmap.get(normed, None)
534 534 if folded is None:
535 535 folded = self._map.dirfoldmap.get(normed, None)
536 536 if folded is None:
537 537 if isknown:
538 538 folded = path
539 539 else:
540 540 # store discovered result in dirfoldmap so that future
541 541 # normalizefile calls don't start matching directories
542 542 folded = self._discoverpath(path, normed, ignoremissing, exists,
543 543 self._map.dirfoldmap)
544 544 return folded
545 545
546 546 def normalize(self, path, isknown=False, ignoremissing=False):
547 547 '''
548 548 normalize the case of a pathname when on a casefolding filesystem
549 549
550 550 isknown specifies whether the filename came from walking the
551 551 disk, to avoid extra filesystem access.
552 552
553 553 If ignoremissing is True, missing path are returned
554 554 unchanged. Otherwise, we try harder to normalize possibly
555 555 existing path components.
556 556
557 557 The normalized case is determined based on the following precedence:
558 558
559 559 - version of name already stored in the dirstate
560 560 - version of name stored on disk
561 561 - version provided via command arguments
562 562 '''
563 563
564 564 if self._checkcase:
565 565 return self._normalize(path, isknown, ignoremissing)
566 566 return path
567 567
568 568 def clear(self):
569 569 self._map.clear()
570 570 self._lastnormaltime = 0
571 571 self._updatedfiles.clear()
572 572 self._dirty = True
573 573
574 574 def rebuild(self, parent, allfiles, changedfiles=None):
575 575 if changedfiles is None:
576 576 # Rebuild entire dirstate
577 577 changedfiles = allfiles
578 578 lastnormaltime = self._lastnormaltime
579 579 self.clear()
580 580 self._lastnormaltime = lastnormaltime
581 581
582 582 if self._origpl is None:
583 583 self._origpl = self._pl
584 584 self._map.setparents(parent, nullid)
585 585 for f in changedfiles:
586 586 if f in allfiles:
587 587 self.normallookup(f)
588 588 else:
589 589 self.drop(f)
590 590
591 591 self._dirty = True
592 592
593 593 def identity(self):
594 594 '''Return identity of dirstate itself to detect changing in storage
595 595
596 596 If identity of previous dirstate is equal to this, writing
597 597 changes based on the former dirstate out can keep consistency.
598 598 '''
599 599 return self._map.identity
600 600
601 601 def write(self, tr):
602 602 if not self._dirty:
603 603 return
604 604
605 605 filename = self._filename
606 606 if tr:
607 607 # 'dirstate.write()' is not only for writing in-memory
608 608 # changes out, but also for dropping ambiguous timestamp.
609 609 # delayed writing re-raise "ambiguous timestamp issue".
610 610 # See also the wiki page below for detail:
611 611 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
612 612
613 613 # emulate dropping timestamp in 'parsers.pack_dirstate'
614 614 now = _getfsnow(self._opener)
615 615 self._map.clearambiguoustimes(self._updatedfiles, now)
616 616
617 617 # emulate that all 'dirstate.normal' results are written out
618 618 self._lastnormaltime = 0
619 619 self._updatedfiles.clear()
620 620
621 621 # delay writing in-memory changes out
622 622 tr.addfilegenerator('dirstate', (self._filename,),
623 623 self._writedirstate, location='plain')
624 624 return
625 625
626 626 st = self._opener(filename, "w", atomictemp=True, checkambig=True)
627 627 self._writedirstate(st)
628 628
629 629 def addparentchangecallback(self, category, callback):
630 630 """add a callback to be called when the wd parents are changed
631 631
632 632 Callback will be called with the following arguments:
633 633 dirstate, (oldp1, oldp2), (newp1, newp2)
634 634
635 635 Category is a unique identifier to allow overwriting an old callback
636 636 with a newer callback.
637 637 """
638 638 self._plchangecallbacks[category] = callback
639 639
640 640 def _writedirstate(self, st):
641 641 # notify callbacks about parents change
642 642 if self._origpl is not None and self._origpl != self._pl:
643 643 for c, callback in sorted(self._plchangecallbacks.iteritems()):
644 644 callback(self, self._origpl, self._pl)
645 645 self._origpl = None
646 646 # use the modification time of the newly created temporary file as the
647 647 # filesystem's notion of 'now'
648 648 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
649 649
650 650 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
651 651 # timestamp of each entries in dirstate, because of 'now > mtime'
652 652 delaywrite = self._ui.configint('debug', 'dirstate.delaywrite')
653 653 if delaywrite > 0:
654 654 # do we have any files to delay for?
655 655 for f, e in self._map.iteritems():
656 656 if e[0] == 'n' and e[3] == now:
657 657 import time # to avoid useless import
658 658 # rather than sleep n seconds, sleep until the next
659 659 # multiple of n seconds
660 660 clock = time.time()
661 661 start = int(clock) - (int(clock) % delaywrite)
662 662 end = start + delaywrite
663 663 time.sleep(end - clock)
664 664 now = end # trust our estimate that the end is near now
665 665 break
666 666
667 667 self._map.write(st, now)
668 668 self._lastnormaltime = 0
669 669 self._dirty = False
670 670
671 671 def _dirignore(self, f):
672 672 if self._ignore(f):
673 673 return True
674 674 for p in util.finddirs(f):
675 675 if self._ignore(p):
676 676 return True
677 677 return False
678 678
679 679 def _ignorefiles(self):
680 680 files = []
681 681 if os.path.exists(self._join('.hgignore')):
682 682 files.append(self._join('.hgignore'))
683 683 for name, path in self._ui.configitems("ui"):
684 684 if name == 'ignore' or name.startswith('ignore.'):
685 685 # we need to use os.path.join here rather than self._join
686 686 # because path is arbitrary and user-specified
687 687 files.append(os.path.join(self._rootdir, util.expandpath(path)))
688 688 return files
689 689
690 690 def _ignorefileandline(self, f):
691 691 files = collections.deque(self._ignorefiles())
692 692 visited = set()
693 693 while files:
694 694 i = files.popleft()
695 695 patterns = matchmod.readpatternfile(i, self._ui.warn,
696 696 sourceinfo=True)
697 697 for pattern, lineno, line in patterns:
698 698 kind, p = matchmod._patsplit(pattern, 'glob')
699 699 if kind == "subinclude":
700 700 if p not in visited:
701 701 files.append(p)
702 702 continue
703 703 m = matchmod.match(self._root, '', [], [pattern],
704 704 warn=self._ui.warn)
705 705 if m(f):
706 706 return (i, lineno, line)
707 707 visited.add(i)
708 708 return (None, -1, "")
709 709
710 710 def _walkexplicit(self, match, subrepos):
711 711 '''Get stat data about the files explicitly specified by match.
712 712
713 713 Return a triple (results, dirsfound, dirsnotfound).
714 714 - results is a mapping from filename to stat result. It also contains
715 715 listings mapping subrepos and .hg to None.
716 716 - dirsfound is a list of files found to be directories.
717 717 - dirsnotfound is a list of files that the dirstate thinks are
718 718 directories and that were not found.'''
719 719
720 720 def badtype(mode):
721 721 kind = _('unknown')
722 722 if stat.S_ISCHR(mode):
723 723 kind = _('character device')
724 724 elif stat.S_ISBLK(mode):
725 725 kind = _('block device')
726 726 elif stat.S_ISFIFO(mode):
727 727 kind = _('fifo')
728 728 elif stat.S_ISSOCK(mode):
729 729 kind = _('socket')
730 730 elif stat.S_ISDIR(mode):
731 731 kind = _('directory')
732 732 return _('unsupported file type (type is %s)') % kind
733 733
734 734 matchedir = match.explicitdir
735 735 badfn = match.bad
736 736 dmap = self._map
737 737 lstat = os.lstat
738 738 getkind = stat.S_IFMT
739 739 dirkind = stat.S_IFDIR
740 740 regkind = stat.S_IFREG
741 741 lnkkind = stat.S_IFLNK
742 742 join = self._join
743 743 dirsfound = []
744 744 foundadd = dirsfound.append
745 745 dirsnotfound = []
746 746 notfoundadd = dirsnotfound.append
747 747
748 748 if not match.isexact() and self._checkcase:
749 749 normalize = self._normalize
750 750 else:
751 751 normalize = None
752 752
753 753 files = sorted(match.files())
754 754 subrepos.sort()
755 755 i, j = 0, 0
756 756 while i < len(files) and j < len(subrepos):
757 757 subpath = subrepos[j] + "/"
758 758 if files[i] < subpath:
759 759 i += 1
760 760 continue
761 761 while i < len(files) and files[i].startswith(subpath):
762 762 del files[i]
763 763 j += 1
764 764
765 765 if not files or '' in files:
766 766 files = ['']
767 767 # constructing the foldmap is expensive, so don't do it for the
768 768 # common case where files is ['']
769 769 normalize = None
770 770 results = dict.fromkeys(subrepos)
771 771 results['.hg'] = None
772 772
773 773 for ff in files:
774 774 if normalize:
775 775 nf = normalize(ff, False, True)
776 776 else:
777 777 nf = ff
778 778 if nf in results:
779 779 continue
780 780
781 781 try:
782 782 st = lstat(join(nf))
783 783 kind = getkind(st.st_mode)
784 784 if kind == dirkind:
785 785 if nf in dmap:
786 786 # file replaced by dir on disk but still in dirstate
787 787 results[nf] = None
788 788 if matchedir:
789 789 matchedir(nf)
790 790 foundadd((nf, ff))
791 791 elif kind == regkind or kind == lnkkind:
792 792 results[nf] = st
793 793 else:
794 794 badfn(ff, badtype(kind))
795 795 if nf in dmap:
796 796 results[nf] = None
797 797 except OSError as inst: # nf not found on disk - it is dirstate only
798 798 if nf in dmap: # does it exactly match a missing file?
799 799 results[nf] = None
800 800 else: # does it match a missing directory?
801 801 if self._map.hasdir(nf):
802 802 if matchedir:
803 803 matchedir(nf)
804 804 notfoundadd(nf)
805 805 else:
806 806 badfn(ff, encoding.strtolocal(inst.strerror))
807 807
808 808 # match.files() may contain explicitly-specified paths that shouldn't
809 809 # be taken; drop them from the list of files found. dirsfound/notfound
810 810 # aren't filtered here because they will be tested later.
811 811 if match.anypats():
812 812 for f in list(results):
813 813 if f == '.hg' or f in subrepos:
814 814 # keep sentinel to disable further out-of-repo walks
815 815 continue
816 816 if not match(f):
817 817 del results[f]
818 818
819 819 # Case insensitive filesystems cannot rely on lstat() failing to detect
820 820 # a case-only rename. Prune the stat object for any file that does not
821 821 # match the case in the filesystem, if there are multiple files that
822 822 # normalize to the same path.
823 823 if match.isexact() and self._checkcase:
824 824 normed = {}
825 825
826 826 for f, st in results.iteritems():
827 827 if st is None:
828 828 continue
829 829
830 830 nc = util.normcase(f)
831 831 paths = normed.get(nc)
832 832
833 833 if paths is None:
834 834 paths = set()
835 835 normed[nc] = paths
836 836
837 837 paths.add(f)
838 838
839 839 for norm, paths in normed.iteritems():
840 840 if len(paths) > 1:
841 841 for path in paths:
842 842 folded = self._discoverpath(path, norm, True, None,
843 843 self._map.dirfoldmap)
844 844 if path != folded:
845 845 results[path] = None
846 846
847 847 return results, dirsfound, dirsnotfound
848 848
849 849 def walk(self, match, subrepos, unknown, ignored, full=True):
850 850 '''
851 851 Walk recursively through the directory tree, finding all files
852 852 matched by match.
853 853
854 854 If full is False, maybe skip some known-clean files.
855 855
856 856 Return a dict mapping filename to stat-like object (either
857 857 mercurial.osutil.stat instance or return value of os.stat()).
858 858
859 859 '''
860 860 # full is a flag that extensions that hook into walk can use -- this
861 861 # implementation doesn't use it at all. This satisfies the contract
862 862 # because we only guarantee a "maybe".
863 863
864 864 if ignored:
865 865 ignore = util.never
866 866 dirignore = util.never
867 867 elif unknown:
868 868 ignore = self._ignore
869 869 dirignore = self._dirignore
870 870 else:
871 871 # if not unknown and not ignored, drop dir recursion and step 2
872 872 ignore = util.always
873 873 dirignore = util.always
874 874
875 875 matchfn = match.matchfn
876 876 matchalways = match.always()
877 877 matchtdir = match.traversedir
878 878 dmap = self._map
879 879 listdir = util.listdir
880 880 lstat = os.lstat
881 881 dirkind = stat.S_IFDIR
882 882 regkind = stat.S_IFREG
883 883 lnkkind = stat.S_IFLNK
884 884 join = self._join
885 885
886 886 exact = skipstep3 = False
887 887 if match.isexact(): # match.exact
888 888 exact = True
889 889 dirignore = util.always # skip step 2
890 890 elif match.prefix(): # match.match, no patterns
891 891 skipstep3 = True
892 892
893 893 if not exact and self._checkcase:
894 894 normalize = self._normalize
895 895 normalizefile = self._normalizefile
896 896 skipstep3 = False
897 897 else:
898 898 normalize = self._normalize
899 899 normalizefile = None
900 900
901 901 # step 1: find all explicit files
902 902 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
903 903
904 904 skipstep3 = skipstep3 and not (work or dirsnotfound)
905 905 work = [d for d in work if not dirignore(d[0])]
906 906
907 907 # step 2: visit subdirectories
908 908 def traverse(work, alreadynormed):
909 909 wadd = work.append
910 910 while work:
911 911 nd = work.pop()
912 912 visitentries = match.visitchildrenset(nd)
913 913 if not visitentries:
914 914 continue
915 915 if visitentries == 'this' or visitentries == 'all':
916 916 visitentries = None
917 917 skip = None
918 918 if nd != '':
919 919 skip = '.hg'
920 920 try:
921 921 entries = listdir(join(nd), stat=True, skip=skip)
922 922 except OSError as inst:
923 923 if inst.errno in (errno.EACCES, errno.ENOENT):
924 924 match.bad(self.pathto(nd),
925 925 encoding.strtolocal(inst.strerror))
926 926 continue
927 927 raise
928 928 for f, kind, st in entries:
929 929 # Some matchers may return files in the visitentries set,
930 930 # instead of 'this', if the matcher explicitly mentions them
931 931 # and is not an exactmatcher. This is acceptable; we do not
932 932 # make any hard assumptions about file-or-directory below
933 933 # based on the presence of `f` in visitentries. If
934 934 # visitchildrenset returned a set, we can always skip the
935 935 # entries *not* in the set it provided regardless of whether
936 936 # they're actually a file or a directory.
937 937 if visitentries and f not in visitentries:
938 938 continue
939 939 if normalizefile:
940 940 # even though f might be a directory, we're only
941 941 # interested in comparing it to files currently in the
942 942 # dmap -- therefore normalizefile is enough
943 943 nf = normalizefile(nd and (nd + "/" + f) or f, True,
944 944 True)
945 945 else:
946 946 nf = nd and (nd + "/" + f) or f
947 947 if nf not in results:
948 948 if kind == dirkind:
949 949 if not ignore(nf):
950 950 if matchtdir:
951 951 matchtdir(nf)
952 952 wadd(nf)
953 953 if nf in dmap and (matchalways or matchfn(nf)):
954 954 results[nf] = None
955 955 elif kind == regkind or kind == lnkkind:
956 956 if nf in dmap:
957 957 if matchalways or matchfn(nf):
958 958 results[nf] = st
959 959 elif ((matchalways or matchfn(nf))
960 960 and not ignore(nf)):
961 961 # unknown file -- normalize if necessary
962 962 if not alreadynormed:
963 963 nf = normalize(nf, False, True)
964 964 results[nf] = st
965 965 elif nf in dmap and (matchalways or matchfn(nf)):
966 966 results[nf] = None
967 967
968 968 for nd, d in work:
969 969 # alreadynormed means that processwork doesn't have to do any
970 970 # expensive directory normalization
971 971 alreadynormed = not normalize or nd == d
972 972 traverse([d], alreadynormed)
973 973
974 974 for s in subrepos:
975 975 del results[s]
976 976 del results['.hg']
977 977
978 978 # step 3: visit remaining files from dmap
979 979 if not skipstep3 and not exact:
980 980 # If a dmap file is not in results yet, it was either
981 981 # a) not matching matchfn b) ignored, c) missing, or d) under a
982 982 # symlink directory.
983 983 if not results and matchalways:
984 984 visit = [f for f in dmap]
985 985 else:
986 986 visit = [f for f in dmap if f not in results and matchfn(f)]
987 987 visit.sort()
988 988
989 989 if unknown:
990 990 # unknown == True means we walked all dirs under the roots
991 991 # that wasn't ignored, and everything that matched was stat'ed
992 992 # and is already in results.
993 993 # The rest must thus be ignored or under a symlink.
994 994 audit_path = pathutil.pathauditor(self._root, cached=True)
995 995
996 996 for nf in iter(visit):
997 997 # If a stat for the same file was already added with a
998 998 # different case, don't add one for this, since that would
999 999 # make it appear as if the file exists under both names
1000 1000 # on disk.
1001 1001 if (normalizefile and
1002 1002 normalizefile(nf, True, True) in results):
1003 1003 results[nf] = None
1004 1004 # Report ignored items in the dmap as long as they are not
1005 1005 # under a symlink directory.
1006 1006 elif audit_path.check(nf):
1007 1007 try:
1008 1008 results[nf] = lstat(join(nf))
1009 1009 # file was just ignored, no links, and exists
1010 1010 except OSError:
1011 1011 # file doesn't exist
1012 1012 results[nf] = None
1013 1013 else:
1014 1014 # It's either missing or under a symlink directory
1015 1015 # which we in this case report as missing
1016 1016 results[nf] = None
1017 1017 else:
1018 1018 # We may not have walked the full directory tree above,
1019 1019 # so stat and check everything we missed.
1020 1020 iv = iter(visit)
1021 1021 for st in util.statfiles([join(i) for i in visit]):
1022 1022 results[next(iv)] = st
1023 1023 return results
1024 1024
1025 1025 def status(self, match, subrepos, ignored, clean, unknown):
1026 1026 '''Determine the status of the working copy relative to the
1027 1027 dirstate and return a pair of (unsure, status), where status is of type
1028 1028 scmutil.status and:
1029 1029
1030 1030 unsure:
1031 1031 files that might have been modified since the dirstate was
1032 1032 written, but need to be read to be sure (size is the same
1033 1033 but mtime differs)
1034 1034 status.modified:
1035 1035 files that have definitely been modified since the dirstate
1036 1036 was written (different size or mode)
1037 1037 status.clean:
1038 1038 files that have definitely not been modified since the
1039 1039 dirstate was written
1040 1040 '''
1041 1041 listignored, listclean, listunknown = ignored, clean, unknown
1042 1042 lookup, modified, added, unknown, ignored = [], [], [], [], []
1043 1043 removed, deleted, clean = [], [], []
1044 1044
1045 1045 dmap = self._map
1046 1046 dmap.preload()
1047 1047 dcontains = dmap.__contains__
1048 1048 dget = dmap.__getitem__
1049 1049 ladd = lookup.append # aka "unsure"
1050 1050 madd = modified.append
1051 1051 aadd = added.append
1052 1052 uadd = unknown.append
1053 1053 iadd = ignored.append
1054 1054 radd = removed.append
1055 1055 dadd = deleted.append
1056 1056 cadd = clean.append
1057 1057 mexact = match.exact
1058 1058 dirignore = self._dirignore
1059 1059 checkexec = self._checkexec
1060 1060 copymap = self._map.copymap
1061 1061 lastnormaltime = self._lastnormaltime
1062 1062
1063 1063 # We need to do full walks when either
1064 1064 # - we're listing all clean files, or
1065 1065 # - match.traversedir does something, because match.traversedir should
1066 1066 # be called for every dir in the working dir
1067 1067 full = listclean or match.traversedir is not None
1068 1068 for fn, st in self.walk(match, subrepos, listunknown, listignored,
1069 1069 full=full).iteritems():
1070 1070 if not dcontains(fn):
1071 1071 if (listignored or mexact(fn)) and dirignore(fn):
1072 1072 if listignored:
1073 1073 iadd(fn)
1074 1074 else:
1075 1075 uadd(fn)
1076 1076 continue
1077 1077
1078 1078 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1079 1079 # written like that for performance reasons. dmap[fn] is not a
1080 1080 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1081 1081 # opcode has fast paths when the value to be unpacked is a tuple or
1082 1082 # a list, but falls back to creating a full-fledged iterator in
1083 1083 # general. That is much slower than simply accessing and storing the
1084 1084 # tuple members one by one.
1085 1085 t = dget(fn)
1086 1086 state = t[0]
1087 1087 mode = t[1]
1088 1088 size = t[2]
1089 1089 time = t[3]
1090 1090
1091 1091 if not st and state in "nma":
1092 1092 dadd(fn)
1093 1093 elif state == 'n':
1094 1094 if (size >= 0 and
1095 1095 ((size != st.st_size and size != st.st_size & _rangemask)
1096 1096 or ((mode ^ st.st_mode) & 0o100 and checkexec))
1097 1097 or size == -2 # other parent
1098 1098 or fn in copymap):
1099 1099 madd(fn)
1100 1100 elif (time != st[stat.ST_MTIME]
1101 1101 and time != st[stat.ST_MTIME] & _rangemask):
1102 1102 ladd(fn)
1103 1103 elif st[stat.ST_MTIME] == lastnormaltime:
1104 1104 # fn may have just been marked as normal and it may have
1105 1105 # changed in the same second without changing its size.
1106 1106 # This can happen if we quickly do multiple commits.
1107 1107 # Force lookup, so we don't miss such a racy file change.
1108 1108 ladd(fn)
1109 1109 elif listclean:
1110 1110 cadd(fn)
1111 1111 elif state == 'm':
1112 1112 madd(fn)
1113 1113 elif state == 'a':
1114 1114 aadd(fn)
1115 1115 elif state == 'r':
1116 1116 radd(fn)
1117 1117
1118 1118 return (lookup, scmutil.status(modified, added, removed, deleted,
1119 1119 unknown, ignored, clean))
1120 1120
1121 1121 def matches(self, match):
1122 1122 '''
1123 1123 return files in the dirstate (in whatever state) filtered by match
1124 1124 '''
1125 1125 dmap = self._map
1126 1126 if match.always():
1127 1127 return dmap.keys()
1128 1128 files = match.files()
1129 1129 if match.isexact():
1130 1130 # fast path -- filter the other way around, since typically files is
1131 1131 # much smaller than dmap
1132 1132 return [f for f in files if f in dmap]
1133 1133 if match.prefix() and all(fn in dmap for fn in files):
1134 1134 # fast path -- all the values are known to be files, so just return
1135 1135 # that
1136 1136 return list(files)
1137 1137 return [f for f in dmap if match(f)]
1138 1138
1139 1139 def _actualfilename(self, tr):
1140 1140 if tr:
1141 1141 return self._pendingfilename
1142 1142 else:
1143 1143 return self._filename
1144 1144
1145 1145 def savebackup(self, tr, backupname):
1146 1146 '''Save current dirstate into backup file'''
1147 1147 filename = self._actualfilename(tr)
1148 1148 assert backupname != filename
1149 1149
1150 1150 # use '_writedirstate' instead of 'write' to write changes certainly,
1151 1151 # because the latter omits writing out if transaction is running.
1152 1152 # output file will be used to create backup of dirstate at this point.
1153 1153 if self._dirty or not self._opener.exists(filename):
1154 1154 self._writedirstate(self._opener(filename, "w", atomictemp=True,
1155 1155 checkambig=True))
1156 1156
1157 1157 if tr:
1158 1158 # ensure that subsequent tr.writepending returns True for
1159 1159 # changes written out above, even if dirstate is never
1160 1160 # changed after this
1161 1161 tr.addfilegenerator('dirstate', (self._filename,),
1162 1162 self._writedirstate, location='plain')
1163 1163
1164 1164 # ensure that pending file written above is unlinked at
1165 1165 # failure, even if tr.writepending isn't invoked until the
1166 1166 # end of this transaction
1167 1167 tr.registertmp(filename, location='plain')
1168 1168
1169 1169 self._opener.tryunlink(backupname)
1170 1170 # hardlink backup is okay because _writedirstate is always called
1171 1171 # with an "atomictemp=True" file.
1172 1172 util.copyfile(self._opener.join(filename),
1173 1173 self._opener.join(backupname), hardlink=True)
1174 1174
1175 1175 def restorebackup(self, tr, backupname):
1176 1176 '''Restore dirstate by backup file'''
1177 1177 # this "invalidate()" prevents "wlock.release()" from writing
1178 1178 # changes of dirstate out after restoring from backup file
1179 1179 self.invalidate()
1180 1180 filename = self._actualfilename(tr)
1181 1181 o = self._opener
1182 1182 if util.samefile(o.join(backupname), o.join(filename)):
1183 1183 o.unlink(backupname)
1184 1184 else:
1185 1185 o.rename(backupname, filename, checkambig=True)
1186 1186
1187 1187 def clearbackup(self, tr, backupname):
1188 1188 '''Clear backup file'''
1189 1189 self._opener.unlink(backupname)
1190 1190
1191 1191 class dirstatemap(object):
1192 1192 """Map encapsulating the dirstate's contents.
1193 1193
1194 1194 The dirstate contains the following state:
1195 1195
1196 1196 - `identity` is the identity of the dirstate file, which can be used to
1197 1197 detect when changes have occurred to the dirstate file.
1198 1198
1199 1199 - `parents` is a pair containing the parents of the working copy. The
1200 1200 parents are updated by calling `setparents`.
1201 1201
1202 1202 - the state map maps filenames to tuples of (state, mode, size, mtime),
1203 1203 where state is a single character representing 'normal', 'added',
1204 1204 'removed', or 'merged'. It is read by treating the dirstate as a
1205 1205 dict. File state is updated by calling the `addfile`, `removefile` and
1206 1206 `dropfile` methods.
1207 1207
1208 1208 - `copymap` maps destination filenames to their source filename.
1209 1209
1210 1210 The dirstate also provides the following views onto the state:
1211 1211
1212 1212 - `nonnormalset` is a set of the filenames that have state other
1213 1213 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1214 1214
1215 1215 - `otherparentset` is a set of the filenames that are marked as coming
1216 1216 from the second parent when the dirstate is currently being merged.
1217 1217
1218 1218 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1219 1219 form that they appear as in the dirstate.
1220 1220
1221 1221 - `dirfoldmap` is a dict mapping normalized directory names to the
1222 1222 denormalized form that they appear as in the dirstate.
1223 1223 """
1224 1224
1225 1225 def __init__(self, ui, opener, root):
1226 1226 self._ui = ui
1227 1227 self._opener = opener
1228 1228 self._root = root
1229 1229 self._filename = 'dirstate'
1230 1230
1231 1231 self._parents = None
1232 1232 self._dirtyparents = False
1233 1233
1234 1234 # for consistent view between _pl() and _read() invocations
1235 1235 self._pendingmode = None
1236 1236
1237 1237 @propertycache
1238 1238 def _map(self):
1239 1239 self._map = {}
1240 1240 self.read()
1241 1241 return self._map
1242 1242
1243 1243 @propertycache
1244 1244 def copymap(self):
1245 1245 self.copymap = {}
1246 1246 self._map
1247 1247 return self.copymap
1248 1248
1249 1249 def clear(self):
1250 1250 self._map.clear()
1251 1251 self.copymap.clear()
1252 1252 self.setparents(nullid, nullid)
1253 1253 util.clearcachedproperty(self, "_dirs")
1254 1254 util.clearcachedproperty(self, "_alldirs")
1255 1255 util.clearcachedproperty(self, "filefoldmap")
1256 1256 util.clearcachedproperty(self, "dirfoldmap")
1257 1257 util.clearcachedproperty(self, "nonnormalset")
1258 1258 util.clearcachedproperty(self, "otherparentset")
1259 1259
1260 1260 def items(self):
1261 1261 return self._map.iteritems()
1262 1262
1263 1263 # forward for python2,3 compat
1264 1264 iteritems = items
1265 1265
1266 1266 def __len__(self):
1267 1267 return len(self._map)
1268 1268
1269 1269 def __iter__(self):
1270 1270 return iter(self._map)
1271 1271
1272 1272 def get(self, key, default=None):
1273 1273 return self._map.get(key, default)
1274 1274
1275 1275 def __contains__(self, key):
1276 1276 return key in self._map
1277 1277
1278 1278 def __getitem__(self, key):
1279 1279 return self._map[key]
1280 1280
1281 1281 def keys(self):
1282 1282 return self._map.keys()
1283 1283
1284 1284 def preload(self):
1285 1285 """Loads the underlying data, if it's not already loaded"""
1286 1286 self._map
1287 1287
1288 1288 def addfile(self, f, oldstate, state, mode, size, mtime):
1289 1289 """Add a tracked file to the dirstate."""
1290 1290 if oldstate in "?r" and r"_dirs" in self.__dict__:
1291 1291 self._dirs.addpath(f)
1292 1292 if oldstate == "?" and r"_alldirs" in self.__dict__:
1293 1293 self._alldirs.addpath(f)
1294 1294 self._map[f] = dirstatetuple(state, mode, size, mtime)
1295 1295 if state != 'n' or mtime == -1:
1296 1296 self.nonnormalset.add(f)
1297 1297 if size == -2:
1298 1298 self.otherparentset.add(f)
1299 1299
1300 1300 def removefile(self, f, oldstate, size):
1301 1301 """
1302 1302 Mark a file as removed in the dirstate.
1303 1303
1304 1304 The `size` parameter is used to store sentinel values that indicate
1305 1305 the file's previous state. In the future, we should refactor this
1306 1306 to be more explicit about what that state is.
1307 1307 """
1308 1308 if oldstate not in "?r" and r"_dirs" in self.__dict__:
1309 1309 self._dirs.delpath(f)
1310 1310 if oldstate == "?" and r"_alldirs" in self.__dict__:
1311 1311 self._alldirs.addpath(f)
1312 1312 if r"filefoldmap" in self.__dict__:
1313 1313 normed = util.normcase(f)
1314 1314 self.filefoldmap.pop(normed, None)
1315 1315 self._map[f] = dirstatetuple('r', 0, size, 0)
1316 1316 self.nonnormalset.add(f)
1317 1317
1318 1318 def dropfile(self, f, oldstate):
1319 1319 """
1320 1320 Remove a file from the dirstate. Returns True if the file was
1321 1321 previously recorded.
1322 1322 """
1323 1323 exists = self._map.pop(f, None) is not None
1324 1324 if exists:
1325 1325 if oldstate != "r" and r"_dirs" in self.__dict__:
1326 1326 self._dirs.delpath(f)
1327 1327 if r"_alldirs" in self.__dict__:
1328 1328 self._alldirs.delpath(f)
1329 1329 if r"filefoldmap" in self.__dict__:
1330 1330 normed = util.normcase(f)
1331 1331 self.filefoldmap.pop(normed, None)
1332 1332 self.nonnormalset.discard(f)
1333 1333 return exists
1334 1334
1335 1335 def clearambiguoustimes(self, files, now):
1336 1336 for f in files:
1337 1337 e = self.get(f)
1338 1338 if e is not None and e[0] == 'n' and e[3] == now:
1339 1339 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1340 1340 self.nonnormalset.add(f)
1341 1341
1342 1342 def nonnormalentries(self):
1343 1343 '''Compute the nonnormal dirstate entries from the dmap'''
1344 1344 try:
1345 1345 return parsers.nonnormalotherparententries(self._map)
1346 1346 except AttributeError:
1347 1347 nonnorm = set()
1348 1348 otherparent = set()
1349 1349 for fname, e in self._map.iteritems():
1350 1350 if e[0] != 'n' or e[3] == -1:
1351 1351 nonnorm.add(fname)
1352 1352 if e[0] == 'n' and e[2] == -2:
1353 1353 otherparent.add(fname)
1354 1354 return nonnorm, otherparent
1355 1355
1356 1356 @propertycache
1357 1357 def filefoldmap(self):
1358 1358 """Returns a dictionary mapping normalized case paths to their
1359 1359 non-normalized versions.
1360 1360 """
1361 1361 try:
1362 1362 makefilefoldmap = parsers.make_file_foldmap
1363 1363 except AttributeError:
1364 1364 pass
1365 1365 else:
1366 1366 return makefilefoldmap(self._map, util.normcasespec,
1367 1367 util.normcasefallback)
1368 1368
1369 1369 f = {}
1370 1370 normcase = util.normcase
1371 1371 for name, s in self._map.iteritems():
1372 1372 if s[0] != 'r':
1373 1373 f[normcase(name)] = name
1374 1374 f['.'] = '.' # prevents useless util.fspath() invocation
1375 1375 return f
1376 1376
1377 1377 def hastrackeddir(self, d):
1378 1378 """
1379 1379 Returns True if the dirstate contains a tracked (not removed) file
1380 1380 in this directory.
1381 1381 """
1382 1382 return d in self._dirs
1383 1383
1384 1384 def hasdir(self, d):
1385 1385 """
1386 1386 Returns True if the dirstate contains a file (tracked or removed)
1387 1387 in this directory.
1388 1388 """
1389 1389 return d in self._alldirs
1390 1390
1391 1391 @propertycache
1392 1392 def _dirs(self):
1393 1393 return util.dirs(self._map, 'r')
1394 1394
1395 1395 @propertycache
1396 1396 def _alldirs(self):
1397 1397 return util.dirs(self._map)
1398 1398
1399 1399 def _opendirstatefile(self):
1400 1400 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1401 1401 if self._pendingmode is not None and self._pendingmode != mode:
1402 1402 fp.close()
1403 1403 raise error.Abort(_('working directory state may be '
1404 1404 'changed parallelly'))
1405 1405 self._pendingmode = mode
1406 1406 return fp
1407 1407
1408 1408 def parents(self):
1409 1409 if not self._parents:
1410 1410 try:
1411 1411 fp = self._opendirstatefile()
1412 1412 st = fp.read(40)
1413 1413 fp.close()
1414 1414 except IOError as err:
1415 1415 if err.errno != errno.ENOENT:
1416 1416 raise
1417 1417 # File doesn't exist, so the current state is empty
1418 1418 st = ''
1419 1419
1420 1420 l = len(st)
1421 1421 if l == 40:
1422 1422 self._parents = (st[:20], st[20:40])
1423 1423 elif l == 0:
1424 1424 self._parents = (nullid, nullid)
1425 1425 else:
1426 1426 raise error.Abort(_('working directory state appears '
1427 1427 'damaged!'))
1428 1428
1429 1429 return self._parents
1430 1430
1431 1431 def setparents(self, p1, p2):
1432 1432 self._parents = (p1, p2)
1433 1433 self._dirtyparents = True
1434 1434
1435 1435 def read(self):
1436 1436 # ignore HG_PENDING because identity is used only for writing
1437 1437 self.identity = util.filestat.frompath(
1438 1438 self._opener.join(self._filename))
1439 1439
1440 1440 try:
1441 1441 fp = self._opendirstatefile()
1442 1442 try:
1443 1443 st = fp.read()
1444 1444 finally:
1445 1445 fp.close()
1446 1446 except IOError as err:
1447 1447 if err.errno != errno.ENOENT:
1448 1448 raise
1449 1449 return
1450 1450 if not st:
1451 1451 return
1452 1452
1453 1453 if util.safehasattr(parsers, 'dict_new_presized'):
1454 1454 # Make an estimate of the number of files in the dirstate based on
1455 1455 # its size. From a linear regression on a set of real-world repos,
1456 1456 # all over 10,000 files, the size of a dirstate entry is 85
1457 1457 # bytes. The cost of resizing is significantly higher than the cost
1458 1458 # of filling in a larger presized dict, so subtract 20% from the
1459 1459 # size.
1460 1460 #
1461 1461 # This heuristic is imperfect in many ways, so in a future dirstate
1462 1462 # format update it makes sense to just record the number of entries
1463 1463 # on write.
1464 1464 self._map = parsers.dict_new_presized(len(st) // 71)
1465 1465
1466 1466 # Python's garbage collector triggers a GC each time a certain number
1467 1467 # of container objects (the number being defined by
1468 1468 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1469 1469 # for each file in the dirstate. The C version then immediately marks
1470 1470 # them as not to be tracked by the collector. However, this has no
1471 1471 # effect on when GCs are triggered, only on what objects the GC looks
1472 1472 # into. This means that O(number of files) GCs are unavoidable.
1473 1473 # Depending on when in the process's lifetime the dirstate is parsed,
1474 1474 # this can get very expensive. As a workaround, disable GC while
1475 1475 # parsing the dirstate.
1476 1476 #
1477 1477 # (we cannot decorate the function directly since it is in a C module)
1478 parse_dirstate = util.nogc(dirstatemod.parse_dirstate)
1478 parse_dirstate = util.nogc(parsers.parse_dirstate)
1479 1479 p = parse_dirstate(self._map, self.copymap, st)
1480 1480 if not self._dirtyparents:
1481 1481 self.setparents(*p)
1482 1482
1483 1483 # Avoid excess attribute lookups by fast pathing certain checks
1484 1484 self.__contains__ = self._map.__contains__
1485 1485 self.__getitem__ = self._map.__getitem__
1486 1486 self.get = self._map.get
1487 1487
1488 1488 def write(self, st, now):
1489 st.write(dirstatemod.pack_dirstate(self._map, self.copymap,
1490 self.parents(), now))
1489 st.write(parsers.pack_dirstate(self._map, self.copymap,
1490 self.parents(), now))
1491 1491 st.close()
1492 1492 self._dirtyparents = False
1493 1493 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1494 1494
1495 1495 @propertycache
1496 1496 def nonnormalset(self):
1497 1497 nonnorm, otherparents = self.nonnormalentries()
1498 1498 self.otherparentset = otherparents
1499 1499 return nonnorm
1500 1500
1501 1501 @propertycache
1502 1502 def otherparentset(self):
1503 1503 nonnorm, otherparents = self.nonnormalentries()
1504 1504 self.nonnormalset = nonnorm
1505 1505 return otherparents
1506 1506
1507 1507 @propertycache
1508 1508 def identity(self):
1509 1509 self._map
1510 1510 return self.identity
1511 1511
1512 1512 @propertycache
1513 1513 def dirfoldmap(self):
1514 1514 f = {}
1515 1515 normcase = util.normcase
1516 1516 for name in self._dirs:
1517 1517 f[normcase(name)] = name
1518 1518 return f
@@ -1,239 +1,100 b''
1 1 // dirstate.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
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
8 8 //! Bindings for the `hg::dirstate` module provided by the
9 9 //! `hg-core` package.
10 10 //!
11 11 //! From Python, this will be seen as `mercurial.rustext.dirstate`
12 12 mod dirs_multiset;
13 13 use crate::dirstate::dirs_multiset::Dirs;
14 14 use cpython::{
15 exc, PyBytes, PyDict, PyErr, PyInt, PyModule, PyObject, PyResult,
16 PySequence, PyTuple, Python, PythonObject, ToPyObject,
15 PyBytes, PyDict, PyErr, PyModule, PyObject, PyResult, PySequence, Python,
17 16 };
18 use hg::{
19 pack_dirstate, parse_dirstate, CopyVecEntry, DirstateEntry,
20 DirstatePackError, DirstateParents, DirstateParseError, DirstateVec,
21 };
17 use hg::{DirstateEntry, DirstateVec};
22 18 use libc::{c_char, c_int};
23 19 #[cfg(feature = "python27")]
24 20 use python27_sys::PyCapsule_Import;
25 21 #[cfg(feature = "python3")]
26 22 use python3_sys::PyCapsule_Import;
27 use std::collections::HashMap;
28 23 use std::ffi::CStr;
29 24 use std::mem::transmute;
30 25
31 26 /// C code uses a custom `dirstate_tuple` type, checks in multiple instances
32 27 /// for this type, and raises a Python `Exception` if the check does not pass.
33 28 /// Because this type differs only in name from the regular Python tuple, it
34 29 /// would be a good idea in the near future to remove it entirely to allow
35 30 /// for a pure Python tuple of the same effective structure to be used,
36 31 /// rendering this type and the capsule below useless.
37 32 type MakeDirstateTupleFn = extern "C" fn(
38 33 state: c_char,
39 34 mode: c_int,
40 35 size: c_int,
41 36 mtime: c_int,
42 37 ) -> PyObject;
43 38
44 39 /// This is largely a copy/paste from cindex.rs, pending the merge of a
45 40 /// `py_capsule_fn!` macro in the rust-cpython project:
46 41 /// https://github.com/dgrunwald/rust-cpython/pull/169
47 fn decapsule_make_dirstate_tuple(py: Python) -> PyResult<MakeDirstateTupleFn> {
42 pub fn decapsule_make_dirstate_tuple(
43 py: Python,
44 ) -> PyResult<MakeDirstateTupleFn> {
48 45 unsafe {
49 46 let caps_name = CStr::from_bytes_with_nul_unchecked(
50 47 b"mercurial.cext.parsers.make_dirstate_tuple_CAPI\0",
51 48 );
52 49 let from_caps = PyCapsule_Import(caps_name.as_ptr(), 0);
53 50 if from_caps.is_null() {
54 51 return Err(PyErr::fetch(py));
55 52 }
56 53 Ok(transmute(from_caps))
57 54 }
58 55 }
59 56
60 fn parse_dirstate_wrapper(
61 py: Python,
62 dmap: PyDict,
63 copymap: PyDict,
64 st: PyBytes,
65 ) -> PyResult<PyTuple> {
66 match parse_dirstate(st.data(py)) {
67 Ok((parents, dirstate_vec, copies)) => {
68 for (filename, entry) in dirstate_vec {
69 dmap.set_item(
70 py,
71 PyBytes::new(py, &filename[..]),
72 decapsule_make_dirstate_tuple(py)?(
73 entry.state as c_char,
74 entry.mode,
75 entry.size,
76 entry.mtime,
77 ),
78 )?;
79 }
80 for CopyVecEntry { path, copy_path } in copies {
81 copymap.set_item(
82 py,
83 PyBytes::new(py, path),
84 PyBytes::new(py, copy_path),
85 )?;
86 }
87 Ok((PyBytes::new(py, parents.p1), PyBytes::new(py, parents.p2))
88 .to_py_object(py))
89 }
90 Err(e) => Err(PyErr::new::<exc::ValueError, _>(
91 py,
92 match e {
93 DirstateParseError::TooLittleData => {
94 "too little data for parents".to_string()
95 }
96 DirstateParseError::Overflow => {
97 "overflow in dirstate".to_string()
98 }
99 DirstateParseError::CorruptedEntry(e) => e,
100 },
101 )),
102 }
103 }
104
105 fn extract_dirstate_vec(
57 pub fn extract_dirstate_vec(
106 58 py: Python,
107 59 dmap: &PyDict,
108 60 ) -> Result<DirstateVec, PyErr> {
109 61 dmap.items(py)
110 62 .iter()
111 63 .map(|(filename, stats)| {
112 64 let stats = stats.extract::<PySequence>(py)?;
113 65 let state = stats.get_item(py, 0)?.extract::<PyBytes>(py)?;
114 66 let state = state.data(py)[0] as i8;
115 67 let mode = stats.get_item(py, 1)?.extract(py)?;
116 68 let size = stats.get_item(py, 2)?.extract(py)?;
117 69 let mtime = stats.get_item(py, 3)?.extract(py)?;
118 70 let filename = filename.extract::<PyBytes>(py)?;
119 71 let filename = filename.data(py);
120 72 Ok((
121 73 filename.to_owned(),
122 74 DirstateEntry {
123 75 state,
124 76 mode,
125 77 size,
126 78 mtime,
127 79 },
128 80 ))
129 81 })
130 82 .collect()
131 83 }
132 84
133 fn pack_dirstate_wrapper(
134 py: Python,
135 dmap: PyDict,
136 copymap: PyDict,
137 pl: PyTuple,
138 now: PyInt,
139 ) -> PyResult<PyBytes> {
140 let p1 = pl.get_item(py, 0).extract::<PyBytes>(py)?;
141 let p1: &[u8] = p1.data(py);
142 let p2 = pl.get_item(py, 1).extract::<PyBytes>(py)?;
143 let p2: &[u8] = p2.data(py);
144
145 let dirstate_vec = extract_dirstate_vec(py, &dmap)?;
146
147 let copies: Result<HashMap<Vec<u8>, Vec<u8>>, PyErr> = copymap
148 .items(py)
149 .iter()
150 .map(|(key, value)| {
151 Ok((
152 key.extract::<PyBytes>(py)?.data(py).to_owned(),
153 value.extract::<PyBytes>(py)?.data(py).to_owned(),
154 ))
155 })
156 .collect();
157
158 match pack_dirstate(
159 &dirstate_vec,
160 &copies?,
161 DirstateParents { p1, p2 },
162 now.as_object().extract::<i32>(py)?,
163 ) {
164 Ok((packed, new_dirstate_vec)) => {
165 for (
166 filename,
167 DirstateEntry {
168 state,
169 mode,
170 size,
171 mtime,
172 },
173 ) in new_dirstate_vec
174 {
175 dmap.set_item(
176 py,
177 PyBytes::new(py, &filename[..]),
178 decapsule_make_dirstate_tuple(py)?(
179 state as c_char,
180 mode,
181 size,
182 mtime,
183 ),
184 )?;
185 }
186 Ok(PyBytes::new(py, &packed))
187 }
188 Err(error) => Err(PyErr::new::<exc::ValueError, _>(
189 py,
190 match error {
191 DirstatePackError::CorruptedParent => {
192 "expected a 20-byte hash".to_string()
193 }
194 DirstatePackError::CorruptedEntry(e) => e,
195 DirstatePackError::BadSize(expected, actual) => {
196 format!("bad dirstate size: {} != {}", actual, expected)
197 }
198 },
199 )),
200 }
201 }
202
203 85 /// Create the module, with `__package__` given from parent
204 86 pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
205 87 let dotted_name = &format!("{}.dirstate", package);
206 88 let m = PyModule::new(py, dotted_name)?;
207 89
208 90 m.add(py, "__package__", package)?;
209 91 m.add(py, "__doc__", "Dirstate - Rust implementation")?;
210 m.add(
211 py,
212 "parse_dirstate",
213 py_fn!(
214 py,
215 parse_dirstate_wrapper(dmap: PyDict, copymap: PyDict, st: PyBytes)
216 ),
217 )?;
218 m.add(
219 py,
220 "pack_dirstate",
221 py_fn!(
222 py,
223 pack_dirstate_wrapper(
224 dmap: PyDict,
225 copymap: PyDict,
226 pl: PyTuple,
227 now: PyInt
228 )
229 ),
230 )?;
231 92
232 93 m.add_class::<Dirs>(py)?;
233 94
234 95 let sys = PyModule::import(py, "sys")?;
235 96 let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
236 97 sys_modules.set_item(py, dotted_name, &m)?;
237 98
238 99 Ok(m)
239 100 }
@@ -1,65 +1,71 b''
1 1 // lib.rs
2 2 //
3 3 // Copyright 2018 Georges Racinet <gracinet@anybox.fr>
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
8 8 //! Python bindings of `hg-core` objects using the `cpython` crate.
9 9 //! Once compiled, the resulting single shared library object can be placed in
10 10 //! the `mercurial` package directly as `rustext.so` or `rustext.dll`.
11 11 //! It holds several modules, so that from the point of view of Python,
12 12 //! it behaves as the `cext` package.
13 13 //!
14 14 //! Example:
15 15 //!
16 16 //! ```text
17 17 //! >>> from mercurial.rustext import ancestor
18 18 //! >>> ancestor.__doc__
19 19 //! 'Generic DAG ancestor algorithms - Rust implementation'
20 20 //! ```
21 21
22 22 /// This crate uses nested private macros, `extern crate` is still needed in
23 23 /// 2018 edition.
24 24 #[macro_use]
25 25 extern crate cpython;
26 26
27 27 pub mod ancestors;
28 28 mod cindex;
29 29 mod conversion;
30 30 pub mod dagops;
31 31 pub mod dirstate;
32 pub mod parsers;
32 33 pub mod discovery;
33 34 pub mod exceptions;
34 35 pub mod filepatterns;
35 36
36 37 py_module_initializer!(rustext, initrustext, PyInit_rustext, |py, m| {
37 38 m.add(
38 39 py,
39 40 "__doc__",
40 41 "Mercurial core concepts - Rust implementation",
41 42 )?;
42 43
43 44 let dotted_name: String = m.get(py, "__name__")?.extract(py)?;
44 45 m.add(py, "ancestor", ancestors::init_module(py, &dotted_name)?)?;
45 46 m.add(py, "dagop", dagops::init_module(py, &dotted_name)?)?;
46 47 m.add(py, "discovery", discovery::init_module(py, &dotted_name)?)?;
47 48 m.add(py, "dirstate", dirstate::init_module(py, &dotted_name)?)?;
48 49 m.add(
49 50 py,
50 51 "filepatterns",
51 52 filepatterns::init_module(py, &dotted_name)?,
52 53 )?;
54 m.add(
55 py,
56 "parsers",
57 parsers::init_parsers_module(py, &dotted_name)?,
58 )?;
53 59 m.add(py, "GraphError", py.get_type::<exceptions::GraphError>())?;
54 60 m.add(
55 61 py,
56 62 "PatternFileError",
57 63 py.get_type::<exceptions::PatternFileError>(),
58 64 )?;
59 65 m.add(
60 66 py,
61 67 "PatternError",
62 68 py.get_type::<exceptions::PatternError>(),
63 69 )?;
64 70 Ok(())
65 71 });
@@ -1,239 +1,177 b''
1 // dirstate.rs
1 // parsers.rs
2 2 //
3 3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
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
8 //! Bindings for the `hg::dirstate` module provided by the
8 //! Bindings for the `hg::dirstate::parsers` module provided by the
9 9 //! `hg-core` package.
10 10 //!
11 //! From Python, this will be seen as `mercurial.rustext.dirstate`
12 mod dirs_multiset;
13 use crate::dirstate::dirs_multiset::Dirs;
11 //! From Python, this will be seen as `mercurial.rustext.parsers`
12 //!
14 13 use cpython::{
15 exc, PyBytes, PyDict, PyErr, PyInt, PyModule, PyObject, PyResult,
16 PySequence, PyTuple, Python, PythonObject, ToPyObject,
14 exc, PyBytes, PyDict, PyErr, PyInt, PyModule, PyResult, PyTuple, Python,
15 PythonObject, ToPyObject,
17 16 };
18 17 use hg::{
19 18 pack_dirstate, parse_dirstate, CopyVecEntry, DirstateEntry,
20 DirstatePackError, DirstateParents, DirstateParseError, DirstateVec,
19 DirstatePackError, DirstateParents, DirstateParseError,
21 20 };
22 use libc::{c_char, c_int};
23 #[cfg(feature = "python27")]
24 use python27_sys::PyCapsule_Import;
25 #[cfg(feature = "python3")]
26 use python3_sys::PyCapsule_Import;
27 21 use std::collections::HashMap;
28 use std::ffi::CStr;
29 use std::mem::transmute;
30 22
31 /// C code uses a custom `dirstate_tuple` type, checks in multiple instances
32 /// for this type, and raises a Python `Exception` if the check does not pass.
33 /// Because this type differs only in name from the regular Python tuple, it
34 /// would be a good idea in the near future to remove it entirely to allow
35 /// for a pure Python tuple of the same effective structure to be used,
36 /// rendering this type and the capsule below useless.
37 type MakeDirstateTupleFn = extern "C" fn(
38 state: c_char,
39 mode: c_int,
40 size: c_int,
41 mtime: c_int,
42 ) -> PyObject;
23 use libc::c_char;
43 24
44 /// This is largely a copy/paste from cindex.rs, pending the merge of a
45 /// `py_capsule_fn!` macro in the rust-cpython project:
46 /// https://github.com/dgrunwald/rust-cpython/pull/169
47 fn decapsule_make_dirstate_tuple(py: Python) -> PyResult<MakeDirstateTupleFn> {
48 unsafe {
49 let caps_name = CStr::from_bytes_with_nul_unchecked(
50 b"mercurial.cext.parsers.make_dirstate_tuple_CAPI\0",
51 );
52 let from_caps = PyCapsule_Import(caps_name.as_ptr(), 0);
53 if from_caps.is_null() {
54 return Err(PyErr::fetch(py));
55 }
56 Ok(transmute(from_caps))
57 }
58 }
25 use crate::dirstate::{decapsule_make_dirstate_tuple, extract_dirstate_vec};
59 26
60 27 fn parse_dirstate_wrapper(
61 28 py: Python,
62 29 dmap: PyDict,
63 30 copymap: PyDict,
64 31 st: PyBytes,
65 32 ) -> PyResult<PyTuple> {
66 33 match parse_dirstate(st.data(py)) {
67 34 Ok((parents, dirstate_vec, copies)) => {
68 35 for (filename, entry) in dirstate_vec {
69 36 dmap.set_item(
70 37 py,
71 38 PyBytes::new(py, &filename[..]),
72 39 decapsule_make_dirstate_tuple(py)?(
73 40 entry.state as c_char,
74 41 entry.mode,
75 42 entry.size,
76 43 entry.mtime,
77 44 ),
78 45 )?;
79 46 }
80 47 for CopyVecEntry { path, copy_path } in copies {
81 48 copymap.set_item(
82 49 py,
83 50 PyBytes::new(py, path),
84 51 PyBytes::new(py, copy_path),
85 52 )?;
86 53 }
87 54 Ok((PyBytes::new(py, parents.p1), PyBytes::new(py, parents.p2))
88 55 .to_py_object(py))
89 56 }
90 57 Err(e) => Err(PyErr::new::<exc::ValueError, _>(
91 58 py,
92 59 match e {
93 60 DirstateParseError::TooLittleData => {
94 61 "too little data for parents".to_string()
95 62 }
96 63 DirstateParseError::Overflow => {
97 64 "overflow in dirstate".to_string()
98 65 }
99 66 DirstateParseError::CorruptedEntry(e) => e,
100 67 },
101 68 )),
102 69 }
103 70 }
104 71
105 fn extract_dirstate_vec(
106 py: Python,
107 dmap: &PyDict,
108 ) -> Result<DirstateVec, PyErr> {
109 dmap.items(py)
110 .iter()
111 .map(|(filename, stats)| {
112 let stats = stats.extract::<PySequence>(py)?;
113 let state = stats.get_item(py, 0)?.extract::<PyBytes>(py)?;
114 let state = state.data(py)[0] as i8;
115 let mode = stats.get_item(py, 1)?.extract(py)?;
116 let size = stats.get_item(py, 2)?.extract(py)?;
117 let mtime = stats.get_item(py, 3)?.extract(py)?;
118 let filename = filename.extract::<PyBytes>(py)?;
119 let filename = filename.data(py);
120 Ok((
121 filename.to_owned(),
122 DirstateEntry {
123 state,
124 mode,
125 size,
126 mtime,
127 },
128 ))
129 })
130 .collect()
131 }
132
133 72 fn pack_dirstate_wrapper(
134 73 py: Python,
135 74 dmap: PyDict,
136 75 copymap: PyDict,
137 76 pl: PyTuple,
138 77 now: PyInt,
139 78 ) -> PyResult<PyBytes> {
140 79 let p1 = pl.get_item(py, 0).extract::<PyBytes>(py)?;
141 80 let p1: &[u8] = p1.data(py);
142 81 let p2 = pl.get_item(py, 1).extract::<PyBytes>(py)?;
143 82 let p2: &[u8] = p2.data(py);
144 83
145 84 let dirstate_vec = extract_dirstate_vec(py, &dmap)?;
146 85
147 86 let copies: Result<HashMap<Vec<u8>, Vec<u8>>, PyErr> = copymap
148 87 .items(py)
149 88 .iter()
150 89 .map(|(key, value)| {
151 90 Ok((
152 91 key.extract::<PyBytes>(py)?.data(py).to_owned(),
153 92 value.extract::<PyBytes>(py)?.data(py).to_owned(),
154 93 ))
155 94 })
156 95 .collect();
157 96
158 97 match pack_dirstate(
159 98 &dirstate_vec,
160 99 &copies?,
161 100 DirstateParents { p1, p2 },
162 101 now.as_object().extract::<i32>(py)?,
163 102 ) {
164 103 Ok((packed, new_dirstate_vec)) => {
165 104 for (
166 105 filename,
167 106 DirstateEntry {
168 107 state,
169 108 mode,
170 109 size,
171 110 mtime,
172 111 },
173 112 ) in new_dirstate_vec
174 113 {
175 114 dmap.set_item(
176 115 py,
177 116 PyBytes::new(py, &filename[..]),
178 117 decapsule_make_dirstate_tuple(py)?(
179 118 state as c_char,
180 119 mode,
181 120 size,
182 121 mtime,
183 122 ),
184 123 )?;
185 124 }
186 125 Ok(PyBytes::new(py, &packed))
187 126 }
188 127 Err(error) => Err(PyErr::new::<exc::ValueError, _>(
189 128 py,
190 129 match error {
191 130 DirstatePackError::CorruptedParent => {
192 131 "expected a 20-byte hash".to_string()
193 132 }
194 133 DirstatePackError::CorruptedEntry(e) => e,
195 134 DirstatePackError::BadSize(expected, actual) => {
196 135 format!("bad dirstate size: {} != {}", actual, expected)
197 136 }
198 137 },
199 138 )),
200 139 }
201 140 }
202 141
203 142 /// Create the module, with `__package__` given from parent
204 pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
205 let dotted_name = &format!("{}.dirstate", package);
143 pub fn init_parsers_module(py: Python, package: &str) -> PyResult<PyModule> {
144 let dotted_name = &format!("{}.parsers", package);
206 145 let m = PyModule::new(py, dotted_name)?;
207 146
208 147 m.add(py, "__package__", package)?;
209 m.add(py, "__doc__", "Dirstate - Rust implementation")?;
148 m.add(py, "__doc__", "Parsers - Rust implementation")?;
149
210 150 m.add(
211 151 py,
212 152 "parse_dirstate",
213 153 py_fn!(
214 154 py,
215 155 parse_dirstate_wrapper(dmap: PyDict, copymap: PyDict, st: PyBytes)
216 156 ),
217 157 )?;
218 158 m.add(
219 159 py,
220 160 "pack_dirstate",
221 161 py_fn!(
222 162 py,
223 163 pack_dirstate_wrapper(
224 164 dmap: PyDict,
225 165 copymap: PyDict,
226 166 pl: PyTuple,
227 167 now: PyInt
228 168 )
229 169 ),
230 170 )?;
231 171
232 m.add_class::<Dirs>(py)?;
233
234 172 let sys = PyModule::import(py, "sys")?;
235 173 let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
236 174 sys_modules.set_item(py, dotted_name, &m)?;
237 175
238 176 Ok(m)
239 177 }
@@ -1,90 +1,98 b''
1 1 # extension to emulate invoking 'dirstate.write()' at the time
2 2 # specified by '[fakedirstatewritetime] fakenow', only when
3 3 # 'dirstate.write()' is invoked via functions below:
4 4 #
5 5 # - 'workingctx._poststatusfixup()' (= 'repo.status()')
6 6 # - 'committablectx.markcommitted()'
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from mercurial import (
11 11 context,
12 12 dirstate,
13 13 extensions,
14 14 policy,
15 15 registrar,
16 16 )
17 17 from mercurial.utils import dateutil
18 18
19 19 try:
20 20 from mercurial import rustext
21 21 rustext.__name__ # force actual import (see hgdemandimport)
22 22 except ImportError:
23 23 rustext = None
24 24
25 25 configtable = {}
26 26 configitem = registrar.configitem(configtable)
27 27
28 28 configitem(b'fakedirstatewritetime', b'fakenow',
29 29 default=None,
30 30 )
31 31
32 32 parsers = policy.importmod(r'parsers')
33 rustmod = policy.importrust(r'parsers')
33 34
34 35 def pack_dirstate(fakenow, orig, dmap, copymap, pl, now):
35 36 # execute what original parsers.pack_dirstate should do actually
36 37 # for consistency
37 38 actualnow = int(now)
38 39 for f, e in dmap.items():
39 40 if e[0] == 'n' and e[3] == actualnow:
40 41 e = parsers.dirstatetuple(e[0], e[1], e[2], -1)
41 42 dmap[f] = e
42 43
43 44 return orig(dmap, copymap, pl, fakenow)
44 45
45 46 def fakewrite(ui, func):
46 47 # fake "now" of 'pack_dirstate' only if it is invoked while 'func'
47 48
48 49 fakenow = ui.config(b'fakedirstatewritetime', b'fakenow')
49 50 if not fakenow:
50 51 # Execute original one, if fakenow isn't configured. This is
51 52 # useful to prevent subrepos from executing replaced one,
52 53 # because replacing 'parsers.pack_dirstate' is also effective
53 54 # in subrepos.
54 55 return func()
55 56
56 57 # parsing 'fakenow' in YYYYmmddHHMM format makes comparison between
57 58 # 'fakenow' value and 'touch -t YYYYmmddHHMM' argument easy
58 59 fakenow = dateutil.parsedate(fakenow, [b'%Y%m%d%H%M'])[0]
59 60
60 if rustext is not None:
61 orig_module = rustext.dirstate
62 orig_pack_dirstate = rustext.dirstate.pack_dirstate
63 else:
64 orig_module = parsers
65 orig_pack_dirstate = parsers.pack_dirstate
61 if rustmod is not None:
62 # The Rust implementation does not use public parse/pack dirstate
63 # to prevent conversion round-trips
64 orig_dirstatemap_write = dirstate.dirstatemap.write
65 wrapper = lambda self, st, now: orig_dirstatemap_write(self,
66 st,
67 fakenow)
68 dirstate.dirstatemap.write = wrapper
66 69
67 70 orig_dirstate_getfsnow = dirstate._getfsnow
68 71 wrapper = lambda *args: pack_dirstate(fakenow, orig_pack_dirstate, *args)
69 72
73 orig_module = parsers
74 orig_pack_dirstate = parsers.pack_dirstate
75
70 76 orig_module.pack_dirstate = wrapper
71 77 dirstate._getfsnow = lambda *args: fakenow
72 78 try:
73 79 return func()
74 80 finally:
75 81 orig_module.pack_dirstate = orig_pack_dirstate
76 82 dirstate._getfsnow = orig_dirstate_getfsnow
83 if rustmod is not None:
84 dirstate.dirstatemap.write = orig_dirstatemap_write
77 85
78 86 def _poststatusfixup(orig, workingctx, status, fixup):
79 87 ui = workingctx.repo().ui
80 88 return fakewrite(ui, lambda : orig(workingctx, status, fixup))
81 89
82 90 def markcommitted(orig, committablectx, node):
83 91 ui = committablectx.repo().ui
84 92 return fakewrite(ui, lambda : orig(committablectx, node))
85 93
86 94 def extsetup(ui):
87 95 extensions.wrapfunction(context.workingctx, '_poststatusfixup',
88 96 _poststatusfixup)
89 97 extensions.wrapfunction(context.workingctx, 'markcommitted',
90 98 markcommitted)
General Comments 0
You need to be logged in to leave comments. Login now