##// END OF EJS Templates
hgweb: forward archivelist() of hgweb to webutil...
Yuya Nishihara -
r37532:034a422a default
parent child Browse files
Show More
@@ -1,461 +1,458 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.maxchanges = self.configint('web', 'maxchanges')
93 self.maxchanges = self.configint('web', 'maxchanges')
94 self.stripecount = self.configint('web', 'stripes')
94 self.stripecount = self.configint('web', 'stripes')
95 self.maxshortchanges = self.configint('web', 'maxshortchanges')
95 self.maxshortchanges = self.configint('web', 'maxshortchanges')
96 self.maxfiles = self.configint('web', 'maxfiles')
96 self.maxfiles = self.configint('web', 'maxfiles')
97 self.allowpull = self.configbool('web', 'allow-pull')
97 self.allowpull = self.configbool('web', 'allow-pull')
98
98
99 # we use untrusted=False to prevent a repo owner from using
99 # we use untrusted=False to prevent a repo owner from using
100 # 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
101 # by the user running the CGI script
101 # by the user running the CGI script
102 self.templatepath = self.config('web', 'templates', untrusted=False)
102 self.templatepath = self.config('web', 'templates', untrusted=False)
103
103
104 # This object is more expensive to build than simple config values.
104 # This object is more expensive to build than simple config values.
105 # It is shared across requests. The app will replace the object
105 # It is shared across requests. The app will replace the object
106 # 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
107 # modify the underlying object, it should be constant for the lifetime
107 # modify the underlying object, it should be constant for the lifetime
108 # of the request.
108 # of the request.
109 self.websubtable = app.websubtable
109 self.websubtable = app.websubtable
110
110
111 self.csp, self.nonce = cspvalues(self.repo.ui)
111 self.csp, self.nonce = cspvalues(self.repo.ui)
112
112
113 # Trust the settings from the .hg/hgrc files by default.
113 # Trust the settings from the .hg/hgrc files by default.
114 def config(self, section, name, default=uimod._unset, untrusted=True):
114 def config(self, section, name, default=uimod._unset, untrusted=True):
115 return self.repo.ui.config(section, name, default,
115 return self.repo.ui.config(section, name, default,
116 untrusted=untrusted)
116 untrusted=untrusted)
117
117
118 def configbool(self, section, name, default=uimod._unset, untrusted=True):
118 def configbool(self, section, name, default=uimod._unset, untrusted=True):
119 return self.repo.ui.configbool(section, name, default,
119 return self.repo.ui.configbool(section, name, default,
120 untrusted=untrusted)
120 untrusted=untrusted)
121
121
122 def configint(self, section, name, default=uimod._unset, untrusted=True):
122 def configint(self, section, name, default=uimod._unset, untrusted=True):
123 return self.repo.ui.configint(section, name, default,
123 return self.repo.ui.configint(section, name, default,
124 untrusted=untrusted)
124 untrusted=untrusted)
125
125
126 def configlist(self, section, name, default=uimod._unset, untrusted=True):
126 def configlist(self, section, name, default=uimod._unset, untrusted=True):
127 return self.repo.ui.configlist(section, name, default,
127 return self.repo.ui.configlist(section, name, default,
128 untrusted=untrusted)
128 untrusted=untrusted)
129
129
130 def archivelist(self, nodeid):
130 def archivelist(self, nodeid):
131 allowed = self.configlist('web', 'allow_archive')
131 return webutil.archivelist(self.repo.ui, nodeid)
132 for typ, spec in webutil.archivespecs.iteritems():
133 if typ in allowed or self.configbool('web', 'allow%s' % typ):
134 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
135
132
136 def templater(self, req):
133 def templater(self, req):
137 # determine scheme, port and server name
134 # determine scheme, port and server name
138 # this is needed to create absolute urls
135 # this is needed to create absolute urls
139 logourl = self.config('web', 'logourl')
136 logourl = self.config('web', 'logourl')
140 logoimg = self.config('web', 'logoimg')
137 logoimg = self.config('web', 'logoimg')
141 staticurl = (self.config('web', 'staticurl')
138 staticurl = (self.config('web', 'staticurl')
142 or req.apppath + '/static/')
139 or req.apppath + '/static/')
143 if not staticurl.endswith('/'):
140 if not staticurl.endswith('/'):
144 staticurl += '/'
141 staticurl += '/'
145
142
146 # some functions for the templater
143 # some functions for the templater
147
144
148 def motd(**map):
145 def motd(**map):
149 yield self.config('web', 'motd')
146 yield self.config('web', 'motd')
150
147
151 # figure out which style to use
148 # figure out which style to use
152
149
153 vars = {}
150 vars = {}
154 styles, (style, mapfile) = getstyle(req, self.config,
151 styles, (style, mapfile) = getstyle(req, self.config,
155 self.templatepath)
152 self.templatepath)
156 if style == styles[0]:
153 if style == styles[0]:
157 vars['style'] = style
154 vars['style'] = style
158
155
159 sessionvars = webutil.sessionvars(vars, '?')
156 sessionvars = webutil.sessionvars(vars, '?')
160
157
161 if not self.reponame:
158 if not self.reponame:
162 self.reponame = (self.config('web', 'name', '')
159 self.reponame = (self.config('web', 'name', '')
163 or req.reponame
160 or req.reponame
164 or req.apppath
161 or req.apppath
165 or self.repo.root)
162 or self.repo.root)
166
163
167 filters = {}
164 filters = {}
168 templatefilter = registrar.templatefilter(filters)
165 templatefilter = registrar.templatefilter(filters)
169 @templatefilter('websub', intype=bytes)
166 @templatefilter('websub', intype=bytes)
170 def websubfilter(text):
167 def websubfilter(text):
171 return templatefilters.websub(text, self.websubtable)
168 return templatefilters.websub(text, self.websubtable)
172
169
173 # create the templater
170 # create the templater
174 # TODO: export all keywords: defaults = templatekw.keywords.copy()
171 # TODO: export all keywords: defaults = templatekw.keywords.copy()
175 defaults = {
172 defaults = {
176 'url': req.apppath + '/',
173 'url': req.apppath + '/',
177 'logourl': logourl,
174 'logourl': logourl,
178 'logoimg': logoimg,
175 'logoimg': logoimg,
179 'staticurl': staticurl,
176 'staticurl': staticurl,
180 'urlbase': req.advertisedbaseurl,
177 'urlbase': req.advertisedbaseurl,
181 'repo': self.reponame,
178 'repo': self.reponame,
182 'encoding': encoding.encoding,
179 'encoding': encoding.encoding,
183 'motd': motd,
180 'motd': motd,
184 'sessionvars': sessionvars,
181 'sessionvars': sessionvars,
185 'pathdef': makebreadcrumb(req.apppath),
182 'pathdef': makebreadcrumb(req.apppath),
186 'style': style,
183 'style': style,
187 'nonce': self.nonce,
184 'nonce': self.nonce,
188 }
185 }
189 tres = formatter.templateresources(self.repo.ui, self.repo)
186 tres = formatter.templateresources(self.repo.ui, self.repo)
190 tmpl = templater.templater.frommapfile(mapfile,
187 tmpl = templater.templater.frommapfile(mapfile,
191 filters=filters,
188 filters=filters,
192 defaults=defaults,
189 defaults=defaults,
193 resources=tres)
190 resources=tres)
194 return tmpl
191 return tmpl
195
192
196 def sendtemplate(self, name, **kwargs):
193 def sendtemplate(self, name, **kwargs):
197 """Helper function to send a response generated from a template."""
194 """Helper function to send a response generated from a template."""
198 kwargs = pycompat.byteskwargs(kwargs)
195 kwargs = pycompat.byteskwargs(kwargs)
199 self.res.setbodygen(self.tmpl.generate(name, kwargs))
196 self.res.setbodygen(self.tmpl.generate(name, kwargs))
200 return self.res.sendresponse()
197 return self.res.sendresponse()
201
198
202 class hgweb(object):
199 class hgweb(object):
203 """HTTP server for individual repositories.
200 """HTTP server for individual repositories.
204
201
205 Instances of this class serve HTTP responses for a particular
202 Instances of this class serve HTTP responses for a particular
206 repository.
203 repository.
207
204
208 Instances are typically used as WSGI applications.
205 Instances are typically used as WSGI applications.
209
206
210 Some servers are multi-threaded. On these servers, there may
207 Some servers are multi-threaded. On these servers, there may
211 be multiple active threads inside __call__.
208 be multiple active threads inside __call__.
212 """
209 """
213 def __init__(self, repo, name=None, baseui=None):
210 def __init__(self, repo, name=None, baseui=None):
214 if isinstance(repo, str):
211 if isinstance(repo, str):
215 if baseui:
212 if baseui:
216 u = baseui.copy()
213 u = baseui.copy()
217 else:
214 else:
218 u = uimod.ui.load()
215 u = uimod.ui.load()
219 r = hg.repository(u, repo)
216 r = hg.repository(u, repo)
220 else:
217 else:
221 # we trust caller to give us a private copy
218 # we trust caller to give us a private copy
222 r = repo
219 r = repo
223
220
224 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
221 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
222 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
223 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
224 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 # resolve file patterns relative to repo root
225 # resolve file patterns relative to repo root
229 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
226 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
227 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 # displaying bundling progress bar while serving feel wrong and may
228 # displaying bundling progress bar while serving feel wrong and may
232 # break some wsgi implementation.
229 # break some wsgi implementation.
233 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
230 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
234 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
231 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
235 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
232 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
236 self._lastrepo = self._repos[0]
233 self._lastrepo = self._repos[0]
237 hook.redirect(True)
234 hook.redirect(True)
238 self.reponame = name
235 self.reponame = name
239
236
240 def _webifyrepo(self, repo):
237 def _webifyrepo(self, repo):
241 repo = getwebview(repo)
238 repo = getwebview(repo)
242 self.websubtable = webutil.getwebsubs(repo)
239 self.websubtable = webutil.getwebsubs(repo)
243 return repo
240 return repo
244
241
245 @contextlib.contextmanager
242 @contextlib.contextmanager
246 def _obtainrepo(self):
243 def _obtainrepo(self):
247 """Obtain a repo unique to the caller.
244 """Obtain a repo unique to the caller.
248
245
249 Internally we maintain a stack of cachedlocalrepo instances
246 Internally we maintain a stack of cachedlocalrepo instances
250 to be handed out. If one is available, we pop it and return it,
247 to be handed out. If one is available, we pop it and return it,
251 ensuring it is up to date in the process. If one is not available,
248 ensuring it is up to date in the process. If one is not available,
252 we clone the most recently used repo instance and return it.
249 we clone the most recently used repo instance and return it.
253
250
254 It is currently possible for the stack to grow without bounds
251 It is currently possible for the stack to grow without bounds
255 if the server allows infinite threads. However, servers should
252 if the server allows infinite threads. However, servers should
256 have a thread limit, thus establishing our limit.
253 have a thread limit, thus establishing our limit.
257 """
254 """
258 if self._repos:
255 if self._repos:
259 cached = self._repos.pop()
256 cached = self._repos.pop()
260 r, created = cached.fetch()
257 r, created = cached.fetch()
261 else:
258 else:
262 cached = self._lastrepo.copy()
259 cached = self._lastrepo.copy()
263 r, created = cached.fetch()
260 r, created = cached.fetch()
264 if created:
261 if created:
265 r = self._webifyrepo(r)
262 r = self._webifyrepo(r)
266
263
267 self._lastrepo = cached
264 self._lastrepo = cached
268 self.mtime = cached.mtime
265 self.mtime = cached.mtime
269 try:
266 try:
270 yield r
267 yield r
271 finally:
268 finally:
272 self._repos.append(cached)
269 self._repos.append(cached)
273
270
274 def run(self):
271 def run(self):
275 """Start a server from CGI environment.
272 """Start a server from CGI environment.
276
273
277 Modern servers should be using WSGI and should avoid this
274 Modern servers should be using WSGI and should avoid this
278 method, if possible.
275 method, if possible.
279 """
276 """
280 if not encoding.environ.get('GATEWAY_INTERFACE',
277 if not encoding.environ.get('GATEWAY_INTERFACE',
281 '').startswith("CGI/1."):
278 '').startswith("CGI/1."):
282 raise RuntimeError("This function is only intended to be "
279 raise RuntimeError("This function is only intended to be "
283 "called while running as a CGI script.")
280 "called while running as a CGI script.")
284 wsgicgi.launch(self)
281 wsgicgi.launch(self)
285
282
286 def __call__(self, env, respond):
283 def __call__(self, env, respond):
287 """Run the WSGI application.
284 """Run the WSGI application.
288
285
289 This may be called by multiple threads.
286 This may be called by multiple threads.
290 """
287 """
291 req = requestmod.parserequestfromenv(env)
288 req = requestmod.parserequestfromenv(env)
292 res = requestmod.wsgiresponse(req, respond)
289 res = requestmod.wsgiresponse(req, respond)
293
290
294 return self.run_wsgi(req, res)
291 return self.run_wsgi(req, res)
295
292
296 def run_wsgi(self, req, res):
293 def run_wsgi(self, req, res):
297 """Internal method to run the WSGI application.
294 """Internal method to run the WSGI application.
298
295
299 This is typically only called by Mercurial. External consumers
296 This is typically only called by Mercurial. External consumers
300 should be using instances of this class as the WSGI application.
297 should be using instances of this class as the WSGI application.
301 """
298 """
302 with self._obtainrepo() as repo:
299 with self._obtainrepo() as repo:
303 profile = repo.ui.configbool('profiling', 'enabled')
300 profile = repo.ui.configbool('profiling', 'enabled')
304 with profiling.profile(repo.ui, enabled=profile):
301 with profiling.profile(repo.ui, enabled=profile):
305 for r in self._runwsgi(req, res, repo):
302 for r in self._runwsgi(req, res, repo):
306 yield r
303 yield r
307
304
308 def _runwsgi(self, req, res, repo):
305 def _runwsgi(self, req, res, repo):
309 rctx = requestcontext(self, repo, req, res)
306 rctx = requestcontext(self, repo, req, res)
310
307
311 # This state is global across all threads.
308 # This state is global across all threads.
312 encoding.encoding = rctx.config('web', 'encoding')
309 encoding.encoding = rctx.config('web', 'encoding')
313 rctx.repo.ui.environ = req.rawenv
310 rctx.repo.ui.environ = req.rawenv
314
311
315 if rctx.csp:
312 if rctx.csp:
316 # hgwebdir may have added CSP header. Since we generate our own,
313 # hgwebdir may have added CSP header. Since we generate our own,
317 # replace it.
314 # replace it.
318 res.headers['Content-Security-Policy'] = rctx.csp
315 res.headers['Content-Security-Policy'] = rctx.csp
319
316
320 # /api/* is reserved for various API implementations. Dispatch
317 # /api/* is reserved for various API implementations. Dispatch
321 # accordingly. But URL paths can conflict with subrepos and virtual
318 # accordingly. But URL paths can conflict with subrepos and virtual
322 # repos in hgwebdir. So until we have a workaround for this, only
319 # repos in hgwebdir. So until we have a workaround for this, only
323 # expose the URLs if the feature is enabled.
320 # expose the URLs if the feature is enabled.
324 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
321 apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
325 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
322 if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
326 wireprotoserver.handlewsgiapirequest(rctx, req, res,
323 wireprotoserver.handlewsgiapirequest(rctx, req, res,
327 self.check_perm)
324 self.check_perm)
328 return res.sendresponse()
325 return res.sendresponse()
329
326
330 handled = wireprotoserver.handlewsgirequest(
327 handled = wireprotoserver.handlewsgirequest(
331 rctx, req, res, self.check_perm)
328 rctx, req, res, self.check_perm)
332 if handled:
329 if handled:
333 return res.sendresponse()
330 return res.sendresponse()
334
331
335 # Old implementations of hgweb supported dispatching the request via
332 # Old implementations of hgweb supported dispatching the request via
336 # the initial query string parameter instead of using PATH_INFO.
333 # the initial query string parameter instead of using PATH_INFO.
337 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
334 # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
338 # a value), we use it. Otherwise fall back to the query string.
335 # a value), we use it. Otherwise fall back to the query string.
339 if req.dispatchpath is not None:
336 if req.dispatchpath is not None:
340 query = req.dispatchpath
337 query = req.dispatchpath
341 else:
338 else:
342 query = req.querystring.partition('&')[0].partition(';')[0]
339 query = req.querystring.partition('&')[0].partition(';')[0]
343
340
344 # translate user-visible url structure to internal structure
341 # translate user-visible url structure to internal structure
345
342
346 args = query.split('/', 2)
343 args = query.split('/', 2)
347 if 'cmd' not in req.qsparams and args and args[0]:
344 if 'cmd' not in req.qsparams and args and args[0]:
348 cmd = args.pop(0)
345 cmd = args.pop(0)
349 style = cmd.rfind('-')
346 style = cmd.rfind('-')
350 if style != -1:
347 if style != -1:
351 req.qsparams['style'] = cmd[:style]
348 req.qsparams['style'] = cmd[:style]
352 cmd = cmd[style + 1:]
349 cmd = cmd[style + 1:]
353
350
354 # avoid accepting e.g. style parameter as command
351 # avoid accepting e.g. style parameter as command
355 if util.safehasattr(webcommands, cmd):
352 if util.safehasattr(webcommands, cmd):
356 req.qsparams['cmd'] = cmd
353 req.qsparams['cmd'] = cmd
357
354
358 if cmd == 'static':
355 if cmd == 'static':
359 req.qsparams['file'] = '/'.join(args)
356 req.qsparams['file'] = '/'.join(args)
360 else:
357 else:
361 if args and args[0]:
358 if args and args[0]:
362 node = args.pop(0).replace('%2F', '/')
359 node = args.pop(0).replace('%2F', '/')
363 req.qsparams['node'] = node
360 req.qsparams['node'] = node
364 if args:
361 if args:
365 if 'file' in req.qsparams:
362 if 'file' in req.qsparams:
366 del req.qsparams['file']
363 del req.qsparams['file']
367 for a in args:
364 for a in args:
368 req.qsparams.add('file', a)
365 req.qsparams.add('file', a)
369
366
370 ua = req.headers.get('User-Agent', '')
367 ua = req.headers.get('User-Agent', '')
371 if cmd == 'rev' and 'mercurial' in ua:
368 if cmd == 'rev' and 'mercurial' in ua:
372 req.qsparams['style'] = 'raw'
369 req.qsparams['style'] = 'raw'
373
370
374 if cmd == 'archive':
371 if cmd == 'archive':
375 fn = req.qsparams['node']
372 fn = req.qsparams['node']
376 for type_, spec in webutil.archivespecs.iteritems():
373 for type_, spec in webutil.archivespecs.iteritems():
377 ext = spec[2]
374 ext = spec[2]
378 if fn.endswith(ext):
375 if fn.endswith(ext):
379 req.qsparams['node'] = fn[:-len(ext)]
376 req.qsparams['node'] = fn[:-len(ext)]
380 req.qsparams['type'] = type_
377 req.qsparams['type'] = type_
381 else:
378 else:
382 cmd = req.qsparams.get('cmd', '')
379 cmd = req.qsparams.get('cmd', '')
383
380
384 # process the web interface request
381 # process the web interface request
385
382
386 try:
383 try:
387 rctx.tmpl = rctx.templater(req)
384 rctx.tmpl = rctx.templater(req)
388 ctype = rctx.tmpl.render('mimetype',
385 ctype = rctx.tmpl.render('mimetype',
389 {'encoding': encoding.encoding})
386 {'encoding': encoding.encoding})
390
387
391 # check read permissions non-static content
388 # check read permissions non-static content
392 if cmd != 'static':
389 if cmd != 'static':
393 self.check_perm(rctx, req, None)
390 self.check_perm(rctx, req, None)
394
391
395 if cmd == '':
392 if cmd == '':
396 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
393 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
397 cmd = req.qsparams['cmd']
394 cmd = req.qsparams['cmd']
398
395
399 # Don't enable caching if using a CSP nonce because then it wouldn't
396 # Don't enable caching if using a CSP nonce because then it wouldn't
400 # be a nonce.
397 # be a nonce.
401 if rctx.configbool('web', 'cache') and not rctx.nonce:
398 if rctx.configbool('web', 'cache') and not rctx.nonce:
402 tag = 'W/"%d"' % self.mtime
399 tag = 'W/"%d"' % self.mtime
403 if req.headers.get('If-None-Match') == tag:
400 if req.headers.get('If-None-Match') == tag:
404 res.status = '304 Not Modified'
401 res.status = '304 Not Modified'
405 # Response body not allowed on 304.
402 # Response body not allowed on 304.
406 res.setbodybytes('')
403 res.setbodybytes('')
407 return res.sendresponse()
404 return res.sendresponse()
408
405
409 res.headers['ETag'] = tag
406 res.headers['ETag'] = tag
410
407
411 if cmd not in webcommands.__all__:
408 if cmd not in webcommands.__all__:
412 msg = 'no such method: %s' % cmd
409 msg = 'no such method: %s' % cmd
413 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
410 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
414 else:
411 else:
415 # Set some globals appropriate for web handlers. Commands can
412 # Set some globals appropriate for web handlers. Commands can
416 # override easily enough.
413 # override easily enough.
417 res.status = '200 Script output follows'
414 res.status = '200 Script output follows'
418 res.headers['Content-Type'] = ctype
415 res.headers['Content-Type'] = ctype
419 return getattr(webcommands, cmd)(rctx)
416 return getattr(webcommands, cmd)(rctx)
420
417
421 except (error.LookupError, error.RepoLookupError) as err:
418 except (error.LookupError, error.RepoLookupError) as err:
422 msg = pycompat.bytestr(err)
419 msg = pycompat.bytestr(err)
423 if (util.safehasattr(err, 'name') and
420 if (util.safehasattr(err, 'name') and
424 not isinstance(err, error.ManifestLookupError)):
421 not isinstance(err, error.ManifestLookupError)):
425 msg = 'revision not found: %s' % err.name
422 msg = 'revision not found: %s' % err.name
426
423
427 res.status = '404 Not Found'
424 res.status = '404 Not Found'
428 res.headers['Content-Type'] = ctype
425 res.headers['Content-Type'] = ctype
429 return rctx.sendtemplate('error', error=msg)
426 return rctx.sendtemplate('error', error=msg)
430 except (error.RepoError, error.RevlogError) as e:
427 except (error.RepoError, error.RevlogError) as e:
431 res.status = '500 Internal Server Error'
428 res.status = '500 Internal Server Error'
432 res.headers['Content-Type'] = ctype
429 res.headers['Content-Type'] = ctype
433 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
430 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
434 except ErrorResponse as e:
431 except ErrorResponse as e:
435 res.status = statusmessage(e.code, pycompat.bytestr(e))
432 res.status = statusmessage(e.code, pycompat.bytestr(e))
436 res.headers['Content-Type'] = ctype
433 res.headers['Content-Type'] = ctype
437 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
434 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
438
435
439 def check_perm(self, rctx, req, op):
436 def check_perm(self, rctx, req, op):
440 for permhook in permhooks:
437 for permhook in permhooks:
441 permhook(rctx, req, op)
438 permhook(rctx, req, op)
442
439
443 def getwebview(repo):
440 def getwebview(repo):
444 """The 'web.view' config controls changeset filter to hgweb. Possible
441 """The 'web.view' config controls changeset filter to hgweb. Possible
445 values are ``served``, ``visible`` and ``all``. Default is ``served``.
442 values are ``served``, ``visible`` and ``all``. Default is ``served``.
446 The ``served`` filter only shows changesets that can be pulled from the
443 The ``served`` filter only shows changesets that can be pulled from the
447 hgweb instance. The``visible`` filter includes secret changesets but
444 hgweb instance. The``visible`` filter includes secret changesets but
448 still excludes "hidden" one.
445 still excludes "hidden" one.
449
446
450 See the repoview module for details.
447 See the repoview module for details.
451
448
452 The option has been around undocumented since Mercurial 2.5, but no
449 The option has been around undocumented since Mercurial 2.5, but no
453 user ever asked about it. So we better keep it undocumented for now."""
450 user ever asked about it. So we better keep it undocumented for now."""
454 # experimental config: web.view
451 # experimental config: web.view
455 viewconfig = repo.ui.config('web', 'view', untrusted=True)
452 viewconfig = repo.ui.config('web', 'view', untrusted=True)
456 if viewconfig == 'all':
453 if viewconfig == 'all':
457 return repo.unfiltered()
454 return repo.unfiltered()
458 elif viewconfig in repoview.filtertable:
455 elif viewconfig in repoview.filtertable:
459 return repo.filtered(viewconfig)
456 return repo.filtered(viewconfig)
460 else:
457 else:
461 return repo.filtered('served')
458 return repo.filtered('served')
@@ -1,716 +1,716 b''
1 # hgweb/webutil.py - utility library for the web interface.
1 # hgweb/webutil.py - utility library for the web interface.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import copy
11 import copy
12 import difflib
12 import difflib
13 import os
13 import os
14 import re
14 import re
15
15
16 from ..i18n import _
16 from ..i18n import _
17 from ..node import hex, nullid, short
17 from ..node import hex, nullid, short
18
18
19 from .common import (
19 from .common import (
20 ErrorResponse,
20 ErrorResponse,
21 HTTP_BAD_REQUEST,
21 HTTP_BAD_REQUEST,
22 HTTP_NOT_FOUND,
22 HTTP_NOT_FOUND,
23 paritygen,
23 paritygen,
24 )
24 )
25
25
26 from .. import (
26 from .. import (
27 context,
27 context,
28 error,
28 error,
29 match,
29 match,
30 mdiff,
30 mdiff,
31 obsutil,
31 obsutil,
32 patch,
32 patch,
33 pathutil,
33 pathutil,
34 pycompat,
34 pycompat,
35 scmutil,
35 scmutil,
36 templatefilters,
36 templatefilters,
37 templatekw,
37 templatekw,
38 ui as uimod,
38 ui as uimod,
39 util,
39 util,
40 )
40 )
41
41
42 from ..utils import (
42 from ..utils import (
43 stringutil,
43 stringutil,
44 )
44 )
45
45
46 archivespecs = util.sortdict((
46 archivespecs = util.sortdict((
47 ('zip', ('application/zip', 'zip', '.zip', None)),
47 ('zip', ('application/zip', 'zip', '.zip', None)),
48 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
48 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
49 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
49 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
50 ))
50 ))
51
51
52 def archivelist(ui, nodeid, url):
52 def archivelist(ui, nodeid, url=None):
53 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
53 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
54 archives = []
54 archives = []
55
55
56 for typ, spec in archivespecs.iteritems():
56 for typ, spec in archivespecs.iteritems():
57 if typ in allowed or ui.configbool('web', 'allow' + typ,
57 if typ in allowed or ui.configbool('web', 'allow' + typ,
58 untrusted=True):
58 untrusted=True):
59 archives.append({
59 archives.append({
60 'type': typ,
60 'type': typ,
61 'extension': spec[2],
61 'extension': spec[2],
62 'node': nodeid,
62 'node': nodeid,
63 'url': url,
63 'url': url,
64 })
64 })
65
65
66 return archives
66 return archives
67
67
68 def up(p):
68 def up(p):
69 if p[0:1] != "/":
69 if p[0:1] != "/":
70 p = "/" + p
70 p = "/" + p
71 if p[-1:] == "/":
71 if p[-1:] == "/":
72 p = p[:-1]
72 p = p[:-1]
73 up = os.path.dirname(p)
73 up = os.path.dirname(p)
74 if up == "/":
74 if up == "/":
75 return "/"
75 return "/"
76 return up + "/"
76 return up + "/"
77
77
78 def _navseq(step, firststep=None):
78 def _navseq(step, firststep=None):
79 if firststep:
79 if firststep:
80 yield firststep
80 yield firststep
81 if firststep >= 20 and firststep <= 40:
81 if firststep >= 20 and firststep <= 40:
82 firststep = 50
82 firststep = 50
83 yield firststep
83 yield firststep
84 assert step > 0
84 assert step > 0
85 assert firststep > 0
85 assert firststep > 0
86 while step <= firststep:
86 while step <= firststep:
87 step *= 10
87 step *= 10
88 while True:
88 while True:
89 yield 1 * step
89 yield 1 * step
90 yield 3 * step
90 yield 3 * step
91 step *= 10
91 step *= 10
92
92
93 class revnav(object):
93 class revnav(object):
94
94
95 def __init__(self, repo):
95 def __init__(self, repo):
96 """Navigation generation object
96 """Navigation generation object
97
97
98 :repo: repo object we generate nav for
98 :repo: repo object we generate nav for
99 """
99 """
100 # used for hex generation
100 # used for hex generation
101 self._revlog = repo.changelog
101 self._revlog = repo.changelog
102
102
103 def __nonzero__(self):
103 def __nonzero__(self):
104 """return True if any revision to navigate over"""
104 """return True if any revision to navigate over"""
105 return self._first() is not None
105 return self._first() is not None
106
106
107 __bool__ = __nonzero__
107 __bool__ = __nonzero__
108
108
109 def _first(self):
109 def _first(self):
110 """return the minimum non-filtered changeset or None"""
110 """return the minimum non-filtered changeset or None"""
111 try:
111 try:
112 return next(iter(self._revlog))
112 return next(iter(self._revlog))
113 except StopIteration:
113 except StopIteration:
114 return None
114 return None
115
115
116 def hex(self, rev):
116 def hex(self, rev):
117 return hex(self._revlog.node(rev))
117 return hex(self._revlog.node(rev))
118
118
119 def gen(self, pos, pagelen, limit):
119 def gen(self, pos, pagelen, limit):
120 """computes label and revision id for navigation link
120 """computes label and revision id for navigation link
121
121
122 :pos: is the revision relative to which we generate navigation.
122 :pos: is the revision relative to which we generate navigation.
123 :pagelen: the size of each navigation page
123 :pagelen: the size of each navigation page
124 :limit: how far shall we link
124 :limit: how far shall we link
125
125
126 The return is:
126 The return is:
127 - a single element tuple
127 - a single element tuple
128 - containing a dictionary with a `before` and `after` key
128 - containing a dictionary with a `before` and `after` key
129 - values are generator functions taking arbitrary number of kwargs
129 - values are generator functions taking arbitrary number of kwargs
130 - yield items are dictionaries with `label` and `node` keys
130 - yield items are dictionaries with `label` and `node` keys
131 """
131 """
132 if not self:
132 if not self:
133 # empty repo
133 # empty repo
134 return ({'before': (), 'after': ()},)
134 return ({'before': (), 'after': ()},)
135
135
136 targets = []
136 targets = []
137 for f in _navseq(1, pagelen):
137 for f in _navseq(1, pagelen):
138 if f > limit:
138 if f > limit:
139 break
139 break
140 targets.append(pos + f)
140 targets.append(pos + f)
141 targets.append(pos - f)
141 targets.append(pos - f)
142 targets.sort()
142 targets.sort()
143
143
144 first = self._first()
144 first = self._first()
145 navbefore = [("(%i)" % first, self.hex(first))]
145 navbefore = [("(%i)" % first, self.hex(first))]
146 navafter = []
146 navafter = []
147 for rev in targets:
147 for rev in targets:
148 if rev not in self._revlog:
148 if rev not in self._revlog:
149 continue
149 continue
150 if pos < rev < limit:
150 if pos < rev < limit:
151 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
151 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
152 if 0 < rev < pos:
152 if 0 < rev < pos:
153 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
153 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
154
154
155
155
156 navafter.append(("tip", "tip"))
156 navafter.append(("tip", "tip"))
157
157
158 data = lambda i: {"label": i[0], "node": i[1]}
158 data = lambda i: {"label": i[0], "node": i[1]}
159 return ({'before': lambda **map: (data(i) for i in navbefore),
159 return ({'before': lambda **map: (data(i) for i in navbefore),
160 'after': lambda **map: (data(i) for i in navafter)},)
160 'after': lambda **map: (data(i) for i in navafter)},)
161
161
162 class filerevnav(revnav):
162 class filerevnav(revnav):
163
163
164 def __init__(self, repo, path):
164 def __init__(self, repo, path):
165 """Navigation generation object
165 """Navigation generation object
166
166
167 :repo: repo object we generate nav for
167 :repo: repo object we generate nav for
168 :path: path of the file we generate nav for
168 :path: path of the file we generate nav for
169 """
169 """
170 # used for iteration
170 # used for iteration
171 self._changelog = repo.unfiltered().changelog
171 self._changelog = repo.unfiltered().changelog
172 # used for hex generation
172 # used for hex generation
173 self._revlog = repo.file(path)
173 self._revlog = repo.file(path)
174
174
175 def hex(self, rev):
175 def hex(self, rev):
176 return hex(self._changelog.node(self._revlog.linkrev(rev)))
176 return hex(self._changelog.node(self._revlog.linkrev(rev)))
177
177
178 class _siblings(object):
178 class _siblings(object):
179 def __init__(self, siblings=None, hiderev=None):
179 def __init__(self, siblings=None, hiderev=None):
180 if siblings is None:
180 if siblings is None:
181 siblings = []
181 siblings = []
182 self.siblings = [s for s in siblings if s.node() != nullid]
182 self.siblings = [s for s in siblings if s.node() != nullid]
183 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
183 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
184 self.siblings = []
184 self.siblings = []
185
185
186 def __iter__(self):
186 def __iter__(self):
187 for s in self.siblings:
187 for s in self.siblings:
188 d = {
188 d = {
189 'node': s.hex(),
189 'node': s.hex(),
190 'rev': s.rev(),
190 'rev': s.rev(),
191 'user': s.user(),
191 'user': s.user(),
192 'date': s.date(),
192 'date': s.date(),
193 'description': s.description(),
193 'description': s.description(),
194 'branch': s.branch(),
194 'branch': s.branch(),
195 }
195 }
196 if util.safehasattr(s, 'path'):
196 if util.safehasattr(s, 'path'):
197 d['file'] = s.path()
197 d['file'] = s.path()
198 yield d
198 yield d
199
199
200 def __len__(self):
200 def __len__(self):
201 return len(self.siblings)
201 return len(self.siblings)
202
202
203 def difffeatureopts(req, ui, section):
203 def difffeatureopts(req, ui, section):
204 diffopts = patch.difffeatureopts(ui, untrusted=True,
204 diffopts = patch.difffeatureopts(ui, untrusted=True,
205 section=section, whitespace=True)
205 section=section, whitespace=True)
206
206
207 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
207 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
208 v = req.qsparams.get(k)
208 v = req.qsparams.get(k)
209 if v is not None:
209 if v is not None:
210 v = stringutil.parsebool(v)
210 v = stringutil.parsebool(v)
211 setattr(diffopts, k, v if v is not None else True)
211 setattr(diffopts, k, v if v is not None else True)
212
212
213 return diffopts
213 return diffopts
214
214
215 def annotate(req, fctx, ui):
215 def annotate(req, fctx, ui):
216 diffopts = difffeatureopts(req, ui, 'annotate')
216 diffopts = difffeatureopts(req, ui, 'annotate')
217 return fctx.annotate(follow=True, diffopts=diffopts)
217 return fctx.annotate(follow=True, diffopts=diffopts)
218
218
219 def parents(ctx, hide=None):
219 def parents(ctx, hide=None):
220 if isinstance(ctx, context.basefilectx):
220 if isinstance(ctx, context.basefilectx):
221 introrev = ctx.introrev()
221 introrev = ctx.introrev()
222 if ctx.changectx().rev() != introrev:
222 if ctx.changectx().rev() != introrev:
223 return _siblings([ctx.repo()[introrev]], hide)
223 return _siblings([ctx.repo()[introrev]], hide)
224 return _siblings(ctx.parents(), hide)
224 return _siblings(ctx.parents(), hide)
225
225
226 def children(ctx, hide=None):
226 def children(ctx, hide=None):
227 return _siblings(ctx.children(), hide)
227 return _siblings(ctx.children(), hide)
228
228
229 def renamelink(fctx):
229 def renamelink(fctx):
230 r = fctx.renamed()
230 r = fctx.renamed()
231 if r:
231 if r:
232 return [{'file': r[0], 'node': hex(r[1])}]
232 return [{'file': r[0], 'node': hex(r[1])}]
233 return []
233 return []
234
234
235 def nodetagsdict(repo, node):
235 def nodetagsdict(repo, node):
236 return [{"name": i} for i in repo.nodetags(node)]
236 return [{"name": i} for i in repo.nodetags(node)]
237
237
238 def nodebookmarksdict(repo, node):
238 def nodebookmarksdict(repo, node):
239 return [{"name": i} for i in repo.nodebookmarks(node)]
239 return [{"name": i} for i in repo.nodebookmarks(node)]
240
240
241 def nodebranchdict(repo, ctx):
241 def nodebranchdict(repo, ctx):
242 branches = []
242 branches = []
243 branch = ctx.branch()
243 branch = ctx.branch()
244 # If this is an empty repo, ctx.node() == nullid,
244 # If this is an empty repo, ctx.node() == nullid,
245 # ctx.branch() == 'default'.
245 # ctx.branch() == 'default'.
246 try:
246 try:
247 branchnode = repo.branchtip(branch)
247 branchnode = repo.branchtip(branch)
248 except error.RepoLookupError:
248 except error.RepoLookupError:
249 branchnode = None
249 branchnode = None
250 if branchnode == ctx.node():
250 if branchnode == ctx.node():
251 branches.append({"name": branch})
251 branches.append({"name": branch})
252 return branches
252 return branches
253
253
254 def nodeinbranch(repo, ctx):
254 def nodeinbranch(repo, ctx):
255 branches = []
255 branches = []
256 branch = ctx.branch()
256 branch = ctx.branch()
257 try:
257 try:
258 branchnode = repo.branchtip(branch)
258 branchnode = repo.branchtip(branch)
259 except error.RepoLookupError:
259 except error.RepoLookupError:
260 branchnode = None
260 branchnode = None
261 if branch != 'default' and branchnode != ctx.node():
261 if branch != 'default' and branchnode != ctx.node():
262 branches.append({"name": branch})
262 branches.append({"name": branch})
263 return branches
263 return branches
264
264
265 def nodebranchnodefault(ctx):
265 def nodebranchnodefault(ctx):
266 branches = []
266 branches = []
267 branch = ctx.branch()
267 branch = ctx.branch()
268 if branch != 'default':
268 if branch != 'default':
269 branches.append({"name": branch})
269 branches.append({"name": branch})
270 return branches
270 return branches
271
271
272 def showtag(repo, tmpl, t1, node=nullid, **args):
272 def showtag(repo, tmpl, t1, node=nullid, **args):
273 args = pycompat.byteskwargs(args)
273 args = pycompat.byteskwargs(args)
274 for t in repo.nodetags(node):
274 for t in repo.nodetags(node):
275 lm = args.copy()
275 lm = args.copy()
276 lm['tag'] = t
276 lm['tag'] = t
277 yield tmpl.generate(t1, lm)
277 yield tmpl.generate(t1, lm)
278
278
279 def showbookmark(repo, tmpl, t1, node=nullid, **args):
279 def showbookmark(repo, tmpl, t1, node=nullid, **args):
280 args = pycompat.byteskwargs(args)
280 args = pycompat.byteskwargs(args)
281 for t in repo.nodebookmarks(node):
281 for t in repo.nodebookmarks(node):
282 lm = args.copy()
282 lm = args.copy()
283 lm['bookmark'] = t
283 lm['bookmark'] = t
284 yield tmpl.generate(t1, lm)
284 yield tmpl.generate(t1, lm)
285
285
286 def branchentries(repo, stripecount, limit=0):
286 def branchentries(repo, stripecount, limit=0):
287 tips = []
287 tips = []
288 heads = repo.heads()
288 heads = repo.heads()
289 parity = paritygen(stripecount)
289 parity = paritygen(stripecount)
290 sortkey = lambda item: (not item[1], item[0].rev())
290 sortkey = lambda item: (not item[1], item[0].rev())
291
291
292 def entries(**map):
292 def entries(**map):
293 count = 0
293 count = 0
294 if not tips:
294 if not tips:
295 for tag, hs, tip, closed in repo.branchmap().iterbranches():
295 for tag, hs, tip, closed in repo.branchmap().iterbranches():
296 tips.append((repo[tip], closed))
296 tips.append((repo[tip], closed))
297 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
297 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
298 if limit > 0 and count >= limit:
298 if limit > 0 and count >= limit:
299 return
299 return
300 count += 1
300 count += 1
301 if closed:
301 if closed:
302 status = 'closed'
302 status = 'closed'
303 elif ctx.node() not in heads:
303 elif ctx.node() not in heads:
304 status = 'inactive'
304 status = 'inactive'
305 else:
305 else:
306 status = 'open'
306 status = 'open'
307 yield {
307 yield {
308 'parity': next(parity),
308 'parity': next(parity),
309 'branch': ctx.branch(),
309 'branch': ctx.branch(),
310 'status': status,
310 'status': status,
311 'node': ctx.hex(),
311 'node': ctx.hex(),
312 'date': ctx.date()
312 'date': ctx.date()
313 }
313 }
314
314
315 return entries
315 return entries
316
316
317 def cleanpath(repo, path):
317 def cleanpath(repo, path):
318 path = path.lstrip('/')
318 path = path.lstrip('/')
319 return pathutil.canonpath(repo.root, '', path)
319 return pathutil.canonpath(repo.root, '', path)
320
320
321 def changectx(repo, req):
321 def changectx(repo, req):
322 changeid = "tip"
322 changeid = "tip"
323 if 'node' in req.qsparams:
323 if 'node' in req.qsparams:
324 changeid = req.qsparams['node']
324 changeid = req.qsparams['node']
325 ipos = changeid.find(':')
325 ipos = changeid.find(':')
326 if ipos != -1:
326 if ipos != -1:
327 changeid = changeid[(ipos + 1):]
327 changeid = changeid[(ipos + 1):]
328
328
329 return scmutil.revsymbol(repo, changeid)
329 return scmutil.revsymbol(repo, changeid)
330
330
331 def basechangectx(repo, req):
331 def basechangectx(repo, req):
332 if 'node' in req.qsparams:
332 if 'node' in req.qsparams:
333 changeid = req.qsparams['node']
333 changeid = req.qsparams['node']
334 ipos = changeid.find(':')
334 ipos = changeid.find(':')
335 if ipos != -1:
335 if ipos != -1:
336 changeid = changeid[:ipos]
336 changeid = changeid[:ipos]
337 return scmutil.revsymbol(repo, changeid)
337 return scmutil.revsymbol(repo, changeid)
338
338
339 return None
339 return None
340
340
341 def filectx(repo, req):
341 def filectx(repo, req):
342 if 'file' not in req.qsparams:
342 if 'file' not in req.qsparams:
343 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
343 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
344 path = cleanpath(repo, req.qsparams['file'])
344 path = cleanpath(repo, req.qsparams['file'])
345 if 'node' in req.qsparams:
345 if 'node' in req.qsparams:
346 changeid = req.qsparams['node']
346 changeid = req.qsparams['node']
347 elif 'filenode' in req.qsparams:
347 elif 'filenode' in req.qsparams:
348 changeid = req.qsparams['filenode']
348 changeid = req.qsparams['filenode']
349 else:
349 else:
350 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
350 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
351 try:
351 try:
352 fctx = scmutil.revsymbol(repo, changeid)[path]
352 fctx = scmutil.revsymbol(repo, changeid)[path]
353 except error.RepoError:
353 except error.RepoError:
354 fctx = repo.filectx(path, fileid=changeid)
354 fctx = repo.filectx(path, fileid=changeid)
355
355
356 return fctx
356 return fctx
357
357
358 def linerange(req):
358 def linerange(req):
359 linerange = req.qsparams.getall('linerange')
359 linerange = req.qsparams.getall('linerange')
360 if not linerange:
360 if not linerange:
361 return None
361 return None
362 if len(linerange) > 1:
362 if len(linerange) > 1:
363 raise ErrorResponse(HTTP_BAD_REQUEST,
363 raise ErrorResponse(HTTP_BAD_REQUEST,
364 'redundant linerange parameter')
364 'redundant linerange parameter')
365 try:
365 try:
366 fromline, toline = map(int, linerange[0].split(':', 1))
366 fromline, toline = map(int, linerange[0].split(':', 1))
367 except ValueError:
367 except ValueError:
368 raise ErrorResponse(HTTP_BAD_REQUEST,
368 raise ErrorResponse(HTTP_BAD_REQUEST,
369 'invalid linerange parameter')
369 'invalid linerange parameter')
370 try:
370 try:
371 return util.processlinerange(fromline, toline)
371 return util.processlinerange(fromline, toline)
372 except error.ParseError as exc:
372 except error.ParseError as exc:
373 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
373 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
374
374
375 def formatlinerange(fromline, toline):
375 def formatlinerange(fromline, toline):
376 return '%d:%d' % (fromline + 1, toline)
376 return '%d:%d' % (fromline + 1, toline)
377
377
378 def succsandmarkers(context, mapping):
378 def succsandmarkers(context, mapping):
379 repo = context.resource(mapping, 'repo')
379 repo = context.resource(mapping, 'repo')
380 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
380 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
381 for item in itemmappings.tovalue(context, mapping):
381 for item in itemmappings.tovalue(context, mapping):
382 item['successors'] = _siblings(repo[successor]
382 item['successors'] = _siblings(repo[successor]
383 for successor in item['successors'])
383 for successor in item['successors'])
384 yield item
384 yield item
385
385
386 # teach templater succsandmarkers is switched to (context, mapping) API
386 # teach templater succsandmarkers is switched to (context, mapping) API
387 succsandmarkers._requires = {'repo', 'ctx'}
387 succsandmarkers._requires = {'repo', 'ctx'}
388
388
389 def whyunstable(context, mapping):
389 def whyunstable(context, mapping):
390 repo = context.resource(mapping, 'repo')
390 repo = context.resource(mapping, 'repo')
391 ctx = context.resource(mapping, 'ctx')
391 ctx = context.resource(mapping, 'ctx')
392
392
393 entries = obsutil.whyunstable(repo, ctx)
393 entries = obsutil.whyunstable(repo, ctx)
394 for entry in entries:
394 for entry in entries:
395 if entry.get('divergentnodes'):
395 if entry.get('divergentnodes'):
396 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
396 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
397 yield entry
397 yield entry
398
398
399 whyunstable._requires = {'repo', 'ctx'}
399 whyunstable._requires = {'repo', 'ctx'}
400
400
401 def commonentry(repo, ctx):
401 def commonentry(repo, ctx):
402 node = ctx.node()
402 node = ctx.node()
403 return {
403 return {
404 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
404 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
405 # filectx, but I'm not pretty sure if that would always work because
405 # filectx, but I'm not pretty sure if that would always work because
406 # fctx.parents() != fctx.changectx.parents() for example.
406 # fctx.parents() != fctx.changectx.parents() for example.
407 'ctx': ctx,
407 'ctx': ctx,
408 'rev': ctx.rev(),
408 'rev': ctx.rev(),
409 'node': hex(node),
409 'node': hex(node),
410 'author': ctx.user(),
410 'author': ctx.user(),
411 'desc': ctx.description(),
411 'desc': ctx.description(),
412 'date': ctx.date(),
412 'date': ctx.date(),
413 'extra': ctx.extra(),
413 'extra': ctx.extra(),
414 'phase': ctx.phasestr(),
414 'phase': ctx.phasestr(),
415 'obsolete': ctx.obsolete(),
415 'obsolete': ctx.obsolete(),
416 'succsandmarkers': succsandmarkers,
416 'succsandmarkers': succsandmarkers,
417 'instabilities': [{"instability": i} for i in ctx.instabilities()],
417 'instabilities': [{"instability": i} for i in ctx.instabilities()],
418 'whyunstable': whyunstable,
418 'whyunstable': whyunstable,
419 'branch': nodebranchnodefault(ctx),
419 'branch': nodebranchnodefault(ctx),
420 'inbranch': nodeinbranch(repo, ctx),
420 'inbranch': nodeinbranch(repo, ctx),
421 'branches': nodebranchdict(repo, ctx),
421 'branches': nodebranchdict(repo, ctx),
422 'tags': nodetagsdict(repo, node),
422 'tags': nodetagsdict(repo, node),
423 'bookmarks': nodebookmarksdict(repo, node),
423 'bookmarks': nodebookmarksdict(repo, node),
424 'parent': lambda **x: parents(ctx),
424 'parent': lambda **x: parents(ctx),
425 'child': lambda **x: children(ctx),
425 'child': lambda **x: children(ctx),
426 }
426 }
427
427
428 def changelistentry(web, ctx):
428 def changelistentry(web, ctx):
429 '''Obtain a dictionary to be used for entries in a changelist.
429 '''Obtain a dictionary to be used for entries in a changelist.
430
430
431 This function is called when producing items for the "entries" list passed
431 This function is called when producing items for the "entries" list passed
432 to the "shortlog" and "changelog" templates.
432 to the "shortlog" and "changelog" templates.
433 '''
433 '''
434 repo = web.repo
434 repo = web.repo
435 rev = ctx.rev()
435 rev = ctx.rev()
436 n = ctx.node()
436 n = ctx.node()
437 showtags = showtag(repo, web.tmpl, 'changelogtag', n)
437 showtags = showtag(repo, web.tmpl, 'changelogtag', n)
438 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
438 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
439
439
440 entry = commonentry(repo, ctx)
440 entry = commonentry(repo, ctx)
441 entry.update(
441 entry.update(
442 allparents=lambda **x: parents(ctx),
442 allparents=lambda **x: parents(ctx),
443 parent=lambda **x: parents(ctx, rev - 1),
443 parent=lambda **x: parents(ctx, rev - 1),
444 child=lambda **x: children(ctx, rev + 1),
444 child=lambda **x: children(ctx, rev + 1),
445 changelogtag=showtags,
445 changelogtag=showtags,
446 files=files,
446 files=files,
447 )
447 )
448 return entry
448 return entry
449
449
450 def symrevorshortnode(req, ctx):
450 def symrevorshortnode(req, ctx):
451 if 'node' in req.qsparams:
451 if 'node' in req.qsparams:
452 return templatefilters.revescape(req.qsparams['node'])
452 return templatefilters.revescape(req.qsparams['node'])
453 else:
453 else:
454 return short(ctx.node())
454 return short(ctx.node())
455
455
456 def changesetentry(web, ctx):
456 def changesetentry(web, ctx):
457 '''Obtain a dictionary to be used to render the "changeset" template.'''
457 '''Obtain a dictionary to be used to render the "changeset" template.'''
458
458
459 showtags = showtag(web.repo, web.tmpl, 'changesettag', ctx.node())
459 showtags = showtag(web.repo, web.tmpl, 'changesettag', ctx.node())
460 showbookmarks = showbookmark(web.repo, web.tmpl, 'changesetbookmark',
460 showbookmarks = showbookmark(web.repo, web.tmpl, 'changesetbookmark',
461 ctx.node())
461 ctx.node())
462 showbranch = nodebranchnodefault(ctx)
462 showbranch = nodebranchnodefault(ctx)
463
463
464 files = []
464 files = []
465 parity = paritygen(web.stripecount)
465 parity = paritygen(web.stripecount)
466 for blockno, f in enumerate(ctx.files()):
466 for blockno, f in enumerate(ctx.files()):
467 template = 'filenodelink' if f in ctx else 'filenolink'
467 template = 'filenodelink' if f in ctx else 'filenolink'
468 files.append(web.tmpl.generate(template, {
468 files.append(web.tmpl.generate(template, {
469 'node': ctx.hex(),
469 'node': ctx.hex(),
470 'file': f,
470 'file': f,
471 'blockno': blockno + 1,
471 'blockno': blockno + 1,
472 'parity': next(parity),
472 'parity': next(parity),
473 }))
473 }))
474
474
475 basectx = basechangectx(web.repo, web.req)
475 basectx = basechangectx(web.repo, web.req)
476 if basectx is None:
476 if basectx is None:
477 basectx = ctx.p1()
477 basectx = ctx.p1()
478
478
479 style = web.config('web', 'style')
479 style = web.config('web', 'style')
480 if 'style' in web.req.qsparams:
480 if 'style' in web.req.qsparams:
481 style = web.req.qsparams['style']
481 style = web.req.qsparams['style']
482
482
483 diff = diffs(web, ctx, basectx, None, style)
483 diff = diffs(web, ctx, basectx, None, style)
484
484
485 parity = paritygen(web.stripecount)
485 parity = paritygen(web.stripecount)
486 diffstatsgen = diffstatgen(ctx, basectx)
486 diffstatsgen = diffstatgen(ctx, basectx)
487 diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
487 diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
488
488
489 return dict(
489 return dict(
490 diff=diff,
490 diff=diff,
491 symrev=symrevorshortnode(web.req, ctx),
491 symrev=symrevorshortnode(web.req, ctx),
492 basenode=basectx.hex(),
492 basenode=basectx.hex(),
493 changesettag=showtags,
493 changesettag=showtags,
494 changesetbookmark=showbookmarks,
494 changesetbookmark=showbookmarks,
495 changesetbranch=showbranch,
495 changesetbranch=showbranch,
496 files=files,
496 files=files,
497 diffsummary=lambda **x: diffsummary(diffstatsgen),
497 diffsummary=lambda **x: diffsummary(diffstatsgen),
498 diffstat=diffstats,
498 diffstat=diffstats,
499 archives=web.archivelist(ctx.hex()),
499 archives=web.archivelist(ctx.hex()),
500 **pycompat.strkwargs(commonentry(web.repo, ctx)))
500 **pycompat.strkwargs(commonentry(web.repo, ctx)))
501
501
502 def listfilediffs(tmpl, files, node, max):
502 def listfilediffs(tmpl, files, node, max):
503 for f in files[:max]:
503 for f in files[:max]:
504 yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
504 yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
505 if len(files) > max:
505 if len(files) > max:
506 yield tmpl.generate('fileellipses', {})
506 yield tmpl.generate('fileellipses', {})
507
507
508 def diffs(web, ctx, basectx, files, style, linerange=None,
508 def diffs(web, ctx, basectx, files, style, linerange=None,
509 lineidprefix=''):
509 lineidprefix=''):
510
510
511 def prettyprintlines(lines, blockno):
511 def prettyprintlines(lines, blockno):
512 for lineno, l in enumerate(lines, 1):
512 for lineno, l in enumerate(lines, 1):
513 difflineno = "%d.%d" % (blockno, lineno)
513 difflineno = "%d.%d" % (blockno, lineno)
514 if l.startswith('+'):
514 if l.startswith('+'):
515 ltype = "difflineplus"
515 ltype = "difflineplus"
516 elif l.startswith('-'):
516 elif l.startswith('-'):
517 ltype = "difflineminus"
517 ltype = "difflineminus"
518 elif l.startswith('@'):
518 elif l.startswith('@'):
519 ltype = "difflineat"
519 ltype = "difflineat"
520 else:
520 else:
521 ltype = "diffline"
521 ltype = "diffline"
522 yield web.tmpl.generate(ltype, {
522 yield web.tmpl.generate(ltype, {
523 'line': l,
523 'line': l,
524 'lineno': lineno,
524 'lineno': lineno,
525 'lineid': lineidprefix + "l%s" % difflineno,
525 'lineid': lineidprefix + "l%s" % difflineno,
526 'linenumber': "% 8s" % difflineno,
526 'linenumber': "% 8s" % difflineno,
527 })
527 })
528
528
529 repo = web.repo
529 repo = web.repo
530 if files:
530 if files:
531 m = match.exact(repo.root, repo.getcwd(), files)
531 m = match.exact(repo.root, repo.getcwd(), files)
532 else:
532 else:
533 m = match.always(repo.root, repo.getcwd())
533 m = match.always(repo.root, repo.getcwd())
534
534
535 diffopts = patch.diffopts(repo.ui, untrusted=True)
535 diffopts = patch.diffopts(repo.ui, untrusted=True)
536 node1 = basectx.node()
536 node1 = basectx.node()
537 node2 = ctx.node()
537 node2 = ctx.node()
538 parity = paritygen(web.stripecount)
538 parity = paritygen(web.stripecount)
539
539
540 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
540 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
541 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
541 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
542 if style != 'raw':
542 if style != 'raw':
543 header = header[1:]
543 header = header[1:]
544 lines = [h + '\n' for h in header]
544 lines = [h + '\n' for h in header]
545 for hunkrange, hunklines in hunks:
545 for hunkrange, hunklines in hunks:
546 if linerange is not None and hunkrange is not None:
546 if linerange is not None and hunkrange is not None:
547 s1, l1, s2, l2 = hunkrange
547 s1, l1, s2, l2 = hunkrange
548 if not mdiff.hunkinrange((s2, l2), linerange):
548 if not mdiff.hunkinrange((s2, l2), linerange):
549 continue
549 continue
550 lines.extend(hunklines)
550 lines.extend(hunklines)
551 if lines:
551 if lines:
552 yield web.tmpl.generate('diffblock', {
552 yield web.tmpl.generate('diffblock', {
553 'parity': next(parity),
553 'parity': next(parity),
554 'blockno': blockno,
554 'blockno': blockno,
555 'lines': prettyprintlines(lines, blockno),
555 'lines': prettyprintlines(lines, blockno),
556 })
556 })
557
557
558 def compare(tmpl, context, leftlines, rightlines):
558 def compare(tmpl, context, leftlines, rightlines):
559 '''Generator function that provides side-by-side comparison data.'''
559 '''Generator function that provides side-by-side comparison data.'''
560
560
561 def compline(type, leftlineno, leftline, rightlineno, rightline):
561 def compline(type, leftlineno, leftline, rightlineno, rightline):
562 lineid = leftlineno and ("l%d" % leftlineno) or ''
562 lineid = leftlineno and ("l%d" % leftlineno) or ''
563 lineid += rightlineno and ("r%d" % rightlineno) or ''
563 lineid += rightlineno and ("r%d" % rightlineno) or ''
564 llno = '%d' % leftlineno if leftlineno else ''
564 llno = '%d' % leftlineno if leftlineno else ''
565 rlno = '%d' % rightlineno if rightlineno else ''
565 rlno = '%d' % rightlineno if rightlineno else ''
566 return tmpl.generate('comparisonline', {
566 return tmpl.generate('comparisonline', {
567 'type': type,
567 'type': type,
568 'lineid': lineid,
568 'lineid': lineid,
569 'leftlineno': leftlineno,
569 'leftlineno': leftlineno,
570 'leftlinenumber': "% 6s" % llno,
570 'leftlinenumber': "% 6s" % llno,
571 'leftline': leftline or '',
571 'leftline': leftline or '',
572 'rightlineno': rightlineno,
572 'rightlineno': rightlineno,
573 'rightlinenumber': "% 6s" % rlno,
573 'rightlinenumber': "% 6s" % rlno,
574 'rightline': rightline or '',
574 'rightline': rightline or '',
575 })
575 })
576
576
577 def getblock(opcodes):
577 def getblock(opcodes):
578 for type, llo, lhi, rlo, rhi in opcodes:
578 for type, llo, lhi, rlo, rhi in opcodes:
579 len1 = lhi - llo
579 len1 = lhi - llo
580 len2 = rhi - rlo
580 len2 = rhi - rlo
581 count = min(len1, len2)
581 count = min(len1, len2)
582 for i in xrange(count):
582 for i in xrange(count):
583 yield compline(type=type,
583 yield compline(type=type,
584 leftlineno=llo + i + 1,
584 leftlineno=llo + i + 1,
585 leftline=leftlines[llo + i],
585 leftline=leftlines[llo + i],
586 rightlineno=rlo + i + 1,
586 rightlineno=rlo + i + 1,
587 rightline=rightlines[rlo + i])
587 rightline=rightlines[rlo + i])
588 if len1 > len2:
588 if len1 > len2:
589 for i in xrange(llo + count, lhi):
589 for i in xrange(llo + count, lhi):
590 yield compline(type=type,
590 yield compline(type=type,
591 leftlineno=i + 1,
591 leftlineno=i + 1,
592 leftline=leftlines[i],
592 leftline=leftlines[i],
593 rightlineno=None,
593 rightlineno=None,
594 rightline=None)
594 rightline=None)
595 elif len2 > len1:
595 elif len2 > len1:
596 for i in xrange(rlo + count, rhi):
596 for i in xrange(rlo + count, rhi):
597 yield compline(type=type,
597 yield compline(type=type,
598 leftlineno=None,
598 leftlineno=None,
599 leftline=None,
599 leftline=None,
600 rightlineno=i + 1,
600 rightlineno=i + 1,
601 rightline=rightlines[i])
601 rightline=rightlines[i])
602
602
603 s = difflib.SequenceMatcher(None, leftlines, rightlines)
603 s = difflib.SequenceMatcher(None, leftlines, rightlines)
604 if context < 0:
604 if context < 0:
605 yield tmpl.generate('comparisonblock',
605 yield tmpl.generate('comparisonblock',
606 {'lines': getblock(s.get_opcodes())})
606 {'lines': getblock(s.get_opcodes())})
607 else:
607 else:
608 for oc in s.get_grouped_opcodes(n=context):
608 for oc in s.get_grouped_opcodes(n=context):
609 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
609 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
610
610
611 def diffstatgen(ctx, basectx):
611 def diffstatgen(ctx, basectx):
612 '''Generator function that provides the diffstat data.'''
612 '''Generator function that provides the diffstat data.'''
613
613
614 stats = patch.diffstatdata(
614 stats = patch.diffstatdata(
615 util.iterlines(ctx.diff(basectx, noprefix=False)))
615 util.iterlines(ctx.diff(basectx, noprefix=False)))
616 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
616 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
617 while True:
617 while True:
618 yield stats, maxname, maxtotal, addtotal, removetotal, binary
618 yield stats, maxname, maxtotal, addtotal, removetotal, binary
619
619
620 def diffsummary(statgen):
620 def diffsummary(statgen):
621 '''Return a short summary of the diff.'''
621 '''Return a short summary of the diff.'''
622
622
623 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
623 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
624 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
624 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
625 len(stats), addtotal, removetotal)
625 len(stats), addtotal, removetotal)
626
626
627 def diffstat(tmpl, ctx, statgen, parity):
627 def diffstat(tmpl, ctx, statgen, parity):
628 '''Return a diffstat template for each file in the diff.'''
628 '''Return a diffstat template for each file in the diff.'''
629
629
630 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
630 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
631 files = ctx.files()
631 files = ctx.files()
632
632
633 def pct(i):
633 def pct(i):
634 if maxtotal == 0:
634 if maxtotal == 0:
635 return 0
635 return 0
636 return (float(i) / maxtotal) * 100
636 return (float(i) / maxtotal) * 100
637
637
638 fileno = 0
638 fileno = 0
639 for filename, adds, removes, isbinary in stats:
639 for filename, adds, removes, isbinary in stats:
640 template = 'diffstatlink' if filename in files else 'diffstatnolink'
640 template = 'diffstatlink' if filename in files else 'diffstatnolink'
641 total = adds + removes
641 total = adds + removes
642 fileno += 1
642 fileno += 1
643 yield tmpl.generate(template, {
643 yield tmpl.generate(template, {
644 'node': ctx.hex(),
644 'node': ctx.hex(),
645 'file': filename,
645 'file': filename,
646 'fileno': fileno,
646 'fileno': fileno,
647 'total': total,
647 'total': total,
648 'addpct': pct(adds),
648 'addpct': pct(adds),
649 'removepct': pct(removes),
649 'removepct': pct(removes),
650 'parity': next(parity),
650 'parity': next(parity),
651 })
651 })
652
652
653 class sessionvars(object):
653 class sessionvars(object):
654 def __init__(self, vars, start='?'):
654 def __init__(self, vars, start='?'):
655 self.start = start
655 self.start = start
656 self.vars = vars
656 self.vars = vars
657 def __getitem__(self, key):
657 def __getitem__(self, key):
658 return self.vars[key]
658 return self.vars[key]
659 def __setitem__(self, key, value):
659 def __setitem__(self, key, value):
660 self.vars[key] = value
660 self.vars[key] = value
661 def __copy__(self):
661 def __copy__(self):
662 return sessionvars(copy.copy(self.vars), self.start)
662 return sessionvars(copy.copy(self.vars), self.start)
663 def __iter__(self):
663 def __iter__(self):
664 separator = self.start
664 separator = self.start
665 for key, value in sorted(self.vars.iteritems()):
665 for key, value in sorted(self.vars.iteritems()):
666 yield {'name': key,
666 yield {'name': key,
667 'value': pycompat.bytestr(value),
667 'value': pycompat.bytestr(value),
668 'separator': separator,
668 'separator': separator,
669 }
669 }
670 separator = '&'
670 separator = '&'
671
671
672 class wsgiui(uimod.ui):
672 class wsgiui(uimod.ui):
673 # default termwidth breaks under mod_wsgi
673 # default termwidth breaks under mod_wsgi
674 def termwidth(self):
674 def termwidth(self):
675 return 80
675 return 80
676
676
677 def getwebsubs(repo):
677 def getwebsubs(repo):
678 websubtable = []
678 websubtable = []
679 websubdefs = repo.ui.configitems('websub')
679 websubdefs = repo.ui.configitems('websub')
680 # we must maintain interhg backwards compatibility
680 # we must maintain interhg backwards compatibility
681 websubdefs += repo.ui.configitems('interhg')
681 websubdefs += repo.ui.configitems('interhg')
682 for key, pattern in websubdefs:
682 for key, pattern in websubdefs:
683 # grab the delimiter from the character after the "s"
683 # grab the delimiter from the character after the "s"
684 unesc = pattern[1:2]
684 unesc = pattern[1:2]
685 delim = re.escape(unesc)
685 delim = re.escape(unesc)
686
686
687 # identify portions of the pattern, taking care to avoid escaped
687 # identify portions of the pattern, taking care to avoid escaped
688 # delimiters. the replace format and flags are optional, but
688 # delimiters. the replace format and flags are optional, but
689 # delimiters are required.
689 # delimiters are required.
690 match = re.match(
690 match = re.match(
691 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
691 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
692 % (delim, delim, delim), pattern)
692 % (delim, delim, delim), pattern)
693 if not match:
693 if not match:
694 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
694 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
695 % (key, pattern))
695 % (key, pattern))
696 continue
696 continue
697
697
698 # we need to unescape the delimiter for regexp and format
698 # we need to unescape the delimiter for regexp and format
699 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
699 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
700 regexp = delim_re.sub(unesc, match.group(1))
700 regexp = delim_re.sub(unesc, match.group(1))
701 format = delim_re.sub(unesc, match.group(2))
701 format = delim_re.sub(unesc, match.group(2))
702
702
703 # the pattern allows for 6 regexp flags, so set them if necessary
703 # the pattern allows for 6 regexp flags, so set them if necessary
704 flagin = match.group(3)
704 flagin = match.group(3)
705 flags = 0
705 flags = 0
706 if flagin:
706 if flagin:
707 for flag in flagin.upper():
707 for flag in flagin.upper():
708 flags |= re.__dict__[flag]
708 flags |= re.__dict__[flag]
709
709
710 try:
710 try:
711 regexp = re.compile(regexp, flags)
711 regexp = re.compile(regexp, flags)
712 websubtable.append((regexp, format))
712 websubtable.append((regexp, format))
713 except re.error:
713 except re.error:
714 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
714 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
715 % (key, regexp))
715 % (key, regexp))
716 return websubtable
716 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now