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