##// END OF EJS Templates
hgweb: don't blow up on search for unknown keys
mpm@selenic.com -
r166:39624c47 default
parent child Browse files
Show More
@@ -1,642 +1,645 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # hgweb.py - 0.2 - 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # - web interface to a mercurial repository
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 # useful for debugging
10 10 import cgitb
11 11 cgitb.enable()
12 12
13 13 import os, cgi, time, re, difflib, sys, zlib
14 14 from mercurial.hg import *
15 15
16 16 def templatepath():
17 17 for f in "templates/map", "../templates/map":
18 18 p = os.path.join(os.path.dirname(__file__), f)
19 19 if os.path.isfile(p): return p
20 20
21 21 def age(t):
22 22 def plural(t, c):
23 23 if c == 1: return t
24 24 return t + "s"
25 25 def fmt(t, c):
26 26 return "%d %s" % (c, plural(t, c))
27 27
28 28 now = time.time()
29 29 delta = max(1, int(now - t))
30 30
31 31 scales = [["second", 1],
32 32 ["minute", 60],
33 33 ["hour", 3600],
34 34 ["day", 3600 * 24],
35 35 ["week", 3600 * 24 * 7],
36 36 ["month", 3600 * 24 * 30],
37 37 ["year", 3600 * 24 * 365]]
38 38
39 39 scales.reverse()
40 40
41 41 for t, s in scales:
42 42 n = delta / s
43 43 if n >= 1: return fmt(t, n)
44 44
45 45 def nl2br(text):
46 46 return text.replace('\n', '<br/>')
47 47
48 48 def obfuscate(text):
49 49 return ''.join([ '&#%d' % ord(c) for c in text ])
50 50
51 51 def up(p):
52 52 if p[0] != "/": p = "/" + p
53 53 if p[-1] == "/": p = p[:-1]
54 54 up = os.path.dirname(p)
55 55 if up == "/":
56 56 return "/"
57 57 return up + "/"
58 58
59 59 def httphdr(type):
60 60 print 'Content-type: %s\n' % type
61 61
62 62 def write(*things):
63 63 for thing in things:
64 64 if hasattr(thing, "__iter__"):
65 65 for part in thing:
66 66 write(part)
67 67 else:
68 68 sys.stdout.write(str(thing))
69 69
70 70 def template(tmpl, **map):
71 71 while tmpl:
72 72 m = re.search(r"#([a-zA-Z0-9]+)#", tmpl)
73 73 if m:
74 74 yield tmpl[:m.start(0)]
75 75 v = map.get(m.group(1), "")
76 76 yield callable(v) and v() or v
77 77 tmpl = tmpl[m.end(0):]
78 78 else:
79 79 yield tmpl
80 80 return
81 81
82 82 class templater:
83 83 def __init__(self, mapfile):
84 84 self.cache = {}
85 85 self.map = {}
86 86 self.base = os.path.dirname(mapfile)
87 87
88 88 for l in file(mapfile):
89 89 m = re.match(r'(\S+)\s*=\s*"(.*)"$', l)
90 90 if m:
91 91 self.cache[m.group(1)] = m.group(2)
92 92 else:
93 93 m = re.match(r'(\S+)\s*=\s*(\S+)', l)
94 94 if m:
95 95 self.map[m.group(1)] = os.path.join(self.base, m.group(2))
96 96 else:
97 97 raise "unknown map entry '%s'" % l
98 98
99 99 def __call__(self, t, **map):
100 100 try:
101 101 tmpl = self.cache[t]
102 102 except KeyError:
103 103 tmpl = self.cache[t] = file(self.map[t]).read()
104 104 return template(tmpl, **map)
105 105
106 106 class hgweb:
107 107 maxchanges = 20
108 108 maxfiles = 10
109 109
110 110 def __init__(self, path, name, templatemap = ""):
111 111 templatemap = templatemap or templatepath()
112 112
113 113 self.reponame = name
114 114 self.repo = repository(ui(), path)
115 115 self.t = templater(templatemap)
116 116
117 117 def date(self, cs):
118 118 return time.asctime(time.gmtime(float(cs[2].split(' ')[0])))
119 119
120 120 def listfiles(self, files, mf):
121 121 for f in files[:self.maxfiles]:
122 122 yield self.t("filenodelink", node = hex(mf[f]), file = f)
123 123 if len(files) > self.maxfiles:
124 124 yield self.t("fileellipses")
125 125
126 126 def listfilediffs(self, files, changeset):
127 127 for f in files[:self.maxfiles]:
128 128 yield self.t("filedifflink", node = hex(changeset), file = f)
129 129 if len(files) > self.maxfiles:
130 130 yield self.t("fileellipses")
131 131
132 132 def parent(self, t1, node=nullid, rev=-1, **args):
133 133 if node != hex(nullid):
134 134 yield self.t(t1, node = node, rev = rev, **args)
135 135
136 136 def diff(self, node1, node2, files):
137 137 def filterfiles(list, files):
138 138 l = [ x for x in list if x in files ]
139 139
140 140 for f in files:
141 141 if f[-1] != os.sep: f += os.sep
142 142 l += [ x for x in list if x.startswith(f) ]
143 143 return l
144 144
145 145 def prettyprint(diff):
146 146 for l in diff.splitlines(1):
147 147 line = cgi.escape(l)
148 148 if line.startswith('+'):
149 149 yield self.t("difflineplus", line = line)
150 150 elif line.startswith('-'):
151 151 yield self.t("difflineminus", line = line)
152 152 elif line.startswith('@'):
153 153 yield self.t("difflineat", line = line)
154 154 else:
155 155 yield self.t("diffline", line = line)
156 156
157 157 r = self.repo
158 158 cl = r.changelog
159 159 mf = r.manifest
160 160 change1 = cl.read(node1)
161 161 change2 = cl.read(node2)
162 162 mmap1 = mf.read(change1[0])
163 163 mmap2 = mf.read(change2[0])
164 164 date1 = self.date(change1)
165 165 date2 = self.date(change2)
166 166
167 167 c, a, d = r.diffrevs(node1, node2)
168 168 c, a, d = map(lambda x: filterfiles(x, files), (c, a, d))
169 169
170 170 for f in c:
171 171 to = r.file(f).read(mmap1[f])
172 172 tn = r.file(f).read(mmap2[f])
173 173 yield prettyprint(mdiff.unidiff(to, date1, tn, date2, f))
174 174 for f in a:
175 175 to = ""
176 176 tn = r.file(f).read(mmap2[f])
177 177 yield prettyprint(mdiff.unidiff(to, date1, tn, date2, f))
178 178 for f in d:
179 179 to = r.file(f).read(mmap1[f])
180 180 tn = ""
181 181 yield prettyprint(mdiff.unidiff(to, date1, tn, date2, f))
182 182
183 183 def header(self):
184 184 yield self.t("header", repo = self.reponame)
185 185
186 186 def footer(self):
187 187 yield self.t("footer", repo = self.reponame)
188 188
189 189 def changelog(self, pos=None):
190 190 def changenav():
191 191 def seq(factor = 1):
192 192 yield 1 * factor
193 193 yield 2 * factor
194 194 yield 5 * factor
195 195 for f in seq(factor * 10):
196 196 yield f
197 197
198 198 linear = range(0, count - 2, self.maxchanges)[0:8]
199 199
200 200 for i in linear:
201 201 yield self.t("naventry", rev = max(i, 1))
202 202
203 203 for s in seq():
204 204 if s > count - 2: break
205 205 if s > linear[-1]:
206 206 yield self.t("naventry", rev = s)
207 207
208 208 yield self.t("naventry", rev = count - 1)
209 209
210 210 def changelist():
211 211 parity = (start - end) & 1
212 212 cl = self.repo.changelog
213 213 l = [] # build a list in forward order for efficiency
214 214 for i in range(start, end + 1):
215 215 n = cl.node(i)
216 216 changes = cl.read(n)
217 217 hn = hex(n)
218 218 p1, p2 = cl.parents(n)
219 219 t = float(changes[2].split(' ')[0])
220 220
221 221 l.insert(0, self.t(
222 222 'changelogentry',
223 223 parity = parity,
224 224 author = obfuscate(changes[1]),
225 225 shortdesc = cgi.escape(changes[4].splitlines()[0]),
226 226 age = age(t),
227 227 parent1 = self.parent("changelogparent",
228 228 hex(p1), cl.rev(p1)),
229 229 parent2 = self.parent("changelogparent",
230 230 hex(p2), cl.rev(p2)),
231 231 p1 = hex(p1), p2 = hex(p2),
232 232 p1rev = cl.rev(p1), p2rev = cl.rev(p2),
233 233 manifest = hex(changes[0]),
234 234 desc = nl2br(cgi.escape(changes[4])),
235 235 date = time.asctime(time.gmtime(t)),
236 236 files = self.listfilediffs(changes[3], n),
237 237 rev = i,
238 238 node = hn))
239 239 parity = 1 - parity
240 240
241 241 yield l
242 242
243 243 count = self.repo.changelog.count()
244 244 pos = pos or count - 1
245 245 end = min(pos, count - 1)
246 246 start = max(0, pos - self.maxchanges)
247 247 end = min(count - 1, start + self.maxchanges)
248 248
249 249 yield self.t('changelog',
250 250 header = self.header(),
251 251 footer = self.footer(),
252 252 repo = self.reponame,
253 253 changenav = changenav,
254 254 rev = pos, changesets = count, entries = changelist)
255 255
256 256 def changeset(self, nodeid):
257 257 n = bin(nodeid)
258 258 cl = self.repo.changelog
259 259 changes = cl.read(n)
260 260 p1, p2 = cl.parents(n)
261 261 p1rev, p2rev = cl.rev(p1), cl.rev(p2)
262 262 t = float(changes[2].split(' ')[0])
263 263
264 264 files = []
265 265 mf = self.repo.manifest.read(changes[0])
266 266 for f in changes[3]:
267 267 files.append(self.t("filenodelink",
268 268 filenode = hex(mf[f]), file = f))
269 269
270 270 def diff():
271 271 yield self.diff(p1, n, changes[3])
272 272
273 273 yield self.t('changeset',
274 274 header = self.header(),
275 275 footer = self.footer(),
276 276 repo = self.reponame,
277 277 diff = diff,
278 278 rev = cl.rev(n),
279 279 node = nodeid,
280 280 shortdesc = cgi.escape(changes[4].splitlines()[0]),
281 281 parent1 = self.parent("changesetparent",
282 282 hex(p1), cl.rev(p1)),
283 283 parent2 = self.parent("changesetparent",
284 284 hex(p2), cl.rev(p2)),
285 285 p1 = hex(p1), p2 = hex(p2),
286 286 p1rev = cl.rev(p1), p2rev = cl.rev(p2),
287 287 manifest = hex(changes[0]),
288 288 author = obfuscate(changes[1]),
289 289 desc = nl2br(cgi.escape(changes[4])),
290 290 date = time.asctime(time.gmtime(t)),
291 291 files = files)
292 292
293 293 def filelog(self, f, filenode):
294 294 cl = self.repo.changelog
295 295 fl = self.repo.file(f)
296 296 count = fl.count()
297 297
298 298 def entries():
299 299 l = []
300 300 parity = (count - 1) & 1
301 301
302 302 for i in range(count):
303 303
304 304 n = fl.node(i)
305 305 lr = fl.linkrev(n)
306 306 cn = cl.node(lr)
307 307 cs = cl.read(cl.node(lr))
308 308 p1, p2 = fl.parents(n)
309 309 t = float(cs[2].split(' ')[0])
310 310
311 311 l.insert(0, self.t("filelogentry",
312 312 parity = parity,
313 313 filenode = hex(n),
314 314 filerev = i,
315 315 file = f,
316 316 node = hex(cn),
317 317 author = obfuscate(cs[1]),
318 318 age = age(t),
319 319 date = time.asctime(time.gmtime(t)),
320 320 shortdesc = cgi.escape(cs[4].splitlines()[0]),
321 321 p1 = hex(p1), p2 = hex(p2),
322 322 p1rev = fl.rev(p1), p2rev = fl.rev(p2)))
323 323 parity = 1 - parity
324 324
325 325 yield l
326 326
327 327 yield self.t("filelog",
328 328 header = self.header(),
329 329 footer = self.footer(),
330 330 repo = self.reponame,
331 331 file = f,
332 332 filenode = filenode,
333 333 entries = entries)
334 334
335 335 def filerevision(self, f, node):
336 336 fl = self.repo.file(f)
337 337 n = bin(node)
338 338 text = cgi.escape(fl.read(n))
339 339 changerev = fl.linkrev(n)
340 340 cl = self.repo.changelog
341 341 cn = cl.node(changerev)
342 342 cs = cl.read(cn)
343 343 p1, p2 = fl.parents(n)
344 344 t = float(cs[2].split(' ')[0])
345 345 mfn = cs[0]
346 346
347 347 def lines():
348 348 for l, t in enumerate(text.splitlines(1)):
349 349 yield self.t("fileline",
350 350 line = t,
351 351 linenumber = "% 6d" % (l + 1),
352 352 parity = l & 1)
353 353
354 354 yield self.t("filerevision", file = f,
355 355 header = self.header(),
356 356 footer = self.footer(),
357 357 repo = self.reponame,
358 358 filenode = node,
359 359 path = up(f),
360 360 text = lines(),
361 361 rev = changerev,
362 362 node = hex(cn),
363 363 manifest = hex(mfn),
364 364 author = obfuscate(cs[1]),
365 365 age = age(t),
366 366 date = time.asctime(time.gmtime(t)),
367 367 shortdesc = cgi.escape(cs[4].splitlines()[0]),
368 368 parent1 = self.parent("filerevparent",
369 369 hex(p1), fl.rev(p1), file=f),
370 370 parent2 = self.parent("filerevparent",
371 371 hex(p2), fl.rev(p2), file=f),
372 372 p1 = hex(p1), p2 = hex(p2),
373 373 p1rev = fl.rev(p1), p2rev = fl.rev(p2))
374 374
375 375
376 376 def fileannotate(self, f, node):
377 377 bcache = {}
378 378 ncache = {}
379 379 fl = self.repo.file(f)
380 380 n = bin(node)
381 381 changerev = fl.linkrev(n)
382 382
383 383 cl = self.repo.changelog
384 384 cn = cl.node(changerev)
385 385 cs = cl.read(cn)
386 386 p1, p2 = fl.parents(n)
387 387 t = float(cs[2].split(' ')[0])
388 388 mfn = cs[0]
389 389
390 390 def annotate():
391 391 parity = 1
392 392 last = None
393 393 for r, l in fl.annotate(n):
394 394 try:
395 395 cnode = ncache[r]
396 396 except KeyError:
397 397 cnode = ncache[r] = self.repo.changelog.node(r)
398 398
399 399 try:
400 400 name = bcache[r]
401 401 except KeyError:
402 402 cl = self.repo.changelog.read(cnode)
403 403 name = cl[1]
404 404 f = name.find('@')
405 405 if f >= 0:
406 406 name = name[:f]
407 407 bcache[r] = name
408 408
409 409 if last != cnode:
410 410 parity = 1 - parity
411 411 last = cnode
412 412
413 413 yield self.t("annotateline",
414 414 parity = parity,
415 415 node = hex(cnode),
416 416 rev = r,
417 417 author = name,
418 418 file = f,
419 419 line = cgi.escape(l))
420 420
421 421 yield self.t("fileannotate",
422 422 header = self.header(),
423 423 footer = self.footer(),
424 424 repo = self.reponame,
425 425 file = f,
426 426 filenode = node,
427 427 annotate = annotate,
428 428 path = up(f),
429 429 rev = changerev,
430 430 node = hex(cn),
431 431 manifest = hex(mfn),
432 432 author = obfuscate(cs[1]),
433 433 age = age(t),
434 434 date = time.asctime(time.gmtime(t)),
435 435 shortdesc = cgi.escape(cs[4].splitlines()[0]),
436 436 parent1 = self.parent("fileannotateparent",
437 437 hex(p1), fl.rev(p1), file=f),
438 438 parent2 = self.parent("fileannotateparent",
439 439 hex(p2), fl.rev(p2), file=f),
440 440 p1 = hex(p1), p2 = hex(p2),
441 441 p1rev = fl.rev(p1), p2rev = fl.rev(p2))
442 442
443 443 def manifest(self, mnode, path):
444 444 mf = self.repo.manifest.read(bin(mnode))
445 445 rev = self.repo.manifest.rev(bin(mnode))
446 446 node = self.repo.changelog.node(rev)
447 447
448 448 files = {}
449 449
450 450 p = path[1:]
451 451 l = len(p)
452 452
453 453 for f,n in mf.items():
454 454 if f[:l] != p:
455 455 continue
456 456 remain = f[l:]
457 457 if "/" in remain:
458 458 short = remain[:remain.find("/") + 1] # bleah
459 459 files[short] = (f, None)
460 460 else:
461 461 short = os.path.basename(remain)
462 462 files[short] = (f, n)
463 463
464 464 def filelist():
465 465 parity = 0
466 466 fl = files.keys()
467 467 fl.sort()
468 468 for f in fl:
469 469 full, fnode = files[f]
470 470 if fnode:
471 471 yield self.t("manifestfileentry",
472 472 file = full,
473 473 manifest = mnode,
474 474 filenode = hex(fnode),
475 475 parity = parity,
476 476 basename = f)
477 477 else:
478 478 yield self.t("manifestdirentry",
479 479 parity = parity,
480 480 path = os.path.join(path, f),
481 481 manifest = mnode, basename = f[:-1])
482 482 parity = 1 - parity
483 483
484 484 yield self.t("manifest",
485 485 header = self.header(),
486 486 footer = self.footer(),
487 487 repo = self.reponame,
488 488 manifest = mnode,
489 489 rev = rev,
490 490 node = hex(node),
491 491 path = path,
492 492 up = up(path),
493 493 entries = filelist)
494 494
495 495 def filediff(self, file, changeset):
496 496 n = bin(changeset)
497 497 cl = self.repo.changelog
498 498 p1 = cl.parents(n)[0]
499 499 cs = cl.read(n)
500 500 mf = self.repo.manifest.read(cs[0])
501 501
502 502 def diff():
503 503 yield self.diff(p1, n, file)
504 504
505 505 yield self.t("filediff",
506 506 header = self.header(),
507 507 footer = self.footer(),
508 508 repo = self.reponame,
509 509 file = file,
510 510 filenode = hex(mf[file]),
511 511 node = changeset,
512 512 rev = self.repo.changelog.rev(n),
513 513 p1 = hex(p1),
514 514 p1rev = self.repo.changelog.rev(p1),
515 515 diff = diff)
516 516
517 517 # add tags to things
518 518 # tags -> list of changesets corresponding to tags
519 519 # find tag, changeset, file
520 520
521 521 def run(self):
522 522 args = cgi.parse()
523 523
524 524 if not args.has_key('cmd') or args['cmd'][0] == 'changelog':
525 525 hi = self.repo.changelog.count()
526 526 if args.has_key('rev'):
527 527 hi = args['rev'][0]
528 try:
528 529 hi = self.repo.changelog.rev(self.repo.lookup(hi))
530 except KeyError:
531 hi = self.repo.changelog.count()
529 532
530 533 write(self.changelog(hi))
531 534
532 535 elif args['cmd'][0] == 'changeset':
533 536 write(self.changeset(args['node'][0]))
534 537
535 538 elif args['cmd'][0] == 'manifest':
536 539 write(self.manifest(args['manifest'][0], args['path'][0]))
537 540
538 541 elif args['cmd'][0] == 'filediff':
539 542 write(self.filediff(args['file'][0], args['node'][0]))
540 543
541 544 elif args['cmd'][0] == 'file':
542 545 write(self.filerevision(args['file'][0], args['filenode'][0]))
543 546
544 547 elif args['cmd'][0] == 'annotate':
545 548 write(self.fileannotate(args['file'][0], args['filenode'][0]))
546 549
547 550 elif args['cmd'][0] == 'filelog':
548 551 write(self.filelog(args['file'][0], args['filenode'][0]))
549 552
550 553 elif args['cmd'][0] == 'branches':
551 554 httphdr("text/plain")
552 555 nodes = []
553 556 if args.has_key('nodes'):
554 557 nodes = map(bin, args['nodes'][0].split(" "))
555 558 for b in self.repo.branches(nodes):
556 559 sys.stdout.write(" ".join(map(hex, b)) + "\n")
557 560
558 561 elif args['cmd'][0] == 'between':
559 562 httphdr("text/plain")
560 563 nodes = []
561 564 if args.has_key('pairs'):
562 565 pairs = [ map(bin, p.split("-"))
563 566 for p in args['pairs'][0].split(" ") ]
564 567 for b in self.repo.between(pairs):
565 568 sys.stdout.write(" ".join(map(hex, b)) + "\n")
566 569
567 570 elif args['cmd'][0] == 'changegroup':
568 571 httphdr("application/hg-changegroup")
569 572 nodes = []
570 573 if args.has_key('roots'):
571 574 nodes = map(bin, args['roots'][0].split(" "))
572 575
573 576 z = zlib.compressobj()
574 577 for chunk in self.repo.changegroup(nodes):
575 578 sys.stdout.write(z.compress(chunk))
576 579
577 580 sys.stdout.write(z.flush())
578 581
579 582 else:
580 583 write(self.t("error"))
581 584
582 585 def server(path, name, templates, address, port):
583 586
584 587 import BaseHTTPServer
585 588 import sys, os
586 589
587 590 class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler):
588 591 def do_POST(self):
589 592 self.do_hgweb()
590 593
591 594 def do_GET(self):
592 595 self.do_hgweb()
593 596
594 597 def do_hgweb(self):
595 598 query = ""
596 599 p = self.path.find("?")
597 600 if p:
598 601 query = self.path[p + 1:]
599 602 query = query.replace('+', ' ')
600 603
601 604 env = {}
602 605 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
603 606 env['REQUEST_METHOD'] = self.command
604 607 if query:
605 608 env['QUERY_STRING'] = query
606 609 host = self.address_string()
607 610 if host != self.client_address[0]:
608 611 env['REMOTE_HOST'] = host
609 612 env['REMOTE_ADDR'] = self.client_address[0]
610 613
611 614 if self.headers.typeheader is None:
612 615 env['CONTENT_TYPE'] = self.headers.type
613 616 else:
614 617 env['CONTENT_TYPE'] = self.headers.typeheader
615 618 length = self.headers.getheader('content-length')
616 619 if length:
617 620 env['CONTENT_LENGTH'] = length
618 621 accept = []
619 622 for line in self.headers.getallmatchingheaders('accept'):
620 623 if line[:1] in "\t\n\r ":
621 624 accept.append(line.strip())
622 625 else:
623 626 accept = accept + line[7:].split(',')
624 627 env['HTTP_ACCEPT'] = ','.join(accept)
625 628
626 629 os.environ.update(env)
627 630
628 631 save = sys.argv, sys.stdin, sys.stdout, sys.stderr
629 632 try:
630 633 sys.stdin = self.rfile
631 634 sys.stdout = self.wfile
632 635 sys.argv = ["hgweb.py"]
633 636 if '=' not in query:
634 637 sys.argv.append(query)
635 638 self.send_response(200, "Script output follows")
636 639 hg.run()
637 640 finally:
638 641 sys.argv, sys.stdin, sys.stdout, sys.stderr = save
639 642
640 643 hg = hgweb(path, name, templates)
641 644 httpd = BaseHTTPServer.HTTPServer((address, port), hgwebhandler)
642 645 httpd.serve_forever()
General Comments 0
You need to be logged in to leave comments. Login now