##// END OF EJS Templates
hgweb: perform all parameter lookup via qsparams...
Gregory Szorc -
r36881:3d60a22e default
parent child Browse files
Show More
@@ -1,449 +1,449 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
26 26 from .. import (
27 27 encoding,
28 28 error,
29 29 formatter,
30 30 hg,
31 31 hook,
32 32 profiling,
33 33 pycompat,
34 34 repoview,
35 35 templatefilters,
36 36 templater,
37 37 ui as uimod,
38 38 util,
39 39 wireprotoserver,
40 40 )
41 41
42 42 from . import (
43 43 request as requestmod,
44 44 webcommands,
45 45 webutil,
46 46 wsgicgi,
47 47 )
48 48
49 49 archivespecs = util.sortdict((
50 50 ('zip', ('application/zip', 'zip', '.zip', None)),
51 51 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
52 52 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
53 53 ))
54 54
55 55 def getstyle(req, configfn, templatepath):
56 56 styles = (
57 57 req.qsparams.get('style', None),
58 58 configfn('web', 'style'),
59 59 'paper',
60 60 )
61 61 return styles, templater.stylemap(styles, templatepath)
62 62
63 63 def makebreadcrumb(url, prefix=''):
64 64 '''Return a 'URL breadcrumb' list
65 65
66 66 A 'URL breadcrumb' is a list of URL-name pairs,
67 67 corresponding to each of the path items on a URL.
68 68 This can be used to create path navigation entries.
69 69 '''
70 70 if url.endswith('/'):
71 71 url = url[:-1]
72 72 if prefix:
73 73 url = '/' + prefix + url
74 74 relpath = url
75 75 if relpath.startswith('/'):
76 76 relpath = relpath[1:]
77 77
78 78 breadcrumb = []
79 79 urlel = url
80 80 pathitems = [''] + relpath.split('/')
81 81 for pathel in reversed(pathitems):
82 82 if not pathel or not urlel:
83 83 break
84 84 breadcrumb.append({'url': urlel, 'name': pathel})
85 85 urlel = os.path.dirname(urlel)
86 86 return reversed(breadcrumb)
87 87
88 88 class requestcontext(object):
89 89 """Holds state/context for an individual request.
90 90
91 91 Servers can be multi-threaded. Holding state on the WSGI application
92 92 is prone to race conditions. Instances of this class exist to hold
93 93 mutable and race-free state for requests.
94 94 """
95 95 def __init__(self, app, repo):
96 96 self.repo = repo
97 97 self.reponame = app.reponame
98 98
99 99 self.archivespecs = archivespecs
100 100
101 101 self.maxchanges = self.configint('web', 'maxchanges')
102 102 self.stripecount = self.configint('web', 'stripes')
103 103 self.maxshortchanges = self.configint('web', 'maxshortchanges')
104 104 self.maxfiles = self.configint('web', 'maxfiles')
105 105 self.allowpull = self.configbool('web', 'allow-pull')
106 106
107 107 # we use untrusted=False to prevent a repo owner from using
108 108 # web.templates in .hg/hgrc to get access to any file readable
109 109 # by the user running the CGI script
110 110 self.templatepath = self.config('web', 'templates', untrusted=False)
111 111
112 112 # This object is more expensive to build than simple config values.
113 113 # It is shared across requests. The app will replace the object
114 114 # if it is updated. Since this is a reference and nothing should
115 115 # modify the underlying object, it should be constant for the lifetime
116 116 # of the request.
117 117 self.websubtable = app.websubtable
118 118
119 119 self.csp, self.nonce = cspvalues(self.repo.ui)
120 120
121 121 # Trust the settings from the .hg/hgrc files by default.
122 122 def config(self, section, name, default=uimod._unset, untrusted=True):
123 123 return self.repo.ui.config(section, name, default,
124 124 untrusted=untrusted)
125 125
126 126 def configbool(self, section, name, default=uimod._unset, untrusted=True):
127 127 return self.repo.ui.configbool(section, name, default,
128 128 untrusted=untrusted)
129 129
130 130 def configint(self, section, name, default=uimod._unset, untrusted=True):
131 131 return self.repo.ui.configint(section, name, default,
132 132 untrusted=untrusted)
133 133
134 134 def configlist(self, section, name, default=uimod._unset, untrusted=True):
135 135 return self.repo.ui.configlist(section, name, default,
136 136 untrusted=untrusted)
137 137
138 138 def archivelist(self, nodeid):
139 139 allowed = self.configlist('web', 'allow_archive')
140 140 for typ, spec in self.archivespecs.iteritems():
141 141 if typ in allowed or self.configbool('web', 'allow%s' % typ):
142 142 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
143 143
144 144 def templater(self, wsgireq, req):
145 145 # determine scheme, port and server name
146 146 # this is needed to create absolute urls
147 147 logourl = self.config('web', 'logourl')
148 148 logoimg = self.config('web', 'logoimg')
149 149 staticurl = (self.config('web', 'staticurl')
150 150 or req.apppath + '/static/')
151 151 if not staticurl.endswith('/'):
152 152 staticurl += '/'
153 153
154 154 # some functions for the templater
155 155
156 156 def motd(**map):
157 157 yield self.config('web', 'motd')
158 158
159 159 # figure out which style to use
160 160
161 161 vars = {}
162 162 styles, (style, mapfile) = getstyle(wsgireq.req, self.config,
163 163 self.templatepath)
164 164 if style == styles[0]:
165 165 vars['style'] = style
166 166
167 167 sessionvars = webutil.sessionvars(vars, '?')
168 168
169 169 if not self.reponame:
170 170 self.reponame = (self.config('web', 'name', '')
171 171 or wsgireq.env.get('REPO_NAME')
172 172 or req.apppath or self.repo.root)
173 173
174 174 def websubfilter(text):
175 175 return templatefilters.websub(text, self.websubtable)
176 176
177 177 # create the templater
178 178 # TODO: export all keywords: defaults = templatekw.keywords.copy()
179 179 defaults = {
180 180 'url': req.apppath + '/',
181 181 'logourl': logourl,
182 182 'logoimg': logoimg,
183 183 'staticurl': staticurl,
184 184 'urlbase': req.advertisedbaseurl,
185 185 'repo': self.reponame,
186 186 'encoding': encoding.encoding,
187 187 'motd': motd,
188 188 'sessionvars': sessionvars,
189 189 'pathdef': makebreadcrumb(req.apppath),
190 190 'style': style,
191 191 'nonce': self.nonce,
192 192 }
193 193 tres = formatter.templateresources(self.repo.ui, self.repo)
194 194 tmpl = templater.templater.frommapfile(mapfile,
195 195 filters={'websub': websubfilter},
196 196 defaults=defaults,
197 197 resources=tres)
198 198 return tmpl
199 199
200 200
201 201 class hgweb(object):
202 202 """HTTP server for individual repositories.
203 203
204 204 Instances of this class serve HTTP responses for a particular
205 205 repository.
206 206
207 207 Instances are typically used as WSGI applications.
208 208
209 209 Some servers are multi-threaded. On these servers, there may
210 210 be multiple active threads inside __call__.
211 211 """
212 212 def __init__(self, repo, name=None, baseui=None):
213 213 if isinstance(repo, str):
214 214 if baseui:
215 215 u = baseui.copy()
216 216 else:
217 217 u = uimod.ui.load()
218 218 r = hg.repository(u, repo)
219 219 else:
220 220 # we trust caller to give us a private copy
221 221 r = repo
222 222
223 223 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
224 224 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 225 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
226 226 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 227 # resolve file patterns relative to repo root
228 228 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
229 229 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 230 # displaying bundling progress bar while serving feel wrong and may
231 231 # break some wsgi implementation.
232 232 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
233 233 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
234 234 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
235 235 self._lastrepo = self._repos[0]
236 236 hook.redirect(True)
237 237 self.reponame = name
238 238
239 239 def _webifyrepo(self, repo):
240 240 repo = getwebview(repo)
241 241 self.websubtable = webutil.getwebsubs(repo)
242 242 return repo
243 243
244 244 @contextlib.contextmanager
245 245 def _obtainrepo(self):
246 246 """Obtain a repo unique to the caller.
247 247
248 248 Internally we maintain a stack of cachedlocalrepo instances
249 249 to be handed out. If one is available, we pop it and return it,
250 250 ensuring it is up to date in the process. If one is not available,
251 251 we clone the most recently used repo instance and return it.
252 252
253 253 It is currently possible for the stack to grow without bounds
254 254 if the server allows infinite threads. However, servers should
255 255 have a thread limit, thus establishing our limit.
256 256 """
257 257 if self._repos:
258 258 cached = self._repos.pop()
259 259 r, created = cached.fetch()
260 260 else:
261 261 cached = self._lastrepo.copy()
262 262 r, created = cached.fetch()
263 263 if created:
264 264 r = self._webifyrepo(r)
265 265
266 266 self._lastrepo = cached
267 267 self.mtime = cached.mtime
268 268 try:
269 269 yield r
270 270 finally:
271 271 self._repos.append(cached)
272 272
273 273 def run(self):
274 274 """Start a server from CGI environment.
275 275
276 276 Modern servers should be using WSGI and should avoid this
277 277 method, if possible.
278 278 """
279 279 if not encoding.environ.get('GATEWAY_INTERFACE',
280 280 '').startswith("CGI/1."):
281 281 raise RuntimeError("This function is only intended to be "
282 282 "called while running as a CGI script.")
283 283 wsgicgi.launch(self)
284 284
285 285 def __call__(self, env, respond):
286 286 """Run the WSGI application.
287 287
288 288 This may be called by multiple threads.
289 289 """
290 290 req = requestmod.wsgirequest(env, respond)
291 291 return self.run_wsgi(req)
292 292
293 293 def run_wsgi(self, wsgireq):
294 294 """Internal method to run the WSGI application.
295 295
296 296 This is typically only called by Mercurial. External consumers
297 297 should be using instances of this class as the WSGI application.
298 298 """
299 299 with self._obtainrepo() as repo:
300 300 profile = repo.ui.configbool('profiling', 'enabled')
301 301 with profiling.profile(repo.ui, enabled=profile):
302 302 for r in self._runwsgi(wsgireq, repo):
303 303 yield r
304 304
305 305 def _runwsgi(self, wsgireq, repo):
306 306 req = wsgireq.req
307 307 res = wsgireq.res
308 308 rctx = requestcontext(self, repo)
309 309
310 310 # This state is global across all threads.
311 311 encoding.encoding = rctx.config('web', 'encoding')
312 312 rctx.repo.ui.environ = wsgireq.env
313 313
314 314 if rctx.csp:
315 315 # hgwebdir may have added CSP header. Since we generate our own,
316 316 # replace it.
317 317 wsgireq.headers = [h for h in wsgireq.headers
318 318 if h[0] != 'Content-Security-Policy']
319 319 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
320 320 res.headers['Content-Security-Policy'] = rctx.csp
321 321
322 322 handled = wireprotoserver.handlewsgirequest(
323 323 rctx, wsgireq, req, res, self.check_perm)
324 324 if handled:
325 325 return res.sendresponse()
326 326
327 327 if req.havepathinfo:
328 328 query = req.dispatchpath
329 329 else:
330 330 query = req.querystring.partition('&')[0].partition(';')[0]
331 331
332 332 # translate user-visible url structure to internal structure
333 333
334 334 args = query.split('/', 2)
335 if 'cmd' not in wsgireq.form and args and args[0]:
335 if 'cmd' not in req.qsparams and args and args[0]:
336 336 cmd = args.pop(0)
337 337 style = cmd.rfind('-')
338 338 if style != -1:
339 339 req.qsparams['style'] = cmd[:style]
340 340 cmd = cmd[style + 1:]
341 341
342 342 # avoid accepting e.g. style parameter as command
343 343 if util.safehasattr(webcommands, cmd):
344 344 wsgireq.form['cmd'] = [cmd]
345 345 req.qsparams['cmd'] = cmd
346 346
347 347 if cmd == 'static':
348 348 wsgireq.form['file'] = ['/'.join(args)]
349 349 req.qsparams['file'] = '/'.join(args)
350 350 else:
351 351 if args and args[0]:
352 352 node = args.pop(0).replace('%2F', '/')
353 353 wsgireq.form['node'] = [node]
354 354 req.qsparams['node'] = node
355 355 if args:
356 356 wsgireq.form['file'] = args
357 357 if 'file' in req.qsparams:
358 358 del req.qsparams['file']
359 359 for a in args:
360 360 req.qsparams.add('file', a)
361 361
362 362 ua = req.headers.get('User-Agent', '')
363 363 if cmd == 'rev' and 'mercurial' in ua:
364 364 req.qsparams['style'] = 'raw'
365 365
366 366 if cmd == 'archive':
367 fn = wsgireq.form['node'][0]
367 fn = req.qsparams['node']
368 368 for type_, spec in rctx.archivespecs.iteritems():
369 369 ext = spec[2]
370 370 if fn.endswith(ext):
371 371 wsgireq.form['node'] = [fn[:-len(ext)]]
372 req.qsparams['node'] = fn[:-len(next)]
372 req.qsparams['node'] = fn[:-len(ext)]
373 373 wsgireq.form['type'] = [type_]
374 374 req.qsparams['type'] = type_
375 375 else:
376 cmd = wsgireq.form.get('cmd', [''])[0]
376 cmd = req.qsparams.get('cmd', '')
377 377
378 378 # process the web interface request
379 379
380 380 try:
381 381 tmpl = rctx.templater(wsgireq, req)
382 382 ctype = tmpl('mimetype', encoding=encoding.encoding)
383 383 ctype = templater.stringify(ctype)
384 384
385 385 # check read permissions non-static content
386 386 if cmd != 'static':
387 387 self.check_perm(rctx, wsgireq, None)
388 388
389 389 if cmd == '':
390 390 wsgireq.form['cmd'] = [tmpl.cache['default']]
391 391 req.qsparams['cmd'] = tmpl.cache['default']
392 cmd = wsgireq.form['cmd'][0]
392 cmd = req.qsparams['cmd']
393 393
394 394 # Don't enable caching if using a CSP nonce because then it wouldn't
395 395 # be a nonce.
396 396 if rctx.configbool('web', 'cache') and not rctx.nonce:
397 397 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
398 398 if cmd not in webcommands.__all__:
399 399 msg = 'no such method: %s' % cmd
400 400 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
401 401 elif cmd == 'file' and req.qsparams.get('style') == 'raw':
402 402 rctx.ctype = ctype
403 403 content = webcommands.rawfile(rctx, wsgireq, tmpl)
404 404 else:
405 405 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
406 406 wsgireq.respond(HTTP_OK, ctype)
407 407
408 408 return content
409 409
410 410 except (error.LookupError, error.RepoLookupError) as err:
411 411 wsgireq.respond(HTTP_NOT_FOUND, ctype)
412 412 msg = pycompat.bytestr(err)
413 413 if (util.safehasattr(err, 'name') and
414 414 not isinstance(err, error.ManifestLookupError)):
415 415 msg = 'revision not found: %s' % err.name
416 416 return tmpl('error', error=msg)
417 417 except (error.RepoError, error.RevlogError) as inst:
418 418 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
419 419 return tmpl('error', error=pycompat.bytestr(inst))
420 420 except ErrorResponse as inst:
421 421 wsgireq.respond(inst, ctype)
422 422 if inst.code == HTTP_NOT_MODIFIED:
423 423 # Not allowed to return a body on a 304
424 424 return ['']
425 425 return tmpl('error', error=pycompat.bytestr(inst))
426 426
427 427 def check_perm(self, rctx, req, op):
428 428 for permhook in permhooks:
429 429 permhook(rctx, req, op)
430 430
431 431 def getwebview(repo):
432 432 """The 'web.view' config controls changeset filter to hgweb. Possible
433 433 values are ``served``, ``visible`` and ``all``. Default is ``served``.
434 434 The ``served`` filter only shows changesets that can be pulled from the
435 435 hgweb instance. The``visible`` filter includes secret changesets but
436 436 still excludes "hidden" one.
437 437
438 438 See the repoview module for details.
439 439
440 440 The option has been around undocumented since Mercurial 2.5, but no
441 441 user ever asked about it. So we better keep it undocumented for now."""
442 442 # experimental config: web.view
443 443 viewconfig = repo.ui.config('web', 'view', untrusted=True)
444 444 if viewconfig == 'all':
445 445 return repo.unfiltered()
446 446 elif viewconfig in repoview.filtertable:
447 447 return repo.filtered(viewconfig)
448 448 else:
449 449 return repo.filtered('served')
@@ -1,544 +1,546 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 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 os
12 12 import re
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_NOT_FOUND,
20 20 HTTP_OK,
21 21 HTTP_SERVER_ERROR,
22 22 cspvalues,
23 23 get_contact,
24 24 get_mtime,
25 25 ismember,
26 26 paritygen,
27 27 staticfile,
28 28 )
29 29
30 30 from .. import (
31 31 configitems,
32 32 encoding,
33 33 error,
34 34 hg,
35 35 profiling,
36 36 pycompat,
37 37 scmutil,
38 38 templater,
39 39 ui as uimod,
40 40 util,
41 41 )
42 42
43 43 from . import (
44 44 hgweb_mod,
45 45 request as requestmod,
46 46 webutil,
47 47 wsgicgi,
48 48 )
49 49 from ..utils import dateutil
50 50
51 51 def cleannames(items):
52 52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53 53
54 54 def findrepos(paths):
55 55 repos = []
56 56 for prefix, root in cleannames(paths):
57 57 roothead, roottail = os.path.split(root)
58 58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 59 # /bar/ be served as as foo/N .
60 60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 62 try:
63 63 recurse = {'*': False, '**': True}[roottail]
64 64 except KeyError:
65 65 repos.append((prefix, root))
66 66 continue
67 67 roothead = os.path.normpath(os.path.abspath(roothead))
68 68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 69 repos.extend(urlrepos(prefix, roothead, paths))
70 70 return repos
71 71
72 72 def urlrepos(prefix, roothead, paths):
73 73 """yield url paths and filesystem paths from a list of repo paths
74 74
75 75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 80 """
81 81 for path in paths:
82 82 path = os.path.normpath(path)
83 83 yield (prefix + '/' +
84 84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85 85
86 86 def geturlcgivars(baseurl, port):
87 87 """
88 88 Extract CGI variables from baseurl
89 89
90 90 >>> geturlcgivars(b"http://host.org/base", b"80")
91 91 ('host.org', '80', '/base')
92 92 >>> geturlcgivars(b"http://host.org:8000/base", b"80")
93 93 ('host.org', '8000', '/base')
94 94 >>> geturlcgivars(b'/base', 8000)
95 95 ('', '8000', '/base')
96 96 >>> geturlcgivars(b"base", b'8000')
97 97 ('', '8000', '/base')
98 98 >>> geturlcgivars(b"http://host", b'8000')
99 99 ('host', '8000', '/')
100 100 >>> geturlcgivars(b"http://host/", b'8000')
101 101 ('host', '8000', '/')
102 102 """
103 103 u = util.url(baseurl)
104 104 name = u.host or ''
105 105 if u.port:
106 106 port = u.port
107 107 path = u.path or ""
108 108 if not path.startswith('/'):
109 109 path = '/' + path
110 110
111 111 return name, pycompat.bytestr(port), path
112 112
113 113 class hgwebdir(object):
114 114 """HTTP server for multiple repositories.
115 115
116 116 Given a configuration, different repositories will be served depending
117 117 on the request path.
118 118
119 119 Instances are typically used as WSGI applications.
120 120 """
121 121 def __init__(self, conf, baseui=None):
122 122 self.conf = conf
123 123 self.baseui = baseui
124 124 self.ui = None
125 125 self.lastrefresh = 0
126 126 self.motd = None
127 127 self.refresh()
128 128
129 129 def refresh(self):
130 130 if self.ui:
131 131 refreshinterval = self.ui.configint('web', 'refreshinterval')
132 132 else:
133 133 item = configitems.coreitems['web']['refreshinterval']
134 134 refreshinterval = item.default
135 135
136 136 # refreshinterval <= 0 means to always refresh.
137 137 if (refreshinterval > 0 and
138 138 self.lastrefresh + refreshinterval > time.time()):
139 139 return
140 140
141 141 if self.baseui:
142 142 u = self.baseui.copy()
143 143 else:
144 144 u = uimod.ui.load()
145 145 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
146 146 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
147 147 # displaying bundling progress bar while serving feels wrong and may
148 148 # break some wsgi implementations.
149 149 u.setconfig('progress', 'disable', 'true', 'hgweb')
150 150
151 151 if not isinstance(self.conf, (dict, list, tuple)):
152 152 map = {'paths': 'hgweb-paths'}
153 153 if not os.path.exists(self.conf):
154 154 raise error.Abort(_('config file %s not found!') % self.conf)
155 155 u.readconfig(self.conf, remap=map, trust=True)
156 156 paths = []
157 157 for name, ignored in u.configitems('hgweb-paths'):
158 158 for path in u.configlist('hgweb-paths', name):
159 159 paths.append((name, path))
160 160 elif isinstance(self.conf, (list, tuple)):
161 161 paths = self.conf
162 162 elif isinstance(self.conf, dict):
163 163 paths = self.conf.items()
164 164
165 165 repos = findrepos(paths)
166 166 for prefix, root in u.configitems('collections'):
167 167 prefix = util.pconvert(prefix)
168 168 for path in scmutil.walkrepos(root, followsym=True):
169 169 repo = os.path.normpath(path)
170 170 name = util.pconvert(repo)
171 171 if name.startswith(prefix):
172 172 name = name[len(prefix):]
173 173 repos.append((name.lstrip('/'), repo))
174 174
175 175 self.repos = repos
176 176 self.ui = u
177 177 encoding.encoding = self.ui.config('web', 'encoding')
178 178 self.style = self.ui.config('web', 'style')
179 179 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
180 180 self.stripecount = self.ui.config('web', 'stripes')
181 181 if self.stripecount:
182 182 self.stripecount = int(self.stripecount)
183 183 self._baseurl = self.ui.config('web', 'baseurl')
184 184 prefix = self.ui.config('web', 'prefix')
185 185 if prefix.startswith('/'):
186 186 prefix = prefix[1:]
187 187 if prefix.endswith('/'):
188 188 prefix = prefix[:-1]
189 189 self.prefix = prefix
190 190 self.lastrefresh = time.time()
191 191
192 192 def run(self):
193 193 if not encoding.environ.get('GATEWAY_INTERFACE',
194 194 '').startswith("CGI/1."):
195 195 raise RuntimeError("This function is only intended to be "
196 196 "called while running as a CGI script.")
197 197 wsgicgi.launch(self)
198 198
199 199 def __call__(self, env, respond):
200 200 wsgireq = requestmod.wsgirequest(env, respond)
201 201 return self.run_wsgi(wsgireq)
202 202
203 203 def read_allowed(self, ui, wsgireq):
204 204 """Check allow_read and deny_read config options of a repo's ui object
205 205 to determine user permissions. By default, with neither option set (or
206 206 both empty), allow all users to read the repo. There are two ways a
207 207 user can be denied read access: (1) deny_read is not empty, and the
208 208 user is unauthenticated or deny_read contains user (or *), and (2)
209 209 allow_read is not empty and the user is not in allow_read. Return True
210 210 if user is allowed to read the repo, else return False."""
211 211
212 212 user = wsgireq.env.get('REMOTE_USER')
213 213
214 214 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
215 215 if deny_read and (not user or ismember(ui, user, deny_read)):
216 216 return False
217 217
218 218 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
219 219 # by default, allow reading if no allow_read option has been set
220 220 if (not allow_read) or ismember(ui, user, allow_read):
221 221 return True
222 222
223 223 return False
224 224
225 225 def run_wsgi(self, wsgireq):
226 226 profile = self.ui.configbool('profiling', 'enabled')
227 227 with profiling.profile(self.ui, enabled=profile):
228 228 for r in self._runwsgi(wsgireq):
229 229 yield r
230 230
231 231 def _runwsgi(self, wsgireq):
232 req = wsgireq.req
233
232 234 try:
233 235 self.refresh()
234 236
235 237 csp, nonce = cspvalues(self.ui)
236 238 if csp:
237 239 wsgireq.headers.append(('Content-Security-Policy', csp))
238 240
239 241 virtual = wsgireq.env.get("PATH_INFO", "").strip('/')
240 242 tmpl = self.templater(wsgireq, nonce)
241 243 ctype = tmpl('mimetype', encoding=encoding.encoding)
242 244 ctype = templater.stringify(ctype)
243 245
244 246 # a static file
245 if virtual.startswith('static/') or 'static' in wsgireq.form:
247 if virtual.startswith('static/') or 'static' in req.qsparams:
246 248 if virtual.startswith('static/'):
247 249 fname = virtual[7:]
248 250 else:
249 fname = wsgireq.form['static'][0]
251 fname = req.qsparams['static']
250 252 static = self.ui.config("web", "static", None,
251 253 untrusted=False)
252 254 if not static:
253 255 tp = self.templatepath or templater.templatepaths()
254 256 if isinstance(tp, str):
255 257 tp = [tp]
256 258 static = [os.path.join(p, 'static') for p in tp]
257 259 staticfile(static, fname, wsgireq)
258 260 return []
259 261
260 262 # top-level index
261 263
262 264 repos = dict(self.repos)
263 265
264 266 if (not virtual or virtual == 'index') and virtual not in repos:
265 267 wsgireq.respond(HTTP_OK, ctype)
266 268 return self.makeindex(wsgireq, tmpl)
267 269
268 270 # nested indexes and hgwebs
269 271
270 272 if virtual.endswith('/index') and virtual not in repos:
271 273 subdir = virtual[:-len('index')]
272 274 if any(r.startswith(subdir) for r in repos):
273 275 wsgireq.respond(HTTP_OK, ctype)
274 276 return self.makeindex(wsgireq, tmpl, subdir)
275 277
276 278 def _virtualdirs():
277 279 # Check the full virtual path, each parent, and the root ('')
278 280 if virtual != '':
279 281 yield virtual
280 282
281 283 for p in util.finddirs(virtual):
282 284 yield p
283 285
284 286 yield ''
285 287
286 288 for virtualrepo in _virtualdirs():
287 289 real = repos.get(virtualrepo)
288 290 if real:
289 291 wsgireq.env['REPO_NAME'] = virtualrepo
290 292 # We have to re-parse because of updated environment
291 293 # variable.
292 294 # TODO this is kind of hacky and we should have a better
293 295 # way of doing this than with REPO_NAME side-effects.
294 296 wsgireq.req = requestmod.parserequestfromenv(
295 297 wsgireq.env, wsgireq.req.bodyfh)
296 298 try:
297 299 # ensure caller gets private copy of ui
298 300 repo = hg.repository(self.ui.copy(), real)
299 301 return hgweb_mod.hgweb(repo).run_wsgi(wsgireq)
300 302 except IOError as inst:
301 303 msg = encoding.strtolocal(inst.strerror)
302 304 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
303 305 except error.RepoError as inst:
304 306 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
305 307
306 308 # browse subdirectories
307 309 subdir = virtual + '/'
308 310 if [r for r in repos if r.startswith(subdir)]:
309 311 wsgireq.respond(HTTP_OK, ctype)
310 312 return self.makeindex(wsgireq, tmpl, subdir)
311 313
312 314 # prefixes not found
313 315 wsgireq.respond(HTTP_NOT_FOUND, ctype)
314 316 return tmpl("notfound", repo=virtual)
315 317
316 318 except ErrorResponse as err:
317 319 wsgireq.respond(err, ctype)
318 320 return tmpl('error', error=err.message or '')
319 321 finally:
320 322 tmpl = None
321 323
322 324 def makeindex(self, wsgireq, tmpl, subdir=""):
323 325
324 326 def archivelist(ui, nodeid, url):
325 327 allowed = ui.configlist("web", "allow_archive", untrusted=True)
326 328 archives = []
327 329 for typ, spec in hgweb_mod.archivespecs.iteritems():
328 330 if typ in allowed or ui.configbool("web", "allow" + typ,
329 331 untrusted=True):
330 332 archives.append({"type": typ, "extension": spec[2],
331 333 "node": nodeid, "url": url})
332 334 return archives
333 335
334 336 def rawentries(subdir="", **map):
335 337
336 338 descend = self.ui.configbool('web', 'descend')
337 339 collapse = self.ui.configbool('web', 'collapse')
338 340 seenrepos = set()
339 341 seendirs = set()
340 342 for name, path in self.repos:
341 343
342 344 if not name.startswith(subdir):
343 345 continue
344 346 name = name[len(subdir):]
345 347 directory = False
346 348
347 349 if '/' in name:
348 350 if not descend:
349 351 continue
350 352
351 353 nameparts = name.split('/')
352 354 rootname = nameparts[0]
353 355
354 356 if not collapse:
355 357 pass
356 358 elif rootname in seendirs:
357 359 continue
358 360 elif rootname in seenrepos:
359 361 pass
360 362 else:
361 363 directory = True
362 364 name = rootname
363 365
364 366 # redefine the path to refer to the directory
365 367 discarded = '/'.join(nameparts[1:])
366 368
367 369 # remove name parts plus accompanying slash
368 370 path = path[:-len(discarded) - 1]
369 371
370 372 try:
371 373 r = hg.repository(self.ui, path)
372 374 directory = False
373 375 except (IOError, error.RepoError):
374 376 pass
375 377
376 378 parts = [name]
377 379 parts.insert(0, '/' + subdir.rstrip('/'))
378 380 if wsgireq.env['SCRIPT_NAME']:
379 381 parts.insert(0, wsgireq.env['SCRIPT_NAME'])
380 382 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
381 383
382 384 # show either a directory entry or a repository
383 385 if directory:
384 386 # get the directory's time information
385 387 try:
386 388 d = (get_mtime(path), dateutil.makedate()[1])
387 389 except OSError:
388 390 continue
389 391
390 392 # add '/' to the name to make it obvious that
391 393 # the entry is a directory, not a regular repository
392 394 row = {'contact': "",
393 395 'contact_sort': "",
394 396 'name': name + '/',
395 397 'name_sort': name,
396 398 'url': url,
397 399 'description': "",
398 400 'description_sort': "",
399 401 'lastchange': d,
400 402 'lastchange_sort': d[1]-d[0],
401 403 'archives': [],
402 404 'isdirectory': True,
403 405 'labels': [],
404 406 }
405 407
406 408 seendirs.add(name)
407 409 yield row
408 410 continue
409 411
410 412 u = self.ui.copy()
411 413 try:
412 414 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
413 415 except Exception as e:
414 416 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
415 417 continue
416 418 def get(section, name, default=uimod._unset):
417 419 return u.config(section, name, default, untrusted=True)
418 420
419 421 if u.configbool("web", "hidden", untrusted=True):
420 422 continue
421 423
422 424 if not self.read_allowed(u, wsgireq):
423 425 continue
424 426
425 427 # update time with local timezone
426 428 try:
427 429 r = hg.repository(self.ui, path)
428 430 except IOError:
429 431 u.warn(_('error accessing repository at %s\n') % path)
430 432 continue
431 433 except error.RepoError:
432 434 u.warn(_('error accessing repository at %s\n') % path)
433 435 continue
434 436 try:
435 437 d = (get_mtime(r.spath), dateutil.makedate()[1])
436 438 except OSError:
437 439 continue
438 440
439 441 contact = get_contact(get)
440 442 description = get("web", "description")
441 443 seenrepos.add(name)
442 444 name = get("web", "name", name)
443 445 row = {'contact': contact or "unknown",
444 446 'contact_sort': contact.upper() or "unknown",
445 447 'name': name,
446 448 'name_sort': name,
447 449 'url': url,
448 450 'description': description or "unknown",
449 451 'description_sort': description.upper() or "unknown",
450 452 'lastchange': d,
451 453 'lastchange_sort': d[1]-d[0],
452 454 'archives': archivelist(u, "tip", url),
453 455 'isdirectory': None,
454 456 'labels': u.configlist('web', 'labels', untrusted=True),
455 457 }
456 458
457 459 yield row
458 460
459 461 sortdefault = None, False
460 462 def entries(sortcolumn="", descending=False, subdir="", **map):
461 463 rows = rawentries(subdir=subdir, **map)
462 464
463 465 if sortcolumn and sortdefault != (sortcolumn, descending):
464 466 sortkey = '%s_sort' % sortcolumn
465 467 rows = sorted(rows, key=lambda x: x[sortkey],
466 468 reverse=descending)
467 469 for row, parity in zip(rows, paritygen(self.stripecount)):
468 470 row['parity'] = parity
469 471 yield row
470 472
471 473 self.refresh()
472 474 sortable = ["name", "description", "contact", "lastchange"]
473 475 sortcolumn, descending = sortdefault
474 if 'sort' in wsgireq.form:
475 sortcolumn = wsgireq.form['sort'][0]
476 if 'sort' in wsgireq.req.qsparams:
477 sortcolum = wsgireq.req.qsparams['sort']
476 478 descending = sortcolumn.startswith('-')
477 479 if descending:
478 480 sortcolumn = sortcolumn[1:]
479 481 if sortcolumn not in sortable:
480 482 sortcolumn = ""
481 483
482 484 sort = [("sort_%s" % column,
483 485 "%s%s" % ((not descending and column == sortcolumn)
484 486 and "-" or "", column))
485 487 for column in sortable]
486 488
487 489 self.refresh()
488 490 self.updatereqenv(wsgireq.env)
489 491
490 492 return tmpl("index", entries=entries, subdir=subdir,
491 493 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
492 494 sortcolumn=sortcolumn, descending=descending,
493 495 **dict(sort))
494 496
495 497 def templater(self, wsgireq, nonce):
496 498
497 499 def motd(**map):
498 500 if self.motd is not None:
499 501 yield self.motd
500 502 else:
501 503 yield config('web', 'motd')
502 504
503 505 def config(section, name, default=uimod._unset, untrusted=True):
504 506 return self.ui.config(section, name, default, untrusted)
505 507
506 508 self.updatereqenv(wsgireq.env)
507 509
508 510 url = wsgireq.env.get('SCRIPT_NAME', '')
509 511 if not url.endswith('/'):
510 512 url += '/'
511 513
512 514 vars = {}
513 515 styles, (style, mapfile) = hgweb_mod.getstyle(wsgireq.req, config,
514 516 self.templatepath)
515 517 if style == styles[0]:
516 518 vars['style'] = style
517 519
518 520 sessionvars = webutil.sessionvars(vars, r'?')
519 521 logourl = config('web', 'logourl')
520 522 logoimg = config('web', 'logoimg')
521 523 staticurl = config('web', 'staticurl') or url + 'static/'
522 524 if not staticurl.endswith('/'):
523 525 staticurl += '/'
524 526
525 527 defaults = {
526 528 "encoding": encoding.encoding,
527 529 "motd": motd,
528 530 "url": url,
529 531 "logourl": logourl,
530 532 "logoimg": logoimg,
531 533 "staticurl": staticurl,
532 534 "sessionvars": sessionvars,
533 535 "style": style,
534 536 "nonce": nonce,
535 537 }
536 538 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
537 539 return tmpl
538 540
539 541 def updatereqenv(self, env):
540 542 if self._baseurl is not None:
541 543 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
542 544 env['SERVER_NAME'] = name
543 545 env['SERVER_PORT'] = port
544 546 env['SCRIPT_NAME'] = path
@@ -1,1411 +1,1411 b''
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import copy
11 11 import mimetypes
12 12 import os
13 13 import re
14 14
15 15 from ..i18n import _
16 16 from ..node import hex, nullid, short
17 17
18 18 from .common import (
19 19 ErrorResponse,
20 20 HTTP_FORBIDDEN,
21 21 HTTP_NOT_FOUND,
22 22 HTTP_OK,
23 23 get_contact,
24 24 paritygen,
25 25 staticfile,
26 26 )
27 27
28 28 from .. import (
29 29 archival,
30 30 dagop,
31 31 encoding,
32 32 error,
33 33 graphmod,
34 34 pycompat,
35 35 revset,
36 36 revsetlang,
37 37 scmutil,
38 38 smartset,
39 39 templater,
40 40 util,
41 41 )
42 42
43 43 from . import (
44 44 webutil,
45 45 )
46 46
47 47 __all__ = []
48 48 commands = {}
49 49
50 50 class webcommand(object):
51 51 """Decorator used to register a web command handler.
52 52
53 53 The decorator takes as its positional arguments the name/path the
54 54 command should be accessible under.
55 55
56 56 Usage:
57 57
58 58 @webcommand('mycommand')
59 59 def mycommand(web, req, tmpl):
60 60 pass
61 61 """
62 62
63 63 def __init__(self, name):
64 64 self.name = name
65 65
66 66 def __call__(self, func):
67 67 __all__.append(self.name)
68 68 commands[self.name] = func
69 69 return func
70 70
71 71 @webcommand('log')
72 72 def log(web, req, tmpl):
73 73 """
74 74 /log[/{revision}[/{path}]]
75 75 --------------------------
76 76
77 77 Show repository or file history.
78 78
79 79 For URLs of the form ``/log/{revision}``, a list of changesets starting at
80 80 the specified changeset identifier is shown. If ``{revision}`` is not
81 81 defined, the default is ``tip``. This form is equivalent to the
82 82 ``changelog`` handler.
83 83
84 84 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
85 85 file will be shown. This form is equivalent to the ``filelog`` handler.
86 86 """
87 87
88 if 'file' in req.form and req.form['file'][0]:
88 if req.req.qsparams.get('file'):
89 89 return filelog(web, req, tmpl)
90 90 else:
91 91 return changelog(web, req, tmpl)
92 92
93 93 @webcommand('rawfile')
94 94 def rawfile(web, req, tmpl):
95 95 guessmime = web.configbool('web', 'guessmime')
96 96
97 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
97 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
98 98 if not path:
99 99 content = manifest(web, req, tmpl)
100 100 req.respond(HTTP_OK, web.ctype)
101 101 return content
102 102
103 103 try:
104 104 fctx = webutil.filectx(web.repo, req)
105 105 except error.LookupError as inst:
106 106 try:
107 107 content = manifest(web, req, tmpl)
108 108 req.respond(HTTP_OK, web.ctype)
109 109 return content
110 110 except ErrorResponse:
111 111 raise inst
112 112
113 113 path = fctx.path()
114 114 text = fctx.data()
115 115 mt = 'application/binary'
116 116 if guessmime:
117 117 mt = mimetypes.guess_type(path)[0]
118 118 if mt is None:
119 119 if util.binary(text):
120 120 mt = 'application/binary'
121 121 else:
122 122 mt = 'text/plain'
123 123 if mt.startswith('text/'):
124 124 mt += '; charset="%s"' % encoding.encoding
125 125
126 126 req.respond(HTTP_OK, mt, path, body=text)
127 127 return []
128 128
129 129 def _filerevision(web, req, tmpl, fctx):
130 130 f = fctx.path()
131 131 text = fctx.data()
132 132 parity = paritygen(web.stripecount)
133 133 ishead = fctx.filerev() in fctx.filelog().headrevs()
134 134
135 135 if util.binary(text):
136 136 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
137 137 text = '(binary:%s)' % mt
138 138
139 139 def lines():
140 140 for lineno, t in enumerate(text.splitlines(True)):
141 141 yield {"line": t,
142 142 "lineid": "l%d" % (lineno + 1),
143 143 "linenumber": "% 6d" % (lineno + 1),
144 144 "parity": next(parity)}
145 145
146 146 return tmpl("filerevision",
147 147 file=f,
148 148 path=webutil.up(f),
149 149 text=lines(),
150 150 symrev=webutil.symrevorshortnode(req, fctx),
151 151 rename=webutil.renamelink(fctx),
152 152 permissions=fctx.manifest().flags(f),
153 153 ishead=int(ishead),
154 154 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
155 155
156 156 @webcommand('file')
157 157 def file(web, req, tmpl):
158 158 """
159 159 /file/{revision}[/{path}]
160 160 -------------------------
161 161
162 162 Show information about a directory or file in the repository.
163 163
164 164 Info about the ``path`` given as a URL parameter will be rendered.
165 165
166 166 If ``path`` is a directory, information about the entries in that
167 167 directory will be rendered. This form is equivalent to the ``manifest``
168 168 handler.
169 169
170 170 If ``path`` is a file, information about that file will be shown via
171 171 the ``filerevision`` template.
172 172
173 173 If ``path`` is not defined, information about the root directory will
174 174 be rendered.
175 175 """
176 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
176 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
177 177 if not path:
178 178 return manifest(web, req, tmpl)
179 179 try:
180 180 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
181 181 except error.LookupError as inst:
182 182 try:
183 183 return manifest(web, req, tmpl)
184 184 except ErrorResponse:
185 185 raise inst
186 186
187 187 def _search(web, req, tmpl):
188 188 MODE_REVISION = 'rev'
189 189 MODE_KEYWORD = 'keyword'
190 190 MODE_REVSET = 'revset'
191 191
192 192 def revsearch(ctx):
193 193 yield ctx
194 194
195 195 def keywordsearch(query):
196 196 lower = encoding.lower
197 197 qw = lower(query).split()
198 198
199 199 def revgen():
200 200 cl = web.repo.changelog
201 201 for i in xrange(len(web.repo) - 1, 0, -100):
202 202 l = []
203 203 for j in cl.revs(max(0, i - 99), i):
204 204 ctx = web.repo[j]
205 205 l.append(ctx)
206 206 l.reverse()
207 207 for e in l:
208 208 yield e
209 209
210 210 for ctx in revgen():
211 211 miss = 0
212 212 for q in qw:
213 213 if not (q in lower(ctx.user()) or
214 214 q in lower(ctx.description()) or
215 215 q in lower(" ".join(ctx.files()))):
216 216 miss = 1
217 217 break
218 218 if miss:
219 219 continue
220 220
221 221 yield ctx
222 222
223 223 def revsetsearch(revs):
224 224 for r in revs:
225 225 yield web.repo[r]
226 226
227 227 searchfuncs = {
228 228 MODE_REVISION: (revsearch, 'exact revision search'),
229 229 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
230 230 MODE_REVSET: (revsetsearch, 'revset expression search'),
231 231 }
232 232
233 233 def getsearchmode(query):
234 234 try:
235 235 ctx = web.repo[query]
236 236 except (error.RepoError, error.LookupError):
237 237 # query is not an exact revision pointer, need to
238 238 # decide if it's a revset expression or keywords
239 239 pass
240 240 else:
241 241 return MODE_REVISION, ctx
242 242
243 243 revdef = 'reverse(%s)' % query
244 244 try:
245 245 tree = revsetlang.parse(revdef)
246 246 except error.ParseError:
247 247 # can't parse to a revset tree
248 248 return MODE_KEYWORD, query
249 249
250 250 if revsetlang.depth(tree) <= 2:
251 251 # no revset syntax used
252 252 return MODE_KEYWORD, query
253 253
254 254 if any((token, (value or '')[:3]) == ('string', 're:')
255 255 for token, value, pos in revsetlang.tokenize(revdef)):
256 256 return MODE_KEYWORD, query
257 257
258 258 funcsused = revsetlang.funcsused(tree)
259 259 if not funcsused.issubset(revset.safesymbols):
260 260 return MODE_KEYWORD, query
261 261
262 262 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
263 263 try:
264 264 revs = mfunc(web.repo)
265 265 return MODE_REVSET, revs
266 266 # ParseError: wrongly placed tokens, wrongs arguments, etc
267 267 # RepoLookupError: no such revision, e.g. in 'revision:'
268 268 # Abort: bookmark/tag not exists
269 269 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
270 270 except (error.ParseError, error.RepoLookupError, error.Abort,
271 271 LookupError):
272 272 return MODE_KEYWORD, query
273 273
274 274 def changelist(**map):
275 275 count = 0
276 276
277 277 for ctx in searchfunc[0](funcarg):
278 278 count += 1
279 279 n = ctx.node()
280 280 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
281 281 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
282 282
283 283 yield tmpl('searchentry',
284 284 parity=next(parity),
285 285 changelogtag=showtags,
286 286 files=files,
287 287 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
288 288
289 289 if count >= revcount:
290 290 break
291 291
292 query = req.form['rev'][0]
292 query = req.req.qsparams['rev']
293 293 revcount = web.maxchanges
294 if 'revcount' in req.form:
294 if 'revcount' in req.req.qsparams:
295 295 try:
296 revcount = int(req.form.get('revcount', [revcount])[0])
296 revcount = int(req.req.qsparams.get('revcount', revcount))
297 297 revcount = max(revcount, 1)
298 298 tmpl.defaults['sessionvars']['revcount'] = revcount
299 299 except ValueError:
300 300 pass
301 301
302 302 lessvars = copy.copy(tmpl.defaults['sessionvars'])
303 303 lessvars['revcount'] = max(revcount // 2, 1)
304 304 lessvars['rev'] = query
305 305 morevars = copy.copy(tmpl.defaults['sessionvars'])
306 306 morevars['revcount'] = revcount * 2
307 307 morevars['rev'] = query
308 308
309 309 mode, funcarg = getsearchmode(query)
310 310
311 if 'forcekw' in req.form:
311 if 'forcekw' in req.req.qsparams:
312 312 showforcekw = ''
313 313 showunforcekw = searchfuncs[mode][1]
314 314 mode = MODE_KEYWORD
315 315 funcarg = query
316 316 else:
317 317 if mode != MODE_KEYWORD:
318 318 showforcekw = searchfuncs[MODE_KEYWORD][1]
319 319 else:
320 320 showforcekw = ''
321 321 showunforcekw = ''
322 322
323 323 searchfunc = searchfuncs[mode]
324 324
325 325 tip = web.repo['tip']
326 326 parity = paritygen(web.stripecount)
327 327
328 328 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
329 329 entries=changelist, archives=web.archivelist("tip"),
330 330 morevars=morevars, lessvars=lessvars,
331 331 modedesc=searchfunc[1],
332 332 showforcekw=showforcekw, showunforcekw=showunforcekw)
333 333
334 334 @webcommand('changelog')
335 335 def changelog(web, req, tmpl, shortlog=False):
336 336 """
337 337 /changelog[/{revision}]
338 338 -----------------------
339 339
340 340 Show information about multiple changesets.
341 341
342 342 If the optional ``revision`` URL argument is absent, information about
343 343 all changesets starting at ``tip`` will be rendered. If the ``revision``
344 344 argument is present, changesets will be shown starting from the specified
345 345 revision.
346 346
347 347 If ``revision`` is absent, the ``rev`` query string argument may be
348 348 defined. This will perform a search for changesets.
349 349
350 350 The argument for ``rev`` can be a single revision, a revision set,
351 351 or a literal keyword to search for in changeset data (equivalent to
352 352 :hg:`log -k`).
353 353
354 354 The ``revcount`` query string argument defines the maximum numbers of
355 355 changesets to render.
356 356
357 357 For non-searches, the ``changelog`` template will be rendered.
358 358 """
359 359
360 360 query = ''
361 if 'node' in req.form:
361 if 'node' in req.req.qsparams:
362 362 ctx = webutil.changectx(web.repo, req)
363 363 symrev = webutil.symrevorshortnode(req, ctx)
364 elif 'rev' in req.form:
364 elif 'rev' in req.req.qsparams:
365 365 return _search(web, req, tmpl)
366 366 else:
367 367 ctx = web.repo['tip']
368 368 symrev = 'tip'
369 369
370 370 def changelist():
371 371 revs = []
372 372 if pos != -1:
373 373 revs = web.repo.changelog.revs(pos, 0)
374 374 curcount = 0
375 375 for rev in revs:
376 376 curcount += 1
377 377 if curcount > revcount + 1:
378 378 break
379 379
380 380 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
381 381 entry['parity'] = next(parity)
382 382 yield entry
383 383
384 384 if shortlog:
385 385 revcount = web.maxshortchanges
386 386 else:
387 387 revcount = web.maxchanges
388 388
389 if 'revcount' in req.form:
389 if 'revcount' in req.req.qsparams:
390 390 try:
391 revcount = int(req.form.get('revcount', [revcount])[0])
391 revcount = int(req.req.qsparams.get('revcount', revcount))
392 392 revcount = max(revcount, 1)
393 393 tmpl.defaults['sessionvars']['revcount'] = revcount
394 394 except ValueError:
395 395 pass
396 396
397 397 lessvars = copy.copy(tmpl.defaults['sessionvars'])
398 398 lessvars['revcount'] = max(revcount // 2, 1)
399 399 morevars = copy.copy(tmpl.defaults['sessionvars'])
400 400 morevars['revcount'] = revcount * 2
401 401
402 402 count = len(web.repo)
403 403 pos = ctx.rev()
404 404 parity = paritygen(web.stripecount)
405 405
406 406 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
407 407
408 408 entries = list(changelist())
409 409 latestentry = entries[:1]
410 410 if len(entries) > revcount:
411 411 nextentry = entries[-1:]
412 412 entries = entries[:-1]
413 413 else:
414 414 nextentry = []
415 415
416 416 return tmpl('shortlog' if shortlog else 'changelog', changenav=changenav,
417 417 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
418 418 entries=entries,
419 419 latestentry=latestentry, nextentry=nextentry,
420 420 archives=web.archivelist("tip"), revcount=revcount,
421 421 morevars=morevars, lessvars=lessvars, query=query)
422 422
423 423 @webcommand('shortlog')
424 424 def shortlog(web, req, tmpl):
425 425 """
426 426 /shortlog
427 427 ---------
428 428
429 429 Show basic information about a set of changesets.
430 430
431 431 This accepts the same parameters as the ``changelog`` handler. The only
432 432 difference is the ``shortlog`` template will be rendered instead of the
433 433 ``changelog`` template.
434 434 """
435 435 return changelog(web, req, tmpl, shortlog=True)
436 436
437 437 @webcommand('changeset')
438 438 def changeset(web, req, tmpl):
439 439 """
440 440 /changeset[/{revision}]
441 441 -----------------------
442 442
443 443 Show information about a single changeset.
444 444
445 445 A URL path argument is the changeset identifier to show. See ``hg help
446 446 revisions`` for possible values. If not defined, the ``tip`` changeset
447 447 will be shown.
448 448
449 449 The ``changeset`` template is rendered. Contents of the ``changesettag``,
450 450 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
451 451 templates related to diffs may all be used to produce the output.
452 452 """
453 453 ctx = webutil.changectx(web.repo, req)
454 454
455 455 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
456 456
457 457 rev = webcommand('rev')(changeset)
458 458
459 459 def decodepath(path):
460 460 """Hook for mapping a path in the repository to a path in the
461 461 working copy.
462 462
463 463 Extensions (e.g., largefiles) can override this to remap files in
464 464 the virtual file system presented by the manifest command below."""
465 465 return path
466 466
467 467 @webcommand('manifest')
468 468 def manifest(web, req, tmpl):
469 469 """
470 470 /manifest[/{revision}[/{path}]]
471 471 -------------------------------
472 472
473 473 Show information about a directory.
474 474
475 475 If the URL path arguments are omitted, information about the root
476 476 directory for the ``tip`` changeset will be shown.
477 477
478 478 Because this handler can only show information for directories, it
479 479 is recommended to use the ``file`` handler instead, as it can handle both
480 480 directories and files.
481 481
482 482 The ``manifest`` template will be rendered for this handler.
483 483 """
484 if 'node' in req.form:
484 if 'node' in req.req.qsparams:
485 485 ctx = webutil.changectx(web.repo, req)
486 486 symrev = webutil.symrevorshortnode(req, ctx)
487 487 else:
488 488 ctx = web.repo['tip']
489 489 symrev = 'tip'
490 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
490 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
491 491 mf = ctx.manifest()
492 492 node = ctx.node()
493 493
494 494 files = {}
495 495 dirs = {}
496 496 parity = paritygen(web.stripecount)
497 497
498 498 if path and path[-1:] != "/":
499 499 path += "/"
500 500 l = len(path)
501 501 abspath = "/" + path
502 502
503 503 for full, n in mf.iteritems():
504 504 # the virtual path (working copy path) used for the full
505 505 # (repository) path
506 506 f = decodepath(full)
507 507
508 508 if f[:l] != path:
509 509 continue
510 510 remain = f[l:]
511 511 elements = remain.split('/')
512 512 if len(elements) == 1:
513 513 files[remain] = full
514 514 else:
515 515 h = dirs # need to retain ref to dirs (root)
516 516 for elem in elements[0:-1]:
517 517 if elem not in h:
518 518 h[elem] = {}
519 519 h = h[elem]
520 520 if len(h) > 1:
521 521 break
522 522 h[None] = None # denotes files present
523 523
524 524 if mf and not files and not dirs:
525 525 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
526 526
527 527 def filelist(**map):
528 528 for f in sorted(files):
529 529 full = files[f]
530 530
531 531 fctx = ctx.filectx(full)
532 532 yield {"file": full,
533 533 "parity": next(parity),
534 534 "basename": f,
535 535 "date": fctx.date(),
536 536 "size": fctx.size(),
537 537 "permissions": mf.flags(full)}
538 538
539 539 def dirlist(**map):
540 540 for d in sorted(dirs):
541 541
542 542 emptydirs = []
543 543 h = dirs[d]
544 544 while isinstance(h, dict) and len(h) == 1:
545 545 k, v = next(iter(h.items()))
546 546 if v:
547 547 emptydirs.append(k)
548 548 h = v
549 549
550 550 path = "%s%s" % (abspath, d)
551 551 yield {"parity": next(parity),
552 552 "path": path,
553 553 "emptydirs": "/".join(emptydirs),
554 554 "basename": d}
555 555
556 556 return tmpl("manifest",
557 557 symrev=symrev,
558 558 path=abspath,
559 559 up=webutil.up(abspath),
560 560 upparity=next(parity),
561 561 fentries=filelist,
562 562 dentries=dirlist,
563 563 archives=web.archivelist(hex(node)),
564 564 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
565 565
566 566 @webcommand('tags')
567 567 def tags(web, req, tmpl):
568 568 """
569 569 /tags
570 570 -----
571 571
572 572 Show information about tags.
573 573
574 574 No arguments are accepted.
575 575
576 576 The ``tags`` template is rendered.
577 577 """
578 578 i = list(reversed(web.repo.tagslist()))
579 579 parity = paritygen(web.stripecount)
580 580
581 581 def entries(notip, latestonly, **map):
582 582 t = i
583 583 if notip:
584 584 t = [(k, n) for k, n in i if k != "tip"]
585 585 if latestonly:
586 586 t = t[:1]
587 587 for k, n in t:
588 588 yield {"parity": next(parity),
589 589 "tag": k,
590 590 "date": web.repo[n].date(),
591 591 "node": hex(n)}
592 592
593 593 return tmpl("tags",
594 594 node=hex(web.repo.changelog.tip()),
595 595 entries=lambda **x: entries(False, False, **x),
596 596 entriesnotip=lambda **x: entries(True, False, **x),
597 597 latestentry=lambda **x: entries(True, True, **x))
598 598
599 599 @webcommand('bookmarks')
600 600 def bookmarks(web, req, tmpl):
601 601 """
602 602 /bookmarks
603 603 ----------
604 604
605 605 Show information about bookmarks.
606 606
607 607 No arguments are accepted.
608 608
609 609 The ``bookmarks`` template is rendered.
610 610 """
611 611 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
612 612 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
613 613 i = sorted(i, key=sortkey, reverse=True)
614 614 parity = paritygen(web.stripecount)
615 615
616 616 def entries(latestonly, **map):
617 617 t = i
618 618 if latestonly:
619 619 t = i[:1]
620 620 for k, n in t:
621 621 yield {"parity": next(parity),
622 622 "bookmark": k,
623 623 "date": web.repo[n].date(),
624 624 "node": hex(n)}
625 625
626 626 if i:
627 627 latestrev = i[0][1]
628 628 else:
629 629 latestrev = -1
630 630
631 631 return tmpl("bookmarks",
632 632 node=hex(web.repo.changelog.tip()),
633 633 lastchange=[{"date": web.repo[latestrev].date()}],
634 634 entries=lambda **x: entries(latestonly=False, **x),
635 635 latestentry=lambda **x: entries(latestonly=True, **x))
636 636
637 637 @webcommand('branches')
638 638 def branches(web, req, tmpl):
639 639 """
640 640 /branches
641 641 ---------
642 642
643 643 Show information about branches.
644 644
645 645 All known branches are contained in the output, even closed branches.
646 646
647 647 No arguments are accepted.
648 648
649 649 The ``branches`` template is rendered.
650 650 """
651 651 entries = webutil.branchentries(web.repo, web.stripecount)
652 652 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
653 653 return tmpl('branches', node=hex(web.repo.changelog.tip()),
654 654 entries=entries, latestentry=latestentry)
655 655
656 656 @webcommand('summary')
657 657 def summary(web, req, tmpl):
658 658 """
659 659 /summary
660 660 --------
661 661
662 662 Show a summary of repository state.
663 663
664 664 Information about the latest changesets, bookmarks, tags, and branches
665 665 is captured by this handler.
666 666
667 667 The ``summary`` template is rendered.
668 668 """
669 669 i = reversed(web.repo.tagslist())
670 670
671 671 def tagentries(**map):
672 672 parity = paritygen(web.stripecount)
673 673 count = 0
674 674 for k, n in i:
675 675 if k == "tip": # skip tip
676 676 continue
677 677
678 678 count += 1
679 679 if count > 10: # limit to 10 tags
680 680 break
681 681
682 682 yield tmpl("tagentry",
683 683 parity=next(parity),
684 684 tag=k,
685 685 node=hex(n),
686 686 date=web.repo[n].date())
687 687
688 688 def bookmarks(**map):
689 689 parity = paritygen(web.stripecount)
690 690 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
691 691 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
692 692 marks = sorted(marks, key=sortkey, reverse=True)
693 693 for k, n in marks[:10]: # limit to 10 bookmarks
694 694 yield {'parity': next(parity),
695 695 'bookmark': k,
696 696 'date': web.repo[n].date(),
697 697 'node': hex(n)}
698 698
699 699 def changelist(**map):
700 700 parity = paritygen(web.stripecount, offset=start - end)
701 701 l = [] # build a list in forward order for efficiency
702 702 revs = []
703 703 if start < end:
704 704 revs = web.repo.changelog.revs(start, end - 1)
705 705 for i in revs:
706 706 ctx = web.repo[i]
707 707
708 708 l.append(tmpl(
709 709 'shortlogentry',
710 710 parity=next(parity),
711 711 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
712 712
713 713 for entry in reversed(l):
714 714 yield entry
715 715
716 716 tip = web.repo['tip']
717 717 count = len(web.repo)
718 718 start = max(0, count - web.maxchanges)
719 719 end = min(count, start + web.maxchanges)
720 720
721 721 desc = web.config("web", "description")
722 722 if not desc:
723 723 desc = 'unknown'
724 724 return tmpl("summary",
725 725 desc=desc,
726 726 owner=get_contact(web.config) or "unknown",
727 727 lastchange=tip.date(),
728 728 tags=tagentries,
729 729 bookmarks=bookmarks,
730 730 branches=webutil.branchentries(web.repo, web.stripecount, 10),
731 731 shortlog=changelist,
732 732 node=tip.hex(),
733 733 symrev='tip',
734 734 archives=web.archivelist("tip"),
735 735 labels=web.configlist('web', 'labels'))
736 736
737 737 @webcommand('filediff')
738 738 def filediff(web, req, tmpl):
739 739 """
740 740 /diff/{revision}/{path}
741 741 -----------------------
742 742
743 743 Show how a file changed in a particular commit.
744 744
745 745 The ``filediff`` template is rendered.
746 746
747 747 This handler is registered under both the ``/diff`` and ``/filediff``
748 748 paths. ``/diff`` is used in modern code.
749 749 """
750 750 fctx, ctx = None, None
751 751 try:
752 752 fctx = webutil.filectx(web.repo, req)
753 753 except LookupError:
754 754 ctx = webutil.changectx(web.repo, req)
755 path = webutil.cleanpath(web.repo, req.form['file'][0])
755 path = webutil.cleanpath(web.repo, req.req.qsparams['file'])
756 756 if path not in ctx.files():
757 757 raise
758 758
759 759 if fctx is not None:
760 760 path = fctx.path()
761 761 ctx = fctx.changectx()
762 762 basectx = ctx.p1()
763 763
764 764 style = web.config('web', 'style')
765 765 if 'style' in req.req.qsparams:
766 766 style = req.req.qsparams['style']
767 767
768 768 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
769 769 if fctx is not None:
770 770 rename = webutil.renamelink(fctx)
771 771 ctx = fctx
772 772 else:
773 773 rename = []
774 774 ctx = ctx
775 775 return tmpl("filediff",
776 776 file=path,
777 777 symrev=webutil.symrevorshortnode(req, ctx),
778 778 rename=rename,
779 779 diff=diffs,
780 780 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
781 781
782 782 diff = webcommand('diff')(filediff)
783 783
784 784 @webcommand('comparison')
785 785 def comparison(web, req, tmpl):
786 786 """
787 787 /comparison/{revision}/{path}
788 788 -----------------------------
789 789
790 790 Show a comparison between the old and new versions of a file from changes
791 791 made on a particular revision.
792 792
793 793 This is similar to the ``diff`` handler. However, this form features
794 794 a split or side-by-side diff rather than a unified diff.
795 795
796 796 The ``context`` query string argument can be used to control the lines of
797 797 context in the diff.
798 798
799 799 The ``filecomparison`` template is rendered.
800 800 """
801 801 ctx = webutil.changectx(web.repo, req)
802 if 'file' not in req.form:
802 if 'file' not in req.req.qsparams:
803 803 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
804 path = webutil.cleanpath(web.repo, req.form['file'][0])
804 path = webutil.cleanpath(web.repo, req.req.qsparams['file'])
805 805
806 806 parsecontext = lambda v: v == 'full' and -1 or int(v)
807 if 'context' in req.form:
808 context = parsecontext(req.form['context'][0])
807 if 'context' in req.req.qsparams:
808 context = parsecontext(req.req.qsparams['context'])
809 809 else:
810 810 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
811 811
812 812 def filelines(f):
813 813 if f.isbinary():
814 814 mt = mimetypes.guess_type(f.path())[0]
815 815 if not mt:
816 816 mt = 'application/octet-stream'
817 817 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
818 818 return f.data().splitlines()
819 819
820 820 fctx = None
821 821 parent = ctx.p1()
822 822 leftrev = parent.rev()
823 823 leftnode = parent.node()
824 824 rightrev = ctx.rev()
825 825 rightnode = ctx.node()
826 826 if path in ctx:
827 827 fctx = ctx[path]
828 828 rightlines = filelines(fctx)
829 829 if path not in parent:
830 830 leftlines = ()
831 831 else:
832 832 pfctx = parent[path]
833 833 leftlines = filelines(pfctx)
834 834 else:
835 835 rightlines = ()
836 836 pfctx = ctx.parents()[0][path]
837 837 leftlines = filelines(pfctx)
838 838
839 839 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
840 840 if fctx is not None:
841 841 rename = webutil.renamelink(fctx)
842 842 ctx = fctx
843 843 else:
844 844 rename = []
845 845 ctx = ctx
846 846 return tmpl('filecomparison',
847 847 file=path,
848 848 symrev=webutil.symrevorshortnode(req, ctx),
849 849 rename=rename,
850 850 leftrev=leftrev,
851 851 leftnode=hex(leftnode),
852 852 rightrev=rightrev,
853 853 rightnode=hex(rightnode),
854 854 comparison=comparison,
855 855 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
856 856
857 857 @webcommand('annotate')
858 858 def annotate(web, req, tmpl):
859 859 """
860 860 /annotate/{revision}/{path}
861 861 ---------------------------
862 862
863 863 Show changeset information for each line in a file.
864 864
865 865 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
866 866 ``ignoreblanklines`` query string arguments have the same meaning as
867 867 their ``[annotate]`` config equivalents. It uses the hgrc boolean
868 868 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
869 869 false and ``1`` and ``true`` are true. If not defined, the server
870 870 default settings are used.
871 871
872 872 The ``fileannotate`` template is rendered.
873 873 """
874 874 fctx = webutil.filectx(web.repo, req)
875 875 f = fctx.path()
876 876 parity = paritygen(web.stripecount)
877 877 ishead = fctx.filerev() in fctx.filelog().headrevs()
878 878
879 879 # parents() is called once per line and several lines likely belong to
880 880 # same revision. So it is worth caching.
881 881 # TODO there are still redundant operations within basefilectx.parents()
882 882 # and from the fctx.annotate() call itself that could be cached.
883 883 parentscache = {}
884 884 def parents(f):
885 885 rev = f.rev()
886 886 if rev not in parentscache:
887 887 parentscache[rev] = []
888 888 for p in f.parents():
889 889 entry = {
890 890 'node': p.hex(),
891 891 'rev': p.rev(),
892 892 }
893 893 parentscache[rev].append(entry)
894 894
895 895 for p in parentscache[rev]:
896 896 yield p
897 897
898 898 def annotate(**map):
899 899 if fctx.isbinary():
900 900 mt = (mimetypes.guess_type(fctx.path())[0]
901 901 or 'application/octet-stream')
902 902 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
903 903 else:
904 904 lines = webutil.annotate(req, fctx, web.repo.ui)
905 905
906 906 previousrev = None
907 907 blockparitygen = paritygen(1)
908 908 for lineno, (aline, l) in enumerate(lines):
909 909 f = aline.fctx
910 910 rev = f.rev()
911 911 if rev != previousrev:
912 912 blockhead = True
913 913 blockparity = next(blockparitygen)
914 914 else:
915 915 blockhead = None
916 916 previousrev = rev
917 917 yield {"parity": next(parity),
918 918 "node": f.hex(),
919 919 "rev": rev,
920 920 "author": f.user(),
921 921 "parents": parents(f),
922 922 "desc": f.description(),
923 923 "extra": f.extra(),
924 924 "file": f.path(),
925 925 "blockhead": blockhead,
926 926 "blockparity": blockparity,
927 927 "targetline": aline.lineno,
928 928 "line": l,
929 929 "lineno": lineno + 1,
930 930 "lineid": "l%d" % (lineno + 1),
931 931 "linenumber": "% 6d" % (lineno + 1),
932 932 "revdate": f.date()}
933 933
934 934 diffopts = webutil.difffeatureopts(req, web.repo.ui, 'annotate')
935 935 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
936 936
937 937 return tmpl("fileannotate",
938 938 file=f,
939 939 annotate=annotate,
940 940 path=webutil.up(f),
941 941 symrev=webutil.symrevorshortnode(req, fctx),
942 942 rename=webutil.renamelink(fctx),
943 943 permissions=fctx.manifest().flags(f),
944 944 ishead=int(ishead),
945 945 diffopts=diffopts,
946 946 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
947 947
948 948 @webcommand('filelog')
949 949 def filelog(web, req, tmpl):
950 950 """
951 951 /filelog/{revision}/{path}
952 952 --------------------------
953 953
954 954 Show information about the history of a file in the repository.
955 955
956 956 The ``revcount`` query string argument can be defined to control the
957 957 maximum number of entries to show.
958 958
959 959 The ``filelog`` template will be rendered.
960 960 """
961 961
962 962 try:
963 963 fctx = webutil.filectx(web.repo, req)
964 964 f = fctx.path()
965 965 fl = fctx.filelog()
966 966 except error.LookupError:
967 f = webutil.cleanpath(web.repo, req.form['file'][0])
967 f = webutil.cleanpath(web.repo, req.req.qsparams['file'])
968 968 fl = web.repo.file(f)
969 969 numrevs = len(fl)
970 970 if not numrevs: # file doesn't exist at all
971 971 raise
972 972 rev = webutil.changectx(web.repo, req).rev()
973 973 first = fl.linkrev(0)
974 974 if rev < first: # current rev is from before file existed
975 975 raise
976 976 frev = numrevs - 1
977 977 while fl.linkrev(frev) > rev:
978 978 frev -= 1
979 979 fctx = web.repo.filectx(f, fl.linkrev(frev))
980 980
981 981 revcount = web.maxshortchanges
982 if 'revcount' in req.form:
982 if 'revcount' in req.req.qsparams:
983 983 try:
984 revcount = int(req.form.get('revcount', [revcount])[0])
984 revcount = int(req.req.qsparams.get('revcount', revcount))
985 985 revcount = max(revcount, 1)
986 986 tmpl.defaults['sessionvars']['revcount'] = revcount
987 987 except ValueError:
988 988 pass
989 989
990 990 lrange = webutil.linerange(req)
991 991
992 992 lessvars = copy.copy(tmpl.defaults['sessionvars'])
993 993 lessvars['revcount'] = max(revcount // 2, 1)
994 994 morevars = copy.copy(tmpl.defaults['sessionvars'])
995 995 morevars['revcount'] = revcount * 2
996 996
997 patch = 'patch' in req.form
997 patch = 'patch' in req.req.qsparams
998 998 if patch:
999 lessvars['patch'] = morevars['patch'] = req.form['patch'][0]
1000 descend = 'descend' in req.form
999 lessvars['patch'] = morevars['patch'] = req.req.qsparams['patch']
1000 descend = 'descend' in req.req.qsparams
1001 1001 if descend:
1002 lessvars['descend'] = morevars['descend'] = req.form['descend'][0]
1002 lessvars['descend'] = morevars['descend'] = req.req.qsparams['descend']
1003 1003
1004 1004 count = fctx.filerev() + 1
1005 1005 start = max(0, count - revcount) # first rev on this page
1006 1006 end = min(count, start + revcount) # last rev on this page
1007 1007 parity = paritygen(web.stripecount, offset=start - end)
1008 1008
1009 1009 repo = web.repo
1010 1010 revs = fctx.filelog().revs(start, end - 1)
1011 1011 entries = []
1012 1012
1013 1013 diffstyle = web.config('web', 'style')
1014 1014 if 'style' in req.req.qsparams:
1015 1015 diffstyle = req.req.qsparams['style']
1016 1016
1017 1017 def diff(fctx, linerange=None):
1018 1018 ctx = fctx.changectx()
1019 1019 basectx = ctx.p1()
1020 1020 path = fctx.path()
1021 1021 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1022 1022 linerange=linerange,
1023 1023 lineidprefix='%s-' % ctx.hex()[:12])
1024 1024
1025 1025 linerange = None
1026 1026 if lrange is not None:
1027 1027 linerange = webutil.formatlinerange(*lrange)
1028 1028 # deactivate numeric nav links when linerange is specified as this
1029 1029 # would required a dedicated "revnav" class
1030 1030 nav = None
1031 1031 if descend:
1032 1032 it = dagop.blockdescendants(fctx, *lrange)
1033 1033 else:
1034 1034 it = dagop.blockancestors(fctx, *lrange)
1035 1035 for i, (c, lr) in enumerate(it, 1):
1036 1036 diffs = None
1037 1037 if patch:
1038 1038 diffs = diff(c, linerange=lr)
1039 1039 # follow renames accross filtered (not in range) revisions
1040 1040 path = c.path()
1041 1041 entries.append(dict(
1042 1042 parity=next(parity),
1043 1043 filerev=c.rev(),
1044 1044 file=path,
1045 1045 diff=diffs,
1046 1046 linerange=webutil.formatlinerange(*lr),
1047 1047 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1048 1048 if i == revcount:
1049 1049 break
1050 1050 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1051 1051 morevars['linerange'] = lessvars['linerange']
1052 1052 else:
1053 1053 for i in revs:
1054 1054 iterfctx = fctx.filectx(i)
1055 1055 diffs = None
1056 1056 if patch:
1057 1057 diffs = diff(iterfctx)
1058 1058 entries.append(dict(
1059 1059 parity=next(parity),
1060 1060 filerev=i,
1061 1061 file=f,
1062 1062 diff=diffs,
1063 1063 rename=webutil.renamelink(iterfctx),
1064 1064 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1065 1065 entries.reverse()
1066 1066 revnav = webutil.filerevnav(web.repo, fctx.path())
1067 1067 nav = revnav.gen(end - 1, revcount, count)
1068 1068
1069 1069 latestentry = entries[:1]
1070 1070
1071 1071 return tmpl("filelog",
1072 1072 file=f,
1073 1073 nav=nav,
1074 1074 symrev=webutil.symrevorshortnode(req, fctx),
1075 1075 entries=entries,
1076 1076 descend=descend,
1077 1077 patch=patch,
1078 1078 latestentry=latestentry,
1079 1079 linerange=linerange,
1080 1080 revcount=revcount,
1081 1081 morevars=morevars,
1082 1082 lessvars=lessvars,
1083 1083 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1084 1084
1085 1085 @webcommand('archive')
1086 1086 def archive(web, req, tmpl):
1087 1087 """
1088 1088 /archive/{revision}.{format}[/{path}]
1089 1089 -------------------------------------
1090 1090
1091 1091 Obtain an archive of repository content.
1092 1092
1093 1093 The content and type of the archive is defined by a URL path parameter.
1094 1094 ``format`` is the file extension of the archive type to be generated. e.g.
1095 1095 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1096 1096 server configuration.
1097 1097
1098 1098 The optional ``path`` URL parameter controls content to include in the
1099 1099 archive. If omitted, every file in the specified revision is present in the
1100 1100 archive. If included, only the specified file or contents of the specified
1101 1101 directory will be included in the archive.
1102 1102
1103 1103 No template is used for this handler. Raw, binary content is generated.
1104 1104 """
1105 1105
1106 type_ = req.form.get('type', [None])[0]
1106 type_ = req.req.qsparams.get('type')
1107 1107 allowed = web.configlist("web", "allow_archive")
1108 key = req.form['node'][0]
1108 key = req.req.qsparams['node']
1109 1109
1110 1110 if type_ not in web.archivespecs:
1111 1111 msg = 'Unsupported archive type: %s' % type_
1112 1112 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1113 1113
1114 1114 if not ((type_ in allowed or
1115 1115 web.configbool("web", "allow" + type_))):
1116 1116 msg = 'Archive type not allowed: %s' % type_
1117 1117 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1118 1118
1119 1119 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1120 1120 cnode = web.repo.lookup(key)
1121 1121 arch_version = key
1122 1122 if cnode == key or key == 'tip':
1123 1123 arch_version = short(cnode)
1124 1124 name = "%s-%s" % (reponame, arch_version)
1125 1125
1126 1126 ctx = webutil.changectx(web.repo, req)
1127 1127 pats = []
1128 1128 match = scmutil.match(ctx, [])
1129 file = req.form.get('file', None)
1129 file = req.req.qsparams.get('file')
1130 1130 if file:
1131 pats = ['path:' + file[0]]
1131 pats = ['path:' + file]
1132 1132 match = scmutil.match(ctx, pats, default='path')
1133 1133 if pats:
1134 1134 files = [f for f in ctx.manifest().keys() if match(f)]
1135 1135 if not files:
1136 1136 raise ErrorResponse(HTTP_NOT_FOUND,
1137 'file(s) not found: %s' % file[0])
1137 'file(s) not found: %s' % file)
1138 1138
1139 1139 mimetype, artype, extension, encoding = web.archivespecs[type_]
1140 1140 headers = [
1141 1141 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1142 1142 ]
1143 1143 if encoding:
1144 1144 headers.append(('Content-Encoding', encoding))
1145 1145 req.headers.extend(headers)
1146 1146 req.respond(HTTP_OK, mimetype)
1147 1147
1148 1148 archival.archive(web.repo, req, cnode, artype, prefix=name,
1149 1149 matchfn=match,
1150 1150 subrepos=web.configbool("web", "archivesubrepos"))
1151 1151 return []
1152 1152
1153 1153
1154 1154 @webcommand('static')
1155 1155 def static(web, req, tmpl):
1156 fname = req.form['file'][0]
1156 fname = req.req.qsparams['file']
1157 1157 # a repo owner may set web.static in .hg/hgrc to get any file
1158 1158 # readable by the user running the CGI script
1159 1159 static = web.config("web", "static", None, untrusted=False)
1160 1160 if not static:
1161 1161 tp = web.templatepath or templater.templatepaths()
1162 1162 if isinstance(tp, str):
1163 1163 tp = [tp]
1164 1164 static = [os.path.join(p, 'static') for p in tp]
1165 1165 staticfile(static, fname, req)
1166 1166 return []
1167 1167
1168 1168 @webcommand('graph')
1169 1169 def graph(web, req, tmpl):
1170 1170 """
1171 1171 /graph[/{revision}]
1172 1172 -------------------
1173 1173
1174 1174 Show information about the graphical topology of the repository.
1175 1175
1176 1176 Information rendered by this handler can be used to create visual
1177 1177 representations of repository topology.
1178 1178
1179 1179 The ``revision`` URL parameter controls the starting changeset. If it's
1180 1180 absent, the default is ``tip``.
1181 1181
1182 1182 The ``revcount`` query string argument can define the number of changesets
1183 1183 to show information for.
1184 1184
1185 1185 The ``graphtop`` query string argument can specify the starting changeset
1186 1186 for producing ``jsdata`` variable that is used for rendering graph in
1187 1187 JavaScript. By default it has the same value as ``revision``.
1188 1188
1189 1189 This handler will render the ``graph`` template.
1190 1190 """
1191 1191
1192 if 'node' in req.form:
1192 if 'node' in req.req.qsparams:
1193 1193 ctx = webutil.changectx(web.repo, req)
1194 1194 symrev = webutil.symrevorshortnode(req, ctx)
1195 1195 else:
1196 1196 ctx = web.repo['tip']
1197 1197 symrev = 'tip'
1198 1198 rev = ctx.rev()
1199 1199
1200 1200 bg_height = 39
1201 1201 revcount = web.maxshortchanges
1202 if 'revcount' in req.form:
1202 if 'revcount' in req.req.qsparams:
1203 1203 try:
1204 revcount = int(req.form.get('revcount', [revcount])[0])
1204 revcount = int(req.req.qsparams.get('revcount', revcount))
1205 1205 revcount = max(revcount, 1)
1206 1206 tmpl.defaults['sessionvars']['revcount'] = revcount
1207 1207 except ValueError:
1208 1208 pass
1209 1209
1210 1210 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1211 1211 lessvars['revcount'] = max(revcount // 2, 1)
1212 1212 morevars = copy.copy(tmpl.defaults['sessionvars'])
1213 1213 morevars['revcount'] = revcount * 2
1214 1214
1215 graphtop = req.form.get('graphtop', [ctx.hex()])[0]
1215 graphtop = req.req.qsparams.get('graphtop', ctx.hex())
1216 1216 graphvars = copy.copy(tmpl.defaults['sessionvars'])
1217 1217 graphvars['graphtop'] = graphtop
1218 1218
1219 1219 count = len(web.repo)
1220 1220 pos = rev
1221 1221
1222 1222 uprev = min(max(0, count - 1), rev + revcount)
1223 1223 downrev = max(0, rev - revcount)
1224 1224 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1225 1225
1226 1226 tree = []
1227 1227 nextentry = []
1228 1228 lastrev = 0
1229 1229 if pos != -1:
1230 1230 allrevs = web.repo.changelog.revs(pos, 0)
1231 1231 revs = []
1232 1232 for i in allrevs:
1233 1233 revs.append(i)
1234 1234 if len(revs) >= revcount + 1:
1235 1235 break
1236 1236
1237 1237 if len(revs) > revcount:
1238 1238 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1239 1239 revs = revs[:-1]
1240 1240
1241 1241 lastrev = revs[-1]
1242 1242
1243 1243 # We have to feed a baseset to dagwalker as it is expecting smartset
1244 1244 # object. This does not have a big impact on hgweb performance itself
1245 1245 # since hgweb graphing code is not itself lazy yet.
1246 1246 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1247 1247 # As we said one line above... not lazy.
1248 1248 tree = list(item for item in graphmod.colored(dag, web.repo)
1249 1249 if item[1] == graphmod.CHANGESET)
1250 1250
1251 1251 def nodecurrent(ctx):
1252 1252 wpnodes = web.repo.dirstate.parents()
1253 1253 if wpnodes[1] == nullid:
1254 1254 wpnodes = wpnodes[:1]
1255 1255 if ctx.node() in wpnodes:
1256 1256 return '@'
1257 1257 return ''
1258 1258
1259 1259 def nodesymbol(ctx):
1260 1260 if ctx.obsolete():
1261 1261 return 'x'
1262 1262 elif ctx.isunstable():
1263 1263 return '*'
1264 1264 elif ctx.closesbranch():
1265 1265 return '_'
1266 1266 else:
1267 1267 return 'o'
1268 1268
1269 1269 def fulltree():
1270 1270 pos = web.repo[graphtop].rev()
1271 1271 tree = []
1272 1272 if pos != -1:
1273 1273 revs = web.repo.changelog.revs(pos, lastrev)
1274 1274 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1275 1275 tree = list(item for item in graphmod.colored(dag, web.repo)
1276 1276 if item[1] == graphmod.CHANGESET)
1277 1277 return tree
1278 1278
1279 1279 def jsdata():
1280 1280 return [{'node': pycompat.bytestr(ctx),
1281 1281 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1282 1282 'vertex': vtx,
1283 1283 'edges': edges}
1284 1284 for (id, type, ctx, vtx, edges) in fulltree()]
1285 1285
1286 1286 def nodes():
1287 1287 parity = paritygen(web.stripecount)
1288 1288 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1289 1289 entry = webutil.commonentry(web.repo, ctx)
1290 1290 edgedata = [{'col': edge[0],
1291 1291 'nextcol': edge[1],
1292 1292 'color': (edge[2] - 1) % 6 + 1,
1293 1293 'width': edge[3],
1294 1294 'bcolor': edge[4]}
1295 1295 for edge in edges]
1296 1296
1297 1297 entry.update({'col': vtx[0],
1298 1298 'color': (vtx[1] - 1) % 6 + 1,
1299 1299 'parity': next(parity),
1300 1300 'edges': edgedata,
1301 1301 'row': row,
1302 1302 'nextrow': row + 1})
1303 1303
1304 1304 yield entry
1305 1305
1306 1306 rows = len(tree)
1307 1307
1308 1308 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1309 1309 uprev=uprev,
1310 1310 lessvars=lessvars, morevars=morevars, downrev=downrev,
1311 1311 graphvars=graphvars,
1312 1312 rows=rows,
1313 1313 bg_height=bg_height,
1314 1314 changesets=count,
1315 1315 nextentry=nextentry,
1316 1316 jsdata=lambda **x: jsdata(),
1317 1317 nodes=lambda **x: nodes(),
1318 1318 node=ctx.hex(), changenav=changenav)
1319 1319
1320 1320 def _getdoc(e):
1321 1321 doc = e[0].__doc__
1322 1322 if doc:
1323 1323 doc = _(doc).partition('\n')[0]
1324 1324 else:
1325 1325 doc = _('(no help text available)')
1326 1326 return doc
1327 1327
1328 1328 @webcommand('help')
1329 1329 def help(web, req, tmpl):
1330 1330 """
1331 1331 /help[/{topic}]
1332 1332 ---------------
1333 1333
1334 1334 Render help documentation.
1335 1335
1336 1336 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1337 1337 is defined, that help topic will be rendered. If not, an index of
1338 1338 available help topics will be rendered.
1339 1339
1340 1340 The ``help`` template will be rendered when requesting help for a topic.
1341 1341 ``helptopics`` will be rendered for the index of help topics.
1342 1342 """
1343 1343 from .. import commands, help as helpmod # avoid cycle
1344 1344
1345 topicname = req.form.get('node', [None])[0]
1345 topicname = req.req.qsparams.get('node')
1346 1346 if not topicname:
1347 1347 def topics(**map):
1348 1348 for entries, summary, _doc in helpmod.helptable:
1349 1349 yield {'topic': entries[0], 'summary': summary}
1350 1350
1351 1351 early, other = [], []
1352 1352 primary = lambda s: s.partition('|')[0]
1353 1353 for c, e in commands.table.iteritems():
1354 1354 doc = _getdoc(e)
1355 1355 if 'DEPRECATED' in doc or c.startswith('debug'):
1356 1356 continue
1357 1357 cmd = primary(c)
1358 1358 if cmd.startswith('^'):
1359 1359 early.append((cmd[1:], doc))
1360 1360 else:
1361 1361 other.append((cmd, doc))
1362 1362
1363 1363 early.sort()
1364 1364 other.sort()
1365 1365
1366 1366 def earlycommands(**map):
1367 1367 for c, doc in early:
1368 1368 yield {'topic': c, 'summary': doc}
1369 1369
1370 1370 def othercommands(**map):
1371 1371 for c, doc in other:
1372 1372 yield {'topic': c, 'summary': doc}
1373 1373
1374 1374 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1375 1375 othercommands=othercommands, title='Index')
1376 1376
1377 1377 # Render an index of sub-topics.
1378 1378 if topicname in helpmod.subtopics:
1379 1379 topics = []
1380 1380 for entries, summary, _doc in helpmod.subtopics[topicname]:
1381 1381 topics.append({
1382 1382 'topic': '%s.%s' % (topicname, entries[0]),
1383 1383 'basename': entries[0],
1384 1384 'summary': summary,
1385 1385 })
1386 1386
1387 1387 return tmpl('helptopics', topics=topics, title=topicname,
1388 1388 subindex=True)
1389 1389
1390 1390 u = webutil.wsgiui.load()
1391 1391 u.verbose = True
1392 1392
1393 1393 # Render a page from a sub-topic.
1394 1394 if '.' in topicname:
1395 1395 # TODO implement support for rendering sections, like
1396 1396 # `hg help` works.
1397 1397 topic, subtopic = topicname.split('.', 1)
1398 1398 if topic not in helpmod.subtopics:
1399 1399 raise ErrorResponse(HTTP_NOT_FOUND)
1400 1400 else:
1401 1401 topic = topicname
1402 1402 subtopic = None
1403 1403
1404 1404 try:
1405 1405 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1406 1406 except error.Abort:
1407 1407 raise ErrorResponse(HTTP_NOT_FOUND)
1408 1408 return tmpl('help', topic=topicname, doc=doc)
1409 1409
1410 1410 # tell hggettext to extract docstrings from these functions:
1411 1411 i18nfunctions = commands.values()
@@ -1,665 +1,665 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:1] != "/":
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 v = req.form.get(k, [None])[0]
180 v = req.req.qsparams.get(k)
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 if 'node' in req.form:
299 changeid = req.form['node'][0]
298 if 'node' in req.req.qsparams:
299 changeid = req.req.qsparams['node']
300 300 ipos = changeid.find(':')
301 301 if ipos != -1:
302 302 changeid = changeid[(ipos + 1):]
303 elif 'manifest' in req.form:
304 changeid = req.form['manifest'][0]
303 elif 'manifest' in req.req.qsparams:
304 changeid = req.req.qsparams['manifest']
305 305
306 306 return changeidctx(repo, changeid)
307 307
308 308 def basechangectx(repo, req):
309 if 'node' in req.form:
310 changeid = req.form['node'][0]
309 if 'node' in req.req.qsparams:
310 changeid = req.req.qsparams['node']
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 if 'file' not in req.form:
319 if 'file' not in req.req.qsparams:
320 320 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
321 path = cleanpath(repo, req.form['file'][0])
322 if 'node' in req.form:
323 changeid = req.form['node'][0]
324 elif 'filenode' in req.form:
325 changeid = req.form['filenode'][0]
321 path = cleanpath(repo, req.req.qsparams['file'])
322 if 'node' in req.req.qsparams:
323 changeid = req.req.qsparams['node']
324 elif 'filenode' in req.req.qsparams:
325 changeid = req.req.qsparams['filenode']
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 linerange = req.form.get('linerange')
337 if linerange is None:
336 linerange = req.req.qsparams.getall('linerange')
337 if not linerange:
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 355 def succsandmarkers(context, mapping):
356 356 repo = context.resource(mapping, 'repo')
357 357 for item in templatekw.showsuccsandmarkers(context, mapping):
358 358 item['successors'] = _siblings(repo[successor]
359 359 for successor in item['successors'])
360 360 yield item
361 361
362 362 # teach templater succsandmarkers is switched to (context, mapping) API
363 363 succsandmarkers._requires = {'repo', 'ctx', 'templ'}
364 364
365 365 def commonentry(repo, ctx):
366 366 node = ctx.node()
367 367 return {
368 368 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
369 369 # filectx, but I'm not pretty sure if that would always work because
370 370 # fctx.parents() != fctx.changectx.parents() for example.
371 371 'ctx': ctx,
372 372 'revcache': {},
373 373 'rev': ctx.rev(),
374 374 'node': hex(node),
375 375 'author': ctx.user(),
376 376 'desc': ctx.description(),
377 377 'date': ctx.date(),
378 378 'extra': ctx.extra(),
379 379 'phase': ctx.phasestr(),
380 380 'obsolete': ctx.obsolete(),
381 381 'succsandmarkers': succsandmarkers,
382 382 'instabilities': [{"instability": i} for i in ctx.instabilities()],
383 383 'branch': nodebranchnodefault(ctx),
384 384 'inbranch': nodeinbranch(repo, ctx),
385 385 'branches': nodebranchdict(repo, ctx),
386 386 'tags': nodetagsdict(repo, node),
387 387 'bookmarks': nodebookmarksdict(repo, node),
388 388 'parent': lambda **x: parents(ctx),
389 389 'child': lambda **x: children(ctx),
390 390 }
391 391
392 392 def changelistentry(web, ctx, tmpl):
393 393 '''Obtain a dictionary to be used for entries in a changelist.
394 394
395 395 This function is called when producing items for the "entries" list passed
396 396 to the "shortlog" and "changelog" templates.
397 397 '''
398 398 repo = web.repo
399 399 rev = ctx.rev()
400 400 n = ctx.node()
401 401 showtags = showtag(repo, tmpl, 'changelogtag', n)
402 402 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
403 403
404 404 entry = commonentry(repo, ctx)
405 405 entry.update(
406 406 allparents=lambda **x: parents(ctx),
407 407 parent=lambda **x: parents(ctx, rev - 1),
408 408 child=lambda **x: children(ctx, rev + 1),
409 409 changelogtag=showtags,
410 410 files=files,
411 411 )
412 412 return entry
413 413
414 414 def symrevorshortnode(req, ctx):
415 if 'node' in req.form:
416 return templatefilters.revescape(req.form['node'][0])
415 if 'node' in req.req.qsparams:
416 return templatefilters.revescape(req.req.qsparams['node'])
417 417 else:
418 418 return short(ctx.node())
419 419
420 420 def changesetentry(web, req, tmpl, ctx):
421 421 '''Obtain a dictionary to be used to render the "changeset" template.'''
422 422
423 423 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
424 424 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
425 425 ctx.node())
426 426 showbranch = nodebranchnodefault(ctx)
427 427
428 428 files = []
429 429 parity = paritygen(web.stripecount)
430 430 for blockno, f in enumerate(ctx.files()):
431 431 template = 'filenodelink' if f in ctx else 'filenolink'
432 432 files.append(tmpl(template,
433 433 node=ctx.hex(), file=f, blockno=blockno + 1,
434 434 parity=next(parity)))
435 435
436 436 basectx = basechangectx(web.repo, req)
437 437 if basectx is None:
438 438 basectx = ctx.p1()
439 439
440 440 style = web.config('web', 'style')
441 441 if 'style' in req.req.qsparams:
442 442 style = req.req.qsparams['style']
443 443
444 444 diff = diffs(web, tmpl, ctx, basectx, None, style)
445 445
446 446 parity = paritygen(web.stripecount)
447 447 diffstatsgen = diffstatgen(ctx, basectx)
448 448 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
449 449
450 450 return dict(
451 451 diff=diff,
452 452 symrev=symrevorshortnode(req, ctx),
453 453 basenode=basectx.hex(),
454 454 changesettag=showtags,
455 455 changesetbookmark=showbookmarks,
456 456 changesetbranch=showbranch,
457 457 files=files,
458 458 diffsummary=lambda **x: diffsummary(diffstatsgen),
459 459 diffstat=diffstats,
460 460 archives=web.archivelist(ctx.hex()),
461 461 **pycompat.strkwargs(commonentry(web.repo, ctx)))
462 462
463 463 def listfilediffs(tmpl, files, node, max):
464 464 for f in files[:max]:
465 465 yield tmpl('filedifflink', node=hex(node), file=f)
466 466 if len(files) > max:
467 467 yield tmpl('fileellipses')
468 468
469 469 def diffs(web, tmpl, ctx, basectx, files, style, linerange=None,
470 470 lineidprefix=''):
471 471
472 472 def prettyprintlines(lines, blockno):
473 473 for lineno, l in enumerate(lines, 1):
474 474 difflineno = "%d.%d" % (blockno, lineno)
475 475 if l.startswith('+'):
476 476 ltype = "difflineplus"
477 477 elif l.startswith('-'):
478 478 ltype = "difflineminus"
479 479 elif l.startswith('@'):
480 480 ltype = "difflineat"
481 481 else:
482 482 ltype = "diffline"
483 483 yield tmpl(ltype,
484 484 line=l,
485 485 lineno=lineno,
486 486 lineid=lineidprefix + "l%s" % difflineno,
487 487 linenumber="% 8s" % difflineno)
488 488
489 489 repo = web.repo
490 490 if files:
491 491 m = match.exact(repo.root, repo.getcwd(), files)
492 492 else:
493 493 m = match.always(repo.root, repo.getcwd())
494 494
495 495 diffopts = patch.diffopts(repo.ui, untrusted=True)
496 496 node1 = basectx.node()
497 497 node2 = ctx.node()
498 498 parity = paritygen(web.stripecount)
499 499
500 500 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
501 501 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
502 502 if style != 'raw':
503 503 header = header[1:]
504 504 lines = [h + '\n' for h in header]
505 505 for hunkrange, hunklines in hunks:
506 506 if linerange is not None and hunkrange is not None:
507 507 s1, l1, s2, l2 = hunkrange
508 508 if not mdiff.hunkinrange((s2, l2), linerange):
509 509 continue
510 510 lines.extend(hunklines)
511 511 if lines:
512 512 yield tmpl('diffblock', parity=next(parity), blockno=blockno,
513 513 lines=prettyprintlines(lines, blockno))
514 514
515 515 def compare(tmpl, context, leftlines, rightlines):
516 516 '''Generator function that provides side-by-side comparison data.'''
517 517
518 518 def compline(type, leftlineno, leftline, rightlineno, rightline):
519 519 lineid = leftlineno and ("l%d" % leftlineno) or ''
520 520 lineid += rightlineno and ("r%d" % rightlineno) or ''
521 521 llno = '%d' % leftlineno if leftlineno else ''
522 522 rlno = '%d' % rightlineno if rightlineno else ''
523 523 return tmpl('comparisonline',
524 524 type=type,
525 525 lineid=lineid,
526 526 leftlineno=leftlineno,
527 527 leftlinenumber="% 6s" % llno,
528 528 leftline=leftline or '',
529 529 rightlineno=rightlineno,
530 530 rightlinenumber="% 6s" % rlno,
531 531 rightline=rightline or '')
532 532
533 533 def getblock(opcodes):
534 534 for type, llo, lhi, rlo, rhi in opcodes:
535 535 len1 = lhi - llo
536 536 len2 = rhi - rlo
537 537 count = min(len1, len2)
538 538 for i in xrange(count):
539 539 yield compline(type=type,
540 540 leftlineno=llo + i + 1,
541 541 leftline=leftlines[llo + i],
542 542 rightlineno=rlo + i + 1,
543 543 rightline=rightlines[rlo + i])
544 544 if len1 > len2:
545 545 for i in xrange(llo + count, lhi):
546 546 yield compline(type=type,
547 547 leftlineno=i + 1,
548 548 leftline=leftlines[i],
549 549 rightlineno=None,
550 550 rightline=None)
551 551 elif len2 > len1:
552 552 for i in xrange(rlo + count, rhi):
553 553 yield compline(type=type,
554 554 leftlineno=None,
555 555 leftline=None,
556 556 rightlineno=i + 1,
557 557 rightline=rightlines[i])
558 558
559 559 s = difflib.SequenceMatcher(None, leftlines, rightlines)
560 560 if context < 0:
561 561 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
562 562 else:
563 563 for oc in s.get_grouped_opcodes(n=context):
564 564 yield tmpl('comparisonblock', lines=getblock(oc))
565 565
566 566 def diffstatgen(ctx, basectx):
567 567 '''Generator function that provides the diffstat data.'''
568 568
569 569 stats = patch.diffstatdata(
570 570 util.iterlines(ctx.diff(basectx, noprefix=False)))
571 571 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
572 572 while True:
573 573 yield stats, maxname, maxtotal, addtotal, removetotal, binary
574 574
575 575 def diffsummary(statgen):
576 576 '''Return a short summary of the diff.'''
577 577
578 578 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
579 579 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
580 580 len(stats), addtotal, removetotal)
581 581
582 582 def diffstat(tmpl, ctx, statgen, parity):
583 583 '''Return a diffstat template for each file in the diff.'''
584 584
585 585 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
586 586 files = ctx.files()
587 587
588 588 def pct(i):
589 589 if maxtotal == 0:
590 590 return 0
591 591 return (float(i) / maxtotal) * 100
592 592
593 593 fileno = 0
594 594 for filename, adds, removes, isbinary in stats:
595 595 template = 'diffstatlink' if filename in files else 'diffstatnolink'
596 596 total = adds + removes
597 597 fileno += 1
598 598 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
599 599 total=total, addpct=pct(adds), removepct=pct(removes),
600 600 parity=next(parity))
601 601
602 602 class sessionvars(object):
603 603 def __init__(self, vars, start='?'):
604 604 self.start = start
605 605 self.vars = vars
606 606 def __getitem__(self, key):
607 607 return self.vars[key]
608 608 def __setitem__(self, key, value):
609 609 self.vars[key] = value
610 610 def __copy__(self):
611 611 return sessionvars(copy.copy(self.vars), self.start)
612 612 def __iter__(self):
613 613 separator = self.start
614 614 for key, value in sorted(self.vars.iteritems()):
615 615 yield {'name': key,
616 616 'value': pycompat.bytestr(value),
617 617 'separator': separator,
618 618 }
619 619 separator = '&'
620 620
621 621 class wsgiui(uimod.ui):
622 622 # default termwidth breaks under mod_wsgi
623 623 def termwidth(self):
624 624 return 80
625 625
626 626 def getwebsubs(repo):
627 627 websubtable = []
628 628 websubdefs = repo.ui.configitems('websub')
629 629 # we must maintain interhg backwards compatibility
630 630 websubdefs += repo.ui.configitems('interhg')
631 631 for key, pattern in websubdefs:
632 632 # grab the delimiter from the character after the "s"
633 633 unesc = pattern[1:2]
634 634 delim = re.escape(unesc)
635 635
636 636 # identify portions of the pattern, taking care to avoid escaped
637 637 # delimiters. the replace format and flags are optional, but
638 638 # delimiters are required.
639 639 match = re.match(
640 640 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
641 641 % (delim, delim, delim), pattern)
642 642 if not match:
643 643 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
644 644 % (key, pattern))
645 645 continue
646 646
647 647 # we need to unescape the delimiter for regexp and format
648 648 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
649 649 regexp = delim_re.sub(unesc, match.group(1))
650 650 format = delim_re.sub(unesc, match.group(2))
651 651
652 652 # the pattern allows for 6 regexp flags, so set them if necessary
653 653 flagin = match.group(3)
654 654 flags = 0
655 655 if flagin:
656 656 for flag in flagin.upper():
657 657 flags |= re.__dict__[flag]
658 658
659 659 try:
660 660 regexp = re.compile(regexp, flags)
661 661 websubtable.append((regexp, format))
662 662 except re.error:
663 663 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
664 664 % (key, regexp))
665 665 return websubtable
@@ -1,21 +1,21 b''
1 1 # A dummy extension that installs an hgweb command that throws an Exception.
2 2
3 3 from __future__ import absolute_import
4 4
5 5 from mercurial.hgweb import (
6 6 webcommands,
7 7 )
8 8
9 9 def raiseerror(web, req, tmpl):
10 10 '''Dummy web command that raises an uncaught Exception.'''
11 11
12 12 # Simulate an error after partial response.
13 if 'partialresponse' in req.form:
13 if 'partialresponse' in req.req.qsparams:
14 14 req.respond(200, 'text/plain')
15 15 req.write('partial content\n')
16 16
17 17 raise AttributeError('I am an uncaught error!')
18 18
19 19 def extsetup(ui):
20 20 setattr(webcommands, 'raiseerror', raiseerror)
21 21 webcommands.__all__.append('raiseerror')
General Comments 0
You need to be logged in to leave comments. Login now