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