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