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