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