##// END OF EJS Templates
hgweb: better error messages
Dirkjan Ochtman -
r6368:2c370f08 default
parent child Browse files
Show More
@@ -1,975 +1,979 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-2007 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, re
10 10 from mercurial.node import hex, nullid, short
11 11 from mercurial.repo import RepoError
12 12 from mercurial import mdiff, ui, hg, util, archival, patch, hook
13 13 from mercurial import revlog, templater, templatefilters, changegroup
14 14 from common import get_mtime, style_map, paritygen, countgen, get_contact
15 15 from common import ErrorResponse
16 16 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
17 17 from request import wsgirequest
18 18 import webcommands, protocol
19 19
20 20 shortcuts = {
21 21 'cl': [('cmd', ['changelog']), ('rev', None)],
22 22 'sl': [('cmd', ['shortlog']), ('rev', None)],
23 23 'cs': [('cmd', ['changeset']), ('node', None)],
24 24 'f': [('cmd', ['file']), ('filenode', None)],
25 25 'fl': [('cmd', ['filelog']), ('filenode', None)],
26 26 'fd': [('cmd', ['filediff']), ('node', None)],
27 27 'fa': [('cmd', ['annotate']), ('filenode', None)],
28 28 'mf': [('cmd', ['manifest']), ('manifest', None)],
29 29 'ca': [('cmd', ['archive']), ('node', None)],
30 30 'tags': [('cmd', ['tags'])],
31 31 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
32 32 'static': [('cmd', ['static']), ('file', None)]
33 33 }
34 34
35 35 def _up(p):
36 36 if p[0] != "/":
37 37 p = "/" + p
38 38 if p[-1] == "/":
39 39 p = p[:-1]
40 40 up = os.path.dirname(p)
41 41 if up == "/":
42 42 return "/"
43 43 return up + "/"
44 44
45 45 def revnavgen(pos, pagelen, limit, nodefunc):
46 46 def seq(factor, limit=None):
47 47 if limit:
48 48 yield limit
49 49 if limit >= 20 and limit <= 40:
50 50 yield 50
51 51 else:
52 52 yield 1 * factor
53 53 yield 3 * factor
54 54 for f in seq(factor * 10):
55 55 yield f
56 56
57 57 def nav(**map):
58 58 l = []
59 59 last = 0
60 60 for f in seq(1, pagelen):
61 61 if f < pagelen or f <= last:
62 62 continue
63 63 if f > limit:
64 64 break
65 65 last = f
66 66 if pos + f < limit:
67 67 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
68 68 if pos - f >= 0:
69 69 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
70 70
71 71 try:
72 72 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
73 73
74 74 for label, node in l:
75 75 yield {"label": label, "node": node}
76 76
77 77 yield {"label": "tip", "node": "tip"}
78 78 except RepoError:
79 79 pass
80 80
81 81 return nav
82 82
83 83 class hgweb(object):
84 84 def __init__(self, repo, name=None):
85 85 if isinstance(repo, str):
86 86 parentui = ui.ui(report_untrusted=False, interactive=False)
87 87 self.repo = hg.repository(parentui, repo)
88 88 else:
89 89 self.repo = repo
90 90
91 91 hook.redirect(True)
92 92 self.mtime = -1
93 93 self.reponame = name
94 94 self.archives = 'zip', 'gz', 'bz2'
95 95 self.stripecount = 1
96 96 self._capabilities = None
97 97 # a repo owner may set web.templates in .hg/hgrc to get any file
98 98 # readable by the user running the CGI script
99 99 self.templatepath = self.config("web", "templates",
100 100 templater.templatepath(),
101 101 untrusted=False)
102 102
103 103 # The CGI scripts are often run by a user different from the repo owner.
104 104 # Trust the settings from the .hg/hgrc files by default.
105 105 def config(self, section, name, default=None, untrusted=True):
106 106 return self.repo.ui.config(section, name, default,
107 107 untrusted=untrusted)
108 108
109 109 def configbool(self, section, name, default=False, untrusted=True):
110 110 return self.repo.ui.configbool(section, name, default,
111 111 untrusted=untrusted)
112 112
113 113 def configlist(self, section, name, default=None, untrusted=True):
114 114 return self.repo.ui.configlist(section, name, default,
115 115 untrusted=untrusted)
116 116
117 117 def refresh(self):
118 118 mtime = get_mtime(self.repo.root)
119 119 if mtime != self.mtime:
120 120 self.mtime = mtime
121 121 self.repo = hg.repository(self.repo.ui, self.repo.root)
122 122 self.maxchanges = int(self.config("web", "maxchanges", 10))
123 123 self.stripecount = int(self.config("web", "stripes", 1))
124 124 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
125 125 self.maxfiles = int(self.config("web", "maxfiles", 10))
126 126 self.allowpull = self.configbool("web", "allowpull", True)
127 127 self.encoding = self.config("web", "encoding", util._encoding)
128 128 self._capabilities = None
129 129
130 130 def capabilities(self):
131 131 if self._capabilities is not None:
132 132 return self._capabilities
133 133 caps = ['lookup', 'changegroupsubset']
134 134 if self.configbool('server', 'uncompressed'):
135 135 caps.append('stream=%d' % self.repo.changelog.version)
136 136 if changegroup.bundlepriority:
137 137 caps.append('unbundle=%s' % ','.join(changegroup.bundlepriority))
138 138 self._capabilities = caps
139 139 return caps
140 140
141 141 def run(self):
142 142 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
143 143 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
144 144 import mercurial.hgweb.wsgicgi as wsgicgi
145 145 wsgicgi.launch(self)
146 146
147 147 def __call__(self, env, respond):
148 148 req = wsgirequest(env, respond)
149 149 self.run_wsgi(req)
150 150 return req
151 151
152 152 def run_wsgi(self, req):
153 153
154 154 self.refresh()
155 155
156 156 # expand form shortcuts
157 157
158 158 for k in shortcuts.iterkeys():
159 159 if k in req.form:
160 160 for name, value in shortcuts[k]:
161 161 if value is None:
162 162 value = req.form[k]
163 163 req.form[name] = value
164 164 del req.form[k]
165 165
166 166 # work with CGI variables to create coherent structure
167 167 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
168 168
169 169 req.url = req.env['SCRIPT_NAME']
170 170 if not req.url.endswith('/'):
171 171 req.url += '/'
172 172 if 'REPO_NAME' in req.env:
173 173 req.url += req.env['REPO_NAME'] + '/'
174 174
175 175 if req.env.get('PATH_INFO'):
176 176 parts = req.env.get('PATH_INFO').strip('/').split('/')
177 177 repo_parts = req.env.get('REPO_NAME', '').split('/')
178 178 if parts[:len(repo_parts)] == repo_parts:
179 179 parts = parts[len(repo_parts):]
180 180 query = '/'.join(parts)
181 181 else:
182 182 query = req.env['QUERY_STRING'].split('&', 1)[0]
183 183 query = query.split(';', 1)[0]
184 184
185 185 # translate user-visible url structure to internal structure
186 186
187 187 args = query.split('/', 2)
188 188 if 'cmd' not in req.form and args and args[0]:
189 189
190 190 cmd = args.pop(0)
191 191 style = cmd.rfind('-')
192 192 if style != -1:
193 193 req.form['style'] = [cmd[:style]]
194 194 cmd = cmd[style+1:]
195 195
196 196 # avoid accepting e.g. style parameter as command
197 197 if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
198 198 req.form['cmd'] = [cmd]
199 199
200 200 if args and args[0]:
201 201 node = args.pop(0)
202 202 req.form['node'] = [node]
203 203 if args:
204 204 req.form['file'] = args
205 205
206 206 if cmd == 'static':
207 207 req.form['file'] = req.form['node']
208 208 elif cmd == 'archive':
209 209 fn = req.form['node'][0]
210 210 for type_, spec in self.archive_specs.iteritems():
211 211 ext = spec[2]
212 212 if fn.endswith(ext):
213 213 req.form['node'] = [fn[:-len(ext)]]
214 214 req.form['type'] = [type_]
215 215
216 216 # process this if it's a protocol request
217 217
218 218 cmd = req.form.get('cmd', [''])[0]
219 219 if cmd in protocol.__all__:
220 220 method = getattr(protocol, cmd)
221 221 method(self, req)
222 222 return
223 223
224 224 # process the web interface request
225 225
226 226 try:
227 227
228 228 tmpl = self.templater(req)
229 229 ctype = tmpl('mimetype', encoding=self.encoding)
230 230 ctype = templater.stringify(ctype)
231 231
232 232 if cmd == '':
233 233 req.form['cmd'] = [tmpl.cache['default']]
234 234 cmd = req.form['cmd'][0]
235 235
236 236 if cmd not in webcommands.__all__:
237 msg = 'No such method: %s' % cmd
237 msg = 'no such method: %s' % cmd
238 238 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
239 239 elif cmd == 'file' and 'raw' in req.form.get('style', []):
240 240 self.ctype = ctype
241 241 content = webcommands.rawfile(self, req, tmpl)
242 242 else:
243 243 content = getattr(webcommands, cmd)(self, req, tmpl)
244 244 req.respond(HTTP_OK, ctype)
245 245
246 246 req.write(content)
247 247 del tmpl
248 248
249 249 except revlog.LookupError, err:
250 250 req.respond(HTTP_NOT_FOUND, ctype)
251 req.write(tmpl('error', error='revision not found: %s' % err.name))
251 if 'manifest' in err.message:
252 msg = str(err)
253 else:
254 msg = 'revision not found: %s' % err.name
255 req.write(tmpl('error', error=msg))
252 256 except (RepoError, revlog.RevlogError), inst:
253 257 req.respond(HTTP_SERVER_ERROR, ctype)
254 258 req.write(tmpl('error', error=str(inst)))
255 259 except ErrorResponse, inst:
256 260 req.respond(inst.code, ctype)
257 261 req.write(tmpl('error', error=inst.message))
258 262
259 263 def templater(self, req):
260 264
261 265 # determine scheme, port and server name
262 266 # this is needed to create absolute urls
263 267
264 268 proto = req.env.get('wsgi.url_scheme')
265 269 if proto == 'https':
266 270 proto = 'https'
267 271 default_port = "443"
268 272 else:
269 273 proto = 'http'
270 274 default_port = "80"
271 275
272 276 port = req.env["SERVER_PORT"]
273 277 port = port != default_port and (":" + port) or ""
274 278 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
275 279 staticurl = self.config("web", "staticurl") or req.url + 'static/'
276 280 if not staticurl.endswith('/'):
277 281 staticurl += '/'
278 282
279 283 # some functions for the templater
280 284
281 285 def header(**map):
282 286 yield tmpl('header', encoding=self.encoding, **map)
283 287
284 288 def footer(**map):
285 289 yield tmpl("footer", **map)
286 290
287 291 def motd(**map):
288 292 yield self.config("web", "motd", "")
289 293
290 294 def sessionvars(**map):
291 295 fields = []
292 296 if 'style' in req.form:
293 297 style = req.form['style'][0]
294 298 if style != self.config('web', 'style', ''):
295 299 fields.append(('style', style))
296 300
297 301 separator = req.url[-1] == '?' and ';' or '?'
298 302 for name, value in fields:
299 303 yield dict(name=name, value=value, separator=separator)
300 304 separator = ';'
301 305
302 306 # figure out which style to use
303 307
304 308 style = self.config("web", "style", "")
305 309 if 'style' in req.form:
306 310 style = req.form['style'][0]
307 311 mapfile = style_map(self.templatepath, style)
308 312
309 313 if not self.reponame:
310 314 self.reponame = (self.config("web", "name")
311 315 or req.env.get('REPO_NAME')
312 316 or req.url.strip('/') or self.repo.root)
313 317
314 318 # create the templater
315 319
316 320 tmpl = templater.templater(mapfile, templatefilters.filters,
317 321 defaults={"url": req.url,
318 322 "staticurl": staticurl,
319 323 "urlbase": urlbase,
320 324 "repo": self.reponame,
321 325 "header": header,
322 326 "footer": footer,
323 327 "motd": motd,
324 328 "sessionvars": sessionvars
325 329 })
326 330 return tmpl
327 331
328 332 def archivelist(self, nodeid):
329 333 allowed = self.configlist("web", "allow_archive")
330 334 for i, spec in self.archive_specs.iteritems():
331 335 if i in allowed or self.configbool("web", "allow" + i):
332 336 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
333 337
334 338 def listfilediffs(self, tmpl, files, changeset):
335 339 for f in files[:self.maxfiles]:
336 340 yield tmpl("filedifflink", node=hex(changeset), file=f)
337 341 if len(files) > self.maxfiles:
338 342 yield tmpl("fileellipses")
339 343
340 344 def siblings(self, siblings=[], hiderev=None, **args):
341 345 siblings = [s for s in siblings if s.node() != nullid]
342 346 if len(siblings) == 1 and siblings[0].rev() == hiderev:
343 347 return
344 348 for s in siblings:
345 349 d = {'node': hex(s.node()), 'rev': s.rev()}
346 350 if hasattr(s, 'path'):
347 351 d['file'] = s.path()
348 352 d.update(args)
349 353 yield d
350 354
351 355 def renamelink(self, fl, node):
352 356 r = fl.renamed(node)
353 357 if r:
354 358 return [dict(file=r[0], node=hex(r[1]))]
355 359 return []
356 360
357 361 def nodetagsdict(self, node):
358 362 return [{"name": i} for i in self.repo.nodetags(node)]
359 363
360 364 def nodebranchdict(self, ctx):
361 365 branches = []
362 366 branch = ctx.branch()
363 367 # If this is an empty repo, ctx.node() == nullid,
364 368 # ctx.branch() == 'default', but branchtags() is
365 369 # an empty dict. Using dict.get avoids a traceback.
366 370 if self.repo.branchtags().get(branch) == ctx.node():
367 371 branches.append({"name": branch})
368 372 return branches
369 373
370 374 def nodeinbranch(self, ctx):
371 375 branches = []
372 376 branch = ctx.branch()
373 377 if branch != 'default' and self.repo.branchtags().get(branch) != ctx.node():
374 378 branches.append({"name": branch})
375 379 return branches
376 380
377 381 def nodebranchnodefault(self, ctx):
378 382 branches = []
379 383 branch = ctx.branch()
380 384 if branch != 'default':
381 385 branches.append({"name": branch})
382 386 return branches
383 387
384 388 def showtag(self, tmpl, t1, node=nullid, **args):
385 389 for t in self.repo.nodetags(node):
386 390 yield tmpl(t1, tag=t, **args)
387 391
388 392 def diff(self, tmpl, node1, node2, files):
389 393 def filterfiles(filters, files):
390 394 l = [x for x in files if x in filters]
391 395
392 396 for t in filters:
393 397 if t and t[-1] != os.sep:
394 398 t += os.sep
395 399 l += [x for x in files if x.startswith(t)]
396 400 return l
397 401
398 402 parity = paritygen(self.stripecount)
399 403 def diffblock(diff, f, fn):
400 404 yield tmpl("diffblock",
401 405 lines=prettyprintlines(diff),
402 406 parity=parity.next(),
403 407 file=f,
404 408 filenode=hex(fn or nullid))
405 409
406 410 blockcount = countgen()
407 411 def prettyprintlines(diff):
408 412 blockno = blockcount.next()
409 413 for lineno, l in enumerate(diff.splitlines(1)):
410 414 if blockno == 0:
411 415 lineno = lineno + 1
412 416 else:
413 417 lineno = "%d.%d" % (blockno, lineno + 1)
414 418 if l.startswith('+'):
415 419 ltype = "difflineplus"
416 420 elif l.startswith('-'):
417 421 ltype = "difflineminus"
418 422 elif l.startswith('@'):
419 423 ltype = "difflineat"
420 424 else:
421 425 ltype = "diffline"
422 426 yield tmpl(ltype,
423 427 line=l,
424 428 lineid="l%s" % lineno,
425 429 linenumber="% 8s" % lineno)
426 430
427 431 r = self.repo
428 432 c1 = r.changectx(node1)
429 433 c2 = r.changectx(node2)
430 434 date1 = util.datestr(c1.date())
431 435 date2 = util.datestr(c2.date())
432 436
433 437 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
434 438 if files:
435 439 modified, added, removed = map(lambda x: filterfiles(files, x),
436 440 (modified, added, removed))
437 441
438 442 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
439 443 for f in modified:
440 444 to = c1.filectx(f).data()
441 445 tn = c2.filectx(f).data()
442 446 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
443 447 opts=diffopts), f, tn)
444 448 for f in added:
445 449 to = None
446 450 tn = c2.filectx(f).data()
447 451 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
448 452 opts=diffopts), f, tn)
449 453 for f in removed:
450 454 to = c1.filectx(f).data()
451 455 tn = None
452 456 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
453 457 opts=diffopts), f, tn)
454 458
455 459 def changelog(self, tmpl, ctx, shortlog=False):
456 460 def changelist(limit=0,**map):
457 461 cl = self.repo.changelog
458 462 l = [] # build a list in forward order for efficiency
459 463 for i in xrange(start, end):
460 464 ctx = self.repo.changectx(i)
461 465 n = ctx.node()
462 466 showtags = self.showtag(tmpl, 'changelogtag', n)
463 467
464 468 l.insert(0, {"parity": parity.next(),
465 469 "author": ctx.user(),
466 470 "parent": self.siblings(ctx.parents(), i - 1),
467 471 "child": self.siblings(ctx.children(), i + 1),
468 472 "changelogtag": showtags,
469 473 "desc": ctx.description(),
470 474 "date": ctx.date(),
471 475 "files": self.listfilediffs(tmpl, ctx.files(), n),
472 476 "rev": i,
473 477 "node": hex(n),
474 478 "tags": self.nodetagsdict(n),
475 479 "inbranch": self.nodeinbranch(ctx),
476 480 "branches": self.nodebranchdict(ctx)})
477 481
478 482 if limit > 0:
479 483 l = l[:limit]
480 484
481 485 for e in l:
482 486 yield e
483 487
484 488 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
485 489 cl = self.repo.changelog
486 490 count = cl.count()
487 491 pos = ctx.rev()
488 492 start = max(0, pos - maxchanges + 1)
489 493 end = min(count, start + maxchanges)
490 494 pos = end - 1
491 495 parity = paritygen(self.stripecount, offset=start-end)
492 496
493 497 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
494 498
495 499 return tmpl(shortlog and 'shortlog' or 'changelog',
496 500 changenav=changenav,
497 501 node=hex(cl.tip()),
498 502 rev=pos, changesets=count,
499 503 entries=lambda **x: changelist(limit=0,**x),
500 504 latestentry=lambda **x: changelist(limit=1,**x),
501 505 archives=self.archivelist("tip"))
502 506
503 507 def search(self, tmpl, query):
504 508
505 509 def changelist(**map):
506 510 cl = self.repo.changelog
507 511 count = 0
508 512 qw = query.lower().split()
509 513
510 514 def revgen():
511 515 for i in xrange(cl.count() - 1, 0, -100):
512 516 l = []
513 517 for j in xrange(max(0, i - 100), i + 1):
514 518 ctx = self.repo.changectx(j)
515 519 l.append(ctx)
516 520 l.reverse()
517 521 for e in l:
518 522 yield e
519 523
520 524 for ctx in revgen():
521 525 miss = 0
522 526 for q in qw:
523 527 if not (q in ctx.user().lower() or
524 528 q in ctx.description().lower() or
525 529 q in " ".join(ctx.files()).lower()):
526 530 miss = 1
527 531 break
528 532 if miss:
529 533 continue
530 534
531 535 count += 1
532 536 n = ctx.node()
533 537 showtags = self.showtag(tmpl, 'changelogtag', n)
534 538
535 539 yield tmpl('searchentry',
536 540 parity=parity.next(),
537 541 author=ctx.user(),
538 542 parent=self.siblings(ctx.parents()),
539 543 child=self.siblings(ctx.children()),
540 544 changelogtag=showtags,
541 545 desc=ctx.description(),
542 546 date=ctx.date(),
543 547 files=self.listfilediffs(tmpl, ctx.files(), n),
544 548 rev=ctx.rev(),
545 549 node=hex(n),
546 550 tags=self.nodetagsdict(n),
547 551 inbranch=self.nodeinbranch(ctx),
548 552 branches=self.nodebranchdict(ctx))
549 553
550 554 if count >= self.maxchanges:
551 555 break
552 556
553 557 cl = self.repo.changelog
554 558 parity = paritygen(self.stripecount)
555 559
556 560 return tmpl('search',
557 561 query=query,
558 562 node=hex(cl.tip()),
559 563 entries=changelist,
560 564 archives=self.archivelist("tip"))
561 565
562 566 def changeset(self, tmpl, ctx):
563 567 n = ctx.node()
564 568 showtags = self.showtag(tmpl, 'changesettag', n)
565 569 parents = ctx.parents()
566 570 p1 = parents[0].node()
567 571
568 572 files = []
569 573 parity = paritygen(self.stripecount)
570 574 for f in ctx.files():
571 575 files.append(tmpl("filenodelink",
572 576 node=hex(n), file=f,
573 577 parity=parity.next()))
574 578
575 579 def diff(**map):
576 580 yield self.diff(tmpl, p1, n, None)
577 581
578 582 return tmpl('changeset',
579 583 diff=diff,
580 584 rev=ctx.rev(),
581 585 node=hex(n),
582 586 parent=self.siblings(parents),
583 587 child=self.siblings(ctx.children()),
584 588 changesettag=showtags,
585 589 author=ctx.user(),
586 590 desc=ctx.description(),
587 591 date=ctx.date(),
588 592 files=files,
589 593 archives=self.archivelist(hex(n)),
590 594 tags=self.nodetagsdict(n),
591 595 branch=self.nodebranchnodefault(ctx),
592 596 inbranch=self.nodeinbranch(ctx),
593 597 branches=self.nodebranchdict(ctx))
594 598
595 599 def filelog(self, tmpl, fctx):
596 600 f = fctx.path()
597 601 fl = fctx.filelog()
598 602 count = fl.count()
599 603 pagelen = self.maxshortchanges
600 604 pos = fctx.filerev()
601 605 start = max(0, pos - pagelen + 1)
602 606 end = min(count, start + pagelen)
603 607 pos = end - 1
604 608 parity = paritygen(self.stripecount, offset=start-end)
605 609
606 610 def entries(limit=0, **map):
607 611 l = []
608 612
609 613 for i in xrange(start, end):
610 614 ctx = fctx.filectx(i)
611 615 n = fl.node(i)
612 616
613 617 l.insert(0, {"parity": parity.next(),
614 618 "filerev": i,
615 619 "file": f,
616 620 "node": hex(ctx.node()),
617 621 "author": ctx.user(),
618 622 "date": ctx.date(),
619 623 "rename": self.renamelink(fl, n),
620 624 "parent": self.siblings(fctx.parents()),
621 625 "child": self.siblings(fctx.children()),
622 626 "desc": ctx.description()})
623 627
624 628 if limit > 0:
625 629 l = l[:limit]
626 630
627 631 for e in l:
628 632 yield e
629 633
630 634 nodefunc = lambda x: fctx.filectx(fileid=x)
631 635 nav = revnavgen(pos, pagelen, count, nodefunc)
632 636 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
633 637 entries=lambda **x: entries(limit=0, **x),
634 638 latestentry=lambda **x: entries(limit=1, **x))
635 639
636 640 def filerevision(self, tmpl, fctx):
637 641 f = fctx.path()
638 642 text = fctx.data()
639 643 fl = fctx.filelog()
640 644 n = fctx.filenode()
641 645 parity = paritygen(self.stripecount)
642 646
643 647 if util.binary(text):
644 648 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
645 649 text = '(binary:%s)' % mt
646 650
647 651 def lines():
648 652 for lineno, t in enumerate(text.splitlines(1)):
649 653 yield {"line": t,
650 654 "lineid": "l%d" % (lineno + 1),
651 655 "linenumber": "% 6d" % (lineno + 1),
652 656 "parity": parity.next()}
653 657
654 658 return tmpl("filerevision",
655 659 file=f,
656 660 path=_up(f),
657 661 text=lines(),
658 662 rev=fctx.rev(),
659 663 node=hex(fctx.node()),
660 664 author=fctx.user(),
661 665 date=fctx.date(),
662 666 desc=fctx.description(),
663 667 branch=self.nodebranchnodefault(fctx),
664 668 parent=self.siblings(fctx.parents()),
665 669 child=self.siblings(fctx.children()),
666 670 rename=self.renamelink(fl, n),
667 671 permissions=fctx.manifest().flags(f))
668 672
669 673 def fileannotate(self, tmpl, fctx):
670 674 f = fctx.path()
671 675 n = fctx.filenode()
672 676 fl = fctx.filelog()
673 677 parity = paritygen(self.stripecount)
674 678
675 679 def annotate(**map):
676 680 last = None
677 681 if util.binary(fctx.data()):
678 682 mt = (mimetypes.guess_type(fctx.path())[0]
679 683 or 'application/octet-stream')
680 684 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
681 685 '(binary:%s)' % mt)])
682 686 else:
683 687 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
684 688 for lineno, ((f, targetline), l) in lines:
685 689 fnode = f.filenode()
686 690 name = self.repo.ui.shortuser(f.user())
687 691
688 692 if last != fnode:
689 693 last = fnode
690 694
691 695 yield {"parity": parity.next(),
692 696 "node": hex(f.node()),
693 697 "rev": f.rev(),
694 698 "author": name,
695 699 "file": f.path(),
696 700 "targetline": targetline,
697 701 "line": l,
698 702 "lineid": "l%d" % (lineno + 1),
699 703 "linenumber": "% 6d" % (lineno + 1)}
700 704
701 705 return tmpl("fileannotate",
702 706 file=f,
703 707 annotate=annotate,
704 708 path=_up(f),
705 709 rev=fctx.rev(),
706 710 node=hex(fctx.node()),
707 711 author=fctx.user(),
708 712 date=fctx.date(),
709 713 desc=fctx.description(),
710 714 rename=self.renamelink(fl, n),
711 715 branch=self.nodebranchnodefault(fctx),
712 716 parent=self.siblings(fctx.parents()),
713 717 child=self.siblings(fctx.children()),
714 718 permissions=fctx.manifest().flags(f))
715 719
716 720 def manifest(self, tmpl, ctx, path):
717 721 mf = ctx.manifest()
718 722 node = ctx.node()
719 723
720 724 files = {}
721 725 parity = paritygen(self.stripecount)
722 726
723 727 if path and path[-1] != "/":
724 728 path += "/"
725 729 l = len(path)
726 730 abspath = "/" + path
727 731
728 732 for f, n in mf.items():
729 733 if f[:l] != path:
730 734 continue
731 735 remain = f[l:]
732 736 if "/" in remain:
733 737 short = remain[:remain.index("/") + 1] # bleah
734 738 files[short] = (f, None)
735 739 else:
736 740 short = os.path.basename(remain)
737 741 files[short] = (f, n)
738 742
739 743 if not files:
740 raise ErrorResponse(HTTP_NOT_FOUND, 'Path not found: ' + path)
744 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
741 745
742 746 def filelist(**map):
743 747 fl = files.keys()
744 748 fl.sort()
745 749 for f in fl:
746 750 full, fnode = files[f]
747 751 if not fnode:
748 752 continue
749 753
750 754 fctx = ctx.filectx(full)
751 755 yield {"file": full,
752 756 "parity": parity.next(),
753 757 "basename": f,
754 758 "date": fctx.changectx().date(),
755 759 "size": fctx.size(),
756 760 "permissions": mf.flags(full)}
757 761
758 762 def dirlist(**map):
759 763 fl = files.keys()
760 764 fl.sort()
761 765 for f in fl:
762 766 full, fnode = files[f]
763 767 if fnode:
764 768 continue
765 769
766 770 yield {"parity": parity.next(),
767 771 "path": "%s%s" % (abspath, f),
768 772 "basename": f[:-1]}
769 773
770 774 return tmpl("manifest",
771 775 rev=ctx.rev(),
772 776 node=hex(node),
773 777 path=abspath,
774 778 up=_up(abspath),
775 779 upparity=parity.next(),
776 780 fentries=filelist,
777 781 dentries=dirlist,
778 782 archives=self.archivelist(hex(node)),
779 783 tags=self.nodetagsdict(node),
780 784 inbranch=self.nodeinbranch(ctx),
781 785 branches=self.nodebranchdict(ctx))
782 786
783 787 def tags(self, tmpl):
784 788 i = self.repo.tagslist()
785 789 i.reverse()
786 790 parity = paritygen(self.stripecount)
787 791
788 792 def entries(notip=False,limit=0, **map):
789 793 count = 0
790 794 for k, n in i:
791 795 if notip and k == "tip":
792 796 continue
793 797 if limit > 0 and count >= limit:
794 798 continue
795 799 count = count + 1
796 800 yield {"parity": parity.next(),
797 801 "tag": k,
798 802 "date": self.repo.changectx(n).date(),
799 803 "node": hex(n)}
800 804
801 805 return tmpl("tags",
802 806 node=hex(self.repo.changelog.tip()),
803 807 entries=lambda **x: entries(False,0, **x),
804 808 entriesnotip=lambda **x: entries(True,0, **x),
805 809 latestentry=lambda **x: entries(True,1, **x))
806 810
807 811 def summary(self, tmpl):
808 812 i = self.repo.tagslist()
809 813 i.reverse()
810 814
811 815 def tagentries(**map):
812 816 parity = paritygen(self.stripecount)
813 817 count = 0
814 818 for k, n in i:
815 819 if k == "tip": # skip tip
816 820 continue;
817 821
818 822 count += 1
819 823 if count > 10: # limit to 10 tags
820 824 break;
821 825
822 826 yield tmpl("tagentry",
823 827 parity=parity.next(),
824 828 tag=k,
825 829 node=hex(n),
826 830 date=self.repo.changectx(n).date())
827 831
828 832
829 833 def branches(**map):
830 834 parity = paritygen(self.stripecount)
831 835
832 836 b = self.repo.branchtags()
833 837 l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()]
834 838 l.sort()
835 839
836 840 for r,n,t in l:
837 841 ctx = self.repo.changectx(n)
838 842
839 843 yield {'parity': parity.next(),
840 844 'branch': t,
841 845 'node': hex(n),
842 846 'date': ctx.date()}
843 847
844 848 def changelist(**map):
845 849 parity = paritygen(self.stripecount, offset=start-end)
846 850 l = [] # build a list in forward order for efficiency
847 851 for i in xrange(start, end):
848 852 ctx = self.repo.changectx(i)
849 853 n = ctx.node()
850 854 hn = hex(n)
851 855
852 856 l.insert(0, tmpl(
853 857 'shortlogentry',
854 858 parity=parity.next(),
855 859 author=ctx.user(),
856 860 desc=ctx.description(),
857 861 date=ctx.date(),
858 862 rev=i,
859 863 node=hn,
860 864 tags=self.nodetagsdict(n),
861 865 inbranch=self.nodeinbranch(ctx),
862 866 branches=self.nodebranchdict(ctx)))
863 867
864 868 yield l
865 869
866 870 cl = self.repo.changelog
867 871 count = cl.count()
868 872 start = max(0, count - self.maxchanges)
869 873 end = min(count, start + self.maxchanges)
870 874
871 875 return tmpl("summary",
872 876 desc=self.config("web", "description", "unknown"),
873 877 owner=get_contact(self.config) or "unknown",
874 878 lastchange=cl.read(cl.tip())[2],
875 879 tags=tagentries,
876 880 branches=branches,
877 881 shortlog=changelist,
878 882 node=hex(cl.tip()),
879 883 archives=self.archivelist("tip"))
880 884
881 885 def filediff(self, tmpl, fctx):
882 886 n = fctx.node()
883 887 path = fctx.path()
884 888 parents = fctx.parents()
885 889 p1 = parents and parents[0].node() or nullid
886 890
887 891 def diff(**map):
888 892 yield self.diff(tmpl, p1, n, [path])
889 893
890 894 return tmpl("filediff",
891 895 file=path,
892 896 node=hex(n),
893 897 rev=fctx.rev(),
894 898 branch=self.nodebranchnodefault(fctx),
895 899 parent=self.siblings(parents),
896 900 child=self.siblings(fctx.children()),
897 901 diff=diff)
898 902
899 903 archive_specs = {
900 904 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
901 905 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
902 906 'zip': ('application/zip', 'zip', '.zip', None),
903 907 }
904 908
905 909 def archive(self, tmpl, req, key, type_):
906 910 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
907 911 cnode = self.repo.lookup(key)
908 912 arch_version = key
909 913 if cnode == key or key == 'tip':
910 914 arch_version = short(cnode)
911 915 name = "%s-%s" % (reponame, arch_version)
912 916 mimetype, artype, extension, encoding = self.archive_specs[type_]
913 917 headers = [
914 918 ('Content-Type', mimetype),
915 919 ('Content-Disposition', 'attachment; filename=%s%s' %
916 920 (name, extension))
917 921 ]
918 922 if encoding:
919 923 headers.append(('Content-Encoding', encoding))
920 924 req.header(headers)
921 925 req.respond(HTTP_OK)
922 926 archival.archive(self.repo, req, cnode, artype, prefix=name)
923 927
924 928 # add tags to things
925 929 # tags -> list of changesets corresponding to tags
926 930 # find tag, changeset, file
927 931
928 932 def cleanpath(self, path):
929 933 path = path.lstrip('/')
930 934 return util.canonpath(self.repo.root, '', path)
931 935
932 936 def changectx(self, req):
933 937 if 'node' in req.form:
934 938 changeid = req.form['node'][0]
935 939 elif 'manifest' in req.form:
936 940 changeid = req.form['manifest'][0]
937 941 else:
938 942 changeid = self.repo.changelog.count() - 1
939 943
940 944 try:
941 945 ctx = self.repo.changectx(changeid)
942 946 except RepoError:
943 947 man = self.repo.manifest
944 948 mn = man.lookup(changeid)
945 949 ctx = self.repo.changectx(man.linkrev(mn))
946 950
947 951 return ctx
948 952
949 953 def filectx(self, req):
950 954 path = self.cleanpath(req.form['file'][0])
951 955 if 'node' in req.form:
952 956 changeid = req.form['node'][0]
953 957 else:
954 958 changeid = req.form['filenode'][0]
955 959 try:
956 960 ctx = self.repo.changectx(changeid)
957 961 fctx = ctx.filectx(path)
958 962 except RepoError:
959 963 fctx = self.repo.filectx(path, fileid=changeid)
960 964
961 965 return fctx
962 966
963 967 def check_perm(self, req, op, default):
964 968 '''check permission for operation based on user auth.
965 969 return true if op allowed, else false.
966 970 default is policy to use if no config given.'''
967 971
968 972 user = req.env.get('REMOTE_USER')
969 973
970 974 deny = self.configlist('web', 'deny_' + op)
971 975 if deny and (not user or deny == ['*'] or user in deny):
972 976 return False
973 977
974 978 allow = self.configlist('web', 'allow_' + op)
975 979 return (allow and (allow == ['*'] or user in allow)) or default
@@ -1,121 +1,127 b''
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 import os, mimetypes
9 9 from mercurial import revlog, util
10 10 from mercurial.repo import RepoError
11 11 from common import staticfile, ErrorResponse, HTTP_OK, HTTP_NOT_FOUND
12 12
13 13 # __all__ is populated with the allowed commands. Be sure to add to it if
14 14 # you're adding a new command, or the new command won't work.
15 15
16 16 __all__ = [
17 17 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
18 18 'manifest', 'tags', 'summary', 'filediff', 'diff', 'annotate', 'filelog',
19 19 'archive', 'static',
20 20 ]
21 21
22 22 def log(web, req, tmpl):
23 23 if 'file' in req.form and req.form['file'][0]:
24 24 return filelog(web, req, tmpl)
25 25 else:
26 26 return changelog(web, req, tmpl)
27 27
28 28 def rawfile(web, req, tmpl):
29 29 path = web.cleanpath(req.form.get('file', [''])[0])
30 30 if not path:
31 31 content = web.manifest(tmpl, web.changectx(req), path)
32 32 req.respond(HTTP_OK, web.ctype)
33 33 return content
34 34
35 35 try:
36 36 fctx = web.filectx(req)
37 except revlog.LookupError:
38 content = web.manifest(tmpl, web.changectx(req), path)
39 req.respond(HTTP_OK, web.ctype)
40 return content
37 except revlog.LookupError, inst:
38 try:
39 content = web.manifest(tmpl, web.changectx(req), path)
40 req.respond(HTTP_OK, web.ctype)
41 return content
42 except ErrorResponse:
43 raise inst
41 44
42 45 path = fctx.path()
43 46 text = fctx.data()
44 47 mt = mimetypes.guess_type(path)[0]
45 48 if mt is None or util.binary(text):
46 49 mt = mt or 'application/octet-stream'
47 50
48 51 req.respond(HTTP_OK, mt, path, len(text))
49 52 return [text]
50 53
51 54 def file(web, req, tmpl):
52 55 path = web.cleanpath(req.form.get('file', [''])[0])
53 56 if path:
54 57 try:
55 58 return web.filerevision(tmpl, web.filectx(req))
56 except revlog.LookupError:
59 except revlog.LookupError, inst:
57 60 pass
58 61
59 return web.manifest(tmpl, web.changectx(req), path)
62 try:
63 return web.manifest(tmpl, web.changectx(req), path)
64 except ErrorResponse:
65 raise inst
60 66
61 67 def changelog(web, req, tmpl, shortlog = False):
62 68 if 'node' in req.form:
63 69 ctx = web.changectx(req)
64 70 else:
65 71 if 'rev' in req.form:
66 72 hi = req.form['rev'][0]
67 73 else:
68 74 hi = web.repo.changelog.count() - 1
69 75 try:
70 76 ctx = web.repo.changectx(hi)
71 77 except RepoError:
72 78 return web.search(tmpl, hi) # XXX redirect to 404 page?
73 79
74 80 return web.changelog(tmpl, ctx, shortlog = shortlog)
75 81
76 82 def shortlog(web, req, tmpl):
77 83 return changelog(web, req, tmpl, shortlog = True)
78 84
79 85 def changeset(web, req, tmpl):
80 86 return web.changeset(tmpl, web.changectx(req))
81 87
82 88 rev = changeset
83 89
84 90 def manifest(web, req, tmpl):
85 91 return web.manifest(tmpl, web.changectx(req),
86 92 web.cleanpath(req.form['path'][0]))
87 93
88 94 def tags(web, req, tmpl):
89 95 return web.tags(tmpl)
90 96
91 97 def summary(web, req, tmpl):
92 98 return web.summary(tmpl)
93 99
94 100 def filediff(web, req, tmpl):
95 101 return web.filediff(tmpl, web.filectx(req))
96 102
97 103 diff = filediff
98 104
99 105 def annotate(web, req, tmpl):
100 106 return web.fileannotate(tmpl, web.filectx(req))
101 107
102 108 def filelog(web, req, tmpl):
103 109 return web.filelog(tmpl, web.filectx(req))
104 110
105 111 def archive(web, req, tmpl):
106 112 type_ = req.form['type'][0]
107 113 allowed = web.configlist("web", "allow_archive")
108 114 if (type_ in web.archives and (type_ in allowed or
109 115 web.configbool("web", "allow" + type_, False))):
110 116 web.archive(tmpl, req, req.form['node'][0], type_)
111 117 return []
112 raise ErrorResponse(HTTP_NOT_FOUND, 'Unsupported archive type: %s' % type_)
118 raise ErrorResponse(HTTP_NOT_FOUND, 'unsupported archive type: %s' % type_)
113 119
114 120 def static(web, req, tmpl):
115 121 fname = req.form['file'][0]
116 122 # a repo owner may set web.static in .hg/hgrc to get any file
117 123 # readable by the user running the CGI script
118 124 static = web.config("web", "static",
119 125 os.path.join(web.templatepath, "static"),
120 126 untrusted=False)
121 127 return [staticfile(static, fname, req)]
@@ -1,42 +1,44 b''
1 1 #!/bin/sh
2 2 # Some tests for hgweb. Tests static files, plain files and different 404's.
3 3
4 4 hg init test
5 5 cd test
6 6 mkdir da
7 7 echo foo > da/foo
8 8 echo foo > foo
9 9 hg ci -Ambase -d '0 0'
10 10 hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
11 11 cat hg.pid >> $DAEMON_PIDS
12 12 echo % manifest
13 13 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/?style=raw')
14 14 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/da?style=raw')
15 15
16 16 echo % plain file
17 17 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?style=raw'
18 18
19 19 echo % should give a 404 - static file that does not exist
20 20 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/bogus'
21 21
22 22 echo % should give a 404 - bad revision
23 23 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/spam/foo?style=raw'
24 24
25 25 echo % should give a 400 - bad command
26 26 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?cmd=spam&style=raw' | sed 's/400.*/400/'
27 27
28 28 echo % should give a 404 - file does not exist
29 29 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/bork?style=raw'
30 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/bork'
31 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/diff/tip/bork?style=raw'
30 32
31 33 echo % stop and restart
32 34 kill `cat hg.pid`
33 35 hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
34 36 cat hg.pid >> $DAEMON_PIDS
35 37 # Test the access/error files are opened in append mode
36 38 python -c "print len(file('access.log').readlines()), 'log lines written'"
37 39
38 40 echo % static file
39 41 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/style-gitweb.css'
40 42
41 43 echo % errors
42 44 cat errors.log
@@ -1,154 +1,189 b''
1 1 adding da/foo
2 2 adding foo
3 3 % manifest
4 4 200 Script output follows
5 5
6 6
7 7 drwxr-xr-x da
8 8 -rw-r--r-- 4 foo
9 9
10 10
11 11 200 Script output follows
12 12
13 13
14 14 -rw-r--r-- 4 foo
15 15
16 16
17 17 % plain file
18 18 200 Script output follows
19 19
20 20 foo
21 21 % should give a 404 - static file that does not exist
22 22 404 Not Found
23 23
24 24 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
25 25 <html>
26 26 <head>
27 27 <link rel="icon" href="/static/hgicon.png" type="image/png">
28 28 <meta name="robots" content="index, nofollow" />
29 29 <link rel="stylesheet" href="/static/style.css" type="text/css" />
30 30
31 31 <title>Mercurial Error</title>
32 32 </head>
33 33 <body>
34 34
35 35 <h2>Mercurial Error</h2>
36 36
37 37 <p>
38 38 An error occurred while processing your request:
39 39 </p>
40 40 <p>
41 41 Not Found
42 42 </p>
43 43
44 44
45 45 <div class="logo">
46 46 <a href="http://www.selenic.com/mercurial/">
47 47 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial"></a>
48 48 </div>
49 49
50 50 </body>
51 51 </html>
52 52
53 53 % should give a 404 - bad revision
54 54 404 Not Found
55 55
56 56
57 57 error: revision not found: spam
58 58 % should give a 400 - bad command
59 59 400
60 60
61 61
62 error: No such method: spam
62 error: no such method: spam
63 63 % should give a 404 - file does not exist
64 64 404 Not Found
65 65
66 66
67 error: Path not found: bork/
67 error: bork@2ef0ac749a14: not found in manifest
68 404 Not Found
69
70 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
71 <html>
72 <head>
73 <link rel="icon" href="/static/hgicon.png" type="image/png">
74 <meta name="robots" content="index, nofollow" />
75 <link rel="stylesheet" href="/static/style.css" type="text/css" />
76
77 <title>Mercurial Error</title>
78 </head>
79 <body>
80
81 <h2>Mercurial Error</h2>
82
83 <p>
84 An error occurred while processing your request:
85 </p>
86 <p>
87 bork@2ef0ac749a14: not found in manifest
88 </p>
89
90
91 <div class="logo">
92 <a href="http://www.selenic.com/mercurial/">
93 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial"></a>
94 </div>
95
96 </body>
97 </html>
98
99 404 Not Found
100
101
102 error: bork@2ef0ac749a14: not found in manifest
68 103 % stop and restart
69 7 log lines written
104 9 log lines written
70 105 % static file
71 106 200 Script output follows
72 107
73 108 body { font-family: sans-serif; font-size: 12px; margin:0px; border:solid #d9d8d1; border-width:1px; margin:10px; }
74 109 a { color:#0000cc; }
75 110 a:hover, a:visited, a:active { color:#880000; }
76 111 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
77 112 div.page_header a:visited { color:#0000cc; }
78 113 div.page_header a:hover { color:#880000; }
79 114 div.page_nav { padding:8px; }
80 115 div.page_nav a:visited { color:#0000cc; }
81 116 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
82 117 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
83 118 div.page_footer_text { float:left; color:#555555; font-style:italic; }
84 119 div.page_body { padding:8px; }
85 120 div.title, a.title {
86 121 display:block; padding:6px 8px;
87 122 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
88 123 }
89 124 a.title:hover { background-color: #d9d8d1; }
90 125 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
91 126 div.log_body { padding:8px 8px 8px 150px; }
92 127 .age { white-space:nowrap; }
93 128 span.age { position:relative; float:left; width:142px; font-style:italic; }
94 129 div.log_link {
95 130 padding:0px 8px;
96 131 font-size:10px; font-family:sans-serif; font-style:normal;
97 132 position:relative; float:left; width:136px;
98 133 }
99 134 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
100 135 a.list { text-decoration:none; color:#000000; }
101 136 a.list:hover { text-decoration:underline; color:#880000; }
102 137 table { padding:8px 4px; }
103 138 th { padding:2px 5px; font-size:12px; text-align:left; }
104 139 tr.light:hover, .parity0:hover { background-color:#edece6; }
105 140 tr.dark, .parity1 { background-color:#f6f6f0; }
106 141 tr.dark:hover, .parity1:hover { background-color:#edece6; }
107 142 td { padding:2px 5px; font-size:12px; vertical-align:top; }
108 143 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
109 144 td.indexlinks { white-space: nowrap; }
110 145 td.indexlinks a {
111 146 padding: 2px 5px; line-height: 10px;
112 147 border: 1px solid;
113 148 color: #ffffff; background-color: #7777bb;
114 149 border-color: #aaaadd #333366 #333366 #aaaadd;
115 150 font-weight: bold; text-align: center; text-decoration: none;
116 151 font-size: 10px;
117 152 }
118 153 td.indexlinks a:hover { background-color: #6666aa; }
119 154 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
120 155 div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
121 156 div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
122 157 div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
123 158 .linenr { color:#999999; text-decoration:none }
124 159 div.rss_logo { float: right; white-space: nowrap; }
125 160 div.rss_logo a {
126 161 padding:3px 6px; line-height:10px;
127 162 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
128 163 color:#ffffff; background-color:#ff6600;
129 164 font-weight:bold; font-family:sans-serif; font-size:10px;
130 165 text-align:center; text-decoration:none;
131 166 }
132 167 div.rss_logo a:hover { background-color:#ee5500; }
133 168 pre { margin: 0; }
134 169 span.logtags span {
135 170 padding: 0px 4px;
136 171 font-size: 10px;
137 172 font-weight: normal;
138 173 border: 1px solid;
139 174 background-color: #ffaaff;
140 175 border-color: #ffccff #ff00ee #ff00ee #ffccff;
141 176 }
142 177 span.logtags span.tagtag {
143 178 background-color: #ffffaa;
144 179 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
145 180 }
146 181 span.logtags span.branchtag {
147 182 background-color: #aaffaa;
148 183 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
149 184 }
150 185 span.logtags span.inbranchtag {
151 186 background-color: #d5dde6;
152 187 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
153 188 }
154 189 % errors
@@ -1,124 +1,124 b''
1 1 adding a
2 2 adding b
3 3 adding c
4 4 % should give a 404 - file does not exist
5 5 404 Not Found
6 6
7 7
8 error: Path not found: bork/
8 error: bork@8580ff50825a: not found in manifest
9 9 % should succeed
10 10 200 Script output follows
11 11
12 12
13 13 /a/
14 14 /b/
15 15
16 16 200 Script output follows
17 17
18 18 a
19 19 200 Script output follows
20 20
21 21 b
22 22 % should give a 404 - repo is not published
23 23 404 Not Found
24 24
25 25
26 26 error: repository c not found
27 27 % should succeed, slashy names
28 28 200 Script output follows
29 29
30 30
31 31 /b/
32 32 /t/a/
33 33
34 34 200 Script output follows
35 35
36 36
37 37 /t/a/
38 38
39 39 200 Script output follows
40 40
41 41
42 42 /t/a/
43 43
44 44 200 Script output follows
45 45
46 46 <?xml version="1.0" encoding="ascii"?>
47 47 <feed xmlns="http://127.0.0.1/2005/Atom">
48 48 <!-- Changelog -->
49 49 <id>http://127.0.0.1/t/a/</id>
50 50 <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
51 51 <link rel="alternate" href="http://127.0.0.1/t/a/"/>
52 52 <title>t/a Changelog</title>
53 53 <updated>1970-01-01T00:00:01+00:00</updated>
54 54
55 55 <entry>
56 56 <title>a</title>
57 57 <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
58 58 <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
59 59 <author>
60 60 <name>test</name>
61 61 <email>&#116;&#101;&#115;&#116;</email>
62 62 </author>
63 63 <updated>1970-01-01T00:00:01+00:00</updated>
64 64 <published>1970-01-01T00:00:01+00:00</published>
65 65 <content type="xhtml">
66 66 <div xmlns="http://127.0.0.1/1999/xhtml">
67 67 <pre xml:space="preserve">a</pre>
68 68 </div>
69 69 </content>
70 70 </entry>
71 71
72 72 </feed>
73 73 200 Script output follows
74 74
75 75 <?xml version="1.0" encoding="ascii"?>
76 76 <feed xmlns="http://127.0.0.1/2005/Atom">
77 77 <!-- Changelog -->
78 78 <id>http://127.0.0.1/t/a/</id>
79 79 <link rel="self" href="http://127.0.0.1/t/a/atom-log"/>
80 80 <link rel="alternate" href="http://127.0.0.1/t/a/"/>
81 81 <title>t/a Changelog</title>
82 82 <updated>1970-01-01T00:00:01+00:00</updated>
83 83
84 84 <entry>
85 85 <title>a</title>
86 86 <id>http://127.0.0.1/mercurial/#changeset-8580ff50825a50c8f716709acdf8de0deddcd6ab</id>
87 87 <link href="http://127.0.0.1/t/a/rev/8580ff50825a50c8f716709acdf8de0deddcd6ab"/>
88 88 <author>
89 89 <name>test</name>
90 90 <email>&#116;&#101;&#115;&#116;</email>
91 91 </author>
92 92 <updated>1970-01-01T00:00:01+00:00</updated>
93 93 <published>1970-01-01T00:00:01+00:00</published>
94 94 <content type="xhtml">
95 95 <div xmlns="http://127.0.0.1/1999/xhtml">
96 96 <pre xml:space="preserve">a</pre>
97 97 </div>
98 98 </content>
99 99 </entry>
100 100
101 101 </feed>
102 102 200 Script output follows
103 103
104 104 a
105 105 % should succeed
106 106 200 Script output follows
107 107
108 108
109 109 /a/
110 110 /b/
111 111 /c/
112 112
113 113 200 Script output follows
114 114
115 115 a
116 116 200 Script output follows
117 117
118 118 b
119 119 200 Script output follows
120 120
121 121 c
122 122 % paths errors 1
123 123 % paths errors 2
124 124 % collections errors
General Comments 0
You need to be logged in to leave comments. Login now