##// END OF EJS Templates
hgweb: move archive related attributes to requestcontext...
Gregory Szorc -
r26136:6defc74f default
parent child Browse files
Show More
@@ -1,489 +1,486
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 os, re
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 63
64 64 class requestcontext(object):
65 65 """Holds state/context for an individual request.
66 66
67 67 Servers can be multi-threaded. Holding state on the WSGI application
68 68 is prone to race conditions. Instances of this class exist to hold
69 69 mutable and race-free state for requests.
70 70 """
71 71 def __init__(self, app):
72 72 object.__setattr__(self, 'app', app)
73 73 object.__setattr__(self, 'repo', app.repo)
74 74
75 object.__setattr__(self, 'archives', ('zip', 'gz', 'bz2'))
76
75 77 object.__setattr__(self, 'maxchanges',
76 78 self.configint('web', 'maxchanges', 10))
77 79 object.__setattr__(self, 'stripecount',
78 80 self.configint('web', 'stripes', 1))
79 81 object.__setattr__(self, 'maxshortchanges',
80 82 self.configint('web', 'maxshortchanges', 60))
81 83 object.__setattr__(self, 'maxfiles',
82 84 self.configint('web', 'maxfiles', 10))
83 85 object.__setattr__(self, 'allowpull',
84 86 self.configbool('web', 'allowpull', True))
85 87
86 88 # Proxy unknown reads and writes to the application instance
87 89 # until everything is moved to us.
88 90 def __getattr__(self, name):
89 91 return getattr(self.app, name)
90 92
91 93 def __setattr__(self, name, value):
92 94 return setattr(self.app, name, value)
93 95
94 96 # Servers are often run by a user different from the repo owner.
95 97 # Trust the settings from the .hg/hgrc files by default.
96 98 def config(self, section, name, default=None, untrusted=True):
97 99 return self.repo.ui.config(section, name, default,
98 100 untrusted=untrusted)
99 101
100 102 def configbool(self, section, name, default=False, untrusted=True):
101 103 return self.repo.ui.configbool(section, name, default,
102 104 untrusted=untrusted)
103 105
104 106 def configint(self, section, name, default=None, untrusted=True):
105 107 return self.repo.ui.configint(section, name, default,
106 108 untrusted=untrusted)
107 109
108 110 def configlist(self, section, name, default=None, untrusted=True):
109 111 return self.repo.ui.configlist(section, name, default,
110 112 untrusted=untrusted)
111 113
114 archivespecs = {
115 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
116 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
117 'zip': ('application/zip', 'zip', '.zip', None),
118 }
119
120 def archivelist(self, nodeid):
121 allowed = self.configlist('web', 'allow_archive')
122 for typ, spec in self.archivespecs.iteritems():
123 if typ in allowed or self.configbool('web', 'allow%s' % typ):
124 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
125
112 126 class hgweb(object):
113 127 """HTTP server for individual repositories.
114 128
115 129 Instances of this class serve HTTP responses for a particular
116 130 repository.
117 131
118 132 Instances are typically used as WSGI applications.
119 133
120 134 Some servers are multi-threaded. On these servers, there may
121 135 be multiple active threads inside __call__.
122 136 """
123 137 def __init__(self, repo, name=None, baseui=None):
124 138 if isinstance(repo, str):
125 139 if baseui:
126 140 u = baseui.copy()
127 141 else:
128 142 u = ui.ui()
129 143 r = hg.repository(u, repo)
130 144 else:
131 145 # we trust caller to give us a private copy
132 146 r = repo
133 147
134 148 r = self._getview(r)
135 149 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
136 150 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
137 151 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
138 152 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
139 153 # displaying bundling progress bar while serving feel wrong and may
140 154 # break some wsgi implementation.
141 155 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
142 156 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
143 157 self.repo = r
144 158 hook.redirect(True)
145 159 self.repostate = ((-1, -1), (-1, -1))
146 160 self.mtime = -1
147 161 self.reponame = name
148 self.archives = 'zip', 'gz', 'bz2'
149 162 # a repo owner may set web.templates in .hg/hgrc to get any file
150 163 # readable by the user running the CGI script
151 164 self.templatepath = self.config('web', 'templates')
152 165 self.websubtable = self.loadwebsub()
153 166
154 167 # The CGI scripts are often run by a user different from the repo owner.
155 168 # Trust the settings from the .hg/hgrc files by default.
156 169 def config(self, section, name, default=None, untrusted=True):
157 170 return self.repo.ui.config(section, name, default,
158 171 untrusted=untrusted)
159 172
160 173 def configbool(self, section, name, default=False, untrusted=True):
161 174 return self.repo.ui.configbool(section, name, default,
162 175 untrusted=untrusted)
163 176
164 def configlist(self, section, name, default=None, untrusted=True):
165 return self.repo.ui.configlist(section, name, default,
166 untrusted=untrusted)
167
168 177 def _getview(self, repo):
169 178 """The 'web.view' config controls changeset filter to hgweb. Possible
170 179 values are ``served``, ``visible`` and ``all``. Default is ``served``.
171 180 The ``served`` filter only shows changesets that can be pulled from the
172 181 hgweb instance. The``visible`` filter includes secret changesets but
173 182 still excludes "hidden" one.
174 183
175 184 See the repoview module for details.
176 185
177 186 The option has been around undocumented since Mercurial 2.5, but no
178 187 user ever asked about it. So we better keep it undocumented for now."""
179 188 viewconfig = repo.ui.config('web', 'view', 'served',
180 189 untrusted=True)
181 190 if viewconfig == 'all':
182 191 return repo.unfiltered()
183 192 elif viewconfig in repoview.filtertable:
184 193 return repo.filtered(viewconfig)
185 194 else:
186 195 return repo.filtered('served')
187 196
188 197 def refresh(self, request):
189 198 repostate = []
190 199 # file of interrests mtime and size
191 200 for meth, fname in foi:
192 201 prefix = getattr(self.repo, meth)
193 202 st = get_stat(prefix, fname)
194 203 repostate.append((st.st_mtime, st.st_size))
195 204 repostate = tuple(repostate)
196 205 # we need to compare file size in addition to mtime to catch
197 206 # changes made less than a second ago
198 207 if repostate != self.repostate:
199 208 r = hg.repository(self.repo.baseui, self.repo.url())
200 209 self.repo = self._getview(r)
201 210 encoding.encoding = self.config("web", "encoding",
202 211 encoding.encoding)
203 212 # update these last to avoid threads seeing empty settings
204 213 self.repostate = repostate
205 214 # mtime is needed for ETag
206 215 self.mtime = st.st_mtime
207 216
208 217 self.repo.ui.environ = request.env
209 218
210 219 def run(self):
211 220 """Start a server from CGI environment.
212 221
213 222 Modern servers should be using WSGI and should avoid this
214 223 method, if possible.
215 224 """
216 225 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
217 226 raise RuntimeError("This function is only intended to be "
218 227 "called while running as a CGI script.")
219 228 import mercurial.hgweb.wsgicgi as wsgicgi
220 229 wsgicgi.launch(self)
221 230
222 231 def __call__(self, env, respond):
223 232 """Run the WSGI application.
224 233
225 234 This may be called by multiple threads.
226 235 """
227 236 req = wsgirequest(env, respond)
228 237 return self.run_wsgi(req)
229 238
230 239 def run_wsgi(self, req):
231 240 """Internal method to run the WSGI application.
232 241
233 242 This is typically only called by Mercurial. External consumers
234 243 should be using instances of this class as the WSGI application.
235 244 """
236 245 self.refresh(req)
237 246 rctx = requestcontext(self)
238 247
239 248 # work with CGI variables to create coherent structure
240 249 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
241 250
242 251 req.url = req.env['SCRIPT_NAME']
243 252 if not req.url.endswith('/'):
244 253 req.url += '/'
245 254 if 'REPO_NAME' in req.env:
246 255 req.url += req.env['REPO_NAME'] + '/'
247 256
248 257 if 'PATH_INFO' in req.env:
249 258 parts = req.env['PATH_INFO'].strip('/').split('/')
250 259 repo_parts = req.env.get('REPO_NAME', '').split('/')
251 260 if parts[:len(repo_parts)] == repo_parts:
252 261 parts = parts[len(repo_parts):]
253 262 query = '/'.join(parts)
254 263 else:
255 264 query = req.env['QUERY_STRING'].split('&', 1)[0]
256 265 query = query.split(';', 1)[0]
257 266
258 267 # process this if it's a protocol request
259 268 # protocol bits don't need to create any URLs
260 269 # and the clients always use the old URL structure
261 270
262 271 cmd = req.form.get('cmd', [''])[0]
263 272 if protocol.iscmd(cmd):
264 273 try:
265 274 if query:
266 275 raise ErrorResponse(HTTP_NOT_FOUND)
267 276 if cmd in perms:
268 277 self.check_perm(rctx, req, perms[cmd])
269 278 return protocol.call(self.repo, req, cmd)
270 279 except ErrorResponse as inst:
271 280 # A client that sends unbundle without 100-continue will
272 281 # break if we respond early.
273 282 if (cmd == 'unbundle' and
274 283 (req.env.get('HTTP_EXPECT',
275 284 '').lower() != '100-continue') or
276 285 req.env.get('X-HgHttp2', '')):
277 286 req.drain()
278 287 else:
279 288 req.headers.append(('Connection', 'Close'))
280 289 req.respond(inst, protocol.HGTYPE,
281 290 body='0\n%s\n' % inst.message)
282 291 return ''
283 292
284 293 # translate user-visible url structure to internal structure
285 294
286 295 args = query.split('/', 2)
287 296 if 'cmd' not in req.form and args and args[0]:
288 297
289 298 cmd = args.pop(0)
290 299 style = cmd.rfind('-')
291 300 if style != -1:
292 301 req.form['style'] = [cmd[:style]]
293 302 cmd = cmd[style + 1:]
294 303
295 304 # avoid accepting e.g. style parameter as command
296 305 if util.safehasattr(webcommands, cmd):
297 306 req.form['cmd'] = [cmd]
298 307
299 308 if cmd == 'static':
300 309 req.form['file'] = ['/'.join(args)]
301 310 else:
302 311 if args and args[0]:
303 312 node = args.pop(0).replace('%2F', '/')
304 313 req.form['node'] = [node]
305 314 if args:
306 315 req.form['file'] = args
307 316
308 317 ua = req.env.get('HTTP_USER_AGENT', '')
309 318 if cmd == 'rev' and 'mercurial' in ua:
310 319 req.form['style'] = ['raw']
311 320
312 321 if cmd == 'archive':
313 322 fn = req.form['node'][0]
314 for type_, spec in self.archive_specs.iteritems():
323 for type_, spec in rctx.archivespecs.iteritems():
315 324 ext = spec[2]
316 325 if fn.endswith(ext):
317 326 req.form['node'] = [fn[:-len(ext)]]
318 327 req.form['type'] = [type_]
319 328
320 329 # process the web interface request
321 330
322 331 try:
323 332 tmpl = self.templater(req)
324 333 ctype = tmpl('mimetype', encoding=encoding.encoding)
325 334 ctype = templater.stringify(ctype)
326 335
327 336 # check read permissions non-static content
328 337 if cmd != 'static':
329 338 self.check_perm(rctx, req, None)
330 339
331 340 if cmd == '':
332 341 req.form['cmd'] = [tmpl.cache['default']]
333 342 cmd = req.form['cmd'][0]
334 343
335 344 if self.configbool('web', 'cache', True):
336 345 caching(self, req) # sets ETag header or raises NOT_MODIFIED
337 346 if cmd not in webcommands.__all__:
338 347 msg = 'no such method: %s' % cmd
339 348 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
340 349 elif cmd == 'file' and 'raw' in req.form.get('style', []):
341 350 self.ctype = ctype
342 351 content = webcommands.rawfile(rctx, req, tmpl)
343 352 else:
344 353 content = getattr(webcommands, cmd)(rctx, req, tmpl)
345 354 req.respond(HTTP_OK, ctype)
346 355
347 356 return content
348 357
349 358 except (error.LookupError, error.RepoLookupError) as err:
350 359 req.respond(HTTP_NOT_FOUND, ctype)
351 360 msg = str(err)
352 361 if (util.safehasattr(err, 'name') and
353 362 not isinstance(err, error.ManifestLookupError)):
354 363 msg = 'revision not found: %s' % err.name
355 364 return tmpl('error', error=msg)
356 365 except (error.RepoError, error.RevlogError) as inst:
357 366 req.respond(HTTP_SERVER_ERROR, ctype)
358 367 return tmpl('error', error=str(inst))
359 368 except ErrorResponse as inst:
360 369 req.respond(inst, ctype)
361 370 if inst.code == HTTP_NOT_MODIFIED:
362 371 # Not allowed to return a body on a 304
363 372 return ['']
364 373 return tmpl('error', error=inst.message)
365 374
366 375 def loadwebsub(self):
367 376 websubtable = []
368 377 websubdefs = self.repo.ui.configitems('websub')
369 378 # we must maintain interhg backwards compatibility
370 379 websubdefs += self.repo.ui.configitems('interhg')
371 380 for key, pattern in websubdefs:
372 381 # grab the delimiter from the character after the "s"
373 382 unesc = pattern[1]
374 383 delim = re.escape(unesc)
375 384
376 385 # identify portions of the pattern, taking care to avoid escaped
377 386 # delimiters. the replace format and flags are optional, but
378 387 # delimiters are required.
379 388 match = re.match(
380 389 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
381 390 % (delim, delim, delim), pattern)
382 391 if not match:
383 392 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
384 393 % (key, pattern))
385 394 continue
386 395
387 396 # we need to unescape the delimiter for regexp and format
388 397 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
389 398 regexp = delim_re.sub(unesc, match.group(1))
390 399 format = delim_re.sub(unesc, match.group(2))
391 400
392 401 # the pattern allows for 6 regexp flags, so set them if necessary
393 402 flagin = match.group(3)
394 403 flags = 0
395 404 if flagin:
396 405 for flag in flagin.upper():
397 406 flags |= re.__dict__[flag]
398 407
399 408 try:
400 409 regexp = re.compile(regexp, flags)
401 410 websubtable.append((regexp, format))
402 411 except re.error:
403 412 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
404 413 % (key, regexp))
405 414 return websubtable
406 415
407 416 def templater(self, req):
408 417
409 418 # determine scheme, port and server name
410 419 # this is needed to create absolute urls
411 420
412 421 proto = req.env.get('wsgi.url_scheme')
413 422 if proto == 'https':
414 423 proto = 'https'
415 424 default_port = "443"
416 425 else:
417 426 proto = 'http'
418 427 default_port = "80"
419 428
420 429 port = req.env["SERVER_PORT"]
421 430 port = port != default_port and (":" + port) or ""
422 431 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
423 432 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
424 433 logoimg = self.config("web", "logoimg", "hglogo.png")
425 434 staticurl = self.config("web", "staticurl") or req.url + 'static/'
426 435 if not staticurl.endswith('/'):
427 436 staticurl += '/'
428 437
429 438 # some functions for the templater
430 439
431 440 def motd(**map):
432 441 yield self.config("web", "motd", "")
433 442
434 443 # figure out which style to use
435 444
436 445 vars = {}
437 446 styles = (
438 447 req.form.get('style', [None])[0],
439 448 self.config('web', 'style'),
440 449 'paper',
441 450 )
442 451 style, mapfile = templater.stylemap(styles, self.templatepath)
443 452 if style == styles[0]:
444 453 vars['style'] = style
445 454
446 455 start = req.url[-1] == '?' and '&' or '?'
447 456 sessionvars = webutil.sessionvars(vars, start)
448 457
449 458 if not self.reponame:
450 459 self.reponame = (self.config("web", "name")
451 460 or req.env.get('REPO_NAME')
452 461 or req.url.strip('/') or self.repo.root)
453 462
454 463 def websubfilter(text):
455 464 return websub(text, self.websubtable)
456 465
457 466 # create the templater
458 467
459 468 tmpl = templater.templater(mapfile,
460 469 filters={"websub": websubfilter},
461 470 defaults={"url": req.url,
462 471 "logourl": logourl,
463 472 "logoimg": logoimg,
464 473 "staticurl": staticurl,
465 474 "urlbase": urlbase,
466 475 "repo": self.reponame,
467 476 "encoding": encoding.encoding,
468 477 "motd": motd,
469 478 "sessionvars": sessionvars,
470 479 "pathdef": makebreadcrumb(req.url),
471 480 "style": style,
472 481 })
473 482 return tmpl
474 483
475 def archivelist(self, nodeid):
476 allowed = self.configlist("web", "allow_archive")
477 for i, spec in self.archive_specs.iteritems():
478 if i in allowed or self.configbool("web", "allow" + i):
479 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
480
481 archive_specs = {
482 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
483 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
484 'zip': ('application/zip', 'zip', '.zip', None),
485 }
486
487 484 def check_perm(self, rctx, req, op):
488 485 for permhook in permhooks:
489 486 permhook(rctx, req, op)
@@ -1,1315 +1,1315
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 import os, mimetypes, re, cgi, copy
9 9 import webutil
10 10 from mercurial import error, encoding, archival, templater, templatefilters
11 11 from mercurial.node import short, hex
12 12 from mercurial import util
13 13 from common import paritygen, staticfile, get_contact, ErrorResponse
14 14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
15 15 from mercurial import graphmod, patch
16 16 from mercurial import scmutil
17 17 from mercurial.i18n import _
18 18 from mercurial.error import ParseError, RepoLookupError, Abort
19 19 from mercurial import revset
20 20
21 21 __all__ = []
22 22 commands = {}
23 23
24 24 class webcommand(object):
25 25 """Decorator used to register a web command handler.
26 26
27 27 The decorator takes as its positional arguments the name/path the
28 28 command should be accessible under.
29 29
30 30 Usage:
31 31
32 32 @webcommand('mycommand')
33 33 def mycommand(web, req, tmpl):
34 34 pass
35 35 """
36 36
37 37 def __init__(self, name):
38 38 self.name = name
39 39
40 40 def __call__(self, func):
41 41 __all__.append(self.name)
42 42 commands[self.name] = func
43 43 return func
44 44
45 45 @webcommand('log')
46 46 def log(web, req, tmpl):
47 47 """
48 48 /log[/{revision}[/{path}]]
49 49 --------------------------
50 50
51 51 Show repository or file history.
52 52
53 53 For URLs of the form ``/log/{revision}``, a list of changesets starting at
54 54 the specified changeset identifier is shown. If ``{revision}`` is not
55 55 defined, the default is ``tip``. This form is equivalent to the
56 56 ``changelog`` handler.
57 57
58 58 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
59 59 file will be shown. This form is equivalent to the ``filelog`` handler.
60 60 """
61 61
62 62 if 'file' in req.form and req.form['file'][0]:
63 63 return filelog(web, req, tmpl)
64 64 else:
65 65 return changelog(web, req, tmpl)
66 66
67 67 @webcommand('rawfile')
68 68 def rawfile(web, req, tmpl):
69 69 guessmime = web.configbool('web', 'guessmime', False)
70 70
71 71 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
72 72 if not path:
73 73 content = manifest(web, req, tmpl)
74 74 req.respond(HTTP_OK, web.ctype)
75 75 return content
76 76
77 77 try:
78 78 fctx = webutil.filectx(web.repo, req)
79 79 except error.LookupError as inst:
80 80 try:
81 81 content = manifest(web, req, tmpl)
82 82 req.respond(HTTP_OK, web.ctype)
83 83 return content
84 84 except ErrorResponse:
85 85 raise inst
86 86
87 87 path = fctx.path()
88 88 text = fctx.data()
89 89 mt = 'application/binary'
90 90 if guessmime:
91 91 mt = mimetypes.guess_type(path)[0]
92 92 if mt is None:
93 93 if util.binary(text):
94 94 mt = 'application/binary'
95 95 else:
96 96 mt = 'text/plain'
97 97 if mt.startswith('text/'):
98 98 mt += '; charset="%s"' % encoding.encoding
99 99
100 100 req.respond(HTTP_OK, mt, path, body=text)
101 101 return []
102 102
103 103 def _filerevision(web, req, tmpl, fctx):
104 104 f = fctx.path()
105 105 text = fctx.data()
106 106 parity = paritygen(web.stripecount)
107 107
108 108 if util.binary(text):
109 109 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
110 110 text = '(binary:%s)' % mt
111 111
112 112 def lines():
113 113 for lineno, t in enumerate(text.splitlines(True)):
114 114 yield {"line": t,
115 115 "lineid": "l%d" % (lineno + 1),
116 116 "linenumber": "% 6d" % (lineno + 1),
117 117 "parity": parity.next()}
118 118
119 119 return tmpl("filerevision",
120 120 file=f,
121 121 path=webutil.up(f),
122 122 text=lines(),
123 123 rev=fctx.rev(),
124 124 symrev=webutil.symrevorshortnode(req, fctx),
125 125 node=fctx.hex(),
126 126 author=fctx.user(),
127 127 date=fctx.date(),
128 128 desc=fctx.description(),
129 129 extra=fctx.extra(),
130 130 branch=webutil.nodebranchnodefault(fctx),
131 131 parent=webutil.parents(fctx),
132 132 child=webutil.children(fctx),
133 133 rename=webutil.renamelink(fctx),
134 134 tags=webutil.nodetagsdict(web.repo, fctx.node()),
135 135 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
136 136 permissions=fctx.manifest().flags(f))
137 137
138 138 @webcommand('file')
139 139 def file(web, req, tmpl):
140 140 """
141 141 /file/{revision}[/{path}]
142 142 -------------------------
143 143
144 144 Show information about a directory or file in the repository.
145 145
146 146 Info about the ``path`` given as a URL parameter will be rendered.
147 147
148 148 If ``path`` is a directory, information about the entries in that
149 149 directory will be rendered. This form is equivalent to the ``manifest``
150 150 handler.
151 151
152 152 If ``path`` is a file, information about that file will be shown via
153 153 the ``filerevision`` template.
154 154
155 155 If ``path`` is not defined, information about the root directory will
156 156 be rendered.
157 157 """
158 158 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
159 159 if not path:
160 160 return manifest(web, req, tmpl)
161 161 try:
162 162 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
163 163 except error.LookupError as inst:
164 164 try:
165 165 return manifest(web, req, tmpl)
166 166 except ErrorResponse:
167 167 raise inst
168 168
169 169 def _search(web, req, tmpl):
170 170 MODE_REVISION = 'rev'
171 171 MODE_KEYWORD = 'keyword'
172 172 MODE_REVSET = 'revset'
173 173
174 174 def revsearch(ctx):
175 175 yield ctx
176 176
177 177 def keywordsearch(query):
178 178 lower = encoding.lower
179 179 qw = lower(query).split()
180 180
181 181 def revgen():
182 182 cl = web.repo.changelog
183 183 for i in xrange(len(web.repo) - 1, 0, -100):
184 184 l = []
185 185 for j in cl.revs(max(0, i - 99), i):
186 186 ctx = web.repo[j]
187 187 l.append(ctx)
188 188 l.reverse()
189 189 for e in l:
190 190 yield e
191 191
192 192 for ctx in revgen():
193 193 miss = 0
194 194 for q in qw:
195 195 if not (q in lower(ctx.user()) or
196 196 q in lower(ctx.description()) or
197 197 q in lower(" ".join(ctx.files()))):
198 198 miss = 1
199 199 break
200 200 if miss:
201 201 continue
202 202
203 203 yield ctx
204 204
205 205 def revsetsearch(revs):
206 206 for r in revs:
207 207 yield web.repo[r]
208 208
209 209 searchfuncs = {
210 210 MODE_REVISION: (revsearch, 'exact revision search'),
211 211 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
212 212 MODE_REVSET: (revsetsearch, 'revset expression search'),
213 213 }
214 214
215 215 def getsearchmode(query):
216 216 try:
217 217 ctx = web.repo[query]
218 218 except (error.RepoError, error.LookupError):
219 219 # query is not an exact revision pointer, need to
220 220 # decide if it's a revset expression or keywords
221 221 pass
222 222 else:
223 223 return MODE_REVISION, ctx
224 224
225 225 revdef = 'reverse(%s)' % query
226 226 try:
227 227 tree = revset.parse(revdef)
228 228 except ParseError:
229 229 # can't parse to a revset tree
230 230 return MODE_KEYWORD, query
231 231
232 232 if revset.depth(tree) <= 2:
233 233 # no revset syntax used
234 234 return MODE_KEYWORD, query
235 235
236 236 if any((token, (value or '')[:3]) == ('string', 're:')
237 237 for token, value, pos in revset.tokenize(revdef)):
238 238 return MODE_KEYWORD, query
239 239
240 240 funcsused = revset.funcsused(tree)
241 241 if not funcsused.issubset(revset.safesymbols):
242 242 return MODE_KEYWORD, query
243 243
244 244 mfunc = revset.match(web.repo.ui, revdef)
245 245 try:
246 246 revs = mfunc(web.repo)
247 247 return MODE_REVSET, revs
248 248 # ParseError: wrongly placed tokens, wrongs arguments, etc
249 249 # RepoLookupError: no such revision, e.g. in 'revision:'
250 250 # Abort: bookmark/tag not exists
251 251 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
252 252 except (ParseError, RepoLookupError, Abort, LookupError):
253 253 return MODE_KEYWORD, query
254 254
255 255 def changelist(**map):
256 256 count = 0
257 257
258 258 for ctx in searchfunc[0](funcarg):
259 259 count += 1
260 260 n = ctx.node()
261 261 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
262 262 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
263 263
264 264 yield tmpl('searchentry',
265 265 parity=parity.next(),
266 266 author=ctx.user(),
267 267 parent=webutil.parents(ctx),
268 268 child=webutil.children(ctx),
269 269 changelogtag=showtags,
270 270 desc=ctx.description(),
271 271 extra=ctx.extra(),
272 272 date=ctx.date(),
273 273 files=files,
274 274 rev=ctx.rev(),
275 275 node=hex(n),
276 276 tags=webutil.nodetagsdict(web.repo, n),
277 277 bookmarks=webutil.nodebookmarksdict(web.repo, n),
278 278 inbranch=webutil.nodeinbranch(web.repo, ctx),
279 279 branches=webutil.nodebranchdict(web.repo, ctx))
280 280
281 281 if count >= revcount:
282 282 break
283 283
284 284 query = req.form['rev'][0]
285 285 revcount = web.maxchanges
286 286 if 'revcount' in req.form:
287 287 try:
288 288 revcount = int(req.form.get('revcount', [revcount])[0])
289 289 revcount = max(revcount, 1)
290 290 tmpl.defaults['sessionvars']['revcount'] = revcount
291 291 except ValueError:
292 292 pass
293 293
294 294 lessvars = copy.copy(tmpl.defaults['sessionvars'])
295 295 lessvars['revcount'] = max(revcount / 2, 1)
296 296 lessvars['rev'] = query
297 297 morevars = copy.copy(tmpl.defaults['sessionvars'])
298 298 morevars['revcount'] = revcount * 2
299 299 morevars['rev'] = query
300 300
301 301 mode, funcarg = getsearchmode(query)
302 302
303 303 if 'forcekw' in req.form:
304 304 showforcekw = ''
305 305 showunforcekw = searchfuncs[mode][1]
306 306 mode = MODE_KEYWORD
307 307 funcarg = query
308 308 else:
309 309 if mode != MODE_KEYWORD:
310 310 showforcekw = searchfuncs[MODE_KEYWORD][1]
311 311 else:
312 312 showforcekw = ''
313 313 showunforcekw = ''
314 314
315 315 searchfunc = searchfuncs[mode]
316 316
317 317 tip = web.repo['tip']
318 318 parity = paritygen(web.stripecount)
319 319
320 320 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
321 321 entries=changelist, archives=web.archivelist("tip"),
322 322 morevars=morevars, lessvars=lessvars,
323 323 modedesc=searchfunc[1],
324 324 showforcekw=showforcekw, showunforcekw=showunforcekw)
325 325
326 326 @webcommand('changelog')
327 327 def changelog(web, req, tmpl, shortlog=False):
328 328 """
329 329 /changelog[/{revision}]
330 330 -----------------------
331 331
332 332 Show information about multiple changesets.
333 333
334 334 If the optional ``revision`` URL argument is absent, information about
335 335 all changesets starting at ``tip`` will be rendered. If the ``revision``
336 336 argument is present, changesets will be shown starting from the specified
337 337 revision.
338 338
339 339 If ``revision`` is absent, the ``rev`` query string argument may be
340 340 defined. This will perform a search for changesets.
341 341
342 342 The argument for ``rev`` can be a single revision, a revision set,
343 343 or a literal keyword to search for in changeset data (equivalent to
344 344 :hg:`log -k`).
345 345
346 346 The ``revcount`` query string argument defines the maximum numbers of
347 347 changesets to render.
348 348
349 349 For non-searches, the ``changelog`` template will be rendered.
350 350 """
351 351
352 352 query = ''
353 353 if 'node' in req.form:
354 354 ctx = webutil.changectx(web.repo, req)
355 355 symrev = webutil.symrevorshortnode(req, ctx)
356 356 elif 'rev' in req.form:
357 357 return _search(web, req, tmpl)
358 358 else:
359 359 ctx = web.repo['tip']
360 360 symrev = 'tip'
361 361
362 362 def changelist():
363 363 revs = []
364 364 if pos != -1:
365 365 revs = web.repo.changelog.revs(pos, 0)
366 366 curcount = 0
367 367 for rev in revs:
368 368 curcount += 1
369 369 if curcount > revcount + 1:
370 370 break
371 371
372 372 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
373 373 entry['parity'] = parity.next()
374 374 yield entry
375 375
376 376 if shortlog:
377 377 revcount = web.maxshortchanges
378 378 else:
379 379 revcount = web.maxchanges
380 380
381 381 if 'revcount' in req.form:
382 382 try:
383 383 revcount = int(req.form.get('revcount', [revcount])[0])
384 384 revcount = max(revcount, 1)
385 385 tmpl.defaults['sessionvars']['revcount'] = revcount
386 386 except ValueError:
387 387 pass
388 388
389 389 lessvars = copy.copy(tmpl.defaults['sessionvars'])
390 390 lessvars['revcount'] = max(revcount / 2, 1)
391 391 morevars = copy.copy(tmpl.defaults['sessionvars'])
392 392 morevars['revcount'] = revcount * 2
393 393
394 394 count = len(web.repo)
395 395 pos = ctx.rev()
396 396 parity = paritygen(web.stripecount)
397 397
398 398 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
399 399
400 400 entries = list(changelist())
401 401 latestentry = entries[:1]
402 402 if len(entries) > revcount:
403 403 nextentry = entries[-1:]
404 404 entries = entries[:-1]
405 405 else:
406 406 nextentry = []
407 407
408 408 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
409 409 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
410 410 entries=entries,
411 411 latestentry=latestentry, nextentry=nextentry,
412 412 archives=web.archivelist("tip"), revcount=revcount,
413 413 morevars=morevars, lessvars=lessvars, query=query)
414 414
415 415 @webcommand('shortlog')
416 416 def shortlog(web, req, tmpl):
417 417 """
418 418 /shortlog
419 419 ---------
420 420
421 421 Show basic information about a set of changesets.
422 422
423 423 This accepts the same parameters as the ``changelog`` handler. The only
424 424 difference is the ``shortlog`` template will be rendered instead of the
425 425 ``changelog`` template.
426 426 """
427 427 return changelog(web, req, tmpl, shortlog=True)
428 428
429 429 @webcommand('changeset')
430 430 def changeset(web, req, tmpl):
431 431 """
432 432 /changeset[/{revision}]
433 433 -----------------------
434 434
435 435 Show information about a single changeset.
436 436
437 437 A URL path argument is the changeset identifier to show. See ``hg help
438 438 revisions`` for possible values. If not defined, the ``tip`` changeset
439 439 will be shown.
440 440
441 441 The ``changeset`` template is rendered. Contents of the ``changesettag``,
442 442 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
443 443 templates related to diffs may all be used to produce the output.
444 444 """
445 445 ctx = webutil.changectx(web.repo, req)
446 446
447 447 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
448 448
449 449 rev = webcommand('rev')(changeset)
450 450
451 451 def decodepath(path):
452 452 """Hook for mapping a path in the repository to a path in the
453 453 working copy.
454 454
455 455 Extensions (e.g., largefiles) can override this to remap files in
456 456 the virtual file system presented by the manifest command below."""
457 457 return path
458 458
459 459 @webcommand('manifest')
460 460 def manifest(web, req, tmpl):
461 461 """
462 462 /manifest[/{revision}[/{path}]]
463 463 -------------------------------
464 464
465 465 Show information about a directory.
466 466
467 467 If the URL path arguments are omitted, information about the root
468 468 directory for the ``tip`` changeset will be shown.
469 469
470 470 Because this handler can only show information for directories, it
471 471 is recommended to use the ``file`` handler instead, as it can handle both
472 472 directories and files.
473 473
474 474 The ``manifest`` template will be rendered for this handler.
475 475 """
476 476 if 'node' in req.form:
477 477 ctx = webutil.changectx(web.repo, req)
478 478 symrev = webutil.symrevorshortnode(req, ctx)
479 479 else:
480 480 ctx = web.repo['tip']
481 481 symrev = 'tip'
482 482 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
483 483 mf = ctx.manifest()
484 484 node = ctx.node()
485 485
486 486 files = {}
487 487 dirs = {}
488 488 parity = paritygen(web.stripecount)
489 489
490 490 if path and path[-1] != "/":
491 491 path += "/"
492 492 l = len(path)
493 493 abspath = "/" + path
494 494
495 495 for full, n in mf.iteritems():
496 496 # the virtual path (working copy path) used for the full
497 497 # (repository) path
498 498 f = decodepath(full)
499 499
500 500 if f[:l] != path:
501 501 continue
502 502 remain = f[l:]
503 503 elements = remain.split('/')
504 504 if len(elements) == 1:
505 505 files[remain] = full
506 506 else:
507 507 h = dirs # need to retain ref to dirs (root)
508 508 for elem in elements[0:-1]:
509 509 if elem not in h:
510 510 h[elem] = {}
511 511 h = h[elem]
512 512 if len(h) > 1:
513 513 break
514 514 h[None] = None # denotes files present
515 515
516 516 if mf and not files and not dirs:
517 517 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
518 518
519 519 def filelist(**map):
520 520 for f in sorted(files):
521 521 full = files[f]
522 522
523 523 fctx = ctx.filectx(full)
524 524 yield {"file": full,
525 525 "parity": parity.next(),
526 526 "basename": f,
527 527 "date": fctx.date(),
528 528 "size": fctx.size(),
529 529 "permissions": mf.flags(full)}
530 530
531 531 def dirlist(**map):
532 532 for d in sorted(dirs):
533 533
534 534 emptydirs = []
535 535 h = dirs[d]
536 536 while isinstance(h, dict) and len(h) == 1:
537 537 k, v = h.items()[0]
538 538 if v:
539 539 emptydirs.append(k)
540 540 h = v
541 541
542 542 path = "%s%s" % (abspath, d)
543 543 yield {"parity": parity.next(),
544 544 "path": path,
545 545 "emptydirs": "/".join(emptydirs),
546 546 "basename": d}
547 547
548 548 return tmpl("manifest",
549 549 rev=ctx.rev(),
550 550 symrev=symrev,
551 551 node=hex(node),
552 552 path=abspath,
553 553 up=webutil.up(abspath),
554 554 upparity=parity.next(),
555 555 fentries=filelist,
556 556 dentries=dirlist,
557 557 archives=web.archivelist(hex(node)),
558 558 tags=webutil.nodetagsdict(web.repo, node),
559 559 bookmarks=webutil.nodebookmarksdict(web.repo, node),
560 560 branch=webutil.nodebranchnodefault(ctx),
561 561 inbranch=webutil.nodeinbranch(web.repo, ctx),
562 562 branches=webutil.nodebranchdict(web.repo, ctx))
563 563
564 564 @webcommand('tags')
565 565 def tags(web, req, tmpl):
566 566 """
567 567 /tags
568 568 -----
569 569
570 570 Show information about tags.
571 571
572 572 No arguments are accepted.
573 573
574 574 The ``tags`` template is rendered.
575 575 """
576 576 i = list(reversed(web.repo.tagslist()))
577 577 parity = paritygen(web.stripecount)
578 578
579 579 def entries(notip, latestonly, **map):
580 580 t = i
581 581 if notip:
582 582 t = [(k, n) for k, n in i if k != "tip"]
583 583 if latestonly:
584 584 t = t[:1]
585 585 for k, n in t:
586 586 yield {"parity": parity.next(),
587 587 "tag": k,
588 588 "date": web.repo[n].date(),
589 589 "node": hex(n)}
590 590
591 591 return tmpl("tags",
592 592 node=hex(web.repo.changelog.tip()),
593 593 entries=lambda **x: entries(False, False, **x),
594 594 entriesnotip=lambda **x: entries(True, False, **x),
595 595 latestentry=lambda **x: entries(True, True, **x))
596 596
597 597 @webcommand('bookmarks')
598 598 def bookmarks(web, req, tmpl):
599 599 """
600 600 /bookmarks
601 601 ----------
602 602
603 603 Show information about bookmarks.
604 604
605 605 No arguments are accepted.
606 606
607 607 The ``bookmarks`` template is rendered.
608 608 """
609 609 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
610 610 parity = paritygen(web.stripecount)
611 611
612 612 def entries(latestonly, **map):
613 613 if latestonly:
614 614 t = [min(i)]
615 615 else:
616 616 t = sorted(i)
617 617 for k, n in t:
618 618 yield {"parity": parity.next(),
619 619 "bookmark": k,
620 620 "date": web.repo[n].date(),
621 621 "node": hex(n)}
622 622
623 623 return tmpl("bookmarks",
624 624 node=hex(web.repo.changelog.tip()),
625 625 entries=lambda **x: entries(latestonly=False, **x),
626 626 latestentry=lambda **x: entries(latestonly=True, **x))
627 627
628 628 @webcommand('branches')
629 629 def branches(web, req, tmpl):
630 630 """
631 631 /branches
632 632 ---------
633 633
634 634 Show information about branches.
635 635
636 636 All known branches are contained in the output, even closed branches.
637 637
638 638 No arguments are accepted.
639 639
640 640 The ``branches`` template is rendered.
641 641 """
642 642 entries = webutil.branchentries(web.repo, web.stripecount)
643 643 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
644 644 return tmpl('branches', node=hex(web.repo.changelog.tip()),
645 645 entries=entries, latestentry=latestentry)
646 646
647 647 @webcommand('summary')
648 648 def summary(web, req, tmpl):
649 649 """
650 650 /summary
651 651 --------
652 652
653 653 Show a summary of repository state.
654 654
655 655 Information about the latest changesets, bookmarks, tags, and branches
656 656 is captured by this handler.
657 657
658 658 The ``summary`` template is rendered.
659 659 """
660 660 i = reversed(web.repo.tagslist())
661 661
662 662 def tagentries(**map):
663 663 parity = paritygen(web.stripecount)
664 664 count = 0
665 665 for k, n in i:
666 666 if k == "tip": # skip tip
667 667 continue
668 668
669 669 count += 1
670 670 if count > 10: # limit to 10 tags
671 671 break
672 672
673 673 yield tmpl("tagentry",
674 674 parity=parity.next(),
675 675 tag=k,
676 676 node=hex(n),
677 677 date=web.repo[n].date())
678 678
679 679 def bookmarks(**map):
680 680 parity = paritygen(web.stripecount)
681 681 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
682 682 for k, n in sorted(marks)[:10]: # limit to 10 bookmarks
683 683 yield {'parity': parity.next(),
684 684 'bookmark': k,
685 685 'date': web.repo[n].date(),
686 686 'node': hex(n)}
687 687
688 688 def changelist(**map):
689 689 parity = paritygen(web.stripecount, offset=start - end)
690 690 l = [] # build a list in forward order for efficiency
691 691 revs = []
692 692 if start < end:
693 693 revs = web.repo.changelog.revs(start, end - 1)
694 694 for i in revs:
695 695 ctx = web.repo[i]
696 696 n = ctx.node()
697 697 hn = hex(n)
698 698
699 699 l.append(tmpl(
700 700 'shortlogentry',
701 701 parity=parity.next(),
702 702 author=ctx.user(),
703 703 desc=ctx.description(),
704 704 extra=ctx.extra(),
705 705 date=ctx.date(),
706 706 rev=i,
707 707 node=hn,
708 708 tags=webutil.nodetagsdict(web.repo, n),
709 709 bookmarks=webutil.nodebookmarksdict(web.repo, n),
710 710 inbranch=webutil.nodeinbranch(web.repo, ctx),
711 711 branches=webutil.nodebranchdict(web.repo, ctx)))
712 712
713 713 l.reverse()
714 714 yield l
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 return tmpl("summary",
722 722 desc=web.config("web", "description", "unknown"),
723 723 owner=get_contact(web.config) or "unknown",
724 724 lastchange=tip.date(),
725 725 tags=tagentries,
726 726 bookmarks=bookmarks,
727 727 branches=webutil.branchentries(web.repo, web.stripecount, 10),
728 728 shortlog=changelist,
729 729 node=tip.hex(),
730 730 symrev='tip',
731 731 archives=web.archivelist("tip"))
732 732
733 733 @webcommand('filediff')
734 734 def filediff(web, req, tmpl):
735 735 """
736 736 /diff/{revision}/{path}
737 737 -----------------------
738 738
739 739 Show how a file changed in a particular commit.
740 740
741 741 The ``filediff`` template is rendered.
742 742
743 743 This hander is registered under both the ``/diff`` and ``/filediff``
744 744 paths. ``/diff`` is used in modern code.
745 745 """
746 746 fctx, ctx = None, None
747 747 try:
748 748 fctx = webutil.filectx(web.repo, req)
749 749 except LookupError:
750 750 ctx = webutil.changectx(web.repo, req)
751 751 path = webutil.cleanpath(web.repo, req.form['file'][0])
752 752 if path not in ctx.files():
753 753 raise
754 754
755 755 if fctx is not None:
756 756 n = fctx.node()
757 757 path = fctx.path()
758 758 ctx = fctx.changectx()
759 759 else:
760 760 n = ctx.node()
761 761 # path already defined in except clause
762 762
763 763 parity = paritygen(web.stripecount)
764 764 style = web.config('web', 'style', 'paper')
765 765 if 'style' in req.form:
766 766 style = req.form['style'][0]
767 767
768 768 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
769 769 if fctx:
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 node=hex(n),
778 778 rev=ctx.rev(),
779 779 symrev=webutil.symrevorshortnode(req, ctx),
780 780 date=ctx.date(),
781 781 desc=ctx.description(),
782 782 extra=ctx.extra(),
783 783 author=ctx.user(),
784 784 rename=rename,
785 785 branch=webutil.nodebranchnodefault(ctx),
786 786 parent=webutil.parents(ctx),
787 787 child=webutil.children(ctx),
788 788 tags=webutil.nodetagsdict(web.repo, n),
789 789 bookmarks=webutil.nodebookmarksdict(web.repo, n),
790 790 diff=diffs)
791 791
792 792 diff = webcommand('diff')(filediff)
793 793
794 794 @webcommand('comparison')
795 795 def comparison(web, req, tmpl):
796 796 """
797 797 /comparison/{revision}/{path}
798 798 -----------------------------
799 799
800 800 Show a comparison between the old and new versions of a file from changes
801 801 made on a particular revision.
802 802
803 803 This is similar to the ``diff`` handler. However, this form features
804 804 a split or side-by-side diff rather than a unified diff.
805 805
806 806 The ``context`` query string argument can be used to control the lines of
807 807 context in the diff.
808 808
809 809 The ``filecomparison`` template is rendered.
810 810 """
811 811 ctx = webutil.changectx(web.repo, req)
812 812 if 'file' not in req.form:
813 813 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
814 814 path = webutil.cleanpath(web.repo, req.form['file'][0])
815 815 rename = path in ctx and webutil.renamelink(ctx[path]) or []
816 816
817 817 parsecontext = lambda v: v == 'full' and -1 or int(v)
818 818 if 'context' in req.form:
819 819 context = parsecontext(req.form['context'][0])
820 820 else:
821 821 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
822 822
823 823 def filelines(f):
824 824 if util.binary(f.data()):
825 825 mt = mimetypes.guess_type(f.path())[0]
826 826 if not mt:
827 827 mt = 'application/octet-stream'
828 828 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
829 829 return f.data().splitlines()
830 830
831 831 parent = ctx.p1()
832 832 leftrev = parent.rev()
833 833 leftnode = parent.node()
834 834 rightrev = ctx.rev()
835 835 rightnode = ctx.node()
836 836 if path in ctx:
837 837 fctx = ctx[path]
838 838 rightlines = filelines(fctx)
839 839 if path not in parent:
840 840 leftlines = ()
841 841 else:
842 842 pfctx = parent[path]
843 843 leftlines = filelines(pfctx)
844 844 else:
845 845 rightlines = ()
846 846 fctx = ctx.parents()[0][path]
847 847 leftlines = filelines(fctx)
848 848
849 849 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
850 850 return tmpl('filecomparison',
851 851 file=path,
852 852 node=hex(ctx.node()),
853 853 rev=ctx.rev(),
854 854 symrev=webutil.symrevorshortnode(req, ctx),
855 855 date=ctx.date(),
856 856 desc=ctx.description(),
857 857 extra=ctx.extra(),
858 858 author=ctx.user(),
859 859 rename=rename,
860 860 branch=webutil.nodebranchnodefault(ctx),
861 861 parent=webutil.parents(fctx),
862 862 child=webutil.children(fctx),
863 863 tags=webutil.nodetagsdict(web.repo, ctx.node()),
864 864 bookmarks=webutil.nodebookmarksdict(web.repo, ctx.node()),
865 865 leftrev=leftrev,
866 866 leftnode=hex(leftnode),
867 867 rightrev=rightrev,
868 868 rightnode=hex(rightnode),
869 869 comparison=comparison)
870 870
871 871 @webcommand('annotate')
872 872 def annotate(web, req, tmpl):
873 873 """
874 874 /annotate/{revision}/{path}
875 875 ---------------------------
876 876
877 877 Show changeset information for each line in a file.
878 878
879 879 The ``fileannotate`` template is rendered.
880 880 """
881 881 fctx = webutil.filectx(web.repo, req)
882 882 f = fctx.path()
883 883 parity = paritygen(web.stripecount)
884 884 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
885 885 section='annotate', whitespace=True)
886 886
887 887 def annotate(**map):
888 888 last = None
889 889 if util.binary(fctx.data()):
890 890 mt = (mimetypes.guess_type(fctx.path())[0]
891 891 or 'application/octet-stream')
892 892 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
893 893 '(binary:%s)' % mt)])
894 894 else:
895 895 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
896 896 diffopts=diffopts))
897 897 for lineno, ((f, targetline), l) in lines:
898 898 fnode = f.filenode()
899 899
900 900 if last != fnode:
901 901 last = fnode
902 902
903 903 yield {"parity": parity.next(),
904 904 "node": f.hex(),
905 905 "rev": f.rev(),
906 906 "author": f.user(),
907 907 "desc": f.description(),
908 908 "extra": f.extra(),
909 909 "file": f.path(),
910 910 "targetline": targetline,
911 911 "line": l,
912 912 "lineno": lineno + 1,
913 913 "lineid": "l%d" % (lineno + 1),
914 914 "linenumber": "% 6d" % (lineno + 1),
915 915 "revdate": f.date()}
916 916
917 917 return tmpl("fileannotate",
918 918 file=f,
919 919 annotate=annotate,
920 920 path=webutil.up(f),
921 921 rev=fctx.rev(),
922 922 symrev=webutil.symrevorshortnode(req, fctx),
923 923 node=fctx.hex(),
924 924 author=fctx.user(),
925 925 date=fctx.date(),
926 926 desc=fctx.description(),
927 927 extra=fctx.extra(),
928 928 rename=webutil.renamelink(fctx),
929 929 branch=webutil.nodebranchnodefault(fctx),
930 930 parent=webutil.parents(fctx),
931 931 child=webutil.children(fctx),
932 932 tags=webutil.nodetagsdict(web.repo, fctx.node()),
933 933 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
934 934 permissions=fctx.manifest().flags(f))
935 935
936 936 @webcommand('filelog')
937 937 def filelog(web, req, tmpl):
938 938 """
939 939 /filelog/{revision}/{path}
940 940 --------------------------
941 941
942 942 Show information about the history of a file in the repository.
943 943
944 944 The ``revcount`` query string argument can be defined to control the
945 945 maximum number of entries to show.
946 946
947 947 The ``filelog`` template will be rendered.
948 948 """
949 949
950 950 try:
951 951 fctx = webutil.filectx(web.repo, req)
952 952 f = fctx.path()
953 953 fl = fctx.filelog()
954 954 except error.LookupError:
955 955 f = webutil.cleanpath(web.repo, req.form['file'][0])
956 956 fl = web.repo.file(f)
957 957 numrevs = len(fl)
958 958 if not numrevs: # file doesn't exist at all
959 959 raise
960 960 rev = webutil.changectx(web.repo, req).rev()
961 961 first = fl.linkrev(0)
962 962 if rev < first: # current rev is from before file existed
963 963 raise
964 964 frev = numrevs - 1
965 965 while fl.linkrev(frev) > rev:
966 966 frev -= 1
967 967 fctx = web.repo.filectx(f, fl.linkrev(frev))
968 968
969 969 revcount = web.maxshortchanges
970 970 if 'revcount' in req.form:
971 971 try:
972 972 revcount = int(req.form.get('revcount', [revcount])[0])
973 973 revcount = max(revcount, 1)
974 974 tmpl.defaults['sessionvars']['revcount'] = revcount
975 975 except ValueError:
976 976 pass
977 977
978 978 lessvars = copy.copy(tmpl.defaults['sessionvars'])
979 979 lessvars['revcount'] = max(revcount / 2, 1)
980 980 morevars = copy.copy(tmpl.defaults['sessionvars'])
981 981 morevars['revcount'] = revcount * 2
982 982
983 983 count = fctx.filerev() + 1
984 984 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
985 985 end = min(count, start + revcount) # last rev on this page
986 986 parity = paritygen(web.stripecount, offset=start - end)
987 987
988 988 def entries():
989 989 l = []
990 990
991 991 repo = web.repo
992 992 revs = fctx.filelog().revs(start, end - 1)
993 993 for i in revs:
994 994 iterfctx = fctx.filectx(i)
995 995
996 996 l.append({"parity": parity.next(),
997 997 "filerev": i,
998 998 "file": f,
999 999 "node": iterfctx.hex(),
1000 1000 "author": iterfctx.user(),
1001 1001 "date": iterfctx.date(),
1002 1002 "rename": webutil.renamelink(iterfctx),
1003 1003 "parent": webutil.parents(iterfctx),
1004 1004 "child": webutil.children(iterfctx),
1005 1005 "desc": iterfctx.description(),
1006 1006 "extra": iterfctx.extra(),
1007 1007 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1008 1008 "bookmarks": webutil.nodebookmarksdict(
1009 1009 repo, iterfctx.node()),
1010 1010 "branch": webutil.nodebranchnodefault(iterfctx),
1011 1011 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1012 1012 "branches": webutil.nodebranchdict(repo, iterfctx)})
1013 1013 for e in reversed(l):
1014 1014 yield e
1015 1015
1016 1016 entries = list(entries())
1017 1017 latestentry = entries[:1]
1018 1018
1019 1019 revnav = webutil.filerevnav(web.repo, fctx.path())
1020 1020 nav = revnav.gen(end - 1, revcount, count)
1021 1021 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1022 1022 symrev=webutil.symrevorshortnode(req, fctx),
1023 1023 entries=entries,
1024 1024 latestentry=latestentry,
1025 1025 revcount=revcount, morevars=morevars, lessvars=lessvars)
1026 1026
1027 1027 @webcommand('archive')
1028 1028 def archive(web, req, tmpl):
1029 1029 """
1030 1030 /archive/{revision}.{format}[/{path}]
1031 1031 -------------------------------------
1032 1032
1033 1033 Obtain an archive of repository content.
1034 1034
1035 1035 The content and type of the archive is defined by a URL path parameter.
1036 1036 ``format`` is the file extension of the archive type to be generated. e.g.
1037 1037 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1038 1038 server configuration.
1039 1039
1040 1040 The optional ``path`` URL parameter controls content to include in the
1041 1041 archive. If omitted, every file in the specified revision is present in the
1042 1042 archive. If included, only the specified file or contents of the specified
1043 1043 directory will be included in the archive.
1044 1044
1045 1045 No template is used for this handler. Raw, binary content is generated.
1046 1046 """
1047 1047
1048 1048 type_ = req.form.get('type', [None])[0]
1049 1049 allowed = web.configlist("web", "allow_archive")
1050 1050 key = req.form['node'][0]
1051 1051
1052 1052 if type_ not in web.archives:
1053 1053 msg = 'Unsupported archive type: %s' % type_
1054 1054 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1055 1055
1056 1056 if not ((type_ in allowed or
1057 1057 web.configbool("web", "allow" + type_, False))):
1058 1058 msg = 'Archive type not allowed: %s' % type_
1059 1059 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1060 1060
1061 1061 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1062 1062 cnode = web.repo.lookup(key)
1063 1063 arch_version = key
1064 1064 if cnode == key or key == 'tip':
1065 1065 arch_version = short(cnode)
1066 1066 name = "%s-%s" % (reponame, arch_version)
1067 1067
1068 1068 ctx = webutil.changectx(web.repo, req)
1069 1069 pats = []
1070 1070 matchfn = scmutil.match(ctx, [])
1071 1071 file = req.form.get('file', None)
1072 1072 if file:
1073 1073 pats = ['path:' + file[0]]
1074 1074 matchfn = scmutil.match(ctx, pats, default='path')
1075 1075 if pats:
1076 1076 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1077 1077 if not files:
1078 1078 raise ErrorResponse(HTTP_NOT_FOUND,
1079 1079 'file(s) not found: %s' % file[0])
1080 1080
1081 mimetype, artype, extension, encoding = web.archive_specs[type_]
1081 mimetype, artype, extension, encoding = web.archivespecs[type_]
1082 1082 headers = [
1083 1083 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1084 1084 ]
1085 1085 if encoding:
1086 1086 headers.append(('Content-Encoding', encoding))
1087 1087 req.headers.extend(headers)
1088 1088 req.respond(HTTP_OK, mimetype)
1089 1089
1090 1090 archival.archive(web.repo, req, cnode, artype, prefix=name,
1091 1091 matchfn=matchfn,
1092 1092 subrepos=web.configbool("web", "archivesubrepos"))
1093 1093 return []
1094 1094
1095 1095
1096 1096 @webcommand('static')
1097 1097 def static(web, req, tmpl):
1098 1098 fname = req.form['file'][0]
1099 1099 # a repo owner may set web.static in .hg/hgrc to get any file
1100 1100 # readable by the user running the CGI script
1101 1101 static = web.config("web", "static", None, untrusted=False)
1102 1102 if not static:
1103 1103 tp = web.templatepath or templater.templatepaths()
1104 1104 if isinstance(tp, str):
1105 1105 tp = [tp]
1106 1106 static = [os.path.join(p, 'static') for p in tp]
1107 1107 staticfile(static, fname, req)
1108 1108 return []
1109 1109
1110 1110 @webcommand('graph')
1111 1111 def graph(web, req, tmpl):
1112 1112 """
1113 1113 /graph[/{revision}]
1114 1114 -------------------
1115 1115
1116 1116 Show information about the graphical topology of the repository.
1117 1117
1118 1118 Information rendered by this handler can be used to create visual
1119 1119 representations of repository topology.
1120 1120
1121 1121 The ``revision`` URL parameter controls the starting changeset.
1122 1122
1123 1123 The ``revcount`` query string argument can define the number of changesets
1124 1124 to show information for.
1125 1125
1126 1126 This handler will render the ``graph`` template.
1127 1127 """
1128 1128
1129 1129 if 'node' in req.form:
1130 1130 ctx = webutil.changectx(web.repo, req)
1131 1131 symrev = webutil.symrevorshortnode(req, ctx)
1132 1132 else:
1133 1133 ctx = web.repo['tip']
1134 1134 symrev = 'tip'
1135 1135 rev = ctx.rev()
1136 1136
1137 1137 bg_height = 39
1138 1138 revcount = web.maxshortchanges
1139 1139 if 'revcount' in req.form:
1140 1140 try:
1141 1141 revcount = int(req.form.get('revcount', [revcount])[0])
1142 1142 revcount = max(revcount, 1)
1143 1143 tmpl.defaults['sessionvars']['revcount'] = revcount
1144 1144 except ValueError:
1145 1145 pass
1146 1146
1147 1147 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1148 1148 lessvars['revcount'] = max(revcount / 2, 1)
1149 1149 morevars = copy.copy(tmpl.defaults['sessionvars'])
1150 1150 morevars['revcount'] = revcount * 2
1151 1151
1152 1152 count = len(web.repo)
1153 1153 pos = rev
1154 1154
1155 1155 uprev = min(max(0, count - 1), rev + revcount)
1156 1156 downrev = max(0, rev - revcount)
1157 1157 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1158 1158
1159 1159 tree = []
1160 1160 if pos != -1:
1161 1161 allrevs = web.repo.changelog.revs(pos, 0)
1162 1162 revs = []
1163 1163 for i in allrevs:
1164 1164 revs.append(i)
1165 1165 if len(revs) >= revcount:
1166 1166 break
1167 1167
1168 1168 # We have to feed a baseset to dagwalker as it is expecting smartset
1169 1169 # object. This does not have a big impact on hgweb performance itself
1170 1170 # since hgweb graphing code is not itself lazy yet.
1171 1171 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1172 1172 # As we said one line above... not lazy.
1173 1173 tree = list(graphmod.colored(dag, web.repo))
1174 1174
1175 1175 def getcolumns(tree):
1176 1176 cols = 0
1177 1177 for (id, type, ctx, vtx, edges) in tree:
1178 1178 if type != graphmod.CHANGESET:
1179 1179 continue
1180 1180 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1181 1181 max([edge[1] for edge in edges] or [0]))
1182 1182 return cols
1183 1183
1184 1184 def graphdata(usetuples, **map):
1185 1185 data = []
1186 1186
1187 1187 row = 0
1188 1188 for (id, type, ctx, vtx, edges) in tree:
1189 1189 if type != graphmod.CHANGESET:
1190 1190 continue
1191 1191 node = str(ctx)
1192 1192 age = templatefilters.age(ctx.date())
1193 1193 desc = templatefilters.firstline(ctx.description())
1194 1194 desc = cgi.escape(templatefilters.nonempty(desc))
1195 1195 user = cgi.escape(templatefilters.person(ctx.user()))
1196 1196 branch = cgi.escape(ctx.branch())
1197 1197 try:
1198 1198 branchnode = web.repo.branchtip(branch)
1199 1199 except error.RepoLookupError:
1200 1200 branchnode = None
1201 1201 branch = branch, branchnode == ctx.node()
1202 1202
1203 1203 if usetuples:
1204 1204 data.append((node, vtx, edges, desc, user, age, branch,
1205 1205 [cgi.escape(x) for x in ctx.tags()],
1206 1206 [cgi.escape(x) for x in ctx.bookmarks()]))
1207 1207 else:
1208 1208 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1209 1209 'color': (edge[2] - 1) % 6 + 1,
1210 1210 'width': edge[3], 'bcolor': edge[4]}
1211 1211 for edge in edges]
1212 1212
1213 1213 data.append(
1214 1214 {'node': node,
1215 1215 'col': vtx[0],
1216 1216 'color': (vtx[1] - 1) % 6 + 1,
1217 1217 'edges': edgedata,
1218 1218 'row': row,
1219 1219 'nextrow': row + 1,
1220 1220 'desc': desc,
1221 1221 'user': user,
1222 1222 'age': age,
1223 1223 'bookmarks': webutil.nodebookmarksdict(
1224 1224 web.repo, ctx.node()),
1225 1225 'branches': webutil.nodebranchdict(web.repo, ctx),
1226 1226 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1227 1227 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1228 1228
1229 1229 row += 1
1230 1230
1231 1231 return data
1232 1232
1233 1233 cols = getcolumns(tree)
1234 1234 rows = len(tree)
1235 1235 canvasheight = (rows + 1) * bg_height - 27
1236 1236
1237 1237 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1238 1238 uprev=uprev,
1239 1239 lessvars=lessvars, morevars=morevars, downrev=downrev,
1240 1240 cols=cols, rows=rows,
1241 1241 canvaswidth=(cols + 1) * bg_height,
1242 1242 truecanvasheight=rows * bg_height,
1243 1243 canvasheight=canvasheight, bg_height=bg_height,
1244 1244 jsdata=lambda **x: graphdata(True, **x),
1245 1245 nodes=lambda **x: graphdata(False, **x),
1246 1246 node=ctx.hex(), changenav=changenav)
1247 1247
1248 1248 def _getdoc(e):
1249 1249 doc = e[0].__doc__
1250 1250 if doc:
1251 1251 doc = _(doc).split('\n')[0]
1252 1252 else:
1253 1253 doc = _('(no help text available)')
1254 1254 return doc
1255 1255
1256 1256 @webcommand('help')
1257 1257 def help(web, req, tmpl):
1258 1258 """
1259 1259 /help[/{topic}]
1260 1260 ---------------
1261 1261
1262 1262 Render help documentation.
1263 1263
1264 1264 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1265 1265 is defined, that help topic will be rendered. If not, an index of
1266 1266 available help topics will be rendered.
1267 1267
1268 1268 The ``help`` template will be rendered when requesting help for a topic.
1269 1269 ``helptopics`` will be rendered for the index of help topics.
1270 1270 """
1271 1271 from mercurial import commands # avoid cycle
1272 1272 from mercurial import help as helpmod # avoid cycle
1273 1273
1274 1274 topicname = req.form.get('node', [None])[0]
1275 1275 if not topicname:
1276 1276 def topics(**map):
1277 1277 for entries, summary, _doc in helpmod.helptable:
1278 1278 yield {'topic': entries[0], 'summary': summary}
1279 1279
1280 1280 early, other = [], []
1281 1281 primary = lambda s: s.split('|')[0]
1282 1282 for c, e in commands.table.iteritems():
1283 1283 doc = _getdoc(e)
1284 1284 if 'DEPRECATED' in doc or c.startswith('debug'):
1285 1285 continue
1286 1286 cmd = primary(c)
1287 1287 if cmd.startswith('^'):
1288 1288 early.append((cmd[1:], doc))
1289 1289 else:
1290 1290 other.append((cmd, doc))
1291 1291
1292 1292 early.sort()
1293 1293 other.sort()
1294 1294
1295 1295 def earlycommands(**map):
1296 1296 for c, doc in early:
1297 1297 yield {'topic': c, 'summary': doc}
1298 1298
1299 1299 def othercommands(**map):
1300 1300 for c, doc in other:
1301 1301 yield {'topic': c, 'summary': doc}
1302 1302
1303 1303 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1304 1304 othercommands=othercommands, title='Index')
1305 1305
1306 1306 u = webutil.wsgiui()
1307 1307 u.verbose = True
1308 1308 try:
1309 1309 doc = helpmod.help_(u, topicname)
1310 1310 except error.UnknownCommand:
1311 1311 raise ErrorResponse(HTTP_NOT_FOUND)
1312 1312 return tmpl('help', topic=topicname, doc=doc)
1313 1313
1314 1314 # tell hggettext to extract docstrings from these functions:
1315 1315 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now