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