##// END OF EJS Templates
hgweb: use our new request object for "style" parameter...
Gregory Szorc -
r36879:1a1972b1 default
parent child Browse files
Show More
@@ -1,440 +1,439 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 fromreq = req.form.get('style', [None])[0]
57 56 styles = (
58 fromreq,
57 req.qsparams.get('style', None),
59 58 configfn('web', 'style'),
60 59 'paper',
61 60 )
62 61 return styles, templater.stylemap(styles, templatepath)
63 62
64 63 def makebreadcrumb(url, prefix=''):
65 64 '''Return a 'URL breadcrumb' list
66 65
67 66 A 'URL breadcrumb' is a list of URL-name pairs,
68 67 corresponding to each of the path items on a URL.
69 68 This can be used to create path navigation entries.
70 69 '''
71 70 if url.endswith('/'):
72 71 url = url[:-1]
73 72 if prefix:
74 73 url = '/' + prefix + url
75 74 relpath = url
76 75 if relpath.startswith('/'):
77 76 relpath = relpath[1:]
78 77
79 78 breadcrumb = []
80 79 urlel = url
81 80 pathitems = [''] + relpath.split('/')
82 81 for pathel in reversed(pathitems):
83 82 if not pathel or not urlel:
84 83 break
85 84 breadcrumb.append({'url': urlel, 'name': pathel})
86 85 urlel = os.path.dirname(urlel)
87 86 return reversed(breadcrumb)
88 87
89 88 class requestcontext(object):
90 89 """Holds state/context for an individual request.
91 90
92 91 Servers can be multi-threaded. Holding state on the WSGI application
93 92 is prone to race conditions. Instances of this class exist to hold
94 93 mutable and race-free state for requests.
95 94 """
96 95 def __init__(self, app, repo):
97 96 self.repo = repo
98 97 self.reponame = app.reponame
99 98
100 99 self.archivespecs = archivespecs
101 100
102 101 self.maxchanges = self.configint('web', 'maxchanges')
103 102 self.stripecount = self.configint('web', 'stripes')
104 103 self.maxshortchanges = self.configint('web', 'maxshortchanges')
105 104 self.maxfiles = self.configint('web', 'maxfiles')
106 105 self.allowpull = self.configbool('web', 'allow-pull')
107 106
108 107 # we use untrusted=False to prevent a repo owner from using
109 108 # web.templates in .hg/hgrc to get access to any file readable
110 109 # by the user running the CGI script
111 110 self.templatepath = self.config('web', 'templates', untrusted=False)
112 111
113 112 # This object is more expensive to build than simple config values.
114 113 # It is shared across requests. The app will replace the object
115 114 # if it is updated. Since this is a reference and nothing should
116 115 # modify the underlying object, it should be constant for the lifetime
117 116 # of the request.
118 117 self.websubtable = app.websubtable
119 118
120 119 self.csp, self.nonce = cspvalues(self.repo.ui)
121 120
122 121 # Trust the settings from the .hg/hgrc files by default.
123 122 def config(self, section, name, default=uimod._unset, untrusted=True):
124 123 return self.repo.ui.config(section, name, default,
125 124 untrusted=untrusted)
126 125
127 126 def configbool(self, section, name, default=uimod._unset, untrusted=True):
128 127 return self.repo.ui.configbool(section, name, default,
129 128 untrusted=untrusted)
130 129
131 130 def configint(self, section, name, default=uimod._unset, untrusted=True):
132 131 return self.repo.ui.configint(section, name, default,
133 132 untrusted=untrusted)
134 133
135 134 def configlist(self, section, name, default=uimod._unset, untrusted=True):
136 135 return self.repo.ui.configlist(section, name, default,
137 136 untrusted=untrusted)
138 137
139 138 def archivelist(self, nodeid):
140 139 allowed = self.configlist('web', 'allow_archive')
141 140 for typ, spec in self.archivespecs.iteritems():
142 141 if typ in allowed or self.configbool('web', 'allow%s' % typ):
143 142 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
144 143
145 144 def templater(self, wsgireq, req):
146 145 # determine scheme, port and server name
147 146 # this is needed to create absolute urls
148 147 logourl = self.config('web', 'logourl')
149 148 logoimg = self.config('web', 'logoimg')
150 149 staticurl = (self.config('web', 'staticurl')
151 150 or req.apppath + '/static/')
152 151 if not staticurl.endswith('/'):
153 152 staticurl += '/'
154 153
155 154 # some functions for the templater
156 155
157 156 def motd(**map):
158 157 yield self.config('web', 'motd')
159 158
160 159 # figure out which style to use
161 160
162 161 vars = {}
163 styles, (style, mapfile) = getstyle(wsgireq, self.config,
162 styles, (style, mapfile) = getstyle(wsgireq.req, self.config,
164 163 self.templatepath)
165 164 if style == styles[0]:
166 165 vars['style'] = style
167 166
168 167 sessionvars = webutil.sessionvars(vars, '?')
169 168
170 169 if not self.reponame:
171 170 self.reponame = (self.config('web', 'name', '')
172 171 or wsgireq.env.get('REPO_NAME')
173 172 or req.apppath or self.repo.root)
174 173
175 174 def websubfilter(text):
176 175 return templatefilters.websub(text, self.websubtable)
177 176
178 177 # create the templater
179 178 # TODO: export all keywords: defaults = templatekw.keywords.copy()
180 179 defaults = {
181 180 'url': req.apppath + '/',
182 181 'logourl': logourl,
183 182 'logoimg': logoimg,
184 183 'staticurl': staticurl,
185 184 'urlbase': req.advertisedbaseurl,
186 185 'repo': self.reponame,
187 186 'encoding': encoding.encoding,
188 187 'motd': motd,
189 188 'sessionvars': sessionvars,
190 189 'pathdef': makebreadcrumb(req.apppath),
191 190 'style': style,
192 191 'nonce': self.nonce,
193 192 }
194 193 tres = formatter.templateresources(self.repo.ui, self.repo)
195 194 tmpl = templater.templater.frommapfile(mapfile,
196 195 filters={'websub': websubfilter},
197 196 defaults=defaults,
198 197 resources=tres)
199 198 return tmpl
200 199
201 200
202 201 class hgweb(object):
203 202 """HTTP server for individual repositories.
204 203
205 204 Instances of this class serve HTTP responses for a particular
206 205 repository.
207 206
208 207 Instances are typically used as WSGI applications.
209 208
210 209 Some servers are multi-threaded. On these servers, there may
211 210 be multiple active threads inside __call__.
212 211 """
213 212 def __init__(self, repo, name=None, baseui=None):
214 213 if isinstance(repo, str):
215 214 if baseui:
216 215 u = baseui.copy()
217 216 else:
218 217 u = uimod.ui.load()
219 218 r = hg.repository(u, repo)
220 219 else:
221 220 # we trust caller to give us a private copy
222 221 r = repo
223 222
224 223 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 224 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 225 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 226 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 227 # resolve file patterns relative to repo root
229 228 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 229 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 230 # displaying bundling progress bar while serving feel wrong and may
232 231 # break some wsgi implementation.
233 232 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
234 233 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
235 234 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
236 235 self._lastrepo = self._repos[0]
237 236 hook.redirect(True)
238 237 self.reponame = name
239 238
240 239 def _webifyrepo(self, repo):
241 240 repo = getwebview(repo)
242 241 self.websubtable = webutil.getwebsubs(repo)
243 242 return repo
244 243
245 244 @contextlib.contextmanager
246 245 def _obtainrepo(self):
247 246 """Obtain a repo unique to the caller.
248 247
249 248 Internally we maintain a stack of cachedlocalrepo instances
250 249 to be handed out. If one is available, we pop it and return it,
251 250 ensuring it is up to date in the process. If one is not available,
252 251 we clone the most recently used repo instance and return it.
253 252
254 253 It is currently possible for the stack to grow without bounds
255 254 if the server allows infinite threads. However, servers should
256 255 have a thread limit, thus establishing our limit.
257 256 """
258 257 if self._repos:
259 258 cached = self._repos.pop()
260 259 r, created = cached.fetch()
261 260 else:
262 261 cached = self._lastrepo.copy()
263 262 r, created = cached.fetch()
264 263 if created:
265 264 r = self._webifyrepo(r)
266 265
267 266 self._lastrepo = cached
268 267 self.mtime = cached.mtime
269 268 try:
270 269 yield r
271 270 finally:
272 271 self._repos.append(cached)
273 272
274 273 def run(self):
275 274 """Start a server from CGI environment.
276 275
277 276 Modern servers should be using WSGI and should avoid this
278 277 method, if possible.
279 278 """
280 279 if not encoding.environ.get('GATEWAY_INTERFACE',
281 280 '').startswith("CGI/1."):
282 281 raise RuntimeError("This function is only intended to be "
283 282 "called while running as a CGI script.")
284 283 wsgicgi.launch(self)
285 284
286 285 def __call__(self, env, respond):
287 286 """Run the WSGI application.
288 287
289 288 This may be called by multiple threads.
290 289 """
291 290 req = requestmod.wsgirequest(env, respond)
292 291 return self.run_wsgi(req)
293 292
294 293 def run_wsgi(self, wsgireq):
295 294 """Internal method to run the WSGI application.
296 295
297 296 This is typically only called by Mercurial. External consumers
298 297 should be using instances of this class as the WSGI application.
299 298 """
300 299 with self._obtainrepo() as repo:
301 300 profile = repo.ui.configbool('profiling', 'enabled')
302 301 with profiling.profile(repo.ui, enabled=profile):
303 302 for r in self._runwsgi(wsgireq, repo):
304 303 yield r
305 304
306 305 def _runwsgi(self, wsgireq, repo):
307 306 req = wsgireq.req
308 307 res = wsgireq.res
309 308 rctx = requestcontext(self, repo)
310 309
311 310 # This state is global across all threads.
312 311 encoding.encoding = rctx.config('web', 'encoding')
313 312 rctx.repo.ui.environ = wsgireq.env
314 313
315 314 if rctx.csp:
316 315 # hgwebdir may have added CSP header. Since we generate our own,
317 316 # replace it.
318 317 wsgireq.headers = [h for h in wsgireq.headers
319 318 if h[0] != 'Content-Security-Policy']
320 319 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
321 320 res.headers['Content-Security-Policy'] = rctx.csp
322 321
323 322 handled = wireprotoserver.handlewsgirequest(
324 323 rctx, wsgireq, req, res, self.check_perm)
325 324 if handled:
326 325 return res.sendresponse()
327 326
328 327 if req.havepathinfo:
329 328 query = req.dispatchpath
330 329 else:
331 330 query = req.querystring.partition('&')[0].partition(';')[0]
332 331
333 332 # translate user-visible url structure to internal structure
334 333
335 334 args = query.split('/', 2)
336 335 if 'cmd' not in wsgireq.form and args and args[0]:
337 336 cmd = args.pop(0)
338 337 style = cmd.rfind('-')
339 338 if style != -1:
340 wsgireq.form['style'] = [cmd[:style]]
339 req.qsparams['style'] = cmd[:style]
341 340 cmd = cmd[style + 1:]
342 341
343 342 # avoid accepting e.g. style parameter as command
344 343 if util.safehasattr(webcommands, cmd):
345 344 wsgireq.form['cmd'] = [cmd]
346 345
347 346 if cmd == 'static':
348 347 wsgireq.form['file'] = ['/'.join(args)]
349 348 else:
350 349 if args and args[0]:
351 350 node = args.pop(0).replace('%2F', '/')
352 351 wsgireq.form['node'] = [node]
353 352 if args:
354 353 wsgireq.form['file'] = args
355 354
356 355 ua = req.headers.get('User-Agent', '')
357 356 if cmd == 'rev' and 'mercurial' in ua:
358 wsgireq.form['style'] = ['raw']
357 req.qsparams['style'] = 'raw'
359 358
360 359 if cmd == 'archive':
361 360 fn = wsgireq.form['node'][0]
362 361 for type_, spec in rctx.archivespecs.iteritems():
363 362 ext = spec[2]
364 363 if fn.endswith(ext):
365 364 wsgireq.form['node'] = [fn[:-len(ext)]]
366 365 wsgireq.form['type'] = [type_]
367 366 else:
368 367 cmd = wsgireq.form.get('cmd', [''])[0]
369 368
370 369 # process the web interface request
371 370
372 371 try:
373 372 tmpl = rctx.templater(wsgireq, req)
374 373 ctype = tmpl('mimetype', encoding=encoding.encoding)
375 374 ctype = templater.stringify(ctype)
376 375
377 376 # check read permissions non-static content
378 377 if cmd != 'static':
379 378 self.check_perm(rctx, wsgireq, None)
380 379
381 380 if cmd == '':
382 381 wsgireq.form['cmd'] = [tmpl.cache['default']]
383 382 cmd = wsgireq.form['cmd'][0]
384 383
385 384 # Don't enable caching if using a CSP nonce because then it wouldn't
386 385 # be a nonce.
387 386 if rctx.configbool('web', 'cache') and not rctx.nonce:
388 387 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED
389 388 if cmd not in webcommands.__all__:
390 389 msg = 'no such method: %s' % cmd
391 390 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
392 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []):
391 elif cmd == 'file' and req.qsparams.get('style') == 'raw':
393 392 rctx.ctype = ctype
394 393 content = webcommands.rawfile(rctx, wsgireq, tmpl)
395 394 else:
396 395 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
397 396 wsgireq.respond(HTTP_OK, ctype)
398 397
399 398 return content
400 399
401 400 except (error.LookupError, error.RepoLookupError) as err:
402 401 wsgireq.respond(HTTP_NOT_FOUND, ctype)
403 402 msg = pycompat.bytestr(err)
404 403 if (util.safehasattr(err, 'name') and
405 404 not isinstance(err, error.ManifestLookupError)):
406 405 msg = 'revision not found: %s' % err.name
407 406 return tmpl('error', error=msg)
408 407 except (error.RepoError, error.RevlogError) as inst:
409 408 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
410 409 return tmpl('error', error=pycompat.bytestr(inst))
411 410 except ErrorResponse as inst:
412 411 wsgireq.respond(inst, ctype)
413 412 if inst.code == HTTP_NOT_MODIFIED:
414 413 # Not allowed to return a body on a 304
415 414 return ['']
416 415 return tmpl('error', error=pycompat.bytestr(inst))
417 416
418 417 def check_perm(self, rctx, req, op):
419 418 for permhook in permhooks:
420 419 permhook(rctx, req, op)
421 420
422 421 def getwebview(repo):
423 422 """The 'web.view' config controls changeset filter to hgweb. Possible
424 423 values are ``served``, ``visible`` and ``all``. Default is ``served``.
425 424 The ``served`` filter only shows changesets that can be pulled from the
426 425 hgweb instance. The``visible`` filter includes secret changesets but
427 426 still excludes "hidden" one.
428 427
429 428 See the repoview module for details.
430 429
431 430 The option has been around undocumented since Mercurial 2.5, but no
432 431 user ever asked about it. So we better keep it undocumented for now."""
433 432 # experimental config: web.view
434 433 viewconfig = repo.ui.config('web', 'view', untrusted=True)
435 434 if viewconfig == 'all':
436 435 return repo.unfiltered()
437 436 elif viewconfig in repoview.filtertable:
438 437 return repo.filtered(viewconfig)
439 438 else:
440 439 return repo.filtered('served')
@@ -1,544 +1,544 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 232 try:
233 233 self.refresh()
234 234
235 235 csp, nonce = cspvalues(self.ui)
236 236 if csp:
237 237 wsgireq.headers.append(('Content-Security-Policy', csp))
238 238
239 239 virtual = wsgireq.env.get("PATH_INFO", "").strip('/')
240 240 tmpl = self.templater(wsgireq, nonce)
241 241 ctype = tmpl('mimetype', encoding=encoding.encoding)
242 242 ctype = templater.stringify(ctype)
243 243
244 244 # a static file
245 245 if virtual.startswith('static/') or 'static' in wsgireq.form:
246 246 if virtual.startswith('static/'):
247 247 fname = virtual[7:]
248 248 else:
249 249 fname = wsgireq.form['static'][0]
250 250 static = self.ui.config("web", "static", None,
251 251 untrusted=False)
252 252 if not static:
253 253 tp = self.templatepath or templater.templatepaths()
254 254 if isinstance(tp, str):
255 255 tp = [tp]
256 256 static = [os.path.join(p, 'static') for p in tp]
257 257 staticfile(static, fname, wsgireq)
258 258 return []
259 259
260 260 # top-level index
261 261
262 262 repos = dict(self.repos)
263 263
264 264 if (not virtual or virtual == 'index') and virtual not in repos:
265 265 wsgireq.respond(HTTP_OK, ctype)
266 266 return self.makeindex(wsgireq, tmpl)
267 267
268 268 # nested indexes and hgwebs
269 269
270 270 if virtual.endswith('/index') and virtual not in repos:
271 271 subdir = virtual[:-len('index')]
272 272 if any(r.startswith(subdir) for r in repos):
273 273 wsgireq.respond(HTTP_OK, ctype)
274 274 return self.makeindex(wsgireq, tmpl, subdir)
275 275
276 276 def _virtualdirs():
277 277 # Check the full virtual path, each parent, and the root ('')
278 278 if virtual != '':
279 279 yield virtual
280 280
281 281 for p in util.finddirs(virtual):
282 282 yield p
283 283
284 284 yield ''
285 285
286 286 for virtualrepo in _virtualdirs():
287 287 real = repos.get(virtualrepo)
288 288 if real:
289 289 wsgireq.env['REPO_NAME'] = virtualrepo
290 290 # We have to re-parse because of updated environment
291 291 # variable.
292 292 # TODO this is kind of hacky and we should have a better
293 293 # way of doing this than with REPO_NAME side-effects.
294 294 wsgireq.req = requestmod.parserequestfromenv(
295 295 wsgireq.env, wsgireq.req.bodyfh)
296 296 try:
297 297 # ensure caller gets private copy of ui
298 298 repo = hg.repository(self.ui.copy(), real)
299 299 return hgweb_mod.hgweb(repo).run_wsgi(wsgireq)
300 300 except IOError as inst:
301 301 msg = encoding.strtolocal(inst.strerror)
302 302 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
303 303 except error.RepoError as inst:
304 304 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
305 305
306 306 # browse subdirectories
307 307 subdir = virtual + '/'
308 308 if [r for r in repos if r.startswith(subdir)]:
309 309 wsgireq.respond(HTTP_OK, ctype)
310 310 return self.makeindex(wsgireq, tmpl, subdir)
311 311
312 312 # prefixes not found
313 313 wsgireq.respond(HTTP_NOT_FOUND, ctype)
314 314 return tmpl("notfound", repo=virtual)
315 315
316 316 except ErrorResponse as err:
317 317 wsgireq.respond(err, ctype)
318 318 return tmpl('error', error=err.message or '')
319 319 finally:
320 320 tmpl = None
321 321
322 322 def makeindex(self, wsgireq, tmpl, subdir=""):
323 323
324 324 def archivelist(ui, nodeid, url):
325 325 allowed = ui.configlist("web", "allow_archive", untrusted=True)
326 326 archives = []
327 327 for typ, spec in hgweb_mod.archivespecs.iteritems():
328 328 if typ in allowed or ui.configbool("web", "allow" + typ,
329 329 untrusted=True):
330 330 archives.append({"type": typ, "extension": spec[2],
331 331 "node": nodeid, "url": url})
332 332 return archives
333 333
334 334 def rawentries(subdir="", **map):
335 335
336 336 descend = self.ui.configbool('web', 'descend')
337 337 collapse = self.ui.configbool('web', 'collapse')
338 338 seenrepos = set()
339 339 seendirs = set()
340 340 for name, path in self.repos:
341 341
342 342 if not name.startswith(subdir):
343 343 continue
344 344 name = name[len(subdir):]
345 345 directory = False
346 346
347 347 if '/' in name:
348 348 if not descend:
349 349 continue
350 350
351 351 nameparts = name.split('/')
352 352 rootname = nameparts[0]
353 353
354 354 if not collapse:
355 355 pass
356 356 elif rootname in seendirs:
357 357 continue
358 358 elif rootname in seenrepos:
359 359 pass
360 360 else:
361 361 directory = True
362 362 name = rootname
363 363
364 364 # redefine the path to refer to the directory
365 365 discarded = '/'.join(nameparts[1:])
366 366
367 367 # remove name parts plus accompanying slash
368 368 path = path[:-len(discarded) - 1]
369 369
370 370 try:
371 371 r = hg.repository(self.ui, path)
372 372 directory = False
373 373 except (IOError, error.RepoError):
374 374 pass
375 375
376 376 parts = [name]
377 377 parts.insert(0, '/' + subdir.rstrip('/'))
378 378 if wsgireq.env['SCRIPT_NAME']:
379 379 parts.insert(0, wsgireq.env['SCRIPT_NAME'])
380 380 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
381 381
382 382 # show either a directory entry or a repository
383 383 if directory:
384 384 # get the directory's time information
385 385 try:
386 386 d = (get_mtime(path), dateutil.makedate()[1])
387 387 except OSError:
388 388 continue
389 389
390 390 # add '/' to the name to make it obvious that
391 391 # the entry is a directory, not a regular repository
392 392 row = {'contact': "",
393 393 'contact_sort': "",
394 394 'name': name + '/',
395 395 'name_sort': name,
396 396 'url': url,
397 397 'description': "",
398 398 'description_sort': "",
399 399 'lastchange': d,
400 400 'lastchange_sort': d[1]-d[0],
401 401 'archives': [],
402 402 'isdirectory': True,
403 403 'labels': [],
404 404 }
405 405
406 406 seendirs.add(name)
407 407 yield row
408 408 continue
409 409
410 410 u = self.ui.copy()
411 411 try:
412 412 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
413 413 except Exception as e:
414 414 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
415 415 continue
416 416 def get(section, name, default=uimod._unset):
417 417 return u.config(section, name, default, untrusted=True)
418 418
419 419 if u.configbool("web", "hidden", untrusted=True):
420 420 continue
421 421
422 422 if not self.read_allowed(u, wsgireq):
423 423 continue
424 424
425 425 # update time with local timezone
426 426 try:
427 427 r = hg.repository(self.ui, path)
428 428 except IOError:
429 429 u.warn(_('error accessing repository at %s\n') % path)
430 430 continue
431 431 except error.RepoError:
432 432 u.warn(_('error accessing repository at %s\n') % path)
433 433 continue
434 434 try:
435 435 d = (get_mtime(r.spath), dateutil.makedate()[1])
436 436 except OSError:
437 437 continue
438 438
439 439 contact = get_contact(get)
440 440 description = get("web", "description")
441 441 seenrepos.add(name)
442 442 name = get("web", "name", name)
443 443 row = {'contact': contact or "unknown",
444 444 'contact_sort': contact.upper() or "unknown",
445 445 'name': name,
446 446 'name_sort': name,
447 447 'url': url,
448 448 'description': description or "unknown",
449 449 'description_sort': description.upper() or "unknown",
450 450 'lastchange': d,
451 451 'lastchange_sort': d[1]-d[0],
452 452 'archives': archivelist(u, "tip", url),
453 453 'isdirectory': None,
454 454 'labels': u.configlist('web', 'labels', untrusted=True),
455 455 }
456 456
457 457 yield row
458 458
459 459 sortdefault = None, False
460 460 def entries(sortcolumn="", descending=False, subdir="", **map):
461 461 rows = rawentries(subdir=subdir, **map)
462 462
463 463 if sortcolumn and sortdefault != (sortcolumn, descending):
464 464 sortkey = '%s_sort' % sortcolumn
465 465 rows = sorted(rows, key=lambda x: x[sortkey],
466 466 reverse=descending)
467 467 for row, parity in zip(rows, paritygen(self.stripecount)):
468 468 row['parity'] = parity
469 469 yield row
470 470
471 471 self.refresh()
472 472 sortable = ["name", "description", "contact", "lastchange"]
473 473 sortcolumn, descending = sortdefault
474 474 if 'sort' in wsgireq.form:
475 475 sortcolumn = wsgireq.form['sort'][0]
476 476 descending = sortcolumn.startswith('-')
477 477 if descending:
478 478 sortcolumn = sortcolumn[1:]
479 479 if sortcolumn not in sortable:
480 480 sortcolumn = ""
481 481
482 482 sort = [("sort_%s" % column,
483 483 "%s%s" % ((not descending and column == sortcolumn)
484 484 and "-" or "", column))
485 485 for column in sortable]
486 486
487 487 self.refresh()
488 488 self.updatereqenv(wsgireq.env)
489 489
490 490 return tmpl("index", entries=entries, subdir=subdir,
491 491 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
492 492 sortcolumn=sortcolumn, descending=descending,
493 493 **dict(sort))
494 494
495 495 def templater(self, wsgireq, nonce):
496 496
497 497 def motd(**map):
498 498 if self.motd is not None:
499 499 yield self.motd
500 500 else:
501 501 yield config('web', 'motd')
502 502
503 503 def config(section, name, default=uimod._unset, untrusted=True):
504 504 return self.ui.config(section, name, default, untrusted)
505 505
506 506 self.updatereqenv(wsgireq.env)
507 507
508 508 url = wsgireq.env.get('SCRIPT_NAME', '')
509 509 if not url.endswith('/'):
510 510 url += '/'
511 511
512 512 vars = {}
513 styles, (style, mapfile) = hgweb_mod.getstyle(wsgireq, config,
513 styles, (style, mapfile) = hgweb_mod.getstyle(wsgireq.req, config,
514 514 self.templatepath)
515 515 if style == styles[0]:
516 516 vars['style'] = style
517 517
518 518 sessionvars = webutil.sessionvars(vars, r'?')
519 519 logourl = config('web', 'logourl')
520 520 logoimg = config('web', 'logoimg')
521 521 staticurl = config('web', 'staticurl') or url + 'static/'
522 522 if not staticurl.endswith('/'):
523 523 staticurl += '/'
524 524
525 525 defaults = {
526 526 "encoding": encoding.encoding,
527 527 "motd": motd,
528 528 "url": url,
529 529 "logourl": logourl,
530 530 "logoimg": logoimg,
531 531 "staticurl": staticurl,
532 532 "sessionvars": sessionvars,
533 533 "style": style,
534 534 "nonce": nonce,
535 535 }
536 536 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
537 537 return tmpl
538 538
539 539 def updatereqenv(self, env):
540 540 if self._baseurl is not None:
541 541 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
542 542 env['SERVER_NAME'] = name
543 543 env['SERVER_PORT'] = port
544 544 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 88 if 'file' in req.form and req.form['file'][0]:
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 97 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
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 176 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
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 292 query = req.form['rev'][0]
293 293 revcount = web.maxchanges
294 294 if 'revcount' in req.form:
295 295 try:
296 296 revcount = int(req.form.get('revcount', [revcount])[0])
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 311 if 'forcekw' in req.form:
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 361 if 'node' in req.form:
362 362 ctx = webutil.changectx(web.repo, req)
363 363 symrev = webutil.symrevorshortnode(req, ctx)
364 364 elif 'rev' in req.form:
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 389 if 'revcount' in req.form:
390 390 try:
391 391 revcount = int(req.form.get('revcount', [revcount])[0])
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 484 if 'node' in req.form:
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 490 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
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 755 path = webutil.cleanpath(web.repo, req.form['file'][0])
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 if 'style' in req.form:
766 style = req.form['style'][0]
765 if 'style' in req.req.qsparams:
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 802 if 'file' not in req.form:
803 803 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
804 804 path = webutil.cleanpath(web.repo, req.form['file'][0])
805 805
806 806 parsecontext = lambda v: v == 'full' and -1 or int(v)
807 807 if 'context' in req.form:
808 808 context = parsecontext(req.form['context'][0])
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 967 f = webutil.cleanpath(web.repo, req.form['file'][0])
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 982 if 'revcount' in req.form:
983 983 try:
984 984 revcount = int(req.form.get('revcount', [revcount])[0])
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 997 patch = 'patch' in req.form
998 998 if patch:
999 999 lessvars['patch'] = morevars['patch'] = req.form['patch'][0]
1000 1000 descend = 'descend' in req.form
1001 1001 if descend:
1002 1002 lessvars['descend'] = morevars['descend'] = req.form['descend'][0]
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 if 'style' in req.form:
1015 diffstyle = req.form['style'][0]
1014 if 'style' in req.req.qsparams:
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 1106 type_ = req.form.get('type', [None])[0]
1107 1107 allowed = web.configlist("web", "allow_archive")
1108 1108 key = req.form['node'][0]
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 1129 file = req.form.get('file', None)
1130 1130 if file:
1131 1131 pats = ['path:' + file[0]]
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 1137 'file(s) not found: %s' % file[0])
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 1156 fname = req.form['file'][0]
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 1192 if 'node' in req.form:
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 1202 if 'revcount' in req.form:
1203 1203 try:
1204 1204 revcount = int(req.form.get('revcount', [revcount])[0])
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 1215 graphtop = req.form.get('graphtop', [ctx.hex()])[0]
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 1345 topicname = req.form.get('node', [None])[0]
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 180 v = req.form.get(k, [None])[0]
181 181 if v is not None:
182 182 v = util.parsebool(v)
183 183 setattr(diffopts, k, v if v is not None else True)
184 184
185 185 return diffopts
186 186
187 187 def annotate(req, fctx, ui):
188 188 diffopts = difffeatureopts(req, ui, 'annotate')
189 189 return fctx.annotate(follow=True, linenumber=True, diffopts=diffopts)
190 190
191 191 def parents(ctx, hide=None):
192 192 if isinstance(ctx, context.basefilectx):
193 193 introrev = ctx.introrev()
194 194 if ctx.changectx().rev() != introrev:
195 195 return _siblings([ctx.repo()[introrev]], hide)
196 196 return _siblings(ctx.parents(), hide)
197 197
198 198 def children(ctx, hide=None):
199 199 return _siblings(ctx.children(), hide)
200 200
201 201 def renamelink(fctx):
202 202 r = fctx.renamed()
203 203 if r:
204 204 return [{'file': r[0], 'node': hex(r[1])}]
205 205 return []
206 206
207 207 def nodetagsdict(repo, node):
208 208 return [{"name": i} for i in repo.nodetags(node)]
209 209
210 210 def nodebookmarksdict(repo, node):
211 211 return [{"name": i} for i in repo.nodebookmarks(node)]
212 212
213 213 def nodebranchdict(repo, ctx):
214 214 branches = []
215 215 branch = ctx.branch()
216 216 # If this is an empty repo, ctx.node() == nullid,
217 217 # ctx.branch() == 'default'.
218 218 try:
219 219 branchnode = repo.branchtip(branch)
220 220 except error.RepoLookupError:
221 221 branchnode = None
222 222 if branchnode == ctx.node():
223 223 branches.append({"name": branch})
224 224 return branches
225 225
226 226 def nodeinbranch(repo, ctx):
227 227 branches = []
228 228 branch = ctx.branch()
229 229 try:
230 230 branchnode = repo.branchtip(branch)
231 231 except error.RepoLookupError:
232 232 branchnode = None
233 233 if branch != 'default' and branchnode != ctx.node():
234 234 branches.append({"name": branch})
235 235 return branches
236 236
237 237 def nodebranchnodefault(ctx):
238 238 branches = []
239 239 branch = ctx.branch()
240 240 if branch != 'default':
241 241 branches.append({"name": branch})
242 242 return branches
243 243
244 244 def showtag(repo, tmpl, t1, node=nullid, **args):
245 245 for t in repo.nodetags(node):
246 246 yield tmpl(t1, tag=t, **args)
247 247
248 248 def showbookmark(repo, tmpl, t1, node=nullid, **args):
249 249 for t in repo.nodebookmarks(node):
250 250 yield tmpl(t1, bookmark=t, **args)
251 251
252 252 def branchentries(repo, stripecount, limit=0):
253 253 tips = []
254 254 heads = repo.heads()
255 255 parity = paritygen(stripecount)
256 256 sortkey = lambda item: (not item[1], item[0].rev())
257 257
258 258 def entries(**map):
259 259 count = 0
260 260 if not tips:
261 261 for tag, hs, tip, closed in repo.branchmap().iterbranches():
262 262 tips.append((repo[tip], closed))
263 263 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
264 264 if limit > 0 and count >= limit:
265 265 return
266 266 count += 1
267 267 if closed:
268 268 status = 'closed'
269 269 elif ctx.node() not in heads:
270 270 status = 'inactive'
271 271 else:
272 272 status = 'open'
273 273 yield {
274 274 'parity': next(parity),
275 275 'branch': ctx.branch(),
276 276 'status': status,
277 277 'node': ctx.hex(),
278 278 'date': ctx.date()
279 279 }
280 280
281 281 return entries
282 282
283 283 def cleanpath(repo, path):
284 284 path = path.lstrip('/')
285 285 return pathutil.canonpath(repo.root, '', path)
286 286
287 287 def changeidctx(repo, changeid):
288 288 try:
289 289 ctx = repo[changeid]
290 290 except error.RepoError:
291 291 man = repo.manifestlog._revlog
292 292 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
293 293
294 294 return ctx
295 295
296 296 def changectx(repo, req):
297 297 changeid = "tip"
298 298 if 'node' in req.form:
299 299 changeid = req.form['node'][0]
300 300 ipos = changeid.find(':')
301 301 if ipos != -1:
302 302 changeid = changeid[(ipos + 1):]
303 303 elif 'manifest' in req.form:
304 304 changeid = req.form['manifest'][0]
305 305
306 306 return changeidctx(repo, changeid)
307 307
308 308 def basechangectx(repo, req):
309 309 if 'node' in req.form:
310 310 changeid = req.form['node'][0]
311 311 ipos = changeid.find(':')
312 312 if ipos != -1:
313 313 changeid = changeid[:ipos]
314 314 return changeidctx(repo, changeid)
315 315
316 316 return None
317 317
318 318 def filectx(repo, req):
319 319 if 'file' not in req.form:
320 320 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
321 321 path = cleanpath(repo, req.form['file'][0])
322 322 if 'node' in req.form:
323 323 changeid = req.form['node'][0]
324 324 elif 'filenode' in req.form:
325 325 changeid = req.form['filenode'][0]
326 326 else:
327 327 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
328 328 try:
329 329 fctx = repo[changeid][path]
330 330 except error.RepoError:
331 331 fctx = repo.filectx(path, fileid=changeid)
332 332
333 333 return fctx
334 334
335 335 def linerange(req):
336 336 linerange = req.form.get('linerange')
337 337 if linerange is None:
338 338 return None
339 339 if len(linerange) > 1:
340 340 raise ErrorResponse(HTTP_BAD_REQUEST,
341 341 'redundant linerange parameter')
342 342 try:
343 343 fromline, toline = map(int, linerange[0].split(':', 1))
344 344 except ValueError:
345 345 raise ErrorResponse(HTTP_BAD_REQUEST,
346 346 'invalid linerange parameter')
347 347 try:
348 348 return util.processlinerange(fromline, toline)
349 349 except error.ParseError as exc:
350 350 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
351 351
352 352 def formatlinerange(fromline, toline):
353 353 return '%d:%d' % (fromline + 1, toline)
354 354
355 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 415 if 'node' in req.form:
416 416 return templatefilters.revescape(req.form['node'][0])
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 if 'style' in req.form:
442 style = req.form['style'][0]
441 if 'style' in req.req.qsparams:
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
General Comments 0
You need to be logged in to leave comments. Login now