##// END OF EJS Templates
Arrange for old copies of CGI scripts to still work.
Eric Hopper -
r2535:b8ccf638 default
parent child Browse files
Show More
@@ -1,934 +1,943 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os
10 10 import os.path
11 11 import mimetypes
12 12 from mercurial.demandload import demandload
13 13 demandload(globals(), "re zlib ConfigParser mimetools cStringIO sys tempfile")
14 14 demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,templater")
15 15 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
16 16 from mercurial.node import *
17 17 from mercurial.i18n import gettext as _
18 18
19 19 def _up(p):
20 20 if p[0] != "/":
21 21 p = "/" + p
22 22 if p[-1] == "/":
23 23 p = p[:-1]
24 24 up = os.path.dirname(p)
25 25 if up == "/":
26 26 return "/"
27 27 return up + "/"
28 28
29 29 class hgweb(object):
30 30 def __init__(self, repo, name=None):
31 31 if type(repo) == type(""):
32 32 self.repo = hg.repository(ui.ui(), repo)
33 33 else:
34 34 self.repo = repo
35 35
36 36 self.mtime = -1
37 37 self.reponame = name
38 38 self.archives = 'zip', 'gz', 'bz2'
39 39 self.templatepath = self.repo.ui.config("web", "templates",
40 40 templater.templatepath())
41 41
42 42 def refresh(self):
43 43 mtime = get_mtime(self.repo.root)
44 44 if mtime != self.mtime:
45 45 self.mtime = mtime
46 46 self.repo = hg.repository(self.repo.ui, self.repo.root)
47 47 self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
48 48 self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
49 49 self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
50 50
51 51 def archivelist(self, nodeid):
52 52 allowed = self.repo.ui.configlist("web", "allow_archive")
53 53 for i in self.archives:
54 54 if i in allowed or self.repo.ui.configbool("web", "allow" + i):
55 55 yield {"type" : i, "node" : nodeid, "url": ""}
56 56
57 57 def listfiles(self, files, mf):
58 58 for f in files[:self.maxfiles]:
59 59 yield self.t("filenodelink", node=hex(mf[f]), file=f)
60 60 if len(files) > self.maxfiles:
61 61 yield self.t("fileellipses")
62 62
63 63 def listfilediffs(self, files, changeset):
64 64 for f in files[:self.maxfiles]:
65 65 yield self.t("filedifflink", node=hex(changeset), file=f)
66 66 if len(files) > self.maxfiles:
67 67 yield self.t("fileellipses")
68 68
69 69 def siblings(self, siblings=[], rev=None, hiderev=None, **args):
70 70 if not rev:
71 71 rev = lambda x: ""
72 72 siblings = [s for s in siblings if s != nullid]
73 73 if len(siblings) == 1 and rev(siblings[0]) == hiderev:
74 74 return
75 75 for s in siblings:
76 76 yield dict(node=hex(s), rev=rev(s), **args)
77 77
78 78 def renamelink(self, fl, node):
79 79 r = fl.renamed(node)
80 80 if r:
81 81 return [dict(file=r[0], node=hex(r[1]))]
82 82 return []
83 83
84 84 def showtag(self, t1, node=nullid, **args):
85 85 for t in self.repo.nodetags(node):
86 86 yield self.t(t1, tag=t, **args)
87 87
88 88 def diff(self, node1, node2, files):
89 89 def filterfiles(filters, files):
90 90 l = [x for x in files if x in filters]
91 91
92 92 for t in filters:
93 93 if t and t[-1] != os.sep:
94 94 t += os.sep
95 95 l += [x for x in files if x.startswith(t)]
96 96 return l
97 97
98 98 parity = [0]
99 99 def diffblock(diff, f, fn):
100 100 yield self.t("diffblock",
101 101 lines=prettyprintlines(diff),
102 102 parity=parity[0],
103 103 file=f,
104 104 filenode=hex(fn or nullid))
105 105 parity[0] = 1 - parity[0]
106 106
107 107 def prettyprintlines(diff):
108 108 for l in diff.splitlines(1):
109 109 if l.startswith('+'):
110 110 yield self.t("difflineplus", line=l)
111 111 elif l.startswith('-'):
112 112 yield self.t("difflineminus", line=l)
113 113 elif l.startswith('@'):
114 114 yield self.t("difflineat", line=l)
115 115 else:
116 116 yield self.t("diffline", line=l)
117 117
118 118 r = self.repo
119 119 cl = r.changelog
120 120 mf = r.manifest
121 121 change1 = cl.read(node1)
122 122 change2 = cl.read(node2)
123 123 mmap1 = mf.read(change1[0])
124 124 mmap2 = mf.read(change2[0])
125 125 date1 = util.datestr(change1[2])
126 126 date2 = util.datestr(change2[2])
127 127
128 128 modified, added, removed, deleted, unknown = r.changes(node1, node2)
129 129 if files:
130 130 modified, added, removed = map(lambda x: filterfiles(files, x),
131 131 (modified, added, removed))
132 132
133 133 diffopts = self.repo.ui.diffopts()
134 134 showfunc = diffopts['showfunc']
135 135 ignorews = diffopts['ignorews']
136 136 for f in modified:
137 137 to = r.file(f).read(mmap1[f])
138 138 tn = r.file(f).read(mmap2[f])
139 139 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
140 140 showfunc=showfunc, ignorews=ignorews), f, tn)
141 141 for f in added:
142 142 to = None
143 143 tn = r.file(f).read(mmap2[f])
144 144 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
145 145 showfunc=showfunc, ignorews=ignorews), f, tn)
146 146 for f in removed:
147 147 to = r.file(f).read(mmap1[f])
148 148 tn = None
149 149 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
150 150 showfunc=showfunc, ignorews=ignorews), f, tn)
151 151
152 152 def changelog(self, pos):
153 153 def changenav(**map):
154 154 def seq(factor, maxchanges=None):
155 155 if maxchanges:
156 156 yield maxchanges
157 157 if maxchanges >= 20 and maxchanges <= 40:
158 158 yield 50
159 159 else:
160 160 yield 1 * factor
161 161 yield 3 * factor
162 162 for f in seq(factor * 10):
163 163 yield f
164 164
165 165 l = []
166 166 last = 0
167 167 for f in seq(1, self.maxchanges):
168 168 if f < self.maxchanges or f <= last:
169 169 continue
170 170 if f > count:
171 171 break
172 172 last = f
173 173 r = "%d" % f
174 174 if pos + f < count:
175 175 l.append(("+" + r, pos + f))
176 176 if pos - f >= 0:
177 177 l.insert(0, ("-" + r, pos - f))
178 178
179 179 yield {"rev": 0, "label": "(0)"}
180 180
181 181 for label, rev in l:
182 182 yield {"label": label, "rev": rev}
183 183
184 184 yield {"label": "tip", "rev": "tip"}
185 185
186 186 def changelist(**map):
187 187 parity = (start - end) & 1
188 188 cl = self.repo.changelog
189 189 l = [] # build a list in forward order for efficiency
190 190 for i in range(start, end):
191 191 n = cl.node(i)
192 192 changes = cl.read(n)
193 193 hn = hex(n)
194 194
195 195 l.insert(0, {"parity": parity,
196 196 "author": changes[1],
197 197 "parent": self.siblings(cl.parents(n), cl.rev,
198 198 cl.rev(n) - 1),
199 199 "child": self.siblings(cl.children(n), cl.rev,
200 200 cl.rev(n) + 1),
201 201 "changelogtag": self.showtag("changelogtag",n),
202 202 "manifest": hex(changes[0]),
203 203 "desc": changes[4],
204 204 "date": changes[2],
205 205 "files": self.listfilediffs(changes[3], n),
206 206 "rev": i,
207 207 "node": hn})
208 208 parity = 1 - parity
209 209
210 210 for e in l:
211 211 yield e
212 212
213 213 cl = self.repo.changelog
214 214 mf = cl.read(cl.tip())[0]
215 215 count = cl.count()
216 216 start = max(0, pos - self.maxchanges + 1)
217 217 end = min(count, start + self.maxchanges)
218 218 pos = end - 1
219 219
220 220 yield self.t('changelog',
221 221 changenav=changenav,
222 222 manifest=hex(mf),
223 223 rev=pos, changesets=count, entries=changelist,
224 224 archives=self.archivelist("tip"))
225 225
226 226 def search(self, query):
227 227
228 228 def changelist(**map):
229 229 cl = self.repo.changelog
230 230 count = 0
231 231 qw = query.lower().split()
232 232
233 233 def revgen():
234 234 for i in range(cl.count() - 1, 0, -100):
235 235 l = []
236 236 for j in range(max(0, i - 100), i):
237 237 n = cl.node(j)
238 238 changes = cl.read(n)
239 239 l.append((n, j, changes))
240 240 l.reverse()
241 241 for e in l:
242 242 yield e
243 243
244 244 for n, i, changes in revgen():
245 245 miss = 0
246 246 for q in qw:
247 247 if not (q in changes[1].lower() or
248 248 q in changes[4].lower() or
249 249 q in " ".join(changes[3][:20]).lower()):
250 250 miss = 1
251 251 break
252 252 if miss:
253 253 continue
254 254
255 255 count += 1
256 256 hn = hex(n)
257 257
258 258 yield self.t('searchentry',
259 259 parity=count & 1,
260 260 author=changes[1],
261 261 parent=self.siblings(cl.parents(n), cl.rev),
262 262 child=self.siblings(cl.children(n), cl.rev),
263 263 changelogtag=self.showtag("changelogtag",n),
264 264 manifest=hex(changes[0]),
265 265 desc=changes[4],
266 266 date=changes[2],
267 267 files=self.listfilediffs(changes[3], n),
268 268 rev=i,
269 269 node=hn)
270 270
271 271 if count >= self.maxchanges:
272 272 break
273 273
274 274 cl = self.repo.changelog
275 275 mf = cl.read(cl.tip())[0]
276 276
277 277 yield self.t('search',
278 278 query=query,
279 279 manifest=hex(mf),
280 280 entries=changelist)
281 281
282 282 def changeset(self, nodeid):
283 283 cl = self.repo.changelog
284 284 n = self.repo.lookup(nodeid)
285 285 nodeid = hex(n)
286 286 changes = cl.read(n)
287 287 p1 = cl.parents(n)[0]
288 288
289 289 files = []
290 290 mf = self.repo.manifest.read(changes[0])
291 291 for f in changes[3]:
292 292 files.append(self.t("filenodelink",
293 293 filenode=hex(mf.get(f, nullid)), file=f))
294 294
295 295 def diff(**map):
296 296 yield self.diff(p1, n, None)
297 297
298 298 yield self.t('changeset',
299 299 diff=diff,
300 300 rev=cl.rev(n),
301 301 node=nodeid,
302 302 parent=self.siblings(cl.parents(n), cl.rev),
303 303 child=self.siblings(cl.children(n), cl.rev),
304 304 changesettag=self.showtag("changesettag",n),
305 305 manifest=hex(changes[0]),
306 306 author=changes[1],
307 307 desc=changes[4],
308 308 date=changes[2],
309 309 files=files,
310 310 archives=self.archivelist(nodeid))
311 311
312 312 def filelog(self, f, filenode):
313 313 cl = self.repo.changelog
314 314 fl = self.repo.file(f)
315 315 filenode = hex(fl.lookup(filenode))
316 316 count = fl.count()
317 317
318 318 def entries(**map):
319 319 l = []
320 320 parity = (count - 1) & 1
321 321
322 322 for i in range(count):
323 323 n = fl.node(i)
324 324 lr = fl.linkrev(n)
325 325 cn = cl.node(lr)
326 326 cs = cl.read(cl.node(lr))
327 327
328 328 l.insert(0, {"parity": parity,
329 329 "filenode": hex(n),
330 330 "filerev": i,
331 331 "file": f,
332 332 "node": hex(cn),
333 333 "author": cs[1],
334 334 "date": cs[2],
335 335 "rename": self.renamelink(fl, n),
336 336 "parent": self.siblings(fl.parents(n),
337 337 fl.rev, file=f),
338 338 "child": self.siblings(fl.children(n),
339 339 fl.rev, file=f),
340 340 "desc": cs[4]})
341 341 parity = 1 - parity
342 342
343 343 for e in l:
344 344 yield e
345 345
346 346 yield self.t("filelog", file=f, filenode=filenode, entries=entries)
347 347
348 348 def filerevision(self, f, node):
349 349 fl = self.repo.file(f)
350 350 n = fl.lookup(node)
351 351 node = hex(n)
352 352 text = fl.read(n)
353 353 changerev = fl.linkrev(n)
354 354 cl = self.repo.changelog
355 355 cn = cl.node(changerev)
356 356 cs = cl.read(cn)
357 357 mfn = cs[0]
358 358
359 359 mt = mimetypes.guess_type(f)[0]
360 360 rawtext = text
361 361 if util.binary(text):
362 362 mt = mt or 'application/octet-stream'
363 363 text = "(binary:%s)" % mt
364 364 mt = mt or 'text/plain'
365 365
366 366 def lines():
367 367 for l, t in enumerate(text.splitlines(1)):
368 368 yield {"line": t,
369 369 "linenumber": "% 6d" % (l + 1),
370 370 "parity": l & 1}
371 371
372 372 yield self.t("filerevision",
373 373 file=f,
374 374 filenode=node,
375 375 path=_up(f),
376 376 text=lines(),
377 377 raw=rawtext,
378 378 mimetype=mt,
379 379 rev=changerev,
380 380 node=hex(cn),
381 381 manifest=hex(mfn),
382 382 author=cs[1],
383 383 date=cs[2],
384 384 parent=self.siblings(fl.parents(n), fl.rev, file=f),
385 385 child=self.siblings(fl.children(n), fl.rev, file=f),
386 386 rename=self.renamelink(fl, n),
387 387 permissions=self.repo.manifest.readflags(mfn)[f])
388 388
389 389 def fileannotate(self, f, node):
390 390 bcache = {}
391 391 ncache = {}
392 392 fl = self.repo.file(f)
393 393 n = fl.lookup(node)
394 394 node = hex(n)
395 395 changerev = fl.linkrev(n)
396 396
397 397 cl = self.repo.changelog
398 398 cn = cl.node(changerev)
399 399 cs = cl.read(cn)
400 400 mfn = cs[0]
401 401
402 402 def annotate(**map):
403 403 parity = 1
404 404 last = None
405 405 for r, l in fl.annotate(n):
406 406 try:
407 407 cnode = ncache[r]
408 408 except KeyError:
409 409 cnode = ncache[r] = self.repo.changelog.node(r)
410 410
411 411 try:
412 412 name = bcache[r]
413 413 except KeyError:
414 414 cl = self.repo.changelog.read(cnode)
415 415 bcache[r] = name = self.repo.ui.shortuser(cl[1])
416 416
417 417 if last != cnode:
418 418 parity = 1 - parity
419 419 last = cnode
420 420
421 421 yield {"parity": parity,
422 422 "node": hex(cnode),
423 423 "rev": r,
424 424 "author": name,
425 425 "file": f,
426 426 "line": l}
427 427
428 428 yield self.t("fileannotate",
429 429 file=f,
430 430 filenode=node,
431 431 annotate=annotate,
432 432 path=_up(f),
433 433 rev=changerev,
434 434 node=hex(cn),
435 435 manifest=hex(mfn),
436 436 author=cs[1],
437 437 date=cs[2],
438 438 rename=self.renamelink(fl, n),
439 439 parent=self.siblings(fl.parents(n), fl.rev, file=f),
440 440 child=self.siblings(fl.children(n), fl.rev, file=f),
441 441 permissions=self.repo.manifest.readflags(mfn)[f])
442 442
443 443 def manifest(self, mnode, path):
444 444 man = self.repo.manifest
445 445 mn = man.lookup(mnode)
446 446 mnode = hex(mn)
447 447 mf = man.read(mn)
448 448 rev = man.rev(mn)
449 449 changerev = man.linkrev(mn)
450 450 node = self.repo.changelog.node(changerev)
451 451 mff = man.readflags(mn)
452 452
453 453 files = {}
454 454
455 455 p = path[1:]
456 456 if p and p[-1] != "/":
457 457 p += "/"
458 458 l = len(p)
459 459
460 460 for f,n in mf.items():
461 461 if f[:l] != p:
462 462 continue
463 463 remain = f[l:]
464 464 if "/" in remain:
465 465 short = remain[:remain.find("/") + 1] # bleah
466 466 files[short] = (f, None)
467 467 else:
468 468 short = os.path.basename(remain)
469 469 files[short] = (f, n)
470 470
471 471 def filelist(**map):
472 472 parity = 0
473 473 fl = files.keys()
474 474 fl.sort()
475 475 for f in fl:
476 476 full, fnode = files[f]
477 477 if not fnode:
478 478 continue
479 479
480 480 yield {"file": full,
481 481 "manifest": mnode,
482 482 "filenode": hex(fnode),
483 483 "parity": parity,
484 484 "basename": f,
485 485 "permissions": mff[full]}
486 486 parity = 1 - parity
487 487
488 488 def dirlist(**map):
489 489 parity = 0
490 490 fl = files.keys()
491 491 fl.sort()
492 492 for f in fl:
493 493 full, fnode = files[f]
494 494 if fnode:
495 495 continue
496 496
497 497 yield {"parity": parity,
498 498 "path": os.path.join(path, f),
499 499 "manifest": mnode,
500 500 "basename": f[:-1]}
501 501 parity = 1 - parity
502 502
503 503 yield self.t("manifest",
504 504 manifest=mnode,
505 505 rev=rev,
506 506 node=hex(node),
507 507 path=path,
508 508 up=_up(path),
509 509 fentries=filelist,
510 510 dentries=dirlist,
511 511 archives=self.archivelist(hex(node)))
512 512
513 513 def tags(self):
514 514 cl = self.repo.changelog
515 515 mf = cl.read(cl.tip())[0]
516 516
517 517 i = self.repo.tagslist()
518 518 i.reverse()
519 519
520 520 def entries(notip=False, **map):
521 521 parity = 0
522 522 for k,n in i:
523 523 if notip and k == "tip": continue
524 524 yield {"parity": parity,
525 525 "tag": k,
526 526 "tagmanifest": hex(cl.read(n)[0]),
527 527 "date": cl.read(n)[2],
528 528 "node": hex(n)}
529 529 parity = 1 - parity
530 530
531 531 yield self.t("tags",
532 532 manifest=hex(mf),
533 533 entries=lambda **x: entries(False, **x),
534 534 entriesnotip=lambda **x: entries(True, **x))
535 535
536 536 def summary(self):
537 537 cl = self.repo.changelog
538 538 mf = cl.read(cl.tip())[0]
539 539
540 540 i = self.repo.tagslist()
541 541 i.reverse()
542 542
543 543 def tagentries(**map):
544 544 parity = 0
545 545 count = 0
546 546 for k,n in i:
547 547 if k == "tip": # skip tip
548 548 continue;
549 549
550 550 count += 1
551 551 if count > 10: # limit to 10 tags
552 552 break;
553 553
554 554 c = cl.read(n)
555 555 m = c[0]
556 556 t = c[2]
557 557
558 558 yield self.t("tagentry",
559 559 parity = parity,
560 560 tag = k,
561 561 node = hex(n),
562 562 date = t,
563 563 tagmanifest = hex(m))
564 564 parity = 1 - parity
565 565
566 566 def changelist(**map):
567 567 parity = 0
568 568 cl = self.repo.changelog
569 569 l = [] # build a list in forward order for efficiency
570 570 for i in range(start, end):
571 571 n = cl.node(i)
572 572 changes = cl.read(n)
573 573 hn = hex(n)
574 574 t = changes[2]
575 575
576 576 l.insert(0, self.t(
577 577 'shortlogentry',
578 578 parity = parity,
579 579 author = changes[1],
580 580 manifest = hex(changes[0]),
581 581 desc = changes[4],
582 582 date = t,
583 583 rev = i,
584 584 node = hn))
585 585 parity = 1 - parity
586 586
587 587 yield l
588 588
589 589 cl = self.repo.changelog
590 590 mf = cl.read(cl.tip())[0]
591 591 count = cl.count()
592 592 start = max(0, count - self.maxchanges)
593 593 end = min(count, start + self.maxchanges)
594 594
595 595 yield self.t("summary",
596 596 desc = self.repo.ui.config("web", "description", "unknown"),
597 597 owner = (self.repo.ui.config("ui", "username") or # preferred
598 598 self.repo.ui.config("web", "contact") or # deprecated
599 599 self.repo.ui.config("web", "author", "unknown")), # also
600 600 lastchange = (0, 0), # FIXME
601 601 manifest = hex(mf),
602 602 tags = tagentries,
603 603 shortlog = changelist)
604 604
605 605 def filediff(self, file, changeset):
606 606 cl = self.repo.changelog
607 607 n = self.repo.lookup(changeset)
608 608 changeset = hex(n)
609 609 p1 = cl.parents(n)[0]
610 610 cs = cl.read(n)
611 611 mf = self.repo.manifest.read(cs[0])
612 612
613 613 def diff(**map):
614 614 yield self.diff(p1, n, [file])
615 615
616 616 yield self.t("filediff",
617 617 file=file,
618 618 filenode=hex(mf.get(file, nullid)),
619 619 node=changeset,
620 620 rev=self.repo.changelog.rev(n),
621 621 parent=self.siblings(cl.parents(n), cl.rev),
622 622 child=self.siblings(cl.children(n), cl.rev),
623 623 diff=diff)
624 624
625 625 archive_specs = {
626 626 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
627 627 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
628 628 'zip': ('application/zip', 'zip', '.zip', None),
629 629 }
630 630
631 631 def archive(self, req, cnode, type_):
632 632 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
633 633 name = "%s-%s" % (reponame, short(cnode))
634 634 mimetype, artype, extension, encoding = self.archive_specs[type_]
635 635 headers = [('Content-type', mimetype),
636 636 ('Content-disposition', 'attachment; filename=%s%s' %
637 637 (name, extension))]
638 638 if encoding:
639 639 headers.append(('Content-encoding', encoding))
640 640 req.header(headers)
641 641 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
642 642
643 643 # add tags to things
644 644 # tags -> list of changesets corresponding to tags
645 645 # find tag, changeset, file
646 646
647 647 def cleanpath(self, path):
648 648 p = util.normpath(path)
649 649 if p[:2] == "..":
650 650 raise Exception("suspicious path")
651 651 return p
652 652
653 def run(self, req):
653 def run(self):
654 if os.environ['GATEWAY_INTERFACE'][0:6] != "CGI/1.":
655 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
656 import mercurial.hgweb.wsgicgi as wsgicgi
657 from request import wsgiapplication
658 def make_web_app():
659 return self.__class__(self.repo, self.reponame)
660 wsgicgi.launch(wsgiapplication(make_web_app))
661
662 def run_wsgi(self, req):
654 663 def header(**map):
655 664 header_file = cStringIO.StringIO(''.join(self.t("header", **map)))
656 665 msg = mimetools.Message(header_file, 0)
657 666 req.header(msg.items())
658 667 yield header_file.read()
659 668
660 669 def rawfileheader(**map):
661 670 req.header([('Content-type', map['mimetype']),
662 671 ('Content-disposition', 'filename=%s' % map['file']),
663 672 ('Content-length', str(len(map['raw'])))])
664 673 yield ''
665 674
666 675 def footer(**map):
667 676 yield self.t("footer",
668 677 motd=self.repo.ui.config("web", "motd", ""),
669 678 **map)
670 679
671 680 def expand_form(form):
672 681 shortcuts = {
673 682 'cl': [('cmd', ['changelog']), ('rev', None)],
674 683 'cs': [('cmd', ['changeset']), ('node', None)],
675 684 'f': [('cmd', ['file']), ('filenode', None)],
676 685 'fl': [('cmd', ['filelog']), ('filenode', None)],
677 686 'fd': [('cmd', ['filediff']), ('node', None)],
678 687 'fa': [('cmd', ['annotate']), ('filenode', None)],
679 688 'mf': [('cmd', ['manifest']), ('manifest', None)],
680 689 'ca': [('cmd', ['archive']), ('node', None)],
681 690 'tags': [('cmd', ['tags'])],
682 691 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
683 692 'static': [('cmd', ['static']), ('file', None)]
684 693 }
685 694
686 695 for k in shortcuts.iterkeys():
687 696 if form.has_key(k):
688 697 for name, value in shortcuts[k]:
689 698 if value is None:
690 699 value = form[k]
691 700 form[name] = value
692 701 del form[k]
693 702
694 703 self.refresh()
695 704
696 705 expand_form(req.form)
697 706
698 707 m = os.path.join(self.templatepath, "map")
699 708 style = self.repo.ui.config("web", "style", "")
700 709 if req.form.has_key('style'):
701 710 style = req.form['style'][0]
702 711 if style:
703 712 b = os.path.basename("map-" + style)
704 713 p = os.path.join(self.templatepath, b)
705 714 if os.path.isfile(p):
706 715 m = p
707 716
708 717 port = req.env["SERVER_PORT"]
709 718 port = port != "80" and (":" + port) or ""
710 719 uri = req.env["REQUEST_URI"]
711 720 if "?" in uri:
712 721 uri = uri.split("?")[0]
713 722 url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
714 723 if not self.reponame:
715 724 self.reponame = (self.repo.ui.config("web", "name")
716 725 or uri.strip('/') or self.repo.root)
717 726
718 727 self.t = templater.templater(m, templater.common_filters,
719 728 defaults={"url": url,
720 729 "repo": self.reponame,
721 730 "header": header,
722 731 "footer": footer,
723 732 "rawfileheader": rawfileheader,
724 733 })
725 734
726 735 if not req.form.has_key('cmd'):
727 736 req.form['cmd'] = [self.t.cache['default'],]
728 737
729 738 cmd = req.form['cmd'][0]
730 739
731 740 method = getattr(self, 'do_' + cmd, None)
732 741 if method:
733 742 method(req)
734 743 else:
735 744 req.write(self.t("error"))
736 745
737 746 def do_changelog(self, req):
738 747 hi = self.repo.changelog.count() - 1
739 748 if req.form.has_key('rev'):
740 749 hi = req.form['rev'][0]
741 750 try:
742 751 hi = self.repo.changelog.rev(self.repo.lookup(hi))
743 752 except hg.RepoError:
744 753 req.write(self.search(hi)) # XXX redirect to 404 page?
745 754 return
746 755
747 756 req.write(self.changelog(hi))
748 757
749 758 def do_changeset(self, req):
750 759 req.write(self.changeset(req.form['node'][0]))
751 760
752 761 def do_manifest(self, req):
753 762 req.write(self.manifest(req.form['manifest'][0],
754 763 self.cleanpath(req.form['path'][0])))
755 764
756 765 def do_tags(self, req):
757 766 req.write(self.tags())
758 767
759 768 def do_summary(self, req):
760 769 req.write(self.summary())
761 770
762 771 def do_filediff(self, req):
763 772 req.write(self.filediff(self.cleanpath(req.form['file'][0]),
764 773 req.form['node'][0]))
765 774
766 775 def do_file(self, req):
767 776 req.write(self.filerevision(self.cleanpath(req.form['file'][0]),
768 777 req.form['filenode'][0]))
769 778
770 779 def do_annotate(self, req):
771 780 req.write(self.fileannotate(self.cleanpath(req.form['file'][0]),
772 781 req.form['filenode'][0]))
773 782
774 783 def do_filelog(self, req):
775 784 req.write(self.filelog(self.cleanpath(req.form['file'][0]),
776 785 req.form['filenode'][0]))
777 786
778 787 def do_heads(self, req):
779 788 resp = " ".join(map(hex, self.repo.heads())) + "\n"
780 789 req.httphdr("application/mercurial-0.1", length=len(resp))
781 790 req.write(resp)
782 791
783 792 def do_branches(self, req):
784 793 nodes = []
785 794 if req.form.has_key('nodes'):
786 795 nodes = map(bin, req.form['nodes'][0].split(" "))
787 796 resp = cStringIO.StringIO()
788 797 for b in self.repo.branches(nodes):
789 798 resp.write(" ".join(map(hex, b)) + "\n")
790 799 resp = resp.getvalue()
791 800 req.httphdr("application/mercurial-0.1", length=len(resp))
792 801 req.write(resp)
793 802
794 803 def do_between(self, req):
795 804 nodes = []
796 805 if req.form.has_key('pairs'):
797 806 pairs = [map(bin, p.split("-"))
798 807 for p in req.form['pairs'][0].split(" ")]
799 808 resp = cStringIO.StringIO()
800 809 for b in self.repo.between(pairs):
801 810 resp.write(" ".join(map(hex, b)) + "\n")
802 811 resp = resp.getvalue()
803 812 req.httphdr("application/mercurial-0.1", length=len(resp))
804 813 req.write(resp)
805 814
806 815 def do_changegroup(self, req):
807 816 req.httphdr("application/mercurial-0.1")
808 817 nodes = []
809 818 if not self.allowpull:
810 819 return
811 820
812 821 if req.form.has_key('roots'):
813 822 nodes = map(bin, req.form['roots'][0].split(" "))
814 823
815 824 z = zlib.compressobj()
816 825 f = self.repo.changegroup(nodes, 'serve')
817 826 while 1:
818 827 chunk = f.read(4096)
819 828 if not chunk:
820 829 break
821 830 req.write(z.compress(chunk))
822 831
823 832 req.write(z.flush())
824 833
825 834 def do_archive(self, req):
826 835 changeset = self.repo.lookup(req.form['node'][0])
827 836 type_ = req.form['type'][0]
828 837 allowed = self.repo.ui.configlist("web", "allow_archive")
829 838 if (type_ in self.archives and (type_ in allowed or
830 839 self.repo.ui.configbool("web", "allow" + type_, False))):
831 840 self.archive(req, changeset, type_)
832 841 return
833 842
834 843 req.write(self.t("error"))
835 844
836 845 def do_static(self, req):
837 846 fname = req.form['file'][0]
838 847 static = self.repo.ui.config("web", "static",
839 848 os.path.join(self.templatepath,
840 849 "static"))
841 850 req.write(staticfile(static, fname, req)
842 851 or self.t("error", error="%r not found" % fname))
843 852
844 853 def do_capabilities(self, req):
845 854 resp = 'unbundle'
846 855 req.httphdr("application/mercurial-0.1", length=len(resp))
847 856 req.write(resp)
848 857
849 858 def check_perm(self, req, op, default):
850 859 '''check permission for operation based on user auth.
851 860 return true if op allowed, else false.
852 861 default is policy to use if no config given.'''
853 862
854 863 user = req.env.get('REMOTE_USER')
855 864
856 865 deny = self.repo.ui.configlist('web', 'deny_' + op)
857 866 if deny and (not user or deny == ['*'] or user in deny):
858 867 return False
859 868
860 869 allow = self.repo.ui.configlist('web', 'allow_' + op)
861 870 return (allow and (allow == ['*'] or user in allow)) or default
862 871
863 872 def do_unbundle(self, req):
864 873 def bail(response, headers={}):
865 874 length = int(req.env['CONTENT_LENGTH'])
866 875 for s in util.filechunkiter(req, limit=length):
867 876 # drain incoming bundle, else client will not see
868 877 # response when run outside cgi script
869 878 pass
870 879 req.httphdr("application/mercurial-0.1", headers=headers)
871 880 req.write('0\n')
872 881 req.write(response)
873 882
874 883 # require ssl by default, auth info cannot be sniffed and
875 884 # replayed
876 885 ssl_req = self.repo.ui.configbool('web', 'push_ssl', True)
877 886 if ssl_req and not req.env.get('HTTPS'):
878 887 bail(_('ssl required\n'))
879 888 return
880 889
881 890 # do not allow push unless explicitly allowed
882 891 if not self.check_perm(req, 'push', False):
883 892 bail(_('push not authorized\n'),
884 893 headers={'status': '401 Unauthorized'})
885 894 return
886 895
887 896 req.httphdr("application/mercurial-0.1")
888 897
889 898 their_heads = req.form['heads'][0].split(' ')
890 899
891 900 def check_heads():
892 901 heads = map(hex, self.repo.heads())
893 902 return their_heads == [hex('force')] or their_heads == heads
894 903
895 904 # fail early if possible
896 905 if not check_heads():
897 906 bail(_('unsynced changes\n'))
898 907 return
899 908
900 909 # do not lock repo until all changegroup data is
901 910 # streamed. save to temporary file.
902 911
903 912 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
904 913 fp = os.fdopen(fd, 'wb+')
905 914 try:
906 915 length = int(req.env['CONTENT_LENGTH'])
907 916 for s in util.filechunkiter(req, limit=length):
908 917 fp.write(s)
909 918
910 919 lock = self.repo.lock()
911 920 try:
912 921 if not check_heads():
913 922 req.write('0\n')
914 923 req.write(_('unsynced changes\n'))
915 924 return
916 925
917 926 fp.seek(0)
918 927
919 928 # send addchangegroup output to client
920 929
921 930 old_stdout = sys.stdout
922 931 sys.stdout = cStringIO.StringIO()
923 932
924 933 try:
925 934 ret = self.repo.addchangegroup(fp, 'serve')
926 935 req.write('%d\n' % ret)
927 936 req.write(sys.stdout.getvalue())
928 937 finally:
929 938 sys.stdout = old_stdout
930 939 finally:
931 940 lock.release()
932 941 finally:
933 942 fp.close()
934 943 os.unlink(tempname)
@@ -1,157 +1,167 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 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os
10 10 from mercurial.demandload import demandload
11 11 demandload(globals(), "ConfigParser mimetools cStringIO")
12 12 demandload(globals(), "mercurial:ui,hg,util,templater")
13 13 demandload(globals(), "mercurial.hgweb.hgweb_mod:hgweb")
14 14 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
15 15 from mercurial.i18n import gettext as _
16 16
17 17 # This is a stopgap
18 18 class hgwebdir(object):
19 19 def __init__(self, config):
20 20 def cleannames(items):
21 21 return [(name.strip(os.sep), path) for name, path in items]
22 22
23 self.origconfig = config
23 24 self.motd = ""
24 25 self.repos_sorted = ('name', False)
25 26 if isinstance(config, (list, tuple)):
26 27 self.repos = cleannames(config)
27 28 self.repos_sorted = ('', False)
28 29 elif isinstance(config, dict):
29 30 self.repos = cleannames(config.items())
30 31 self.repos.sort()
31 32 else:
32 33 cp = ConfigParser.SafeConfigParser()
33 34 cp.read(config)
34 35 self.repos = []
35 36 if cp.has_section('web') and cp.has_option('web', 'motd'):
36 37 self.motd = cp.get('web', 'motd')
37 38 if cp.has_section('paths'):
38 39 self.repos.extend(cleannames(cp.items('paths')))
39 40 if cp.has_section('collections'):
40 41 for prefix, root in cp.items('collections'):
41 42 for path in util.walkrepos(root):
42 43 repo = os.path.normpath(path)
43 44 name = repo
44 45 if name.startswith(prefix):
45 46 name = name[len(prefix):]
46 47 self.repos.append((name.lstrip(os.sep), repo))
47 48 self.repos.sort()
48 49
49 def run(self, req):
50 def run(self):
51 if os.environ['GATEWAY_INTERFACE'][0:6] != "CGI/1.":
52 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
53 import mercurial.hgweb.wsgicgi as wsgicgi
54 from request import wsgiapplication
55 def make_web_app():
56 return self.__class__(self.origconfig)
57 wsgicgi.launch(wsgiapplication(make_web_app))
58
59 def run_wsgi(self, req):
50 60 def header(**map):
51 61 header_file = cStringIO.StringIO(''.join(tmpl("header", **map)))
52 62 msg = mimetools.Message(header_file, 0)
53 63 req.header(msg.items())
54 64 yield header_file.read()
55 65
56 66 def footer(**map):
57 67 yield tmpl("footer", motd=self.motd, **map)
58 68
59 69 m = os.path.join(templater.templatepath(), "map")
60 70 tmpl = templater.templater(m, templater.common_filters,
61 71 defaults={"header": header,
62 72 "footer": footer})
63 73
64 74 def archivelist(ui, nodeid, url):
65 75 allowed = ui.configlist("web", "allow_archive")
66 76 for i in ['zip', 'gz', 'bz2']:
67 77 if i in allowed or ui.configbool("web", "allow" + i):
68 78 yield {"type" : i, "node": nodeid, "url": url}
69 79
70 80 def entries(sortcolumn="", descending=False, **map):
71 81 rows = []
72 82 parity = 0
73 83 for name, path in self.repos:
74 84 u = ui.ui()
75 85 try:
76 86 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
77 87 except IOError:
78 88 pass
79 89 get = u.config
80 90
81 91 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
82 92 .replace("//", "/"))
83 93
84 94 # update time with local timezone
85 95 try:
86 96 d = (get_mtime(path), util.makedate()[1])
87 97 except OSError:
88 98 continue
89 99
90 100 contact = (get("ui", "username") or # preferred
91 101 get("web", "contact") or # deprecated
92 102 get("web", "author", "")) # also
93 103 description = get("web", "description", "")
94 104 name = get("web", "name", name)
95 105 row = dict(contact=contact or "unknown",
96 106 contact_sort=contact.upper() or "unknown",
97 107 name=name,
98 108 name_sort=name,
99 109 url=url,
100 110 description=description or "unknown",
101 111 description_sort=description.upper() or "unknown",
102 112 lastchange=d,
103 113 lastchange_sort=d[1]-d[0],
104 114 archives=archivelist(u, "tip", url))
105 115 if (not sortcolumn
106 116 or (sortcolumn, descending) == self.repos_sorted):
107 117 # fast path for unsorted output
108 118 row['parity'] = parity
109 119 parity = 1 - parity
110 120 yield row
111 121 else:
112 122 rows.append((row["%s_sort" % sortcolumn], row))
113 123 if rows:
114 124 rows.sort()
115 125 if descending:
116 126 rows.reverse()
117 127 for key, row in rows:
118 128 row['parity'] = parity
119 129 parity = 1 - parity
120 130 yield row
121 131
122 132 virtual = req.env.get("PATH_INFO", "").strip('/')
123 133 if virtual:
124 134 real = dict(self.repos).get(virtual)
125 135 if real:
126 136 try:
127 137 hgweb(real).run(req)
128 138 except IOError, inst:
129 139 req.write(tmpl("error", error=inst.strerror))
130 140 except hg.RepoError, inst:
131 141 req.write(tmpl("error", error=str(inst)))
132 142 else:
133 143 req.write(tmpl("notfound", repo=virtual))
134 144 else:
135 145 if req.form.has_key('static'):
136 146 static = os.path.join(templater.templatepath(), "static")
137 147 fname = req.form['static'][0]
138 148 req.write(staticfile(static, fname, req)
139 149 or tmpl("error", error="%r not found" % fname))
140 150 else:
141 151 sortable = ["name", "description", "contact", "lastchange"]
142 152 sortcolumn, descending = self.repos_sorted
143 153 if req.form.has_key('sort'):
144 154 sortcolumn = req.form['sort'][0]
145 155 descending = sortcolumn.startswith('-')
146 156 if descending:
147 157 sortcolumn = sortcolumn[1:]
148 158 if sortcolumn not in sortable:
149 159 sortcolumn = ""
150 160
151 161 sort = [("sort_%s" % column,
152 162 "%s%s" % ((not descending and column == sortcolumn)
153 163 and "-" or "", column))
154 164 for column in sortable]
155 165 req.write(tmpl("index", entries=entries,
156 166 sortcolumn=sortcolumn, descending=descending,
157 167 **dict(sort)))
@@ -1,90 +1,90 b''
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 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 from mercurial.demandload import demandload
10 10 demandload(globals(), "socket sys cgi os errno")
11 11 from mercurial.i18n import gettext as _
12 12
13 13 class wsgiapplication(object):
14 14 def __init__(self, destmaker):
15 15 self.destmaker = destmaker
16 16
17 17 def __call__(self, wsgienv, start_response):
18 18 return _wsgirequest(self.destmaker(), wsgienv, start_response)
19 19
20 20 class _wsgioutputfile(object):
21 21 def __init__(self, request):
22 22 self.request = request
23 23
24 24 def write(self, data):
25 25 self.request.write(data)
26 26 def writelines(self, lines):
27 27 for line in lines:
28 28 self.write(line)
29 29 def flush(self):
30 30 return None
31 31 def close(self):
32 32 return None
33 33
34 34 class _wsgirequest(object):
35 35 def __init__(self, destination, wsgienv, start_response):
36 36 version = wsgienv['wsgi.version']
37 37 if (version < (1,0)) or (version >= (2, 0)):
38 38 raise RuntimeError("Unknown and unsupported WSGI version %d.%d" \
39 39 % version)
40 40 self.inp = wsgienv['wsgi.input']
41 41 self.out = _wsgioutputfile(self)
42 42 self.server_write = None
43 43 self.err = wsgienv['wsgi.errors']
44 44 self.threaded = wsgienv['wsgi.multithread']
45 45 self.multiprocess = wsgienv['wsgi.multiprocess']
46 46 self.run_once = wsgienv['wsgi.run_once']
47 47 self.env = wsgienv
48 48 self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
49 49 self.start_response = start_response
50 50 self.headers = []
51 destination.run(self)
51 destination.run_wsgi(self)
52 52
53 53 def __iter__(self):
54 54 return iter([])
55 55
56 56 def read(self, count=-1):
57 57 return self.inp.read(count)
58 58
59 59 def write(self, *things):
60 60 for thing in things:
61 61 if hasattr(thing, "__iter__"):
62 62 for part in thing:
63 63 self.write(part)
64 64 else:
65 65 thing = str(thing)
66 66 if self.server_write is None:
67 67 if not self.headers:
68 68 raise RuntimeError("request.write called before headers sent (%s)." % thing)
69 69 self.server_write = self.start_response('200 Script output follows',
70 70 self.headers)
71 71 self.start_response = None
72 72 self.headers = None
73 73 try:
74 74 self.server_write(thing)
75 75 except socket.error, inst:
76 76 if inst[0] != errno.ECONNRESET:
77 77 raise
78 78
79 79 def header(self, headers=[('Content-type','text/html')]):
80 80 self.headers.extend(headers)
81 81
82 82 def httphdr(self, type, filename=None, length=0, headers={}):
83 83 headers = headers.items()
84 84 headers.append(('Content-type', type))
85 85 if filename:
86 86 headers.append(('Content-disposition', 'attachment; filename=%s' %
87 87 filename))
88 88 if length:
89 89 headers.append(('Content-length', str(length)))
90 90 self.header(headers)
@@ -1,519 +1,519 b''
1 1 # templater.py - template expansion for output
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from demandload import demandload
9 9 from i18n import gettext as _
10 10 from node import *
11 11 demandload(globals(), "cStringIO cgi re sys os time urllib util textwrap")
12 12
13 13 esctable = {
14 14 '\\': '\\',
15 15 'r': '\r',
16 16 't': '\t',
17 17 'n': '\n',
18 18 'v': '\v',
19 19 }
20 20
21 21 def parsestring(s, quoted=True):
22 22 '''parse a string using simple c-like syntax.
23 23 string must be in quotes if quoted is True.'''
24 24 fp = cStringIO.StringIO()
25 25 if quoted:
26 26 first = s[0]
27 27 if len(s) < 2: raise SyntaxError(_('string too short'))
28 28 if first not in "'\"": raise SyntaxError(_('invalid quote'))
29 29 if s[-1] != first: raise SyntaxError(_('unmatched quotes'))
30 30 s = s[1:-1]
31 31 escape = False
32 32 for c in s:
33 33 if escape:
34 34 fp.write(esctable.get(c, c))
35 35 escape = False
36 36 elif c == '\\': escape = True
37 37 elif quoted and c == first: raise SyntaxError(_('string ends early'))
38 38 else: fp.write(c)
39 39 if escape: raise SyntaxError(_('unterminated escape'))
40 40 return fp.getvalue()
41 41
42 42 class templater(object):
43 43 '''template expansion engine.
44 44
45 45 template expansion works like this. a map file contains key=value
46 46 pairs. if value is quoted, it is treated as string. otherwise, it
47 47 is treated as name of template file.
48 48
49 49 templater is asked to expand a key in map. it looks up key, and
50 50 looks for atrings like this: {foo}. it expands {foo} by looking up
51 51 foo in map, and substituting it. expansion is recursive: it stops
52 52 when there is no more {foo} to replace.
53 53
54 54 expansion also allows formatting and filtering.
55 55
56 56 format uses key to expand each item in list. syntax is
57 57 {key%format}.
58 58
59 59 filter uses function to transform value. syntax is
60 60 {key|filter1|filter2|...}.'''
61 61
62 62 def __init__(self, mapfile, filters={}, defaults={}, cache={}):
63 63 '''set up template engine.
64 64 mapfile is name of file to read map definitions from.
65 65 filters is dict of functions. each transforms a value into another.
66 66 defaults is dict of default map definitions.'''
67 67 self.mapfile = mapfile or 'template'
68 68 self.cache = cache.copy()
69 69 self.map = {}
70 70 self.base = (mapfile and os.path.dirname(mapfile)) or ''
71 71 self.filters = filters
72 72 self.defaults = defaults
73 73
74 74 if not mapfile:
75 75 return
76 76 i = 0
77 77 for l in file(mapfile):
78 78 l = l.strip()
79 79 i += 1
80 80 if not l or l[0] in '#;': continue
81 81 m = re.match(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$', l)
82 82 if m:
83 83 key, val = m.groups()
84 84 if val[0] in "'\"":
85 85 try:
86 86 self.cache[key] = parsestring(val)
87 87 except SyntaxError, inst:
88 88 raise SyntaxError('%s:%s: %s' %
89 89 (mapfile, i, inst.args[0]))
90 90 else:
91 91 self.map[key] = os.path.join(self.base, val)
92 92 else:
93 93 raise SyntaxError(_("%s:%s: parse error") % (mapfile, i))
94 94
95 95 def __contains__(self, key):
96 96 return key in self.cache
97 97
98 98 def __call__(self, t, **map):
99 99 '''perform expansion.
100 100 t is name of map element to expand.
101 101 map is added elements to use during expansion.'''
102 102 m = self.defaults.copy()
103 103 m.update(map)
104 104 try:
105 105 tmpl = self.cache[t]
106 106 except KeyError:
107 107 try:
108 108 tmpl = self.cache[t] = file(self.map[t]).read()
109 109 except IOError, inst:
110 110 raise IOError(inst.args[0], _('template file %s: %s') %
111 111 (self.map[t], inst.args[1]))
112 112 return self.template(tmpl, self.filters, **m)
113 113
114 114 template_re = re.compile(r"[#{]([a-zA-Z_][a-zA-Z0-9_]*)"
115 115 r"((%[a-zA-Z_][a-zA-Z0-9_]*)*)"
116 116 r"((\|[a-zA-Z_][a-zA-Z0-9_]*)*)[#}]")
117 117
118 118 def template(self, tmpl, filters={}, **map):
119 119 lm = map.copy()
120 120 while tmpl:
121 121 m = self.template_re.search(tmpl)
122 122 if m:
123 123 start, end = m.span(0)
124 124 s, e = tmpl[start], tmpl[end - 1]
125 125 key = m.group(1)
126 126 if ((s == '#' and e != '#') or (s == '{' and e != '}')):
127 127 raise SyntaxError(_("'%s'/'%s' mismatch expanding '%s'") %
128 128 (s, e, key))
129 129 if start:
130 130 yield tmpl[:start]
131 131 v = map.get(key, "")
132 132 v = callable(v) and v(**map) or v
133 133
134 134 format = m.group(2)
135 135 fl = m.group(4)
136 136
137 137 if format:
138 138 q = v.__iter__
139 139 for i in q():
140 140 lm.update(i)
141 141 yield self(format[1:], **lm)
142 142
143 143 v = ""
144 144
145 145 elif fl:
146 146 for f in fl.split("|")[1:]:
147 147 v = filters[f](v)
148 148
149 149 yield v
150 150 tmpl = tmpl[end:]
151 151 else:
152 152 yield tmpl
153 153 break
154 154
155 155 agescales = [("second", 1),
156 156 ("minute", 60),
157 157 ("hour", 3600),
158 158 ("day", 3600 * 24),
159 159 ("week", 3600 * 24 * 7),
160 160 ("month", 3600 * 24 * 30),
161 161 ("year", 3600 * 24 * 365)]
162 162
163 163 agescales.reverse()
164 164
165 165 def age(date):
166 166 '''turn a (timestamp, tzoff) tuple into an age string.'''
167 167
168 168 def plural(t, c):
169 169 if c == 1:
170 170 return t
171 171 return t + "s"
172 172 def fmt(t, c):
173 173 return "%d %s" % (c, plural(t, c))
174 174
175 175 now = time.time()
176 176 then = date[0]
177 177 delta = max(1, int(now - then))
178 178
179 179 for t, s in agescales:
180 180 n = delta / s
181 181 if n >= 2 or s == 1:
182 182 return fmt(t, n)
183 183
184 184 def stringify(thing):
185 185 '''turn nested template iterator into string.'''
186 186 cs = cStringIO.StringIO()
187 187 def walk(things):
188 188 for t in things:
189 189 if hasattr(t, '__iter__'):
190 190 walk(t)
191 191 else:
192 192 cs.write(t)
193 193 walk(thing)
194 194 return cs.getvalue()
195 195
196 196 para_re = None
197 197 space_re = None
198 198
199 199 def fill(text, width):
200 200 '''fill many paragraphs.'''
201 201 global para_re, space_re
202 202 if para_re is None:
203 203 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
204 204 space_re = re.compile(r' +')
205
205
206 206 def findparas():
207 207 start = 0
208 208 while True:
209 209 m = para_re.search(text, start)
210 210 if not m:
211 211 w = len(text)
212 212 while w > start and text[w-1].isspace(): w -= 1
213 213 yield text[start:w], text[w:]
214 214 break
215 215 yield text[start:m.start(0)], m.group(1)
216 216 start = m.end(1)
217 217
218 218 fp = cStringIO.StringIO()
219 219 for para, rest in findparas():
220 220 fp.write(space_re.sub(' ', textwrap.fill(para, width)))
221 221 fp.write(rest)
222 222 return fp.getvalue()
223 223
224 224 def isodate(date):
225 225 '''turn a (timestamp, tzoff) tuple into an iso 8631 date and time.'''
226 226 return util.datestr(date, format='%Y-%m-%d %H:%M')
227 227
228 228 def nl2br(text):
229 229 '''replace raw newlines with xhtml line breaks.'''
230 230 return text.replace('\n', '<br/>\n')
231 231
232 232 def obfuscate(text):
233 233 return ''.join(['&#%d;' % ord(c) for c in text])
234 234
235 235 def domain(author):
236 236 '''get domain of author, or empty string if none.'''
237 237 f = author.find('@')
238 238 if f == -1: return ''
239 239 author = author[f+1:]
240 240 f = author.find('>')
241 241 if f >= 0: author = author[:f]
242 242 return author
243 243
244 244 def email(author):
245 245 '''get email of author.'''
246 246 r = author.find('>')
247 247 if r == -1: r = None
248 248 return author[author.find('<')+1:r]
249 249
250 250 def person(author):
251 251 '''get name of author, or else username.'''
252 252 f = author.find('<')
253 253 if f == -1: return util.shortuser(author)
254 254 return author[:f].rstrip()
255 255
256 256 def shortdate(date):
257 257 '''turn (timestamp, tzoff) tuple into iso 8631 date.'''
258 258 return util.datestr(date, format='%Y-%m-%d', timezone=False)
259 259
260 260 def indent(text, prefix):
261 261 '''indent each non-empty line of text after first with prefix.'''
262 262 fp = cStringIO.StringIO()
263 263 lines = text.splitlines()
264 264 num_lines = len(lines)
265 265 for i in xrange(num_lines):
266 266 l = lines[i]
267 267 if i and l.strip(): fp.write(prefix)
268 268 fp.write(l)
269 269 if i < num_lines - 1 or text.endswith('\n'):
270 270 fp.write('\n')
271 271 return fp.getvalue()
272 272
273 273 common_filters = {
274 274 "addbreaks": nl2br,
275 275 "basename": os.path.basename,
276 276 "age": age,
277 277 "date": lambda x: util.datestr(x),
278 278 "domain": domain,
279 279 "email": email,
280 280 "escape": lambda x: cgi.escape(x, True),
281 281 "fill68": lambda x: fill(x, width=68),
282 282 "fill76": lambda x: fill(x, width=76),
283 283 "firstline": lambda x: x.splitlines(1)[0].rstrip('\r\n'),
284 284 "tabindent": lambda x: indent(x, '\t'),
285 285 "isodate": isodate,
286 286 "obfuscate": obfuscate,
287 287 "permissions": lambda x: x and "-rwxr-xr-x" or "-rw-r--r--",
288 288 "person": person,
289 289 "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S"),
290 290 "short": lambda x: x[:12],
291 291 "shortdate": shortdate,
292 292 "stringify": stringify,
293 293 "strip": lambda x: x.strip(),
294 294 "urlescape": lambda x: urllib.quote(x),
295 295 "user": lambda x: util.shortuser(x),
296 296 }
297 297
298 298 def templatepath(name=None):
299 299 '''return location of template file or directory (if no name).
300 300 returns None if not found.'''
301 301
302 302 # executable version (py2exe) doesn't support __file__
303 303 if hasattr(sys, 'frozen'):
304 304 module = sys.executable
305 305 else:
306 306 module = __file__
307 307 for f in 'templates', '../templates':
308 308 fl = f.split('/')
309 309 if name: fl.append(name)
310 310 p = os.path.join(os.path.dirname(module), *fl)
311 311 if (name and os.path.exists(p)) or os.path.isdir(p):
312 312 return os.path.normpath(p)
313 313
314 314 class changeset_templater(object):
315 315 '''format changeset information.'''
316 316
317 317 def __init__(self, ui, repo, mapfile, dest=None):
318 318 self.t = templater(mapfile, common_filters,
319 319 cache={'parent': '{rev}:{node|short} ',
320 320 'manifest': '{rev}:{node|short}'})
321 321 self.ui = ui
322 322 self.dest = dest
323 323 self.repo = repo
324 324
325 325 def use_template(self, t):
326 326 '''set template string to use'''
327 327 self.t.cache['changeset'] = t
328 328
329 329 def write(self, thing, header=False):
330 330 '''write expanded template.
331 331 uses in-order recursive traverse of iterators.'''
332 332 dest = self.dest or self.ui
333 333 for t in thing:
334 334 if hasattr(t, '__iter__'):
335 335 self.write(t, header=header)
336 336 elif header:
337 337 dest.write_header(t)
338 338 else:
339 339 dest.write(t)
340 340
341 341 def write_header(self, thing):
342 342 self.write(thing, header=True)
343 343
344 344 def show(self, rev=0, changenode=None, brinfo=None, changes=None,
345 345 **props):
346 346 '''show a single changeset or file revision'''
347 347 log = self.repo.changelog
348 348 if changenode is None:
349 349 changenode = log.node(rev)
350 350 elif not rev:
351 351 rev = log.rev(changenode)
352 352 if changes is None:
353 353 changes = log.read(changenode)
354 354
355 355 def showlist(name, values, plural=None, **args):
356 356 '''expand set of values.
357 357 name is name of key in template map.
358 358 values is list of strings or dicts.
359 359 plural is plural of name, if not simply name + 's'.
360 360
361 361 expansion works like this, given name 'foo'.
362 362
363 363 if values is empty, expand 'no_foos'.
364 364
365 365 if 'foo' not in template map, return values as a string,
366 366 joined by space.
367 367
368 368 expand 'start_foos'.
369 369
370 370 for each value, expand 'foo'. if 'last_foo' in template
371 371 map, expand it instead of 'foo' for last key.
372 372
373 373 expand 'end_foos'.
374 374 '''
375 375 if plural: names = plural
376 376 else: names = name + 's'
377 377 if not values:
378 378 noname = 'no_' + names
379 379 if noname in self.t:
380 380 yield self.t(noname, **args)
381 381 return
382 382 if name not in self.t:
383 383 if isinstance(values[0], str):
384 384 yield ' '.join(values)
385 385 else:
386 386 for v in values:
387 387 yield dict(v, **args)
388 388 return
389 389 startname = 'start_' + names
390 390 if startname in self.t:
391 391 yield self.t(startname, **args)
392 392 vargs = args.copy()
393 393 def one(v, tag=name):
394 394 try:
395 395 vargs.update(v)
396 396 except (AttributeError, ValueError):
397 397 try:
398 398 for a, b in v:
399 399 vargs[a] = b
400 400 except ValueError:
401 401 vargs[name] = v
402 402 return self.t(tag, **vargs)
403 403 lastname = 'last_' + name
404 404 if lastname in self.t:
405 405 last = values.pop()
406 406 else:
407 407 last = None
408 408 for v in values:
409 409 yield one(v)
410 410 if last is not None:
411 411 yield one(last, tag=lastname)
412 412 endname = 'end_' + names
413 413 if endname in self.t:
414 414 yield self.t(endname, **args)
415 415
416 416 if brinfo:
417 417 def showbranches(**args):
418 418 if changenode in brinfo:
419 419 for x in showlist('branch', brinfo[changenode],
420 420 plural='branches', **args):
421 421 yield x
422 422 else:
423 423 showbranches = ''
424 424
425 425 if self.ui.debugflag:
426 426 def showmanifest(**args):
427 427 args = args.copy()
428 428 args.update(dict(rev=self.repo.manifest.rev(changes[0]),
429 429 node=hex(changes[0])))
430 430 yield self.t('manifest', **args)
431 431 else:
432 432 showmanifest = ''
433 433
434 434 def showparents(**args):
435 435 parents = [[('rev', log.rev(p)), ('node', hex(p))]
436 436 for p in log.parents(changenode)
437 437 if self.ui.debugflag or p != nullid]
438 438 if (not self.ui.debugflag and len(parents) == 1 and
439 439 parents[0][0][1] == rev - 1):
440 440 return
441 441 for x in showlist('parent', parents, **args):
442 442 yield x
443 443
444 444 def showtags(**args):
445 445 for x in showlist('tag', self.repo.nodetags(changenode), **args):
446 446 yield x
447 447
448 448 if self.ui.debugflag:
449 449 files = self.repo.changes(log.parents(changenode)[0], changenode)
450 450 def showfiles(**args):
451 451 for x in showlist('file', files[0], **args): yield x
452 452 def showadds(**args):
453 453 for x in showlist('file_add', files[1], **args): yield x
454 454 def showdels(**args):
455 455 for x in showlist('file_del', files[2], **args): yield x
456 456 else:
457 457 def showfiles(**args):
458 458 for x in showlist('file', changes[3], **args): yield x
459 459 showadds = ''
460 460 showdels = ''
461 461
462 462 defprops = {
463 463 'author': changes[1],
464 464 'branches': showbranches,
465 465 'date': changes[2],
466 466 'desc': changes[4],
467 467 'file_adds': showadds,
468 468 'file_dels': showdels,
469 469 'files': showfiles,
470 470 'manifest': showmanifest,
471 471 'node': hex(changenode),
472 472 'parents': showparents,
473 473 'rev': rev,
474 474 'tags': showtags,
475 475 }
476 476 props = props.copy()
477 477 props.update(defprops)
478 478
479 479 try:
480 480 if self.ui.debugflag and 'header_debug' in self.t:
481 481 key = 'header_debug'
482 482 elif self.ui.quiet and 'header_quiet' in self.t:
483 483 key = 'header_quiet'
484 484 elif self.ui.verbose and 'header_verbose' in self.t:
485 485 key = 'header_verbose'
486 486 elif 'header' in self.t:
487 487 key = 'header'
488 488 else:
489 489 key = ''
490 490 if key:
491 491 self.write_header(self.t(key, **props))
492 492 if self.ui.debugflag and 'changeset_debug' in self.t:
493 493 key = 'changeset_debug'
494 494 elif self.ui.quiet and 'changeset_quiet' in self.t:
495 495 key = 'changeset_quiet'
496 496 elif self.ui.verbose and 'changeset_verbose' in self.t:
497 497 key = 'changeset_verbose'
498 498 else:
499 499 key = 'changeset'
500 500 self.write(self.t(key, **props))
501 501 except KeyError, inst:
502 502 raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile,
503 503 inst.args[0]))
504 504 except SyntaxError, inst:
505 505 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
506 506
507 507 class stringio(object):
508 508 '''wrap cStringIO for use by changeset_templater.'''
509 509 def __init__(self):
510 510 self.fp = cStringIO.StringIO()
511 511
512 512 def write(self, *args):
513 513 for a in args:
514 514 self.fp.write(a)
515 515
516 516 write_header = write
517 517
518 518 def __getattr__(self, key):
519 519 return getattr(self.fp, key)
General Comments 0
You need to be logged in to leave comments. Login now