##// END OF EJS Templates
hgweb: port most @webcommand to use modern response type...
Gregory Szorc -
r36887:9fc3d814 default
parent child Browse files
Show More
@@ -1,97 +1,99 b''
1 # highlight - syntax highlighting in hgweb, based on Pygments
1 # highlight - syntax highlighting in hgweb, based on Pygments
2 #
2 #
3 # Copyright 2008, 2009 Patrick Mezard <pmezard@gmail.com> and others
3 # Copyright 2008, 2009 Patrick Mezard <pmezard@gmail.com> and others
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 # The original module was split in an interface and an implementation
8 # The original module was split in an interface and an implementation
9 # file to defer pygments loading and speedup extension setup.
9 # file to defer pygments loading and speedup extension setup.
10
10
11 """syntax highlighting for hgweb (requires Pygments)
11 """syntax highlighting for hgweb (requires Pygments)
12
12
13 It depends on the Pygments syntax highlighting library:
13 It depends on the Pygments syntax highlighting library:
14 http://pygments.org/
14 http://pygments.org/
15
15
16 There are the following configuration options::
16 There are the following configuration options::
17
17
18 [web]
18 [web]
19 pygments_style = <style> (default: colorful)
19 pygments_style = <style> (default: colorful)
20 highlightfiles = <fileset> (default: size('<5M'))
20 highlightfiles = <fileset> (default: size('<5M'))
21 highlightonlymatchfilename = <bool> (default False)
21 highlightonlymatchfilename = <bool> (default False)
22
22
23 ``highlightonlymatchfilename`` will only highlight files if their type could
23 ``highlightonlymatchfilename`` will only highlight files if their type could
24 be identified by their filename. When this is not enabled (the default),
24 be identified by their filename. When this is not enabled (the default),
25 Pygments will try very hard to identify the file type from content and any
25 Pygments will try very hard to identify the file type from content and any
26 match (even matches with a low confidence score) will be used.
26 match (even matches with a low confidence score) will be used.
27 """
27 """
28
28
29 from __future__ import absolute_import
29 from __future__ import absolute_import
30
30
31 from . import highlight
31 from . import highlight
32 from mercurial.hgweb import (
32 from mercurial.hgweb import (
33 common,
34 webcommands,
33 webcommands,
35 webutil,
34 webutil,
36 )
35 )
37
36
38 from mercurial import (
37 from mercurial import (
39 encoding,
38 encoding,
40 extensions,
39 extensions,
41 fileset,
40 fileset,
42 )
41 )
43
42
44 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
43 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
45 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 # be specifying the version(s) of Mercurial they are tested with, or
45 # be specifying the version(s) of Mercurial they are tested with, or
47 # leave the attribute unspecified.
46 # leave the attribute unspecified.
48 testedwith = 'ships-with-hg-core'
47 testedwith = 'ships-with-hg-core'
49
48
50 def pygmentize(web, field, fctx, tmpl):
49 def pygmentize(web, field, fctx, tmpl):
51 style = web.config('web', 'pygments_style', 'colorful')
50 style = web.config('web', 'pygments_style', 'colorful')
52 expr = web.config('web', 'highlightfiles', "size('<5M')")
51 expr = web.config('web', 'highlightfiles', "size('<5M')")
53 filenameonly = web.configbool('web', 'highlightonlymatchfilename', False)
52 filenameonly = web.configbool('web', 'highlightonlymatchfilename', False)
54
53
55 ctx = fctx.changectx()
54 ctx = fctx.changectx()
56 tree = fileset.parse(expr)
55 tree = fileset.parse(expr)
57 mctx = fileset.matchctx(ctx, subset=[fctx.path()], status=None)
56 mctx = fileset.matchctx(ctx, subset=[fctx.path()], status=None)
58 if fctx.path() in fileset.getset(mctx, tree):
57 if fctx.path() in fileset.getset(mctx, tree):
59 highlight.pygmentize(field, fctx, style, tmpl,
58 highlight.pygmentize(field, fctx, style, tmpl,
60 guessfilenameonly=filenameonly)
59 guessfilenameonly=filenameonly)
61
60
62 def filerevision_highlight(orig, web, req, tmpl, fctx):
61 def filerevision_highlight(orig, web, req, tmpl, fctx):
63 mt = ''.join(tmpl('mimetype', encoding=encoding.encoding))
62 mt = ''.join(tmpl('mimetype', encoding=encoding.encoding))
64 # only pygmentize for mimetype containing 'html' so we both match
63 # only pygmentize for mimetype containing 'html' so we both match
65 # 'text/html' and possibly 'application/xhtml+xml' in the future
64 # 'text/html' and possibly 'application/xhtml+xml' in the future
66 # so that we don't have to touch the extension when the mimetype
65 # so that we don't have to touch the extension when the mimetype
67 # for a template changes; also hgweb optimizes the case that a
66 # for a template changes; also hgweb optimizes the case that a
68 # raw file is sent using rawfile() and doesn't call us, so we
67 # raw file is sent using rawfile() and doesn't call us, so we
69 # can't clash with the file's content-type here in case we
68 # can't clash with the file's content-type here in case we
70 # pygmentize a html file
69 # pygmentize a html file
71 if 'html' in mt:
70 if 'html' in mt:
72 pygmentize(web, 'fileline', fctx, tmpl)
71 pygmentize(web, 'fileline', fctx, tmpl)
73
72
74 return orig(web, req, tmpl, fctx)
73 return orig(web, req, tmpl, fctx)
75
74
76 def annotate_highlight(orig, web, req, tmpl):
75 def annotate_highlight(orig, web, req, tmpl):
77 mt = ''.join(tmpl('mimetype', encoding=encoding.encoding))
76 mt = ''.join(tmpl('mimetype', encoding=encoding.encoding))
78 if 'html' in mt:
77 if 'html' in mt:
79 fctx = webutil.filectx(web.repo, req)
78 fctx = webutil.filectx(web.repo, req)
80 pygmentize(web, 'annotateline', fctx, tmpl)
79 pygmentize(web, 'annotateline', fctx, tmpl)
81
80
82 return orig(web, req, tmpl)
81 return orig(web, req, tmpl)
83
82
84 def generate_css(web, req, tmpl):
83 def generate_css(web, req, tmpl):
85 pg_style = web.config('web', 'pygments_style', 'colorful')
84 pg_style = web.config('web', 'pygments_style', 'colorful')
86 fmter = highlight.HtmlFormatter(style=pg_style)
85 fmter = highlight.HtmlFormatter(style=pg_style)
87 req.respond(common.HTTP_OK, 'text/css')
86 web.res.headers['Content-Type'] = 'text/css'
88 return ['/* pygments_style = %s */\n\n' % pg_style,
87 web.res.setbodybytes(''.join([
89 fmter.get_style_defs('')]
88 '/* pygments_style = %s */\n\n' % pg_style,
89 fmter.get_style_defs(''),
90 ]))
91 return web.res
90
92
91 def extsetup():
93 def extsetup():
92 # monkeypatch in the new version
94 # monkeypatch in the new version
93 extensions.wrapfunction(webcommands, '_filerevision',
95 extensions.wrapfunction(webcommands, '_filerevision',
94 filerevision_highlight)
96 filerevision_highlight)
95 extensions.wrapfunction(webcommands, 'annotate', annotate_highlight)
97 extensions.wrapfunction(webcommands, 'annotate', annotate_highlight)
96 webcommands.highlightcss = generate_css
98 webcommands.highlightcss = generate_css
97 webcommands.__all__.append('highlightcss')
99 webcommands.__all__.append('highlightcss')
@@ -1,458 +1,461 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 HTTP_NOT_FOUND,
17 HTTP_NOT_FOUND,
18 HTTP_NOT_MODIFIED,
18 HTTP_NOT_MODIFIED,
19 HTTP_OK,
19 HTTP_OK,
20 HTTP_SERVER_ERROR,
20 HTTP_SERVER_ERROR,
21 cspvalues,
21 cspvalues,
22 permhooks,
22 permhooks,
23 )
23 )
24
24
25 from .. import (
25 from .. import (
26 encoding,
26 encoding,
27 error,
27 error,
28 formatter,
28 formatter,
29 hg,
29 hg,
30 hook,
30 hook,
31 profiling,
31 profiling,
32 pycompat,
32 pycompat,
33 repoview,
33 repoview,
34 templatefilters,
34 templatefilters,
35 templater,
35 templater,
36 ui as uimod,
36 ui as uimod,
37 util,
37 util,
38 wireprotoserver,
38 wireprotoserver,
39 )
39 )
40
40
41 from . import (
41 from . import (
42 request as requestmod,
42 request as requestmod,
43 webcommands,
43 webcommands,
44 webutil,
44 webutil,
45 wsgicgi,
45 wsgicgi,
46 )
46 )
47
47
48 archivespecs = util.sortdict((
48 archivespecs = util.sortdict((
49 ('zip', ('application/zip', 'zip', '.zip', None)),
49 ('zip', ('application/zip', 'zip', '.zip', None)),
50 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
50 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
51 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
51 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
52 ))
52 ))
53
53
54 def getstyle(req, configfn, templatepath):
54 def getstyle(req, configfn, templatepath):
55 styles = (
55 styles = (
56 req.qsparams.get('style', None),
56 req.qsparams.get('style', None),
57 configfn('web', 'style'),
57 configfn('web', 'style'),
58 'paper',
58 'paper',
59 )
59 )
60 return styles, templater.stylemap(styles, templatepath)
60 return styles, templater.stylemap(styles, templatepath)
61
61
62 def makebreadcrumb(url, prefix=''):
62 def makebreadcrumb(url, prefix=''):
63 '''Return a 'URL breadcrumb' list
63 '''Return a 'URL breadcrumb' list
64
64
65 A 'URL breadcrumb' is a list of URL-name pairs,
65 A 'URL breadcrumb' is a list of URL-name pairs,
66 corresponding to each of the path items on a URL.
66 corresponding to each of the path items on a URL.
67 This can be used to create path navigation entries.
67 This can be used to create path navigation entries.
68 '''
68 '''
69 if url.endswith('/'):
69 if url.endswith('/'):
70 url = url[:-1]
70 url = url[:-1]
71 if prefix:
71 if prefix:
72 url = '/' + prefix + url
72 url = '/' + prefix + url
73 relpath = url
73 relpath = url
74 if relpath.startswith('/'):
74 if relpath.startswith('/'):
75 relpath = relpath[1:]
75 relpath = relpath[1:]
76
76
77 breadcrumb = []
77 breadcrumb = []
78 urlel = url
78 urlel = url
79 pathitems = [''] + relpath.split('/')
79 pathitems = [''] + relpath.split('/')
80 for pathel in reversed(pathitems):
80 for pathel in reversed(pathitems):
81 if not pathel or not urlel:
81 if not pathel or not urlel:
82 break
82 break
83 breadcrumb.append({'url': urlel, 'name': pathel})
83 breadcrumb.append({'url': urlel, 'name': pathel})
84 urlel = os.path.dirname(urlel)
84 urlel = os.path.dirname(urlel)
85 return reversed(breadcrumb)
85 return reversed(breadcrumb)
86
86
87 class requestcontext(object):
87 class requestcontext(object):
88 """Holds state/context for an individual request.
88 """Holds state/context for an individual request.
89
89
90 Servers can be multi-threaded. Holding state on the WSGI application
90 Servers can be multi-threaded. Holding state on the WSGI application
91 is prone to race conditions. Instances of this class exist to hold
91 is prone to race conditions. Instances of this class exist to hold
92 mutable and race-free state for requests.
92 mutable and race-free state for requests.
93 """
93 """
94 def __init__(self, app, repo, req, res):
94 def __init__(self, app, repo, req, res):
95 self.repo = repo
95 self.repo = repo
96 self.reponame = app.reponame
96 self.reponame = app.reponame
97 self.req = req
97 self.req = req
98 self.res = res
98 self.res = res
99
99
100 self.archivespecs = archivespecs
100 self.archivespecs = archivespecs
101
101
102 self.maxchanges = self.configint('web', 'maxchanges')
102 self.maxchanges = self.configint('web', 'maxchanges')
103 self.stripecount = self.configint('web', 'stripes')
103 self.stripecount = self.configint('web', 'stripes')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
104 self.maxshortchanges = self.configint('web', 'maxshortchanges')
105 self.maxfiles = self.configint('web', 'maxfiles')
105 self.maxfiles = self.configint('web', 'maxfiles')
106 self.allowpull = self.configbool('web', 'allow-pull')
106 self.allowpull = self.configbool('web', 'allow-pull')
107
107
108 # we use untrusted=False to prevent a repo owner from using
108 # we use untrusted=False to prevent a repo owner from using
109 # web.templates in .hg/hgrc to get access to any file readable
109 # web.templates in .hg/hgrc to get access to any file readable
110 # by the user running the CGI script
110 # by the user running the CGI script
111 self.templatepath = self.config('web', 'templates', untrusted=False)
111 self.templatepath = self.config('web', 'templates', untrusted=False)
112
112
113 # This object is more expensive to build than simple config values.
113 # This object is more expensive to build than simple config values.
114 # It is shared across requests. The app will replace the object
114 # It is shared across requests. The app will replace the object
115 # if it is updated. Since this is a reference and nothing should
115 # if it is updated. Since this is a reference and nothing should
116 # modify the underlying object, it should be constant for the lifetime
116 # modify the underlying object, it should be constant for the lifetime
117 # of the request.
117 # of the request.
118 self.websubtable = app.websubtable
118 self.websubtable = app.websubtable
119
119
120 self.csp, self.nonce = cspvalues(self.repo.ui)
120 self.csp, self.nonce = cspvalues(self.repo.ui)
121
121
122 # Trust the settings from the .hg/hgrc files by default.
122 # Trust the settings from the .hg/hgrc files by default.
123 def config(self, section, name, default=uimod._unset, untrusted=True):
123 def config(self, section, name, default=uimod._unset, untrusted=True):
124 return self.repo.ui.config(section, name, default,
124 return self.repo.ui.config(section, name, default,
125 untrusted=untrusted)
125 untrusted=untrusted)
126
126
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
127 def configbool(self, section, name, default=uimod._unset, untrusted=True):
128 return self.repo.ui.configbool(section, name, default,
128 return self.repo.ui.configbool(section, name, default,
129 untrusted=untrusted)
129 untrusted=untrusted)
130
130
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
131 def configint(self, section, name, default=uimod._unset, untrusted=True):
132 return self.repo.ui.configint(section, name, default,
132 return self.repo.ui.configint(section, name, default,
133 untrusted=untrusted)
133 untrusted=untrusted)
134
134
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
135 def configlist(self, section, name, default=uimod._unset, untrusted=True):
136 return self.repo.ui.configlist(section, name, default,
136 return self.repo.ui.configlist(section, name, default,
137 untrusted=untrusted)
137 untrusted=untrusted)
138
138
139 def archivelist(self, nodeid):
139 def archivelist(self, nodeid):
140 allowed = self.configlist('web', 'allow_archive')
140 allowed = self.configlist('web', 'allow_archive')
141 for typ, spec in self.archivespecs.iteritems():
141 for typ, spec in self.archivespecs.iteritems():
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
142 if typ in allowed or self.configbool('web', 'allow%s' % typ):
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
144
144
145 def templater(self, req):
145 def templater(self, req):
146 # determine scheme, port and server name
146 # determine scheme, port and server name
147 # this is needed to create absolute urls
147 # this is needed to create absolute urls
148 logourl = self.config('web', 'logourl')
148 logourl = self.config('web', 'logourl')
149 logoimg = self.config('web', 'logoimg')
149 logoimg = self.config('web', 'logoimg')
150 staticurl = (self.config('web', 'staticurl')
150 staticurl = (self.config('web', 'staticurl')
151 or req.apppath + '/static/')
151 or req.apppath + '/static/')
152 if not staticurl.endswith('/'):
152 if not staticurl.endswith('/'):
153 staticurl += '/'
153 staticurl += '/'
154
154
155 # some functions for the templater
155 # some functions for the templater
156
156
157 def motd(**map):
157 def motd(**map):
158 yield self.config('web', 'motd')
158 yield self.config('web', 'motd')
159
159
160 # figure out which style to use
160 # figure out which style to use
161
161
162 vars = {}
162 vars = {}
163 styles, (style, mapfile) = getstyle(req, self.config,
163 styles, (style, mapfile) = getstyle(req, self.config,
164 self.templatepath)
164 self.templatepath)
165 if style == styles[0]:
165 if style == styles[0]:
166 vars['style'] = style
166 vars['style'] = style
167
167
168 sessionvars = webutil.sessionvars(vars, '?')
168 sessionvars = webutil.sessionvars(vars, '?')
169
169
170 if not self.reponame:
170 if not self.reponame:
171 self.reponame = (self.config('web', 'name', '')
171 self.reponame = (self.config('web', 'name', '')
172 or req.reponame
172 or req.reponame
173 or req.apppath
173 or req.apppath
174 or self.repo.root)
174 or self.repo.root)
175
175
176 def websubfilter(text):
176 def websubfilter(text):
177 return templatefilters.websub(text, self.websubtable)
177 return templatefilters.websub(text, self.websubtable)
178
178
179 # create the templater
179 # create the templater
180 # TODO: export all keywords: defaults = templatekw.keywords.copy()
180 # TODO: export all keywords: defaults = templatekw.keywords.copy()
181 defaults = {
181 defaults = {
182 'url': req.apppath + '/',
182 'url': req.apppath + '/',
183 'logourl': logourl,
183 'logourl': logourl,
184 'logoimg': logoimg,
184 'logoimg': logoimg,
185 'staticurl': staticurl,
185 'staticurl': staticurl,
186 'urlbase': req.advertisedbaseurl,
186 'urlbase': req.advertisedbaseurl,
187 'repo': self.reponame,
187 'repo': self.reponame,
188 'encoding': encoding.encoding,
188 'encoding': encoding.encoding,
189 'motd': motd,
189 'motd': motd,
190 'sessionvars': sessionvars,
190 'sessionvars': sessionvars,
191 'pathdef': makebreadcrumb(req.apppath),
191 'pathdef': makebreadcrumb(req.apppath),
192 'style': style,
192 'style': style,
193 'nonce': self.nonce,
193 'nonce': self.nonce,
194 }
194 }
195 tres = formatter.templateresources(self.repo.ui, self.repo)
195 tres = formatter.templateresources(self.repo.ui, self.repo)
196 tmpl = templater.templater.frommapfile(mapfile,
196 tmpl = templater.templater.frommapfile(mapfile,
197 filters={'websub': websubfilter},
197 filters={'websub': websubfilter},
198 defaults=defaults,
198 defaults=defaults,
199 resources=tres)
199 resources=tres)
200 return tmpl
200 return tmpl
201
201
202
202
203 class hgweb(object):
203 class hgweb(object):
204 """HTTP server for individual repositories.
204 """HTTP server for individual repositories.
205
205
206 Instances of this class serve HTTP responses for a particular
206 Instances of this class serve HTTP responses for a particular
207 repository.
207 repository.
208
208
209 Instances are typically used as WSGI applications.
209 Instances are typically used as WSGI applications.
210
210
211 Some servers are multi-threaded. On these servers, there may
211 Some servers are multi-threaded. On these servers, there may
212 be multiple active threads inside __call__.
212 be multiple active threads inside __call__.
213 """
213 """
214 def __init__(self, repo, name=None, baseui=None):
214 def __init__(self, repo, name=None, baseui=None):
215 if isinstance(repo, str):
215 if isinstance(repo, str):
216 if baseui:
216 if baseui:
217 u = baseui.copy()
217 u = baseui.copy()
218 else:
218 else:
219 u = uimod.ui.load()
219 u = uimod.ui.load()
220 r = hg.repository(u, repo)
220 r = hg.repository(u, repo)
221 else:
221 else:
222 # we trust caller to give us a private copy
222 # we trust caller to give us a private copy
223 r = repo
223 r = repo
224
224
225 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
225 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
227 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
227 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
229 # resolve file patterns relative to repo root
229 # resolve file patterns relative to repo root
230 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
230 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
232 # displaying bundling progress bar while serving feel wrong and may
232 # displaying bundling progress bar while serving feel wrong and may
233 # break some wsgi implementation.
233 # break some wsgi implementation.
234 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
234 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
235 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
235 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
236 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
236 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
237 self._lastrepo = self._repos[0]
237 self._lastrepo = self._repos[0]
238 hook.redirect(True)
238 hook.redirect(True)
239 self.reponame = name
239 self.reponame = name
240
240
241 def _webifyrepo(self, repo):
241 def _webifyrepo(self, repo):
242 repo = getwebview(repo)
242 repo = getwebview(repo)
243 self.websubtable = webutil.getwebsubs(repo)
243 self.websubtable = webutil.getwebsubs(repo)
244 return repo
244 return repo
245
245
246 @contextlib.contextmanager
246 @contextlib.contextmanager
247 def _obtainrepo(self):
247 def _obtainrepo(self):
248 """Obtain a repo unique to the caller.
248 """Obtain a repo unique to the caller.
249
249
250 Internally we maintain a stack of cachedlocalrepo instances
250 Internally we maintain a stack of cachedlocalrepo instances
251 to be handed out. If one is available, we pop it and return it,
251 to be handed out. If one is available, we pop it and return it,
252 ensuring it is up to date in the process. If one is not available,
252 ensuring it is up to date in the process. If one is not available,
253 we clone the most recently used repo instance and return it.
253 we clone the most recently used repo instance and return it.
254
254
255 It is currently possible for the stack to grow without bounds
255 It is currently possible for the stack to grow without bounds
256 if the server allows infinite threads. However, servers should
256 if the server allows infinite threads. However, servers should
257 have a thread limit, thus establishing our limit.
257 have a thread limit, thus establishing our limit.
258 """
258 """
259 if self._repos:
259 if self._repos:
260 cached = self._repos.pop()
260 cached = self._repos.pop()
261 r, created = cached.fetch()
261 r, created = cached.fetch()
262 else:
262 else:
263 cached = self._lastrepo.copy()
263 cached = self._lastrepo.copy()
264 r, created = cached.fetch()
264 r, created = cached.fetch()
265 if created:
265 if created:
266 r = self._webifyrepo(r)
266 r = self._webifyrepo(r)
267
267
268 self._lastrepo = cached
268 self._lastrepo = cached
269 self.mtime = cached.mtime
269 self.mtime = cached.mtime
270 try:
270 try:
271 yield r
271 yield r
272 finally:
272 finally:
273 self._repos.append(cached)
273 self._repos.append(cached)
274
274
275 def run(self):
275 def run(self):
276 """Start a server from CGI environment.
276 """Start a server from CGI environment.
277
277
278 Modern servers should be using WSGI and should avoid this
278 Modern servers should be using WSGI and should avoid this
279 method, if possible.
279 method, if possible.
280 """
280 """
281 if not encoding.environ.get('GATEWAY_INTERFACE',
281 if not encoding.environ.get('GATEWAY_INTERFACE',
282 '').startswith("CGI/1."):
282 '').startswith("CGI/1."):
283 raise RuntimeError("This function is only intended to be "
283 raise RuntimeError("This function is only intended to be "
284 "called while running as a CGI script.")
284 "called while running as a CGI script.")
285 wsgicgi.launch(self)
285 wsgicgi.launch(self)
286
286
287 def __call__(self, env, respond):
287 def __call__(self, env, respond):
288 """Run the WSGI application.
288 """Run the WSGI application.
289
289
290 This may be called by multiple threads.
290 This may be called by multiple threads.
291 """
291 """
292 req = requestmod.wsgirequest(env, respond)
292 req = requestmod.wsgirequest(env, respond)
293 return self.run_wsgi(req)
293 return self.run_wsgi(req)
294
294
295 def run_wsgi(self, wsgireq):
295 def run_wsgi(self, wsgireq):
296 """Internal method to run the WSGI application.
296 """Internal method to run the WSGI application.
297
297
298 This is typically only called by Mercurial. External consumers
298 This is typically only called by Mercurial. External consumers
299 should be using instances of this class as the WSGI application.
299 should be using instances of this class as the WSGI application.
300 """
300 """
301 with self._obtainrepo() as repo:
301 with self._obtainrepo() as repo:
302 profile = repo.ui.configbool('profiling', 'enabled')
302 profile = repo.ui.configbool('profiling', 'enabled')
303 with profiling.profile(repo.ui, enabled=profile):
303 with profiling.profile(repo.ui, enabled=profile):
304 for r in self._runwsgi(wsgireq, repo):
304 for r in self._runwsgi(wsgireq, repo):
305 yield r
305 yield r
306
306
307 def _runwsgi(self, wsgireq, repo):
307 def _runwsgi(self, wsgireq, repo):
308 req = wsgireq.req
308 req = wsgireq.req
309 res = wsgireq.res
309 res = wsgireq.res
310 rctx = requestcontext(self, repo, req, res)
310 rctx = requestcontext(self, repo, req, res)
311
311
312 # This state is global across all threads.
312 # This state is global across all threads.
313 encoding.encoding = rctx.config('web', 'encoding')
313 encoding.encoding = rctx.config('web', 'encoding')
314 rctx.repo.ui.environ = wsgireq.env
314 rctx.repo.ui.environ = wsgireq.env
315
315
316 if rctx.csp:
316 if rctx.csp:
317 # hgwebdir may have added CSP header. Since we generate our own,
317 # hgwebdir may have added CSP header. Since we generate our own,
318 # replace it.
318 # replace it.
319 wsgireq.headers = [h for h in wsgireq.headers
319 wsgireq.headers = [h for h in wsgireq.headers
320 if h[0] != 'Content-Security-Policy']
320 if h[0] != 'Content-Security-Policy']
321 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
321 wsgireq.headers.append(('Content-Security-Policy', rctx.csp))
322 res.headers['Content-Security-Policy'] = rctx.csp
322 res.headers['Content-Security-Policy'] = rctx.csp
323
323
324 handled = wireprotoserver.handlewsgirequest(
324 handled = wireprotoserver.handlewsgirequest(
325 rctx, wsgireq, req, res, self.check_perm)
325 rctx, wsgireq, req, res, self.check_perm)
326 if handled:
326 if handled:
327 return res.sendresponse()
327 return res.sendresponse()
328
328
329 if req.havepathinfo:
329 if req.havepathinfo:
330 query = req.dispatchpath
330 query = req.dispatchpath
331 else:
331 else:
332 query = req.querystring.partition('&')[0].partition(';')[0]
332 query = req.querystring.partition('&')[0].partition(';')[0]
333
333
334 # translate user-visible url structure to internal structure
334 # translate user-visible url structure to internal structure
335
335
336 args = query.split('/', 2)
336 args = query.split('/', 2)
337 if 'cmd' not in req.qsparams and args and args[0]:
337 if 'cmd' not in req.qsparams and args and args[0]:
338 cmd = args.pop(0)
338 cmd = args.pop(0)
339 style = cmd.rfind('-')
339 style = cmd.rfind('-')
340 if style != -1:
340 if style != -1:
341 req.qsparams['style'] = cmd[:style]
341 req.qsparams['style'] = cmd[:style]
342 cmd = cmd[style + 1:]
342 cmd = cmd[style + 1:]
343
343
344 # avoid accepting e.g. style parameter as command
344 # avoid accepting e.g. style parameter as command
345 if util.safehasattr(webcommands, cmd):
345 if util.safehasattr(webcommands, cmd):
346 req.qsparams['cmd'] = cmd
346 req.qsparams['cmd'] = cmd
347
347
348 if cmd == 'static':
348 if cmd == 'static':
349 req.qsparams['file'] = '/'.join(args)
349 req.qsparams['file'] = '/'.join(args)
350 else:
350 else:
351 if args and args[0]:
351 if args and args[0]:
352 node = args.pop(0).replace('%2F', '/')
352 node = args.pop(0).replace('%2F', '/')
353 req.qsparams['node'] = node
353 req.qsparams['node'] = node
354 if args:
354 if args:
355 if 'file' in req.qsparams:
355 if 'file' in req.qsparams:
356 del req.qsparams['file']
356 del req.qsparams['file']
357 for a in args:
357 for a in args:
358 req.qsparams.add('file', a)
358 req.qsparams.add('file', a)
359
359
360 ua = req.headers.get('User-Agent', '')
360 ua = req.headers.get('User-Agent', '')
361 if cmd == 'rev' and 'mercurial' in ua:
361 if cmd == 'rev' and 'mercurial' in ua:
362 req.qsparams['style'] = 'raw'
362 req.qsparams['style'] = 'raw'
363
363
364 if cmd == 'archive':
364 if cmd == 'archive':
365 fn = req.qsparams['node']
365 fn = req.qsparams['node']
366 for type_, spec in rctx.archivespecs.iteritems():
366 for type_, spec in rctx.archivespecs.iteritems():
367 ext = spec[2]
367 ext = spec[2]
368 if fn.endswith(ext):
368 if fn.endswith(ext):
369 req.qsparams['node'] = fn[:-len(ext)]
369 req.qsparams['node'] = fn[:-len(ext)]
370 req.qsparams['type'] = type_
370 req.qsparams['type'] = type_
371 else:
371 else:
372 cmd = req.qsparams.get('cmd', '')
372 cmd = req.qsparams.get('cmd', '')
373
373
374 # process the web interface request
374 # process the web interface request
375
375
376 try:
376 try:
377 tmpl = rctx.templater(req)
377 tmpl = rctx.templater(req)
378 ctype = tmpl('mimetype', encoding=encoding.encoding)
378 ctype = tmpl('mimetype', encoding=encoding.encoding)
379 ctype = templater.stringify(ctype)
379 ctype = templater.stringify(ctype)
380
380
381 # check read permissions non-static content
381 # check read permissions non-static content
382 if cmd != 'static':
382 if cmd != 'static':
383 self.check_perm(rctx, wsgireq, None)
383 self.check_perm(rctx, wsgireq, None)
384
384
385 if cmd == '':
385 if cmd == '':
386 req.qsparams['cmd'] = tmpl.cache['default']
386 req.qsparams['cmd'] = tmpl.cache['default']
387 cmd = req.qsparams['cmd']
387 cmd = req.qsparams['cmd']
388
388
389 # Don't enable caching if using a CSP nonce because then it wouldn't
389 # Don't enable caching if using a CSP nonce because then it wouldn't
390 # be a nonce.
390 # be a nonce.
391 if rctx.configbool('web', 'cache') and not rctx.nonce:
391 if rctx.configbool('web', 'cache') and not rctx.nonce:
392 tag = 'W/"%d"' % self.mtime
392 tag = 'W/"%d"' % self.mtime
393 if req.headers.get('If-None-Match') == tag:
393 if req.headers.get('If-None-Match') == tag:
394 raise ErrorResponse(HTTP_NOT_MODIFIED)
394 raise ErrorResponse(HTTP_NOT_MODIFIED)
395
395
396 wsgireq.headers.append((r'ETag', pycompat.sysstr(tag)))
396 wsgireq.headers.append((r'ETag', pycompat.sysstr(tag)))
397 res.headers['ETag'] = tag
397 res.headers['ETag'] = tag
398
398
399 if cmd not in webcommands.__all__:
399 if cmd not in webcommands.__all__:
400 msg = 'no such method: %s' % cmd
400 msg = 'no such method: %s' % cmd
401 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
401 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
402 elif cmd == 'file' and req.qsparams.get('style') == 'raw':
402 elif cmd == 'file' and req.qsparams.get('style') == 'raw':
403 rctx.ctype = ctype
403 res.status = '200 Script output follows'
404 res.headers['Content-Type'] = ctype
404 content = webcommands.rawfile(rctx, wsgireq, tmpl)
405 content = webcommands.rawfile(rctx, wsgireq, tmpl)
406 assert content is res
407 return res.sendresponse()
405 else:
408 else:
406 # Set some globals appropriate for web handlers. Commands can
409 # Set some globals appropriate for web handlers. Commands can
407 # override easily enough.
410 # override easily enough.
408 res.status = '200 Script output follows'
411 res.status = '200 Script output follows'
409 res.headers['Content-Type'] = ctype
412 res.headers['Content-Type'] = ctype
410 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
413 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
411
414
412 if content is res:
415 if content is res:
413 return res.sendresponse()
416 return res.sendresponse()
414
417
415 wsgireq.respond(HTTP_OK, ctype)
418 wsgireq.respond(HTTP_OK, ctype)
416
419
417 return content
420 return content
418
421
419 except (error.LookupError, error.RepoLookupError) as err:
422 except (error.LookupError, error.RepoLookupError) as err:
420 wsgireq.respond(HTTP_NOT_FOUND, ctype)
423 wsgireq.respond(HTTP_NOT_FOUND, ctype)
421 msg = pycompat.bytestr(err)
424 msg = pycompat.bytestr(err)
422 if (util.safehasattr(err, 'name') and
425 if (util.safehasattr(err, 'name') and
423 not isinstance(err, error.ManifestLookupError)):
426 not isinstance(err, error.ManifestLookupError)):
424 msg = 'revision not found: %s' % err.name
427 msg = 'revision not found: %s' % err.name
425 return tmpl('error', error=msg)
428 return tmpl('error', error=msg)
426 except (error.RepoError, error.RevlogError) as inst:
429 except (error.RepoError, error.RevlogError) as inst:
427 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
430 wsgireq.respond(HTTP_SERVER_ERROR, ctype)
428 return tmpl('error', error=pycompat.bytestr(inst))
431 return tmpl('error', error=pycompat.bytestr(inst))
429 except ErrorResponse as inst:
432 except ErrorResponse as inst:
430 wsgireq.respond(inst, ctype)
433 wsgireq.respond(inst, ctype)
431 if inst.code == HTTP_NOT_MODIFIED:
434 if inst.code == HTTP_NOT_MODIFIED:
432 # Not allowed to return a body on a 304
435 # Not allowed to return a body on a 304
433 return ['']
436 return ['']
434 return tmpl('error', error=pycompat.bytestr(inst))
437 return tmpl('error', error=pycompat.bytestr(inst))
435
438
436 def check_perm(self, rctx, req, op):
439 def check_perm(self, rctx, req, op):
437 for permhook in permhooks:
440 for permhook in permhooks:
438 permhook(rctx, req, op)
441 permhook(rctx, req, op)
439
442
440 def getwebview(repo):
443 def getwebview(repo):
441 """The 'web.view' config controls changeset filter to hgweb. Possible
444 """The 'web.view' config controls changeset filter to hgweb. Possible
442 values are ``served``, ``visible`` and ``all``. Default is ``served``.
445 values are ``served``, ``visible`` and ``all``. Default is ``served``.
443 The ``served`` filter only shows changesets that can be pulled from the
446 The ``served`` filter only shows changesets that can be pulled from the
444 hgweb instance. The``visible`` filter includes secret changesets but
447 hgweb instance. The``visible`` filter includes secret changesets but
445 still excludes "hidden" one.
448 still excludes "hidden" one.
446
449
447 See the repoview module for details.
450 See the repoview module for details.
448
451
449 The option has been around undocumented since Mercurial 2.5, but no
452 The option has been around undocumented since Mercurial 2.5, but no
450 user ever asked about it. So we better keep it undocumented for now."""
453 user ever asked about it. So we better keep it undocumented for now."""
451 # experimental config: web.view
454 # experimental config: web.view
452 viewconfig = repo.ui.config('web', 'view', untrusted=True)
455 viewconfig = repo.ui.config('web', 'view', untrusted=True)
453 if viewconfig == 'all':
456 if viewconfig == 'all':
454 return repo.unfiltered()
457 return repo.unfiltered()
455 elif viewconfig in repoview.filtertable:
458 elif viewconfig in repoview.filtertable:
456 return repo.filtered(viewconfig)
459 return repo.filtered(viewconfig)
457 else:
460 else:
458 return repo.filtered('served')
461 return repo.filtered('served')
@@ -1,1424 +1,1502 b''
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import mimetypes
11 import mimetypes
12 import os
12 import os
13 import re
13 import re
14
14
15 from ..i18n import _
15 from ..i18n import _
16 from ..node import hex, nullid, short
16 from ..node import hex, nullid, short
17
17
18 from .common import (
18 from .common import (
19 ErrorResponse,
19 ErrorResponse,
20 HTTP_FORBIDDEN,
20 HTTP_FORBIDDEN,
21 HTTP_NOT_FOUND,
21 HTTP_NOT_FOUND,
22 HTTP_OK,
22 HTTP_OK,
23 get_contact,
23 get_contact,
24 paritygen,
24 paritygen,
25 staticfile,
25 staticfile,
26 )
26 )
27
27
28 from .. import (
28 from .. import (
29 archival,
29 archival,
30 dagop,
30 dagop,
31 encoding,
31 encoding,
32 error,
32 error,
33 graphmod,
33 graphmod,
34 pycompat,
34 pycompat,
35 revset,
35 revset,
36 revsetlang,
36 revsetlang,
37 scmutil,
37 scmutil,
38 smartset,
38 smartset,
39 templater,
39 templater,
40 util,
40 util,
41 )
41 )
42
42
43 from . import (
43 from . import (
44 webutil,
44 webutil,
45 )
45 )
46
46
47 __all__ = []
47 __all__ = []
48 commands = {}
48 commands = {}
49
49
50 class webcommand(object):
50 class webcommand(object):
51 """Decorator used to register a web command handler.
51 """Decorator used to register a web command handler.
52
52
53 The decorator takes as its positional arguments the name/path the
53 The decorator takes as its positional arguments the name/path the
54 command should be accessible under.
54 command should be accessible under.
55
55
56 When called, functions receive as arguments a ``requestcontext``,
56 When called, functions receive as arguments a ``requestcontext``,
57 ``wsgirequest``, and a templater instance for generatoring output.
57 ``wsgirequest``, and a templater instance for generatoring output.
58 The functions should populate the ``rctx.res`` object with details
58 The functions should populate the ``rctx.res`` object with details
59 about the HTTP response.
59 about the HTTP response.
60
60
61 The function can return the ``requestcontext.res`` instance to signal
61 The function can return the ``requestcontext.res`` instance to signal
62 that it wants to use this object to generate the response. If an iterable
62 that it wants to use this object to generate the response. If an iterable
63 is returned, the ``wsgirequest`` instance will be used and the returned
63 is returned, the ``wsgirequest`` instance will be used and the returned
64 content will constitute the response body.
64 content will constitute the response body.
65
65
66 Usage:
66 Usage:
67
67
68 @webcommand('mycommand')
68 @webcommand('mycommand')
69 def mycommand(web, req, tmpl):
69 def mycommand(web, req, tmpl):
70 pass
70 pass
71 """
71 """
72
72
73 def __init__(self, name):
73 def __init__(self, name):
74 self.name = name
74 self.name = name
75
75
76 def __call__(self, func):
76 def __call__(self, func):
77 __all__.append(self.name)
77 __all__.append(self.name)
78 commands[self.name] = func
78 commands[self.name] = func
79 return func
79 return func
80
80
81 @webcommand('log')
81 @webcommand('log')
82 def log(web, req, tmpl):
82 def log(web, req, tmpl):
83 """
83 """
84 /log[/{revision}[/{path}]]
84 /log[/{revision}[/{path}]]
85 --------------------------
85 --------------------------
86
86
87 Show repository or file history.
87 Show repository or file history.
88
88
89 For URLs of the form ``/log/{revision}``, a list of changesets starting at
89 For URLs of the form ``/log/{revision}``, a list of changesets starting at
90 the specified changeset identifier is shown. If ``{revision}`` is not
90 the specified changeset identifier is shown. If ``{revision}`` is not
91 defined, the default is ``tip``. This form is equivalent to the
91 defined, the default is ``tip``. This form is equivalent to the
92 ``changelog`` handler.
92 ``changelog`` handler.
93
93
94 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
94 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
95 file will be shown. This form is equivalent to the ``filelog`` handler.
95 file will be shown. This form is equivalent to the ``filelog`` handler.
96 """
96 """
97
97
98 if req.req.qsparams.get('file'):
98 if req.req.qsparams.get('file'):
99 return filelog(web, req, tmpl)
99 return filelog(web, req, tmpl)
100 else:
100 else:
101 return changelog(web, req, tmpl)
101 return changelog(web, req, tmpl)
102
102
103 @webcommand('rawfile')
103 @webcommand('rawfile')
104 def rawfile(web, req, tmpl):
104 def rawfile(web, req, tmpl):
105 guessmime = web.configbool('web', 'guessmime')
105 guessmime = web.configbool('web', 'guessmime')
106
106
107 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
107 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
108 if not path:
108 if not path:
109 content = manifest(web, req, tmpl)
109 return manifest(web, req, tmpl)
110 req.respond(HTTP_OK, web.ctype)
111 return content
112
110
113 try:
111 try:
114 fctx = webutil.filectx(web.repo, req)
112 fctx = webutil.filectx(web.repo, req)
115 except error.LookupError as inst:
113 except error.LookupError as inst:
116 try:
114 try:
117 content = manifest(web, req, tmpl)
115 return manifest(web, req, tmpl)
118 req.respond(HTTP_OK, web.ctype)
119 return content
120 except ErrorResponse:
116 except ErrorResponse:
121 raise inst
117 raise inst
122
118
123 path = fctx.path()
119 path = fctx.path()
124 text = fctx.data()
120 text = fctx.data()
125 mt = 'application/binary'
121 mt = 'application/binary'
126 if guessmime:
122 if guessmime:
127 mt = mimetypes.guess_type(path)[0]
123 mt = mimetypes.guess_type(path)[0]
128 if mt is None:
124 if mt is None:
129 if util.binary(text):
125 if util.binary(text):
130 mt = 'application/binary'
126 mt = 'application/binary'
131 else:
127 else:
132 mt = 'text/plain'
128 mt = 'text/plain'
133 if mt.startswith('text/'):
129 if mt.startswith('text/'):
134 mt += '; charset="%s"' % encoding.encoding
130 mt += '; charset="%s"' % encoding.encoding
135
131
136 req.respond(HTTP_OK, mt, path, body=text)
132 web.res.headers['Content-Type'] = mt
137 return []
133 filename = (path.rpartition('/')[-1]
134 .replace('\\', '\\\\').replace('"', '\\"'))
135 web.res.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
136 web.res.setbodybytes(text)
137 return web.res
138
138
139 def _filerevision(web, req, tmpl, fctx):
139 def _filerevision(web, req, tmpl, fctx):
140 f = fctx.path()
140 f = fctx.path()
141 text = fctx.data()
141 text = fctx.data()
142 parity = paritygen(web.stripecount)
142 parity = paritygen(web.stripecount)
143 ishead = fctx.filerev() in fctx.filelog().headrevs()
143 ishead = fctx.filerev() in fctx.filelog().headrevs()
144
144
145 if util.binary(text):
145 if util.binary(text):
146 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
146 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
147 text = '(binary:%s)' % mt
147 text = '(binary:%s)' % mt
148
148
149 def lines():
149 def lines():
150 for lineno, t in enumerate(text.splitlines(True)):
150 for lineno, t in enumerate(text.splitlines(True)):
151 yield {"line": t,
151 yield {"line": t,
152 "lineid": "l%d" % (lineno + 1),
152 "lineid": "l%d" % (lineno + 1),
153 "linenumber": "% 6d" % (lineno + 1),
153 "linenumber": "% 6d" % (lineno + 1),
154 "parity": next(parity)}
154 "parity": next(parity)}
155
155
156 return tmpl("filerevision",
156 web.res.setbodygen(tmpl(
157 file=f,
157 'filerevision',
158 path=webutil.up(f),
158 file=f,
159 text=lines(),
159 path=webutil.up(f),
160 symrev=webutil.symrevorshortnode(req, fctx),
160 text=lines(),
161 rename=webutil.renamelink(fctx),
161 symrev=webutil.symrevorshortnode(req, fctx),
162 permissions=fctx.manifest().flags(f),
162 rename=webutil.renamelink(fctx),
163 ishead=int(ishead),
163 permissions=fctx.manifest().flags(f),
164 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
164 ishead=int(ishead),
165 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
166
167 return web.res
165
168
166 @webcommand('file')
169 @webcommand('file')
167 def file(web, req, tmpl):
170 def file(web, req, tmpl):
168 """
171 """
169 /file/{revision}[/{path}]
172 /file/{revision}[/{path}]
170 -------------------------
173 -------------------------
171
174
172 Show information about a directory or file in the repository.
175 Show information about a directory or file in the repository.
173
176
174 Info about the ``path`` given as a URL parameter will be rendered.
177 Info about the ``path`` given as a URL parameter will be rendered.
175
178
176 If ``path`` is a directory, information about the entries in that
179 If ``path`` is a directory, information about the entries in that
177 directory will be rendered. This form is equivalent to the ``manifest``
180 directory will be rendered. This form is equivalent to the ``manifest``
178 handler.
181 handler.
179
182
180 If ``path`` is a file, information about that file will be shown via
183 If ``path`` is a file, information about that file will be shown via
181 the ``filerevision`` template.
184 the ``filerevision`` template.
182
185
183 If ``path`` is not defined, information about the root directory will
186 If ``path`` is not defined, information about the root directory will
184 be rendered.
187 be rendered.
185 """
188 """
186 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
189 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
187 if not path:
190 if not path:
188 return manifest(web, req, tmpl)
191 return manifest(web, req, tmpl)
189 try:
192 try:
190 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
193 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
191 except error.LookupError as inst:
194 except error.LookupError as inst:
192 try:
195 try:
193 return manifest(web, req, tmpl)
196 return manifest(web, req, tmpl)
194 except ErrorResponse:
197 except ErrorResponse:
195 raise inst
198 raise inst
196
199
197 def _search(web, req, tmpl):
200 def _search(web, req, tmpl):
198 MODE_REVISION = 'rev'
201 MODE_REVISION = 'rev'
199 MODE_KEYWORD = 'keyword'
202 MODE_KEYWORD = 'keyword'
200 MODE_REVSET = 'revset'
203 MODE_REVSET = 'revset'
201
204
202 def revsearch(ctx):
205 def revsearch(ctx):
203 yield ctx
206 yield ctx
204
207
205 def keywordsearch(query):
208 def keywordsearch(query):
206 lower = encoding.lower
209 lower = encoding.lower
207 qw = lower(query).split()
210 qw = lower(query).split()
208
211
209 def revgen():
212 def revgen():
210 cl = web.repo.changelog
213 cl = web.repo.changelog
211 for i in xrange(len(web.repo) - 1, 0, -100):
214 for i in xrange(len(web.repo) - 1, 0, -100):
212 l = []
215 l = []
213 for j in cl.revs(max(0, i - 99), i):
216 for j in cl.revs(max(0, i - 99), i):
214 ctx = web.repo[j]
217 ctx = web.repo[j]
215 l.append(ctx)
218 l.append(ctx)
216 l.reverse()
219 l.reverse()
217 for e in l:
220 for e in l:
218 yield e
221 yield e
219
222
220 for ctx in revgen():
223 for ctx in revgen():
221 miss = 0
224 miss = 0
222 for q in qw:
225 for q in qw:
223 if not (q in lower(ctx.user()) or
226 if not (q in lower(ctx.user()) or
224 q in lower(ctx.description()) or
227 q in lower(ctx.description()) or
225 q in lower(" ".join(ctx.files()))):
228 q in lower(" ".join(ctx.files()))):
226 miss = 1
229 miss = 1
227 break
230 break
228 if miss:
231 if miss:
229 continue
232 continue
230
233
231 yield ctx
234 yield ctx
232
235
233 def revsetsearch(revs):
236 def revsetsearch(revs):
234 for r in revs:
237 for r in revs:
235 yield web.repo[r]
238 yield web.repo[r]
236
239
237 searchfuncs = {
240 searchfuncs = {
238 MODE_REVISION: (revsearch, 'exact revision search'),
241 MODE_REVISION: (revsearch, 'exact revision search'),
239 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
242 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
240 MODE_REVSET: (revsetsearch, 'revset expression search'),
243 MODE_REVSET: (revsetsearch, 'revset expression search'),
241 }
244 }
242
245
243 def getsearchmode(query):
246 def getsearchmode(query):
244 try:
247 try:
245 ctx = web.repo[query]
248 ctx = web.repo[query]
246 except (error.RepoError, error.LookupError):
249 except (error.RepoError, error.LookupError):
247 # query is not an exact revision pointer, need to
250 # query is not an exact revision pointer, need to
248 # decide if it's a revset expression or keywords
251 # decide if it's a revset expression or keywords
249 pass
252 pass
250 else:
253 else:
251 return MODE_REVISION, ctx
254 return MODE_REVISION, ctx
252
255
253 revdef = 'reverse(%s)' % query
256 revdef = 'reverse(%s)' % query
254 try:
257 try:
255 tree = revsetlang.parse(revdef)
258 tree = revsetlang.parse(revdef)
256 except error.ParseError:
259 except error.ParseError:
257 # can't parse to a revset tree
260 # can't parse to a revset tree
258 return MODE_KEYWORD, query
261 return MODE_KEYWORD, query
259
262
260 if revsetlang.depth(tree) <= 2:
263 if revsetlang.depth(tree) <= 2:
261 # no revset syntax used
264 # no revset syntax used
262 return MODE_KEYWORD, query
265 return MODE_KEYWORD, query
263
266
264 if any((token, (value or '')[:3]) == ('string', 're:')
267 if any((token, (value or '')[:3]) == ('string', 're:')
265 for token, value, pos in revsetlang.tokenize(revdef)):
268 for token, value, pos in revsetlang.tokenize(revdef)):
266 return MODE_KEYWORD, query
269 return MODE_KEYWORD, query
267
270
268 funcsused = revsetlang.funcsused(tree)
271 funcsused = revsetlang.funcsused(tree)
269 if not funcsused.issubset(revset.safesymbols):
272 if not funcsused.issubset(revset.safesymbols):
270 return MODE_KEYWORD, query
273 return MODE_KEYWORD, query
271
274
272 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
275 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
273 try:
276 try:
274 revs = mfunc(web.repo)
277 revs = mfunc(web.repo)
275 return MODE_REVSET, revs
278 return MODE_REVSET, revs
276 # ParseError: wrongly placed tokens, wrongs arguments, etc
279 # ParseError: wrongly placed tokens, wrongs arguments, etc
277 # RepoLookupError: no such revision, e.g. in 'revision:'
280 # RepoLookupError: no such revision, e.g. in 'revision:'
278 # Abort: bookmark/tag not exists
281 # Abort: bookmark/tag not exists
279 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
282 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
280 except (error.ParseError, error.RepoLookupError, error.Abort,
283 except (error.ParseError, error.RepoLookupError, error.Abort,
281 LookupError):
284 LookupError):
282 return MODE_KEYWORD, query
285 return MODE_KEYWORD, query
283
286
284 def changelist(**map):
287 def changelist(**map):
285 count = 0
288 count = 0
286
289
287 for ctx in searchfunc[0](funcarg):
290 for ctx in searchfunc[0](funcarg):
288 count += 1
291 count += 1
289 n = ctx.node()
292 n = ctx.node()
290 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
293 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
291 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
294 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
292
295
293 yield tmpl('searchentry',
296 yield tmpl('searchentry',
294 parity=next(parity),
297 parity=next(parity),
295 changelogtag=showtags,
298 changelogtag=showtags,
296 files=files,
299 files=files,
297 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
300 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
298
301
299 if count >= revcount:
302 if count >= revcount:
300 break
303 break
301
304
302 query = req.req.qsparams['rev']
305 query = req.req.qsparams['rev']
303 revcount = web.maxchanges
306 revcount = web.maxchanges
304 if 'revcount' in req.req.qsparams:
307 if 'revcount' in req.req.qsparams:
305 try:
308 try:
306 revcount = int(req.req.qsparams.get('revcount', revcount))
309 revcount = int(req.req.qsparams.get('revcount', revcount))
307 revcount = max(revcount, 1)
310 revcount = max(revcount, 1)
308 tmpl.defaults['sessionvars']['revcount'] = revcount
311 tmpl.defaults['sessionvars']['revcount'] = revcount
309 except ValueError:
312 except ValueError:
310 pass
313 pass
311
314
312 lessvars = copy.copy(tmpl.defaults['sessionvars'])
315 lessvars = copy.copy(tmpl.defaults['sessionvars'])
313 lessvars['revcount'] = max(revcount // 2, 1)
316 lessvars['revcount'] = max(revcount // 2, 1)
314 lessvars['rev'] = query
317 lessvars['rev'] = query
315 morevars = copy.copy(tmpl.defaults['sessionvars'])
318 morevars = copy.copy(tmpl.defaults['sessionvars'])
316 morevars['revcount'] = revcount * 2
319 morevars['revcount'] = revcount * 2
317 morevars['rev'] = query
320 morevars['rev'] = query
318
321
319 mode, funcarg = getsearchmode(query)
322 mode, funcarg = getsearchmode(query)
320
323
321 if 'forcekw' in req.req.qsparams:
324 if 'forcekw' in req.req.qsparams:
322 showforcekw = ''
325 showforcekw = ''
323 showunforcekw = searchfuncs[mode][1]
326 showunforcekw = searchfuncs[mode][1]
324 mode = MODE_KEYWORD
327 mode = MODE_KEYWORD
325 funcarg = query
328 funcarg = query
326 else:
329 else:
327 if mode != MODE_KEYWORD:
330 if mode != MODE_KEYWORD:
328 showforcekw = searchfuncs[MODE_KEYWORD][1]
331 showforcekw = searchfuncs[MODE_KEYWORD][1]
329 else:
332 else:
330 showforcekw = ''
333 showforcekw = ''
331 showunforcekw = ''
334 showunforcekw = ''
332
335
333 searchfunc = searchfuncs[mode]
336 searchfunc = searchfuncs[mode]
334
337
335 tip = web.repo['tip']
338 tip = web.repo['tip']
336 parity = paritygen(web.stripecount)
339 parity = paritygen(web.stripecount)
337
340
338 return tmpl('search', query=query, node=tip.hex(), symrev='tip',
341 web.res.setbodygen(tmpl(
339 entries=changelist, archives=web.archivelist("tip"),
342 'search',
340 morevars=morevars, lessvars=lessvars,
343 query=query,
341 modedesc=searchfunc[1],
344 node=tip.hex(),
342 showforcekw=showforcekw, showunforcekw=showunforcekw)
345 symrev='tip',
346 entries=changelist,
347 archives=web.archivelist('tip'),
348 morevars=morevars,
349 lessvars=lessvars,
350 modedesc=searchfunc[1],
351 showforcekw=showforcekw,
352 showunforcekw=showunforcekw))
353
354 return web.res
343
355
344 @webcommand('changelog')
356 @webcommand('changelog')
345 def changelog(web, req, tmpl, shortlog=False):
357 def changelog(web, req, tmpl, shortlog=False):
346 """
358 """
347 /changelog[/{revision}]
359 /changelog[/{revision}]
348 -----------------------
360 -----------------------
349
361
350 Show information about multiple changesets.
362 Show information about multiple changesets.
351
363
352 If the optional ``revision`` URL argument is absent, information about
364 If the optional ``revision`` URL argument is absent, information about
353 all changesets starting at ``tip`` will be rendered. If the ``revision``
365 all changesets starting at ``tip`` will be rendered. If the ``revision``
354 argument is present, changesets will be shown starting from the specified
366 argument is present, changesets will be shown starting from the specified
355 revision.
367 revision.
356
368
357 If ``revision`` is absent, the ``rev`` query string argument may be
369 If ``revision`` is absent, the ``rev`` query string argument may be
358 defined. This will perform a search for changesets.
370 defined. This will perform a search for changesets.
359
371
360 The argument for ``rev`` can be a single revision, a revision set,
372 The argument for ``rev`` can be a single revision, a revision set,
361 or a literal keyword to search for in changeset data (equivalent to
373 or a literal keyword to search for in changeset data (equivalent to
362 :hg:`log -k`).
374 :hg:`log -k`).
363
375
364 The ``revcount`` query string argument defines the maximum numbers of
376 The ``revcount`` query string argument defines the maximum numbers of
365 changesets to render.
377 changesets to render.
366
378
367 For non-searches, the ``changelog`` template will be rendered.
379 For non-searches, the ``changelog`` template will be rendered.
368 """
380 """
369
381
370 query = ''
382 query = ''
371 if 'node' in req.req.qsparams:
383 if 'node' in req.req.qsparams:
372 ctx = webutil.changectx(web.repo, req)
384 ctx = webutil.changectx(web.repo, req)
373 symrev = webutil.symrevorshortnode(req, ctx)
385 symrev = webutil.symrevorshortnode(req, ctx)
374 elif 'rev' in req.req.qsparams:
386 elif 'rev' in req.req.qsparams:
375 return _search(web, req, tmpl)
387 return _search(web, req, tmpl)
376 else:
388 else:
377 ctx = web.repo['tip']
389 ctx = web.repo['tip']
378 symrev = 'tip'
390 symrev = 'tip'
379
391
380 def changelist():
392 def changelist():
381 revs = []
393 revs = []
382 if pos != -1:
394 if pos != -1:
383 revs = web.repo.changelog.revs(pos, 0)
395 revs = web.repo.changelog.revs(pos, 0)
384 curcount = 0
396 curcount = 0
385 for rev in revs:
397 for rev in revs:
386 curcount += 1
398 curcount += 1
387 if curcount > revcount + 1:
399 if curcount > revcount + 1:
388 break
400 break
389
401
390 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
402 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
391 entry['parity'] = next(parity)
403 entry['parity'] = next(parity)
392 yield entry
404 yield entry
393
405
394 if shortlog:
406 if shortlog:
395 revcount = web.maxshortchanges
407 revcount = web.maxshortchanges
396 else:
408 else:
397 revcount = web.maxchanges
409 revcount = web.maxchanges
398
410
399 if 'revcount' in req.req.qsparams:
411 if 'revcount' in req.req.qsparams:
400 try:
412 try:
401 revcount = int(req.req.qsparams.get('revcount', revcount))
413 revcount = int(req.req.qsparams.get('revcount', revcount))
402 revcount = max(revcount, 1)
414 revcount = max(revcount, 1)
403 tmpl.defaults['sessionvars']['revcount'] = revcount
415 tmpl.defaults['sessionvars']['revcount'] = revcount
404 except ValueError:
416 except ValueError:
405 pass
417 pass
406
418
407 lessvars = copy.copy(tmpl.defaults['sessionvars'])
419 lessvars = copy.copy(tmpl.defaults['sessionvars'])
408 lessvars['revcount'] = max(revcount // 2, 1)
420 lessvars['revcount'] = max(revcount // 2, 1)
409 morevars = copy.copy(tmpl.defaults['sessionvars'])
421 morevars = copy.copy(tmpl.defaults['sessionvars'])
410 morevars['revcount'] = revcount * 2
422 morevars['revcount'] = revcount * 2
411
423
412 count = len(web.repo)
424 count = len(web.repo)
413 pos = ctx.rev()
425 pos = ctx.rev()
414 parity = paritygen(web.stripecount)
426 parity = paritygen(web.stripecount)
415
427
416 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
428 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
417
429
418 entries = list(changelist())
430 entries = list(changelist())
419 latestentry = entries[:1]
431 latestentry = entries[:1]
420 if len(entries) > revcount:
432 if len(entries) > revcount:
421 nextentry = entries[-1:]
433 nextentry = entries[-1:]
422 entries = entries[:-1]
434 entries = entries[:-1]
423 else:
435 else:
424 nextentry = []
436 nextentry = []
425
437
426 return tmpl('shortlog' if shortlog else 'changelog', changenav=changenav,
438 web.res.setbodygen(tmpl(
427 node=ctx.hex(), rev=pos, symrev=symrev, changesets=count,
439 'shortlog' if shortlog else 'changelog',
428 entries=entries,
440 changenav=changenav,
429 latestentry=latestentry, nextentry=nextentry,
441 node=ctx.hex(),
430 archives=web.archivelist("tip"), revcount=revcount,
442 rev=pos,
431 morevars=morevars, lessvars=lessvars, query=query)
443 symrev=symrev,
444 changesets=count,
445 entries=entries,
446 latestentry=latestentry,
447 nextentry=nextentry,
448 archives=web.archivelist('tip'),
449 revcount=revcount,
450 morevars=morevars,
451 lessvars=lessvars,
452 query=query))
453
454 return web.res
432
455
433 @webcommand('shortlog')
456 @webcommand('shortlog')
434 def shortlog(web, req, tmpl):
457 def shortlog(web, req, tmpl):
435 """
458 """
436 /shortlog
459 /shortlog
437 ---------
460 ---------
438
461
439 Show basic information about a set of changesets.
462 Show basic information about a set of changesets.
440
463
441 This accepts the same parameters as the ``changelog`` handler. The only
464 This accepts the same parameters as the ``changelog`` handler. The only
442 difference is the ``shortlog`` template will be rendered instead of the
465 difference is the ``shortlog`` template will be rendered instead of the
443 ``changelog`` template.
466 ``changelog`` template.
444 """
467 """
445 return changelog(web, req, tmpl, shortlog=True)
468 return changelog(web, req, tmpl, shortlog=True)
446
469
447 @webcommand('changeset')
470 @webcommand('changeset')
448 def changeset(web, req, tmpl):
471 def changeset(web, req, tmpl):
449 """
472 """
450 /changeset[/{revision}]
473 /changeset[/{revision}]
451 -----------------------
474 -----------------------
452
475
453 Show information about a single changeset.
476 Show information about a single changeset.
454
477
455 A URL path argument is the changeset identifier to show. See ``hg help
478 A URL path argument is the changeset identifier to show. See ``hg help
456 revisions`` for possible values. If not defined, the ``tip`` changeset
479 revisions`` for possible values. If not defined, the ``tip`` changeset
457 will be shown.
480 will be shown.
458
481
459 The ``changeset`` template is rendered. Contents of the ``changesettag``,
482 The ``changeset`` template is rendered. Contents of the ``changesettag``,
460 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
483 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
461 templates related to diffs may all be used to produce the output.
484 templates related to diffs may all be used to produce the output.
462 """
485 """
463 ctx = webutil.changectx(web.repo, req)
486 ctx = webutil.changectx(web.repo, req)
464
487 web.res.setbodygen(tmpl('changeset',
465 return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx))
488 **webutil.changesetentry(web, req, tmpl, ctx)))
489 return web.res
466
490
467 rev = webcommand('rev')(changeset)
491 rev = webcommand('rev')(changeset)
468
492
469 def decodepath(path):
493 def decodepath(path):
470 """Hook for mapping a path in the repository to a path in the
494 """Hook for mapping a path in the repository to a path in the
471 working copy.
495 working copy.
472
496
473 Extensions (e.g., largefiles) can override this to remap files in
497 Extensions (e.g., largefiles) can override this to remap files in
474 the virtual file system presented by the manifest command below."""
498 the virtual file system presented by the manifest command below."""
475 return path
499 return path
476
500
477 @webcommand('manifest')
501 @webcommand('manifest')
478 def manifest(web, req, tmpl):
502 def manifest(web, req, tmpl):
479 """
503 """
480 /manifest[/{revision}[/{path}]]
504 /manifest[/{revision}[/{path}]]
481 -------------------------------
505 -------------------------------
482
506
483 Show information about a directory.
507 Show information about a directory.
484
508
485 If the URL path arguments are omitted, information about the root
509 If the URL path arguments are omitted, information about the root
486 directory for the ``tip`` changeset will be shown.
510 directory for the ``tip`` changeset will be shown.
487
511
488 Because this handler can only show information for directories, it
512 Because this handler can only show information for directories, it
489 is recommended to use the ``file`` handler instead, as it can handle both
513 is recommended to use the ``file`` handler instead, as it can handle both
490 directories and files.
514 directories and files.
491
515
492 The ``manifest`` template will be rendered for this handler.
516 The ``manifest`` template will be rendered for this handler.
493 """
517 """
494 if 'node' in req.req.qsparams:
518 if 'node' in req.req.qsparams:
495 ctx = webutil.changectx(web.repo, req)
519 ctx = webutil.changectx(web.repo, req)
496 symrev = webutil.symrevorshortnode(req, ctx)
520 symrev = webutil.symrevorshortnode(req, ctx)
497 else:
521 else:
498 ctx = web.repo['tip']
522 ctx = web.repo['tip']
499 symrev = 'tip'
523 symrev = 'tip'
500 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
524 path = webutil.cleanpath(web.repo, req.req.qsparams.get('file', ''))
501 mf = ctx.manifest()
525 mf = ctx.manifest()
502 node = ctx.node()
526 node = ctx.node()
503
527
504 files = {}
528 files = {}
505 dirs = {}
529 dirs = {}
506 parity = paritygen(web.stripecount)
530 parity = paritygen(web.stripecount)
507
531
508 if path and path[-1:] != "/":
532 if path and path[-1:] != "/":
509 path += "/"
533 path += "/"
510 l = len(path)
534 l = len(path)
511 abspath = "/" + path
535 abspath = "/" + path
512
536
513 for full, n in mf.iteritems():
537 for full, n in mf.iteritems():
514 # the virtual path (working copy path) used for the full
538 # the virtual path (working copy path) used for the full
515 # (repository) path
539 # (repository) path
516 f = decodepath(full)
540 f = decodepath(full)
517
541
518 if f[:l] != path:
542 if f[:l] != path:
519 continue
543 continue
520 remain = f[l:]
544 remain = f[l:]
521 elements = remain.split('/')
545 elements = remain.split('/')
522 if len(elements) == 1:
546 if len(elements) == 1:
523 files[remain] = full
547 files[remain] = full
524 else:
548 else:
525 h = dirs # need to retain ref to dirs (root)
549 h = dirs # need to retain ref to dirs (root)
526 for elem in elements[0:-1]:
550 for elem in elements[0:-1]:
527 if elem not in h:
551 if elem not in h:
528 h[elem] = {}
552 h[elem] = {}
529 h = h[elem]
553 h = h[elem]
530 if len(h) > 1:
554 if len(h) > 1:
531 break
555 break
532 h[None] = None # denotes files present
556 h[None] = None # denotes files present
533
557
534 if mf and not files and not dirs:
558 if mf and not files and not dirs:
535 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
559 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
536
560
537 def filelist(**map):
561 def filelist(**map):
538 for f in sorted(files):
562 for f in sorted(files):
539 full = files[f]
563 full = files[f]
540
564
541 fctx = ctx.filectx(full)
565 fctx = ctx.filectx(full)
542 yield {"file": full,
566 yield {"file": full,
543 "parity": next(parity),
567 "parity": next(parity),
544 "basename": f,
568 "basename": f,
545 "date": fctx.date(),
569 "date": fctx.date(),
546 "size": fctx.size(),
570 "size": fctx.size(),
547 "permissions": mf.flags(full)}
571 "permissions": mf.flags(full)}
548
572
549 def dirlist(**map):
573 def dirlist(**map):
550 for d in sorted(dirs):
574 for d in sorted(dirs):
551
575
552 emptydirs = []
576 emptydirs = []
553 h = dirs[d]
577 h = dirs[d]
554 while isinstance(h, dict) and len(h) == 1:
578 while isinstance(h, dict) and len(h) == 1:
555 k, v = next(iter(h.items()))
579 k, v = next(iter(h.items()))
556 if v:
580 if v:
557 emptydirs.append(k)
581 emptydirs.append(k)
558 h = v
582 h = v
559
583
560 path = "%s%s" % (abspath, d)
584 path = "%s%s" % (abspath, d)
561 yield {"parity": next(parity),
585 yield {"parity": next(parity),
562 "path": path,
586 "path": path,
563 "emptydirs": "/".join(emptydirs),
587 "emptydirs": "/".join(emptydirs),
564 "basename": d}
588 "basename": d}
565
589
566 return tmpl("manifest",
590 web.res.setbodygen(tmpl(
567 symrev=symrev,
591 'manifest',
568 path=abspath,
592 symrev=symrev,
569 up=webutil.up(abspath),
593 path=abspath,
570 upparity=next(parity),
594 up=webutil.up(abspath),
571 fentries=filelist,
595 upparity=next(parity),
572 dentries=dirlist,
596 fentries=filelist,
573 archives=web.archivelist(hex(node)),
597 dentries=dirlist,
574 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
598 archives=web.archivelist(hex(node)),
599 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
600
601 return web.res
575
602
576 @webcommand('tags')
603 @webcommand('tags')
577 def tags(web, req, tmpl):
604 def tags(web, req, tmpl):
578 """
605 """
579 /tags
606 /tags
580 -----
607 -----
581
608
582 Show information about tags.
609 Show information about tags.
583
610
584 No arguments are accepted.
611 No arguments are accepted.
585
612
586 The ``tags`` template is rendered.
613 The ``tags`` template is rendered.
587 """
614 """
588 i = list(reversed(web.repo.tagslist()))
615 i = list(reversed(web.repo.tagslist()))
589 parity = paritygen(web.stripecount)
616 parity = paritygen(web.stripecount)
590
617
591 def entries(notip, latestonly, **map):
618 def entries(notip, latestonly, **map):
592 t = i
619 t = i
593 if notip:
620 if notip:
594 t = [(k, n) for k, n in i if k != "tip"]
621 t = [(k, n) for k, n in i if k != "tip"]
595 if latestonly:
622 if latestonly:
596 t = t[:1]
623 t = t[:1]
597 for k, n in t:
624 for k, n in t:
598 yield {"parity": next(parity),
625 yield {"parity": next(parity),
599 "tag": k,
626 "tag": k,
600 "date": web.repo[n].date(),
627 "date": web.repo[n].date(),
601 "node": hex(n)}
628 "node": hex(n)}
602
629
603 return tmpl("tags",
630 web.res.setbodygen(tmpl(
604 node=hex(web.repo.changelog.tip()),
631 'tags',
605 entries=lambda **x: entries(False, False, **x),
632 node=hex(web.repo.changelog.tip()),
606 entriesnotip=lambda **x: entries(True, False, **x),
633 entries=lambda **x: entries(False, False, **x),
607 latestentry=lambda **x: entries(True, True, **x))
634 entriesnotip=lambda **x: entries(True, False, **x),
635 latestentry=lambda **x: entries(True, True, **x)))
636
637 return web.res
608
638
609 @webcommand('bookmarks')
639 @webcommand('bookmarks')
610 def bookmarks(web, req, tmpl):
640 def bookmarks(web, req, tmpl):
611 """
641 """
612 /bookmarks
642 /bookmarks
613 ----------
643 ----------
614
644
615 Show information about bookmarks.
645 Show information about bookmarks.
616
646
617 No arguments are accepted.
647 No arguments are accepted.
618
648
619 The ``bookmarks`` template is rendered.
649 The ``bookmarks`` template is rendered.
620 """
650 """
621 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
651 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
622 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
652 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
623 i = sorted(i, key=sortkey, reverse=True)
653 i = sorted(i, key=sortkey, reverse=True)
624 parity = paritygen(web.stripecount)
654 parity = paritygen(web.stripecount)
625
655
626 def entries(latestonly, **map):
656 def entries(latestonly, **map):
627 t = i
657 t = i
628 if latestonly:
658 if latestonly:
629 t = i[:1]
659 t = i[:1]
630 for k, n in t:
660 for k, n in t:
631 yield {"parity": next(parity),
661 yield {"parity": next(parity),
632 "bookmark": k,
662 "bookmark": k,
633 "date": web.repo[n].date(),
663 "date": web.repo[n].date(),
634 "node": hex(n)}
664 "node": hex(n)}
635
665
636 if i:
666 if i:
637 latestrev = i[0][1]
667 latestrev = i[0][1]
638 else:
668 else:
639 latestrev = -1
669 latestrev = -1
640
670
641 return tmpl("bookmarks",
671 web.res.setbodygen(tmpl(
642 node=hex(web.repo.changelog.tip()),
672 'bookmarks',
643 lastchange=[{"date": web.repo[latestrev].date()}],
673 node=hex(web.repo.changelog.tip()),
644 entries=lambda **x: entries(latestonly=False, **x),
674 lastchange=[{'date': web.repo[latestrev].date()}],
645 latestentry=lambda **x: entries(latestonly=True, **x))
675 entries=lambda **x: entries(latestonly=False, **x),
676 latestentry=lambda **x: entries(latestonly=True, **x)))
677
678 return web.res
646
679
647 @webcommand('branches')
680 @webcommand('branches')
648 def branches(web, req, tmpl):
681 def branches(web, req, tmpl):
649 """
682 """
650 /branches
683 /branches
651 ---------
684 ---------
652
685
653 Show information about branches.
686 Show information about branches.
654
687
655 All known branches are contained in the output, even closed branches.
688 All known branches are contained in the output, even closed branches.
656
689
657 No arguments are accepted.
690 No arguments are accepted.
658
691
659 The ``branches`` template is rendered.
692 The ``branches`` template is rendered.
660 """
693 """
661 entries = webutil.branchentries(web.repo, web.stripecount)
694 entries = webutil.branchentries(web.repo, web.stripecount)
662 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
695 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
663 return tmpl('branches', node=hex(web.repo.changelog.tip()),
696
664 entries=entries, latestentry=latestentry)
697 web.res.setbodygen(tmpl(
698 'branches',
699 node=hex(web.repo.changelog.tip()),
700 entries=entries,
701 latestentry=latestentry))
702
703 return web.res
665
704
666 @webcommand('summary')
705 @webcommand('summary')
667 def summary(web, req, tmpl):
706 def summary(web, req, tmpl):
668 """
707 """
669 /summary
708 /summary
670 --------
709 --------
671
710
672 Show a summary of repository state.
711 Show a summary of repository state.
673
712
674 Information about the latest changesets, bookmarks, tags, and branches
713 Information about the latest changesets, bookmarks, tags, and branches
675 is captured by this handler.
714 is captured by this handler.
676
715
677 The ``summary`` template is rendered.
716 The ``summary`` template is rendered.
678 """
717 """
679 i = reversed(web.repo.tagslist())
718 i = reversed(web.repo.tagslist())
680
719
681 def tagentries(**map):
720 def tagentries(**map):
682 parity = paritygen(web.stripecount)
721 parity = paritygen(web.stripecount)
683 count = 0
722 count = 0
684 for k, n in i:
723 for k, n in i:
685 if k == "tip": # skip tip
724 if k == "tip": # skip tip
686 continue
725 continue
687
726
688 count += 1
727 count += 1
689 if count > 10: # limit to 10 tags
728 if count > 10: # limit to 10 tags
690 break
729 break
691
730
692 yield tmpl("tagentry",
731 yield tmpl("tagentry",
693 parity=next(parity),
732 parity=next(parity),
694 tag=k,
733 tag=k,
695 node=hex(n),
734 node=hex(n),
696 date=web.repo[n].date())
735 date=web.repo[n].date())
697
736
698 def bookmarks(**map):
737 def bookmarks(**map):
699 parity = paritygen(web.stripecount)
738 parity = paritygen(web.stripecount)
700 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
739 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
701 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
740 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
702 marks = sorted(marks, key=sortkey, reverse=True)
741 marks = sorted(marks, key=sortkey, reverse=True)
703 for k, n in marks[:10]: # limit to 10 bookmarks
742 for k, n in marks[:10]: # limit to 10 bookmarks
704 yield {'parity': next(parity),
743 yield {'parity': next(parity),
705 'bookmark': k,
744 'bookmark': k,
706 'date': web.repo[n].date(),
745 'date': web.repo[n].date(),
707 'node': hex(n)}
746 'node': hex(n)}
708
747
709 def changelist(**map):
748 def changelist(**map):
710 parity = paritygen(web.stripecount, offset=start - end)
749 parity = paritygen(web.stripecount, offset=start - end)
711 l = [] # build a list in forward order for efficiency
750 l = [] # build a list in forward order for efficiency
712 revs = []
751 revs = []
713 if start < end:
752 if start < end:
714 revs = web.repo.changelog.revs(start, end - 1)
753 revs = web.repo.changelog.revs(start, end - 1)
715 for i in revs:
754 for i in revs:
716 ctx = web.repo[i]
755 ctx = web.repo[i]
717
756
718 l.append(tmpl(
757 l.append(tmpl(
719 'shortlogentry',
758 'shortlogentry',
720 parity=next(parity),
759 parity=next(parity),
721 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
760 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
722
761
723 for entry in reversed(l):
762 for entry in reversed(l):
724 yield entry
763 yield entry
725
764
726 tip = web.repo['tip']
765 tip = web.repo['tip']
727 count = len(web.repo)
766 count = len(web.repo)
728 start = max(0, count - web.maxchanges)
767 start = max(0, count - web.maxchanges)
729 end = min(count, start + web.maxchanges)
768 end = min(count, start + web.maxchanges)
730
769
731 desc = web.config("web", "description")
770 desc = web.config("web", "description")
732 if not desc:
771 if not desc:
733 desc = 'unknown'
772 desc = 'unknown'
734 return tmpl("summary",
773
735 desc=desc,
774 web.res.setbodygen(tmpl(
736 owner=get_contact(web.config) or "unknown",
775 'summary',
737 lastchange=tip.date(),
776 desc=desc,
738 tags=tagentries,
777 owner=get_contact(web.config) or 'unknown',
739 bookmarks=bookmarks,
778 lastchange=tip.date(),
740 branches=webutil.branchentries(web.repo, web.stripecount, 10),
779 tags=tagentries,
741 shortlog=changelist,
780 bookmarks=bookmarks,
742 node=tip.hex(),
781 branches=webutil.branchentries(web.repo, web.stripecount, 10),
743 symrev='tip',
782 shortlog=changelist,
744 archives=web.archivelist("tip"),
783 node=tip.hex(),
745 labels=web.configlist('web', 'labels'))
784 symrev='tip',
785 archives=web.archivelist('tip'),
786 labels=web.configlist('web', 'labels')))
787
788 return web.res
746
789
747 @webcommand('filediff')
790 @webcommand('filediff')
748 def filediff(web, req, tmpl):
791 def filediff(web, req, tmpl):
749 """
792 """
750 /diff/{revision}/{path}
793 /diff/{revision}/{path}
751 -----------------------
794 -----------------------
752
795
753 Show how a file changed in a particular commit.
796 Show how a file changed in a particular commit.
754
797
755 The ``filediff`` template is rendered.
798 The ``filediff`` template is rendered.
756
799
757 This handler is registered under both the ``/diff`` and ``/filediff``
800 This handler is registered under both the ``/diff`` and ``/filediff``
758 paths. ``/diff`` is used in modern code.
801 paths. ``/diff`` is used in modern code.
759 """
802 """
760 fctx, ctx = None, None
803 fctx, ctx = None, None
761 try:
804 try:
762 fctx = webutil.filectx(web.repo, req)
805 fctx = webutil.filectx(web.repo, req)
763 except LookupError:
806 except LookupError:
764 ctx = webutil.changectx(web.repo, req)
807 ctx = webutil.changectx(web.repo, req)
765 path = webutil.cleanpath(web.repo, req.req.qsparams['file'])
808 path = webutil.cleanpath(web.repo, req.req.qsparams['file'])
766 if path not in ctx.files():
809 if path not in ctx.files():
767 raise
810 raise
768
811
769 if fctx is not None:
812 if fctx is not None:
770 path = fctx.path()
813 path = fctx.path()
771 ctx = fctx.changectx()
814 ctx = fctx.changectx()
772 basectx = ctx.p1()
815 basectx = ctx.p1()
773
816
774 style = web.config('web', 'style')
817 style = web.config('web', 'style')
775 if 'style' in req.req.qsparams:
818 if 'style' in req.req.qsparams:
776 style = req.req.qsparams['style']
819 style = req.req.qsparams['style']
777
820
778 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
821 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
779 if fctx is not None:
822 if fctx is not None:
780 rename = webutil.renamelink(fctx)
823 rename = webutil.renamelink(fctx)
781 ctx = fctx
824 ctx = fctx
782 else:
825 else:
783 rename = []
826 rename = []
784 ctx = ctx
827 ctx = ctx
785 return tmpl("filediff",
828
786 file=path,
829 web.res.setbodygen(tmpl(
787 symrev=webutil.symrevorshortnode(req, ctx),
830 'filediff',
788 rename=rename,
831 file=path,
789 diff=diffs,
832 symrev=webutil.symrevorshortnode(req, ctx),
790 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
833 rename=rename,
834 diff=diffs,
835 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
836
837 return web.res
791
838
792 diff = webcommand('diff')(filediff)
839 diff = webcommand('diff')(filediff)
793
840
794 @webcommand('comparison')
841 @webcommand('comparison')
795 def comparison(web, req, tmpl):
842 def comparison(web, req, tmpl):
796 """
843 """
797 /comparison/{revision}/{path}
844 /comparison/{revision}/{path}
798 -----------------------------
845 -----------------------------
799
846
800 Show a comparison between the old and new versions of a file from changes
847 Show a comparison between the old and new versions of a file from changes
801 made on a particular revision.
848 made on a particular revision.
802
849
803 This is similar to the ``diff`` handler. However, this form features
850 This is similar to the ``diff`` handler. However, this form features
804 a split or side-by-side diff rather than a unified diff.
851 a split or side-by-side diff rather than a unified diff.
805
852
806 The ``context`` query string argument can be used to control the lines of
853 The ``context`` query string argument can be used to control the lines of
807 context in the diff.
854 context in the diff.
808
855
809 The ``filecomparison`` template is rendered.
856 The ``filecomparison`` template is rendered.
810 """
857 """
811 ctx = webutil.changectx(web.repo, req)
858 ctx = webutil.changectx(web.repo, req)
812 if 'file' not in req.req.qsparams:
859 if 'file' not in req.req.qsparams:
813 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
860 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
814 path = webutil.cleanpath(web.repo, req.req.qsparams['file'])
861 path = webutil.cleanpath(web.repo, req.req.qsparams['file'])
815
862
816 parsecontext = lambda v: v == 'full' and -1 or int(v)
863 parsecontext = lambda v: v == 'full' and -1 or int(v)
817 if 'context' in req.req.qsparams:
864 if 'context' in req.req.qsparams:
818 context = parsecontext(req.req.qsparams['context'])
865 context = parsecontext(req.req.qsparams['context'])
819 else:
866 else:
820 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
867 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
821
868
822 def filelines(f):
869 def filelines(f):
823 if f.isbinary():
870 if f.isbinary():
824 mt = mimetypes.guess_type(f.path())[0]
871 mt = mimetypes.guess_type(f.path())[0]
825 if not mt:
872 if not mt:
826 mt = 'application/octet-stream'
873 mt = 'application/octet-stream'
827 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
874 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
828 return f.data().splitlines()
875 return f.data().splitlines()
829
876
830 fctx = None
877 fctx = None
831 parent = ctx.p1()
878 parent = ctx.p1()
832 leftrev = parent.rev()
879 leftrev = parent.rev()
833 leftnode = parent.node()
880 leftnode = parent.node()
834 rightrev = ctx.rev()
881 rightrev = ctx.rev()
835 rightnode = ctx.node()
882 rightnode = ctx.node()
836 if path in ctx:
883 if path in ctx:
837 fctx = ctx[path]
884 fctx = ctx[path]
838 rightlines = filelines(fctx)
885 rightlines = filelines(fctx)
839 if path not in parent:
886 if path not in parent:
840 leftlines = ()
887 leftlines = ()
841 else:
888 else:
842 pfctx = parent[path]
889 pfctx = parent[path]
843 leftlines = filelines(pfctx)
890 leftlines = filelines(pfctx)
844 else:
891 else:
845 rightlines = ()
892 rightlines = ()
846 pfctx = ctx.parents()[0][path]
893 pfctx = ctx.parents()[0][path]
847 leftlines = filelines(pfctx)
894 leftlines = filelines(pfctx)
848
895
849 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
896 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
850 if fctx is not None:
897 if fctx is not None:
851 rename = webutil.renamelink(fctx)
898 rename = webutil.renamelink(fctx)
852 ctx = fctx
899 ctx = fctx
853 else:
900 else:
854 rename = []
901 rename = []
855 ctx = ctx
902 ctx = ctx
856 return tmpl('filecomparison',
903
857 file=path,
904 web.res.setbodygen(tmpl(
858 symrev=webutil.symrevorshortnode(req, ctx),
905 'filecomparison',
859 rename=rename,
906 file=path,
860 leftrev=leftrev,
907 symrev=webutil.symrevorshortnode(req, ctx),
861 leftnode=hex(leftnode),
908 rename=rename,
862 rightrev=rightrev,
909 leftrev=leftrev,
863 rightnode=hex(rightnode),
910 leftnode=hex(leftnode),
864 comparison=comparison,
911 rightrev=rightrev,
865 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
912 rightnode=hex(rightnode),
913 comparison=comparison,
914 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
915
916 return web.res
866
917
867 @webcommand('annotate')
918 @webcommand('annotate')
868 def annotate(web, req, tmpl):
919 def annotate(web, req, tmpl):
869 """
920 """
870 /annotate/{revision}/{path}
921 /annotate/{revision}/{path}
871 ---------------------------
922 ---------------------------
872
923
873 Show changeset information for each line in a file.
924 Show changeset information for each line in a file.
874
925
875 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
926 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
876 ``ignoreblanklines`` query string arguments have the same meaning as
927 ``ignoreblanklines`` query string arguments have the same meaning as
877 their ``[annotate]`` config equivalents. It uses the hgrc boolean
928 their ``[annotate]`` config equivalents. It uses the hgrc boolean
878 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
929 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
879 false and ``1`` and ``true`` are true. If not defined, the server
930 false and ``1`` and ``true`` are true. If not defined, the server
880 default settings are used.
931 default settings are used.
881
932
882 The ``fileannotate`` template is rendered.
933 The ``fileannotate`` template is rendered.
883 """
934 """
884 fctx = webutil.filectx(web.repo, req)
935 fctx = webutil.filectx(web.repo, req)
885 f = fctx.path()
936 f = fctx.path()
886 parity = paritygen(web.stripecount)
937 parity = paritygen(web.stripecount)
887 ishead = fctx.filerev() in fctx.filelog().headrevs()
938 ishead = fctx.filerev() in fctx.filelog().headrevs()
888
939
889 # parents() is called once per line and several lines likely belong to
940 # parents() is called once per line and several lines likely belong to
890 # same revision. So it is worth caching.
941 # same revision. So it is worth caching.
891 # TODO there are still redundant operations within basefilectx.parents()
942 # TODO there are still redundant operations within basefilectx.parents()
892 # and from the fctx.annotate() call itself that could be cached.
943 # and from the fctx.annotate() call itself that could be cached.
893 parentscache = {}
944 parentscache = {}
894 def parents(f):
945 def parents(f):
895 rev = f.rev()
946 rev = f.rev()
896 if rev not in parentscache:
947 if rev not in parentscache:
897 parentscache[rev] = []
948 parentscache[rev] = []
898 for p in f.parents():
949 for p in f.parents():
899 entry = {
950 entry = {
900 'node': p.hex(),
951 'node': p.hex(),
901 'rev': p.rev(),
952 'rev': p.rev(),
902 }
953 }
903 parentscache[rev].append(entry)
954 parentscache[rev].append(entry)
904
955
905 for p in parentscache[rev]:
956 for p in parentscache[rev]:
906 yield p
957 yield p
907
958
908 def annotate(**map):
959 def annotate(**map):
909 if fctx.isbinary():
960 if fctx.isbinary():
910 mt = (mimetypes.guess_type(fctx.path())[0]
961 mt = (mimetypes.guess_type(fctx.path())[0]
911 or 'application/octet-stream')
962 or 'application/octet-stream')
912 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
963 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
913 else:
964 else:
914 lines = webutil.annotate(req, fctx, web.repo.ui)
965 lines = webutil.annotate(req, fctx, web.repo.ui)
915
966
916 previousrev = None
967 previousrev = None
917 blockparitygen = paritygen(1)
968 blockparitygen = paritygen(1)
918 for lineno, (aline, l) in enumerate(lines):
969 for lineno, (aline, l) in enumerate(lines):
919 f = aline.fctx
970 f = aline.fctx
920 rev = f.rev()
971 rev = f.rev()
921 if rev != previousrev:
972 if rev != previousrev:
922 blockhead = True
973 blockhead = True
923 blockparity = next(blockparitygen)
974 blockparity = next(blockparitygen)
924 else:
975 else:
925 blockhead = None
976 blockhead = None
926 previousrev = rev
977 previousrev = rev
927 yield {"parity": next(parity),
978 yield {"parity": next(parity),
928 "node": f.hex(),
979 "node": f.hex(),
929 "rev": rev,
980 "rev": rev,
930 "author": f.user(),
981 "author": f.user(),
931 "parents": parents(f),
982 "parents": parents(f),
932 "desc": f.description(),
983 "desc": f.description(),
933 "extra": f.extra(),
984 "extra": f.extra(),
934 "file": f.path(),
985 "file": f.path(),
935 "blockhead": blockhead,
986 "blockhead": blockhead,
936 "blockparity": blockparity,
987 "blockparity": blockparity,
937 "targetline": aline.lineno,
988 "targetline": aline.lineno,
938 "line": l,
989 "line": l,
939 "lineno": lineno + 1,
990 "lineno": lineno + 1,
940 "lineid": "l%d" % (lineno + 1),
991 "lineid": "l%d" % (lineno + 1),
941 "linenumber": "% 6d" % (lineno + 1),
992 "linenumber": "% 6d" % (lineno + 1),
942 "revdate": f.date()}
993 "revdate": f.date()}
943
994
944 diffopts = webutil.difffeatureopts(req, web.repo.ui, 'annotate')
995 diffopts = webutil.difffeatureopts(req, web.repo.ui, 'annotate')
945 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
996 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
946
997
947 return tmpl("fileannotate",
998 web.res.setbodygen(tmpl(
948 file=f,
999 'fileannotate',
949 annotate=annotate,
1000 file=f,
950 path=webutil.up(f),
1001 annotate=annotate,
951 symrev=webutil.symrevorshortnode(req, fctx),
1002 path=webutil.up(f),
952 rename=webutil.renamelink(fctx),
1003 symrev=webutil.symrevorshortnode(req, fctx),
953 permissions=fctx.manifest().flags(f),
1004 rename=webutil.renamelink(fctx),
954 ishead=int(ishead),
1005 permissions=fctx.manifest().flags(f),
955 diffopts=diffopts,
1006 ishead=int(ishead),
956 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1007 diffopts=diffopts,
1008 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
1009
1010 return web.res
957
1011
958 @webcommand('filelog')
1012 @webcommand('filelog')
959 def filelog(web, req, tmpl):
1013 def filelog(web, req, tmpl):
960 """
1014 """
961 /filelog/{revision}/{path}
1015 /filelog/{revision}/{path}
962 --------------------------
1016 --------------------------
963
1017
964 Show information about the history of a file in the repository.
1018 Show information about the history of a file in the repository.
965
1019
966 The ``revcount`` query string argument can be defined to control the
1020 The ``revcount`` query string argument can be defined to control the
967 maximum number of entries to show.
1021 maximum number of entries to show.
968
1022
969 The ``filelog`` template will be rendered.
1023 The ``filelog`` template will be rendered.
970 """
1024 """
971
1025
972 try:
1026 try:
973 fctx = webutil.filectx(web.repo, req)
1027 fctx = webutil.filectx(web.repo, req)
974 f = fctx.path()
1028 f = fctx.path()
975 fl = fctx.filelog()
1029 fl = fctx.filelog()
976 except error.LookupError:
1030 except error.LookupError:
977 f = webutil.cleanpath(web.repo, req.req.qsparams['file'])
1031 f = webutil.cleanpath(web.repo, req.req.qsparams['file'])
978 fl = web.repo.file(f)
1032 fl = web.repo.file(f)
979 numrevs = len(fl)
1033 numrevs = len(fl)
980 if not numrevs: # file doesn't exist at all
1034 if not numrevs: # file doesn't exist at all
981 raise
1035 raise
982 rev = webutil.changectx(web.repo, req).rev()
1036 rev = webutil.changectx(web.repo, req).rev()
983 first = fl.linkrev(0)
1037 first = fl.linkrev(0)
984 if rev < first: # current rev is from before file existed
1038 if rev < first: # current rev is from before file existed
985 raise
1039 raise
986 frev = numrevs - 1
1040 frev = numrevs - 1
987 while fl.linkrev(frev) > rev:
1041 while fl.linkrev(frev) > rev:
988 frev -= 1
1042 frev -= 1
989 fctx = web.repo.filectx(f, fl.linkrev(frev))
1043 fctx = web.repo.filectx(f, fl.linkrev(frev))
990
1044
991 revcount = web.maxshortchanges
1045 revcount = web.maxshortchanges
992 if 'revcount' in req.req.qsparams:
1046 if 'revcount' in req.req.qsparams:
993 try:
1047 try:
994 revcount = int(req.req.qsparams.get('revcount', revcount))
1048 revcount = int(req.req.qsparams.get('revcount', revcount))
995 revcount = max(revcount, 1)
1049 revcount = max(revcount, 1)
996 tmpl.defaults['sessionvars']['revcount'] = revcount
1050 tmpl.defaults['sessionvars']['revcount'] = revcount
997 except ValueError:
1051 except ValueError:
998 pass
1052 pass
999
1053
1000 lrange = webutil.linerange(req)
1054 lrange = webutil.linerange(req)
1001
1055
1002 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1056 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1003 lessvars['revcount'] = max(revcount // 2, 1)
1057 lessvars['revcount'] = max(revcount // 2, 1)
1004 morevars = copy.copy(tmpl.defaults['sessionvars'])
1058 morevars = copy.copy(tmpl.defaults['sessionvars'])
1005 morevars['revcount'] = revcount * 2
1059 morevars['revcount'] = revcount * 2
1006
1060
1007 patch = 'patch' in req.req.qsparams
1061 patch = 'patch' in req.req.qsparams
1008 if patch:
1062 if patch:
1009 lessvars['patch'] = morevars['patch'] = req.req.qsparams['patch']
1063 lessvars['patch'] = morevars['patch'] = req.req.qsparams['patch']
1010 descend = 'descend' in req.req.qsparams
1064 descend = 'descend' in req.req.qsparams
1011 if descend:
1065 if descend:
1012 lessvars['descend'] = morevars['descend'] = req.req.qsparams['descend']
1066 lessvars['descend'] = morevars['descend'] = req.req.qsparams['descend']
1013
1067
1014 count = fctx.filerev() + 1
1068 count = fctx.filerev() + 1
1015 start = max(0, count - revcount) # first rev on this page
1069 start = max(0, count - revcount) # first rev on this page
1016 end = min(count, start + revcount) # last rev on this page
1070 end = min(count, start + revcount) # last rev on this page
1017 parity = paritygen(web.stripecount, offset=start - end)
1071 parity = paritygen(web.stripecount, offset=start - end)
1018
1072
1019 repo = web.repo
1073 repo = web.repo
1020 revs = fctx.filelog().revs(start, end - 1)
1074 revs = fctx.filelog().revs(start, end - 1)
1021 entries = []
1075 entries = []
1022
1076
1023 diffstyle = web.config('web', 'style')
1077 diffstyle = web.config('web', 'style')
1024 if 'style' in req.req.qsparams:
1078 if 'style' in req.req.qsparams:
1025 diffstyle = req.req.qsparams['style']
1079 diffstyle = req.req.qsparams['style']
1026
1080
1027 def diff(fctx, linerange=None):
1081 def diff(fctx, linerange=None):
1028 ctx = fctx.changectx()
1082 ctx = fctx.changectx()
1029 basectx = ctx.p1()
1083 basectx = ctx.p1()
1030 path = fctx.path()
1084 path = fctx.path()
1031 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1085 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1032 linerange=linerange,
1086 linerange=linerange,
1033 lineidprefix='%s-' % ctx.hex()[:12])
1087 lineidprefix='%s-' % ctx.hex()[:12])
1034
1088
1035 linerange = None
1089 linerange = None
1036 if lrange is not None:
1090 if lrange is not None:
1037 linerange = webutil.formatlinerange(*lrange)
1091 linerange = webutil.formatlinerange(*lrange)
1038 # deactivate numeric nav links when linerange is specified as this
1092 # deactivate numeric nav links when linerange is specified as this
1039 # would required a dedicated "revnav" class
1093 # would required a dedicated "revnav" class
1040 nav = None
1094 nav = None
1041 if descend:
1095 if descend:
1042 it = dagop.blockdescendants(fctx, *lrange)
1096 it = dagop.blockdescendants(fctx, *lrange)
1043 else:
1097 else:
1044 it = dagop.blockancestors(fctx, *lrange)
1098 it = dagop.blockancestors(fctx, *lrange)
1045 for i, (c, lr) in enumerate(it, 1):
1099 for i, (c, lr) in enumerate(it, 1):
1046 diffs = None
1100 diffs = None
1047 if patch:
1101 if patch:
1048 diffs = diff(c, linerange=lr)
1102 diffs = diff(c, linerange=lr)
1049 # follow renames accross filtered (not in range) revisions
1103 # follow renames accross filtered (not in range) revisions
1050 path = c.path()
1104 path = c.path()
1051 entries.append(dict(
1105 entries.append(dict(
1052 parity=next(parity),
1106 parity=next(parity),
1053 filerev=c.rev(),
1107 filerev=c.rev(),
1054 file=path,
1108 file=path,
1055 diff=diffs,
1109 diff=diffs,
1056 linerange=webutil.formatlinerange(*lr),
1110 linerange=webutil.formatlinerange(*lr),
1057 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1111 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1058 if i == revcount:
1112 if i == revcount:
1059 break
1113 break
1060 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1114 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1061 morevars['linerange'] = lessvars['linerange']
1115 morevars['linerange'] = lessvars['linerange']
1062 else:
1116 else:
1063 for i in revs:
1117 for i in revs:
1064 iterfctx = fctx.filectx(i)
1118 iterfctx = fctx.filectx(i)
1065 diffs = None
1119 diffs = None
1066 if patch:
1120 if patch:
1067 diffs = diff(iterfctx)
1121 diffs = diff(iterfctx)
1068 entries.append(dict(
1122 entries.append(dict(
1069 parity=next(parity),
1123 parity=next(parity),
1070 filerev=i,
1124 filerev=i,
1071 file=f,
1125 file=f,
1072 diff=diffs,
1126 diff=diffs,
1073 rename=webutil.renamelink(iterfctx),
1127 rename=webutil.renamelink(iterfctx),
1074 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1128 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1075 entries.reverse()
1129 entries.reverse()
1076 revnav = webutil.filerevnav(web.repo, fctx.path())
1130 revnav = webutil.filerevnav(web.repo, fctx.path())
1077 nav = revnav.gen(end - 1, revcount, count)
1131 nav = revnav.gen(end - 1, revcount, count)
1078
1132
1079 latestentry = entries[:1]
1133 latestentry = entries[:1]
1080
1134
1081 web.res.setbodygen(tmpl(
1135 web.res.setbodygen(tmpl(
1082 'filelog',
1136 'filelog',
1083 file=f,
1137 file=f,
1084 nav=nav,
1138 nav=nav,
1085 symrev=webutil.symrevorshortnode(req, fctx),
1139 symrev=webutil.symrevorshortnode(req, fctx),
1086 entries=entries,
1140 entries=entries,
1087 descend=descend,
1141 descend=descend,
1088 patch=patch,
1142 patch=patch,
1089 latestentry=latestentry,
1143 latestentry=latestentry,
1090 linerange=linerange,
1144 linerange=linerange,
1091 revcount=revcount,
1145 revcount=revcount,
1092 morevars=morevars,
1146 morevars=morevars,
1093 lessvars=lessvars,
1147 lessvars=lessvars,
1094 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
1148 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
1095
1149
1096 return web.res
1150 return web.res
1097
1151
1098 @webcommand('archive')
1152 @webcommand('archive')
1099 def archive(web, req, tmpl):
1153 def archive(web, req, tmpl):
1100 """
1154 """
1101 /archive/{revision}.{format}[/{path}]
1155 /archive/{revision}.{format}[/{path}]
1102 -------------------------------------
1156 -------------------------------------
1103
1157
1104 Obtain an archive of repository content.
1158 Obtain an archive of repository content.
1105
1159
1106 The content and type of the archive is defined by a URL path parameter.
1160 The content and type of the archive is defined by a URL path parameter.
1107 ``format`` is the file extension of the archive type to be generated. e.g.
1161 ``format`` is the file extension of the archive type to be generated. e.g.
1108 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1162 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1109 server configuration.
1163 server configuration.
1110
1164
1111 The optional ``path`` URL parameter controls content to include in the
1165 The optional ``path`` URL parameter controls content to include in the
1112 archive. If omitted, every file in the specified revision is present in the
1166 archive. If omitted, every file in the specified revision is present in the
1113 archive. If included, only the specified file or contents of the specified
1167 archive. If included, only the specified file or contents of the specified
1114 directory will be included in the archive.
1168 directory will be included in the archive.
1115
1169
1116 No template is used for this handler. Raw, binary content is generated.
1170 No template is used for this handler. Raw, binary content is generated.
1117 """
1171 """
1118
1172
1119 type_ = req.req.qsparams.get('type')
1173 type_ = req.req.qsparams.get('type')
1120 allowed = web.configlist("web", "allow_archive")
1174 allowed = web.configlist("web", "allow_archive")
1121 key = req.req.qsparams['node']
1175 key = req.req.qsparams['node']
1122
1176
1123 if type_ not in web.archivespecs:
1177 if type_ not in web.archivespecs:
1124 msg = 'Unsupported archive type: %s' % type_
1178 msg = 'Unsupported archive type: %s' % type_
1125 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1179 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1126
1180
1127 if not ((type_ in allowed or
1181 if not ((type_ in allowed or
1128 web.configbool("web", "allow" + type_))):
1182 web.configbool("web", "allow" + type_))):
1129 msg = 'Archive type not allowed: %s' % type_
1183 msg = 'Archive type not allowed: %s' % type_
1130 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1184 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1131
1185
1132 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1186 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1133 cnode = web.repo.lookup(key)
1187 cnode = web.repo.lookup(key)
1134 arch_version = key
1188 arch_version = key
1135 if cnode == key or key == 'tip':
1189 if cnode == key or key == 'tip':
1136 arch_version = short(cnode)
1190 arch_version = short(cnode)
1137 name = "%s-%s" % (reponame, arch_version)
1191 name = "%s-%s" % (reponame, arch_version)
1138
1192
1139 ctx = webutil.changectx(web.repo, req)
1193 ctx = webutil.changectx(web.repo, req)
1140 pats = []
1194 pats = []
1141 match = scmutil.match(ctx, [])
1195 match = scmutil.match(ctx, [])
1142 file = req.req.qsparams.get('file')
1196 file = req.req.qsparams.get('file')
1143 if file:
1197 if file:
1144 pats = ['path:' + file]
1198 pats = ['path:' + file]
1145 match = scmutil.match(ctx, pats, default='path')
1199 match = scmutil.match(ctx, pats, default='path')
1146 if pats:
1200 if pats:
1147 files = [f for f in ctx.manifest().keys() if match(f)]
1201 files = [f for f in ctx.manifest().keys() if match(f)]
1148 if not files:
1202 if not files:
1149 raise ErrorResponse(HTTP_NOT_FOUND,
1203 raise ErrorResponse(HTTP_NOT_FOUND,
1150 'file(s) not found: %s' % file)
1204 'file(s) not found: %s' % file)
1151
1205
1152 mimetype, artype, extension, encoding = web.archivespecs[type_]
1206 mimetype, artype, extension, encoding = web.archivespecs[type_]
1153 headers = [
1207 headers = [
1154 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1208 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1155 ]
1209 ]
1156 if encoding:
1210 if encoding:
1157 headers.append(('Content-Encoding', encoding))
1211 headers.append(('Content-Encoding', encoding))
1158 req.headers.extend(headers)
1212 req.headers.extend(headers)
1159 req.respond(HTTP_OK, mimetype)
1213 req.respond(HTTP_OK, mimetype)
1160
1214
1161 archival.archive(web.repo, req, cnode, artype, prefix=name,
1215 archival.archive(web.repo, req, cnode, artype, prefix=name,
1162 matchfn=match,
1216 matchfn=match,
1163 subrepos=web.configbool("web", "archivesubrepos"))
1217 subrepos=web.configbool("web", "archivesubrepos"))
1164 return []
1218 return []
1165
1219
1166
1220
1167 @webcommand('static')
1221 @webcommand('static')
1168 def static(web, req, tmpl):
1222 def static(web, req, tmpl):
1169 fname = req.req.qsparams['file']
1223 fname = req.req.qsparams['file']
1170 # a repo owner may set web.static in .hg/hgrc to get any file
1224 # a repo owner may set web.static in .hg/hgrc to get any file
1171 # readable by the user running the CGI script
1225 # readable by the user running the CGI script
1172 static = web.config("web", "static", None, untrusted=False)
1226 static = web.config("web", "static", None, untrusted=False)
1173 if not static:
1227 if not static:
1174 tp = web.templatepath or templater.templatepaths()
1228 tp = web.templatepath or templater.templatepaths()
1175 if isinstance(tp, str):
1229 if isinstance(tp, str):
1176 tp = [tp]
1230 tp = [tp]
1177 static = [os.path.join(p, 'static') for p in tp]
1231 static = [os.path.join(p, 'static') for p in tp]
1178 staticfile(static, fname, req)
1232 staticfile(static, fname, req)
1179 return []
1233 return []
1180
1234
1181 @webcommand('graph')
1235 @webcommand('graph')
1182 def graph(web, req, tmpl):
1236 def graph(web, req, tmpl):
1183 """
1237 """
1184 /graph[/{revision}]
1238 /graph[/{revision}]
1185 -------------------
1239 -------------------
1186
1240
1187 Show information about the graphical topology of the repository.
1241 Show information about the graphical topology of the repository.
1188
1242
1189 Information rendered by this handler can be used to create visual
1243 Information rendered by this handler can be used to create visual
1190 representations of repository topology.
1244 representations of repository topology.
1191
1245
1192 The ``revision`` URL parameter controls the starting changeset. If it's
1246 The ``revision`` URL parameter controls the starting changeset. If it's
1193 absent, the default is ``tip``.
1247 absent, the default is ``tip``.
1194
1248
1195 The ``revcount`` query string argument can define the number of changesets
1249 The ``revcount`` query string argument can define the number of changesets
1196 to show information for.
1250 to show information for.
1197
1251
1198 The ``graphtop`` query string argument can specify the starting changeset
1252 The ``graphtop`` query string argument can specify the starting changeset
1199 for producing ``jsdata`` variable that is used for rendering graph in
1253 for producing ``jsdata`` variable that is used for rendering graph in
1200 JavaScript. By default it has the same value as ``revision``.
1254 JavaScript. By default it has the same value as ``revision``.
1201
1255
1202 This handler will render the ``graph`` template.
1256 This handler will render the ``graph`` template.
1203 """
1257 """
1204
1258
1205 if 'node' in req.req.qsparams:
1259 if 'node' in req.req.qsparams:
1206 ctx = webutil.changectx(web.repo, req)
1260 ctx = webutil.changectx(web.repo, req)
1207 symrev = webutil.symrevorshortnode(req, ctx)
1261 symrev = webutil.symrevorshortnode(req, ctx)
1208 else:
1262 else:
1209 ctx = web.repo['tip']
1263 ctx = web.repo['tip']
1210 symrev = 'tip'
1264 symrev = 'tip'
1211 rev = ctx.rev()
1265 rev = ctx.rev()
1212
1266
1213 bg_height = 39
1267 bg_height = 39
1214 revcount = web.maxshortchanges
1268 revcount = web.maxshortchanges
1215 if 'revcount' in req.req.qsparams:
1269 if 'revcount' in req.req.qsparams:
1216 try:
1270 try:
1217 revcount = int(req.req.qsparams.get('revcount', revcount))
1271 revcount = int(req.req.qsparams.get('revcount', revcount))
1218 revcount = max(revcount, 1)
1272 revcount = max(revcount, 1)
1219 tmpl.defaults['sessionvars']['revcount'] = revcount
1273 tmpl.defaults['sessionvars']['revcount'] = revcount
1220 except ValueError:
1274 except ValueError:
1221 pass
1275 pass
1222
1276
1223 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1277 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1224 lessvars['revcount'] = max(revcount // 2, 1)
1278 lessvars['revcount'] = max(revcount // 2, 1)
1225 morevars = copy.copy(tmpl.defaults['sessionvars'])
1279 morevars = copy.copy(tmpl.defaults['sessionvars'])
1226 morevars['revcount'] = revcount * 2
1280 morevars['revcount'] = revcount * 2
1227
1281
1228 graphtop = req.req.qsparams.get('graphtop', ctx.hex())
1282 graphtop = req.req.qsparams.get('graphtop', ctx.hex())
1229 graphvars = copy.copy(tmpl.defaults['sessionvars'])
1283 graphvars = copy.copy(tmpl.defaults['sessionvars'])
1230 graphvars['graphtop'] = graphtop
1284 graphvars['graphtop'] = graphtop
1231
1285
1232 count = len(web.repo)
1286 count = len(web.repo)
1233 pos = rev
1287 pos = rev
1234
1288
1235 uprev = min(max(0, count - 1), rev + revcount)
1289 uprev = min(max(0, count - 1), rev + revcount)
1236 downrev = max(0, rev - revcount)
1290 downrev = max(0, rev - revcount)
1237 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1291 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1238
1292
1239 tree = []
1293 tree = []
1240 nextentry = []
1294 nextentry = []
1241 lastrev = 0
1295 lastrev = 0
1242 if pos != -1:
1296 if pos != -1:
1243 allrevs = web.repo.changelog.revs(pos, 0)
1297 allrevs = web.repo.changelog.revs(pos, 0)
1244 revs = []
1298 revs = []
1245 for i in allrevs:
1299 for i in allrevs:
1246 revs.append(i)
1300 revs.append(i)
1247 if len(revs) >= revcount + 1:
1301 if len(revs) >= revcount + 1:
1248 break
1302 break
1249
1303
1250 if len(revs) > revcount:
1304 if len(revs) > revcount:
1251 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1305 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1252 revs = revs[:-1]
1306 revs = revs[:-1]
1253
1307
1254 lastrev = revs[-1]
1308 lastrev = revs[-1]
1255
1309
1256 # We have to feed a baseset to dagwalker as it is expecting smartset
1310 # We have to feed a baseset to dagwalker as it is expecting smartset
1257 # object. This does not have a big impact on hgweb performance itself
1311 # object. This does not have a big impact on hgweb performance itself
1258 # since hgweb graphing code is not itself lazy yet.
1312 # since hgweb graphing code is not itself lazy yet.
1259 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1313 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1260 # As we said one line above... not lazy.
1314 # As we said one line above... not lazy.
1261 tree = list(item for item in graphmod.colored(dag, web.repo)
1315 tree = list(item for item in graphmod.colored(dag, web.repo)
1262 if item[1] == graphmod.CHANGESET)
1316 if item[1] == graphmod.CHANGESET)
1263
1317
1264 def nodecurrent(ctx):
1318 def nodecurrent(ctx):
1265 wpnodes = web.repo.dirstate.parents()
1319 wpnodes = web.repo.dirstate.parents()
1266 if wpnodes[1] == nullid:
1320 if wpnodes[1] == nullid:
1267 wpnodes = wpnodes[:1]
1321 wpnodes = wpnodes[:1]
1268 if ctx.node() in wpnodes:
1322 if ctx.node() in wpnodes:
1269 return '@'
1323 return '@'
1270 return ''
1324 return ''
1271
1325
1272 def nodesymbol(ctx):
1326 def nodesymbol(ctx):
1273 if ctx.obsolete():
1327 if ctx.obsolete():
1274 return 'x'
1328 return 'x'
1275 elif ctx.isunstable():
1329 elif ctx.isunstable():
1276 return '*'
1330 return '*'
1277 elif ctx.closesbranch():
1331 elif ctx.closesbranch():
1278 return '_'
1332 return '_'
1279 else:
1333 else:
1280 return 'o'
1334 return 'o'
1281
1335
1282 def fulltree():
1336 def fulltree():
1283 pos = web.repo[graphtop].rev()
1337 pos = web.repo[graphtop].rev()
1284 tree = []
1338 tree = []
1285 if pos != -1:
1339 if pos != -1:
1286 revs = web.repo.changelog.revs(pos, lastrev)
1340 revs = web.repo.changelog.revs(pos, lastrev)
1287 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1341 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1288 tree = list(item for item in graphmod.colored(dag, web.repo)
1342 tree = list(item for item in graphmod.colored(dag, web.repo)
1289 if item[1] == graphmod.CHANGESET)
1343 if item[1] == graphmod.CHANGESET)
1290 return tree
1344 return tree
1291
1345
1292 def jsdata():
1346 def jsdata():
1293 return [{'node': pycompat.bytestr(ctx),
1347 return [{'node': pycompat.bytestr(ctx),
1294 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1348 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1295 'vertex': vtx,
1349 'vertex': vtx,
1296 'edges': edges}
1350 'edges': edges}
1297 for (id, type, ctx, vtx, edges) in fulltree()]
1351 for (id, type, ctx, vtx, edges) in fulltree()]
1298
1352
1299 def nodes():
1353 def nodes():
1300 parity = paritygen(web.stripecount)
1354 parity = paritygen(web.stripecount)
1301 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1355 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1302 entry = webutil.commonentry(web.repo, ctx)
1356 entry = webutil.commonentry(web.repo, ctx)
1303 edgedata = [{'col': edge[0],
1357 edgedata = [{'col': edge[0],
1304 'nextcol': edge[1],
1358 'nextcol': edge[1],
1305 'color': (edge[2] - 1) % 6 + 1,
1359 'color': (edge[2] - 1) % 6 + 1,
1306 'width': edge[3],
1360 'width': edge[3],
1307 'bcolor': edge[4]}
1361 'bcolor': edge[4]}
1308 for edge in edges]
1362 for edge in edges]
1309
1363
1310 entry.update({'col': vtx[0],
1364 entry.update({'col': vtx[0],
1311 'color': (vtx[1] - 1) % 6 + 1,
1365 'color': (vtx[1] - 1) % 6 + 1,
1312 'parity': next(parity),
1366 'parity': next(parity),
1313 'edges': edgedata,
1367 'edges': edgedata,
1314 'row': row,
1368 'row': row,
1315 'nextrow': row + 1})
1369 'nextrow': row + 1})
1316
1370
1317 yield entry
1371 yield entry
1318
1372
1319 rows = len(tree)
1373 rows = len(tree)
1320
1374
1321 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1375 web.res.setbodygen(tmpl(
1322 uprev=uprev,
1376 'graph',
1323 lessvars=lessvars, morevars=morevars, downrev=downrev,
1377 rev=rev,
1324 graphvars=graphvars,
1378 symrev=symrev,
1325 rows=rows,
1379 revcount=revcount,
1326 bg_height=bg_height,
1380 uprev=uprev,
1327 changesets=count,
1381 lessvars=lessvars,
1328 nextentry=nextentry,
1382 morevars=morevars,
1329 jsdata=lambda **x: jsdata(),
1383 downrev=downrev,
1330 nodes=lambda **x: nodes(),
1384 graphvars=graphvars,
1331 node=ctx.hex(), changenav=changenav)
1385 rows=rows,
1386 bg_height=bg_height,
1387 changesets=count,
1388 nextentry=nextentry,
1389 jsdata=lambda **x: jsdata(),
1390 nodes=lambda **x: nodes(),
1391 node=ctx.hex(),
1392 changenav=changenav))
1393
1394 return web.res
1332
1395
1333 def _getdoc(e):
1396 def _getdoc(e):
1334 doc = e[0].__doc__
1397 doc = e[0].__doc__
1335 if doc:
1398 if doc:
1336 doc = _(doc).partition('\n')[0]
1399 doc = _(doc).partition('\n')[0]
1337 else:
1400 else:
1338 doc = _('(no help text available)')
1401 doc = _('(no help text available)')
1339 return doc
1402 return doc
1340
1403
1341 @webcommand('help')
1404 @webcommand('help')
1342 def help(web, req, tmpl):
1405 def help(web, req, tmpl):
1343 """
1406 """
1344 /help[/{topic}]
1407 /help[/{topic}]
1345 ---------------
1408 ---------------
1346
1409
1347 Render help documentation.
1410 Render help documentation.
1348
1411
1349 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1412 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1350 is defined, that help topic will be rendered. If not, an index of
1413 is defined, that help topic will be rendered. If not, an index of
1351 available help topics will be rendered.
1414 available help topics will be rendered.
1352
1415
1353 The ``help`` template will be rendered when requesting help for a topic.
1416 The ``help`` template will be rendered when requesting help for a topic.
1354 ``helptopics`` will be rendered for the index of help topics.
1417 ``helptopics`` will be rendered for the index of help topics.
1355 """
1418 """
1356 from .. import commands, help as helpmod # avoid cycle
1419 from .. import commands, help as helpmod # avoid cycle
1357
1420
1358 topicname = req.req.qsparams.get('node')
1421 topicname = req.req.qsparams.get('node')
1359 if not topicname:
1422 if not topicname:
1360 def topics(**map):
1423 def topics(**map):
1361 for entries, summary, _doc in helpmod.helptable:
1424 for entries, summary, _doc in helpmod.helptable:
1362 yield {'topic': entries[0], 'summary': summary}
1425 yield {'topic': entries[0], 'summary': summary}
1363
1426
1364 early, other = [], []
1427 early, other = [], []
1365 primary = lambda s: s.partition('|')[0]
1428 primary = lambda s: s.partition('|')[0]
1366 for c, e in commands.table.iteritems():
1429 for c, e in commands.table.iteritems():
1367 doc = _getdoc(e)
1430 doc = _getdoc(e)
1368 if 'DEPRECATED' in doc or c.startswith('debug'):
1431 if 'DEPRECATED' in doc or c.startswith('debug'):
1369 continue
1432 continue
1370 cmd = primary(c)
1433 cmd = primary(c)
1371 if cmd.startswith('^'):
1434 if cmd.startswith('^'):
1372 early.append((cmd[1:], doc))
1435 early.append((cmd[1:], doc))
1373 else:
1436 else:
1374 other.append((cmd, doc))
1437 other.append((cmd, doc))
1375
1438
1376 early.sort()
1439 early.sort()
1377 other.sort()
1440 other.sort()
1378
1441
1379 def earlycommands(**map):
1442 def earlycommands(**map):
1380 for c, doc in early:
1443 for c, doc in early:
1381 yield {'topic': c, 'summary': doc}
1444 yield {'topic': c, 'summary': doc}
1382
1445
1383 def othercommands(**map):
1446 def othercommands(**map):
1384 for c, doc in other:
1447 for c, doc in other:
1385 yield {'topic': c, 'summary': doc}
1448 yield {'topic': c, 'summary': doc}
1386
1449
1387 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1450 web.res.setbodygen(tmpl(
1388 othercommands=othercommands, title='Index')
1451 'helptopics',
1452 topics=topics,
1453 earlycommands=earlycommands,
1454 othercommands=othercommands,
1455 title='Index'))
1456 return web.res
1389
1457
1390 # Render an index of sub-topics.
1458 # Render an index of sub-topics.
1391 if topicname in helpmod.subtopics:
1459 if topicname in helpmod.subtopics:
1392 topics = []
1460 topics = []
1393 for entries, summary, _doc in helpmod.subtopics[topicname]:
1461 for entries, summary, _doc in helpmod.subtopics[topicname]:
1394 topics.append({
1462 topics.append({
1395 'topic': '%s.%s' % (topicname, entries[0]),
1463 'topic': '%s.%s' % (topicname, entries[0]),
1396 'basename': entries[0],
1464 'basename': entries[0],
1397 'summary': summary,
1465 'summary': summary,
1398 })
1466 })
1399
1467
1400 return tmpl('helptopics', topics=topics, title=topicname,
1468 web.res.setbodygen(tmpl(
1401 subindex=True)
1469 'helptopics',
1470 topics=topics,
1471 title=topicname,
1472 subindex=True))
1473 return web.res
1402
1474
1403 u = webutil.wsgiui.load()
1475 u = webutil.wsgiui.load()
1404 u.verbose = True
1476 u.verbose = True
1405
1477
1406 # Render a page from a sub-topic.
1478 # Render a page from a sub-topic.
1407 if '.' in topicname:
1479 if '.' in topicname:
1408 # TODO implement support for rendering sections, like
1480 # TODO implement support for rendering sections, like
1409 # `hg help` works.
1481 # `hg help` works.
1410 topic, subtopic = topicname.split('.', 1)
1482 topic, subtopic = topicname.split('.', 1)
1411 if topic not in helpmod.subtopics:
1483 if topic not in helpmod.subtopics:
1412 raise ErrorResponse(HTTP_NOT_FOUND)
1484 raise ErrorResponse(HTTP_NOT_FOUND)
1413 else:
1485 else:
1414 topic = topicname
1486 topic = topicname
1415 subtopic = None
1487 subtopic = None
1416
1488
1417 try:
1489 try:
1418 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1490 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1419 except error.Abort:
1491 except error.Abort:
1420 raise ErrorResponse(HTTP_NOT_FOUND)
1492 raise ErrorResponse(HTTP_NOT_FOUND)
1421 return tmpl('help', topic=topicname, doc=doc)
1493
1494 web.res.setbodygen(tmpl(
1495 'help',
1496 topic=topicname,
1497 doc=doc))
1498
1499 return web.res
1422
1500
1423 # tell hggettext to extract docstrings from these functions:
1501 # tell hggettext to extract docstrings from these functions:
1424 i18nfunctions = commands.values()
1502 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now