##// END OF EJS Templates
hgweb: Search templates in templatepath/style/map, too, using a common function....
Thomas Arendsen Hein -
r3276:db9d2a62 default
parent child Browse files
Show More
@@ -1,44 +1,61 b''
1 1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os, mimetypes
10 10 import os.path
11 11
12 12 def get_mtime(repo_path):
13 13 hg_path = os.path.join(repo_path, ".hg")
14 14 cl_path = os.path.join(hg_path, "00changelog.i")
15 15 if os.path.exists(os.path.join(cl_path)):
16 16 return os.stat(cl_path).st_mtime
17 17 else:
18 18 return os.stat(hg_path).st_mtime
19 19
20 20 def staticfile(directory, fname, req):
21 21 """return a file inside directory with guessed content-type header
22 22
23 23 fname always uses '/' as directory separator and isn't allowed to
24 24 contain unusual path components.
25 25 Content-type is guessed using the mimetypes module.
26 26 Return an empty string if fname is illegal or file not found.
27 27
28 28 """
29 29 parts = fname.split('/')
30 30 path = directory
31 31 for part in parts:
32 32 if (part in ('', os.curdir, os.pardir) or
33 33 os.sep in part or os.altsep is not None and os.altsep in part):
34 34 return ""
35 35 path = os.path.join(path, part)
36 36 try:
37 37 os.stat(path)
38 38 ct = mimetypes.guess_type(path)[0] or "text/plain"
39 39 req.header([('Content-type', ct),
40 40 ('Content-length', os.path.getsize(path))])
41 41 return file(path, 'rb').read()
42 42 except (TypeError, OSError):
43 43 # illegal fname or unreadable file
44 44 return ""
45
46 def style_map(templatepath, style):
47 """Return path to mapfile for a given style.
48
49 Searches mapfile in the following locations:
50 1. templatepath/style/map
51 2. templatepath/map-style
52 3. templatepath/map
53 """
54 locations = style and [os.path.join(style, "map"), "map-"+style] or []
55 locations.append("map")
56 for location in locations:
57 mapfile = os.path.join(templatepath, location)
58 if os.path.isfile(mapfile):
59 return mapfile
60 raise RuntimeError("No hgweb templates found in %r" % templatepath)
61
@@ -1,1060 +1,1055 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, 2006 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(), 'urllib')
15 15 demandload(globals(), "mercurial:mdiff,ui,hg,util,archival,streamclone,patch")
16 16 demandload(globals(), "mercurial:templater")
17 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
17 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile,style_map")
18 18 from mercurial.node import *
19 19 from mercurial.i18n import gettext as _
20 20
21 21 def _up(p):
22 22 if p[0] != "/":
23 23 p = "/" + p
24 24 if p[-1] == "/":
25 25 p = p[:-1]
26 26 up = os.path.dirname(p)
27 27 if up == "/":
28 28 return "/"
29 29 return up + "/"
30 30
31 31 class hgweb(object):
32 32 def __init__(self, repo, name=None):
33 33 if type(repo) == type(""):
34 34 self.repo = hg.repository(ui.ui(), repo)
35 35 else:
36 36 self.repo = repo
37 37
38 38 self.mtime = -1
39 39 self.reponame = name
40 40 self.archives = 'zip', 'gz', 'bz2'
41 41 self.stripecount = 1
42 42 self.templatepath = self.repo.ui.config("web", "templates",
43 43 templater.templatepath())
44 44
45 45 def refresh(self):
46 46 mtime = get_mtime(self.repo.root)
47 47 if mtime != self.mtime:
48 48 self.mtime = mtime
49 49 self.repo = hg.repository(self.repo.ui, self.repo.root)
50 50 self.maxchanges = int(self.repo.ui.config("web", "maxchanges", 10))
51 51 self.stripecount = int(self.repo.ui.config("web", "stripes", 1))
52 52 self.maxshortchanges = int(self.repo.ui.config("web", "maxshortchanges", 60))
53 53 self.maxfiles = int(self.repo.ui.config("web", "maxfiles", 10))
54 54 self.allowpull = self.repo.ui.configbool("web", "allowpull", True)
55 55
56 56 def archivelist(self, nodeid):
57 57 allowed = self.repo.ui.configlist("web", "allow_archive")
58 58 for i, spec in self.archive_specs.iteritems():
59 59 if i in allowed or self.repo.ui.configbool("web", "allow" + i):
60 60 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
61 61
62 62 def listfiles(self, files, mf):
63 63 for f in files[:self.maxfiles]:
64 64 yield self.t("filenodelink", node=hex(mf[f]), file=f)
65 65 if len(files) > self.maxfiles:
66 66 yield self.t("fileellipses")
67 67
68 68 def listfilediffs(self, files, changeset):
69 69 for f in files[:self.maxfiles]:
70 70 yield self.t("filedifflink", node=hex(changeset), file=f)
71 71 if len(files) > self.maxfiles:
72 72 yield self.t("fileellipses")
73 73
74 74 def siblings(self, siblings=[], hiderev=None, **args):
75 75 siblings = [s for s in siblings if s.node() != nullid]
76 76 if len(siblings) == 1 and siblings[0].rev() == hiderev:
77 77 return
78 78 for s in siblings:
79 79 yield dict(node=hex(s.node()), rev=s.rev(), **args)
80 80
81 81 def renamelink(self, fl, node):
82 82 r = fl.renamed(node)
83 83 if r:
84 84 return [dict(file=r[0], node=hex(r[1]))]
85 85 return []
86 86
87 87 def showtag(self, t1, node=nullid, **args):
88 88 for t in self.repo.nodetags(node):
89 89 yield self.t(t1, tag=t, **args)
90 90
91 91 def diff(self, node1, node2, files):
92 92 def filterfiles(filters, files):
93 93 l = [x for x in files if x in filters]
94 94
95 95 for t in filters:
96 96 if t and t[-1] != os.sep:
97 97 t += os.sep
98 98 l += [x for x in files if x.startswith(t)]
99 99 return l
100 100
101 101 parity = [0]
102 102 def diffblock(diff, f, fn):
103 103 yield self.t("diffblock",
104 104 lines=prettyprintlines(diff),
105 105 parity=parity[0],
106 106 file=f,
107 107 filenode=hex(fn or nullid))
108 108 parity[0] = 1 - parity[0]
109 109
110 110 def prettyprintlines(diff):
111 111 for l in diff.splitlines(1):
112 112 if l.startswith('+'):
113 113 yield self.t("difflineplus", line=l)
114 114 elif l.startswith('-'):
115 115 yield self.t("difflineminus", line=l)
116 116 elif l.startswith('@'):
117 117 yield self.t("difflineat", line=l)
118 118 else:
119 119 yield self.t("diffline", line=l)
120 120
121 121 r = self.repo
122 122 cl = r.changelog
123 123 mf = r.manifest
124 124 change1 = cl.read(node1)
125 125 change2 = cl.read(node2)
126 126 mmap1 = mf.read(change1[0])
127 127 mmap2 = mf.read(change2[0])
128 128 date1 = util.datestr(change1[2])
129 129 date2 = util.datestr(change2[2])
130 130
131 131 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
132 132 if files:
133 133 modified, added, removed = map(lambda x: filterfiles(files, x),
134 134 (modified, added, removed))
135 135
136 136 diffopts = patch.diffopts(self.repo.ui)
137 137 for f in modified:
138 138 to = r.file(f).read(mmap1[f])
139 139 tn = r.file(f).read(mmap2[f])
140 140 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
141 141 opts=diffopts), f, tn)
142 142 for f in added:
143 143 to = None
144 144 tn = r.file(f).read(mmap2[f])
145 145 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
146 146 opts=diffopts), f, tn)
147 147 for f in removed:
148 148 to = r.file(f).read(mmap1[f])
149 149 tn = None
150 150 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
151 151 opts=diffopts), f, tn)
152 152
153 153 def changelog(self, ctx, shortlog=False):
154 154 pos = ctx.rev()
155 155 def changenav(**map):
156 156 def seq(factor, maxchanges=None):
157 157 if maxchanges:
158 158 yield maxchanges
159 159 if maxchanges >= 20 and maxchanges <= 40:
160 160 yield 50
161 161 else:
162 162 yield 1 * factor
163 163 yield 3 * factor
164 164 for f in seq(factor * 10):
165 165 yield f
166 166
167 167 l = []
168 168 last = 0
169 169 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
170 170 for f in seq(1, maxchanges):
171 171 if f < maxchanges or f <= last:
172 172 continue
173 173 if f > count:
174 174 break
175 175 last = f
176 176 r = "%d" % f
177 177 if pos + f < count:
178 178 l.append(("+" + r, pos + f))
179 179 if pos - f >= 0:
180 180 l.insert(0, ("-" + r, pos - f))
181 181
182 182 yield {"rev": 0, "label": "(0)"}
183 183
184 184 for label, rev in l:
185 185 yield {"label": label, "rev": rev}
186 186
187 187 yield {"label": "tip", "rev": "tip"}
188 188
189 189 def changelist(**map):
190 190 parity = (start - end) & 1
191 191 cl = self.repo.changelog
192 192 l = [] # build a list in forward order for efficiency
193 193 for i in range(start, end):
194 194 ctx = self.repo.changectx(i)
195 195 n = ctx.node()
196 196
197 197 l.insert(0, {"parity": parity,
198 198 "author": ctx.user(),
199 199 "parent": self.siblings(ctx.parents(), i - 1),
200 200 "child": self.siblings(ctx.children(), i + 1),
201 201 "changelogtag": self.showtag("changelogtag",n),
202 202 "desc": ctx.description(),
203 203 "date": ctx.date(),
204 204 "files": self.listfilediffs(ctx.files(), n),
205 205 "rev": i,
206 206 "node": hex(n)})
207 207 parity = 1 - parity
208 208
209 209 for e in l:
210 210 yield e
211 211
212 212 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
213 213 cl = self.repo.changelog
214 214 count = cl.count()
215 215 start = max(0, pos - maxchanges + 1)
216 216 end = min(count, start + maxchanges)
217 217 pos = end - 1
218 218
219 219 yield self.t(shortlog and 'shortlog' or 'changelog',
220 220 changenav=changenav,
221 221 node=hex(cl.tip()),
222 222 rev=pos, changesets=count, entries=changelist,
223 223 archives=self.archivelist("tip"))
224 224
225 225 def search(self, query):
226 226
227 227 def changelist(**map):
228 228 cl = self.repo.changelog
229 229 count = 0
230 230 qw = query.lower().split()
231 231
232 232 def revgen():
233 233 for i in range(cl.count() - 1, 0, -100):
234 234 l = []
235 235 for j in range(max(0, i - 100), i):
236 236 ctx = self.repo.changectx(j)
237 237 l.append(ctx)
238 238 l.reverse()
239 239 for e in l:
240 240 yield e
241 241
242 242 for ctx in revgen():
243 243 miss = 0
244 244 for q in qw:
245 245 if not (q in ctx.user().lower() or
246 246 q in ctx.description().lower() or
247 247 q in " ".join(ctx.files()[:20]).lower()):
248 248 miss = 1
249 249 break
250 250 if miss:
251 251 continue
252 252
253 253 count += 1
254 254 n = ctx.node()
255 255
256 256 yield self.t('searchentry',
257 257 parity=self.stripes(count),
258 258 author=ctx.user(),
259 259 parent=self.siblings(ctx.parents()),
260 260 child=self.siblings(ctx.children()),
261 261 changelogtag=self.showtag("changelogtag",n),
262 262 desc=ctx.description(),
263 263 date=ctx.date(),
264 264 files=self.listfilediffs(ctx.files(), n),
265 265 rev=ctx.rev(),
266 266 node=hex(n))
267 267
268 268 if count >= self.maxchanges:
269 269 break
270 270
271 271 cl = self.repo.changelog
272 272
273 273 yield self.t('search',
274 274 query=query,
275 275 node=hex(cl.tip()),
276 276 entries=changelist)
277 277
278 278 def changeset(self, ctx):
279 279 n = ctx.node()
280 280 parents = ctx.parents()
281 281 p1 = parents[0].node()
282 282
283 283 files = []
284 284 parity = 0
285 285 for f in ctx.files():
286 286 files.append(self.t("filenodelink",
287 287 node=hex(n), file=f,
288 288 parity=parity))
289 289 parity = 1 - parity
290 290
291 291 def diff(**map):
292 292 yield self.diff(p1, n, None)
293 293
294 294 yield self.t('changeset',
295 295 diff=diff,
296 296 rev=ctx.rev(),
297 297 node=hex(n),
298 298 parent=self.siblings(parents),
299 299 child=self.siblings(ctx.children()),
300 300 changesettag=self.showtag("changesettag",n),
301 301 author=ctx.user(),
302 302 desc=ctx.description(),
303 303 date=ctx.date(),
304 304 files=files,
305 305 archives=self.archivelist(hex(n)))
306 306
307 307 def filelog(self, fctx):
308 308 f = fctx.path()
309 309 fl = fctx.filelog()
310 310 count = fl.count()
311 311
312 312 def entries(**map):
313 313 l = []
314 314 parity = (count - 1) & 1
315 315
316 316 for i in range(count):
317 317 ctx = fctx.filectx(i)
318 318 n = fl.node(i)
319 319
320 320 l.insert(0, {"parity": parity,
321 321 "filerev": i,
322 322 "file": f,
323 323 "node": hex(ctx.node()),
324 324 "author": ctx.user(),
325 325 "date": ctx.date(),
326 326 "rename": self.renamelink(fl, n),
327 327 "parent": self.siblings(fctx.parents(), file=f),
328 328 "child": self.siblings(fctx.children(), file=f),
329 329 "desc": ctx.description()})
330 330 parity = 1 - parity
331 331
332 332 for e in l:
333 333 yield e
334 334
335 335 yield self.t("filelog", file=f, node=hex(fctx.node()), entries=entries)
336 336
337 337 def filerevision(self, fctx):
338 338 f = fctx.path()
339 339 text = fctx.data()
340 340 fl = fctx.filelog()
341 341 n = fctx.filenode()
342 342
343 343 mt = mimetypes.guess_type(f)[0]
344 344 rawtext = text
345 345 if util.binary(text):
346 346 mt = mt or 'application/octet-stream'
347 347 text = "(binary:%s)" % mt
348 348 mt = mt or 'text/plain'
349 349
350 350 def lines():
351 351 for l, t in enumerate(text.splitlines(1)):
352 352 yield {"line": t,
353 353 "linenumber": "% 6d" % (l + 1),
354 354 "parity": self.stripes(l)}
355 355
356 356 yield self.t("filerevision",
357 357 file=f,
358 358 path=_up(f),
359 359 text=lines(),
360 360 raw=rawtext,
361 361 mimetype=mt,
362 362 rev=fctx.rev(),
363 363 node=hex(fctx.node()),
364 364 author=fctx.user(),
365 365 date=fctx.date(),
366 366 parent=self.siblings(fctx.parents(), file=f),
367 367 child=self.siblings(fctx.children(), file=f),
368 368 rename=self.renamelink(fl, n),
369 369 permissions=fctx.manifest().execf(f))
370 370
371 371 def fileannotate(self, fctx):
372 372 f = fctx.path()
373 373 n = fctx.filenode()
374 374 fl = fctx.filelog()
375 375
376 376 def annotate(**map):
377 377 parity = 0
378 378 last = None
379 379 for f, l in fctx.annotate(follow=True):
380 380 fnode = f.filenode()
381 381 name = self.repo.ui.shortuser(f.user())
382 382
383 383 if last != fnode:
384 384 parity = 1 - parity
385 385 last = fnode
386 386
387 387 yield {"parity": parity,
388 388 "node": hex(f.node()),
389 389 "rev": f.rev(),
390 390 "author": name,
391 391 "file": f.path(),
392 392 "line": l}
393 393
394 394 yield self.t("fileannotate",
395 395 file=f,
396 396 annotate=annotate,
397 397 path=_up(f),
398 398 rev=fctx.rev(),
399 399 node=hex(fctx.node()),
400 400 author=fctx.user(),
401 401 date=fctx.date(),
402 402 rename=self.renamelink(fl, n),
403 403 parent=self.siblings(fctx.parents(), file=f),
404 404 child=self.siblings(fctx.children(), file=f),
405 405 permissions=fctx.manifest().execf(f))
406 406
407 407 def manifest(self, ctx, path):
408 408 mf = ctx.manifest()
409 409 node = ctx.node()
410 410
411 411 files = {}
412 412
413 413 p = path[1:]
414 414 if p and p[-1] != "/":
415 415 p += "/"
416 416 l = len(p)
417 417
418 418 for f,n in mf.items():
419 419 if f[:l] != p:
420 420 continue
421 421 remain = f[l:]
422 422 if "/" in remain:
423 423 short = remain[:remain.index("/") + 1] # bleah
424 424 files[short] = (f, None)
425 425 else:
426 426 short = os.path.basename(remain)
427 427 files[short] = (f, n)
428 428
429 429 def filelist(**map):
430 430 parity = 0
431 431 fl = files.keys()
432 432 fl.sort()
433 433 for f in fl:
434 434 full, fnode = files[f]
435 435 if not fnode:
436 436 continue
437 437
438 438 yield {"file": full,
439 439 "filenode": hex(fnode),
440 440 "parity": self.stripes(parity),
441 441 "basename": f,
442 442 "permissions": mf.execf(full)}
443 443 parity += 1
444 444
445 445 def dirlist(**map):
446 446 parity = 0
447 447 fl = files.keys()
448 448 fl.sort()
449 449 for f in fl:
450 450 full, fnode = files[f]
451 451 if fnode:
452 452 continue
453 453
454 454 yield {"parity": self.stripes(parity),
455 455 "path": os.path.join(path, f),
456 456 "basename": f[:-1]}
457 457 parity += 1
458 458
459 459 yield self.t("manifest",
460 460 rev=ctx.rev(),
461 461 node=hex(node),
462 462 path=path,
463 463 up=_up(path),
464 464 fentries=filelist,
465 465 dentries=dirlist,
466 466 archives=self.archivelist(hex(node)))
467 467
468 468 def tags(self):
469 469 cl = self.repo.changelog
470 470
471 471 i = self.repo.tagslist()
472 472 i.reverse()
473 473
474 474 def entries(notip=False, **map):
475 475 parity = 0
476 476 for k,n in i:
477 477 if notip and k == "tip": continue
478 478 yield {"parity": self.stripes(parity),
479 479 "tag": k,
480 480 "date": cl.read(n)[2],
481 481 "node": hex(n)}
482 482 parity += 1
483 483
484 484 yield self.t("tags",
485 485 node=hex(self.repo.changelog.tip()),
486 486 entries=lambda **x: entries(False, **x),
487 487 entriesnotip=lambda **x: entries(True, **x))
488 488
489 489 def summary(self):
490 490 cl = self.repo.changelog
491 491
492 492 i = self.repo.tagslist()
493 493 i.reverse()
494 494
495 495 def tagentries(**map):
496 496 parity = 0
497 497 count = 0
498 498 for k,n in i:
499 499 if k == "tip": # skip tip
500 500 continue;
501 501
502 502 count += 1
503 503 if count > 10: # limit to 10 tags
504 504 break;
505 505
506 506 c = cl.read(n)
507 507 t = c[2]
508 508
509 509 yield self.t("tagentry",
510 510 parity = self.stripes(parity),
511 511 tag = k,
512 512 node = hex(n),
513 513 date = t)
514 514 parity += 1
515 515
516 516 def changelist(**map):
517 517 parity = 0
518 518 cl = self.repo.changelog
519 519 l = [] # build a list in forward order for efficiency
520 520 for i in range(start, end):
521 521 n = cl.node(i)
522 522 changes = cl.read(n)
523 523 hn = hex(n)
524 524 t = changes[2]
525 525
526 526 l.insert(0, self.t(
527 527 'shortlogentry',
528 528 parity = parity,
529 529 author = changes[1],
530 530 desc = changes[4],
531 531 date = t,
532 532 rev = i,
533 533 node = hn))
534 534 parity = 1 - parity
535 535
536 536 yield l
537 537
538 538 count = cl.count()
539 539 start = max(0, count - self.maxchanges)
540 540 end = min(count, start + self.maxchanges)
541 541
542 542 yield self.t("summary",
543 543 desc = self.repo.ui.config("web", "description", "unknown"),
544 544 owner = (self.repo.ui.config("ui", "username") or # preferred
545 545 self.repo.ui.config("web", "contact") or # deprecated
546 546 self.repo.ui.config("web", "author", "unknown")), # also
547 547 lastchange = (0, 0), # FIXME
548 548 tags = tagentries,
549 549 shortlog = changelist,
550 550 node = hex(self.repo.changelog.tip()),
551 551 archives=self.archivelist("tip"))
552 552
553 553 def filediff(self, fctx):
554 554 n = fctx.node()
555 555 path = fctx.path()
556 556 parents = fctx.changectx().parents()
557 557 p1 = parents[0].node()
558 558
559 559 def diff(**map):
560 560 yield self.diff(p1, n, [path])
561 561
562 562 yield self.t("filediff",
563 563 file=path,
564 564 node=hex(n),
565 565 rev=fctx.rev(),
566 566 parent=self.siblings(parents),
567 567 child=self.siblings(fctx.children()),
568 568 diff=diff)
569 569
570 570 archive_specs = {
571 571 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
572 572 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
573 573 'zip': ('application/zip', 'zip', '.zip', None),
574 574 }
575 575
576 576 def archive(self, req, cnode, type_):
577 577 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
578 578 name = "%s-%s" % (reponame, short(cnode))
579 579 mimetype, artype, extension, encoding = self.archive_specs[type_]
580 580 headers = [('Content-type', mimetype),
581 581 ('Content-disposition', 'attachment; filename=%s%s' %
582 582 (name, extension))]
583 583 if encoding:
584 584 headers.append(('Content-encoding', encoding))
585 585 req.header(headers)
586 586 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
587 587
588 588 # add tags to things
589 589 # tags -> list of changesets corresponding to tags
590 590 # find tag, changeset, file
591 591
592 592 def cleanpath(self, path):
593 593 p = util.normpath(path)
594 594 if p[:2] == "..":
595 595 raise Exception("suspicious path")
596 596 return p
597 597
598 598 def run(self):
599 599 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
600 600 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
601 601 import mercurial.hgweb.wsgicgi as wsgicgi
602 602 from request import wsgiapplication
603 603 def make_web_app():
604 604 return self
605 605 wsgicgi.launch(wsgiapplication(make_web_app))
606 606
607 607 def run_wsgi(self, req):
608 608 def header(**map):
609 609 header_file = cStringIO.StringIO(''.join(self.t("header", **map)))
610 610 msg = mimetools.Message(header_file, 0)
611 611 req.header(msg.items())
612 612 yield header_file.read()
613 613
614 614 def rawfileheader(**map):
615 615 req.header([('Content-type', map['mimetype']),
616 616 ('Content-disposition', 'filename=%s' % map['file']),
617 617 ('Content-length', str(len(map['raw'])))])
618 618 yield ''
619 619
620 620 def footer(**map):
621 621 yield self.t("footer",
622 622 motd=self.repo.ui.config("web", "motd", ""),
623 623 **map)
624 624
625 625 def expand_form(form):
626 626 shortcuts = {
627 627 'cl': [('cmd', ['changelog']), ('rev', None)],
628 628 'sl': [('cmd', ['shortlog']), ('rev', None)],
629 629 'cs': [('cmd', ['changeset']), ('node', None)],
630 630 'f': [('cmd', ['file']), ('filenode', None)],
631 631 'fl': [('cmd', ['filelog']), ('filenode', None)],
632 632 'fd': [('cmd', ['filediff']), ('node', None)],
633 633 'fa': [('cmd', ['annotate']), ('filenode', None)],
634 634 'mf': [('cmd', ['manifest']), ('manifest', None)],
635 635 'ca': [('cmd', ['archive']), ('node', None)],
636 636 'tags': [('cmd', ['tags'])],
637 637 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
638 638 'static': [('cmd', ['static']), ('file', None)]
639 639 }
640 640
641 641 for k in shortcuts.iterkeys():
642 642 if form.has_key(k):
643 643 for name, value in shortcuts[k]:
644 644 if value is None:
645 645 value = form[k]
646 646 form[name] = value
647 647 del form[k]
648 648
649 649 def rewrite_request(req):
650 650 '''translate new web interface to traditional format'''
651 651
652 652 def spliturl(req):
653 653 def firstitem(query):
654 654 return query.split('&', 1)[0].split(';', 1)[0]
655 655
656 656 root = req.env.get('REQUEST_URI', '').split('?', 1)[0]
657 657 pi = req.env.get('PATH_INFO', '')
658 658 if pi:
659 659 root = root[:-len(pi)]
660 660
661 661 if req.env.has_key('REPO_NAME'):
662 662 base = '/' + req.env['REPO_NAME']
663 663 else:
664 664 base = root
665 665
666 666 if pi:
667 667 while pi.startswith('//'):
668 668 pi = pi[1:]
669 669 if pi.startswith(base):
670 670 if len(pi) > len(base):
671 671 base += '/'
672 672 query = pi[len(base):]
673 673 else:
674 674 if req.env.has_key('REPO_NAME'):
675 675 # We are using hgwebdir
676 676 base += '/'
677 677 else:
678 678 base += '?'
679 679 query = firstitem(req.env['QUERY_STRING'])
680 680 else:
681 681 base += '/'
682 682 query = pi[1:]
683 683 else:
684 684 base += '?'
685 685 query = firstitem(req.env['QUERY_STRING'])
686 686
687 687 return (root + base, query)
688 688
689 689 req.url, query = spliturl(req)
690 690
691 691 if req.form.has_key('cmd'):
692 692 # old style
693 693 return
694 694
695 695 args = query.split('/', 2)
696 696 if not args or not args[0]:
697 697 return
698 698
699 699 cmd = args.pop(0)
700 700 style = cmd.rfind('-')
701 701 if style != -1:
702 702 req.form['style'] = [cmd[:style]]
703 703 cmd = cmd[style+1:]
704 704 # avoid accepting e.g. style parameter as command
705 705 if hasattr(self, 'do_' + cmd):
706 706 req.form['cmd'] = [cmd]
707 707
708 708 if args and args[0]:
709 709 node = args.pop(0)
710 710 req.form['node'] = [node]
711 711 if args:
712 712 req.form['file'] = args
713 713
714 714 if cmd == 'static':
715 715 req.form['file'] = req.form['node']
716 716 elif cmd == 'archive':
717 717 fn = req.form['node'][0]
718 718 for type_, spec in self.archive_specs.iteritems():
719 719 ext = spec[2]
720 720 if fn.endswith(ext):
721 721 req.form['node'] = [fn[:-len(ext)]]
722 722 req.form['type'] = [type_]
723 723
724 724 def queryprefix(**map):
725 725 return req.url[-1] == '?' and ';' or '?'
726 726
727 727 def getentries(**map):
728 728 fields = {}
729 729 if req.form.has_key('style'):
730 730 style = req.form['style'][0]
731 731 if style != self.repo.ui.config('web', 'style', ''):
732 732 fields['style'] = style
733 733
734 734 if fields:
735 735 fields = ['%s=%s' % (k, urllib.quote(v))
736 736 for k, v in fields.iteritems()]
737 737 yield '%s%s' % (queryprefix(), ';'.join(fields))
738 738 else:
739 739 yield ''
740 740
741 741 self.refresh()
742 742
743 743 expand_form(req.form)
744 744 rewrite_request(req)
745 745
746 m = os.path.join(self.templatepath, "map")
747 746 style = self.repo.ui.config("web", "style", "")
748 747 if req.form.has_key('style'):
749 748 style = req.form['style'][0]
750 if style:
751 b = os.path.basename("map-" + style)
752 p = os.path.join(self.templatepath, b)
753 if os.path.isfile(p):
754 m = p
749 mapfile = style_map(self.templatepath, style)
755 750
756 751 if not req.url:
757 752 port = req.env["SERVER_PORT"]
758 753 port = port != "80" and (":" + port) or ""
759 754 uri = req.env["REQUEST_URI"]
760 755 if "?" in uri:
761 756 uri = uri.split("?")[0]
762 757 req.url = "http://%s%s%s" % (req.env["SERVER_NAME"], port, uri)
763 758
764 759 if not self.reponame:
765 760 self.reponame = (self.repo.ui.config("web", "name")
766 761 or req.env.get('REPO_NAME')
767 762 or req.url.strip('/') or self.repo.root)
768 763
769 self.t = templater.templater(m, templater.common_filters,
764 self.t = templater.templater(mapfile, templater.common_filters,
770 765 defaults={"url": req.url,
771 766 "repo": self.reponame,
772 767 "header": header,
773 768 "footer": footer,
774 769 "rawfileheader": rawfileheader,
775 770 "queryprefix": queryprefix,
776 771 "getentries": getentries
777 772 })
778 773
779 774 if not req.form.has_key('cmd'):
780 775 req.form['cmd'] = [self.t.cache['default'],]
781 776
782 777 cmd = req.form['cmd'][0]
783 778
784 779 method = getattr(self, 'do_' + cmd, None)
785 780 if method:
786 781 method(req)
787 782 else:
788 783 req.write(self.t("error"))
789 784
790 785 def changectx(self, req):
791 786 if req.form.has_key('node'):
792 787 changeid = req.form['node'][0]
793 788 else:
794 789 changeid = req.form['manifest'][0]
795 790 try:
796 791 ctx = self.repo.changectx(changeid)
797 792 except hg.RepoError:
798 793 man = self.repo.manifest
799 794 mn = man.lookup(changeid)
800 795 ctx = self.repo.changectx(man.linkrev(mn))
801 796
802 797 return ctx
803 798
804 799 def filectx(self, req):
805 800 path = self.cleanpath(req.form['file'][0])
806 801 if req.form.has_key('node'):
807 802 changeid = req.form['node'][0]
808 803 else:
809 804 changeid = req.form['filenode'][0]
810 805 try:
811 806 ctx = self.repo.changectx(changeid)
812 807 fctx = ctx.filectx(path)
813 808 except hg.RepoError:
814 809 fctx = self.repo.filectx(path, fileid=changeid)
815 810
816 811 return fctx
817 812
818 813 def stripes(self, parity):
819 814 "make horizontal stripes for easier reading"
820 815 if self.stripecount:
821 816 return (1 + parity / self.stripecount) & 1
822 817 else:
823 818 return 0
824 819
825 820 def do_log(self, req):
826 821 if req.form.has_key('file') and req.form['file'][0]:
827 822 self.do_filelog(req)
828 823 else:
829 824 self.do_changelog(req)
830 825
831 826 def do_rev(self, req):
832 827 self.do_changeset(req)
833 828
834 829 def do_file(self, req):
835 830 path = req.form.get('file', [''])[0]
836 831 if path:
837 832 try:
838 833 req.write(self.filerevision(self.filectx(req)))
839 834 return
840 835 except hg.RepoError:
841 836 pass
842 837 path = self.cleanpath(path)
843 838
844 839 req.write(self.manifest(self.changectx(req), '/' + path))
845 840
846 841 def do_diff(self, req):
847 842 self.do_filediff(req)
848 843
849 844 def do_changelog(self, req, shortlog = False):
850 845 if req.form.has_key('node'):
851 846 ctx = self.changectx(req)
852 847 else:
853 848 if req.form.has_key('rev'):
854 849 hi = req.form['rev'][0]
855 850 else:
856 851 hi = self.repo.changelog.count() - 1
857 852 try:
858 853 ctx = self.repo.changectx(hi)
859 854 except hg.RepoError:
860 855 req.write(self.search(hi)) # XXX redirect to 404 page?
861 856 return
862 857
863 858 req.write(self.changelog(ctx, shortlog = shortlog))
864 859
865 860 def do_shortlog(self, req):
866 861 self.do_changelog(req, shortlog = True)
867 862
868 863 def do_changeset(self, req):
869 864 ctx = self.repo.changectx(req.form['node'][0])
870 865 req.write(self.changeset(ctx))
871 866
872 867 def do_manifest(self, req):
873 868 req.write(self.manifest(self.changectx(req),
874 869 self.cleanpath(req.form['path'][0])))
875 870
876 871 def do_tags(self, req):
877 872 req.write(self.tags())
878 873
879 874 def do_summary(self, req):
880 875 req.write(self.summary())
881 876
882 877 def do_filediff(self, req):
883 878 req.write(self.filediff(self.filectx(req)))
884 879
885 880 def do_annotate(self, req):
886 881 req.write(self.fileannotate(self.filectx(req)))
887 882
888 883 def do_filelog(self, req):
889 884 req.write(self.filelog(self.filectx(req)))
890 885
891 886 def do_heads(self, req):
892 887 resp = " ".join(map(hex, self.repo.heads())) + "\n"
893 888 req.httphdr("application/mercurial-0.1", length=len(resp))
894 889 req.write(resp)
895 890
896 891 def do_branches(self, req):
897 892 nodes = []
898 893 if req.form.has_key('nodes'):
899 894 nodes = map(bin, req.form['nodes'][0].split(" "))
900 895 resp = cStringIO.StringIO()
901 896 for b in self.repo.branches(nodes):
902 897 resp.write(" ".join(map(hex, b)) + "\n")
903 898 resp = resp.getvalue()
904 899 req.httphdr("application/mercurial-0.1", length=len(resp))
905 900 req.write(resp)
906 901
907 902 def do_between(self, req):
908 903 if req.form.has_key('pairs'):
909 904 pairs = [map(bin, p.split("-"))
910 905 for p in req.form['pairs'][0].split(" ")]
911 906 resp = cStringIO.StringIO()
912 907 for b in self.repo.between(pairs):
913 908 resp.write(" ".join(map(hex, b)) + "\n")
914 909 resp = resp.getvalue()
915 910 req.httphdr("application/mercurial-0.1", length=len(resp))
916 911 req.write(resp)
917 912
918 913 def do_changegroup(self, req):
919 914 req.httphdr("application/mercurial-0.1")
920 915 nodes = []
921 916 if not self.allowpull:
922 917 return
923 918
924 919 if req.form.has_key('roots'):
925 920 nodes = map(bin, req.form['roots'][0].split(" "))
926 921
927 922 z = zlib.compressobj()
928 923 f = self.repo.changegroup(nodes, 'serve')
929 924 while 1:
930 925 chunk = f.read(4096)
931 926 if not chunk:
932 927 break
933 928 req.write(z.compress(chunk))
934 929
935 930 req.write(z.flush())
936 931
937 932 def do_archive(self, req):
938 933 changeset = self.repo.lookup(req.form['node'][0])
939 934 type_ = req.form['type'][0]
940 935 allowed = self.repo.ui.configlist("web", "allow_archive")
941 936 if (type_ in self.archives and (type_ in allowed or
942 937 self.repo.ui.configbool("web", "allow" + type_, False))):
943 938 self.archive(req, changeset, type_)
944 939 return
945 940
946 941 req.write(self.t("error"))
947 942
948 943 def do_static(self, req):
949 944 fname = req.form['file'][0]
950 945 static = self.repo.ui.config("web", "static",
951 946 os.path.join(self.templatepath,
952 947 "static"))
953 948 req.write(staticfile(static, fname, req)
954 949 or self.t("error", error="%r not found" % fname))
955 950
956 951 def do_capabilities(self, req):
957 952 caps = ['unbundle']
958 953 if self.repo.ui.configbool('server', 'uncompressed'):
959 954 caps.append('stream=%d' % self.repo.revlogversion)
960 955 resp = ' '.join(caps)
961 956 req.httphdr("application/mercurial-0.1", length=len(resp))
962 957 req.write(resp)
963 958
964 959 def check_perm(self, req, op, default):
965 960 '''check permission for operation based on user auth.
966 961 return true if op allowed, else false.
967 962 default is policy to use if no config given.'''
968 963
969 964 user = req.env.get('REMOTE_USER')
970 965
971 966 deny = self.repo.ui.configlist('web', 'deny_' + op)
972 967 if deny and (not user or deny == ['*'] or user in deny):
973 968 return False
974 969
975 970 allow = self.repo.ui.configlist('web', 'allow_' + op)
976 971 return (allow and (allow == ['*'] or user in allow)) or default
977 972
978 973 def do_unbundle(self, req):
979 974 def bail(response, headers={}):
980 975 length = int(req.env['CONTENT_LENGTH'])
981 976 for s in util.filechunkiter(req, limit=length):
982 977 # drain incoming bundle, else client will not see
983 978 # response when run outside cgi script
984 979 pass
985 980 req.httphdr("application/mercurial-0.1", headers=headers)
986 981 req.write('0\n')
987 982 req.write(response)
988 983
989 984 # require ssl by default, auth info cannot be sniffed and
990 985 # replayed
991 986 ssl_req = self.repo.ui.configbool('web', 'push_ssl', True)
992 987 if ssl_req:
993 988 if not req.env.get('HTTPS'):
994 989 bail(_('ssl required\n'))
995 990 return
996 991 proto = 'https'
997 992 else:
998 993 proto = 'http'
999 994
1000 995 # do not allow push unless explicitly allowed
1001 996 if not self.check_perm(req, 'push', False):
1002 997 bail(_('push not authorized\n'),
1003 998 headers={'status': '401 Unauthorized'})
1004 999 return
1005 1000
1006 1001 req.httphdr("application/mercurial-0.1")
1007 1002
1008 1003 their_heads = req.form['heads'][0].split(' ')
1009 1004
1010 1005 def check_heads():
1011 1006 heads = map(hex, self.repo.heads())
1012 1007 return their_heads == [hex('force')] or their_heads == heads
1013 1008
1014 1009 # fail early if possible
1015 1010 if not check_heads():
1016 1011 bail(_('unsynced changes\n'))
1017 1012 return
1018 1013
1019 1014 # do not lock repo until all changegroup data is
1020 1015 # streamed. save to temporary file.
1021 1016
1022 1017 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1023 1018 fp = os.fdopen(fd, 'wb+')
1024 1019 try:
1025 1020 length = int(req.env['CONTENT_LENGTH'])
1026 1021 for s in util.filechunkiter(req, limit=length):
1027 1022 fp.write(s)
1028 1023
1029 1024 lock = self.repo.lock()
1030 1025 try:
1031 1026 if not check_heads():
1032 1027 req.write('0\n')
1033 1028 req.write(_('unsynced changes\n'))
1034 1029 return
1035 1030
1036 1031 fp.seek(0)
1037 1032
1038 1033 # send addchangegroup output to client
1039 1034
1040 1035 old_stdout = sys.stdout
1041 1036 sys.stdout = cStringIO.StringIO()
1042 1037
1043 1038 try:
1044 1039 url = 'remote:%s:%s' % (proto,
1045 1040 req.env.get('REMOTE_HOST', ''))
1046 1041 ret = self.repo.addchangegroup(fp, 'serve', url)
1047 1042 finally:
1048 1043 val = sys.stdout.getvalue()
1049 1044 sys.stdout = old_stdout
1050 1045 req.write('%d\n' % ret)
1051 1046 req.write(val)
1052 1047 finally:
1053 1048 lock.release()
1054 1049 finally:
1055 1050 fp.close()
1056 1051 os.unlink(tempname)
1057 1052
1058 1053 def do_stream_out(self, req):
1059 1054 req.httphdr("application/mercurial-0.1")
1060 1055 streamclone.stream_out(self.repo, req)
@@ -1,193 +1,187 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
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 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile")
14 demandload(globals(), "mercurial.hgweb.common:get_mtime,staticfile,style_map")
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 23 self.motd = ""
24 24 self.style = ""
25 25 self.repos_sorted = ('name', False)
26 26 if isinstance(config, (list, tuple)):
27 27 self.repos = cleannames(config)
28 28 self.repos_sorted = ('', False)
29 29 elif isinstance(config, dict):
30 30 self.repos = cleannames(config.items())
31 31 self.repos.sort()
32 32 else:
33 33 cp = ConfigParser.SafeConfigParser()
34 34 cp.read(config)
35 35 self.repos = []
36 36 if cp.has_section('web'):
37 37 if cp.has_option('web', 'motd'):
38 38 self.motd = cp.get('web', 'motd')
39 39 if cp.has_option('web', 'style'):
40 40 self.style = cp.get('web', 'style')
41 41 if cp.has_section('paths'):
42 42 self.repos.extend(cleannames(cp.items('paths')))
43 43 if cp.has_section('collections'):
44 44 for prefix, root in cp.items('collections'):
45 45 for path in util.walkrepos(root):
46 46 repo = os.path.normpath(path)
47 47 name = repo
48 48 if name.startswith(prefix):
49 49 name = name[len(prefix):]
50 50 self.repos.append((name.lstrip(os.sep), repo))
51 51 self.repos.sort()
52 52
53 53 def run(self):
54 54 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
55 55 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
56 56 import mercurial.hgweb.wsgicgi as wsgicgi
57 57 from request import wsgiapplication
58 58 def make_web_app():
59 59 return self
60 60 wsgicgi.launch(wsgiapplication(make_web_app))
61 61
62 62 def run_wsgi(self, req):
63 63 def header(**map):
64 64 header_file = cStringIO.StringIO(''.join(tmpl("header", **map)))
65 65 msg = mimetools.Message(header_file, 0)
66 66 req.header(msg.items())
67 67 yield header_file.read()
68 68
69 69 def footer(**map):
70 70 yield tmpl("footer", motd=self.motd, **map)
71 71
72 m = os.path.join(templater.templatepath(), "map")
73 72 style = self.style
74 73 if req.form.has_key('style'):
75 74 style = req.form['style'][0]
76 if style != "":
77 b = os.path.basename("map-" + style)
78 p = os.path.join(templater.templatepath(), b)
79 if os.path.isfile(p):
80 m = p
81
82 tmpl = templater.templater(m, templater.common_filters,
75 mapfile = style_map(templater.templatepath(), style)
76 tmpl = templater.templater(mapfile, templater.common_filters,
83 77 defaults={"header": header,
84 78 "footer": footer})
85 79
86 80 def archivelist(ui, nodeid, url):
87 81 allowed = ui.configlist("web", "allow_archive")
88 82 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
89 83 if i[0] in allowed or ui.configbool("web", "allow" + i[0]):
90 84 yield {"type" : i[0], "extension": i[1],
91 85 "node": nodeid, "url": url}
92 86
93 87 def entries(sortcolumn="", descending=False, **map):
94 88 rows = []
95 89 parity = 0
96 90 for name, path in self.repos:
97 91 u = ui.ui()
98 92 try:
99 93 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
100 94 except IOError:
101 95 pass
102 96 get = u.config
103 97
104 98 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
105 99 .replace("//", "/")) + '/'
106 100
107 101 # update time with local timezone
108 102 try:
109 103 d = (get_mtime(path), util.makedate()[1])
110 104 except OSError:
111 105 continue
112 106
113 107 contact = (get("ui", "username") or # preferred
114 108 get("web", "contact") or # deprecated
115 109 get("web", "author", "")) # also
116 110 description = get("web", "description", "")
117 111 name = get("web", "name", name)
118 112 row = dict(contact=contact or "unknown",
119 113 contact_sort=contact.upper() or "unknown",
120 114 name=name,
121 115 name_sort=name,
122 116 url=url,
123 117 description=description or "unknown",
124 118 description_sort=description.upper() or "unknown",
125 119 lastchange=d,
126 120 lastchange_sort=d[1]-d[0],
127 121 archives=archivelist(u, "tip", url))
128 122 if (not sortcolumn
129 123 or (sortcolumn, descending) == self.repos_sorted):
130 124 # fast path for unsorted output
131 125 row['parity'] = parity
132 126 parity = 1 - parity
133 127 yield row
134 128 else:
135 129 rows.append((row["%s_sort" % sortcolumn], row))
136 130 if rows:
137 131 rows.sort()
138 132 if descending:
139 133 rows.reverse()
140 134 for key, row in rows:
141 135 row['parity'] = parity
142 136 parity = 1 - parity
143 137 yield row
144 138
145 139 virtual = req.env.get("PATH_INFO", "").strip('/')
146 140 if virtual.startswith('static/'):
147 141 static = os.path.join(templater.templatepath(), 'static')
148 142 fname = virtual[7:]
149 143 req.write(staticfile(static, fname, req) or
150 144 tmpl('error', error='%r not found' % fname))
151 145 elif virtual:
152 146 while virtual:
153 147 real = dict(self.repos).get(virtual)
154 148 if real:
155 149 break
156 150 up = virtual.rfind('/')
157 151 if up < 0:
158 152 break
159 153 virtual = virtual[:up]
160 154 if real:
161 155 req.env['REPO_NAME'] = virtual
162 156 try:
163 157 hgweb(real).run_wsgi(req)
164 158 except IOError, inst:
165 159 req.write(tmpl("error", error=inst.strerror))
166 160 except hg.RepoError, inst:
167 161 req.write(tmpl("error", error=str(inst)))
168 162 else:
169 163 req.write(tmpl("notfound", repo=virtual))
170 164 else:
171 165 if req.form.has_key('static'):
172 166 static = os.path.join(templater.templatepath(), "static")
173 167 fname = req.form['static'][0]
174 168 req.write(staticfile(static, fname, req)
175 169 or tmpl("error", error="%r not found" % fname))
176 170 else:
177 171 sortable = ["name", "description", "contact", "lastchange"]
178 172 sortcolumn, descending = self.repos_sorted
179 173 if req.form.has_key('sort'):
180 174 sortcolumn = req.form['sort'][0]
181 175 descending = sortcolumn.startswith('-')
182 176 if descending:
183 177 sortcolumn = sortcolumn[1:]
184 178 if sortcolumn not in sortable:
185 179 sortcolumn = ""
186 180
187 181 sort = [("sort_%s" % column,
188 182 "%s%s" % ((not descending and column == sortcolumn)
189 183 and "-" or "", column))
190 184 for column in sortable]
191 185 req.write(tmpl("index", entries=entries,
192 186 sortcolumn=sortcolumn, descending=descending,
193 187 **dict(sort)))
General Comments 0
You need to be logged in to leave comments. Login now