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