##// END OF EJS Templates
hgweb: move archivelist() of hgwebdir to webutil
Yuya Nishihara -
r37531:40a7c1dd default
parent child Browse files
Show More
@@ -1,542 +1,526 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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 gc
12 12 import os
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_SERVER_ERROR,
20 20 cspvalues,
21 21 get_contact,
22 22 get_mtime,
23 23 ismember,
24 24 paritygen,
25 25 staticfile,
26 26 statusmessage,
27 27 )
28 28
29 29 from .. import (
30 30 configitems,
31 31 encoding,
32 32 error,
33 33 hg,
34 34 profiling,
35 35 pycompat,
36 36 scmutil,
37 37 templater,
38 38 templateutil,
39 39 ui as uimod,
40 40 util,
41 41 )
42 42
43 43 from . import (
44 44 hgweb_mod,
45 45 request as requestmod,
46 46 webutil,
47 47 wsgicgi,
48 48 )
49 49 from ..utils import dateutil
50 50
51 51 def cleannames(items):
52 52 return [(util.pconvert(name).strip('/'), path) for name, path in items]
53 53
54 54 def findrepos(paths):
55 55 repos = []
56 56 for prefix, root in cleannames(paths):
57 57 roothead, roottail = os.path.split(root)
58 58 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
59 59 # /bar/ be served as as foo/N .
60 60 # '*' will not search inside dirs with .hg (except .hg/patches),
61 61 # '**' will search inside dirs with .hg (and thus also find subrepos).
62 62 try:
63 63 recurse = {'*': False, '**': True}[roottail]
64 64 except KeyError:
65 65 repos.append((prefix, root))
66 66 continue
67 67 roothead = os.path.normpath(os.path.abspath(roothead))
68 68 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
69 69 repos.extend(urlrepos(prefix, roothead, paths))
70 70 return repos
71 71
72 72 def urlrepos(prefix, roothead, paths):
73 73 """yield url paths and filesystem paths from a list of repo paths
74 74
75 75 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
76 76 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
77 77 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
78 78 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
79 79 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
80 80 """
81 81 for path in paths:
82 82 path = os.path.normpath(path)
83 83 yield (prefix + '/' +
84 84 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
85 85
86 86 def readallowed(ui, req):
87 87 """Check allow_read and deny_read config options of a repo's ui object
88 88 to determine user permissions. By default, with neither option set (or
89 89 both empty), allow all users to read the repo. There are two ways a
90 90 user can be denied read access: (1) deny_read is not empty, and the
91 91 user is unauthenticated or deny_read contains user (or *), and (2)
92 92 allow_read is not empty and the user is not in allow_read. Return True
93 93 if user is allowed to read the repo, else return False."""
94 94
95 95 user = req.remoteuser
96 96
97 97 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
98 98 if deny_read and (not user or ismember(ui, user, deny_read)):
99 99 return False
100 100
101 101 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
102 102 # by default, allow reading if no allow_read option has been set
103 103 if not allow_read or ismember(ui, user, allow_read):
104 104 return True
105 105
106 106 return False
107 107
108 def archivelist(ui, nodeid, url):
109 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
110 archives = []
111
112 for typ, spec in webutil.archivespecs.iteritems():
113 if typ in allowed or ui.configbool('web', 'allow' + typ,
114 untrusted=True):
115 archives.append({
116 'type': typ,
117 'extension': spec[2],
118 'node': nodeid,
119 'url': url,
120 })
121
122 return archives
123
124 108 def rawindexentries(ui, repos, req, subdir=''):
125 109 descend = ui.configbool('web', 'descend')
126 110 collapse = ui.configbool('web', 'collapse')
127 111 seenrepos = set()
128 112 seendirs = set()
129 113 for name, path in repos:
130 114
131 115 if not name.startswith(subdir):
132 116 continue
133 117 name = name[len(subdir):]
134 118 directory = False
135 119
136 120 if '/' in name:
137 121 if not descend:
138 122 continue
139 123
140 124 nameparts = name.split('/')
141 125 rootname = nameparts[0]
142 126
143 127 if not collapse:
144 128 pass
145 129 elif rootname in seendirs:
146 130 continue
147 131 elif rootname in seenrepos:
148 132 pass
149 133 else:
150 134 directory = True
151 135 name = rootname
152 136
153 137 # redefine the path to refer to the directory
154 138 discarded = '/'.join(nameparts[1:])
155 139
156 140 # remove name parts plus accompanying slash
157 141 path = path[:-len(discarded) - 1]
158 142
159 143 try:
160 144 r = hg.repository(ui, path)
161 145 directory = False
162 146 except (IOError, error.RepoError):
163 147 pass
164 148
165 149 parts = [
166 150 req.apppath.strip('/'),
167 151 subdir.strip('/'),
168 152 name.strip('/'),
169 153 ]
170 154 url = '/' + '/'.join(p for p in parts if p) + '/'
171 155
172 156 # show either a directory entry or a repository
173 157 if directory:
174 158 # get the directory's time information
175 159 try:
176 160 d = (get_mtime(path), dateutil.makedate()[1])
177 161 except OSError:
178 162 continue
179 163
180 164 # add '/' to the name to make it obvious that
181 165 # the entry is a directory, not a regular repository
182 166 row = {'contact': "",
183 167 'contact_sort': "",
184 168 'name': name + '/',
185 169 'name_sort': name,
186 170 'url': url,
187 171 'description': "",
188 172 'description_sort': "",
189 173 'lastchange': d,
190 174 'lastchange_sort': d[1] - d[0],
191 175 'archives': [],
192 176 'isdirectory': True,
193 177 'labels': templateutil.hybridlist([], name='label'),
194 178 }
195 179
196 180 seendirs.add(name)
197 181 yield row
198 182 continue
199 183
200 184 u = ui.copy()
201 185 try:
202 186 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
203 187 except Exception as e:
204 188 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
205 189 continue
206 190
207 191 def get(section, name, default=uimod._unset):
208 192 return u.config(section, name, default, untrusted=True)
209 193
210 194 if u.configbool("web", "hidden", untrusted=True):
211 195 continue
212 196
213 197 if not readallowed(u, req):
214 198 continue
215 199
216 200 # update time with local timezone
217 201 try:
218 202 r = hg.repository(ui, path)
219 203 except IOError:
220 204 u.warn(_('error accessing repository at %s\n') % path)
221 205 continue
222 206 except error.RepoError:
223 207 u.warn(_('error accessing repository at %s\n') % path)
224 208 continue
225 209 try:
226 210 d = (get_mtime(r.spath), dateutil.makedate()[1])
227 211 except OSError:
228 212 continue
229 213
230 214 contact = get_contact(get)
231 215 description = get("web", "description")
232 216 seenrepos.add(name)
233 217 name = get("web", "name", name)
234 218 labels = u.configlist('web', 'labels', untrusted=True)
235 219 row = {'contact': contact or "unknown",
236 220 'contact_sort': contact.upper() or "unknown",
237 221 'name': name,
238 222 'name_sort': name,
239 223 'url': url,
240 224 'description': description or "unknown",
241 225 'description_sort': description.upper() or "unknown",
242 226 'lastchange': d,
243 227 'lastchange_sort': d[1] - d[0],
244 'archives': archivelist(u, "tip", url),
228 'archives': webutil.archivelist(u, "tip", url),
245 229 'isdirectory': None,
246 230 'labels': templateutil.hybridlist(labels, name='label'),
247 231 }
248 232
249 233 yield row
250 234
251 235 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
252 236 descending, subdir):
253 237 rows = rawindexentries(ui, repos, req, subdir=subdir)
254 238
255 239 sortdefault = None, False
256 240
257 241 if sortcolumn and sortdefault != (sortcolumn, descending):
258 242 sortkey = '%s_sort' % sortcolumn
259 243 rows = sorted(rows, key=lambda x: x[sortkey],
260 244 reverse=descending)
261 245
262 246 for row, parity in zip(rows, paritygen(stripecount)):
263 247 row['parity'] = parity
264 248 yield row
265 249
266 250 def indexentries(ui, repos, req, stripecount, sortcolumn='',
267 251 descending=False, subdir=''):
268 252 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
269 253 return templateutil.mappinggenerator(_indexentriesgen, args=args)
270 254
271 255 class hgwebdir(object):
272 256 """HTTP server for multiple repositories.
273 257
274 258 Given a configuration, different repositories will be served depending
275 259 on the request path.
276 260
277 261 Instances are typically used as WSGI applications.
278 262 """
279 263 def __init__(self, conf, baseui=None):
280 264 self.conf = conf
281 265 self.baseui = baseui
282 266 self.ui = None
283 267 self.lastrefresh = 0
284 268 self.motd = None
285 269 self.refresh()
286 270
287 271 def refresh(self):
288 272 if self.ui:
289 273 refreshinterval = self.ui.configint('web', 'refreshinterval')
290 274 else:
291 275 item = configitems.coreitems['web']['refreshinterval']
292 276 refreshinterval = item.default
293 277
294 278 # refreshinterval <= 0 means to always refresh.
295 279 if (refreshinterval > 0 and
296 280 self.lastrefresh + refreshinterval > time.time()):
297 281 return
298 282
299 283 if self.baseui:
300 284 u = self.baseui.copy()
301 285 else:
302 286 u = uimod.ui.load()
303 287 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
304 288 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
305 289 # displaying bundling progress bar while serving feels wrong and may
306 290 # break some wsgi implementations.
307 291 u.setconfig('progress', 'disable', 'true', 'hgweb')
308 292
309 293 if not isinstance(self.conf, (dict, list, tuple)):
310 294 map = {'paths': 'hgweb-paths'}
311 295 if not os.path.exists(self.conf):
312 296 raise error.Abort(_('config file %s not found!') % self.conf)
313 297 u.readconfig(self.conf, remap=map, trust=True)
314 298 paths = []
315 299 for name, ignored in u.configitems('hgweb-paths'):
316 300 for path in u.configlist('hgweb-paths', name):
317 301 paths.append((name, path))
318 302 elif isinstance(self.conf, (list, tuple)):
319 303 paths = self.conf
320 304 elif isinstance(self.conf, dict):
321 305 paths = self.conf.items()
322 306
323 307 repos = findrepos(paths)
324 308 for prefix, root in u.configitems('collections'):
325 309 prefix = util.pconvert(prefix)
326 310 for path in scmutil.walkrepos(root, followsym=True):
327 311 repo = os.path.normpath(path)
328 312 name = util.pconvert(repo)
329 313 if name.startswith(prefix):
330 314 name = name[len(prefix):]
331 315 repos.append((name.lstrip('/'), repo))
332 316
333 317 self.repos = repos
334 318 self.ui = u
335 319 encoding.encoding = self.ui.config('web', 'encoding')
336 320 self.style = self.ui.config('web', 'style')
337 321 self.templatepath = self.ui.config('web', 'templates', untrusted=False)
338 322 self.stripecount = self.ui.config('web', 'stripes')
339 323 if self.stripecount:
340 324 self.stripecount = int(self.stripecount)
341 325 prefix = self.ui.config('web', 'prefix')
342 326 if prefix.startswith('/'):
343 327 prefix = prefix[1:]
344 328 if prefix.endswith('/'):
345 329 prefix = prefix[:-1]
346 330 self.prefix = prefix
347 331 self.lastrefresh = time.time()
348 332
349 333 def run(self):
350 334 if not encoding.environ.get('GATEWAY_INTERFACE',
351 335 '').startswith("CGI/1."):
352 336 raise RuntimeError("This function is only intended to be "
353 337 "called while running as a CGI script.")
354 338 wsgicgi.launch(self)
355 339
356 340 def __call__(self, env, respond):
357 341 baseurl = self.ui.config('web', 'baseurl')
358 342 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
359 343 res = requestmod.wsgiresponse(req, respond)
360 344
361 345 return self.run_wsgi(req, res)
362 346
363 347 def run_wsgi(self, req, res):
364 348 profile = self.ui.configbool('profiling', 'enabled')
365 349 with profiling.profile(self.ui, enabled=profile):
366 350 try:
367 351 for r in self._runwsgi(req, res):
368 352 yield r
369 353 finally:
370 354 # There are known cycles in localrepository that prevent
371 355 # those objects (and tons of held references) from being
372 356 # collected through normal refcounting. We mitigate those
373 357 # leaks by performing an explicit GC on every request.
374 358 # TODO remove this once leaks are fixed.
375 359 # TODO only run this on requests that create localrepository
376 360 # instances instead of every request.
377 361 gc.collect()
378 362
379 363 def _runwsgi(self, req, res):
380 364 try:
381 365 self.refresh()
382 366
383 367 csp, nonce = cspvalues(self.ui)
384 368 if csp:
385 369 res.headers['Content-Security-Policy'] = csp
386 370
387 371 virtual = req.dispatchpath.strip('/')
388 372 tmpl = self.templater(req, nonce)
389 373 ctype = tmpl.render('mimetype', {'encoding': encoding.encoding})
390 374
391 375 # Global defaults. These can be overridden by any handler.
392 376 res.status = '200 Script output follows'
393 377 res.headers['Content-Type'] = ctype
394 378
395 379 # a static file
396 380 if virtual.startswith('static/') or 'static' in req.qsparams:
397 381 if virtual.startswith('static/'):
398 382 fname = virtual[7:]
399 383 else:
400 384 fname = req.qsparams['static']
401 385 static = self.ui.config("web", "static", None,
402 386 untrusted=False)
403 387 if not static:
404 388 tp = self.templatepath or templater.templatepaths()
405 389 if isinstance(tp, str):
406 390 tp = [tp]
407 391 static = [os.path.join(p, 'static') for p in tp]
408 392
409 393 staticfile(static, fname, res)
410 394 return res.sendresponse()
411 395
412 396 # top-level index
413 397
414 398 repos = dict(self.repos)
415 399
416 400 if (not virtual or virtual == 'index') and virtual not in repos:
417 401 return self.makeindex(req, res, tmpl)
418 402
419 403 # nested indexes and hgwebs
420 404
421 405 if virtual.endswith('/index') and virtual not in repos:
422 406 subdir = virtual[:-len('index')]
423 407 if any(r.startswith(subdir) for r in repos):
424 408 return self.makeindex(req, res, tmpl, subdir)
425 409
426 410 def _virtualdirs():
427 411 # Check the full virtual path, each parent, and the root ('')
428 412 if virtual != '':
429 413 yield virtual
430 414
431 415 for p in util.finddirs(virtual):
432 416 yield p
433 417
434 418 yield ''
435 419
436 420 for virtualrepo in _virtualdirs():
437 421 real = repos.get(virtualrepo)
438 422 if real:
439 423 # Re-parse the WSGI environment to take into account our
440 424 # repository path component.
441 425 req = requestmod.parserequestfromenv(
442 426 req.rawenv, reponame=virtualrepo,
443 427 altbaseurl=self.ui.config('web', 'baseurl'))
444 428 try:
445 429 # ensure caller gets private copy of ui
446 430 repo = hg.repository(self.ui.copy(), real)
447 431 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
448 432 except IOError as inst:
449 433 msg = encoding.strtolocal(inst.strerror)
450 434 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
451 435 except error.RepoError as inst:
452 436 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
453 437
454 438 # browse subdirectories
455 439 subdir = virtual + '/'
456 440 if [r for r in repos if r.startswith(subdir)]:
457 441 return self.makeindex(req, res, tmpl, subdir)
458 442
459 443 # prefixes not found
460 444 res.status = '404 Not Found'
461 445 res.setbodygen(tmpl.generate('notfound', {'repo': virtual}))
462 446 return res.sendresponse()
463 447
464 448 except ErrorResponse as e:
465 449 res.status = statusmessage(e.code, pycompat.bytestr(e))
466 450 res.setbodygen(tmpl.generate('error', {'error': e.message or ''}))
467 451 return res.sendresponse()
468 452 finally:
469 453 tmpl = None
470 454
471 455 def makeindex(self, req, res, tmpl, subdir=""):
472 456 self.refresh()
473 457 sortable = ["name", "description", "contact", "lastchange"]
474 458 sortcolumn, descending = None, False
475 459 if 'sort' in req.qsparams:
476 460 sortcolumn = req.qsparams['sort']
477 461 descending = sortcolumn.startswith('-')
478 462 if descending:
479 463 sortcolumn = sortcolumn[1:]
480 464 if sortcolumn not in sortable:
481 465 sortcolumn = ""
482 466
483 467 sort = [("sort_%s" % column,
484 468 "%s%s" % ((not descending and column == sortcolumn)
485 469 and "-" or "", column))
486 470 for column in sortable]
487 471
488 472 self.refresh()
489 473
490 474 entries = indexentries(self.ui, self.repos, req,
491 475 self.stripecount, sortcolumn=sortcolumn,
492 476 descending=descending, subdir=subdir)
493 477
494 478 mapping = {
495 479 'entries': entries,
496 480 'subdir': subdir,
497 481 'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
498 482 'sortcolumn': sortcolumn,
499 483 'descending': descending,
500 484 }
501 485 mapping.update(sort)
502 486 res.setbodygen(tmpl.generate('index', mapping))
503 487 return res.sendresponse()
504 488
505 489 def templater(self, req, nonce):
506 490
507 491 def motd(**map):
508 492 if self.motd is not None:
509 493 yield self.motd
510 494 else:
511 495 yield config('web', 'motd')
512 496
513 497 def config(section, name, default=uimod._unset, untrusted=True):
514 498 return self.ui.config(section, name, default, untrusted)
515 499
516 500 vars = {}
517 501 styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
518 502 self.templatepath)
519 503 if style == styles[0]:
520 504 vars['style'] = style
521 505
522 506 sessionvars = webutil.sessionvars(vars, r'?')
523 507 logourl = config('web', 'logourl')
524 508 logoimg = config('web', 'logoimg')
525 509 staticurl = (config('web', 'staticurl')
526 510 or req.apppath + '/static/')
527 511 if not staticurl.endswith('/'):
528 512 staticurl += '/'
529 513
530 514 defaults = {
531 515 "encoding": encoding.encoding,
532 516 "motd": motd,
533 517 "url": req.apppath + '/',
534 518 "logourl": logourl,
535 519 "logoimg": logoimg,
536 520 "staticurl": staticurl,
537 521 "sessionvars": sessionvars,
538 522 "style": style,
539 523 "nonce": nonce,
540 524 }
541 525 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
542 526 return tmpl
@@ -1,700 +1,716 b''
1 1 # hgweb/webutil.py - utility library for the web interface.
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 copy
12 12 import difflib
13 13 import os
14 14 import re
15 15
16 16 from ..i18n import _
17 17 from ..node import hex, nullid, short
18 18
19 19 from .common import (
20 20 ErrorResponse,
21 21 HTTP_BAD_REQUEST,
22 22 HTTP_NOT_FOUND,
23 23 paritygen,
24 24 )
25 25
26 26 from .. import (
27 27 context,
28 28 error,
29 29 match,
30 30 mdiff,
31 31 obsutil,
32 32 patch,
33 33 pathutil,
34 34 pycompat,
35 35 scmutil,
36 36 templatefilters,
37 37 templatekw,
38 38 ui as uimod,
39 39 util,
40 40 )
41 41
42 42 from ..utils import (
43 43 stringutil,
44 44 )
45 45
46 46 archivespecs = util.sortdict((
47 47 ('zip', ('application/zip', 'zip', '.zip', None)),
48 48 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
49 49 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
50 50 ))
51 51
52 def archivelist(ui, nodeid, url):
53 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
54 archives = []
55
56 for typ, spec in archivespecs.iteritems():
57 if typ in allowed or ui.configbool('web', 'allow' + typ,
58 untrusted=True):
59 archives.append({
60 'type': typ,
61 'extension': spec[2],
62 'node': nodeid,
63 'url': url,
64 })
65
66 return archives
67
52 68 def up(p):
53 69 if p[0:1] != "/":
54 70 p = "/" + p
55 71 if p[-1:] == "/":
56 72 p = p[:-1]
57 73 up = os.path.dirname(p)
58 74 if up == "/":
59 75 return "/"
60 76 return up + "/"
61 77
62 78 def _navseq(step, firststep=None):
63 79 if firststep:
64 80 yield firststep
65 81 if firststep >= 20 and firststep <= 40:
66 82 firststep = 50
67 83 yield firststep
68 84 assert step > 0
69 85 assert firststep > 0
70 86 while step <= firststep:
71 87 step *= 10
72 88 while True:
73 89 yield 1 * step
74 90 yield 3 * step
75 91 step *= 10
76 92
77 93 class revnav(object):
78 94
79 95 def __init__(self, repo):
80 96 """Navigation generation object
81 97
82 98 :repo: repo object we generate nav for
83 99 """
84 100 # used for hex generation
85 101 self._revlog = repo.changelog
86 102
87 103 def __nonzero__(self):
88 104 """return True if any revision to navigate over"""
89 105 return self._first() is not None
90 106
91 107 __bool__ = __nonzero__
92 108
93 109 def _first(self):
94 110 """return the minimum non-filtered changeset or None"""
95 111 try:
96 112 return next(iter(self._revlog))
97 113 except StopIteration:
98 114 return None
99 115
100 116 def hex(self, rev):
101 117 return hex(self._revlog.node(rev))
102 118
103 119 def gen(self, pos, pagelen, limit):
104 120 """computes label and revision id for navigation link
105 121
106 122 :pos: is the revision relative to which we generate navigation.
107 123 :pagelen: the size of each navigation page
108 124 :limit: how far shall we link
109 125
110 126 The return is:
111 127 - a single element tuple
112 128 - containing a dictionary with a `before` and `after` key
113 129 - values are generator functions taking arbitrary number of kwargs
114 130 - yield items are dictionaries with `label` and `node` keys
115 131 """
116 132 if not self:
117 133 # empty repo
118 134 return ({'before': (), 'after': ()},)
119 135
120 136 targets = []
121 137 for f in _navseq(1, pagelen):
122 138 if f > limit:
123 139 break
124 140 targets.append(pos + f)
125 141 targets.append(pos - f)
126 142 targets.sort()
127 143
128 144 first = self._first()
129 145 navbefore = [("(%i)" % first, self.hex(first))]
130 146 navafter = []
131 147 for rev in targets:
132 148 if rev not in self._revlog:
133 149 continue
134 150 if pos < rev < limit:
135 151 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
136 152 if 0 < rev < pos:
137 153 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
138 154
139 155
140 156 navafter.append(("tip", "tip"))
141 157
142 158 data = lambda i: {"label": i[0], "node": i[1]}
143 159 return ({'before': lambda **map: (data(i) for i in navbefore),
144 160 'after': lambda **map: (data(i) for i in navafter)},)
145 161
146 162 class filerevnav(revnav):
147 163
148 164 def __init__(self, repo, path):
149 165 """Navigation generation object
150 166
151 167 :repo: repo object we generate nav for
152 168 :path: path of the file we generate nav for
153 169 """
154 170 # used for iteration
155 171 self._changelog = repo.unfiltered().changelog
156 172 # used for hex generation
157 173 self._revlog = repo.file(path)
158 174
159 175 def hex(self, rev):
160 176 return hex(self._changelog.node(self._revlog.linkrev(rev)))
161 177
162 178 class _siblings(object):
163 179 def __init__(self, siblings=None, hiderev=None):
164 180 if siblings is None:
165 181 siblings = []
166 182 self.siblings = [s for s in siblings if s.node() != nullid]
167 183 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
168 184 self.siblings = []
169 185
170 186 def __iter__(self):
171 187 for s in self.siblings:
172 188 d = {
173 189 'node': s.hex(),
174 190 'rev': s.rev(),
175 191 'user': s.user(),
176 192 'date': s.date(),
177 193 'description': s.description(),
178 194 'branch': s.branch(),
179 195 }
180 196 if util.safehasattr(s, 'path'):
181 197 d['file'] = s.path()
182 198 yield d
183 199
184 200 def __len__(self):
185 201 return len(self.siblings)
186 202
187 203 def difffeatureopts(req, ui, section):
188 204 diffopts = patch.difffeatureopts(ui, untrusted=True,
189 205 section=section, whitespace=True)
190 206
191 207 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
192 208 v = req.qsparams.get(k)
193 209 if v is not None:
194 210 v = stringutil.parsebool(v)
195 211 setattr(diffopts, k, v if v is not None else True)
196 212
197 213 return diffopts
198 214
199 215 def annotate(req, fctx, ui):
200 216 diffopts = difffeatureopts(req, ui, 'annotate')
201 217 return fctx.annotate(follow=True, diffopts=diffopts)
202 218
203 219 def parents(ctx, hide=None):
204 220 if isinstance(ctx, context.basefilectx):
205 221 introrev = ctx.introrev()
206 222 if ctx.changectx().rev() != introrev:
207 223 return _siblings([ctx.repo()[introrev]], hide)
208 224 return _siblings(ctx.parents(), hide)
209 225
210 226 def children(ctx, hide=None):
211 227 return _siblings(ctx.children(), hide)
212 228
213 229 def renamelink(fctx):
214 230 r = fctx.renamed()
215 231 if r:
216 232 return [{'file': r[0], 'node': hex(r[1])}]
217 233 return []
218 234
219 235 def nodetagsdict(repo, node):
220 236 return [{"name": i} for i in repo.nodetags(node)]
221 237
222 238 def nodebookmarksdict(repo, node):
223 239 return [{"name": i} for i in repo.nodebookmarks(node)]
224 240
225 241 def nodebranchdict(repo, ctx):
226 242 branches = []
227 243 branch = ctx.branch()
228 244 # If this is an empty repo, ctx.node() == nullid,
229 245 # ctx.branch() == 'default'.
230 246 try:
231 247 branchnode = repo.branchtip(branch)
232 248 except error.RepoLookupError:
233 249 branchnode = None
234 250 if branchnode == ctx.node():
235 251 branches.append({"name": branch})
236 252 return branches
237 253
238 254 def nodeinbranch(repo, ctx):
239 255 branches = []
240 256 branch = ctx.branch()
241 257 try:
242 258 branchnode = repo.branchtip(branch)
243 259 except error.RepoLookupError:
244 260 branchnode = None
245 261 if branch != 'default' and branchnode != ctx.node():
246 262 branches.append({"name": branch})
247 263 return branches
248 264
249 265 def nodebranchnodefault(ctx):
250 266 branches = []
251 267 branch = ctx.branch()
252 268 if branch != 'default':
253 269 branches.append({"name": branch})
254 270 return branches
255 271
256 272 def showtag(repo, tmpl, t1, node=nullid, **args):
257 273 args = pycompat.byteskwargs(args)
258 274 for t in repo.nodetags(node):
259 275 lm = args.copy()
260 276 lm['tag'] = t
261 277 yield tmpl.generate(t1, lm)
262 278
263 279 def showbookmark(repo, tmpl, t1, node=nullid, **args):
264 280 args = pycompat.byteskwargs(args)
265 281 for t in repo.nodebookmarks(node):
266 282 lm = args.copy()
267 283 lm['bookmark'] = t
268 284 yield tmpl.generate(t1, lm)
269 285
270 286 def branchentries(repo, stripecount, limit=0):
271 287 tips = []
272 288 heads = repo.heads()
273 289 parity = paritygen(stripecount)
274 290 sortkey = lambda item: (not item[1], item[0].rev())
275 291
276 292 def entries(**map):
277 293 count = 0
278 294 if not tips:
279 295 for tag, hs, tip, closed in repo.branchmap().iterbranches():
280 296 tips.append((repo[tip], closed))
281 297 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
282 298 if limit > 0 and count >= limit:
283 299 return
284 300 count += 1
285 301 if closed:
286 302 status = 'closed'
287 303 elif ctx.node() not in heads:
288 304 status = 'inactive'
289 305 else:
290 306 status = 'open'
291 307 yield {
292 308 'parity': next(parity),
293 309 'branch': ctx.branch(),
294 310 'status': status,
295 311 'node': ctx.hex(),
296 312 'date': ctx.date()
297 313 }
298 314
299 315 return entries
300 316
301 317 def cleanpath(repo, path):
302 318 path = path.lstrip('/')
303 319 return pathutil.canonpath(repo.root, '', path)
304 320
305 321 def changectx(repo, req):
306 322 changeid = "tip"
307 323 if 'node' in req.qsparams:
308 324 changeid = req.qsparams['node']
309 325 ipos = changeid.find(':')
310 326 if ipos != -1:
311 327 changeid = changeid[(ipos + 1):]
312 328
313 329 return scmutil.revsymbol(repo, changeid)
314 330
315 331 def basechangectx(repo, req):
316 332 if 'node' in req.qsparams:
317 333 changeid = req.qsparams['node']
318 334 ipos = changeid.find(':')
319 335 if ipos != -1:
320 336 changeid = changeid[:ipos]
321 337 return scmutil.revsymbol(repo, changeid)
322 338
323 339 return None
324 340
325 341 def filectx(repo, req):
326 342 if 'file' not in req.qsparams:
327 343 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
328 344 path = cleanpath(repo, req.qsparams['file'])
329 345 if 'node' in req.qsparams:
330 346 changeid = req.qsparams['node']
331 347 elif 'filenode' in req.qsparams:
332 348 changeid = req.qsparams['filenode']
333 349 else:
334 350 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
335 351 try:
336 352 fctx = scmutil.revsymbol(repo, changeid)[path]
337 353 except error.RepoError:
338 354 fctx = repo.filectx(path, fileid=changeid)
339 355
340 356 return fctx
341 357
342 358 def linerange(req):
343 359 linerange = req.qsparams.getall('linerange')
344 360 if not linerange:
345 361 return None
346 362 if len(linerange) > 1:
347 363 raise ErrorResponse(HTTP_BAD_REQUEST,
348 364 'redundant linerange parameter')
349 365 try:
350 366 fromline, toline = map(int, linerange[0].split(':', 1))
351 367 except ValueError:
352 368 raise ErrorResponse(HTTP_BAD_REQUEST,
353 369 'invalid linerange parameter')
354 370 try:
355 371 return util.processlinerange(fromline, toline)
356 372 except error.ParseError as exc:
357 373 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
358 374
359 375 def formatlinerange(fromline, toline):
360 376 return '%d:%d' % (fromline + 1, toline)
361 377
362 378 def succsandmarkers(context, mapping):
363 379 repo = context.resource(mapping, 'repo')
364 380 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
365 381 for item in itemmappings.tovalue(context, mapping):
366 382 item['successors'] = _siblings(repo[successor]
367 383 for successor in item['successors'])
368 384 yield item
369 385
370 386 # teach templater succsandmarkers is switched to (context, mapping) API
371 387 succsandmarkers._requires = {'repo', 'ctx'}
372 388
373 389 def whyunstable(context, mapping):
374 390 repo = context.resource(mapping, 'repo')
375 391 ctx = context.resource(mapping, 'ctx')
376 392
377 393 entries = obsutil.whyunstable(repo, ctx)
378 394 for entry in entries:
379 395 if entry.get('divergentnodes'):
380 396 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
381 397 yield entry
382 398
383 399 whyunstable._requires = {'repo', 'ctx'}
384 400
385 401 def commonentry(repo, ctx):
386 402 node = ctx.node()
387 403 return {
388 404 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
389 405 # filectx, but I'm not pretty sure if that would always work because
390 406 # fctx.parents() != fctx.changectx.parents() for example.
391 407 'ctx': ctx,
392 408 'rev': ctx.rev(),
393 409 'node': hex(node),
394 410 'author': ctx.user(),
395 411 'desc': ctx.description(),
396 412 'date': ctx.date(),
397 413 'extra': ctx.extra(),
398 414 'phase': ctx.phasestr(),
399 415 'obsolete': ctx.obsolete(),
400 416 'succsandmarkers': succsandmarkers,
401 417 'instabilities': [{"instability": i} for i in ctx.instabilities()],
402 418 'whyunstable': whyunstable,
403 419 'branch': nodebranchnodefault(ctx),
404 420 'inbranch': nodeinbranch(repo, ctx),
405 421 'branches': nodebranchdict(repo, ctx),
406 422 'tags': nodetagsdict(repo, node),
407 423 'bookmarks': nodebookmarksdict(repo, node),
408 424 'parent': lambda **x: parents(ctx),
409 425 'child': lambda **x: children(ctx),
410 426 }
411 427
412 428 def changelistentry(web, ctx):
413 429 '''Obtain a dictionary to be used for entries in a changelist.
414 430
415 431 This function is called when producing items for the "entries" list passed
416 432 to the "shortlog" and "changelog" templates.
417 433 '''
418 434 repo = web.repo
419 435 rev = ctx.rev()
420 436 n = ctx.node()
421 437 showtags = showtag(repo, web.tmpl, 'changelogtag', n)
422 438 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
423 439
424 440 entry = commonentry(repo, ctx)
425 441 entry.update(
426 442 allparents=lambda **x: parents(ctx),
427 443 parent=lambda **x: parents(ctx, rev - 1),
428 444 child=lambda **x: children(ctx, rev + 1),
429 445 changelogtag=showtags,
430 446 files=files,
431 447 )
432 448 return entry
433 449
434 450 def symrevorshortnode(req, ctx):
435 451 if 'node' in req.qsparams:
436 452 return templatefilters.revescape(req.qsparams['node'])
437 453 else:
438 454 return short(ctx.node())
439 455
440 456 def changesetentry(web, ctx):
441 457 '''Obtain a dictionary to be used to render the "changeset" template.'''
442 458
443 459 showtags = showtag(web.repo, web.tmpl, 'changesettag', ctx.node())
444 460 showbookmarks = showbookmark(web.repo, web.tmpl, 'changesetbookmark',
445 461 ctx.node())
446 462 showbranch = nodebranchnodefault(ctx)
447 463
448 464 files = []
449 465 parity = paritygen(web.stripecount)
450 466 for blockno, f in enumerate(ctx.files()):
451 467 template = 'filenodelink' if f in ctx else 'filenolink'
452 468 files.append(web.tmpl.generate(template, {
453 469 'node': ctx.hex(),
454 470 'file': f,
455 471 'blockno': blockno + 1,
456 472 'parity': next(parity),
457 473 }))
458 474
459 475 basectx = basechangectx(web.repo, web.req)
460 476 if basectx is None:
461 477 basectx = ctx.p1()
462 478
463 479 style = web.config('web', 'style')
464 480 if 'style' in web.req.qsparams:
465 481 style = web.req.qsparams['style']
466 482
467 483 diff = diffs(web, ctx, basectx, None, style)
468 484
469 485 parity = paritygen(web.stripecount)
470 486 diffstatsgen = diffstatgen(ctx, basectx)
471 487 diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
472 488
473 489 return dict(
474 490 diff=diff,
475 491 symrev=symrevorshortnode(web.req, ctx),
476 492 basenode=basectx.hex(),
477 493 changesettag=showtags,
478 494 changesetbookmark=showbookmarks,
479 495 changesetbranch=showbranch,
480 496 files=files,
481 497 diffsummary=lambda **x: diffsummary(diffstatsgen),
482 498 diffstat=diffstats,
483 499 archives=web.archivelist(ctx.hex()),
484 500 **pycompat.strkwargs(commonentry(web.repo, ctx)))
485 501
486 502 def listfilediffs(tmpl, files, node, max):
487 503 for f in files[:max]:
488 504 yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
489 505 if len(files) > max:
490 506 yield tmpl.generate('fileellipses', {})
491 507
492 508 def diffs(web, ctx, basectx, files, style, linerange=None,
493 509 lineidprefix=''):
494 510
495 511 def prettyprintlines(lines, blockno):
496 512 for lineno, l in enumerate(lines, 1):
497 513 difflineno = "%d.%d" % (blockno, lineno)
498 514 if l.startswith('+'):
499 515 ltype = "difflineplus"
500 516 elif l.startswith('-'):
501 517 ltype = "difflineminus"
502 518 elif l.startswith('@'):
503 519 ltype = "difflineat"
504 520 else:
505 521 ltype = "diffline"
506 522 yield web.tmpl.generate(ltype, {
507 523 'line': l,
508 524 'lineno': lineno,
509 525 'lineid': lineidprefix + "l%s" % difflineno,
510 526 'linenumber': "% 8s" % difflineno,
511 527 })
512 528
513 529 repo = web.repo
514 530 if files:
515 531 m = match.exact(repo.root, repo.getcwd(), files)
516 532 else:
517 533 m = match.always(repo.root, repo.getcwd())
518 534
519 535 diffopts = patch.diffopts(repo.ui, untrusted=True)
520 536 node1 = basectx.node()
521 537 node2 = ctx.node()
522 538 parity = paritygen(web.stripecount)
523 539
524 540 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
525 541 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
526 542 if style != 'raw':
527 543 header = header[1:]
528 544 lines = [h + '\n' for h in header]
529 545 for hunkrange, hunklines in hunks:
530 546 if linerange is not None and hunkrange is not None:
531 547 s1, l1, s2, l2 = hunkrange
532 548 if not mdiff.hunkinrange((s2, l2), linerange):
533 549 continue
534 550 lines.extend(hunklines)
535 551 if lines:
536 552 yield web.tmpl.generate('diffblock', {
537 553 'parity': next(parity),
538 554 'blockno': blockno,
539 555 'lines': prettyprintlines(lines, blockno),
540 556 })
541 557
542 558 def compare(tmpl, context, leftlines, rightlines):
543 559 '''Generator function that provides side-by-side comparison data.'''
544 560
545 561 def compline(type, leftlineno, leftline, rightlineno, rightline):
546 562 lineid = leftlineno and ("l%d" % leftlineno) or ''
547 563 lineid += rightlineno and ("r%d" % rightlineno) or ''
548 564 llno = '%d' % leftlineno if leftlineno else ''
549 565 rlno = '%d' % rightlineno if rightlineno else ''
550 566 return tmpl.generate('comparisonline', {
551 567 'type': type,
552 568 'lineid': lineid,
553 569 'leftlineno': leftlineno,
554 570 'leftlinenumber': "% 6s" % llno,
555 571 'leftline': leftline or '',
556 572 'rightlineno': rightlineno,
557 573 'rightlinenumber': "% 6s" % rlno,
558 574 'rightline': rightline or '',
559 575 })
560 576
561 577 def getblock(opcodes):
562 578 for type, llo, lhi, rlo, rhi in opcodes:
563 579 len1 = lhi - llo
564 580 len2 = rhi - rlo
565 581 count = min(len1, len2)
566 582 for i in xrange(count):
567 583 yield compline(type=type,
568 584 leftlineno=llo + i + 1,
569 585 leftline=leftlines[llo + i],
570 586 rightlineno=rlo + i + 1,
571 587 rightline=rightlines[rlo + i])
572 588 if len1 > len2:
573 589 for i in xrange(llo + count, lhi):
574 590 yield compline(type=type,
575 591 leftlineno=i + 1,
576 592 leftline=leftlines[i],
577 593 rightlineno=None,
578 594 rightline=None)
579 595 elif len2 > len1:
580 596 for i in xrange(rlo + count, rhi):
581 597 yield compline(type=type,
582 598 leftlineno=None,
583 599 leftline=None,
584 600 rightlineno=i + 1,
585 601 rightline=rightlines[i])
586 602
587 603 s = difflib.SequenceMatcher(None, leftlines, rightlines)
588 604 if context < 0:
589 605 yield tmpl.generate('comparisonblock',
590 606 {'lines': getblock(s.get_opcodes())})
591 607 else:
592 608 for oc in s.get_grouped_opcodes(n=context):
593 609 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
594 610
595 611 def diffstatgen(ctx, basectx):
596 612 '''Generator function that provides the diffstat data.'''
597 613
598 614 stats = patch.diffstatdata(
599 615 util.iterlines(ctx.diff(basectx, noprefix=False)))
600 616 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
601 617 while True:
602 618 yield stats, maxname, maxtotal, addtotal, removetotal, binary
603 619
604 620 def diffsummary(statgen):
605 621 '''Return a short summary of the diff.'''
606 622
607 623 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
608 624 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
609 625 len(stats), addtotal, removetotal)
610 626
611 627 def diffstat(tmpl, ctx, statgen, parity):
612 628 '''Return a diffstat template for each file in the diff.'''
613 629
614 630 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
615 631 files = ctx.files()
616 632
617 633 def pct(i):
618 634 if maxtotal == 0:
619 635 return 0
620 636 return (float(i) / maxtotal) * 100
621 637
622 638 fileno = 0
623 639 for filename, adds, removes, isbinary in stats:
624 640 template = 'diffstatlink' if filename in files else 'diffstatnolink'
625 641 total = adds + removes
626 642 fileno += 1
627 643 yield tmpl.generate(template, {
628 644 'node': ctx.hex(),
629 645 'file': filename,
630 646 'fileno': fileno,
631 647 'total': total,
632 648 'addpct': pct(adds),
633 649 'removepct': pct(removes),
634 650 'parity': next(parity),
635 651 })
636 652
637 653 class sessionvars(object):
638 654 def __init__(self, vars, start='?'):
639 655 self.start = start
640 656 self.vars = vars
641 657 def __getitem__(self, key):
642 658 return self.vars[key]
643 659 def __setitem__(self, key, value):
644 660 self.vars[key] = value
645 661 def __copy__(self):
646 662 return sessionvars(copy.copy(self.vars), self.start)
647 663 def __iter__(self):
648 664 separator = self.start
649 665 for key, value in sorted(self.vars.iteritems()):
650 666 yield {'name': key,
651 667 'value': pycompat.bytestr(value),
652 668 'separator': separator,
653 669 }
654 670 separator = '&'
655 671
656 672 class wsgiui(uimod.ui):
657 673 # default termwidth breaks under mod_wsgi
658 674 def termwidth(self):
659 675 return 80
660 676
661 677 def getwebsubs(repo):
662 678 websubtable = []
663 679 websubdefs = repo.ui.configitems('websub')
664 680 # we must maintain interhg backwards compatibility
665 681 websubdefs += repo.ui.configitems('interhg')
666 682 for key, pattern in websubdefs:
667 683 # grab the delimiter from the character after the "s"
668 684 unesc = pattern[1:2]
669 685 delim = re.escape(unesc)
670 686
671 687 # identify portions of the pattern, taking care to avoid escaped
672 688 # delimiters. the replace format and flags are optional, but
673 689 # delimiters are required.
674 690 match = re.match(
675 691 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
676 692 % (delim, delim, delim), pattern)
677 693 if not match:
678 694 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
679 695 % (key, pattern))
680 696 continue
681 697
682 698 # we need to unescape the delimiter for regexp and format
683 699 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
684 700 regexp = delim_re.sub(unesc, match.group(1))
685 701 format = delim_re.sub(unesc, match.group(2))
686 702
687 703 # the pattern allows for 6 regexp flags, so set them if necessary
688 704 flagin = match.group(3)
689 705 flags = 0
690 706 if flagin:
691 707 for flag in flagin.upper():
692 708 flags |= re.__dict__[flag]
693 709
694 710 try:
695 711 regexp = re.compile(regexp, flags)
696 712 websubtable.append((regexp, format))
697 713 except re.error:
698 714 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
699 715 % (key, regexp))
700 716 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now