##// END OF EJS Templates
Make hgwebdir columns sortable.
Thomas Arendsen Hein -
r2173:d1943df6 default
parent child Browse files
Show More
@@ -1,1103 +1,1139 b''
1 1 # hgweb.py - web interface to a mercurial 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, cgi, sys
10 10 import mimetypes
11 11 from demandload import demandload
12 12 demandload(globals(), "mdiff time re socket zlib errno ui hg ConfigParser")
13 13 demandload(globals(), "tempfile StringIO BaseHTTPServer util SocketServer")
14 14 demandload(globals(), "archival mimetypes templater urllib")
15 15 from node import *
16 16 from i18n import gettext as _
17 17
18 18 def splitURI(uri):
19 19 """ Return path and query splited from uri
20 20
21 21 Just like CGI environment, the path is unquoted, the query is
22 22 not.
23 23 """
24 24 if '?' in uri:
25 25 path, query = uri.split('?', 1)
26 26 else:
27 27 path, query = uri, ''
28 28 return urllib.unquote(path), query
29 29
30 30 def up(p):
31 31 if p[0] != "/":
32 32 p = "/" + p
33 33 if p[-1] == "/":
34 34 p = p[:-1]
35 35 up = os.path.dirname(p)
36 36 if up == "/":
37 37 return "/"
38 38 return up + "/"
39 39
40 40 def get_mtime(repo_path):
41 41 hg_path = os.path.join(repo_path, ".hg")
42 42 cl_path = os.path.join(hg_path, "00changelog.i")
43 43 if os.path.exists(os.path.join(cl_path)):
44 44 return os.stat(cl_path).st_mtime
45 45 else:
46 46 return os.stat(hg_path).st_mtime
47 47
48 48 def staticfile(directory, fname):
49 49 """return a file inside directory with guessed content-type header
50 50
51 51 fname always uses '/' as directory separator and isn't allowed to
52 52 contain unusual path components.
53 53 Content-type is guessed using the mimetypes module.
54 54 Return an empty string if fname is illegal or file not found.
55 55
56 56 """
57 57 parts = fname.split('/')
58 58 path = directory
59 59 for part in parts:
60 60 if (part in ('', os.curdir, os.pardir) or
61 61 os.sep in part or os.altsep is not None and os.altsep in part):
62 62 return ""
63 63 path = os.path.join(path, part)
64 64 try:
65 65 os.stat(path)
66 66 ct = mimetypes.guess_type(path)[0] or "text/plain"
67 67 return "Content-type: %s\n\n%s" % (ct, file(path).read())
68 68 except (TypeError, OSError):
69 69 # illegal fname or unreadable file
70 70 return ""
71 71
72 72 class hgrequest(object):
73 73 def __init__(self, inp=None, out=None, env=None):
74 74 self.inp = inp or sys.stdin
75 75 self.out = out or sys.stdout
76 76 self.env = env or os.environ
77 77 self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
78 78
79 79 def write(self, *things):
80 80 for thing in things:
81 81 if hasattr(thing, "__iter__"):
82 82 for part in thing:
83 83 self.write(part)
84 84 else:
85 85 try:
86 86 self.out.write(str(thing))
87 87 except socket.error, inst:
88 88 if inst[0] != errno.ECONNRESET:
89 89 raise
90 90
91 91 def header(self, headers=[('Content-type','text/html')]):
92 92 for header in headers:
93 93 self.out.write("%s: %s\r\n" % header)
94 94 self.out.write("\r\n")
95 95
96 96 def httphdr(self, type, file="", size=0):
97 97
98 98 headers = [('Content-type', type)]
99 99 if file:
100 100 headers.append(('Content-disposition', 'attachment; filename=%s' % file))
101 101 if size > 0:
102 102 headers.append(('Content-length', str(size)))
103 103 self.header(headers)
104 104
105 105 class hgweb(object):
106 106 def __init__(self, repo, name=None):
107 107 if type(repo) == type(""):
108 108 self.repo = hg.repository(ui.ui(), repo)
109 109 else:
110 110 self.repo = repo
111 111
112 112 self.mtime = -1
113 113 self.reponame = name
114 114 self.archives = 'zip', 'gz', 'bz2'
115 115
116 116 def refresh(self):
117 117 mtime = get_mtime(self.repo.root)
118 118 if mtime != self.mtime:
119 119 self.mtime = mtime
120 120 self.repo = hg.repository(self.repo.ui, self.repo.root)
121 121 self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
122 122 self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
123 123 self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
124 124
125 125 def archivelist(self, nodeid):
126 126 for i in self.archives:
127 127 if self.repo.ui.configbool("web", "allow" + i, False):
128 128 yield {"type" : i, "node" : nodeid, "url": ""}
129 129
130 130 def listfiles(self, files, mf):
131 131 for f in files[:self.maxfiles]:
132 132 yield self.t("filenodelink", node=hex(mf[f]), file=f)
133 133 if len(files) > self.maxfiles:
134 134 yield self.t("fileellipses")
135 135
136 136 def listfilediffs(self, files, changeset):
137 137 for f in files[:self.maxfiles]:
138 138 yield self.t("filedifflink", node=hex(changeset), file=f)
139 139 if len(files) > self.maxfiles:
140 140 yield self.t("fileellipses")
141 141
142 142 def siblings(self, siblings=[], rev=None, hiderev=None, **args):
143 143 if not rev:
144 144 rev = lambda x: ""
145 145 siblings = [s for s in siblings if s != nullid]
146 146 if len(siblings) == 1 and rev(siblings[0]) == hiderev:
147 147 return
148 148 for s in siblings:
149 149 yield dict(node=hex(s), rev=rev(s), **args)
150 150
151 151 def renamelink(self, fl, node):
152 152 r = fl.renamed(node)
153 153 if r:
154 154 return [dict(file=r[0], node=hex(r[1]))]
155 155 return []
156 156
157 157 def showtag(self, t1, node=nullid, **args):
158 158 for t in self.repo.nodetags(node):
159 159 yield self.t(t1, tag=t, **args)
160 160
161 161 def diff(self, node1, node2, files):
162 162 def filterfiles(filters, files):
163 163 l = [x for x in files if x in filters]
164 164
165 165 for t in filters:
166 166 if t and t[-1] != os.sep:
167 167 t += os.sep
168 168 l += [x for x in files if x.startswith(t)]
169 169 return l
170 170
171 171 parity = [0]
172 172 def diffblock(diff, f, fn):
173 173 yield self.t("diffblock",
174 174 lines=prettyprintlines(diff),
175 175 parity=parity[0],
176 176 file=f,
177 177 filenode=hex(fn or nullid))
178 178 parity[0] = 1 - parity[0]
179 179
180 180 def prettyprintlines(diff):
181 181 for l in diff.splitlines(1):
182 182 if l.startswith('+'):
183 183 yield self.t("difflineplus", line=l)
184 184 elif l.startswith('-'):
185 185 yield self.t("difflineminus", line=l)
186 186 elif l.startswith('@'):
187 187 yield self.t("difflineat", line=l)
188 188 else:
189 189 yield self.t("diffline", line=l)
190 190
191 191 r = self.repo
192 192 cl = r.changelog
193 193 mf = r.manifest
194 194 change1 = cl.read(node1)
195 195 change2 = cl.read(node2)
196 196 mmap1 = mf.read(change1[0])
197 197 mmap2 = mf.read(change2[0])
198 198 date1 = util.datestr(change1[2])
199 199 date2 = util.datestr(change2[2])
200 200
201 201 modified, added, removed, deleted, unknown = r.changes(node1, node2)
202 202 if files:
203 203 modified, added, removed = map(lambda x: filterfiles(files, x),
204 204 (modified, added, removed))
205 205
206 206 diffopts = self.repo.ui.diffopts()
207 207 showfunc = diffopts['showfunc']
208 208 ignorews = diffopts['ignorews']
209 209 for f in modified:
210 210 to = r.file(f).read(mmap1[f])
211 211 tn = r.file(f).read(mmap2[f])
212 212 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
213 213 showfunc=showfunc, ignorews=ignorews), f, tn)
214 214 for f in added:
215 215 to = None
216 216 tn = r.file(f).read(mmap2[f])
217 217 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
218 218 showfunc=showfunc, ignorews=ignorews), f, tn)
219 219 for f in removed:
220 220 to = r.file(f).read(mmap1[f])
221 221 tn = None
222 222 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
223 223 showfunc=showfunc, ignorews=ignorews), f, tn)
224 224
225 225 def changelog(self, pos):
226 226 def changenav(**map):
227 227 def seq(factor, maxchanges=None):
228 228 if maxchanges:
229 229 yield maxchanges
230 230 if maxchanges >= 20 and maxchanges <= 40:
231 231 yield 50
232 232 else:
233 233 yield 1 * factor
234 234 yield 3 * factor
235 235 for f in seq(factor * 10):
236 236 yield f
237 237
238 238 l = []
239 239 last = 0
240 240 for f in seq(1, self.maxchanges):
241 241 if f < self.maxchanges or f <= last:
242 242 continue
243 243 if f > count:
244 244 break
245 245 last = f
246 246 r = "%d" % f
247 247 if pos + f < count:
248 248 l.append(("+" + r, pos + f))
249 249 if pos - f >= 0:
250 250 l.insert(0, ("-" + r, pos - f))
251 251
252 252 yield {"rev": 0, "label": "(0)"}
253 253
254 254 for label, rev in l:
255 255 yield {"label": label, "rev": rev}
256 256
257 257 yield {"label": "tip", "rev": "tip"}
258 258
259 259 def changelist(**map):
260 260 parity = (start - end) & 1
261 261 cl = self.repo.changelog
262 262 l = [] # build a list in forward order for efficiency
263 263 for i in range(start, end):
264 264 n = cl.node(i)
265 265 changes = cl.read(n)
266 266 hn = hex(n)
267 267
268 268 l.insert(0, {"parity": parity,
269 269 "author": changes[1],
270 270 "parent": self.siblings(cl.parents(n), cl.rev,
271 271 cl.rev(n) - 1),
272 272 "child": self.siblings(cl.children(n), cl.rev,
273 273 cl.rev(n) + 1),
274 274 "changelogtag": self.showtag("changelogtag",n),
275 275 "manifest": hex(changes[0]),
276 276 "desc": changes[4],
277 277 "date": changes[2],
278 278 "files": self.listfilediffs(changes[3], n),
279 279 "rev": i,
280 280 "node": hn})
281 281 parity = 1 - parity
282 282
283 283 for e in l:
284 284 yield e
285 285
286 286 cl = self.repo.changelog
287 287 mf = cl.read(cl.tip())[0]
288 288 count = cl.count()
289 289 start = max(0, pos - self.maxchanges + 1)
290 290 end = min(count, start + self.maxchanges)
291 291 pos = end - 1
292 292
293 293 yield self.t('changelog',
294 294 changenav=changenav,
295 295 manifest=hex(mf),
296 296 rev=pos, changesets=count, entries=changelist,
297 297 archives=self.archivelist("tip"))
298 298
299 299 def search(self, query):
300 300
301 301 def changelist(**map):
302 302 cl = self.repo.changelog
303 303 count = 0
304 304 qw = query.lower().split()
305 305
306 306 def revgen():
307 307 for i in range(cl.count() - 1, 0, -100):
308 308 l = []
309 309 for j in range(max(0, i - 100), i):
310 310 n = cl.node(j)
311 311 changes = cl.read(n)
312 312 l.append((n, j, changes))
313 313 l.reverse()
314 314 for e in l:
315 315 yield e
316 316
317 317 for n, i, changes in revgen():
318 318 miss = 0
319 319 for q in qw:
320 320 if not (q in changes[1].lower() or
321 321 q in changes[4].lower() or
322 322 q in " ".join(changes[3][:20]).lower()):
323 323 miss = 1
324 324 break
325 325 if miss:
326 326 continue
327 327
328 328 count += 1
329 329 hn = hex(n)
330 330
331 331 yield self.t('searchentry',
332 332 parity=count & 1,
333 333 author=changes[1],
334 334 parent=self.siblings(cl.parents(n), cl.rev),
335 335 child=self.siblings(cl.children(n), cl.rev),
336 336 changelogtag=self.showtag("changelogtag",n),
337 337 manifest=hex(changes[0]),
338 338 desc=changes[4],
339 339 date=changes[2],
340 340 files=self.listfilediffs(changes[3], n),
341 341 rev=i,
342 342 node=hn)
343 343
344 344 if count >= self.maxchanges:
345 345 break
346 346
347 347 cl = self.repo.changelog
348 348 mf = cl.read(cl.tip())[0]
349 349
350 350 yield self.t('search',
351 351 query=query,
352 352 manifest=hex(mf),
353 353 entries=changelist)
354 354
355 355 def changeset(self, nodeid):
356 356 cl = self.repo.changelog
357 357 n = self.repo.lookup(nodeid)
358 358 nodeid = hex(n)
359 359 changes = cl.read(n)
360 360 p1 = cl.parents(n)[0]
361 361
362 362 files = []
363 363 mf = self.repo.manifest.read(changes[0])
364 364 for f in changes[3]:
365 365 files.append(self.t("filenodelink",
366 366 filenode=hex(mf.get(f, nullid)), file=f))
367 367
368 368 def diff(**map):
369 369 yield self.diff(p1, n, None)
370 370
371 371 yield self.t('changeset',
372 372 diff=diff,
373 373 rev=cl.rev(n),
374 374 node=nodeid,
375 375 parent=self.siblings(cl.parents(n), cl.rev),
376 376 child=self.siblings(cl.children(n), cl.rev),
377 377 changesettag=self.showtag("changesettag",n),
378 378 manifest=hex(changes[0]),
379 379 author=changes[1],
380 380 desc=changes[4],
381 381 date=changes[2],
382 382 files=files,
383 383 archives=self.archivelist(nodeid))
384 384
385 385 def filelog(self, f, filenode):
386 386 cl = self.repo.changelog
387 387 fl = self.repo.file(f)
388 388 filenode = hex(fl.lookup(filenode))
389 389 count = fl.count()
390 390
391 391 def entries(**map):
392 392 l = []
393 393 parity = (count - 1) & 1
394 394
395 395 for i in range(count):
396 396 n = fl.node(i)
397 397 lr = fl.linkrev(n)
398 398 cn = cl.node(lr)
399 399 cs = cl.read(cl.node(lr))
400 400
401 401 l.insert(0, {"parity": parity,
402 402 "filenode": hex(n),
403 403 "filerev": i,
404 404 "file": f,
405 405 "node": hex(cn),
406 406 "author": cs[1],
407 407 "date": cs[2],
408 408 "rename": self.renamelink(fl, n),
409 409 "parent": self.siblings(fl.parents(n),
410 410 fl.rev, file=f),
411 411 "child": self.siblings(fl.children(n),
412 412 fl.rev, file=f),
413 413 "desc": cs[4]})
414 414 parity = 1 - parity
415 415
416 416 for e in l:
417 417 yield e
418 418
419 419 yield self.t("filelog", file=f, filenode=filenode, entries=entries)
420 420
421 421 def filerevision(self, f, node):
422 422 fl = self.repo.file(f)
423 423 n = fl.lookup(node)
424 424 node = hex(n)
425 425 text = fl.read(n)
426 426 changerev = fl.linkrev(n)
427 427 cl = self.repo.changelog
428 428 cn = cl.node(changerev)
429 429 cs = cl.read(cn)
430 430 mfn = cs[0]
431 431
432 432 mt = mimetypes.guess_type(f)[0]
433 433 rawtext = text
434 434 if util.binary(text):
435 435 mt = mt or 'application/octet-stream'
436 436 text = "(binary:%s)" % mt
437 437 mt = mt or 'text/plain'
438 438
439 439 def lines():
440 440 for l, t in enumerate(text.splitlines(1)):
441 441 yield {"line": t,
442 442 "linenumber": "% 6d" % (l + 1),
443 443 "parity": l & 1}
444 444
445 445 yield self.t("filerevision",
446 446 file=f,
447 447 filenode=node,
448 448 path=up(f),
449 449 text=lines(),
450 450 raw=rawtext,
451 451 mimetype=mt,
452 452 rev=changerev,
453 453 node=hex(cn),
454 454 manifest=hex(mfn),
455 455 author=cs[1],
456 456 date=cs[2],
457 457 parent=self.siblings(fl.parents(n), fl.rev, file=f),
458 458 child=self.siblings(fl.children(n), fl.rev, file=f),
459 459 rename=self.renamelink(fl, n),
460 460 permissions=self.repo.manifest.readflags(mfn)[f])
461 461
462 462 def fileannotate(self, f, node):
463 463 bcache = {}
464 464 ncache = {}
465 465 fl = self.repo.file(f)
466 466 n = fl.lookup(node)
467 467 node = hex(n)
468 468 changerev = fl.linkrev(n)
469 469
470 470 cl = self.repo.changelog
471 471 cn = cl.node(changerev)
472 472 cs = cl.read(cn)
473 473 mfn = cs[0]
474 474
475 475 def annotate(**map):
476 476 parity = 1
477 477 last = None
478 478 for r, l in fl.annotate(n):
479 479 try:
480 480 cnode = ncache[r]
481 481 except KeyError:
482 482 cnode = ncache[r] = self.repo.changelog.node(r)
483 483
484 484 try:
485 485 name = bcache[r]
486 486 except KeyError:
487 487 cl = self.repo.changelog.read(cnode)
488 488 bcache[r] = name = self.repo.ui.shortuser(cl[1])
489 489
490 490 if last != cnode:
491 491 parity = 1 - parity
492 492 last = cnode
493 493
494 494 yield {"parity": parity,
495 495 "node": hex(cnode),
496 496 "rev": r,
497 497 "author": name,
498 498 "file": f,
499 499 "line": l}
500 500
501 501 yield self.t("fileannotate",
502 502 file=f,
503 503 filenode=node,
504 504 annotate=annotate,
505 505 path=up(f),
506 506 rev=changerev,
507 507 node=hex(cn),
508 508 manifest=hex(mfn),
509 509 author=cs[1],
510 510 date=cs[2],
511 511 rename=self.renamelink(fl, n),
512 512 parent=self.siblings(fl.parents(n), fl.rev, file=f),
513 513 child=self.siblings(fl.children(n), fl.rev, file=f),
514 514 permissions=self.repo.manifest.readflags(mfn)[f])
515 515
516 516 def manifest(self, mnode, path):
517 517 man = self.repo.manifest
518 518 mn = man.lookup(mnode)
519 519 mnode = hex(mn)
520 520 mf = man.read(mn)
521 521 rev = man.rev(mn)
522 522 node = self.repo.changelog.node(rev)
523 523 mff = man.readflags(mn)
524 524
525 525 files = {}
526 526
527 527 p = path[1:]
528 528 if p and p[-1] != "/":
529 529 p += "/"
530 530 l = len(p)
531 531
532 532 for f,n in mf.items():
533 533 if f[:l] != p:
534 534 continue
535 535 remain = f[l:]
536 536 if "/" in remain:
537 537 short = remain[:remain.find("/") + 1] # bleah
538 538 files[short] = (f, None)
539 539 else:
540 540 short = os.path.basename(remain)
541 541 files[short] = (f, n)
542 542
543 543 def filelist(**map):
544 544 parity = 0
545 545 fl = files.keys()
546 546 fl.sort()
547 547 for f in fl:
548 548 full, fnode = files[f]
549 549 if not fnode:
550 550 continue
551 551
552 552 yield {"file": full,
553 553 "manifest": mnode,
554 554 "filenode": hex(fnode),
555 555 "parity": parity,
556 556 "basename": f,
557 557 "permissions": mff[full]}
558 558 parity = 1 - parity
559 559
560 560 def dirlist(**map):
561 561 parity = 0
562 562 fl = files.keys()
563 563 fl.sort()
564 564 for f in fl:
565 565 full, fnode = files[f]
566 566 if fnode:
567 567 continue
568 568
569 569 yield {"parity": parity,
570 570 "path": os.path.join(path, f),
571 571 "manifest": mnode,
572 572 "basename": f[:-1]}
573 573 parity = 1 - parity
574 574
575 575 yield self.t("manifest",
576 576 manifest=mnode,
577 577 rev=rev,
578 578 node=hex(node),
579 579 path=path,
580 580 up=up(path),
581 581 fentries=filelist,
582 582 dentries=dirlist,
583 583 archives=self.archivelist(hex(node)))
584 584
585 585 def tags(self):
586 586 cl = self.repo.changelog
587 587 mf = cl.read(cl.tip())[0]
588 588
589 589 i = self.repo.tagslist()
590 590 i.reverse()
591 591
592 592 def entries(notip=False, **map):
593 593 parity = 0
594 594 for k,n in i:
595 595 if notip and k == "tip": continue
596 596 yield {"parity": parity,
597 597 "tag": k,
598 598 "tagmanifest": hex(cl.read(n)[0]),
599 599 "date": cl.read(n)[2],
600 600 "node": hex(n)}
601 601 parity = 1 - parity
602 602
603 603 yield self.t("tags",
604 604 manifest=hex(mf),
605 605 entries=lambda **x: entries(False, **x),
606 606 entriesnotip=lambda **x: entries(True, **x))
607 607
608 608 def summary(self):
609 609 cl = self.repo.changelog
610 610 mf = cl.read(cl.tip())[0]
611 611
612 612 i = self.repo.tagslist()
613 613 i.reverse()
614 614
615 615 def tagentries(**map):
616 616 parity = 0
617 617 count = 0
618 618 for k,n in i:
619 619 if k == "tip": # skip tip
620 620 continue;
621 621
622 622 count += 1
623 623 if count > 10: # limit to 10 tags
624 624 break;
625 625
626 626 c = cl.read(n)
627 627 m = c[0]
628 628 t = c[2]
629 629
630 630 yield self.t("tagentry",
631 631 parity = parity,
632 632 tag = k,
633 633 node = hex(n),
634 634 date = t,
635 635 tagmanifest = hex(m))
636 636 parity = 1 - parity
637 637
638 638 def changelist(**map):
639 639 parity = 0
640 640 cl = self.repo.changelog
641 641 l = [] # build a list in forward order for efficiency
642 642 for i in range(start, end):
643 643 n = cl.node(i)
644 644 changes = cl.read(n)
645 645 hn = hex(n)
646 646 t = changes[2]
647 647
648 648 l.insert(0, self.t(
649 649 'shortlogentry',
650 650 parity = parity,
651 651 author = changes[1],
652 652 manifest = hex(changes[0]),
653 653 desc = changes[4],
654 654 date = t,
655 655 rev = i,
656 656 node = hn))
657 657 parity = 1 - parity
658 658
659 659 yield l
660 660
661 661 cl = self.repo.changelog
662 662 mf = cl.read(cl.tip())[0]
663 663 count = cl.count()
664 664 start = max(0, count - self.maxchanges)
665 665 end = min(count, start + self.maxchanges)
666 666 pos = end - 1
667 667
668 668 yield self.t("summary",
669 669 desc = self.repo.ui.config("web", "description", "unknown"),
670 670 owner = (self.repo.ui.config("ui", "username") or # preferred
671 671 self.repo.ui.config("web", "contact") or # deprecated
672 672 self.repo.ui.config("web", "author", "unknown")), # also
673 673 lastchange = (0, 0), # FIXME
674 674 manifest = hex(mf),
675 675 tags = tagentries,
676 676 shortlog = changelist)
677 677
678 678 def filediff(self, file, changeset):
679 679 cl = self.repo.changelog
680 680 n = self.repo.lookup(changeset)
681 681 changeset = hex(n)
682 682 p1 = cl.parents(n)[0]
683 683 cs = cl.read(n)
684 684 mf = self.repo.manifest.read(cs[0])
685 685
686 686 def diff(**map):
687 687 yield self.diff(p1, n, file)
688 688
689 689 yield self.t("filediff",
690 690 file=file,
691 691 filenode=hex(mf.get(file, nullid)),
692 692 node=changeset,
693 693 rev=self.repo.changelog.rev(n),
694 694 parent=self.siblings(cl.parents(n), cl.rev),
695 695 child=self.siblings(cl.children(n), cl.rev),
696 696 diff=diff)
697 697
698 698 archive_specs = {
699 699 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', 'x-bzip2'),
700 700 'gz': ('application/x-tar', 'tgz', '.tar.gz', 'x-gzip'),
701 701 'zip': ('application/zip', 'zip', '.zip', None),
702 702 }
703 703
704 704 def archive(self, req, cnode, type):
705 705 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
706 706 name = "%s-%s" % (reponame, short(cnode))
707 707 mimetype, artype, extension, encoding = self.archive_specs[type]
708 708 headers = [('Content-type', mimetype),
709 709 ('Content-disposition', 'attachment; filename=%s%s' %
710 710 (name, extension))]
711 711 if encoding:
712 712 headers.append(('Content-encoding', encoding))
713 713 req.header(headers)
714 714 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
715 715
716 716 # add tags to things
717 717 # tags -> list of changesets corresponding to tags
718 718 # find tag, changeset, file
719 719
720 720 def run(self, req=hgrequest()):
721 721 def clean(path):
722 722 p = util.normpath(path)
723 723 if p[:2] == "..":
724 724 raise "suspicious path"
725 725 return p
726 726
727 727 def header(**map):
728 728 yield self.t("header", **map)
729 729
730 730 def footer(**map):
731 731 yield self.t("footer",
732 732 motd=self.repo.ui.config("web", "motd", ""),
733 733 **map)
734 734
735 735 def expand_form(form):
736 736 shortcuts = {
737 737 'cl': [('cmd', ['changelog']), ('rev', None)],
738 738 'cs': [('cmd', ['changeset']), ('node', None)],
739 739 'f': [('cmd', ['file']), ('filenode', None)],
740 740 'fl': [('cmd', ['filelog']), ('filenode', None)],
741 741 'fd': [('cmd', ['filediff']), ('node', None)],
742 742 'fa': [('cmd', ['annotate']), ('filenode', None)],
743 743 'mf': [('cmd', ['manifest']), ('manifest', None)],
744 744 'ca': [('cmd', ['archive']), ('node', None)],
745 745 'tags': [('cmd', ['tags'])],
746 746 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
747 747 'static': [('cmd', ['static']), ('file', None)]
748 748 }
749 749
750 750 for k in shortcuts.iterkeys():
751 751 if form.has_key(k):
752 752 for name, value in shortcuts[k]:
753 753 if value is None:
754 754 value = form[k]
755 755 form[name] = value
756 756 del form[k]
757 757
758 758 self.refresh()
759 759
760 760 expand_form(req.form)
761 761
762 762 t = self.repo.ui.config("web", "templates", templater.templatepath())
763 763 static = self.repo.ui.config("web", "static", os.path.join(t,"static"))
764 764 m = os.path.join(t, "map")
765 765 style = self.repo.ui.config("web", "style", "")
766 766 if req.form.has_key('style'):
767 767 style = req.form['style'][0]
768 768 if style:
769 769 b = os.path.basename("map-" + style)
770 770 p = os.path.join(t, b)
771 771 if os.path.isfile(p):
772 772 m = p
773 773
774 774 port = req.env["SERVER_PORT"]
775 775 port = port != "80" and (":" + port) or ""
776 776 uri = req.env["REQUEST_URI"]
777 777 if "?" in uri:
778 778 uri = uri.split("?")[0]
779 779 url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
780 780 if not self.reponame:
781 781 self.reponame = (self.repo.ui.config("web", "name")
782 782 or uri.strip('/') or self.repo.root)
783 783
784 784 self.t = templater.templater(m, templater.common_filters,
785 785 defaults={"url": url,
786 786 "repo": self.reponame,
787 787 "header": header,
788 788 "footer": footer,
789 789 })
790 790
791 791 if not req.form.has_key('cmd'):
792 792 req.form['cmd'] = [self.t.cache['default'],]
793 793
794 794 cmd = req.form['cmd'][0]
795 795 if cmd == 'changelog':
796 796 hi = self.repo.changelog.count() - 1
797 797 if req.form.has_key('rev'):
798 798 hi = req.form['rev'][0]
799 799 try:
800 800 hi = self.repo.changelog.rev(self.repo.lookup(hi))
801 801 except hg.RepoError:
802 802 req.write(self.search(hi)) # XXX redirect to 404 page?
803 803 return
804 804
805 805 req.write(self.changelog(hi))
806 806
807 807 elif cmd == 'changeset':
808 808 req.write(self.changeset(req.form['node'][0]))
809 809
810 810 elif cmd == 'manifest':
811 811 req.write(self.manifest(req.form['manifest'][0],
812 812 clean(req.form['path'][0])))
813 813
814 814 elif cmd == 'tags':
815 815 req.write(self.tags())
816 816
817 817 elif cmd == 'summary':
818 818 req.write(self.summary())
819 819
820 820 elif cmd == 'filediff':
821 821 req.write(self.filediff(clean(req.form['file'][0]),
822 822 req.form['node'][0]))
823 823
824 824 elif cmd == 'file':
825 825 req.write(self.filerevision(clean(req.form['file'][0]),
826 826 req.form['filenode'][0]))
827 827
828 828 elif cmd == 'annotate':
829 829 req.write(self.fileannotate(clean(req.form['file'][0]),
830 830 req.form['filenode'][0]))
831 831
832 832 elif cmd == 'filelog':
833 833 req.write(self.filelog(clean(req.form['file'][0]),
834 834 req.form['filenode'][0]))
835 835
836 836 elif cmd == 'heads':
837 837 req.httphdr("application/mercurial-0.1")
838 838 h = self.repo.heads()
839 839 req.write(" ".join(map(hex, h)) + "\n")
840 840
841 841 elif cmd == 'branches':
842 842 req.httphdr("application/mercurial-0.1")
843 843 nodes = []
844 844 if req.form.has_key('nodes'):
845 845 nodes = map(bin, req.form['nodes'][0].split(" "))
846 846 for b in self.repo.branches(nodes):
847 847 req.write(" ".join(map(hex, b)) + "\n")
848 848
849 849 elif cmd == 'between':
850 850 req.httphdr("application/mercurial-0.1")
851 851 nodes = []
852 852 if req.form.has_key('pairs'):
853 853 pairs = [map(bin, p.split("-"))
854 854 for p in req.form['pairs'][0].split(" ")]
855 855 for b in self.repo.between(pairs):
856 856 req.write(" ".join(map(hex, b)) + "\n")
857 857
858 858 elif cmd == 'changegroup':
859 859 req.httphdr("application/mercurial-0.1")
860 860 nodes = []
861 861 if not self.allowpull:
862 862 return
863 863
864 864 if req.form.has_key('roots'):
865 865 nodes = map(bin, req.form['roots'][0].split(" "))
866 866
867 867 z = zlib.compressobj()
868 868 f = self.repo.changegroup(nodes, 'serve')
869 869 while 1:
870 870 chunk = f.read(4096)
871 871 if not chunk:
872 872 break
873 873 req.write(z.compress(chunk))
874 874
875 875 req.write(z.flush())
876 876
877 877 elif cmd == 'archive':
878 878 changeset = self.repo.lookup(req.form['node'][0])
879 879 type = req.form['type'][0]
880 880 if (type in self.archives and
881 881 self.repo.ui.configbool("web", "allow" + type, False)):
882 882 self.archive(req, changeset, type)
883 883 return
884 884
885 885 req.write(self.t("error"))
886 886
887 887 elif cmd == 'static':
888 888 fname = req.form['file'][0]
889 889 req.write(staticfile(static, fname)
890 890 or self.t("error", error="%r not found" % fname))
891 891
892 892 else:
893 893 req.write(self.t("error"))
894 894
895 895 def create_server(ui, repo):
896 896 use_threads = True
897 897
898 898 def openlog(opt, default):
899 899 if opt and opt != '-':
900 900 return open(opt, 'w')
901 901 return default
902 902
903 903 address = ui.config("web", "address", "")
904 904 port = int(ui.config("web", "port", 8000))
905 905 use_ipv6 = ui.configbool("web", "ipv6")
906 906 webdir_conf = ui.config("web", "webdir_conf")
907 907 accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout)
908 908 errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr)
909 909
910 910 if use_threads:
911 911 try:
912 912 from threading import activeCount
913 913 except ImportError:
914 914 use_threads = False
915 915
916 916 if use_threads:
917 917 _mixin = SocketServer.ThreadingMixIn
918 918 else:
919 919 if hasattr(os, "fork"):
920 920 _mixin = SocketServer.ForkingMixIn
921 921 else:
922 922 class _mixin: pass
923 923
924 924 class MercurialHTTPServer(_mixin, BaseHTTPServer.HTTPServer):
925 925 pass
926 926
927 927 class IPv6HTTPServer(MercurialHTTPServer):
928 928 address_family = getattr(socket, 'AF_INET6', None)
929 929
930 930 def __init__(self, *args, **kwargs):
931 931 if self.address_family is None:
932 932 raise hg.RepoError(_('IPv6 not available on this system'))
933 933 BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
934 934
935 935 class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler):
936 936
937 937 def log_error(self, format, *args):
938 938 errorlog.write("%s - - [%s] %s\n" % (self.address_string(),
939 939 self.log_date_time_string(),
940 940 format % args))
941 941
942 942 def log_message(self, format, *args):
943 943 accesslog.write("%s - - [%s] %s\n" % (self.address_string(),
944 944 self.log_date_time_string(),
945 945 format % args))
946 946
947 947 def do_POST(self):
948 948 try:
949 949 self.do_hgweb()
950 950 except socket.error, inst:
951 951 if inst[0] != errno.EPIPE:
952 952 raise
953 953
954 954 def do_GET(self):
955 955 self.do_POST()
956 956
957 957 def do_hgweb(self):
958 958 path_info, query = splitURI(self.path)
959 959
960 960 env = {}
961 961 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
962 962 env['REQUEST_METHOD'] = self.command
963 963 env['SERVER_NAME'] = self.server.server_name
964 964 env['SERVER_PORT'] = str(self.server.server_port)
965 965 env['REQUEST_URI'] = "/"
966 966 env['PATH_INFO'] = path_info
967 967 if query:
968 968 env['QUERY_STRING'] = query
969 969 host = self.address_string()
970 970 if host != self.client_address[0]:
971 971 env['REMOTE_HOST'] = host
972 972 env['REMOTE_ADDR'] = self.client_address[0]
973 973
974 974 if self.headers.typeheader is None:
975 975 env['CONTENT_TYPE'] = self.headers.type
976 976 else:
977 977 env['CONTENT_TYPE'] = self.headers.typeheader
978 978 length = self.headers.getheader('content-length')
979 979 if length:
980 980 env['CONTENT_LENGTH'] = length
981 981 accept = []
982 982 for line in self.headers.getallmatchingheaders('accept'):
983 983 if line[:1] in "\t\n\r ":
984 984 accept.append(line.strip())
985 985 else:
986 986 accept = accept + line[7:].split(',')
987 987 env['HTTP_ACCEPT'] = ','.join(accept)
988 988
989 989 req = hgrequest(self.rfile, self.wfile, env)
990 990 self.send_response(200, "Script output follows")
991 991
992 992 if webdir_conf:
993 993 hgwebobj = hgwebdir(webdir_conf)
994 994 elif repo is not None:
995 995 hgwebobj = hgweb(repo.__class__(repo.ui, repo.origroot))
996 996 else:
997 997 raise hg.RepoError(_('no repo found'))
998 998 hgwebobj.run(req)
999 999
1000 1000
1001 1001 if use_ipv6:
1002 1002 return IPv6HTTPServer((address, port), hgwebhandler)
1003 1003 else:
1004 1004 return MercurialHTTPServer((address, port), hgwebhandler)
1005 1005
1006 1006 # This is a stopgap
1007 1007 class hgwebdir(object):
1008 1008 def __init__(self, config):
1009 1009 def cleannames(items):
1010 1010 return [(name.strip(os.sep), path) for name, path in items]
1011 1011
1012 1012 self.motd = ""
1013 1013 if isinstance(config, (list, tuple)):
1014 1014 self.repos = cleannames(config)
1015 1015 elif isinstance(config, dict):
1016 1016 self.repos = cleannames(config.items())
1017 1017 self.repos.sort()
1018 1018 else:
1019 1019 cp = ConfigParser.SafeConfigParser()
1020 1020 cp.read(config)
1021 1021 self.repos = []
1022 1022 if cp.has_section('web') and cp.has_option('web', 'motd'):
1023 1023 self.motd = cp.get('web', 'motd')
1024 1024 if cp.has_section('paths'):
1025 1025 self.repos.extend(cleannames(cp.items('paths')))
1026 1026 if cp.has_section('collections'):
1027 1027 for prefix, root in cp.items('collections'):
1028 1028 for path in util.walkrepos(root):
1029 1029 repo = os.path.normpath(path)
1030 1030 name = repo
1031 1031 if name.startswith(prefix):
1032 1032 name = name[len(prefix):]
1033 1033 self.repos.append((name.lstrip(os.sep), repo))
1034 1034 self.repos.sort()
1035 1035
1036 1036 def run(self, req=hgrequest()):
1037 1037 def header(**map):
1038 1038 yield tmpl("header", **map)
1039 1039
1040 1040 def footer(**map):
1041 1041 yield tmpl("footer", motd=self.motd, **map)
1042 1042
1043 1043 m = os.path.join(templater.templatepath(), "map")
1044 1044 tmpl = templater.templater(m, templater.common_filters,
1045 1045 defaults={"header": header,
1046 1046 "footer": footer})
1047 1047
1048 1048 def archivelist(ui, nodeid, url):
1049 1049 for i in ['zip', 'gz', 'bz2']:
1050 1050 if ui.configbool("web", "allow" + i, False):
1051 1051 yield {"type" : i, "node": nodeid, "url": url}
1052 1052
1053 def entries(**map):
1053 def entries(sortcolumn="", descending=False, **map):
1054 rows = []
1054 1055 parity = 0
1055 1056 for name, path in self.repos:
1056 1057 u = ui.ui()
1057 1058 try:
1058 1059 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
1059 1060 except IOError:
1060 1061 pass
1061 1062 get = u.config
1062 1063
1063 1064 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
1064 1065 .replace("//", "/"))
1065 1066
1066 1067 # update time with local timezone
1067 1068 try:
1068 1069 d = (get_mtime(path), util.makedate()[1])
1069 1070 except OSError:
1070 1071 continue
1071 1072
1072 yield dict(contact=(get("ui", "username") or # preferred
1073 get("web", "contact") or # deprecated
1074 get("web", "author", "unknown")), # also
1075 name=get("web", "name", name),
1073 contact = (get("ui", "username") or # preferred
1074 get("web", "contact") or # deprecated
1075 get("web", "author", "")) # also
1076 description = get("web", "description", "")
1077 name = get("web", "name", name)
1078 row = dict(contact=contact or "unknown",
1079 contact_sort=contact.upper() or "unknown",
1080 name=name,
1081 name_sort=name.upper(),
1076 1082 url=url,
1077 parity=parity,
1078 shortdesc=get("web", "description", "unknown"),
1079 lastupdate=d,
1083 description=description or "unknown",
1084 description_sort=description.upper() or "unknown",
1085 lastchange=d,
1086 lastchange_sort=d[1]-d[0],
1080 1087 archives=archivelist(u, "tip", url))
1081
1082 parity = 1 - parity
1088 if not sortcolumn:
1089 # fast path for unsorted output
1090 row['parity'] = parity
1091 parity = 1 - parity
1092 yield row
1093 else:
1094 rows.append((row["%s_sort" % sortcolumn], row))
1095 if rows:
1096 rows.sort()
1097 if descending:
1098 rows.reverse()
1099 for key, row in rows:
1100 row['parity'] = parity
1101 parity = 1 - parity
1102 yield row
1083 1103
1084 1104 virtual = req.env.get("PATH_INFO", "").strip('/')
1085 1105 if virtual:
1086 1106 real = dict(self.repos).get(virtual)
1087 1107 if real:
1088 1108 try:
1089 1109 hgweb(real).run(req)
1090 1110 except IOError, inst:
1091 1111 req.write(tmpl("error", error=inst.strerror))
1092 1112 except hg.RepoError, inst:
1093 1113 req.write(tmpl("error", error=str(inst)))
1094 1114 else:
1095 1115 req.write(tmpl("notfound", repo=virtual))
1096 1116 else:
1097 1117 if req.form.has_key('static'):
1098 1118 static = os.path.join(templater.templatepath(), "static")
1099 1119 fname = req.form['static'][0]
1100 1120 req.write(staticfile(static, fname)
1101 1121 or tmpl("error", error="%r not found" % fname))
1102 1122 else:
1103 req.write(tmpl("index", entries=entries))
1123 sortable = ["name", "description", "contact", "lastchange"]
1124 sortcolumn = ""
1125 if req.form.has_key('sort'):
1126 sortcolumn = req.form['sort'][0]
1127 descending = sortcolumn.startswith('-')
1128 if descending:
1129 sortcolumn = sortcolumn[1:]
1130 if sortcolumn not in sortable:
1131 sortcolumn = ""
1132
1133 sort = [("sort_%s" % column,
1134 "%s%s" % ((not descending and column == sortcolumn)
1135 and "-" or "", column))
1136 for column in sortable]
1137 req.write(tmpl("index", entries=entries,
1138 sortcolumn=sortcolumn, descending=descending,
1139 **dict(sort)))
@@ -1,19 +1,19 b''
1 1 #header#
2 2 <title>Mercurial repositories index</title>
3 3 </head>
4 4 <body>
5 5
6 6 <h2>Mercurial Repositories</h2>
7 7
8 8 <table>
9 9 <tr>
10 <td>Name</td>
11 <td>Description</td>
12 <td>Contact</td>
13 <td>Last change</td>
10 <td><a href="?sort=#sort_name#">Name</a></td>
11 <td><a href="?sort=#sort_description#">Description</a></td>
12 <td><a href="?sort=#sort_contact#">Contact</a></td>
13 <td><a href="?sort=#sort_lastchange#">Last change</a></td>
14 14 <td>&nbsp;</td>
15 15 <tr>
16 16 #entries%indexentry#
17 17 </table>
18 18
19 19 #footer#
@@ -1,50 +1,50 b''
1 1 default = 'changelog'
2 2 header = header.tmpl
3 3 footer = footer.tmpl
4 4 search = search.tmpl
5 5 changelog = changelog.tmpl
6 6 naventry = '<a href="?cl=#rev#">#label|escape#</a> '
7 7 filedifflink = '<a href="?fd=#node|short#;file=#file|urlescape#">#file|escape#</a> '
8 8 filenodelink = '<a href="?f=#filenode|short#;file=#file|urlescape#">#file|escape#</a> '
9 9 fileellipses = '...'
10 10 changelogentry = changelogentry.tmpl
11 11 searchentry = changelogentry.tmpl
12 12 changeset = changeset.tmpl
13 13 manifest = manifest.tmpl
14 14 manifestdirentry = '<tr class="parity#parity#"><td><tt>drwxr-xr-x</tt>&nbsp;<td><a href="?cmd=manifest;manifest=#manifest#;path=#path|urlescape#">#basename|escape#/</a>'
15 15 manifestfileentry = '<tr class="parity#parity#"><td><tt>#permissions|permissions#</tt>&nbsp;<td><a href="?f=#filenode|short#;file=#file|urlescape#">#basename|escape#</a>'
16 16 filerevision = filerevision.tmpl
17 17 fileannotate = fileannotate.tmpl
18 18 filediff = filediff.tmpl
19 19 filelog = filelog.tmpl
20 20 fileline = '<div class="parity#parity#"><span class="lineno">#linenumber#</span>#line|escape#</div>'
21 21 filelogentry = filelogentry.tmpl
22 22 annotateline = '<tr class="parity#parity#"><td class="annotate"><a href="?cs=#node|short#">#author|obfuscate#@#rev#</a></td><td><pre>#line|escape#</pre></td></tr>'
23 23 difflineplus = '<span class="plusline">#line|escape#</span>'
24 24 difflineminus = '<span class="minusline">#line|escape#</span>'
25 25 difflineat = '<span class="atline">#line|escape#</span>'
26 26 diffline = '#line|escape#'
27 27 changelogparent = '<tr><th class="parent">parent #rev#:</th><td class="parent"><a href="?cs=#node|short#">#node|short#</a></td></tr>'
28 28 changesetparent = '<tr><th class="parent">parent #rev#:</th><td class="parent"><a href="?cs=#node|short#">#node|short#</a></td></tr>'
29 29 filerevparent = '<tr><td class="metatag">parent:</td><td><a href="?f=#node|short#;file=#file|urlescape#">#node|short#</a></td></tr>'
30 30 filerename = '<tr><td class="metatag">parent:</td><td><a href="?f=#node|short#;file=#file|urlescape#">#file|escape#@#node|short#</a></td></tr>'
31 31 filelogrename = '<tr><th>base:</th><td><a href="?f=#node|short#;file=#file|urlescape#">#file|escape#@#node|short#</a></td></tr>'
32 32 fileannotateparent = '<tr><td class="metatag">parent:</td><td><a href="?fa=#filenode|short#;file=#file|urlescape#">#node|short#</a></td></tr>'
33 33 changesetchild = '<tr><th class="child">child #rev#:</th><td class="child"><a href="?cs=#node|short#">#node|short#</a></td></tr>'
34 34 changelogchild = '<tr><th class="child">child #rev#:</th><td class="child"><a href="?cs=#node|short#">#node|short#</a></td></tr>'
35 35 filerevchild = '<tr><td class="metatag">child:</td><td><a href="?f=#node|short#;file=#file|urlescape#">#node|short#</a></td></tr>'
36 36 fileannotatechild = '<tr><td class="metatag">child:</td><td><a href="?fa=#filenode|short#;file=#file|urlescape#">#node|short#</a></td></tr>'
37 37 tags = tags.tmpl
38 38 tagentry = '<li class="tagEntry parity#parity#"><tt class="node">#node#</tt> <a href="?cs=#node|short#">#tag|escape#</a></li>'
39 39 diffblock = '<pre class="parity#parity#">#lines#</pre>'
40 40 changelogtag = '<tr><th class="tag">tag:</th><td class="tag">#tag|escape#</td></tr>'
41 41 changesettag = '<tr><th class="tag">tag:</th><td class="tag">#tag|escape#</td></tr>'
42 42 filediffparent = '<tr><th class="parent">parent #rev#:</th><td class="parent"><a href="?cs=#node|short#">#node|short#</a></td></tr>'
43 43 filelogparent = '<tr><th>parent #rev#:</th><td><a href="?f=#node|short#;file=#file|urlescape#">#node|short#</a></td></tr>'
44 44 filediffchild = '<tr><th class="child">child #rev#:</th><td class="child"><a href="?cs=#node|short#">#node|short#</a></td></tr>'
45 45 filelogchild = '<tr><th>child #rev#:</th><td><a href="?f=#node|short#;file=#file|urlescape#">#node|short#</a></td></tr>'
46 indexentry = '<tr class="parity#parity#"><td><a href="#url#">#name|escape#</a></td><td>#shortdesc#</td><td>#contact|obfuscate#</td><td class="age">#lastupdate|age# ago</td><td class="indexlinks"><a href="#url#?cl=tip;style=rss">RSS</a> #archives%archiveentry#</td></tr>'
46 indexentry = '<tr class="parity#parity#"><td><a href="#url#">#name|escape#</a></td><td>#description#</td><td>#contact|obfuscate#</td><td class="age">#lastchange|age# ago</td><td class="indexlinks"><a href="#url#?cl=tip;style=rss">RSS</a> #archives%archiveentry#</td></tr>'
47 47 index = index.tmpl
48 48 archiveentry = '<a href="#url#?ca=#node|short#;type=#type|urlescape#">#type|escape#</a> '
49 49 notfound = notfound.tmpl
50 50 error = error.tmpl
General Comments 0
You need to be logged in to leave comments. Login now