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