##// END OF EJS Templates
hgweb: do not import templatefilters.revescape and websub as symbol...
Yuya Nishihara -
r27008:7f19f331 default
parent child Browse files
Show More
@@ -1,442 +1,442 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 import contextlib
9 import contextlib
10 import os
10 import os
11 from mercurial import hg, hook, error, encoding, templater, util, repoview
11 from mercurial import hg, hook, error, encoding, templater, util, repoview
12 from mercurial import ui as uimod
12 from mercurial import ui as uimod
13 from mercurial.templatefilters import websub
13 from mercurial import templatefilters
14 from common import ErrorResponse, permhooks, caching
14 from common import ErrorResponse, permhooks, caching
15 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
15 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
16 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
17 from request import wsgirequest
17 from request import wsgirequest
18 import webcommands, protocol, webutil
18 import webcommands, protocol, webutil
19
19
20 perms = {
20 perms = {
21 'changegroup': 'pull',
21 'changegroup': 'pull',
22 'changegroupsubset': 'pull',
22 'changegroupsubset': 'pull',
23 'getbundle': 'pull',
23 'getbundle': 'pull',
24 'stream_out': 'pull',
24 'stream_out': 'pull',
25 'listkeys': 'pull',
25 'listkeys': 'pull',
26 'unbundle': 'push',
26 'unbundle': 'push',
27 'pushkey': 'push',
27 'pushkey': 'push',
28 }
28 }
29
29
30 def makebreadcrumb(url, prefix=''):
30 def makebreadcrumb(url, prefix=''):
31 '''Return a 'URL breadcrumb' list
31 '''Return a 'URL breadcrumb' list
32
32
33 A 'URL breadcrumb' is a list of URL-name pairs,
33 A 'URL breadcrumb' is a list of URL-name pairs,
34 corresponding to each of the path items on a URL.
34 corresponding to each of the path items on a URL.
35 This can be used to create path navigation entries.
35 This can be used to create path navigation entries.
36 '''
36 '''
37 if url.endswith('/'):
37 if url.endswith('/'):
38 url = url[:-1]
38 url = url[:-1]
39 if prefix:
39 if prefix:
40 url = '/' + prefix + url
40 url = '/' + prefix + url
41 relpath = url
41 relpath = url
42 if relpath.startswith('/'):
42 if relpath.startswith('/'):
43 relpath = relpath[1:]
43 relpath = relpath[1:]
44
44
45 breadcrumb = []
45 breadcrumb = []
46 urlel = url
46 urlel = url
47 pathitems = [''] + relpath.split('/')
47 pathitems = [''] + relpath.split('/')
48 for pathel in reversed(pathitems):
48 for pathel in reversed(pathitems):
49 if not pathel or not urlel:
49 if not pathel or not urlel:
50 break
50 break
51 breadcrumb.append({'url': urlel, 'name': pathel})
51 breadcrumb.append({'url': urlel, 'name': pathel})
52 urlel = os.path.dirname(urlel)
52 urlel = os.path.dirname(urlel)
53 return reversed(breadcrumb)
53 return reversed(breadcrumb)
54
54
55 class requestcontext(object):
55 class requestcontext(object):
56 """Holds state/context for an individual request.
56 """Holds state/context for an individual request.
57
57
58 Servers can be multi-threaded. Holding state on the WSGI application
58 Servers can be multi-threaded. Holding state on the WSGI application
59 is prone to race conditions. Instances of this class exist to hold
59 is prone to race conditions. Instances of this class exist to hold
60 mutable and race-free state for requests.
60 mutable and race-free state for requests.
61 """
61 """
62 def __init__(self, app, repo):
62 def __init__(self, app, repo):
63 self.repo = repo
63 self.repo = repo
64 self.reponame = app.reponame
64 self.reponame = app.reponame
65
65
66 self.archives = ('zip', 'gz', 'bz2')
66 self.archives = ('zip', 'gz', 'bz2')
67
67
68 self.maxchanges = self.configint('web', 'maxchanges', 10)
68 self.maxchanges = self.configint('web', 'maxchanges', 10)
69 self.stripecount = self.configint('web', 'stripes', 1)
69 self.stripecount = self.configint('web', 'stripes', 1)
70 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
70 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
71 self.maxfiles = self.configint('web', 'maxfiles', 10)
71 self.maxfiles = self.configint('web', 'maxfiles', 10)
72 self.allowpull = self.configbool('web', 'allowpull', True)
72 self.allowpull = self.configbool('web', 'allowpull', True)
73
73
74 # we use untrusted=False to prevent a repo owner from using
74 # we use untrusted=False to prevent a repo owner from using
75 # web.templates in .hg/hgrc to get access to any file readable
75 # web.templates in .hg/hgrc to get access to any file readable
76 # by the user running the CGI script
76 # by the user running the CGI script
77 self.templatepath = self.config('web', 'templates', untrusted=False)
77 self.templatepath = self.config('web', 'templates', untrusted=False)
78
78
79 # This object is more expensive to build than simple config values.
79 # This object is more expensive to build than simple config values.
80 # It is shared across requests. The app will replace the object
80 # It is shared across requests. The app will replace the object
81 # if it is updated. Since this is a reference and nothing should
81 # if it is updated. Since this is a reference and nothing should
82 # modify the underlying object, it should be constant for the lifetime
82 # modify the underlying object, it should be constant for the lifetime
83 # of the request.
83 # of the request.
84 self.websubtable = app.websubtable
84 self.websubtable = app.websubtable
85
85
86 # Trust the settings from the .hg/hgrc files by default.
86 # Trust the settings from the .hg/hgrc files by default.
87 def config(self, section, name, default=None, untrusted=True):
87 def config(self, section, name, default=None, untrusted=True):
88 return self.repo.ui.config(section, name, default,
88 return self.repo.ui.config(section, name, default,
89 untrusted=untrusted)
89 untrusted=untrusted)
90
90
91 def configbool(self, section, name, default=False, untrusted=True):
91 def configbool(self, section, name, default=False, untrusted=True):
92 return self.repo.ui.configbool(section, name, default,
92 return self.repo.ui.configbool(section, name, default,
93 untrusted=untrusted)
93 untrusted=untrusted)
94
94
95 def configint(self, section, name, default=None, untrusted=True):
95 def configint(self, section, name, default=None, untrusted=True):
96 return self.repo.ui.configint(section, name, default,
96 return self.repo.ui.configint(section, name, default,
97 untrusted=untrusted)
97 untrusted=untrusted)
98
98
99 def configlist(self, section, name, default=None, untrusted=True):
99 def configlist(self, section, name, default=None, untrusted=True):
100 return self.repo.ui.configlist(section, name, default,
100 return self.repo.ui.configlist(section, name, default,
101 untrusted=untrusted)
101 untrusted=untrusted)
102
102
103 archivespecs = {
103 archivespecs = {
104 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
104 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
105 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
105 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
106 'zip': ('application/zip', 'zip', '.zip', None),
106 'zip': ('application/zip', 'zip', '.zip', None),
107 }
107 }
108
108
109 def archivelist(self, nodeid):
109 def archivelist(self, nodeid):
110 allowed = self.configlist('web', 'allow_archive')
110 allowed = self.configlist('web', 'allow_archive')
111 for typ, spec in self.archivespecs.iteritems():
111 for typ, spec in self.archivespecs.iteritems():
112 if typ in allowed or self.configbool('web', 'allow%s' % typ):
112 if typ in allowed or self.configbool('web', 'allow%s' % typ):
113 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
113 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
114
114
115 def templater(self, req):
115 def templater(self, req):
116 # determine scheme, port and server name
116 # determine scheme, port and server name
117 # this is needed to create absolute urls
117 # this is needed to create absolute urls
118
118
119 proto = req.env.get('wsgi.url_scheme')
119 proto = req.env.get('wsgi.url_scheme')
120 if proto == 'https':
120 if proto == 'https':
121 proto = 'https'
121 proto = 'https'
122 default_port = '443'
122 default_port = '443'
123 else:
123 else:
124 proto = 'http'
124 proto = 'http'
125 default_port = '80'
125 default_port = '80'
126
126
127 port = req.env['SERVER_PORT']
127 port = req.env['SERVER_PORT']
128 port = port != default_port and (':' + port) or ''
128 port = port != default_port and (':' + port) or ''
129 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
129 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
130 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
130 logourl = self.config('web', 'logourl', 'https://mercurial-scm.org/')
131 logoimg = self.config('web', 'logoimg', 'hglogo.png')
131 logoimg = self.config('web', 'logoimg', 'hglogo.png')
132 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
132 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
133 if not staticurl.endswith('/'):
133 if not staticurl.endswith('/'):
134 staticurl += '/'
134 staticurl += '/'
135
135
136 # some functions for the templater
136 # some functions for the templater
137
137
138 def motd(**map):
138 def motd(**map):
139 yield self.config('web', 'motd', '')
139 yield self.config('web', 'motd', '')
140
140
141 # figure out which style to use
141 # figure out which style to use
142
142
143 vars = {}
143 vars = {}
144 styles = (
144 styles = (
145 req.form.get('style', [None])[0],
145 req.form.get('style', [None])[0],
146 self.config('web', 'style'),
146 self.config('web', 'style'),
147 'paper',
147 'paper',
148 )
148 )
149 style, mapfile = templater.stylemap(styles, self.templatepath)
149 style, mapfile = templater.stylemap(styles, self.templatepath)
150 if style == styles[0]:
150 if style == styles[0]:
151 vars['style'] = style
151 vars['style'] = style
152
152
153 start = req.url[-1] == '?' and '&' or '?'
153 start = req.url[-1] == '?' and '&' or '?'
154 sessionvars = webutil.sessionvars(vars, start)
154 sessionvars = webutil.sessionvars(vars, start)
155
155
156 if not self.reponame:
156 if not self.reponame:
157 self.reponame = (self.config('web', 'name')
157 self.reponame = (self.config('web', 'name')
158 or req.env.get('REPO_NAME')
158 or req.env.get('REPO_NAME')
159 or req.url.strip('/') or self.repo.root)
159 or req.url.strip('/') or self.repo.root)
160
160
161 def websubfilter(text):
161 def websubfilter(text):
162 return websub(text, self.websubtable)
162 return templatefilters.websub(text, self.websubtable)
163
163
164 # create the templater
164 # create the templater
165
165
166 tmpl = templater.templater(mapfile,
166 tmpl = templater.templater(mapfile,
167 filters={'websub': websubfilter},
167 filters={'websub': websubfilter},
168 defaults={'url': req.url,
168 defaults={'url': req.url,
169 'logourl': logourl,
169 'logourl': logourl,
170 'logoimg': logoimg,
170 'logoimg': logoimg,
171 'staticurl': staticurl,
171 'staticurl': staticurl,
172 'urlbase': urlbase,
172 'urlbase': urlbase,
173 'repo': self.reponame,
173 'repo': self.reponame,
174 'encoding': encoding.encoding,
174 'encoding': encoding.encoding,
175 'motd': motd,
175 'motd': motd,
176 'sessionvars': sessionvars,
176 'sessionvars': sessionvars,
177 'pathdef': makebreadcrumb(req.url),
177 'pathdef': makebreadcrumb(req.url),
178 'style': style,
178 'style': style,
179 })
179 })
180 return tmpl
180 return tmpl
181
181
182
182
183 class hgweb(object):
183 class hgweb(object):
184 """HTTP server for individual repositories.
184 """HTTP server for individual repositories.
185
185
186 Instances of this class serve HTTP responses for a particular
186 Instances of this class serve HTTP responses for a particular
187 repository.
187 repository.
188
188
189 Instances are typically used as WSGI applications.
189 Instances are typically used as WSGI applications.
190
190
191 Some servers are multi-threaded. On these servers, there may
191 Some servers are multi-threaded. On these servers, there may
192 be multiple active threads inside __call__.
192 be multiple active threads inside __call__.
193 """
193 """
194 def __init__(self, repo, name=None, baseui=None):
194 def __init__(self, repo, name=None, baseui=None):
195 if isinstance(repo, str):
195 if isinstance(repo, str):
196 if baseui:
196 if baseui:
197 u = baseui.copy()
197 u = baseui.copy()
198 else:
198 else:
199 u = uimod.ui()
199 u = uimod.ui()
200 r = hg.repository(u, repo)
200 r = hg.repository(u, repo)
201 else:
201 else:
202 # we trust caller to give us a private copy
202 # we trust caller to give us a private copy
203 r = repo
203 r = repo
204
204
205 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
205 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
206 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
206 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
207 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
207 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
208 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
208 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
209 # resolve file patterns relative to repo root
209 # resolve file patterns relative to repo root
210 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
210 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
211 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
211 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
212 # displaying bundling progress bar while serving feel wrong and may
212 # displaying bundling progress bar while serving feel wrong and may
213 # break some wsgi implementation.
213 # break some wsgi implementation.
214 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
214 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
215 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
215 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
216 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
216 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
217 self._lastrepo = self._repos[0]
217 self._lastrepo = self._repos[0]
218 hook.redirect(True)
218 hook.redirect(True)
219 self.reponame = name
219 self.reponame = name
220
220
221 def _webifyrepo(self, repo):
221 def _webifyrepo(self, repo):
222 repo = getwebview(repo)
222 repo = getwebview(repo)
223 self.websubtable = webutil.getwebsubs(repo)
223 self.websubtable = webutil.getwebsubs(repo)
224 return repo
224 return repo
225
225
226 @contextlib.contextmanager
226 @contextlib.contextmanager
227 def _obtainrepo(self):
227 def _obtainrepo(self):
228 """Obtain a repo unique to the caller.
228 """Obtain a repo unique to the caller.
229
229
230 Internally we maintain a stack of cachedlocalrepo instances
230 Internally we maintain a stack of cachedlocalrepo instances
231 to be handed out. If one is available, we pop it and return it,
231 to be handed out. If one is available, we pop it and return it,
232 ensuring it is up to date in the process. If one is not available,
232 ensuring it is up to date in the process. If one is not available,
233 we clone the most recently used repo instance and return it.
233 we clone the most recently used repo instance and return it.
234
234
235 It is currently possible for the stack to grow without bounds
235 It is currently possible for the stack to grow without bounds
236 if the server allows infinite threads. However, servers should
236 if the server allows infinite threads. However, servers should
237 have a thread limit, thus establishing our limit.
237 have a thread limit, thus establishing our limit.
238 """
238 """
239 if self._repos:
239 if self._repos:
240 cached = self._repos.pop()
240 cached = self._repos.pop()
241 r, created = cached.fetch()
241 r, created = cached.fetch()
242 else:
242 else:
243 cached = self._lastrepo.copy()
243 cached = self._lastrepo.copy()
244 r, created = cached.fetch()
244 r, created = cached.fetch()
245 if created:
245 if created:
246 r = self._webifyrepo(r)
246 r = self._webifyrepo(r)
247
247
248 self._lastrepo = cached
248 self._lastrepo = cached
249 self.mtime = cached.mtime
249 self.mtime = cached.mtime
250 try:
250 try:
251 yield r
251 yield r
252 finally:
252 finally:
253 self._repos.append(cached)
253 self._repos.append(cached)
254
254
255 def run(self):
255 def run(self):
256 """Start a server from CGI environment.
256 """Start a server from CGI environment.
257
257
258 Modern servers should be using WSGI and should avoid this
258 Modern servers should be using WSGI and should avoid this
259 method, if possible.
259 method, if possible.
260 """
260 """
261 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
261 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
262 raise RuntimeError("This function is only intended to be "
262 raise RuntimeError("This function is only intended to be "
263 "called while running as a CGI script.")
263 "called while running as a CGI script.")
264 import mercurial.hgweb.wsgicgi as wsgicgi
264 import mercurial.hgweb.wsgicgi as wsgicgi
265 wsgicgi.launch(self)
265 wsgicgi.launch(self)
266
266
267 def __call__(self, env, respond):
267 def __call__(self, env, respond):
268 """Run the WSGI application.
268 """Run the WSGI application.
269
269
270 This may be called by multiple threads.
270 This may be called by multiple threads.
271 """
271 """
272 req = wsgirequest(env, respond)
272 req = wsgirequest(env, respond)
273 return self.run_wsgi(req)
273 return self.run_wsgi(req)
274
274
275 def run_wsgi(self, req):
275 def run_wsgi(self, req):
276 """Internal method to run the WSGI application.
276 """Internal method to run the WSGI application.
277
277
278 This is typically only called by Mercurial. External consumers
278 This is typically only called by Mercurial. External consumers
279 should be using instances of this class as the WSGI application.
279 should be using instances of this class as the WSGI application.
280 """
280 """
281 with self._obtainrepo() as repo:
281 with self._obtainrepo() as repo:
282 for r in self._runwsgi(req, repo):
282 for r in self._runwsgi(req, repo):
283 yield r
283 yield r
284
284
285 def _runwsgi(self, req, repo):
285 def _runwsgi(self, req, repo):
286 rctx = requestcontext(self, repo)
286 rctx = requestcontext(self, repo)
287
287
288 # This state is global across all threads.
288 # This state is global across all threads.
289 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
289 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
290 rctx.repo.ui.environ = req.env
290 rctx.repo.ui.environ = req.env
291
291
292 # work with CGI variables to create coherent structure
292 # work with CGI variables to create coherent structure
293 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
293 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
294
294
295 req.url = req.env['SCRIPT_NAME']
295 req.url = req.env['SCRIPT_NAME']
296 if not req.url.endswith('/'):
296 if not req.url.endswith('/'):
297 req.url += '/'
297 req.url += '/'
298 if 'REPO_NAME' in req.env:
298 if 'REPO_NAME' in req.env:
299 req.url += req.env['REPO_NAME'] + '/'
299 req.url += req.env['REPO_NAME'] + '/'
300
300
301 if 'PATH_INFO' in req.env:
301 if 'PATH_INFO' in req.env:
302 parts = req.env['PATH_INFO'].strip('/').split('/')
302 parts = req.env['PATH_INFO'].strip('/').split('/')
303 repo_parts = req.env.get('REPO_NAME', '').split('/')
303 repo_parts = req.env.get('REPO_NAME', '').split('/')
304 if parts[:len(repo_parts)] == repo_parts:
304 if parts[:len(repo_parts)] == repo_parts:
305 parts = parts[len(repo_parts):]
305 parts = parts[len(repo_parts):]
306 query = '/'.join(parts)
306 query = '/'.join(parts)
307 else:
307 else:
308 query = req.env['QUERY_STRING'].partition('&')[0]
308 query = req.env['QUERY_STRING'].partition('&')[0]
309 query = query.partition(';')[0]
309 query = query.partition(';')[0]
310
310
311 # process this if it's a protocol request
311 # process this if it's a protocol request
312 # protocol bits don't need to create any URLs
312 # protocol bits don't need to create any URLs
313 # and the clients always use the old URL structure
313 # and the clients always use the old URL structure
314
314
315 cmd = req.form.get('cmd', [''])[0]
315 cmd = req.form.get('cmd', [''])[0]
316 if protocol.iscmd(cmd):
316 if protocol.iscmd(cmd):
317 try:
317 try:
318 if query:
318 if query:
319 raise ErrorResponse(HTTP_NOT_FOUND)
319 raise ErrorResponse(HTTP_NOT_FOUND)
320 if cmd in perms:
320 if cmd in perms:
321 self.check_perm(rctx, req, perms[cmd])
321 self.check_perm(rctx, req, perms[cmd])
322 return protocol.call(rctx.repo, req, cmd)
322 return protocol.call(rctx.repo, req, cmd)
323 except ErrorResponse as inst:
323 except ErrorResponse as inst:
324 # A client that sends unbundle without 100-continue will
324 # A client that sends unbundle without 100-continue will
325 # break if we respond early.
325 # break if we respond early.
326 if (cmd == 'unbundle' and
326 if (cmd == 'unbundle' and
327 (req.env.get('HTTP_EXPECT',
327 (req.env.get('HTTP_EXPECT',
328 '').lower() != '100-continue') or
328 '').lower() != '100-continue') or
329 req.env.get('X-HgHttp2', '')):
329 req.env.get('X-HgHttp2', '')):
330 req.drain()
330 req.drain()
331 else:
331 else:
332 req.headers.append(('Connection', 'Close'))
332 req.headers.append(('Connection', 'Close'))
333 req.respond(inst, protocol.HGTYPE,
333 req.respond(inst, protocol.HGTYPE,
334 body='0\n%s\n' % inst)
334 body='0\n%s\n' % inst)
335 return ''
335 return ''
336
336
337 # translate user-visible url structure to internal structure
337 # translate user-visible url structure to internal structure
338
338
339 args = query.split('/', 2)
339 args = query.split('/', 2)
340 if 'cmd' not in req.form and args and args[0]:
340 if 'cmd' not in req.form and args and args[0]:
341
341
342 cmd = args.pop(0)
342 cmd = args.pop(0)
343 style = cmd.rfind('-')
343 style = cmd.rfind('-')
344 if style != -1:
344 if style != -1:
345 req.form['style'] = [cmd[:style]]
345 req.form['style'] = [cmd[:style]]
346 cmd = cmd[style + 1:]
346 cmd = cmd[style + 1:]
347
347
348 # avoid accepting e.g. style parameter as command
348 # avoid accepting e.g. style parameter as command
349 if util.safehasattr(webcommands, cmd):
349 if util.safehasattr(webcommands, cmd):
350 req.form['cmd'] = [cmd]
350 req.form['cmd'] = [cmd]
351
351
352 if cmd == 'static':
352 if cmd == 'static':
353 req.form['file'] = ['/'.join(args)]
353 req.form['file'] = ['/'.join(args)]
354 else:
354 else:
355 if args and args[0]:
355 if args and args[0]:
356 node = args.pop(0).replace('%2F', '/')
356 node = args.pop(0).replace('%2F', '/')
357 req.form['node'] = [node]
357 req.form['node'] = [node]
358 if args:
358 if args:
359 req.form['file'] = args
359 req.form['file'] = args
360
360
361 ua = req.env.get('HTTP_USER_AGENT', '')
361 ua = req.env.get('HTTP_USER_AGENT', '')
362 if cmd == 'rev' and 'mercurial' in ua:
362 if cmd == 'rev' and 'mercurial' in ua:
363 req.form['style'] = ['raw']
363 req.form['style'] = ['raw']
364
364
365 if cmd == 'archive':
365 if cmd == 'archive':
366 fn = req.form['node'][0]
366 fn = req.form['node'][0]
367 for type_, spec in rctx.archivespecs.iteritems():
367 for type_, spec in rctx.archivespecs.iteritems():
368 ext = spec[2]
368 ext = spec[2]
369 if fn.endswith(ext):
369 if fn.endswith(ext):
370 req.form['node'] = [fn[:-len(ext)]]
370 req.form['node'] = [fn[:-len(ext)]]
371 req.form['type'] = [type_]
371 req.form['type'] = [type_]
372
372
373 # process the web interface request
373 # process the web interface request
374
374
375 try:
375 try:
376 tmpl = rctx.templater(req)
376 tmpl = rctx.templater(req)
377 ctype = tmpl('mimetype', encoding=encoding.encoding)
377 ctype = tmpl('mimetype', encoding=encoding.encoding)
378 ctype = templater.stringify(ctype)
378 ctype = templater.stringify(ctype)
379
379
380 # check read permissions non-static content
380 # check read permissions non-static content
381 if cmd != 'static':
381 if cmd != 'static':
382 self.check_perm(rctx, req, None)
382 self.check_perm(rctx, req, None)
383
383
384 if cmd == '':
384 if cmd == '':
385 req.form['cmd'] = [tmpl.cache['default']]
385 req.form['cmd'] = [tmpl.cache['default']]
386 cmd = req.form['cmd'][0]
386 cmd = req.form['cmd'][0]
387
387
388 if rctx.configbool('web', 'cache', True):
388 if rctx.configbool('web', 'cache', True):
389 caching(self, req) # sets ETag header or raises NOT_MODIFIED
389 caching(self, req) # sets ETag header or raises NOT_MODIFIED
390 if cmd not in webcommands.__all__:
390 if cmd not in webcommands.__all__:
391 msg = 'no such method: %s' % cmd
391 msg = 'no such method: %s' % cmd
392 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
392 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
393 elif cmd == 'file' and 'raw' in req.form.get('style', []):
393 elif cmd == 'file' and 'raw' in req.form.get('style', []):
394 rctx.ctype = ctype
394 rctx.ctype = ctype
395 content = webcommands.rawfile(rctx, req, tmpl)
395 content = webcommands.rawfile(rctx, req, tmpl)
396 else:
396 else:
397 content = getattr(webcommands, cmd)(rctx, req, tmpl)
397 content = getattr(webcommands, cmd)(rctx, req, tmpl)
398 req.respond(HTTP_OK, ctype)
398 req.respond(HTTP_OK, ctype)
399
399
400 return content
400 return content
401
401
402 except (error.LookupError, error.RepoLookupError) as err:
402 except (error.LookupError, error.RepoLookupError) as err:
403 req.respond(HTTP_NOT_FOUND, ctype)
403 req.respond(HTTP_NOT_FOUND, ctype)
404 msg = str(err)
404 msg = str(err)
405 if (util.safehasattr(err, 'name') and
405 if (util.safehasattr(err, 'name') and
406 not isinstance(err, error.ManifestLookupError)):
406 not isinstance(err, error.ManifestLookupError)):
407 msg = 'revision not found: %s' % err.name
407 msg = 'revision not found: %s' % err.name
408 return tmpl('error', error=msg)
408 return tmpl('error', error=msg)
409 except (error.RepoError, error.RevlogError) as inst:
409 except (error.RepoError, error.RevlogError) as inst:
410 req.respond(HTTP_SERVER_ERROR, ctype)
410 req.respond(HTTP_SERVER_ERROR, ctype)
411 return tmpl('error', error=str(inst))
411 return tmpl('error', error=str(inst))
412 except ErrorResponse as inst:
412 except ErrorResponse as inst:
413 req.respond(inst, ctype)
413 req.respond(inst, ctype)
414 if inst.code == HTTP_NOT_MODIFIED:
414 if inst.code == HTTP_NOT_MODIFIED:
415 # Not allowed to return a body on a 304
415 # Not allowed to return a body on a 304
416 return ['']
416 return ['']
417 return tmpl('error', error=str(inst))
417 return tmpl('error', error=str(inst))
418
418
419 def check_perm(self, rctx, req, op):
419 def check_perm(self, rctx, req, op):
420 for permhook in permhooks:
420 for permhook in permhooks:
421 permhook(rctx, req, op)
421 permhook(rctx, req, op)
422
422
423 def getwebview(repo):
423 def getwebview(repo):
424 """The 'web.view' config controls changeset filter to hgweb. Possible
424 """The 'web.view' config controls changeset filter to hgweb. Possible
425 values are ``served``, ``visible`` and ``all``. Default is ``served``.
425 values are ``served``, ``visible`` and ``all``. Default is ``served``.
426 The ``served`` filter only shows changesets that can be pulled from the
426 The ``served`` filter only shows changesets that can be pulled from the
427 hgweb instance. The``visible`` filter includes secret changesets but
427 hgweb instance. The``visible`` filter includes secret changesets but
428 still excludes "hidden" one.
428 still excludes "hidden" one.
429
429
430 See the repoview module for details.
430 See the repoview module for details.
431
431
432 The option has been around undocumented since Mercurial 2.5, but no
432 The option has been around undocumented since Mercurial 2.5, but no
433 user ever asked about it. So we better keep it undocumented for now."""
433 user ever asked about it. So we better keep it undocumented for now."""
434 viewconfig = repo.ui.config('web', 'view', 'served',
434 viewconfig = repo.ui.config('web', 'view', 'served',
435 untrusted=True)
435 untrusted=True)
436 if viewconfig == 'all':
436 if viewconfig == 'all':
437 return repo.unfiltered()
437 return repo.unfiltered()
438 elif viewconfig in repoview.filtertable:
438 elif viewconfig in repoview.filtertable:
439 return repo.filtered(viewconfig)
439 return repo.filtered(viewconfig)
440 else:
440 else:
441 return repo.filtered('served')
441 return repo.filtered('served')
442
442
@@ -1,585 +1,585 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 import os, copy
9 import os, copy
10 import re
10 import re
11 from mercurial import match, patch, error, util, pathutil, context
11 from mercurial import match, patch, error, util, pathutil, context
12 from mercurial import ui as uimod
12 from mercurial import ui as uimod
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial.node import hex, nullid, short
14 from mercurial.node import hex, nullid, short
15 from mercurial.templatefilters import revescape
15 from mercurial import templatefilters
16 from common import ErrorResponse, paritygen
16 from common import ErrorResponse, paritygen
17 from common import HTTP_NOT_FOUND
17 from common import HTTP_NOT_FOUND
18 import difflib
18 import difflib
19
19
20 def up(p):
20 def up(p):
21 if p[0] != "/":
21 if p[0] != "/":
22 p = "/" + p
22 p = "/" + p
23 if p[-1] == "/":
23 if p[-1] == "/":
24 p = p[:-1]
24 p = p[:-1]
25 up = os.path.dirname(p)
25 up = os.path.dirname(p)
26 if up == "/":
26 if up == "/":
27 return "/"
27 return "/"
28 return up + "/"
28 return up + "/"
29
29
30 def _navseq(step, firststep=None):
30 def _navseq(step, firststep=None):
31 if firststep:
31 if firststep:
32 yield firststep
32 yield firststep
33 if firststep >= 20 and firststep <= 40:
33 if firststep >= 20 and firststep <= 40:
34 firststep = 50
34 firststep = 50
35 yield firststep
35 yield firststep
36 assert step > 0
36 assert step > 0
37 assert firststep > 0
37 assert firststep > 0
38 while step <= firststep:
38 while step <= firststep:
39 step *= 10
39 step *= 10
40 while True:
40 while True:
41 yield 1 * step
41 yield 1 * step
42 yield 3 * step
42 yield 3 * step
43 step *= 10
43 step *= 10
44
44
45 class revnav(object):
45 class revnav(object):
46
46
47 def __init__(self, repo):
47 def __init__(self, repo):
48 """Navigation generation object
48 """Navigation generation object
49
49
50 :repo: repo object we generate nav for
50 :repo: repo object we generate nav for
51 """
51 """
52 # used for hex generation
52 # used for hex generation
53 self._revlog = repo.changelog
53 self._revlog = repo.changelog
54
54
55 def __nonzero__(self):
55 def __nonzero__(self):
56 """return True if any revision to navigate over"""
56 """return True if any revision to navigate over"""
57 return self._first() is not None
57 return self._first() is not None
58
58
59 def _first(self):
59 def _first(self):
60 """return the minimum non-filtered changeset or None"""
60 """return the minimum non-filtered changeset or None"""
61 try:
61 try:
62 return iter(self._revlog).next()
62 return iter(self._revlog).next()
63 except StopIteration:
63 except StopIteration:
64 return None
64 return None
65
65
66 def hex(self, rev):
66 def hex(self, rev):
67 return hex(self._revlog.node(rev))
67 return hex(self._revlog.node(rev))
68
68
69 def gen(self, pos, pagelen, limit):
69 def gen(self, pos, pagelen, limit):
70 """computes label and revision id for navigation link
70 """computes label and revision id for navigation link
71
71
72 :pos: is the revision relative to which we generate navigation.
72 :pos: is the revision relative to which we generate navigation.
73 :pagelen: the size of each navigation page
73 :pagelen: the size of each navigation page
74 :limit: how far shall we link
74 :limit: how far shall we link
75
75
76 The return is:
76 The return is:
77 - a single element tuple
77 - a single element tuple
78 - containing a dictionary with a `before` and `after` key
78 - containing a dictionary with a `before` and `after` key
79 - values are generator functions taking arbitrary number of kwargs
79 - values are generator functions taking arbitrary number of kwargs
80 - yield items are dictionaries with `label` and `node` keys
80 - yield items are dictionaries with `label` and `node` keys
81 """
81 """
82 if not self:
82 if not self:
83 # empty repo
83 # empty repo
84 return ({'before': (), 'after': ()},)
84 return ({'before': (), 'after': ()},)
85
85
86 targets = []
86 targets = []
87 for f in _navseq(1, pagelen):
87 for f in _navseq(1, pagelen):
88 if f > limit:
88 if f > limit:
89 break
89 break
90 targets.append(pos + f)
90 targets.append(pos + f)
91 targets.append(pos - f)
91 targets.append(pos - f)
92 targets.sort()
92 targets.sort()
93
93
94 first = self._first()
94 first = self._first()
95 navbefore = [("(%i)" % first, self.hex(first))]
95 navbefore = [("(%i)" % first, self.hex(first))]
96 navafter = []
96 navafter = []
97 for rev in targets:
97 for rev in targets:
98 if rev not in self._revlog:
98 if rev not in self._revlog:
99 continue
99 continue
100 if pos < rev < limit:
100 if pos < rev < limit:
101 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
101 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
102 if 0 < rev < pos:
102 if 0 < rev < pos:
103 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
103 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
104
104
105
105
106 navafter.append(("tip", "tip"))
106 navafter.append(("tip", "tip"))
107
107
108 data = lambda i: {"label": i[0], "node": i[1]}
108 data = lambda i: {"label": i[0], "node": i[1]}
109 return ({'before': lambda **map: (data(i) for i in navbefore),
109 return ({'before': lambda **map: (data(i) for i in navbefore),
110 'after': lambda **map: (data(i) for i in navafter)},)
110 'after': lambda **map: (data(i) for i in navafter)},)
111
111
112 class filerevnav(revnav):
112 class filerevnav(revnav):
113
113
114 def __init__(self, repo, path):
114 def __init__(self, repo, path):
115 """Navigation generation object
115 """Navigation generation object
116
116
117 :repo: repo object we generate nav for
117 :repo: repo object we generate nav for
118 :path: path of the file we generate nav for
118 :path: path of the file we generate nav for
119 """
119 """
120 # used for iteration
120 # used for iteration
121 self._changelog = repo.unfiltered().changelog
121 self._changelog = repo.unfiltered().changelog
122 # used for hex generation
122 # used for hex generation
123 self._revlog = repo.file(path)
123 self._revlog = repo.file(path)
124
124
125 def hex(self, rev):
125 def hex(self, rev):
126 return hex(self._changelog.node(self._revlog.linkrev(rev)))
126 return hex(self._changelog.node(self._revlog.linkrev(rev)))
127
127
128
128
129 def _siblings(siblings=[], hiderev=None):
129 def _siblings(siblings=[], hiderev=None):
130 siblings = [s for s in siblings if s.node() != nullid]
130 siblings = [s for s in siblings if s.node() != nullid]
131 if len(siblings) == 1 and siblings[0].rev() == hiderev:
131 if len(siblings) == 1 and siblings[0].rev() == hiderev:
132 return
132 return
133 for s in siblings:
133 for s in siblings:
134 d = {'node': s.hex(), 'rev': s.rev()}
134 d = {'node': s.hex(), 'rev': s.rev()}
135 d['user'] = s.user()
135 d['user'] = s.user()
136 d['date'] = s.date()
136 d['date'] = s.date()
137 d['description'] = s.description()
137 d['description'] = s.description()
138 d['branch'] = s.branch()
138 d['branch'] = s.branch()
139 if util.safehasattr(s, 'path'):
139 if util.safehasattr(s, 'path'):
140 d['file'] = s.path()
140 d['file'] = s.path()
141 yield d
141 yield d
142
142
143 def parents(ctx, hide=None):
143 def parents(ctx, hide=None):
144 if isinstance(ctx, context.basefilectx):
144 if isinstance(ctx, context.basefilectx):
145 introrev = ctx.introrev()
145 introrev = ctx.introrev()
146 if ctx.changectx().rev() != introrev:
146 if ctx.changectx().rev() != introrev:
147 return _siblings([ctx.repo()[introrev]], hide)
147 return _siblings([ctx.repo()[introrev]], hide)
148 return _siblings(ctx.parents(), hide)
148 return _siblings(ctx.parents(), hide)
149
149
150 def children(ctx, hide=None):
150 def children(ctx, hide=None):
151 return _siblings(ctx.children(), hide)
151 return _siblings(ctx.children(), hide)
152
152
153 def renamelink(fctx):
153 def renamelink(fctx):
154 r = fctx.renamed()
154 r = fctx.renamed()
155 if r:
155 if r:
156 return [{'file': r[0], 'node': hex(r[1])}]
156 return [{'file': r[0], 'node': hex(r[1])}]
157 return []
157 return []
158
158
159 def nodetagsdict(repo, node):
159 def nodetagsdict(repo, node):
160 return [{"name": i} for i in repo.nodetags(node)]
160 return [{"name": i} for i in repo.nodetags(node)]
161
161
162 def nodebookmarksdict(repo, node):
162 def nodebookmarksdict(repo, node):
163 return [{"name": i} for i in repo.nodebookmarks(node)]
163 return [{"name": i} for i in repo.nodebookmarks(node)]
164
164
165 def nodebranchdict(repo, ctx):
165 def nodebranchdict(repo, ctx):
166 branches = []
166 branches = []
167 branch = ctx.branch()
167 branch = ctx.branch()
168 # If this is an empty repo, ctx.node() == nullid,
168 # If this is an empty repo, ctx.node() == nullid,
169 # ctx.branch() == 'default'.
169 # ctx.branch() == 'default'.
170 try:
170 try:
171 branchnode = repo.branchtip(branch)
171 branchnode = repo.branchtip(branch)
172 except error.RepoLookupError:
172 except error.RepoLookupError:
173 branchnode = None
173 branchnode = None
174 if branchnode == ctx.node():
174 if branchnode == ctx.node():
175 branches.append({"name": branch})
175 branches.append({"name": branch})
176 return branches
176 return branches
177
177
178 def nodeinbranch(repo, ctx):
178 def nodeinbranch(repo, ctx):
179 branches = []
179 branches = []
180 branch = ctx.branch()
180 branch = ctx.branch()
181 try:
181 try:
182 branchnode = repo.branchtip(branch)
182 branchnode = repo.branchtip(branch)
183 except error.RepoLookupError:
183 except error.RepoLookupError:
184 branchnode = None
184 branchnode = None
185 if branch != 'default' and branchnode != ctx.node():
185 if branch != 'default' and branchnode != ctx.node():
186 branches.append({"name": branch})
186 branches.append({"name": branch})
187 return branches
187 return branches
188
188
189 def nodebranchnodefault(ctx):
189 def nodebranchnodefault(ctx):
190 branches = []
190 branches = []
191 branch = ctx.branch()
191 branch = ctx.branch()
192 if branch != 'default':
192 if branch != 'default':
193 branches.append({"name": branch})
193 branches.append({"name": branch})
194 return branches
194 return branches
195
195
196 def showtag(repo, tmpl, t1, node=nullid, **args):
196 def showtag(repo, tmpl, t1, node=nullid, **args):
197 for t in repo.nodetags(node):
197 for t in repo.nodetags(node):
198 yield tmpl(t1, tag=t, **args)
198 yield tmpl(t1, tag=t, **args)
199
199
200 def showbookmark(repo, tmpl, t1, node=nullid, **args):
200 def showbookmark(repo, tmpl, t1, node=nullid, **args):
201 for t in repo.nodebookmarks(node):
201 for t in repo.nodebookmarks(node):
202 yield tmpl(t1, bookmark=t, **args)
202 yield tmpl(t1, bookmark=t, **args)
203
203
204 def branchentries(repo, stripecount, limit=0):
204 def branchentries(repo, stripecount, limit=0):
205 tips = []
205 tips = []
206 heads = repo.heads()
206 heads = repo.heads()
207 parity = paritygen(stripecount)
207 parity = paritygen(stripecount)
208 sortkey = lambda item: (not item[1], item[0].rev())
208 sortkey = lambda item: (not item[1], item[0].rev())
209
209
210 def entries(**map):
210 def entries(**map):
211 count = 0
211 count = 0
212 if not tips:
212 if not tips:
213 for tag, hs, tip, closed in repo.branchmap().iterbranches():
213 for tag, hs, tip, closed in repo.branchmap().iterbranches():
214 tips.append((repo[tip], closed))
214 tips.append((repo[tip], closed))
215 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
215 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
216 if limit > 0 and count >= limit:
216 if limit > 0 and count >= limit:
217 return
217 return
218 count += 1
218 count += 1
219 if closed:
219 if closed:
220 status = 'closed'
220 status = 'closed'
221 elif ctx.node() not in heads:
221 elif ctx.node() not in heads:
222 status = 'inactive'
222 status = 'inactive'
223 else:
223 else:
224 status = 'open'
224 status = 'open'
225 yield {
225 yield {
226 'parity': parity.next(),
226 'parity': parity.next(),
227 'branch': ctx.branch(),
227 'branch': ctx.branch(),
228 'status': status,
228 'status': status,
229 'node': ctx.hex(),
229 'node': ctx.hex(),
230 'date': ctx.date()
230 'date': ctx.date()
231 }
231 }
232
232
233 return entries
233 return entries
234
234
235 def cleanpath(repo, path):
235 def cleanpath(repo, path):
236 path = path.lstrip('/')
236 path = path.lstrip('/')
237 return pathutil.canonpath(repo.root, '', path)
237 return pathutil.canonpath(repo.root, '', path)
238
238
239 def changeidctx(repo, changeid):
239 def changeidctx(repo, changeid):
240 try:
240 try:
241 ctx = repo[changeid]
241 ctx = repo[changeid]
242 except error.RepoError:
242 except error.RepoError:
243 man = repo.manifest
243 man = repo.manifest
244 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
244 ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
245
245
246 return ctx
246 return ctx
247
247
248 def changectx(repo, req):
248 def changectx(repo, req):
249 changeid = "tip"
249 changeid = "tip"
250 if 'node' in req.form:
250 if 'node' in req.form:
251 changeid = req.form['node'][0]
251 changeid = req.form['node'][0]
252 ipos = changeid.find(':')
252 ipos = changeid.find(':')
253 if ipos != -1:
253 if ipos != -1:
254 changeid = changeid[(ipos + 1):]
254 changeid = changeid[(ipos + 1):]
255 elif 'manifest' in req.form:
255 elif 'manifest' in req.form:
256 changeid = req.form['manifest'][0]
256 changeid = req.form['manifest'][0]
257
257
258 return changeidctx(repo, changeid)
258 return changeidctx(repo, changeid)
259
259
260 def basechangectx(repo, req):
260 def basechangectx(repo, req):
261 if 'node' in req.form:
261 if 'node' in req.form:
262 changeid = req.form['node'][0]
262 changeid = req.form['node'][0]
263 ipos = changeid.find(':')
263 ipos = changeid.find(':')
264 if ipos != -1:
264 if ipos != -1:
265 changeid = changeid[:ipos]
265 changeid = changeid[:ipos]
266 return changeidctx(repo, changeid)
266 return changeidctx(repo, changeid)
267
267
268 return None
268 return None
269
269
270 def filectx(repo, req):
270 def filectx(repo, req):
271 if 'file' not in req.form:
271 if 'file' not in req.form:
272 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
272 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
273 path = cleanpath(repo, req.form['file'][0])
273 path = cleanpath(repo, req.form['file'][0])
274 if 'node' in req.form:
274 if 'node' in req.form:
275 changeid = req.form['node'][0]
275 changeid = req.form['node'][0]
276 elif 'filenode' in req.form:
276 elif 'filenode' in req.form:
277 changeid = req.form['filenode'][0]
277 changeid = req.form['filenode'][0]
278 else:
278 else:
279 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
279 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
280 try:
280 try:
281 fctx = repo[changeid][path]
281 fctx = repo[changeid][path]
282 except error.RepoError:
282 except error.RepoError:
283 fctx = repo.filectx(path, fileid=changeid)
283 fctx = repo.filectx(path, fileid=changeid)
284
284
285 return fctx
285 return fctx
286
286
287 def changelistentry(web, ctx, tmpl):
287 def changelistentry(web, ctx, tmpl):
288 '''Obtain a dictionary to be used for entries in a changelist.
288 '''Obtain a dictionary to be used for entries in a changelist.
289
289
290 This function is called when producing items for the "entries" list passed
290 This function is called when producing items for the "entries" list passed
291 to the "shortlog" and "changelog" templates.
291 to the "shortlog" and "changelog" templates.
292 '''
292 '''
293 repo = web.repo
293 repo = web.repo
294 rev = ctx.rev()
294 rev = ctx.rev()
295 n = ctx.node()
295 n = ctx.node()
296 showtags = showtag(repo, tmpl, 'changelogtag', n)
296 showtags = showtag(repo, tmpl, 'changelogtag', n)
297 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
297 files = listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
298
298
299 return {
299 return {
300 "author": ctx.user(),
300 "author": ctx.user(),
301 "parent": lambda **x: parents(ctx, rev - 1),
301 "parent": lambda **x: parents(ctx, rev - 1),
302 "child": lambda **x: children(ctx, rev + 1),
302 "child": lambda **x: children(ctx, rev + 1),
303 "changelogtag": showtags,
303 "changelogtag": showtags,
304 "desc": ctx.description(),
304 "desc": ctx.description(),
305 "extra": ctx.extra(),
305 "extra": ctx.extra(),
306 "date": ctx.date(),
306 "date": ctx.date(),
307 "files": files,
307 "files": files,
308 "rev": rev,
308 "rev": rev,
309 "node": hex(n),
309 "node": hex(n),
310 "tags": nodetagsdict(repo, n),
310 "tags": nodetagsdict(repo, n),
311 "bookmarks": nodebookmarksdict(repo, n),
311 "bookmarks": nodebookmarksdict(repo, n),
312 "inbranch": nodeinbranch(repo, ctx),
312 "inbranch": nodeinbranch(repo, ctx),
313 "branches": nodebranchdict(repo, ctx)
313 "branches": nodebranchdict(repo, ctx)
314 }
314 }
315
315
316 def symrevorshortnode(req, ctx):
316 def symrevorshortnode(req, ctx):
317 if 'node' in req.form:
317 if 'node' in req.form:
318 return revescape(req.form['node'][0])
318 return templatefilters.revescape(req.form['node'][0])
319 else:
319 else:
320 return short(ctx.node())
320 return short(ctx.node())
321
321
322 def changesetentry(web, req, tmpl, ctx):
322 def changesetentry(web, req, tmpl, ctx):
323 '''Obtain a dictionary to be used to render the "changeset" template.'''
323 '''Obtain a dictionary to be used to render the "changeset" template.'''
324
324
325 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
325 showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node())
326 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
326 showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark',
327 ctx.node())
327 ctx.node())
328 showbranch = nodebranchnodefault(ctx)
328 showbranch = nodebranchnodefault(ctx)
329
329
330 files = []
330 files = []
331 parity = paritygen(web.stripecount)
331 parity = paritygen(web.stripecount)
332 for blockno, f in enumerate(ctx.files()):
332 for blockno, f in enumerate(ctx.files()):
333 template = f in ctx and 'filenodelink' or 'filenolink'
333 template = f in ctx and 'filenodelink' or 'filenolink'
334 files.append(tmpl(template,
334 files.append(tmpl(template,
335 node=ctx.hex(), file=f, blockno=blockno + 1,
335 node=ctx.hex(), file=f, blockno=blockno + 1,
336 parity=parity.next()))
336 parity=parity.next()))
337
337
338 basectx = basechangectx(web.repo, req)
338 basectx = basechangectx(web.repo, req)
339 if basectx is None:
339 if basectx is None:
340 basectx = ctx.p1()
340 basectx = ctx.p1()
341
341
342 style = web.config('web', 'style', 'paper')
342 style = web.config('web', 'style', 'paper')
343 if 'style' in req.form:
343 if 'style' in req.form:
344 style = req.form['style'][0]
344 style = req.form['style'][0]
345
345
346 parity = paritygen(web.stripecount)
346 parity = paritygen(web.stripecount)
347 diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
347 diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
348
348
349 parity = paritygen(web.stripecount)
349 parity = paritygen(web.stripecount)
350 diffstatsgen = diffstatgen(ctx, basectx)
350 diffstatsgen = diffstatgen(ctx, basectx)
351 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
351 diffstats = diffstat(tmpl, ctx, diffstatsgen, parity)
352
352
353 return dict(
353 return dict(
354 diff=diff,
354 diff=diff,
355 rev=ctx.rev(),
355 rev=ctx.rev(),
356 node=ctx.hex(),
356 node=ctx.hex(),
357 symrev=symrevorshortnode(req, ctx),
357 symrev=symrevorshortnode(req, ctx),
358 parent=tuple(parents(ctx)),
358 parent=tuple(parents(ctx)),
359 child=children(ctx),
359 child=children(ctx),
360 basenode=basectx.hex(),
360 basenode=basectx.hex(),
361 changesettag=showtags,
361 changesettag=showtags,
362 changesetbookmark=showbookmarks,
362 changesetbookmark=showbookmarks,
363 changesetbranch=showbranch,
363 changesetbranch=showbranch,
364 author=ctx.user(),
364 author=ctx.user(),
365 desc=ctx.description(),
365 desc=ctx.description(),
366 extra=ctx.extra(),
366 extra=ctx.extra(),
367 date=ctx.date(),
367 date=ctx.date(),
368 phase=ctx.phasestr(),
368 phase=ctx.phasestr(),
369 files=files,
369 files=files,
370 diffsummary=lambda **x: diffsummary(diffstatsgen),
370 diffsummary=lambda **x: diffsummary(diffstatsgen),
371 diffstat=diffstats,
371 diffstat=diffstats,
372 archives=web.archivelist(ctx.hex()),
372 archives=web.archivelist(ctx.hex()),
373 tags=nodetagsdict(web.repo, ctx.node()),
373 tags=nodetagsdict(web.repo, ctx.node()),
374 bookmarks=nodebookmarksdict(web.repo, ctx.node()),
374 bookmarks=nodebookmarksdict(web.repo, ctx.node()),
375 branch=showbranch,
375 branch=showbranch,
376 inbranch=nodeinbranch(web.repo, ctx),
376 inbranch=nodeinbranch(web.repo, ctx),
377 branches=nodebranchdict(web.repo, ctx))
377 branches=nodebranchdict(web.repo, ctx))
378
378
379 def listfilediffs(tmpl, files, node, max):
379 def listfilediffs(tmpl, files, node, max):
380 for f in files[:max]:
380 for f in files[:max]:
381 yield tmpl('filedifflink', node=hex(node), file=f)
381 yield tmpl('filedifflink', node=hex(node), file=f)
382 if len(files) > max:
382 if len(files) > max:
383 yield tmpl('fileellipses')
383 yield tmpl('fileellipses')
384
384
385 def diffs(repo, tmpl, ctx, basectx, files, parity, style):
385 def diffs(repo, tmpl, ctx, basectx, files, parity, style):
386
386
387 def countgen():
387 def countgen():
388 start = 1
388 start = 1
389 while True:
389 while True:
390 yield start
390 yield start
391 start += 1
391 start += 1
392
392
393 blockcount = countgen()
393 blockcount = countgen()
394 def prettyprintlines(diff, blockno):
394 def prettyprintlines(diff, blockno):
395 for lineno, l in enumerate(diff.splitlines(True)):
395 for lineno, l in enumerate(diff.splitlines(True)):
396 difflineno = "%d.%d" % (blockno, lineno + 1)
396 difflineno = "%d.%d" % (blockno, lineno + 1)
397 if l.startswith('+'):
397 if l.startswith('+'):
398 ltype = "difflineplus"
398 ltype = "difflineplus"
399 elif l.startswith('-'):
399 elif l.startswith('-'):
400 ltype = "difflineminus"
400 ltype = "difflineminus"
401 elif l.startswith('@'):
401 elif l.startswith('@'):
402 ltype = "difflineat"
402 ltype = "difflineat"
403 else:
403 else:
404 ltype = "diffline"
404 ltype = "diffline"
405 yield tmpl(ltype,
405 yield tmpl(ltype,
406 line=l,
406 line=l,
407 lineno=lineno + 1,
407 lineno=lineno + 1,
408 lineid="l%s" % difflineno,
408 lineid="l%s" % difflineno,
409 linenumber="% 8s" % difflineno)
409 linenumber="% 8s" % difflineno)
410
410
411 if files:
411 if files:
412 m = match.exact(repo.root, repo.getcwd(), files)
412 m = match.exact(repo.root, repo.getcwd(), files)
413 else:
413 else:
414 m = match.always(repo.root, repo.getcwd())
414 m = match.always(repo.root, repo.getcwd())
415
415
416 diffopts = patch.diffopts(repo.ui, untrusted=True)
416 diffopts = patch.diffopts(repo.ui, untrusted=True)
417 if basectx is None:
417 if basectx is None:
418 parents = ctx.parents()
418 parents = ctx.parents()
419 if parents:
419 if parents:
420 node1 = parents[0].node()
420 node1 = parents[0].node()
421 else:
421 else:
422 node1 = nullid
422 node1 = nullid
423 else:
423 else:
424 node1 = basectx.node()
424 node1 = basectx.node()
425 node2 = ctx.node()
425 node2 = ctx.node()
426
426
427 block = []
427 block = []
428 for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
428 for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
429 if chunk.startswith('diff') and block:
429 if chunk.startswith('diff') and block:
430 blockno = blockcount.next()
430 blockno = blockcount.next()
431 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
431 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
432 lines=prettyprintlines(''.join(block), blockno))
432 lines=prettyprintlines(''.join(block), blockno))
433 block = []
433 block = []
434 if chunk.startswith('diff') and style != 'raw':
434 if chunk.startswith('diff') and style != 'raw':
435 chunk = ''.join(chunk.splitlines(True)[1:])
435 chunk = ''.join(chunk.splitlines(True)[1:])
436 block.append(chunk)
436 block.append(chunk)
437 blockno = blockcount.next()
437 blockno = blockcount.next()
438 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
438 yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
439 lines=prettyprintlines(''.join(block), blockno))
439 lines=prettyprintlines(''.join(block), blockno))
440
440
441 def compare(tmpl, context, leftlines, rightlines):
441 def compare(tmpl, context, leftlines, rightlines):
442 '''Generator function that provides side-by-side comparison data.'''
442 '''Generator function that provides side-by-side comparison data.'''
443
443
444 def compline(type, leftlineno, leftline, rightlineno, rightline):
444 def compline(type, leftlineno, leftline, rightlineno, rightline):
445 lineid = leftlineno and ("l%s" % leftlineno) or ''
445 lineid = leftlineno and ("l%s" % leftlineno) or ''
446 lineid += rightlineno and ("r%s" % rightlineno) or ''
446 lineid += rightlineno and ("r%s" % rightlineno) or ''
447 return tmpl('comparisonline',
447 return tmpl('comparisonline',
448 type=type,
448 type=type,
449 lineid=lineid,
449 lineid=lineid,
450 leftlineno=leftlineno,
450 leftlineno=leftlineno,
451 leftlinenumber="% 6s" % (leftlineno or ''),
451 leftlinenumber="% 6s" % (leftlineno or ''),
452 leftline=leftline or '',
452 leftline=leftline or '',
453 rightlineno=rightlineno,
453 rightlineno=rightlineno,
454 rightlinenumber="% 6s" % (rightlineno or ''),
454 rightlinenumber="% 6s" % (rightlineno or ''),
455 rightline=rightline or '')
455 rightline=rightline or '')
456
456
457 def getblock(opcodes):
457 def getblock(opcodes):
458 for type, llo, lhi, rlo, rhi in opcodes:
458 for type, llo, lhi, rlo, rhi in opcodes:
459 len1 = lhi - llo
459 len1 = lhi - llo
460 len2 = rhi - rlo
460 len2 = rhi - rlo
461 count = min(len1, len2)
461 count = min(len1, len2)
462 for i in xrange(count):
462 for i in xrange(count):
463 yield compline(type=type,
463 yield compline(type=type,
464 leftlineno=llo + i + 1,
464 leftlineno=llo + i + 1,
465 leftline=leftlines[llo + i],
465 leftline=leftlines[llo + i],
466 rightlineno=rlo + i + 1,
466 rightlineno=rlo + i + 1,
467 rightline=rightlines[rlo + i])
467 rightline=rightlines[rlo + i])
468 if len1 > len2:
468 if len1 > len2:
469 for i in xrange(llo + count, lhi):
469 for i in xrange(llo + count, lhi):
470 yield compline(type=type,
470 yield compline(type=type,
471 leftlineno=i + 1,
471 leftlineno=i + 1,
472 leftline=leftlines[i],
472 leftline=leftlines[i],
473 rightlineno=None,
473 rightlineno=None,
474 rightline=None)
474 rightline=None)
475 elif len2 > len1:
475 elif len2 > len1:
476 for i in xrange(rlo + count, rhi):
476 for i in xrange(rlo + count, rhi):
477 yield compline(type=type,
477 yield compline(type=type,
478 leftlineno=None,
478 leftlineno=None,
479 leftline=None,
479 leftline=None,
480 rightlineno=i + 1,
480 rightlineno=i + 1,
481 rightline=rightlines[i])
481 rightline=rightlines[i])
482
482
483 s = difflib.SequenceMatcher(None, leftlines, rightlines)
483 s = difflib.SequenceMatcher(None, leftlines, rightlines)
484 if context < 0:
484 if context < 0:
485 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
485 yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
486 else:
486 else:
487 for oc in s.get_grouped_opcodes(n=context):
487 for oc in s.get_grouped_opcodes(n=context):
488 yield tmpl('comparisonblock', lines=getblock(oc))
488 yield tmpl('comparisonblock', lines=getblock(oc))
489
489
490 def diffstatgen(ctx, basectx):
490 def diffstatgen(ctx, basectx):
491 '''Generator function that provides the diffstat data.'''
491 '''Generator function that provides the diffstat data.'''
492
492
493 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
493 stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
494 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
494 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
495 while True:
495 while True:
496 yield stats, maxname, maxtotal, addtotal, removetotal, binary
496 yield stats, maxname, maxtotal, addtotal, removetotal, binary
497
497
498 def diffsummary(statgen):
498 def diffsummary(statgen):
499 '''Return a short summary of the diff.'''
499 '''Return a short summary of the diff.'''
500
500
501 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
501 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
502 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
502 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
503 len(stats), addtotal, removetotal)
503 len(stats), addtotal, removetotal)
504
504
505 def diffstat(tmpl, ctx, statgen, parity):
505 def diffstat(tmpl, ctx, statgen, parity):
506 '''Return a diffstat template for each file in the diff.'''
506 '''Return a diffstat template for each file in the diff.'''
507
507
508 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
508 stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
509 files = ctx.files()
509 files = ctx.files()
510
510
511 def pct(i):
511 def pct(i):
512 if maxtotal == 0:
512 if maxtotal == 0:
513 return 0
513 return 0
514 return (float(i) / maxtotal) * 100
514 return (float(i) / maxtotal) * 100
515
515
516 fileno = 0
516 fileno = 0
517 for filename, adds, removes, isbinary in stats:
517 for filename, adds, removes, isbinary in stats:
518 template = filename in files and 'diffstatlink' or 'diffstatnolink'
518 template = filename in files and 'diffstatlink' or 'diffstatnolink'
519 total = adds + removes
519 total = adds + removes
520 fileno += 1
520 fileno += 1
521 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
521 yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
522 total=total, addpct=pct(adds), removepct=pct(removes),
522 total=total, addpct=pct(adds), removepct=pct(removes),
523 parity=parity.next())
523 parity=parity.next())
524
524
525 class sessionvars(object):
525 class sessionvars(object):
526 def __init__(self, vars, start='?'):
526 def __init__(self, vars, start='?'):
527 self.start = start
527 self.start = start
528 self.vars = vars
528 self.vars = vars
529 def __getitem__(self, key):
529 def __getitem__(self, key):
530 return self.vars[key]
530 return self.vars[key]
531 def __setitem__(self, key, value):
531 def __setitem__(self, key, value):
532 self.vars[key] = value
532 self.vars[key] = value
533 def __copy__(self):
533 def __copy__(self):
534 return sessionvars(copy.copy(self.vars), self.start)
534 return sessionvars(copy.copy(self.vars), self.start)
535 def __iter__(self):
535 def __iter__(self):
536 separator = self.start
536 separator = self.start
537 for key, value in sorted(self.vars.iteritems()):
537 for key, value in sorted(self.vars.iteritems()):
538 yield {'name': key, 'value': str(value), 'separator': separator}
538 yield {'name': key, 'value': str(value), 'separator': separator}
539 separator = '&'
539 separator = '&'
540
540
541 class wsgiui(uimod.ui):
541 class wsgiui(uimod.ui):
542 # default termwidth breaks under mod_wsgi
542 # default termwidth breaks under mod_wsgi
543 def termwidth(self):
543 def termwidth(self):
544 return 80
544 return 80
545
545
546 def getwebsubs(repo):
546 def getwebsubs(repo):
547 websubtable = []
547 websubtable = []
548 websubdefs = repo.ui.configitems('websub')
548 websubdefs = repo.ui.configitems('websub')
549 # we must maintain interhg backwards compatibility
549 # we must maintain interhg backwards compatibility
550 websubdefs += repo.ui.configitems('interhg')
550 websubdefs += repo.ui.configitems('interhg')
551 for key, pattern in websubdefs:
551 for key, pattern in websubdefs:
552 # grab the delimiter from the character after the "s"
552 # grab the delimiter from the character after the "s"
553 unesc = pattern[1]
553 unesc = pattern[1]
554 delim = re.escape(unesc)
554 delim = re.escape(unesc)
555
555
556 # identify portions of the pattern, taking care to avoid escaped
556 # identify portions of the pattern, taking care to avoid escaped
557 # delimiters. the replace format and flags are optional, but
557 # delimiters. the replace format and flags are optional, but
558 # delimiters are required.
558 # delimiters are required.
559 match = re.match(
559 match = re.match(
560 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
560 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
561 % (delim, delim, delim), pattern)
561 % (delim, delim, delim), pattern)
562 if not match:
562 if not match:
563 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
563 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
564 % (key, pattern))
564 % (key, pattern))
565 continue
565 continue
566
566
567 # we need to unescape the delimiter for regexp and format
567 # we need to unescape the delimiter for regexp and format
568 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
568 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
569 regexp = delim_re.sub(unesc, match.group(1))
569 regexp = delim_re.sub(unesc, match.group(1))
570 format = delim_re.sub(unesc, match.group(2))
570 format = delim_re.sub(unesc, match.group(2))
571
571
572 # the pattern allows for 6 regexp flags, so set them if necessary
572 # the pattern allows for 6 regexp flags, so set them if necessary
573 flagin = match.group(3)
573 flagin = match.group(3)
574 flags = 0
574 flags = 0
575 if flagin:
575 if flagin:
576 for flag in flagin.upper():
576 for flag in flagin.upper():
577 flags |= re.__dict__[flag]
577 flags |= re.__dict__[flag]
578
578
579 try:
579 try:
580 regexp = re.compile(regexp, flags)
580 regexp = re.compile(regexp, flags)
581 websubtable.append((regexp, format))
581 websubtable.append((regexp, format))
582 except re.error:
582 except re.error:
583 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
583 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
584 % (key, regexp))
584 % (key, regexp))
585 return websubtable
585 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now