##// END OF EJS Templates
hgweb: make templater mostly compatible with log templates...
Yuya Nishihara -
r36535:7937850a default
parent child Browse files
Show More
@@ -1,481 +1,484 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import contextlib
12 12 import os
13 13
14 14 from .common import (
15 15 ErrorResponse,
16 16 HTTP_BAD_REQUEST,
17 17 HTTP_NOT_FOUND,
18 18 HTTP_NOT_MODIFIED,
19 19 HTTP_OK,
20 20 HTTP_SERVER_ERROR,
21 21 caching,
22 22 cspvalues,
23 23 permhooks,
24 24 )
25 25 from .request import wsgirequest
26 26
27 27 from .. import (
28 28 encoding,
29 29 error,
30 formatter,
30 31 hg,
31 32 hook,
32 33 profiling,
33 34 pycompat,
34 35 repoview,
35 36 templatefilters,
36 37 templater,
37 38 ui as uimod,
38 39 util,
39 40 wireprotoserver,
40 41 )
41 42
42 43 from . import (
43 44 webcommands,
44 45 webutil,
45 46 wsgicgi,
46 47 )
47 48
48 49 perms = {
49 50 'changegroup': 'pull',
50 51 'changegroupsubset': 'pull',
51 52 'getbundle': 'pull',
52 53 'stream_out': 'pull',
53 54 'listkeys': 'pull',
54 55 'unbundle': 'push',
55 56 'pushkey': 'push',
56 57 }
57 58
58 59 archivespecs = util.sortdict((
59 60 ('zip', ('application/zip', 'zip', '.zip', None)),
60 61 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
61 62 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
62 63 ))
63 64
64 65 def getstyle(req, configfn, templatepath):
65 66 fromreq = req.form.get('style', [None])[0]
66 67 styles = (
67 68 fromreq,
68 69 configfn('web', 'style'),
69 70 'paper',
70 71 )
71 72 return styles, templater.stylemap(styles, templatepath)
72 73
73 74 def makebreadcrumb(url, prefix=''):
74 75 '''Return a 'URL breadcrumb' list
75 76
76 77 A 'URL breadcrumb' is a list of URL-name pairs,
77 78 corresponding to each of the path items on a URL.
78 79 This can be used to create path navigation entries.
79 80 '''
80 81 if url.endswith('/'):
81 82 url = url[:-1]
82 83 if prefix:
83 84 url = '/' + prefix + url
84 85 relpath = url
85 86 if relpath.startswith('/'):
86 87 relpath = relpath[1:]
87 88
88 89 breadcrumb = []
89 90 urlel = url
90 91 pathitems = [''] + relpath.split('/')
91 92 for pathel in reversed(pathitems):
92 93 if not pathel or not urlel:
93 94 break
94 95 breadcrumb.append({'url': urlel, 'name': pathel})
95 96 urlel = os.path.dirname(urlel)
96 97 return reversed(breadcrumb)
97 98
98 99 class requestcontext(object):
99 100 """Holds state/context for an individual request.
100 101
101 102 Servers can be multi-threaded. Holding state on the WSGI application
102 103 is prone to race conditions. Instances of this class exist to hold
103 104 mutable and race-free state for requests.
104 105 """
105 106 def __init__(self, app, repo):
106 107 self.repo = repo
107 108 self.reponame = app.reponame
108 109
109 110 self.archivespecs = archivespecs
110 111
111 112 self.maxchanges = self.configint('web', 'maxchanges')
112 113 self.stripecount = self.configint('web', 'stripes')
113 114 self.maxshortchanges = self.configint('web', 'maxshortchanges')
114 115 self.maxfiles = self.configint('web', 'maxfiles')
115 116 self.allowpull = self.configbool('web', 'allow-pull')
116 117
117 118 # we use untrusted=False to prevent a repo owner from using
118 119 # web.templates in .hg/hgrc to get access to any file readable
119 120 # by the user running the CGI script
120 121 self.templatepath = self.config('web', 'templates', untrusted=False)
121 122
122 123 # This object is more expensive to build than simple config values.
123 124 # It is shared across requests. The app will replace the object
124 125 # if it is updated. Since this is a reference and nothing should
125 126 # modify the underlying object, it should be constant for the lifetime
126 127 # of the request.
127 128 self.websubtable = app.websubtable
128 129
129 130 self.csp, self.nonce = cspvalues(self.repo.ui)
130 131
131 132 # Trust the settings from the .hg/hgrc files by default.
132 133 def config(self, section, name, default=uimod._unset, untrusted=True):
133 134 return self.repo.ui.config(section, name, default,
134 135 untrusted=untrusted)
135 136
136 137 def configbool(self, section, name, default=uimod._unset, untrusted=True):
137 138 return self.repo.ui.configbool(section, name, default,
138 139 untrusted=untrusted)
139 140
140 141 def configint(self, section, name, default=uimod._unset, untrusted=True):
141 142 return self.repo.ui.configint(section, name, default,
142 143 untrusted=untrusted)
143 144
144 145 def configlist(self, section, name, default=uimod._unset, untrusted=True):
145 146 return self.repo.ui.configlist(section, name, default,
146 147 untrusted=untrusted)
147 148
148 149 def archivelist(self, nodeid):
149 150 allowed = self.configlist('web', 'allow_archive')
150 151 for typ, spec in self.archivespecs.iteritems():
151 152 if typ in allowed or self.configbool('web', 'allow%s' % typ):
152 153 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
153 154
154 155 def templater(self, req):
155 156 # determine scheme, port and server name
156 157 # this is needed to create absolute urls
157 158
158 159 proto = req.env.get('wsgi.url_scheme')
159 160 if proto == 'https':
160 161 proto = 'https'
161 162 default_port = '443'
162 163 else:
163 164 proto = 'http'
164 165 default_port = '80'
165 166
166 167 port = req.env[r'SERVER_PORT']
167 168 port = port != default_port and (r':' + port) or r''
168 169 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port)
169 170 logourl = self.config('web', 'logourl')
170 171 logoimg = self.config('web', 'logoimg')
171 172 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
172 173 if not staticurl.endswith('/'):
173 174 staticurl += '/'
174 175
175 176 # some functions for the templater
176 177
177 178 def motd(**map):
178 179 yield self.config('web', 'motd')
179 180
180 181 # figure out which style to use
181 182
182 183 vars = {}
183 184 styles, (style, mapfile) = getstyle(req, self.config,
184 185 self.templatepath)
185 186 if style == styles[0]:
186 187 vars['style'] = style
187 188
188 189 start = '&' if req.url[-1] == r'?' else '?'
189 190 sessionvars = webutil.sessionvars(vars, start)
190 191
191 192 if not self.reponame:
192 193 self.reponame = (self.config('web', 'name', '')
193 194 or req.env.get('REPO_NAME')
194 195 or req.url.strip('/') or self.repo.root)
195 196
196 197 def websubfilter(text):
197 198 return templatefilters.websub(text, self.websubtable)
198 199
199 200 # create the templater
200
201 # TODO: export all keywords: defaults = templatekw.keywords.copy()
201 202 defaults = {
202 203 'url': req.url,
203 204 'logourl': logourl,
204 205 'logoimg': logoimg,
205 206 'staticurl': staticurl,
206 207 'urlbase': urlbase,
207 208 'repo': self.reponame,
208 209 'encoding': encoding.encoding,
209 210 'motd': motd,
210 211 'sessionvars': sessionvars,
211 212 'pathdef': makebreadcrumb(req.url),
212 213 'style': style,
213 214 'nonce': self.nonce,
214 215 }
216 tres = formatter.templateresources(self.repo.ui, self.repo)
215 217 tmpl = templater.templater.frommapfile(mapfile,
216 218 filters={'websub': websubfilter},
217 defaults=defaults)
219 defaults=defaults,
220 resources=tres)
218 221 return tmpl
219 222
220 223
221 224 class hgweb(object):
222 225 """HTTP server for individual repositories.
223 226
224 227 Instances of this class serve HTTP responses for a particular
225 228 repository.
226 229
227 230 Instances are typically used as WSGI applications.
228 231
229 232 Some servers are multi-threaded. On these servers, there may
230 233 be multiple active threads inside __call__.
231 234 """
232 235 def __init__(self, repo, name=None, baseui=None):
233 236 if isinstance(repo, str):
234 237 if baseui:
235 238 u = baseui.copy()
236 239 else:
237 240 u = uimod.ui.load()
238 241 r = hg.repository(u, repo)
239 242 else:
240 243 # we trust caller to give us a private copy
241 244 r = repo
242 245
243 246 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
244 247 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
245 248 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
246 249 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
247 250 # resolve file patterns relative to repo root
248 251 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
249 252 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
250 253 # displaying bundling progress bar while serving feel wrong and may
251 254 # break some wsgi implementation.
252 255 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
253 256 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
254 257 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
255 258 self._lastrepo = self._repos[0]
256 259 hook.redirect(True)
257 260 self.reponame = name
258 261
259 262 def _webifyrepo(self, repo):
260 263 repo = getwebview(repo)
261 264 self.websubtable = webutil.getwebsubs(repo)
262 265 return repo
263 266
264 267 @contextlib.contextmanager
265 268 def _obtainrepo(self):
266 269 """Obtain a repo unique to the caller.
267 270
268 271 Internally we maintain a stack of cachedlocalrepo instances
269 272 to be handed out. If one is available, we pop it and return it,
270 273 ensuring it is up to date in the process. If one is not available,
271 274 we clone the most recently used repo instance and return it.
272 275
273 276 It is currently possible for the stack to grow without bounds
274 277 if the server allows infinite threads. However, servers should
275 278 have a thread limit, thus establishing our limit.
276 279 """
277 280 if self._repos:
278 281 cached = self._repos.pop()
279 282 r, created = cached.fetch()
280 283 else:
281 284 cached = self._lastrepo.copy()
282 285 r, created = cached.fetch()
283 286 if created:
284 287 r = self._webifyrepo(r)
285 288
286 289 self._lastrepo = cached
287 290 self.mtime = cached.mtime
288 291 try:
289 292 yield r
290 293 finally:
291 294 self._repos.append(cached)
292 295
293 296 def run(self):
294 297 """Start a server from CGI environment.
295 298
296 299 Modern servers should be using WSGI and should avoid this
297 300 method, if possible.
298 301 """
299 302 if not encoding.environ.get('GATEWAY_INTERFACE',
300 303 '').startswith("CGI/1."):
301 304 raise RuntimeError("This function is only intended to be "
302 305 "called while running as a CGI script.")
303 306 wsgicgi.launch(self)
304 307
305 308 def __call__(self, env, respond):
306 309 """Run the WSGI application.
307 310
308 311 This may be called by multiple threads.
309 312 """
310 313 req = wsgirequest(env, respond)
311 314 return self.run_wsgi(req)
312 315
313 316 def run_wsgi(self, req):
314 317 """Internal method to run the WSGI application.
315 318
316 319 This is typically only called by Mercurial. External consumers
317 320 should be using instances of this class as the WSGI application.
318 321 """
319 322 with self._obtainrepo() as repo:
320 323 profile = repo.ui.configbool('profiling', 'enabled')
321 324 with profiling.profile(repo.ui, enabled=profile):
322 325 for r in self._runwsgi(req, repo):
323 326 yield r
324 327
325 328 def _runwsgi(self, req, repo):
326 329 rctx = requestcontext(self, repo)
327 330
328 331 # This state is global across all threads.
329 332 encoding.encoding = rctx.config('web', 'encoding')
330 333 rctx.repo.ui.environ = req.env
331 334
332 335 if rctx.csp:
333 336 # hgwebdir may have added CSP header. Since we generate our own,
334 337 # replace it.
335 338 req.headers = [h for h in req.headers
336 339 if h[0] != 'Content-Security-Policy']
337 340 req.headers.append(('Content-Security-Policy', rctx.csp))
338 341
339 342 # work with CGI variables to create coherent structure
340 343 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
341 344
342 345 req.url = req.env[r'SCRIPT_NAME']
343 346 if not req.url.endswith('/'):
344 347 req.url += '/'
345 348 if req.env.get('REPO_NAME'):
346 349 req.url += req.env[r'REPO_NAME'] + r'/'
347 350
348 351 if r'PATH_INFO' in req.env:
349 352 parts = req.env[r'PATH_INFO'].strip('/').split('/')
350 353 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/')
351 354 if parts[:len(repo_parts)] == repo_parts:
352 355 parts = parts[len(repo_parts):]
353 356 query = '/'.join(parts)
354 357 else:
355 358 query = req.env[r'QUERY_STRING'].partition(r'&')[0]
356 359 query = query.partition(r';')[0]
357 360
358 361 # Route it to a wire protocol handler if it looks like a wire protocol
359 362 # request.
360 363 protohandler = wireprotoserver.parsehttprequest(rctx.repo, req, query)
361 364
362 365 if protohandler:
363 366 cmd = protohandler['cmd']
364 367 try:
365 368 if query:
366 369 raise ErrorResponse(HTTP_NOT_FOUND)
367 370 if cmd in perms:
368 371 self.check_perm(rctx, req, perms[cmd])
369 372 except ErrorResponse as inst:
370 373 return protohandler['handleerror'](inst)
371 374
372 375 return protohandler['dispatch']()
373 376
374 377 # translate user-visible url structure to internal structure
375 378
376 379 args = query.split('/', 2)
377 380 if r'cmd' not in req.form and args and args[0]:
378 381 cmd = args.pop(0)
379 382 style = cmd.rfind('-')
380 383 if style != -1:
381 384 req.form['style'] = [cmd[:style]]
382 385 cmd = cmd[style + 1:]
383 386
384 387 # avoid accepting e.g. style parameter as command
385 388 if util.safehasattr(webcommands, cmd):
386 389 req.form[r'cmd'] = [cmd]
387 390
388 391 if cmd == 'static':
389 392 req.form['file'] = ['/'.join(args)]
390 393 else:
391 394 if args and args[0]:
392 395 node = args.pop(0).replace('%2F', '/')
393 396 req.form['node'] = [node]
394 397 if args:
395 398 req.form['file'] = args
396 399
397 400 ua = req.env.get('HTTP_USER_AGENT', '')
398 401 if cmd == 'rev' and 'mercurial' in ua:
399 402 req.form['style'] = ['raw']
400 403
401 404 if cmd == 'archive':
402 405 fn = req.form['node'][0]
403 406 for type_, spec in rctx.archivespecs.iteritems():
404 407 ext = spec[2]
405 408 if fn.endswith(ext):
406 409 req.form['node'] = [fn[:-len(ext)]]
407 410 req.form['type'] = [type_]
408 411 else:
409 412 cmd = pycompat.sysbytes(req.form.get(r'cmd', [r''])[0])
410 413
411 414 # process the web interface request
412 415
413 416 try:
414 417 tmpl = rctx.templater(req)
415 418 ctype = tmpl('mimetype', encoding=encoding.encoding)
416 419 ctype = templater.stringify(ctype)
417 420
418 421 # check read permissions non-static content
419 422 if cmd != 'static':
420 423 self.check_perm(rctx, req, None)
421 424
422 425 if cmd == '':
423 426 req.form[r'cmd'] = [tmpl.cache['default']]
424 427 cmd = req.form[r'cmd'][0]
425 428
426 429 # Don't enable caching if using a CSP nonce because then it wouldn't
427 430 # be a nonce.
428 431 if rctx.configbool('web', 'cache') and not rctx.nonce:
429 432 caching(self, req) # sets ETag header or raises NOT_MODIFIED
430 433 if cmd not in webcommands.__all__:
431 434 msg = 'no such method: %s' % cmd
432 435 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
433 436 elif cmd == 'file' and r'raw' in req.form.get(r'style', []):
434 437 rctx.ctype = ctype
435 438 content = webcommands.rawfile(rctx, req, tmpl)
436 439 else:
437 440 content = getattr(webcommands, cmd)(rctx, req, tmpl)
438 441 req.respond(HTTP_OK, ctype)
439 442
440 443 return content
441 444
442 445 except (error.LookupError, error.RepoLookupError) as err:
443 446 req.respond(HTTP_NOT_FOUND, ctype)
444 447 msg = pycompat.bytestr(err)
445 448 if (util.safehasattr(err, 'name') and
446 449 not isinstance(err, error.ManifestLookupError)):
447 450 msg = 'revision not found: %s' % err.name
448 451 return tmpl('error', error=msg)
449 452 except (error.RepoError, error.RevlogError) as inst:
450 453 req.respond(HTTP_SERVER_ERROR, ctype)
451 454 return tmpl('error', error=pycompat.bytestr(inst))
452 455 except ErrorResponse as inst:
453 456 req.respond(inst, ctype)
454 457 if inst.code == HTTP_NOT_MODIFIED:
455 458 # Not allowed to return a body on a 304
456 459 return ['']
457 460 return tmpl('error', error=pycompat.bytestr(inst))
458 461
459 462 def check_perm(self, rctx, req, op):
460 463 for permhook in permhooks:
461 464 permhook(rctx, req, op)
462 465
463 466 def getwebview(repo):
464 467 """The 'web.view' config controls changeset filter to hgweb. Possible
465 468 values are ``served``, ``visible`` and ``all``. Default is ``served``.
466 469 The ``served`` filter only shows changesets that can be pulled from the
467 470 hgweb instance. The``visible`` filter includes secret changesets but
468 471 still excludes "hidden" one.
469 472
470 473 See the repoview module for details.
471 474
472 475 The option has been around undocumented since Mercurial 2.5, but no
473 476 user ever asked about it. So we better keep it undocumented for now."""
474 477 # experimental config: web.view
475 478 viewconfig = repo.ui.config('web', 'view', untrusted=True)
476 479 if viewconfig == 'all':
477 480 return repo.unfiltered()
478 481 elif viewconfig in repoview.filtertable:
479 482 return repo.filtered(viewconfig)
480 483 else:
481 484 return repo.filtered('served')
@@ -1,654 +1,659 b''
1 1 # hgweb/webutil.py - utility library for the web interface.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import copy
12 12 import difflib
13 13 import os
14 14 import re
15 15
16 16 from ..i18n import _
17 17 from ..node import hex, nullid, short
18 18
19 19 from .common import (
20 20 ErrorResponse,
21 21 HTTP_BAD_REQUEST,
22 22 HTTP_NOT_FOUND,
23 23 paritygen,
24 24 )
25 25
26 26 from .. import (
27 27 context,
28 28 error,
29 29 match,
30 30 mdiff,
31 31 patch,
32 32 pathutil,
33 33 pycompat,
34 34 templatefilters,
35 35 templatekw,
36 36 ui as uimod,
37 37 util,
38 38 )
39 39
40 40 def up(p):
41 41 if p[0] != "/":
42 42 p = "/" + p
43 43 if p[-1] == "/":
44 44 p = p[:-1]
45 45 up = os.path.dirname(p)
46 46 if up == "/":
47 47 return "/"
48 48 return up + "/"
49 49
50 50 def _navseq(step, firststep=None):
51 51 if firststep:
52 52 yield firststep
53 53 if firststep >= 20 and firststep <= 40:
54 54 firststep = 50
55 55 yield firststep
56 56 assert step > 0
57 57 assert firststep > 0
58 58 while step <= firststep:
59 59 step *= 10
60 60 while True:
61 61 yield 1 * step
62 62 yield 3 * step
63 63 step *= 10
64 64
65 65 class revnav(object):
66 66
67 67 def __init__(self, repo):
68 68 """Navigation generation object
69 69
70 70 :repo: repo object we generate nav for
71 71 """
72 72 # used for hex generation
73 73 self._revlog = repo.changelog
74 74
75 75 def __nonzero__(self):
76 76 """return True if any revision to navigate over"""
77 77 return self._first() is not None
78 78
79 79 __bool__ = __nonzero__
80 80
81 81 def _first(self):
82 82 """return the minimum non-filtered changeset or None"""
83 83 try:
84 84 return next(iter(self._revlog))
85 85 except StopIteration:
86 86 return None
87 87
88 88 def hex(self, rev):
89 89 return hex(self._revlog.node(rev))
90 90
91 91 def gen(self, pos, pagelen, limit):
92 92 """computes label and revision id for navigation link
93 93
94 94 :pos: is the revision relative to which we generate navigation.
95 95 :pagelen: the size of each navigation page
96 96 :limit: how far shall we link
97 97
98 98 The return is:
99 99 - a single element tuple
100 100 - containing a dictionary with a `before` and `after` key
101 101 - values are generator functions taking arbitrary number of kwargs
102 102 - yield items are dictionaries with `label` and `node` keys
103 103 """
104 104 if not self:
105 105 # empty repo
106 106 return ({'before': (), 'after': ()},)
107 107
108 108 targets = []
109 109 for f in _navseq(1, pagelen):
110 110 if f > limit:
111 111 break
112 112 targets.append(pos + f)
113 113 targets.append(pos - f)
114 114 targets.sort()
115 115
116 116 first = self._first()
117 117 navbefore = [("(%i)" % first, self.hex(first))]
118 118 navafter = []
119 119 for rev in targets:
120 120 if rev not in self._revlog:
121 121 continue
122 122 if pos < rev < limit:
123 123 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
124 124 if 0 < rev < pos:
125 125 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
126 126
127 127
128 128 navafter.append(("tip", "tip"))
129 129
130 130 data = lambda i: {"label": i[0], "node": i[1]}
131 131 return ({'before': lambda **map: (data(i) for i in navbefore),
132 132 'after': lambda **map: (data(i) for i in navafter)},)
133 133
134 134 class filerevnav(revnav):
135 135
136 136 def __init__(self, repo, path):
137 137 """Navigation generation object
138 138
139 139 :repo: repo object we generate nav for
140 140 :path: path of the file we generate nav for
141 141 """
142 142 # used for iteration
143 143 self._changelog = repo.unfiltered().changelog
144 144 # used for hex generation
145 145 self._revlog = repo.file(path)
146 146
147 147 def hex(self, rev):
148 148 return hex(self._changelog.node(self._revlog.linkrev(rev)))
149 149
150 150 class _siblings(object):
151 151 def __init__(self, siblings=None, hiderev=None):
152 152 if siblings is None:
153 153 siblings = []
154 154 self.siblings = [s for s in siblings if s.node() != nullid]
155 155 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
156 156 self.siblings = []
157 157
158 158 def __iter__(self):
159 159 for s in self.siblings:
160 160 d = {
161 161 'node': s.hex(),
162 162 'rev': s.rev(),
163 163 'user': s.user(),
164 164 'date': s.date(),
165 165 'description': s.description(),
166 166 'branch': s.branch(),
167 167 }
168 168 if util.safehasattr(s, 'path'):
169 169 d['file'] = s.path()
170 170 yield d
171 171
172 172 def __len__(self):
173 173 return len(self.siblings)
174 174
175 175 def difffeatureopts(req, ui, section):
176 176 diffopts = patch.difffeatureopts(ui, untrusted=True,
177 177 section=section, whitespace=True)
178 178
179 179 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
180 180 v = req.form.get(k, [None])[0]
181 181 if v is not None:
182 182 v = util.parsebool(v)
183 183 setattr(diffopts, k, v if v is not None else True)
184 184
185 185 return diffopts
186 186
187 187 def annotate(req, fctx, ui):
188 188 diffopts = difffeatureopts(req, ui, 'annotate')
189 189 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
190 190
191 191 def parents(ctx, hide=None):
192 192 if isinstance(ctx, context.basefilectx):
193 193 introrev = ctx.introrev()
194 194 if ctx.changectx().rev() != introrev:
195 195 return _siblings([ctx.repo()[introrev]], hide)
196 196 return _siblings(ctx.parents(), hide)
197 197
198 198 def children(ctx, hide=None):
199 199 return _siblings(ctx.children(), hide)
200 200
201 201 def renamelink(fctx):
202 202 r = fctx.renamed()
203 203 if r:
204 204 return [{'file': r[0], 'node': hex(r[1])}]
205 205 return []
206 206
207 207 def nodetagsdict(repo, node):
208 208 return [{"name": i} for i in repo.nodetags(node)]
209 209
210 210 def nodebookmarksdict(repo, node):
211 211 return [{"name": i} for i in repo.nodebookmarks(node)]
212 212
213 213 def nodebranchdict(repo, ctx):
214 214 branches = []
215 215 branch = ctx.branch()
216 216 # If this is an empty repo, ctx.node() == nullid,
217 217 # ctx.branch() == 'default'.
218 218 try:
219 219 branchnode = repo.branchtip(branch)
220 220 except error.RepoLookupError:
221 221 branchnode = None
222 222 if branchnode == ctx.node():
223 223 branches.append({"name": branch})
224 224 return branches
225 225
226 226 def nodeinbranch(repo, ctx):
227 227 branches = []
228 228 branch = ctx.branch()
229 229 try:
230 230 branchnode = repo.branchtip(branch)
231 231 except error.RepoLookupError:
232 232 branchnode = None
233 233 if branch != 'default' and branchnode != ctx.node():
234 234 branches.append({"name": branch})
235 235 return branches
236 236
237 237 def nodebranchnodefault(ctx):
238 238 branches = []
239 239 branch = ctx.branch()
240 240 if branch != 'default':
241 241 branches.append({"name": branch})
242 242 return branches
243 243
244 244 def showtag(repo, tmpl, t1, node=nullid, **args):
245 245 for t in repo.nodetags(node):
246 246 yield tmpl(t1, tag=t, **args)
247 247
248 248 def showbookmark(repo, tmpl, t1, node=nullid, **args):
249 249 for t in repo.nodebookmarks(node):
250 250 yield tmpl(t1, bookmark=t, **args)
251 251
252 252 def branchentries(repo, stripecount, limit=0):
253 253 tips = []
254 254 heads = repo.heads()
255 255 parity = paritygen(stripecount)
256 256 sortkey = lambda item: (not item[1], item[0].rev())
257 257
258 258 def entries(**map):
259 259 count = 0
260 260 if not tips:
261 261 for tag, hs, tip, closed in repo.branchmap().iterbranches():
262 262 tips.append((repo[tip], closed))
263 263 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
264 264 if limit > 0 and count >= limit:
265 265 return
266 266 count += 1
267 267 if closed:
268 268 status = 'closed'
269 269 elif ctx.node() not in heads:
270 270 status = 'inactive'
271 271 else:
272 272 status = 'open'
273 273 yield {
274 274 'parity': next(parity),
275 275 'branch': ctx.branch(),
276 276 'status': status,
277 277 'node': ctx.hex(),
278 278 'date': ctx.date()
279 279 }
280 280
281 281 return entries
282 282
283 283 def cleanpath(repo, path):
284 284 path = path.lstrip('/')
285 285 return pathutil.canonpath(repo.root, '', path)
286 286
287 287 def changeidctx(repo, changeid):
288 288 try:
289 289 ctx = repo[changeid]
290 290 except error.RepoError:
291 291 man = repo.manifestlog._revlog
292 292 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
293 293
294 294 return ctx
295 295
296 296 def changectx(repo, req):
297 297 changeid = "tip"
298 298 if 'node' in req.form:
299 299 changeid = req.form['node'][0]
300 300 ipos = changeid.find(':')
301 301 if ipos != -1:
302 302 changeid = changeid[(ipos + 1):]
303 303 elif 'manifest' in req.form:
304 304 changeid = req.form['manifest'][0]
305 305
306 306 return changeidctx(repo, changeid)
307 307
308 308 def basechangectx(repo, req):
309 309 if 'node' in req.form:
310 310 changeid = req.form['node'][0]
311 311 ipos = changeid.find(':')
312 312 if ipos != -1:
313 313 changeid = changeid[:ipos]
314 314 return changeidctx(repo, changeid)
315 315
316 316 return None
317 317
318 318 def filectx(repo, req):
319 319 if 'file' not in req.form:
320 320 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
321 321 path = cleanpath(repo, req.form['file'][0])
322 322 if 'node' in req.form:
323 323 changeid = req.form['node'][0]
324 324 elif 'filenode' in req.form:
325 325 changeid = req.form['filenode'][0]
326 326 else:
327 327 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
328 328 try:
329 329 fctx = repo[changeid][path]
330 330 except error.RepoError:
331 331 fctx = repo.filectx(path, fileid=changeid)
332 332
333 333 return fctx
334 334
335 335 def linerange(req):
336 336 linerange = req.form.get('linerange')
337 337 if linerange is None:
338 338 return None
339 339 if len(linerange) > 1:
340 340 raise ErrorResponse(HTTP_BAD_REQUEST,
341 341 'redundant linerange parameter')
342 342 try:
343 343 fromline, toline = map(int, linerange[0].split(':', 1))
344 344 except ValueError:
345 345 raise ErrorResponse(HTTP_BAD_REQUEST,
346 346 'invalid linerange parameter')
347 347 try:
348 348 return util.processlinerange(fromline, toline)
349 349 except error.ParseError as exc:
350 350 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
351 351
352 352 def formatlinerange(fromline, toline):
353 353 return '%d:%d' % (fromline + 1, toline)
354 354
355 def succsandmarkers(repo, ctx):
356 for item in templatekw.showsuccsandmarkers(repo, ctx):
355 def succsandmarkers(repo, ctx, **args):
356 for item in templatekw.showsuccsandmarkers(repo, ctx, **args):
357 357 item['successors'] = _siblings(repo[successor]
358 358 for successor in item['successors'])
359 359 yield item
360 360
361 361 def commonentry(repo, ctx):
362 362 node = ctx.node()
363 363 return {
364 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
365 # filectx, but I'm not pretty sure if that would always work because
366 # fctx.parents() != fctx.changectx.parents() for example.
367 'ctx': ctx,
368 'revcache': {},
364 369 'rev': ctx.rev(),
365 370 'node': hex(node),
366 371 'author': ctx.user(),
367 372 'desc': ctx.description(),
368 373 'date': ctx.date(),
369 374 'extra': ctx.extra(),
370 375 'phase': ctx.phasestr(),
371 376 'obsolete': ctx.obsolete(),
372 'succsandmarkers': lambda **x: succsandmarkers(repo, ctx),
377 'succsandmarkers': succsandmarkers,
373 378 'instabilities': [{"instability": i} for i in ctx.instabilities()],
374 379 'branch': nodebranchnodefault(ctx),
375 380 'inbranch': nodeinbranch(repo, ctx),
376 381 'branches': nodebranchdict(repo, ctx),
377 382 'tags': nodetagsdict(repo, node),
378 383 'bookmarks': nodebookmarksdict(repo, node),
379 384 'parent': lambda **x: parents(ctx),
380 385 'child': lambda **x: children(ctx),
381 386 }
382 387
383 388 def changelistentry(web, ctx, tmpl):
384 389 '''Obtain a dictionary to be used for entries in a changelist.
385 390
386 391 This function is called when producing items for the "entries" list passed
387 392 to the "shortlog" and "changelog" templates.
388 393 '''
389 394 repo = web.repo
390 395 rev = ctx.rev()
391 396 n = ctx.node()
392 397 showtags = showtag(repo, tmpl, 'changelogtag', n)
393 398 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
394 399
395 400 entry = commonentry(repo, ctx)
396 401 entry.update(
397 402 allparents=lambda **x: parents(ctx),
398 403 parent=lambda **x: parents(ctx, rev - 1),
399 404 child=lambda **x: children(ctx, rev + 1),
400 405 changelogtag=showtags,
401 406 files=files,
402 407 )
403 408 return entry
404 409
405 410 def symrevorshortnode(req, ctx):
406 411 if 'node' in req.form:
407 412 return templatefilters.revescape(req.form['node'][0])
408 413 else:
409 414 return short(ctx.node())
410 415
411 416 def changesetentry(web, req, tmpl, ctx):
412 417 '''Obtain a dictionary to be used to render the "changeset" template.'''
413 418
414 419 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
415 420 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
416 421 ctx.node())
417 422 showbranch = nodebranchnodefault(ctx)
418 423
419 424 files = []
420 425 parity = paritygen(web.stripecount)
421 426 for blockno, f in enumerate(ctx.files()):
422 427 template = 'filenodelink' if f in ctx else 'filenolink'
423 428 files.append(tmpl(template,
424 429 node=ctx.hex(), file=f, blockno=blockno + 1,
425 430 parity=next(parity)))
426 431
427 432 basectx = basechangectx(web.repo, req)
428 433 if basectx is None:
429 434 basectx = ctx.p1()
430 435
431 436 style = web.config('web', 'style')
432 437 if 'style' in req.form:
433 438 style = req.form['style'][0]
434 439
435 440 diff = diffs(web, tmpl, ctx, basectx, None, style)
436 441
437 442 parity = paritygen(web.stripecount)
438 443 diffstatsgen = diffstatgen(ctx, basectx)
439 444 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
440 445
441 446 return dict(
442 447 diff=diff,
443 448 symrev=symrevorshortnode(req, ctx),
444 449 basenode=basectx.hex(),
445 450 changesettag=showtags,
446 451 changesetbookmark=showbookmarks,
447 452 changesetbranch=showbranch,
448 453 files=files,
449 454 diffsummary=lambda **x: diffsummary(diffstatsgen),
450 455 diffstat=diffstats,
451 456 archives=web.archivelist(ctx.hex()),
452 457 **pycompat.strkwargs(commonentry(web.repo, ctx)))
453 458
454 459 def listfilediffs(tmpl, files, node, max):
455 460 for f in files[:max]:
456 461 yield tmpl('filedifflink', node=hex(node), file=f)
457 462 if len(files) > max:
458 463 yield tmpl('fileellipses')
459 464
460 465 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
461 466 lineidprefix=''):
462 467
463 468 def prettyprintlines(lines, blockno):
464 469 for lineno, l in enumerate(lines, 1):
465 470 difflineno = "%d.%d" % (blockno, lineno)
466 471 if l.startswith('+'):
467 472 ltype = "difflineplus"
468 473 elif l.startswith('-'):
469 474 ltype = "difflineminus"
470 475 elif l.startswith('@'):
471 476 ltype = "difflineat"
472 477 else:
473 478 ltype = "diffline"
474 479 yield tmpl(ltype,
475 480 line=l,
476 481 lineno=lineno,
477 482 lineid=lineidprefix + "l%s" % difflineno,
478 483 linenumber="% 8s" % difflineno)
479 484
480 485 repo = web.repo
481 486 if files:
482 487 m = match.exact(repo.root, repo.getcwd(), files)
483 488 else:
484 489 m = match.always(repo.root, repo.getcwd())
485 490
486 491 diffopts = patch.diffopts(repo.ui, untrusted=True)
487 492 node1 = basectx.node()
488 493 node2 = ctx.node()
489 494 parity = paritygen(web.stripecount)
490 495
491 496 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
492 497 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
493 498 if style != 'raw':
494 499 header = header[1:]
495 500 lines = [h + '\n' for h in header]
496 501 for hunkrange, hunklines in hunks:
497 502 if linerange is not None and hunkrange is not None:
498 503 s1, l1, s2, l2 = hunkrange
499 504 if not mdiff.hunkinrange((s2, l2), linerange):
500 505 continue
501 506 lines.extend(hunklines)
502 507 if lines:
503 508 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
504 509 lines=prettyprintlines(lines, blockno))
505 510
506 511 def compare(tmpl, context, leftlines, rightlines):
507 512 '''Generator function that provides side-by-side comparison data.'''
508 513
509 514 def compline(type, leftlineno, leftline, rightlineno, rightline):
510 515 lineid = leftlineno and ("l%s" % leftlineno) or ''
511 516 lineid += rightlineno and ("r%s" % rightlineno) or ''
512 517 return tmpl('comparisonline',
513 518 type=type,
514 519 lineid=lineid,
515 520 leftlineno=leftlineno,
516 521 leftlinenumber="% 6s" % (leftlineno or ''),
517 522 leftline=leftline or '',
518 523 rightlineno=rightlineno,
519 524 rightlinenumber="% 6s" % (rightlineno or ''),
520 525 rightline=rightline or '')
521 526
522 527 def getblock(opcodes):
523 528 for type, llo, lhi, rlo, rhi in opcodes:
524 529 len1 = lhi - llo
525 530 len2 = rhi - rlo
526 531 count = min(len1, len2)
527 532 for i in xrange(count):
528 533 yield compline(type=type,
529 534 leftlineno=llo + i + 1,
530 535 leftline=leftlines[llo + i],
531 536 rightlineno=rlo + i + 1,
532 537 rightline=rightlines[rlo + i])
533 538 if len1 > len2:
534 539 for i in xrange(llo + count, lhi):
535 540 yield compline(type=type,
536 541 leftlineno=i + 1,
537 542 leftline=leftlines[i],
538 543 rightlineno=None,
539 544 rightline=None)
540 545 elif len2 > len1:
541 546 for i in xrange(rlo + count, rhi):
542 547 yield compline(type=type,
543 548 leftlineno=None,
544 549 leftline=None,
545 550 rightlineno=i + 1,
546 551 rightline=rightlines[i])
547 552
548 553 s = difflib.SequenceMatcher(None, leftlines, rightlines)
549 554 if context < 0:
550 555 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
551 556 else:
552 557 for oc in s.get_grouped_opcodes(n=context):
553 558 yield tmpl('comparisonblock', lines=getblock(oc))
554 559
555 560 def diffstatgen(ctx, basectx):
556 561 '''Generator function that provides the diffstat data.'''
557 562
558 563 stats = patch.diffstatdata(
559 564 util.iterlines(ctx.diff(basectx, noprefix=False)))
560 565 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
561 566 while True:
562 567 yield stats, maxname, maxtotal, addtotal, removetotal, binary
563 568
564 569 def diffsummary(statgen):
565 570 '''Return a short summary of the diff.'''
566 571
567 572 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
568 573 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
569 574 len(stats), addtotal, removetotal)
570 575
571 576 def diffstat(tmpl, ctx, statgen, parity):
572 577 '''Return a diffstat template for each file in the diff.'''
573 578
574 579 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
575 580 files = ctx.files()
576 581
577 582 def pct(i):
578 583 if maxtotal == 0:
579 584 return 0
580 585 return (float(i) / maxtotal) * 100
581 586
582 587 fileno = 0
583 588 for filename, adds, removes, isbinary in stats:
584 589 template = 'diffstatlink' if filename in files else 'diffstatnolink'
585 590 total = adds + removes
586 591 fileno += 1
587 592 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
588 593 total=total, addpct=pct(adds), removepct=pct(removes),
589 594 parity=next(parity))
590 595
591 596 class sessionvars(object):
592 597 def __init__(self, vars, start='?'):
593 598 self.start = start
594 599 self.vars = vars
595 600 def __getitem__(self, key):
596 601 return self.vars[key]
597 602 def __setitem__(self, key, value):
598 603 self.vars[key] = value
599 604 def __copy__(self):
600 605 return sessionvars(copy.copy(self.vars), self.start)
601 606 def __iter__(self):
602 607 separator = self.start
603 608 for key, value in sorted(self.vars.iteritems()):
604 609 yield {'name': key,
605 610 'value': pycompat.bytestr(value),
606 611 'separator': separator,
607 612 }
608 613 separator = '&'
609 614
610 615 class wsgiui(uimod.ui):
611 616 # default termwidth breaks under mod_wsgi
612 617 def termwidth(self):
613 618 return 80
614 619
615 620 def getwebsubs(repo):
616 621 websubtable = []
617 622 websubdefs = repo.ui.configitems('websub')
618 623 # we must maintain interhg backwards compatibility
619 624 websubdefs += repo.ui.configitems('interhg')
620 625 for key, pattern in websubdefs:
621 626 # grab the delimiter from the character after the "s"
622 627 unesc = pattern[1:2]
623 628 delim = re.escape(unesc)
624 629
625 630 # identify portions of the pattern, taking care to avoid escaped
626 631 # delimiters. the replace format and flags are optional, but
627 632 # delimiters are required.
628 633 match = re.match(
629 634 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
630 635 % (delim, delim, delim), pattern)
631 636 if not match:
632 637 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
633 638 % (key, pattern))
634 639 continue
635 640
636 641 # we need to unescape the delimiter for regexp and format
637 642 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
638 643 regexp = delim_re.sub(unesc, match.group(1))
639 644 format = delim_re.sub(unesc, match.group(2))
640 645
641 646 # the pattern allows for 6 regexp flags, so set them if necessary
642 647 flagin = match.group(3)
643 648 flags = 0
644 649 if flagin:
645 650 for flag in flagin.upper():
646 651 flags |= re.__dict__[flag]
647 652
648 653 try:
649 654 regexp = re.compile(regexp, flags)
650 655 websubtable.append((regexp, format))
651 656 except re.error:
652 657 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
653 658 % (key, regexp))
654 659 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now