##// END OF EJS Templates
changegroup: add v4 changegroup for revlog v2 exchange...
Raphaël Gomès -
r47445:a41565be default
parent child Browse files
Show More
@@ -1,513 +1,516 b''
1 1 # remotefilelog.py - filelog implementation where filelog history is stored
2 2 # remotely
3 3 #
4 4 # Copyright 2013 Facebook, Inc.
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 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import os
12 12
13 13 from mercurial.node import (
14 14 bin,
15 15 nullid,
16 16 wdirfilenodeids,
17 17 wdirid,
18 18 )
19 19 from mercurial.i18n import _
20 20 from mercurial import (
21 21 ancestor,
22 22 error,
23 23 mdiff,
24 24 pycompat,
25 25 revlog,
26 26 util,
27 27 )
28 28 from mercurial.utils import storageutil
29 29 from mercurial.revlogutils import flagutil
30 30
31 31 from . import (
32 32 constants,
33 33 fileserverclient,
34 34 shallowutil,
35 35 )
36 36
37 37
38 38 class remotefilelognodemap(object):
39 39 def __init__(self, filename, store):
40 40 self._filename = filename
41 41 self._store = store
42 42
43 43 def __contains__(self, node):
44 44 missing = self._store.getmissing([(self._filename, node)])
45 45 return not bool(missing)
46 46
47 47 def __get__(self, node):
48 48 if node not in self:
49 49 raise KeyError(node)
50 50 return node
51 51
52 52
53 53 class remotefilelog(object):
54 54
55 55 _generaldelta = True
56 56 _flagserrorclass = error.RevlogError
57 57
58 58 def __init__(self, opener, path, repo):
59 59 self.opener = opener
60 60 self.filename = path
61 61 self.repo = repo
62 62 self.nodemap = remotefilelognodemap(self.filename, repo.contentstore)
63 63
64 64 self.version = 1
65 65
66 66 self._flagprocessors = dict(flagutil.flagprocessors)
67 67
68 68 def read(self, node):
69 69 """returns the file contents at this node"""
70 70 t = self.revision(node)
71 71 if not t.startswith(b'\1\n'):
72 72 return t
73 73 s = t.index(b'\1\n', 2)
74 74 return t[s + 2 :]
75 75
76 76 def add(self, text, meta, transaction, linknode, p1=None, p2=None):
77 77 # hash with the metadata, like in vanilla filelogs
78 78 hashtext = shallowutil.createrevlogtext(
79 79 text, meta.get(b'copy'), meta.get(b'copyrev')
80 80 )
81 81 node = storageutil.hashrevisionsha1(hashtext, p1, p2)
82 82 return self.addrevision(
83 83 hashtext, transaction, linknode, p1, p2, node=node
84 84 )
85 85
86 86 def _createfileblob(self, text, meta, flags, p1, p2, node, linknode):
87 87 # text passed to "_createfileblob" does not include filelog metadata
88 88 header = shallowutil.buildfileblobheader(len(text), flags)
89 89 data = b"%s\0%s" % (header, text)
90 90
91 91 realp1 = p1
92 92 copyfrom = b""
93 93 if meta and b'copy' in meta:
94 94 copyfrom = meta[b'copy']
95 95 realp1 = bin(meta[b'copyrev'])
96 96
97 97 data += b"%s%s%s%s%s\0" % (node, realp1, p2, linknode, copyfrom)
98 98
99 99 visited = set()
100 100
101 101 pancestors = {}
102 102 queue = []
103 103 if realp1 != nullid:
104 104 p1flog = self
105 105 if copyfrom:
106 106 p1flog = remotefilelog(self.opener, copyfrom, self.repo)
107 107
108 108 pancestors.update(p1flog.ancestormap(realp1))
109 109 queue.append(realp1)
110 110 visited.add(realp1)
111 111 if p2 != nullid:
112 112 pancestors.update(self.ancestormap(p2))
113 113 queue.append(p2)
114 114 visited.add(p2)
115 115
116 116 ancestortext = b""
117 117
118 118 # add the ancestors in topological order
119 119 while queue:
120 120 c = queue.pop(0)
121 121 pa1, pa2, ancestorlinknode, pacopyfrom = pancestors[c]
122 122
123 123 pacopyfrom = pacopyfrom or b''
124 124 ancestortext += b"%s%s%s%s%s\0" % (
125 125 c,
126 126 pa1,
127 127 pa2,
128 128 ancestorlinknode,
129 129 pacopyfrom,
130 130 )
131 131
132 132 if pa1 != nullid and pa1 not in visited:
133 133 queue.append(pa1)
134 134 visited.add(pa1)
135 135 if pa2 != nullid and pa2 not in visited:
136 136 queue.append(pa2)
137 137 visited.add(pa2)
138 138
139 139 data += ancestortext
140 140
141 141 return data
142 142
143 143 def addrevision(
144 144 self,
145 145 text,
146 146 transaction,
147 147 linknode,
148 148 p1,
149 149 p2,
150 150 cachedelta=None,
151 151 node=None,
152 152 flags=revlog.REVIDX_DEFAULT_FLAGS,
153 153 sidedata=None,
154 154 ):
155 155 # text passed to "addrevision" includes hg filelog metadata header
156 156 if node is None:
157 157 node = storageutil.hashrevisionsha1(text, p1, p2)
158 158
159 159 meta, metaoffset = storageutil.parsemeta(text)
160 160 rawtext, validatehash = flagutil.processflagswrite(
161 161 self,
162 162 text,
163 163 flags,
164 164 )
165 165 return self.addrawrevision(
166 166 rawtext,
167 167 transaction,
168 168 linknode,
169 169 p1,
170 170 p2,
171 171 node,
172 172 flags,
173 173 cachedelta,
174 174 _metatuple=(meta, metaoffset),
175 175 )
176 176
177 177 def addrawrevision(
178 178 self,
179 179 rawtext,
180 180 transaction,
181 181 linknode,
182 182 p1,
183 183 p2,
184 184 node,
185 185 flags,
186 186 cachedelta=None,
187 187 _metatuple=None,
188 188 ):
189 189 if _metatuple:
190 190 # _metatuple: used by "addrevision" internally by remotefilelog
191 191 # meta was parsed confidently
192 192 meta, metaoffset = _metatuple
193 193 else:
194 194 # not from self.addrevision, but something else (repo._filecommit)
195 195 # calls addrawrevision directly. remotefilelog needs to get and
196 196 # strip filelog metadata.
197 197 # we don't have confidence about whether rawtext contains filelog
198 198 # metadata or not (flag processor could replace it), so we just
199 199 # parse it as best-effort.
200 200 # in LFS (flags != 0)'s case, the best way is to call LFS code to
201 201 # get the meta information, instead of storageutil.parsemeta.
202 202 meta, metaoffset = storageutil.parsemeta(rawtext)
203 203 if flags != 0:
204 204 # when flags != 0, be conservative and do not mangle rawtext, since
205 205 # a read flag processor expects the text not being mangled at all.
206 206 metaoffset = 0
207 207 if metaoffset:
208 208 # remotefilelog fileblob stores copy metadata in its ancestortext,
209 209 # not its main blob. so we need to remove filelog metadata
210 210 # (containing copy information) from text.
211 211 blobtext = rawtext[metaoffset:]
212 212 else:
213 213 blobtext = rawtext
214 214 data = self._createfileblob(
215 215 blobtext, meta, flags, p1, p2, node, linknode
216 216 )
217 217 self.repo.contentstore.addremotefilelognode(self.filename, node, data)
218 218
219 219 return node
220 220
221 221 def renamed(self, node):
222 222 ancestors = self.repo.metadatastore.getancestors(self.filename, node)
223 223 p1, p2, linknode, copyfrom = ancestors[node]
224 224 if copyfrom:
225 225 return (copyfrom, p1)
226 226
227 227 return False
228 228
229 229 def size(self, node):
230 230 """return the size of a given revision"""
231 231 return len(self.read(node))
232 232
233 233 rawsize = size
234 234
235 235 def cmp(self, node, text):
236 236 """compare text with a given file revision
237 237
238 238 returns True if text is different than what is stored.
239 239 """
240 240
241 241 if node == nullid:
242 242 return True
243 243
244 244 nodetext = self.read(node)
245 245 return nodetext != text
246 246
247 247 def __nonzero__(self):
248 248 return True
249 249
250 250 __bool__ = __nonzero__
251 251
252 252 def __len__(self):
253 253 if self.filename == b'.hgtags':
254 254 # The length of .hgtags is used to fast path tag checking.
255 255 # remotefilelog doesn't support .hgtags since the entire .hgtags
256 256 # history is needed. Use the excludepattern setting to make
257 257 # .hgtags a normal filelog.
258 258 return 0
259 259
260 260 raise RuntimeError(b"len not supported")
261 261
262 262 def heads(self):
263 263 # Fake heads of the filelog to satisfy hgweb.
264 264 return []
265 265
266 266 def empty(self):
267 267 return False
268 268
269 269 def flags(self, node):
270 270 if isinstance(node, int):
271 271 raise error.ProgrammingError(
272 272 b'remotefilelog does not accept integer rev for flags'
273 273 )
274 274 store = self.repo.contentstore
275 275 return store.getmeta(self.filename, node).get(constants.METAKEYFLAG, 0)
276 276
277 277 def parents(self, node):
278 278 if node == nullid:
279 279 return nullid, nullid
280 280
281 281 ancestormap = self.repo.metadatastore.getancestors(self.filename, node)
282 282 p1, p2, linknode, copyfrom = ancestormap[node]
283 283 if copyfrom:
284 284 p1 = nullid
285 285
286 286 return p1, p2
287 287
288 288 def parentrevs(self, rev):
289 289 # TODO(augie): this is a node and should be a rev, but for now
290 290 # nothing in core seems to actually break.
291 291 return self.parents(rev)
292 292
293 293 def linknode(self, node):
294 294 ancestormap = self.repo.metadatastore.getancestors(self.filename, node)
295 295 p1, p2, linknode, copyfrom = ancestormap[node]
296 296 return linknode
297 297
298 298 def linkrev(self, node):
299 299 return self.repo.unfiltered().changelog.rev(self.linknode(node))
300 300
301 301 def emitrevisions(
302 302 self,
303 303 nodes,
304 304 nodesorder=None,
305 305 revisiondata=False,
306 306 assumehaveparentrevisions=False,
307 307 deltaprevious=False,
308 308 deltamode=None,
309 sidedata_helpers=None,
309 310 ):
310 311 # we don't use any of these parameters here
311 312 del nodesorder, revisiondata, assumehaveparentrevisions, deltaprevious
312 313 del deltamode
313 314 prevnode = None
314 315 for node in nodes:
315 316 p1, p2 = self.parents(node)
316 317 if prevnode is None:
317 318 basenode = prevnode = p1
318 319 if basenode == node:
319 320 basenode = nullid
320 321 if basenode != nullid:
321 322 revision = None
322 323 delta = self.revdiff(basenode, node)
323 324 else:
324 325 revision = self.rawdata(node)
325 326 delta = None
326 327 yield revlog.revlogrevisiondelta(
327 328 node=node,
328 329 p1node=p1,
329 330 p2node=p2,
330 331 linknode=self.linknode(node),
331 332 basenode=basenode,
332 333 flags=self.flags(node),
333 334 baserevisionsize=None,
334 335 revision=revision,
335 336 delta=delta,
337 # Sidedata is not supported yet
338 sidedata=None,
336 339 )
337 340
338 341 def revdiff(self, node1, node2):
339 342 return mdiff.textdiff(self.rawdata(node1), self.rawdata(node2))
340 343
341 344 def lookup(self, node):
342 345 if len(node) == 40:
343 346 node = bin(node)
344 347 if len(node) != 20:
345 348 raise error.LookupError(
346 349 node, self.filename, _(b'invalid lookup input')
347 350 )
348 351
349 352 return node
350 353
351 354 def rev(self, node):
352 355 # This is a hack to make TortoiseHG work.
353 356 return node
354 357
355 358 def node(self, rev):
356 359 # This is a hack.
357 360 if isinstance(rev, int):
358 361 raise error.ProgrammingError(
359 362 b'remotefilelog does not convert integer rev to node'
360 363 )
361 364 return rev
362 365
363 366 def _processflags(self, text, flags, operation, raw=False):
364 367 """deprecated entry point to access flag processors"""
365 368 msg = b'_processflag(...) use the specialized variant'
366 369 util.nouideprecwarn(msg, b'5.2', stacklevel=2)
367 370 if raw:
368 371 return text, flagutil.processflagsraw(self, text, flags)
369 372 elif operation == b'read':
370 373 return flagutil.processflagsread(self, text, flags)
371 374 else: # write operation
372 375 return flagutil.processflagswrite(self, text, flags)
373 376
374 377 def revision(self, node, raw=False):
375 378 """returns the revlog contents at this node.
376 379 this includes the meta data traditionally included in file revlogs.
377 380 this is generally only used for bundling and communicating with vanilla
378 381 hg clients.
379 382 """
380 383 if node == nullid:
381 384 return b""
382 385 if len(node) != 20:
383 386 raise error.LookupError(
384 387 node, self.filename, _(b'invalid revision input')
385 388 )
386 389 if node == wdirid or node in wdirfilenodeids:
387 390 raise error.WdirUnsupported
388 391
389 392 store = self.repo.contentstore
390 393 rawtext = store.get(self.filename, node)
391 394 if raw:
392 395 return rawtext
393 396 flags = store.getmeta(self.filename, node).get(constants.METAKEYFLAG, 0)
394 397 if flags == 0:
395 398 return rawtext
396 399 return flagutil.processflagsread(self, rawtext, flags)[0]
397 400
398 401 def rawdata(self, node):
399 402 return self.revision(node, raw=False)
400 403
401 404 def _read(self, id):
402 405 """reads the raw file blob from disk, cache, or server"""
403 406 fileservice = self.repo.fileservice
404 407 localcache = fileservice.localcache
405 408 cachekey = fileserverclient.getcachekey(
406 409 self.repo.name, self.filename, id
407 410 )
408 411 try:
409 412 return localcache.read(cachekey)
410 413 except KeyError:
411 414 pass
412 415
413 416 localkey = fileserverclient.getlocalkey(self.filename, id)
414 417 localpath = os.path.join(self.localpath, localkey)
415 418 try:
416 419 return shallowutil.readfile(localpath)
417 420 except IOError:
418 421 pass
419 422
420 423 fileservice.prefetch([(self.filename, id)])
421 424 try:
422 425 return localcache.read(cachekey)
423 426 except KeyError:
424 427 pass
425 428
426 429 raise error.LookupError(id, self.filename, _(b'no node'))
427 430
428 431 def ancestormap(self, node):
429 432 return self.repo.metadatastore.getancestors(self.filename, node)
430 433
431 434 def ancestor(self, a, b):
432 435 if a == nullid or b == nullid:
433 436 return nullid
434 437
435 438 revmap, parentfunc = self._buildrevgraph(a, b)
436 439 nodemap = {v: k for (k, v) in pycompat.iteritems(revmap)}
437 440
438 441 ancs = ancestor.ancestors(parentfunc, revmap[a], revmap[b])
439 442 if ancs:
440 443 # choose a consistent winner when there's a tie
441 444 return min(map(nodemap.__getitem__, ancs))
442 445 return nullid
443 446
444 447 def commonancestorsheads(self, a, b):
445 448 """calculate all the heads of the common ancestors of nodes a and b"""
446 449
447 450 if a == nullid or b == nullid:
448 451 return nullid
449 452
450 453 revmap, parentfunc = self._buildrevgraph(a, b)
451 454 nodemap = {v: k for (k, v) in pycompat.iteritems(revmap)}
452 455
453 456 ancs = ancestor.commonancestorsheads(parentfunc, revmap[a], revmap[b])
454 457 return map(nodemap.__getitem__, ancs)
455 458
456 459 def _buildrevgraph(self, a, b):
457 460 """Builds a numeric revision graph for the given two nodes.
458 461 Returns a node->rev map and a rev->[revs] parent function.
459 462 """
460 463 amap = self.ancestormap(a)
461 464 bmap = self.ancestormap(b)
462 465
463 466 # Union the two maps
464 467 parentsmap = collections.defaultdict(list)
465 468 allparents = set()
466 469 for mapping in (amap, bmap):
467 470 for node, pdata in pycompat.iteritems(mapping):
468 471 parents = parentsmap[node]
469 472 p1, p2, linknode, copyfrom = pdata
470 473 # Don't follow renames (copyfrom).
471 474 # remotefilectx.ancestor does that.
472 475 if p1 != nullid and not copyfrom:
473 476 parents.append(p1)
474 477 allparents.add(p1)
475 478 if p2 != nullid:
476 479 parents.append(p2)
477 480 allparents.add(p2)
478 481
479 482 # Breadth first traversal to build linkrev graph
480 483 parentrevs = collections.defaultdict(list)
481 484 revmap = {}
482 485 queue = collections.deque(
483 486 ((None, n) for n in parentsmap if n not in allparents)
484 487 )
485 488 while queue:
486 489 prevrev, current = queue.pop()
487 490 if current in revmap:
488 491 if prevrev:
489 492 parentrevs[prevrev].append(revmap[current])
490 493 continue
491 494
492 495 # Assign linkrevs in reverse order, so start at
493 496 # len(parentsmap) and work backwards.
494 497 currentrev = len(parentsmap) - len(revmap) - 1
495 498 revmap[current] = currentrev
496 499
497 500 if prevrev:
498 501 parentrevs[prevrev].append(currentrev)
499 502
500 503 for parent in parentsmap.get(current):
501 504 queue.appendleft((currentrev, parent))
502 505
503 506 return revmap, parentrevs.__getitem__
504 507
505 508 def strip(self, minlink, transaction):
506 509 pass
507 510
508 511 # misc unused things
509 512 def files(self):
510 513 return []
511 514
512 515 def checksize(self):
513 516 return 0, 0
@@ -1,303 +1,307 b''
1 1 # shallowbundle.py - bundle10 implementation for use with shallow repositories
2 2 #
3 3 # Copyright 2013 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 from __future__ import absolute_import
8 8
9 9 from mercurial.i18n import _
10 10 from mercurial.node import bin, hex, nullid
11 11 from mercurial import (
12 12 bundlerepo,
13 13 changegroup,
14 14 error,
15 15 match,
16 16 mdiff,
17 17 pycompat,
18 18 )
19 19 from . import (
20 20 constants,
21 21 remotefilelog,
22 22 shallowutil,
23 23 )
24 24
25 25 NoFiles = 0
26 26 LocalFiles = 1
27 27 AllFiles = 2
28 28
29 29
30 30 def shallowgroup(cls, self, nodelist, rlog, lookup, units=None, reorder=None):
31 31 if not isinstance(rlog, remotefilelog.remotefilelog):
32 32 for c in super(cls, self).group(nodelist, rlog, lookup, units=units):
33 33 yield c
34 34 return
35 35
36 36 if len(nodelist) == 0:
37 37 yield self.close()
38 38 return
39 39
40 40 nodelist = shallowutil.sortnodes(nodelist, rlog.parents)
41 41
42 42 # add the parent of the first rev
43 43 p = rlog.parents(nodelist[0])[0]
44 44 nodelist.insert(0, p)
45 45
46 46 # build deltas
47 47 for i in pycompat.xrange(len(nodelist) - 1):
48 48 prev, curr = nodelist[i], nodelist[i + 1]
49 49 linknode = lookup(curr)
50 50 for c in self.nodechunk(rlog, curr, prev, linknode):
51 51 yield c
52 52
53 53 yield self.close()
54 54
55 55
56 56 class shallowcg1packer(changegroup.cgpacker):
57 57 def generate(self, commonrevs, clnodes, fastpathlinkrev, source, **kwargs):
58 58 if shallowutil.isenabled(self._repo):
59 59 fastpathlinkrev = False
60 60
61 61 return super(shallowcg1packer, self).generate(
62 62 commonrevs, clnodes, fastpathlinkrev, source, **kwargs
63 63 )
64 64
65 65 def group(self, nodelist, rlog, lookup, units=None, reorder=None):
66 66 return shallowgroup(
67 67 shallowcg1packer, self, nodelist, rlog, lookup, units=units
68 68 )
69 69
70 def generatefiles(self, changedfiles, *args):
70 def generatefiles(self, changedfiles, *args, **kwargs):
71 71 try:
72 72 linknodes, commonrevs, source = args
73 73 except ValueError:
74 74 commonrevs, source, mfdicts, fastpathlinkrev, fnodes, clrevs = args
75 75 if shallowutil.isenabled(self._repo):
76 76 repo = self._repo
77 77 if isinstance(repo, bundlerepo.bundlerepository):
78 78 # If the bundle contains filelogs, we can't pull from it, since
79 79 # bundlerepo is heavily tied to revlogs. Instead require that
80 80 # the user use unbundle instead.
81 81 # Force load the filelog data.
82 82 bundlerepo.bundlerepository.file(repo, b'foo')
83 83 if repo._cgfilespos:
84 84 raise error.Abort(
85 85 b"cannot pull from full bundles",
86 86 hint=b"use `hg unbundle` instead",
87 87 )
88 88 return []
89 89 filestosend = self.shouldaddfilegroups(source)
90 90 if filestosend == NoFiles:
91 91 changedfiles = list(
92 92 [f for f in changedfiles if not repo.shallowmatch(f)]
93 93 )
94 94
95 return super(shallowcg1packer, self).generatefiles(changedfiles, *args)
95 return super(shallowcg1packer, self).generatefiles(
96 changedfiles, *args, **kwargs
97 )
96 98
97 99 def shouldaddfilegroups(self, source):
98 100 repo = self._repo
99 101 if not shallowutil.isenabled(repo):
100 102 return AllFiles
101 103
102 104 if source == b"push" or source == b"bundle":
103 105 return AllFiles
104 106
105 107 caps = self._bundlecaps or []
106 108 if source == b"serve" or source == b"pull":
107 109 if constants.BUNDLE2_CAPABLITY in caps:
108 110 return LocalFiles
109 111 else:
110 112 # Serving to a full repo requires us to serve everything
111 113 repo.ui.warn(_(b"pulling from a shallow repo\n"))
112 114 return AllFiles
113 115
114 116 return NoFiles
115 117
116 118 def prune(self, rlog, missing, commonrevs):
117 119 if not isinstance(rlog, remotefilelog.remotefilelog):
118 120 return super(shallowcg1packer, self).prune(
119 121 rlog, missing, commonrevs
120 122 )
121 123
122 124 repo = self._repo
123 125 results = []
124 126 for fnode in missing:
125 127 fctx = repo.filectx(rlog.filename, fileid=fnode)
126 128 if fctx.linkrev() not in commonrevs:
127 129 results.append(fnode)
128 130 return results
129 131
130 132 def nodechunk(self, revlog, node, prevnode, linknode):
131 133 prefix = b''
132 134 if prevnode == nullid:
133 135 delta = revlog.rawdata(node)
134 136 prefix = mdiff.trivialdiffheader(len(delta))
135 137 else:
136 138 # Actually uses remotefilelog.revdiff which works on nodes, not revs
137 139 delta = revlog.revdiff(prevnode, node)
138 140 p1, p2 = revlog.parents(node)
139 141 flags = revlog.flags(node)
140 142 meta = self.builddeltaheader(node, p1, p2, prevnode, linknode, flags)
141 143 meta += prefix
142 144 l = len(meta) + len(delta)
143 145 yield changegroup.chunkheader(l)
144 146 yield meta
145 147 yield delta
146 148
147 149
148 150 def makechangegroup(orig, repo, outgoing, version, source, *args, **kwargs):
149 151 if not shallowutil.isenabled(repo):
150 152 return orig(repo, outgoing, version, source, *args, **kwargs)
151 153
152 154 original = repo.shallowmatch
153 155 try:
154 156 # if serving, only send files the clients has patterns for
155 157 if source == b'serve':
156 158 bundlecaps = kwargs.get('bundlecaps')
157 159 includepattern = None
158 160 excludepattern = None
159 161 for cap in bundlecaps or []:
160 162 if cap.startswith(b"includepattern="):
161 163 raw = cap[len(b"includepattern=") :]
162 164 if raw:
163 165 includepattern = raw.split(b'\0')
164 166 elif cap.startswith(b"excludepattern="):
165 167 raw = cap[len(b"excludepattern=") :]
166 168 if raw:
167 169 excludepattern = raw.split(b'\0')
168 170 if includepattern or excludepattern:
169 171 repo.shallowmatch = match.match(
170 172 repo.root, b'', None, includepattern, excludepattern
171 173 )
172 174 else:
173 175 repo.shallowmatch = match.always()
174 176 return orig(repo, outgoing, version, source, *args, **kwargs)
175 177 finally:
176 178 repo.shallowmatch = original
177 179
178 180
179 def addchangegroupfiles(orig, repo, source, revmap, trp, expectedfiles, *args):
181 def addchangegroupfiles(
182 orig, repo, source, revmap, trp, expectedfiles, *args, **kwargs
183 ):
180 184 if not shallowutil.isenabled(repo):
181 return orig(repo, source, revmap, trp, expectedfiles, *args)
185 return orig(repo, source, revmap, trp, expectedfiles, *args, **kwargs)
182 186
183 187 newfiles = 0
184 188 visited = set()
185 189 revisiondatas = {}
186 190 queue = []
187 191
188 192 # Normal Mercurial processes each file one at a time, adding all
189 193 # the new revisions for that file at once. In remotefilelog a file
190 194 # revision may depend on a different file's revision (in the case
191 195 # of a rename/copy), so we must lay all revisions down across all
192 196 # files in topological order.
193 197
194 198 # read all the file chunks but don't add them
195 199 progress = repo.ui.makeprogress(_(b'files'), total=expectedfiles)
196 200 while True:
197 201 chunkdata = source.filelogheader()
198 202 if not chunkdata:
199 203 break
200 204 f = chunkdata[b"filename"]
201 205 repo.ui.debug(b"adding %s revisions\n" % f)
202 206 progress.increment()
203 207
204 208 if not repo.shallowmatch(f):
205 209 fl = repo.file(f)
206 210 deltas = source.deltaiter()
207 211 fl.addgroup(deltas, revmap, trp)
208 212 continue
209 213
210 214 chain = None
211 215 while True:
212 216 # returns: (node, p1, p2, cs, deltabase, delta, flags) or None
213 217 revisiondata = source.deltachunk(chain)
214 218 if not revisiondata:
215 219 break
216 220
217 221 chain = revisiondata[0]
218 222
219 223 revisiondatas[(f, chain)] = revisiondata
220 224 queue.append((f, chain))
221 225
222 226 if f not in visited:
223 227 newfiles += 1
224 228 visited.add(f)
225 229
226 230 if chain is None:
227 231 raise error.Abort(_(b"received file revlog group is empty"))
228 232
229 233 processed = set()
230 234
231 235 def available(f, node, depf, depnode):
232 236 if depnode != nullid and (depf, depnode) not in processed:
233 237 if not (depf, depnode) in revisiondatas:
234 238 # It's not in the changegroup, assume it's already
235 239 # in the repo
236 240 return True
237 241 # re-add self to queue
238 242 queue.insert(0, (f, node))
239 243 # add dependency in front
240 244 queue.insert(0, (depf, depnode))
241 245 return False
242 246 return True
243 247
244 248 skipcount = 0
245 249
246 250 # Prefetch the non-bundled revisions that we will need
247 251 prefetchfiles = []
248 252 for f, node in queue:
249 253 revisiondata = revisiondatas[(f, node)]
250 254 # revisiondata: (node, p1, p2, cs, deltabase, delta, flags)
251 255 dependents = [revisiondata[1], revisiondata[2], revisiondata[4]]
252 256
253 257 for dependent in dependents:
254 258 if dependent == nullid or (f, dependent) in revisiondatas:
255 259 continue
256 260 prefetchfiles.append((f, hex(dependent)))
257 261
258 262 repo.fileservice.prefetch(prefetchfiles)
259 263
260 264 # Apply the revisions in topological order such that a revision
261 265 # is only written once it's deltabase and parents have been written.
262 266 while queue:
263 267 f, node = queue.pop(0)
264 268 if (f, node) in processed:
265 269 continue
266 270
267 271 skipcount += 1
268 272 if skipcount > len(queue) + 1:
269 273 raise error.Abort(_(b"circular node dependency"))
270 274
271 275 fl = repo.file(f)
272 276
273 277 revisiondata = revisiondatas[(f, node)]
274 278 # revisiondata: (node, p1, p2, cs, deltabase, delta, flags)
275 node, p1, p2, linknode, deltabase, delta, flags = revisiondata
279 node, p1, p2, linknode, deltabase, delta, flags, sidedata = revisiondata
276 280
277 281 if not available(f, node, f, deltabase):
278 282 continue
279 283
280 284 base = fl.rawdata(deltabase)
281 285 text = mdiff.patch(base, delta)
282 286 if not isinstance(text, bytes):
283 287 text = bytes(text)
284 288
285 289 meta, text = shallowutil.parsemeta(text)
286 290 if b'copy' in meta:
287 291 copyfrom = meta[b'copy']
288 292 copynode = bin(meta[b'copyrev'])
289 293 if not available(f, node, copyfrom, copynode):
290 294 continue
291 295
292 296 for p in [p1, p2]:
293 297 if p != nullid:
294 298 if not available(f, node, f, p):
295 299 continue
296 300
297 301 fl.add(text, meta, trp, linknode, p1, p2)
298 302 processed.add((f, node))
299 303 skipcount = 0
300 304
301 305 progress.complete()
302 306
303 307 return len(revisiondatas), newfiles
@@ -1,1301 +1,1310 b''
1 1 # sqlitestore.py - Storage backend that uses SQLite
2 2 #
3 3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.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 """store repository data in SQLite (EXPERIMENTAL)
9 9
10 10 The sqlitestore extension enables the storage of repository data in SQLite.
11 11
12 12 This extension is HIGHLY EXPERIMENTAL. There are NO BACKWARDS COMPATIBILITY
13 13 GUARANTEES. This means that repositories created with this extension may
14 14 only be usable with the exact version of this extension/Mercurial that was
15 15 used. The extension attempts to enforce this in order to prevent repository
16 16 corruption.
17 17
18 18 In addition, several features are not yet supported or have known bugs:
19 19
20 20 * Only some data is stored in SQLite. Changeset, manifest, and other repository
21 21 data is not yet stored in SQLite.
22 22 * Transactions are not robust. If the process is aborted at the right time
23 23 during transaction close/rollback, the repository could be in an inconsistent
24 24 state. This problem will diminish once all repository data is tracked by
25 25 SQLite.
26 26 * Bundle repositories do not work (the ability to use e.g.
27 27 `hg -R <bundle-file> log` to automatically overlay a bundle on top of the
28 28 existing repository).
29 29 * Various other features don't work.
30 30
31 31 This extension should work for basic clone/pull, update, and commit workflows.
32 32 Some history rewriting operations may fail due to lack of support for bundle
33 33 repositories.
34 34
35 35 To use, activate the extension and set the ``storage.new-repo-backend`` config
36 36 option to ``sqlite`` to enable new repositories to use SQLite for storage.
37 37 """
38 38
39 39 # To run the test suite with repos using SQLite by default, execute the
40 40 # following:
41 41 #
42 42 # HGREPOFEATURES="sqlitestore" run-tests.py \
43 43 # --extra-config-opt extensions.sqlitestore= \
44 44 # --extra-config-opt storage.new-repo-backend=sqlite
45 45
46 46 from __future__ import absolute_import
47 47
48 48 import sqlite3
49 49 import struct
50 50 import threading
51 51 import zlib
52 52
53 53 from mercurial.i18n import _
54 54 from mercurial.node import (
55 55 nullid,
56 56 nullrev,
57 57 short,
58 58 )
59 59 from mercurial.thirdparty import attr
60 60 from mercurial import (
61 61 ancestor,
62 62 dagop,
63 63 encoding,
64 64 error,
65 65 extensions,
66 66 localrepo,
67 67 mdiff,
68 68 pycompat,
69 69 registrar,
70 70 requirements,
71 71 util,
72 72 verify,
73 73 )
74 74 from mercurial.interfaces import (
75 75 repository,
76 76 util as interfaceutil,
77 77 )
78 78 from mercurial.utils import (
79 79 hashutil,
80 80 storageutil,
81 81 )
82 82
83 83 try:
84 84 from mercurial import zstd
85 85
86 86 zstd.__version__
87 87 except ImportError:
88 88 zstd = None
89 89
90 90 configtable = {}
91 91 configitem = registrar.configitem(configtable)
92 92
93 93 # experimental config: storage.sqlite.compression
94 94 configitem(
95 95 b'storage',
96 96 b'sqlite.compression',
97 97 default=b'zstd' if zstd else b'zlib',
98 98 experimental=True,
99 99 )
100 100
101 101 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
102 102 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
103 103 # be specifying the version(s) of Mercurial they are tested with, or
104 104 # leave the attribute unspecified.
105 105 testedwith = b'ships-with-hg-core'
106 106
107 107 REQUIREMENT = b'exp-sqlite-001'
108 108 REQUIREMENT_ZSTD = b'exp-sqlite-comp-001=zstd'
109 109 REQUIREMENT_ZLIB = b'exp-sqlite-comp-001=zlib'
110 110 REQUIREMENT_NONE = b'exp-sqlite-comp-001=none'
111 111 REQUIREMENT_SHALLOW_FILES = b'exp-sqlite-shallow-files'
112 112
113 113 CURRENT_SCHEMA_VERSION = 1
114 114
115 115 COMPRESSION_NONE = 1
116 116 COMPRESSION_ZSTD = 2
117 117 COMPRESSION_ZLIB = 3
118 118
119 119 FLAG_CENSORED = 1
120 120 FLAG_MISSING_P1 = 2
121 121 FLAG_MISSING_P2 = 4
122 122
123 123 CREATE_SCHEMA = [
124 124 # Deltas are stored as content-indexed blobs.
125 125 # compression column holds COMPRESSION_* constant for how the
126 126 # delta is encoded.
127 127 'CREATE TABLE delta ('
128 128 ' id INTEGER PRIMARY KEY, '
129 129 ' compression INTEGER NOT NULL, '
130 130 ' hash BLOB UNIQUE ON CONFLICT ABORT, '
131 131 ' delta BLOB NOT NULL '
132 132 ')',
133 133 # Tracked paths are denormalized to integers to avoid redundant
134 134 # storage of the path name.
135 135 'CREATE TABLE filepath ('
136 136 ' id INTEGER PRIMARY KEY, '
137 137 ' path BLOB NOT NULL '
138 138 ')',
139 139 'CREATE UNIQUE INDEX filepath_path ON filepath (path)',
140 140 # We have a single table for all file revision data.
141 141 # Each file revision is uniquely described by a (path, rev) and
142 142 # (path, node).
143 143 #
144 144 # Revision data is stored as a pointer to the delta producing this
145 145 # revision and the file revision whose delta should be applied before
146 146 # that one. One can reconstruct the delta chain by recursively following
147 147 # the delta base revision pointers until one encounters NULL.
148 148 #
149 149 # flags column holds bitwise integer flags controlling storage options.
150 150 # These flags are defined by the FLAG_* constants.
151 151 'CREATE TABLE fileindex ('
152 152 ' id INTEGER PRIMARY KEY, '
153 153 ' pathid INTEGER REFERENCES filepath(id), '
154 154 ' revnum INTEGER NOT NULL, '
155 155 ' p1rev INTEGER NOT NULL, '
156 156 ' p2rev INTEGER NOT NULL, '
157 157 ' linkrev INTEGER NOT NULL, '
158 158 ' flags INTEGER NOT NULL, '
159 159 ' deltaid INTEGER REFERENCES delta(id), '
160 160 ' deltabaseid INTEGER REFERENCES fileindex(id), '
161 161 ' node BLOB NOT NULL '
162 162 ')',
163 163 'CREATE UNIQUE INDEX fileindex_pathrevnum '
164 164 ' ON fileindex (pathid, revnum)',
165 165 'CREATE UNIQUE INDEX fileindex_pathnode ON fileindex (pathid, node)',
166 166 # Provide a view over all file data for convenience.
167 167 'CREATE VIEW filedata AS '
168 168 'SELECT '
169 169 ' fileindex.id AS id, '
170 170 ' filepath.id AS pathid, '
171 171 ' filepath.path AS path, '
172 172 ' fileindex.revnum AS revnum, '
173 173 ' fileindex.node AS node, '
174 174 ' fileindex.p1rev AS p1rev, '
175 175 ' fileindex.p2rev AS p2rev, '
176 176 ' fileindex.linkrev AS linkrev, '
177 177 ' fileindex.flags AS flags, '
178 178 ' fileindex.deltaid AS deltaid, '
179 179 ' fileindex.deltabaseid AS deltabaseid '
180 180 'FROM filepath, fileindex '
181 181 'WHERE fileindex.pathid=filepath.id',
182 182 'PRAGMA user_version=%d' % CURRENT_SCHEMA_VERSION,
183 183 ]
184 184
185 185
186 186 def resolvedeltachain(db, pathid, node, revisioncache, stoprids, zstddctx=None):
187 187 """Resolve a delta chain for a file node."""
188 188
189 189 # TODO the "not in ({stops})" here is possibly slowing down the query
190 190 # because it needs to perform the lookup on every recursive invocation.
191 191 # This could possibly be faster if we created a temporary query with
192 192 # baseid "poisoned" to null and limited the recursive filter to
193 193 # "is not null".
194 194 res = db.execute(
195 195 'WITH RECURSIVE '
196 196 ' deltachain(deltaid, baseid) AS ('
197 197 ' SELECT deltaid, deltabaseid FROM fileindex '
198 198 ' WHERE pathid=? AND node=? '
199 199 ' UNION ALL '
200 200 ' SELECT fileindex.deltaid, deltabaseid '
201 201 ' FROM fileindex, deltachain '
202 202 ' WHERE '
203 203 ' fileindex.id=deltachain.baseid '
204 204 ' AND deltachain.baseid IS NOT NULL '
205 205 ' AND fileindex.id NOT IN ({stops}) '
206 206 ' ) '
207 207 'SELECT deltachain.baseid, compression, delta '
208 208 'FROM deltachain, delta '
209 209 'WHERE delta.id=deltachain.deltaid'.format(
210 210 stops=','.join(['?'] * len(stoprids))
211 211 ),
212 212 tuple([pathid, node] + list(stoprids.keys())),
213 213 )
214 214
215 215 deltas = []
216 216 lastdeltabaseid = None
217 217
218 218 for deltabaseid, compression, delta in res:
219 219 lastdeltabaseid = deltabaseid
220 220
221 221 if compression == COMPRESSION_ZSTD:
222 222 delta = zstddctx.decompress(delta)
223 223 elif compression == COMPRESSION_NONE:
224 224 delta = delta
225 225 elif compression == COMPRESSION_ZLIB:
226 226 delta = zlib.decompress(delta)
227 227 else:
228 228 raise SQLiteStoreError(
229 229 b'unhandled compression type: %d' % compression
230 230 )
231 231
232 232 deltas.append(delta)
233 233
234 234 if lastdeltabaseid in stoprids:
235 235 basetext = revisioncache[stoprids[lastdeltabaseid]]
236 236 else:
237 237 basetext = deltas.pop()
238 238
239 239 deltas.reverse()
240 240 fulltext = mdiff.patches(basetext, deltas)
241 241
242 242 # SQLite returns buffer instances for blob columns on Python 2. This
243 243 # type can propagate through the delta application layer. Because
244 244 # downstream callers assume revisions are bytes, cast as needed.
245 245 if not isinstance(fulltext, bytes):
246 246 fulltext = bytes(delta)
247 247
248 248 return fulltext
249 249
250 250
251 251 def insertdelta(db, compression, hash, delta):
252 252 try:
253 253 return db.execute(
254 254 'INSERT INTO delta (compression, hash, delta) VALUES (?, ?, ?)',
255 255 (compression, hash, delta),
256 256 ).lastrowid
257 257 except sqlite3.IntegrityError:
258 258 return db.execute(
259 259 'SELECT id FROM delta WHERE hash=?', (hash,)
260 260 ).fetchone()[0]
261 261
262 262
263 263 class SQLiteStoreError(error.StorageError):
264 264 pass
265 265
266 266
267 267 @attr.s
268 268 class revisionentry(object):
269 269 rid = attr.ib()
270 270 rev = attr.ib()
271 271 node = attr.ib()
272 272 p1rev = attr.ib()
273 273 p2rev = attr.ib()
274 274 p1node = attr.ib()
275 275 p2node = attr.ib()
276 276 linkrev = attr.ib()
277 277 flags = attr.ib()
278 278
279 279
280 280 @interfaceutil.implementer(repository.irevisiondelta)
281 281 @attr.s(slots=True)
282 282 class sqliterevisiondelta(object):
283 283 node = attr.ib()
284 284 p1node = attr.ib()
285 285 p2node = attr.ib()
286 286 basenode = attr.ib()
287 287 flags = attr.ib()
288 288 baserevisionsize = attr.ib()
289 289 revision = attr.ib()
290 290 delta = attr.ib()
291 291 linknode = attr.ib(default=None)
292 292
293 293
294 294 @interfaceutil.implementer(repository.iverifyproblem)
295 295 @attr.s(frozen=True)
296 296 class sqliteproblem(object):
297 297 warning = attr.ib(default=None)
298 298 error = attr.ib(default=None)
299 299 node = attr.ib(default=None)
300 300
301 301
302 302 @interfaceutil.implementer(repository.ifilestorage)
303 303 class sqlitefilestore(object):
304 304 """Implements storage for an individual tracked path."""
305 305
306 306 def __init__(self, db, path, compression):
307 307 self._db = db
308 308 self._path = path
309 309
310 310 self._pathid = None
311 311
312 312 # revnum -> node
313 313 self._revtonode = {}
314 314 # node -> revnum
315 315 self._nodetorev = {}
316 316 # node -> data structure
317 317 self._revisions = {}
318 318
319 319 self._revisioncache = util.lrucachedict(10)
320 320
321 321 self._compengine = compression
322 322
323 323 if compression == b'zstd':
324 324 self._cctx = zstd.ZstdCompressor(level=3)
325 325 self._dctx = zstd.ZstdDecompressor()
326 326 else:
327 327 self._cctx = None
328 328 self._dctx = None
329 329
330 330 self._refreshindex()
331 331
332 332 def _refreshindex(self):
333 333 self._revtonode = {}
334 334 self._nodetorev = {}
335 335 self._revisions = {}
336 336
337 337 res = list(
338 338 self._db.execute(
339 339 'SELECT id FROM filepath WHERE path=?', (self._path,)
340 340 )
341 341 )
342 342
343 343 if not res:
344 344 self._pathid = None
345 345 return
346 346
347 347 self._pathid = res[0][0]
348 348
349 349 res = self._db.execute(
350 350 'SELECT id, revnum, node, p1rev, p2rev, linkrev, flags '
351 351 'FROM fileindex '
352 352 'WHERE pathid=? '
353 353 'ORDER BY revnum ASC',
354 354 (self._pathid,),
355 355 )
356 356
357 357 for i, row in enumerate(res):
358 358 rid, rev, node, p1rev, p2rev, linkrev, flags = row
359 359
360 360 if i != rev:
361 361 raise SQLiteStoreError(
362 362 _(b'sqlite database has inconsistent revision numbers')
363 363 )
364 364
365 365 if p1rev == nullrev:
366 366 p1node = nullid
367 367 else:
368 368 p1node = self._revtonode[p1rev]
369 369
370 370 if p2rev == nullrev:
371 371 p2node = nullid
372 372 else:
373 373 p2node = self._revtonode[p2rev]
374 374
375 375 entry = revisionentry(
376 376 rid=rid,
377 377 rev=rev,
378 378 node=node,
379 379 p1rev=p1rev,
380 380 p2rev=p2rev,
381 381 p1node=p1node,
382 382 p2node=p2node,
383 383 linkrev=linkrev,
384 384 flags=flags,
385 385 )
386 386
387 387 self._revtonode[rev] = node
388 388 self._nodetorev[node] = rev
389 389 self._revisions[node] = entry
390 390
391 391 # Start of ifileindex interface.
392 392
393 393 def __len__(self):
394 394 return len(self._revisions)
395 395
396 396 def __iter__(self):
397 397 return iter(pycompat.xrange(len(self._revisions)))
398 398
399 399 def hasnode(self, node):
400 400 if node == nullid:
401 401 return False
402 402
403 403 return node in self._nodetorev
404 404
405 405 def revs(self, start=0, stop=None):
406 406 return storageutil.iterrevs(
407 407 len(self._revisions), start=start, stop=stop
408 408 )
409 409
410 410 def parents(self, node):
411 411 if node == nullid:
412 412 return nullid, nullid
413 413
414 414 if node not in self._revisions:
415 415 raise error.LookupError(node, self._path, _(b'no node'))
416 416
417 417 entry = self._revisions[node]
418 418 return entry.p1node, entry.p2node
419 419
420 420 def parentrevs(self, rev):
421 421 if rev == nullrev:
422 422 return nullrev, nullrev
423 423
424 424 if rev not in self._revtonode:
425 425 raise IndexError(rev)
426 426
427 427 entry = self._revisions[self._revtonode[rev]]
428 428 return entry.p1rev, entry.p2rev
429 429
430 430 def rev(self, node):
431 431 if node == nullid:
432 432 return nullrev
433 433
434 434 if node not in self._nodetorev:
435 435 raise error.LookupError(node, self._path, _(b'no node'))
436 436
437 437 return self._nodetorev[node]
438 438
439 439 def node(self, rev):
440 440 if rev == nullrev:
441 441 return nullid
442 442
443 443 if rev not in self._revtonode:
444 444 raise IndexError(rev)
445 445
446 446 return self._revtonode[rev]
447 447
448 448 def lookup(self, node):
449 449 return storageutil.fileidlookup(self, node, self._path)
450 450
451 451 def linkrev(self, rev):
452 452 if rev == nullrev:
453 453 return nullrev
454 454
455 455 if rev not in self._revtonode:
456 456 raise IndexError(rev)
457 457
458 458 entry = self._revisions[self._revtonode[rev]]
459 459 return entry.linkrev
460 460
461 461 def iscensored(self, rev):
462 462 if rev == nullrev:
463 463 return False
464 464
465 465 if rev not in self._revtonode:
466 466 raise IndexError(rev)
467 467
468 468 return self._revisions[self._revtonode[rev]].flags & FLAG_CENSORED
469 469
470 470 def commonancestorsheads(self, node1, node2):
471 471 rev1 = self.rev(node1)
472 472 rev2 = self.rev(node2)
473 473
474 474 ancestors = ancestor.commonancestorsheads(self.parentrevs, rev1, rev2)
475 475 return pycompat.maplist(self.node, ancestors)
476 476
477 477 def descendants(self, revs):
478 478 # TODO we could implement this using a recursive SQL query, which
479 479 # might be faster.
480 480 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
481 481
482 482 def heads(self, start=None, stop=None):
483 483 if start is None and stop is None:
484 484 if not len(self):
485 485 return [nullid]
486 486
487 487 startrev = self.rev(start) if start is not None else nullrev
488 488 stoprevs = {self.rev(n) for n in stop or []}
489 489
490 490 revs = dagop.headrevssubset(
491 491 self.revs, self.parentrevs, startrev=startrev, stoprevs=stoprevs
492 492 )
493 493
494 494 return [self.node(rev) for rev in revs]
495 495
496 496 def children(self, node):
497 497 rev = self.rev(node)
498 498
499 499 res = self._db.execute(
500 500 'SELECT'
501 501 ' node '
502 502 ' FROM filedata '
503 503 ' WHERE path=? AND (p1rev=? OR p2rev=?) '
504 504 ' ORDER BY revnum ASC',
505 505 (self._path, rev, rev),
506 506 )
507 507
508 508 return [row[0] for row in res]
509 509
510 510 # End of ifileindex interface.
511 511
512 512 # Start of ifiledata interface.
513 513
514 514 def size(self, rev):
515 515 if rev == nullrev:
516 516 return 0
517 517
518 518 if rev not in self._revtonode:
519 519 raise IndexError(rev)
520 520
521 521 node = self._revtonode[rev]
522 522
523 523 if self.renamed(node):
524 524 return len(self.read(node))
525 525
526 526 return len(self.revision(node))
527 527
528 528 def revision(self, node, raw=False, _verifyhash=True):
529 529 if node in (nullid, nullrev):
530 530 return b''
531 531
532 532 if isinstance(node, int):
533 533 node = self.node(node)
534 534
535 535 if node not in self._nodetorev:
536 536 raise error.LookupError(node, self._path, _(b'no node'))
537 537
538 538 if node in self._revisioncache:
539 539 return self._revisioncache[node]
540 540
541 541 # Because we have a fulltext revision cache, we are able to
542 542 # short-circuit delta chain traversal and decompression as soon as
543 543 # we encounter a revision in the cache.
544 544
545 545 stoprids = {self._revisions[n].rid: n for n in self._revisioncache}
546 546
547 547 if not stoprids:
548 548 stoprids[-1] = None
549 549
550 550 fulltext = resolvedeltachain(
551 551 self._db,
552 552 self._pathid,
553 553 node,
554 554 self._revisioncache,
555 555 stoprids,
556 556 zstddctx=self._dctx,
557 557 )
558 558
559 559 # Don't verify hashes if parent nodes were rewritten, as the hash
560 560 # wouldn't verify.
561 561 if self._revisions[node].flags & (FLAG_MISSING_P1 | FLAG_MISSING_P2):
562 562 _verifyhash = False
563 563
564 564 if _verifyhash:
565 565 self._checkhash(fulltext, node)
566 566 self._revisioncache[node] = fulltext
567 567
568 568 return fulltext
569 569
570 570 def rawdata(self, *args, **kwargs):
571 571 return self.revision(*args, **kwargs)
572 572
573 573 def read(self, node):
574 574 return storageutil.filtermetadata(self.revision(node))
575 575
576 576 def renamed(self, node):
577 577 return storageutil.filerevisioncopied(self, node)
578 578
579 579 def cmp(self, node, fulltext):
580 580 return not storageutil.filedataequivalent(self, node, fulltext)
581 581
582 582 def emitrevisions(
583 583 self,
584 584 nodes,
585 585 nodesorder=None,
586 586 revisiondata=False,
587 587 assumehaveparentrevisions=False,
588 588 deltamode=repository.CG_DELTAMODE_STD,
589 589 ):
590 590 if nodesorder not in (b'nodes', b'storage', b'linear', None):
591 591 raise error.ProgrammingError(
592 592 b'unhandled value for nodesorder: %s' % nodesorder
593 593 )
594 594
595 595 nodes = [n for n in nodes if n != nullid]
596 596
597 597 if not nodes:
598 598 return
599 599
600 600 # TODO perform in a single query.
601 601 res = self._db.execute(
602 602 'SELECT revnum, deltaid FROM fileindex '
603 603 'WHERE pathid=? '
604 604 ' AND node in (%s)' % (','.join(['?'] * len(nodes))),
605 605 tuple([self._pathid] + nodes),
606 606 )
607 607
608 608 deltabases = {}
609 609
610 610 for rev, deltaid in res:
611 611 res = self._db.execute(
612 612 'SELECT revnum from fileindex WHERE pathid=? AND deltaid=?',
613 613 (self._pathid, deltaid),
614 614 )
615 615 deltabases[rev] = res.fetchone()[0]
616 616
617 617 # TODO define revdifffn so we can use delta from storage.
618 618 for delta in storageutil.emitrevisions(
619 619 self,
620 620 nodes,
621 621 nodesorder,
622 622 sqliterevisiondelta,
623 623 deltaparentfn=deltabases.__getitem__,
624 624 revisiondata=revisiondata,
625 625 assumehaveparentrevisions=assumehaveparentrevisions,
626 626 deltamode=deltamode,
627 627 ):
628 628
629 629 yield delta
630 630
631 631 # End of ifiledata interface.
632 632
633 633 # Start of ifilemutation interface.
634 634
635 635 def add(self, filedata, meta, transaction, linkrev, p1, p2):
636 636 if meta or filedata.startswith(b'\x01\n'):
637 637 filedata = storageutil.packmeta(meta, filedata)
638 638
639 639 rev = self.addrevision(filedata, transaction, linkrev, p1, p2)
640 640 return self.node(rev)
641 641
642 642 def addrevision(
643 643 self,
644 644 revisiondata,
645 645 transaction,
646 646 linkrev,
647 647 p1,
648 648 p2,
649 649 node=None,
650 650 flags=0,
651 651 cachedelta=None,
652 652 ):
653 653 if flags:
654 654 raise SQLiteStoreError(_(b'flags not supported on revisions'))
655 655
656 656 validatehash = node is not None
657 657 node = node or storageutil.hashrevisionsha1(revisiondata, p1, p2)
658 658
659 659 if validatehash:
660 660 self._checkhash(revisiondata, node, p1, p2)
661 661
662 662 rev = self._nodetorev.get(node)
663 663 if rev is not None:
664 664 return rev
665 665
666 666 rev = self._addrawrevision(
667 667 node, revisiondata, transaction, linkrev, p1, p2
668 668 )
669 669
670 670 self._revisioncache[node] = revisiondata
671 671 return rev
672 672
673 673 def addgroup(
674 674 self,
675 675 deltas,
676 676 linkmapper,
677 677 transaction,
678 678 addrevisioncb=None,
679 679 duplicaterevisioncb=None,
680 680 maybemissingparents=False,
681 681 ):
682 682 empty = True
683 683
684 for node, p1, p2, linknode, deltabase, delta, wireflags in deltas:
684 for (
685 node,
686 p1,
687 p2,
688 linknode,
689 deltabase,
690 delta,
691 wireflags,
692 sidedata,
693 ) in deltas:
685 694 storeflags = 0
686 695
687 696 if wireflags & repository.REVISION_FLAG_CENSORED:
688 697 storeflags |= FLAG_CENSORED
689 698
690 699 if wireflags & ~repository.REVISION_FLAG_CENSORED:
691 700 raise SQLiteStoreError(b'unhandled revision flag')
692 701
693 702 if maybemissingparents:
694 703 if p1 != nullid and not self.hasnode(p1):
695 704 p1 = nullid
696 705 storeflags |= FLAG_MISSING_P1
697 706
698 707 if p2 != nullid and not self.hasnode(p2):
699 708 p2 = nullid
700 709 storeflags |= FLAG_MISSING_P2
701 710
702 711 baserev = self.rev(deltabase)
703 712
704 713 # If base is censored, delta must be full replacement in a single
705 714 # patch operation.
706 715 if baserev != nullrev and self.iscensored(baserev):
707 716 hlen = struct.calcsize(b'>lll')
708 717 oldlen = len(self.rawdata(deltabase, _verifyhash=False))
709 718 newlen = len(delta) - hlen
710 719
711 720 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
712 721 raise error.CensoredBaseError(self._path, deltabase)
713 722
714 723 if not (storeflags & FLAG_CENSORED) and storageutil.deltaiscensored(
715 724 delta, baserev, lambda x: len(self.rawdata(x))
716 725 ):
717 726 storeflags |= FLAG_CENSORED
718 727
719 728 linkrev = linkmapper(linknode)
720 729
721 730 if node in self._revisions:
722 731 # Possibly reset parents to make them proper.
723 732 entry = self._revisions[node]
724 733
725 734 if entry.flags & FLAG_MISSING_P1 and p1 != nullid:
726 735 entry.p1node = p1
727 736 entry.p1rev = self._nodetorev[p1]
728 737 entry.flags &= ~FLAG_MISSING_P1
729 738
730 739 self._db.execute(
731 740 'UPDATE fileindex SET p1rev=?, flags=? WHERE id=?',
732 741 (self._nodetorev[p1], entry.flags, entry.rid),
733 742 )
734 743
735 744 if entry.flags & FLAG_MISSING_P2 and p2 != nullid:
736 745 entry.p2node = p2
737 746 entry.p2rev = self._nodetorev[p2]
738 747 entry.flags &= ~FLAG_MISSING_P2
739 748
740 749 self._db.execute(
741 750 'UPDATE fileindex SET p2rev=?, flags=? WHERE id=?',
742 751 (self._nodetorev[p1], entry.flags, entry.rid),
743 752 )
744 753
745 754 if duplicaterevisioncb:
746 755 duplicaterevisioncb(self, self.rev(node))
747 756 empty = False
748 757 continue
749 758
750 759 if deltabase == nullid:
751 760 text = mdiff.patch(b'', delta)
752 761 storedelta = None
753 762 else:
754 763 text = None
755 764 storedelta = (deltabase, delta)
756 765
757 766 rev = self._addrawrevision(
758 767 node,
759 768 text,
760 769 transaction,
761 770 linkrev,
762 771 p1,
763 772 p2,
764 773 storedelta=storedelta,
765 774 flags=storeflags,
766 775 )
767 776
768 777 if addrevisioncb:
769 778 addrevisioncb(self, rev)
770 779 empty = False
771 780
772 781 return not empty
773 782
774 783 def censorrevision(self, tr, censornode, tombstone=b''):
775 784 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
776 785
777 786 # This restriction is cargo culted from revlogs and makes no sense for
778 787 # SQLite, since columns can be resized at will.
779 788 if len(tombstone) > len(self.rawdata(censornode)):
780 789 raise error.Abort(
781 790 _(b'censor tombstone must be no longer than censored data')
782 791 )
783 792
784 793 # We need to replace the censored revision's data with the tombstone.
785 794 # But replacing that data will have implications for delta chains that
786 795 # reference it.
787 796 #
788 797 # While "better," more complex strategies are possible, we do something
789 798 # simple: we find delta chain children of the censored revision and we
790 799 # replace those incremental deltas with fulltexts of their corresponding
791 800 # revision. Then we delete the now-unreferenced delta and original
792 801 # revision and insert a replacement.
793 802
794 803 # Find the delta to be censored.
795 804 censoreddeltaid = self._db.execute(
796 805 'SELECT deltaid FROM fileindex WHERE id=?',
797 806 (self._revisions[censornode].rid,),
798 807 ).fetchone()[0]
799 808
800 809 # Find all its delta chain children.
801 810 # TODO once we support storing deltas for !files, we'll need to look
802 811 # for those delta chains too.
803 812 rows = list(
804 813 self._db.execute(
805 814 'SELECT id, pathid, node FROM fileindex '
806 815 'WHERE deltabaseid=? OR deltaid=?',
807 816 (censoreddeltaid, censoreddeltaid),
808 817 )
809 818 )
810 819
811 820 for row in rows:
812 821 rid, pathid, node = row
813 822
814 823 fulltext = resolvedeltachain(
815 824 self._db, pathid, node, {}, {-1: None}, zstddctx=self._dctx
816 825 )
817 826
818 827 deltahash = hashutil.sha1(fulltext).digest()
819 828
820 829 if self._compengine == b'zstd':
821 830 deltablob = self._cctx.compress(fulltext)
822 831 compression = COMPRESSION_ZSTD
823 832 elif self._compengine == b'zlib':
824 833 deltablob = zlib.compress(fulltext)
825 834 compression = COMPRESSION_ZLIB
826 835 elif self._compengine == b'none':
827 836 deltablob = fulltext
828 837 compression = COMPRESSION_NONE
829 838 else:
830 839 raise error.ProgrammingError(
831 840 b'unhandled compression engine: %s' % self._compengine
832 841 )
833 842
834 843 if len(deltablob) >= len(fulltext):
835 844 deltablob = fulltext
836 845 compression = COMPRESSION_NONE
837 846
838 847 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
839 848
840 849 self._db.execute(
841 850 'UPDATE fileindex SET deltaid=?, deltabaseid=NULL '
842 851 'WHERE id=?',
843 852 (deltaid, rid),
844 853 )
845 854
846 855 # Now create the tombstone delta and replace the delta on the censored
847 856 # node.
848 857 deltahash = hashutil.sha1(tombstone).digest()
849 858 tombstonedeltaid = insertdelta(
850 859 self._db, COMPRESSION_NONE, deltahash, tombstone
851 860 )
852 861
853 862 flags = self._revisions[censornode].flags
854 863 flags |= FLAG_CENSORED
855 864
856 865 self._db.execute(
857 866 'UPDATE fileindex SET flags=?, deltaid=?, deltabaseid=NULL '
858 867 'WHERE pathid=? AND node=?',
859 868 (flags, tombstonedeltaid, self._pathid, censornode),
860 869 )
861 870
862 871 self._db.execute('DELETE FROM delta WHERE id=?', (censoreddeltaid,))
863 872
864 873 self._refreshindex()
865 874 self._revisioncache.clear()
866 875
867 876 def getstrippoint(self, minlink):
868 877 return storageutil.resolvestripinfo(
869 878 minlink,
870 879 len(self) - 1,
871 880 [self.rev(n) for n in self.heads()],
872 881 self.linkrev,
873 882 self.parentrevs,
874 883 )
875 884
876 885 def strip(self, minlink, transaction):
877 886 if not len(self):
878 887 return
879 888
880 889 rev, _ignored = self.getstrippoint(minlink)
881 890
882 891 if rev == len(self):
883 892 return
884 893
885 894 for rev in self.revs(rev):
886 895 self._db.execute(
887 896 'DELETE FROM fileindex WHERE pathid=? AND node=?',
888 897 (self._pathid, self.node(rev)),
889 898 )
890 899
891 900 # TODO how should we garbage collect data in delta table?
892 901
893 902 self._refreshindex()
894 903
895 904 # End of ifilemutation interface.
896 905
897 906 # Start of ifilestorage interface.
898 907
899 908 def files(self):
900 909 return []
901 910
902 911 def storageinfo(
903 912 self,
904 913 exclusivefiles=False,
905 914 sharedfiles=False,
906 915 revisionscount=False,
907 916 trackedsize=False,
908 917 storedsize=False,
909 918 ):
910 919 d = {}
911 920
912 921 if exclusivefiles:
913 922 d[b'exclusivefiles'] = []
914 923
915 924 if sharedfiles:
916 925 # TODO list sqlite file(s) here.
917 926 d[b'sharedfiles'] = []
918 927
919 928 if revisionscount:
920 929 d[b'revisionscount'] = len(self)
921 930
922 931 if trackedsize:
923 932 d[b'trackedsize'] = sum(
924 933 len(self.revision(node)) for node in self._nodetorev
925 934 )
926 935
927 936 if storedsize:
928 937 # TODO implement this?
929 938 d[b'storedsize'] = None
930 939
931 940 return d
932 941
933 942 def verifyintegrity(self, state):
934 943 state[b'skipread'] = set()
935 944
936 945 for rev in self:
937 946 node = self.node(rev)
938 947
939 948 try:
940 949 self.revision(node)
941 950 except Exception as e:
942 951 yield sqliteproblem(
943 952 error=_(b'unpacking %s: %s') % (short(node), e), node=node
944 953 )
945 954
946 955 state[b'skipread'].add(node)
947 956
948 957 # End of ifilestorage interface.
949 958
950 959 def _checkhash(self, fulltext, node, p1=None, p2=None):
951 960 if p1 is None and p2 is None:
952 961 p1, p2 = self.parents(node)
953 962
954 963 if node == storageutil.hashrevisionsha1(fulltext, p1, p2):
955 964 return
956 965
957 966 try:
958 967 del self._revisioncache[node]
959 968 except KeyError:
960 969 pass
961 970
962 971 if storageutil.iscensoredtext(fulltext):
963 972 raise error.CensoredNodeError(self._path, node, fulltext)
964 973
965 974 raise SQLiteStoreError(_(b'integrity check failed on %s') % self._path)
966 975
967 976 def _addrawrevision(
968 977 self,
969 978 node,
970 979 revisiondata,
971 980 transaction,
972 981 linkrev,
973 982 p1,
974 983 p2,
975 984 storedelta=None,
976 985 flags=0,
977 986 ):
978 987 if self._pathid is None:
979 988 res = self._db.execute(
980 989 'INSERT INTO filepath (path) VALUES (?)', (self._path,)
981 990 )
982 991 self._pathid = res.lastrowid
983 992
984 993 # For simplicity, always store a delta against p1.
985 994 # TODO we need a lot more logic here to make behavior reasonable.
986 995
987 996 if storedelta:
988 997 deltabase, delta = storedelta
989 998
990 999 if isinstance(deltabase, int):
991 1000 deltabase = self.node(deltabase)
992 1001
993 1002 else:
994 1003 assert revisiondata is not None
995 1004 deltabase = p1
996 1005
997 1006 if deltabase == nullid:
998 1007 delta = revisiondata
999 1008 else:
1000 1009 delta = mdiff.textdiff(
1001 1010 self.revision(self.rev(deltabase)), revisiondata
1002 1011 )
1003 1012
1004 1013 # File index stores a pointer to its delta and the parent delta.
1005 1014 # The parent delta is stored via a pointer to the fileindex PK.
1006 1015 if deltabase == nullid:
1007 1016 baseid = None
1008 1017 else:
1009 1018 baseid = self._revisions[deltabase].rid
1010 1019
1011 1020 # Deltas are stored with a hash of their content. This allows
1012 1021 # us to de-duplicate. The table is configured to ignore conflicts
1013 1022 # and it is faster to just insert and silently noop than to look
1014 1023 # first.
1015 1024 deltahash = hashutil.sha1(delta).digest()
1016 1025
1017 1026 if self._compengine == b'zstd':
1018 1027 deltablob = self._cctx.compress(delta)
1019 1028 compression = COMPRESSION_ZSTD
1020 1029 elif self._compengine == b'zlib':
1021 1030 deltablob = zlib.compress(delta)
1022 1031 compression = COMPRESSION_ZLIB
1023 1032 elif self._compengine == b'none':
1024 1033 deltablob = delta
1025 1034 compression = COMPRESSION_NONE
1026 1035 else:
1027 1036 raise error.ProgrammingError(
1028 1037 b'unhandled compression engine: %s' % self._compengine
1029 1038 )
1030 1039
1031 1040 # Don't store compressed data if it isn't practical.
1032 1041 if len(deltablob) >= len(delta):
1033 1042 deltablob = delta
1034 1043 compression = COMPRESSION_NONE
1035 1044
1036 1045 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
1037 1046
1038 1047 rev = len(self)
1039 1048
1040 1049 if p1 == nullid:
1041 1050 p1rev = nullrev
1042 1051 else:
1043 1052 p1rev = self._nodetorev[p1]
1044 1053
1045 1054 if p2 == nullid:
1046 1055 p2rev = nullrev
1047 1056 else:
1048 1057 p2rev = self._nodetorev[p2]
1049 1058
1050 1059 rid = self._db.execute(
1051 1060 'INSERT INTO fileindex ('
1052 1061 ' pathid, revnum, node, p1rev, p2rev, linkrev, flags, '
1053 1062 ' deltaid, deltabaseid) '
1054 1063 ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
1055 1064 (
1056 1065 self._pathid,
1057 1066 rev,
1058 1067 node,
1059 1068 p1rev,
1060 1069 p2rev,
1061 1070 linkrev,
1062 1071 flags,
1063 1072 deltaid,
1064 1073 baseid,
1065 1074 ),
1066 1075 ).lastrowid
1067 1076
1068 1077 entry = revisionentry(
1069 1078 rid=rid,
1070 1079 rev=rev,
1071 1080 node=node,
1072 1081 p1rev=p1rev,
1073 1082 p2rev=p2rev,
1074 1083 p1node=p1,
1075 1084 p2node=p2,
1076 1085 linkrev=linkrev,
1077 1086 flags=flags,
1078 1087 )
1079 1088
1080 1089 self._nodetorev[node] = rev
1081 1090 self._revtonode[rev] = node
1082 1091 self._revisions[node] = entry
1083 1092
1084 1093 return rev
1085 1094
1086 1095
1087 1096 class sqliterepository(localrepo.localrepository):
1088 1097 def cancopy(self):
1089 1098 return False
1090 1099
1091 1100 def transaction(self, *args, **kwargs):
1092 1101 current = self.currenttransaction()
1093 1102
1094 1103 tr = super(sqliterepository, self).transaction(*args, **kwargs)
1095 1104
1096 1105 if current:
1097 1106 return tr
1098 1107
1099 1108 self._dbconn.execute('BEGIN TRANSACTION')
1100 1109
1101 1110 def committransaction(_):
1102 1111 self._dbconn.commit()
1103 1112
1104 1113 tr.addfinalize(b'sqlitestore', committransaction)
1105 1114
1106 1115 return tr
1107 1116
1108 1117 @property
1109 1118 def _dbconn(self):
1110 1119 # SQLite connections can only be used on the thread that created
1111 1120 # them. In most cases, this "just works." However, hgweb uses
1112 1121 # multiple threads.
1113 1122 tid = threading.current_thread().ident
1114 1123
1115 1124 if self._db:
1116 1125 if self._db[0] == tid:
1117 1126 return self._db[1]
1118 1127
1119 1128 db = makedb(self.svfs.join(b'db.sqlite'))
1120 1129 self._db = (tid, db)
1121 1130
1122 1131 return db
1123 1132
1124 1133
1125 1134 def makedb(path):
1126 1135 """Construct a database handle for a database at path."""
1127 1136
1128 1137 db = sqlite3.connect(encoding.strfromlocal(path))
1129 1138 db.text_factory = bytes
1130 1139
1131 1140 res = db.execute('PRAGMA user_version').fetchone()[0]
1132 1141
1133 1142 # New database.
1134 1143 if res == 0:
1135 1144 for statement in CREATE_SCHEMA:
1136 1145 db.execute(statement)
1137 1146
1138 1147 db.commit()
1139 1148
1140 1149 elif res == CURRENT_SCHEMA_VERSION:
1141 1150 pass
1142 1151
1143 1152 else:
1144 1153 raise error.Abort(_(b'sqlite database has unrecognized version'))
1145 1154
1146 1155 db.execute('PRAGMA journal_mode=WAL')
1147 1156
1148 1157 return db
1149 1158
1150 1159
1151 1160 def featuresetup(ui, supported):
1152 1161 supported.add(REQUIREMENT)
1153 1162
1154 1163 if zstd:
1155 1164 supported.add(REQUIREMENT_ZSTD)
1156 1165
1157 1166 supported.add(REQUIREMENT_ZLIB)
1158 1167 supported.add(REQUIREMENT_NONE)
1159 1168 supported.add(REQUIREMENT_SHALLOW_FILES)
1160 1169 supported.add(requirements.NARROW_REQUIREMENT)
1161 1170
1162 1171
1163 1172 def newreporequirements(orig, ui, createopts):
1164 1173 if createopts[b'backend'] != b'sqlite':
1165 1174 return orig(ui, createopts)
1166 1175
1167 1176 # This restriction can be lifted once we have more confidence.
1168 1177 if b'sharedrepo' in createopts:
1169 1178 raise error.Abort(
1170 1179 _(b'shared repositories not supported with SQLite store')
1171 1180 )
1172 1181
1173 1182 # This filtering is out of an abundance of caution: we want to ensure
1174 1183 # we honor creation options and we do that by annotating exactly the
1175 1184 # creation options we recognize.
1176 1185 known = {
1177 1186 b'narrowfiles',
1178 1187 b'backend',
1179 1188 b'shallowfilestore',
1180 1189 }
1181 1190
1182 1191 unsupported = set(createopts) - known
1183 1192 if unsupported:
1184 1193 raise error.Abort(
1185 1194 _(b'SQLite store does not support repo creation option: %s')
1186 1195 % b', '.join(sorted(unsupported))
1187 1196 )
1188 1197
1189 1198 # Since we're a hybrid store that still relies on revlogs, we fall back
1190 1199 # to using the revlogv1 backend's storage requirements then adding our
1191 1200 # own requirement.
1192 1201 createopts[b'backend'] = b'revlogv1'
1193 1202 requirements = orig(ui, createopts)
1194 1203 requirements.add(REQUIREMENT)
1195 1204
1196 1205 compression = ui.config(b'storage', b'sqlite.compression')
1197 1206
1198 1207 if compression == b'zstd' and not zstd:
1199 1208 raise error.Abort(
1200 1209 _(
1201 1210 b'storage.sqlite.compression set to "zstd" but '
1202 1211 b'zstandard compression not available to this '
1203 1212 b'Mercurial install'
1204 1213 )
1205 1214 )
1206 1215
1207 1216 if compression == b'zstd':
1208 1217 requirements.add(REQUIREMENT_ZSTD)
1209 1218 elif compression == b'zlib':
1210 1219 requirements.add(REQUIREMENT_ZLIB)
1211 1220 elif compression == b'none':
1212 1221 requirements.add(REQUIREMENT_NONE)
1213 1222 else:
1214 1223 raise error.Abort(
1215 1224 _(
1216 1225 b'unknown compression engine defined in '
1217 1226 b'storage.sqlite.compression: %s'
1218 1227 )
1219 1228 % compression
1220 1229 )
1221 1230
1222 1231 if createopts.get(b'shallowfilestore'):
1223 1232 requirements.add(REQUIREMENT_SHALLOW_FILES)
1224 1233
1225 1234 return requirements
1226 1235
1227 1236
1228 1237 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
1229 1238 class sqlitefilestorage(object):
1230 1239 """Repository file storage backed by SQLite."""
1231 1240
1232 1241 def file(self, path):
1233 1242 if path[0] == b'/':
1234 1243 path = path[1:]
1235 1244
1236 1245 if REQUIREMENT_ZSTD in self.requirements:
1237 1246 compression = b'zstd'
1238 1247 elif REQUIREMENT_ZLIB in self.requirements:
1239 1248 compression = b'zlib'
1240 1249 elif REQUIREMENT_NONE in self.requirements:
1241 1250 compression = b'none'
1242 1251 else:
1243 1252 raise error.Abort(
1244 1253 _(
1245 1254 b'unable to determine what compression engine '
1246 1255 b'to use for SQLite storage'
1247 1256 )
1248 1257 )
1249 1258
1250 1259 return sqlitefilestore(self._dbconn, path, compression)
1251 1260
1252 1261
1253 1262 def makefilestorage(orig, requirements, features, **kwargs):
1254 1263 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
1255 1264 if REQUIREMENT in requirements:
1256 1265 if REQUIREMENT_SHALLOW_FILES in requirements:
1257 1266 features.add(repository.REPO_FEATURE_SHALLOW_FILE_STORAGE)
1258 1267
1259 1268 return sqlitefilestorage
1260 1269 else:
1261 1270 return orig(requirements=requirements, features=features, **kwargs)
1262 1271
1263 1272
1264 1273 def makemain(orig, ui, requirements, **kwargs):
1265 1274 if REQUIREMENT in requirements:
1266 1275 if REQUIREMENT_ZSTD in requirements and not zstd:
1267 1276 raise error.Abort(
1268 1277 _(
1269 1278 b'repository uses zstandard compression, which '
1270 1279 b'is not available to this Mercurial install'
1271 1280 )
1272 1281 )
1273 1282
1274 1283 return sqliterepository
1275 1284
1276 1285 return orig(requirements=requirements, **kwargs)
1277 1286
1278 1287
1279 1288 def verifierinit(orig, self, *args, **kwargs):
1280 1289 orig(self, *args, **kwargs)
1281 1290
1282 1291 # We don't care that files in the store don't align with what is
1283 1292 # advertised. So suppress these warnings.
1284 1293 self.warnorphanstorefiles = False
1285 1294
1286 1295
1287 1296 def extsetup(ui):
1288 1297 localrepo.featuresetupfuncs.add(featuresetup)
1289 1298 extensions.wrapfunction(
1290 1299 localrepo, b'newreporequirements', newreporequirements
1291 1300 )
1292 1301 extensions.wrapfunction(localrepo, b'makefilestorage', makefilestorage)
1293 1302 extensions.wrapfunction(localrepo, b'makemain', makemain)
1294 1303 extensions.wrapfunction(verify.verifier, b'__init__', verifierinit)
1295 1304
1296 1305
1297 1306 def reposetup(ui, repo):
1298 1307 if isinstance(repo, sqliterepository):
1299 1308 repo._db = None
1300 1309
1301 1310 # TODO check for bundlerepository?
@@ -1,680 +1,680 b''
1 1 # bundlerepo.py - repository class for viewing uncompressed bundles
2 2 #
3 3 # Copyright 2006, 2007 Benoit Boissinot <bboissin@gmail.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 """Repository class for viewing uncompressed bundles.
9 9
10 10 This provides a read-only repository interface to bundles as if they
11 11 were part of the actual repository.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import os
17 17 import shutil
18 18
19 19 from .i18n import _
20 20 from .node import (
21 21 hex,
22 22 nullid,
23 23 nullrev,
24 24 )
25 25
26 26 from . import (
27 27 bundle2,
28 28 changegroup,
29 29 changelog,
30 30 cmdutil,
31 31 discovery,
32 32 encoding,
33 33 error,
34 34 exchange,
35 35 filelog,
36 36 localrepo,
37 37 manifest,
38 38 mdiff,
39 39 pathutil,
40 40 phases,
41 41 pycompat,
42 42 revlog,
43 43 util,
44 44 vfs as vfsmod,
45 45 )
46 46
47 47
48 48 class bundlerevlog(revlog.revlog):
49 49 def __init__(self, opener, indexfile, cgunpacker, linkmapper):
50 50 # How it works:
51 51 # To retrieve a revision, we need to know the offset of the revision in
52 52 # the bundle (an unbundle object). We store this offset in the index
53 53 # (start). The base of the delta is stored in the base field.
54 54 #
55 55 # To differentiate a rev in the bundle from a rev in the revlog, we
56 56 # check revision against repotiprev.
57 57 opener = vfsmod.readonlyvfs(opener)
58 58 revlog.revlog.__init__(self, opener, indexfile)
59 59 self.bundle = cgunpacker
60 60 n = len(self)
61 61 self.repotiprev = n - 1
62 62 self.bundlerevs = set() # used by 'bundle()' revset expression
63 63 for deltadata in cgunpacker.deltaiter():
64 node, p1, p2, cs, deltabase, delta, flags = deltadata
64 node, p1, p2, cs, deltabase, delta, flags, sidedata = deltadata
65 65
66 66 size = len(delta)
67 67 start = cgunpacker.tell() - size
68 68
69 69 if self.index.has_node(node):
70 70 # this can happen if two branches make the same change
71 71 self.bundlerevs.add(self.index.rev(node))
72 72 continue
73 73 if cs == node:
74 74 linkrev = nullrev
75 75 else:
76 76 linkrev = linkmapper(cs)
77 77
78 78 for p in (p1, p2):
79 79 if not self.index.has_node(p):
80 80 raise error.LookupError(
81 81 p, self.indexfile, _(b"unknown parent")
82 82 )
83 83
84 84 if not self.index.has_node(deltabase):
85 85 raise LookupError(
86 86 deltabase, self.indexfile, _(b'unknown delta base')
87 87 )
88 88
89 89 baserev = self.rev(deltabase)
90 90 # start, size, full unc. size, base (unused), link, p1, p2, node
91 91 e = (
92 92 revlog.offset_type(start, flags),
93 93 size,
94 94 -1,
95 95 baserev,
96 96 linkrev,
97 97 self.rev(p1),
98 98 self.rev(p2),
99 99 node,
100 100 )
101 101 self.index.append(e)
102 102 self.bundlerevs.add(n)
103 103 n += 1
104 104
105 105 def _chunk(self, rev, df=None):
106 106 # Warning: in case of bundle, the diff is against what we stored as
107 107 # delta base, not against rev - 1
108 108 # XXX: could use some caching
109 109 if rev <= self.repotiprev:
110 110 return revlog.revlog._chunk(self, rev)
111 111 self.bundle.seek(self.start(rev))
112 112 return self.bundle.read(self.length(rev))
113 113
114 114 def revdiff(self, rev1, rev2):
115 115 """return or calculate a delta between two revisions"""
116 116 if rev1 > self.repotiprev and rev2 > self.repotiprev:
117 117 # hot path for bundle
118 118 revb = self.index[rev2][3]
119 119 if revb == rev1:
120 120 return self._chunk(rev2)
121 121 elif rev1 <= self.repotiprev and rev2 <= self.repotiprev:
122 122 return revlog.revlog.revdiff(self, rev1, rev2)
123 123
124 124 return mdiff.textdiff(self.rawdata(rev1), self.rawdata(rev2))
125 125
126 126 def _rawtext(self, node, rev, _df=None):
127 127 if rev is None:
128 128 rev = self.rev(node)
129 129 validated = False
130 130 rawtext = None
131 131 chain = []
132 132 iterrev = rev
133 133 # reconstruct the revision if it is from a changegroup
134 134 while iterrev > self.repotiprev:
135 135 if self._revisioncache and self._revisioncache[1] == iterrev:
136 136 rawtext = self._revisioncache[2]
137 137 break
138 138 chain.append(iterrev)
139 139 iterrev = self.index[iterrev][3]
140 140 if iterrev == nullrev:
141 141 rawtext = b''
142 142 elif rawtext is None:
143 143 r = super(bundlerevlog, self)._rawtext(
144 144 self.node(iterrev), iterrev, _df=_df
145 145 )
146 146 __, rawtext, validated = r
147 147 if chain:
148 148 validated = False
149 149 while chain:
150 150 delta = self._chunk(chain.pop())
151 151 rawtext = mdiff.patches(rawtext, [delta])
152 152 return rev, rawtext, validated
153 153
154 154 def addrevision(self, *args, **kwargs):
155 155 raise NotImplementedError
156 156
157 157 def addgroup(self, *args, **kwargs):
158 158 raise NotImplementedError
159 159
160 160 def strip(self, *args, **kwargs):
161 161 raise NotImplementedError
162 162
163 163 def checksize(self):
164 164 raise NotImplementedError
165 165
166 166
167 167 class bundlechangelog(bundlerevlog, changelog.changelog):
168 168 def __init__(self, opener, cgunpacker):
169 169 changelog.changelog.__init__(self, opener)
170 170 linkmapper = lambda x: x
171 171 bundlerevlog.__init__(
172 172 self, opener, self.indexfile, cgunpacker, linkmapper
173 173 )
174 174
175 175
176 176 class bundlemanifest(bundlerevlog, manifest.manifestrevlog):
177 177 def __init__(
178 178 self, opener, cgunpacker, linkmapper, dirlogstarts=None, dir=b''
179 179 ):
180 180 manifest.manifestrevlog.__init__(self, opener, tree=dir)
181 181 bundlerevlog.__init__(
182 182 self, opener, self.indexfile, cgunpacker, linkmapper
183 183 )
184 184 if dirlogstarts is None:
185 185 dirlogstarts = {}
186 186 if self.bundle.version == b"03":
187 187 dirlogstarts = _getfilestarts(self.bundle)
188 188 self._dirlogstarts = dirlogstarts
189 189 self._linkmapper = linkmapper
190 190
191 191 def dirlog(self, d):
192 192 if d in self._dirlogstarts:
193 193 self.bundle.seek(self._dirlogstarts[d])
194 194 return bundlemanifest(
195 195 self.opener,
196 196 self.bundle,
197 197 self._linkmapper,
198 198 self._dirlogstarts,
199 199 dir=d,
200 200 )
201 201 return super(bundlemanifest, self).dirlog(d)
202 202
203 203
204 204 class bundlefilelog(filelog.filelog):
205 205 def __init__(self, opener, path, cgunpacker, linkmapper):
206 206 filelog.filelog.__init__(self, opener, path)
207 207 self._revlog = bundlerevlog(
208 208 opener, self.indexfile, cgunpacker, linkmapper
209 209 )
210 210
211 211
212 212 class bundlepeer(localrepo.localpeer):
213 213 def canpush(self):
214 214 return False
215 215
216 216
217 217 class bundlephasecache(phases.phasecache):
218 218 def __init__(self, *args, **kwargs):
219 219 super(bundlephasecache, self).__init__(*args, **kwargs)
220 220 if util.safehasattr(self, 'opener'):
221 221 self.opener = vfsmod.readonlyvfs(self.opener)
222 222
223 223 def write(self):
224 224 raise NotImplementedError
225 225
226 226 def _write(self, fp):
227 227 raise NotImplementedError
228 228
229 229 def _updateroots(self, phase, newroots, tr):
230 230 self.phaseroots[phase] = newroots
231 231 self.invalidate()
232 232 self.dirty = True
233 233
234 234
235 235 def _getfilestarts(cgunpacker):
236 236 filespos = {}
237 237 for chunkdata in iter(cgunpacker.filelogheader, {}):
238 238 fname = chunkdata[b'filename']
239 239 filespos[fname] = cgunpacker.tell()
240 240 for chunk in iter(lambda: cgunpacker.deltachunk(None), {}):
241 241 pass
242 242 return filespos
243 243
244 244
245 245 class bundlerepository(object):
246 246 """A repository instance that is a union of a local repo and a bundle.
247 247
248 248 Instances represent a read-only repository composed of a local repository
249 249 with the contents of a bundle file applied. The repository instance is
250 250 conceptually similar to the state of a repository after an
251 251 ``hg unbundle`` operation. However, the contents of the bundle are never
252 252 applied to the actual base repository.
253 253
254 254 Instances constructed directly are not usable as repository objects.
255 255 Use instance() or makebundlerepository() to create instances.
256 256 """
257 257
258 258 def __init__(self, bundlepath, url, tempparent):
259 259 self._tempparent = tempparent
260 260 self._url = url
261 261
262 262 self.ui.setconfig(b'phases', b'publish', False, b'bundlerepo')
263 263
264 264 self.tempfile = None
265 265 f = util.posixfile(bundlepath, b"rb")
266 266 bundle = exchange.readbundle(self.ui, f, bundlepath)
267 267
268 268 if isinstance(bundle, bundle2.unbundle20):
269 269 self._bundlefile = bundle
270 270 self._cgunpacker = None
271 271
272 272 cgpart = None
273 273 for part in bundle.iterparts(seekable=True):
274 274 if part.type == b'changegroup':
275 275 if cgpart:
276 276 raise NotImplementedError(
277 277 b"can't process multiple changegroups"
278 278 )
279 279 cgpart = part
280 280
281 281 self._handlebundle2part(bundle, part)
282 282
283 283 if not cgpart:
284 284 raise error.Abort(_(b"No changegroups found"))
285 285
286 286 # This is required to placate a later consumer, which expects
287 287 # the payload offset to be at the beginning of the changegroup.
288 288 # We need to do this after the iterparts() generator advances
289 289 # because iterparts() will seek to end of payload after the
290 290 # generator returns control to iterparts().
291 291 cgpart.seek(0, os.SEEK_SET)
292 292
293 293 elif isinstance(bundle, changegroup.cg1unpacker):
294 294 if bundle.compressed():
295 295 f = self._writetempbundle(
296 296 bundle.read, b'.hg10un', header=b'HG10UN'
297 297 )
298 298 bundle = exchange.readbundle(self.ui, f, bundlepath, self.vfs)
299 299
300 300 self._bundlefile = bundle
301 301 self._cgunpacker = bundle
302 302 else:
303 303 raise error.Abort(
304 304 _(b'bundle type %s cannot be read') % type(bundle)
305 305 )
306 306
307 307 # dict with the mapping 'filename' -> position in the changegroup.
308 308 self._cgfilespos = {}
309 309
310 310 self.firstnewrev = self.changelog.repotiprev + 1
311 311 phases.retractboundary(
312 312 self,
313 313 None,
314 314 phases.draft,
315 315 [ctx.node() for ctx in self[self.firstnewrev :]],
316 316 )
317 317
318 318 def _handlebundle2part(self, bundle, part):
319 319 if part.type != b'changegroup':
320 320 return
321 321
322 322 cgstream = part
323 323 version = part.params.get(b'version', b'01')
324 324 legalcgvers = changegroup.supportedincomingversions(self)
325 325 if version not in legalcgvers:
326 326 msg = _(b'Unsupported changegroup version: %s')
327 327 raise error.Abort(msg % version)
328 328 if bundle.compressed():
329 329 cgstream = self._writetempbundle(part.read, b'.cg%sun' % version)
330 330
331 331 self._cgunpacker = changegroup.getunbundler(version, cgstream, b'UN')
332 332
333 333 def _writetempbundle(self, readfn, suffix, header=b''):
334 334 """Write a temporary file to disk"""
335 335 fdtemp, temp = self.vfs.mkstemp(prefix=b"hg-bundle-", suffix=suffix)
336 336 self.tempfile = temp
337 337
338 338 with os.fdopen(fdtemp, 'wb') as fptemp:
339 339 fptemp.write(header)
340 340 while True:
341 341 chunk = readfn(2 ** 18)
342 342 if not chunk:
343 343 break
344 344 fptemp.write(chunk)
345 345
346 346 return self.vfs.open(self.tempfile, mode=b"rb")
347 347
348 348 @localrepo.unfilteredpropertycache
349 349 def _phasecache(self):
350 350 return bundlephasecache(self, self._phasedefaults)
351 351
352 352 @localrepo.unfilteredpropertycache
353 353 def changelog(self):
354 354 # consume the header if it exists
355 355 self._cgunpacker.changelogheader()
356 356 c = bundlechangelog(self.svfs, self._cgunpacker)
357 357 self.manstart = self._cgunpacker.tell()
358 358 return c
359 359
360 360 def _refreshchangelog(self):
361 361 # changelog for bundle repo are not filecache, this method is not
362 362 # applicable.
363 363 pass
364 364
365 365 @localrepo.unfilteredpropertycache
366 366 def manifestlog(self):
367 367 self._cgunpacker.seek(self.manstart)
368 368 # consume the header if it exists
369 369 self._cgunpacker.manifestheader()
370 370 linkmapper = self.unfiltered().changelog.rev
371 371 rootstore = bundlemanifest(self.svfs, self._cgunpacker, linkmapper)
372 372 self.filestart = self._cgunpacker.tell()
373 373
374 374 return manifest.manifestlog(
375 375 self.svfs, self, rootstore, self.narrowmatch()
376 376 )
377 377
378 378 def _consumemanifest(self):
379 379 """Consumes the manifest portion of the bundle, setting filestart so the
380 380 file portion can be read."""
381 381 self._cgunpacker.seek(self.manstart)
382 382 self._cgunpacker.manifestheader()
383 383 for delta in self._cgunpacker.deltaiter():
384 384 pass
385 385 self.filestart = self._cgunpacker.tell()
386 386
387 387 @localrepo.unfilteredpropertycache
388 388 def manstart(self):
389 389 self.changelog
390 390 return self.manstart
391 391
392 392 @localrepo.unfilteredpropertycache
393 393 def filestart(self):
394 394 self.manifestlog
395 395
396 396 # If filestart was not set by self.manifestlog, that means the
397 397 # manifestlog implementation did not consume the manifests from the
398 398 # changegroup (ex: it might be consuming trees from a separate bundle2
399 399 # part instead). So we need to manually consume it.
400 400 if 'filestart' not in self.__dict__:
401 401 self._consumemanifest()
402 402
403 403 return self.filestart
404 404
405 405 def url(self):
406 406 return self._url
407 407
408 408 def file(self, f):
409 409 if not self._cgfilespos:
410 410 self._cgunpacker.seek(self.filestart)
411 411 self._cgfilespos = _getfilestarts(self._cgunpacker)
412 412
413 413 if f in self._cgfilespos:
414 414 self._cgunpacker.seek(self._cgfilespos[f])
415 415 linkmapper = self.unfiltered().changelog.rev
416 416 return bundlefilelog(self.svfs, f, self._cgunpacker, linkmapper)
417 417 else:
418 418 return super(bundlerepository, self).file(f)
419 419
420 420 def close(self):
421 421 """Close assigned bundle file immediately."""
422 422 self._bundlefile.close()
423 423 if self.tempfile is not None:
424 424 self.vfs.unlink(self.tempfile)
425 425 if self._tempparent:
426 426 shutil.rmtree(self._tempparent, True)
427 427
428 428 def cancopy(self):
429 429 return False
430 430
431 431 def peer(self):
432 432 return bundlepeer(self)
433 433
434 434 def getcwd(self):
435 435 return encoding.getcwd() # always outside the repo
436 436
437 437 # Check if parents exist in localrepo before setting
438 438 def setparents(self, p1, p2=nullid):
439 439 p1rev = self.changelog.rev(p1)
440 440 p2rev = self.changelog.rev(p2)
441 441 msg = _(b"setting parent to node %s that only exists in the bundle\n")
442 442 if self.changelog.repotiprev < p1rev:
443 443 self.ui.warn(msg % hex(p1))
444 444 if self.changelog.repotiprev < p2rev:
445 445 self.ui.warn(msg % hex(p2))
446 446 return super(bundlerepository, self).setparents(p1, p2)
447 447
448 448
449 449 def instance(ui, path, create, intents=None, createopts=None):
450 450 if create:
451 451 raise error.Abort(_(b'cannot create new bundle repository'))
452 452 # internal config: bundle.mainreporoot
453 453 parentpath = ui.config(b"bundle", b"mainreporoot")
454 454 if not parentpath:
455 455 # try to find the correct path to the working directory repo
456 456 parentpath = cmdutil.findrepo(encoding.getcwd())
457 457 if parentpath is None:
458 458 parentpath = b''
459 459 if parentpath:
460 460 # Try to make the full path relative so we get a nice, short URL.
461 461 # In particular, we don't want temp dir names in test outputs.
462 462 cwd = encoding.getcwd()
463 463 if parentpath == cwd:
464 464 parentpath = b''
465 465 else:
466 466 cwd = pathutil.normasprefix(cwd)
467 467 if parentpath.startswith(cwd):
468 468 parentpath = parentpath[len(cwd) :]
469 469 u = util.url(path)
470 470 path = u.localpath()
471 471 if u.scheme == b'bundle':
472 472 s = path.split(b"+", 1)
473 473 if len(s) == 1:
474 474 repopath, bundlename = parentpath, s[0]
475 475 else:
476 476 repopath, bundlename = s
477 477 else:
478 478 repopath, bundlename = parentpath, path
479 479
480 480 return makebundlerepository(ui, repopath, bundlename)
481 481
482 482
483 483 def makebundlerepository(ui, repopath, bundlepath):
484 484 """Make a bundle repository object based on repo and bundle paths."""
485 485 if repopath:
486 486 url = b'bundle:%s+%s' % (util.expandpath(repopath), bundlepath)
487 487 else:
488 488 url = b'bundle:%s' % bundlepath
489 489
490 490 # Because we can't make any guarantees about the type of the base
491 491 # repository, we can't have a static class representing the bundle
492 492 # repository. We also can't make any guarantees about how to even
493 493 # call the base repository's constructor!
494 494 #
495 495 # So, our strategy is to go through ``localrepo.instance()`` to construct
496 496 # a repo instance. Then, we dynamically create a new type derived from
497 497 # both it and our ``bundlerepository`` class which overrides some
498 498 # functionality. We then change the type of the constructed repository
499 499 # to this new type and initialize the bundle-specific bits of it.
500 500
501 501 try:
502 502 repo = localrepo.instance(ui, repopath, create=False)
503 503 tempparent = None
504 504 except error.RepoError:
505 505 tempparent = pycompat.mkdtemp()
506 506 try:
507 507 repo = localrepo.instance(ui, tempparent, create=True)
508 508 except Exception:
509 509 shutil.rmtree(tempparent)
510 510 raise
511 511
512 512 class derivedbundlerepository(bundlerepository, repo.__class__):
513 513 pass
514 514
515 515 repo.__class__ = derivedbundlerepository
516 516 bundlerepository.__init__(repo, bundlepath, url, tempparent)
517 517
518 518 return repo
519 519
520 520
521 521 class bundletransactionmanager(object):
522 522 def transaction(self):
523 523 return None
524 524
525 525 def close(self):
526 526 raise NotImplementedError
527 527
528 528 def release(self):
529 529 raise NotImplementedError
530 530
531 531
532 532 def getremotechanges(
533 533 ui, repo, peer, onlyheads=None, bundlename=None, force=False
534 534 ):
535 535 """obtains a bundle of changes incoming from peer
536 536
537 537 "onlyheads" restricts the returned changes to those reachable from the
538 538 specified heads.
539 539 "bundlename", if given, stores the bundle to this file path permanently;
540 540 otherwise it's stored to a temp file and gets deleted again when you call
541 541 the returned "cleanupfn".
542 542 "force" indicates whether to proceed on unrelated repos.
543 543
544 544 Returns a tuple (local, csets, cleanupfn):
545 545
546 546 "local" is a local repo from which to obtain the actual incoming
547 547 changesets; it is a bundlerepo for the obtained bundle when the
548 548 original "peer" is remote.
549 549 "csets" lists the incoming changeset node ids.
550 550 "cleanupfn" must be called without arguments when you're done processing
551 551 the changes; it closes both the original "peer" and the one returned
552 552 here.
553 553 """
554 554 tmp = discovery.findcommonincoming(repo, peer, heads=onlyheads, force=force)
555 555 common, incoming, rheads = tmp
556 556 if not incoming:
557 557 try:
558 558 if bundlename:
559 559 os.unlink(bundlename)
560 560 except OSError:
561 561 pass
562 562 return repo, [], peer.close
563 563
564 564 commonset = set(common)
565 565 rheads = [x for x in rheads if x not in commonset]
566 566
567 567 bundle = None
568 568 bundlerepo = None
569 569 localrepo = peer.local()
570 570 if bundlename or not localrepo:
571 571 # create a bundle (uncompressed if peer repo is not local)
572 572
573 573 # developer config: devel.legacy.exchange
574 574 legexc = ui.configlist(b'devel', b'legacy.exchange')
575 575 forcebundle1 = b'bundle2' not in legexc and b'bundle1' in legexc
576 576 canbundle2 = (
577 577 not forcebundle1
578 578 and peer.capable(b'getbundle')
579 579 and peer.capable(b'bundle2')
580 580 )
581 581 if canbundle2:
582 582 with peer.commandexecutor() as e:
583 583 b2 = e.callcommand(
584 584 b'getbundle',
585 585 {
586 586 b'source': b'incoming',
587 587 b'common': common,
588 588 b'heads': rheads,
589 589 b'bundlecaps': exchange.caps20to10(
590 590 repo, role=b'client'
591 591 ),
592 592 b'cg': True,
593 593 },
594 594 ).result()
595 595
596 596 fname = bundle = changegroup.writechunks(
597 597 ui, b2._forwardchunks(), bundlename
598 598 )
599 599 else:
600 600 if peer.capable(b'getbundle'):
601 601 with peer.commandexecutor() as e:
602 602 cg = e.callcommand(
603 603 b'getbundle',
604 604 {
605 605 b'source': b'incoming',
606 606 b'common': common,
607 607 b'heads': rheads,
608 608 },
609 609 ).result()
610 610 elif onlyheads is None and not peer.capable(b'changegroupsubset'):
611 611 # compat with older servers when pulling all remote heads
612 612
613 613 with peer.commandexecutor() as e:
614 614 cg = e.callcommand(
615 615 b'changegroup',
616 616 {
617 617 b'nodes': incoming,
618 618 b'source': b'incoming',
619 619 },
620 620 ).result()
621 621
622 622 rheads = None
623 623 else:
624 624 with peer.commandexecutor() as e:
625 625 cg = e.callcommand(
626 626 b'changegroupsubset',
627 627 {
628 628 b'bases': incoming,
629 629 b'heads': rheads,
630 630 b'source': b'incoming',
631 631 },
632 632 ).result()
633 633
634 634 if localrepo:
635 635 bundletype = b"HG10BZ"
636 636 else:
637 637 bundletype = b"HG10UN"
638 638 fname = bundle = bundle2.writebundle(ui, cg, bundlename, bundletype)
639 639 # keep written bundle?
640 640 if bundlename:
641 641 bundle = None
642 642 if not localrepo:
643 643 # use the created uncompressed bundlerepo
644 644 localrepo = bundlerepo = makebundlerepository(
645 645 repo.baseui, repo.root, fname
646 646 )
647 647
648 648 # this repo contains local and peer now, so filter out local again
649 649 common = repo.heads()
650 650 if localrepo:
651 651 # Part of common may be remotely filtered
652 652 # So use an unfiltered version
653 653 # The discovery process probably need cleanup to avoid that
654 654 localrepo = localrepo.unfiltered()
655 655
656 656 csets = localrepo.changelog.findmissing(common, rheads)
657 657
658 658 if bundlerepo:
659 659 reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev :]]
660 660
661 661 with peer.commandexecutor() as e:
662 662 remotephases = e.callcommand(
663 663 b'listkeys',
664 664 {
665 665 b'namespace': b'phases',
666 666 },
667 667 ).result()
668 668
669 669 pullop = exchange.pulloperation(bundlerepo, peer, heads=reponodes)
670 670 pullop.trmanager = bundletransactionmanager()
671 671 exchange._pullapplyphases(pullop, remotephases)
672 672
673 673 def cleanup():
674 674 if bundlerepo:
675 675 bundlerepo.close()
676 676 if bundle:
677 677 os.unlink(bundle)
678 678 peer.close()
679 679
680 680 return (localrepo, csets, cleanup)
@@ -1,1710 +1,1784 b''
1 1 # changegroup.py - Mercurial changegroup manipulation functions
2 2 #
3 3 # Copyright 2006 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 os
11 11 import struct
12 12 import weakref
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 hex,
17 17 nullid,
18 18 nullrev,
19 19 short,
20 20 )
21 21 from .pycompat import open
22 22
23 23 from . import (
24 24 error,
25 25 match as matchmod,
26 26 mdiff,
27 27 phases,
28 28 pycompat,
29 29 requirements,
30 30 scmutil,
31 31 util,
32 32 )
33 33
34 34 from .interfaces import repository
35 from .revlogutils import sidedata as sidedatamod
35 36
36 37 _CHANGEGROUPV1_DELTA_HEADER = struct.Struct(b"20s20s20s20s")
37 38 _CHANGEGROUPV2_DELTA_HEADER = struct.Struct(b"20s20s20s20s20s")
38 39 _CHANGEGROUPV3_DELTA_HEADER = struct.Struct(b">20s20s20s20s20sH")
39 40
40 41 LFS_REQUIREMENT = b'lfs'
41 42
42 43 readexactly = util.readexactly
43 44
44 45
45 46 def getchunk(stream):
46 47 """return the next chunk from stream as a string"""
47 48 d = readexactly(stream, 4)
48 49 l = struct.unpack(b">l", d)[0]
49 50 if l <= 4:
50 51 if l:
51 52 raise error.Abort(_(b"invalid chunk length %d") % l)
52 53 return b""
53 54 return readexactly(stream, l - 4)
54 55
55 56
56 57 def chunkheader(length):
57 58 """return a changegroup chunk header (string)"""
58 59 return struct.pack(b">l", length + 4)
59 60
60 61
61 62 def closechunk():
62 63 """return a changegroup chunk header (string) for a zero-length chunk"""
63 64 return struct.pack(b">l", 0)
64 65
65 66
66 67 def _fileheader(path):
67 68 """Obtain a changegroup chunk header for a named path."""
68 69 return chunkheader(len(path)) + path
69 70
70 71
71 72 def writechunks(ui, chunks, filename, vfs=None):
72 73 """Write chunks to a file and return its filename.
73 74
74 75 The stream is assumed to be a bundle file.
75 76 Existing files will not be overwritten.
76 77 If no filename is specified, a temporary file is created.
77 78 """
78 79 fh = None
79 80 cleanup = None
80 81 try:
81 82 if filename:
82 83 if vfs:
83 84 fh = vfs.open(filename, b"wb")
84 85 else:
85 86 # Increase default buffer size because default is usually
86 87 # small (4k is common on Linux).
87 88 fh = open(filename, b"wb", 131072)
88 89 else:
89 90 fd, filename = pycompat.mkstemp(prefix=b"hg-bundle-", suffix=b".hg")
90 91 fh = os.fdopen(fd, "wb")
91 92 cleanup = filename
92 93 for c in chunks:
93 94 fh.write(c)
94 95 cleanup = None
95 96 return filename
96 97 finally:
97 98 if fh is not None:
98 99 fh.close()
99 100 if cleanup is not None:
100 101 if filename and vfs:
101 102 vfs.unlink(cleanup)
102 103 else:
103 104 os.unlink(cleanup)
104 105
105 106
106 107 class cg1unpacker(object):
107 108 """Unpacker for cg1 changegroup streams.
108 109
109 110 A changegroup unpacker handles the framing of the revision data in
110 111 the wire format. Most consumers will want to use the apply()
111 112 method to add the changes from the changegroup to a repository.
112 113
113 114 If you're forwarding a changegroup unmodified to another consumer,
114 115 use getchunks(), which returns an iterator of changegroup
115 116 chunks. This is mostly useful for cases where you need to know the
116 117 data stream has ended by observing the end of the changegroup.
117 118
118 119 deltachunk() is useful only if you're applying delta data. Most
119 120 consumers should prefer apply() instead.
120 121
121 122 A few other public methods exist. Those are used only for
122 123 bundlerepo and some debug commands - their use is discouraged.
123 124 """
124 125
125 126 deltaheader = _CHANGEGROUPV1_DELTA_HEADER
126 127 deltaheadersize = deltaheader.size
127 128 version = b'01'
128 129 _grouplistcount = 1 # One list of files after the manifests
129 130
130 131 def __init__(self, fh, alg, extras=None):
131 132 if alg is None:
132 133 alg = b'UN'
133 134 if alg not in util.compengines.supportedbundletypes:
134 135 raise error.Abort(_(b'unknown stream compression type: %s') % alg)
135 136 if alg == b'BZ':
136 137 alg = b'_truncatedBZ'
137 138
138 139 compengine = util.compengines.forbundletype(alg)
139 140 self._stream = compengine.decompressorreader(fh)
140 141 self._type = alg
141 142 self.extras = extras or {}
142 143 self.callback = None
143 144
144 145 # These methods (compressed, read, seek, tell) all appear to only
145 146 # be used by bundlerepo, but it's a little hard to tell.
146 147 def compressed(self):
147 148 return self._type is not None and self._type != b'UN'
148 149
149 150 def read(self, l):
150 151 return self._stream.read(l)
151 152
152 153 def seek(self, pos):
153 154 return self._stream.seek(pos)
154 155
155 156 def tell(self):
156 157 return self._stream.tell()
157 158
158 159 def close(self):
159 160 return self._stream.close()
160 161
161 162 def _chunklength(self):
162 163 d = readexactly(self._stream, 4)
163 164 l = struct.unpack(b">l", d)[0]
164 165 if l <= 4:
165 166 if l:
166 167 raise error.Abort(_(b"invalid chunk length %d") % l)
167 168 return 0
168 169 if self.callback:
169 170 self.callback()
170 171 return l - 4
171 172
172 173 def changelogheader(self):
173 174 """v10 does not have a changelog header chunk"""
174 175 return {}
175 176
176 177 def manifestheader(self):
177 178 """v10 does not have a manifest header chunk"""
178 179 return {}
179 180
180 181 def filelogheader(self):
181 182 """return the header of the filelogs chunk, v10 only has the filename"""
182 183 l = self._chunklength()
183 184 if not l:
184 185 return {}
185 186 fname = readexactly(self._stream, l)
186 187 return {b'filename': fname}
187 188
188 189 def _deltaheader(self, headertuple, prevnode):
189 190 node, p1, p2, cs = headertuple
190 191 if prevnode is None:
191 192 deltabase = p1
192 193 else:
193 194 deltabase = prevnode
194 195 flags = 0
195 196 return node, p1, p2, deltabase, cs, flags
196 197
197 198 def deltachunk(self, prevnode):
198 199 l = self._chunklength()
199 200 if not l:
200 201 return {}
201 202 headerdata = readexactly(self._stream, self.deltaheadersize)
202 203 header = self.deltaheader.unpack(headerdata)
203 204 delta = readexactly(self._stream, l - self.deltaheadersize)
204 205 node, p1, p2, deltabase, cs, flags = self._deltaheader(header, prevnode)
205 return (node, p1, p2, cs, deltabase, delta, flags)
206 # cg4 forward-compat
207 sidedata = {}
208 return (node, p1, p2, cs, deltabase, delta, flags, sidedata)
206 209
207 210 def getchunks(self):
208 211 """returns all the chunks contains in the bundle
209 212
210 213 Used when you need to forward the binary stream to a file or another
211 214 network API. To do so, it parse the changegroup data, otherwise it will
212 215 block in case of sshrepo because it don't know the end of the stream.
213 216 """
214 217 # For changegroup 1 and 2, we expect 3 parts: changelog, manifestlog,
215 218 # and a list of filelogs. For changegroup 3, we expect 4 parts:
216 219 # changelog, manifestlog, a list of tree manifestlogs, and a list of
217 220 # filelogs.
218 221 #
219 222 # Changelog and manifestlog parts are terminated with empty chunks. The
220 223 # tree and file parts are a list of entry sections. Each entry section
221 224 # is a series of chunks terminating in an empty chunk. The list of these
222 225 # entry sections is terminated in yet another empty chunk, so we know
223 226 # we've reached the end of the tree/file list when we reach an empty
224 227 # chunk that was proceeded by no non-empty chunks.
225 228
226 229 parts = 0
227 230 while parts < 2 + self._grouplistcount:
228 231 noentries = True
229 232 while True:
230 233 chunk = getchunk(self)
231 234 if not chunk:
232 235 # The first two empty chunks represent the end of the
233 236 # changelog and the manifestlog portions. The remaining
234 237 # empty chunks represent either A) the end of individual
235 238 # tree or file entries in the file list, or B) the end of
236 239 # the entire list. It's the end of the entire list if there
237 240 # were no entries (i.e. noentries is True).
238 241 if parts < 2:
239 242 parts += 1
240 243 elif noentries:
241 244 parts += 1
242 245 break
243 246 noentries = False
244 247 yield chunkheader(len(chunk))
245 248 pos = 0
246 249 while pos < len(chunk):
247 250 next = pos + 2 ** 20
248 251 yield chunk[pos:next]
249 252 pos = next
250 253 yield closechunk()
251 254
252 255 def _unpackmanifests(self, repo, revmap, trp, prog):
253 256 self.callback = prog.increment
254 257 # no need to check for empty manifest group here:
255 258 # if the result of the merge of 1 and 2 is the same in 3 and 4,
256 259 # no new manifest will be created and the manifest group will
257 260 # be empty during the pull
258 261 self.manifestheader()
259 262 deltas = self.deltaiter()
260 263 repo.manifestlog.getstorage(b'').addgroup(deltas, revmap, trp)
261 264 prog.complete()
262 265 self.callback = None
263 266
264 267 def apply(
265 268 self,
266 269 repo,
267 270 tr,
268 271 srctype,
269 272 url,
270 273 targetphase=phases.draft,
271 274 expectedtotal=None,
272 275 ):
273 276 """Add the changegroup returned by source.read() to this repo.
274 277 srctype is a string like 'push', 'pull', or 'unbundle'. url is
275 278 the URL of the repo where this changegroup is coming from.
276 279
277 280 Return an integer summarizing the change to this repo:
278 281 - nothing changed or no source: 0
279 282 - more heads than before: 1+added heads (2..n)
280 283 - fewer heads than before: -1-removed heads (-2..-n)
281 284 - number of heads stays the same: 1
282 285 """
283 286 repo = repo.unfiltered()
284 287
285 288 def csmap(x):
286 289 repo.ui.debug(b"add changeset %s\n" % short(x))
287 290 return len(cl)
288 291
289 292 def revmap(x):
290 293 return cl.rev(x)
291 294
292 295 try:
293 296 # The transaction may already carry source information. In this
294 297 # case we use the top level data. We overwrite the argument
295 298 # because we need to use the top level value (if they exist)
296 299 # in this function.
297 300 srctype = tr.hookargs.setdefault(b'source', srctype)
298 301 tr.hookargs.setdefault(b'url', url)
299 302 repo.hook(
300 303 b'prechangegroup', throw=True, **pycompat.strkwargs(tr.hookargs)
301 304 )
302 305
303 306 # write changelog data to temp files so concurrent readers
304 307 # will not see an inconsistent view
305 308 cl = repo.changelog
306 309 cl.delayupdate(tr)
307 310 oldheads = set(cl.heads())
308 311
309 312 trp = weakref.proxy(tr)
310 313 # pull off the changeset group
311 314 repo.ui.status(_(b"adding changesets\n"))
312 315 clstart = len(cl)
313 316 progress = repo.ui.makeprogress(
314 317 _(b'changesets'), unit=_(b'chunks'), total=expectedtotal
315 318 )
316 319 self.callback = progress.increment
317 320
318 321 efilesset = set()
319 322 duprevs = []
320 323
321 324 def ondupchangelog(cl, rev):
322 325 if rev < clstart:
323 326 duprevs.append(rev)
324 327
325 328 def onchangelog(cl, rev):
326 329 ctx = cl.changelogrevision(rev)
327 330 efilesset.update(ctx.files)
328 331 repo.register_changeset(rev, ctx)
329 332
330 333 self.changelogheader()
331 334 deltas = self.deltaiter()
332 335 if not cl.addgroup(
333 336 deltas,
334 337 csmap,
335 338 trp,
336 339 alwayscache=True,
337 340 addrevisioncb=onchangelog,
338 341 duplicaterevisioncb=ondupchangelog,
339 342 ):
340 343 repo.ui.develwarn(
341 344 b'applied empty changelog from changegroup',
342 345 config=b'warn-empty-changegroup',
343 346 )
344 347 efiles = len(efilesset)
345 348 clend = len(cl)
346 349 changesets = clend - clstart
347 350 progress.complete()
348 351 del deltas
349 352 # TODO Python 2.7 removal
350 353 # del efilesset
351 354 efilesset = None
352 355 self.callback = None
353 356
354 357 # pull off the manifest group
355 358 repo.ui.status(_(b"adding manifests\n"))
356 359 # We know that we'll never have more manifests than we had
357 360 # changesets.
358 361 progress = repo.ui.makeprogress(
359 362 _(b'manifests'), unit=_(b'chunks'), total=changesets
360 363 )
361 364 self._unpackmanifests(repo, revmap, trp, progress)
362 365
363 366 needfiles = {}
364 367 if repo.ui.configbool(b'server', b'validate'):
365 368 cl = repo.changelog
366 369 ml = repo.manifestlog
367 370 # validate incoming csets have their manifests
368 371 for cset in pycompat.xrange(clstart, clend):
369 372 mfnode = cl.changelogrevision(cset).manifest
370 373 mfest = ml[mfnode].readdelta()
371 374 # store file nodes we must see
372 375 for f, n in pycompat.iteritems(mfest):
373 376 needfiles.setdefault(f, set()).add(n)
374 377
375 378 # process the files
376 379 repo.ui.status(_(b"adding file changes\n"))
377 380 newrevs, newfiles = _addchangegroupfiles(
378 381 repo, self, revmap, trp, efiles, needfiles
379 382 )
380 383
381 384 # making sure the value exists
382 385 tr.changes.setdefault(b'changegroup-count-changesets', 0)
383 386 tr.changes.setdefault(b'changegroup-count-revisions', 0)
384 387 tr.changes.setdefault(b'changegroup-count-files', 0)
385 388 tr.changes.setdefault(b'changegroup-count-heads', 0)
386 389
387 390 # some code use bundle operation for internal purpose. They usually
388 391 # set `ui.quiet` to do this outside of user sight. Size the report
389 392 # of such operation now happens at the end of the transaction, that
390 393 # ui.quiet has not direct effect on the output.
391 394 #
392 395 # To preserve this intend use an inelegant hack, we fail to report
393 396 # the change if `quiet` is set. We should probably move to
394 397 # something better, but this is a good first step to allow the "end
395 398 # of transaction report" to pass tests.
396 399 if not repo.ui.quiet:
397 400 tr.changes[b'changegroup-count-changesets'] += changesets
398 401 tr.changes[b'changegroup-count-revisions'] += newrevs
399 402 tr.changes[b'changegroup-count-files'] += newfiles
400 403
401 404 deltaheads = 0
402 405 if oldheads:
403 406 heads = cl.heads()
404 407 deltaheads += len(heads) - len(oldheads)
405 408 for h in heads:
406 409 if h not in oldheads and repo[h].closesbranch():
407 410 deltaheads -= 1
408 411
409 412 # see previous comment about checking ui.quiet
410 413 if not repo.ui.quiet:
411 414 tr.changes[b'changegroup-count-heads'] += deltaheads
412 415 repo.invalidatevolatilesets()
413 416
414 417 if changesets > 0:
415 418 if b'node' not in tr.hookargs:
416 419 tr.hookargs[b'node'] = hex(cl.node(clstart))
417 420 tr.hookargs[b'node_last'] = hex(cl.node(clend - 1))
418 421 hookargs = dict(tr.hookargs)
419 422 else:
420 423 hookargs = dict(tr.hookargs)
421 424 hookargs[b'node'] = hex(cl.node(clstart))
422 425 hookargs[b'node_last'] = hex(cl.node(clend - 1))
423 426 repo.hook(
424 427 b'pretxnchangegroup',
425 428 throw=True,
426 429 **pycompat.strkwargs(hookargs)
427 430 )
428 431
429 432 added = pycompat.xrange(clstart, clend)
430 433 phaseall = None
431 434 if srctype in (b'push', b'serve'):
432 435 # Old servers can not push the boundary themselves.
433 436 # New servers won't push the boundary if changeset already
434 437 # exists locally as secret
435 438 #
436 439 # We should not use added here but the list of all change in
437 440 # the bundle
438 441 if repo.publishing():
439 442 targetphase = phaseall = phases.public
440 443 else:
441 444 # closer target phase computation
442 445
443 446 # Those changesets have been pushed from the
444 447 # outside, their phases are going to be pushed
445 448 # alongside. Therefor `targetphase` is
446 449 # ignored.
447 450 targetphase = phaseall = phases.draft
448 451 if added:
449 452 phases.registernew(repo, tr, targetphase, added)
450 453 if phaseall is not None:
451 454 if duprevs:
452 455 duprevs.extend(added)
453 456 else:
454 457 duprevs = added
455 458 phases.advanceboundary(repo, tr, phaseall, [], revs=duprevs)
456 459 duprevs = []
457 460
458 461 if changesets > 0:
459 462
460 463 def runhooks(unused_success):
461 464 # These hooks run when the lock releases, not when the
462 465 # transaction closes. So it's possible for the changelog
463 466 # to have changed since we last saw it.
464 467 if clstart >= len(repo):
465 468 return
466 469
467 470 repo.hook(b"changegroup", **pycompat.strkwargs(hookargs))
468 471
469 472 for rev in added:
470 473 args = hookargs.copy()
471 474 args[b'node'] = hex(cl.node(rev))
472 475 del args[b'node_last']
473 476 repo.hook(b"incoming", **pycompat.strkwargs(args))
474 477
475 478 newheads = [h for h in repo.heads() if h not in oldheads]
476 479 repo.ui.log(
477 480 b"incoming",
478 481 b"%d incoming changes - new heads: %s\n",
479 482 len(added),
480 483 b', '.join([hex(c[:6]) for c in newheads]),
481 484 )
482 485
483 486 tr.addpostclose(
484 487 b'changegroup-runhooks-%020i' % clstart,
485 488 lambda tr: repo._afterlock(runhooks),
486 489 )
487 490 finally:
488 491 repo.ui.flush()
489 492 # never return 0 here:
490 493 if deltaheads < 0:
491 494 ret = deltaheads - 1
492 495 else:
493 496 ret = deltaheads + 1
494 497 return ret
495 498
496 499 def deltaiter(self):
497 500 """
498 501 returns an iterator of the deltas in this changegroup
499 502
500 503 Useful for passing to the underlying storage system to be stored.
501 504 """
502 505 chain = None
503 506 for chunkdata in iter(lambda: self.deltachunk(chain), {}):
504 507 # Chunkdata: (node, p1, p2, cs, deltabase, delta, flags)
505 508 yield chunkdata
506 509 chain = chunkdata[0]
507 510
508 511
509 512 class cg2unpacker(cg1unpacker):
510 513 """Unpacker for cg2 streams.
511 514
512 515 cg2 streams add support for generaldelta, so the delta header
513 516 format is slightly different. All other features about the data
514 517 remain the same.
515 518 """
516 519
517 520 deltaheader = _CHANGEGROUPV2_DELTA_HEADER
518 521 deltaheadersize = deltaheader.size
519 522 version = b'02'
520 523
521 524 def _deltaheader(self, headertuple, prevnode):
522 525 node, p1, p2, deltabase, cs = headertuple
523 526 flags = 0
524 527 return node, p1, p2, deltabase, cs, flags
525 528
526 529
527 530 class cg3unpacker(cg2unpacker):
528 531 """Unpacker for cg3 streams.
529 532
530 533 cg3 streams add support for exchanging treemanifests and revlog
531 534 flags. It adds the revlog flags to the delta header and an empty chunk
532 535 separating manifests and files.
533 536 """
534 537
535 538 deltaheader = _CHANGEGROUPV3_DELTA_HEADER
536 539 deltaheadersize = deltaheader.size
537 540 version = b'03'
538 541 _grouplistcount = 2 # One list of manifests and one list of files
539 542
540 543 def _deltaheader(self, headertuple, prevnode):
541 544 node, p1, p2, deltabase, cs, flags = headertuple
542 545 return node, p1, p2, deltabase, cs, flags
543 546
544 547 def _unpackmanifests(self, repo, revmap, trp, prog):
545 548 super(cg3unpacker, self)._unpackmanifests(repo, revmap, trp, prog)
546 549 for chunkdata in iter(self.filelogheader, {}):
547 550 # If we get here, there are directory manifests in the changegroup
548 551 d = chunkdata[b"filename"]
549 552 repo.ui.debug(b"adding %s revisions\n" % d)
550 553 deltas = self.deltaiter()
551 554 if not repo.manifestlog.getstorage(d).addgroup(deltas, revmap, trp):
552 555 raise error.Abort(_(b"received dir revlog group is empty"))
553 556
554 557
558 class cg4unpacker(cg3unpacker):
559 """Unpacker for cg4 streams.
560
561 cg4 streams add support for exchanging sidedata.
562 """
563
564 version = b'04'
565
566 def deltachunk(self, prevnode):
567 res = super(cg4unpacker, self).deltachunk(prevnode)
568 if not res:
569 return res
570
571 (node, p1, p2, cs, deltabase, delta, flags, _sidedata) = res
572
573 sidedata_raw = getchunk(self._stream)
574 sidedata = {}
575 if len(sidedata_raw) > 0:
576 sidedata = sidedatamod.deserialize_sidedata(sidedata_raw)
577
578 return node, p1, p2, cs, deltabase, delta, flags, sidedata
579
580
555 581 class headerlessfixup(object):
556 582 def __init__(self, fh, h):
557 583 self._h = h
558 584 self._fh = fh
559 585
560 586 def read(self, n):
561 587 if self._h:
562 588 d, self._h = self._h[:n], self._h[n:]
563 589 if len(d) < n:
564 590 d += readexactly(self._fh, n - len(d))
565 591 return d
566 592 return readexactly(self._fh, n)
567 593
568 594
569 595 def _revisiondeltatochunks(delta, headerfn):
570 596 """Serialize a revisiondelta to changegroup chunks."""
571 597
572 598 # The captured revision delta may be encoded as a delta against
573 599 # a base revision or as a full revision. The changegroup format
574 600 # requires that everything on the wire be deltas. So for full
575 601 # revisions, we need to invent a header that says to rewrite
576 602 # data.
577 603
578 604 if delta.delta is not None:
579 605 prefix, data = b'', delta.delta
580 606 elif delta.basenode == nullid:
581 607 data = delta.revision
582 608 prefix = mdiff.trivialdiffheader(len(data))
583 609 else:
584 610 data = delta.revision
585 611 prefix = mdiff.replacediffheader(delta.baserevisionsize, len(data))
586 612
587 613 meta = headerfn(delta)
588 614
589 615 yield chunkheader(len(meta) + len(prefix) + len(data))
590 616 yield meta
591 617 if prefix:
592 618 yield prefix
593 619 yield data
594 620
595 621
596 622 def _sortnodesellipsis(store, nodes, cl, lookup):
597 623 """Sort nodes for changegroup generation."""
598 624 # Ellipses serving mode.
599 625 #
600 626 # In a perfect world, we'd generate better ellipsis-ified graphs
601 627 # for non-changelog revlogs. In practice, we haven't started doing
602 628 # that yet, so the resulting DAGs for the manifestlog and filelogs
603 629 # are actually full of bogus parentage on all the ellipsis
604 630 # nodes. This has the side effect that, while the contents are
605 631 # correct, the individual DAGs might be completely out of whack in
606 632 # a case like 882681bc3166 and its ancestors (back about 10
607 633 # revisions or so) in the main hg repo.
608 634 #
609 635 # The one invariant we *know* holds is that the new (potentially
610 636 # bogus) DAG shape will be valid if we order the nodes in the
611 637 # order that they're introduced in dramatis personae by the
612 638 # changelog, so what we do is we sort the non-changelog histories
613 639 # by the order in which they are used by the changelog.
614 640 key = lambda n: cl.rev(lookup(n))
615 641 return sorted(nodes, key=key)
616 642
617 643
618 644 def _resolvenarrowrevisioninfo(
619 645 cl,
620 646 store,
621 647 ischangelog,
622 648 rev,
623 649 linkrev,
624 650 linknode,
625 651 clrevtolocalrev,
626 652 fullclnodes,
627 653 precomputedellipsis,
628 654 ):
629 655 linkparents = precomputedellipsis[linkrev]
630 656
631 657 def local(clrev):
632 658 """Turn a changelog revnum into a local revnum.
633 659
634 660 The ellipsis dag is stored as revnums on the changelog,
635 661 but when we're producing ellipsis entries for
636 662 non-changelog revlogs, we need to turn those numbers into
637 663 something local. This does that for us, and during the
638 664 changelog sending phase will also expand the stored
639 665 mappings as needed.
640 666 """
641 667 if clrev == nullrev:
642 668 return nullrev
643 669
644 670 if ischangelog:
645 671 return clrev
646 672
647 673 # Walk the ellipsis-ized changelog breadth-first looking for a
648 674 # change that has been linked from the current revlog.
649 675 #
650 676 # For a flat manifest revlog only a single step should be necessary
651 677 # as all relevant changelog entries are relevant to the flat
652 678 # manifest.
653 679 #
654 680 # For a filelog or tree manifest dirlog however not every changelog
655 681 # entry will have been relevant, so we need to skip some changelog
656 682 # nodes even after ellipsis-izing.
657 683 walk = [clrev]
658 684 while walk:
659 685 p = walk[0]
660 686 walk = walk[1:]
661 687 if p in clrevtolocalrev:
662 688 return clrevtolocalrev[p]
663 689 elif p in fullclnodes:
664 690 walk.extend([pp for pp in cl.parentrevs(p) if pp != nullrev])
665 691 elif p in precomputedellipsis:
666 692 walk.extend(
667 693 [pp for pp in precomputedellipsis[p] if pp != nullrev]
668 694 )
669 695 else:
670 696 # In this case, we've got an ellipsis with parents
671 697 # outside the current bundle (likely an
672 698 # incremental pull). We "know" that we can use the
673 699 # value of this same revlog at whatever revision
674 700 # is pointed to by linknode. "Know" is in scare
675 701 # quotes because I haven't done enough examination
676 702 # of edge cases to convince myself this is really
677 703 # a fact - it works for all the (admittedly
678 704 # thorough) cases in our testsuite, but I would be
679 705 # somewhat unsurprised to find a case in the wild
680 706 # where this breaks down a bit. That said, I don't
681 707 # know if it would hurt anything.
682 708 for i in pycompat.xrange(rev, 0, -1):
683 709 if store.linkrev(i) == clrev:
684 710 return i
685 711 # We failed to resolve a parent for this node, so
686 712 # we crash the changegroup construction.
687 713 raise error.Abort(
688 714 b"unable to resolve parent while packing '%s' %r"
689 715 b' for changeset %r' % (store.indexfile, rev, clrev)
690 716 )
691 717
692 718 return nullrev
693 719
694 720 if not linkparents or (store.parentrevs(rev) == (nullrev, nullrev)):
695 721 p1, p2 = nullrev, nullrev
696 722 elif len(linkparents) == 1:
697 723 (p1,) = sorted(local(p) for p in linkparents)
698 724 p2 = nullrev
699 725 else:
700 726 p1, p2 = sorted(local(p) for p in linkparents)
701 727
702 728 p1node, p2node = store.node(p1), store.node(p2)
703 729
704 730 return p1node, p2node, linknode
705 731
706 732
707 733 def deltagroup(
708 734 repo,
709 735 store,
710 736 nodes,
711 737 ischangelog,
712 738 lookup,
713 739 forcedeltaparentprev,
714 740 topic=None,
715 741 ellipses=False,
716 742 clrevtolocalrev=None,
717 743 fullclnodes=None,
718 744 precomputedellipsis=None,
719 745 ):
720 746 """Calculate deltas for a set of revisions.
721 747
722 748 Is a generator of ``revisiondelta`` instances.
723 749
724 750 If topic is not None, progress detail will be generated using this
725 751 topic name (e.g. changesets, manifests, etc).
726 752 """
727 753 if not nodes:
728 754 return
729 755
730 756 cl = repo.changelog
731 757
732 758 if ischangelog:
733 759 # `hg log` shows changesets in storage order. To preserve order
734 760 # across clones, send out changesets in storage order.
735 761 nodesorder = b'storage'
736 762 elif ellipses:
737 763 nodes = _sortnodesellipsis(store, nodes, cl, lookup)
738 764 nodesorder = b'nodes'
739 765 else:
740 766 nodesorder = None
741 767
742 768 # Perform ellipses filtering and revision massaging. We do this before
743 769 # emitrevisions() because a) filtering out revisions creates less work
744 770 # for emitrevisions() b) dropping revisions would break emitrevisions()'s
745 771 # assumptions about delta choices and we would possibly send a delta
746 772 # referencing a missing base revision.
747 773 #
748 774 # Also, calling lookup() has side-effects with regards to populating
749 775 # data structures. If we don't call lookup() for each node or if we call
750 776 # lookup() after the first pass through each node, things can break -
751 777 # possibly intermittently depending on the python hash seed! For that
752 778 # reason, we store a mapping of all linknodes during the initial node
753 779 # pass rather than use lookup() on the output side.
754 780 if ellipses:
755 781 filtered = []
756 782 adjustedparents = {}
757 783 linknodes = {}
758 784
759 785 for node in nodes:
760 786 rev = store.rev(node)
761 787 linknode = lookup(node)
762 788 linkrev = cl.rev(linknode)
763 789 clrevtolocalrev[linkrev] = rev
764 790
765 791 # If linknode is in fullclnodes, it means the corresponding
766 792 # changeset was a full changeset and is being sent unaltered.
767 793 if linknode in fullclnodes:
768 794 linknodes[node] = linknode
769 795
770 796 # If the corresponding changeset wasn't in the set computed
771 797 # as relevant to us, it should be dropped outright.
772 798 elif linkrev not in precomputedellipsis:
773 799 continue
774 800
775 801 else:
776 802 # We could probably do this later and avoid the dict
777 803 # holding state. But it likely doesn't matter.
778 804 p1node, p2node, linknode = _resolvenarrowrevisioninfo(
779 805 cl,
780 806 store,
781 807 ischangelog,
782 808 rev,
783 809 linkrev,
784 810 linknode,
785 811 clrevtolocalrev,
786 812 fullclnodes,
787 813 precomputedellipsis,
788 814 )
789 815
790 816 adjustedparents[node] = (p1node, p2node)
791 817 linknodes[node] = linknode
792 818
793 819 filtered.append(node)
794 820
795 821 nodes = filtered
796 822
797 823 # We expect the first pass to be fast, so we only engage the progress
798 824 # meter for constructing the revision deltas.
799 825 progress = None
800 826 if topic is not None:
801 827 progress = repo.ui.makeprogress(
802 828 topic, unit=_(b'chunks'), total=len(nodes)
803 829 )
804 830
805 831 configtarget = repo.ui.config(b'devel', b'bundle.delta')
806 832 if configtarget not in (b'', b'p1', b'full'):
807 833 msg = _("""config "devel.bundle.delta" as unknown value: %s""")
808 834 repo.ui.warn(msg % configtarget)
809 835
810 836 deltamode = repository.CG_DELTAMODE_STD
811 837 if forcedeltaparentprev:
812 838 deltamode = repository.CG_DELTAMODE_PREV
813 839 elif configtarget == b'p1':
814 840 deltamode = repository.CG_DELTAMODE_P1
815 841 elif configtarget == b'full':
816 842 deltamode = repository.CG_DELTAMODE_FULL
817 843
818 844 revisions = store.emitrevisions(
819 845 nodes,
820 846 nodesorder=nodesorder,
821 847 revisiondata=True,
822 848 assumehaveparentrevisions=not ellipses,
823 849 deltamode=deltamode,
824 850 )
825 851
826 852 for i, revision in enumerate(revisions):
827 853 if progress:
828 854 progress.update(i + 1)
829 855
830 856 if ellipses:
831 857 linknode = linknodes[revision.node]
832 858
833 859 if revision.node in adjustedparents:
834 860 p1node, p2node = adjustedparents[revision.node]
835 861 revision.p1node = p1node
836 862 revision.p2node = p2node
837 863 revision.flags |= repository.REVISION_FLAG_ELLIPSIS
838 864
839 865 else:
840 866 linknode = lookup(revision.node)
841 867
842 868 revision.linknode = linknode
843 869 yield revision
844 870
845 871 if progress:
846 872 progress.complete()
847 873
848 874
849 875 class cgpacker(object):
850 876 def __init__(
851 877 self,
852 878 repo,
853 879 oldmatcher,
854 880 matcher,
855 881 version,
856 882 builddeltaheader,
857 883 manifestsend,
858 884 forcedeltaparentprev=False,
859 885 bundlecaps=None,
860 886 ellipses=False,
861 887 shallow=False,
862 888 ellipsisroots=None,
863 889 fullnodes=None,
890 remote_sidedata=None,
864 891 ):
865 892 """Given a source repo, construct a bundler.
866 893
867 894 oldmatcher is a matcher that matches on files the client already has.
868 895 These will not be included in the changegroup.
869 896
870 897 matcher is a matcher that matches on files to include in the
871 898 changegroup. Used to facilitate sparse changegroups.
872 899
873 900 forcedeltaparentprev indicates whether delta parents must be against
874 901 the previous revision in a delta group. This should only be used for
875 902 compatibility with changegroup version 1.
876 903
877 904 builddeltaheader is a callable that constructs the header for a group
878 905 delta.
879 906
880 907 manifestsend is a chunk to send after manifests have been fully emitted.
881 908
882 909 ellipses indicates whether ellipsis serving mode is enabled.
883 910
884 911 bundlecaps is optional and can be used to specify the set of
885 912 capabilities which can be used to build the bundle. While bundlecaps is
886 913 unused in core Mercurial, extensions rely on this feature to communicate
887 914 capabilities to customize the changegroup packer.
888 915
889 916 shallow indicates whether shallow data might be sent. The packer may
890 917 need to pack file contents not introduced by the changes being packed.
891 918
892 919 fullnodes is the set of changelog nodes which should not be ellipsis
893 920 nodes. We store this rather than the set of nodes that should be
894 921 ellipsis because for very large histories we expect this to be
895 922 significantly smaller.
923
924 remote_sidedata is the set of sidedata categories wanted by the remote.
896 925 """
897 926 assert oldmatcher
898 927 assert matcher
899 928 self._oldmatcher = oldmatcher
900 929 self._matcher = matcher
901 930
902 931 self.version = version
903 932 self._forcedeltaparentprev = forcedeltaparentprev
904 933 self._builddeltaheader = builddeltaheader
905 934 self._manifestsend = manifestsend
906 935 self._ellipses = ellipses
907 936
908 937 # Set of capabilities we can use to build the bundle.
909 938 if bundlecaps is None:
910 939 bundlecaps = set()
911 940 self._bundlecaps = bundlecaps
912 941 self._isshallow = shallow
913 942 self._fullclnodes = fullnodes
914 943
915 944 # Maps ellipsis revs to their roots at the changelog level.
916 945 self._precomputedellipsis = ellipsisroots
917 946
918 947 self._repo = repo
919 948
920 949 if self._repo.ui.verbose and not self._repo.ui.debugflag:
921 950 self._verbosenote = self._repo.ui.note
922 951 else:
923 952 self._verbosenote = lambda s: None
924 953
925 954 def generate(
926 955 self, commonrevs, clnodes, fastpathlinkrev, source, changelog=True
927 956 ):
928 957 """Yield a sequence of changegroup byte chunks.
929 958 If changelog is False, changelog data won't be added to changegroup
930 959 """
931 960
932 961 repo = self._repo
933 962 cl = repo.changelog
934 963
935 964 self._verbosenote(_(b'uncompressed size of bundle content:\n'))
936 965 size = 0
937 966
938 967 clstate, deltas = self._generatechangelog(
939 968 cl, clnodes, generate=changelog
940 969 )
941 970 for delta in deltas:
942 971 for chunk in _revisiondeltatochunks(delta, self._builddeltaheader):
943 972 size += len(chunk)
944 973 yield chunk
945 974
946 975 close = closechunk()
947 976 size += len(close)
948 977 yield closechunk()
949 978
950 979 self._verbosenote(_(b'%8.i (changelog)\n') % size)
951 980
952 981 clrevorder = clstate[b'clrevorder']
953 982 manifests = clstate[b'manifests']
954 983 changedfiles = clstate[b'changedfiles']
955 984
956 985 # We need to make sure that the linkrev in the changegroup refers to
957 986 # the first changeset that introduced the manifest or file revision.
958 987 # The fastpath is usually safer than the slowpath, because the filelogs
959 988 # are walked in revlog order.
960 989 #
961 990 # When taking the slowpath when the manifest revlog uses generaldelta,
962 991 # the manifest may be walked in the "wrong" order. Without 'clrevorder',
963 992 # we would get an incorrect linkrev (see fix in cc0ff93d0c0c).
964 993 #
965 994 # When taking the fastpath, we are only vulnerable to reordering
966 995 # of the changelog itself. The changelog never uses generaldelta and is
967 996 # never reordered. To handle this case, we simply take the slowpath,
968 997 # which already has the 'clrevorder' logic. This was also fixed in
969 998 # cc0ff93d0c0c.
970 999
971 1000 # Treemanifests don't work correctly with fastpathlinkrev
972 1001 # either, because we don't discover which directory nodes to
973 1002 # send along with files. This could probably be fixed.
974 1003 fastpathlinkrev = fastpathlinkrev and not scmutil.istreemanifest(repo)
975 1004
976 1005 fnodes = {} # needed file nodes
977 1006
978 1007 size = 0
979 1008 it = self.generatemanifests(
980 1009 commonrevs,
981 1010 clrevorder,
982 1011 fastpathlinkrev,
983 1012 manifests,
984 1013 fnodes,
985 1014 source,
986 1015 clstate[b'clrevtomanifestrev'],
987 1016 )
988 1017
989 1018 for tree, deltas in it:
990 1019 if tree:
991 assert self.version == b'03'
1020 assert self.version in (b'03', b'04')
992 1021 chunk = _fileheader(tree)
993 1022 size += len(chunk)
994 1023 yield chunk
995 1024
996 1025 for delta in deltas:
997 1026 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
998 1027 for chunk in chunks:
999 1028 size += len(chunk)
1000 1029 yield chunk
1001 1030
1002 1031 close = closechunk()
1003 1032 size += len(close)
1004 1033 yield close
1005 1034
1006 1035 self._verbosenote(_(b'%8.i (manifests)\n') % size)
1007 1036 yield self._manifestsend
1008 1037
1009 1038 mfdicts = None
1010 1039 if self._ellipses and self._isshallow:
1011 1040 mfdicts = [
1012 1041 (repo.manifestlog[n].read(), lr)
1013 1042 for (n, lr) in pycompat.iteritems(manifests)
1014 1043 ]
1015 1044
1016 1045 manifests.clear()
1017 1046 clrevs = {cl.rev(x) for x in clnodes}
1018 1047
1019 1048 it = self.generatefiles(
1020 1049 changedfiles,
1021 1050 commonrevs,
1022 1051 source,
1023 1052 mfdicts,
1024 1053 fastpathlinkrev,
1025 1054 fnodes,
1026 1055 clrevs,
1027 1056 )
1028 1057
1029 1058 for path, deltas in it:
1030 1059 h = _fileheader(path)
1031 1060 size = len(h)
1032 1061 yield h
1033 1062
1034 1063 for delta in deltas:
1035 1064 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
1036 1065 for chunk in chunks:
1037 1066 size += len(chunk)
1038 1067 yield chunk
1039 1068
1040 1069 close = closechunk()
1041 1070 size += len(close)
1042 1071 yield close
1043 1072
1044 1073 self._verbosenote(_(b'%8.i %s\n') % (size, path))
1045 1074
1046 1075 yield closechunk()
1047 1076
1048 1077 if clnodes:
1049 1078 repo.hook(b'outgoing', node=hex(clnodes[0]), source=source)
1050 1079
1051 1080 def _generatechangelog(self, cl, nodes, generate=True):
1052 1081 """Generate data for changelog chunks.
1053 1082
1054 1083 Returns a 2-tuple of a dict containing state and an iterable of
1055 1084 byte chunks. The state will not be fully populated until the
1056 1085 chunk stream has been fully consumed.
1057 1086
1058 1087 if generate is False, the state will be fully populated and no chunk
1059 1088 stream will be yielded
1060 1089 """
1061 1090 clrevorder = {}
1062 1091 manifests = {}
1063 1092 mfl = self._repo.manifestlog
1064 1093 changedfiles = set()
1065 1094 clrevtomanifestrev = {}
1066 1095
1067 1096 state = {
1068 1097 b'clrevorder': clrevorder,
1069 1098 b'manifests': manifests,
1070 1099 b'changedfiles': changedfiles,
1071 1100 b'clrevtomanifestrev': clrevtomanifestrev,
1072 1101 }
1073 1102
1074 1103 if not (generate or self._ellipses):
1075 1104 # sort the nodes in storage order
1076 1105 nodes = sorted(nodes, key=cl.rev)
1077 1106 for node in nodes:
1078 1107 c = cl.changelogrevision(node)
1079 1108 clrevorder[node] = len(clrevorder)
1080 1109 # record the first changeset introducing this manifest version
1081 1110 manifests.setdefault(c.manifest, node)
1082 1111 # Record a complete list of potentially-changed files in
1083 1112 # this manifest.
1084 1113 changedfiles.update(c.files)
1085 1114
1086 1115 return state, ()
1087 1116
1088 1117 # Callback for the changelog, used to collect changed files and
1089 1118 # manifest nodes.
1090 1119 # Returns the linkrev node (identity in the changelog case).
1091 1120 def lookupcl(x):
1092 1121 c = cl.changelogrevision(x)
1093 1122 clrevorder[x] = len(clrevorder)
1094 1123
1095 1124 if self._ellipses:
1096 1125 # Only update manifests if x is going to be sent. Otherwise we
1097 1126 # end up with bogus linkrevs specified for manifests and
1098 1127 # we skip some manifest nodes that we should otherwise
1099 1128 # have sent.
1100 1129 if (
1101 1130 x in self._fullclnodes
1102 1131 or cl.rev(x) in self._precomputedellipsis
1103 1132 ):
1104 1133
1105 1134 manifestnode = c.manifest
1106 1135 # Record the first changeset introducing this manifest
1107 1136 # version.
1108 1137 manifests.setdefault(manifestnode, x)
1109 1138 # Set this narrow-specific dict so we have the lowest
1110 1139 # manifest revnum to look up for this cl revnum. (Part of
1111 1140 # mapping changelog ellipsis parents to manifest ellipsis
1112 1141 # parents)
1113 1142 clrevtomanifestrev.setdefault(
1114 1143 cl.rev(x), mfl.rev(manifestnode)
1115 1144 )
1116 1145 # We can't trust the changed files list in the changeset if the
1117 1146 # client requested a shallow clone.
1118 1147 if self._isshallow:
1119 1148 changedfiles.update(mfl[c.manifest].read().keys())
1120 1149 else:
1121 1150 changedfiles.update(c.files)
1122 1151 else:
1123 1152 # record the first changeset introducing this manifest version
1124 1153 manifests.setdefault(c.manifest, x)
1125 1154 # Record a complete list of potentially-changed files in
1126 1155 # this manifest.
1127 1156 changedfiles.update(c.files)
1128 1157
1129 1158 return x
1130 1159
1131 1160 gen = deltagroup(
1132 1161 self._repo,
1133 1162 cl,
1134 1163 nodes,
1135 1164 True,
1136 1165 lookupcl,
1137 1166 self._forcedeltaparentprev,
1138 1167 ellipses=self._ellipses,
1139 1168 topic=_(b'changesets'),
1140 1169 clrevtolocalrev={},
1141 1170 fullclnodes=self._fullclnodes,
1142 1171 precomputedellipsis=self._precomputedellipsis,
1143 1172 )
1144 1173
1145 1174 return state, gen
1146 1175
1147 1176 def generatemanifests(
1148 1177 self,
1149 1178 commonrevs,
1150 1179 clrevorder,
1151 1180 fastpathlinkrev,
1152 1181 manifests,
1153 1182 fnodes,
1154 1183 source,
1155 1184 clrevtolocalrev,
1156 1185 ):
1157 1186 """Returns an iterator of changegroup chunks containing manifests.
1158 1187
1159 1188 `source` is unused here, but is used by extensions like remotefilelog to
1160 1189 change what is sent based in pulls vs pushes, etc.
1161 1190 """
1162 1191 repo = self._repo
1163 1192 mfl = repo.manifestlog
1164 1193 tmfnodes = {b'': manifests}
1165 1194
1166 1195 # Callback for the manifest, used to collect linkrevs for filelog
1167 1196 # revisions.
1168 1197 # Returns the linkrev node (collected in lookupcl).
1169 1198 def makelookupmflinknode(tree, nodes):
1170 1199 if fastpathlinkrev:
1171 1200 assert not tree
1172 1201 return (
1173 1202 manifests.__getitem__
1174 1203 ) # pytype: disable=unsupported-operands
1175 1204
1176 1205 def lookupmflinknode(x):
1177 1206 """Callback for looking up the linknode for manifests.
1178 1207
1179 1208 Returns the linkrev node for the specified manifest.
1180 1209
1181 1210 SIDE EFFECT:
1182 1211
1183 1212 1) fclnodes gets populated with the list of relevant
1184 1213 file nodes if we're not using fastpathlinkrev
1185 1214 2) When treemanifests are in use, collects treemanifest nodes
1186 1215 to send
1187 1216
1188 1217 Note that this means manifests must be completely sent to
1189 1218 the client before you can trust the list of files and
1190 1219 treemanifests to send.
1191 1220 """
1192 1221 clnode = nodes[x]
1193 1222 mdata = mfl.get(tree, x).readfast(shallow=True)
1194 1223 for p, n, fl in mdata.iterentries():
1195 1224 if fl == b't': # subdirectory manifest
1196 1225 subtree = tree + p + b'/'
1197 1226 tmfclnodes = tmfnodes.setdefault(subtree, {})
1198 1227 tmfclnode = tmfclnodes.setdefault(n, clnode)
1199 1228 if clrevorder[clnode] < clrevorder[tmfclnode]:
1200 1229 tmfclnodes[n] = clnode
1201 1230 else:
1202 1231 f = tree + p
1203 1232 fclnodes = fnodes.setdefault(f, {})
1204 1233 fclnode = fclnodes.setdefault(n, clnode)
1205 1234 if clrevorder[clnode] < clrevorder[fclnode]:
1206 1235 fclnodes[n] = clnode
1207 1236 return clnode
1208 1237
1209 1238 return lookupmflinknode
1210 1239
1211 1240 while tmfnodes:
1212 1241 tree, nodes = tmfnodes.popitem()
1213 1242
1214 1243 should_visit = self._matcher.visitdir(tree[:-1])
1215 1244 if tree and not should_visit:
1216 1245 continue
1217 1246
1218 1247 store = mfl.getstorage(tree)
1219 1248
1220 1249 if not should_visit:
1221 1250 # No nodes to send because this directory is out of
1222 1251 # the client's view of the repository (probably
1223 1252 # because of narrow clones). Do this even for the root
1224 1253 # directory (tree=='')
1225 1254 prunednodes = []
1226 1255 else:
1227 1256 # Avoid sending any manifest nodes we can prove the
1228 1257 # client already has by checking linkrevs. See the
1229 1258 # related comment in generatefiles().
1230 1259 prunednodes = self._prunemanifests(store, nodes, commonrevs)
1231 1260
1232 1261 if tree and not prunednodes:
1233 1262 continue
1234 1263
1235 1264 lookupfn = makelookupmflinknode(tree, nodes)
1236 1265
1237 1266 deltas = deltagroup(
1238 1267 self._repo,
1239 1268 store,
1240 1269 prunednodes,
1241 1270 False,
1242 1271 lookupfn,
1243 1272 self._forcedeltaparentprev,
1244 1273 ellipses=self._ellipses,
1245 1274 topic=_(b'manifests'),
1246 1275 clrevtolocalrev=clrevtolocalrev,
1247 1276 fullclnodes=self._fullclnodes,
1248 1277 precomputedellipsis=self._precomputedellipsis,
1249 1278 )
1250 1279
1251 1280 if not self._oldmatcher.visitdir(store.tree[:-1]):
1252 1281 yield tree, deltas
1253 1282 else:
1254 1283 # 'deltas' is a generator and we need to consume it even if
1255 1284 # we are not going to send it because a side-effect is that
1256 1285 # it updates tmdnodes (via lookupfn)
1257 1286 for d in deltas:
1258 1287 pass
1259 1288 if not tree:
1260 1289 yield tree, []
1261 1290
1262 1291 def _prunemanifests(self, store, nodes, commonrevs):
1263 1292 if not self._ellipses:
1264 1293 # In non-ellipses case and large repositories, it is better to
1265 1294 # prevent calling of store.rev and store.linkrev on a lot of
1266 1295 # nodes as compared to sending some extra data
1267 1296 return nodes.copy()
1268 1297 # This is split out as a separate method to allow filtering
1269 1298 # commonrevs in extension code.
1270 1299 #
1271 1300 # TODO(augie): this shouldn't be required, instead we should
1272 1301 # make filtering of revisions to send delegated to the store
1273 1302 # layer.
1274 1303 frev, flr = store.rev, store.linkrev
1275 1304 return [n for n in nodes if flr(frev(n)) not in commonrevs]
1276 1305
1277 1306 # The 'source' parameter is useful for extensions
1278 1307 def generatefiles(
1279 1308 self,
1280 1309 changedfiles,
1281 1310 commonrevs,
1282 1311 source,
1283 1312 mfdicts,
1284 1313 fastpathlinkrev,
1285 1314 fnodes,
1286 1315 clrevs,
1287 1316 ):
1288 1317 changedfiles = [
1289 1318 f
1290 1319 for f in changedfiles
1291 1320 if self._matcher(f) and not self._oldmatcher(f)
1292 1321 ]
1293 1322
1294 1323 if not fastpathlinkrev:
1295 1324
1296 1325 def normallinknodes(unused, fname):
1297 1326 return fnodes.get(fname, {})
1298 1327
1299 1328 else:
1300 1329 cln = self._repo.changelog.node
1301 1330
1302 1331 def normallinknodes(store, fname):
1303 1332 flinkrev = store.linkrev
1304 1333 fnode = store.node
1305 1334 revs = ((r, flinkrev(r)) for r in store)
1306 1335 return {fnode(r): cln(lr) for r, lr in revs if lr in clrevs}
1307 1336
1308 1337 clrevtolocalrev = {}
1309 1338
1310 1339 if self._isshallow:
1311 1340 # In a shallow clone, the linknodes callback needs to also include
1312 1341 # those file nodes that are in the manifests we sent but weren't
1313 1342 # introduced by those manifests.
1314 1343 commonctxs = [self._repo[c] for c in commonrevs]
1315 1344 clrev = self._repo.changelog.rev
1316 1345
1317 1346 def linknodes(flog, fname):
1318 1347 for c in commonctxs:
1319 1348 try:
1320 1349 fnode = c.filenode(fname)
1321 1350 clrevtolocalrev[c.rev()] = flog.rev(fnode)
1322 1351 except error.ManifestLookupError:
1323 1352 pass
1324 1353 links = normallinknodes(flog, fname)
1325 1354 if len(links) != len(mfdicts):
1326 1355 for mf, lr in mfdicts:
1327 1356 fnode = mf.get(fname, None)
1328 1357 if fnode in links:
1329 1358 links[fnode] = min(links[fnode], lr, key=clrev)
1330 1359 elif fnode:
1331 1360 links[fnode] = lr
1332 1361 return links
1333 1362
1334 1363 else:
1335 1364 linknodes = normallinknodes
1336 1365
1337 1366 repo = self._repo
1338 1367 progress = repo.ui.makeprogress(
1339 1368 _(b'files'), unit=_(b'files'), total=len(changedfiles)
1340 1369 )
1341 1370 for i, fname in enumerate(sorted(changedfiles)):
1342 1371 filerevlog = repo.file(fname)
1343 1372 if not filerevlog:
1344 1373 raise error.Abort(
1345 1374 _(b"empty or missing file data for %s") % fname
1346 1375 )
1347 1376
1348 1377 clrevtolocalrev.clear()
1349 1378
1350 1379 linkrevnodes = linknodes(filerevlog, fname)
1351 1380 # Lookup for filenodes, we collected the linkrev nodes above in the
1352 1381 # fastpath case and with lookupmf in the slowpath case.
1353 1382 def lookupfilelog(x):
1354 1383 return linkrevnodes[x]
1355 1384
1356 1385 frev, flr = filerevlog.rev, filerevlog.linkrev
1357 1386 # Skip sending any filenode we know the client already
1358 1387 # has. This avoids over-sending files relatively
1359 1388 # inexpensively, so it's not a problem if we under-filter
1360 1389 # here.
1361 1390 filenodes = [
1362 1391 n for n in linkrevnodes if flr(frev(n)) not in commonrevs
1363 1392 ]
1364 1393
1365 1394 if not filenodes:
1366 1395 continue
1367 1396
1368 1397 progress.update(i + 1, item=fname)
1369 1398
1370 1399 deltas = deltagroup(
1371 1400 self._repo,
1372 1401 filerevlog,
1373 1402 filenodes,
1374 1403 False,
1375 1404 lookupfilelog,
1376 1405 self._forcedeltaparentprev,
1377 1406 ellipses=self._ellipses,
1378 1407 clrevtolocalrev=clrevtolocalrev,
1379 1408 fullclnodes=self._fullclnodes,
1380 1409 precomputedellipsis=self._precomputedellipsis,
1381 1410 )
1382 1411
1383 1412 yield fname, deltas
1384 1413
1385 1414 progress.complete()
1386 1415
1387 1416
1388 1417 def _makecg1packer(
1389 1418 repo,
1390 1419 oldmatcher,
1391 1420 matcher,
1392 1421 bundlecaps,
1393 1422 ellipses=False,
1394 1423 shallow=False,
1395 1424 ellipsisroots=None,
1396 1425 fullnodes=None,
1426 remote_sidedata=None,
1397 1427 ):
1398 1428 builddeltaheader = lambda d: _CHANGEGROUPV1_DELTA_HEADER.pack(
1399 1429 d.node, d.p1node, d.p2node, d.linknode
1400 1430 )
1401 1431
1402 1432 return cgpacker(
1403 1433 repo,
1404 1434 oldmatcher,
1405 1435 matcher,
1406 1436 b'01',
1407 1437 builddeltaheader=builddeltaheader,
1408 1438 manifestsend=b'',
1409 1439 forcedeltaparentprev=True,
1410 1440 bundlecaps=bundlecaps,
1411 1441 ellipses=ellipses,
1412 1442 shallow=shallow,
1413 1443 ellipsisroots=ellipsisroots,
1414 1444 fullnodes=fullnodes,
1415 1445 )
1416 1446
1417 1447
1418 1448 def _makecg2packer(
1419 1449 repo,
1420 1450 oldmatcher,
1421 1451 matcher,
1422 1452 bundlecaps,
1423 1453 ellipses=False,
1424 1454 shallow=False,
1425 1455 ellipsisroots=None,
1426 1456 fullnodes=None,
1457 remote_sidedata=None,
1427 1458 ):
1428 1459 builddeltaheader = lambda d: _CHANGEGROUPV2_DELTA_HEADER.pack(
1429 1460 d.node, d.p1node, d.p2node, d.basenode, d.linknode
1430 1461 )
1431 1462
1432 1463 return cgpacker(
1433 1464 repo,
1434 1465 oldmatcher,
1435 1466 matcher,
1436 1467 b'02',
1437 1468 builddeltaheader=builddeltaheader,
1438 1469 manifestsend=b'',
1439 1470 bundlecaps=bundlecaps,
1440 1471 ellipses=ellipses,
1441 1472 shallow=shallow,
1442 1473 ellipsisroots=ellipsisroots,
1443 1474 fullnodes=fullnodes,
1444 1475 )
1445 1476
1446 1477
1447 1478 def _makecg3packer(
1448 1479 repo,
1449 1480 oldmatcher,
1450 1481 matcher,
1451 1482 bundlecaps,
1452 1483 ellipses=False,
1453 1484 shallow=False,
1454 1485 ellipsisroots=None,
1455 1486 fullnodes=None,
1487 remote_sidedata=None,
1456 1488 ):
1457 1489 builddeltaheader = lambda d: _CHANGEGROUPV3_DELTA_HEADER.pack(
1458 1490 d.node, d.p1node, d.p2node, d.basenode, d.linknode, d.flags
1459 1491 )
1460 1492
1461 1493 return cgpacker(
1462 1494 repo,
1463 1495 oldmatcher,
1464 1496 matcher,
1465 1497 b'03',
1466 1498 builddeltaheader=builddeltaheader,
1467 1499 manifestsend=closechunk(),
1468 1500 bundlecaps=bundlecaps,
1469 1501 ellipses=ellipses,
1470 1502 shallow=shallow,
1471 1503 ellipsisroots=ellipsisroots,
1472 1504 fullnodes=fullnodes,
1473 1505 )
1474 1506
1475 1507
1508 def _makecg4packer(
1509 repo,
1510 oldmatcher,
1511 matcher,
1512 bundlecaps,
1513 ellipses=False,
1514 shallow=False,
1515 ellipsisroots=None,
1516 fullnodes=None,
1517 remote_sidedata=None,
1518 ):
1519 # Same header func as cg3. Sidedata is in a separate chunk from the delta to
1520 # differenciate "raw delta" and sidedata.
1521 builddeltaheader = lambda d: _CHANGEGROUPV3_DELTA_HEADER.pack(
1522 d.node, d.p1node, d.p2node, d.basenode, d.linknode, d.flags
1523 )
1524
1525 return cgpacker(
1526 repo,
1527 oldmatcher,
1528 matcher,
1529 b'04',
1530 builddeltaheader=builddeltaheader,
1531 manifestsend=closechunk(),
1532 bundlecaps=bundlecaps,
1533 ellipses=ellipses,
1534 shallow=shallow,
1535 ellipsisroots=ellipsisroots,
1536 fullnodes=fullnodes,
1537 remote_sidedata=remote_sidedata,
1538 )
1539
1540
1476 1541 _packermap = {
1477 1542 b'01': (_makecg1packer, cg1unpacker),
1478 1543 # cg2 adds support for exchanging generaldelta
1479 1544 b'02': (_makecg2packer, cg2unpacker),
1480 1545 # cg3 adds support for exchanging revlog flags and treemanifests
1481 1546 b'03': (_makecg3packer, cg3unpacker),
1547 # ch4 adds support for exchanging sidedata
1548 b'04': (_makecg4packer, cg4unpacker),
1482 1549 }
1483 1550
1484 1551
1485 1552 def allsupportedversions(repo):
1486 1553 versions = set(_packermap.keys())
1487 1554 needv03 = False
1488 1555 if (
1489 1556 repo.ui.configbool(b'experimental', b'changegroup3')
1490 1557 or repo.ui.configbool(b'experimental', b'treemanifest')
1491 1558 or scmutil.istreemanifest(repo)
1492 1559 ):
1493 1560 # we keep version 03 because we need to to exchange treemanifest data
1494 1561 #
1495 1562 # we also keep vresion 01 and 02, because it is possible for repo to
1496 1563 # contains both normal and tree manifest at the same time. so using
1497 1564 # older version to pull data is viable
1498 1565 #
1499 1566 # (or even to push subset of history)
1500 1567 needv03 = True
1501 if b'exp-sidedata-flag' in repo.requirements:
1502 needv03 = True
1503 # don't attempt to use 01/02 until we do sidedata cleaning
1504 versions.discard(b'01')
1505 versions.discard(b'02')
1568 has_revlogv2 = requirements.REVLOGV2_REQUIREMENT in repo.requirements
1569 if not has_revlogv2:
1570 versions.discard(b'04')
1506 1571 if not needv03:
1507 1572 versions.discard(b'03')
1508 1573 return versions
1509 1574
1510 1575
1511 1576 # Changegroup versions that can be applied to the repo
1512 1577 def supportedincomingversions(repo):
1513 1578 return allsupportedversions(repo)
1514 1579
1515 1580
1516 1581 # Changegroup versions that can be created from the repo
1517 1582 def supportedoutgoingversions(repo):
1518 1583 versions = allsupportedversions(repo)
1519 1584 if scmutil.istreemanifest(repo):
1520 1585 # Versions 01 and 02 support only flat manifests and it's just too
1521 1586 # expensive to convert between the flat manifest and tree manifest on
1522 1587 # the fly. Since tree manifests are hashed differently, all of history
1523 1588 # would have to be converted. Instead, we simply don't even pretend to
1524 1589 # support versions 01 and 02.
1525 1590 versions.discard(b'01')
1526 1591 versions.discard(b'02')
1527 1592 if requirements.NARROW_REQUIREMENT in repo.requirements:
1528 1593 # Versions 01 and 02 don't support revlog flags, and we need to
1529 1594 # support that for stripping and unbundling to work.
1530 1595 versions.discard(b'01')
1531 1596 versions.discard(b'02')
1532 1597 if LFS_REQUIREMENT in repo.requirements:
1533 1598 # Versions 01 and 02 don't support revlog flags, and we need to
1534 1599 # mark LFS entries with REVIDX_EXTSTORED.
1535 1600 versions.discard(b'01')
1536 1601 versions.discard(b'02')
1537 1602
1538 1603 return versions
1539 1604
1540 1605
1541 1606 def localversion(repo):
1542 1607 # Finds the best version to use for bundles that are meant to be used
1543 1608 # locally, such as those from strip and shelve, and temporary bundles.
1544 1609 return max(supportedoutgoingversions(repo))
1545 1610
1546 1611
1547 1612 def safeversion(repo):
1548 1613 # Finds the smallest version that it's safe to assume clients of the repo
1549 1614 # will support. For example, all hg versions that support generaldelta also
1550 1615 # support changegroup 02.
1551 1616 versions = supportedoutgoingversions(repo)
1552 1617 if requirements.GENERALDELTA_REQUIREMENT in repo.requirements:
1553 1618 versions.discard(b'01')
1554 1619 assert versions
1555 1620 return min(versions)
1556 1621
1557 1622
1558 1623 def getbundler(
1559 1624 version,
1560 1625 repo,
1561 1626 bundlecaps=None,
1562 1627 oldmatcher=None,
1563 1628 matcher=None,
1564 1629 ellipses=False,
1565 1630 shallow=False,
1566 1631 ellipsisroots=None,
1567 1632 fullnodes=None,
1633 remote_sidedata=None,
1568 1634 ):
1569 1635 assert version in supportedoutgoingversions(repo)
1570 1636
1571 1637 if matcher is None:
1572 1638 matcher = matchmod.always()
1573 1639 if oldmatcher is None:
1574 1640 oldmatcher = matchmod.never()
1575 1641
1576 1642 if version == b'01' and not matcher.always():
1577 1643 raise error.ProgrammingError(
1578 1644 b'version 01 changegroups do not support sparse file matchers'
1579 1645 )
1580 1646
1581 1647 if ellipses and version in (b'01', b'02'):
1582 1648 raise error.Abort(
1583 1649 _(
1584 1650 b'ellipsis nodes require at least cg3 on client and server, '
1585 1651 b'but negotiated version %s'
1586 1652 )
1587 1653 % version
1588 1654 )
1589 1655
1590 1656 # Requested files could include files not in the local store. So
1591 1657 # filter those out.
1592 1658 matcher = repo.narrowmatch(matcher)
1593 1659
1594 1660 fn = _packermap[version][0]
1595 1661 return fn(
1596 1662 repo,
1597 1663 oldmatcher,
1598 1664 matcher,
1599 1665 bundlecaps,
1600 1666 ellipses=ellipses,
1601 1667 shallow=shallow,
1602 1668 ellipsisroots=ellipsisroots,
1603 1669 fullnodes=fullnodes,
1670 remote_sidedata=remote_sidedata,
1604 1671 )
1605 1672
1606 1673
1607 1674 def getunbundler(version, fh, alg, extras=None):
1608 1675 return _packermap[version][1](fh, alg, extras=extras)
1609 1676
1610 1677
1611 1678 def _changegroupinfo(repo, nodes, source):
1612 1679 if repo.ui.verbose or source == b'bundle':
1613 1680 repo.ui.status(_(b"%d changesets found\n") % len(nodes))
1614 1681 if repo.ui.debugflag:
1615 1682 repo.ui.debug(b"list of changesets:\n")
1616 1683 for node in nodes:
1617 1684 repo.ui.debug(b"%s\n" % hex(node))
1618 1685
1619 1686
1620 1687 def makechangegroup(
1621 1688 repo, outgoing, version, source, fastpath=False, bundlecaps=None
1622 1689 ):
1623 1690 cgstream = makestream(
1624 1691 repo,
1625 1692 outgoing,
1626 1693 version,
1627 1694 source,
1628 1695 fastpath=fastpath,
1629 1696 bundlecaps=bundlecaps,
1630 1697 )
1631 1698 return getunbundler(
1632 1699 version,
1633 1700 util.chunkbuffer(cgstream),
1634 1701 None,
1635 1702 {b'clcount': len(outgoing.missing)},
1636 1703 )
1637 1704
1638 1705
1639 1706 def makestream(
1640 1707 repo,
1641 1708 outgoing,
1642 1709 version,
1643 1710 source,
1644 1711 fastpath=False,
1645 1712 bundlecaps=None,
1646 1713 matcher=None,
1714 remote_sidedata=None,
1647 1715 ):
1648 bundler = getbundler(version, repo, bundlecaps=bundlecaps, matcher=matcher)
1716 bundler = getbundler(
1717 version,
1718 repo,
1719 bundlecaps=bundlecaps,
1720 matcher=matcher,
1721 remote_sidedata=remote_sidedata,
1722 )
1649 1723
1650 1724 repo = repo.unfiltered()
1651 1725 commonrevs = outgoing.common
1652 1726 csets = outgoing.missing
1653 1727 heads = outgoing.ancestorsof
1654 1728 # We go through the fast path if we get told to, or if all (unfiltered
1655 1729 # heads have been requested (since we then know there all linkrevs will
1656 1730 # be pulled by the client).
1657 1731 heads.sort()
1658 1732 fastpathlinkrev = fastpath or (
1659 1733 repo.filtername is None and heads == sorted(repo.heads())
1660 1734 )
1661 1735
1662 1736 repo.hook(b'preoutgoing', throw=True, source=source)
1663 1737 _changegroupinfo(repo, csets, source)
1664 1738 return bundler.generate(commonrevs, csets, fastpathlinkrev, source)
1665 1739
1666 1740
1667 1741 def _addchangegroupfiles(repo, source, revmap, trp, expectedfiles, needfiles):
1668 1742 revisions = 0
1669 1743 files = 0
1670 1744 progress = repo.ui.makeprogress(
1671 1745 _(b'files'), unit=_(b'files'), total=expectedfiles
1672 1746 )
1673 1747 for chunkdata in iter(source.filelogheader, {}):
1674 1748 files += 1
1675 1749 f = chunkdata[b"filename"]
1676 1750 repo.ui.debug(b"adding %s revisions\n" % f)
1677 1751 progress.increment()
1678 1752 fl = repo.file(f)
1679 1753 o = len(fl)
1680 1754 try:
1681 1755 deltas = source.deltaiter()
1682 1756 if not fl.addgroup(deltas, revmap, trp):
1683 1757 raise error.Abort(_(b"received file revlog group is empty"))
1684 1758 except error.CensoredBaseError as e:
1685 1759 raise error.Abort(_(b"received delta base is censored: %s") % e)
1686 1760 revisions += len(fl) - o
1687 1761 if f in needfiles:
1688 1762 needs = needfiles[f]
1689 1763 for new in pycompat.xrange(o, len(fl)):
1690 1764 n = fl.node(new)
1691 1765 if n in needs:
1692 1766 needs.remove(n)
1693 1767 else:
1694 1768 raise error.Abort(_(b"received spurious file revlog entry"))
1695 1769 if not needs:
1696 1770 del needfiles[f]
1697 1771 progress.complete()
1698 1772
1699 1773 for f, needs in pycompat.iteritems(needfiles):
1700 1774 fl = repo.file(f)
1701 1775 for n in needs:
1702 1776 try:
1703 1777 fl.rev(n)
1704 1778 except error.LookupError:
1705 1779 raise error.Abort(
1706 1780 _(b'missing file data for %s:%s - run hg verify')
1707 1781 % (f, hex(n))
1708 1782 )
1709 1783
1710 1784 return revisions, files
@@ -1,4773 +1,4773 b''
1 1 # debugcommands.py - command processing for debug* commands
2 2 #
3 3 # Copyright 2005-2016 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 codecs
11 11 import collections
12 12 import difflib
13 13 import errno
14 14 import glob
15 15 import operator
16 16 import os
17 17 import platform
18 18 import random
19 19 import re
20 20 import socket
21 21 import ssl
22 22 import stat
23 23 import string
24 24 import subprocess
25 25 import sys
26 26 import time
27 27
28 28 from .i18n import _
29 29 from .node import (
30 30 bin,
31 31 hex,
32 32 nullid,
33 33 nullrev,
34 34 short,
35 35 )
36 36 from .pycompat import (
37 37 getattr,
38 38 open,
39 39 )
40 40 from . import (
41 41 bundle2,
42 42 bundlerepo,
43 43 changegroup,
44 44 cmdutil,
45 45 color,
46 46 context,
47 47 copies,
48 48 dagparser,
49 49 encoding,
50 50 error,
51 51 exchange,
52 52 extensions,
53 53 filemerge,
54 54 filesetlang,
55 55 formatter,
56 56 hg,
57 57 httppeer,
58 58 localrepo,
59 59 lock as lockmod,
60 60 logcmdutil,
61 61 mergestate as mergestatemod,
62 62 metadata,
63 63 obsolete,
64 64 obsutil,
65 65 pathutil,
66 66 phases,
67 67 policy,
68 68 pvec,
69 69 pycompat,
70 70 registrar,
71 71 repair,
72 72 repoview,
73 73 revlog,
74 74 revset,
75 75 revsetlang,
76 76 scmutil,
77 77 setdiscovery,
78 78 simplemerge,
79 79 sshpeer,
80 80 sslutil,
81 81 streamclone,
82 82 strip,
83 83 tags as tagsmod,
84 84 templater,
85 85 treediscovery,
86 86 upgrade,
87 87 url as urlmod,
88 88 util,
89 89 vfs as vfsmod,
90 90 wireprotoframing,
91 91 wireprotoserver,
92 92 wireprotov2peer,
93 93 )
94 94 from .utils import (
95 95 cborutil,
96 96 compression,
97 97 dateutil,
98 98 procutil,
99 99 stringutil,
100 100 )
101 101
102 102 from .revlogutils import (
103 103 deltas as deltautil,
104 104 nodemap,
105 105 sidedata,
106 106 )
107 107
108 108 release = lockmod.release
109 109
110 110 table = {}
111 111 table.update(strip.command._table)
112 112 command = registrar.command(table)
113 113
114 114
115 115 @command(b'debugancestor', [], _(b'[INDEX] REV1 REV2'), optionalrepo=True)
116 116 def debugancestor(ui, repo, *args):
117 117 """find the ancestor revision of two revisions in a given index"""
118 118 if len(args) == 3:
119 119 index, rev1, rev2 = args
120 120 r = revlog.revlog(vfsmod.vfs(encoding.getcwd(), audit=False), index)
121 121 lookup = r.lookup
122 122 elif len(args) == 2:
123 123 if not repo:
124 124 raise error.Abort(
125 125 _(b'there is no Mercurial repository here (.hg not found)')
126 126 )
127 127 rev1, rev2 = args
128 128 r = repo.changelog
129 129 lookup = repo.lookup
130 130 else:
131 131 raise error.Abort(_(b'either two or three arguments required'))
132 132 a = r.ancestor(lookup(rev1), lookup(rev2))
133 133 ui.write(b'%d:%s\n' % (r.rev(a), hex(a)))
134 134
135 135
136 136 @command(b'debugantivirusrunning', [])
137 137 def debugantivirusrunning(ui, repo):
138 138 """attempt to trigger an antivirus scanner to see if one is active"""
139 139 with repo.cachevfs.open('eicar-test-file.com', b'wb') as f:
140 140 f.write(
141 141 util.b85decode(
142 142 # This is a base85-armored version of the EICAR test file. See
143 143 # https://en.wikipedia.org/wiki/EICAR_test_file for details.
144 144 b'ST#=}P$fV?P+K%yP+C|uG$>GBDK|qyDK~v2MM*<JQY}+dK~6+LQba95P'
145 145 b'E<)&Nm5l)EmTEQR4qnHOhq9iNGnJx'
146 146 )
147 147 )
148 148 # Give an AV engine time to scan the file.
149 149 time.sleep(2)
150 150 util.unlink(repo.cachevfs.join('eicar-test-file.com'))
151 151
152 152
153 153 @command(b'debugapplystreamclonebundle', [], b'FILE')
154 154 def debugapplystreamclonebundle(ui, repo, fname):
155 155 """apply a stream clone bundle file"""
156 156 f = hg.openpath(ui, fname)
157 157 gen = exchange.readbundle(ui, f, fname)
158 158 gen.apply(repo)
159 159
160 160
161 161 @command(
162 162 b'debugbuilddag',
163 163 [
164 164 (
165 165 b'm',
166 166 b'mergeable-file',
167 167 None,
168 168 _(b'add single file mergeable changes'),
169 169 ),
170 170 (
171 171 b'o',
172 172 b'overwritten-file',
173 173 None,
174 174 _(b'add single file all revs overwrite'),
175 175 ),
176 176 (b'n', b'new-file', None, _(b'add new file at each rev')),
177 177 ],
178 178 _(b'[OPTION]... [TEXT]'),
179 179 )
180 180 def debugbuilddag(
181 181 ui,
182 182 repo,
183 183 text=None,
184 184 mergeable_file=False,
185 185 overwritten_file=False,
186 186 new_file=False,
187 187 ):
188 188 """builds a repo with a given DAG from scratch in the current empty repo
189 189
190 190 The description of the DAG is read from stdin if not given on the
191 191 command line.
192 192
193 193 Elements:
194 194
195 195 - "+n" is a linear run of n nodes based on the current default parent
196 196 - "." is a single node based on the current default parent
197 197 - "$" resets the default parent to null (implied at the start);
198 198 otherwise the default parent is always the last node created
199 199 - "<p" sets the default parent to the backref p
200 200 - "*p" is a fork at parent p, which is a backref
201 201 - "*p1/p2" is a merge of parents p1 and p2, which are backrefs
202 202 - "/p2" is a merge of the preceding node and p2
203 203 - ":tag" defines a local tag for the preceding node
204 204 - "@branch" sets the named branch for subsequent nodes
205 205 - "#...\\n" is a comment up to the end of the line
206 206
207 207 Whitespace between the above elements is ignored.
208 208
209 209 A backref is either
210 210
211 211 - a number n, which references the node curr-n, where curr is the current
212 212 node, or
213 213 - the name of a local tag you placed earlier using ":tag", or
214 214 - empty to denote the default parent.
215 215
216 216 All string valued-elements are either strictly alphanumeric, or must
217 217 be enclosed in double quotes ("..."), with "\\" as escape character.
218 218 """
219 219
220 220 if text is None:
221 221 ui.status(_(b"reading DAG from stdin\n"))
222 222 text = ui.fin.read()
223 223
224 224 cl = repo.changelog
225 225 if len(cl) > 0:
226 226 raise error.Abort(_(b'repository is not empty'))
227 227
228 228 # determine number of revs in DAG
229 229 total = 0
230 230 for type, data in dagparser.parsedag(text):
231 231 if type == b'n':
232 232 total += 1
233 233
234 234 if mergeable_file:
235 235 linesperrev = 2
236 236 # make a file with k lines per rev
237 237 initialmergedlines = [
238 238 b'%d' % i for i in pycompat.xrange(0, total * linesperrev)
239 239 ]
240 240 initialmergedlines.append(b"")
241 241
242 242 tags = []
243 243 progress = ui.makeprogress(
244 244 _(b'building'), unit=_(b'revisions'), total=total
245 245 )
246 246 with progress, repo.wlock(), repo.lock(), repo.transaction(b"builddag"):
247 247 at = -1
248 248 atbranch = b'default'
249 249 nodeids = []
250 250 id = 0
251 251 progress.update(id)
252 252 for type, data in dagparser.parsedag(text):
253 253 if type == b'n':
254 254 ui.note((b'node %s\n' % pycompat.bytestr(data)))
255 255 id, ps = data
256 256
257 257 files = []
258 258 filecontent = {}
259 259
260 260 p2 = None
261 261 if mergeable_file:
262 262 fn = b"mf"
263 263 p1 = repo[ps[0]]
264 264 if len(ps) > 1:
265 265 p2 = repo[ps[1]]
266 266 pa = p1.ancestor(p2)
267 267 base, local, other = [
268 268 x[fn].data() for x in (pa, p1, p2)
269 269 ]
270 270 m3 = simplemerge.Merge3Text(base, local, other)
271 271 ml = [l.strip() for l in m3.merge_lines()]
272 272 ml.append(b"")
273 273 elif at > 0:
274 274 ml = p1[fn].data().split(b"\n")
275 275 else:
276 276 ml = initialmergedlines
277 277 ml[id * linesperrev] += b" r%i" % id
278 278 mergedtext = b"\n".join(ml)
279 279 files.append(fn)
280 280 filecontent[fn] = mergedtext
281 281
282 282 if overwritten_file:
283 283 fn = b"of"
284 284 files.append(fn)
285 285 filecontent[fn] = b"r%i\n" % id
286 286
287 287 if new_file:
288 288 fn = b"nf%i" % id
289 289 files.append(fn)
290 290 filecontent[fn] = b"r%i\n" % id
291 291 if len(ps) > 1:
292 292 if not p2:
293 293 p2 = repo[ps[1]]
294 294 for fn in p2:
295 295 if fn.startswith(b"nf"):
296 296 files.append(fn)
297 297 filecontent[fn] = p2[fn].data()
298 298
299 299 def fctxfn(repo, cx, path):
300 300 if path in filecontent:
301 301 return context.memfilectx(
302 302 repo, cx, path, filecontent[path]
303 303 )
304 304 return None
305 305
306 306 if len(ps) == 0 or ps[0] < 0:
307 307 pars = [None, None]
308 308 elif len(ps) == 1:
309 309 pars = [nodeids[ps[0]], None]
310 310 else:
311 311 pars = [nodeids[p] for p in ps]
312 312 cx = context.memctx(
313 313 repo,
314 314 pars,
315 315 b"r%i" % id,
316 316 files,
317 317 fctxfn,
318 318 date=(id, 0),
319 319 user=b"debugbuilddag",
320 320 extra={b'branch': atbranch},
321 321 )
322 322 nodeid = repo.commitctx(cx)
323 323 nodeids.append(nodeid)
324 324 at = id
325 325 elif type == b'l':
326 326 id, name = data
327 327 ui.note((b'tag %s\n' % name))
328 328 tags.append(b"%s %s\n" % (hex(repo.changelog.node(id)), name))
329 329 elif type == b'a':
330 330 ui.note((b'branch %s\n' % data))
331 331 atbranch = data
332 332 progress.update(id)
333 333
334 334 if tags:
335 335 repo.vfs.write(b"localtags", b"".join(tags))
336 336
337 337
338 338 def _debugchangegroup(ui, gen, all=None, indent=0, **opts):
339 339 indent_string = b' ' * indent
340 340 if all:
341 341 ui.writenoi18n(
342 342 b"%sformat: id, p1, p2, cset, delta base, len(delta)\n"
343 343 % indent_string
344 344 )
345 345
346 346 def showchunks(named):
347 347 ui.write(b"\n%s%s\n" % (indent_string, named))
348 348 for deltadata in gen.deltaiter():
349 node, p1, p2, cs, deltabase, delta, flags = deltadata
349 node, p1, p2, cs, deltabase, delta, flags, sidedata = deltadata
350 350 ui.write(
351 351 b"%s%s %s %s %s %s %d\n"
352 352 % (
353 353 indent_string,
354 354 hex(node),
355 355 hex(p1),
356 356 hex(p2),
357 357 hex(cs),
358 358 hex(deltabase),
359 359 len(delta),
360 360 )
361 361 )
362 362
363 363 gen.changelogheader()
364 364 showchunks(b"changelog")
365 365 gen.manifestheader()
366 366 showchunks(b"manifest")
367 367 for chunkdata in iter(gen.filelogheader, {}):
368 368 fname = chunkdata[b'filename']
369 369 showchunks(fname)
370 370 else:
371 371 if isinstance(gen, bundle2.unbundle20):
372 372 raise error.Abort(_(b'use debugbundle2 for this file'))
373 373 gen.changelogheader()
374 374 for deltadata in gen.deltaiter():
375 node, p1, p2, cs, deltabase, delta, flags = deltadata
375 node, p1, p2, cs, deltabase, delta, flags, sidedata = deltadata
376 376 ui.write(b"%s%s\n" % (indent_string, hex(node)))
377 377
378 378
379 379 def _debugobsmarkers(ui, part, indent=0, **opts):
380 380 """display version and markers contained in 'data'"""
381 381 opts = pycompat.byteskwargs(opts)
382 382 data = part.read()
383 383 indent_string = b' ' * indent
384 384 try:
385 385 version, markers = obsolete._readmarkers(data)
386 386 except error.UnknownVersion as exc:
387 387 msg = b"%sunsupported version: %s (%d bytes)\n"
388 388 msg %= indent_string, exc.version, len(data)
389 389 ui.write(msg)
390 390 else:
391 391 msg = b"%sversion: %d (%d bytes)\n"
392 392 msg %= indent_string, version, len(data)
393 393 ui.write(msg)
394 394 fm = ui.formatter(b'debugobsolete', opts)
395 395 for rawmarker in sorted(markers):
396 396 m = obsutil.marker(None, rawmarker)
397 397 fm.startitem()
398 398 fm.plain(indent_string)
399 399 cmdutil.showmarker(fm, m)
400 400 fm.end()
401 401
402 402
403 403 def _debugphaseheads(ui, data, indent=0):
404 404 """display version and markers contained in 'data'"""
405 405 indent_string = b' ' * indent
406 406 headsbyphase = phases.binarydecode(data)
407 407 for phase in phases.allphases:
408 408 for head in headsbyphase[phase]:
409 409 ui.write(indent_string)
410 410 ui.write(b'%s %s\n' % (hex(head), phases.phasenames[phase]))
411 411
412 412
413 413 def _quasirepr(thing):
414 414 if isinstance(thing, (dict, util.sortdict, collections.OrderedDict)):
415 415 return b'{%s}' % (
416 416 b', '.join(b'%s: %s' % (k, thing[k]) for k in sorted(thing))
417 417 )
418 418 return pycompat.bytestr(repr(thing))
419 419
420 420
421 421 def _debugbundle2(ui, gen, all=None, **opts):
422 422 """lists the contents of a bundle2"""
423 423 if not isinstance(gen, bundle2.unbundle20):
424 424 raise error.Abort(_(b'not a bundle2 file'))
425 425 ui.write((b'Stream params: %s\n' % _quasirepr(gen.params)))
426 426 parttypes = opts.get('part_type', [])
427 427 for part in gen.iterparts():
428 428 if parttypes and part.type not in parttypes:
429 429 continue
430 430 msg = b'%s -- %s (mandatory: %r)\n'
431 431 ui.write((msg % (part.type, _quasirepr(part.params), part.mandatory)))
432 432 if part.type == b'changegroup':
433 433 version = part.params.get(b'version', b'01')
434 434 cg = changegroup.getunbundler(version, part, b'UN')
435 435 if not ui.quiet:
436 436 _debugchangegroup(ui, cg, all=all, indent=4, **opts)
437 437 if part.type == b'obsmarkers':
438 438 if not ui.quiet:
439 439 _debugobsmarkers(ui, part, indent=4, **opts)
440 440 if part.type == b'phase-heads':
441 441 if not ui.quiet:
442 442 _debugphaseheads(ui, part, indent=4)
443 443
444 444
445 445 @command(
446 446 b'debugbundle',
447 447 [
448 448 (b'a', b'all', None, _(b'show all details')),
449 449 (b'', b'part-type', [], _(b'show only the named part type')),
450 450 (b'', b'spec', None, _(b'print the bundlespec of the bundle')),
451 451 ],
452 452 _(b'FILE'),
453 453 norepo=True,
454 454 )
455 455 def debugbundle(ui, bundlepath, all=None, spec=None, **opts):
456 456 """lists the contents of a bundle"""
457 457 with hg.openpath(ui, bundlepath) as f:
458 458 if spec:
459 459 spec = exchange.getbundlespec(ui, f)
460 460 ui.write(b'%s\n' % spec)
461 461 return
462 462
463 463 gen = exchange.readbundle(ui, f, bundlepath)
464 464 if isinstance(gen, bundle2.unbundle20):
465 465 return _debugbundle2(ui, gen, all=all, **opts)
466 466 _debugchangegroup(ui, gen, all=all, **opts)
467 467
468 468
469 469 @command(b'debugcapabilities', [], _(b'PATH'), norepo=True)
470 470 def debugcapabilities(ui, path, **opts):
471 471 """lists the capabilities of a remote peer"""
472 472 opts = pycompat.byteskwargs(opts)
473 473 peer = hg.peer(ui, opts, path)
474 474 try:
475 475 caps = peer.capabilities()
476 476 ui.writenoi18n(b'Main capabilities:\n')
477 477 for c in sorted(caps):
478 478 ui.write(b' %s\n' % c)
479 479 b2caps = bundle2.bundle2caps(peer)
480 480 if b2caps:
481 481 ui.writenoi18n(b'Bundle2 capabilities:\n')
482 482 for key, values in sorted(pycompat.iteritems(b2caps)):
483 483 ui.write(b' %s\n' % key)
484 484 for v in values:
485 485 ui.write(b' %s\n' % v)
486 486 finally:
487 487 peer.close()
488 488
489 489
490 490 @command(
491 491 b'debugchangedfiles',
492 492 [
493 493 (
494 494 b'',
495 495 b'compute',
496 496 False,
497 497 b"compute information instead of reading it from storage",
498 498 ),
499 499 ],
500 500 b'REV',
501 501 )
502 502 def debugchangedfiles(ui, repo, rev, **opts):
503 503 """list the stored files changes for a revision"""
504 504 ctx = scmutil.revsingle(repo, rev, None)
505 505 files = None
506 506
507 507 if opts['compute']:
508 508 files = metadata.compute_all_files_changes(ctx)
509 509 else:
510 510 sd = repo.changelog.sidedata(ctx.rev())
511 511 files_block = sd.get(sidedata.SD_FILES)
512 512 if files_block is not None:
513 513 files = metadata.decode_files_sidedata(sd)
514 514 if files is not None:
515 515 for f in sorted(files.touched):
516 516 if f in files.added:
517 517 action = b"added"
518 518 elif f in files.removed:
519 519 action = b"removed"
520 520 elif f in files.merged:
521 521 action = b"merged"
522 522 elif f in files.salvaged:
523 523 action = b"salvaged"
524 524 else:
525 525 action = b"touched"
526 526
527 527 copy_parent = b""
528 528 copy_source = b""
529 529 if f in files.copied_from_p1:
530 530 copy_parent = b"p1"
531 531 copy_source = files.copied_from_p1[f]
532 532 elif f in files.copied_from_p2:
533 533 copy_parent = b"p2"
534 534 copy_source = files.copied_from_p2[f]
535 535
536 536 data = (action, copy_parent, f, copy_source)
537 537 template = b"%-8s %2s: %s, %s;\n"
538 538 ui.write(template % data)
539 539
540 540
541 541 @command(b'debugcheckstate', [], b'')
542 542 def debugcheckstate(ui, repo):
543 543 """validate the correctness of the current dirstate"""
544 544 parent1, parent2 = repo.dirstate.parents()
545 545 m1 = repo[parent1].manifest()
546 546 m2 = repo[parent2].manifest()
547 547 errors = 0
548 548 for f in repo.dirstate:
549 549 state = repo.dirstate[f]
550 550 if state in b"nr" and f not in m1:
551 551 ui.warn(_(b"%s in state %s, but not in manifest1\n") % (f, state))
552 552 errors += 1
553 553 if state in b"a" and f in m1:
554 554 ui.warn(_(b"%s in state %s, but also in manifest1\n") % (f, state))
555 555 errors += 1
556 556 if state in b"m" and f not in m1 and f not in m2:
557 557 ui.warn(
558 558 _(b"%s in state %s, but not in either manifest\n") % (f, state)
559 559 )
560 560 errors += 1
561 561 for f in m1:
562 562 state = repo.dirstate[f]
563 563 if state not in b"nrm":
564 564 ui.warn(_(b"%s in manifest1, but listed as state %s") % (f, state))
565 565 errors += 1
566 566 if errors:
567 567 errstr = _(b".hg/dirstate inconsistent with current parent's manifest")
568 568 raise error.Abort(errstr)
569 569
570 570
571 571 @command(
572 572 b'debugcolor',
573 573 [(b'', b'style', None, _(b'show all configured styles'))],
574 574 b'hg debugcolor',
575 575 )
576 576 def debugcolor(ui, repo, **opts):
577 577 """show available color, effects or style"""
578 578 ui.writenoi18n(b'color mode: %s\n' % stringutil.pprint(ui._colormode))
579 579 if opts.get('style'):
580 580 return _debugdisplaystyle(ui)
581 581 else:
582 582 return _debugdisplaycolor(ui)
583 583
584 584
585 585 def _debugdisplaycolor(ui):
586 586 ui = ui.copy()
587 587 ui._styles.clear()
588 588 for effect in color._activeeffects(ui).keys():
589 589 ui._styles[effect] = effect
590 590 if ui._terminfoparams:
591 591 for k, v in ui.configitems(b'color'):
592 592 if k.startswith(b'color.'):
593 593 ui._styles[k] = k[6:]
594 594 elif k.startswith(b'terminfo.'):
595 595 ui._styles[k] = k[9:]
596 596 ui.write(_(b'available colors:\n'))
597 597 # sort label with a '_' after the other to group '_background' entry.
598 598 items = sorted(ui._styles.items(), key=lambda i: (b'_' in i[0], i[0], i[1]))
599 599 for colorname, label in items:
600 600 ui.write(b'%s\n' % colorname, label=label)
601 601
602 602
603 603 def _debugdisplaystyle(ui):
604 604 ui.write(_(b'available style:\n'))
605 605 if not ui._styles:
606 606 return
607 607 width = max(len(s) for s in ui._styles)
608 608 for label, effects in sorted(ui._styles.items()):
609 609 ui.write(b'%s' % label, label=label)
610 610 if effects:
611 611 # 50
612 612 ui.write(b': ')
613 613 ui.write(b' ' * (max(0, width - len(label))))
614 614 ui.write(b', '.join(ui.label(e, e) for e in effects.split()))
615 615 ui.write(b'\n')
616 616
617 617
618 618 @command(b'debugcreatestreamclonebundle', [], b'FILE')
619 619 def debugcreatestreamclonebundle(ui, repo, fname):
620 620 """create a stream clone bundle file
621 621
622 622 Stream bundles are special bundles that are essentially archives of
623 623 revlog files. They are commonly used for cloning very quickly.
624 624 """
625 625 # TODO we may want to turn this into an abort when this functionality
626 626 # is moved into `hg bundle`.
627 627 if phases.hassecret(repo):
628 628 ui.warn(
629 629 _(
630 630 b'(warning: stream clone bundle will contain secret '
631 631 b'revisions)\n'
632 632 )
633 633 )
634 634
635 635 requirements, gen = streamclone.generatebundlev1(repo)
636 636 changegroup.writechunks(ui, gen, fname)
637 637
638 638 ui.write(_(b'bundle requirements: %s\n') % b', '.join(sorted(requirements)))
639 639
640 640
641 641 @command(
642 642 b'debugdag',
643 643 [
644 644 (b't', b'tags', None, _(b'use tags as labels')),
645 645 (b'b', b'branches', None, _(b'annotate with branch names')),
646 646 (b'', b'dots', None, _(b'use dots for runs')),
647 647 (b's', b'spaces', None, _(b'separate elements by spaces')),
648 648 ],
649 649 _(b'[OPTION]... [FILE [REV]...]'),
650 650 optionalrepo=True,
651 651 )
652 652 def debugdag(ui, repo, file_=None, *revs, **opts):
653 653 """format the changelog or an index DAG as a concise textual description
654 654
655 655 If you pass a revlog index, the revlog's DAG is emitted. If you list
656 656 revision numbers, they get labeled in the output as rN.
657 657
658 658 Otherwise, the changelog DAG of the current repo is emitted.
659 659 """
660 660 spaces = opts.get('spaces')
661 661 dots = opts.get('dots')
662 662 if file_:
663 663 rlog = revlog.revlog(vfsmod.vfs(encoding.getcwd(), audit=False), file_)
664 664 revs = {int(r) for r in revs}
665 665
666 666 def events():
667 667 for r in rlog:
668 668 yield b'n', (r, list(p for p in rlog.parentrevs(r) if p != -1))
669 669 if r in revs:
670 670 yield b'l', (r, b"r%i" % r)
671 671
672 672 elif repo:
673 673 cl = repo.changelog
674 674 tags = opts.get('tags')
675 675 branches = opts.get('branches')
676 676 if tags:
677 677 labels = {}
678 678 for l, n in repo.tags().items():
679 679 labels.setdefault(cl.rev(n), []).append(l)
680 680
681 681 def events():
682 682 b = b"default"
683 683 for r in cl:
684 684 if branches:
685 685 newb = cl.read(cl.node(r))[5][b'branch']
686 686 if newb != b:
687 687 yield b'a', newb
688 688 b = newb
689 689 yield b'n', (r, list(p for p in cl.parentrevs(r) if p != -1))
690 690 if tags:
691 691 ls = labels.get(r)
692 692 if ls:
693 693 for l in ls:
694 694 yield b'l', (r, l)
695 695
696 696 else:
697 697 raise error.Abort(_(b'need repo for changelog dag'))
698 698
699 699 for line in dagparser.dagtextlines(
700 700 events(),
701 701 addspaces=spaces,
702 702 wraplabels=True,
703 703 wrapannotations=True,
704 704 wrapnonlinear=dots,
705 705 usedots=dots,
706 706 maxlinewidth=70,
707 707 ):
708 708 ui.write(line)
709 709 ui.write(b"\n")
710 710
711 711
712 712 @command(b'debugdata', cmdutil.debugrevlogopts, _(b'-c|-m|FILE REV'))
713 713 def debugdata(ui, repo, file_, rev=None, **opts):
714 714 """dump the contents of a data file revision"""
715 715 opts = pycompat.byteskwargs(opts)
716 716 if opts.get(b'changelog') or opts.get(b'manifest') or opts.get(b'dir'):
717 717 if rev is not None:
718 718 raise error.CommandError(b'debugdata', _(b'invalid arguments'))
719 719 file_, rev = None, file_
720 720 elif rev is None:
721 721 raise error.CommandError(b'debugdata', _(b'invalid arguments'))
722 722 r = cmdutil.openstorage(repo, b'debugdata', file_, opts)
723 723 try:
724 724 ui.write(r.rawdata(r.lookup(rev)))
725 725 except KeyError:
726 726 raise error.Abort(_(b'invalid revision identifier %s') % rev)
727 727
728 728
729 729 @command(
730 730 b'debugdate',
731 731 [(b'e', b'extended', None, _(b'try extended date formats'))],
732 732 _(b'[-e] DATE [RANGE]'),
733 733 norepo=True,
734 734 optionalrepo=True,
735 735 )
736 736 def debugdate(ui, date, range=None, **opts):
737 737 """parse and display a date"""
738 738 if opts["extended"]:
739 739 d = dateutil.parsedate(date, dateutil.extendeddateformats)
740 740 else:
741 741 d = dateutil.parsedate(date)
742 742 ui.writenoi18n(b"internal: %d %d\n" % d)
743 743 ui.writenoi18n(b"standard: %s\n" % dateutil.datestr(d))
744 744 if range:
745 745 m = dateutil.matchdate(range)
746 746 ui.writenoi18n(b"match: %s\n" % m(d[0]))
747 747
748 748
749 749 @command(
750 750 b'debugdeltachain',
751 751 cmdutil.debugrevlogopts + cmdutil.formatteropts,
752 752 _(b'-c|-m|FILE'),
753 753 optionalrepo=True,
754 754 )
755 755 def debugdeltachain(ui, repo, file_=None, **opts):
756 756 """dump information about delta chains in a revlog
757 757
758 758 Output can be templatized. Available template keywords are:
759 759
760 760 :``rev``: revision number
761 761 :``chainid``: delta chain identifier (numbered by unique base)
762 762 :``chainlen``: delta chain length to this revision
763 763 :``prevrev``: previous revision in delta chain
764 764 :``deltatype``: role of delta / how it was computed
765 765 :``compsize``: compressed size of revision
766 766 :``uncompsize``: uncompressed size of revision
767 767 :``chainsize``: total size of compressed revisions in chain
768 768 :``chainratio``: total chain size divided by uncompressed revision size
769 769 (new delta chains typically start at ratio 2.00)
770 770 :``lindist``: linear distance from base revision in delta chain to end
771 771 of this revision
772 772 :``extradist``: total size of revisions not part of this delta chain from
773 773 base of delta chain to end of this revision; a measurement
774 774 of how much extra data we need to read/seek across to read
775 775 the delta chain for this revision
776 776 :``extraratio``: extradist divided by chainsize; another representation of
777 777 how much unrelated data is needed to load this delta chain
778 778
779 779 If the repository is configured to use the sparse read, additional keywords
780 780 are available:
781 781
782 782 :``readsize``: total size of data read from the disk for a revision
783 783 (sum of the sizes of all the blocks)
784 784 :``largestblock``: size of the largest block of data read from the disk
785 785 :``readdensity``: density of useful bytes in the data read from the disk
786 786 :``srchunks``: in how many data hunks the whole revision would be read
787 787
788 788 The sparse read can be enabled with experimental.sparse-read = True
789 789 """
790 790 opts = pycompat.byteskwargs(opts)
791 791 r = cmdutil.openrevlog(repo, b'debugdeltachain', file_, opts)
792 792 index = r.index
793 793 start = r.start
794 794 length = r.length
795 795 generaldelta = r.version & revlog.FLAG_GENERALDELTA
796 796 withsparseread = getattr(r, '_withsparseread', False)
797 797
798 798 def revinfo(rev):
799 799 e = index[rev]
800 800 compsize = e[1]
801 801 uncompsize = e[2]
802 802 chainsize = 0
803 803
804 804 if generaldelta:
805 805 if e[3] == e[5]:
806 806 deltatype = b'p1'
807 807 elif e[3] == e[6]:
808 808 deltatype = b'p2'
809 809 elif e[3] == rev - 1:
810 810 deltatype = b'prev'
811 811 elif e[3] == rev:
812 812 deltatype = b'base'
813 813 else:
814 814 deltatype = b'other'
815 815 else:
816 816 if e[3] == rev:
817 817 deltatype = b'base'
818 818 else:
819 819 deltatype = b'prev'
820 820
821 821 chain = r._deltachain(rev)[0]
822 822 for iterrev in chain:
823 823 e = index[iterrev]
824 824 chainsize += e[1]
825 825
826 826 return compsize, uncompsize, deltatype, chain, chainsize
827 827
828 828 fm = ui.formatter(b'debugdeltachain', opts)
829 829
830 830 fm.plain(
831 831 b' rev chain# chainlen prev delta '
832 832 b'size rawsize chainsize ratio lindist extradist '
833 833 b'extraratio'
834 834 )
835 835 if withsparseread:
836 836 fm.plain(b' readsize largestblk rddensity srchunks')
837 837 fm.plain(b'\n')
838 838
839 839 chainbases = {}
840 840 for rev in r:
841 841 comp, uncomp, deltatype, chain, chainsize = revinfo(rev)
842 842 chainbase = chain[0]
843 843 chainid = chainbases.setdefault(chainbase, len(chainbases) + 1)
844 844 basestart = start(chainbase)
845 845 revstart = start(rev)
846 846 lineardist = revstart + comp - basestart
847 847 extradist = lineardist - chainsize
848 848 try:
849 849 prevrev = chain[-2]
850 850 except IndexError:
851 851 prevrev = -1
852 852
853 853 if uncomp != 0:
854 854 chainratio = float(chainsize) / float(uncomp)
855 855 else:
856 856 chainratio = chainsize
857 857
858 858 if chainsize != 0:
859 859 extraratio = float(extradist) / float(chainsize)
860 860 else:
861 861 extraratio = extradist
862 862
863 863 fm.startitem()
864 864 fm.write(
865 865 b'rev chainid chainlen prevrev deltatype compsize '
866 866 b'uncompsize chainsize chainratio lindist extradist '
867 867 b'extraratio',
868 868 b'%7d %7d %8d %8d %7s %10d %10d %10d %9.5f %9d %9d %10.5f',
869 869 rev,
870 870 chainid,
871 871 len(chain),
872 872 prevrev,
873 873 deltatype,
874 874 comp,
875 875 uncomp,
876 876 chainsize,
877 877 chainratio,
878 878 lineardist,
879 879 extradist,
880 880 extraratio,
881 881 rev=rev,
882 882 chainid=chainid,
883 883 chainlen=len(chain),
884 884 prevrev=prevrev,
885 885 deltatype=deltatype,
886 886 compsize=comp,
887 887 uncompsize=uncomp,
888 888 chainsize=chainsize,
889 889 chainratio=chainratio,
890 890 lindist=lineardist,
891 891 extradist=extradist,
892 892 extraratio=extraratio,
893 893 )
894 894 if withsparseread:
895 895 readsize = 0
896 896 largestblock = 0
897 897 srchunks = 0
898 898
899 899 for revschunk in deltautil.slicechunk(r, chain):
900 900 srchunks += 1
901 901 blkend = start(revschunk[-1]) + length(revschunk[-1])
902 902 blksize = blkend - start(revschunk[0])
903 903
904 904 readsize += blksize
905 905 if largestblock < blksize:
906 906 largestblock = blksize
907 907
908 908 if readsize:
909 909 readdensity = float(chainsize) / float(readsize)
910 910 else:
911 911 readdensity = 1
912 912
913 913 fm.write(
914 914 b'readsize largestblock readdensity srchunks',
915 915 b' %10d %10d %9.5f %8d',
916 916 readsize,
917 917 largestblock,
918 918 readdensity,
919 919 srchunks,
920 920 readsize=readsize,
921 921 largestblock=largestblock,
922 922 readdensity=readdensity,
923 923 srchunks=srchunks,
924 924 )
925 925
926 926 fm.plain(b'\n')
927 927
928 928 fm.end()
929 929
930 930
931 931 @command(
932 932 b'debugdirstate|debugstate',
933 933 [
934 934 (
935 935 b'',
936 936 b'nodates',
937 937 None,
938 938 _(b'do not display the saved mtime (DEPRECATED)'),
939 939 ),
940 940 (b'', b'dates', True, _(b'display the saved mtime')),
941 941 (b'', b'datesort', None, _(b'sort by saved mtime')),
942 942 ],
943 943 _(b'[OPTION]...'),
944 944 )
945 945 def debugstate(ui, repo, **opts):
946 946 """show the contents of the current dirstate"""
947 947
948 948 nodates = not opts['dates']
949 949 if opts.get('nodates') is not None:
950 950 nodates = True
951 951 datesort = opts.get('datesort')
952 952
953 953 if datesort:
954 954 keyfunc = lambda x: (x[1][3], x[0]) # sort by mtime, then by filename
955 955 else:
956 956 keyfunc = None # sort by filename
957 957 for file_, ent in sorted(pycompat.iteritems(repo.dirstate), key=keyfunc):
958 958 if ent[3] == -1:
959 959 timestr = b'unset '
960 960 elif nodates:
961 961 timestr = b'set '
962 962 else:
963 963 timestr = time.strftime(
964 964 "%Y-%m-%d %H:%M:%S ", time.localtime(ent[3])
965 965 )
966 966 timestr = encoding.strtolocal(timestr)
967 967 if ent[1] & 0o20000:
968 968 mode = b'lnk'
969 969 else:
970 970 mode = b'%3o' % (ent[1] & 0o777 & ~util.umask)
971 971 ui.write(b"%c %s %10d %s%s\n" % (ent[0], mode, ent[2], timestr, file_))
972 972 for f in repo.dirstate.copies():
973 973 ui.write(_(b"copy: %s -> %s\n") % (repo.dirstate.copied(f), f))
974 974
975 975
976 976 @command(
977 977 b'debugdiscovery',
978 978 [
979 979 (b'', b'old', None, _(b'use old-style discovery')),
980 980 (
981 981 b'',
982 982 b'nonheads',
983 983 None,
984 984 _(b'use old-style discovery with non-heads included'),
985 985 ),
986 986 (b'', b'rev', [], b'restrict discovery to this set of revs'),
987 987 (b'', b'seed', b'12323', b'specify the random seed use for discovery'),
988 988 (
989 989 b'',
990 990 b'local-as-revs',
991 991 "",
992 992 'treat local has having these revisions only',
993 993 ),
994 994 (
995 995 b'',
996 996 b'remote-as-revs',
997 997 "",
998 998 'use local as remote, with only these these revisions',
999 999 ),
1000 1000 ]
1001 1001 + cmdutil.remoteopts,
1002 1002 _(b'[--rev REV] [OTHER]'),
1003 1003 )
1004 1004 def debugdiscovery(ui, repo, remoteurl=b"default", **opts):
1005 1005 """runs the changeset discovery protocol in isolation
1006 1006
1007 1007 The local peer can be "replaced" by a subset of the local repository by
1008 1008 using the `--local-as-revs` flag. Int he same way, usual `remote` peer can
1009 1009 be "replaced" by a subset of the local repository using the
1010 1010 `--local-as-revs` flag. This is useful to efficiently debug pathological
1011 1011 discovery situation.
1012 1012 """
1013 1013 opts = pycompat.byteskwargs(opts)
1014 1014 unfi = repo.unfiltered()
1015 1015
1016 1016 # setup potential extra filtering
1017 1017 local_revs = opts[b"local_as_revs"]
1018 1018 remote_revs = opts[b"remote_as_revs"]
1019 1019
1020 1020 # make sure tests are repeatable
1021 1021 random.seed(int(opts[b'seed']))
1022 1022
1023 1023 if not remote_revs:
1024 1024
1025 1025 remoteurl, branches = hg.parseurl(ui.expandpath(remoteurl))
1026 1026 remote = hg.peer(repo, opts, remoteurl)
1027 1027 ui.status(_(b'comparing with %s\n') % util.hidepassword(remoteurl))
1028 1028 else:
1029 1029 branches = (None, [])
1030 1030 remote_filtered_revs = scmutil.revrange(
1031 1031 unfi, [b"not (::(%s))" % remote_revs]
1032 1032 )
1033 1033 remote_filtered_revs = frozenset(remote_filtered_revs)
1034 1034
1035 1035 def remote_func(x):
1036 1036 return remote_filtered_revs
1037 1037
1038 1038 repoview.filtertable[b'debug-discovery-remote-filter'] = remote_func
1039 1039
1040 1040 remote = repo.peer()
1041 1041 remote._repo = remote._repo.filtered(b'debug-discovery-remote-filter')
1042 1042
1043 1043 if local_revs:
1044 1044 local_filtered_revs = scmutil.revrange(
1045 1045 unfi, [b"not (::(%s))" % local_revs]
1046 1046 )
1047 1047 local_filtered_revs = frozenset(local_filtered_revs)
1048 1048
1049 1049 def local_func(x):
1050 1050 return local_filtered_revs
1051 1051
1052 1052 repoview.filtertable[b'debug-discovery-local-filter'] = local_func
1053 1053 repo = repo.filtered(b'debug-discovery-local-filter')
1054 1054
1055 1055 data = {}
1056 1056 if opts.get(b'old'):
1057 1057
1058 1058 def doit(pushedrevs, remoteheads, remote=remote):
1059 1059 if not util.safehasattr(remote, b'branches'):
1060 1060 # enable in-client legacy support
1061 1061 remote = localrepo.locallegacypeer(remote.local())
1062 1062 common, _in, hds = treediscovery.findcommonincoming(
1063 1063 repo, remote, force=True, audit=data
1064 1064 )
1065 1065 common = set(common)
1066 1066 if not opts.get(b'nonheads'):
1067 1067 ui.writenoi18n(
1068 1068 b"unpruned common: %s\n"
1069 1069 % b" ".join(sorted(short(n) for n in common))
1070 1070 )
1071 1071
1072 1072 clnode = repo.changelog.node
1073 1073 common = repo.revs(b'heads(::%ln)', common)
1074 1074 common = {clnode(r) for r in common}
1075 1075 return common, hds
1076 1076
1077 1077 else:
1078 1078
1079 1079 def doit(pushedrevs, remoteheads, remote=remote):
1080 1080 nodes = None
1081 1081 if pushedrevs:
1082 1082 revs = scmutil.revrange(repo, pushedrevs)
1083 1083 nodes = [repo[r].node() for r in revs]
1084 1084 common, any, hds = setdiscovery.findcommonheads(
1085 1085 ui, repo, remote, ancestorsof=nodes, audit=data
1086 1086 )
1087 1087 return common, hds
1088 1088
1089 1089 remoterevs, _checkout = hg.addbranchrevs(repo, remote, branches, revs=None)
1090 1090 localrevs = opts[b'rev']
1091 1091 with util.timedcm('debug-discovery') as t:
1092 1092 common, hds = doit(localrevs, remoterevs)
1093 1093
1094 1094 # compute all statistics
1095 1095 heads_common = set(common)
1096 1096 heads_remote = set(hds)
1097 1097 heads_local = set(repo.heads())
1098 1098 # note: they cannot be a local or remote head that is in common and not
1099 1099 # itself a head of common.
1100 1100 heads_common_local = heads_common & heads_local
1101 1101 heads_common_remote = heads_common & heads_remote
1102 1102 heads_common_both = heads_common & heads_remote & heads_local
1103 1103
1104 1104 all = repo.revs(b'all()')
1105 1105 common = repo.revs(b'::%ln', common)
1106 1106 roots_common = repo.revs(b'roots(::%ld)', common)
1107 1107 missing = repo.revs(b'not ::%ld', common)
1108 1108 heads_missing = repo.revs(b'heads(%ld)', missing)
1109 1109 roots_missing = repo.revs(b'roots(%ld)', missing)
1110 1110 assert len(common) + len(missing) == len(all)
1111 1111
1112 1112 initial_undecided = repo.revs(
1113 1113 b'not (::%ln or %ln::)', heads_common_remote, heads_common_local
1114 1114 )
1115 1115 heads_initial_undecided = repo.revs(b'heads(%ld)', initial_undecided)
1116 1116 roots_initial_undecided = repo.revs(b'roots(%ld)', initial_undecided)
1117 1117 common_initial_undecided = initial_undecided & common
1118 1118 missing_initial_undecided = initial_undecided & missing
1119 1119
1120 1120 data[b'elapsed'] = t.elapsed
1121 1121 data[b'nb-common-heads'] = len(heads_common)
1122 1122 data[b'nb-common-heads-local'] = len(heads_common_local)
1123 1123 data[b'nb-common-heads-remote'] = len(heads_common_remote)
1124 1124 data[b'nb-common-heads-both'] = len(heads_common_both)
1125 1125 data[b'nb-common-roots'] = len(roots_common)
1126 1126 data[b'nb-head-local'] = len(heads_local)
1127 1127 data[b'nb-head-local-missing'] = len(heads_local) - len(heads_common_local)
1128 1128 data[b'nb-head-remote'] = len(heads_remote)
1129 1129 data[b'nb-head-remote-unknown'] = len(heads_remote) - len(
1130 1130 heads_common_remote
1131 1131 )
1132 1132 data[b'nb-revs'] = len(all)
1133 1133 data[b'nb-revs-common'] = len(common)
1134 1134 data[b'nb-revs-missing'] = len(missing)
1135 1135 data[b'nb-missing-heads'] = len(heads_missing)
1136 1136 data[b'nb-missing-roots'] = len(roots_missing)
1137 1137 data[b'nb-ini_und'] = len(initial_undecided)
1138 1138 data[b'nb-ini_und-heads'] = len(heads_initial_undecided)
1139 1139 data[b'nb-ini_und-roots'] = len(roots_initial_undecided)
1140 1140 data[b'nb-ini_und-common'] = len(common_initial_undecided)
1141 1141 data[b'nb-ini_und-missing'] = len(missing_initial_undecided)
1142 1142
1143 1143 # display discovery summary
1144 1144 ui.writenoi18n(b"elapsed time: %(elapsed)f seconds\n" % data)
1145 1145 ui.writenoi18n(b"round-trips: %(total-roundtrips)9d\n" % data)
1146 1146 ui.writenoi18n(b"heads summary:\n")
1147 1147 ui.writenoi18n(b" total common heads: %(nb-common-heads)9d\n" % data)
1148 1148 ui.writenoi18n(
1149 1149 b" also local heads: %(nb-common-heads-local)9d\n" % data
1150 1150 )
1151 1151 ui.writenoi18n(
1152 1152 b" also remote heads: %(nb-common-heads-remote)9d\n" % data
1153 1153 )
1154 1154 ui.writenoi18n(b" both: %(nb-common-heads-both)9d\n" % data)
1155 1155 ui.writenoi18n(b" local heads: %(nb-head-local)9d\n" % data)
1156 1156 ui.writenoi18n(
1157 1157 b" common: %(nb-common-heads-local)9d\n" % data
1158 1158 )
1159 1159 ui.writenoi18n(
1160 1160 b" missing: %(nb-head-local-missing)9d\n" % data
1161 1161 )
1162 1162 ui.writenoi18n(b" remote heads: %(nb-head-remote)9d\n" % data)
1163 1163 ui.writenoi18n(
1164 1164 b" common: %(nb-common-heads-remote)9d\n" % data
1165 1165 )
1166 1166 ui.writenoi18n(
1167 1167 b" unknown: %(nb-head-remote-unknown)9d\n" % data
1168 1168 )
1169 1169 ui.writenoi18n(b"local changesets: %(nb-revs)9d\n" % data)
1170 1170 ui.writenoi18n(b" common: %(nb-revs-common)9d\n" % data)
1171 1171 ui.writenoi18n(b" heads: %(nb-common-heads)9d\n" % data)
1172 1172 ui.writenoi18n(b" roots: %(nb-common-roots)9d\n" % data)
1173 1173 ui.writenoi18n(b" missing: %(nb-revs-missing)9d\n" % data)
1174 1174 ui.writenoi18n(b" heads: %(nb-missing-heads)9d\n" % data)
1175 1175 ui.writenoi18n(b" roots: %(nb-missing-roots)9d\n" % data)
1176 1176 ui.writenoi18n(b" first undecided set: %(nb-ini_und)9d\n" % data)
1177 1177 ui.writenoi18n(b" heads: %(nb-ini_und-heads)9d\n" % data)
1178 1178 ui.writenoi18n(b" roots: %(nb-ini_und-roots)9d\n" % data)
1179 1179 ui.writenoi18n(b" common: %(nb-ini_und-common)9d\n" % data)
1180 1180 ui.writenoi18n(b" missing: %(nb-ini_und-missing)9d\n" % data)
1181 1181
1182 1182 if ui.verbose:
1183 1183 ui.writenoi18n(
1184 1184 b"common heads: %s\n"
1185 1185 % b" ".join(sorted(short(n) for n in heads_common))
1186 1186 )
1187 1187
1188 1188
1189 1189 _chunksize = 4 << 10
1190 1190
1191 1191
1192 1192 @command(
1193 1193 b'debugdownload',
1194 1194 [
1195 1195 (b'o', b'output', b'', _(b'path')),
1196 1196 ],
1197 1197 optionalrepo=True,
1198 1198 )
1199 1199 def debugdownload(ui, repo, url, output=None, **opts):
1200 1200 """download a resource using Mercurial logic and config"""
1201 1201 fh = urlmod.open(ui, url, output)
1202 1202
1203 1203 dest = ui
1204 1204 if output:
1205 1205 dest = open(output, b"wb", _chunksize)
1206 1206 try:
1207 1207 data = fh.read(_chunksize)
1208 1208 while data:
1209 1209 dest.write(data)
1210 1210 data = fh.read(_chunksize)
1211 1211 finally:
1212 1212 if output:
1213 1213 dest.close()
1214 1214
1215 1215
1216 1216 @command(b'debugextensions', cmdutil.formatteropts, [], optionalrepo=True)
1217 1217 def debugextensions(ui, repo, **opts):
1218 1218 '''show information about active extensions'''
1219 1219 opts = pycompat.byteskwargs(opts)
1220 1220 exts = extensions.extensions(ui)
1221 1221 hgver = util.version()
1222 1222 fm = ui.formatter(b'debugextensions', opts)
1223 1223 for extname, extmod in sorted(exts, key=operator.itemgetter(0)):
1224 1224 isinternal = extensions.ismoduleinternal(extmod)
1225 1225 extsource = None
1226 1226
1227 1227 if util.safehasattr(extmod, '__file__'):
1228 1228 extsource = pycompat.fsencode(extmod.__file__)
1229 1229 elif getattr(sys, 'oxidized', False):
1230 1230 extsource = pycompat.sysexecutable
1231 1231 if isinternal:
1232 1232 exttestedwith = [] # never expose magic string to users
1233 1233 else:
1234 1234 exttestedwith = getattr(extmod, 'testedwith', b'').split()
1235 1235 extbuglink = getattr(extmod, 'buglink', None)
1236 1236
1237 1237 fm.startitem()
1238 1238
1239 1239 if ui.quiet or ui.verbose:
1240 1240 fm.write(b'name', b'%s\n', extname)
1241 1241 else:
1242 1242 fm.write(b'name', b'%s', extname)
1243 1243 if isinternal or hgver in exttestedwith:
1244 1244 fm.plain(b'\n')
1245 1245 elif not exttestedwith:
1246 1246 fm.plain(_(b' (untested!)\n'))
1247 1247 else:
1248 1248 lasttestedversion = exttestedwith[-1]
1249 1249 fm.plain(b' (%s!)\n' % lasttestedversion)
1250 1250
1251 1251 fm.condwrite(
1252 1252 ui.verbose and extsource,
1253 1253 b'source',
1254 1254 _(b' location: %s\n'),
1255 1255 extsource or b"",
1256 1256 )
1257 1257
1258 1258 if ui.verbose:
1259 1259 fm.plain(_(b' bundled: %s\n') % [b'no', b'yes'][isinternal])
1260 1260 fm.data(bundled=isinternal)
1261 1261
1262 1262 fm.condwrite(
1263 1263 ui.verbose and exttestedwith,
1264 1264 b'testedwith',
1265 1265 _(b' tested with: %s\n'),
1266 1266 fm.formatlist(exttestedwith, name=b'ver'),
1267 1267 )
1268 1268
1269 1269 fm.condwrite(
1270 1270 ui.verbose and extbuglink,
1271 1271 b'buglink',
1272 1272 _(b' bug reporting: %s\n'),
1273 1273 extbuglink or b"",
1274 1274 )
1275 1275
1276 1276 fm.end()
1277 1277
1278 1278
1279 1279 @command(
1280 1280 b'debugfileset',
1281 1281 [
1282 1282 (
1283 1283 b'r',
1284 1284 b'rev',
1285 1285 b'',
1286 1286 _(b'apply the filespec on this revision'),
1287 1287 _(b'REV'),
1288 1288 ),
1289 1289 (
1290 1290 b'',
1291 1291 b'all-files',
1292 1292 False,
1293 1293 _(b'test files from all revisions and working directory'),
1294 1294 ),
1295 1295 (
1296 1296 b's',
1297 1297 b'show-matcher',
1298 1298 None,
1299 1299 _(b'print internal representation of matcher'),
1300 1300 ),
1301 1301 (
1302 1302 b'p',
1303 1303 b'show-stage',
1304 1304 [],
1305 1305 _(b'print parsed tree at the given stage'),
1306 1306 _(b'NAME'),
1307 1307 ),
1308 1308 ],
1309 1309 _(b'[-r REV] [--all-files] [OPTION]... FILESPEC'),
1310 1310 )
1311 1311 def debugfileset(ui, repo, expr, **opts):
1312 1312 '''parse and apply a fileset specification'''
1313 1313 from . import fileset
1314 1314
1315 1315 fileset.symbols # force import of fileset so we have predicates to optimize
1316 1316 opts = pycompat.byteskwargs(opts)
1317 1317 ctx = scmutil.revsingle(repo, opts.get(b'rev'), None)
1318 1318
1319 1319 stages = [
1320 1320 (b'parsed', pycompat.identity),
1321 1321 (b'analyzed', filesetlang.analyze),
1322 1322 (b'optimized', filesetlang.optimize),
1323 1323 ]
1324 1324 stagenames = {n for n, f in stages}
1325 1325
1326 1326 showalways = set()
1327 1327 if ui.verbose and not opts[b'show_stage']:
1328 1328 # show parsed tree by --verbose (deprecated)
1329 1329 showalways.add(b'parsed')
1330 1330 if opts[b'show_stage'] == [b'all']:
1331 1331 showalways.update(stagenames)
1332 1332 else:
1333 1333 for n in opts[b'show_stage']:
1334 1334 if n not in stagenames:
1335 1335 raise error.Abort(_(b'invalid stage name: %s') % n)
1336 1336 showalways.update(opts[b'show_stage'])
1337 1337
1338 1338 tree = filesetlang.parse(expr)
1339 1339 for n, f in stages:
1340 1340 tree = f(tree)
1341 1341 if n in showalways:
1342 1342 if opts[b'show_stage'] or n != b'parsed':
1343 1343 ui.write(b"* %s:\n" % n)
1344 1344 ui.write(filesetlang.prettyformat(tree), b"\n")
1345 1345
1346 1346 files = set()
1347 1347 if opts[b'all_files']:
1348 1348 for r in repo:
1349 1349 c = repo[r]
1350 1350 files.update(c.files())
1351 1351 files.update(c.substate)
1352 1352 if opts[b'all_files'] or ctx.rev() is None:
1353 1353 wctx = repo[None]
1354 1354 files.update(
1355 1355 repo.dirstate.walk(
1356 1356 scmutil.matchall(repo),
1357 1357 subrepos=list(wctx.substate),
1358 1358 unknown=True,
1359 1359 ignored=True,
1360 1360 )
1361 1361 )
1362 1362 files.update(wctx.substate)
1363 1363 else:
1364 1364 files.update(ctx.files())
1365 1365 files.update(ctx.substate)
1366 1366
1367 1367 m = ctx.matchfileset(repo.getcwd(), expr)
1368 1368 if opts[b'show_matcher'] or (opts[b'show_matcher'] is None and ui.verbose):
1369 1369 ui.writenoi18n(b'* matcher:\n', stringutil.prettyrepr(m), b'\n')
1370 1370 for f in sorted(files):
1371 1371 if not m(f):
1372 1372 continue
1373 1373 ui.write(b"%s\n" % f)
1374 1374
1375 1375
1376 1376 @command(b'debugformat', [] + cmdutil.formatteropts)
1377 1377 def debugformat(ui, repo, **opts):
1378 1378 """display format information about the current repository
1379 1379
1380 1380 Use --verbose to get extra information about current config value and
1381 1381 Mercurial default."""
1382 1382 opts = pycompat.byteskwargs(opts)
1383 1383 maxvariantlength = max(len(fv.name) for fv in upgrade.allformatvariant)
1384 1384 maxvariantlength = max(len(b'format-variant'), maxvariantlength)
1385 1385
1386 1386 def makeformatname(name):
1387 1387 return b'%s:' + (b' ' * (maxvariantlength - len(name)))
1388 1388
1389 1389 fm = ui.formatter(b'debugformat', opts)
1390 1390 if fm.isplain():
1391 1391
1392 1392 def formatvalue(value):
1393 1393 if util.safehasattr(value, b'startswith'):
1394 1394 return value
1395 1395 if value:
1396 1396 return b'yes'
1397 1397 else:
1398 1398 return b'no'
1399 1399
1400 1400 else:
1401 1401 formatvalue = pycompat.identity
1402 1402
1403 1403 fm.plain(b'format-variant')
1404 1404 fm.plain(b' ' * (maxvariantlength - len(b'format-variant')))
1405 1405 fm.plain(b' repo')
1406 1406 if ui.verbose:
1407 1407 fm.plain(b' config default')
1408 1408 fm.plain(b'\n')
1409 1409 for fv in upgrade.allformatvariant:
1410 1410 fm.startitem()
1411 1411 repovalue = fv.fromrepo(repo)
1412 1412 configvalue = fv.fromconfig(repo)
1413 1413
1414 1414 if repovalue != configvalue:
1415 1415 namelabel = b'formatvariant.name.mismatchconfig'
1416 1416 repolabel = b'formatvariant.repo.mismatchconfig'
1417 1417 elif repovalue != fv.default:
1418 1418 namelabel = b'formatvariant.name.mismatchdefault'
1419 1419 repolabel = b'formatvariant.repo.mismatchdefault'
1420 1420 else:
1421 1421 namelabel = b'formatvariant.name.uptodate'
1422 1422 repolabel = b'formatvariant.repo.uptodate'
1423 1423
1424 1424 fm.write(b'name', makeformatname(fv.name), fv.name, label=namelabel)
1425 1425 fm.write(b'repo', b' %3s', formatvalue(repovalue), label=repolabel)
1426 1426 if fv.default != configvalue:
1427 1427 configlabel = b'formatvariant.config.special'
1428 1428 else:
1429 1429 configlabel = b'formatvariant.config.default'
1430 1430 fm.condwrite(
1431 1431 ui.verbose,
1432 1432 b'config',
1433 1433 b' %6s',
1434 1434 formatvalue(configvalue),
1435 1435 label=configlabel,
1436 1436 )
1437 1437 fm.condwrite(
1438 1438 ui.verbose,
1439 1439 b'default',
1440 1440 b' %7s',
1441 1441 formatvalue(fv.default),
1442 1442 label=b'formatvariant.default',
1443 1443 )
1444 1444 fm.plain(b'\n')
1445 1445 fm.end()
1446 1446
1447 1447
1448 1448 @command(b'debugfsinfo', [], _(b'[PATH]'), norepo=True)
1449 1449 def debugfsinfo(ui, path=b"."):
1450 1450 """show information detected about current filesystem"""
1451 1451 ui.writenoi18n(b'path: %s\n' % path)
1452 1452 ui.writenoi18n(
1453 1453 b'mounted on: %s\n' % (util.getfsmountpoint(path) or b'(unknown)')
1454 1454 )
1455 1455 ui.writenoi18n(b'exec: %s\n' % (util.checkexec(path) and b'yes' or b'no'))
1456 1456 ui.writenoi18n(b'fstype: %s\n' % (util.getfstype(path) or b'(unknown)'))
1457 1457 ui.writenoi18n(
1458 1458 b'symlink: %s\n' % (util.checklink(path) and b'yes' or b'no')
1459 1459 )
1460 1460 ui.writenoi18n(
1461 1461 b'hardlink: %s\n' % (util.checknlink(path) and b'yes' or b'no')
1462 1462 )
1463 1463 casesensitive = b'(unknown)'
1464 1464 try:
1465 1465 with pycompat.namedtempfile(prefix=b'.debugfsinfo', dir=path) as f:
1466 1466 casesensitive = util.fscasesensitive(f.name) and b'yes' or b'no'
1467 1467 except OSError:
1468 1468 pass
1469 1469 ui.writenoi18n(b'case-sensitive: %s\n' % casesensitive)
1470 1470
1471 1471
1472 1472 @command(
1473 1473 b'debuggetbundle',
1474 1474 [
1475 1475 (b'H', b'head', [], _(b'id of head node'), _(b'ID')),
1476 1476 (b'C', b'common', [], _(b'id of common node'), _(b'ID')),
1477 1477 (
1478 1478 b't',
1479 1479 b'type',
1480 1480 b'bzip2',
1481 1481 _(b'bundle compression type to use'),
1482 1482 _(b'TYPE'),
1483 1483 ),
1484 1484 ],
1485 1485 _(b'REPO FILE [-H|-C ID]...'),
1486 1486 norepo=True,
1487 1487 )
1488 1488 def debuggetbundle(ui, repopath, bundlepath, head=None, common=None, **opts):
1489 1489 """retrieves a bundle from a repo
1490 1490
1491 1491 Every ID must be a full-length hex node id string. Saves the bundle to the
1492 1492 given file.
1493 1493 """
1494 1494 opts = pycompat.byteskwargs(opts)
1495 1495 repo = hg.peer(ui, opts, repopath)
1496 1496 if not repo.capable(b'getbundle'):
1497 1497 raise error.Abort(b"getbundle() not supported by target repository")
1498 1498 args = {}
1499 1499 if common:
1500 1500 args['common'] = [bin(s) for s in common]
1501 1501 if head:
1502 1502 args['heads'] = [bin(s) for s in head]
1503 1503 # TODO: get desired bundlecaps from command line.
1504 1504 args['bundlecaps'] = None
1505 1505 bundle = repo.getbundle(b'debug', **args)
1506 1506
1507 1507 bundletype = opts.get(b'type', b'bzip2').lower()
1508 1508 btypes = {
1509 1509 b'none': b'HG10UN',
1510 1510 b'bzip2': b'HG10BZ',
1511 1511 b'gzip': b'HG10GZ',
1512 1512 b'bundle2': b'HG20',
1513 1513 }
1514 1514 bundletype = btypes.get(bundletype)
1515 1515 if bundletype not in bundle2.bundletypes:
1516 1516 raise error.Abort(_(b'unknown bundle type specified with --type'))
1517 1517 bundle2.writebundle(ui, bundle, bundlepath, bundletype)
1518 1518
1519 1519
1520 1520 @command(b'debugignore', [], b'[FILE]')
1521 1521 def debugignore(ui, repo, *files, **opts):
1522 1522 """display the combined ignore pattern and information about ignored files
1523 1523
1524 1524 With no argument display the combined ignore pattern.
1525 1525
1526 1526 Given space separated file names, shows if the given file is ignored and
1527 1527 if so, show the ignore rule (file and line number) that matched it.
1528 1528 """
1529 1529 ignore = repo.dirstate._ignore
1530 1530 if not files:
1531 1531 # Show all the patterns
1532 1532 ui.write(b"%s\n" % pycompat.byterepr(ignore))
1533 1533 else:
1534 1534 m = scmutil.match(repo[None], pats=files)
1535 1535 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1536 1536 for f in m.files():
1537 1537 nf = util.normpath(f)
1538 1538 ignored = None
1539 1539 ignoredata = None
1540 1540 if nf != b'.':
1541 1541 if ignore(nf):
1542 1542 ignored = nf
1543 1543 ignoredata = repo.dirstate._ignorefileandline(nf)
1544 1544 else:
1545 1545 for p in pathutil.finddirs(nf):
1546 1546 if ignore(p):
1547 1547 ignored = p
1548 1548 ignoredata = repo.dirstate._ignorefileandline(p)
1549 1549 break
1550 1550 if ignored:
1551 1551 if ignored == nf:
1552 1552 ui.write(_(b"%s is ignored\n") % uipathfn(f))
1553 1553 else:
1554 1554 ui.write(
1555 1555 _(
1556 1556 b"%s is ignored because of "
1557 1557 b"containing directory %s\n"
1558 1558 )
1559 1559 % (uipathfn(f), ignored)
1560 1560 )
1561 1561 ignorefile, lineno, line = ignoredata
1562 1562 ui.write(
1563 1563 _(b"(ignore rule in %s, line %d: '%s')\n")
1564 1564 % (ignorefile, lineno, line)
1565 1565 )
1566 1566 else:
1567 1567 ui.write(_(b"%s is not ignored\n") % uipathfn(f))
1568 1568
1569 1569
1570 1570 @command(
1571 1571 b'debugindex',
1572 1572 cmdutil.debugrevlogopts + cmdutil.formatteropts,
1573 1573 _(b'-c|-m|FILE'),
1574 1574 )
1575 1575 def debugindex(ui, repo, file_=None, **opts):
1576 1576 """dump index data for a storage primitive"""
1577 1577 opts = pycompat.byteskwargs(opts)
1578 1578 store = cmdutil.openstorage(repo, b'debugindex', file_, opts)
1579 1579
1580 1580 if ui.debugflag:
1581 1581 shortfn = hex
1582 1582 else:
1583 1583 shortfn = short
1584 1584
1585 1585 idlen = 12
1586 1586 for i in store:
1587 1587 idlen = len(shortfn(store.node(i)))
1588 1588 break
1589 1589
1590 1590 fm = ui.formatter(b'debugindex', opts)
1591 1591 fm.plain(
1592 1592 b' rev linkrev %s %s p2\n'
1593 1593 % (b'nodeid'.ljust(idlen), b'p1'.ljust(idlen))
1594 1594 )
1595 1595
1596 1596 for rev in store:
1597 1597 node = store.node(rev)
1598 1598 parents = store.parents(node)
1599 1599
1600 1600 fm.startitem()
1601 1601 fm.write(b'rev', b'%6d ', rev)
1602 1602 fm.write(b'linkrev', b'%7d ', store.linkrev(rev))
1603 1603 fm.write(b'node', b'%s ', shortfn(node))
1604 1604 fm.write(b'p1', b'%s ', shortfn(parents[0]))
1605 1605 fm.write(b'p2', b'%s', shortfn(parents[1]))
1606 1606 fm.plain(b'\n')
1607 1607
1608 1608 fm.end()
1609 1609
1610 1610
1611 1611 @command(
1612 1612 b'debugindexdot',
1613 1613 cmdutil.debugrevlogopts,
1614 1614 _(b'-c|-m|FILE'),
1615 1615 optionalrepo=True,
1616 1616 )
1617 1617 def debugindexdot(ui, repo, file_=None, **opts):
1618 1618 """dump an index DAG as a graphviz dot file"""
1619 1619 opts = pycompat.byteskwargs(opts)
1620 1620 r = cmdutil.openstorage(repo, b'debugindexdot', file_, opts)
1621 1621 ui.writenoi18n(b"digraph G {\n")
1622 1622 for i in r:
1623 1623 node = r.node(i)
1624 1624 pp = r.parents(node)
1625 1625 ui.write(b"\t%d -> %d\n" % (r.rev(pp[0]), i))
1626 1626 if pp[1] != nullid:
1627 1627 ui.write(b"\t%d -> %d\n" % (r.rev(pp[1]), i))
1628 1628 ui.write(b"}\n")
1629 1629
1630 1630
1631 1631 @command(b'debugindexstats', [])
1632 1632 def debugindexstats(ui, repo):
1633 1633 """show stats related to the changelog index"""
1634 1634 repo.changelog.shortest(nullid, 1)
1635 1635 index = repo.changelog.index
1636 1636 if not util.safehasattr(index, b'stats'):
1637 1637 raise error.Abort(_(b'debugindexstats only works with native code'))
1638 1638 for k, v in sorted(index.stats().items()):
1639 1639 ui.write(b'%s: %d\n' % (k, v))
1640 1640
1641 1641
1642 1642 @command(b'debuginstall', [] + cmdutil.formatteropts, b'', norepo=True)
1643 1643 def debuginstall(ui, **opts):
1644 1644 """test Mercurial installation
1645 1645
1646 1646 Returns 0 on success.
1647 1647 """
1648 1648 opts = pycompat.byteskwargs(opts)
1649 1649
1650 1650 problems = 0
1651 1651
1652 1652 fm = ui.formatter(b'debuginstall', opts)
1653 1653 fm.startitem()
1654 1654
1655 1655 # encoding might be unknown or wrong. don't translate these messages.
1656 1656 fm.write(b'encoding', b"checking encoding (%s)...\n", encoding.encoding)
1657 1657 err = None
1658 1658 try:
1659 1659 codecs.lookup(pycompat.sysstr(encoding.encoding))
1660 1660 except LookupError as inst:
1661 1661 err = stringutil.forcebytestr(inst)
1662 1662 problems += 1
1663 1663 fm.condwrite(
1664 1664 err,
1665 1665 b'encodingerror',
1666 1666 b" %s\n (check that your locale is properly set)\n",
1667 1667 err,
1668 1668 )
1669 1669
1670 1670 # Python
1671 1671 pythonlib = None
1672 1672 if util.safehasattr(os, '__file__'):
1673 1673 pythonlib = os.path.dirname(pycompat.fsencode(os.__file__))
1674 1674 elif getattr(sys, 'oxidized', False):
1675 1675 pythonlib = pycompat.sysexecutable
1676 1676
1677 1677 fm.write(
1678 1678 b'pythonexe',
1679 1679 _(b"checking Python executable (%s)\n"),
1680 1680 pycompat.sysexecutable or _(b"unknown"),
1681 1681 )
1682 1682 fm.write(
1683 1683 b'pythonimplementation',
1684 1684 _(b"checking Python implementation (%s)\n"),
1685 1685 pycompat.sysbytes(platform.python_implementation()),
1686 1686 )
1687 1687 fm.write(
1688 1688 b'pythonver',
1689 1689 _(b"checking Python version (%s)\n"),
1690 1690 (b"%d.%d.%d" % sys.version_info[:3]),
1691 1691 )
1692 1692 fm.write(
1693 1693 b'pythonlib',
1694 1694 _(b"checking Python lib (%s)...\n"),
1695 1695 pythonlib or _(b"unknown"),
1696 1696 )
1697 1697
1698 1698 try:
1699 1699 from . import rustext
1700 1700
1701 1701 rustext.__doc__ # trigger lazy import
1702 1702 except ImportError:
1703 1703 rustext = None
1704 1704
1705 1705 security = set(sslutil.supportedprotocols)
1706 1706 if sslutil.hassni:
1707 1707 security.add(b'sni')
1708 1708
1709 1709 fm.write(
1710 1710 b'pythonsecurity',
1711 1711 _(b"checking Python security support (%s)\n"),
1712 1712 fm.formatlist(sorted(security), name=b'protocol', fmt=b'%s', sep=b','),
1713 1713 )
1714 1714
1715 1715 # These are warnings, not errors. So don't increment problem count. This
1716 1716 # may change in the future.
1717 1717 if b'tls1.2' not in security:
1718 1718 fm.plain(
1719 1719 _(
1720 1720 b' TLS 1.2 not supported by Python install; '
1721 1721 b'network connections lack modern security\n'
1722 1722 )
1723 1723 )
1724 1724 if b'sni' not in security:
1725 1725 fm.plain(
1726 1726 _(
1727 1727 b' SNI not supported by Python install; may have '
1728 1728 b'connectivity issues with some servers\n'
1729 1729 )
1730 1730 )
1731 1731
1732 1732 fm.plain(
1733 1733 _(
1734 1734 b"checking Rust extensions (%s)\n"
1735 1735 % (b'missing' if rustext is None else b'installed')
1736 1736 ),
1737 1737 )
1738 1738
1739 1739 # TODO print CA cert info
1740 1740
1741 1741 # hg version
1742 1742 hgver = util.version()
1743 1743 fm.write(
1744 1744 b'hgver', _(b"checking Mercurial version (%s)\n"), hgver.split(b'+')[0]
1745 1745 )
1746 1746 fm.write(
1747 1747 b'hgverextra',
1748 1748 _(b"checking Mercurial custom build (%s)\n"),
1749 1749 b'+'.join(hgver.split(b'+')[1:]),
1750 1750 )
1751 1751
1752 1752 # compiled modules
1753 1753 hgmodules = None
1754 1754 if util.safehasattr(sys.modules[__name__], '__file__'):
1755 1755 hgmodules = os.path.dirname(pycompat.fsencode(__file__))
1756 1756 elif getattr(sys, 'oxidized', False):
1757 1757 hgmodules = pycompat.sysexecutable
1758 1758
1759 1759 fm.write(
1760 1760 b'hgmodulepolicy', _(b"checking module policy (%s)\n"), policy.policy
1761 1761 )
1762 1762 fm.write(
1763 1763 b'hgmodules',
1764 1764 _(b"checking installed modules (%s)...\n"),
1765 1765 hgmodules or _(b"unknown"),
1766 1766 )
1767 1767
1768 1768 rustandc = policy.policy in (b'rust+c', b'rust+c-allow')
1769 1769 rustext = rustandc # for now, that's the only case
1770 1770 cext = policy.policy in (b'c', b'allow') or rustandc
1771 1771 nopure = cext or rustext
1772 1772 if nopure:
1773 1773 err = None
1774 1774 try:
1775 1775 if cext:
1776 1776 from .cext import ( # pytype: disable=import-error
1777 1777 base85,
1778 1778 bdiff,
1779 1779 mpatch,
1780 1780 osutil,
1781 1781 )
1782 1782
1783 1783 # quiet pyflakes
1784 1784 dir(bdiff), dir(mpatch), dir(base85), dir(osutil)
1785 1785 if rustext:
1786 1786 from .rustext import ( # pytype: disable=import-error
1787 1787 ancestor,
1788 1788 dirstate,
1789 1789 )
1790 1790
1791 1791 dir(ancestor), dir(dirstate) # quiet pyflakes
1792 1792 except Exception as inst:
1793 1793 err = stringutil.forcebytestr(inst)
1794 1794 problems += 1
1795 1795 fm.condwrite(err, b'extensionserror', b" %s\n", err)
1796 1796
1797 1797 compengines = util.compengines._engines.values()
1798 1798 fm.write(
1799 1799 b'compengines',
1800 1800 _(b'checking registered compression engines (%s)\n'),
1801 1801 fm.formatlist(
1802 1802 sorted(e.name() for e in compengines),
1803 1803 name=b'compengine',
1804 1804 fmt=b'%s',
1805 1805 sep=b', ',
1806 1806 ),
1807 1807 )
1808 1808 fm.write(
1809 1809 b'compenginesavail',
1810 1810 _(b'checking available compression engines (%s)\n'),
1811 1811 fm.formatlist(
1812 1812 sorted(e.name() for e in compengines if e.available()),
1813 1813 name=b'compengine',
1814 1814 fmt=b'%s',
1815 1815 sep=b', ',
1816 1816 ),
1817 1817 )
1818 1818 wirecompengines = compression.compengines.supportedwireengines(
1819 1819 compression.SERVERROLE
1820 1820 )
1821 1821 fm.write(
1822 1822 b'compenginesserver',
1823 1823 _(
1824 1824 b'checking available compression engines '
1825 1825 b'for wire protocol (%s)\n'
1826 1826 ),
1827 1827 fm.formatlist(
1828 1828 [e.name() for e in wirecompengines if e.wireprotosupport()],
1829 1829 name=b'compengine',
1830 1830 fmt=b'%s',
1831 1831 sep=b', ',
1832 1832 ),
1833 1833 )
1834 1834 re2 = b'missing'
1835 1835 if util._re2:
1836 1836 re2 = b'available'
1837 1837 fm.plain(_(b'checking "re2" regexp engine (%s)\n') % re2)
1838 1838 fm.data(re2=bool(util._re2))
1839 1839
1840 1840 # templates
1841 1841 p = templater.templatedir()
1842 1842 fm.write(b'templatedirs', b'checking templates (%s)...\n', p or b'')
1843 1843 fm.condwrite(not p, b'', _(b" no template directories found\n"))
1844 1844 if p:
1845 1845 (m, fp) = templater.try_open_template(b"map-cmdline.default")
1846 1846 if m:
1847 1847 # template found, check if it is working
1848 1848 err = None
1849 1849 try:
1850 1850 templater.templater.frommapfile(m)
1851 1851 except Exception as inst:
1852 1852 err = stringutil.forcebytestr(inst)
1853 1853 p = None
1854 1854 fm.condwrite(err, b'defaulttemplateerror', b" %s\n", err)
1855 1855 else:
1856 1856 p = None
1857 1857 fm.condwrite(
1858 1858 p, b'defaulttemplate', _(b"checking default template (%s)\n"), m
1859 1859 )
1860 1860 fm.condwrite(
1861 1861 not m,
1862 1862 b'defaulttemplatenotfound',
1863 1863 _(b" template '%s' not found\n"),
1864 1864 b"default",
1865 1865 )
1866 1866 if not p:
1867 1867 problems += 1
1868 1868 fm.condwrite(
1869 1869 not p, b'', _(b" (templates seem to have been installed incorrectly)\n")
1870 1870 )
1871 1871
1872 1872 # editor
1873 1873 editor = ui.geteditor()
1874 1874 editor = util.expandpath(editor)
1875 1875 editorbin = procutil.shellsplit(editor)[0]
1876 1876 fm.write(b'editor', _(b"checking commit editor... (%s)\n"), editorbin)
1877 1877 cmdpath = procutil.findexe(editorbin)
1878 1878 fm.condwrite(
1879 1879 not cmdpath and editor == b'vi',
1880 1880 b'vinotfound',
1881 1881 _(
1882 1882 b" No commit editor set and can't find %s in PATH\n"
1883 1883 b" (specify a commit editor in your configuration"
1884 1884 b" file)\n"
1885 1885 ),
1886 1886 not cmdpath and editor == b'vi' and editorbin,
1887 1887 )
1888 1888 fm.condwrite(
1889 1889 not cmdpath and editor != b'vi',
1890 1890 b'editornotfound',
1891 1891 _(
1892 1892 b" Can't find editor '%s' in PATH\n"
1893 1893 b" (specify a commit editor in your configuration"
1894 1894 b" file)\n"
1895 1895 ),
1896 1896 not cmdpath and editorbin,
1897 1897 )
1898 1898 if not cmdpath and editor != b'vi':
1899 1899 problems += 1
1900 1900
1901 1901 # check username
1902 1902 username = None
1903 1903 err = None
1904 1904 try:
1905 1905 username = ui.username()
1906 1906 except error.Abort as e:
1907 1907 err = e.message
1908 1908 problems += 1
1909 1909
1910 1910 fm.condwrite(
1911 1911 username, b'username', _(b"checking username (%s)\n"), username
1912 1912 )
1913 1913 fm.condwrite(
1914 1914 err,
1915 1915 b'usernameerror',
1916 1916 _(
1917 1917 b"checking username...\n %s\n"
1918 1918 b" (specify a username in your configuration file)\n"
1919 1919 ),
1920 1920 err,
1921 1921 )
1922 1922
1923 1923 for name, mod in extensions.extensions():
1924 1924 handler = getattr(mod, 'debuginstall', None)
1925 1925 if handler is not None:
1926 1926 problems += handler(ui, fm)
1927 1927
1928 1928 fm.condwrite(not problems, b'', _(b"no problems detected\n"))
1929 1929 if not problems:
1930 1930 fm.data(problems=problems)
1931 1931 fm.condwrite(
1932 1932 problems,
1933 1933 b'problems',
1934 1934 _(b"%d problems detected, please check your install!\n"),
1935 1935 problems,
1936 1936 )
1937 1937 fm.end()
1938 1938
1939 1939 return problems
1940 1940
1941 1941
1942 1942 @command(b'debugknown', [], _(b'REPO ID...'), norepo=True)
1943 1943 def debugknown(ui, repopath, *ids, **opts):
1944 1944 """test whether node ids are known to a repo
1945 1945
1946 1946 Every ID must be a full-length hex node id string. Returns a list of 0s
1947 1947 and 1s indicating unknown/known.
1948 1948 """
1949 1949 opts = pycompat.byteskwargs(opts)
1950 1950 repo = hg.peer(ui, opts, repopath)
1951 1951 if not repo.capable(b'known'):
1952 1952 raise error.Abort(b"known() not supported by target repository")
1953 1953 flags = repo.known([bin(s) for s in ids])
1954 1954 ui.write(b"%s\n" % (b"".join([f and b"1" or b"0" for f in flags])))
1955 1955
1956 1956
1957 1957 @command(b'debuglabelcomplete', [], _(b'LABEL...'))
1958 1958 def debuglabelcomplete(ui, repo, *args):
1959 1959 '''backwards compatibility with old bash completion scripts (DEPRECATED)'''
1960 1960 debugnamecomplete(ui, repo, *args)
1961 1961
1962 1962
1963 1963 @command(
1964 1964 b'debuglocks',
1965 1965 [
1966 1966 (b'L', b'force-free-lock', None, _(b'free the store lock (DANGEROUS)')),
1967 1967 (
1968 1968 b'W',
1969 1969 b'force-free-wlock',
1970 1970 None,
1971 1971 _(b'free the working state lock (DANGEROUS)'),
1972 1972 ),
1973 1973 (b's', b'set-lock', None, _(b'set the store lock until stopped')),
1974 1974 (
1975 1975 b'S',
1976 1976 b'set-wlock',
1977 1977 None,
1978 1978 _(b'set the working state lock until stopped'),
1979 1979 ),
1980 1980 ],
1981 1981 _(b'[OPTION]...'),
1982 1982 )
1983 1983 def debuglocks(ui, repo, **opts):
1984 1984 """show or modify state of locks
1985 1985
1986 1986 By default, this command will show which locks are held. This
1987 1987 includes the user and process holding the lock, the amount of time
1988 1988 the lock has been held, and the machine name where the process is
1989 1989 running if it's not local.
1990 1990
1991 1991 Locks protect the integrity of Mercurial's data, so should be
1992 1992 treated with care. System crashes or other interruptions may cause
1993 1993 locks to not be properly released, though Mercurial will usually
1994 1994 detect and remove such stale locks automatically.
1995 1995
1996 1996 However, detecting stale locks may not always be possible (for
1997 1997 instance, on a shared filesystem). Removing locks may also be
1998 1998 blocked by filesystem permissions.
1999 1999
2000 2000 Setting a lock will prevent other commands from changing the data.
2001 2001 The command will wait until an interruption (SIGINT, SIGTERM, ...) occurs.
2002 2002 The set locks are removed when the command exits.
2003 2003
2004 2004 Returns 0 if no locks are held.
2005 2005
2006 2006 """
2007 2007
2008 2008 if opts.get('force_free_lock'):
2009 2009 repo.svfs.unlink(b'lock')
2010 2010 if opts.get('force_free_wlock'):
2011 2011 repo.vfs.unlink(b'wlock')
2012 2012 if opts.get('force_free_lock') or opts.get('force_free_wlock'):
2013 2013 return 0
2014 2014
2015 2015 locks = []
2016 2016 try:
2017 2017 if opts.get('set_wlock'):
2018 2018 try:
2019 2019 locks.append(repo.wlock(False))
2020 2020 except error.LockHeld:
2021 2021 raise error.Abort(_(b'wlock is already held'))
2022 2022 if opts.get('set_lock'):
2023 2023 try:
2024 2024 locks.append(repo.lock(False))
2025 2025 except error.LockHeld:
2026 2026 raise error.Abort(_(b'lock is already held'))
2027 2027 if len(locks):
2028 2028 ui.promptchoice(_(b"ready to release the lock (y)? $$ &Yes"))
2029 2029 return 0
2030 2030 finally:
2031 2031 release(*locks)
2032 2032
2033 2033 now = time.time()
2034 2034 held = 0
2035 2035
2036 2036 def report(vfs, name, method):
2037 2037 # this causes stale locks to get reaped for more accurate reporting
2038 2038 try:
2039 2039 l = method(False)
2040 2040 except error.LockHeld:
2041 2041 l = None
2042 2042
2043 2043 if l:
2044 2044 l.release()
2045 2045 else:
2046 2046 try:
2047 2047 st = vfs.lstat(name)
2048 2048 age = now - st[stat.ST_MTIME]
2049 2049 user = util.username(st.st_uid)
2050 2050 locker = vfs.readlock(name)
2051 2051 if b":" in locker:
2052 2052 host, pid = locker.split(b':')
2053 2053 if host == socket.gethostname():
2054 2054 locker = b'user %s, process %s' % (user or b'None', pid)
2055 2055 else:
2056 2056 locker = b'user %s, process %s, host %s' % (
2057 2057 user or b'None',
2058 2058 pid,
2059 2059 host,
2060 2060 )
2061 2061 ui.writenoi18n(b"%-6s %s (%ds)\n" % (name + b":", locker, age))
2062 2062 return 1
2063 2063 except OSError as e:
2064 2064 if e.errno != errno.ENOENT:
2065 2065 raise
2066 2066
2067 2067 ui.writenoi18n(b"%-6s free\n" % (name + b":"))
2068 2068 return 0
2069 2069
2070 2070 held += report(repo.svfs, b"lock", repo.lock)
2071 2071 held += report(repo.vfs, b"wlock", repo.wlock)
2072 2072
2073 2073 return held
2074 2074
2075 2075
2076 2076 @command(
2077 2077 b'debugmanifestfulltextcache',
2078 2078 [
2079 2079 (b'', b'clear', False, _(b'clear the cache')),
2080 2080 (
2081 2081 b'a',
2082 2082 b'add',
2083 2083 [],
2084 2084 _(b'add the given manifest nodes to the cache'),
2085 2085 _(b'NODE'),
2086 2086 ),
2087 2087 ],
2088 2088 b'',
2089 2089 )
2090 2090 def debugmanifestfulltextcache(ui, repo, add=(), **opts):
2091 2091 """show, clear or amend the contents of the manifest fulltext cache"""
2092 2092
2093 2093 def getcache():
2094 2094 r = repo.manifestlog.getstorage(b'')
2095 2095 try:
2096 2096 return r._fulltextcache
2097 2097 except AttributeError:
2098 2098 msg = _(
2099 2099 b"Current revlog implementation doesn't appear to have a "
2100 2100 b"manifest fulltext cache\n"
2101 2101 )
2102 2102 raise error.Abort(msg)
2103 2103
2104 2104 if opts.get('clear'):
2105 2105 with repo.wlock():
2106 2106 cache = getcache()
2107 2107 cache.clear(clear_persisted_data=True)
2108 2108 return
2109 2109
2110 2110 if add:
2111 2111 with repo.wlock():
2112 2112 m = repo.manifestlog
2113 2113 store = m.getstorage(b'')
2114 2114 for n in add:
2115 2115 try:
2116 2116 manifest = m[store.lookup(n)]
2117 2117 except error.LookupError as e:
2118 2118 raise error.Abort(e, hint=b"Check your manifest node id")
2119 2119 manifest.read() # stores revisision in cache too
2120 2120 return
2121 2121
2122 2122 cache = getcache()
2123 2123 if not len(cache):
2124 2124 ui.write(_(b'cache empty\n'))
2125 2125 else:
2126 2126 ui.write(
2127 2127 _(
2128 2128 b'cache contains %d manifest entries, in order of most to '
2129 2129 b'least recent:\n'
2130 2130 )
2131 2131 % (len(cache),)
2132 2132 )
2133 2133 totalsize = 0
2134 2134 for nodeid in cache:
2135 2135 # Use cache.get to not update the LRU order
2136 2136 data = cache.peek(nodeid)
2137 2137 size = len(data)
2138 2138 totalsize += size + 24 # 20 bytes nodeid, 4 bytes size
2139 2139 ui.write(
2140 2140 _(b'id: %s, size %s\n') % (hex(nodeid), util.bytecount(size))
2141 2141 )
2142 2142 ondisk = cache._opener.stat(b'manifestfulltextcache').st_size
2143 2143 ui.write(
2144 2144 _(b'total cache data size %s, on-disk %s\n')
2145 2145 % (util.bytecount(totalsize), util.bytecount(ondisk))
2146 2146 )
2147 2147
2148 2148
2149 2149 @command(b'debugmergestate', [] + cmdutil.templateopts, b'')
2150 2150 def debugmergestate(ui, repo, *args, **opts):
2151 2151 """print merge state
2152 2152
2153 2153 Use --verbose to print out information about whether v1 or v2 merge state
2154 2154 was chosen."""
2155 2155
2156 2156 if ui.verbose:
2157 2157 ms = mergestatemod.mergestate(repo)
2158 2158
2159 2159 # sort so that reasonable information is on top
2160 2160 v1records = ms._readrecordsv1()
2161 2161 v2records = ms._readrecordsv2()
2162 2162
2163 2163 if not v1records and not v2records:
2164 2164 pass
2165 2165 elif not v2records:
2166 2166 ui.writenoi18n(b'no version 2 merge state\n')
2167 2167 elif ms._v1v2match(v1records, v2records):
2168 2168 ui.writenoi18n(b'v1 and v2 states match: using v2\n')
2169 2169 else:
2170 2170 ui.writenoi18n(b'v1 and v2 states mismatch: using v1\n')
2171 2171
2172 2172 opts = pycompat.byteskwargs(opts)
2173 2173 if not opts[b'template']:
2174 2174 opts[b'template'] = (
2175 2175 b'{if(commits, "", "no merge state found\n")}'
2176 2176 b'{commits % "{name}{if(label, " ({label})")}: {node}\n"}'
2177 2177 b'{files % "file: {path} (state \\"{state}\\")\n'
2178 2178 b'{if(local_path, "'
2179 2179 b' local path: {local_path} (hash {local_key}, flags \\"{local_flags}\\")\n'
2180 2180 b' ancestor path: {ancestor_path} (node {ancestor_node})\n'
2181 2181 b' other path: {other_path} (node {other_node})\n'
2182 2182 b'")}'
2183 2183 b'{if(rename_side, "'
2184 2184 b' rename side: {rename_side}\n'
2185 2185 b' renamed path: {renamed_path}\n'
2186 2186 b'")}'
2187 2187 b'{extras % " extra: {key} = {value}\n"}'
2188 2188 b'"}'
2189 2189 b'{extras % "extra: {file} ({key} = {value})\n"}'
2190 2190 )
2191 2191
2192 2192 ms = mergestatemod.mergestate.read(repo)
2193 2193
2194 2194 fm = ui.formatter(b'debugmergestate', opts)
2195 2195 fm.startitem()
2196 2196
2197 2197 fm_commits = fm.nested(b'commits')
2198 2198 if ms.active():
2199 2199 for name, node, label_index in (
2200 2200 (b'local', ms.local, 0),
2201 2201 (b'other', ms.other, 1),
2202 2202 ):
2203 2203 fm_commits.startitem()
2204 2204 fm_commits.data(name=name)
2205 2205 fm_commits.data(node=hex(node))
2206 2206 if ms._labels and len(ms._labels) > label_index:
2207 2207 fm_commits.data(label=ms._labels[label_index])
2208 2208 fm_commits.end()
2209 2209
2210 2210 fm_files = fm.nested(b'files')
2211 2211 if ms.active():
2212 2212 for f in ms:
2213 2213 fm_files.startitem()
2214 2214 fm_files.data(path=f)
2215 2215 state = ms._state[f]
2216 2216 fm_files.data(state=state[0])
2217 2217 if state[0] in (
2218 2218 mergestatemod.MERGE_RECORD_UNRESOLVED,
2219 2219 mergestatemod.MERGE_RECORD_RESOLVED,
2220 2220 ):
2221 2221 fm_files.data(local_key=state[1])
2222 2222 fm_files.data(local_path=state[2])
2223 2223 fm_files.data(ancestor_path=state[3])
2224 2224 fm_files.data(ancestor_node=state[4])
2225 2225 fm_files.data(other_path=state[5])
2226 2226 fm_files.data(other_node=state[6])
2227 2227 fm_files.data(local_flags=state[7])
2228 2228 elif state[0] in (
2229 2229 mergestatemod.MERGE_RECORD_UNRESOLVED_PATH,
2230 2230 mergestatemod.MERGE_RECORD_RESOLVED_PATH,
2231 2231 ):
2232 2232 fm_files.data(renamed_path=state[1])
2233 2233 fm_files.data(rename_side=state[2])
2234 2234 fm_extras = fm_files.nested(b'extras')
2235 2235 for k, v in sorted(ms.extras(f).items()):
2236 2236 fm_extras.startitem()
2237 2237 fm_extras.data(key=k)
2238 2238 fm_extras.data(value=v)
2239 2239 fm_extras.end()
2240 2240
2241 2241 fm_files.end()
2242 2242
2243 2243 fm_extras = fm.nested(b'extras')
2244 2244 for f, d in sorted(pycompat.iteritems(ms.allextras())):
2245 2245 if f in ms:
2246 2246 # If file is in mergestate, we have already processed it's extras
2247 2247 continue
2248 2248 for k, v in pycompat.iteritems(d):
2249 2249 fm_extras.startitem()
2250 2250 fm_extras.data(file=f)
2251 2251 fm_extras.data(key=k)
2252 2252 fm_extras.data(value=v)
2253 2253 fm_extras.end()
2254 2254
2255 2255 fm.end()
2256 2256
2257 2257
2258 2258 @command(b'debugnamecomplete', [], _(b'NAME...'))
2259 2259 def debugnamecomplete(ui, repo, *args):
2260 2260 '''complete "names" - tags, open branch names, bookmark names'''
2261 2261
2262 2262 names = set()
2263 2263 # since we previously only listed open branches, we will handle that
2264 2264 # specially (after this for loop)
2265 2265 for name, ns in pycompat.iteritems(repo.names):
2266 2266 if name != b'branches':
2267 2267 names.update(ns.listnames(repo))
2268 2268 names.update(
2269 2269 tag
2270 2270 for (tag, heads, tip, closed) in repo.branchmap().iterbranches()
2271 2271 if not closed
2272 2272 )
2273 2273 completions = set()
2274 2274 if not args:
2275 2275 args = [b'']
2276 2276 for a in args:
2277 2277 completions.update(n for n in names if n.startswith(a))
2278 2278 ui.write(b'\n'.join(sorted(completions)))
2279 2279 ui.write(b'\n')
2280 2280
2281 2281
2282 2282 @command(
2283 2283 b'debugnodemap',
2284 2284 [
2285 2285 (
2286 2286 b'',
2287 2287 b'dump-new',
2288 2288 False,
2289 2289 _(b'write a (new) persistent binary nodemap on stdout'),
2290 2290 ),
2291 2291 (b'', b'dump-disk', False, _(b'dump on-disk data on stdout')),
2292 2292 (
2293 2293 b'',
2294 2294 b'check',
2295 2295 False,
2296 2296 _(b'check that the data on disk data are correct.'),
2297 2297 ),
2298 2298 (
2299 2299 b'',
2300 2300 b'metadata',
2301 2301 False,
2302 2302 _(b'display the on disk meta data for the nodemap'),
2303 2303 ),
2304 2304 ],
2305 2305 )
2306 2306 def debugnodemap(ui, repo, **opts):
2307 2307 """write and inspect on disk nodemap"""
2308 2308 if opts['dump_new']:
2309 2309 unfi = repo.unfiltered()
2310 2310 cl = unfi.changelog
2311 2311 if util.safehasattr(cl.index, "nodemap_data_all"):
2312 2312 data = cl.index.nodemap_data_all()
2313 2313 else:
2314 2314 data = nodemap.persistent_data(cl.index)
2315 2315 ui.write(data)
2316 2316 elif opts['dump_disk']:
2317 2317 unfi = repo.unfiltered()
2318 2318 cl = unfi.changelog
2319 2319 nm_data = nodemap.persisted_data(cl)
2320 2320 if nm_data is not None:
2321 2321 docket, data = nm_data
2322 2322 ui.write(data[:])
2323 2323 elif opts['check']:
2324 2324 unfi = repo.unfiltered()
2325 2325 cl = unfi.changelog
2326 2326 nm_data = nodemap.persisted_data(cl)
2327 2327 if nm_data is not None:
2328 2328 docket, data = nm_data
2329 2329 return nodemap.check_data(ui, cl.index, data)
2330 2330 elif opts['metadata']:
2331 2331 unfi = repo.unfiltered()
2332 2332 cl = unfi.changelog
2333 2333 nm_data = nodemap.persisted_data(cl)
2334 2334 if nm_data is not None:
2335 2335 docket, data = nm_data
2336 2336 ui.write((b"uid: %s\n") % docket.uid)
2337 2337 ui.write((b"tip-rev: %d\n") % docket.tip_rev)
2338 2338 ui.write((b"tip-node: %s\n") % hex(docket.tip_node))
2339 2339 ui.write((b"data-length: %d\n") % docket.data_length)
2340 2340 ui.write((b"data-unused: %d\n") % docket.data_unused)
2341 2341 unused_perc = docket.data_unused * 100.0 / docket.data_length
2342 2342 ui.write((b"data-unused: %2.3f%%\n") % unused_perc)
2343 2343
2344 2344
2345 2345 @command(
2346 2346 b'debugobsolete',
2347 2347 [
2348 2348 (b'', b'flags', 0, _(b'markers flag')),
2349 2349 (
2350 2350 b'',
2351 2351 b'record-parents',
2352 2352 False,
2353 2353 _(b'record parent information for the precursor'),
2354 2354 ),
2355 2355 (b'r', b'rev', [], _(b'display markers relevant to REV')),
2356 2356 (
2357 2357 b'',
2358 2358 b'exclusive',
2359 2359 False,
2360 2360 _(b'restrict display to markers only relevant to REV'),
2361 2361 ),
2362 2362 (b'', b'index', False, _(b'display index of the marker')),
2363 2363 (b'', b'delete', [], _(b'delete markers specified by indices')),
2364 2364 ]
2365 2365 + cmdutil.commitopts2
2366 2366 + cmdutil.formatteropts,
2367 2367 _(b'[OBSOLETED [REPLACEMENT ...]]'),
2368 2368 )
2369 2369 def debugobsolete(ui, repo, precursor=None, *successors, **opts):
2370 2370 """create arbitrary obsolete marker
2371 2371
2372 2372 With no arguments, displays the list of obsolescence markers."""
2373 2373
2374 2374 opts = pycompat.byteskwargs(opts)
2375 2375
2376 2376 def parsenodeid(s):
2377 2377 try:
2378 2378 # We do not use revsingle/revrange functions here to accept
2379 2379 # arbitrary node identifiers, possibly not present in the
2380 2380 # local repository.
2381 2381 n = bin(s)
2382 2382 if len(n) != len(nullid):
2383 2383 raise TypeError()
2384 2384 return n
2385 2385 except TypeError:
2386 2386 raise error.InputError(
2387 2387 b'changeset references must be full hexadecimal '
2388 2388 b'node identifiers'
2389 2389 )
2390 2390
2391 2391 if opts.get(b'delete'):
2392 2392 indices = []
2393 2393 for v in opts.get(b'delete'):
2394 2394 try:
2395 2395 indices.append(int(v))
2396 2396 except ValueError:
2397 2397 raise error.InputError(
2398 2398 _(b'invalid index value: %r') % v,
2399 2399 hint=_(b'use integers for indices'),
2400 2400 )
2401 2401
2402 2402 if repo.currenttransaction():
2403 2403 raise error.Abort(
2404 2404 _(b'cannot delete obsmarkers in the middle of transaction.')
2405 2405 )
2406 2406
2407 2407 with repo.lock():
2408 2408 n = repair.deleteobsmarkers(repo.obsstore, indices)
2409 2409 ui.write(_(b'deleted %i obsolescence markers\n') % n)
2410 2410
2411 2411 return
2412 2412
2413 2413 if precursor is not None:
2414 2414 if opts[b'rev']:
2415 2415 raise error.InputError(
2416 2416 b'cannot select revision when creating marker'
2417 2417 )
2418 2418 metadata = {}
2419 2419 metadata[b'user'] = encoding.fromlocal(opts[b'user'] or ui.username())
2420 2420 succs = tuple(parsenodeid(succ) for succ in successors)
2421 2421 l = repo.lock()
2422 2422 try:
2423 2423 tr = repo.transaction(b'debugobsolete')
2424 2424 try:
2425 2425 date = opts.get(b'date')
2426 2426 if date:
2427 2427 date = dateutil.parsedate(date)
2428 2428 else:
2429 2429 date = None
2430 2430 prec = parsenodeid(precursor)
2431 2431 parents = None
2432 2432 if opts[b'record_parents']:
2433 2433 if prec not in repo.unfiltered():
2434 2434 raise error.Abort(
2435 2435 b'cannot used --record-parents on '
2436 2436 b'unknown changesets'
2437 2437 )
2438 2438 parents = repo.unfiltered()[prec].parents()
2439 2439 parents = tuple(p.node() for p in parents)
2440 2440 repo.obsstore.create(
2441 2441 tr,
2442 2442 prec,
2443 2443 succs,
2444 2444 opts[b'flags'],
2445 2445 parents=parents,
2446 2446 date=date,
2447 2447 metadata=metadata,
2448 2448 ui=ui,
2449 2449 )
2450 2450 tr.close()
2451 2451 except ValueError as exc:
2452 2452 raise error.Abort(
2453 2453 _(b'bad obsmarker input: %s') % pycompat.bytestr(exc)
2454 2454 )
2455 2455 finally:
2456 2456 tr.release()
2457 2457 finally:
2458 2458 l.release()
2459 2459 else:
2460 2460 if opts[b'rev']:
2461 2461 revs = scmutil.revrange(repo, opts[b'rev'])
2462 2462 nodes = [repo[r].node() for r in revs]
2463 2463 markers = list(
2464 2464 obsutil.getmarkers(
2465 2465 repo, nodes=nodes, exclusive=opts[b'exclusive']
2466 2466 )
2467 2467 )
2468 2468 markers.sort(key=lambda x: x._data)
2469 2469 else:
2470 2470 markers = obsutil.getmarkers(repo)
2471 2471
2472 2472 markerstoiter = markers
2473 2473 isrelevant = lambda m: True
2474 2474 if opts.get(b'rev') and opts.get(b'index'):
2475 2475 markerstoiter = obsutil.getmarkers(repo)
2476 2476 markerset = set(markers)
2477 2477 isrelevant = lambda m: m in markerset
2478 2478
2479 2479 fm = ui.formatter(b'debugobsolete', opts)
2480 2480 for i, m in enumerate(markerstoiter):
2481 2481 if not isrelevant(m):
2482 2482 # marker can be irrelevant when we're iterating over a set
2483 2483 # of markers (markerstoiter) which is bigger than the set
2484 2484 # of markers we want to display (markers)
2485 2485 # this can happen if both --index and --rev options are
2486 2486 # provided and thus we need to iterate over all of the markers
2487 2487 # to get the correct indices, but only display the ones that
2488 2488 # are relevant to --rev value
2489 2489 continue
2490 2490 fm.startitem()
2491 2491 ind = i if opts.get(b'index') else None
2492 2492 cmdutil.showmarker(fm, m, index=ind)
2493 2493 fm.end()
2494 2494
2495 2495
2496 2496 @command(
2497 2497 b'debugp1copies',
2498 2498 [(b'r', b'rev', b'', _(b'revision to debug'), _(b'REV'))],
2499 2499 _(b'[-r REV]'),
2500 2500 )
2501 2501 def debugp1copies(ui, repo, **opts):
2502 2502 """dump copy information compared to p1"""
2503 2503
2504 2504 opts = pycompat.byteskwargs(opts)
2505 2505 ctx = scmutil.revsingle(repo, opts.get(b'rev'), default=None)
2506 2506 for dst, src in ctx.p1copies().items():
2507 2507 ui.write(b'%s -> %s\n' % (src, dst))
2508 2508
2509 2509
2510 2510 @command(
2511 2511 b'debugp2copies',
2512 2512 [(b'r', b'rev', b'', _(b'revision to debug'), _(b'REV'))],
2513 2513 _(b'[-r REV]'),
2514 2514 )
2515 2515 def debugp1copies(ui, repo, **opts):
2516 2516 """dump copy information compared to p2"""
2517 2517
2518 2518 opts = pycompat.byteskwargs(opts)
2519 2519 ctx = scmutil.revsingle(repo, opts.get(b'rev'), default=None)
2520 2520 for dst, src in ctx.p2copies().items():
2521 2521 ui.write(b'%s -> %s\n' % (src, dst))
2522 2522
2523 2523
2524 2524 @command(
2525 2525 b'debugpathcomplete',
2526 2526 [
2527 2527 (b'f', b'full', None, _(b'complete an entire path')),
2528 2528 (b'n', b'normal', None, _(b'show only normal files')),
2529 2529 (b'a', b'added', None, _(b'show only added files')),
2530 2530 (b'r', b'removed', None, _(b'show only removed files')),
2531 2531 ],
2532 2532 _(b'FILESPEC...'),
2533 2533 )
2534 2534 def debugpathcomplete(ui, repo, *specs, **opts):
2535 2535 """complete part or all of a tracked path
2536 2536
2537 2537 This command supports shells that offer path name completion. It
2538 2538 currently completes only files already known to the dirstate.
2539 2539
2540 2540 Completion extends only to the next path segment unless
2541 2541 --full is specified, in which case entire paths are used."""
2542 2542
2543 2543 def complete(path, acceptable):
2544 2544 dirstate = repo.dirstate
2545 2545 spec = os.path.normpath(os.path.join(encoding.getcwd(), path))
2546 2546 rootdir = repo.root + pycompat.ossep
2547 2547 if spec != repo.root and not spec.startswith(rootdir):
2548 2548 return [], []
2549 2549 if os.path.isdir(spec):
2550 2550 spec += b'/'
2551 2551 spec = spec[len(rootdir) :]
2552 2552 fixpaths = pycompat.ossep != b'/'
2553 2553 if fixpaths:
2554 2554 spec = spec.replace(pycompat.ossep, b'/')
2555 2555 speclen = len(spec)
2556 2556 fullpaths = opts['full']
2557 2557 files, dirs = set(), set()
2558 2558 adddir, addfile = dirs.add, files.add
2559 2559 for f, st in pycompat.iteritems(dirstate):
2560 2560 if f.startswith(spec) and st[0] in acceptable:
2561 2561 if fixpaths:
2562 2562 f = f.replace(b'/', pycompat.ossep)
2563 2563 if fullpaths:
2564 2564 addfile(f)
2565 2565 continue
2566 2566 s = f.find(pycompat.ossep, speclen)
2567 2567 if s >= 0:
2568 2568 adddir(f[:s])
2569 2569 else:
2570 2570 addfile(f)
2571 2571 return files, dirs
2572 2572
2573 2573 acceptable = b''
2574 2574 if opts['normal']:
2575 2575 acceptable += b'nm'
2576 2576 if opts['added']:
2577 2577 acceptable += b'a'
2578 2578 if opts['removed']:
2579 2579 acceptable += b'r'
2580 2580 cwd = repo.getcwd()
2581 2581 if not specs:
2582 2582 specs = [b'.']
2583 2583
2584 2584 files, dirs = set(), set()
2585 2585 for spec in specs:
2586 2586 f, d = complete(spec, acceptable or b'nmar')
2587 2587 files.update(f)
2588 2588 dirs.update(d)
2589 2589 files.update(dirs)
2590 2590 ui.write(b'\n'.join(repo.pathto(p, cwd) for p in sorted(files)))
2591 2591 ui.write(b'\n')
2592 2592
2593 2593
2594 2594 @command(
2595 2595 b'debugpathcopies',
2596 2596 cmdutil.walkopts,
2597 2597 b'hg debugpathcopies REV1 REV2 [FILE]',
2598 2598 inferrepo=True,
2599 2599 )
2600 2600 def debugpathcopies(ui, repo, rev1, rev2, *pats, **opts):
2601 2601 """show copies between two revisions"""
2602 2602 ctx1 = scmutil.revsingle(repo, rev1)
2603 2603 ctx2 = scmutil.revsingle(repo, rev2)
2604 2604 m = scmutil.match(ctx1, pats, opts)
2605 2605 for dst, src in sorted(copies.pathcopies(ctx1, ctx2, m).items()):
2606 2606 ui.write(b'%s -> %s\n' % (src, dst))
2607 2607
2608 2608
2609 2609 @command(b'debugpeer', [], _(b'PATH'), norepo=True)
2610 2610 def debugpeer(ui, path):
2611 2611 """establish a connection to a peer repository"""
2612 2612 # Always enable peer request logging. Requires --debug to display
2613 2613 # though.
2614 2614 overrides = {
2615 2615 (b'devel', b'debug.peer-request'): True,
2616 2616 }
2617 2617
2618 2618 with ui.configoverride(overrides):
2619 2619 peer = hg.peer(ui, {}, path)
2620 2620
2621 2621 try:
2622 2622 local = peer.local() is not None
2623 2623 canpush = peer.canpush()
2624 2624
2625 2625 ui.write(_(b'url: %s\n') % peer.url())
2626 2626 ui.write(_(b'local: %s\n') % (_(b'yes') if local else _(b'no')))
2627 2627 ui.write(
2628 2628 _(b'pushable: %s\n') % (_(b'yes') if canpush else _(b'no'))
2629 2629 )
2630 2630 finally:
2631 2631 peer.close()
2632 2632
2633 2633
2634 2634 @command(
2635 2635 b'debugpickmergetool',
2636 2636 [
2637 2637 (b'r', b'rev', b'', _(b'check for files in this revision'), _(b'REV')),
2638 2638 (b'', b'changedelete', None, _(b'emulate merging change and delete')),
2639 2639 ]
2640 2640 + cmdutil.walkopts
2641 2641 + cmdutil.mergetoolopts,
2642 2642 _(b'[PATTERN]...'),
2643 2643 inferrepo=True,
2644 2644 )
2645 2645 def debugpickmergetool(ui, repo, *pats, **opts):
2646 2646 """examine which merge tool is chosen for specified file
2647 2647
2648 2648 As described in :hg:`help merge-tools`, Mercurial examines
2649 2649 configurations below in this order to decide which merge tool is
2650 2650 chosen for specified file.
2651 2651
2652 2652 1. ``--tool`` option
2653 2653 2. ``HGMERGE`` environment variable
2654 2654 3. configurations in ``merge-patterns`` section
2655 2655 4. configuration of ``ui.merge``
2656 2656 5. configurations in ``merge-tools`` section
2657 2657 6. ``hgmerge`` tool (for historical reason only)
2658 2658 7. default tool for fallback (``:merge`` or ``:prompt``)
2659 2659
2660 2660 This command writes out examination result in the style below::
2661 2661
2662 2662 FILE = MERGETOOL
2663 2663
2664 2664 By default, all files known in the first parent context of the
2665 2665 working directory are examined. Use file patterns and/or -I/-X
2666 2666 options to limit target files. -r/--rev is also useful to examine
2667 2667 files in another context without actual updating to it.
2668 2668
2669 2669 With --debug, this command shows warning messages while matching
2670 2670 against ``merge-patterns`` and so on, too. It is recommended to
2671 2671 use this option with explicit file patterns and/or -I/-X options,
2672 2672 because this option increases amount of output per file according
2673 2673 to configurations in hgrc.
2674 2674
2675 2675 With -v/--verbose, this command shows configurations below at
2676 2676 first (only if specified).
2677 2677
2678 2678 - ``--tool`` option
2679 2679 - ``HGMERGE`` environment variable
2680 2680 - configuration of ``ui.merge``
2681 2681
2682 2682 If merge tool is chosen before matching against
2683 2683 ``merge-patterns``, this command can't show any helpful
2684 2684 information, even with --debug. In such case, information above is
2685 2685 useful to know why a merge tool is chosen.
2686 2686 """
2687 2687 opts = pycompat.byteskwargs(opts)
2688 2688 overrides = {}
2689 2689 if opts[b'tool']:
2690 2690 overrides[(b'ui', b'forcemerge')] = opts[b'tool']
2691 2691 ui.notenoi18n(b'with --tool %r\n' % (pycompat.bytestr(opts[b'tool'])))
2692 2692
2693 2693 with ui.configoverride(overrides, b'debugmergepatterns'):
2694 2694 hgmerge = encoding.environ.get(b"HGMERGE")
2695 2695 if hgmerge is not None:
2696 2696 ui.notenoi18n(b'with HGMERGE=%r\n' % (pycompat.bytestr(hgmerge)))
2697 2697 uimerge = ui.config(b"ui", b"merge")
2698 2698 if uimerge:
2699 2699 ui.notenoi18n(b'with ui.merge=%r\n' % (pycompat.bytestr(uimerge)))
2700 2700
2701 2701 ctx = scmutil.revsingle(repo, opts.get(b'rev'))
2702 2702 m = scmutil.match(ctx, pats, opts)
2703 2703 changedelete = opts[b'changedelete']
2704 2704 for path in ctx.walk(m):
2705 2705 fctx = ctx[path]
2706 2706 try:
2707 2707 if not ui.debugflag:
2708 2708 ui.pushbuffer(error=True)
2709 2709 tool, toolpath = filemerge._picktool(
2710 2710 repo,
2711 2711 ui,
2712 2712 path,
2713 2713 fctx.isbinary(),
2714 2714 b'l' in fctx.flags(),
2715 2715 changedelete,
2716 2716 )
2717 2717 finally:
2718 2718 if not ui.debugflag:
2719 2719 ui.popbuffer()
2720 2720 ui.write(b'%s = %s\n' % (path, tool))
2721 2721
2722 2722
2723 2723 @command(b'debugpushkey', [], _(b'REPO NAMESPACE [KEY OLD NEW]'), norepo=True)
2724 2724 def debugpushkey(ui, repopath, namespace, *keyinfo, **opts):
2725 2725 """access the pushkey key/value protocol
2726 2726
2727 2727 With two args, list the keys in the given namespace.
2728 2728
2729 2729 With five args, set a key to new if it currently is set to old.
2730 2730 Reports success or failure.
2731 2731 """
2732 2732
2733 2733 target = hg.peer(ui, {}, repopath)
2734 2734 try:
2735 2735 if keyinfo:
2736 2736 key, old, new = keyinfo
2737 2737 with target.commandexecutor() as e:
2738 2738 r = e.callcommand(
2739 2739 b'pushkey',
2740 2740 {
2741 2741 b'namespace': namespace,
2742 2742 b'key': key,
2743 2743 b'old': old,
2744 2744 b'new': new,
2745 2745 },
2746 2746 ).result()
2747 2747
2748 2748 ui.status(pycompat.bytestr(r) + b'\n')
2749 2749 return not r
2750 2750 else:
2751 2751 for k, v in sorted(pycompat.iteritems(target.listkeys(namespace))):
2752 2752 ui.write(
2753 2753 b"%s\t%s\n"
2754 2754 % (stringutil.escapestr(k), stringutil.escapestr(v))
2755 2755 )
2756 2756 finally:
2757 2757 target.close()
2758 2758
2759 2759
2760 2760 @command(b'debugpvec', [], _(b'A B'))
2761 2761 def debugpvec(ui, repo, a, b=None):
2762 2762 ca = scmutil.revsingle(repo, a)
2763 2763 cb = scmutil.revsingle(repo, b)
2764 2764 pa = pvec.ctxpvec(ca)
2765 2765 pb = pvec.ctxpvec(cb)
2766 2766 if pa == pb:
2767 2767 rel = b"="
2768 2768 elif pa > pb:
2769 2769 rel = b">"
2770 2770 elif pa < pb:
2771 2771 rel = b"<"
2772 2772 elif pa | pb:
2773 2773 rel = b"|"
2774 2774 ui.write(_(b"a: %s\n") % pa)
2775 2775 ui.write(_(b"b: %s\n") % pb)
2776 2776 ui.write(_(b"depth(a): %d depth(b): %d\n") % (pa._depth, pb._depth))
2777 2777 ui.write(
2778 2778 _(b"delta: %d hdist: %d distance: %d relation: %s\n")
2779 2779 % (
2780 2780 abs(pa._depth - pb._depth),
2781 2781 pvec._hamming(pa._vec, pb._vec),
2782 2782 pa.distance(pb),
2783 2783 rel,
2784 2784 )
2785 2785 )
2786 2786
2787 2787
2788 2788 @command(
2789 2789 b'debugrebuilddirstate|debugrebuildstate',
2790 2790 [
2791 2791 (b'r', b'rev', b'', _(b'revision to rebuild to'), _(b'REV')),
2792 2792 (
2793 2793 b'',
2794 2794 b'minimal',
2795 2795 None,
2796 2796 _(
2797 2797 b'only rebuild files that are inconsistent with '
2798 2798 b'the working copy parent'
2799 2799 ),
2800 2800 ),
2801 2801 ],
2802 2802 _(b'[-r REV]'),
2803 2803 )
2804 2804 def debugrebuilddirstate(ui, repo, rev, **opts):
2805 2805 """rebuild the dirstate as it would look like for the given revision
2806 2806
2807 2807 If no revision is specified the first current parent will be used.
2808 2808
2809 2809 The dirstate will be set to the files of the given revision.
2810 2810 The actual working directory content or existing dirstate
2811 2811 information such as adds or removes is not considered.
2812 2812
2813 2813 ``minimal`` will only rebuild the dirstate status for files that claim to be
2814 2814 tracked but are not in the parent manifest, or that exist in the parent
2815 2815 manifest but are not in the dirstate. It will not change adds, removes, or
2816 2816 modified files that are in the working copy parent.
2817 2817
2818 2818 One use of this command is to make the next :hg:`status` invocation
2819 2819 check the actual file content.
2820 2820 """
2821 2821 ctx = scmutil.revsingle(repo, rev)
2822 2822 with repo.wlock():
2823 2823 dirstate = repo.dirstate
2824 2824 changedfiles = None
2825 2825 # See command doc for what minimal does.
2826 2826 if opts.get('minimal'):
2827 2827 manifestfiles = set(ctx.manifest().keys())
2828 2828 dirstatefiles = set(dirstate)
2829 2829 manifestonly = manifestfiles - dirstatefiles
2830 2830 dsonly = dirstatefiles - manifestfiles
2831 2831 dsnotadded = {f for f in dsonly if dirstate[f] != b'a'}
2832 2832 changedfiles = manifestonly | dsnotadded
2833 2833
2834 2834 dirstate.rebuild(ctx.node(), ctx.manifest(), changedfiles)
2835 2835
2836 2836
2837 2837 @command(b'debugrebuildfncache', [], b'')
2838 2838 def debugrebuildfncache(ui, repo):
2839 2839 """rebuild the fncache file"""
2840 2840 repair.rebuildfncache(ui, repo)
2841 2841
2842 2842
2843 2843 @command(
2844 2844 b'debugrename',
2845 2845 [(b'r', b'rev', b'', _(b'revision to debug'), _(b'REV'))],
2846 2846 _(b'[-r REV] [FILE]...'),
2847 2847 )
2848 2848 def debugrename(ui, repo, *pats, **opts):
2849 2849 """dump rename information"""
2850 2850
2851 2851 opts = pycompat.byteskwargs(opts)
2852 2852 ctx = scmutil.revsingle(repo, opts.get(b'rev'))
2853 2853 m = scmutil.match(ctx, pats, opts)
2854 2854 for abs in ctx.walk(m):
2855 2855 fctx = ctx[abs]
2856 2856 o = fctx.filelog().renamed(fctx.filenode())
2857 2857 rel = repo.pathto(abs)
2858 2858 if o:
2859 2859 ui.write(_(b"%s renamed from %s:%s\n") % (rel, o[0], hex(o[1])))
2860 2860 else:
2861 2861 ui.write(_(b"%s not renamed\n") % rel)
2862 2862
2863 2863
2864 2864 @command(b'debugrequires|debugrequirements', [], b'')
2865 2865 def debugrequirements(ui, repo):
2866 2866 """ print the current repo requirements """
2867 2867 for r in sorted(repo.requirements):
2868 2868 ui.write(b"%s\n" % r)
2869 2869
2870 2870
2871 2871 @command(
2872 2872 b'debugrevlog',
2873 2873 cmdutil.debugrevlogopts + [(b'd', b'dump', False, _(b'dump index data'))],
2874 2874 _(b'-c|-m|FILE'),
2875 2875 optionalrepo=True,
2876 2876 )
2877 2877 def debugrevlog(ui, repo, file_=None, **opts):
2878 2878 """show data and statistics about a revlog"""
2879 2879 opts = pycompat.byteskwargs(opts)
2880 2880 r = cmdutil.openrevlog(repo, b'debugrevlog', file_, opts)
2881 2881
2882 2882 if opts.get(b"dump"):
2883 2883 numrevs = len(r)
2884 2884 ui.write(
2885 2885 (
2886 2886 b"# rev p1rev p2rev start end deltastart base p1 p2"
2887 2887 b" rawsize totalsize compression heads chainlen\n"
2888 2888 )
2889 2889 )
2890 2890 ts = 0
2891 2891 heads = set()
2892 2892
2893 2893 for rev in pycompat.xrange(numrevs):
2894 2894 dbase = r.deltaparent(rev)
2895 2895 if dbase == -1:
2896 2896 dbase = rev
2897 2897 cbase = r.chainbase(rev)
2898 2898 clen = r.chainlen(rev)
2899 2899 p1, p2 = r.parentrevs(rev)
2900 2900 rs = r.rawsize(rev)
2901 2901 ts = ts + rs
2902 2902 heads -= set(r.parentrevs(rev))
2903 2903 heads.add(rev)
2904 2904 try:
2905 2905 compression = ts / r.end(rev)
2906 2906 except ZeroDivisionError:
2907 2907 compression = 0
2908 2908 ui.write(
2909 2909 b"%5d %5d %5d %5d %5d %10d %4d %4d %4d %7d %9d "
2910 2910 b"%11d %5d %8d\n"
2911 2911 % (
2912 2912 rev,
2913 2913 p1,
2914 2914 p2,
2915 2915 r.start(rev),
2916 2916 r.end(rev),
2917 2917 r.start(dbase),
2918 2918 r.start(cbase),
2919 2919 r.start(p1),
2920 2920 r.start(p2),
2921 2921 rs,
2922 2922 ts,
2923 2923 compression,
2924 2924 len(heads),
2925 2925 clen,
2926 2926 )
2927 2927 )
2928 2928 return 0
2929 2929
2930 2930 v = r.version
2931 2931 format = v & 0xFFFF
2932 2932 flags = []
2933 2933 gdelta = False
2934 2934 if v & revlog.FLAG_INLINE_DATA:
2935 2935 flags.append(b'inline')
2936 2936 if v & revlog.FLAG_GENERALDELTA:
2937 2937 gdelta = True
2938 2938 flags.append(b'generaldelta')
2939 2939 if not flags:
2940 2940 flags = [b'(none)']
2941 2941
2942 2942 ### tracks merge vs single parent
2943 2943 nummerges = 0
2944 2944
2945 2945 ### tracks ways the "delta" are build
2946 2946 # nodelta
2947 2947 numempty = 0
2948 2948 numemptytext = 0
2949 2949 numemptydelta = 0
2950 2950 # full file content
2951 2951 numfull = 0
2952 2952 # intermediate snapshot against a prior snapshot
2953 2953 numsemi = 0
2954 2954 # snapshot count per depth
2955 2955 numsnapdepth = collections.defaultdict(lambda: 0)
2956 2956 # delta against previous revision
2957 2957 numprev = 0
2958 2958 # delta against first or second parent (not prev)
2959 2959 nump1 = 0
2960 2960 nump2 = 0
2961 2961 # delta against neither prev nor parents
2962 2962 numother = 0
2963 2963 # delta against prev that are also first or second parent
2964 2964 # (details of `numprev`)
2965 2965 nump1prev = 0
2966 2966 nump2prev = 0
2967 2967
2968 2968 # data about delta chain of each revs
2969 2969 chainlengths = []
2970 2970 chainbases = []
2971 2971 chainspans = []
2972 2972
2973 2973 # data about each revision
2974 2974 datasize = [None, 0, 0]
2975 2975 fullsize = [None, 0, 0]
2976 2976 semisize = [None, 0, 0]
2977 2977 # snapshot count per depth
2978 2978 snapsizedepth = collections.defaultdict(lambda: [None, 0, 0])
2979 2979 deltasize = [None, 0, 0]
2980 2980 chunktypecounts = {}
2981 2981 chunktypesizes = {}
2982 2982
2983 2983 def addsize(size, l):
2984 2984 if l[0] is None or size < l[0]:
2985 2985 l[0] = size
2986 2986 if size > l[1]:
2987 2987 l[1] = size
2988 2988 l[2] += size
2989 2989
2990 2990 numrevs = len(r)
2991 2991 for rev in pycompat.xrange(numrevs):
2992 2992 p1, p2 = r.parentrevs(rev)
2993 2993 delta = r.deltaparent(rev)
2994 2994 if format > 0:
2995 2995 addsize(r.rawsize(rev), datasize)
2996 2996 if p2 != nullrev:
2997 2997 nummerges += 1
2998 2998 size = r.length(rev)
2999 2999 if delta == nullrev:
3000 3000 chainlengths.append(0)
3001 3001 chainbases.append(r.start(rev))
3002 3002 chainspans.append(size)
3003 3003 if size == 0:
3004 3004 numempty += 1
3005 3005 numemptytext += 1
3006 3006 else:
3007 3007 numfull += 1
3008 3008 numsnapdepth[0] += 1
3009 3009 addsize(size, fullsize)
3010 3010 addsize(size, snapsizedepth[0])
3011 3011 else:
3012 3012 chainlengths.append(chainlengths[delta] + 1)
3013 3013 baseaddr = chainbases[delta]
3014 3014 revaddr = r.start(rev)
3015 3015 chainbases.append(baseaddr)
3016 3016 chainspans.append((revaddr - baseaddr) + size)
3017 3017 if size == 0:
3018 3018 numempty += 1
3019 3019 numemptydelta += 1
3020 3020 elif r.issnapshot(rev):
3021 3021 addsize(size, semisize)
3022 3022 numsemi += 1
3023 3023 depth = r.snapshotdepth(rev)
3024 3024 numsnapdepth[depth] += 1
3025 3025 addsize(size, snapsizedepth[depth])
3026 3026 else:
3027 3027 addsize(size, deltasize)
3028 3028 if delta == rev - 1:
3029 3029 numprev += 1
3030 3030 if delta == p1:
3031 3031 nump1prev += 1
3032 3032 elif delta == p2:
3033 3033 nump2prev += 1
3034 3034 elif delta == p1:
3035 3035 nump1 += 1
3036 3036 elif delta == p2:
3037 3037 nump2 += 1
3038 3038 elif delta != nullrev:
3039 3039 numother += 1
3040 3040
3041 3041 # Obtain data on the raw chunks in the revlog.
3042 3042 if util.safehasattr(r, b'_getsegmentforrevs'):
3043 3043 segment = r._getsegmentforrevs(rev, rev)[1]
3044 3044 else:
3045 3045 segment = r._revlog._getsegmentforrevs(rev, rev)[1]
3046 3046 if segment:
3047 3047 chunktype = bytes(segment[0:1])
3048 3048 else:
3049 3049 chunktype = b'empty'
3050 3050
3051 3051 if chunktype not in chunktypecounts:
3052 3052 chunktypecounts[chunktype] = 0
3053 3053 chunktypesizes[chunktype] = 0
3054 3054
3055 3055 chunktypecounts[chunktype] += 1
3056 3056 chunktypesizes[chunktype] += size
3057 3057
3058 3058 # Adjust size min value for empty cases
3059 3059 for size in (datasize, fullsize, semisize, deltasize):
3060 3060 if size[0] is None:
3061 3061 size[0] = 0
3062 3062
3063 3063 numdeltas = numrevs - numfull - numempty - numsemi
3064 3064 numoprev = numprev - nump1prev - nump2prev
3065 3065 totalrawsize = datasize[2]
3066 3066 datasize[2] /= numrevs
3067 3067 fulltotal = fullsize[2]
3068 3068 if numfull == 0:
3069 3069 fullsize[2] = 0
3070 3070 else:
3071 3071 fullsize[2] /= numfull
3072 3072 semitotal = semisize[2]
3073 3073 snaptotal = {}
3074 3074 if numsemi > 0:
3075 3075 semisize[2] /= numsemi
3076 3076 for depth in snapsizedepth:
3077 3077 snaptotal[depth] = snapsizedepth[depth][2]
3078 3078 snapsizedepth[depth][2] /= numsnapdepth[depth]
3079 3079
3080 3080 deltatotal = deltasize[2]
3081 3081 if numdeltas > 0:
3082 3082 deltasize[2] /= numdeltas
3083 3083 totalsize = fulltotal + semitotal + deltatotal
3084 3084 avgchainlen = sum(chainlengths) / numrevs
3085 3085 maxchainlen = max(chainlengths)
3086 3086 maxchainspan = max(chainspans)
3087 3087 compratio = 1
3088 3088 if totalsize:
3089 3089 compratio = totalrawsize / totalsize
3090 3090
3091 3091 basedfmtstr = b'%%%dd\n'
3092 3092 basepcfmtstr = b'%%%dd %s(%%5.2f%%%%)\n'
3093 3093
3094 3094 def dfmtstr(max):
3095 3095 return basedfmtstr % len(str(max))
3096 3096
3097 3097 def pcfmtstr(max, padding=0):
3098 3098 return basepcfmtstr % (len(str(max)), b' ' * padding)
3099 3099
3100 3100 def pcfmt(value, total):
3101 3101 if total:
3102 3102 return (value, 100 * float(value) / total)
3103 3103 else:
3104 3104 return value, 100.0
3105 3105
3106 3106 ui.writenoi18n(b'format : %d\n' % format)
3107 3107 ui.writenoi18n(b'flags : %s\n' % b', '.join(flags))
3108 3108
3109 3109 ui.write(b'\n')
3110 3110 fmt = pcfmtstr(totalsize)
3111 3111 fmt2 = dfmtstr(totalsize)
3112 3112 ui.writenoi18n(b'revisions : ' + fmt2 % numrevs)
3113 3113 ui.writenoi18n(b' merges : ' + fmt % pcfmt(nummerges, numrevs))
3114 3114 ui.writenoi18n(
3115 3115 b' normal : ' + fmt % pcfmt(numrevs - nummerges, numrevs)
3116 3116 )
3117 3117 ui.writenoi18n(b'revisions : ' + fmt2 % numrevs)
3118 3118 ui.writenoi18n(b' empty : ' + fmt % pcfmt(numempty, numrevs))
3119 3119 ui.writenoi18n(
3120 3120 b' text : '
3121 3121 + fmt % pcfmt(numemptytext, numemptytext + numemptydelta)
3122 3122 )
3123 3123 ui.writenoi18n(
3124 3124 b' delta : '
3125 3125 + fmt % pcfmt(numemptydelta, numemptytext + numemptydelta)
3126 3126 )
3127 3127 ui.writenoi18n(
3128 3128 b' snapshot : ' + fmt % pcfmt(numfull + numsemi, numrevs)
3129 3129 )
3130 3130 for depth in sorted(numsnapdepth):
3131 3131 ui.write(
3132 3132 (b' lvl-%-3d : ' % depth)
3133 3133 + fmt % pcfmt(numsnapdepth[depth], numrevs)
3134 3134 )
3135 3135 ui.writenoi18n(b' deltas : ' + fmt % pcfmt(numdeltas, numrevs))
3136 3136 ui.writenoi18n(b'revision size : ' + fmt2 % totalsize)
3137 3137 ui.writenoi18n(
3138 3138 b' snapshot : ' + fmt % pcfmt(fulltotal + semitotal, totalsize)
3139 3139 )
3140 3140 for depth in sorted(numsnapdepth):
3141 3141 ui.write(
3142 3142 (b' lvl-%-3d : ' % depth)
3143 3143 + fmt % pcfmt(snaptotal[depth], totalsize)
3144 3144 )
3145 3145 ui.writenoi18n(b' deltas : ' + fmt % pcfmt(deltatotal, totalsize))
3146 3146
3147 3147 def fmtchunktype(chunktype):
3148 3148 if chunktype == b'empty':
3149 3149 return b' %s : ' % chunktype
3150 3150 elif chunktype in pycompat.bytestr(string.ascii_letters):
3151 3151 return b' 0x%s (%s) : ' % (hex(chunktype), chunktype)
3152 3152 else:
3153 3153 return b' 0x%s : ' % hex(chunktype)
3154 3154
3155 3155 ui.write(b'\n')
3156 3156 ui.writenoi18n(b'chunks : ' + fmt2 % numrevs)
3157 3157 for chunktype in sorted(chunktypecounts):
3158 3158 ui.write(fmtchunktype(chunktype))
3159 3159 ui.write(fmt % pcfmt(chunktypecounts[chunktype], numrevs))
3160 3160 ui.writenoi18n(b'chunks size : ' + fmt2 % totalsize)
3161 3161 for chunktype in sorted(chunktypecounts):
3162 3162 ui.write(fmtchunktype(chunktype))
3163 3163 ui.write(fmt % pcfmt(chunktypesizes[chunktype], totalsize))
3164 3164
3165 3165 ui.write(b'\n')
3166 3166 fmt = dfmtstr(max(avgchainlen, maxchainlen, maxchainspan, compratio))
3167 3167 ui.writenoi18n(b'avg chain length : ' + fmt % avgchainlen)
3168 3168 ui.writenoi18n(b'max chain length : ' + fmt % maxchainlen)
3169 3169 ui.writenoi18n(b'max chain reach : ' + fmt % maxchainspan)
3170 3170 ui.writenoi18n(b'compression ratio : ' + fmt % compratio)
3171 3171
3172 3172 if format > 0:
3173 3173 ui.write(b'\n')
3174 3174 ui.writenoi18n(
3175 3175 b'uncompressed data size (min/max/avg) : %d / %d / %d\n'
3176 3176 % tuple(datasize)
3177 3177 )
3178 3178 ui.writenoi18n(
3179 3179 b'full revision size (min/max/avg) : %d / %d / %d\n'
3180 3180 % tuple(fullsize)
3181 3181 )
3182 3182 ui.writenoi18n(
3183 3183 b'inter-snapshot size (min/max/avg) : %d / %d / %d\n'
3184 3184 % tuple(semisize)
3185 3185 )
3186 3186 for depth in sorted(snapsizedepth):
3187 3187 if depth == 0:
3188 3188 continue
3189 3189 ui.writenoi18n(
3190 3190 b' level-%-3d (min/max/avg) : %d / %d / %d\n'
3191 3191 % ((depth,) + tuple(snapsizedepth[depth]))
3192 3192 )
3193 3193 ui.writenoi18n(
3194 3194 b'delta size (min/max/avg) : %d / %d / %d\n'
3195 3195 % tuple(deltasize)
3196 3196 )
3197 3197
3198 3198 if numdeltas > 0:
3199 3199 ui.write(b'\n')
3200 3200 fmt = pcfmtstr(numdeltas)
3201 3201 fmt2 = pcfmtstr(numdeltas, 4)
3202 3202 ui.writenoi18n(
3203 3203 b'deltas against prev : ' + fmt % pcfmt(numprev, numdeltas)
3204 3204 )
3205 3205 if numprev > 0:
3206 3206 ui.writenoi18n(
3207 3207 b' where prev = p1 : ' + fmt2 % pcfmt(nump1prev, numprev)
3208 3208 )
3209 3209 ui.writenoi18n(
3210 3210 b' where prev = p2 : ' + fmt2 % pcfmt(nump2prev, numprev)
3211 3211 )
3212 3212 ui.writenoi18n(
3213 3213 b' other : ' + fmt2 % pcfmt(numoprev, numprev)
3214 3214 )
3215 3215 if gdelta:
3216 3216 ui.writenoi18n(
3217 3217 b'deltas against p1 : ' + fmt % pcfmt(nump1, numdeltas)
3218 3218 )
3219 3219 ui.writenoi18n(
3220 3220 b'deltas against p2 : ' + fmt % pcfmt(nump2, numdeltas)
3221 3221 )
3222 3222 ui.writenoi18n(
3223 3223 b'deltas against other : ' + fmt % pcfmt(numother, numdeltas)
3224 3224 )
3225 3225
3226 3226
3227 3227 @command(
3228 3228 b'debugrevlogindex',
3229 3229 cmdutil.debugrevlogopts
3230 3230 + [(b'f', b'format', 0, _(b'revlog format'), _(b'FORMAT'))],
3231 3231 _(b'[-f FORMAT] -c|-m|FILE'),
3232 3232 optionalrepo=True,
3233 3233 )
3234 3234 def debugrevlogindex(ui, repo, file_=None, **opts):
3235 3235 """dump the contents of a revlog index"""
3236 3236 opts = pycompat.byteskwargs(opts)
3237 3237 r = cmdutil.openrevlog(repo, b'debugrevlogindex', file_, opts)
3238 3238 format = opts.get(b'format', 0)
3239 3239 if format not in (0, 1):
3240 3240 raise error.Abort(_(b"unknown format %d") % format)
3241 3241
3242 3242 if ui.debugflag:
3243 3243 shortfn = hex
3244 3244 else:
3245 3245 shortfn = short
3246 3246
3247 3247 # There might not be anything in r, so have a sane default
3248 3248 idlen = 12
3249 3249 for i in r:
3250 3250 idlen = len(shortfn(r.node(i)))
3251 3251 break
3252 3252
3253 3253 if format == 0:
3254 3254 if ui.verbose:
3255 3255 ui.writenoi18n(
3256 3256 b" rev offset length linkrev %s %s p2\n"
3257 3257 % (b"nodeid".ljust(idlen), b"p1".ljust(idlen))
3258 3258 )
3259 3259 else:
3260 3260 ui.writenoi18n(
3261 3261 b" rev linkrev %s %s p2\n"
3262 3262 % (b"nodeid".ljust(idlen), b"p1".ljust(idlen))
3263 3263 )
3264 3264 elif format == 1:
3265 3265 if ui.verbose:
3266 3266 ui.writenoi18n(
3267 3267 (
3268 3268 b" rev flag offset length size link p1"
3269 3269 b" p2 %s\n"
3270 3270 )
3271 3271 % b"nodeid".rjust(idlen)
3272 3272 )
3273 3273 else:
3274 3274 ui.writenoi18n(
3275 3275 b" rev flag size link p1 p2 %s\n"
3276 3276 % b"nodeid".rjust(idlen)
3277 3277 )
3278 3278
3279 3279 for i in r:
3280 3280 node = r.node(i)
3281 3281 if format == 0:
3282 3282 try:
3283 3283 pp = r.parents(node)
3284 3284 except Exception:
3285 3285 pp = [nullid, nullid]
3286 3286 if ui.verbose:
3287 3287 ui.write(
3288 3288 b"% 6d % 9d % 7d % 7d %s %s %s\n"
3289 3289 % (
3290 3290 i,
3291 3291 r.start(i),
3292 3292 r.length(i),
3293 3293 r.linkrev(i),
3294 3294 shortfn(node),
3295 3295 shortfn(pp[0]),
3296 3296 shortfn(pp[1]),
3297 3297 )
3298 3298 )
3299 3299 else:
3300 3300 ui.write(
3301 3301 b"% 6d % 7d %s %s %s\n"
3302 3302 % (
3303 3303 i,
3304 3304 r.linkrev(i),
3305 3305 shortfn(node),
3306 3306 shortfn(pp[0]),
3307 3307 shortfn(pp[1]),
3308 3308 )
3309 3309 )
3310 3310 elif format == 1:
3311 3311 pr = r.parentrevs(i)
3312 3312 if ui.verbose:
3313 3313 ui.write(
3314 3314 b"% 6d %04x % 8d % 8d % 8d % 6d % 6d % 6d %s\n"
3315 3315 % (
3316 3316 i,
3317 3317 r.flags(i),
3318 3318 r.start(i),
3319 3319 r.length(i),
3320 3320 r.rawsize(i),
3321 3321 r.linkrev(i),
3322 3322 pr[0],
3323 3323 pr[1],
3324 3324 shortfn(node),
3325 3325 )
3326 3326 )
3327 3327 else:
3328 3328 ui.write(
3329 3329 b"% 6d %04x % 8d % 6d % 6d % 6d %s\n"
3330 3330 % (
3331 3331 i,
3332 3332 r.flags(i),
3333 3333 r.rawsize(i),
3334 3334 r.linkrev(i),
3335 3335 pr[0],
3336 3336 pr[1],
3337 3337 shortfn(node),
3338 3338 )
3339 3339 )
3340 3340
3341 3341
3342 3342 @command(
3343 3343 b'debugrevspec',
3344 3344 [
3345 3345 (
3346 3346 b'',
3347 3347 b'optimize',
3348 3348 None,
3349 3349 _(b'print parsed tree after optimizing (DEPRECATED)'),
3350 3350 ),
3351 3351 (
3352 3352 b'',
3353 3353 b'show-revs',
3354 3354 True,
3355 3355 _(b'print list of result revisions (default)'),
3356 3356 ),
3357 3357 (
3358 3358 b's',
3359 3359 b'show-set',
3360 3360 None,
3361 3361 _(b'print internal representation of result set'),
3362 3362 ),
3363 3363 (
3364 3364 b'p',
3365 3365 b'show-stage',
3366 3366 [],
3367 3367 _(b'print parsed tree at the given stage'),
3368 3368 _(b'NAME'),
3369 3369 ),
3370 3370 (b'', b'no-optimized', False, _(b'evaluate tree without optimization')),
3371 3371 (b'', b'verify-optimized', False, _(b'verify optimized result')),
3372 3372 ],
3373 3373 b'REVSPEC',
3374 3374 )
3375 3375 def debugrevspec(ui, repo, expr, **opts):
3376 3376 """parse and apply a revision specification
3377 3377
3378 3378 Use -p/--show-stage option to print the parsed tree at the given stages.
3379 3379 Use -p all to print tree at every stage.
3380 3380
3381 3381 Use --no-show-revs option with -s or -p to print only the set
3382 3382 representation or the parsed tree respectively.
3383 3383
3384 3384 Use --verify-optimized to compare the optimized result with the unoptimized
3385 3385 one. Returns 1 if the optimized result differs.
3386 3386 """
3387 3387 opts = pycompat.byteskwargs(opts)
3388 3388 aliases = ui.configitems(b'revsetalias')
3389 3389 stages = [
3390 3390 (b'parsed', lambda tree: tree),
3391 3391 (
3392 3392 b'expanded',
3393 3393 lambda tree: revsetlang.expandaliases(tree, aliases, ui.warn),
3394 3394 ),
3395 3395 (b'concatenated', revsetlang.foldconcat),
3396 3396 (b'analyzed', revsetlang.analyze),
3397 3397 (b'optimized', revsetlang.optimize),
3398 3398 ]
3399 3399 if opts[b'no_optimized']:
3400 3400 stages = stages[:-1]
3401 3401 if opts[b'verify_optimized'] and opts[b'no_optimized']:
3402 3402 raise error.Abort(
3403 3403 _(b'cannot use --verify-optimized with --no-optimized')
3404 3404 )
3405 3405 stagenames = {n for n, f in stages}
3406 3406
3407 3407 showalways = set()
3408 3408 showchanged = set()
3409 3409 if ui.verbose and not opts[b'show_stage']:
3410 3410 # show parsed tree by --verbose (deprecated)
3411 3411 showalways.add(b'parsed')
3412 3412 showchanged.update([b'expanded', b'concatenated'])
3413 3413 if opts[b'optimize']:
3414 3414 showalways.add(b'optimized')
3415 3415 if opts[b'show_stage'] and opts[b'optimize']:
3416 3416 raise error.Abort(_(b'cannot use --optimize with --show-stage'))
3417 3417 if opts[b'show_stage'] == [b'all']:
3418 3418 showalways.update(stagenames)
3419 3419 else:
3420 3420 for n in opts[b'show_stage']:
3421 3421 if n not in stagenames:
3422 3422 raise error.Abort(_(b'invalid stage name: %s') % n)
3423 3423 showalways.update(opts[b'show_stage'])
3424 3424
3425 3425 treebystage = {}
3426 3426 printedtree = None
3427 3427 tree = revsetlang.parse(expr, lookup=revset.lookupfn(repo))
3428 3428 for n, f in stages:
3429 3429 treebystage[n] = tree = f(tree)
3430 3430 if n in showalways or (n in showchanged and tree != printedtree):
3431 3431 if opts[b'show_stage'] or n != b'parsed':
3432 3432 ui.write(b"* %s:\n" % n)
3433 3433 ui.write(revsetlang.prettyformat(tree), b"\n")
3434 3434 printedtree = tree
3435 3435
3436 3436 if opts[b'verify_optimized']:
3437 3437 arevs = revset.makematcher(treebystage[b'analyzed'])(repo)
3438 3438 brevs = revset.makematcher(treebystage[b'optimized'])(repo)
3439 3439 if opts[b'show_set'] or (opts[b'show_set'] is None and ui.verbose):
3440 3440 ui.writenoi18n(
3441 3441 b"* analyzed set:\n", stringutil.prettyrepr(arevs), b"\n"
3442 3442 )
3443 3443 ui.writenoi18n(
3444 3444 b"* optimized set:\n", stringutil.prettyrepr(brevs), b"\n"
3445 3445 )
3446 3446 arevs = list(arevs)
3447 3447 brevs = list(brevs)
3448 3448 if arevs == brevs:
3449 3449 return 0
3450 3450 ui.writenoi18n(b'--- analyzed\n', label=b'diff.file_a')
3451 3451 ui.writenoi18n(b'+++ optimized\n', label=b'diff.file_b')
3452 3452 sm = difflib.SequenceMatcher(None, arevs, brevs)
3453 3453 for tag, alo, ahi, blo, bhi in sm.get_opcodes():
3454 3454 if tag in ('delete', 'replace'):
3455 3455 for c in arevs[alo:ahi]:
3456 3456 ui.write(b'-%d\n' % c, label=b'diff.deleted')
3457 3457 if tag in ('insert', 'replace'):
3458 3458 for c in brevs[blo:bhi]:
3459 3459 ui.write(b'+%d\n' % c, label=b'diff.inserted')
3460 3460 if tag == 'equal':
3461 3461 for c in arevs[alo:ahi]:
3462 3462 ui.write(b' %d\n' % c)
3463 3463 return 1
3464 3464
3465 3465 func = revset.makematcher(tree)
3466 3466 revs = func(repo)
3467 3467 if opts[b'show_set'] or (opts[b'show_set'] is None and ui.verbose):
3468 3468 ui.writenoi18n(b"* set:\n", stringutil.prettyrepr(revs), b"\n")
3469 3469 if not opts[b'show_revs']:
3470 3470 return
3471 3471 for c in revs:
3472 3472 ui.write(b"%d\n" % c)
3473 3473
3474 3474
3475 3475 @command(
3476 3476 b'debugserve',
3477 3477 [
3478 3478 (
3479 3479 b'',
3480 3480 b'sshstdio',
3481 3481 False,
3482 3482 _(b'run an SSH server bound to process handles'),
3483 3483 ),
3484 3484 (b'', b'logiofd', b'', _(b'file descriptor to log server I/O to')),
3485 3485 (b'', b'logiofile', b'', _(b'file to log server I/O to')),
3486 3486 ],
3487 3487 b'',
3488 3488 )
3489 3489 def debugserve(ui, repo, **opts):
3490 3490 """run a server with advanced settings
3491 3491
3492 3492 This command is similar to :hg:`serve`. It exists partially as a
3493 3493 workaround to the fact that ``hg serve --stdio`` must have specific
3494 3494 arguments for security reasons.
3495 3495 """
3496 3496 opts = pycompat.byteskwargs(opts)
3497 3497
3498 3498 if not opts[b'sshstdio']:
3499 3499 raise error.Abort(_(b'only --sshstdio is currently supported'))
3500 3500
3501 3501 logfh = None
3502 3502
3503 3503 if opts[b'logiofd'] and opts[b'logiofile']:
3504 3504 raise error.Abort(_(b'cannot use both --logiofd and --logiofile'))
3505 3505
3506 3506 if opts[b'logiofd']:
3507 3507 # Ideally we would be line buffered. But line buffering in binary
3508 3508 # mode isn't supported and emits a warning in Python 3.8+. Disabling
3509 3509 # buffering could have performance impacts. But since this isn't
3510 3510 # performance critical code, it should be fine.
3511 3511 try:
3512 3512 logfh = os.fdopen(int(opts[b'logiofd']), 'ab', 0)
3513 3513 except OSError as e:
3514 3514 if e.errno != errno.ESPIPE:
3515 3515 raise
3516 3516 # can't seek a pipe, so `ab` mode fails on py3
3517 3517 logfh = os.fdopen(int(opts[b'logiofd']), 'wb', 0)
3518 3518 elif opts[b'logiofile']:
3519 3519 logfh = open(opts[b'logiofile'], b'ab', 0)
3520 3520
3521 3521 s = wireprotoserver.sshserver(ui, repo, logfh=logfh)
3522 3522 s.serve_forever()
3523 3523
3524 3524
3525 3525 @command(b'debugsetparents', [], _(b'REV1 [REV2]'))
3526 3526 def debugsetparents(ui, repo, rev1, rev2=None):
3527 3527 """manually set the parents of the current working directory (DANGEROUS)
3528 3528
3529 3529 This command is not what you are looking for and should not be used. Using
3530 3530 this command will most certainly results in slight corruption of the file
3531 3531 level histories withing your repository. DO NOT USE THIS COMMAND.
3532 3532
3533 3533 The command update the p1 and p2 field in the dirstate, and not touching
3534 3534 anything else. This useful for writing repository conversion tools, but
3535 3535 should be used with extreme care. For example, neither the working
3536 3536 directory nor the dirstate is updated, so file status may be incorrect
3537 3537 after running this command. Only used if you are one of the few people that
3538 3538 deeply unstand both conversion tools and file level histories. If you are
3539 3539 reading this help, you are not one of this people (most of them sailed west
3540 3540 from Mithlond anyway.
3541 3541
3542 3542 So one last time DO NOT USE THIS COMMAND.
3543 3543
3544 3544 Returns 0 on success.
3545 3545 """
3546 3546
3547 3547 node1 = scmutil.revsingle(repo, rev1).node()
3548 3548 node2 = scmutil.revsingle(repo, rev2, b'null').node()
3549 3549
3550 3550 with repo.wlock():
3551 3551 repo.setparents(node1, node2)
3552 3552
3553 3553
3554 3554 @command(b'debugsidedata', cmdutil.debugrevlogopts, _(b'-c|-m|FILE REV'))
3555 3555 def debugsidedata(ui, repo, file_, rev=None, **opts):
3556 3556 """dump the side data for a cl/manifest/file revision
3557 3557
3558 3558 Use --verbose to dump the sidedata content."""
3559 3559 opts = pycompat.byteskwargs(opts)
3560 3560 if opts.get(b'changelog') or opts.get(b'manifest') or opts.get(b'dir'):
3561 3561 if rev is not None:
3562 3562 raise error.CommandError(b'debugdata', _(b'invalid arguments'))
3563 3563 file_, rev = None, file_
3564 3564 elif rev is None:
3565 3565 raise error.CommandError(b'debugdata', _(b'invalid arguments'))
3566 3566 r = cmdutil.openstorage(repo, b'debugdata', file_, opts)
3567 3567 r = getattr(r, '_revlog', r)
3568 3568 try:
3569 3569 sidedata = r.sidedata(r.lookup(rev))
3570 3570 except KeyError:
3571 3571 raise error.Abort(_(b'invalid revision identifier %s') % rev)
3572 3572 if sidedata:
3573 3573 sidedata = list(sidedata.items())
3574 3574 sidedata.sort()
3575 3575 ui.writenoi18n(b'%d sidedata entries\n' % len(sidedata))
3576 3576 for key, value in sidedata:
3577 3577 ui.writenoi18n(b' entry-%04o size %d\n' % (key, len(value)))
3578 3578 if ui.verbose:
3579 3579 ui.writenoi18n(b' %s\n' % stringutil.pprint(value))
3580 3580
3581 3581
3582 3582 @command(b'debugssl', [], b'[SOURCE]', optionalrepo=True)
3583 3583 def debugssl(ui, repo, source=None, **opts):
3584 3584 """test a secure connection to a server
3585 3585
3586 3586 This builds the certificate chain for the server on Windows, installing the
3587 3587 missing intermediates and trusted root via Windows Update if necessary. It
3588 3588 does nothing on other platforms.
3589 3589
3590 3590 If SOURCE is omitted, the 'default' path will be used. If a URL is given,
3591 3591 that server is used. See :hg:`help urls` for more information.
3592 3592
3593 3593 If the update succeeds, retry the original operation. Otherwise, the cause
3594 3594 of the SSL error is likely another issue.
3595 3595 """
3596 3596 if not pycompat.iswindows:
3597 3597 raise error.Abort(
3598 3598 _(b'certificate chain building is only possible on Windows')
3599 3599 )
3600 3600
3601 3601 if not source:
3602 3602 if not repo:
3603 3603 raise error.Abort(
3604 3604 _(
3605 3605 b"there is no Mercurial repository here, and no "
3606 3606 b"server specified"
3607 3607 )
3608 3608 )
3609 3609 source = b"default"
3610 3610
3611 3611 source, branches = hg.parseurl(ui.expandpath(source))
3612 3612 url = util.url(source)
3613 3613
3614 3614 defaultport = {b'https': 443, b'ssh': 22}
3615 3615 if url.scheme in defaultport:
3616 3616 try:
3617 3617 addr = (url.host, int(url.port or defaultport[url.scheme]))
3618 3618 except ValueError:
3619 3619 raise error.Abort(_(b"malformed port number in URL"))
3620 3620 else:
3621 3621 raise error.Abort(_(b"only https and ssh connections are supported"))
3622 3622
3623 3623 from . import win32
3624 3624
3625 3625 s = ssl.wrap_socket(
3626 3626 socket.socket(),
3627 3627 ssl_version=ssl.PROTOCOL_TLS,
3628 3628 cert_reqs=ssl.CERT_NONE,
3629 3629 ca_certs=None,
3630 3630 )
3631 3631
3632 3632 try:
3633 3633 s.connect(addr)
3634 3634 cert = s.getpeercert(True)
3635 3635
3636 3636 ui.status(_(b'checking the certificate chain for %s\n') % url.host)
3637 3637
3638 3638 complete = win32.checkcertificatechain(cert, build=False)
3639 3639
3640 3640 if not complete:
3641 3641 ui.status(_(b'certificate chain is incomplete, updating... '))
3642 3642
3643 3643 if not win32.checkcertificatechain(cert):
3644 3644 ui.status(_(b'failed.\n'))
3645 3645 else:
3646 3646 ui.status(_(b'done.\n'))
3647 3647 else:
3648 3648 ui.status(_(b'full certificate chain is available\n'))
3649 3649 finally:
3650 3650 s.close()
3651 3651
3652 3652
3653 3653 @command(
3654 3654 b"debugbackupbundle",
3655 3655 [
3656 3656 (
3657 3657 b"",
3658 3658 b"recover",
3659 3659 b"",
3660 3660 b"brings the specified changeset back into the repository",
3661 3661 )
3662 3662 ]
3663 3663 + cmdutil.logopts,
3664 3664 _(b"hg debugbackupbundle [--recover HASH]"),
3665 3665 )
3666 3666 def debugbackupbundle(ui, repo, *pats, **opts):
3667 3667 """lists the changesets available in backup bundles
3668 3668
3669 3669 Without any arguments, this command prints a list of the changesets in each
3670 3670 backup bundle.
3671 3671
3672 3672 --recover takes a changeset hash and unbundles the first bundle that
3673 3673 contains that hash, which puts that changeset back in your repository.
3674 3674
3675 3675 --verbose will print the entire commit message and the bundle path for that
3676 3676 backup.
3677 3677 """
3678 3678 backups = list(
3679 3679 filter(
3680 3680 os.path.isfile, glob.glob(repo.vfs.join(b"strip-backup") + b"/*.hg")
3681 3681 )
3682 3682 )
3683 3683 backups.sort(key=lambda x: os.path.getmtime(x), reverse=True)
3684 3684
3685 3685 opts = pycompat.byteskwargs(opts)
3686 3686 opts[b"bundle"] = b""
3687 3687 opts[b"force"] = None
3688 3688 limit = logcmdutil.getlimit(opts)
3689 3689
3690 3690 def display(other, chlist, displayer):
3691 3691 if opts.get(b"newest_first"):
3692 3692 chlist.reverse()
3693 3693 count = 0
3694 3694 for n in chlist:
3695 3695 if limit is not None and count >= limit:
3696 3696 break
3697 3697 parents = [True for p in other.changelog.parents(n) if p != nullid]
3698 3698 if opts.get(b"no_merges") and len(parents) == 2:
3699 3699 continue
3700 3700 count += 1
3701 3701 displayer.show(other[n])
3702 3702
3703 3703 recovernode = opts.get(b"recover")
3704 3704 if recovernode:
3705 3705 if scmutil.isrevsymbol(repo, recovernode):
3706 3706 ui.warn(_(b"%s already exists in the repo\n") % recovernode)
3707 3707 return
3708 3708 elif backups:
3709 3709 msg = _(
3710 3710 b"Recover changesets using: hg debugbackupbundle --recover "
3711 3711 b"<changeset hash>\n\nAvailable backup changesets:"
3712 3712 )
3713 3713 ui.status(msg, label=b"status.removed")
3714 3714 else:
3715 3715 ui.status(_(b"no backup changesets found\n"))
3716 3716 return
3717 3717
3718 3718 for backup in backups:
3719 3719 # Much of this is copied from the hg incoming logic
3720 3720 source = ui.expandpath(os.path.relpath(backup, encoding.getcwd()))
3721 3721 source, branches = hg.parseurl(source, opts.get(b"branch"))
3722 3722 try:
3723 3723 other = hg.peer(repo, opts, source)
3724 3724 except error.LookupError as ex:
3725 3725 msg = _(b"\nwarning: unable to open bundle %s") % source
3726 3726 hint = _(b"\n(missing parent rev %s)\n") % short(ex.name)
3727 3727 ui.warn(msg, hint=hint)
3728 3728 continue
3729 3729 revs, checkout = hg.addbranchrevs(
3730 3730 repo, other, branches, opts.get(b"rev")
3731 3731 )
3732 3732
3733 3733 if revs:
3734 3734 revs = [other.lookup(rev) for rev in revs]
3735 3735
3736 3736 quiet = ui.quiet
3737 3737 try:
3738 3738 ui.quiet = True
3739 3739 other, chlist, cleanupfn = bundlerepo.getremotechanges(
3740 3740 ui, repo, other, revs, opts[b"bundle"], opts[b"force"]
3741 3741 )
3742 3742 except error.LookupError:
3743 3743 continue
3744 3744 finally:
3745 3745 ui.quiet = quiet
3746 3746
3747 3747 try:
3748 3748 if not chlist:
3749 3749 continue
3750 3750 if recovernode:
3751 3751 with repo.lock(), repo.transaction(b"unbundle") as tr:
3752 3752 if scmutil.isrevsymbol(other, recovernode):
3753 3753 ui.status(_(b"Unbundling %s\n") % (recovernode))
3754 3754 f = hg.openpath(ui, source)
3755 3755 gen = exchange.readbundle(ui, f, source)
3756 3756 if isinstance(gen, bundle2.unbundle20):
3757 3757 bundle2.applybundle(
3758 3758 repo,
3759 3759 gen,
3760 3760 tr,
3761 3761 source=b"unbundle",
3762 3762 url=b"bundle:" + source,
3763 3763 )
3764 3764 else:
3765 3765 gen.apply(repo, b"unbundle", b"bundle:" + source)
3766 3766 break
3767 3767 else:
3768 3768 backupdate = encoding.strtolocal(
3769 3769 time.strftime(
3770 3770 "%a %H:%M, %Y-%m-%d",
3771 3771 time.localtime(os.path.getmtime(source)),
3772 3772 )
3773 3773 )
3774 3774 ui.status(b"\n%s\n" % (backupdate.ljust(50)))
3775 3775 if ui.verbose:
3776 3776 ui.status(b"%s%s\n" % (b"bundle:".ljust(13), source))
3777 3777 else:
3778 3778 opts[
3779 3779 b"template"
3780 3780 ] = b"{label('status.modified', node|short)} {desc|firstline}\n"
3781 3781 displayer = logcmdutil.changesetdisplayer(
3782 3782 ui, other, opts, False
3783 3783 )
3784 3784 display(other, chlist, displayer)
3785 3785 displayer.close()
3786 3786 finally:
3787 3787 cleanupfn()
3788 3788
3789 3789
3790 3790 @command(
3791 3791 b'debugsub',
3792 3792 [(b'r', b'rev', b'', _(b'revision to check'), _(b'REV'))],
3793 3793 _(b'[-r REV] [REV]'),
3794 3794 )
3795 3795 def debugsub(ui, repo, rev=None):
3796 3796 ctx = scmutil.revsingle(repo, rev, None)
3797 3797 for k, v in sorted(ctx.substate.items()):
3798 3798 ui.writenoi18n(b'path %s\n' % k)
3799 3799 ui.writenoi18n(b' source %s\n' % v[0])
3800 3800 ui.writenoi18n(b' revision %s\n' % v[1])
3801 3801
3802 3802
3803 3803 @command(b'debugshell', optionalrepo=True)
3804 3804 def debugshell(ui, repo):
3805 3805 """run an interactive Python interpreter
3806 3806
3807 3807 The local namespace is provided with a reference to the ui and
3808 3808 the repo instance (if available).
3809 3809 """
3810 3810 import code
3811 3811
3812 3812 imported_objects = {
3813 3813 'ui': ui,
3814 3814 'repo': repo,
3815 3815 }
3816 3816
3817 3817 code.interact(local=imported_objects)
3818 3818
3819 3819
3820 3820 @command(
3821 3821 b'debugsuccessorssets',
3822 3822 [(b'', b'closest', False, _(b'return closest successors sets only'))],
3823 3823 _(b'[REV]'),
3824 3824 )
3825 3825 def debugsuccessorssets(ui, repo, *revs, **opts):
3826 3826 """show set of successors for revision
3827 3827
3828 3828 A successors set of changeset A is a consistent group of revisions that
3829 3829 succeed A. It contains non-obsolete changesets only unless closests
3830 3830 successors set is set.
3831 3831
3832 3832 In most cases a changeset A has a single successors set containing a single
3833 3833 successor (changeset A replaced by A').
3834 3834
3835 3835 A changeset that is made obsolete with no successors are called "pruned".
3836 3836 Such changesets have no successors sets at all.
3837 3837
3838 3838 A changeset that has been "split" will have a successors set containing
3839 3839 more than one successor.
3840 3840
3841 3841 A changeset that has been rewritten in multiple different ways is called
3842 3842 "divergent". Such changesets have multiple successor sets (each of which
3843 3843 may also be split, i.e. have multiple successors).
3844 3844
3845 3845 Results are displayed as follows::
3846 3846
3847 3847 <rev1>
3848 3848 <successors-1A>
3849 3849 <rev2>
3850 3850 <successors-2A>
3851 3851 <successors-2B1> <successors-2B2> <successors-2B3>
3852 3852
3853 3853 Here rev2 has two possible (i.e. divergent) successors sets. The first
3854 3854 holds one element, whereas the second holds three (i.e. the changeset has
3855 3855 been split).
3856 3856 """
3857 3857 # passed to successorssets caching computation from one call to another
3858 3858 cache = {}
3859 3859 ctx2str = bytes
3860 3860 node2str = short
3861 3861 for rev in scmutil.revrange(repo, revs):
3862 3862 ctx = repo[rev]
3863 3863 ui.write(b'%s\n' % ctx2str(ctx))
3864 3864 for succsset in obsutil.successorssets(
3865 3865 repo, ctx.node(), closest=opts['closest'], cache=cache
3866 3866 ):
3867 3867 if succsset:
3868 3868 ui.write(b' ')
3869 3869 ui.write(node2str(succsset[0]))
3870 3870 for node in succsset[1:]:
3871 3871 ui.write(b' ')
3872 3872 ui.write(node2str(node))
3873 3873 ui.write(b'\n')
3874 3874
3875 3875
3876 3876 @command(b'debugtagscache', [])
3877 3877 def debugtagscache(ui, repo):
3878 3878 """display the contents of .hg/cache/hgtagsfnodes1"""
3879 3879 cache = tagsmod.hgtagsfnodescache(repo.unfiltered())
3880 3880 flog = repo.file(b'.hgtags')
3881 3881 for r in repo:
3882 3882 node = repo[r].node()
3883 3883 tagsnode = cache.getfnode(node, computemissing=False)
3884 3884 if tagsnode:
3885 3885 tagsnodedisplay = hex(tagsnode)
3886 3886 if not flog.hasnode(tagsnode):
3887 3887 tagsnodedisplay += b' (unknown node)'
3888 3888 elif tagsnode is None:
3889 3889 tagsnodedisplay = b'missing'
3890 3890 else:
3891 3891 tagsnodedisplay = b'invalid'
3892 3892
3893 3893 ui.write(b'%d %s %s\n' % (r, hex(node), tagsnodedisplay))
3894 3894
3895 3895
3896 3896 @command(
3897 3897 b'debugtemplate',
3898 3898 [
3899 3899 (b'r', b'rev', [], _(b'apply template on changesets'), _(b'REV')),
3900 3900 (b'D', b'define', [], _(b'define template keyword'), _(b'KEY=VALUE')),
3901 3901 ],
3902 3902 _(b'[-r REV]... [-D KEY=VALUE]... TEMPLATE'),
3903 3903 optionalrepo=True,
3904 3904 )
3905 3905 def debugtemplate(ui, repo, tmpl, **opts):
3906 3906 """parse and apply a template
3907 3907
3908 3908 If -r/--rev is given, the template is processed as a log template and
3909 3909 applied to the given changesets. Otherwise, it is processed as a generic
3910 3910 template.
3911 3911
3912 3912 Use --verbose to print the parsed tree.
3913 3913 """
3914 3914 revs = None
3915 3915 if opts['rev']:
3916 3916 if repo is None:
3917 3917 raise error.RepoError(
3918 3918 _(b'there is no Mercurial repository here (.hg not found)')
3919 3919 )
3920 3920 revs = scmutil.revrange(repo, opts['rev'])
3921 3921
3922 3922 props = {}
3923 3923 for d in opts['define']:
3924 3924 try:
3925 3925 k, v = (e.strip() for e in d.split(b'=', 1))
3926 3926 if not k or k == b'ui':
3927 3927 raise ValueError
3928 3928 props[k] = v
3929 3929 except ValueError:
3930 3930 raise error.Abort(_(b'malformed keyword definition: %s') % d)
3931 3931
3932 3932 if ui.verbose:
3933 3933 aliases = ui.configitems(b'templatealias')
3934 3934 tree = templater.parse(tmpl)
3935 3935 ui.note(templater.prettyformat(tree), b'\n')
3936 3936 newtree = templater.expandaliases(tree, aliases)
3937 3937 if newtree != tree:
3938 3938 ui.notenoi18n(
3939 3939 b"* expanded:\n", templater.prettyformat(newtree), b'\n'
3940 3940 )
3941 3941
3942 3942 if revs is None:
3943 3943 tres = formatter.templateresources(ui, repo)
3944 3944 t = formatter.maketemplater(ui, tmpl, resources=tres)
3945 3945 if ui.verbose:
3946 3946 kwds, funcs = t.symbolsuseddefault()
3947 3947 ui.writenoi18n(b"* keywords: %s\n" % b', '.join(sorted(kwds)))
3948 3948 ui.writenoi18n(b"* functions: %s\n" % b', '.join(sorted(funcs)))
3949 3949 ui.write(t.renderdefault(props))
3950 3950 else:
3951 3951 displayer = logcmdutil.maketemplater(ui, repo, tmpl)
3952 3952 if ui.verbose:
3953 3953 kwds, funcs = displayer.t.symbolsuseddefault()
3954 3954 ui.writenoi18n(b"* keywords: %s\n" % b', '.join(sorted(kwds)))
3955 3955 ui.writenoi18n(b"* functions: %s\n" % b', '.join(sorted(funcs)))
3956 3956 for r in revs:
3957 3957 displayer.show(repo[r], **pycompat.strkwargs(props))
3958 3958 displayer.close()
3959 3959
3960 3960
3961 3961 @command(
3962 3962 b'debuguigetpass',
3963 3963 [
3964 3964 (b'p', b'prompt', b'', _(b'prompt text'), _(b'TEXT')),
3965 3965 ],
3966 3966 _(b'[-p TEXT]'),
3967 3967 norepo=True,
3968 3968 )
3969 3969 def debuguigetpass(ui, prompt=b''):
3970 3970 """show prompt to type password"""
3971 3971 r = ui.getpass(prompt)
3972 3972 if r is None:
3973 3973 r = b"<default response>"
3974 3974 ui.writenoi18n(b'response: %s\n' % r)
3975 3975
3976 3976
3977 3977 @command(
3978 3978 b'debuguiprompt',
3979 3979 [
3980 3980 (b'p', b'prompt', b'', _(b'prompt text'), _(b'TEXT')),
3981 3981 ],
3982 3982 _(b'[-p TEXT]'),
3983 3983 norepo=True,
3984 3984 )
3985 3985 def debuguiprompt(ui, prompt=b''):
3986 3986 """show plain prompt"""
3987 3987 r = ui.prompt(prompt)
3988 3988 ui.writenoi18n(b'response: %s\n' % r)
3989 3989
3990 3990
3991 3991 @command(b'debugupdatecaches', [])
3992 3992 def debugupdatecaches(ui, repo, *pats, **opts):
3993 3993 """warm all known caches in the repository"""
3994 3994 with repo.wlock(), repo.lock():
3995 3995 repo.updatecaches(full=True)
3996 3996
3997 3997
3998 3998 @command(
3999 3999 b'debugupgraderepo',
4000 4000 [
4001 4001 (
4002 4002 b'o',
4003 4003 b'optimize',
4004 4004 [],
4005 4005 _(b'extra optimization to perform'),
4006 4006 _(b'NAME'),
4007 4007 ),
4008 4008 (b'', b'run', False, _(b'performs an upgrade')),
4009 4009 (b'', b'backup', True, _(b'keep the old repository content around')),
4010 4010 (b'', b'changelog', None, _(b'select the changelog for upgrade')),
4011 4011 (b'', b'manifest', None, _(b'select the manifest for upgrade')),
4012 4012 (b'', b'filelogs', None, _(b'select all filelogs for upgrade')),
4013 4013 ],
4014 4014 )
4015 4015 def debugupgraderepo(ui, repo, run=False, optimize=None, backup=True, **opts):
4016 4016 """upgrade a repository to use different features
4017 4017
4018 4018 If no arguments are specified, the repository is evaluated for upgrade
4019 4019 and a list of problems and potential optimizations is printed.
4020 4020
4021 4021 With ``--run``, a repository upgrade is performed. Behavior of the upgrade
4022 4022 can be influenced via additional arguments. More details will be provided
4023 4023 by the command output when run without ``--run``.
4024 4024
4025 4025 During the upgrade, the repository will be locked and no writes will be
4026 4026 allowed.
4027 4027
4028 4028 At the end of the upgrade, the repository may not be readable while new
4029 4029 repository data is swapped in. This window will be as long as it takes to
4030 4030 rename some directories inside the ``.hg`` directory. On most machines, this
4031 4031 should complete almost instantaneously and the chances of a consumer being
4032 4032 unable to access the repository should be low.
4033 4033
4034 4034 By default, all revlog will be upgraded. You can restrict this using flag
4035 4035 such as `--manifest`:
4036 4036
4037 4037 * `--manifest`: only optimize the manifest
4038 4038 * `--no-manifest`: optimize all revlog but the manifest
4039 4039 * `--changelog`: optimize the changelog only
4040 4040 * `--no-changelog --no-manifest`: optimize filelogs only
4041 4041 * `--filelogs`: optimize the filelogs only
4042 4042 * `--no-changelog --no-manifest --no-filelogs`: skip all revlog optimizations
4043 4043 """
4044 4044 return upgrade.upgraderepo(
4045 4045 ui, repo, run=run, optimize=set(optimize), backup=backup, **opts
4046 4046 )
4047 4047
4048 4048
4049 4049 @command(
4050 4050 b'debugwalk', cmdutil.walkopts, _(b'[OPTION]... [FILE]...'), inferrepo=True
4051 4051 )
4052 4052 def debugwalk(ui, repo, *pats, **opts):
4053 4053 """show how files match on given patterns"""
4054 4054 opts = pycompat.byteskwargs(opts)
4055 4055 m = scmutil.match(repo[None], pats, opts)
4056 4056 if ui.verbose:
4057 4057 ui.writenoi18n(b'* matcher:\n', stringutil.prettyrepr(m), b'\n')
4058 4058 items = list(repo[None].walk(m))
4059 4059 if not items:
4060 4060 return
4061 4061 f = lambda fn: fn
4062 4062 if ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/':
4063 4063 f = lambda fn: util.normpath(fn)
4064 4064 fmt = b'f %%-%ds %%-%ds %%s' % (
4065 4065 max([len(abs) for abs in items]),
4066 4066 max([len(repo.pathto(abs)) for abs in items]),
4067 4067 )
4068 4068 for abs in items:
4069 4069 line = fmt % (
4070 4070 abs,
4071 4071 f(repo.pathto(abs)),
4072 4072 m.exact(abs) and b'exact' or b'',
4073 4073 )
4074 4074 ui.write(b"%s\n" % line.rstrip())
4075 4075
4076 4076
4077 4077 @command(b'debugwhyunstable', [], _(b'REV'))
4078 4078 def debugwhyunstable(ui, repo, rev):
4079 4079 """explain instabilities of a changeset"""
4080 4080 for entry in obsutil.whyunstable(repo, scmutil.revsingle(repo, rev)):
4081 4081 dnodes = b''
4082 4082 if entry.get(b'divergentnodes'):
4083 4083 dnodes = (
4084 4084 b' '.join(
4085 4085 b'%s (%s)' % (ctx.hex(), ctx.phasestr())
4086 4086 for ctx in entry[b'divergentnodes']
4087 4087 )
4088 4088 + b' '
4089 4089 )
4090 4090 ui.write(
4091 4091 b'%s: %s%s %s\n'
4092 4092 % (entry[b'instability'], dnodes, entry[b'reason'], entry[b'node'])
4093 4093 )
4094 4094
4095 4095
4096 4096 @command(
4097 4097 b'debugwireargs',
4098 4098 [
4099 4099 (b'', b'three', b'', b'three'),
4100 4100 (b'', b'four', b'', b'four'),
4101 4101 (b'', b'five', b'', b'five'),
4102 4102 ]
4103 4103 + cmdutil.remoteopts,
4104 4104 _(b'REPO [OPTIONS]... [ONE [TWO]]'),
4105 4105 norepo=True,
4106 4106 )
4107 4107 def debugwireargs(ui, repopath, *vals, **opts):
4108 4108 opts = pycompat.byteskwargs(opts)
4109 4109 repo = hg.peer(ui, opts, repopath)
4110 4110 try:
4111 4111 for opt in cmdutil.remoteopts:
4112 4112 del opts[opt[1]]
4113 4113 args = {}
4114 4114 for k, v in pycompat.iteritems(opts):
4115 4115 if v:
4116 4116 args[k] = v
4117 4117 args = pycompat.strkwargs(args)
4118 4118 # run twice to check that we don't mess up the stream for the next command
4119 4119 res1 = repo.debugwireargs(*vals, **args)
4120 4120 res2 = repo.debugwireargs(*vals, **args)
4121 4121 ui.write(b"%s\n" % res1)
4122 4122 if res1 != res2:
4123 4123 ui.warn(b"%s\n" % res2)
4124 4124 finally:
4125 4125 repo.close()
4126 4126
4127 4127
4128 4128 def _parsewirelangblocks(fh):
4129 4129 activeaction = None
4130 4130 blocklines = []
4131 4131 lastindent = 0
4132 4132
4133 4133 for line in fh:
4134 4134 line = line.rstrip()
4135 4135 if not line:
4136 4136 continue
4137 4137
4138 4138 if line.startswith(b'#'):
4139 4139 continue
4140 4140
4141 4141 if not line.startswith(b' '):
4142 4142 # New block. Flush previous one.
4143 4143 if activeaction:
4144 4144 yield activeaction, blocklines
4145 4145
4146 4146 activeaction = line
4147 4147 blocklines = []
4148 4148 lastindent = 0
4149 4149 continue
4150 4150
4151 4151 # Else we start with an indent.
4152 4152
4153 4153 if not activeaction:
4154 4154 raise error.Abort(_(b'indented line outside of block'))
4155 4155
4156 4156 indent = len(line) - len(line.lstrip())
4157 4157
4158 4158 # If this line is indented more than the last line, concatenate it.
4159 4159 if indent > lastindent and blocklines:
4160 4160 blocklines[-1] += line.lstrip()
4161 4161 else:
4162 4162 blocklines.append(line)
4163 4163 lastindent = indent
4164 4164
4165 4165 # Flush last block.
4166 4166 if activeaction:
4167 4167 yield activeaction, blocklines
4168 4168
4169 4169
4170 4170 @command(
4171 4171 b'debugwireproto',
4172 4172 [
4173 4173 (b'', b'localssh', False, _(b'start an SSH server for this repo')),
4174 4174 (b'', b'peer', b'', _(b'construct a specific version of the peer')),
4175 4175 (
4176 4176 b'',
4177 4177 b'noreadstderr',
4178 4178 False,
4179 4179 _(b'do not read from stderr of the remote'),
4180 4180 ),
4181 4181 (
4182 4182 b'',
4183 4183 b'nologhandshake',
4184 4184 False,
4185 4185 _(b'do not log I/O related to the peer handshake'),
4186 4186 ),
4187 4187 ]
4188 4188 + cmdutil.remoteopts,
4189 4189 _(b'[PATH]'),
4190 4190 optionalrepo=True,
4191 4191 )
4192 4192 def debugwireproto(ui, repo, path=None, **opts):
4193 4193 """send wire protocol commands to a server
4194 4194
4195 4195 This command can be used to issue wire protocol commands to remote
4196 4196 peers and to debug the raw data being exchanged.
4197 4197
4198 4198 ``--localssh`` will start an SSH server against the current repository
4199 4199 and connect to that. By default, the connection will perform a handshake
4200 4200 and establish an appropriate peer instance.
4201 4201
4202 4202 ``--peer`` can be used to bypass the handshake protocol and construct a
4203 4203 peer instance using the specified class type. Valid values are ``raw``,
4204 4204 ``http2``, ``ssh1``, and ``ssh2``. ``raw`` instances only allow sending
4205 4205 raw data payloads and don't support higher-level command actions.
4206 4206
4207 4207 ``--noreadstderr`` can be used to disable automatic reading from stderr
4208 4208 of the peer (for SSH connections only). Disabling automatic reading of
4209 4209 stderr is useful for making output more deterministic.
4210 4210
4211 4211 Commands are issued via a mini language which is specified via stdin.
4212 4212 The language consists of individual actions to perform. An action is
4213 4213 defined by a block. A block is defined as a line with no leading
4214 4214 space followed by 0 or more lines with leading space. Blocks are
4215 4215 effectively a high-level command with additional metadata.
4216 4216
4217 4217 Lines beginning with ``#`` are ignored.
4218 4218
4219 4219 The following sections denote available actions.
4220 4220
4221 4221 raw
4222 4222 ---
4223 4223
4224 4224 Send raw data to the server.
4225 4225
4226 4226 The block payload contains the raw data to send as one atomic send
4227 4227 operation. The data may not actually be delivered in a single system
4228 4228 call: it depends on the abilities of the transport being used.
4229 4229
4230 4230 Each line in the block is de-indented and concatenated. Then, that
4231 4231 value is evaluated as a Python b'' literal. This allows the use of
4232 4232 backslash escaping, etc.
4233 4233
4234 4234 raw+
4235 4235 ----
4236 4236
4237 4237 Behaves like ``raw`` except flushes output afterwards.
4238 4238
4239 4239 command <X>
4240 4240 -----------
4241 4241
4242 4242 Send a request to run a named command, whose name follows the ``command``
4243 4243 string.
4244 4244
4245 4245 Arguments to the command are defined as lines in this block. The format of
4246 4246 each line is ``<key> <value>``. e.g.::
4247 4247
4248 4248 command listkeys
4249 4249 namespace bookmarks
4250 4250
4251 4251 If the value begins with ``eval:``, it will be interpreted as a Python
4252 4252 literal expression. Otherwise values are interpreted as Python b'' literals.
4253 4253 This allows sending complex types and encoding special byte sequences via
4254 4254 backslash escaping.
4255 4255
4256 4256 The following arguments have special meaning:
4257 4257
4258 4258 ``PUSHFILE``
4259 4259 When defined, the *push* mechanism of the peer will be used instead
4260 4260 of the static request-response mechanism and the content of the
4261 4261 file specified in the value of this argument will be sent as the
4262 4262 command payload.
4263 4263
4264 4264 This can be used to submit a local bundle file to the remote.
4265 4265
4266 4266 batchbegin
4267 4267 ----------
4268 4268
4269 4269 Instruct the peer to begin a batched send.
4270 4270
4271 4271 All ``command`` blocks are queued for execution until the next
4272 4272 ``batchsubmit`` block.
4273 4273
4274 4274 batchsubmit
4275 4275 -----------
4276 4276
4277 4277 Submit previously queued ``command`` blocks as a batch request.
4278 4278
4279 4279 This action MUST be paired with a ``batchbegin`` action.
4280 4280
4281 4281 httprequest <method> <path>
4282 4282 ---------------------------
4283 4283
4284 4284 (HTTP peer only)
4285 4285
4286 4286 Send an HTTP request to the peer.
4287 4287
4288 4288 The HTTP request line follows the ``httprequest`` action. e.g. ``GET /foo``.
4289 4289
4290 4290 Arguments of the form ``<key>: <value>`` are interpreted as HTTP request
4291 4291 headers to add to the request. e.g. ``Accept: foo``.
4292 4292
4293 4293 The following arguments are special:
4294 4294
4295 4295 ``BODYFILE``
4296 4296 The content of the file defined as the value to this argument will be
4297 4297 transferred verbatim as the HTTP request body.
4298 4298
4299 4299 ``frame <type> <flags> <payload>``
4300 4300 Send a unified protocol frame as part of the request body.
4301 4301
4302 4302 All frames will be collected and sent as the body to the HTTP
4303 4303 request.
4304 4304
4305 4305 close
4306 4306 -----
4307 4307
4308 4308 Close the connection to the server.
4309 4309
4310 4310 flush
4311 4311 -----
4312 4312
4313 4313 Flush data written to the server.
4314 4314
4315 4315 readavailable
4316 4316 -------------
4317 4317
4318 4318 Close the write end of the connection and read all available data from
4319 4319 the server.
4320 4320
4321 4321 If the connection to the server encompasses multiple pipes, we poll both
4322 4322 pipes and read available data.
4323 4323
4324 4324 readline
4325 4325 --------
4326 4326
4327 4327 Read a line of output from the server. If there are multiple output
4328 4328 pipes, reads only the main pipe.
4329 4329
4330 4330 ereadline
4331 4331 ---------
4332 4332
4333 4333 Like ``readline``, but read from the stderr pipe, if available.
4334 4334
4335 4335 read <X>
4336 4336 --------
4337 4337
4338 4338 ``read()`` N bytes from the server's main output pipe.
4339 4339
4340 4340 eread <X>
4341 4341 ---------
4342 4342
4343 4343 ``read()`` N bytes from the server's stderr pipe, if available.
4344 4344
4345 4345 Specifying Unified Frame-Based Protocol Frames
4346 4346 ----------------------------------------------
4347 4347
4348 4348 It is possible to emit a *Unified Frame-Based Protocol* by using special
4349 4349 syntax.
4350 4350
4351 4351 A frame is composed as a type, flags, and payload. These can be parsed
4352 4352 from a string of the form:
4353 4353
4354 4354 <request-id> <stream-id> <stream-flags> <type> <flags> <payload>
4355 4355
4356 4356 ``request-id`` and ``stream-id`` are integers defining the request and
4357 4357 stream identifiers.
4358 4358
4359 4359 ``type`` can be an integer value for the frame type or the string name
4360 4360 of the type. The strings are defined in ``wireprotoframing.py``. e.g.
4361 4361 ``command-name``.
4362 4362
4363 4363 ``stream-flags`` and ``flags`` are a ``|`` delimited list of flag
4364 4364 components. Each component (and there can be just one) can be an integer
4365 4365 or a flag name for stream flags or frame flags, respectively. Values are
4366 4366 resolved to integers and then bitwise OR'd together.
4367 4367
4368 4368 ``payload`` represents the raw frame payload. If it begins with
4369 4369 ``cbor:``, the following string is evaluated as Python code and the
4370 4370 resulting object is fed into a CBOR encoder. Otherwise it is interpreted
4371 4371 as a Python byte string literal.
4372 4372 """
4373 4373 opts = pycompat.byteskwargs(opts)
4374 4374
4375 4375 if opts[b'localssh'] and not repo:
4376 4376 raise error.Abort(_(b'--localssh requires a repository'))
4377 4377
4378 4378 if opts[b'peer'] and opts[b'peer'] not in (
4379 4379 b'raw',
4380 4380 b'http2',
4381 4381 b'ssh1',
4382 4382 b'ssh2',
4383 4383 ):
4384 4384 raise error.Abort(
4385 4385 _(b'invalid value for --peer'),
4386 4386 hint=_(b'valid values are "raw", "ssh1", and "ssh2"'),
4387 4387 )
4388 4388
4389 4389 if path and opts[b'localssh']:
4390 4390 raise error.Abort(_(b'cannot specify --localssh with an explicit path'))
4391 4391
4392 4392 if ui.interactive():
4393 4393 ui.write(_(b'(waiting for commands on stdin)\n'))
4394 4394
4395 4395 blocks = list(_parsewirelangblocks(ui.fin))
4396 4396
4397 4397 proc = None
4398 4398 stdin = None
4399 4399 stdout = None
4400 4400 stderr = None
4401 4401 opener = None
4402 4402
4403 4403 if opts[b'localssh']:
4404 4404 # We start the SSH server in its own process so there is process
4405 4405 # separation. This prevents a whole class of potential bugs around
4406 4406 # shared state from interfering with server operation.
4407 4407 args = procutil.hgcmd() + [
4408 4408 b'-R',
4409 4409 repo.root,
4410 4410 b'debugserve',
4411 4411 b'--sshstdio',
4412 4412 ]
4413 4413 proc = subprocess.Popen(
4414 4414 pycompat.rapply(procutil.tonativestr, args),
4415 4415 stdin=subprocess.PIPE,
4416 4416 stdout=subprocess.PIPE,
4417 4417 stderr=subprocess.PIPE,
4418 4418 bufsize=0,
4419 4419 )
4420 4420
4421 4421 stdin = proc.stdin
4422 4422 stdout = proc.stdout
4423 4423 stderr = proc.stderr
4424 4424
4425 4425 # We turn the pipes into observers so we can log I/O.
4426 4426 if ui.verbose or opts[b'peer'] == b'raw':
4427 4427 stdin = util.makeloggingfileobject(
4428 4428 ui, proc.stdin, b'i', logdata=True
4429 4429 )
4430 4430 stdout = util.makeloggingfileobject(
4431 4431 ui, proc.stdout, b'o', logdata=True
4432 4432 )
4433 4433 stderr = util.makeloggingfileobject(
4434 4434 ui, proc.stderr, b'e', logdata=True
4435 4435 )
4436 4436
4437 4437 # --localssh also implies the peer connection settings.
4438 4438
4439 4439 url = b'ssh://localserver'
4440 4440 autoreadstderr = not opts[b'noreadstderr']
4441 4441
4442 4442 if opts[b'peer'] == b'ssh1':
4443 4443 ui.write(_(b'creating ssh peer for wire protocol version 1\n'))
4444 4444 peer = sshpeer.sshv1peer(
4445 4445 ui,
4446 4446 url,
4447 4447 proc,
4448 4448 stdin,
4449 4449 stdout,
4450 4450 stderr,
4451 4451 None,
4452 4452 autoreadstderr=autoreadstderr,
4453 4453 )
4454 4454 elif opts[b'peer'] == b'ssh2':
4455 4455 ui.write(_(b'creating ssh peer for wire protocol version 2\n'))
4456 4456 peer = sshpeer.sshv2peer(
4457 4457 ui,
4458 4458 url,
4459 4459 proc,
4460 4460 stdin,
4461 4461 stdout,
4462 4462 stderr,
4463 4463 None,
4464 4464 autoreadstderr=autoreadstderr,
4465 4465 )
4466 4466 elif opts[b'peer'] == b'raw':
4467 4467 ui.write(_(b'using raw connection to peer\n'))
4468 4468 peer = None
4469 4469 else:
4470 4470 ui.write(_(b'creating ssh peer from handshake results\n'))
4471 4471 peer = sshpeer.makepeer(
4472 4472 ui,
4473 4473 url,
4474 4474 proc,
4475 4475 stdin,
4476 4476 stdout,
4477 4477 stderr,
4478 4478 autoreadstderr=autoreadstderr,
4479 4479 )
4480 4480
4481 4481 elif path:
4482 4482 # We bypass hg.peer() so we can proxy the sockets.
4483 4483 # TODO consider not doing this because we skip
4484 4484 # ``hg.wirepeersetupfuncs`` and potentially other useful functionality.
4485 4485 u = util.url(path)
4486 4486 if u.scheme != b'http':
4487 4487 raise error.Abort(_(b'only http:// paths are currently supported'))
4488 4488
4489 4489 url, authinfo = u.authinfo()
4490 4490 openerargs = {
4491 4491 'useragent': b'Mercurial debugwireproto',
4492 4492 }
4493 4493
4494 4494 # Turn pipes/sockets into observers so we can log I/O.
4495 4495 if ui.verbose:
4496 4496 openerargs.update(
4497 4497 {
4498 4498 'loggingfh': ui,
4499 4499 'loggingname': b's',
4500 4500 'loggingopts': {
4501 4501 'logdata': True,
4502 4502 'logdataapis': False,
4503 4503 },
4504 4504 }
4505 4505 )
4506 4506
4507 4507 if ui.debugflag:
4508 4508 openerargs['loggingopts']['logdataapis'] = True
4509 4509
4510 4510 # Don't send default headers when in raw mode. This allows us to
4511 4511 # bypass most of the behavior of our URL handling code so we can
4512 4512 # have near complete control over what's sent on the wire.
4513 4513 if opts[b'peer'] == b'raw':
4514 4514 openerargs['sendaccept'] = False
4515 4515
4516 4516 opener = urlmod.opener(ui, authinfo, **openerargs)
4517 4517
4518 4518 if opts[b'peer'] == b'http2':
4519 4519 ui.write(_(b'creating http peer for wire protocol version 2\n'))
4520 4520 # We go through makepeer() because we need an API descriptor for
4521 4521 # the peer instance to be useful.
4522 4522 with ui.configoverride(
4523 4523 {(b'experimental', b'httppeer.advertise-v2'): True}
4524 4524 ):
4525 4525 if opts[b'nologhandshake']:
4526 4526 ui.pushbuffer()
4527 4527
4528 4528 peer = httppeer.makepeer(ui, path, opener=opener)
4529 4529
4530 4530 if opts[b'nologhandshake']:
4531 4531 ui.popbuffer()
4532 4532
4533 4533 if not isinstance(peer, httppeer.httpv2peer):
4534 4534 raise error.Abort(
4535 4535 _(
4536 4536 b'could not instantiate HTTP peer for '
4537 4537 b'wire protocol version 2'
4538 4538 ),
4539 4539 hint=_(
4540 4540 b'the server may not have the feature '
4541 4541 b'enabled or is not allowing this '
4542 4542 b'client version'
4543 4543 ),
4544 4544 )
4545 4545
4546 4546 elif opts[b'peer'] == b'raw':
4547 4547 ui.write(_(b'using raw connection to peer\n'))
4548 4548 peer = None
4549 4549 elif opts[b'peer']:
4550 4550 raise error.Abort(
4551 4551 _(b'--peer %s not supported with HTTP peers') % opts[b'peer']
4552 4552 )
4553 4553 else:
4554 4554 peer = httppeer.makepeer(ui, path, opener=opener)
4555 4555
4556 4556 # We /could/ populate stdin/stdout with sock.makefile()...
4557 4557 else:
4558 4558 raise error.Abort(_(b'unsupported connection configuration'))
4559 4559
4560 4560 batchedcommands = None
4561 4561
4562 4562 # Now perform actions based on the parsed wire language instructions.
4563 4563 for action, lines in blocks:
4564 4564 if action in (b'raw', b'raw+'):
4565 4565 if not stdin:
4566 4566 raise error.Abort(_(b'cannot call raw/raw+ on this peer'))
4567 4567
4568 4568 # Concatenate the data together.
4569 4569 data = b''.join(l.lstrip() for l in lines)
4570 4570 data = stringutil.unescapestr(data)
4571 4571 stdin.write(data)
4572 4572
4573 4573 if action == b'raw+':
4574 4574 stdin.flush()
4575 4575 elif action == b'flush':
4576 4576 if not stdin:
4577 4577 raise error.Abort(_(b'cannot call flush on this peer'))
4578 4578 stdin.flush()
4579 4579 elif action.startswith(b'command'):
4580 4580 if not peer:
4581 4581 raise error.Abort(
4582 4582 _(
4583 4583 b'cannot send commands unless peer instance '
4584 4584 b'is available'
4585 4585 )
4586 4586 )
4587 4587
4588 4588 command = action.split(b' ', 1)[1]
4589 4589
4590 4590 args = {}
4591 4591 for line in lines:
4592 4592 # We need to allow empty values.
4593 4593 fields = line.lstrip().split(b' ', 1)
4594 4594 if len(fields) == 1:
4595 4595 key = fields[0]
4596 4596 value = b''
4597 4597 else:
4598 4598 key, value = fields
4599 4599
4600 4600 if value.startswith(b'eval:'):
4601 4601 value = stringutil.evalpythonliteral(value[5:])
4602 4602 else:
4603 4603 value = stringutil.unescapestr(value)
4604 4604
4605 4605 args[key] = value
4606 4606
4607 4607 if batchedcommands is not None:
4608 4608 batchedcommands.append((command, args))
4609 4609 continue
4610 4610
4611 4611 ui.status(_(b'sending %s command\n') % command)
4612 4612
4613 4613 if b'PUSHFILE' in args:
4614 4614 with open(args[b'PUSHFILE'], 'rb') as fh:
4615 4615 del args[b'PUSHFILE']
4616 4616 res, output = peer._callpush(
4617 4617 command, fh, **pycompat.strkwargs(args)
4618 4618 )
4619 4619 ui.status(_(b'result: %s\n') % stringutil.escapestr(res))
4620 4620 ui.status(
4621 4621 _(b'remote output: %s\n') % stringutil.escapestr(output)
4622 4622 )
4623 4623 else:
4624 4624 with peer.commandexecutor() as e:
4625 4625 res = e.callcommand(command, args).result()
4626 4626
4627 4627 if isinstance(res, wireprotov2peer.commandresponse):
4628 4628 val = res.objects()
4629 4629 ui.status(
4630 4630 _(b'response: %s\n')
4631 4631 % stringutil.pprint(val, bprefix=True, indent=2)
4632 4632 )
4633 4633 else:
4634 4634 ui.status(
4635 4635 _(b'response: %s\n')
4636 4636 % stringutil.pprint(res, bprefix=True, indent=2)
4637 4637 )
4638 4638
4639 4639 elif action == b'batchbegin':
4640 4640 if batchedcommands is not None:
4641 4641 raise error.Abort(_(b'nested batchbegin not allowed'))
4642 4642
4643 4643 batchedcommands = []
4644 4644 elif action == b'batchsubmit':
4645 4645 # There is a batching API we could go through. But it would be
4646 4646 # difficult to normalize requests into function calls. It is easier
4647 4647 # to bypass this layer and normalize to commands + args.
4648 4648 ui.status(
4649 4649 _(b'sending batch with %d sub-commands\n')
4650 4650 % len(batchedcommands)
4651 4651 )
4652 4652 assert peer is not None
4653 4653 for i, chunk in enumerate(peer._submitbatch(batchedcommands)):
4654 4654 ui.status(
4655 4655 _(b'response #%d: %s\n') % (i, stringutil.escapestr(chunk))
4656 4656 )
4657 4657
4658 4658 batchedcommands = None
4659 4659
4660 4660 elif action.startswith(b'httprequest '):
4661 4661 if not opener:
4662 4662 raise error.Abort(
4663 4663 _(b'cannot use httprequest without an HTTP peer')
4664 4664 )
4665 4665
4666 4666 request = action.split(b' ', 2)
4667 4667 if len(request) != 3:
4668 4668 raise error.Abort(
4669 4669 _(
4670 4670 b'invalid httprequest: expected format is '
4671 4671 b'"httprequest <method> <path>'
4672 4672 )
4673 4673 )
4674 4674
4675 4675 method, httppath = request[1:]
4676 4676 headers = {}
4677 4677 body = None
4678 4678 frames = []
4679 4679 for line in lines:
4680 4680 line = line.lstrip()
4681 4681 m = re.match(b'^([a-zA-Z0-9_-]+): (.*)$', line)
4682 4682 if m:
4683 4683 # Headers need to use native strings.
4684 4684 key = pycompat.strurl(m.group(1))
4685 4685 value = pycompat.strurl(m.group(2))
4686 4686 headers[key] = value
4687 4687 continue
4688 4688
4689 4689 if line.startswith(b'BODYFILE '):
4690 4690 with open(line.split(b' ', 1), b'rb') as fh:
4691 4691 body = fh.read()
4692 4692 elif line.startswith(b'frame '):
4693 4693 frame = wireprotoframing.makeframefromhumanstring(
4694 4694 line[len(b'frame ') :]
4695 4695 )
4696 4696
4697 4697 frames.append(frame)
4698 4698 else:
4699 4699 raise error.Abort(
4700 4700 _(b'unknown argument to httprequest: %s') % line
4701 4701 )
4702 4702
4703 4703 url = path + httppath
4704 4704
4705 4705 if frames:
4706 4706 body = b''.join(bytes(f) for f in frames)
4707 4707
4708 4708 req = urlmod.urlreq.request(pycompat.strurl(url), body, headers)
4709 4709
4710 4710 # urllib.Request insists on using has_data() as a proxy for
4711 4711 # determining the request method. Override that to use our
4712 4712 # explicitly requested method.
4713 4713 req.get_method = lambda: pycompat.sysstr(method)
4714 4714
4715 4715 try:
4716 4716 res = opener.open(req)
4717 4717 body = res.read()
4718 4718 except util.urlerr.urlerror as e:
4719 4719 # read() method must be called, but only exists in Python 2
4720 4720 getattr(e, 'read', lambda: None)()
4721 4721 continue
4722 4722
4723 4723 ct = res.headers.get('Content-Type')
4724 4724 if ct == 'application/mercurial-cbor':
4725 4725 ui.write(
4726 4726 _(b'cbor> %s\n')
4727 4727 % stringutil.pprint(
4728 4728 cborutil.decodeall(body), bprefix=True, indent=2
4729 4729 )
4730 4730 )
4731 4731
4732 4732 elif action == b'close':
4733 4733 assert peer is not None
4734 4734 peer.close()
4735 4735 elif action == b'readavailable':
4736 4736 if not stdout or not stderr:
4737 4737 raise error.Abort(
4738 4738 _(b'readavailable not available on this peer')
4739 4739 )
4740 4740
4741 4741 stdin.close()
4742 4742 stdout.read()
4743 4743 stderr.read()
4744 4744
4745 4745 elif action == b'readline':
4746 4746 if not stdout:
4747 4747 raise error.Abort(_(b'readline not available on this peer'))
4748 4748 stdout.readline()
4749 4749 elif action == b'ereadline':
4750 4750 if not stderr:
4751 4751 raise error.Abort(_(b'ereadline not available on this peer'))
4752 4752 stderr.readline()
4753 4753 elif action.startswith(b'read '):
4754 4754 count = int(action.split(b' ', 1)[1])
4755 4755 if not stdout:
4756 4756 raise error.Abort(_(b'read not available on this peer'))
4757 4757 stdout.read(count)
4758 4758 elif action.startswith(b'eread '):
4759 4759 count = int(action.split(b' ', 1)[1])
4760 4760 if not stderr:
4761 4761 raise error.Abort(_(b'eread not available on this peer'))
4762 4762 stderr.read(count)
4763 4763 else:
4764 4764 raise error.Abort(_(b'unknown action: %s') % action)
4765 4765
4766 4766 if batchedcommands is not None:
4767 4767 raise error.Abort(_(b'unclosed "batchbegin" request'))
4768 4768
4769 4769 if peer:
4770 4770 peer.close()
4771 4771
4772 4772 if proc:
4773 4773 proc.kill()
@@ -1,2756 +1,2775 b''
1 1 # exchange.py - utility to exchange data between repos.
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 weakref
12 12
13 13 from .i18n import _
14 14 from .node import (
15 15 hex,
16 16 nullid,
17 17 nullrev,
18 18 )
19 19 from . import (
20 20 bookmarks as bookmod,
21 21 bundle2,
22 22 bundlecaches,
23 23 changegroup,
24 24 discovery,
25 25 error,
26 26 exchangev2,
27 27 lock as lockmod,
28 28 logexchange,
29 29 narrowspec,
30 30 obsolete,
31 31 obsutil,
32 32 phases,
33 33 pushkey,
34 34 pycompat,
35 35 requirements,
36 36 scmutil,
37 37 streamclone,
38 38 url as urlmod,
39 39 util,
40 40 wireprototypes,
41 41 )
42 42 from .utils import (
43 43 hashutil,
44 44 stringutil,
45 45 )
46 46
47 47 urlerr = util.urlerr
48 48 urlreq = util.urlreq
49 49
50 50 _NARROWACL_SECTION = b'narrowacl'
51 51
52 52
53 53 def readbundle(ui, fh, fname, vfs=None):
54 54 header = changegroup.readexactly(fh, 4)
55 55
56 56 alg = None
57 57 if not fname:
58 58 fname = b"stream"
59 59 if not header.startswith(b'HG') and header.startswith(b'\0'):
60 60 fh = changegroup.headerlessfixup(fh, header)
61 61 header = b"HG10"
62 62 alg = b'UN'
63 63 elif vfs:
64 64 fname = vfs.join(fname)
65 65
66 66 magic, version = header[0:2], header[2:4]
67 67
68 68 if magic != b'HG':
69 69 raise error.Abort(_(b'%s: not a Mercurial bundle') % fname)
70 70 if version == b'10':
71 71 if alg is None:
72 72 alg = changegroup.readexactly(fh, 2)
73 73 return changegroup.cg1unpacker(fh, alg)
74 74 elif version.startswith(b'2'):
75 75 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
76 76 elif version == b'S1':
77 77 return streamclone.streamcloneapplier(fh)
78 78 else:
79 79 raise error.Abort(
80 80 _(b'%s: unknown bundle version %s') % (fname, version)
81 81 )
82 82
83 83
84 84 def getbundlespec(ui, fh):
85 85 """Infer the bundlespec from a bundle file handle.
86 86
87 87 The input file handle is seeked and the original seek position is not
88 88 restored.
89 89 """
90 90
91 91 def speccompression(alg):
92 92 try:
93 93 return util.compengines.forbundletype(alg).bundletype()[0]
94 94 except KeyError:
95 95 return None
96 96
97 97 b = readbundle(ui, fh, None)
98 98 if isinstance(b, changegroup.cg1unpacker):
99 99 alg = b._type
100 100 if alg == b'_truncatedBZ':
101 101 alg = b'BZ'
102 102 comp = speccompression(alg)
103 103 if not comp:
104 104 raise error.Abort(_(b'unknown compression algorithm: %s') % alg)
105 105 return b'%s-v1' % comp
106 106 elif isinstance(b, bundle2.unbundle20):
107 107 if b'Compression' in b.params:
108 108 comp = speccompression(b.params[b'Compression'])
109 109 if not comp:
110 110 raise error.Abort(
111 111 _(b'unknown compression algorithm: %s') % comp
112 112 )
113 113 else:
114 114 comp = b'none'
115 115
116 116 version = None
117 117 for part in b.iterparts():
118 118 if part.type == b'changegroup':
119 119 version = part.params[b'version']
120 120 if version in (b'01', b'02'):
121 121 version = b'v2'
122 122 else:
123 123 raise error.Abort(
124 124 _(
125 125 b'changegroup version %s does not have '
126 126 b'a known bundlespec'
127 127 )
128 128 % version,
129 129 hint=_(b'try upgrading your Mercurial client'),
130 130 )
131 131 elif part.type == b'stream2' and version is None:
132 132 # A stream2 part requires to be part of a v2 bundle
133 133 requirements = urlreq.unquote(part.params[b'requirements'])
134 134 splitted = requirements.split()
135 135 params = bundle2._formatrequirementsparams(splitted)
136 136 return b'none-v2;stream=v2;%s' % params
137 137
138 138 if not version:
139 139 raise error.Abort(
140 140 _(b'could not identify changegroup version in bundle')
141 141 )
142 142
143 143 return b'%s-%s' % (comp, version)
144 144 elif isinstance(b, streamclone.streamcloneapplier):
145 145 requirements = streamclone.readbundle1header(fh)[2]
146 146 formatted = bundle2._formatrequirementsparams(requirements)
147 147 return b'none-packed1;%s' % formatted
148 148 else:
149 149 raise error.Abort(_(b'unknown bundle type: %s') % b)
150 150
151 151
152 152 def _computeoutgoing(repo, heads, common):
153 153 """Computes which revs are outgoing given a set of common
154 154 and a set of heads.
155 155
156 156 This is a separate function so extensions can have access to
157 157 the logic.
158 158
159 159 Returns a discovery.outgoing object.
160 160 """
161 161 cl = repo.changelog
162 162 if common:
163 163 hasnode = cl.hasnode
164 164 common = [n for n in common if hasnode(n)]
165 165 else:
166 166 common = [nullid]
167 167 if not heads:
168 168 heads = cl.heads()
169 169 return discovery.outgoing(repo, common, heads)
170 170
171 171
172 172 def _checkpublish(pushop):
173 173 repo = pushop.repo
174 174 ui = repo.ui
175 175 behavior = ui.config(b'experimental', b'auto-publish')
176 176 if pushop.publish or behavior not in (b'warn', b'confirm', b'abort'):
177 177 return
178 178 remotephases = listkeys(pushop.remote, b'phases')
179 179 if not remotephases.get(b'publishing', False):
180 180 return
181 181
182 182 if pushop.revs is None:
183 183 published = repo.filtered(b'served').revs(b'not public()')
184 184 else:
185 185 published = repo.revs(b'::%ln - public()', pushop.revs)
186 186 if published:
187 187 if behavior == b'warn':
188 188 ui.warn(
189 189 _(b'%i changesets about to be published\n') % len(published)
190 190 )
191 191 elif behavior == b'confirm':
192 192 if ui.promptchoice(
193 193 _(b'push and publish %i changesets (yn)?$$ &Yes $$ &No')
194 194 % len(published)
195 195 ):
196 196 raise error.CanceledError(_(b'user quit'))
197 197 elif behavior == b'abort':
198 198 msg = _(b'push would publish %i changesets') % len(published)
199 199 hint = _(
200 200 b"use --publish or adjust 'experimental.auto-publish'"
201 201 b" config"
202 202 )
203 203 raise error.Abort(msg, hint=hint)
204 204
205 205
206 206 def _forcebundle1(op):
207 207 """return true if a pull/push must use bundle1
208 208
209 209 This function is used to allow testing of the older bundle version"""
210 210 ui = op.repo.ui
211 211 # The goal is this config is to allow developer to choose the bundle
212 212 # version used during exchanged. This is especially handy during test.
213 213 # Value is a list of bundle version to be picked from, highest version
214 214 # should be used.
215 215 #
216 216 # developer config: devel.legacy.exchange
217 217 exchange = ui.configlist(b'devel', b'legacy.exchange')
218 218 forcebundle1 = b'bundle2' not in exchange and b'bundle1' in exchange
219 219 return forcebundle1 or not op.remote.capable(b'bundle2')
220 220
221 221
222 222 class pushoperation(object):
223 223 """A object that represent a single push operation
224 224
225 225 Its purpose is to carry push related state and very common operations.
226 226
227 227 A new pushoperation should be created at the beginning of each push and
228 228 discarded afterward.
229 229 """
230 230
231 231 def __init__(
232 232 self,
233 233 repo,
234 234 remote,
235 235 force=False,
236 236 revs=None,
237 237 newbranch=False,
238 238 bookmarks=(),
239 239 publish=False,
240 240 pushvars=None,
241 241 ):
242 242 # repo we push from
243 243 self.repo = repo
244 244 self.ui = repo.ui
245 245 # repo we push to
246 246 self.remote = remote
247 247 # force option provided
248 248 self.force = force
249 249 # revs to be pushed (None is "all")
250 250 self.revs = revs
251 251 # bookmark explicitly pushed
252 252 self.bookmarks = bookmarks
253 253 # allow push of new branch
254 254 self.newbranch = newbranch
255 255 # step already performed
256 256 # (used to check what steps have been already performed through bundle2)
257 257 self.stepsdone = set()
258 258 # Integer version of the changegroup push result
259 259 # - None means nothing to push
260 260 # - 0 means HTTP error
261 261 # - 1 means we pushed and remote head count is unchanged *or*
262 262 # we have outgoing changesets but refused to push
263 263 # - other values as described by addchangegroup()
264 264 self.cgresult = None
265 265 # Boolean value for the bookmark push
266 266 self.bkresult = None
267 267 # discover.outgoing object (contains common and outgoing data)
268 268 self.outgoing = None
269 269 # all remote topological heads before the push
270 270 self.remoteheads = None
271 271 # Details of the remote branch pre and post push
272 272 #
273 273 # mapping: {'branch': ([remoteheads],
274 274 # [newheads],
275 275 # [unsyncedheads],
276 276 # [discardedheads])}
277 277 # - branch: the branch name
278 278 # - remoteheads: the list of remote heads known locally
279 279 # None if the branch is new
280 280 # - newheads: the new remote heads (known locally) with outgoing pushed
281 281 # - unsyncedheads: the list of remote heads unknown locally.
282 282 # - discardedheads: the list of remote heads made obsolete by the push
283 283 self.pushbranchmap = None
284 284 # testable as a boolean indicating if any nodes are missing locally.
285 285 self.incoming = None
286 286 # summary of the remote phase situation
287 287 self.remotephases = None
288 288 # phases changes that must be pushed along side the changesets
289 289 self.outdatedphases = None
290 290 # phases changes that must be pushed if changeset push fails
291 291 self.fallbackoutdatedphases = None
292 292 # outgoing obsmarkers
293 293 self.outobsmarkers = set()
294 294 # outgoing bookmarks, list of (bm, oldnode | '', newnode | '')
295 295 self.outbookmarks = []
296 296 # transaction manager
297 297 self.trmanager = None
298 298 # map { pushkey partid -> callback handling failure}
299 299 # used to handle exception from mandatory pushkey part failure
300 300 self.pkfailcb = {}
301 301 # an iterable of pushvars or None
302 302 self.pushvars = pushvars
303 303 # publish pushed changesets
304 304 self.publish = publish
305 305
306 306 @util.propertycache
307 307 def futureheads(self):
308 308 """future remote heads if the changeset push succeeds"""
309 309 return self.outgoing.ancestorsof
310 310
311 311 @util.propertycache
312 312 def fallbackheads(self):
313 313 """future remote heads if the changeset push fails"""
314 314 if self.revs is None:
315 315 # not target to push, all common are relevant
316 316 return self.outgoing.commonheads
317 317 unfi = self.repo.unfiltered()
318 318 # I want cheads = heads(::ancestorsof and ::commonheads)
319 319 # (ancestorsof is revs with secret changeset filtered out)
320 320 #
321 321 # This can be expressed as:
322 322 # cheads = ( (ancestorsof and ::commonheads)
323 323 # + (commonheads and ::ancestorsof))"
324 324 # )
325 325 #
326 326 # while trying to push we already computed the following:
327 327 # common = (::commonheads)
328 328 # missing = ((commonheads::ancestorsof) - commonheads)
329 329 #
330 330 # We can pick:
331 331 # * ancestorsof part of common (::commonheads)
332 332 common = self.outgoing.common
333 333 rev = self.repo.changelog.index.rev
334 334 cheads = [node for node in self.revs if rev(node) in common]
335 335 # and
336 336 # * commonheads parents on missing
337 337 revset = unfi.set(
338 338 b'%ln and parents(roots(%ln))',
339 339 self.outgoing.commonheads,
340 340 self.outgoing.missing,
341 341 )
342 342 cheads.extend(c.node() for c in revset)
343 343 return cheads
344 344
345 345 @property
346 346 def commonheads(self):
347 347 """set of all common heads after changeset bundle push"""
348 348 if self.cgresult:
349 349 return self.futureheads
350 350 else:
351 351 return self.fallbackheads
352 352
353 353
354 354 # mapping of message used when pushing bookmark
355 355 bookmsgmap = {
356 356 b'update': (
357 357 _(b"updating bookmark %s\n"),
358 358 _(b'updating bookmark %s failed\n'),
359 359 ),
360 360 b'export': (
361 361 _(b"exporting bookmark %s\n"),
362 362 _(b'exporting bookmark %s failed\n'),
363 363 ),
364 364 b'delete': (
365 365 _(b"deleting remote bookmark %s\n"),
366 366 _(b'deleting remote bookmark %s failed\n'),
367 367 ),
368 368 }
369 369
370 370
371 371 def push(
372 372 repo,
373 373 remote,
374 374 force=False,
375 375 revs=None,
376 376 newbranch=False,
377 377 bookmarks=(),
378 378 publish=False,
379 379 opargs=None,
380 380 ):
381 381 """Push outgoing changesets (limited by revs) from a local
382 382 repository to remote. Return an integer:
383 383 - None means nothing to push
384 384 - 0 means HTTP error
385 385 - 1 means we pushed and remote head count is unchanged *or*
386 386 we have outgoing changesets but refused to push
387 387 - other values as described by addchangegroup()
388 388 """
389 389 if opargs is None:
390 390 opargs = {}
391 391 pushop = pushoperation(
392 392 repo,
393 393 remote,
394 394 force,
395 395 revs,
396 396 newbranch,
397 397 bookmarks,
398 398 publish,
399 399 **pycompat.strkwargs(opargs)
400 400 )
401 401 if pushop.remote.local():
402 402 missing = (
403 403 set(pushop.repo.requirements) - pushop.remote.local().supported
404 404 )
405 405 if missing:
406 406 msg = _(
407 407 b"required features are not"
408 408 b" supported in the destination:"
409 409 b" %s"
410 410 ) % (b', '.join(sorted(missing)))
411 411 raise error.Abort(msg)
412 412
413 413 if not pushop.remote.canpush():
414 414 raise error.Abort(_(b"destination does not support push"))
415 415
416 416 if not pushop.remote.capable(b'unbundle'):
417 417 raise error.Abort(
418 418 _(
419 419 b'cannot push: destination does not support the '
420 420 b'unbundle wire protocol command'
421 421 )
422 422 )
423 423
424 424 # get lock as we might write phase data
425 425 wlock = lock = None
426 426 try:
427 427 # bundle2 push may receive a reply bundle touching bookmarks
428 428 # requiring the wlock. Take it now to ensure proper ordering.
429 429 maypushback = pushop.ui.configbool(b'experimental', b'bundle2.pushback')
430 430 if (
431 431 (not _forcebundle1(pushop))
432 432 and maypushback
433 433 and not bookmod.bookmarksinstore(repo)
434 434 ):
435 435 wlock = pushop.repo.wlock()
436 436 lock = pushop.repo.lock()
437 437 pushop.trmanager = transactionmanager(
438 438 pushop.repo, b'push-response', pushop.remote.url()
439 439 )
440 440 except error.LockUnavailable as err:
441 441 # source repo cannot be locked.
442 442 # We do not abort the push, but just disable the local phase
443 443 # synchronisation.
444 444 msg = b'cannot lock source repository: %s\n' % stringutil.forcebytestr(
445 445 err
446 446 )
447 447 pushop.ui.debug(msg)
448 448
449 449 with wlock or util.nullcontextmanager():
450 450 with lock or util.nullcontextmanager():
451 451 with pushop.trmanager or util.nullcontextmanager():
452 452 pushop.repo.checkpush(pushop)
453 453 _checkpublish(pushop)
454 454 _pushdiscovery(pushop)
455 455 if not pushop.force:
456 456 _checksubrepostate(pushop)
457 457 if not _forcebundle1(pushop):
458 458 _pushbundle2(pushop)
459 459 _pushchangeset(pushop)
460 460 _pushsyncphase(pushop)
461 461 _pushobsolete(pushop)
462 462 _pushbookmark(pushop)
463 463
464 464 if repo.ui.configbool(b'experimental', b'remotenames'):
465 465 logexchange.pullremotenames(repo, remote)
466 466
467 467 return pushop
468 468
469 469
470 470 # list of steps to perform discovery before push
471 471 pushdiscoveryorder = []
472 472
473 473 # Mapping between step name and function
474 474 #
475 475 # This exists to help extensions wrap steps if necessary
476 476 pushdiscoverymapping = {}
477 477
478 478
479 479 def pushdiscovery(stepname):
480 480 """decorator for function performing discovery before push
481 481
482 482 The function is added to the step -> function mapping and appended to the
483 483 list of steps. Beware that decorated function will be added in order (this
484 484 may matter).
485 485
486 486 You can only use this decorator for a new step, if you want to wrap a step
487 487 from an extension, change the pushdiscovery dictionary directly."""
488 488
489 489 def dec(func):
490 490 assert stepname not in pushdiscoverymapping
491 491 pushdiscoverymapping[stepname] = func
492 492 pushdiscoveryorder.append(stepname)
493 493 return func
494 494
495 495 return dec
496 496
497 497
498 498 def _pushdiscovery(pushop):
499 499 """Run all discovery steps"""
500 500 for stepname in pushdiscoveryorder:
501 501 step = pushdiscoverymapping[stepname]
502 502 step(pushop)
503 503
504 504
505 505 def _checksubrepostate(pushop):
506 506 """Ensure all outgoing referenced subrepo revisions are present locally"""
507 507 for n in pushop.outgoing.missing:
508 508 ctx = pushop.repo[n]
509 509
510 510 if b'.hgsub' in ctx.manifest() and b'.hgsubstate' in ctx.files():
511 511 for subpath in sorted(ctx.substate):
512 512 sub = ctx.sub(subpath)
513 513 sub.verify(onpush=True)
514 514
515 515
516 516 @pushdiscovery(b'changeset')
517 517 def _pushdiscoverychangeset(pushop):
518 518 """discover the changeset that need to be pushed"""
519 519 fci = discovery.findcommonincoming
520 520 if pushop.revs:
521 521 commoninc = fci(
522 522 pushop.repo,
523 523 pushop.remote,
524 524 force=pushop.force,
525 525 ancestorsof=pushop.revs,
526 526 )
527 527 else:
528 528 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
529 529 common, inc, remoteheads = commoninc
530 530 fco = discovery.findcommonoutgoing
531 531 outgoing = fco(
532 532 pushop.repo,
533 533 pushop.remote,
534 534 onlyheads=pushop.revs,
535 535 commoninc=commoninc,
536 536 force=pushop.force,
537 537 )
538 538 pushop.outgoing = outgoing
539 539 pushop.remoteheads = remoteheads
540 540 pushop.incoming = inc
541 541
542 542
543 543 @pushdiscovery(b'phase')
544 544 def _pushdiscoveryphase(pushop):
545 545 """discover the phase that needs to be pushed
546 546
547 547 (computed for both success and failure case for changesets push)"""
548 548 outgoing = pushop.outgoing
549 549 unfi = pushop.repo.unfiltered()
550 550 remotephases = listkeys(pushop.remote, b'phases')
551 551
552 552 if (
553 553 pushop.ui.configbool(b'ui', b'_usedassubrepo')
554 554 and remotephases # server supports phases
555 555 and not pushop.outgoing.missing # no changesets to be pushed
556 556 and remotephases.get(b'publishing', False)
557 557 ):
558 558 # When:
559 559 # - this is a subrepo push
560 560 # - and remote support phase
561 561 # - and no changeset are to be pushed
562 562 # - and remote is publishing
563 563 # We may be in issue 3781 case!
564 564 # We drop the possible phase synchronisation done by
565 565 # courtesy to publish changesets possibly locally draft
566 566 # on the remote.
567 567 pushop.outdatedphases = []
568 568 pushop.fallbackoutdatedphases = []
569 569 return
570 570
571 571 pushop.remotephases = phases.remotephasessummary(
572 572 pushop.repo, pushop.fallbackheads, remotephases
573 573 )
574 574 droots = pushop.remotephases.draftroots
575 575
576 576 extracond = b''
577 577 if not pushop.remotephases.publishing:
578 578 extracond = b' and public()'
579 579 revset = b'heads((%%ln::%%ln) %s)' % extracond
580 580 # Get the list of all revs draft on remote by public here.
581 581 # XXX Beware that revset break if droots is not strictly
582 582 # XXX root we may want to ensure it is but it is costly
583 583 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
584 584 if not pushop.remotephases.publishing and pushop.publish:
585 585 future = list(
586 586 unfi.set(
587 587 b'%ln and (not public() or %ln::)', pushop.futureheads, droots
588 588 )
589 589 )
590 590 elif not outgoing.missing:
591 591 future = fallback
592 592 else:
593 593 # adds changeset we are going to push as draft
594 594 #
595 595 # should not be necessary for publishing server, but because of an
596 596 # issue fixed in xxxxx we have to do it anyway.
597 597 fdroots = list(
598 598 unfi.set(b'roots(%ln + %ln::)', outgoing.missing, droots)
599 599 )
600 600 fdroots = [f.node() for f in fdroots]
601 601 future = list(unfi.set(revset, fdroots, pushop.futureheads))
602 602 pushop.outdatedphases = future
603 603 pushop.fallbackoutdatedphases = fallback
604 604
605 605
606 606 @pushdiscovery(b'obsmarker')
607 607 def _pushdiscoveryobsmarkers(pushop):
608 608 if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt):
609 609 return
610 610
611 611 if not pushop.repo.obsstore:
612 612 return
613 613
614 614 if b'obsolete' not in listkeys(pushop.remote, b'namespaces'):
615 615 return
616 616
617 617 repo = pushop.repo
618 618 # very naive computation, that can be quite expensive on big repo.
619 619 # However: evolution is currently slow on them anyway.
620 620 nodes = (c.node() for c in repo.set(b'::%ln', pushop.futureheads))
621 621 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
622 622
623 623
624 624 @pushdiscovery(b'bookmarks')
625 625 def _pushdiscoverybookmarks(pushop):
626 626 ui = pushop.ui
627 627 repo = pushop.repo.unfiltered()
628 628 remote = pushop.remote
629 629 ui.debug(b"checking for updated bookmarks\n")
630 630 ancestors = ()
631 631 if pushop.revs:
632 632 revnums = pycompat.maplist(repo.changelog.rev, pushop.revs)
633 633 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
634 634
635 635 remotebookmark = bookmod.unhexlifybookmarks(listkeys(remote, b'bookmarks'))
636 636
637 637 explicit = {
638 638 repo._bookmarks.expandname(bookmark) for bookmark in pushop.bookmarks
639 639 }
640 640
641 641 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
642 642 return _processcompared(pushop, ancestors, explicit, remotebookmark, comp)
643 643
644 644
645 645 def _processcompared(pushop, pushed, explicit, remotebms, comp):
646 646 """take decision on bookmarks to push to the remote repo
647 647
648 648 Exists to help extensions alter this behavior.
649 649 """
650 650 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
651 651
652 652 repo = pushop.repo
653 653
654 654 for b, scid, dcid in advsrc:
655 655 if b in explicit:
656 656 explicit.remove(b)
657 657 if not pushed or repo[scid].rev() in pushed:
658 658 pushop.outbookmarks.append((b, dcid, scid))
659 659 # search added bookmark
660 660 for b, scid, dcid in addsrc:
661 661 if b in explicit:
662 662 explicit.remove(b)
663 663 if bookmod.isdivergent(b):
664 664 pushop.ui.warn(_(b'cannot push divergent bookmark %s!\n') % b)
665 665 pushop.bkresult = 2
666 666 else:
667 667 pushop.outbookmarks.append((b, b'', scid))
668 668 # search for overwritten bookmark
669 669 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
670 670 if b in explicit:
671 671 explicit.remove(b)
672 672 pushop.outbookmarks.append((b, dcid, scid))
673 673 # search for bookmark to delete
674 674 for b, scid, dcid in adddst:
675 675 if b in explicit:
676 676 explicit.remove(b)
677 677 # treat as "deleted locally"
678 678 pushop.outbookmarks.append((b, dcid, b''))
679 679 # identical bookmarks shouldn't get reported
680 680 for b, scid, dcid in same:
681 681 if b in explicit:
682 682 explicit.remove(b)
683 683
684 684 if explicit:
685 685 explicit = sorted(explicit)
686 686 # we should probably list all of them
687 687 pushop.ui.warn(
688 688 _(
689 689 b'bookmark %s does not exist on the local '
690 690 b'or remote repository!\n'
691 691 )
692 692 % explicit[0]
693 693 )
694 694 pushop.bkresult = 2
695 695
696 696 pushop.outbookmarks.sort()
697 697
698 698
699 699 def _pushcheckoutgoing(pushop):
700 700 outgoing = pushop.outgoing
701 701 unfi = pushop.repo.unfiltered()
702 702 if not outgoing.missing:
703 703 # nothing to push
704 704 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
705 705 return False
706 706 # something to push
707 707 if not pushop.force:
708 708 # if repo.obsstore == False --> no obsolete
709 709 # then, save the iteration
710 710 if unfi.obsstore:
711 711 # this message are here for 80 char limit reason
712 712 mso = _(b"push includes obsolete changeset: %s!")
713 713 mspd = _(b"push includes phase-divergent changeset: %s!")
714 714 mscd = _(b"push includes content-divergent changeset: %s!")
715 715 mst = {
716 716 b"orphan": _(b"push includes orphan changeset: %s!"),
717 717 b"phase-divergent": mspd,
718 718 b"content-divergent": mscd,
719 719 }
720 720 # If we are to push if there is at least one
721 721 # obsolete or unstable changeset in missing, at
722 722 # least one of the missinghead will be obsolete or
723 723 # unstable. So checking heads only is ok
724 724 for node in outgoing.ancestorsof:
725 725 ctx = unfi[node]
726 726 if ctx.obsolete():
727 727 raise error.Abort(mso % ctx)
728 728 elif ctx.isunstable():
729 729 # TODO print more than one instability in the abort
730 730 # message
731 731 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
732 732
733 733 discovery.checkheads(pushop)
734 734 return True
735 735
736 736
737 737 # List of names of steps to perform for an outgoing bundle2, order matters.
738 738 b2partsgenorder = []
739 739
740 740 # Mapping between step name and function
741 741 #
742 742 # This exists to help extensions wrap steps if necessary
743 743 b2partsgenmapping = {}
744 744
745 745
746 746 def b2partsgenerator(stepname, idx=None):
747 747 """decorator for function generating bundle2 part
748 748
749 749 The function is added to the step -> function mapping and appended to the
750 750 list of steps. Beware that decorated functions will be added in order
751 751 (this may matter).
752 752
753 753 You can only use this decorator for new steps, if you want to wrap a step
754 754 from an extension, attack the b2partsgenmapping dictionary directly."""
755 755
756 756 def dec(func):
757 757 assert stepname not in b2partsgenmapping
758 758 b2partsgenmapping[stepname] = func
759 759 if idx is None:
760 760 b2partsgenorder.append(stepname)
761 761 else:
762 762 b2partsgenorder.insert(idx, stepname)
763 763 return func
764 764
765 765 return dec
766 766
767 767
768 768 def _pushb2ctxcheckheads(pushop, bundler):
769 769 """Generate race condition checking parts
770 770
771 771 Exists as an independent function to aid extensions
772 772 """
773 773 # * 'force' do not check for push race,
774 774 # * if we don't push anything, there are nothing to check.
775 775 if not pushop.force and pushop.outgoing.ancestorsof:
776 776 allowunrelated = b'related' in bundler.capabilities.get(
777 777 b'checkheads', ()
778 778 )
779 779 emptyremote = pushop.pushbranchmap is None
780 780 if not allowunrelated or emptyremote:
781 781 bundler.newpart(b'check:heads', data=iter(pushop.remoteheads))
782 782 else:
783 783 affected = set()
784 784 for branch, heads in pycompat.iteritems(pushop.pushbranchmap):
785 785 remoteheads, newheads, unsyncedheads, discardedheads = heads
786 786 if remoteheads is not None:
787 787 remote = set(remoteheads)
788 788 affected |= set(discardedheads) & remote
789 789 affected |= remote - set(newheads)
790 790 if affected:
791 791 data = iter(sorted(affected))
792 792 bundler.newpart(b'check:updated-heads', data=data)
793 793
794 794
795 795 def _pushing(pushop):
796 796 """return True if we are pushing anything"""
797 797 return bool(
798 798 pushop.outgoing.missing
799 799 or pushop.outdatedphases
800 800 or pushop.outobsmarkers
801 801 or pushop.outbookmarks
802 802 )
803 803
804 804
805 805 @b2partsgenerator(b'check-bookmarks')
806 806 def _pushb2checkbookmarks(pushop, bundler):
807 807 """insert bookmark move checking"""
808 808 if not _pushing(pushop) or pushop.force:
809 809 return
810 810 b2caps = bundle2.bundle2caps(pushop.remote)
811 811 hasbookmarkcheck = b'bookmarks' in b2caps
812 812 if not (pushop.outbookmarks and hasbookmarkcheck):
813 813 return
814 814 data = []
815 815 for book, old, new in pushop.outbookmarks:
816 816 data.append((book, old))
817 817 checkdata = bookmod.binaryencode(data)
818 818 bundler.newpart(b'check:bookmarks', data=checkdata)
819 819
820 820
821 821 @b2partsgenerator(b'check-phases')
822 822 def _pushb2checkphases(pushop, bundler):
823 823 """insert phase move checking"""
824 824 if not _pushing(pushop) or pushop.force:
825 825 return
826 826 b2caps = bundle2.bundle2caps(pushop.remote)
827 827 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
828 828 if pushop.remotephases is not None and hasphaseheads:
829 829 # check that the remote phase has not changed
830 830 checks = {p: [] for p in phases.allphases}
831 831 checks[phases.public].extend(pushop.remotephases.publicheads)
832 832 checks[phases.draft].extend(pushop.remotephases.draftroots)
833 833 if any(pycompat.itervalues(checks)):
834 834 for phase in checks:
835 835 checks[phase].sort()
836 836 checkdata = phases.binaryencode(checks)
837 837 bundler.newpart(b'check:phases', data=checkdata)
838 838
839 839
840 840 @b2partsgenerator(b'changeset')
841 841 def _pushb2ctx(pushop, bundler):
842 842 """handle changegroup push through bundle2
843 843
844 844 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
845 845 """
846 846 if b'changesets' in pushop.stepsdone:
847 847 return
848 848 pushop.stepsdone.add(b'changesets')
849 849 # Send known heads to the server for race detection.
850 850 if not _pushcheckoutgoing(pushop):
851 851 return
852 852 pushop.repo.prepushoutgoinghooks(pushop)
853 853
854 854 _pushb2ctxcheckheads(pushop, bundler)
855 855
856 856 b2caps = bundle2.bundle2caps(pushop.remote)
857 857 version = b'01'
858 858 cgversions = b2caps.get(b'changegroup')
859 859 if cgversions: # 3.1 and 3.2 ship with an empty value
860 860 cgversions = [
861 861 v
862 862 for v in cgversions
863 863 if v in changegroup.supportedoutgoingversions(pushop.repo)
864 864 ]
865 865 if not cgversions:
866 866 raise error.Abort(_(b'no common changegroup version'))
867 867 version = max(cgversions)
868 868 cgstream = changegroup.makestream(
869 869 pushop.repo, pushop.outgoing, version, b'push'
870 870 )
871 871 cgpart = bundler.newpart(b'changegroup', data=cgstream)
872 872 if cgversions:
873 873 cgpart.addparam(b'version', version)
874 874 if scmutil.istreemanifest(pushop.repo):
875 875 cgpart.addparam(b'treemanifest', b'1')
876 876 if b'exp-sidedata-flag' in pushop.repo.requirements:
877 877 cgpart.addparam(b'exp-sidedata', b'1')
878 878
879 879 def handlereply(op):
880 880 """extract addchangegroup returns from server reply"""
881 881 cgreplies = op.records.getreplies(cgpart.id)
882 882 assert len(cgreplies[b'changegroup']) == 1
883 883 pushop.cgresult = cgreplies[b'changegroup'][0][b'return']
884 884
885 885 return handlereply
886 886
887 887
888 888 @b2partsgenerator(b'phase')
889 889 def _pushb2phases(pushop, bundler):
890 890 """handle phase push through bundle2"""
891 891 if b'phases' in pushop.stepsdone:
892 892 return
893 893 b2caps = bundle2.bundle2caps(pushop.remote)
894 894 ui = pushop.repo.ui
895 895
896 896 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
897 897 haspushkey = b'pushkey' in b2caps
898 898 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
899 899
900 900 if hasphaseheads and not legacyphase:
901 901 return _pushb2phaseheads(pushop, bundler)
902 902 elif haspushkey:
903 903 return _pushb2phasespushkey(pushop, bundler)
904 904
905 905
906 906 def _pushb2phaseheads(pushop, bundler):
907 907 """push phase information through a bundle2 - binary part"""
908 908 pushop.stepsdone.add(b'phases')
909 909 if pushop.outdatedphases:
910 910 updates = {p: [] for p in phases.allphases}
911 911 updates[0].extend(h.node() for h in pushop.outdatedphases)
912 912 phasedata = phases.binaryencode(updates)
913 913 bundler.newpart(b'phase-heads', data=phasedata)
914 914
915 915
916 916 def _pushb2phasespushkey(pushop, bundler):
917 917 """push phase information through a bundle2 - pushkey part"""
918 918 pushop.stepsdone.add(b'phases')
919 919 part2node = []
920 920
921 921 def handlefailure(pushop, exc):
922 922 targetid = int(exc.partid)
923 923 for partid, node in part2node:
924 924 if partid == targetid:
925 925 raise error.Abort(_(b'updating %s to public failed') % node)
926 926
927 927 enc = pushkey.encode
928 928 for newremotehead in pushop.outdatedphases:
929 929 part = bundler.newpart(b'pushkey')
930 930 part.addparam(b'namespace', enc(b'phases'))
931 931 part.addparam(b'key', enc(newremotehead.hex()))
932 932 part.addparam(b'old', enc(b'%d' % phases.draft))
933 933 part.addparam(b'new', enc(b'%d' % phases.public))
934 934 part2node.append((part.id, newremotehead))
935 935 pushop.pkfailcb[part.id] = handlefailure
936 936
937 937 def handlereply(op):
938 938 for partid, node in part2node:
939 939 partrep = op.records.getreplies(partid)
940 940 results = partrep[b'pushkey']
941 941 assert len(results) <= 1
942 942 msg = None
943 943 if not results:
944 944 msg = _(b'server ignored update of %s to public!\n') % node
945 945 elif not int(results[0][b'return']):
946 946 msg = _(b'updating %s to public failed!\n') % node
947 947 if msg is not None:
948 948 pushop.ui.warn(msg)
949 949
950 950 return handlereply
951 951
952 952
953 953 @b2partsgenerator(b'obsmarkers')
954 954 def _pushb2obsmarkers(pushop, bundler):
955 955 if b'obsmarkers' in pushop.stepsdone:
956 956 return
957 957 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
958 958 if obsolete.commonversion(remoteversions) is None:
959 959 return
960 960 pushop.stepsdone.add(b'obsmarkers')
961 961 if pushop.outobsmarkers:
962 962 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
963 963 bundle2.buildobsmarkerspart(bundler, markers)
964 964
965 965
966 966 @b2partsgenerator(b'bookmarks')
967 967 def _pushb2bookmarks(pushop, bundler):
968 968 """handle bookmark push through bundle2"""
969 969 if b'bookmarks' in pushop.stepsdone:
970 970 return
971 971 b2caps = bundle2.bundle2caps(pushop.remote)
972 972
973 973 legacy = pushop.repo.ui.configlist(b'devel', b'legacy.exchange')
974 974 legacybooks = b'bookmarks' in legacy
975 975
976 976 if not legacybooks and b'bookmarks' in b2caps:
977 977 return _pushb2bookmarkspart(pushop, bundler)
978 978 elif b'pushkey' in b2caps:
979 979 return _pushb2bookmarkspushkey(pushop, bundler)
980 980
981 981
982 982 def _bmaction(old, new):
983 983 """small utility for bookmark pushing"""
984 984 if not old:
985 985 return b'export'
986 986 elif not new:
987 987 return b'delete'
988 988 return b'update'
989 989
990 990
991 991 def _abortonsecretctx(pushop, node, b):
992 992 """abort if a given bookmark points to a secret changeset"""
993 993 if node and pushop.repo[node].phase() == phases.secret:
994 994 raise error.Abort(
995 995 _(b'cannot push bookmark %s as it points to a secret changeset') % b
996 996 )
997 997
998 998
999 999 def _pushb2bookmarkspart(pushop, bundler):
1000 1000 pushop.stepsdone.add(b'bookmarks')
1001 1001 if not pushop.outbookmarks:
1002 1002 return
1003 1003
1004 1004 allactions = []
1005 1005 data = []
1006 1006 for book, old, new in pushop.outbookmarks:
1007 1007 _abortonsecretctx(pushop, new, book)
1008 1008 data.append((book, new))
1009 1009 allactions.append((book, _bmaction(old, new)))
1010 1010 checkdata = bookmod.binaryencode(data)
1011 1011 bundler.newpart(b'bookmarks', data=checkdata)
1012 1012
1013 1013 def handlereply(op):
1014 1014 ui = pushop.ui
1015 1015 # if success
1016 1016 for book, action in allactions:
1017 1017 ui.status(bookmsgmap[action][0] % book)
1018 1018
1019 1019 return handlereply
1020 1020
1021 1021
1022 1022 def _pushb2bookmarkspushkey(pushop, bundler):
1023 1023 pushop.stepsdone.add(b'bookmarks')
1024 1024 part2book = []
1025 1025 enc = pushkey.encode
1026 1026
1027 1027 def handlefailure(pushop, exc):
1028 1028 targetid = int(exc.partid)
1029 1029 for partid, book, action in part2book:
1030 1030 if partid == targetid:
1031 1031 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
1032 1032 # we should not be called for part we did not generated
1033 1033 assert False
1034 1034
1035 1035 for book, old, new in pushop.outbookmarks:
1036 1036 _abortonsecretctx(pushop, new, book)
1037 1037 part = bundler.newpart(b'pushkey')
1038 1038 part.addparam(b'namespace', enc(b'bookmarks'))
1039 1039 part.addparam(b'key', enc(book))
1040 1040 part.addparam(b'old', enc(hex(old)))
1041 1041 part.addparam(b'new', enc(hex(new)))
1042 1042 action = b'update'
1043 1043 if not old:
1044 1044 action = b'export'
1045 1045 elif not new:
1046 1046 action = b'delete'
1047 1047 part2book.append((part.id, book, action))
1048 1048 pushop.pkfailcb[part.id] = handlefailure
1049 1049
1050 1050 def handlereply(op):
1051 1051 ui = pushop.ui
1052 1052 for partid, book, action in part2book:
1053 1053 partrep = op.records.getreplies(partid)
1054 1054 results = partrep[b'pushkey']
1055 1055 assert len(results) <= 1
1056 1056 if not results:
1057 1057 pushop.ui.warn(_(b'server ignored bookmark %s update\n') % book)
1058 1058 else:
1059 1059 ret = int(results[0][b'return'])
1060 1060 if ret:
1061 1061 ui.status(bookmsgmap[action][0] % book)
1062 1062 else:
1063 1063 ui.warn(bookmsgmap[action][1] % book)
1064 1064 if pushop.bkresult is not None:
1065 1065 pushop.bkresult = 1
1066 1066
1067 1067 return handlereply
1068 1068
1069 1069
1070 1070 @b2partsgenerator(b'pushvars', idx=0)
1071 1071 def _getbundlesendvars(pushop, bundler):
1072 1072 '''send shellvars via bundle2'''
1073 1073 pushvars = pushop.pushvars
1074 1074 if pushvars:
1075 1075 shellvars = {}
1076 1076 for raw in pushvars:
1077 1077 if b'=' not in raw:
1078 1078 msg = (
1079 1079 b"unable to parse variable '%s', should follow "
1080 1080 b"'KEY=VALUE' or 'KEY=' format"
1081 1081 )
1082 1082 raise error.Abort(msg % raw)
1083 1083 k, v = raw.split(b'=', 1)
1084 1084 shellvars[k] = v
1085 1085
1086 1086 part = bundler.newpart(b'pushvars')
1087 1087
1088 1088 for key, value in pycompat.iteritems(shellvars):
1089 1089 part.addparam(key, value, mandatory=False)
1090 1090
1091 1091
1092 1092 def _pushbundle2(pushop):
1093 1093 """push data to the remote using bundle2
1094 1094
1095 1095 The only currently supported type of data is changegroup but this will
1096 1096 evolve in the future."""
1097 1097 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
1098 1098 pushback = pushop.trmanager and pushop.ui.configbool(
1099 1099 b'experimental', b'bundle2.pushback'
1100 1100 )
1101 1101
1102 1102 # create reply capability
1103 1103 capsblob = bundle2.encodecaps(
1104 1104 bundle2.getrepocaps(pushop.repo, allowpushback=pushback, role=b'client')
1105 1105 )
1106 1106 bundler.newpart(b'replycaps', data=capsblob)
1107 1107 replyhandlers = []
1108 1108 for partgenname in b2partsgenorder:
1109 1109 partgen = b2partsgenmapping[partgenname]
1110 1110 ret = partgen(pushop, bundler)
1111 1111 if callable(ret):
1112 1112 replyhandlers.append(ret)
1113 1113 # do not push if nothing to push
1114 1114 if bundler.nbparts <= 1:
1115 1115 return
1116 1116 stream = util.chunkbuffer(bundler.getchunks())
1117 1117 try:
1118 1118 try:
1119 1119 with pushop.remote.commandexecutor() as e:
1120 1120 reply = e.callcommand(
1121 1121 b'unbundle',
1122 1122 {
1123 1123 b'bundle': stream,
1124 1124 b'heads': [b'force'],
1125 1125 b'url': pushop.remote.url(),
1126 1126 },
1127 1127 ).result()
1128 1128 except error.BundleValueError as exc:
1129 1129 raise error.Abort(_(b'missing support for %s') % exc)
1130 1130 try:
1131 1131 trgetter = None
1132 1132 if pushback:
1133 1133 trgetter = pushop.trmanager.transaction
1134 1134 op = bundle2.processbundle(pushop.repo, reply, trgetter)
1135 1135 except error.BundleValueError as exc:
1136 1136 raise error.Abort(_(b'missing support for %s') % exc)
1137 1137 except bundle2.AbortFromPart as exc:
1138 1138 pushop.ui.error(_(b'remote: %s\n') % exc)
1139 1139 if exc.hint is not None:
1140 1140 pushop.ui.error(_(b'remote: %s\n') % (b'(%s)' % exc.hint))
1141 1141 raise error.Abort(_(b'push failed on remote'))
1142 1142 except error.PushkeyFailed as exc:
1143 1143 partid = int(exc.partid)
1144 1144 if partid not in pushop.pkfailcb:
1145 1145 raise
1146 1146 pushop.pkfailcb[partid](pushop, exc)
1147 1147 for rephand in replyhandlers:
1148 1148 rephand(op)
1149 1149
1150 1150
1151 1151 def _pushchangeset(pushop):
1152 1152 """Make the actual push of changeset bundle to remote repo"""
1153 1153 if b'changesets' in pushop.stepsdone:
1154 1154 return
1155 1155 pushop.stepsdone.add(b'changesets')
1156 1156 if not _pushcheckoutgoing(pushop):
1157 1157 return
1158 1158
1159 1159 # Should have verified this in push().
1160 1160 assert pushop.remote.capable(b'unbundle')
1161 1161
1162 1162 pushop.repo.prepushoutgoinghooks(pushop)
1163 1163 outgoing = pushop.outgoing
1164 1164 # TODO: get bundlecaps from remote
1165 1165 bundlecaps = None
1166 1166 # create a changegroup from local
1167 1167 if pushop.revs is None and not (
1168 1168 outgoing.excluded or pushop.repo.changelog.filteredrevs
1169 1169 ):
1170 1170 # push everything,
1171 1171 # use the fast path, no race possible on push
1172 1172 cg = changegroup.makechangegroup(
1173 1173 pushop.repo,
1174 1174 outgoing,
1175 1175 b'01',
1176 1176 b'push',
1177 1177 fastpath=True,
1178 1178 bundlecaps=bundlecaps,
1179 1179 )
1180 1180 else:
1181 1181 cg = changegroup.makechangegroup(
1182 1182 pushop.repo, outgoing, b'01', b'push', bundlecaps=bundlecaps
1183 1183 )
1184 1184
1185 1185 # apply changegroup to remote
1186 1186 # local repo finds heads on server, finds out what
1187 1187 # revs it must push. once revs transferred, if server
1188 1188 # finds it has different heads (someone else won
1189 1189 # commit/push race), server aborts.
1190 1190 if pushop.force:
1191 1191 remoteheads = [b'force']
1192 1192 else:
1193 1193 remoteheads = pushop.remoteheads
1194 1194 # ssh: return remote's addchangegroup()
1195 1195 # http: return remote's addchangegroup() or 0 for error
1196 1196 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads, pushop.repo.url())
1197 1197
1198 1198
1199 1199 def _pushsyncphase(pushop):
1200 1200 """synchronise phase information locally and remotely"""
1201 1201 cheads = pushop.commonheads
1202 1202 # even when we don't push, exchanging phase data is useful
1203 1203 remotephases = listkeys(pushop.remote, b'phases')
1204 1204 if (
1205 1205 pushop.ui.configbool(b'ui', b'_usedassubrepo')
1206 1206 and remotephases # server supports phases
1207 1207 and pushop.cgresult is None # nothing was pushed
1208 1208 and remotephases.get(b'publishing', False)
1209 1209 ):
1210 1210 # When:
1211 1211 # - this is a subrepo push
1212 1212 # - and remote support phase
1213 1213 # - and no changeset was pushed
1214 1214 # - and remote is publishing
1215 1215 # We may be in issue 3871 case!
1216 1216 # We drop the possible phase synchronisation done by
1217 1217 # courtesy to publish changesets possibly locally draft
1218 1218 # on the remote.
1219 1219 remotephases = {b'publishing': b'True'}
1220 1220 if not remotephases: # old server or public only reply from non-publishing
1221 1221 _localphasemove(pushop, cheads)
1222 1222 # don't push any phase data as there is nothing to push
1223 1223 else:
1224 1224 ana = phases.analyzeremotephases(pushop.repo, cheads, remotephases)
1225 1225 pheads, droots = ana
1226 1226 ### Apply remote phase on local
1227 1227 if remotephases.get(b'publishing', False):
1228 1228 _localphasemove(pushop, cheads)
1229 1229 else: # publish = False
1230 1230 _localphasemove(pushop, pheads)
1231 1231 _localphasemove(pushop, cheads, phases.draft)
1232 1232 ### Apply local phase on remote
1233 1233
1234 1234 if pushop.cgresult:
1235 1235 if b'phases' in pushop.stepsdone:
1236 1236 # phases already pushed though bundle2
1237 1237 return
1238 1238 outdated = pushop.outdatedphases
1239 1239 else:
1240 1240 outdated = pushop.fallbackoutdatedphases
1241 1241
1242 1242 pushop.stepsdone.add(b'phases')
1243 1243
1244 1244 # filter heads already turned public by the push
1245 1245 outdated = [c for c in outdated if c.node() not in pheads]
1246 1246 # fallback to independent pushkey command
1247 1247 for newremotehead in outdated:
1248 1248 with pushop.remote.commandexecutor() as e:
1249 1249 r = e.callcommand(
1250 1250 b'pushkey',
1251 1251 {
1252 1252 b'namespace': b'phases',
1253 1253 b'key': newremotehead.hex(),
1254 1254 b'old': b'%d' % phases.draft,
1255 1255 b'new': b'%d' % phases.public,
1256 1256 },
1257 1257 ).result()
1258 1258
1259 1259 if not r:
1260 1260 pushop.ui.warn(
1261 1261 _(b'updating %s to public failed!\n') % newremotehead
1262 1262 )
1263 1263
1264 1264
1265 1265 def _localphasemove(pushop, nodes, phase=phases.public):
1266 1266 """move <nodes> to <phase> in the local source repo"""
1267 1267 if pushop.trmanager:
1268 1268 phases.advanceboundary(
1269 1269 pushop.repo, pushop.trmanager.transaction(), phase, nodes
1270 1270 )
1271 1271 else:
1272 1272 # repo is not locked, do not change any phases!
1273 1273 # Informs the user that phases should have been moved when
1274 1274 # applicable.
1275 1275 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1276 1276 phasestr = phases.phasenames[phase]
1277 1277 if actualmoves:
1278 1278 pushop.ui.status(
1279 1279 _(
1280 1280 b'cannot lock source repo, skipping '
1281 1281 b'local %s phase update\n'
1282 1282 )
1283 1283 % phasestr
1284 1284 )
1285 1285
1286 1286
1287 1287 def _pushobsolete(pushop):
1288 1288 """utility function to push obsolete markers to a remote"""
1289 1289 if b'obsmarkers' in pushop.stepsdone:
1290 1290 return
1291 1291 repo = pushop.repo
1292 1292 remote = pushop.remote
1293 1293 pushop.stepsdone.add(b'obsmarkers')
1294 1294 if pushop.outobsmarkers:
1295 1295 pushop.ui.debug(b'try to push obsolete markers to remote\n')
1296 1296 rslts = []
1297 1297 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1298 1298 remotedata = obsolete._pushkeyescape(markers)
1299 1299 for key in sorted(remotedata, reverse=True):
1300 1300 # reverse sort to ensure we end with dump0
1301 1301 data = remotedata[key]
1302 1302 rslts.append(remote.pushkey(b'obsolete', key, b'', data))
1303 1303 if [r for r in rslts if not r]:
1304 1304 msg = _(b'failed to push some obsolete markers!\n')
1305 1305 repo.ui.warn(msg)
1306 1306
1307 1307
1308 1308 def _pushbookmark(pushop):
1309 1309 """Update bookmark position on remote"""
1310 1310 if pushop.cgresult == 0 or b'bookmarks' in pushop.stepsdone:
1311 1311 return
1312 1312 pushop.stepsdone.add(b'bookmarks')
1313 1313 ui = pushop.ui
1314 1314 remote = pushop.remote
1315 1315
1316 1316 for b, old, new in pushop.outbookmarks:
1317 1317 action = b'update'
1318 1318 if not old:
1319 1319 action = b'export'
1320 1320 elif not new:
1321 1321 action = b'delete'
1322 1322
1323 1323 with remote.commandexecutor() as e:
1324 1324 r = e.callcommand(
1325 1325 b'pushkey',
1326 1326 {
1327 1327 b'namespace': b'bookmarks',
1328 1328 b'key': b,
1329 1329 b'old': hex(old),
1330 1330 b'new': hex(new),
1331 1331 },
1332 1332 ).result()
1333 1333
1334 1334 if r:
1335 1335 ui.status(bookmsgmap[action][0] % b)
1336 1336 else:
1337 1337 ui.warn(bookmsgmap[action][1] % b)
1338 1338 # discovery can have set the value form invalid entry
1339 1339 if pushop.bkresult is not None:
1340 1340 pushop.bkresult = 1
1341 1341
1342 1342
1343 1343 class pulloperation(object):
1344 1344 """A object that represent a single pull operation
1345 1345
1346 1346 It purpose is to carry pull related state and very common operation.
1347 1347
1348 1348 A new should be created at the beginning of each pull and discarded
1349 1349 afterward.
1350 1350 """
1351 1351
1352 1352 def __init__(
1353 1353 self,
1354 1354 repo,
1355 1355 remote,
1356 1356 heads=None,
1357 1357 force=False,
1358 1358 bookmarks=(),
1359 1359 remotebookmarks=None,
1360 1360 streamclonerequested=None,
1361 1361 includepats=None,
1362 1362 excludepats=None,
1363 1363 depth=None,
1364 1364 ):
1365 1365 # repo we pull into
1366 1366 self.repo = repo
1367 1367 # repo we pull from
1368 1368 self.remote = remote
1369 1369 # revision we try to pull (None is "all")
1370 1370 self.heads = heads
1371 1371 # bookmark pulled explicitly
1372 1372 self.explicitbookmarks = [
1373 1373 repo._bookmarks.expandname(bookmark) for bookmark in bookmarks
1374 1374 ]
1375 1375 # do we force pull?
1376 1376 self.force = force
1377 1377 # whether a streaming clone was requested
1378 1378 self.streamclonerequested = streamclonerequested
1379 1379 # transaction manager
1380 1380 self.trmanager = None
1381 1381 # set of common changeset between local and remote before pull
1382 1382 self.common = None
1383 1383 # set of pulled head
1384 1384 self.rheads = None
1385 1385 # list of missing changeset to fetch remotely
1386 1386 self.fetch = None
1387 1387 # remote bookmarks data
1388 1388 self.remotebookmarks = remotebookmarks
1389 1389 # result of changegroup pulling (used as return code by pull)
1390 1390 self.cgresult = None
1391 1391 # list of step already done
1392 1392 self.stepsdone = set()
1393 1393 # Whether we attempted a clone from pre-generated bundles.
1394 1394 self.clonebundleattempted = False
1395 1395 # Set of file patterns to include.
1396 1396 self.includepats = includepats
1397 1397 # Set of file patterns to exclude.
1398 1398 self.excludepats = excludepats
1399 1399 # Number of ancestor changesets to pull from each pulled head.
1400 1400 self.depth = depth
1401 1401
1402 1402 @util.propertycache
1403 1403 def pulledsubset(self):
1404 1404 """heads of the set of changeset target by the pull"""
1405 1405 # compute target subset
1406 1406 if self.heads is None:
1407 1407 # We pulled every thing possible
1408 1408 # sync on everything common
1409 1409 c = set(self.common)
1410 1410 ret = list(self.common)
1411 1411 for n in self.rheads:
1412 1412 if n not in c:
1413 1413 ret.append(n)
1414 1414 return ret
1415 1415 else:
1416 1416 # We pulled a specific subset
1417 1417 # sync on this subset
1418 1418 return self.heads
1419 1419
1420 1420 @util.propertycache
1421 1421 def canusebundle2(self):
1422 1422 return not _forcebundle1(self)
1423 1423
1424 1424 @util.propertycache
1425 1425 def remotebundle2caps(self):
1426 1426 return bundle2.bundle2caps(self.remote)
1427 1427
1428 1428 def gettransaction(self):
1429 1429 # deprecated; talk to trmanager directly
1430 1430 return self.trmanager.transaction()
1431 1431
1432 1432
1433 1433 class transactionmanager(util.transactional):
1434 1434 """An object to manage the life cycle of a transaction
1435 1435
1436 1436 It creates the transaction on demand and calls the appropriate hooks when
1437 1437 closing the transaction."""
1438 1438
1439 1439 def __init__(self, repo, source, url):
1440 1440 self.repo = repo
1441 1441 self.source = source
1442 1442 self.url = url
1443 1443 self._tr = None
1444 1444
1445 1445 def transaction(self):
1446 1446 """Return an open transaction object, constructing if necessary"""
1447 1447 if not self._tr:
1448 1448 trname = b'%s\n%s' % (self.source, util.hidepassword(self.url))
1449 1449 self._tr = self.repo.transaction(trname)
1450 1450 self._tr.hookargs[b'source'] = self.source
1451 1451 self._tr.hookargs[b'url'] = self.url
1452 1452 return self._tr
1453 1453
1454 1454 def close(self):
1455 1455 """close transaction if created"""
1456 1456 if self._tr is not None:
1457 1457 self._tr.close()
1458 1458
1459 1459 def release(self):
1460 1460 """release transaction if created"""
1461 1461 if self._tr is not None:
1462 1462 self._tr.release()
1463 1463
1464 1464
1465 1465 def listkeys(remote, namespace):
1466 1466 with remote.commandexecutor() as e:
1467 1467 return e.callcommand(b'listkeys', {b'namespace': namespace}).result()
1468 1468
1469 1469
1470 1470 def _fullpullbundle2(repo, pullop):
1471 1471 # The server may send a partial reply, i.e. when inlining
1472 1472 # pre-computed bundles. In that case, update the common
1473 1473 # set based on the results and pull another bundle.
1474 1474 #
1475 1475 # There are two indicators that the process is finished:
1476 1476 # - no changeset has been added, or
1477 1477 # - all remote heads are known locally.
1478 1478 # The head check must use the unfiltered view as obsoletion
1479 1479 # markers can hide heads.
1480 1480 unfi = repo.unfiltered()
1481 1481 unficl = unfi.changelog
1482 1482
1483 1483 def headsofdiff(h1, h2):
1484 1484 """Returns heads(h1 % h2)"""
1485 1485 res = unfi.set(b'heads(%ln %% %ln)', h1, h2)
1486 1486 return {ctx.node() for ctx in res}
1487 1487
1488 1488 def headsofunion(h1, h2):
1489 1489 """Returns heads((h1 + h2) - null)"""
1490 1490 res = unfi.set(b'heads((%ln + %ln - null))', h1, h2)
1491 1491 return {ctx.node() for ctx in res}
1492 1492
1493 1493 while True:
1494 1494 old_heads = unficl.heads()
1495 1495 clstart = len(unficl)
1496 1496 _pullbundle2(pullop)
1497 1497 if requirements.NARROW_REQUIREMENT in repo.requirements:
1498 1498 # XXX narrow clones filter the heads on the server side during
1499 1499 # XXX getbundle and result in partial replies as well.
1500 1500 # XXX Disable pull bundles in this case as band aid to avoid
1501 1501 # XXX extra round trips.
1502 1502 break
1503 1503 if clstart == len(unficl):
1504 1504 break
1505 1505 if all(unficl.hasnode(n) for n in pullop.rheads):
1506 1506 break
1507 1507 new_heads = headsofdiff(unficl.heads(), old_heads)
1508 1508 pullop.common = headsofunion(new_heads, pullop.common)
1509 1509 pullop.rheads = set(pullop.rheads) - pullop.common
1510 1510
1511 1511
1512 1512 def add_confirm_callback(repo, pullop):
1513 1513 """adds a finalize callback to transaction which can be used to show stats
1514 1514 to user and confirm the pull before committing transaction"""
1515 1515
1516 1516 tr = pullop.trmanager.transaction()
1517 1517 scmutil.registersummarycallback(
1518 1518 repo, tr, txnname=b'pull', as_validator=True
1519 1519 )
1520 1520 reporef = weakref.ref(repo.unfiltered())
1521 1521
1522 1522 def prompt(tr):
1523 1523 repo = reporef()
1524 1524 cm = _(b'accept incoming changes (yn)?$$ &Yes $$ &No')
1525 1525 if repo.ui.promptchoice(cm):
1526 1526 raise error.Abort(b"user aborted")
1527 1527
1528 1528 tr.addvalidator(b'900-pull-prompt', prompt)
1529 1529
1530 1530
1531 1531 def pull(
1532 1532 repo,
1533 1533 remote,
1534 1534 heads=None,
1535 1535 force=False,
1536 1536 bookmarks=(),
1537 1537 opargs=None,
1538 1538 streamclonerequested=None,
1539 1539 includepats=None,
1540 1540 excludepats=None,
1541 1541 depth=None,
1542 1542 confirm=None,
1543 1543 ):
1544 1544 """Fetch repository data from a remote.
1545 1545
1546 1546 This is the main function used to retrieve data from a remote repository.
1547 1547
1548 1548 ``repo`` is the local repository to clone into.
1549 1549 ``remote`` is a peer instance.
1550 1550 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1551 1551 default) means to pull everything from the remote.
1552 1552 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1553 1553 default, all remote bookmarks are pulled.
1554 1554 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1555 1555 initialization.
1556 1556 ``streamclonerequested`` is a boolean indicating whether a "streaming
1557 1557 clone" is requested. A "streaming clone" is essentially a raw file copy
1558 1558 of revlogs from the server. This only works when the local repository is
1559 1559 empty. The default value of ``None`` means to respect the server
1560 1560 configuration for preferring stream clones.
1561 1561 ``includepats`` and ``excludepats`` define explicit file patterns to
1562 1562 include and exclude in storage, respectively. If not defined, narrow
1563 1563 patterns from the repo instance are used, if available.
1564 1564 ``depth`` is an integer indicating the DAG depth of history we're
1565 1565 interested in. If defined, for each revision specified in ``heads``, we
1566 1566 will fetch up to this many of its ancestors and data associated with them.
1567 1567 ``confirm`` is a boolean indicating whether the pull should be confirmed
1568 1568 before committing the transaction. This overrides HGPLAIN.
1569 1569
1570 1570 Returns the ``pulloperation`` created for this pull.
1571 1571 """
1572 1572 if opargs is None:
1573 1573 opargs = {}
1574 1574
1575 1575 # We allow the narrow patterns to be passed in explicitly to provide more
1576 1576 # flexibility for API consumers.
1577 1577 if includepats or excludepats:
1578 1578 includepats = includepats or set()
1579 1579 excludepats = excludepats or set()
1580 1580 else:
1581 1581 includepats, excludepats = repo.narrowpats
1582 1582
1583 1583 narrowspec.validatepatterns(includepats)
1584 1584 narrowspec.validatepatterns(excludepats)
1585 1585
1586 1586 pullop = pulloperation(
1587 1587 repo,
1588 1588 remote,
1589 1589 heads,
1590 1590 force,
1591 1591 bookmarks=bookmarks,
1592 1592 streamclonerequested=streamclonerequested,
1593 1593 includepats=includepats,
1594 1594 excludepats=excludepats,
1595 1595 depth=depth,
1596 1596 **pycompat.strkwargs(opargs)
1597 1597 )
1598 1598
1599 1599 peerlocal = pullop.remote.local()
1600 1600 if peerlocal:
1601 1601 missing = set(peerlocal.requirements) - pullop.repo.supported
1602 1602 if missing:
1603 1603 msg = _(
1604 1604 b"required features are not"
1605 1605 b" supported in the destination:"
1606 1606 b" %s"
1607 1607 ) % (b', '.join(sorted(missing)))
1608 1608 raise error.Abort(msg)
1609 1609
1610 1610 pullop.trmanager = transactionmanager(repo, b'pull', remote.url())
1611 1611 wlock = util.nullcontextmanager()
1612 1612 if not bookmod.bookmarksinstore(repo):
1613 1613 wlock = repo.wlock()
1614 1614 with wlock, repo.lock(), pullop.trmanager:
1615 1615 if confirm or (
1616 1616 repo.ui.configbool(b"pull", b"confirm") and not repo.ui.plain()
1617 1617 ):
1618 1618 add_confirm_callback(repo, pullop)
1619 1619
1620 1620 # Use the modern wire protocol, if available.
1621 1621 if remote.capable(b'command-changesetdata'):
1622 1622 exchangev2.pull(pullop)
1623 1623 else:
1624 1624 # This should ideally be in _pullbundle2(). However, it needs to run
1625 1625 # before discovery to avoid extra work.
1626 1626 _maybeapplyclonebundle(pullop)
1627 1627 streamclone.maybeperformlegacystreamclone(pullop)
1628 1628 _pulldiscovery(pullop)
1629 1629 if pullop.canusebundle2:
1630 1630 _fullpullbundle2(repo, pullop)
1631 1631 _pullchangeset(pullop)
1632 1632 _pullphase(pullop)
1633 1633 _pullbookmarks(pullop)
1634 1634 _pullobsolete(pullop)
1635 1635
1636 1636 # storing remotenames
1637 1637 if repo.ui.configbool(b'experimental', b'remotenames'):
1638 1638 logexchange.pullremotenames(repo, remote)
1639 1639
1640 1640 return pullop
1641 1641
1642 1642
1643 1643 # list of steps to perform discovery before pull
1644 1644 pulldiscoveryorder = []
1645 1645
1646 1646 # Mapping between step name and function
1647 1647 #
1648 1648 # This exists to help extensions wrap steps if necessary
1649 1649 pulldiscoverymapping = {}
1650 1650
1651 1651
1652 1652 def pulldiscovery(stepname):
1653 1653 """decorator for function performing discovery before pull
1654 1654
1655 1655 The function is added to the step -> function mapping and appended to the
1656 1656 list of steps. Beware that decorated function will be added in order (this
1657 1657 may matter).
1658 1658
1659 1659 You can only use this decorator for a new step, if you want to wrap a step
1660 1660 from an extension, change the pulldiscovery dictionary directly."""
1661 1661
1662 1662 def dec(func):
1663 1663 assert stepname not in pulldiscoverymapping
1664 1664 pulldiscoverymapping[stepname] = func
1665 1665 pulldiscoveryorder.append(stepname)
1666 1666 return func
1667 1667
1668 1668 return dec
1669 1669
1670 1670
1671 1671 def _pulldiscovery(pullop):
1672 1672 """Run all discovery steps"""
1673 1673 for stepname in pulldiscoveryorder:
1674 1674 step = pulldiscoverymapping[stepname]
1675 1675 step(pullop)
1676 1676
1677 1677
1678 1678 @pulldiscovery(b'b1:bookmarks')
1679 1679 def _pullbookmarkbundle1(pullop):
1680 1680 """fetch bookmark data in bundle1 case
1681 1681
1682 1682 If not using bundle2, we have to fetch bookmarks before changeset
1683 1683 discovery to reduce the chance and impact of race conditions."""
1684 1684 if pullop.remotebookmarks is not None:
1685 1685 return
1686 1686 if pullop.canusebundle2 and b'listkeys' in pullop.remotebundle2caps:
1687 1687 # all known bundle2 servers now support listkeys, but lets be nice with
1688 1688 # new implementation.
1689 1689 return
1690 1690 books = listkeys(pullop.remote, b'bookmarks')
1691 1691 pullop.remotebookmarks = bookmod.unhexlifybookmarks(books)
1692 1692
1693 1693
1694 1694 @pulldiscovery(b'changegroup')
1695 1695 def _pulldiscoverychangegroup(pullop):
1696 1696 """discovery phase for the pull
1697 1697
1698 1698 Current handle changeset discovery only, will change handle all discovery
1699 1699 at some point."""
1700 1700 tmp = discovery.findcommonincoming(
1701 1701 pullop.repo, pullop.remote, heads=pullop.heads, force=pullop.force
1702 1702 )
1703 1703 common, fetch, rheads = tmp
1704 1704 has_node = pullop.repo.unfiltered().changelog.index.has_node
1705 1705 if fetch and rheads:
1706 1706 # If a remote heads is filtered locally, put in back in common.
1707 1707 #
1708 1708 # This is a hackish solution to catch most of "common but locally
1709 1709 # hidden situation". We do not performs discovery on unfiltered
1710 1710 # repository because it end up doing a pathological amount of round
1711 1711 # trip for w huge amount of changeset we do not care about.
1712 1712 #
1713 1713 # If a set of such "common but filtered" changeset exist on the server
1714 1714 # but are not including a remote heads, we'll not be able to detect it,
1715 1715 scommon = set(common)
1716 1716 for n in rheads:
1717 1717 if has_node(n):
1718 1718 if n not in scommon:
1719 1719 common.append(n)
1720 1720 if set(rheads).issubset(set(common)):
1721 1721 fetch = []
1722 1722 pullop.common = common
1723 1723 pullop.fetch = fetch
1724 1724 pullop.rheads = rheads
1725 1725
1726 1726
1727 1727 def _pullbundle2(pullop):
1728 1728 """pull data using bundle2
1729 1729
1730 1730 For now, the only supported data are changegroup."""
1731 1731 kwargs = {b'bundlecaps': caps20to10(pullop.repo, role=b'client')}
1732 1732
1733 1733 # make ui easier to access
1734 1734 ui = pullop.repo.ui
1735 1735
1736 1736 # At the moment we don't do stream clones over bundle2. If that is
1737 1737 # implemented then here's where the check for that will go.
1738 1738 streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0]
1739 1739
1740 1740 # declare pull perimeters
1741 1741 kwargs[b'common'] = pullop.common
1742 1742 kwargs[b'heads'] = pullop.heads or pullop.rheads
1743 1743
1744 1744 # check server supports narrow and then adding includepats and excludepats
1745 1745 servernarrow = pullop.remote.capable(wireprototypes.NARROWCAP)
1746 1746 if servernarrow and pullop.includepats:
1747 1747 kwargs[b'includepats'] = pullop.includepats
1748 1748 if servernarrow and pullop.excludepats:
1749 1749 kwargs[b'excludepats'] = pullop.excludepats
1750 1750
1751 1751 if streaming:
1752 1752 kwargs[b'cg'] = False
1753 1753 kwargs[b'stream'] = True
1754 1754 pullop.stepsdone.add(b'changegroup')
1755 1755 pullop.stepsdone.add(b'phases')
1756 1756
1757 1757 else:
1758 1758 # pulling changegroup
1759 1759 pullop.stepsdone.add(b'changegroup')
1760 1760
1761 1761 kwargs[b'cg'] = pullop.fetch
1762 1762
1763 1763 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
1764 1764 hasbinaryphase = b'heads' in pullop.remotebundle2caps.get(b'phases', ())
1765 1765 if not legacyphase and hasbinaryphase:
1766 1766 kwargs[b'phases'] = True
1767 1767 pullop.stepsdone.add(b'phases')
1768 1768
1769 1769 if b'listkeys' in pullop.remotebundle2caps:
1770 1770 if b'phases' not in pullop.stepsdone:
1771 1771 kwargs[b'listkeys'] = [b'phases']
1772 1772
1773 1773 bookmarksrequested = False
1774 1774 legacybookmark = b'bookmarks' in ui.configlist(b'devel', b'legacy.exchange')
1775 1775 hasbinarybook = b'bookmarks' in pullop.remotebundle2caps
1776 1776
1777 1777 if pullop.remotebookmarks is not None:
1778 1778 pullop.stepsdone.add(b'request-bookmarks')
1779 1779
1780 1780 if (
1781 1781 b'request-bookmarks' not in pullop.stepsdone
1782 1782 and pullop.remotebookmarks is None
1783 1783 and not legacybookmark
1784 1784 and hasbinarybook
1785 1785 ):
1786 1786 kwargs[b'bookmarks'] = True
1787 1787 bookmarksrequested = True
1788 1788
1789 1789 if b'listkeys' in pullop.remotebundle2caps:
1790 1790 if b'request-bookmarks' not in pullop.stepsdone:
1791 1791 # make sure to always includes bookmark data when migrating
1792 1792 # `hg incoming --bundle` to using this function.
1793 1793 pullop.stepsdone.add(b'request-bookmarks')
1794 1794 kwargs.setdefault(b'listkeys', []).append(b'bookmarks')
1795 1795
1796 1796 # If this is a full pull / clone and the server supports the clone bundles
1797 1797 # feature, tell the server whether we attempted a clone bundle. The
1798 1798 # presence of this flag indicates the client supports clone bundles. This
1799 1799 # will enable the server to treat clients that support clone bundles
1800 1800 # differently from those that don't.
1801 1801 if (
1802 1802 pullop.remote.capable(b'clonebundles')
1803 1803 and pullop.heads is None
1804 1804 and list(pullop.common) == [nullid]
1805 1805 ):
1806 1806 kwargs[b'cbattempted'] = pullop.clonebundleattempted
1807 1807
1808 1808 if streaming:
1809 1809 pullop.repo.ui.status(_(b'streaming all changes\n'))
1810 1810 elif not pullop.fetch:
1811 1811 pullop.repo.ui.status(_(b"no changes found\n"))
1812 1812 pullop.cgresult = 0
1813 1813 else:
1814 1814 if pullop.heads is None and list(pullop.common) == [nullid]:
1815 1815 pullop.repo.ui.status(_(b"requesting all changes\n"))
1816 1816 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1817 1817 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1818 1818 if obsolete.commonversion(remoteversions) is not None:
1819 1819 kwargs[b'obsmarkers'] = True
1820 1820 pullop.stepsdone.add(b'obsmarkers')
1821 1821 _pullbundle2extraprepare(pullop, kwargs)
1822 1822
1823 1823 with pullop.remote.commandexecutor() as e:
1824 1824 args = dict(kwargs)
1825 1825 args[b'source'] = b'pull'
1826 1826 bundle = e.callcommand(b'getbundle', args).result()
1827 1827
1828 1828 try:
1829 1829 op = bundle2.bundleoperation(
1830 1830 pullop.repo, pullop.gettransaction, source=b'pull'
1831 1831 )
1832 1832 op.modes[b'bookmarks'] = b'records'
1833 1833 bundle2.processbundle(pullop.repo, bundle, op=op)
1834 1834 except bundle2.AbortFromPart as exc:
1835 1835 pullop.repo.ui.error(_(b'remote: abort: %s\n') % exc)
1836 1836 raise error.Abort(_(b'pull failed on remote'), hint=exc.hint)
1837 1837 except error.BundleValueError as exc:
1838 1838 raise error.Abort(_(b'missing support for %s') % exc)
1839 1839
1840 1840 if pullop.fetch:
1841 1841 pullop.cgresult = bundle2.combinechangegroupresults(op)
1842 1842
1843 1843 # processing phases change
1844 1844 for namespace, value in op.records[b'listkeys']:
1845 1845 if namespace == b'phases':
1846 1846 _pullapplyphases(pullop, value)
1847 1847
1848 1848 # processing bookmark update
1849 1849 if bookmarksrequested:
1850 1850 books = {}
1851 1851 for record in op.records[b'bookmarks']:
1852 1852 books[record[b'bookmark']] = record[b"node"]
1853 1853 pullop.remotebookmarks = books
1854 1854 else:
1855 1855 for namespace, value in op.records[b'listkeys']:
1856 1856 if namespace == b'bookmarks':
1857 1857 pullop.remotebookmarks = bookmod.unhexlifybookmarks(value)
1858 1858
1859 1859 # bookmark data were either already there or pulled in the bundle
1860 1860 if pullop.remotebookmarks is not None:
1861 1861 _pullbookmarks(pullop)
1862 1862
1863 1863
1864 1864 def _pullbundle2extraprepare(pullop, kwargs):
1865 1865 """hook function so that extensions can extend the getbundle call"""
1866 1866
1867 1867
1868 1868 def _pullchangeset(pullop):
1869 1869 """pull changeset from unbundle into the local repo"""
1870 1870 # We delay the open of the transaction as late as possible so we
1871 1871 # don't open transaction for nothing or you break future useful
1872 1872 # rollback call
1873 1873 if b'changegroup' in pullop.stepsdone:
1874 1874 return
1875 1875 pullop.stepsdone.add(b'changegroup')
1876 1876 if not pullop.fetch:
1877 1877 pullop.repo.ui.status(_(b"no changes found\n"))
1878 1878 pullop.cgresult = 0
1879 1879 return
1880 1880 tr = pullop.gettransaction()
1881 1881 if pullop.heads is None and list(pullop.common) == [nullid]:
1882 1882 pullop.repo.ui.status(_(b"requesting all changes\n"))
1883 1883 elif pullop.heads is None and pullop.remote.capable(b'changegroupsubset'):
1884 1884 # issue1320, avoid a race if remote changed after discovery
1885 1885 pullop.heads = pullop.rheads
1886 1886
1887 1887 if pullop.remote.capable(b'getbundle'):
1888 1888 # TODO: get bundlecaps from remote
1889 1889 cg = pullop.remote.getbundle(
1890 1890 b'pull', common=pullop.common, heads=pullop.heads or pullop.rheads
1891 1891 )
1892 1892 elif pullop.heads is None:
1893 1893 with pullop.remote.commandexecutor() as e:
1894 1894 cg = e.callcommand(
1895 1895 b'changegroup',
1896 1896 {
1897 1897 b'nodes': pullop.fetch,
1898 1898 b'source': b'pull',
1899 1899 },
1900 1900 ).result()
1901 1901
1902 1902 elif not pullop.remote.capable(b'changegroupsubset'):
1903 1903 raise error.Abort(
1904 1904 _(
1905 1905 b"partial pull cannot be done because "
1906 1906 b"other repository doesn't support "
1907 1907 b"changegroupsubset."
1908 1908 )
1909 1909 )
1910 1910 else:
1911 1911 with pullop.remote.commandexecutor() as e:
1912 1912 cg = e.callcommand(
1913 1913 b'changegroupsubset',
1914 1914 {
1915 1915 b'bases': pullop.fetch,
1916 1916 b'heads': pullop.heads,
1917 1917 b'source': b'pull',
1918 1918 },
1919 1919 ).result()
1920 1920
1921 1921 bundleop = bundle2.applybundle(
1922 1922 pullop.repo, cg, tr, b'pull', pullop.remote.url()
1923 1923 )
1924 1924 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
1925 1925
1926 1926
1927 1927 def _pullphase(pullop):
1928 1928 # Get remote phases data from remote
1929 1929 if b'phases' in pullop.stepsdone:
1930 1930 return
1931 1931 remotephases = listkeys(pullop.remote, b'phases')
1932 1932 _pullapplyphases(pullop, remotephases)
1933 1933
1934 1934
1935 1935 def _pullapplyphases(pullop, remotephases):
1936 1936 """apply phase movement from observed remote state"""
1937 1937 if b'phases' in pullop.stepsdone:
1938 1938 return
1939 1939 pullop.stepsdone.add(b'phases')
1940 1940 publishing = bool(remotephases.get(b'publishing', False))
1941 1941 if remotephases and not publishing:
1942 1942 # remote is new and non-publishing
1943 1943 pheads, _dr = phases.analyzeremotephases(
1944 1944 pullop.repo, pullop.pulledsubset, remotephases
1945 1945 )
1946 1946 dheads = pullop.pulledsubset
1947 1947 else:
1948 1948 # Remote is old or publishing all common changesets
1949 1949 # should be seen as public
1950 1950 pheads = pullop.pulledsubset
1951 1951 dheads = []
1952 1952 unfi = pullop.repo.unfiltered()
1953 1953 phase = unfi._phasecache.phase
1954 1954 rev = unfi.changelog.index.get_rev
1955 1955 public = phases.public
1956 1956 draft = phases.draft
1957 1957
1958 1958 # exclude changesets already public locally and update the others
1959 1959 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1960 1960 if pheads:
1961 1961 tr = pullop.gettransaction()
1962 1962 phases.advanceboundary(pullop.repo, tr, public, pheads)
1963 1963
1964 1964 # exclude changesets already draft locally and update the others
1965 1965 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1966 1966 if dheads:
1967 1967 tr = pullop.gettransaction()
1968 1968 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1969 1969
1970 1970
1971 1971 def _pullbookmarks(pullop):
1972 1972 """process the remote bookmark information to update the local one"""
1973 1973 if b'bookmarks' in pullop.stepsdone:
1974 1974 return
1975 1975 pullop.stepsdone.add(b'bookmarks')
1976 1976 repo = pullop.repo
1977 1977 remotebookmarks = pullop.remotebookmarks
1978 1978 bookmod.updatefromremote(
1979 1979 repo.ui,
1980 1980 repo,
1981 1981 remotebookmarks,
1982 1982 pullop.remote.url(),
1983 1983 pullop.gettransaction,
1984 1984 explicit=pullop.explicitbookmarks,
1985 1985 )
1986 1986
1987 1987
1988 1988 def _pullobsolete(pullop):
1989 1989 """utility function to pull obsolete markers from a remote
1990 1990
1991 1991 The `gettransaction` is function that return the pull transaction, creating
1992 1992 one if necessary. We return the transaction to inform the calling code that
1993 1993 a new transaction have been created (when applicable).
1994 1994
1995 1995 Exists mostly to allow overriding for experimentation purpose"""
1996 1996 if b'obsmarkers' in pullop.stepsdone:
1997 1997 return
1998 1998 pullop.stepsdone.add(b'obsmarkers')
1999 1999 tr = None
2000 2000 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
2001 2001 pullop.repo.ui.debug(b'fetching remote obsolete markers\n')
2002 2002 remoteobs = listkeys(pullop.remote, b'obsolete')
2003 2003 if b'dump0' in remoteobs:
2004 2004 tr = pullop.gettransaction()
2005 2005 markers = []
2006 2006 for key in sorted(remoteobs, reverse=True):
2007 2007 if key.startswith(b'dump'):
2008 2008 data = util.b85decode(remoteobs[key])
2009 2009 version, newmarks = obsolete._readmarkers(data)
2010 2010 markers += newmarks
2011 2011 if markers:
2012 2012 pullop.repo.obsstore.add(tr, markers)
2013 2013 pullop.repo.invalidatevolatilesets()
2014 2014 return tr
2015 2015
2016 2016
2017 2017 def applynarrowacl(repo, kwargs):
2018 2018 """Apply narrow fetch access control.
2019 2019
2020 2020 This massages the named arguments for getbundle wire protocol commands
2021 2021 so requested data is filtered through access control rules.
2022 2022 """
2023 2023 ui = repo.ui
2024 2024 # TODO this assumes existence of HTTP and is a layering violation.
2025 2025 username = ui.shortuser(ui.environ.get(b'REMOTE_USER') or ui.username())
2026 2026 user_includes = ui.configlist(
2027 2027 _NARROWACL_SECTION,
2028 2028 username + b'.includes',
2029 2029 ui.configlist(_NARROWACL_SECTION, b'default.includes'),
2030 2030 )
2031 2031 user_excludes = ui.configlist(
2032 2032 _NARROWACL_SECTION,
2033 2033 username + b'.excludes',
2034 2034 ui.configlist(_NARROWACL_SECTION, b'default.excludes'),
2035 2035 )
2036 2036 if not user_includes:
2037 2037 raise error.Abort(
2038 2038 _(b"%s configuration for user %s is empty")
2039 2039 % (_NARROWACL_SECTION, username)
2040 2040 )
2041 2041
2042 2042 user_includes = [
2043 2043 b'path:.' if p == b'*' else b'path:' + p for p in user_includes
2044 2044 ]
2045 2045 user_excludes = [
2046 2046 b'path:.' if p == b'*' else b'path:' + p for p in user_excludes
2047 2047 ]
2048 2048
2049 2049 req_includes = set(kwargs.get('includepats', []))
2050 2050 req_excludes = set(kwargs.get('excludepats', []))
2051 2051
2052 2052 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
2053 2053 req_includes, req_excludes, user_includes, user_excludes
2054 2054 )
2055 2055
2056 2056 if invalid_includes:
2057 2057 raise error.Abort(
2058 2058 _(b"The following includes are not accessible for %s: %s")
2059 2059 % (username, stringutil.pprint(invalid_includes))
2060 2060 )
2061 2061
2062 2062 new_args = {}
2063 2063 new_args.update(kwargs)
2064 2064 new_args['narrow'] = True
2065 2065 new_args['narrow_acl'] = True
2066 2066 new_args['includepats'] = req_includes
2067 2067 if req_excludes:
2068 2068 new_args['excludepats'] = req_excludes
2069 2069
2070 2070 return new_args
2071 2071
2072 2072
2073 2073 def _computeellipsis(repo, common, heads, known, match, depth=None):
2074 2074 """Compute the shape of a narrowed DAG.
2075 2075
2076 2076 Args:
2077 2077 repo: The repository we're transferring.
2078 2078 common: The roots of the DAG range we're transferring.
2079 2079 May be just [nullid], which means all ancestors of heads.
2080 2080 heads: The heads of the DAG range we're transferring.
2081 2081 match: The narrowmatcher that allows us to identify relevant changes.
2082 2082 depth: If not None, only consider nodes to be full nodes if they are at
2083 2083 most depth changesets away from one of heads.
2084 2084
2085 2085 Returns:
2086 2086 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
2087 2087
2088 2088 visitnodes: The list of nodes (either full or ellipsis) which
2089 2089 need to be sent to the client.
2090 2090 relevant_nodes: The set of changelog nodes which change a file inside
2091 2091 the narrowspec. The client needs these as non-ellipsis nodes.
2092 2092 ellipsisroots: A dict of {rev: parents} that is used in
2093 2093 narrowchangegroup to produce ellipsis nodes with the
2094 2094 correct parents.
2095 2095 """
2096 2096 cl = repo.changelog
2097 2097 mfl = repo.manifestlog
2098 2098
2099 2099 clrev = cl.rev
2100 2100
2101 2101 commonrevs = {clrev(n) for n in common} | {nullrev}
2102 2102 headsrevs = {clrev(n) for n in heads}
2103 2103
2104 2104 if depth:
2105 2105 revdepth = {h: 0 for h in headsrevs}
2106 2106
2107 2107 ellipsisheads = collections.defaultdict(set)
2108 2108 ellipsisroots = collections.defaultdict(set)
2109 2109
2110 2110 def addroot(head, curchange):
2111 2111 """Add a root to an ellipsis head, splitting heads with 3 roots."""
2112 2112 ellipsisroots[head].add(curchange)
2113 2113 # Recursively split ellipsis heads with 3 roots by finding the
2114 2114 # roots' youngest common descendant which is an elided merge commit.
2115 2115 # That descendant takes 2 of the 3 roots as its own, and becomes a
2116 2116 # root of the head.
2117 2117 while len(ellipsisroots[head]) > 2:
2118 2118 child, roots = splithead(head)
2119 2119 splitroots(head, child, roots)
2120 2120 head = child # Recurse in case we just added a 3rd root
2121 2121
2122 2122 def splitroots(head, child, roots):
2123 2123 ellipsisroots[head].difference_update(roots)
2124 2124 ellipsisroots[head].add(child)
2125 2125 ellipsisroots[child].update(roots)
2126 2126 ellipsisroots[child].discard(child)
2127 2127
2128 2128 def splithead(head):
2129 2129 r1, r2, r3 = sorted(ellipsisroots[head])
2130 2130 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
2131 2131 mid = repo.revs(
2132 2132 b'sort(merge() & %d::%d & %d::%d, -rev)', nr1, head, nr2, head
2133 2133 )
2134 2134 for j in mid:
2135 2135 if j == nr2:
2136 2136 return nr2, (nr1, nr2)
2137 2137 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
2138 2138 return j, (nr1, nr2)
2139 2139 raise error.Abort(
2140 2140 _(
2141 2141 b'Failed to split up ellipsis node! head: %d, '
2142 2142 b'roots: %d %d %d'
2143 2143 )
2144 2144 % (head, r1, r2, r3)
2145 2145 )
2146 2146
2147 2147 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
2148 2148 visit = reversed(missing)
2149 2149 relevant_nodes = set()
2150 2150 visitnodes = [cl.node(m) for m in missing]
2151 2151 required = set(headsrevs) | known
2152 2152 for rev in visit:
2153 2153 clrev = cl.changelogrevision(rev)
2154 2154 ps = [prev for prev in cl.parentrevs(rev) if prev != nullrev]
2155 2155 if depth is not None:
2156 2156 curdepth = revdepth[rev]
2157 2157 for p in ps:
2158 2158 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
2159 2159 needed = False
2160 2160 shallow_enough = depth is None or revdepth[rev] <= depth
2161 2161 if shallow_enough:
2162 2162 curmf = mfl[clrev.manifest].read()
2163 2163 if ps:
2164 2164 # We choose to not trust the changed files list in
2165 2165 # changesets because it's not always correct. TODO: could
2166 2166 # we trust it for the non-merge case?
2167 2167 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
2168 2168 needed = bool(curmf.diff(p1mf, match))
2169 2169 if not needed and len(ps) > 1:
2170 2170 # For merge changes, the list of changed files is not
2171 2171 # helpful, since we need to emit the merge if a file
2172 2172 # in the narrow spec has changed on either side of the
2173 2173 # merge. As a result, we do a manifest diff to check.
2174 2174 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
2175 2175 needed = bool(curmf.diff(p2mf, match))
2176 2176 else:
2177 2177 # For a root node, we need to include the node if any
2178 2178 # files in the node match the narrowspec.
2179 2179 needed = any(curmf.walk(match))
2180 2180
2181 2181 if needed:
2182 2182 for head in ellipsisheads[rev]:
2183 2183 addroot(head, rev)
2184 2184 for p in ps:
2185 2185 required.add(p)
2186 2186 relevant_nodes.add(cl.node(rev))
2187 2187 else:
2188 2188 if not ps:
2189 2189 ps = [nullrev]
2190 2190 if rev in required:
2191 2191 for head in ellipsisheads[rev]:
2192 2192 addroot(head, rev)
2193 2193 for p in ps:
2194 2194 ellipsisheads[p].add(rev)
2195 2195 else:
2196 2196 for p in ps:
2197 2197 ellipsisheads[p] |= ellipsisheads[rev]
2198 2198
2199 2199 # add common changesets as roots of their reachable ellipsis heads
2200 2200 for c in commonrevs:
2201 2201 for head in ellipsisheads[c]:
2202 2202 addroot(head, c)
2203 2203 return visitnodes, relevant_nodes, ellipsisroots
2204 2204
2205 2205
2206 2206 def caps20to10(repo, role):
2207 2207 """return a set with appropriate options to use bundle20 during getbundle"""
2208 2208 caps = {b'HG20'}
2209 2209 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role))
2210 2210 caps.add(b'bundle2=' + urlreq.quote(capsblob))
2211 2211 return caps
2212 2212
2213 2213
2214 2214 # List of names of steps to perform for a bundle2 for getbundle, order matters.
2215 2215 getbundle2partsorder = []
2216 2216
2217 2217 # Mapping between step name and function
2218 2218 #
2219 2219 # This exists to help extensions wrap steps if necessary
2220 2220 getbundle2partsmapping = {}
2221 2221
2222 2222
2223 2223 def getbundle2partsgenerator(stepname, idx=None):
2224 2224 """decorator for function generating bundle2 part for getbundle
2225 2225
2226 2226 The function is added to the step -> function mapping and appended to the
2227 2227 list of steps. Beware that decorated functions will be added in order
2228 2228 (this may matter).
2229 2229
2230 2230 You can only use this decorator for new steps, if you want to wrap a step
2231 2231 from an extension, attack the getbundle2partsmapping dictionary directly."""
2232 2232
2233 2233 def dec(func):
2234 2234 assert stepname not in getbundle2partsmapping
2235 2235 getbundle2partsmapping[stepname] = func
2236 2236 if idx is None:
2237 2237 getbundle2partsorder.append(stepname)
2238 2238 else:
2239 2239 getbundle2partsorder.insert(idx, stepname)
2240 2240 return func
2241 2241
2242 2242 return dec
2243 2243
2244 2244
2245 2245 def bundle2requested(bundlecaps):
2246 2246 if bundlecaps is not None:
2247 2247 return any(cap.startswith(b'HG2') for cap in bundlecaps)
2248 2248 return False
2249 2249
2250 2250
2251 2251 def getbundlechunks(
2252 repo, source, heads=None, common=None, bundlecaps=None, **kwargs
2252 repo,
2253 source,
2254 heads=None,
2255 common=None,
2256 bundlecaps=None,
2257 remote_sidedata=None,
2258 **kwargs
2253 2259 ):
2254 2260 """Return chunks constituting a bundle's raw data.
2255 2261
2256 2262 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
2257 2263 passed.
2258 2264
2259 2265 Returns a 2-tuple of a dict with metadata about the generated bundle
2260 2266 and an iterator over raw chunks (of varying sizes).
2261 2267 """
2262 2268 kwargs = pycompat.byteskwargs(kwargs)
2263 2269 info = {}
2264 2270 usebundle2 = bundle2requested(bundlecaps)
2265 2271 # bundle10 case
2266 2272 if not usebundle2:
2267 2273 if bundlecaps and not kwargs.get(b'cg', True):
2268 2274 raise ValueError(
2269 2275 _(b'request for bundle10 must include changegroup')
2270 2276 )
2271 2277
2272 2278 if kwargs:
2273 2279 raise ValueError(
2274 2280 _(b'unsupported getbundle arguments: %s')
2275 2281 % b', '.join(sorted(kwargs.keys()))
2276 2282 )
2277 2283 outgoing = _computeoutgoing(repo, heads, common)
2278 2284 info[b'bundleversion'] = 1
2279 2285 return (
2280 2286 info,
2281 2287 changegroup.makestream(
2282 repo, outgoing, b'01', source, bundlecaps=bundlecaps
2288 repo,
2289 outgoing,
2290 b'01',
2291 source,
2292 bundlecaps=bundlecaps,
2293 remote_sidedata=remote_sidedata,
2283 2294 ),
2284 2295 )
2285 2296
2286 2297 # bundle20 case
2287 2298 info[b'bundleversion'] = 2
2288 2299 b2caps = {}
2289 2300 for bcaps in bundlecaps:
2290 2301 if bcaps.startswith(b'bundle2='):
2291 2302 blob = urlreq.unquote(bcaps[len(b'bundle2=') :])
2292 2303 b2caps.update(bundle2.decodecaps(blob))
2293 2304 bundler = bundle2.bundle20(repo.ui, b2caps)
2294 2305
2295 2306 kwargs[b'heads'] = heads
2296 2307 kwargs[b'common'] = common
2297 2308
2298 2309 for name in getbundle2partsorder:
2299 2310 func = getbundle2partsmapping[name]
2300 2311 func(
2301 2312 bundler,
2302 2313 repo,
2303 2314 source,
2304 2315 bundlecaps=bundlecaps,
2305 2316 b2caps=b2caps,
2317 remote_sidedata=remote_sidedata,
2306 2318 **pycompat.strkwargs(kwargs)
2307 2319 )
2308 2320
2309 2321 info[b'prefercompressed'] = bundler.prefercompressed
2310 2322
2311 2323 return info, bundler.getchunks()
2312 2324
2313 2325
2314 2326 @getbundle2partsgenerator(b'stream2')
2315 2327 def _getbundlestream2(bundler, repo, *args, **kwargs):
2316 2328 return bundle2.addpartbundlestream2(bundler, repo, **kwargs)
2317 2329
2318 2330
2319 2331 @getbundle2partsgenerator(b'changegroup')
2320 2332 def _getbundlechangegrouppart(
2321 2333 bundler,
2322 2334 repo,
2323 2335 source,
2324 2336 bundlecaps=None,
2325 2337 b2caps=None,
2326 2338 heads=None,
2327 2339 common=None,
2340 remote_sidedata=None,
2328 2341 **kwargs
2329 2342 ):
2330 2343 """add a changegroup part to the requested bundle"""
2331 2344 if not kwargs.get('cg', True) or not b2caps:
2332 2345 return
2333 2346
2334 2347 version = b'01'
2335 2348 cgversions = b2caps.get(b'changegroup')
2336 2349 if cgversions: # 3.1 and 3.2 ship with an empty value
2337 2350 cgversions = [
2338 2351 v
2339 2352 for v in cgversions
2340 2353 if v in changegroup.supportedoutgoingversions(repo)
2341 2354 ]
2342 2355 if not cgversions:
2343 2356 raise error.Abort(_(b'no common changegroup version'))
2344 2357 version = max(cgversions)
2345 2358
2346 2359 outgoing = _computeoutgoing(repo, heads, common)
2347 2360 if not outgoing.missing:
2348 2361 return
2349 2362
2350 2363 if kwargs.get('narrow', False):
2351 2364 include = sorted(filter(bool, kwargs.get('includepats', [])))
2352 2365 exclude = sorted(filter(bool, kwargs.get('excludepats', [])))
2353 2366 matcher = narrowspec.match(repo.root, include=include, exclude=exclude)
2354 2367 else:
2355 2368 matcher = None
2356 2369
2357 2370 cgstream = changegroup.makestream(
2358 repo, outgoing, version, source, bundlecaps=bundlecaps, matcher=matcher
2371 repo,
2372 outgoing,
2373 version,
2374 source,
2375 bundlecaps=bundlecaps,
2376 matcher=matcher,
2377 remote_sidedata=remote_sidedata,
2359 2378 )
2360 2379
2361 2380 part = bundler.newpart(b'changegroup', data=cgstream)
2362 2381 if cgversions:
2363 2382 part.addparam(b'version', version)
2364 2383
2365 2384 part.addparam(b'nbchanges', b'%d' % len(outgoing.missing), mandatory=False)
2366 2385
2367 2386 if scmutil.istreemanifest(repo):
2368 2387 part.addparam(b'treemanifest', b'1')
2369 2388
2370 2389 if b'exp-sidedata-flag' in repo.requirements:
2371 2390 part.addparam(b'exp-sidedata', b'1')
2372 2391
2373 2392 if (
2374 2393 kwargs.get('narrow', False)
2375 2394 and kwargs.get('narrow_acl', False)
2376 2395 and (include or exclude)
2377 2396 ):
2378 2397 # this is mandatory because otherwise ACL clients won't work
2379 2398 narrowspecpart = bundler.newpart(b'Narrow:responsespec')
2380 2399 narrowspecpart.data = b'%s\0%s' % (
2381 2400 b'\n'.join(include),
2382 2401 b'\n'.join(exclude),
2383 2402 )
2384 2403
2385 2404
2386 2405 @getbundle2partsgenerator(b'bookmarks')
2387 2406 def _getbundlebookmarkpart(
2388 2407 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2389 2408 ):
2390 2409 """add a bookmark part to the requested bundle"""
2391 2410 if not kwargs.get('bookmarks', False):
2392 2411 return
2393 2412 if not b2caps or b'bookmarks' not in b2caps:
2394 2413 raise error.Abort(_(b'no common bookmarks exchange method'))
2395 2414 books = bookmod.listbinbookmarks(repo)
2396 2415 data = bookmod.binaryencode(books)
2397 2416 if data:
2398 2417 bundler.newpart(b'bookmarks', data=data)
2399 2418
2400 2419
2401 2420 @getbundle2partsgenerator(b'listkeys')
2402 2421 def _getbundlelistkeysparts(
2403 2422 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2404 2423 ):
2405 2424 """add parts containing listkeys namespaces to the requested bundle"""
2406 2425 listkeys = kwargs.get('listkeys', ())
2407 2426 for namespace in listkeys:
2408 2427 part = bundler.newpart(b'listkeys')
2409 2428 part.addparam(b'namespace', namespace)
2410 2429 keys = repo.listkeys(namespace).items()
2411 2430 part.data = pushkey.encodekeys(keys)
2412 2431
2413 2432
2414 2433 @getbundle2partsgenerator(b'obsmarkers')
2415 2434 def _getbundleobsmarkerpart(
2416 2435 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2417 2436 ):
2418 2437 """add an obsolescence markers part to the requested bundle"""
2419 2438 if kwargs.get('obsmarkers', False):
2420 2439 if heads is None:
2421 2440 heads = repo.heads()
2422 2441 subset = [c.node() for c in repo.set(b'::%ln', heads)]
2423 2442 markers = repo.obsstore.relevantmarkers(subset)
2424 2443 markers = obsutil.sortedmarkers(markers)
2425 2444 bundle2.buildobsmarkerspart(bundler, markers)
2426 2445
2427 2446
2428 2447 @getbundle2partsgenerator(b'phases')
2429 2448 def _getbundlephasespart(
2430 2449 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2431 2450 ):
2432 2451 """add phase heads part to the requested bundle"""
2433 2452 if kwargs.get('phases', False):
2434 2453 if not b2caps or b'heads' not in b2caps.get(b'phases'):
2435 2454 raise error.Abort(_(b'no common phases exchange method'))
2436 2455 if heads is None:
2437 2456 heads = repo.heads()
2438 2457
2439 2458 headsbyphase = collections.defaultdict(set)
2440 2459 if repo.publishing():
2441 2460 headsbyphase[phases.public] = heads
2442 2461 else:
2443 2462 # find the appropriate heads to move
2444 2463
2445 2464 phase = repo._phasecache.phase
2446 2465 node = repo.changelog.node
2447 2466 rev = repo.changelog.rev
2448 2467 for h in heads:
2449 2468 headsbyphase[phase(repo, rev(h))].add(h)
2450 2469 seenphases = list(headsbyphase.keys())
2451 2470
2452 2471 # We do not handle anything but public and draft phase for now)
2453 2472 if seenphases:
2454 2473 assert max(seenphases) <= phases.draft
2455 2474
2456 2475 # if client is pulling non-public changesets, we need to find
2457 2476 # intermediate public heads.
2458 2477 draftheads = headsbyphase.get(phases.draft, set())
2459 2478 if draftheads:
2460 2479 publicheads = headsbyphase.get(phases.public, set())
2461 2480
2462 2481 revset = b'heads(only(%ln, %ln) and public())'
2463 2482 extraheads = repo.revs(revset, draftheads, publicheads)
2464 2483 for r in extraheads:
2465 2484 headsbyphase[phases.public].add(node(r))
2466 2485
2467 2486 # transform data in a format used by the encoding function
2468 2487 phasemapping = {
2469 2488 phase: sorted(headsbyphase[phase]) for phase in phases.allphases
2470 2489 }
2471 2490
2472 2491 # generate the actual part
2473 2492 phasedata = phases.binaryencode(phasemapping)
2474 2493 bundler.newpart(b'phase-heads', data=phasedata)
2475 2494
2476 2495
2477 2496 @getbundle2partsgenerator(b'hgtagsfnodes')
2478 2497 def _getbundletagsfnodes(
2479 2498 bundler,
2480 2499 repo,
2481 2500 source,
2482 2501 bundlecaps=None,
2483 2502 b2caps=None,
2484 2503 heads=None,
2485 2504 common=None,
2486 2505 **kwargs
2487 2506 ):
2488 2507 """Transfer the .hgtags filenodes mapping.
2489 2508
2490 2509 Only values for heads in this bundle will be transferred.
2491 2510
2492 2511 The part data consists of pairs of 20 byte changeset node and .hgtags
2493 2512 filenodes raw values.
2494 2513 """
2495 2514 # Don't send unless:
2496 2515 # - changeset are being exchanged,
2497 2516 # - the client supports it.
2498 2517 if not b2caps or not (kwargs.get('cg', True) and b'hgtagsfnodes' in b2caps):
2499 2518 return
2500 2519
2501 2520 outgoing = _computeoutgoing(repo, heads, common)
2502 2521 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
2503 2522
2504 2523
2505 2524 @getbundle2partsgenerator(b'cache:rev-branch-cache')
2506 2525 def _getbundlerevbranchcache(
2507 2526 bundler,
2508 2527 repo,
2509 2528 source,
2510 2529 bundlecaps=None,
2511 2530 b2caps=None,
2512 2531 heads=None,
2513 2532 common=None,
2514 2533 **kwargs
2515 2534 ):
2516 2535 """Transfer the rev-branch-cache mapping
2517 2536
2518 2537 The payload is a series of data related to each branch
2519 2538
2520 2539 1) branch name length
2521 2540 2) number of open heads
2522 2541 3) number of closed heads
2523 2542 4) open heads nodes
2524 2543 5) closed heads nodes
2525 2544 """
2526 2545 # Don't send unless:
2527 2546 # - changeset are being exchanged,
2528 2547 # - the client supports it.
2529 2548 # - narrow bundle isn't in play (not currently compatible).
2530 2549 if (
2531 2550 not kwargs.get('cg', True)
2532 2551 or not b2caps
2533 2552 or b'rev-branch-cache' not in b2caps
2534 2553 or kwargs.get('narrow', False)
2535 2554 or repo.ui.has_section(_NARROWACL_SECTION)
2536 2555 ):
2537 2556 return
2538 2557
2539 2558 outgoing = _computeoutgoing(repo, heads, common)
2540 2559 bundle2.addpartrevbranchcache(repo, bundler, outgoing)
2541 2560
2542 2561
2543 2562 def check_heads(repo, their_heads, context):
2544 2563 """check if the heads of a repo have been modified
2545 2564
2546 2565 Used by peer for unbundling.
2547 2566 """
2548 2567 heads = repo.heads()
2549 2568 heads_hash = hashutil.sha1(b''.join(sorted(heads))).digest()
2550 2569 if not (
2551 2570 their_heads == [b'force']
2552 2571 or their_heads == heads
2553 2572 or their_heads == [b'hashed', heads_hash]
2554 2573 ):
2555 2574 # someone else committed/pushed/unbundled while we
2556 2575 # were transferring data
2557 2576 raise error.PushRaced(
2558 2577 b'repository changed while %s - please try again' % context
2559 2578 )
2560 2579
2561 2580
2562 2581 def unbundle(repo, cg, heads, source, url):
2563 2582 """Apply a bundle to a repo.
2564 2583
2565 2584 this function makes sure the repo is locked during the application and have
2566 2585 mechanism to check that no push race occurred between the creation of the
2567 2586 bundle and its application.
2568 2587
2569 2588 If the push was raced as PushRaced exception is raised."""
2570 2589 r = 0
2571 2590 # need a transaction when processing a bundle2 stream
2572 2591 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
2573 2592 lockandtr = [None, None, None]
2574 2593 recordout = None
2575 2594 # quick fix for output mismatch with bundle2 in 3.4
2576 2595 captureoutput = repo.ui.configbool(
2577 2596 b'experimental', b'bundle2-output-capture'
2578 2597 )
2579 2598 if url.startswith(b'remote:http:') or url.startswith(b'remote:https:'):
2580 2599 captureoutput = True
2581 2600 try:
2582 2601 # note: outside bundle1, 'heads' is expected to be empty and this
2583 2602 # 'check_heads' call wil be a no-op
2584 2603 check_heads(repo, heads, b'uploading changes')
2585 2604 # push can proceed
2586 2605 if not isinstance(cg, bundle2.unbundle20):
2587 2606 # legacy case: bundle1 (changegroup 01)
2588 2607 txnname = b"\n".join([source, util.hidepassword(url)])
2589 2608 with repo.lock(), repo.transaction(txnname) as tr:
2590 2609 op = bundle2.applybundle(repo, cg, tr, source, url)
2591 2610 r = bundle2.combinechangegroupresults(op)
2592 2611 else:
2593 2612 r = None
2594 2613 try:
2595 2614
2596 2615 def gettransaction():
2597 2616 if not lockandtr[2]:
2598 2617 if not bookmod.bookmarksinstore(repo):
2599 2618 lockandtr[0] = repo.wlock()
2600 2619 lockandtr[1] = repo.lock()
2601 2620 lockandtr[2] = repo.transaction(source)
2602 2621 lockandtr[2].hookargs[b'source'] = source
2603 2622 lockandtr[2].hookargs[b'url'] = url
2604 2623 lockandtr[2].hookargs[b'bundle2'] = b'1'
2605 2624 return lockandtr[2]
2606 2625
2607 2626 # Do greedy locking by default until we're satisfied with lazy
2608 2627 # locking.
2609 2628 if not repo.ui.configbool(
2610 2629 b'experimental', b'bundle2lazylocking'
2611 2630 ):
2612 2631 gettransaction()
2613 2632
2614 2633 op = bundle2.bundleoperation(
2615 2634 repo,
2616 2635 gettransaction,
2617 2636 captureoutput=captureoutput,
2618 2637 source=b'push',
2619 2638 )
2620 2639 try:
2621 2640 op = bundle2.processbundle(repo, cg, op=op)
2622 2641 finally:
2623 2642 r = op.reply
2624 2643 if captureoutput and r is not None:
2625 2644 repo.ui.pushbuffer(error=True, subproc=True)
2626 2645
2627 2646 def recordout(output):
2628 2647 r.newpart(b'output', data=output, mandatory=False)
2629 2648
2630 2649 if lockandtr[2] is not None:
2631 2650 lockandtr[2].close()
2632 2651 except BaseException as exc:
2633 2652 exc.duringunbundle2 = True
2634 2653 if captureoutput and r is not None:
2635 2654 parts = exc._bundle2salvagedoutput = r.salvageoutput()
2636 2655
2637 2656 def recordout(output):
2638 2657 part = bundle2.bundlepart(
2639 2658 b'output', data=output, mandatory=False
2640 2659 )
2641 2660 parts.append(part)
2642 2661
2643 2662 raise
2644 2663 finally:
2645 2664 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
2646 2665 if recordout is not None:
2647 2666 recordout(repo.ui.popbuffer())
2648 2667 return r
2649 2668
2650 2669
2651 2670 def _maybeapplyclonebundle(pullop):
2652 2671 """Apply a clone bundle from a remote, if possible."""
2653 2672
2654 2673 repo = pullop.repo
2655 2674 remote = pullop.remote
2656 2675
2657 2676 if not repo.ui.configbool(b'ui', b'clonebundles'):
2658 2677 return
2659 2678
2660 2679 # Only run if local repo is empty.
2661 2680 if len(repo):
2662 2681 return
2663 2682
2664 2683 if pullop.heads:
2665 2684 return
2666 2685
2667 2686 if not remote.capable(b'clonebundles'):
2668 2687 return
2669 2688
2670 2689 with remote.commandexecutor() as e:
2671 2690 res = e.callcommand(b'clonebundles', {}).result()
2672 2691
2673 2692 # If we call the wire protocol command, that's good enough to record the
2674 2693 # attempt.
2675 2694 pullop.clonebundleattempted = True
2676 2695
2677 2696 entries = bundlecaches.parseclonebundlesmanifest(repo, res)
2678 2697 if not entries:
2679 2698 repo.ui.note(
2680 2699 _(
2681 2700 b'no clone bundles available on remote; '
2682 2701 b'falling back to regular clone\n'
2683 2702 )
2684 2703 )
2685 2704 return
2686 2705
2687 2706 entries = bundlecaches.filterclonebundleentries(
2688 2707 repo, entries, streamclonerequested=pullop.streamclonerequested
2689 2708 )
2690 2709
2691 2710 if not entries:
2692 2711 # There is a thundering herd concern here. However, if a server
2693 2712 # operator doesn't advertise bundles appropriate for its clients,
2694 2713 # they deserve what's coming. Furthermore, from a client's
2695 2714 # perspective, no automatic fallback would mean not being able to
2696 2715 # clone!
2697 2716 repo.ui.warn(
2698 2717 _(
2699 2718 b'no compatible clone bundles available on server; '
2700 2719 b'falling back to regular clone\n'
2701 2720 )
2702 2721 )
2703 2722 repo.ui.warn(
2704 2723 _(b'(you may want to report this to the server operator)\n')
2705 2724 )
2706 2725 return
2707 2726
2708 2727 entries = bundlecaches.sortclonebundleentries(repo.ui, entries)
2709 2728
2710 2729 url = entries[0][b'URL']
2711 2730 repo.ui.status(_(b'applying clone bundle from %s\n') % url)
2712 2731 if trypullbundlefromurl(repo.ui, repo, url):
2713 2732 repo.ui.status(_(b'finished applying clone bundle\n'))
2714 2733 # Bundle failed.
2715 2734 #
2716 2735 # We abort by default to avoid the thundering herd of
2717 2736 # clients flooding a server that was expecting expensive
2718 2737 # clone load to be offloaded.
2719 2738 elif repo.ui.configbool(b'ui', b'clonebundlefallback'):
2720 2739 repo.ui.warn(_(b'falling back to normal clone\n'))
2721 2740 else:
2722 2741 raise error.Abort(
2723 2742 _(b'error applying bundle'),
2724 2743 hint=_(
2725 2744 b'if this error persists, consider contacting '
2726 2745 b'the server operator or disable clone '
2727 2746 b'bundles via '
2728 2747 b'"--config ui.clonebundles=false"'
2729 2748 ),
2730 2749 )
2731 2750
2732 2751
2733 2752 def trypullbundlefromurl(ui, repo, url):
2734 2753 """Attempt to apply a bundle from a URL."""
2735 2754 with repo.lock(), repo.transaction(b'bundleurl') as tr:
2736 2755 try:
2737 2756 fh = urlmod.open(ui, url)
2738 2757 cg = readbundle(ui, fh, b'stream')
2739 2758
2740 2759 if isinstance(cg, streamclone.streamcloneapplier):
2741 2760 cg.apply(repo)
2742 2761 else:
2743 2762 bundle2.applybundle(repo, cg, tr, b'clonebundles', url)
2744 2763 return True
2745 2764 except urlerr.httperror as e:
2746 2765 ui.warn(
2747 2766 _(b'HTTP error fetching bundle: %s\n')
2748 2767 % stringutil.forcebytestr(e)
2749 2768 )
2750 2769 except urlerr.urlerror as e:
2751 2770 ui.warn(
2752 2771 _(b'error fetching bundle: %s\n')
2753 2772 % stringutil.forcebytestr(e.reason)
2754 2773 )
2755 2774
2756 2775 return False
@@ -1,799 +1,807 b''
1 1 # exchangev2.py - repository exchange for wire protocol version 2
2 2 #
3 3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.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 weakref
12 12
13 13 from .i18n import _
14 14 from .node import (
15 15 nullid,
16 16 short,
17 17 )
18 18 from . import (
19 19 bookmarks,
20 20 error,
21 21 mdiff,
22 22 narrowspec,
23 23 phases,
24 24 pycompat,
25 25 requirements as requirementsmod,
26 26 setdiscovery,
27 27 )
28 28 from .interfaces import repository
29 29
30 30
31 31 def pull(pullop):
32 32 """Pull using wire protocol version 2."""
33 33 repo = pullop.repo
34 34 remote = pullop.remote
35 35
36 36 usingrawchangelogandmanifest = _checkuserawstorefiledata(pullop)
37 37
38 38 # If this is a clone and it was requested to perform a "stream clone",
39 39 # we obtain the raw files data from the remote then fall back to an
40 40 # incremental pull. This is somewhat hacky and is not nearly robust enough
41 41 # for long-term usage.
42 42 if usingrawchangelogandmanifest:
43 43 with repo.transaction(b'clone'):
44 44 _fetchrawstorefiles(repo, remote)
45 45 repo.invalidate(clearfilecache=True)
46 46
47 47 tr = pullop.trmanager.transaction()
48 48
49 49 # We don't use the repo's narrow matcher here because the patterns passed
50 50 # to exchange.pull() could be different.
51 51 narrowmatcher = narrowspec.match(
52 52 repo.root,
53 53 # Empty maps to nevermatcher. So always
54 54 # set includes if missing.
55 55 pullop.includepats or {b'path:.'},
56 56 pullop.excludepats,
57 57 )
58 58
59 59 if pullop.includepats or pullop.excludepats:
60 60 pathfilter = {}
61 61 if pullop.includepats:
62 62 pathfilter[b'include'] = sorted(pullop.includepats)
63 63 if pullop.excludepats:
64 64 pathfilter[b'exclude'] = sorted(pullop.excludepats)
65 65 else:
66 66 pathfilter = None
67 67
68 68 # Figure out what needs to be fetched.
69 69 common, fetch, remoteheads = _pullchangesetdiscovery(
70 70 repo, remote, pullop.heads, abortwhenunrelated=pullop.force
71 71 )
72 72
73 73 # And fetch the data.
74 74 pullheads = pullop.heads or remoteheads
75 75 csetres = _fetchchangesets(repo, tr, remote, common, fetch, pullheads)
76 76
77 77 # New revisions are written to the changelog. But all other updates
78 78 # are deferred. Do those now.
79 79
80 80 # Ensure all new changesets are draft by default. If the repo is
81 81 # publishing, the phase will be adjusted by the loop below.
82 82 if csetres[b'added']:
83 83 phases.registernew(
84 84 repo, tr, phases.draft, [repo[n].rev() for n in csetres[b'added']]
85 85 )
86 86
87 87 # And adjust the phase of all changesets accordingly.
88 88 for phasenumber, phase in phases.phasenames.items():
89 89 if phase == b'secret' or not csetres[b'nodesbyphase'][phase]:
90 90 continue
91 91
92 92 phases.advanceboundary(
93 93 repo,
94 94 tr,
95 95 phasenumber,
96 96 csetres[b'nodesbyphase'][phase],
97 97 )
98 98
99 99 # Write bookmark updates.
100 100 bookmarks.updatefromremote(
101 101 repo.ui,
102 102 repo,
103 103 csetres[b'bookmarks'],
104 104 remote.url(),
105 105 pullop.gettransaction,
106 106 explicit=pullop.explicitbookmarks,
107 107 )
108 108
109 109 manres = _fetchmanifests(repo, tr, remote, csetres[b'manifestnodes'])
110 110
111 111 # We don't properly support shallow changeset and manifest yet. So we apply
112 112 # depth limiting locally.
113 113 if pullop.depth:
114 114 relevantcsetnodes = set()
115 115 clnode = repo.changelog.node
116 116
117 117 for rev in repo.revs(
118 118 b'ancestors(%ln, %s)', pullheads, pullop.depth - 1
119 119 ):
120 120 relevantcsetnodes.add(clnode(rev))
121 121
122 122 csetrelevantfilter = lambda n: n in relevantcsetnodes
123 123
124 124 else:
125 125 csetrelevantfilter = lambda n: True
126 126
127 127 # If obtaining the raw store files, we need to scan the full repo to
128 128 # derive all the changesets, manifests, and linkrevs.
129 129 if usingrawchangelogandmanifest:
130 130 csetsforfiles = []
131 131 mnodesforfiles = []
132 132 manifestlinkrevs = {}
133 133
134 134 for rev in repo:
135 135 ctx = repo[rev]
136 136 node = ctx.node()
137 137
138 138 if not csetrelevantfilter(node):
139 139 continue
140 140
141 141 mnode = ctx.manifestnode()
142 142
143 143 csetsforfiles.append(node)
144 144 mnodesforfiles.append(mnode)
145 145 manifestlinkrevs[mnode] = rev
146 146
147 147 else:
148 148 csetsforfiles = [n for n in csetres[b'added'] if csetrelevantfilter(n)]
149 149 mnodesforfiles = manres[b'added']
150 150 manifestlinkrevs = manres[b'linkrevs']
151 151
152 152 # Find all file nodes referenced by added manifests and fetch those
153 153 # revisions.
154 154 fnodes = _derivefilesfrommanifests(repo, narrowmatcher, mnodesforfiles)
155 155 _fetchfilesfromcsets(
156 156 repo,
157 157 tr,
158 158 remote,
159 159 pathfilter,
160 160 fnodes,
161 161 csetsforfiles,
162 162 manifestlinkrevs,
163 163 shallow=bool(pullop.depth),
164 164 )
165 165
166 166
167 167 def _checkuserawstorefiledata(pullop):
168 168 """Check whether we should use rawstorefiledata command to retrieve data."""
169 169
170 170 repo = pullop.repo
171 171 remote = pullop.remote
172 172
173 173 # Command to obtain raw store data isn't available.
174 174 if b'rawstorefiledata' not in remote.apidescriptor[b'commands']:
175 175 return False
176 176
177 177 # Only honor if user requested stream clone operation.
178 178 if not pullop.streamclonerequested:
179 179 return False
180 180
181 181 # Only works on empty repos.
182 182 if len(repo):
183 183 return False
184 184
185 185 # TODO This is super hacky. There needs to be a storage API for this. We
186 186 # also need to check for compatibility with the remote.
187 187 if requirementsmod.REVLOGV1_REQUIREMENT not in repo.requirements:
188 188 return False
189 189
190 190 return True
191 191
192 192
193 193 def _fetchrawstorefiles(repo, remote):
194 194 with remote.commandexecutor() as e:
195 195 objs = e.callcommand(
196 196 b'rawstorefiledata',
197 197 {
198 198 b'files': [b'changelog', b'manifestlog'],
199 199 },
200 200 ).result()
201 201
202 202 # First object is a summary of files data that follows.
203 203 overall = next(objs)
204 204
205 205 progress = repo.ui.makeprogress(
206 206 _(b'clone'), total=overall[b'totalsize'], unit=_(b'bytes')
207 207 )
208 208 with progress:
209 209 progress.update(0)
210 210
211 211 # Next are pairs of file metadata, data.
212 212 while True:
213 213 try:
214 214 filemeta = next(objs)
215 215 except StopIteration:
216 216 break
217 217
218 218 for k in (b'location', b'path', b'size'):
219 219 if k not in filemeta:
220 220 raise error.Abort(
221 221 _(b'remote file data missing key: %s') % k
222 222 )
223 223
224 224 if filemeta[b'location'] == b'store':
225 225 vfs = repo.svfs
226 226 else:
227 227 raise error.Abort(
228 228 _(b'invalid location for raw file data: %s')
229 229 % filemeta[b'location']
230 230 )
231 231
232 232 bytesremaining = filemeta[b'size']
233 233
234 234 with vfs.open(filemeta[b'path'], b'wb') as fh:
235 235 while True:
236 236 try:
237 237 chunk = next(objs)
238 238 except StopIteration:
239 239 break
240 240
241 241 bytesremaining -= len(chunk)
242 242
243 243 if bytesremaining < 0:
244 244 raise error.Abort(
245 245 _(
246 246 b'received invalid number of bytes for file '
247 247 b'data; expected %d, got extra'
248 248 )
249 249 % filemeta[b'size']
250 250 )
251 251
252 252 progress.increment(step=len(chunk))
253 253 fh.write(chunk)
254 254
255 255 try:
256 256 if chunk.islast:
257 257 break
258 258 except AttributeError:
259 259 raise error.Abort(
260 260 _(
261 261 b'did not receive indefinite length bytestring '
262 262 b'for file data'
263 263 )
264 264 )
265 265
266 266 if bytesremaining:
267 267 raise error.Abort(
268 268 _(
269 269 b'received invalid number of bytes for'
270 270 b'file data; expected %d got %d'
271 271 )
272 272 % (
273 273 filemeta[b'size'],
274 274 filemeta[b'size'] - bytesremaining,
275 275 )
276 276 )
277 277
278 278
279 279 def _pullchangesetdiscovery(repo, remote, heads, abortwhenunrelated=True):
280 280 """Determine which changesets need to be pulled."""
281 281
282 282 if heads:
283 283 knownnode = repo.changelog.hasnode
284 284 if all(knownnode(head) for head in heads):
285 285 return heads, False, heads
286 286
287 287 # TODO wire protocol version 2 is capable of more efficient discovery
288 288 # than setdiscovery. Consider implementing something better.
289 289 common, fetch, remoteheads = setdiscovery.findcommonheads(
290 290 repo.ui, repo, remote, abortwhenunrelated=abortwhenunrelated
291 291 )
292 292
293 293 common = set(common)
294 294 remoteheads = set(remoteheads)
295 295
296 296 # If a remote head is filtered locally, put it back in the common set.
297 297 # See the comment in exchange._pulldiscoverychangegroup() for more.
298 298
299 299 if fetch and remoteheads:
300 300 has_node = repo.unfiltered().changelog.index.has_node
301 301
302 302 common |= {head for head in remoteheads if has_node(head)}
303 303
304 304 if set(remoteheads).issubset(common):
305 305 fetch = []
306 306
307 307 common.discard(nullid)
308 308
309 309 return common, fetch, remoteheads
310 310
311 311
312 312 def _fetchchangesets(repo, tr, remote, common, fetch, remoteheads):
313 313 # TODO consider adding a step here where we obtain the DAG shape first
314 314 # (or ask the server to slice changesets into chunks for us) so that
315 315 # we can perform multiple fetches in batches. This will facilitate
316 316 # resuming interrupted clones, higher server-side cache hit rates due
317 317 # to smaller segments, etc.
318 318 with remote.commandexecutor() as e:
319 319 objs = e.callcommand(
320 320 b'changesetdata',
321 321 {
322 322 b'revisions': [
323 323 {
324 324 b'type': b'changesetdagrange',
325 325 b'roots': sorted(common),
326 326 b'heads': sorted(remoteheads),
327 327 }
328 328 ],
329 329 b'fields': {b'bookmarks', b'parents', b'phase', b'revision'},
330 330 },
331 331 ).result()
332 332
333 333 # The context manager waits on all response data when exiting. So
334 334 # we need to remain in the context manager in order to stream data.
335 335 return _processchangesetdata(repo, tr, objs)
336 336
337 337
338 338 def _processchangesetdata(repo, tr, objs):
339 339 repo.hook(b'prechangegroup', throw=True, **pycompat.strkwargs(tr.hookargs))
340 340
341 341 urepo = repo.unfiltered()
342 342 cl = urepo.changelog
343 343
344 344 cl.delayupdate(tr)
345 345
346 346 # The first emitted object is a header describing the data that
347 347 # follows.
348 348 meta = next(objs)
349 349
350 350 progress = repo.ui.makeprogress(
351 351 _(b'changesets'), unit=_(b'chunks'), total=meta.get(b'totalitems')
352 352 )
353 353
354 354 manifestnodes = {}
355 355 added = []
356 356
357 357 def linkrev(node):
358 358 repo.ui.debug(b'add changeset %s\n' % short(node))
359 359 # Linkrev for changelog is always self.
360 360 return len(cl)
361 361
362 362 def ondupchangeset(cl, rev):
363 363 added.append(cl.node(rev))
364 364
365 365 def onchangeset(cl, rev):
366 366 progress.increment()
367 367
368 368 revision = cl.changelogrevision(rev)
369 369 added.append(cl.node(rev))
370 370
371 371 # We need to preserve the mapping of changelog revision to node
372 372 # so we can set the linkrev accordingly when manifests are added.
373 373 manifestnodes[rev] = revision.manifest
374 374
375 375 repo.register_changeset(rev, revision)
376 376
377 377 nodesbyphase = {phase: set() for phase in phases.phasenames.values()}
378 378 remotebookmarks = {}
379 379
380 380 # addgroup() expects a 7-tuple describing revisions. This normalizes
381 381 # the wire data to that format.
382 382 #
383 383 # This loop also aggregates non-revision metadata, such as phase
384 384 # data.
385 385 def iterrevisions():
386 386 for cset in objs:
387 387 node = cset[b'node']
388 388
389 389 if b'phase' in cset:
390 390 nodesbyphase[cset[b'phase']].add(node)
391 391
392 392 for mark in cset.get(b'bookmarks', []):
393 393 remotebookmarks[mark] = node
394 394
395 395 # TODO add mechanism for extensions to examine records so they
396 396 # can siphon off custom data fields.
397 397
398 398 extrafields = {}
399 399
400 400 for field, size in cset.get(b'fieldsfollowing', []):
401 401 extrafields[field] = next(objs)
402 402
403 403 # Some entries might only be metadata only updates.
404 404 if b'revision' not in extrafields:
405 405 continue
406 406
407 407 data = extrafields[b'revision']
408 408
409 409 yield (
410 410 node,
411 411 cset[b'parents'][0],
412 412 cset[b'parents'][1],
413 413 # Linknode is always itself for changesets.
414 414 cset[b'node'],
415 415 # We always send full revisions. So delta base is not set.
416 416 nullid,
417 417 mdiff.trivialdiffheader(len(data)) + data,
418 418 # Flags not yet supported.
419 419 0,
420 # Sidedata not yet supported
421 {},
420 422 )
421 423
422 424 cl.addgroup(
423 425 iterrevisions(),
424 426 linkrev,
425 427 weakref.proxy(tr),
426 428 alwayscache=True,
427 429 addrevisioncb=onchangeset,
428 430 duplicaterevisioncb=ondupchangeset,
429 431 )
430 432
431 433 progress.complete()
432 434
433 435 return {
434 436 b'added': added,
435 437 b'nodesbyphase': nodesbyphase,
436 438 b'bookmarks': remotebookmarks,
437 439 b'manifestnodes': manifestnodes,
438 440 }
439 441
440 442
441 443 def _fetchmanifests(repo, tr, remote, manifestnodes):
442 444 rootmanifest = repo.manifestlog.getstorage(b'')
443 445
444 446 # Some manifests can be shared between changesets. Filter out revisions
445 447 # we already know about.
446 448 fetchnodes = []
447 449 linkrevs = {}
448 450 seen = set()
449 451
450 452 for clrev, node in sorted(pycompat.iteritems(manifestnodes)):
451 453 if node in seen:
452 454 continue
453 455
454 456 try:
455 457 rootmanifest.rev(node)
456 458 except error.LookupError:
457 459 fetchnodes.append(node)
458 460 linkrevs[node] = clrev
459 461
460 462 seen.add(node)
461 463
462 464 # TODO handle tree manifests
463 465
464 466 # addgroup() expects 7-tuple describing revisions. This normalizes
465 467 # the wire data to that format.
466 468 def iterrevisions(objs, progress):
467 469 for manifest in objs:
468 470 node = manifest[b'node']
469 471
470 472 extrafields = {}
471 473
472 474 for field, size in manifest.get(b'fieldsfollowing', []):
473 475 extrafields[field] = next(objs)
474 476
475 477 if b'delta' in extrafields:
476 478 basenode = manifest[b'deltabasenode']
477 479 delta = extrafields[b'delta']
478 480 elif b'revision' in extrafields:
479 481 basenode = nullid
480 482 revision = extrafields[b'revision']
481 483 delta = mdiff.trivialdiffheader(len(revision)) + revision
482 484 else:
483 485 continue
484 486
485 487 yield (
486 488 node,
487 489 manifest[b'parents'][0],
488 490 manifest[b'parents'][1],
489 491 # The value passed in is passed to the lookup function passed
490 492 # to addgroup(). We already have a map of manifest node to
491 493 # changelog revision number. So we just pass in the
492 494 # manifest node here and use linkrevs.__getitem__ as the
493 495 # resolution function.
494 496 node,
495 497 basenode,
496 498 delta,
497 499 # Flags not yet supported.
498 500 0,
501 # Sidedata not yet supported.
502 {},
499 503 )
500 504
501 505 progress.increment()
502 506
503 507 progress = repo.ui.makeprogress(
504 508 _(b'manifests'), unit=_(b'chunks'), total=len(fetchnodes)
505 509 )
506 510
507 511 commandmeta = remote.apidescriptor[b'commands'][b'manifestdata']
508 512 batchsize = commandmeta.get(b'recommendedbatchsize', 10000)
509 513 # TODO make size configurable on client?
510 514
511 515 # We send commands 1 at a time to the remote. This is not the most
512 516 # efficient because we incur a round trip at the end of each batch.
513 517 # However, the existing frame-based reactor keeps consuming server
514 518 # data in the background. And this results in response data buffering
515 519 # in memory. This can consume gigabytes of memory.
516 520 # TODO send multiple commands in a request once background buffering
517 521 # issues are resolved.
518 522
519 523 added = []
520 524
521 525 for i in pycompat.xrange(0, len(fetchnodes), batchsize):
522 526 batch = [node for node in fetchnodes[i : i + batchsize]]
523 527 if not batch:
524 528 continue
525 529
526 530 with remote.commandexecutor() as e:
527 531 objs = e.callcommand(
528 532 b'manifestdata',
529 533 {
530 534 b'tree': b'',
531 535 b'nodes': batch,
532 536 b'fields': {b'parents', b'revision'},
533 537 b'haveparents': True,
534 538 },
535 539 ).result()
536 540
537 541 # Chomp off header object.
538 542 next(objs)
539 543
540 544 def onchangeset(cl, rev):
541 545 added.append(cl.node(rev))
542 546
543 547 rootmanifest.addgroup(
544 548 iterrevisions(objs, progress),
545 549 linkrevs.__getitem__,
546 550 weakref.proxy(tr),
547 551 addrevisioncb=onchangeset,
548 552 duplicaterevisioncb=onchangeset,
549 553 )
550 554
551 555 progress.complete()
552 556
553 557 return {
554 558 b'added': added,
555 559 b'linkrevs': linkrevs,
556 560 }
557 561
558 562
559 563 def _derivefilesfrommanifests(repo, matcher, manifestnodes):
560 564 """Determine what file nodes are relevant given a set of manifest nodes.
561 565
562 566 Returns a dict mapping file paths to dicts of file node to first manifest
563 567 node.
564 568 """
565 569 ml = repo.manifestlog
566 570 fnodes = collections.defaultdict(dict)
567 571
568 572 progress = repo.ui.makeprogress(
569 573 _(b'scanning manifests'), total=len(manifestnodes)
570 574 )
571 575
572 576 with progress:
573 577 for manifestnode in manifestnodes:
574 578 m = ml.get(b'', manifestnode)
575 579
576 580 # TODO this will pull in unwanted nodes because it takes the storage
577 581 # delta into consideration. What we really want is something that
578 582 # takes the delta between the manifest's parents. And ideally we
579 583 # would ignore file nodes that are known locally. For now, ignore
580 584 # both these limitations. This will result in incremental fetches
581 585 # requesting data we already have. So this is far from ideal.
582 586 md = m.readfast()
583 587
584 588 for path, fnode in md.items():
585 589 if matcher(path):
586 590 fnodes[path].setdefault(fnode, manifestnode)
587 591
588 592 progress.increment()
589 593
590 594 return fnodes
591 595
592 596
593 597 def _fetchfiles(repo, tr, remote, fnodes, linkrevs):
594 598 """Fetch file data from explicit file revisions."""
595 599
596 600 def iterrevisions(objs, progress):
597 601 for filerevision in objs:
598 602 node = filerevision[b'node']
599 603
600 604 extrafields = {}
601 605
602 606 for field, size in filerevision.get(b'fieldsfollowing', []):
603 607 extrafields[field] = next(objs)
604 608
605 609 if b'delta' in extrafields:
606 610 basenode = filerevision[b'deltabasenode']
607 611 delta = extrafields[b'delta']
608 612 elif b'revision' in extrafields:
609 613 basenode = nullid
610 614 revision = extrafields[b'revision']
611 615 delta = mdiff.trivialdiffheader(len(revision)) + revision
612 616 else:
613 617 continue
614 618
615 619 yield (
616 620 node,
617 621 filerevision[b'parents'][0],
618 622 filerevision[b'parents'][1],
619 623 node,
620 624 basenode,
621 625 delta,
622 626 # Flags not yet supported.
623 627 0,
628 # Sidedata not yet supported.
629 {},
624 630 )
625 631
626 632 progress.increment()
627 633
628 634 progress = repo.ui.makeprogress(
629 635 _(b'files'),
630 636 unit=_(b'chunks'),
631 637 total=sum(len(v) for v in pycompat.itervalues(fnodes)),
632 638 )
633 639
634 640 # TODO make batch size configurable
635 641 batchsize = 10000
636 642 fnodeslist = [x for x in sorted(fnodes.items())]
637 643
638 644 for i in pycompat.xrange(0, len(fnodeslist), batchsize):
639 645 batch = [x for x in fnodeslist[i : i + batchsize]]
640 646 if not batch:
641 647 continue
642 648
643 649 with remote.commandexecutor() as e:
644 650 fs = []
645 651 locallinkrevs = {}
646 652
647 653 for path, nodes in batch:
648 654 fs.append(
649 655 (
650 656 path,
651 657 e.callcommand(
652 658 b'filedata',
653 659 {
654 660 b'path': path,
655 661 b'nodes': sorted(nodes),
656 662 b'fields': {b'parents', b'revision'},
657 663 b'haveparents': True,
658 664 },
659 665 ),
660 666 )
661 667 )
662 668
663 669 locallinkrevs[path] = {
664 670 node: linkrevs[manifestnode]
665 671 for node, manifestnode in pycompat.iteritems(nodes)
666 672 }
667 673
668 674 for path, f in fs:
669 675 objs = f.result()
670 676
671 677 # Chomp off header objects.
672 678 next(objs)
673 679
674 680 store = repo.file(path)
675 681 store.addgroup(
676 682 iterrevisions(objs, progress),
677 683 locallinkrevs[path].__getitem__,
678 684 weakref.proxy(tr),
679 685 )
680 686
681 687
682 688 def _fetchfilesfromcsets(
683 689 repo, tr, remote, pathfilter, fnodes, csets, manlinkrevs, shallow=False
684 690 ):
685 691 """Fetch file data from explicit changeset revisions."""
686 692
687 693 def iterrevisions(objs, remaining, progress):
688 694 while remaining:
689 695 filerevision = next(objs)
690 696
691 697 node = filerevision[b'node']
692 698
693 699 extrafields = {}
694 700
695 701 for field, size in filerevision.get(b'fieldsfollowing', []):
696 702 extrafields[field] = next(objs)
697 703
698 704 if b'delta' in extrafields:
699 705 basenode = filerevision[b'deltabasenode']
700 706 delta = extrafields[b'delta']
701 707 elif b'revision' in extrafields:
702 708 basenode = nullid
703 709 revision = extrafields[b'revision']
704 710 delta = mdiff.trivialdiffheader(len(revision)) + revision
705 711 else:
706 712 continue
707 713
708 714 if b'linknode' in filerevision:
709 715 linknode = filerevision[b'linknode']
710 716 else:
711 717 linknode = node
712 718
713 719 yield (
714 720 node,
715 721 filerevision[b'parents'][0],
716 722 filerevision[b'parents'][1],
717 723 linknode,
718 724 basenode,
719 725 delta,
720 726 # Flags not yet supported.
721 727 0,
728 # Sidedata not yet supported.
729 {},
722 730 )
723 731
724 732 progress.increment()
725 733 remaining -= 1
726 734
727 735 progress = repo.ui.makeprogress(
728 736 _(b'files'),
729 737 unit=_(b'chunks'),
730 738 total=sum(len(v) for v in pycompat.itervalues(fnodes)),
731 739 )
732 740
733 741 commandmeta = remote.apidescriptor[b'commands'][b'filesdata']
734 742 batchsize = commandmeta.get(b'recommendedbatchsize', 50000)
735 743
736 744 shallowfiles = repository.REPO_FEATURE_SHALLOW_FILE_STORAGE in repo.features
737 745 fields = {b'parents', b'revision'}
738 746 clrev = repo.changelog.rev
739 747
740 748 # There are no guarantees that we'll have ancestor revisions if
741 749 # a) this repo has shallow file storage b) shallow data fetching is enabled.
742 750 # Force remote to not delta against possibly unknown revisions when these
743 751 # conditions hold.
744 752 haveparents = not (shallowfiles or shallow)
745 753
746 754 # Similarly, we may not have calculated linkrevs for all incoming file
747 755 # revisions. Ask the remote to do work for us in this case.
748 756 if not haveparents:
749 757 fields.add(b'linknode')
750 758
751 759 for i in pycompat.xrange(0, len(csets), batchsize):
752 760 batch = [x for x in csets[i : i + batchsize]]
753 761 if not batch:
754 762 continue
755 763
756 764 with remote.commandexecutor() as e:
757 765 args = {
758 766 b'revisions': [
759 767 {
760 768 b'type': b'changesetexplicit',
761 769 b'nodes': batch,
762 770 }
763 771 ],
764 772 b'fields': fields,
765 773 b'haveparents': haveparents,
766 774 }
767 775
768 776 if pathfilter:
769 777 args[b'pathfilter'] = pathfilter
770 778
771 779 objs = e.callcommand(b'filesdata', args).result()
772 780
773 781 # First object is an overall header.
774 782 overall = next(objs)
775 783
776 784 # We have overall['totalpaths'] segments.
777 785 for i in pycompat.xrange(overall[b'totalpaths']):
778 786 header = next(objs)
779 787
780 788 path = header[b'path']
781 789 store = repo.file(path)
782 790
783 791 linkrevs = {
784 792 fnode: manlinkrevs[mnode]
785 793 for fnode, mnode in pycompat.iteritems(fnodes[path])
786 794 }
787 795
788 796 def getlinkrev(node):
789 797 if node in linkrevs:
790 798 return linkrevs[node]
791 799 else:
792 800 return clrev(node)
793 801
794 802 store.addgroup(
795 803 iterrevisions(objs, header[b'totalitems'], progress),
796 804 getlinkrev,
797 805 weakref.proxy(tr),
798 806 maybemissingparents=shallow,
799 807 )
@@ -1,3702 +1,3709 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 functools
12 12 import os
13 13 import random
14 14 import sys
15 15 import time
16 16 import weakref
17 17
18 18 from .i18n import _
19 19 from .node import (
20 20 bin,
21 21 hex,
22 22 nullid,
23 23 nullrev,
24 24 short,
25 25 )
26 26 from .pycompat import (
27 27 delattr,
28 28 getattr,
29 29 )
30 30 from . import (
31 31 bookmarks,
32 32 branchmap,
33 33 bundle2,
34 34 bundlecaches,
35 35 changegroup,
36 36 color,
37 37 commit,
38 38 context,
39 39 dirstate,
40 40 dirstateguard,
41 41 discovery,
42 42 encoding,
43 43 error,
44 44 exchange,
45 45 extensions,
46 46 filelog,
47 47 hook,
48 48 lock as lockmod,
49 49 match as matchmod,
50 50 mergestate as mergestatemod,
51 51 mergeutil,
52 52 namespaces,
53 53 narrowspec,
54 54 obsolete,
55 55 pathutil,
56 56 phases,
57 57 pushkey,
58 58 pycompat,
59 59 rcutil,
60 60 repoview,
61 61 requirements as requirementsmod,
62 62 revlog,
63 63 revset,
64 64 revsetlang,
65 65 scmutil,
66 66 sparse,
67 67 store as storemod,
68 68 subrepoutil,
69 69 tags as tagsmod,
70 70 transaction,
71 71 txnutil,
72 72 util,
73 73 vfs as vfsmod,
74 74 )
75 75
76 76 from .interfaces import (
77 77 repository,
78 78 util as interfaceutil,
79 79 )
80 80
81 81 from .utils import (
82 82 hashutil,
83 83 procutil,
84 84 stringutil,
85 85 )
86 86
87 87 from .revlogutils import (
88 88 concurrency_checker as revlogchecker,
89 89 constants as revlogconst,
90 90 )
91 91
92 92 release = lockmod.release
93 93 urlerr = util.urlerr
94 94 urlreq = util.urlreq
95 95
96 96 # set of (path, vfs-location) tuples. vfs-location is:
97 97 # - 'plain for vfs relative paths
98 98 # - '' for svfs relative paths
99 99 _cachedfiles = set()
100 100
101 101
102 102 class _basefilecache(scmutil.filecache):
103 103 """All filecache usage on repo are done for logic that should be unfiltered"""
104 104
105 105 def __get__(self, repo, type=None):
106 106 if repo is None:
107 107 return self
108 108 # proxy to unfiltered __dict__ since filtered repo has no entry
109 109 unfi = repo.unfiltered()
110 110 try:
111 111 return unfi.__dict__[self.sname]
112 112 except KeyError:
113 113 pass
114 114 return super(_basefilecache, self).__get__(unfi, type)
115 115
116 116 def set(self, repo, value):
117 117 return super(_basefilecache, self).set(repo.unfiltered(), value)
118 118
119 119
120 120 class repofilecache(_basefilecache):
121 121 """filecache for files in .hg but outside of .hg/store"""
122 122
123 123 def __init__(self, *paths):
124 124 super(repofilecache, self).__init__(*paths)
125 125 for path in paths:
126 126 _cachedfiles.add((path, b'plain'))
127 127
128 128 def join(self, obj, fname):
129 129 return obj.vfs.join(fname)
130 130
131 131
132 132 class storecache(_basefilecache):
133 133 """filecache for files in the store"""
134 134
135 135 def __init__(self, *paths):
136 136 super(storecache, self).__init__(*paths)
137 137 for path in paths:
138 138 _cachedfiles.add((path, b''))
139 139
140 140 def join(self, obj, fname):
141 141 return obj.sjoin(fname)
142 142
143 143
144 144 class mixedrepostorecache(_basefilecache):
145 145 """filecache for a mix files in .hg/store and outside"""
146 146
147 147 def __init__(self, *pathsandlocations):
148 148 # scmutil.filecache only uses the path for passing back into our
149 149 # join(), so we can safely pass a list of paths and locations
150 150 super(mixedrepostorecache, self).__init__(*pathsandlocations)
151 151 _cachedfiles.update(pathsandlocations)
152 152
153 153 def join(self, obj, fnameandlocation):
154 154 fname, location = fnameandlocation
155 155 if location == b'plain':
156 156 return obj.vfs.join(fname)
157 157 else:
158 158 if location != b'':
159 159 raise error.ProgrammingError(
160 160 b'unexpected location: %s' % location
161 161 )
162 162 return obj.sjoin(fname)
163 163
164 164
165 165 def isfilecached(repo, name):
166 166 """check if a repo has already cached "name" filecache-ed property
167 167
168 168 This returns (cachedobj-or-None, iscached) tuple.
169 169 """
170 170 cacheentry = repo.unfiltered()._filecache.get(name, None)
171 171 if not cacheentry:
172 172 return None, False
173 173 return cacheentry.obj, True
174 174
175 175
176 176 class unfilteredpropertycache(util.propertycache):
177 177 """propertycache that apply to unfiltered repo only"""
178 178
179 179 def __get__(self, repo, type=None):
180 180 unfi = repo.unfiltered()
181 181 if unfi is repo:
182 182 return super(unfilteredpropertycache, self).__get__(unfi)
183 183 return getattr(unfi, self.name)
184 184
185 185
186 186 class filteredpropertycache(util.propertycache):
187 187 """propertycache that must take filtering in account"""
188 188
189 189 def cachevalue(self, obj, value):
190 190 object.__setattr__(obj, self.name, value)
191 191
192 192
193 193 def hasunfilteredcache(repo, name):
194 194 """check if a repo has an unfilteredpropertycache value for <name>"""
195 195 return name in vars(repo.unfiltered())
196 196
197 197
198 198 def unfilteredmethod(orig):
199 199 """decorate method that always need to be run on unfiltered version"""
200 200
201 201 @functools.wraps(orig)
202 202 def wrapper(repo, *args, **kwargs):
203 203 return orig(repo.unfiltered(), *args, **kwargs)
204 204
205 205 return wrapper
206 206
207 207
208 208 moderncaps = {
209 209 b'lookup',
210 210 b'branchmap',
211 211 b'pushkey',
212 212 b'known',
213 213 b'getbundle',
214 214 b'unbundle',
215 215 }
216 216 legacycaps = moderncaps.union({b'changegroupsubset'})
217 217
218 218
219 219 @interfaceutil.implementer(repository.ipeercommandexecutor)
220 220 class localcommandexecutor(object):
221 221 def __init__(self, peer):
222 222 self._peer = peer
223 223 self._sent = False
224 224 self._closed = False
225 225
226 226 def __enter__(self):
227 227 return self
228 228
229 229 def __exit__(self, exctype, excvalue, exctb):
230 230 self.close()
231 231
232 232 def callcommand(self, command, args):
233 233 if self._sent:
234 234 raise error.ProgrammingError(
235 235 b'callcommand() cannot be used after sendcommands()'
236 236 )
237 237
238 238 if self._closed:
239 239 raise error.ProgrammingError(
240 240 b'callcommand() cannot be used after close()'
241 241 )
242 242
243 243 # We don't need to support anything fancy. Just call the named
244 244 # method on the peer and return a resolved future.
245 245 fn = getattr(self._peer, pycompat.sysstr(command))
246 246
247 247 f = pycompat.futures.Future()
248 248
249 249 try:
250 250 result = fn(**pycompat.strkwargs(args))
251 251 except Exception:
252 252 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
253 253 else:
254 254 f.set_result(result)
255 255
256 256 return f
257 257
258 258 def sendcommands(self):
259 259 self._sent = True
260 260
261 261 def close(self):
262 262 self._closed = True
263 263
264 264
265 265 @interfaceutil.implementer(repository.ipeercommands)
266 266 class localpeer(repository.peer):
267 267 '''peer for a local repo; reflects only the most recent API'''
268 268
269 269 def __init__(self, repo, caps=None):
270 270 super(localpeer, self).__init__()
271 271
272 272 if caps is None:
273 273 caps = moderncaps.copy()
274 274 self._repo = repo.filtered(b'served')
275 275 self.ui = repo.ui
276 276 self._caps = repo._restrictcapabilities(caps)
277 277
278 278 # Begin of _basepeer interface.
279 279
280 280 def url(self):
281 281 return self._repo.url()
282 282
283 283 def local(self):
284 284 return self._repo
285 285
286 286 def peer(self):
287 287 return self
288 288
289 289 def canpush(self):
290 290 return True
291 291
292 292 def close(self):
293 293 self._repo.close()
294 294
295 295 # End of _basepeer interface.
296 296
297 297 # Begin of _basewirecommands interface.
298 298
299 299 def branchmap(self):
300 300 return self._repo.branchmap()
301 301
302 302 def capabilities(self):
303 303 return self._caps
304 304
305 305 def clonebundles(self):
306 306 return self._repo.tryread(bundlecaches.CB_MANIFEST_FILE)
307 307
308 308 def debugwireargs(self, one, two, three=None, four=None, five=None):
309 309 """Used to test argument passing over the wire"""
310 310 return b"%s %s %s %s %s" % (
311 311 one,
312 312 two,
313 313 pycompat.bytestr(three),
314 314 pycompat.bytestr(four),
315 315 pycompat.bytestr(five),
316 316 )
317 317
318 318 def getbundle(
319 self, source, heads=None, common=None, bundlecaps=None, **kwargs
319 self,
320 source,
321 heads=None,
322 common=None,
323 bundlecaps=None,
324 remote_sidedata=None,
325 **kwargs
320 326 ):
321 327 chunks = exchange.getbundlechunks(
322 328 self._repo,
323 329 source,
324 330 heads=heads,
325 331 common=common,
326 332 bundlecaps=bundlecaps,
333 remote_sidedata=remote_sidedata,
327 334 **kwargs
328 335 )[1]
329 336 cb = util.chunkbuffer(chunks)
330 337
331 338 if exchange.bundle2requested(bundlecaps):
332 339 # When requesting a bundle2, getbundle returns a stream to make the
333 340 # wire level function happier. We need to build a proper object
334 341 # from it in local peer.
335 342 return bundle2.getunbundler(self.ui, cb)
336 343 else:
337 344 return changegroup.getunbundler(b'01', cb, None)
338 345
339 346 def heads(self):
340 347 return self._repo.heads()
341 348
342 349 def known(self, nodes):
343 350 return self._repo.known(nodes)
344 351
345 352 def listkeys(self, namespace):
346 353 return self._repo.listkeys(namespace)
347 354
348 355 def lookup(self, key):
349 356 return self._repo.lookup(key)
350 357
351 358 def pushkey(self, namespace, key, old, new):
352 359 return self._repo.pushkey(namespace, key, old, new)
353 360
354 361 def stream_out(self):
355 362 raise error.Abort(_(b'cannot perform stream clone against local peer'))
356 363
357 364 def unbundle(self, bundle, heads, url):
358 365 """apply a bundle on a repo
359 366
360 367 This function handles the repo locking itself."""
361 368 try:
362 369 try:
363 370 bundle = exchange.readbundle(self.ui, bundle, None)
364 371 ret = exchange.unbundle(self._repo, bundle, heads, b'push', url)
365 372 if util.safehasattr(ret, b'getchunks'):
366 373 # This is a bundle20 object, turn it into an unbundler.
367 374 # This little dance should be dropped eventually when the
368 375 # API is finally improved.
369 376 stream = util.chunkbuffer(ret.getchunks())
370 377 ret = bundle2.getunbundler(self.ui, stream)
371 378 return ret
372 379 except Exception as exc:
373 380 # If the exception contains output salvaged from a bundle2
374 381 # reply, we need to make sure it is printed before continuing
375 382 # to fail. So we build a bundle2 with such output and consume
376 383 # it directly.
377 384 #
378 385 # This is not very elegant but allows a "simple" solution for
379 386 # issue4594
380 387 output = getattr(exc, '_bundle2salvagedoutput', ())
381 388 if output:
382 389 bundler = bundle2.bundle20(self._repo.ui)
383 390 for out in output:
384 391 bundler.addpart(out)
385 392 stream = util.chunkbuffer(bundler.getchunks())
386 393 b = bundle2.getunbundler(self.ui, stream)
387 394 bundle2.processbundle(self._repo, b)
388 395 raise
389 396 except error.PushRaced as exc:
390 397 raise error.ResponseError(
391 398 _(b'push failed:'), stringutil.forcebytestr(exc)
392 399 )
393 400
394 401 # End of _basewirecommands interface.
395 402
396 403 # Begin of peer interface.
397 404
398 405 def commandexecutor(self):
399 406 return localcommandexecutor(self)
400 407
401 408 # End of peer interface.
402 409
403 410
404 411 @interfaceutil.implementer(repository.ipeerlegacycommands)
405 412 class locallegacypeer(localpeer):
406 413 """peer extension which implements legacy methods too; used for tests with
407 414 restricted capabilities"""
408 415
409 416 def __init__(self, repo):
410 417 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
411 418
412 419 # Begin of baselegacywirecommands interface.
413 420
414 421 def between(self, pairs):
415 422 return self._repo.between(pairs)
416 423
417 424 def branches(self, nodes):
418 425 return self._repo.branches(nodes)
419 426
420 427 def changegroup(self, nodes, source):
421 428 outgoing = discovery.outgoing(
422 429 self._repo, missingroots=nodes, ancestorsof=self._repo.heads()
423 430 )
424 431 return changegroup.makechangegroup(self._repo, outgoing, b'01', source)
425 432
426 433 def changegroupsubset(self, bases, heads, source):
427 434 outgoing = discovery.outgoing(
428 435 self._repo, missingroots=bases, ancestorsof=heads
429 436 )
430 437 return changegroup.makechangegroup(self._repo, outgoing, b'01', source)
431 438
432 439 # End of baselegacywirecommands interface.
433 440
434 441
435 442 # Functions receiving (ui, features) that extensions can register to impact
436 443 # the ability to load repositories with custom requirements. Only
437 444 # functions defined in loaded extensions are called.
438 445 #
439 446 # The function receives a set of requirement strings that the repository
440 447 # is capable of opening. Functions will typically add elements to the
441 448 # set to reflect that the extension knows how to handle that requirements.
442 449 featuresetupfuncs = set()
443 450
444 451
445 452 def _getsharedvfs(hgvfs, requirements):
446 453 """returns the vfs object pointing to root of shared source
447 454 repo for a shared repository
448 455
449 456 hgvfs is vfs pointing at .hg/ of current repo (shared one)
450 457 requirements is a set of requirements of current repo (shared one)
451 458 """
452 459 # The ``shared`` or ``relshared`` requirements indicate the
453 460 # store lives in the path contained in the ``.hg/sharedpath`` file.
454 461 # This is an absolute path for ``shared`` and relative to
455 462 # ``.hg/`` for ``relshared``.
456 463 sharedpath = hgvfs.read(b'sharedpath').rstrip(b'\n')
457 464 if requirementsmod.RELATIVE_SHARED_REQUIREMENT in requirements:
458 465 sharedpath = hgvfs.join(sharedpath)
459 466
460 467 sharedvfs = vfsmod.vfs(sharedpath, realpath=True)
461 468
462 469 if not sharedvfs.exists():
463 470 raise error.RepoError(
464 471 _(b'.hg/sharedpath points to nonexistent directory %s')
465 472 % sharedvfs.base
466 473 )
467 474 return sharedvfs
468 475
469 476
470 477 def _readrequires(vfs, allowmissing):
471 478 """reads the require file present at root of this vfs
472 479 and return a set of requirements
473 480
474 481 If allowmissing is True, we suppress ENOENT if raised"""
475 482 # requires file contains a newline-delimited list of
476 483 # features/capabilities the opener (us) must have in order to use
477 484 # the repository. This file was introduced in Mercurial 0.9.2,
478 485 # which means very old repositories may not have one. We assume
479 486 # a missing file translates to no requirements.
480 487 try:
481 488 requirements = set(vfs.read(b'requires').splitlines())
482 489 except IOError as e:
483 490 if not (allowmissing and e.errno == errno.ENOENT):
484 491 raise
485 492 requirements = set()
486 493 return requirements
487 494
488 495
489 496 def makelocalrepository(baseui, path, intents=None):
490 497 """Create a local repository object.
491 498
492 499 Given arguments needed to construct a local repository, this function
493 500 performs various early repository loading functionality (such as
494 501 reading the ``.hg/requires`` and ``.hg/hgrc`` files), validates that
495 502 the repository can be opened, derives a type suitable for representing
496 503 that repository, and returns an instance of it.
497 504
498 505 The returned object conforms to the ``repository.completelocalrepository``
499 506 interface.
500 507
501 508 The repository type is derived by calling a series of factory functions
502 509 for each aspect/interface of the final repository. These are defined by
503 510 ``REPO_INTERFACES``.
504 511
505 512 Each factory function is called to produce a type implementing a specific
506 513 interface. The cumulative list of returned types will be combined into a
507 514 new type and that type will be instantiated to represent the local
508 515 repository.
509 516
510 517 The factory functions each receive various state that may be consulted
511 518 as part of deriving a type.
512 519
513 520 Extensions should wrap these factory functions to customize repository type
514 521 creation. Note that an extension's wrapped function may be called even if
515 522 that extension is not loaded for the repo being constructed. Extensions
516 523 should check if their ``__name__`` appears in the
517 524 ``extensionmodulenames`` set passed to the factory function and no-op if
518 525 not.
519 526 """
520 527 ui = baseui.copy()
521 528 # Prevent copying repo configuration.
522 529 ui.copy = baseui.copy
523 530
524 531 # Working directory VFS rooted at repository root.
525 532 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
526 533
527 534 # Main VFS for .hg/ directory.
528 535 hgpath = wdirvfs.join(b'.hg')
529 536 hgvfs = vfsmod.vfs(hgpath, cacheaudited=True)
530 537 # Whether this repository is shared one or not
531 538 shared = False
532 539 # If this repository is shared, vfs pointing to shared repo
533 540 sharedvfs = None
534 541
535 542 # The .hg/ path should exist and should be a directory. All other
536 543 # cases are errors.
537 544 if not hgvfs.isdir():
538 545 try:
539 546 hgvfs.stat()
540 547 except OSError as e:
541 548 if e.errno != errno.ENOENT:
542 549 raise
543 550 except ValueError as e:
544 551 # Can be raised on Python 3.8 when path is invalid.
545 552 raise error.Abort(
546 553 _(b'invalid path %s: %s') % (path, stringutil.forcebytestr(e))
547 554 )
548 555
549 556 raise error.RepoError(_(b'repository %s not found') % path)
550 557
551 558 requirements = _readrequires(hgvfs, True)
552 559 shared = (
553 560 requirementsmod.SHARED_REQUIREMENT in requirements
554 561 or requirementsmod.RELATIVE_SHARED_REQUIREMENT in requirements
555 562 )
556 563 storevfs = None
557 564 if shared:
558 565 # This is a shared repo
559 566 sharedvfs = _getsharedvfs(hgvfs, requirements)
560 567 storevfs = vfsmod.vfs(sharedvfs.join(b'store'))
561 568 else:
562 569 storevfs = vfsmod.vfs(hgvfs.join(b'store'))
563 570
564 571 # if .hg/requires contains the sharesafe requirement, it means
565 572 # there exists a `.hg/store/requires` too and we should read it
566 573 # NOTE: presence of SHARESAFE_REQUIREMENT imply that store requirement
567 574 # is present. We never write SHARESAFE_REQUIREMENT for a repo if store
568 575 # is not present, refer checkrequirementscompat() for that
569 576 #
570 577 # However, if SHARESAFE_REQUIREMENT is not present, it means that the
571 578 # repository was shared the old way. We check the share source .hg/requires
572 579 # for SHARESAFE_REQUIREMENT to detect whether the current repository needs
573 580 # to be reshared
574 581 hint = _(b"see `hg help config.format.use-share-safe` for more information")
575 582 if requirementsmod.SHARESAFE_REQUIREMENT in requirements:
576 583
577 584 if (
578 585 shared
579 586 and requirementsmod.SHARESAFE_REQUIREMENT
580 587 not in _readrequires(sharedvfs, True)
581 588 ):
582 589 mismatch_warn = ui.configbool(
583 590 b'share', b'safe-mismatch.source-not-safe.warn'
584 591 )
585 592 mismatch_config = ui.config(
586 593 b'share', b'safe-mismatch.source-not-safe'
587 594 )
588 595 if mismatch_config in (
589 596 b'downgrade-allow',
590 597 b'allow',
591 598 b'downgrade-abort',
592 599 ):
593 600 # prevent cyclic import localrepo -> upgrade -> localrepo
594 601 from . import upgrade
595 602
596 603 upgrade.downgrade_share_to_non_safe(
597 604 ui,
598 605 hgvfs,
599 606 sharedvfs,
600 607 requirements,
601 608 mismatch_config,
602 609 mismatch_warn,
603 610 )
604 611 elif mismatch_config == b'abort':
605 612 raise error.Abort(
606 613 _(b"share source does not support share-safe requirement"),
607 614 hint=hint,
608 615 )
609 616 else:
610 617 raise error.Abort(
611 618 _(
612 619 b"share-safe mismatch with source.\nUnrecognized"
613 620 b" value '%s' of `share.safe-mismatch.source-not-safe`"
614 621 b" set."
615 622 )
616 623 % mismatch_config,
617 624 hint=hint,
618 625 )
619 626 else:
620 627 requirements |= _readrequires(storevfs, False)
621 628 elif shared:
622 629 sourcerequires = _readrequires(sharedvfs, False)
623 630 if requirementsmod.SHARESAFE_REQUIREMENT in sourcerequires:
624 631 mismatch_config = ui.config(b'share', b'safe-mismatch.source-safe')
625 632 mismatch_warn = ui.configbool(
626 633 b'share', b'safe-mismatch.source-safe.warn'
627 634 )
628 635 if mismatch_config in (
629 636 b'upgrade-allow',
630 637 b'allow',
631 638 b'upgrade-abort',
632 639 ):
633 640 # prevent cyclic import localrepo -> upgrade -> localrepo
634 641 from . import upgrade
635 642
636 643 upgrade.upgrade_share_to_safe(
637 644 ui,
638 645 hgvfs,
639 646 storevfs,
640 647 requirements,
641 648 mismatch_config,
642 649 mismatch_warn,
643 650 )
644 651 elif mismatch_config == b'abort':
645 652 raise error.Abort(
646 653 _(
647 654 b'version mismatch: source uses share-safe'
648 655 b' functionality while the current share does not'
649 656 ),
650 657 hint=hint,
651 658 )
652 659 else:
653 660 raise error.Abort(
654 661 _(
655 662 b"share-safe mismatch with source.\nUnrecognized"
656 663 b" value '%s' of `share.safe-mismatch.source-safe` set."
657 664 )
658 665 % mismatch_config,
659 666 hint=hint,
660 667 )
661 668
662 669 # The .hg/hgrc file may load extensions or contain config options
663 670 # that influence repository construction. Attempt to load it and
664 671 # process any new extensions that it may have pulled in.
665 672 if loadhgrc(ui, wdirvfs, hgvfs, requirements, sharedvfs):
666 673 afterhgrcload(ui, wdirvfs, hgvfs, requirements)
667 674 extensions.loadall(ui)
668 675 extensions.populateui(ui)
669 676
670 677 # Set of module names of extensions loaded for this repository.
671 678 extensionmodulenames = {m.__name__ for n, m in extensions.extensions(ui)}
672 679
673 680 supportedrequirements = gathersupportedrequirements(ui)
674 681
675 682 # We first validate the requirements are known.
676 683 ensurerequirementsrecognized(requirements, supportedrequirements)
677 684
678 685 # Then we validate that the known set is reasonable to use together.
679 686 ensurerequirementscompatible(ui, requirements)
680 687
681 688 # TODO there are unhandled edge cases related to opening repositories with
682 689 # shared storage. If storage is shared, we should also test for requirements
683 690 # compatibility in the pointed-to repo. This entails loading the .hg/hgrc in
684 691 # that repo, as that repo may load extensions needed to open it. This is a
685 692 # bit complicated because we don't want the other hgrc to overwrite settings
686 693 # in this hgrc.
687 694 #
688 695 # This bug is somewhat mitigated by the fact that we copy the .hg/requires
689 696 # file when sharing repos. But if a requirement is added after the share is
690 697 # performed, thereby introducing a new requirement for the opener, we may
691 698 # will not see that and could encounter a run-time error interacting with
692 699 # that shared store since it has an unknown-to-us requirement.
693 700
694 701 # At this point, we know we should be capable of opening the repository.
695 702 # Now get on with doing that.
696 703
697 704 features = set()
698 705
699 706 # The "store" part of the repository holds versioned data. How it is
700 707 # accessed is determined by various requirements. If `shared` or
701 708 # `relshared` requirements are present, this indicates current repository
702 709 # is a share and store exists in path mentioned in `.hg/sharedpath`
703 710 if shared:
704 711 storebasepath = sharedvfs.base
705 712 cachepath = sharedvfs.join(b'cache')
706 713 features.add(repository.REPO_FEATURE_SHARED_STORAGE)
707 714 else:
708 715 storebasepath = hgvfs.base
709 716 cachepath = hgvfs.join(b'cache')
710 717 wcachepath = hgvfs.join(b'wcache')
711 718
712 719 # The store has changed over time and the exact layout is dictated by
713 720 # requirements. The store interface abstracts differences across all
714 721 # of them.
715 722 store = makestore(
716 723 requirements,
717 724 storebasepath,
718 725 lambda base: vfsmod.vfs(base, cacheaudited=True),
719 726 )
720 727 hgvfs.createmode = store.createmode
721 728
722 729 storevfs = store.vfs
723 730 storevfs.options = resolvestorevfsoptions(ui, requirements, features)
724 731
725 732 # The cache vfs is used to manage cache files.
726 733 cachevfs = vfsmod.vfs(cachepath, cacheaudited=True)
727 734 cachevfs.createmode = store.createmode
728 735 # The cache vfs is used to manage cache files related to the working copy
729 736 wcachevfs = vfsmod.vfs(wcachepath, cacheaudited=True)
730 737 wcachevfs.createmode = store.createmode
731 738
732 739 # Now resolve the type for the repository object. We do this by repeatedly
733 740 # calling a factory function to produces types for specific aspects of the
734 741 # repo's operation. The aggregate returned types are used as base classes
735 742 # for a dynamically-derived type, which will represent our new repository.
736 743
737 744 bases = []
738 745 extrastate = {}
739 746
740 747 for iface, fn in REPO_INTERFACES:
741 748 # We pass all potentially useful state to give extensions tons of
742 749 # flexibility.
743 750 typ = fn()(
744 751 ui=ui,
745 752 intents=intents,
746 753 requirements=requirements,
747 754 features=features,
748 755 wdirvfs=wdirvfs,
749 756 hgvfs=hgvfs,
750 757 store=store,
751 758 storevfs=storevfs,
752 759 storeoptions=storevfs.options,
753 760 cachevfs=cachevfs,
754 761 wcachevfs=wcachevfs,
755 762 extensionmodulenames=extensionmodulenames,
756 763 extrastate=extrastate,
757 764 baseclasses=bases,
758 765 )
759 766
760 767 if not isinstance(typ, type):
761 768 raise error.ProgrammingError(
762 769 b'unable to construct type for %s' % iface
763 770 )
764 771
765 772 bases.append(typ)
766 773
767 774 # type() allows you to use characters in type names that wouldn't be
768 775 # recognized as Python symbols in source code. We abuse that to add
769 776 # rich information about our constructed repo.
770 777 name = pycompat.sysstr(
771 778 b'derivedrepo:%s<%s>' % (wdirvfs.base, b','.join(sorted(requirements)))
772 779 )
773 780
774 781 cls = type(name, tuple(bases), {})
775 782
776 783 return cls(
777 784 baseui=baseui,
778 785 ui=ui,
779 786 origroot=path,
780 787 wdirvfs=wdirvfs,
781 788 hgvfs=hgvfs,
782 789 requirements=requirements,
783 790 supportedrequirements=supportedrequirements,
784 791 sharedpath=storebasepath,
785 792 store=store,
786 793 cachevfs=cachevfs,
787 794 wcachevfs=wcachevfs,
788 795 features=features,
789 796 intents=intents,
790 797 )
791 798
792 799
793 800 def loadhgrc(ui, wdirvfs, hgvfs, requirements, sharedvfs=None):
794 801 """Load hgrc files/content into a ui instance.
795 802
796 803 This is called during repository opening to load any additional
797 804 config files or settings relevant to the current repository.
798 805
799 806 Returns a bool indicating whether any additional configs were loaded.
800 807
801 808 Extensions should monkeypatch this function to modify how per-repo
802 809 configs are loaded. For example, an extension may wish to pull in
803 810 configs from alternate files or sources.
804 811
805 812 sharedvfs is vfs object pointing to source repo if the current one is a
806 813 shared one
807 814 """
808 815 if not rcutil.use_repo_hgrc():
809 816 return False
810 817
811 818 ret = False
812 819 # first load config from shared source if we has to
813 820 if requirementsmod.SHARESAFE_REQUIREMENT in requirements and sharedvfs:
814 821 try:
815 822 ui.readconfig(sharedvfs.join(b'hgrc'), root=sharedvfs.base)
816 823 ret = True
817 824 except IOError:
818 825 pass
819 826
820 827 try:
821 828 ui.readconfig(hgvfs.join(b'hgrc'), root=wdirvfs.base)
822 829 ret = True
823 830 except IOError:
824 831 pass
825 832
826 833 try:
827 834 ui.readconfig(hgvfs.join(b'hgrc-not-shared'), root=wdirvfs.base)
828 835 ret = True
829 836 except IOError:
830 837 pass
831 838
832 839 return ret
833 840
834 841
835 842 def afterhgrcload(ui, wdirvfs, hgvfs, requirements):
836 843 """Perform additional actions after .hg/hgrc is loaded.
837 844
838 845 This function is called during repository loading immediately after
839 846 the .hg/hgrc file is loaded and before per-repo extensions are loaded.
840 847
841 848 The function can be used to validate configs, automatically add
842 849 options (including extensions) based on requirements, etc.
843 850 """
844 851
845 852 # Map of requirements to list of extensions to load automatically when
846 853 # requirement is present.
847 854 autoextensions = {
848 855 b'git': [b'git'],
849 856 b'largefiles': [b'largefiles'],
850 857 b'lfs': [b'lfs'],
851 858 }
852 859
853 860 for requirement, names in sorted(autoextensions.items()):
854 861 if requirement not in requirements:
855 862 continue
856 863
857 864 for name in names:
858 865 if not ui.hasconfig(b'extensions', name):
859 866 ui.setconfig(b'extensions', name, b'', source=b'autoload')
860 867
861 868
862 869 def gathersupportedrequirements(ui):
863 870 """Determine the complete set of recognized requirements."""
864 871 # Start with all requirements supported by this file.
865 872 supported = set(localrepository._basesupported)
866 873
867 874 # Execute ``featuresetupfuncs`` entries if they belong to an extension
868 875 # relevant to this ui instance.
869 876 modules = {m.__name__ for n, m in extensions.extensions(ui)}
870 877
871 878 for fn in featuresetupfuncs:
872 879 if fn.__module__ in modules:
873 880 fn(ui, supported)
874 881
875 882 # Add derived requirements from registered compression engines.
876 883 for name in util.compengines:
877 884 engine = util.compengines[name]
878 885 if engine.available() and engine.revlogheader():
879 886 supported.add(b'exp-compression-%s' % name)
880 887 if engine.name() == b'zstd':
881 888 supported.add(b'revlog-compression-zstd')
882 889
883 890 return supported
884 891
885 892
886 893 def ensurerequirementsrecognized(requirements, supported):
887 894 """Validate that a set of local requirements is recognized.
888 895
889 896 Receives a set of requirements. Raises an ``error.RepoError`` if there
890 897 exists any requirement in that set that currently loaded code doesn't
891 898 recognize.
892 899
893 900 Returns a set of supported requirements.
894 901 """
895 902 missing = set()
896 903
897 904 for requirement in requirements:
898 905 if requirement in supported:
899 906 continue
900 907
901 908 if not requirement or not requirement[0:1].isalnum():
902 909 raise error.RequirementError(_(b'.hg/requires file is corrupt'))
903 910
904 911 missing.add(requirement)
905 912
906 913 if missing:
907 914 raise error.RequirementError(
908 915 _(b'repository requires features unknown to this Mercurial: %s')
909 916 % b' '.join(sorted(missing)),
910 917 hint=_(
911 918 b'see https://mercurial-scm.org/wiki/MissingRequirement '
912 919 b'for more information'
913 920 ),
914 921 )
915 922
916 923
917 924 def ensurerequirementscompatible(ui, requirements):
918 925 """Validates that a set of recognized requirements is mutually compatible.
919 926
920 927 Some requirements may not be compatible with others or require
921 928 config options that aren't enabled. This function is called during
922 929 repository opening to ensure that the set of requirements needed
923 930 to open a repository is sane and compatible with config options.
924 931
925 932 Extensions can monkeypatch this function to perform additional
926 933 checking.
927 934
928 935 ``error.RepoError`` should be raised on failure.
929 936 """
930 937 if (
931 938 requirementsmod.SPARSE_REQUIREMENT in requirements
932 939 and not sparse.enabled
933 940 ):
934 941 raise error.RepoError(
935 942 _(
936 943 b'repository is using sparse feature but '
937 944 b'sparse is not enabled; enable the '
938 945 b'"sparse" extensions to access'
939 946 )
940 947 )
941 948
942 949
943 950 def makestore(requirements, path, vfstype):
944 951 """Construct a storage object for a repository."""
945 952 if requirementsmod.STORE_REQUIREMENT in requirements:
946 953 if requirementsmod.FNCACHE_REQUIREMENT in requirements:
947 954 dotencode = requirementsmod.DOTENCODE_REQUIREMENT in requirements
948 955 return storemod.fncachestore(path, vfstype, dotencode)
949 956
950 957 return storemod.encodedstore(path, vfstype)
951 958
952 959 return storemod.basicstore(path, vfstype)
953 960
954 961
955 962 def resolvestorevfsoptions(ui, requirements, features):
956 963 """Resolve the options to pass to the store vfs opener.
957 964
958 965 The returned dict is used to influence behavior of the storage layer.
959 966 """
960 967 options = {}
961 968
962 969 if requirementsmod.TREEMANIFEST_REQUIREMENT in requirements:
963 970 options[b'treemanifest'] = True
964 971
965 972 # experimental config: format.manifestcachesize
966 973 manifestcachesize = ui.configint(b'format', b'manifestcachesize')
967 974 if manifestcachesize is not None:
968 975 options[b'manifestcachesize'] = manifestcachesize
969 976
970 977 # In the absence of another requirement superseding a revlog-related
971 978 # requirement, we have to assume the repo is using revlog version 0.
972 979 # This revlog format is super old and we don't bother trying to parse
973 980 # opener options for it because those options wouldn't do anything
974 981 # meaningful on such old repos.
975 982 if (
976 983 requirementsmod.REVLOGV1_REQUIREMENT in requirements
977 984 or requirementsmod.REVLOGV2_REQUIREMENT in requirements
978 985 ):
979 986 options.update(resolverevlogstorevfsoptions(ui, requirements, features))
980 987 else: # explicitly mark repo as using revlogv0
981 988 options[b'revlogv0'] = True
982 989
983 990 if requirementsmod.COPIESSDC_REQUIREMENT in requirements:
984 991 options[b'copies-storage'] = b'changeset-sidedata'
985 992 else:
986 993 writecopiesto = ui.config(b'experimental', b'copies.write-to')
987 994 copiesextramode = (b'changeset-only', b'compatibility')
988 995 if writecopiesto in copiesextramode:
989 996 options[b'copies-storage'] = b'extra'
990 997
991 998 return options
992 999
993 1000
994 1001 def resolverevlogstorevfsoptions(ui, requirements, features):
995 1002 """Resolve opener options specific to revlogs."""
996 1003
997 1004 options = {}
998 1005 options[b'flagprocessors'] = {}
999 1006
1000 1007 if requirementsmod.REVLOGV1_REQUIREMENT in requirements:
1001 1008 options[b'revlogv1'] = True
1002 1009 if requirementsmod.REVLOGV2_REQUIREMENT in requirements:
1003 1010 options[b'revlogv2'] = True
1004 1011
1005 1012 if requirementsmod.GENERALDELTA_REQUIREMENT in requirements:
1006 1013 options[b'generaldelta'] = True
1007 1014
1008 1015 # experimental config: format.chunkcachesize
1009 1016 chunkcachesize = ui.configint(b'format', b'chunkcachesize')
1010 1017 if chunkcachesize is not None:
1011 1018 options[b'chunkcachesize'] = chunkcachesize
1012 1019
1013 1020 deltabothparents = ui.configbool(
1014 1021 b'storage', b'revlog.optimize-delta-parent-choice'
1015 1022 )
1016 1023 options[b'deltabothparents'] = deltabothparents
1017 1024
1018 1025 lazydelta = ui.configbool(b'storage', b'revlog.reuse-external-delta')
1019 1026 lazydeltabase = False
1020 1027 if lazydelta:
1021 1028 lazydeltabase = ui.configbool(
1022 1029 b'storage', b'revlog.reuse-external-delta-parent'
1023 1030 )
1024 1031 if lazydeltabase is None:
1025 1032 lazydeltabase = not scmutil.gddeltaconfig(ui)
1026 1033 options[b'lazydelta'] = lazydelta
1027 1034 options[b'lazydeltabase'] = lazydeltabase
1028 1035
1029 1036 chainspan = ui.configbytes(b'experimental', b'maxdeltachainspan')
1030 1037 if 0 <= chainspan:
1031 1038 options[b'maxdeltachainspan'] = chainspan
1032 1039
1033 1040 mmapindexthreshold = ui.configbytes(b'experimental', b'mmapindexthreshold')
1034 1041 if mmapindexthreshold is not None:
1035 1042 options[b'mmapindexthreshold'] = mmapindexthreshold
1036 1043
1037 1044 withsparseread = ui.configbool(b'experimental', b'sparse-read')
1038 1045 srdensitythres = float(
1039 1046 ui.config(b'experimental', b'sparse-read.density-threshold')
1040 1047 )
1041 1048 srmingapsize = ui.configbytes(b'experimental', b'sparse-read.min-gap-size')
1042 1049 options[b'with-sparse-read'] = withsparseread
1043 1050 options[b'sparse-read-density-threshold'] = srdensitythres
1044 1051 options[b'sparse-read-min-gap-size'] = srmingapsize
1045 1052
1046 1053 sparserevlog = requirementsmod.SPARSEREVLOG_REQUIREMENT in requirements
1047 1054 options[b'sparse-revlog'] = sparserevlog
1048 1055 if sparserevlog:
1049 1056 options[b'generaldelta'] = True
1050 1057
1051 1058 sidedata = requirementsmod.SIDEDATA_REQUIREMENT in requirements
1052 1059 options[b'side-data'] = sidedata
1053 1060
1054 1061 maxchainlen = None
1055 1062 if sparserevlog:
1056 1063 maxchainlen = revlogconst.SPARSE_REVLOG_MAX_CHAIN_LENGTH
1057 1064 # experimental config: format.maxchainlen
1058 1065 maxchainlen = ui.configint(b'format', b'maxchainlen', maxchainlen)
1059 1066 if maxchainlen is not None:
1060 1067 options[b'maxchainlen'] = maxchainlen
1061 1068
1062 1069 for r in requirements:
1063 1070 # we allow multiple compression engine requirement to co-exist because
1064 1071 # strickly speaking, revlog seems to support mixed compression style.
1065 1072 #
1066 1073 # The compression used for new entries will be "the last one"
1067 1074 prefix = r.startswith
1068 1075 if prefix(b'revlog-compression-') or prefix(b'exp-compression-'):
1069 1076 options[b'compengine'] = r.split(b'-', 2)[2]
1070 1077
1071 1078 options[b'zlib.level'] = ui.configint(b'storage', b'revlog.zlib.level')
1072 1079 if options[b'zlib.level'] is not None:
1073 1080 if not (0 <= options[b'zlib.level'] <= 9):
1074 1081 msg = _(b'invalid value for `storage.revlog.zlib.level` config: %d')
1075 1082 raise error.Abort(msg % options[b'zlib.level'])
1076 1083 options[b'zstd.level'] = ui.configint(b'storage', b'revlog.zstd.level')
1077 1084 if options[b'zstd.level'] is not None:
1078 1085 if not (0 <= options[b'zstd.level'] <= 22):
1079 1086 msg = _(b'invalid value for `storage.revlog.zstd.level` config: %d')
1080 1087 raise error.Abort(msg % options[b'zstd.level'])
1081 1088
1082 1089 if requirementsmod.NARROW_REQUIREMENT in requirements:
1083 1090 options[b'enableellipsis'] = True
1084 1091
1085 1092 if ui.configbool(b'experimental', b'rust.index'):
1086 1093 options[b'rust.index'] = True
1087 1094 if requirementsmod.NODEMAP_REQUIREMENT in requirements:
1088 1095 slow_path = ui.config(
1089 1096 b'storage', b'revlog.persistent-nodemap.slow-path'
1090 1097 )
1091 1098 if slow_path not in (b'allow', b'warn', b'abort'):
1092 1099 default = ui.config_default(
1093 1100 b'storage', b'revlog.persistent-nodemap.slow-path'
1094 1101 )
1095 1102 msg = _(
1096 1103 b'unknown value for config '
1097 1104 b'"storage.revlog.persistent-nodemap.slow-path": "%s"\n'
1098 1105 )
1099 1106 ui.warn(msg % slow_path)
1100 1107 if not ui.quiet:
1101 1108 ui.warn(_(b'falling back to default value: %s\n') % default)
1102 1109 slow_path = default
1103 1110
1104 1111 msg = _(
1105 1112 b"accessing `persistent-nodemap` repository without associated "
1106 1113 b"fast implementation."
1107 1114 )
1108 1115 hint = _(
1109 1116 b"check `hg help config.format.use-persistent-nodemap` "
1110 1117 b"for details"
1111 1118 )
1112 1119 if not revlog.HAS_FAST_PERSISTENT_NODEMAP:
1113 1120 if slow_path == b'warn':
1114 1121 msg = b"warning: " + msg + b'\n'
1115 1122 ui.warn(msg)
1116 1123 if not ui.quiet:
1117 1124 hint = b'(' + hint + b')\n'
1118 1125 ui.warn(hint)
1119 1126 if slow_path == b'abort':
1120 1127 raise error.Abort(msg, hint=hint)
1121 1128 options[b'persistent-nodemap'] = True
1122 1129 if ui.configbool(b'storage', b'revlog.persistent-nodemap.mmap'):
1123 1130 options[b'persistent-nodemap.mmap'] = True
1124 1131 if ui.configbool(b'devel', b'persistent-nodemap'):
1125 1132 options[b'devel-force-nodemap'] = True
1126 1133
1127 1134 return options
1128 1135
1129 1136
1130 1137 def makemain(**kwargs):
1131 1138 """Produce a type conforming to ``ilocalrepositorymain``."""
1132 1139 return localrepository
1133 1140
1134 1141
1135 1142 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
1136 1143 class revlogfilestorage(object):
1137 1144 """File storage when using revlogs."""
1138 1145
1139 1146 def file(self, path):
1140 1147 if path.startswith(b'/'):
1141 1148 path = path[1:]
1142 1149
1143 1150 return filelog.filelog(self.svfs, path)
1144 1151
1145 1152
1146 1153 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
1147 1154 class revlognarrowfilestorage(object):
1148 1155 """File storage when using revlogs and narrow files."""
1149 1156
1150 1157 def file(self, path):
1151 1158 if path.startswith(b'/'):
1152 1159 path = path[1:]
1153 1160
1154 1161 return filelog.narrowfilelog(self.svfs, path, self._storenarrowmatch)
1155 1162
1156 1163
1157 1164 def makefilestorage(requirements, features, **kwargs):
1158 1165 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
1159 1166 features.add(repository.REPO_FEATURE_REVLOG_FILE_STORAGE)
1160 1167 features.add(repository.REPO_FEATURE_STREAM_CLONE)
1161 1168
1162 1169 if requirementsmod.NARROW_REQUIREMENT in requirements:
1163 1170 return revlognarrowfilestorage
1164 1171 else:
1165 1172 return revlogfilestorage
1166 1173
1167 1174
1168 1175 # List of repository interfaces and factory functions for them. Each
1169 1176 # will be called in order during ``makelocalrepository()`` to iteratively
1170 1177 # derive the final type for a local repository instance. We capture the
1171 1178 # function as a lambda so we don't hold a reference and the module-level
1172 1179 # functions can be wrapped.
1173 1180 REPO_INTERFACES = [
1174 1181 (repository.ilocalrepositorymain, lambda: makemain),
1175 1182 (repository.ilocalrepositoryfilestorage, lambda: makefilestorage),
1176 1183 ]
1177 1184
1178 1185
1179 1186 @interfaceutil.implementer(repository.ilocalrepositorymain)
1180 1187 class localrepository(object):
1181 1188 """Main class for representing local repositories.
1182 1189
1183 1190 All local repositories are instances of this class.
1184 1191
1185 1192 Constructed on its own, instances of this class are not usable as
1186 1193 repository objects. To obtain a usable repository object, call
1187 1194 ``hg.repository()``, ``localrepo.instance()``, or
1188 1195 ``localrepo.makelocalrepository()``. The latter is the lowest-level.
1189 1196 ``instance()`` adds support for creating new repositories.
1190 1197 ``hg.repository()`` adds more extension integration, including calling
1191 1198 ``reposetup()``. Generally speaking, ``hg.repository()`` should be
1192 1199 used.
1193 1200 """
1194 1201
1195 1202 # obsolete experimental requirements:
1196 1203 # - manifestv2: An experimental new manifest format that allowed
1197 1204 # for stem compression of long paths. Experiment ended up not
1198 1205 # being successful (repository sizes went up due to worse delta
1199 1206 # chains), and the code was deleted in 4.6.
1200 1207 supportedformats = {
1201 1208 requirementsmod.REVLOGV1_REQUIREMENT,
1202 1209 requirementsmod.GENERALDELTA_REQUIREMENT,
1203 1210 requirementsmod.TREEMANIFEST_REQUIREMENT,
1204 1211 requirementsmod.COPIESSDC_REQUIREMENT,
1205 1212 requirementsmod.REVLOGV2_REQUIREMENT,
1206 1213 requirementsmod.SIDEDATA_REQUIREMENT,
1207 1214 requirementsmod.SPARSEREVLOG_REQUIREMENT,
1208 1215 requirementsmod.NODEMAP_REQUIREMENT,
1209 1216 bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT,
1210 1217 requirementsmod.SHARESAFE_REQUIREMENT,
1211 1218 }
1212 1219 _basesupported = supportedformats | {
1213 1220 requirementsmod.STORE_REQUIREMENT,
1214 1221 requirementsmod.FNCACHE_REQUIREMENT,
1215 1222 requirementsmod.SHARED_REQUIREMENT,
1216 1223 requirementsmod.RELATIVE_SHARED_REQUIREMENT,
1217 1224 requirementsmod.DOTENCODE_REQUIREMENT,
1218 1225 requirementsmod.SPARSE_REQUIREMENT,
1219 1226 requirementsmod.INTERNAL_PHASE_REQUIREMENT,
1220 1227 }
1221 1228
1222 1229 # list of prefix for file which can be written without 'wlock'
1223 1230 # Extensions should extend this list when needed
1224 1231 _wlockfreeprefix = {
1225 1232 # We migh consider requiring 'wlock' for the next
1226 1233 # two, but pretty much all the existing code assume
1227 1234 # wlock is not needed so we keep them excluded for
1228 1235 # now.
1229 1236 b'hgrc',
1230 1237 b'requires',
1231 1238 # XXX cache is a complicatged business someone
1232 1239 # should investigate this in depth at some point
1233 1240 b'cache/',
1234 1241 # XXX shouldn't be dirstate covered by the wlock?
1235 1242 b'dirstate',
1236 1243 # XXX bisect was still a bit too messy at the time
1237 1244 # this changeset was introduced. Someone should fix
1238 1245 # the remainig bit and drop this line
1239 1246 b'bisect.state',
1240 1247 }
1241 1248
1242 1249 def __init__(
1243 1250 self,
1244 1251 baseui,
1245 1252 ui,
1246 1253 origroot,
1247 1254 wdirvfs,
1248 1255 hgvfs,
1249 1256 requirements,
1250 1257 supportedrequirements,
1251 1258 sharedpath,
1252 1259 store,
1253 1260 cachevfs,
1254 1261 wcachevfs,
1255 1262 features,
1256 1263 intents=None,
1257 1264 ):
1258 1265 """Create a new local repository instance.
1259 1266
1260 1267 Most callers should use ``hg.repository()``, ``localrepo.instance()``,
1261 1268 or ``localrepo.makelocalrepository()`` for obtaining a new repository
1262 1269 object.
1263 1270
1264 1271 Arguments:
1265 1272
1266 1273 baseui
1267 1274 ``ui.ui`` instance that ``ui`` argument was based off of.
1268 1275
1269 1276 ui
1270 1277 ``ui.ui`` instance for use by the repository.
1271 1278
1272 1279 origroot
1273 1280 ``bytes`` path to working directory root of this repository.
1274 1281
1275 1282 wdirvfs
1276 1283 ``vfs.vfs`` rooted at the working directory.
1277 1284
1278 1285 hgvfs
1279 1286 ``vfs.vfs`` rooted at .hg/
1280 1287
1281 1288 requirements
1282 1289 ``set`` of bytestrings representing repository opening requirements.
1283 1290
1284 1291 supportedrequirements
1285 1292 ``set`` of bytestrings representing repository requirements that we
1286 1293 know how to open. May be a supetset of ``requirements``.
1287 1294
1288 1295 sharedpath
1289 1296 ``bytes`` Defining path to storage base directory. Points to a
1290 1297 ``.hg/`` directory somewhere.
1291 1298
1292 1299 store
1293 1300 ``store.basicstore`` (or derived) instance providing access to
1294 1301 versioned storage.
1295 1302
1296 1303 cachevfs
1297 1304 ``vfs.vfs`` used for cache files.
1298 1305
1299 1306 wcachevfs
1300 1307 ``vfs.vfs`` used for cache files related to the working copy.
1301 1308
1302 1309 features
1303 1310 ``set`` of bytestrings defining features/capabilities of this
1304 1311 instance.
1305 1312
1306 1313 intents
1307 1314 ``set`` of system strings indicating what this repo will be used
1308 1315 for.
1309 1316 """
1310 1317 self.baseui = baseui
1311 1318 self.ui = ui
1312 1319 self.origroot = origroot
1313 1320 # vfs rooted at working directory.
1314 1321 self.wvfs = wdirvfs
1315 1322 self.root = wdirvfs.base
1316 1323 # vfs rooted at .hg/. Used to access most non-store paths.
1317 1324 self.vfs = hgvfs
1318 1325 self.path = hgvfs.base
1319 1326 self.requirements = requirements
1320 1327 self.supported = supportedrequirements
1321 1328 self.sharedpath = sharedpath
1322 1329 self.store = store
1323 1330 self.cachevfs = cachevfs
1324 1331 self.wcachevfs = wcachevfs
1325 1332 self.features = features
1326 1333
1327 1334 self.filtername = None
1328 1335
1329 1336 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
1330 1337 b'devel', b'check-locks'
1331 1338 ):
1332 1339 self.vfs.audit = self._getvfsward(self.vfs.audit)
1333 1340 # A list of callback to shape the phase if no data were found.
1334 1341 # Callback are in the form: func(repo, roots) --> processed root.
1335 1342 # This list it to be filled by extension during repo setup
1336 1343 self._phasedefaults = []
1337 1344
1338 1345 color.setup(self.ui)
1339 1346
1340 1347 self.spath = self.store.path
1341 1348 self.svfs = self.store.vfs
1342 1349 self.sjoin = self.store.join
1343 1350 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
1344 1351 b'devel', b'check-locks'
1345 1352 ):
1346 1353 if util.safehasattr(self.svfs, b'vfs'): # this is filtervfs
1347 1354 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
1348 1355 else: # standard vfs
1349 1356 self.svfs.audit = self._getsvfsward(self.svfs.audit)
1350 1357
1351 1358 self._dirstatevalidatewarned = False
1352 1359
1353 1360 self._branchcaches = branchmap.BranchMapCache()
1354 1361 self._revbranchcache = None
1355 1362 self._filterpats = {}
1356 1363 self._datafilters = {}
1357 1364 self._transref = self._lockref = self._wlockref = None
1358 1365
1359 1366 # A cache for various files under .hg/ that tracks file changes,
1360 1367 # (used by the filecache decorator)
1361 1368 #
1362 1369 # Maps a property name to its util.filecacheentry
1363 1370 self._filecache = {}
1364 1371
1365 1372 # hold sets of revision to be filtered
1366 1373 # should be cleared when something might have changed the filter value:
1367 1374 # - new changesets,
1368 1375 # - phase change,
1369 1376 # - new obsolescence marker,
1370 1377 # - working directory parent change,
1371 1378 # - bookmark changes
1372 1379 self.filteredrevcache = {}
1373 1380
1374 1381 # post-dirstate-status hooks
1375 1382 self._postdsstatus = []
1376 1383
1377 1384 # generic mapping between names and nodes
1378 1385 self.names = namespaces.namespaces()
1379 1386
1380 1387 # Key to signature value.
1381 1388 self._sparsesignaturecache = {}
1382 1389 # Signature to cached matcher instance.
1383 1390 self._sparsematchercache = {}
1384 1391
1385 1392 self._extrafilterid = repoview.extrafilter(ui)
1386 1393
1387 1394 self.filecopiesmode = None
1388 1395 if requirementsmod.COPIESSDC_REQUIREMENT in self.requirements:
1389 1396 self.filecopiesmode = b'changeset-sidedata'
1390 1397
1391 1398 def _getvfsward(self, origfunc):
1392 1399 """build a ward for self.vfs"""
1393 1400 rref = weakref.ref(self)
1394 1401
1395 1402 def checkvfs(path, mode=None):
1396 1403 ret = origfunc(path, mode=mode)
1397 1404 repo = rref()
1398 1405 if (
1399 1406 repo is None
1400 1407 or not util.safehasattr(repo, b'_wlockref')
1401 1408 or not util.safehasattr(repo, b'_lockref')
1402 1409 ):
1403 1410 return
1404 1411 if mode in (None, b'r', b'rb'):
1405 1412 return
1406 1413 if path.startswith(repo.path):
1407 1414 # truncate name relative to the repository (.hg)
1408 1415 path = path[len(repo.path) + 1 :]
1409 1416 if path.startswith(b'cache/'):
1410 1417 msg = b'accessing cache with vfs instead of cachevfs: "%s"'
1411 1418 repo.ui.develwarn(msg % path, stacklevel=3, config=b"cache-vfs")
1412 1419 # path prefixes covered by 'lock'
1413 1420 vfs_path_prefixes = (
1414 1421 b'journal.',
1415 1422 b'undo.',
1416 1423 b'strip-backup/',
1417 1424 b'cache/',
1418 1425 )
1419 1426 if any(path.startswith(prefix) for prefix in vfs_path_prefixes):
1420 1427 if repo._currentlock(repo._lockref) is None:
1421 1428 repo.ui.develwarn(
1422 1429 b'write with no lock: "%s"' % path,
1423 1430 stacklevel=3,
1424 1431 config=b'check-locks',
1425 1432 )
1426 1433 elif repo._currentlock(repo._wlockref) is None:
1427 1434 # rest of vfs files are covered by 'wlock'
1428 1435 #
1429 1436 # exclude special files
1430 1437 for prefix in self._wlockfreeprefix:
1431 1438 if path.startswith(prefix):
1432 1439 return
1433 1440 repo.ui.develwarn(
1434 1441 b'write with no wlock: "%s"' % path,
1435 1442 stacklevel=3,
1436 1443 config=b'check-locks',
1437 1444 )
1438 1445 return ret
1439 1446
1440 1447 return checkvfs
1441 1448
1442 1449 def _getsvfsward(self, origfunc):
1443 1450 """build a ward for self.svfs"""
1444 1451 rref = weakref.ref(self)
1445 1452
1446 1453 def checksvfs(path, mode=None):
1447 1454 ret = origfunc(path, mode=mode)
1448 1455 repo = rref()
1449 1456 if repo is None or not util.safehasattr(repo, b'_lockref'):
1450 1457 return
1451 1458 if mode in (None, b'r', b'rb'):
1452 1459 return
1453 1460 if path.startswith(repo.sharedpath):
1454 1461 # truncate name relative to the repository (.hg)
1455 1462 path = path[len(repo.sharedpath) + 1 :]
1456 1463 if repo._currentlock(repo._lockref) is None:
1457 1464 repo.ui.develwarn(
1458 1465 b'write with no lock: "%s"' % path, stacklevel=4
1459 1466 )
1460 1467 return ret
1461 1468
1462 1469 return checksvfs
1463 1470
1464 1471 def close(self):
1465 1472 self._writecaches()
1466 1473
1467 1474 def _writecaches(self):
1468 1475 if self._revbranchcache:
1469 1476 self._revbranchcache.write()
1470 1477
1471 1478 def _restrictcapabilities(self, caps):
1472 1479 if self.ui.configbool(b'experimental', b'bundle2-advertise'):
1473 1480 caps = set(caps)
1474 1481 capsblob = bundle2.encodecaps(
1475 1482 bundle2.getrepocaps(self, role=b'client')
1476 1483 )
1477 1484 caps.add(b'bundle2=' + urlreq.quote(capsblob))
1478 1485 return caps
1479 1486
1480 1487 # Don't cache auditor/nofsauditor, or you'll end up with reference cycle:
1481 1488 # self -> auditor -> self._checknested -> self
1482 1489
1483 1490 @property
1484 1491 def auditor(self):
1485 1492 # This is only used by context.workingctx.match in order to
1486 1493 # detect files in subrepos.
1487 1494 return pathutil.pathauditor(self.root, callback=self._checknested)
1488 1495
1489 1496 @property
1490 1497 def nofsauditor(self):
1491 1498 # This is only used by context.basectx.match in order to detect
1492 1499 # files in subrepos.
1493 1500 return pathutil.pathauditor(
1494 1501 self.root, callback=self._checknested, realfs=False, cached=True
1495 1502 )
1496 1503
1497 1504 def _checknested(self, path):
1498 1505 """Determine if path is a legal nested repository."""
1499 1506 if not path.startswith(self.root):
1500 1507 return False
1501 1508 subpath = path[len(self.root) + 1 :]
1502 1509 normsubpath = util.pconvert(subpath)
1503 1510
1504 1511 # XXX: Checking against the current working copy is wrong in
1505 1512 # the sense that it can reject things like
1506 1513 #
1507 1514 # $ hg cat -r 10 sub/x.txt
1508 1515 #
1509 1516 # if sub/ is no longer a subrepository in the working copy
1510 1517 # parent revision.
1511 1518 #
1512 1519 # However, it can of course also allow things that would have
1513 1520 # been rejected before, such as the above cat command if sub/
1514 1521 # is a subrepository now, but was a normal directory before.
1515 1522 # The old path auditor would have rejected by mistake since it
1516 1523 # panics when it sees sub/.hg/.
1517 1524 #
1518 1525 # All in all, checking against the working copy seems sensible
1519 1526 # since we want to prevent access to nested repositories on
1520 1527 # the filesystem *now*.
1521 1528 ctx = self[None]
1522 1529 parts = util.splitpath(subpath)
1523 1530 while parts:
1524 1531 prefix = b'/'.join(parts)
1525 1532 if prefix in ctx.substate:
1526 1533 if prefix == normsubpath:
1527 1534 return True
1528 1535 else:
1529 1536 sub = ctx.sub(prefix)
1530 1537 return sub.checknested(subpath[len(prefix) + 1 :])
1531 1538 else:
1532 1539 parts.pop()
1533 1540 return False
1534 1541
1535 1542 def peer(self):
1536 1543 return localpeer(self) # not cached to avoid reference cycle
1537 1544
1538 1545 def unfiltered(self):
1539 1546 """Return unfiltered version of the repository
1540 1547
1541 1548 Intended to be overwritten by filtered repo."""
1542 1549 return self
1543 1550
1544 1551 def filtered(self, name, visibilityexceptions=None):
1545 1552 """Return a filtered version of a repository
1546 1553
1547 1554 The `name` parameter is the identifier of the requested view. This
1548 1555 will return a repoview object set "exactly" to the specified view.
1549 1556
1550 1557 This function does not apply recursive filtering to a repository. For
1551 1558 example calling `repo.filtered("served")` will return a repoview using
1552 1559 the "served" view, regardless of the initial view used by `repo`.
1553 1560
1554 1561 In other word, there is always only one level of `repoview` "filtering".
1555 1562 """
1556 1563 if self._extrafilterid is not None and b'%' not in name:
1557 1564 name = name + b'%' + self._extrafilterid
1558 1565
1559 1566 cls = repoview.newtype(self.unfiltered().__class__)
1560 1567 return cls(self, name, visibilityexceptions)
1561 1568
1562 1569 @mixedrepostorecache(
1563 1570 (b'bookmarks', b'plain'),
1564 1571 (b'bookmarks.current', b'plain'),
1565 1572 (b'bookmarks', b''),
1566 1573 (b'00changelog.i', b''),
1567 1574 )
1568 1575 def _bookmarks(self):
1569 1576 # Since the multiple files involved in the transaction cannot be
1570 1577 # written atomically (with current repository format), there is a race
1571 1578 # condition here.
1572 1579 #
1573 1580 # 1) changelog content A is read
1574 1581 # 2) outside transaction update changelog to content B
1575 1582 # 3) outside transaction update bookmark file referring to content B
1576 1583 # 4) bookmarks file content is read and filtered against changelog-A
1577 1584 #
1578 1585 # When this happens, bookmarks against nodes missing from A are dropped.
1579 1586 #
1580 1587 # Having this happening during read is not great, but it become worse
1581 1588 # when this happen during write because the bookmarks to the "unknown"
1582 1589 # nodes will be dropped for good. However, writes happen within locks.
1583 1590 # This locking makes it possible to have a race free consistent read.
1584 1591 # For this purpose data read from disc before locking are
1585 1592 # "invalidated" right after the locks are taken. This invalidations are
1586 1593 # "light", the `filecache` mechanism keep the data in memory and will
1587 1594 # reuse them if the underlying files did not changed. Not parsing the
1588 1595 # same data multiple times helps performances.
1589 1596 #
1590 1597 # Unfortunately in the case describe above, the files tracked by the
1591 1598 # bookmarks file cache might not have changed, but the in-memory
1592 1599 # content is still "wrong" because we used an older changelog content
1593 1600 # to process the on-disk data. So after locking, the changelog would be
1594 1601 # refreshed but `_bookmarks` would be preserved.
1595 1602 # Adding `00changelog.i` to the list of tracked file is not
1596 1603 # enough, because at the time we build the content for `_bookmarks` in
1597 1604 # (4), the changelog file has already diverged from the content used
1598 1605 # for loading `changelog` in (1)
1599 1606 #
1600 1607 # To prevent the issue, we force the changelog to be explicitly
1601 1608 # reloaded while computing `_bookmarks`. The data race can still happen
1602 1609 # without the lock (with a narrower window), but it would no longer go
1603 1610 # undetected during the lock time refresh.
1604 1611 #
1605 1612 # The new schedule is as follow
1606 1613 #
1607 1614 # 1) filecache logic detect that `_bookmarks` needs to be computed
1608 1615 # 2) cachestat for `bookmarks` and `changelog` are captured (for book)
1609 1616 # 3) We force `changelog` filecache to be tested
1610 1617 # 4) cachestat for `changelog` are captured (for changelog)
1611 1618 # 5) `_bookmarks` is computed and cached
1612 1619 #
1613 1620 # The step in (3) ensure we have a changelog at least as recent as the
1614 1621 # cache stat computed in (1). As a result at locking time:
1615 1622 # * if the changelog did not changed since (1) -> we can reuse the data
1616 1623 # * otherwise -> the bookmarks get refreshed.
1617 1624 self._refreshchangelog()
1618 1625 return bookmarks.bmstore(self)
1619 1626
1620 1627 def _refreshchangelog(self):
1621 1628 """make sure the in memory changelog match the on-disk one"""
1622 1629 if 'changelog' in vars(self) and self.currenttransaction() is None:
1623 1630 del self.changelog
1624 1631
1625 1632 @property
1626 1633 def _activebookmark(self):
1627 1634 return self._bookmarks.active
1628 1635
1629 1636 # _phasesets depend on changelog. what we need is to call
1630 1637 # _phasecache.invalidate() if '00changelog.i' was changed, but it
1631 1638 # can't be easily expressed in filecache mechanism.
1632 1639 @storecache(b'phaseroots', b'00changelog.i')
1633 1640 def _phasecache(self):
1634 1641 return phases.phasecache(self, self._phasedefaults)
1635 1642
1636 1643 @storecache(b'obsstore')
1637 1644 def obsstore(self):
1638 1645 return obsolete.makestore(self.ui, self)
1639 1646
1640 1647 @storecache(b'00changelog.i')
1641 1648 def changelog(self):
1642 1649 # load dirstate before changelog to avoid race see issue6303
1643 1650 self.dirstate.prefetch_parents()
1644 1651 return self.store.changelog(
1645 1652 txnutil.mayhavepending(self.root),
1646 1653 concurrencychecker=revlogchecker.get_checker(self.ui, b'changelog'),
1647 1654 )
1648 1655
1649 1656 @storecache(b'00manifest.i')
1650 1657 def manifestlog(self):
1651 1658 return self.store.manifestlog(self, self._storenarrowmatch)
1652 1659
1653 1660 @repofilecache(b'dirstate')
1654 1661 def dirstate(self):
1655 1662 return self._makedirstate()
1656 1663
1657 1664 def _makedirstate(self):
1658 1665 """Extension point for wrapping the dirstate per-repo."""
1659 1666 sparsematchfn = lambda: sparse.matcher(self)
1660 1667
1661 1668 return dirstate.dirstate(
1662 1669 self.vfs, self.ui, self.root, self._dirstatevalidate, sparsematchfn
1663 1670 )
1664 1671
1665 1672 def _dirstatevalidate(self, node):
1666 1673 try:
1667 1674 self.changelog.rev(node)
1668 1675 return node
1669 1676 except error.LookupError:
1670 1677 if not self._dirstatevalidatewarned:
1671 1678 self._dirstatevalidatewarned = True
1672 1679 self.ui.warn(
1673 1680 _(b"warning: ignoring unknown working parent %s!\n")
1674 1681 % short(node)
1675 1682 )
1676 1683 return nullid
1677 1684
1678 1685 @storecache(narrowspec.FILENAME)
1679 1686 def narrowpats(self):
1680 1687 """matcher patterns for this repository's narrowspec
1681 1688
1682 1689 A tuple of (includes, excludes).
1683 1690 """
1684 1691 return narrowspec.load(self)
1685 1692
1686 1693 @storecache(narrowspec.FILENAME)
1687 1694 def _storenarrowmatch(self):
1688 1695 if requirementsmod.NARROW_REQUIREMENT not in self.requirements:
1689 1696 return matchmod.always()
1690 1697 include, exclude = self.narrowpats
1691 1698 return narrowspec.match(self.root, include=include, exclude=exclude)
1692 1699
1693 1700 @storecache(narrowspec.FILENAME)
1694 1701 def _narrowmatch(self):
1695 1702 if requirementsmod.NARROW_REQUIREMENT not in self.requirements:
1696 1703 return matchmod.always()
1697 1704 narrowspec.checkworkingcopynarrowspec(self)
1698 1705 include, exclude = self.narrowpats
1699 1706 return narrowspec.match(self.root, include=include, exclude=exclude)
1700 1707
1701 1708 def narrowmatch(self, match=None, includeexact=False):
1702 1709 """matcher corresponding the the repo's narrowspec
1703 1710
1704 1711 If `match` is given, then that will be intersected with the narrow
1705 1712 matcher.
1706 1713
1707 1714 If `includeexact` is True, then any exact matches from `match` will
1708 1715 be included even if they're outside the narrowspec.
1709 1716 """
1710 1717 if match:
1711 1718 if includeexact and not self._narrowmatch.always():
1712 1719 # do not exclude explicitly-specified paths so that they can
1713 1720 # be warned later on
1714 1721 em = matchmod.exact(match.files())
1715 1722 nm = matchmod.unionmatcher([self._narrowmatch, em])
1716 1723 return matchmod.intersectmatchers(match, nm)
1717 1724 return matchmod.intersectmatchers(match, self._narrowmatch)
1718 1725 return self._narrowmatch
1719 1726
1720 1727 def setnarrowpats(self, newincludes, newexcludes):
1721 1728 narrowspec.save(self, newincludes, newexcludes)
1722 1729 self.invalidate(clearfilecache=True)
1723 1730
1724 1731 @unfilteredpropertycache
1725 1732 def _quick_access_changeid_null(self):
1726 1733 return {
1727 1734 b'null': (nullrev, nullid),
1728 1735 nullrev: (nullrev, nullid),
1729 1736 nullid: (nullrev, nullid),
1730 1737 }
1731 1738
1732 1739 @unfilteredpropertycache
1733 1740 def _quick_access_changeid_wc(self):
1734 1741 # also fast path access to the working copy parents
1735 1742 # however, only do it for filter that ensure wc is visible.
1736 1743 quick = self._quick_access_changeid_null.copy()
1737 1744 cl = self.unfiltered().changelog
1738 1745 for node in self.dirstate.parents():
1739 1746 if node == nullid:
1740 1747 continue
1741 1748 rev = cl.index.get_rev(node)
1742 1749 if rev is None:
1743 1750 # unknown working copy parent case:
1744 1751 #
1745 1752 # skip the fast path and let higher code deal with it
1746 1753 continue
1747 1754 pair = (rev, node)
1748 1755 quick[rev] = pair
1749 1756 quick[node] = pair
1750 1757 # also add the parents of the parents
1751 1758 for r in cl.parentrevs(rev):
1752 1759 if r == nullrev:
1753 1760 continue
1754 1761 n = cl.node(r)
1755 1762 pair = (r, n)
1756 1763 quick[r] = pair
1757 1764 quick[n] = pair
1758 1765 p1node = self.dirstate.p1()
1759 1766 if p1node != nullid:
1760 1767 quick[b'.'] = quick[p1node]
1761 1768 return quick
1762 1769
1763 1770 @unfilteredmethod
1764 1771 def _quick_access_changeid_invalidate(self):
1765 1772 if '_quick_access_changeid_wc' in vars(self):
1766 1773 del self.__dict__['_quick_access_changeid_wc']
1767 1774
1768 1775 @property
1769 1776 def _quick_access_changeid(self):
1770 1777 """an helper dictionnary for __getitem__ calls
1771 1778
1772 1779 This contains a list of symbol we can recognise right away without
1773 1780 further processing.
1774 1781 """
1775 1782 if self.filtername in repoview.filter_has_wc:
1776 1783 return self._quick_access_changeid_wc
1777 1784 return self._quick_access_changeid_null
1778 1785
1779 1786 def __getitem__(self, changeid):
1780 1787 # dealing with special cases
1781 1788 if changeid is None:
1782 1789 return context.workingctx(self)
1783 1790 if isinstance(changeid, context.basectx):
1784 1791 return changeid
1785 1792
1786 1793 # dealing with multiple revisions
1787 1794 if isinstance(changeid, slice):
1788 1795 # wdirrev isn't contiguous so the slice shouldn't include it
1789 1796 return [
1790 1797 self[i]
1791 1798 for i in pycompat.xrange(*changeid.indices(len(self)))
1792 1799 if i not in self.changelog.filteredrevs
1793 1800 ]
1794 1801
1795 1802 # dealing with some special values
1796 1803 quick_access = self._quick_access_changeid.get(changeid)
1797 1804 if quick_access is not None:
1798 1805 rev, node = quick_access
1799 1806 return context.changectx(self, rev, node, maybe_filtered=False)
1800 1807 if changeid == b'tip':
1801 1808 node = self.changelog.tip()
1802 1809 rev = self.changelog.rev(node)
1803 1810 return context.changectx(self, rev, node)
1804 1811
1805 1812 # dealing with arbitrary values
1806 1813 try:
1807 1814 if isinstance(changeid, int):
1808 1815 node = self.changelog.node(changeid)
1809 1816 rev = changeid
1810 1817 elif changeid == b'.':
1811 1818 # this is a hack to delay/avoid loading obsmarkers
1812 1819 # when we know that '.' won't be hidden
1813 1820 node = self.dirstate.p1()
1814 1821 rev = self.unfiltered().changelog.rev(node)
1815 1822 elif len(changeid) == 20:
1816 1823 try:
1817 1824 node = changeid
1818 1825 rev = self.changelog.rev(changeid)
1819 1826 except error.FilteredLookupError:
1820 1827 changeid = hex(changeid) # for the error message
1821 1828 raise
1822 1829 except LookupError:
1823 1830 # check if it might have come from damaged dirstate
1824 1831 #
1825 1832 # XXX we could avoid the unfiltered if we had a recognizable
1826 1833 # exception for filtered changeset access
1827 1834 if (
1828 1835 self.local()
1829 1836 and changeid in self.unfiltered().dirstate.parents()
1830 1837 ):
1831 1838 msg = _(b"working directory has unknown parent '%s'!")
1832 1839 raise error.Abort(msg % short(changeid))
1833 1840 changeid = hex(changeid) # for the error message
1834 1841 raise
1835 1842
1836 1843 elif len(changeid) == 40:
1837 1844 node = bin(changeid)
1838 1845 rev = self.changelog.rev(node)
1839 1846 else:
1840 1847 raise error.ProgrammingError(
1841 1848 b"unsupported changeid '%s' of type %s"
1842 1849 % (changeid, pycompat.bytestr(type(changeid)))
1843 1850 )
1844 1851
1845 1852 return context.changectx(self, rev, node)
1846 1853
1847 1854 except (error.FilteredIndexError, error.FilteredLookupError):
1848 1855 raise error.FilteredRepoLookupError(
1849 1856 _(b"filtered revision '%s'") % pycompat.bytestr(changeid)
1850 1857 )
1851 1858 except (IndexError, LookupError):
1852 1859 raise error.RepoLookupError(
1853 1860 _(b"unknown revision '%s'") % pycompat.bytestr(changeid)
1854 1861 )
1855 1862 except error.WdirUnsupported:
1856 1863 return context.workingctx(self)
1857 1864
1858 1865 def __contains__(self, changeid):
1859 1866 """True if the given changeid exists"""
1860 1867 try:
1861 1868 self[changeid]
1862 1869 return True
1863 1870 except error.RepoLookupError:
1864 1871 return False
1865 1872
1866 1873 def __nonzero__(self):
1867 1874 return True
1868 1875
1869 1876 __bool__ = __nonzero__
1870 1877
1871 1878 def __len__(self):
1872 1879 # no need to pay the cost of repoview.changelog
1873 1880 unfi = self.unfiltered()
1874 1881 return len(unfi.changelog)
1875 1882
1876 1883 def __iter__(self):
1877 1884 return iter(self.changelog)
1878 1885
1879 1886 def revs(self, expr, *args):
1880 1887 """Find revisions matching a revset.
1881 1888
1882 1889 The revset is specified as a string ``expr`` that may contain
1883 1890 %-formatting to escape certain types. See ``revsetlang.formatspec``.
1884 1891
1885 1892 Revset aliases from the configuration are not expanded. To expand
1886 1893 user aliases, consider calling ``scmutil.revrange()`` or
1887 1894 ``repo.anyrevs([expr], user=True)``.
1888 1895
1889 1896 Returns a smartset.abstractsmartset, which is a list-like interface
1890 1897 that contains integer revisions.
1891 1898 """
1892 1899 tree = revsetlang.spectree(expr, *args)
1893 1900 return revset.makematcher(tree)(self)
1894 1901
1895 1902 def set(self, expr, *args):
1896 1903 """Find revisions matching a revset and emit changectx instances.
1897 1904
1898 1905 This is a convenience wrapper around ``revs()`` that iterates the
1899 1906 result and is a generator of changectx instances.
1900 1907
1901 1908 Revset aliases from the configuration are not expanded. To expand
1902 1909 user aliases, consider calling ``scmutil.revrange()``.
1903 1910 """
1904 1911 for r in self.revs(expr, *args):
1905 1912 yield self[r]
1906 1913
1907 1914 def anyrevs(self, specs, user=False, localalias=None):
1908 1915 """Find revisions matching one of the given revsets.
1909 1916
1910 1917 Revset aliases from the configuration are not expanded by default. To
1911 1918 expand user aliases, specify ``user=True``. To provide some local
1912 1919 definitions overriding user aliases, set ``localalias`` to
1913 1920 ``{name: definitionstring}``.
1914 1921 """
1915 1922 if specs == [b'null']:
1916 1923 return revset.baseset([nullrev])
1917 1924 if specs == [b'.']:
1918 1925 quick_data = self._quick_access_changeid.get(b'.')
1919 1926 if quick_data is not None:
1920 1927 return revset.baseset([quick_data[0]])
1921 1928 if user:
1922 1929 m = revset.matchany(
1923 1930 self.ui,
1924 1931 specs,
1925 1932 lookup=revset.lookupfn(self),
1926 1933 localalias=localalias,
1927 1934 )
1928 1935 else:
1929 1936 m = revset.matchany(None, specs, localalias=localalias)
1930 1937 return m(self)
1931 1938
1932 1939 def url(self):
1933 1940 return b'file:' + self.root
1934 1941
1935 1942 def hook(self, name, throw=False, **args):
1936 1943 """Call a hook, passing this repo instance.
1937 1944
1938 1945 This a convenience method to aid invoking hooks. Extensions likely
1939 1946 won't call this unless they have registered a custom hook or are
1940 1947 replacing code that is expected to call a hook.
1941 1948 """
1942 1949 return hook.hook(self.ui, self, name, throw, **args)
1943 1950
1944 1951 @filteredpropertycache
1945 1952 def _tagscache(self):
1946 1953 """Returns a tagscache object that contains various tags related
1947 1954 caches."""
1948 1955
1949 1956 # This simplifies its cache management by having one decorated
1950 1957 # function (this one) and the rest simply fetch things from it.
1951 1958 class tagscache(object):
1952 1959 def __init__(self):
1953 1960 # These two define the set of tags for this repository. tags
1954 1961 # maps tag name to node; tagtypes maps tag name to 'global' or
1955 1962 # 'local'. (Global tags are defined by .hgtags across all
1956 1963 # heads, and local tags are defined in .hg/localtags.)
1957 1964 # They constitute the in-memory cache of tags.
1958 1965 self.tags = self.tagtypes = None
1959 1966
1960 1967 self.nodetagscache = self.tagslist = None
1961 1968
1962 1969 cache = tagscache()
1963 1970 cache.tags, cache.tagtypes = self._findtags()
1964 1971
1965 1972 return cache
1966 1973
1967 1974 def tags(self):
1968 1975 '''return a mapping of tag to node'''
1969 1976 t = {}
1970 1977 if self.changelog.filteredrevs:
1971 1978 tags, tt = self._findtags()
1972 1979 else:
1973 1980 tags = self._tagscache.tags
1974 1981 rev = self.changelog.rev
1975 1982 for k, v in pycompat.iteritems(tags):
1976 1983 try:
1977 1984 # ignore tags to unknown nodes
1978 1985 rev(v)
1979 1986 t[k] = v
1980 1987 except (error.LookupError, ValueError):
1981 1988 pass
1982 1989 return t
1983 1990
1984 1991 def _findtags(self):
1985 1992 """Do the hard work of finding tags. Return a pair of dicts
1986 1993 (tags, tagtypes) where tags maps tag name to node, and tagtypes
1987 1994 maps tag name to a string like \'global\' or \'local\'.
1988 1995 Subclasses or extensions are free to add their own tags, but
1989 1996 should be aware that the returned dicts will be retained for the
1990 1997 duration of the localrepo object."""
1991 1998
1992 1999 # XXX what tagtype should subclasses/extensions use? Currently
1993 2000 # mq and bookmarks add tags, but do not set the tagtype at all.
1994 2001 # Should each extension invent its own tag type? Should there
1995 2002 # be one tagtype for all such "virtual" tags? Or is the status
1996 2003 # quo fine?
1997 2004
1998 2005 # map tag name to (node, hist)
1999 2006 alltags = tagsmod.findglobaltags(self.ui, self)
2000 2007 # map tag name to tag type
2001 2008 tagtypes = {tag: b'global' for tag in alltags}
2002 2009
2003 2010 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
2004 2011
2005 2012 # Build the return dicts. Have to re-encode tag names because
2006 2013 # the tags module always uses UTF-8 (in order not to lose info
2007 2014 # writing to the cache), but the rest of Mercurial wants them in
2008 2015 # local encoding.
2009 2016 tags = {}
2010 2017 for (name, (node, hist)) in pycompat.iteritems(alltags):
2011 2018 if node != nullid:
2012 2019 tags[encoding.tolocal(name)] = node
2013 2020 tags[b'tip'] = self.changelog.tip()
2014 2021 tagtypes = {
2015 2022 encoding.tolocal(name): value
2016 2023 for (name, value) in pycompat.iteritems(tagtypes)
2017 2024 }
2018 2025 return (tags, tagtypes)
2019 2026
2020 2027 def tagtype(self, tagname):
2021 2028 """
2022 2029 return the type of the given tag. result can be:
2023 2030
2024 2031 'local' : a local tag
2025 2032 'global' : a global tag
2026 2033 None : tag does not exist
2027 2034 """
2028 2035
2029 2036 return self._tagscache.tagtypes.get(tagname)
2030 2037
2031 2038 def tagslist(self):
2032 2039 '''return a list of tags ordered by revision'''
2033 2040 if not self._tagscache.tagslist:
2034 2041 l = []
2035 2042 for t, n in pycompat.iteritems(self.tags()):
2036 2043 l.append((self.changelog.rev(n), t, n))
2037 2044 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
2038 2045
2039 2046 return self._tagscache.tagslist
2040 2047
2041 2048 def nodetags(self, node):
2042 2049 '''return the tags associated with a node'''
2043 2050 if not self._tagscache.nodetagscache:
2044 2051 nodetagscache = {}
2045 2052 for t, n in pycompat.iteritems(self._tagscache.tags):
2046 2053 nodetagscache.setdefault(n, []).append(t)
2047 2054 for tags in pycompat.itervalues(nodetagscache):
2048 2055 tags.sort()
2049 2056 self._tagscache.nodetagscache = nodetagscache
2050 2057 return self._tagscache.nodetagscache.get(node, [])
2051 2058
2052 2059 def nodebookmarks(self, node):
2053 2060 """return the list of bookmarks pointing to the specified node"""
2054 2061 return self._bookmarks.names(node)
2055 2062
2056 2063 def branchmap(self):
2057 2064 """returns a dictionary {branch: [branchheads]} with branchheads
2058 2065 ordered by increasing revision number"""
2059 2066 return self._branchcaches[self]
2060 2067
2061 2068 @unfilteredmethod
2062 2069 def revbranchcache(self):
2063 2070 if not self._revbranchcache:
2064 2071 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
2065 2072 return self._revbranchcache
2066 2073
2067 2074 def register_changeset(self, rev, changelogrevision):
2068 2075 self.revbranchcache().setdata(rev, changelogrevision)
2069 2076
2070 2077 def branchtip(self, branch, ignoremissing=False):
2071 2078 """return the tip node for a given branch
2072 2079
2073 2080 If ignoremissing is True, then this method will not raise an error.
2074 2081 This is helpful for callers that only expect None for a missing branch
2075 2082 (e.g. namespace).
2076 2083
2077 2084 """
2078 2085 try:
2079 2086 return self.branchmap().branchtip(branch)
2080 2087 except KeyError:
2081 2088 if not ignoremissing:
2082 2089 raise error.RepoLookupError(_(b"unknown branch '%s'") % branch)
2083 2090 else:
2084 2091 pass
2085 2092
2086 2093 def lookup(self, key):
2087 2094 node = scmutil.revsymbol(self, key).node()
2088 2095 if node is None:
2089 2096 raise error.RepoLookupError(_(b"unknown revision '%s'") % key)
2090 2097 return node
2091 2098
2092 2099 def lookupbranch(self, key):
2093 2100 if self.branchmap().hasbranch(key):
2094 2101 return key
2095 2102
2096 2103 return scmutil.revsymbol(self, key).branch()
2097 2104
2098 2105 def known(self, nodes):
2099 2106 cl = self.changelog
2100 2107 get_rev = cl.index.get_rev
2101 2108 filtered = cl.filteredrevs
2102 2109 result = []
2103 2110 for n in nodes:
2104 2111 r = get_rev(n)
2105 2112 resp = not (r is None or r in filtered)
2106 2113 result.append(resp)
2107 2114 return result
2108 2115
2109 2116 def local(self):
2110 2117 return self
2111 2118
2112 2119 def publishing(self):
2113 2120 # it's safe (and desirable) to trust the publish flag unconditionally
2114 2121 # so that we don't finalize changes shared between users via ssh or nfs
2115 2122 return self.ui.configbool(b'phases', b'publish', untrusted=True)
2116 2123
2117 2124 def cancopy(self):
2118 2125 # so statichttprepo's override of local() works
2119 2126 if not self.local():
2120 2127 return False
2121 2128 if not self.publishing():
2122 2129 return True
2123 2130 # if publishing we can't copy if there is filtered content
2124 2131 return not self.filtered(b'visible').changelog.filteredrevs
2125 2132
2126 2133 def shared(self):
2127 2134 '''the type of shared repository (None if not shared)'''
2128 2135 if self.sharedpath != self.path:
2129 2136 return b'store'
2130 2137 return None
2131 2138
2132 2139 def wjoin(self, f, *insidef):
2133 2140 return self.vfs.reljoin(self.root, f, *insidef)
2134 2141
2135 2142 def setparents(self, p1, p2=nullid):
2136 2143 self[None].setparents(p1, p2)
2137 2144 self._quick_access_changeid_invalidate()
2138 2145
2139 2146 def filectx(self, path, changeid=None, fileid=None, changectx=None):
2140 2147 """changeid must be a changeset revision, if specified.
2141 2148 fileid can be a file revision or node."""
2142 2149 return context.filectx(
2143 2150 self, path, changeid, fileid, changectx=changectx
2144 2151 )
2145 2152
2146 2153 def getcwd(self):
2147 2154 return self.dirstate.getcwd()
2148 2155
2149 2156 def pathto(self, f, cwd=None):
2150 2157 return self.dirstate.pathto(f, cwd)
2151 2158
2152 2159 def _loadfilter(self, filter):
2153 2160 if filter not in self._filterpats:
2154 2161 l = []
2155 2162 for pat, cmd in self.ui.configitems(filter):
2156 2163 if cmd == b'!':
2157 2164 continue
2158 2165 mf = matchmod.match(self.root, b'', [pat])
2159 2166 fn = None
2160 2167 params = cmd
2161 2168 for name, filterfn in pycompat.iteritems(self._datafilters):
2162 2169 if cmd.startswith(name):
2163 2170 fn = filterfn
2164 2171 params = cmd[len(name) :].lstrip()
2165 2172 break
2166 2173 if not fn:
2167 2174 fn = lambda s, c, **kwargs: procutil.filter(s, c)
2168 2175 fn.__name__ = 'commandfilter'
2169 2176 # Wrap old filters not supporting keyword arguments
2170 2177 if not pycompat.getargspec(fn)[2]:
2171 2178 oldfn = fn
2172 2179 fn = lambda s, c, oldfn=oldfn, **kwargs: oldfn(s, c)
2173 2180 fn.__name__ = 'compat-' + oldfn.__name__
2174 2181 l.append((mf, fn, params))
2175 2182 self._filterpats[filter] = l
2176 2183 return self._filterpats[filter]
2177 2184
2178 2185 def _filter(self, filterpats, filename, data):
2179 2186 for mf, fn, cmd in filterpats:
2180 2187 if mf(filename):
2181 2188 self.ui.debug(
2182 2189 b"filtering %s through %s\n"
2183 2190 % (filename, cmd or pycompat.sysbytes(fn.__name__))
2184 2191 )
2185 2192 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
2186 2193 break
2187 2194
2188 2195 return data
2189 2196
2190 2197 @unfilteredpropertycache
2191 2198 def _encodefilterpats(self):
2192 2199 return self._loadfilter(b'encode')
2193 2200
2194 2201 @unfilteredpropertycache
2195 2202 def _decodefilterpats(self):
2196 2203 return self._loadfilter(b'decode')
2197 2204
2198 2205 def adddatafilter(self, name, filter):
2199 2206 self._datafilters[name] = filter
2200 2207
2201 2208 def wread(self, filename):
2202 2209 if self.wvfs.islink(filename):
2203 2210 data = self.wvfs.readlink(filename)
2204 2211 else:
2205 2212 data = self.wvfs.read(filename)
2206 2213 return self._filter(self._encodefilterpats, filename, data)
2207 2214
2208 2215 def wwrite(self, filename, data, flags, backgroundclose=False, **kwargs):
2209 2216 """write ``data`` into ``filename`` in the working directory
2210 2217
2211 2218 This returns length of written (maybe decoded) data.
2212 2219 """
2213 2220 data = self._filter(self._decodefilterpats, filename, data)
2214 2221 if b'l' in flags:
2215 2222 self.wvfs.symlink(data, filename)
2216 2223 else:
2217 2224 self.wvfs.write(
2218 2225 filename, data, backgroundclose=backgroundclose, **kwargs
2219 2226 )
2220 2227 if b'x' in flags:
2221 2228 self.wvfs.setflags(filename, False, True)
2222 2229 else:
2223 2230 self.wvfs.setflags(filename, False, False)
2224 2231 return len(data)
2225 2232
2226 2233 def wwritedata(self, filename, data):
2227 2234 return self._filter(self._decodefilterpats, filename, data)
2228 2235
2229 2236 def currenttransaction(self):
2230 2237 """return the current transaction or None if non exists"""
2231 2238 if self._transref:
2232 2239 tr = self._transref()
2233 2240 else:
2234 2241 tr = None
2235 2242
2236 2243 if tr and tr.running():
2237 2244 return tr
2238 2245 return None
2239 2246
2240 2247 def transaction(self, desc, report=None):
2241 2248 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
2242 2249 b'devel', b'check-locks'
2243 2250 ):
2244 2251 if self._currentlock(self._lockref) is None:
2245 2252 raise error.ProgrammingError(b'transaction requires locking')
2246 2253 tr = self.currenttransaction()
2247 2254 if tr is not None:
2248 2255 return tr.nest(name=desc)
2249 2256
2250 2257 # abort here if the journal already exists
2251 2258 if self.svfs.exists(b"journal"):
2252 2259 raise error.RepoError(
2253 2260 _(b"abandoned transaction found"),
2254 2261 hint=_(b"run 'hg recover' to clean up transaction"),
2255 2262 )
2256 2263
2257 2264 idbase = b"%.40f#%f" % (random.random(), time.time())
2258 2265 ha = hex(hashutil.sha1(idbase).digest())
2259 2266 txnid = b'TXN:' + ha
2260 2267 self.hook(b'pretxnopen', throw=True, txnname=desc, txnid=txnid)
2261 2268
2262 2269 self._writejournal(desc)
2263 2270 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
2264 2271 if report:
2265 2272 rp = report
2266 2273 else:
2267 2274 rp = self.ui.warn
2268 2275 vfsmap = {b'plain': self.vfs, b'store': self.svfs} # root of .hg/
2269 2276 # we must avoid cyclic reference between repo and transaction.
2270 2277 reporef = weakref.ref(self)
2271 2278 # Code to track tag movement
2272 2279 #
2273 2280 # Since tags are all handled as file content, it is actually quite hard
2274 2281 # to track these movement from a code perspective. So we fallback to a
2275 2282 # tracking at the repository level. One could envision to track changes
2276 2283 # to the '.hgtags' file through changegroup apply but that fails to
2277 2284 # cope with case where transaction expose new heads without changegroup
2278 2285 # being involved (eg: phase movement).
2279 2286 #
2280 2287 # For now, We gate the feature behind a flag since this likely comes
2281 2288 # with performance impacts. The current code run more often than needed
2282 2289 # and do not use caches as much as it could. The current focus is on
2283 2290 # the behavior of the feature so we disable it by default. The flag
2284 2291 # will be removed when we are happy with the performance impact.
2285 2292 #
2286 2293 # Once this feature is no longer experimental move the following
2287 2294 # documentation to the appropriate help section:
2288 2295 #
2289 2296 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
2290 2297 # tags (new or changed or deleted tags). In addition the details of
2291 2298 # these changes are made available in a file at:
2292 2299 # ``REPOROOT/.hg/changes/tags.changes``.
2293 2300 # Make sure you check for HG_TAG_MOVED before reading that file as it
2294 2301 # might exist from a previous transaction even if no tag were touched
2295 2302 # in this one. Changes are recorded in a line base format::
2296 2303 #
2297 2304 # <action> <hex-node> <tag-name>\n
2298 2305 #
2299 2306 # Actions are defined as follow:
2300 2307 # "-R": tag is removed,
2301 2308 # "+A": tag is added,
2302 2309 # "-M": tag is moved (old value),
2303 2310 # "+M": tag is moved (new value),
2304 2311 tracktags = lambda x: None
2305 2312 # experimental config: experimental.hook-track-tags
2306 2313 shouldtracktags = self.ui.configbool(
2307 2314 b'experimental', b'hook-track-tags'
2308 2315 )
2309 2316 if desc != b'strip' and shouldtracktags:
2310 2317 oldheads = self.changelog.headrevs()
2311 2318
2312 2319 def tracktags(tr2):
2313 2320 repo = reporef()
2314 2321 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
2315 2322 newheads = repo.changelog.headrevs()
2316 2323 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
2317 2324 # notes: we compare lists here.
2318 2325 # As we do it only once buiding set would not be cheaper
2319 2326 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
2320 2327 if changes:
2321 2328 tr2.hookargs[b'tag_moved'] = b'1'
2322 2329 with repo.vfs(
2323 2330 b'changes/tags.changes', b'w', atomictemp=True
2324 2331 ) as changesfile:
2325 2332 # note: we do not register the file to the transaction
2326 2333 # because we needs it to still exist on the transaction
2327 2334 # is close (for txnclose hooks)
2328 2335 tagsmod.writediff(changesfile, changes)
2329 2336
2330 2337 def validate(tr2):
2331 2338 """will run pre-closing hooks"""
2332 2339 # XXX the transaction API is a bit lacking here so we take a hacky
2333 2340 # path for now
2334 2341 #
2335 2342 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
2336 2343 # dict is copied before these run. In addition we needs the data
2337 2344 # available to in memory hooks too.
2338 2345 #
2339 2346 # Moreover, we also need to make sure this runs before txnclose
2340 2347 # hooks and there is no "pending" mechanism that would execute
2341 2348 # logic only if hooks are about to run.
2342 2349 #
2343 2350 # Fixing this limitation of the transaction is also needed to track
2344 2351 # other families of changes (bookmarks, phases, obsolescence).
2345 2352 #
2346 2353 # This will have to be fixed before we remove the experimental
2347 2354 # gating.
2348 2355 tracktags(tr2)
2349 2356 repo = reporef()
2350 2357
2351 2358 singleheadopt = (b'experimental', b'single-head-per-branch')
2352 2359 singlehead = repo.ui.configbool(*singleheadopt)
2353 2360 if singlehead:
2354 2361 singleheadsub = repo.ui.configsuboptions(*singleheadopt)[1]
2355 2362 accountclosed = singleheadsub.get(
2356 2363 b"account-closed-heads", False
2357 2364 )
2358 2365 if singleheadsub.get(b"public-changes-only", False):
2359 2366 filtername = b"immutable"
2360 2367 else:
2361 2368 filtername = b"visible"
2362 2369 scmutil.enforcesinglehead(
2363 2370 repo, tr2, desc, accountclosed, filtername
2364 2371 )
2365 2372 if hook.hashook(repo.ui, b'pretxnclose-bookmark'):
2366 2373 for name, (old, new) in sorted(
2367 2374 tr.changes[b'bookmarks'].items()
2368 2375 ):
2369 2376 args = tr.hookargs.copy()
2370 2377 args.update(bookmarks.preparehookargs(name, old, new))
2371 2378 repo.hook(
2372 2379 b'pretxnclose-bookmark',
2373 2380 throw=True,
2374 2381 **pycompat.strkwargs(args)
2375 2382 )
2376 2383 if hook.hashook(repo.ui, b'pretxnclose-phase'):
2377 2384 cl = repo.unfiltered().changelog
2378 2385 for revs, (old, new) in tr.changes[b'phases']:
2379 2386 for rev in revs:
2380 2387 args = tr.hookargs.copy()
2381 2388 node = hex(cl.node(rev))
2382 2389 args.update(phases.preparehookargs(node, old, new))
2383 2390 repo.hook(
2384 2391 b'pretxnclose-phase',
2385 2392 throw=True,
2386 2393 **pycompat.strkwargs(args)
2387 2394 )
2388 2395
2389 2396 repo.hook(
2390 2397 b'pretxnclose', throw=True, **pycompat.strkwargs(tr.hookargs)
2391 2398 )
2392 2399
2393 2400 def releasefn(tr, success):
2394 2401 repo = reporef()
2395 2402 if repo is None:
2396 2403 # If the repo has been GC'd (and this release function is being
2397 2404 # called from transaction.__del__), there's not much we can do,
2398 2405 # so just leave the unfinished transaction there and let the
2399 2406 # user run `hg recover`.
2400 2407 return
2401 2408 if success:
2402 2409 # this should be explicitly invoked here, because
2403 2410 # in-memory changes aren't written out at closing
2404 2411 # transaction, if tr.addfilegenerator (via
2405 2412 # dirstate.write or so) isn't invoked while
2406 2413 # transaction running
2407 2414 repo.dirstate.write(None)
2408 2415 else:
2409 2416 # discard all changes (including ones already written
2410 2417 # out) in this transaction
2411 2418 narrowspec.restorebackup(self, b'journal.narrowspec')
2412 2419 narrowspec.restorewcbackup(self, b'journal.narrowspec.dirstate')
2413 2420 repo.dirstate.restorebackup(None, b'journal.dirstate')
2414 2421
2415 2422 repo.invalidate(clearfilecache=True)
2416 2423
2417 2424 tr = transaction.transaction(
2418 2425 rp,
2419 2426 self.svfs,
2420 2427 vfsmap,
2421 2428 b"journal",
2422 2429 b"undo",
2423 2430 aftertrans(renames),
2424 2431 self.store.createmode,
2425 2432 validator=validate,
2426 2433 releasefn=releasefn,
2427 2434 checkambigfiles=_cachedfiles,
2428 2435 name=desc,
2429 2436 )
2430 2437 tr.changes[b'origrepolen'] = len(self)
2431 2438 tr.changes[b'obsmarkers'] = set()
2432 2439 tr.changes[b'phases'] = []
2433 2440 tr.changes[b'bookmarks'] = {}
2434 2441
2435 2442 tr.hookargs[b'txnid'] = txnid
2436 2443 tr.hookargs[b'txnname'] = desc
2437 2444 tr.hookargs[b'changes'] = tr.changes
2438 2445 # note: writing the fncache only during finalize mean that the file is
2439 2446 # outdated when running hooks. As fncache is used for streaming clone,
2440 2447 # this is not expected to break anything that happen during the hooks.
2441 2448 tr.addfinalize(b'flush-fncache', self.store.write)
2442 2449
2443 2450 def txnclosehook(tr2):
2444 2451 """To be run if transaction is successful, will schedule a hook run"""
2445 2452 # Don't reference tr2 in hook() so we don't hold a reference.
2446 2453 # This reduces memory consumption when there are multiple
2447 2454 # transactions per lock. This can likely go away if issue5045
2448 2455 # fixes the function accumulation.
2449 2456 hookargs = tr2.hookargs
2450 2457
2451 2458 def hookfunc(unused_success):
2452 2459 repo = reporef()
2453 2460 if hook.hashook(repo.ui, b'txnclose-bookmark'):
2454 2461 bmchanges = sorted(tr.changes[b'bookmarks'].items())
2455 2462 for name, (old, new) in bmchanges:
2456 2463 args = tr.hookargs.copy()
2457 2464 args.update(bookmarks.preparehookargs(name, old, new))
2458 2465 repo.hook(
2459 2466 b'txnclose-bookmark',
2460 2467 throw=False,
2461 2468 **pycompat.strkwargs(args)
2462 2469 )
2463 2470
2464 2471 if hook.hashook(repo.ui, b'txnclose-phase'):
2465 2472 cl = repo.unfiltered().changelog
2466 2473 phasemv = sorted(
2467 2474 tr.changes[b'phases'], key=lambda r: r[0][0]
2468 2475 )
2469 2476 for revs, (old, new) in phasemv:
2470 2477 for rev in revs:
2471 2478 args = tr.hookargs.copy()
2472 2479 node = hex(cl.node(rev))
2473 2480 args.update(phases.preparehookargs(node, old, new))
2474 2481 repo.hook(
2475 2482 b'txnclose-phase',
2476 2483 throw=False,
2477 2484 **pycompat.strkwargs(args)
2478 2485 )
2479 2486
2480 2487 repo.hook(
2481 2488 b'txnclose', throw=False, **pycompat.strkwargs(hookargs)
2482 2489 )
2483 2490
2484 2491 reporef()._afterlock(hookfunc)
2485 2492
2486 2493 tr.addfinalize(b'txnclose-hook', txnclosehook)
2487 2494 # Include a leading "-" to make it happen before the transaction summary
2488 2495 # reports registered via scmutil.registersummarycallback() whose names
2489 2496 # are 00-txnreport etc. That way, the caches will be warm when the
2490 2497 # callbacks run.
2491 2498 tr.addpostclose(b'-warm-cache', self._buildcacheupdater(tr))
2492 2499
2493 2500 def txnaborthook(tr2):
2494 2501 """To be run if transaction is aborted"""
2495 2502 reporef().hook(
2496 2503 b'txnabort', throw=False, **pycompat.strkwargs(tr2.hookargs)
2497 2504 )
2498 2505
2499 2506 tr.addabort(b'txnabort-hook', txnaborthook)
2500 2507 # avoid eager cache invalidation. in-memory data should be identical
2501 2508 # to stored data if transaction has no error.
2502 2509 tr.addpostclose(b'refresh-filecachestats', self._refreshfilecachestats)
2503 2510 self._transref = weakref.ref(tr)
2504 2511 scmutil.registersummarycallback(self, tr, desc)
2505 2512 return tr
2506 2513
2507 2514 def _journalfiles(self):
2508 2515 return (
2509 2516 (self.svfs, b'journal'),
2510 2517 (self.svfs, b'journal.narrowspec'),
2511 2518 (self.vfs, b'journal.narrowspec.dirstate'),
2512 2519 (self.vfs, b'journal.dirstate'),
2513 2520 (self.vfs, b'journal.branch'),
2514 2521 (self.vfs, b'journal.desc'),
2515 2522 (bookmarks.bookmarksvfs(self), b'journal.bookmarks'),
2516 2523 (self.svfs, b'journal.phaseroots'),
2517 2524 )
2518 2525
2519 2526 def undofiles(self):
2520 2527 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
2521 2528
2522 2529 @unfilteredmethod
2523 2530 def _writejournal(self, desc):
2524 2531 self.dirstate.savebackup(None, b'journal.dirstate')
2525 2532 narrowspec.savewcbackup(self, b'journal.narrowspec.dirstate')
2526 2533 narrowspec.savebackup(self, b'journal.narrowspec')
2527 2534 self.vfs.write(
2528 2535 b"journal.branch", encoding.fromlocal(self.dirstate.branch())
2529 2536 )
2530 2537 self.vfs.write(b"journal.desc", b"%d\n%s\n" % (len(self), desc))
2531 2538 bookmarksvfs = bookmarks.bookmarksvfs(self)
2532 2539 bookmarksvfs.write(
2533 2540 b"journal.bookmarks", bookmarksvfs.tryread(b"bookmarks")
2534 2541 )
2535 2542 self.svfs.write(b"journal.phaseroots", self.svfs.tryread(b"phaseroots"))
2536 2543
2537 2544 def recover(self):
2538 2545 with self.lock():
2539 2546 if self.svfs.exists(b"journal"):
2540 2547 self.ui.status(_(b"rolling back interrupted transaction\n"))
2541 2548 vfsmap = {
2542 2549 b'': self.svfs,
2543 2550 b'plain': self.vfs,
2544 2551 }
2545 2552 transaction.rollback(
2546 2553 self.svfs,
2547 2554 vfsmap,
2548 2555 b"journal",
2549 2556 self.ui.warn,
2550 2557 checkambigfiles=_cachedfiles,
2551 2558 )
2552 2559 self.invalidate()
2553 2560 return True
2554 2561 else:
2555 2562 self.ui.warn(_(b"no interrupted transaction available\n"))
2556 2563 return False
2557 2564
2558 2565 def rollback(self, dryrun=False, force=False):
2559 2566 wlock = lock = dsguard = None
2560 2567 try:
2561 2568 wlock = self.wlock()
2562 2569 lock = self.lock()
2563 2570 if self.svfs.exists(b"undo"):
2564 2571 dsguard = dirstateguard.dirstateguard(self, b'rollback')
2565 2572
2566 2573 return self._rollback(dryrun, force, dsguard)
2567 2574 else:
2568 2575 self.ui.warn(_(b"no rollback information available\n"))
2569 2576 return 1
2570 2577 finally:
2571 2578 release(dsguard, lock, wlock)
2572 2579
2573 2580 @unfilteredmethod # Until we get smarter cache management
2574 2581 def _rollback(self, dryrun, force, dsguard):
2575 2582 ui = self.ui
2576 2583 try:
2577 2584 args = self.vfs.read(b'undo.desc').splitlines()
2578 2585 (oldlen, desc, detail) = (int(args[0]), args[1], None)
2579 2586 if len(args) >= 3:
2580 2587 detail = args[2]
2581 2588 oldtip = oldlen - 1
2582 2589
2583 2590 if detail and ui.verbose:
2584 2591 msg = _(
2585 2592 b'repository tip rolled back to revision %d'
2586 2593 b' (undo %s: %s)\n'
2587 2594 ) % (oldtip, desc, detail)
2588 2595 else:
2589 2596 msg = _(
2590 2597 b'repository tip rolled back to revision %d (undo %s)\n'
2591 2598 ) % (oldtip, desc)
2592 2599 except IOError:
2593 2600 msg = _(b'rolling back unknown transaction\n')
2594 2601 desc = None
2595 2602
2596 2603 if not force and self[b'.'] != self[b'tip'] and desc == b'commit':
2597 2604 raise error.Abort(
2598 2605 _(
2599 2606 b'rollback of last commit while not checked out '
2600 2607 b'may lose data'
2601 2608 ),
2602 2609 hint=_(b'use -f to force'),
2603 2610 )
2604 2611
2605 2612 ui.status(msg)
2606 2613 if dryrun:
2607 2614 return 0
2608 2615
2609 2616 parents = self.dirstate.parents()
2610 2617 self.destroying()
2611 2618 vfsmap = {b'plain': self.vfs, b'': self.svfs}
2612 2619 transaction.rollback(
2613 2620 self.svfs, vfsmap, b'undo', ui.warn, checkambigfiles=_cachedfiles
2614 2621 )
2615 2622 bookmarksvfs = bookmarks.bookmarksvfs(self)
2616 2623 if bookmarksvfs.exists(b'undo.bookmarks'):
2617 2624 bookmarksvfs.rename(
2618 2625 b'undo.bookmarks', b'bookmarks', checkambig=True
2619 2626 )
2620 2627 if self.svfs.exists(b'undo.phaseroots'):
2621 2628 self.svfs.rename(b'undo.phaseroots', b'phaseroots', checkambig=True)
2622 2629 self.invalidate()
2623 2630
2624 2631 has_node = self.changelog.index.has_node
2625 2632 parentgone = any(not has_node(p) for p in parents)
2626 2633 if parentgone:
2627 2634 # prevent dirstateguard from overwriting already restored one
2628 2635 dsguard.close()
2629 2636
2630 2637 narrowspec.restorebackup(self, b'undo.narrowspec')
2631 2638 narrowspec.restorewcbackup(self, b'undo.narrowspec.dirstate')
2632 2639 self.dirstate.restorebackup(None, b'undo.dirstate')
2633 2640 try:
2634 2641 branch = self.vfs.read(b'undo.branch')
2635 2642 self.dirstate.setbranch(encoding.tolocal(branch))
2636 2643 except IOError:
2637 2644 ui.warn(
2638 2645 _(
2639 2646 b'named branch could not be reset: '
2640 2647 b'current branch is still \'%s\'\n'
2641 2648 )
2642 2649 % self.dirstate.branch()
2643 2650 )
2644 2651
2645 2652 parents = tuple([p.rev() for p in self[None].parents()])
2646 2653 if len(parents) > 1:
2647 2654 ui.status(
2648 2655 _(
2649 2656 b'working directory now based on '
2650 2657 b'revisions %d and %d\n'
2651 2658 )
2652 2659 % parents
2653 2660 )
2654 2661 else:
2655 2662 ui.status(
2656 2663 _(b'working directory now based on revision %d\n') % parents
2657 2664 )
2658 2665 mergestatemod.mergestate.clean(self)
2659 2666
2660 2667 # TODO: if we know which new heads may result from this rollback, pass
2661 2668 # them to destroy(), which will prevent the branchhead cache from being
2662 2669 # invalidated.
2663 2670 self.destroyed()
2664 2671 return 0
2665 2672
2666 2673 def _buildcacheupdater(self, newtransaction):
2667 2674 """called during transaction to build the callback updating cache
2668 2675
2669 2676 Lives on the repository to help extension who might want to augment
2670 2677 this logic. For this purpose, the created transaction is passed to the
2671 2678 method.
2672 2679 """
2673 2680 # we must avoid cyclic reference between repo and transaction.
2674 2681 reporef = weakref.ref(self)
2675 2682
2676 2683 def updater(tr):
2677 2684 repo = reporef()
2678 2685 repo.updatecaches(tr)
2679 2686
2680 2687 return updater
2681 2688
2682 2689 @unfilteredmethod
2683 2690 def updatecaches(self, tr=None, full=False):
2684 2691 """warm appropriate caches
2685 2692
2686 2693 If this function is called after a transaction closed. The transaction
2687 2694 will be available in the 'tr' argument. This can be used to selectively
2688 2695 update caches relevant to the changes in that transaction.
2689 2696
2690 2697 If 'full' is set, make sure all caches the function knows about have
2691 2698 up-to-date data. Even the ones usually loaded more lazily.
2692 2699 """
2693 2700 if tr is not None and tr.hookargs.get(b'source') == b'strip':
2694 2701 # During strip, many caches are invalid but
2695 2702 # later call to `destroyed` will refresh them.
2696 2703 return
2697 2704
2698 2705 if tr is None or tr.changes[b'origrepolen'] < len(self):
2699 2706 # accessing the 'served' branchmap should refresh all the others,
2700 2707 self.ui.debug(b'updating the branch cache\n')
2701 2708 self.filtered(b'served').branchmap()
2702 2709 self.filtered(b'served.hidden').branchmap()
2703 2710
2704 2711 if full:
2705 2712 unfi = self.unfiltered()
2706 2713
2707 2714 self.changelog.update_caches(transaction=tr)
2708 2715 self.manifestlog.update_caches(transaction=tr)
2709 2716
2710 2717 rbc = unfi.revbranchcache()
2711 2718 for r in unfi.changelog:
2712 2719 rbc.branchinfo(r)
2713 2720 rbc.write()
2714 2721
2715 2722 # ensure the working copy parents are in the manifestfulltextcache
2716 2723 for ctx in self[b'.'].parents():
2717 2724 ctx.manifest() # accessing the manifest is enough
2718 2725
2719 2726 # accessing fnode cache warms the cache
2720 2727 tagsmod.fnoderevs(self.ui, unfi, unfi.changelog.revs())
2721 2728 # accessing tags warm the cache
2722 2729 self.tags()
2723 2730 self.filtered(b'served').tags()
2724 2731
2725 2732 # The `full` arg is documented as updating even the lazily-loaded
2726 2733 # caches immediately, so we're forcing a write to cause these caches
2727 2734 # to be warmed up even if they haven't explicitly been requested
2728 2735 # yet (if they've never been used by hg, they won't ever have been
2729 2736 # written, even if they're a subset of another kind of cache that
2730 2737 # *has* been used).
2731 2738 for filt in repoview.filtertable.keys():
2732 2739 filtered = self.filtered(filt)
2733 2740 filtered.branchmap().write(filtered)
2734 2741
2735 2742 def invalidatecaches(self):
2736 2743
2737 2744 if '_tagscache' in vars(self):
2738 2745 # can't use delattr on proxy
2739 2746 del self.__dict__['_tagscache']
2740 2747
2741 2748 self._branchcaches.clear()
2742 2749 self.invalidatevolatilesets()
2743 2750 self._sparsesignaturecache.clear()
2744 2751
2745 2752 def invalidatevolatilesets(self):
2746 2753 self.filteredrevcache.clear()
2747 2754 obsolete.clearobscaches(self)
2748 2755 self._quick_access_changeid_invalidate()
2749 2756
2750 2757 def invalidatedirstate(self):
2751 2758 """Invalidates the dirstate, causing the next call to dirstate
2752 2759 to check if it was modified since the last time it was read,
2753 2760 rereading it if it has.
2754 2761
2755 2762 This is different to dirstate.invalidate() that it doesn't always
2756 2763 rereads the dirstate. Use dirstate.invalidate() if you want to
2757 2764 explicitly read the dirstate again (i.e. restoring it to a previous
2758 2765 known good state)."""
2759 2766 if hasunfilteredcache(self, 'dirstate'):
2760 2767 for k in self.dirstate._filecache:
2761 2768 try:
2762 2769 delattr(self.dirstate, k)
2763 2770 except AttributeError:
2764 2771 pass
2765 2772 delattr(self.unfiltered(), 'dirstate')
2766 2773
2767 2774 def invalidate(self, clearfilecache=False):
2768 2775 """Invalidates both store and non-store parts other than dirstate
2769 2776
2770 2777 If a transaction is running, invalidation of store is omitted,
2771 2778 because discarding in-memory changes might cause inconsistency
2772 2779 (e.g. incomplete fncache causes unintentional failure, but
2773 2780 redundant one doesn't).
2774 2781 """
2775 2782 unfiltered = self.unfiltered() # all file caches are stored unfiltered
2776 2783 for k in list(self._filecache.keys()):
2777 2784 # dirstate is invalidated separately in invalidatedirstate()
2778 2785 if k == b'dirstate':
2779 2786 continue
2780 2787 if (
2781 2788 k == b'changelog'
2782 2789 and self.currenttransaction()
2783 2790 and self.changelog._delayed
2784 2791 ):
2785 2792 # The changelog object may store unwritten revisions. We don't
2786 2793 # want to lose them.
2787 2794 # TODO: Solve the problem instead of working around it.
2788 2795 continue
2789 2796
2790 2797 if clearfilecache:
2791 2798 del self._filecache[k]
2792 2799 try:
2793 2800 delattr(unfiltered, k)
2794 2801 except AttributeError:
2795 2802 pass
2796 2803 self.invalidatecaches()
2797 2804 if not self.currenttransaction():
2798 2805 # TODO: Changing contents of store outside transaction
2799 2806 # causes inconsistency. We should make in-memory store
2800 2807 # changes detectable, and abort if changed.
2801 2808 self.store.invalidatecaches()
2802 2809
2803 2810 def invalidateall(self):
2804 2811 """Fully invalidates both store and non-store parts, causing the
2805 2812 subsequent operation to reread any outside changes."""
2806 2813 # extension should hook this to invalidate its caches
2807 2814 self.invalidate()
2808 2815 self.invalidatedirstate()
2809 2816
2810 2817 @unfilteredmethod
2811 2818 def _refreshfilecachestats(self, tr):
2812 2819 """Reload stats of cached files so that they are flagged as valid"""
2813 2820 for k, ce in self._filecache.items():
2814 2821 k = pycompat.sysstr(k)
2815 2822 if k == 'dirstate' or k not in self.__dict__:
2816 2823 continue
2817 2824 ce.refresh()
2818 2825
2819 2826 def _lock(
2820 2827 self,
2821 2828 vfs,
2822 2829 lockname,
2823 2830 wait,
2824 2831 releasefn,
2825 2832 acquirefn,
2826 2833 desc,
2827 2834 ):
2828 2835 timeout = 0
2829 2836 warntimeout = 0
2830 2837 if wait:
2831 2838 timeout = self.ui.configint(b"ui", b"timeout")
2832 2839 warntimeout = self.ui.configint(b"ui", b"timeout.warn")
2833 2840 # internal config: ui.signal-safe-lock
2834 2841 signalsafe = self.ui.configbool(b'ui', b'signal-safe-lock')
2835 2842
2836 2843 l = lockmod.trylock(
2837 2844 self.ui,
2838 2845 vfs,
2839 2846 lockname,
2840 2847 timeout,
2841 2848 warntimeout,
2842 2849 releasefn=releasefn,
2843 2850 acquirefn=acquirefn,
2844 2851 desc=desc,
2845 2852 signalsafe=signalsafe,
2846 2853 )
2847 2854 return l
2848 2855
2849 2856 def _afterlock(self, callback):
2850 2857 """add a callback to be run when the repository is fully unlocked
2851 2858
2852 2859 The callback will be executed when the outermost lock is released
2853 2860 (with wlock being higher level than 'lock')."""
2854 2861 for ref in (self._wlockref, self._lockref):
2855 2862 l = ref and ref()
2856 2863 if l and l.held:
2857 2864 l.postrelease.append(callback)
2858 2865 break
2859 2866 else: # no lock have been found.
2860 2867 callback(True)
2861 2868
2862 2869 def lock(self, wait=True):
2863 2870 """Lock the repository store (.hg/store) and return a weak reference
2864 2871 to the lock. Use this before modifying the store (e.g. committing or
2865 2872 stripping). If you are opening a transaction, get a lock as well.)
2866 2873
2867 2874 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2868 2875 'wlock' first to avoid a dead-lock hazard."""
2869 2876 l = self._currentlock(self._lockref)
2870 2877 if l is not None:
2871 2878 l.lock()
2872 2879 return l
2873 2880
2874 2881 l = self._lock(
2875 2882 vfs=self.svfs,
2876 2883 lockname=b"lock",
2877 2884 wait=wait,
2878 2885 releasefn=None,
2879 2886 acquirefn=self.invalidate,
2880 2887 desc=_(b'repository %s') % self.origroot,
2881 2888 )
2882 2889 self._lockref = weakref.ref(l)
2883 2890 return l
2884 2891
2885 2892 def wlock(self, wait=True):
2886 2893 """Lock the non-store parts of the repository (everything under
2887 2894 .hg except .hg/store) and return a weak reference to the lock.
2888 2895
2889 2896 Use this before modifying files in .hg.
2890 2897
2891 2898 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2892 2899 'wlock' first to avoid a dead-lock hazard."""
2893 2900 l = self._wlockref and self._wlockref()
2894 2901 if l is not None and l.held:
2895 2902 l.lock()
2896 2903 return l
2897 2904
2898 2905 # We do not need to check for non-waiting lock acquisition. Such
2899 2906 # acquisition would not cause dead-lock as they would just fail.
2900 2907 if wait and (
2901 2908 self.ui.configbool(b'devel', b'all-warnings')
2902 2909 or self.ui.configbool(b'devel', b'check-locks')
2903 2910 ):
2904 2911 if self._currentlock(self._lockref) is not None:
2905 2912 self.ui.develwarn(b'"wlock" acquired after "lock"')
2906 2913
2907 2914 def unlock():
2908 2915 if self.dirstate.pendingparentchange():
2909 2916 self.dirstate.invalidate()
2910 2917 else:
2911 2918 self.dirstate.write(None)
2912 2919
2913 2920 self._filecache[b'dirstate'].refresh()
2914 2921
2915 2922 l = self._lock(
2916 2923 self.vfs,
2917 2924 b"wlock",
2918 2925 wait,
2919 2926 unlock,
2920 2927 self.invalidatedirstate,
2921 2928 _(b'working directory of %s') % self.origroot,
2922 2929 )
2923 2930 self._wlockref = weakref.ref(l)
2924 2931 return l
2925 2932
2926 2933 def _currentlock(self, lockref):
2927 2934 """Returns the lock if it's held, or None if it's not."""
2928 2935 if lockref is None:
2929 2936 return None
2930 2937 l = lockref()
2931 2938 if l is None or not l.held:
2932 2939 return None
2933 2940 return l
2934 2941
2935 2942 def currentwlock(self):
2936 2943 """Returns the wlock if it's held, or None if it's not."""
2937 2944 return self._currentlock(self._wlockref)
2938 2945
2939 2946 def checkcommitpatterns(self, wctx, match, status, fail):
2940 2947 """check for commit arguments that aren't committable"""
2941 2948 if match.isexact() or match.prefix():
2942 2949 matched = set(status.modified + status.added + status.removed)
2943 2950
2944 2951 for f in match.files():
2945 2952 f = self.dirstate.normalize(f)
2946 2953 if f == b'.' or f in matched or f in wctx.substate:
2947 2954 continue
2948 2955 if f in status.deleted:
2949 2956 fail(f, _(b'file not found!'))
2950 2957 # Is it a directory that exists or used to exist?
2951 2958 if self.wvfs.isdir(f) or wctx.p1().hasdir(f):
2952 2959 d = f + b'/'
2953 2960 for mf in matched:
2954 2961 if mf.startswith(d):
2955 2962 break
2956 2963 else:
2957 2964 fail(f, _(b"no match under directory!"))
2958 2965 elif f not in self.dirstate:
2959 2966 fail(f, _(b"file not tracked!"))
2960 2967
2961 2968 @unfilteredmethod
2962 2969 def commit(
2963 2970 self,
2964 2971 text=b"",
2965 2972 user=None,
2966 2973 date=None,
2967 2974 match=None,
2968 2975 force=False,
2969 2976 editor=None,
2970 2977 extra=None,
2971 2978 ):
2972 2979 """Add a new revision to current repository.
2973 2980
2974 2981 Revision information is gathered from the working directory,
2975 2982 match can be used to filter the committed files. If editor is
2976 2983 supplied, it is called to get a commit message.
2977 2984 """
2978 2985 if extra is None:
2979 2986 extra = {}
2980 2987
2981 2988 def fail(f, msg):
2982 2989 raise error.InputError(b'%s: %s' % (f, msg))
2983 2990
2984 2991 if not match:
2985 2992 match = matchmod.always()
2986 2993
2987 2994 if not force:
2988 2995 match.bad = fail
2989 2996
2990 2997 # lock() for recent changelog (see issue4368)
2991 2998 with self.wlock(), self.lock():
2992 2999 wctx = self[None]
2993 3000 merge = len(wctx.parents()) > 1
2994 3001
2995 3002 if not force and merge and not match.always():
2996 3003 raise error.Abort(
2997 3004 _(
2998 3005 b'cannot partially commit a merge '
2999 3006 b'(do not specify files or patterns)'
3000 3007 )
3001 3008 )
3002 3009
3003 3010 status = self.status(match=match, clean=force)
3004 3011 if force:
3005 3012 status.modified.extend(
3006 3013 status.clean
3007 3014 ) # mq may commit clean files
3008 3015
3009 3016 # check subrepos
3010 3017 subs, commitsubs, newstate = subrepoutil.precommit(
3011 3018 self.ui, wctx, status, match, force=force
3012 3019 )
3013 3020
3014 3021 # make sure all explicit patterns are matched
3015 3022 if not force:
3016 3023 self.checkcommitpatterns(wctx, match, status, fail)
3017 3024
3018 3025 cctx = context.workingcommitctx(
3019 3026 self, status, text, user, date, extra
3020 3027 )
3021 3028
3022 3029 ms = mergestatemod.mergestate.read(self)
3023 3030 mergeutil.checkunresolved(ms)
3024 3031
3025 3032 # internal config: ui.allowemptycommit
3026 3033 if cctx.isempty() and not self.ui.configbool(
3027 3034 b'ui', b'allowemptycommit'
3028 3035 ):
3029 3036 self.ui.debug(b'nothing to commit, clearing merge state\n')
3030 3037 ms.reset()
3031 3038 return None
3032 3039
3033 3040 if merge and cctx.deleted():
3034 3041 raise error.Abort(_(b"cannot commit merge with missing files"))
3035 3042
3036 3043 if editor:
3037 3044 cctx._text = editor(self, cctx, subs)
3038 3045 edited = text != cctx._text
3039 3046
3040 3047 # Save commit message in case this transaction gets rolled back
3041 3048 # (e.g. by a pretxncommit hook). Leave the content alone on
3042 3049 # the assumption that the user will use the same editor again.
3043 3050 msgfn = self.savecommitmessage(cctx._text)
3044 3051
3045 3052 # commit subs and write new state
3046 3053 if subs:
3047 3054 uipathfn = scmutil.getuipathfn(self)
3048 3055 for s in sorted(commitsubs):
3049 3056 sub = wctx.sub(s)
3050 3057 self.ui.status(
3051 3058 _(b'committing subrepository %s\n')
3052 3059 % uipathfn(subrepoutil.subrelpath(sub))
3053 3060 )
3054 3061 sr = sub.commit(cctx._text, user, date)
3055 3062 newstate[s] = (newstate[s][0], sr)
3056 3063 subrepoutil.writestate(self, newstate)
3057 3064
3058 3065 p1, p2 = self.dirstate.parents()
3059 3066 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or b'')
3060 3067 try:
3061 3068 self.hook(
3062 3069 b"precommit", throw=True, parent1=hookp1, parent2=hookp2
3063 3070 )
3064 3071 with self.transaction(b'commit'):
3065 3072 ret = self.commitctx(cctx, True)
3066 3073 # update bookmarks, dirstate and mergestate
3067 3074 bookmarks.update(self, [p1, p2], ret)
3068 3075 cctx.markcommitted(ret)
3069 3076 ms.reset()
3070 3077 except: # re-raises
3071 3078 if edited:
3072 3079 self.ui.write(
3073 3080 _(b'note: commit message saved in %s\n') % msgfn
3074 3081 )
3075 3082 self.ui.write(
3076 3083 _(
3077 3084 b"note: use 'hg commit --logfile "
3078 3085 b".hg/last-message.txt --edit' to reuse it\n"
3079 3086 )
3080 3087 )
3081 3088 raise
3082 3089
3083 3090 def commithook(unused_success):
3084 3091 # hack for command that use a temporary commit (eg: histedit)
3085 3092 # temporary commit got stripped before hook release
3086 3093 if self.changelog.hasnode(ret):
3087 3094 self.hook(
3088 3095 b"commit", node=hex(ret), parent1=hookp1, parent2=hookp2
3089 3096 )
3090 3097
3091 3098 self._afterlock(commithook)
3092 3099 return ret
3093 3100
3094 3101 @unfilteredmethod
3095 3102 def commitctx(self, ctx, error=False, origctx=None):
3096 3103 return commit.commitctx(self, ctx, error=error, origctx=origctx)
3097 3104
3098 3105 @unfilteredmethod
3099 3106 def destroying(self):
3100 3107 """Inform the repository that nodes are about to be destroyed.
3101 3108 Intended for use by strip and rollback, so there's a common
3102 3109 place for anything that has to be done before destroying history.
3103 3110
3104 3111 This is mostly useful for saving state that is in memory and waiting
3105 3112 to be flushed when the current lock is released. Because a call to
3106 3113 destroyed is imminent, the repo will be invalidated causing those
3107 3114 changes to stay in memory (waiting for the next unlock), or vanish
3108 3115 completely.
3109 3116 """
3110 3117 # When using the same lock to commit and strip, the phasecache is left
3111 3118 # dirty after committing. Then when we strip, the repo is invalidated,
3112 3119 # causing those changes to disappear.
3113 3120 if '_phasecache' in vars(self):
3114 3121 self._phasecache.write()
3115 3122
3116 3123 @unfilteredmethod
3117 3124 def destroyed(self):
3118 3125 """Inform the repository that nodes have been destroyed.
3119 3126 Intended for use by strip and rollback, so there's a common
3120 3127 place for anything that has to be done after destroying history.
3121 3128 """
3122 3129 # When one tries to:
3123 3130 # 1) destroy nodes thus calling this method (e.g. strip)
3124 3131 # 2) use phasecache somewhere (e.g. commit)
3125 3132 #
3126 3133 # then 2) will fail because the phasecache contains nodes that were
3127 3134 # removed. We can either remove phasecache from the filecache,
3128 3135 # causing it to reload next time it is accessed, or simply filter
3129 3136 # the removed nodes now and write the updated cache.
3130 3137 self._phasecache.filterunknown(self)
3131 3138 self._phasecache.write()
3132 3139
3133 3140 # refresh all repository caches
3134 3141 self.updatecaches()
3135 3142
3136 3143 # Ensure the persistent tag cache is updated. Doing it now
3137 3144 # means that the tag cache only has to worry about destroyed
3138 3145 # heads immediately after a strip/rollback. That in turn
3139 3146 # guarantees that "cachetip == currenttip" (comparing both rev
3140 3147 # and node) always means no nodes have been added or destroyed.
3141 3148
3142 3149 # XXX this is suboptimal when qrefresh'ing: we strip the current
3143 3150 # head, refresh the tag cache, then immediately add a new head.
3144 3151 # But I think doing it this way is necessary for the "instant
3145 3152 # tag cache retrieval" case to work.
3146 3153 self.invalidate()
3147 3154
3148 3155 def status(
3149 3156 self,
3150 3157 node1=b'.',
3151 3158 node2=None,
3152 3159 match=None,
3153 3160 ignored=False,
3154 3161 clean=False,
3155 3162 unknown=False,
3156 3163 listsubrepos=False,
3157 3164 ):
3158 3165 '''a convenience method that calls node1.status(node2)'''
3159 3166 return self[node1].status(
3160 3167 node2, match, ignored, clean, unknown, listsubrepos
3161 3168 )
3162 3169
3163 3170 def addpostdsstatus(self, ps):
3164 3171 """Add a callback to run within the wlock, at the point at which status
3165 3172 fixups happen.
3166 3173
3167 3174 On status completion, callback(wctx, status) will be called with the
3168 3175 wlock held, unless the dirstate has changed from underneath or the wlock
3169 3176 couldn't be grabbed.
3170 3177
3171 3178 Callbacks should not capture and use a cached copy of the dirstate --
3172 3179 it might change in the meanwhile. Instead, they should access the
3173 3180 dirstate via wctx.repo().dirstate.
3174 3181
3175 3182 This list is emptied out after each status run -- extensions should
3176 3183 make sure it adds to this list each time dirstate.status is called.
3177 3184 Extensions should also make sure they don't call this for statuses
3178 3185 that don't involve the dirstate.
3179 3186 """
3180 3187
3181 3188 # The list is located here for uniqueness reasons -- it is actually
3182 3189 # managed by the workingctx, but that isn't unique per-repo.
3183 3190 self._postdsstatus.append(ps)
3184 3191
3185 3192 def postdsstatus(self):
3186 3193 """Used by workingctx to get the list of post-dirstate-status hooks."""
3187 3194 return self._postdsstatus
3188 3195
3189 3196 def clearpostdsstatus(self):
3190 3197 """Used by workingctx to clear post-dirstate-status hooks."""
3191 3198 del self._postdsstatus[:]
3192 3199
3193 3200 def heads(self, start=None):
3194 3201 if start is None:
3195 3202 cl = self.changelog
3196 3203 headrevs = reversed(cl.headrevs())
3197 3204 return [cl.node(rev) for rev in headrevs]
3198 3205
3199 3206 heads = self.changelog.heads(start)
3200 3207 # sort the output in rev descending order
3201 3208 return sorted(heads, key=self.changelog.rev, reverse=True)
3202 3209
3203 3210 def branchheads(self, branch=None, start=None, closed=False):
3204 3211 """return a (possibly filtered) list of heads for the given branch
3205 3212
3206 3213 Heads are returned in topological order, from newest to oldest.
3207 3214 If branch is None, use the dirstate branch.
3208 3215 If start is not None, return only heads reachable from start.
3209 3216 If closed is True, return heads that are marked as closed as well.
3210 3217 """
3211 3218 if branch is None:
3212 3219 branch = self[None].branch()
3213 3220 branches = self.branchmap()
3214 3221 if not branches.hasbranch(branch):
3215 3222 return []
3216 3223 # the cache returns heads ordered lowest to highest
3217 3224 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
3218 3225 if start is not None:
3219 3226 # filter out the heads that cannot be reached from startrev
3220 3227 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
3221 3228 bheads = [h for h in bheads if h in fbheads]
3222 3229 return bheads
3223 3230
3224 3231 def branches(self, nodes):
3225 3232 if not nodes:
3226 3233 nodes = [self.changelog.tip()]
3227 3234 b = []
3228 3235 for n in nodes:
3229 3236 t = n
3230 3237 while True:
3231 3238 p = self.changelog.parents(n)
3232 3239 if p[1] != nullid or p[0] == nullid:
3233 3240 b.append((t, n, p[0], p[1]))
3234 3241 break
3235 3242 n = p[0]
3236 3243 return b
3237 3244
3238 3245 def between(self, pairs):
3239 3246 r = []
3240 3247
3241 3248 for top, bottom in pairs:
3242 3249 n, l, i = top, [], 0
3243 3250 f = 1
3244 3251
3245 3252 while n != bottom and n != nullid:
3246 3253 p = self.changelog.parents(n)[0]
3247 3254 if i == f:
3248 3255 l.append(n)
3249 3256 f = f * 2
3250 3257 n = p
3251 3258 i += 1
3252 3259
3253 3260 r.append(l)
3254 3261
3255 3262 return r
3256 3263
3257 3264 def checkpush(self, pushop):
3258 3265 """Extensions can override this function if additional checks have
3259 3266 to be performed before pushing, or call it if they override push
3260 3267 command.
3261 3268 """
3262 3269
3263 3270 @unfilteredpropertycache
3264 3271 def prepushoutgoinghooks(self):
3265 3272 """Return util.hooks consists of a pushop with repo, remote, outgoing
3266 3273 methods, which are called before pushing changesets.
3267 3274 """
3268 3275 return util.hooks()
3269 3276
3270 3277 def pushkey(self, namespace, key, old, new):
3271 3278 try:
3272 3279 tr = self.currenttransaction()
3273 3280 hookargs = {}
3274 3281 if tr is not None:
3275 3282 hookargs.update(tr.hookargs)
3276 3283 hookargs = pycompat.strkwargs(hookargs)
3277 3284 hookargs['namespace'] = namespace
3278 3285 hookargs['key'] = key
3279 3286 hookargs['old'] = old
3280 3287 hookargs['new'] = new
3281 3288 self.hook(b'prepushkey', throw=True, **hookargs)
3282 3289 except error.HookAbort as exc:
3283 3290 self.ui.write_err(_(b"pushkey-abort: %s\n") % exc)
3284 3291 if exc.hint:
3285 3292 self.ui.write_err(_(b"(%s)\n") % exc.hint)
3286 3293 return False
3287 3294 self.ui.debug(b'pushing key for "%s:%s"\n' % (namespace, key))
3288 3295 ret = pushkey.push(self, namespace, key, old, new)
3289 3296
3290 3297 def runhook(unused_success):
3291 3298 self.hook(
3292 3299 b'pushkey',
3293 3300 namespace=namespace,
3294 3301 key=key,
3295 3302 old=old,
3296 3303 new=new,
3297 3304 ret=ret,
3298 3305 )
3299 3306
3300 3307 self._afterlock(runhook)
3301 3308 return ret
3302 3309
3303 3310 def listkeys(self, namespace):
3304 3311 self.hook(b'prelistkeys', throw=True, namespace=namespace)
3305 3312 self.ui.debug(b'listing keys for "%s"\n' % namespace)
3306 3313 values = pushkey.list(self, namespace)
3307 3314 self.hook(b'listkeys', namespace=namespace, values=values)
3308 3315 return values
3309 3316
3310 3317 def debugwireargs(self, one, two, three=None, four=None, five=None):
3311 3318 '''used to test argument passing over the wire'''
3312 3319 return b"%s %s %s %s %s" % (
3313 3320 one,
3314 3321 two,
3315 3322 pycompat.bytestr(three),
3316 3323 pycompat.bytestr(four),
3317 3324 pycompat.bytestr(five),
3318 3325 )
3319 3326
3320 3327 def savecommitmessage(self, text):
3321 3328 fp = self.vfs(b'last-message.txt', b'wb')
3322 3329 try:
3323 3330 fp.write(text)
3324 3331 finally:
3325 3332 fp.close()
3326 3333 return self.pathto(fp.name[len(self.root) + 1 :])
3327 3334
3328 3335
3329 3336 # used to avoid circular references so destructors work
3330 3337 def aftertrans(files):
3331 3338 renamefiles = [tuple(t) for t in files]
3332 3339
3333 3340 def a():
3334 3341 for vfs, src, dest in renamefiles:
3335 3342 # if src and dest refer to a same file, vfs.rename is a no-op,
3336 3343 # leaving both src and dest on disk. delete dest to make sure
3337 3344 # the rename couldn't be such a no-op.
3338 3345 vfs.tryunlink(dest)
3339 3346 try:
3340 3347 vfs.rename(src, dest)
3341 3348 except OSError: # journal file does not yet exist
3342 3349 pass
3343 3350
3344 3351 return a
3345 3352
3346 3353
3347 3354 def undoname(fn):
3348 3355 base, name = os.path.split(fn)
3349 3356 assert name.startswith(b'journal')
3350 3357 return os.path.join(base, name.replace(b'journal', b'undo', 1))
3351 3358
3352 3359
3353 3360 def instance(ui, path, create, intents=None, createopts=None):
3354 3361 localpath = util.urllocalpath(path)
3355 3362 if create:
3356 3363 createrepository(ui, localpath, createopts=createopts)
3357 3364
3358 3365 return makelocalrepository(ui, localpath, intents=intents)
3359 3366
3360 3367
3361 3368 def islocal(path):
3362 3369 return True
3363 3370
3364 3371
3365 3372 def defaultcreateopts(ui, createopts=None):
3366 3373 """Populate the default creation options for a repository.
3367 3374
3368 3375 A dictionary of explicitly requested creation options can be passed
3369 3376 in. Missing keys will be populated.
3370 3377 """
3371 3378 createopts = dict(createopts or {})
3372 3379
3373 3380 if b'backend' not in createopts:
3374 3381 # experimental config: storage.new-repo-backend
3375 3382 createopts[b'backend'] = ui.config(b'storage', b'new-repo-backend')
3376 3383
3377 3384 return createopts
3378 3385
3379 3386
3380 3387 def newreporequirements(ui, createopts):
3381 3388 """Determine the set of requirements for a new local repository.
3382 3389
3383 3390 Extensions can wrap this function to specify custom requirements for
3384 3391 new repositories.
3385 3392 """
3386 3393 # If the repo is being created from a shared repository, we copy
3387 3394 # its requirements.
3388 3395 if b'sharedrepo' in createopts:
3389 3396 requirements = set(createopts[b'sharedrepo'].requirements)
3390 3397 if createopts.get(b'sharedrelative'):
3391 3398 requirements.add(requirementsmod.RELATIVE_SHARED_REQUIREMENT)
3392 3399 else:
3393 3400 requirements.add(requirementsmod.SHARED_REQUIREMENT)
3394 3401
3395 3402 return requirements
3396 3403
3397 3404 if b'backend' not in createopts:
3398 3405 raise error.ProgrammingError(
3399 3406 b'backend key not present in createopts; '
3400 3407 b'was defaultcreateopts() called?'
3401 3408 )
3402 3409
3403 3410 if createopts[b'backend'] != b'revlogv1':
3404 3411 raise error.Abort(
3405 3412 _(
3406 3413 b'unable to determine repository requirements for '
3407 3414 b'storage backend: %s'
3408 3415 )
3409 3416 % createopts[b'backend']
3410 3417 )
3411 3418
3412 3419 requirements = {requirementsmod.REVLOGV1_REQUIREMENT}
3413 3420 if ui.configbool(b'format', b'usestore'):
3414 3421 requirements.add(requirementsmod.STORE_REQUIREMENT)
3415 3422 if ui.configbool(b'format', b'usefncache'):
3416 3423 requirements.add(requirementsmod.FNCACHE_REQUIREMENT)
3417 3424 if ui.configbool(b'format', b'dotencode'):
3418 3425 requirements.add(requirementsmod.DOTENCODE_REQUIREMENT)
3419 3426
3420 3427 compengines = ui.configlist(b'format', b'revlog-compression')
3421 3428 for compengine in compengines:
3422 3429 if compengine in util.compengines:
3423 3430 break
3424 3431 else:
3425 3432 raise error.Abort(
3426 3433 _(
3427 3434 b'compression engines %s defined by '
3428 3435 b'format.revlog-compression not available'
3429 3436 )
3430 3437 % b', '.join(b'"%s"' % e for e in compengines),
3431 3438 hint=_(
3432 3439 b'run "hg debuginstall" to list available '
3433 3440 b'compression engines'
3434 3441 ),
3435 3442 )
3436 3443
3437 3444 # zlib is the historical default and doesn't need an explicit requirement.
3438 3445 if compengine == b'zstd':
3439 3446 requirements.add(b'revlog-compression-zstd')
3440 3447 elif compengine != b'zlib':
3441 3448 requirements.add(b'exp-compression-%s' % compengine)
3442 3449
3443 3450 if scmutil.gdinitconfig(ui):
3444 3451 requirements.add(requirementsmod.GENERALDELTA_REQUIREMENT)
3445 3452 if ui.configbool(b'format', b'sparse-revlog'):
3446 3453 requirements.add(requirementsmod.SPARSEREVLOG_REQUIREMENT)
3447 3454
3448 3455 # experimental config: format.exp-use-side-data
3449 3456 if ui.configbool(b'format', b'exp-use-side-data'):
3450 3457 requirements.discard(requirementsmod.REVLOGV1_REQUIREMENT)
3451 3458 requirements.add(requirementsmod.REVLOGV2_REQUIREMENT)
3452 3459 requirements.add(requirementsmod.SIDEDATA_REQUIREMENT)
3453 3460 # experimental config: format.exp-use-copies-side-data-changeset
3454 3461 if ui.configbool(b'format', b'exp-use-copies-side-data-changeset'):
3455 3462 requirements.discard(requirementsmod.REVLOGV1_REQUIREMENT)
3456 3463 requirements.add(requirementsmod.REVLOGV2_REQUIREMENT)
3457 3464 requirements.add(requirementsmod.SIDEDATA_REQUIREMENT)
3458 3465 requirements.add(requirementsmod.COPIESSDC_REQUIREMENT)
3459 3466 if ui.configbool(b'experimental', b'treemanifest'):
3460 3467 requirements.add(requirementsmod.TREEMANIFEST_REQUIREMENT)
3461 3468
3462 3469 revlogv2 = ui.config(b'experimental', b'revlogv2')
3463 3470 if revlogv2 == b'enable-unstable-format-and-corrupt-my-data':
3464 3471 requirements.discard(requirementsmod.REVLOGV1_REQUIREMENT)
3465 3472 # generaldelta is implied by revlogv2.
3466 3473 requirements.discard(requirementsmod.GENERALDELTA_REQUIREMENT)
3467 3474 requirements.add(requirementsmod.REVLOGV2_REQUIREMENT)
3468 3475 # experimental config: format.internal-phase
3469 3476 if ui.configbool(b'format', b'internal-phase'):
3470 3477 requirements.add(requirementsmod.INTERNAL_PHASE_REQUIREMENT)
3471 3478
3472 3479 if createopts.get(b'narrowfiles'):
3473 3480 requirements.add(requirementsmod.NARROW_REQUIREMENT)
3474 3481
3475 3482 if createopts.get(b'lfs'):
3476 3483 requirements.add(b'lfs')
3477 3484
3478 3485 if ui.configbool(b'format', b'bookmarks-in-store'):
3479 3486 requirements.add(bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT)
3480 3487
3481 3488 if ui.configbool(b'format', b'use-persistent-nodemap'):
3482 3489 requirements.add(requirementsmod.NODEMAP_REQUIREMENT)
3483 3490
3484 3491 # if share-safe is enabled, let's create the new repository with the new
3485 3492 # requirement
3486 3493 if ui.configbool(b'format', b'use-share-safe'):
3487 3494 requirements.add(requirementsmod.SHARESAFE_REQUIREMENT)
3488 3495
3489 3496 return requirements
3490 3497
3491 3498
3492 3499 def checkrequirementscompat(ui, requirements):
3493 3500 """Checks compatibility of repository requirements enabled and disabled.
3494 3501
3495 3502 Returns a set of requirements which needs to be dropped because dependend
3496 3503 requirements are not enabled. Also warns users about it"""
3497 3504
3498 3505 dropped = set()
3499 3506
3500 3507 if requirementsmod.STORE_REQUIREMENT not in requirements:
3501 3508 if bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT in requirements:
3502 3509 ui.warn(
3503 3510 _(
3504 3511 b'ignoring enabled \'format.bookmarks-in-store\' config '
3505 3512 b'beacuse it is incompatible with disabled '
3506 3513 b'\'format.usestore\' config\n'
3507 3514 )
3508 3515 )
3509 3516 dropped.add(bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT)
3510 3517
3511 3518 if (
3512 3519 requirementsmod.SHARED_REQUIREMENT in requirements
3513 3520 or requirementsmod.RELATIVE_SHARED_REQUIREMENT in requirements
3514 3521 ):
3515 3522 raise error.Abort(
3516 3523 _(
3517 3524 b"cannot create shared repository as source was created"
3518 3525 b" with 'format.usestore' config disabled"
3519 3526 )
3520 3527 )
3521 3528
3522 3529 if requirementsmod.SHARESAFE_REQUIREMENT in requirements:
3523 3530 ui.warn(
3524 3531 _(
3525 3532 b"ignoring enabled 'format.use-share-safe' config because "
3526 3533 b"it is incompatible with disabled 'format.usestore'"
3527 3534 b" config\n"
3528 3535 )
3529 3536 )
3530 3537 dropped.add(requirementsmod.SHARESAFE_REQUIREMENT)
3531 3538
3532 3539 return dropped
3533 3540
3534 3541
3535 3542 def filterknowncreateopts(ui, createopts):
3536 3543 """Filters a dict of repo creation options against options that are known.
3537 3544
3538 3545 Receives a dict of repo creation options and returns a dict of those
3539 3546 options that we don't know how to handle.
3540 3547
3541 3548 This function is called as part of repository creation. If the
3542 3549 returned dict contains any items, repository creation will not
3543 3550 be allowed, as it means there was a request to create a repository
3544 3551 with options not recognized by loaded code.
3545 3552
3546 3553 Extensions can wrap this function to filter out creation options
3547 3554 they know how to handle.
3548 3555 """
3549 3556 known = {
3550 3557 b'backend',
3551 3558 b'lfs',
3552 3559 b'narrowfiles',
3553 3560 b'sharedrepo',
3554 3561 b'sharedrelative',
3555 3562 b'shareditems',
3556 3563 b'shallowfilestore',
3557 3564 }
3558 3565
3559 3566 return {k: v for k, v in createopts.items() if k not in known}
3560 3567
3561 3568
3562 3569 def createrepository(ui, path, createopts=None):
3563 3570 """Create a new repository in a vfs.
3564 3571
3565 3572 ``path`` path to the new repo's working directory.
3566 3573 ``createopts`` options for the new repository.
3567 3574
3568 3575 The following keys for ``createopts`` are recognized:
3569 3576
3570 3577 backend
3571 3578 The storage backend to use.
3572 3579 lfs
3573 3580 Repository will be created with ``lfs`` requirement. The lfs extension
3574 3581 will automatically be loaded when the repository is accessed.
3575 3582 narrowfiles
3576 3583 Set up repository to support narrow file storage.
3577 3584 sharedrepo
3578 3585 Repository object from which storage should be shared.
3579 3586 sharedrelative
3580 3587 Boolean indicating if the path to the shared repo should be
3581 3588 stored as relative. By default, the pointer to the "parent" repo
3582 3589 is stored as an absolute path.
3583 3590 shareditems
3584 3591 Set of items to share to the new repository (in addition to storage).
3585 3592 shallowfilestore
3586 3593 Indicates that storage for files should be shallow (not all ancestor
3587 3594 revisions are known).
3588 3595 """
3589 3596 createopts = defaultcreateopts(ui, createopts=createopts)
3590 3597
3591 3598 unknownopts = filterknowncreateopts(ui, createopts)
3592 3599
3593 3600 if not isinstance(unknownopts, dict):
3594 3601 raise error.ProgrammingError(
3595 3602 b'filterknowncreateopts() did not return a dict'
3596 3603 )
3597 3604
3598 3605 if unknownopts:
3599 3606 raise error.Abort(
3600 3607 _(
3601 3608 b'unable to create repository because of unknown '
3602 3609 b'creation option: %s'
3603 3610 )
3604 3611 % b', '.join(sorted(unknownopts)),
3605 3612 hint=_(b'is a required extension not loaded?'),
3606 3613 )
3607 3614
3608 3615 requirements = newreporequirements(ui, createopts=createopts)
3609 3616 requirements -= checkrequirementscompat(ui, requirements)
3610 3617
3611 3618 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
3612 3619
3613 3620 hgvfs = vfsmod.vfs(wdirvfs.join(b'.hg'))
3614 3621 if hgvfs.exists():
3615 3622 raise error.RepoError(_(b'repository %s already exists') % path)
3616 3623
3617 3624 if b'sharedrepo' in createopts:
3618 3625 sharedpath = createopts[b'sharedrepo'].sharedpath
3619 3626
3620 3627 if createopts.get(b'sharedrelative'):
3621 3628 try:
3622 3629 sharedpath = os.path.relpath(sharedpath, hgvfs.base)
3623 3630 except (IOError, ValueError) as e:
3624 3631 # ValueError is raised on Windows if the drive letters differ
3625 3632 # on each path.
3626 3633 raise error.Abort(
3627 3634 _(b'cannot calculate relative path'),
3628 3635 hint=stringutil.forcebytestr(e),
3629 3636 )
3630 3637
3631 3638 if not wdirvfs.exists():
3632 3639 wdirvfs.makedirs()
3633 3640
3634 3641 hgvfs.makedir(notindexed=True)
3635 3642 if b'sharedrepo' not in createopts:
3636 3643 hgvfs.mkdir(b'cache')
3637 3644 hgvfs.mkdir(b'wcache')
3638 3645
3639 3646 has_store = requirementsmod.STORE_REQUIREMENT in requirements
3640 3647 if has_store and b'sharedrepo' not in createopts:
3641 3648 hgvfs.mkdir(b'store')
3642 3649
3643 3650 # We create an invalid changelog outside the store so very old
3644 3651 # Mercurial versions (which didn't know about the requirements
3645 3652 # file) encounter an error on reading the changelog. This
3646 3653 # effectively locks out old clients and prevents them from
3647 3654 # mucking with a repo in an unknown format.
3648 3655 #
3649 3656 # The revlog header has version 65535, which won't be recognized by
3650 3657 # such old clients.
3651 3658 hgvfs.append(
3652 3659 b'00changelog.i',
3653 3660 b'\0\0\xFF\xFF dummy changelog to prevent using the old repo '
3654 3661 b'layout',
3655 3662 )
3656 3663
3657 3664 # Filter the requirements into working copy and store ones
3658 3665 wcreq, storereq = scmutil.filterrequirements(requirements)
3659 3666 # write working copy ones
3660 3667 scmutil.writerequires(hgvfs, wcreq)
3661 3668 # If there are store requirements and the current repository
3662 3669 # is not a shared one, write stored requirements
3663 3670 # For new shared repository, we don't need to write the store
3664 3671 # requirements as they are already present in store requires
3665 3672 if storereq and b'sharedrepo' not in createopts:
3666 3673 storevfs = vfsmod.vfs(hgvfs.join(b'store'), cacheaudited=True)
3667 3674 scmutil.writerequires(storevfs, storereq)
3668 3675
3669 3676 # Write out file telling readers where to find the shared store.
3670 3677 if b'sharedrepo' in createopts:
3671 3678 hgvfs.write(b'sharedpath', sharedpath)
3672 3679
3673 3680 if createopts.get(b'shareditems'):
3674 3681 shared = b'\n'.join(sorted(createopts[b'shareditems'])) + b'\n'
3675 3682 hgvfs.write(b'shared', shared)
3676 3683
3677 3684
3678 3685 def poisonrepository(repo):
3679 3686 """Poison a repository instance so it can no longer be used."""
3680 3687 # Perform any cleanup on the instance.
3681 3688 repo.close()
3682 3689
3683 3690 # Our strategy is to replace the type of the object with one that
3684 3691 # has all attribute lookups result in error.
3685 3692 #
3686 3693 # But we have to allow the close() method because some constructors
3687 3694 # of repos call close() on repo references.
3688 3695 class poisonedrepository(object):
3689 3696 def __getattribute__(self, item):
3690 3697 if item == 'close':
3691 3698 return object.__getattribute__(self, item)
3692 3699
3693 3700 raise error.ProgrammingError(
3694 3701 b'repo instances should not be used after unshare'
3695 3702 )
3696 3703
3697 3704 def close(self):
3698 3705 pass
3699 3706
3700 3707 # We may have a repoview, which intercepts __setattr__. So be sure
3701 3708 # we operate at the lowest level possible.
3702 3709 object.__setattr__(repo, '__class__', poisonedrepository)
@@ -1,3199 +1,3199 b''
1 1 # revlog.py - storage back-end 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 """Storage back-end for Mercurial.
9 9
10 10 This provides efficient delta storage with O(1) retrieve and append
11 11 and O(changes) merge between branches.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import collections
17 17 import contextlib
18 18 import errno
19 19 import io
20 20 import os
21 21 import struct
22 22 import zlib
23 23
24 24 # import stuff from node for others to import from revlog
25 25 from .node import (
26 26 bin,
27 27 hex,
28 28 nullhex,
29 29 nullid,
30 30 nullrev,
31 31 short,
32 32 wdirfilenodeids,
33 33 wdirhex,
34 34 wdirid,
35 35 wdirrev,
36 36 )
37 37 from .i18n import _
38 38 from .pycompat import getattr
39 39 from .revlogutils.constants import (
40 40 FLAG_GENERALDELTA,
41 41 FLAG_INLINE_DATA,
42 42 REVLOGV0,
43 43 REVLOGV1,
44 44 REVLOGV1_FLAGS,
45 45 REVLOGV2,
46 46 REVLOGV2_FLAGS,
47 47 REVLOG_DEFAULT_FLAGS,
48 48 REVLOG_DEFAULT_FORMAT,
49 49 REVLOG_DEFAULT_VERSION,
50 50 )
51 51 from .revlogutils.flagutil import (
52 52 REVIDX_DEFAULT_FLAGS,
53 53 REVIDX_ELLIPSIS,
54 54 REVIDX_EXTSTORED,
55 55 REVIDX_FLAGS_ORDER,
56 56 REVIDX_HASCOPIESINFO,
57 57 REVIDX_ISCENSORED,
58 58 REVIDX_RAWTEXT_CHANGING_FLAGS,
59 59 REVIDX_SIDEDATA,
60 60 )
61 61 from .thirdparty import attr
62 62 from . import (
63 63 ancestor,
64 64 dagop,
65 65 error,
66 66 mdiff,
67 67 policy,
68 68 pycompat,
69 69 templatefilters,
70 70 util,
71 71 )
72 72 from .interfaces import (
73 73 repository,
74 74 util as interfaceutil,
75 75 )
76 76 from .revlogutils import (
77 77 deltas as deltautil,
78 78 flagutil,
79 79 nodemap as nodemaputil,
80 80 sidedata as sidedatautil,
81 81 )
82 82 from .utils import (
83 83 storageutil,
84 84 stringutil,
85 85 )
86 86 from .pure import parsers as pureparsers
87 87
88 88 # blanked usage of all the name to prevent pyflakes constraints
89 89 # We need these name available in the module for extensions.
90 90 REVLOGV0
91 91 REVLOGV1
92 92 REVLOGV2
93 93 FLAG_INLINE_DATA
94 94 FLAG_GENERALDELTA
95 95 REVLOG_DEFAULT_FLAGS
96 96 REVLOG_DEFAULT_FORMAT
97 97 REVLOG_DEFAULT_VERSION
98 98 REVLOGV1_FLAGS
99 99 REVLOGV2_FLAGS
100 100 REVIDX_ISCENSORED
101 101 REVIDX_ELLIPSIS
102 102 REVIDX_SIDEDATA
103 103 REVIDX_HASCOPIESINFO
104 104 REVIDX_EXTSTORED
105 105 REVIDX_DEFAULT_FLAGS
106 106 REVIDX_FLAGS_ORDER
107 107 REVIDX_RAWTEXT_CHANGING_FLAGS
108 108
109 109 parsers = policy.importmod('parsers')
110 110 rustancestor = policy.importrust('ancestor')
111 111 rustdagop = policy.importrust('dagop')
112 112 rustrevlog = policy.importrust('revlog')
113 113
114 114 # Aliased for performance.
115 115 _zlibdecompress = zlib.decompress
116 116
117 117 # max size of revlog with inline data
118 118 _maxinline = 131072
119 119 _chunksize = 1048576
120 120
121 121 # Flag processors for REVIDX_ELLIPSIS.
122 122 def ellipsisreadprocessor(rl, text):
123 123 return text, False
124 124
125 125
126 126 def ellipsiswriteprocessor(rl, text):
127 127 return text, False
128 128
129 129
130 130 def ellipsisrawprocessor(rl, text):
131 131 return False
132 132
133 133
134 134 ellipsisprocessor = (
135 135 ellipsisreadprocessor,
136 136 ellipsiswriteprocessor,
137 137 ellipsisrawprocessor,
138 138 )
139 139
140 140
141 141 def getoffset(q):
142 142 return int(q >> 16)
143 143
144 144
145 145 def gettype(q):
146 146 return int(q & 0xFFFF)
147 147
148 148
149 149 def offset_type(offset, type):
150 150 if (type & ~flagutil.REVIDX_KNOWN_FLAGS) != 0:
151 151 raise ValueError(b'unknown revlog index flags')
152 152 return int(int(offset) << 16 | type)
153 153
154 154
155 155 def _verify_revision(rl, skipflags, state, node):
156 156 """Verify the integrity of the given revlog ``node`` while providing a hook
157 157 point for extensions to influence the operation."""
158 158 if skipflags:
159 159 state[b'skipread'].add(node)
160 160 else:
161 161 # Side-effect: read content and verify hash.
162 162 rl.revision(node)
163 163
164 164
165 165 # True if a fast implementation for persistent-nodemap is available
166 166 #
167 167 # We also consider we have a "fast" implementation in "pure" python because
168 168 # people using pure don't really have performance consideration (and a
169 169 # wheelbarrow of other slowness source)
170 170 HAS_FAST_PERSISTENT_NODEMAP = rustrevlog is not None or util.safehasattr(
171 171 parsers, 'BaseIndexObject'
172 172 )
173 173
174 174
175 175 @attr.s(slots=True, frozen=True)
176 176 class _revisioninfo(object):
177 177 """Information about a revision that allows building its fulltext
178 178 node: expected hash of the revision
179 179 p1, p2: parent revs of the revision
180 180 btext: built text cache consisting of a one-element list
181 181 cachedelta: (baserev, uncompressed_delta) or None
182 182 flags: flags associated to the revision storage
183 183
184 184 One of btext[0] or cachedelta must be set.
185 185 """
186 186
187 187 node = attr.ib()
188 188 p1 = attr.ib()
189 189 p2 = attr.ib()
190 190 btext = attr.ib()
191 191 textlen = attr.ib()
192 192 cachedelta = attr.ib()
193 193 flags = attr.ib()
194 194
195 195
196 196 @interfaceutil.implementer(repository.irevisiondelta)
197 197 @attr.s(slots=True)
198 198 class revlogrevisiondelta(object):
199 199 node = attr.ib()
200 200 p1node = attr.ib()
201 201 p2node = attr.ib()
202 202 basenode = attr.ib()
203 203 flags = attr.ib()
204 204 baserevisionsize = attr.ib()
205 205 revision = attr.ib()
206 206 delta = attr.ib()
207 207 linknode = attr.ib(default=None)
208 208
209 209
210 210 @interfaceutil.implementer(repository.iverifyproblem)
211 211 @attr.s(frozen=True)
212 212 class revlogproblem(object):
213 213 warning = attr.ib(default=None)
214 214 error = attr.ib(default=None)
215 215 node = attr.ib(default=None)
216 216
217 217
218 218 # index v0:
219 219 # 4 bytes: offset
220 220 # 4 bytes: compressed length
221 221 # 4 bytes: base rev
222 222 # 4 bytes: link rev
223 223 # 20 bytes: parent 1 nodeid
224 224 # 20 bytes: parent 2 nodeid
225 225 # 20 bytes: nodeid
226 226 indexformatv0 = struct.Struct(b">4l20s20s20s")
227 227 indexformatv0_pack = indexformatv0.pack
228 228 indexformatv0_unpack = indexformatv0.unpack
229 229
230 230
231 231 class revlogoldindex(list):
232 232 @property
233 233 def nodemap(self):
234 234 msg = b"index.nodemap is deprecated, use index.[has_node|rev|get_rev]"
235 235 util.nouideprecwarn(msg, b'5.3', stacklevel=2)
236 236 return self._nodemap
237 237
238 238 @util.propertycache
239 239 def _nodemap(self):
240 240 nodemap = nodemaputil.NodeMap({nullid: nullrev})
241 241 for r in range(0, len(self)):
242 242 n = self[r][7]
243 243 nodemap[n] = r
244 244 return nodemap
245 245
246 246 def has_node(self, node):
247 247 """return True if the node exist in the index"""
248 248 return node in self._nodemap
249 249
250 250 def rev(self, node):
251 251 """return a revision for a node
252 252
253 253 If the node is unknown, raise a RevlogError"""
254 254 return self._nodemap[node]
255 255
256 256 def get_rev(self, node):
257 257 """return a revision for a node
258 258
259 259 If the node is unknown, return None"""
260 260 return self._nodemap.get(node)
261 261
262 262 def append(self, tup):
263 263 self._nodemap[tup[7]] = len(self)
264 264 super(revlogoldindex, self).append(tup)
265 265
266 266 def __delitem__(self, i):
267 267 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
268 268 raise ValueError(b"deleting slices only supports a:-1 with step 1")
269 269 for r in pycompat.xrange(i.start, len(self)):
270 270 del self._nodemap[self[r][7]]
271 271 super(revlogoldindex, self).__delitem__(i)
272 272
273 273 def clearcaches(self):
274 274 self.__dict__.pop('_nodemap', None)
275 275
276 276 def __getitem__(self, i):
277 277 if i == -1:
278 278 return (0, 0, 0, -1, -1, -1, -1, nullid)
279 279 return list.__getitem__(self, i)
280 280
281 281
282 282 class revlogoldio(object):
283 283 def __init__(self):
284 284 self.size = indexformatv0.size
285 285
286 286 def parseindex(self, data, inline):
287 287 s = self.size
288 288 index = []
289 289 nodemap = nodemaputil.NodeMap({nullid: nullrev})
290 290 n = off = 0
291 291 l = len(data)
292 292 while off + s <= l:
293 293 cur = data[off : off + s]
294 294 off += s
295 295 e = indexformatv0_unpack(cur)
296 296 # transform to revlogv1 format
297 297 e2 = (
298 298 offset_type(e[0], 0),
299 299 e[1],
300 300 -1,
301 301 e[2],
302 302 e[3],
303 303 nodemap.get(e[4], nullrev),
304 304 nodemap.get(e[5], nullrev),
305 305 e[6],
306 306 )
307 307 index.append(e2)
308 308 nodemap[e[6]] = n
309 309 n += 1
310 310
311 311 index = revlogoldindex(index)
312 312 return index, None
313 313
314 314 def packentry(self, entry, node, version, rev):
315 315 if gettype(entry[0]):
316 316 raise error.RevlogError(
317 317 _(b'index entry flags need revlog version 1')
318 318 )
319 319 e2 = (
320 320 getoffset(entry[0]),
321 321 entry[1],
322 322 entry[3],
323 323 entry[4],
324 324 node(entry[5]),
325 325 node(entry[6]),
326 326 entry[7],
327 327 )
328 328 return indexformatv0_pack(*e2)
329 329
330 330
331 331 # index ng:
332 332 # 6 bytes: offset
333 333 # 2 bytes: flags
334 334 # 4 bytes: compressed length
335 335 # 4 bytes: uncompressed length
336 336 # 4 bytes: base rev
337 337 # 4 bytes: link rev
338 338 # 4 bytes: parent 1 rev
339 339 # 4 bytes: parent 2 rev
340 340 # 32 bytes: nodeid
341 341 indexformatng = struct.Struct(b">Qiiiiii20s12x")
342 342 indexformatng_pack = indexformatng.pack
343 343 versionformat = struct.Struct(b">I")
344 344 versionformat_pack = versionformat.pack
345 345 versionformat_unpack = versionformat.unpack
346 346
347 347 # corresponds to uncompressed length of indexformatng (2 gigs, 4-byte
348 348 # signed integer)
349 349 _maxentrysize = 0x7FFFFFFF
350 350
351 351
352 352 class revlogio(object):
353 353 def __init__(self):
354 354 self.size = indexformatng.size
355 355
356 356 def parseindex(self, data, inline):
357 357 # call the C implementation to parse the index data
358 358 index, cache = parsers.parse_index2(data, inline)
359 359 return index, cache
360 360
361 361 def packentry(self, entry, node, version, rev):
362 362 p = indexformatng_pack(*entry)
363 363 if rev == 0:
364 364 p = versionformat_pack(version) + p[4:]
365 365 return p
366 366
367 367
368 368 indexformatv2 = struct.Struct(pureparsers.Index2Mixin.index_format)
369 369 indexformatv2_pack = indexformatv2.pack
370 370
371 371
372 372 class revlogv2io(object):
373 373 def __init__(self):
374 374 self.size = indexformatv2.size
375 375
376 376 def parseindex(self, data, inline):
377 377 index, cache = parsers.parse_index2(data, inline, revlogv2=True)
378 378 return index, cache
379 379
380 380 def packentry(self, entry, node, version, rev):
381 381 p = indexformatv2_pack(*entry)
382 382 if rev == 0:
383 383 p = versionformat_pack(version) + p[4:]
384 384 return p
385 385
386 386
387 387 NodemapRevlogIO = None
388 388
389 389 if util.safehasattr(parsers, 'parse_index_devel_nodemap'):
390 390
391 391 class NodemapRevlogIO(revlogio):
392 392 """A debug oriented IO class that return a PersistentNodeMapIndexObject
393 393
394 394 The PersistentNodeMapIndexObject object is meant to test the persistent nodemap feature.
395 395 """
396 396
397 397 def parseindex(self, data, inline):
398 398 index, cache = parsers.parse_index_devel_nodemap(data, inline)
399 399 return index, cache
400 400
401 401
402 402 class rustrevlogio(revlogio):
403 403 def parseindex(self, data, inline):
404 404 index, cache = super(rustrevlogio, self).parseindex(data, inline)
405 405 return rustrevlog.MixedIndex(index), cache
406 406
407 407
408 408 class revlog(object):
409 409 """
410 410 the underlying revision storage object
411 411
412 412 A revlog consists of two parts, an index and the revision data.
413 413
414 414 The index is a file with a fixed record size containing
415 415 information on each revision, including its nodeid (hash), the
416 416 nodeids of its parents, the position and offset of its data within
417 417 the data file, and the revision it's based on. Finally, each entry
418 418 contains a linkrev entry that can serve as a pointer to external
419 419 data.
420 420
421 421 The revision data itself is a linear collection of data chunks.
422 422 Each chunk represents a revision and is usually represented as a
423 423 delta against the previous chunk. To bound lookup time, runs of
424 424 deltas are limited to about 2 times the length of the original
425 425 version data. This makes retrieval of a version proportional to
426 426 its size, or O(1) relative to the number of revisions.
427 427
428 428 Both pieces of the revlog are written to in an append-only
429 429 fashion, which means we never need to rewrite a file to insert or
430 430 remove data, and can use some simple techniques to avoid the need
431 431 for locking while reading.
432 432
433 433 If checkambig, indexfile is opened with checkambig=True at
434 434 writing, to avoid file stat ambiguity.
435 435
436 436 If mmaplargeindex is True, and an mmapindexthreshold is set, the
437 437 index will be mmapped rather than read if it is larger than the
438 438 configured threshold.
439 439
440 440 If censorable is True, the revlog can have censored revisions.
441 441
442 442 If `upperboundcomp` is not None, this is the expected maximal gain from
443 443 compression for the data content.
444 444
445 445 `concurrencychecker` is an optional function that receives 3 arguments: a
446 446 file handle, a filename, and an expected position. It should check whether
447 447 the current position in the file handle is valid, and log/warn/fail (by
448 448 raising).
449 449 """
450 450
451 451 _flagserrorclass = error.RevlogError
452 452
453 453 def __init__(
454 454 self,
455 455 opener,
456 456 indexfile,
457 457 datafile=None,
458 458 checkambig=False,
459 459 mmaplargeindex=False,
460 460 censorable=False,
461 461 upperboundcomp=None,
462 462 persistentnodemap=False,
463 463 concurrencychecker=None,
464 464 ):
465 465 """
466 466 create a revlog object
467 467
468 468 opener is a function that abstracts the file opening operation
469 469 and can be used to implement COW semantics or the like.
470 470
471 471 """
472 472 self.upperboundcomp = upperboundcomp
473 473 self.indexfile = indexfile
474 474 self.datafile = datafile or (indexfile[:-2] + b".d")
475 475 self.nodemap_file = None
476 476 if persistentnodemap:
477 477 self.nodemap_file = nodemaputil.get_nodemap_file(
478 478 opener, self.indexfile
479 479 )
480 480
481 481 self.opener = opener
482 482 # When True, indexfile is opened with checkambig=True at writing, to
483 483 # avoid file stat ambiguity.
484 484 self._checkambig = checkambig
485 485 self._mmaplargeindex = mmaplargeindex
486 486 self._censorable = censorable
487 487 # 3-tuple of (node, rev, text) for a raw revision.
488 488 self._revisioncache = None
489 489 # Maps rev to chain base rev.
490 490 self._chainbasecache = util.lrucachedict(100)
491 491 # 2-tuple of (offset, data) of raw data from the revlog at an offset.
492 492 self._chunkcache = (0, b'')
493 493 # How much data to read and cache into the raw revlog data cache.
494 494 self._chunkcachesize = 65536
495 495 self._maxchainlen = None
496 496 self._deltabothparents = True
497 497 self.index = None
498 498 self._nodemap_docket = None
499 499 # Mapping of partial identifiers to full nodes.
500 500 self._pcache = {}
501 501 # Mapping of revision integer to full node.
502 502 self._compengine = b'zlib'
503 503 self._compengineopts = {}
504 504 self._maxdeltachainspan = -1
505 505 self._withsparseread = False
506 506 self._sparserevlog = False
507 507 self._srdensitythreshold = 0.50
508 508 self._srmingapsize = 262144
509 509
510 510 # Make copy of flag processors so each revlog instance can support
511 511 # custom flags.
512 512 self._flagprocessors = dict(flagutil.flagprocessors)
513 513
514 514 # 2-tuple of file handles being used for active writing.
515 515 self._writinghandles = None
516 516
517 517 self._loadindex()
518 518
519 519 self._concurrencychecker = concurrencychecker
520 520
521 521 def _loadindex(self):
522 522 mmapindexthreshold = None
523 523 opts = self.opener.options
524 524
525 525 if b'revlogv2' in opts:
526 526 newversionflags = REVLOGV2 | FLAG_INLINE_DATA
527 527 elif b'revlogv1' in opts:
528 528 newversionflags = REVLOGV1 | FLAG_INLINE_DATA
529 529 if b'generaldelta' in opts:
530 530 newversionflags |= FLAG_GENERALDELTA
531 531 elif b'revlogv0' in self.opener.options:
532 532 newversionflags = REVLOGV0
533 533 else:
534 534 newversionflags = REVLOG_DEFAULT_VERSION
535 535
536 536 if b'chunkcachesize' in opts:
537 537 self._chunkcachesize = opts[b'chunkcachesize']
538 538 if b'maxchainlen' in opts:
539 539 self._maxchainlen = opts[b'maxchainlen']
540 540 if b'deltabothparents' in opts:
541 541 self._deltabothparents = opts[b'deltabothparents']
542 542 self._lazydelta = bool(opts.get(b'lazydelta', True))
543 543 self._lazydeltabase = False
544 544 if self._lazydelta:
545 545 self._lazydeltabase = bool(opts.get(b'lazydeltabase', False))
546 546 if b'compengine' in opts:
547 547 self._compengine = opts[b'compengine']
548 548 if b'zlib.level' in opts:
549 549 self._compengineopts[b'zlib.level'] = opts[b'zlib.level']
550 550 if b'zstd.level' in opts:
551 551 self._compengineopts[b'zstd.level'] = opts[b'zstd.level']
552 552 if b'maxdeltachainspan' in opts:
553 553 self._maxdeltachainspan = opts[b'maxdeltachainspan']
554 554 if self._mmaplargeindex and b'mmapindexthreshold' in opts:
555 555 mmapindexthreshold = opts[b'mmapindexthreshold']
556 556 self.hassidedata = bool(opts.get(b'side-data', False))
557 557 self._sparserevlog = bool(opts.get(b'sparse-revlog', False))
558 558 withsparseread = bool(opts.get(b'with-sparse-read', False))
559 559 # sparse-revlog forces sparse-read
560 560 self._withsparseread = self._sparserevlog or withsparseread
561 561 if b'sparse-read-density-threshold' in opts:
562 562 self._srdensitythreshold = opts[b'sparse-read-density-threshold']
563 563 if b'sparse-read-min-gap-size' in opts:
564 564 self._srmingapsize = opts[b'sparse-read-min-gap-size']
565 565 if opts.get(b'enableellipsis'):
566 566 self._flagprocessors[REVIDX_ELLIPSIS] = ellipsisprocessor
567 567
568 568 # revlog v0 doesn't have flag processors
569 569 for flag, processor in pycompat.iteritems(
570 570 opts.get(b'flagprocessors', {})
571 571 ):
572 572 flagutil.insertflagprocessor(flag, processor, self._flagprocessors)
573 573
574 574 if self._chunkcachesize <= 0:
575 575 raise error.RevlogError(
576 576 _(b'revlog chunk cache size %r is not greater than 0')
577 577 % self._chunkcachesize
578 578 )
579 579 elif self._chunkcachesize & (self._chunkcachesize - 1):
580 580 raise error.RevlogError(
581 581 _(b'revlog chunk cache size %r is not a power of 2')
582 582 % self._chunkcachesize
583 583 )
584 584
585 585 indexdata = b''
586 586 self._initempty = True
587 587 try:
588 588 with self._indexfp() as f:
589 589 if (
590 590 mmapindexthreshold is not None
591 591 and self.opener.fstat(f).st_size >= mmapindexthreshold
592 592 ):
593 593 # TODO: should .close() to release resources without
594 594 # relying on Python GC
595 595 indexdata = util.buffer(util.mmapread(f))
596 596 else:
597 597 indexdata = f.read()
598 598 if len(indexdata) > 0:
599 599 versionflags = versionformat_unpack(indexdata[:4])[0]
600 600 self._initempty = False
601 601 else:
602 602 versionflags = newversionflags
603 603 except IOError as inst:
604 604 if inst.errno != errno.ENOENT:
605 605 raise
606 606
607 607 versionflags = newversionflags
608 608
609 609 self.version = versionflags
610 610
611 611 flags = versionflags & ~0xFFFF
612 612 fmt = versionflags & 0xFFFF
613 613
614 614 if fmt == REVLOGV0:
615 615 if flags:
616 616 raise error.RevlogError(
617 617 _(b'unknown flags (%#04x) in version %d revlog %s')
618 618 % (flags >> 16, fmt, self.indexfile)
619 619 )
620 620
621 621 self._inline = False
622 622 self._generaldelta = False
623 623
624 624 elif fmt == REVLOGV1:
625 625 if flags & ~REVLOGV1_FLAGS:
626 626 raise error.RevlogError(
627 627 _(b'unknown flags (%#04x) in version %d revlog %s')
628 628 % (flags >> 16, fmt, self.indexfile)
629 629 )
630 630
631 631 self._inline = versionflags & FLAG_INLINE_DATA
632 632 self._generaldelta = versionflags & FLAG_GENERALDELTA
633 633
634 634 elif fmt == REVLOGV2:
635 635 if flags & ~REVLOGV2_FLAGS:
636 636 raise error.RevlogError(
637 637 _(b'unknown flags (%#04x) in version %d revlog %s')
638 638 % (flags >> 16, fmt, self.indexfile)
639 639 )
640 640
641 641 self._inline = versionflags & FLAG_INLINE_DATA
642 642 # generaldelta implied by version 2 revlogs.
643 643 self._generaldelta = True
644 644
645 645 else:
646 646 raise error.RevlogError(
647 647 _(b'unknown version (%d) in revlog %s') % (fmt, self.indexfile)
648 648 )
649 649 # sparse-revlog can't be on without general-delta (issue6056)
650 650 if not self._generaldelta:
651 651 self._sparserevlog = False
652 652
653 653 self._storedeltachains = True
654 654
655 655 devel_nodemap = (
656 656 self.nodemap_file
657 657 and opts.get(b'devel-force-nodemap', False)
658 658 and NodemapRevlogIO is not None
659 659 )
660 660
661 661 use_rust_index = False
662 662 if rustrevlog is not None:
663 663 if self.nodemap_file is not None:
664 664 use_rust_index = True
665 665 else:
666 666 use_rust_index = self.opener.options.get(b'rust.index')
667 667
668 668 self._io = revlogio()
669 669 if self.version == REVLOGV0:
670 670 self._io = revlogoldio()
671 671 elif fmt == REVLOGV2:
672 672 self._io = revlogv2io()
673 673 elif devel_nodemap:
674 674 self._io = NodemapRevlogIO()
675 675 elif use_rust_index:
676 676 self._io = rustrevlogio()
677 677 try:
678 678 d = self._io.parseindex(indexdata, self._inline)
679 679 index, _chunkcache = d
680 680 use_nodemap = (
681 681 not self._inline
682 682 and self.nodemap_file is not None
683 683 and util.safehasattr(index, 'update_nodemap_data')
684 684 )
685 685 if use_nodemap:
686 686 nodemap_data = nodemaputil.persisted_data(self)
687 687 if nodemap_data is not None:
688 688 docket = nodemap_data[0]
689 689 if (
690 690 len(d[0]) > docket.tip_rev
691 691 and d[0][docket.tip_rev][7] == docket.tip_node
692 692 ):
693 693 # no changelog tampering
694 694 self._nodemap_docket = docket
695 695 index.update_nodemap_data(*nodemap_data)
696 696 except (ValueError, IndexError):
697 697 raise error.RevlogError(
698 698 _(b"index %s is corrupted") % self.indexfile
699 699 )
700 700 self.index, self._chunkcache = d
701 701 if not self._chunkcache:
702 702 self._chunkclear()
703 703 # revnum -> (chain-length, sum-delta-length)
704 704 self._chaininfocache = util.lrucachedict(500)
705 705 # revlog header -> revlog compressor
706 706 self._decompressors = {}
707 707
708 708 @util.propertycache
709 709 def _compressor(self):
710 710 engine = util.compengines[self._compengine]
711 711 return engine.revlogcompressor(self._compengineopts)
712 712
713 713 def _indexfp(self, mode=b'r'):
714 714 """file object for the revlog's index file"""
715 715 args = {'mode': mode}
716 716 if mode != b'r':
717 717 args['checkambig'] = self._checkambig
718 718 if mode == b'w':
719 719 args['atomictemp'] = True
720 720 return self.opener(self.indexfile, **args)
721 721
722 722 def _datafp(self, mode=b'r'):
723 723 """file object for the revlog's data file"""
724 724 return self.opener(self.datafile, mode=mode)
725 725
726 726 @contextlib.contextmanager
727 727 def _datareadfp(self, existingfp=None):
728 728 """file object suitable to read data"""
729 729 # Use explicit file handle, if given.
730 730 if existingfp is not None:
731 731 yield existingfp
732 732
733 733 # Use a file handle being actively used for writes, if available.
734 734 # There is some danger to doing this because reads will seek the
735 735 # file. However, _writeentry() performs a SEEK_END before all writes,
736 736 # so we should be safe.
737 737 elif self._writinghandles:
738 738 if self._inline:
739 739 yield self._writinghandles[0]
740 740 else:
741 741 yield self._writinghandles[1]
742 742
743 743 # Otherwise open a new file handle.
744 744 else:
745 745 if self._inline:
746 746 func = self._indexfp
747 747 else:
748 748 func = self._datafp
749 749 with func() as fp:
750 750 yield fp
751 751
752 752 def tiprev(self):
753 753 return len(self.index) - 1
754 754
755 755 def tip(self):
756 756 return self.node(self.tiprev())
757 757
758 758 def __contains__(self, rev):
759 759 return 0 <= rev < len(self)
760 760
761 761 def __len__(self):
762 762 return len(self.index)
763 763
764 764 def __iter__(self):
765 765 return iter(pycompat.xrange(len(self)))
766 766
767 767 def revs(self, start=0, stop=None):
768 768 """iterate over all rev in this revlog (from start to stop)"""
769 769 return storageutil.iterrevs(len(self), start=start, stop=stop)
770 770
771 771 @property
772 772 def nodemap(self):
773 773 msg = (
774 774 b"revlog.nodemap is deprecated, "
775 775 b"use revlog.index.[has_node|rev|get_rev]"
776 776 )
777 777 util.nouideprecwarn(msg, b'5.3', stacklevel=2)
778 778 return self.index.nodemap
779 779
780 780 @property
781 781 def _nodecache(self):
782 782 msg = b"revlog._nodecache is deprecated, use revlog.index.nodemap"
783 783 util.nouideprecwarn(msg, b'5.3', stacklevel=2)
784 784 return self.index.nodemap
785 785
786 786 def hasnode(self, node):
787 787 try:
788 788 self.rev(node)
789 789 return True
790 790 except KeyError:
791 791 return False
792 792
793 793 def candelta(self, baserev, rev):
794 794 """whether two revisions (baserev, rev) can be delta-ed or not"""
795 795 # Disable delta if either rev requires a content-changing flag
796 796 # processor (ex. LFS). This is because such flag processor can alter
797 797 # the rawtext content that the delta will be based on, and two clients
798 798 # could have a same revlog node with different flags (i.e. different
799 799 # rawtext contents) and the delta could be incompatible.
800 800 if (self.flags(baserev) & REVIDX_RAWTEXT_CHANGING_FLAGS) or (
801 801 self.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS
802 802 ):
803 803 return False
804 804 return True
805 805
806 806 def update_caches(self, transaction):
807 807 if self.nodemap_file is not None:
808 808 if transaction is None:
809 809 nodemaputil.update_persistent_nodemap(self)
810 810 else:
811 811 nodemaputil.setup_persistent_nodemap(transaction, self)
812 812
813 813 def clearcaches(self):
814 814 self._revisioncache = None
815 815 self._chainbasecache.clear()
816 816 self._chunkcache = (0, b'')
817 817 self._pcache = {}
818 818 self._nodemap_docket = None
819 819 self.index.clearcaches()
820 820 # The python code is the one responsible for validating the docket, we
821 821 # end up having to refresh it here.
822 822 use_nodemap = (
823 823 not self._inline
824 824 and self.nodemap_file is not None
825 825 and util.safehasattr(self.index, 'update_nodemap_data')
826 826 )
827 827 if use_nodemap:
828 828 nodemap_data = nodemaputil.persisted_data(self)
829 829 if nodemap_data is not None:
830 830 self._nodemap_docket = nodemap_data[0]
831 831 self.index.update_nodemap_data(*nodemap_data)
832 832
833 833 def rev(self, node):
834 834 try:
835 835 return self.index.rev(node)
836 836 except TypeError:
837 837 raise
838 838 except error.RevlogError:
839 839 # parsers.c radix tree lookup failed
840 840 if node == wdirid or node in wdirfilenodeids:
841 841 raise error.WdirUnsupported
842 842 raise error.LookupError(node, self.indexfile, _(b'no node'))
843 843
844 844 # Accessors for index entries.
845 845
846 846 # First tuple entry is 8 bytes. First 6 bytes are offset. Last 2 bytes
847 847 # are flags.
848 848 def start(self, rev):
849 849 return int(self.index[rev][0] >> 16)
850 850
851 851 def flags(self, rev):
852 852 return self.index[rev][0] & 0xFFFF
853 853
854 854 def length(self, rev):
855 855 return self.index[rev][1]
856 856
857 857 def sidedata_length(self, rev):
858 858 if self.version & 0xFFFF != REVLOGV2:
859 859 return 0
860 860 return self.index[rev][9]
861 861
862 862 def rawsize(self, rev):
863 863 """return the length of the uncompressed text for a given revision"""
864 864 l = self.index[rev][2]
865 865 if l >= 0:
866 866 return l
867 867
868 868 t = self.rawdata(rev)
869 869 return len(t)
870 870
871 871 def size(self, rev):
872 872 """length of non-raw text (processed by a "read" flag processor)"""
873 873 # fast path: if no "read" flag processor could change the content,
874 874 # size is rawsize. note: ELLIPSIS is known to not change the content.
875 875 flags = self.flags(rev)
876 876 if flags & (flagutil.REVIDX_KNOWN_FLAGS ^ REVIDX_ELLIPSIS) == 0:
877 877 return self.rawsize(rev)
878 878
879 879 return len(self.revision(rev, raw=False))
880 880
881 881 def chainbase(self, rev):
882 882 base = self._chainbasecache.get(rev)
883 883 if base is not None:
884 884 return base
885 885
886 886 index = self.index
887 887 iterrev = rev
888 888 base = index[iterrev][3]
889 889 while base != iterrev:
890 890 iterrev = base
891 891 base = index[iterrev][3]
892 892
893 893 self._chainbasecache[rev] = base
894 894 return base
895 895
896 896 def linkrev(self, rev):
897 897 return self.index[rev][4]
898 898
899 899 def parentrevs(self, rev):
900 900 try:
901 901 entry = self.index[rev]
902 902 except IndexError:
903 903 if rev == wdirrev:
904 904 raise error.WdirUnsupported
905 905 raise
906 906
907 907 return entry[5], entry[6]
908 908
909 909 # fast parentrevs(rev) where rev isn't filtered
910 910 _uncheckedparentrevs = parentrevs
911 911
912 912 def node(self, rev):
913 913 try:
914 914 return self.index[rev][7]
915 915 except IndexError:
916 916 if rev == wdirrev:
917 917 raise error.WdirUnsupported
918 918 raise
919 919
920 920 # Derived from index values.
921 921
922 922 def end(self, rev):
923 923 return self.start(rev) + self.length(rev)
924 924
925 925 def parents(self, node):
926 926 i = self.index
927 927 d = i[self.rev(node)]
928 928 return i[d[5]][7], i[d[6]][7] # map revisions to nodes inline
929 929
930 930 def chainlen(self, rev):
931 931 return self._chaininfo(rev)[0]
932 932
933 933 def _chaininfo(self, rev):
934 934 chaininfocache = self._chaininfocache
935 935 if rev in chaininfocache:
936 936 return chaininfocache[rev]
937 937 index = self.index
938 938 generaldelta = self._generaldelta
939 939 iterrev = rev
940 940 e = index[iterrev]
941 941 clen = 0
942 942 compresseddeltalen = 0
943 943 while iterrev != e[3]:
944 944 clen += 1
945 945 compresseddeltalen += e[1]
946 946 if generaldelta:
947 947 iterrev = e[3]
948 948 else:
949 949 iterrev -= 1
950 950 if iterrev in chaininfocache:
951 951 t = chaininfocache[iterrev]
952 952 clen += t[0]
953 953 compresseddeltalen += t[1]
954 954 break
955 955 e = index[iterrev]
956 956 else:
957 957 # Add text length of base since decompressing that also takes
958 958 # work. For cache hits the length is already included.
959 959 compresseddeltalen += e[1]
960 960 r = (clen, compresseddeltalen)
961 961 chaininfocache[rev] = r
962 962 return r
963 963
964 964 def _deltachain(self, rev, stoprev=None):
965 965 """Obtain the delta chain for a revision.
966 966
967 967 ``stoprev`` specifies a revision to stop at. If not specified, we
968 968 stop at the base of the chain.
969 969
970 970 Returns a 2-tuple of (chain, stopped) where ``chain`` is a list of
971 971 revs in ascending order and ``stopped`` is a bool indicating whether
972 972 ``stoprev`` was hit.
973 973 """
974 974 # Try C implementation.
975 975 try:
976 976 return self.index.deltachain(rev, stoprev, self._generaldelta)
977 977 except AttributeError:
978 978 pass
979 979
980 980 chain = []
981 981
982 982 # Alias to prevent attribute lookup in tight loop.
983 983 index = self.index
984 984 generaldelta = self._generaldelta
985 985
986 986 iterrev = rev
987 987 e = index[iterrev]
988 988 while iterrev != e[3] and iterrev != stoprev:
989 989 chain.append(iterrev)
990 990 if generaldelta:
991 991 iterrev = e[3]
992 992 else:
993 993 iterrev -= 1
994 994 e = index[iterrev]
995 995
996 996 if iterrev == stoprev:
997 997 stopped = True
998 998 else:
999 999 chain.append(iterrev)
1000 1000 stopped = False
1001 1001
1002 1002 chain.reverse()
1003 1003 return chain, stopped
1004 1004
1005 1005 def ancestors(self, revs, stoprev=0, inclusive=False):
1006 1006 """Generate the ancestors of 'revs' in reverse revision order.
1007 1007 Does not generate revs lower than stoprev.
1008 1008
1009 1009 See the documentation for ancestor.lazyancestors for more details."""
1010 1010
1011 1011 # first, make sure start revisions aren't filtered
1012 1012 revs = list(revs)
1013 1013 checkrev = self.node
1014 1014 for r in revs:
1015 1015 checkrev(r)
1016 1016 # and we're sure ancestors aren't filtered as well
1017 1017
1018 1018 if rustancestor is not None:
1019 1019 lazyancestors = rustancestor.LazyAncestors
1020 1020 arg = self.index
1021 1021 else:
1022 1022 lazyancestors = ancestor.lazyancestors
1023 1023 arg = self._uncheckedparentrevs
1024 1024 return lazyancestors(arg, revs, stoprev=stoprev, inclusive=inclusive)
1025 1025
1026 1026 def descendants(self, revs):
1027 1027 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
1028 1028
1029 1029 def findcommonmissing(self, common=None, heads=None):
1030 1030 """Return a tuple of the ancestors of common and the ancestors of heads
1031 1031 that are not ancestors of common. In revset terminology, we return the
1032 1032 tuple:
1033 1033
1034 1034 ::common, (::heads) - (::common)
1035 1035
1036 1036 The list is sorted by revision number, meaning it is
1037 1037 topologically sorted.
1038 1038
1039 1039 'heads' and 'common' are both lists of node IDs. If heads is
1040 1040 not supplied, uses all of the revlog's heads. If common is not
1041 1041 supplied, uses nullid."""
1042 1042 if common is None:
1043 1043 common = [nullid]
1044 1044 if heads is None:
1045 1045 heads = self.heads()
1046 1046
1047 1047 common = [self.rev(n) for n in common]
1048 1048 heads = [self.rev(n) for n in heads]
1049 1049
1050 1050 # we want the ancestors, but inclusive
1051 1051 class lazyset(object):
1052 1052 def __init__(self, lazyvalues):
1053 1053 self.addedvalues = set()
1054 1054 self.lazyvalues = lazyvalues
1055 1055
1056 1056 def __contains__(self, value):
1057 1057 return value in self.addedvalues or value in self.lazyvalues
1058 1058
1059 1059 def __iter__(self):
1060 1060 added = self.addedvalues
1061 1061 for r in added:
1062 1062 yield r
1063 1063 for r in self.lazyvalues:
1064 1064 if not r in added:
1065 1065 yield r
1066 1066
1067 1067 def add(self, value):
1068 1068 self.addedvalues.add(value)
1069 1069
1070 1070 def update(self, values):
1071 1071 self.addedvalues.update(values)
1072 1072
1073 1073 has = lazyset(self.ancestors(common))
1074 1074 has.add(nullrev)
1075 1075 has.update(common)
1076 1076
1077 1077 # take all ancestors from heads that aren't in has
1078 1078 missing = set()
1079 1079 visit = collections.deque(r for r in heads if r not in has)
1080 1080 while visit:
1081 1081 r = visit.popleft()
1082 1082 if r in missing:
1083 1083 continue
1084 1084 else:
1085 1085 missing.add(r)
1086 1086 for p in self.parentrevs(r):
1087 1087 if p not in has:
1088 1088 visit.append(p)
1089 1089 missing = list(missing)
1090 1090 missing.sort()
1091 1091 return has, [self.node(miss) for miss in missing]
1092 1092
1093 1093 def incrementalmissingrevs(self, common=None):
1094 1094 """Return an object that can be used to incrementally compute the
1095 1095 revision numbers of the ancestors of arbitrary sets that are not
1096 1096 ancestors of common. This is an ancestor.incrementalmissingancestors
1097 1097 object.
1098 1098
1099 1099 'common' is a list of revision numbers. If common is not supplied, uses
1100 1100 nullrev.
1101 1101 """
1102 1102 if common is None:
1103 1103 common = [nullrev]
1104 1104
1105 1105 if rustancestor is not None:
1106 1106 return rustancestor.MissingAncestors(self.index, common)
1107 1107 return ancestor.incrementalmissingancestors(self.parentrevs, common)
1108 1108
1109 1109 def findmissingrevs(self, common=None, heads=None):
1110 1110 """Return the revision numbers of the ancestors of heads that
1111 1111 are not ancestors of common.
1112 1112
1113 1113 More specifically, return a list of revision numbers corresponding to
1114 1114 nodes N such that every N satisfies the following constraints:
1115 1115
1116 1116 1. N is an ancestor of some node in 'heads'
1117 1117 2. N is not an ancestor of any node in 'common'
1118 1118
1119 1119 The list is sorted by revision number, meaning it is
1120 1120 topologically sorted.
1121 1121
1122 1122 'heads' and 'common' are both lists of revision numbers. If heads is
1123 1123 not supplied, uses all of the revlog's heads. If common is not
1124 1124 supplied, uses nullid."""
1125 1125 if common is None:
1126 1126 common = [nullrev]
1127 1127 if heads is None:
1128 1128 heads = self.headrevs()
1129 1129
1130 1130 inc = self.incrementalmissingrevs(common=common)
1131 1131 return inc.missingancestors(heads)
1132 1132
1133 1133 def findmissing(self, common=None, heads=None):
1134 1134 """Return the ancestors of heads that are not ancestors of common.
1135 1135
1136 1136 More specifically, return a list of nodes N such that every N
1137 1137 satisfies the following constraints:
1138 1138
1139 1139 1. N is an ancestor of some node in 'heads'
1140 1140 2. N is not an ancestor of any node in 'common'
1141 1141
1142 1142 The list is sorted by revision number, meaning it is
1143 1143 topologically sorted.
1144 1144
1145 1145 'heads' and 'common' are both lists of node IDs. If heads is
1146 1146 not supplied, uses all of the revlog's heads. If common is not
1147 1147 supplied, uses nullid."""
1148 1148 if common is None:
1149 1149 common = [nullid]
1150 1150 if heads is None:
1151 1151 heads = self.heads()
1152 1152
1153 1153 common = [self.rev(n) for n in common]
1154 1154 heads = [self.rev(n) for n in heads]
1155 1155
1156 1156 inc = self.incrementalmissingrevs(common=common)
1157 1157 return [self.node(r) for r in inc.missingancestors(heads)]
1158 1158
1159 1159 def nodesbetween(self, roots=None, heads=None):
1160 1160 """Return a topological path from 'roots' to 'heads'.
1161 1161
1162 1162 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
1163 1163 topologically sorted list of all nodes N that satisfy both of
1164 1164 these constraints:
1165 1165
1166 1166 1. N is a descendant of some node in 'roots'
1167 1167 2. N is an ancestor of some node in 'heads'
1168 1168
1169 1169 Every node is considered to be both a descendant and an ancestor
1170 1170 of itself, so every reachable node in 'roots' and 'heads' will be
1171 1171 included in 'nodes'.
1172 1172
1173 1173 'outroots' is the list of reachable nodes in 'roots', i.e., the
1174 1174 subset of 'roots' that is returned in 'nodes'. Likewise,
1175 1175 'outheads' is the subset of 'heads' that is also in 'nodes'.
1176 1176
1177 1177 'roots' and 'heads' are both lists of node IDs. If 'roots' is
1178 1178 unspecified, uses nullid as the only root. If 'heads' is
1179 1179 unspecified, uses list of all of the revlog's heads."""
1180 1180 nonodes = ([], [], [])
1181 1181 if roots is not None:
1182 1182 roots = list(roots)
1183 1183 if not roots:
1184 1184 return nonodes
1185 1185 lowestrev = min([self.rev(n) for n in roots])
1186 1186 else:
1187 1187 roots = [nullid] # Everybody's a descendant of nullid
1188 1188 lowestrev = nullrev
1189 1189 if (lowestrev == nullrev) and (heads is None):
1190 1190 # We want _all_ the nodes!
1191 1191 return ([self.node(r) for r in self], [nullid], list(self.heads()))
1192 1192 if heads is None:
1193 1193 # All nodes are ancestors, so the latest ancestor is the last
1194 1194 # node.
1195 1195 highestrev = len(self) - 1
1196 1196 # Set ancestors to None to signal that every node is an ancestor.
1197 1197 ancestors = None
1198 1198 # Set heads to an empty dictionary for later discovery of heads
1199 1199 heads = {}
1200 1200 else:
1201 1201 heads = list(heads)
1202 1202 if not heads:
1203 1203 return nonodes
1204 1204 ancestors = set()
1205 1205 # Turn heads into a dictionary so we can remove 'fake' heads.
1206 1206 # Also, later we will be using it to filter out the heads we can't
1207 1207 # find from roots.
1208 1208 heads = dict.fromkeys(heads, False)
1209 1209 # Start at the top and keep marking parents until we're done.
1210 1210 nodestotag = set(heads)
1211 1211 # Remember where the top was so we can use it as a limit later.
1212 1212 highestrev = max([self.rev(n) for n in nodestotag])
1213 1213 while nodestotag:
1214 1214 # grab a node to tag
1215 1215 n = nodestotag.pop()
1216 1216 # Never tag nullid
1217 1217 if n == nullid:
1218 1218 continue
1219 1219 # A node's revision number represents its place in a
1220 1220 # topologically sorted list of nodes.
1221 1221 r = self.rev(n)
1222 1222 if r >= lowestrev:
1223 1223 if n not in ancestors:
1224 1224 # If we are possibly a descendant of one of the roots
1225 1225 # and we haven't already been marked as an ancestor
1226 1226 ancestors.add(n) # Mark as ancestor
1227 1227 # Add non-nullid parents to list of nodes to tag.
1228 1228 nodestotag.update(
1229 1229 [p for p in self.parents(n) if p != nullid]
1230 1230 )
1231 1231 elif n in heads: # We've seen it before, is it a fake head?
1232 1232 # So it is, real heads should not be the ancestors of
1233 1233 # any other heads.
1234 1234 heads.pop(n)
1235 1235 if not ancestors:
1236 1236 return nonodes
1237 1237 # Now that we have our set of ancestors, we want to remove any
1238 1238 # roots that are not ancestors.
1239 1239
1240 1240 # If one of the roots was nullid, everything is included anyway.
1241 1241 if lowestrev > nullrev:
1242 1242 # But, since we weren't, let's recompute the lowest rev to not
1243 1243 # include roots that aren't ancestors.
1244 1244
1245 1245 # Filter out roots that aren't ancestors of heads
1246 1246 roots = [root for root in roots if root in ancestors]
1247 1247 # Recompute the lowest revision
1248 1248 if roots:
1249 1249 lowestrev = min([self.rev(root) for root in roots])
1250 1250 else:
1251 1251 # No more roots? Return empty list
1252 1252 return nonodes
1253 1253 else:
1254 1254 # We are descending from nullid, and don't need to care about
1255 1255 # any other roots.
1256 1256 lowestrev = nullrev
1257 1257 roots = [nullid]
1258 1258 # Transform our roots list into a set.
1259 1259 descendants = set(roots)
1260 1260 # Also, keep the original roots so we can filter out roots that aren't
1261 1261 # 'real' roots (i.e. are descended from other roots).
1262 1262 roots = descendants.copy()
1263 1263 # Our topologically sorted list of output nodes.
1264 1264 orderedout = []
1265 1265 # Don't start at nullid since we don't want nullid in our output list,
1266 1266 # and if nullid shows up in descendants, empty parents will look like
1267 1267 # they're descendants.
1268 1268 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
1269 1269 n = self.node(r)
1270 1270 isdescendant = False
1271 1271 if lowestrev == nullrev: # Everybody is a descendant of nullid
1272 1272 isdescendant = True
1273 1273 elif n in descendants:
1274 1274 # n is already a descendant
1275 1275 isdescendant = True
1276 1276 # This check only needs to be done here because all the roots
1277 1277 # will start being marked is descendants before the loop.
1278 1278 if n in roots:
1279 1279 # If n was a root, check if it's a 'real' root.
1280 1280 p = tuple(self.parents(n))
1281 1281 # If any of its parents are descendants, it's not a root.
1282 1282 if (p[0] in descendants) or (p[1] in descendants):
1283 1283 roots.remove(n)
1284 1284 else:
1285 1285 p = tuple(self.parents(n))
1286 1286 # A node is a descendant if either of its parents are
1287 1287 # descendants. (We seeded the dependents list with the roots
1288 1288 # up there, remember?)
1289 1289 if (p[0] in descendants) or (p[1] in descendants):
1290 1290 descendants.add(n)
1291 1291 isdescendant = True
1292 1292 if isdescendant and ((ancestors is None) or (n in ancestors)):
1293 1293 # Only include nodes that are both descendants and ancestors.
1294 1294 orderedout.append(n)
1295 1295 if (ancestors is not None) and (n in heads):
1296 1296 # We're trying to figure out which heads are reachable
1297 1297 # from roots.
1298 1298 # Mark this head as having been reached
1299 1299 heads[n] = True
1300 1300 elif ancestors is None:
1301 1301 # Otherwise, we're trying to discover the heads.
1302 1302 # Assume this is a head because if it isn't, the next step
1303 1303 # will eventually remove it.
1304 1304 heads[n] = True
1305 1305 # But, obviously its parents aren't.
1306 1306 for p in self.parents(n):
1307 1307 heads.pop(p, None)
1308 1308 heads = [head for head, flag in pycompat.iteritems(heads) if flag]
1309 1309 roots = list(roots)
1310 1310 assert orderedout
1311 1311 assert roots
1312 1312 assert heads
1313 1313 return (orderedout, roots, heads)
1314 1314
1315 1315 def headrevs(self, revs=None):
1316 1316 if revs is None:
1317 1317 try:
1318 1318 return self.index.headrevs()
1319 1319 except AttributeError:
1320 1320 return self._headrevs()
1321 1321 if rustdagop is not None:
1322 1322 return rustdagop.headrevs(self.index, revs)
1323 1323 return dagop.headrevs(revs, self._uncheckedparentrevs)
1324 1324
1325 1325 def computephases(self, roots):
1326 1326 return self.index.computephasesmapsets(roots)
1327 1327
1328 1328 def _headrevs(self):
1329 1329 count = len(self)
1330 1330 if not count:
1331 1331 return [nullrev]
1332 1332 # we won't iter over filtered rev so nobody is a head at start
1333 1333 ishead = [0] * (count + 1)
1334 1334 index = self.index
1335 1335 for r in self:
1336 1336 ishead[r] = 1 # I may be an head
1337 1337 e = index[r]
1338 1338 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
1339 1339 return [r for r, val in enumerate(ishead) if val]
1340 1340
1341 1341 def heads(self, start=None, stop=None):
1342 1342 """return the list of all nodes that have no children
1343 1343
1344 1344 if start is specified, only heads that are descendants of
1345 1345 start will be returned
1346 1346 if stop is specified, it will consider all the revs from stop
1347 1347 as if they had no children
1348 1348 """
1349 1349 if start is None and stop is None:
1350 1350 if not len(self):
1351 1351 return [nullid]
1352 1352 return [self.node(r) for r in self.headrevs()]
1353 1353
1354 1354 if start is None:
1355 1355 start = nullrev
1356 1356 else:
1357 1357 start = self.rev(start)
1358 1358
1359 1359 stoprevs = {self.rev(n) for n in stop or []}
1360 1360
1361 1361 revs = dagop.headrevssubset(
1362 1362 self.revs, self.parentrevs, startrev=start, stoprevs=stoprevs
1363 1363 )
1364 1364
1365 1365 return [self.node(rev) for rev in revs]
1366 1366
1367 1367 def children(self, node):
1368 1368 """find the children of a given node"""
1369 1369 c = []
1370 1370 p = self.rev(node)
1371 1371 for r in self.revs(start=p + 1):
1372 1372 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
1373 1373 if prevs:
1374 1374 for pr in prevs:
1375 1375 if pr == p:
1376 1376 c.append(self.node(r))
1377 1377 elif p == nullrev:
1378 1378 c.append(self.node(r))
1379 1379 return c
1380 1380
1381 1381 def commonancestorsheads(self, a, b):
1382 1382 """calculate all the heads of the common ancestors of nodes a and b"""
1383 1383 a, b = self.rev(a), self.rev(b)
1384 1384 ancs = self._commonancestorsheads(a, b)
1385 1385 return pycompat.maplist(self.node, ancs)
1386 1386
1387 1387 def _commonancestorsheads(self, *revs):
1388 1388 """calculate all the heads of the common ancestors of revs"""
1389 1389 try:
1390 1390 ancs = self.index.commonancestorsheads(*revs)
1391 1391 except (AttributeError, OverflowError): # C implementation failed
1392 1392 ancs = ancestor.commonancestorsheads(self.parentrevs, *revs)
1393 1393 return ancs
1394 1394
1395 1395 def isancestor(self, a, b):
1396 1396 """return True if node a is an ancestor of node b
1397 1397
1398 1398 A revision is considered an ancestor of itself."""
1399 1399 a, b = self.rev(a), self.rev(b)
1400 1400 return self.isancestorrev(a, b)
1401 1401
1402 1402 def isancestorrev(self, a, b):
1403 1403 """return True if revision a is an ancestor of revision b
1404 1404
1405 1405 A revision is considered an ancestor of itself.
1406 1406
1407 1407 The implementation of this is trivial but the use of
1408 1408 reachableroots is not."""
1409 1409 if a == nullrev:
1410 1410 return True
1411 1411 elif a == b:
1412 1412 return True
1413 1413 elif a > b:
1414 1414 return False
1415 1415 return bool(self.reachableroots(a, [b], [a], includepath=False))
1416 1416
1417 1417 def reachableroots(self, minroot, heads, roots, includepath=False):
1418 1418 """return (heads(::(<roots> and <roots>::<heads>)))
1419 1419
1420 1420 If includepath is True, return (<roots>::<heads>)."""
1421 1421 try:
1422 1422 return self.index.reachableroots2(
1423 1423 minroot, heads, roots, includepath
1424 1424 )
1425 1425 except AttributeError:
1426 1426 return dagop._reachablerootspure(
1427 1427 self.parentrevs, minroot, roots, heads, includepath
1428 1428 )
1429 1429
1430 1430 def ancestor(self, a, b):
1431 1431 """calculate the "best" common ancestor of nodes a and b"""
1432 1432
1433 1433 a, b = self.rev(a), self.rev(b)
1434 1434 try:
1435 1435 ancs = self.index.ancestors(a, b)
1436 1436 except (AttributeError, OverflowError):
1437 1437 ancs = ancestor.ancestors(self.parentrevs, a, b)
1438 1438 if ancs:
1439 1439 # choose a consistent winner when there's a tie
1440 1440 return min(map(self.node, ancs))
1441 1441 return nullid
1442 1442
1443 1443 def _match(self, id):
1444 1444 if isinstance(id, int):
1445 1445 # rev
1446 1446 return self.node(id)
1447 1447 if len(id) == 20:
1448 1448 # possibly a binary node
1449 1449 # odds of a binary node being all hex in ASCII are 1 in 10**25
1450 1450 try:
1451 1451 node = id
1452 1452 self.rev(node) # quick search the index
1453 1453 return node
1454 1454 except error.LookupError:
1455 1455 pass # may be partial hex id
1456 1456 try:
1457 1457 # str(rev)
1458 1458 rev = int(id)
1459 1459 if b"%d" % rev != id:
1460 1460 raise ValueError
1461 1461 if rev < 0:
1462 1462 rev = len(self) + rev
1463 1463 if rev < 0 or rev >= len(self):
1464 1464 raise ValueError
1465 1465 return self.node(rev)
1466 1466 except (ValueError, OverflowError):
1467 1467 pass
1468 1468 if len(id) == 40:
1469 1469 try:
1470 1470 # a full hex nodeid?
1471 1471 node = bin(id)
1472 1472 self.rev(node)
1473 1473 return node
1474 1474 except (TypeError, error.LookupError):
1475 1475 pass
1476 1476
1477 1477 def _partialmatch(self, id):
1478 1478 # we don't care wdirfilenodeids as they should be always full hash
1479 1479 maybewdir = wdirhex.startswith(id)
1480 1480 try:
1481 1481 partial = self.index.partialmatch(id)
1482 1482 if partial and self.hasnode(partial):
1483 1483 if maybewdir:
1484 1484 # single 'ff...' match in radix tree, ambiguous with wdir
1485 1485 raise error.RevlogError
1486 1486 return partial
1487 1487 if maybewdir:
1488 1488 # no 'ff...' match in radix tree, wdir identified
1489 1489 raise error.WdirUnsupported
1490 1490 return None
1491 1491 except error.RevlogError:
1492 1492 # parsers.c radix tree lookup gave multiple matches
1493 1493 # fast path: for unfiltered changelog, radix tree is accurate
1494 1494 if not getattr(self, 'filteredrevs', None):
1495 1495 raise error.AmbiguousPrefixLookupError(
1496 1496 id, self.indexfile, _(b'ambiguous identifier')
1497 1497 )
1498 1498 # fall through to slow path that filters hidden revisions
1499 1499 except (AttributeError, ValueError):
1500 1500 # we are pure python, or key was too short to search radix tree
1501 1501 pass
1502 1502
1503 1503 if id in self._pcache:
1504 1504 return self._pcache[id]
1505 1505
1506 1506 if len(id) <= 40:
1507 1507 try:
1508 1508 # hex(node)[:...]
1509 1509 l = len(id) // 2 # grab an even number of digits
1510 1510 prefix = bin(id[: l * 2])
1511 1511 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
1512 1512 nl = [
1513 1513 n for n in nl if hex(n).startswith(id) and self.hasnode(n)
1514 1514 ]
1515 1515 if nullhex.startswith(id):
1516 1516 nl.append(nullid)
1517 1517 if len(nl) > 0:
1518 1518 if len(nl) == 1 and not maybewdir:
1519 1519 self._pcache[id] = nl[0]
1520 1520 return nl[0]
1521 1521 raise error.AmbiguousPrefixLookupError(
1522 1522 id, self.indexfile, _(b'ambiguous identifier')
1523 1523 )
1524 1524 if maybewdir:
1525 1525 raise error.WdirUnsupported
1526 1526 return None
1527 1527 except TypeError:
1528 1528 pass
1529 1529
1530 1530 def lookup(self, id):
1531 1531 """locate a node based on:
1532 1532 - revision number or str(revision number)
1533 1533 - nodeid or subset of hex nodeid
1534 1534 """
1535 1535 n = self._match(id)
1536 1536 if n is not None:
1537 1537 return n
1538 1538 n = self._partialmatch(id)
1539 1539 if n:
1540 1540 return n
1541 1541
1542 1542 raise error.LookupError(id, self.indexfile, _(b'no match found'))
1543 1543
1544 1544 def shortest(self, node, minlength=1):
1545 1545 """Find the shortest unambiguous prefix that matches node."""
1546 1546
1547 1547 def isvalid(prefix):
1548 1548 try:
1549 1549 matchednode = self._partialmatch(prefix)
1550 1550 except error.AmbiguousPrefixLookupError:
1551 1551 return False
1552 1552 except error.WdirUnsupported:
1553 1553 # single 'ff...' match
1554 1554 return True
1555 1555 if matchednode is None:
1556 1556 raise error.LookupError(node, self.indexfile, _(b'no node'))
1557 1557 return True
1558 1558
1559 1559 def maybewdir(prefix):
1560 1560 return all(c == b'f' for c in pycompat.iterbytestr(prefix))
1561 1561
1562 1562 hexnode = hex(node)
1563 1563
1564 1564 def disambiguate(hexnode, minlength):
1565 1565 """Disambiguate against wdirid."""
1566 1566 for length in range(minlength, len(hexnode) + 1):
1567 1567 prefix = hexnode[:length]
1568 1568 if not maybewdir(prefix):
1569 1569 return prefix
1570 1570
1571 1571 if not getattr(self, 'filteredrevs', None):
1572 1572 try:
1573 1573 length = max(self.index.shortest(node), minlength)
1574 1574 return disambiguate(hexnode, length)
1575 1575 except error.RevlogError:
1576 1576 if node != wdirid:
1577 1577 raise error.LookupError(node, self.indexfile, _(b'no node'))
1578 1578 except AttributeError:
1579 1579 # Fall through to pure code
1580 1580 pass
1581 1581
1582 1582 if node == wdirid:
1583 1583 for length in range(minlength, len(hexnode) + 1):
1584 1584 prefix = hexnode[:length]
1585 1585 if isvalid(prefix):
1586 1586 return prefix
1587 1587
1588 1588 for length in range(minlength, len(hexnode) + 1):
1589 1589 prefix = hexnode[:length]
1590 1590 if isvalid(prefix):
1591 1591 return disambiguate(hexnode, length)
1592 1592
1593 1593 def cmp(self, node, text):
1594 1594 """compare text with a given file revision
1595 1595
1596 1596 returns True if text is different than what is stored.
1597 1597 """
1598 1598 p1, p2 = self.parents(node)
1599 1599 return storageutil.hashrevisionsha1(text, p1, p2) != node
1600 1600
1601 1601 def _cachesegment(self, offset, data):
1602 1602 """Add a segment to the revlog cache.
1603 1603
1604 1604 Accepts an absolute offset and the data that is at that location.
1605 1605 """
1606 1606 o, d = self._chunkcache
1607 1607 # try to add to existing cache
1608 1608 if o + len(d) == offset and len(d) + len(data) < _chunksize:
1609 1609 self._chunkcache = o, d + data
1610 1610 else:
1611 1611 self._chunkcache = offset, data
1612 1612
1613 1613 def _readsegment(self, offset, length, df=None):
1614 1614 """Load a segment of raw data from the revlog.
1615 1615
1616 1616 Accepts an absolute offset, length to read, and an optional existing
1617 1617 file handle to read from.
1618 1618
1619 1619 If an existing file handle is passed, it will be seeked and the
1620 1620 original seek position will NOT be restored.
1621 1621
1622 1622 Returns a str or buffer of raw byte data.
1623 1623
1624 1624 Raises if the requested number of bytes could not be read.
1625 1625 """
1626 1626 # Cache data both forward and backward around the requested
1627 1627 # data, in a fixed size window. This helps speed up operations
1628 1628 # involving reading the revlog backwards.
1629 1629 cachesize = self._chunkcachesize
1630 1630 realoffset = offset & ~(cachesize - 1)
1631 1631 reallength = (
1632 1632 (offset + length + cachesize) & ~(cachesize - 1)
1633 1633 ) - realoffset
1634 1634 with self._datareadfp(df) as df:
1635 1635 df.seek(realoffset)
1636 1636 d = df.read(reallength)
1637 1637
1638 1638 self._cachesegment(realoffset, d)
1639 1639 if offset != realoffset or reallength != length:
1640 1640 startoffset = offset - realoffset
1641 1641 if len(d) - startoffset < length:
1642 1642 raise error.RevlogError(
1643 1643 _(
1644 1644 b'partial read of revlog %s; expected %d bytes from '
1645 1645 b'offset %d, got %d'
1646 1646 )
1647 1647 % (
1648 1648 self.indexfile if self._inline else self.datafile,
1649 1649 length,
1650 1650 realoffset,
1651 1651 len(d) - startoffset,
1652 1652 )
1653 1653 )
1654 1654
1655 1655 return util.buffer(d, startoffset, length)
1656 1656
1657 1657 if len(d) < length:
1658 1658 raise error.RevlogError(
1659 1659 _(
1660 1660 b'partial read of revlog %s; expected %d bytes from offset '
1661 1661 b'%d, got %d'
1662 1662 )
1663 1663 % (
1664 1664 self.indexfile if self._inline else self.datafile,
1665 1665 length,
1666 1666 offset,
1667 1667 len(d),
1668 1668 )
1669 1669 )
1670 1670
1671 1671 return d
1672 1672
1673 1673 def _getsegment(self, offset, length, df=None):
1674 1674 """Obtain a segment of raw data from the revlog.
1675 1675
1676 1676 Accepts an absolute offset, length of bytes to obtain, and an
1677 1677 optional file handle to the already-opened revlog. If the file
1678 1678 handle is used, it's original seek position will not be preserved.
1679 1679
1680 1680 Requests for data may be returned from a cache.
1681 1681
1682 1682 Returns a str or a buffer instance of raw byte data.
1683 1683 """
1684 1684 o, d = self._chunkcache
1685 1685 l = len(d)
1686 1686
1687 1687 # is it in the cache?
1688 1688 cachestart = offset - o
1689 1689 cacheend = cachestart + length
1690 1690 if cachestart >= 0 and cacheend <= l:
1691 1691 if cachestart == 0 and cacheend == l:
1692 1692 return d # avoid a copy
1693 1693 return util.buffer(d, cachestart, cacheend - cachestart)
1694 1694
1695 1695 return self._readsegment(offset, length, df=df)
1696 1696
1697 1697 def _getsegmentforrevs(self, startrev, endrev, df=None):
1698 1698 """Obtain a segment of raw data corresponding to a range of revisions.
1699 1699
1700 1700 Accepts the start and end revisions and an optional already-open
1701 1701 file handle to be used for reading. If the file handle is read, its
1702 1702 seek position will not be preserved.
1703 1703
1704 1704 Requests for data may be satisfied by a cache.
1705 1705
1706 1706 Returns a 2-tuple of (offset, data) for the requested range of
1707 1707 revisions. Offset is the integer offset from the beginning of the
1708 1708 revlog and data is a str or buffer of the raw byte data.
1709 1709
1710 1710 Callers will need to call ``self.start(rev)`` and ``self.length(rev)``
1711 1711 to determine where each revision's data begins and ends.
1712 1712 """
1713 1713 # Inlined self.start(startrev) & self.end(endrev) for perf reasons
1714 1714 # (functions are expensive).
1715 1715 index = self.index
1716 1716 istart = index[startrev]
1717 1717 start = int(istart[0] >> 16)
1718 1718 if startrev == endrev:
1719 1719 end = start + istart[1]
1720 1720 else:
1721 1721 iend = index[endrev]
1722 1722 end = int(iend[0] >> 16) + iend[1]
1723 1723
1724 1724 if self._inline:
1725 1725 start += (startrev + 1) * self._io.size
1726 1726 end += (endrev + 1) * self._io.size
1727 1727 length = end - start
1728 1728
1729 1729 return start, self._getsegment(start, length, df=df)
1730 1730
1731 1731 def _chunk(self, rev, df=None):
1732 1732 """Obtain a single decompressed chunk for a revision.
1733 1733
1734 1734 Accepts an integer revision and an optional already-open file handle
1735 1735 to be used for reading. If used, the seek position of the file will not
1736 1736 be preserved.
1737 1737
1738 1738 Returns a str holding uncompressed data for the requested revision.
1739 1739 """
1740 1740 return self.decompress(self._getsegmentforrevs(rev, rev, df=df)[1])
1741 1741
1742 1742 def _chunks(self, revs, df=None, targetsize=None):
1743 1743 """Obtain decompressed chunks for the specified revisions.
1744 1744
1745 1745 Accepts an iterable of numeric revisions that are assumed to be in
1746 1746 ascending order. Also accepts an optional already-open file handle
1747 1747 to be used for reading. If used, the seek position of the file will
1748 1748 not be preserved.
1749 1749
1750 1750 This function is similar to calling ``self._chunk()`` multiple times,
1751 1751 but is faster.
1752 1752
1753 1753 Returns a list with decompressed data for each requested revision.
1754 1754 """
1755 1755 if not revs:
1756 1756 return []
1757 1757 start = self.start
1758 1758 length = self.length
1759 1759 inline = self._inline
1760 1760 iosize = self._io.size
1761 1761 buffer = util.buffer
1762 1762
1763 1763 l = []
1764 1764 ladd = l.append
1765 1765
1766 1766 if not self._withsparseread:
1767 1767 slicedchunks = (revs,)
1768 1768 else:
1769 1769 slicedchunks = deltautil.slicechunk(
1770 1770 self, revs, targetsize=targetsize
1771 1771 )
1772 1772
1773 1773 for revschunk in slicedchunks:
1774 1774 firstrev = revschunk[0]
1775 1775 # Skip trailing revisions with empty diff
1776 1776 for lastrev in revschunk[::-1]:
1777 1777 if length(lastrev) != 0:
1778 1778 break
1779 1779
1780 1780 try:
1781 1781 offset, data = self._getsegmentforrevs(firstrev, lastrev, df=df)
1782 1782 except OverflowError:
1783 1783 # issue4215 - we can't cache a run of chunks greater than
1784 1784 # 2G on Windows
1785 1785 return [self._chunk(rev, df=df) for rev in revschunk]
1786 1786
1787 1787 decomp = self.decompress
1788 1788 for rev in revschunk:
1789 1789 chunkstart = start(rev)
1790 1790 if inline:
1791 1791 chunkstart += (rev + 1) * iosize
1792 1792 chunklength = length(rev)
1793 1793 ladd(decomp(buffer(data, chunkstart - offset, chunklength)))
1794 1794
1795 1795 return l
1796 1796
1797 1797 def _chunkclear(self):
1798 1798 """Clear the raw chunk cache."""
1799 1799 self._chunkcache = (0, b'')
1800 1800
1801 1801 def deltaparent(self, rev):
1802 1802 """return deltaparent of the given revision"""
1803 1803 base = self.index[rev][3]
1804 1804 if base == rev:
1805 1805 return nullrev
1806 1806 elif self._generaldelta:
1807 1807 return base
1808 1808 else:
1809 1809 return rev - 1
1810 1810
1811 1811 def issnapshot(self, rev):
1812 1812 """tells whether rev is a snapshot"""
1813 1813 if not self._sparserevlog:
1814 1814 return self.deltaparent(rev) == nullrev
1815 1815 elif util.safehasattr(self.index, b'issnapshot'):
1816 1816 # directly assign the method to cache the testing and access
1817 1817 self.issnapshot = self.index.issnapshot
1818 1818 return self.issnapshot(rev)
1819 1819 if rev == nullrev:
1820 1820 return True
1821 1821 entry = self.index[rev]
1822 1822 base = entry[3]
1823 1823 if base == rev:
1824 1824 return True
1825 1825 if base == nullrev:
1826 1826 return True
1827 1827 p1 = entry[5]
1828 1828 p2 = entry[6]
1829 1829 if base == p1 or base == p2:
1830 1830 return False
1831 1831 return self.issnapshot(base)
1832 1832
1833 1833 def snapshotdepth(self, rev):
1834 1834 """number of snapshot in the chain before this one"""
1835 1835 if not self.issnapshot(rev):
1836 1836 raise error.ProgrammingError(b'revision %d not a snapshot')
1837 1837 return len(self._deltachain(rev)[0]) - 1
1838 1838
1839 1839 def revdiff(self, rev1, rev2):
1840 1840 """return or calculate a delta between two revisions
1841 1841
1842 1842 The delta calculated is in binary form and is intended to be written to
1843 1843 revlog data directly. So this function needs raw revision data.
1844 1844 """
1845 1845 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
1846 1846 return bytes(self._chunk(rev2))
1847 1847
1848 1848 return mdiff.textdiff(self.rawdata(rev1), self.rawdata(rev2))
1849 1849
1850 1850 def _processflags(self, text, flags, operation, raw=False):
1851 1851 """deprecated entry point to access flag processors"""
1852 1852 msg = b'_processflag(...) use the specialized variant'
1853 1853 util.nouideprecwarn(msg, b'5.2', stacklevel=2)
1854 1854 if raw:
1855 1855 return text, flagutil.processflagsraw(self, text, flags)
1856 1856 elif operation == b'read':
1857 1857 return flagutil.processflagsread(self, text, flags)
1858 1858 else: # write operation
1859 1859 return flagutil.processflagswrite(self, text, flags)
1860 1860
1861 1861 def revision(self, nodeorrev, _df=None, raw=False):
1862 1862 """return an uncompressed revision of a given node or revision
1863 1863 number.
1864 1864
1865 1865 _df - an existing file handle to read from. (internal-only)
1866 1866 raw - an optional argument specifying if the revision data is to be
1867 1867 treated as raw data when applying flag transforms. 'raw' should be set
1868 1868 to True when generating changegroups or in debug commands.
1869 1869 """
1870 1870 if raw:
1871 1871 msg = (
1872 1872 b'revlog.revision(..., raw=True) is deprecated, '
1873 1873 b'use revlog.rawdata(...)'
1874 1874 )
1875 1875 util.nouideprecwarn(msg, b'5.2', stacklevel=2)
1876 1876 return self._revisiondata(nodeorrev, _df, raw=raw)[0]
1877 1877
1878 1878 def sidedata(self, nodeorrev, _df=None):
1879 1879 """a map of extra data related to the changeset but not part of the hash
1880 1880
1881 1881 This function currently return a dictionary. However, more advanced
1882 1882 mapping object will likely be used in the future for a more
1883 1883 efficient/lazy code.
1884 1884 """
1885 1885 return self._revisiondata(nodeorrev, _df)[1]
1886 1886
1887 1887 def _revisiondata(self, nodeorrev, _df=None, raw=False):
1888 1888 # deal with <nodeorrev> argument type
1889 1889 if isinstance(nodeorrev, int):
1890 1890 rev = nodeorrev
1891 1891 node = self.node(rev)
1892 1892 else:
1893 1893 node = nodeorrev
1894 1894 rev = None
1895 1895
1896 1896 # fast path the special `nullid` rev
1897 1897 if node == nullid:
1898 1898 return b"", {}
1899 1899
1900 1900 # ``rawtext`` is the text as stored inside the revlog. Might be the
1901 1901 # revision or might need to be processed to retrieve the revision.
1902 1902 rev, rawtext, validated = self._rawtext(node, rev, _df=_df)
1903 1903
1904 1904 if self.version & 0xFFFF == REVLOGV2:
1905 1905 if rev is None:
1906 1906 rev = self.rev(node)
1907 1907 sidedata = self._sidedata(rev)
1908 1908 else:
1909 1909 sidedata = {}
1910 1910
1911 1911 if raw and validated:
1912 1912 # if we don't want to process the raw text and that raw
1913 1913 # text is cached, we can exit early.
1914 1914 return rawtext, sidedata
1915 1915 if rev is None:
1916 1916 rev = self.rev(node)
1917 1917 # the revlog's flag for this revision
1918 1918 # (usually alter its state or content)
1919 1919 flags = self.flags(rev)
1920 1920
1921 1921 if validated and flags == REVIDX_DEFAULT_FLAGS:
1922 1922 # no extra flags set, no flag processor runs, text = rawtext
1923 1923 return rawtext, sidedata
1924 1924
1925 1925 if raw:
1926 1926 validatehash = flagutil.processflagsraw(self, rawtext, flags)
1927 1927 text = rawtext
1928 1928 else:
1929 1929 r = flagutil.processflagsread(self, rawtext, flags)
1930 1930 text, validatehash = r
1931 1931 if validatehash:
1932 1932 self.checkhash(text, node, rev=rev)
1933 1933 if not validated:
1934 1934 self._revisioncache = (node, rev, rawtext)
1935 1935
1936 1936 return text, sidedata
1937 1937
1938 1938 def _rawtext(self, node, rev, _df=None):
1939 1939 """return the possibly unvalidated rawtext for a revision
1940 1940
1941 1941 returns (rev, rawtext, validated)
1942 1942 """
1943 1943
1944 1944 # revision in the cache (could be useful to apply delta)
1945 1945 cachedrev = None
1946 1946 # An intermediate text to apply deltas to
1947 1947 basetext = None
1948 1948
1949 1949 # Check if we have the entry in cache
1950 1950 # The cache entry looks like (node, rev, rawtext)
1951 1951 if self._revisioncache:
1952 1952 if self._revisioncache[0] == node:
1953 1953 return (rev, self._revisioncache[2], True)
1954 1954 cachedrev = self._revisioncache[1]
1955 1955
1956 1956 if rev is None:
1957 1957 rev = self.rev(node)
1958 1958
1959 1959 chain, stopped = self._deltachain(rev, stoprev=cachedrev)
1960 1960 if stopped:
1961 1961 basetext = self._revisioncache[2]
1962 1962
1963 1963 # drop cache to save memory, the caller is expected to
1964 1964 # update self._revisioncache after validating the text
1965 1965 self._revisioncache = None
1966 1966
1967 1967 targetsize = None
1968 1968 rawsize = self.index[rev][2]
1969 1969 if 0 <= rawsize:
1970 1970 targetsize = 4 * rawsize
1971 1971
1972 1972 bins = self._chunks(chain, df=_df, targetsize=targetsize)
1973 1973 if basetext is None:
1974 1974 basetext = bytes(bins[0])
1975 1975 bins = bins[1:]
1976 1976
1977 1977 rawtext = mdiff.patches(basetext, bins)
1978 1978 del basetext # let us have a chance to free memory early
1979 1979 return (rev, rawtext, False)
1980 1980
1981 1981 def _sidedata(self, rev):
1982 1982 """Return the sidedata for a given revision number."""
1983 1983 index_entry = self.index[rev]
1984 1984 sidedata_offset = index_entry[8]
1985 1985 sidedata_size = index_entry[9]
1986 1986
1987 1987 if self._inline:
1988 1988 sidedata_offset += self._io.size * (1 + rev)
1989 1989 if sidedata_size == 0:
1990 1990 return {}
1991 1991
1992 1992 segment = self._getsegment(sidedata_offset, sidedata_size)
1993 1993 sidedata = sidedatautil.deserialize_sidedata(segment)
1994 1994 return sidedata
1995 1995
1996 1996 def rawdata(self, nodeorrev, _df=None):
1997 1997 """return an uncompressed raw data of a given node or revision number.
1998 1998
1999 1999 _df - an existing file handle to read from. (internal-only)
2000 2000 """
2001 2001 return self._revisiondata(nodeorrev, _df, raw=True)[0]
2002 2002
2003 2003 def hash(self, text, p1, p2):
2004 2004 """Compute a node hash.
2005 2005
2006 2006 Available as a function so that subclasses can replace the hash
2007 2007 as needed.
2008 2008 """
2009 2009 return storageutil.hashrevisionsha1(text, p1, p2)
2010 2010
2011 2011 def checkhash(self, text, node, p1=None, p2=None, rev=None):
2012 2012 """Check node hash integrity.
2013 2013
2014 2014 Available as a function so that subclasses can extend hash mismatch
2015 2015 behaviors as needed.
2016 2016 """
2017 2017 try:
2018 2018 if p1 is None and p2 is None:
2019 2019 p1, p2 = self.parents(node)
2020 2020 if node != self.hash(text, p1, p2):
2021 2021 # Clear the revision cache on hash failure. The revision cache
2022 2022 # only stores the raw revision and clearing the cache does have
2023 2023 # the side-effect that we won't have a cache hit when the raw
2024 2024 # revision data is accessed. But this case should be rare and
2025 2025 # it is extra work to teach the cache about the hash
2026 2026 # verification state.
2027 2027 if self._revisioncache and self._revisioncache[0] == node:
2028 2028 self._revisioncache = None
2029 2029
2030 2030 revornode = rev
2031 2031 if revornode is None:
2032 2032 revornode = templatefilters.short(hex(node))
2033 2033 raise error.RevlogError(
2034 2034 _(b"integrity check failed on %s:%s")
2035 2035 % (self.indexfile, pycompat.bytestr(revornode))
2036 2036 )
2037 2037 except error.RevlogError:
2038 2038 if self._censorable and storageutil.iscensoredtext(text):
2039 2039 raise error.CensoredNodeError(self.indexfile, node, text)
2040 2040 raise
2041 2041
2042 2042 def _enforceinlinesize(self, tr, fp=None):
2043 2043 """Check if the revlog is too big for inline and convert if so.
2044 2044
2045 2045 This should be called after revisions are added to the revlog. If the
2046 2046 revlog has grown too large to be an inline revlog, it will convert it
2047 2047 to use multiple index and data files.
2048 2048 """
2049 2049 tiprev = len(self) - 1
2050 2050 if (
2051 2051 not self._inline
2052 2052 or (self.start(tiprev) + self.length(tiprev)) < _maxinline
2053 2053 ):
2054 2054 return
2055 2055
2056 2056 troffset = tr.findoffset(self.indexfile)
2057 2057 if troffset is None:
2058 2058 raise error.RevlogError(
2059 2059 _(b"%s not found in the transaction") % self.indexfile
2060 2060 )
2061 2061 trindex = 0
2062 2062 tr.add(self.datafile, 0)
2063 2063
2064 2064 if fp:
2065 2065 fp.flush()
2066 2066 fp.close()
2067 2067 # We can't use the cached file handle after close(). So prevent
2068 2068 # its usage.
2069 2069 self._writinghandles = None
2070 2070
2071 2071 with self._indexfp(b'r') as ifh, self._datafp(b'w') as dfh:
2072 2072 for r in self:
2073 2073 dfh.write(self._getsegmentforrevs(r, r, df=ifh)[1])
2074 2074 if troffset <= self.start(r):
2075 2075 trindex = r
2076 2076
2077 2077 with self._indexfp(b'w') as fp:
2078 2078 self.version &= ~FLAG_INLINE_DATA
2079 2079 self._inline = False
2080 2080 io = self._io
2081 2081 for i in self:
2082 2082 e = io.packentry(self.index[i], self.node, self.version, i)
2083 2083 fp.write(e)
2084 2084
2085 2085 # the temp file replace the real index when we exit the context
2086 2086 # manager
2087 2087
2088 2088 tr.replace(self.indexfile, trindex * self._io.size)
2089 2089 nodemaputil.setup_persistent_nodemap(tr, self)
2090 2090 self._chunkclear()
2091 2091
2092 2092 def _nodeduplicatecallback(self, transaction, node):
2093 2093 """called when trying to add a node already stored."""
2094 2094
2095 2095 def addrevision(
2096 2096 self,
2097 2097 text,
2098 2098 transaction,
2099 2099 link,
2100 2100 p1,
2101 2101 p2,
2102 2102 cachedelta=None,
2103 2103 node=None,
2104 2104 flags=REVIDX_DEFAULT_FLAGS,
2105 2105 deltacomputer=None,
2106 2106 sidedata=None,
2107 2107 ):
2108 2108 """add a revision to the log
2109 2109
2110 2110 text - the revision data to add
2111 2111 transaction - the transaction object used for rollback
2112 2112 link - the linkrev data to add
2113 2113 p1, p2 - the parent nodeids of the revision
2114 2114 cachedelta - an optional precomputed delta
2115 2115 node - nodeid of revision; typically node is not specified, and it is
2116 2116 computed by default as hash(text, p1, p2), however subclasses might
2117 2117 use different hashing method (and override checkhash() in such case)
2118 2118 flags - the known flags to set on the revision
2119 2119 deltacomputer - an optional deltacomputer instance shared between
2120 2120 multiple calls
2121 2121 """
2122 2122 if link == nullrev:
2123 2123 raise error.RevlogError(
2124 2124 _(b"attempted to add linkrev -1 to %s") % self.indexfile
2125 2125 )
2126 2126
2127 2127 if sidedata is None:
2128 2128 sidedata = {}
2129 2129 elif not self.hassidedata:
2130 2130 raise error.ProgrammingError(
2131 2131 _(b"trying to add sidedata to a revlog who don't support them")
2132 2132 )
2133 2133
2134 2134 if flags:
2135 2135 node = node or self.hash(text, p1, p2)
2136 2136
2137 2137 rawtext, validatehash = flagutil.processflagswrite(self, text, flags)
2138 2138
2139 2139 # If the flag processor modifies the revision data, ignore any provided
2140 2140 # cachedelta.
2141 2141 if rawtext != text:
2142 2142 cachedelta = None
2143 2143
2144 2144 if len(rawtext) > _maxentrysize:
2145 2145 raise error.RevlogError(
2146 2146 _(
2147 2147 b"%s: size of %d bytes exceeds maximum revlog storage of 2GiB"
2148 2148 )
2149 2149 % (self.indexfile, len(rawtext))
2150 2150 )
2151 2151
2152 2152 node = node or self.hash(rawtext, p1, p2)
2153 2153 rev = self.index.get_rev(node)
2154 2154 if rev is not None:
2155 2155 return rev
2156 2156
2157 2157 if validatehash:
2158 2158 self.checkhash(rawtext, node, p1=p1, p2=p2)
2159 2159
2160 2160 return self.addrawrevision(
2161 2161 rawtext,
2162 2162 transaction,
2163 2163 link,
2164 2164 p1,
2165 2165 p2,
2166 2166 node,
2167 2167 flags,
2168 2168 cachedelta=cachedelta,
2169 2169 deltacomputer=deltacomputer,
2170 2170 sidedata=sidedata,
2171 2171 )
2172 2172
2173 2173 def addrawrevision(
2174 2174 self,
2175 2175 rawtext,
2176 2176 transaction,
2177 2177 link,
2178 2178 p1,
2179 2179 p2,
2180 2180 node,
2181 2181 flags,
2182 2182 cachedelta=None,
2183 2183 deltacomputer=None,
2184 2184 sidedata=None,
2185 2185 ):
2186 2186 """add a raw revision with known flags, node and parents
2187 2187 useful when reusing a revision not stored in this revlog (ex: received
2188 2188 over wire, or read from an external bundle).
2189 2189 """
2190 2190 dfh = None
2191 2191 if not self._inline:
2192 2192 dfh = self._datafp(b"a+")
2193 2193 ifh = self._indexfp(b"a+")
2194 2194 try:
2195 2195 return self._addrevision(
2196 2196 node,
2197 2197 rawtext,
2198 2198 transaction,
2199 2199 link,
2200 2200 p1,
2201 2201 p2,
2202 2202 flags,
2203 2203 cachedelta,
2204 2204 ifh,
2205 2205 dfh,
2206 2206 deltacomputer=deltacomputer,
2207 2207 sidedata=sidedata,
2208 2208 )
2209 2209 finally:
2210 2210 if dfh:
2211 2211 dfh.close()
2212 2212 ifh.close()
2213 2213
2214 2214 def compress(self, data):
2215 2215 """Generate a possibly-compressed representation of data."""
2216 2216 if not data:
2217 2217 return b'', data
2218 2218
2219 2219 compressed = self._compressor.compress(data)
2220 2220
2221 2221 if compressed:
2222 2222 # The revlog compressor added the header in the returned data.
2223 2223 return b'', compressed
2224 2224
2225 2225 if data[0:1] == b'\0':
2226 2226 return b'', data
2227 2227 return b'u', data
2228 2228
2229 2229 def decompress(self, data):
2230 2230 """Decompress a revlog chunk.
2231 2231
2232 2232 The chunk is expected to begin with a header identifying the
2233 2233 format type so it can be routed to an appropriate decompressor.
2234 2234 """
2235 2235 if not data:
2236 2236 return data
2237 2237
2238 2238 # Revlogs are read much more frequently than they are written and many
2239 2239 # chunks only take microseconds to decompress, so performance is
2240 2240 # important here.
2241 2241 #
2242 2242 # We can make a few assumptions about revlogs:
2243 2243 #
2244 2244 # 1) the majority of chunks will be compressed (as opposed to inline
2245 2245 # raw data).
2246 2246 # 2) decompressing *any* data will likely by at least 10x slower than
2247 2247 # returning raw inline data.
2248 2248 # 3) we want to prioritize common and officially supported compression
2249 2249 # engines
2250 2250 #
2251 2251 # It follows that we want to optimize for "decompress compressed data
2252 2252 # when encoded with common and officially supported compression engines"
2253 2253 # case over "raw data" and "data encoded by less common or non-official
2254 2254 # compression engines." That is why we have the inline lookup first
2255 2255 # followed by the compengines lookup.
2256 2256 #
2257 2257 # According to `hg perfrevlogchunks`, this is ~0.5% faster for zlib
2258 2258 # compressed chunks. And this matters for changelog and manifest reads.
2259 2259 t = data[0:1]
2260 2260
2261 2261 if t == b'x':
2262 2262 try:
2263 2263 return _zlibdecompress(data)
2264 2264 except zlib.error as e:
2265 2265 raise error.RevlogError(
2266 2266 _(b'revlog decompress error: %s')
2267 2267 % stringutil.forcebytestr(e)
2268 2268 )
2269 2269 # '\0' is more common than 'u' so it goes first.
2270 2270 elif t == b'\0':
2271 2271 return data
2272 2272 elif t == b'u':
2273 2273 return util.buffer(data, 1)
2274 2274
2275 2275 try:
2276 2276 compressor = self._decompressors[t]
2277 2277 except KeyError:
2278 2278 try:
2279 2279 engine = util.compengines.forrevlogheader(t)
2280 2280 compressor = engine.revlogcompressor(self._compengineopts)
2281 2281 self._decompressors[t] = compressor
2282 2282 except KeyError:
2283 2283 raise error.RevlogError(_(b'unknown compression type %r') % t)
2284 2284
2285 2285 return compressor.decompress(data)
2286 2286
2287 2287 def _addrevision(
2288 2288 self,
2289 2289 node,
2290 2290 rawtext,
2291 2291 transaction,
2292 2292 link,
2293 2293 p1,
2294 2294 p2,
2295 2295 flags,
2296 2296 cachedelta,
2297 2297 ifh,
2298 2298 dfh,
2299 2299 alwayscache=False,
2300 2300 deltacomputer=None,
2301 2301 sidedata=None,
2302 2302 ):
2303 2303 """internal function to add revisions to the log
2304 2304
2305 2305 see addrevision for argument descriptions.
2306 2306
2307 2307 note: "addrevision" takes non-raw text, "_addrevision" takes raw text.
2308 2308
2309 2309 if "deltacomputer" is not provided or None, a defaultdeltacomputer will
2310 2310 be used.
2311 2311
2312 2312 invariants:
2313 2313 - rawtext is optional (can be None); if not set, cachedelta must be set.
2314 2314 if both are set, they must correspond to each other.
2315 2315 """
2316 2316 if node == nullid:
2317 2317 raise error.RevlogError(
2318 2318 _(b"%s: attempt to add null revision") % self.indexfile
2319 2319 )
2320 2320 if node == wdirid or node in wdirfilenodeids:
2321 2321 raise error.RevlogError(
2322 2322 _(b"%s: attempt to add wdir revision") % self.indexfile
2323 2323 )
2324 2324
2325 2325 if self._inline:
2326 2326 fh = ifh
2327 2327 else:
2328 2328 fh = dfh
2329 2329
2330 2330 btext = [rawtext]
2331 2331
2332 2332 curr = len(self)
2333 2333 prev = curr - 1
2334 2334
2335 2335 offset = self._get_data_offset(prev)
2336 2336
2337 2337 if self._concurrencychecker:
2338 2338 if self._inline:
2339 2339 # offset is "as if" it were in the .d file, so we need to add on
2340 2340 # the size of the entry metadata.
2341 2341 self._concurrencychecker(
2342 2342 ifh, self.indexfile, offset + curr * self._io.size
2343 2343 )
2344 2344 else:
2345 2345 # Entries in the .i are a consistent size.
2346 2346 self._concurrencychecker(
2347 2347 ifh, self.indexfile, curr * self._io.size
2348 2348 )
2349 2349 self._concurrencychecker(dfh, self.datafile, offset)
2350 2350
2351 2351 p1r, p2r = self.rev(p1), self.rev(p2)
2352 2352
2353 2353 # full versions are inserted when the needed deltas
2354 2354 # become comparable to the uncompressed text
2355 2355 if rawtext is None:
2356 2356 # need rawtext size, before changed by flag processors, which is
2357 2357 # the non-raw size. use revlog explicitly to avoid filelog's extra
2358 2358 # logic that might remove metadata size.
2359 2359 textlen = mdiff.patchedsize(
2360 2360 revlog.size(self, cachedelta[0]), cachedelta[1]
2361 2361 )
2362 2362 else:
2363 2363 textlen = len(rawtext)
2364 2364
2365 2365 if deltacomputer is None:
2366 2366 deltacomputer = deltautil.deltacomputer(self)
2367 2367
2368 2368 revinfo = _revisioninfo(node, p1, p2, btext, textlen, cachedelta, flags)
2369 2369
2370 2370 deltainfo = deltacomputer.finddeltainfo(revinfo, fh)
2371 2371
2372 2372 if sidedata:
2373 2373 serialized_sidedata = sidedatautil.serialize_sidedata(sidedata)
2374 2374 sidedata_offset = offset + deltainfo.deltalen
2375 2375 else:
2376 2376 serialized_sidedata = b""
2377 2377 # Don't store the offset if the sidedata is empty, that way
2378 2378 # we can easily detect empty sidedata and they will be no different
2379 2379 # than ones we manually add.
2380 2380 sidedata_offset = 0
2381 2381
2382 2382 e = (
2383 2383 offset_type(offset, flags),
2384 2384 deltainfo.deltalen,
2385 2385 textlen,
2386 2386 deltainfo.base,
2387 2387 link,
2388 2388 p1r,
2389 2389 p2r,
2390 2390 node,
2391 2391 sidedata_offset,
2392 2392 len(serialized_sidedata),
2393 2393 )
2394 2394
2395 2395 if self.version & 0xFFFF != REVLOGV2:
2396 2396 e = e[:8]
2397 2397
2398 2398 self.index.append(e)
2399 2399 entry = self._io.packentry(e, self.node, self.version, curr)
2400 2400 self._writeentry(
2401 2401 transaction,
2402 2402 ifh,
2403 2403 dfh,
2404 2404 entry,
2405 2405 deltainfo.data,
2406 2406 link,
2407 2407 offset,
2408 2408 serialized_sidedata,
2409 2409 )
2410 2410
2411 2411 rawtext = btext[0]
2412 2412
2413 2413 if alwayscache and rawtext is None:
2414 2414 rawtext = deltacomputer.buildtext(revinfo, fh)
2415 2415
2416 2416 if type(rawtext) == bytes: # only accept immutable objects
2417 2417 self._revisioncache = (node, curr, rawtext)
2418 2418 self._chainbasecache[curr] = deltainfo.chainbase
2419 2419 return curr
2420 2420
2421 2421 def _get_data_offset(self, prev):
2422 2422 """Returns the current offset in the (in-transaction) data file.
2423 2423 Versions < 2 of the revlog can get this 0(1), revlog v2 needs a docket
2424 2424 file to store that information: since sidedata can be rewritten to the
2425 2425 end of the data file within a transaction, you can have cases where, for
2426 2426 example, rev `n` does not have sidedata while rev `n - 1` does, leading
2427 2427 to `n - 1`'s sidedata being written after `n`'s data.
2428 2428
2429 2429 TODO cache this in a docket file before getting out of experimental."""
2430 2430 if self.version & 0xFFFF != REVLOGV2:
2431 2431 return self.end(prev)
2432 2432
2433 2433 offset = 0
2434 2434 for rev, entry in enumerate(self.index):
2435 2435 sidedata_end = entry[8] + entry[9]
2436 2436 # Sidedata for a previous rev has potentially been written after
2437 2437 # this rev's end, so take the max.
2438 2438 offset = max(self.end(rev), offset, sidedata_end)
2439 2439 return offset
2440 2440
2441 2441 def _writeentry(
2442 2442 self, transaction, ifh, dfh, entry, data, link, offset, sidedata
2443 2443 ):
2444 2444 # Files opened in a+ mode have inconsistent behavior on various
2445 2445 # platforms. Windows requires that a file positioning call be made
2446 2446 # when the file handle transitions between reads and writes. See
2447 2447 # 3686fa2b8eee and the mixedfilemodewrapper in windows.py. On other
2448 2448 # platforms, Python or the platform itself can be buggy. Some versions
2449 2449 # of Solaris have been observed to not append at the end of the file
2450 2450 # if the file was seeked to before the end. See issue4943 for more.
2451 2451 #
2452 2452 # We work around this issue by inserting a seek() before writing.
2453 2453 # Note: This is likely not necessary on Python 3. However, because
2454 2454 # the file handle is reused for reads and may be seeked there, we need
2455 2455 # to be careful before changing this.
2456 2456 ifh.seek(0, os.SEEK_END)
2457 2457 if dfh:
2458 2458 dfh.seek(0, os.SEEK_END)
2459 2459
2460 2460 curr = len(self) - 1
2461 2461 if not self._inline:
2462 2462 transaction.add(self.datafile, offset)
2463 2463 transaction.add(self.indexfile, curr * len(entry))
2464 2464 if data[0]:
2465 2465 dfh.write(data[0])
2466 2466 dfh.write(data[1])
2467 2467 if sidedata:
2468 2468 dfh.write(sidedata)
2469 2469 ifh.write(entry)
2470 2470 else:
2471 2471 offset += curr * self._io.size
2472 2472 transaction.add(self.indexfile, offset)
2473 2473 ifh.write(entry)
2474 2474 ifh.write(data[0])
2475 2475 ifh.write(data[1])
2476 2476 if sidedata:
2477 2477 ifh.write(sidedata)
2478 2478 self._enforceinlinesize(transaction, ifh)
2479 2479 nodemaputil.setup_persistent_nodemap(transaction, self)
2480 2480
2481 2481 def addgroup(
2482 2482 self,
2483 2483 deltas,
2484 2484 linkmapper,
2485 2485 transaction,
2486 2486 alwayscache=False,
2487 2487 addrevisioncb=None,
2488 2488 duplicaterevisioncb=None,
2489 2489 ):
2490 2490 """
2491 2491 add a delta group
2492 2492
2493 2493 given a set of deltas, add them to the revision log. the
2494 2494 first delta is against its parent, which should be in our
2495 2495 log, the rest are against the previous delta.
2496 2496
2497 2497 If ``addrevisioncb`` is defined, it will be called with arguments of
2498 2498 this revlog and the node that was added.
2499 2499 """
2500 2500
2501 2501 if self._writinghandles:
2502 2502 raise error.ProgrammingError(b'cannot nest addgroup() calls')
2503 2503
2504 2504 r = len(self)
2505 2505 end = 0
2506 2506 if r:
2507 2507 end = self.end(r - 1)
2508 2508 ifh = self._indexfp(b"a+")
2509 2509 isize = r * self._io.size
2510 2510 if self._inline:
2511 2511 transaction.add(self.indexfile, end + isize)
2512 2512 dfh = None
2513 2513 else:
2514 2514 transaction.add(self.indexfile, isize)
2515 2515 transaction.add(self.datafile, end)
2516 2516 dfh = self._datafp(b"a+")
2517 2517
2518 2518 def flush():
2519 2519 if dfh:
2520 2520 dfh.flush()
2521 2521 ifh.flush()
2522 2522
2523 2523 self._writinghandles = (ifh, dfh)
2524 2524 empty = True
2525 2525
2526 2526 try:
2527 2527 deltacomputer = deltautil.deltacomputer(self)
2528 2528 # loop through our set of deltas
2529 2529 for data in deltas:
2530 node, p1, p2, linknode, deltabase, delta, flags = data
2530 node, p1, p2, linknode, deltabase, delta, flags, sidedata = data
2531 2531 link = linkmapper(linknode)
2532 2532 flags = flags or REVIDX_DEFAULT_FLAGS
2533 2533
2534 2534 rev = self.index.get_rev(node)
2535 2535 if rev is not None:
2536 2536 # this can happen if two branches make the same change
2537 2537 self._nodeduplicatecallback(transaction, rev)
2538 2538 if duplicaterevisioncb:
2539 2539 duplicaterevisioncb(self, rev)
2540 2540 empty = False
2541 2541 continue
2542 2542
2543 2543 for p in (p1, p2):
2544 2544 if not self.index.has_node(p):
2545 2545 raise error.LookupError(
2546 2546 p, self.indexfile, _(b'unknown parent')
2547 2547 )
2548 2548
2549 2549 if not self.index.has_node(deltabase):
2550 2550 raise error.LookupError(
2551 2551 deltabase, self.indexfile, _(b'unknown delta base')
2552 2552 )
2553 2553
2554 2554 baserev = self.rev(deltabase)
2555 2555
2556 2556 if baserev != nullrev and self.iscensored(baserev):
2557 2557 # if base is censored, delta must be full replacement in a
2558 2558 # single patch operation
2559 2559 hlen = struct.calcsize(b">lll")
2560 2560 oldlen = self.rawsize(baserev)
2561 2561 newlen = len(delta) - hlen
2562 2562 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
2563 2563 raise error.CensoredBaseError(
2564 2564 self.indexfile, self.node(baserev)
2565 2565 )
2566 2566
2567 2567 if not flags and self._peek_iscensored(baserev, delta, flush):
2568 2568 flags |= REVIDX_ISCENSORED
2569 2569
2570 2570 # We assume consumers of addrevisioncb will want to retrieve
2571 2571 # the added revision, which will require a call to
2572 2572 # revision(). revision() will fast path if there is a cache
2573 2573 # hit. So, we tell _addrevision() to always cache in this case.
2574 2574 # We're only using addgroup() in the context of changegroup
2575 2575 # generation so the revision data can always be handled as raw
2576 2576 # by the flagprocessor.
2577 2577 rev = self._addrevision(
2578 2578 node,
2579 2579 None,
2580 2580 transaction,
2581 2581 link,
2582 2582 p1,
2583 2583 p2,
2584 2584 flags,
2585 2585 (baserev, delta),
2586 2586 ifh,
2587 2587 dfh,
2588 2588 alwayscache=alwayscache,
2589 2589 deltacomputer=deltacomputer,
2590 2590 )
2591 2591
2592 2592 if addrevisioncb:
2593 2593 addrevisioncb(self, rev)
2594 2594 empty = False
2595 2595
2596 2596 if not dfh and not self._inline:
2597 2597 # addrevision switched from inline to conventional
2598 2598 # reopen the index
2599 2599 ifh.close()
2600 2600 dfh = self._datafp(b"a+")
2601 2601 ifh = self._indexfp(b"a+")
2602 2602 self._writinghandles = (ifh, dfh)
2603 2603 finally:
2604 2604 self._writinghandles = None
2605 2605
2606 2606 if dfh:
2607 2607 dfh.close()
2608 2608 ifh.close()
2609 2609 return not empty
2610 2610
2611 2611 def iscensored(self, rev):
2612 2612 """Check if a file revision is censored."""
2613 2613 if not self._censorable:
2614 2614 return False
2615 2615
2616 2616 return self.flags(rev) & REVIDX_ISCENSORED
2617 2617
2618 2618 def _peek_iscensored(self, baserev, delta, flush):
2619 2619 """Quickly check if a delta produces a censored revision."""
2620 2620 if not self._censorable:
2621 2621 return False
2622 2622
2623 2623 return storageutil.deltaiscensored(delta, baserev, self.rawsize)
2624 2624
2625 2625 def getstrippoint(self, minlink):
2626 2626 """find the minimum rev that must be stripped to strip the linkrev
2627 2627
2628 2628 Returns a tuple containing the minimum rev and a set of all revs that
2629 2629 have linkrevs that will be broken by this strip.
2630 2630 """
2631 2631 return storageutil.resolvestripinfo(
2632 2632 minlink,
2633 2633 len(self) - 1,
2634 2634 self.headrevs(),
2635 2635 self.linkrev,
2636 2636 self.parentrevs,
2637 2637 )
2638 2638
2639 2639 def strip(self, minlink, transaction):
2640 2640 """truncate the revlog on the first revision with a linkrev >= minlink
2641 2641
2642 2642 This function is called when we're stripping revision minlink and
2643 2643 its descendants from the repository.
2644 2644
2645 2645 We have to remove all revisions with linkrev >= minlink, because
2646 2646 the equivalent changelog revisions will be renumbered after the
2647 2647 strip.
2648 2648
2649 2649 So we truncate the revlog on the first of these revisions, and
2650 2650 trust that the caller has saved the revisions that shouldn't be
2651 2651 removed and that it'll re-add them after this truncation.
2652 2652 """
2653 2653 if len(self) == 0:
2654 2654 return
2655 2655
2656 2656 rev, _ = self.getstrippoint(minlink)
2657 2657 if rev == len(self):
2658 2658 return
2659 2659
2660 2660 # first truncate the files on disk
2661 2661 end = self.start(rev)
2662 2662 if not self._inline:
2663 2663 transaction.add(self.datafile, end)
2664 2664 end = rev * self._io.size
2665 2665 else:
2666 2666 end += rev * self._io.size
2667 2667
2668 2668 transaction.add(self.indexfile, end)
2669 2669
2670 2670 # then reset internal state in memory to forget those revisions
2671 2671 self._revisioncache = None
2672 2672 self._chaininfocache = util.lrucachedict(500)
2673 2673 self._chunkclear()
2674 2674
2675 2675 del self.index[rev:-1]
2676 2676
2677 2677 def checksize(self):
2678 2678 """Check size of index and data files
2679 2679
2680 2680 return a (dd, di) tuple.
2681 2681 - dd: extra bytes for the "data" file
2682 2682 - di: extra bytes for the "index" file
2683 2683
2684 2684 A healthy revlog will return (0, 0).
2685 2685 """
2686 2686 expected = 0
2687 2687 if len(self):
2688 2688 expected = max(0, self.end(len(self) - 1))
2689 2689
2690 2690 try:
2691 2691 with self._datafp() as f:
2692 2692 f.seek(0, io.SEEK_END)
2693 2693 actual = f.tell()
2694 2694 dd = actual - expected
2695 2695 except IOError as inst:
2696 2696 if inst.errno != errno.ENOENT:
2697 2697 raise
2698 2698 dd = 0
2699 2699
2700 2700 try:
2701 2701 f = self.opener(self.indexfile)
2702 2702 f.seek(0, io.SEEK_END)
2703 2703 actual = f.tell()
2704 2704 f.close()
2705 2705 s = self._io.size
2706 2706 i = max(0, actual // s)
2707 2707 di = actual - (i * s)
2708 2708 if self._inline:
2709 2709 databytes = 0
2710 2710 for r in self:
2711 2711 databytes += max(0, self.length(r))
2712 2712 dd = 0
2713 2713 di = actual - len(self) * s - databytes
2714 2714 except IOError as inst:
2715 2715 if inst.errno != errno.ENOENT:
2716 2716 raise
2717 2717 di = 0
2718 2718
2719 2719 return (dd, di)
2720 2720
2721 2721 def files(self):
2722 2722 res = [self.indexfile]
2723 2723 if not self._inline:
2724 2724 res.append(self.datafile)
2725 2725 return res
2726 2726
2727 2727 def emitrevisions(
2728 2728 self,
2729 2729 nodes,
2730 2730 nodesorder=None,
2731 2731 revisiondata=False,
2732 2732 assumehaveparentrevisions=False,
2733 2733 deltamode=repository.CG_DELTAMODE_STD,
2734 2734 ):
2735 2735 if nodesorder not in (b'nodes', b'storage', b'linear', None):
2736 2736 raise error.ProgrammingError(
2737 2737 b'unhandled value for nodesorder: %s' % nodesorder
2738 2738 )
2739 2739
2740 2740 if nodesorder is None and not self._generaldelta:
2741 2741 nodesorder = b'storage'
2742 2742
2743 2743 if (
2744 2744 not self._storedeltachains
2745 2745 and deltamode != repository.CG_DELTAMODE_PREV
2746 2746 ):
2747 2747 deltamode = repository.CG_DELTAMODE_FULL
2748 2748
2749 2749 return storageutil.emitrevisions(
2750 2750 self,
2751 2751 nodes,
2752 2752 nodesorder,
2753 2753 revlogrevisiondelta,
2754 2754 deltaparentfn=self.deltaparent,
2755 2755 candeltafn=self.candelta,
2756 2756 rawsizefn=self.rawsize,
2757 2757 revdifffn=self.revdiff,
2758 2758 flagsfn=self.flags,
2759 2759 deltamode=deltamode,
2760 2760 revisiondata=revisiondata,
2761 2761 assumehaveparentrevisions=assumehaveparentrevisions,
2762 2762 )
2763 2763
2764 2764 DELTAREUSEALWAYS = b'always'
2765 2765 DELTAREUSESAMEREVS = b'samerevs'
2766 2766 DELTAREUSENEVER = b'never'
2767 2767
2768 2768 DELTAREUSEFULLADD = b'fulladd'
2769 2769
2770 2770 DELTAREUSEALL = {b'always', b'samerevs', b'never', b'fulladd'}
2771 2771
2772 2772 def clone(
2773 2773 self,
2774 2774 tr,
2775 2775 destrevlog,
2776 2776 addrevisioncb=None,
2777 2777 deltareuse=DELTAREUSESAMEREVS,
2778 2778 forcedeltabothparents=None,
2779 2779 sidedatacompanion=None,
2780 2780 ):
2781 2781 """Copy this revlog to another, possibly with format changes.
2782 2782
2783 2783 The destination revlog will contain the same revisions and nodes.
2784 2784 However, it may not be bit-for-bit identical due to e.g. delta encoding
2785 2785 differences.
2786 2786
2787 2787 The ``deltareuse`` argument control how deltas from the existing revlog
2788 2788 are preserved in the destination revlog. The argument can have the
2789 2789 following values:
2790 2790
2791 2791 DELTAREUSEALWAYS
2792 2792 Deltas will always be reused (if possible), even if the destination
2793 2793 revlog would not select the same revisions for the delta. This is the
2794 2794 fastest mode of operation.
2795 2795 DELTAREUSESAMEREVS
2796 2796 Deltas will be reused if the destination revlog would pick the same
2797 2797 revisions for the delta. This mode strikes a balance between speed
2798 2798 and optimization.
2799 2799 DELTAREUSENEVER
2800 2800 Deltas will never be reused. This is the slowest mode of execution.
2801 2801 This mode can be used to recompute deltas (e.g. if the diff/delta
2802 2802 algorithm changes).
2803 2803 DELTAREUSEFULLADD
2804 2804 Revision will be re-added as if their were new content. This is
2805 2805 slower than DELTAREUSEALWAYS but allow more mechanism to kicks in.
2806 2806 eg: large file detection and handling.
2807 2807
2808 2808 Delta computation can be slow, so the choice of delta reuse policy can
2809 2809 significantly affect run time.
2810 2810
2811 2811 The default policy (``DELTAREUSESAMEREVS``) strikes a balance between
2812 2812 two extremes. Deltas will be reused if they are appropriate. But if the
2813 2813 delta could choose a better revision, it will do so. This means if you
2814 2814 are converting a non-generaldelta revlog to a generaldelta revlog,
2815 2815 deltas will be recomputed if the delta's parent isn't a parent of the
2816 2816 revision.
2817 2817
2818 2818 In addition to the delta policy, the ``forcedeltabothparents``
2819 2819 argument controls whether to force compute deltas against both parents
2820 2820 for merges. By default, the current default is used.
2821 2821
2822 2822 If not None, the `sidedatacompanion` is callable that accept two
2823 2823 arguments:
2824 2824
2825 2825 (srcrevlog, rev)
2826 2826
2827 2827 and return a quintet that control changes to sidedata content from the
2828 2828 old revision to the new clone result:
2829 2829
2830 2830 (dropall, filterout, update, new_flags, dropped_flags)
2831 2831
2832 2832 * if `dropall` is True, all sidedata should be dropped
2833 2833 * `filterout` is a set of sidedata keys that should be dropped
2834 2834 * `update` is a mapping of additionnal/new key -> value
2835 2835 * new_flags is a bitfields of new flags that the revision should get
2836 2836 * dropped_flags is a bitfields of new flags that the revision shoudl not longer have
2837 2837 """
2838 2838 if deltareuse not in self.DELTAREUSEALL:
2839 2839 raise ValueError(
2840 2840 _(b'value for deltareuse invalid: %s') % deltareuse
2841 2841 )
2842 2842
2843 2843 if len(destrevlog):
2844 2844 raise ValueError(_(b'destination revlog is not empty'))
2845 2845
2846 2846 if getattr(self, 'filteredrevs', None):
2847 2847 raise ValueError(_(b'source revlog has filtered revisions'))
2848 2848 if getattr(destrevlog, 'filteredrevs', None):
2849 2849 raise ValueError(_(b'destination revlog has filtered revisions'))
2850 2850
2851 2851 # lazydelta and lazydeltabase controls whether to reuse a cached delta,
2852 2852 # if possible.
2853 2853 oldlazydelta = destrevlog._lazydelta
2854 2854 oldlazydeltabase = destrevlog._lazydeltabase
2855 2855 oldamd = destrevlog._deltabothparents
2856 2856
2857 2857 try:
2858 2858 if deltareuse == self.DELTAREUSEALWAYS:
2859 2859 destrevlog._lazydeltabase = True
2860 2860 destrevlog._lazydelta = True
2861 2861 elif deltareuse == self.DELTAREUSESAMEREVS:
2862 2862 destrevlog._lazydeltabase = False
2863 2863 destrevlog._lazydelta = True
2864 2864 elif deltareuse == self.DELTAREUSENEVER:
2865 2865 destrevlog._lazydeltabase = False
2866 2866 destrevlog._lazydelta = False
2867 2867
2868 2868 destrevlog._deltabothparents = forcedeltabothparents or oldamd
2869 2869
2870 2870 self._clone(
2871 2871 tr,
2872 2872 destrevlog,
2873 2873 addrevisioncb,
2874 2874 deltareuse,
2875 2875 forcedeltabothparents,
2876 2876 sidedatacompanion,
2877 2877 )
2878 2878
2879 2879 finally:
2880 2880 destrevlog._lazydelta = oldlazydelta
2881 2881 destrevlog._lazydeltabase = oldlazydeltabase
2882 2882 destrevlog._deltabothparents = oldamd
2883 2883
2884 2884 def _clone(
2885 2885 self,
2886 2886 tr,
2887 2887 destrevlog,
2888 2888 addrevisioncb,
2889 2889 deltareuse,
2890 2890 forcedeltabothparents,
2891 2891 sidedatacompanion,
2892 2892 ):
2893 2893 """perform the core duty of `revlog.clone` after parameter processing"""
2894 2894 deltacomputer = deltautil.deltacomputer(destrevlog)
2895 2895 index = self.index
2896 2896 for rev in self:
2897 2897 entry = index[rev]
2898 2898
2899 2899 # Some classes override linkrev to take filtered revs into
2900 2900 # account. Use raw entry from index.
2901 2901 flags = entry[0] & 0xFFFF
2902 2902 linkrev = entry[4]
2903 2903 p1 = index[entry[5]][7]
2904 2904 p2 = index[entry[6]][7]
2905 2905 node = entry[7]
2906 2906
2907 2907 sidedataactions = (False, [], {}, 0, 0)
2908 2908 if sidedatacompanion is not None:
2909 2909 sidedataactions = sidedatacompanion(self, rev)
2910 2910
2911 2911 # (Possibly) reuse the delta from the revlog if allowed and
2912 2912 # the revlog chunk is a delta.
2913 2913 cachedelta = None
2914 2914 rawtext = None
2915 2915 if any(sidedataactions) or deltareuse == self.DELTAREUSEFULLADD:
2916 2916 dropall = sidedataactions[0]
2917 2917 filterout = sidedataactions[1]
2918 2918 update = sidedataactions[2]
2919 2919 new_flags = sidedataactions[3]
2920 2920 dropped_flags = sidedataactions[4]
2921 2921 text, sidedata = self._revisiondata(rev)
2922 2922 if dropall:
2923 2923 sidedata = {}
2924 2924 for key in filterout:
2925 2925 sidedata.pop(key, None)
2926 2926 sidedata.update(update)
2927 2927 if not sidedata:
2928 2928 sidedata = None
2929 2929
2930 2930 flags |= new_flags
2931 2931 flags &= ~dropped_flags
2932 2932
2933 2933 destrevlog.addrevision(
2934 2934 text,
2935 2935 tr,
2936 2936 linkrev,
2937 2937 p1,
2938 2938 p2,
2939 2939 cachedelta=cachedelta,
2940 2940 node=node,
2941 2941 flags=flags,
2942 2942 deltacomputer=deltacomputer,
2943 2943 sidedata=sidedata,
2944 2944 )
2945 2945 else:
2946 2946 if destrevlog._lazydelta:
2947 2947 dp = self.deltaparent(rev)
2948 2948 if dp != nullrev:
2949 2949 cachedelta = (dp, bytes(self._chunk(rev)))
2950 2950
2951 2951 if not cachedelta:
2952 2952 rawtext = self.rawdata(rev)
2953 2953
2954 2954 ifh = destrevlog.opener(
2955 2955 destrevlog.indexfile, b'a+', checkambig=False
2956 2956 )
2957 2957 dfh = None
2958 2958 if not destrevlog._inline:
2959 2959 dfh = destrevlog.opener(destrevlog.datafile, b'a+')
2960 2960 try:
2961 2961 destrevlog._addrevision(
2962 2962 node,
2963 2963 rawtext,
2964 2964 tr,
2965 2965 linkrev,
2966 2966 p1,
2967 2967 p2,
2968 2968 flags,
2969 2969 cachedelta,
2970 2970 ifh,
2971 2971 dfh,
2972 2972 deltacomputer=deltacomputer,
2973 2973 )
2974 2974 finally:
2975 2975 if dfh:
2976 2976 dfh.close()
2977 2977 ifh.close()
2978 2978
2979 2979 if addrevisioncb:
2980 2980 addrevisioncb(self, rev, node)
2981 2981
2982 2982 def censorrevision(self, tr, censornode, tombstone=b''):
2983 2983 if (self.version & 0xFFFF) == REVLOGV0:
2984 2984 raise error.RevlogError(
2985 2985 _(b'cannot censor with version %d revlogs') % self.version
2986 2986 )
2987 2987
2988 2988 censorrev = self.rev(censornode)
2989 2989 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
2990 2990
2991 2991 if len(tombstone) > self.rawsize(censorrev):
2992 2992 raise error.Abort(
2993 2993 _(b'censor tombstone must be no longer than censored data')
2994 2994 )
2995 2995
2996 2996 # Rewriting the revlog in place is hard. Our strategy for censoring is
2997 2997 # to create a new revlog, copy all revisions to it, then replace the
2998 2998 # revlogs on transaction close.
2999 2999
3000 3000 newindexfile = self.indexfile + b'.tmpcensored'
3001 3001 newdatafile = self.datafile + b'.tmpcensored'
3002 3002
3003 3003 # This is a bit dangerous. We could easily have a mismatch of state.
3004 3004 newrl = revlog(self.opener, newindexfile, newdatafile, censorable=True)
3005 3005 newrl.version = self.version
3006 3006 newrl._generaldelta = self._generaldelta
3007 3007 newrl._io = self._io
3008 3008
3009 3009 for rev in self.revs():
3010 3010 node = self.node(rev)
3011 3011 p1, p2 = self.parents(node)
3012 3012
3013 3013 if rev == censorrev:
3014 3014 newrl.addrawrevision(
3015 3015 tombstone,
3016 3016 tr,
3017 3017 self.linkrev(censorrev),
3018 3018 p1,
3019 3019 p2,
3020 3020 censornode,
3021 3021 REVIDX_ISCENSORED,
3022 3022 )
3023 3023
3024 3024 if newrl.deltaparent(rev) != nullrev:
3025 3025 raise error.Abort(
3026 3026 _(
3027 3027 b'censored revision stored as delta; '
3028 3028 b'cannot censor'
3029 3029 ),
3030 3030 hint=_(
3031 3031 b'censoring of revlogs is not '
3032 3032 b'fully implemented; please report '
3033 3033 b'this bug'
3034 3034 ),
3035 3035 )
3036 3036 continue
3037 3037
3038 3038 if self.iscensored(rev):
3039 3039 if self.deltaparent(rev) != nullrev:
3040 3040 raise error.Abort(
3041 3041 _(
3042 3042 b'cannot censor due to censored '
3043 3043 b'revision having delta stored'
3044 3044 )
3045 3045 )
3046 3046 rawtext = self._chunk(rev)
3047 3047 else:
3048 3048 rawtext = self.rawdata(rev)
3049 3049
3050 3050 newrl.addrawrevision(
3051 3051 rawtext, tr, self.linkrev(rev), p1, p2, node, self.flags(rev)
3052 3052 )
3053 3053
3054 3054 tr.addbackup(self.indexfile, location=b'store')
3055 3055 if not self._inline:
3056 3056 tr.addbackup(self.datafile, location=b'store')
3057 3057
3058 3058 self.opener.rename(newrl.indexfile, self.indexfile)
3059 3059 if not self._inline:
3060 3060 self.opener.rename(newrl.datafile, self.datafile)
3061 3061
3062 3062 self.clearcaches()
3063 3063 self._loadindex()
3064 3064
3065 3065 def verifyintegrity(self, state):
3066 3066 """Verifies the integrity of the revlog.
3067 3067
3068 3068 Yields ``revlogproblem`` instances describing problems that are
3069 3069 found.
3070 3070 """
3071 3071 dd, di = self.checksize()
3072 3072 if dd:
3073 3073 yield revlogproblem(error=_(b'data length off by %d bytes') % dd)
3074 3074 if di:
3075 3075 yield revlogproblem(error=_(b'index contains %d extra bytes') % di)
3076 3076
3077 3077 version = self.version & 0xFFFF
3078 3078
3079 3079 # The verifier tells us what version revlog we should be.
3080 3080 if version != state[b'expectedversion']:
3081 3081 yield revlogproblem(
3082 3082 warning=_(b"warning: '%s' uses revlog format %d; expected %d")
3083 3083 % (self.indexfile, version, state[b'expectedversion'])
3084 3084 )
3085 3085
3086 3086 state[b'skipread'] = set()
3087 3087 state[b'safe_renamed'] = set()
3088 3088
3089 3089 for rev in self:
3090 3090 node = self.node(rev)
3091 3091
3092 3092 # Verify contents. 4 cases to care about:
3093 3093 #
3094 3094 # common: the most common case
3095 3095 # rename: with a rename
3096 3096 # meta: file content starts with b'\1\n', the metadata
3097 3097 # header defined in filelog.py, but without a rename
3098 3098 # ext: content stored externally
3099 3099 #
3100 3100 # More formally, their differences are shown below:
3101 3101 #
3102 3102 # | common | rename | meta | ext
3103 3103 # -------------------------------------------------------
3104 3104 # flags() | 0 | 0 | 0 | not 0
3105 3105 # renamed() | False | True | False | ?
3106 3106 # rawtext[0:2]=='\1\n'| False | True | True | ?
3107 3107 #
3108 3108 # "rawtext" means the raw text stored in revlog data, which
3109 3109 # could be retrieved by "rawdata(rev)". "text"
3110 3110 # mentioned below is "revision(rev)".
3111 3111 #
3112 3112 # There are 3 different lengths stored physically:
3113 3113 # 1. L1: rawsize, stored in revlog index
3114 3114 # 2. L2: len(rawtext), stored in revlog data
3115 3115 # 3. L3: len(text), stored in revlog data if flags==0, or
3116 3116 # possibly somewhere else if flags!=0
3117 3117 #
3118 3118 # L1 should be equal to L2. L3 could be different from them.
3119 3119 # "text" may or may not affect commit hash depending on flag
3120 3120 # processors (see flagutil.addflagprocessor).
3121 3121 #
3122 3122 # | common | rename | meta | ext
3123 3123 # -------------------------------------------------
3124 3124 # rawsize() | L1 | L1 | L1 | L1
3125 3125 # size() | L1 | L2-LM | L1(*) | L1 (?)
3126 3126 # len(rawtext) | L2 | L2 | L2 | L2
3127 3127 # len(text) | L2 | L2 | L2 | L3
3128 3128 # len(read()) | L2 | L2-LM | L2-LM | L3 (?)
3129 3129 #
3130 3130 # LM: length of metadata, depending on rawtext
3131 3131 # (*): not ideal, see comment in filelog.size
3132 3132 # (?): could be "- len(meta)" if the resolved content has
3133 3133 # rename metadata
3134 3134 #
3135 3135 # Checks needed to be done:
3136 3136 # 1. length check: L1 == L2, in all cases.
3137 3137 # 2. hash check: depending on flag processor, we may need to
3138 3138 # use either "text" (external), or "rawtext" (in revlog).
3139 3139
3140 3140 try:
3141 3141 skipflags = state.get(b'skipflags', 0)
3142 3142 if skipflags:
3143 3143 skipflags &= self.flags(rev)
3144 3144
3145 3145 _verify_revision(self, skipflags, state, node)
3146 3146
3147 3147 l1 = self.rawsize(rev)
3148 3148 l2 = len(self.rawdata(node))
3149 3149
3150 3150 if l1 != l2:
3151 3151 yield revlogproblem(
3152 3152 error=_(b'unpacked size is %d, %d expected') % (l2, l1),
3153 3153 node=node,
3154 3154 )
3155 3155
3156 3156 except error.CensoredNodeError:
3157 3157 if state[b'erroroncensored']:
3158 3158 yield revlogproblem(
3159 3159 error=_(b'censored file data'), node=node
3160 3160 )
3161 3161 state[b'skipread'].add(node)
3162 3162 except Exception as e:
3163 3163 yield revlogproblem(
3164 3164 error=_(b'unpacking %s: %s')
3165 3165 % (short(node), stringutil.forcebytestr(e)),
3166 3166 node=node,
3167 3167 )
3168 3168 state[b'skipread'].add(node)
3169 3169
3170 3170 def storageinfo(
3171 3171 self,
3172 3172 exclusivefiles=False,
3173 3173 sharedfiles=False,
3174 3174 revisionscount=False,
3175 3175 trackedsize=False,
3176 3176 storedsize=False,
3177 3177 ):
3178 3178 d = {}
3179 3179
3180 3180 if exclusivefiles:
3181 3181 d[b'exclusivefiles'] = [(self.opener, self.indexfile)]
3182 3182 if not self._inline:
3183 3183 d[b'exclusivefiles'].append((self.opener, self.datafile))
3184 3184
3185 3185 if sharedfiles:
3186 3186 d[b'sharedfiles'] = []
3187 3187
3188 3188 if revisionscount:
3189 3189 d[b'revisionscount'] = len(self)
3190 3190
3191 3191 if trackedsize:
3192 3192 d[b'trackedsize'] = sum(map(self.rawsize, iter(self)))
3193 3193
3194 3194 if storedsize:
3195 3195 d[b'storedsize'] = sum(
3196 3196 self.opener.stat(path).st_size for path in self.files()
3197 3197 )
3198 3198
3199 3199 return d
@@ -1,287 +1,288 b''
1 1 # Test that certain objects conform to well-defined interfaces.
2 2
3 3 from __future__ import absolute_import, print_function
4 4
5 5 from mercurial import encoding
6 6
7 7 encoding.environ[b'HGREALINTERFACES'] = b'1'
8 8
9 9 import os
10 10 import subprocess
11 11 import sys
12 12
13 13 # Only run if tests are run in a repo
14 14 if subprocess.call(
15 15 [sys.executable, '%s/hghave' % os.environ['TESTDIR'], 'test-repo']
16 16 ):
17 17 sys.exit(80)
18 18
19 19 from mercurial.interfaces import (
20 20 dirstate as intdirstate,
21 21 repository,
22 22 )
23 23 from mercurial.thirdparty.zope import interface as zi
24 24 from mercurial.thirdparty.zope.interface import verify as ziverify
25 25 from mercurial import (
26 26 bundlerepo,
27 27 dirstate,
28 28 filelog,
29 29 httppeer,
30 30 localrepo,
31 31 manifest,
32 32 pycompat,
33 33 revlog,
34 34 sshpeer,
35 35 statichttprepo,
36 36 ui as uimod,
37 37 unionrepo,
38 38 vfs as vfsmod,
39 39 wireprotoserver,
40 40 wireprototypes,
41 41 wireprotov1peer,
42 42 wireprotov2server,
43 43 )
44 44
45 45 testdir = os.path.dirname(__file__)
46 46 rootdir = pycompat.fsencode(os.path.normpath(os.path.join(testdir, '..')))
47 47
48 48 sys.path[0:0] = [testdir]
49 49 import simplestorerepo
50 50
51 51 del sys.path[0]
52 52
53 53
54 54 def checkzobject(o, allowextra=False):
55 55 """Verify an object with a zope interface."""
56 56 ifaces = zi.providedBy(o)
57 57 if not ifaces:
58 58 print('%r does not provide any zope interfaces' % o)
59 59 return
60 60
61 61 # Run zope.interface's built-in verification routine. This verifies that
62 62 # everything that is supposed to be present is present.
63 63 for iface in ifaces:
64 64 ziverify.verifyObject(iface, o)
65 65
66 66 if allowextra:
67 67 return
68 68
69 69 # Now verify that the object provides no extra public attributes that
70 70 # aren't declared as part of interfaces.
71 71 allowed = set()
72 72 for iface in ifaces:
73 73 allowed |= set(iface.names(all=True))
74 74
75 75 public = {a for a in dir(o) if not a.startswith('_')}
76 76
77 77 for attr in sorted(public - allowed):
78 78 print(
79 79 'public attribute not declared in interfaces: %s.%s'
80 80 % (o.__class__.__name__, attr)
81 81 )
82 82
83 83
84 84 # Facilitates testing localpeer.
85 85 class dummyrepo(object):
86 86 def __init__(self):
87 87 self.ui = uimod.ui()
88 88
89 89 def filtered(self, name):
90 90 pass
91 91
92 92 def _restrictcapabilities(self, caps):
93 93 pass
94 94
95 95
96 96 class dummyopener(object):
97 97 handlers = []
98 98
99 99
100 100 # Facilitates testing sshpeer without requiring a server.
101 101 class badpeer(httppeer.httppeer):
102 102 def __init__(self):
103 103 super(badpeer, self).__init__(
104 104 None, None, None, dummyopener(), None, None
105 105 )
106 106 self.badattribute = True
107 107
108 108 def badmethod(self):
109 109 pass
110 110
111 111
112 112 class dummypipe(object):
113 113 def close(self):
114 114 pass
115 115
116 116 @property
117 117 def closed(self):
118 118 pass
119 119
120 120
121 121 def main():
122 122 ui = uimod.ui()
123 123 # Needed so we can open a local repo with obsstore without a warning.
124 124 ui.setconfig(b'experimental', b'evolution.createmarkers', True)
125 125
126 126 checkzobject(badpeer())
127 127
128 128 ziverify.verifyClass(repository.ipeerbase, httppeer.httppeer)
129 129 checkzobject(httppeer.httppeer(None, None, None, dummyopener(), None, None))
130 130
131 131 ziverify.verifyClass(repository.ipeerv2, httppeer.httpv2peer)
132 132 checkzobject(httppeer.httpv2peer(None, b'', b'', None, None, None))
133 133
134 134 ziverify.verifyClass(repository.ipeerbase, localrepo.localpeer)
135 135 checkzobject(localrepo.localpeer(dummyrepo()))
136 136
137 137 ziverify.verifyClass(
138 138 repository.ipeercommandexecutor, localrepo.localcommandexecutor
139 139 )
140 140 checkzobject(localrepo.localcommandexecutor(None))
141 141
142 142 ziverify.verifyClass(
143 143 repository.ipeercommandexecutor, wireprotov1peer.peerexecutor
144 144 )
145 145 checkzobject(wireprotov1peer.peerexecutor(None))
146 146
147 147 ziverify.verifyClass(repository.ipeerbase, sshpeer.sshv1peer)
148 148 checkzobject(
149 149 sshpeer.sshv1peer(
150 150 ui,
151 151 b'ssh://localhost/foo',
152 152 b'',
153 153 dummypipe(),
154 154 dummypipe(),
155 155 None,
156 156 None,
157 157 )
158 158 )
159 159
160 160 ziverify.verifyClass(repository.ipeerbase, sshpeer.sshv2peer)
161 161 checkzobject(
162 162 sshpeer.sshv2peer(
163 163 ui,
164 164 b'ssh://localhost/foo',
165 165 b'',
166 166 dummypipe(),
167 167 dummypipe(),
168 168 None,
169 169 None,
170 170 )
171 171 )
172 172
173 173 ziverify.verifyClass(repository.ipeerbase, bundlerepo.bundlepeer)
174 174 checkzobject(bundlerepo.bundlepeer(dummyrepo()))
175 175
176 176 ziverify.verifyClass(repository.ipeerbase, statichttprepo.statichttppeer)
177 177 checkzobject(statichttprepo.statichttppeer(dummyrepo()))
178 178
179 179 ziverify.verifyClass(repository.ipeerbase, unionrepo.unionpeer)
180 180 checkzobject(unionrepo.unionpeer(dummyrepo()))
181 181
182 182 ziverify.verifyClass(
183 183 repository.ilocalrepositorymain, localrepo.localrepository
184 184 )
185 185 ziverify.verifyClass(
186 186 repository.ilocalrepositoryfilestorage, localrepo.revlogfilestorage
187 187 )
188 188 repo = localrepo.makelocalrepository(ui, rootdir)
189 189 checkzobject(repo)
190 190
191 191 ziverify.verifyClass(
192 192 wireprototypes.baseprotocolhandler, wireprotoserver.sshv1protocolhandler
193 193 )
194 194 ziverify.verifyClass(
195 195 wireprototypes.baseprotocolhandler, wireprotoserver.sshv2protocolhandler
196 196 )
197 197 ziverify.verifyClass(
198 198 wireprototypes.baseprotocolhandler,
199 199 wireprotoserver.httpv1protocolhandler,
200 200 )
201 201 ziverify.verifyClass(
202 202 wireprototypes.baseprotocolhandler,
203 203 wireprotov2server.httpv2protocolhandler,
204 204 )
205 205
206 206 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
207 207 checkzobject(sshv1)
208 208 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
209 209 checkzobject(sshv2)
210 210
211 211 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
212 212 checkzobject(httpv1)
213 213 httpv2 = wireprotov2server.httpv2protocolhandler(None, None)
214 214 checkzobject(httpv2)
215 215
216 216 ziverify.verifyClass(repository.ifilestorage, filelog.filelog)
217 217 ziverify.verifyClass(repository.imanifestdict, manifest.manifestdict)
218 218 ziverify.verifyClass(repository.imanifestdict, manifest.treemanifest)
219 219 ziverify.verifyClass(
220 220 repository.imanifestrevisionstored, manifest.manifestctx
221 221 )
222 222 ziverify.verifyClass(
223 223 repository.imanifestrevisionwritable, manifest.memmanifestctx
224 224 )
225 225 ziverify.verifyClass(
226 226 repository.imanifestrevisionstored, manifest.treemanifestctx
227 227 )
228 228 ziverify.verifyClass(
229 229 repository.imanifestrevisionwritable, manifest.memtreemanifestctx
230 230 )
231 231 ziverify.verifyClass(repository.imanifestlog, manifest.manifestlog)
232 232 ziverify.verifyClass(repository.imanifeststorage, manifest.manifestrevlog)
233 233
234 234 ziverify.verifyClass(
235 235 repository.irevisiondelta, simplestorerepo.simplestorerevisiondelta
236 236 )
237 237 ziverify.verifyClass(repository.ifilestorage, simplestorerepo.filestorage)
238 238 ziverify.verifyClass(
239 239 repository.iverifyproblem, simplestorerepo.simplefilestoreproblem
240 240 )
241 241
242 242 ziverify.verifyClass(intdirstate.idirstate, dirstate.dirstate)
243 243
244 244 vfs = vfsmod.vfs(b'.')
245 245 fl = filelog.filelog(vfs, b'dummy.i')
246 246 checkzobject(fl, allowextra=True)
247 247
248 248 # Conforms to imanifestlog.
249 249 ml = manifest.manifestlog(
250 250 vfs, repo, manifest.manifestrevlog(repo.svfs), repo.narrowmatch()
251 251 )
252 252 checkzobject(ml)
253 253 checkzobject(repo.manifestlog)
254 254
255 255 # Conforms to imanifestrevision.
256 256 mctx = ml[repo[0].manifestnode()]
257 257 checkzobject(mctx)
258 258
259 259 # Conforms to imanifestrevisionwritable.
260 260 checkzobject(mctx.copy())
261 261
262 262 # Conforms to imanifestdict.
263 263 checkzobject(mctx.read())
264 264
265 265 mrl = manifest.manifestrevlog(vfs)
266 266 checkzobject(mrl)
267 267
268 268 ziverify.verifyClass(repository.irevisiondelta, revlog.revlogrevisiondelta)
269 269
270 270 rd = revlog.revlogrevisiondelta(
271 271 node=b'',
272 272 p1node=b'',
273 273 p2node=b'',
274 274 basenode=b'',
275 275 linknode=b'',
276 276 flags=b'',
277 277 baserevisionsize=None,
278 278 revision=b'',
279 sidedata=b'',
279 280 delta=None,
280 281 )
281 282 checkzobject(rd)
282 283
283 284 ziverify.verifyClass(repository.iverifyproblem, revlog.revlogproblem)
284 285 checkzobject(revlog.revlogproblem())
285 286
286 287
287 288 main()
@@ -1,502 +1,504 b''
1 1 # test revlog interaction about raw data (flagprocessor)
2 2
3 3 from __future__ import absolute_import, print_function
4 4
5 5 import collections
6 6 import hashlib
7 7 import sys
8 8
9 9 from mercurial.node import nullid
10 10 from mercurial import (
11 11 encoding,
12 12 revlog,
13 13 transaction,
14 14 vfs,
15 15 )
16 16
17 17 from mercurial.revlogutils import (
18 18 deltas,
19 19 flagutil,
20 20 )
21 21
22 22 # TESTTMP is optional. This makes it convenient to run without run-tests.py
23 23 tvfs = vfs.vfs(encoding.environ.get(b'TESTTMP', b'/tmp'))
24 24
25 25 # Enable generaldelta otherwise revlog won't use delta as expected by the test
26 26 tvfs.options = {
27 27 b'generaldelta': True,
28 28 b'revlogv1': True,
29 29 b'sparse-revlog': True,
30 30 }
31 31
32 32 # The test wants to control whether to use delta explicitly, based on
33 33 # "storedeltachains".
34 34 revlog.revlog._isgooddeltainfo = lambda self, d, textlen: self._storedeltachains
35 35
36 36
37 37 def abort(msg):
38 38 print('abort: %s' % msg)
39 39 # Return 0 so run-tests.py could compare the output.
40 40 sys.exit()
41 41
42 42
43 43 # Register a revlog processor for flag EXTSTORED.
44 44 #
45 45 # It simply prepends a fixed header, and replaces '1' to 'i'. So it has
46 46 # insertion and replacement, and may be interesting to test revlog's line-based
47 47 # deltas.
48 48 _extheader = b'E\n'
49 49
50 50
51 51 def readprocessor(self, rawtext):
52 52 # True: the returned text could be used to verify hash
53 53 text = rawtext[len(_extheader) :].replace(b'i', b'1')
54 54 return text, True
55 55
56 56
57 57 def writeprocessor(self, text):
58 58 # False: the returned rawtext shouldn't be used to verify hash
59 59 rawtext = _extheader + text.replace(b'1', b'i')
60 60 return rawtext, False
61 61
62 62
63 63 def rawprocessor(self, rawtext):
64 64 # False: do not verify hash. Only the content returned by "readprocessor"
65 65 # can be used to verify hash.
66 66 return False
67 67
68 68
69 69 flagutil.addflagprocessor(
70 70 revlog.REVIDX_EXTSTORED, (readprocessor, writeprocessor, rawprocessor)
71 71 )
72 72
73 73 # Utilities about reading and appending revlog
74 74
75 75
76 76 def newtransaction():
77 77 # A transaction is required to write revlogs
78 78 report = lambda msg: None
79 79 return transaction.transaction(report, tvfs, {'plain': tvfs}, b'journal')
80 80
81 81
82 82 def newrevlog(name=b'_testrevlog.i', recreate=False):
83 83 if recreate:
84 84 tvfs.tryunlink(name)
85 85 rlog = revlog.revlog(tvfs, name)
86 86 return rlog
87 87
88 88
89 89 def appendrev(rlog, text, tr, isext=False, isdelta=True):
90 90 """Append a revision. If isext is True, set the EXTSTORED flag so flag
91 91 processor will be used (and rawtext is different from text). If isdelta is
92 92 True, force the revision to be a delta, otherwise it's full text.
93 93 """
94 94 nextrev = len(rlog)
95 95 p1 = rlog.node(nextrev - 1)
96 96 p2 = nullid
97 97 if isext:
98 98 flags = revlog.REVIDX_EXTSTORED
99 99 else:
100 100 flags = revlog.REVIDX_DEFAULT_FLAGS
101 101 # Change storedeltachains temporarily, to override revlog's delta decision
102 102 rlog._storedeltachains = isdelta
103 103 try:
104 104 rlog.addrevision(text, tr, nextrev, p1, p2, flags=flags)
105 105 return nextrev
106 106 except Exception as ex:
107 107 abort('rev %d: failed to append: %s' % (nextrev, ex))
108 108 finally:
109 109 # Restore storedeltachains. It is always True, see revlog.__init__
110 110 rlog._storedeltachains = True
111 111
112 112
113 113 def addgroupcopy(rlog, tr, destname=b'_destrevlog.i', optimaldelta=True):
114 114 """Copy revlog to destname using revlog.addgroup. Return the copied revlog.
115 115
116 116 This emulates push or pull. They use changegroup. Changegroup requires
117 117 repo to work. We don't have a repo, so a dummy changegroup is used.
118 118
119 119 If optimaldelta is True, use optimized delta parent, so the destination
120 120 revlog could probably reuse it. Otherwise it builds sub-optimal delta, and
121 121 the destination revlog needs more work to use it.
122 122
123 123 This exercises some revlog.addgroup (and revlog._addrevision(text=None))
124 124 code path, which is not covered by "appendrev" alone.
125 125 """
126 126
127 127 class dummychangegroup(object):
128 128 @staticmethod
129 129 def deltachunk(pnode):
130 130 pnode = pnode or nullid
131 131 parentrev = rlog.rev(pnode)
132 132 r = parentrev + 1
133 133 if r >= len(rlog):
134 134 return {}
135 135 if optimaldelta:
136 136 deltaparent = parentrev
137 137 else:
138 138 # suboptimal deltaparent
139 139 deltaparent = min(0, parentrev)
140 140 if not rlog.candelta(deltaparent, r):
141 141 deltaparent = -1
142 142 return {
143 143 b'node': rlog.node(r),
144 144 b'p1': pnode,
145 145 b'p2': nullid,
146 146 b'cs': rlog.node(rlog.linkrev(r)),
147 147 b'flags': rlog.flags(r),
148 148 b'deltabase': rlog.node(deltaparent),
149 149 b'delta': rlog.revdiff(deltaparent, r),
150 b'sidedata': rlog.sidedata(r),
150 151 }
151 152
152 153 def deltaiter(self):
153 154 chain = None
154 155 for chunkdata in iter(lambda: self.deltachunk(chain), {}):
155 156 node = chunkdata[b'node']
156 157 p1 = chunkdata[b'p1']
157 158 p2 = chunkdata[b'p2']
158 159 cs = chunkdata[b'cs']
159 160 deltabase = chunkdata[b'deltabase']
160 161 delta = chunkdata[b'delta']
161 162 flags = chunkdata[b'flags']
163 sidedata = chunkdata[b'sidedata']
162 164
163 165 chain = node
164 166
165 yield (node, p1, p2, cs, deltabase, delta, flags)
167 yield (node, p1, p2, cs, deltabase, delta, flags, sidedata)
166 168
167 169 def linkmap(lnode):
168 170 return rlog.rev(lnode)
169 171
170 172 dlog = newrevlog(destname, recreate=True)
171 173 dummydeltas = dummychangegroup().deltaiter()
172 174 dlog.addgroup(dummydeltas, linkmap, tr)
173 175 return dlog
174 176
175 177
176 178 def lowlevelcopy(rlog, tr, destname=b'_destrevlog.i'):
177 179 """Like addgroupcopy, but use the low level revlog._addrevision directly.
178 180
179 181 It exercises some code paths that are hard to reach easily otherwise.
180 182 """
181 183 dlog = newrevlog(destname, recreate=True)
182 184 for r in rlog:
183 185 p1 = rlog.node(r - 1)
184 186 p2 = nullid
185 187 if r == 0 or (rlog.flags(r) & revlog.REVIDX_EXTSTORED):
186 188 text = rlog.rawdata(r)
187 189 cachedelta = None
188 190 else:
189 191 # deltaparent cannot have EXTSTORED flag.
190 192 deltaparent = max(
191 193 [-1]
192 194 + [
193 195 p
194 196 for p in range(r)
195 197 if rlog.flags(p) & revlog.REVIDX_EXTSTORED == 0
196 198 ]
197 199 )
198 200 text = None
199 201 cachedelta = (deltaparent, rlog.revdiff(deltaparent, r))
200 202 flags = rlog.flags(r)
201 203 ifh = dfh = None
202 204 try:
203 205 ifh = dlog.opener(dlog.indexfile, b'a+')
204 206 if not dlog._inline:
205 207 dfh = dlog.opener(dlog.datafile, b'a+')
206 208 dlog._addrevision(
207 209 rlog.node(r), text, tr, r, p1, p2, flags, cachedelta, ifh, dfh
208 210 )
209 211 finally:
210 212 if dfh is not None:
211 213 dfh.close()
212 214 if ifh is not None:
213 215 ifh.close()
214 216 return dlog
215 217
216 218
217 219 # Utilities to generate revisions for testing
218 220
219 221
220 222 def genbits(n):
221 223 """Given a number n, generate (2 ** (n * 2) + 1) numbers in range(2 ** n).
222 224 i.e. the generated numbers have a width of n bits.
223 225
224 226 The combination of two adjacent numbers will cover all possible cases.
225 227 That is to say, given any x, y where both x, and y are in range(2 ** n),
226 228 there is an x followed immediately by y in the generated sequence.
227 229 """
228 230 m = 2 ** n
229 231
230 232 # Gray Code. See https://en.wikipedia.org/wiki/Gray_code
231 233 gray = lambda x: x ^ (x >> 1)
232 234 reversegray = {gray(i): i for i in range(m)}
233 235
234 236 # Generate (n * 2) bit gray code, yield lower n bits as X, and look for
235 237 # the next unused gray code where higher n bits equal to X.
236 238
237 239 # For gray codes whose higher bits are X, a[X] of them have been used.
238 240 a = [0] * m
239 241
240 242 # Iterate from 0.
241 243 x = 0
242 244 yield x
243 245 for i in range(m * m):
244 246 x = reversegray[x]
245 247 y = gray(a[x] + x * m) & (m - 1)
246 248 assert a[x] < m
247 249 a[x] += 1
248 250 x = y
249 251 yield x
250 252
251 253
252 254 def gentext(rev):
253 255 '''Given a revision number, generate dummy text'''
254 256 return b''.join(b'%d\n' % j for j in range(-1, rev % 5))
255 257
256 258
257 259 def writecases(rlog, tr):
258 260 """Write some revisions interested to the test.
259 261
260 262 The test is interested in 3 properties of a revision:
261 263
262 264 - Is it a delta or a full text? (isdelta)
263 265 This is to catch some delta application issues.
264 266 - Does it have a flag of EXTSTORED? (isext)
265 267 This is to catch some flag processor issues. Especially when
266 268 interacted with revlog deltas.
267 269 - Is its text empty? (isempty)
268 270 This is less important. It is intended to try to catch some careless
269 271 checks like "if text" instead of "if text is None". Note: if flag
270 272 processor is involved, raw text may be not empty.
271 273
272 274 Write 65 revisions. So that all combinations of the above flags for
273 275 adjacent revisions are covered. That is to say,
274 276
275 277 len(set(
276 278 (r.delta, r.ext, r.empty, (r+1).delta, (r+1).ext, (r+1).empty)
277 279 for r in range(len(rlog) - 1)
278 280 )) is 64.
279 281
280 282 Where "r.delta", "r.ext", and "r.empty" are booleans matching properties
281 283 mentioned above.
282 284
283 285 Return expected [(text, rawtext)].
284 286 """
285 287 result = []
286 288 for i, x in enumerate(genbits(3)):
287 289 isdelta, isext, isempty = bool(x & 1), bool(x & 2), bool(x & 4)
288 290 if isempty:
289 291 text = b''
290 292 else:
291 293 text = gentext(i)
292 294 rev = appendrev(rlog, text, tr, isext=isext, isdelta=isdelta)
293 295
294 296 # Verify text, rawtext, and rawsize
295 297 if isext:
296 298 rawtext = writeprocessor(None, text)[0]
297 299 else:
298 300 rawtext = text
299 301 if rlog.rawsize(rev) != len(rawtext):
300 302 abort('rev %d: wrong rawsize' % rev)
301 303 if rlog.revision(rev, raw=False) != text:
302 304 abort('rev %d: wrong text' % rev)
303 305 if rlog.rawdata(rev) != rawtext:
304 306 abort('rev %d: wrong rawtext' % rev)
305 307 result.append((text, rawtext))
306 308
307 309 # Verify flags like isdelta, isext work as expected
308 310 # isdelta can be overridden to False if this or p1 has isext set
309 311 if bool(rlog.deltaparent(rev) > -1) and not isdelta:
310 312 abort('rev %d: isdelta is unexpected' % rev)
311 313 if bool(rlog.flags(rev)) != isext:
312 314 abort('rev %d: isext is ineffective' % rev)
313 315 return result
314 316
315 317
316 318 # Main test and checking
317 319
318 320
319 321 def checkrevlog(rlog, expected):
320 322 '''Check if revlog has expected contents. expected is [(text, rawtext)]'''
321 323 # Test using different access orders. This could expose some issues
322 324 # depending on revlog caching (see revlog._cache).
323 325 for r0 in range(len(rlog) - 1):
324 326 r1 = r0 + 1
325 327 for revorder in [[r0, r1], [r1, r0]]:
326 328 for raworder in [[True], [False], [True, False], [False, True]]:
327 329 nlog = newrevlog()
328 330 for rev in revorder:
329 331 for raw in raworder:
330 332 if raw:
331 333 t = nlog.rawdata(rev)
332 334 else:
333 335 t = nlog.revision(rev)
334 336 if t != expected[rev][int(raw)]:
335 337 abort(
336 338 'rev %d: corrupted %stext'
337 339 % (rev, raw and 'raw' or '')
338 340 )
339 341
340 342
341 343 slicingdata = [
342 344 ([0, 1, 2, 3, 55, 56, 58, 59, 60], [[0, 1], [2], [58], [59, 60]], 10),
343 345 ([0, 1, 2, 3, 55, 56, 58, 59, 60], [[0, 1], [2], [58], [59, 60]], 10),
344 346 (
345 347 [-1, 0, 1, 2, 3, 55, 56, 58, 59, 60],
346 348 [[-1, 0, 1], [2], [58], [59, 60]],
347 349 10,
348 350 ),
349 351 ]
350 352
351 353
352 354 def slicingtest(rlog):
353 355 oldmin = rlog._srmingapsize
354 356 try:
355 357 # the test revlog is small, we remove the floor under which we
356 358 # slicing is diregarded.
357 359 rlog._srmingapsize = 0
358 360 for item in slicingdata:
359 361 chain, expected, target = item
360 362 result = deltas.slicechunk(rlog, chain, targetsize=target)
361 363 result = list(result)
362 364 if result != expected:
363 365 print('slicing differ:')
364 366 print(' chain: %s' % chain)
365 367 print(' target: %s' % target)
366 368 print(' expected: %s' % expected)
367 369 print(' result: %s' % result)
368 370 finally:
369 371 rlog._srmingapsize = oldmin
370 372
371 373
372 374 def md5sum(s):
373 375 return hashlib.md5(s).digest()
374 376
375 377
376 378 def _maketext(*coord):
377 379 """create piece of text according to range of integers
378 380
379 381 The test returned use a md5sum of the integer to make it less
380 382 compressible"""
381 383 pieces = []
382 384 for start, size in coord:
383 385 num = range(start, start + size)
384 386 p = [md5sum(b'%d' % r) for r in num]
385 387 pieces.append(b'\n'.join(p))
386 388 return b'\n'.join(pieces) + b'\n'
387 389
388 390
389 391 data = [
390 392 _maketext((0, 120), (456, 60)),
391 393 _maketext((0, 120), (345, 60)),
392 394 _maketext((0, 120), (734, 60)),
393 395 _maketext((0, 120), (734, 60), (923, 45)),
394 396 _maketext((0, 120), (734, 60), (234, 45)),
395 397 _maketext((0, 120), (734, 60), (564, 45)),
396 398 _maketext((0, 120), (734, 60), (361, 45)),
397 399 _maketext((0, 120), (734, 60), (489, 45)),
398 400 _maketext((0, 120), (123, 60)),
399 401 _maketext((0, 120), (145, 60)),
400 402 _maketext((0, 120), (104, 60)),
401 403 _maketext((0, 120), (430, 60)),
402 404 _maketext((0, 120), (430, 60), (923, 45)),
403 405 _maketext((0, 120), (430, 60), (234, 45)),
404 406 _maketext((0, 120), (430, 60), (564, 45)),
405 407 _maketext((0, 120), (430, 60), (361, 45)),
406 408 _maketext((0, 120), (430, 60), (489, 45)),
407 409 _maketext((0, 120), (249, 60)),
408 410 _maketext((0, 120), (832, 60)),
409 411 _maketext((0, 120), (891, 60)),
410 412 _maketext((0, 120), (543, 60)),
411 413 _maketext((0, 120), (120, 60)),
412 414 _maketext((0, 120), (60, 60), (768, 30)),
413 415 _maketext((0, 120), (60, 60), (260, 30)),
414 416 _maketext((0, 120), (60, 60), (450, 30)),
415 417 _maketext((0, 120), (60, 60), (361, 30)),
416 418 _maketext((0, 120), (60, 60), (886, 30)),
417 419 _maketext((0, 120), (60, 60), (116, 30)),
418 420 _maketext((0, 120), (60, 60), (567, 30), (629, 40)),
419 421 _maketext((0, 120), (60, 60), (569, 30), (745, 40)),
420 422 _maketext((0, 120), (60, 60), (777, 30), (700, 40)),
421 423 _maketext((0, 120), (60, 60), (618, 30), (398, 40), (158, 10)),
422 424 ]
423 425
424 426
425 427 def makesnapshot(tr):
426 428 rl = newrevlog(name=b'_snaprevlog3.i', recreate=True)
427 429 for i in data:
428 430 appendrev(rl, i, tr)
429 431 return rl
430 432
431 433
432 434 snapshots = [-1, 0, 6, 8, 11, 17, 19, 21, 25, 30]
433 435
434 436
435 437 def issnapshottest(rlog):
436 438 result = []
437 439 if rlog.issnapshot(-1):
438 440 result.append(-1)
439 441 for rev in rlog:
440 442 if rlog.issnapshot(rev):
441 443 result.append(rev)
442 444 if snapshots != result:
443 445 print('snapshot differ:')
444 446 print(' expected: %s' % snapshots)
445 447 print(' got: %s' % result)
446 448
447 449
448 450 snapshotmapall = {0: [6, 8, 11, 17, 19, 25], 8: [21], -1: [0, 30]}
449 451 snapshotmap15 = {0: [17, 19, 25], 8: [21], -1: [30]}
450 452
451 453
452 454 def findsnapshottest(rlog):
453 455 resultall = collections.defaultdict(list)
454 456 deltas._findsnapshots(rlog, resultall, 0)
455 457 resultall = dict(resultall.items())
456 458 if resultall != snapshotmapall:
457 459 print('snapshot map differ:')
458 460 print(' expected: %s' % snapshotmapall)
459 461 print(' got: %s' % resultall)
460 462 result15 = collections.defaultdict(list)
461 463 deltas._findsnapshots(rlog, result15, 15)
462 464 result15 = dict(result15.items())
463 465 if result15 != snapshotmap15:
464 466 print('snapshot map differ:')
465 467 print(' expected: %s' % snapshotmap15)
466 468 print(' got: %s' % result15)
467 469
468 470
469 471 def maintest():
470 472 with newtransaction() as tr:
471 473 rl = newrevlog(recreate=True)
472 474 expected = writecases(rl, tr)
473 475 checkrevlog(rl, expected)
474 476 print('local test passed')
475 477 # Copy via revlog.addgroup
476 478 rl1 = addgroupcopy(rl, tr)
477 479 checkrevlog(rl1, expected)
478 480 rl2 = addgroupcopy(rl, tr, optimaldelta=False)
479 481 checkrevlog(rl2, expected)
480 482 print('addgroupcopy test passed')
481 483 # Copy via revlog.clone
482 484 rl3 = newrevlog(name=b'_destrevlog3.i', recreate=True)
483 485 rl.clone(tr, rl3)
484 486 checkrevlog(rl3, expected)
485 487 print('clone test passed')
486 488 # Copy via low-level revlog._addrevision
487 489 rl4 = lowlevelcopy(rl, tr)
488 490 checkrevlog(rl4, expected)
489 491 print('lowlevelcopy test passed')
490 492 slicingtest(rl)
491 493 print('slicing test passed')
492 494 rl5 = makesnapshot(tr)
493 495 issnapshottest(rl5)
494 496 print('issnapshot test passed')
495 497 findsnapshottest(rl5)
496 498 print('findsnapshot test passed')
497 499
498 500
499 501 try:
500 502 maintest()
501 503 except Exception as ex:
502 504 abort('crashed: %s' % ex)
General Comments 0
You need to be logged in to leave comments. Login now