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