##// END OF EJS Templates
webcommands: document "tags" web command
Gregory Szorc -
r24084:ef06e2b1 default
parent child Browse files
Show More
@@ -1,1160 +1,1170 b''
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import os, mimetypes, re, cgi, copy
9 9 import webutil
10 10 from mercurial import error, encoding, archival, templater, templatefilters
11 11 from mercurial.node import short, hex
12 12 from mercurial import util
13 13 from common import paritygen, staticfile, get_contact, ErrorResponse
14 14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
15 15 from mercurial import graphmod, patch
16 16 from mercurial import scmutil
17 17 from mercurial.i18n import _
18 18 from mercurial.error import ParseError, RepoLookupError, Abort
19 19 from mercurial import revset
20 20
21 21 __all__ = []
22 22 commands = {}
23 23
24 24 class webcommand(object):
25 25 """Decorator used to register a web command handler.
26 26
27 27 The decorator takes as its positional arguments the name/path the
28 28 command should be accessible under.
29 29
30 30 Usage:
31 31
32 32 @webcommand('mycommand')
33 33 def mycommand(web, req, tmpl):
34 34 pass
35 35 """
36 36
37 37 def __init__(self, name):
38 38 self.name = name
39 39
40 40 def __call__(self, func):
41 41 __all__.append(self.name)
42 42 commands[self.name] = func
43 43 return func
44 44
45 45 @webcommand('log')
46 46 def log(web, req, tmpl):
47 47 if 'file' in req.form and req.form['file'][0]:
48 48 return filelog(web, req, tmpl)
49 49 else:
50 50 return changelog(web, req, tmpl)
51 51
52 52 @webcommand('rawfile')
53 53 def rawfile(web, req, tmpl):
54 54 guessmime = web.configbool('web', 'guessmime', False)
55 55
56 56 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
57 57 if not path:
58 58 content = manifest(web, req, tmpl)
59 59 req.respond(HTTP_OK, web.ctype)
60 60 return content
61 61
62 62 try:
63 63 fctx = webutil.filectx(web.repo, req)
64 64 except error.LookupError, inst:
65 65 try:
66 66 content = manifest(web, req, tmpl)
67 67 req.respond(HTTP_OK, web.ctype)
68 68 return content
69 69 except ErrorResponse:
70 70 raise inst
71 71
72 72 path = fctx.path()
73 73 text = fctx.data()
74 74 mt = 'application/binary'
75 75 if guessmime:
76 76 mt = mimetypes.guess_type(path)[0]
77 77 if mt is None:
78 78 mt = util.binary(text) and 'application/binary' or 'text/plain'
79 79 if mt.startswith('text/'):
80 80 mt += '; charset="%s"' % encoding.encoding
81 81
82 82 req.respond(HTTP_OK, mt, path, body=text)
83 83 return []
84 84
85 85 def _filerevision(web, tmpl, fctx):
86 86 f = fctx.path()
87 87 text = fctx.data()
88 88 parity = paritygen(web.stripecount)
89 89
90 90 if util.binary(text):
91 91 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
92 92 text = '(binary:%s)' % mt
93 93
94 94 def lines():
95 95 for lineno, t in enumerate(text.splitlines(True)):
96 96 yield {"line": t,
97 97 "lineid": "l%d" % (lineno + 1),
98 98 "linenumber": "% 6d" % (lineno + 1),
99 99 "parity": parity.next()}
100 100
101 101 return tmpl("filerevision",
102 102 file=f,
103 103 path=webutil.up(f),
104 104 text=lines(),
105 105 rev=fctx.rev(),
106 106 node=fctx.hex(),
107 107 author=fctx.user(),
108 108 date=fctx.date(),
109 109 desc=fctx.description(),
110 110 extra=fctx.extra(),
111 111 branch=webutil.nodebranchnodefault(fctx),
112 112 parent=webutil.parents(fctx),
113 113 child=webutil.children(fctx),
114 114 rename=webutil.renamelink(fctx),
115 115 permissions=fctx.manifest().flags(f))
116 116
117 117 @webcommand('file')
118 118 def file(web, req, tmpl):
119 119 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
120 120 if not path:
121 121 return manifest(web, req, tmpl)
122 122 try:
123 123 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
124 124 except error.LookupError, inst:
125 125 try:
126 126 return manifest(web, req, tmpl)
127 127 except ErrorResponse:
128 128 raise inst
129 129
130 130 def _search(web, req, tmpl):
131 131 MODE_REVISION = 'rev'
132 132 MODE_KEYWORD = 'keyword'
133 133 MODE_REVSET = 'revset'
134 134
135 135 def revsearch(ctx):
136 136 yield ctx
137 137
138 138 def keywordsearch(query):
139 139 lower = encoding.lower
140 140 qw = lower(query).split()
141 141
142 142 def revgen():
143 143 cl = web.repo.changelog
144 144 for i in xrange(len(web.repo) - 1, 0, -100):
145 145 l = []
146 146 for j in cl.revs(max(0, i - 99), i):
147 147 ctx = web.repo[j]
148 148 l.append(ctx)
149 149 l.reverse()
150 150 for e in l:
151 151 yield e
152 152
153 153 for ctx in revgen():
154 154 miss = 0
155 155 for q in qw:
156 156 if not (q in lower(ctx.user()) or
157 157 q in lower(ctx.description()) or
158 158 q in lower(" ".join(ctx.files()))):
159 159 miss = 1
160 160 break
161 161 if miss:
162 162 continue
163 163
164 164 yield ctx
165 165
166 166 def revsetsearch(revs):
167 167 for r in revs:
168 168 yield web.repo[r]
169 169
170 170 searchfuncs = {
171 171 MODE_REVISION: (revsearch, 'exact revision search'),
172 172 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
173 173 MODE_REVSET: (revsetsearch, 'revset expression search'),
174 174 }
175 175
176 176 def getsearchmode(query):
177 177 try:
178 178 ctx = web.repo[query]
179 179 except (error.RepoError, error.LookupError):
180 180 # query is not an exact revision pointer, need to
181 181 # decide if it's a revset expression or keywords
182 182 pass
183 183 else:
184 184 return MODE_REVISION, ctx
185 185
186 186 revdef = 'reverse(%s)' % query
187 187 try:
188 188 tree, pos = revset.parse(revdef)
189 189 except ParseError:
190 190 # can't parse to a revset tree
191 191 return MODE_KEYWORD, query
192 192
193 193 if revset.depth(tree) <= 2:
194 194 # no revset syntax used
195 195 return MODE_KEYWORD, query
196 196
197 197 if util.any((token, (value or '')[:3]) == ('string', 're:')
198 198 for token, value, pos in revset.tokenize(revdef)):
199 199 return MODE_KEYWORD, query
200 200
201 201 funcsused = revset.funcsused(tree)
202 202 if not funcsused.issubset(revset.safesymbols):
203 203 return MODE_KEYWORD, query
204 204
205 205 mfunc = revset.match(web.repo.ui, revdef)
206 206 try:
207 207 revs = mfunc(web.repo, revset.spanset(web.repo))
208 208 return MODE_REVSET, revs
209 209 # ParseError: wrongly placed tokens, wrongs arguments, etc
210 210 # RepoLookupError: no such revision, e.g. in 'revision:'
211 211 # Abort: bookmark/tag not exists
212 212 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
213 213 except (ParseError, RepoLookupError, Abort, LookupError):
214 214 return MODE_KEYWORD, query
215 215
216 216 def changelist(**map):
217 217 count = 0
218 218
219 219 for ctx in searchfunc[0](funcarg):
220 220 count += 1
221 221 n = ctx.node()
222 222 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
223 223 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
224 224
225 225 yield tmpl('searchentry',
226 226 parity=parity.next(),
227 227 author=ctx.user(),
228 228 parent=webutil.parents(ctx),
229 229 child=webutil.children(ctx),
230 230 changelogtag=showtags,
231 231 desc=ctx.description(),
232 232 extra=ctx.extra(),
233 233 date=ctx.date(),
234 234 files=files,
235 235 rev=ctx.rev(),
236 236 node=hex(n),
237 237 tags=webutil.nodetagsdict(web.repo, n),
238 238 bookmarks=webutil.nodebookmarksdict(web.repo, n),
239 239 inbranch=webutil.nodeinbranch(web.repo, ctx),
240 240 branches=webutil.nodebranchdict(web.repo, ctx))
241 241
242 242 if count >= revcount:
243 243 break
244 244
245 245 query = req.form['rev'][0]
246 246 revcount = web.maxchanges
247 247 if 'revcount' in req.form:
248 248 try:
249 249 revcount = int(req.form.get('revcount', [revcount])[0])
250 250 revcount = max(revcount, 1)
251 251 tmpl.defaults['sessionvars']['revcount'] = revcount
252 252 except ValueError:
253 253 pass
254 254
255 255 lessvars = copy.copy(tmpl.defaults['sessionvars'])
256 256 lessvars['revcount'] = max(revcount / 2, 1)
257 257 lessvars['rev'] = query
258 258 morevars = copy.copy(tmpl.defaults['sessionvars'])
259 259 morevars['revcount'] = revcount * 2
260 260 morevars['rev'] = query
261 261
262 262 mode, funcarg = getsearchmode(query)
263 263
264 264 if 'forcekw' in req.form:
265 265 showforcekw = ''
266 266 showunforcekw = searchfuncs[mode][1]
267 267 mode = MODE_KEYWORD
268 268 funcarg = query
269 269 else:
270 270 if mode != MODE_KEYWORD:
271 271 showforcekw = searchfuncs[MODE_KEYWORD][1]
272 272 else:
273 273 showforcekw = ''
274 274 showunforcekw = ''
275 275
276 276 searchfunc = searchfuncs[mode]
277 277
278 278 tip = web.repo['tip']
279 279 parity = paritygen(web.stripecount)
280 280
281 281 return tmpl('search', query=query, node=tip.hex(),
282 282 entries=changelist, archives=web.archivelist("tip"),
283 283 morevars=morevars, lessvars=lessvars,
284 284 modedesc=searchfunc[1],
285 285 showforcekw=showforcekw, showunforcekw=showunforcekw)
286 286
287 287 @webcommand('changelog')
288 288 def changelog(web, req, tmpl, shortlog=False):
289 289
290 290 query = ''
291 291 if 'node' in req.form:
292 292 ctx = webutil.changectx(web.repo, req)
293 293 elif 'rev' in req.form:
294 294 return _search(web, req, tmpl)
295 295 else:
296 296 ctx = web.repo['tip']
297 297
298 298 def changelist():
299 299 revs = []
300 300 if pos != -1:
301 301 revs = web.repo.changelog.revs(pos, 0)
302 302 curcount = 0
303 303 for rev in revs:
304 304 curcount += 1
305 305 if curcount > revcount + 1:
306 306 break
307 307
308 308 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
309 309 entry['parity'] = parity.next()
310 310 yield entry
311 311
312 312 revcount = shortlog and web.maxshortchanges or web.maxchanges
313 313 if 'revcount' in req.form:
314 314 try:
315 315 revcount = int(req.form.get('revcount', [revcount])[0])
316 316 revcount = max(revcount, 1)
317 317 tmpl.defaults['sessionvars']['revcount'] = revcount
318 318 except ValueError:
319 319 pass
320 320
321 321 lessvars = copy.copy(tmpl.defaults['sessionvars'])
322 322 lessvars['revcount'] = max(revcount / 2, 1)
323 323 morevars = copy.copy(tmpl.defaults['sessionvars'])
324 324 morevars['revcount'] = revcount * 2
325 325
326 326 count = len(web.repo)
327 327 pos = ctx.rev()
328 328 parity = paritygen(web.stripecount)
329 329
330 330 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
331 331
332 332 entries = list(changelist())
333 333 latestentry = entries[:1]
334 334 if len(entries) > revcount:
335 335 nextentry = entries[-1:]
336 336 entries = entries[:-1]
337 337 else:
338 338 nextentry = []
339 339
340 340 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
341 341 node=ctx.hex(), rev=pos, changesets=count,
342 342 entries=entries,
343 343 latestentry=latestentry, nextentry=nextentry,
344 344 archives=web.archivelist("tip"), revcount=revcount,
345 345 morevars=morevars, lessvars=lessvars, query=query)
346 346
347 347 @webcommand('shortlog')
348 348 def shortlog(web, req, tmpl):
349 349 return changelog(web, req, tmpl, shortlog=True)
350 350
351 351 @webcommand('changeset')
352 352 def changeset(web, req, tmpl):
353 353 ctx = webutil.changectx(web.repo, req)
354 354 basectx = webutil.basechangectx(web.repo, req)
355 355 if basectx is None:
356 356 basectx = ctx.p1()
357 357 showtags = webutil.showtag(web.repo, tmpl, 'changesettag', ctx.node())
358 358 showbookmarks = webutil.showbookmark(web.repo, tmpl, 'changesetbookmark',
359 359 ctx.node())
360 360 showbranch = webutil.nodebranchnodefault(ctx)
361 361
362 362 files = []
363 363 parity = paritygen(web.stripecount)
364 364 for blockno, f in enumerate(ctx.files()):
365 365 template = f in ctx and 'filenodelink' or 'filenolink'
366 366 files.append(tmpl(template,
367 367 node=ctx.hex(), file=f, blockno=blockno + 1,
368 368 parity=parity.next()))
369 369
370 370 style = web.config('web', 'style', 'paper')
371 371 if 'style' in req.form:
372 372 style = req.form['style'][0]
373 373
374 374 parity = paritygen(web.stripecount)
375 375 diffs = webutil.diffs(web.repo, tmpl, ctx, basectx, None, parity, style)
376 376
377 377 parity = paritygen(web.stripecount)
378 378 diffstatgen = webutil.diffstatgen(ctx, basectx)
379 379 diffstat = webutil.diffstat(tmpl, ctx, diffstatgen, parity)
380 380
381 381 return tmpl('changeset',
382 382 diff=diffs,
383 383 rev=ctx.rev(),
384 384 node=ctx.hex(),
385 385 parent=tuple(webutil.parents(ctx)),
386 386 child=webutil.children(ctx),
387 387 basenode=basectx.hex(),
388 388 changesettag=showtags,
389 389 changesetbookmark=showbookmarks,
390 390 changesetbranch=showbranch,
391 391 author=ctx.user(),
392 392 desc=ctx.description(),
393 393 extra=ctx.extra(),
394 394 date=ctx.date(),
395 395 files=files,
396 396 diffsummary=lambda **x: webutil.diffsummary(diffstatgen),
397 397 diffstat=diffstat,
398 398 archives=web.archivelist(ctx.hex()),
399 399 tags=webutil.nodetagsdict(web.repo, ctx.node()),
400 400 bookmarks=webutil.nodebookmarksdict(web.repo, ctx.node()),
401 401 branch=webutil.nodebranchnodefault(ctx),
402 402 inbranch=webutil.nodeinbranch(web.repo, ctx),
403 403 branches=webutil.nodebranchdict(web.repo, ctx))
404 404
405 405 rev = webcommand('rev')(changeset)
406 406
407 407 def decodepath(path):
408 408 """Hook for mapping a path in the repository to a path in the
409 409 working copy.
410 410
411 411 Extensions (e.g., largefiles) can override this to remap files in
412 412 the virtual file system presented by the manifest command below."""
413 413 return path
414 414
415 415 @webcommand('manifest')
416 416 def manifest(web, req, tmpl):
417 417 ctx = webutil.changectx(web.repo, req)
418 418 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
419 419 mf = ctx.manifest()
420 420 node = ctx.node()
421 421
422 422 files = {}
423 423 dirs = {}
424 424 parity = paritygen(web.stripecount)
425 425
426 426 if path and path[-1] != "/":
427 427 path += "/"
428 428 l = len(path)
429 429 abspath = "/" + path
430 430
431 431 for full, n in mf.iteritems():
432 432 # the virtual path (working copy path) used for the full
433 433 # (repository) path
434 434 f = decodepath(full)
435 435
436 436 if f[:l] != path:
437 437 continue
438 438 remain = f[l:]
439 439 elements = remain.split('/')
440 440 if len(elements) == 1:
441 441 files[remain] = full
442 442 else:
443 443 h = dirs # need to retain ref to dirs (root)
444 444 for elem in elements[0:-1]:
445 445 if elem not in h:
446 446 h[elem] = {}
447 447 h = h[elem]
448 448 if len(h) > 1:
449 449 break
450 450 h[None] = None # denotes files present
451 451
452 452 if mf and not files and not dirs:
453 453 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
454 454
455 455 def filelist(**map):
456 456 for f in sorted(files):
457 457 full = files[f]
458 458
459 459 fctx = ctx.filectx(full)
460 460 yield {"file": full,
461 461 "parity": parity.next(),
462 462 "basename": f,
463 463 "date": fctx.date(),
464 464 "size": fctx.size(),
465 465 "permissions": mf.flags(full)}
466 466
467 467 def dirlist(**map):
468 468 for d in sorted(dirs):
469 469
470 470 emptydirs = []
471 471 h = dirs[d]
472 472 while isinstance(h, dict) and len(h) == 1:
473 473 k, v = h.items()[0]
474 474 if v:
475 475 emptydirs.append(k)
476 476 h = v
477 477
478 478 path = "%s%s" % (abspath, d)
479 479 yield {"parity": parity.next(),
480 480 "path": path,
481 481 "emptydirs": "/".join(emptydirs),
482 482 "basename": d}
483 483
484 484 return tmpl("manifest",
485 485 rev=ctx.rev(),
486 486 node=hex(node),
487 487 path=abspath,
488 488 up=webutil.up(abspath),
489 489 upparity=parity.next(),
490 490 fentries=filelist,
491 491 dentries=dirlist,
492 492 archives=web.archivelist(hex(node)),
493 493 tags=webutil.nodetagsdict(web.repo, node),
494 494 bookmarks=webutil.nodebookmarksdict(web.repo, node),
495 495 inbranch=webutil.nodeinbranch(web.repo, ctx),
496 496 branches=webutil.nodebranchdict(web.repo, ctx))
497 497
498 498 @webcommand('tags')
499 499 def tags(web, req, tmpl):
500 """
501 /tags
502 -----
503
504 Show information about tags.
505
506 No arguments are accepted.
507
508 The ``tags`` template is rendered.
509 """
500 510 i = list(reversed(web.repo.tagslist()))
501 511 parity = paritygen(web.stripecount)
502 512
503 513 def entries(notip, latestonly, **map):
504 514 t = i
505 515 if notip:
506 516 t = [(k, n) for k, n in i if k != "tip"]
507 517 if latestonly:
508 518 t = t[:1]
509 519 for k, n in t:
510 520 yield {"parity": parity.next(),
511 521 "tag": k,
512 522 "date": web.repo[n].date(),
513 523 "node": hex(n)}
514 524
515 525 return tmpl("tags",
516 526 node=hex(web.repo.changelog.tip()),
517 527 entries=lambda **x: entries(False, False, **x),
518 528 entriesnotip=lambda **x: entries(True, False, **x),
519 529 latestentry=lambda **x: entries(True, True, **x))
520 530
521 531 @webcommand('bookmarks')
522 532 def bookmarks(web, req, tmpl):
523 533 """
524 534 /bookmarks
525 535 ----------
526 536
527 537 Show information about bookmarks.
528 538
529 539 No arguments are accepted.
530 540
531 541 The ``bookmarks`` template is rendered.
532 542 """
533 543 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
534 544 parity = paritygen(web.stripecount)
535 545
536 546 def entries(latestonly, **map):
537 547 if latestonly:
538 548 t = [min(i)]
539 549 else:
540 550 t = sorted(i)
541 551 for k, n in t:
542 552 yield {"parity": parity.next(),
543 553 "bookmark": k,
544 554 "date": web.repo[n].date(),
545 555 "node": hex(n)}
546 556
547 557 return tmpl("bookmarks",
548 558 node=hex(web.repo.changelog.tip()),
549 559 entries=lambda **x: entries(latestonly=False, **x),
550 560 latestentry=lambda **x: entries(latestonly=True, **x))
551 561
552 562 @webcommand('branches')
553 563 def branches(web, req, tmpl):
554 564 """
555 565 /branches
556 566 ---------
557 567
558 568 Show information about branches.
559 569
560 570 All known branches are contained in the output, even closed branches.
561 571
562 572 No arguments are accepted.
563 573
564 574 The ``branches`` template is rendered.
565 575 """
566 576 tips = []
567 577 heads = web.repo.heads()
568 578 parity = paritygen(web.stripecount)
569 579 sortkey = lambda item: (not item[1], item[0].rev())
570 580
571 581 def entries(limit, **map):
572 582 count = 0
573 583 if not tips:
574 584 for tag, hs, tip, closed in web.repo.branchmap().iterbranches():
575 585 tips.append((web.repo[tip], closed))
576 586 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
577 587 if limit > 0 and count >= limit:
578 588 return
579 589 count += 1
580 590 if closed:
581 591 status = 'closed'
582 592 elif ctx.node() not in heads:
583 593 status = 'inactive'
584 594 else:
585 595 status = 'open'
586 596 yield {'parity': parity.next(),
587 597 'branch': ctx.branch(),
588 598 'status': status,
589 599 'node': ctx.hex(),
590 600 'date': ctx.date()}
591 601
592 602 return tmpl('branches', node=hex(web.repo.changelog.tip()),
593 603 entries=lambda **x: entries(0, **x),
594 604 latestentry=lambda **x: entries(1, **x))
595 605
596 606 @webcommand('summary')
597 607 def summary(web, req, tmpl):
598 608 i = reversed(web.repo.tagslist())
599 609
600 610 def tagentries(**map):
601 611 parity = paritygen(web.stripecount)
602 612 count = 0
603 613 for k, n in i:
604 614 if k == "tip": # skip tip
605 615 continue
606 616
607 617 count += 1
608 618 if count > 10: # limit to 10 tags
609 619 break
610 620
611 621 yield tmpl("tagentry",
612 622 parity=parity.next(),
613 623 tag=k,
614 624 node=hex(n),
615 625 date=web.repo[n].date())
616 626
617 627 def bookmarks(**map):
618 628 parity = paritygen(web.stripecount)
619 629 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
620 630 for k, n in sorted(marks)[:10]: # limit to 10 bookmarks
621 631 yield {'parity': parity.next(),
622 632 'bookmark': k,
623 633 'date': web.repo[n].date(),
624 634 'node': hex(n)}
625 635
626 636 def branches(**map):
627 637 parity = paritygen(web.stripecount)
628 638
629 639 b = web.repo.branchmap()
630 640 l = [(-web.repo.changelog.rev(tip), tip, tag)
631 641 for tag, heads, tip, closed in b.iterbranches()]
632 642 for r, n, t in sorted(l):
633 643 yield {'parity': parity.next(),
634 644 'branch': t,
635 645 'node': hex(n),
636 646 'date': web.repo[n].date()}
637 647
638 648 def changelist(**map):
639 649 parity = paritygen(web.stripecount, offset=start - end)
640 650 l = [] # build a list in forward order for efficiency
641 651 revs = []
642 652 if start < end:
643 653 revs = web.repo.changelog.revs(start, end - 1)
644 654 for i in revs:
645 655 ctx = web.repo[i]
646 656 n = ctx.node()
647 657 hn = hex(n)
648 658
649 659 l.append(tmpl(
650 660 'shortlogentry',
651 661 parity=parity.next(),
652 662 author=ctx.user(),
653 663 desc=ctx.description(),
654 664 extra=ctx.extra(),
655 665 date=ctx.date(),
656 666 rev=i,
657 667 node=hn,
658 668 tags=webutil.nodetagsdict(web.repo, n),
659 669 bookmarks=webutil.nodebookmarksdict(web.repo, n),
660 670 inbranch=webutil.nodeinbranch(web.repo, ctx),
661 671 branches=webutil.nodebranchdict(web.repo, ctx)))
662 672
663 673 l.reverse()
664 674 yield l
665 675
666 676 tip = web.repo['tip']
667 677 count = len(web.repo)
668 678 start = max(0, count - web.maxchanges)
669 679 end = min(count, start + web.maxchanges)
670 680
671 681 return tmpl("summary",
672 682 desc=web.config("web", "description", "unknown"),
673 683 owner=get_contact(web.config) or "unknown",
674 684 lastchange=tip.date(),
675 685 tags=tagentries,
676 686 bookmarks=bookmarks,
677 687 branches=branches,
678 688 shortlog=changelist,
679 689 node=tip.hex(),
680 690 archives=web.archivelist("tip"))
681 691
682 692 @webcommand('filediff')
683 693 def filediff(web, req, tmpl):
684 694 fctx, ctx = None, None
685 695 try:
686 696 fctx = webutil.filectx(web.repo, req)
687 697 except LookupError:
688 698 ctx = webutil.changectx(web.repo, req)
689 699 path = webutil.cleanpath(web.repo, req.form['file'][0])
690 700 if path not in ctx.files():
691 701 raise
692 702
693 703 if fctx is not None:
694 704 n = fctx.node()
695 705 path = fctx.path()
696 706 ctx = fctx.changectx()
697 707 else:
698 708 n = ctx.node()
699 709 # path already defined in except clause
700 710
701 711 parity = paritygen(web.stripecount)
702 712 style = web.config('web', 'style', 'paper')
703 713 if 'style' in req.form:
704 714 style = req.form['style'][0]
705 715
706 716 diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style)
707 717 rename = fctx and webutil.renamelink(fctx) or []
708 718 ctx = fctx and fctx or ctx
709 719 return tmpl("filediff",
710 720 file=path,
711 721 node=hex(n),
712 722 rev=ctx.rev(),
713 723 date=ctx.date(),
714 724 desc=ctx.description(),
715 725 extra=ctx.extra(),
716 726 author=ctx.user(),
717 727 rename=rename,
718 728 branch=webutil.nodebranchnodefault(ctx),
719 729 parent=webutil.parents(ctx),
720 730 child=webutil.children(ctx),
721 731 diff=diffs)
722 732
723 733 diff = webcommand('diff')(filediff)
724 734
725 735 @webcommand('comparison')
726 736 def comparison(web, req, tmpl):
727 737 ctx = webutil.changectx(web.repo, req)
728 738 if 'file' not in req.form:
729 739 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
730 740 path = webutil.cleanpath(web.repo, req.form['file'][0])
731 741 rename = path in ctx and webutil.renamelink(ctx[path]) or []
732 742
733 743 parsecontext = lambda v: v == 'full' and -1 or int(v)
734 744 if 'context' in req.form:
735 745 context = parsecontext(req.form['context'][0])
736 746 else:
737 747 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
738 748
739 749 def filelines(f):
740 750 if util.binary(f.data()):
741 751 mt = mimetypes.guess_type(f.path())[0]
742 752 if not mt:
743 753 mt = 'application/octet-stream'
744 754 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
745 755 return f.data().splitlines()
746 756
747 757 parent = ctx.p1()
748 758 leftrev = parent.rev()
749 759 leftnode = parent.node()
750 760 rightrev = ctx.rev()
751 761 rightnode = ctx.node()
752 762 if path in ctx:
753 763 fctx = ctx[path]
754 764 rightlines = filelines(fctx)
755 765 if path not in parent:
756 766 leftlines = ()
757 767 else:
758 768 pfctx = parent[path]
759 769 leftlines = filelines(pfctx)
760 770 else:
761 771 rightlines = ()
762 772 fctx = ctx.parents()[0][path]
763 773 leftlines = filelines(fctx)
764 774
765 775 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
766 776 return tmpl('filecomparison',
767 777 file=path,
768 778 node=hex(ctx.node()),
769 779 rev=ctx.rev(),
770 780 date=ctx.date(),
771 781 desc=ctx.description(),
772 782 extra=ctx.extra(),
773 783 author=ctx.user(),
774 784 rename=rename,
775 785 branch=webutil.nodebranchnodefault(ctx),
776 786 parent=webutil.parents(fctx),
777 787 child=webutil.children(fctx),
778 788 leftrev=leftrev,
779 789 leftnode=hex(leftnode),
780 790 rightrev=rightrev,
781 791 rightnode=hex(rightnode),
782 792 comparison=comparison)
783 793
784 794 @webcommand('annotate')
785 795 def annotate(web, req, tmpl):
786 796 fctx = webutil.filectx(web.repo, req)
787 797 f = fctx.path()
788 798 parity = paritygen(web.stripecount)
789 799 diffopts = patch.difffeatureopts(web.repo.ui, untrusted=True,
790 800 section='annotate', whitespace=True)
791 801
792 802 def annotate(**map):
793 803 last = None
794 804 if util.binary(fctx.data()):
795 805 mt = (mimetypes.guess_type(fctx.path())[0]
796 806 or 'application/octet-stream')
797 807 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
798 808 '(binary:%s)' % mt)])
799 809 else:
800 810 lines = enumerate(fctx.annotate(follow=True, linenumber=True,
801 811 diffopts=diffopts))
802 812 for lineno, ((f, targetline), l) in lines:
803 813 fnode = f.filenode()
804 814
805 815 if last != fnode:
806 816 last = fnode
807 817
808 818 yield {"parity": parity.next(),
809 819 "node": f.hex(),
810 820 "rev": f.rev(),
811 821 "author": f.user(),
812 822 "desc": f.description(),
813 823 "extra": f.extra(),
814 824 "file": f.path(),
815 825 "targetline": targetline,
816 826 "line": l,
817 827 "lineid": "l%d" % (lineno + 1),
818 828 "linenumber": "% 6d" % (lineno + 1),
819 829 "revdate": f.date()}
820 830
821 831 return tmpl("fileannotate",
822 832 file=f,
823 833 annotate=annotate,
824 834 path=webutil.up(f),
825 835 rev=fctx.rev(),
826 836 node=fctx.hex(),
827 837 author=fctx.user(),
828 838 date=fctx.date(),
829 839 desc=fctx.description(),
830 840 extra=fctx.extra(),
831 841 rename=webutil.renamelink(fctx),
832 842 branch=webutil.nodebranchnodefault(fctx),
833 843 parent=webutil.parents(fctx),
834 844 child=webutil.children(fctx),
835 845 permissions=fctx.manifest().flags(f))
836 846
837 847 @webcommand('filelog')
838 848 def filelog(web, req, tmpl):
839 849
840 850 try:
841 851 fctx = webutil.filectx(web.repo, req)
842 852 f = fctx.path()
843 853 fl = fctx.filelog()
844 854 except error.LookupError:
845 855 f = webutil.cleanpath(web.repo, req.form['file'][0])
846 856 fl = web.repo.file(f)
847 857 numrevs = len(fl)
848 858 if not numrevs: # file doesn't exist at all
849 859 raise
850 860 rev = webutil.changectx(web.repo, req).rev()
851 861 first = fl.linkrev(0)
852 862 if rev < first: # current rev is from before file existed
853 863 raise
854 864 frev = numrevs - 1
855 865 while fl.linkrev(frev) > rev:
856 866 frev -= 1
857 867 fctx = web.repo.filectx(f, fl.linkrev(frev))
858 868
859 869 revcount = web.maxshortchanges
860 870 if 'revcount' in req.form:
861 871 try:
862 872 revcount = int(req.form.get('revcount', [revcount])[0])
863 873 revcount = max(revcount, 1)
864 874 tmpl.defaults['sessionvars']['revcount'] = revcount
865 875 except ValueError:
866 876 pass
867 877
868 878 lessvars = copy.copy(tmpl.defaults['sessionvars'])
869 879 lessvars['revcount'] = max(revcount / 2, 1)
870 880 morevars = copy.copy(tmpl.defaults['sessionvars'])
871 881 morevars['revcount'] = revcount * 2
872 882
873 883 count = fctx.filerev() + 1
874 884 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
875 885 end = min(count, start + revcount) # last rev on this page
876 886 parity = paritygen(web.stripecount, offset=start - end)
877 887
878 888 def entries():
879 889 l = []
880 890
881 891 repo = web.repo
882 892 revs = fctx.filelog().revs(start, end - 1)
883 893 for i in revs:
884 894 iterfctx = fctx.filectx(i)
885 895
886 896 l.append({"parity": parity.next(),
887 897 "filerev": i,
888 898 "file": f,
889 899 "node": iterfctx.hex(),
890 900 "author": iterfctx.user(),
891 901 "date": iterfctx.date(),
892 902 "rename": webutil.renamelink(iterfctx),
893 903 "parent": webutil.parents(iterfctx),
894 904 "child": webutil.children(iterfctx),
895 905 "desc": iterfctx.description(),
896 906 "extra": iterfctx.extra(),
897 907 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
898 908 "bookmarks": webutil.nodebookmarksdict(
899 909 repo, iterfctx.node()),
900 910 "branch": webutil.nodebranchnodefault(iterfctx),
901 911 "inbranch": webutil.nodeinbranch(repo, iterfctx),
902 912 "branches": webutil.nodebranchdict(repo, iterfctx)})
903 913 for e in reversed(l):
904 914 yield e
905 915
906 916 entries = list(entries())
907 917 latestentry = entries[:1]
908 918
909 919 revnav = webutil.filerevnav(web.repo, fctx.path())
910 920 nav = revnav.gen(end - 1, revcount, count)
911 921 return tmpl("filelog", file=f, node=fctx.hex(), nav=nav,
912 922 entries=entries,
913 923 latestentry=latestentry,
914 924 revcount=revcount, morevars=morevars, lessvars=lessvars)
915 925
916 926 @webcommand('archive')
917 927 def archive(web, req, tmpl):
918 928 type_ = req.form.get('type', [None])[0]
919 929 allowed = web.configlist("web", "allow_archive")
920 930 key = req.form['node'][0]
921 931
922 932 if type_ not in web.archives:
923 933 msg = 'Unsupported archive type: %s' % type_
924 934 raise ErrorResponse(HTTP_NOT_FOUND, msg)
925 935
926 936 if not ((type_ in allowed or
927 937 web.configbool("web", "allow" + type_, False))):
928 938 msg = 'Archive type not allowed: %s' % type_
929 939 raise ErrorResponse(HTTP_FORBIDDEN, msg)
930 940
931 941 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
932 942 cnode = web.repo.lookup(key)
933 943 arch_version = key
934 944 if cnode == key or key == 'tip':
935 945 arch_version = short(cnode)
936 946 name = "%s-%s" % (reponame, arch_version)
937 947
938 948 ctx = webutil.changectx(web.repo, req)
939 949 pats = []
940 950 matchfn = scmutil.match(ctx, [])
941 951 file = req.form.get('file', None)
942 952 if file:
943 953 pats = ['path:' + file[0]]
944 954 matchfn = scmutil.match(ctx, pats, default='path')
945 955 if pats:
946 956 files = [f for f in ctx.manifest().keys() if matchfn(f)]
947 957 if not files:
948 958 raise ErrorResponse(HTTP_NOT_FOUND,
949 959 'file(s) not found: %s' % file[0])
950 960
951 961 mimetype, artype, extension, encoding = web.archive_specs[type_]
952 962 headers = [
953 963 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
954 964 ]
955 965 if encoding:
956 966 headers.append(('Content-Encoding', encoding))
957 967 req.headers.extend(headers)
958 968 req.respond(HTTP_OK, mimetype)
959 969
960 970 archival.archive(web.repo, req, cnode, artype, prefix=name,
961 971 matchfn=matchfn,
962 972 subrepos=web.configbool("web", "archivesubrepos"))
963 973 return []
964 974
965 975
966 976 @webcommand('static')
967 977 def static(web, req, tmpl):
968 978 fname = req.form['file'][0]
969 979 # a repo owner may set web.static in .hg/hgrc to get any file
970 980 # readable by the user running the CGI script
971 981 static = web.config("web", "static", None, untrusted=False)
972 982 if not static:
973 983 tp = web.templatepath or templater.templatepaths()
974 984 if isinstance(tp, str):
975 985 tp = [tp]
976 986 static = [os.path.join(p, 'static') for p in tp]
977 987 staticfile(static, fname, req)
978 988 return []
979 989
980 990 @webcommand('graph')
981 991 def graph(web, req, tmpl):
982 992
983 993 ctx = webutil.changectx(web.repo, req)
984 994 rev = ctx.rev()
985 995
986 996 bg_height = 39
987 997 revcount = web.maxshortchanges
988 998 if 'revcount' in req.form:
989 999 try:
990 1000 revcount = int(req.form.get('revcount', [revcount])[0])
991 1001 revcount = max(revcount, 1)
992 1002 tmpl.defaults['sessionvars']['revcount'] = revcount
993 1003 except ValueError:
994 1004 pass
995 1005
996 1006 lessvars = copy.copy(tmpl.defaults['sessionvars'])
997 1007 lessvars['revcount'] = max(revcount / 2, 1)
998 1008 morevars = copy.copy(tmpl.defaults['sessionvars'])
999 1009 morevars['revcount'] = revcount * 2
1000 1010
1001 1011 count = len(web.repo)
1002 1012 pos = rev
1003 1013
1004 1014 uprev = min(max(0, count - 1), rev + revcount)
1005 1015 downrev = max(0, rev - revcount)
1006 1016 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1007 1017
1008 1018 tree = []
1009 1019 if pos != -1:
1010 1020 allrevs = web.repo.changelog.revs(pos, 0)
1011 1021 revs = []
1012 1022 for i in allrevs:
1013 1023 revs.append(i)
1014 1024 if len(revs) >= revcount:
1015 1025 break
1016 1026
1017 1027 # We have to feed a baseset to dagwalker as it is expecting smartset
1018 1028 # object. This does not have a big impact on hgweb performance itself
1019 1029 # since hgweb graphing code is not itself lazy yet.
1020 1030 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1021 1031 # As we said one line above... not lazy.
1022 1032 tree = list(graphmod.colored(dag, web.repo))
1023 1033
1024 1034 def getcolumns(tree):
1025 1035 cols = 0
1026 1036 for (id, type, ctx, vtx, edges) in tree:
1027 1037 if type != graphmod.CHANGESET:
1028 1038 continue
1029 1039 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1030 1040 max([edge[1] for edge in edges] or [0]))
1031 1041 return cols
1032 1042
1033 1043 def graphdata(usetuples, **map):
1034 1044 data = []
1035 1045
1036 1046 row = 0
1037 1047 for (id, type, ctx, vtx, edges) in tree:
1038 1048 if type != graphmod.CHANGESET:
1039 1049 continue
1040 1050 node = str(ctx)
1041 1051 age = templatefilters.age(ctx.date())
1042 1052 desc = templatefilters.firstline(ctx.description())
1043 1053 desc = cgi.escape(templatefilters.nonempty(desc))
1044 1054 user = cgi.escape(templatefilters.person(ctx.user()))
1045 1055 branch = cgi.escape(ctx.branch())
1046 1056 try:
1047 1057 branchnode = web.repo.branchtip(branch)
1048 1058 except error.RepoLookupError:
1049 1059 branchnode = None
1050 1060 branch = branch, branchnode == ctx.node()
1051 1061
1052 1062 if usetuples:
1053 1063 data.append((node, vtx, edges, desc, user, age, branch,
1054 1064 [cgi.escape(x) for x in ctx.tags()],
1055 1065 [cgi.escape(x) for x in ctx.bookmarks()]))
1056 1066 else:
1057 1067 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1058 1068 'color': (edge[2] - 1) % 6 + 1,
1059 1069 'width': edge[3], 'bcolor': edge[4]}
1060 1070 for edge in edges]
1061 1071
1062 1072 data.append(
1063 1073 {'node': node,
1064 1074 'col': vtx[0],
1065 1075 'color': (vtx[1] - 1) % 6 + 1,
1066 1076 'edges': edgedata,
1067 1077 'row': row,
1068 1078 'nextrow': row + 1,
1069 1079 'desc': desc,
1070 1080 'user': user,
1071 1081 'age': age,
1072 1082 'bookmarks': webutil.nodebookmarksdict(
1073 1083 web.repo, ctx.node()),
1074 1084 'branches': webutil.nodebranchdict(web.repo, ctx),
1075 1085 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1076 1086 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1077 1087
1078 1088 row += 1
1079 1089
1080 1090 return data
1081 1091
1082 1092 cols = getcolumns(tree)
1083 1093 rows = len(tree)
1084 1094 canvasheight = (rows + 1) * bg_height - 27
1085 1095
1086 1096 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
1087 1097 lessvars=lessvars, morevars=morevars, downrev=downrev,
1088 1098 cols=cols, rows=rows,
1089 1099 canvaswidth=(cols + 1) * bg_height,
1090 1100 truecanvasheight=rows * bg_height,
1091 1101 canvasheight=canvasheight, bg_height=bg_height,
1092 1102 jsdata=lambda **x: graphdata(True, **x),
1093 1103 nodes=lambda **x: graphdata(False, **x),
1094 1104 node=ctx.hex(), changenav=changenav)
1095 1105
1096 1106 def _getdoc(e):
1097 1107 doc = e[0].__doc__
1098 1108 if doc:
1099 1109 doc = _(doc).split('\n')[0]
1100 1110 else:
1101 1111 doc = _('(no help text available)')
1102 1112 return doc
1103 1113
1104 1114 @webcommand('help')
1105 1115 def help(web, req, tmpl):
1106 1116 """
1107 1117 /help[/{topic}]
1108 1118 ---------------
1109 1119
1110 1120 Render help documentation.
1111 1121
1112 1122 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1113 1123 is defined, that help topic will be rendered. If not, an index of
1114 1124 available help topics will be rendered.
1115 1125
1116 1126 The ``help`` template will be rendered when requesting help for a topic.
1117 1127 ``helptopics`` will be rendered for the index of help topics.
1118 1128 """
1119 1129 from mercurial import commands # avoid cycle
1120 1130 from mercurial import help as helpmod # avoid cycle
1121 1131
1122 1132 topicname = req.form.get('node', [None])[0]
1123 1133 if not topicname:
1124 1134 def topics(**map):
1125 1135 for entries, summary, _doc in helpmod.helptable:
1126 1136 yield {'topic': entries[0], 'summary': summary}
1127 1137
1128 1138 early, other = [], []
1129 1139 primary = lambda s: s.split('|')[0]
1130 1140 for c, e in commands.table.iteritems():
1131 1141 doc = _getdoc(e)
1132 1142 if 'DEPRECATED' in doc or c.startswith('debug'):
1133 1143 continue
1134 1144 cmd = primary(c)
1135 1145 if cmd.startswith('^'):
1136 1146 early.append((cmd[1:], doc))
1137 1147 else:
1138 1148 other.append((cmd, doc))
1139 1149
1140 1150 early.sort()
1141 1151 other.sort()
1142 1152
1143 1153 def earlycommands(**map):
1144 1154 for c, doc in early:
1145 1155 yield {'topic': c, 'summary': doc}
1146 1156
1147 1157 def othercommands(**map):
1148 1158 for c, doc in other:
1149 1159 yield {'topic': c, 'summary': doc}
1150 1160
1151 1161 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1152 1162 othercommands=othercommands, title='Index')
1153 1163
1154 1164 u = webutil.wsgiui()
1155 1165 u.verbose = True
1156 1166 try:
1157 1167 doc = helpmod.help_(u, topicname)
1158 1168 except error.UnknownCommand:
1159 1169 raise ErrorResponse(HTTP_NOT_FOUND)
1160 1170 return tmpl('help', topic=topicname, doc=doc)
General Comments 0
You need to be logged in to leave comments. Login now