##// END OF EJS Templates
dirstate: align the dirstate's API to the lower level ones...
marmoute -
r48956:6f54afb0 default
parent child Browse files
Show More
@@ -1,798 +1,788 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 165 def set_tracked(self, f):
166 166 return super(largefilesdirstate, self).set_tracked(unixpath(f))
167 167
168 168 def set_untracked(self, f):
169 169 return super(largefilesdirstate, self).set_untracked(unixpath(f))
170 170
171 171 def normal(self, f, parentfiledata=None):
172 172 # not sure if we should pass the `parentfiledata` down or throw it
173 173 # away. So throwing it away to stay on the safe side.
174 174 return super(largefilesdirstate, self).normal(unixpath(f))
175 175
176 176 def remove(self, f):
177 177 return super(largefilesdirstate, self).remove(unixpath(f))
178 178
179 179 def add(self, f):
180 180 return super(largefilesdirstate, self).add(unixpath(f))
181 181
182 182 def drop(self, f):
183 183 return super(largefilesdirstate, self).drop(unixpath(f))
184 184
185 185 def forget(self, f):
186 186 return super(largefilesdirstate, self).forget(unixpath(f))
187 187
188 188 def normallookup(self, f):
189 189 return super(largefilesdirstate, self).normallookup(unixpath(f))
190 190
191 191 def _ignore(self, f):
192 192 return False
193 193
194 194 def write(self, tr=False):
195 195 # (1) disable PENDING mode always
196 196 # (lfdirstate isn't yet managed as a part of the transaction)
197 197 # (2) avoid develwarn 'use dirstate.write with ....'
198 198 super(largefilesdirstate, self).write(None)
199 199
200 200
201 201 def openlfdirstate(ui, repo, create=True):
202 202 """
203 203 Return a dirstate object that tracks largefiles: i.e. its root is
204 204 the repo root, but it is saved in .hg/largefiles/dirstate.
205 205 """
206 206 vfs = repo.vfs
207 207 lfstoredir = longname
208 208 opener = vfsmod.vfs(vfs.join(lfstoredir))
209 209 use_dirstate_v2 = requirements.DIRSTATE_V2_REQUIREMENT in repo.requirements
210 210 lfdirstate = largefilesdirstate(
211 211 opener,
212 212 ui,
213 213 repo.root,
214 214 repo.dirstate._validate,
215 215 lambda: sparse.matcher(repo),
216 216 repo.nodeconstants,
217 217 use_dirstate_v2,
218 218 )
219 219
220 220 # If the largefiles dirstate does not exist, populate and create
221 221 # it. This ensures that we create it on the first meaningful
222 222 # largefiles operation in a new clone.
223 223 if create and not vfs.exists(vfs.join(lfstoredir, b'dirstate')):
224 224 matcher = getstandinmatcher(repo)
225 225 standins = repo.dirstate.walk(
226 226 matcher, subrepos=[], unknown=False, ignored=False
227 227 )
228 228
229 229 if len(standins) > 0:
230 230 vfs.makedirs(lfstoredir)
231 231
232 232 with lfdirstate.parentchange():
233 233 for standin in standins:
234 234 lfile = splitstandin(standin)
235 235 lfdirstate.update_file(
236 236 lfile, p1_tracked=True, wc_tracked=True, possibly_dirty=True
237 237 )
238 238 return lfdirstate
239 239
240 240
241 241 def lfdirstatestatus(lfdirstate, repo):
242 242 pctx = repo[b'.']
243 243 match = matchmod.always()
244 244 unsure, s = lfdirstate.status(
245 245 match, subrepos=[], ignored=False, clean=False, unknown=False
246 246 )
247 247 modified, clean = s.modified, s.clean
248 248 for lfile in unsure:
249 249 try:
250 250 fctx = pctx[standin(lfile)]
251 251 except LookupError:
252 252 fctx = None
253 253 if not fctx or readasstandin(fctx) != hashfile(repo.wjoin(lfile)):
254 254 modified.append(lfile)
255 255 else:
256 256 clean.append(lfile)
257 257 lfdirstate.set_clean(lfile)
258 258 return s
259 259
260 260
261 261 def listlfiles(repo, rev=None, matcher=None):
262 262 """return a list of largefiles in the working copy or the
263 263 specified changeset"""
264 264
265 265 if matcher is None:
266 266 matcher = getstandinmatcher(repo)
267 267
268 268 # ignore unknown files in working directory
269 269 return [
270 270 splitstandin(f)
271 271 for f in repo[rev].walk(matcher)
272 272 if rev is not None or repo.dirstate.get_entry(f).any_tracked
273 273 ]
274 274
275 275
276 276 def instore(repo, hash, forcelocal=False):
277 277 '''Return true if a largefile with the given hash exists in the store'''
278 278 return os.path.exists(storepath(repo, hash, forcelocal))
279 279
280 280
281 281 def storepath(repo, hash, forcelocal=False):
282 282 """Return the correct location in the repository largefiles store for a
283 283 file with the given hash."""
284 284 if not forcelocal and repo.shared():
285 285 return repo.vfs.reljoin(repo.sharedpath, longname, hash)
286 286 return repo.vfs.join(longname, hash)
287 287
288 288
289 289 def findstorepath(repo, hash):
290 290 """Search through the local store path(s) to find the file for the given
291 291 hash. If the file is not found, its path in the primary store is returned.
292 292 The return value is a tuple of (path, exists(path)).
293 293 """
294 294 # For shared repos, the primary store is in the share source. But for
295 295 # backward compatibility, force a lookup in the local store if it wasn't
296 296 # found in the share source.
297 297 path = storepath(repo, hash, False)
298 298
299 299 if instore(repo, hash):
300 300 return (path, True)
301 301 elif repo.shared() and instore(repo, hash, True):
302 302 return storepath(repo, hash, True), True
303 303
304 304 return (path, False)
305 305
306 306
307 307 def copyfromcache(repo, hash, filename):
308 308 """Copy the specified largefile from the repo or system cache to
309 309 filename in the repository. Return true on success or false if the
310 310 file was not found in either cache (which should not happened:
311 311 this is meant to be called only after ensuring that the needed
312 312 largefile exists in the cache)."""
313 313 wvfs = repo.wvfs
314 314 path = findfile(repo, hash)
315 315 if path is None:
316 316 return False
317 317 wvfs.makedirs(wvfs.dirname(wvfs.join(filename)))
318 318 # The write may fail before the file is fully written, but we
319 319 # don't use atomic writes in the working copy.
320 320 with open(path, b'rb') as srcfd, wvfs(filename, b'wb') as destfd:
321 321 gothash = copyandhash(util.filechunkiter(srcfd), destfd)
322 322 if gothash != hash:
323 323 repo.ui.warn(
324 324 _(b'%s: data corruption in %s with hash %s\n')
325 325 % (filename, path, gothash)
326 326 )
327 327 wvfs.unlink(filename)
328 328 return False
329 329 return True
330 330
331 331
332 332 def copytostore(repo, ctx, file, fstandin):
333 333 wvfs = repo.wvfs
334 334 hash = readasstandin(ctx[fstandin])
335 335 if instore(repo, hash):
336 336 return
337 337 if wvfs.exists(file):
338 338 copytostoreabsolute(repo, wvfs.join(file), hash)
339 339 else:
340 340 repo.ui.warn(
341 341 _(b"%s: largefile %s not available from local store\n")
342 342 % (file, hash)
343 343 )
344 344
345 345
346 346 def copyalltostore(repo, node):
347 347 '''Copy all largefiles in a given revision to the store'''
348 348
349 349 ctx = repo[node]
350 350 for filename in ctx.files():
351 351 realfile = splitstandin(filename)
352 352 if realfile is not None and filename in ctx.manifest():
353 353 copytostore(repo, ctx, realfile, filename)
354 354
355 355
356 356 def copytostoreabsolute(repo, file, hash):
357 357 if inusercache(repo.ui, hash):
358 358 link(usercachepath(repo.ui, hash), storepath(repo, hash))
359 359 else:
360 360 util.makedirs(os.path.dirname(storepath(repo, hash)))
361 361 with open(file, b'rb') as srcf:
362 362 with util.atomictempfile(
363 363 storepath(repo, hash), createmode=repo.store.createmode
364 364 ) as dstf:
365 365 for chunk in util.filechunkiter(srcf):
366 366 dstf.write(chunk)
367 367 linktousercache(repo, hash)
368 368
369 369
370 370 def linktousercache(repo, hash):
371 371 """Link / copy the largefile with the specified hash from the store
372 372 to the cache."""
373 373 path = usercachepath(repo.ui, hash)
374 374 link(storepath(repo, hash), path)
375 375
376 376
377 377 def getstandinmatcher(repo, rmatcher=None):
378 378 '''Return a match object that applies rmatcher to the standin directory'''
379 379 wvfs = repo.wvfs
380 380 standindir = shortname
381 381
382 382 # no warnings about missing files or directories
383 383 badfn = lambda f, msg: None
384 384
385 385 if rmatcher and not rmatcher.always():
386 386 pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()]
387 387 if not pats:
388 388 pats = [wvfs.join(standindir)]
389 389 match = scmutil.match(repo[None], pats, badfn=badfn)
390 390 else:
391 391 # no patterns: relative to repo root
392 392 match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn)
393 393 return match
394 394
395 395
396 396 def composestandinmatcher(repo, rmatcher):
397 397 """Return a matcher that accepts standins corresponding to the
398 398 files accepted by rmatcher. Pass the list of files in the matcher
399 399 as the paths specified by the user."""
400 400 smatcher = getstandinmatcher(repo, rmatcher)
401 401 isstandin = smatcher.matchfn
402 402
403 403 def composedmatchfn(f):
404 404 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
405 405
406 406 smatcher.matchfn = composedmatchfn
407 407
408 408 return smatcher
409 409
410 410
411 411 def standin(filename):
412 412 """Return the repo-relative path to the standin for the specified big
413 413 file."""
414 414 # Notes:
415 415 # 1) Some callers want an absolute path, but for instance addlargefiles
416 416 # needs it repo-relative so it can be passed to repo[None].add(). So
417 417 # leave it up to the caller to use repo.wjoin() to get an absolute path.
418 418 # 2) Join with '/' because that's what dirstate always uses, even on
419 419 # Windows. Change existing separator to '/' first in case we are
420 420 # passed filenames from an external source (like the command line).
421 421 return shortnameslash + util.pconvert(filename)
422 422
423 423
424 424 def isstandin(filename):
425 425 """Return true if filename is a big file standin. filename must be
426 426 in Mercurial's internal form (slash-separated)."""
427 427 return filename.startswith(shortnameslash)
428 428
429 429
430 430 def splitstandin(filename):
431 431 # Split on / because that's what dirstate always uses, even on Windows.
432 432 # Change local separator to / first just in case we are passed filenames
433 433 # from an external source (like the command line).
434 434 bits = util.pconvert(filename).split(b'/', 1)
435 435 if len(bits) == 2 and bits[0] == shortname:
436 436 return bits[1]
437 437 else:
438 438 return None
439 439
440 440
441 441 def updatestandin(repo, lfile, standin):
442 442 """Re-calculate hash value of lfile and write it into standin
443 443
444 444 This assumes that "lfutil.standin(lfile) == standin", for efficiency.
445 445 """
446 446 file = repo.wjoin(lfile)
447 447 if repo.wvfs.exists(lfile):
448 448 hash = hashfile(file)
449 449 executable = getexecutable(file)
450 450 writestandin(repo, standin, hash, executable)
451 451 else:
452 452 raise error.Abort(_(b'%s: file not found!') % lfile)
453 453
454 454
455 455 def readasstandin(fctx):
456 456 """read hex hash from given filectx of standin file
457 457
458 458 This encapsulates how "standin" data is stored into storage layer."""
459 459 return fctx.data().strip()
460 460
461 461
462 462 def writestandin(repo, standin, hash, executable):
463 463 '''write hash to <repo.root>/<standin>'''
464 464 repo.wwrite(standin, hash + b'\n', executable and b'x' or b'')
465 465
466 466
467 467 def copyandhash(instream, outfile):
468 468 """Read bytes from instream (iterable) and write them to outfile,
469 469 computing the SHA-1 hash of the data along the way. Return the hash."""
470 470 hasher = hashutil.sha1(b'')
471 471 for data in instream:
472 472 hasher.update(data)
473 473 outfile.write(data)
474 474 return hex(hasher.digest())
475 475
476 476
477 477 def hashfile(file):
478 478 if not os.path.exists(file):
479 479 return b''
480 480 with open(file, b'rb') as fd:
481 481 return hexsha1(fd)
482 482
483 483
484 484 def getexecutable(filename):
485 485 mode = os.stat(filename).st_mode
486 486 return (
487 487 (mode & stat.S_IXUSR)
488 488 and (mode & stat.S_IXGRP)
489 489 and (mode & stat.S_IXOTH)
490 490 )
491 491
492 492
493 493 def urljoin(first, second, *arg):
494 494 def join(left, right):
495 495 if not left.endswith(b'/'):
496 496 left += b'/'
497 497 if right.startswith(b'/'):
498 498 right = right[1:]
499 499 return left + right
500 500
501 501 url = join(first, second)
502 502 for a in arg:
503 503 url = join(url, a)
504 504 return url
505 505
506 506
507 507 def hexsha1(fileobj):
508 508 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
509 509 object data"""
510 510 h = hashutil.sha1()
511 511 for chunk in util.filechunkiter(fileobj):
512 512 h.update(chunk)
513 513 return hex(h.digest())
514 514
515 515
516 516 def httpsendfile(ui, filename):
517 517 return httpconnection.httpsendfile(ui, filename, b'rb')
518 518
519 519
520 520 def unixpath(path):
521 521 '''Return a version of path normalized for use with the lfdirstate.'''
522 522 return util.pconvert(os.path.normpath(path))
523 523
524 524
525 525 def islfilesrepo(repo):
526 526 '''Return true if the repo is a largefile repo.'''
527 527 if b'largefiles' in repo.requirements and any(
528 528 shortnameslash in f[1] for f in repo.store.datafiles()
529 529 ):
530 530 return True
531 531
532 532 return any(openlfdirstate(repo.ui, repo, False))
533 533
534 534
535 535 class storeprotonotcapable(Exception):
536 536 def __init__(self, storetypes):
537 537 self.storetypes = storetypes
538 538
539 539
540 540 def getstandinsstate(repo):
541 541 standins = []
542 542 matcher = getstandinmatcher(repo)
543 543 wctx = repo[None]
544 544 for standin in repo.dirstate.walk(
545 545 matcher, subrepos=[], unknown=False, ignored=False
546 546 ):
547 547 lfile = splitstandin(standin)
548 548 try:
549 549 hash = readasstandin(wctx[standin])
550 550 except IOError:
551 551 hash = None
552 552 standins.append((lfile, hash))
553 553 return standins
554 554
555 555
556 556 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
557 557 lfstandin = standin(lfile)
558 558 if lfstandin not in repo.dirstate:
559 559 lfdirstate.update_file(lfile, p1_tracked=False, wc_tracked=False)
560 560 else:
561 stat = repo.dirstate.get_entry(lfstandin)
562 state, mtime = stat.state, stat.mtime
563 if state == b'n':
564 if normallookup or mtime < 0 or not repo.wvfs.exists(lfile):
565 # state 'n' doesn't ensure 'clean' in this case
566 lfdirstate.update_file(
567 lfile, p1_tracked=True, wc_tracked=True, possibly_dirty=True
568 )
569 else:
570 lfdirstate.update_file(lfile, p1_tracked=True, wc_tracked=True)
571 elif state == b'm':
572 lfdirstate.update_file(
573 lfile, p1_tracked=True, wc_tracked=True, merged=True
574 )
575 elif state == b'r':
576 lfdirstate.update_file(lfile, p1_tracked=True, wc_tracked=False)
577 elif state == b'a':
578 lfdirstate.update_file(lfile, p1_tracked=False, wc_tracked=True)
561 entry = repo.dirstate.get_entry(lfstandin)
562 lfdirstate.update_file(
563 lfile,
564 wc_tracked=entry.tracked,
565 p1_tracked=entry.p1_tracked,
566 p2_info=entry.p2_info,
567 possibly_dirty=True,
568 )
579 569
580 570
581 571 def markcommitted(orig, ctx, node):
582 572 repo = ctx.repo()
583 573
584 574 lfdirstate = openlfdirstate(repo.ui, repo)
585 575 with lfdirstate.parentchange():
586 576 orig(node)
587 577
588 578 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
589 579 # because files coming from the 2nd parent are omitted in the latter.
590 580 #
591 581 # The former should be used to get targets of "synclfdirstate",
592 582 # because such files:
593 583 # - are marked as "a" by "patch.patch()" (e.g. via transplant), and
594 584 # - have to be marked as "n" after commit, but
595 585 # - aren't listed in "repo[node].files()"
596 586
597 587 for f in ctx.files():
598 588 lfile = splitstandin(f)
599 589 if lfile is not None:
600 590 synclfdirstate(repo, lfdirstate, lfile, False)
601 591 lfdirstate.write()
602 592
603 593 # As part of committing, copy all of the largefiles into the cache.
604 594 #
605 595 # Using "node" instead of "ctx" implies additional "repo[node]"
606 596 # lookup while copyalltostore(), but can omit redundant check for
607 597 # files comming from the 2nd parent, which should exist in store
608 598 # at merging.
609 599 copyalltostore(repo, node)
610 600
611 601
612 602 def getlfilestoupdate(oldstandins, newstandins):
613 603 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
614 604 filelist = []
615 605 for f in changedstandins:
616 606 if f[0] not in filelist:
617 607 filelist.append(f[0])
618 608 return filelist
619 609
620 610
621 611 def getlfilestoupload(repo, missing, addfunc):
622 612 makeprogress = repo.ui.makeprogress
623 613 with makeprogress(
624 614 _(b'finding outgoing largefiles'),
625 615 unit=_(b'revisions'),
626 616 total=len(missing),
627 617 ) as progress:
628 618 for i, n in enumerate(missing):
629 619 progress.update(i)
630 620 parents = [p for p in repo[n].parents() if p != repo.nullid]
631 621
632 622 with lfstatus(repo, value=False):
633 623 ctx = repo[n]
634 624
635 625 files = set(ctx.files())
636 626 if len(parents) == 2:
637 627 mc = ctx.manifest()
638 628 mp1 = ctx.p1().manifest()
639 629 mp2 = ctx.p2().manifest()
640 630 for f in mp1:
641 631 if f not in mc:
642 632 files.add(f)
643 633 for f in mp2:
644 634 if f not in mc:
645 635 files.add(f)
646 636 for f in mc:
647 637 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
648 638 files.add(f)
649 639 for fn in files:
650 640 if isstandin(fn) and fn in ctx:
651 641 addfunc(fn, readasstandin(ctx[fn]))
652 642
653 643
654 644 def updatestandinsbymatch(repo, match):
655 645 """Update standins in the working directory according to specified match
656 646
657 647 This returns (possibly modified) ``match`` object to be used for
658 648 subsequent commit process.
659 649 """
660 650
661 651 ui = repo.ui
662 652
663 653 # Case 1: user calls commit with no specific files or
664 654 # include/exclude patterns: refresh and commit all files that
665 655 # are "dirty".
666 656 if match is None or match.always():
667 657 # Spend a bit of time here to get a list of files we know
668 658 # are modified so we can compare only against those.
669 659 # It can cost a lot of time (several seconds)
670 660 # otherwise to update all standins if the largefiles are
671 661 # large.
672 662 lfdirstate = openlfdirstate(ui, repo)
673 663 dirtymatch = matchmod.always()
674 664 unsure, s = lfdirstate.status(
675 665 dirtymatch, subrepos=[], ignored=False, clean=False, unknown=False
676 666 )
677 667 modifiedfiles = unsure + s.modified + s.added + s.removed
678 668 lfiles = listlfiles(repo)
679 669 # this only loops through largefiles that exist (not
680 670 # removed/renamed)
681 671 for lfile in lfiles:
682 672 if lfile in modifiedfiles:
683 673 fstandin = standin(lfile)
684 674 if repo.wvfs.exists(fstandin):
685 675 # this handles the case where a rebase is being
686 676 # performed and the working copy is not updated
687 677 # yet.
688 678 if repo.wvfs.exists(lfile):
689 679 updatestandin(repo, lfile, fstandin)
690 680
691 681 return match
692 682
693 683 lfiles = listlfiles(repo)
694 684 match._files = repo._subdirlfs(match.files(), lfiles)
695 685
696 686 # Case 2: user calls commit with specified patterns: refresh
697 687 # any matching big files.
698 688 smatcher = composestandinmatcher(repo, match)
699 689 standins = repo.dirstate.walk(
700 690 smatcher, subrepos=[], unknown=False, ignored=False
701 691 )
702 692
703 693 # No matching big files: get out of the way and pass control to
704 694 # the usual commit() method.
705 695 if not standins:
706 696 return match
707 697
708 698 # Refresh all matching big files. It's possible that the
709 699 # commit will end up failing, in which case the big files will
710 700 # stay refreshed. No harm done: the user modified them and
711 701 # asked to commit them, so sooner or later we're going to
712 702 # refresh the standins. Might as well leave them refreshed.
713 703 lfdirstate = openlfdirstate(ui, repo)
714 704 for fstandin in standins:
715 705 lfile = splitstandin(fstandin)
716 706 if lfdirstate.get_entry(lfile).tracked:
717 707 updatestandin(repo, lfile, fstandin)
718 708
719 709 # Cook up a new matcher that only matches regular files or
720 710 # standins corresponding to the big files requested by the
721 711 # user. Have to modify _files to prevent commit() from
722 712 # complaining "not tracked" for big files.
723 713 match = copy.copy(match)
724 714 origmatchfn = match.matchfn
725 715
726 716 # Check both the list of largefiles and the list of
727 717 # standins because if a largefile was removed, it
728 718 # won't be in the list of largefiles at this point
729 719 match._files += sorted(standins)
730 720
731 721 actualfiles = []
732 722 for f in match._files:
733 723 fstandin = standin(f)
734 724
735 725 # For largefiles, only one of the normal and standin should be
736 726 # committed (except if one of them is a remove). In the case of a
737 727 # standin removal, drop the normal file if it is unknown to dirstate.
738 728 # Thus, skip plain largefile names but keep the standin.
739 729 if f in lfiles or fstandin in standins:
740 730 if not repo.dirstate.get_entry(fstandin).removed:
741 731 if not repo.dirstate.get_entry(f).removed:
742 732 continue
743 733 elif not repo.dirstate.get_entry(f).any_tracked:
744 734 continue
745 735
746 736 actualfiles.append(f)
747 737 match._files = actualfiles
748 738
749 739 def matchfn(f):
750 740 if origmatchfn(f):
751 741 return f not in lfiles
752 742 else:
753 743 return f in standins
754 744
755 745 match.matchfn = matchfn
756 746
757 747 return match
758 748
759 749
760 750 class automatedcommithook(object):
761 751 """Stateful hook to update standins at the 1st commit of resuming
762 752
763 753 For efficiency, updating standins in the working directory should
764 754 be avoided while automated committing (like rebase, transplant and
765 755 so on), because they should be updated before committing.
766 756
767 757 But the 1st commit of resuming automated committing (e.g. ``rebase
768 758 --continue``) should update them, because largefiles may be
769 759 modified manually.
770 760 """
771 761
772 762 def __init__(self, resuming):
773 763 self.resuming = resuming
774 764
775 765 def __call__(self, repo, match):
776 766 if self.resuming:
777 767 self.resuming = False # avoids updating at subsequent commits
778 768 return updatestandinsbymatch(repo, match)
779 769 else:
780 770 return match
781 771
782 772
783 773 def getstatuswriter(ui, repo, forcibly=None):
784 774 """Return the function to write largefiles specific status out
785 775
786 776 If ``forcibly`` is ``None``, this returns the last element of
787 777 ``repo._lfstatuswriters`` as "default" writer function.
788 778
789 779 Otherwise, this returns the function to always write out (or
790 780 ignore if ``not forcibly``) status.
791 781 """
792 782 if forcibly is None and util.safehasattr(repo, b'_largefilesenabled'):
793 783 return repo._lfstatuswriters[-1]
794 784 else:
795 785 if forcibly:
796 786 return ui.status # forcibly WRITE OUT
797 787 else:
798 788 return lambda *msg, **opts: None # forcibly IGNORE
@@ -1,1534 +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 = dirstatemap.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 86 def requires_no_parents_change(func):
87 87 def wrap(self, *args, **kwargs):
88 88 if self.pendingparentchange():
89 89 msg = 'calling `%s` inside of a parentchange context'
90 90 msg %= func.__name__
91 91 raise error.ProgrammingError(msg)
92 92 return func(self, *args, **kwargs)
93 93
94 94 return wrap
95 95
96 96
97 97 @interfaceutil.implementer(intdirstate.idirstate)
98 98 class dirstate(object):
99 99 def __init__(
100 100 self,
101 101 opener,
102 102 ui,
103 103 root,
104 104 validate,
105 105 sparsematchfn,
106 106 nodeconstants,
107 107 use_dirstate_v2,
108 108 ):
109 109 """Create a new dirstate object.
110 110
111 111 opener is an open()-like callable that can be used to open the
112 112 dirstate file; root is the root of the directory tracked by
113 113 the dirstate.
114 114 """
115 115 self._use_dirstate_v2 = use_dirstate_v2
116 116 self._nodeconstants = nodeconstants
117 117 self._opener = opener
118 118 self._validate = validate
119 119 self._root = root
120 120 self._sparsematchfn = sparsematchfn
121 121 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
122 122 # UNC path pointing to root share (issue4557)
123 123 self._rootdir = pathutil.normasprefix(root)
124 124 self._dirty = False
125 125 self._lastnormaltime = 0
126 126 self._ui = ui
127 127 self._filecache = {}
128 128 self._parentwriters = 0
129 129 self._filename = b'dirstate'
130 130 self._pendingfilename = b'%s.pending' % self._filename
131 131 self._plchangecallbacks = {}
132 132 self._origpl = None
133 133 self._mapcls = dirstatemap.dirstatemap
134 134 # Access and cache cwd early, so we don't access it for the first time
135 135 # after a working-copy update caused it to not exist (accessing it then
136 136 # raises an exception).
137 137 self._cwd
138 138
139 139 def prefetch_parents(self):
140 140 """make sure the parents are loaded
141 141
142 142 Used to avoid a race condition.
143 143 """
144 144 self._pl
145 145
146 146 @contextlib.contextmanager
147 147 def parentchange(self):
148 148 """Context manager for handling dirstate parents.
149 149
150 150 If an exception occurs in the scope of the context manager,
151 151 the incoherent dirstate won't be written when wlock is
152 152 released.
153 153 """
154 154 self._parentwriters += 1
155 155 yield
156 156 # Typically we want the "undo" step of a context manager in a
157 157 # finally block so it happens even when an exception
158 158 # occurs. In this case, however, we only want to decrement
159 159 # parentwriters if the code in the with statement exits
160 160 # normally, so we don't have a try/finally here on purpose.
161 161 self._parentwriters -= 1
162 162
163 163 def pendingparentchange(self):
164 164 """Returns true if the dirstate is in the middle of a set of changes
165 165 that modify the dirstate parent.
166 166 """
167 167 return self._parentwriters > 0
168 168
169 169 @propertycache
170 170 def _map(self):
171 171 """Return the dirstate contents (see documentation for dirstatemap)."""
172 172 self._map = self._mapcls(
173 173 self._ui,
174 174 self._opener,
175 175 self._root,
176 176 self._nodeconstants,
177 177 self._use_dirstate_v2,
178 178 )
179 179 return self._map
180 180
181 181 @property
182 182 def _sparsematcher(self):
183 183 """The matcher for the sparse checkout.
184 184
185 185 The working directory may not include every file from a manifest. The
186 186 matcher obtained by this property will match a path if it is to be
187 187 included in the working directory.
188 188 """
189 189 # TODO there is potential to cache this property. For now, the matcher
190 190 # is resolved on every access. (But the called function does use a
191 191 # cache to keep the lookup fast.)
192 192 return self._sparsematchfn()
193 193
194 194 @repocache(b'branch')
195 195 def _branch(self):
196 196 try:
197 197 return self._opener.read(b"branch").strip() or b"default"
198 198 except IOError as inst:
199 199 if inst.errno != errno.ENOENT:
200 200 raise
201 201 return b"default"
202 202
203 203 @property
204 204 def _pl(self):
205 205 return self._map.parents()
206 206
207 207 def hasdir(self, d):
208 208 return self._map.hastrackeddir(d)
209 209
210 210 @rootcache(b'.hgignore')
211 211 def _ignore(self):
212 212 files = self._ignorefiles()
213 213 if not files:
214 214 return matchmod.never()
215 215
216 216 pats = [b'include:%s' % f for f in files]
217 217 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
218 218
219 219 @propertycache
220 220 def _slash(self):
221 221 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
222 222
223 223 @propertycache
224 224 def _checklink(self):
225 225 return util.checklink(self._root)
226 226
227 227 @propertycache
228 228 def _checkexec(self):
229 229 return bool(util.checkexec(self._root))
230 230
231 231 @propertycache
232 232 def _checkcase(self):
233 233 return not util.fscasesensitive(self._join(b'.hg'))
234 234
235 235 def _join(self, f):
236 236 # much faster than os.path.join()
237 237 # it's safe because f is always a relative path
238 238 return self._rootdir + f
239 239
240 240 def flagfunc(self, buildfallback):
241 241 if self._checklink and self._checkexec:
242 242
243 243 def f(x):
244 244 try:
245 245 st = os.lstat(self._join(x))
246 246 if util.statislink(st):
247 247 return b'l'
248 248 if util.statisexec(st):
249 249 return b'x'
250 250 except OSError:
251 251 pass
252 252 return b''
253 253
254 254 return f
255 255
256 256 fallback = buildfallback()
257 257 if self._checklink:
258 258
259 259 def f(x):
260 260 if os.path.islink(self._join(x)):
261 261 return b'l'
262 262 if b'x' in fallback(x):
263 263 return b'x'
264 264 return b''
265 265
266 266 return f
267 267 if self._checkexec:
268 268
269 269 def f(x):
270 270 if b'l' in fallback(x):
271 271 return b'l'
272 272 if util.isexec(self._join(x)):
273 273 return b'x'
274 274 return b''
275 275
276 276 return f
277 277 else:
278 278 return fallback
279 279
280 280 @propertycache
281 281 def _cwd(self):
282 282 # internal config: ui.forcecwd
283 283 forcecwd = self._ui.config(b'ui', b'forcecwd')
284 284 if forcecwd:
285 285 return forcecwd
286 286 return encoding.getcwd()
287 287
288 288 def getcwd(self):
289 289 """Return the path from which a canonical path is calculated.
290 290
291 291 This path should be used to resolve file patterns or to convert
292 292 canonical paths back to file paths for display. It shouldn't be
293 293 used to get real file paths. Use vfs functions instead.
294 294 """
295 295 cwd = self._cwd
296 296 if cwd == self._root:
297 297 return b''
298 298 # self._root ends with a path separator if self._root is '/' or 'C:\'
299 299 rootsep = self._root
300 300 if not util.endswithsep(rootsep):
301 301 rootsep += pycompat.ossep
302 302 if cwd.startswith(rootsep):
303 303 return cwd[len(rootsep) :]
304 304 else:
305 305 # we're outside the repo. return an absolute path.
306 306 return cwd
307 307
308 308 def pathto(self, f, cwd=None):
309 309 if cwd is None:
310 310 cwd = self.getcwd()
311 311 path = util.pathto(self._root, cwd, f)
312 312 if self._slash:
313 313 return util.pconvert(path)
314 314 return path
315 315
316 316 def __getitem__(self, key):
317 317 """Return the current state of key (a filename) in the dirstate.
318 318
319 319 States are:
320 320 n normal
321 321 m needs merging
322 322 r marked for removal
323 323 a marked for addition
324 324 ? not tracked
325 325
326 326 XXX The "state" is a bit obscure to be in the "public" API. we should
327 327 consider migrating all user of this to going through the dirstate entry
328 328 instead.
329 329 """
330 330 msg = b"don't use dirstate[file], use dirstate.get_entry(file)"
331 331 util.nouideprecwarn(msg, b'6.1', stacklevel=2)
332 332 entry = self._map.get(key)
333 333 if entry is not None:
334 334 return entry.state
335 335 return b'?'
336 336
337 337 def get_entry(self, path):
338 338 """return a DirstateItem for the associated path"""
339 339 entry = self._map.get(path)
340 340 if entry is None:
341 341 return DirstateItem()
342 342 return entry
343 343
344 344 def __contains__(self, key):
345 345 return key in self._map
346 346
347 347 def __iter__(self):
348 348 return iter(sorted(self._map))
349 349
350 350 def items(self):
351 351 return pycompat.iteritems(self._map)
352 352
353 353 iteritems = items
354 354
355 355 def parents(self):
356 356 return [self._validate(p) for p in self._pl]
357 357
358 358 def p1(self):
359 359 return self._validate(self._pl[0])
360 360
361 361 def p2(self):
362 362 return self._validate(self._pl[1])
363 363
364 364 @property
365 365 def in_merge(self):
366 366 """True if a merge is in progress"""
367 367 return self._pl[1] != self._nodeconstants.nullid
368 368
369 369 def branch(self):
370 370 return encoding.tolocal(self._branch)
371 371
372 372 def setparents(self, p1, p2=None):
373 373 """Set dirstate parents to p1 and p2.
374 374
375 375 When moving from two parents to one, "merged" entries a
376 376 adjusted to normal and previous copy records discarded and
377 377 returned by the call.
378 378
379 379 See localrepo.setparents()
380 380 """
381 381 if p2 is None:
382 382 p2 = self._nodeconstants.nullid
383 383 if self._parentwriters == 0:
384 384 raise ValueError(
385 385 b"cannot set dirstate parent outside of "
386 386 b"dirstate.parentchange context manager"
387 387 )
388 388
389 389 self._dirty = True
390 390 oldp2 = self._pl[1]
391 391 if self._origpl is None:
392 392 self._origpl = self._pl
393 393 nullid = self._nodeconstants.nullid
394 394 # True if we need to fold p2 related state back to a linear case
395 395 fold_p2 = oldp2 != nullid and p2 == nullid
396 396 return self._map.setparents(p1, p2, fold_p2=fold_p2)
397 397
398 398 def setbranch(self, branch):
399 399 self.__class__._branch.set(self, encoding.fromlocal(branch))
400 400 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
401 401 try:
402 402 f.write(self._branch + b'\n')
403 403 f.close()
404 404
405 405 # make sure filecache has the correct stat info for _branch after
406 406 # replacing the underlying file
407 407 ce = self._filecache[b'_branch']
408 408 if ce:
409 409 ce.refresh()
410 410 except: # re-raises
411 411 f.discard()
412 412 raise
413 413
414 414 def invalidate(self):
415 415 """Causes the next access to reread the dirstate.
416 416
417 417 This is different from localrepo.invalidatedirstate() because it always
418 418 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
419 419 check whether the dirstate has changed before rereading it."""
420 420
421 421 for a in ("_map", "_branch", "_ignore"):
422 422 if a in self.__dict__:
423 423 delattr(self, a)
424 424 self._lastnormaltime = 0
425 425 self._dirty = False
426 426 self._parentwriters = 0
427 427 self._origpl = None
428 428
429 429 def copy(self, source, dest):
430 430 """Mark dest as a copy of source. Unmark dest if source is None."""
431 431 if source == dest:
432 432 return
433 433 self._dirty = True
434 434 if source is not None:
435 435 self._map.copymap[dest] = source
436 436 else:
437 437 self._map.copymap.pop(dest, None)
438 438
439 439 def copied(self, file):
440 440 return self._map.copymap.get(file, None)
441 441
442 442 def copies(self):
443 443 return self._map.copymap
444 444
445 445 @requires_no_parents_change
446 446 def set_tracked(self, filename):
447 447 """a "public" method for generic code to mark a file as tracked
448 448
449 449 This function is to be called outside of "update/merge" case. For
450 450 example by a command like `hg add X`.
451 451
452 452 return True the file was previously untracked, False otherwise.
453 453 """
454 454 self._dirty = True
455 455 entry = self._map.get(filename)
456 456 if entry is None or not entry.tracked:
457 457 self._check_new_tracked_filename(filename)
458 458 return self._map.set_tracked(filename)
459 459
460 460 @requires_no_parents_change
461 461 def set_untracked(self, filename):
462 462 """a "public" method for generic code to mark a file as untracked
463 463
464 464 This function is to be called outside of "update/merge" case. For
465 465 example by a command like `hg remove X`.
466 466
467 467 return True the file was previously tracked, False otherwise.
468 468 """
469 469 ret = self._map.set_untracked(filename)
470 470 if ret:
471 471 self._dirty = True
472 472 return ret
473 473
474 474 @requires_no_parents_change
475 475 def set_clean(self, filename, parentfiledata=None):
476 476 """record that the current state of the file on disk is known to be clean"""
477 477 self._dirty = True
478 478 if parentfiledata:
479 479 (mode, size, mtime) = parentfiledata
480 480 else:
481 481 (mode, size, mtime) = self._get_filedata(filename)
482 482 if not self._map[filename].tracked:
483 483 self._check_new_tracked_filename(filename)
484 484 self._map.set_clean(filename, mode, size, mtime)
485 485 if mtime > self._lastnormaltime:
486 486 # Remember the most recent modification timeslot for status(),
487 487 # to make sure we won't miss future size-preserving file content
488 488 # modifications that happen within the same timeslot.
489 489 self._lastnormaltime = mtime
490 490
491 491 @requires_no_parents_change
492 492 def set_possibly_dirty(self, filename):
493 493 """record that the current state of the file on disk is unknown"""
494 494 self._dirty = True
495 495 self._map.set_possibly_dirty(filename)
496 496
497 497 @requires_parents_change
498 498 def update_file_p1(
499 499 self,
500 500 filename,
501 501 p1_tracked,
502 502 ):
503 503 """Set a file as tracked in the parent (or not)
504 504
505 505 This is to be called when adjust the dirstate to a new parent after an history
506 506 rewriting operation.
507 507
508 508 It should not be called during a merge (p2 != nullid) and only within
509 509 a `with dirstate.parentchange():` context.
510 510 """
511 511 if self.in_merge:
512 512 msg = b'update_file_reference should not be called when merging'
513 513 raise error.ProgrammingError(msg)
514 514 entry = self._map.get(filename)
515 515 if entry is None:
516 516 wc_tracked = False
517 517 else:
518 518 wc_tracked = entry.tracked
519 519 if not (p1_tracked or wc_tracked):
520 520 # the file is no longer relevant to anyone
521 521 if self._map.get(filename) is not None:
522 522 self._map.reset_state(filename)
523 523 self._dirty = True
524 524 elif (not p1_tracked) and wc_tracked:
525 525 if entry is not None and entry.added:
526 526 return # avoid dropping copy information (maybe?)
527 527
528 528 parentfiledata = None
529 529 if wc_tracked and p1_tracked:
530 530 parentfiledata = self._get_filedata(filename)
531 531
532 532 self._map.reset_state(
533 533 filename,
534 534 wc_tracked,
535 535 p1_tracked,
536 536 # the underlying reference might have changed, we will have to
537 537 # check it.
538 538 has_meaningful_mtime=False,
539 539 parentfiledata=parentfiledata,
540 540 )
541 541 if (
542 542 parentfiledata is not None
543 543 and parentfiledata[2] > self._lastnormaltime
544 544 ):
545 545 # Remember the most recent modification timeslot for status(),
546 546 # to make sure we won't miss future size-preserving file content
547 547 # modifications that happen within the same timeslot.
548 548 self._lastnormaltime = parentfiledata[2]
549 549
550 550 @requires_parents_change
551 551 def update_file(
552 552 self,
553 553 filename,
554 554 wc_tracked,
555 555 p1_tracked,
556 p2_tracked=False,
557 merged=False,
558 clean_p1=False,
559 clean_p2=False,
556 p2_info=False,
560 557 possibly_dirty=False,
561 558 parentfiledata=None,
562 559 ):
563 560 """update the information about a file in the dirstate
564 561
565 562 This is to be called when the direstates parent changes to keep track
566 563 of what is the file situation in regards to the working copy and its parent.
567 564
568 565 This function must be called within a `dirstate.parentchange` context.
569 566
570 567 note: the API is at an early stage and we might need to adjust it
571 568 depending of what information ends up being relevant and useful to
572 569 other processing.
573 570 """
574 if merged and (clean_p1 or clean_p2):
575 msg = b'`merged` argument incompatible with `clean_p1`/`clean_p2`'
576 raise error.ProgrammingError(msg)
577 571
578 572 # note: I do not think we need to double check name clash here since we
579 573 # are in a update/merge case that should already have taken care of
580 574 # this. The test agrees
581 575
582 576 self._dirty = True
583 577
584 578 need_parent_file_data = (
585 not (possibly_dirty or clean_p2 or merged)
586 and wc_tracked
587 and p1_tracked
579 not possibly_dirty and not p2_info and wc_tracked and p1_tracked
588 580 )
589 581
590 582 # this mean we are doing call for file we do not really care about the
591 583 # data (eg: added or removed), however this should be a minor overhead
592 584 # compared to the overall update process calling this.
593 585 if need_parent_file_data:
594 586 if parentfiledata is None:
595 587 parentfiledata = self._get_filedata(filename)
596 588 mtime = parentfiledata[2]
597 589
598 590 if mtime > self._lastnormaltime:
599 591 # Remember the most recent modification timeslot for
600 592 # status(), to make sure we won't miss future
601 593 # size-preserving file content modifications that happen
602 594 # within the same timeslot.
603 595 self._lastnormaltime = mtime
604 596
605 597 self._map.reset_state(
606 598 filename,
607 599 wc_tracked,
608 600 p1_tracked,
609 p2_info=merged or clean_p2,
601 p2_info=p2_info,
610 602 has_meaningful_mtime=not possibly_dirty,
611 603 parentfiledata=parentfiledata,
612 604 )
613 605 if (
614 606 parentfiledata is not None
615 607 and parentfiledata[2] > self._lastnormaltime
616 608 ):
617 609 # Remember the most recent modification timeslot for status(),
618 610 # to make sure we won't miss future size-preserving file content
619 611 # modifications that happen within the same timeslot.
620 612 self._lastnormaltime = parentfiledata[2]
621 613
622 614 def _check_new_tracked_filename(self, filename):
623 615 scmutil.checkfilename(filename)
624 616 if self._map.hastrackeddir(filename):
625 617 msg = _(b'directory %r already in dirstate')
626 618 msg %= pycompat.bytestr(filename)
627 619 raise error.Abort(msg)
628 620 # shadows
629 621 for d in pathutil.finddirs(filename):
630 622 if self._map.hastrackeddir(d):
631 623 break
632 624 entry = self._map.get(d)
633 625 if entry is not None and not entry.removed:
634 626 msg = _(b'file %r in dirstate clashes with %r')
635 627 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
636 628 raise error.Abort(msg)
637 629
638 630 def _get_filedata(self, filename):
639 631 """returns"""
640 632 s = os.lstat(self._join(filename))
641 633 mode = s.st_mode
642 634 size = s.st_size
643 635 mtime = s[stat.ST_MTIME]
644 636 return (mode, size, mtime)
645 637
646 638 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
647 639 if exists is None:
648 640 exists = os.path.lexists(os.path.join(self._root, path))
649 641 if not exists:
650 642 # Maybe a path component exists
651 643 if not ignoremissing and b'/' in path:
652 644 d, f = path.rsplit(b'/', 1)
653 645 d = self._normalize(d, False, ignoremissing, None)
654 646 folded = d + b"/" + f
655 647 else:
656 648 # No path components, preserve original case
657 649 folded = path
658 650 else:
659 651 # recursively normalize leading directory components
660 652 # against dirstate
661 653 if b'/' in normed:
662 654 d, f = normed.rsplit(b'/', 1)
663 655 d = self._normalize(d, False, ignoremissing, True)
664 656 r = self._root + b"/" + d
665 657 folded = d + b"/" + util.fspath(f, r)
666 658 else:
667 659 folded = util.fspath(normed, self._root)
668 660 storemap[normed] = folded
669 661
670 662 return folded
671 663
672 664 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
673 665 normed = util.normcase(path)
674 666 folded = self._map.filefoldmap.get(normed, None)
675 667 if folded is None:
676 668 if isknown:
677 669 folded = path
678 670 else:
679 671 folded = self._discoverpath(
680 672 path, normed, ignoremissing, exists, self._map.filefoldmap
681 673 )
682 674 return folded
683 675
684 676 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
685 677 normed = util.normcase(path)
686 678 folded = self._map.filefoldmap.get(normed, None)
687 679 if folded is None:
688 680 folded = self._map.dirfoldmap.get(normed, None)
689 681 if folded is None:
690 682 if isknown:
691 683 folded = path
692 684 else:
693 685 # store discovered result in dirfoldmap so that future
694 686 # normalizefile calls don't start matching directories
695 687 folded = self._discoverpath(
696 688 path, normed, ignoremissing, exists, self._map.dirfoldmap
697 689 )
698 690 return folded
699 691
700 692 def normalize(self, path, isknown=False, ignoremissing=False):
701 693 """
702 694 normalize the case of a pathname when on a casefolding filesystem
703 695
704 696 isknown specifies whether the filename came from walking the
705 697 disk, to avoid extra filesystem access.
706 698
707 699 If ignoremissing is True, missing path are returned
708 700 unchanged. Otherwise, we try harder to normalize possibly
709 701 existing path components.
710 702
711 703 The normalized case is determined based on the following precedence:
712 704
713 705 - version of name already stored in the dirstate
714 706 - version of name stored on disk
715 707 - version provided via command arguments
716 708 """
717 709
718 710 if self._checkcase:
719 711 return self._normalize(path, isknown, ignoremissing)
720 712 return path
721 713
722 714 def clear(self):
723 715 self._map.clear()
724 716 self._lastnormaltime = 0
725 717 self._dirty = True
726 718
727 719 def rebuild(self, parent, allfiles, changedfiles=None):
728 720 if changedfiles is None:
729 721 # Rebuild entire dirstate
730 722 to_lookup = allfiles
731 723 to_drop = []
732 724 lastnormaltime = self._lastnormaltime
733 725 self.clear()
734 726 self._lastnormaltime = lastnormaltime
735 727 elif len(changedfiles) < 10:
736 728 # Avoid turning allfiles into a set, which can be expensive if it's
737 729 # large.
738 730 to_lookup = []
739 731 to_drop = []
740 732 for f in changedfiles:
741 733 if f in allfiles:
742 734 to_lookup.append(f)
743 735 else:
744 736 to_drop.append(f)
745 737 else:
746 738 changedfilesset = set(changedfiles)
747 739 to_lookup = changedfilesset & set(allfiles)
748 740 to_drop = changedfilesset - to_lookup
749 741
750 742 if self._origpl is None:
751 743 self._origpl = self._pl
752 744 self._map.setparents(parent, self._nodeconstants.nullid)
753 745
754 746 for f in to_lookup:
755 747
756 748 if self.in_merge:
757 749 self.set_tracked(f)
758 750 else:
759 751 self._map.reset_state(
760 752 f,
761 753 wc_tracked=True,
762 754 p1_tracked=True,
763 755 )
764 756 for f in to_drop:
765 757 self._map.reset_state(f)
766 758
767 759 self._dirty = True
768 760
769 761 def identity(self):
770 762 """Return identity of dirstate itself to detect changing in storage
771 763
772 764 If identity of previous dirstate is equal to this, writing
773 765 changes based on the former dirstate out can keep consistency.
774 766 """
775 767 return self._map.identity
776 768
777 769 def write(self, tr):
778 770 if not self._dirty:
779 771 return
780 772
781 773 filename = self._filename
782 774 if tr:
783 775 # 'dirstate.write()' is not only for writing in-memory
784 776 # changes out, but also for dropping ambiguous timestamp.
785 777 # delayed writing re-raise "ambiguous timestamp issue".
786 778 # See also the wiki page below for detail:
787 779 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
788 780
789 781 # record when mtime start to be ambiguous
790 782 now = _getfsnow(self._opener)
791 783
792 784 # delay writing in-memory changes out
793 785 tr.addfilegenerator(
794 786 b'dirstate',
795 787 (self._filename,),
796 788 lambda f: self._writedirstate(tr, f, now=now),
797 789 location=b'plain',
798 790 )
799 791 return
800 792
801 793 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
802 794 self._writedirstate(tr, st)
803 795
804 796 def addparentchangecallback(self, category, callback):
805 797 """add a callback to be called when the wd parents are changed
806 798
807 799 Callback will be called with the following arguments:
808 800 dirstate, (oldp1, oldp2), (newp1, newp2)
809 801
810 802 Category is a unique identifier to allow overwriting an old callback
811 803 with a newer callback.
812 804 """
813 805 self._plchangecallbacks[category] = callback
814 806
815 807 def _writedirstate(self, tr, st, now=None):
816 808 # notify callbacks about parents change
817 809 if self._origpl is not None and self._origpl != self._pl:
818 810 for c, callback in sorted(
819 811 pycompat.iteritems(self._plchangecallbacks)
820 812 ):
821 813 callback(self, self._origpl, self._pl)
822 814 self._origpl = None
823 815
824 816 if now is None:
825 817 # use the modification time of the newly created temporary file as the
826 818 # filesystem's notion of 'now'
827 819 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
828 820
829 821 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
830 822 # timestamp of each entries in dirstate, because of 'now > mtime'
831 823 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
832 824 if delaywrite > 0:
833 825 # do we have any files to delay for?
834 826 for f, e in pycompat.iteritems(self._map):
835 827 if e.need_delay(now):
836 828 import time # to avoid useless import
837 829
838 830 # rather than sleep n seconds, sleep until the next
839 831 # multiple of n seconds
840 832 clock = time.time()
841 833 start = int(clock) - (int(clock) % delaywrite)
842 834 end = start + delaywrite
843 835 time.sleep(end - clock)
844 836 now = end # trust our estimate that the end is near now
845 837 break
846 838
847 839 self._map.write(tr, st, now)
848 840 self._lastnormaltime = 0
849 841 self._dirty = False
850 842
851 843 def _dirignore(self, f):
852 844 if self._ignore(f):
853 845 return True
854 846 for p in pathutil.finddirs(f):
855 847 if self._ignore(p):
856 848 return True
857 849 return False
858 850
859 851 def _ignorefiles(self):
860 852 files = []
861 853 if os.path.exists(self._join(b'.hgignore')):
862 854 files.append(self._join(b'.hgignore'))
863 855 for name, path in self._ui.configitems(b"ui"):
864 856 if name == b'ignore' or name.startswith(b'ignore.'):
865 857 # we need to use os.path.join here rather than self._join
866 858 # because path is arbitrary and user-specified
867 859 files.append(os.path.join(self._rootdir, util.expandpath(path)))
868 860 return files
869 861
870 862 def _ignorefileandline(self, f):
871 863 files = collections.deque(self._ignorefiles())
872 864 visited = set()
873 865 while files:
874 866 i = files.popleft()
875 867 patterns = matchmod.readpatternfile(
876 868 i, self._ui.warn, sourceinfo=True
877 869 )
878 870 for pattern, lineno, line in patterns:
879 871 kind, p = matchmod._patsplit(pattern, b'glob')
880 872 if kind == b"subinclude":
881 873 if p not in visited:
882 874 files.append(p)
883 875 continue
884 876 m = matchmod.match(
885 877 self._root, b'', [], [pattern], warn=self._ui.warn
886 878 )
887 879 if m(f):
888 880 return (i, lineno, line)
889 881 visited.add(i)
890 882 return (None, -1, b"")
891 883
892 884 def _walkexplicit(self, match, subrepos):
893 885 """Get stat data about the files explicitly specified by match.
894 886
895 887 Return a triple (results, dirsfound, dirsnotfound).
896 888 - results is a mapping from filename to stat result. It also contains
897 889 listings mapping subrepos and .hg to None.
898 890 - dirsfound is a list of files found to be directories.
899 891 - dirsnotfound is a list of files that the dirstate thinks are
900 892 directories and that were not found."""
901 893
902 894 def badtype(mode):
903 895 kind = _(b'unknown')
904 896 if stat.S_ISCHR(mode):
905 897 kind = _(b'character device')
906 898 elif stat.S_ISBLK(mode):
907 899 kind = _(b'block device')
908 900 elif stat.S_ISFIFO(mode):
909 901 kind = _(b'fifo')
910 902 elif stat.S_ISSOCK(mode):
911 903 kind = _(b'socket')
912 904 elif stat.S_ISDIR(mode):
913 905 kind = _(b'directory')
914 906 return _(b'unsupported file type (type is %s)') % kind
915 907
916 908 badfn = match.bad
917 909 dmap = self._map
918 910 lstat = os.lstat
919 911 getkind = stat.S_IFMT
920 912 dirkind = stat.S_IFDIR
921 913 regkind = stat.S_IFREG
922 914 lnkkind = stat.S_IFLNK
923 915 join = self._join
924 916 dirsfound = []
925 917 foundadd = dirsfound.append
926 918 dirsnotfound = []
927 919 notfoundadd = dirsnotfound.append
928 920
929 921 if not match.isexact() and self._checkcase:
930 922 normalize = self._normalize
931 923 else:
932 924 normalize = None
933 925
934 926 files = sorted(match.files())
935 927 subrepos.sort()
936 928 i, j = 0, 0
937 929 while i < len(files) and j < len(subrepos):
938 930 subpath = subrepos[j] + b"/"
939 931 if files[i] < subpath:
940 932 i += 1
941 933 continue
942 934 while i < len(files) and files[i].startswith(subpath):
943 935 del files[i]
944 936 j += 1
945 937
946 938 if not files or b'' in files:
947 939 files = [b'']
948 940 # constructing the foldmap is expensive, so don't do it for the
949 941 # common case where files is ['']
950 942 normalize = None
951 943 results = dict.fromkeys(subrepos)
952 944 results[b'.hg'] = None
953 945
954 946 for ff in files:
955 947 if normalize:
956 948 nf = normalize(ff, False, True)
957 949 else:
958 950 nf = ff
959 951 if nf in results:
960 952 continue
961 953
962 954 try:
963 955 st = lstat(join(nf))
964 956 kind = getkind(st.st_mode)
965 957 if kind == dirkind:
966 958 if nf in dmap:
967 959 # file replaced by dir on disk but still in dirstate
968 960 results[nf] = None
969 961 foundadd((nf, ff))
970 962 elif kind == regkind or kind == lnkkind:
971 963 results[nf] = st
972 964 else:
973 965 badfn(ff, badtype(kind))
974 966 if nf in dmap:
975 967 results[nf] = None
976 968 except OSError as inst: # nf not found on disk - it is dirstate only
977 969 if nf in dmap: # does it exactly match a missing file?
978 970 results[nf] = None
979 971 else: # does it match a missing directory?
980 972 if self._map.hasdir(nf):
981 973 notfoundadd(nf)
982 974 else:
983 975 badfn(ff, encoding.strtolocal(inst.strerror))
984 976
985 977 # match.files() may contain explicitly-specified paths that shouldn't
986 978 # be taken; drop them from the list of files found. dirsfound/notfound
987 979 # aren't filtered here because they will be tested later.
988 980 if match.anypats():
989 981 for f in list(results):
990 982 if f == b'.hg' or f in subrepos:
991 983 # keep sentinel to disable further out-of-repo walks
992 984 continue
993 985 if not match(f):
994 986 del results[f]
995 987
996 988 # Case insensitive filesystems cannot rely on lstat() failing to detect
997 989 # a case-only rename. Prune the stat object for any file that does not
998 990 # match the case in the filesystem, if there are multiple files that
999 991 # normalize to the same path.
1000 992 if match.isexact() and self._checkcase:
1001 993 normed = {}
1002 994
1003 995 for f, st in pycompat.iteritems(results):
1004 996 if st is None:
1005 997 continue
1006 998
1007 999 nc = util.normcase(f)
1008 1000 paths = normed.get(nc)
1009 1001
1010 1002 if paths is None:
1011 1003 paths = set()
1012 1004 normed[nc] = paths
1013 1005
1014 1006 paths.add(f)
1015 1007
1016 1008 for norm, paths in pycompat.iteritems(normed):
1017 1009 if len(paths) > 1:
1018 1010 for path in paths:
1019 1011 folded = self._discoverpath(
1020 1012 path, norm, True, None, self._map.dirfoldmap
1021 1013 )
1022 1014 if path != folded:
1023 1015 results[path] = None
1024 1016
1025 1017 return results, dirsfound, dirsnotfound
1026 1018
1027 1019 def walk(self, match, subrepos, unknown, ignored, full=True):
1028 1020 """
1029 1021 Walk recursively through the directory tree, finding all files
1030 1022 matched by match.
1031 1023
1032 1024 If full is False, maybe skip some known-clean files.
1033 1025
1034 1026 Return a dict mapping filename to stat-like object (either
1035 1027 mercurial.osutil.stat instance or return value of os.stat()).
1036 1028
1037 1029 """
1038 1030 # full is a flag that extensions that hook into walk can use -- this
1039 1031 # implementation doesn't use it at all. This satisfies the contract
1040 1032 # because we only guarantee a "maybe".
1041 1033
1042 1034 if ignored:
1043 1035 ignore = util.never
1044 1036 dirignore = util.never
1045 1037 elif unknown:
1046 1038 ignore = self._ignore
1047 1039 dirignore = self._dirignore
1048 1040 else:
1049 1041 # if not unknown and not ignored, drop dir recursion and step 2
1050 1042 ignore = util.always
1051 1043 dirignore = util.always
1052 1044
1053 1045 matchfn = match.matchfn
1054 1046 matchalways = match.always()
1055 1047 matchtdir = match.traversedir
1056 1048 dmap = self._map
1057 1049 listdir = util.listdir
1058 1050 lstat = os.lstat
1059 1051 dirkind = stat.S_IFDIR
1060 1052 regkind = stat.S_IFREG
1061 1053 lnkkind = stat.S_IFLNK
1062 1054 join = self._join
1063 1055
1064 1056 exact = skipstep3 = False
1065 1057 if match.isexact(): # match.exact
1066 1058 exact = True
1067 1059 dirignore = util.always # skip step 2
1068 1060 elif match.prefix(): # match.match, no patterns
1069 1061 skipstep3 = True
1070 1062
1071 1063 if not exact and self._checkcase:
1072 1064 normalize = self._normalize
1073 1065 normalizefile = self._normalizefile
1074 1066 skipstep3 = False
1075 1067 else:
1076 1068 normalize = self._normalize
1077 1069 normalizefile = None
1078 1070
1079 1071 # step 1: find all explicit files
1080 1072 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1081 1073 if matchtdir:
1082 1074 for d in work:
1083 1075 matchtdir(d[0])
1084 1076 for d in dirsnotfound:
1085 1077 matchtdir(d)
1086 1078
1087 1079 skipstep3 = skipstep3 and not (work or dirsnotfound)
1088 1080 work = [d for d in work if not dirignore(d[0])]
1089 1081
1090 1082 # step 2: visit subdirectories
1091 1083 def traverse(work, alreadynormed):
1092 1084 wadd = work.append
1093 1085 while work:
1094 1086 tracing.counter('dirstate.walk work', len(work))
1095 1087 nd = work.pop()
1096 1088 visitentries = match.visitchildrenset(nd)
1097 1089 if not visitentries:
1098 1090 continue
1099 1091 if visitentries == b'this' or visitentries == b'all':
1100 1092 visitentries = None
1101 1093 skip = None
1102 1094 if nd != b'':
1103 1095 skip = b'.hg'
1104 1096 try:
1105 1097 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1106 1098 entries = listdir(join(nd), stat=True, skip=skip)
1107 1099 except OSError as inst:
1108 1100 if inst.errno in (errno.EACCES, errno.ENOENT):
1109 1101 match.bad(
1110 1102 self.pathto(nd), encoding.strtolocal(inst.strerror)
1111 1103 )
1112 1104 continue
1113 1105 raise
1114 1106 for f, kind, st in entries:
1115 1107 # Some matchers may return files in the visitentries set,
1116 1108 # instead of 'this', if the matcher explicitly mentions them
1117 1109 # and is not an exactmatcher. This is acceptable; we do not
1118 1110 # make any hard assumptions about file-or-directory below
1119 1111 # based on the presence of `f` in visitentries. If
1120 1112 # visitchildrenset returned a set, we can always skip the
1121 1113 # entries *not* in the set it provided regardless of whether
1122 1114 # they're actually a file or a directory.
1123 1115 if visitentries and f not in visitentries:
1124 1116 continue
1125 1117 if normalizefile:
1126 1118 # even though f might be a directory, we're only
1127 1119 # interested in comparing it to files currently in the
1128 1120 # dmap -- therefore normalizefile is enough
1129 1121 nf = normalizefile(
1130 1122 nd and (nd + b"/" + f) or f, True, True
1131 1123 )
1132 1124 else:
1133 1125 nf = nd and (nd + b"/" + f) or f
1134 1126 if nf not in results:
1135 1127 if kind == dirkind:
1136 1128 if not ignore(nf):
1137 1129 if matchtdir:
1138 1130 matchtdir(nf)
1139 1131 wadd(nf)
1140 1132 if nf in dmap and (matchalways or matchfn(nf)):
1141 1133 results[nf] = None
1142 1134 elif kind == regkind or kind == lnkkind:
1143 1135 if nf in dmap:
1144 1136 if matchalways or matchfn(nf):
1145 1137 results[nf] = st
1146 1138 elif (matchalways or matchfn(nf)) and not ignore(
1147 1139 nf
1148 1140 ):
1149 1141 # unknown file -- normalize if necessary
1150 1142 if not alreadynormed:
1151 1143 nf = normalize(nf, False, True)
1152 1144 results[nf] = st
1153 1145 elif nf in dmap and (matchalways or matchfn(nf)):
1154 1146 results[nf] = None
1155 1147
1156 1148 for nd, d in work:
1157 1149 # alreadynormed means that processwork doesn't have to do any
1158 1150 # expensive directory normalization
1159 1151 alreadynormed = not normalize or nd == d
1160 1152 traverse([d], alreadynormed)
1161 1153
1162 1154 for s in subrepos:
1163 1155 del results[s]
1164 1156 del results[b'.hg']
1165 1157
1166 1158 # step 3: visit remaining files from dmap
1167 1159 if not skipstep3 and not exact:
1168 1160 # If a dmap file is not in results yet, it was either
1169 1161 # a) not matching matchfn b) ignored, c) missing, or d) under a
1170 1162 # symlink directory.
1171 1163 if not results and matchalways:
1172 1164 visit = [f for f in dmap]
1173 1165 else:
1174 1166 visit = [f for f in dmap if f not in results and matchfn(f)]
1175 1167 visit.sort()
1176 1168
1177 1169 if unknown:
1178 1170 # unknown == True means we walked all dirs under the roots
1179 1171 # that wasn't ignored, and everything that matched was stat'ed
1180 1172 # and is already in results.
1181 1173 # The rest must thus be ignored or under a symlink.
1182 1174 audit_path = pathutil.pathauditor(self._root, cached=True)
1183 1175
1184 1176 for nf in iter(visit):
1185 1177 # If a stat for the same file was already added with a
1186 1178 # different case, don't add one for this, since that would
1187 1179 # make it appear as if the file exists under both names
1188 1180 # on disk.
1189 1181 if (
1190 1182 normalizefile
1191 1183 and normalizefile(nf, True, True) in results
1192 1184 ):
1193 1185 results[nf] = None
1194 1186 # Report ignored items in the dmap as long as they are not
1195 1187 # under a symlink directory.
1196 1188 elif audit_path.check(nf):
1197 1189 try:
1198 1190 results[nf] = lstat(join(nf))
1199 1191 # file was just ignored, no links, and exists
1200 1192 except OSError:
1201 1193 # file doesn't exist
1202 1194 results[nf] = None
1203 1195 else:
1204 1196 # It's either missing or under a symlink directory
1205 1197 # which we in this case report as missing
1206 1198 results[nf] = None
1207 1199 else:
1208 1200 # We may not have walked the full directory tree above,
1209 1201 # so stat and check everything we missed.
1210 1202 iv = iter(visit)
1211 1203 for st in util.statfiles([join(i) for i in visit]):
1212 1204 results[next(iv)] = st
1213 1205 return results
1214 1206
1215 1207 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1216 1208 # Force Rayon (Rust parallelism library) to respect the number of
1217 1209 # workers. This is a temporary workaround until Rust code knows
1218 1210 # how to read the config file.
1219 1211 numcpus = self._ui.configint(b"worker", b"numcpus")
1220 1212 if numcpus is not None:
1221 1213 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1222 1214
1223 1215 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1224 1216 if not workers_enabled:
1225 1217 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1226 1218
1227 1219 (
1228 1220 lookup,
1229 1221 modified,
1230 1222 added,
1231 1223 removed,
1232 1224 deleted,
1233 1225 clean,
1234 1226 ignored,
1235 1227 unknown,
1236 1228 warnings,
1237 1229 bad,
1238 1230 traversed,
1239 1231 dirty,
1240 1232 ) = rustmod.status(
1241 1233 self._map._map,
1242 1234 matcher,
1243 1235 self._rootdir,
1244 1236 self._ignorefiles(),
1245 1237 self._checkexec,
1246 1238 self._lastnormaltime,
1247 1239 bool(list_clean),
1248 1240 bool(list_ignored),
1249 1241 bool(list_unknown),
1250 1242 bool(matcher.traversedir),
1251 1243 )
1252 1244
1253 1245 self._dirty |= dirty
1254 1246
1255 1247 if matcher.traversedir:
1256 1248 for dir in traversed:
1257 1249 matcher.traversedir(dir)
1258 1250
1259 1251 if self._ui.warn:
1260 1252 for item in warnings:
1261 1253 if isinstance(item, tuple):
1262 1254 file_path, syntax = item
1263 1255 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1264 1256 file_path,
1265 1257 syntax,
1266 1258 )
1267 1259 self._ui.warn(msg)
1268 1260 else:
1269 1261 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1270 1262 self._ui.warn(
1271 1263 msg
1272 1264 % (
1273 1265 pathutil.canonpath(
1274 1266 self._rootdir, self._rootdir, item
1275 1267 ),
1276 1268 b"No such file or directory",
1277 1269 )
1278 1270 )
1279 1271
1280 1272 for (fn, message) in bad:
1281 1273 matcher.bad(fn, encoding.strtolocal(message))
1282 1274
1283 1275 status = scmutil.status(
1284 1276 modified=modified,
1285 1277 added=added,
1286 1278 removed=removed,
1287 1279 deleted=deleted,
1288 1280 unknown=unknown,
1289 1281 ignored=ignored,
1290 1282 clean=clean,
1291 1283 )
1292 1284 return (lookup, status)
1293 1285
1294 1286 def status(self, match, subrepos, ignored, clean, unknown):
1295 1287 """Determine the status of the working copy relative to the
1296 1288 dirstate and return a pair of (unsure, status), where status is of type
1297 1289 scmutil.status and:
1298 1290
1299 1291 unsure:
1300 1292 files that might have been modified since the dirstate was
1301 1293 written, but need to be read to be sure (size is the same
1302 1294 but mtime differs)
1303 1295 status.modified:
1304 1296 files that have definitely been modified since the dirstate
1305 1297 was written (different size or mode)
1306 1298 status.clean:
1307 1299 files that have definitely not been modified since the
1308 1300 dirstate was written
1309 1301 """
1310 1302 listignored, listclean, listunknown = ignored, clean, unknown
1311 1303 lookup, modified, added, unknown, ignored = [], [], [], [], []
1312 1304 removed, deleted, clean = [], [], []
1313 1305
1314 1306 dmap = self._map
1315 1307 dmap.preload()
1316 1308
1317 1309 use_rust = True
1318 1310
1319 1311 allowed_matchers = (
1320 1312 matchmod.alwaysmatcher,
1321 1313 matchmod.exactmatcher,
1322 1314 matchmod.includematcher,
1323 1315 )
1324 1316
1325 1317 if rustmod is None:
1326 1318 use_rust = False
1327 1319 elif self._checkcase:
1328 1320 # Case-insensitive filesystems are not handled yet
1329 1321 use_rust = False
1330 1322 elif subrepos:
1331 1323 use_rust = False
1332 1324 elif sparse.enabled:
1333 1325 use_rust = False
1334 1326 elif not isinstance(match, allowed_matchers):
1335 1327 # Some matchers have yet to be implemented
1336 1328 use_rust = False
1337 1329
1338 1330 if use_rust:
1339 1331 try:
1340 1332 return self._rust_status(
1341 1333 match, listclean, listignored, listunknown
1342 1334 )
1343 1335 except rustmod.FallbackError:
1344 1336 pass
1345 1337
1346 1338 def noop(f):
1347 1339 pass
1348 1340
1349 1341 dcontains = dmap.__contains__
1350 1342 dget = dmap.__getitem__
1351 1343 ladd = lookup.append # aka "unsure"
1352 1344 madd = modified.append
1353 1345 aadd = added.append
1354 1346 uadd = unknown.append if listunknown else noop
1355 1347 iadd = ignored.append if listignored else noop
1356 1348 radd = removed.append
1357 1349 dadd = deleted.append
1358 1350 cadd = clean.append if listclean else noop
1359 1351 mexact = match.exact
1360 1352 dirignore = self._dirignore
1361 1353 checkexec = self._checkexec
1362 1354 copymap = self._map.copymap
1363 1355 lastnormaltime = self._lastnormaltime
1364 1356
1365 1357 # We need to do full walks when either
1366 1358 # - we're listing all clean files, or
1367 1359 # - match.traversedir does something, because match.traversedir should
1368 1360 # be called for every dir in the working dir
1369 1361 full = listclean or match.traversedir is not None
1370 1362 for fn, st in pycompat.iteritems(
1371 1363 self.walk(match, subrepos, listunknown, listignored, full=full)
1372 1364 ):
1373 1365 if not dcontains(fn):
1374 1366 if (listignored or mexact(fn)) and dirignore(fn):
1375 1367 if listignored:
1376 1368 iadd(fn)
1377 1369 else:
1378 1370 uadd(fn)
1379 1371 continue
1380 1372
1381 1373 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1382 1374 # written like that for performance reasons. dmap[fn] is not a
1383 1375 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1384 1376 # opcode has fast paths when the value to be unpacked is a tuple or
1385 1377 # a list, but falls back to creating a full-fledged iterator in
1386 1378 # general. That is much slower than simply accessing and storing the
1387 1379 # tuple members one by one.
1388 1380 t = dget(fn)
1389 1381 mode = t.mode
1390 1382 size = t.size
1391 1383 time = t.mtime
1392 1384
1393 1385 if not st and t.tracked:
1394 1386 dadd(fn)
1395 1387 elif t.merged:
1396 1388 madd(fn)
1397 1389 elif t.added:
1398 1390 aadd(fn)
1399 1391 elif t.removed:
1400 1392 radd(fn)
1401 1393 elif t.tracked:
1402 1394 if (
1403 1395 size >= 0
1404 1396 and (
1405 1397 (size != st.st_size and size != st.st_size & _rangemask)
1406 1398 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1407 1399 )
1408 1400 or t.from_p2
1409 1401 or fn in copymap
1410 1402 ):
1411 1403 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1412 1404 # issue6456: Size returned may be longer due to
1413 1405 # encryption on EXT-4 fscrypt, undecided.
1414 1406 ladd(fn)
1415 1407 else:
1416 1408 madd(fn)
1417 1409 elif (
1418 1410 time != st[stat.ST_MTIME]
1419 1411 and time != st[stat.ST_MTIME] & _rangemask
1420 1412 ):
1421 1413 ladd(fn)
1422 1414 elif st[stat.ST_MTIME] == lastnormaltime:
1423 1415 # fn may have just been marked as normal and it may have
1424 1416 # changed in the same second without changing its size.
1425 1417 # This can happen if we quickly do multiple commits.
1426 1418 # Force lookup, so we don't miss such a racy file change.
1427 1419 ladd(fn)
1428 1420 elif listclean:
1429 1421 cadd(fn)
1430 1422 status = scmutil.status(
1431 1423 modified, added, removed, deleted, unknown, ignored, clean
1432 1424 )
1433 1425 return (lookup, status)
1434 1426
1435 1427 def matches(self, match):
1436 1428 """
1437 1429 return files in the dirstate (in whatever state) filtered by match
1438 1430 """
1439 1431 dmap = self._map
1440 1432 if rustmod is not None:
1441 1433 dmap = self._map._map
1442 1434
1443 1435 if match.always():
1444 1436 return dmap.keys()
1445 1437 files = match.files()
1446 1438 if match.isexact():
1447 1439 # fast path -- filter the other way around, since typically files is
1448 1440 # much smaller than dmap
1449 1441 return [f for f in files if f in dmap]
1450 1442 if match.prefix() and all(fn in dmap for fn in files):
1451 1443 # fast path -- all the values are known to be files, so just return
1452 1444 # that
1453 1445 return list(files)
1454 1446 return [f for f in dmap if match(f)]
1455 1447
1456 1448 def _actualfilename(self, tr):
1457 1449 if tr:
1458 1450 return self._pendingfilename
1459 1451 else:
1460 1452 return self._filename
1461 1453
1462 1454 def savebackup(self, tr, backupname):
1463 1455 '''Save current dirstate into backup file'''
1464 1456 filename = self._actualfilename(tr)
1465 1457 assert backupname != filename
1466 1458
1467 1459 # use '_writedirstate' instead of 'write' to write changes certainly,
1468 1460 # because the latter omits writing out if transaction is running.
1469 1461 # output file will be used to create backup of dirstate at this point.
1470 1462 if self._dirty or not self._opener.exists(filename):
1471 1463 self._writedirstate(
1472 1464 tr,
1473 1465 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1474 1466 )
1475 1467
1476 1468 if tr:
1477 1469 # ensure that subsequent tr.writepending returns True for
1478 1470 # changes written out above, even if dirstate is never
1479 1471 # changed after this
1480 1472 tr.addfilegenerator(
1481 1473 b'dirstate',
1482 1474 (self._filename,),
1483 1475 lambda f: self._writedirstate(tr, f),
1484 1476 location=b'plain',
1485 1477 )
1486 1478
1487 1479 # ensure that pending file written above is unlinked at
1488 1480 # failure, even if tr.writepending isn't invoked until the
1489 1481 # end of this transaction
1490 1482 tr.registertmp(filename, location=b'plain')
1491 1483
1492 1484 self._opener.tryunlink(backupname)
1493 1485 # hardlink backup is okay because _writedirstate is always called
1494 1486 # with an "atomictemp=True" file.
1495 1487 util.copyfile(
1496 1488 self._opener.join(filename),
1497 1489 self._opener.join(backupname),
1498 1490 hardlink=True,
1499 1491 )
1500 1492
1501 1493 def restorebackup(self, tr, backupname):
1502 1494 '''Restore dirstate by backup file'''
1503 1495 # this "invalidate()" prevents "wlock.release()" from writing
1504 1496 # changes of dirstate out after restoring from backup file
1505 1497 self.invalidate()
1506 1498 filename = self._actualfilename(tr)
1507 1499 o = self._opener
1508 1500 if util.samefile(o.join(backupname), o.join(filename)):
1509 1501 o.unlink(backupname)
1510 1502 else:
1511 1503 o.rename(backupname, filename, checkambig=True)
1512 1504
1513 1505 def clearbackup(self, tr, backupname):
1514 1506 '''Clear backup file'''
1515 1507 self._opener.unlink(backupname)
1516 1508
1517 1509 def verify(self, m1, m2):
1518 1510 """check the dirstate content again the parent manifest and yield errors"""
1519 1511 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1520 1512 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1521 1513 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1522 1514 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1523 1515 for f, entry in self.items():
1524 1516 state = entry.state
1525 1517 if state in b"nr" and f not in m1:
1526 1518 yield (missing_from_p1, f, state)
1527 1519 if state in b"a" and f in m1:
1528 1520 yield (unexpected_in_p1, f, state)
1529 1521 if state in b"m" and f not in m1 and f not in m2:
1530 1522 yield (missing_from_ps, f, state)
1531 1523 for f in m1:
1532 1524 state = self.get_entry(f).state
1533 1525 if state not in b"nrm":
1534 1526 yield (missing_from_ds, f, state)
@@ -1,876 +1,871 b''
1 1 from __future__ import absolute_import
2 2
3 3 import collections
4 4 import errno
5 5 import shutil
6 6 import struct
7 7
8 8 from .i18n import _
9 9 from .node import (
10 10 bin,
11 11 hex,
12 12 nullrev,
13 13 )
14 14 from . import (
15 15 error,
16 16 filemerge,
17 17 pycompat,
18 18 util,
19 19 )
20 20 from .utils import hashutil
21 21
22 22 _pack = struct.pack
23 23 _unpack = struct.unpack
24 24
25 25
26 26 def _droponode(data):
27 27 # used for compatibility for v1
28 28 bits = data.split(b'\0')
29 29 bits = bits[:-2] + bits[-1:]
30 30 return b'\0'.join(bits)
31 31
32 32
33 33 def _filectxorabsent(hexnode, ctx, f):
34 34 if hexnode == ctx.repo().nodeconstants.nullhex:
35 35 return filemerge.absentfilectx(ctx, f)
36 36 else:
37 37 return ctx[f]
38 38
39 39
40 40 # Merge state record types. See ``mergestate`` docs for more.
41 41
42 42 ####
43 43 # merge records which records metadata about a current merge
44 44 # exists only once in a mergestate
45 45 #####
46 46 RECORD_LOCAL = b'L'
47 47 RECORD_OTHER = b'O'
48 48 # record merge labels
49 49 RECORD_LABELS = b'l'
50 50
51 51 #####
52 52 # record extra information about files, with one entry containing info about one
53 53 # file. Hence, multiple of them can exists
54 54 #####
55 55 RECORD_FILE_VALUES = b'f'
56 56
57 57 #####
58 58 # merge records which represents state of individual merges of files/folders
59 59 # These are top level records for each entry containing merge related info.
60 60 # Each record of these has info about one file. Hence multiple of them can
61 61 # exists
62 62 #####
63 63 RECORD_MERGED = b'F'
64 64 RECORD_CHANGEDELETE_CONFLICT = b'C'
65 65 # the path was dir on one side of merge and file on another
66 66 RECORD_PATH_CONFLICT = b'P'
67 67
68 68 #####
69 69 # possible state which a merge entry can have. These are stored inside top-level
70 70 # merge records mentioned just above.
71 71 #####
72 72 MERGE_RECORD_UNRESOLVED = b'u'
73 73 MERGE_RECORD_RESOLVED = b'r'
74 74 MERGE_RECORD_UNRESOLVED_PATH = b'pu'
75 75 MERGE_RECORD_RESOLVED_PATH = b'pr'
76 76 # represents that the file was automatically merged in favor
77 77 # of other version. This info is used on commit.
78 78 # This is now deprecated and commit related information is now
79 79 # stored in RECORD_FILE_VALUES
80 80 MERGE_RECORD_MERGED_OTHER = b'o'
81 81
82 82 #####
83 83 # top level record which stores other unknown records. Multiple of these can
84 84 # exists
85 85 #####
86 86 RECORD_OVERRIDE = b't'
87 87
88 88 #####
89 89 # legacy records which are no longer used but kept to prevent breaking BC
90 90 #####
91 91 # This record was release in 5.4 and usage was removed in 5.5
92 92 LEGACY_RECORD_RESOLVED_OTHER = b'R'
93 93 # This record was release in 3.7 and usage was removed in 5.6
94 94 LEGACY_RECORD_DRIVER_RESOLVED = b'd'
95 95 # This record was release in 3.7 and usage was removed in 5.6
96 96 LEGACY_MERGE_DRIVER_STATE = b'm'
97 97 # This record was release in 3.7 and usage was removed in 5.6
98 98 LEGACY_MERGE_DRIVER_MERGE = b'D'
99 99
100 100
101 101 ACTION_FORGET = b'f'
102 102 ACTION_REMOVE = b'r'
103 103 ACTION_ADD = b'a'
104 104 ACTION_GET = b'g'
105 105 ACTION_PATH_CONFLICT = b'p'
106 106 ACTION_PATH_CONFLICT_RESOLVE = b'pr'
107 107 ACTION_ADD_MODIFIED = b'am'
108 108 ACTION_CREATED = b'c'
109 109 ACTION_DELETED_CHANGED = b'dc'
110 110 ACTION_CHANGED_DELETED = b'cd'
111 111 ACTION_MERGE = b'm'
112 112 ACTION_LOCAL_DIR_RENAME_GET = b'dg'
113 113 ACTION_DIR_RENAME_MOVE_LOCAL = b'dm'
114 114 ACTION_KEEP = b'k'
115 115 # the file was absent on local side before merge and we should
116 116 # keep it absent (absent means file not present, it can be a result
117 117 # of file deletion, rename etc.)
118 118 ACTION_KEEP_ABSENT = b'ka'
119 119 # the file is absent on the ancestor and remote side of the merge
120 120 # hence this file is new and we should keep it
121 121 ACTION_KEEP_NEW = b'kn'
122 122 ACTION_EXEC = b'e'
123 123 ACTION_CREATED_MERGE = b'cm'
124 124
125 125 # actions which are no op
126 126 NO_OP_ACTIONS = (
127 127 ACTION_KEEP,
128 128 ACTION_KEEP_ABSENT,
129 129 ACTION_KEEP_NEW,
130 130 )
131 131
132 132
133 133 class _mergestate_base(object):
134 134 """track 3-way merge state of individual files
135 135
136 136 The merge state is stored on disk when needed. Two files are used: one with
137 137 an old format (version 1), and one with a new format (version 2). Version 2
138 138 stores a superset of the data in version 1, including new kinds of records
139 139 in the future. For more about the new format, see the documentation for
140 140 `_readrecordsv2`.
141 141
142 142 Each record can contain arbitrary content, and has an associated type. This
143 143 `type` should be a letter. If `type` is uppercase, the record is mandatory:
144 144 versions of Mercurial that don't support it should abort. If `type` is
145 145 lowercase, the record can be safely ignored.
146 146
147 147 Currently known records:
148 148
149 149 L: the node of the "local" part of the merge (hexified version)
150 150 O: the node of the "other" part of the merge (hexified version)
151 151 F: a file to be merged entry
152 152 C: a change/delete or delete/change conflict
153 153 P: a path conflict (file vs directory)
154 154 f: a (filename, dictionary) tuple of optional values for a given file
155 155 l: the labels for the parts of the merge.
156 156
157 157 Merge record states (stored in self._state, indexed by filename):
158 158 u: unresolved conflict
159 159 r: resolved conflict
160 160 pu: unresolved path conflict (file conflicts with directory)
161 161 pr: resolved path conflict
162 162 o: file was merged in favor of other parent of merge (DEPRECATED)
163 163
164 164 The resolve command transitions between 'u' and 'r' for conflicts and
165 165 'pu' and 'pr' for path conflicts.
166 166 """
167 167
168 168 def __init__(self, repo):
169 169 """Initialize the merge state.
170 170
171 171 Do not use this directly! Instead call read() or clean()."""
172 172 self._repo = repo
173 173 self._state = {}
174 174 self._stateextras = collections.defaultdict(dict)
175 175 self._local = None
176 176 self._other = None
177 177 self._labels = None
178 178 # contains a mapping of form:
179 179 # {filename : (merge_return_value, action_to_be_performed}
180 180 # these are results of re-running merge process
181 181 # this dict is used to perform actions on dirstate caused by re-running
182 182 # the merge
183 183 self._results = {}
184 184 self._dirty = False
185 185
186 186 def reset(self):
187 187 pass
188 188
189 189 def start(self, node, other, labels=None):
190 190 self._local = node
191 191 self._other = other
192 192 self._labels = labels
193 193
194 194 @util.propertycache
195 195 def local(self):
196 196 if self._local is None:
197 197 msg = b"local accessed but self._local isn't set"
198 198 raise error.ProgrammingError(msg)
199 199 return self._local
200 200
201 201 @util.propertycache
202 202 def localctx(self):
203 203 return self._repo[self.local]
204 204
205 205 @util.propertycache
206 206 def other(self):
207 207 if self._other is None:
208 208 msg = b"other accessed but self._other isn't set"
209 209 raise error.ProgrammingError(msg)
210 210 return self._other
211 211
212 212 @util.propertycache
213 213 def otherctx(self):
214 214 return self._repo[self.other]
215 215
216 216 def active(self):
217 217 """Whether mergestate is active.
218 218
219 219 Returns True if there appears to be mergestate. This is a rough proxy
220 220 for "is a merge in progress."
221 221 """
222 222 return bool(self._local) or bool(self._state)
223 223
224 224 def commit(self):
225 225 """Write current state on disk (if necessary)"""
226 226
227 227 @staticmethod
228 228 def getlocalkey(path):
229 229 """hash the path of a local file context for storage in the .hg/merge
230 230 directory."""
231 231
232 232 return hex(hashutil.sha1(path).digest())
233 233
234 234 def _make_backup(self, fctx, localkey):
235 235 raise NotImplementedError()
236 236
237 237 def _restore_backup(self, fctx, localkey, flags):
238 238 raise NotImplementedError()
239 239
240 240 def add(self, fcl, fco, fca, fd):
241 241 """add a new (potentially?) conflicting file the merge state
242 242 fcl: file context for local,
243 243 fco: file context for remote,
244 244 fca: file context for ancestors,
245 245 fd: file path of the resulting merge.
246 246
247 247 note: also write the local version to the `.hg/merge` directory.
248 248 """
249 249 if fcl.isabsent():
250 250 localkey = self._repo.nodeconstants.nullhex
251 251 else:
252 252 localkey = mergestate.getlocalkey(fcl.path())
253 253 self._make_backup(fcl, localkey)
254 254 self._state[fd] = [
255 255 MERGE_RECORD_UNRESOLVED,
256 256 localkey,
257 257 fcl.path(),
258 258 fca.path(),
259 259 hex(fca.filenode()),
260 260 fco.path(),
261 261 hex(fco.filenode()),
262 262 fcl.flags(),
263 263 ]
264 264 self._stateextras[fd][b'ancestorlinknode'] = hex(fca.node())
265 265 self._dirty = True
266 266
267 267 def addpathconflict(self, path, frename, forigin):
268 268 """add a new conflicting path to the merge state
269 269 path: the path that conflicts
270 270 frename: the filename the conflicting file was renamed to
271 271 forigin: origin of the file ('l' or 'r' for local/remote)
272 272 """
273 273 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin]
274 274 self._dirty = True
275 275
276 276 def addcommitinfo(self, path, data):
277 277 """stores information which is required at commit
278 278 into _stateextras"""
279 279 self._stateextras[path].update(data)
280 280 self._dirty = True
281 281
282 282 def __contains__(self, dfile):
283 283 return dfile in self._state
284 284
285 285 def __getitem__(self, dfile):
286 286 return self._state[dfile][0]
287 287
288 288 def __iter__(self):
289 289 return iter(sorted(self._state))
290 290
291 291 def files(self):
292 292 return self._state.keys()
293 293
294 294 def mark(self, dfile, state):
295 295 self._state[dfile][0] = state
296 296 self._dirty = True
297 297
298 298 def unresolved(self):
299 299 """Obtain the paths of unresolved files."""
300 300
301 301 for f, entry in pycompat.iteritems(self._state):
302 302 if entry[0] in (
303 303 MERGE_RECORD_UNRESOLVED,
304 304 MERGE_RECORD_UNRESOLVED_PATH,
305 305 ):
306 306 yield f
307 307
308 308 def allextras(self):
309 309 """return all extras information stored with the mergestate"""
310 310 return self._stateextras
311 311
312 312 def extras(self, filename):
313 313 """return extras stored with the mergestate for the given filename"""
314 314 return self._stateextras[filename]
315 315
316 316 def _resolve(self, preresolve, dfile, wctx):
317 317 """rerun merge process for file path `dfile`.
318 318 Returns whether the merge was completed and the return value of merge
319 319 obtained from filemerge._filemerge().
320 320 """
321 321 if self[dfile] in (
322 322 MERGE_RECORD_RESOLVED,
323 323 LEGACY_RECORD_DRIVER_RESOLVED,
324 324 ):
325 325 return True, 0
326 326 stateentry = self._state[dfile]
327 327 state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry
328 328 octx = self._repo[self._other]
329 329 extras = self.extras(dfile)
330 330 anccommitnode = extras.get(b'ancestorlinknode')
331 331 if anccommitnode:
332 332 actx = self._repo[anccommitnode]
333 333 else:
334 334 actx = None
335 335 fcd = _filectxorabsent(localkey, wctx, dfile)
336 336 fco = _filectxorabsent(onode, octx, ofile)
337 337 # TODO: move this to filectxorabsent
338 338 fca = self._repo.filectx(afile, fileid=anode, changectx=actx)
339 339 # "premerge" x flags
340 340 flo = fco.flags()
341 341 fla = fca.flags()
342 342 if b'x' in flags + flo + fla and b'l' not in flags + flo + fla:
343 343 if fca.rev() == nullrev and flags != flo:
344 344 if preresolve:
345 345 self._repo.ui.warn(
346 346 _(
347 347 b'warning: cannot merge flags for %s '
348 348 b'without common ancestor - keeping local flags\n'
349 349 )
350 350 % afile
351 351 )
352 352 elif flags == fla:
353 353 flags = flo
354 354 if preresolve:
355 355 # restore local
356 356 if localkey != self._repo.nodeconstants.nullhex:
357 357 self._restore_backup(wctx[dfile], localkey, flags)
358 358 else:
359 359 wctx[dfile].remove(ignoremissing=True)
360 360 complete, merge_ret, deleted = filemerge.premerge(
361 361 self._repo,
362 362 wctx,
363 363 self._local,
364 364 lfile,
365 365 fcd,
366 366 fco,
367 367 fca,
368 368 labels=self._labels,
369 369 )
370 370 else:
371 371 complete, merge_ret, deleted = filemerge.filemerge(
372 372 self._repo,
373 373 wctx,
374 374 self._local,
375 375 lfile,
376 376 fcd,
377 377 fco,
378 378 fca,
379 379 labels=self._labels,
380 380 )
381 381 if merge_ret is None:
382 382 # If return value of merge is None, then there are no real conflict
383 383 del self._state[dfile]
384 384 self._dirty = True
385 385 elif not merge_ret:
386 386 self.mark(dfile, MERGE_RECORD_RESOLVED)
387 387
388 388 if complete:
389 389 action = None
390 390 if deleted:
391 391 if fcd.isabsent():
392 392 # dc: local picked. Need to drop if present, which may
393 393 # happen on re-resolves.
394 394 action = ACTION_FORGET
395 395 else:
396 396 # cd: remote picked (or otherwise deleted)
397 397 action = ACTION_REMOVE
398 398 else:
399 399 if fcd.isabsent(): # dc: remote picked
400 400 action = ACTION_GET
401 401 elif fco.isabsent(): # cd: local picked
402 402 if dfile in self.localctx:
403 403 action = ACTION_ADD_MODIFIED
404 404 else:
405 405 action = ACTION_ADD
406 406 # else: regular merges (no action necessary)
407 407 self._results[dfile] = merge_ret, action
408 408
409 409 return complete, merge_ret
410 410
411 411 def preresolve(self, dfile, wctx):
412 412 """run premerge process for dfile
413 413
414 414 Returns whether the merge is complete, and the exit code."""
415 415 return self._resolve(True, dfile, wctx)
416 416
417 417 def resolve(self, dfile, wctx):
418 418 """run merge process (assuming premerge was run) for dfile
419 419
420 420 Returns the exit code of the merge."""
421 421 return self._resolve(False, dfile, wctx)[1]
422 422
423 423 def counts(self):
424 424 """return counts for updated, merged and removed files in this
425 425 session"""
426 426 updated, merged, removed = 0, 0, 0
427 427 for r, action in pycompat.itervalues(self._results):
428 428 if r is None:
429 429 updated += 1
430 430 elif r == 0:
431 431 if action == ACTION_REMOVE:
432 432 removed += 1
433 433 else:
434 434 merged += 1
435 435 return updated, merged, removed
436 436
437 437 def unresolvedcount(self):
438 438 """get unresolved count for this merge (persistent)"""
439 439 return len(list(self.unresolved()))
440 440
441 441 def actions(self):
442 442 """return lists of actions to perform on the dirstate"""
443 443 actions = {
444 444 ACTION_REMOVE: [],
445 445 ACTION_FORGET: [],
446 446 ACTION_ADD: [],
447 447 ACTION_ADD_MODIFIED: [],
448 448 ACTION_GET: [],
449 449 }
450 450 for f, (r, action) in pycompat.iteritems(self._results):
451 451 if action is not None:
452 452 actions[action].append((f, None, b"merge result"))
453 453 return actions
454 454
455 455
456 456 class mergestate(_mergestate_base):
457 457
458 458 statepathv1 = b'merge/state'
459 459 statepathv2 = b'merge/state2'
460 460
461 461 @staticmethod
462 462 def clean(repo):
463 463 """Initialize a brand new merge state, removing any existing state on
464 464 disk."""
465 465 ms = mergestate(repo)
466 466 ms.reset()
467 467 return ms
468 468
469 469 @staticmethod
470 470 def read(repo):
471 471 """Initialize the merge state, reading it from disk."""
472 472 ms = mergestate(repo)
473 473 ms._read()
474 474 return ms
475 475
476 476 def _read(self):
477 477 """Analyse each record content to restore a serialized state from disk
478 478
479 479 This function process "record" entry produced by the de-serialization
480 480 of on disk file.
481 481 """
482 482 unsupported = set()
483 483 records = self._readrecords()
484 484 for rtype, record in records:
485 485 if rtype == RECORD_LOCAL:
486 486 self._local = bin(record)
487 487 elif rtype == RECORD_OTHER:
488 488 self._other = bin(record)
489 489 elif rtype == LEGACY_MERGE_DRIVER_STATE:
490 490 pass
491 491 elif rtype in (
492 492 RECORD_MERGED,
493 493 RECORD_CHANGEDELETE_CONFLICT,
494 494 RECORD_PATH_CONFLICT,
495 495 LEGACY_MERGE_DRIVER_MERGE,
496 496 LEGACY_RECORD_RESOLVED_OTHER,
497 497 ):
498 498 bits = record.split(b'\0')
499 499 # merge entry type MERGE_RECORD_MERGED_OTHER is deprecated
500 500 # and we now store related information in _stateextras, so
501 501 # lets write to _stateextras directly
502 502 if bits[1] == MERGE_RECORD_MERGED_OTHER:
503 503 self._stateextras[bits[0]][b'filenode-source'] = b'other'
504 504 else:
505 505 self._state[bits[0]] = bits[1:]
506 506 elif rtype == RECORD_FILE_VALUES:
507 507 filename, rawextras = record.split(b'\0', 1)
508 508 extraparts = rawextras.split(b'\0')
509 509 extras = {}
510 510 i = 0
511 511 while i < len(extraparts):
512 512 extras[extraparts[i]] = extraparts[i + 1]
513 513 i += 2
514 514
515 515 self._stateextras[filename] = extras
516 516 elif rtype == RECORD_LABELS:
517 517 labels = record.split(b'\0', 2)
518 518 self._labels = [l for l in labels if len(l) > 0]
519 519 elif not rtype.islower():
520 520 unsupported.add(rtype)
521 521
522 522 if unsupported:
523 523 raise error.UnsupportedMergeRecords(unsupported)
524 524
525 525 def _readrecords(self):
526 526 """Read merge state from disk and return a list of record (TYPE, data)
527 527
528 528 We read data from both v1 and v2 files and decide which one to use.
529 529
530 530 V1 has been used by version prior to 2.9.1 and contains less data than
531 531 v2. We read both versions and check if no data in v2 contradicts
532 532 v1. If there is not contradiction we can safely assume that both v1
533 533 and v2 were written at the same time and use the extract data in v2. If
534 534 there is contradiction we ignore v2 content as we assume an old version
535 535 of Mercurial has overwritten the mergestate file and left an old v2
536 536 file around.
537 537
538 538 returns list of record [(TYPE, data), ...]"""
539 539 v1records = self._readrecordsv1()
540 540 v2records = self._readrecordsv2()
541 541 if self._v1v2match(v1records, v2records):
542 542 return v2records
543 543 else:
544 544 # v1 file is newer than v2 file, use it
545 545 # we have to infer the "other" changeset of the merge
546 546 # we cannot do better than that with v1 of the format
547 547 mctx = self._repo[None].parents()[-1]
548 548 v1records.append((RECORD_OTHER, mctx.hex()))
549 549 # add place holder "other" file node information
550 550 # nobody is using it yet so we do no need to fetch the data
551 551 # if mctx was wrong `mctx[bits[-2]]` may fails.
552 552 for idx, r in enumerate(v1records):
553 553 if r[0] == RECORD_MERGED:
554 554 bits = r[1].split(b'\0')
555 555 bits.insert(-2, b'')
556 556 v1records[idx] = (r[0], b'\0'.join(bits))
557 557 return v1records
558 558
559 559 def _v1v2match(self, v1records, v2records):
560 560 oldv2 = set() # old format version of v2 record
561 561 for rec in v2records:
562 562 if rec[0] == RECORD_LOCAL:
563 563 oldv2.add(rec)
564 564 elif rec[0] == RECORD_MERGED:
565 565 # drop the onode data (not contained in v1)
566 566 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
567 567 for rec in v1records:
568 568 if rec not in oldv2:
569 569 return False
570 570 else:
571 571 return True
572 572
573 573 def _readrecordsv1(self):
574 574 """read on disk merge state for version 1 file
575 575
576 576 returns list of record [(TYPE, data), ...]
577 577
578 578 Note: the "F" data from this file are one entry short
579 579 (no "other file node" entry)
580 580 """
581 581 records = []
582 582 try:
583 583 f = self._repo.vfs(self.statepathv1)
584 584 for i, l in enumerate(f):
585 585 if i == 0:
586 586 records.append((RECORD_LOCAL, l[:-1]))
587 587 else:
588 588 records.append((RECORD_MERGED, l[:-1]))
589 589 f.close()
590 590 except IOError as err:
591 591 if err.errno != errno.ENOENT:
592 592 raise
593 593 return records
594 594
595 595 def _readrecordsv2(self):
596 596 """read on disk merge state for version 2 file
597 597
598 598 This format is a list of arbitrary records of the form:
599 599
600 600 [type][length][content]
601 601
602 602 `type` is a single character, `length` is a 4 byte integer, and
603 603 `content` is an arbitrary byte sequence of length `length`.
604 604
605 605 Mercurial versions prior to 3.7 have a bug where if there are
606 606 unsupported mandatory merge records, attempting to clear out the merge
607 607 state with hg update --clean or similar aborts. The 't' record type
608 608 works around that by writing out what those versions treat as an
609 609 advisory record, but later versions interpret as special: the first
610 610 character is the 'real' record type and everything onwards is the data.
611 611
612 612 Returns list of records [(TYPE, data), ...]."""
613 613 records = []
614 614 try:
615 615 f = self._repo.vfs(self.statepathv2)
616 616 data = f.read()
617 617 off = 0
618 618 end = len(data)
619 619 while off < end:
620 620 rtype = data[off : off + 1]
621 621 off += 1
622 622 length = _unpack(b'>I', data[off : (off + 4)])[0]
623 623 off += 4
624 624 record = data[off : (off + length)]
625 625 off += length
626 626 if rtype == RECORD_OVERRIDE:
627 627 rtype, record = record[0:1], record[1:]
628 628 records.append((rtype, record))
629 629 f.close()
630 630 except IOError as err:
631 631 if err.errno != errno.ENOENT:
632 632 raise
633 633 return records
634 634
635 635 def commit(self):
636 636 if self._dirty:
637 637 records = self._makerecords()
638 638 self._writerecords(records)
639 639 self._dirty = False
640 640
641 641 def _makerecords(self):
642 642 records = []
643 643 records.append((RECORD_LOCAL, hex(self._local)))
644 644 records.append((RECORD_OTHER, hex(self._other)))
645 645 # Write out state items. In all cases, the value of the state map entry
646 646 # is written as the contents of the record. The record type depends on
647 647 # the type of state that is stored, and capital-letter records are used
648 648 # to prevent older versions of Mercurial that do not support the feature
649 649 # from loading them.
650 650 for filename, v in pycompat.iteritems(self._state):
651 651 if v[0] in (
652 652 MERGE_RECORD_UNRESOLVED_PATH,
653 653 MERGE_RECORD_RESOLVED_PATH,
654 654 ):
655 655 # Path conflicts. These are stored in 'P' records. The current
656 656 # resolution state ('pu' or 'pr') is stored within the record.
657 657 records.append(
658 658 (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v))
659 659 )
660 660 elif (
661 661 v[1] == self._repo.nodeconstants.nullhex
662 662 or v[6] == self._repo.nodeconstants.nullhex
663 663 ):
664 664 # Change/Delete or Delete/Change conflicts. These are stored in
665 665 # 'C' records. v[1] is the local file, and is nullhex when the
666 666 # file is deleted locally ('dc'). v[6] is the remote file, and
667 667 # is nullhex when the file is deleted remotely ('cd').
668 668 records.append(
669 669 (RECORD_CHANGEDELETE_CONFLICT, b'\0'.join([filename] + v))
670 670 )
671 671 else:
672 672 # Normal files. These are stored in 'F' records.
673 673 records.append((RECORD_MERGED, b'\0'.join([filename] + v)))
674 674 for filename, extras in sorted(pycompat.iteritems(self._stateextras)):
675 675 rawextras = b'\0'.join(
676 676 b'%s\0%s' % (k, v) for k, v in pycompat.iteritems(extras)
677 677 )
678 678 records.append(
679 679 (RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras))
680 680 )
681 681 if self._labels is not None:
682 682 labels = b'\0'.join(self._labels)
683 683 records.append((RECORD_LABELS, labels))
684 684 return records
685 685
686 686 def _writerecords(self, records):
687 687 """Write current state on disk (both v1 and v2)"""
688 688 self._writerecordsv1(records)
689 689 self._writerecordsv2(records)
690 690
691 691 def _writerecordsv1(self, records):
692 692 """Write current state on disk in a version 1 file"""
693 693 f = self._repo.vfs(self.statepathv1, b'wb')
694 694 irecords = iter(records)
695 695 lrecords = next(irecords)
696 696 assert lrecords[0] == RECORD_LOCAL
697 697 f.write(hex(self._local) + b'\n')
698 698 for rtype, data in irecords:
699 699 if rtype == RECORD_MERGED:
700 700 f.write(b'%s\n' % _droponode(data))
701 701 f.close()
702 702
703 703 def _writerecordsv2(self, records):
704 704 """Write current state on disk in a version 2 file
705 705
706 706 See the docstring for _readrecordsv2 for why we use 't'."""
707 707 # these are the records that all version 2 clients can read
708 708 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
709 709 f = self._repo.vfs(self.statepathv2, b'wb')
710 710 for key, data in records:
711 711 assert len(key) == 1
712 712 if key not in allowlist:
713 713 key, data = RECORD_OVERRIDE, b'%s%s' % (key, data)
714 714 format = b'>sI%is' % len(data)
715 715 f.write(_pack(format, key, len(data), data))
716 716 f.close()
717 717
718 718 def _make_backup(self, fctx, localkey):
719 719 self._repo.vfs.write(b'merge/' + localkey, fctx.data())
720 720
721 721 def _restore_backup(self, fctx, localkey, flags):
722 722 with self._repo.vfs(b'merge/' + localkey) as f:
723 723 fctx.write(f.read(), flags)
724 724
725 725 def reset(self):
726 726 shutil.rmtree(self._repo.vfs.join(b'merge'), True)
727 727
728 728
729 729 class memmergestate(_mergestate_base):
730 730 def __init__(self, repo):
731 731 super(memmergestate, self).__init__(repo)
732 732 self._backups = {}
733 733
734 734 def _make_backup(self, fctx, localkey):
735 735 self._backups[localkey] = fctx.data()
736 736
737 737 def _restore_backup(self, fctx, localkey, flags):
738 738 fctx.write(self._backups[localkey], flags)
739 739
740 740
741 741 def recordupdates(repo, actions, branchmerge, getfiledata):
742 742 """record merge actions to the dirstate"""
743 743 # remove (must come first)
744 744 for f, args, msg in actions.get(ACTION_REMOVE, []):
745 745 if branchmerge:
746 746 repo.dirstate.update_file(f, p1_tracked=True, wc_tracked=False)
747 747 else:
748 748 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=False)
749 749
750 750 # forget (must come first)
751 751 for f, args, msg in actions.get(ACTION_FORGET, []):
752 752 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=False)
753 753
754 754 # resolve path conflicts
755 755 for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []):
756 756 (f0, origf0) = args
757 757 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True)
758 758 repo.dirstate.copy(origf0, f)
759 759 if f0 == origf0:
760 760 repo.dirstate.update_file(f0, p1_tracked=True, wc_tracked=False)
761 761 else:
762 762 repo.dirstate.update_file(f0, p1_tracked=False, wc_tracked=False)
763 763
764 764 # re-add
765 765 for f, args, msg in actions.get(ACTION_ADD, []):
766 766 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True)
767 767
768 768 # re-add/mark as modified
769 769 for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []):
770 770 if branchmerge:
771 771 repo.dirstate.update_file(
772 772 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
773 773 )
774 774 else:
775 775 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True)
776 776
777 777 # exec change
778 778 for f, args, msg in actions.get(ACTION_EXEC, []):
779 779 repo.dirstate.update_file(
780 780 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
781 781 )
782 782
783 783 # keep
784 784 for f, args, msg in actions.get(ACTION_KEEP, []):
785 785 pass
786 786
787 787 # keep deleted
788 788 for f, args, msg in actions.get(ACTION_KEEP_ABSENT, []):
789 789 pass
790 790
791 791 # keep new
792 792 for f, args, msg in actions.get(ACTION_KEEP_NEW, []):
793 793 pass
794 794
795 795 # get
796 796 for f, args, msg in actions.get(ACTION_GET, []):
797 797 if branchmerge:
798 798 # tracked in p1 can be True also but update_file should not care
799 799 old_entry = repo.dirstate.get_entry(f)
800 800 p1_tracked = old_entry.any_tracked and not old_entry.added
801 801 repo.dirstate.update_file(
802 802 f,
803 803 p1_tracked=p1_tracked,
804 p2_tracked=True,
805 804 wc_tracked=True,
806 clean_p2=not p1_tracked,
807 merged=p1_tracked,
805 p2_info=True,
808 806 )
809 807 else:
810 808 parentfiledata = getfiledata[f] if getfiledata else None
811 809 repo.dirstate.update_file(
812 810 f,
813 811 p1_tracked=True,
814 812 wc_tracked=True,
815 813 parentfiledata=parentfiledata,
816 814 )
817 815
818 816 # merge
819 817 for f, args, msg in actions.get(ACTION_MERGE, []):
820 818 f1, f2, fa, move, anc = args
821 819 if branchmerge:
822 820 # We've done a branch merge, mark this file as merged
823 821 # so that we properly record the merger later
824 822 p1_tracked = f1 == f
825 p2_tracked = f2 == f
826 823 repo.dirstate.update_file(
827 824 f,
828 825 p1_tracked=p1_tracked,
829 p2_tracked=p2_tracked,
830 826 wc_tracked=True,
831 merged=p1_tracked,
832 clean_p2=not p1_tracked,
827 p2_info=True,
833 828 )
834 829 if f1 != f2: # copy/rename
835 830 if move:
836 831 repo.dirstate.update_file(
837 832 f1, p1_tracked=True, wc_tracked=False
838 833 )
839 834 if f1 != f:
840 835 repo.dirstate.copy(f1, f)
841 836 else:
842 837 repo.dirstate.copy(f2, f)
843 838 else:
844 839 # We've update-merged a locally modified file, so
845 840 # we set the dirstate to emulate a normal checkout
846 841 # of that file some time in the past. Thus our
847 842 # merge will appear as a normal local file
848 843 # modification.
849 844 if f2 == f: # file not locally copied/moved
850 845 repo.dirstate.update_file(
851 846 f, p1_tracked=True, wc_tracked=True, possibly_dirty=True
852 847 )
853 848 if move:
854 849 repo.dirstate.update_file(
855 850 f1, p1_tracked=False, wc_tracked=False
856 851 )
857 852
858 853 # directory rename, move local
859 854 for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []):
860 855 f0, flag = args
861 856 if branchmerge:
862 857 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True)
863 858 repo.dirstate.update_file(f0, p1_tracked=True, wc_tracked=False)
864 859 repo.dirstate.copy(f0, f)
865 860 else:
866 861 repo.dirstate.update_file(f, p1_tracked=True, wc_tracked=True)
867 862 repo.dirstate.update_file(f0, p1_tracked=False, wc_tracked=False)
868 863
869 864 # directory rename, get
870 865 for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []):
871 866 f0, flag = args
872 867 if branchmerge:
873 868 repo.dirstate.update_file(f, p1_tracked=False, wc_tracked=True)
874 869 repo.dirstate.copy(f0, f)
875 870 else:
876 871 repo.dirstate.update_file(f, p1_tracked=True, wc_tracked=True)
General Comments 0
You need to be logged in to leave comments. Login now