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