##// END OF EJS Templates
dirstate: add a `set_tracked` method for "hg add"-like usage...
marmoute -
r48393:f927ad5a default
parent child Browse files
Show More
@@ -1,784 +1,787 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''largefiles utility code: must not import other modules in this package.'''
10 10 from __future__ import absolute_import
11 11
12 12 import contextlib
13 13 import copy
14 14 import os
15 15 import stat
16 16
17 17 from mercurial.i18n import _
18 18 from mercurial.node import hex
19 19 from mercurial.pycompat import open
20 20
21 21 from mercurial import (
22 22 dirstate,
23 23 encoding,
24 24 error,
25 25 httpconnection,
26 26 match as matchmod,
27 27 pycompat,
28 28 requirements,
29 29 scmutil,
30 30 sparse,
31 31 util,
32 32 vfs as vfsmod,
33 33 )
34 34 from mercurial.utils import hashutil
35 35
36 36 shortname = b'.hglf'
37 37 shortnameslash = shortname + b'/'
38 38 longname = b'largefiles'
39 39
40 40 # -- Private worker functions ------------------------------------------
41 41
42 42
43 43 @contextlib.contextmanager
44 44 def lfstatus(repo, value=True):
45 45 oldvalue = getattr(repo, 'lfstatus', False)
46 46 repo.lfstatus = value
47 47 try:
48 48 yield
49 49 finally:
50 50 repo.lfstatus = oldvalue
51 51
52 52
53 53 def getminsize(ui, assumelfiles, opt, default=10):
54 54 lfsize = opt
55 55 if not lfsize and assumelfiles:
56 56 lfsize = ui.config(longname, b'minsize', default=default)
57 57 if lfsize:
58 58 try:
59 59 lfsize = float(lfsize)
60 60 except ValueError:
61 61 raise error.Abort(
62 62 _(b'largefiles: size must be number (not %s)\n') % lfsize
63 63 )
64 64 if lfsize is None:
65 65 raise error.Abort(_(b'minimum size for largefiles must be specified'))
66 66 return lfsize
67 67
68 68
69 69 def link(src, dest):
70 70 """Try to create hardlink - if that fails, efficiently make a copy."""
71 71 util.makedirs(os.path.dirname(dest))
72 72 try:
73 73 util.oslink(src, dest)
74 74 except OSError:
75 75 # if hardlinks fail, fallback on atomic copy
76 76 with open(src, b'rb') as srcf, util.atomictempfile(dest) as dstf:
77 77 for chunk in util.filechunkiter(srcf):
78 78 dstf.write(chunk)
79 79 os.chmod(dest, os.stat(src).st_mode)
80 80
81 81
82 82 def usercachepath(ui, hash):
83 83 """Return the correct location in the "global" largefiles cache for a file
84 84 with the given hash.
85 85 This cache is used for sharing of largefiles across repositories - both
86 86 to preserve download bandwidth and storage space."""
87 87 return os.path.join(_usercachedir(ui), hash)
88 88
89 89
90 90 def _usercachedir(ui, name=longname):
91 91 '''Return the location of the "global" largefiles cache.'''
92 92 path = ui.configpath(name, b'usercache')
93 93 if path:
94 94 return path
95 95
96 96 hint = None
97 97
98 98 if pycompat.iswindows:
99 99 appdata = encoding.environ.get(
100 100 b'LOCALAPPDATA', encoding.environ.get(b'APPDATA')
101 101 )
102 102 if appdata:
103 103 return os.path.join(appdata, name)
104 104
105 105 hint = _(b"define %s or %s in the environment, or set %s.usercache") % (
106 106 b"LOCALAPPDATA",
107 107 b"APPDATA",
108 108 name,
109 109 )
110 110 elif pycompat.isdarwin:
111 111 home = encoding.environ.get(b'HOME')
112 112 if home:
113 113 return os.path.join(home, b'Library', b'Caches', name)
114 114
115 115 hint = _(b"define %s in the environment, or set %s.usercache") % (
116 116 b"HOME",
117 117 name,
118 118 )
119 119 elif pycompat.isposix:
120 120 path = encoding.environ.get(b'XDG_CACHE_HOME')
121 121 if path:
122 122 return os.path.join(path, name)
123 123 home = encoding.environ.get(b'HOME')
124 124 if home:
125 125 return os.path.join(home, b'.cache', name)
126 126
127 127 hint = _(b"define %s or %s in the environment, or set %s.usercache") % (
128 128 b"XDG_CACHE_HOME",
129 129 b"HOME",
130 130 name,
131 131 )
132 132 else:
133 133 raise error.Abort(
134 134 _(b'unknown operating system: %s\n') % pycompat.osname
135 135 )
136 136
137 137 raise error.Abort(_(b'unknown %s usercache location') % name, hint=hint)
138 138
139 139
140 140 def inusercache(ui, hash):
141 141 path = usercachepath(ui, hash)
142 142 return os.path.exists(path)
143 143
144 144
145 145 def findfile(repo, hash):
146 146 """Return store path of the largefile with the specified hash.
147 147 As a side effect, the file might be linked from user cache.
148 148 Return None if the file can't be found locally."""
149 149 path, exists = findstorepath(repo, hash)
150 150 if exists:
151 151 repo.ui.note(_(b'found %s in store\n') % hash)
152 152 return path
153 153 elif inusercache(repo.ui, hash):
154 154 repo.ui.note(_(b'found %s in system cache\n') % hash)
155 155 path = storepath(repo, hash)
156 156 link(usercachepath(repo.ui, hash), path)
157 157 return path
158 158 return None
159 159
160 160
161 161 class largefilesdirstate(dirstate.dirstate):
162 162 def __getitem__(self, key):
163 163 return super(largefilesdirstate, self).__getitem__(unixpath(key))
164 164
165 def set_tracked(self, f):
166 return super(largefilesdirstate, self).set_tracked(unixpath(f))
167
165 168 def normal(self, f):
166 169 return super(largefilesdirstate, self).normal(unixpath(f))
167 170
168 171 def remove(self, f):
169 172 return super(largefilesdirstate, self).remove(unixpath(f))
170 173
171 174 def add(self, f):
172 175 return super(largefilesdirstate, self).add(unixpath(f))
173 176
174 177 def drop(self, f):
175 178 return super(largefilesdirstate, self).drop(unixpath(f))
176 179
177 180 def forget(self, f):
178 181 return super(largefilesdirstate, self).forget(unixpath(f))
179 182
180 183 def normallookup(self, f):
181 184 return super(largefilesdirstate, self).normallookup(unixpath(f))
182 185
183 186 def _ignore(self, f):
184 187 return False
185 188
186 189 def write(self, tr=False):
187 190 # (1) disable PENDING mode always
188 191 # (lfdirstate isn't yet managed as a part of the transaction)
189 192 # (2) avoid develwarn 'use dirstate.write with ....'
190 193 super(largefilesdirstate, self).write(None)
191 194
192 195
193 196 def openlfdirstate(ui, repo, create=True):
194 197 """
195 198 Return a dirstate object that tracks largefiles: i.e. its root is
196 199 the repo root, but it is saved in .hg/largefiles/dirstate.
197 200 """
198 201 vfs = repo.vfs
199 202 lfstoredir = longname
200 203 opener = vfsmod.vfs(vfs.join(lfstoredir))
201 204 use_dirstate_v2 = requirements.DIRSTATE_V2_REQUIREMENT in repo.requirements
202 205 lfdirstate = largefilesdirstate(
203 206 opener,
204 207 ui,
205 208 repo.root,
206 209 repo.dirstate._validate,
207 210 lambda: sparse.matcher(repo),
208 211 repo.nodeconstants,
209 212 use_dirstate_v2,
210 213 )
211 214
212 215 # If the largefiles dirstate does not exist, populate and create
213 216 # it. This ensures that we create it on the first meaningful
214 217 # largefiles operation in a new clone.
215 218 if create and not vfs.exists(vfs.join(lfstoredir, b'dirstate')):
216 219 matcher = getstandinmatcher(repo)
217 220 standins = repo.dirstate.walk(
218 221 matcher, subrepos=[], unknown=False, ignored=False
219 222 )
220 223
221 224 if len(standins) > 0:
222 225 vfs.makedirs(lfstoredir)
223 226
224 227 for standin in standins:
225 228 lfile = splitstandin(standin)
226 229 lfdirstate.normallookup(lfile)
227 230 return lfdirstate
228 231
229 232
230 233 def lfdirstatestatus(lfdirstate, repo):
231 234 pctx = repo[b'.']
232 235 match = matchmod.always()
233 236 unsure, s = lfdirstate.status(
234 237 match, subrepos=[], ignored=False, clean=False, unknown=False
235 238 )
236 239 modified, clean = s.modified, s.clean
237 240 for lfile in unsure:
238 241 try:
239 242 fctx = pctx[standin(lfile)]
240 243 except LookupError:
241 244 fctx = None
242 245 if not fctx or readasstandin(fctx) != hashfile(repo.wjoin(lfile)):
243 246 modified.append(lfile)
244 247 else:
245 248 clean.append(lfile)
246 249 lfdirstate.normal(lfile)
247 250 return s
248 251
249 252
250 253 def listlfiles(repo, rev=None, matcher=None):
251 254 """return a list of largefiles in the working copy or the
252 255 specified changeset"""
253 256
254 257 if matcher is None:
255 258 matcher = getstandinmatcher(repo)
256 259
257 260 # ignore unknown files in working directory
258 261 return [
259 262 splitstandin(f)
260 263 for f in repo[rev].walk(matcher)
261 264 if rev is not None or repo.dirstate[f] != b'?'
262 265 ]
263 266
264 267
265 268 def instore(repo, hash, forcelocal=False):
266 269 '''Return true if a largefile with the given hash exists in the store'''
267 270 return os.path.exists(storepath(repo, hash, forcelocal))
268 271
269 272
270 273 def storepath(repo, hash, forcelocal=False):
271 274 """Return the correct location in the repository largefiles store for a
272 275 file with the given hash."""
273 276 if not forcelocal and repo.shared():
274 277 return repo.vfs.reljoin(repo.sharedpath, longname, hash)
275 278 return repo.vfs.join(longname, hash)
276 279
277 280
278 281 def findstorepath(repo, hash):
279 282 """Search through the local store path(s) to find the file for the given
280 283 hash. If the file is not found, its path in the primary store is returned.
281 284 The return value is a tuple of (path, exists(path)).
282 285 """
283 286 # For shared repos, the primary store is in the share source. But for
284 287 # backward compatibility, force a lookup in the local store if it wasn't
285 288 # found in the share source.
286 289 path = storepath(repo, hash, False)
287 290
288 291 if instore(repo, hash):
289 292 return (path, True)
290 293 elif repo.shared() and instore(repo, hash, True):
291 294 return storepath(repo, hash, True), True
292 295
293 296 return (path, False)
294 297
295 298
296 299 def copyfromcache(repo, hash, filename):
297 300 """Copy the specified largefile from the repo or system cache to
298 301 filename in the repository. Return true on success or false if the
299 302 file was not found in either cache (which should not happened:
300 303 this is meant to be called only after ensuring that the needed
301 304 largefile exists in the cache)."""
302 305 wvfs = repo.wvfs
303 306 path = findfile(repo, hash)
304 307 if path is None:
305 308 return False
306 309 wvfs.makedirs(wvfs.dirname(wvfs.join(filename)))
307 310 # The write may fail before the file is fully written, but we
308 311 # don't use atomic writes in the working copy.
309 312 with open(path, b'rb') as srcfd, wvfs(filename, b'wb') as destfd:
310 313 gothash = copyandhash(util.filechunkiter(srcfd), destfd)
311 314 if gothash != hash:
312 315 repo.ui.warn(
313 316 _(b'%s: data corruption in %s with hash %s\n')
314 317 % (filename, path, gothash)
315 318 )
316 319 wvfs.unlink(filename)
317 320 return False
318 321 return True
319 322
320 323
321 324 def copytostore(repo, ctx, file, fstandin):
322 325 wvfs = repo.wvfs
323 326 hash = readasstandin(ctx[fstandin])
324 327 if instore(repo, hash):
325 328 return
326 329 if wvfs.exists(file):
327 330 copytostoreabsolute(repo, wvfs.join(file), hash)
328 331 else:
329 332 repo.ui.warn(
330 333 _(b"%s: largefile %s not available from local store\n")
331 334 % (file, hash)
332 335 )
333 336
334 337
335 338 def copyalltostore(repo, node):
336 339 '''Copy all largefiles in a given revision to the store'''
337 340
338 341 ctx = repo[node]
339 342 for filename in ctx.files():
340 343 realfile = splitstandin(filename)
341 344 if realfile is not None and filename in ctx.manifest():
342 345 copytostore(repo, ctx, realfile, filename)
343 346
344 347
345 348 def copytostoreabsolute(repo, file, hash):
346 349 if inusercache(repo.ui, hash):
347 350 link(usercachepath(repo.ui, hash), storepath(repo, hash))
348 351 else:
349 352 util.makedirs(os.path.dirname(storepath(repo, hash)))
350 353 with open(file, b'rb') as srcf:
351 354 with util.atomictempfile(
352 355 storepath(repo, hash), createmode=repo.store.createmode
353 356 ) as dstf:
354 357 for chunk in util.filechunkiter(srcf):
355 358 dstf.write(chunk)
356 359 linktousercache(repo, hash)
357 360
358 361
359 362 def linktousercache(repo, hash):
360 363 """Link / copy the largefile with the specified hash from the store
361 364 to the cache."""
362 365 path = usercachepath(repo.ui, hash)
363 366 link(storepath(repo, hash), path)
364 367
365 368
366 369 def getstandinmatcher(repo, rmatcher=None):
367 370 '''Return a match object that applies rmatcher to the standin directory'''
368 371 wvfs = repo.wvfs
369 372 standindir = shortname
370 373
371 374 # no warnings about missing files or directories
372 375 badfn = lambda f, msg: None
373 376
374 377 if rmatcher and not rmatcher.always():
375 378 pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()]
376 379 if not pats:
377 380 pats = [wvfs.join(standindir)]
378 381 match = scmutil.match(repo[None], pats, badfn=badfn)
379 382 else:
380 383 # no patterns: relative to repo root
381 384 match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn)
382 385 return match
383 386
384 387
385 388 def composestandinmatcher(repo, rmatcher):
386 389 """Return a matcher that accepts standins corresponding to the
387 390 files accepted by rmatcher. Pass the list of files in the matcher
388 391 as the paths specified by the user."""
389 392 smatcher = getstandinmatcher(repo, rmatcher)
390 393 isstandin = smatcher.matchfn
391 394
392 395 def composedmatchfn(f):
393 396 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
394 397
395 398 smatcher.matchfn = composedmatchfn
396 399
397 400 return smatcher
398 401
399 402
400 403 def standin(filename):
401 404 """Return the repo-relative path to the standin for the specified big
402 405 file."""
403 406 # Notes:
404 407 # 1) Some callers want an absolute path, but for instance addlargefiles
405 408 # needs it repo-relative so it can be passed to repo[None].add(). So
406 409 # leave it up to the caller to use repo.wjoin() to get an absolute path.
407 410 # 2) Join with '/' because that's what dirstate always uses, even on
408 411 # Windows. Change existing separator to '/' first in case we are
409 412 # passed filenames from an external source (like the command line).
410 413 return shortnameslash + util.pconvert(filename)
411 414
412 415
413 416 def isstandin(filename):
414 417 """Return true if filename is a big file standin. filename must be
415 418 in Mercurial's internal form (slash-separated)."""
416 419 return filename.startswith(shortnameslash)
417 420
418 421
419 422 def splitstandin(filename):
420 423 # Split on / because that's what dirstate always uses, even on Windows.
421 424 # Change local separator to / first just in case we are passed filenames
422 425 # from an external source (like the command line).
423 426 bits = util.pconvert(filename).split(b'/', 1)
424 427 if len(bits) == 2 and bits[0] == shortname:
425 428 return bits[1]
426 429 else:
427 430 return None
428 431
429 432
430 433 def updatestandin(repo, lfile, standin):
431 434 """Re-calculate hash value of lfile and write it into standin
432 435
433 436 This assumes that "lfutil.standin(lfile) == standin", for efficiency.
434 437 """
435 438 file = repo.wjoin(lfile)
436 439 if repo.wvfs.exists(lfile):
437 440 hash = hashfile(file)
438 441 executable = getexecutable(file)
439 442 writestandin(repo, standin, hash, executable)
440 443 else:
441 444 raise error.Abort(_(b'%s: file not found!') % lfile)
442 445
443 446
444 447 def readasstandin(fctx):
445 448 """read hex hash from given filectx of standin file
446 449
447 450 This encapsulates how "standin" data is stored into storage layer."""
448 451 return fctx.data().strip()
449 452
450 453
451 454 def writestandin(repo, standin, hash, executable):
452 455 '''write hash to <repo.root>/<standin>'''
453 456 repo.wwrite(standin, hash + b'\n', executable and b'x' or b'')
454 457
455 458
456 459 def copyandhash(instream, outfile):
457 460 """Read bytes from instream (iterable) and write them to outfile,
458 461 computing the SHA-1 hash of the data along the way. Return the hash."""
459 462 hasher = hashutil.sha1(b'')
460 463 for data in instream:
461 464 hasher.update(data)
462 465 outfile.write(data)
463 466 return hex(hasher.digest())
464 467
465 468
466 469 def hashfile(file):
467 470 if not os.path.exists(file):
468 471 return b''
469 472 with open(file, b'rb') as fd:
470 473 return hexsha1(fd)
471 474
472 475
473 476 def getexecutable(filename):
474 477 mode = os.stat(filename).st_mode
475 478 return (
476 479 (mode & stat.S_IXUSR)
477 480 and (mode & stat.S_IXGRP)
478 481 and (mode & stat.S_IXOTH)
479 482 )
480 483
481 484
482 485 def urljoin(first, second, *arg):
483 486 def join(left, right):
484 487 if not left.endswith(b'/'):
485 488 left += b'/'
486 489 if right.startswith(b'/'):
487 490 right = right[1:]
488 491 return left + right
489 492
490 493 url = join(first, second)
491 494 for a in arg:
492 495 url = join(url, a)
493 496 return url
494 497
495 498
496 499 def hexsha1(fileobj):
497 500 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
498 501 object data"""
499 502 h = hashutil.sha1()
500 503 for chunk in util.filechunkiter(fileobj):
501 504 h.update(chunk)
502 505 return hex(h.digest())
503 506
504 507
505 508 def httpsendfile(ui, filename):
506 509 return httpconnection.httpsendfile(ui, filename, b'rb')
507 510
508 511
509 512 def unixpath(path):
510 513 '''Return a version of path normalized for use with the lfdirstate.'''
511 514 return util.pconvert(os.path.normpath(path))
512 515
513 516
514 517 def islfilesrepo(repo):
515 518 '''Return true if the repo is a largefile repo.'''
516 519 if b'largefiles' in repo.requirements and any(
517 520 shortnameslash in f[1] for f in repo.store.datafiles()
518 521 ):
519 522 return True
520 523
521 524 return any(openlfdirstate(repo.ui, repo, False))
522 525
523 526
524 527 class storeprotonotcapable(Exception):
525 528 def __init__(self, storetypes):
526 529 self.storetypes = storetypes
527 530
528 531
529 532 def getstandinsstate(repo):
530 533 standins = []
531 534 matcher = getstandinmatcher(repo)
532 535 wctx = repo[None]
533 536 for standin in repo.dirstate.walk(
534 537 matcher, subrepos=[], unknown=False, ignored=False
535 538 ):
536 539 lfile = splitstandin(standin)
537 540 try:
538 541 hash = readasstandin(wctx[standin])
539 542 except IOError:
540 543 hash = None
541 544 standins.append((lfile, hash))
542 545 return standins
543 546
544 547
545 548 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
546 549 lfstandin = standin(lfile)
547 550 if lfstandin in repo.dirstate:
548 551 stat = repo.dirstate._map[lfstandin]
549 552 state, mtime = stat.state, stat.mtime
550 553 else:
551 554 state, mtime = b'?', -1
552 555 if state == b'n':
553 556 if normallookup or mtime < 0 or not repo.wvfs.exists(lfile):
554 557 # state 'n' doesn't ensure 'clean' in this case
555 558 lfdirstate.normallookup(lfile)
556 559 else:
557 560 lfdirstate.normal(lfile)
558 561 elif state == b'm':
559 562 lfdirstate.normallookup(lfile)
560 563 elif state == b'r':
561 564 lfdirstate.remove(lfile)
562 565 elif state == b'a':
563 566 lfdirstate.add(lfile)
564 567 elif state == b'?':
565 568 lfdirstate.drop(lfile)
566 569
567 570
568 571 def markcommitted(orig, ctx, node):
569 572 repo = ctx.repo()
570 573
571 574 orig(node)
572 575
573 576 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
574 577 # because files coming from the 2nd parent are omitted in the latter.
575 578 #
576 579 # The former should be used to get targets of "synclfdirstate",
577 580 # because such files:
578 581 # - are marked as "a" by "patch.patch()" (e.g. via transplant), and
579 582 # - have to be marked as "n" after commit, but
580 583 # - aren't listed in "repo[node].files()"
581 584
582 585 lfdirstate = openlfdirstate(repo.ui, repo)
583 586 for f in ctx.files():
584 587 lfile = splitstandin(f)
585 588 if lfile is not None:
586 589 synclfdirstate(repo, lfdirstate, lfile, False)
587 590 lfdirstate.write()
588 591
589 592 # As part of committing, copy all of the largefiles into the cache.
590 593 #
591 594 # Using "node" instead of "ctx" implies additional "repo[node]"
592 595 # lookup while copyalltostore(), but can omit redundant check for
593 596 # files comming from the 2nd parent, which should exist in store
594 597 # at merging.
595 598 copyalltostore(repo, node)
596 599
597 600
598 601 def getlfilestoupdate(oldstandins, newstandins):
599 602 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
600 603 filelist = []
601 604 for f in changedstandins:
602 605 if f[0] not in filelist:
603 606 filelist.append(f[0])
604 607 return filelist
605 608
606 609
607 610 def getlfilestoupload(repo, missing, addfunc):
608 611 makeprogress = repo.ui.makeprogress
609 612 with makeprogress(
610 613 _(b'finding outgoing largefiles'),
611 614 unit=_(b'revisions'),
612 615 total=len(missing),
613 616 ) as progress:
614 617 for i, n in enumerate(missing):
615 618 progress.update(i)
616 619 parents = [p for p in repo[n].parents() if p != repo.nullid]
617 620
618 621 with lfstatus(repo, value=False):
619 622 ctx = repo[n]
620 623
621 624 files = set(ctx.files())
622 625 if len(parents) == 2:
623 626 mc = ctx.manifest()
624 627 mp1 = ctx.p1().manifest()
625 628 mp2 = ctx.p2().manifest()
626 629 for f in mp1:
627 630 if f not in mc:
628 631 files.add(f)
629 632 for f in mp2:
630 633 if f not in mc:
631 634 files.add(f)
632 635 for f in mc:
633 636 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
634 637 files.add(f)
635 638 for fn in files:
636 639 if isstandin(fn) and fn in ctx:
637 640 addfunc(fn, readasstandin(ctx[fn]))
638 641
639 642
640 643 def updatestandinsbymatch(repo, match):
641 644 """Update standins in the working directory according to specified match
642 645
643 646 This returns (possibly modified) ``match`` object to be used for
644 647 subsequent commit process.
645 648 """
646 649
647 650 ui = repo.ui
648 651
649 652 # Case 1: user calls commit with no specific files or
650 653 # include/exclude patterns: refresh and commit all files that
651 654 # are "dirty".
652 655 if match is None or match.always():
653 656 # Spend a bit of time here to get a list of files we know
654 657 # are modified so we can compare only against those.
655 658 # It can cost a lot of time (several seconds)
656 659 # otherwise to update all standins if the largefiles are
657 660 # large.
658 661 lfdirstate = openlfdirstate(ui, repo)
659 662 dirtymatch = matchmod.always()
660 663 unsure, s = lfdirstate.status(
661 664 dirtymatch, subrepos=[], ignored=False, clean=False, unknown=False
662 665 )
663 666 modifiedfiles = unsure + s.modified + s.added + s.removed
664 667 lfiles = listlfiles(repo)
665 668 # this only loops through largefiles that exist (not
666 669 # removed/renamed)
667 670 for lfile in lfiles:
668 671 if lfile in modifiedfiles:
669 672 fstandin = standin(lfile)
670 673 if repo.wvfs.exists(fstandin):
671 674 # this handles the case where a rebase is being
672 675 # performed and the working copy is not updated
673 676 # yet.
674 677 if repo.wvfs.exists(lfile):
675 678 updatestandin(repo, lfile, fstandin)
676 679
677 680 return match
678 681
679 682 lfiles = listlfiles(repo)
680 683 match._files = repo._subdirlfs(match.files(), lfiles)
681 684
682 685 # Case 2: user calls commit with specified patterns: refresh
683 686 # any matching big files.
684 687 smatcher = composestandinmatcher(repo, match)
685 688 standins = repo.dirstate.walk(
686 689 smatcher, subrepos=[], unknown=False, ignored=False
687 690 )
688 691
689 692 # No matching big files: get out of the way and pass control to
690 693 # the usual commit() method.
691 694 if not standins:
692 695 return match
693 696
694 697 # Refresh all matching big files. It's possible that the
695 698 # commit will end up failing, in which case the big files will
696 699 # stay refreshed. No harm done: the user modified them and
697 700 # asked to commit them, so sooner or later we're going to
698 701 # refresh the standins. Might as well leave them refreshed.
699 702 lfdirstate = openlfdirstate(ui, repo)
700 703 for fstandin in standins:
701 704 lfile = splitstandin(fstandin)
702 705 if lfdirstate[lfile] != b'r':
703 706 updatestandin(repo, lfile, fstandin)
704 707
705 708 # Cook up a new matcher that only matches regular files or
706 709 # standins corresponding to the big files requested by the
707 710 # user. Have to modify _files to prevent commit() from
708 711 # complaining "not tracked" for big files.
709 712 match = copy.copy(match)
710 713 origmatchfn = match.matchfn
711 714
712 715 # Check both the list of largefiles and the list of
713 716 # standins because if a largefile was removed, it
714 717 # won't be in the list of largefiles at this point
715 718 match._files += sorted(standins)
716 719
717 720 actualfiles = []
718 721 for f in match._files:
719 722 fstandin = standin(f)
720 723
721 724 # For largefiles, only one of the normal and standin should be
722 725 # committed (except if one of them is a remove). In the case of a
723 726 # standin removal, drop the normal file if it is unknown to dirstate.
724 727 # Thus, skip plain largefile names but keep the standin.
725 728 if f in lfiles or fstandin in standins:
726 729 if repo.dirstate[fstandin] != b'r':
727 730 if repo.dirstate[f] != b'r':
728 731 continue
729 732 elif repo.dirstate[f] == b'?':
730 733 continue
731 734
732 735 actualfiles.append(f)
733 736 match._files = actualfiles
734 737
735 738 def matchfn(f):
736 739 if origmatchfn(f):
737 740 return f not in lfiles
738 741 else:
739 742 return f in standins
740 743
741 744 match.matchfn = matchfn
742 745
743 746 return match
744 747
745 748
746 749 class automatedcommithook(object):
747 750 """Stateful hook to update standins at the 1st commit of resuming
748 751
749 752 For efficiency, updating standins in the working directory should
750 753 be avoided while automated committing (like rebase, transplant and
751 754 so on), because they should be updated before committing.
752 755
753 756 But the 1st commit of resuming automated committing (e.g. ``rebase
754 757 --continue``) should update them, because largefiles may be
755 758 modified manually.
756 759 """
757 760
758 761 def __init__(self, resuming):
759 762 self.resuming = resuming
760 763
761 764 def __call__(self, repo, match):
762 765 if self.resuming:
763 766 self.resuming = False # avoids updating at subsequent commits
764 767 return updatestandinsbymatch(repo, match)
765 768 else:
766 769 return match
767 770
768 771
769 772 def getstatuswriter(ui, repo, forcibly=None):
770 773 """Return the function to write largefiles specific status out
771 774
772 775 If ``forcibly`` is ``None``, this returns the last element of
773 776 ``repo._lfstatuswriters`` as "default" writer function.
774 777
775 778 Otherwise, this returns the function to always write out (or
776 779 ignore if ``not forcibly``) status.
777 780 """
778 781 if forcibly is None and util.safehasattr(repo, b'_largefilesenabled'):
779 782 return repo._lfstatuswriters[-1]
780 783 else:
781 784 if forcibly:
782 785 return ui.status # forcibly WRITE OUT
783 786 else:
784 787 return lambda *msg, **opts: None # forcibly IGNORE
@@ -1,68 +1,72 b''
1 1 # narrowdirstate.py - extensions to mercurial dirstate to support narrow clones
2 2 #
3 3 # Copyright 2017 Google, Inc.
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 from mercurial.i18n import _
11 11 from mercurial import error
12 12
13 13
14 14 def wrapdirstate(repo, dirstate):
15 15 """Add narrow spec dirstate ignore, block changes outside narrow spec."""
16 16
17 17 def _editfunc(fn):
18 18 def _wrapper(self, *args, **kwargs):
19 19 narrowmatch = repo.narrowmatch()
20 20 for f in args:
21 21 if f is not None and not narrowmatch(f) and f not in self:
22 22 raise error.Abort(
23 23 _(
24 24 b"cannot track '%s' - it is outside "
25 25 + b"the narrow clone"
26 26 )
27 27 % f
28 28 )
29 29 return fn(self, *args, **kwargs)
30 30
31 31 return _wrapper
32 32
33 33 class narrowdirstate(dirstate.__class__):
34 34 # Prevent adding/editing/copying/deleting files that are outside the
35 35 # sparse checkout
36 36 @_editfunc
37 37 def normal(self, *args, **kwargs):
38 38 return super(narrowdirstate, self).normal(*args, **kwargs)
39 39
40 40 @_editfunc
41 def set_tracked(self, *args):
42 return super(narrowdirstate, self).set_tracked(*args)
43
44 @_editfunc
41 45 def add(self, *args):
42 46 return super(narrowdirstate, self).add(*args)
43 47
44 48 @_editfunc
45 49 def normallookup(self, *args):
46 50 return super(narrowdirstate, self).normallookup(*args)
47 51
48 52 @_editfunc
49 53 def copy(self, *args):
50 54 return super(narrowdirstate, self).copy(*args)
51 55
52 56 @_editfunc
53 57 def remove(self, *args):
54 58 return super(narrowdirstate, self).remove(*args)
55 59
56 60 @_editfunc
57 61 def merge(self, *args):
58 62 return super(narrowdirstate, self).merge(*args)
59 63
60 64 def rebuild(self, parent, allfiles, changedfiles=None):
61 65 if changedfiles is None:
62 66 # Rebuilding entire dirstate, let's filter allfiles to match the
63 67 # narrowspec.
64 68 allfiles = [f for f in allfiles if repo.narrowmatch()(f)]
65 69 super(narrowdirstate, self).rebuild(parent, allfiles, changedfiles)
66 70
67 71 dirstate.__class__ = narrowdirstate
68 72 return dirstate
@@ -1,440 +1,441 b''
1 1 # sparse.py - allow sparse checkouts of the working directory
2 2 #
3 3 # Copyright 2014 Facebook, Inc.
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 """allow sparse checkouts of the working directory (EXPERIMENTAL)
9 9
10 10 (This extension is not yet protected by backwards compatibility
11 11 guarantees. Any aspect may break in future releases until this
12 12 notice is removed.)
13 13
14 14 This extension allows the working directory to only consist of a
15 15 subset of files for the revision. This allows specific files or
16 16 directories to be explicitly included or excluded. Many repository
17 17 operations have performance proportional to the number of files in
18 18 the working directory. So only realizing a subset of files in the
19 19 working directory can improve performance.
20 20
21 21 Sparse Config Files
22 22 -------------------
23 23
24 24 The set of files that are part of a sparse checkout are defined by
25 25 a sparse config file. The file defines 3 things: includes (files to
26 26 include in the sparse checkout), excludes (files to exclude from the
27 27 sparse checkout), and profiles (links to other config files).
28 28
29 29 The file format is newline delimited. Empty lines and lines beginning
30 30 with ``#`` are ignored.
31 31
32 32 Lines beginning with ``%include `` denote another sparse config file
33 33 to include. e.g. ``%include tests.sparse``. The filename is relative
34 34 to the repository root.
35 35
36 36 The special lines ``[include]`` and ``[exclude]`` denote the section
37 37 for includes and excludes that follow, respectively. It is illegal to
38 38 have ``[include]`` after ``[exclude]``.
39 39
40 40 Non-special lines resemble file patterns to be added to either includes
41 41 or excludes. The syntax of these lines is documented by :hg:`help patterns`.
42 42 Patterns are interpreted as ``glob:`` by default and match against the
43 43 root of the repository.
44 44
45 45 Exclusion patterns take precedence over inclusion patterns. So even
46 46 if a file is explicitly included, an ``[exclude]`` entry can remove it.
47 47
48 48 For example, say you have a repository with 3 directories, ``frontend/``,
49 49 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
50 50 to different projects and it is uncommon for someone working on one
51 51 to need the files for the other. But ``tools/`` contains files shared
52 52 between both projects. Your sparse config files may resemble::
53 53
54 54 # frontend.sparse
55 55 frontend/**
56 56 tools/**
57 57
58 58 # backend.sparse
59 59 backend/**
60 60 tools/**
61 61
62 62 Say the backend grows in size. Or there's a directory with thousands
63 63 of files you wish to exclude. You can modify the profile to exclude
64 64 certain files::
65 65
66 66 [include]
67 67 backend/**
68 68 tools/**
69 69
70 70 [exclude]
71 71 tools/tests/**
72 72 """
73 73
74 74 from __future__ import absolute_import
75 75
76 76 from mercurial.i18n import _
77 77 from mercurial.pycompat import setattr
78 78 from mercurial import (
79 79 commands,
80 80 dirstate,
81 81 error,
82 82 extensions,
83 83 logcmdutil,
84 84 match as matchmod,
85 85 merge as mergemod,
86 86 pycompat,
87 87 registrar,
88 88 sparse,
89 89 util,
90 90 )
91 91
92 92 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
93 93 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
94 94 # be specifying the version(s) of Mercurial they are tested with, or
95 95 # leave the attribute unspecified.
96 96 testedwith = b'ships-with-hg-core'
97 97
98 98 cmdtable = {}
99 99 command = registrar.command(cmdtable)
100 100
101 101
102 102 def extsetup(ui):
103 103 sparse.enabled = True
104 104
105 105 _setupclone(ui)
106 106 _setuplog(ui)
107 107 _setupadd(ui)
108 108 _setupdirstate(ui)
109 109
110 110
111 111 def replacefilecache(cls, propname, replacement):
112 112 """Replace a filecache property with a new class. This allows changing the
113 113 cache invalidation condition."""
114 114 origcls = cls
115 115 assert callable(replacement)
116 116 while cls is not object:
117 117 if propname in cls.__dict__:
118 118 orig = cls.__dict__[propname]
119 119 setattr(cls, propname, replacement(orig))
120 120 break
121 121 cls = cls.__bases__[0]
122 122
123 123 if cls is object:
124 124 raise AttributeError(
125 125 _(b"type '%s' has no property '%s'") % (origcls, propname)
126 126 )
127 127
128 128
129 129 def _setuplog(ui):
130 130 entry = commands.table[b'log|history']
131 131 entry[1].append(
132 132 (
133 133 b'',
134 134 b'sparse',
135 135 None,
136 136 b"limit to changesets affecting the sparse checkout",
137 137 )
138 138 )
139 139
140 140 def _initialrevs(orig, repo, wopts):
141 141 revs = orig(repo, wopts)
142 142 if wopts.opts.get(b'sparse'):
143 143 sparsematch = sparse.matcher(repo)
144 144
145 145 def ctxmatch(rev):
146 146 ctx = repo[rev]
147 147 return any(f for f in ctx.files() if sparsematch(f))
148 148
149 149 revs = revs.filter(ctxmatch)
150 150 return revs
151 151
152 152 extensions.wrapfunction(logcmdutil, b'_initialrevs', _initialrevs)
153 153
154 154
155 155 def _clonesparsecmd(orig, ui, repo, *args, **opts):
156 156 include_pat = opts.get('include')
157 157 exclude_pat = opts.get('exclude')
158 158 enableprofile_pat = opts.get('enable_profile')
159 159 narrow_pat = opts.get('narrow')
160 160 include = exclude = enableprofile = False
161 161 if include_pat:
162 162 pat = include_pat
163 163 include = True
164 164 if exclude_pat:
165 165 pat = exclude_pat
166 166 exclude = True
167 167 if enableprofile_pat:
168 168 pat = enableprofile_pat
169 169 enableprofile = True
170 170 if sum([include, exclude, enableprofile]) > 1:
171 171 raise error.Abort(_(b"too many flags specified."))
172 172 # if --narrow is passed, it means they are includes and excludes for narrow
173 173 # clone
174 174 if not narrow_pat and (include or exclude or enableprofile):
175 175
176 176 def clonesparse(orig, ctx, *args, **kwargs):
177 177 sparse.updateconfig(
178 178 ctx.repo().unfiltered(),
179 179 pat,
180 180 {},
181 181 include=include,
182 182 exclude=exclude,
183 183 enableprofile=enableprofile,
184 184 usereporootpaths=True,
185 185 )
186 186 return orig(ctx, *args, **kwargs)
187 187
188 188 extensions.wrapfunction(mergemod, b'update', clonesparse)
189 189 return orig(ui, repo, *args, **opts)
190 190
191 191
192 192 def _setupclone(ui):
193 193 entry = commands.table[b'clone']
194 194 entry[1].append((b'', b'enable-profile', [], b'enable a sparse profile'))
195 195 entry[1].append((b'', b'include', [], b'include sparse pattern'))
196 196 entry[1].append((b'', b'exclude', [], b'exclude sparse pattern'))
197 197 extensions.wrapcommand(commands.table, b'clone', _clonesparsecmd)
198 198
199 199
200 200 def _setupadd(ui):
201 201 entry = commands.table[b'add']
202 202 entry[1].append(
203 203 (
204 204 b's',
205 205 b'sparse',
206 206 None,
207 207 b'also include directories of added files in sparse config',
208 208 )
209 209 )
210 210
211 211 def _add(orig, ui, repo, *pats, **opts):
212 212 if opts.get('sparse'):
213 213 dirs = set()
214 214 for pat in pats:
215 215 dirname, basename = util.split(pat)
216 216 dirs.add(dirname)
217 217 sparse.updateconfig(repo, list(dirs), opts, include=True)
218 218 return orig(ui, repo, *pats, **opts)
219 219
220 220 extensions.wrapcommand(commands.table, b'add', _add)
221 221
222 222
223 223 def _setupdirstate(ui):
224 224 """Modify the dirstate to prevent stat'ing excluded files,
225 225 and to prevent modifications to files outside the checkout.
226 226 """
227 227
228 228 def walk(orig, self, match, subrepos, unknown, ignored, full=True):
229 229 # hack to not exclude explicitly-specified paths so that they can
230 230 # be warned later on e.g. dirstate.add()
231 231 em = matchmod.exact(match.files())
232 232 sm = matchmod.unionmatcher([self._sparsematcher, em])
233 233 match = matchmod.intersectmatchers(match, sm)
234 234 return orig(self, match, subrepos, unknown, ignored, full)
235 235
236 236 extensions.wrapfunction(dirstate.dirstate, b'walk', walk)
237 237
238 238 # dirstate.rebuild should not add non-matching files
239 239 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
240 240 matcher = self._sparsematcher
241 241 if not matcher.always():
242 242 allfiles = [f for f in allfiles if matcher(f)]
243 243 if changedfiles:
244 244 changedfiles = [f for f in changedfiles if matcher(f)]
245 245
246 246 if changedfiles is not None:
247 247 # In _rebuild, these files will be deleted from the dirstate
248 248 # when they are not found to be in allfiles
249 249 dirstatefilestoremove = {f for f in self if not matcher(f)}
250 250 changedfiles = dirstatefilestoremove.union(changedfiles)
251 251
252 252 return orig(self, parent, allfiles, changedfiles)
253 253
254 254 extensions.wrapfunction(dirstate.dirstate, b'rebuild', _rebuild)
255 255
256 256 # Prevent adding files that are outside the sparse checkout
257 257 editfuncs = [
258 258 b'normal',
259 b'set_tracked',
259 260 b'add',
260 261 b'normallookup',
261 262 b'copy',
262 263 b'remove',
263 264 b'merge',
264 265 ]
265 266 hint = _(
266 267 b'include file with `hg debugsparse --include <pattern>` or use '
267 268 + b'`hg add -s <file>` to include file directory while adding'
268 269 )
269 270 for func in editfuncs:
270 271
271 272 def _wrapper(orig, self, *args, **kwargs):
272 273 sparsematch = self._sparsematcher
273 274 if not sparsematch.always():
274 275 for f in args:
275 276 if f is not None and not sparsematch(f) and f not in self:
276 277 raise error.Abort(
277 278 _(
278 279 b"cannot add '%s' - it is outside "
279 280 b"the sparse checkout"
280 281 )
281 282 % f,
282 283 hint=hint,
283 284 )
284 285 return orig(self, *args, **kwargs)
285 286
286 287 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
287 288
288 289
289 290 @command(
290 291 b'debugsparse',
291 292 [
292 293 (b'I', b'include', False, _(b'include files in the sparse checkout')),
293 294 (b'X', b'exclude', False, _(b'exclude files in the sparse checkout')),
294 295 (b'd', b'delete', False, _(b'delete an include/exclude rule')),
295 296 (
296 297 b'f',
297 298 b'force',
298 299 False,
299 300 _(b'allow changing rules even with pending changes'),
300 301 ),
301 302 (b'', b'enable-profile', False, _(b'enables the specified profile')),
302 303 (b'', b'disable-profile', False, _(b'disables the specified profile')),
303 304 (b'', b'import-rules', False, _(b'imports rules from a file')),
304 305 (b'', b'clear-rules', False, _(b'clears local include/exclude rules')),
305 306 (
306 307 b'',
307 308 b'refresh',
308 309 False,
309 310 _(b'updates the working after sparseness changes'),
310 311 ),
311 312 (b'', b'reset', False, _(b'makes the repo full again')),
312 313 ]
313 314 + commands.templateopts,
314 315 _(b'[--OPTION] PATTERN...'),
315 316 helpbasic=True,
316 317 )
317 318 def debugsparse(ui, repo, *pats, **opts):
318 319 """make the current checkout sparse, or edit the existing checkout
319 320
320 321 The sparse command is used to make the current checkout sparse.
321 322 This means files that don't meet the sparse condition will not be
322 323 written to disk, or show up in any working copy operations. It does
323 324 not affect files in history in any way.
324 325
325 326 Passing no arguments prints the currently applied sparse rules.
326 327
327 328 --include and --exclude are used to add and remove files from the sparse
328 329 checkout. The effects of adding an include or exclude rule are applied
329 330 immediately. If applying the new rule would cause a file with pending
330 331 changes to be added or removed, the command will fail. Pass --force to
331 332 force a rule change even with pending changes (the changes on disk will
332 333 be preserved).
333 334
334 335 --delete removes an existing include/exclude rule. The effects are
335 336 immediate.
336 337
337 338 --refresh refreshes the files on disk based on the sparse rules. This is
338 339 only necessary if .hg/sparse was changed by hand.
339 340
340 341 --enable-profile and --disable-profile accept a path to a .hgsparse file.
341 342 This allows defining sparse checkouts and tracking them inside the
342 343 repository. This is useful for defining commonly used sparse checkouts for
343 344 many people to use. As the profile definition changes over time, the sparse
344 345 checkout will automatically be updated appropriately, depending on which
345 346 changeset is checked out. Changes to .hgsparse are not applied until they
346 347 have been committed.
347 348
348 349 --import-rules accepts a path to a file containing rules in the .hgsparse
349 350 format, allowing you to add --include, --exclude and --enable-profile rules
350 351 in bulk. Like the --include, --exclude and --enable-profile switches, the
351 352 changes are applied immediately.
352 353
353 354 --clear-rules removes all local include and exclude rules, while leaving
354 355 any enabled profiles in place.
355 356
356 357 Returns 0 if editing the sparse checkout succeeds.
357 358 """
358 359 opts = pycompat.byteskwargs(opts)
359 360 include = opts.get(b'include')
360 361 exclude = opts.get(b'exclude')
361 362 force = opts.get(b'force')
362 363 enableprofile = opts.get(b'enable_profile')
363 364 disableprofile = opts.get(b'disable_profile')
364 365 importrules = opts.get(b'import_rules')
365 366 clearrules = opts.get(b'clear_rules')
366 367 delete = opts.get(b'delete')
367 368 refresh = opts.get(b'refresh')
368 369 reset = opts.get(b'reset')
369 370 count = sum(
370 371 [
371 372 include,
372 373 exclude,
373 374 enableprofile,
374 375 disableprofile,
375 376 delete,
376 377 importrules,
377 378 refresh,
378 379 clearrules,
379 380 reset,
380 381 ]
381 382 )
382 383 if count > 1:
383 384 raise error.Abort(_(b"too many flags specified"))
384 385
385 386 if count == 0:
386 387 if repo.vfs.exists(b'sparse'):
387 388 ui.status(repo.vfs.read(b"sparse") + b"\n")
388 389 temporaryincludes = sparse.readtemporaryincludes(repo)
389 390 if temporaryincludes:
390 391 ui.status(
391 392 _(b"Temporarily Included Files (for merge/rebase):\n")
392 393 )
393 394 ui.status((b"\n".join(temporaryincludes) + b"\n"))
394 395 return
395 396 else:
396 397 raise error.Abort(
397 398 _(
398 399 b'the debugsparse command is only supported on'
399 400 b' sparse repositories'
400 401 )
401 402 )
402 403
403 404 if include or exclude or delete or reset or enableprofile or disableprofile:
404 405 sparse.updateconfig(
405 406 repo,
406 407 pats,
407 408 opts,
408 409 include=include,
409 410 exclude=exclude,
410 411 reset=reset,
411 412 delete=delete,
412 413 enableprofile=enableprofile,
413 414 disableprofile=disableprofile,
414 415 force=force,
415 416 )
416 417
417 418 if importrules:
418 419 sparse.importfromfiles(repo, opts, pats, force=force)
419 420
420 421 if clearrules:
421 422 sparse.clearrules(repo, force=force)
422 423
423 424 if refresh:
424 425 try:
425 426 wlock = repo.wlock()
426 427 fcounts = map(
427 428 len,
428 429 sparse.refreshwdir(
429 430 repo, repo.status(), sparse.matcher(repo), force=force
430 431 ),
431 432 )
432 433 sparse.printchanges(
433 434 ui,
434 435 opts,
435 436 added=fcounts[0],
436 437 dropped=fcounts[1],
437 438 conflicting=fcounts[2],
438 439 )
439 440 finally:
440 441 wlock.release()
@@ -1,1497 +1,1526 b''
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@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 .pycompat import delattr
18 18
19 19 from hgdemandimport import tracing
20 20
21 21 from . import (
22 22 dirstatemap,
23 23 encoding,
24 24 error,
25 25 match as matchmod,
26 26 pathutil,
27 27 policy,
28 28 pycompat,
29 29 scmutil,
30 30 sparse,
31 31 util,
32 32 )
33 33
34 34 from .interfaces import (
35 35 dirstate as intdirstate,
36 36 util as interfaceutil,
37 37 )
38 38
39 39 parsers = policy.importmod('parsers')
40 40 rustmod = policy.importrust('dirstate')
41 41
42 42 SUPPORTS_DIRSTATE_V2 = rustmod is not None
43 43
44 44 propertycache = util.propertycache
45 45 filecache = scmutil.filecache
46 46 _rangemask = dirstatemap.rangemask
47 47
48 48 DirstateItem = parsers.DirstateItem
49 49
50 50
51 51 class repocache(filecache):
52 52 """filecache for files in .hg/"""
53 53
54 54 def join(self, obj, fname):
55 55 return obj._opener.join(fname)
56 56
57 57
58 58 class rootcache(filecache):
59 59 """filecache for files in the repository root"""
60 60
61 61 def join(self, obj, fname):
62 62 return obj._join(fname)
63 63
64 64
65 65 def _getfsnow(vfs):
66 66 '''Get "now" timestamp on filesystem'''
67 67 tmpfd, tmpname = vfs.mkstemp()
68 68 try:
69 69 return os.fstat(tmpfd)[stat.ST_MTIME]
70 70 finally:
71 71 os.close(tmpfd)
72 72 vfs.unlink(tmpname)
73 73
74 74
75 75 def requires_parents_change(func):
76 76 def wrap(self, *args, **kwargs):
77 77 if not self.pendingparentchange():
78 78 msg = 'calling `%s` outside of a parentchange context'
79 79 msg %= func.__name__
80 80 raise error.ProgrammingError(msg)
81 81 return func(self, *args, **kwargs)
82 82
83 83 return wrap
84 84
85 85
86 def requires_no_parents_change(func):
87 def wrap(self, *args, **kwargs):
88 if not self.pendingparentchange():
89 msg = 'calling `%s` inside of a parentchange context'
90 msg %= func.__name__
91 raise error.ProgrammingError(msg)
92 return func(self, *args, **kwargs)
93
94 return wrap
95
96
86 97 @interfaceutil.implementer(intdirstate.idirstate)
87 98 class dirstate(object):
88 99 def __init__(
89 100 self,
90 101 opener,
91 102 ui,
92 103 root,
93 104 validate,
94 105 sparsematchfn,
95 106 nodeconstants,
96 107 use_dirstate_v2,
97 108 ):
98 109 """Create a new dirstate object.
99 110
100 111 opener is an open()-like callable that can be used to open the
101 112 dirstate file; root is the root of the directory tracked by
102 113 the dirstate.
103 114 """
104 115 self._use_dirstate_v2 = use_dirstate_v2
105 116 self._nodeconstants = nodeconstants
106 117 self._opener = opener
107 118 self._validate = validate
108 119 self._root = root
109 120 self._sparsematchfn = sparsematchfn
110 121 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
111 122 # UNC path pointing to root share (issue4557)
112 123 self._rootdir = pathutil.normasprefix(root)
113 124 self._dirty = False
114 125 self._lastnormaltime = 0
115 126 self._ui = ui
116 127 self._filecache = {}
117 128 self._parentwriters = 0
118 129 self._filename = b'dirstate'
119 130 self._pendingfilename = b'%s.pending' % self._filename
120 131 self._plchangecallbacks = {}
121 132 self._origpl = None
122 133 self._updatedfiles = set()
123 134 self._mapcls = dirstatemap.dirstatemap
124 135 # Access and cache cwd early, so we don't access it for the first time
125 136 # after a working-copy update caused it to not exist (accessing it then
126 137 # raises an exception).
127 138 self._cwd
128 139
129 140 def prefetch_parents(self):
130 141 """make sure the parents are loaded
131 142
132 143 Used to avoid a race condition.
133 144 """
134 145 self._pl
135 146
136 147 @contextlib.contextmanager
137 148 def parentchange(self):
138 149 """Context manager for handling dirstate parents.
139 150
140 151 If an exception occurs in the scope of the context manager,
141 152 the incoherent dirstate won't be written when wlock is
142 153 released.
143 154 """
144 155 self._parentwriters += 1
145 156 yield
146 157 # Typically we want the "undo" step of a context manager in a
147 158 # finally block so it happens even when an exception
148 159 # occurs. In this case, however, we only want to decrement
149 160 # parentwriters if the code in the with statement exits
150 161 # normally, so we don't have a try/finally here on purpose.
151 162 self._parentwriters -= 1
152 163
153 164 def pendingparentchange(self):
154 165 """Returns true if the dirstate is in the middle of a set of changes
155 166 that modify the dirstate parent.
156 167 """
157 168 return self._parentwriters > 0
158 169
159 170 @propertycache
160 171 def _map(self):
161 172 """Return the dirstate contents (see documentation for dirstatemap)."""
162 173 self._map = self._mapcls(
163 174 self._ui,
164 175 self._opener,
165 176 self._root,
166 177 self._nodeconstants,
167 178 self._use_dirstate_v2,
168 179 )
169 180 return self._map
170 181
171 182 @property
172 183 def _sparsematcher(self):
173 184 """The matcher for the sparse checkout.
174 185
175 186 The working directory may not include every file from a manifest. The
176 187 matcher obtained by this property will match a path if it is to be
177 188 included in the working directory.
178 189 """
179 190 # TODO there is potential to cache this property. For now, the matcher
180 191 # is resolved on every access. (But the called function does use a
181 192 # cache to keep the lookup fast.)
182 193 return self._sparsematchfn()
183 194
184 195 @repocache(b'branch')
185 196 def _branch(self):
186 197 try:
187 198 return self._opener.read(b"branch").strip() or b"default"
188 199 except IOError as inst:
189 200 if inst.errno != errno.ENOENT:
190 201 raise
191 202 return b"default"
192 203
193 204 @property
194 205 def _pl(self):
195 206 return self._map.parents()
196 207
197 208 def hasdir(self, d):
198 209 return self._map.hastrackeddir(d)
199 210
200 211 @rootcache(b'.hgignore')
201 212 def _ignore(self):
202 213 files = self._ignorefiles()
203 214 if not files:
204 215 return matchmod.never()
205 216
206 217 pats = [b'include:%s' % f for f in files]
207 218 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
208 219
209 220 @propertycache
210 221 def _slash(self):
211 222 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
212 223
213 224 @propertycache
214 225 def _checklink(self):
215 226 return util.checklink(self._root)
216 227
217 228 @propertycache
218 229 def _checkexec(self):
219 230 return bool(util.checkexec(self._root))
220 231
221 232 @propertycache
222 233 def _checkcase(self):
223 234 return not util.fscasesensitive(self._join(b'.hg'))
224 235
225 236 def _join(self, f):
226 237 # much faster than os.path.join()
227 238 # it's safe because f is always a relative path
228 239 return self._rootdir + f
229 240
230 241 def flagfunc(self, buildfallback):
231 242 if self._checklink and self._checkexec:
232 243
233 244 def f(x):
234 245 try:
235 246 st = os.lstat(self._join(x))
236 247 if util.statislink(st):
237 248 return b'l'
238 249 if util.statisexec(st):
239 250 return b'x'
240 251 except OSError:
241 252 pass
242 253 return b''
243 254
244 255 return f
245 256
246 257 fallback = buildfallback()
247 258 if self._checklink:
248 259
249 260 def f(x):
250 261 if os.path.islink(self._join(x)):
251 262 return b'l'
252 263 if b'x' in fallback(x):
253 264 return b'x'
254 265 return b''
255 266
256 267 return f
257 268 if self._checkexec:
258 269
259 270 def f(x):
260 271 if b'l' in fallback(x):
261 272 return b'l'
262 273 if util.isexec(self._join(x)):
263 274 return b'x'
264 275 return b''
265 276
266 277 return f
267 278 else:
268 279 return fallback
269 280
270 281 @propertycache
271 282 def _cwd(self):
272 283 # internal config: ui.forcecwd
273 284 forcecwd = self._ui.config(b'ui', b'forcecwd')
274 285 if forcecwd:
275 286 return forcecwd
276 287 return encoding.getcwd()
277 288
278 289 def getcwd(self):
279 290 """Return the path from which a canonical path is calculated.
280 291
281 292 This path should be used to resolve file patterns or to convert
282 293 canonical paths back to file paths for display. It shouldn't be
283 294 used to get real file paths. Use vfs functions instead.
284 295 """
285 296 cwd = self._cwd
286 297 if cwd == self._root:
287 298 return b''
288 299 # self._root ends with a path separator if self._root is '/' or 'C:\'
289 300 rootsep = self._root
290 301 if not util.endswithsep(rootsep):
291 302 rootsep += pycompat.ossep
292 303 if cwd.startswith(rootsep):
293 304 return cwd[len(rootsep) :]
294 305 else:
295 306 # we're outside the repo. return an absolute path.
296 307 return cwd
297 308
298 309 def pathto(self, f, cwd=None):
299 310 if cwd is None:
300 311 cwd = self.getcwd()
301 312 path = util.pathto(self._root, cwd, f)
302 313 if self._slash:
303 314 return util.pconvert(path)
304 315 return path
305 316
306 317 def __getitem__(self, key):
307 318 """Return the current state of key (a filename) in the dirstate.
308 319
309 320 States are:
310 321 n normal
311 322 m needs merging
312 323 r marked for removal
313 324 a marked for addition
314 325 ? not tracked
315 326
316 327 XXX The "state" is a bit obscure to be in the "public" API. we should
317 328 consider migrating all user of this to going through the dirstate entry
318 329 instead.
319 330 """
320 331 entry = self._map.get(key)
321 332 if entry is not None:
322 333 return entry.state
323 334 return b'?'
324 335
325 336 def __contains__(self, key):
326 337 return key in self._map
327 338
328 339 def __iter__(self):
329 340 return iter(sorted(self._map))
330 341
331 342 def items(self):
332 343 return pycompat.iteritems(self._map)
333 344
334 345 iteritems = items
335 346
336 347 def directories(self):
337 348 return self._map.directories()
338 349
339 350 def parents(self):
340 351 return [self._validate(p) for p in self._pl]
341 352
342 353 def p1(self):
343 354 return self._validate(self._pl[0])
344 355
345 356 def p2(self):
346 357 return self._validate(self._pl[1])
347 358
348 359 @property
349 360 def in_merge(self):
350 361 """True if a merge is in progress"""
351 362 return self._pl[1] != self._nodeconstants.nullid
352 363
353 364 def branch(self):
354 365 return encoding.tolocal(self._branch)
355 366
356 367 def setparents(self, p1, p2=None):
357 368 """Set dirstate parents to p1 and p2.
358 369
359 370 When moving from two parents to one, "merged" entries a
360 371 adjusted to normal and previous copy records discarded and
361 372 returned by the call.
362 373
363 374 See localrepo.setparents()
364 375 """
365 376 if p2 is None:
366 377 p2 = self._nodeconstants.nullid
367 378 if self._parentwriters == 0:
368 379 raise ValueError(
369 380 b"cannot set dirstate parent outside of "
370 381 b"dirstate.parentchange context manager"
371 382 )
372 383
373 384 self._dirty = True
374 385 oldp2 = self._pl[1]
375 386 if self._origpl is None:
376 387 self._origpl = self._pl
377 388 self._map.setparents(p1, p2)
378 389 copies = {}
379 390 if (
380 391 oldp2 != self._nodeconstants.nullid
381 392 and p2 == self._nodeconstants.nullid
382 393 ):
383 394 candidatefiles = self._map.non_normal_or_other_parent_paths()
384 395
385 396 for f in candidatefiles:
386 397 s = self._map.get(f)
387 398 if s is None:
388 399 continue
389 400
390 401 # Discard "merged" markers when moving away from a merge state
391 402 if s.merged:
392 403 source = self._map.copymap.get(f)
393 404 if source:
394 405 copies[f] = source
395 406 self.normallookup(f)
396 407 # Also fix up otherparent markers
397 408 elif s.from_p2:
398 409 source = self._map.copymap.get(f)
399 410 if source:
400 411 copies[f] = source
401 412 self._add(f)
402 413 return copies
403 414
404 415 def setbranch(self, branch):
405 416 self.__class__._branch.set(self, encoding.fromlocal(branch))
406 417 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
407 418 try:
408 419 f.write(self._branch + b'\n')
409 420 f.close()
410 421
411 422 # make sure filecache has the correct stat info for _branch after
412 423 # replacing the underlying file
413 424 ce = self._filecache[b'_branch']
414 425 if ce:
415 426 ce.refresh()
416 427 except: # re-raises
417 428 f.discard()
418 429 raise
419 430
420 431 def invalidate(self):
421 432 """Causes the next access to reread the dirstate.
422 433
423 434 This is different from localrepo.invalidatedirstate() because it always
424 435 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
425 436 check whether the dirstate has changed before rereading it."""
426 437
427 438 for a in ("_map", "_branch", "_ignore"):
428 439 if a in self.__dict__:
429 440 delattr(self, a)
430 441 self._lastnormaltime = 0
431 442 self._dirty = False
432 443 self._updatedfiles.clear()
433 444 self._parentwriters = 0
434 445 self._origpl = None
435 446
436 447 def copy(self, source, dest):
437 448 """Mark dest as a copy of source. Unmark dest if source is None."""
438 449 if source == dest:
439 450 return
440 451 self._dirty = True
441 452 if source is not None:
442 453 self._map.copymap[dest] = source
443 454 self._updatedfiles.add(source)
444 455 self._updatedfiles.add(dest)
445 456 elif self._map.copymap.pop(dest, None):
446 457 self._updatedfiles.add(dest)
447 458
448 459 def copied(self, file):
449 460 return self._map.copymap.get(file, None)
450 461
451 462 def copies(self):
452 463 return self._map.copymap
453 464
465 @requires_no_parents_change
466 def set_tracked(self, filename):
467 """a "public" method for generic code to mark a file as tracked
468
469 This function is to be called outside of "update/merge" case. For
470 example by a command like `hg add X`.
471
472 return True the file was previously untracked, False otherwise.
473 """
474 entry = self._map.get(filename)
475 if entry is None:
476 self._add(filename)
477 return True
478 elif not entry.tracked:
479 self.normallookup(filename)
480 return True
481 return False
482
454 483 @requires_parents_change
455 484 def update_file_reference(
456 485 self,
457 486 filename,
458 487 p1_tracked,
459 488 ):
460 489 """Set a file as tracked in the parent (or not)
461 490
462 491 This is to be called when adjust the dirstate to a new parent after an history
463 492 rewriting operation.
464 493
465 494 It should not be called during a merge (p2 != nullid) and only within
466 495 a `with dirstate.parentchange():` context.
467 496 """
468 497 if self.in_merge:
469 498 msg = b'update_file_reference should not be called when merging'
470 499 raise error.ProgrammingError(msg)
471 500 entry = self._map.get(filename)
472 501 if entry is None:
473 502 wc_tracked = False
474 503 else:
475 504 wc_tracked = entry.tracked
476 505 if p1_tracked and wc_tracked:
477 506 # the underlying reference might have changed, we will have to
478 507 # check it.
479 508 self.normallookup(filename)
480 509 elif not (p1_tracked or wc_tracked):
481 510 # the file is no longer relevant to anyone
482 511 self._drop(filename)
483 512 elif (not p1_tracked) and wc_tracked:
484 513 if not entry.added:
485 514 self._add(filename)
486 515 elif p1_tracked and not wc_tracked:
487 516 if entry is None or not entry.removed:
488 517 self._remove(filename)
489 518 else:
490 519 assert False, 'unreachable'
491 520
492 521 def _addpath(
493 522 self,
494 523 f,
495 524 mode=0,
496 525 size=None,
497 526 mtime=None,
498 527 added=False,
499 528 merged=False,
500 529 from_p2=False,
501 530 possibly_dirty=False,
502 531 ):
503 532 entry = self._map.get(f)
504 533 if added or entry is not None and entry.removed:
505 534 scmutil.checkfilename(f)
506 535 if self._map.hastrackeddir(f):
507 536 msg = _(b'directory %r already in dirstate')
508 537 msg %= pycompat.bytestr(f)
509 538 raise error.Abort(msg)
510 539 # shadows
511 540 for d in pathutil.finddirs(f):
512 541 if self._map.hastrackeddir(d):
513 542 break
514 543 entry = self._map.get(d)
515 544 if entry is not None and not entry.removed:
516 545 msg = _(b'file %r in dirstate clashes with %r')
517 546 msg %= (pycompat.bytestr(d), pycompat.bytestr(f))
518 547 raise error.Abort(msg)
519 548 self._dirty = True
520 549 self._updatedfiles.add(f)
521 550 self._map.addfile(
522 551 f,
523 552 mode=mode,
524 553 size=size,
525 554 mtime=mtime,
526 555 added=added,
527 556 merged=merged,
528 557 from_p2=from_p2,
529 558 possibly_dirty=possibly_dirty,
530 559 )
531 560
532 561 def normal(self, f, parentfiledata=None):
533 562 """Mark a file normal and clean.
534 563
535 564 parentfiledata: (mode, size, mtime) of the clean file
536 565
537 566 parentfiledata should be computed from memory (for mode,
538 567 size), as or close as possible from the point where we
539 568 determined the file was clean, to limit the risk of the
540 569 file having been changed by an external process between the
541 570 moment where the file was determined to be clean and now."""
542 571 if parentfiledata:
543 572 (mode, size, mtime) = parentfiledata
544 573 else:
545 574 s = os.lstat(self._join(f))
546 575 mode = s.st_mode
547 576 size = s.st_size
548 577 mtime = s[stat.ST_MTIME]
549 578 self._addpath(f, mode=mode, size=size, mtime=mtime)
550 579 self._map.copymap.pop(f, None)
551 580 if f in self._map.nonnormalset:
552 581 self._map.nonnormalset.remove(f)
553 582 if mtime > self._lastnormaltime:
554 583 # Remember the most recent modification timeslot for status(),
555 584 # to make sure we won't miss future size-preserving file content
556 585 # modifications that happen within the same timeslot.
557 586 self._lastnormaltime = mtime
558 587
559 588 def normallookup(self, f):
560 589 '''Mark a file normal, but possibly dirty.'''
561 590 if self.in_merge:
562 591 # if there is a merge going on and the file was either
563 592 # "merged" or coming from other parent (-2) before
564 593 # being removed, restore that state.
565 594 entry = self._map.get(f)
566 595 if entry is not None:
567 596 # XXX this should probably be dealt with a a lower level
568 597 # (see `merged_removed` and `from_p2_removed`)
569 598 if entry.merged_removed or entry.from_p2_removed:
570 599 source = self._map.copymap.get(f)
571 600 if entry.merged_removed:
572 601 self.merge(f)
573 602 elif entry.from_p2_removed:
574 603 self.otherparent(f)
575 604 if source is not None:
576 605 self.copy(source, f)
577 606 return
578 607 elif entry.merged or entry.from_p2:
579 608 return
580 609 self._addpath(f, possibly_dirty=True)
581 610 self._map.copymap.pop(f, None)
582 611
583 612 def otherparent(self, f):
584 613 '''Mark as coming from the other parent, always dirty.'''
585 614 if not self.in_merge:
586 615 msg = _(b"setting %r to other parent only allowed in merges") % f
587 616 raise error.Abort(msg)
588 617 entry = self._map.get(f)
589 618 if entry is not None and entry.tracked:
590 619 # merge-like
591 620 self._addpath(f, merged=True)
592 621 else:
593 622 # add-like
594 623 self._addpath(f, from_p2=True)
595 624 self._map.copymap.pop(f, None)
596 625
597 626 def add(self, f):
598 627 '''Mark a file added.'''
599 628 self._add(f)
600 629
601 630 def _add(self, filename):
602 631 """internal function to mark a file as added"""
603 632 self._addpath(filename, added=True)
604 633 self._map.copymap.pop(filename, None)
605 634
606 635 def remove(self, f):
607 636 '''Mark a file removed'''
608 637 self._remove(f)
609 638
610 639 def _remove(self, filename):
611 640 """internal function to mark a file removed"""
612 641 self._dirty = True
613 642 self._updatedfiles.add(filename)
614 643 self._map.removefile(filename, in_merge=self.in_merge)
615 644
616 645 def merge(self, f):
617 646 '''Mark a file merged.'''
618 647 if not self.in_merge:
619 648 return self.normallookup(f)
620 649 return self.otherparent(f)
621 650
622 651 def drop(self, f):
623 652 '''Drop a file from the dirstate'''
624 653 self._drop(f)
625 654
626 655 def _drop(self, filename):
627 656 """internal function to drop a file from the dirstate"""
628 657 if self._map.dropfile(filename):
629 658 self._dirty = True
630 659 self._updatedfiles.add(filename)
631 660 self._map.copymap.pop(filename, None)
632 661
633 662 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
634 663 if exists is None:
635 664 exists = os.path.lexists(os.path.join(self._root, path))
636 665 if not exists:
637 666 # Maybe a path component exists
638 667 if not ignoremissing and b'/' in path:
639 668 d, f = path.rsplit(b'/', 1)
640 669 d = self._normalize(d, False, ignoremissing, None)
641 670 folded = d + b"/" + f
642 671 else:
643 672 # No path components, preserve original case
644 673 folded = path
645 674 else:
646 675 # recursively normalize leading directory components
647 676 # against dirstate
648 677 if b'/' in normed:
649 678 d, f = normed.rsplit(b'/', 1)
650 679 d = self._normalize(d, False, ignoremissing, True)
651 680 r = self._root + b"/" + d
652 681 folded = d + b"/" + util.fspath(f, r)
653 682 else:
654 683 folded = util.fspath(normed, self._root)
655 684 storemap[normed] = folded
656 685
657 686 return folded
658 687
659 688 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
660 689 normed = util.normcase(path)
661 690 folded = self._map.filefoldmap.get(normed, None)
662 691 if folded is None:
663 692 if isknown:
664 693 folded = path
665 694 else:
666 695 folded = self._discoverpath(
667 696 path, normed, ignoremissing, exists, self._map.filefoldmap
668 697 )
669 698 return folded
670 699
671 700 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
672 701 normed = util.normcase(path)
673 702 folded = self._map.filefoldmap.get(normed, None)
674 703 if folded is None:
675 704 folded = self._map.dirfoldmap.get(normed, None)
676 705 if folded is None:
677 706 if isknown:
678 707 folded = path
679 708 else:
680 709 # store discovered result in dirfoldmap so that future
681 710 # normalizefile calls don't start matching directories
682 711 folded = self._discoverpath(
683 712 path, normed, ignoremissing, exists, self._map.dirfoldmap
684 713 )
685 714 return folded
686 715
687 716 def normalize(self, path, isknown=False, ignoremissing=False):
688 717 """
689 718 normalize the case of a pathname when on a casefolding filesystem
690 719
691 720 isknown specifies whether the filename came from walking the
692 721 disk, to avoid extra filesystem access.
693 722
694 723 If ignoremissing is True, missing path are returned
695 724 unchanged. Otherwise, we try harder to normalize possibly
696 725 existing path components.
697 726
698 727 The normalized case is determined based on the following precedence:
699 728
700 729 - version of name already stored in the dirstate
701 730 - version of name stored on disk
702 731 - version provided via command arguments
703 732 """
704 733
705 734 if self._checkcase:
706 735 return self._normalize(path, isknown, ignoremissing)
707 736 return path
708 737
709 738 def clear(self):
710 739 self._map.clear()
711 740 self._lastnormaltime = 0
712 741 self._updatedfiles.clear()
713 742 self._dirty = True
714 743
715 744 def rebuild(self, parent, allfiles, changedfiles=None):
716 745 if changedfiles is None:
717 746 # Rebuild entire dirstate
718 747 to_lookup = allfiles
719 748 to_drop = []
720 749 lastnormaltime = self._lastnormaltime
721 750 self.clear()
722 751 self._lastnormaltime = lastnormaltime
723 752 elif len(changedfiles) < 10:
724 753 # Avoid turning allfiles into a set, which can be expensive if it's
725 754 # large.
726 755 to_lookup = []
727 756 to_drop = []
728 757 for f in changedfiles:
729 758 if f in allfiles:
730 759 to_lookup.append(f)
731 760 else:
732 761 to_drop.append(f)
733 762 else:
734 763 changedfilesset = set(changedfiles)
735 764 to_lookup = changedfilesset & set(allfiles)
736 765 to_drop = changedfilesset - to_lookup
737 766
738 767 if self._origpl is None:
739 768 self._origpl = self._pl
740 769 self._map.setparents(parent, self._nodeconstants.nullid)
741 770
742 771 for f in to_lookup:
743 772 self.normallookup(f)
744 773 for f in to_drop:
745 774 self._drop(f)
746 775
747 776 self._dirty = True
748 777
749 778 def identity(self):
750 779 """Return identity of dirstate itself to detect changing in storage
751 780
752 781 If identity of previous dirstate is equal to this, writing
753 782 changes based on the former dirstate out can keep consistency.
754 783 """
755 784 return self._map.identity
756 785
757 786 def write(self, tr):
758 787 if not self._dirty:
759 788 return
760 789
761 790 filename = self._filename
762 791 if tr:
763 792 # 'dirstate.write()' is not only for writing in-memory
764 793 # changes out, but also for dropping ambiguous timestamp.
765 794 # delayed writing re-raise "ambiguous timestamp issue".
766 795 # See also the wiki page below for detail:
767 796 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
768 797
769 798 # emulate dropping timestamp in 'parsers.pack_dirstate'
770 799 now = _getfsnow(self._opener)
771 800 self._map.clearambiguoustimes(self._updatedfiles, now)
772 801
773 802 # emulate that all 'dirstate.normal' results are written out
774 803 self._lastnormaltime = 0
775 804 self._updatedfiles.clear()
776 805
777 806 # delay writing in-memory changes out
778 807 tr.addfilegenerator(
779 808 b'dirstate',
780 809 (self._filename,),
781 810 self._writedirstate,
782 811 location=b'plain',
783 812 )
784 813 return
785 814
786 815 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
787 816 self._writedirstate(st)
788 817
789 818 def addparentchangecallback(self, category, callback):
790 819 """add a callback to be called when the wd parents are changed
791 820
792 821 Callback will be called with the following arguments:
793 822 dirstate, (oldp1, oldp2), (newp1, newp2)
794 823
795 824 Category is a unique identifier to allow overwriting an old callback
796 825 with a newer callback.
797 826 """
798 827 self._plchangecallbacks[category] = callback
799 828
800 829 def _writedirstate(self, st):
801 830 # notify callbacks about parents change
802 831 if self._origpl is not None and self._origpl != self._pl:
803 832 for c, callback in sorted(
804 833 pycompat.iteritems(self._plchangecallbacks)
805 834 ):
806 835 callback(self, self._origpl, self._pl)
807 836 self._origpl = None
808 837 # use the modification time of the newly created temporary file as the
809 838 # filesystem's notion of 'now'
810 839 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
811 840
812 841 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
813 842 # timestamp of each entries in dirstate, because of 'now > mtime'
814 843 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
815 844 if delaywrite > 0:
816 845 # do we have any files to delay for?
817 846 for f, e in pycompat.iteritems(self._map):
818 847 if e.need_delay(now):
819 848 import time # to avoid useless import
820 849
821 850 # rather than sleep n seconds, sleep until the next
822 851 # multiple of n seconds
823 852 clock = time.time()
824 853 start = int(clock) - (int(clock) % delaywrite)
825 854 end = start + delaywrite
826 855 time.sleep(end - clock)
827 856 now = end # trust our estimate that the end is near now
828 857 break
829 858
830 859 self._map.write(st, now)
831 860 self._lastnormaltime = 0
832 861 self._dirty = False
833 862
834 863 def _dirignore(self, f):
835 864 if self._ignore(f):
836 865 return True
837 866 for p in pathutil.finddirs(f):
838 867 if self._ignore(p):
839 868 return True
840 869 return False
841 870
842 871 def _ignorefiles(self):
843 872 files = []
844 873 if os.path.exists(self._join(b'.hgignore')):
845 874 files.append(self._join(b'.hgignore'))
846 875 for name, path in self._ui.configitems(b"ui"):
847 876 if name == b'ignore' or name.startswith(b'ignore.'):
848 877 # we need to use os.path.join here rather than self._join
849 878 # because path is arbitrary and user-specified
850 879 files.append(os.path.join(self._rootdir, util.expandpath(path)))
851 880 return files
852 881
853 882 def _ignorefileandline(self, f):
854 883 files = collections.deque(self._ignorefiles())
855 884 visited = set()
856 885 while files:
857 886 i = files.popleft()
858 887 patterns = matchmod.readpatternfile(
859 888 i, self._ui.warn, sourceinfo=True
860 889 )
861 890 for pattern, lineno, line in patterns:
862 891 kind, p = matchmod._patsplit(pattern, b'glob')
863 892 if kind == b"subinclude":
864 893 if p not in visited:
865 894 files.append(p)
866 895 continue
867 896 m = matchmod.match(
868 897 self._root, b'', [], [pattern], warn=self._ui.warn
869 898 )
870 899 if m(f):
871 900 return (i, lineno, line)
872 901 visited.add(i)
873 902 return (None, -1, b"")
874 903
875 904 def _walkexplicit(self, match, subrepos):
876 905 """Get stat data about the files explicitly specified by match.
877 906
878 907 Return a triple (results, dirsfound, dirsnotfound).
879 908 - results is a mapping from filename to stat result. It also contains
880 909 listings mapping subrepos and .hg to None.
881 910 - dirsfound is a list of files found to be directories.
882 911 - dirsnotfound is a list of files that the dirstate thinks are
883 912 directories and that were not found."""
884 913
885 914 def badtype(mode):
886 915 kind = _(b'unknown')
887 916 if stat.S_ISCHR(mode):
888 917 kind = _(b'character device')
889 918 elif stat.S_ISBLK(mode):
890 919 kind = _(b'block device')
891 920 elif stat.S_ISFIFO(mode):
892 921 kind = _(b'fifo')
893 922 elif stat.S_ISSOCK(mode):
894 923 kind = _(b'socket')
895 924 elif stat.S_ISDIR(mode):
896 925 kind = _(b'directory')
897 926 return _(b'unsupported file type (type is %s)') % kind
898 927
899 928 badfn = match.bad
900 929 dmap = self._map
901 930 lstat = os.lstat
902 931 getkind = stat.S_IFMT
903 932 dirkind = stat.S_IFDIR
904 933 regkind = stat.S_IFREG
905 934 lnkkind = stat.S_IFLNK
906 935 join = self._join
907 936 dirsfound = []
908 937 foundadd = dirsfound.append
909 938 dirsnotfound = []
910 939 notfoundadd = dirsnotfound.append
911 940
912 941 if not match.isexact() and self._checkcase:
913 942 normalize = self._normalize
914 943 else:
915 944 normalize = None
916 945
917 946 files = sorted(match.files())
918 947 subrepos.sort()
919 948 i, j = 0, 0
920 949 while i < len(files) and j < len(subrepos):
921 950 subpath = subrepos[j] + b"/"
922 951 if files[i] < subpath:
923 952 i += 1
924 953 continue
925 954 while i < len(files) and files[i].startswith(subpath):
926 955 del files[i]
927 956 j += 1
928 957
929 958 if not files or b'' in files:
930 959 files = [b'']
931 960 # constructing the foldmap is expensive, so don't do it for the
932 961 # common case where files is ['']
933 962 normalize = None
934 963 results = dict.fromkeys(subrepos)
935 964 results[b'.hg'] = None
936 965
937 966 for ff in files:
938 967 if normalize:
939 968 nf = normalize(ff, False, True)
940 969 else:
941 970 nf = ff
942 971 if nf in results:
943 972 continue
944 973
945 974 try:
946 975 st = lstat(join(nf))
947 976 kind = getkind(st.st_mode)
948 977 if kind == dirkind:
949 978 if nf in dmap:
950 979 # file replaced by dir on disk but still in dirstate
951 980 results[nf] = None
952 981 foundadd((nf, ff))
953 982 elif kind == regkind or kind == lnkkind:
954 983 results[nf] = st
955 984 else:
956 985 badfn(ff, badtype(kind))
957 986 if nf in dmap:
958 987 results[nf] = None
959 988 except OSError as inst: # nf not found on disk - it is dirstate only
960 989 if nf in dmap: # does it exactly match a missing file?
961 990 results[nf] = None
962 991 else: # does it match a missing directory?
963 992 if self._map.hasdir(nf):
964 993 notfoundadd(nf)
965 994 else:
966 995 badfn(ff, encoding.strtolocal(inst.strerror))
967 996
968 997 # match.files() may contain explicitly-specified paths that shouldn't
969 998 # be taken; drop them from the list of files found. dirsfound/notfound
970 999 # aren't filtered here because they will be tested later.
971 1000 if match.anypats():
972 1001 for f in list(results):
973 1002 if f == b'.hg' or f in subrepos:
974 1003 # keep sentinel to disable further out-of-repo walks
975 1004 continue
976 1005 if not match(f):
977 1006 del results[f]
978 1007
979 1008 # Case insensitive filesystems cannot rely on lstat() failing to detect
980 1009 # a case-only rename. Prune the stat object for any file that does not
981 1010 # match the case in the filesystem, if there are multiple files that
982 1011 # normalize to the same path.
983 1012 if match.isexact() and self._checkcase:
984 1013 normed = {}
985 1014
986 1015 for f, st in pycompat.iteritems(results):
987 1016 if st is None:
988 1017 continue
989 1018
990 1019 nc = util.normcase(f)
991 1020 paths = normed.get(nc)
992 1021
993 1022 if paths is None:
994 1023 paths = set()
995 1024 normed[nc] = paths
996 1025
997 1026 paths.add(f)
998 1027
999 1028 for norm, paths in pycompat.iteritems(normed):
1000 1029 if len(paths) > 1:
1001 1030 for path in paths:
1002 1031 folded = self._discoverpath(
1003 1032 path, norm, True, None, self._map.dirfoldmap
1004 1033 )
1005 1034 if path != folded:
1006 1035 results[path] = None
1007 1036
1008 1037 return results, dirsfound, dirsnotfound
1009 1038
1010 1039 def walk(self, match, subrepos, unknown, ignored, full=True):
1011 1040 """
1012 1041 Walk recursively through the directory tree, finding all files
1013 1042 matched by match.
1014 1043
1015 1044 If full is False, maybe skip some known-clean files.
1016 1045
1017 1046 Return a dict mapping filename to stat-like object (either
1018 1047 mercurial.osutil.stat instance or return value of os.stat()).
1019 1048
1020 1049 """
1021 1050 # full is a flag that extensions that hook into walk can use -- this
1022 1051 # implementation doesn't use it at all. This satisfies the contract
1023 1052 # because we only guarantee a "maybe".
1024 1053
1025 1054 if ignored:
1026 1055 ignore = util.never
1027 1056 dirignore = util.never
1028 1057 elif unknown:
1029 1058 ignore = self._ignore
1030 1059 dirignore = self._dirignore
1031 1060 else:
1032 1061 # if not unknown and not ignored, drop dir recursion and step 2
1033 1062 ignore = util.always
1034 1063 dirignore = util.always
1035 1064
1036 1065 matchfn = match.matchfn
1037 1066 matchalways = match.always()
1038 1067 matchtdir = match.traversedir
1039 1068 dmap = self._map
1040 1069 listdir = util.listdir
1041 1070 lstat = os.lstat
1042 1071 dirkind = stat.S_IFDIR
1043 1072 regkind = stat.S_IFREG
1044 1073 lnkkind = stat.S_IFLNK
1045 1074 join = self._join
1046 1075
1047 1076 exact = skipstep3 = False
1048 1077 if match.isexact(): # match.exact
1049 1078 exact = True
1050 1079 dirignore = util.always # skip step 2
1051 1080 elif match.prefix(): # match.match, no patterns
1052 1081 skipstep3 = True
1053 1082
1054 1083 if not exact and self._checkcase:
1055 1084 normalize = self._normalize
1056 1085 normalizefile = self._normalizefile
1057 1086 skipstep3 = False
1058 1087 else:
1059 1088 normalize = self._normalize
1060 1089 normalizefile = None
1061 1090
1062 1091 # step 1: find all explicit files
1063 1092 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1064 1093 if matchtdir:
1065 1094 for d in work:
1066 1095 matchtdir(d[0])
1067 1096 for d in dirsnotfound:
1068 1097 matchtdir(d)
1069 1098
1070 1099 skipstep3 = skipstep3 and not (work or dirsnotfound)
1071 1100 work = [d for d in work if not dirignore(d[0])]
1072 1101
1073 1102 # step 2: visit subdirectories
1074 1103 def traverse(work, alreadynormed):
1075 1104 wadd = work.append
1076 1105 while work:
1077 1106 tracing.counter('dirstate.walk work', len(work))
1078 1107 nd = work.pop()
1079 1108 visitentries = match.visitchildrenset(nd)
1080 1109 if not visitentries:
1081 1110 continue
1082 1111 if visitentries == b'this' or visitentries == b'all':
1083 1112 visitentries = None
1084 1113 skip = None
1085 1114 if nd != b'':
1086 1115 skip = b'.hg'
1087 1116 try:
1088 1117 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1089 1118 entries = listdir(join(nd), stat=True, skip=skip)
1090 1119 except OSError as inst:
1091 1120 if inst.errno in (errno.EACCES, errno.ENOENT):
1092 1121 match.bad(
1093 1122 self.pathto(nd), encoding.strtolocal(inst.strerror)
1094 1123 )
1095 1124 continue
1096 1125 raise
1097 1126 for f, kind, st in entries:
1098 1127 # Some matchers may return files in the visitentries set,
1099 1128 # instead of 'this', if the matcher explicitly mentions them
1100 1129 # and is not an exactmatcher. This is acceptable; we do not
1101 1130 # make any hard assumptions about file-or-directory below
1102 1131 # based on the presence of `f` in visitentries. If
1103 1132 # visitchildrenset returned a set, we can always skip the
1104 1133 # entries *not* in the set it provided regardless of whether
1105 1134 # they're actually a file or a directory.
1106 1135 if visitentries and f not in visitentries:
1107 1136 continue
1108 1137 if normalizefile:
1109 1138 # even though f might be a directory, we're only
1110 1139 # interested in comparing it to files currently in the
1111 1140 # dmap -- therefore normalizefile is enough
1112 1141 nf = normalizefile(
1113 1142 nd and (nd + b"/" + f) or f, True, True
1114 1143 )
1115 1144 else:
1116 1145 nf = nd and (nd + b"/" + f) or f
1117 1146 if nf not in results:
1118 1147 if kind == dirkind:
1119 1148 if not ignore(nf):
1120 1149 if matchtdir:
1121 1150 matchtdir(nf)
1122 1151 wadd(nf)
1123 1152 if nf in dmap and (matchalways or matchfn(nf)):
1124 1153 results[nf] = None
1125 1154 elif kind == regkind or kind == lnkkind:
1126 1155 if nf in dmap:
1127 1156 if matchalways or matchfn(nf):
1128 1157 results[nf] = st
1129 1158 elif (matchalways or matchfn(nf)) and not ignore(
1130 1159 nf
1131 1160 ):
1132 1161 # unknown file -- normalize if necessary
1133 1162 if not alreadynormed:
1134 1163 nf = normalize(nf, False, True)
1135 1164 results[nf] = st
1136 1165 elif nf in dmap and (matchalways or matchfn(nf)):
1137 1166 results[nf] = None
1138 1167
1139 1168 for nd, d in work:
1140 1169 # alreadynormed means that processwork doesn't have to do any
1141 1170 # expensive directory normalization
1142 1171 alreadynormed = not normalize or nd == d
1143 1172 traverse([d], alreadynormed)
1144 1173
1145 1174 for s in subrepos:
1146 1175 del results[s]
1147 1176 del results[b'.hg']
1148 1177
1149 1178 # step 3: visit remaining files from dmap
1150 1179 if not skipstep3 and not exact:
1151 1180 # If a dmap file is not in results yet, it was either
1152 1181 # a) not matching matchfn b) ignored, c) missing, or d) under a
1153 1182 # symlink directory.
1154 1183 if not results and matchalways:
1155 1184 visit = [f for f in dmap]
1156 1185 else:
1157 1186 visit = [f for f in dmap if f not in results and matchfn(f)]
1158 1187 visit.sort()
1159 1188
1160 1189 if unknown:
1161 1190 # unknown == True means we walked all dirs under the roots
1162 1191 # that wasn't ignored, and everything that matched was stat'ed
1163 1192 # and is already in results.
1164 1193 # The rest must thus be ignored or under a symlink.
1165 1194 audit_path = pathutil.pathauditor(self._root, cached=True)
1166 1195
1167 1196 for nf in iter(visit):
1168 1197 # If a stat for the same file was already added with a
1169 1198 # different case, don't add one for this, since that would
1170 1199 # make it appear as if the file exists under both names
1171 1200 # on disk.
1172 1201 if (
1173 1202 normalizefile
1174 1203 and normalizefile(nf, True, True) in results
1175 1204 ):
1176 1205 results[nf] = None
1177 1206 # Report ignored items in the dmap as long as they are not
1178 1207 # under a symlink directory.
1179 1208 elif audit_path.check(nf):
1180 1209 try:
1181 1210 results[nf] = lstat(join(nf))
1182 1211 # file was just ignored, no links, and exists
1183 1212 except OSError:
1184 1213 # file doesn't exist
1185 1214 results[nf] = None
1186 1215 else:
1187 1216 # It's either missing or under a symlink directory
1188 1217 # which we in this case report as missing
1189 1218 results[nf] = None
1190 1219 else:
1191 1220 # We may not have walked the full directory tree above,
1192 1221 # so stat and check everything we missed.
1193 1222 iv = iter(visit)
1194 1223 for st in util.statfiles([join(i) for i in visit]):
1195 1224 results[next(iv)] = st
1196 1225 return results
1197 1226
1198 1227 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1199 1228 # Force Rayon (Rust parallelism library) to respect the number of
1200 1229 # workers. This is a temporary workaround until Rust code knows
1201 1230 # how to read the config file.
1202 1231 numcpus = self._ui.configint(b"worker", b"numcpus")
1203 1232 if numcpus is not None:
1204 1233 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1205 1234
1206 1235 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1207 1236 if not workers_enabled:
1208 1237 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1209 1238
1210 1239 (
1211 1240 lookup,
1212 1241 modified,
1213 1242 added,
1214 1243 removed,
1215 1244 deleted,
1216 1245 clean,
1217 1246 ignored,
1218 1247 unknown,
1219 1248 warnings,
1220 1249 bad,
1221 1250 traversed,
1222 1251 dirty,
1223 1252 ) = rustmod.status(
1224 1253 self._map._rustmap,
1225 1254 matcher,
1226 1255 self._rootdir,
1227 1256 self._ignorefiles(),
1228 1257 self._checkexec,
1229 1258 self._lastnormaltime,
1230 1259 bool(list_clean),
1231 1260 bool(list_ignored),
1232 1261 bool(list_unknown),
1233 1262 bool(matcher.traversedir),
1234 1263 )
1235 1264
1236 1265 self._dirty |= dirty
1237 1266
1238 1267 if matcher.traversedir:
1239 1268 for dir in traversed:
1240 1269 matcher.traversedir(dir)
1241 1270
1242 1271 if self._ui.warn:
1243 1272 for item in warnings:
1244 1273 if isinstance(item, tuple):
1245 1274 file_path, syntax = item
1246 1275 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1247 1276 file_path,
1248 1277 syntax,
1249 1278 )
1250 1279 self._ui.warn(msg)
1251 1280 else:
1252 1281 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1253 1282 self._ui.warn(
1254 1283 msg
1255 1284 % (
1256 1285 pathutil.canonpath(
1257 1286 self._rootdir, self._rootdir, item
1258 1287 ),
1259 1288 b"No such file or directory",
1260 1289 )
1261 1290 )
1262 1291
1263 1292 for (fn, message) in bad:
1264 1293 matcher.bad(fn, encoding.strtolocal(message))
1265 1294
1266 1295 status = scmutil.status(
1267 1296 modified=modified,
1268 1297 added=added,
1269 1298 removed=removed,
1270 1299 deleted=deleted,
1271 1300 unknown=unknown,
1272 1301 ignored=ignored,
1273 1302 clean=clean,
1274 1303 )
1275 1304 return (lookup, status)
1276 1305
1277 1306 def status(self, match, subrepos, ignored, clean, unknown):
1278 1307 """Determine the status of the working copy relative to the
1279 1308 dirstate and return a pair of (unsure, status), where status is of type
1280 1309 scmutil.status and:
1281 1310
1282 1311 unsure:
1283 1312 files that might have been modified since the dirstate was
1284 1313 written, but need to be read to be sure (size is the same
1285 1314 but mtime differs)
1286 1315 status.modified:
1287 1316 files that have definitely been modified since the dirstate
1288 1317 was written (different size or mode)
1289 1318 status.clean:
1290 1319 files that have definitely not been modified since the
1291 1320 dirstate was written
1292 1321 """
1293 1322 listignored, listclean, listunknown = ignored, clean, unknown
1294 1323 lookup, modified, added, unknown, ignored = [], [], [], [], []
1295 1324 removed, deleted, clean = [], [], []
1296 1325
1297 1326 dmap = self._map
1298 1327 dmap.preload()
1299 1328
1300 1329 use_rust = True
1301 1330
1302 1331 allowed_matchers = (
1303 1332 matchmod.alwaysmatcher,
1304 1333 matchmod.exactmatcher,
1305 1334 matchmod.includematcher,
1306 1335 )
1307 1336
1308 1337 if rustmod is None:
1309 1338 use_rust = False
1310 1339 elif self._checkcase:
1311 1340 # Case-insensitive filesystems are not handled yet
1312 1341 use_rust = False
1313 1342 elif subrepos:
1314 1343 use_rust = False
1315 1344 elif sparse.enabled:
1316 1345 use_rust = False
1317 1346 elif not isinstance(match, allowed_matchers):
1318 1347 # Some matchers have yet to be implemented
1319 1348 use_rust = False
1320 1349
1321 1350 if use_rust:
1322 1351 try:
1323 1352 return self._rust_status(
1324 1353 match, listclean, listignored, listunknown
1325 1354 )
1326 1355 except rustmod.FallbackError:
1327 1356 pass
1328 1357
1329 1358 def noop(f):
1330 1359 pass
1331 1360
1332 1361 dcontains = dmap.__contains__
1333 1362 dget = dmap.__getitem__
1334 1363 ladd = lookup.append # aka "unsure"
1335 1364 madd = modified.append
1336 1365 aadd = added.append
1337 1366 uadd = unknown.append if listunknown else noop
1338 1367 iadd = ignored.append if listignored else noop
1339 1368 radd = removed.append
1340 1369 dadd = deleted.append
1341 1370 cadd = clean.append if listclean else noop
1342 1371 mexact = match.exact
1343 1372 dirignore = self._dirignore
1344 1373 checkexec = self._checkexec
1345 1374 copymap = self._map.copymap
1346 1375 lastnormaltime = self._lastnormaltime
1347 1376
1348 1377 # We need to do full walks when either
1349 1378 # - we're listing all clean files, or
1350 1379 # - match.traversedir does something, because match.traversedir should
1351 1380 # be called for every dir in the working dir
1352 1381 full = listclean or match.traversedir is not None
1353 1382 for fn, st in pycompat.iteritems(
1354 1383 self.walk(match, subrepos, listunknown, listignored, full=full)
1355 1384 ):
1356 1385 if not dcontains(fn):
1357 1386 if (listignored or mexact(fn)) and dirignore(fn):
1358 1387 if listignored:
1359 1388 iadd(fn)
1360 1389 else:
1361 1390 uadd(fn)
1362 1391 continue
1363 1392
1364 1393 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1365 1394 # written like that for performance reasons. dmap[fn] is not a
1366 1395 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1367 1396 # opcode has fast paths when the value to be unpacked is a tuple or
1368 1397 # a list, but falls back to creating a full-fledged iterator in
1369 1398 # general. That is much slower than simply accessing and storing the
1370 1399 # tuple members one by one.
1371 1400 t = dget(fn)
1372 1401 mode = t.mode
1373 1402 size = t.size
1374 1403 time = t.mtime
1375 1404
1376 1405 if not st and t.tracked:
1377 1406 dadd(fn)
1378 1407 elif t.merged:
1379 1408 madd(fn)
1380 1409 elif t.added:
1381 1410 aadd(fn)
1382 1411 elif t.removed:
1383 1412 radd(fn)
1384 1413 elif t.tracked:
1385 1414 if (
1386 1415 size >= 0
1387 1416 and (
1388 1417 (size != st.st_size and size != st.st_size & _rangemask)
1389 1418 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1390 1419 )
1391 1420 or t.from_p2
1392 1421 or fn in copymap
1393 1422 ):
1394 1423 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1395 1424 # issue6456: Size returned may be longer due to
1396 1425 # encryption on EXT-4 fscrypt, undecided.
1397 1426 ladd(fn)
1398 1427 else:
1399 1428 madd(fn)
1400 1429 elif (
1401 1430 time != st[stat.ST_MTIME]
1402 1431 and time != st[stat.ST_MTIME] & _rangemask
1403 1432 ):
1404 1433 ladd(fn)
1405 1434 elif st[stat.ST_MTIME] == lastnormaltime:
1406 1435 # fn may have just been marked as normal and it may have
1407 1436 # changed in the same second without changing its size.
1408 1437 # This can happen if we quickly do multiple commits.
1409 1438 # Force lookup, so we don't miss such a racy file change.
1410 1439 ladd(fn)
1411 1440 elif listclean:
1412 1441 cadd(fn)
1413 1442 status = scmutil.status(
1414 1443 modified, added, removed, deleted, unknown, ignored, clean
1415 1444 )
1416 1445 return (lookup, status)
1417 1446
1418 1447 def matches(self, match):
1419 1448 """
1420 1449 return files in the dirstate (in whatever state) filtered by match
1421 1450 """
1422 1451 dmap = self._map
1423 1452 if rustmod is not None:
1424 1453 dmap = self._map._rustmap
1425 1454
1426 1455 if match.always():
1427 1456 return dmap.keys()
1428 1457 files = match.files()
1429 1458 if match.isexact():
1430 1459 # fast path -- filter the other way around, since typically files is
1431 1460 # much smaller than dmap
1432 1461 return [f for f in files if f in dmap]
1433 1462 if match.prefix() and all(fn in dmap for fn in files):
1434 1463 # fast path -- all the values are known to be files, so just return
1435 1464 # that
1436 1465 return list(files)
1437 1466 return [f for f in dmap if match(f)]
1438 1467
1439 1468 def _actualfilename(self, tr):
1440 1469 if tr:
1441 1470 return self._pendingfilename
1442 1471 else:
1443 1472 return self._filename
1444 1473
1445 1474 def savebackup(self, tr, backupname):
1446 1475 '''Save current dirstate into backup file'''
1447 1476 filename = self._actualfilename(tr)
1448 1477 assert backupname != filename
1449 1478
1450 1479 # use '_writedirstate' instead of 'write' to write changes certainly,
1451 1480 # because the latter omits writing out if transaction is running.
1452 1481 # output file will be used to create backup of dirstate at this point.
1453 1482 if self._dirty or not self._opener.exists(filename):
1454 1483 self._writedirstate(
1455 1484 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1456 1485 )
1457 1486
1458 1487 if tr:
1459 1488 # ensure that subsequent tr.writepending returns True for
1460 1489 # changes written out above, even if dirstate is never
1461 1490 # changed after this
1462 1491 tr.addfilegenerator(
1463 1492 b'dirstate',
1464 1493 (self._filename,),
1465 1494 self._writedirstate,
1466 1495 location=b'plain',
1467 1496 )
1468 1497
1469 1498 # ensure that pending file written above is unlinked at
1470 1499 # failure, even if tr.writepending isn't invoked until the
1471 1500 # end of this transaction
1472 1501 tr.registertmp(filename, location=b'plain')
1473 1502
1474 1503 self._opener.tryunlink(backupname)
1475 1504 # hardlink backup is okay because _writedirstate is always called
1476 1505 # with an "atomictemp=True" file.
1477 1506 util.copyfile(
1478 1507 self._opener.join(filename),
1479 1508 self._opener.join(backupname),
1480 1509 hardlink=True,
1481 1510 )
1482 1511
1483 1512 def restorebackup(self, tr, backupname):
1484 1513 '''Restore dirstate by backup file'''
1485 1514 # this "invalidate()" prevents "wlock.release()" from writing
1486 1515 # changes of dirstate out after restoring from backup file
1487 1516 self.invalidate()
1488 1517 filename = self._actualfilename(tr)
1489 1518 o = self._opener
1490 1519 if util.samefile(o.join(backupname), o.join(filename)):
1491 1520 o.unlink(backupname)
1492 1521 else:
1493 1522 o.rename(backupname, filename, checkambig=True)
1494 1523
1495 1524 def clearbackup(self, tr, backupname):
1496 1525 '''Clear backup file'''
1497 1526 self._opener.unlink(backupname)
General Comments 0
You need to be logged in to leave comments. Login now