##// END OF EJS Templates
dirstate: expose a sparse matcher on dirstate (API)...
Gregory Szorc -
r33373:fb320398 default
parent child Browse files
Show More
@@ -1,670 +1,672 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 copy
13 13 import hashlib
14 14 import os
15 15 import platform
16 16 import stat
17 17
18 18 from mercurial.i18n import _
19 19
20 20 from mercurial import (
21 21 dirstate,
22 22 encoding,
23 23 error,
24 24 httpconnection,
25 25 match as matchmod,
26 26 node,
27 27 pycompat,
28 28 scmutil,
29 sparse,
29 30 util,
30 31 vfs as vfsmod,
31 32 )
32 33
33 34 shortname = '.hglf'
34 35 shortnameslash = shortname + '/'
35 36 longname = 'largefiles'
36 37
37 38 # -- Private worker functions ------------------------------------------
38 39
39 40 def getminsize(ui, assumelfiles, opt, default=10):
40 41 lfsize = opt
41 42 if not lfsize and assumelfiles:
42 43 lfsize = ui.config(longname, 'minsize', default=default)
43 44 if lfsize:
44 45 try:
45 46 lfsize = float(lfsize)
46 47 except ValueError:
47 48 raise error.Abort(_('largefiles: size must be number (not %s)\n')
48 49 % lfsize)
49 50 if lfsize is None:
50 51 raise error.Abort(_('minimum size for largefiles must be specified'))
51 52 return lfsize
52 53
53 54 def link(src, dest):
54 55 """Try to create hardlink - if that fails, efficiently make a copy."""
55 56 util.makedirs(os.path.dirname(dest))
56 57 try:
57 58 util.oslink(src, dest)
58 59 except OSError:
59 60 # if hardlinks fail, fallback on atomic copy
60 61 with open(src, 'rb') as srcf:
61 62 with util.atomictempfile(dest) as dstf:
62 63 for chunk in util.filechunkiter(srcf):
63 64 dstf.write(chunk)
64 65 os.chmod(dest, os.stat(src).st_mode)
65 66
66 67 def usercachepath(ui, hash):
67 68 '''Return the correct location in the "global" largefiles cache for a file
68 69 with the given hash.
69 70 This cache is used for sharing of largefiles across repositories - both
70 71 to preserve download bandwidth and storage space.'''
71 72 return os.path.join(_usercachedir(ui), hash)
72 73
73 74 def _usercachedir(ui):
74 75 '''Return the location of the "global" largefiles cache.'''
75 76 path = ui.configpath(longname, 'usercache', None)
76 77 if path:
77 78 return path
78 79 if pycompat.osname == 'nt':
79 80 appdata = encoding.environ.get('LOCALAPPDATA',\
80 81 encoding.environ.get('APPDATA'))
81 82 if appdata:
82 83 return os.path.join(appdata, longname)
83 84 elif platform.system() == 'Darwin':
84 85 home = encoding.environ.get('HOME')
85 86 if home:
86 87 return os.path.join(home, 'Library', 'Caches', longname)
87 88 elif pycompat.osname == 'posix':
88 89 path = encoding.environ.get('XDG_CACHE_HOME')
89 90 if path:
90 91 return os.path.join(path, longname)
91 92 home = encoding.environ.get('HOME')
92 93 if home:
93 94 return os.path.join(home, '.cache', longname)
94 95 else:
95 96 raise error.Abort(_('unknown operating system: %s\n')
96 97 % pycompat.osname)
97 98 raise error.Abort(_('unknown %s usercache location') % longname)
98 99
99 100 def inusercache(ui, hash):
100 101 path = usercachepath(ui, hash)
101 102 return os.path.exists(path)
102 103
103 104 def findfile(repo, hash):
104 105 '''Return store path of the largefile with the specified hash.
105 106 As a side effect, the file might be linked from user cache.
106 107 Return None if the file can't be found locally.'''
107 108 path, exists = findstorepath(repo, hash)
108 109 if exists:
109 110 repo.ui.note(_('found %s in store\n') % hash)
110 111 return path
111 112 elif inusercache(repo.ui, hash):
112 113 repo.ui.note(_('found %s in system cache\n') % hash)
113 114 path = storepath(repo, hash)
114 115 link(usercachepath(repo.ui, hash), path)
115 116 return path
116 117 return None
117 118
118 119 class largefilesdirstate(dirstate.dirstate):
119 120 def __getitem__(self, key):
120 121 return super(largefilesdirstate, self).__getitem__(unixpath(key))
121 122 def normal(self, f):
122 123 return super(largefilesdirstate, self).normal(unixpath(f))
123 124 def remove(self, f):
124 125 return super(largefilesdirstate, self).remove(unixpath(f))
125 126 def add(self, f):
126 127 return super(largefilesdirstate, self).add(unixpath(f))
127 128 def drop(self, f):
128 129 return super(largefilesdirstate, self).drop(unixpath(f))
129 130 def forget(self, f):
130 131 return super(largefilesdirstate, self).forget(unixpath(f))
131 132 def normallookup(self, f):
132 133 return super(largefilesdirstate, self).normallookup(unixpath(f))
133 134 def _ignore(self, f):
134 135 return False
135 136 def write(self, tr=False):
136 137 # (1) disable PENDING mode always
137 138 # (lfdirstate isn't yet managed as a part of the transaction)
138 139 # (2) avoid develwarn 'use dirstate.write with ....'
139 140 super(largefilesdirstate, self).write(None)
140 141
141 142 def openlfdirstate(ui, repo, create=True):
142 143 '''
143 144 Return a dirstate object that tracks largefiles: i.e. its root is
144 145 the repo root, but it is saved in .hg/largefiles/dirstate.
145 146 '''
146 147 vfs = repo.vfs
147 148 lfstoredir = longname
148 149 opener = vfsmod.vfs(vfs.join(lfstoredir))
149 150 lfdirstate = largefilesdirstate(opener, ui, repo.root,
150 repo.dirstate._validate)
151 repo.dirstate._validate,
152 lambda: sparse.matcher(repo))
151 153
152 154 # If the largefiles dirstate does not exist, populate and create
153 155 # it. This ensures that we create it on the first meaningful
154 156 # largefiles operation in a new clone.
155 157 if create and not vfs.exists(vfs.join(lfstoredir, 'dirstate')):
156 158 matcher = getstandinmatcher(repo)
157 159 standins = repo.dirstate.walk(matcher, [], False, False)
158 160
159 161 if len(standins) > 0:
160 162 vfs.makedirs(lfstoredir)
161 163
162 164 for standin in standins:
163 165 lfile = splitstandin(standin)
164 166 lfdirstate.normallookup(lfile)
165 167 return lfdirstate
166 168
167 169 def lfdirstatestatus(lfdirstate, repo):
168 170 pctx = repo['.']
169 171 match = matchmod.always(repo.root, repo.getcwd())
170 172 unsure, s = lfdirstate.status(match, [], False, False, False)
171 173 modified, clean = s.modified, s.clean
172 174 for lfile in unsure:
173 175 try:
174 176 fctx = pctx[standin(lfile)]
175 177 except LookupError:
176 178 fctx = None
177 179 if not fctx or readasstandin(fctx) != hashfile(repo.wjoin(lfile)):
178 180 modified.append(lfile)
179 181 else:
180 182 clean.append(lfile)
181 183 lfdirstate.normal(lfile)
182 184 return s
183 185
184 186 def listlfiles(repo, rev=None, matcher=None):
185 187 '''return a list of largefiles in the working copy or the
186 188 specified changeset'''
187 189
188 190 if matcher is None:
189 191 matcher = getstandinmatcher(repo)
190 192
191 193 # ignore unknown files in working directory
192 194 return [splitstandin(f)
193 195 for f in repo[rev].walk(matcher)
194 196 if rev is not None or repo.dirstate[f] != '?']
195 197
196 198 def instore(repo, hash, forcelocal=False):
197 199 '''Return true if a largefile with the given hash exists in the store'''
198 200 return os.path.exists(storepath(repo, hash, forcelocal))
199 201
200 202 def storepath(repo, hash, forcelocal=False):
201 203 '''Return the correct location in the repository largefiles store for a
202 204 file with the given hash.'''
203 205 if not forcelocal and repo.shared():
204 206 return repo.vfs.reljoin(repo.sharedpath, longname, hash)
205 207 return repo.vfs.join(longname, hash)
206 208
207 209 def findstorepath(repo, hash):
208 210 '''Search through the local store path(s) to find the file for the given
209 211 hash. If the file is not found, its path in the primary store is returned.
210 212 The return value is a tuple of (path, exists(path)).
211 213 '''
212 214 # For shared repos, the primary store is in the share source. But for
213 215 # backward compatibility, force a lookup in the local store if it wasn't
214 216 # found in the share source.
215 217 path = storepath(repo, hash, False)
216 218
217 219 if instore(repo, hash):
218 220 return (path, True)
219 221 elif repo.shared() and instore(repo, hash, True):
220 222 return storepath(repo, hash, True), True
221 223
222 224 return (path, False)
223 225
224 226 def copyfromcache(repo, hash, filename):
225 227 '''Copy the specified largefile from the repo or system cache to
226 228 filename in the repository. Return true on success or false if the
227 229 file was not found in either cache (which should not happened:
228 230 this is meant to be called only after ensuring that the needed
229 231 largefile exists in the cache).'''
230 232 wvfs = repo.wvfs
231 233 path = findfile(repo, hash)
232 234 if path is None:
233 235 return False
234 236 wvfs.makedirs(wvfs.dirname(wvfs.join(filename)))
235 237 # The write may fail before the file is fully written, but we
236 238 # don't use atomic writes in the working copy.
237 239 with open(path, 'rb') as srcfd:
238 240 with wvfs(filename, 'wb') as destfd:
239 241 gothash = copyandhash(
240 242 util.filechunkiter(srcfd), destfd)
241 243 if gothash != hash:
242 244 repo.ui.warn(_('%s: data corruption in %s with hash %s\n')
243 245 % (filename, path, gothash))
244 246 wvfs.unlink(filename)
245 247 return False
246 248 return True
247 249
248 250 def copytostore(repo, ctx, file, fstandin):
249 251 wvfs = repo.wvfs
250 252 hash = readasstandin(ctx[fstandin])
251 253 if instore(repo, hash):
252 254 return
253 255 if wvfs.exists(file):
254 256 copytostoreabsolute(repo, wvfs.join(file), hash)
255 257 else:
256 258 repo.ui.warn(_("%s: largefile %s not available from local store\n") %
257 259 (file, hash))
258 260
259 261 def copyalltostore(repo, node):
260 262 '''Copy all largefiles in a given revision to the store'''
261 263
262 264 ctx = repo[node]
263 265 for filename in ctx.files():
264 266 realfile = splitstandin(filename)
265 267 if realfile is not None and filename in ctx.manifest():
266 268 copytostore(repo, ctx, realfile, filename)
267 269
268 270 def copytostoreabsolute(repo, file, hash):
269 271 if inusercache(repo.ui, hash):
270 272 link(usercachepath(repo.ui, hash), storepath(repo, hash))
271 273 else:
272 274 util.makedirs(os.path.dirname(storepath(repo, hash)))
273 275 with open(file, 'rb') as srcf:
274 276 with util.atomictempfile(storepath(repo, hash),
275 277 createmode=repo.store.createmode) as dstf:
276 278 for chunk in util.filechunkiter(srcf):
277 279 dstf.write(chunk)
278 280 linktousercache(repo, hash)
279 281
280 282 def linktousercache(repo, hash):
281 283 '''Link / copy the largefile with the specified hash from the store
282 284 to the cache.'''
283 285 path = usercachepath(repo.ui, hash)
284 286 link(storepath(repo, hash), path)
285 287
286 288 def getstandinmatcher(repo, rmatcher=None):
287 289 '''Return a match object that applies rmatcher to the standin directory'''
288 290 wvfs = repo.wvfs
289 291 standindir = shortname
290 292
291 293 # no warnings about missing files or directories
292 294 badfn = lambda f, msg: None
293 295
294 296 if rmatcher and not rmatcher.always():
295 297 pats = [wvfs.join(standindir, pat) for pat in rmatcher.files()]
296 298 if not pats:
297 299 pats = [wvfs.join(standindir)]
298 300 match = scmutil.match(repo[None], pats, badfn=badfn)
299 301 else:
300 302 # no patterns: relative to repo root
301 303 match = scmutil.match(repo[None], [wvfs.join(standindir)], badfn=badfn)
302 304 return match
303 305
304 306 def composestandinmatcher(repo, rmatcher):
305 307 '''Return a matcher that accepts standins corresponding to the
306 308 files accepted by rmatcher. Pass the list of files in the matcher
307 309 as the paths specified by the user.'''
308 310 smatcher = getstandinmatcher(repo, rmatcher)
309 311 isstandin = smatcher.matchfn
310 312 def composedmatchfn(f):
311 313 return isstandin(f) and rmatcher.matchfn(splitstandin(f))
312 314 smatcher.matchfn = composedmatchfn
313 315
314 316 return smatcher
315 317
316 318 def standin(filename):
317 319 '''Return the repo-relative path to the standin for the specified big
318 320 file.'''
319 321 # Notes:
320 322 # 1) Some callers want an absolute path, but for instance addlargefiles
321 323 # needs it repo-relative so it can be passed to repo[None].add(). So
322 324 # leave it up to the caller to use repo.wjoin() to get an absolute path.
323 325 # 2) Join with '/' because that's what dirstate always uses, even on
324 326 # Windows. Change existing separator to '/' first in case we are
325 327 # passed filenames from an external source (like the command line).
326 328 return shortnameslash + util.pconvert(filename)
327 329
328 330 def isstandin(filename):
329 331 '''Return true if filename is a big file standin. filename must be
330 332 in Mercurial's internal form (slash-separated).'''
331 333 return filename.startswith(shortnameslash)
332 334
333 335 def splitstandin(filename):
334 336 # Split on / because that's what dirstate always uses, even on Windows.
335 337 # Change local separator to / first just in case we are passed filenames
336 338 # from an external source (like the command line).
337 339 bits = util.pconvert(filename).split('/', 1)
338 340 if len(bits) == 2 and bits[0] == shortname:
339 341 return bits[1]
340 342 else:
341 343 return None
342 344
343 345 def updatestandin(repo, lfile, standin):
344 346 """Re-calculate hash value of lfile and write it into standin
345 347
346 348 This assumes that "lfutil.standin(lfile) == standin", for efficiency.
347 349 """
348 350 file = repo.wjoin(lfile)
349 351 if repo.wvfs.exists(lfile):
350 352 hash = hashfile(file)
351 353 executable = getexecutable(file)
352 354 writestandin(repo, standin, hash, executable)
353 355 else:
354 356 raise error.Abort(_('%s: file not found!') % lfile)
355 357
356 358 def readasstandin(fctx):
357 359 '''read hex hash from given filectx of standin file
358 360
359 361 This encapsulates how "standin" data is stored into storage layer.'''
360 362 return fctx.data().strip()
361 363
362 364 def writestandin(repo, standin, hash, executable):
363 365 '''write hash to <repo.root>/<standin>'''
364 366 repo.wwrite(standin, hash + '\n', executable and 'x' or '')
365 367
366 368 def copyandhash(instream, outfile):
367 369 '''Read bytes from instream (iterable) and write them to outfile,
368 370 computing the SHA-1 hash of the data along the way. Return the hash.'''
369 371 hasher = hashlib.sha1('')
370 372 for data in instream:
371 373 hasher.update(data)
372 374 outfile.write(data)
373 375 return hasher.hexdigest()
374 376
375 377 def hashfile(file):
376 378 if not os.path.exists(file):
377 379 return ''
378 380 with open(file, 'rb') as fd:
379 381 return hexsha1(fd)
380 382
381 383 def getexecutable(filename):
382 384 mode = os.stat(filename).st_mode
383 385 return ((mode & stat.S_IXUSR) and
384 386 (mode & stat.S_IXGRP) and
385 387 (mode & stat.S_IXOTH))
386 388
387 389 def urljoin(first, second, *arg):
388 390 def join(left, right):
389 391 if not left.endswith('/'):
390 392 left += '/'
391 393 if right.startswith('/'):
392 394 right = right[1:]
393 395 return left + right
394 396
395 397 url = join(first, second)
396 398 for a in arg:
397 399 url = join(url, a)
398 400 return url
399 401
400 402 def hexsha1(fileobj):
401 403 """hexsha1 returns the hex-encoded sha1 sum of the data in the file-like
402 404 object data"""
403 405 h = hashlib.sha1()
404 406 for chunk in util.filechunkiter(fileobj):
405 407 h.update(chunk)
406 408 return h.hexdigest()
407 409
408 410 def httpsendfile(ui, filename):
409 411 return httpconnection.httpsendfile(ui, filename, 'rb')
410 412
411 413 def unixpath(path):
412 414 '''Return a version of path normalized for use with the lfdirstate.'''
413 415 return util.pconvert(os.path.normpath(path))
414 416
415 417 def islfilesrepo(repo):
416 418 '''Return true if the repo is a largefile repo.'''
417 419 if ('largefiles' in repo.requirements and
418 420 any(shortnameslash in f[0] for f in repo.store.datafiles())):
419 421 return True
420 422
421 423 return any(openlfdirstate(repo.ui, repo, False))
422 424
423 425 class storeprotonotcapable(Exception):
424 426 def __init__(self, storetypes):
425 427 self.storetypes = storetypes
426 428
427 429 def getstandinsstate(repo):
428 430 standins = []
429 431 matcher = getstandinmatcher(repo)
430 432 wctx = repo[None]
431 433 for standin in repo.dirstate.walk(matcher, [], False, False):
432 434 lfile = splitstandin(standin)
433 435 try:
434 436 hash = readasstandin(wctx[standin])
435 437 except IOError:
436 438 hash = None
437 439 standins.append((lfile, hash))
438 440 return standins
439 441
440 442 def synclfdirstate(repo, lfdirstate, lfile, normallookup):
441 443 lfstandin = standin(lfile)
442 444 if lfstandin in repo.dirstate:
443 445 stat = repo.dirstate._map[lfstandin]
444 446 state, mtime = stat[0], stat[3]
445 447 else:
446 448 state, mtime = '?', -1
447 449 if state == 'n':
448 450 if (normallookup or mtime < 0 or
449 451 not repo.wvfs.exists(lfile)):
450 452 # state 'n' doesn't ensure 'clean' in this case
451 453 lfdirstate.normallookup(lfile)
452 454 else:
453 455 lfdirstate.normal(lfile)
454 456 elif state == 'm':
455 457 lfdirstate.normallookup(lfile)
456 458 elif state == 'r':
457 459 lfdirstate.remove(lfile)
458 460 elif state == 'a':
459 461 lfdirstate.add(lfile)
460 462 elif state == '?':
461 463 lfdirstate.drop(lfile)
462 464
463 465 def markcommitted(orig, ctx, node):
464 466 repo = ctx.repo()
465 467
466 468 orig(node)
467 469
468 470 # ATTENTION: "ctx.files()" may differ from "repo[node].files()"
469 471 # because files coming from the 2nd parent are omitted in the latter.
470 472 #
471 473 # The former should be used to get targets of "synclfdirstate",
472 474 # because such files:
473 475 # - are marked as "a" by "patch.patch()" (e.g. via transplant), and
474 476 # - have to be marked as "n" after commit, but
475 477 # - aren't listed in "repo[node].files()"
476 478
477 479 lfdirstate = openlfdirstate(repo.ui, repo)
478 480 for f in ctx.files():
479 481 lfile = splitstandin(f)
480 482 if lfile is not None:
481 483 synclfdirstate(repo, lfdirstate, lfile, False)
482 484 lfdirstate.write()
483 485
484 486 # As part of committing, copy all of the largefiles into the cache.
485 487 #
486 488 # Using "node" instead of "ctx" implies additional "repo[node]"
487 489 # lookup while copyalltostore(), but can omit redundant check for
488 490 # files comming from the 2nd parent, which should exist in store
489 491 # at merging.
490 492 copyalltostore(repo, node)
491 493
492 494 def getlfilestoupdate(oldstandins, newstandins):
493 495 changedstandins = set(oldstandins).symmetric_difference(set(newstandins))
494 496 filelist = []
495 497 for f in changedstandins:
496 498 if f[0] not in filelist:
497 499 filelist.append(f[0])
498 500 return filelist
499 501
500 502 def getlfilestoupload(repo, missing, addfunc):
501 503 for i, n in enumerate(missing):
502 504 repo.ui.progress(_('finding outgoing largefiles'), i,
503 505 unit=_('revisions'), total=len(missing))
504 506 parents = [p for p in repo[n].parents() if p != node.nullid]
505 507
506 508 oldlfstatus = repo.lfstatus
507 509 repo.lfstatus = False
508 510 try:
509 511 ctx = repo[n]
510 512 finally:
511 513 repo.lfstatus = oldlfstatus
512 514
513 515 files = set(ctx.files())
514 516 if len(parents) == 2:
515 517 mc = ctx.manifest()
516 518 mp1 = ctx.parents()[0].manifest()
517 519 mp2 = ctx.parents()[1].manifest()
518 520 for f in mp1:
519 521 if f not in mc:
520 522 files.add(f)
521 523 for f in mp2:
522 524 if f not in mc:
523 525 files.add(f)
524 526 for f in mc:
525 527 if mc[f] != mp1.get(f, None) or mc[f] != mp2.get(f, None):
526 528 files.add(f)
527 529 for fn in files:
528 530 if isstandin(fn) and fn in ctx:
529 531 addfunc(fn, readasstandin(ctx[fn]))
530 532 repo.ui.progress(_('finding outgoing largefiles'), None)
531 533
532 534 def updatestandinsbymatch(repo, match):
533 535 '''Update standins in the working directory according to specified match
534 536
535 537 This returns (possibly modified) ``match`` object to be used for
536 538 subsequent commit process.
537 539 '''
538 540
539 541 ui = repo.ui
540 542
541 543 # Case 1: user calls commit with no specific files or
542 544 # include/exclude patterns: refresh and commit all files that
543 545 # are "dirty".
544 546 if match is None or match.always():
545 547 # Spend a bit of time here to get a list of files we know
546 548 # are modified so we can compare only against those.
547 549 # It can cost a lot of time (several seconds)
548 550 # otherwise to update all standins if the largefiles are
549 551 # large.
550 552 lfdirstate = openlfdirstate(ui, repo)
551 553 dirtymatch = matchmod.always(repo.root, repo.getcwd())
552 554 unsure, s = lfdirstate.status(dirtymatch, [], False, False,
553 555 False)
554 556 modifiedfiles = unsure + s.modified + s.added + s.removed
555 557 lfiles = listlfiles(repo)
556 558 # this only loops through largefiles that exist (not
557 559 # removed/renamed)
558 560 for lfile in lfiles:
559 561 if lfile in modifiedfiles:
560 562 fstandin = standin(lfile)
561 563 if repo.wvfs.exists(fstandin):
562 564 # this handles the case where a rebase is being
563 565 # performed and the working copy is not updated
564 566 # yet.
565 567 if repo.wvfs.exists(lfile):
566 568 updatestandin(repo, lfile, fstandin)
567 569
568 570 return match
569 571
570 572 lfiles = listlfiles(repo)
571 573 match._files = repo._subdirlfs(match.files(), lfiles)
572 574
573 575 # Case 2: user calls commit with specified patterns: refresh
574 576 # any matching big files.
575 577 smatcher = composestandinmatcher(repo, match)
576 578 standins = repo.dirstate.walk(smatcher, [], False, False)
577 579
578 580 # No matching big files: get out of the way and pass control to
579 581 # the usual commit() method.
580 582 if not standins:
581 583 return match
582 584
583 585 # Refresh all matching big files. It's possible that the
584 586 # commit will end up failing, in which case the big files will
585 587 # stay refreshed. No harm done: the user modified them and
586 588 # asked to commit them, so sooner or later we're going to
587 589 # refresh the standins. Might as well leave them refreshed.
588 590 lfdirstate = openlfdirstate(ui, repo)
589 591 for fstandin in standins:
590 592 lfile = splitstandin(fstandin)
591 593 if lfdirstate[lfile] != 'r':
592 594 updatestandin(repo, lfile, fstandin)
593 595
594 596 # Cook up a new matcher that only matches regular files or
595 597 # standins corresponding to the big files requested by the
596 598 # user. Have to modify _files to prevent commit() from
597 599 # complaining "not tracked" for big files.
598 600 match = copy.copy(match)
599 601 origmatchfn = match.matchfn
600 602
601 603 # Check both the list of largefiles and the list of
602 604 # standins because if a largefile was removed, it
603 605 # won't be in the list of largefiles at this point
604 606 match._files += sorted(standins)
605 607
606 608 actualfiles = []
607 609 for f in match._files:
608 610 fstandin = standin(f)
609 611
610 612 # For largefiles, only one of the normal and standin should be
611 613 # committed (except if one of them is a remove). In the case of a
612 614 # standin removal, drop the normal file if it is unknown to dirstate.
613 615 # Thus, skip plain largefile names but keep the standin.
614 616 if f in lfiles or fstandin in standins:
615 617 if repo.dirstate[fstandin] != 'r':
616 618 if repo.dirstate[f] != 'r':
617 619 continue
618 620 elif repo.dirstate[f] == '?':
619 621 continue
620 622
621 623 actualfiles.append(f)
622 624 match._files = actualfiles
623 625
624 626 def matchfn(f):
625 627 if origmatchfn(f):
626 628 return f not in lfiles
627 629 else:
628 630 return f in standins
629 631
630 632 match.matchfn = matchfn
631 633
632 634 return match
633 635
634 636 class automatedcommithook(object):
635 637 '''Stateful hook to update standins at the 1st commit of resuming
636 638
637 639 For efficiency, updating standins in the working directory should
638 640 be avoided while automated committing (like rebase, transplant and
639 641 so on), because they should be updated before committing.
640 642
641 643 But the 1st commit of resuming automated committing (e.g. ``rebase
642 644 --continue``) should update them, because largefiles may be
643 645 modified manually.
644 646 '''
645 647 def __init__(self, resuming):
646 648 self.resuming = resuming
647 649
648 650 def __call__(self, repo, match):
649 651 if self.resuming:
650 652 self.resuming = False # avoids updating at subsequent commits
651 653 return updatestandinsbymatch(repo, match)
652 654 else:
653 655 return match
654 656
655 657 def getstatuswriter(ui, repo, forcibly=None):
656 658 '''Return the function to write largefiles specific status out
657 659
658 660 If ``forcibly`` is ``None``, this returns the last element of
659 661 ``repo._lfstatuswriters`` as "default" writer function.
660 662
661 663 Otherwise, this returns the function to always write out (or
662 664 ignore if ``not forcibly``) status.
663 665 '''
664 666 if forcibly is None and util.safehasattr(repo, '_largefilesenabled'):
665 667 return repo._lfstatuswriters[-1]
666 668 else:
667 669 if forcibly:
668 670 return ui.status # forcibly WRITE OUT
669 671 else:
670 672 return lambda *msg, **opts: None # forcibly IGNORE
@@ -1,445 +1,428 b''
1 1 # sparse.py - allow sparse checkouts of the working directory
2 2 #
3 3 # Copyright 2014 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """allow sparse checkouts of the working directory (EXPERIMENTAL)
9 9
10 10 (This extension is not yet protected by backwards compatibility
11 11 guarantees. Any aspect may break in future releases until this
12 12 notice is removed.)
13 13
14 14 This extension allows the working directory to only consist of a
15 15 subset of files for the revision. This allows specific files or
16 16 directories to be explicitly included or excluded. Many repository
17 17 operations have performance proportional to the number of files in
18 18 the working directory. So only realizing a subset of files in the
19 19 working directory can improve performance.
20 20
21 21 Sparse Config Files
22 22 -------------------
23 23
24 24 The set of files that are part of a sparse checkout are defined by
25 25 a sparse config file. The file defines 3 things: includes (files to
26 26 include in the sparse checkout), excludes (files to exclude from the
27 27 sparse checkout), and profiles (links to other config files).
28 28
29 29 The file format is newline delimited. Empty lines and lines beginning
30 30 with ``#`` are ignored.
31 31
32 32 Lines beginning with ``%include `` denote another sparse config file
33 33 to include. e.g. ``%include tests.sparse``. The filename is relative
34 34 to the repository root.
35 35
36 36 The special lines ``[include]`` and ``[exclude]`` denote the section
37 37 for includes and excludes that follow, respectively. It is illegal to
38 38 have ``[include]`` after ``[exclude]``. If no sections are defined,
39 39 entries are assumed to be in the ``[include]`` section.
40 40
41 41 Non-special lines resemble file patterns to be added to either includes
42 42 or excludes. The syntax of these lines is documented by :hg:`help patterns`.
43 43 Patterns are interpreted as ``glob:`` by default and match against the
44 44 root of the repository.
45 45
46 46 Exclusion patterns take precedence over inclusion patterns. So even
47 47 if a file is explicitly included, an ``[exclude]`` entry can remove it.
48 48
49 49 For example, say you have a repository with 3 directories, ``frontend/``,
50 50 ``backend/``, and ``tools/``. ``frontend/`` and ``backend/`` correspond
51 51 to different projects and it is uncommon for someone working on one
52 52 to need the files for the other. But ``tools/`` contains files shared
53 53 between both projects. Your sparse config files may resemble::
54 54
55 55 # frontend.sparse
56 56 frontend/**
57 57 tools/**
58 58
59 59 # backend.sparse
60 60 backend/**
61 61 tools/**
62 62
63 63 Say the backend grows in size. Or there's a directory with thousands
64 64 of files you wish to exclude. You can modify the profile to exclude
65 65 certain files::
66 66
67 67 [include]
68 68 backend/**
69 69 tools/**
70 70
71 71 [exclude]
72 72 tools/tests/**
73 73 """
74 74
75 75 from __future__ import absolute_import
76 76
77 77 from mercurial.i18n import _
78 78 from mercurial import (
79 79 cmdutil,
80 80 commands,
81 81 dirstate,
82 82 error,
83 83 extensions,
84 84 hg,
85 localrepo,
86 85 match as matchmod,
87 86 registrar,
88 87 sparse,
89 88 util,
90 89 )
91 90
92 91 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
93 92 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
94 93 # be specifying the version(s) of Mercurial they are tested with, or
95 94 # leave the attribute unspecified.
96 95 testedwith = 'ships-with-hg-core'
97 96
98 97 cmdtable = {}
99 98 command = registrar.command(cmdtable)
100 99
101 100 def extsetup(ui):
102 101 sparse.enabled = True
103 102
104 103 _setupclone(ui)
105 104 _setuplog(ui)
106 105 _setupadd(ui)
107 106 _setupdirstate(ui)
108 107
109 def reposetup(ui, repo):
110 if not util.safehasattr(repo, 'dirstate'):
111 return
112
113 if 'dirstate' in repo._filecache:
114 repo.dirstate.repo = repo
115
116 108 def replacefilecache(cls, propname, replacement):
117 109 """Replace a filecache property with a new class. This allows changing the
118 110 cache invalidation condition."""
119 111 origcls = cls
120 112 assert callable(replacement)
121 113 while cls is not object:
122 114 if propname in cls.__dict__:
123 115 orig = cls.__dict__[propname]
124 116 setattr(cls, propname, replacement(orig))
125 117 break
126 118 cls = cls.__bases__[0]
127 119
128 120 if cls is object:
129 121 raise AttributeError(_("type '%s' has no property '%s'") % (origcls,
130 122 propname))
131 123
132 124 def _setuplog(ui):
133 125 entry = commands.table['^log|history']
134 126 entry[1].append(('', 'sparse', None,
135 127 "limit to changesets affecting the sparse checkout"))
136 128
137 129 def _logrevs(orig, repo, opts):
138 130 revs = orig(repo, opts)
139 131 if opts.get('sparse'):
140 132 sparsematch = sparse.matcher(repo)
141 133 def ctxmatch(rev):
142 134 ctx = repo[rev]
143 135 return any(f for f in ctx.files() if sparsematch(f))
144 136 revs = revs.filter(ctxmatch)
145 137 return revs
146 138 extensions.wrapfunction(cmdutil, '_logrevs', _logrevs)
147 139
148 140 def _clonesparsecmd(orig, ui, repo, *args, **opts):
149 141 include_pat = opts.get('include')
150 142 exclude_pat = opts.get('exclude')
151 143 enableprofile_pat = opts.get('enable_profile')
152 144 include = exclude = enableprofile = False
153 145 if include_pat:
154 146 pat = include_pat
155 147 include = True
156 148 if exclude_pat:
157 149 pat = exclude_pat
158 150 exclude = True
159 151 if enableprofile_pat:
160 152 pat = enableprofile_pat
161 153 enableprofile = True
162 154 if sum([include, exclude, enableprofile]) > 1:
163 155 raise error.Abort(_("too many flags specified."))
164 156 if include or exclude or enableprofile:
165 157 def clonesparse(orig, self, node, overwrite, *args, **kwargs):
166 158 _config(self.ui, self.unfiltered(), pat, {}, include=include,
167 159 exclude=exclude, enableprofile=enableprofile)
168 160 return orig(self, node, overwrite, *args, **kwargs)
169 161 extensions.wrapfunction(hg, 'updaterepo', clonesparse)
170 162 return orig(ui, repo, *args, **opts)
171 163
172 164 def _setupclone(ui):
173 165 entry = commands.table['^clone']
174 166 entry[1].append(('', 'enable-profile', [],
175 167 'enable a sparse profile'))
176 168 entry[1].append(('', 'include', [],
177 169 'include sparse pattern'))
178 170 entry[1].append(('', 'exclude', [],
179 171 'exclude sparse pattern'))
180 172 extensions.wrapcommand(commands.table, 'clone', _clonesparsecmd)
181 173
182 174 def _setupadd(ui):
183 175 entry = commands.table['^add']
184 176 entry[1].append(('s', 'sparse', None,
185 177 'also include directories of added files in sparse config'))
186 178
187 179 def _add(orig, ui, repo, *pats, **opts):
188 180 if opts.get('sparse'):
189 181 dirs = set()
190 182 for pat in pats:
191 183 dirname, basename = util.split(pat)
192 184 dirs.add(dirname)
193 185 _config(ui, repo, list(dirs), opts, include=True)
194 186 return orig(ui, repo, *pats, **opts)
195 187
196 188 extensions.wrapcommand(commands.table, 'add', _add)
197 189
198 190 def _setupdirstate(ui):
199 191 """Modify the dirstate to prevent stat'ing excluded files,
200 192 and to prevent modifications to files outside the checkout.
201 193 """
202 194
203 def _dirstate(orig, repo):
204 dirstate = orig(repo)
205 dirstate.repo = repo
206 return dirstate
207 extensions.wrapfunction(
208 localrepo.localrepository.dirstate, 'func', _dirstate)
209
210 195 # The atrocity below is needed to wrap dirstate._ignore. It is a cached
211 196 # property, which means normal function wrapping doesn't work.
212 197 class ignorewrapper(object):
213 198 def __init__(self, orig):
214 199 self.orig = orig
215 200 self.origignore = None
216 201 self.func = None
217 202 self.sparsematch = None
218 203
219 204 def __get__(self, obj, type=None):
220 repo = obj.repo
221 205 origignore = self.orig.__get__(obj)
222 206
223 sparsematch = sparse.matcher(repo)
207 sparsematch = obj._sparsematcher
224 208 if sparsematch.always():
225 209 return origignore
226 210
227 211 if self.sparsematch != sparsematch or self.origignore != origignore:
228 212 self.func = matchmod.unionmatcher([
229 213 origignore, matchmod.negatematcher(sparsematch)])
230 214 self.sparsematch = sparsematch
231 215 self.origignore = origignore
232 216 return self.func
233 217
234 218 def __set__(self, obj, value):
235 219 return self.orig.__set__(obj, value)
236 220
237 221 def __delete__(self, obj):
238 222 return self.orig.__delete__(obj)
239 223
240 224 replacefilecache(dirstate.dirstate, '_ignore', ignorewrapper)
241 225
242 226 # dirstate.rebuild should not add non-matching files
243 227 def _rebuild(orig, self, parent, allfiles, changedfiles=None):
244 matcher = sparse.matcher(self.repo)
228 matcher = self._sparsematcher
245 229 if not matcher.always():
246 230 allfiles = allfiles.matches(matcher)
247 231 if changedfiles:
248 232 changedfiles = [f for f in changedfiles if matcher(f)]
249 233
250 234 if changedfiles is not None:
251 235 # In _rebuild, these files will be deleted from the dirstate
252 236 # when they are not found to be in allfiles
253 237 dirstatefilestoremove = set(f for f in self if not matcher(f))
254 238 changedfiles = dirstatefilestoremove.union(changedfiles)
255 239
256 240 return orig(self, parent, allfiles, changedfiles)
257 241 extensions.wrapfunction(dirstate.dirstate, 'rebuild', _rebuild)
258 242
259 243 # Prevent adding files that are outside the sparse checkout
260 244 editfuncs = ['normal', 'add', 'normallookup', 'copy', 'remove', 'merge']
261 245 hint = _('include file with `hg debugsparse --include <pattern>` or use ' +
262 246 '`hg add -s <file>` to include file directory while adding')
263 247 for func in editfuncs:
264 248 def _wrapper(orig, self, *args):
265 repo = self.repo
266 sparsematch = sparse.matcher(repo)
249 sparsematch = self._sparsematcher
267 250 if not sparsematch.always():
268 251 for f in args:
269 252 if (f is not None and not sparsematch(f) and
270 253 f not in self):
271 254 raise error.Abort(_("cannot add '%s' - it is outside "
272 255 "the sparse checkout") % f,
273 256 hint=hint)
274 257 return orig(self, *args)
275 258 extensions.wrapfunction(dirstate.dirstate, func, _wrapper)
276 259
277 260 @command('^debugsparse', [
278 261 ('I', 'include', False, _('include files in the sparse checkout')),
279 262 ('X', 'exclude', False, _('exclude files in the sparse checkout')),
280 263 ('d', 'delete', False, _('delete an include/exclude rule')),
281 264 ('f', 'force', False, _('allow changing rules even with pending changes')),
282 265 ('', 'enable-profile', False, _('enables the specified profile')),
283 266 ('', 'disable-profile', False, _('disables the specified profile')),
284 267 ('', 'import-rules', False, _('imports rules from a file')),
285 268 ('', 'clear-rules', False, _('clears local include/exclude rules')),
286 269 ('', 'refresh', False, _('updates the working after sparseness changes')),
287 270 ('', 'reset', False, _('makes the repo full again')),
288 271 ] + commands.templateopts,
289 272 _('[--OPTION] PATTERN...'))
290 273 def debugsparse(ui, repo, *pats, **opts):
291 274 """make the current checkout sparse, or edit the existing checkout
292 275
293 276 The sparse command is used to make the current checkout sparse.
294 277 This means files that don't meet the sparse condition will not be
295 278 written to disk, or show up in any working copy operations. It does
296 279 not affect files in history in any way.
297 280
298 281 Passing no arguments prints the currently applied sparse rules.
299 282
300 283 --include and --exclude are used to add and remove files from the sparse
301 284 checkout. The effects of adding an include or exclude rule are applied
302 285 immediately. If applying the new rule would cause a file with pending
303 286 changes to be added or removed, the command will fail. Pass --force to
304 287 force a rule change even with pending changes (the changes on disk will
305 288 be preserved).
306 289
307 290 --delete removes an existing include/exclude rule. The effects are
308 291 immediate.
309 292
310 293 --refresh refreshes the files on disk based on the sparse rules. This is
311 294 only necessary if .hg/sparse was changed by hand.
312 295
313 296 --enable-profile and --disable-profile accept a path to a .hgsparse file.
314 297 This allows defining sparse checkouts and tracking them inside the
315 298 repository. This is useful for defining commonly used sparse checkouts for
316 299 many people to use. As the profile definition changes over time, the sparse
317 300 checkout will automatically be updated appropriately, depending on which
318 301 changeset is checked out. Changes to .hgsparse are not applied until they
319 302 have been committed.
320 303
321 304 --import-rules accepts a path to a file containing rules in the .hgsparse
322 305 format, allowing you to add --include, --exclude and --enable-profile rules
323 306 in bulk. Like the --include, --exclude and --enable-profile switches, the
324 307 changes are applied immediately.
325 308
326 309 --clear-rules removes all local include and exclude rules, while leaving
327 310 any enabled profiles in place.
328 311
329 312 Returns 0 if editing the sparse checkout succeeds.
330 313 """
331 314 include = opts.get('include')
332 315 exclude = opts.get('exclude')
333 316 force = opts.get('force')
334 317 enableprofile = opts.get('enable_profile')
335 318 disableprofile = opts.get('disable_profile')
336 319 importrules = opts.get('import_rules')
337 320 clearrules = opts.get('clear_rules')
338 321 delete = opts.get('delete')
339 322 refresh = opts.get('refresh')
340 323 reset = opts.get('reset')
341 324 count = sum([include, exclude, enableprofile, disableprofile, delete,
342 325 importrules, refresh, clearrules, reset])
343 326 if count > 1:
344 327 raise error.Abort(_("too many flags specified"))
345 328
346 329 if count == 0:
347 330 if repo.vfs.exists('sparse'):
348 331 ui.status(repo.vfs.read("sparse") + "\n")
349 332 temporaryincludes = sparse.readtemporaryincludes(repo)
350 333 if temporaryincludes:
351 334 ui.status(_("Temporarily Included Files (for merge/rebase):\n"))
352 335 ui.status(("\n".join(temporaryincludes) + "\n"))
353 336 else:
354 337 ui.status(_('repo is not sparse\n'))
355 338 return
356 339
357 340 if include or exclude or delete or reset or enableprofile or disableprofile:
358 341 _config(ui, repo, pats, opts, include=include, exclude=exclude,
359 342 reset=reset, delete=delete, enableprofile=enableprofile,
360 343 disableprofile=disableprofile, force=force)
361 344
362 345 if importrules:
363 346 sparse.importfromfiles(repo, opts, pats, force=force)
364 347
365 348 if clearrules:
366 349 sparse.clearrules(repo, force=force)
367 350
368 351 if refresh:
369 352 try:
370 353 wlock = repo.wlock()
371 354 fcounts = map(
372 355 len,
373 356 sparse.refreshwdir(repo, repo.status(), sparse.matcher(repo),
374 357 force=force))
375 358 sparse.printchanges(ui, opts, added=fcounts[0], dropped=fcounts[1],
376 359 conflicting=fcounts[2])
377 360 finally:
378 361 wlock.release()
379 362
380 363 def _config(ui, repo, pats, opts, include=False, exclude=False, reset=False,
381 364 delete=False, enableprofile=False, disableprofile=False,
382 365 force=False):
383 366 """
384 367 Perform a sparse config update. Only one of the kwargs may be specified.
385 368 """
386 369 wlock = repo.wlock()
387 370 try:
388 371 oldsparsematch = sparse.matcher(repo)
389 372
390 373 raw = repo.vfs.tryread('sparse')
391 374 if raw:
392 375 oldinclude, oldexclude, oldprofiles = map(
393 376 set, sparse.parseconfig(ui, raw))
394 377 else:
395 378 oldinclude = set()
396 379 oldexclude = set()
397 380 oldprofiles = set()
398 381
399 382 try:
400 383 if reset:
401 384 newinclude = set()
402 385 newexclude = set()
403 386 newprofiles = set()
404 387 else:
405 388 newinclude = set(oldinclude)
406 389 newexclude = set(oldexclude)
407 390 newprofiles = set(oldprofiles)
408 391
409 392 oldstatus = repo.status()
410 393
411 394 if any(pat.startswith('/') for pat in pats):
412 395 ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
413 396 % ([pat for pat in pats if pat.startswith('/')]))
414 397 elif include:
415 398 newinclude.update(pats)
416 399 elif exclude:
417 400 newexclude.update(pats)
418 401 elif enableprofile:
419 402 newprofiles.update(pats)
420 403 elif disableprofile:
421 404 newprofiles.difference_update(pats)
422 405 elif delete:
423 406 newinclude.difference_update(pats)
424 407 newexclude.difference_update(pats)
425 408
426 409 sparse.writeconfig(repo, newinclude, newexclude, newprofiles)
427 410
428 411 fcounts = map(
429 412 len,
430 413 sparse.refreshwdir(repo, oldstatus, oldsparsematch,
431 414 force=force))
432 415
433 416 profilecount = (len(newprofiles - oldprofiles) -
434 417 len(oldprofiles - newprofiles))
435 418 includecount = (len(newinclude - oldinclude) -
436 419 len(oldinclude - newinclude))
437 420 excludecount = (len(newexclude - oldexclude) -
438 421 len(oldexclude - newexclude))
439 422 sparse.printchanges(ui, opts, profilecount, includecount,
440 423 excludecount, *fcounts)
441 424 except Exception:
442 425 sparse.writeconfig(repo, oldinclude, oldexclude, oldprofiles)
443 426 raise
444 427 finally:
445 428 wlock.release()
@@ -1,1336 +1,1350 b''
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from .node import nullid
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 match as matchmod,
22 22 pathutil,
23 23 policy,
24 24 pycompat,
25 25 scmutil,
26 26 txnutil,
27 27 util,
28 28 )
29 29
30 30 parsers = policy.importmod(r'parsers')
31 31
32 32 propertycache = util.propertycache
33 33 filecache = scmutil.filecache
34 34 _rangemask = 0x7fffffff
35 35
36 36 dirstatetuple = parsers.dirstatetuple
37 37
38 38 class repocache(filecache):
39 39 """filecache for files in .hg/"""
40 40 def join(self, obj, fname):
41 41 return obj._opener.join(fname)
42 42
43 43 class rootcache(filecache):
44 44 """filecache for files in the repository root"""
45 45 def join(self, obj, fname):
46 46 return obj._join(fname)
47 47
48 48 def _getfsnow(vfs):
49 49 '''Get "now" timestamp on filesystem'''
50 50 tmpfd, tmpname = vfs.mkstemp()
51 51 try:
52 52 return os.fstat(tmpfd).st_mtime
53 53 finally:
54 54 os.close(tmpfd)
55 55 vfs.unlink(tmpname)
56 56
57 57 def nonnormalentries(dmap):
58 58 '''Compute the nonnormal dirstate entries from the dmap'''
59 59 try:
60 60 return parsers.nonnormalotherparententries(dmap)
61 61 except AttributeError:
62 62 nonnorm = set()
63 63 otherparent = set()
64 64 for fname, e in dmap.iteritems():
65 65 if e[0] != 'n' or e[3] == -1:
66 66 nonnorm.add(fname)
67 67 if e[0] == 'n' and e[2] == -2:
68 68 otherparent.add(fname)
69 69 return nonnorm, otherparent
70 70
71 71 class dirstate(object):
72 72
73 def __init__(self, opener, ui, root, validate):
73 def __init__(self, opener, ui, root, validate, sparsematchfn):
74 74 '''Create a new dirstate object.
75 75
76 76 opener is an open()-like callable that can be used to open the
77 77 dirstate file; root is the root of the directory tracked by
78 78 the dirstate.
79 79 '''
80 80 self._opener = opener
81 81 self._validate = validate
82 82 self._root = root
83 self._sparsematchfn = sparsematchfn
83 84 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
84 85 # UNC path pointing to root share (issue4557)
85 86 self._rootdir = pathutil.normasprefix(root)
86 87 self._dirty = False
87 88 self._dirtypl = False
88 89 self._lastnormaltime = 0
89 90 self._ui = ui
90 91 self._filecache = {}
91 92 self._parentwriters = 0
92 93 self._filename = 'dirstate'
93 94 self._pendingfilename = '%s.pending' % self._filename
94 95 self._plchangecallbacks = {}
95 96 self._origpl = None
96 97 self._updatedfiles = set()
97 98
98 99 # for consistent view between _pl() and _read() invocations
99 100 self._pendingmode = None
100 101
101 102 @contextlib.contextmanager
102 103 def parentchange(self):
103 104 '''Context manager for handling dirstate parents.
104 105
105 106 If an exception occurs in the scope of the context manager,
106 107 the incoherent dirstate won't be written when wlock is
107 108 released.
108 109 '''
109 110 self._parentwriters += 1
110 111 yield
111 112 # Typically we want the "undo" step of a context manager in a
112 113 # finally block so it happens even when an exception
113 114 # occurs. In this case, however, we only want to decrement
114 115 # parentwriters if the code in the with statement exits
115 116 # normally, so we don't have a try/finally here on purpose.
116 117 self._parentwriters -= 1
117 118
118 119 def beginparentchange(self):
119 120 '''Marks the beginning of a set of changes that involve changing
120 121 the dirstate parents. If there is an exception during this time,
121 122 the dirstate will not be written when the wlock is released. This
122 123 prevents writing an incoherent dirstate where the parent doesn't
123 124 match the contents.
124 125 '''
125 126 self._ui.deprecwarn('beginparentchange is obsoleted by the '
126 127 'parentchange context manager.', '4.3')
127 128 self._parentwriters += 1
128 129
129 130 def endparentchange(self):
130 131 '''Marks the end of a set of changes that involve changing the
131 132 dirstate parents. Once all parent changes have been marked done,
132 133 the wlock will be free to write the dirstate on release.
133 134 '''
134 135 self._ui.deprecwarn('endparentchange is obsoleted by the '
135 136 'parentchange context manager.', '4.3')
136 137 if self._parentwriters > 0:
137 138 self._parentwriters -= 1
138 139
139 140 def pendingparentchange(self):
140 141 '''Returns true if the dirstate is in the middle of a set of changes
141 142 that modify the dirstate parent.
142 143 '''
143 144 return self._parentwriters > 0
144 145
145 146 @propertycache
146 147 def _map(self):
147 148 '''Return the dirstate contents as a map from filename to
148 149 (state, mode, size, time).'''
149 150 self._read()
150 151 return self._map
151 152
152 153 @propertycache
153 154 def _copymap(self):
154 155 self._read()
155 156 return self._copymap
156 157
157 158 @propertycache
158 159 def _identity(self):
159 160 self._read()
160 161 return self._identity
161 162
162 163 @propertycache
163 164 def _nonnormalset(self):
164 165 nonnorm, otherparents = nonnormalentries(self._map)
165 166 self._otherparentset = otherparents
166 167 return nonnorm
167 168
168 169 @propertycache
169 170 def _otherparentset(self):
170 171 nonnorm, otherparents = nonnormalentries(self._map)
171 172 self._nonnormalset = nonnorm
172 173 return otherparents
173 174
174 175 @propertycache
175 176 def _filefoldmap(self):
176 177 try:
177 178 makefilefoldmap = parsers.make_file_foldmap
178 179 except AttributeError:
179 180 pass
180 181 else:
181 182 return makefilefoldmap(self._map, util.normcasespec,
182 183 util.normcasefallback)
183 184
184 185 f = {}
185 186 normcase = util.normcase
186 187 for name, s in self._map.iteritems():
187 188 if s[0] != 'r':
188 189 f[normcase(name)] = name
189 190 f['.'] = '.' # prevents useless util.fspath() invocation
190 191 return f
191 192
192 193 @propertycache
193 194 def _dirfoldmap(self):
194 195 f = {}
195 196 normcase = util.normcase
196 197 for name in self._dirs:
197 198 f[normcase(name)] = name
198 199 return f
199 200
201 @property
202 def _sparsematcher(self):
203 """The matcher for the sparse checkout.
204
205 The working directory may not include every file from a manifest. The
206 matcher obtained by this property will match a path if it is to be
207 included in the working directory.
208 """
209 # TODO there is potential to cache this property. For now, the matcher
210 # is resolved on every access. (But the called function does use a
211 # cache to keep the lookup fast.)
212 return self._sparsematchfn()
213
200 214 @repocache('branch')
201 215 def _branch(self):
202 216 try:
203 217 return self._opener.read("branch").strip() or "default"
204 218 except IOError as inst:
205 219 if inst.errno != errno.ENOENT:
206 220 raise
207 221 return "default"
208 222
209 223 @propertycache
210 224 def _pl(self):
211 225 try:
212 226 fp = self._opendirstatefile()
213 227 st = fp.read(40)
214 228 fp.close()
215 229 l = len(st)
216 230 if l == 40:
217 231 return st[:20], st[20:40]
218 232 elif l > 0 and l < 40:
219 233 raise error.Abort(_('working directory state appears damaged!'))
220 234 except IOError as err:
221 235 if err.errno != errno.ENOENT:
222 236 raise
223 237 return [nullid, nullid]
224 238
225 239 @propertycache
226 240 def _dirs(self):
227 241 return util.dirs(self._map, 'r')
228 242
229 243 def dirs(self):
230 244 return self._dirs
231 245
232 246 @rootcache('.hgignore')
233 247 def _ignore(self):
234 248 files = self._ignorefiles()
235 249 if not files:
236 250 return matchmod.never(self._root, '')
237 251
238 252 pats = ['include:%s' % f for f in files]
239 253 return matchmod.match(self._root, '', [], pats, warn=self._ui.warn)
240 254
241 255 @propertycache
242 256 def _slash(self):
243 257 return self._ui.configbool('ui', 'slash') and pycompat.ossep != '/'
244 258
245 259 @propertycache
246 260 def _checklink(self):
247 261 return util.checklink(self._root)
248 262
249 263 @propertycache
250 264 def _checkexec(self):
251 265 return util.checkexec(self._root)
252 266
253 267 @propertycache
254 268 def _checkcase(self):
255 269 return not util.fscasesensitive(self._join('.hg'))
256 270
257 271 def _join(self, f):
258 272 # much faster than os.path.join()
259 273 # it's safe because f is always a relative path
260 274 return self._rootdir + f
261 275
262 276 def flagfunc(self, buildfallback):
263 277 if self._checklink and self._checkexec:
264 278 def f(x):
265 279 try:
266 280 st = os.lstat(self._join(x))
267 281 if util.statislink(st):
268 282 return 'l'
269 283 if util.statisexec(st):
270 284 return 'x'
271 285 except OSError:
272 286 pass
273 287 return ''
274 288 return f
275 289
276 290 fallback = buildfallback()
277 291 if self._checklink:
278 292 def f(x):
279 293 if os.path.islink(self._join(x)):
280 294 return 'l'
281 295 if 'x' in fallback(x):
282 296 return 'x'
283 297 return ''
284 298 return f
285 299 if self._checkexec:
286 300 def f(x):
287 301 if 'l' in fallback(x):
288 302 return 'l'
289 303 if util.isexec(self._join(x)):
290 304 return 'x'
291 305 return ''
292 306 return f
293 307 else:
294 308 return fallback
295 309
296 310 @propertycache
297 311 def _cwd(self):
298 312 # internal config: ui.forcecwd
299 313 forcecwd = self._ui.config('ui', 'forcecwd')
300 314 if forcecwd:
301 315 return forcecwd
302 316 return pycompat.getcwd()
303 317
304 318 def getcwd(self):
305 319 '''Return the path from which a canonical path is calculated.
306 320
307 321 This path should be used to resolve file patterns or to convert
308 322 canonical paths back to file paths for display. It shouldn't be
309 323 used to get real file paths. Use vfs functions instead.
310 324 '''
311 325 cwd = self._cwd
312 326 if cwd == self._root:
313 327 return ''
314 328 # self._root ends with a path separator if self._root is '/' or 'C:\'
315 329 rootsep = self._root
316 330 if not util.endswithsep(rootsep):
317 331 rootsep += pycompat.ossep
318 332 if cwd.startswith(rootsep):
319 333 return cwd[len(rootsep):]
320 334 else:
321 335 # we're outside the repo. return an absolute path.
322 336 return cwd
323 337
324 338 def pathto(self, f, cwd=None):
325 339 if cwd is None:
326 340 cwd = self.getcwd()
327 341 path = util.pathto(self._root, cwd, f)
328 342 if self._slash:
329 343 return util.pconvert(path)
330 344 return path
331 345
332 346 def __getitem__(self, key):
333 347 '''Return the current state of key (a filename) in the dirstate.
334 348
335 349 States are:
336 350 n normal
337 351 m needs merging
338 352 r marked for removal
339 353 a marked for addition
340 354 ? not tracked
341 355 '''
342 356 return self._map.get(key, ("?",))[0]
343 357
344 358 def __contains__(self, key):
345 359 return key in self._map
346 360
347 361 def __iter__(self):
348 362 for x in sorted(self._map):
349 363 yield x
350 364
351 365 def items(self):
352 366 return self._map.iteritems()
353 367
354 368 iteritems = items
355 369
356 370 def parents(self):
357 371 return [self._validate(p) for p in self._pl]
358 372
359 373 def p1(self):
360 374 return self._validate(self._pl[0])
361 375
362 376 def p2(self):
363 377 return self._validate(self._pl[1])
364 378
365 379 def branch(self):
366 380 return encoding.tolocal(self._branch)
367 381
368 382 def setparents(self, p1, p2=nullid):
369 383 """Set dirstate parents to p1 and p2.
370 384
371 385 When moving from two parents to one, 'm' merged entries a
372 386 adjusted to normal and previous copy records discarded and
373 387 returned by the call.
374 388
375 389 See localrepo.setparents()
376 390 """
377 391 if self._parentwriters == 0:
378 392 raise ValueError("cannot set dirstate parent without "
379 393 "calling dirstate.beginparentchange")
380 394
381 395 self._dirty = self._dirtypl = True
382 396 oldp2 = self._pl[1]
383 397 if self._origpl is None:
384 398 self._origpl = self._pl
385 399 self._pl = p1, p2
386 400 copies = {}
387 401 if oldp2 != nullid and p2 == nullid:
388 402 candidatefiles = self._nonnormalset.union(self._otherparentset)
389 403 for f in candidatefiles:
390 404 s = self._map.get(f)
391 405 if s is None:
392 406 continue
393 407
394 408 # Discard 'm' markers when moving away from a merge state
395 409 if s[0] == 'm':
396 410 if f in self._copymap:
397 411 copies[f] = self._copymap[f]
398 412 self.normallookup(f)
399 413 # Also fix up otherparent markers
400 414 elif s[0] == 'n' and s[2] == -2:
401 415 if f in self._copymap:
402 416 copies[f] = self._copymap[f]
403 417 self.add(f)
404 418 return copies
405 419
406 420 def setbranch(self, branch):
407 421 self._branch = encoding.fromlocal(branch)
408 422 f = self._opener('branch', 'w', atomictemp=True, checkambig=True)
409 423 try:
410 424 f.write(self._branch + '\n')
411 425 f.close()
412 426
413 427 # make sure filecache has the correct stat info for _branch after
414 428 # replacing the underlying file
415 429 ce = self._filecache['_branch']
416 430 if ce:
417 431 ce.refresh()
418 432 except: # re-raises
419 433 f.discard()
420 434 raise
421 435
422 436 def _opendirstatefile(self):
423 437 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
424 438 if self._pendingmode is not None and self._pendingmode != mode:
425 439 fp.close()
426 440 raise error.Abort(_('working directory state may be '
427 441 'changed parallelly'))
428 442 self._pendingmode = mode
429 443 return fp
430 444
431 445 def _read(self):
432 446 self._map = {}
433 447 self._copymap = {}
434 448 # ignore HG_PENDING because identity is used only for writing
435 449 self._identity = util.filestat.frompath(
436 450 self._opener.join(self._filename))
437 451 try:
438 452 fp = self._opendirstatefile()
439 453 try:
440 454 st = fp.read()
441 455 finally:
442 456 fp.close()
443 457 except IOError as err:
444 458 if err.errno != errno.ENOENT:
445 459 raise
446 460 return
447 461 if not st:
448 462 return
449 463
450 464 if util.safehasattr(parsers, 'dict_new_presized'):
451 465 # Make an estimate of the number of files in the dirstate based on
452 466 # its size. From a linear regression on a set of real-world repos,
453 467 # all over 10,000 files, the size of a dirstate entry is 85
454 468 # bytes. The cost of resizing is significantly higher than the cost
455 469 # of filling in a larger presized dict, so subtract 20% from the
456 470 # size.
457 471 #
458 472 # This heuristic is imperfect in many ways, so in a future dirstate
459 473 # format update it makes sense to just record the number of entries
460 474 # on write.
461 475 self._map = parsers.dict_new_presized(len(st) / 71)
462 476
463 477 # Python's garbage collector triggers a GC each time a certain number
464 478 # of container objects (the number being defined by
465 479 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
466 480 # for each file in the dirstate. The C version then immediately marks
467 481 # them as not to be tracked by the collector. However, this has no
468 482 # effect on when GCs are triggered, only on what objects the GC looks
469 483 # into. This means that O(number of files) GCs are unavoidable.
470 484 # Depending on when in the process's lifetime the dirstate is parsed,
471 485 # this can get very expensive. As a workaround, disable GC while
472 486 # parsing the dirstate.
473 487 #
474 488 # (we cannot decorate the function directly since it is in a C module)
475 489 parse_dirstate = util.nogc(parsers.parse_dirstate)
476 490 p = parse_dirstate(self._map, self._copymap, st)
477 491 if not self._dirtypl:
478 492 self._pl = p
479 493
480 494 def invalidate(self):
481 495 '''Causes the next access to reread the dirstate.
482 496
483 497 This is different from localrepo.invalidatedirstate() because it always
484 498 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
485 499 check whether the dirstate has changed before rereading it.'''
486 500
487 501 for a in ("_map", "_copymap", "_identity",
488 502 "_filefoldmap", "_dirfoldmap", "_branch",
489 503 "_pl", "_dirs", "_ignore", "_nonnormalset",
490 504 "_otherparentset"):
491 505 if a in self.__dict__:
492 506 delattr(self, a)
493 507 self._lastnormaltime = 0
494 508 self._dirty = False
495 509 self._updatedfiles.clear()
496 510 self._parentwriters = 0
497 511 self._origpl = None
498 512
499 513 def copy(self, source, dest):
500 514 """Mark dest as a copy of source. Unmark dest if source is None."""
501 515 if source == dest:
502 516 return
503 517 self._dirty = True
504 518 if source is not None:
505 519 self._copymap[dest] = source
506 520 self._updatedfiles.add(source)
507 521 self._updatedfiles.add(dest)
508 522 elif dest in self._copymap:
509 523 del self._copymap[dest]
510 524 self._updatedfiles.add(dest)
511 525
512 526 def copied(self, file):
513 527 return self._copymap.get(file, None)
514 528
515 529 def copies(self):
516 530 return self._copymap
517 531
518 532 def _droppath(self, f):
519 533 if self[f] not in "?r" and "_dirs" in self.__dict__:
520 534 self._dirs.delpath(f)
521 535
522 536 if "_filefoldmap" in self.__dict__:
523 537 normed = util.normcase(f)
524 538 if normed in self._filefoldmap:
525 539 del self._filefoldmap[normed]
526 540
527 541 self._updatedfiles.add(f)
528 542
529 543 def _addpath(self, f, state, mode, size, mtime):
530 544 oldstate = self[f]
531 545 if state == 'a' or oldstate == 'r':
532 546 scmutil.checkfilename(f)
533 547 if f in self._dirs:
534 548 raise error.Abort(_('directory %r already in dirstate') % f)
535 549 # shadows
536 550 for d in util.finddirs(f):
537 551 if d in self._dirs:
538 552 break
539 553 if d in self._map and self[d] != 'r':
540 554 raise error.Abort(
541 555 _('file %r in dirstate clashes with %r') % (d, f))
542 556 if oldstate in "?r" and "_dirs" in self.__dict__:
543 557 self._dirs.addpath(f)
544 558 self._dirty = True
545 559 self._updatedfiles.add(f)
546 560 self._map[f] = dirstatetuple(state, mode, size, mtime)
547 561 if state != 'n' or mtime == -1:
548 562 self._nonnormalset.add(f)
549 563 if size == -2:
550 564 self._otherparentset.add(f)
551 565
552 566 def normal(self, f):
553 567 '''Mark a file normal and clean.'''
554 568 s = os.lstat(self._join(f))
555 569 mtime = s.st_mtime
556 570 self._addpath(f, 'n', s.st_mode,
557 571 s.st_size & _rangemask, mtime & _rangemask)
558 572 if f in self._copymap:
559 573 del self._copymap[f]
560 574 if f in self._nonnormalset:
561 575 self._nonnormalset.remove(f)
562 576 if mtime > self._lastnormaltime:
563 577 # Remember the most recent modification timeslot for status(),
564 578 # to make sure we won't miss future size-preserving file content
565 579 # modifications that happen within the same timeslot.
566 580 self._lastnormaltime = mtime
567 581
568 582 def normallookup(self, f):
569 583 '''Mark a file normal, but possibly dirty.'''
570 584 if self._pl[1] != nullid and f in self._map:
571 585 # if there is a merge going on and the file was either
572 586 # in state 'm' (-1) or coming from other parent (-2) before
573 587 # being removed, restore that state.
574 588 entry = self._map[f]
575 589 if entry[0] == 'r' and entry[2] in (-1, -2):
576 590 source = self._copymap.get(f)
577 591 if entry[2] == -1:
578 592 self.merge(f)
579 593 elif entry[2] == -2:
580 594 self.otherparent(f)
581 595 if source:
582 596 self.copy(source, f)
583 597 return
584 598 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
585 599 return
586 600 self._addpath(f, 'n', 0, -1, -1)
587 601 if f in self._copymap:
588 602 del self._copymap[f]
589 603 if f in self._nonnormalset:
590 604 self._nonnormalset.remove(f)
591 605
592 606 def otherparent(self, f):
593 607 '''Mark as coming from the other parent, always dirty.'''
594 608 if self._pl[1] == nullid:
595 609 raise error.Abort(_("setting %r to other parent "
596 610 "only allowed in merges") % f)
597 611 if f in self and self[f] == 'n':
598 612 # merge-like
599 613 self._addpath(f, 'm', 0, -2, -1)
600 614 else:
601 615 # add-like
602 616 self._addpath(f, 'n', 0, -2, -1)
603 617
604 618 if f in self._copymap:
605 619 del self._copymap[f]
606 620
607 621 def add(self, f):
608 622 '''Mark a file added.'''
609 623 self._addpath(f, 'a', 0, -1, -1)
610 624 if f in self._copymap:
611 625 del self._copymap[f]
612 626
613 627 def remove(self, f):
614 628 '''Mark a file removed.'''
615 629 self._dirty = True
616 630 self._droppath(f)
617 631 size = 0
618 632 if self._pl[1] != nullid and f in self._map:
619 633 # backup the previous state
620 634 entry = self._map[f]
621 635 if entry[0] == 'm': # merge
622 636 size = -1
623 637 elif entry[0] == 'n' and entry[2] == -2: # other parent
624 638 size = -2
625 639 self._otherparentset.add(f)
626 640 self._map[f] = dirstatetuple('r', 0, size, 0)
627 641 self._nonnormalset.add(f)
628 642 if size == 0 and f in self._copymap:
629 643 del self._copymap[f]
630 644
631 645 def merge(self, f):
632 646 '''Mark a file merged.'''
633 647 if self._pl[1] == nullid:
634 648 return self.normallookup(f)
635 649 return self.otherparent(f)
636 650
637 651 def drop(self, f):
638 652 '''Drop a file from the dirstate'''
639 653 if f in self._map:
640 654 self._dirty = True
641 655 self._droppath(f)
642 656 del self._map[f]
643 657 if f in self._nonnormalset:
644 658 self._nonnormalset.remove(f)
645 659 if f in self._copymap:
646 660 del self._copymap[f]
647 661
648 662 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
649 663 if exists is None:
650 664 exists = os.path.lexists(os.path.join(self._root, path))
651 665 if not exists:
652 666 # Maybe a path component exists
653 667 if not ignoremissing and '/' in path:
654 668 d, f = path.rsplit('/', 1)
655 669 d = self._normalize(d, False, ignoremissing, None)
656 670 folded = d + "/" + f
657 671 else:
658 672 # No path components, preserve original case
659 673 folded = path
660 674 else:
661 675 # recursively normalize leading directory components
662 676 # against dirstate
663 677 if '/' in normed:
664 678 d, f = normed.rsplit('/', 1)
665 679 d = self._normalize(d, False, ignoremissing, True)
666 680 r = self._root + "/" + d
667 681 folded = d + "/" + util.fspath(f, r)
668 682 else:
669 683 folded = util.fspath(normed, self._root)
670 684 storemap[normed] = folded
671 685
672 686 return folded
673 687
674 688 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
675 689 normed = util.normcase(path)
676 690 folded = self._filefoldmap.get(normed, None)
677 691 if folded is None:
678 692 if isknown:
679 693 folded = path
680 694 else:
681 695 folded = self._discoverpath(path, normed, ignoremissing, exists,
682 696 self._filefoldmap)
683 697 return folded
684 698
685 699 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
686 700 normed = util.normcase(path)
687 701 folded = self._filefoldmap.get(normed, None)
688 702 if folded is None:
689 703 folded = self._dirfoldmap.get(normed, None)
690 704 if folded is None:
691 705 if isknown:
692 706 folded = path
693 707 else:
694 708 # store discovered result in dirfoldmap so that future
695 709 # normalizefile calls don't start matching directories
696 710 folded = self._discoverpath(path, normed, ignoremissing, exists,
697 711 self._dirfoldmap)
698 712 return folded
699 713
700 714 def normalize(self, path, isknown=False, ignoremissing=False):
701 715 '''
702 716 normalize the case of a pathname when on a casefolding filesystem
703 717
704 718 isknown specifies whether the filename came from walking the
705 719 disk, to avoid extra filesystem access.
706 720
707 721 If ignoremissing is True, missing path are returned
708 722 unchanged. Otherwise, we try harder to normalize possibly
709 723 existing path components.
710 724
711 725 The normalized case is determined based on the following precedence:
712 726
713 727 - version of name already stored in the dirstate
714 728 - version of name stored on disk
715 729 - version provided via command arguments
716 730 '''
717 731
718 732 if self._checkcase:
719 733 return self._normalize(path, isknown, ignoremissing)
720 734 return path
721 735
722 736 def clear(self):
723 737 self._map = {}
724 738 self._nonnormalset = set()
725 739 self._otherparentset = set()
726 740 if "_dirs" in self.__dict__:
727 741 delattr(self, "_dirs")
728 742 self._copymap = {}
729 743 self._pl = [nullid, nullid]
730 744 self._lastnormaltime = 0
731 745 self._updatedfiles.clear()
732 746 self._dirty = True
733 747
734 748 def rebuild(self, parent, allfiles, changedfiles=None):
735 749 if changedfiles is None:
736 750 # Rebuild entire dirstate
737 751 changedfiles = allfiles
738 752 lastnormaltime = self._lastnormaltime
739 753 self.clear()
740 754 self._lastnormaltime = lastnormaltime
741 755
742 756 if self._origpl is None:
743 757 self._origpl = self._pl
744 758 self._pl = (parent, nullid)
745 759 for f in changedfiles:
746 760 if f in allfiles:
747 761 self.normallookup(f)
748 762 else:
749 763 self.drop(f)
750 764
751 765 self._dirty = True
752 766
753 767 def identity(self):
754 768 '''Return identity of dirstate itself to detect changing in storage
755 769
756 770 If identity of previous dirstate is equal to this, writing
757 771 changes based on the former dirstate out can keep consistency.
758 772 '''
759 773 return self._identity
760 774
761 775 def write(self, tr):
762 776 if not self._dirty:
763 777 return
764 778
765 779 filename = self._filename
766 780 if tr:
767 781 # 'dirstate.write()' is not only for writing in-memory
768 782 # changes out, but also for dropping ambiguous timestamp.
769 783 # delayed writing re-raise "ambiguous timestamp issue".
770 784 # See also the wiki page below for detail:
771 785 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
772 786
773 787 # emulate dropping timestamp in 'parsers.pack_dirstate'
774 788 now = _getfsnow(self._opener)
775 789 dmap = self._map
776 790 for f in self._updatedfiles:
777 791 e = dmap.get(f)
778 792 if e is not None and e[0] == 'n' and e[3] == now:
779 793 dmap[f] = dirstatetuple(e[0], e[1], e[2], -1)
780 794 self._nonnormalset.add(f)
781 795
782 796 # emulate that all 'dirstate.normal' results are written out
783 797 self._lastnormaltime = 0
784 798 self._updatedfiles.clear()
785 799
786 800 # delay writing in-memory changes out
787 801 tr.addfilegenerator('dirstate', (self._filename,),
788 802 self._writedirstate, location='plain')
789 803 return
790 804
791 805 st = self._opener(filename, "w", atomictemp=True, checkambig=True)
792 806 self._writedirstate(st)
793 807
794 808 def addparentchangecallback(self, category, callback):
795 809 """add a callback to be called when the wd parents are changed
796 810
797 811 Callback will be called with the following arguments:
798 812 dirstate, (oldp1, oldp2), (newp1, newp2)
799 813
800 814 Category is a unique identifier to allow overwriting an old callback
801 815 with a newer callback.
802 816 """
803 817 self._plchangecallbacks[category] = callback
804 818
805 819 def _writedirstate(self, st):
806 820 # notify callbacks about parents change
807 821 if self._origpl is not None and self._origpl != self._pl:
808 822 for c, callback in sorted(self._plchangecallbacks.iteritems()):
809 823 callback(self, self._origpl, self._pl)
810 824 self._origpl = None
811 825 # use the modification time of the newly created temporary file as the
812 826 # filesystem's notion of 'now'
813 827 now = util.fstat(st).st_mtime & _rangemask
814 828
815 829 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
816 830 # timestamp of each entries in dirstate, because of 'now > mtime'
817 831 delaywrite = self._ui.configint('debug', 'dirstate.delaywrite', 0)
818 832 if delaywrite > 0:
819 833 # do we have any files to delay for?
820 834 for f, e in self._map.iteritems():
821 835 if e[0] == 'n' and e[3] == now:
822 836 import time # to avoid useless import
823 837 # rather than sleep n seconds, sleep until the next
824 838 # multiple of n seconds
825 839 clock = time.time()
826 840 start = int(clock) - (int(clock) % delaywrite)
827 841 end = start + delaywrite
828 842 time.sleep(end - clock)
829 843 now = end # trust our estimate that the end is near now
830 844 break
831 845
832 846 st.write(parsers.pack_dirstate(self._map, self._copymap, self._pl, now))
833 847 self._nonnormalset, self._otherparentset = nonnormalentries(self._map)
834 848 st.close()
835 849 self._lastnormaltime = 0
836 850 self._dirty = self._dirtypl = False
837 851
838 852 def _dirignore(self, f):
839 853 if f == '.':
840 854 return False
841 855 if self._ignore(f):
842 856 return True
843 857 for p in util.finddirs(f):
844 858 if self._ignore(p):
845 859 return True
846 860 return False
847 861
848 862 def _ignorefiles(self):
849 863 files = []
850 864 if os.path.exists(self._join('.hgignore')):
851 865 files.append(self._join('.hgignore'))
852 866 for name, path in self._ui.configitems("ui"):
853 867 if name == 'ignore' or name.startswith('ignore.'):
854 868 # we need to use os.path.join here rather than self._join
855 869 # because path is arbitrary and user-specified
856 870 files.append(os.path.join(self._rootdir, util.expandpath(path)))
857 871 return files
858 872
859 873 def _ignorefileandline(self, f):
860 874 files = collections.deque(self._ignorefiles())
861 875 visited = set()
862 876 while files:
863 877 i = files.popleft()
864 878 patterns = matchmod.readpatternfile(i, self._ui.warn,
865 879 sourceinfo=True)
866 880 for pattern, lineno, line in patterns:
867 881 kind, p = matchmod._patsplit(pattern, 'glob')
868 882 if kind == "subinclude":
869 883 if p not in visited:
870 884 files.append(p)
871 885 continue
872 886 m = matchmod.match(self._root, '', [], [pattern],
873 887 warn=self._ui.warn)
874 888 if m(f):
875 889 return (i, lineno, line)
876 890 visited.add(i)
877 891 return (None, -1, "")
878 892
879 893 def _walkexplicit(self, match, subrepos):
880 894 '''Get stat data about the files explicitly specified by match.
881 895
882 896 Return a triple (results, dirsfound, dirsnotfound).
883 897 - results is a mapping from filename to stat result. It also contains
884 898 listings mapping subrepos and .hg to None.
885 899 - dirsfound is a list of files found to be directories.
886 900 - dirsnotfound is a list of files that the dirstate thinks are
887 901 directories and that were not found.'''
888 902
889 903 def badtype(mode):
890 904 kind = _('unknown')
891 905 if stat.S_ISCHR(mode):
892 906 kind = _('character device')
893 907 elif stat.S_ISBLK(mode):
894 908 kind = _('block device')
895 909 elif stat.S_ISFIFO(mode):
896 910 kind = _('fifo')
897 911 elif stat.S_ISSOCK(mode):
898 912 kind = _('socket')
899 913 elif stat.S_ISDIR(mode):
900 914 kind = _('directory')
901 915 return _('unsupported file type (type is %s)') % kind
902 916
903 917 matchedir = match.explicitdir
904 918 badfn = match.bad
905 919 dmap = self._map
906 920 lstat = os.lstat
907 921 getkind = stat.S_IFMT
908 922 dirkind = stat.S_IFDIR
909 923 regkind = stat.S_IFREG
910 924 lnkkind = stat.S_IFLNK
911 925 join = self._join
912 926 dirsfound = []
913 927 foundadd = dirsfound.append
914 928 dirsnotfound = []
915 929 notfoundadd = dirsnotfound.append
916 930
917 931 if not match.isexact() and self._checkcase:
918 932 normalize = self._normalize
919 933 else:
920 934 normalize = None
921 935
922 936 files = sorted(match.files())
923 937 subrepos.sort()
924 938 i, j = 0, 0
925 939 while i < len(files) and j < len(subrepos):
926 940 subpath = subrepos[j] + "/"
927 941 if files[i] < subpath:
928 942 i += 1
929 943 continue
930 944 while i < len(files) and files[i].startswith(subpath):
931 945 del files[i]
932 946 j += 1
933 947
934 948 if not files or '.' in files:
935 949 files = ['.']
936 950 results = dict.fromkeys(subrepos)
937 951 results['.hg'] = None
938 952
939 953 alldirs = None
940 954 for ff in files:
941 955 # constructing the foldmap is expensive, so don't do it for the
942 956 # common case where files is ['.']
943 957 if normalize and ff != '.':
944 958 nf = normalize(ff, False, True)
945 959 else:
946 960 nf = ff
947 961 if nf in results:
948 962 continue
949 963
950 964 try:
951 965 st = lstat(join(nf))
952 966 kind = getkind(st.st_mode)
953 967 if kind == dirkind:
954 968 if nf in dmap:
955 969 # file replaced by dir on disk but still in dirstate
956 970 results[nf] = None
957 971 if matchedir:
958 972 matchedir(nf)
959 973 foundadd((nf, ff))
960 974 elif kind == regkind or kind == lnkkind:
961 975 results[nf] = st
962 976 else:
963 977 badfn(ff, badtype(kind))
964 978 if nf in dmap:
965 979 results[nf] = None
966 980 except OSError as inst: # nf not found on disk - it is dirstate only
967 981 if nf in dmap: # does it exactly match a missing file?
968 982 results[nf] = None
969 983 else: # does it match a missing directory?
970 984 if alldirs is None:
971 985 alldirs = util.dirs(dmap)
972 986 if nf in alldirs:
973 987 if matchedir:
974 988 matchedir(nf)
975 989 notfoundadd(nf)
976 990 else:
977 991 badfn(ff, inst.strerror)
978 992
979 993 # Case insensitive filesystems cannot rely on lstat() failing to detect
980 994 # a case-only rename. Prune the stat object for any file that does not
981 995 # match the case in the filesystem, if there are multiple files that
982 996 # normalize to the same path.
983 997 if match.isexact() and self._checkcase:
984 998 normed = {}
985 999
986 1000 for f, st in results.iteritems():
987 1001 if st is None:
988 1002 continue
989 1003
990 1004 nc = util.normcase(f)
991 1005 paths = normed.get(nc)
992 1006
993 1007 if paths is None:
994 1008 paths = set()
995 1009 normed[nc] = paths
996 1010
997 1011 paths.add(f)
998 1012
999 1013 for norm, paths in normed.iteritems():
1000 1014 if len(paths) > 1:
1001 1015 for path in paths:
1002 1016 folded = self._discoverpath(path, norm, True, None,
1003 1017 self._dirfoldmap)
1004 1018 if path != folded:
1005 1019 results[path] = None
1006 1020
1007 1021 return results, dirsfound, dirsnotfound
1008 1022
1009 1023 def walk(self, match, subrepos, unknown, ignored, full=True):
1010 1024 '''
1011 1025 Walk recursively through the directory tree, finding all files
1012 1026 matched by match.
1013 1027
1014 1028 If full is False, maybe skip some known-clean files.
1015 1029
1016 1030 Return a dict mapping filename to stat-like object (either
1017 1031 mercurial.osutil.stat instance or return value of os.stat()).
1018 1032
1019 1033 '''
1020 1034 # full is a flag that extensions that hook into walk can use -- this
1021 1035 # implementation doesn't use it at all. This satisfies the contract
1022 1036 # because we only guarantee a "maybe".
1023 1037
1024 1038 if ignored:
1025 1039 ignore = util.never
1026 1040 dirignore = util.never
1027 1041 elif unknown:
1028 1042 ignore = self._ignore
1029 1043 dirignore = self._dirignore
1030 1044 else:
1031 1045 # if not unknown and not ignored, drop dir recursion and step 2
1032 1046 ignore = util.always
1033 1047 dirignore = util.always
1034 1048
1035 1049 matchfn = match.matchfn
1036 1050 matchalways = match.always()
1037 1051 matchtdir = match.traversedir
1038 1052 dmap = self._map
1039 1053 listdir = util.listdir
1040 1054 lstat = os.lstat
1041 1055 dirkind = stat.S_IFDIR
1042 1056 regkind = stat.S_IFREG
1043 1057 lnkkind = stat.S_IFLNK
1044 1058 join = self._join
1045 1059
1046 1060 exact = skipstep3 = False
1047 1061 if match.isexact(): # match.exact
1048 1062 exact = True
1049 1063 dirignore = util.always # skip step 2
1050 1064 elif match.prefix(): # match.match, no patterns
1051 1065 skipstep3 = True
1052 1066
1053 1067 if not exact and self._checkcase:
1054 1068 normalize = self._normalize
1055 1069 normalizefile = self._normalizefile
1056 1070 skipstep3 = False
1057 1071 else:
1058 1072 normalize = self._normalize
1059 1073 normalizefile = None
1060 1074
1061 1075 # step 1: find all explicit files
1062 1076 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1063 1077
1064 1078 skipstep3 = skipstep3 and not (work or dirsnotfound)
1065 1079 work = [d for d in work if not dirignore(d[0])]
1066 1080
1067 1081 # step 2: visit subdirectories
1068 1082 def traverse(work, alreadynormed):
1069 1083 wadd = work.append
1070 1084 while work:
1071 1085 nd = work.pop()
1072 1086 if not match.visitdir(nd):
1073 1087 continue
1074 1088 skip = None
1075 1089 if nd == '.':
1076 1090 nd = ''
1077 1091 else:
1078 1092 skip = '.hg'
1079 1093 try:
1080 1094 entries = listdir(join(nd), stat=True, skip=skip)
1081 1095 except OSError as inst:
1082 1096 if inst.errno in (errno.EACCES, errno.ENOENT):
1083 1097 match.bad(self.pathto(nd), inst.strerror)
1084 1098 continue
1085 1099 raise
1086 1100 for f, kind, st in entries:
1087 1101 if normalizefile:
1088 1102 # even though f might be a directory, we're only
1089 1103 # interested in comparing it to files currently in the
1090 1104 # dmap -- therefore normalizefile is enough
1091 1105 nf = normalizefile(nd and (nd + "/" + f) or f, True,
1092 1106 True)
1093 1107 else:
1094 1108 nf = nd and (nd + "/" + f) or f
1095 1109 if nf not in results:
1096 1110 if kind == dirkind:
1097 1111 if not ignore(nf):
1098 1112 if matchtdir:
1099 1113 matchtdir(nf)
1100 1114 wadd(nf)
1101 1115 if nf in dmap and (matchalways or matchfn(nf)):
1102 1116 results[nf] = None
1103 1117 elif kind == regkind or kind == lnkkind:
1104 1118 if nf in dmap:
1105 1119 if matchalways or matchfn(nf):
1106 1120 results[nf] = st
1107 1121 elif ((matchalways or matchfn(nf))
1108 1122 and not ignore(nf)):
1109 1123 # unknown file -- normalize if necessary
1110 1124 if not alreadynormed:
1111 1125 nf = normalize(nf, False, True)
1112 1126 results[nf] = st
1113 1127 elif nf in dmap and (matchalways or matchfn(nf)):
1114 1128 results[nf] = None
1115 1129
1116 1130 for nd, d in work:
1117 1131 # alreadynormed means that processwork doesn't have to do any
1118 1132 # expensive directory normalization
1119 1133 alreadynormed = not normalize or nd == d
1120 1134 traverse([d], alreadynormed)
1121 1135
1122 1136 for s in subrepos:
1123 1137 del results[s]
1124 1138 del results['.hg']
1125 1139
1126 1140 # step 3: visit remaining files from dmap
1127 1141 if not skipstep3 and not exact:
1128 1142 # If a dmap file is not in results yet, it was either
1129 1143 # a) not matching matchfn b) ignored, c) missing, or d) under a
1130 1144 # symlink directory.
1131 1145 if not results and matchalways:
1132 1146 visit = [f for f in dmap]
1133 1147 else:
1134 1148 visit = [f for f in dmap if f not in results and matchfn(f)]
1135 1149 visit.sort()
1136 1150
1137 1151 if unknown:
1138 1152 # unknown == True means we walked all dirs under the roots
1139 1153 # that wasn't ignored, and everything that matched was stat'ed
1140 1154 # and is already in results.
1141 1155 # The rest must thus be ignored or under a symlink.
1142 1156 audit_path = pathutil.pathauditor(self._root)
1143 1157
1144 1158 for nf in iter(visit):
1145 1159 # If a stat for the same file was already added with a
1146 1160 # different case, don't add one for this, since that would
1147 1161 # make it appear as if the file exists under both names
1148 1162 # on disk.
1149 1163 if (normalizefile and
1150 1164 normalizefile(nf, True, True) in results):
1151 1165 results[nf] = None
1152 1166 # Report ignored items in the dmap as long as they are not
1153 1167 # under a symlink directory.
1154 1168 elif audit_path.check(nf):
1155 1169 try:
1156 1170 results[nf] = lstat(join(nf))
1157 1171 # file was just ignored, no links, and exists
1158 1172 except OSError:
1159 1173 # file doesn't exist
1160 1174 results[nf] = None
1161 1175 else:
1162 1176 # It's either missing or under a symlink directory
1163 1177 # which we in this case report as missing
1164 1178 results[nf] = None
1165 1179 else:
1166 1180 # We may not have walked the full directory tree above,
1167 1181 # so stat and check everything we missed.
1168 1182 iv = iter(visit)
1169 1183 for st in util.statfiles([join(i) for i in visit]):
1170 1184 results[next(iv)] = st
1171 1185 return results
1172 1186
1173 1187 def status(self, match, subrepos, ignored, clean, unknown):
1174 1188 '''Determine the status of the working copy relative to the
1175 1189 dirstate and return a pair of (unsure, status), where status is of type
1176 1190 scmutil.status and:
1177 1191
1178 1192 unsure:
1179 1193 files that might have been modified since the dirstate was
1180 1194 written, but need to be read to be sure (size is the same
1181 1195 but mtime differs)
1182 1196 status.modified:
1183 1197 files that have definitely been modified since the dirstate
1184 1198 was written (different size or mode)
1185 1199 status.clean:
1186 1200 files that have definitely not been modified since the
1187 1201 dirstate was written
1188 1202 '''
1189 1203 listignored, listclean, listunknown = ignored, clean, unknown
1190 1204 lookup, modified, added, unknown, ignored = [], [], [], [], []
1191 1205 removed, deleted, clean = [], [], []
1192 1206
1193 1207 dmap = self._map
1194 1208 ladd = lookup.append # aka "unsure"
1195 1209 madd = modified.append
1196 1210 aadd = added.append
1197 1211 uadd = unknown.append
1198 1212 iadd = ignored.append
1199 1213 radd = removed.append
1200 1214 dadd = deleted.append
1201 1215 cadd = clean.append
1202 1216 mexact = match.exact
1203 1217 dirignore = self._dirignore
1204 1218 checkexec = self._checkexec
1205 1219 copymap = self._copymap
1206 1220 lastnormaltime = self._lastnormaltime
1207 1221
1208 1222 # We need to do full walks when either
1209 1223 # - we're listing all clean files, or
1210 1224 # - match.traversedir does something, because match.traversedir should
1211 1225 # be called for every dir in the working dir
1212 1226 full = listclean or match.traversedir is not None
1213 1227 for fn, st in self.walk(match, subrepos, listunknown, listignored,
1214 1228 full=full).iteritems():
1215 1229 if fn not in dmap:
1216 1230 if (listignored or mexact(fn)) and dirignore(fn):
1217 1231 if listignored:
1218 1232 iadd(fn)
1219 1233 else:
1220 1234 uadd(fn)
1221 1235 continue
1222 1236
1223 1237 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1224 1238 # written like that for performance reasons. dmap[fn] is not a
1225 1239 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1226 1240 # opcode has fast paths when the value to be unpacked is a tuple or
1227 1241 # a list, but falls back to creating a full-fledged iterator in
1228 1242 # general. That is much slower than simply accessing and storing the
1229 1243 # tuple members one by one.
1230 1244 t = dmap[fn]
1231 1245 state = t[0]
1232 1246 mode = t[1]
1233 1247 size = t[2]
1234 1248 time = t[3]
1235 1249
1236 1250 if not st and state in "nma":
1237 1251 dadd(fn)
1238 1252 elif state == 'n':
1239 1253 if (size >= 0 and
1240 1254 ((size != st.st_size and size != st.st_size & _rangemask)
1241 1255 or ((mode ^ st.st_mode) & 0o100 and checkexec))
1242 1256 or size == -2 # other parent
1243 1257 or fn in copymap):
1244 1258 madd(fn)
1245 1259 elif time != st.st_mtime and time != st.st_mtime & _rangemask:
1246 1260 ladd(fn)
1247 1261 elif st.st_mtime == lastnormaltime:
1248 1262 # fn may have just been marked as normal and it may have
1249 1263 # changed in the same second without changing its size.
1250 1264 # This can happen if we quickly do multiple commits.
1251 1265 # Force lookup, so we don't miss such a racy file change.
1252 1266 ladd(fn)
1253 1267 elif listclean:
1254 1268 cadd(fn)
1255 1269 elif state == 'm':
1256 1270 madd(fn)
1257 1271 elif state == 'a':
1258 1272 aadd(fn)
1259 1273 elif state == 'r':
1260 1274 radd(fn)
1261 1275
1262 1276 return (lookup, scmutil.status(modified, added, removed, deleted,
1263 1277 unknown, ignored, clean))
1264 1278
1265 1279 def matches(self, match):
1266 1280 '''
1267 1281 return files in the dirstate (in whatever state) filtered by match
1268 1282 '''
1269 1283 dmap = self._map
1270 1284 if match.always():
1271 1285 return dmap.keys()
1272 1286 files = match.files()
1273 1287 if match.isexact():
1274 1288 # fast path -- filter the other way around, since typically files is
1275 1289 # much smaller than dmap
1276 1290 return [f for f in files if f in dmap]
1277 1291 if match.prefix() and all(fn in dmap for fn in files):
1278 1292 # fast path -- all the values are known to be files, so just return
1279 1293 # that
1280 1294 return list(files)
1281 1295 return [f for f in dmap if match(f)]
1282 1296
1283 1297 def _actualfilename(self, tr):
1284 1298 if tr:
1285 1299 return self._pendingfilename
1286 1300 else:
1287 1301 return self._filename
1288 1302
1289 1303 def savebackup(self, tr, suffix='', prefix=''):
1290 1304 '''Save current dirstate into backup file with suffix'''
1291 1305 assert len(suffix) > 0 or len(prefix) > 0
1292 1306 filename = self._actualfilename(tr)
1293 1307
1294 1308 # use '_writedirstate' instead of 'write' to write changes certainly,
1295 1309 # because the latter omits writing out if transaction is running.
1296 1310 # output file will be used to create backup of dirstate at this point.
1297 1311 if self._dirty or not self._opener.exists(filename):
1298 1312 self._writedirstate(self._opener(filename, "w", atomictemp=True,
1299 1313 checkambig=True))
1300 1314
1301 1315 if tr:
1302 1316 # ensure that subsequent tr.writepending returns True for
1303 1317 # changes written out above, even if dirstate is never
1304 1318 # changed after this
1305 1319 tr.addfilegenerator('dirstate', (self._filename,),
1306 1320 self._writedirstate, location='plain')
1307 1321
1308 1322 # ensure that pending file written above is unlinked at
1309 1323 # failure, even if tr.writepending isn't invoked until the
1310 1324 # end of this transaction
1311 1325 tr.registertmp(filename, location='plain')
1312 1326
1313 1327 backupname = prefix + self._filename + suffix
1314 1328 assert backupname != filename
1315 1329 self._opener.tryunlink(backupname)
1316 1330 # hardlink backup is okay because _writedirstate is always called
1317 1331 # with an "atomictemp=True" file.
1318 1332 util.copyfile(self._opener.join(filename),
1319 1333 self._opener.join(backupname), hardlink=True)
1320 1334
1321 1335 def restorebackup(self, tr, suffix='', prefix=''):
1322 1336 '''Restore dirstate by backup file with suffix'''
1323 1337 assert len(suffix) > 0 or len(prefix) > 0
1324 1338 # this "invalidate()" prevents "wlock.release()" from writing
1325 1339 # changes of dirstate out after restoring from backup file
1326 1340 self.invalidate()
1327 1341 filename = self._actualfilename(tr)
1328 1342 # using self._filename to avoid having "pending" in the backup filename
1329 1343 self._opener.rename(prefix + self._filename + suffix, filename,
1330 1344 checkambig=True)
1331 1345
1332 1346 def clearbackup(self, tr, suffix='', prefix=''):
1333 1347 '''Clear backup file with suffix'''
1334 1348 assert len(suffix) > 0 or len(prefix) > 0
1335 1349 # using self._filename to avoid having "pending" in the backup filename
1336 1350 self._opener.unlink(prefix + self._filename + suffix)
@@ -1,2140 +1,2143 b''
1 1 # localrepo.py - read/write repository class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import hashlib
12 12 import inspect
13 13 import os
14 14 import random
15 15 import time
16 16 import weakref
17 17
18 18 from .i18n import _
19 19 from .node import (
20 20 hex,
21 21 nullid,
22 22 short,
23 23 )
24 24 from . import (
25 25 bookmarks,
26 26 branchmap,
27 27 bundle2,
28 28 changegroup,
29 29 changelog,
30 30 color,
31 31 context,
32 32 dirstate,
33 33 dirstateguard,
34 34 encoding,
35 35 error,
36 36 exchange,
37 37 extensions,
38 38 filelog,
39 39 hook,
40 40 lock as lockmod,
41 41 manifest,
42 42 match as matchmod,
43 43 merge as mergemod,
44 44 mergeutil,
45 45 namespaces,
46 46 obsolete,
47 47 pathutil,
48 48 peer,
49 49 phases,
50 50 pushkey,
51 51 pycompat,
52 52 repoview,
53 53 revset,
54 54 revsetlang,
55 55 scmutil,
56 sparse,
56 57 store,
57 58 subrepo,
58 59 tags as tagsmod,
59 60 transaction,
60 61 txnutil,
61 62 util,
62 63 vfs as vfsmod,
63 64 )
64 65
65 66 release = lockmod.release
66 67 urlerr = util.urlerr
67 68 urlreq = util.urlreq
68 69
69 70 # set of (path, vfs-location) tuples. vfs-location is:
70 71 # - 'plain for vfs relative paths
71 72 # - '' for svfs relative paths
72 73 _cachedfiles = set()
73 74
74 75 class _basefilecache(scmutil.filecache):
75 76 """All filecache usage on repo are done for logic that should be unfiltered
76 77 """
77 78 def __get__(self, repo, type=None):
78 79 if repo is None:
79 80 return self
80 81 return super(_basefilecache, self).__get__(repo.unfiltered(), type)
81 82 def __set__(self, repo, value):
82 83 return super(_basefilecache, self).__set__(repo.unfiltered(), value)
83 84 def __delete__(self, repo):
84 85 return super(_basefilecache, self).__delete__(repo.unfiltered())
85 86
86 87 class repofilecache(_basefilecache):
87 88 """filecache for files in .hg but outside of .hg/store"""
88 89 def __init__(self, *paths):
89 90 super(repofilecache, self).__init__(*paths)
90 91 for path in paths:
91 92 _cachedfiles.add((path, 'plain'))
92 93
93 94 def join(self, obj, fname):
94 95 return obj.vfs.join(fname)
95 96
96 97 class storecache(_basefilecache):
97 98 """filecache for files in the store"""
98 99 def __init__(self, *paths):
99 100 super(storecache, self).__init__(*paths)
100 101 for path in paths:
101 102 _cachedfiles.add((path, ''))
102 103
103 104 def join(self, obj, fname):
104 105 return obj.sjoin(fname)
105 106
106 107 class unfilteredpropertycache(util.propertycache):
107 108 """propertycache that apply to unfiltered repo only"""
108 109
109 110 def __get__(self, repo, type=None):
110 111 unfi = repo.unfiltered()
111 112 if unfi is repo:
112 113 return super(unfilteredpropertycache, self).__get__(unfi)
113 114 return getattr(unfi, self.name)
114 115
115 116 class filteredpropertycache(util.propertycache):
116 117 """propertycache that must take filtering in account"""
117 118
118 119 def cachevalue(self, obj, value):
119 120 object.__setattr__(obj, self.name, value)
120 121
121 122
122 123 def hasunfilteredcache(repo, name):
123 124 """check if a repo has an unfilteredpropertycache value for <name>"""
124 125 return name in vars(repo.unfiltered())
125 126
126 127 def unfilteredmethod(orig):
127 128 """decorate method that always need to be run on unfiltered version"""
128 129 def wrapper(repo, *args, **kwargs):
129 130 return orig(repo.unfiltered(), *args, **kwargs)
130 131 return wrapper
131 132
132 133 moderncaps = {'lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
133 134 'unbundle'}
134 135 legacycaps = moderncaps.union({'changegroupsubset'})
135 136
136 137 class localpeer(peer.peerrepository):
137 138 '''peer for a local repo; reflects only the most recent API'''
138 139
139 140 def __init__(self, repo, caps=None):
140 141 if caps is None:
141 142 caps = moderncaps.copy()
142 143 peer.peerrepository.__init__(self)
143 144 self._repo = repo.filtered('served')
144 145 self.ui = repo.ui
145 146 self._caps = repo._restrictcapabilities(caps)
146 147 self.requirements = repo.requirements
147 148 self.supportedformats = repo.supportedformats
148 149
149 150 def close(self):
150 151 self._repo.close()
151 152
152 153 def _capabilities(self):
153 154 return self._caps
154 155
155 156 def local(self):
156 157 return self._repo
157 158
158 159 def canpush(self):
159 160 return True
160 161
161 162 def url(self):
162 163 return self._repo.url()
163 164
164 165 def lookup(self, key):
165 166 return self._repo.lookup(key)
166 167
167 168 def branchmap(self):
168 169 return self._repo.branchmap()
169 170
170 171 def heads(self):
171 172 return self._repo.heads()
172 173
173 174 def known(self, nodes):
174 175 return self._repo.known(nodes)
175 176
176 177 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
177 178 **kwargs):
178 179 chunks = exchange.getbundlechunks(self._repo, source, heads=heads,
179 180 common=common, bundlecaps=bundlecaps,
180 181 **kwargs)
181 182 cb = util.chunkbuffer(chunks)
182 183
183 184 if exchange.bundle2requested(bundlecaps):
184 185 # When requesting a bundle2, getbundle returns a stream to make the
185 186 # wire level function happier. We need to build a proper object
186 187 # from it in local peer.
187 188 return bundle2.getunbundler(self.ui, cb)
188 189 else:
189 190 return changegroup.getunbundler('01', cb, None)
190 191
191 192 # TODO We might want to move the next two calls into legacypeer and add
192 193 # unbundle instead.
193 194
194 195 def unbundle(self, cg, heads, url):
195 196 """apply a bundle on a repo
196 197
197 198 This function handles the repo locking itself."""
198 199 try:
199 200 try:
200 201 cg = exchange.readbundle(self.ui, cg, None)
201 202 ret = exchange.unbundle(self._repo, cg, heads, 'push', url)
202 203 if util.safehasattr(ret, 'getchunks'):
203 204 # This is a bundle20 object, turn it into an unbundler.
204 205 # This little dance should be dropped eventually when the
205 206 # API is finally improved.
206 207 stream = util.chunkbuffer(ret.getchunks())
207 208 ret = bundle2.getunbundler(self.ui, stream)
208 209 return ret
209 210 except Exception as exc:
210 211 # If the exception contains output salvaged from a bundle2
211 212 # reply, we need to make sure it is printed before continuing
212 213 # to fail. So we build a bundle2 with such output and consume
213 214 # it directly.
214 215 #
215 216 # This is not very elegant but allows a "simple" solution for
216 217 # issue4594
217 218 output = getattr(exc, '_bundle2salvagedoutput', ())
218 219 if output:
219 220 bundler = bundle2.bundle20(self._repo.ui)
220 221 for out in output:
221 222 bundler.addpart(out)
222 223 stream = util.chunkbuffer(bundler.getchunks())
223 224 b = bundle2.getunbundler(self.ui, stream)
224 225 bundle2.processbundle(self._repo, b)
225 226 raise
226 227 except error.PushRaced as exc:
227 228 raise error.ResponseError(_('push failed:'), str(exc))
228 229
229 230 def lock(self):
230 231 return self._repo.lock()
231 232
232 233 def pushkey(self, namespace, key, old, new):
233 234 return self._repo.pushkey(namespace, key, old, new)
234 235
235 236 def listkeys(self, namespace):
236 237 return self._repo.listkeys(namespace)
237 238
238 239 def debugwireargs(self, one, two, three=None, four=None, five=None):
239 240 '''used to test argument passing over the wire'''
240 241 return "%s %s %s %s %s" % (one, two, three, four, five)
241 242
242 243 class locallegacypeer(localpeer):
243 244 '''peer extension which implements legacy methods too; used for tests with
244 245 restricted capabilities'''
245 246
246 247 def __init__(self, repo):
247 248 localpeer.__init__(self, repo, caps=legacycaps)
248 249
249 250 def branches(self, nodes):
250 251 return self._repo.branches(nodes)
251 252
252 253 def between(self, pairs):
253 254 return self._repo.between(pairs)
254 255
255 256 def changegroup(self, basenodes, source):
256 257 return changegroup.changegroup(self._repo, basenodes, source)
257 258
258 259 def changegroupsubset(self, bases, heads, source):
259 260 return changegroup.changegroupsubset(self._repo, bases, heads, source)
260 261
261 262 # Increment the sub-version when the revlog v2 format changes to lock out old
262 263 # clients.
263 264 REVLOGV2_REQUIREMENT = 'exp-revlogv2.0'
264 265
265 266 class localrepository(object):
266 267
267 268 supportedformats = {
268 269 'revlogv1',
269 270 'generaldelta',
270 271 'treemanifest',
271 272 'manifestv2',
272 273 REVLOGV2_REQUIREMENT,
273 274 }
274 275 _basesupported = supportedformats | {
275 276 'store',
276 277 'fncache',
277 278 'shared',
278 279 'relshared',
279 280 'dotencode',
280 281 }
281 282 openerreqs = {
282 283 'revlogv1',
283 284 'generaldelta',
284 285 'treemanifest',
285 286 'manifestv2',
286 287 }
287 288
288 289 # a list of (ui, featureset) functions.
289 290 # only functions defined in module of enabled extensions are invoked
290 291 featuresetupfuncs = set()
291 292
292 293 def __init__(self, baseui, path, create=False):
293 294 self.requirements = set()
294 295 self.filtername = None
295 296 # wvfs: rooted at the repository root, used to access the working copy
296 297 self.wvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
297 298 # vfs: rooted at .hg, used to access repo files outside of .hg/store
298 299 self.vfs = None
299 300 # svfs: usually rooted at .hg/store, used to access repository history
300 301 # If this is a shared repository, this vfs may point to another
301 302 # repository's .hg/store directory.
302 303 self.svfs = None
303 304 self.root = self.wvfs.base
304 305 self.path = self.wvfs.join(".hg")
305 306 self.origroot = path
306 307 # These auditor are not used by the vfs,
307 308 # only used when writing this comment: basectx.match
308 309 self.auditor = pathutil.pathauditor(self.root, self._checknested)
309 310 self.nofsauditor = pathutil.pathauditor(self.root, self._checknested,
310 311 realfs=False)
311 312 self.vfs = vfsmod.vfs(self.path)
312 313 self.baseui = baseui
313 314 self.ui = baseui.copy()
314 315 self.ui.copy = baseui.copy # prevent copying repo configuration
315 316 # A list of callback to shape the phase if no data were found.
316 317 # Callback are in the form: func(repo, roots) --> processed root.
317 318 # This list it to be filled by extension during repo setup
318 319 self._phasedefaults = []
319 320 try:
320 321 self.ui.readconfig(self.vfs.join("hgrc"), self.root)
321 322 self._loadextensions()
322 323 except IOError:
323 324 pass
324 325
325 326 if self.featuresetupfuncs:
326 327 self.supported = set(self._basesupported) # use private copy
327 328 extmods = set(m.__name__ for n, m
328 329 in extensions.extensions(self.ui))
329 330 for setupfunc in self.featuresetupfuncs:
330 331 if setupfunc.__module__ in extmods:
331 332 setupfunc(self.ui, self.supported)
332 333 else:
333 334 self.supported = self._basesupported
334 335 color.setup(self.ui)
335 336
336 337 # Add compression engines.
337 338 for name in util.compengines:
338 339 engine = util.compengines[name]
339 340 if engine.revlogheader():
340 341 self.supported.add('exp-compression-%s' % name)
341 342
342 343 if not self.vfs.isdir():
343 344 if create:
344 345 self.requirements = newreporequirements(self)
345 346
346 347 if not self.wvfs.exists():
347 348 self.wvfs.makedirs()
348 349 self.vfs.makedir(notindexed=True)
349 350
350 351 if 'store' in self.requirements:
351 352 self.vfs.mkdir("store")
352 353
353 354 # create an invalid changelog
354 355 self.vfs.append(
355 356 "00changelog.i",
356 357 '\0\0\0\2' # represents revlogv2
357 358 ' dummy changelog to prevent using the old repo layout'
358 359 )
359 360 else:
360 361 raise error.RepoError(_("repository %s not found") % path)
361 362 elif create:
362 363 raise error.RepoError(_("repository %s already exists") % path)
363 364 else:
364 365 try:
365 366 self.requirements = scmutil.readrequires(
366 367 self.vfs, self.supported)
367 368 except IOError as inst:
368 369 if inst.errno != errno.ENOENT:
369 370 raise
370 371
371 372 self.sharedpath = self.path
372 373 try:
373 374 sharedpath = self.vfs.read("sharedpath").rstrip('\n')
374 375 if 'relshared' in self.requirements:
375 376 sharedpath = self.vfs.join(sharedpath)
376 377 vfs = vfsmod.vfs(sharedpath, realpath=True)
377 378 s = vfs.base
378 379 if not vfs.exists():
379 380 raise error.RepoError(
380 381 _('.hg/sharedpath points to nonexistent directory %s') % s)
381 382 self.sharedpath = s
382 383 except IOError as inst:
383 384 if inst.errno != errno.ENOENT:
384 385 raise
385 386
386 387 self.store = store.store(
387 388 self.requirements, self.sharedpath, vfsmod.vfs)
388 389 self.spath = self.store.path
389 390 self.svfs = self.store.vfs
390 391 self.sjoin = self.store.join
391 392 self.vfs.createmode = self.store.createmode
392 393 self._applyopenerreqs()
393 394 if create:
394 395 self._writerequirements()
395 396
396 397 self._dirstatevalidatewarned = False
397 398
398 399 self._branchcaches = {}
399 400 self._revbranchcache = None
400 401 self.filterpats = {}
401 402 self._datafilters = {}
402 403 self._transref = self._lockref = self._wlockref = None
403 404
404 405 # A cache for various files under .hg/ that tracks file changes,
405 406 # (used by the filecache decorator)
406 407 #
407 408 # Maps a property name to its util.filecacheentry
408 409 self._filecache = {}
409 410
410 411 # hold sets of revision to be filtered
411 412 # should be cleared when something might have changed the filter value:
412 413 # - new changesets,
413 414 # - phase change,
414 415 # - new obsolescence marker,
415 416 # - working directory parent change,
416 417 # - bookmark changes
417 418 self.filteredrevcache = {}
418 419
419 420 # post-dirstate-status hooks
420 421 self._postdsstatus = []
421 422
422 423 # generic mapping between names and nodes
423 424 self.names = namespaces.namespaces()
424 425
425 426 # Key to signature value.
426 427 self._sparsesignaturecache = {}
427 428 # Signature to cached matcher instance.
428 429 self._sparsematchercache = {}
429 430
430 431 def close(self):
431 432 self._writecaches()
432 433
433 434 def _loadextensions(self):
434 435 extensions.loadall(self.ui)
435 436
436 437 def _writecaches(self):
437 438 if self._revbranchcache:
438 439 self._revbranchcache.write()
439 440
440 441 def _restrictcapabilities(self, caps):
441 442 if self.ui.configbool('experimental', 'bundle2-advertise', True):
442 443 caps = set(caps)
443 444 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self))
444 445 caps.add('bundle2=' + urlreq.quote(capsblob))
445 446 return caps
446 447
447 448 def _applyopenerreqs(self):
448 449 self.svfs.options = dict((r, 1) for r in self.requirements
449 450 if r in self.openerreqs)
450 451 # experimental config: format.chunkcachesize
451 452 chunkcachesize = self.ui.configint('format', 'chunkcachesize')
452 453 if chunkcachesize is not None:
453 454 self.svfs.options['chunkcachesize'] = chunkcachesize
454 455 # experimental config: format.maxchainlen
455 456 maxchainlen = self.ui.configint('format', 'maxchainlen')
456 457 if maxchainlen is not None:
457 458 self.svfs.options['maxchainlen'] = maxchainlen
458 459 # experimental config: format.manifestcachesize
459 460 manifestcachesize = self.ui.configint('format', 'manifestcachesize')
460 461 if manifestcachesize is not None:
461 462 self.svfs.options['manifestcachesize'] = manifestcachesize
462 463 # experimental config: format.aggressivemergedeltas
463 464 aggressivemergedeltas = self.ui.configbool('format',
464 465 'aggressivemergedeltas')
465 466 self.svfs.options['aggressivemergedeltas'] = aggressivemergedeltas
466 467 self.svfs.options['lazydeltabase'] = not scmutil.gddeltaconfig(self.ui)
467 468 chainspan = self.ui.configbytes('experimental', 'maxdeltachainspan', -1)
468 469 if 0 <= chainspan:
469 470 self.svfs.options['maxdeltachainspan'] = chainspan
470 471
471 472 for r in self.requirements:
472 473 if r.startswith('exp-compression-'):
473 474 self.svfs.options['compengine'] = r[len('exp-compression-'):]
474 475
475 476 # TODO move "revlogv2" to openerreqs once finalized.
476 477 if REVLOGV2_REQUIREMENT in self.requirements:
477 478 self.svfs.options['revlogv2'] = True
478 479
479 480 def _writerequirements(self):
480 481 scmutil.writerequires(self.vfs, self.requirements)
481 482
482 483 def _checknested(self, path):
483 484 """Determine if path is a legal nested repository."""
484 485 if not path.startswith(self.root):
485 486 return False
486 487 subpath = path[len(self.root) + 1:]
487 488 normsubpath = util.pconvert(subpath)
488 489
489 490 # XXX: Checking against the current working copy is wrong in
490 491 # the sense that it can reject things like
491 492 #
492 493 # $ hg cat -r 10 sub/x.txt
493 494 #
494 495 # if sub/ is no longer a subrepository in the working copy
495 496 # parent revision.
496 497 #
497 498 # However, it can of course also allow things that would have
498 499 # been rejected before, such as the above cat command if sub/
499 500 # is a subrepository now, but was a normal directory before.
500 501 # The old path auditor would have rejected by mistake since it
501 502 # panics when it sees sub/.hg/.
502 503 #
503 504 # All in all, checking against the working copy seems sensible
504 505 # since we want to prevent access to nested repositories on
505 506 # the filesystem *now*.
506 507 ctx = self[None]
507 508 parts = util.splitpath(subpath)
508 509 while parts:
509 510 prefix = '/'.join(parts)
510 511 if prefix in ctx.substate:
511 512 if prefix == normsubpath:
512 513 return True
513 514 else:
514 515 sub = ctx.sub(prefix)
515 516 return sub.checknested(subpath[len(prefix) + 1:])
516 517 else:
517 518 parts.pop()
518 519 return False
519 520
520 521 def peer(self):
521 522 return localpeer(self) # not cached to avoid reference cycle
522 523
523 524 def unfiltered(self):
524 525 """Return unfiltered version of the repository
525 526
526 527 Intended to be overwritten by filtered repo."""
527 528 return self
528 529
529 530 def filtered(self, name):
530 531 """Return a filtered version of a repository"""
531 532 # build a new class with the mixin and the current class
532 533 # (possibly subclass of the repo)
533 534 class filteredrepo(repoview.repoview, self.unfiltered().__class__):
534 535 pass
535 536 return filteredrepo(self, name)
536 537
537 538 @repofilecache('bookmarks', 'bookmarks.current')
538 539 def _bookmarks(self):
539 540 return bookmarks.bmstore(self)
540 541
541 542 @property
542 543 def _activebookmark(self):
543 544 return self._bookmarks.active
544 545
545 546 # _phaserevs and _phasesets depend on changelog. what we need is to
546 547 # call _phasecache.invalidate() if '00changelog.i' was changed, but it
547 548 # can't be easily expressed in filecache mechanism.
548 549 @storecache('phaseroots', '00changelog.i')
549 550 def _phasecache(self):
550 551 return phases.phasecache(self, self._phasedefaults)
551 552
552 553 @storecache('obsstore')
553 554 def obsstore(self):
554 555 return obsolete.makestore(self.ui, self)
555 556
556 557 @storecache('00changelog.i')
557 558 def changelog(self):
558 559 return changelog.changelog(self.svfs,
559 560 trypending=txnutil.mayhavepending(self.root))
560 561
561 562 def _constructmanifest(self):
562 563 # This is a temporary function while we migrate from manifest to
563 564 # manifestlog. It allows bundlerepo and unionrepo to intercept the
564 565 # manifest creation.
565 566 return manifest.manifestrevlog(self.svfs)
566 567
567 568 @storecache('00manifest.i')
568 569 def manifestlog(self):
569 570 return manifest.manifestlog(self.svfs, self)
570 571
571 572 @repofilecache('dirstate')
572 573 def dirstate(self):
574 sparsematchfn = lambda: sparse.matcher(self)
575
573 576 return dirstate.dirstate(self.vfs, self.ui, self.root,
574 self._dirstatevalidate)
577 self._dirstatevalidate, sparsematchfn)
575 578
576 579 def _dirstatevalidate(self, node):
577 580 try:
578 581 self.changelog.rev(node)
579 582 return node
580 583 except error.LookupError:
581 584 if not self._dirstatevalidatewarned:
582 585 self._dirstatevalidatewarned = True
583 586 self.ui.warn(_("warning: ignoring unknown"
584 587 " working parent %s!\n") % short(node))
585 588 return nullid
586 589
587 590 def __getitem__(self, changeid):
588 591 if changeid is None:
589 592 return context.workingctx(self)
590 593 if isinstance(changeid, slice):
591 594 # wdirrev isn't contiguous so the slice shouldn't include it
592 595 return [context.changectx(self, i)
593 596 for i in xrange(*changeid.indices(len(self)))
594 597 if i not in self.changelog.filteredrevs]
595 598 try:
596 599 return context.changectx(self, changeid)
597 600 except error.WdirUnsupported:
598 601 return context.workingctx(self)
599 602
600 603 def __contains__(self, changeid):
601 604 """True if the given changeid exists
602 605
603 606 error.LookupError is raised if an ambiguous node specified.
604 607 """
605 608 try:
606 609 self[changeid]
607 610 return True
608 611 except error.RepoLookupError:
609 612 return False
610 613
611 614 def __nonzero__(self):
612 615 return True
613 616
614 617 __bool__ = __nonzero__
615 618
616 619 def __len__(self):
617 620 return len(self.changelog)
618 621
619 622 def __iter__(self):
620 623 return iter(self.changelog)
621 624
622 625 def revs(self, expr, *args):
623 626 '''Find revisions matching a revset.
624 627
625 628 The revset is specified as a string ``expr`` that may contain
626 629 %-formatting to escape certain types. See ``revsetlang.formatspec``.
627 630
628 631 Revset aliases from the configuration are not expanded. To expand
629 632 user aliases, consider calling ``scmutil.revrange()`` or
630 633 ``repo.anyrevs([expr], user=True)``.
631 634
632 635 Returns a revset.abstractsmartset, which is a list-like interface
633 636 that contains integer revisions.
634 637 '''
635 638 expr = revsetlang.formatspec(expr, *args)
636 639 m = revset.match(None, expr)
637 640 return m(self)
638 641
639 642 def set(self, expr, *args):
640 643 '''Find revisions matching a revset and emit changectx instances.
641 644
642 645 This is a convenience wrapper around ``revs()`` that iterates the
643 646 result and is a generator of changectx instances.
644 647
645 648 Revset aliases from the configuration are not expanded. To expand
646 649 user aliases, consider calling ``scmutil.revrange()``.
647 650 '''
648 651 for r in self.revs(expr, *args):
649 652 yield self[r]
650 653
651 654 def anyrevs(self, specs, user=False, localalias=None):
652 655 '''Find revisions matching one of the given revsets.
653 656
654 657 Revset aliases from the configuration are not expanded by default. To
655 658 expand user aliases, specify ``user=True``. To provide some local
656 659 definitions overriding user aliases, set ``localalias`` to
657 660 ``{name: definitionstring}``.
658 661 '''
659 662 if user:
660 663 m = revset.matchany(self.ui, specs, repo=self,
661 664 localalias=localalias)
662 665 else:
663 666 m = revset.matchany(None, specs, localalias=localalias)
664 667 return m(self)
665 668
666 669 def url(self):
667 670 return 'file:' + self.root
668 671
669 672 def hook(self, name, throw=False, **args):
670 673 """Call a hook, passing this repo instance.
671 674
672 675 This a convenience method to aid invoking hooks. Extensions likely
673 676 won't call this unless they have registered a custom hook or are
674 677 replacing code that is expected to call a hook.
675 678 """
676 679 return hook.hook(self.ui, self, name, throw, **args)
677 680
678 681 @filteredpropertycache
679 682 def _tagscache(self):
680 683 '''Returns a tagscache object that contains various tags related
681 684 caches.'''
682 685
683 686 # This simplifies its cache management by having one decorated
684 687 # function (this one) and the rest simply fetch things from it.
685 688 class tagscache(object):
686 689 def __init__(self):
687 690 # These two define the set of tags for this repository. tags
688 691 # maps tag name to node; tagtypes maps tag name to 'global' or
689 692 # 'local'. (Global tags are defined by .hgtags across all
690 693 # heads, and local tags are defined in .hg/localtags.)
691 694 # They constitute the in-memory cache of tags.
692 695 self.tags = self.tagtypes = None
693 696
694 697 self.nodetagscache = self.tagslist = None
695 698
696 699 cache = tagscache()
697 700 cache.tags, cache.tagtypes = self._findtags()
698 701
699 702 return cache
700 703
701 704 def tags(self):
702 705 '''return a mapping of tag to node'''
703 706 t = {}
704 707 if self.changelog.filteredrevs:
705 708 tags, tt = self._findtags()
706 709 else:
707 710 tags = self._tagscache.tags
708 711 for k, v in tags.iteritems():
709 712 try:
710 713 # ignore tags to unknown nodes
711 714 self.changelog.rev(v)
712 715 t[k] = v
713 716 except (error.LookupError, ValueError):
714 717 pass
715 718 return t
716 719
717 720 def _findtags(self):
718 721 '''Do the hard work of finding tags. Return a pair of dicts
719 722 (tags, tagtypes) where tags maps tag name to node, and tagtypes
720 723 maps tag name to a string like \'global\' or \'local\'.
721 724 Subclasses or extensions are free to add their own tags, but
722 725 should be aware that the returned dicts will be retained for the
723 726 duration of the localrepo object.'''
724 727
725 728 # XXX what tagtype should subclasses/extensions use? Currently
726 729 # mq and bookmarks add tags, but do not set the tagtype at all.
727 730 # Should each extension invent its own tag type? Should there
728 731 # be one tagtype for all such "virtual" tags? Or is the status
729 732 # quo fine?
730 733
731 734
732 735 # map tag name to (node, hist)
733 736 alltags = tagsmod.findglobaltags(self.ui, self)
734 737 # map tag name to tag type
735 738 tagtypes = dict((tag, 'global') for tag in alltags)
736 739
737 740 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
738 741
739 742 # Build the return dicts. Have to re-encode tag names because
740 743 # the tags module always uses UTF-8 (in order not to lose info
741 744 # writing to the cache), but the rest of Mercurial wants them in
742 745 # local encoding.
743 746 tags = {}
744 747 for (name, (node, hist)) in alltags.iteritems():
745 748 if node != nullid:
746 749 tags[encoding.tolocal(name)] = node
747 750 tags['tip'] = self.changelog.tip()
748 751 tagtypes = dict([(encoding.tolocal(name), value)
749 752 for (name, value) in tagtypes.iteritems()])
750 753 return (tags, tagtypes)
751 754
752 755 def tagtype(self, tagname):
753 756 '''
754 757 return the type of the given tag. result can be:
755 758
756 759 'local' : a local tag
757 760 'global' : a global tag
758 761 None : tag does not exist
759 762 '''
760 763
761 764 return self._tagscache.tagtypes.get(tagname)
762 765
763 766 def tagslist(self):
764 767 '''return a list of tags ordered by revision'''
765 768 if not self._tagscache.tagslist:
766 769 l = []
767 770 for t, n in self.tags().iteritems():
768 771 l.append((self.changelog.rev(n), t, n))
769 772 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
770 773
771 774 return self._tagscache.tagslist
772 775
773 776 def nodetags(self, node):
774 777 '''return the tags associated with a node'''
775 778 if not self._tagscache.nodetagscache:
776 779 nodetagscache = {}
777 780 for t, n in self._tagscache.tags.iteritems():
778 781 nodetagscache.setdefault(n, []).append(t)
779 782 for tags in nodetagscache.itervalues():
780 783 tags.sort()
781 784 self._tagscache.nodetagscache = nodetagscache
782 785 return self._tagscache.nodetagscache.get(node, [])
783 786
784 787 def nodebookmarks(self, node):
785 788 """return the list of bookmarks pointing to the specified node"""
786 789 marks = []
787 790 for bookmark, n in self._bookmarks.iteritems():
788 791 if n == node:
789 792 marks.append(bookmark)
790 793 return sorted(marks)
791 794
792 795 def branchmap(self):
793 796 '''returns a dictionary {branch: [branchheads]} with branchheads
794 797 ordered by increasing revision number'''
795 798 branchmap.updatecache(self)
796 799 return self._branchcaches[self.filtername]
797 800
798 801 @unfilteredmethod
799 802 def revbranchcache(self):
800 803 if not self._revbranchcache:
801 804 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
802 805 return self._revbranchcache
803 806
804 807 def branchtip(self, branch, ignoremissing=False):
805 808 '''return the tip node for a given branch
806 809
807 810 If ignoremissing is True, then this method will not raise an error.
808 811 This is helpful for callers that only expect None for a missing branch
809 812 (e.g. namespace).
810 813
811 814 '''
812 815 try:
813 816 return self.branchmap().branchtip(branch)
814 817 except KeyError:
815 818 if not ignoremissing:
816 819 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
817 820 else:
818 821 pass
819 822
820 823 def lookup(self, key):
821 824 return self[key].node()
822 825
823 826 def lookupbranch(self, key, remote=None):
824 827 repo = remote or self
825 828 if key in repo.branchmap():
826 829 return key
827 830
828 831 repo = (remote and remote.local()) and remote or self
829 832 return repo[key].branch()
830 833
831 834 def known(self, nodes):
832 835 cl = self.changelog
833 836 nm = cl.nodemap
834 837 filtered = cl.filteredrevs
835 838 result = []
836 839 for n in nodes:
837 840 r = nm.get(n)
838 841 resp = not (r is None or r in filtered)
839 842 result.append(resp)
840 843 return result
841 844
842 845 def local(self):
843 846 return self
844 847
845 848 def publishing(self):
846 849 # it's safe (and desirable) to trust the publish flag unconditionally
847 850 # so that we don't finalize changes shared between users via ssh or nfs
848 851 return self.ui.configbool('phases', 'publish', True, untrusted=True)
849 852
850 853 def cancopy(self):
851 854 # so statichttprepo's override of local() works
852 855 if not self.local():
853 856 return False
854 857 if not self.publishing():
855 858 return True
856 859 # if publishing we can't copy if there is filtered content
857 860 return not self.filtered('visible').changelog.filteredrevs
858 861
859 862 def shared(self):
860 863 '''the type of shared repository (None if not shared)'''
861 864 if self.sharedpath != self.path:
862 865 return 'store'
863 866 return None
864 867
865 868 def wjoin(self, f, *insidef):
866 869 return self.vfs.reljoin(self.root, f, *insidef)
867 870
868 871 def file(self, f):
869 872 if f[0] == '/':
870 873 f = f[1:]
871 874 return filelog.filelog(self.svfs, f)
872 875
873 876 def changectx(self, changeid):
874 877 return self[changeid]
875 878
876 879 def setparents(self, p1, p2=nullid):
877 880 with self.dirstate.parentchange():
878 881 copies = self.dirstate.setparents(p1, p2)
879 882 pctx = self[p1]
880 883 if copies:
881 884 # Adjust copy records, the dirstate cannot do it, it
882 885 # requires access to parents manifests. Preserve them
883 886 # only for entries added to first parent.
884 887 for f in copies:
885 888 if f not in pctx and copies[f] in pctx:
886 889 self.dirstate.copy(copies[f], f)
887 890 if p2 == nullid:
888 891 for f, s in sorted(self.dirstate.copies().items()):
889 892 if f not in pctx and s not in pctx:
890 893 self.dirstate.copy(None, f)
891 894
892 895 def filectx(self, path, changeid=None, fileid=None):
893 896 """changeid can be a changeset revision, node, or tag.
894 897 fileid can be a file revision or node."""
895 898 return context.filectx(self, path, changeid, fileid)
896 899
897 900 def getcwd(self):
898 901 return self.dirstate.getcwd()
899 902
900 903 def pathto(self, f, cwd=None):
901 904 return self.dirstate.pathto(f, cwd)
902 905
903 906 def _loadfilter(self, filter):
904 907 if filter not in self.filterpats:
905 908 l = []
906 909 for pat, cmd in self.ui.configitems(filter):
907 910 if cmd == '!':
908 911 continue
909 912 mf = matchmod.match(self.root, '', [pat])
910 913 fn = None
911 914 params = cmd
912 915 for name, filterfn in self._datafilters.iteritems():
913 916 if cmd.startswith(name):
914 917 fn = filterfn
915 918 params = cmd[len(name):].lstrip()
916 919 break
917 920 if not fn:
918 921 fn = lambda s, c, **kwargs: util.filter(s, c)
919 922 # Wrap old filters not supporting keyword arguments
920 923 if not inspect.getargspec(fn)[2]:
921 924 oldfn = fn
922 925 fn = lambda s, c, **kwargs: oldfn(s, c)
923 926 l.append((mf, fn, params))
924 927 self.filterpats[filter] = l
925 928 return self.filterpats[filter]
926 929
927 930 def _filter(self, filterpats, filename, data):
928 931 for mf, fn, cmd in filterpats:
929 932 if mf(filename):
930 933 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
931 934 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
932 935 break
933 936
934 937 return data
935 938
936 939 @unfilteredpropertycache
937 940 def _encodefilterpats(self):
938 941 return self._loadfilter('encode')
939 942
940 943 @unfilteredpropertycache
941 944 def _decodefilterpats(self):
942 945 return self._loadfilter('decode')
943 946
944 947 def adddatafilter(self, name, filter):
945 948 self._datafilters[name] = filter
946 949
947 950 def wread(self, filename):
948 951 if self.wvfs.islink(filename):
949 952 data = self.wvfs.readlink(filename)
950 953 else:
951 954 data = self.wvfs.read(filename)
952 955 return self._filter(self._encodefilterpats, filename, data)
953 956
954 957 def wwrite(self, filename, data, flags, backgroundclose=False):
955 958 """write ``data`` into ``filename`` in the working directory
956 959
957 960 This returns length of written (maybe decoded) data.
958 961 """
959 962 data = self._filter(self._decodefilterpats, filename, data)
960 963 if 'l' in flags:
961 964 self.wvfs.symlink(data, filename)
962 965 else:
963 966 self.wvfs.write(filename, data, backgroundclose=backgroundclose)
964 967 if 'x' in flags:
965 968 self.wvfs.setflags(filename, False, True)
966 969 return len(data)
967 970
968 971 def wwritedata(self, filename, data):
969 972 return self._filter(self._decodefilterpats, filename, data)
970 973
971 974 def currenttransaction(self):
972 975 """return the current transaction or None if non exists"""
973 976 if self._transref:
974 977 tr = self._transref()
975 978 else:
976 979 tr = None
977 980
978 981 if tr and tr.running():
979 982 return tr
980 983 return None
981 984
982 985 def transaction(self, desc, report=None):
983 986 if (self.ui.configbool('devel', 'all-warnings')
984 987 or self.ui.configbool('devel', 'check-locks')):
985 988 if self._currentlock(self._lockref) is None:
986 989 raise error.ProgrammingError('transaction requires locking')
987 990 tr = self.currenttransaction()
988 991 if tr is not None:
989 992 return tr.nest()
990 993
991 994 # abort here if the journal already exists
992 995 if self.svfs.exists("journal"):
993 996 raise error.RepoError(
994 997 _("abandoned transaction found"),
995 998 hint=_("run 'hg recover' to clean up transaction"))
996 999
997 1000 idbase = "%.40f#%f" % (random.random(), time.time())
998 1001 ha = hex(hashlib.sha1(idbase).digest())
999 1002 txnid = 'TXN:' + ha
1000 1003 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1001 1004
1002 1005 self._writejournal(desc)
1003 1006 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1004 1007 if report:
1005 1008 rp = report
1006 1009 else:
1007 1010 rp = self.ui.warn
1008 1011 vfsmap = {'plain': self.vfs} # root of .hg/
1009 1012 # we must avoid cyclic reference between repo and transaction.
1010 1013 reporef = weakref.ref(self)
1011 1014 # Code to track tag movement
1012 1015 #
1013 1016 # Since tags are all handled as file content, it is actually quite hard
1014 1017 # to track these movement from a code perspective. So we fallback to a
1015 1018 # tracking at the repository level. One could envision to track changes
1016 1019 # to the '.hgtags' file through changegroup apply but that fails to
1017 1020 # cope with case where transaction expose new heads without changegroup
1018 1021 # being involved (eg: phase movement).
1019 1022 #
1020 1023 # For now, We gate the feature behind a flag since this likely comes
1021 1024 # with performance impacts. The current code run more often than needed
1022 1025 # and do not use caches as much as it could. The current focus is on
1023 1026 # the behavior of the feature so we disable it by default. The flag
1024 1027 # will be removed when we are happy with the performance impact.
1025 1028 #
1026 1029 # Once this feature is no longer experimental move the following
1027 1030 # documentation to the appropriate help section:
1028 1031 #
1029 1032 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
1030 1033 # tags (new or changed or deleted tags). In addition the details of
1031 1034 # these changes are made available in a file at:
1032 1035 # ``REPOROOT/.hg/changes/tags.changes``.
1033 1036 # Make sure you check for HG_TAG_MOVED before reading that file as it
1034 1037 # might exist from a previous transaction even if no tag were touched
1035 1038 # in this one. Changes are recorded in a line base format::
1036 1039 #
1037 1040 # <action> <hex-node> <tag-name>\n
1038 1041 #
1039 1042 # Actions are defined as follow:
1040 1043 # "-R": tag is removed,
1041 1044 # "+A": tag is added,
1042 1045 # "-M": tag is moved (old value),
1043 1046 # "+M": tag is moved (new value),
1044 1047 tracktags = lambda x: None
1045 1048 # experimental config: experimental.hook-track-tags
1046 1049 shouldtracktags = self.ui.configbool('experimental', 'hook-track-tags',
1047 1050 False)
1048 1051 if desc != 'strip' and shouldtracktags:
1049 1052 oldheads = self.changelog.headrevs()
1050 1053 def tracktags(tr2):
1051 1054 repo = reporef()
1052 1055 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
1053 1056 newheads = repo.changelog.headrevs()
1054 1057 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
1055 1058 # notes: we compare lists here.
1056 1059 # As we do it only once buiding set would not be cheaper
1057 1060 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
1058 1061 if changes:
1059 1062 tr2.hookargs['tag_moved'] = '1'
1060 1063 with repo.vfs('changes/tags.changes', 'w',
1061 1064 atomictemp=True) as changesfile:
1062 1065 # note: we do not register the file to the transaction
1063 1066 # because we needs it to still exist on the transaction
1064 1067 # is close (for txnclose hooks)
1065 1068 tagsmod.writediff(changesfile, changes)
1066 1069 def validate(tr2):
1067 1070 """will run pre-closing hooks"""
1068 1071 # XXX the transaction API is a bit lacking here so we take a hacky
1069 1072 # path for now
1070 1073 #
1071 1074 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
1072 1075 # dict is copied before these run. In addition we needs the data
1073 1076 # available to in memory hooks too.
1074 1077 #
1075 1078 # Moreover, we also need to make sure this runs before txnclose
1076 1079 # hooks and there is no "pending" mechanism that would execute
1077 1080 # logic only if hooks are about to run.
1078 1081 #
1079 1082 # Fixing this limitation of the transaction is also needed to track
1080 1083 # other families of changes (bookmarks, phases, obsolescence).
1081 1084 #
1082 1085 # This will have to be fixed before we remove the experimental
1083 1086 # gating.
1084 1087 tracktags(tr2)
1085 1088 reporef().hook('pretxnclose', throw=True,
1086 1089 txnname=desc, **pycompat.strkwargs(tr.hookargs))
1087 1090 def releasefn(tr, success):
1088 1091 repo = reporef()
1089 1092 if success:
1090 1093 # this should be explicitly invoked here, because
1091 1094 # in-memory changes aren't written out at closing
1092 1095 # transaction, if tr.addfilegenerator (via
1093 1096 # dirstate.write or so) isn't invoked while
1094 1097 # transaction running
1095 1098 repo.dirstate.write(None)
1096 1099 else:
1097 1100 # discard all changes (including ones already written
1098 1101 # out) in this transaction
1099 1102 repo.dirstate.restorebackup(None, prefix='journal.')
1100 1103
1101 1104 repo.invalidate(clearfilecache=True)
1102 1105
1103 1106 tr = transaction.transaction(rp, self.svfs, vfsmap,
1104 1107 "journal",
1105 1108 "undo",
1106 1109 aftertrans(renames),
1107 1110 self.store.createmode,
1108 1111 validator=validate,
1109 1112 releasefn=releasefn,
1110 1113 checkambigfiles=_cachedfiles)
1111 1114 tr.changes['revs'] = set()
1112 1115 tr.changes['obsmarkers'] = set()
1113 1116
1114 1117 tr.hookargs['txnid'] = txnid
1115 1118 # note: writing the fncache only during finalize mean that the file is
1116 1119 # outdated when running hooks. As fncache is used for streaming clone,
1117 1120 # this is not expected to break anything that happen during the hooks.
1118 1121 tr.addfinalize('flush-fncache', self.store.write)
1119 1122 def txnclosehook(tr2):
1120 1123 """To be run if transaction is successful, will schedule a hook run
1121 1124 """
1122 1125 # Don't reference tr2 in hook() so we don't hold a reference.
1123 1126 # This reduces memory consumption when there are multiple
1124 1127 # transactions per lock. This can likely go away if issue5045
1125 1128 # fixes the function accumulation.
1126 1129 hookargs = tr2.hookargs
1127 1130
1128 1131 def hook():
1129 1132 reporef().hook('txnclose', throw=False, txnname=desc,
1130 1133 **pycompat.strkwargs(hookargs))
1131 1134 reporef()._afterlock(hook)
1132 1135 tr.addfinalize('txnclose-hook', txnclosehook)
1133 1136 tr.addpostclose('warms-cache', self._buildcacheupdater(tr))
1134 1137 def txnaborthook(tr2):
1135 1138 """To be run if transaction is aborted
1136 1139 """
1137 1140 reporef().hook('txnabort', throw=False, txnname=desc,
1138 1141 **tr2.hookargs)
1139 1142 tr.addabort('txnabort-hook', txnaborthook)
1140 1143 # avoid eager cache invalidation. in-memory data should be identical
1141 1144 # to stored data if transaction has no error.
1142 1145 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
1143 1146 self._transref = weakref.ref(tr)
1144 1147 return tr
1145 1148
1146 1149 def _journalfiles(self):
1147 1150 return ((self.svfs, 'journal'),
1148 1151 (self.vfs, 'journal.dirstate'),
1149 1152 (self.vfs, 'journal.branch'),
1150 1153 (self.vfs, 'journal.desc'),
1151 1154 (self.vfs, 'journal.bookmarks'),
1152 1155 (self.svfs, 'journal.phaseroots'))
1153 1156
1154 1157 def undofiles(self):
1155 1158 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
1156 1159
1157 1160 @unfilteredmethod
1158 1161 def _writejournal(self, desc):
1159 1162 self.dirstate.savebackup(None, prefix='journal.')
1160 1163 self.vfs.write("journal.branch",
1161 1164 encoding.fromlocal(self.dirstate.branch()))
1162 1165 self.vfs.write("journal.desc",
1163 1166 "%d\n%s\n" % (len(self), desc))
1164 1167 self.vfs.write("journal.bookmarks",
1165 1168 self.vfs.tryread("bookmarks"))
1166 1169 self.svfs.write("journal.phaseroots",
1167 1170 self.svfs.tryread("phaseroots"))
1168 1171
1169 1172 def recover(self):
1170 1173 with self.lock():
1171 1174 if self.svfs.exists("journal"):
1172 1175 self.ui.status(_("rolling back interrupted transaction\n"))
1173 1176 vfsmap = {'': self.svfs,
1174 1177 'plain': self.vfs,}
1175 1178 transaction.rollback(self.svfs, vfsmap, "journal",
1176 1179 self.ui.warn,
1177 1180 checkambigfiles=_cachedfiles)
1178 1181 self.invalidate()
1179 1182 return True
1180 1183 else:
1181 1184 self.ui.warn(_("no interrupted transaction available\n"))
1182 1185 return False
1183 1186
1184 1187 def rollback(self, dryrun=False, force=False):
1185 1188 wlock = lock = dsguard = None
1186 1189 try:
1187 1190 wlock = self.wlock()
1188 1191 lock = self.lock()
1189 1192 if self.svfs.exists("undo"):
1190 1193 dsguard = dirstateguard.dirstateguard(self, 'rollback')
1191 1194
1192 1195 return self._rollback(dryrun, force, dsguard)
1193 1196 else:
1194 1197 self.ui.warn(_("no rollback information available\n"))
1195 1198 return 1
1196 1199 finally:
1197 1200 release(dsguard, lock, wlock)
1198 1201
1199 1202 @unfilteredmethod # Until we get smarter cache management
1200 1203 def _rollback(self, dryrun, force, dsguard):
1201 1204 ui = self.ui
1202 1205 try:
1203 1206 args = self.vfs.read('undo.desc').splitlines()
1204 1207 (oldlen, desc, detail) = (int(args[0]), args[1], None)
1205 1208 if len(args) >= 3:
1206 1209 detail = args[2]
1207 1210 oldtip = oldlen - 1
1208 1211
1209 1212 if detail and ui.verbose:
1210 1213 msg = (_('repository tip rolled back to revision %d'
1211 1214 ' (undo %s: %s)\n')
1212 1215 % (oldtip, desc, detail))
1213 1216 else:
1214 1217 msg = (_('repository tip rolled back to revision %d'
1215 1218 ' (undo %s)\n')
1216 1219 % (oldtip, desc))
1217 1220 except IOError:
1218 1221 msg = _('rolling back unknown transaction\n')
1219 1222 desc = None
1220 1223
1221 1224 if not force and self['.'] != self['tip'] and desc == 'commit':
1222 1225 raise error.Abort(
1223 1226 _('rollback of last commit while not checked out '
1224 1227 'may lose data'), hint=_('use -f to force'))
1225 1228
1226 1229 ui.status(msg)
1227 1230 if dryrun:
1228 1231 return 0
1229 1232
1230 1233 parents = self.dirstate.parents()
1231 1234 self.destroying()
1232 1235 vfsmap = {'plain': self.vfs, '': self.svfs}
1233 1236 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn,
1234 1237 checkambigfiles=_cachedfiles)
1235 1238 if self.vfs.exists('undo.bookmarks'):
1236 1239 self.vfs.rename('undo.bookmarks', 'bookmarks', checkambig=True)
1237 1240 if self.svfs.exists('undo.phaseroots'):
1238 1241 self.svfs.rename('undo.phaseroots', 'phaseroots', checkambig=True)
1239 1242 self.invalidate()
1240 1243
1241 1244 parentgone = (parents[0] not in self.changelog.nodemap or
1242 1245 parents[1] not in self.changelog.nodemap)
1243 1246 if parentgone:
1244 1247 # prevent dirstateguard from overwriting already restored one
1245 1248 dsguard.close()
1246 1249
1247 1250 self.dirstate.restorebackup(None, prefix='undo.')
1248 1251 try:
1249 1252 branch = self.vfs.read('undo.branch')
1250 1253 self.dirstate.setbranch(encoding.tolocal(branch))
1251 1254 except IOError:
1252 1255 ui.warn(_('named branch could not be reset: '
1253 1256 'current branch is still \'%s\'\n')
1254 1257 % self.dirstate.branch())
1255 1258
1256 1259 parents = tuple([p.rev() for p in self[None].parents()])
1257 1260 if len(parents) > 1:
1258 1261 ui.status(_('working directory now based on '
1259 1262 'revisions %d and %d\n') % parents)
1260 1263 else:
1261 1264 ui.status(_('working directory now based on '
1262 1265 'revision %d\n') % parents)
1263 1266 mergemod.mergestate.clean(self, self['.'].node())
1264 1267
1265 1268 # TODO: if we know which new heads may result from this rollback, pass
1266 1269 # them to destroy(), which will prevent the branchhead cache from being
1267 1270 # invalidated.
1268 1271 self.destroyed()
1269 1272 return 0
1270 1273
1271 1274 def _buildcacheupdater(self, newtransaction):
1272 1275 """called during transaction to build the callback updating cache
1273 1276
1274 1277 Lives on the repository to help extension who might want to augment
1275 1278 this logic. For this purpose, the created transaction is passed to the
1276 1279 method.
1277 1280 """
1278 1281 # we must avoid cyclic reference between repo and transaction.
1279 1282 reporef = weakref.ref(self)
1280 1283 def updater(tr):
1281 1284 repo = reporef()
1282 1285 repo.updatecaches(tr)
1283 1286 return updater
1284 1287
1285 1288 @unfilteredmethod
1286 1289 def updatecaches(self, tr=None):
1287 1290 """warm appropriate caches
1288 1291
1289 1292 If this function is called after a transaction closed. The transaction
1290 1293 will be available in the 'tr' argument. This can be used to selectively
1291 1294 update caches relevant to the changes in that transaction.
1292 1295 """
1293 1296 if tr is not None and tr.hookargs.get('source') == 'strip':
1294 1297 # During strip, many caches are invalid but
1295 1298 # later call to `destroyed` will refresh them.
1296 1299 return
1297 1300
1298 1301 if tr is None or tr.changes['revs']:
1299 1302 # updating the unfiltered branchmap should refresh all the others,
1300 1303 self.ui.debug('updating the branch cache\n')
1301 1304 branchmap.updatecache(self.filtered('served'))
1302 1305
1303 1306 def invalidatecaches(self):
1304 1307
1305 1308 if '_tagscache' in vars(self):
1306 1309 # can't use delattr on proxy
1307 1310 del self.__dict__['_tagscache']
1308 1311
1309 1312 self.unfiltered()._branchcaches.clear()
1310 1313 self.invalidatevolatilesets()
1311 1314 self._sparsesignaturecache.clear()
1312 1315
1313 1316 def invalidatevolatilesets(self):
1314 1317 self.filteredrevcache.clear()
1315 1318 obsolete.clearobscaches(self)
1316 1319
1317 1320 def invalidatedirstate(self):
1318 1321 '''Invalidates the dirstate, causing the next call to dirstate
1319 1322 to check if it was modified since the last time it was read,
1320 1323 rereading it if it has.
1321 1324
1322 1325 This is different to dirstate.invalidate() that it doesn't always
1323 1326 rereads the dirstate. Use dirstate.invalidate() if you want to
1324 1327 explicitly read the dirstate again (i.e. restoring it to a previous
1325 1328 known good state).'''
1326 1329 if hasunfilteredcache(self, 'dirstate'):
1327 1330 for k in self.dirstate._filecache:
1328 1331 try:
1329 1332 delattr(self.dirstate, k)
1330 1333 except AttributeError:
1331 1334 pass
1332 1335 delattr(self.unfiltered(), 'dirstate')
1333 1336
1334 1337 def invalidate(self, clearfilecache=False):
1335 1338 '''Invalidates both store and non-store parts other than dirstate
1336 1339
1337 1340 If a transaction is running, invalidation of store is omitted,
1338 1341 because discarding in-memory changes might cause inconsistency
1339 1342 (e.g. incomplete fncache causes unintentional failure, but
1340 1343 redundant one doesn't).
1341 1344 '''
1342 1345 unfiltered = self.unfiltered() # all file caches are stored unfiltered
1343 1346 for k in list(self._filecache.keys()):
1344 1347 # dirstate is invalidated separately in invalidatedirstate()
1345 1348 if k == 'dirstate':
1346 1349 continue
1347 1350
1348 1351 if clearfilecache:
1349 1352 del self._filecache[k]
1350 1353 try:
1351 1354 delattr(unfiltered, k)
1352 1355 except AttributeError:
1353 1356 pass
1354 1357 self.invalidatecaches()
1355 1358 if not self.currenttransaction():
1356 1359 # TODO: Changing contents of store outside transaction
1357 1360 # causes inconsistency. We should make in-memory store
1358 1361 # changes detectable, and abort if changed.
1359 1362 self.store.invalidatecaches()
1360 1363
1361 1364 def invalidateall(self):
1362 1365 '''Fully invalidates both store and non-store parts, causing the
1363 1366 subsequent operation to reread any outside changes.'''
1364 1367 # extension should hook this to invalidate its caches
1365 1368 self.invalidate()
1366 1369 self.invalidatedirstate()
1367 1370
1368 1371 @unfilteredmethod
1369 1372 def _refreshfilecachestats(self, tr):
1370 1373 """Reload stats of cached files so that they are flagged as valid"""
1371 1374 for k, ce in self._filecache.items():
1372 1375 if k == 'dirstate' or k not in self.__dict__:
1373 1376 continue
1374 1377 ce.refresh()
1375 1378
1376 1379 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
1377 1380 inheritchecker=None, parentenvvar=None):
1378 1381 parentlock = None
1379 1382 # the contents of parentenvvar are used by the underlying lock to
1380 1383 # determine whether it can be inherited
1381 1384 if parentenvvar is not None:
1382 1385 parentlock = encoding.environ.get(parentenvvar)
1383 1386 try:
1384 1387 l = lockmod.lock(vfs, lockname, 0, releasefn=releasefn,
1385 1388 acquirefn=acquirefn, desc=desc,
1386 1389 inheritchecker=inheritchecker,
1387 1390 parentlock=parentlock)
1388 1391 except error.LockHeld as inst:
1389 1392 if not wait:
1390 1393 raise
1391 1394 # show more details for new-style locks
1392 1395 if ':' in inst.locker:
1393 1396 host, pid = inst.locker.split(":", 1)
1394 1397 self.ui.warn(
1395 1398 _("waiting for lock on %s held by process %r "
1396 1399 "on host %r\n") % (desc, pid, host))
1397 1400 else:
1398 1401 self.ui.warn(_("waiting for lock on %s held by %r\n") %
1399 1402 (desc, inst.locker))
1400 1403 # default to 600 seconds timeout
1401 1404 l = lockmod.lock(vfs, lockname,
1402 1405 int(self.ui.config("ui", "timeout", "600")),
1403 1406 releasefn=releasefn, acquirefn=acquirefn,
1404 1407 desc=desc)
1405 1408 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
1406 1409 return l
1407 1410
1408 1411 def _afterlock(self, callback):
1409 1412 """add a callback to be run when the repository is fully unlocked
1410 1413
1411 1414 The callback will be executed when the outermost lock is released
1412 1415 (with wlock being higher level than 'lock')."""
1413 1416 for ref in (self._wlockref, self._lockref):
1414 1417 l = ref and ref()
1415 1418 if l and l.held:
1416 1419 l.postrelease.append(callback)
1417 1420 break
1418 1421 else: # no lock have been found.
1419 1422 callback()
1420 1423
1421 1424 def lock(self, wait=True):
1422 1425 '''Lock the repository store (.hg/store) and return a weak reference
1423 1426 to the lock. Use this before modifying the store (e.g. committing or
1424 1427 stripping). If you are opening a transaction, get a lock as well.)
1425 1428
1426 1429 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1427 1430 'wlock' first to avoid a dead-lock hazard.'''
1428 1431 l = self._currentlock(self._lockref)
1429 1432 if l is not None:
1430 1433 l.lock()
1431 1434 return l
1432 1435
1433 1436 l = self._lock(self.svfs, "lock", wait, None,
1434 1437 self.invalidate, _('repository %s') % self.origroot)
1435 1438 self._lockref = weakref.ref(l)
1436 1439 return l
1437 1440
1438 1441 def _wlockchecktransaction(self):
1439 1442 if self.currenttransaction() is not None:
1440 1443 raise error.LockInheritanceContractViolation(
1441 1444 'wlock cannot be inherited in the middle of a transaction')
1442 1445
1443 1446 def wlock(self, wait=True):
1444 1447 '''Lock the non-store parts of the repository (everything under
1445 1448 .hg except .hg/store) and return a weak reference to the lock.
1446 1449
1447 1450 Use this before modifying files in .hg.
1448 1451
1449 1452 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1450 1453 'wlock' first to avoid a dead-lock hazard.'''
1451 1454 l = self._wlockref and self._wlockref()
1452 1455 if l is not None and l.held:
1453 1456 l.lock()
1454 1457 return l
1455 1458
1456 1459 # We do not need to check for non-waiting lock acquisition. Such
1457 1460 # acquisition would not cause dead-lock as they would just fail.
1458 1461 if wait and (self.ui.configbool('devel', 'all-warnings')
1459 1462 or self.ui.configbool('devel', 'check-locks')):
1460 1463 if self._currentlock(self._lockref) is not None:
1461 1464 self.ui.develwarn('"wlock" acquired after "lock"')
1462 1465
1463 1466 def unlock():
1464 1467 if self.dirstate.pendingparentchange():
1465 1468 self.dirstate.invalidate()
1466 1469 else:
1467 1470 self.dirstate.write(None)
1468 1471
1469 1472 self._filecache['dirstate'].refresh()
1470 1473
1471 1474 l = self._lock(self.vfs, "wlock", wait, unlock,
1472 1475 self.invalidatedirstate, _('working directory of %s') %
1473 1476 self.origroot,
1474 1477 inheritchecker=self._wlockchecktransaction,
1475 1478 parentenvvar='HG_WLOCK_LOCKER')
1476 1479 self._wlockref = weakref.ref(l)
1477 1480 return l
1478 1481
1479 1482 def _currentlock(self, lockref):
1480 1483 """Returns the lock if it's held, or None if it's not."""
1481 1484 if lockref is None:
1482 1485 return None
1483 1486 l = lockref()
1484 1487 if l is None or not l.held:
1485 1488 return None
1486 1489 return l
1487 1490
1488 1491 def currentwlock(self):
1489 1492 """Returns the wlock if it's held, or None if it's not."""
1490 1493 return self._currentlock(self._wlockref)
1491 1494
1492 1495 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
1493 1496 """
1494 1497 commit an individual file as part of a larger transaction
1495 1498 """
1496 1499
1497 1500 fname = fctx.path()
1498 1501 fparent1 = manifest1.get(fname, nullid)
1499 1502 fparent2 = manifest2.get(fname, nullid)
1500 1503 if isinstance(fctx, context.filectx):
1501 1504 node = fctx.filenode()
1502 1505 if node in [fparent1, fparent2]:
1503 1506 self.ui.debug('reusing %s filelog entry\n' % fname)
1504 1507 if manifest1.flags(fname) != fctx.flags():
1505 1508 changelist.append(fname)
1506 1509 return node
1507 1510
1508 1511 flog = self.file(fname)
1509 1512 meta = {}
1510 1513 copy = fctx.renamed()
1511 1514 if copy and copy[0] != fname:
1512 1515 # Mark the new revision of this file as a copy of another
1513 1516 # file. This copy data will effectively act as a parent
1514 1517 # of this new revision. If this is a merge, the first
1515 1518 # parent will be the nullid (meaning "look up the copy data")
1516 1519 # and the second one will be the other parent. For example:
1517 1520 #
1518 1521 # 0 --- 1 --- 3 rev1 changes file foo
1519 1522 # \ / rev2 renames foo to bar and changes it
1520 1523 # \- 2 -/ rev3 should have bar with all changes and
1521 1524 # should record that bar descends from
1522 1525 # bar in rev2 and foo in rev1
1523 1526 #
1524 1527 # this allows this merge to succeed:
1525 1528 #
1526 1529 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
1527 1530 # \ / merging rev3 and rev4 should use bar@rev2
1528 1531 # \- 2 --- 4 as the merge base
1529 1532 #
1530 1533
1531 1534 cfname = copy[0]
1532 1535 crev = manifest1.get(cfname)
1533 1536 newfparent = fparent2
1534 1537
1535 1538 if manifest2: # branch merge
1536 1539 if fparent2 == nullid or crev is None: # copied on remote side
1537 1540 if cfname in manifest2:
1538 1541 crev = manifest2[cfname]
1539 1542 newfparent = fparent1
1540 1543
1541 1544 # Here, we used to search backwards through history to try to find
1542 1545 # where the file copy came from if the source of a copy was not in
1543 1546 # the parent directory. However, this doesn't actually make sense to
1544 1547 # do (what does a copy from something not in your working copy even
1545 1548 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
1546 1549 # the user that copy information was dropped, so if they didn't
1547 1550 # expect this outcome it can be fixed, but this is the correct
1548 1551 # behavior in this circumstance.
1549 1552
1550 1553 if crev:
1551 1554 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
1552 1555 meta["copy"] = cfname
1553 1556 meta["copyrev"] = hex(crev)
1554 1557 fparent1, fparent2 = nullid, newfparent
1555 1558 else:
1556 1559 self.ui.warn(_("warning: can't find ancestor for '%s' "
1557 1560 "copied from '%s'!\n") % (fname, cfname))
1558 1561
1559 1562 elif fparent1 == nullid:
1560 1563 fparent1, fparent2 = fparent2, nullid
1561 1564 elif fparent2 != nullid:
1562 1565 # is one parent an ancestor of the other?
1563 1566 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
1564 1567 if fparent1 in fparentancestors:
1565 1568 fparent1, fparent2 = fparent2, nullid
1566 1569 elif fparent2 in fparentancestors:
1567 1570 fparent2 = nullid
1568 1571
1569 1572 # is the file changed?
1570 1573 text = fctx.data()
1571 1574 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
1572 1575 changelist.append(fname)
1573 1576 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
1574 1577 # are just the flags changed during merge?
1575 1578 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
1576 1579 changelist.append(fname)
1577 1580
1578 1581 return fparent1
1579 1582
1580 1583 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
1581 1584 """check for commit arguments that aren't committable"""
1582 1585 if match.isexact() or match.prefix():
1583 1586 matched = set(status.modified + status.added + status.removed)
1584 1587
1585 1588 for f in match.files():
1586 1589 f = self.dirstate.normalize(f)
1587 1590 if f == '.' or f in matched or f in wctx.substate:
1588 1591 continue
1589 1592 if f in status.deleted:
1590 1593 fail(f, _('file not found!'))
1591 1594 if f in vdirs: # visited directory
1592 1595 d = f + '/'
1593 1596 for mf in matched:
1594 1597 if mf.startswith(d):
1595 1598 break
1596 1599 else:
1597 1600 fail(f, _("no match under directory!"))
1598 1601 elif f not in self.dirstate:
1599 1602 fail(f, _("file not tracked!"))
1600 1603
1601 1604 @unfilteredmethod
1602 1605 def commit(self, text="", user=None, date=None, match=None, force=False,
1603 1606 editor=False, extra=None):
1604 1607 """Add a new revision to current repository.
1605 1608
1606 1609 Revision information is gathered from the working directory,
1607 1610 match can be used to filter the committed files. If editor is
1608 1611 supplied, it is called to get a commit message.
1609 1612 """
1610 1613 if extra is None:
1611 1614 extra = {}
1612 1615
1613 1616 def fail(f, msg):
1614 1617 raise error.Abort('%s: %s' % (f, msg))
1615 1618
1616 1619 if not match:
1617 1620 match = matchmod.always(self.root, '')
1618 1621
1619 1622 if not force:
1620 1623 vdirs = []
1621 1624 match.explicitdir = vdirs.append
1622 1625 match.bad = fail
1623 1626
1624 1627 wlock = lock = tr = None
1625 1628 try:
1626 1629 wlock = self.wlock()
1627 1630 lock = self.lock() # for recent changelog (see issue4368)
1628 1631
1629 1632 wctx = self[None]
1630 1633 merge = len(wctx.parents()) > 1
1631 1634
1632 1635 if not force and merge and not match.always():
1633 1636 raise error.Abort(_('cannot partially commit a merge '
1634 1637 '(do not specify files or patterns)'))
1635 1638
1636 1639 status = self.status(match=match, clean=force)
1637 1640 if force:
1638 1641 status.modified.extend(status.clean) # mq may commit clean files
1639 1642
1640 1643 # check subrepos
1641 1644 subs = []
1642 1645 commitsubs = set()
1643 1646 newstate = wctx.substate.copy()
1644 1647 # only manage subrepos and .hgsubstate if .hgsub is present
1645 1648 if '.hgsub' in wctx:
1646 1649 # we'll decide whether to track this ourselves, thanks
1647 1650 for c in status.modified, status.added, status.removed:
1648 1651 if '.hgsubstate' in c:
1649 1652 c.remove('.hgsubstate')
1650 1653
1651 1654 # compare current state to last committed state
1652 1655 # build new substate based on last committed state
1653 1656 oldstate = wctx.p1().substate
1654 1657 for s in sorted(newstate.keys()):
1655 1658 if not match(s):
1656 1659 # ignore working copy, use old state if present
1657 1660 if s in oldstate:
1658 1661 newstate[s] = oldstate[s]
1659 1662 continue
1660 1663 if not force:
1661 1664 raise error.Abort(
1662 1665 _("commit with new subrepo %s excluded") % s)
1663 1666 dirtyreason = wctx.sub(s).dirtyreason(True)
1664 1667 if dirtyreason:
1665 1668 if not self.ui.configbool('ui', 'commitsubrepos'):
1666 1669 raise error.Abort(dirtyreason,
1667 1670 hint=_("use --subrepos for recursive commit"))
1668 1671 subs.append(s)
1669 1672 commitsubs.add(s)
1670 1673 else:
1671 1674 bs = wctx.sub(s).basestate()
1672 1675 newstate[s] = (newstate[s][0], bs, newstate[s][2])
1673 1676 if oldstate.get(s, (None, None, None))[1] != bs:
1674 1677 subs.append(s)
1675 1678
1676 1679 # check for removed subrepos
1677 1680 for p in wctx.parents():
1678 1681 r = [s for s in p.substate if s not in newstate]
1679 1682 subs += [s for s in r if match(s)]
1680 1683 if subs:
1681 1684 if (not match('.hgsub') and
1682 1685 '.hgsub' in (wctx.modified() + wctx.added())):
1683 1686 raise error.Abort(
1684 1687 _("can't commit subrepos without .hgsub"))
1685 1688 status.modified.insert(0, '.hgsubstate')
1686 1689
1687 1690 elif '.hgsub' in status.removed:
1688 1691 # clean up .hgsubstate when .hgsub is removed
1689 1692 if ('.hgsubstate' in wctx and
1690 1693 '.hgsubstate' not in (status.modified + status.added +
1691 1694 status.removed)):
1692 1695 status.removed.insert(0, '.hgsubstate')
1693 1696
1694 1697 # make sure all explicit patterns are matched
1695 1698 if not force:
1696 1699 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
1697 1700
1698 1701 cctx = context.workingcommitctx(self, status,
1699 1702 text, user, date, extra)
1700 1703
1701 1704 # internal config: ui.allowemptycommit
1702 1705 allowemptycommit = (wctx.branch() != wctx.p1().branch()
1703 1706 or extra.get('close') or merge or cctx.files()
1704 1707 or self.ui.configbool('ui', 'allowemptycommit'))
1705 1708 if not allowemptycommit:
1706 1709 return None
1707 1710
1708 1711 if merge and cctx.deleted():
1709 1712 raise error.Abort(_("cannot commit merge with missing files"))
1710 1713
1711 1714 ms = mergemod.mergestate.read(self)
1712 1715 mergeutil.checkunresolved(ms)
1713 1716
1714 1717 if editor:
1715 1718 cctx._text = editor(self, cctx, subs)
1716 1719 edited = (text != cctx._text)
1717 1720
1718 1721 # Save commit message in case this transaction gets rolled back
1719 1722 # (e.g. by a pretxncommit hook). Leave the content alone on
1720 1723 # the assumption that the user will use the same editor again.
1721 1724 msgfn = self.savecommitmessage(cctx._text)
1722 1725
1723 1726 # commit subs and write new state
1724 1727 if subs:
1725 1728 for s in sorted(commitsubs):
1726 1729 sub = wctx.sub(s)
1727 1730 self.ui.status(_('committing subrepository %s\n') %
1728 1731 subrepo.subrelpath(sub))
1729 1732 sr = sub.commit(cctx._text, user, date)
1730 1733 newstate[s] = (newstate[s][0], sr)
1731 1734 subrepo.writestate(self, newstate)
1732 1735
1733 1736 p1, p2 = self.dirstate.parents()
1734 1737 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
1735 1738 try:
1736 1739 self.hook("precommit", throw=True, parent1=hookp1,
1737 1740 parent2=hookp2)
1738 1741 tr = self.transaction('commit')
1739 1742 ret = self.commitctx(cctx, True)
1740 1743 except: # re-raises
1741 1744 if edited:
1742 1745 self.ui.write(
1743 1746 _('note: commit message saved in %s\n') % msgfn)
1744 1747 raise
1745 1748 # update bookmarks, dirstate and mergestate
1746 1749 bookmarks.update(self, [p1, p2], ret)
1747 1750 cctx.markcommitted(ret)
1748 1751 ms.reset()
1749 1752 tr.close()
1750 1753
1751 1754 finally:
1752 1755 lockmod.release(tr, lock, wlock)
1753 1756
1754 1757 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
1755 1758 # hack for command that use a temporary commit (eg: histedit)
1756 1759 # temporary commit got stripped before hook release
1757 1760 if self.changelog.hasnode(ret):
1758 1761 self.hook("commit", node=node, parent1=parent1,
1759 1762 parent2=parent2)
1760 1763 self._afterlock(commithook)
1761 1764 return ret
1762 1765
1763 1766 @unfilteredmethod
1764 1767 def commitctx(self, ctx, error=False):
1765 1768 """Add a new revision to current repository.
1766 1769 Revision information is passed via the context argument.
1767 1770 """
1768 1771
1769 1772 tr = None
1770 1773 p1, p2 = ctx.p1(), ctx.p2()
1771 1774 user = ctx.user()
1772 1775
1773 1776 lock = self.lock()
1774 1777 try:
1775 1778 tr = self.transaction("commit")
1776 1779 trp = weakref.proxy(tr)
1777 1780
1778 1781 if ctx.manifestnode():
1779 1782 # reuse an existing manifest revision
1780 1783 mn = ctx.manifestnode()
1781 1784 files = ctx.files()
1782 1785 elif ctx.files():
1783 1786 m1ctx = p1.manifestctx()
1784 1787 m2ctx = p2.manifestctx()
1785 1788 mctx = m1ctx.copy()
1786 1789
1787 1790 m = mctx.read()
1788 1791 m1 = m1ctx.read()
1789 1792 m2 = m2ctx.read()
1790 1793
1791 1794 # check in files
1792 1795 added = []
1793 1796 changed = []
1794 1797 removed = list(ctx.removed())
1795 1798 linkrev = len(self)
1796 1799 self.ui.note(_("committing files:\n"))
1797 1800 for f in sorted(ctx.modified() + ctx.added()):
1798 1801 self.ui.note(f + "\n")
1799 1802 try:
1800 1803 fctx = ctx[f]
1801 1804 if fctx is None:
1802 1805 removed.append(f)
1803 1806 else:
1804 1807 added.append(f)
1805 1808 m[f] = self._filecommit(fctx, m1, m2, linkrev,
1806 1809 trp, changed)
1807 1810 m.setflag(f, fctx.flags())
1808 1811 except OSError as inst:
1809 1812 self.ui.warn(_("trouble committing %s!\n") % f)
1810 1813 raise
1811 1814 except IOError as inst:
1812 1815 errcode = getattr(inst, 'errno', errno.ENOENT)
1813 1816 if error or errcode and errcode != errno.ENOENT:
1814 1817 self.ui.warn(_("trouble committing %s!\n") % f)
1815 1818 raise
1816 1819
1817 1820 # update manifest
1818 1821 self.ui.note(_("committing manifest\n"))
1819 1822 removed = [f for f in sorted(removed) if f in m1 or f in m2]
1820 1823 drop = [f for f in removed if f in m]
1821 1824 for f in drop:
1822 1825 del m[f]
1823 1826 mn = mctx.write(trp, linkrev,
1824 1827 p1.manifestnode(), p2.manifestnode(),
1825 1828 added, drop)
1826 1829 files = changed + removed
1827 1830 else:
1828 1831 mn = p1.manifestnode()
1829 1832 files = []
1830 1833
1831 1834 # update changelog
1832 1835 self.ui.note(_("committing changelog\n"))
1833 1836 self.changelog.delayupdate(tr)
1834 1837 n = self.changelog.add(mn, files, ctx.description(),
1835 1838 trp, p1.node(), p2.node(),
1836 1839 user, ctx.date(), ctx.extra().copy())
1837 1840 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
1838 1841 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1839 1842 parent2=xp2)
1840 1843 # set the new commit is proper phase
1841 1844 targetphase = subrepo.newcommitphase(self.ui, ctx)
1842 1845 if targetphase:
1843 1846 # retract boundary do not alter parent changeset.
1844 1847 # if a parent have higher the resulting phase will
1845 1848 # be compliant anyway
1846 1849 #
1847 1850 # if minimal phase was 0 we don't need to retract anything
1848 1851 phases.retractboundary(self, tr, targetphase, [n])
1849 1852 tr.close()
1850 1853 return n
1851 1854 finally:
1852 1855 if tr:
1853 1856 tr.release()
1854 1857 lock.release()
1855 1858
1856 1859 @unfilteredmethod
1857 1860 def destroying(self):
1858 1861 '''Inform the repository that nodes are about to be destroyed.
1859 1862 Intended for use by strip and rollback, so there's a common
1860 1863 place for anything that has to be done before destroying history.
1861 1864
1862 1865 This is mostly useful for saving state that is in memory and waiting
1863 1866 to be flushed when the current lock is released. Because a call to
1864 1867 destroyed is imminent, the repo will be invalidated causing those
1865 1868 changes to stay in memory (waiting for the next unlock), or vanish
1866 1869 completely.
1867 1870 '''
1868 1871 # When using the same lock to commit and strip, the phasecache is left
1869 1872 # dirty after committing. Then when we strip, the repo is invalidated,
1870 1873 # causing those changes to disappear.
1871 1874 if '_phasecache' in vars(self):
1872 1875 self._phasecache.write()
1873 1876
1874 1877 @unfilteredmethod
1875 1878 def destroyed(self):
1876 1879 '''Inform the repository that nodes have been destroyed.
1877 1880 Intended for use by strip and rollback, so there's a common
1878 1881 place for anything that has to be done after destroying history.
1879 1882 '''
1880 1883 # When one tries to:
1881 1884 # 1) destroy nodes thus calling this method (e.g. strip)
1882 1885 # 2) use phasecache somewhere (e.g. commit)
1883 1886 #
1884 1887 # then 2) will fail because the phasecache contains nodes that were
1885 1888 # removed. We can either remove phasecache from the filecache,
1886 1889 # causing it to reload next time it is accessed, or simply filter
1887 1890 # the removed nodes now and write the updated cache.
1888 1891 self._phasecache.filterunknown(self)
1889 1892 self._phasecache.write()
1890 1893
1891 1894 # refresh all repository caches
1892 1895 self.updatecaches()
1893 1896
1894 1897 # Ensure the persistent tag cache is updated. Doing it now
1895 1898 # means that the tag cache only has to worry about destroyed
1896 1899 # heads immediately after a strip/rollback. That in turn
1897 1900 # guarantees that "cachetip == currenttip" (comparing both rev
1898 1901 # and node) always means no nodes have been added or destroyed.
1899 1902
1900 1903 # XXX this is suboptimal when qrefresh'ing: we strip the current
1901 1904 # head, refresh the tag cache, then immediately add a new head.
1902 1905 # But I think doing it this way is necessary for the "instant
1903 1906 # tag cache retrieval" case to work.
1904 1907 self.invalidate()
1905 1908
1906 1909 def walk(self, match, node=None):
1907 1910 '''
1908 1911 walk recursively through the directory tree or a given
1909 1912 changeset, finding all files matched by the match
1910 1913 function
1911 1914 '''
1912 1915 self.ui.deprecwarn('use repo[node].walk instead of repo.walk', '4.3')
1913 1916 return self[node].walk(match)
1914 1917
1915 1918 def status(self, node1='.', node2=None, match=None,
1916 1919 ignored=False, clean=False, unknown=False,
1917 1920 listsubrepos=False):
1918 1921 '''a convenience method that calls node1.status(node2)'''
1919 1922 return self[node1].status(node2, match, ignored, clean, unknown,
1920 1923 listsubrepos)
1921 1924
1922 1925 def addpostdsstatus(self, ps):
1923 1926 """Add a callback to run within the wlock, at the point at which status
1924 1927 fixups happen.
1925 1928
1926 1929 On status completion, callback(wctx, status) will be called with the
1927 1930 wlock held, unless the dirstate has changed from underneath or the wlock
1928 1931 couldn't be grabbed.
1929 1932
1930 1933 Callbacks should not capture and use a cached copy of the dirstate --
1931 1934 it might change in the meanwhile. Instead, they should access the
1932 1935 dirstate via wctx.repo().dirstate.
1933 1936
1934 1937 This list is emptied out after each status run -- extensions should
1935 1938 make sure it adds to this list each time dirstate.status is called.
1936 1939 Extensions should also make sure they don't call this for statuses
1937 1940 that don't involve the dirstate.
1938 1941 """
1939 1942
1940 1943 # The list is located here for uniqueness reasons -- it is actually
1941 1944 # managed by the workingctx, but that isn't unique per-repo.
1942 1945 self._postdsstatus.append(ps)
1943 1946
1944 1947 def postdsstatus(self):
1945 1948 """Used by workingctx to get the list of post-dirstate-status hooks."""
1946 1949 return self._postdsstatus
1947 1950
1948 1951 def clearpostdsstatus(self):
1949 1952 """Used by workingctx to clear post-dirstate-status hooks."""
1950 1953 del self._postdsstatus[:]
1951 1954
1952 1955 def heads(self, start=None):
1953 1956 if start is None:
1954 1957 cl = self.changelog
1955 1958 headrevs = reversed(cl.headrevs())
1956 1959 return [cl.node(rev) for rev in headrevs]
1957 1960
1958 1961 heads = self.changelog.heads(start)
1959 1962 # sort the output in rev descending order
1960 1963 return sorted(heads, key=self.changelog.rev, reverse=True)
1961 1964
1962 1965 def branchheads(self, branch=None, start=None, closed=False):
1963 1966 '''return a (possibly filtered) list of heads for the given branch
1964 1967
1965 1968 Heads are returned in topological order, from newest to oldest.
1966 1969 If branch is None, use the dirstate branch.
1967 1970 If start is not None, return only heads reachable from start.
1968 1971 If closed is True, return heads that are marked as closed as well.
1969 1972 '''
1970 1973 if branch is None:
1971 1974 branch = self[None].branch()
1972 1975 branches = self.branchmap()
1973 1976 if branch not in branches:
1974 1977 return []
1975 1978 # the cache returns heads ordered lowest to highest
1976 1979 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
1977 1980 if start is not None:
1978 1981 # filter out the heads that cannot be reached from startrev
1979 1982 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
1980 1983 bheads = [h for h in bheads if h in fbheads]
1981 1984 return bheads
1982 1985
1983 1986 def branches(self, nodes):
1984 1987 if not nodes:
1985 1988 nodes = [self.changelog.tip()]
1986 1989 b = []
1987 1990 for n in nodes:
1988 1991 t = n
1989 1992 while True:
1990 1993 p = self.changelog.parents(n)
1991 1994 if p[1] != nullid or p[0] == nullid:
1992 1995 b.append((t, n, p[0], p[1]))
1993 1996 break
1994 1997 n = p[0]
1995 1998 return b
1996 1999
1997 2000 def between(self, pairs):
1998 2001 r = []
1999 2002
2000 2003 for top, bottom in pairs:
2001 2004 n, l, i = top, [], 0
2002 2005 f = 1
2003 2006
2004 2007 while n != bottom and n != nullid:
2005 2008 p = self.changelog.parents(n)[0]
2006 2009 if i == f:
2007 2010 l.append(n)
2008 2011 f = f * 2
2009 2012 n = p
2010 2013 i += 1
2011 2014
2012 2015 r.append(l)
2013 2016
2014 2017 return r
2015 2018
2016 2019 def checkpush(self, pushop):
2017 2020 """Extensions can override this function if additional checks have
2018 2021 to be performed before pushing, or call it if they override push
2019 2022 command.
2020 2023 """
2021 2024 pass
2022 2025
2023 2026 @unfilteredpropertycache
2024 2027 def prepushoutgoinghooks(self):
2025 2028 """Return util.hooks consists of a pushop with repo, remote, outgoing
2026 2029 methods, which are called before pushing changesets.
2027 2030 """
2028 2031 return util.hooks()
2029 2032
2030 2033 def pushkey(self, namespace, key, old, new):
2031 2034 try:
2032 2035 tr = self.currenttransaction()
2033 2036 hookargs = {}
2034 2037 if tr is not None:
2035 2038 hookargs.update(tr.hookargs)
2036 2039 hookargs['namespace'] = namespace
2037 2040 hookargs['key'] = key
2038 2041 hookargs['old'] = old
2039 2042 hookargs['new'] = new
2040 2043 self.hook('prepushkey', throw=True, **hookargs)
2041 2044 except error.HookAbort as exc:
2042 2045 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
2043 2046 if exc.hint:
2044 2047 self.ui.write_err(_("(%s)\n") % exc.hint)
2045 2048 return False
2046 2049 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
2047 2050 ret = pushkey.push(self, namespace, key, old, new)
2048 2051 def runhook():
2049 2052 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
2050 2053 ret=ret)
2051 2054 self._afterlock(runhook)
2052 2055 return ret
2053 2056
2054 2057 def listkeys(self, namespace):
2055 2058 self.hook('prelistkeys', throw=True, namespace=namespace)
2056 2059 self.ui.debug('listing keys for "%s"\n' % namespace)
2057 2060 values = pushkey.list(self, namespace)
2058 2061 self.hook('listkeys', namespace=namespace, values=values)
2059 2062 return values
2060 2063
2061 2064 def debugwireargs(self, one, two, three=None, four=None, five=None):
2062 2065 '''used to test argument passing over the wire'''
2063 2066 return "%s %s %s %s %s" % (one, two, three, four, five)
2064 2067
2065 2068 def savecommitmessage(self, text):
2066 2069 fp = self.vfs('last-message.txt', 'wb')
2067 2070 try:
2068 2071 fp.write(text)
2069 2072 finally:
2070 2073 fp.close()
2071 2074 return self.pathto(fp.name[len(self.root) + 1:])
2072 2075
2073 2076 # used to avoid circular references so destructors work
2074 2077 def aftertrans(files):
2075 2078 renamefiles = [tuple(t) for t in files]
2076 2079 def a():
2077 2080 for vfs, src, dest in renamefiles:
2078 2081 # if src and dest refer to a same file, vfs.rename is a no-op,
2079 2082 # leaving both src and dest on disk. delete dest to make sure
2080 2083 # the rename couldn't be such a no-op.
2081 2084 vfs.tryunlink(dest)
2082 2085 try:
2083 2086 vfs.rename(src, dest)
2084 2087 except OSError: # journal file does not yet exist
2085 2088 pass
2086 2089 return a
2087 2090
2088 2091 def undoname(fn):
2089 2092 base, name = os.path.split(fn)
2090 2093 assert name.startswith('journal')
2091 2094 return os.path.join(base, name.replace('journal', 'undo', 1))
2092 2095
2093 2096 def instance(ui, path, create):
2094 2097 return localrepository(ui, util.urllocalpath(path), create)
2095 2098
2096 2099 def islocal(path):
2097 2100 return True
2098 2101
2099 2102 def newreporequirements(repo):
2100 2103 """Determine the set of requirements for a new local repository.
2101 2104
2102 2105 Extensions can wrap this function to specify custom requirements for
2103 2106 new repositories.
2104 2107 """
2105 2108 ui = repo.ui
2106 2109 requirements = {'revlogv1'}
2107 2110 if ui.configbool('format', 'usestore'):
2108 2111 requirements.add('store')
2109 2112 if ui.configbool('format', 'usefncache'):
2110 2113 requirements.add('fncache')
2111 2114 if ui.configbool('format', 'dotencode'):
2112 2115 requirements.add('dotencode')
2113 2116
2114 2117 compengine = ui.config('experimental', 'format.compression', 'zlib')
2115 2118 if compengine not in util.compengines:
2116 2119 raise error.Abort(_('compression engine %s defined by '
2117 2120 'experimental.format.compression not available') %
2118 2121 compengine,
2119 2122 hint=_('run "hg debuginstall" to list available '
2120 2123 'compression engines'))
2121 2124
2122 2125 # zlib is the historical default and doesn't need an explicit requirement.
2123 2126 if compengine != 'zlib':
2124 2127 requirements.add('exp-compression-%s' % compengine)
2125 2128
2126 2129 if scmutil.gdinitconfig(ui):
2127 2130 requirements.add('generaldelta')
2128 2131 if ui.configbool('experimental', 'treemanifest', False):
2129 2132 requirements.add('treemanifest')
2130 2133 if ui.configbool('experimental', 'manifestv2', False):
2131 2134 requirements.add('manifestv2')
2132 2135
2133 2136 revlogv2 = ui.config('experimental', 'revlogv2')
2134 2137 if revlogv2 == 'enable-unstable-format-and-corrupt-my-data':
2135 2138 requirements.remove('revlogv1')
2136 2139 # generaldelta is implied by revlogv2.
2137 2140 requirements.discard('generaldelta')
2138 2141 requirements.add(REVLOGV2_REQUIREMENT)
2139 2142
2140 2143 return requirements
General Comments 0
You need to be logged in to leave comments. Login now