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