##// END OF EJS Templates
i18n: translate abort messages...
liscju -
r29389:98e8313d default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,542 +1,542 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 import tempfile
19 19
20 20 from .i18n import _
21 21 from .node import nullid
22 22
23 23 from . import (
24 24 bundle2,
25 25 changegroup,
26 26 changelog,
27 27 cmdutil,
28 28 discovery,
29 29 error,
30 30 exchange,
31 31 filelog,
32 32 localrepo,
33 33 manifest,
34 34 mdiff,
35 35 node as nodemod,
36 36 pathutil,
37 37 phases,
38 38 revlog,
39 39 scmutil,
40 40 util,
41 41 )
42 42
43 43 class bundlerevlog(revlog.revlog):
44 44 def __init__(self, opener, indexfile, bundle, linkmapper):
45 45 # How it works:
46 46 # To retrieve a revision, we need to know the offset of the revision in
47 47 # the bundle (an unbundle object). We store this offset in the index
48 48 # (start). The base of the delta is stored in the base field.
49 49 #
50 50 # To differentiate a rev in the bundle from a rev in the revlog, we
51 51 # check revision against repotiprev.
52 52 opener = scmutil.readonlyvfs(opener)
53 53 revlog.revlog.__init__(self, opener, indexfile)
54 54 self.bundle = bundle
55 55 n = len(self)
56 56 self.repotiprev = n - 1
57 57 chain = None
58 58 self.bundlerevs = set() # used by 'bundle()' revset expression
59 59 while True:
60 60 chunkdata = bundle.deltachunk(chain)
61 61 if not chunkdata:
62 62 break
63 63 node = chunkdata['node']
64 64 p1 = chunkdata['p1']
65 65 p2 = chunkdata['p2']
66 66 cs = chunkdata['cs']
67 67 deltabase = chunkdata['deltabase']
68 68 delta = chunkdata['delta']
69 69
70 70 size = len(delta)
71 71 start = bundle.tell() - size
72 72
73 73 link = linkmapper(cs)
74 74 if node in self.nodemap:
75 75 # this can happen if two branches make the same change
76 76 chain = node
77 77 self.bundlerevs.add(self.nodemap[node])
78 78 continue
79 79
80 80 for p in (p1, p2):
81 81 if p not in self.nodemap:
82 82 raise error.LookupError(p, self.indexfile,
83 83 _("unknown parent"))
84 84
85 85 if deltabase not in self.nodemap:
86 86 raise LookupError(deltabase, self.indexfile,
87 87 _('unknown delta base'))
88 88
89 89 baserev = self.rev(deltabase)
90 90 # start, size, full unc. size, base (unused), link, p1, p2, node
91 91 e = (revlog.offset_type(start, 0), size, -1, baserev, link,
92 92 self.rev(p1), self.rev(p2), node)
93 93 self.index.insert(-1, e)
94 94 self.nodemap[node] = n
95 95 self.bundlerevs.add(n)
96 96 chain = node
97 97 n += 1
98 98
99 99 def _chunk(self, rev):
100 100 # Warning: in case of bundle, the diff is against what we stored as
101 101 # delta base, not against rev - 1
102 102 # XXX: could use some caching
103 103 if rev <= self.repotiprev:
104 104 return revlog.revlog._chunk(self, rev)
105 105 self.bundle.seek(self.start(rev))
106 106 return self.bundle.read(self.length(rev))
107 107
108 108 def revdiff(self, rev1, rev2):
109 109 """return or calculate a delta between two revisions"""
110 110 if rev1 > self.repotiprev and rev2 > self.repotiprev:
111 111 # hot path for bundle
112 112 revb = self.index[rev2][3]
113 113 if revb == rev1:
114 114 return self._chunk(rev2)
115 115 elif rev1 <= self.repotiprev and rev2 <= self.repotiprev:
116 116 return revlog.revlog.revdiff(self, rev1, rev2)
117 117
118 118 return mdiff.textdiff(self.revision(self.node(rev1)),
119 119 self.revision(self.node(rev2)))
120 120
121 121 def revision(self, nodeorrev):
122 122 """return an uncompressed revision of a given node or revision
123 123 number.
124 124 """
125 125 if isinstance(nodeorrev, int):
126 126 rev = nodeorrev
127 127 node = self.node(rev)
128 128 else:
129 129 node = nodeorrev
130 130 rev = self.rev(node)
131 131
132 132 if node == nullid:
133 133 return ""
134 134
135 135 text = None
136 136 chain = []
137 137 iterrev = rev
138 138 # reconstruct the revision if it is from a changegroup
139 139 while iterrev > self.repotiprev:
140 140 if self._cache and self._cache[1] == iterrev:
141 141 text = self._cache[2]
142 142 break
143 143 chain.append(iterrev)
144 144 iterrev = self.index[iterrev][3]
145 145 if text is None:
146 146 text = self.baserevision(iterrev)
147 147
148 148 while chain:
149 149 delta = self._chunk(chain.pop())
150 150 text = mdiff.patches(text, [delta])
151 151
152 152 self._checkhash(text, node, rev)
153 153 self._cache = (node, rev, text)
154 154 return text
155 155
156 156 def baserevision(self, nodeorrev):
157 157 # Revlog subclasses may override 'revision' method to modify format of
158 158 # content retrieved from revlog. To use bundlerevlog with such class one
159 159 # needs to override 'baserevision' and make more specific call here.
160 160 return revlog.revlog.revision(self, nodeorrev)
161 161
162 162 def addrevision(self, text, transaction, link, p1=None, p2=None, d=None):
163 163 raise NotImplementedError
164 164 def addgroup(self, revs, linkmapper, transaction):
165 165 raise NotImplementedError
166 166 def strip(self, rev, minlink):
167 167 raise NotImplementedError
168 168 def checksize(self):
169 169 raise NotImplementedError
170 170
171 171 class bundlechangelog(bundlerevlog, changelog.changelog):
172 172 def __init__(self, opener, bundle):
173 173 changelog.changelog.__init__(self, opener)
174 174 linkmapper = lambda x: x
175 175 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
176 176 linkmapper)
177 177
178 178 def baserevision(self, nodeorrev):
179 179 # Although changelog doesn't override 'revision' method, some extensions
180 180 # may replace this class with another that does. Same story with
181 181 # manifest and filelog classes.
182 182
183 183 # This bypasses filtering on changelog.node() and rev() because we need
184 184 # revision text of the bundle base even if it is hidden.
185 185 oldfilter = self.filteredrevs
186 186 try:
187 187 self.filteredrevs = ()
188 188 return changelog.changelog.revision(self, nodeorrev)
189 189 finally:
190 190 self.filteredrevs = oldfilter
191 191
192 192 class bundlemanifest(bundlerevlog, manifest.manifest):
193 193 def __init__(self, opener, bundle, linkmapper):
194 194 manifest.manifest.__init__(self, opener)
195 195 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
196 196 linkmapper)
197 197
198 198 def baserevision(self, nodeorrev):
199 199 node = nodeorrev
200 200 if isinstance(node, int):
201 201 node = self.node(node)
202 202
203 203 if node in self._mancache:
204 204 result = self._mancache[node][0].text()
205 205 else:
206 206 result = manifest.manifest.revision(self, nodeorrev)
207 207 return result
208 208
209 209 class bundlefilelog(bundlerevlog, filelog.filelog):
210 210 def __init__(self, opener, path, bundle, linkmapper):
211 211 filelog.filelog.__init__(self, opener, path)
212 212 bundlerevlog.__init__(self, opener, self.indexfile, bundle,
213 213 linkmapper)
214 214
215 215 def baserevision(self, nodeorrev):
216 216 return filelog.filelog.revision(self, nodeorrev)
217 217
218 218 class bundlepeer(localrepo.localpeer):
219 219 def canpush(self):
220 220 return False
221 221
222 222 class bundlephasecache(phases.phasecache):
223 223 def __init__(self, *args, **kwargs):
224 224 super(bundlephasecache, self).__init__(*args, **kwargs)
225 225 if util.safehasattr(self, 'opener'):
226 226 self.opener = scmutil.readonlyvfs(self.opener)
227 227
228 228 def write(self):
229 229 raise NotImplementedError
230 230
231 231 def _write(self, fp):
232 232 raise NotImplementedError
233 233
234 234 def _updateroots(self, phase, newroots, tr):
235 235 self.phaseroots[phase] = newroots
236 236 self.invalidate()
237 237 self.dirty = True
238 238
239 239 class bundlerepository(localrepo.localrepository):
240 240 def __init__(self, ui, path, bundlename):
241 241 def _writetempbundle(read, suffix, header=''):
242 242 """Write a temporary file to disk
243 243
244 244 This is closure because we need to make sure this tracked by
245 245 self.tempfile for cleanup purposes."""
246 246 fdtemp, temp = self.vfs.mkstemp(prefix="hg-bundle-",
247 247 suffix=".hg10un")
248 248 self.tempfile = temp
249 249
250 250 with os.fdopen(fdtemp, 'wb') as fptemp:
251 251 fptemp.write(header)
252 252 while True:
253 253 chunk = read(2**18)
254 254 if not chunk:
255 255 break
256 256 fptemp.write(chunk)
257 257
258 258 return self.vfs.open(self.tempfile, mode="rb")
259 259 self._tempparent = None
260 260 try:
261 261 localrepo.localrepository.__init__(self, ui, path)
262 262 except error.RepoError:
263 263 self._tempparent = tempfile.mkdtemp()
264 264 localrepo.instance(ui, self._tempparent, 1)
265 265 localrepo.localrepository.__init__(self, ui, self._tempparent)
266 266 self.ui.setconfig('phases', 'publish', False, 'bundlerepo')
267 267
268 268 if path:
269 269 self._url = 'bundle:' + util.expandpath(path) + '+' + bundlename
270 270 else:
271 271 self._url = 'bundle:' + bundlename
272 272
273 273 self.tempfile = None
274 274 f = util.posixfile(bundlename, "rb")
275 275 self.bundlefile = self.bundle = exchange.readbundle(ui, f, bundlename)
276 276
277 277 if isinstance(self.bundle, bundle2.unbundle20):
278 278 cgstream = None
279 279 for part in self.bundle.iterparts():
280 280 if part.type == 'changegroup':
281 281 if cgstream is not None:
282 282 raise NotImplementedError("can't process "
283 283 "multiple changegroups")
284 284 cgstream = part
285 285 version = part.params.get('version', '01')
286 286 if version not in changegroup.allsupportedversions(ui):
287 287 msg = _('Unsupported changegroup version: %s')
288 288 raise error.Abort(msg % version)
289 289 if self.bundle.compressed():
290 290 cgstream = _writetempbundle(part.read,
291 291 ".cg%sun" % version)
292 292
293 293 if cgstream is None:
294 raise error.Abort('No changegroups found')
294 raise error.Abort(_('No changegroups found'))
295 295 cgstream.seek(0)
296 296
297 297 self.bundle = changegroup.getunbundler(version, cgstream, 'UN')
298 298
299 299 elif self.bundle.compressed():
300 300 f = _writetempbundle(self.bundle.read, '.hg10un', header='HG10UN')
301 301 self.bundlefile = self.bundle = exchange.readbundle(ui, f,
302 302 bundlename,
303 303 self.vfs)
304 304
305 305 # dict with the mapping 'filename' -> position in the bundle
306 306 self.bundlefilespos = {}
307 307
308 308 self.firstnewrev = self.changelog.repotiprev + 1
309 309 phases.retractboundary(self, None, phases.draft,
310 310 [ctx.node() for ctx in self[self.firstnewrev:]])
311 311
312 312 @localrepo.unfilteredpropertycache
313 313 def _phasecache(self):
314 314 return bundlephasecache(self, self._phasedefaults)
315 315
316 316 @localrepo.unfilteredpropertycache
317 317 def changelog(self):
318 318 # consume the header if it exists
319 319 self.bundle.changelogheader()
320 320 c = bundlechangelog(self.svfs, self.bundle)
321 321 self.manstart = self.bundle.tell()
322 322 return c
323 323
324 324 @localrepo.unfilteredpropertycache
325 325 def manifest(self):
326 326 self.bundle.seek(self.manstart)
327 327 # consume the header if it exists
328 328 self.bundle.manifestheader()
329 329 linkmapper = self.unfiltered().changelog.rev
330 330 m = bundlemanifest(self.svfs, self.bundle, linkmapper)
331 331 # XXX: hack to work with changegroup3, but we still don't handle
332 332 # tree manifests correctly
333 333 if self.bundle.version == "03":
334 334 self.bundle.filelogheader()
335 335 self.filestart = self.bundle.tell()
336 336 return m
337 337
338 338 @localrepo.unfilteredpropertycache
339 339 def manstart(self):
340 340 self.changelog
341 341 return self.manstart
342 342
343 343 @localrepo.unfilteredpropertycache
344 344 def filestart(self):
345 345 self.manifest
346 346 return self.filestart
347 347
348 348 def url(self):
349 349 return self._url
350 350
351 351 def file(self, f):
352 352 if not self.bundlefilespos:
353 353 self.bundle.seek(self.filestart)
354 354 while True:
355 355 chunkdata = self.bundle.filelogheader()
356 356 if not chunkdata:
357 357 break
358 358 fname = chunkdata['filename']
359 359 self.bundlefilespos[fname] = self.bundle.tell()
360 360 while True:
361 361 c = self.bundle.deltachunk(None)
362 362 if not c:
363 363 break
364 364
365 365 if f in self.bundlefilespos:
366 366 self.bundle.seek(self.bundlefilespos[f])
367 367 linkmapper = self.unfiltered().changelog.rev
368 368 return bundlefilelog(self.svfs, f, self.bundle, linkmapper)
369 369 else:
370 370 return filelog.filelog(self.svfs, f)
371 371
372 372 def close(self):
373 373 """Close assigned bundle file immediately."""
374 374 self.bundlefile.close()
375 375 if self.tempfile is not None:
376 376 self.vfs.unlink(self.tempfile)
377 377 if self._tempparent:
378 378 shutil.rmtree(self._tempparent, True)
379 379
380 380 def cancopy(self):
381 381 return False
382 382
383 383 def peer(self):
384 384 return bundlepeer(self)
385 385
386 386 def getcwd(self):
387 387 return os.getcwd() # always outside the repo
388 388
389 389 # Check if parents exist in localrepo before setting
390 390 def setparents(self, p1, p2=nullid):
391 391 p1rev = self.changelog.rev(p1)
392 392 p2rev = self.changelog.rev(p2)
393 393 msg = _("setting parent to node %s that only exists in the bundle\n")
394 394 if self.changelog.repotiprev < p1rev:
395 395 self.ui.warn(msg % nodemod.hex(p1))
396 396 if self.changelog.repotiprev < p2rev:
397 397 self.ui.warn(msg % nodemod.hex(p2))
398 398 return super(bundlerepository, self).setparents(p1, p2)
399 399
400 400 def instance(ui, path, create):
401 401 if create:
402 402 raise error.Abort(_('cannot create new bundle repository'))
403 403 # internal config: bundle.mainreporoot
404 404 parentpath = ui.config("bundle", "mainreporoot", "")
405 405 if not parentpath:
406 406 # try to find the correct path to the working directory repo
407 407 parentpath = cmdutil.findrepo(os.getcwd())
408 408 if parentpath is None:
409 409 parentpath = ''
410 410 if parentpath:
411 411 # Try to make the full path relative so we get a nice, short URL.
412 412 # In particular, we don't want temp dir names in test outputs.
413 413 cwd = os.getcwd()
414 414 if parentpath == cwd:
415 415 parentpath = ''
416 416 else:
417 417 cwd = pathutil.normasprefix(cwd)
418 418 if parentpath.startswith(cwd):
419 419 parentpath = parentpath[len(cwd):]
420 420 u = util.url(path)
421 421 path = u.localpath()
422 422 if u.scheme == 'bundle':
423 423 s = path.split("+", 1)
424 424 if len(s) == 1:
425 425 repopath, bundlename = parentpath, s[0]
426 426 else:
427 427 repopath, bundlename = s
428 428 else:
429 429 repopath, bundlename = parentpath, path
430 430 return bundlerepository(ui, repopath, bundlename)
431 431
432 432 class bundletransactionmanager(object):
433 433 def transaction(self):
434 434 return None
435 435
436 436 def close(self):
437 437 raise NotImplementedError
438 438
439 439 def release(self):
440 440 raise NotImplementedError
441 441
442 442 def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None,
443 443 force=False):
444 444 '''obtains a bundle of changes incoming from other
445 445
446 446 "onlyheads" restricts the returned changes to those reachable from the
447 447 specified heads.
448 448 "bundlename", if given, stores the bundle to this file path permanently;
449 449 otherwise it's stored to a temp file and gets deleted again when you call
450 450 the returned "cleanupfn".
451 451 "force" indicates whether to proceed on unrelated repos.
452 452
453 453 Returns a tuple (local, csets, cleanupfn):
454 454
455 455 "local" is a local repo from which to obtain the actual incoming
456 456 changesets; it is a bundlerepo for the obtained bundle when the
457 457 original "other" is remote.
458 458 "csets" lists the incoming changeset node ids.
459 459 "cleanupfn" must be called without arguments when you're done processing
460 460 the changes; it closes both the original "other" and the one returned
461 461 here.
462 462 '''
463 463 tmp = discovery.findcommonincoming(repo, other, heads=onlyheads,
464 464 force=force)
465 465 common, incoming, rheads = tmp
466 466 if not incoming:
467 467 try:
468 468 if bundlename:
469 469 os.unlink(bundlename)
470 470 except OSError:
471 471 pass
472 472 return repo, [], other.close
473 473
474 474 commonset = set(common)
475 475 rheads = [x for x in rheads if x not in commonset]
476 476
477 477 bundle = None
478 478 bundlerepo = None
479 479 localrepo = other.local()
480 480 if bundlename or not localrepo:
481 481 # create a bundle (uncompressed if other repo is not local)
482 482
483 483 canbundle2 = (ui.configbool('experimental', 'bundle2-exp', True)
484 484 and other.capable('getbundle')
485 485 and other.capable('bundle2'))
486 486 if canbundle2:
487 487 kwargs = {}
488 488 kwargs['common'] = common
489 489 kwargs['heads'] = rheads
490 490 kwargs['bundlecaps'] = exchange.caps20to10(repo)
491 491 kwargs['cg'] = True
492 492 b2 = other.getbundle('incoming', **kwargs)
493 493 fname = bundle = changegroup.writechunks(ui, b2._forwardchunks(),
494 494 bundlename)
495 495 else:
496 496 if other.capable('getbundle'):
497 497 cg = other.getbundle('incoming', common=common, heads=rheads)
498 498 elif onlyheads is None and not other.capable('changegroupsubset'):
499 499 # compat with older servers when pulling all remote heads
500 500 cg = other.changegroup(incoming, "incoming")
501 501 rheads = None
502 502 else:
503 503 cg = other.changegroupsubset(incoming, rheads, 'incoming')
504 504 if localrepo:
505 505 bundletype = "HG10BZ"
506 506 else:
507 507 bundletype = "HG10UN"
508 508 fname = bundle = bundle2.writebundle(ui, cg, bundlename,
509 509 bundletype)
510 510 # keep written bundle?
511 511 if bundlename:
512 512 bundle = None
513 513 if not localrepo:
514 514 # use the created uncompressed bundlerepo
515 515 localrepo = bundlerepo = bundlerepository(repo.baseui, repo.root,
516 516 fname)
517 517 # this repo contains local and other now, so filter out local again
518 518 common = repo.heads()
519 519 if localrepo:
520 520 # Part of common may be remotely filtered
521 521 # So use an unfiltered version
522 522 # The discovery process probably need cleanup to avoid that
523 523 localrepo = localrepo.unfiltered()
524 524
525 525 csets = localrepo.changelog.findmissing(common, rheads)
526 526
527 527 if bundlerepo:
528 528 reponodes = [ctx.node() for ctx in bundlerepo[bundlerepo.firstnewrev:]]
529 529 remotephases = other.listkeys('phases')
530 530
531 531 pullop = exchange.pulloperation(bundlerepo, other, heads=reponodes)
532 532 pullop.trmanager = bundletransactionmanager()
533 533 exchange._pullapplyphases(pullop, remotephases)
534 534
535 535 def cleanup():
536 536 if bundlerepo:
537 537 bundlerepo.close()
538 538 if bundle:
539 539 os.unlink(bundle)
540 540 other.close()
541 541
542 542 return (localrepo, csets, cleanup)
@@ -1,1932 +1,1932 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 errno
11 11 import hashlib
12 12
13 13 from .i18n import _
14 14 from .node import (
15 15 hex,
16 16 nullid,
17 17 )
18 18 from . import (
19 19 base85,
20 20 bookmarks as bookmod,
21 21 bundle2,
22 22 changegroup,
23 23 discovery,
24 24 error,
25 25 lock as lockmod,
26 26 obsolete,
27 27 phases,
28 28 pushkey,
29 29 scmutil,
30 30 sslutil,
31 31 streamclone,
32 32 tags,
33 33 url as urlmod,
34 34 util,
35 35 )
36 36
37 37 urlerr = util.urlerr
38 38 urlreq = util.urlreq
39 39
40 40 # Maps bundle compression human names to internal representation.
41 41 _bundlespeccompressions = {'none': None,
42 42 'bzip2': 'BZ',
43 43 'gzip': 'GZ',
44 44 }
45 45
46 46 # Maps bundle version human names to changegroup versions.
47 47 _bundlespeccgversions = {'v1': '01',
48 48 'v2': '02',
49 49 'packed1': 's1',
50 50 'bundle2': '02', #legacy
51 51 }
52 52
53 53 def parsebundlespec(repo, spec, strict=True, externalnames=False):
54 54 """Parse a bundle string specification into parts.
55 55
56 56 Bundle specifications denote a well-defined bundle/exchange format.
57 57 The content of a given specification should not change over time in
58 58 order to ensure that bundles produced by a newer version of Mercurial are
59 59 readable from an older version.
60 60
61 61 The string currently has the form:
62 62
63 63 <compression>-<type>[;<parameter0>[;<parameter1>]]
64 64
65 65 Where <compression> is one of the supported compression formats
66 66 and <type> is (currently) a version string. A ";" can follow the type and
67 67 all text afterwards is interpretted as URI encoded, ";" delimited key=value
68 68 pairs.
69 69
70 70 If ``strict`` is True (the default) <compression> is required. Otherwise,
71 71 it is optional.
72 72
73 73 If ``externalnames`` is False (the default), the human-centric names will
74 74 be converted to their internal representation.
75 75
76 76 Returns a 3-tuple of (compression, version, parameters). Compression will
77 77 be ``None`` if not in strict mode and a compression isn't defined.
78 78
79 79 An ``InvalidBundleSpecification`` is raised when the specification is
80 80 not syntactically well formed.
81 81
82 82 An ``UnsupportedBundleSpecification`` is raised when the compression or
83 83 bundle type/version is not recognized.
84 84
85 85 Note: this function will likely eventually return a more complex data
86 86 structure, including bundle2 part information.
87 87 """
88 88 def parseparams(s):
89 89 if ';' not in s:
90 90 return s, {}
91 91
92 92 params = {}
93 93 version, paramstr = s.split(';', 1)
94 94
95 95 for p in paramstr.split(';'):
96 96 if '=' not in p:
97 97 raise error.InvalidBundleSpecification(
98 98 _('invalid bundle specification: '
99 99 'missing "=" in parameter: %s') % p)
100 100
101 101 key, value = p.split('=', 1)
102 102 key = urlreq.unquote(key)
103 103 value = urlreq.unquote(value)
104 104 params[key] = value
105 105
106 106 return version, params
107 107
108 108
109 109 if strict and '-' not in spec:
110 110 raise error.InvalidBundleSpecification(
111 111 _('invalid bundle specification; '
112 112 'must be prefixed with compression: %s') % spec)
113 113
114 114 if '-' in spec:
115 115 compression, version = spec.split('-', 1)
116 116
117 117 if compression not in _bundlespeccompressions:
118 118 raise error.UnsupportedBundleSpecification(
119 119 _('%s compression is not supported') % compression)
120 120
121 121 version, params = parseparams(version)
122 122
123 123 if version not in _bundlespeccgversions:
124 124 raise error.UnsupportedBundleSpecification(
125 125 _('%s is not a recognized bundle version') % version)
126 126 else:
127 127 # Value could be just the compression or just the version, in which
128 128 # case some defaults are assumed (but only when not in strict mode).
129 129 assert not strict
130 130
131 131 spec, params = parseparams(spec)
132 132
133 133 if spec in _bundlespeccompressions:
134 134 compression = spec
135 135 version = 'v1'
136 136 if 'generaldelta' in repo.requirements:
137 137 version = 'v2'
138 138 elif spec in _bundlespeccgversions:
139 139 if spec == 'packed1':
140 140 compression = 'none'
141 141 else:
142 142 compression = 'bzip2'
143 143 version = spec
144 144 else:
145 145 raise error.UnsupportedBundleSpecification(
146 146 _('%s is not a recognized bundle specification') % spec)
147 147
148 148 # The specification for packed1 can optionally declare the data formats
149 149 # required to apply it. If we see this metadata, compare against what the
150 150 # repo supports and error if the bundle isn't compatible.
151 151 if version == 'packed1' and 'requirements' in params:
152 152 requirements = set(params['requirements'].split(','))
153 153 missingreqs = requirements - repo.supportedformats
154 154 if missingreqs:
155 155 raise error.UnsupportedBundleSpecification(
156 156 _('missing support for repository features: %s') %
157 157 ', '.join(sorted(missingreqs)))
158 158
159 159 if not externalnames:
160 160 compression = _bundlespeccompressions[compression]
161 161 version = _bundlespeccgversions[version]
162 162 return compression, version, params
163 163
164 164 def readbundle(ui, fh, fname, vfs=None):
165 165 header = changegroup.readexactly(fh, 4)
166 166
167 167 alg = None
168 168 if not fname:
169 169 fname = "stream"
170 170 if not header.startswith('HG') and header.startswith('\0'):
171 171 fh = changegroup.headerlessfixup(fh, header)
172 172 header = "HG10"
173 173 alg = 'UN'
174 174 elif vfs:
175 175 fname = vfs.join(fname)
176 176
177 177 magic, version = header[0:2], header[2:4]
178 178
179 179 if magic != 'HG':
180 180 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
181 181 if version == '10':
182 182 if alg is None:
183 183 alg = changegroup.readexactly(fh, 2)
184 184 return changegroup.cg1unpacker(fh, alg)
185 185 elif version.startswith('2'):
186 186 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
187 187 elif version == 'S1':
188 188 return streamclone.streamcloneapplier(fh)
189 189 else:
190 190 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
191 191
192 192 def getbundlespec(ui, fh):
193 193 """Infer the bundlespec from a bundle file handle.
194 194
195 195 The input file handle is seeked and the original seek position is not
196 196 restored.
197 197 """
198 198 def speccompression(alg):
199 199 for k, v in _bundlespeccompressions.items():
200 200 if v == alg:
201 201 return k
202 202 return None
203 203
204 204 b = readbundle(ui, fh, None)
205 205 if isinstance(b, changegroup.cg1unpacker):
206 206 alg = b._type
207 207 if alg == '_truncatedBZ':
208 208 alg = 'BZ'
209 209 comp = speccompression(alg)
210 210 if not comp:
211 211 raise error.Abort(_('unknown compression algorithm: %s') % alg)
212 212 return '%s-v1' % comp
213 213 elif isinstance(b, bundle2.unbundle20):
214 214 if 'Compression' in b.params:
215 215 comp = speccompression(b.params['Compression'])
216 216 if not comp:
217 217 raise error.Abort(_('unknown compression algorithm: %s') % comp)
218 218 else:
219 219 comp = 'none'
220 220
221 221 version = None
222 222 for part in b.iterparts():
223 223 if part.type == 'changegroup':
224 224 version = part.params['version']
225 225 if version in ('01', '02'):
226 226 version = 'v2'
227 227 else:
228 228 raise error.Abort(_('changegroup version %s does not have '
229 229 'a known bundlespec') % version,
230 230 hint=_('try upgrading your Mercurial '
231 231 'client'))
232 232
233 233 if not version:
234 234 raise error.Abort(_('could not identify changegroup version in '
235 235 'bundle'))
236 236
237 237 return '%s-%s' % (comp, version)
238 238 elif isinstance(b, streamclone.streamcloneapplier):
239 239 requirements = streamclone.readbundle1header(fh)[2]
240 240 params = 'requirements=%s' % ','.join(sorted(requirements))
241 241 return 'none-packed1;%s' % urlreq.quote(params)
242 242 else:
243 243 raise error.Abort(_('unknown bundle type: %s') % b)
244 244
245 245 def buildobsmarkerspart(bundler, markers):
246 246 """add an obsmarker part to the bundler with <markers>
247 247
248 248 No part is created if markers is empty.
249 249 Raises ValueError if the bundler doesn't support any known obsmarker format.
250 250 """
251 251 if markers:
252 252 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
253 253 version = obsolete.commonversion(remoteversions)
254 254 if version is None:
255 255 raise ValueError('bundler does not support common obsmarker format')
256 256 stream = obsolete.encodemarkers(markers, True, version=version)
257 257 return bundler.newpart('obsmarkers', data=stream)
258 258 return None
259 259
260 260 def _canusebundle2(op):
261 261 """return true if a pull/push can use bundle2
262 262
263 263 Feel free to nuke this function when we drop the experimental option"""
264 264 return (op.repo.ui.configbool('experimental', 'bundle2-exp', True)
265 265 and op.remote.capable('bundle2'))
266 266
267 267
268 268 class pushoperation(object):
269 269 """A object that represent a single push operation
270 270
271 271 Its purpose is to carry push related state and very common operations.
272 272
273 273 A new pushoperation should be created at the beginning of each push and
274 274 discarded afterward.
275 275 """
276 276
277 277 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
278 278 bookmarks=()):
279 279 # repo we push from
280 280 self.repo = repo
281 281 self.ui = repo.ui
282 282 # repo we push to
283 283 self.remote = remote
284 284 # force option provided
285 285 self.force = force
286 286 # revs to be pushed (None is "all")
287 287 self.revs = revs
288 288 # bookmark explicitly pushed
289 289 self.bookmarks = bookmarks
290 290 # allow push of new branch
291 291 self.newbranch = newbranch
292 292 # did a local lock get acquired?
293 293 self.locallocked = None
294 294 # step already performed
295 295 # (used to check what steps have been already performed through bundle2)
296 296 self.stepsdone = set()
297 297 # Integer version of the changegroup push result
298 298 # - None means nothing to push
299 299 # - 0 means HTTP error
300 300 # - 1 means we pushed and remote head count is unchanged *or*
301 301 # we have outgoing changesets but refused to push
302 302 # - other values as described by addchangegroup()
303 303 self.cgresult = None
304 304 # Boolean value for the bookmark push
305 305 self.bkresult = None
306 306 # discover.outgoing object (contains common and outgoing data)
307 307 self.outgoing = None
308 308 # all remote heads before the push
309 309 self.remoteheads = None
310 310 # testable as a boolean indicating if any nodes are missing locally.
311 311 self.incoming = None
312 312 # phases changes that must be pushed along side the changesets
313 313 self.outdatedphases = None
314 314 # phases changes that must be pushed if changeset push fails
315 315 self.fallbackoutdatedphases = None
316 316 # outgoing obsmarkers
317 317 self.outobsmarkers = set()
318 318 # outgoing bookmarks
319 319 self.outbookmarks = []
320 320 # transaction manager
321 321 self.trmanager = None
322 322 # map { pushkey partid -> callback handling failure}
323 323 # used to handle exception from mandatory pushkey part failure
324 324 self.pkfailcb = {}
325 325
326 326 @util.propertycache
327 327 def futureheads(self):
328 328 """future remote heads if the changeset push succeeds"""
329 329 return self.outgoing.missingheads
330 330
331 331 @util.propertycache
332 332 def fallbackheads(self):
333 333 """future remote heads if the changeset push fails"""
334 334 if self.revs is None:
335 335 # not target to push, all common are relevant
336 336 return self.outgoing.commonheads
337 337 unfi = self.repo.unfiltered()
338 338 # I want cheads = heads(::missingheads and ::commonheads)
339 339 # (missingheads is revs with secret changeset filtered out)
340 340 #
341 341 # This can be expressed as:
342 342 # cheads = ( (missingheads and ::commonheads)
343 343 # + (commonheads and ::missingheads))"
344 344 # )
345 345 #
346 346 # while trying to push we already computed the following:
347 347 # common = (::commonheads)
348 348 # missing = ((commonheads::missingheads) - commonheads)
349 349 #
350 350 # We can pick:
351 351 # * missingheads part of common (::commonheads)
352 352 common = self.outgoing.common
353 353 nm = self.repo.changelog.nodemap
354 354 cheads = [node for node in self.revs if nm[node] in common]
355 355 # and
356 356 # * commonheads parents on missing
357 357 revset = unfi.set('%ln and parents(roots(%ln))',
358 358 self.outgoing.commonheads,
359 359 self.outgoing.missing)
360 360 cheads.extend(c.node() for c in revset)
361 361 return cheads
362 362
363 363 @property
364 364 def commonheads(self):
365 365 """set of all common heads after changeset bundle push"""
366 366 if self.cgresult:
367 367 return self.futureheads
368 368 else:
369 369 return self.fallbackheads
370 370
371 371 # mapping of message used when pushing bookmark
372 372 bookmsgmap = {'update': (_("updating bookmark %s\n"),
373 373 _('updating bookmark %s failed!\n')),
374 374 'export': (_("exporting bookmark %s\n"),
375 375 _('exporting bookmark %s failed!\n')),
376 376 'delete': (_("deleting remote bookmark %s\n"),
377 377 _('deleting remote bookmark %s failed!\n')),
378 378 }
379 379
380 380
381 381 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
382 382 opargs=None):
383 383 '''Push outgoing changesets (limited by revs) from a local
384 384 repository to remote. Return an integer:
385 385 - None means nothing to push
386 386 - 0 means HTTP error
387 387 - 1 means we pushed and remote head count is unchanged *or*
388 388 we have outgoing changesets but refused to push
389 389 - other values as described by addchangegroup()
390 390 '''
391 391 if opargs is None:
392 392 opargs = {}
393 393 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
394 394 **opargs)
395 395 if pushop.remote.local():
396 396 missing = (set(pushop.repo.requirements)
397 397 - pushop.remote.local().supported)
398 398 if missing:
399 399 msg = _("required features are not"
400 400 " supported in the destination:"
401 401 " %s") % (', '.join(sorted(missing)))
402 402 raise error.Abort(msg)
403 403
404 404 # there are two ways to push to remote repo:
405 405 #
406 406 # addchangegroup assumes local user can lock remote
407 407 # repo (local filesystem, old ssh servers).
408 408 #
409 409 # unbundle assumes local user cannot lock remote repo (new ssh
410 410 # servers, http servers).
411 411
412 412 if not pushop.remote.canpush():
413 413 raise error.Abort(_("destination does not support push"))
414 414 # get local lock as we might write phase data
415 415 localwlock = locallock = None
416 416 try:
417 417 # bundle2 push may receive a reply bundle touching bookmarks or other
418 418 # things requiring the wlock. Take it now to ensure proper ordering.
419 419 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
420 420 if _canusebundle2(pushop) and maypushback:
421 421 localwlock = pushop.repo.wlock()
422 422 locallock = pushop.repo.lock()
423 423 pushop.locallocked = True
424 424 except IOError as err:
425 425 pushop.locallocked = False
426 426 if err.errno != errno.EACCES:
427 427 raise
428 428 # source repo cannot be locked.
429 429 # We do not abort the push, but just disable the local phase
430 430 # synchronisation.
431 431 msg = 'cannot lock source repository: %s\n' % err
432 432 pushop.ui.debug(msg)
433 433 try:
434 434 if pushop.locallocked:
435 435 pushop.trmanager = transactionmanager(pushop.repo,
436 436 'push-response',
437 437 pushop.remote.url())
438 438 pushop.repo.checkpush(pushop)
439 439 lock = None
440 440 unbundle = pushop.remote.capable('unbundle')
441 441 if not unbundle:
442 442 lock = pushop.remote.lock()
443 443 try:
444 444 _pushdiscovery(pushop)
445 445 if _canusebundle2(pushop):
446 446 _pushbundle2(pushop)
447 447 _pushchangeset(pushop)
448 448 _pushsyncphase(pushop)
449 449 _pushobsolete(pushop)
450 450 _pushbookmark(pushop)
451 451 finally:
452 452 if lock is not None:
453 453 lock.release()
454 454 if pushop.trmanager:
455 455 pushop.trmanager.close()
456 456 finally:
457 457 if pushop.trmanager:
458 458 pushop.trmanager.release()
459 459 if locallock is not None:
460 460 locallock.release()
461 461 if localwlock is not None:
462 462 localwlock.release()
463 463
464 464 return pushop
465 465
466 466 # list of steps to perform discovery before push
467 467 pushdiscoveryorder = []
468 468
469 469 # Mapping between step name and function
470 470 #
471 471 # This exists to help extensions wrap steps if necessary
472 472 pushdiscoverymapping = {}
473 473
474 474 def pushdiscovery(stepname):
475 475 """decorator for function performing discovery before push
476 476
477 477 The function is added to the step -> function mapping and appended to the
478 478 list of steps. Beware that decorated function will be added in order (this
479 479 may matter).
480 480
481 481 You can only use this decorator for a new step, if you want to wrap a step
482 482 from an extension, change the pushdiscovery dictionary directly."""
483 483 def dec(func):
484 484 assert stepname not in pushdiscoverymapping
485 485 pushdiscoverymapping[stepname] = func
486 486 pushdiscoveryorder.append(stepname)
487 487 return func
488 488 return dec
489 489
490 490 def _pushdiscovery(pushop):
491 491 """Run all discovery steps"""
492 492 for stepname in pushdiscoveryorder:
493 493 step = pushdiscoverymapping[stepname]
494 494 step(pushop)
495 495
496 496 @pushdiscovery('changeset')
497 497 def _pushdiscoverychangeset(pushop):
498 498 """discover the changeset that need to be pushed"""
499 499 fci = discovery.findcommonincoming
500 500 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
501 501 common, inc, remoteheads = commoninc
502 502 fco = discovery.findcommonoutgoing
503 503 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
504 504 commoninc=commoninc, force=pushop.force)
505 505 pushop.outgoing = outgoing
506 506 pushop.remoteheads = remoteheads
507 507 pushop.incoming = inc
508 508
509 509 @pushdiscovery('phase')
510 510 def _pushdiscoveryphase(pushop):
511 511 """discover the phase that needs to be pushed
512 512
513 513 (computed for both success and failure case for changesets push)"""
514 514 outgoing = pushop.outgoing
515 515 unfi = pushop.repo.unfiltered()
516 516 remotephases = pushop.remote.listkeys('phases')
517 517 publishing = remotephases.get('publishing', False)
518 518 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
519 519 and remotephases # server supports phases
520 520 and not pushop.outgoing.missing # no changesets to be pushed
521 521 and publishing):
522 522 # When:
523 523 # - this is a subrepo push
524 524 # - and remote support phase
525 525 # - and no changeset are to be pushed
526 526 # - and remote is publishing
527 527 # We may be in issue 3871 case!
528 528 # We drop the possible phase synchronisation done by
529 529 # courtesy to publish changesets possibly locally draft
530 530 # on the remote.
531 531 remotephases = {'publishing': 'True'}
532 532 ana = phases.analyzeremotephases(pushop.repo,
533 533 pushop.fallbackheads,
534 534 remotephases)
535 535 pheads, droots = ana
536 536 extracond = ''
537 537 if not publishing:
538 538 extracond = ' and public()'
539 539 revset = 'heads((%%ln::%%ln) %s)' % extracond
540 540 # Get the list of all revs draft on remote by public here.
541 541 # XXX Beware that revset break if droots is not strictly
542 542 # XXX root we may want to ensure it is but it is costly
543 543 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
544 544 if not outgoing.missing:
545 545 future = fallback
546 546 else:
547 547 # adds changeset we are going to push as draft
548 548 #
549 549 # should not be necessary for publishing server, but because of an
550 550 # issue fixed in xxxxx we have to do it anyway.
551 551 fdroots = list(unfi.set('roots(%ln + %ln::)',
552 552 outgoing.missing, droots))
553 553 fdroots = [f.node() for f in fdroots]
554 554 future = list(unfi.set(revset, fdroots, pushop.futureheads))
555 555 pushop.outdatedphases = future
556 556 pushop.fallbackoutdatedphases = fallback
557 557
558 558 @pushdiscovery('obsmarker')
559 559 def _pushdiscoveryobsmarkers(pushop):
560 560 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
561 561 and pushop.repo.obsstore
562 562 and 'obsolete' in pushop.remote.listkeys('namespaces')):
563 563 repo = pushop.repo
564 564 # very naive computation, that can be quite expensive on big repo.
565 565 # However: evolution is currently slow on them anyway.
566 566 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
567 567 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
568 568
569 569 @pushdiscovery('bookmarks')
570 570 def _pushdiscoverybookmarks(pushop):
571 571 ui = pushop.ui
572 572 repo = pushop.repo.unfiltered()
573 573 remote = pushop.remote
574 574 ui.debug("checking for updated bookmarks\n")
575 575 ancestors = ()
576 576 if pushop.revs:
577 577 revnums = map(repo.changelog.rev, pushop.revs)
578 578 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
579 579 remotebookmark = remote.listkeys('bookmarks')
580 580
581 581 explicit = set([repo._bookmarks.expandname(bookmark)
582 582 for bookmark in pushop.bookmarks])
583 583
584 584 comp = bookmod.compare(repo, repo._bookmarks, remotebookmark, srchex=hex)
585 585 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
586 586 for b, scid, dcid in advsrc:
587 587 if b in explicit:
588 588 explicit.remove(b)
589 589 if not ancestors or repo[scid].rev() in ancestors:
590 590 pushop.outbookmarks.append((b, dcid, scid))
591 591 # search added bookmark
592 592 for b, scid, dcid in addsrc:
593 593 if b in explicit:
594 594 explicit.remove(b)
595 595 pushop.outbookmarks.append((b, '', scid))
596 596 # search for overwritten bookmark
597 597 for b, scid, dcid in advdst + diverge + differ:
598 598 if b in explicit:
599 599 explicit.remove(b)
600 600 pushop.outbookmarks.append((b, dcid, scid))
601 601 # search for bookmark to delete
602 602 for b, scid, dcid in adddst:
603 603 if b in explicit:
604 604 explicit.remove(b)
605 605 # treat as "deleted locally"
606 606 pushop.outbookmarks.append((b, dcid, ''))
607 607 # identical bookmarks shouldn't get reported
608 608 for b, scid, dcid in same:
609 609 if b in explicit:
610 610 explicit.remove(b)
611 611
612 612 if explicit:
613 613 explicit = sorted(explicit)
614 614 # we should probably list all of them
615 615 ui.warn(_('bookmark %s does not exist on the local '
616 616 'or remote repository!\n') % explicit[0])
617 617 pushop.bkresult = 2
618 618
619 619 pushop.outbookmarks.sort()
620 620
621 621 def _pushcheckoutgoing(pushop):
622 622 outgoing = pushop.outgoing
623 623 unfi = pushop.repo.unfiltered()
624 624 if not outgoing.missing:
625 625 # nothing to push
626 626 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
627 627 return False
628 628 # something to push
629 629 if not pushop.force:
630 630 # if repo.obsstore == False --> no obsolete
631 631 # then, save the iteration
632 632 if unfi.obsstore:
633 633 # this message are here for 80 char limit reason
634 634 mso = _("push includes obsolete changeset: %s!")
635 635 mst = {"unstable": _("push includes unstable changeset: %s!"),
636 636 "bumped": _("push includes bumped changeset: %s!"),
637 637 "divergent": _("push includes divergent changeset: %s!")}
638 638 # If we are to push if there is at least one
639 639 # obsolete or unstable changeset in missing, at
640 640 # least one of the missinghead will be obsolete or
641 641 # unstable. So checking heads only is ok
642 642 for node in outgoing.missingheads:
643 643 ctx = unfi[node]
644 644 if ctx.obsolete():
645 645 raise error.Abort(mso % ctx)
646 646 elif ctx.troubled():
647 647 raise error.Abort(mst[ctx.troubles()[0]] % ctx)
648 648
649 649 discovery.checkheads(pushop)
650 650 return True
651 651
652 652 # List of names of steps to perform for an outgoing bundle2, order matters.
653 653 b2partsgenorder = []
654 654
655 655 # Mapping between step name and function
656 656 #
657 657 # This exists to help extensions wrap steps if necessary
658 658 b2partsgenmapping = {}
659 659
660 660 def b2partsgenerator(stepname, idx=None):
661 661 """decorator for function generating bundle2 part
662 662
663 663 The function is added to the step -> function mapping and appended to the
664 664 list of steps. Beware that decorated functions will be added in order
665 665 (this may matter).
666 666
667 667 You can only use this decorator for new steps, if you want to wrap a step
668 668 from an extension, attack the b2partsgenmapping dictionary directly."""
669 669 def dec(func):
670 670 assert stepname not in b2partsgenmapping
671 671 b2partsgenmapping[stepname] = func
672 672 if idx is None:
673 673 b2partsgenorder.append(stepname)
674 674 else:
675 675 b2partsgenorder.insert(idx, stepname)
676 676 return func
677 677 return dec
678 678
679 679 def _pushb2ctxcheckheads(pushop, bundler):
680 680 """Generate race condition checking parts
681 681
682 682 Exists as an independent function to aid extensions
683 683 """
684 684 if not pushop.force:
685 685 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
686 686
687 687 @b2partsgenerator('changeset')
688 688 def _pushb2ctx(pushop, bundler):
689 689 """handle changegroup push through bundle2
690 690
691 691 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
692 692 """
693 693 if 'changesets' in pushop.stepsdone:
694 694 return
695 695 pushop.stepsdone.add('changesets')
696 696 # Send known heads to the server for race detection.
697 697 if not _pushcheckoutgoing(pushop):
698 698 return
699 699 pushop.repo.prepushoutgoinghooks(pushop)
700 700
701 701 _pushb2ctxcheckheads(pushop, bundler)
702 702
703 703 b2caps = bundle2.bundle2caps(pushop.remote)
704 704 version = '01'
705 705 cgversions = b2caps.get('changegroup')
706 706 if cgversions: # 3.1 and 3.2 ship with an empty value
707 707 cgversions = [v for v in cgversions
708 708 if v in changegroup.supportedoutgoingversions(
709 709 pushop.repo)]
710 710 if not cgversions:
711 711 raise ValueError(_('no common changegroup version'))
712 712 version = max(cgversions)
713 713 cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push',
714 714 pushop.outgoing,
715 715 version=version)
716 716 cgpart = bundler.newpart('changegroup', data=cg)
717 717 if cgversions:
718 718 cgpart.addparam('version', version)
719 719 if 'treemanifest' in pushop.repo.requirements:
720 720 cgpart.addparam('treemanifest', '1')
721 721 def handlereply(op):
722 722 """extract addchangegroup returns from server reply"""
723 723 cgreplies = op.records.getreplies(cgpart.id)
724 724 assert len(cgreplies['changegroup']) == 1
725 725 pushop.cgresult = cgreplies['changegroup'][0]['return']
726 726 return handlereply
727 727
728 728 @b2partsgenerator('phase')
729 729 def _pushb2phases(pushop, bundler):
730 730 """handle phase push through bundle2"""
731 731 if 'phases' in pushop.stepsdone:
732 732 return
733 733 b2caps = bundle2.bundle2caps(pushop.remote)
734 734 if not 'pushkey' in b2caps:
735 735 return
736 736 pushop.stepsdone.add('phases')
737 737 part2node = []
738 738
739 739 def handlefailure(pushop, exc):
740 740 targetid = int(exc.partid)
741 741 for partid, node in part2node:
742 742 if partid == targetid:
743 743 raise error.Abort(_('updating %s to public failed') % node)
744 744
745 745 enc = pushkey.encode
746 746 for newremotehead in pushop.outdatedphases:
747 747 part = bundler.newpart('pushkey')
748 748 part.addparam('namespace', enc('phases'))
749 749 part.addparam('key', enc(newremotehead.hex()))
750 750 part.addparam('old', enc(str(phases.draft)))
751 751 part.addparam('new', enc(str(phases.public)))
752 752 part2node.append((part.id, newremotehead))
753 753 pushop.pkfailcb[part.id] = handlefailure
754 754
755 755 def handlereply(op):
756 756 for partid, node in part2node:
757 757 partrep = op.records.getreplies(partid)
758 758 results = partrep['pushkey']
759 759 assert len(results) <= 1
760 760 msg = None
761 761 if not results:
762 762 msg = _('server ignored update of %s to public!\n') % node
763 763 elif not int(results[0]['return']):
764 764 msg = _('updating %s to public failed!\n') % node
765 765 if msg is not None:
766 766 pushop.ui.warn(msg)
767 767 return handlereply
768 768
769 769 @b2partsgenerator('obsmarkers')
770 770 def _pushb2obsmarkers(pushop, bundler):
771 771 if 'obsmarkers' in pushop.stepsdone:
772 772 return
773 773 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
774 774 if obsolete.commonversion(remoteversions) is None:
775 775 return
776 776 pushop.stepsdone.add('obsmarkers')
777 777 if pushop.outobsmarkers:
778 778 markers = sorted(pushop.outobsmarkers)
779 779 buildobsmarkerspart(bundler, markers)
780 780
781 781 @b2partsgenerator('bookmarks')
782 782 def _pushb2bookmarks(pushop, bundler):
783 783 """handle bookmark push through bundle2"""
784 784 if 'bookmarks' in pushop.stepsdone:
785 785 return
786 786 b2caps = bundle2.bundle2caps(pushop.remote)
787 787 if 'pushkey' not in b2caps:
788 788 return
789 789 pushop.stepsdone.add('bookmarks')
790 790 part2book = []
791 791 enc = pushkey.encode
792 792
793 793 def handlefailure(pushop, exc):
794 794 targetid = int(exc.partid)
795 795 for partid, book, action in part2book:
796 796 if partid == targetid:
797 797 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
798 798 # we should not be called for part we did not generated
799 799 assert False
800 800
801 801 for book, old, new in pushop.outbookmarks:
802 802 part = bundler.newpart('pushkey')
803 803 part.addparam('namespace', enc('bookmarks'))
804 804 part.addparam('key', enc(book))
805 805 part.addparam('old', enc(old))
806 806 part.addparam('new', enc(new))
807 807 action = 'update'
808 808 if not old:
809 809 action = 'export'
810 810 elif not new:
811 811 action = 'delete'
812 812 part2book.append((part.id, book, action))
813 813 pushop.pkfailcb[part.id] = handlefailure
814 814
815 815 def handlereply(op):
816 816 ui = pushop.ui
817 817 for partid, book, action in part2book:
818 818 partrep = op.records.getreplies(partid)
819 819 results = partrep['pushkey']
820 820 assert len(results) <= 1
821 821 if not results:
822 822 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
823 823 else:
824 824 ret = int(results[0]['return'])
825 825 if ret:
826 826 ui.status(bookmsgmap[action][0] % book)
827 827 else:
828 828 ui.warn(bookmsgmap[action][1] % book)
829 829 if pushop.bkresult is not None:
830 830 pushop.bkresult = 1
831 831 return handlereply
832 832
833 833
834 834 def _pushbundle2(pushop):
835 835 """push data to the remote using bundle2
836 836
837 837 The only currently supported type of data is changegroup but this will
838 838 evolve in the future."""
839 839 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
840 840 pushback = (pushop.trmanager
841 841 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
842 842
843 843 # create reply capability
844 844 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
845 845 allowpushback=pushback))
846 846 bundler.newpart('replycaps', data=capsblob)
847 847 replyhandlers = []
848 848 for partgenname in b2partsgenorder:
849 849 partgen = b2partsgenmapping[partgenname]
850 850 ret = partgen(pushop, bundler)
851 851 if callable(ret):
852 852 replyhandlers.append(ret)
853 853 # do not push if nothing to push
854 854 if bundler.nbparts <= 1:
855 855 return
856 856 stream = util.chunkbuffer(bundler.getchunks())
857 857 try:
858 858 try:
859 859 reply = pushop.remote.unbundle(stream, ['force'], 'push')
860 860 except error.BundleValueError as exc:
861 raise error.Abort('missing support for %s' % exc)
861 raise error.Abort(_('missing support for %s') % exc)
862 862 try:
863 863 trgetter = None
864 864 if pushback:
865 865 trgetter = pushop.trmanager.transaction
866 866 op = bundle2.processbundle(pushop.repo, reply, trgetter)
867 867 except error.BundleValueError as exc:
868 raise error.Abort('missing support for %s' % exc)
868 raise error.Abort(_('missing support for %s') % exc)
869 869 except bundle2.AbortFromPart as exc:
870 870 pushop.ui.status(_('remote: %s\n') % exc)
871 871 raise error.Abort(_('push failed on remote'), hint=exc.hint)
872 872 except error.PushkeyFailed as exc:
873 873 partid = int(exc.partid)
874 874 if partid not in pushop.pkfailcb:
875 875 raise
876 876 pushop.pkfailcb[partid](pushop, exc)
877 877 for rephand in replyhandlers:
878 878 rephand(op)
879 879
880 880 def _pushchangeset(pushop):
881 881 """Make the actual push of changeset bundle to remote repo"""
882 882 if 'changesets' in pushop.stepsdone:
883 883 return
884 884 pushop.stepsdone.add('changesets')
885 885 if not _pushcheckoutgoing(pushop):
886 886 return
887 887 pushop.repo.prepushoutgoinghooks(pushop)
888 888 outgoing = pushop.outgoing
889 889 unbundle = pushop.remote.capable('unbundle')
890 890 # TODO: get bundlecaps from remote
891 891 bundlecaps = None
892 892 # create a changegroup from local
893 893 if pushop.revs is None and not (outgoing.excluded
894 894 or pushop.repo.changelog.filteredrevs):
895 895 # push everything,
896 896 # use the fast path, no race possible on push
897 897 bundler = changegroup.cg1packer(pushop.repo, bundlecaps)
898 898 cg = changegroup.getsubset(pushop.repo,
899 899 outgoing,
900 900 bundler,
901 901 'push',
902 902 fastpath=True)
903 903 else:
904 904 cg = changegroup.getlocalchangegroup(pushop.repo, 'push', outgoing,
905 905 bundlecaps)
906 906
907 907 # apply changegroup to remote
908 908 if unbundle:
909 909 # local repo finds heads on server, finds out what
910 910 # revs it must push. once revs transferred, if server
911 911 # finds it has different heads (someone else won
912 912 # commit/push race), server aborts.
913 913 if pushop.force:
914 914 remoteheads = ['force']
915 915 else:
916 916 remoteheads = pushop.remoteheads
917 917 # ssh: return remote's addchangegroup()
918 918 # http: return remote's addchangegroup() or 0 for error
919 919 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
920 920 pushop.repo.url())
921 921 else:
922 922 # we return an integer indicating remote head count
923 923 # change
924 924 pushop.cgresult = pushop.remote.addchangegroup(cg, 'push',
925 925 pushop.repo.url())
926 926
927 927 def _pushsyncphase(pushop):
928 928 """synchronise phase information locally and remotely"""
929 929 cheads = pushop.commonheads
930 930 # even when we don't push, exchanging phase data is useful
931 931 remotephases = pushop.remote.listkeys('phases')
932 932 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
933 933 and remotephases # server supports phases
934 934 and pushop.cgresult is None # nothing was pushed
935 935 and remotephases.get('publishing', False)):
936 936 # When:
937 937 # - this is a subrepo push
938 938 # - and remote support phase
939 939 # - and no changeset was pushed
940 940 # - and remote is publishing
941 941 # We may be in issue 3871 case!
942 942 # We drop the possible phase synchronisation done by
943 943 # courtesy to publish changesets possibly locally draft
944 944 # on the remote.
945 945 remotephases = {'publishing': 'True'}
946 946 if not remotephases: # old server or public only reply from non-publishing
947 947 _localphasemove(pushop, cheads)
948 948 # don't push any phase data as there is nothing to push
949 949 else:
950 950 ana = phases.analyzeremotephases(pushop.repo, cheads,
951 951 remotephases)
952 952 pheads, droots = ana
953 953 ### Apply remote phase on local
954 954 if remotephases.get('publishing', False):
955 955 _localphasemove(pushop, cheads)
956 956 else: # publish = False
957 957 _localphasemove(pushop, pheads)
958 958 _localphasemove(pushop, cheads, phases.draft)
959 959 ### Apply local phase on remote
960 960
961 961 if pushop.cgresult:
962 962 if 'phases' in pushop.stepsdone:
963 963 # phases already pushed though bundle2
964 964 return
965 965 outdated = pushop.outdatedphases
966 966 else:
967 967 outdated = pushop.fallbackoutdatedphases
968 968
969 969 pushop.stepsdone.add('phases')
970 970
971 971 # filter heads already turned public by the push
972 972 outdated = [c for c in outdated if c.node() not in pheads]
973 973 # fallback to independent pushkey command
974 974 for newremotehead in outdated:
975 975 r = pushop.remote.pushkey('phases',
976 976 newremotehead.hex(),
977 977 str(phases.draft),
978 978 str(phases.public))
979 979 if not r:
980 980 pushop.ui.warn(_('updating %s to public failed!\n')
981 981 % newremotehead)
982 982
983 983 def _localphasemove(pushop, nodes, phase=phases.public):
984 984 """move <nodes> to <phase> in the local source repo"""
985 985 if pushop.trmanager:
986 986 phases.advanceboundary(pushop.repo,
987 987 pushop.trmanager.transaction(),
988 988 phase,
989 989 nodes)
990 990 else:
991 991 # repo is not locked, do not change any phases!
992 992 # Informs the user that phases should have been moved when
993 993 # applicable.
994 994 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
995 995 phasestr = phases.phasenames[phase]
996 996 if actualmoves:
997 997 pushop.ui.status(_('cannot lock source repo, skipping '
998 998 'local %s phase update\n') % phasestr)
999 999
1000 1000 def _pushobsolete(pushop):
1001 1001 """utility function to push obsolete markers to a remote"""
1002 1002 if 'obsmarkers' in pushop.stepsdone:
1003 1003 return
1004 1004 repo = pushop.repo
1005 1005 remote = pushop.remote
1006 1006 pushop.stepsdone.add('obsmarkers')
1007 1007 if pushop.outobsmarkers:
1008 1008 pushop.ui.debug('try to push obsolete markers to remote\n')
1009 1009 rslts = []
1010 1010 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1011 1011 for key in sorted(remotedata, reverse=True):
1012 1012 # reverse sort to ensure we end with dump0
1013 1013 data = remotedata[key]
1014 1014 rslts.append(remote.pushkey('obsolete', key, '', data))
1015 1015 if [r for r in rslts if not r]:
1016 1016 msg = _('failed to push some obsolete markers!\n')
1017 1017 repo.ui.warn(msg)
1018 1018
1019 1019 def _pushbookmark(pushop):
1020 1020 """Update bookmark position on remote"""
1021 1021 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1022 1022 return
1023 1023 pushop.stepsdone.add('bookmarks')
1024 1024 ui = pushop.ui
1025 1025 remote = pushop.remote
1026 1026
1027 1027 for b, old, new in pushop.outbookmarks:
1028 1028 action = 'update'
1029 1029 if not old:
1030 1030 action = 'export'
1031 1031 elif not new:
1032 1032 action = 'delete'
1033 1033 if remote.pushkey('bookmarks', b, old, new):
1034 1034 ui.status(bookmsgmap[action][0] % b)
1035 1035 else:
1036 1036 ui.warn(bookmsgmap[action][1] % b)
1037 1037 # discovery can have set the value form invalid entry
1038 1038 if pushop.bkresult is not None:
1039 1039 pushop.bkresult = 1
1040 1040
1041 1041 class pulloperation(object):
1042 1042 """A object that represent a single pull operation
1043 1043
1044 1044 It purpose is to carry pull related state and very common operation.
1045 1045
1046 1046 A new should be created at the beginning of each pull and discarded
1047 1047 afterward.
1048 1048 """
1049 1049
1050 1050 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1051 1051 remotebookmarks=None, streamclonerequested=None):
1052 1052 # repo we pull into
1053 1053 self.repo = repo
1054 1054 # repo we pull from
1055 1055 self.remote = remote
1056 1056 # revision we try to pull (None is "all")
1057 1057 self.heads = heads
1058 1058 # bookmark pulled explicitly
1059 1059 self.explicitbookmarks = [repo._bookmarks.expandname(bookmark)
1060 1060 for bookmark in bookmarks]
1061 1061 # do we force pull?
1062 1062 self.force = force
1063 1063 # whether a streaming clone was requested
1064 1064 self.streamclonerequested = streamclonerequested
1065 1065 # transaction manager
1066 1066 self.trmanager = None
1067 1067 # set of common changeset between local and remote before pull
1068 1068 self.common = None
1069 1069 # set of pulled head
1070 1070 self.rheads = None
1071 1071 # list of missing changeset to fetch remotely
1072 1072 self.fetch = None
1073 1073 # remote bookmarks data
1074 1074 self.remotebookmarks = remotebookmarks
1075 1075 # result of changegroup pulling (used as return code by pull)
1076 1076 self.cgresult = None
1077 1077 # list of step already done
1078 1078 self.stepsdone = set()
1079 1079 # Whether we attempted a clone from pre-generated bundles.
1080 1080 self.clonebundleattempted = False
1081 1081
1082 1082 @util.propertycache
1083 1083 def pulledsubset(self):
1084 1084 """heads of the set of changeset target by the pull"""
1085 1085 # compute target subset
1086 1086 if self.heads is None:
1087 1087 # We pulled every thing possible
1088 1088 # sync on everything common
1089 1089 c = set(self.common)
1090 1090 ret = list(self.common)
1091 1091 for n in self.rheads:
1092 1092 if n not in c:
1093 1093 ret.append(n)
1094 1094 return ret
1095 1095 else:
1096 1096 # We pulled a specific subset
1097 1097 # sync on this subset
1098 1098 return self.heads
1099 1099
1100 1100 @util.propertycache
1101 1101 def canusebundle2(self):
1102 1102 return _canusebundle2(self)
1103 1103
1104 1104 @util.propertycache
1105 1105 def remotebundle2caps(self):
1106 1106 return bundle2.bundle2caps(self.remote)
1107 1107
1108 1108 def gettransaction(self):
1109 1109 # deprecated; talk to trmanager directly
1110 1110 return self.trmanager.transaction()
1111 1111
1112 1112 class transactionmanager(object):
1113 1113 """An object to manage the life cycle of a transaction
1114 1114
1115 1115 It creates the transaction on demand and calls the appropriate hooks when
1116 1116 closing the transaction."""
1117 1117 def __init__(self, repo, source, url):
1118 1118 self.repo = repo
1119 1119 self.source = source
1120 1120 self.url = url
1121 1121 self._tr = None
1122 1122
1123 1123 def transaction(self):
1124 1124 """Return an open transaction object, constructing if necessary"""
1125 1125 if not self._tr:
1126 1126 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1127 1127 self._tr = self.repo.transaction(trname)
1128 1128 self._tr.hookargs['source'] = self.source
1129 1129 self._tr.hookargs['url'] = self.url
1130 1130 return self._tr
1131 1131
1132 1132 def close(self):
1133 1133 """close transaction if created"""
1134 1134 if self._tr is not None:
1135 1135 self._tr.close()
1136 1136
1137 1137 def release(self):
1138 1138 """release transaction if created"""
1139 1139 if self._tr is not None:
1140 1140 self._tr.release()
1141 1141
1142 1142 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1143 1143 streamclonerequested=None):
1144 1144 """Fetch repository data from a remote.
1145 1145
1146 1146 This is the main function used to retrieve data from a remote repository.
1147 1147
1148 1148 ``repo`` is the local repository to clone into.
1149 1149 ``remote`` is a peer instance.
1150 1150 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1151 1151 default) means to pull everything from the remote.
1152 1152 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1153 1153 default, all remote bookmarks are pulled.
1154 1154 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1155 1155 initialization.
1156 1156 ``streamclonerequested`` is a boolean indicating whether a "streaming
1157 1157 clone" is requested. A "streaming clone" is essentially a raw file copy
1158 1158 of revlogs from the server. This only works when the local repository is
1159 1159 empty. The default value of ``None`` means to respect the server
1160 1160 configuration for preferring stream clones.
1161 1161
1162 1162 Returns the ``pulloperation`` created for this pull.
1163 1163 """
1164 1164 if opargs is None:
1165 1165 opargs = {}
1166 1166 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1167 1167 streamclonerequested=streamclonerequested, **opargs)
1168 1168 if pullop.remote.local():
1169 1169 missing = set(pullop.remote.requirements) - pullop.repo.supported
1170 1170 if missing:
1171 1171 msg = _("required features are not"
1172 1172 " supported in the destination:"
1173 1173 " %s") % (', '.join(sorted(missing)))
1174 1174 raise error.Abort(msg)
1175 1175
1176 1176 lock = pullop.repo.lock()
1177 1177 try:
1178 1178 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1179 1179 streamclone.maybeperformlegacystreamclone(pullop)
1180 1180 # This should ideally be in _pullbundle2(). However, it needs to run
1181 1181 # before discovery to avoid extra work.
1182 1182 _maybeapplyclonebundle(pullop)
1183 1183 _pulldiscovery(pullop)
1184 1184 if pullop.canusebundle2:
1185 1185 _pullbundle2(pullop)
1186 1186 _pullchangeset(pullop)
1187 1187 _pullphase(pullop)
1188 1188 _pullbookmarks(pullop)
1189 1189 _pullobsolete(pullop)
1190 1190 pullop.trmanager.close()
1191 1191 finally:
1192 1192 pullop.trmanager.release()
1193 1193 lock.release()
1194 1194
1195 1195 return pullop
1196 1196
1197 1197 # list of steps to perform discovery before pull
1198 1198 pulldiscoveryorder = []
1199 1199
1200 1200 # Mapping between step name and function
1201 1201 #
1202 1202 # This exists to help extensions wrap steps if necessary
1203 1203 pulldiscoverymapping = {}
1204 1204
1205 1205 def pulldiscovery(stepname):
1206 1206 """decorator for function performing discovery before pull
1207 1207
1208 1208 The function is added to the step -> function mapping and appended to the
1209 1209 list of steps. Beware that decorated function will be added in order (this
1210 1210 may matter).
1211 1211
1212 1212 You can only use this decorator for a new step, if you want to wrap a step
1213 1213 from an extension, change the pulldiscovery dictionary directly."""
1214 1214 def dec(func):
1215 1215 assert stepname not in pulldiscoverymapping
1216 1216 pulldiscoverymapping[stepname] = func
1217 1217 pulldiscoveryorder.append(stepname)
1218 1218 return func
1219 1219 return dec
1220 1220
1221 1221 def _pulldiscovery(pullop):
1222 1222 """Run all discovery steps"""
1223 1223 for stepname in pulldiscoveryorder:
1224 1224 step = pulldiscoverymapping[stepname]
1225 1225 step(pullop)
1226 1226
1227 1227 @pulldiscovery('b1:bookmarks')
1228 1228 def _pullbookmarkbundle1(pullop):
1229 1229 """fetch bookmark data in bundle1 case
1230 1230
1231 1231 If not using bundle2, we have to fetch bookmarks before changeset
1232 1232 discovery to reduce the chance and impact of race conditions."""
1233 1233 if pullop.remotebookmarks is not None:
1234 1234 return
1235 1235 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1236 1236 # all known bundle2 servers now support listkeys, but lets be nice with
1237 1237 # new implementation.
1238 1238 return
1239 1239 pullop.remotebookmarks = pullop.remote.listkeys('bookmarks')
1240 1240
1241 1241
1242 1242 @pulldiscovery('changegroup')
1243 1243 def _pulldiscoverychangegroup(pullop):
1244 1244 """discovery phase for the pull
1245 1245
1246 1246 Current handle changeset discovery only, will change handle all discovery
1247 1247 at some point."""
1248 1248 tmp = discovery.findcommonincoming(pullop.repo,
1249 1249 pullop.remote,
1250 1250 heads=pullop.heads,
1251 1251 force=pullop.force)
1252 1252 common, fetch, rheads = tmp
1253 1253 nm = pullop.repo.unfiltered().changelog.nodemap
1254 1254 if fetch and rheads:
1255 1255 # If a remote heads in filtered locally, lets drop it from the unknown
1256 1256 # remote heads and put in back in common.
1257 1257 #
1258 1258 # This is a hackish solution to catch most of "common but locally
1259 1259 # hidden situation". We do not performs discovery on unfiltered
1260 1260 # repository because it end up doing a pathological amount of round
1261 1261 # trip for w huge amount of changeset we do not care about.
1262 1262 #
1263 1263 # If a set of such "common but filtered" changeset exist on the server
1264 1264 # but are not including a remote heads, we'll not be able to detect it,
1265 1265 scommon = set(common)
1266 1266 filteredrheads = []
1267 1267 for n in rheads:
1268 1268 if n in nm:
1269 1269 if n not in scommon:
1270 1270 common.append(n)
1271 1271 else:
1272 1272 filteredrheads.append(n)
1273 1273 if not filteredrheads:
1274 1274 fetch = []
1275 1275 rheads = filteredrheads
1276 1276 pullop.common = common
1277 1277 pullop.fetch = fetch
1278 1278 pullop.rheads = rheads
1279 1279
1280 1280 def _pullbundle2(pullop):
1281 1281 """pull data using bundle2
1282 1282
1283 1283 For now, the only supported data are changegroup."""
1284 1284 kwargs = {'bundlecaps': caps20to10(pullop.repo)}
1285 1285
1286 1286 streaming, streamreqs = streamclone.canperformstreamclone(pullop)
1287 1287
1288 1288 # pulling changegroup
1289 1289 pullop.stepsdone.add('changegroup')
1290 1290
1291 1291 kwargs['common'] = pullop.common
1292 1292 kwargs['heads'] = pullop.heads or pullop.rheads
1293 1293 kwargs['cg'] = pullop.fetch
1294 1294 if 'listkeys' in pullop.remotebundle2caps:
1295 1295 kwargs['listkeys'] = ['phases']
1296 1296 if pullop.remotebookmarks is None:
1297 1297 # make sure to always includes bookmark data when migrating
1298 1298 # `hg incoming --bundle` to using this function.
1299 1299 kwargs['listkeys'].append('bookmarks')
1300 1300
1301 1301 # If this is a full pull / clone and the server supports the clone bundles
1302 1302 # feature, tell the server whether we attempted a clone bundle. The
1303 1303 # presence of this flag indicates the client supports clone bundles. This
1304 1304 # will enable the server to treat clients that support clone bundles
1305 1305 # differently from those that don't.
1306 1306 if (pullop.remote.capable('clonebundles')
1307 1307 and pullop.heads is None and list(pullop.common) == [nullid]):
1308 1308 kwargs['cbattempted'] = pullop.clonebundleattempted
1309 1309
1310 1310 if streaming:
1311 1311 pullop.repo.ui.status(_('streaming all changes\n'))
1312 1312 elif not pullop.fetch:
1313 1313 pullop.repo.ui.status(_("no changes found\n"))
1314 1314 pullop.cgresult = 0
1315 1315 else:
1316 1316 if pullop.heads is None and list(pullop.common) == [nullid]:
1317 1317 pullop.repo.ui.status(_("requesting all changes\n"))
1318 1318 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1319 1319 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1320 1320 if obsolete.commonversion(remoteversions) is not None:
1321 1321 kwargs['obsmarkers'] = True
1322 1322 pullop.stepsdone.add('obsmarkers')
1323 1323 _pullbundle2extraprepare(pullop, kwargs)
1324 1324 bundle = pullop.remote.getbundle('pull', **kwargs)
1325 1325 try:
1326 1326 op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
1327 1327 except error.BundleValueError as exc:
1328 raise error.Abort('missing support for %s' % exc)
1328 raise error.Abort(_('missing support for %s') % exc)
1329 1329
1330 1330 if pullop.fetch:
1331 1331 results = [cg['return'] for cg in op.records['changegroup']]
1332 1332 pullop.cgresult = changegroup.combineresults(results)
1333 1333
1334 1334 # processing phases change
1335 1335 for namespace, value in op.records['listkeys']:
1336 1336 if namespace == 'phases':
1337 1337 _pullapplyphases(pullop, value)
1338 1338
1339 1339 # processing bookmark update
1340 1340 for namespace, value in op.records['listkeys']:
1341 1341 if namespace == 'bookmarks':
1342 1342 pullop.remotebookmarks = value
1343 1343
1344 1344 # bookmark data were either already there or pulled in the bundle
1345 1345 if pullop.remotebookmarks is not None:
1346 1346 _pullbookmarks(pullop)
1347 1347
1348 1348 def _pullbundle2extraprepare(pullop, kwargs):
1349 1349 """hook function so that extensions can extend the getbundle call"""
1350 1350 pass
1351 1351
1352 1352 def _pullchangeset(pullop):
1353 1353 """pull changeset from unbundle into the local repo"""
1354 1354 # We delay the open of the transaction as late as possible so we
1355 1355 # don't open transaction for nothing or you break future useful
1356 1356 # rollback call
1357 1357 if 'changegroup' in pullop.stepsdone:
1358 1358 return
1359 1359 pullop.stepsdone.add('changegroup')
1360 1360 if not pullop.fetch:
1361 1361 pullop.repo.ui.status(_("no changes found\n"))
1362 1362 pullop.cgresult = 0
1363 1363 return
1364 1364 pullop.gettransaction()
1365 1365 if pullop.heads is None and list(pullop.common) == [nullid]:
1366 1366 pullop.repo.ui.status(_("requesting all changes\n"))
1367 1367 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1368 1368 # issue1320, avoid a race if remote changed after discovery
1369 1369 pullop.heads = pullop.rheads
1370 1370
1371 1371 if pullop.remote.capable('getbundle'):
1372 1372 # TODO: get bundlecaps from remote
1373 1373 cg = pullop.remote.getbundle('pull', common=pullop.common,
1374 1374 heads=pullop.heads or pullop.rheads)
1375 1375 elif pullop.heads is None:
1376 1376 cg = pullop.remote.changegroup(pullop.fetch, 'pull')
1377 1377 elif not pullop.remote.capable('changegroupsubset'):
1378 1378 raise error.Abort(_("partial pull cannot be done because "
1379 1379 "other repository doesn't support "
1380 1380 "changegroupsubset."))
1381 1381 else:
1382 1382 cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
1383 1383 pullop.cgresult = cg.apply(pullop.repo, 'pull', pullop.remote.url())
1384 1384
1385 1385 def _pullphase(pullop):
1386 1386 # Get remote phases data from remote
1387 1387 if 'phases' in pullop.stepsdone:
1388 1388 return
1389 1389 remotephases = pullop.remote.listkeys('phases')
1390 1390 _pullapplyphases(pullop, remotephases)
1391 1391
1392 1392 def _pullapplyphases(pullop, remotephases):
1393 1393 """apply phase movement from observed remote state"""
1394 1394 if 'phases' in pullop.stepsdone:
1395 1395 return
1396 1396 pullop.stepsdone.add('phases')
1397 1397 publishing = bool(remotephases.get('publishing', False))
1398 1398 if remotephases and not publishing:
1399 1399 # remote is new and unpublishing
1400 1400 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1401 1401 pullop.pulledsubset,
1402 1402 remotephases)
1403 1403 dheads = pullop.pulledsubset
1404 1404 else:
1405 1405 # Remote is old or publishing all common changesets
1406 1406 # should be seen as public
1407 1407 pheads = pullop.pulledsubset
1408 1408 dheads = []
1409 1409 unfi = pullop.repo.unfiltered()
1410 1410 phase = unfi._phasecache.phase
1411 1411 rev = unfi.changelog.nodemap.get
1412 1412 public = phases.public
1413 1413 draft = phases.draft
1414 1414
1415 1415 # exclude changesets already public locally and update the others
1416 1416 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1417 1417 if pheads:
1418 1418 tr = pullop.gettransaction()
1419 1419 phases.advanceboundary(pullop.repo, tr, public, pheads)
1420 1420
1421 1421 # exclude changesets already draft locally and update the others
1422 1422 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1423 1423 if dheads:
1424 1424 tr = pullop.gettransaction()
1425 1425 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1426 1426
1427 1427 def _pullbookmarks(pullop):
1428 1428 """process the remote bookmark information to update the local one"""
1429 1429 if 'bookmarks' in pullop.stepsdone:
1430 1430 return
1431 1431 pullop.stepsdone.add('bookmarks')
1432 1432 repo = pullop.repo
1433 1433 remotebookmarks = pullop.remotebookmarks
1434 1434 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1435 1435 pullop.remote.url(),
1436 1436 pullop.gettransaction,
1437 1437 explicit=pullop.explicitbookmarks)
1438 1438
1439 1439 def _pullobsolete(pullop):
1440 1440 """utility function to pull obsolete markers from a remote
1441 1441
1442 1442 The `gettransaction` is function that return the pull transaction, creating
1443 1443 one if necessary. We return the transaction to inform the calling code that
1444 1444 a new transaction have been created (when applicable).
1445 1445
1446 1446 Exists mostly to allow overriding for experimentation purpose"""
1447 1447 if 'obsmarkers' in pullop.stepsdone:
1448 1448 return
1449 1449 pullop.stepsdone.add('obsmarkers')
1450 1450 tr = None
1451 1451 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1452 1452 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1453 1453 remoteobs = pullop.remote.listkeys('obsolete')
1454 1454 if 'dump0' in remoteobs:
1455 1455 tr = pullop.gettransaction()
1456 1456 markers = []
1457 1457 for key in sorted(remoteobs, reverse=True):
1458 1458 if key.startswith('dump'):
1459 1459 data = base85.b85decode(remoteobs[key])
1460 1460 version, newmarks = obsolete._readmarkers(data)
1461 1461 markers += newmarks
1462 1462 if markers:
1463 1463 pullop.repo.obsstore.add(tr, markers)
1464 1464 pullop.repo.invalidatevolatilesets()
1465 1465 return tr
1466 1466
1467 1467 def caps20to10(repo):
1468 1468 """return a set with appropriate options to use bundle20 during getbundle"""
1469 1469 caps = set(['HG20'])
1470 1470 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
1471 1471 caps.add('bundle2=' + urlreq.quote(capsblob))
1472 1472 return caps
1473 1473
1474 1474 # List of names of steps to perform for a bundle2 for getbundle, order matters.
1475 1475 getbundle2partsorder = []
1476 1476
1477 1477 # Mapping between step name and function
1478 1478 #
1479 1479 # This exists to help extensions wrap steps if necessary
1480 1480 getbundle2partsmapping = {}
1481 1481
1482 1482 def getbundle2partsgenerator(stepname, idx=None):
1483 1483 """decorator for function generating bundle2 part for getbundle
1484 1484
1485 1485 The function is added to the step -> function mapping and appended to the
1486 1486 list of steps. Beware that decorated functions will be added in order
1487 1487 (this may matter).
1488 1488
1489 1489 You can only use this decorator for new steps, if you want to wrap a step
1490 1490 from an extension, attack the getbundle2partsmapping dictionary directly."""
1491 1491 def dec(func):
1492 1492 assert stepname not in getbundle2partsmapping
1493 1493 getbundle2partsmapping[stepname] = func
1494 1494 if idx is None:
1495 1495 getbundle2partsorder.append(stepname)
1496 1496 else:
1497 1497 getbundle2partsorder.insert(idx, stepname)
1498 1498 return func
1499 1499 return dec
1500 1500
1501 1501 def bundle2requested(bundlecaps):
1502 1502 if bundlecaps is not None:
1503 1503 return any(cap.startswith('HG2') for cap in bundlecaps)
1504 1504 return False
1505 1505
1506 1506 def getbundle(repo, source, heads=None, common=None, bundlecaps=None,
1507 1507 **kwargs):
1508 1508 """return a full bundle (with potentially multiple kind of parts)
1509 1509
1510 1510 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
1511 1511 passed. For now, the bundle can contain only changegroup, but this will
1512 1512 changes when more part type will be available for bundle2.
1513 1513
1514 1514 This is different from changegroup.getchangegroup that only returns an HG10
1515 1515 changegroup bundle. They may eventually get reunited in the future when we
1516 1516 have a clearer idea of the API we what to query different data.
1517 1517
1518 1518 The implementation is at a very early stage and will get massive rework
1519 1519 when the API of bundle is refined.
1520 1520 """
1521 1521 usebundle2 = bundle2requested(bundlecaps)
1522 1522 # bundle10 case
1523 1523 if not usebundle2:
1524 1524 if bundlecaps and not kwargs.get('cg', True):
1525 1525 raise ValueError(_('request for bundle10 must include changegroup'))
1526 1526
1527 1527 if kwargs:
1528 1528 raise ValueError(_('unsupported getbundle arguments: %s')
1529 1529 % ', '.join(sorted(kwargs.keys())))
1530 1530 return changegroup.getchangegroup(repo, source, heads=heads,
1531 1531 common=common, bundlecaps=bundlecaps)
1532 1532
1533 1533 # bundle20 case
1534 1534 b2caps = {}
1535 1535 for bcaps in bundlecaps:
1536 1536 if bcaps.startswith('bundle2='):
1537 1537 blob = urlreq.unquote(bcaps[len('bundle2='):])
1538 1538 b2caps.update(bundle2.decodecaps(blob))
1539 1539 bundler = bundle2.bundle20(repo.ui, b2caps)
1540 1540
1541 1541 kwargs['heads'] = heads
1542 1542 kwargs['common'] = common
1543 1543
1544 1544 for name in getbundle2partsorder:
1545 1545 func = getbundle2partsmapping[name]
1546 1546 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
1547 1547 **kwargs)
1548 1548
1549 1549 return util.chunkbuffer(bundler.getchunks())
1550 1550
1551 1551 @getbundle2partsgenerator('changegroup')
1552 1552 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
1553 1553 b2caps=None, heads=None, common=None, **kwargs):
1554 1554 """add a changegroup part to the requested bundle"""
1555 1555 cg = None
1556 1556 if kwargs.get('cg', True):
1557 1557 # build changegroup bundle here.
1558 1558 version = '01'
1559 1559 cgversions = b2caps.get('changegroup')
1560 1560 if cgversions: # 3.1 and 3.2 ship with an empty value
1561 1561 cgversions = [v for v in cgversions
1562 1562 if v in changegroup.supportedoutgoingversions(repo)]
1563 1563 if not cgversions:
1564 1564 raise ValueError(_('no common changegroup version'))
1565 1565 version = max(cgversions)
1566 1566 outgoing = changegroup.computeoutgoing(repo, heads, common)
1567 1567 cg = changegroup.getlocalchangegroupraw(repo, source, outgoing,
1568 1568 bundlecaps=bundlecaps,
1569 1569 version=version)
1570 1570
1571 1571 if cg:
1572 1572 part = bundler.newpart('changegroup', data=cg)
1573 1573 if cgversions:
1574 1574 part.addparam('version', version)
1575 1575 part.addparam('nbchanges', str(len(outgoing.missing)), mandatory=False)
1576 1576 if 'treemanifest' in repo.requirements:
1577 1577 part.addparam('treemanifest', '1')
1578 1578
1579 1579 @getbundle2partsgenerator('listkeys')
1580 1580 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
1581 1581 b2caps=None, **kwargs):
1582 1582 """add parts containing listkeys namespaces to the requested bundle"""
1583 1583 listkeys = kwargs.get('listkeys', ())
1584 1584 for namespace in listkeys:
1585 1585 part = bundler.newpart('listkeys')
1586 1586 part.addparam('namespace', namespace)
1587 1587 keys = repo.listkeys(namespace).items()
1588 1588 part.data = pushkey.encodekeys(keys)
1589 1589
1590 1590 @getbundle2partsgenerator('obsmarkers')
1591 1591 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
1592 1592 b2caps=None, heads=None, **kwargs):
1593 1593 """add an obsolescence markers part to the requested bundle"""
1594 1594 if kwargs.get('obsmarkers', False):
1595 1595 if heads is None:
1596 1596 heads = repo.heads()
1597 1597 subset = [c.node() for c in repo.set('::%ln', heads)]
1598 1598 markers = repo.obsstore.relevantmarkers(subset)
1599 1599 markers = sorted(markers)
1600 1600 buildobsmarkerspart(bundler, markers)
1601 1601
1602 1602 @getbundle2partsgenerator('hgtagsfnodes')
1603 1603 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
1604 1604 b2caps=None, heads=None, common=None,
1605 1605 **kwargs):
1606 1606 """Transfer the .hgtags filenodes mapping.
1607 1607
1608 1608 Only values for heads in this bundle will be transferred.
1609 1609
1610 1610 The part data consists of pairs of 20 byte changeset node and .hgtags
1611 1611 filenodes raw values.
1612 1612 """
1613 1613 # Don't send unless:
1614 1614 # - changeset are being exchanged,
1615 1615 # - the client supports it.
1616 1616 if not (kwargs.get('cg', True) and 'hgtagsfnodes' in b2caps):
1617 1617 return
1618 1618
1619 1619 outgoing = changegroup.computeoutgoing(repo, heads, common)
1620 1620
1621 1621 if not outgoing.missingheads:
1622 1622 return
1623 1623
1624 1624 cache = tags.hgtagsfnodescache(repo.unfiltered())
1625 1625 chunks = []
1626 1626
1627 1627 # .hgtags fnodes are only relevant for head changesets. While we could
1628 1628 # transfer values for all known nodes, there will likely be little to
1629 1629 # no benefit.
1630 1630 #
1631 1631 # We don't bother using a generator to produce output data because
1632 1632 # a) we only have 40 bytes per head and even esoteric numbers of heads
1633 1633 # consume little memory (1M heads is 40MB) b) we don't want to send the
1634 1634 # part if we don't have entries and knowing if we have entries requires
1635 1635 # cache lookups.
1636 1636 for node in outgoing.missingheads:
1637 1637 # Don't compute missing, as this may slow down serving.
1638 1638 fnode = cache.getfnode(node, computemissing=False)
1639 1639 if fnode is not None:
1640 1640 chunks.extend([node, fnode])
1641 1641
1642 1642 if chunks:
1643 1643 bundler.newpart('hgtagsfnodes', data=''.join(chunks))
1644 1644
1645 1645 def check_heads(repo, their_heads, context):
1646 1646 """check if the heads of a repo have been modified
1647 1647
1648 1648 Used by peer for unbundling.
1649 1649 """
1650 1650 heads = repo.heads()
1651 1651 heads_hash = hashlib.sha1(''.join(sorted(heads))).digest()
1652 1652 if not (their_heads == ['force'] or their_heads == heads or
1653 1653 their_heads == ['hashed', heads_hash]):
1654 1654 # someone else committed/pushed/unbundled while we
1655 1655 # were transferring data
1656 1656 raise error.PushRaced('repository changed while %s - '
1657 1657 'please try again' % context)
1658 1658
1659 1659 def unbundle(repo, cg, heads, source, url):
1660 1660 """Apply a bundle to a repo.
1661 1661
1662 1662 this function makes sure the repo is locked during the application and have
1663 1663 mechanism to check that no push race occurred between the creation of the
1664 1664 bundle and its application.
1665 1665
1666 1666 If the push was raced as PushRaced exception is raised."""
1667 1667 r = 0
1668 1668 # need a transaction when processing a bundle2 stream
1669 1669 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
1670 1670 lockandtr = [None, None, None]
1671 1671 recordout = None
1672 1672 # quick fix for output mismatch with bundle2 in 3.4
1673 1673 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture',
1674 1674 False)
1675 1675 if url.startswith('remote:http:') or url.startswith('remote:https:'):
1676 1676 captureoutput = True
1677 1677 try:
1678 1678 check_heads(repo, heads, 'uploading changes')
1679 1679 # push can proceed
1680 1680 if util.safehasattr(cg, 'params'):
1681 1681 r = None
1682 1682 try:
1683 1683 def gettransaction():
1684 1684 if not lockandtr[2]:
1685 1685 lockandtr[0] = repo.wlock()
1686 1686 lockandtr[1] = repo.lock()
1687 1687 lockandtr[2] = repo.transaction(source)
1688 1688 lockandtr[2].hookargs['source'] = source
1689 1689 lockandtr[2].hookargs['url'] = url
1690 1690 lockandtr[2].hookargs['bundle2'] = '1'
1691 1691 return lockandtr[2]
1692 1692
1693 1693 # Do greedy locking by default until we're satisfied with lazy
1694 1694 # locking.
1695 1695 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
1696 1696 gettransaction()
1697 1697
1698 1698 op = bundle2.bundleoperation(repo, gettransaction,
1699 1699 captureoutput=captureoutput)
1700 1700 try:
1701 1701 op = bundle2.processbundle(repo, cg, op=op)
1702 1702 finally:
1703 1703 r = op.reply
1704 1704 if captureoutput and r is not None:
1705 1705 repo.ui.pushbuffer(error=True, subproc=True)
1706 1706 def recordout(output):
1707 1707 r.newpart('output', data=output, mandatory=False)
1708 1708 if lockandtr[2] is not None:
1709 1709 lockandtr[2].close()
1710 1710 except BaseException as exc:
1711 1711 exc.duringunbundle2 = True
1712 1712 if captureoutput and r is not None:
1713 1713 parts = exc._bundle2salvagedoutput = r.salvageoutput()
1714 1714 def recordout(output):
1715 1715 part = bundle2.bundlepart('output', data=output,
1716 1716 mandatory=False)
1717 1717 parts.append(part)
1718 1718 raise
1719 1719 else:
1720 1720 lockandtr[1] = repo.lock()
1721 1721 r = cg.apply(repo, source, url)
1722 1722 finally:
1723 1723 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
1724 1724 if recordout is not None:
1725 1725 recordout(repo.ui.popbuffer())
1726 1726 return r
1727 1727
1728 1728 def _maybeapplyclonebundle(pullop):
1729 1729 """Apply a clone bundle from a remote, if possible."""
1730 1730
1731 1731 repo = pullop.repo
1732 1732 remote = pullop.remote
1733 1733
1734 1734 if not repo.ui.configbool('ui', 'clonebundles', True):
1735 1735 return
1736 1736
1737 1737 # Only run if local repo is empty.
1738 1738 if len(repo):
1739 1739 return
1740 1740
1741 1741 if pullop.heads:
1742 1742 return
1743 1743
1744 1744 if not remote.capable('clonebundles'):
1745 1745 return
1746 1746
1747 1747 res = remote._call('clonebundles')
1748 1748
1749 1749 # If we call the wire protocol command, that's good enough to record the
1750 1750 # attempt.
1751 1751 pullop.clonebundleattempted = True
1752 1752
1753 1753 entries = parseclonebundlesmanifest(repo, res)
1754 1754 if not entries:
1755 1755 repo.ui.note(_('no clone bundles available on remote; '
1756 1756 'falling back to regular clone\n'))
1757 1757 return
1758 1758
1759 1759 entries = filterclonebundleentries(repo, entries)
1760 1760 if not entries:
1761 1761 # There is a thundering herd concern here. However, if a server
1762 1762 # operator doesn't advertise bundles appropriate for its clients,
1763 1763 # they deserve what's coming. Furthermore, from a client's
1764 1764 # perspective, no automatic fallback would mean not being able to
1765 1765 # clone!
1766 1766 repo.ui.warn(_('no compatible clone bundles available on server; '
1767 1767 'falling back to regular clone\n'))
1768 1768 repo.ui.warn(_('(you may want to report this to the server '
1769 1769 'operator)\n'))
1770 1770 return
1771 1771
1772 1772 entries = sortclonebundleentries(repo.ui, entries)
1773 1773
1774 1774 url = entries[0]['URL']
1775 1775 repo.ui.status(_('applying clone bundle from %s\n') % url)
1776 1776 if trypullbundlefromurl(repo.ui, repo, url):
1777 1777 repo.ui.status(_('finished applying clone bundle\n'))
1778 1778 # Bundle failed.
1779 1779 #
1780 1780 # We abort by default to avoid the thundering herd of
1781 1781 # clients flooding a server that was expecting expensive
1782 1782 # clone load to be offloaded.
1783 1783 elif repo.ui.configbool('ui', 'clonebundlefallback', False):
1784 1784 repo.ui.warn(_('falling back to normal clone\n'))
1785 1785 else:
1786 1786 raise error.Abort(_('error applying bundle'),
1787 1787 hint=_('if this error persists, consider contacting '
1788 1788 'the server operator or disable clone '
1789 1789 'bundles via '
1790 1790 '"--config ui.clonebundles=false"'))
1791 1791
1792 1792 def parseclonebundlesmanifest(repo, s):
1793 1793 """Parses the raw text of a clone bundles manifest.
1794 1794
1795 1795 Returns a list of dicts. The dicts have a ``URL`` key corresponding
1796 1796 to the URL and other keys are the attributes for the entry.
1797 1797 """
1798 1798 m = []
1799 1799 for line in s.splitlines():
1800 1800 fields = line.split()
1801 1801 if not fields:
1802 1802 continue
1803 1803 attrs = {'URL': fields[0]}
1804 1804 for rawattr in fields[1:]:
1805 1805 key, value = rawattr.split('=', 1)
1806 1806 key = urlreq.unquote(key)
1807 1807 value = urlreq.unquote(value)
1808 1808 attrs[key] = value
1809 1809
1810 1810 # Parse BUNDLESPEC into components. This makes client-side
1811 1811 # preferences easier to specify since you can prefer a single
1812 1812 # component of the BUNDLESPEC.
1813 1813 if key == 'BUNDLESPEC':
1814 1814 try:
1815 1815 comp, version, params = parsebundlespec(repo, value,
1816 1816 externalnames=True)
1817 1817 attrs['COMPRESSION'] = comp
1818 1818 attrs['VERSION'] = version
1819 1819 except error.InvalidBundleSpecification:
1820 1820 pass
1821 1821 except error.UnsupportedBundleSpecification:
1822 1822 pass
1823 1823
1824 1824 m.append(attrs)
1825 1825
1826 1826 return m
1827 1827
1828 1828 def filterclonebundleentries(repo, entries):
1829 1829 """Remove incompatible clone bundle manifest entries.
1830 1830
1831 1831 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
1832 1832 and returns a new list consisting of only the entries that this client
1833 1833 should be able to apply.
1834 1834
1835 1835 There is no guarantee we'll be able to apply all returned entries because
1836 1836 the metadata we use to filter on may be missing or wrong.
1837 1837 """
1838 1838 newentries = []
1839 1839 for entry in entries:
1840 1840 spec = entry.get('BUNDLESPEC')
1841 1841 if spec:
1842 1842 try:
1843 1843 parsebundlespec(repo, spec, strict=True)
1844 1844 except error.InvalidBundleSpecification as e:
1845 1845 repo.ui.debug(str(e) + '\n')
1846 1846 continue
1847 1847 except error.UnsupportedBundleSpecification as e:
1848 1848 repo.ui.debug('filtering %s because unsupported bundle '
1849 1849 'spec: %s\n' % (entry['URL'], str(e)))
1850 1850 continue
1851 1851
1852 1852 if 'REQUIRESNI' in entry and not sslutil.hassni:
1853 1853 repo.ui.debug('filtering %s because SNI not supported\n' %
1854 1854 entry['URL'])
1855 1855 continue
1856 1856
1857 1857 newentries.append(entry)
1858 1858
1859 1859 return newentries
1860 1860
1861 1861 def sortclonebundleentries(ui, entries):
1862 1862 prefers = ui.configlist('ui', 'clonebundleprefers', default=[])
1863 1863 if not prefers:
1864 1864 return list(entries)
1865 1865
1866 1866 prefers = [p.split('=', 1) for p in prefers]
1867 1867
1868 1868 # Our sort function.
1869 1869 def compareentry(a, b):
1870 1870 for prefkey, prefvalue in prefers:
1871 1871 avalue = a.get(prefkey)
1872 1872 bvalue = b.get(prefkey)
1873 1873
1874 1874 # Special case for b missing attribute and a matches exactly.
1875 1875 if avalue is not None and bvalue is None and avalue == prefvalue:
1876 1876 return -1
1877 1877
1878 1878 # Special case for a missing attribute and b matches exactly.
1879 1879 if bvalue is not None and avalue is None and bvalue == prefvalue:
1880 1880 return 1
1881 1881
1882 1882 # We can't compare unless attribute present on both.
1883 1883 if avalue is None or bvalue is None:
1884 1884 continue
1885 1885
1886 1886 # Same values should fall back to next attribute.
1887 1887 if avalue == bvalue:
1888 1888 continue
1889 1889
1890 1890 # Exact matches come first.
1891 1891 if avalue == prefvalue:
1892 1892 return -1
1893 1893 if bvalue == prefvalue:
1894 1894 return 1
1895 1895
1896 1896 # Fall back to next attribute.
1897 1897 continue
1898 1898
1899 1899 # If we got here we couldn't sort by attributes and prefers. Fall
1900 1900 # back to index order.
1901 1901 return 0
1902 1902
1903 1903 return sorted(entries, cmp=compareentry)
1904 1904
1905 1905 def trypullbundlefromurl(ui, repo, url):
1906 1906 """Attempt to apply a bundle from a URL."""
1907 1907 lock = repo.lock()
1908 1908 try:
1909 1909 tr = repo.transaction('bundleurl')
1910 1910 try:
1911 1911 try:
1912 1912 fh = urlmod.open(ui, url)
1913 1913 cg = readbundle(ui, fh, 'stream')
1914 1914
1915 1915 if isinstance(cg, bundle2.unbundle20):
1916 1916 bundle2.processbundle(repo, cg, lambda: tr)
1917 1917 elif isinstance(cg, streamclone.streamcloneapplier):
1918 1918 cg.apply(repo)
1919 1919 else:
1920 1920 cg.apply(repo, 'clonebundles', url)
1921 1921 tr.close()
1922 1922 return True
1923 1923 except urlerr.httperror as e:
1924 1924 ui.warn(_('HTTP error fetching bundle: %s\n') % str(e))
1925 1925 except urlerr.urlerror as e:
1926 1926 ui.warn(_('error fetching bundle: %s\n') % e.reason[1])
1927 1927
1928 1928 return False
1929 1929 finally:
1930 1930 tr.release()
1931 1931 finally:
1932 1932 lock.release()
@@ -1,1014 +1,1015 b''
1 1 # hg.py - repository classes for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import hashlib
13 13 import os
14 14 import shutil
15 15
16 16 from .i18n import _
17 17 from .node import nullid
18 18
19 19 from . import (
20 20 bookmarks,
21 21 bundlerepo,
22 22 cmdutil,
23 23 destutil,
24 24 discovery,
25 25 error,
26 26 exchange,
27 27 extensions,
28 28 httppeer,
29 29 localrepo,
30 30 lock,
31 31 merge as mergemod,
32 32 node,
33 33 phases,
34 34 repoview,
35 35 scmutil,
36 36 sshpeer,
37 37 statichttprepo,
38 38 ui as uimod,
39 39 unionrepo,
40 40 url,
41 41 util,
42 42 verify as verifymod,
43 43 )
44 44
45 45 release = lock.release
46 46
47 47 def _local(path):
48 48 path = util.expandpath(util.urllocalpath(path))
49 49 return (os.path.isfile(path) and bundlerepo or localrepo)
50 50
51 51 def addbranchrevs(lrepo, other, branches, revs):
52 52 peer = other.peer() # a courtesy to callers using a localrepo for other
53 53 hashbranch, branches = branches
54 54 if not hashbranch and not branches:
55 55 x = revs or None
56 56 if util.safehasattr(revs, 'first'):
57 57 y = revs.first()
58 58 elif revs:
59 59 y = revs[0]
60 60 else:
61 61 y = None
62 62 return x, y
63 63 if revs:
64 64 revs = list(revs)
65 65 else:
66 66 revs = []
67 67
68 68 if not peer.capable('branchmap'):
69 69 if branches:
70 70 raise error.Abort(_("remote branch lookup not supported"))
71 71 revs.append(hashbranch)
72 72 return revs, revs[0]
73 73 branchmap = peer.branchmap()
74 74
75 75 def primary(branch):
76 76 if branch == '.':
77 77 if not lrepo:
78 78 raise error.Abort(_("dirstate branch not accessible"))
79 79 branch = lrepo.dirstate.branch()
80 80 if branch in branchmap:
81 81 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
82 82 return True
83 83 else:
84 84 return False
85 85
86 86 for branch in branches:
87 87 if not primary(branch):
88 88 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
89 89 if hashbranch:
90 90 if not primary(hashbranch):
91 91 revs.append(hashbranch)
92 92 return revs, revs[0]
93 93
94 94 def parseurl(path, branches=None):
95 95 '''parse url#branch, returning (url, (branch, branches))'''
96 96
97 97 u = util.url(path)
98 98 branch = None
99 99 if u.fragment:
100 100 branch = u.fragment
101 101 u.fragment = None
102 102 return str(u), (branch, branches or [])
103 103
104 104 schemes = {
105 105 'bundle': bundlerepo,
106 106 'union': unionrepo,
107 107 'file': _local,
108 108 'http': httppeer,
109 109 'https': httppeer,
110 110 'ssh': sshpeer,
111 111 'static-http': statichttprepo,
112 112 }
113 113
114 114 def _peerlookup(path):
115 115 u = util.url(path)
116 116 scheme = u.scheme or 'file'
117 117 thing = schemes.get(scheme) or schemes['file']
118 118 try:
119 119 return thing(path)
120 120 except TypeError:
121 121 # we can't test callable(thing) because 'thing' can be an unloaded
122 122 # module that implements __call__
123 123 if not util.safehasattr(thing, 'instance'):
124 124 raise
125 125 return thing
126 126
127 127 def islocal(repo):
128 128 '''return true if repo (or path pointing to repo) is local'''
129 129 if isinstance(repo, str):
130 130 try:
131 131 return _peerlookup(repo).islocal(repo)
132 132 except AttributeError:
133 133 return False
134 134 return repo.local()
135 135
136 136 def openpath(ui, path):
137 137 '''open path with open if local, url.open if remote'''
138 138 pathurl = util.url(path, parsequery=False, parsefragment=False)
139 139 if pathurl.islocal():
140 140 return util.posixfile(pathurl.localpath(), 'rb')
141 141 else:
142 142 return url.open(ui, path)
143 143
144 144 # a list of (ui, repo) functions called for wire peer initialization
145 145 wirepeersetupfuncs = []
146 146
147 147 def _peerorrepo(ui, path, create=False):
148 148 """return a repository object for the specified path"""
149 149 obj = _peerlookup(path).instance(ui, path, create)
150 150 ui = getattr(obj, "ui", ui)
151 151 for name, module in extensions.extensions(ui):
152 152 hook = getattr(module, 'reposetup', None)
153 153 if hook:
154 154 hook(ui, obj)
155 155 if not obj.local():
156 156 for f in wirepeersetupfuncs:
157 157 f(ui, obj)
158 158 return obj
159 159
160 160 def repository(ui, path='', create=False):
161 161 """return a repository object for the specified path"""
162 162 peer = _peerorrepo(ui, path, create)
163 163 repo = peer.local()
164 164 if not repo:
165 165 raise error.Abort(_("repository '%s' is not local") %
166 166 (path or peer.url()))
167 167 return repo.filtered('visible')
168 168
169 169 def peer(uiorrepo, opts, path, create=False):
170 170 '''return a repository peer for the specified path'''
171 171 rui = remoteui(uiorrepo, opts)
172 172 return _peerorrepo(rui, path, create).peer()
173 173
174 174 def defaultdest(source):
175 175 '''return default destination of clone if none is given
176 176
177 177 >>> defaultdest('foo')
178 178 'foo'
179 179 >>> defaultdest('/foo/bar')
180 180 'bar'
181 181 >>> defaultdest('/')
182 182 ''
183 183 >>> defaultdest('')
184 184 ''
185 185 >>> defaultdest('http://example.org/')
186 186 ''
187 187 >>> defaultdest('http://example.org/foo/')
188 188 'foo'
189 189 '''
190 190 path = util.url(source).path
191 191 if not path:
192 192 return ''
193 193 return os.path.basename(os.path.normpath(path))
194 194
195 195 def share(ui, source, dest=None, update=True, bookmarks=True):
196 196 '''create a shared repository'''
197 197
198 198 if not islocal(source):
199 199 raise error.Abort(_('can only share local repositories'))
200 200
201 201 if not dest:
202 202 dest = defaultdest(source)
203 203 else:
204 204 dest = ui.expandpath(dest)
205 205
206 206 if isinstance(source, str):
207 207 origsource = ui.expandpath(source)
208 208 source, branches = parseurl(origsource)
209 209 srcrepo = repository(ui, source)
210 210 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
211 211 else:
212 212 srcrepo = source.local()
213 213 origsource = source = srcrepo.url()
214 214 checkout = None
215 215
216 216 sharedpath = srcrepo.sharedpath # if our source is already sharing
217 217
218 218 destwvfs = scmutil.vfs(dest, realpath=True)
219 219 destvfs = scmutil.vfs(os.path.join(destwvfs.base, '.hg'), realpath=True)
220 220
221 221 if destvfs.lexists():
222 222 raise error.Abort(_('destination already exists'))
223 223
224 224 if not destwvfs.isdir():
225 225 destwvfs.mkdir()
226 226 destvfs.makedir()
227 227
228 228 requirements = ''
229 229 try:
230 230 requirements = srcrepo.vfs.read('requires')
231 231 except IOError as inst:
232 232 if inst.errno != errno.ENOENT:
233 233 raise
234 234
235 235 requirements += 'shared\n'
236 236 destvfs.write('requires', requirements)
237 237 destvfs.write('sharedpath', sharedpath)
238 238
239 239 r = repository(ui, destwvfs.base)
240 240 postshare(srcrepo, r, bookmarks=bookmarks)
241 241 _postshareupdate(r, update, checkout=checkout)
242 242
243 243 def postshare(sourcerepo, destrepo, bookmarks=True):
244 244 """Called after a new shared repo is created.
245 245
246 246 The new repo only has a requirements file and pointer to the source.
247 247 This function configures additional shared data.
248 248
249 249 Extensions can wrap this function and write additional entries to
250 250 destrepo/.hg/shared to indicate additional pieces of data to be shared.
251 251 """
252 252 default = sourcerepo.ui.config('paths', 'default')
253 253 if default:
254 254 fp = destrepo.vfs("hgrc", "w", text=True)
255 255 fp.write("[paths]\n")
256 256 fp.write("default = %s\n" % default)
257 257 fp.close()
258 258
259 259 if bookmarks:
260 260 fp = destrepo.vfs('shared', 'w')
261 261 fp.write('bookmarks\n')
262 262 fp.close()
263 263
264 264 def _postshareupdate(repo, update, checkout=None):
265 265 """Maybe perform a working directory update after a shared repo is created.
266 266
267 267 ``update`` can be a boolean or a revision to update to.
268 268 """
269 269 if not update:
270 270 return
271 271
272 272 repo.ui.status(_("updating working directory\n"))
273 273 if update is not True:
274 274 checkout = update
275 275 for test in (checkout, 'default', 'tip'):
276 276 if test is None:
277 277 continue
278 278 try:
279 279 uprev = repo.lookup(test)
280 280 break
281 281 except error.RepoLookupError:
282 282 continue
283 283 _update(repo, uprev)
284 284
285 285 def copystore(ui, srcrepo, destpath):
286 286 '''copy files from store of srcrepo in destpath
287 287
288 288 returns destlock
289 289 '''
290 290 destlock = None
291 291 try:
292 292 hardlink = None
293 293 num = 0
294 294 closetopic = [None]
295 295 def prog(topic, pos):
296 296 if pos is None:
297 297 closetopic[0] = topic
298 298 else:
299 299 ui.progress(topic, pos + num)
300 300 srcpublishing = srcrepo.publishing()
301 301 srcvfs = scmutil.vfs(srcrepo.sharedpath)
302 302 dstvfs = scmutil.vfs(destpath)
303 303 for f in srcrepo.store.copylist():
304 304 if srcpublishing and f.endswith('phaseroots'):
305 305 continue
306 306 dstbase = os.path.dirname(f)
307 307 if dstbase and not dstvfs.exists(dstbase):
308 308 dstvfs.mkdir(dstbase)
309 309 if srcvfs.exists(f):
310 310 if f.endswith('data'):
311 311 # 'dstbase' may be empty (e.g. revlog format 0)
312 312 lockfile = os.path.join(dstbase, "lock")
313 313 # lock to avoid premature writing to the target
314 314 destlock = lock.lock(dstvfs, lockfile)
315 315 hardlink, n = util.copyfiles(srcvfs.join(f), dstvfs.join(f),
316 316 hardlink, progress=prog)
317 317 num += n
318 318 if hardlink:
319 319 ui.debug("linked %d files\n" % num)
320 320 if closetopic[0]:
321 321 ui.progress(closetopic[0], None)
322 322 else:
323 323 ui.debug("copied %d files\n" % num)
324 324 if closetopic[0]:
325 325 ui.progress(closetopic[0], None)
326 326 return destlock
327 327 except: # re-raises
328 328 release(destlock)
329 329 raise
330 330
331 331 def clonewithshare(ui, peeropts, sharepath, source, srcpeer, dest, pull=False,
332 332 rev=None, update=True, stream=False):
333 333 """Perform a clone using a shared repo.
334 334
335 335 The store for the repository will be located at <sharepath>/.hg. The
336 336 specified revisions will be cloned or pulled from "source". A shared repo
337 337 will be created at "dest" and a working copy will be created if "update" is
338 338 True.
339 339 """
340 340 revs = None
341 341 if rev:
342 342 if not srcpeer.capable('lookup'):
343 343 raise error.Abort(_("src repository does not support "
344 344 "revision lookup and so doesn't "
345 345 "support clone by revision"))
346 346 revs = [srcpeer.lookup(r) for r in rev]
347 347
348 348 # Obtain a lock before checking for or cloning the pooled repo otherwise
349 349 # 2 clients may race creating or populating it.
350 350 pooldir = os.path.dirname(sharepath)
351 351 # lock class requires the directory to exist.
352 352 try:
353 353 util.makedir(pooldir, False)
354 354 except OSError as e:
355 355 if e.errno != errno.EEXIST:
356 356 raise
357 357
358 358 poolvfs = scmutil.vfs(pooldir)
359 359 basename = os.path.basename(sharepath)
360 360
361 361 with lock.lock(poolvfs, '%s.lock' % basename):
362 362 if os.path.exists(sharepath):
363 363 ui.status(_('(sharing from existing pooled repository %s)\n') %
364 364 basename)
365 365 else:
366 366 ui.status(_('(sharing from new pooled repository %s)\n') % basename)
367 367 # Always use pull mode because hardlinks in share mode don't work
368 368 # well. Never update because working copies aren't necessary in
369 369 # share mode.
370 370 clone(ui, peeropts, source, dest=sharepath, pull=True,
371 371 rev=rev, update=False, stream=stream)
372 372
373 373 sharerepo = repository(ui, path=sharepath)
374 374 share(ui, sharerepo, dest=dest, update=False, bookmarks=False)
375 375
376 376 # We need to perform a pull against the dest repo to fetch bookmarks
377 377 # and other non-store data that isn't shared by default. In the case of
378 378 # non-existing shared repo, this means we pull from the remote twice. This
379 379 # is a bit weird. But at the time it was implemented, there wasn't an easy
380 380 # way to pull just non-changegroup data.
381 381 destrepo = repository(ui, path=dest)
382 382 exchange.pull(destrepo, srcpeer, heads=revs)
383 383
384 384 _postshareupdate(destrepo, update)
385 385
386 386 return srcpeer, peer(ui, peeropts, dest)
387 387
388 388 def clone(ui, peeropts, source, dest=None, pull=False, rev=None,
389 389 update=True, stream=False, branch=None, shareopts=None):
390 390 """Make a copy of an existing repository.
391 391
392 392 Create a copy of an existing repository in a new directory. The
393 393 source and destination are URLs, as passed to the repository
394 394 function. Returns a pair of repository peers, the source and
395 395 newly created destination.
396 396
397 397 The location of the source is added to the new repository's
398 398 .hg/hgrc file, as the default to be used for future pulls and
399 399 pushes.
400 400
401 401 If an exception is raised, the partly cloned/updated destination
402 402 repository will be deleted.
403 403
404 404 Arguments:
405 405
406 406 source: repository object or URL
407 407
408 408 dest: URL of destination repository to create (defaults to base
409 409 name of source repository)
410 410
411 411 pull: always pull from source repository, even in local case or if the
412 412 server prefers streaming
413 413
414 414 stream: stream raw data uncompressed from repository (fast over
415 415 LAN, slow over WAN)
416 416
417 417 rev: revision to clone up to (implies pull=True)
418 418
419 419 update: update working directory after clone completes, if
420 420 destination is local repository (True means update to default rev,
421 421 anything else is treated as a revision)
422 422
423 423 branch: branches to clone
424 424
425 425 shareopts: dict of options to control auto sharing behavior. The "pool" key
426 426 activates auto sharing mode and defines the directory for stores. The
427 427 "mode" key determines how to construct the directory name of the shared
428 428 repository. "identity" means the name is derived from the node of the first
429 429 changeset in the repository. "remote" means the name is derived from the
430 430 remote's path/URL. Defaults to "identity."
431 431 """
432 432
433 433 if isinstance(source, str):
434 434 origsource = ui.expandpath(source)
435 435 source, branch = parseurl(origsource, branch)
436 436 srcpeer = peer(ui, peeropts, source)
437 437 else:
438 438 srcpeer = source.peer() # in case we were called with a localrepo
439 439 branch = (None, branch or [])
440 440 origsource = source = srcpeer.url()
441 441 rev, checkout = addbranchrevs(srcpeer, srcpeer, branch, rev)
442 442
443 443 if dest is None:
444 444 dest = defaultdest(source)
445 445 if dest:
446 446 ui.status(_("destination directory: %s\n") % dest)
447 447 else:
448 448 dest = ui.expandpath(dest)
449 449
450 450 dest = util.urllocalpath(dest)
451 451 source = util.urllocalpath(source)
452 452
453 453 if not dest:
454 454 raise error.Abort(_("empty destination path is not valid"))
455 455
456 456 destvfs = scmutil.vfs(dest, expandpath=True)
457 457 if destvfs.lexists():
458 458 if not destvfs.isdir():
459 459 raise error.Abort(_("destination '%s' already exists") % dest)
460 460 elif destvfs.listdir():
461 461 raise error.Abort(_("destination '%s' is not empty") % dest)
462 462
463 463 shareopts = shareopts or {}
464 464 sharepool = shareopts.get('pool')
465 465 sharenamemode = shareopts.get('mode')
466 466 if sharepool and islocal(dest):
467 467 sharepath = None
468 468 if sharenamemode == 'identity':
469 469 # Resolve the name from the initial changeset in the remote
470 470 # repository. This returns nullid when the remote is empty. It
471 471 # raises RepoLookupError if revision 0 is filtered or otherwise
472 472 # not available. If we fail to resolve, sharing is not enabled.
473 473 try:
474 474 rootnode = srcpeer.lookup('0')
475 475 if rootnode != node.nullid:
476 476 sharepath = os.path.join(sharepool, node.hex(rootnode))
477 477 else:
478 478 ui.status(_('(not using pooled storage: '
479 479 'remote appears to be empty)\n'))
480 480 except error.RepoLookupError:
481 481 ui.status(_('(not using pooled storage: '
482 482 'unable to resolve identity of remote)\n'))
483 483 elif sharenamemode == 'remote':
484 484 sharepath = os.path.join(
485 485 sharepool, hashlib.sha1(source).hexdigest())
486 486 else:
487 raise error.Abort('unknown share naming mode: %s' % sharenamemode)
487 raise error.Abort(_('unknown share naming mode: %s') %
488 sharenamemode)
488 489
489 490 if sharepath:
490 491 return clonewithshare(ui, peeropts, sharepath, source, srcpeer,
491 492 dest, pull=pull, rev=rev, update=update,
492 493 stream=stream)
493 494
494 495 srclock = destlock = cleandir = None
495 496 srcrepo = srcpeer.local()
496 497 try:
497 498 abspath = origsource
498 499 if islocal(origsource):
499 500 abspath = os.path.abspath(util.urllocalpath(origsource))
500 501
501 502 if islocal(dest):
502 503 cleandir = dest
503 504
504 505 copy = False
505 506 if (srcrepo and srcrepo.cancopy() and islocal(dest)
506 507 and not phases.hassecret(srcrepo)):
507 508 copy = not pull and not rev
508 509
509 510 if copy:
510 511 try:
511 512 # we use a lock here because if we race with commit, we
512 513 # can end up with extra data in the cloned revlogs that's
513 514 # not pointed to by changesets, thus causing verify to
514 515 # fail
515 516 srclock = srcrepo.lock(wait=False)
516 517 except error.LockError:
517 518 copy = False
518 519
519 520 if copy:
520 521 srcrepo.hook('preoutgoing', throw=True, source='clone')
521 522 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
522 523 if not os.path.exists(dest):
523 524 os.mkdir(dest)
524 525 else:
525 526 # only clean up directories we create ourselves
526 527 cleandir = hgdir
527 528 try:
528 529 destpath = hgdir
529 530 util.makedir(destpath, notindexed=True)
530 531 except OSError as inst:
531 532 if inst.errno == errno.EEXIST:
532 533 cleandir = None
533 534 raise error.Abort(_("destination '%s' already exists")
534 535 % dest)
535 536 raise
536 537
537 538 destlock = copystore(ui, srcrepo, destpath)
538 539 # copy bookmarks over
539 540 srcbookmarks = srcrepo.join('bookmarks')
540 541 dstbookmarks = os.path.join(destpath, 'bookmarks')
541 542 if os.path.exists(srcbookmarks):
542 543 util.copyfile(srcbookmarks, dstbookmarks)
543 544
544 545 # Recomputing branch cache might be slow on big repos,
545 546 # so just copy it
546 547 def copybranchcache(fname):
547 548 srcbranchcache = srcrepo.join('cache/%s' % fname)
548 549 dstbranchcache = os.path.join(dstcachedir, fname)
549 550 if os.path.exists(srcbranchcache):
550 551 if not os.path.exists(dstcachedir):
551 552 os.mkdir(dstcachedir)
552 553 util.copyfile(srcbranchcache, dstbranchcache)
553 554
554 555 dstcachedir = os.path.join(destpath, 'cache')
555 556 # In local clones we're copying all nodes, not just served
556 557 # ones. Therefore copy all branch caches over.
557 558 copybranchcache('branch2')
558 559 for cachename in repoview.filtertable:
559 560 copybranchcache('branch2-%s' % cachename)
560 561
561 562 # we need to re-init the repo after manually copying the data
562 563 # into it
563 564 destpeer = peer(srcrepo, peeropts, dest)
564 565 srcrepo.hook('outgoing', source='clone',
565 566 node=node.hex(node.nullid))
566 567 else:
567 568 try:
568 569 destpeer = peer(srcrepo or ui, peeropts, dest, create=True)
569 570 # only pass ui when no srcrepo
570 571 except OSError as inst:
571 572 if inst.errno == errno.EEXIST:
572 573 cleandir = None
573 574 raise error.Abort(_("destination '%s' already exists")
574 575 % dest)
575 576 raise
576 577
577 578 revs = None
578 579 if rev:
579 580 if not srcpeer.capable('lookup'):
580 581 raise error.Abort(_("src repository does not support "
581 582 "revision lookup and so doesn't "
582 583 "support clone by revision"))
583 584 revs = [srcpeer.lookup(r) for r in rev]
584 585 checkout = revs[0]
585 586 local = destpeer.local()
586 587 if local:
587 588 if not stream:
588 589 if pull:
589 590 stream = False
590 591 else:
591 592 stream = None
592 593 # internal config: ui.quietbookmarkmove
593 594 quiet = local.ui.backupconfig('ui', 'quietbookmarkmove')
594 595 try:
595 596 local.ui.setconfig(
596 597 'ui', 'quietbookmarkmove', True, 'clone')
597 598 exchange.pull(local, srcpeer, revs,
598 599 streamclonerequested=stream)
599 600 finally:
600 601 local.ui.restoreconfig(quiet)
601 602 elif srcrepo:
602 603 exchange.push(srcrepo, destpeer, revs=revs,
603 604 bookmarks=srcrepo._bookmarks.keys())
604 605 else:
605 606 raise error.Abort(_("clone from remote to remote not supported")
606 607 )
607 608
608 609 cleandir = None
609 610
610 611 destrepo = destpeer.local()
611 612 if destrepo:
612 613 template = uimod.samplehgrcs['cloned']
613 614 fp = destrepo.vfs("hgrc", "w", text=True)
614 615 u = util.url(abspath)
615 616 u.passwd = None
616 617 defaulturl = str(u)
617 618 fp.write(template % defaulturl)
618 619 fp.close()
619 620
620 621 destrepo.ui.setconfig('paths', 'default', defaulturl, 'clone')
621 622
622 623 if update:
623 624 if update is not True:
624 625 checkout = srcpeer.lookup(update)
625 626 uprev = None
626 627 status = None
627 628 if checkout is not None:
628 629 try:
629 630 uprev = destrepo.lookup(checkout)
630 631 except error.RepoLookupError:
631 632 if update is not True:
632 633 try:
633 634 uprev = destrepo.lookup(update)
634 635 except error.RepoLookupError:
635 636 pass
636 637 if uprev is None:
637 638 try:
638 639 uprev = destrepo._bookmarks['@']
639 640 update = '@'
640 641 bn = destrepo[uprev].branch()
641 642 if bn == 'default':
642 643 status = _("updating to bookmark @\n")
643 644 else:
644 645 status = (_("updating to bookmark @ on branch %s\n")
645 646 % bn)
646 647 except KeyError:
647 648 try:
648 649 uprev = destrepo.branchtip('default')
649 650 except error.RepoLookupError:
650 651 uprev = destrepo.lookup('tip')
651 652 if not status:
652 653 bn = destrepo[uprev].branch()
653 654 status = _("updating to branch %s\n") % bn
654 655 destrepo.ui.status(status)
655 656 _update(destrepo, uprev)
656 657 if update in destrepo._bookmarks:
657 658 bookmarks.activate(destrepo, update)
658 659 finally:
659 660 release(srclock, destlock)
660 661 if cleandir is not None:
661 662 shutil.rmtree(cleandir, True)
662 663 if srcpeer is not None:
663 664 srcpeer.close()
664 665 return srcpeer, destpeer
665 666
666 667 def _showstats(repo, stats, quietempty=False):
667 668 if quietempty and not any(stats):
668 669 return
669 670 repo.ui.status(_("%d files updated, %d files merged, "
670 671 "%d files removed, %d files unresolved\n") % stats)
671 672
672 673 def updaterepo(repo, node, overwrite):
673 674 """Update the working directory to node.
674 675
675 676 When overwrite is set, changes are clobbered, merged else
676 677
677 678 returns stats (see pydoc mercurial.merge.applyupdates)"""
678 679 return mergemod.update(repo, node, False, overwrite,
679 680 labels=['working copy', 'destination'])
680 681
681 682 def update(repo, node, quietempty=False):
682 683 """update the working directory to node, merging linear changes"""
683 684 stats = updaterepo(repo, node, False)
684 685 _showstats(repo, stats, quietempty)
685 686 if stats[3]:
686 687 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
687 688 return stats[3] > 0
688 689
689 690 # naming conflict in clone()
690 691 _update = update
691 692
692 693 def clean(repo, node, show_stats=True, quietempty=False):
693 694 """forcibly switch the working directory to node, clobbering changes"""
694 695 stats = updaterepo(repo, node, True)
695 696 util.unlinkpath(repo.join('graftstate'), ignoremissing=True)
696 697 if show_stats:
697 698 _showstats(repo, stats, quietempty)
698 699 return stats[3] > 0
699 700
700 701 # naming conflict in updatetotally()
701 702 _clean = clean
702 703
703 704 def updatetotally(ui, repo, checkout, brev, clean=False, check=False):
704 705 """Update the working directory with extra care for non-file components
705 706
706 707 This takes care of non-file components below:
707 708
708 709 :bookmark: might be advanced or (in)activated
709 710
710 711 This takes arguments below:
711 712
712 713 :checkout: to which revision the working directory is updated
713 714 :brev: a name, which might be a bookmark to be activated after updating
714 715 :clean: whether changes in the working directory can be discarded
715 716 :check: whether changes in the working directory should be checked
716 717
717 718 This returns whether conflict is detected at updating or not.
718 719 """
719 720 with repo.wlock():
720 721 movemarkfrom = None
721 722 warndest = False
722 723 if checkout is None:
723 724 updata = destutil.destupdate(repo, clean=clean, check=check)
724 725 checkout, movemarkfrom, brev = updata
725 726 warndest = True
726 727
727 728 if clean:
728 729 ret = _clean(repo, checkout)
729 730 else:
730 731 ret = _update(repo, checkout)
731 732
732 733 if not ret and movemarkfrom:
733 734 if movemarkfrom == repo['.'].node():
734 735 pass # no-op update
735 736 elif bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
736 737 ui.status(_("updating bookmark %s\n") % repo._activebookmark)
737 738 else:
738 739 # this can happen with a non-linear update
739 740 ui.status(_("(leaving bookmark %s)\n") %
740 741 repo._activebookmark)
741 742 bookmarks.deactivate(repo)
742 743 elif brev in repo._bookmarks:
743 744 if brev != repo._activebookmark:
744 745 ui.status(_("(activating bookmark %s)\n") % brev)
745 746 bookmarks.activate(repo, brev)
746 747 elif brev:
747 748 if repo._activebookmark:
748 749 ui.status(_("(leaving bookmark %s)\n") %
749 750 repo._activebookmark)
750 751 bookmarks.deactivate(repo)
751 752
752 753 if warndest:
753 754 destutil.statusotherdests(ui, repo)
754 755
755 756 return ret
756 757
757 758 def merge(repo, node, force=None, remind=True, mergeforce=False):
758 759 """Branch merge with node, resolving changes. Return true if any
759 760 unresolved conflicts."""
760 761 stats = mergemod.update(repo, node, True, force, mergeforce=mergeforce)
761 762 _showstats(repo, stats)
762 763 if stats[3]:
763 764 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
764 765 "or 'hg update -C .' to abandon\n"))
765 766 elif remind:
766 767 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
767 768 return stats[3] > 0
768 769
769 770 def _incoming(displaychlist, subreporecurse, ui, repo, source,
770 771 opts, buffered=False):
771 772 """
772 773 Helper for incoming / gincoming.
773 774 displaychlist gets called with
774 775 (remoterepo, incomingchangesetlist, displayer) parameters,
775 776 and is supposed to contain only code that can't be unified.
776 777 """
777 778 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
778 779 other = peer(repo, opts, source)
779 780 ui.status(_('comparing with %s\n') % util.hidepassword(source))
780 781 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
781 782
782 783 if revs:
783 784 revs = [other.lookup(rev) for rev in revs]
784 785 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
785 786 revs, opts["bundle"], opts["force"])
786 787 try:
787 788 if not chlist:
788 789 ui.status(_("no changes found\n"))
789 790 return subreporecurse()
790 791
791 792 displayer = cmdutil.show_changeset(ui, other, opts, buffered)
792 793 displaychlist(other, chlist, displayer)
793 794 displayer.close()
794 795 finally:
795 796 cleanupfn()
796 797 subreporecurse()
797 798 return 0 # exit code is zero since we found incoming changes
798 799
799 800 def incoming(ui, repo, source, opts):
800 801 def subreporecurse():
801 802 ret = 1
802 803 if opts.get('subrepos'):
803 804 ctx = repo[None]
804 805 for subpath in sorted(ctx.substate):
805 806 sub = ctx.sub(subpath)
806 807 ret = min(ret, sub.incoming(ui, source, opts))
807 808 return ret
808 809
809 810 def display(other, chlist, displayer):
810 811 limit = cmdutil.loglimit(opts)
811 812 if opts.get('newest_first'):
812 813 chlist.reverse()
813 814 count = 0
814 815 for n in chlist:
815 816 if limit is not None and count >= limit:
816 817 break
817 818 parents = [p for p in other.changelog.parents(n) if p != nullid]
818 819 if opts.get('no_merges') and len(parents) == 2:
819 820 continue
820 821 count += 1
821 822 displayer.show(other[n])
822 823 return _incoming(display, subreporecurse, ui, repo, source, opts)
823 824
824 825 def _outgoing(ui, repo, dest, opts):
825 826 dest = ui.expandpath(dest or 'default-push', dest or 'default')
826 827 dest, branches = parseurl(dest, opts.get('branch'))
827 828 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
828 829 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
829 830 if revs:
830 831 revs = [repo.lookup(rev) for rev in scmutil.revrange(repo, revs)]
831 832
832 833 other = peer(repo, opts, dest)
833 834 outgoing = discovery.findcommonoutgoing(repo.unfiltered(), other, revs,
834 835 force=opts.get('force'))
835 836 o = outgoing.missing
836 837 if not o:
837 838 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
838 839 return o, other
839 840
840 841 def outgoing(ui, repo, dest, opts):
841 842 def recurse():
842 843 ret = 1
843 844 if opts.get('subrepos'):
844 845 ctx = repo[None]
845 846 for subpath in sorted(ctx.substate):
846 847 sub = ctx.sub(subpath)
847 848 ret = min(ret, sub.outgoing(ui, dest, opts))
848 849 return ret
849 850
850 851 limit = cmdutil.loglimit(opts)
851 852 o, other = _outgoing(ui, repo, dest, opts)
852 853 if not o:
853 854 cmdutil.outgoinghooks(ui, repo, other, opts, o)
854 855 return recurse()
855 856
856 857 if opts.get('newest_first'):
857 858 o.reverse()
858 859 displayer = cmdutil.show_changeset(ui, repo, opts)
859 860 count = 0
860 861 for n in o:
861 862 if limit is not None and count >= limit:
862 863 break
863 864 parents = [p for p in repo.changelog.parents(n) if p != nullid]
864 865 if opts.get('no_merges') and len(parents) == 2:
865 866 continue
866 867 count += 1
867 868 displayer.show(repo[n])
868 869 displayer.close()
869 870 cmdutil.outgoinghooks(ui, repo, other, opts, o)
870 871 recurse()
871 872 return 0 # exit code is zero since we found outgoing changes
872 873
873 874 def verify(repo):
874 875 """verify the consistency of a repository"""
875 876 ret = verifymod.verify(repo)
876 877
877 878 # Broken subrepo references in hidden csets don't seem worth worrying about,
878 879 # since they can't be pushed/pulled, and --hidden can be used if they are a
879 880 # concern.
880 881
881 882 # pathto() is needed for -R case
882 883 revs = repo.revs("filelog(%s)",
883 884 util.pathto(repo.root, repo.getcwd(), '.hgsubstate'))
884 885
885 886 if revs:
886 887 repo.ui.status(_('checking subrepo links\n'))
887 888 for rev in revs:
888 889 ctx = repo[rev]
889 890 try:
890 891 for subpath in ctx.substate:
891 892 try:
892 893 ret = (ctx.sub(subpath, allowcreate=False).verify()
893 894 or ret)
894 895 except error.RepoError as e:
895 896 repo.ui.warn(_('%s: %s\n') % (rev, e))
896 897 except Exception:
897 898 repo.ui.warn(_('.hgsubstate is corrupt in revision %s\n') %
898 899 node.short(ctx.node()))
899 900
900 901 return ret
901 902
902 903 def remoteui(src, opts):
903 904 'build a remote ui from ui or repo and opts'
904 905 if util.safehasattr(src, 'baseui'): # looks like a repository
905 906 dst = src.baseui.copy() # drop repo-specific config
906 907 src = src.ui # copy target options from repo
907 908 else: # assume it's a global ui object
908 909 dst = src.copy() # keep all global options
909 910
910 911 # copy ssh-specific options
911 912 for o in 'ssh', 'remotecmd':
912 913 v = opts.get(o) or src.config('ui', o)
913 914 if v:
914 915 dst.setconfig("ui", o, v, 'copied')
915 916
916 917 # copy bundle-specific options
917 918 r = src.config('bundle', 'mainreporoot')
918 919 if r:
919 920 dst.setconfig('bundle', 'mainreporoot', r, 'copied')
920 921
921 922 # copy selected local settings to the remote ui
922 923 for sect in ('auth', 'hostfingerprints', 'http_proxy'):
923 924 for key, val in src.configitems(sect):
924 925 dst.setconfig(sect, key, val, 'copied')
925 926 v = src.config('web', 'cacerts')
926 927 if v == '!':
927 928 dst.setconfig('web', 'cacerts', v, 'copied')
928 929 elif v:
929 930 dst.setconfig('web', 'cacerts', util.expandpath(v), 'copied')
930 931
931 932 return dst
932 933
933 934 # Files of interest
934 935 # Used to check if the repository has changed looking at mtime and size of
935 936 # these files.
936 937 foi = [('spath', '00changelog.i'),
937 938 ('spath', 'phaseroots'), # ! phase can change content at the same size
938 939 ('spath', 'obsstore'),
939 940 ('path', 'bookmarks'), # ! bookmark can change content at the same size
940 941 ]
941 942
942 943 class cachedlocalrepo(object):
943 944 """Holds a localrepository that can be cached and reused."""
944 945
945 946 def __init__(self, repo):
946 947 """Create a new cached repo from an existing repo.
947 948
948 949 We assume the passed in repo was recently created. If the
949 950 repo has changed between when it was created and when it was
950 951 turned into a cache, it may not refresh properly.
951 952 """
952 953 assert isinstance(repo, localrepo.localrepository)
953 954 self._repo = repo
954 955 self._state, self.mtime = self._repostate()
955 956 self._filtername = repo.filtername
956 957
957 958 def fetch(self):
958 959 """Refresh (if necessary) and return a repository.
959 960
960 961 If the cached instance is out of date, it will be recreated
961 962 automatically and returned.
962 963
963 964 Returns a tuple of the repo and a boolean indicating whether a new
964 965 repo instance was created.
965 966 """
966 967 # We compare the mtimes and sizes of some well-known files to
967 968 # determine if the repo changed. This is not precise, as mtimes
968 969 # are susceptible to clock skew and imprecise filesystems and
969 970 # file content can change while maintaining the same size.
970 971
971 972 state, mtime = self._repostate()
972 973 if state == self._state:
973 974 return self._repo, False
974 975
975 976 repo = repository(self._repo.baseui, self._repo.url())
976 977 if self._filtername:
977 978 self._repo = repo.filtered(self._filtername)
978 979 else:
979 980 self._repo = repo.unfiltered()
980 981 self._state = state
981 982 self.mtime = mtime
982 983
983 984 return self._repo, True
984 985
985 986 def _repostate(self):
986 987 state = []
987 988 maxmtime = -1
988 989 for attr, fname in foi:
989 990 prefix = getattr(self._repo, attr)
990 991 p = os.path.join(prefix, fname)
991 992 try:
992 993 st = os.stat(p)
993 994 except OSError:
994 995 st = os.stat(prefix)
995 996 state.append((st.st_mtime, st.st_size))
996 997 maxmtime = max(maxmtime, st.st_mtime)
997 998
998 999 return tuple(state), maxmtime
999 1000
1000 1001 def copy(self):
1001 1002 """Obtain a copy of this class instance.
1002 1003
1003 1004 A new localrepository instance is obtained. The new instance should be
1004 1005 completely independent of the original.
1005 1006 """
1006 1007 repo = repository(self._repo.baseui, self._repo.origroot)
1007 1008 if self._filtername:
1008 1009 repo = repo.filtered(self._filtername)
1009 1010 else:
1010 1011 repo = repo.unfiltered()
1011 1012 c = cachedlocalrepo(repo)
1012 1013 c._state = self._state
1013 1014 c.mtime = self.mtime
1014 1015 return c
@@ -1,1096 +1,1096 b''
1 1 # manifest.py - manifest revision 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 array
11 11 import heapq
12 12 import os
13 13 import struct
14 14
15 15 from .i18n import _
16 16 from . import (
17 17 error,
18 18 mdiff,
19 19 parsers,
20 20 revlog,
21 21 util,
22 22 )
23 23
24 24 propertycache = util.propertycache
25 25
26 26 def _parsev1(data):
27 27 # This method does a little bit of excessive-looking
28 28 # precondition checking. This is so that the behavior of this
29 29 # class exactly matches its C counterpart to try and help
30 30 # prevent surprise breakage for anyone that develops against
31 31 # the pure version.
32 32 if data and data[-1] != '\n':
33 33 raise ValueError('Manifest did not end in a newline.')
34 34 prev = None
35 35 for l in data.splitlines():
36 36 if prev is not None and prev > l:
37 37 raise ValueError('Manifest lines not in sorted order.')
38 38 prev = l
39 39 f, n = l.split('\0')
40 40 if len(n) > 40:
41 41 yield f, revlog.bin(n[:40]), n[40:]
42 42 else:
43 43 yield f, revlog.bin(n), ''
44 44
45 45 def _parsev2(data):
46 46 metadataend = data.find('\n')
47 47 # Just ignore metadata for now
48 48 pos = metadataend + 1
49 49 prevf = ''
50 50 while pos < len(data):
51 51 end = data.find('\n', pos + 1) # +1 to skip stem length byte
52 52 if end == -1:
53 53 raise ValueError('Manifest ended with incomplete file entry.')
54 54 stemlen = ord(data[pos])
55 55 items = data[pos + 1:end].split('\0')
56 56 f = prevf[:stemlen] + items[0]
57 57 if prevf > f:
58 58 raise ValueError('Manifest entries not in sorted order.')
59 59 fl = items[1]
60 60 # Just ignore metadata (items[2:] for now)
61 61 n = data[end + 1:end + 21]
62 62 yield f, n, fl
63 63 pos = end + 22
64 64 prevf = f
65 65
66 66 def _parse(data):
67 67 """Generates (path, node, flags) tuples from a manifest text"""
68 68 if data.startswith('\0'):
69 69 return iter(_parsev2(data))
70 70 else:
71 71 return iter(_parsev1(data))
72 72
73 73 def _text(it, usemanifestv2):
74 74 """Given an iterator over (path, node, flags) tuples, returns a manifest
75 75 text"""
76 76 if usemanifestv2:
77 77 return _textv2(it)
78 78 else:
79 79 return _textv1(it)
80 80
81 81 def _textv1(it):
82 82 files = []
83 83 lines = []
84 84 _hex = revlog.hex
85 85 for f, n, fl in it:
86 86 files.append(f)
87 87 # if this is changed to support newlines in filenames,
88 88 # be sure to check the templates/ dir again (especially *-raw.tmpl)
89 89 lines.append("%s\0%s%s\n" % (f, _hex(n), fl))
90 90
91 91 _checkforbidden(files)
92 92 return ''.join(lines)
93 93
94 94 def _textv2(it):
95 95 files = []
96 96 lines = ['\0\n']
97 97 prevf = ''
98 98 for f, n, fl in it:
99 99 files.append(f)
100 100 stem = os.path.commonprefix([prevf, f])
101 101 stemlen = min(len(stem), 255)
102 102 lines.append("%c%s\0%s\n%s\n" % (stemlen, f[stemlen:], fl, n))
103 103 prevf = f
104 104 _checkforbidden(files)
105 105 return ''.join(lines)
106 106
107 107 class _lazymanifest(dict):
108 108 """This is the pure implementation of lazymanifest.
109 109
110 110 It has not been optimized *at all* and is not lazy.
111 111 """
112 112
113 113 def __init__(self, data):
114 114 dict.__init__(self)
115 115 for f, n, fl in _parse(data):
116 116 self[f] = n, fl
117 117
118 118 def __setitem__(self, k, v):
119 119 node, flag = v
120 120 assert node is not None
121 121 if len(node) > 21:
122 122 node = node[:21] # match c implementation behavior
123 123 dict.__setitem__(self, k, (node, flag))
124 124
125 125 def __iter__(self):
126 126 return iter(sorted(dict.keys(self)))
127 127
128 128 def iterkeys(self):
129 129 return iter(sorted(dict.keys(self)))
130 130
131 131 def iterentries(self):
132 132 return ((f, e[0], e[1]) for f, e in sorted(self.iteritems()))
133 133
134 134 def copy(self):
135 135 c = _lazymanifest('')
136 136 c.update(self)
137 137 return c
138 138
139 139 def diff(self, m2, clean=False):
140 140 '''Finds changes between the current manifest and m2.'''
141 141 diff = {}
142 142
143 143 for fn, e1 in self.iteritems():
144 144 if fn not in m2:
145 145 diff[fn] = e1, (None, '')
146 146 else:
147 147 e2 = m2[fn]
148 148 if e1 != e2:
149 149 diff[fn] = e1, e2
150 150 elif clean:
151 151 diff[fn] = None
152 152
153 153 for fn, e2 in m2.iteritems():
154 154 if fn not in self:
155 155 diff[fn] = (None, ''), e2
156 156
157 157 return diff
158 158
159 159 def filtercopy(self, filterfn):
160 160 c = _lazymanifest('')
161 161 for f, n, fl in self.iterentries():
162 162 if filterfn(f):
163 163 c[f] = n, fl
164 164 return c
165 165
166 166 def text(self):
167 167 """Get the full data of this manifest as a bytestring."""
168 168 return _textv1(self.iterentries())
169 169
170 170 try:
171 171 _lazymanifest = parsers.lazymanifest
172 172 except AttributeError:
173 173 pass
174 174
175 175 class manifestdict(object):
176 176 def __init__(self, data=''):
177 177 if data.startswith('\0'):
178 178 #_lazymanifest can not parse v2
179 179 self._lm = _lazymanifest('')
180 180 for f, n, fl in _parsev2(data):
181 181 self._lm[f] = n, fl
182 182 else:
183 183 self._lm = _lazymanifest(data)
184 184
185 185 def __getitem__(self, key):
186 186 return self._lm[key][0]
187 187
188 188 def find(self, key):
189 189 return self._lm[key]
190 190
191 191 def __len__(self):
192 192 return len(self._lm)
193 193
194 194 def __setitem__(self, key, node):
195 195 self._lm[key] = node, self.flags(key, '')
196 196
197 197 def __contains__(self, key):
198 198 return key in self._lm
199 199
200 200 def __delitem__(self, key):
201 201 del self._lm[key]
202 202
203 203 def __iter__(self):
204 204 return self._lm.__iter__()
205 205
206 206 def iterkeys(self):
207 207 return self._lm.iterkeys()
208 208
209 209 def keys(self):
210 210 return list(self.iterkeys())
211 211
212 212 def filesnotin(self, m2):
213 213 '''Set of files in this manifest that are not in the other'''
214 214 diff = self.diff(m2)
215 215 files = set(filepath
216 216 for filepath, hashflags in diff.iteritems()
217 217 if hashflags[1][0] is None)
218 218 return files
219 219
220 220 @propertycache
221 221 def _dirs(self):
222 222 return util.dirs(self)
223 223
224 224 def dirs(self):
225 225 return self._dirs
226 226
227 227 def hasdir(self, dir):
228 228 return dir in self._dirs
229 229
230 230 def _filesfastpath(self, match):
231 231 '''Checks whether we can correctly and quickly iterate over matcher
232 232 files instead of over manifest files.'''
233 233 files = match.files()
234 234 return (len(files) < 100 and (match.isexact() or
235 235 (match.prefix() and all(fn in self for fn in files))))
236 236
237 237 def walk(self, match):
238 238 '''Generates matching file names.
239 239
240 240 Equivalent to manifest.matches(match).iterkeys(), but without creating
241 241 an entirely new manifest.
242 242
243 243 It also reports nonexistent files by marking them bad with match.bad().
244 244 '''
245 245 if match.always():
246 246 for f in iter(self):
247 247 yield f
248 248 return
249 249
250 250 fset = set(match.files())
251 251
252 252 # avoid the entire walk if we're only looking for specific files
253 253 if self._filesfastpath(match):
254 254 for fn in sorted(fset):
255 255 yield fn
256 256 return
257 257
258 258 for fn in self:
259 259 if fn in fset:
260 260 # specified pattern is the exact name
261 261 fset.remove(fn)
262 262 if match(fn):
263 263 yield fn
264 264
265 265 # for dirstate.walk, files=['.'] means "walk the whole tree".
266 266 # follow that here, too
267 267 fset.discard('.')
268 268
269 269 for fn in sorted(fset):
270 270 if not self.hasdir(fn):
271 271 match.bad(fn, None)
272 272
273 273 def matches(self, match):
274 274 '''generate a new manifest filtered by the match argument'''
275 275 if match.always():
276 276 return self.copy()
277 277
278 278 if self._filesfastpath(match):
279 279 m = manifestdict()
280 280 lm = self._lm
281 281 for fn in match.files():
282 282 if fn in lm:
283 283 m._lm[fn] = lm[fn]
284 284 return m
285 285
286 286 m = manifestdict()
287 287 m._lm = self._lm.filtercopy(match)
288 288 return m
289 289
290 290 def diff(self, m2, clean=False):
291 291 '''Finds changes between the current manifest and m2.
292 292
293 293 Args:
294 294 m2: the manifest to which this manifest should be compared.
295 295 clean: if true, include files unchanged between these manifests
296 296 with a None value in the returned dictionary.
297 297
298 298 The result is returned as a dict with filename as key and
299 299 values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
300 300 nodeid in the current/other manifest and fl1/fl2 is the flag
301 301 in the current/other manifest. Where the file does not exist,
302 302 the nodeid will be None and the flags will be the empty
303 303 string.
304 304 '''
305 305 return self._lm.diff(m2._lm, clean)
306 306
307 307 def setflag(self, key, flag):
308 308 self._lm[key] = self[key], flag
309 309
310 310 def get(self, key, default=None):
311 311 try:
312 312 return self._lm[key][0]
313 313 except KeyError:
314 314 return default
315 315
316 316 def flags(self, key, default=''):
317 317 try:
318 318 return self._lm[key][1]
319 319 except KeyError:
320 320 return default
321 321
322 322 def copy(self):
323 323 c = manifestdict()
324 324 c._lm = self._lm.copy()
325 325 return c
326 326
327 327 def iteritems(self):
328 328 return (x[:2] for x in self._lm.iterentries())
329 329
330 330 def iterentries(self):
331 331 return self._lm.iterentries()
332 332
333 333 def text(self, usemanifestv2=False):
334 334 if usemanifestv2:
335 335 return _textv2(self._lm.iterentries())
336 336 else:
337 337 # use (probably) native version for v1
338 338 return self._lm.text()
339 339
340 340 def fastdelta(self, base, changes):
341 341 """Given a base manifest text as an array.array and a list of changes
342 342 relative to that text, compute a delta that can be used by revlog.
343 343 """
344 344 delta = []
345 345 dstart = None
346 346 dend = None
347 347 dline = [""]
348 348 start = 0
349 349 # zero copy representation of base as a buffer
350 350 addbuf = util.buffer(base)
351 351
352 352 changes = list(changes)
353 353 if len(changes) < 1000:
354 354 # start with a readonly loop that finds the offset of
355 355 # each line and creates the deltas
356 356 for f, todelete in changes:
357 357 # bs will either be the index of the item or the insert point
358 358 start, end = _msearch(addbuf, f, start)
359 359 if not todelete:
360 360 h, fl = self._lm[f]
361 361 l = "%s\0%s%s\n" % (f, revlog.hex(h), fl)
362 362 else:
363 363 if start == end:
364 364 # item we want to delete was not found, error out
365 365 raise AssertionError(
366 366 _("failed to remove %s from manifest") % f)
367 367 l = ""
368 368 if dstart is not None and dstart <= start and dend >= start:
369 369 if dend < end:
370 370 dend = end
371 371 if l:
372 372 dline.append(l)
373 373 else:
374 374 if dstart is not None:
375 375 delta.append([dstart, dend, "".join(dline)])
376 376 dstart = start
377 377 dend = end
378 378 dline = [l]
379 379
380 380 if dstart is not None:
381 381 delta.append([dstart, dend, "".join(dline)])
382 382 # apply the delta to the base, and get a delta for addrevision
383 383 deltatext, arraytext = _addlistdelta(base, delta)
384 384 else:
385 385 # For large changes, it's much cheaper to just build the text and
386 386 # diff it.
387 387 arraytext = array.array('c', self.text())
388 388 deltatext = mdiff.textdiff(base, arraytext)
389 389
390 390 return arraytext, deltatext
391 391
392 392 def _msearch(m, s, lo=0, hi=None):
393 393 '''return a tuple (start, end) that says where to find s within m.
394 394
395 395 If the string is found m[start:end] are the line containing
396 396 that string. If start == end the string was not found and
397 397 they indicate the proper sorted insertion point.
398 398
399 399 m should be a buffer or a string
400 400 s is a string'''
401 401 def advance(i, c):
402 402 while i < lenm and m[i] != c:
403 403 i += 1
404 404 return i
405 405 if not s:
406 406 return (lo, lo)
407 407 lenm = len(m)
408 408 if not hi:
409 409 hi = lenm
410 410 while lo < hi:
411 411 mid = (lo + hi) // 2
412 412 start = mid
413 413 while start > 0 and m[start - 1] != '\n':
414 414 start -= 1
415 415 end = advance(start, '\0')
416 416 if m[start:end] < s:
417 417 # we know that after the null there are 40 bytes of sha1
418 418 # this translates to the bisect lo = mid + 1
419 419 lo = advance(end + 40, '\n') + 1
420 420 else:
421 421 # this translates to the bisect hi = mid
422 422 hi = start
423 423 end = advance(lo, '\0')
424 424 found = m[lo:end]
425 425 if s == found:
426 426 # we know that after the null there are 40 bytes of sha1
427 427 end = advance(end + 40, '\n')
428 428 return (lo, end + 1)
429 429 else:
430 430 return (lo, lo)
431 431
432 432 def _checkforbidden(l):
433 433 """Check filenames for illegal characters."""
434 434 for f in l:
435 435 if '\n' in f or '\r' in f:
436 436 raise error.RevlogError(
437 437 _("'\\n' and '\\r' disallowed in filenames: %r") % f)
438 438
439 439
440 440 # apply the changes collected during the bisect loop to our addlist
441 441 # return a delta suitable for addrevision
442 442 def _addlistdelta(addlist, x):
443 443 # for large addlist arrays, building a new array is cheaper
444 444 # than repeatedly modifying the existing one
445 445 currentposition = 0
446 446 newaddlist = array.array('c')
447 447
448 448 for start, end, content in x:
449 449 newaddlist += addlist[currentposition:start]
450 450 if content:
451 451 newaddlist += array.array('c', content)
452 452
453 453 currentposition = end
454 454
455 455 newaddlist += addlist[currentposition:]
456 456
457 457 deltatext = "".join(struct.pack(">lll", start, end, len(content))
458 458 + content for start, end, content in x)
459 459 return deltatext, newaddlist
460 460
461 461 def _splittopdir(f):
462 462 if '/' in f:
463 463 dir, subpath = f.split('/', 1)
464 464 return dir + '/', subpath
465 465 else:
466 466 return '', f
467 467
468 468 _noop = lambda s: None
469 469
470 470 class treemanifest(object):
471 471 def __init__(self, dir='', text=''):
472 472 self._dir = dir
473 473 self._node = revlog.nullid
474 474 self._loadfunc = _noop
475 475 self._copyfunc = _noop
476 476 self._dirty = False
477 477 self._dirs = {}
478 478 # Using _lazymanifest here is a little slower than plain old dicts
479 479 self._files = {}
480 480 self._flags = {}
481 481 if text:
482 482 def readsubtree(subdir, subm):
483 483 raise AssertionError('treemanifest constructor only accepts '
484 484 'flat manifests')
485 485 self.parse(text, readsubtree)
486 486 self._dirty = True # Mark flat manifest dirty after parsing
487 487
488 488 def _subpath(self, path):
489 489 return self._dir + path
490 490
491 491 def __len__(self):
492 492 self._load()
493 493 size = len(self._files)
494 494 for m in self._dirs.values():
495 495 size += m.__len__()
496 496 return size
497 497
498 498 def _isempty(self):
499 499 self._load() # for consistency; already loaded by all callers
500 500 return (not self._files and (not self._dirs or
501 501 all(m._isempty() for m in self._dirs.values())))
502 502
503 503 def __repr__(self):
504 504 return ('<treemanifest dir=%s, node=%s, loaded=%s, dirty=%s at 0x%x>' %
505 505 (self._dir, revlog.hex(self._node),
506 506 bool(self._loadfunc is _noop),
507 507 self._dirty, id(self)))
508 508
509 509 def dir(self):
510 510 '''The directory that this tree manifest represents, including a
511 511 trailing '/'. Empty string for the repo root directory.'''
512 512 return self._dir
513 513
514 514 def node(self):
515 515 '''This node of this instance. nullid for unsaved instances. Should
516 516 be updated when the instance is read or written from a revlog.
517 517 '''
518 518 assert not self._dirty
519 519 return self._node
520 520
521 521 def setnode(self, node):
522 522 self._node = node
523 523 self._dirty = False
524 524
525 525 def iterentries(self):
526 526 self._load()
527 527 for p, n in sorted(self._dirs.items() + self._files.items()):
528 528 if p in self._files:
529 529 yield self._subpath(p), n, self._flags.get(p, '')
530 530 else:
531 531 for x in n.iterentries():
532 532 yield x
533 533
534 534 def iteritems(self):
535 535 self._load()
536 536 for p, n in sorted(self._dirs.items() + self._files.items()):
537 537 if p in self._files:
538 538 yield self._subpath(p), n
539 539 else:
540 540 for f, sn in n.iteritems():
541 541 yield f, sn
542 542
543 543 def iterkeys(self):
544 544 self._load()
545 545 for p in sorted(self._dirs.keys() + self._files.keys()):
546 546 if p in self._files:
547 547 yield self._subpath(p)
548 548 else:
549 549 for f in self._dirs[p].iterkeys():
550 550 yield f
551 551
552 552 def keys(self):
553 553 return list(self.iterkeys())
554 554
555 555 def __iter__(self):
556 556 return self.iterkeys()
557 557
558 558 def __contains__(self, f):
559 559 if f is None:
560 560 return False
561 561 self._load()
562 562 dir, subpath = _splittopdir(f)
563 563 if dir:
564 564 if dir not in self._dirs:
565 565 return False
566 566 return self._dirs[dir].__contains__(subpath)
567 567 else:
568 568 return f in self._files
569 569
570 570 def get(self, f, default=None):
571 571 self._load()
572 572 dir, subpath = _splittopdir(f)
573 573 if dir:
574 574 if dir not in self._dirs:
575 575 return default
576 576 return self._dirs[dir].get(subpath, default)
577 577 else:
578 578 return self._files.get(f, default)
579 579
580 580 def __getitem__(self, f):
581 581 self._load()
582 582 dir, subpath = _splittopdir(f)
583 583 if dir:
584 584 return self._dirs[dir].__getitem__(subpath)
585 585 else:
586 586 return self._files[f]
587 587
588 588 def flags(self, f):
589 589 self._load()
590 590 dir, subpath = _splittopdir(f)
591 591 if dir:
592 592 if dir not in self._dirs:
593 593 return ''
594 594 return self._dirs[dir].flags(subpath)
595 595 else:
596 596 if f in self._dirs:
597 597 return ''
598 598 return self._flags.get(f, '')
599 599
600 600 def find(self, f):
601 601 self._load()
602 602 dir, subpath = _splittopdir(f)
603 603 if dir:
604 604 return self._dirs[dir].find(subpath)
605 605 else:
606 606 return self._files[f], self._flags.get(f, '')
607 607
608 608 def __delitem__(self, f):
609 609 self._load()
610 610 dir, subpath = _splittopdir(f)
611 611 if dir:
612 612 self._dirs[dir].__delitem__(subpath)
613 613 # If the directory is now empty, remove it
614 614 if self._dirs[dir]._isempty():
615 615 del self._dirs[dir]
616 616 else:
617 617 del self._files[f]
618 618 if f in self._flags:
619 619 del self._flags[f]
620 620 self._dirty = True
621 621
622 622 def __setitem__(self, f, n):
623 623 assert n is not None
624 624 self._load()
625 625 dir, subpath = _splittopdir(f)
626 626 if dir:
627 627 if dir not in self._dirs:
628 628 self._dirs[dir] = treemanifest(self._subpath(dir))
629 629 self._dirs[dir].__setitem__(subpath, n)
630 630 else:
631 631 self._files[f] = n[:21] # to match manifestdict's behavior
632 632 self._dirty = True
633 633
634 634 def _load(self):
635 635 if self._loadfunc is not _noop:
636 636 lf, self._loadfunc = self._loadfunc, _noop
637 637 lf(self)
638 638 elif self._copyfunc is not _noop:
639 639 cf, self._copyfunc = self._copyfunc, _noop
640 640 cf(self)
641 641
642 642 def setflag(self, f, flags):
643 643 """Set the flags (symlink, executable) for path f."""
644 644 self._load()
645 645 dir, subpath = _splittopdir(f)
646 646 if dir:
647 647 if dir not in self._dirs:
648 648 self._dirs[dir] = treemanifest(self._subpath(dir))
649 649 self._dirs[dir].setflag(subpath, flags)
650 650 else:
651 651 self._flags[f] = flags
652 652 self._dirty = True
653 653
654 654 def copy(self):
655 655 copy = treemanifest(self._dir)
656 656 copy._node = self._node
657 657 copy._dirty = self._dirty
658 658 if self._copyfunc is _noop:
659 659 def _copyfunc(s):
660 660 self._load()
661 661 for d in self._dirs:
662 662 s._dirs[d] = self._dirs[d].copy()
663 663 s._files = dict.copy(self._files)
664 664 s._flags = dict.copy(self._flags)
665 665 if self._loadfunc is _noop:
666 666 _copyfunc(copy)
667 667 else:
668 668 copy._copyfunc = _copyfunc
669 669 else:
670 670 copy._copyfunc = self._copyfunc
671 671 return copy
672 672
673 673 def filesnotin(self, m2):
674 674 '''Set of files in this manifest that are not in the other'''
675 675 files = set()
676 676 def _filesnotin(t1, t2):
677 677 if t1._node == t2._node and not t1._dirty and not t2._dirty:
678 678 return
679 679 t1._load()
680 680 t2._load()
681 681 for d, m1 in t1._dirs.iteritems():
682 682 if d in t2._dirs:
683 683 m2 = t2._dirs[d]
684 684 _filesnotin(m1, m2)
685 685 else:
686 686 files.update(m1.iterkeys())
687 687
688 688 for fn in t1._files.iterkeys():
689 689 if fn not in t2._files:
690 690 files.add(t1._subpath(fn))
691 691
692 692 _filesnotin(self, m2)
693 693 return files
694 694
695 695 @propertycache
696 696 def _alldirs(self):
697 697 return util.dirs(self)
698 698
699 699 def dirs(self):
700 700 return self._alldirs
701 701
702 702 def hasdir(self, dir):
703 703 self._load()
704 704 topdir, subdir = _splittopdir(dir)
705 705 if topdir:
706 706 if topdir in self._dirs:
707 707 return self._dirs[topdir].hasdir(subdir)
708 708 return False
709 709 return (dir + '/') in self._dirs
710 710
711 711 def walk(self, match):
712 712 '''Generates matching file names.
713 713
714 714 Equivalent to manifest.matches(match).iterkeys(), but without creating
715 715 an entirely new manifest.
716 716
717 717 It also reports nonexistent files by marking them bad with match.bad().
718 718 '''
719 719 if match.always():
720 720 for f in iter(self):
721 721 yield f
722 722 return
723 723
724 724 fset = set(match.files())
725 725
726 726 for fn in self._walk(match):
727 727 if fn in fset:
728 728 # specified pattern is the exact name
729 729 fset.remove(fn)
730 730 yield fn
731 731
732 732 # for dirstate.walk, files=['.'] means "walk the whole tree".
733 733 # follow that here, too
734 734 fset.discard('.')
735 735
736 736 for fn in sorted(fset):
737 737 if not self.hasdir(fn):
738 738 match.bad(fn, None)
739 739
740 740 def _walk(self, match):
741 741 '''Recursively generates matching file names for walk().'''
742 742 if not match.visitdir(self._dir[:-1] or '.'):
743 743 return
744 744
745 745 # yield this dir's files and walk its submanifests
746 746 self._load()
747 747 for p in sorted(self._dirs.keys() + self._files.keys()):
748 748 if p in self._files:
749 749 fullp = self._subpath(p)
750 750 if match(fullp):
751 751 yield fullp
752 752 else:
753 753 for f in self._dirs[p]._walk(match):
754 754 yield f
755 755
756 756 def matches(self, match):
757 757 '''generate a new manifest filtered by the match argument'''
758 758 if match.always():
759 759 return self.copy()
760 760
761 761 return self._matches(match)
762 762
763 763 def _matches(self, match):
764 764 '''recursively generate a new manifest filtered by the match argument.
765 765 '''
766 766
767 767 visit = match.visitdir(self._dir[:-1] or '.')
768 768 if visit == 'all':
769 769 return self.copy()
770 770 ret = treemanifest(self._dir)
771 771 if not visit:
772 772 return ret
773 773
774 774 self._load()
775 775 for fn in self._files:
776 776 fullp = self._subpath(fn)
777 777 if not match(fullp):
778 778 continue
779 779 ret._files[fn] = self._files[fn]
780 780 if fn in self._flags:
781 781 ret._flags[fn] = self._flags[fn]
782 782
783 783 for dir, subm in self._dirs.iteritems():
784 784 m = subm._matches(match)
785 785 if not m._isempty():
786 786 ret._dirs[dir] = m
787 787
788 788 if not ret._isempty():
789 789 ret._dirty = True
790 790 return ret
791 791
792 792 def diff(self, m2, clean=False):
793 793 '''Finds changes between the current manifest and m2.
794 794
795 795 Args:
796 796 m2: the manifest to which this manifest should be compared.
797 797 clean: if true, include files unchanged between these manifests
798 798 with a None value in the returned dictionary.
799 799
800 800 The result is returned as a dict with filename as key and
801 801 values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
802 802 nodeid in the current/other manifest and fl1/fl2 is the flag
803 803 in the current/other manifest. Where the file does not exist,
804 804 the nodeid will be None and the flags will be the empty
805 805 string.
806 806 '''
807 807 result = {}
808 808 emptytree = treemanifest()
809 809 def _diff(t1, t2):
810 810 if t1._node == t2._node and not t1._dirty and not t2._dirty:
811 811 return
812 812 t1._load()
813 813 t2._load()
814 814 for d, m1 in t1._dirs.iteritems():
815 815 m2 = t2._dirs.get(d, emptytree)
816 816 _diff(m1, m2)
817 817
818 818 for d, m2 in t2._dirs.iteritems():
819 819 if d not in t1._dirs:
820 820 _diff(emptytree, m2)
821 821
822 822 for fn, n1 in t1._files.iteritems():
823 823 fl1 = t1._flags.get(fn, '')
824 824 n2 = t2._files.get(fn, None)
825 825 fl2 = t2._flags.get(fn, '')
826 826 if n1 != n2 or fl1 != fl2:
827 827 result[t1._subpath(fn)] = ((n1, fl1), (n2, fl2))
828 828 elif clean:
829 829 result[t1._subpath(fn)] = None
830 830
831 831 for fn, n2 in t2._files.iteritems():
832 832 if fn not in t1._files:
833 833 fl2 = t2._flags.get(fn, '')
834 834 result[t2._subpath(fn)] = ((None, ''), (n2, fl2))
835 835
836 836 _diff(self, m2)
837 837 return result
838 838
839 839 def unmodifiedsince(self, m2):
840 840 return not self._dirty and not m2._dirty and self._node == m2._node
841 841
842 842 def parse(self, text, readsubtree):
843 843 for f, n, fl in _parse(text):
844 844 if fl == 't':
845 845 f = f + '/'
846 846 self._dirs[f] = readsubtree(self._subpath(f), n)
847 847 elif '/' in f:
848 848 # This is a flat manifest, so use __setitem__ and setflag rather
849 849 # than assigning directly to _files and _flags, so we can
850 850 # assign a path in a subdirectory, and to mark dirty (compared
851 851 # to nullid).
852 852 self[f] = n
853 853 if fl:
854 854 self.setflag(f, fl)
855 855 else:
856 856 # Assigning to _files and _flags avoids marking as dirty,
857 857 # and should be a little faster.
858 858 self._files[f] = n
859 859 if fl:
860 860 self._flags[f] = fl
861 861
862 862 def text(self, usemanifestv2=False):
863 863 """Get the full data of this manifest as a bytestring."""
864 864 self._load()
865 865 return _text(self.iterentries(), usemanifestv2)
866 866
867 867 def dirtext(self, usemanifestv2=False):
868 868 """Get the full data of this directory as a bytestring. Make sure that
869 869 any submanifests have been written first, so their nodeids are correct.
870 870 """
871 871 self._load()
872 872 flags = self.flags
873 873 dirs = [(d[:-1], self._dirs[d]._node, 't') for d in self._dirs]
874 874 files = [(f, self._files[f], flags(f)) for f in self._files]
875 875 return _text(sorted(dirs + files), usemanifestv2)
876 876
877 877 def read(self, gettext, readsubtree):
878 878 def _load_for_read(s):
879 879 s.parse(gettext(), readsubtree)
880 880 s._dirty = False
881 881 self._loadfunc = _load_for_read
882 882
883 883 def writesubtrees(self, m1, m2, writesubtree):
884 884 self._load() # for consistency; should never have any effect here
885 885 emptytree = treemanifest()
886 886 for d, subm in self._dirs.iteritems():
887 887 subp1 = m1._dirs.get(d, emptytree)._node
888 888 subp2 = m2._dirs.get(d, emptytree)._node
889 889 if subp1 == revlog.nullid:
890 890 subp1, subp2 = subp2, subp1
891 891 writesubtree(subm, subp1, subp2)
892 892
893 893 class manifest(revlog.revlog):
894 894 def __init__(self, opener, dir='', dirlogcache=None):
895 895 '''The 'dir' and 'dirlogcache' arguments are for internal use by
896 896 manifest.manifest only. External users should create a root manifest
897 897 log with manifest.manifest(opener) and call dirlog() on it.
898 898 '''
899 899 # During normal operations, we expect to deal with not more than four
900 900 # revs at a time (such as during commit --amend). When rebasing large
901 901 # stacks of commits, the number can go up, hence the config knob below.
902 902 cachesize = 4
903 903 usetreemanifest = False
904 904 usemanifestv2 = False
905 905 opts = getattr(opener, 'options', None)
906 906 if opts is not None:
907 907 cachesize = opts.get('manifestcachesize', cachesize)
908 908 usetreemanifest = opts.get('treemanifest', usetreemanifest)
909 909 usemanifestv2 = opts.get('manifestv2', usemanifestv2)
910 910 self._mancache = util.lrucachedict(cachesize)
911 911 self._treeinmem = usetreemanifest
912 912 self._treeondisk = usetreemanifest
913 913 self._usemanifestv2 = usemanifestv2
914 914 indexfile = "00manifest.i"
915 915 if dir:
916 916 assert self._treeondisk
917 917 if not dir.endswith('/'):
918 918 dir = dir + '/'
919 919 indexfile = "meta/" + dir + "00manifest.i"
920 920 revlog.revlog.__init__(self, opener, indexfile)
921 921 self._dir = dir
922 922 # The dirlogcache is kept on the root manifest log
923 923 if dir:
924 924 self._dirlogcache = dirlogcache
925 925 else:
926 926 self._dirlogcache = {'': self}
927 927
928 928 def _newmanifest(self, data=''):
929 929 if self._treeinmem:
930 930 return treemanifest(self._dir, data)
931 931 return manifestdict(data)
932 932
933 933 def dirlog(self, dir):
934 934 if dir:
935 935 assert self._treeondisk
936 936 if dir not in self._dirlogcache:
937 937 self._dirlogcache[dir] = manifest(self.opener, dir,
938 938 self._dirlogcache)
939 939 return self._dirlogcache[dir]
940 940
941 941 def _slowreaddelta(self, node):
942 942 r0 = self.deltaparent(self.rev(node))
943 943 m0 = self.read(self.node(r0))
944 944 m1 = self.read(node)
945 945 md = self._newmanifest()
946 946 for f, ((n0, fl0), (n1, fl1)) in m0.diff(m1).iteritems():
947 947 if n1:
948 948 md[f] = n1
949 949 if fl1:
950 950 md.setflag(f, fl1)
951 951 return md
952 952
953 953 def readdelta(self, node):
954 954 if self._usemanifestv2 or self._treeondisk:
955 955 return self._slowreaddelta(node)
956 956 r = self.rev(node)
957 957 d = mdiff.patchtext(self.revdiff(self.deltaparent(r), r))
958 958 return self._newmanifest(d)
959 959
960 960 def readshallowdelta(self, node):
961 961 '''For flat manifests, this is the same as readdelta(). For
962 962 treemanifests, this will read the delta for this revlog's directory,
963 963 without recursively reading subdirectory manifests. Instead, any
964 964 subdirectory entry will be reported as it appears in the manifests, i.e.
965 965 the subdirectory will be reported among files and distinguished only by
966 966 its 't' flag.'''
967 967 if not self._treeondisk:
968 968 return self.readdelta(node)
969 969 if self._usemanifestv2:
970 970 raise error.Abort(
971 "readshallowdelta() not implemented for manifestv2")
971 _("readshallowdelta() not implemented for manifestv2"))
972 972 r = self.rev(node)
973 973 d = mdiff.patchtext(self.revdiff(self.deltaparent(r), r))
974 974 return manifestdict(d)
975 975
976 976 def readfast(self, node):
977 977 '''use the faster of readdelta or read
978 978
979 979 This will return a manifest which is either only the files
980 980 added/modified relative to p1, or all files in the
981 981 manifest. Which one is returned depends on the codepath used
982 982 to retrieve the data.
983 983 '''
984 984 r = self.rev(node)
985 985 deltaparent = self.deltaparent(r)
986 986 if deltaparent != revlog.nullrev and deltaparent in self.parentrevs(r):
987 987 return self.readdelta(node)
988 988 return self.read(node)
989 989
990 990 def readshallowfast(self, node):
991 991 '''like readfast(), but calls readshallowdelta() instead of readdelta()
992 992 '''
993 993 r = self.rev(node)
994 994 deltaparent = self.deltaparent(r)
995 995 if deltaparent != revlog.nullrev and deltaparent in self.parentrevs(r):
996 996 return self.readshallowdelta(node)
997 997 return self.readshallow(node)
998 998
999 999 def read(self, node):
1000 1000 if node == revlog.nullid:
1001 1001 return self._newmanifest() # don't upset local cache
1002 1002 if node in self._mancache:
1003 1003 return self._mancache[node][0]
1004 1004 if self._treeondisk:
1005 1005 def gettext():
1006 1006 return self.revision(node)
1007 1007 def readsubtree(dir, subm):
1008 1008 return self.dirlog(dir).read(subm)
1009 1009 m = self._newmanifest()
1010 1010 m.read(gettext, readsubtree)
1011 1011 m.setnode(node)
1012 1012 arraytext = None
1013 1013 else:
1014 1014 text = self.revision(node)
1015 1015 m = self._newmanifest(text)
1016 1016 arraytext = array.array('c', text)
1017 1017 self._mancache[node] = (m, arraytext)
1018 1018 return m
1019 1019
1020 1020 def readshallow(self, node):
1021 1021 '''Reads the manifest in this directory. When using flat manifests,
1022 1022 this manifest will generally have files in subdirectories in it. Does
1023 1023 not cache the manifest as the callers generally do not read the same
1024 1024 version twice.'''
1025 1025 return manifestdict(self.revision(node))
1026 1026
1027 1027 def find(self, node, f):
1028 1028 '''look up entry for a single file efficiently.
1029 1029 return (node, flags) pair if found, (None, None) if not.'''
1030 1030 m = self.read(node)
1031 1031 try:
1032 1032 return m.find(f)
1033 1033 except KeyError:
1034 1034 return None, None
1035 1035
1036 1036 def add(self, m, transaction, link, p1, p2, added, removed):
1037 1037 if (p1 in self._mancache and not self._treeinmem
1038 1038 and not self._usemanifestv2):
1039 1039 # If our first parent is in the manifest cache, we can
1040 1040 # compute a delta here using properties we know about the
1041 1041 # manifest up-front, which may save time later for the
1042 1042 # revlog layer.
1043 1043
1044 1044 _checkforbidden(added)
1045 1045 # combine the changed lists into one sorted iterator
1046 1046 work = heapq.merge([(x, False) for x in added],
1047 1047 [(x, True) for x in removed])
1048 1048
1049 1049 arraytext, deltatext = m.fastdelta(self._mancache[p1][1], work)
1050 1050 cachedelta = self.rev(p1), deltatext
1051 1051 text = util.buffer(arraytext)
1052 1052 n = self.addrevision(text, transaction, link, p1, p2, cachedelta)
1053 1053 else:
1054 1054 # The first parent manifest isn't already loaded, so we'll
1055 1055 # just encode a fulltext of the manifest and pass that
1056 1056 # through to the revlog layer, and let it handle the delta
1057 1057 # process.
1058 1058 if self._treeondisk:
1059 1059 m1 = self.read(p1)
1060 1060 m2 = self.read(p2)
1061 1061 n = self._addtree(m, transaction, link, m1, m2)
1062 1062 arraytext = None
1063 1063 else:
1064 1064 text = m.text(self._usemanifestv2)
1065 1065 n = self.addrevision(text, transaction, link, p1, p2)
1066 1066 arraytext = array.array('c', text)
1067 1067
1068 1068 self._mancache[n] = (m, arraytext)
1069 1069
1070 1070 return n
1071 1071
1072 1072 def _addtree(self, m, transaction, link, m1, m2):
1073 1073 # If the manifest is unchanged compared to one parent,
1074 1074 # don't write a new revision
1075 1075 if m.unmodifiedsince(m1) or m.unmodifiedsince(m2):
1076 1076 return m.node()
1077 1077 def writesubtree(subm, subp1, subp2):
1078 1078 sublog = self.dirlog(subm.dir())
1079 1079 sublog.add(subm, transaction, link, subp1, subp2, None, None)
1080 1080 m.writesubtrees(m1, m2, writesubtree)
1081 1081 text = m.dirtext(self._usemanifestv2)
1082 1082 # Double-check whether contents are unchanged to one parent
1083 1083 if text == m1.dirtext(self._usemanifestv2):
1084 1084 n = m1.node()
1085 1085 elif text == m2.dirtext(self._usemanifestv2):
1086 1086 n = m2.node()
1087 1087 else:
1088 1088 n = self.addrevision(text, transaction, link, m1.node(), m2.node())
1089 1089 # Save nodeid so parent manifest can calculate its nodeid
1090 1090 m.setnode(n)
1091 1091 return n
1092 1092
1093 1093 def clearcaches(self):
1094 1094 super(manifest, self).clearcaches()
1095 1095 self._mancache.clear()
1096 1096 self._dirlogcache = {'': self}
@@ -1,712 +1,712 b''
1 1 # match.py - filename matching
2 2 #
3 3 # Copyright 2008, 2009 Matt Mackall <mpm@selenic.com> and others
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 copy
11 11 import os
12 12 import re
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 error,
17 17 pathutil,
18 18 util,
19 19 )
20 20
21 21 propertycache = util.propertycache
22 22
23 23 def _rematcher(regex):
24 24 '''compile the regexp with the best available regexp engine and return a
25 25 matcher function'''
26 26 m = util.re.compile(regex)
27 27 try:
28 28 # slightly faster, provided by facebook's re2 bindings
29 29 return m.test_match
30 30 except AttributeError:
31 31 return m.match
32 32
33 33 def _expandsets(kindpats, ctx, listsubrepos):
34 34 '''Returns the kindpats list with the 'set' patterns expanded.'''
35 35 fset = set()
36 36 other = []
37 37
38 38 for kind, pat, source in kindpats:
39 39 if kind == 'set':
40 40 if not ctx:
41 raise error.Abort("fileset expression with no context")
41 raise error.Abort(_("fileset expression with no context"))
42 42 s = ctx.getfileset(pat)
43 43 fset.update(s)
44 44
45 45 if listsubrepos:
46 46 for subpath in ctx.substate:
47 47 s = ctx.sub(subpath).getfileset(pat)
48 48 fset.update(subpath + '/' + f for f in s)
49 49
50 50 continue
51 51 other.append((kind, pat, source))
52 52 return fset, other
53 53
54 54 def _expandsubinclude(kindpats, root):
55 55 '''Returns the list of subinclude matchers and the kindpats without the
56 56 subincludes in it.'''
57 57 relmatchers = []
58 58 other = []
59 59
60 60 for kind, pat, source in kindpats:
61 61 if kind == 'subinclude':
62 62 sourceroot = pathutil.dirname(util.normpath(source))
63 63 pat = util.pconvert(pat)
64 64 path = pathutil.join(sourceroot, pat)
65 65
66 66 newroot = pathutil.dirname(path)
67 67 relmatcher = match(newroot, '', [], ['include:%s' % path])
68 68
69 69 prefix = pathutil.canonpath(root, root, newroot)
70 70 if prefix:
71 71 prefix += '/'
72 72 relmatchers.append((prefix, relmatcher))
73 73 else:
74 74 other.append((kind, pat, source))
75 75
76 76 return relmatchers, other
77 77
78 78 def _kindpatsalwaysmatch(kindpats):
79 79 """"Checks whether the kindspats match everything, as e.g.
80 80 'relpath:.' does.
81 81 """
82 82 for kind, pat, source in kindpats:
83 83 if pat != '' or kind not in ['relpath', 'glob']:
84 84 return False
85 85 return True
86 86
87 87 class match(object):
88 88 def __init__(self, root, cwd, patterns, include=[], exclude=[],
89 89 default='glob', exact=False, auditor=None, ctx=None,
90 90 listsubrepos=False, warn=None, badfn=None):
91 91 """build an object to match a set of file patterns
92 92
93 93 arguments:
94 94 root - the canonical root of the tree you're matching against
95 95 cwd - the current working directory, if relevant
96 96 patterns - patterns to find
97 97 include - patterns to include (unless they are excluded)
98 98 exclude - patterns to exclude (even if they are included)
99 99 default - if a pattern in patterns has no explicit type, assume this one
100 100 exact - patterns are actually filenames (include/exclude still apply)
101 101 warn - optional function used for printing warnings
102 102 badfn - optional bad() callback for this matcher instead of the default
103 103
104 104 a pattern is one of:
105 105 'glob:<glob>' - a glob relative to cwd
106 106 're:<regexp>' - a regular expression
107 107 'path:<path>' - a path relative to repository root
108 108 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
109 109 'relpath:<path>' - a path relative to cwd
110 110 'relre:<regexp>' - a regexp that needn't match the start of a name
111 111 'set:<fileset>' - a fileset expression
112 112 'include:<path>' - a file of patterns to read and include
113 113 'subinclude:<path>' - a file of patterns to match against files under
114 114 the same directory
115 115 '<something>' - a pattern of the specified default type
116 116 """
117 117
118 118 self._root = root
119 119 self._cwd = cwd
120 120 self._files = [] # exact files and roots of patterns
121 121 self._anypats = bool(include or exclude)
122 122 self._always = False
123 123 self._pathrestricted = bool(include or exclude or patterns)
124 124 self._warn = warn
125 125 self._includeroots = set()
126 126 self._includedirs = set(['.'])
127 127 self._excluderoots = set()
128 128
129 129 if badfn is not None:
130 130 self.bad = badfn
131 131
132 132 matchfns = []
133 133 if include:
134 134 kindpats = self._normalize(include, 'glob', root, cwd, auditor)
135 135 self.includepat, im = _buildmatch(ctx, kindpats, '(?:/|$)',
136 136 listsubrepos, root)
137 137 self._includeroots.update(_roots(kindpats))
138 138 self._includedirs.update(util.dirs(self._includeroots))
139 139 matchfns.append(im)
140 140 if exclude:
141 141 kindpats = self._normalize(exclude, 'glob', root, cwd, auditor)
142 142 self.excludepat, em = _buildmatch(ctx, kindpats, '(?:/|$)',
143 143 listsubrepos, root)
144 144 if not _anypats(kindpats):
145 145 self._excluderoots.update(_roots(kindpats))
146 146 matchfns.append(lambda f: not em(f))
147 147 if exact:
148 148 if isinstance(patterns, list):
149 149 self._files = patterns
150 150 else:
151 151 self._files = list(patterns)
152 152 matchfns.append(self.exact)
153 153 elif patterns:
154 154 kindpats = self._normalize(patterns, default, root, cwd, auditor)
155 155 if not _kindpatsalwaysmatch(kindpats):
156 156 self._files = _roots(kindpats)
157 157 self._anypats = self._anypats or _anypats(kindpats)
158 158 self.patternspat, pm = _buildmatch(ctx, kindpats, '$',
159 159 listsubrepos, root)
160 160 matchfns.append(pm)
161 161
162 162 if not matchfns:
163 163 m = util.always
164 164 self._always = True
165 165 elif len(matchfns) == 1:
166 166 m = matchfns[0]
167 167 else:
168 168 def m(f):
169 169 for matchfn in matchfns:
170 170 if not matchfn(f):
171 171 return False
172 172 return True
173 173
174 174 self.matchfn = m
175 175 self._fileroots = set(self._files)
176 176
177 177 def __call__(self, fn):
178 178 return self.matchfn(fn)
179 179 def __iter__(self):
180 180 for f in self._files:
181 181 yield f
182 182
183 183 # Callbacks related to how the matcher is used by dirstate.walk.
184 184 # Subscribers to these events must monkeypatch the matcher object.
185 185 def bad(self, f, msg):
186 186 '''Callback from dirstate.walk for each explicit file that can't be
187 187 found/accessed, with an error message.'''
188 188 pass
189 189
190 190 # If an explicitdir is set, it will be called when an explicitly listed
191 191 # directory is visited.
192 192 explicitdir = None
193 193
194 194 # If an traversedir is set, it will be called when a directory discovered
195 195 # by recursive traversal is visited.
196 196 traversedir = None
197 197
198 198 def abs(self, f):
199 199 '''Convert a repo path back to path that is relative to the root of the
200 200 matcher.'''
201 201 return f
202 202
203 203 def rel(self, f):
204 204 '''Convert repo path back to path that is relative to cwd of matcher.'''
205 205 return util.pathto(self._root, self._cwd, f)
206 206
207 207 def uipath(self, f):
208 208 '''Convert repo path to a display path. If patterns or -I/-X were used
209 209 to create this matcher, the display path will be relative to cwd.
210 210 Otherwise it is relative to the root of the repo.'''
211 211 return (self._pathrestricted and self.rel(f)) or self.abs(f)
212 212
213 213 def files(self):
214 214 '''Explicitly listed files or patterns or roots:
215 215 if no patterns or .always(): empty list,
216 216 if exact: list exact files,
217 217 if not .anypats(): list all files and dirs,
218 218 else: optimal roots'''
219 219 return self._files
220 220
221 221 @propertycache
222 222 def _dirs(self):
223 223 return set(util.dirs(self._fileroots)) | set(['.'])
224 224
225 225 def visitdir(self, dir):
226 226 '''Decides whether a directory should be visited based on whether it
227 227 has potential matches in it or one of its subdirectories. This is
228 228 based on the match's primary, included, and excluded patterns.
229 229
230 230 Returns the string 'all' if the given directory and all subdirectories
231 231 should be visited. Otherwise returns True or False indicating whether
232 232 the given directory should be visited.
233 233
234 234 This function's behavior is undefined if it has returned False for
235 235 one of the dir's parent directories.
236 236 '''
237 237 if self.prefix() and dir in self._fileroots:
238 238 return 'all'
239 239 if dir in self._excluderoots:
240 240 return False
241 241 if (self._includeroots and
242 242 '.' not in self._includeroots and
243 243 dir not in self._includeroots and
244 244 dir not in self._includedirs and
245 245 not any(parent in self._includeroots
246 246 for parent in util.finddirs(dir))):
247 247 return False
248 248 return (not self._fileroots or
249 249 '.' in self._fileroots or
250 250 dir in self._fileroots or
251 251 dir in self._dirs or
252 252 any(parentdir in self._fileroots
253 253 for parentdir in util.finddirs(dir)))
254 254
255 255 def exact(self, f):
256 256 '''Returns True if f is in .files().'''
257 257 return f in self._fileroots
258 258
259 259 def anypats(self):
260 260 '''Matcher uses patterns or include/exclude.'''
261 261 return self._anypats
262 262
263 263 def always(self):
264 264 '''Matcher will match everything and .files() will be empty
265 265 - optimization might be possible and necessary.'''
266 266 return self._always
267 267
268 268 def ispartial(self):
269 269 '''True if the matcher won't always match.
270 270
271 271 Although it's just the inverse of _always in this implementation,
272 272 an extension such as narrowhg might make it return something
273 273 slightly different.'''
274 274 return not self._always
275 275
276 276 def isexact(self):
277 277 return self.matchfn == self.exact
278 278
279 279 def prefix(self):
280 280 return not self.always() and not self.isexact() and not self.anypats()
281 281
282 282 def _normalize(self, patterns, default, root, cwd, auditor):
283 283 '''Convert 'kind:pat' from the patterns list to tuples with kind and
284 284 normalized and rooted patterns and with listfiles expanded.'''
285 285 kindpats = []
286 286 for kind, pat in [_patsplit(p, default) for p in patterns]:
287 287 if kind in ('glob', 'relpath'):
288 288 pat = pathutil.canonpath(root, cwd, pat, auditor)
289 289 elif kind in ('relglob', 'path'):
290 290 pat = util.normpath(pat)
291 291 elif kind in ('listfile', 'listfile0'):
292 292 try:
293 293 files = util.readfile(pat)
294 294 if kind == 'listfile0':
295 295 files = files.split('\0')
296 296 else:
297 297 files = files.splitlines()
298 298 files = [f for f in files if f]
299 299 except EnvironmentError:
300 300 raise error.Abort(_("unable to read file list (%s)") % pat)
301 301 for k, p, source in self._normalize(files, default, root, cwd,
302 302 auditor):
303 303 kindpats.append((k, p, pat))
304 304 continue
305 305 elif kind == 'include':
306 306 try:
307 307 fullpath = os.path.join(root, util.localpath(pat))
308 308 includepats = readpatternfile(fullpath, self._warn)
309 309 for k, p, source in self._normalize(includepats, default,
310 310 root, cwd, auditor):
311 311 kindpats.append((k, p, source or pat))
312 312 except error.Abort as inst:
313 313 raise error.Abort('%s: %s' % (pat, inst[0]))
314 314 except IOError as inst:
315 315 if self._warn:
316 316 self._warn(_("skipping unreadable pattern file "
317 317 "'%s': %s\n") % (pat, inst.strerror))
318 318 continue
319 319 # else: re or relre - which cannot be normalized
320 320 kindpats.append((kind, pat, ''))
321 321 return kindpats
322 322
323 323 def exact(root, cwd, files, badfn=None):
324 324 return match(root, cwd, files, exact=True, badfn=badfn)
325 325
326 326 def always(root, cwd):
327 327 return match(root, cwd, [])
328 328
329 329 def badmatch(match, badfn):
330 330 """Make a copy of the given matcher, replacing its bad method with the given
331 331 one.
332 332 """
333 333 m = copy.copy(match)
334 334 m.bad = badfn
335 335 return m
336 336
337 337 class subdirmatcher(match):
338 338 """Adapt a matcher to work on a subdirectory only.
339 339
340 340 The paths are remapped to remove/insert the path as needed:
341 341
342 342 >>> m1 = match('root', '', ['a.txt', 'sub/b.txt'])
343 343 >>> m2 = subdirmatcher('sub', m1)
344 344 >>> bool(m2('a.txt'))
345 345 False
346 346 >>> bool(m2('b.txt'))
347 347 True
348 348 >>> bool(m2.matchfn('a.txt'))
349 349 False
350 350 >>> bool(m2.matchfn('b.txt'))
351 351 True
352 352 >>> m2.files()
353 353 ['b.txt']
354 354 >>> m2.exact('b.txt')
355 355 True
356 356 >>> util.pconvert(m2.rel('b.txt'))
357 357 'sub/b.txt'
358 358 >>> def bad(f, msg):
359 359 ... print "%s: %s" % (f, msg)
360 360 >>> m1.bad = bad
361 361 >>> m2.bad('x.txt', 'No such file')
362 362 sub/x.txt: No such file
363 363 >>> m2.abs('c.txt')
364 364 'sub/c.txt'
365 365 """
366 366
367 367 def __init__(self, path, matcher):
368 368 self._root = matcher._root
369 369 self._cwd = matcher._cwd
370 370 self._path = path
371 371 self._matcher = matcher
372 372 self._always = matcher._always
373 373 self._pathrestricted = matcher._pathrestricted
374 374
375 375 self._files = [f[len(path) + 1:] for f in matcher._files
376 376 if f.startswith(path + "/")]
377 377
378 378 # If the parent repo had a path to this subrepo and no patterns are
379 379 # specified, this submatcher always matches.
380 380 if not self._always and not matcher._anypats:
381 381 self._always = any(f == path for f in matcher._files)
382 382
383 383 self._anypats = matcher._anypats
384 384 # Some information is lost in the superclass's constructor, so we
385 385 # can not accurately create the matching function for the subdirectory
386 386 # from the inputs. Instead, we override matchfn() and visitdir() to
387 387 # call the original matcher with the subdirectory path prepended.
388 388 self.matchfn = lambda fn: matcher.matchfn(self._path + "/" + fn)
389 389 def visitdir(dir):
390 390 if dir == '.':
391 391 return matcher.visitdir(self._path)
392 392 return matcher.visitdir(self._path + "/" + dir)
393 393 self.visitdir = visitdir
394 394 self._fileroots = set(self._files)
395 395
396 396 def abs(self, f):
397 397 return self._matcher.abs(self._path + "/" + f)
398 398
399 399 def bad(self, f, msg):
400 400 self._matcher.bad(self._path + "/" + f, msg)
401 401
402 402 def rel(self, f):
403 403 return self._matcher.rel(self._path + "/" + f)
404 404
405 405 class icasefsmatcher(match):
406 406 """A matcher for wdir on case insensitive filesystems, which normalizes the
407 407 given patterns to the case in the filesystem.
408 408 """
409 409
410 410 def __init__(self, root, cwd, patterns, include, exclude, default, auditor,
411 411 ctx, listsubrepos=False, badfn=None):
412 412 init = super(icasefsmatcher, self).__init__
413 413 self._dirstate = ctx.repo().dirstate
414 414 self._dsnormalize = self._dirstate.normalize
415 415
416 416 init(root, cwd, patterns, include, exclude, default, auditor=auditor,
417 417 ctx=ctx, listsubrepos=listsubrepos, badfn=badfn)
418 418
419 419 # m.exact(file) must be based off of the actual user input, otherwise
420 420 # inexact case matches are treated as exact, and not noted without -v.
421 421 if self._files:
422 422 self._fileroots = set(_roots(self._kp))
423 423
424 424 def _normalize(self, patterns, default, root, cwd, auditor):
425 425 self._kp = super(icasefsmatcher, self)._normalize(patterns, default,
426 426 root, cwd, auditor)
427 427 kindpats = []
428 428 for kind, pats, source in self._kp:
429 429 if kind not in ('re', 'relre'): # regex can't be normalized
430 430 p = pats
431 431 pats = self._dsnormalize(pats)
432 432
433 433 # Preserve the original to handle a case only rename.
434 434 if p != pats and p in self._dirstate:
435 435 kindpats.append((kind, p, source))
436 436
437 437 kindpats.append((kind, pats, source))
438 438 return kindpats
439 439
440 440 def patkind(pattern, default=None):
441 441 '''If pattern is 'kind:pat' with a known kind, return kind.'''
442 442 return _patsplit(pattern, default)[0]
443 443
444 444 def _patsplit(pattern, default):
445 445 """Split a string into the optional pattern kind prefix and the actual
446 446 pattern."""
447 447 if ':' in pattern:
448 448 kind, pat = pattern.split(':', 1)
449 449 if kind in ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
450 450 'listfile', 'listfile0', 'set', 'include', 'subinclude'):
451 451 return kind, pat
452 452 return default, pattern
453 453
454 454 def _globre(pat):
455 455 r'''Convert an extended glob string to a regexp string.
456 456
457 457 >>> print _globre(r'?')
458 458 .
459 459 >>> print _globre(r'*')
460 460 [^/]*
461 461 >>> print _globre(r'**')
462 462 .*
463 463 >>> print _globre(r'**/a')
464 464 (?:.*/)?a
465 465 >>> print _globre(r'a/**/b')
466 466 a\/(?:.*/)?b
467 467 >>> print _globre(r'[a*?!^][^b][!c]')
468 468 [a*?!^][\^b][^c]
469 469 >>> print _globre(r'{a,b}')
470 470 (?:a|b)
471 471 >>> print _globre(r'.\*\?')
472 472 \.\*\?
473 473 '''
474 474 i, n = 0, len(pat)
475 475 res = ''
476 476 group = 0
477 477 escape = util.re.escape
478 478 def peek():
479 479 return i < n and pat[i]
480 480 while i < n:
481 481 c = pat[i]
482 482 i += 1
483 483 if c not in '*?[{},\\':
484 484 res += escape(c)
485 485 elif c == '*':
486 486 if peek() == '*':
487 487 i += 1
488 488 if peek() == '/':
489 489 i += 1
490 490 res += '(?:.*/)?'
491 491 else:
492 492 res += '.*'
493 493 else:
494 494 res += '[^/]*'
495 495 elif c == '?':
496 496 res += '.'
497 497 elif c == '[':
498 498 j = i
499 499 if j < n and pat[j] in '!]':
500 500 j += 1
501 501 while j < n and pat[j] != ']':
502 502 j += 1
503 503 if j >= n:
504 504 res += '\\['
505 505 else:
506 506 stuff = pat[i:j].replace('\\','\\\\')
507 507 i = j + 1
508 508 if stuff[0] == '!':
509 509 stuff = '^' + stuff[1:]
510 510 elif stuff[0] == '^':
511 511 stuff = '\\' + stuff
512 512 res = '%s[%s]' % (res, stuff)
513 513 elif c == '{':
514 514 group += 1
515 515 res += '(?:'
516 516 elif c == '}' and group:
517 517 res += ')'
518 518 group -= 1
519 519 elif c == ',' and group:
520 520 res += '|'
521 521 elif c == '\\':
522 522 p = peek()
523 523 if p:
524 524 i += 1
525 525 res += escape(p)
526 526 else:
527 527 res += escape(c)
528 528 else:
529 529 res += escape(c)
530 530 return res
531 531
532 532 def _regex(kind, pat, globsuffix):
533 533 '''Convert a (normalized) pattern of any kind into a regular expression.
534 534 globsuffix is appended to the regexp of globs.'''
535 535 if not pat:
536 536 return ''
537 537 if kind == 're':
538 538 return pat
539 539 if kind == 'path':
540 540 if pat == '.':
541 541 return ''
542 542 return '^' + util.re.escape(pat) + '(?:/|$)'
543 543 if kind == 'relglob':
544 544 return '(?:|.*/)' + _globre(pat) + globsuffix
545 545 if kind == 'relpath':
546 546 return util.re.escape(pat) + '(?:/|$)'
547 547 if kind == 'relre':
548 548 if pat.startswith('^'):
549 549 return pat
550 550 return '.*' + pat
551 551 return _globre(pat) + globsuffix
552 552
553 553 def _buildmatch(ctx, kindpats, globsuffix, listsubrepos, root):
554 554 '''Return regexp string and a matcher function for kindpats.
555 555 globsuffix is appended to the regexp of globs.'''
556 556 matchfuncs = []
557 557
558 558 subincludes, kindpats = _expandsubinclude(kindpats, root)
559 559 if subincludes:
560 560 def matchsubinclude(f):
561 561 for prefix, mf in subincludes:
562 562 if f.startswith(prefix) and mf(f[len(prefix):]):
563 563 return True
564 564 return False
565 565 matchfuncs.append(matchsubinclude)
566 566
567 567 fset, kindpats = _expandsets(kindpats, ctx, listsubrepos)
568 568 if fset:
569 569 matchfuncs.append(fset.__contains__)
570 570
571 571 regex = ''
572 572 if kindpats:
573 573 regex, mf = _buildregexmatch(kindpats, globsuffix)
574 574 matchfuncs.append(mf)
575 575
576 576 if len(matchfuncs) == 1:
577 577 return regex, matchfuncs[0]
578 578 else:
579 579 return regex, lambda f: any(mf(f) for mf in matchfuncs)
580 580
581 581 def _buildregexmatch(kindpats, globsuffix):
582 582 """Build a match function from a list of kinds and kindpats,
583 583 return regexp string and a matcher function."""
584 584 try:
585 585 regex = '(?:%s)' % '|'.join([_regex(k, p, globsuffix)
586 586 for (k, p, s) in kindpats])
587 587 if len(regex) > 20000:
588 588 raise OverflowError
589 589 return regex, _rematcher(regex)
590 590 except OverflowError:
591 591 # We're using a Python with a tiny regex engine and we
592 592 # made it explode, so we'll divide the pattern list in two
593 593 # until it works
594 594 l = len(kindpats)
595 595 if l < 2:
596 596 raise
597 597 regexa, a = _buildregexmatch(kindpats[:l//2], globsuffix)
598 598 regexb, b = _buildregexmatch(kindpats[l//2:], globsuffix)
599 599 return regex, lambda s: a(s) or b(s)
600 600 except re.error:
601 601 for k, p, s in kindpats:
602 602 try:
603 603 _rematcher('(?:%s)' % _regex(k, p, globsuffix))
604 604 except re.error:
605 605 if s:
606 606 raise error.Abort(_("%s: invalid pattern (%s): %s") %
607 607 (s, k, p))
608 608 else:
609 609 raise error.Abort(_("invalid pattern (%s): %s") % (k, p))
610 610 raise error.Abort(_("invalid pattern"))
611 611
612 612 def _roots(kindpats):
613 613 '''return roots and exact explicitly listed files from patterns
614 614
615 615 >>> _roots([('glob', 'g/*', ''), ('glob', 'g', ''), ('glob', 'g*', '')])
616 616 ['g', 'g', '.']
617 617 >>> _roots([('relpath', 'r', ''), ('path', 'p/p', ''), ('path', '', '')])
618 618 ['r', 'p/p', '.']
619 619 >>> _roots([('relglob', 'rg*', ''), ('re', 're/', ''), ('relre', 'rr', '')])
620 620 ['.', '.', '.']
621 621 '''
622 622 r = []
623 623 for kind, pat, source in kindpats:
624 624 if kind == 'glob': # find the non-glob prefix
625 625 root = []
626 626 for p in pat.split('/'):
627 627 if '[' in p or '{' in p or '*' in p or '?' in p:
628 628 break
629 629 root.append(p)
630 630 r.append('/'.join(root) or '.')
631 631 elif kind in ('relpath', 'path'):
632 632 r.append(pat or '.')
633 633 else: # relglob, re, relre
634 634 r.append('.')
635 635 return r
636 636
637 637 def _anypats(kindpats):
638 638 for kind, pat, source in kindpats:
639 639 if kind in ('glob', 're', 'relglob', 'relre', 'set'):
640 640 return True
641 641
642 642 _commentre = None
643 643
644 644 def readpatternfile(filepath, warn, sourceinfo=False):
645 645 '''parse a pattern file, returning a list of
646 646 patterns. These patterns should be given to compile()
647 647 to be validated and converted into a match function.
648 648
649 649 trailing white space is dropped.
650 650 the escape character is backslash.
651 651 comments start with #.
652 652 empty lines are skipped.
653 653
654 654 lines can be of the following formats:
655 655
656 656 syntax: regexp # defaults following lines to non-rooted regexps
657 657 syntax: glob # defaults following lines to non-rooted globs
658 658 re:pattern # non-rooted regular expression
659 659 glob:pattern # non-rooted glob
660 660 pattern # pattern of the current default type
661 661
662 662 if sourceinfo is set, returns a list of tuples:
663 663 (pattern, lineno, originalline). This is useful to debug ignore patterns.
664 664 '''
665 665
666 666 syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:',
667 667 'include': 'include', 'subinclude': 'subinclude'}
668 668 syntax = 'relre:'
669 669 patterns = []
670 670
671 671 fp = open(filepath)
672 672 for lineno, line in enumerate(fp, start=1):
673 673 if "#" in line:
674 674 global _commentre
675 675 if not _commentre:
676 676 _commentre = util.re.compile(r'((?:^|[^\\])(?:\\\\)*)#.*')
677 677 # remove comments prefixed by an even number of escapes
678 678 m = _commentre.search(line)
679 679 if m:
680 680 line = line[:m.end(1)]
681 681 # fixup properly escaped comments that survived the above
682 682 line = line.replace("\\#", "#")
683 683 line = line.rstrip()
684 684 if not line:
685 685 continue
686 686
687 687 if line.startswith('syntax:'):
688 688 s = line[7:].strip()
689 689 try:
690 690 syntax = syntaxes[s]
691 691 except KeyError:
692 692 if warn:
693 693 warn(_("%s: ignoring invalid syntax '%s'\n") %
694 694 (filepath, s))
695 695 continue
696 696
697 697 linesyntax = syntax
698 698 for s, rels in syntaxes.iteritems():
699 699 if line.startswith(rels):
700 700 linesyntax = rels
701 701 line = line[len(rels):]
702 702 break
703 703 elif line.startswith(s+':'):
704 704 linesyntax = rels
705 705 line = line[len(s) + 1:]
706 706 break
707 707 if sourceinfo:
708 708 patterns.append((linesyntax + line, lineno, line))
709 709 else:
710 710 patterns.append(linesyntax + line)
711 711 fp.close()
712 712 return patterns
@@ -1,1282 +1,1283 b''
1 1 # obsolete.py - obsolete markers handling
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """Obsolete marker handling
10 10
11 11 An obsolete marker maps an old changeset to a list of new
12 12 changesets. If the list of new changesets is empty, the old changeset
13 13 is said to be "killed". Otherwise, the old changeset is being
14 14 "replaced" by the new changesets.
15 15
16 16 Obsolete markers can be used to record and distribute changeset graph
17 17 transformations performed by history rewrite operations, and help
18 18 building new tools to reconcile conflicting rewrite actions. To
19 19 facilitate conflict resolution, markers include various annotations
20 20 besides old and news changeset identifiers, such as creation date or
21 21 author name.
22 22
23 23 The old obsoleted changeset is called a "precursor" and possible
24 24 replacements are called "successors". Markers that used changeset X as
25 25 a precursor are called "successor markers of X" because they hold
26 26 information about the successors of X. Markers that use changeset Y as
27 27 a successors are call "precursor markers of Y" because they hold
28 28 information about the precursors of Y.
29 29
30 30 Examples:
31 31
32 32 - When changeset A is replaced by changeset A', one marker is stored:
33 33
34 34 (A, (A',))
35 35
36 36 - When changesets A and B are folded into a new changeset C, two markers are
37 37 stored:
38 38
39 39 (A, (C,)) and (B, (C,))
40 40
41 41 - When changeset A is simply "pruned" from the graph, a marker is created:
42 42
43 43 (A, ())
44 44
45 45 - When changeset A is split into B and C, a single marker are used:
46 46
47 47 (A, (C, C))
48 48
49 49 We use a single marker to distinguish the "split" case from the "divergence"
50 50 case. If two independent operations rewrite the same changeset A in to A' and
51 51 A'', we have an error case: divergent rewriting. We can detect it because
52 52 two markers will be created independently:
53 53
54 54 (A, (B,)) and (A, (C,))
55 55
56 56 Format
57 57 ------
58 58
59 59 Markers are stored in an append-only file stored in
60 60 '.hg/store/obsstore'.
61 61
62 62 The file starts with a version header:
63 63
64 64 - 1 unsigned byte: version number, starting at zero.
65 65
66 66 The header is followed by the markers. Marker format depend of the version. See
67 67 comment associated with each format for details.
68 68
69 69 """
70 70 from __future__ import absolute_import
71 71
72 72 import errno
73 73 import struct
74 74
75 75 from .i18n import _
76 76 from . import (
77 77 base85,
78 78 error,
79 79 node,
80 80 parsers,
81 81 phases,
82 82 util,
83 83 )
84 84
85 85 _pack = struct.pack
86 86 _unpack = struct.unpack
87 87 _calcsize = struct.calcsize
88 88 propertycache = util.propertycache
89 89
90 90 # the obsolete feature is not mature enough to be enabled by default.
91 91 # you have to rely on third party extension extension to enable this.
92 92 _enabled = False
93 93
94 94 # Options for obsolescence
95 95 createmarkersopt = 'createmarkers'
96 96 allowunstableopt = 'allowunstable'
97 97 exchangeopt = 'exchange'
98 98
99 99 ### obsolescence marker flag
100 100
101 101 ## bumpedfix flag
102 102 #
103 103 # When a changeset A' succeed to a changeset A which became public, we call A'
104 104 # "bumped" because it's a successors of a public changesets
105 105 #
106 106 # o A' (bumped)
107 107 # |`:
108 108 # | o A
109 109 # |/
110 110 # o Z
111 111 #
112 112 # The way to solve this situation is to create a new changeset Ad as children
113 113 # of A. This changeset have the same content than A'. So the diff from A to A'
114 114 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
115 115 #
116 116 # o Ad
117 117 # |`:
118 118 # | x A'
119 119 # |'|
120 120 # o | A
121 121 # |/
122 122 # o Z
123 123 #
124 124 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
125 125 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
126 126 # This flag mean that the successors express the changes between the public and
127 127 # bumped version and fix the situation, breaking the transitivity of
128 128 # "bumped" here.
129 129 bumpedfix = 1
130 130 usingsha256 = 2
131 131
132 132 ## Parsing and writing of version "0"
133 133 #
134 134 # The header is followed by the markers. Each marker is made of:
135 135 #
136 136 # - 1 uint8 : number of new changesets "N", can be zero.
137 137 #
138 138 # - 1 uint32: metadata size "M" in bytes.
139 139 #
140 140 # - 1 byte: a bit field. It is reserved for flags used in common
141 141 # obsolete marker operations, to avoid repeated decoding of metadata
142 142 # entries.
143 143 #
144 144 # - 20 bytes: obsoleted changeset identifier.
145 145 #
146 146 # - N*20 bytes: new changesets identifiers.
147 147 #
148 148 # - M bytes: metadata as a sequence of nul-terminated strings. Each
149 149 # string contains a key and a value, separated by a colon ':', without
150 150 # additional encoding. Keys cannot contain '\0' or ':' and values
151 151 # cannot contain '\0'.
152 152 _fm0version = 0
153 153 _fm0fixed = '>BIB20s'
154 154 _fm0node = '20s'
155 155 _fm0fsize = _calcsize(_fm0fixed)
156 156 _fm0fnodesize = _calcsize(_fm0node)
157 157
158 158 def _fm0readmarkers(data, off):
159 159 # Loop on markers
160 160 l = len(data)
161 161 while off + _fm0fsize <= l:
162 162 # read fixed part
163 163 cur = data[off:off + _fm0fsize]
164 164 off += _fm0fsize
165 165 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
166 166 # read replacement
167 167 sucs = ()
168 168 if numsuc:
169 169 s = (_fm0fnodesize * numsuc)
170 170 cur = data[off:off + s]
171 171 sucs = _unpack(_fm0node * numsuc, cur)
172 172 off += s
173 173 # read metadata
174 174 # (metadata will be decoded on demand)
175 175 metadata = data[off:off + mdsize]
176 176 if len(metadata) != mdsize:
177 177 raise error.Abort(_('parsing obsolete marker: metadata is too '
178 178 'short, %d bytes expected, got %d')
179 179 % (mdsize, len(metadata)))
180 180 off += mdsize
181 181 metadata = _fm0decodemeta(metadata)
182 182 try:
183 183 when, offset = metadata.pop('date', '0 0').split(' ')
184 184 date = float(when), int(offset)
185 185 except ValueError:
186 186 date = (0., 0)
187 187 parents = None
188 188 if 'p2' in metadata:
189 189 parents = (metadata.pop('p1', None), metadata.pop('p2', None))
190 190 elif 'p1' in metadata:
191 191 parents = (metadata.pop('p1', None),)
192 192 elif 'p0' in metadata:
193 193 parents = ()
194 194 if parents is not None:
195 195 try:
196 196 parents = tuple(node.bin(p) for p in parents)
197 197 # if parent content is not a nodeid, drop the data
198 198 for p in parents:
199 199 if len(p) != 20:
200 200 parents = None
201 201 break
202 202 except TypeError:
203 203 # if content cannot be translated to nodeid drop the data.
204 204 parents = None
205 205
206 206 metadata = tuple(sorted(metadata.iteritems()))
207 207
208 208 yield (pre, sucs, flags, metadata, date, parents)
209 209
210 210 def _fm0encodeonemarker(marker):
211 211 pre, sucs, flags, metadata, date, parents = marker
212 212 if flags & usingsha256:
213 213 raise error.Abort(_('cannot handle sha256 with old obsstore format'))
214 214 metadata = dict(metadata)
215 215 time, tz = date
216 216 metadata['date'] = '%r %i' % (time, tz)
217 217 if parents is not None:
218 218 if not parents:
219 219 # mark that we explicitly recorded no parents
220 220 metadata['p0'] = ''
221 221 for i, p in enumerate(parents):
222 222 metadata['p%i' % (i + 1)] = node.hex(p)
223 223 metadata = _fm0encodemeta(metadata)
224 224 numsuc = len(sucs)
225 225 format = _fm0fixed + (_fm0node * numsuc)
226 226 data = [numsuc, len(metadata), flags, pre]
227 227 data.extend(sucs)
228 228 return _pack(format, *data) + metadata
229 229
230 230 def _fm0encodemeta(meta):
231 231 """Return encoded metadata string to string mapping.
232 232
233 233 Assume no ':' in key and no '\0' in both key and value."""
234 234 for key, value in meta.iteritems():
235 235 if ':' in key or '\0' in key:
236 236 raise ValueError("':' and '\0' are forbidden in metadata key'")
237 237 if '\0' in value:
238 238 raise ValueError("':' is forbidden in metadata value'")
239 239 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
240 240
241 241 def _fm0decodemeta(data):
242 242 """Return string to string dictionary from encoded version."""
243 243 d = {}
244 244 for l in data.split('\0'):
245 245 if l:
246 246 key, value = l.split(':')
247 247 d[key] = value
248 248 return d
249 249
250 250 ## Parsing and writing of version "1"
251 251 #
252 252 # The header is followed by the markers. Each marker is made of:
253 253 #
254 254 # - uint32: total size of the marker (including this field)
255 255 #
256 256 # - float64: date in seconds since epoch
257 257 #
258 258 # - int16: timezone offset in minutes
259 259 #
260 260 # - uint16: a bit field. It is reserved for flags used in common
261 261 # obsolete marker operations, to avoid repeated decoding of metadata
262 262 # entries.
263 263 #
264 264 # - uint8: number of successors "N", can be zero.
265 265 #
266 266 # - uint8: number of parents "P", can be zero.
267 267 #
268 268 # 0: parents data stored but no parent,
269 269 # 1: one parent stored,
270 270 # 2: two parents stored,
271 271 # 3: no parent data stored
272 272 #
273 273 # - uint8: number of metadata entries M
274 274 #
275 275 # - 20 or 32 bytes: precursor changeset identifier.
276 276 #
277 277 # - N*(20 or 32) bytes: successors changesets identifiers.
278 278 #
279 279 # - P*(20 or 32) bytes: parents of the precursors changesets.
280 280 #
281 281 # - M*(uint8, uint8): size of all metadata entries (key and value)
282 282 #
283 283 # - remaining bytes: the metadata, each (key, value) pair after the other.
284 284 _fm1version = 1
285 285 _fm1fixed = '>IdhHBBB20s'
286 286 _fm1nodesha1 = '20s'
287 287 _fm1nodesha256 = '32s'
288 288 _fm1nodesha1size = _calcsize(_fm1nodesha1)
289 289 _fm1nodesha256size = _calcsize(_fm1nodesha256)
290 290 _fm1fsize = _calcsize(_fm1fixed)
291 291 _fm1parentnone = 3
292 292 _fm1parentshift = 14
293 293 _fm1parentmask = (_fm1parentnone << _fm1parentshift)
294 294 _fm1metapair = 'BB'
295 295 _fm1metapairsize = _calcsize('BB')
296 296
297 297 def _fm1purereadmarkers(data, off):
298 298 # make some global constants local for performance
299 299 noneflag = _fm1parentnone
300 300 sha2flag = usingsha256
301 301 sha1size = _fm1nodesha1size
302 302 sha2size = _fm1nodesha256size
303 303 sha1fmt = _fm1nodesha1
304 304 sha2fmt = _fm1nodesha256
305 305 metasize = _fm1metapairsize
306 306 metafmt = _fm1metapair
307 307 fsize = _fm1fsize
308 308 unpack = _unpack
309 309
310 310 # Loop on markers
311 311 stop = len(data) - _fm1fsize
312 312 ufixed = struct.Struct(_fm1fixed).unpack
313 313
314 314 while off <= stop:
315 315 # read fixed part
316 316 o1 = off + fsize
317 317 t, secs, tz, flags, numsuc, numpar, nummeta, prec = ufixed(data[off:o1])
318 318
319 319 if flags & sha2flag:
320 320 # FIXME: prec was read as a SHA1, needs to be amended
321 321
322 322 # read 0 or more successors
323 323 if numsuc == 1:
324 324 o2 = o1 + sha2size
325 325 sucs = (data[o1:o2],)
326 326 else:
327 327 o2 = o1 + sha2size * numsuc
328 328 sucs = unpack(sha2fmt * numsuc, data[o1:o2])
329 329
330 330 # read parents
331 331 if numpar == noneflag:
332 332 o3 = o2
333 333 parents = None
334 334 elif numpar == 1:
335 335 o3 = o2 + sha2size
336 336 parents = (data[o2:o3],)
337 337 else:
338 338 o3 = o2 + sha2size * numpar
339 339 parents = unpack(sha2fmt * numpar, data[o2:o3])
340 340 else:
341 341 # read 0 or more successors
342 342 if numsuc == 1:
343 343 o2 = o1 + sha1size
344 344 sucs = (data[o1:o2],)
345 345 else:
346 346 o2 = o1 + sha1size * numsuc
347 347 sucs = unpack(sha1fmt * numsuc, data[o1:o2])
348 348
349 349 # read parents
350 350 if numpar == noneflag:
351 351 o3 = o2
352 352 parents = None
353 353 elif numpar == 1:
354 354 o3 = o2 + sha1size
355 355 parents = (data[o2:o3],)
356 356 else:
357 357 o3 = o2 + sha1size * numpar
358 358 parents = unpack(sha1fmt * numpar, data[o2:o3])
359 359
360 360 # read metadata
361 361 off = o3 + metasize * nummeta
362 362 metapairsize = unpack('>' + (metafmt * nummeta), data[o3:off])
363 363 metadata = []
364 364 for idx in xrange(0, len(metapairsize), 2):
365 365 o1 = off + metapairsize[idx]
366 366 o2 = o1 + metapairsize[idx + 1]
367 367 metadata.append((data[off:o1], data[o1:o2]))
368 368 off = o2
369 369
370 370 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
371 371
372 372 def _fm1encodeonemarker(marker):
373 373 pre, sucs, flags, metadata, date, parents = marker
374 374 # determine node size
375 375 _fm1node = _fm1nodesha1
376 376 if flags & usingsha256:
377 377 _fm1node = _fm1nodesha256
378 378 numsuc = len(sucs)
379 379 numextranodes = numsuc
380 380 if parents is None:
381 381 numpar = _fm1parentnone
382 382 else:
383 383 numpar = len(parents)
384 384 numextranodes += numpar
385 385 formatnodes = _fm1node * numextranodes
386 386 formatmeta = _fm1metapair * len(metadata)
387 387 format = _fm1fixed + formatnodes + formatmeta
388 388 # tz is stored in minutes so we divide by 60
389 389 tz = date[1]//60
390 390 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
391 391 data.extend(sucs)
392 392 if parents is not None:
393 393 data.extend(parents)
394 394 totalsize = _calcsize(format)
395 395 for key, value in metadata:
396 396 lk = len(key)
397 397 lv = len(value)
398 398 data.append(lk)
399 399 data.append(lv)
400 400 totalsize += lk + lv
401 401 data[0] = totalsize
402 402 data = [_pack(format, *data)]
403 403 for key, value in metadata:
404 404 data.append(key)
405 405 data.append(value)
406 406 return ''.join(data)
407 407
408 408 def _fm1readmarkers(data, off):
409 409 native = getattr(parsers, 'fm1readmarkers', None)
410 410 if not native:
411 411 return _fm1purereadmarkers(data, off)
412 412 stop = len(data) - _fm1fsize
413 413 return native(data, off, stop)
414 414
415 415 # mapping to read/write various marker formats
416 416 # <version> -> (decoder, encoder)
417 417 formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker),
418 418 _fm1version: (_fm1readmarkers, _fm1encodeonemarker)}
419 419
420 420 @util.nogc
421 421 def _readmarkers(data):
422 422 """Read and enumerate markers from raw data"""
423 423 off = 0
424 424 diskversion = _unpack('>B', data[off:off + 1])[0]
425 425 off += 1
426 426 if diskversion not in formats:
427 427 raise error.Abort(_('parsing obsolete marker: unknown version %r')
428 428 % diskversion)
429 429 return diskversion, formats[diskversion][0](data, off)
430 430
431 431 def encodemarkers(markers, addheader=False, version=_fm0version):
432 432 # Kept separate from flushmarkers(), it will be reused for
433 433 # markers exchange.
434 434 encodeone = formats[version][1]
435 435 if addheader:
436 436 yield _pack('>B', version)
437 437 for marker in markers:
438 438 yield encodeone(marker)
439 439
440 440
441 441 class marker(object):
442 442 """Wrap obsolete marker raw data"""
443 443
444 444 def __init__(self, repo, data):
445 445 # the repo argument will be used to create changectx in later version
446 446 self._repo = repo
447 447 self._data = data
448 448 self._decodedmeta = None
449 449
450 450 def __hash__(self):
451 451 return hash(self._data)
452 452
453 453 def __eq__(self, other):
454 454 if type(other) != type(self):
455 455 return False
456 456 return self._data == other._data
457 457
458 458 def precnode(self):
459 459 """Precursor changeset node identifier"""
460 460 return self._data[0]
461 461
462 462 def succnodes(self):
463 463 """List of successor changesets node identifiers"""
464 464 return self._data[1]
465 465
466 466 def parentnodes(self):
467 467 """Parents of the precursors (None if not recorded)"""
468 468 return self._data[5]
469 469
470 470 def metadata(self):
471 471 """Decoded metadata dictionary"""
472 472 return dict(self._data[3])
473 473
474 474 def date(self):
475 475 """Creation date as (unixtime, offset)"""
476 476 return self._data[4]
477 477
478 478 def flags(self):
479 479 """The flags field of the marker"""
480 480 return self._data[2]
481 481
482 482 @util.nogc
483 483 def _addsuccessors(successors, markers):
484 484 for mark in markers:
485 485 successors.setdefault(mark[0], set()).add(mark)
486 486
487 487 @util.nogc
488 488 def _addprecursors(precursors, markers):
489 489 for mark in markers:
490 490 for suc in mark[1]:
491 491 precursors.setdefault(suc, set()).add(mark)
492 492
493 493 @util.nogc
494 494 def _addchildren(children, markers):
495 495 for mark in markers:
496 496 parents = mark[5]
497 497 if parents is not None:
498 498 for p in parents:
499 499 children.setdefault(p, set()).add(mark)
500 500
501 501 def _checkinvalidmarkers(markers):
502 502 """search for marker with invalid data and raise error if needed
503 503
504 504 Exist as a separated function to allow the evolve extension for a more
505 505 subtle handling.
506 506 """
507 507 for mark in markers:
508 508 if node.nullid in mark[1]:
509 509 raise error.Abort(_('bad obsolescence marker detected: '
510 510 'invalid successors nullid'))
511 511
512 512 class obsstore(object):
513 513 """Store obsolete markers
514 514
515 515 Markers can be accessed with two mappings:
516 516 - precursors[x] -> set(markers on precursors edges of x)
517 517 - successors[x] -> set(markers on successors edges of x)
518 518 - children[x] -> set(markers on precursors edges of children(x)
519 519 """
520 520
521 521 fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
522 522 # prec: nodeid, precursor changesets
523 523 # succs: tuple of nodeid, successor changesets (0-N length)
524 524 # flag: integer, flag field carrying modifier for the markers (see doc)
525 525 # meta: binary blob, encoded metadata dictionary
526 526 # date: (float, int) tuple, date of marker creation
527 527 # parents: (tuple of nodeid) or None, parents of precursors
528 528 # None is used when no data has been recorded
529 529
530 530 def __init__(self, svfs, defaultformat=_fm1version, readonly=False):
531 531 # caches for various obsolescence related cache
532 532 self.caches = {}
533 533 self.svfs = svfs
534 534 self._version = defaultformat
535 535 self._readonly = readonly
536 536
537 537 def __iter__(self):
538 538 return iter(self._all)
539 539
540 540 def __len__(self):
541 541 return len(self._all)
542 542
543 543 def __nonzero__(self):
544 544 if not self._cached('_all'):
545 545 try:
546 546 return self.svfs.stat('obsstore').st_size > 1
547 547 except OSError as inst:
548 548 if inst.errno != errno.ENOENT:
549 549 raise
550 550 # just build an empty _all list if no obsstore exists, which
551 551 # avoids further stat() syscalls
552 552 pass
553 553 return bool(self._all)
554 554
555 555 @property
556 556 def readonly(self):
557 557 """True if marker creation is disabled
558 558
559 559 Remove me in the future when obsolete marker is always on."""
560 560 return self._readonly
561 561
562 562 def create(self, transaction, prec, succs=(), flag=0, parents=None,
563 563 date=None, metadata=None):
564 564 """obsolete: add a new obsolete marker
565 565
566 566 * ensuring it is hashable
567 567 * check mandatory metadata
568 568 * encode metadata
569 569
570 570 If you are a human writing code creating marker you want to use the
571 571 `createmarkers` function in this module instead.
572 572
573 573 return True if a new marker have been added, False if the markers
574 574 already existed (no op).
575 575 """
576 576 if metadata is None:
577 577 metadata = {}
578 578 if date is None:
579 579 if 'date' in metadata:
580 580 # as a courtesy for out-of-tree extensions
581 581 date = util.parsedate(metadata.pop('date'))
582 582 else:
583 583 date = util.makedate()
584 584 if len(prec) != 20:
585 585 raise ValueError(prec)
586 586 for succ in succs:
587 587 if len(succ) != 20:
588 588 raise ValueError(succ)
589 589 if prec in succs:
590 590 raise ValueError(_('in-marker cycle with %s') % node.hex(prec))
591 591
592 592 metadata = tuple(sorted(metadata.iteritems()))
593 593
594 594 marker = (str(prec), tuple(succs), int(flag), metadata, date, parents)
595 595 return bool(self.add(transaction, [marker]))
596 596
597 597 def add(self, transaction, markers):
598 598 """Add new markers to the store
599 599
600 600 Take care of filtering duplicate.
601 601 Return the number of new marker."""
602 602 if self._readonly:
603 raise error.Abort('creating obsolete markers is not enabled on '
604 'this repo')
603 raise error.Abort(_('creating obsolete markers is not enabled on '
604 'this repo'))
605 605 known = set(self._all)
606 606 new = []
607 607 for m in markers:
608 608 if m not in known:
609 609 known.add(m)
610 610 new.append(m)
611 611 if new:
612 612 f = self.svfs('obsstore', 'ab')
613 613 try:
614 614 offset = f.tell()
615 615 transaction.add('obsstore', offset)
616 616 # offset == 0: new file - add the version header
617 617 for bytes in encodemarkers(new, offset == 0, self._version):
618 618 f.write(bytes)
619 619 finally:
620 620 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
621 621 # call 'filecacheentry.refresh()' here
622 622 f.close()
623 623 self._addmarkers(new)
624 624 # new marker *may* have changed several set. invalidate the cache.
625 625 self.caches.clear()
626 626 # records the number of new markers for the transaction hooks
627 627 previous = int(transaction.hookargs.get('new_obsmarkers', '0'))
628 628 transaction.hookargs['new_obsmarkers'] = str(previous + len(new))
629 629 return len(new)
630 630
631 631 def mergemarkers(self, transaction, data):
632 632 """merge a binary stream of markers inside the obsstore
633 633
634 634 Returns the number of new markers added."""
635 635 version, markers = _readmarkers(data)
636 636 return self.add(transaction, markers)
637 637
638 638 @propertycache
639 639 def _all(self):
640 640 data = self.svfs.tryread('obsstore')
641 641 if not data:
642 642 return []
643 643 self._version, markers = _readmarkers(data)
644 644 markers = list(markers)
645 645 _checkinvalidmarkers(markers)
646 646 return markers
647 647
648 648 @propertycache
649 649 def successors(self):
650 650 successors = {}
651 651 _addsuccessors(successors, self._all)
652 652 return successors
653 653
654 654 @propertycache
655 655 def precursors(self):
656 656 precursors = {}
657 657 _addprecursors(precursors, self._all)
658 658 return precursors
659 659
660 660 @propertycache
661 661 def children(self):
662 662 children = {}
663 663 _addchildren(children, self._all)
664 664 return children
665 665
666 666 def _cached(self, attr):
667 667 return attr in self.__dict__
668 668
669 669 def _addmarkers(self, markers):
670 670 markers = list(markers) # to allow repeated iteration
671 671 self._all.extend(markers)
672 672 if self._cached('successors'):
673 673 _addsuccessors(self.successors, markers)
674 674 if self._cached('precursors'):
675 675 _addprecursors(self.precursors, markers)
676 676 if self._cached('children'):
677 677 _addchildren(self.children, markers)
678 678 _checkinvalidmarkers(markers)
679 679
680 680 def relevantmarkers(self, nodes):
681 681 """return a set of all obsolescence markers relevant to a set of nodes.
682 682
683 683 "relevant" to a set of nodes mean:
684 684
685 685 - marker that use this changeset as successor
686 686 - prune marker of direct children on this changeset
687 687 - recursive application of the two rules on precursors of these markers
688 688
689 689 It is a set so you cannot rely on order."""
690 690
691 691 pendingnodes = set(nodes)
692 692 seenmarkers = set()
693 693 seennodes = set(pendingnodes)
694 694 precursorsmarkers = self.precursors
695 695 children = self.children
696 696 while pendingnodes:
697 697 direct = set()
698 698 for current in pendingnodes:
699 699 direct.update(precursorsmarkers.get(current, ()))
700 700 pruned = [m for m in children.get(current, ()) if not m[1]]
701 701 direct.update(pruned)
702 702 direct -= seenmarkers
703 703 pendingnodes = set([m[0] for m in direct])
704 704 seenmarkers |= direct
705 705 pendingnodes -= seennodes
706 706 seennodes |= pendingnodes
707 707 return seenmarkers
708 708
709 709 def commonversion(versions):
710 710 """Return the newest version listed in both versions and our local formats.
711 711
712 712 Returns None if no common version exists.
713 713 """
714 714 versions.sort(reverse=True)
715 715 # search for highest version known on both side
716 716 for v in versions:
717 717 if v in formats:
718 718 return v
719 719 return None
720 720
721 721 # arbitrary picked to fit into 8K limit from HTTP server
722 722 # you have to take in account:
723 723 # - the version header
724 724 # - the base85 encoding
725 725 _maxpayload = 5300
726 726
727 727 def _pushkeyescape(markers):
728 728 """encode markers into a dict suitable for pushkey exchange
729 729
730 730 - binary data is base85 encoded
731 731 - split in chunks smaller than 5300 bytes"""
732 732 keys = {}
733 733 parts = []
734 734 currentlen = _maxpayload * 2 # ensure we create a new part
735 735 for marker in markers:
736 736 nextdata = _fm0encodeonemarker(marker)
737 737 if (len(nextdata) + currentlen > _maxpayload):
738 738 currentpart = []
739 739 currentlen = 0
740 740 parts.append(currentpart)
741 741 currentpart.append(nextdata)
742 742 currentlen += len(nextdata)
743 743 for idx, part in enumerate(reversed(parts)):
744 744 data = ''.join([_pack('>B', _fm0version)] + part)
745 745 keys['dump%i' % idx] = base85.b85encode(data)
746 746 return keys
747 747
748 748 def listmarkers(repo):
749 749 """List markers over pushkey"""
750 750 if not repo.obsstore:
751 751 return {}
752 752 return _pushkeyescape(sorted(repo.obsstore))
753 753
754 754 def pushmarker(repo, key, old, new):
755 755 """Push markers over pushkey"""
756 756 if not key.startswith('dump'):
757 757 repo.ui.warn(_('unknown key: %r') % key)
758 758 return 0
759 759 if old:
760 760 repo.ui.warn(_('unexpected old value for %r') % key)
761 761 return 0
762 762 data = base85.b85decode(new)
763 763 lock = repo.lock()
764 764 try:
765 765 tr = repo.transaction('pushkey: obsolete markers')
766 766 try:
767 767 repo.obsstore.mergemarkers(tr, data)
768 768 tr.close()
769 769 return 1
770 770 finally:
771 771 tr.release()
772 772 finally:
773 773 lock.release()
774 774
775 775 def getmarkers(repo, nodes=None):
776 776 """returns markers known in a repository
777 777
778 778 If <nodes> is specified, only markers "relevant" to those nodes are are
779 779 returned"""
780 780 if nodes is None:
781 781 rawmarkers = repo.obsstore
782 782 else:
783 783 rawmarkers = repo.obsstore.relevantmarkers(nodes)
784 784
785 785 for markerdata in rawmarkers:
786 786 yield marker(repo, markerdata)
787 787
788 788 def relevantmarkers(repo, node):
789 789 """all obsolete markers relevant to some revision"""
790 790 for markerdata in repo.obsstore.relevantmarkers(node):
791 791 yield marker(repo, markerdata)
792 792
793 793
794 794 def precursormarkers(ctx):
795 795 """obsolete marker marking this changeset as a successors"""
796 796 for data in ctx.repo().obsstore.precursors.get(ctx.node(), ()):
797 797 yield marker(ctx.repo(), data)
798 798
799 799 def successormarkers(ctx):
800 800 """obsolete marker making this changeset obsolete"""
801 801 for data in ctx.repo().obsstore.successors.get(ctx.node(), ()):
802 802 yield marker(ctx.repo(), data)
803 803
804 804 def allsuccessors(obsstore, nodes, ignoreflags=0):
805 805 """Yield node for every successor of <nodes>.
806 806
807 807 Some successors may be unknown locally.
808 808
809 809 This is a linear yield unsuited to detecting split changesets. It includes
810 810 initial nodes too."""
811 811 remaining = set(nodes)
812 812 seen = set(remaining)
813 813 while remaining:
814 814 current = remaining.pop()
815 815 yield current
816 816 for mark in obsstore.successors.get(current, ()):
817 817 # ignore marker flagged with specified flag
818 818 if mark[2] & ignoreflags:
819 819 continue
820 820 for suc in mark[1]:
821 821 if suc not in seen:
822 822 seen.add(suc)
823 823 remaining.add(suc)
824 824
825 825 def allprecursors(obsstore, nodes, ignoreflags=0):
826 826 """Yield node for every precursors of <nodes>.
827 827
828 828 Some precursors may be unknown locally.
829 829
830 830 This is a linear yield unsuited to detecting folded changesets. It includes
831 831 initial nodes too."""
832 832
833 833 remaining = set(nodes)
834 834 seen = set(remaining)
835 835 while remaining:
836 836 current = remaining.pop()
837 837 yield current
838 838 for mark in obsstore.precursors.get(current, ()):
839 839 # ignore marker flagged with specified flag
840 840 if mark[2] & ignoreflags:
841 841 continue
842 842 suc = mark[0]
843 843 if suc not in seen:
844 844 seen.add(suc)
845 845 remaining.add(suc)
846 846
847 847 def foreground(repo, nodes):
848 848 """return all nodes in the "foreground" of other node
849 849
850 850 The foreground of a revision is anything reachable using parent -> children
851 851 or precursor -> successor relation. It is very similar to "descendant" but
852 852 augmented with obsolescence information.
853 853
854 854 Beware that possible obsolescence cycle may result if complex situation.
855 855 """
856 856 repo = repo.unfiltered()
857 857 foreground = set(repo.set('%ln::', nodes))
858 858 if repo.obsstore:
859 859 # We only need this complicated logic if there is obsolescence
860 860 # XXX will probably deserve an optimised revset.
861 861 nm = repo.changelog.nodemap
862 862 plen = -1
863 863 # compute the whole set of successors or descendants
864 864 while len(foreground) != plen:
865 865 plen = len(foreground)
866 866 succs = set(c.node() for c in foreground)
867 867 mutable = [c.node() for c in foreground if c.mutable()]
868 868 succs.update(allsuccessors(repo.obsstore, mutable))
869 869 known = (n for n in succs if n in nm)
870 870 foreground = set(repo.set('%ln::', known))
871 871 return set(c.node() for c in foreground)
872 872
873 873
874 874 def successorssets(repo, initialnode, cache=None):
875 875 """Return set of all latest successors of initial nodes
876 876
877 877 The successors set of a changeset A are the group of revisions that succeed
878 878 A. It succeeds A as a consistent whole, each revision being only a partial
879 879 replacement. The successors set contains non-obsolete changesets only.
880 880
881 881 This function returns the full list of successor sets which is why it
882 882 returns a list of tuples and not just a single tuple. Each tuple is a valid
883 883 successors set. Note that (A,) may be a valid successors set for changeset A
884 884 (see below).
885 885
886 886 In most cases, a changeset A will have a single element (e.g. the changeset
887 887 A is replaced by A') in its successors set. Though, it is also common for a
888 888 changeset A to have no elements in its successor set (e.g. the changeset
889 889 has been pruned). Therefore, the returned list of successors sets will be
890 890 [(A',)] or [], respectively.
891 891
892 892 When a changeset A is split into A' and B', however, it will result in a
893 893 successors set containing more than a single element, i.e. [(A',B')].
894 894 Divergent changesets will result in multiple successors sets, i.e. [(A',),
895 895 (A'')].
896 896
897 897 If a changeset A is not obsolete, then it will conceptually have no
898 898 successors set. To distinguish this from a pruned changeset, the successor
899 899 set will contain itself only, i.e. [(A,)].
900 900
901 901 Finally, successors unknown locally are considered to be pruned (obsoleted
902 902 without any successors).
903 903
904 904 The optional `cache` parameter is a dictionary that may contain precomputed
905 905 successors sets. It is meant to reuse the computation of a previous call to
906 906 `successorssets` when multiple calls are made at the same time. The cache
907 907 dictionary is updated in place. The caller is responsible for its life
908 908 span. Code that makes multiple calls to `successorssets` *must* use this
909 909 cache mechanism or suffer terrible performance.
910 910 """
911 911
912 912 succmarkers = repo.obsstore.successors
913 913
914 914 # Stack of nodes we search successors sets for
915 915 toproceed = [initialnode]
916 916 # set version of above list for fast loop detection
917 917 # element added to "toproceed" must be added here
918 918 stackedset = set(toproceed)
919 919 if cache is None:
920 920 cache = {}
921 921
922 922 # This while loop is the flattened version of a recursive search for
923 923 # successors sets
924 924 #
925 925 # def successorssets(x):
926 926 # successors = directsuccessors(x)
927 927 # ss = [[]]
928 928 # for succ in directsuccessors(x):
929 929 # # product as in itertools cartesian product
930 930 # ss = product(ss, successorssets(succ))
931 931 # return ss
932 932 #
933 933 # But we can not use plain recursive calls here:
934 934 # - that would blow the python call stack
935 935 # - obsolescence markers may have cycles, we need to handle them.
936 936 #
937 937 # The `toproceed` list act as our call stack. Every node we search
938 938 # successors set for are stacked there.
939 939 #
940 940 # The `stackedset` is set version of this stack used to check if a node is
941 941 # already stacked. This check is used to detect cycles and prevent infinite
942 942 # loop.
943 943 #
944 944 # successors set of all nodes are stored in the `cache` dictionary.
945 945 #
946 946 # After this while loop ends we use the cache to return the successors sets
947 947 # for the node requested by the caller.
948 948 while toproceed:
949 949 # Every iteration tries to compute the successors sets of the topmost
950 950 # node of the stack: CURRENT.
951 951 #
952 952 # There are four possible outcomes:
953 953 #
954 954 # 1) We already know the successors sets of CURRENT:
955 955 # -> mission accomplished, pop it from the stack.
956 956 # 2) Node is not obsolete:
957 957 # -> the node is its own successors sets. Add it to the cache.
958 958 # 3) We do not know successors set of direct successors of CURRENT:
959 959 # -> We add those successors to the stack.
960 960 # 4) We know successors sets of all direct successors of CURRENT:
961 961 # -> We can compute CURRENT successors set and add it to the
962 962 # cache.
963 963 #
964 964 current = toproceed[-1]
965 965 if current in cache:
966 966 # case (1): We already know the successors sets
967 967 stackedset.remove(toproceed.pop())
968 968 elif current not in succmarkers:
969 969 # case (2): The node is not obsolete.
970 970 if current in repo:
971 971 # We have a valid last successors.
972 972 cache[current] = [(current,)]
973 973 else:
974 974 # Final obsolete version is unknown locally.
975 975 # Do not count that as a valid successors
976 976 cache[current] = []
977 977 else:
978 978 # cases (3) and (4)
979 979 #
980 980 # We proceed in two phases. Phase 1 aims to distinguish case (3)
981 981 # from case (4):
982 982 #
983 983 # For each direct successors of CURRENT, we check whether its
984 984 # successors sets are known. If they are not, we stack the
985 985 # unknown node and proceed to the next iteration of the while
986 986 # loop. (case 3)
987 987 #
988 988 # During this step, we may detect obsolescence cycles: a node
989 989 # with unknown successors sets but already in the call stack.
990 990 # In such a situation, we arbitrary set the successors sets of
991 991 # the node to nothing (node pruned) to break the cycle.
992 992 #
993 993 # If no break was encountered we proceed to phase 2.
994 994 #
995 995 # Phase 2 computes successors sets of CURRENT (case 4); see details
996 996 # in phase 2 itself.
997 997 #
998 998 # Note the two levels of iteration in each phase.
999 999 # - The first one handles obsolescence markers using CURRENT as
1000 1000 # precursor (successors markers of CURRENT).
1001 1001 #
1002 1002 # Having multiple entry here means divergence.
1003 1003 #
1004 1004 # - The second one handles successors defined in each marker.
1005 1005 #
1006 1006 # Having none means pruned node, multiple successors means split,
1007 1007 # single successors are standard replacement.
1008 1008 #
1009 1009 for mark in sorted(succmarkers[current]):
1010 1010 for suc in mark[1]:
1011 1011 if suc not in cache:
1012 1012 if suc in stackedset:
1013 1013 # cycle breaking
1014 1014 cache[suc] = []
1015 1015 else:
1016 1016 # case (3) If we have not computed successors sets
1017 1017 # of one of those successors we add it to the
1018 1018 # `toproceed` stack and stop all work for this
1019 1019 # iteration.
1020 1020 toproceed.append(suc)
1021 1021 stackedset.add(suc)
1022 1022 break
1023 1023 else:
1024 1024 continue
1025 1025 break
1026 1026 else:
1027 1027 # case (4): we know all successors sets of all direct
1028 1028 # successors
1029 1029 #
1030 1030 # Successors set contributed by each marker depends on the
1031 1031 # successors sets of all its "successors" node.
1032 1032 #
1033 1033 # Each different marker is a divergence in the obsolescence
1034 1034 # history. It contributes successors sets distinct from other
1035 1035 # markers.
1036 1036 #
1037 1037 # Within a marker, a successor may have divergent successors
1038 1038 # sets. In such a case, the marker will contribute multiple
1039 1039 # divergent successors sets. If multiple successors have
1040 1040 # divergent successors sets, a Cartesian product is used.
1041 1041 #
1042 1042 # At the end we post-process successors sets to remove
1043 1043 # duplicated entry and successors set that are strict subset of
1044 1044 # another one.
1045 1045 succssets = []
1046 1046 for mark in sorted(succmarkers[current]):
1047 1047 # successors sets contributed by this marker
1048 1048 markss = [[]]
1049 1049 for suc in mark[1]:
1050 1050 # cardinal product with previous successors
1051 1051 productresult = []
1052 1052 for prefix in markss:
1053 1053 for suffix in cache[suc]:
1054 1054 newss = list(prefix)
1055 1055 for part in suffix:
1056 1056 # do not duplicated entry in successors set
1057 1057 # first entry wins.
1058 1058 if part not in newss:
1059 1059 newss.append(part)
1060 1060 productresult.append(newss)
1061 1061 markss = productresult
1062 1062 succssets.extend(markss)
1063 1063 # remove duplicated and subset
1064 1064 seen = []
1065 1065 final = []
1066 1066 candidate = sorted(((set(s), s) for s in succssets if s),
1067 1067 key=lambda x: len(x[1]), reverse=True)
1068 1068 for setversion, listversion in candidate:
1069 1069 for seenset in seen:
1070 1070 if setversion.issubset(seenset):
1071 1071 break
1072 1072 else:
1073 1073 final.append(listversion)
1074 1074 seen.append(setversion)
1075 1075 final.reverse() # put small successors set first
1076 1076 cache[current] = final
1077 1077 return cache[initialnode]
1078 1078
1079 1079 # mapping of 'set-name' -> <function to compute this set>
1080 1080 cachefuncs = {}
1081 1081 def cachefor(name):
1082 1082 """Decorator to register a function as computing the cache for a set"""
1083 1083 def decorator(func):
1084 1084 assert name not in cachefuncs
1085 1085 cachefuncs[name] = func
1086 1086 return func
1087 1087 return decorator
1088 1088
1089 1089 def getrevs(repo, name):
1090 1090 """Return the set of revision that belong to the <name> set
1091 1091
1092 1092 Such access may compute the set and cache it for future use"""
1093 1093 repo = repo.unfiltered()
1094 1094 if not repo.obsstore:
1095 1095 return frozenset()
1096 1096 if name not in repo.obsstore.caches:
1097 1097 repo.obsstore.caches[name] = cachefuncs[name](repo)
1098 1098 return repo.obsstore.caches[name]
1099 1099
1100 1100 # To be simple we need to invalidate obsolescence cache when:
1101 1101 #
1102 1102 # - new changeset is added:
1103 1103 # - public phase is changed
1104 1104 # - obsolescence marker are added
1105 1105 # - strip is used a repo
1106 1106 def clearobscaches(repo):
1107 1107 """Remove all obsolescence related cache from a repo
1108 1108
1109 1109 This remove all cache in obsstore is the obsstore already exist on the
1110 1110 repo.
1111 1111
1112 1112 (We could be smarter here given the exact event that trigger the cache
1113 1113 clearing)"""
1114 1114 # only clear cache is there is obsstore data in this repo
1115 1115 if 'obsstore' in repo._filecache:
1116 1116 repo.obsstore.caches.clear()
1117 1117
1118 1118 @cachefor('obsolete')
1119 1119 def _computeobsoleteset(repo):
1120 1120 """the set of obsolete revisions"""
1121 1121 obs = set()
1122 1122 getnode = repo.changelog.node
1123 1123 notpublic = repo.revs("not public()")
1124 1124 for r in notpublic:
1125 1125 if getnode(r) in repo.obsstore.successors:
1126 1126 obs.add(r)
1127 1127 return obs
1128 1128
1129 1129 @cachefor('unstable')
1130 1130 def _computeunstableset(repo):
1131 1131 """the set of non obsolete revisions with obsolete parents"""
1132 1132 revs = [(ctx.rev(), ctx) for ctx in
1133 1133 repo.set('(not public()) and (not obsolete())')]
1134 1134 revs.sort(key=lambda x:x[0])
1135 1135 unstable = set()
1136 1136 for rev, ctx in revs:
1137 1137 # A rev is unstable if one of its parent is obsolete or unstable
1138 1138 # this works since we traverse following growing rev order
1139 1139 if any((x.obsolete() or (x.rev() in unstable))
1140 1140 for x in ctx.parents()):
1141 1141 unstable.add(rev)
1142 1142 return unstable
1143 1143
1144 1144 @cachefor('suspended')
1145 1145 def _computesuspendedset(repo):
1146 1146 """the set of obsolete parents with non obsolete descendants"""
1147 1147 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
1148 1148 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
1149 1149
1150 1150 @cachefor('extinct')
1151 1151 def _computeextinctset(repo):
1152 1152 """the set of obsolete parents without non obsolete descendants"""
1153 1153 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
1154 1154
1155 1155
1156 1156 @cachefor('bumped')
1157 1157 def _computebumpedset(repo):
1158 1158 """the set of revs trying to obsolete public revisions"""
1159 1159 bumped = set()
1160 1160 # util function (avoid attribute lookup in the loop)
1161 1161 phase = repo._phasecache.phase # would be faster to grab the full list
1162 1162 public = phases.public
1163 1163 cl = repo.changelog
1164 1164 torev = cl.nodemap.get
1165 1165 for ctx in repo.set('(not public()) and (not obsolete())'):
1166 1166 rev = ctx.rev()
1167 1167 # We only evaluate mutable, non-obsolete revision
1168 1168 node = ctx.node()
1169 1169 # (future) A cache of precursors may worth if split is very common
1170 1170 for pnode in allprecursors(repo.obsstore, [node],
1171 1171 ignoreflags=bumpedfix):
1172 1172 prev = torev(pnode) # unfiltered! but so is phasecache
1173 1173 if (prev is not None) and (phase(repo, prev) <= public):
1174 1174 # we have a public precursor
1175 1175 bumped.add(rev)
1176 1176 break # Next draft!
1177 1177 return bumped
1178 1178
1179 1179 @cachefor('divergent')
1180 1180 def _computedivergentset(repo):
1181 1181 """the set of rev that compete to be the final successors of some revision.
1182 1182 """
1183 1183 divergent = set()
1184 1184 obsstore = repo.obsstore
1185 1185 newermap = {}
1186 1186 for ctx in repo.set('(not public()) - obsolete()'):
1187 1187 mark = obsstore.precursors.get(ctx.node(), ())
1188 1188 toprocess = set(mark)
1189 1189 seen = set()
1190 1190 while toprocess:
1191 1191 prec = toprocess.pop()[0]
1192 1192 if prec in seen:
1193 1193 continue # emergency cycle hanging prevention
1194 1194 seen.add(prec)
1195 1195 if prec not in newermap:
1196 1196 successorssets(repo, prec, newermap)
1197 1197 newer = [n for n in newermap[prec] if n]
1198 1198 if len(newer) > 1:
1199 1199 divergent.add(ctx.rev())
1200 1200 break
1201 1201 toprocess.update(obsstore.precursors.get(prec, ()))
1202 1202 return divergent
1203 1203
1204 1204
1205 1205 def createmarkers(repo, relations, flag=0, date=None, metadata=None):
1206 1206 """Add obsolete markers between changesets in a repo
1207 1207
1208 1208 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
1209 1209 tuple. `old` and `news` are changectx. metadata is an optional dictionary
1210 1210 containing metadata for this marker only. It is merged with the global
1211 1211 metadata specified through the `metadata` argument of this function,
1212 1212
1213 1213 Trying to obsolete a public changeset will raise an exception.
1214 1214
1215 1215 Current user and date are used except if specified otherwise in the
1216 1216 metadata attribute.
1217 1217
1218 1218 This function operates within a transaction of its own, but does
1219 1219 not take any lock on the repo.
1220 1220 """
1221 1221 # prepare metadata
1222 1222 if metadata is None:
1223 1223 metadata = {}
1224 1224 if 'user' not in metadata:
1225 1225 metadata['user'] = repo.ui.username()
1226 1226 tr = repo.transaction('add-obsolescence-marker')
1227 1227 try:
1228 1228 markerargs = []
1229 1229 for rel in relations:
1230 1230 prec = rel[0]
1231 1231 sucs = rel[1]
1232 1232 localmetadata = metadata.copy()
1233 1233 if 2 < len(rel):
1234 1234 localmetadata.update(rel[2])
1235 1235
1236 1236 if not prec.mutable():
1237 raise error.Abort("cannot obsolete public changeset: %s"
1237 raise error.Abort(_("cannot obsolete public changeset: %s")
1238 1238 % prec,
1239 1239 hint='see "hg help phases" for details')
1240 1240 nprec = prec.node()
1241 1241 nsucs = tuple(s.node() for s in sucs)
1242 1242 npare = None
1243 1243 if not nsucs:
1244 1244 npare = tuple(p.node() for p in prec.parents())
1245 1245 if nprec in nsucs:
1246 raise error.Abort("changeset %s cannot obsolete itself" % prec)
1246 raise error.Abort(_("changeset %s cannot obsolete itself")
1247 % prec)
1247 1248
1248 1249 # Creating the marker causes the hidden cache to become invalid,
1249 1250 # which causes recomputation when we ask for prec.parents() above.
1250 1251 # Resulting in n^2 behavior. So let's prepare all of the args
1251 1252 # first, then create the markers.
1252 1253 markerargs.append((nprec, nsucs, npare, localmetadata))
1253 1254
1254 1255 for args in markerargs:
1255 1256 nprec, nsucs, npare, localmetadata = args
1256 1257 repo.obsstore.create(tr, nprec, nsucs, flag, parents=npare,
1257 1258 date=date, metadata=localmetadata)
1258 1259 repo.filteredrevcache.clear()
1259 1260 tr.close()
1260 1261 finally:
1261 1262 tr.release()
1262 1263
1263 1264 def isenabled(repo, option):
1264 1265 """Returns True if the given repository has the given obsolete option
1265 1266 enabled.
1266 1267 """
1267 1268 result = set(repo.ui.configlist('experimental', 'evolution'))
1268 1269 if 'all' in result:
1269 1270 return True
1270 1271
1271 1272 # For migration purposes, temporarily return true if the config hasn't been
1272 1273 # set but _enabled is true.
1273 1274 if len(result) == 0 and _enabled:
1274 1275 return True
1275 1276
1276 1277 # createmarkers must be enabled if other options are enabled
1277 1278 if ((allowunstableopt in result or exchangeopt in result) and
1278 1279 not createmarkersopt in result):
1279 1280 raise error.Abort(_("'createmarkers' obsolete option must be enabled "
1280 1281 "if other obsolete options are enabled"))
1281 1282
1282 1283 return option in result
@@ -1,3671 +1,3672 b''
1 1 # revset.py - revision set queries for mercurial
2 2 #
3 3 # Copyright 2010 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 heapq
11 11 import re
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 destutil,
16 16 encoding,
17 17 error,
18 18 hbisect,
19 19 match as matchmod,
20 20 node,
21 21 obsolete as obsmod,
22 22 parser,
23 23 pathutil,
24 24 phases,
25 25 registrar,
26 26 repoview,
27 27 util,
28 28 )
29 29
30 30 def _revancestors(repo, revs, followfirst):
31 31 """Like revlog.ancestors(), but supports followfirst."""
32 32 if followfirst:
33 33 cut = 1
34 34 else:
35 35 cut = None
36 36 cl = repo.changelog
37 37
38 38 def iterate():
39 39 revs.sort(reverse=True)
40 40 irevs = iter(revs)
41 41 h = []
42 42
43 43 inputrev = next(irevs, None)
44 44 if inputrev is not None:
45 45 heapq.heappush(h, -inputrev)
46 46
47 47 seen = set()
48 48 while h:
49 49 current = -heapq.heappop(h)
50 50 if current == inputrev:
51 51 inputrev = next(irevs, None)
52 52 if inputrev is not None:
53 53 heapq.heappush(h, -inputrev)
54 54 if current not in seen:
55 55 seen.add(current)
56 56 yield current
57 57 for parent in cl.parentrevs(current)[:cut]:
58 58 if parent != node.nullrev:
59 59 heapq.heappush(h, -parent)
60 60
61 61 return generatorset(iterate(), iterasc=False)
62 62
63 63 def _revdescendants(repo, revs, followfirst):
64 64 """Like revlog.descendants() but supports followfirst."""
65 65 if followfirst:
66 66 cut = 1
67 67 else:
68 68 cut = None
69 69
70 70 def iterate():
71 71 cl = repo.changelog
72 72 # XXX this should be 'parentset.min()' assuming 'parentset' is a
73 73 # smartset (and if it is not, it should.)
74 74 first = min(revs)
75 75 nullrev = node.nullrev
76 76 if first == nullrev:
77 77 # Are there nodes with a null first parent and a non-null
78 78 # second one? Maybe. Do we care? Probably not.
79 79 for i in cl:
80 80 yield i
81 81 else:
82 82 seen = set(revs)
83 83 for i in cl.revs(first + 1):
84 84 for x in cl.parentrevs(i)[:cut]:
85 85 if x != nullrev and x in seen:
86 86 seen.add(i)
87 87 yield i
88 88 break
89 89
90 90 return generatorset(iterate(), iterasc=True)
91 91
92 92 def _reachablerootspure(repo, minroot, roots, heads, includepath):
93 93 """return (heads(::<roots> and ::<heads>))
94 94
95 95 If includepath is True, return (<roots>::<heads>)."""
96 96 if not roots:
97 97 return []
98 98 parentrevs = repo.changelog.parentrevs
99 99 roots = set(roots)
100 100 visit = list(heads)
101 101 reachable = set()
102 102 seen = {}
103 103 # prefetch all the things! (because python is slow)
104 104 reached = reachable.add
105 105 dovisit = visit.append
106 106 nextvisit = visit.pop
107 107 # open-code the post-order traversal due to the tiny size of
108 108 # sys.getrecursionlimit()
109 109 while visit:
110 110 rev = nextvisit()
111 111 if rev in roots:
112 112 reached(rev)
113 113 if not includepath:
114 114 continue
115 115 parents = parentrevs(rev)
116 116 seen[rev] = parents
117 117 for parent in parents:
118 118 if parent >= minroot and parent not in seen:
119 119 dovisit(parent)
120 120 if not reachable:
121 121 return baseset()
122 122 if not includepath:
123 123 return reachable
124 124 for rev in sorted(seen):
125 125 for parent in seen[rev]:
126 126 if parent in reachable:
127 127 reached(rev)
128 128 return reachable
129 129
130 130 def reachableroots(repo, roots, heads, includepath=False):
131 131 """return (heads(::<roots> and ::<heads>))
132 132
133 133 If includepath is True, return (<roots>::<heads>)."""
134 134 if not roots:
135 135 return baseset()
136 136 minroot = roots.min()
137 137 roots = list(roots)
138 138 heads = list(heads)
139 139 try:
140 140 revs = repo.changelog.reachableroots(minroot, heads, roots, includepath)
141 141 except AttributeError:
142 142 revs = _reachablerootspure(repo, minroot, roots, heads, includepath)
143 143 revs = baseset(revs)
144 144 revs.sort()
145 145 return revs
146 146
147 147 elements = {
148 148 # token-type: binding-strength, primary, prefix, infix, suffix
149 149 "(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None),
150 150 "##": (20, None, None, ("_concat", 20), None),
151 151 "~": (18, None, None, ("ancestor", 18), None),
152 152 "^": (18, None, None, ("parent", 18), ("parentpost", 18)),
153 153 "-": (5, None, ("negate", 19), ("minus", 5), None),
154 154 "::": (17, None, ("dagrangepre", 17), ("dagrange", 17),
155 155 ("dagrangepost", 17)),
156 156 "..": (17, None, ("dagrangepre", 17), ("dagrange", 17),
157 157 ("dagrangepost", 17)),
158 158 ":": (15, "rangeall", ("rangepre", 15), ("range", 15), ("rangepost", 15)),
159 159 "not": (10, None, ("not", 10), None, None),
160 160 "!": (10, None, ("not", 10), None, None),
161 161 "and": (5, None, None, ("and", 5), None),
162 162 "&": (5, None, None, ("and", 5), None),
163 163 "%": (5, None, None, ("only", 5), ("onlypost", 5)),
164 164 "or": (4, None, None, ("or", 4), None),
165 165 "|": (4, None, None, ("or", 4), None),
166 166 "+": (4, None, None, ("or", 4), None),
167 167 "=": (3, None, None, ("keyvalue", 3), None),
168 168 ",": (2, None, None, ("list", 2), None),
169 169 ")": (0, None, None, None, None),
170 170 "symbol": (0, "symbol", None, None, None),
171 171 "string": (0, "string", None, None, None),
172 172 "end": (0, None, None, None, None),
173 173 }
174 174
175 175 keywords = set(['and', 'or', 'not'])
176 176
177 177 # default set of valid characters for the initial letter of symbols
178 178 _syminitletters = set(c for c in [chr(i) for i in xrange(256)]
179 179 if c.isalnum() or c in '._@' or ord(c) > 127)
180 180
181 181 # default set of valid characters for non-initial letters of symbols
182 182 _symletters = set(c for c in [chr(i) for i in xrange(256)]
183 183 if c.isalnum() or c in '-._/@' or ord(c) > 127)
184 184
185 185 def tokenize(program, lookup=None, syminitletters=None, symletters=None):
186 186 '''
187 187 Parse a revset statement into a stream of tokens
188 188
189 189 ``syminitletters`` is the set of valid characters for the initial
190 190 letter of symbols.
191 191
192 192 By default, character ``c`` is recognized as valid for initial
193 193 letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``.
194 194
195 195 ``symletters`` is the set of valid characters for non-initial
196 196 letters of symbols.
197 197
198 198 By default, character ``c`` is recognized as valid for non-initial
199 199 letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``.
200 200
201 201 Check that @ is a valid unquoted token character (issue3686):
202 202 >>> list(tokenize("@::"))
203 203 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
204 204
205 205 '''
206 206 if syminitletters is None:
207 207 syminitletters = _syminitletters
208 208 if symletters is None:
209 209 symletters = _symletters
210 210
211 211 if program and lookup:
212 212 # attempt to parse old-style ranges first to deal with
213 213 # things like old-tag which contain query metacharacters
214 214 parts = program.split(':', 1)
215 215 if all(lookup(sym) for sym in parts if sym):
216 216 if parts[0]:
217 217 yield ('symbol', parts[0], 0)
218 218 if len(parts) > 1:
219 219 s = len(parts[0])
220 220 yield (':', None, s)
221 221 if parts[1]:
222 222 yield ('symbol', parts[1], s + 1)
223 223 yield ('end', None, len(program))
224 224 return
225 225
226 226 pos, l = 0, len(program)
227 227 while pos < l:
228 228 c = program[pos]
229 229 if c.isspace(): # skip inter-token whitespace
230 230 pass
231 231 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
232 232 yield ('::', None, pos)
233 233 pos += 1 # skip ahead
234 234 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
235 235 yield ('..', None, pos)
236 236 pos += 1 # skip ahead
237 237 elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully
238 238 yield ('##', None, pos)
239 239 pos += 1 # skip ahead
240 240 elif c in "():=,-|&+!~^%": # handle simple operators
241 241 yield (c, None, pos)
242 242 elif (c in '"\'' or c == 'r' and
243 243 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
244 244 if c == 'r':
245 245 pos += 1
246 246 c = program[pos]
247 247 decode = lambda x: x
248 248 else:
249 249 decode = parser.unescapestr
250 250 pos += 1
251 251 s = pos
252 252 while pos < l: # find closing quote
253 253 d = program[pos]
254 254 if d == '\\': # skip over escaped characters
255 255 pos += 2
256 256 continue
257 257 if d == c:
258 258 yield ('string', decode(program[s:pos]), s)
259 259 break
260 260 pos += 1
261 261 else:
262 262 raise error.ParseError(_("unterminated string"), s)
263 263 # gather up a symbol/keyword
264 264 elif c in syminitletters:
265 265 s = pos
266 266 pos += 1
267 267 while pos < l: # find end of symbol
268 268 d = program[pos]
269 269 if d not in symletters:
270 270 break
271 271 if d == '.' and program[pos - 1] == '.': # special case for ..
272 272 pos -= 1
273 273 break
274 274 pos += 1
275 275 sym = program[s:pos]
276 276 if sym in keywords: # operator keywords
277 277 yield (sym, None, s)
278 278 elif '-' in sym:
279 279 # some jerk gave us foo-bar-baz, try to check if it's a symbol
280 280 if lookup and lookup(sym):
281 281 # looks like a real symbol
282 282 yield ('symbol', sym, s)
283 283 else:
284 284 # looks like an expression
285 285 parts = sym.split('-')
286 286 for p in parts[:-1]:
287 287 if p: # possible consecutive -
288 288 yield ('symbol', p, s)
289 289 s += len(p)
290 290 yield ('-', None, pos)
291 291 s += 1
292 292 if parts[-1]: # possible trailing -
293 293 yield ('symbol', parts[-1], s)
294 294 else:
295 295 yield ('symbol', sym, s)
296 296 pos -= 1
297 297 else:
298 298 raise error.ParseError(_("syntax error in revset '%s'") %
299 299 program, pos)
300 300 pos += 1
301 301 yield ('end', None, pos)
302 302
303 303 # helpers
304 304
305 305 def getstring(x, err):
306 306 if x and (x[0] == 'string' or x[0] == 'symbol'):
307 307 return x[1]
308 308 raise error.ParseError(err)
309 309
310 310 def getlist(x):
311 311 if not x:
312 312 return []
313 313 if x[0] == 'list':
314 314 return list(x[1:])
315 315 return [x]
316 316
317 317 def getargs(x, min, max, err):
318 318 l = getlist(x)
319 319 if len(l) < min or (max >= 0 and len(l) > max):
320 320 raise error.ParseError(err)
321 321 return l
322 322
323 323 def getargsdict(x, funcname, keys):
324 324 return parser.buildargsdict(getlist(x), funcname, keys.split(),
325 325 keyvaluenode='keyvalue', keynode='symbol')
326 326
327 327 def getset(repo, subset, x):
328 328 if not x:
329 329 raise error.ParseError(_("missing argument"))
330 330 s = methods[x[0]](repo, subset, *x[1:])
331 331 if util.safehasattr(s, 'isascending'):
332 332 return s
333 333 # else case should not happen, because all non-func are internal,
334 334 # ignoring for now.
335 335 if x[0] == 'func' and x[1][0] == 'symbol' and x[1][1] in symbols:
336 336 repo.ui.deprecwarn('revset "%s" uses list instead of smartset'
337 337 % x[1][1],
338 338 '3.9')
339 339 return baseset(s)
340 340
341 341 def _getrevsource(repo, r):
342 342 extra = repo[r].extra()
343 343 for label in ('source', 'transplant_source', 'rebase_source'):
344 344 if label in extra:
345 345 try:
346 346 return repo[extra[label]].rev()
347 347 except error.RepoLookupError:
348 348 pass
349 349 return None
350 350
351 351 # operator methods
352 352
353 353 def stringset(repo, subset, x):
354 354 x = repo[x].rev()
355 355 if (x in subset
356 356 or x == node.nullrev and isinstance(subset, fullreposet)):
357 357 return baseset([x])
358 358 return baseset()
359 359
360 360 def rangeset(repo, subset, x, y):
361 361 m = getset(repo, fullreposet(repo), x)
362 362 n = getset(repo, fullreposet(repo), y)
363 363
364 364 if not m or not n:
365 365 return baseset()
366 366 m, n = m.first(), n.last()
367 367
368 368 if m == n:
369 369 r = baseset([m])
370 370 elif n == node.wdirrev:
371 371 r = spanset(repo, m, len(repo)) + baseset([n])
372 372 elif m == node.wdirrev:
373 373 r = baseset([m]) + spanset(repo, len(repo) - 1, n - 1)
374 374 elif m < n:
375 375 r = spanset(repo, m, n + 1)
376 376 else:
377 377 r = spanset(repo, m, n - 1)
378 378 # XXX We should combine with subset first: 'subset & baseset(...)'. This is
379 379 # necessary to ensure we preserve the order in subset.
380 380 #
381 381 # This has performance implication, carrying the sorting over when possible
382 382 # would be more efficient.
383 383 return r & subset
384 384
385 385 def dagrange(repo, subset, x, y):
386 386 r = fullreposet(repo)
387 387 xs = reachableroots(repo, getset(repo, r, x), getset(repo, r, y),
388 388 includepath=True)
389 389 return subset & xs
390 390
391 391 def andset(repo, subset, x, y):
392 392 return getset(repo, getset(repo, subset, x), y)
393 393
394 394 def differenceset(repo, subset, x, y):
395 395 return getset(repo, subset, x) - getset(repo, subset, y)
396 396
397 397 def orset(repo, subset, *xs):
398 398 assert xs
399 399 if len(xs) == 1:
400 400 return getset(repo, subset, xs[0])
401 401 p = len(xs) // 2
402 402 a = orset(repo, subset, *xs[:p])
403 403 b = orset(repo, subset, *xs[p:])
404 404 return a + b
405 405
406 406 def notset(repo, subset, x):
407 407 return subset - getset(repo, subset, x)
408 408
409 409 def listset(repo, subset, *xs):
410 410 raise error.ParseError(_("can't use a list in this context"),
411 411 hint=_('see hg help "revsets.x or y"'))
412 412
413 413 def keyvaluepair(repo, subset, k, v):
414 414 raise error.ParseError(_("can't use a key-value pair in this context"))
415 415
416 416 def func(repo, subset, a, b):
417 417 if a[0] == 'symbol' and a[1] in symbols:
418 418 return symbols[a[1]](repo, subset, b)
419 419
420 420 keep = lambda fn: getattr(fn, '__doc__', None) is not None
421 421
422 422 syms = [s for (s, fn) in symbols.items() if keep(fn)]
423 423 raise error.UnknownIdentifier(a[1], syms)
424 424
425 425 # functions
426 426
427 427 # symbols are callables like:
428 428 # fn(repo, subset, x)
429 429 # with:
430 430 # repo - current repository instance
431 431 # subset - of revisions to be examined
432 432 # x - argument in tree form
433 433 symbols = {}
434 434
435 435 # symbols which can't be used for a DoS attack for any given input
436 436 # (e.g. those which accept regexes as plain strings shouldn't be included)
437 437 # functions that just return a lot of changesets (like all) don't count here
438 438 safesymbols = set()
439 439
440 440 predicate = registrar.revsetpredicate()
441 441
442 442 @predicate('_destupdate')
443 443 def _destupdate(repo, subset, x):
444 444 # experimental revset for update destination
445 445 args = getargsdict(x, 'limit', 'clean check')
446 446 return subset & baseset([destutil.destupdate(repo, **args)[0]])
447 447
448 448 @predicate('_destmerge')
449 449 def _destmerge(repo, subset, x):
450 450 # experimental revset for merge destination
451 451 sourceset = None
452 452 if x is not None:
453 453 sourceset = getset(repo, fullreposet(repo), x)
454 454 return subset & baseset([destutil.destmerge(repo, sourceset=sourceset)])
455 455
456 456 @predicate('adds(pattern)', safe=True)
457 457 def adds(repo, subset, x):
458 458 """Changesets that add a file matching pattern.
459 459
460 460 The pattern without explicit kind like ``glob:`` is expected to be
461 461 relative to the current directory and match against a file or a
462 462 directory.
463 463 """
464 464 # i18n: "adds" is a keyword
465 465 pat = getstring(x, _("adds requires a pattern"))
466 466 return checkstatus(repo, subset, pat, 1)
467 467
468 468 @predicate('ancestor(*changeset)', safe=True)
469 469 def ancestor(repo, subset, x):
470 470 """A greatest common ancestor of the changesets.
471 471
472 472 Accepts 0 or more changesets.
473 473 Will return empty list when passed no args.
474 474 Greatest common ancestor of a single changeset is that changeset.
475 475 """
476 476 # i18n: "ancestor" is a keyword
477 477 l = getlist(x)
478 478 rl = fullreposet(repo)
479 479 anc = None
480 480
481 481 # (getset(repo, rl, i) for i in l) generates a list of lists
482 482 for revs in (getset(repo, rl, i) for i in l):
483 483 for r in revs:
484 484 if anc is None:
485 485 anc = repo[r]
486 486 else:
487 487 anc = anc.ancestor(repo[r])
488 488
489 489 if anc is not None and anc.rev() in subset:
490 490 return baseset([anc.rev()])
491 491 return baseset()
492 492
493 493 def _ancestors(repo, subset, x, followfirst=False):
494 494 heads = getset(repo, fullreposet(repo), x)
495 495 if not heads:
496 496 return baseset()
497 497 s = _revancestors(repo, heads, followfirst)
498 498 return subset & s
499 499
500 500 @predicate('ancestors(set)', safe=True)
501 501 def ancestors(repo, subset, x):
502 502 """Changesets that are ancestors of a changeset in set.
503 503 """
504 504 return _ancestors(repo, subset, x)
505 505
506 506 @predicate('_firstancestors', safe=True)
507 507 def _firstancestors(repo, subset, x):
508 508 # ``_firstancestors(set)``
509 509 # Like ``ancestors(set)`` but follows only the first parents.
510 510 return _ancestors(repo, subset, x, followfirst=True)
511 511
512 512 def ancestorspec(repo, subset, x, n):
513 513 """``set~n``
514 514 Changesets that are the Nth ancestor (first parents only) of a changeset
515 515 in set.
516 516 """
517 517 try:
518 518 n = int(n[1])
519 519 except (TypeError, ValueError):
520 520 raise error.ParseError(_("~ expects a number"))
521 521 ps = set()
522 522 cl = repo.changelog
523 523 for r in getset(repo, fullreposet(repo), x):
524 524 for i in range(n):
525 525 r = cl.parentrevs(r)[0]
526 526 ps.add(r)
527 527 return subset & ps
528 528
529 529 @predicate('author(string)', safe=True)
530 530 def author(repo, subset, x):
531 531 """Alias for ``user(string)``.
532 532 """
533 533 # i18n: "author" is a keyword
534 534 n = encoding.lower(getstring(x, _("author requires a string")))
535 535 kind, pattern, matcher = _substringmatcher(n)
536 536 return subset.filter(lambda x: matcher(encoding.lower(repo[x].user())),
537 537 condrepr=('<user %r>', n))
538 538
539 539 @predicate('bisect(string)', safe=True)
540 540 def bisect(repo, subset, x):
541 541 """Changesets marked in the specified bisect status:
542 542
543 543 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
544 544 - ``goods``, ``bads`` : csets topologically good/bad
545 545 - ``range`` : csets taking part in the bisection
546 546 - ``pruned`` : csets that are goods, bads or skipped
547 547 - ``untested`` : csets whose fate is yet unknown
548 548 - ``ignored`` : csets ignored due to DAG topology
549 549 - ``current`` : the cset currently being bisected
550 550 """
551 551 # i18n: "bisect" is a keyword
552 552 status = getstring(x, _("bisect requires a string")).lower()
553 553 state = set(hbisect.get(repo, status))
554 554 return subset & state
555 555
556 556 # Backward-compatibility
557 557 # - no help entry so that we do not advertise it any more
558 558 @predicate('bisected', safe=True)
559 559 def bisected(repo, subset, x):
560 560 return bisect(repo, subset, x)
561 561
562 562 @predicate('bookmark([name])', safe=True)
563 563 def bookmark(repo, subset, x):
564 564 """The named bookmark or all bookmarks.
565 565
566 566 If `name` starts with `re:`, the remainder of the name is treated as
567 567 a regular expression. To match a bookmark that actually starts with `re:`,
568 568 use the prefix `literal:`.
569 569 """
570 570 # i18n: "bookmark" is a keyword
571 571 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
572 572 if args:
573 573 bm = getstring(args[0],
574 574 # i18n: "bookmark" is a keyword
575 575 _('the argument to bookmark must be a string'))
576 576 kind, pattern, matcher = util.stringmatcher(bm)
577 577 bms = set()
578 578 if kind == 'literal':
579 579 bmrev = repo._bookmarks.get(pattern, None)
580 580 if not bmrev:
581 581 raise error.RepoLookupError(_("bookmark '%s' does not exist")
582 582 % pattern)
583 583 bms.add(repo[bmrev].rev())
584 584 else:
585 585 matchrevs = set()
586 586 for name, bmrev in repo._bookmarks.iteritems():
587 587 if matcher(name):
588 588 matchrevs.add(bmrev)
589 589 if not matchrevs:
590 590 raise error.RepoLookupError(_("no bookmarks exist"
591 591 " that match '%s'") % pattern)
592 592 for bmrev in matchrevs:
593 593 bms.add(repo[bmrev].rev())
594 594 else:
595 595 bms = set([repo[r].rev()
596 596 for r in repo._bookmarks.values()])
597 597 bms -= set([node.nullrev])
598 598 return subset & bms
599 599
600 600 @predicate('branch(string or set)', safe=True)
601 601 def branch(repo, subset, x):
602 602 """
603 603 All changesets belonging to the given branch or the branches of the given
604 604 changesets.
605 605
606 606 If `string` starts with `re:`, the remainder of the name is treated as
607 607 a regular expression. To match a branch that actually starts with `re:`,
608 608 use the prefix `literal:`.
609 609 """
610 610 getbi = repo.revbranchcache().branchinfo
611 611
612 612 try:
613 613 b = getstring(x, '')
614 614 except error.ParseError:
615 615 # not a string, but another revspec, e.g. tip()
616 616 pass
617 617 else:
618 618 kind, pattern, matcher = util.stringmatcher(b)
619 619 if kind == 'literal':
620 620 # note: falls through to the revspec case if no branch with
621 621 # this name exists and pattern kind is not specified explicitly
622 622 if pattern in repo.branchmap():
623 623 return subset.filter(lambda r: matcher(getbi(r)[0]),
624 624 condrepr=('<branch %r>', b))
625 625 if b.startswith('literal:'):
626 626 raise error.RepoLookupError(_("branch '%s' does not exist")
627 627 % pattern)
628 628 else:
629 629 return subset.filter(lambda r: matcher(getbi(r)[0]),
630 630 condrepr=('<branch %r>', b))
631 631
632 632 s = getset(repo, fullreposet(repo), x)
633 633 b = set()
634 634 for r in s:
635 635 b.add(getbi(r)[0])
636 636 c = s.__contains__
637 637 return subset.filter(lambda r: c(r) or getbi(r)[0] in b,
638 638 condrepr=lambda: '<branch %r>' % sorted(b))
639 639
640 640 @predicate('bumped()', safe=True)
641 641 def bumped(repo, subset, x):
642 642 """Mutable changesets marked as successors of public changesets.
643 643
644 644 Only non-public and non-obsolete changesets can be `bumped`.
645 645 """
646 646 # i18n: "bumped" is a keyword
647 647 getargs(x, 0, 0, _("bumped takes no arguments"))
648 648 bumped = obsmod.getrevs(repo, 'bumped')
649 649 return subset & bumped
650 650
651 651 @predicate('bundle()', safe=True)
652 652 def bundle(repo, subset, x):
653 653 """Changesets in the bundle.
654 654
655 655 Bundle must be specified by the -R option."""
656 656
657 657 try:
658 658 bundlerevs = repo.changelog.bundlerevs
659 659 except AttributeError:
660 660 raise error.Abort(_("no bundle provided - specify with -R"))
661 661 return subset & bundlerevs
662 662
663 663 def checkstatus(repo, subset, pat, field):
664 664 hasset = matchmod.patkind(pat) == 'set'
665 665
666 666 mcache = [None]
667 667 def matches(x):
668 668 c = repo[x]
669 669 if not mcache[0] or hasset:
670 670 mcache[0] = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
671 671 m = mcache[0]
672 672 fname = None
673 673 if not m.anypats() and len(m.files()) == 1:
674 674 fname = m.files()[0]
675 675 if fname is not None:
676 676 if fname not in c.files():
677 677 return False
678 678 else:
679 679 for f in c.files():
680 680 if m(f):
681 681 break
682 682 else:
683 683 return False
684 684 files = repo.status(c.p1().node(), c.node())[field]
685 685 if fname is not None:
686 686 if fname in files:
687 687 return True
688 688 else:
689 689 for f in files:
690 690 if m(f):
691 691 return True
692 692
693 693 return subset.filter(matches, condrepr=('<status[%r] %r>', field, pat))
694 694
695 695 def _children(repo, narrow, parentset):
696 696 if not parentset:
697 697 return baseset()
698 698 cs = set()
699 699 pr = repo.changelog.parentrevs
700 700 minrev = parentset.min()
701 701 for r in narrow:
702 702 if r <= minrev:
703 703 continue
704 704 for p in pr(r):
705 705 if p in parentset:
706 706 cs.add(r)
707 707 # XXX using a set to feed the baseset is wrong. Sets are not ordered.
708 708 # This does not break because of other fullreposet misbehavior.
709 709 return baseset(cs)
710 710
711 711 @predicate('children(set)', safe=True)
712 712 def children(repo, subset, x):
713 713 """Child changesets of changesets in set.
714 714 """
715 715 s = getset(repo, fullreposet(repo), x)
716 716 cs = _children(repo, subset, s)
717 717 return subset & cs
718 718
719 719 @predicate('closed()', safe=True)
720 720 def closed(repo, subset, x):
721 721 """Changeset is closed.
722 722 """
723 723 # i18n: "closed" is a keyword
724 724 getargs(x, 0, 0, _("closed takes no arguments"))
725 725 return subset.filter(lambda r: repo[r].closesbranch(),
726 726 condrepr='<branch closed>')
727 727
728 728 @predicate('contains(pattern)')
729 729 def contains(repo, subset, x):
730 730 """The revision's manifest contains a file matching pattern (but might not
731 731 modify it). See :hg:`help patterns` for information about file patterns.
732 732
733 733 The pattern without explicit kind like ``glob:`` is expected to be
734 734 relative to the current directory and match against a file exactly
735 735 for efficiency.
736 736 """
737 737 # i18n: "contains" is a keyword
738 738 pat = getstring(x, _("contains requires a pattern"))
739 739
740 740 def matches(x):
741 741 if not matchmod.patkind(pat):
742 742 pats = pathutil.canonpath(repo.root, repo.getcwd(), pat)
743 743 if pats in repo[x]:
744 744 return True
745 745 else:
746 746 c = repo[x]
747 747 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
748 748 for f in c.manifest():
749 749 if m(f):
750 750 return True
751 751 return False
752 752
753 753 return subset.filter(matches, condrepr=('<contains %r>', pat))
754 754
755 755 @predicate('converted([id])', safe=True)
756 756 def converted(repo, subset, x):
757 757 """Changesets converted from the given identifier in the old repository if
758 758 present, or all converted changesets if no identifier is specified.
759 759 """
760 760
761 761 # There is exactly no chance of resolving the revision, so do a simple
762 762 # string compare and hope for the best
763 763
764 764 rev = None
765 765 # i18n: "converted" is a keyword
766 766 l = getargs(x, 0, 1, _('converted takes one or no arguments'))
767 767 if l:
768 768 # i18n: "converted" is a keyword
769 769 rev = getstring(l[0], _('converted requires a revision'))
770 770
771 771 def _matchvalue(r):
772 772 source = repo[r].extra().get('convert_revision', None)
773 773 return source is not None and (rev is None or source.startswith(rev))
774 774
775 775 return subset.filter(lambda r: _matchvalue(r),
776 776 condrepr=('<converted %r>', rev))
777 777
778 778 @predicate('date(interval)', safe=True)
779 779 def date(repo, subset, x):
780 780 """Changesets within the interval, see :hg:`help dates`.
781 781 """
782 782 # i18n: "date" is a keyword
783 783 ds = getstring(x, _("date requires a string"))
784 784 dm = util.matchdate(ds)
785 785 return subset.filter(lambda x: dm(repo[x].date()[0]),
786 786 condrepr=('<date %r>', ds))
787 787
788 788 @predicate('desc(string)', safe=True)
789 789 def desc(repo, subset, x):
790 790 """Search commit message for string. The match is case-insensitive.
791 791 """
792 792 # i18n: "desc" is a keyword
793 793 ds = encoding.lower(getstring(x, _("desc requires a string")))
794 794
795 795 def matches(x):
796 796 c = repo[x]
797 797 return ds in encoding.lower(c.description())
798 798
799 799 return subset.filter(matches, condrepr=('<desc %r>', ds))
800 800
801 801 def _descendants(repo, subset, x, followfirst=False):
802 802 roots = getset(repo, fullreposet(repo), x)
803 803 if not roots:
804 804 return baseset()
805 805 s = _revdescendants(repo, roots, followfirst)
806 806
807 807 # Both sets need to be ascending in order to lazily return the union
808 808 # in the correct order.
809 809 base = subset & roots
810 810 desc = subset & s
811 811 result = base + desc
812 812 if subset.isascending():
813 813 result.sort()
814 814 elif subset.isdescending():
815 815 result.sort(reverse=True)
816 816 else:
817 817 result = subset & result
818 818 return result
819 819
820 820 @predicate('descendants(set)', safe=True)
821 821 def descendants(repo, subset, x):
822 822 """Changesets which are descendants of changesets in set.
823 823 """
824 824 return _descendants(repo, subset, x)
825 825
826 826 @predicate('_firstdescendants', safe=True)
827 827 def _firstdescendants(repo, subset, x):
828 828 # ``_firstdescendants(set)``
829 829 # Like ``descendants(set)`` but follows only the first parents.
830 830 return _descendants(repo, subset, x, followfirst=True)
831 831
832 832 @predicate('destination([set])', safe=True)
833 833 def destination(repo, subset, x):
834 834 """Changesets that were created by a graft, transplant or rebase operation,
835 835 with the given revisions specified as the source. Omitting the optional set
836 836 is the same as passing all().
837 837 """
838 838 if x is not None:
839 839 sources = getset(repo, fullreposet(repo), x)
840 840 else:
841 841 sources = fullreposet(repo)
842 842
843 843 dests = set()
844 844
845 845 # subset contains all of the possible destinations that can be returned, so
846 846 # iterate over them and see if their source(s) were provided in the arg set.
847 847 # Even if the immediate src of r is not in the arg set, src's source (or
848 848 # further back) may be. Scanning back further than the immediate src allows
849 849 # transitive transplants and rebases to yield the same results as transitive
850 850 # grafts.
851 851 for r in subset:
852 852 src = _getrevsource(repo, r)
853 853 lineage = None
854 854
855 855 while src is not None:
856 856 if lineage is None:
857 857 lineage = list()
858 858
859 859 lineage.append(r)
860 860
861 861 # The visited lineage is a match if the current source is in the arg
862 862 # set. Since every candidate dest is visited by way of iterating
863 863 # subset, any dests further back in the lineage will be tested by a
864 864 # different iteration over subset. Likewise, if the src was already
865 865 # selected, the current lineage can be selected without going back
866 866 # further.
867 867 if src in sources or src in dests:
868 868 dests.update(lineage)
869 869 break
870 870
871 871 r = src
872 872 src = _getrevsource(repo, r)
873 873
874 874 return subset.filter(dests.__contains__,
875 875 condrepr=lambda: '<destination %r>' % sorted(dests))
876 876
877 877 @predicate('divergent()', safe=True)
878 878 def divergent(repo, subset, x):
879 879 """
880 880 Final successors of changesets with an alternative set of final successors.
881 881 """
882 882 # i18n: "divergent" is a keyword
883 883 getargs(x, 0, 0, _("divergent takes no arguments"))
884 884 divergent = obsmod.getrevs(repo, 'divergent')
885 885 return subset & divergent
886 886
887 887 @predicate('extinct()', safe=True)
888 888 def extinct(repo, subset, x):
889 889 """Obsolete changesets with obsolete descendants only.
890 890 """
891 891 # i18n: "extinct" is a keyword
892 892 getargs(x, 0, 0, _("extinct takes no arguments"))
893 893 extincts = obsmod.getrevs(repo, 'extinct')
894 894 return subset & extincts
895 895
896 896 @predicate('extra(label, [value])', safe=True)
897 897 def extra(repo, subset, x):
898 898 """Changesets with the given label in the extra metadata, with the given
899 899 optional value.
900 900
901 901 If `value` starts with `re:`, the remainder of the value is treated as
902 902 a regular expression. To match a value that actually starts with `re:`,
903 903 use the prefix `literal:`.
904 904 """
905 905 args = getargsdict(x, 'extra', 'label value')
906 906 if 'label' not in args:
907 907 # i18n: "extra" is a keyword
908 908 raise error.ParseError(_('extra takes at least 1 argument'))
909 909 # i18n: "extra" is a keyword
910 910 label = getstring(args['label'], _('first argument to extra must be '
911 911 'a string'))
912 912 value = None
913 913
914 914 if 'value' in args:
915 915 # i18n: "extra" is a keyword
916 916 value = getstring(args['value'], _('second argument to extra must be '
917 917 'a string'))
918 918 kind, value, matcher = util.stringmatcher(value)
919 919
920 920 def _matchvalue(r):
921 921 extra = repo[r].extra()
922 922 return label in extra and (value is None or matcher(extra[label]))
923 923
924 924 return subset.filter(lambda r: _matchvalue(r),
925 925 condrepr=('<extra[%r] %r>', label, value))
926 926
927 927 @predicate('filelog(pattern)', safe=True)
928 928 def filelog(repo, subset, x):
929 929 """Changesets connected to the specified filelog.
930 930
931 931 For performance reasons, visits only revisions mentioned in the file-level
932 932 filelog, rather than filtering through all changesets (much faster, but
933 933 doesn't include deletes or duplicate changes). For a slower, more accurate
934 934 result, use ``file()``.
935 935
936 936 The pattern without explicit kind like ``glob:`` is expected to be
937 937 relative to the current directory and match against a file exactly
938 938 for efficiency.
939 939
940 940 If some linkrev points to revisions filtered by the current repoview, we'll
941 941 work around it to return a non-filtered value.
942 942 """
943 943
944 944 # i18n: "filelog" is a keyword
945 945 pat = getstring(x, _("filelog requires a pattern"))
946 946 s = set()
947 947 cl = repo.changelog
948 948
949 949 if not matchmod.patkind(pat):
950 950 f = pathutil.canonpath(repo.root, repo.getcwd(), pat)
951 951 files = [f]
952 952 else:
953 953 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[None])
954 954 files = (f for f in repo[None] if m(f))
955 955
956 956 for f in files:
957 957 fl = repo.file(f)
958 958 known = {}
959 959 scanpos = 0
960 960 for fr in list(fl):
961 961 fn = fl.node(fr)
962 962 if fn in known:
963 963 s.add(known[fn])
964 964 continue
965 965
966 966 lr = fl.linkrev(fr)
967 967 if lr in cl:
968 968 s.add(lr)
969 969 elif scanpos is not None:
970 970 # lowest matching changeset is filtered, scan further
971 971 # ahead in changelog
972 972 start = max(lr, scanpos) + 1
973 973 scanpos = None
974 974 for r in cl.revs(start):
975 975 # minimize parsing of non-matching entries
976 976 if f in cl.revision(r) and f in cl.readfiles(r):
977 977 try:
978 978 # try to use manifest delta fastpath
979 979 n = repo[r].filenode(f)
980 980 if n not in known:
981 981 if n == fn:
982 982 s.add(r)
983 983 scanpos = r
984 984 break
985 985 else:
986 986 known[n] = r
987 987 except error.ManifestLookupError:
988 988 # deletion in changelog
989 989 continue
990 990
991 991 return subset & s
992 992
993 993 @predicate('first(set, [n])', safe=True)
994 994 def first(repo, subset, x):
995 995 """An alias for limit().
996 996 """
997 997 return limit(repo, subset, x)
998 998
999 999 def _follow(repo, subset, x, name, followfirst=False):
1000 1000 l = getargs(x, 0, 1, _("%s takes no arguments or a pattern") % name)
1001 1001 c = repo['.']
1002 1002 if l:
1003 1003 x = getstring(l[0], _("%s expected a pattern") % name)
1004 1004 matcher = matchmod.match(repo.root, repo.getcwd(), [x],
1005 1005 ctx=repo[None], default='path')
1006 1006
1007 1007 files = c.manifest().walk(matcher)
1008 1008
1009 1009 s = set()
1010 1010 for fname in files:
1011 1011 fctx = c[fname]
1012 1012 s = s.union(set(c.rev() for c in fctx.ancestors(followfirst)))
1013 1013 # include the revision responsible for the most recent version
1014 1014 s.add(fctx.introrev())
1015 1015 else:
1016 1016 s = _revancestors(repo, baseset([c.rev()]), followfirst)
1017 1017
1018 1018 return subset & s
1019 1019
1020 1020 @predicate('follow([pattern])', safe=True)
1021 1021 def follow(repo, subset, x):
1022 1022 """
1023 1023 An alias for ``::.`` (ancestors of the working directory's first parent).
1024 1024 If pattern is specified, the histories of files matching given
1025 1025 pattern is followed, including copies.
1026 1026 """
1027 1027 return _follow(repo, subset, x, 'follow')
1028 1028
1029 1029 @predicate('_followfirst', safe=True)
1030 1030 def _followfirst(repo, subset, x):
1031 1031 # ``followfirst([pattern])``
1032 1032 # Like ``follow([pattern])`` but follows only the first parent of
1033 1033 # every revisions or files revisions.
1034 1034 return _follow(repo, subset, x, '_followfirst', followfirst=True)
1035 1035
1036 1036 @predicate('all()', safe=True)
1037 1037 def getall(repo, subset, x):
1038 1038 """All changesets, the same as ``0:tip``.
1039 1039 """
1040 1040 # i18n: "all" is a keyword
1041 1041 getargs(x, 0, 0, _("all takes no arguments"))
1042 1042 return subset & spanset(repo) # drop "null" if any
1043 1043
1044 1044 @predicate('grep(regex)')
1045 1045 def grep(repo, subset, x):
1046 1046 """Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
1047 1047 to ensure special escape characters are handled correctly. Unlike
1048 1048 ``keyword(string)``, the match is case-sensitive.
1049 1049 """
1050 1050 try:
1051 1051 # i18n: "grep" is a keyword
1052 1052 gr = re.compile(getstring(x, _("grep requires a string")))
1053 1053 except re.error as e:
1054 1054 raise error.ParseError(_('invalid match pattern: %s') % e)
1055 1055
1056 1056 def matches(x):
1057 1057 c = repo[x]
1058 1058 for e in c.files() + [c.user(), c.description()]:
1059 1059 if gr.search(e):
1060 1060 return True
1061 1061 return False
1062 1062
1063 1063 return subset.filter(matches, condrepr=('<grep %r>', gr.pattern))
1064 1064
1065 1065 @predicate('_matchfiles', safe=True)
1066 1066 def _matchfiles(repo, subset, x):
1067 1067 # _matchfiles takes a revset list of prefixed arguments:
1068 1068 #
1069 1069 # [p:foo, i:bar, x:baz]
1070 1070 #
1071 1071 # builds a match object from them and filters subset. Allowed
1072 1072 # prefixes are 'p:' for regular patterns, 'i:' for include
1073 1073 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
1074 1074 # a revision identifier, or the empty string to reference the
1075 1075 # working directory, from which the match object is
1076 1076 # initialized. Use 'd:' to set the default matching mode, default
1077 1077 # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
1078 1078
1079 1079 l = getargs(x, 1, -1, "_matchfiles requires at least one argument")
1080 1080 pats, inc, exc = [], [], []
1081 1081 rev, default = None, None
1082 1082 for arg in l:
1083 1083 s = getstring(arg, "_matchfiles requires string arguments")
1084 1084 prefix, value = s[:2], s[2:]
1085 1085 if prefix == 'p:':
1086 1086 pats.append(value)
1087 1087 elif prefix == 'i:':
1088 1088 inc.append(value)
1089 1089 elif prefix == 'x:':
1090 1090 exc.append(value)
1091 1091 elif prefix == 'r:':
1092 1092 if rev is not None:
1093 1093 raise error.ParseError('_matchfiles expected at most one '
1094 1094 'revision')
1095 1095 if value != '': # empty means working directory; leave rev as None
1096 1096 rev = value
1097 1097 elif prefix == 'd:':
1098 1098 if default is not None:
1099 1099 raise error.ParseError('_matchfiles expected at most one '
1100 1100 'default mode')
1101 1101 default = value
1102 1102 else:
1103 1103 raise error.ParseError('invalid _matchfiles prefix: %s' % prefix)
1104 1104 if not default:
1105 1105 default = 'glob'
1106 1106
1107 1107 m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc,
1108 1108 exclude=exc, ctx=repo[rev], default=default)
1109 1109
1110 1110 # This directly read the changelog data as creating changectx for all
1111 1111 # revisions is quite expensive.
1112 1112 getfiles = repo.changelog.readfiles
1113 1113 wdirrev = node.wdirrev
1114 1114 def matches(x):
1115 1115 if x == wdirrev:
1116 1116 files = repo[x].files()
1117 1117 else:
1118 1118 files = getfiles(x)
1119 1119 for f in files:
1120 1120 if m(f):
1121 1121 return True
1122 1122 return False
1123 1123
1124 1124 return subset.filter(matches,
1125 1125 condrepr=('<matchfiles patterns=%r, include=%r '
1126 1126 'exclude=%r, default=%r, rev=%r>',
1127 1127 pats, inc, exc, default, rev))
1128 1128
1129 1129 @predicate('file(pattern)', safe=True)
1130 1130 def hasfile(repo, subset, x):
1131 1131 """Changesets affecting files matched by pattern.
1132 1132
1133 1133 For a faster but less accurate result, consider using ``filelog()``
1134 1134 instead.
1135 1135
1136 1136 This predicate uses ``glob:`` as the default kind of pattern.
1137 1137 """
1138 1138 # i18n: "file" is a keyword
1139 1139 pat = getstring(x, _("file requires a pattern"))
1140 1140 return _matchfiles(repo, subset, ('string', 'p:' + pat))
1141 1141
1142 1142 @predicate('head()', safe=True)
1143 1143 def head(repo, subset, x):
1144 1144 """Changeset is a named branch head.
1145 1145 """
1146 1146 # i18n: "head" is a keyword
1147 1147 getargs(x, 0, 0, _("head takes no arguments"))
1148 1148 hs = set()
1149 1149 cl = repo.changelog
1150 1150 for b, ls in repo.branchmap().iteritems():
1151 1151 hs.update(cl.rev(h) for h in ls)
1152 1152 # XXX using a set to feed the baseset is wrong. Sets are not ordered.
1153 1153 # This does not break because of other fullreposet misbehavior.
1154 1154 # XXX We should combine with subset first: 'subset & baseset(...)'. This is
1155 1155 # necessary to ensure we preserve the order in subset.
1156 1156 return baseset(hs) & subset
1157 1157
1158 1158 @predicate('heads(set)', safe=True)
1159 1159 def heads(repo, subset, x):
1160 1160 """Members of set with no children in set.
1161 1161 """
1162 1162 s = getset(repo, subset, x)
1163 1163 ps = parents(repo, subset, x)
1164 1164 return s - ps
1165 1165
1166 1166 @predicate('hidden()', safe=True)
1167 1167 def hidden(repo, subset, x):
1168 1168 """Hidden changesets.
1169 1169 """
1170 1170 # i18n: "hidden" is a keyword
1171 1171 getargs(x, 0, 0, _("hidden takes no arguments"))
1172 1172 hiddenrevs = repoview.filterrevs(repo, 'visible')
1173 1173 return subset & hiddenrevs
1174 1174
1175 1175 @predicate('keyword(string)', safe=True)
1176 1176 def keyword(repo, subset, x):
1177 1177 """Search commit message, user name, and names of changed files for
1178 1178 string. The match is case-insensitive.
1179 1179 """
1180 1180 # i18n: "keyword" is a keyword
1181 1181 kw = encoding.lower(getstring(x, _("keyword requires a string")))
1182 1182
1183 1183 def matches(r):
1184 1184 c = repo[r]
1185 1185 return any(kw in encoding.lower(t)
1186 1186 for t in c.files() + [c.user(), c.description()])
1187 1187
1188 1188 return subset.filter(matches, condrepr=('<keyword %r>', kw))
1189 1189
1190 1190 @predicate('limit(set[, n[, offset]])', safe=True)
1191 1191 def limit(repo, subset, x):
1192 1192 """First n members of set, defaulting to 1, starting from offset.
1193 1193 """
1194 1194 args = getargsdict(x, 'limit', 'set n offset')
1195 1195 if 'set' not in args:
1196 1196 # i18n: "limit" is a keyword
1197 1197 raise error.ParseError(_("limit requires one to three arguments"))
1198 1198 try:
1199 1199 lim, ofs = 1, 0
1200 1200 if 'n' in args:
1201 1201 # i18n: "limit" is a keyword
1202 1202 lim = int(getstring(args['n'], _("limit requires a number")))
1203 1203 if 'offset' in args:
1204 1204 # i18n: "limit" is a keyword
1205 1205 ofs = int(getstring(args['offset'], _("limit requires a number")))
1206 1206 if ofs < 0:
1207 1207 raise error.ParseError(_("negative offset"))
1208 1208 except (TypeError, ValueError):
1209 1209 # i18n: "limit" is a keyword
1210 1210 raise error.ParseError(_("limit expects a number"))
1211 1211 os = getset(repo, fullreposet(repo), args['set'])
1212 1212 result = []
1213 1213 it = iter(os)
1214 1214 for x in xrange(ofs):
1215 1215 y = next(it, None)
1216 1216 if y is None:
1217 1217 break
1218 1218 for x in xrange(lim):
1219 1219 y = next(it, None)
1220 1220 if y is None:
1221 1221 break
1222 1222 elif y in subset:
1223 1223 result.append(y)
1224 1224 return baseset(result, datarepr=('<limit n=%d, offset=%d, %r, %r>',
1225 1225 lim, ofs, subset, os))
1226 1226
1227 1227 @predicate('last(set, [n])', safe=True)
1228 1228 def last(repo, subset, x):
1229 1229 """Last n members of set, defaulting to 1.
1230 1230 """
1231 1231 # i18n: "last" is a keyword
1232 1232 l = getargs(x, 1, 2, _("last requires one or two arguments"))
1233 1233 try:
1234 1234 lim = 1
1235 1235 if len(l) == 2:
1236 1236 # i18n: "last" is a keyword
1237 1237 lim = int(getstring(l[1], _("last requires a number")))
1238 1238 except (TypeError, ValueError):
1239 1239 # i18n: "last" is a keyword
1240 1240 raise error.ParseError(_("last expects a number"))
1241 1241 os = getset(repo, fullreposet(repo), l[0])
1242 1242 os.reverse()
1243 1243 result = []
1244 1244 it = iter(os)
1245 1245 for x in xrange(lim):
1246 1246 y = next(it, None)
1247 1247 if y is None:
1248 1248 break
1249 1249 elif y in subset:
1250 1250 result.append(y)
1251 1251 return baseset(result, datarepr=('<last n=%d, %r, %r>', lim, subset, os))
1252 1252
1253 1253 @predicate('max(set)', safe=True)
1254 1254 def maxrev(repo, subset, x):
1255 1255 """Changeset with highest revision number in set.
1256 1256 """
1257 1257 os = getset(repo, fullreposet(repo), x)
1258 1258 try:
1259 1259 m = os.max()
1260 1260 if m in subset:
1261 1261 return baseset([m], datarepr=('<max %r, %r>', subset, os))
1262 1262 except ValueError:
1263 1263 # os.max() throws a ValueError when the collection is empty.
1264 1264 # Same as python's max().
1265 1265 pass
1266 1266 return baseset(datarepr=('<max %r, %r>', subset, os))
1267 1267
1268 1268 @predicate('merge()', safe=True)
1269 1269 def merge(repo, subset, x):
1270 1270 """Changeset is a merge changeset.
1271 1271 """
1272 1272 # i18n: "merge" is a keyword
1273 1273 getargs(x, 0, 0, _("merge takes no arguments"))
1274 1274 cl = repo.changelog
1275 1275 return subset.filter(lambda r: cl.parentrevs(r)[1] != -1,
1276 1276 condrepr='<merge>')
1277 1277
1278 1278 @predicate('branchpoint()', safe=True)
1279 1279 def branchpoint(repo, subset, x):
1280 1280 """Changesets with more than one child.
1281 1281 """
1282 1282 # i18n: "branchpoint" is a keyword
1283 1283 getargs(x, 0, 0, _("branchpoint takes no arguments"))
1284 1284 cl = repo.changelog
1285 1285 if not subset:
1286 1286 return baseset()
1287 1287 # XXX this should be 'parentset.min()' assuming 'parentset' is a smartset
1288 1288 # (and if it is not, it should.)
1289 1289 baserev = min(subset)
1290 1290 parentscount = [0]*(len(repo) - baserev)
1291 1291 for r in cl.revs(start=baserev + 1):
1292 1292 for p in cl.parentrevs(r):
1293 1293 if p >= baserev:
1294 1294 parentscount[p - baserev] += 1
1295 1295 return subset.filter(lambda r: parentscount[r - baserev] > 1,
1296 1296 condrepr='<branchpoint>')
1297 1297
1298 1298 @predicate('min(set)', safe=True)
1299 1299 def minrev(repo, subset, x):
1300 1300 """Changeset with lowest revision number in set.
1301 1301 """
1302 1302 os = getset(repo, fullreposet(repo), x)
1303 1303 try:
1304 1304 m = os.min()
1305 1305 if m in subset:
1306 1306 return baseset([m], datarepr=('<min %r, %r>', subset, os))
1307 1307 except ValueError:
1308 1308 # os.min() throws a ValueError when the collection is empty.
1309 1309 # Same as python's min().
1310 1310 pass
1311 1311 return baseset(datarepr=('<min %r, %r>', subset, os))
1312 1312
1313 1313 @predicate('modifies(pattern)', safe=True)
1314 1314 def modifies(repo, subset, x):
1315 1315 """Changesets modifying files matched by pattern.
1316 1316
1317 1317 The pattern without explicit kind like ``glob:`` is expected to be
1318 1318 relative to the current directory and match against a file or a
1319 1319 directory.
1320 1320 """
1321 1321 # i18n: "modifies" is a keyword
1322 1322 pat = getstring(x, _("modifies requires a pattern"))
1323 1323 return checkstatus(repo, subset, pat, 0)
1324 1324
1325 1325 @predicate('named(namespace)')
1326 1326 def named(repo, subset, x):
1327 1327 """The changesets in a given namespace.
1328 1328
1329 1329 If `namespace` starts with `re:`, the remainder of the string is treated as
1330 1330 a regular expression. To match a namespace that actually starts with `re:`,
1331 1331 use the prefix `literal:`.
1332 1332 """
1333 1333 # i18n: "named" is a keyword
1334 1334 args = getargs(x, 1, 1, _('named requires a namespace argument'))
1335 1335
1336 1336 ns = getstring(args[0],
1337 1337 # i18n: "named" is a keyword
1338 1338 _('the argument to named must be a string'))
1339 1339 kind, pattern, matcher = util.stringmatcher(ns)
1340 1340 namespaces = set()
1341 1341 if kind == 'literal':
1342 1342 if pattern not in repo.names:
1343 1343 raise error.RepoLookupError(_("namespace '%s' does not exist")
1344 1344 % ns)
1345 1345 namespaces.add(repo.names[pattern])
1346 1346 else:
1347 1347 for name, ns in repo.names.iteritems():
1348 1348 if matcher(name):
1349 1349 namespaces.add(ns)
1350 1350 if not namespaces:
1351 1351 raise error.RepoLookupError(_("no namespace exists"
1352 1352 " that match '%s'") % pattern)
1353 1353
1354 1354 names = set()
1355 1355 for ns in namespaces:
1356 1356 for name in ns.listnames(repo):
1357 1357 if name not in ns.deprecated:
1358 1358 names.update(repo[n].rev() for n in ns.nodes(repo, name))
1359 1359
1360 1360 names -= set([node.nullrev])
1361 1361 return subset & names
1362 1362
1363 1363 @predicate('id(string)', safe=True)
1364 1364 def node_(repo, subset, x):
1365 1365 """Revision non-ambiguously specified by the given hex string prefix.
1366 1366 """
1367 1367 # i18n: "id" is a keyword
1368 1368 l = getargs(x, 1, 1, _("id requires one argument"))
1369 1369 # i18n: "id" is a keyword
1370 1370 n = getstring(l[0], _("id requires a string"))
1371 1371 if len(n) == 40:
1372 1372 try:
1373 1373 rn = repo.changelog.rev(node.bin(n))
1374 1374 except (LookupError, TypeError):
1375 1375 rn = None
1376 1376 else:
1377 1377 rn = None
1378 1378 pm = repo.changelog._partialmatch(n)
1379 1379 if pm is not None:
1380 1380 rn = repo.changelog.rev(pm)
1381 1381
1382 1382 if rn is None:
1383 1383 return baseset()
1384 1384 result = baseset([rn])
1385 1385 return result & subset
1386 1386
1387 1387 @predicate('obsolete()', safe=True)
1388 1388 def obsolete(repo, subset, x):
1389 1389 """Mutable changeset with a newer version."""
1390 1390 # i18n: "obsolete" is a keyword
1391 1391 getargs(x, 0, 0, _("obsolete takes no arguments"))
1392 1392 obsoletes = obsmod.getrevs(repo, 'obsolete')
1393 1393 return subset & obsoletes
1394 1394
1395 1395 @predicate('only(set, [set])', safe=True)
1396 1396 def only(repo, subset, x):
1397 1397 """Changesets that are ancestors of the first set that are not ancestors
1398 1398 of any other head in the repo. If a second set is specified, the result
1399 1399 is ancestors of the first set that are not ancestors of the second set
1400 1400 (i.e. ::<set1> - ::<set2>).
1401 1401 """
1402 1402 cl = repo.changelog
1403 1403 # i18n: "only" is a keyword
1404 1404 args = getargs(x, 1, 2, _('only takes one or two arguments'))
1405 1405 include = getset(repo, fullreposet(repo), args[0])
1406 1406 if len(args) == 1:
1407 1407 if not include:
1408 1408 return baseset()
1409 1409
1410 1410 descendants = set(_revdescendants(repo, include, False))
1411 1411 exclude = [rev for rev in cl.headrevs()
1412 1412 if not rev in descendants and not rev in include]
1413 1413 else:
1414 1414 exclude = getset(repo, fullreposet(repo), args[1])
1415 1415
1416 1416 results = set(cl.findmissingrevs(common=exclude, heads=include))
1417 1417 # XXX we should turn this into a baseset instead of a set, smartset may do
1418 1418 # some optimisations from the fact this is a baseset.
1419 1419 return subset & results
1420 1420
1421 1421 @predicate('origin([set])', safe=True)
1422 1422 def origin(repo, subset, x):
1423 1423 """
1424 1424 Changesets that were specified as a source for the grafts, transplants or
1425 1425 rebases that created the given revisions. Omitting the optional set is the
1426 1426 same as passing all(). If a changeset created by these operations is itself
1427 1427 specified as a source for one of these operations, only the source changeset
1428 1428 for the first operation is selected.
1429 1429 """
1430 1430 if x is not None:
1431 1431 dests = getset(repo, fullreposet(repo), x)
1432 1432 else:
1433 1433 dests = fullreposet(repo)
1434 1434
1435 1435 def _firstsrc(rev):
1436 1436 src = _getrevsource(repo, rev)
1437 1437 if src is None:
1438 1438 return None
1439 1439
1440 1440 while True:
1441 1441 prev = _getrevsource(repo, src)
1442 1442
1443 1443 if prev is None:
1444 1444 return src
1445 1445 src = prev
1446 1446
1447 1447 o = set([_firstsrc(r) for r in dests])
1448 1448 o -= set([None])
1449 1449 # XXX we should turn this into a baseset instead of a set, smartset may do
1450 1450 # some optimisations from the fact this is a baseset.
1451 1451 return subset & o
1452 1452
1453 1453 @predicate('outgoing([path])', safe=True)
1454 1454 def outgoing(repo, subset, x):
1455 1455 """Changesets not found in the specified destination repository, or the
1456 1456 default push location.
1457 1457 """
1458 1458 # Avoid cycles.
1459 1459 from . import (
1460 1460 discovery,
1461 1461 hg,
1462 1462 )
1463 1463 # i18n: "outgoing" is a keyword
1464 1464 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
1465 1465 # i18n: "outgoing" is a keyword
1466 1466 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
1467 1467 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
1468 1468 dest, branches = hg.parseurl(dest)
1469 1469 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1470 1470 if revs:
1471 1471 revs = [repo.lookup(rev) for rev in revs]
1472 1472 other = hg.peer(repo, {}, dest)
1473 1473 repo.ui.pushbuffer()
1474 1474 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
1475 1475 repo.ui.popbuffer()
1476 1476 cl = repo.changelog
1477 1477 o = set([cl.rev(r) for r in outgoing.missing])
1478 1478 return subset & o
1479 1479
1480 1480 @predicate('p1([set])', safe=True)
1481 1481 def p1(repo, subset, x):
1482 1482 """First parent of changesets in set, or the working directory.
1483 1483 """
1484 1484 if x is None:
1485 1485 p = repo[x].p1().rev()
1486 1486 if p >= 0:
1487 1487 return subset & baseset([p])
1488 1488 return baseset()
1489 1489
1490 1490 ps = set()
1491 1491 cl = repo.changelog
1492 1492 for r in getset(repo, fullreposet(repo), x):
1493 1493 ps.add(cl.parentrevs(r)[0])
1494 1494 ps -= set([node.nullrev])
1495 1495 # XXX we should turn this into a baseset instead of a set, smartset may do
1496 1496 # some optimisations from the fact this is a baseset.
1497 1497 return subset & ps
1498 1498
1499 1499 @predicate('p2([set])', safe=True)
1500 1500 def p2(repo, subset, x):
1501 1501 """Second parent of changesets in set, or the working directory.
1502 1502 """
1503 1503 if x is None:
1504 1504 ps = repo[x].parents()
1505 1505 try:
1506 1506 p = ps[1].rev()
1507 1507 if p >= 0:
1508 1508 return subset & baseset([p])
1509 1509 return baseset()
1510 1510 except IndexError:
1511 1511 return baseset()
1512 1512
1513 1513 ps = set()
1514 1514 cl = repo.changelog
1515 1515 for r in getset(repo, fullreposet(repo), x):
1516 1516 ps.add(cl.parentrevs(r)[1])
1517 1517 ps -= set([node.nullrev])
1518 1518 # XXX we should turn this into a baseset instead of a set, smartset may do
1519 1519 # some optimisations from the fact this is a baseset.
1520 1520 return subset & ps
1521 1521
1522 1522 @predicate('parents([set])', safe=True)
1523 1523 def parents(repo, subset, x):
1524 1524 """
1525 1525 The set of all parents for all changesets in set, or the working directory.
1526 1526 """
1527 1527 if x is None:
1528 1528 ps = set(p.rev() for p in repo[x].parents())
1529 1529 else:
1530 1530 ps = set()
1531 1531 cl = repo.changelog
1532 1532 up = ps.update
1533 1533 parentrevs = cl.parentrevs
1534 1534 for r in getset(repo, fullreposet(repo), x):
1535 1535 if r == node.wdirrev:
1536 1536 up(p.rev() for p in repo[r].parents())
1537 1537 else:
1538 1538 up(parentrevs(r))
1539 1539 ps -= set([node.nullrev])
1540 1540 return subset & ps
1541 1541
1542 1542 def _phase(repo, subset, target):
1543 1543 """helper to select all rev in phase <target>"""
1544 1544 repo._phasecache.loadphaserevs(repo) # ensure phase's sets are loaded
1545 1545 if repo._phasecache._phasesets:
1546 1546 s = repo._phasecache._phasesets[target] - repo.changelog.filteredrevs
1547 1547 s = baseset(s)
1548 1548 s.sort() # set are non ordered, so we enforce ascending
1549 1549 return subset & s
1550 1550 else:
1551 1551 phase = repo._phasecache.phase
1552 1552 condition = lambda r: phase(repo, r) == target
1553 1553 return subset.filter(condition, condrepr=('<phase %r>', target),
1554 1554 cache=False)
1555 1555
1556 1556 @predicate('draft()', safe=True)
1557 1557 def draft(repo, subset, x):
1558 1558 """Changeset in draft phase."""
1559 1559 # i18n: "draft" is a keyword
1560 1560 getargs(x, 0, 0, _("draft takes no arguments"))
1561 1561 target = phases.draft
1562 1562 return _phase(repo, subset, target)
1563 1563
1564 1564 @predicate('secret()', safe=True)
1565 1565 def secret(repo, subset, x):
1566 1566 """Changeset in secret phase."""
1567 1567 # i18n: "secret" is a keyword
1568 1568 getargs(x, 0, 0, _("secret takes no arguments"))
1569 1569 target = phases.secret
1570 1570 return _phase(repo, subset, target)
1571 1571
1572 1572 def parentspec(repo, subset, x, n):
1573 1573 """``set^0``
1574 1574 The set.
1575 1575 ``set^1`` (or ``set^``), ``set^2``
1576 1576 First or second parent, respectively, of all changesets in set.
1577 1577 """
1578 1578 try:
1579 1579 n = int(n[1])
1580 1580 if n not in (0, 1, 2):
1581 1581 raise ValueError
1582 1582 except (TypeError, ValueError):
1583 1583 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
1584 1584 ps = set()
1585 1585 cl = repo.changelog
1586 1586 for r in getset(repo, fullreposet(repo), x):
1587 1587 if n == 0:
1588 1588 ps.add(r)
1589 1589 elif n == 1:
1590 1590 ps.add(cl.parentrevs(r)[0])
1591 1591 elif n == 2:
1592 1592 parents = cl.parentrevs(r)
1593 1593 if len(parents) > 1:
1594 1594 ps.add(parents[1])
1595 1595 return subset & ps
1596 1596
1597 1597 @predicate('present(set)', safe=True)
1598 1598 def present(repo, subset, x):
1599 1599 """An empty set, if any revision in set isn't found; otherwise,
1600 1600 all revisions in set.
1601 1601
1602 1602 If any of specified revisions is not present in the local repository,
1603 1603 the query is normally aborted. But this predicate allows the query
1604 1604 to continue even in such cases.
1605 1605 """
1606 1606 try:
1607 1607 return getset(repo, subset, x)
1608 1608 except error.RepoLookupError:
1609 1609 return baseset()
1610 1610
1611 1611 # for internal use
1612 1612 @predicate('_notpublic', safe=True)
1613 1613 def _notpublic(repo, subset, x):
1614 1614 getargs(x, 0, 0, "_notpublic takes no arguments")
1615 1615 repo._phasecache.loadphaserevs(repo) # ensure phase's sets are loaded
1616 1616 if repo._phasecache._phasesets:
1617 1617 s = set()
1618 1618 for u in repo._phasecache._phasesets[1:]:
1619 1619 s.update(u)
1620 1620 s = baseset(s - repo.changelog.filteredrevs)
1621 1621 s.sort()
1622 1622 return subset & s
1623 1623 else:
1624 1624 phase = repo._phasecache.phase
1625 1625 target = phases.public
1626 1626 condition = lambda r: phase(repo, r) != target
1627 1627 return subset.filter(condition, condrepr=('<phase %r>', target),
1628 1628 cache=False)
1629 1629
1630 1630 @predicate('public()', safe=True)
1631 1631 def public(repo, subset, x):
1632 1632 """Changeset in public phase."""
1633 1633 # i18n: "public" is a keyword
1634 1634 getargs(x, 0, 0, _("public takes no arguments"))
1635 1635 phase = repo._phasecache.phase
1636 1636 target = phases.public
1637 1637 condition = lambda r: phase(repo, r) == target
1638 1638 return subset.filter(condition, condrepr=('<phase %r>', target),
1639 1639 cache=False)
1640 1640
1641 1641 @predicate('remote([id [,path]])', safe=True)
1642 1642 def remote(repo, subset, x):
1643 1643 """Local revision that corresponds to the given identifier in a
1644 1644 remote repository, if present. Here, the '.' identifier is a
1645 1645 synonym for the current local branch.
1646 1646 """
1647 1647
1648 1648 from . import hg # avoid start-up nasties
1649 1649 # i18n: "remote" is a keyword
1650 1650 l = getargs(x, 0, 2, _("remote takes zero, one, or two arguments"))
1651 1651
1652 1652 q = '.'
1653 1653 if len(l) > 0:
1654 1654 # i18n: "remote" is a keyword
1655 1655 q = getstring(l[0], _("remote requires a string id"))
1656 1656 if q == '.':
1657 1657 q = repo['.'].branch()
1658 1658
1659 1659 dest = ''
1660 1660 if len(l) > 1:
1661 1661 # i18n: "remote" is a keyword
1662 1662 dest = getstring(l[1], _("remote requires a repository path"))
1663 1663 dest = repo.ui.expandpath(dest or 'default')
1664 1664 dest, branches = hg.parseurl(dest)
1665 1665 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1666 1666 if revs:
1667 1667 revs = [repo.lookup(rev) for rev in revs]
1668 1668 other = hg.peer(repo, {}, dest)
1669 1669 n = other.lookup(q)
1670 1670 if n in repo:
1671 1671 r = repo[n].rev()
1672 1672 if r in subset:
1673 1673 return baseset([r])
1674 1674 return baseset()
1675 1675
1676 1676 @predicate('removes(pattern)', safe=True)
1677 1677 def removes(repo, subset, x):
1678 1678 """Changesets which remove files matching pattern.
1679 1679
1680 1680 The pattern without explicit kind like ``glob:`` is expected to be
1681 1681 relative to the current directory and match against a file or a
1682 1682 directory.
1683 1683 """
1684 1684 # i18n: "removes" is a keyword
1685 1685 pat = getstring(x, _("removes requires a pattern"))
1686 1686 return checkstatus(repo, subset, pat, 2)
1687 1687
1688 1688 @predicate('rev(number)', safe=True)
1689 1689 def rev(repo, subset, x):
1690 1690 """Revision with the given numeric identifier.
1691 1691 """
1692 1692 # i18n: "rev" is a keyword
1693 1693 l = getargs(x, 1, 1, _("rev requires one argument"))
1694 1694 try:
1695 1695 # i18n: "rev" is a keyword
1696 1696 l = int(getstring(l[0], _("rev requires a number")))
1697 1697 except (TypeError, ValueError):
1698 1698 # i18n: "rev" is a keyword
1699 1699 raise error.ParseError(_("rev expects a number"))
1700 1700 if l not in repo.changelog and l != node.nullrev:
1701 1701 return baseset()
1702 1702 return subset & baseset([l])
1703 1703
1704 1704 @predicate('matching(revision [, field])', safe=True)
1705 1705 def matching(repo, subset, x):
1706 1706 """Changesets in which a given set of fields match the set of fields in the
1707 1707 selected revision or set.
1708 1708
1709 1709 To match more than one field pass the list of fields to match separated
1710 1710 by spaces (e.g. ``author description``).
1711 1711
1712 1712 Valid fields are most regular revision fields and some special fields.
1713 1713
1714 1714 Regular revision fields are ``description``, ``author``, ``branch``,
1715 1715 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
1716 1716 and ``diff``.
1717 1717 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
1718 1718 contents of the revision. Two revisions matching their ``diff`` will
1719 1719 also match their ``files``.
1720 1720
1721 1721 Special fields are ``summary`` and ``metadata``:
1722 1722 ``summary`` matches the first line of the description.
1723 1723 ``metadata`` is equivalent to matching ``description user date``
1724 1724 (i.e. it matches the main metadata fields).
1725 1725
1726 1726 ``metadata`` is the default field which is used when no fields are
1727 1727 specified. You can match more than one field at a time.
1728 1728 """
1729 1729 # i18n: "matching" is a keyword
1730 1730 l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments"))
1731 1731
1732 1732 revs = getset(repo, fullreposet(repo), l[0])
1733 1733
1734 1734 fieldlist = ['metadata']
1735 1735 if len(l) > 1:
1736 1736 fieldlist = getstring(l[1],
1737 1737 # i18n: "matching" is a keyword
1738 1738 _("matching requires a string "
1739 1739 "as its second argument")).split()
1740 1740
1741 1741 # Make sure that there are no repeated fields,
1742 1742 # expand the 'special' 'metadata' field type
1743 1743 # and check the 'files' whenever we check the 'diff'
1744 1744 fields = []
1745 1745 for field in fieldlist:
1746 1746 if field == 'metadata':
1747 1747 fields += ['user', 'description', 'date']
1748 1748 elif field == 'diff':
1749 1749 # a revision matching the diff must also match the files
1750 1750 # since matching the diff is very costly, make sure to
1751 1751 # also match the files first
1752 1752 fields += ['files', 'diff']
1753 1753 else:
1754 1754 if field == 'author':
1755 1755 field = 'user'
1756 1756 fields.append(field)
1757 1757 fields = set(fields)
1758 1758 if 'summary' in fields and 'description' in fields:
1759 1759 # If a revision matches its description it also matches its summary
1760 1760 fields.discard('summary')
1761 1761
1762 1762 # We may want to match more than one field
1763 1763 # Not all fields take the same amount of time to be matched
1764 1764 # Sort the selected fields in order of increasing matching cost
1765 1765 fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary',
1766 1766 'files', 'description', 'substate', 'diff']
1767 1767 def fieldkeyfunc(f):
1768 1768 try:
1769 1769 return fieldorder.index(f)
1770 1770 except ValueError:
1771 1771 # assume an unknown field is very costly
1772 1772 return len(fieldorder)
1773 1773 fields = list(fields)
1774 1774 fields.sort(key=fieldkeyfunc)
1775 1775
1776 1776 # Each field will be matched with its own "getfield" function
1777 1777 # which will be added to the getfieldfuncs array of functions
1778 1778 getfieldfuncs = []
1779 1779 _funcs = {
1780 1780 'user': lambda r: repo[r].user(),
1781 1781 'branch': lambda r: repo[r].branch(),
1782 1782 'date': lambda r: repo[r].date(),
1783 1783 'description': lambda r: repo[r].description(),
1784 1784 'files': lambda r: repo[r].files(),
1785 1785 'parents': lambda r: repo[r].parents(),
1786 1786 'phase': lambda r: repo[r].phase(),
1787 1787 'substate': lambda r: repo[r].substate,
1788 1788 'summary': lambda r: repo[r].description().splitlines()[0],
1789 1789 'diff': lambda r: list(repo[r].diff(git=True),)
1790 1790 }
1791 1791 for info in fields:
1792 1792 getfield = _funcs.get(info, None)
1793 1793 if getfield is None:
1794 1794 raise error.ParseError(
1795 1795 # i18n: "matching" is a keyword
1796 1796 _("unexpected field name passed to matching: %s") % info)
1797 1797 getfieldfuncs.append(getfield)
1798 1798 # convert the getfield array of functions into a "getinfo" function
1799 1799 # which returns an array of field values (or a single value if there
1800 1800 # is only one field to match)
1801 1801 getinfo = lambda r: [f(r) for f in getfieldfuncs]
1802 1802
1803 1803 def matches(x):
1804 1804 for rev in revs:
1805 1805 target = getinfo(rev)
1806 1806 match = True
1807 1807 for n, f in enumerate(getfieldfuncs):
1808 1808 if target[n] != f(x):
1809 1809 match = False
1810 1810 if match:
1811 1811 return True
1812 1812 return False
1813 1813
1814 1814 return subset.filter(matches, condrepr=('<matching%r %r>', fields, revs))
1815 1815
1816 1816 @predicate('reverse(set)', safe=True)
1817 1817 def reverse(repo, subset, x):
1818 1818 """Reverse order of set.
1819 1819 """
1820 1820 l = getset(repo, subset, x)
1821 1821 l.reverse()
1822 1822 return l
1823 1823
1824 1824 @predicate('roots(set)', safe=True)
1825 1825 def roots(repo, subset, x):
1826 1826 """Changesets in set with no parent changeset in set.
1827 1827 """
1828 1828 s = getset(repo, fullreposet(repo), x)
1829 1829 parents = repo.changelog.parentrevs
1830 1830 def filter(r):
1831 1831 for p in parents(r):
1832 1832 if 0 <= p and p in s:
1833 1833 return False
1834 1834 return True
1835 1835 return subset & s.filter(filter, condrepr='<roots>')
1836 1836
1837 1837 _sortkeyfuncs = {
1838 1838 'rev': lambda c: c.rev(),
1839 1839 'branch': lambda c: c.branch(),
1840 1840 'desc': lambda c: c.description(),
1841 1841 'user': lambda c: c.user(),
1842 1842 'author': lambda c: c.user(),
1843 1843 'date': lambda c: c.date()[0],
1844 1844 }
1845 1845
1846 1846 def _getsortargs(x):
1847 1847 """Parse sort options into (set, [(key, reverse)], opts)"""
1848 1848 args = getargsdict(x, 'sort', 'set keys topo.firstbranch')
1849 1849 if 'set' not in args:
1850 1850 # i18n: "sort" is a keyword
1851 1851 raise error.ParseError(_('sort requires one or two arguments'))
1852 1852 keys = "rev"
1853 1853 if 'keys' in args:
1854 1854 # i18n: "sort" is a keyword
1855 1855 keys = getstring(args['keys'], _("sort spec must be a string"))
1856 1856
1857 1857 keyflags = []
1858 1858 for k in keys.split():
1859 1859 fk = k
1860 1860 reverse = (k[0] == '-')
1861 1861 if reverse:
1862 1862 k = k[1:]
1863 1863 if k not in _sortkeyfuncs and k != 'topo':
1864 1864 raise error.ParseError(_("unknown sort key %r") % fk)
1865 1865 keyflags.append((k, reverse))
1866 1866
1867 1867 if len(keyflags) > 1 and any(k == 'topo' for k, reverse in keyflags):
1868 1868 # i18n: "topo" is a keyword
1869 1869 raise error.ParseError(_(
1870 1870 'topo sort order cannot be combined with other sort keys'))
1871 1871
1872 1872 opts = {}
1873 1873 if 'topo.firstbranch' in args:
1874 1874 if any(k == 'topo' for k, reverse in keyflags):
1875 1875 opts['topo.firstbranch'] = args['topo.firstbranch']
1876 1876 else:
1877 1877 # i18n: "topo" and "topo.firstbranch" are keywords
1878 1878 raise error.ParseError(_(
1879 1879 'topo.firstbranch can only be used when using the topo sort '
1880 1880 'key'))
1881 1881
1882 1882 return args['set'], keyflags, opts
1883 1883
1884 1884 @predicate('sort(set[, [-]key... [, ...]])', safe=True)
1885 1885 def sort(repo, subset, x):
1886 1886 """Sort set by keys. The default sort order is ascending, specify a key
1887 1887 as ``-key`` to sort in descending order.
1888 1888
1889 1889 The keys can be:
1890 1890
1891 1891 - ``rev`` for the revision number,
1892 1892 - ``branch`` for the branch name,
1893 1893 - ``desc`` for the commit message (description),
1894 1894 - ``user`` for user name (``author`` can be used as an alias),
1895 1895 - ``date`` for the commit date
1896 1896 - ``topo`` for a reverse topographical sort
1897 1897
1898 1898 The ``topo`` sort order cannot be combined with other sort keys. This sort
1899 1899 takes one optional argument, ``topo.firstbranch``, which takes a revset that
1900 1900 specifies what topographical branches to prioritize in the sort.
1901 1901
1902 1902 """
1903 1903 s, keyflags, opts = _getsortargs(x)
1904 1904 revs = getset(repo, subset, s)
1905 1905
1906 1906 if not keyflags:
1907 1907 return revs
1908 1908 if len(keyflags) == 1 and keyflags[0][0] == "rev":
1909 1909 revs.sort(reverse=keyflags[0][1])
1910 1910 return revs
1911 1911 elif keyflags[0][0] == "topo":
1912 1912 firstbranch = ()
1913 1913 if 'topo.firstbranch' in opts:
1914 1914 firstbranch = getset(repo, subset, opts['topo.firstbranch'])
1915 1915 revs = baseset(_toposort(revs, repo.changelog.parentrevs, firstbranch),
1916 1916 istopo=True)
1917 1917 if keyflags[0][1]:
1918 1918 revs.reverse()
1919 1919 return revs
1920 1920
1921 1921 # sort() is guaranteed to be stable
1922 1922 ctxs = [repo[r] for r in revs]
1923 1923 for k, reverse in reversed(keyflags):
1924 1924 ctxs.sort(key=_sortkeyfuncs[k], reverse=reverse)
1925 1925 return baseset([c.rev() for c in ctxs])
1926 1926
1927 1927 def _toposort(revs, parentsfunc, firstbranch=()):
1928 1928 """Yield revisions from heads to roots one (topo) branch at a time.
1929 1929
1930 1930 This function aims to be used by a graph generator that wishes to minimize
1931 1931 the number of parallel branches and their interleaving.
1932 1932
1933 1933 Example iteration order (numbers show the "true" order in a changelog):
1934 1934
1935 1935 o 4
1936 1936 |
1937 1937 o 1
1938 1938 |
1939 1939 | o 3
1940 1940 | |
1941 1941 | o 2
1942 1942 |/
1943 1943 o 0
1944 1944
1945 1945 Note that the ancestors of merges are understood by the current
1946 1946 algorithm to be on the same branch. This means no reordering will
1947 1947 occur behind a merge.
1948 1948 """
1949 1949
1950 1950 ### Quick summary of the algorithm
1951 1951 #
1952 1952 # This function is based around a "retention" principle. We keep revisions
1953 1953 # in memory until we are ready to emit a whole branch that immediately
1954 1954 # "merges" into an existing one. This reduces the number of parallel
1955 1955 # branches with interleaved revisions.
1956 1956 #
1957 1957 # During iteration revs are split into two groups:
1958 1958 # A) revision already emitted
1959 1959 # B) revision in "retention". They are stored as different subgroups.
1960 1960 #
1961 1961 # for each REV, we do the following logic:
1962 1962 #
1963 1963 # 1) if REV is a parent of (A), we will emit it. If there is a
1964 1964 # retention group ((B) above) that is blocked on REV being
1965 1965 # available, we emit all the revisions out of that retention
1966 1966 # group first.
1967 1967 #
1968 1968 # 2) else, we'll search for a subgroup in (B) awaiting for REV to be
1969 1969 # available, if such subgroup exist, we add REV to it and the subgroup is
1970 1970 # now awaiting for REV.parents() to be available.
1971 1971 #
1972 1972 # 3) finally if no such group existed in (B), we create a new subgroup.
1973 1973 #
1974 1974 #
1975 1975 # To bootstrap the algorithm, we emit the tipmost revision (which
1976 1976 # puts it in group (A) from above).
1977 1977
1978 1978 revs.sort(reverse=True)
1979 1979
1980 1980 # Set of parents of revision that have been emitted. They can be considered
1981 1981 # unblocked as the graph generator is already aware of them so there is no
1982 1982 # need to delay the revisions that reference them.
1983 1983 #
1984 1984 # If someone wants to prioritize a branch over the others, pre-filling this
1985 1985 # set will force all other branches to wait until this branch is ready to be
1986 1986 # emitted.
1987 1987 unblocked = set(firstbranch)
1988 1988
1989 1989 # list of groups waiting to be displayed, each group is defined by:
1990 1990 #
1991 1991 # (revs: lists of revs waiting to be displayed,
1992 1992 # blocked: set of that cannot be displayed before those in 'revs')
1993 1993 #
1994 1994 # The second value ('blocked') correspond to parents of any revision in the
1995 1995 # group ('revs') that is not itself contained in the group. The main idea
1996 1996 # of this algorithm is to delay as much as possible the emission of any
1997 1997 # revision. This means waiting for the moment we are about to display
1998 1998 # these parents to display the revs in a group.
1999 1999 #
2000 2000 # This first implementation is smart until it encounters a merge: it will
2001 2001 # emit revs as soon as any parent is about to be emitted and can grow an
2002 2002 # arbitrary number of revs in 'blocked'. In practice this mean we properly
2003 2003 # retains new branches but gives up on any special ordering for ancestors
2004 2004 # of merges. The implementation can be improved to handle this better.
2005 2005 #
2006 2006 # The first subgroup is special. It corresponds to all the revision that
2007 2007 # were already emitted. The 'revs' lists is expected to be empty and the
2008 2008 # 'blocked' set contains the parents revisions of already emitted revision.
2009 2009 #
2010 2010 # You could pre-seed the <parents> set of groups[0] to a specific
2011 2011 # changesets to select what the first emitted branch should be.
2012 2012 groups = [([], unblocked)]
2013 2013 pendingheap = []
2014 2014 pendingset = set()
2015 2015
2016 2016 heapq.heapify(pendingheap)
2017 2017 heappop = heapq.heappop
2018 2018 heappush = heapq.heappush
2019 2019 for currentrev in revs:
2020 2020 # Heap works with smallest element, we want highest so we invert
2021 2021 if currentrev not in pendingset:
2022 2022 heappush(pendingheap, -currentrev)
2023 2023 pendingset.add(currentrev)
2024 2024 # iterates on pending rev until after the current rev have been
2025 2025 # processed.
2026 2026 rev = None
2027 2027 while rev != currentrev:
2028 2028 rev = -heappop(pendingheap)
2029 2029 pendingset.remove(rev)
2030 2030
2031 2031 # Seek for a subgroup blocked, waiting for the current revision.
2032 2032 matching = [i for i, g in enumerate(groups) if rev in g[1]]
2033 2033
2034 2034 if matching:
2035 2035 # The main idea is to gather together all sets that are blocked
2036 2036 # on the same revision.
2037 2037 #
2038 2038 # Groups are merged when a common blocking ancestor is
2039 2039 # observed. For example, given two groups:
2040 2040 #
2041 2041 # revs [5, 4] waiting for 1
2042 2042 # revs [3, 2] waiting for 1
2043 2043 #
2044 2044 # These two groups will be merged when we process
2045 2045 # 1. In theory, we could have merged the groups when
2046 2046 # we added 2 to the group it is now in (we could have
2047 2047 # noticed the groups were both blocked on 1 then), but
2048 2048 # the way it works now makes the algorithm simpler.
2049 2049 #
2050 2050 # We also always keep the oldest subgroup first. We can
2051 2051 # probably improve the behavior by having the longest set
2052 2052 # first. That way, graph algorithms could minimise the length
2053 2053 # of parallel lines their drawing. This is currently not done.
2054 2054 targetidx = matching.pop(0)
2055 2055 trevs, tparents = groups[targetidx]
2056 2056 for i in matching:
2057 2057 gr = groups[i]
2058 2058 trevs.extend(gr[0])
2059 2059 tparents |= gr[1]
2060 2060 # delete all merged subgroups (except the one we kept)
2061 2061 # (starting from the last subgroup for performance and
2062 2062 # sanity reasons)
2063 2063 for i in reversed(matching):
2064 2064 del groups[i]
2065 2065 else:
2066 2066 # This is a new head. We create a new subgroup for it.
2067 2067 targetidx = len(groups)
2068 2068 groups.append(([], set([rev])))
2069 2069
2070 2070 gr = groups[targetidx]
2071 2071
2072 2072 # We now add the current nodes to this subgroups. This is done
2073 2073 # after the subgroup merging because all elements from a subgroup
2074 2074 # that relied on this rev must precede it.
2075 2075 #
2076 2076 # we also update the <parents> set to include the parents of the
2077 2077 # new nodes.
2078 2078 if rev == currentrev: # only display stuff in rev
2079 2079 gr[0].append(rev)
2080 2080 gr[1].remove(rev)
2081 2081 parents = [p for p in parentsfunc(rev) if p > node.nullrev]
2082 2082 gr[1].update(parents)
2083 2083 for p in parents:
2084 2084 if p not in pendingset:
2085 2085 pendingset.add(p)
2086 2086 heappush(pendingheap, -p)
2087 2087
2088 2088 # Look for a subgroup to display
2089 2089 #
2090 2090 # When unblocked is empty (if clause), we were not waiting for any
2091 2091 # revisions during the first iteration (if no priority was given) or
2092 2092 # if we emitted a whole disconnected set of the graph (reached a
2093 2093 # root). In that case we arbitrarily take the oldest known
2094 2094 # subgroup. The heuristic could probably be better.
2095 2095 #
2096 2096 # Otherwise (elif clause) if the subgroup is blocked on
2097 2097 # a revision we just emitted, we can safely emit it as
2098 2098 # well.
2099 2099 if not unblocked:
2100 2100 if len(groups) > 1: # display other subset
2101 2101 targetidx = 1
2102 2102 gr = groups[1]
2103 2103 elif not gr[1] & unblocked:
2104 2104 gr = None
2105 2105
2106 2106 if gr is not None:
2107 2107 # update the set of awaited revisions with the one from the
2108 2108 # subgroup
2109 2109 unblocked |= gr[1]
2110 2110 # output all revisions in the subgroup
2111 2111 for r in gr[0]:
2112 2112 yield r
2113 2113 # delete the subgroup that you just output
2114 2114 # unless it is groups[0] in which case you just empty it.
2115 2115 if targetidx:
2116 2116 del groups[targetidx]
2117 2117 else:
2118 2118 gr[0][:] = []
2119 2119 # Check if we have some subgroup waiting for revisions we are not going to
2120 2120 # iterate over
2121 2121 for g in groups:
2122 2122 for r in g[0]:
2123 2123 yield r
2124 2124
2125 2125 @predicate('subrepo([pattern])')
2126 2126 def subrepo(repo, subset, x):
2127 2127 """Changesets that add, modify or remove the given subrepo. If no subrepo
2128 2128 pattern is named, any subrepo changes are returned.
2129 2129 """
2130 2130 # i18n: "subrepo" is a keyword
2131 2131 args = getargs(x, 0, 1, _('subrepo takes at most one argument'))
2132 2132 pat = None
2133 2133 if len(args) != 0:
2134 2134 pat = getstring(args[0], _("subrepo requires a pattern"))
2135 2135
2136 2136 m = matchmod.exact(repo.root, repo.root, ['.hgsubstate'])
2137 2137
2138 2138 def submatches(names):
2139 2139 k, p, m = util.stringmatcher(pat)
2140 2140 for name in names:
2141 2141 if m(name):
2142 2142 yield name
2143 2143
2144 2144 def matches(x):
2145 2145 c = repo[x]
2146 2146 s = repo.status(c.p1().node(), c.node(), match=m)
2147 2147
2148 2148 if pat is None:
2149 2149 return s.added or s.modified or s.removed
2150 2150
2151 2151 if s.added:
2152 2152 return any(submatches(c.substate.keys()))
2153 2153
2154 2154 if s.modified:
2155 2155 subs = set(c.p1().substate.keys())
2156 2156 subs.update(c.substate.keys())
2157 2157
2158 2158 for path in submatches(subs):
2159 2159 if c.p1().substate.get(path) != c.substate.get(path):
2160 2160 return True
2161 2161
2162 2162 if s.removed:
2163 2163 return any(submatches(c.p1().substate.keys()))
2164 2164
2165 2165 return False
2166 2166
2167 2167 return subset.filter(matches, condrepr=('<subrepo %r>', pat))
2168 2168
2169 2169 def _substringmatcher(pattern):
2170 2170 kind, pattern, matcher = util.stringmatcher(pattern)
2171 2171 if kind == 'literal':
2172 2172 matcher = lambda s: pattern in s
2173 2173 return kind, pattern, matcher
2174 2174
2175 2175 @predicate('tag([name])', safe=True)
2176 2176 def tag(repo, subset, x):
2177 2177 """The specified tag by name, or all tagged revisions if no name is given.
2178 2178
2179 2179 If `name` starts with `re:`, the remainder of the name is treated as
2180 2180 a regular expression. To match a tag that actually starts with `re:`,
2181 2181 use the prefix `literal:`.
2182 2182 """
2183 2183 # i18n: "tag" is a keyword
2184 2184 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
2185 2185 cl = repo.changelog
2186 2186 if args:
2187 2187 pattern = getstring(args[0],
2188 2188 # i18n: "tag" is a keyword
2189 2189 _('the argument to tag must be a string'))
2190 2190 kind, pattern, matcher = util.stringmatcher(pattern)
2191 2191 if kind == 'literal':
2192 2192 # avoid resolving all tags
2193 2193 tn = repo._tagscache.tags.get(pattern, None)
2194 2194 if tn is None:
2195 2195 raise error.RepoLookupError(_("tag '%s' does not exist")
2196 2196 % pattern)
2197 2197 s = set([repo[tn].rev()])
2198 2198 else:
2199 2199 s = set([cl.rev(n) for t, n in repo.tagslist() if matcher(t)])
2200 2200 else:
2201 2201 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
2202 2202 return subset & s
2203 2203
2204 2204 @predicate('tagged', safe=True)
2205 2205 def tagged(repo, subset, x):
2206 2206 return tag(repo, subset, x)
2207 2207
2208 2208 @predicate('unstable()', safe=True)
2209 2209 def unstable(repo, subset, x):
2210 2210 """Non-obsolete changesets with obsolete ancestors.
2211 2211 """
2212 2212 # i18n: "unstable" is a keyword
2213 2213 getargs(x, 0, 0, _("unstable takes no arguments"))
2214 2214 unstables = obsmod.getrevs(repo, 'unstable')
2215 2215 return subset & unstables
2216 2216
2217 2217
2218 2218 @predicate('user(string)', safe=True)
2219 2219 def user(repo, subset, x):
2220 2220 """User name contains string. The match is case-insensitive.
2221 2221
2222 2222 If `string` starts with `re:`, the remainder of the string is treated as
2223 2223 a regular expression. To match a user that actually contains `re:`, use
2224 2224 the prefix `literal:`.
2225 2225 """
2226 2226 return author(repo, subset, x)
2227 2227
2228 2228 # experimental
2229 2229 @predicate('wdir', safe=True)
2230 2230 def wdir(repo, subset, x):
2231 2231 # i18n: "wdir" is a keyword
2232 2232 getargs(x, 0, 0, _("wdir takes no arguments"))
2233 2233 if node.wdirrev in subset or isinstance(subset, fullreposet):
2234 2234 return baseset([node.wdirrev])
2235 2235 return baseset()
2236 2236
2237 2237 # for internal use
2238 2238 @predicate('_list', safe=True)
2239 2239 def _list(repo, subset, x):
2240 2240 s = getstring(x, "internal error")
2241 2241 if not s:
2242 2242 return baseset()
2243 2243 # remove duplicates here. it's difficult for caller to deduplicate sets
2244 2244 # because different symbols can point to the same rev.
2245 2245 cl = repo.changelog
2246 2246 ls = []
2247 2247 seen = set()
2248 2248 for t in s.split('\0'):
2249 2249 try:
2250 2250 # fast path for integer revision
2251 2251 r = int(t)
2252 2252 if str(r) != t or r not in cl:
2253 2253 raise ValueError
2254 2254 revs = [r]
2255 2255 except ValueError:
2256 2256 revs = stringset(repo, subset, t)
2257 2257
2258 2258 for r in revs:
2259 2259 if r in seen:
2260 2260 continue
2261 2261 if (r in subset
2262 2262 or r == node.nullrev and isinstance(subset, fullreposet)):
2263 2263 ls.append(r)
2264 2264 seen.add(r)
2265 2265 return baseset(ls)
2266 2266
2267 2267 # for internal use
2268 2268 @predicate('_intlist', safe=True)
2269 2269 def _intlist(repo, subset, x):
2270 2270 s = getstring(x, "internal error")
2271 2271 if not s:
2272 2272 return baseset()
2273 2273 ls = [int(r) for r in s.split('\0')]
2274 2274 s = subset
2275 2275 return baseset([r for r in ls if r in s])
2276 2276
2277 2277 # for internal use
2278 2278 @predicate('_hexlist', safe=True)
2279 2279 def _hexlist(repo, subset, x):
2280 2280 s = getstring(x, "internal error")
2281 2281 if not s:
2282 2282 return baseset()
2283 2283 cl = repo.changelog
2284 2284 ls = [cl.rev(node.bin(r)) for r in s.split('\0')]
2285 2285 s = subset
2286 2286 return baseset([r for r in ls if r in s])
2287 2287
2288 2288 methods = {
2289 2289 "range": rangeset,
2290 2290 "dagrange": dagrange,
2291 2291 "string": stringset,
2292 2292 "symbol": stringset,
2293 2293 "and": andset,
2294 2294 "or": orset,
2295 2295 "not": notset,
2296 2296 "difference": differenceset,
2297 2297 "list": listset,
2298 2298 "keyvalue": keyvaluepair,
2299 2299 "func": func,
2300 2300 "ancestor": ancestorspec,
2301 2301 "parent": parentspec,
2302 2302 "parentpost": p1,
2303 2303 }
2304 2304
2305 2305 def _matchonly(revs, bases):
2306 2306 """
2307 2307 >>> f = lambda *args: _matchonly(*map(parse, args))
2308 2308 >>> f('ancestors(A)', 'not ancestors(B)')
2309 2309 ('list', ('symbol', 'A'), ('symbol', 'B'))
2310 2310 """
2311 2311 if (revs is not None
2312 2312 and revs[0] == 'func'
2313 2313 and getstring(revs[1], _('not a symbol')) == 'ancestors'
2314 2314 and bases is not None
2315 2315 and bases[0] == 'not'
2316 2316 and bases[1][0] == 'func'
2317 2317 and getstring(bases[1][1], _('not a symbol')) == 'ancestors'):
2318 2318 return ('list', revs[2], bases[1][2])
2319 2319
2320 2320 def _optimize(x, small):
2321 2321 if x is None:
2322 2322 return 0, x
2323 2323
2324 2324 smallbonus = 1
2325 2325 if small:
2326 2326 smallbonus = .5
2327 2327
2328 2328 op = x[0]
2329 2329 if op == 'minus':
2330 2330 return _optimize(('and', x[1], ('not', x[2])), small)
2331 2331 elif op == 'only':
2332 2332 t = ('func', ('symbol', 'only'), ('list', x[1], x[2]))
2333 2333 return _optimize(t, small)
2334 2334 elif op == 'onlypost':
2335 2335 return _optimize(('func', ('symbol', 'only'), x[1]), small)
2336 2336 elif op == 'dagrangepre':
2337 2337 return _optimize(('func', ('symbol', 'ancestors'), x[1]), small)
2338 2338 elif op == 'dagrangepost':
2339 2339 return _optimize(('func', ('symbol', 'descendants'), x[1]), small)
2340 2340 elif op == 'rangeall':
2341 2341 return _optimize(('range', ('string', '0'), ('string', 'tip')), small)
2342 2342 elif op == 'rangepre':
2343 2343 return _optimize(('range', ('string', '0'), x[1]), small)
2344 2344 elif op == 'rangepost':
2345 2345 return _optimize(('range', x[1], ('string', 'tip')), small)
2346 2346 elif op == 'negate':
2347 2347 s = getstring(x[1], _("can't negate that"))
2348 2348 return _optimize(('string', '-' + s), small)
2349 2349 elif op in 'string symbol negate':
2350 2350 return smallbonus, x # single revisions are small
2351 2351 elif op == 'and':
2352 2352 wa, ta = _optimize(x[1], True)
2353 2353 wb, tb = _optimize(x[2], True)
2354 2354 w = min(wa, wb)
2355 2355
2356 2356 # (::x and not ::y)/(not ::y and ::x) have a fast path
2357 2357 tm = _matchonly(ta, tb) or _matchonly(tb, ta)
2358 2358 if tm:
2359 2359 return w, ('func', ('symbol', 'only'), tm)
2360 2360
2361 2361 if tb is not None and tb[0] == 'not':
2362 2362 return wa, ('difference', ta, tb[1])
2363 2363
2364 2364 if wa > wb:
2365 2365 return w, (op, tb, ta)
2366 2366 return w, (op, ta, tb)
2367 2367 elif op == 'or':
2368 2368 # fast path for machine-generated expression, that is likely to have
2369 2369 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
2370 2370 ws, ts, ss = [], [], []
2371 2371 def flushss():
2372 2372 if not ss:
2373 2373 return
2374 2374 if len(ss) == 1:
2375 2375 w, t = ss[0]
2376 2376 else:
2377 2377 s = '\0'.join(t[1] for w, t in ss)
2378 2378 y = ('func', ('symbol', '_list'), ('string', s))
2379 2379 w, t = _optimize(y, False)
2380 2380 ws.append(w)
2381 2381 ts.append(t)
2382 2382 del ss[:]
2383 2383 for y in x[1:]:
2384 2384 w, t = _optimize(y, False)
2385 2385 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
2386 2386 ss.append((w, t))
2387 2387 continue
2388 2388 flushss()
2389 2389 ws.append(w)
2390 2390 ts.append(t)
2391 2391 flushss()
2392 2392 if len(ts) == 1:
2393 2393 return ws[0], ts[0] # 'or' operation is fully optimized out
2394 2394 # we can't reorder trees by weight because it would change the order.
2395 2395 # ("sort(a + b)" == "sort(b + a)", but "a + b" != "b + a")
2396 2396 # ts = tuple(t for w, t in sorted(zip(ws, ts), key=lambda wt: wt[0]))
2397 2397 return max(ws), (op,) + tuple(ts)
2398 2398 elif op == 'not':
2399 2399 # Optimize not public() to _notpublic() because we have a fast version
2400 2400 if x[1] == ('func', ('symbol', 'public'), None):
2401 2401 newsym = ('func', ('symbol', '_notpublic'), None)
2402 2402 o = _optimize(newsym, not small)
2403 2403 return o[0], o[1]
2404 2404 else:
2405 2405 o = _optimize(x[1], not small)
2406 2406 return o[0], (op, o[1])
2407 2407 elif op == 'parentpost':
2408 2408 o = _optimize(x[1], small)
2409 2409 return o[0], (op, o[1])
2410 2410 elif op == 'group':
2411 2411 return _optimize(x[1], small)
2412 2412 elif op in 'dagrange range parent ancestorspec':
2413 2413 if op == 'parent':
2414 2414 # x^:y means (x^) : y, not x ^ (:y)
2415 2415 post = ('parentpost', x[1])
2416 2416 if x[2][0] == 'dagrangepre':
2417 2417 return _optimize(('dagrange', post, x[2][1]), small)
2418 2418 elif x[2][0] == 'rangepre':
2419 2419 return _optimize(('range', post, x[2][1]), small)
2420 2420
2421 2421 wa, ta = _optimize(x[1], small)
2422 2422 wb, tb = _optimize(x[2], small)
2423 2423 return wa + wb, (op, ta, tb)
2424 2424 elif op == 'list':
2425 2425 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
2426 2426 return sum(ws), (op,) + ts
2427 2427 elif op == 'func':
2428 2428 f = getstring(x[1], _("not a symbol"))
2429 2429 wa, ta = _optimize(x[2], small)
2430 2430 if f in ("author branch closed date desc file grep keyword "
2431 2431 "outgoing user"):
2432 2432 w = 10 # slow
2433 2433 elif f in "modifies adds removes":
2434 2434 w = 30 # slower
2435 2435 elif f == "contains":
2436 2436 w = 100 # very slow
2437 2437 elif f == "ancestor":
2438 2438 w = 1 * smallbonus
2439 2439 elif f in "reverse limit first _intlist":
2440 2440 w = 0
2441 2441 elif f in "sort":
2442 2442 w = 10 # assume most sorts look at changelog
2443 2443 else:
2444 2444 w = 1
2445 2445 return w + wa, (op, x[1], ta)
2446 2446 return 1, x
2447 2447
2448 2448 def optimize(tree):
2449 2449 _weight, newtree = _optimize(tree, small=True)
2450 2450 return newtree
2451 2451
2452 2452 # the set of valid characters for the initial letter of symbols in
2453 2453 # alias declarations and definitions
2454 2454 _aliassyminitletters = set(c for c in [chr(i) for i in xrange(256)]
2455 2455 if c.isalnum() or c in '._@$' or ord(c) > 127)
2456 2456
2457 2457 def _parsewith(spec, lookup=None, syminitletters=None):
2458 2458 """Generate a parse tree of given spec with given tokenizing options
2459 2459
2460 2460 >>> _parsewith('foo($1)', syminitletters=_aliassyminitletters)
2461 2461 ('func', ('symbol', 'foo'), ('symbol', '$1'))
2462 2462 >>> _parsewith('$1')
2463 2463 Traceback (most recent call last):
2464 2464 ...
2465 2465 ParseError: ("syntax error in revset '$1'", 0)
2466 2466 >>> _parsewith('foo bar')
2467 2467 Traceback (most recent call last):
2468 2468 ...
2469 2469 ParseError: ('invalid token', 4)
2470 2470 """
2471 2471 p = parser.parser(elements)
2472 2472 tree, pos = p.parse(tokenize(spec, lookup=lookup,
2473 2473 syminitletters=syminitletters))
2474 2474 if pos != len(spec):
2475 2475 raise error.ParseError(_('invalid token'), pos)
2476 2476 return parser.simplifyinfixops(tree, ('list', 'or'))
2477 2477
2478 2478 class _aliasrules(parser.basealiasrules):
2479 2479 """Parsing and expansion rule set of revset aliases"""
2480 2480 _section = _('revset alias')
2481 2481
2482 2482 @staticmethod
2483 2483 def _parse(spec):
2484 2484 """Parse alias declaration/definition ``spec``
2485 2485
2486 2486 This allows symbol names to use also ``$`` as an initial letter
2487 2487 (for backward compatibility), and callers of this function should
2488 2488 examine whether ``$`` is used also for unexpected symbols or not.
2489 2489 """
2490 2490 return _parsewith(spec, syminitletters=_aliassyminitletters)
2491 2491
2492 2492 @staticmethod
2493 2493 def _trygetfunc(tree):
2494 2494 if tree[0] == 'func' and tree[1][0] == 'symbol':
2495 2495 return tree[1][1], getlist(tree[2])
2496 2496
2497 2497 def expandaliases(ui, tree, showwarning=None):
2498 2498 aliases = _aliasrules.buildmap(ui.configitems('revsetalias'))
2499 2499 tree = _aliasrules.expand(aliases, tree)
2500 2500 if showwarning:
2501 2501 # warn about problematic (but not referred) aliases
2502 2502 for name, alias in sorted(aliases.iteritems()):
2503 2503 if alias.error and not alias.warned:
2504 2504 showwarning(_('warning: %s\n') % (alias.error))
2505 2505 alias.warned = True
2506 2506 return tree
2507 2507
2508 2508 def foldconcat(tree):
2509 2509 """Fold elements to be concatenated by `##`
2510 2510 """
2511 2511 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
2512 2512 return tree
2513 2513 if tree[0] == '_concat':
2514 2514 pending = [tree]
2515 2515 l = []
2516 2516 while pending:
2517 2517 e = pending.pop()
2518 2518 if e[0] == '_concat':
2519 2519 pending.extend(reversed(e[1:]))
2520 2520 elif e[0] in ('string', 'symbol'):
2521 2521 l.append(e[1])
2522 2522 else:
2523 2523 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
2524 2524 raise error.ParseError(msg)
2525 2525 return ('string', ''.join(l))
2526 2526 else:
2527 2527 return tuple(foldconcat(t) for t in tree)
2528 2528
2529 2529 def parse(spec, lookup=None):
2530 2530 return _parsewith(spec, lookup=lookup)
2531 2531
2532 2532 def posttreebuilthook(tree, repo):
2533 2533 # hook for extensions to execute code on the optimized tree
2534 2534 pass
2535 2535
2536 2536 def match(ui, spec, repo=None):
2537 2537 if not spec:
2538 2538 raise error.ParseError(_("empty query"))
2539 2539 lookup = None
2540 2540 if repo:
2541 2541 lookup = repo.__contains__
2542 2542 tree = parse(spec, lookup)
2543 2543 return _makematcher(ui, tree, repo)
2544 2544
2545 2545 def matchany(ui, specs, repo=None):
2546 2546 """Create a matcher that will include any revisions matching one of the
2547 2547 given specs"""
2548 2548 if not specs:
2549 2549 def mfunc(repo, subset=None):
2550 2550 return baseset()
2551 2551 return mfunc
2552 2552 if not all(specs):
2553 2553 raise error.ParseError(_("empty query"))
2554 2554 lookup = None
2555 2555 if repo:
2556 2556 lookup = repo.__contains__
2557 2557 if len(specs) == 1:
2558 2558 tree = parse(specs[0], lookup)
2559 2559 else:
2560 2560 tree = ('or',) + tuple(parse(s, lookup) for s in specs)
2561 2561 return _makematcher(ui, tree, repo)
2562 2562
2563 2563 def _makematcher(ui, tree, repo):
2564 2564 if ui:
2565 2565 tree = expandaliases(ui, tree, showwarning=ui.warn)
2566 2566 tree = foldconcat(tree)
2567 2567 tree = optimize(tree)
2568 2568 posttreebuilthook(tree, repo)
2569 2569 def mfunc(repo, subset=None):
2570 2570 if subset is None:
2571 2571 subset = fullreposet(repo)
2572 2572 if util.safehasattr(subset, 'isascending'):
2573 2573 result = getset(repo, subset, tree)
2574 2574 else:
2575 2575 result = getset(repo, baseset(subset), tree)
2576 2576 return result
2577 2577 return mfunc
2578 2578
2579 2579 def formatspec(expr, *args):
2580 2580 '''
2581 2581 This is a convenience function for using revsets internally, and
2582 2582 escapes arguments appropriately. Aliases are intentionally ignored
2583 2583 so that intended expression behavior isn't accidentally subverted.
2584 2584
2585 2585 Supported arguments:
2586 2586
2587 2587 %r = revset expression, parenthesized
2588 2588 %d = int(arg), no quoting
2589 2589 %s = string(arg), escaped and single-quoted
2590 2590 %b = arg.branch(), escaped and single-quoted
2591 2591 %n = hex(arg), single-quoted
2592 2592 %% = a literal '%'
2593 2593
2594 2594 Prefixing the type with 'l' specifies a parenthesized list of that type.
2595 2595
2596 2596 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
2597 2597 '(10 or 11):: and ((this()) or (that()))'
2598 2598 >>> formatspec('%d:: and not %d::', 10, 20)
2599 2599 '10:: and not 20::'
2600 2600 >>> formatspec('%ld or %ld', [], [1])
2601 2601 "_list('') or 1"
2602 2602 >>> formatspec('keyword(%s)', 'foo\\xe9')
2603 2603 "keyword('foo\\\\xe9')"
2604 2604 >>> b = lambda: 'default'
2605 2605 >>> b.branch = b
2606 2606 >>> formatspec('branch(%b)', b)
2607 2607 "branch('default')"
2608 2608 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
2609 2609 "root(_list('a\\x00b\\x00c\\x00d'))"
2610 2610 '''
2611 2611
2612 2612 def quote(s):
2613 2613 return repr(str(s))
2614 2614
2615 2615 def argtype(c, arg):
2616 2616 if c == 'd':
2617 2617 return str(int(arg))
2618 2618 elif c == 's':
2619 2619 return quote(arg)
2620 2620 elif c == 'r':
2621 2621 parse(arg) # make sure syntax errors are confined
2622 2622 return '(%s)' % arg
2623 2623 elif c == 'n':
2624 2624 return quote(node.hex(arg))
2625 2625 elif c == 'b':
2626 2626 return quote(arg.branch())
2627 2627
2628 2628 def listexp(s, t):
2629 2629 l = len(s)
2630 2630 if l == 0:
2631 2631 return "_list('')"
2632 2632 elif l == 1:
2633 2633 return argtype(t, s[0])
2634 2634 elif t == 'd':
2635 2635 return "_intlist('%s')" % "\0".join(str(int(a)) for a in s)
2636 2636 elif t == 's':
2637 2637 return "_list('%s')" % "\0".join(s)
2638 2638 elif t == 'n':
2639 2639 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
2640 2640 elif t == 'b':
2641 2641 return "_list('%s')" % "\0".join(a.branch() for a in s)
2642 2642
2643 2643 m = l // 2
2644 2644 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
2645 2645
2646 2646 ret = ''
2647 2647 pos = 0
2648 2648 arg = 0
2649 2649 while pos < len(expr):
2650 2650 c = expr[pos]
2651 2651 if c == '%':
2652 2652 pos += 1
2653 2653 d = expr[pos]
2654 2654 if d == '%':
2655 2655 ret += d
2656 2656 elif d in 'dsnbr':
2657 2657 ret += argtype(d, args[arg])
2658 2658 arg += 1
2659 2659 elif d == 'l':
2660 2660 # a list of some type
2661 2661 pos += 1
2662 2662 d = expr[pos]
2663 2663 ret += listexp(list(args[arg]), d)
2664 2664 arg += 1
2665 2665 else:
2666 raise error.Abort('unexpected revspec format character %s' % d)
2666 raise error.Abort(_('unexpected revspec format character %s')
2667 % d)
2667 2668 else:
2668 2669 ret += c
2669 2670 pos += 1
2670 2671
2671 2672 return ret
2672 2673
2673 2674 def prettyformat(tree):
2674 2675 return parser.prettyformat(tree, ('string', 'symbol'))
2675 2676
2676 2677 def depth(tree):
2677 2678 if isinstance(tree, tuple):
2678 2679 return max(map(depth, tree)) + 1
2679 2680 else:
2680 2681 return 0
2681 2682
2682 2683 def funcsused(tree):
2683 2684 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
2684 2685 return set()
2685 2686 else:
2686 2687 funcs = set()
2687 2688 for s in tree[1:]:
2688 2689 funcs |= funcsused(s)
2689 2690 if tree[0] == 'func':
2690 2691 funcs.add(tree[1][1])
2691 2692 return funcs
2692 2693
2693 2694 def _formatsetrepr(r):
2694 2695 """Format an optional printable representation of a set
2695 2696
2696 2697 ======== =================================
2697 2698 type(r) example
2698 2699 ======== =================================
2699 2700 tuple ('<not %r>', other)
2700 2701 str '<branch closed>'
2701 2702 callable lambda: '<branch %r>' % sorted(b)
2702 2703 object other
2703 2704 ======== =================================
2704 2705 """
2705 2706 if r is None:
2706 2707 return ''
2707 2708 elif isinstance(r, tuple):
2708 2709 return r[0] % r[1:]
2709 2710 elif isinstance(r, str):
2710 2711 return r
2711 2712 elif callable(r):
2712 2713 return r()
2713 2714 else:
2714 2715 return repr(r)
2715 2716
2716 2717 class abstractsmartset(object):
2717 2718
2718 2719 def __nonzero__(self):
2719 2720 """True if the smartset is not empty"""
2720 2721 raise NotImplementedError()
2721 2722
2722 2723 def __contains__(self, rev):
2723 2724 """provide fast membership testing"""
2724 2725 raise NotImplementedError()
2725 2726
2726 2727 def __iter__(self):
2727 2728 """iterate the set in the order it is supposed to be iterated"""
2728 2729 raise NotImplementedError()
2729 2730
2730 2731 # Attributes containing a function to perform a fast iteration in a given
2731 2732 # direction. A smartset can have none, one, or both defined.
2732 2733 #
2733 2734 # Default value is None instead of a function returning None to avoid
2734 2735 # initializing an iterator just for testing if a fast method exists.
2735 2736 fastasc = None
2736 2737 fastdesc = None
2737 2738
2738 2739 def isascending(self):
2739 2740 """True if the set will iterate in ascending order"""
2740 2741 raise NotImplementedError()
2741 2742
2742 2743 def isdescending(self):
2743 2744 """True if the set will iterate in descending order"""
2744 2745 raise NotImplementedError()
2745 2746
2746 2747 def istopo(self):
2747 2748 """True if the set will iterate in topographical order"""
2748 2749 raise NotImplementedError()
2749 2750
2750 2751 @util.cachefunc
2751 2752 def min(self):
2752 2753 """return the minimum element in the set"""
2753 2754 if self.fastasc is not None:
2754 2755 for r in self.fastasc():
2755 2756 return r
2756 2757 raise ValueError('arg is an empty sequence')
2757 2758 return min(self)
2758 2759
2759 2760 @util.cachefunc
2760 2761 def max(self):
2761 2762 """return the maximum element in the set"""
2762 2763 if self.fastdesc is not None:
2763 2764 for r in self.fastdesc():
2764 2765 return r
2765 2766 raise ValueError('arg is an empty sequence')
2766 2767 return max(self)
2767 2768
2768 2769 def first(self):
2769 2770 """return the first element in the set (user iteration perspective)
2770 2771
2771 2772 Return None if the set is empty"""
2772 2773 raise NotImplementedError()
2773 2774
2774 2775 def last(self):
2775 2776 """return the last element in the set (user iteration perspective)
2776 2777
2777 2778 Return None if the set is empty"""
2778 2779 raise NotImplementedError()
2779 2780
2780 2781 def __len__(self):
2781 2782 """return the length of the smartsets
2782 2783
2783 2784 This can be expensive on smartset that could be lazy otherwise."""
2784 2785 raise NotImplementedError()
2785 2786
2786 2787 def reverse(self):
2787 2788 """reverse the expected iteration order"""
2788 2789 raise NotImplementedError()
2789 2790
2790 2791 def sort(self, reverse=True):
2791 2792 """get the set to iterate in an ascending or descending order"""
2792 2793 raise NotImplementedError()
2793 2794
2794 2795 def __and__(self, other):
2795 2796 """Returns a new object with the intersection of the two collections.
2796 2797
2797 2798 This is part of the mandatory API for smartset."""
2798 2799 if isinstance(other, fullreposet):
2799 2800 return self
2800 2801 return self.filter(other.__contains__, condrepr=other, cache=False)
2801 2802
2802 2803 def __add__(self, other):
2803 2804 """Returns a new object with the union of the two collections.
2804 2805
2805 2806 This is part of the mandatory API for smartset."""
2806 2807 return addset(self, other)
2807 2808
2808 2809 def __sub__(self, other):
2809 2810 """Returns a new object with the substraction of the two collections.
2810 2811
2811 2812 This is part of the mandatory API for smartset."""
2812 2813 c = other.__contains__
2813 2814 return self.filter(lambda r: not c(r), condrepr=('<not %r>', other),
2814 2815 cache=False)
2815 2816
2816 2817 def filter(self, condition, condrepr=None, cache=True):
2817 2818 """Returns this smartset filtered by condition as a new smartset.
2818 2819
2819 2820 `condition` is a callable which takes a revision number and returns a
2820 2821 boolean. Optional `condrepr` provides a printable representation of
2821 2822 the given `condition`.
2822 2823
2823 2824 This is part of the mandatory API for smartset."""
2824 2825 # builtin cannot be cached. but do not needs to
2825 2826 if cache and util.safehasattr(condition, 'func_code'):
2826 2827 condition = util.cachefunc(condition)
2827 2828 return filteredset(self, condition, condrepr)
2828 2829
2829 2830 class baseset(abstractsmartset):
2830 2831 """Basic data structure that represents a revset and contains the basic
2831 2832 operation that it should be able to perform.
2832 2833
2833 2834 Every method in this class should be implemented by any smartset class.
2834 2835 """
2835 2836 def __init__(self, data=(), datarepr=None, istopo=False):
2836 2837 """
2837 2838 datarepr: a tuple of (format, obj, ...), a function or an object that
2838 2839 provides a printable representation of the given data.
2839 2840 """
2840 2841 self._ascending = None
2841 2842 self._istopo = istopo
2842 2843 if not isinstance(data, list):
2843 2844 if isinstance(data, set):
2844 2845 self._set = data
2845 2846 # set has no order we pick one for stability purpose
2846 2847 self._ascending = True
2847 2848 data = list(data)
2848 2849 self._list = data
2849 2850 self._datarepr = datarepr
2850 2851
2851 2852 @util.propertycache
2852 2853 def _set(self):
2853 2854 return set(self._list)
2854 2855
2855 2856 @util.propertycache
2856 2857 def _asclist(self):
2857 2858 asclist = self._list[:]
2858 2859 asclist.sort()
2859 2860 return asclist
2860 2861
2861 2862 def __iter__(self):
2862 2863 if self._ascending is None:
2863 2864 return iter(self._list)
2864 2865 elif self._ascending:
2865 2866 return iter(self._asclist)
2866 2867 else:
2867 2868 return reversed(self._asclist)
2868 2869
2869 2870 def fastasc(self):
2870 2871 return iter(self._asclist)
2871 2872
2872 2873 def fastdesc(self):
2873 2874 return reversed(self._asclist)
2874 2875
2875 2876 @util.propertycache
2876 2877 def __contains__(self):
2877 2878 return self._set.__contains__
2878 2879
2879 2880 def __nonzero__(self):
2880 2881 return bool(self._list)
2881 2882
2882 2883 def sort(self, reverse=False):
2883 2884 self._ascending = not bool(reverse)
2884 2885 self._istopo = False
2885 2886
2886 2887 def reverse(self):
2887 2888 if self._ascending is None:
2888 2889 self._list.reverse()
2889 2890 else:
2890 2891 self._ascending = not self._ascending
2891 2892 self._istopo = False
2892 2893
2893 2894 def __len__(self):
2894 2895 return len(self._list)
2895 2896
2896 2897 def isascending(self):
2897 2898 """Returns True if the collection is ascending order, False if not.
2898 2899
2899 2900 This is part of the mandatory API for smartset."""
2900 2901 if len(self) <= 1:
2901 2902 return True
2902 2903 return self._ascending is not None and self._ascending
2903 2904
2904 2905 def isdescending(self):
2905 2906 """Returns True if the collection is descending order, False if not.
2906 2907
2907 2908 This is part of the mandatory API for smartset."""
2908 2909 if len(self) <= 1:
2909 2910 return True
2910 2911 return self._ascending is not None and not self._ascending
2911 2912
2912 2913 def istopo(self):
2913 2914 """Is the collection is in topographical order or not.
2914 2915
2915 2916 This is part of the mandatory API for smartset."""
2916 2917 if len(self) <= 1:
2917 2918 return True
2918 2919 return self._istopo
2919 2920
2920 2921 def first(self):
2921 2922 if self:
2922 2923 if self._ascending is None:
2923 2924 return self._list[0]
2924 2925 elif self._ascending:
2925 2926 return self._asclist[0]
2926 2927 else:
2927 2928 return self._asclist[-1]
2928 2929 return None
2929 2930
2930 2931 def last(self):
2931 2932 if self:
2932 2933 if self._ascending is None:
2933 2934 return self._list[-1]
2934 2935 elif self._ascending:
2935 2936 return self._asclist[-1]
2936 2937 else:
2937 2938 return self._asclist[0]
2938 2939 return None
2939 2940
2940 2941 def __repr__(self):
2941 2942 d = {None: '', False: '-', True: '+'}[self._ascending]
2942 2943 s = _formatsetrepr(self._datarepr)
2943 2944 if not s:
2944 2945 l = self._list
2945 2946 # if _list has been built from a set, it might have a different
2946 2947 # order from one python implementation to another.
2947 2948 # We fallback to the sorted version for a stable output.
2948 2949 if self._ascending is not None:
2949 2950 l = self._asclist
2950 2951 s = repr(l)
2951 2952 return '<%s%s %s>' % (type(self).__name__, d, s)
2952 2953
2953 2954 class filteredset(abstractsmartset):
2954 2955 """Duck type for baseset class which iterates lazily over the revisions in
2955 2956 the subset and contains a function which tests for membership in the
2956 2957 revset
2957 2958 """
2958 2959 def __init__(self, subset, condition=lambda x: True, condrepr=None):
2959 2960 """
2960 2961 condition: a function that decide whether a revision in the subset
2961 2962 belongs to the revset or not.
2962 2963 condrepr: a tuple of (format, obj, ...), a function or an object that
2963 2964 provides a printable representation of the given condition.
2964 2965 """
2965 2966 self._subset = subset
2966 2967 self._condition = condition
2967 2968 self._condrepr = condrepr
2968 2969
2969 2970 def __contains__(self, x):
2970 2971 return x in self._subset and self._condition(x)
2971 2972
2972 2973 def __iter__(self):
2973 2974 return self._iterfilter(self._subset)
2974 2975
2975 2976 def _iterfilter(self, it):
2976 2977 cond = self._condition
2977 2978 for x in it:
2978 2979 if cond(x):
2979 2980 yield x
2980 2981
2981 2982 @property
2982 2983 def fastasc(self):
2983 2984 it = self._subset.fastasc
2984 2985 if it is None:
2985 2986 return None
2986 2987 return lambda: self._iterfilter(it())
2987 2988
2988 2989 @property
2989 2990 def fastdesc(self):
2990 2991 it = self._subset.fastdesc
2991 2992 if it is None:
2992 2993 return None
2993 2994 return lambda: self._iterfilter(it())
2994 2995
2995 2996 def __nonzero__(self):
2996 2997 fast = None
2997 2998 candidates = [self.fastasc if self.isascending() else None,
2998 2999 self.fastdesc if self.isdescending() else None,
2999 3000 self.fastasc,
3000 3001 self.fastdesc]
3001 3002 for candidate in candidates:
3002 3003 if candidate is not None:
3003 3004 fast = candidate
3004 3005 break
3005 3006
3006 3007 if fast is not None:
3007 3008 it = fast()
3008 3009 else:
3009 3010 it = self
3010 3011
3011 3012 for r in it:
3012 3013 return True
3013 3014 return False
3014 3015
3015 3016 def __len__(self):
3016 3017 # Basic implementation to be changed in future patches.
3017 3018 # until this gets improved, we use generator expression
3018 3019 # here, since list compr is free to call __len__ again
3019 3020 # causing infinite recursion
3020 3021 l = baseset(r for r in self)
3021 3022 return len(l)
3022 3023
3023 3024 def sort(self, reverse=False):
3024 3025 self._subset.sort(reverse=reverse)
3025 3026
3026 3027 def reverse(self):
3027 3028 self._subset.reverse()
3028 3029
3029 3030 def isascending(self):
3030 3031 return self._subset.isascending()
3031 3032
3032 3033 def isdescending(self):
3033 3034 return self._subset.isdescending()
3034 3035
3035 3036 def istopo(self):
3036 3037 return self._subset.istopo()
3037 3038
3038 3039 def first(self):
3039 3040 for x in self:
3040 3041 return x
3041 3042 return None
3042 3043
3043 3044 def last(self):
3044 3045 it = None
3045 3046 if self.isascending():
3046 3047 it = self.fastdesc
3047 3048 elif self.isdescending():
3048 3049 it = self.fastasc
3049 3050 if it is not None:
3050 3051 for x in it():
3051 3052 return x
3052 3053 return None #empty case
3053 3054 else:
3054 3055 x = None
3055 3056 for x in self:
3056 3057 pass
3057 3058 return x
3058 3059
3059 3060 def __repr__(self):
3060 3061 xs = [repr(self._subset)]
3061 3062 s = _formatsetrepr(self._condrepr)
3062 3063 if s:
3063 3064 xs.append(s)
3064 3065 return '<%s %s>' % (type(self).__name__, ', '.join(xs))
3065 3066
3066 3067 def _iterordered(ascending, iter1, iter2):
3067 3068 """produce an ordered iteration from two iterators with the same order
3068 3069
3069 3070 The ascending is used to indicated the iteration direction.
3070 3071 """
3071 3072 choice = max
3072 3073 if ascending:
3073 3074 choice = min
3074 3075
3075 3076 val1 = None
3076 3077 val2 = None
3077 3078 try:
3078 3079 # Consume both iterators in an ordered way until one is empty
3079 3080 while True:
3080 3081 if val1 is None:
3081 3082 val1 = next(iter1)
3082 3083 if val2 is None:
3083 3084 val2 = next(iter2)
3084 3085 n = choice(val1, val2)
3085 3086 yield n
3086 3087 if val1 == n:
3087 3088 val1 = None
3088 3089 if val2 == n:
3089 3090 val2 = None
3090 3091 except StopIteration:
3091 3092 # Flush any remaining values and consume the other one
3092 3093 it = iter2
3093 3094 if val1 is not None:
3094 3095 yield val1
3095 3096 it = iter1
3096 3097 elif val2 is not None:
3097 3098 # might have been equality and both are empty
3098 3099 yield val2
3099 3100 for val in it:
3100 3101 yield val
3101 3102
3102 3103 class addset(abstractsmartset):
3103 3104 """Represent the addition of two sets
3104 3105
3105 3106 Wrapper structure for lazily adding two structures without losing much
3106 3107 performance on the __contains__ method
3107 3108
3108 3109 If the ascending attribute is set, that means the two structures are
3109 3110 ordered in either an ascending or descending way. Therefore, we can add
3110 3111 them maintaining the order by iterating over both at the same time
3111 3112
3112 3113 >>> xs = baseset([0, 3, 2])
3113 3114 >>> ys = baseset([5, 2, 4])
3114 3115
3115 3116 >>> rs = addset(xs, ys)
3116 3117 >>> bool(rs), 0 in rs, 1 in rs, 5 in rs, rs.first(), rs.last()
3117 3118 (True, True, False, True, 0, 4)
3118 3119 >>> rs = addset(xs, baseset([]))
3119 3120 >>> bool(rs), 0 in rs, 1 in rs, rs.first(), rs.last()
3120 3121 (True, True, False, 0, 2)
3121 3122 >>> rs = addset(baseset([]), baseset([]))
3122 3123 >>> bool(rs), 0 in rs, rs.first(), rs.last()
3123 3124 (False, False, None, None)
3124 3125
3125 3126 iterate unsorted:
3126 3127 >>> rs = addset(xs, ys)
3127 3128 >>> # (use generator because pypy could call len())
3128 3129 >>> list(x for x in rs) # without _genlist
3129 3130 [0, 3, 2, 5, 4]
3130 3131 >>> assert not rs._genlist
3131 3132 >>> len(rs)
3132 3133 5
3133 3134 >>> [x for x in rs] # with _genlist
3134 3135 [0, 3, 2, 5, 4]
3135 3136 >>> assert rs._genlist
3136 3137
3137 3138 iterate ascending:
3138 3139 >>> rs = addset(xs, ys, ascending=True)
3139 3140 >>> # (use generator because pypy could call len())
3140 3141 >>> list(x for x in rs), list(x for x in rs.fastasc()) # without _asclist
3141 3142 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
3142 3143 >>> assert not rs._asclist
3143 3144 >>> len(rs)
3144 3145 5
3145 3146 >>> [x for x in rs], [x for x in rs.fastasc()]
3146 3147 ([0, 2, 3, 4, 5], [0, 2, 3, 4, 5])
3147 3148 >>> assert rs._asclist
3148 3149
3149 3150 iterate descending:
3150 3151 >>> rs = addset(xs, ys, ascending=False)
3151 3152 >>> # (use generator because pypy could call len())
3152 3153 >>> list(x for x in rs), list(x for x in rs.fastdesc()) # without _asclist
3153 3154 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
3154 3155 >>> assert not rs._asclist
3155 3156 >>> len(rs)
3156 3157 5
3157 3158 >>> [x for x in rs], [x for x in rs.fastdesc()]
3158 3159 ([5, 4, 3, 2, 0], [5, 4, 3, 2, 0])
3159 3160 >>> assert rs._asclist
3160 3161
3161 3162 iterate ascending without fastasc:
3162 3163 >>> rs = addset(xs, generatorset(ys), ascending=True)
3163 3164 >>> assert rs.fastasc is None
3164 3165 >>> [x for x in rs]
3165 3166 [0, 2, 3, 4, 5]
3166 3167
3167 3168 iterate descending without fastdesc:
3168 3169 >>> rs = addset(generatorset(xs), ys, ascending=False)
3169 3170 >>> assert rs.fastdesc is None
3170 3171 >>> [x for x in rs]
3171 3172 [5, 4, 3, 2, 0]
3172 3173 """
3173 3174 def __init__(self, revs1, revs2, ascending=None):
3174 3175 self._r1 = revs1
3175 3176 self._r2 = revs2
3176 3177 self._iter = None
3177 3178 self._ascending = ascending
3178 3179 self._genlist = None
3179 3180 self._asclist = None
3180 3181
3181 3182 def __len__(self):
3182 3183 return len(self._list)
3183 3184
3184 3185 def __nonzero__(self):
3185 3186 return bool(self._r1) or bool(self._r2)
3186 3187
3187 3188 @util.propertycache
3188 3189 def _list(self):
3189 3190 if not self._genlist:
3190 3191 self._genlist = baseset(iter(self))
3191 3192 return self._genlist
3192 3193
3193 3194 def __iter__(self):
3194 3195 """Iterate over both collections without repeating elements
3195 3196
3196 3197 If the ascending attribute is not set, iterate over the first one and
3197 3198 then over the second one checking for membership on the first one so we
3198 3199 dont yield any duplicates.
3199 3200
3200 3201 If the ascending attribute is set, iterate over both collections at the
3201 3202 same time, yielding only one value at a time in the given order.
3202 3203 """
3203 3204 if self._ascending is None:
3204 3205 if self._genlist:
3205 3206 return iter(self._genlist)
3206 3207 def arbitraryordergen():
3207 3208 for r in self._r1:
3208 3209 yield r
3209 3210 inr1 = self._r1.__contains__
3210 3211 for r in self._r2:
3211 3212 if not inr1(r):
3212 3213 yield r
3213 3214 return arbitraryordergen()
3214 3215 # try to use our own fast iterator if it exists
3215 3216 self._trysetasclist()
3216 3217 if self._ascending:
3217 3218 attr = 'fastasc'
3218 3219 else:
3219 3220 attr = 'fastdesc'
3220 3221 it = getattr(self, attr)
3221 3222 if it is not None:
3222 3223 return it()
3223 3224 # maybe half of the component supports fast
3224 3225 # get iterator for _r1
3225 3226 iter1 = getattr(self._r1, attr)
3226 3227 if iter1 is None:
3227 3228 # let's avoid side effect (not sure it matters)
3228 3229 iter1 = iter(sorted(self._r1, reverse=not self._ascending))
3229 3230 else:
3230 3231 iter1 = iter1()
3231 3232 # get iterator for _r2
3232 3233 iter2 = getattr(self._r2, attr)
3233 3234 if iter2 is None:
3234 3235 # let's avoid side effect (not sure it matters)
3235 3236 iter2 = iter(sorted(self._r2, reverse=not self._ascending))
3236 3237 else:
3237 3238 iter2 = iter2()
3238 3239 return _iterordered(self._ascending, iter1, iter2)
3239 3240
3240 3241 def _trysetasclist(self):
3241 3242 """populate the _asclist attribute if possible and necessary"""
3242 3243 if self._genlist is not None and self._asclist is None:
3243 3244 self._asclist = sorted(self._genlist)
3244 3245
3245 3246 @property
3246 3247 def fastasc(self):
3247 3248 self._trysetasclist()
3248 3249 if self._asclist is not None:
3249 3250 return self._asclist.__iter__
3250 3251 iter1 = self._r1.fastasc
3251 3252 iter2 = self._r2.fastasc
3252 3253 if None in (iter1, iter2):
3253 3254 return None
3254 3255 return lambda: _iterordered(True, iter1(), iter2())
3255 3256
3256 3257 @property
3257 3258 def fastdesc(self):
3258 3259 self._trysetasclist()
3259 3260 if self._asclist is not None:
3260 3261 return self._asclist.__reversed__
3261 3262 iter1 = self._r1.fastdesc
3262 3263 iter2 = self._r2.fastdesc
3263 3264 if None in (iter1, iter2):
3264 3265 return None
3265 3266 return lambda: _iterordered(False, iter1(), iter2())
3266 3267
3267 3268 def __contains__(self, x):
3268 3269 return x in self._r1 or x in self._r2
3269 3270
3270 3271 def sort(self, reverse=False):
3271 3272 """Sort the added set
3272 3273
3273 3274 For this we use the cached list with all the generated values and if we
3274 3275 know they are ascending or descending we can sort them in a smart way.
3275 3276 """
3276 3277 self._ascending = not reverse
3277 3278
3278 3279 def isascending(self):
3279 3280 return self._ascending is not None and self._ascending
3280 3281
3281 3282 def isdescending(self):
3282 3283 return self._ascending is not None and not self._ascending
3283 3284
3284 3285 def istopo(self):
3285 3286 # not worth the trouble asserting if the two sets combined are still
3286 3287 # in topographical order. Use the sort() predicate to explicitly sort
3287 3288 # again instead.
3288 3289 return False
3289 3290
3290 3291 def reverse(self):
3291 3292 if self._ascending is None:
3292 3293 self._list.reverse()
3293 3294 else:
3294 3295 self._ascending = not self._ascending
3295 3296
3296 3297 def first(self):
3297 3298 for x in self:
3298 3299 return x
3299 3300 return None
3300 3301
3301 3302 def last(self):
3302 3303 self.reverse()
3303 3304 val = self.first()
3304 3305 self.reverse()
3305 3306 return val
3306 3307
3307 3308 def __repr__(self):
3308 3309 d = {None: '', False: '-', True: '+'}[self._ascending]
3309 3310 return '<%s%s %r, %r>' % (type(self).__name__, d, self._r1, self._r2)
3310 3311
3311 3312 class generatorset(abstractsmartset):
3312 3313 """Wrap a generator for lazy iteration
3313 3314
3314 3315 Wrapper structure for generators that provides lazy membership and can
3315 3316 be iterated more than once.
3316 3317 When asked for membership it generates values until either it finds the
3317 3318 requested one or has gone through all the elements in the generator
3318 3319 """
3319 3320 def __init__(self, gen, iterasc=None):
3320 3321 """
3321 3322 gen: a generator producing the values for the generatorset.
3322 3323 """
3323 3324 self._gen = gen
3324 3325 self._asclist = None
3325 3326 self._cache = {}
3326 3327 self._genlist = []
3327 3328 self._finished = False
3328 3329 self._ascending = True
3329 3330 if iterasc is not None:
3330 3331 if iterasc:
3331 3332 self.fastasc = self._iterator
3332 3333 self.__contains__ = self._asccontains
3333 3334 else:
3334 3335 self.fastdesc = self._iterator
3335 3336 self.__contains__ = self._desccontains
3336 3337
3337 3338 def __nonzero__(self):
3338 3339 # Do not use 'for r in self' because it will enforce the iteration
3339 3340 # order (default ascending), possibly unrolling a whole descending
3340 3341 # iterator.
3341 3342 if self._genlist:
3342 3343 return True
3343 3344 for r in self._consumegen():
3344 3345 return True
3345 3346 return False
3346 3347
3347 3348 def __contains__(self, x):
3348 3349 if x in self._cache:
3349 3350 return self._cache[x]
3350 3351
3351 3352 # Use new values only, as existing values would be cached.
3352 3353 for l in self._consumegen():
3353 3354 if l == x:
3354 3355 return True
3355 3356
3356 3357 self._cache[x] = False
3357 3358 return False
3358 3359
3359 3360 def _asccontains(self, x):
3360 3361 """version of contains optimised for ascending generator"""
3361 3362 if x in self._cache:
3362 3363 return self._cache[x]
3363 3364
3364 3365 # Use new values only, as existing values would be cached.
3365 3366 for l in self._consumegen():
3366 3367 if l == x:
3367 3368 return True
3368 3369 if l > x:
3369 3370 break
3370 3371
3371 3372 self._cache[x] = False
3372 3373 return False
3373 3374
3374 3375 def _desccontains(self, x):
3375 3376 """version of contains optimised for descending generator"""
3376 3377 if x in self._cache:
3377 3378 return self._cache[x]
3378 3379
3379 3380 # Use new values only, as existing values would be cached.
3380 3381 for l in self._consumegen():
3381 3382 if l == x:
3382 3383 return True
3383 3384 if l < x:
3384 3385 break
3385 3386
3386 3387 self._cache[x] = False
3387 3388 return False
3388 3389
3389 3390 def __iter__(self):
3390 3391 if self._ascending:
3391 3392 it = self.fastasc
3392 3393 else:
3393 3394 it = self.fastdesc
3394 3395 if it is not None:
3395 3396 return it()
3396 3397 # we need to consume the iterator
3397 3398 for x in self._consumegen():
3398 3399 pass
3399 3400 # recall the same code
3400 3401 return iter(self)
3401 3402
3402 3403 def _iterator(self):
3403 3404 if self._finished:
3404 3405 return iter(self._genlist)
3405 3406
3406 3407 # We have to use this complex iteration strategy to allow multiple
3407 3408 # iterations at the same time. We need to be able to catch revision
3408 3409 # removed from _consumegen and added to genlist in another instance.
3409 3410 #
3410 3411 # Getting rid of it would provide an about 15% speed up on this
3411 3412 # iteration.
3412 3413 genlist = self._genlist
3413 3414 nextrev = self._consumegen().next
3414 3415 _len = len # cache global lookup
3415 3416 def gen():
3416 3417 i = 0
3417 3418 while True:
3418 3419 if i < _len(genlist):
3419 3420 yield genlist[i]
3420 3421 else:
3421 3422 yield nextrev()
3422 3423 i += 1
3423 3424 return gen()
3424 3425
3425 3426 def _consumegen(self):
3426 3427 cache = self._cache
3427 3428 genlist = self._genlist.append
3428 3429 for item in self._gen:
3429 3430 cache[item] = True
3430 3431 genlist(item)
3431 3432 yield item
3432 3433 if not self._finished:
3433 3434 self._finished = True
3434 3435 asc = self._genlist[:]
3435 3436 asc.sort()
3436 3437 self._asclist = asc
3437 3438 self.fastasc = asc.__iter__
3438 3439 self.fastdesc = asc.__reversed__
3439 3440
3440 3441 def __len__(self):
3441 3442 for x in self._consumegen():
3442 3443 pass
3443 3444 return len(self._genlist)
3444 3445
3445 3446 def sort(self, reverse=False):
3446 3447 self._ascending = not reverse
3447 3448
3448 3449 def reverse(self):
3449 3450 self._ascending = not self._ascending
3450 3451
3451 3452 def isascending(self):
3452 3453 return self._ascending
3453 3454
3454 3455 def isdescending(self):
3455 3456 return not self._ascending
3456 3457
3457 3458 def istopo(self):
3458 3459 # not worth the trouble asserting if the two sets combined are still
3459 3460 # in topographical order. Use the sort() predicate to explicitly sort
3460 3461 # again instead.
3461 3462 return False
3462 3463
3463 3464 def first(self):
3464 3465 if self._ascending:
3465 3466 it = self.fastasc
3466 3467 else:
3467 3468 it = self.fastdesc
3468 3469 if it is None:
3469 3470 # we need to consume all and try again
3470 3471 for x in self._consumegen():
3471 3472 pass
3472 3473 return self.first()
3473 3474 return next(it(), None)
3474 3475
3475 3476 def last(self):
3476 3477 if self._ascending:
3477 3478 it = self.fastdesc
3478 3479 else:
3479 3480 it = self.fastasc
3480 3481 if it is None:
3481 3482 # we need to consume all and try again
3482 3483 for x in self._consumegen():
3483 3484 pass
3484 3485 return self.first()
3485 3486 return next(it(), None)
3486 3487
3487 3488 def __repr__(self):
3488 3489 d = {False: '-', True: '+'}[self._ascending]
3489 3490 return '<%s%s>' % (type(self).__name__, d)
3490 3491
3491 3492 class spanset(abstractsmartset):
3492 3493 """Duck type for baseset class which represents a range of revisions and
3493 3494 can work lazily and without having all the range in memory
3494 3495
3495 3496 Note that spanset(x, y) behave almost like xrange(x, y) except for two
3496 3497 notable points:
3497 3498 - when x < y it will be automatically descending,
3498 3499 - revision filtered with this repoview will be skipped.
3499 3500
3500 3501 """
3501 3502 def __init__(self, repo, start=0, end=None):
3502 3503 """
3503 3504 start: first revision included the set
3504 3505 (default to 0)
3505 3506 end: first revision excluded (last+1)
3506 3507 (default to len(repo)
3507 3508
3508 3509 Spanset will be descending if `end` < `start`.
3509 3510 """
3510 3511 if end is None:
3511 3512 end = len(repo)
3512 3513 self._ascending = start <= end
3513 3514 if not self._ascending:
3514 3515 start, end = end + 1, start +1
3515 3516 self._start = start
3516 3517 self._end = end
3517 3518 self._hiddenrevs = repo.changelog.filteredrevs
3518 3519
3519 3520 def sort(self, reverse=False):
3520 3521 self._ascending = not reverse
3521 3522
3522 3523 def reverse(self):
3523 3524 self._ascending = not self._ascending
3524 3525
3525 3526 def istopo(self):
3526 3527 # not worth the trouble asserting if the two sets combined are still
3527 3528 # in topographical order. Use the sort() predicate to explicitly sort
3528 3529 # again instead.
3529 3530 return False
3530 3531
3531 3532 def _iterfilter(self, iterrange):
3532 3533 s = self._hiddenrevs
3533 3534 for r in iterrange:
3534 3535 if r not in s:
3535 3536 yield r
3536 3537
3537 3538 def __iter__(self):
3538 3539 if self._ascending:
3539 3540 return self.fastasc()
3540 3541 else:
3541 3542 return self.fastdesc()
3542 3543
3543 3544 def fastasc(self):
3544 3545 iterrange = xrange(self._start, self._end)
3545 3546 if self._hiddenrevs:
3546 3547 return self._iterfilter(iterrange)
3547 3548 return iter(iterrange)
3548 3549
3549 3550 def fastdesc(self):
3550 3551 iterrange = xrange(self._end - 1, self._start - 1, -1)
3551 3552 if self._hiddenrevs:
3552 3553 return self._iterfilter(iterrange)
3553 3554 return iter(iterrange)
3554 3555
3555 3556 def __contains__(self, rev):
3556 3557 hidden = self._hiddenrevs
3557 3558 return ((self._start <= rev < self._end)
3558 3559 and not (hidden and rev in hidden))
3559 3560
3560 3561 def __nonzero__(self):
3561 3562 for r in self:
3562 3563 return True
3563 3564 return False
3564 3565
3565 3566 def __len__(self):
3566 3567 if not self._hiddenrevs:
3567 3568 return abs(self._end - self._start)
3568 3569 else:
3569 3570 count = 0
3570 3571 start = self._start
3571 3572 end = self._end
3572 3573 for rev in self._hiddenrevs:
3573 3574 if (end < rev <= start) or (start <= rev < end):
3574 3575 count += 1
3575 3576 return abs(self._end - self._start) - count
3576 3577
3577 3578 def isascending(self):
3578 3579 return self._ascending
3579 3580
3580 3581 def isdescending(self):
3581 3582 return not self._ascending
3582 3583
3583 3584 def first(self):
3584 3585 if self._ascending:
3585 3586 it = self.fastasc
3586 3587 else:
3587 3588 it = self.fastdesc
3588 3589 for x in it():
3589 3590 return x
3590 3591 return None
3591 3592
3592 3593 def last(self):
3593 3594 if self._ascending:
3594 3595 it = self.fastdesc
3595 3596 else:
3596 3597 it = self.fastasc
3597 3598 for x in it():
3598 3599 return x
3599 3600 return None
3600 3601
3601 3602 def __repr__(self):
3602 3603 d = {False: '-', True: '+'}[self._ascending]
3603 3604 return '<%s%s %d:%d>' % (type(self).__name__, d,
3604 3605 self._start, self._end - 1)
3605 3606
3606 3607 class fullreposet(spanset):
3607 3608 """a set containing all revisions in the repo
3608 3609
3609 3610 This class exists to host special optimization and magic to handle virtual
3610 3611 revisions such as "null".
3611 3612 """
3612 3613
3613 3614 def __init__(self, repo):
3614 3615 super(fullreposet, self).__init__(repo)
3615 3616
3616 3617 def __and__(self, other):
3617 3618 """As self contains the whole repo, all of the other set should also be
3618 3619 in self. Therefore `self & other = other`.
3619 3620
3620 3621 This boldly assumes the other contains valid revs only.
3621 3622 """
3622 3623 # other not a smartset, make is so
3623 3624 if not util.safehasattr(other, 'isascending'):
3624 3625 # filter out hidden revision
3625 3626 # (this boldly assumes all smartset are pure)
3626 3627 #
3627 3628 # `other` was used with "&", let's assume this is a set like
3628 3629 # object.
3629 3630 other = baseset(other - self._hiddenrevs)
3630 3631
3631 3632 # XXX As fullreposet is also used as bootstrap, this is wrong.
3632 3633 #
3633 3634 # With a giveme312() revset returning [3,1,2], this makes
3634 3635 # 'hg log -r "giveme312()"' -> 1, 2, 3 (wrong)
3635 3636 # We cannot just drop it because other usage still need to sort it:
3636 3637 # 'hg log -r "all() and giveme312()"' -> 1, 2, 3 (right)
3637 3638 #
3638 3639 # There is also some faulty revset implementations that rely on it
3639 3640 # (eg: children as of its state in e8075329c5fb)
3640 3641 #
3641 3642 # When we fix the two points above we can move this into the if clause
3642 3643 other.sort(reverse=self.isdescending())
3643 3644 return other
3644 3645
3645 3646 def prettyformatset(revs):
3646 3647 lines = []
3647 3648 rs = repr(revs)
3648 3649 p = 0
3649 3650 while p < len(rs):
3650 3651 q = rs.find('<', p + 1)
3651 3652 if q < 0:
3652 3653 q = len(rs)
3653 3654 l = rs.count('<', 0, p) - rs.count('>', 0, p)
3654 3655 assert l >= 0
3655 3656 lines.append((l, rs[p:q].rstrip()))
3656 3657 p = q
3657 3658 return '\n'.join(' ' * l + s for l, s in lines)
3658 3659
3659 3660 def loadpredicate(ui, extname, registrarobj):
3660 3661 """Load revset predicates from specified registrarobj
3661 3662 """
3662 3663 for name, func in registrarobj._table.iteritems():
3663 3664 symbols[name] = func
3664 3665 if func._safe:
3665 3666 safesymbols.add(name)
3666 3667
3667 3668 # load built-in predicates explicitly to setup safesymbols
3668 3669 loadpredicate(None, None, predicate)
3669 3670
3670 3671 # tell hggettext to extract docstrings from these functions:
3671 3672 i18nfunctions = symbols.values()
@@ -1,1402 +1,1404 b''
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright 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 contextlib
11 11 import errno
12 12 import glob
13 13 import hashlib
14 14 import os
15 15 import re
16 16 import shutil
17 17 import stat
18 18 import tempfile
19 19 import threading
20 20
21 21 from .i18n import _
22 22 from .node import wdirrev
23 23 from . import (
24 24 encoding,
25 25 error,
26 26 match as matchmod,
27 27 osutil,
28 28 pathutil,
29 29 phases,
30 30 revset,
31 31 similar,
32 32 util,
33 33 )
34 34
35 35 if os.name == 'nt':
36 36 from . import scmwindows as scmplatform
37 37 else:
38 38 from . import scmposix as scmplatform
39 39
40 40 systemrcpath = scmplatform.systemrcpath
41 41 userrcpath = scmplatform.userrcpath
42 42
43 43 class status(tuple):
44 44 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
45 45 and 'ignored' properties are only relevant to the working copy.
46 46 '''
47 47
48 48 __slots__ = ()
49 49
50 50 def __new__(cls, modified, added, removed, deleted, unknown, ignored,
51 51 clean):
52 52 return tuple.__new__(cls, (modified, added, removed, deleted, unknown,
53 53 ignored, clean))
54 54
55 55 @property
56 56 def modified(self):
57 57 '''files that have been modified'''
58 58 return self[0]
59 59
60 60 @property
61 61 def added(self):
62 62 '''files that have been added'''
63 63 return self[1]
64 64
65 65 @property
66 66 def removed(self):
67 67 '''files that have been removed'''
68 68 return self[2]
69 69
70 70 @property
71 71 def deleted(self):
72 72 '''files that are in the dirstate, but have been deleted from the
73 73 working copy (aka "missing")
74 74 '''
75 75 return self[3]
76 76
77 77 @property
78 78 def unknown(self):
79 79 '''files not in the dirstate that are not ignored'''
80 80 return self[4]
81 81
82 82 @property
83 83 def ignored(self):
84 84 '''files not in the dirstate that are ignored (by _dirignore())'''
85 85 return self[5]
86 86
87 87 @property
88 88 def clean(self):
89 89 '''files that have not been modified'''
90 90 return self[6]
91 91
92 92 def __repr__(self, *args, **kwargs):
93 93 return (('<status modified=%r, added=%r, removed=%r, deleted=%r, '
94 94 'unknown=%r, ignored=%r, clean=%r>') % self)
95 95
96 96 def itersubrepos(ctx1, ctx2):
97 97 """find subrepos in ctx1 or ctx2"""
98 98 # Create a (subpath, ctx) mapping where we prefer subpaths from
99 99 # ctx1. The subpaths from ctx2 are important when the .hgsub file
100 100 # has been modified (in ctx2) but not yet committed (in ctx1).
101 101 subpaths = dict.fromkeys(ctx2.substate, ctx2)
102 102 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
103 103
104 104 missing = set()
105 105
106 106 for subpath in ctx2.substate:
107 107 if subpath not in ctx1.substate:
108 108 del subpaths[subpath]
109 109 missing.add(subpath)
110 110
111 111 for subpath, ctx in sorted(subpaths.iteritems()):
112 112 yield subpath, ctx.sub(subpath)
113 113
114 114 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
115 115 # status and diff will have an accurate result when it does
116 116 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
117 117 # against itself.
118 118 for subpath in missing:
119 119 yield subpath, ctx2.nullsub(subpath, ctx1)
120 120
121 121 def nochangesfound(ui, repo, excluded=None):
122 122 '''Report no changes for push/pull, excluded is None or a list of
123 123 nodes excluded from the push/pull.
124 124 '''
125 125 secretlist = []
126 126 if excluded:
127 127 for n in excluded:
128 128 if n not in repo:
129 129 # discovery should not have included the filtered revision,
130 130 # we have to explicitly exclude it until discovery is cleanup.
131 131 continue
132 132 ctx = repo[n]
133 133 if ctx.phase() >= phases.secret and not ctx.extinct():
134 134 secretlist.append(n)
135 135
136 136 if secretlist:
137 137 ui.status(_("no changes found (ignored %d secret changesets)\n")
138 138 % len(secretlist))
139 139 else:
140 140 ui.status(_("no changes found\n"))
141 141
142 142 def checknewlabel(repo, lbl, kind):
143 143 # Do not use the "kind" parameter in ui output.
144 144 # It makes strings difficult to translate.
145 145 if lbl in ['tip', '.', 'null']:
146 146 raise error.Abort(_("the name '%s' is reserved") % lbl)
147 147 for c in (':', '\0', '\n', '\r'):
148 148 if c in lbl:
149 149 raise error.Abort(_("%r cannot be used in a name") % c)
150 150 try:
151 151 int(lbl)
152 152 raise error.Abort(_("cannot use an integer as a name"))
153 153 except ValueError:
154 154 pass
155 155
156 156 def checkfilename(f):
157 157 '''Check that the filename f is an acceptable filename for a tracked file'''
158 158 if '\r' in f or '\n' in f:
159 159 raise error.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
160 160
161 161 def checkportable(ui, f):
162 162 '''Check if filename f is portable and warn or abort depending on config'''
163 163 checkfilename(f)
164 164 abort, warn = checkportabilityalert(ui)
165 165 if abort or warn:
166 166 msg = util.checkwinfilename(f)
167 167 if msg:
168 168 msg = "%s: %r" % (msg, f)
169 169 if abort:
170 170 raise error.Abort(msg)
171 171 ui.warn(_("warning: %s\n") % msg)
172 172
173 173 def checkportabilityalert(ui):
174 174 '''check if the user's config requests nothing, a warning, or abort for
175 175 non-portable filenames'''
176 176 val = ui.config('ui', 'portablefilenames', 'warn')
177 177 lval = val.lower()
178 178 bval = util.parsebool(val)
179 179 abort = os.name == 'nt' or lval == 'abort'
180 180 warn = bval or lval == 'warn'
181 181 if bval is None and not (warn or abort or lval == 'ignore'):
182 182 raise error.ConfigError(
183 183 _("ui.portablefilenames value is invalid ('%s')") % val)
184 184 return abort, warn
185 185
186 186 class casecollisionauditor(object):
187 187 def __init__(self, ui, abort, dirstate):
188 188 self._ui = ui
189 189 self._abort = abort
190 190 allfiles = '\0'.join(dirstate._map)
191 191 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
192 192 self._dirstate = dirstate
193 193 # The purpose of _newfiles is so that we don't complain about
194 194 # case collisions if someone were to call this object with the
195 195 # same filename twice.
196 196 self._newfiles = set()
197 197
198 198 def __call__(self, f):
199 199 if f in self._newfiles:
200 200 return
201 201 fl = encoding.lower(f)
202 202 if fl in self._loweredfiles and f not in self._dirstate:
203 203 msg = _('possible case-folding collision for %s') % f
204 204 if self._abort:
205 205 raise error.Abort(msg)
206 206 self._ui.warn(_("warning: %s\n") % msg)
207 207 self._loweredfiles.add(fl)
208 208 self._newfiles.add(f)
209 209
210 210 def filteredhash(repo, maxrev):
211 211 """build hash of filtered revisions in the current repoview.
212 212
213 213 Multiple caches perform up-to-date validation by checking that the
214 214 tiprev and tipnode stored in the cache file match the current repository.
215 215 However, this is not sufficient for validating repoviews because the set
216 216 of revisions in the view may change without the repository tiprev and
217 217 tipnode changing.
218 218
219 219 This function hashes all the revs filtered from the view and returns
220 220 that SHA-1 digest.
221 221 """
222 222 cl = repo.changelog
223 223 if not cl.filteredrevs:
224 224 return None
225 225 key = None
226 226 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
227 227 if revs:
228 228 s = hashlib.sha1()
229 229 for rev in revs:
230 230 s.update('%s;' % rev)
231 231 key = s.digest()
232 232 return key
233 233
234 234 class abstractvfs(object):
235 235 """Abstract base class; cannot be instantiated"""
236 236
237 237 def __init__(self, *args, **kwargs):
238 238 '''Prevent instantiation; don't call this from subclasses.'''
239 239 raise NotImplementedError('attempted instantiating ' + str(type(self)))
240 240
241 241 def tryread(self, path):
242 242 '''gracefully return an empty string for missing files'''
243 243 try:
244 244 return self.read(path)
245 245 except IOError as inst:
246 246 if inst.errno != errno.ENOENT:
247 247 raise
248 248 return ""
249 249
250 250 def tryreadlines(self, path, mode='rb'):
251 251 '''gracefully return an empty array for missing files'''
252 252 try:
253 253 return self.readlines(path, mode=mode)
254 254 except IOError as inst:
255 255 if inst.errno != errno.ENOENT:
256 256 raise
257 257 return []
258 258
259 259 def open(self, path, mode="r", text=False, atomictemp=False,
260 260 notindexed=False, backgroundclose=False):
261 261 '''Open ``path`` file, which is relative to vfs root.
262 262
263 263 Newly created directories are marked as "not to be indexed by
264 264 the content indexing service", if ``notindexed`` is specified
265 265 for "write" mode access.
266 266 '''
267 267 self.open = self.__call__
268 268 return self.__call__(path, mode, text, atomictemp, notindexed,
269 269 backgroundclose=backgroundclose)
270 270
271 271 def read(self, path):
272 272 with self(path, 'rb') as fp:
273 273 return fp.read()
274 274
275 275 def readlines(self, path, mode='rb'):
276 276 with self(path, mode=mode) as fp:
277 277 return fp.readlines()
278 278
279 279 def write(self, path, data, backgroundclose=False):
280 280 with self(path, 'wb', backgroundclose=backgroundclose) as fp:
281 281 return fp.write(data)
282 282
283 283 def writelines(self, path, data, mode='wb', notindexed=False):
284 284 with self(path, mode=mode, notindexed=notindexed) as fp:
285 285 return fp.writelines(data)
286 286
287 287 def append(self, path, data):
288 288 with self(path, 'ab') as fp:
289 289 return fp.write(data)
290 290
291 291 def basename(self, path):
292 292 """return base element of a path (as os.path.basename would do)
293 293
294 294 This exists to allow handling of strange encoding if needed."""
295 295 return os.path.basename(path)
296 296
297 297 def chmod(self, path, mode):
298 298 return os.chmod(self.join(path), mode)
299 299
300 300 def dirname(self, path):
301 301 """return dirname element of a path (as os.path.dirname would do)
302 302
303 303 This exists to allow handling of strange encoding if needed."""
304 304 return os.path.dirname(path)
305 305
306 306 def exists(self, path=None):
307 307 return os.path.exists(self.join(path))
308 308
309 309 def fstat(self, fp):
310 310 return util.fstat(fp)
311 311
312 312 def isdir(self, path=None):
313 313 return os.path.isdir(self.join(path))
314 314
315 315 def isfile(self, path=None):
316 316 return os.path.isfile(self.join(path))
317 317
318 318 def islink(self, path=None):
319 319 return os.path.islink(self.join(path))
320 320
321 321 def isfileorlink(self, path=None):
322 322 '''return whether path is a regular file or a symlink
323 323
324 324 Unlike isfile, this doesn't follow symlinks.'''
325 325 try:
326 326 st = self.lstat(path)
327 327 except OSError:
328 328 return False
329 329 mode = st.st_mode
330 330 return stat.S_ISREG(mode) or stat.S_ISLNK(mode)
331 331
332 332 def reljoin(self, *paths):
333 333 """join various elements of a path together (as os.path.join would do)
334 334
335 335 The vfs base is not injected so that path stay relative. This exists
336 336 to allow handling of strange encoding if needed."""
337 337 return os.path.join(*paths)
338 338
339 339 def split(self, path):
340 340 """split top-most element of a path (as os.path.split would do)
341 341
342 342 This exists to allow handling of strange encoding if needed."""
343 343 return os.path.split(path)
344 344
345 345 def lexists(self, path=None):
346 346 return os.path.lexists(self.join(path))
347 347
348 348 def lstat(self, path=None):
349 349 return os.lstat(self.join(path))
350 350
351 351 def listdir(self, path=None):
352 352 return os.listdir(self.join(path))
353 353
354 354 def makedir(self, path=None, notindexed=True):
355 355 return util.makedir(self.join(path), notindexed)
356 356
357 357 def makedirs(self, path=None, mode=None):
358 358 return util.makedirs(self.join(path), mode)
359 359
360 360 def makelock(self, info, path):
361 361 return util.makelock(info, self.join(path))
362 362
363 363 def mkdir(self, path=None):
364 364 return os.mkdir(self.join(path))
365 365
366 366 def mkstemp(self, suffix='', prefix='tmp', dir=None, text=False):
367 367 fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix,
368 368 dir=self.join(dir), text=text)
369 369 dname, fname = util.split(name)
370 370 if dir:
371 371 return fd, os.path.join(dir, fname)
372 372 else:
373 373 return fd, fname
374 374
375 375 def readdir(self, path=None, stat=None, skip=None):
376 376 return osutil.listdir(self.join(path), stat, skip)
377 377
378 378 def readlock(self, path):
379 379 return util.readlock(self.join(path))
380 380
381 381 def rename(self, src, dst, checkambig=False):
382 382 """Rename from src to dst
383 383
384 384 checkambig argument is used with util.filestat, and is useful
385 385 only if destination file is guarded by any lock
386 386 (e.g. repo.lock or repo.wlock).
387 387 """
388 388 dstpath = self.join(dst)
389 389 oldstat = checkambig and util.filestat(dstpath)
390 390 if oldstat and oldstat.stat:
391 391 ret = util.rename(self.join(src), dstpath)
392 392 newstat = util.filestat(dstpath)
393 393 if newstat.isambig(oldstat):
394 394 # stat of renamed file is ambiguous to original one
395 395 advanced = (oldstat.stat.st_mtime + 1) & 0x7fffffff
396 396 os.utime(dstpath, (advanced, advanced))
397 397 return ret
398 398 return util.rename(self.join(src), dstpath)
399 399
400 400 def readlink(self, path):
401 401 return os.readlink(self.join(path))
402 402
403 403 def removedirs(self, path=None):
404 404 """Remove a leaf directory and all empty intermediate ones
405 405 """
406 406 return util.removedirs(self.join(path))
407 407
408 408 def rmtree(self, path=None, ignore_errors=False, forcibly=False):
409 409 """Remove a directory tree recursively
410 410
411 411 If ``forcibly``, this tries to remove READ-ONLY files, too.
412 412 """
413 413 if forcibly:
414 414 def onerror(function, path, excinfo):
415 415 if function is not os.remove:
416 416 raise
417 417 # read-only files cannot be unlinked under Windows
418 418 s = os.stat(path)
419 419 if (s.st_mode & stat.S_IWRITE) != 0:
420 420 raise
421 421 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
422 422 os.remove(path)
423 423 else:
424 424 onerror = None
425 425 return shutil.rmtree(self.join(path),
426 426 ignore_errors=ignore_errors, onerror=onerror)
427 427
428 428 def setflags(self, path, l, x):
429 429 return util.setflags(self.join(path), l, x)
430 430
431 431 def stat(self, path=None):
432 432 return os.stat(self.join(path))
433 433
434 434 def unlink(self, path=None):
435 435 return util.unlink(self.join(path))
436 436
437 437 def unlinkpath(self, path=None, ignoremissing=False):
438 438 return util.unlinkpath(self.join(path), ignoremissing)
439 439
440 440 def utime(self, path=None, t=None):
441 441 return os.utime(self.join(path), t)
442 442
443 443 def walk(self, path=None, onerror=None):
444 444 """Yield (dirpath, dirs, files) tuple for each directories under path
445 445
446 446 ``dirpath`` is relative one from the root of this vfs. This
447 447 uses ``os.sep`` as path separator, even you specify POSIX
448 448 style ``path``.
449 449
450 450 "The root of this vfs" is represented as empty ``dirpath``.
451 451 """
452 452 root = os.path.normpath(self.join(None))
453 453 # when dirpath == root, dirpath[prefixlen:] becomes empty
454 454 # because len(dirpath) < prefixlen.
455 455 prefixlen = len(pathutil.normasprefix(root))
456 456 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
457 457 yield (dirpath[prefixlen:], dirs, files)
458 458
459 459 @contextlib.contextmanager
460 460 def backgroundclosing(self, ui, expectedcount=-1):
461 461 """Allow files to be closed asynchronously.
462 462
463 463 When this context manager is active, ``backgroundclose`` can be passed
464 464 to ``__call__``/``open`` to result in the file possibly being closed
465 465 asynchronously, on a background thread.
466 466 """
467 467 # This is an arbitrary restriction and could be changed if we ever
468 468 # have a use case.
469 469 vfs = getattr(self, 'vfs', self)
470 470 if getattr(vfs, '_backgroundfilecloser', None):
471 raise error.Abort('can only have 1 active background file closer')
471 raise error.Abort(
472 _('can only have 1 active background file closer'))
472 473
473 474 with backgroundfilecloser(ui, expectedcount=expectedcount) as bfc:
474 475 try:
475 476 vfs._backgroundfilecloser = bfc
476 477 yield bfc
477 478 finally:
478 479 vfs._backgroundfilecloser = None
479 480
480 481 class vfs(abstractvfs):
481 482 '''Operate files relative to a base directory
482 483
483 484 This class is used to hide the details of COW semantics and
484 485 remote file access from higher level code.
485 486 '''
486 487 def __init__(self, base, audit=True, expandpath=False, realpath=False):
487 488 if expandpath:
488 489 base = util.expandpath(base)
489 490 if realpath:
490 491 base = os.path.realpath(base)
491 492 self.base = base
492 493 self.mustaudit = audit
493 494 self.createmode = None
494 495 self._trustnlink = None
495 496
496 497 @property
497 498 def mustaudit(self):
498 499 return self._audit
499 500
500 501 @mustaudit.setter
501 502 def mustaudit(self, onoff):
502 503 self._audit = onoff
503 504 if onoff:
504 505 self.audit = pathutil.pathauditor(self.base)
505 506 else:
506 507 self.audit = util.always
507 508
508 509 @util.propertycache
509 510 def _cansymlink(self):
510 511 return util.checklink(self.base)
511 512
512 513 @util.propertycache
513 514 def _chmod(self):
514 515 return util.checkexec(self.base)
515 516
516 517 def _fixfilemode(self, name):
517 518 if self.createmode is None or not self._chmod:
518 519 return
519 520 os.chmod(name, self.createmode & 0o666)
520 521
521 522 def __call__(self, path, mode="r", text=False, atomictemp=False,
522 523 notindexed=False, backgroundclose=False, checkambig=False):
523 524 '''Open ``path`` file, which is relative to vfs root.
524 525
525 526 Newly created directories are marked as "not to be indexed by
526 527 the content indexing service", if ``notindexed`` is specified
527 528 for "write" mode access.
528 529
529 530 If ``backgroundclose`` is passed, the file may be closed asynchronously.
530 531 It can only be used if the ``self.backgroundclosing()`` context manager
531 532 is active. This should only be specified if the following criteria hold:
532 533
533 534 1. There is a potential for writing thousands of files. Unless you
534 535 are writing thousands of files, the performance benefits of
535 536 asynchronously closing files is not realized.
536 537 2. Files are opened exactly once for the ``backgroundclosing``
537 538 active duration and are therefore free of race conditions between
538 539 closing a file on a background thread and reopening it. (If the
539 540 file were opened multiple times, there could be unflushed data
540 541 because the original file handle hasn't been flushed/closed yet.)
541 542
542 543 ``checkambig`` argument is passed to atomictemplfile (valid
543 544 only for writing), and is useful only if target file is
544 545 guarded by any lock (e.g. repo.lock or repo.wlock).
545 546 '''
546 547 if self._audit:
547 548 r = util.checkosfilename(path)
548 549 if r:
549 550 raise error.Abort("%s: %r" % (r, path))
550 551 self.audit(path)
551 552 f = self.join(path)
552 553
553 554 if not text and "b" not in mode:
554 555 mode += "b" # for that other OS
555 556
556 557 nlink = -1
557 558 if mode not in ('r', 'rb'):
558 559 dirname, basename = util.split(f)
559 560 # If basename is empty, then the path is malformed because it points
560 561 # to a directory. Let the posixfile() call below raise IOError.
561 562 if basename:
562 563 if atomictemp:
563 564 util.makedirs(dirname, self.createmode, notindexed)
564 565 return util.atomictempfile(f, mode, self.createmode,
565 566 checkambig=checkambig)
566 567 try:
567 568 if 'w' in mode:
568 569 util.unlink(f)
569 570 nlink = 0
570 571 else:
571 572 # nlinks() may behave differently for files on Windows
572 573 # shares if the file is open.
573 574 with util.posixfile(f):
574 575 nlink = util.nlinks(f)
575 576 if nlink < 1:
576 577 nlink = 2 # force mktempcopy (issue1922)
577 578 except (OSError, IOError) as e:
578 579 if e.errno != errno.ENOENT:
579 580 raise
580 581 nlink = 0
581 582 util.makedirs(dirname, self.createmode, notindexed)
582 583 if nlink > 0:
583 584 if self._trustnlink is None:
584 585 self._trustnlink = nlink > 1 or util.checknlink(f)
585 586 if nlink > 1 or not self._trustnlink:
586 587 util.rename(util.mktempcopy(f), f)
587 588 fp = util.posixfile(f, mode)
588 589 if nlink == 0:
589 590 self._fixfilemode(f)
590 591
591 592 if backgroundclose:
592 593 if not self._backgroundfilecloser:
593 raise error.Abort('backgroundclose can only be used when a '
594 raise error.Abort(_('backgroundclose can only be used when a '
594 595 'backgroundclosing context manager is active')
596 )
595 597
596 598 fp = delayclosedfile(fp, self._backgroundfilecloser)
597 599
598 600 return fp
599 601
600 602 def symlink(self, src, dst):
601 603 self.audit(dst)
602 604 linkname = self.join(dst)
603 605 try:
604 606 os.unlink(linkname)
605 607 except OSError:
606 608 pass
607 609
608 610 util.makedirs(os.path.dirname(linkname), self.createmode)
609 611
610 612 if self._cansymlink:
611 613 try:
612 614 os.symlink(src, linkname)
613 615 except OSError as err:
614 616 raise OSError(err.errno, _('could not symlink to %r: %s') %
615 617 (src, err.strerror), linkname)
616 618 else:
617 619 self.write(dst, src)
618 620
619 621 def join(self, path, *insidef):
620 622 if path:
621 623 return os.path.join(self.base, path, *insidef)
622 624 else:
623 625 return self.base
624 626
625 627 opener = vfs
626 628
627 629 class auditvfs(object):
628 630 def __init__(self, vfs):
629 631 self.vfs = vfs
630 632
631 633 @property
632 634 def mustaudit(self):
633 635 return self.vfs.mustaudit
634 636
635 637 @mustaudit.setter
636 638 def mustaudit(self, onoff):
637 639 self.vfs.mustaudit = onoff
638 640
639 641 class filtervfs(abstractvfs, auditvfs):
640 642 '''Wrapper vfs for filtering filenames with a function.'''
641 643
642 644 def __init__(self, vfs, filter):
643 645 auditvfs.__init__(self, vfs)
644 646 self._filter = filter
645 647
646 648 def __call__(self, path, *args, **kwargs):
647 649 return self.vfs(self._filter(path), *args, **kwargs)
648 650
649 651 def join(self, path, *insidef):
650 652 if path:
651 653 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
652 654 else:
653 655 return self.vfs.join(path)
654 656
655 657 filteropener = filtervfs
656 658
657 659 class readonlyvfs(abstractvfs, auditvfs):
658 660 '''Wrapper vfs preventing any writing.'''
659 661
660 662 def __init__(self, vfs):
661 663 auditvfs.__init__(self, vfs)
662 664
663 665 def __call__(self, path, mode='r', *args, **kw):
664 666 if mode not in ('r', 'rb'):
665 raise error.Abort('this vfs is read only')
667 raise error.Abort(_('this vfs is read only'))
666 668 return self.vfs(path, mode, *args, **kw)
667 669
668 670 def join(self, path, *insidef):
669 671 return self.vfs.join(path, *insidef)
670 672
671 673 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
672 674 '''yield every hg repository under path, always recursively.
673 675 The recurse flag will only control recursion into repo working dirs'''
674 676 def errhandler(err):
675 677 if err.filename == path:
676 678 raise err
677 679 samestat = getattr(os.path, 'samestat', None)
678 680 if followsym and samestat is not None:
679 681 def adddir(dirlst, dirname):
680 682 match = False
681 683 dirstat = os.stat(dirname)
682 684 for lstdirstat in dirlst:
683 685 if samestat(dirstat, lstdirstat):
684 686 match = True
685 687 break
686 688 if not match:
687 689 dirlst.append(dirstat)
688 690 return not match
689 691 else:
690 692 followsym = False
691 693
692 694 if (seen_dirs is None) and followsym:
693 695 seen_dirs = []
694 696 adddir(seen_dirs, path)
695 697 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
696 698 dirs.sort()
697 699 if '.hg' in dirs:
698 700 yield root # found a repository
699 701 qroot = os.path.join(root, '.hg', 'patches')
700 702 if os.path.isdir(os.path.join(qroot, '.hg')):
701 703 yield qroot # we have a patch queue repo here
702 704 if recurse:
703 705 # avoid recursing inside the .hg directory
704 706 dirs.remove('.hg')
705 707 else:
706 708 dirs[:] = [] # don't descend further
707 709 elif followsym:
708 710 newdirs = []
709 711 for d in dirs:
710 712 fname = os.path.join(root, d)
711 713 if adddir(seen_dirs, fname):
712 714 if os.path.islink(fname):
713 715 for hgname in walkrepos(fname, True, seen_dirs):
714 716 yield hgname
715 717 else:
716 718 newdirs.append(d)
717 719 dirs[:] = newdirs
718 720
719 721 def osrcpath():
720 722 '''return default os-specific hgrc search path'''
721 723 path = []
722 724 defaultpath = os.path.join(util.datapath, 'default.d')
723 725 if os.path.isdir(defaultpath):
724 726 for f, kind in osutil.listdir(defaultpath):
725 727 if f.endswith('.rc'):
726 728 path.append(os.path.join(defaultpath, f))
727 729 path.extend(systemrcpath())
728 730 path.extend(userrcpath())
729 731 path = [os.path.normpath(f) for f in path]
730 732 return path
731 733
732 734 _rcpath = None
733 735
734 736 def rcpath():
735 737 '''return hgrc search path. if env var HGRCPATH is set, use it.
736 738 for each item in path, if directory, use files ending in .rc,
737 739 else use item.
738 740 make HGRCPATH empty to only look in .hg/hgrc of current repo.
739 741 if no HGRCPATH, use default os-specific path.'''
740 742 global _rcpath
741 743 if _rcpath is None:
742 744 if 'HGRCPATH' in os.environ:
743 745 _rcpath = []
744 746 for p in os.environ['HGRCPATH'].split(os.pathsep):
745 747 if not p:
746 748 continue
747 749 p = util.expandpath(p)
748 750 if os.path.isdir(p):
749 751 for f, kind in osutil.listdir(p):
750 752 if f.endswith('.rc'):
751 753 _rcpath.append(os.path.join(p, f))
752 754 else:
753 755 _rcpath.append(p)
754 756 else:
755 757 _rcpath = osrcpath()
756 758 return _rcpath
757 759
758 760 def intrev(rev):
759 761 """Return integer for a given revision that can be used in comparison or
760 762 arithmetic operation"""
761 763 if rev is None:
762 764 return wdirrev
763 765 return rev
764 766
765 767 def revsingle(repo, revspec, default='.'):
766 768 if not revspec and revspec != 0:
767 769 return repo[default]
768 770
769 771 l = revrange(repo, [revspec])
770 772 if not l:
771 773 raise error.Abort(_('empty revision set'))
772 774 return repo[l.last()]
773 775
774 776 def _pairspec(revspec):
775 777 tree = revset.parse(revspec)
776 778 tree = revset.optimize(tree) # fix up "x^:y" -> "(x^):y"
777 779 return tree and tree[0] in ('range', 'rangepre', 'rangepost', 'rangeall')
778 780
779 781 def revpair(repo, revs):
780 782 if not revs:
781 783 return repo.dirstate.p1(), None
782 784
783 785 l = revrange(repo, revs)
784 786
785 787 if not l:
786 788 first = second = None
787 789 elif l.isascending():
788 790 first = l.min()
789 791 second = l.max()
790 792 elif l.isdescending():
791 793 first = l.max()
792 794 second = l.min()
793 795 else:
794 796 first = l.first()
795 797 second = l.last()
796 798
797 799 if first is None:
798 800 raise error.Abort(_('empty revision range'))
799 801 if (first == second and len(revs) >= 2
800 802 and not all(revrange(repo, [r]) for r in revs)):
801 803 raise error.Abort(_('empty revision on one side of range'))
802 804
803 805 # if top-level is range expression, the result must always be a pair
804 806 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
805 807 return repo.lookup(first), None
806 808
807 809 return repo.lookup(first), repo.lookup(second)
808 810
809 811 def revrange(repo, revs):
810 812 """Yield revision as strings from a list of revision specifications."""
811 813 allspecs = []
812 814 for spec in revs:
813 815 if isinstance(spec, int):
814 816 spec = revset.formatspec('rev(%d)', spec)
815 817 allspecs.append(spec)
816 818 m = revset.matchany(repo.ui, allspecs, repo)
817 819 return m(repo)
818 820
819 821 def meaningfulparents(repo, ctx):
820 822 """Return list of meaningful (or all if debug) parentrevs for rev.
821 823
822 824 For merges (two non-nullrev revisions) both parents are meaningful.
823 825 Otherwise the first parent revision is considered meaningful if it
824 826 is not the preceding revision.
825 827 """
826 828 parents = ctx.parents()
827 829 if len(parents) > 1:
828 830 return parents
829 831 if repo.ui.debugflag:
830 832 return [parents[0], repo['null']]
831 833 if parents[0].rev() >= intrev(ctx.rev()) - 1:
832 834 return []
833 835 return parents
834 836
835 837 def expandpats(pats):
836 838 '''Expand bare globs when running on windows.
837 839 On posix we assume it already has already been done by sh.'''
838 840 if not util.expandglobs:
839 841 return list(pats)
840 842 ret = []
841 843 for kindpat in pats:
842 844 kind, pat = matchmod._patsplit(kindpat, None)
843 845 if kind is None:
844 846 try:
845 847 globbed = glob.glob(pat)
846 848 except re.error:
847 849 globbed = [pat]
848 850 if globbed:
849 851 ret.extend(globbed)
850 852 continue
851 853 ret.append(kindpat)
852 854 return ret
853 855
854 856 def matchandpats(ctx, pats=(), opts=None, globbed=False, default='relpath',
855 857 badfn=None):
856 858 '''Return a matcher and the patterns that were used.
857 859 The matcher will warn about bad matches, unless an alternate badfn callback
858 860 is provided.'''
859 861 if pats == ("",):
860 862 pats = []
861 863 if opts is None:
862 864 opts = {}
863 865 if not globbed and default == 'relpath':
864 866 pats = expandpats(pats or [])
865 867
866 868 def bad(f, msg):
867 869 ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg))
868 870
869 871 if badfn is None:
870 872 badfn = bad
871 873
872 874 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
873 875 default, listsubrepos=opts.get('subrepos'), badfn=badfn)
874 876
875 877 if m.always():
876 878 pats = []
877 879 return m, pats
878 880
879 881 def match(ctx, pats=(), opts=None, globbed=False, default='relpath',
880 882 badfn=None):
881 883 '''Return a matcher that will warn about bad matches.'''
882 884 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
883 885
884 886 def matchall(repo):
885 887 '''Return a matcher that will efficiently match everything.'''
886 888 return matchmod.always(repo.root, repo.getcwd())
887 889
888 890 def matchfiles(repo, files, badfn=None):
889 891 '''Return a matcher that will efficiently match exactly these files.'''
890 892 return matchmod.exact(repo.root, repo.getcwd(), files, badfn=badfn)
891 893
892 894 def origpath(ui, repo, filepath):
893 895 '''customize where .orig files are created
894 896
895 897 Fetch user defined path from config file: [ui] origbackuppath = <path>
896 898 Fall back to default (filepath) if not specified
897 899 '''
898 900 origbackuppath = ui.config('ui', 'origbackuppath', None)
899 901 if origbackuppath is None:
900 902 return filepath + ".orig"
901 903
902 904 filepathfromroot = os.path.relpath(filepath, start=repo.root)
903 905 fullorigpath = repo.wjoin(origbackuppath, filepathfromroot)
904 906
905 907 origbackupdir = repo.vfs.dirname(fullorigpath)
906 908 if not repo.vfs.exists(origbackupdir):
907 909 ui.note(_('creating directory: %s\n') % origbackupdir)
908 910 util.makedirs(origbackupdir)
909 911
910 912 return fullorigpath + ".orig"
911 913
912 914 def addremove(repo, matcher, prefix, opts=None, dry_run=None, similarity=None):
913 915 if opts is None:
914 916 opts = {}
915 917 m = matcher
916 918 if dry_run is None:
917 919 dry_run = opts.get('dry_run')
918 920 if similarity is None:
919 921 similarity = float(opts.get('similarity') or 0)
920 922
921 923 ret = 0
922 924 join = lambda f: os.path.join(prefix, f)
923 925
924 926 def matchessubrepo(matcher, subpath):
925 927 if matcher.exact(subpath):
926 928 return True
927 929 for f in matcher.files():
928 930 if f.startswith(subpath):
929 931 return True
930 932 return False
931 933
932 934 wctx = repo[None]
933 935 for subpath in sorted(wctx.substate):
934 936 if opts.get('subrepos') or matchessubrepo(m, subpath):
935 937 sub = wctx.sub(subpath)
936 938 try:
937 939 submatch = matchmod.subdirmatcher(subpath, m)
938 940 if sub.addremove(submatch, prefix, opts, dry_run, similarity):
939 941 ret = 1
940 942 except error.LookupError:
941 943 repo.ui.status(_("skipping missing subrepository: %s\n")
942 944 % join(subpath))
943 945
944 946 rejected = []
945 947 def badfn(f, msg):
946 948 if f in m.files():
947 949 m.bad(f, msg)
948 950 rejected.append(f)
949 951
950 952 badmatch = matchmod.badmatch(m, badfn)
951 953 added, unknown, deleted, removed, forgotten = _interestingfiles(repo,
952 954 badmatch)
953 955
954 956 unknownset = set(unknown + forgotten)
955 957 toprint = unknownset.copy()
956 958 toprint.update(deleted)
957 959 for abs in sorted(toprint):
958 960 if repo.ui.verbose or not m.exact(abs):
959 961 if abs in unknownset:
960 962 status = _('adding %s\n') % m.uipath(abs)
961 963 else:
962 964 status = _('removing %s\n') % m.uipath(abs)
963 965 repo.ui.status(status)
964 966
965 967 renames = _findrenames(repo, m, added + unknown, removed + deleted,
966 968 similarity)
967 969
968 970 if not dry_run:
969 971 _markchanges(repo, unknown + forgotten, deleted, renames)
970 972
971 973 for f in rejected:
972 974 if f in m.files():
973 975 return 1
974 976 return ret
975 977
976 978 def marktouched(repo, files, similarity=0.0):
977 979 '''Assert that files have somehow been operated upon. files are relative to
978 980 the repo root.'''
979 981 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
980 982 rejected = []
981 983
982 984 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
983 985
984 986 if repo.ui.verbose:
985 987 unknownset = set(unknown + forgotten)
986 988 toprint = unknownset.copy()
987 989 toprint.update(deleted)
988 990 for abs in sorted(toprint):
989 991 if abs in unknownset:
990 992 status = _('adding %s\n') % abs
991 993 else:
992 994 status = _('removing %s\n') % abs
993 995 repo.ui.status(status)
994 996
995 997 renames = _findrenames(repo, m, added + unknown, removed + deleted,
996 998 similarity)
997 999
998 1000 _markchanges(repo, unknown + forgotten, deleted, renames)
999 1001
1000 1002 for f in rejected:
1001 1003 if f in m.files():
1002 1004 return 1
1003 1005 return 0
1004 1006
1005 1007 def _interestingfiles(repo, matcher):
1006 1008 '''Walk dirstate with matcher, looking for files that addremove would care
1007 1009 about.
1008 1010
1009 1011 This is different from dirstate.status because it doesn't care about
1010 1012 whether files are modified or clean.'''
1011 1013 added, unknown, deleted, removed, forgotten = [], [], [], [], []
1012 1014 audit_path = pathutil.pathauditor(repo.root)
1013 1015
1014 1016 ctx = repo[None]
1015 1017 dirstate = repo.dirstate
1016 1018 walkresults = dirstate.walk(matcher, sorted(ctx.substate), True, False,
1017 1019 full=False)
1018 1020 for abs, st in walkresults.iteritems():
1019 1021 dstate = dirstate[abs]
1020 1022 if dstate == '?' and audit_path.check(abs):
1021 1023 unknown.append(abs)
1022 1024 elif dstate != 'r' and not st:
1023 1025 deleted.append(abs)
1024 1026 elif dstate == 'r' and st:
1025 1027 forgotten.append(abs)
1026 1028 # for finding renames
1027 1029 elif dstate == 'r' and not st:
1028 1030 removed.append(abs)
1029 1031 elif dstate == 'a':
1030 1032 added.append(abs)
1031 1033
1032 1034 return added, unknown, deleted, removed, forgotten
1033 1035
1034 1036 def _findrenames(repo, matcher, added, removed, similarity):
1035 1037 '''Find renames from removed files to added ones.'''
1036 1038 renames = {}
1037 1039 if similarity > 0:
1038 1040 for old, new, score in similar.findrenames(repo, added, removed,
1039 1041 similarity):
1040 1042 if (repo.ui.verbose or not matcher.exact(old)
1041 1043 or not matcher.exact(new)):
1042 1044 repo.ui.status(_('recording removal of %s as rename to %s '
1043 1045 '(%d%% similar)\n') %
1044 1046 (matcher.rel(old), matcher.rel(new),
1045 1047 score * 100))
1046 1048 renames[new] = old
1047 1049 return renames
1048 1050
1049 1051 def _markchanges(repo, unknown, deleted, renames):
1050 1052 '''Marks the files in unknown as added, the files in deleted as removed,
1051 1053 and the files in renames as copied.'''
1052 1054 wctx = repo[None]
1053 1055 with repo.wlock():
1054 1056 wctx.forget(deleted)
1055 1057 wctx.add(unknown)
1056 1058 for new, old in renames.iteritems():
1057 1059 wctx.copy(old, new)
1058 1060
1059 1061 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
1060 1062 """Update the dirstate to reflect the intent of copying src to dst. For
1061 1063 different reasons it might not end with dst being marked as copied from src.
1062 1064 """
1063 1065 origsrc = repo.dirstate.copied(src) or src
1064 1066 if dst == origsrc: # copying back a copy?
1065 1067 if repo.dirstate[dst] not in 'mn' and not dryrun:
1066 1068 repo.dirstate.normallookup(dst)
1067 1069 else:
1068 1070 if repo.dirstate[origsrc] == 'a' and origsrc == src:
1069 1071 if not ui.quiet:
1070 1072 ui.warn(_("%s has not been committed yet, so no copy "
1071 1073 "data will be stored for %s.\n")
1072 1074 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
1073 1075 if repo.dirstate[dst] in '?r' and not dryrun:
1074 1076 wctx.add([dst])
1075 1077 elif not dryrun:
1076 1078 wctx.copy(origsrc, dst)
1077 1079
1078 1080 def readrequires(opener, supported):
1079 1081 '''Reads and parses .hg/requires and checks if all entries found
1080 1082 are in the list of supported features.'''
1081 1083 requirements = set(opener.read("requires").splitlines())
1082 1084 missings = []
1083 1085 for r in requirements:
1084 1086 if r not in supported:
1085 1087 if not r or not r[0].isalnum():
1086 1088 raise error.RequirementError(_(".hg/requires file is corrupt"))
1087 1089 missings.append(r)
1088 1090 missings.sort()
1089 1091 if missings:
1090 1092 raise error.RequirementError(
1091 1093 _("repository requires features unknown to this Mercurial: %s")
1092 1094 % " ".join(missings),
1093 1095 hint=_("see https://mercurial-scm.org/wiki/MissingRequirement"
1094 1096 " for more information"))
1095 1097 return requirements
1096 1098
1097 1099 def writerequires(opener, requirements):
1098 1100 with opener('requires', 'w') as fp:
1099 1101 for r in sorted(requirements):
1100 1102 fp.write("%s\n" % r)
1101 1103
1102 1104 class filecachesubentry(object):
1103 1105 def __init__(self, path, stat):
1104 1106 self.path = path
1105 1107 self.cachestat = None
1106 1108 self._cacheable = None
1107 1109
1108 1110 if stat:
1109 1111 self.cachestat = filecachesubentry.stat(self.path)
1110 1112
1111 1113 if self.cachestat:
1112 1114 self._cacheable = self.cachestat.cacheable()
1113 1115 else:
1114 1116 # None means we don't know yet
1115 1117 self._cacheable = None
1116 1118
1117 1119 def refresh(self):
1118 1120 if self.cacheable():
1119 1121 self.cachestat = filecachesubentry.stat(self.path)
1120 1122
1121 1123 def cacheable(self):
1122 1124 if self._cacheable is not None:
1123 1125 return self._cacheable
1124 1126
1125 1127 # we don't know yet, assume it is for now
1126 1128 return True
1127 1129
1128 1130 def changed(self):
1129 1131 # no point in going further if we can't cache it
1130 1132 if not self.cacheable():
1131 1133 return True
1132 1134
1133 1135 newstat = filecachesubentry.stat(self.path)
1134 1136
1135 1137 # we may not know if it's cacheable yet, check again now
1136 1138 if newstat and self._cacheable is None:
1137 1139 self._cacheable = newstat.cacheable()
1138 1140
1139 1141 # check again
1140 1142 if not self._cacheable:
1141 1143 return True
1142 1144
1143 1145 if self.cachestat != newstat:
1144 1146 self.cachestat = newstat
1145 1147 return True
1146 1148 else:
1147 1149 return False
1148 1150
1149 1151 @staticmethod
1150 1152 def stat(path):
1151 1153 try:
1152 1154 return util.cachestat(path)
1153 1155 except OSError as e:
1154 1156 if e.errno != errno.ENOENT:
1155 1157 raise
1156 1158
1157 1159 class filecacheentry(object):
1158 1160 def __init__(self, paths, stat=True):
1159 1161 self._entries = []
1160 1162 for path in paths:
1161 1163 self._entries.append(filecachesubentry(path, stat))
1162 1164
1163 1165 def changed(self):
1164 1166 '''true if any entry has changed'''
1165 1167 for entry in self._entries:
1166 1168 if entry.changed():
1167 1169 return True
1168 1170 return False
1169 1171
1170 1172 def refresh(self):
1171 1173 for entry in self._entries:
1172 1174 entry.refresh()
1173 1175
1174 1176 class filecache(object):
1175 1177 '''A property like decorator that tracks files under .hg/ for updates.
1176 1178
1177 1179 Records stat info when called in _filecache.
1178 1180
1179 1181 On subsequent calls, compares old stat info with new info, and recreates the
1180 1182 object when any of the files changes, updating the new stat info in
1181 1183 _filecache.
1182 1184
1183 1185 Mercurial either atomic renames or appends for files under .hg,
1184 1186 so to ensure the cache is reliable we need the filesystem to be able
1185 1187 to tell us if a file has been replaced. If it can't, we fallback to
1186 1188 recreating the object on every call (essentially the same behavior as
1187 1189 propertycache).
1188 1190
1189 1191 '''
1190 1192 def __init__(self, *paths):
1191 1193 self.paths = paths
1192 1194
1193 1195 def join(self, obj, fname):
1194 1196 """Used to compute the runtime path of a cached file.
1195 1197
1196 1198 Users should subclass filecache and provide their own version of this
1197 1199 function to call the appropriate join function on 'obj' (an instance
1198 1200 of the class that its member function was decorated).
1199 1201 """
1200 1202 return obj.join(fname)
1201 1203
1202 1204 def __call__(self, func):
1203 1205 self.func = func
1204 1206 self.name = func.__name__
1205 1207 return self
1206 1208
1207 1209 def __get__(self, obj, type=None):
1208 1210 # if accessed on the class, return the descriptor itself.
1209 1211 if obj is None:
1210 1212 return self
1211 1213 # do we need to check if the file changed?
1212 1214 if self.name in obj.__dict__:
1213 1215 assert self.name in obj._filecache, self.name
1214 1216 return obj.__dict__[self.name]
1215 1217
1216 1218 entry = obj._filecache.get(self.name)
1217 1219
1218 1220 if entry:
1219 1221 if entry.changed():
1220 1222 entry.obj = self.func(obj)
1221 1223 else:
1222 1224 paths = [self.join(obj, path) for path in self.paths]
1223 1225
1224 1226 # We stat -before- creating the object so our cache doesn't lie if
1225 1227 # a writer modified between the time we read and stat
1226 1228 entry = filecacheentry(paths, True)
1227 1229 entry.obj = self.func(obj)
1228 1230
1229 1231 obj._filecache[self.name] = entry
1230 1232
1231 1233 obj.__dict__[self.name] = entry.obj
1232 1234 return entry.obj
1233 1235
1234 1236 def __set__(self, obj, value):
1235 1237 if self.name not in obj._filecache:
1236 1238 # we add an entry for the missing value because X in __dict__
1237 1239 # implies X in _filecache
1238 1240 paths = [self.join(obj, path) for path in self.paths]
1239 1241 ce = filecacheentry(paths, False)
1240 1242 obj._filecache[self.name] = ce
1241 1243 else:
1242 1244 ce = obj._filecache[self.name]
1243 1245
1244 1246 ce.obj = value # update cached copy
1245 1247 obj.__dict__[self.name] = value # update copy returned by obj.x
1246 1248
1247 1249 def __delete__(self, obj):
1248 1250 try:
1249 1251 del obj.__dict__[self.name]
1250 1252 except KeyError:
1251 1253 raise AttributeError(self.name)
1252 1254
1253 1255 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1254 1256 if lock is None:
1255 1257 raise error.LockInheritanceContractViolation(
1256 1258 'lock can only be inherited while held')
1257 1259 if environ is None:
1258 1260 environ = {}
1259 1261 with lock.inherit() as locker:
1260 1262 environ[envvar] = locker
1261 1263 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1262 1264
1263 1265 def wlocksub(repo, cmd, *args, **kwargs):
1264 1266 """run cmd as a subprocess that allows inheriting repo's wlock
1265 1267
1266 1268 This can only be called while the wlock is held. This takes all the
1267 1269 arguments that ui.system does, and returns the exit code of the
1268 1270 subprocess."""
1269 1271 return _locksub(repo, repo.currentwlock(), 'HG_WLOCK_LOCKER', cmd, *args,
1270 1272 **kwargs)
1271 1273
1272 1274 def gdinitconfig(ui):
1273 1275 """helper function to know if a repo should be created as general delta
1274 1276 """
1275 1277 # experimental config: format.generaldelta
1276 1278 return (ui.configbool('format', 'generaldelta', False)
1277 1279 or ui.configbool('format', 'usegeneraldelta', True))
1278 1280
1279 1281 def gddeltaconfig(ui):
1280 1282 """helper function to know if incoming delta should be optimised
1281 1283 """
1282 1284 # experimental config: format.generaldelta
1283 1285 return ui.configbool('format', 'generaldelta', False)
1284 1286
1285 1287 class delayclosedfile(object):
1286 1288 """Proxy for a file object whose close is delayed.
1287 1289
1288 1290 Do not instantiate outside of the vfs layer.
1289 1291 """
1290 1292
1291 1293 def __init__(self, fh, closer):
1292 1294 object.__setattr__(self, '_origfh', fh)
1293 1295 object.__setattr__(self, '_closer', closer)
1294 1296
1295 1297 def __getattr__(self, attr):
1296 1298 return getattr(self._origfh, attr)
1297 1299
1298 1300 def __setattr__(self, attr, value):
1299 1301 return setattr(self._origfh, attr, value)
1300 1302
1301 1303 def __delattr__(self, attr):
1302 1304 return delattr(self._origfh, attr)
1303 1305
1304 1306 def __enter__(self):
1305 1307 return self._origfh.__enter__()
1306 1308
1307 1309 def __exit__(self, exc_type, exc_value, exc_tb):
1308 1310 self._closer.close(self._origfh)
1309 1311
1310 1312 def close(self):
1311 1313 self._closer.close(self._origfh)
1312 1314
1313 1315 class backgroundfilecloser(object):
1314 1316 """Coordinates background closing of file handles on multiple threads."""
1315 1317 def __init__(self, ui, expectedcount=-1):
1316 1318 self._running = False
1317 1319 self._entered = False
1318 1320 self._threads = []
1319 1321 self._threadexception = None
1320 1322
1321 1323 # Only Windows/NTFS has slow file closing. So only enable by default
1322 1324 # on that platform. But allow to be enabled elsewhere for testing.
1323 1325 defaultenabled = os.name == 'nt'
1324 1326 enabled = ui.configbool('worker', 'backgroundclose', defaultenabled)
1325 1327
1326 1328 if not enabled:
1327 1329 return
1328 1330
1329 1331 # There is overhead to starting and stopping the background threads.
1330 1332 # Don't do background processing unless the file count is large enough
1331 1333 # to justify it.
1332 1334 minfilecount = ui.configint('worker', 'backgroundcloseminfilecount',
1333 1335 2048)
1334 1336 # FUTURE dynamically start background threads after minfilecount closes.
1335 1337 # (We don't currently have any callers that don't know their file count)
1336 1338 if expectedcount > 0 and expectedcount < minfilecount:
1337 1339 return
1338 1340
1339 1341 # Windows defaults to a limit of 512 open files. A buffer of 128
1340 1342 # should give us enough headway.
1341 1343 maxqueue = ui.configint('worker', 'backgroundclosemaxqueue', 384)
1342 1344 threadcount = ui.configint('worker', 'backgroundclosethreadcount', 4)
1343 1345
1344 1346 ui.debug('starting %d threads for background file closing\n' %
1345 1347 threadcount)
1346 1348
1347 1349 self._queue = util.queue(maxsize=maxqueue)
1348 1350 self._running = True
1349 1351
1350 1352 for i in range(threadcount):
1351 1353 t = threading.Thread(target=self._worker, name='backgroundcloser')
1352 1354 self._threads.append(t)
1353 1355 t.start()
1354 1356
1355 1357 def __enter__(self):
1356 1358 self._entered = True
1357 1359 return self
1358 1360
1359 1361 def __exit__(self, exc_type, exc_value, exc_tb):
1360 1362 self._running = False
1361 1363
1362 1364 # Wait for threads to finish closing so open files don't linger for
1363 1365 # longer than lifetime of context manager.
1364 1366 for t in self._threads:
1365 1367 t.join()
1366 1368
1367 1369 def _worker(self):
1368 1370 """Main routine for worker thread."""
1369 1371 while True:
1370 1372 try:
1371 1373 fh = self._queue.get(block=True, timeout=0.100)
1372 1374 # Need to catch or the thread will terminate and
1373 1375 # we could orphan file descriptors.
1374 1376 try:
1375 1377 fh.close()
1376 1378 except Exception as e:
1377 1379 # Stash so can re-raise from main thread later.
1378 1380 self._threadexception = e
1379 1381 except util.empty:
1380 1382 if not self._running:
1381 1383 break
1382 1384
1383 1385 def close(self, fh):
1384 1386 """Schedule a file for closing."""
1385 1387 if not self._entered:
1386 raise error.Abort('can only call close() when context manager '
1387 'active')
1388 raise error.Abort(_('can only call close() when context manager '
1389 'active'))
1388 1390
1389 1391 # If a background thread encountered an exception, raise now so we fail
1390 1392 # fast. Otherwise we may potentially go on for minutes until the error
1391 1393 # is acted on.
1392 1394 if self._threadexception:
1393 1395 e = self._threadexception
1394 1396 self._threadexception = None
1395 1397 raise e
1396 1398
1397 1399 # If we're not actively running, close synchronously.
1398 1400 if not self._running:
1399 1401 fh.close()
1400 1402 return
1401 1403
1402 1404 self._queue.put(fh, block=True, timeout=None)
@@ -1,374 +1,374 b''
1 1 # sshpeer.py - ssh repository proxy class for mercurial
2 2 #
3 3 # Copyright 2005, 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 re
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 error,
15 15 util,
16 16 wireproto,
17 17 )
18 18
19 19 class remotelock(object):
20 20 def __init__(self, repo):
21 21 self.repo = repo
22 22 def release(self):
23 23 self.repo.unlock()
24 24 self.repo = None
25 25 def __enter__(self):
26 26 return self
27 27 def __exit__(self, exc_type, exc_val, exc_tb):
28 28 if self.repo:
29 29 self.release()
30 30 def __del__(self):
31 31 if self.repo:
32 32 self.release()
33 33
34 34 def _serverquote(s):
35 35 if not s:
36 36 return s
37 37 '''quote a string for the remote shell ... which we assume is sh'''
38 38 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
39 39 return s
40 40 return "'%s'" % s.replace("'", "'\\''")
41 41
42 42 def _forwardoutput(ui, pipe):
43 43 """display all data currently available on pipe as remote output.
44 44
45 45 This is non blocking."""
46 46 s = util.readpipe(pipe)
47 47 if s:
48 48 for l in s.splitlines():
49 49 ui.status(_("remote: "), l, '\n')
50 50
51 51 class doublepipe(object):
52 52 """Operate a side-channel pipe in addition of a main one
53 53
54 54 The side-channel pipe contains server output to be forwarded to the user
55 55 input. The double pipe will behave as the "main" pipe, but will ensure the
56 56 content of the "side" pipe is properly processed while we wait for blocking
57 57 call on the "main" pipe.
58 58
59 59 If large amounts of data are read from "main", the forward will cease after
60 60 the first bytes start to appear. This simplifies the implementation
61 61 without affecting actual output of sshpeer too much as we rarely issue
62 62 large read for data not yet emitted by the server.
63 63
64 64 The main pipe is expected to be a 'bufferedinputpipe' from the util module
65 65 that handle all the os specific bites. This class lives in this module
66 66 because it focus on behavior specific to the ssh protocol."""
67 67
68 68 def __init__(self, ui, main, side):
69 69 self._ui = ui
70 70 self._main = main
71 71 self._side = side
72 72
73 73 def _wait(self):
74 74 """wait until some data are available on main or side
75 75
76 76 return a pair of boolean (ismainready, issideready)
77 77
78 78 (This will only wait for data if the setup is supported by `util.poll`)
79 79 """
80 80 if getattr(self._main, 'hasbuffer', False): # getattr for classic pipe
81 81 return (True, True) # main has data, assume side is worth poking at.
82 82 fds = [self._main.fileno(), self._side.fileno()]
83 83 try:
84 84 act = util.poll(fds)
85 85 except NotImplementedError:
86 86 # non supported yet case, assume all have data.
87 87 act = fds
88 88 return (self._main.fileno() in act, self._side.fileno() in act)
89 89
90 90 def write(self, data):
91 91 return self._call('write', data)
92 92
93 93 def read(self, size):
94 94 return self._call('read', size)
95 95
96 96 def readline(self):
97 97 return self._call('readline')
98 98
99 99 def _call(self, methname, data=None):
100 100 """call <methname> on "main", forward output of "side" while blocking
101 101 """
102 102 # data can be '' or 0
103 103 if (data is not None and not data) or self._main.closed:
104 104 _forwardoutput(self._ui, self._side)
105 105 return ''
106 106 while True:
107 107 mainready, sideready = self._wait()
108 108 if sideready:
109 109 _forwardoutput(self._ui, self._side)
110 110 if mainready:
111 111 meth = getattr(self._main, methname)
112 112 if data is None:
113 113 return meth()
114 114 else:
115 115 return meth(data)
116 116
117 117 def close(self):
118 118 return self._main.close()
119 119
120 120 def flush(self):
121 121 return self._main.flush()
122 122
123 123 class sshpeer(wireproto.wirepeer):
124 124 def __init__(self, ui, path, create=False):
125 125 self._url = path
126 126 self.ui = ui
127 127 self.pipeo = self.pipei = self.pipee = None
128 128
129 129 u = util.url(path, parsequery=False, parsefragment=False)
130 130 if u.scheme != 'ssh' or not u.host or u.path is None:
131 131 self._abort(error.RepoError(_("couldn't parse location %s") % path))
132 132
133 133 self.user = u.user
134 134 if u.passwd is not None:
135 135 self._abort(error.RepoError(_("password in URL not supported")))
136 136 self.host = u.host
137 137 self.port = u.port
138 138 self.path = u.path or "."
139 139
140 140 sshcmd = self.ui.config("ui", "ssh", "ssh")
141 141 remotecmd = self.ui.config("ui", "remotecmd", "hg")
142 142
143 143 args = util.sshargs(sshcmd,
144 144 _serverquote(self.host),
145 145 _serverquote(self.user),
146 146 _serverquote(self.port))
147 147
148 148 if create:
149 149 cmd = '%s %s %s' % (sshcmd, args,
150 150 util.shellquote("%s init %s" %
151 151 (_serverquote(remotecmd), _serverquote(self.path))))
152 152 ui.debug('running %s\n' % cmd)
153 153 res = ui.system(cmd)
154 154 if res != 0:
155 155 self._abort(error.RepoError(_("could not create remote repo")))
156 156
157 157 self._validaterepo(sshcmd, args, remotecmd)
158 158
159 159 def url(self):
160 160 return self._url
161 161
162 162 def _validaterepo(self, sshcmd, args, remotecmd):
163 163 # cleanup up previous run
164 164 self.cleanup()
165 165
166 166 cmd = '%s %s %s' % (sshcmd, args,
167 167 util.shellquote("%s -R %s serve --stdio" %
168 168 (_serverquote(remotecmd), _serverquote(self.path))))
169 169 self.ui.debug('running %s\n' % cmd)
170 170 cmd = util.quotecommand(cmd)
171 171
172 172 # while self.subprocess isn't used, having it allows the subprocess to
173 173 # to clean up correctly later
174 174 #
175 175 # no buffer allow the use of 'select'
176 176 # feel free to remove buffering and select usage when we ultimately
177 177 # move to threading.
178 178 sub = util.popen4(cmd, bufsize=0)
179 179 self.pipeo, self.pipei, self.pipee, self.subprocess = sub
180 180
181 181 self.pipei = util.bufferedinputpipe(self.pipei)
182 182 self.pipei = doublepipe(self.ui, self.pipei, self.pipee)
183 183 self.pipeo = doublepipe(self.ui, self.pipeo, self.pipee)
184 184
185 185 # skip any noise generated by remote shell
186 186 self._callstream("hello")
187 187 r = self._callstream("between", pairs=("%s-%s" % ("0"*40, "0"*40)))
188 188 lines = ["", "dummy"]
189 189 max_noise = 500
190 190 while lines[-1] and max_noise:
191 191 l = r.readline()
192 192 self.readerr()
193 193 if lines[-1] == "1\n" and l == "\n":
194 194 break
195 195 if l:
196 196 self.ui.debug("remote: ", l)
197 197 lines.append(l)
198 198 max_noise -= 1
199 199 else:
200 200 self._abort(error.RepoError(_('no suitable response from '
201 201 'remote hg')))
202 202
203 203 self._caps = set()
204 204 for l in reversed(lines):
205 205 if l.startswith("capabilities:"):
206 206 self._caps.update(l[:-1].split(":")[1].split())
207 207 break
208 208
209 209 def _capabilities(self):
210 210 return self._caps
211 211
212 212 def readerr(self):
213 213 _forwardoutput(self.ui, self.pipee)
214 214
215 215 def _abort(self, exception):
216 216 self.cleanup()
217 217 raise exception
218 218
219 219 def cleanup(self):
220 220 if self.pipeo is None:
221 221 return
222 222 self.pipeo.close()
223 223 self.pipei.close()
224 224 try:
225 225 # read the error descriptor until EOF
226 226 for l in self.pipee:
227 227 self.ui.status(_("remote: "), l)
228 228 except (IOError, ValueError):
229 229 pass
230 230 self.pipee.close()
231 231
232 232 __del__ = cleanup
233 233
234 234 def _submitbatch(self, req):
235 235 cmds = []
236 236 for op, argsdict in req:
237 237 args = ','.join('%s=%s' % (wireproto.escapearg(k),
238 238 wireproto.escapearg(v))
239 239 for k, v in argsdict.iteritems())
240 240 cmds.append('%s %s' % (op, args))
241 241 rsp = self._callstream("batch", cmds=';'.join(cmds))
242 242 available = self._getamount()
243 243 # TODO this response parsing is probably suboptimal for large
244 244 # batches with large responses.
245 245 toread = min(available, 1024)
246 246 work = rsp.read(toread)
247 247 available -= toread
248 248 chunk = work
249 249 while chunk:
250 250 while ';' in work:
251 251 one, work = work.split(';', 1)
252 252 yield wireproto.unescapearg(one)
253 253 toread = min(available, 1024)
254 254 chunk = rsp.read(toread)
255 255 available -= toread
256 256 work += chunk
257 257 yield wireproto.unescapearg(work)
258 258
259 259 def _callstream(self, cmd, **args):
260 260 self.ui.debug("sending %s command\n" % cmd)
261 261 self.pipeo.write("%s\n" % cmd)
262 262 _func, names = wireproto.commands[cmd]
263 263 keys = names.split()
264 264 wireargs = {}
265 265 for k in keys:
266 266 if k == '*':
267 267 wireargs['*'] = args
268 268 break
269 269 else:
270 270 wireargs[k] = args[k]
271 271 del args[k]
272 272 for k, v in sorted(wireargs.iteritems()):
273 273 self.pipeo.write("%s %d\n" % (k, len(v)))
274 274 if isinstance(v, dict):
275 275 for dk, dv in v.iteritems():
276 276 self.pipeo.write("%s %d\n" % (dk, len(dv)))
277 277 self.pipeo.write(dv)
278 278 else:
279 279 self.pipeo.write(v)
280 280 self.pipeo.flush()
281 281
282 282 return self.pipei
283 283
284 284 def _callcompressable(self, cmd, **args):
285 285 return self._callstream(cmd, **args)
286 286
287 287 def _call(self, cmd, **args):
288 288 self._callstream(cmd, **args)
289 289 return self._recv()
290 290
291 291 def _callpush(self, cmd, fp, **args):
292 292 r = self._call(cmd, **args)
293 293 if r:
294 294 return '', r
295 295 while True:
296 296 d = fp.read(4096)
297 297 if not d:
298 298 break
299 299 self._send(d)
300 300 self._send("", flush=True)
301 301 r = self._recv()
302 302 if r:
303 303 return '', r
304 304 return self._recv(), ''
305 305
306 306 def _calltwowaystream(self, cmd, fp, **args):
307 307 r = self._call(cmd, **args)
308 308 if r:
309 309 # XXX needs to be made better
310 raise error.Abort('unexpected remote reply: %s' % r)
310 raise error.Abort(_('unexpected remote reply: %s') % r)
311 311 while True:
312 312 d = fp.read(4096)
313 313 if not d:
314 314 break
315 315 self._send(d)
316 316 self._send("", flush=True)
317 317 return self.pipei
318 318
319 319 def _getamount(self):
320 320 l = self.pipei.readline()
321 321 if l == '\n':
322 322 self.readerr()
323 323 msg = _('check previous remote output')
324 324 self._abort(error.OutOfBandError(hint=msg))
325 325 self.readerr()
326 326 try:
327 327 return int(l)
328 328 except ValueError:
329 329 self._abort(error.ResponseError(_("unexpected response:"), l))
330 330
331 331 def _recv(self):
332 332 return self.pipei.read(self._getamount())
333 333
334 334 def _send(self, data, flush=False):
335 335 self.pipeo.write("%d\n" % len(data))
336 336 if data:
337 337 self.pipeo.write(data)
338 338 if flush:
339 339 self.pipeo.flush()
340 340 self.readerr()
341 341
342 342 def lock(self):
343 343 self._call("lock")
344 344 return remotelock(self)
345 345
346 346 def unlock(self):
347 347 self._call("unlock")
348 348
349 349 def addchangegroup(self, cg, source, url, lock=None):
350 350 '''Send a changegroup to the remote server. Return an integer
351 351 similar to unbundle(). DEPRECATED, since it requires locking the
352 352 remote.'''
353 353 d = self._call("addchangegroup")
354 354 if d:
355 355 self._abort(error.RepoError(_("push refused: %s") % d))
356 356 while True:
357 357 d = cg.read(4096)
358 358 if not d:
359 359 break
360 360 self.pipeo.write(d)
361 361 self.readerr()
362 362
363 363 self.pipeo.flush()
364 364
365 365 self.readerr()
366 366 r = self._recv()
367 367 if not r:
368 368 return 1
369 369 try:
370 370 return int(r)
371 371 except ValueError:
372 372 self._abort(error.ResponseError(_("unexpected response:"), r))
373 373
374 374 instance = sshpeer
@@ -1,134 +1,135 b''
1 1 # sshserver.py - ssh protocol server support for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import os
12 12 import sys
13 13
14 from .i18n import _
14 15 from . import (
15 16 error,
16 17 hook,
17 18 util,
18 19 wireproto,
19 20 )
20 21
21 22 class sshserver(wireproto.abstractserverproto):
22 23 def __init__(self, ui, repo):
23 24 self.ui = ui
24 25 self.repo = repo
25 26 self.lock = None
26 27 self.fin = ui.fin
27 28 self.fout = ui.fout
28 29
29 30 hook.redirect(True)
30 31 ui.fout = repo.ui.fout = ui.ferr
31 32
32 33 # Prevent insertion/deletion of CRs
33 34 util.setbinary(self.fin)
34 35 util.setbinary(self.fout)
35 36
36 37 def getargs(self, args):
37 38 data = {}
38 39 keys = args.split()
39 40 for n in xrange(len(keys)):
40 41 argline = self.fin.readline()[:-1]
41 42 arg, l = argline.split()
42 43 if arg not in keys:
43 raise error.Abort("unexpected parameter %r" % arg)
44 raise error.Abort(_("unexpected parameter %r") % arg)
44 45 if arg == '*':
45 46 star = {}
46 47 for k in xrange(int(l)):
47 48 argline = self.fin.readline()[:-1]
48 49 arg, l = argline.split()
49 50 val = self.fin.read(int(l))
50 51 star[arg] = val
51 52 data['*'] = star
52 53 else:
53 54 val = self.fin.read(int(l))
54 55 data[arg] = val
55 56 return [data[k] for k in keys]
56 57
57 58 def getarg(self, name):
58 59 return self.getargs(name)[0]
59 60
60 61 def getfile(self, fpout):
61 62 self.sendresponse('')
62 63 count = int(self.fin.readline())
63 64 while count:
64 65 fpout.write(self.fin.read(count))
65 66 count = int(self.fin.readline())
66 67
67 68 def redirect(self):
68 69 pass
69 70
70 71 def groupchunks(self, changegroup):
71 72 while True:
72 73 d = changegroup.read(4096)
73 74 if not d:
74 75 break
75 76 yield d
76 77
77 78 def sendresponse(self, v):
78 79 self.fout.write("%d\n" % len(v))
79 80 self.fout.write(v)
80 81 self.fout.flush()
81 82
82 83 def sendstream(self, source):
83 84 write = self.fout.write
84 85 for chunk in source.gen:
85 86 write(chunk)
86 87 self.fout.flush()
87 88
88 89 def sendpushresponse(self, rsp):
89 90 self.sendresponse('')
90 91 self.sendresponse(str(rsp.res))
91 92
92 93 def sendpusherror(self, rsp):
93 94 self.sendresponse(rsp.res)
94 95
95 96 def sendooberror(self, rsp):
96 97 self.ui.ferr.write('%s\n-\n' % rsp.message)
97 98 self.ui.ferr.flush()
98 99 self.fout.write('\n')
99 100 self.fout.flush()
100 101
101 102 def serve_forever(self):
102 103 try:
103 104 while self.serve_one():
104 105 pass
105 106 finally:
106 107 if self.lock is not None:
107 108 self.lock.release()
108 109 sys.exit(0)
109 110
110 111 handlers = {
111 112 str: sendresponse,
112 113 wireproto.streamres: sendstream,
113 114 wireproto.pushres: sendpushresponse,
114 115 wireproto.pusherr: sendpusherror,
115 116 wireproto.ooberror: sendooberror,
116 117 }
117 118
118 119 def serve_one(self):
119 120 cmd = self.fin.readline()[:-1]
120 121 if cmd and cmd in wireproto.commands:
121 122 rsp = wireproto.dispatch(self.repo, self, cmd)
122 123 self.handlers[rsp.__class__](self, rsp)
123 124 elif cmd:
124 125 impl = getattr(self, 'do_' + cmd, None)
125 126 if impl:
126 127 r = impl()
127 128 if r is not None:
128 129 self.sendresponse(r)
129 130 else: self.sendresponse("")
130 131 return cmd != ''
131 132
132 133 def _client(self):
133 134 client = os.environ.get('SSH_CLIENT', '').split(' ', 1)[0]
134 135 return 'remote:ssh:' + client
@@ -1,432 +1,432 b''
1 1 # sslutil.py - SSL handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import hashlib
13 13 import os
14 14 import ssl
15 15 import sys
16 16
17 17 from .i18n import _
18 18 from . import (
19 19 error,
20 20 util,
21 21 )
22 22
23 23 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
24 24 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
25 25 # all exposed via the "ssl" module.
26 26 #
27 27 # Depending on the version of Python being used, SSL/TLS support is either
28 28 # modern/secure or legacy/insecure. Many operations in this module have
29 29 # separate code paths depending on support in Python.
30 30
31 31 hassni = getattr(ssl, 'HAS_SNI', False)
32 32
33 33 try:
34 34 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
35 35 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
36 36 except AttributeError:
37 37 OP_NO_SSLv2 = 0x1000000
38 38 OP_NO_SSLv3 = 0x2000000
39 39
40 40 try:
41 41 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
42 42 # SSL/TLS features are available.
43 43 SSLContext = ssl.SSLContext
44 44 modernssl = True
45 45 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
46 46 except AttributeError:
47 47 modernssl = False
48 48 _canloaddefaultcerts = False
49 49
50 50 # We implement SSLContext using the interface from the standard library.
51 51 class SSLContext(object):
52 52 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
53 53 _supportsciphers = sys.version_info >= (2, 7)
54 54
55 55 def __init__(self, protocol):
56 56 # From the public interface of SSLContext
57 57 self.protocol = protocol
58 58 self.check_hostname = False
59 59 self.options = 0
60 60 self.verify_mode = ssl.CERT_NONE
61 61
62 62 # Used by our implementation.
63 63 self._certfile = None
64 64 self._keyfile = None
65 65 self._certpassword = None
66 66 self._cacerts = None
67 67 self._ciphers = None
68 68
69 69 def load_cert_chain(self, certfile, keyfile=None, password=None):
70 70 self._certfile = certfile
71 71 self._keyfile = keyfile
72 72 self._certpassword = password
73 73
74 74 def load_default_certs(self, purpose=None):
75 75 pass
76 76
77 77 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
78 78 if capath:
79 raise error.Abort('capath not supported')
79 raise error.Abort(_('capath not supported'))
80 80 if cadata:
81 raise error.Abort('cadata not supported')
81 raise error.Abort(_('cadata not supported'))
82 82
83 83 self._cacerts = cafile
84 84
85 85 def set_ciphers(self, ciphers):
86 86 if not self._supportsciphers:
87 raise error.Abort('setting ciphers not supported')
87 raise error.Abort(_('setting ciphers not supported'))
88 88
89 89 self._ciphers = ciphers
90 90
91 91 def wrap_socket(self, socket, server_hostname=None, server_side=False):
92 92 # server_hostname is unique to SSLContext.wrap_socket and is used
93 93 # for SNI in that context. So there's nothing for us to do with it
94 94 # in this legacy code since we don't support SNI.
95 95
96 96 args = {
97 97 'keyfile': self._keyfile,
98 98 'certfile': self._certfile,
99 99 'server_side': server_side,
100 100 'cert_reqs': self.verify_mode,
101 101 'ssl_version': self.protocol,
102 102 'ca_certs': self._cacerts,
103 103 }
104 104
105 105 if self._supportsciphers:
106 106 args['ciphers'] = self._ciphers
107 107
108 108 return ssl.wrap_socket(socket, **args)
109 109
110 110 def _hostsettings(ui, hostname):
111 111 """Obtain security settings for a hostname.
112 112
113 113 Returns a dict of settings relevant to that hostname.
114 114 """
115 115 s = {
116 116 # Whether we should attempt to load default/available CA certs
117 117 # if an explicit ``cafile`` is not defined.
118 118 'allowloaddefaultcerts': True,
119 119 # List of 2-tuple of (hash algorithm, hash).
120 120 'certfingerprints': [],
121 121 # Path to file containing concatenated CA certs. Used by
122 122 # SSLContext.load_verify_locations().
123 123 'cafile': None,
124 124 # Whether certificate verification should be disabled.
125 125 'disablecertverification': False,
126 126 # Whether the legacy [hostfingerprints] section has data for this host.
127 127 'legacyfingerprint': False,
128 128 # ssl.CERT_* constant used by SSLContext.verify_mode.
129 129 'verifymode': None,
130 130 }
131 131
132 132 # Look for fingerprints in [hostsecurity] section. Value is a list
133 133 # of <alg>:<fingerprint> strings.
134 134 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
135 135 [])
136 136 for fingerprint in fingerprints:
137 137 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
138 138 raise error.Abort(_('invalid fingerprint for %s: %s') % (
139 139 hostname, fingerprint),
140 140 hint=_('must begin with "sha1:", "sha256:", '
141 141 'or "sha512:"'))
142 142
143 143 alg, fingerprint = fingerprint.split(':', 1)
144 144 fingerprint = fingerprint.replace(':', '').lower()
145 145 s['certfingerprints'].append((alg, fingerprint))
146 146
147 147 # Fingerprints from [hostfingerprints] are always SHA-1.
148 148 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
149 149 fingerprint = fingerprint.replace(':', '').lower()
150 150 s['certfingerprints'].append(('sha1', fingerprint))
151 151 s['legacyfingerprint'] = True
152 152
153 153 # If a host cert fingerprint is defined, it is the only thing that
154 154 # matters. No need to validate CA certs.
155 155 if s['certfingerprints']:
156 156 s['verifymode'] = ssl.CERT_NONE
157 157
158 158 # If --insecure is used, don't take CAs into consideration.
159 159 elif ui.insecureconnections:
160 160 s['disablecertverification'] = True
161 161 s['verifymode'] = ssl.CERT_NONE
162 162
163 163 if ui.configbool('devel', 'disableloaddefaultcerts'):
164 164 s['allowloaddefaultcerts'] = False
165 165
166 166 # If both fingerprints and a per-host ca file are specified, issue a warning
167 167 # because users should not be surprised about what security is or isn't
168 168 # being performed.
169 169 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
170 170 if s['certfingerprints'] and cafile:
171 171 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
172 172 'fingerprints defined; using host fingerprints for '
173 173 'verification)\n') % hostname)
174 174
175 175 # Try to hook up CA certificate validation unless something above
176 176 # makes it not necessary.
177 177 if s['verifymode'] is None:
178 178 # Look at per-host ca file first.
179 179 if cafile:
180 180 cafile = util.expandpath(cafile)
181 181 if not os.path.exists(cafile):
182 182 raise error.Abort(_('path specified by %s does not exist: %s') %
183 183 ('hostsecurity.%s:verifycertsfile' % hostname,
184 184 cafile))
185 185 s['cafile'] = cafile
186 186 else:
187 187 # Find global certificates file in config.
188 188 cafile = ui.config('web', 'cacerts')
189 189
190 190 if cafile:
191 191 cafile = util.expandpath(cafile)
192 192 if not os.path.exists(cafile):
193 193 raise error.Abort(_('could not find web.cacerts: %s') %
194 194 cafile)
195 195 else:
196 196 # No global CA certs. See if we can load defaults.
197 197 cafile = _defaultcacerts()
198 198 if cafile:
199 199 ui.debug('using %s to enable OS X system CA\n' % cafile)
200 200
201 201 s['cafile'] = cafile
202 202
203 203 # Require certificate validation if CA certs are being loaded and
204 204 # verification hasn't been disabled above.
205 205 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
206 206 s['verifymode'] = ssl.CERT_REQUIRED
207 207 else:
208 208 # At this point we don't have a fingerprint, aren't being
209 209 # explicitly insecure, and can't load CA certs. Connecting
210 210 # at this point is insecure. But we do it for BC reasons.
211 211 # TODO abort here to make secure by default.
212 212 s['verifymode'] = ssl.CERT_NONE
213 213
214 214 assert s['verifymode'] is not None
215 215
216 216 return s
217 217
218 218 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
219 219 """Add SSL/TLS to a socket.
220 220
221 221 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
222 222 choices based on what security options are available.
223 223
224 224 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
225 225 the following additional arguments:
226 226
227 227 * serverhostname - The expected hostname of the remote server. If the
228 228 server (and client) support SNI, this tells the server which certificate
229 229 to use.
230 230 """
231 231 if not serverhostname:
232 raise error.Abort('serverhostname argument is required')
232 raise error.Abort(_('serverhostname argument is required'))
233 233
234 234 settings = _hostsettings(ui, serverhostname)
235 235
236 236 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
237 237 # that both ends support, including TLS protocols. On legacy stacks,
238 238 # the highest it likely goes in TLS 1.0. On modern stacks, it can
239 239 # support TLS 1.2.
240 240 #
241 241 # The PROTOCOL_TLSv* constants select a specific TLS version
242 242 # only (as opposed to multiple versions). So the method for
243 243 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
244 244 # disable protocols via SSLContext.options and OP_NO_* constants.
245 245 # However, SSLContext.options doesn't work unless we have the
246 246 # full/real SSLContext available to us.
247 247 #
248 248 # SSLv2 and SSLv3 are broken. We ban them outright.
249 249 if modernssl:
250 250 protocol = ssl.PROTOCOL_SSLv23
251 251 else:
252 252 protocol = ssl.PROTOCOL_TLSv1
253 253
254 254 # TODO use ssl.create_default_context() on modernssl.
255 255 sslcontext = SSLContext(protocol)
256 256
257 257 # This is a no-op on old Python.
258 258 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
259 259
260 260 # This still works on our fake SSLContext.
261 261 sslcontext.verify_mode = settings['verifymode']
262 262
263 263 if certfile is not None:
264 264 def password():
265 265 f = keyfile or certfile
266 266 return ui.getpass(_('passphrase for %s: ') % f, '')
267 267 sslcontext.load_cert_chain(certfile, keyfile, password)
268 268
269 269 if settings['cafile'] is not None:
270 270 sslcontext.load_verify_locations(cafile=settings['cafile'])
271 271 caloaded = True
272 272 elif settings['allowloaddefaultcerts']:
273 273 # This is a no-op on old Python.
274 274 sslcontext.load_default_certs()
275 275 caloaded = True
276 276 else:
277 277 caloaded = False
278 278
279 279 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
280 280 # check if wrap_socket failed silently because socket had been
281 281 # closed
282 282 # - see http://bugs.python.org/issue13721
283 283 if not sslsocket.cipher():
284 284 raise error.Abort(_('ssl connection failed'))
285 285
286 286 sslsocket._hgstate = {
287 287 'caloaded': caloaded,
288 288 'hostname': serverhostname,
289 289 'settings': settings,
290 290 'ui': ui,
291 291 }
292 292
293 293 return sslsocket
294 294
295 295 def _verifycert(cert, hostname):
296 296 '''Verify that cert (in socket.getpeercert() format) matches hostname.
297 297 CRLs is not handled.
298 298
299 299 Returns error message if any problems are found and None on success.
300 300 '''
301 301 if not cert:
302 302 return _('no certificate received')
303 303 dnsname = hostname.lower()
304 304 def matchdnsname(certname):
305 305 return (certname == dnsname or
306 306 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
307 307
308 308 san = cert.get('subjectAltName', [])
309 309 if san:
310 310 certnames = [value.lower() for key, value in san if key == 'DNS']
311 311 for name in certnames:
312 312 if matchdnsname(name):
313 313 return None
314 314 if certnames:
315 315 return _('certificate is for %s') % ', '.join(certnames)
316 316
317 317 # subject is only checked when subjectAltName is empty
318 318 for s in cert.get('subject', []):
319 319 key, value = s[0]
320 320 if key == 'commonName':
321 321 try:
322 322 # 'subject' entries are unicode
323 323 certname = value.lower().encode('ascii')
324 324 except UnicodeEncodeError:
325 325 return _('IDN in certificate not supported')
326 326 if matchdnsname(certname):
327 327 return None
328 328 return _('certificate is for %s') % certname
329 329 return _('no commonName or subjectAltName found in certificate')
330 330
331 331
332 332 # CERT_REQUIRED means fetch the cert from the server all the time AND
333 333 # validate it against the CA store provided in web.cacerts.
334 334
335 335 def _plainapplepython():
336 336 """return true if this seems to be a pure Apple Python that
337 337 * is unfrozen and presumably has the whole mercurial module in the file
338 338 system
339 339 * presumably is an Apple Python that uses Apple OpenSSL which has patches
340 340 for using system certificate store CAs in addition to the provided
341 341 cacerts file
342 342 """
343 343 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
344 344 return False
345 345 exe = os.path.realpath(sys.executable).lower()
346 346 return (exe.startswith('/usr/bin/python') or
347 347 exe.startswith('/system/library/frameworks/python.framework/'))
348 348
349 349 def _defaultcacerts():
350 350 """return path to default CA certificates or None."""
351 351 if _plainapplepython():
352 352 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
353 353 if os.path.exists(dummycert):
354 354 return dummycert
355 355
356 356 return None
357 357
358 358 def validatesocket(sock):
359 359 """Validate a socket meets security requiremnets.
360 360
361 361 The passed socket must have been created with ``wrapsocket()``.
362 362 """
363 363 host = sock._hgstate['hostname']
364 364 ui = sock._hgstate['ui']
365 365 settings = sock._hgstate['settings']
366 366
367 367 try:
368 368 peercert = sock.getpeercert(True)
369 369 peercert2 = sock.getpeercert()
370 370 except AttributeError:
371 371 raise error.Abort(_('%s ssl connection error') % host)
372 372
373 373 if not peercert:
374 374 raise error.Abort(_('%s certificate error: '
375 375 'no certificate received') % host)
376 376
377 377 if settings['disablecertverification']:
378 378 # We don't print the certificate fingerprint because it shouldn't
379 379 # be necessary: if the user requested certificate verification be
380 380 # disabled, they presumably already saw a message about the inability
381 381 # to verify the certificate and this message would have printed the
382 382 # fingerprint. So printing the fingerprint here adds little to no
383 383 # value.
384 384 ui.warn(_('warning: connection security to %s is disabled per current '
385 385 'settings; communication is susceptible to eavesdropping '
386 386 'and tampering\n') % host)
387 387 return
388 388
389 389 # If a certificate fingerprint is pinned, use it and only it to
390 390 # validate the remote cert.
391 391 peerfingerprints = {
392 392 'sha1': hashlib.sha1(peercert).hexdigest(),
393 393 'sha256': hashlib.sha256(peercert).hexdigest(),
394 394 'sha512': hashlib.sha512(peercert).hexdigest(),
395 395 }
396 396
397 397 def fmtfingerprint(s):
398 398 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
399 399
400 400 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
401 401
402 402 if settings['certfingerprints']:
403 403 for hash, fingerprint in settings['certfingerprints']:
404 404 if peerfingerprints[hash].lower() == fingerprint:
405 405 ui.debug('%s certificate matched fingerprint %s:%s\n' %
406 406 (host, hash, fmtfingerprint(fingerprint)))
407 407 return
408 408
409 409 # Pinned fingerprint didn't match. This is a fatal error.
410 410 if settings['legacyfingerprint']:
411 411 section = 'hostfingerprint'
412 412 nice = fmtfingerprint(peerfingerprints['sha1'])
413 413 else:
414 414 section = 'hostsecurity'
415 415 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
416 416 raise error.Abort(_('certificate for %s has unexpected '
417 417 'fingerprint %s') % (host, nice),
418 418 hint=_('check %s configuration') % section)
419 419
420 420 if not sock._hgstate['caloaded']:
421 421 ui.warn(_('warning: certificate for %s not verified '
422 422 '(set hostsecurity.%s:certfingerprints=%s or web.cacerts '
423 423 'config settings)\n') % (host, host, nicefingerprint))
424 424 return
425 425
426 426 msg = _verifycert(peercert2, host)
427 427 if msg:
428 428 raise error.Abort(_('%s certificate error: %s') % (host, msg),
429 429 hint=_('set hostsecurity.%s:certfingerprints=%s '
430 430 'config setting or use --insecure to connect '
431 431 'insecurely') %
432 432 (host, nicefingerprint))
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now