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