##// END OF EJS Templates
hgweb: move archive related attributes to requestcontext...
Gregory Szorc -
r26136:6defc74f default
parent child Browse files
Show More
@@ -1,489 +1,486
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, re
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
63
64 class requestcontext(object):
64 class requestcontext(object):
65 """Holds state/context for an individual request.
65 """Holds state/context for an individual request.
66
66
67 Servers can be multi-threaded. Holding state on the WSGI application
67 Servers can be multi-threaded. Holding state on the WSGI application
68 is prone to race conditions. Instances of this class exist to hold
68 is prone to race conditions. Instances of this class exist to hold
69 mutable and race-free state for requests.
69 mutable and race-free state for requests.
70 """
70 """
71 def __init__(self, app):
71 def __init__(self, app):
72 object.__setattr__(self, 'app', app)
72 object.__setattr__(self, 'app', app)
73 object.__setattr__(self, 'repo', app.repo)
73 object.__setattr__(self, 'repo', app.repo)
74
74
75 object.__setattr__(self, 'archives', ('zip', 'gz', 'bz2'))
76
75 object.__setattr__(self, 'maxchanges',
77 object.__setattr__(self, 'maxchanges',
76 self.configint('web', 'maxchanges', 10))
78 self.configint('web', 'maxchanges', 10))
77 object.__setattr__(self, 'stripecount',
79 object.__setattr__(self, 'stripecount',
78 self.configint('web', 'stripes', 1))
80 self.configint('web', 'stripes', 1))
79 object.__setattr__(self, 'maxshortchanges',
81 object.__setattr__(self, 'maxshortchanges',
80 self.configint('web', 'maxshortchanges', 60))
82 self.configint('web', 'maxshortchanges', 60))
81 object.__setattr__(self, 'maxfiles',
83 object.__setattr__(self, 'maxfiles',
82 self.configint('web', 'maxfiles', 10))
84 self.configint('web', 'maxfiles', 10))
83 object.__setattr__(self, 'allowpull',
85 object.__setattr__(self, 'allowpull',
84 self.configbool('web', 'allowpull', True))
86 self.configbool('web', 'allowpull', True))
85
87
86 # Proxy unknown reads and writes to the application instance
88 # Proxy unknown reads and writes to the application instance
87 # until everything is moved to us.
89 # until everything is moved to us.
88 def __getattr__(self, name):
90 def __getattr__(self, name):
89 return getattr(self.app, name)
91 return getattr(self.app, name)
90
92
91 def __setattr__(self, name, value):
93 def __setattr__(self, name, value):
92 return setattr(self.app, name, value)
94 return setattr(self.app, name, value)
93
95
94 # Servers are often run by a user different from the repo owner.
96 # Servers are often run by a user different from the repo owner.
95 # Trust the settings from the .hg/hgrc files by default.
97 # Trust the settings from the .hg/hgrc files by default.
96 def config(self, section, name, default=None, untrusted=True):
98 def config(self, section, name, default=None, untrusted=True):
97 return self.repo.ui.config(section, name, default,
99 return self.repo.ui.config(section, name, default,
98 untrusted=untrusted)
100 untrusted=untrusted)
99
101
100 def configbool(self, section, name, default=False, untrusted=True):
102 def configbool(self, section, name, default=False, untrusted=True):
101 return self.repo.ui.configbool(section, name, default,
103 return self.repo.ui.configbool(section, name, default,
102 untrusted=untrusted)
104 untrusted=untrusted)
103
105
104 def configint(self, section, name, default=None, untrusted=True):
106 def configint(self, section, name, default=None, untrusted=True):
105 return self.repo.ui.configint(section, name, default,
107 return self.repo.ui.configint(section, name, default,
106 untrusted=untrusted)
108 untrusted=untrusted)
107
109
108 def configlist(self, section, name, default=None, untrusted=True):
110 def configlist(self, section, name, default=None, untrusted=True):
109 return self.repo.ui.configlist(section, name, default,
111 return self.repo.ui.configlist(section, name, default,
110 untrusted=untrusted)
112 untrusted=untrusted)
111
113
114 archivespecs = {
115 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
116 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
117 'zip': ('application/zip', 'zip', '.zip', None),
118 }
119
120 def archivelist(self, nodeid):
121 allowed = self.configlist('web', 'allow_archive')
122 for typ, spec in self.archivespecs.iteritems():
123 if typ in allowed or self.configbool('web', 'allow%s' % typ):
124 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
125
112 class hgweb(object):
126 class hgweb(object):
113 """HTTP server for individual repositories.
127 """HTTP server for individual repositories.
114
128
115 Instances of this class serve HTTP responses for a particular
129 Instances of this class serve HTTP responses for a particular
116 repository.
130 repository.
117
131
118 Instances are typically used as WSGI applications.
132 Instances are typically used as WSGI applications.
119
133
120 Some servers are multi-threaded. On these servers, there may
134 Some servers are multi-threaded. On these servers, there may
121 be multiple active threads inside __call__.
135 be multiple active threads inside __call__.
122 """
136 """
123 def __init__(self, repo, name=None, baseui=None):
137 def __init__(self, repo, name=None, baseui=None):
124 if isinstance(repo, str):
138 if isinstance(repo, str):
125 if baseui:
139 if baseui:
126 u = baseui.copy()
140 u = baseui.copy()
127 else:
141 else:
128 u = ui.ui()
142 u = ui.ui()
129 r = hg.repository(u, repo)
143 r = hg.repository(u, repo)
130 else:
144 else:
131 # we trust caller to give us a private copy
145 # we trust caller to give us a private copy
132 r = repo
146 r = repo
133
147
134 r = self._getview(r)
148 r = self._getview(r)
135 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
149 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
136 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
150 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
137 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
151 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
138 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
152 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
139 # displaying bundling progress bar while serving feel wrong and may
153 # displaying bundling progress bar while serving feel wrong and may
140 # break some wsgi implementation.
154 # break some wsgi implementation.
141 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
155 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
142 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
156 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
143 self.repo = r
157 self.repo = r
144 hook.redirect(True)
158 hook.redirect(True)
145 self.repostate = ((-1, -1), (-1, -1))
159 self.repostate = ((-1, -1), (-1, -1))
146 self.mtime = -1
160 self.mtime = -1
147 self.reponame = name
161 self.reponame = name
148 self.archives = 'zip', 'gz', 'bz2'
149 # a repo owner may set web.templates in .hg/hgrc to get any file
162 # a repo owner may set web.templates in .hg/hgrc to get any file
150 # readable by the user running the CGI script
163 # readable by the user running the CGI script
151 self.templatepath = self.config('web', 'templates')
164 self.templatepath = self.config('web', 'templates')
152 self.websubtable = self.loadwebsub()
165 self.websubtable = self.loadwebsub()
153
166
154 # 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.
155 # Trust the settings from the .hg/hgrc files by default.
168 # Trust the settings from the .hg/hgrc files by default.
156 def config(self, section, name, default=None, untrusted=True):
169 def config(self, section, name, default=None, untrusted=True):
157 return self.repo.ui.config(section, name, default,
170 return self.repo.ui.config(section, name, default,
158 untrusted=untrusted)
171 untrusted=untrusted)
159
172
160 def configbool(self, section, name, default=False, untrusted=True):
173 def configbool(self, section, name, default=False, untrusted=True):
161 return self.repo.ui.configbool(section, name, default,
174 return self.repo.ui.configbool(section, name, default,
162 untrusted=untrusted)
175 untrusted=untrusted)
163
176
164 def configlist(self, section, name, default=None, untrusted=True):
165 return self.repo.ui.configlist(section, name, default,
166 untrusted=untrusted)
167
168 def _getview(self, repo):
177 def _getview(self, repo):
169 """The 'web.view' config controls changeset filter to hgweb. Possible
178 """The 'web.view' config controls changeset filter to hgweb. Possible
170 values are ``served``, ``visible`` and ``all``. Default is ``served``.
179 values are ``served``, ``visible`` and ``all``. Default is ``served``.
171 The ``served`` filter only shows changesets that can be pulled from the
180 The ``served`` filter only shows changesets that can be pulled from the
172 hgweb instance. The``visible`` filter includes secret changesets but
181 hgweb instance. The``visible`` filter includes secret changesets but
173 still excludes "hidden" one.
182 still excludes "hidden" one.
174
183
175 See the repoview module for details.
184 See the repoview module for details.
176
185
177 The option has been around undocumented since Mercurial 2.5, but no
186 The option has been around undocumented since Mercurial 2.5, but no
178 user ever asked about it. So we better keep it undocumented for now."""
187 user ever asked about it. So we better keep it undocumented for now."""
179 viewconfig = repo.ui.config('web', 'view', 'served',
188 viewconfig = repo.ui.config('web', 'view', 'served',
180 untrusted=True)
189 untrusted=True)
181 if viewconfig == 'all':
190 if viewconfig == 'all':
182 return repo.unfiltered()
191 return repo.unfiltered()
183 elif viewconfig in repoview.filtertable:
192 elif viewconfig in repoview.filtertable:
184 return repo.filtered(viewconfig)
193 return repo.filtered(viewconfig)
185 else:
194 else:
186 return repo.filtered('served')
195 return repo.filtered('served')
187
196
188 def refresh(self, request):
197 def refresh(self, request):
189 repostate = []
198 repostate = []
190 # file of interrests mtime and size
199 # file of interrests mtime and size
191 for meth, fname in foi:
200 for meth, fname in foi:
192 prefix = getattr(self.repo, meth)
201 prefix = getattr(self.repo, meth)
193 st = get_stat(prefix, fname)
202 st = get_stat(prefix, fname)
194 repostate.append((st.st_mtime, st.st_size))
203 repostate.append((st.st_mtime, st.st_size))
195 repostate = tuple(repostate)
204 repostate = tuple(repostate)
196 # we need to compare file size in addition to mtime to catch
205 # we need to compare file size in addition to mtime to catch
197 # changes made less than a second ago
206 # changes made less than a second ago
198 if repostate != self.repostate:
207 if repostate != self.repostate:
199 r = hg.repository(self.repo.baseui, self.repo.url())
208 r = hg.repository(self.repo.baseui, self.repo.url())
200 self.repo = self._getview(r)
209 self.repo = self._getview(r)
201 encoding.encoding = self.config("web", "encoding",
210 encoding.encoding = self.config("web", "encoding",
202 encoding.encoding)
211 encoding.encoding)
203 # update these last to avoid threads seeing empty settings
212 # update these last to avoid threads seeing empty settings
204 self.repostate = repostate
213 self.repostate = repostate
205 # mtime is needed for ETag
214 # mtime is needed for ETag
206 self.mtime = st.st_mtime
215 self.mtime = st.st_mtime
207
216
208 self.repo.ui.environ = request.env
217 self.repo.ui.environ = request.env
209
218
210 def run(self):
219 def run(self):
211 """Start a server from CGI environment.
220 """Start a server from CGI environment.
212
221
213 Modern servers should be using WSGI and should avoid this
222 Modern servers should be using WSGI and should avoid this
214 method, if possible.
223 method, if possible.
215 """
224 """
216 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
225 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
217 raise RuntimeError("This function is only intended to be "
226 raise RuntimeError("This function is only intended to be "
218 "called while running as a CGI script.")
227 "called while running as a CGI script.")
219 import mercurial.hgweb.wsgicgi as wsgicgi
228 import mercurial.hgweb.wsgicgi as wsgicgi
220 wsgicgi.launch(self)
229 wsgicgi.launch(self)
221
230
222 def __call__(self, env, respond):
231 def __call__(self, env, respond):
223 """Run the WSGI application.
232 """Run the WSGI application.
224
233
225 This may be called by multiple threads.
234 This may be called by multiple threads.
226 """
235 """
227 req = wsgirequest(env, respond)
236 req = wsgirequest(env, respond)
228 return self.run_wsgi(req)
237 return self.run_wsgi(req)
229
238
230 def run_wsgi(self, req):
239 def run_wsgi(self, req):
231 """Internal method to run the WSGI application.
240 """Internal method to run the WSGI application.
232
241
233 This is typically only called by Mercurial. External consumers
242 This is typically only called by Mercurial. External consumers
234 should be using instances of this class as the WSGI application.
243 should be using instances of this class as the WSGI application.
235 """
244 """
236 self.refresh(req)
245 self.refresh(req)
237 rctx = requestcontext(self)
246 rctx = requestcontext(self)
238
247
239 # work with CGI variables to create coherent structure
248 # work with CGI variables to create coherent structure
240 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
249 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
241
250
242 req.url = req.env['SCRIPT_NAME']
251 req.url = req.env['SCRIPT_NAME']
243 if not req.url.endswith('/'):
252 if not req.url.endswith('/'):
244 req.url += '/'
253 req.url += '/'
245 if 'REPO_NAME' in req.env:
254 if 'REPO_NAME' in req.env:
246 req.url += req.env['REPO_NAME'] + '/'
255 req.url += req.env['REPO_NAME'] + '/'
247
256
248 if 'PATH_INFO' in req.env:
257 if 'PATH_INFO' in req.env:
249 parts = req.env['PATH_INFO'].strip('/').split('/')
258 parts = req.env['PATH_INFO'].strip('/').split('/')
250 repo_parts = req.env.get('REPO_NAME', '').split('/')
259 repo_parts = req.env.get('REPO_NAME', '').split('/')
251 if parts[:len(repo_parts)] == repo_parts:
260 if parts[:len(repo_parts)] == repo_parts:
252 parts = parts[len(repo_parts):]
261 parts = parts[len(repo_parts):]
253 query = '/'.join(parts)
262 query = '/'.join(parts)
254 else:
263 else:
255 query = req.env['QUERY_STRING'].split('&', 1)[0]
264 query = req.env['QUERY_STRING'].split('&', 1)[0]
256 query = query.split(';', 1)[0]
265 query = query.split(';', 1)[0]
257
266
258 # process this if it's a protocol request
267 # process this if it's a protocol request
259 # protocol bits don't need to create any URLs
268 # protocol bits don't need to create any URLs
260 # and the clients always use the old URL structure
269 # and the clients always use the old URL structure
261
270
262 cmd = req.form.get('cmd', [''])[0]
271 cmd = req.form.get('cmd', [''])[0]
263 if protocol.iscmd(cmd):
272 if protocol.iscmd(cmd):
264 try:
273 try:
265 if query:
274 if query:
266 raise ErrorResponse(HTTP_NOT_FOUND)
275 raise ErrorResponse(HTTP_NOT_FOUND)
267 if cmd in perms:
276 if cmd in perms:
268 self.check_perm(rctx, req, perms[cmd])
277 self.check_perm(rctx, req, perms[cmd])
269 return protocol.call(self.repo, req, cmd)
278 return protocol.call(self.repo, req, cmd)
270 except ErrorResponse as inst:
279 except ErrorResponse as inst:
271 # A client that sends unbundle without 100-continue will
280 # A client that sends unbundle without 100-continue will
272 # break if we respond early.
281 # break if we respond early.
273 if (cmd == 'unbundle' and
282 if (cmd == 'unbundle' and
274 (req.env.get('HTTP_EXPECT',
283 (req.env.get('HTTP_EXPECT',
275 '').lower() != '100-continue') or
284 '').lower() != '100-continue') or
276 req.env.get('X-HgHttp2', '')):
285 req.env.get('X-HgHttp2', '')):
277 req.drain()
286 req.drain()
278 else:
287 else:
279 req.headers.append(('Connection', 'Close'))
288 req.headers.append(('Connection', 'Close'))
280 req.respond(inst, protocol.HGTYPE,
289 req.respond(inst, protocol.HGTYPE,
281 body='0\n%s\n' % inst.message)
290 body='0\n%s\n' % inst.message)
282 return ''
291 return ''
283
292
284 # translate user-visible url structure to internal structure
293 # translate user-visible url structure to internal structure
285
294
286 args = query.split('/', 2)
295 args = query.split('/', 2)
287 if 'cmd' not in req.form and args and args[0]:
296 if 'cmd' not in req.form and args and args[0]:
288
297
289 cmd = args.pop(0)
298 cmd = args.pop(0)
290 style = cmd.rfind('-')
299 style = cmd.rfind('-')
291 if style != -1:
300 if style != -1:
292 req.form['style'] = [cmd[:style]]
301 req.form['style'] = [cmd[:style]]
293 cmd = cmd[style + 1:]
302 cmd = cmd[style + 1:]
294
303
295 # avoid accepting e.g. style parameter as command
304 # avoid accepting e.g. style parameter as command
296 if util.safehasattr(webcommands, cmd):
305 if util.safehasattr(webcommands, cmd):
297 req.form['cmd'] = [cmd]
306 req.form['cmd'] = [cmd]
298
307
299 if cmd == 'static':
308 if cmd == 'static':
300 req.form['file'] = ['/'.join(args)]
309 req.form['file'] = ['/'.join(args)]
301 else:
310 else:
302 if args and args[0]:
311 if args and args[0]:
303 node = args.pop(0).replace('%2F', '/')
312 node = args.pop(0).replace('%2F', '/')
304 req.form['node'] = [node]
313 req.form['node'] = [node]
305 if args:
314 if args:
306 req.form['file'] = args
315 req.form['file'] = args
307
316
308 ua = req.env.get('HTTP_USER_AGENT', '')
317 ua = req.env.get('HTTP_USER_AGENT', '')
309 if cmd == 'rev' and 'mercurial' in ua:
318 if cmd == 'rev' and 'mercurial' in ua:
310 req.form['style'] = ['raw']
319 req.form['style'] = ['raw']
311
320
312 if cmd == 'archive':
321 if cmd == 'archive':
313 fn = req.form['node'][0]
322 fn = req.form['node'][0]
314 for type_, spec in self.archive_specs.iteritems():
323 for type_, spec in rctx.archivespecs.iteritems():
315 ext = spec[2]
324 ext = spec[2]
316 if fn.endswith(ext):
325 if fn.endswith(ext):
317 req.form['node'] = [fn[:-len(ext)]]
326 req.form['node'] = [fn[:-len(ext)]]
318 req.form['type'] = [type_]
327 req.form['type'] = [type_]
319
328
320 # process the web interface request
329 # process the web interface request
321
330
322 try:
331 try:
323 tmpl = self.templater(req)
332 tmpl = self.templater(req)
324 ctype = tmpl('mimetype', encoding=encoding.encoding)
333 ctype = tmpl('mimetype', encoding=encoding.encoding)
325 ctype = templater.stringify(ctype)
334 ctype = templater.stringify(ctype)
326
335
327 # check read permissions non-static content
336 # check read permissions non-static content
328 if cmd != 'static':
337 if cmd != 'static':
329 self.check_perm(rctx, req, None)
338 self.check_perm(rctx, req, None)
330
339
331 if cmd == '':
340 if cmd == '':
332 req.form['cmd'] = [tmpl.cache['default']]
341 req.form['cmd'] = [tmpl.cache['default']]
333 cmd = req.form['cmd'][0]
342 cmd = req.form['cmd'][0]
334
343
335 if self.configbool('web', 'cache', True):
344 if self.configbool('web', 'cache', True):
336 caching(self, req) # sets ETag header or raises NOT_MODIFIED
345 caching(self, req) # sets ETag header or raises NOT_MODIFIED
337 if cmd not in webcommands.__all__:
346 if cmd not in webcommands.__all__:
338 msg = 'no such method: %s' % cmd
347 msg = 'no such method: %s' % cmd
339 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
348 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
340 elif cmd == 'file' and 'raw' in req.form.get('style', []):
349 elif cmd == 'file' and 'raw' in req.form.get('style', []):
341 self.ctype = ctype
350 self.ctype = ctype
342 content = webcommands.rawfile(rctx, req, tmpl)
351 content = webcommands.rawfile(rctx, req, tmpl)
343 else:
352 else:
344 content = getattr(webcommands, cmd)(rctx, req, tmpl)
353 content = getattr(webcommands, cmd)(rctx, req, tmpl)
345 req.respond(HTTP_OK, ctype)
354 req.respond(HTTP_OK, ctype)
346
355
347 return content
356 return content
348
357
349 except (error.LookupError, error.RepoLookupError) as err:
358 except (error.LookupError, error.RepoLookupError) as err:
350 req.respond(HTTP_NOT_FOUND, ctype)
359 req.respond(HTTP_NOT_FOUND, ctype)
351 msg = str(err)
360 msg = str(err)
352 if (util.safehasattr(err, 'name') and
361 if (util.safehasattr(err, 'name') and
353 not isinstance(err, error.ManifestLookupError)):
362 not isinstance(err, error.ManifestLookupError)):
354 msg = 'revision not found: %s' % err.name
363 msg = 'revision not found: %s' % err.name
355 return tmpl('error', error=msg)
364 return tmpl('error', error=msg)
356 except (error.RepoError, error.RevlogError) as inst:
365 except (error.RepoError, error.RevlogError) as inst:
357 req.respond(HTTP_SERVER_ERROR, ctype)
366 req.respond(HTTP_SERVER_ERROR, ctype)
358 return tmpl('error', error=str(inst))
367 return tmpl('error', error=str(inst))
359 except ErrorResponse as inst:
368 except ErrorResponse as inst:
360 req.respond(inst, ctype)
369 req.respond(inst, ctype)
361 if inst.code == HTTP_NOT_MODIFIED:
370 if inst.code == HTTP_NOT_MODIFIED:
362 # Not allowed to return a body on a 304
371 # Not allowed to return a body on a 304
363 return ['']
372 return ['']
364 return tmpl('error', error=inst.message)
373 return tmpl('error', error=inst.message)
365
374
366 def loadwebsub(self):
375 def loadwebsub(self):
367 websubtable = []
376 websubtable = []
368 websubdefs = self.repo.ui.configitems('websub')
377 websubdefs = self.repo.ui.configitems('websub')
369 # we must maintain interhg backwards compatibility
378 # we must maintain interhg backwards compatibility
370 websubdefs += self.repo.ui.configitems('interhg')
379 websubdefs += self.repo.ui.configitems('interhg')
371 for key, pattern in websubdefs:
380 for key, pattern in websubdefs:
372 # grab the delimiter from the character after the "s"
381 # grab the delimiter from the character after the "s"
373 unesc = pattern[1]
382 unesc = pattern[1]
374 delim = re.escape(unesc)
383 delim = re.escape(unesc)
375
384
376 # identify portions of the pattern, taking care to avoid escaped
385 # identify portions of the pattern, taking care to avoid escaped
377 # delimiters. the replace format and flags are optional, but
386 # delimiters. the replace format and flags are optional, but
378 # delimiters are required.
387 # delimiters are required.
379 match = re.match(
388 match = re.match(
380 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
389 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
381 % (delim, delim, delim), pattern)
390 % (delim, delim, delim), pattern)
382 if not match:
391 if not match:
383 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
392 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
384 % (key, pattern))
393 % (key, pattern))
385 continue
394 continue
386
395
387 # we need to unescape the delimiter for regexp and format
396 # we need to unescape the delimiter for regexp and format
388 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
397 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
389 regexp = delim_re.sub(unesc, match.group(1))
398 regexp = delim_re.sub(unesc, match.group(1))
390 format = delim_re.sub(unesc, match.group(2))
399 format = delim_re.sub(unesc, match.group(2))
391
400
392 # the pattern allows for 6 regexp flags, so set them if necessary
401 # the pattern allows for 6 regexp flags, so set them if necessary
393 flagin = match.group(3)
402 flagin = match.group(3)
394 flags = 0
403 flags = 0
395 if flagin:
404 if flagin:
396 for flag in flagin.upper():
405 for flag in flagin.upper():
397 flags |= re.__dict__[flag]
406 flags |= re.__dict__[flag]
398
407
399 try:
408 try:
400 regexp = re.compile(regexp, flags)
409 regexp = re.compile(regexp, flags)
401 websubtable.append((regexp, format))
410 websubtable.append((regexp, format))
402 except re.error:
411 except re.error:
403 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
412 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
404 % (key, regexp))
413 % (key, regexp))
405 return websubtable
414 return websubtable
406
415
407 def templater(self, req):
416 def templater(self, req):
408
417
409 # determine scheme, port and server name
418 # determine scheme, port and server name
410 # this is needed to create absolute urls
419 # this is needed to create absolute urls
411
420
412 proto = req.env.get('wsgi.url_scheme')
421 proto = req.env.get('wsgi.url_scheme')
413 if proto == 'https':
422 if proto == 'https':
414 proto = 'https'
423 proto = 'https'
415 default_port = "443"
424 default_port = "443"
416 else:
425 else:
417 proto = 'http'
426 proto = 'http'
418 default_port = "80"
427 default_port = "80"
419
428
420 port = req.env["SERVER_PORT"]
429 port = req.env["SERVER_PORT"]
421 port = port != default_port and (":" + port) or ""
430 port = port != default_port and (":" + port) or ""
422 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
431 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
423 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
432 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
424 logoimg = self.config("web", "logoimg", "hglogo.png")
433 logoimg = self.config("web", "logoimg", "hglogo.png")
425 staticurl = self.config("web", "staticurl") or req.url + 'static/'
434 staticurl = self.config("web", "staticurl") or req.url + 'static/'
426 if not staticurl.endswith('/'):
435 if not staticurl.endswith('/'):
427 staticurl += '/'
436 staticurl += '/'
428
437
429 # some functions for the templater
438 # some functions for the templater
430
439
431 def motd(**map):
440 def motd(**map):
432 yield self.config("web", "motd", "")
441 yield self.config("web", "motd", "")
433
442
434 # figure out which style to use
443 # figure out which style to use
435
444
436 vars = {}
445 vars = {}
437 styles = (
446 styles = (
438 req.form.get('style', [None])[0],
447 req.form.get('style', [None])[0],
439 self.config('web', 'style'),
448 self.config('web', 'style'),
440 'paper',
449 'paper',
441 )
450 )
442 style, mapfile = templater.stylemap(styles, self.templatepath)
451 style, mapfile = templater.stylemap(styles, self.templatepath)
443 if style == styles[0]:
452 if style == styles[0]:
444 vars['style'] = style
453 vars['style'] = style
445
454
446 start = req.url[-1] == '?' and '&' or '?'
455 start = req.url[-1] == '?' and '&' or '?'
447 sessionvars = webutil.sessionvars(vars, start)
456 sessionvars = webutil.sessionvars(vars, start)
448
457
449 if not self.reponame:
458 if not self.reponame:
450 self.reponame = (self.config("web", "name")
459 self.reponame = (self.config("web", "name")
451 or req.env.get('REPO_NAME')
460 or req.env.get('REPO_NAME')
452 or req.url.strip('/') or self.repo.root)
461 or req.url.strip('/') or self.repo.root)
453
462
454 def websubfilter(text):
463 def websubfilter(text):
455 return websub(text, self.websubtable)
464 return websub(text, self.websubtable)
456
465
457 # create the templater
466 # create the templater
458
467
459 tmpl = templater.templater(mapfile,
468 tmpl = templater.templater(mapfile,
460 filters={"websub": websubfilter},
469 filters={"websub": websubfilter},
461 defaults={"url": req.url,
470 defaults={"url": req.url,
462 "logourl": logourl,
471 "logourl": logourl,
463 "logoimg": logoimg,
472 "logoimg": logoimg,
464 "staticurl": staticurl,
473 "staticurl": staticurl,
465 "urlbase": urlbase,
474 "urlbase": urlbase,
466 "repo": self.reponame,
475 "repo": self.reponame,
467 "encoding": encoding.encoding,
476 "encoding": encoding.encoding,
468 "motd": motd,
477 "motd": motd,
469 "sessionvars": sessionvars,
478 "sessionvars": sessionvars,
470 "pathdef": makebreadcrumb(req.url),
479 "pathdef": makebreadcrumb(req.url),
471 "style": style,
480 "style": style,
472 })
481 })
473 return tmpl
482 return tmpl
474
483
475 def archivelist(self, nodeid):
476 allowed = self.configlist("web", "allow_archive")
477 for i, spec in self.archive_specs.iteritems():
478 if i in allowed or self.configbool("web", "allow" + i):
479 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
480
481 archive_specs = {
482 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
483 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
484 'zip': ('application/zip', 'zip', '.zip', None),
485 }
486
487 def check_perm(self, rctx, req, op):
484 def check_perm(self, rctx, req, op):
488 for permhook in permhooks:
485 for permhook in permhooks:
489 permhook(rctx, req, op)
486 permhook(rctx, req, op)
@@ -1,1315 +1,1315
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import os, mimetypes, re, cgi, copy
8 import os, mimetypes, re, cgi, copy
9 import webutil
9 import webutil
10 from mercurial import error, encoding, archival, templater, templatefilters
10 from mercurial import error, encoding, archival, templater, templatefilters
11 from mercurial.node import short, hex
11 from mercurial.node import short, hex
12 from mercurial import util
12 from mercurial import util
13 from common import paritygen, staticfile, get_contact, ErrorResponse
13 from common import paritygen, staticfile, get_contact, ErrorResponse
14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
15 from mercurial import graphmod, patch
15 from mercurial import graphmod, patch
16 from mercurial import scmutil
16 from mercurial import scmutil
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18 from mercurial.error import ParseError, RepoLookupError, Abort
18 from mercurial.error import ParseError, RepoLookupError, Abort
19 from mercurial import revset
19 from mercurial import revset
20
20
21 __all__ = []
21 __all__ = []
22 commands = {}
22 commands = {}
23
23
24 class webcommand(object):
24 class webcommand(object):
25 """Decorator used to register a web command handler.
25 """Decorator used to register a web command handler.
26
26
27 The decorator takes as its positional arguments the name/path the
27 The decorator takes as its positional arguments the name/path the
28 command should be accessible under.
28 command should be accessible under.
29
29
30 Usage:
30 Usage:
31
31
32 @webcommand('mycommand')
32 @webcommand('mycommand')
33 def mycommand(web, req, tmpl):
33 def mycommand(web, req, tmpl):
34 pass
34 pass
35 """
35 """
36
36
37 def __init__(self, name):
37 def __init__(self, name):
38 self.name = name
38 self.name = name
39
39
40 def __call__(self, func):
40 def __call__(self, func):
41 __all__.append(self.name)
41 __all__.append(self.name)
42 commands[self.name] = func
42 commands[self.name] = func
43 return func
43 return func
44
44
45 @webcommand('log')
45 @webcommand('log')
46 def log(web, req, tmpl):
46 def log(web, req, tmpl):
47 """
47 """
48 /log[/{revision}[/{path}]]
48 /log[/{revision}[/{path}]]
49 --------------------------
49 --------------------------
50
50
51 Show repository or file history.
51 Show repository or file history.
52
52
53 For URLs of the form ``/log/{revision}``, a list of changesets starting at
53 For URLs of the form ``/log/{revision}``, a list of changesets starting at
54 the specified changeset identifier is shown. If ``{revision}`` is not
54 the specified changeset identifier is shown. If ``{revision}`` is not
55 defined, the default is ``tip``. This form is equivalent to the
55 defined, the default is ``tip``. This form is equivalent to the
56 ``changelog`` handler.
56 ``changelog`` handler.
57
57
58 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
58 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
59 file will be shown. This form is equivalent to the ``filelog`` handler.
59 file will be shown. This form is equivalent to the ``filelog`` handler.
60 """
60 """
61
61
62 if 'file' in req.form and req.form['file'][0]:
62 if 'file' in req.form and req.form['file'][0]:
63 return filelog(web, req, tmpl)
63 return filelog(web, req, tmpl)
64 else:
64 else:
65 return changelog(web, req, tmpl)
65 return changelog(web, req, tmpl)
66
66
67 @webcommand('rawfile')
67 @webcommand('rawfile')
68 def rawfile(web, req, tmpl):
68 def rawfile(web, req, tmpl):
69 guessmime = web.configbool('web', 'guessmime', False)
69 guessmime = web.configbool('web', 'guessmime', False)
70
70
71 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
71 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
72 if not path:
72 if not path:
73 content = manifest(web, req, tmpl)
73 content = manifest(web, req, tmpl)
74 req.respond(HTTP_OK, web.ctype)
74 req.respond(HTTP_OK, web.ctype)
75 return content
75 return content
76
76
77 try:
77 try:
78 fctx = webutil.filectx(web.repo, req)
78 fctx = webutil.filectx(web.repo, req)
79 except error.LookupError as inst:
79 except error.LookupError as inst:
80 try:
80 try:
81 content = manifest(web, req, tmpl)
81 content = manifest(web, req, tmpl)
82 req.respond(HTTP_OK, web.ctype)
82 req.respond(HTTP_OK, web.ctype)
83 return content
83 return content
84 except ErrorResponse:
84 except ErrorResponse:
85 raise inst
85 raise inst
86
86
87 path = fctx.path()
87 path = fctx.path()
88 text = fctx.data()
88 text = fctx.data()
89 mt = 'application/binary'
89 mt = 'application/binary'
90 if guessmime:
90 if guessmime:
91 mt = mimetypes.guess_type(path)[0]
91 mt = mimetypes.guess_type(path)[0]
92 if mt is None:
92 if mt is None:
93 if util.binary(text):
93 if util.binary(text):
94 mt = 'application/binary'
94 mt = 'application/binary'
95 else:
95 else:
96 mt = 'text/plain'
96 mt = 'text/plain'
97 if mt.startswith('text/'):
97 if mt.startswith('text/'):
98 mt += '; charset="%s"' % encoding.encoding
98 mt += '; charset="%s"' % encoding.encoding
99
99
100 req.respond(HTTP_OK, mt, path, body=text)
100 req.respond(HTTP_OK, mt, path, body=text)
101 return []
101 return []
102
102
103 def _filerevision(web, req, tmpl, fctx):
103 def _filerevision(web, req, tmpl, fctx):
104 f = fctx.path()
104 f = fctx.path()
105 text = fctx.data()
105 text = fctx.data()
106 parity = paritygen(web.stripecount)
106 parity = paritygen(web.stripecount)
107
107
108 if util.binary(text):
108 if util.binary(text):
109 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
109 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
110 text = '(binary:%s)' % mt
110 text = '(binary:%s)' % mt
111
111
112 def lines():
112 def lines():
113 for lineno, t in enumerate(text.splitlines(True)):
113 for lineno, t in enumerate(text.splitlines(True)):
114 yield {"line": t,
114 yield {"line": t,
115 "lineid": "l%d" % (lineno + 1),
115 "lineid": "l%d" % (lineno + 1),
116 "linenumber": "% 6d" % (lineno + 1),
116 "linenumber": "% 6d" % (lineno + 1),
117 "parity": parity.next()}
117 "parity": parity.next()}
118
118
119 return tmpl("filerevision",
119 return tmpl("filerevision",
120 file=f,
120 file=f,
121 path=webutil.up(f),
121 path=webutil.up(f),
122 text=lines(),
122 text=lines(),
123 rev=fctx.rev(),
123 rev=fctx.rev(),
124 symrev=webutil.symrevorshortnode(req, fctx),
124 symrev=webutil.symrevorshortnode(req, fctx),
125 node=fctx.hex(),
125 node=fctx.hex(),
126 author=fctx.user(),
126 author=fctx.user(),
127 date=fctx.date(),
127 date=fctx.date(),
128 desc=fctx.description(),
128 desc=fctx.description(),
129 extra=fctx.extra(),
129 extra=fctx.extra(),
130 branch=webutil.nodebranchnodefault(fctx),
130 branch=webutil.nodebranchnodefault(fctx),
131 parent=webutil.parents(fctx),
131 parent=webutil.parents(fctx),
132 child=webutil.children(fctx),
132 child=webutil.children(fctx),
133 rename=webutil.renamelink(fctx),
133 rename=webutil.renamelink(fctx),
134 tags=webutil.nodetagsdict(web.repo, fctx.node()),
134 tags=webutil.nodetagsdict(web.repo, fctx.node()),
135 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
135 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
136 permissions=fctx.manifest().flags(f))
136 permissions=fctx.manifest().flags(f))
137
137
138 @webcommand('file')
138 @webcommand('file')
139 def file(web, req, tmpl):
139 def file(web, req, tmpl):
140 """
140 """
141 /file/{revision}[/{path}]
141 /file/{revision}[/{path}]
142 -------------------------
142 -------------------------
143
143
144 Show information about a directory or file in the repository.
144 Show information about a directory or file in the repository.
145
145
146 Info about the ``path`` given as a URL parameter will be rendered.
146 Info about the ``path`` given as a URL parameter will be rendered.
147
147
148 If ``path`` is a directory, information about the entries in that
148 If ``path`` is a directory, information about the entries in that
149 directory will be rendered. This form is equivalent to the ``manifest``
149 directory will be rendered. This form is equivalent to the ``manifest``
150 handler.
150 handler.
151
151
152 If ``path`` is a file, information about that file will be shown via
152 If ``path`` is a file, information about that file will be shown via
153 the ``filerevision`` template.
153 the ``filerevision`` template.
154
154
155 If ``path`` is not defined, information about the root directory will
155 If ``path`` is not defined, information about the root directory will
156 be rendered.
156 be rendered.
157 """
157 """
158 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
158 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
159 if not path:
159 if not path:
160 return manifest(web, req, tmpl)
160 return manifest(web, req, tmpl)
161 try:
161 try:
162 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
162 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
163 except error.LookupError as inst:
163 except error.LookupError as inst:
164 try:
164 try:
165 return manifest(web, req, tmpl)
165 return manifest(web, req, tmpl)
166 except ErrorResponse:
166 except ErrorResponse:
167 raise inst
167 raise inst
168
168
169 def _search(web, req, tmpl):
169 def _search(web, req, tmpl):
170 MODE_REVISION = 'rev'
170 MODE_REVISION = 'rev'
171 MODE_KEYWORD = 'keyword'
171 MODE_KEYWORD = 'keyword'
172 MODE_REVSET = 'revset'
172 MODE_REVSET = 'revset'
173
173
174 def revsearch(ctx):
174 def revsearch(ctx):
175 yield ctx
175 yield ctx
176
176
177 def keywordsearch(query):
177 def keywordsearch(query):
178 lower = encoding.lower
178 lower = encoding.lower
179 qw = lower(query).split()
179 qw = lower(query).split()
180
180
181 def revgen():
181 def revgen():
182 cl = web.repo.changelog
182 cl = web.repo.changelog
183 for i in xrange(len(web.repo) - 1, 0, -100):
183 for i in xrange(len(web.repo) - 1, 0, -100):
184 l = []
184 l = []
185 for j in cl.revs(max(0, i - 99), i):
185 for j in cl.revs(max(0, i - 99), i):
186 ctx = web.repo[j]
186 ctx = web.repo[j]
187 l.append(ctx)
187 l.append(ctx)
188 l.reverse()
188 l.reverse()
189 for e in l:
189 for e in l:
190 yield e
190 yield e
191
191
192 for ctx in revgen():
192 for ctx in revgen():
193 miss = 0
193 miss = 0
194 for q in qw:
194 for q in qw:
195 if not (q in lower(ctx.user()) or
195 if not (q in lower(ctx.user()) or
196 q in lower(ctx.description()) or
196 q in lower(ctx.description()) or
197 q in lower(" ".join(ctx.files()))):
197 q in lower(" ".join(ctx.files()))):
198 miss = 1
198 miss = 1
199 break
199 break
200 if miss:
200 if miss:
201 continue
201 continue
202
202
203 yield ctx
203 yield ctx
204
204
205 def revsetsearch(revs):
205 def revsetsearch(revs):
206 for r in revs:
206 for r in revs:
207 yield web.repo[r]
207 yield web.repo[r]
208
208
209 searchfuncs = {
209 searchfuncs = {
210 MODE_REVISION: (revsearch, 'exact revision search'),
210 MODE_REVISION: (revsearch, 'exact revision search'),
211 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
211 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
212 MODE_REVSET: (revsetsearch, 'revset expression search'),
212 MODE_REVSET: (revsetsearch, 'revset expression search'),
213 }
213 }
214
214
215 def getsearchmode(query):
215 def getsearchmode(query):
216 try:
216 try:
217 ctx = web.repo[query]
217 ctx = web.repo[query]
218 except (error.RepoError, error.LookupError):
218 except (error.RepoError, error.LookupError):
219 # query is not an exact revision pointer, need to
219 # query is not an exact revision pointer, need to
220 # decide if it's a revset expression or keywords
220 # decide if it's a revset expression or keywords
221 pass
221 pass
222 else:
222 else:
223 return MODE_REVISION, ctx
223 return MODE_REVISION, ctx
224
224
225 revdef = 'reverse(%s)' % query
225 revdef = 'reverse(%s)' % query
226 try:
226 try:
227 tree = revset.parse(revdef)
227 tree = revset.parse(revdef)
228 except ParseError:
228 except ParseError:
229 # can't parse to a revset tree
229 # can't parse to a revset tree
230 return MODE_KEYWORD, query
230 return MODE_KEYWORD, query
231
231
232 if revset.depth(tree) <= 2:
232 if revset.depth(tree) <= 2:
233 # no revset syntax used
233 # no revset syntax used
234 return MODE_KEYWORD, query
234 return MODE_KEYWORD, query
235
235
236 if any((token, (value or '')[:3]) == ('string', 're:')
236 if any((token, (value or '')[:3]) == ('string', 're:')
237 for token, value, pos in revset.tokenize(revdef)):
237 for token, value, pos in revset.tokenize(revdef)):
238 return MODE_KEYWORD, query
238 return MODE_KEYWORD, query
239
239
240 funcsused = revset.funcsused(tree)
240 funcsused = revset.funcsused(tree)
241 if not funcsused.issubset(revset.safesymbols):
241 if not funcsused.issubset(revset.safesymbols):
242 return MODE_KEYWORD, query
242 return MODE_KEYWORD, query
243
243
244 mfunc = revset.match(web.repo.ui, revdef)
244 mfunc = revset.match(web.repo.ui, revdef)
245 try:
245 try:
246 revs = mfunc(web.repo)
246 revs = mfunc(web.repo)
247 return MODE_REVSET, revs
247 return MODE_REVSET, revs
248 # ParseError: wrongly placed tokens, wrongs arguments, etc
248 # ParseError: wrongly placed tokens, wrongs arguments, etc
249 # RepoLookupError: no such revision, e.g. in 'revision:'
249 # RepoLookupError: no such revision, e.g. in 'revision:'
250 # Abort: bookmark/tag not exists
250 # Abort: bookmark/tag not exists
251 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
251 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
252 except (ParseError, RepoLookupError, Abort, LookupError):
252 except (ParseError, RepoLookupError, Abort, LookupError):
253 return MODE_KEYWORD, query
253 return MODE_KEYWORD, query
254
254
255 def changelist(**map):
255 def changelist(**map):
256 count = 0
256 count = 0
257
257
258 for ctx in searchfunc[0](funcarg):
258 for ctx in searchfunc[0](funcarg):
259 count += 1
259 count += 1
260 n = ctx.node()
260 n = ctx.node()
261 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
261 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
262 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
262 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
263
263
264 yield tmpl('searchentry',
264 yield tmpl('searchentry',
265 parity=parity.next(),
265 parity=parity.next(),
266 author=ctx.user(),
266 author=ctx.user(),
267 parent=webutil.parents(ctx),
267 parent=webutil.parents(ctx),
268 child=webutil.children(ctx),
268 child=webutil.children(ctx),
269 changelogtag=showtags,
269 changelogtag=showtags,
270 desc=ctx.description(),
270 desc=ctx.description(),
271 extra=ctx.extra(),
271 extra=ctx.extra(),
272 date=ctx.date(),
272 date=ctx.date(),
273 files=files,
273 files=files,
274 rev=ctx.rev(),
274 rev=ctx.rev(),
275 node=hex(n),
275 node=hex(n),
276 tags=webutil.nodetagsdict(web.repo, n),
276 tags=webutil.nodetagsdict(web.repo, n),
277 bookmarks=webutil.nodebookmarksdict(web.repo, n),
277 bookmarks=webutil.nodebookmarksdict(web.repo, n),
278 inbranch=webutil.nodeinbranch(web.repo, ctx),
278 inbranch=webutil.nodeinbranch(web.repo, ctx),
279 branches=webutil.nodebranchdict(web.repo, ctx))
279 branches=webutil.nodebranchdict(web.repo, ctx))
280
280
281 if count >= revcount:
281 if count >= revcount:
282 break
282 break
283
283
284 query = req.form['rev'][0]
284 query = req.form['rev'][0]
285 revcount = web.maxchanges
285 revcount = web.maxchanges
286 if 'revcount' in req.form:
286 if 'revcount' in req.form:
287 try:
287 try:
288 revcount = int(req.form.get('revcount', [revcount])[0])
288 revcount = int(req.form.get('revcount', [revcount])[0])
289 revcount = max(revcount, 1)
289 revcount = max(revcount, 1)
290 tmpl.defaults['sessionvars']['revcount'] = revcount
290 tmpl.defaults['sessionvars']['revcount'] = revcount
291 except ValueError:
291 except ValueError:
292 pass
292 pass
293
293
294 lessvars = copy.copy(tmpl.defaults['sessionvars'])
294 lessvars = copy.copy(tmpl.defaults['sessionvars'])
295 lessvars['revcount'] = max(revcount / 2, 1)
295 lessvars['revcount'] = max(revcount / 2, 1)
296 lessvars['rev'] = query
296 lessvars['rev'] = query
297 morevars = copy.copy(tmpl.defaults['sessionvars'])
297 morevars = copy.copy(tmpl.defaults['sessionvars'])
298 morevars['revcount'] = revcount * 2
298 morevars['revcount'] = revcount * 2
299 morevars['rev'] = query
299 morevars['rev'] = query
300
300
301 mode, funcarg = getsearchmode(query)
301 mode, funcarg = getsearchmode(query)
302
302
303 if 'forcekw' in req.form:
303 if 'forcekw' in req.form:
304 showforcekw = ''
304 showforcekw = ''
305 showunforcekw = searchfuncs[mode][1]
305 showunforcekw = searchfuncs[mode][1]
306 mode = MODE_KEYWORD
306 mode = MODE_KEYWORD
307 funcarg = query
307 funcarg = query
308 else:
308 else:
309 if mode != MODE_KEYWORD:
309 if mode != MODE_KEYWORD:
310 showforcekw = searchfuncs[MODE_KEYWORD][1]
310 showforcekw = searchfuncs[MODE_KEYWORD][1]
311 else:
311 else:
312 showforcekw = ''
312 showforcekw = ''
313 showunforcekw = ''
313 showunforcekw = ''
314
314
315 searchfunc = searchfuncs[mode]
315 searchfunc = searchfuncs[mode]
316
316
317 tip = web.repo['tip']
317 tip = web.repo['tip']
318 parity = paritygen(web.stripecount)
318 parity = paritygen(web.stripecount)
319
319
320 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
320 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
321 entries=changelist, archives=web.archivelist("tip"),
321 entries=changelist, archives=web.archivelist("tip"),
322 morevars=morevars, lessvars=lessvars,
322 morevars=morevars, lessvars=lessvars,
323 modedesc=searchfunc[1],
323 modedesc=searchfunc[1],
324 showforcekw=showforcekw, showunforcekw=showunforcekw)
324 showforcekw=showforcekw, showunforcekw=showunforcekw)
325
325
326 @webcommand('changelog')
326 @webcommand('changelog')
327 def changelog(web, req, tmpl, shortlog=False):
327 def changelog(web, req, tmpl, shortlog=False):
328 """
328 """
329 /changelog[/{revision}]
329 /changelog[/{revision}]
330 -----------------------
330 -----------------------
331
331
332 Show information about multiple changesets.
332 Show information about multiple changesets.
333
333
334 If the optional ``revision`` URL argument is absent, information about
334 If the optional ``revision`` URL argument is absent, information about
335 all changesets starting at ``tip`` will be rendered. If the ``revision``
335 all changesets starting at ``tip`` will be rendered. If the ``revision``
336 argument is present, changesets will be shown starting from the specified
336 argument is present, changesets will be shown starting from the specified
337 revision.
337 revision.
338
338
339 If ``revision`` is absent, the ``rev`` query string argument may be
339 If ``revision`` is absent, the ``rev`` query string argument may be
340 defined. This will perform a search for changesets.
340 defined. This will perform a search for changesets.
341
341
342 The argument for ``rev`` can be a single revision, a revision set,
342 The argument for ``rev`` can be a single revision, a revision set,
343 or a literal keyword to search for in changeset data (equivalent to
343 or a literal keyword to search for in changeset data (equivalent to
344 :hg:`log -k`).
344 :hg:`log -k`).
345
345
346 The ``revcount`` query string argument defines the maximum numbers of
346 The ``revcount`` query string argument defines the maximum numbers of
347 changesets to render.
347 changesets to render.
348
348
349 For non-searches, the ``changelog`` template will be rendered.
349 For non-searches, the ``changelog`` template will be rendered.
350 """
350 """
351
351
352 query = ''
352 query = ''
353 if 'node' in req.form:
353 if 'node' in req.form:
354 ctx = webutil.changectx(web.repo, req)
354 ctx = webutil.changectx(web.repo, req)
355 symrev = webutil.symrevorshortnode(req, ctx)
355 symrev = webutil.symrevorshortnode(req, ctx)
356 elif 'rev' in req.form:
356 elif 'rev' in req.form:
357 return _search(web, req, tmpl)
357 return _search(web, req, tmpl)
358 else:
358 else:
359 ctx = web.repo['tip']
359 ctx = web.repo['tip']
360 symrev = 'tip'
360 symrev = 'tip'
361
361
362 def changelist():
362 def changelist():
363 revs = []
363 revs = []
364 if pos != -1:
364 if pos != -1:
365 revs = web.repo.changelog.revs(pos, 0)
365 revs = web.repo.changelog.revs(pos, 0)
366 curcount = 0
366 curcount = 0
367 for rev in revs:
367 for rev in revs:
368 curcount += 1
368 curcount += 1
369 if curcount > revcount + 1:
369 if curcount > revcount + 1:
370 break
370 break
371
371
372 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
372 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
373 entry['parity'] = parity.next()
373 entry['parity'] = parity.next()
374 yield entry
374 yield entry
375
375
376 if shortlog:
376 if shortlog:
377 revcount = web.maxshortchanges
377 revcount = web.maxshortchanges
378 else:
378 else:
379 revcount = web.maxchanges
379 revcount = web.maxchanges
380
380
381 if 'revcount' in req.form:
381 if 'revcount' in req.form:
382 try:
382 try:
383 revcount = int(req.form.get('revcount', [revcount])[0])
383 revcount = int(req.form.get('revcount', [revcount])[0])
384 revcount = max(revcount, 1)
384 revcount = max(revcount, 1)
385 tmpl.defaults['sessionvars']['revcount'] = revcount
385 tmpl.defaults['sessionvars']['revcount'] = revcount
386 except ValueError:
386 except ValueError:
387 pass
387 pass
388
388
389 lessvars = copy.copy(tmpl.defaults['sessionvars'])
389 lessvars = copy.copy(tmpl.defaults['sessionvars'])
390 lessvars['revcount'] = max(revcount / 2, 1)
390 lessvars['revcount'] = max(revcount / 2, 1)
391 morevars = copy.copy(tmpl.defaults['sessionvars'])
391 morevars = copy.copy(tmpl.defaults['sessionvars'])
392 morevars['revcount'] = revcount * 2
392 morevars['revcount'] = revcount * 2
393
393
394 count = len(web.repo)
394 count = len(web.repo)
395 pos = ctx.rev()
395 pos = ctx.rev()
396 parity = paritygen(web.stripecount)
396 parity = paritygen(web.stripecount)
397
397
398 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
398 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
399
399
400 entries = list(changelist())
400 entries = list(changelist())
401 latestentry = entries[:1]
401 latestentry = entries[:1]
402 if len(entries) > revcount:
402 if len(entries) > revcount:
403 nextentry = entries[-1:]
403 nextentry = entries[-1:]
404 entries = entries[:-1]
404 entries = entries[:-1]
405 else:
405 else:
406 nextentry = []
406 nextentry = []
407
407
408 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
408 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
409 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
409 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
410 entries=entries,
410 entries=entries,
411 latestentry=latestentry, nextentry=nextentry,
411 latestentry=latestentry, nextentry=nextentry,
412 archives=web.archivelist("tip"), revcount=revcount,
412 archives=web.archivelist("tip"), revcount=revcount,
413 morevars=morevars, lessvars=lessvars, query=query)
413 morevars=morevars, lessvars=lessvars, query=query)
414
414
415 @webcommand('shortlog')
415 @webcommand('shortlog')
416 def shortlog(web, req, tmpl):
416 def shortlog(web, req, tmpl):
417 """
417 """
418 /shortlog
418 /shortlog
419 ---------
419 ---------
420
420
421 Show basic information about a set of changesets.
421 Show basic information about a set of changesets.
422
422
423 This accepts the same parameters as the ``changelog`` handler. The only
423 This accepts the same parameters as the ``changelog`` handler. The only
424 difference is the ``shortlog`` template will be rendered instead of the
424 difference is the ``shortlog`` template will be rendered instead of the
425 ``changelog`` template.
425 ``changelog`` template.
426 """
426 """
427 return changelog(web, req, tmpl, shortlog=True)
427 return changelog(web, req, tmpl, shortlog=True)
428
428
429 @webcommand('changeset')
429 @webcommand('changeset')
430 def changeset(web, req, tmpl):
430 def changeset(web, req, tmpl):
431 """
431 """
432 /changeset[/{revision}]
432 /changeset[/{revision}]
433 -----------------------
433 -----------------------
434
434
435 Show information about a single changeset.
435 Show information about a single changeset.
436
436
437 A URL path argument is the changeset identifier to show. See ``hg help
437 A URL path argument is the changeset identifier to show. See ``hg help
438 revisions`` for possible values. If not defined, the ``tip`` changeset
438 revisions`` for possible values. If not defined, the ``tip`` changeset
439 will be shown.
439 will be shown.
440
440
441 The ``changeset`` template is rendered. Contents of the ``changesettag``,
441 The ``changeset`` template is rendered. Contents of the ``changesettag``,
442 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
442 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
443 templates related to diffs may all be used to produce the output.
443 templates related to diffs may all be used to produce the output.
444 """
444 """
445 ctx = webutil.changectx(web.repo, req)
445 ctx = webutil.changectx(web.repo, req)
446
446
447 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
447 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
448
448
449 rev = webcommand('rev')(changeset)
449 rev = webcommand('rev')(changeset)
450
450
451 def decodepath(path):
451 def decodepath(path):
452 """Hook for mapping a path in the repository to a path in the
452 """Hook for mapping a path in the repository to a path in the
453 working copy.
453 working copy.
454
454
455 Extensions (e.g., largefiles) can override this to remap files in
455 Extensions (e.g., largefiles) can override this to remap files in
456 the virtual file system presented by the manifest command below."""
456 the virtual file system presented by the manifest command below."""
457 return path
457 return path
458
458
459 @webcommand('manifest')
459 @webcommand('manifest')
460 def manifest(web, req, tmpl):
460 def manifest(web, req, tmpl):
461 """
461 """
462 /manifest[/{revision}[/{path}]]
462 /manifest[/{revision}[/{path}]]
463 -------------------------------
463 -------------------------------
464
464
465 Show information about a directory.
465 Show information about a directory.
466
466
467 If the URL path arguments are omitted, information about the root
467 If the URL path arguments are omitted, information about the root
468 directory for the ``tip`` changeset will be shown.
468 directory for the ``tip`` changeset will be shown.
469
469
470 Because this handler can only show information for directories, it
470 Because this handler can only show information for directories, it
471 is recommended to use the ``file`` handler instead, as it can handle both
471 is recommended to use the ``file`` handler instead, as it can handle both
472 directories and files.
472 directories and files.
473
473
474 The ``manifest`` template will be rendered for this handler.
474 The ``manifest`` template will be rendered for this handler.
475 """
475 """
476 if 'node' in req.form:
476 if 'node' in req.form:
477 ctx = webutil.changectx(web.repo, req)
477 ctx = webutil.changectx(web.repo, req)
478 symrev = webutil.symrevorshortnode(req, ctx)
478 symrev = webutil.symrevorshortnode(req, ctx)
479 else:
479 else:
480 ctx = web.repo['tip']
480 ctx = web.repo['tip']
481 symrev = 'tip'
481 symrev = 'tip'
482 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
482 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
483 mf = ctx.manifest()
483 mf = ctx.manifest()
484 node = ctx.node()
484 node = ctx.node()
485
485
486 files = {}
486 files = {}
487 dirs = {}
487 dirs = {}
488 parity = paritygen(web.stripecount)
488 parity = paritygen(web.stripecount)
489
489
490 if path and path[-1] != "/":
490 if path and path[-1] != "/":
491 path += "/"
491 path += "/"
492 l = len(path)
492 l = len(path)
493 abspath = "/" + path
493 abspath = "/" + path
494
494
495 for full, n in mf.iteritems():
495 for full, n in mf.iteritems():
496 # the virtual path (working copy path) used for the full
496 # the virtual path (working copy path) used for the full
497 # (repository) path
497 # (repository) path
498 f = decodepath(full)
498 f = decodepath(full)
499
499
500 if f[:l] != path:
500 if f[:l] != path:
501 continue
501 continue
502 remain = f[l:]
502 remain = f[l:]
503 elements = remain.split('/')
503 elements = remain.split('/')
504 if len(elements) == 1:
504 if len(elements) == 1:
505 files[remain] = full
505 files[remain] = full
506 else:
506 else:
507 h = dirs # need to retain ref to dirs (root)
507 h = dirs # need to retain ref to dirs (root)
508 for elem in elements[0:-1]:
508 for elem in elements[0:-1]:
509 if elem not in h:
509 if elem not in h:
510 h[elem] = {}
510 h[elem] = {}
511 h = h[elem]
511 h = h[elem]
512 if len(h) > 1:
512 if len(h) > 1:
513 break
513 break
514 h[None] = None # denotes files present
514 h[None] = None # denotes files present
515
515
516 if mf and not files and not dirs:
516 if mf and not files and not dirs:
517 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
517 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
518
518
519 def filelist(**map):
519 def filelist(**map):
520 for f in sorted(files):
520 for f in sorted(files):
521 full = files[f]
521 full = files[f]
522
522
523 fctx = ctx.filectx(full)
523 fctx = ctx.filectx(full)
524 yield {"file": full,
524 yield {"file": full,
525 "parity": parity.next(),
525 "parity": parity.next(),
526 "basename": f,
526 "basename": f,
527 "date": fctx.date(),
527 "date": fctx.date(),
528 "size": fctx.size(),
528 "size": fctx.size(),
529 "permissions": mf.flags(full)}
529 "permissions": mf.flags(full)}
530
530
531 def dirlist(**map):
531 def dirlist(**map):
532 for d in sorted(dirs):
532 for d in sorted(dirs):
533
533
534 emptydirs = []
534 emptydirs = []
535 h = dirs[d]
535 h = dirs[d]
536 while isinstance(h, dict) and len(h) == 1:
536 while isinstance(h, dict) and len(h) == 1:
537 k, v = h.items()[0]
537 k, v = h.items()[0]
538 if v:
538 if v:
539 emptydirs.append(k)
539 emptydirs.append(k)
540 h = v
540 h = v
541
541
542 path = "%s%s" % (abspath, d)
542 path = "%s%s" % (abspath, d)
543 yield {"parity": parity.next(),
543 yield {"parity": parity.next(),
544 "path": path,
544 "path": path,
545 "emptydirs": "/".join(emptydirs),
545 "emptydirs": "/".join(emptydirs),
546 "basename": d}
546 "basename": d}
547
547
548 return tmpl("manifest",
548 return tmpl("manifest",
549 rev=ctx.rev(),
549 rev=ctx.rev(),
550 symrev=symrev,
550 symrev=symrev,
551 node=hex(node),
551 node=hex(node),
552 path=abspath,
552 path=abspath,
553 up=webutil.up(abspath),
553 up=webutil.up(abspath),
554 upparity=parity.next(),
554 upparity=parity.next(),
555 fentries=filelist,
555 fentries=filelist,
556 dentries=dirlist,
556 dentries=dirlist,
557 archives=web.archivelist(hex(node)),
557 archives=web.archivelist(hex(node)),
558 tags=webutil.nodetagsdict(web.repo, node),
558 tags=webutil.nodetagsdict(web.repo, node),
559 bookmarks=webutil.nodebookmarksdict(web.repo, node),
559 bookmarks=webutil.nodebookmarksdict(web.repo, node),
560 branch=webutil.nodebranchnodefault(ctx),
560 branch=webutil.nodebranchnodefault(ctx),
561 inbranch=webutil.nodeinbranch(web.repo, ctx),
561 inbranch=webutil.nodeinbranch(web.repo, ctx),
562 branches=webutil.nodebranchdict(web.repo, ctx))
562 branches=webutil.nodebranchdict(web.repo, ctx))
563
563
564 @webcommand('tags')
564 @webcommand('tags')
565 def tags(web, req, tmpl):
565 def tags(web, req, tmpl):
566 """
566 """
567 /tags
567 /tags
568 -----
568 -----
569
569
570 Show information about tags.
570 Show information about tags.
571
571
572 No arguments are accepted.
572 No arguments are accepted.
573
573
574 The ``tags`` template is rendered.
574 The ``tags`` template is rendered.
575 """
575 """
576 i = list(reversed(web.repo.tagslist()))
576 i = list(reversed(web.repo.tagslist()))
577 parity = paritygen(web.stripecount)
577 parity = paritygen(web.stripecount)
578
578
579 def entries(notip, latestonly, **map):
579 def entries(notip, latestonly, **map):
580 t = i
580 t = i
581 if notip:
581 if notip:
582 t = [(k, n) for k, n in i if k != "tip"]
582 t = [(k, n) for k, n in i if k != "tip"]
583 if latestonly:
583 if latestonly:
584 t = t[:1]
584 t = t[:1]
585 for k, n in t:
585 for k, n in t:
586 yield {"parity": parity.next(),
586 yield {"parity": parity.next(),
587 "tag": k,
587 "tag": k,
588 "date": web.repo[n].date(),
588 "date": web.repo[n].date(),
589 "node": hex(n)}
589 "node": hex(n)}
590
590
591 return tmpl("tags",
591 return tmpl("tags",
592 node=hex(web.repo.changelog.tip()),
592 node=hex(web.repo.changelog.tip()),
593 entries=lambda **x: entries(False, False, **x),
593 entries=lambda **x: entries(False, False, **x),
594 entriesnotip=lambda **x: entries(True, False, **x),
594 entriesnotip=lambda **x: entries(True, False, **x),
595 latestentry=lambda **x: entries(True, True, **x))
595 latestentry=lambda **x: entries(True, True, **x))
596
596
597 @webcommand('bookmarks')
597 @webcommand('bookmarks')
598 def bookmarks(web, req, tmpl):
598 def bookmarks(web, req, tmpl):
599 """
599 """
600 /bookmarks
600 /bookmarks
601 ----------
601 ----------
602
602
603 Show information about bookmarks.
603 Show information about bookmarks.
604
604
605 No arguments are accepted.
605 No arguments are accepted.
606
606
607 The ``bookmarks`` template is rendered.
607 The ``bookmarks`` template is rendered.
608 """
608 """
609 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
609 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
610 parity = paritygen(web.stripecount)
610 parity = paritygen(web.stripecount)
611
611
612 def entries(latestonly, **map):
612 def entries(latestonly, **map):
613 if latestonly:
613 if latestonly:
614 t = [min(i)]
614 t = [min(i)]
615 else:
615 else:
616 t = sorted(i)
616 t = sorted(i)
617 for k, n in t:
617 for k, n in t:
618 yield {"parity": parity.next(),
618 yield {"parity": parity.next(),
619 "bookmark": k,
619 "bookmark": k,
620 "date": web.repo[n].date(),
620 "date": web.repo[n].date(),
621 "node": hex(n)}
621 "node": hex(n)}
622
622
623 return tmpl("bookmarks",
623 return tmpl("bookmarks",
624 node=hex(web.repo.changelog.tip()),
624 node=hex(web.repo.changelog.tip()),
625 entries=lambda **x: entries(latestonly=False, **x),
625 entries=lambda **x: entries(latestonly=False, **x),
626 latestentry=lambda **x: entries(latestonly=True, **x))
626 latestentry=lambda **x: entries(latestonly=True, **x))
627
627
628 @webcommand('branches')
628 @webcommand('branches')
629 def branches(web, req, tmpl):
629 def branches(web, req, tmpl):
630 """
630 """
631 /branches
631 /branches
632 ---------
632 ---------
633
633
634 Show information about branches.
634 Show information about branches.
635
635
636 All known branches are contained in the output, even closed branches.
636 All known branches are contained in the output, even closed branches.
637
637
638 No arguments are accepted.
638 No arguments are accepted.
639
639
640 The ``branches`` template is rendered.
640 The ``branches`` template is rendered.
641 """
641 """
642 entries = webutil.branchentries(web.repo, web.stripecount)
642 entries = webutil.branchentries(web.repo, web.stripecount)
643 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
643 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
644 return tmpl('branches', node=hex(web.repo.changelog.tip()),
644 return tmpl('branches', node=hex(web.repo.changelog.tip()),
645 entries=entries, latestentry=latestentry)
645 entries=entries, latestentry=latestentry)
646
646
647 @webcommand('summary')
647 @webcommand('summary')
648 def summary(web, req, tmpl):
648 def summary(web, req, tmpl):
649 """
649 """
650 /summary
650 /summary
651 --------
651 --------
652
652
653 Show a summary of repository state.
653 Show a summary of repository state.
654
654
655 Information about the latest changesets, bookmarks, tags, and branches
655 Information about the latest changesets, bookmarks, tags, and branches
656 is captured by this handler.
656 is captured by this handler.
657
657
658 The ``summary`` template is rendered.
658 The ``summary`` template is rendered.
659 """
659 """
660 i = reversed(web.repo.tagslist())
660 i = reversed(web.repo.tagslist())
661
661
662 def tagentries(**map):
662 def tagentries(**map):
663 parity = paritygen(web.stripecount)
663 parity = paritygen(web.stripecount)
664 count = 0
664 count = 0
665 for k, n in i:
665 for k, n in i:
666 if k == "tip": # skip tip
666 if k == "tip": # skip tip
667 continue
667 continue
668
668
669 count += 1
669 count += 1
670 if count > 10: # limit to 10 tags
670 if count > 10: # limit to 10 tags
671 break
671 break
672
672
673 yield tmpl("tagentry",
673 yield tmpl("tagentry",
674 parity=parity.next(),
674 parity=parity.next(),
675 tag=k,
675 tag=k,
676 node=hex(n),
676 node=hex(n),
677 date=web.repo[n].date())
677 date=web.repo[n].date())
678
678
679 def bookmarks(**map):
679 def bookmarks(**map):
680 parity = paritygen(web.stripecount)
680 parity = paritygen(web.stripecount)
681 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
681 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
682 for k, n in sorted(marks)[:10]: # limit to 10 bookmarks
682 for k, n in sorted(marks)[:10]: # limit to 10 bookmarks
683 yield {'parity': parity.next(),
683 yield {'parity': parity.next(),
684 'bookmark': k,
684 'bookmark': k,
685 'date': web.repo[n].date(),
685 'date': web.repo[n].date(),
686 'node': hex(n)}
686 'node': hex(n)}
687
687
688 def changelist(**map):
688 def changelist(**map):
689 parity = paritygen(web.stripecount, offset=start - end)
689 parity = paritygen(web.stripecount, offset=start - end)
690 l = [] # build a list in forward order for efficiency
690 l = [] # build a list in forward order for efficiency
691 revs = []
691 revs = []
692 if start < end:
692 if start < end:
693 revs = web.repo.changelog.revs(start, end - 1)
693 revs = web.repo.changelog.revs(start, end - 1)
694 for i in revs:
694 for i in revs:
695 ctx = web.repo[i]
695 ctx = web.repo[i]
696 n = ctx.node()
696 n = ctx.node()
697 hn = hex(n)
697 hn = hex(n)
698
698
699 l.append(tmpl(
699 l.append(tmpl(
700 'shortlogentry',
700 'shortlogentry',
701 parity=parity.next(),
701 parity=parity.next(),
702 author=ctx.user(),
702 author=ctx.user(),
703 desc=ctx.description(),
703 desc=ctx.description(),
704 extra=ctx.extra(),
704 extra=ctx.extra(),
705 date=ctx.date(),
705 date=ctx.date(),
706 rev=i,
706 rev=i,
707 node=hn,
707 node=hn,
708 tags=webutil.nodetagsdict(web.repo, n),
708 tags=webutil.nodetagsdict(web.repo, n),
709 bookmarks=webutil.nodebookmarksdict(web.repo, n),
709 bookmarks=webutil.nodebookmarksdict(web.repo, n),
710 inbranch=webutil.nodeinbranch(web.repo, ctx),
710 inbranch=webutil.nodeinbranch(web.repo, ctx),
711 branches=webutil.nodebranchdict(web.repo, ctx)))
711 branches=webutil.nodebranchdict(web.repo, ctx)))
712
712
713 l.reverse()
713 l.reverse()
714 yield l
714 yield l
715
715
716 tip = web.repo['tip']
716 tip = web.repo['tip']
717 count = len(web.repo)
717 count = len(web.repo)
718 start = max(0, count - web.maxchanges)
718 start = max(0, count - web.maxchanges)
719 end = min(count, start + web.maxchanges)
719 end = min(count, start + web.maxchanges)
720
720
721 return tmpl("summary",
721 return tmpl("summary",
722 desc=web.config("web", "description", "unknown"),
722 desc=web.config("web", "description", "unknown"),
723 owner=get_contact(web.config) or "unknown",
723 owner=get_contact(web.config) or "unknown",
724 lastchange=tip.date(),
724 lastchange=tip.date(),
725 tags=tagentries,
725 tags=tagentries,
726 bookmarks=bookmarks,
726 bookmarks=bookmarks,
727 branches=webutil.branchentries(web.repo, web.stripecount, 10),
727 branches=webutil.branchentries(web.repo, web.stripecount, 10),
728 shortlog=changelist,
728 shortlog=changelist,
729 node=tip.hex(),
729 node=tip.hex(),
730 symrev='tip',
730 symrev='tip',
731 archives=web.archivelist("tip"))
731 archives=web.archivelist("tip"))
732
732
733 @webcommand('filediff')
733 @webcommand('filediff')
734 def filediff(web, req, tmpl):
734 def filediff(web, req, tmpl):
735 """
735 """
736 /diff/{revision}/{path}
736 /diff/{revision}/{path}
737 -----------------------
737 -----------------------
738
738
739 Show how a file changed in a particular commit.
739 Show how a file changed in a particular commit.
740
740
741 The ``filediff`` template is rendered.
741 The ``filediff`` template is rendered.
742
742
743 This hander is registered under both the ``/diff`` and ``/filediff``
743 This hander is registered under both the ``/diff`` and ``/filediff``
744 paths. ``/diff`` is used in modern code.
744 paths. ``/diff`` is used in modern code.
745 """
745 """
746 fctx, ctx = None, None
746 fctx, ctx = None, None
747 try:
747 try:
748 fctx = webutil.filectx(web.repo, req)
748 fctx = webutil.filectx(web.repo, req)
749 except LookupError:
749 except LookupError:
750 ctx = webutil.changectx(web.repo, req)
750 ctx = webutil.changectx(web.repo, req)
751 path = webutil.cleanpath(web.repo, req.form['file'][0])
751 path = webutil.cleanpath(web.repo, req.form['file'][0])
752 if path not in ctx.files():
752 if path not in ctx.files():
753 raise
753 raise
754
754
755 if fctx is not None:
755 if fctx is not None:
756 n = fctx.node()
756 n = fctx.node()
757 path = fctx.path()
757 path = fctx.path()
758 ctx = fctx.changectx()
758 ctx = fctx.changectx()
759 else:
759 else:
760 n = ctx.node()
760 n = ctx.node()
761 # path already defined in except clause
761 # path already defined in except clause
762
762
763 parity = paritygen(web.stripecount)
763 parity = paritygen(web.stripecount)
764 style = web.config('web', 'style', 'paper')
764 style = web.config('web', 'style', 'paper')
765 if 'style' in req.form:
765 if 'style' in req.form:
766 style = req.form['style'][0]
766 style = req.form['style'][0]
767
767
768 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
768 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
769 if fctx:
769 if fctx:
770 rename = webutil.renamelink(fctx)
770 rename = webutil.renamelink(fctx)
771 ctx = fctx
771 ctx = fctx
772 else:
772 else:
773 rename = []
773 rename = []
774 ctx = ctx
774 ctx = ctx
775 return tmpl("filediff",
775 return tmpl("filediff",
776 file=path,
776 file=path,
777 node=hex(n),
777 node=hex(n),
778 rev=ctx.rev(),
778 rev=ctx.rev(),
779 symrev=webutil.symrevorshortnode(req, ctx),
779 symrev=webutil.symrevorshortnode(req, ctx),
780 date=ctx.date(),
780 date=ctx.date(),
781 desc=ctx.description(),
781 desc=ctx.description(),
782 extra=ctx.extra(),
782 extra=ctx.extra(),
783 author=ctx.user(),
783 author=ctx.user(),
784 rename=rename,
784 rename=rename,
785 branch=webutil.nodebranchnodefault(ctx),
785 branch=webutil.nodebranchnodefault(ctx),
786 parent=webutil.parents(ctx),
786 parent=webutil.parents(ctx),
787 child=webutil.children(ctx),
787 child=webutil.children(ctx),
788 tags=webutil.nodetagsdict(web.repo, n),
788 tags=webutil.nodetagsdict(web.repo, n),
789 bookmarks=webutil.nodebookmarksdict(web.repo, n),
789 bookmarks=webutil.nodebookmarksdict(web.repo, n),
790 diff=diffs)
790 diff=diffs)
791
791
792 diff = webcommand('diff')(filediff)
792 diff = webcommand('diff')(filediff)
793
793
794 @webcommand('comparison')
794 @webcommand('comparison')
795 def comparison(web, req, tmpl):
795 def comparison(web, req, tmpl):
796 """
796 """
797 /comparison/{revision}/{path}
797 /comparison/{revision}/{path}
798 -----------------------------
798 -----------------------------
799
799
800 Show a comparison between the old and new versions of a file from changes
800 Show a comparison between the old and new versions of a file from changes
801 made on a particular revision.
801 made on a particular revision.
802
802
803 This is similar to the ``diff`` handler. However, this form features
803 This is similar to the ``diff`` handler. However, this form features
804 a split or side-by-side diff rather than a unified diff.
804 a split or side-by-side diff rather than a unified diff.
805
805
806 The ``context`` query string argument can be used to control the lines of
806 The ``context`` query string argument can be used to control the lines of
807 context in the diff.
807 context in the diff.
808
808
809 The ``filecomparison`` template is rendered.
809 The ``filecomparison`` template is rendered.
810 """
810 """
811 ctx = webutil.changectx(web.repo, req)
811 ctx = webutil.changectx(web.repo, req)
812 if 'file' not in req.form:
812 if 'file' not in req.form:
813 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
813 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
814 path = webutil.cleanpath(web.repo, req.form['file'][0])
814 path = webutil.cleanpath(web.repo, req.form['file'][0])
815 rename = path in ctx and webutil.renamelink(ctx[path]) or []
815 rename = path in ctx and webutil.renamelink(ctx[path]) or []
816
816
817 parsecontext = lambda v: v == 'full' and -1 or int(v)
817 parsecontext = lambda v: v == 'full' and -1 or int(v)
818 if 'context' in req.form:
818 if 'context' in req.form:
819 context = parsecontext(req.form['context'][0])
819 context = parsecontext(req.form['context'][0])
820 else:
820 else:
821 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
821 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
822
822
823 def filelines(f):
823 def filelines(f):
824 if util.binary(f.data()):
824 if util.binary(f.data()):
825 mt = mimetypes.guess_type(f.path())[0]
825 mt = mimetypes.guess_type(f.path())[0]
826 if not mt:
826 if not mt:
827 mt = 'application/octet-stream'
827 mt = 'application/octet-stream'
828 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
828 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
829 return f.data().splitlines()
829 return f.data().splitlines()
830
830
831 parent = ctx.p1()
831 parent = ctx.p1()
832 leftrev = parent.rev()
832 leftrev = parent.rev()
833 leftnode = parent.node()
833 leftnode = parent.node()
834 rightrev = ctx.rev()
834 rightrev = ctx.rev()
835 rightnode = ctx.node()
835 rightnode = ctx.node()
836 if path in ctx:
836 if path in ctx:
837 fctx = ctx[path]
837 fctx = ctx[path]
838 rightlines = filelines(fctx)
838 rightlines = filelines(fctx)
839 if path not in parent:
839 if path not in parent:
840 leftlines = ()
840 leftlines = ()
841 else:
841 else:
842 pfctx = parent[path]
842 pfctx = parent[path]
843 leftlines = filelines(pfctx)
843 leftlines = filelines(pfctx)
844 else:
844 else:
845 rightlines = ()
845 rightlines = ()
846 fctx = ctx.parents()[0][path]
846 fctx = ctx.parents()[0][path]
847 leftlines = filelines(fctx)
847 leftlines = filelines(fctx)
848
848
849 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
849 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
850 return tmpl('filecomparison',
850 return tmpl('filecomparison',
851 file=path,
851 file=path,
852 node=hex(ctx.node()),
852 node=hex(ctx.node()),
853 rev=ctx.rev(),
853 rev=ctx.rev(),
854 symrev=webutil.symrevorshortnode(req, ctx),
854 symrev=webutil.symrevorshortnode(req, ctx),
855 date=ctx.date(),
855 date=ctx.date(),
856 desc=ctx.description(),
856 desc=ctx.description(),
857 extra=ctx.extra(),
857 extra=ctx.extra(),
858 author=ctx.user(),
858 author=ctx.user(),
859 rename=rename,
859 rename=rename,
860 branch=webutil.nodebranchnodefault(ctx),
860 branch=webutil.nodebranchnodefault(ctx),
861 parent=webutil.parents(fctx),
861 parent=webutil.parents(fctx),
862 child=webutil.children(fctx),
862 child=webutil.children(fctx),
863 tags=webutil.nodetagsdict(web.repo, ctx.node()),
863 tags=webutil.nodetagsdict(web.repo, ctx.node()),
864 bookmarks=webutil.nodebookmarksdict(web.repo, ctx.node()),
864 bookmarks=webutil.nodebookmarksdict(web.repo, ctx.node()),
865 leftrev=leftrev,
865 leftrev=leftrev,
866 leftnode=hex(leftnode),
866 leftnode=hex(leftnode),
867 rightrev=rightrev,
867 rightrev=rightrev,
868 rightnode=hex(rightnode),
868 rightnode=hex(rightnode),
869 comparison=comparison)
869 comparison=comparison)
870
870
871 @webcommand('annotate')
871 @webcommand('annotate')
872 def annotate(web, req, tmpl):
872 def annotate(web, req, tmpl):
873 """
873 """
874 /annotate/{revision}/{path}
874 /annotate/{revision}/{path}
875 ---------------------------
875 ---------------------------
876
876
877 Show changeset information for each line in a file.
877 Show changeset information for each line in a file.
878
878
879 The ``fileannotate`` template is rendered.
879 The ``fileannotate`` template is rendered.
880 """
880 """
881 fctx = webutil.filectx(web.repo, req)
881 fctx = webutil.filectx(web.repo, req)
882 f = fctx.path()
882 f = fctx.path()
883 parity = paritygen(web.stripecount)
883 parity = paritygen(web.stripecount)
884 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
884 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
885 section='annotate', whitespace=True)
885 section='annotate', whitespace=True)
886
886
887 def annotate(**map):
887 def annotate(**map):
888 last = None
888 last = None
889 if util.binary(fctx.data()):
889 if util.binary(fctx.data()):
890 mt = (mimetypes.guess_type(fctx.path())[0]
890 mt = (mimetypes.guess_type(fctx.path())[0]
891 or 'application/octet-stream')
891 or 'application/octet-stream')
892 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
892 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
893 '(binary:%s)' % mt)])
893 '(binary:%s)' % mt)])
894 else:
894 else:
895 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
895 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
896 diffopts=diffopts))
896 diffopts=diffopts))
897 for lineno, ((f, targetline), l) in lines:
897 for lineno, ((f, targetline), l) in lines:
898 fnode = f.filenode()
898 fnode = f.filenode()
899
899
900 if last != fnode:
900 if last != fnode:
901 last = fnode
901 last = fnode
902
902
903 yield {"parity": parity.next(),
903 yield {"parity": parity.next(),
904 "node": f.hex(),
904 "node": f.hex(),
905 "rev": f.rev(),
905 "rev": f.rev(),
906 "author": f.user(),
906 "author": f.user(),
907 "desc": f.description(),
907 "desc": f.description(),
908 "extra": f.extra(),
908 "extra": f.extra(),
909 "file": f.path(),
909 "file": f.path(),
910 "targetline": targetline,
910 "targetline": targetline,
911 "line": l,
911 "line": l,
912 "lineno": lineno + 1,
912 "lineno": lineno + 1,
913 "lineid": "l%d" % (lineno + 1),
913 "lineid": "l%d" % (lineno + 1),
914 "linenumber": "% 6d" % (lineno + 1),
914 "linenumber": "% 6d" % (lineno + 1),
915 "revdate": f.date()}
915 "revdate": f.date()}
916
916
917 return tmpl("fileannotate",
917 return tmpl("fileannotate",
918 file=f,
918 file=f,
919 annotate=annotate,
919 annotate=annotate,
920 path=webutil.up(f),
920 path=webutil.up(f),
921 rev=fctx.rev(),
921 rev=fctx.rev(),
922 symrev=webutil.symrevorshortnode(req, fctx),
922 symrev=webutil.symrevorshortnode(req, fctx),
923 node=fctx.hex(),
923 node=fctx.hex(),
924 author=fctx.user(),
924 author=fctx.user(),
925 date=fctx.date(),
925 date=fctx.date(),
926 desc=fctx.description(),
926 desc=fctx.description(),
927 extra=fctx.extra(),
927 extra=fctx.extra(),
928 rename=webutil.renamelink(fctx),
928 rename=webutil.renamelink(fctx),
929 branch=webutil.nodebranchnodefault(fctx),
929 branch=webutil.nodebranchnodefault(fctx),
930 parent=webutil.parents(fctx),
930 parent=webutil.parents(fctx),
931 child=webutil.children(fctx),
931 child=webutil.children(fctx),
932 tags=webutil.nodetagsdict(web.repo, fctx.node()),
932 tags=webutil.nodetagsdict(web.repo, fctx.node()),
933 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
933 bookmarks=webutil.nodebookmarksdict(web.repo, fctx.node()),
934 permissions=fctx.manifest().flags(f))
934 permissions=fctx.manifest().flags(f))
935
935
936 @webcommand('filelog')
936 @webcommand('filelog')
937 def filelog(web, req, tmpl):
937 def filelog(web, req, tmpl):
938 """
938 """
939 /filelog/{revision}/{path}
939 /filelog/{revision}/{path}
940 --------------------------
940 --------------------------
941
941
942 Show information about the history of a file in the repository.
942 Show information about the history of a file in the repository.
943
943
944 The ``revcount`` query string argument can be defined to control the
944 The ``revcount`` query string argument can be defined to control the
945 maximum number of entries to show.
945 maximum number of entries to show.
946
946
947 The ``filelog`` template will be rendered.
947 The ``filelog`` template will be rendered.
948 """
948 """
949
949
950 try:
950 try:
951 fctx = webutil.filectx(web.repo, req)
951 fctx = webutil.filectx(web.repo, req)
952 f = fctx.path()
952 f = fctx.path()
953 fl = fctx.filelog()
953 fl = fctx.filelog()
954 except error.LookupError:
954 except error.LookupError:
955 f = webutil.cleanpath(web.repo, req.form['file'][0])
955 f = webutil.cleanpath(web.repo, req.form['file'][0])
956 fl = web.repo.file(f)
956 fl = web.repo.file(f)
957 numrevs = len(fl)
957 numrevs = len(fl)
958 if not numrevs: # file doesn't exist at all
958 if not numrevs: # file doesn't exist at all
959 raise
959 raise
960 rev = webutil.changectx(web.repo, req).rev()
960 rev = webutil.changectx(web.repo, req).rev()
961 first = fl.linkrev(0)
961 first = fl.linkrev(0)
962 if rev < first: # current rev is from before file existed
962 if rev < first: # current rev is from before file existed
963 raise
963 raise
964 frev = numrevs - 1
964 frev = numrevs - 1
965 while fl.linkrev(frev) > rev:
965 while fl.linkrev(frev) > rev:
966 frev -= 1
966 frev -= 1
967 fctx = web.repo.filectx(f, fl.linkrev(frev))
967 fctx = web.repo.filectx(f, fl.linkrev(frev))
968
968
969 revcount = web.maxshortchanges
969 revcount = web.maxshortchanges
970 if 'revcount' in req.form:
970 if 'revcount' in req.form:
971 try:
971 try:
972 revcount = int(req.form.get('revcount', [revcount])[0])
972 revcount = int(req.form.get('revcount', [revcount])[0])
973 revcount = max(revcount, 1)
973 revcount = max(revcount, 1)
974 tmpl.defaults['sessionvars']['revcount'] = revcount
974 tmpl.defaults['sessionvars']['revcount'] = revcount
975 except ValueError:
975 except ValueError:
976 pass
976 pass
977
977
978 lessvars = copy.copy(tmpl.defaults['sessionvars'])
978 lessvars = copy.copy(tmpl.defaults['sessionvars'])
979 lessvars['revcount'] = max(revcount / 2, 1)
979 lessvars['revcount'] = max(revcount / 2, 1)
980 morevars = copy.copy(tmpl.defaults['sessionvars'])
980 morevars = copy.copy(tmpl.defaults['sessionvars'])
981 morevars['revcount'] = revcount * 2
981 morevars['revcount'] = revcount * 2
982
982
983 count = fctx.filerev() + 1
983 count = fctx.filerev() + 1
984 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
984 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
985 end = min(count, start + revcount) # last rev on this page
985 end = min(count, start + revcount) # last rev on this page
986 parity = paritygen(web.stripecount, offset=start - end)
986 parity = paritygen(web.stripecount, offset=start - end)
987
987
988 def entries():
988 def entries():
989 l = []
989 l = []
990
990
991 repo = web.repo
991 repo = web.repo
992 revs = fctx.filelog().revs(start, end - 1)
992 revs = fctx.filelog().revs(start, end - 1)
993 for i in revs:
993 for i in revs:
994 iterfctx = fctx.filectx(i)
994 iterfctx = fctx.filectx(i)
995
995
996 l.append({"parity": parity.next(),
996 l.append({"parity": parity.next(),
997 "filerev": i,
997 "filerev": i,
998 "file": f,
998 "file": f,
999 "node": iterfctx.hex(),
999 "node": iterfctx.hex(),
1000 "author": iterfctx.user(),
1000 "author": iterfctx.user(),
1001 "date": iterfctx.date(),
1001 "date": iterfctx.date(),
1002 "rename": webutil.renamelink(iterfctx),
1002 "rename": webutil.renamelink(iterfctx),
1003 "parent": webutil.parents(iterfctx),
1003 "parent": webutil.parents(iterfctx),
1004 "child": webutil.children(iterfctx),
1004 "child": webutil.children(iterfctx),
1005 "desc": iterfctx.description(),
1005 "desc": iterfctx.description(),
1006 "extra": iterfctx.extra(),
1006 "extra": iterfctx.extra(),
1007 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1007 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
1008 "bookmarks": webutil.nodebookmarksdict(
1008 "bookmarks": webutil.nodebookmarksdict(
1009 repo, iterfctx.node()),
1009 repo, iterfctx.node()),
1010 "branch": webutil.nodebranchnodefault(iterfctx),
1010 "branch": webutil.nodebranchnodefault(iterfctx),
1011 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1011 "inbranch": webutil.nodeinbranch(repo, iterfctx),
1012 "branches": webutil.nodebranchdict(repo, iterfctx)})
1012 "branches": webutil.nodebranchdict(repo, iterfctx)})
1013 for e in reversed(l):
1013 for e in reversed(l):
1014 yield e
1014 yield e
1015
1015
1016 entries = list(entries())
1016 entries = list(entries())
1017 latestentry = entries[:1]
1017 latestentry = entries[:1]
1018
1018
1019 revnav = webutil.filerevnav(web.repo, fctx.path())
1019 revnav = webutil.filerevnav(web.repo, fctx.path())
1020 nav = revnav.gen(end - 1, revcount, count)
1020 nav = revnav.gen(end - 1, revcount, count)
1021 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1021 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
1022 symrev=webutil.symrevorshortnode(req, fctx),
1022 symrev=webutil.symrevorshortnode(req, fctx),
1023 entries=entries,
1023 entries=entries,
1024 latestentry=latestentry,
1024 latestentry=latestentry,
1025 revcount=revcount, morevars=morevars, lessvars=lessvars)
1025 revcount=revcount, morevars=morevars, lessvars=lessvars)
1026
1026
1027 @webcommand('archive')
1027 @webcommand('archive')
1028 def archive(web, req, tmpl):
1028 def archive(web, req, tmpl):
1029 """
1029 """
1030 /archive/{revision}.{format}[/{path}]
1030 /archive/{revision}.{format}[/{path}]
1031 -------------------------------------
1031 -------------------------------------
1032
1032
1033 Obtain an archive of repository content.
1033 Obtain an archive of repository content.
1034
1034
1035 The content and type of the archive is defined by a URL path parameter.
1035 The content and type of the archive is defined by a URL path parameter.
1036 ``format`` is the file extension of the archive type to be generated. e.g.
1036 ``format`` is the file extension of the archive type to be generated. e.g.
1037 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1037 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1038 server configuration.
1038 server configuration.
1039
1039
1040 The optional ``path`` URL parameter controls content to include in the
1040 The optional ``path`` URL parameter controls content to include in the
1041 archive. If omitted, every file in the specified revision is present in the
1041 archive. If omitted, every file in the specified revision is present in the
1042 archive. If included, only the specified file or contents of the specified
1042 archive. If included, only the specified file or contents of the specified
1043 directory will be included in the archive.
1043 directory will be included in the archive.
1044
1044
1045 No template is used for this handler. Raw, binary content is generated.
1045 No template is used for this handler. Raw, binary content is generated.
1046 """
1046 """
1047
1047
1048 type_ = req.form.get('type', [None])[0]
1048 type_ = req.form.get('type', [None])[0]
1049 allowed = web.configlist("web", "allow_archive")
1049 allowed = web.configlist("web", "allow_archive")
1050 key = req.form['node'][0]
1050 key = req.form['node'][0]
1051
1051
1052 if type_ not in web.archives:
1052 if type_ not in web.archives:
1053 msg = 'Unsupported archive type: %s' % type_
1053 msg = 'Unsupported archive type: %s' % type_
1054 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1054 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1055
1055
1056 if not ((type_ in allowed or
1056 if not ((type_ in allowed or
1057 web.configbool("web", "allow" + type_, False))):
1057 web.configbool("web", "allow" + type_, False))):
1058 msg = 'Archive type not allowed: %s' % type_
1058 msg = 'Archive type not allowed: %s' % type_
1059 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1059 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1060
1060
1061 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1061 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1062 cnode = web.repo.lookup(key)
1062 cnode = web.repo.lookup(key)
1063 arch_version = key
1063 arch_version = key
1064 if cnode == key or key == 'tip':
1064 if cnode == key or key == 'tip':
1065 arch_version = short(cnode)
1065 arch_version = short(cnode)
1066 name = "%s-%s" % (reponame, arch_version)
1066 name = "%s-%s" % (reponame, arch_version)
1067
1067
1068 ctx = webutil.changectx(web.repo, req)
1068 ctx = webutil.changectx(web.repo, req)
1069 pats = []
1069 pats = []
1070 matchfn = scmutil.match(ctx, [])
1070 matchfn = scmutil.match(ctx, [])
1071 file = req.form.get('file', None)
1071 file = req.form.get('file', None)
1072 if file:
1072 if file:
1073 pats = ['path:' + file[0]]
1073 pats = ['path:' + file[0]]
1074 matchfn = scmutil.match(ctx, pats, default='path')
1074 matchfn = scmutil.match(ctx, pats, default='path')
1075 if pats:
1075 if pats:
1076 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1076 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1077 if not files:
1077 if not files:
1078 raise ErrorResponse(HTTP_NOT_FOUND,
1078 raise ErrorResponse(HTTP_NOT_FOUND,
1079 'file(s) not found: %s' % file[0])
1079 'file(s) not found: %s' % file[0])
1080
1080
1081 mimetype, artype, extension, encoding = web.archive_specs[type_]
1081 mimetype, artype, extension, encoding = web.archivespecs[type_]
1082 headers = [
1082 headers = [
1083 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1083 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1084 ]
1084 ]
1085 if encoding:
1085 if encoding:
1086 headers.append(('Content-Encoding', encoding))
1086 headers.append(('Content-Encoding', encoding))
1087 req.headers.extend(headers)
1087 req.headers.extend(headers)
1088 req.respond(HTTP_OK, mimetype)
1088 req.respond(HTTP_OK, mimetype)
1089
1089
1090 archival.archive(web.repo, req, cnode, artype, prefix=name,
1090 archival.archive(web.repo, req, cnode, artype, prefix=name,
1091 matchfn=matchfn,
1091 matchfn=matchfn,
1092 subrepos=web.configbool("web", "archivesubrepos"))
1092 subrepos=web.configbool("web", "archivesubrepos"))
1093 return []
1093 return []
1094
1094
1095
1095
1096 @webcommand('static')
1096 @webcommand('static')
1097 def static(web, req, tmpl):
1097 def static(web, req, tmpl):
1098 fname = req.form['file'][0]
1098 fname = req.form['file'][0]
1099 # a repo owner may set web.static in .hg/hgrc to get any file
1099 # a repo owner may set web.static in .hg/hgrc to get any file
1100 # readable by the user running the CGI script
1100 # readable by the user running the CGI script
1101 static = web.config("web", "static", None, untrusted=False)
1101 static = web.config("web", "static", None, untrusted=False)
1102 if not static:
1102 if not static:
1103 tp = web.templatepath or templater.templatepaths()
1103 tp = web.templatepath or templater.templatepaths()
1104 if isinstance(tp, str):
1104 if isinstance(tp, str):
1105 tp = [tp]
1105 tp = [tp]
1106 static = [os.path.join(p, 'static') for p in tp]
1106 static = [os.path.join(p, 'static') for p in tp]
1107 staticfile(static, fname, req)
1107 staticfile(static, fname, req)
1108 return []
1108 return []
1109
1109
1110 @webcommand('graph')
1110 @webcommand('graph')
1111 def graph(web, req, tmpl):
1111 def graph(web, req, tmpl):
1112 """
1112 """
1113 /graph[/{revision}]
1113 /graph[/{revision}]
1114 -------------------
1114 -------------------
1115
1115
1116 Show information about the graphical topology of the repository.
1116 Show information about the graphical topology of the repository.
1117
1117
1118 Information rendered by this handler can be used to create visual
1118 Information rendered by this handler can be used to create visual
1119 representations of repository topology.
1119 representations of repository topology.
1120
1120
1121 The ``revision`` URL parameter controls the starting changeset.
1121 The ``revision`` URL parameter controls the starting changeset.
1122
1122
1123 The ``revcount`` query string argument can define the number of changesets
1123 The ``revcount`` query string argument can define the number of changesets
1124 to show information for.
1124 to show information for.
1125
1125
1126 This handler will render the ``graph`` template.
1126 This handler will render the ``graph`` template.
1127 """
1127 """
1128
1128
1129 if 'node' in req.form:
1129 if 'node' in req.form:
1130 ctx = webutil.changectx(web.repo, req)
1130 ctx = webutil.changectx(web.repo, req)
1131 symrev = webutil.symrevorshortnode(req, ctx)
1131 symrev = webutil.symrevorshortnode(req, ctx)
1132 else:
1132 else:
1133 ctx = web.repo['tip']
1133 ctx = web.repo['tip']
1134 symrev = 'tip'
1134 symrev = 'tip'
1135 rev = ctx.rev()
1135 rev = ctx.rev()
1136
1136
1137 bg_height = 39
1137 bg_height = 39
1138 revcount = web.maxshortchanges
1138 revcount = web.maxshortchanges
1139 if 'revcount' in req.form:
1139 if 'revcount' in req.form:
1140 try:
1140 try:
1141 revcount = int(req.form.get('revcount', [revcount])[0])
1141 revcount = int(req.form.get('revcount', [revcount])[0])
1142 revcount = max(revcount, 1)
1142 revcount = max(revcount, 1)
1143 tmpl.defaults['sessionvars']['revcount'] = revcount
1143 tmpl.defaults['sessionvars']['revcount'] = revcount
1144 except ValueError:
1144 except ValueError:
1145 pass
1145 pass
1146
1146
1147 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1147 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1148 lessvars['revcount'] = max(revcount / 2, 1)
1148 lessvars['revcount'] = max(revcount / 2, 1)
1149 morevars = copy.copy(tmpl.defaults['sessionvars'])
1149 morevars = copy.copy(tmpl.defaults['sessionvars'])
1150 morevars['revcount'] = revcount * 2
1150 morevars['revcount'] = revcount * 2
1151
1151
1152 count = len(web.repo)
1152 count = len(web.repo)
1153 pos = rev
1153 pos = rev
1154
1154
1155 uprev = min(max(0, count - 1), rev + revcount)
1155 uprev = min(max(0, count - 1), rev + revcount)
1156 downrev = max(0, rev - revcount)
1156 downrev = max(0, rev - revcount)
1157 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1157 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1158
1158
1159 tree = []
1159 tree = []
1160 if pos != -1:
1160 if pos != -1:
1161 allrevs = web.repo.changelog.revs(pos, 0)
1161 allrevs = web.repo.changelog.revs(pos, 0)
1162 revs = []
1162 revs = []
1163 for i in allrevs:
1163 for i in allrevs:
1164 revs.append(i)
1164 revs.append(i)
1165 if len(revs) >= revcount:
1165 if len(revs) >= revcount:
1166 break
1166 break
1167
1167
1168 # We have to feed a baseset to dagwalker as it is expecting smartset
1168 # We have to feed a baseset to dagwalker as it is expecting smartset
1169 # object. This does not have a big impact on hgweb performance itself
1169 # object. This does not have a big impact on hgweb performance itself
1170 # since hgweb graphing code is not itself lazy yet.
1170 # since hgweb graphing code is not itself lazy yet.
1171 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1171 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1172 # As we said one line above... not lazy.
1172 # As we said one line above... not lazy.
1173 tree = list(graphmod.colored(dag, web.repo))
1173 tree = list(graphmod.colored(dag, web.repo))
1174
1174
1175 def getcolumns(tree):
1175 def getcolumns(tree):
1176 cols = 0
1176 cols = 0
1177 for (id, type, ctx, vtx, edges) in tree:
1177 for (id, type, ctx, vtx, edges) in tree:
1178 if type != graphmod.CHANGESET:
1178 if type != graphmod.CHANGESET:
1179 continue
1179 continue
1180 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1180 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1181 max([edge[1] for edge in edges] or [0]))
1181 max([edge[1] for edge in edges] or [0]))
1182 return cols
1182 return cols
1183
1183
1184 def graphdata(usetuples, **map):
1184 def graphdata(usetuples, **map):
1185 data = []
1185 data = []
1186
1186
1187 row = 0
1187 row = 0
1188 for (id, type, ctx, vtx, edges) in tree:
1188 for (id, type, ctx, vtx, edges) in tree:
1189 if type != graphmod.CHANGESET:
1189 if type != graphmod.CHANGESET:
1190 continue
1190 continue
1191 node = str(ctx)
1191 node = str(ctx)
1192 age = templatefilters.age(ctx.date())
1192 age = templatefilters.age(ctx.date())
1193 desc = templatefilters.firstline(ctx.description())
1193 desc = templatefilters.firstline(ctx.description())
1194 desc = cgi.escape(templatefilters.nonempty(desc))
1194 desc = cgi.escape(templatefilters.nonempty(desc))
1195 user = cgi.escape(templatefilters.person(ctx.user()))
1195 user = cgi.escape(templatefilters.person(ctx.user()))
1196 branch = cgi.escape(ctx.branch())
1196 branch = cgi.escape(ctx.branch())
1197 try:
1197 try:
1198 branchnode = web.repo.branchtip(branch)
1198 branchnode = web.repo.branchtip(branch)
1199 except error.RepoLookupError:
1199 except error.RepoLookupError:
1200 branchnode = None
1200 branchnode = None
1201 branch = branch, branchnode == ctx.node()
1201 branch = branch, branchnode == ctx.node()
1202
1202
1203 if usetuples:
1203 if usetuples:
1204 data.append((node, vtx, edges, desc, user, age, branch,
1204 data.append((node, vtx, edges, desc, user, age, branch,
1205 [cgi.escape(x) for x in ctx.tags()],
1205 [cgi.escape(x) for x in ctx.tags()],
1206 [cgi.escape(x) for x in ctx.bookmarks()]))
1206 [cgi.escape(x) for x in ctx.bookmarks()]))
1207 else:
1207 else:
1208 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1208 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1209 'color': (edge[2] - 1) % 6 + 1,
1209 'color': (edge[2] - 1) % 6 + 1,
1210 'width': edge[3], 'bcolor': edge[4]}
1210 'width': edge[3], 'bcolor': edge[4]}
1211 for edge in edges]
1211 for edge in edges]
1212
1212
1213 data.append(
1213 data.append(
1214 {'node': node,
1214 {'node': node,
1215 'col': vtx[0],
1215 'col': vtx[0],
1216 'color': (vtx[1] - 1) % 6 + 1,
1216 'color': (vtx[1] - 1) % 6 + 1,
1217 'edges': edgedata,
1217 'edges': edgedata,
1218 'row': row,
1218 'row': row,
1219 'nextrow': row + 1,
1219 'nextrow': row + 1,
1220 'desc': desc,
1220 'desc': desc,
1221 'user': user,
1221 'user': user,
1222 'age': age,
1222 'age': age,
1223 'bookmarks': webutil.nodebookmarksdict(
1223 'bookmarks': webutil.nodebookmarksdict(
1224 web.repo, ctx.node()),
1224 web.repo, ctx.node()),
1225 'branches': webutil.nodebranchdict(web.repo, ctx),
1225 'branches': webutil.nodebranchdict(web.repo, ctx),
1226 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1226 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1227 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1227 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1228
1228
1229 row += 1
1229 row += 1
1230
1230
1231 return data
1231 return data
1232
1232
1233 cols = getcolumns(tree)
1233 cols = getcolumns(tree)
1234 rows = len(tree)
1234 rows = len(tree)
1235 canvasheight = (rows + 1) * bg_height - 27
1235 canvasheight = (rows + 1) * bg_height - 27
1236
1236
1237 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1237 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1238 uprev=uprev,
1238 uprev=uprev,
1239 lessvars=lessvars, morevars=morevars, downrev=downrev,
1239 lessvars=lessvars, morevars=morevars, downrev=downrev,
1240 cols=cols, rows=rows,
1240 cols=cols, rows=rows,
1241 canvaswidth=(cols + 1) * bg_height,
1241 canvaswidth=(cols + 1) * bg_height,
1242 truecanvasheight=rows * bg_height,
1242 truecanvasheight=rows * bg_height,
1243 canvasheight=canvasheight, bg_height=bg_height,
1243 canvasheight=canvasheight, bg_height=bg_height,
1244 jsdata=lambda **x: graphdata(True, **x),
1244 jsdata=lambda **x: graphdata(True, **x),
1245 nodes=lambda **x: graphdata(False, **x),
1245 nodes=lambda **x: graphdata(False, **x),
1246 node=ctx.hex(), changenav=changenav)
1246 node=ctx.hex(), changenav=changenav)
1247
1247
1248 def _getdoc(e):
1248 def _getdoc(e):
1249 doc = e[0].__doc__
1249 doc = e[0].__doc__
1250 if doc:
1250 if doc:
1251 doc = _(doc).split('\n')[0]
1251 doc = _(doc).split('\n')[0]
1252 else:
1252 else:
1253 doc = _('(no help text available)')
1253 doc = _('(no help text available)')
1254 return doc
1254 return doc
1255
1255
1256 @webcommand('help')
1256 @webcommand('help')
1257 def help(web, req, tmpl):
1257 def help(web, req, tmpl):
1258 """
1258 """
1259 /help[/{topic}]
1259 /help[/{topic}]
1260 ---------------
1260 ---------------
1261
1261
1262 Render help documentation.
1262 Render help documentation.
1263
1263
1264 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1264 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1265 is defined, that help topic will be rendered. If not, an index of
1265 is defined, that help topic will be rendered. If not, an index of
1266 available help topics will be rendered.
1266 available help topics will be rendered.
1267
1267
1268 The ``help`` template will be rendered when requesting help for a topic.
1268 The ``help`` template will be rendered when requesting help for a topic.
1269 ``helptopics`` will be rendered for the index of help topics.
1269 ``helptopics`` will be rendered for the index of help topics.
1270 """
1270 """
1271 from mercurial import commands # avoid cycle
1271 from mercurial import commands # avoid cycle
1272 from mercurial import help as helpmod # avoid cycle
1272 from mercurial import help as helpmod # avoid cycle
1273
1273
1274 topicname = req.form.get('node', [None])[0]
1274 topicname = req.form.get('node', [None])[0]
1275 if not topicname:
1275 if not topicname:
1276 def topics(**map):
1276 def topics(**map):
1277 for entries, summary, _doc in helpmod.helptable:
1277 for entries, summary, _doc in helpmod.helptable:
1278 yield {'topic': entries[0], 'summary': summary}
1278 yield {'topic': entries[0], 'summary': summary}
1279
1279
1280 early, other = [], []
1280 early, other = [], []
1281 primary = lambda s: s.split('|')[0]
1281 primary = lambda s: s.split('|')[0]
1282 for c, e in commands.table.iteritems():
1282 for c, e in commands.table.iteritems():
1283 doc = _getdoc(e)
1283 doc = _getdoc(e)
1284 if 'DEPRECATED' in doc or c.startswith('debug'):
1284 if 'DEPRECATED' in doc or c.startswith('debug'):
1285 continue
1285 continue
1286 cmd = primary(c)
1286 cmd = primary(c)
1287 if cmd.startswith('^'):
1287 if cmd.startswith('^'):
1288 early.append((cmd[1:], doc))
1288 early.append((cmd[1:], doc))
1289 else:
1289 else:
1290 other.append((cmd, doc))
1290 other.append((cmd, doc))
1291
1291
1292 early.sort()
1292 early.sort()
1293 other.sort()
1293 other.sort()
1294
1294
1295 def earlycommands(**map):
1295 def earlycommands(**map):
1296 for c, doc in early:
1296 for c, doc in early:
1297 yield {'topic': c, 'summary': doc}
1297 yield {'topic': c, 'summary': doc}
1298
1298
1299 def othercommands(**map):
1299 def othercommands(**map):
1300 for c, doc in other:
1300 for c, doc in other:
1301 yield {'topic': c, 'summary': doc}
1301 yield {'topic': c, 'summary': doc}
1302
1302
1303 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1303 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1304 othercommands=othercommands, title='Index')
1304 othercommands=othercommands, title='Index')
1305
1305
1306 u = webutil.wsgiui()
1306 u = webutil.wsgiui()
1307 u.verbose = True
1307 u.verbose = True
1308 try:
1308 try:
1309 doc = helpmod.help_(u, topicname)
1309 doc = helpmod.help_(u, topicname)
1310 except error.UnknownCommand:
1310 except error.UnknownCommand:
1311 raise ErrorResponse(HTTP_NOT_FOUND)
1311 raise ErrorResponse(HTTP_NOT_FOUND)
1312 return tmpl('help', topic=topicname, doc=doc)
1312 return tmpl('help', topic=topicname, doc=doc)
1313
1313
1314 # tell hggettext to extract docstrings from these functions:
1314 # tell hggettext to extract docstrings from these functions:
1315 i18nfunctions = commands.values()
1315 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now