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