##// END OF EJS Templates
hgweb: return meaningful HTTP status codes instead of nonsense
Bryan O'Sullivan -
r5561:22713dce default
parent child Browse files
Show More
@@ -0,0 +1,55
1 #!/bin/sh
2
3 mkdir webdir
4 cd webdir
5
6 hg init a
7 echo a > a/a
8 hg --cwd a ci -Ama -d'1 0'
9
10 hg init b
11 echo b > b/b
12 hg --cwd b ci -Amb -d'2 0'
13
14 hg init c
15 echo c > c/c
16 hg --cwd c ci -Amc -d'3 0'
17 root=`pwd`
18
19 cd ..
20
21 cat > paths.conf <<EOF
22 [paths]
23 a=$root/a
24 b=$root/b
25 EOF
26
27 hg serve -p $HGPORT -d --pid-file=hg.pid --webdir-conf paths.conf \
28 -A access-paths.log -E error-paths.log
29 cat hg.pid >> $DAEMON_PIDS
30
31 echo % should give a 404 - file does not exist
32 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/bork?style=raw'
33
34 echo % should succeed
35 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/?style=raw'
36 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/a/file/tip/a?style=raw'
37 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/b/file/tip/b?style=raw'
38
39 echo % should give a 404 - repo is not published
40 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/c/file/tip/c?style=raw'
41
42 cat > collections.conf <<EOF
43 [collections]
44 $root=$root
45 EOF
46
47 hg serve -p $HGPORT1 -d --pid-file=hg.pid --webdir-conf collections.conf \
48 -A access-collections.log -E error-collections.log
49 cat hg.pid >> $DAEMON_PIDS
50
51 echo % should succeed
52 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/?style=raw'
53 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/a/file/tip/a?style=raw'
54 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/b/file/tip/b?style=raw'
55 "$TESTDIR/get-with-headers.py" localhost:$HGPORT1 '/c/file/tip/c?style=raw'
@@ -0,0 +1,43
1 adding a
2 adding b
3 adding c
4 % should give a 404 - file does not exist
5 404 Not Found
6
7
8 error: Path not found: bork/
9 % should succeed
10 200 Script output follows
11
12
13 /a/
14 /b/
15
16 200 Script output follows
17
18 a
19 200 Script output follows
20
21 b
22 % should give a 404 - repo is not published
23 404 Not Found
24
25
26 error: repository c not found
27 % should succeed
28 200 Script output follows
29
30
31 /a/
32 /b/
33 /c/
34
35 200 Script output follows
36
37 a
38 200 Script output follows
39
40 b
41 200 Script output follows
42
43 c
@@ -1,78 +1,92
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 import os, mimetypes
9 import errno, mimetypes, os
10
11 class ErrorResponse(Exception):
12 def __init__(self, code, message=None):
13 Exception.__init__(self)
14 self.code = code
15 if message is None:
16 from httplib import responses
17 self.message = responses.get(code, 'Error')
18 else:
19 self.message = message
10 20
11 21 def get_mtime(repo_path):
12 22 store_path = os.path.join(repo_path, ".hg")
13 23 if not os.path.isdir(os.path.join(store_path, "data")):
14 24 store_path = os.path.join(store_path, "store")
15 25 cl_path = os.path.join(store_path, "00changelog.i")
16 26 if os.path.exists(cl_path):
17 27 return os.stat(cl_path).st_mtime
18 28 else:
19 29 return os.stat(store_path).st_mtime
20 30
21 31 def staticfile(directory, fname, req):
22 32 """return a file inside directory with guessed content-type header
23 33
24 34 fname always uses '/' as directory separator and isn't allowed to
25 35 contain unusual path components.
26 36 Content-type is guessed using the mimetypes module.
27 37 Return an empty string if fname is illegal or file not found.
28 38
29 39 """
30 40 parts = fname.split('/')
31 41 path = directory
32 42 for part in parts:
33 43 if (part in ('', os.curdir, os.pardir) or
34 44 os.sep in part or os.altsep is not None and os.altsep in part):
35 45 return ""
36 46 path = os.path.join(path, part)
37 47 try:
38 48 os.stat(path)
39 49 ct = mimetypes.guess_type(path)[0] or "text/plain"
40 50 req.header([('Content-type', ct),
41 51 ('Content-length', str(os.path.getsize(path)))])
42 52 return file(path, 'rb').read()
43 except (TypeError, OSError):
44 # illegal fname or unreadable file
45 return ""
53 except TypeError:
54 raise ErrorResponse(500, 'illegal file name')
55 except OSError, err:
56 if err.errno == errno.ENOENT:
57 raise ErrorResponse(404)
58 else:
59 raise ErrorResponse(500, err.strerror)
46 60
47 61 def style_map(templatepath, style):
48 62 """Return path to mapfile for a given style.
49 63
50 64 Searches mapfile in the following locations:
51 65 1. templatepath/style/map
52 66 2. templatepath/map-style
53 67 3. templatepath/map
54 68 """
55 69 locations = style and [os.path.join(style, "map"), "map-"+style] or []
56 70 locations.append("map")
57 71 for location in locations:
58 72 mapfile = os.path.join(templatepath, location)
59 73 if os.path.isfile(mapfile):
60 74 return mapfile
61 75 raise RuntimeError("No hgweb templates found in %r" % templatepath)
62 76
63 77 def paritygen(stripecount, offset=0):
64 78 """count parity of horizontal stripes for easier reading"""
65 79 if stripecount and offset:
66 80 # account for offset, e.g. due to building the list in reverse
67 81 count = (stripecount + offset) % stripecount
68 82 parity = (stripecount + offset) / stripecount & 1
69 83 else:
70 84 count = 0
71 85 parity = 0
72 86 while True:
73 87 yield parity
74 88 count += 1
75 89 if stripecount and count >= stripecount:
76 90 parity = 1 - parity
77 91 count = 0
78 92
@@ -1,1208 +1,1221
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 import os, mimetypes, re, zlib, mimetools, cStringIO, sys
9 import errno, os, mimetypes, re, zlib, mimetools, cStringIO, sys
10 10 import tempfile, urllib, bz2
11 11 from mercurial.node import *
12 12 from mercurial.i18n import gettext as _
13 13 from mercurial import mdiff, ui, hg, util, archival, streamclone, patch
14 14 from mercurial import revlog, templater
15 from common import get_mtime, staticfile, style_map, paritygen
15 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen
16 16
17 17 def _up(p):
18 18 if p[0] != "/":
19 19 p = "/" + p
20 20 if p[-1] == "/":
21 21 p = p[:-1]
22 22 up = os.path.dirname(p)
23 23 if up == "/":
24 24 return "/"
25 25 return up + "/"
26 26
27 27 def revnavgen(pos, pagelen, limit, nodefunc):
28 28 def seq(factor, limit=None):
29 29 if limit:
30 30 yield limit
31 31 if limit >= 20 and limit <= 40:
32 32 yield 50
33 33 else:
34 34 yield 1 * factor
35 35 yield 3 * factor
36 36 for f in seq(factor * 10):
37 37 yield f
38 38
39 39 def nav(**map):
40 40 l = []
41 41 last = 0
42 42 for f in seq(1, pagelen):
43 43 if f < pagelen or f <= last:
44 44 continue
45 45 if f > limit:
46 46 break
47 47 last = f
48 48 if pos + f < limit:
49 49 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
50 50 if pos - f >= 0:
51 51 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
52 52
53 53 try:
54 54 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
55 55
56 56 for label, node in l:
57 57 yield {"label": label, "node": node}
58 58
59 59 yield {"label": "tip", "node": "tip"}
60 60 except hg.RepoError:
61 61 pass
62 62
63 63 return nav
64 64
65 65 class hgweb(object):
66 66 def __init__(self, repo, name=None):
67 67 if isinstance(repo, str):
68 68 parentui = ui.ui(report_untrusted=False, interactive=False)
69 69 self.repo = hg.repository(parentui, repo)
70 70 else:
71 71 self.repo = repo
72 72
73 73 self.mtime = -1
74 74 self.reponame = name
75 75 self.archives = 'zip', 'gz', 'bz2'
76 76 self.stripecount = 1
77 77 # a repo owner may set web.templates in .hg/hgrc to get any file
78 78 # readable by the user running the CGI script
79 79 self.templatepath = self.config("web", "templates",
80 80 templater.templatepath(),
81 81 untrusted=False)
82 82
83 83 # The CGI scripts are often run by a user different from the repo owner.
84 84 # Trust the settings from the .hg/hgrc files by default.
85 85 def config(self, section, name, default=None, untrusted=True):
86 86 return self.repo.ui.config(section, name, default,
87 87 untrusted=untrusted)
88 88
89 89 def configbool(self, section, name, default=False, untrusted=True):
90 90 return self.repo.ui.configbool(section, name, default,
91 91 untrusted=untrusted)
92 92
93 93 def configlist(self, section, name, default=None, untrusted=True):
94 94 return self.repo.ui.configlist(section, name, default,
95 95 untrusted=untrusted)
96 96
97 97 def refresh(self):
98 98 mtime = get_mtime(self.repo.root)
99 99 if mtime != self.mtime:
100 100 self.mtime = mtime
101 101 self.repo = hg.repository(self.repo.ui, self.repo.root)
102 102 self.maxchanges = int(self.config("web", "maxchanges", 10))
103 103 self.stripecount = int(self.config("web", "stripes", 1))
104 104 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
105 105 self.maxfiles = int(self.config("web", "maxfiles", 10))
106 106 self.allowpull = self.configbool("web", "allowpull", True)
107 107 self.encoding = self.config("web", "encoding", util._encoding)
108 108
109 109 def archivelist(self, nodeid):
110 110 allowed = self.configlist("web", "allow_archive")
111 111 for i, spec in self.archive_specs.iteritems():
112 112 if i in allowed or self.configbool("web", "allow" + i):
113 113 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
114 114
115 115 def listfilediffs(self, files, changeset):
116 116 for f in files[:self.maxfiles]:
117 117 yield self.t("filedifflink", node=hex(changeset), file=f)
118 118 if len(files) > self.maxfiles:
119 119 yield self.t("fileellipses")
120 120
121 121 def siblings(self, siblings=[], hiderev=None, **args):
122 122 siblings = [s for s in siblings if s.node() != nullid]
123 123 if len(siblings) == 1 and siblings[0].rev() == hiderev:
124 124 return
125 125 for s in siblings:
126 126 d = {'node': hex(s.node()), 'rev': s.rev()}
127 127 if hasattr(s, 'path'):
128 128 d['file'] = s.path()
129 129 d.update(args)
130 130 yield d
131 131
132 132 def renamelink(self, fl, node):
133 133 r = fl.renamed(node)
134 134 if r:
135 135 return [dict(file=r[0], node=hex(r[1]))]
136 136 return []
137 137
138 138 def nodetagsdict(self, node):
139 139 return [{"name": i} for i in self.repo.nodetags(node)]
140 140
141 141 def nodebranchdict(self, ctx):
142 142 branches = []
143 143 branch = ctx.branch()
144 144 # If this is an empty repo, ctx.node() == nullid,
145 145 # ctx.branch() == 'default', but branchtags() is
146 146 # an empty dict. Using dict.get avoids a traceback.
147 147 if self.repo.branchtags().get(branch) == ctx.node():
148 148 branches.append({"name": branch})
149 149 return branches
150 150
151 151 def showtag(self, t1, node=nullid, **args):
152 152 for t in self.repo.nodetags(node):
153 153 yield self.t(t1, tag=t, **args)
154 154
155 155 def diff(self, node1, node2, files):
156 156 def filterfiles(filters, files):
157 157 l = [x for x in files if x in filters]
158 158
159 159 for t in filters:
160 160 if t and t[-1] != os.sep:
161 161 t += os.sep
162 162 l += [x for x in files if x.startswith(t)]
163 163 return l
164 164
165 165 parity = paritygen(self.stripecount)
166 166 def diffblock(diff, f, fn):
167 167 yield self.t("diffblock",
168 168 lines=prettyprintlines(diff),
169 169 parity=parity.next(),
170 170 file=f,
171 171 filenode=hex(fn or nullid))
172 172
173 173 def prettyprintlines(diff):
174 174 for l in diff.splitlines(1):
175 175 if l.startswith('+'):
176 176 yield self.t("difflineplus", line=l)
177 177 elif l.startswith('-'):
178 178 yield self.t("difflineminus", line=l)
179 179 elif l.startswith('@'):
180 180 yield self.t("difflineat", line=l)
181 181 else:
182 182 yield self.t("diffline", line=l)
183 183
184 184 r = self.repo
185 185 c1 = r.changectx(node1)
186 186 c2 = r.changectx(node2)
187 187 date1 = util.datestr(c1.date())
188 188 date2 = util.datestr(c2.date())
189 189
190 190 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
191 191 if files:
192 192 modified, added, removed = map(lambda x: filterfiles(files, x),
193 193 (modified, added, removed))
194 194
195 195 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
196 196 for f in modified:
197 197 to = c1.filectx(f).data()
198 198 tn = c2.filectx(f).data()
199 199 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
200 200 opts=diffopts), f, tn)
201 201 for f in added:
202 202 to = None
203 203 tn = c2.filectx(f).data()
204 204 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
205 205 opts=diffopts), f, tn)
206 206 for f in removed:
207 207 to = c1.filectx(f).data()
208 208 tn = None
209 209 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
210 210 opts=diffopts), f, tn)
211 211
212 212 def changelog(self, ctx, shortlog=False):
213 213 def changelist(limit=0,**map):
214 214 cl = self.repo.changelog
215 215 l = [] # build a list in forward order for efficiency
216 216 for i in xrange(start, end):
217 217 ctx = self.repo.changectx(i)
218 218 n = ctx.node()
219 219
220 220 l.insert(0, {"parity": parity.next(),
221 221 "author": ctx.user(),
222 222 "parent": self.siblings(ctx.parents(), i - 1),
223 223 "child": self.siblings(ctx.children(), i + 1),
224 224 "changelogtag": self.showtag("changelogtag",n),
225 225 "desc": ctx.description(),
226 226 "date": ctx.date(),
227 227 "files": self.listfilediffs(ctx.files(), n),
228 228 "rev": i,
229 229 "node": hex(n),
230 230 "tags": self.nodetagsdict(n),
231 231 "branches": self.nodebranchdict(ctx)})
232 232
233 233 if limit > 0:
234 234 l = l[:limit]
235 235
236 236 for e in l:
237 237 yield e
238 238
239 239 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
240 240 cl = self.repo.changelog
241 241 count = cl.count()
242 242 pos = ctx.rev()
243 243 start = max(0, pos - maxchanges + 1)
244 244 end = min(count, start + maxchanges)
245 245 pos = end - 1
246 246 parity = paritygen(self.stripecount, offset=start-end)
247 247
248 248 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
249 249
250 250 yield self.t(shortlog and 'shortlog' or 'changelog',
251 251 changenav=changenav,
252 252 node=hex(cl.tip()),
253 253 rev=pos, changesets=count,
254 254 entries=lambda **x: changelist(limit=0,**x),
255 255 latestentry=lambda **x: changelist(limit=1,**x),
256 256 archives=self.archivelist("tip"))
257 257
258 258 def search(self, query):
259 259
260 260 def changelist(**map):
261 261 cl = self.repo.changelog
262 262 count = 0
263 263 qw = query.lower().split()
264 264
265 265 def revgen():
266 266 for i in xrange(cl.count() - 1, 0, -100):
267 267 l = []
268 268 for j in xrange(max(0, i - 100), i):
269 269 ctx = self.repo.changectx(j)
270 270 l.append(ctx)
271 271 l.reverse()
272 272 for e in l:
273 273 yield e
274 274
275 275 for ctx in revgen():
276 276 miss = 0
277 277 for q in qw:
278 278 if not (q in ctx.user().lower() or
279 279 q in ctx.description().lower() or
280 280 q in " ".join(ctx.files()).lower()):
281 281 miss = 1
282 282 break
283 283 if miss:
284 284 continue
285 285
286 286 count += 1
287 287 n = ctx.node()
288 288
289 289 yield self.t('searchentry',
290 290 parity=parity.next(),
291 291 author=ctx.user(),
292 292 parent=self.siblings(ctx.parents()),
293 293 child=self.siblings(ctx.children()),
294 294 changelogtag=self.showtag("changelogtag",n),
295 295 desc=ctx.description(),
296 296 date=ctx.date(),
297 297 files=self.listfilediffs(ctx.files(), n),
298 298 rev=ctx.rev(),
299 299 node=hex(n),
300 300 tags=self.nodetagsdict(n),
301 301 branches=self.nodebranchdict(ctx))
302 302
303 303 if count >= self.maxchanges:
304 304 break
305 305
306 306 cl = self.repo.changelog
307 307 parity = paritygen(self.stripecount)
308 308
309 309 yield self.t('search',
310 310 query=query,
311 311 node=hex(cl.tip()),
312 312 entries=changelist,
313 313 archives=self.archivelist("tip"))
314 314
315 315 def changeset(self, ctx):
316 316 n = ctx.node()
317 317 parents = ctx.parents()
318 318 p1 = parents[0].node()
319 319
320 320 files = []
321 321 parity = paritygen(self.stripecount)
322 322 for f in ctx.files():
323 323 files.append(self.t("filenodelink",
324 324 node=hex(n), file=f,
325 325 parity=parity.next()))
326 326
327 327 def diff(**map):
328 328 yield self.diff(p1, n, None)
329 329
330 330 yield self.t('changeset',
331 331 diff=diff,
332 332 rev=ctx.rev(),
333 333 node=hex(n),
334 334 parent=self.siblings(parents),
335 335 child=self.siblings(ctx.children()),
336 336 changesettag=self.showtag("changesettag",n),
337 337 author=ctx.user(),
338 338 desc=ctx.description(),
339 339 date=ctx.date(),
340 340 files=files,
341 341 archives=self.archivelist(hex(n)),
342 342 tags=self.nodetagsdict(n),
343 343 branches=self.nodebranchdict(ctx))
344 344
345 345 def filelog(self, fctx):
346 346 f = fctx.path()
347 347 fl = fctx.filelog()
348 348 count = fl.count()
349 349 pagelen = self.maxshortchanges
350 350 pos = fctx.filerev()
351 351 start = max(0, pos - pagelen + 1)
352 352 end = min(count, start + pagelen)
353 353 pos = end - 1
354 354 parity = paritygen(self.stripecount, offset=start-end)
355 355
356 356 def entries(limit=0, **map):
357 357 l = []
358 358
359 359 for i in xrange(start, end):
360 360 ctx = fctx.filectx(i)
361 361 n = fl.node(i)
362 362
363 363 l.insert(0, {"parity": parity.next(),
364 364 "filerev": i,
365 365 "file": f,
366 366 "node": hex(ctx.node()),
367 367 "author": ctx.user(),
368 368 "date": ctx.date(),
369 369 "rename": self.renamelink(fl, n),
370 370 "parent": self.siblings(fctx.parents()),
371 371 "child": self.siblings(fctx.children()),
372 372 "desc": ctx.description()})
373 373
374 374 if limit > 0:
375 375 l = l[:limit]
376 376
377 377 for e in l:
378 378 yield e
379 379
380 380 nodefunc = lambda x: fctx.filectx(fileid=x)
381 381 nav = revnavgen(pos, pagelen, count, nodefunc)
382 382 yield self.t("filelog", file=f, node=hex(fctx.node()), nav=nav,
383 383 entries=lambda **x: entries(limit=0, **x),
384 384 latestentry=lambda **x: entries(limit=1, **x))
385 385
386 386 def filerevision(self, fctx):
387 387 f = fctx.path()
388 388 text = fctx.data()
389 389 fl = fctx.filelog()
390 390 n = fctx.filenode()
391 391 parity = paritygen(self.stripecount)
392 392
393 393 mt = mimetypes.guess_type(f)[0]
394 394 rawtext = text
395 395 if util.binary(text):
396 396 mt = mt or 'application/octet-stream'
397 397 text = "(binary:%s)" % mt
398 398 mt = mt or 'text/plain'
399 399
400 400 def lines():
401 401 for l, t in enumerate(text.splitlines(1)):
402 402 yield {"line": t,
403 403 "linenumber": "% 6d" % (l + 1),
404 404 "parity": parity.next()}
405 405
406 406 yield self.t("filerevision",
407 407 file=f,
408 408 path=_up(f),
409 409 text=lines(),
410 410 raw=rawtext,
411 411 mimetype=mt,
412 412 rev=fctx.rev(),
413 413 node=hex(fctx.node()),
414 414 author=fctx.user(),
415 415 date=fctx.date(),
416 416 desc=fctx.description(),
417 417 parent=self.siblings(fctx.parents()),
418 418 child=self.siblings(fctx.children()),
419 419 rename=self.renamelink(fl, n),
420 420 permissions=fctx.manifest().flags(f))
421 421
422 422 def fileannotate(self, fctx):
423 423 f = fctx.path()
424 424 n = fctx.filenode()
425 425 fl = fctx.filelog()
426 426 parity = paritygen(self.stripecount)
427 427
428 428 def annotate(**map):
429 429 last = None
430 430 for f, l in fctx.annotate(follow=True):
431 431 fnode = f.filenode()
432 432 name = self.repo.ui.shortuser(f.user())
433 433
434 434 if last != fnode:
435 435 last = fnode
436 436
437 437 yield {"parity": parity.next(),
438 438 "node": hex(f.node()),
439 439 "rev": f.rev(),
440 440 "author": name,
441 441 "file": f.path(),
442 442 "line": l}
443 443
444 444 yield self.t("fileannotate",
445 445 file=f,
446 446 annotate=annotate,
447 447 path=_up(f),
448 448 rev=fctx.rev(),
449 449 node=hex(fctx.node()),
450 450 author=fctx.user(),
451 451 date=fctx.date(),
452 452 desc=fctx.description(),
453 453 rename=self.renamelink(fl, n),
454 454 parent=self.siblings(fctx.parents()),
455 455 child=self.siblings(fctx.children()),
456 456 permissions=fctx.manifest().flags(f))
457 457
458 458 def manifest(self, ctx, path):
459 459 mf = ctx.manifest()
460 460 node = ctx.node()
461 461
462 462 files = {}
463 463 parity = paritygen(self.stripecount)
464 464
465 465 if path and path[-1] != "/":
466 466 path += "/"
467 467 l = len(path)
468 468 abspath = "/" + path
469 469
470 470 for f, n in mf.items():
471 471 if f[:l] != path:
472 472 continue
473 473 remain = f[l:]
474 474 if "/" in remain:
475 475 short = remain[:remain.index("/") + 1] # bleah
476 476 files[short] = (f, None)
477 477 else:
478 478 short = os.path.basename(remain)
479 479 files[short] = (f, n)
480 480
481 if not files:
482 raise ErrorResponse(404, 'Path not found: ' + path)
483
481 484 def filelist(**map):
482 485 fl = files.keys()
483 486 fl.sort()
484 487 for f in fl:
485 488 full, fnode = files[f]
486 489 if not fnode:
487 490 continue
488 491
489 492 fctx = ctx.filectx(full)
490 493 yield {"file": full,
491 494 "parity": parity.next(),
492 495 "basename": f,
493 496 "date": fctx.changectx().date(),
494 497 "size": fctx.size(),
495 498 "permissions": mf.flags(full)}
496 499
497 500 def dirlist(**map):
498 501 fl = files.keys()
499 502 fl.sort()
500 503 for f in fl:
501 504 full, fnode = files[f]
502 505 if fnode:
503 506 continue
504 507
505 508 yield {"parity": parity.next(),
506 509 "path": "%s%s" % (abspath, f),
507 510 "basename": f[:-1]}
508 511
509 512 yield self.t("manifest",
510 513 rev=ctx.rev(),
511 514 node=hex(node),
512 515 path=abspath,
513 516 up=_up(abspath),
514 517 upparity=parity.next(),
515 518 fentries=filelist,
516 519 dentries=dirlist,
517 520 archives=self.archivelist(hex(node)),
518 521 tags=self.nodetagsdict(node),
519 522 branches=self.nodebranchdict(ctx))
520 523
521 524 def tags(self):
522 525 i = self.repo.tagslist()
523 526 i.reverse()
524 527 parity = paritygen(self.stripecount)
525 528
526 529 def entries(notip=False,limit=0, **map):
527 530 count = 0
528 531 for k, n in i:
529 532 if notip and k == "tip":
530 533 continue
531 534 if limit > 0 and count >= limit:
532 535 continue
533 536 count = count + 1
534 537 yield {"parity": parity.next(),
535 538 "tag": k,
536 539 "date": self.repo.changectx(n).date(),
537 540 "node": hex(n)}
538 541
539 542 yield self.t("tags",
540 543 node=hex(self.repo.changelog.tip()),
541 544 entries=lambda **x: entries(False,0, **x),
542 545 entriesnotip=lambda **x: entries(True,0, **x),
543 546 latestentry=lambda **x: entries(True,1, **x))
544 547
545 548 def summary(self):
546 549 i = self.repo.tagslist()
547 550 i.reverse()
548 551
549 552 def tagentries(**map):
550 553 parity = paritygen(self.stripecount)
551 554 count = 0
552 555 for k, n in i:
553 556 if k == "tip": # skip tip
554 557 continue;
555 558
556 559 count += 1
557 560 if count > 10: # limit to 10 tags
558 561 break;
559 562
560 563 yield self.t("tagentry",
561 564 parity=parity.next(),
562 565 tag=k,
563 566 node=hex(n),
564 567 date=self.repo.changectx(n).date())
565 568
566 569
567 570 def branches(**map):
568 571 parity = paritygen(self.stripecount)
569 572
570 573 b = self.repo.branchtags()
571 574 l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()]
572 575 l.sort()
573 576
574 577 for r,n,t in l:
575 578 ctx = self.repo.changectx(n)
576 579
577 580 yield {'parity': parity.next(),
578 581 'branch': t,
579 582 'node': hex(n),
580 583 'date': ctx.date()}
581 584
582 585 def changelist(**map):
583 586 parity = paritygen(self.stripecount, offset=start-end)
584 587 l = [] # build a list in forward order for efficiency
585 588 for i in xrange(start, end):
586 589 ctx = self.repo.changectx(i)
587 590 n = ctx.node()
588 591 hn = hex(n)
589 592
590 593 l.insert(0, self.t(
591 594 'shortlogentry',
592 595 parity=parity.next(),
593 596 author=ctx.user(),
594 597 desc=ctx.description(),
595 598 date=ctx.date(),
596 599 rev=i,
597 600 node=hn,
598 601 tags=self.nodetagsdict(n),
599 602 branches=self.nodebranchdict(ctx)))
600 603
601 604 yield l
602 605
603 606 cl = self.repo.changelog
604 607 count = cl.count()
605 608 start = max(0, count - self.maxchanges)
606 609 end = min(count, start + self.maxchanges)
607 610
608 611 yield self.t("summary",
609 612 desc=self.config("web", "description", "unknown"),
610 613 owner=(self.config("ui", "username") or # preferred
611 614 self.config("web", "contact") or # deprecated
612 615 self.config("web", "author", "unknown")), # also
613 616 lastchange=cl.read(cl.tip())[2],
614 617 tags=tagentries,
615 618 branches=branches,
616 619 shortlog=changelist,
617 620 node=hex(cl.tip()),
618 621 archives=self.archivelist("tip"))
619 622
620 623 def filediff(self, fctx):
621 624 n = fctx.node()
622 625 path = fctx.path()
623 626 parents = fctx.parents()
624 627 p1 = parents and parents[0].node() or nullid
625 628
626 629 def diff(**map):
627 630 yield self.diff(p1, n, [path])
628 631
629 632 yield self.t("filediff",
630 633 file=path,
631 634 node=hex(n),
632 635 rev=fctx.rev(),
633 636 parent=self.siblings(parents),
634 637 child=self.siblings(fctx.children()),
635 638 diff=diff)
636 639
637 640 archive_specs = {
638 641 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
639 642 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
640 643 'zip': ('application/zip', 'zip', '.zip', None),
641 644 }
642 645
643 646 def archive(self, req, key, type_):
644 647 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
645 648 cnode = self.repo.lookup(key)
646 649 arch_version = key
647 650 if cnode == key or key == 'tip':
648 651 arch_version = short(cnode)
649 652 name = "%s-%s" % (reponame, arch_version)
650 653 mimetype, artype, extension, encoding = self.archive_specs[type_]
651 654 headers = [('Content-type', mimetype),
652 655 ('Content-disposition', 'attachment; filename=%s%s' %
653 656 (name, extension))]
654 657 if encoding:
655 658 headers.append(('Content-encoding', encoding))
656 659 req.header(headers)
657 660 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
658 661
659 662 # add tags to things
660 663 # tags -> list of changesets corresponding to tags
661 664 # find tag, changeset, file
662 665
663 666 def cleanpath(self, path):
664 667 path = path.lstrip('/')
665 668 return util.canonpath(self.repo.root, '', path)
666 669
667 670 def run(self):
668 671 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
669 672 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
670 673 import mercurial.hgweb.wsgicgi as wsgicgi
671 674 from request import wsgiapplication
672 675 def make_web_app():
673 676 return self
674 677 wsgicgi.launch(wsgiapplication(make_web_app))
675 678
676 679 def run_wsgi(self, req):
677 680 def header(**map):
678 681 header_file = cStringIO.StringIO(
679 682 ''.join(self.t("header", encoding=self.encoding, **map)))
680 683 msg = mimetools.Message(header_file, 0)
681 684 req.header(msg.items())
682 685 yield header_file.read()
683 686
684 687 def rawfileheader(**map):
685 688 req.header([('Content-type', map['mimetype']),
686 689 ('Content-disposition', 'filename=%s' % map['file']),
687 690 ('Content-length', str(len(map['raw'])))])
688 691 yield ''
689 692
690 693 def footer(**map):
691 694 yield self.t("footer", **map)
692 695
693 696 def motd(**map):
694 697 yield self.config("web", "motd", "")
695 698
696 699 def expand_form(form):
697 700 shortcuts = {
698 701 'cl': [('cmd', ['changelog']), ('rev', None)],
699 702 'sl': [('cmd', ['shortlog']), ('rev', None)],
700 703 'cs': [('cmd', ['changeset']), ('node', None)],
701 704 'f': [('cmd', ['file']), ('filenode', None)],
702 705 'fl': [('cmd', ['filelog']), ('filenode', None)],
703 706 'fd': [('cmd', ['filediff']), ('node', None)],
704 707 'fa': [('cmd', ['annotate']), ('filenode', None)],
705 708 'mf': [('cmd', ['manifest']), ('manifest', None)],
706 709 'ca': [('cmd', ['archive']), ('node', None)],
707 710 'tags': [('cmd', ['tags'])],
708 711 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
709 712 'static': [('cmd', ['static']), ('file', None)]
710 713 }
711 714
712 715 for k in shortcuts.iterkeys():
713 716 if form.has_key(k):
714 717 for name, value in shortcuts[k]:
715 718 if value is None:
716 719 value = form[k]
717 720 form[name] = value
718 721 del form[k]
719 722
720 723 def rewrite_request(req):
721 724 '''translate new web interface to traditional format'''
722 725
723 726 def spliturl(req):
724 727 def firstitem(query):
725 728 return query.split('&', 1)[0].split(';', 1)[0]
726 729
727 730 def normurl(url):
728 731 inner = '/'.join([x for x in url.split('/') if x])
729 732 tl = len(url) > 1 and url.endswith('/') and '/' or ''
730 733
731 734 return '%s%s%s' % (url.startswith('/') and '/' or '',
732 735 inner, tl)
733 736
734 737 root = normurl(urllib.unquote(req.env.get('REQUEST_URI', '').split('?', 1)[0]))
735 738 pi = normurl(req.env.get('PATH_INFO', ''))
736 739 if pi:
737 740 # strip leading /
738 741 pi = pi[1:]
739 742 if pi:
740 743 root = root[:root.rfind(pi)]
741 744 if req.env.has_key('REPO_NAME'):
742 745 rn = req.env['REPO_NAME'] + '/'
743 746 root += rn
744 747 query = pi[len(rn):]
745 748 else:
746 749 query = pi
747 750 else:
748 751 root += '?'
749 752 query = firstitem(req.env['QUERY_STRING'])
750 753
751 754 return (root, query)
752 755
753 756 req.url, query = spliturl(req)
754 757
755 758 if req.form.has_key('cmd'):
756 759 # old style
757 760 return
758 761
759 762 args = query.split('/', 2)
760 763 if not args or not args[0]:
761 764 return
762 765
763 766 cmd = args.pop(0)
764 767 style = cmd.rfind('-')
765 768 if style != -1:
766 769 req.form['style'] = [cmd[:style]]
767 770 cmd = cmd[style+1:]
768 771 # avoid accepting e.g. style parameter as command
769 772 if hasattr(self, 'do_' + cmd):
770 773 req.form['cmd'] = [cmd]
771 774
772 775 if args and args[0]:
773 776 node = args.pop(0)
774 777 req.form['node'] = [node]
775 778 if args:
776 779 req.form['file'] = args
777 780
778 781 if cmd == 'static':
779 782 req.form['file'] = req.form['node']
780 783 elif cmd == 'archive':
781 784 fn = req.form['node'][0]
782 785 for type_, spec in self.archive_specs.iteritems():
783 786 ext = spec[2]
784 787 if fn.endswith(ext):
785 788 req.form['node'] = [fn[:-len(ext)]]
786 789 req.form['type'] = [type_]
787 790
788 791 def sessionvars(**map):
789 792 fields = []
790 793 if req.form.has_key('style'):
791 794 style = req.form['style'][0]
792 795 if style != self.config('web', 'style', ''):
793 796 fields.append(('style', style))
794 797
795 798 separator = req.url[-1] == '?' and ';' or '?'
796 799 for name, value in fields:
797 800 yield dict(name=name, value=value, separator=separator)
798 801 separator = ';'
799 802
800 803 self.refresh()
801 804
802 805 expand_form(req.form)
803 806 rewrite_request(req)
804 807
805 808 style = self.config("web", "style", "")
806 809 if req.form.has_key('style'):
807 810 style = req.form['style'][0]
808 811 mapfile = style_map(self.templatepath, style)
809 812
810 813 proto = req.env.get('wsgi.url_scheme')
811 814 if proto == 'https':
812 815 proto = 'https'
813 816 default_port = "443"
814 817 else:
815 818 proto = 'http'
816 819 default_port = "80"
817 820
818 821 port = req.env["SERVER_PORT"]
819 822 port = port != default_port and (":" + port) or ""
820 823 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
821 824 staticurl = self.config("web", "staticurl") or req.url + 'static/'
822 825 if not staticurl.endswith('/'):
823 826 staticurl += '/'
824 827
825 828 if not self.reponame:
826 829 self.reponame = (self.config("web", "name")
827 830 or req.env.get('REPO_NAME')
828 831 or req.url.strip('/') or self.repo.root)
829 832
830 833 self.t = templater.templater(mapfile, templater.common_filters,
831 834 defaults={"url": req.url,
832 835 "staticurl": staticurl,
833 836 "urlbase": urlbase,
834 837 "repo": self.reponame,
835 838 "header": header,
836 839 "footer": footer,
837 840 "motd": motd,
838 841 "rawfileheader": rawfileheader,
839 842 "sessionvars": sessionvars
840 843 })
841 844
842 845 try:
843 846 if not req.form.has_key('cmd'):
844 847 req.form['cmd'] = [self.t.cache['default']]
845 848
846 849 cmd = req.form['cmd'][0]
847 850
848 method = getattr(self, 'do_' + cmd, None)
849 if method:
850 851 try:
852 method = getattr(self, 'do_' + cmd)
851 853 method(req)
854 except revlog.LookupError, err:
855 req.respond(404, self.t(
856 'error', error='revision not found: %s' % err.name))
852 857 except (hg.RepoError, revlog.RevlogError), inst:
853 req.write(self.t("error", error=str(inst)))
854 else:
855 req.write(self.t("error", error='No such method: ' + cmd))
858 req.respond('500 Internal Server Error',
859 self.t('error', error=str(inst)))
860 except ErrorResponse, inst:
861 req.respond(inst.code, self.t('error', error=inst.message))
862 except AttributeError:
863 req.respond(400,
864 self.t('error', error='No such method: ' + cmd))
856 865 finally:
857 866 self.t = None
858 867
859 868 def changectx(self, req):
860 869 if req.form.has_key('node'):
861 870 changeid = req.form['node'][0]
862 871 elif req.form.has_key('manifest'):
863 872 changeid = req.form['manifest'][0]
864 873 else:
865 874 changeid = self.repo.changelog.count() - 1
866 875
867 876 try:
868 877 ctx = self.repo.changectx(changeid)
869 878 except hg.RepoError:
870 879 man = self.repo.manifest
871 880 mn = man.lookup(changeid)
872 881 ctx = self.repo.changectx(man.linkrev(mn))
873 882
874 883 return ctx
875 884
876 885 def filectx(self, req):
877 886 path = self.cleanpath(req.form['file'][0])
878 887 if req.form.has_key('node'):
879 888 changeid = req.form['node'][0]
880 889 else:
881 890 changeid = req.form['filenode'][0]
882 891 try:
883 892 ctx = self.repo.changectx(changeid)
884 893 fctx = ctx.filectx(path)
885 894 except hg.RepoError:
886 895 fctx = self.repo.filectx(path, fileid=changeid)
887 896
888 897 return fctx
889 898
890 899 def do_log(self, req):
891 900 if req.form.has_key('file') and req.form['file'][0]:
892 901 self.do_filelog(req)
893 902 else:
894 903 self.do_changelog(req)
895 904
896 905 def do_rev(self, req):
897 906 self.do_changeset(req)
898 907
899 908 def do_file(self, req):
900 909 path = self.cleanpath(req.form.get('file', [''])[0])
901 910 if path:
902 911 try:
903 912 req.write(self.filerevision(self.filectx(req)))
904 913 return
905 914 except revlog.LookupError:
906 915 pass
907 916
908 917 req.write(self.manifest(self.changectx(req), path))
909 918
910 919 def do_diff(self, req):
911 920 self.do_filediff(req)
912 921
913 922 def do_changelog(self, req, shortlog = False):
914 923 if req.form.has_key('node'):
915 924 ctx = self.changectx(req)
916 925 else:
917 926 if req.form.has_key('rev'):
918 927 hi = req.form['rev'][0]
919 928 else:
920 929 hi = self.repo.changelog.count() - 1
921 930 try:
922 931 ctx = self.repo.changectx(hi)
923 932 except hg.RepoError:
924 933 req.write(self.search(hi)) # XXX redirect to 404 page?
925 934 return
926 935
927 936 req.write(self.changelog(ctx, shortlog = shortlog))
928 937
929 938 def do_shortlog(self, req):
930 939 self.do_changelog(req, shortlog = True)
931 940
932 941 def do_changeset(self, req):
933 942 req.write(self.changeset(self.changectx(req)))
934 943
935 944 def do_manifest(self, req):
936 945 req.write(self.manifest(self.changectx(req),
937 946 self.cleanpath(req.form['path'][0])))
938 947
939 948 def do_tags(self, req):
940 949 req.write(self.tags())
941 950
942 951 def do_summary(self, req):
943 952 req.write(self.summary())
944 953
945 954 def do_filediff(self, req):
946 955 req.write(self.filediff(self.filectx(req)))
947 956
948 957 def do_annotate(self, req):
949 958 req.write(self.fileannotate(self.filectx(req)))
950 959
951 960 def do_filelog(self, req):
952 961 req.write(self.filelog(self.filectx(req)))
953 962
954 963 def do_lookup(self, req):
955 964 try:
956 965 r = hex(self.repo.lookup(req.form['key'][0]))
957 966 success = 1
958 967 except Exception,inst:
959 968 r = str(inst)
960 969 success = 0
961 970 resp = "%s %s\n" % (success, r)
962 971 req.httphdr("application/mercurial-0.1", length=len(resp))
963 972 req.write(resp)
964 973
965 974 def do_heads(self, req):
966 975 resp = " ".join(map(hex, self.repo.heads())) + "\n"
967 976 req.httphdr("application/mercurial-0.1", length=len(resp))
968 977 req.write(resp)
969 978
970 979 def do_branches(self, req):
971 980 nodes = []
972 981 if req.form.has_key('nodes'):
973 982 nodes = map(bin, req.form['nodes'][0].split(" "))
974 983 resp = cStringIO.StringIO()
975 984 for b in self.repo.branches(nodes):
976 985 resp.write(" ".join(map(hex, b)) + "\n")
977 986 resp = resp.getvalue()
978 987 req.httphdr("application/mercurial-0.1", length=len(resp))
979 988 req.write(resp)
980 989
981 990 def do_between(self, req):
982 991 if req.form.has_key('pairs'):
983 992 pairs = [map(bin, p.split("-"))
984 993 for p in req.form['pairs'][0].split(" ")]
985 994 resp = cStringIO.StringIO()
986 995 for b in self.repo.between(pairs):
987 996 resp.write(" ".join(map(hex, b)) + "\n")
988 997 resp = resp.getvalue()
989 998 req.httphdr("application/mercurial-0.1", length=len(resp))
990 999 req.write(resp)
991 1000
992 1001 def do_changegroup(self, req):
993 1002 req.httphdr("application/mercurial-0.1")
994 1003 nodes = []
995 1004 if not self.allowpull:
996 1005 return
997 1006
998 1007 if req.form.has_key('roots'):
999 1008 nodes = map(bin, req.form['roots'][0].split(" "))
1000 1009
1001 1010 z = zlib.compressobj()
1002 1011 f = self.repo.changegroup(nodes, 'serve')
1003 1012 while 1:
1004 1013 chunk = f.read(4096)
1005 1014 if not chunk:
1006 1015 break
1007 1016 req.write(z.compress(chunk))
1008 1017
1009 1018 req.write(z.flush())
1010 1019
1011 1020 def do_changegroupsubset(self, req):
1012 1021 req.httphdr("application/mercurial-0.1")
1013 1022 bases = []
1014 1023 heads = []
1015 1024 if not self.allowpull:
1016 1025 return
1017 1026
1018 1027 if req.form.has_key('bases'):
1019 1028 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
1020 1029 if req.form.has_key('heads'):
1021 1030 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
1022 1031
1023 1032 z = zlib.compressobj()
1024 1033 f = self.repo.changegroupsubset(bases, heads, 'serve')
1025 1034 while 1:
1026 1035 chunk = f.read(4096)
1027 1036 if not chunk:
1028 1037 break
1029 1038 req.write(z.compress(chunk))
1030 1039
1031 1040 req.write(z.flush())
1032 1041
1033 1042 def do_archive(self, req):
1034 1043 type_ = req.form['type'][0]
1035 1044 allowed = self.configlist("web", "allow_archive")
1036 1045 if (type_ in self.archives and (type_ in allowed or
1037 1046 self.configbool("web", "allow" + type_, False))):
1038 1047 self.archive(req, req.form['node'][0], type_)
1039 1048 return
1040 1049
1041 req.write(self.t("error"))
1050 req.respond(400, self.t('error',
1051 error='Unsupported archive type: %s' % type_))
1042 1052
1043 1053 def do_static(self, req):
1044 1054 fname = req.form['file'][0]
1045 1055 # a repo owner may set web.static in .hg/hgrc to get any file
1046 1056 # readable by the user running the CGI script
1047 1057 static = self.config("web", "static",
1048 1058 os.path.join(self.templatepath, "static"),
1049 1059 untrusted=False)
1050 req.write(staticfile(static, fname, req)
1051 or self.t("error", error="%r not found" % fname))
1060 req.write(staticfile(static, fname, req))
1052 1061
1053 1062 def do_capabilities(self, req):
1054 1063 caps = ['lookup', 'changegroupsubset']
1055 1064 if self.configbool('server', 'uncompressed'):
1056 1065 caps.append('stream=%d' % self.repo.changelog.version)
1057 1066 # XXX: make configurable and/or share code with do_unbundle:
1058 1067 unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN']
1059 1068 if unbundleversions:
1060 1069 caps.append('unbundle=%s' % ','.join(unbundleversions))
1061 1070 resp = ' '.join(caps)
1062 1071 req.httphdr("application/mercurial-0.1", length=len(resp))
1063 1072 req.write(resp)
1064 1073
1065 1074 def check_perm(self, req, op, default):
1066 1075 '''check permission for operation based on user auth.
1067 1076 return true if op allowed, else false.
1068 1077 default is policy to use if no config given.'''
1069 1078
1070 1079 user = req.env.get('REMOTE_USER')
1071 1080
1072 1081 deny = self.configlist('web', 'deny_' + op)
1073 1082 if deny and (not user or deny == ['*'] or user in deny):
1074 1083 return False
1075 1084
1076 1085 allow = self.configlist('web', 'allow_' + op)
1077 1086 return (allow and (allow == ['*'] or user in allow)) or default
1078 1087
1079 1088 def do_unbundle(self, req):
1080 1089 def bail(response, headers={}):
1081 1090 length = int(req.env['CONTENT_LENGTH'])
1082 1091 for s in util.filechunkiter(req, limit=length):
1083 1092 # drain incoming bundle, else client will not see
1084 1093 # response when run outside cgi script
1085 1094 pass
1086 1095 req.httphdr("application/mercurial-0.1", headers=headers)
1087 1096 req.write('0\n')
1088 1097 req.write(response)
1089 1098
1090 1099 # require ssl by default, auth info cannot be sniffed and
1091 1100 # replayed
1092 1101 ssl_req = self.configbool('web', 'push_ssl', True)
1093 1102 if ssl_req:
1094 1103 if req.env.get('wsgi.url_scheme') != 'https':
1095 1104 bail(_('ssl required\n'))
1096 1105 return
1097 1106 proto = 'https'
1098 1107 else:
1099 1108 proto = 'http'
1100 1109
1101 1110 # do not allow push unless explicitly allowed
1102 1111 if not self.check_perm(req, 'push', False):
1103 1112 bail(_('push not authorized\n'),
1104 1113 headers={'status': '401 Unauthorized'})
1105 1114 return
1106 1115
1107 1116 their_heads = req.form['heads'][0].split(' ')
1108 1117
1109 1118 def check_heads():
1110 1119 heads = map(hex, self.repo.heads())
1111 1120 return their_heads == [hex('force')] or their_heads == heads
1112 1121
1113 1122 # fail early if possible
1114 1123 if not check_heads():
1115 1124 bail(_('unsynced changes\n'))
1116 1125 return
1117 1126
1118 1127 req.httphdr("application/mercurial-0.1")
1119 1128
1120 1129 # do not lock repo until all changegroup data is
1121 1130 # streamed. save to temporary file.
1122 1131
1123 1132 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1124 1133 fp = os.fdopen(fd, 'wb+')
1125 1134 try:
1126 1135 length = int(req.env['CONTENT_LENGTH'])
1127 1136 for s in util.filechunkiter(req, limit=length):
1128 1137 fp.write(s)
1129 1138
1130 1139 try:
1131 1140 lock = self.repo.lock()
1132 1141 try:
1133 1142 if not check_heads():
1134 1143 req.write('0\n')
1135 1144 req.write(_('unsynced changes\n'))
1136 1145 return
1137 1146
1138 1147 fp.seek(0)
1139 1148 header = fp.read(6)
1140 1149 if not header.startswith("HG"):
1141 1150 # old client with uncompressed bundle
1142 1151 def generator(f):
1143 1152 yield header
1144 1153 for chunk in f:
1145 1154 yield chunk
1146 1155 elif not header.startswith("HG10"):
1147 1156 req.write("0\n")
1148 1157 req.write(_("unknown bundle version\n"))
1149 1158 return
1150 1159 elif header == "HG10GZ":
1151 1160 def generator(f):
1152 1161 zd = zlib.decompressobj()
1153 1162 for chunk in f:
1154 1163 yield zd.decompress(chunk)
1155 1164 elif header == "HG10BZ":
1156 1165 def generator(f):
1157 1166 zd = bz2.BZ2Decompressor()
1158 1167 zd.decompress("BZ")
1159 1168 for chunk in f:
1160 1169 yield zd.decompress(chunk)
1161 1170 elif header == "HG10UN":
1162 1171 def generator(f):
1163 1172 for chunk in f:
1164 1173 yield chunk
1165 1174 else:
1166 1175 req.write("0\n")
1167 1176 req.write(_("unknown bundle compression type\n"))
1168 1177 return
1169 1178 gen = generator(util.filechunkiter(fp, 4096))
1170 1179
1171 1180 # send addchangegroup output to client
1172 1181
1173 1182 old_stdout = sys.stdout
1174 1183 sys.stdout = cStringIO.StringIO()
1175 1184
1176 1185 try:
1177 1186 url = 'remote:%s:%s' % (proto,
1178 1187 req.env.get('REMOTE_HOST', ''))
1179 1188 try:
1180 1189 ret = self.repo.addchangegroup(
1181 1190 util.chunkbuffer(gen), 'serve', url)
1182 1191 except util.Abort, inst:
1183 1192 sys.stdout.write("abort: %s\n" % inst)
1184 1193 ret = 0
1185 1194 finally:
1186 1195 val = sys.stdout.getvalue()
1187 1196 sys.stdout = old_stdout
1188 1197 req.write('%d\n' % ret)
1189 1198 req.write(val)
1190 1199 finally:
1191 1200 del lock
1192 1201 except (OSError, IOError), inst:
1193 1202 req.write('0\n')
1194 1203 filename = getattr(inst, 'filename', '')
1195 1204 # Don't send our filesystem layout to the client
1196 1205 if filename.startswith(self.repo.root):
1197 1206 filename = filename[len(self.repo.root)+1:]
1198 1207 else:
1199 1208 filename = ''
1200 1209 error = getattr(inst, 'strerror', 'Unknown error')
1201 req.write('%s: %s\n' % (error, filename))
1210 if inst.errno == errno.ENOENT:
1211 code = 404
1212 else:
1213 code = 500
1214 req.respond(code, '%s: %s\n' % (error, filename))
1202 1215 finally:
1203 1216 fp.close()
1204 1217 os.unlink(tempname)
1205 1218
1206 1219 def do_stream_out(self, req):
1207 1220 req.httphdr("application/mercurial-0.1")
1208 1221 streamclone.stream_out(self.repo, req, untrusted=True)
@@ -1,260 +1,261
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, mimetools, cStringIO
10 10 from mercurial.i18n import gettext as _
11 11 from mercurial import ui, hg, util, templater
12 from common import get_mtime, staticfile, style_map, paritygen
12 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen
13 13 from hgweb_mod import hgweb
14 14
15 15 # This is a stopgap
16 16 class hgwebdir(object):
17 17 def __init__(self, config, parentui=None):
18 18 def cleannames(items):
19 19 return [(util.pconvert(name.strip(os.sep)), path)
20 20 for name, path in items]
21 21
22 22 self.parentui = parentui
23 23 self.motd = None
24 24 self.style = None
25 25 self.stripecount = None
26 26 self.repos_sorted = ('name', False)
27 27 if isinstance(config, (list, tuple)):
28 28 self.repos = cleannames(config)
29 29 self.repos_sorted = ('', False)
30 30 elif isinstance(config, dict):
31 31 self.repos = cleannames(config.items())
32 32 self.repos.sort()
33 33 else:
34 34 if isinstance(config, util.configparser):
35 35 cp = config
36 36 else:
37 37 cp = util.configparser()
38 38 cp.read(config)
39 39 self.repos = []
40 40 if cp.has_section('web'):
41 41 if cp.has_option('web', 'motd'):
42 42 self.motd = cp.get('web', 'motd')
43 43 if cp.has_option('web', 'style'):
44 44 self.style = cp.get('web', 'style')
45 45 if cp.has_option('web', 'stripes'):
46 46 self.stripecount = int(cp.get('web', 'stripes'))
47 47 if cp.has_section('paths'):
48 48 self.repos.extend(cleannames(cp.items('paths')))
49 49 if cp.has_section('collections'):
50 50 for prefix, root in cp.items('collections'):
51 51 for path in util.walkrepos(root):
52 52 repo = os.path.normpath(path)
53 53 name = repo
54 54 if name.startswith(prefix):
55 55 name = name[len(prefix):]
56 56 self.repos.append((name.lstrip(os.sep), repo))
57 57 self.repos.sort()
58 58
59 59 def run(self):
60 60 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
61 61 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
62 62 import mercurial.hgweb.wsgicgi as wsgicgi
63 63 from request import wsgiapplication
64 64 def make_web_app():
65 65 return self
66 66 wsgicgi.launch(wsgiapplication(make_web_app))
67 67
68 68 def run_wsgi(self, req):
69 69 def header(**map):
70 70 header_file = cStringIO.StringIO(
71 71 ''.join(tmpl("header", encoding=util._encoding, **map)))
72 72 msg = mimetools.Message(header_file, 0)
73 73 req.header(msg.items())
74 74 yield header_file.read()
75 75
76 76 def footer(**map):
77 77 yield tmpl("footer", **map)
78 78
79 79 def motd(**map):
80 80 if self.motd is not None:
81 81 yield self.motd
82 82 else:
83 83 yield config('web', 'motd', '')
84 84
85 85 parentui = self.parentui or ui.ui(report_untrusted=False,
86 86 interactive=False)
87 87
88 88 def config(section, name, default=None, untrusted=True):
89 89 return parentui.config(section, name, default, untrusted)
90 90
91 91 url = req.env['REQUEST_URI'].split('?')[0]
92 92 if not url.endswith('/'):
93 93 url += '/'
94 94 pathinfo = req.env.get('PATH_INFO', '').strip('/') + '/'
95 95 base = url[:len(url) - len(pathinfo)]
96 96 if not base.endswith('/'):
97 97 base += '/'
98 98
99 99 staticurl = config('web', 'staticurl') or base + 'static/'
100 100 if not staticurl.endswith('/'):
101 101 staticurl += '/'
102 102
103 103 style = self.style
104 104 if style is None:
105 105 style = config('web', 'style', '')
106 106 if req.form.has_key('style'):
107 107 style = req.form['style'][0]
108 108 if self.stripecount is None:
109 109 self.stripecount = int(config('web', 'stripes', 1))
110 110 mapfile = style_map(templater.templatepath(), style)
111 111 tmpl = templater.templater(mapfile, templater.common_filters,
112 112 defaults={"header": header,
113 113 "footer": footer,
114 114 "motd": motd,
115 115 "url": url,
116 116 "staticurl": staticurl})
117 117
118 118 def archivelist(ui, nodeid, url):
119 119 allowed = ui.configlist("web", "allow_archive", untrusted=True)
120 120 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
121 121 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
122 122 untrusted=True):
123 123 yield {"type" : i[0], "extension": i[1],
124 124 "node": nodeid, "url": url}
125 125
126 126 def entries(sortcolumn="", descending=False, subdir="", **map):
127 127 def sessionvars(**map):
128 128 fields = []
129 129 if req.form.has_key('style'):
130 130 style = req.form['style'][0]
131 131 if style != get('web', 'style', ''):
132 132 fields.append(('style', style))
133 133
134 134 separator = url[-1] == '?' and ';' or '?'
135 135 for name, value in fields:
136 136 yield dict(name=name, value=value, separator=separator)
137 137 separator = ';'
138 138
139 139 rows = []
140 140 parity = paritygen(self.stripecount)
141 141 for name, path in self.repos:
142 142 if not name.startswith(subdir):
143 143 continue
144 144 name = name[len(subdir):]
145 145
146 146 u = ui.ui(parentui=parentui)
147 147 try:
148 148 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
149 149 except Exception, e:
150 150 u.warn(_('error reading %s/.hg/hgrc: %s\n' % (path, e)))
151 151 continue
152 152 def get(section, name, default=None):
153 153 return u.config(section, name, default, untrusted=True)
154 154
155 155 if u.configbool("web", "hidden", untrusted=True):
156 156 continue
157 157
158 158 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
159 159 .replace("//", "/")) + '/'
160 160
161 161 # update time with local timezone
162 162 try:
163 163 d = (get_mtime(path), util.makedate()[1])
164 164 except OSError:
165 165 continue
166 166
167 167 contact = (get("ui", "username") or # preferred
168 168 get("web", "contact") or # deprecated
169 169 get("web", "author", "")) # also
170 170 description = get("web", "description", "")
171 171 name = get("web", "name", name)
172 172 row = dict(contact=contact or "unknown",
173 173 contact_sort=contact.upper() or "unknown",
174 174 name=name,
175 175 name_sort=name,
176 176 url=url,
177 177 description=description or "unknown",
178 178 description_sort=description.upper() or "unknown",
179 179 lastchange=d,
180 180 lastchange_sort=d[1]-d[0],
181 181 sessionvars=sessionvars,
182 182 archives=archivelist(u, "tip", url))
183 183 if (not sortcolumn
184 184 or (sortcolumn, descending) == self.repos_sorted):
185 185 # fast path for unsorted output
186 186 row['parity'] = parity.next()
187 187 yield row
188 188 else:
189 189 rows.append((row["%s_sort" % sortcolumn], row))
190 190 if rows:
191 191 rows.sort()
192 192 if descending:
193 193 rows.reverse()
194 194 for key, row in rows:
195 195 row['parity'] = parity.next()
196 196 yield row
197 197
198 198 def makeindex(req, subdir=""):
199 199 sortable = ["name", "description", "contact", "lastchange"]
200 200 sortcolumn, descending = self.repos_sorted
201 201 if req.form.has_key('sort'):
202 202 sortcolumn = req.form['sort'][0]
203 203 descending = sortcolumn.startswith('-')
204 204 if descending:
205 205 sortcolumn = sortcolumn[1:]
206 206 if sortcolumn not in sortable:
207 207 sortcolumn = ""
208 208
209 209 sort = [("sort_%s" % column,
210 210 "%s%s" % ((not descending and column == sortcolumn)
211 211 and "-" or "", column))
212 212 for column in sortable]
213 213 req.write(tmpl("index", entries=entries, subdir=subdir,
214 214 sortcolumn=sortcolumn, descending=descending,
215 215 **dict(sort)))
216 216
217 217 try:
218 try:
218 219 virtual = req.env.get("PATH_INFO", "").strip('/')
219 220 if virtual.startswith('static/'):
220 221 static = os.path.join(templater.templatepath(), 'static')
221 222 fname = virtual[7:]
222 req.write(staticfile(static, fname, req) or
223 tmpl('error', error='%r not found' % fname))
223 req.write(staticfile(static, fname, req))
224 224 elif virtual:
225 225 repos = dict(self.repos)
226 226 while virtual:
227 227 real = repos.get(virtual)
228 228 if real:
229 229 req.env['REPO_NAME'] = virtual
230 230 try:
231 231 repo = hg.repository(parentui, real)
232 232 hgweb(repo).run_wsgi(req)
233 return
233 234 except IOError, inst:
234 req.write(tmpl("error", error=inst.strerror))
235 raise ErrorResponse(500, inst.strerror)
235 236 except hg.RepoError, inst:
236 req.write(tmpl("error", error=str(inst)))
237 return
237 raise ErrorResponse(500, str(inst))
238 238
239 239 # browse subdirectories
240 240 subdir = virtual + '/'
241 241 if [r for r in repos if r.startswith(subdir)]:
242 242 makeindex(req, subdir)
243 243 return
244 244
245 245 up = virtual.rfind('/')
246 246 if up < 0:
247 247 break
248 248 virtual = virtual[:up]
249 249
250 req.write(tmpl("notfound", repo=virtual))
250 req.respond(404, tmpl("notfound", repo=virtual))
251 251 else:
252 252 if req.form.has_key('static'):
253 253 static = os.path.join(templater.templatepath(), "static")
254 254 fname = req.form['static'][0]
255 req.write(staticfile(static, fname, req)
256 or tmpl("error", error="%r not found" % fname))
255 req.write(staticfile(static, fname, req))
257 256 else:
258 257 makeindex(req)
258 except ErrorResponse, err:
259 req.respond(err.code, tmpl('error', error=err.message or ''))
259 260 finally:
260 261 tmpl = None
@@ -1,86 +1,99
1 1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 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 socket, cgi, errno
10 10 from mercurial.i18n import gettext as _
11 from common import ErrorResponse
11 12
12 13 class wsgiapplication(object):
13 14 def __init__(self, destmaker):
14 15 self.destmaker = destmaker
15 16
16 17 def __call__(self, wsgienv, start_response):
17 18 return _wsgirequest(self.destmaker(), wsgienv, start_response)
18 19
19 20 class _wsgirequest(object):
20 21 def __init__(self, destination, wsgienv, start_response):
21 22 version = wsgienv['wsgi.version']
22 23 if (version < (1, 0)) or (version >= (2, 0)):
23 24 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
24 25 % version)
25 26 self.inp = wsgienv['wsgi.input']
26 27 self.server_write = None
27 28 self.err = wsgienv['wsgi.errors']
28 29 self.threaded = wsgienv['wsgi.multithread']
29 30 self.multiprocess = wsgienv['wsgi.multiprocess']
30 31 self.run_once = wsgienv['wsgi.run_once']
31 32 self.env = wsgienv
32 33 self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
33 34 self.start_response = start_response
34 35 self.headers = []
35 36 destination.run_wsgi(self)
36 37
37 38 out = property(lambda self: self)
38 39
39 40 def __iter__(self):
40 41 return iter([])
41 42
42 43 def read(self, count=-1):
43 44 return self.inp.read(count)
44 45
45 def write(self, *things):
46 def respond(self, status, *things):
46 47 for thing in things:
47 48 if hasattr(thing, "__iter__"):
48 49 for part in thing:
49 self.write(part)
50 self.respond(status, part)
50 51 else:
51 52 thing = str(thing)
52 53 if self.server_write is None:
53 54 if not self.headers:
54 55 raise RuntimeError("request.write called before headers sent (%s)." % thing)
55 self.server_write = self.start_response('200 Script output follows',
56 code = None
57 if isinstance(status, ErrorResponse):
58 code = status.code
59 elif isinstance(status, int):
60 code = status
61 if code:
62 from httplib import responses
63 status = '%d %s' % (
64 code, responses.get(code, 'Error'))
65 self.server_write = self.start_response(status,
56 66 self.headers)
57 67 self.start_response = None
58 self.headers = None
68 self.headers = []
59 69 try:
60 70 self.server_write(thing)
61 71 except socket.error, inst:
62 72 if inst[0] != errno.ECONNRESET:
63 73 raise
64 74
75 def write(self, *things):
76 self.respond('200 Script output follows', *things)
77
65 78 def writelines(self, lines):
66 79 for line in lines:
67 80 self.write(line)
68 81
69 82 def flush(self):
70 83 return None
71 84
72 85 def close(self):
73 86 return None
74 87
75 88 def header(self, headers=[('Content-type','text/html')]):
76 89 self.headers.extend(headers)
77 90
78 91 def httphdr(self, type, filename=None, length=0, headers={}):
79 92 headers = headers.items()
80 93 headers.append(('Content-type', type))
81 94 if filename:
82 95 headers.append(('Content-disposition', 'attachment; filename=%s' %
83 96 filename))
84 97 if length:
85 98 headers.append(('Content-length', str(length)))
86 99 self.header(headers)
@@ -1,16 +1,20
1 1 #!/usr/bin/env python
2 2
3 3 __doc__ = """This does HTTP get requests given a host:port and path and returns
4 4 a subset of the headers plus the body of the result."""
5 5
6 6 import httplib, sys
7 7 headers = [h.lower() for h in sys.argv[3:]]
8 8 conn = httplib.HTTPConnection(sys.argv[1])
9 9 conn.request("GET", sys.argv[2])
10 10 response = conn.getresponse()
11 11 print response.status, response.reason
12 12 for h in headers:
13 13 if response.getheader(h, None) is not None:
14 14 print "%s: %s" % (h, response.getheader(h))
15 15 print
16 16 sys.stdout.write(response.read())
17
18 if 200 <= response.status <= 299:
19 sys.exit(0)
20 sys.exit(1)
@@ -1,13 +1,31
1 1 #!/bin/sh
2 2
3 3 hg init test
4 4 cd test
5 5 mkdir da
6 6 echo foo > da/foo
7 7 echo foo > foo
8 8 hg ci -Ambase -d '0 0'
9 9 hg serve -p $HGPORT -d --pid-file=hg.pid
10 cat hg.pid >> $DAEMON_PIDS
10 11 echo % manifest
11 12 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/?style=raw')
12 13 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/da?style=raw')
13 kill `cat hg.pid`
14
15 echo % plain file
16 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?style=raw'
17
18 echo % should give a 404 - static file that does not exist
19 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/bogus'
20
21 echo % should give a 404 - bad revision
22 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/spam/foo?style=raw'
23
24 echo % should give a 400 - bad command
25 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?cmd=spam&style=raw'
26
27 echo % should give a 404 - file does not exist
28 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/bork?style=raw'
29
30 echo % static file
31 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/style-gitweb.css'
@@ -1,16 +1,136
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 % plain file
18 200 Script output follows
19
20 foo
21 % should give a 404 - static file that does not exist
22 404 Not Found
23
24 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
25 <html>
26 <head>
27 <link rel="icon" href="/static/hgicon.png" type="image/png">
28 <meta name="robots" content="index, nofollow" />
29 <link rel="stylesheet" href="/static/style.css" type="text/css" />
30
31 <title>Mercurial Error</title>
32 </head>
33 <body>
34
35 <h2>Mercurial Error</h2>
36
37 <p>
38 An error occured while processing your request:
39 </p>
40 <p>
41 Not Found
42 </p>
43
44
45 <div class="logo">
46 powered by<br/>
47 <a href="http://www.selenic.com/mercurial/">mercurial</a>
48 </div>
49
50 </body>
51 </html>
52
53 % should give a 404 - bad revision
54 404 Not Found
55
56
57 error: revision not found: spam
58 % should give a 400 - bad command
59 400 Bad Request
60
61
62 error: No such method: spam
63 % should give a 404 - file does not exist
64 404 Not Found
65
66
67 error: Path not found: bork/
68 % static file
69 200 Script output follows
70
71 body { font-family: sans-serif; font-size: 12px; margin:0px; border:solid #d9d8d1; border-width:1px; margin:10px; }
72 a { color:#0000cc; }
73 a:hover, a:visited, a:active { color:#880000; }
74 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
75 div.page_header a:visited { color:#0000cc; }
76 div.page_header a:hover { color:#880000; }
77 div.page_nav { padding:8px; }
78 div.page_nav a:visited { color:#0000cc; }
79 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
80 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
81 div.page_footer_text { float:left; color:#555555; font-style:italic; }
82 div.page_body { padding:8px; }
83 div.title, a.title {
84 display:block; padding:6px 8px;
85 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
86 }
87 a.title:hover { background-color: #d9d8d1; }
88 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
89 div.log_body { padding:8px 8px 8px 150px; }
90 .age { white-space:nowrap; }
91 span.age { position:relative; float:left; width:142px; font-style:italic; }
92 div.log_link {
93 padding:0px 8px;
94 font-size:10px; font-family:sans-serif; font-style:normal;
95 position:relative; float:left; width:136px;
96 }
97 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
98 a.list { text-decoration:none; color:#000000; }
99 a.list:hover { text-decoration:underline; color:#880000; }
100 table { padding:8px 4px; }
101 th { padding:2px 5px; font-size:12px; text-align:left; }
102 tr.light:hover, .parity0:hover { background-color:#edece6; }
103 tr.dark, .parity1 { background-color:#f6f6f0; }
104 tr.dark:hover, .parity1:hover { background-color:#edece6; }
105 td { padding:2px 5px; font-size:12px; vertical-align:top; }
106 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
107 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
108 div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
109 div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
110 div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
111 .linenr { color:#999999; text-decoration:none }
112 a.rss_logo {
113 float:right; padding:3px 0px; width:35px; line-height:10px;
114 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
115 color:#ffffff; background-color:#ff6600;
116 font-weight:bold; font-family:sans-serif; font-size:10px;
117 text-align:center; text-decoration:none;
118 }
119 a.rss_logo:hover { background-color:#ee5500; }
120 pre { margin: 0; }
121 span.logtags span {
122 padding: 0px 4px;
123 font-size: 10px;
124 font-weight: normal;
125 border: 1px solid;
126 background-color: #ffaaff;
127 border-color: #ffccff #ff00ee #ff00ee #ffccff;
128 }
129 span.logtags span.tagtag {
130 background-color: #ffffaa;
131 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
132 }
133 span.logtags span.branchtag {
134 background-color: #aaffaa;
135 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
136 }
General Comments 0
You need to be logged in to leave comments. Login now