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